/** * Regenerates src/catalog.ts from the published Command Code CLI package. * * The CLI ships the authoritative model reference (ids, context windows, * reasoning efforts, advertised rates) and its bundle carries the * input-modality and max-output metadata that the Provider API does not * expose. Run `npm run sync:catalog` after Command Code publishes a new CLI * release; review the printed diff before committing the regenerated file. * * Usage: * node scripts/sync-catalog.mjs [--version 1.54.0] [--check] */ import { execFile } from "node:child_process" import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises" import { tmpdir } from "node:os" import { dirname, join } from "node:path" import { fileURLToPath } from "node:url" import { promisify } from "node:util" const execFileAsync = promisify(execFile) const projectRoot = join(dirname(fileURLToPath(import.meta.url)), "..") const catalogPath = join(projectRoot, "src", "catalog.ts") const modelsReferencePath = "package/dist/bundled/command-code-knowledge/reference/models.md" const cliBundlePath = "package/dist/cli.mjs" const validEfforts = ["minimal", "low", "medium", "high", "xhigh", "max"] function parseArguments(argv) { const options = { version: undefined, check: false } for (let index = 0; index < argv.length; index += 1) { const argument = argv[index] if (argument === "--check") options.check = true else if (argument === "--version") { options.version = argv[index + 1] index += 1 } else throw new Error(`Unknown argument: ${argument}`) } return options } /** Parse one `| \`id\` | Name | Context | Efforts | Rates | Min plan | Notes |` row. */ export function parseReferenceRow(line) { const match = /^\|\s*`([^`]+)`\s*\|([^|]*)\|([^|]*)\|([^|]*)\|([^|]*)\|/.exec(line) if (!match) return undefined const [, id, name, context, efforts, rates] = match const effortText = efforts.trim() return { id: id.trim(), name: name.trim(), context: context.trim(), efforts: effortText === "—" || effortText === "" ? [] : effortText.split(",").map((e) => e.trim()), rates: rates.trim(), } } /** `$0.66/$1.98 · cache $0.022 (write $2.5)` → per-million-token rates. */ export function parseRates(text) { const match = /\$([\d.]+)\s*\/\s*\$([\d.]+)\s*·\s*cache\s*\$([\d.]+)(?:\s*\(write\s*\$([\d.]+)\))?/.exec(text) if (!match) throw new Error(`Could not parse rates: ${text}`) return { input: Number(match[1]), output: Number(match[2]), cacheRead: Number(match[3]), cacheWrite: match[4] === undefined ? 0 : Number(match[4]), } } export function parseReference(markdown) { const models = [] for (const line of markdown.split("\n")) { const row = parseReferenceRow(line) if (!row) continue if (models.some((model) => model.id === row.id)) throw new Error(`Duplicate model id: ${row.id}`) for (const effort of row.efforts) { if (!validEfforts.includes(effort)) throw new Error(`Unknown effort for ${row.id}: ${effort}`) } models.push({ ...row, cost: parseRates(row.rates) }) } if (models.length === 0) throw new Error("No model rows found in the Command Code reference") return models } /** * Slice the `{id:"",inputModalities:...}` object literal out of the * minified CLI bundle, tracking nested braces and string literals. */ export function sliceModelObject(bundle, id) { const start = bundle.indexOf(`{id:${JSON.stringify(id)},inputModalities:`) if (start < 0) return undefined let depth = 0 let quote = "" let escaped = false for (let index = start; index < bundle.length; index += 1) { const character = bundle[index] if (quote) { if (escaped) escaped = false else if (character === "\\") escaped = true else if (character === quote) quote = "" continue } if (character === '"' || character === "'" || character === "`") { quote = character } else if (character === "{") { depth += 1 } else if (character === "}") { depth -= 1 if (depth === 0) return bundle.slice(start, index + 1) } } throw new Error(`Unterminated model object for ${id}`) } function numberField(entry, key) { const match = new RegExp(`${key}:(-?[0-9][0-9.e+]*)`).exec(entry) if (!match) return undefined const value = Number(match[1]) return Number.isFinite(value) ? value : undefined } function stringArrayField(entry, key) { const match = new RegExp(`${key}:\\[([^\\]]*)\\]`).exec(entry) if (!match) return undefined return [...match[1].matchAll(/"([^"]*)"/g)].map((item) => item[1]) } export function parseBundleMetadata(bundle, ids) { const metadata = new Map() for (const id of ids) { const entry = sliceModelObject(bundle, id) if (!entry) continue const input = stringArrayField(entry, "inputModalities") ?? ["text"] metadata.set(id, { input, reasoning: entry.includes("reasoning:!0") || entry.includes("reasoningEfforts:["), maxOutputTokens: numberField(entry, "maxOutputTokens"), contextWindow: numberField(entry, "contextWindow"), }) } return metadata } export function buildCatalog(version, reference, metadata) { return reference.map((model) => { const bundle = metadata.get(model.id) if (!bundle) throw new Error(`No CLI bundle metadata found for ${model.id}`) const efforts = model.efforts return { id: model.id, name: model.name, contextWindow: bundle.contextWindow ?? 0, efforts, reasoning: bundle.reasoning || model.efforts.length > 0, input: bundle.input, maxOutputTokens: bundle.maxOutputTokens ?? 0, cost: model.cost, } }) } function renderCatalog(version, models) { const lines = [] lines.push("/**") lines.push(" * Generated by scripts/sync-catalog.mjs from the published Command Code CLI") lines.push(" * package. Do not edit manually: run `npm run sync:catalog` instead.") lines.push(" */") lines.push("") lines.push(`export const COMMAND_CODE_CLI_VERSION = ${JSON.stringify(version)}`) lines.push("") lines.push('export type CommandCodeInputModality = "text" | "image"') lines.push("") lines.push('export type CommandCodeReasoningEffort =') for (const effort of validEfforts) lines.push(` | ${JSON.stringify(effort)}`) lines.push("") lines.push("export interface CommandCodeModelCost {") lines.push(" input: number") lines.push(" output: number") lines.push(" cacheRead: number") lines.push(" cacheWrite: number") lines.push("}") lines.push("") lines.push("export interface CatalogModel {") lines.push(" id: string") lines.push(" name: string") lines.push(" contextWindow: number") lines.push(" efforts: readonly CommandCodeReasoningEffort[]") lines.push(" reasoning: boolean") lines.push(" input: readonly CommandCodeInputModality[]") lines.push(" maxOutputTokens: number") lines.push(" cost: CommandCodeModelCost") lines.push("}") lines.push("") lines.push("export const CATALOG: readonly CatalogModel[] = [") for (const model of models) { lines.push(" {") lines.push(` id: ${JSON.stringify(model.id)},`) lines.push(` name: ${JSON.stringify(model.name)},`) lines.push(` contextWindow: ${model.contextWindow},`) lines.push(` efforts: [${model.efforts.map((effort) => JSON.stringify(effort)).join(", ")}],`) lines.push(` reasoning: ${model.reasoning},`) lines.push(` input: [${model.input.map((item) => JSON.stringify(item)).join(", ")}],`) lines.push(` maxOutputTokens: ${model.maxOutputTokens},`) lines.push( ` cost: { input: ${model.cost.input}, output: ${model.cost.output}, cacheRead: ${model.cost.cacheRead}, cacheWrite: ${model.cost.cacheWrite} },`, ) lines.push(" },") } lines.push("]") lines.push("") return lines.join("\n") } async function latestVersion() { const { stdout } = await execFileAsync("npm", ["view", "command-code", "version"], { encoding: "utf-8" }) return stdout.trim() } async function downloadCli(version, directory) { const { stdout } = await execFileAsync("npm", ["pack", `command-code@${version}`, "--silent"], { cwd: directory, encoding: "utf-8", }) const tarball = stdout.trim().split("\n").pop() if (!tarball) throw new Error("npm pack did not report a tarball") await execFileAsync("tar", ["xzf", tarball], { cwd: directory }) return { reference: await readFile(join(directory, modelsReferencePath), "utf-8"), bundle: await readFile(join(directory, cliBundlePath), "utf-8"), } } function summarize(previous, next) { const parse = (content) => content ? new Map( [...content.matchAll(/^ {4}id: "([^"]+)",$/gm)].map((match) => [match[1], match[1]]), ) : new Map() const before = parse(previous) const after = parse(next) const added = [...after.keys()].filter((id) => !before.has(id)) const removed = [...before.keys()].filter((id) => !after.has(id)) return { added, removed } } async function main() { const options = parseArguments(process.argv.slice(2)) const version = options.version ?? (await latestVersion()) const directory = await mkdtemp(join(tmpdir(), "commandcode-catalog-")) try { const { reference, bundle } = await downloadCli(version, directory) const models = parseReference(reference) const metadata = parseBundleMetadata( bundle, models.map((model) => model.id), ) const catalog = buildCatalog(version, models, metadata) const previous = await readFile(catalogPath, "utf-8").catch(() => undefined) const next = renderCatalog(version, catalog) const { added, removed } = summarize(previous, next) console.log(`command-code@${version}: ${catalog.length} models`) console.log(`added: ${added.length > 0 ? added.join(", ") : "none"}`) console.log(`removed: ${removed.length > 0 ? removed.join(", ") : "none"}`) if (options.check) { if (previous !== next) { console.error("src/catalog.ts is out of date; run `npm run sync:catalog`.") process.exitCode = 1 } return } await mkdir(dirname(catalogPath), { recursive: true }) await writeFile(catalogPath, next, "utf-8") console.log(`wrote ${catalogPath}`) } finally { await rm(directory, { recursive: true, force: true }) } } if (process.argv[1] === fileURLToPath(import.meta.url)) { await main() }