Merge remote-tracking branch 'origin/main' into feat/gitlab-issues-mrs

# Conflicts:
#	packages/ui/src/components/chat/work-status/WorkStatusContextSection.tsx
#	packages/ui/src/components/chat/work-status/WorkStatusPrimaryGroup.tsx
This commit is contained in:
2026-08-20 14:47:33 +00:00
164 changed files with 4467 additions and 1731 deletions
+18 -179
View File
@@ -33,15 +33,6 @@ type SkillFrontmatter = {
[key: string]: unknown;
};
type ClawdHubSkillMetadata = {
slug: string;
version: string;
displayName?: string;
owner?: string;
downloads?: number;
stars?: number;
};
type SkillsCatalogItem = {
repoSource: string;
repoSubpath?: string;
@@ -51,9 +42,7 @@ type SkillsCatalogItem = {
description?: string;
installable: boolean;
warnings?: string[];
clawdhub?: ClawdHubSkillMetadata;
};
type SkillsCatalogItemWithBadge = SkillsCatalogItem & {
sourceId: string;
installed: { isInstalled: boolean; scope?: SkillScope; source?: SkillInstallSource };
@@ -84,143 +73,27 @@ const CURATED_SOURCES: CuratedSource[] = [
defaultSubpath: 'skills',
},
{
id: 'clawdhub',
label: 'ClawHub',
description: 'Community skill registry with vector search',
source: 'clawdhub:registry',
id: 'openai',
label: 'OpenAI',
description: "OpenAI's curated skills",
source: 'openai/skills',
defaultSubpath: 'skills/.curated',
},
{
id: 'cursor',
label: 'Cursor',
description: "Cursor's plugin skills",
source: 'cursor/plugins',
defaultSubpath: 'pstack/skills',
},
{
id: 'mattpocock',
label: 'Matt Pocock',
description: 'Matt Pocock skills collection',
source: 'mattpocock/skills',
},
];
// ============== ClawdHub API ==============
const CLAWDHUB_API_BASE = 'https://clawdhub.com/api/v1';
const CLAWDHUB_PAGE_LIMIT = 25;
const CLAWDHUB_RATE_LIMIT_MS = 100;
let clawdhubLastRequest = 0;
function isClawdHubSource(source: string): boolean {
return typeof source === 'string' && source.startsWith('clawdhub:');
}
async function clawdhubFetch(url: string, options?: RequestInit): Promise<Response> {
const maxAttempts = 10;
let lastResponse: Response | null = null;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const now = Date.now();
const elapsed = now - clawdhubLastRequest;
if (elapsed < CLAWDHUB_RATE_LIMIT_MS) {
await new Promise((resolve) => setTimeout(resolve, CLAWDHUB_RATE_LIMIT_MS - elapsed));
}
clawdhubLastRequest = Date.now();
const response = await fetch(url, {
...options,
headers: {
Accept: 'application/json',
'User-Agent': 'OpenChamber-VSCode/1.0',
...options?.headers,
},
});
lastResponse = response;
if (response.status === 429 || response.status >= 500) {
if (attempt < maxAttempts - 1) {
const waitMs = 50 * (attempt + 1);
await new Promise((resolve) => setTimeout(resolve, waitMs));
continue;
}
}
return response;
}
return lastResponse as Response;
}
type ClawdHubSkillListItem = {
slug: string;
displayName?: string;
summary?: string;
tags?: { latest?: string };
latestVersion?: { version?: string };
stats?: { downloads?: number; stars?: number };
owner?: { handle?: string };
};
type ClawdHubSkillsResponse = {
items: ClawdHubSkillListItem[];
nextCursor?: string;
};
async function scanClawdHub(): Promise<SkillsRepoScanResult> {
try {
const allItems: SkillsCatalogItem[] = [];
let cursor: string | null = null;
const maxPages = 20;
for (let page = 0; page < maxPages; page++) {
const url = cursor
? `${CLAWDHUB_API_BASE}/skills?cursor=${encodeURIComponent(cursor)}&limit=${CLAWDHUB_PAGE_LIMIT}`
: `${CLAWDHUB_API_BASE}/skills?limit=${CLAWDHUB_PAGE_LIMIT}`;
let data: ClawdHubSkillsResponse;
try {
const response = await clawdhubFetch(url);
if (!response.ok) {
throw new Error(`ClawdHub API error: ${response.status}`);
}
data = (await response.json()) as ClawdHubSkillsResponse;
} catch (error) {
if (page > 0 && allItems.length > 0) {
break;
}
throw error;
}
for (const item of data.items || []) {
const latestVersion = item.tags?.latest || item.latestVersion?.version || '1.0.0';
allItems.push({
repoSource: 'clawdhub:registry',
skillDir: item.slug,
skillName: item.slug,
frontmatterName: item.displayName || item.slug,
description: item.summary || undefined,
installable: true,
clawdhub: {
slug: item.slug,
version: latestVersion,
displayName: item.displayName,
owner: item.owner?.handle,
downloads: item.stats?.downloads || 0,
stars: item.stats?.stars || 0,
},
});
}
if (!data.nextCursor) break;
cursor = data.nextCursor;
}
// Sort by downloads (most popular first)
allItems.sort((a, b) => (b.clawdhub?.downloads || 0) - (a.clawdhub?.downloads || 0));
return { ok: true, items: allItems };
} catch (error) {
return {
ok: false,
error: {
kind: 'networkError',
message: error instanceof Error ? error.message : 'Failed to fetch skills from ClawdHub',
},
};
}
}
function validateSkillName(skillName: string): boolean {
if (skillName.length < 1 || skillName.length > 64) return false;
return SKILL_NAME_PATTERN.test(skillName);
@@ -716,40 +589,6 @@ export async function getSkillsCatalog(
const itemsBySource: Record<string, SkillsCatalogItemWithBadge[]> = {};
for (const src of sources) {
// Handle ClawdHub sources separately (API-based, not git-based)
if (isClawdHubSource(src.source)) {
const cacheKey = 'clawdhub:registry';
let cached = !refresh ? catalogCache.get(cacheKey) : null;
if (cached && Date.now() >= cached.expiresAt) {
catalogCache.delete(cacheKey);
cached = null;
}
let items: SkillsCatalogItem[] = [];
if (cached) {
items = cached.items;
} else {
const scanned = await scanClawdHub();
if (!scanned.ok) {
itemsBySource[src.id] = [];
continue;
}
items = scanned.items || [];
catalogCache.set(cacheKey, { expiresAt: Date.now() + CATALOG_TTL_MS, items });
}
itemsBySource[src.id] = items.map((item) => {
const installed = installedByName.get(item.skillName);
return {
sourceId: src.id,
...item,
installed: installed ? { isInstalled: true, scope: installed.scope, source: installed.source === 'agents' ? 'agents' : 'opencode' } : { isInstalled: false },
};
});
continue;
}
// Handle GitHub sources (git clone based)
const parsed = parseSkillRepoSource(src.source);
if (!parsed.ok) {
itemsBySource[src.id] = [];