Files
cat-shark 5947e133de feat: rewrite the Command Code provider on pi's native provider API
Replace the previous implementation with one that registers the Provider API
catalog through pi's own provider layer instead of shipping a custom transport,
cache file, and hand-maintained pricing table.

- models: derive the catalog from the published command-code CLI package
  (context windows, reasoning efforts, image input, output limits, rates) and
  keep it as the offline baseline; scripts/sync-catalog.mjs regenerates it and
  supports --check
- refresh: use refreshModels plus context.publish so pi persists the live
  /provider/v1/models listing in models-store.json and restores it offline
- auth: /login browser transfer through a localhost callback server with a
  pasted-key fallback; $COMMAND_CODE_API_KEY, --api-key and auth.json keep
  working
- streaming: pi's native openai-completions and anthropic-messages adapters;
  the generate-transport fallback and Oh My Pi branches are gone
- keep the context-overflow rewrite that enables pi's compaction retry and the
  /commandcode-quota command
- tests: 51 cases under tests/<module>/ covering models, catalog sync, auth,
  the callback server, overflow handling, quota, and the extension factory

Verified against the live API: chat, tool round trip, image input and
--thinking max on deepseek/deepseek-v4.1-flash, quota output, and catalog
persistence in an interactive session.
2026-09-14 11:19:33 +08:00

286 lines
10 KiB
JavaScript

/**
* 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:"<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()
}