Refactor application architecture and shared functionality

This commit is contained in:
Jakub Syty
2026-08-21 10:59:47 +02:00
398 changed files with 28819 additions and 4361 deletions
+6
View File
@@ -40,11 +40,16 @@ Keep `bridge.ts` as a thin orchestration layer that delegates message handling t
- active-directory selection across multi-root workspaces
- dropped-file parsing and attachment reading
- models metadata fetch helper
- Read paths are authorized in the requested workspace path space before symlink resolution, matching the web runtime; directly requested outside-workspace paths remain denied.
The webview CSP permits `blob:` only for `worker-src` so shared UI parsers can run bounded local decompression off the main thread. Blob scripts remain disallowed by `script-src`.
- `bridge-localfs-proxy-runtime.ts`
- Local `/api/fs/read` and `/api/fs/raw` proxy helpers and shared proxy utility helpers.
- Workspace-contained Markdown gallery images use these local filesystem
routes without calling the server grant route. Grant requests for OpenCode
temporary-directory images return an explicit unsupported response instead
of being forwarded to OpenCode.
- `bridge-proxy-runtime.ts`
- Proxy route handlers (`api:proxy`, `api:session:message`) with injected helper dependencies.
@@ -54,6 +59,7 @@ The webview CSP permits `blob:` only for `worker-src` so shared UI parsers can r
- `bridge-config-runtime.ts`
- Config and skills message handlers (`api:config/*`).
- Includes OpenCode resolution diagnostics parity handler used by shared UI (`/api/config/opencode-resolution`).
- OpenCode JSONC reads in `opencodeConfig.ts` fail closed on a partial or non-object `jsonc-parser` tree (`INVALID_JSONC`) so mutations cannot rewrite a `$schema`-only stub over an existing config. Comment-only files read as empty, while other content that yields no JSON value (YAML, plain text) fails closed. A broken layer is omitted from the merge and recorded on `layerErrors`; valid sibling layers still load, including plugin list/read via `getPluginConfigSources`. Writes still refuse to overwrite the broken file.
- `bridge-settings-runtime.ts`
- Settings read/write and OpenCode skills discovery via API for bridge consumers.
@@ -583,12 +583,8 @@ export const resolveFileReadPath = async (targetPath: string, requestedRoot?: st
}
try {
const [canonicalPath, canonicalBase] = await Promise.all([
fs.promises.realpath(resolved),
fs.promises.realpath(baseRoot).catch(() => path.resolve(baseRoot)),
]);
if (!isPathInside(canonicalPath, canonicalBase)) {
const canonicalPath = await fs.promises.realpath(resolved);
if (!isPathInside(resolved, path.resolve(baseRoot))) {
return { ok: false, status: 403, error: 'Access to file denied' };
}
@@ -37,6 +37,14 @@ mock.module('vscode', () => ({
const { tryHandleLocalFsProxy } = await import('./bridge-localfs-proxy-runtime');
describe('bridge local fs proxy', () => {
it('does not forward server-owned Markdown image grant routes to OpenCode', async () => {
const response = await tryHandleLocalFsProxy('POST', '/api/openchamber/sessions/ses_1/markdown-image-grants');
expect(response?.status).toBe(501);
expect(Buffer.from(response?.bodyBase64 ?? '', 'base64').toString('utf8'))
.toContain('not supported in the VS Code runtime');
});
it('returns a quiet optional stat miss for missing files', async () => {
const response = await tryHandleLocalFsProxy('GET', '/api/fs/stat?path=%2Fmissing.ts&optional=true');
@@ -56,6 +56,9 @@ export const tryHandleLocalFsProxy = async (method: string, requestPath: string)
}
const fsProxyPath = normalizeFsProxyPath(parsed.pathname);
if (/^\/api\/openchamber\/sessions\/[^/]+\/markdown-image-grants$/.test(parsed.pathname)) {
return buildProxyJsonError(501, 'Markdown image grants are not supported in the VS Code runtime');
}
if (!fsProxyPath) {
return null;
}
+66
View File
@@ -0,0 +1,66 @@
type CommandCodeCredits = {
credits?: { monthlyCredits?: number; purchasedCredits?: number; freeCredits?: number };
windowLimits?: {
fiveHour?: { used?: number; cap?: number; resetAt?: number };
weekly?: { used?: number; cap?: number; resetAt?: number };
};
};
type WindowData = { usedPercent: number | null; resetAt: number | null; windowSeconds: number | null; valueLabel: string };
const toWindow = (data: WindowData) => ({
usedPercent: data.usedPercent,
remainingPercent: data.usedPercent === null ? null : Math.max(0, 100 - data.usedPercent),
windowSeconds: data.windowSeconds,
resetAfterSeconds: data.resetAt === null ? null : Math.max(0, Math.floor((data.resetAt - Date.now()) / 1000)),
resetAt: data.resetAt,
resetAtFormatted: null,
resetAfterFormatted: null,
valueLabel: data.valueLabel,
});
const isFiniteNumber = (value: unknown): value is number => typeof value === 'number' && Number.isFinite(value);
const formatCredits = (value: number): string => String(Math.round((value + Number.EPSILON) * 100) / 100);
const parseCredits = (value: unknown): CommandCodeCredits | null => {
if (!value || typeof value !== 'object') return null;
const payload = value as CommandCodeCredits;
return payload;
};
const parseOrgId = (value: unknown): string | null | undefined => {
if (!value || typeof value !== 'object') return undefined;
const org = (value as { org?: { id?: unknown } }).org;
return typeof org?.id === 'string' && org.id.trim() ? org.id.trim() : null;
};
const parseCommandCodeCredits = (payload: CommandCodeCredits) => {
const windows: Record<string, ReturnType<typeof toWindow>> = {};
for (const [label, value] of [['monthly_credits', payload.credits?.monthlyCredits], ['purchased_credits', payload.credits?.purchasedCredits], ['free_credits', payload.credits?.freeCredits]] as const) {
if (isFiniteNumber(value)) windows[label] = toWindow({ usedPercent: null, resetAt: null, windowSeconds: null, valueLabel: formatCredits(value) });
}
for (const [label, limit, seconds] of [['5h', payload.windowLimits?.fiveHour, 5 * 60 * 60], ['weekly', payload.windowLimits?.weekly, 7 * 24 * 60 * 60]] as const) {
if (!isFiniteNumber(limit?.used) || !isFiniteNumber(limit.cap) || limit.cap <= 0) continue;
const resetAt = isFiniteNumber(limit.resetAt) ? (limit.resetAt < 1_000_000_000_000 ? limit.resetAt * 1000 : limit.resetAt) : null;
windows[label] = toWindow({ usedPercent: Math.min(100, Math.max(0, limit.used / limit.cap * 100)), resetAt, windowSeconds: seconds, valueLabel: `${formatCredits(limit.used)} / ${formatCredits(limit.cap)}` });
}
return windows;
};
const requestJson = async (path: string, apiKey: string): Promise<unknown> => {
const response = await fetch(`https://api.commandcode.ai${path}`, { headers: { Accept: 'application/json', Authorization: `Bearer ${apiKey}` }, signal: AbortSignal.timeout(15_000) });
if (response.status === 401 || response.status === 403) throw new Error('Command Code authentication failed');
if (!response.ok) throw new Error(`Command Code usage API returned HTTP ${response.status}`);
return response.json().catch(() => null);
};
export const fetchCommandCodeUsage = async (apiKey: string) => {
const orgId = parseOrgId(await requestJson('/alpha/whoami', apiKey));
if (orgId === undefined) throw new Error('Command Code account could not be determined');
const creditsPath = orgId ? `/alpha/billing/credits?orgId=${encodeURIComponent(orgId)}` : '/alpha/billing/credits';
const payload = parseCredits(await requestJson(creditsPath, apiKey));
if (!payload) throw new Error('Command Code usage data could not be parsed');
const windows = parseCommandCodeCredits(payload);
if (!Object.keys(windows).length) throw new Error('Command Code usage data could not be parsed');
return windows;
};
@@ -0,0 +1,149 @@
import { afterEach, beforeEach, describe, test } from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { listPluginEntries, updateMcpConfig } from './opencodeConfig';
const PARTIAL_PARSE_CONFIG = [
'{',
' "$schema": "https://opencode.ai/config.json",',
' plugin: ["opencode-see-image"],',
' mcp: {',
' openproject: {',
' type: "remote",',
' url: "https://openproject.example.com/mcp",',
' enabled: true',
' }',
' },',
' provider: {',
' "ollama-cloud": {',
' npm: "@ai-sdk/openai-compatible",',
' name: "Ollama Cloud"',
' }',
' }',
'}',
'',
].join('\n');
const VALID_CONFIG = [
'{',
' "$schema": "https://opencode.ai/config.json",',
' "plugin": ["opencode-see-image"],',
' "mcp": {',
' "openproject": {',
' "type": "remote",',
' "url": "https://openproject.example.com/mcp",',
' "enabled": true',
' }',
' },',
' "provider": {',
' "ollama-cloud": {',
' "npm": "@ai-sdk/openai-compatible",',
' "name": "Ollama Cloud"',
' }',
' }',
'}',
'',
].join('\n');
const isInvalidJsoncError = (error: unknown): boolean => {
if (!(error instanceof Error) || !/cannot be loaded safely/.test(error.message)) {
return false;
}
// SAFETY: the config layer throws Error instances carrying the coded `code` field.
return (error as Error & { code?: string }).code === 'INVALID_JSONC';
};
describe('opencodeConfig JSONC parse safety (issue #2923)', () => {
let tempDir: string;
let previousOpenCodeConfig: string | undefined;
beforeEach(() => {
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'openchamber-vscode-config-parse-'));
previousOpenCodeConfig = process.env.OPENCODE_CONFIG;
});
afterEach(() => {
if (previousOpenCodeConfig === undefined) delete process.env.OPENCODE_CONFIG;
else process.env.OPENCODE_CONFIG = previousOpenCodeConfig;
fs.rmSync(tempDir, { recursive: true, force: true });
});
test('refuses MCP updates that would overwrite a partial-parse config', () => {
const configPath = path.join(tempDir, 'opencode.jsonc');
fs.writeFileSync(configPath, PARTIAL_PARSE_CONFIG, 'utf8');
process.env.OPENCODE_CONFIG = configPath;
assert.throws(() => updateMcpConfig('openproject', { enabled: true }), isInvalidJsoncError);
assert.equal(fs.readFileSync(configPath, 'utf8'), PARTIAL_PARSE_CONFIG);
assert.equal(fs.existsSync(`${configPath}.openchamber.backup`), false);
});
test('preserves unrelated keys when updating a valid MCP config', () => {
const configPath = path.join(tempDir, 'opencode.jsonc');
fs.writeFileSync(configPath, VALID_CONFIG, 'utf8');
process.env.OPENCODE_CONFIG = configPath;
updateMcpConfig('openproject', { enabled: false });
const rewritten = JSON.parse(fs.readFileSync(configPath, 'utf8'));
assert.deepEqual(rewritten.plugin, ['opencode-see-image']);
assert.equal(rewritten.provider['ollama-cloud'].name, 'Ollama Cloud');
assert.equal(rewritten.mcp.openproject.enabled, false);
assert.equal(fs.readFileSync(`${configPath}.openchamber.backup`, 'utf8'), VALID_CONFIG);
});
test('returns an empty object for a comment-only config file', () => {
const configPath = path.join(tempDir, 'comments.jsonc');
fs.writeFileSync(configPath, '// placeholder\n/* still empty */\n', 'utf8');
process.env.OPENCODE_CONFIG = configPath;
assert.deepEqual(listPluginEntries(), []);
});
test('refuses MCP updates against content that yields no JSON value at all', () => {
const configPath = path.join(tempDir, 'yamlish.jsonc');
const contents = 'mcp:\n openproject:\n type: remote\n';
fs.writeFileSync(configPath, contents, 'utf8');
process.env.OPENCODE_CONFIG = configPath;
assert.throws(() => updateMcpConfig('openproject', { enabled: true }), isInvalidJsoncError);
assert.equal(fs.readFileSync(configPath, 'utf8'), contents);
assert.equal(fs.existsSync(`${configPath}.openchamber.backup`), false);
});
test('lists custom-layer plugins when a project layer is unparseable', () => {
const customPath = path.join(tempDir, 'custom.jsonc');
const projectDir = path.join(tempDir, 'project');
const projectFile = path.join(projectDir, '.opencode', 'opencode.jsonc');
fs.writeFileSync(customPath, VALID_CONFIG, 'utf8');
fs.mkdirSync(path.dirname(projectFile), { recursive: true });
fs.writeFileSync(projectFile, PARTIAL_PARSE_CONFIG, 'utf8');
process.env.OPENCODE_CONFIG = customPath;
const specs = listPluginEntries(projectDir).map((entry) => entry.spec);
assert.deepEqual(specs, ['opencode-see-image']);
assert.equal(fs.readFileSync(projectFile, 'utf8'), PARTIAL_PARSE_CONFIG);
assert.equal(fs.existsSync(`${projectFile}.openchamber.backup`), false);
});
test('keeps a valid custom layer writable when a project layer is unparseable', () => {
const customPath = path.join(tempDir, 'custom.jsonc');
const projectDir = path.join(tempDir, 'project');
const projectFile = path.join(projectDir, '.opencode', 'opencode.jsonc');
fs.writeFileSync(customPath, VALID_CONFIG, 'utf8');
fs.mkdirSync(path.dirname(projectFile), { recursive: true });
fs.writeFileSync(projectFile, PARTIAL_PARSE_CONFIG, 'utf8');
process.env.OPENCODE_CONFIG = customPath;
updateMcpConfig('openproject', { enabled: false }, projectDir);
const rewritten = JSON.parse(fs.readFileSync(customPath, 'utf8'));
assert.deepEqual(rewritten.plugin, ['opencode-see-image']);
assert.equal(rewritten.mcp.openproject.enabled, false);
assert.equal(fs.readFileSync(projectFile, 'utf8'), PARTIAL_PARSE_CONFIG);
assert.equal(fs.existsSync(`${projectFile}.openchamber.backup`), false);
});
});
+114 -19
View File
@@ -2,7 +2,7 @@ import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import yaml from 'yaml';
import { parse as parseJsonc } from 'jsonc-parser';
import { parse as parseJsonc, printParseErrorCode, type ParseError } from 'jsonc-parser';
const OPENCODE_CONFIG_DIR = path.join(os.homedir(), '.config', 'opencode');
const AGENT_DIR = path.join(OPENCODE_CONFIG_DIR, 'agents');
@@ -554,12 +554,46 @@ const getPrimaryUserConfigPath = (userPaths: string[]): string => {
return CONFIG_FILE;
};
const INVALID_JSONC = 'INVALID_JSONC';
const formatJsoncParseError = (filePath: string, errors: ParseError[]): string => {
const first = errors.length > 0 ? errors[0] : null;
const location = first && Number.isFinite(first.offset)
? ` (${printParseErrorCode(first.error)} at offset ${first.offset})`
: '';
return `OpenCode configuration at ${filePath} contains invalid JSONC and cannot be loaded safely${location}`;
};
const isInvalidJsoncError = (error: unknown): error is Error & { code: string } =>
Boolean(error && typeof error === 'object' && 'code' in error && error.code === INVALID_JSONC);
// Comment-only / whitespace-only files parse to undefined with nothing but
// ValueExpected. Any other error means real content we failed to understand
// (YAML, plain text, a stray leading token), which must not read as empty.
const isCommentOnlyParse = (parsed: unknown, errors: ParseError[]): boolean =>
parsed === undefined
&& errors.every((entry) => printParseErrorCode(entry.error) === 'ValueExpected');
const parseConfigObject = (content: string, filePath: string): Record<string, unknown> => {
const errors: ParseError[] = [];
const parsed = parseJsonc(content, errors, { allowTrailingComma: true });
if (isCommentOnlyParse(parsed, errors)) {
return {};
}
if (errors.length > 0 || !parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
throw codedError(formatJsoncParseError(filePath, errors), INVALID_JSONC);
}
return parsed as Record<string, unknown>;
};
const readConfigFile = (filePath?: string | null): Record<string, unknown> => {
if (!filePath || !fs.existsSync(filePath)) return {};
const content = fs.readFileSync(filePath, 'utf8');
const normalized = content.trim();
if (!normalized) return {};
return parseJsonc(normalized, [], { allowTrailingComma: true }) as Record<string, unknown>;
// Refuse partial jsonc-parser trees. Ignoring errors previously let mutations
// rewrite a truncated object (often only `$schema`) over the full config.
return parseConfigObject(normalized, filePath);
};
const isPlainObject = (value: unknown): value is Record<string, unknown> =>
@@ -582,20 +616,50 @@ const mergeConfigs = (base: Record<string, unknown>, override: Record<string, un
return result;
};
const readConfigLayer = (filePath?: string | null): {
config: Record<string, unknown>;
error: (Error & { code: string }) | null;
} => {
try {
return { config: readConfigFile(filePath), error: null };
} catch (error) {
if (isInvalidJsoncError(error)) {
console.error(error.message);
return { config: {}, error };
}
throw error;
}
};
const readConfigLayers = (workingDirectory?: string) => {
const { userPaths, projectPath, customPath } = getConfigPaths(workingDirectory);
const userPath = getPrimaryUserConfigPath(userPaths);
const userConfig = readConfigFile(userPath);
const projectConfig = readConfigFile(projectPath);
const customConfig = readConfigFile(customPath);
const mergedConfig = mergeConfigs(mergeConfigs(userConfig, projectConfig), customConfig);
const userLayer = readConfigLayer(userPath);
const projectLayer = readConfigLayer(projectPath);
const customLayer = readConfigLayer(customPath);
const mergedConfig = mergeConfigs(
mergeConfigs(userLayer.config, projectLayer.config),
customLayer.config,
);
const layerErrors: Array<{ path: string; code: string; message: string }> = [];
if (userLayer.error) {
layerErrors.push({ path: userPath, code: userLayer.error.code, message: userLayer.error.message });
}
if (projectLayer.error && projectPath) {
layerErrors.push({ path: projectPath, code: projectLayer.error.code, message: projectLayer.error.message });
}
if (customLayer.error && customPath) {
layerErrors.push({ path: customPath, code: customLayer.error.code, message: customLayer.error.message });
}
return {
userConfig,
projectConfig,
customConfig,
userConfig: userLayer.config,
projectConfig: projectLayer.config,
customConfig: customLayer.config,
mergedConfig,
paths: { userPath, projectPath, customPath }
paths: { userPath, projectPath, customPath },
layerErrors,
};
};
@@ -695,6 +759,11 @@ const getConfigForPath = (layers: ReturnType<typeof readConfigLayers>, targetPat
const writeConfig = (config: Record<string, unknown>, filePath: string = CONFIG_FILE) => {
if (fs.existsSync(filePath)) {
// Defense in depth: never overwrite a file we cannot fully parse.
const existing = fs.readFileSync(filePath, 'utf8').trim();
if (existing) {
parseConfigObject(existing, filePath);
}
const backupFile = `${filePath}.openchamber.backup`;
try {
fs.copyFileSync(filePath, backupFile);
@@ -862,10 +931,10 @@ const getPluginConfigSources = (workingDirectory?: string | null): Array<{ scope
const projectPath = getProjectConfigPath(workingDirectory || undefined);
return [
customPath
? { scope: 'user', path: customPath, config: readConfigFile(customPath) }
: { scope: 'user', path: userPath, config: readConfigFile(userPath) },
? { scope: 'user', path: customPath, config: readConfigLayer(customPath).config }
: { scope: 'user', path: userPath, config: readConfigLayer(userPath).config },
...(projectPath
? [{ scope: 'project' as const, path: projectPath, config: readConfigFile(projectPath) }]
? [{ scope: 'project' as const, path: projectPath, config: readConfigLayer(projectPath).config }]
: []),
];
};
@@ -1376,22 +1445,45 @@ export const deleteMcpConfig = (name: string, workingDirectory?: string): void =
writeConfig(config, targetPath);
};
const getLayerError = (
layers: ReturnType<typeof readConfigLayers>,
filePath?: string | null,
) => {
if (!filePath) return null;
return layers.layerErrors.find((entry) => entry.path === filePath) || null;
};
const throwIfLayerError = (
layers: ReturnType<typeof readConfigLayers>,
filePath?: string | null,
) => {
const failed = getLayerError(layers, filePath);
if (!failed) return;
throw codedError(failed.message, failed.code);
};
const getJsonEntrySource = (
layers: ReturnType<typeof readConfigLayers>,
sectionKey: 'agent' | 'command' | 'mcp',
entryName: string
) => {
const { userConfig, projectConfig, customConfig, paths } = layers;
const customSection = (customConfig as Record<string, unknown>)?.[sectionKey] as Record<string, unknown> | undefined;
if (customSection?.[entryName] !== undefined) {
return { section: customSection[entryName], config: customConfig, path: paths.customPath, exists: true };
if (paths.customPath) {
throwIfLayerError(layers, paths.customPath);
const customSection = (customConfig as Record<string, unknown>)?.[sectionKey] as Record<string, unknown> | undefined;
if (customSection?.[entryName] !== undefined) {
return { section: customSection[entryName], config: customConfig, path: paths.customPath, exists: true };
}
}
const projectSection = (projectConfig as Record<string, unknown>)?.[sectionKey] as Record<string, unknown> | undefined;
if (projectSection?.[entryName] !== undefined) {
return { section: projectSection[entryName], config: projectConfig, path: paths.projectPath, exists: true };
if (paths.projectPath && !getLayerError(layers, paths.projectPath)) {
const projectSection = (projectConfig as Record<string, unknown>)?.[sectionKey] as Record<string, unknown> | undefined;
if (projectSection?.[entryName] !== undefined) {
return { section: projectSection[entryName], config: projectConfig, path: paths.projectPath, exists: true };
}
}
throwIfLayerError(layers, paths.userPath);
const userSection = (userConfig as Record<string, unknown>)?.[sectionKey] as Record<string, unknown> | undefined;
if (userSection?.[entryName] !== undefined) {
return { section: userSection[entryName], config: userConfig, path: paths.userPath, exists: true };
@@ -1406,11 +1498,14 @@ const getJsonWriteTarget = (
) => {
const { userConfig, projectConfig, customConfig, paths } = layers;
if (paths.customPath) {
throwIfLayerError(layers, paths.customPath);
return { config: customConfig, path: paths.customPath };
}
if (preferredScope === AGENT_SCOPE.PROJECT && paths.projectPath) {
throwIfLayerError(layers, paths.projectPath);
return { config: projectConfig, path: paths.projectPath };
}
throwIfLayerError(layers, paths.userPath);
return { config: userConfig, path: paths.userPath };
};
+156
View File
@@ -17,9 +17,11 @@ const AUTH = JSON.stringify({
crof: { key: 'test-token' },
neuralwatt: { key: 'test-token' },
'opencode-go': { key: 'test-token' },
'command-code': { type: 'oauth', access: 'test-token' },
'zai-coding-plan': { key: 'test-token' },
deepseek: { key: 'test-token' },
'github-copilot': { access: 'test-token' },
anthropic: { access: 'test-token', refresh: 'test-refresh' },
});
((fs as unknown) as { existsSync: () => boolean }).existsSync = () => true;
((fs as unknown) as { readFileSync: () => string }).readFileSync = () => AUTH;
@@ -103,6 +105,58 @@ describe('OpenCode Go quota provider (VS Code parity)', () => {
});
});
describe('Command Code quota provider (VS Code parity)', () => {
test('uses the OAuth access token and resolves server-backed limits', async () => {
const requests: Array<{ url: string; init?: RequestInit }> = [];
globalThis.fetch = (async (url: string, init?: RequestInit) => {
requests.push({ url, init });
return mockResponse(url.endsWith('/alpha/whoami')
? { org: { id: 'org/a' } }
: { credits: { monthlyCredits: 120 }, windowLimits: { fiveHour: { used: 25, cap: 100, resetAt: 1_776_000_000 } } });
}) as typeof fetch;
const result = await fetchQuotaForProvider('command-code');
assert.equal(result.ok, true);
assert.deepEqual(requests.map(({ url }) => url), [
'https://api.commandcode.ai/alpha/whoami',
'https://api.commandcode.ai/alpha/billing/credits?orgId=org%2Fa',
]);
assert.equal((requests[0].init?.headers as Record<string, string>).Authorization, 'Bearer test-token');
assert.equal(result.usage!.windows['5h']!.usedPercent, 25);
assert.equal(result.usage!.windows.monthly_credits!.valueLabel, '120');
});
test('omits orgId for personal accounts', async () => {
const urls: string[] = [];
globalThis.fetch = (async (url: string) => {
urls.push(url);
return mockResponse(url.endsWith('/alpha/whoami')
? { user: { id: 'user-1' }, org: null }
: { credits: { monthlyCredits: 120 } });
}) as typeof fetch;
const result = await fetchQuotaForProvider('command-code');
assert.equal(result.ok, true);
assert.deepEqual(urls, [
'https://api.commandcode.ai/alpha/whoami',
'https://api.commandcode.ai/alpha/billing/credits',
]);
});
test('formats fractional credit values for display', async () => {
globalThis.fetch = (async (url: string) => mockResponse(url.endsWith('/alpha/whoami')
? { org: null }
: { credits: { monthlyCredits: 69.7947070034 }, windowLimits: { fiveHour: { used: 0.2052929966, cap: 14 } } })) as typeof fetch;
const result = await fetchQuotaForProvider('command-code');
assert.equal(result.usage!.windows.monthly_credits!.valueLabel, '69.79');
assert.equal(result.usage!.windows['5h']!.valueLabel, '0.21 / 14');
});
});
describe('Crof quota provider (VS Code parity)', () => {
test('reports credits balance as valueLabel with null percent', async () => {
stubFetchReturning(() => Promise.resolve(mockResponse({ usable_requests: 450, credits: 12.3456 })));
@@ -150,6 +204,27 @@ describe('Crof quota provider (VS Code parity)', () => {
});
describe('Codex quota provider (VS Code parity)', () => {
test('coalesces concurrent refreshes for the same provider', async () => {
let resolveResponse: ((response: Response) => void) | undefined;
let requestCount = 0;
globalThis.fetch = (() => {
requestCount += 1;
return new Promise<Response>((resolve) => {
resolveResponse = resolve;
});
}) as typeof fetch;
const first = fetchQuotaForProvider('codex');
const second = fetchQuotaForProvider('codex');
resolveResponse?.(mockResponse({ rate_limit: null }));
const [firstResult, secondResult] = await Promise.all([first, second]);
assert.equal(firstResult.ok, true);
assert.equal(secondResult.ok, true);
assert.equal(requestCount, 1);
});
test('surfaces spend_control individual limit for business accounts', async () => {
stubFetchReturning(() => Promise.resolve(mockResponse({
plan_type: 'business',
@@ -194,6 +269,60 @@ describe('GitHub Copilot quota provider (VS Code parity)', () => {
});
});
describe('Claude quota provider (VS Code parity)', () => {
test('parses current limits, model-scoped limits, and extra usage', async () => {
stubFetchReturning(() => Promise.resolve(mockResponse({
limits: [
{ kind: 'session', percent: 12, resets_at: '2026-08-20T12:00:00Z', scope: null },
{ kind: 'weekly_all', percent: 34, resets_at: '2026-08-24T12:00:00Z', scope: null },
{ kind: 'weekly_scoped', percent: 56, resets_at: '2026-08-24T12:00:00Z', scope: { model: { display_name: 'Sonnet' } } },
],
spend: {
enabled: true,
percent: 25,
used: { amount_minor: 2500, exponent: 2, currency: 'USD' },
limit: { amount_minor: 10000, exponent: 2, currency: 'USD' },
},
})));
const result = await fetchQuotaForProvider('claude');
assert.equal(result.ok, true);
assert.equal(result.usage?.windows['5h']?.usedPercent, 12);
assert.equal(result.usage?.windows['7d']?.usedPercent, 34);
assert.equal(result.usage?.models?.Sonnet?.windows['7d']?.usedPercent, 56);
assert.equal(result.usage?.windows.extra_usage?.valueLabel, '$25.00 / $100.00');
});
test('keeps serving the last good values while Anthropic rate limits', async () => {
const responses = [
mockResponse({ five_hour: { utilization: 12, resets_at: '2026-08-20T12:00:00Z' } }),
{
ok: false,
status: 429,
headers: new Headers({ 'retry-after': '120' }),
json: async () => ({}),
} as Response,
];
let requestCount = 0;
globalThis.fetch = (async () => {
const response = responses[requestCount];
requestCount += 1;
return response;
}) as typeof fetch;
const initial = await fetchQuotaForProvider('claude');
const rateLimited = await fetchQuotaForProvider('claude');
const duringCooldown = await fetchQuotaForProvider('claude');
assert.equal(initial.ok, true);
assert.equal(rateLimited.ok, true);
assert.equal(duringCooldown.ok, true);
assert.equal(duringCooldown.usage?.windows['5h']?.usedPercent, 12);
assert.equal(requestCount, 2);
});
});
describe('Z.ai quota provider (VS Code parity)', () => {
test('surfaces 5-hour, weekly, and MCP quota windows', async () => {
stubFetchReturning(() => Promise.resolve(mockResponse({
@@ -219,6 +348,33 @@ describe('Z.ai quota provider (VS Code parity)', () => {
assert.equal(windows['MCP Tools']!.windowSeconds, 30 * 24 * 60 * 60);
assert.equal(windows['MCP Tools']!.resetAt, 1787128459979);
});
test('maps CREDIT_LIMIT entries to windows with credit value labels and plan level', async () => {
stubFetchReturning(() => Promise.resolve(mockResponse({
code: 200,
data: {
limits: [
{ type: 'CREDIT_LIMIT', unit: 3, number: 5, usage: 12000, currentValue: 65, remaining: 11934, percentage: 1, nextResetTime: 1787257978907 },
{ type: 'CREDIT_LIMIT', unit: 6, number: 1, usage: 60000, currentValue: 65, remaining: 59934, percentage: 1, nextResetTime: 1787844668997 },
],
level: 'pro',
},
})));
const result = await fetchQuotaForProvider('zai-coding-plan');
const windows = result.usage!.windows;
assert.equal(result.ok, true);
assert.equal(result.planLabel, 'pro');
assert.equal(windows['5h']!.usedPercent, 1);
assert.equal(windows['5h']!.windowSeconds, 5 * 60 * 60);
assert.equal(windows['5h']!.resetAt, 1787257978907);
assert.equal(windows['5h']!.valueLabel, '65 / 12k credits');
assert.equal(windows.weekly!.usedPercent, 1);
assert.equal(windows.weekly!.windowSeconds, 7 * 24 * 60 * 60);
assert.equal(windows.weekly!.resetAt, 1787844668997);
assert.equal(windows.weekly!.valueLabel, '65 / 60k credits');
});
});
describe('NeuralWatt quota provider (VS Code parity)', () => {
+204 -50
View File
@@ -2,6 +2,7 @@ import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import { fetchOpenCodeGoUsage } from './opencodeGoQuota';
import { fetchCommandCodeUsage } from './commandCodeQuota';
import { deleteLegacyOpenCodeGoCredential, readCredential } from './quotaCredentials';
import { getProviderAuth, updateProviderAuth } from './opencodeAuth';
@@ -71,13 +72,31 @@ type ZaiLimit = {
type?: string;
number?: number;
unit?: number;
usage?: number;
currentValue?: number;
remaining?: number;
nextResetTime?: number;
percentage?: number;
};
// CREDIT_LIMIT entries carry `usage` (total credits) and `currentValue` (consumed);
// TOKENS_LIMIT entries only carry a percentage.
const formatZaiCreditAmount = (value: number): string => {
if (value < 1000) return value.toLocaleString('en-US');
return `${Math.round(value / 100) / 10}k`;
};
const formatZaiCreditValueLabel = (limit: ZaiLimit): string | null => {
const used = toNumber(limit.currentValue);
const total = toNumber(limit.usage);
if (used === null || total === null) return null;
return `${formatZaiCreditAmount(used)} / ${formatZaiCreditAmount(total)} credits`;
};
type ZaiPayload = {
data?: {
limits?: ZaiLimit[];
level?: string;
};
};
@@ -169,6 +188,7 @@ export type ProviderResult = {
usage: ProviderUsage | null;
fetchedAt: number;
error?: string;
planLabel?: string | null;
};
const OPENCODE_CONFIG_DIR = path.join(os.homedir(), '.config', 'opencode');
@@ -409,15 +429,20 @@ const buildResult = (data: {
configured: boolean;
usage?: ProviderUsage | null;
error?: string;
}): ProviderResult => ({
providerId: data.providerId,
providerName: data.providerName,
ok: data.ok,
configured: data.configured,
usage: data.usage ?? null,
...(data.error ? { error: data.error } : {}),
fetchedAt: Date.now(),
});
planLabel?: string | null;
}): ProviderResult => {
const result: ProviderResult = {
providerId: data.providerId,
providerName: data.providerName,
ok: data.ok,
configured: data.configured,
usage: data.usage ?? null,
...(data.error ? { error: data.error } : {}),
fetchedAt: Date.now(),
};
if (data.planLabel) result.planLabel = data.planLabel;
return result;
};
const resolveXaiAuth = (): XaiAuthEntry | null => {
const entry = getProviderAuth('xai');
@@ -748,6 +773,9 @@ export const listConfiguredQuotaProviders = () => {
const configured = new Set<string>();
const openCodeGoAuth = normalizeAuthEntry(getAuthEntry(auth, ['opencode-go']));
if (openCodeGoAuth && (typeof openCodeGoAuth.key === 'string' || typeof openCodeGoAuth.token === 'string')) configured.add('opencode-go');
const commandCodeAuth = normalizeAuthEntry(getAuthEntry(auth, ['command-code']));
if (commandCodeAuth && (typeof commandCodeAuth.key === 'string' || typeof commandCodeAuth.access === 'string' || typeof commandCodeAuth.token === 'string')) configured.add('command-code');
if (process.env.COMMAND_CODE_API_KEY?.trim()) configured.add('command-code');
if (readCredential('ollama-cloud')) configured.add('ollama-cloud');
if (readCredential('cursor')) configured.add('cursor');
@@ -1251,6 +1279,112 @@ const fetchGoogleQuota = async (): Promise<ProviderResult> => {
});
};
const CLAUDE_DEFAULT_COOLDOWN_MS = 5 * 60 * 1000;
const CLAUDE_MAX_COOLDOWN_MS = 60 * 60 * 1000;
let claudeCredentialFingerprint: string | null = null;
let claudeCachedUsage: ProviderUsage | null = null;
let claudeCooldownUntil = 0;
const claudeCooldownFromResponse = (response: Response): number => {
const raw = response.headers.get('retry-after');
const seconds = raw ? Number(raw) : Number.NaN;
if (Number.isFinite(seconds) && seconds > 0) {
return Math.min(seconds * 1000, CLAUDE_MAX_COOLDOWN_MS);
}
if (raw) {
const retryAt = Date.parse(raw);
if (Number.isFinite(retryAt) && retryAt > Date.now()) {
return Math.min(retryAt - Date.now(), CLAUDE_MAX_COOLDOWN_MS);
}
}
return CLAUDE_DEFAULT_COOLDOWN_MS;
};
const buildClaudeRateLimitResult = (): ProviderResult => (
claudeCachedUsage
? buildResult({
providerId: 'claude',
providerName: 'Claude',
ok: true,
configured: true,
usage: claudeCachedUsage,
})
: buildResult({
providerId: 'claude',
providerName: 'Claude',
ok: false,
configured: true,
error: 'Rate limited. Retrying soon.',
})
);
const buildClaudeUsage = (payload: Record<string, unknown>): ProviderUsage => {
const windows: Record<string, UsageWindow> = {};
const models: Record<string, ProviderUsage> = {};
const limits = Array.isArray(payload.limits) ? payload.limits : [];
for (const entry of limits) {
const limit = asObject(entry);
if (!limit) continue;
const usedPercent = toNumber(limit.percent);
const resetAt = toTimestamp(limit.resets_at);
if (limit.kind === 'session') {
windows['5h'] = toUsageWindow({ usedPercent, windowSeconds: 5 * 60 * 60, resetAt });
} else if (limit.kind === 'weekly_all') {
windows['7d'] = toUsageWindow({ usedPercent, windowSeconds: 7 * 24 * 60 * 60, resetAt });
} else if (limit.kind === 'weekly_scoped') {
const modelName = asNonEmptyString(asObject(asObject(limit.scope)?.model)?.display_name);
if (modelName) {
models[modelName] = {
windows: {
'7d': toUsageWindow({ usedPercent, windowSeconds: 7 * 24 * 60 * 60, resetAt }),
},
};
}
}
}
if (!limits.length) {
const fiveHour = asObject(payload.five_hour);
const sevenDay = asObject(payload.seven_day);
if (fiveHour) {
windows['5h'] = toUsageWindow({
usedPercent: toNumber(fiveHour.utilization),
windowSeconds: 5 * 60 * 60,
resetAt: toTimestamp(fiveHour.resets_at),
});
}
if (sevenDay) {
windows['7d'] = toUsageWindow({
usedPercent: toNumber(sevenDay.utilization),
windowSeconds: 7 * 24 * 60 * 60,
resetAt: toTimestamp(sevenDay.resets_at),
});
}
}
const spend = asObject(payload.spend);
if (spend?.enabled === true) {
const usedMoney = asObject(spend.used);
const limitMoney = asObject(spend.limit);
const usedMinor = toNumber(usedMoney?.amount_minor);
const limitMinor = toNumber(limitMoney?.amount_minor);
const exponent = toNumber(usedMoney?.exponent) ?? 2;
const currency = asNonEmptyString(usedMoney?.currency);
const prefix = currency === 'USD' || !currency ? '$' : `${currency} `;
const used = usedMinor === null ? null : usedMinor / 10 ** exponent;
const limit = limitMinor === null ? null : limitMinor / 10 ** (toNumber(limitMoney?.exponent) ?? 2);
windows.extra_usage = toUsageWindow({
usedPercent: toNumber(spend.percent),
windowSeconds: null,
resetAt: null,
valueLabel: used === null ? null : `${prefix}${formatMoney(used)}${limit === null ? '' : ` / ${prefix}${formatMoney(limit)}`}`,
});
}
return Object.keys(models).length ? { windows, models } : { windows };
};
const fetchClaudeQuota = async (): Promise<ProviderResult> => {
const auth = readAuthFile();
const entry = normalizeAuthEntry(getAuthEntry(auth, ['anthropic', 'claude'])) as Record<string, unknown> | null;
@@ -1266,6 +1400,15 @@ const fetchClaudeQuota = async (): Promise<ProviderResult> => {
});
}
const refreshToken = typeof entry?.refresh === 'string' ? entry.refresh : '';
const fingerprint = `${accessToken}\0${refreshToken}`;
if (claudeCredentialFingerprint !== fingerprint) {
claudeCredentialFingerprint = fingerprint;
claudeCachedUsage = null;
claudeCooldownUntil = 0;
}
if (Date.now() < claudeCooldownUntil) return buildClaudeRateLimitResult();
try {
const response = await fetch('https://api.anthropic.com/api/oauth/usage', {
method: 'GET',
@@ -1275,6 +1418,21 @@ const fetchClaudeQuota = async (): Promise<ProviderResult> => {
},
});
if (response.status === 429) {
claudeCooldownUntil = Date.now() + claudeCooldownFromResponse(response);
return buildClaudeRateLimitResult();
}
if (response.status === 401 || response.status === 403) {
return buildResult({
providerId: 'claude',
providerName: 'Claude',
ok: false,
configured: true,
error: 'Claude session expired. Open Claude Code to sign in again.',
});
}
if (!response.ok) {
return buildResult({
providerId: 'claude',
@@ -1286,47 +1444,14 @@ const fetchClaudeQuota = async (): Promise<ProviderResult> => {
}
const payload = await response.json() as Record<string, unknown>;
const windows: Record<string, UsageWindow> = {};
const fiveHour = (payload as Record<string, unknown>).five_hour as Record<string, unknown> | undefined;
const sevenDay = (payload as Record<string, unknown>).seven_day as Record<string, unknown> | undefined;
const sevenDaySonnet = (payload as Record<string, unknown>).seven_day_sonnet as Record<string, unknown> | undefined;
const sevenDayOpus = (payload as Record<string, unknown>).seven_day_opus as Record<string, unknown> | undefined;
if (fiveHour) {
windows['5h'] = toUsageWindow({
usedPercent: toNumber(fiveHour.utilization),
windowSeconds: null,
resetAt: toTimestamp(fiveHour.resets_at),
});
}
if (sevenDay) {
windows['7d'] = toUsageWindow({
usedPercent: toNumber(sevenDay.utilization),
windowSeconds: null,
resetAt: toTimestamp(sevenDay.resets_at),
});
}
if (sevenDaySonnet) {
windows['7d-sonnet'] = toUsageWindow({
usedPercent: toNumber(sevenDaySonnet.utilization),
windowSeconds: null,
resetAt: toTimestamp(sevenDaySonnet.resets_at),
});
}
if (sevenDayOpus) {
windows['7d-opus'] = toUsageWindow({
usedPercent: toNumber(sevenDayOpus.utilization),
windowSeconds: null,
resetAt: toTimestamp(sevenDayOpus.resets_at),
});
}
const usage = buildClaudeUsage(payload);
claudeCachedUsage = usage;
return buildResult({
providerId: 'claude',
providerName: 'Claude',
ok: true,
configured: true,
usage: { windows },
usage,
});
} catch (error) {
return buildResult({
@@ -1957,16 +2082,19 @@ const fetchZaiQuota = async (): Promise<ProviderResult> => {
const payload = await response.json() as ZaiPayload;
const limits = Array.isArray(payload?.data?.limits) ? payload.data.limits : [];
const windows: Record<string, UsageWindow> = {};
for (const tokensLimit of limits.filter((limit) => limit?.type === 'TOKENS_LIMIT')) {
const windowSeconds = resolveWindowSeconds(tokensLimit as Record<string, unknown>);
// The API renamed TOKENS_LIMIT to CREDIT_LIMIT; field semantics stayed the same,
// so both limit types map to the same windows.
for (const limit of limits.filter((entry) => entry?.type === 'TOKENS_LIMIT' || entry?.type === 'CREDIT_LIMIT')) {
const windowSeconds = resolveWindowSeconds(limit as Record<string, unknown>);
const windowLabel = resolveWindowLabel(windowSeconds);
const resetAt = tokensLimit.nextResetTime ? normalizeTimestamp(tokensLimit.nextResetTime) : null;
const usedPercent = typeof tokensLimit.percentage === 'number' ? tokensLimit.percentage : null;
const resetAt = limit.nextResetTime ? normalizeTimestamp(limit.nextResetTime) : null;
const usedPercent = typeof limit.percentage === 'number' ? limit.percentage : null;
windows[windowLabel] = toUsageWindow({
usedPercent,
windowSeconds,
resetAt,
valueLabel: formatZaiCreditValueLabel(limit),
});
}
@@ -1985,6 +2113,7 @@ const fetchZaiQuota = async (): Promise<ProviderResult> => {
ok: true,
configured: true,
usage: { windows },
planLabel: payload?.data?.level || null,
});
} catch (error) {
return buildResult({
@@ -2705,7 +2834,7 @@ const fetchXaiQuota = async (): Promise<ProviderResult> => {
}
};
export const fetchQuotaForProvider = async (providerId: string): Promise<ProviderResult> => {
const fetchQuotaForProviderUncoalesced = async (providerId: string): Promise<ProviderResult> => {
switch (providerId) {
case 'claude':
return fetchClaudeQuota();
@@ -2746,6 +2875,18 @@ export const fetchQuotaForProvider = async (providerId: string): Promise<Provide
return buildResult({ providerId, providerName: 'OpenCode Go', ok: false, configured: true, error: error instanceof Error ? error.message : 'Request failed' });
}
}
case 'command-code': {
try {
const entry = normalizeAuthEntry(getAuthEntry(readAuthFile(), ['command-code']));
const stored = typeof entry?.key === 'string' ? entry.key : typeof entry?.access === 'string' ? entry.access : typeof entry?.token === 'string' ? entry.token : null;
const environment = process.env.COMMAND_CODE_API_KEY?.trim() || null;
const apiKey = stored?.trim() || environment;
if (!apiKey) return buildResult({ providerId, providerName: 'Command Code', ok: false, configured: false, error: 'Not configured' });
return buildResult({ providerId, providerName: 'Command Code', ok: true, configured: true, usage: { windows: await fetchCommandCodeUsage(apiKey) } });
} catch (error) {
return buildResult({ providerId, providerName: 'Command Code', ok: false, configured: true, error: error instanceof Error ? error.message : 'Request failed' });
}
}
case 'cursor':
return fetchCursorQuota();
case 'crof':
@@ -2766,3 +2907,16 @@ export const fetchQuotaForProvider = async (providerId: string): Promise<Provide
});
}
};
const pendingQuotaFetches = new Map<string, Promise<ProviderResult>>();
export const fetchQuotaForProvider = (providerId: string): Promise<ProviderResult> => {
const existing = pendingQuotaFetches.get(providerId);
if (existing) return existing;
const pending = fetchQuotaForProviderUncoalesced(providerId).finally(() => {
if (pendingQuotaFetches.get(providerId) === pending) pendingQuotaFetches.delete(providerId);
});
pendingQuotaFetches.set(providerId, pending);
return pending;
};
+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: 'ClawdHub',
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] = [];