Repository Public Research preview
1#!/usr/bin/env node2/**3 * Offline companion for the claude-api-toolkit skill. No network, no API key, no dependencies.4 *5 * node claude-api.mjs models [model-id] model table, or everything known about one model6 * node claude-api.mjs lint <request.json|-> findings for a Messages request (exit 2 if it would be rejected)7 * node claude-api.mjs fix <request.json|-> corrected request + list of changes8 * node claude-api.mjs migrate <request.json|-> <target-model>9 * node claude-api.mjs cost <model> <input_tokens> <output_tokens> [--cache-read N] [--batch] [--fast] [--requests N]10 * node claude-api.mjs compare <input_tokens> <output_tokens> [--requests N]11 * node claude-api.mjs scaffold <approach> [--language typescript|python] [--model ID] [--out FILE]12 * node claude-api.mjs guide [topic]13 * Add --api-version 2023-06-01 to lint/fix for code that pins the legacy version. Output is JSON unless noted.14 */15import { readFileSync, writeFileSync, existsSync } from "node:fs";16import { compareCost, estimateCost } from "./lib/cost.mjs";17import { GUIDES } from "./lib/guides.mjs";18import { CURRENT, KB, MODELS, modelSummary, resolveModel } from "./lib/knowledge.mjs";19import { fixRequest, lintRequest } from "./lib/lint.mjs";20import { APPROACHES, scaffold } from "./lib/scaffold.mjs";2122const [cmd, ...rest] = process.argv.slice(2);23const flags = {}, args = [];24for (let i = 0; i < rest.length; i++) {25 if (rest[i].startsWith("--")) { const k = rest[i].slice(2); const v = rest[i + 1]; if (v === undefined || v.startsWith("--")) flags[k] = true; else { flags[k] = v; i++; } }26 else args.push(rest[i]);27}28const print = (x) => process.stdout.write(JSON.stringify(x, null, 2) + "\n");29const die = (msg, code = 1) => { process.stderr.write(`error: ${msg}\n`); process.exit(code); };30function readRequest(src) {31 if (!src) die("give a request file, or - for stdin");32 let text;33 try { text = src === "-" ? readFileSync(0, "utf8") : readFileSync(src, "utf8"); } catch { die(`cannot read ${src}`); }34 try { return JSON.parse(text); } catch (e) { die(`${src} is not valid JSON: ${e.message}`); }35}36const opts = { apiVersion: typeof flags["api-version"] === "string" ? flags["api-version"] : undefined };37const int = (v, name) => { const n = Number(v); if (!Number.isInteger(n) || n < 0) die(`${name} must be a non-negative integer`); return n; };3839switch (cmd) {40 case "models": {41 if (args[0]) { const m = resolveModel(args[0]); if (!m) die(`unknown model "${args[0]}". Known: ${Object.keys(MODELS).join(", ")}`); print({ observed_at: KB.observed_at, ...m }); }42 else print({ observed_at: KB.observed_at, current_generation: CURRENT, models: Object.values(MODELS).sort((a, b) => b.released.localeCompare(a.released)).map(modelSummary) });43 break;44 }45 case "lint": { const r = lintRequest(readRequest(args[0]), opts); print(r); process.exit(r.valid ? 0 : 2); }46 case "fix": { print(fixRequest(readRequest(args[0]), opts)); break; }47 case "migrate": {48 const target = resolveModel(args[1]);49 if (!target) die(`unknown target model "${args[1]}"`);50 print({ to: target.id, ...fixRequest({ ...readRequest(args[0]), model: target.id }, opts) });51 break;52 }53 case "cost": {54 if (args.length < 3) die("usage: cost <model> <input_tokens> <output_tokens>");55 const r = estimateCost({ model: args[0], input_tokens: int(args[1], "input_tokens"), output_tokens: int(args[2], "output_tokens"), cache_read_input_tokens: flags["cache-read"] ? int(flags["cache-read"], "--cache-read") : 0, cache_write_5m_tokens: flags["cache-write"] ? int(flags["cache-write"], "--cache-write") : 0, batch: !!flags.batch, fast: !!flags.fast, requests: flags.requests ? int(flags.requests, "--requests") : 1 });56 if (r.error) die(r.error);57 print(r);58 break;59 }60 case "compare": {61 if (args.length < 2) die("usage: compare <input_tokens> <output_tokens>");62 print(compareCost({ input_tokens: int(args[0], "input_tokens"), output_tokens: int(args[1], "output_tokens"), requests: flags.requests ? int(flags.requests, "--requests") : 1, batch: !!flags.batch }, Object.keys(MODELS)));63 break;64 }65 case "scaffold": {66 const r = scaffold({ approach: args[0], language: flags.language ?? "typescript", model: flags.model, effort: flags.effort });67 if (r.error) die(`${r.error}\napproaches: ${Object.entries(APPROACHES).map(([k, v]) => `\n ${k} — ${v}`).join("")}`);68 if (flags.out) { if (existsSync(flags.out)) die(`${flags.out} already exists; refusing to overwrite`); writeFileSync(flags.out, r.code); process.stderr.write(`wrote ${flags.out}\ninstall: ${r.install}\nrun: ${r.run}\n`); }69 else process.stdout.write(r.code);70 break;71 }72 case "guide": {73 if (!args[0]) print({ topics: Object.keys(GUIDES) });74 else if (!GUIDES[args[0]]) die(`unknown topic "${args[0]}". Topics: ${Object.keys(GUIDES).join(", ")}`);75 else process.stdout.write(GUIDES[args[0]] + "\n");76 break;77 }78 default:79 process.stderr.write(readFileSync(new URL(import.meta.url), "utf8").split("*/")[0].split("\n").slice(2).map((l) => l.replace(/^ \* ?/, "")).join("\n") + "\n");80 process.exit(cmd ? 1 : 0);81}82