{"repo": "cdesktop-ai/cdesktop", "files": {"scripts/import-provider-catalog.ts": {"75bd015e88f9797d785c9fabcfca03c7151fd5cc": {"t": 1785596024.444697, "n": 15405, "h": "997cf9735a660a39", "c": "#!/usr/bin/env tsx\n/**\n * Imports the unified provider catalog from cc-switch by walking all five\n * preset sources and merging by preset name.\n *\n * Run from the cdesktop/ directory:\n * node_modules/.pnpm/node_modules/.bin/tsx scripts/import-provider-catalog.ts\n *\n * Output: crates/db/src/provider_catalog.json\n *\n * Sources walked:\n * - related/cc-switch/src/config/claudeProviderPresets.ts\n * - related/cc-switch/src/config/codexProviderPresets.ts\n * - related/cc-switch/src/config/opencodeProviderPresets.ts\n * - related/cc-switch/src/config/hermesProviderPresets.ts\n *\n * For each preset name in WANTED, we look it up in each of the four sources\n * and emit a unified record with per-agent payloads. agents[] = sources where\n * the preset is found AND passes eligibility filters:\n * - CODEX: config TOML must contain `wire_api = \"responses\"`.\n * - GEMINI: never sourced from cc-switch — the only Western-official entry\n * (\"Google Official\") overlaps the Default provider's ambient\n * gemini-cli auth, and remaining cc-switch gemini presets have\n * known upstream issues. All catalog presets emit an empty\n * gemini slot; users wanting Gemini routing create a Custom record.\n * - DEEPSEEK_TUI: any preset that exposes an OpenAI Chat Completions\n * endpoint, which is what DeepSeek TUI speaks. Two cc-switch\n * signals: opencode `npm == \"@ai-sdk/openai-compatible\"` with a\n * baseURL, OR hermes `api_mode == \"chat_completions\"` with a\n * base_url (catches OpenRouter, Kimi, etc. whose OpenCode preset\n * uses @ai-sdk/anthropic but whose service still offers an\n * OpenAI-compatible route via Hermes). (The executor wrapper\n * itself ships in Phase E.)\n *\n * Normalization applied at instantiation time (not here):\n * - ANTHROPIC_MODEL / ANTHROPIC_DEFAULT_SONNET_MODEL / ANTHROPIC_DEFAULT_OPUS_MODEL\n * stripped from claude.env, surfaced under \"Advanced env\"\n * - ANTHROPIC_DEFAULT_HAIKU_MODEL migrated to claude.haikuModel field\n */\n\nimport { execSync, spawnSync } from \"node:child_process\";\nimport { writeFileSync, unlinkSync, existsSync } from \"node:fs\";\nimport { resolve, dirname } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = dirname(__filename);\n\nconst CC_SWITCH_DIR = resolve(__dirname, \"../../related/cc-switch\");\nconst OUT_PATH = resolve(__dirname, \"../crates/db/src/provider_catalog.json\");\n\n// Preset IDs we want, in display order. Key = our slug; value = cc-switch name\n// (must match exactly across all five sources where the preset appears).\nconst WANTED: Record = {\n \"openrouter\": \"OpenRouter\",\n \"deepseek\": \"DeepSeek\",\n \"zhipu-glm\": \"Zhipu GLM\",\n \"zhipu-glm-en\": \"Zhipu GLM en\",\n \"bailian\": \"Bailian\",\n \"bailian-for-coding\": \"Bailian For Coding\",\n \"kimi\": \"Kimi\",\n \"kimi-for-coding\": \"Kimi For Coding\",\n \"minimax\": \"MiniMax\",\n \"minimax-en\": \"MiniMax en\",\n \"stepfun\": \"StepFun\",\n \"stepfun-en\": \"StepFun en\",\n \"doubao-seed\": \"DouBaoSeed\",\n \"xiaomi-mimo\": \"Xiaomi MiMo\",\n \"nvidia\": \"Nvidia\",\n \"aws-bedrock-api-key\": \"AWS Bedrock (API Key)\",\n \"novita-ai\": \"Novita AI\",\n \"kat-coder\": \"KAT-Coder\",\n \"longcat\": \"Longcat\",\n \"bailing\": \"BaiLing\",\n \"modelscope\": \"ModelScope\",\n \"baidu-qianfan\": \"Baidu Qianfan Coding Plan\",\n};\n\n// Extractor script: dumps every preset array to JSON on stdout.\nconst EXTRACTOR_PATH = resolve(CC_SWITCH_DIR, \"_cdesktop_extractor.ts\");\nconst extractorSrc = `\nimport { providerPresets } from \"./src/config/claudeProviderPresets.ts\";\nimport { codexProviderPresets } from \"./src/config/codexProviderPresets.ts\";\nimport { opencodeProviderPresets } from \"./src/config/opencodeProviderPresets.ts\";\nimport { hermesProviderPresets } from \"./src/config/hermesProviderPresets.ts\";\nprocess.stdout.write(JSON.stringify({\n claude: providerPresets,\n codex: codexProviderPresets,\n opencode: opencodeProviderPresets,\n hermes: hermesProviderPresets,\n}));\n`;\n\nwriteFileSync(EXTRACTOR_PATH, extractorSrc);\n\ninterface ExtractedSources {\n claude: ClaudeRaw[];\n codex: CodexRaw[];\n opencode: OpencodeRaw[];\n hermes: HermesRaw[];\n}\n\ninterface ClaudeRaw {\n name: string;\n websiteUrl: string;\n apiKeyUrl?: string;\n apiKeyField?: string;\n settingsConfig?: { env?: Record };\n category?: string;\n}\n\ninterface CodexRaw {\n name: string;\n config: string; // TOML string\n}\n\ninterface OpencodeRaw {\n name: string;\n settingsConfig: {\n npm?: string;\n name?: string;\n options?: Record;\n models?: Record;\n };\n}\n\ninterface HermesRaw {\n name: string;\n settingsConfig: {\n name: string;\n base_url?: string;\n api_key?: string;\n api_mode?: string;\n models?: Array<{ id: string; name?: string }>;\n };\n}\n\nlet sources: ExtractedSources;\ntry {\n const tsx = resolve(\n CC_SWITCH_DIR,\n \"../../cdesktop/node_modules/.pnpm/node_modules/.bin/tsx\"\n );\n const result = spawnSync(tsx, [EXTRACTOR_PATH], {\n cwd: CC_SWITCH_DIR,\n encoding: \"utf-8\",\n });\n if (result.error) throw result.error;\n if (result.status !== 0) {\n throw new Error(`extractor failed:\\n${result.stderr}`);\n }\n sources = JSON.parse(result.stdout);\n} finally {\n if (existsSync(EXTRACTOR_PATH)) unlinkSync(EXTRACTOR_PATH);\n}\n\nfunction indexByName(arr: T[]): Map {\n const m = new Map();\n for (const item of arr) m.set(item.name, item);\n return m;\n}\n\nconst claudeByName = indexByName(sources.claude);\nconst codexByName = indexByName(sources.codex);\nconst opencodeByName = indexByName(sources.opencode);\nconst hermesByName = indexByName(sources.hermes);\n\n// --- Per-agent payload extractors ---\n\ninterface ClaudePayload {\n apiKeyField: string | null;\n baseUrl: string | null;\n haikuModel: string | null;\n env: Record;\n}\n\nfunction extractClaude(raw: ClaudeRaw | undefined): ClaudePayload | null {\n if (!raw) return null;\n const rawEnv = raw.settingsConfig?.env ?? {};\n const env: Record = {};\n for (const [k, v] of Object.entries(rawEnv)) env[k] = String(v);\n\n const baseUrl = env.ANTHROPIC_BASE_URL ?? null;\n delete env.ANTHROPIC_BASE_URL;\n\n const haikuModel = env.ANTHROPIC_DEFAULT_HAIKU_MODEL ?? null;\n delete env.ANTHROPIC_DEFAULT_HAIKU_MODEL;\n\n // Strip per-message-conflicting keys (§6.1)\n delete env.ANTHROPIC_MODEL;\n delete env.ANTHROPIC_DEFAULT_SONNET_MODEL;\n delete env.ANTHROPIC_DEFAULT_OPUS_MODEL;\n\n let apiKeyField = raw.apiKeyField ?? null;\n if (!apiKeyField) {\n if (\"ANTHROPIC_AUTH_TOKEN\" in env) apiKeyField = \"ANTHROPIC_AUTH_TOKEN\";\n else if (\"ANTHROPIC_API_KEY\" in env) apiKeyField = \"ANTHROPIC_API_KEY\";\n else apiKeyField = \"ANTHROPIC_AUTH_TOKEN\";\n }\n delete env.ANTHROPIC_AUTH_TOKEN;\n delete env.ANTHROPIC_API_KEY;\n\n return { apiKeyField, baseUrl, haikuModel, env };\n}\n\ninterface CodexPayload {\n baseUrl: string | null;\n env: Record;\n wireApi: string | null; // captured for eligibility check; not emitted\n}\n\nfunction extractCodex(raw: CodexRaw | undefined): CodexPayload | null {\n if (!raw) return null;\n const toml = raw.config;\n if (!toml) return null;\n\n const baseUrlMatch = toml.match(/\\bbase_url\\s*=\\s*\"([^\"]+)\"/);\n const wireApiMatch = toml.match(/\\bwire_api\\s*=\\s*\"([^\"]+)\"/);\n return {\n baseUrl: baseUrlMatch?.[1] ?? null,\n env: {},\n wireApi: wireApiMatch?.[1] ?? null,\n };\n}\n\ninterface OpencodePayload {\n npm: string | null;\n baseUrl: string | null;\n options: Record;\n env: Record;\n}\n\nfunction extractOpencode(raw: OpencodeRaw | undefined): OpencodePayload | null {\n if (!raw) return null;\n const sc = raw.settingsConfig ?? {};\n const opts = { ...(sc.options ?? {}) } as Record;\n const baseUrl =\n typeof opts.baseURL === \"string\" ? (opts.baseURL as string) : null;\n // baseURL hoisted to top-level baseUrl; apiKey is synthesized at spawn.\n delete opts.baseURL;\n delete opts.apiKey;\n return {\n npm: sc.npm ?? null,\n baseUrl,\n options: opts,\n env: {},\n };\n}\n\ninterface DeepseekTuiPayload {\n baseUrl: string;\n env: Record;\n}\n\n// DeepSeek TUI speaks OpenAI Chat Completions. Two cc-switch signals:\n// 1. OpenCode preset uses `npm: \"@ai-sdk/openai-compatible\"` + baseURL.\n// 2. Hermes preset uses `api_mode: \"chat_completions\"` + base_url.\n// Either is sufficient — many providers (OpenRouter, Kimi, …) ship their\n// OpenAI-compatible endpoint under Hermes while OpenCode uses a different\n// adapter for richer semantics. Prefer (1) when present, fall back to (2).\nfunction extractDeepseekTui(\n opencodeRaw: OpencodeRaw | undefined,\n hermesRaw: HermesRaw | undefined\n): DeepseekTuiPayload | null {\n const ocSc = opencodeRaw?.settingsConfig ?? {};\n if (ocSc.npm === \"@ai-sdk/openai-compatible\") {\n const opts = ocSc.options ?? {};\n const baseUrl = typeof opts.baseURL === \"string\" ? opts.baseURL : null;\n if (baseUrl) return { baseUrl, env: {} };\n }\n\n const hSc = hermesRaw?.settingsConfig;\n if (hSc?.api_mode === \"chat_completions\" && hSc.base_url) {\n return { baseUrl: hSc.base_url, env: {} };\n }\n\n return null;\n}\n\ninterface HermesPayload {\n baseUrl: string | null;\n apiMode: string | null;\n env: Record;\n}\n\nfunction extractHermes(raw: HermesRaw | undefined): HermesPayload | null {\n if (!raw) return null;\n const sc = raw.settingsConfig;\n return {\n baseUrl: sc.base_url ?? null,\n apiMode: sc.api_mode ?? null,\n env: {},\n };\n}\n\nfunction extractEnabledModels(\n opencode: OpencodeRaw | undefined,\n hermes: HermesRaw | undefined\n): string[] {\n // Preference: OpenCode (richer model lists) → Hermes → empty.\n // Caller layers in any other source they find useful.\n const fromOpencode = opencode?.settingsConfig?.models\n ? Object.keys(opencode.settingsConfig.models)\n : [];\n if (fromOpencode.length > 0) return fromOpencode;\n const fromHermes = hermes?.settingsConfig?.models?.map((m) => m.id) ?? [];\n return fromHermes;\n}\n\n// --- Build the catalog ---\n\n// Catalog presets never carry apiKey — that's a user-supplied credential. We\n// emit `apiKey: null` for every per-agent payload to match the shared\n// (catalog + user-record) shape; users override via the form's per-agent field.\ninterface CatalogPreset {\n id: string;\n name: string;\n agents: string[];\n claude: {\n apiKeyField: string | null;\n baseUrl: string | null;\n haikuModel: string | null;\n apiKey: string | null;\n env: Record;\n };\n codex: {\n baseUrl: string | null;\n apiKey: string | null;\n env: Record;\n };\n opencode: {\n npm: string | null;\n baseUrl: string | null;\n options: Record;\n apiKey: string | null;\n env: Record;\n };\n deepseekTui: {\n baseUrl: string | null;\n apiKey: string | null;\n env: Record;\n };\n gemini: {\n baseUrl: string | null;\n apiKey: string | null;\n env: Record;\n };\n hermes: {\n baseUrl: string | null;\n apiMode: string | null;\n apiKey: string | null;\n env: Record;\n };\n enabledModels: string[];\n}\n\nconst sha = execSync(\"git rev-parse HEAD\", { cwd: CC_SWITCH_DIR })\n .toString()\n .trim();\n\nconst presets: CatalogPreset[] = [];\nconst warnings: string[] = [];\n\nfor (const [id, ccName] of Object.entries(WANTED)) {\n const claudeRaw = claudeByName.get(ccName);\n const codexRaw = codexByName.get(ccName);\n const opencodeRaw = opencodeByName.get(ccName);\n const hermesRaw = hermesByName.get(ccName);\n\n if (!claudeRaw && !codexRaw && !opencodeRaw && !hermesRaw) {\n warnings.push(`preset \"${ccName}\" not found in any cc-switch source`);\n continue;\n }\n\n const claudeExtracted = extractClaude(claudeRaw);\n const codexExtracted = extractCodex(codexRaw);\n const opencodeExtracted = extractOpencode(opencodeRaw);\n const deepseekTuiExtracted = extractDeepseekTui(opencodeRaw, hermesRaw);\n const hermesExtracted = extractHermes(hermesRaw);\n\n // Eligibility filters → recommended agents[] for this preset.\n // Per plan §3.2: when an agent is INELIGIBLE for a preset, its payload slot\n // must be empty (null baseUrl + empty env), not populated with cc-switch's\n // raw data. Otherwise a user toggling perAgentEnabled[X]=true on an\n // ineligible agent would inherit a misleading non-null baseUrl and the\n // spawn applier would 404 silently.\n const agents: string[] = [];\n\n const claudeEligible = !!(claudeExtracted && claudeExtracted.baseUrl);\n if (claudeEligible) agents.push(\"CLAUDE_CODE\");\n\n // Codex: must speak Responses API.\n const codexEligible = !!(\n codexExtracted &&\n codexExtracted.baseUrl &&\n codexExtracted.wireApi === \"responses\"\n );\n if (codexEligible) agents.push(\"CODEX\");\n\n const opencodeEligible = !!(opencodeExtracted && opencodeExtracted.baseUrl);\n if (opencodeEligible) agents.push(\"OPENCODE\");\n\n // GEMINI: never sourced from cc-switch (see header comment). Slot stays empty.\n\n const hermesEligible = !!(hermesExtracted && hermesExtracted.baseUrl);\n if (hermesEligible) agents.push(\"HERMES\");\n\n const deepseekTuiEligible = !!deepseekTuiExtracted;\n if (deepseekTuiEligible) agents.push(\"DEEPSEEK_TUI\");\n\n presets.push({\n id,\n name: claudeRaw?.name ?? ccName,\n agents,\n claude: claudeEligible\n ? { ...claudeExtracted!, apiKey: null }\n : {\n apiKeyField: null,\n baseUrl: null,\n haikuModel: null,\n apiKey: null,\n env: {},\n },\n codex: codexEligible\n ? {\n baseUrl: codexExtracted!.baseUrl,\n apiKey: null,\n env: codexExtracted!.env,\n }\n : { baseUrl: null, apiKey: null, env: {} },\n opencode: opencodeEligible\n ? { ...opencodeExtracted!, apiKey: null }\n : { npm: null, baseUrl: null, options: {}, apiKey: null, env: {} },\n deepseekTui: deepseekTuiEligible\n ? { ...deepseekTuiExtracted!, apiKey: null }\n : { baseUrl: null, apiKey: null, env: {} },\n gemini: { baseUrl: null, apiKey: null, env: {} },\n hermes: hermesEligible\n ? { ...hermesExtracted!, apiKey: null }\n : { baseUrl: null, apiMode: null, apiKey: null, env: {} },\n enabledModels: extractEnabledModels(opencodeRaw, hermesRaw),\n });\n}\n\nconst catalog = {\n schema_version: 2,\n cc_switch_source_sha: sha,\n note:\n \"Per-agent payloads derived programmatically from cc-switch's claude/codex/opencode/hermes preset sources. \" +\n \"agents[] = recommended set, computed from per-agent payload availability + eligibility filters \" +\n \"(CODEX requires wire_api='responses'; GEMINI is never sourced from cc-switch — Default's ambient \" +\n \"auth covers official Google routing and remaining cc-switch gemini presets have known upstream issues; \" +\n \"DEEPSEEK_TUI is enabled for any preset exposing an OpenAI Chat Completions endpoint — either via OpenCode's npm='@ai-sdk/openai-compatible' or via Hermes's api_mode='chat_completions').\",\n presets,\n};\n\nwriteFileSync(OUT_PATH, JSON.stringify(catalog, null, 2) + \"\\n\");\nconsole.log(`Wrote ${presets.length} presets to ${OUT_PATH}`);\nconsole.log(`Pinned to cc-switch SHA: ${sha}`);\nif (warnings.length > 0) {\n console.log(`\\nWarnings:`);\n for (const w of warnings) console.log(` - ${w}`);\n}\n"}}}}