@kal-elsam/kairo-runtime 0.16.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/package.json +2 -1
  3. package/scripts/cockpit-smoke.mjs +1 -1
  4. package/scripts/ux-smoke-test.sh +3 -3
  5. package/src/cli.js +96 -11
  6. package/src/global/agent-capabilities/create-capability-adapter.js +2 -2
  7. package/src/global/architect/architect-cli.js +76 -0
  8. package/src/global/architect/architect-codex.js +146 -0
  9. package/src/global/architect/architect-manager.js +125 -0
  10. package/src/global/architect/architect-store.js +377 -0
  11. package/src/global/architect/architect-types.js +47 -0
  12. package/src/global/cli-help.js +10 -1
  13. package/src/global/cockpit/app.js +493 -0
  14. package/src/global/cockpit/card.js +111 -0
  15. package/src/global/cockpit/cli.js +33 -0
  16. package/src/global/cockpit/gauge.js +31 -0
  17. package/src/global/cockpit/project-overlay.js +693 -0
  18. package/src/global/cockpit/rows.js +148 -0
  19. package/src/global/cockpit/theme.js +118 -0
  20. package/src/global/cockpit/view.js +1298 -0
  21. package/src/global/conversation/bootstrap-analyzer-adapters.js +251 -0
  22. package/src/global/conversation/cli.js +53 -0
  23. package/src/global/conversation/codex-sandbox.js +230 -0
  24. package/src/global/conversation/cursor-sandbox.js +215 -0
  25. package/src/global/conversation/project-analysis.js +204 -0
  26. package/src/global/conversation/project-profile.js +178 -0
  27. package/src/global/conversation/project-router.js +149 -0
  28. package/src/global/conversation/project-strategy-store.js +64 -0
  29. package/src/global/conversation/project-strategy.js +514 -0
  30. package/src/global/conversation/sanitized-snapshot.js +169 -0
  31. package/src/global/conversation/secret-scanner.js +71 -0
  32. package/src/global/conversation/service.js +1090 -0
  33. package/src/global/conversation/session-store.js +75 -0
  34. package/src/global/conversation/transcript-store.js +79 -0
  35. package/src/global/conversation/ui.js +195 -0
  36. package/src/global/intelligence/capability-scoring.js +480 -0
  37. package/src/global/intelligence/execution-router.js +466 -0
  38. package/src/global/intelligence/kairo-telemetry-source.js +59 -0
  39. package/src/global/intelligence/kairobench-runner.js +85 -0
  40. package/src/global/intelligence/kairobench-source.js +34 -0
  41. package/src/global/intelligence/kairobench-tasks.js +47 -0
  42. package/src/global/intelligence/model-candidate-catalog.js +456 -0
  43. package/src/global/intelligence/model-capability-registry-sources.js +145 -0
  44. package/src/global/intelligence/model-capability-registry.js +125 -0
  45. package/src/global/intelligence/model-intelligence.js +1646 -0
  46. package/src/global/intelligence/official-benchmark-snapshots.js +162 -0
  47. package/src/global/intelligence/quick-ask.js +149 -0
  48. package/src/global/intelligence/role-profiles.js +251 -0
  49. package/src/global/intelligence/skill-catalog.js +67 -0
  50. package/src/global/intelligence/subscription-pressure-source.js +41 -0
  51. package/src/global/mcp/kairo-mcp.js +51 -18
  52. package/src/global/mcp/work-snapshot-rule.js +4 -2
  53. package/src/global/mcp/workspace-binding.js +88 -0
  54. package/src/global/mcp/workspace-mcp-entry.js +74 -0
  55. package/src/global/mcp-install.js +8 -1
  56. package/src/global/observability/artificial-analysis-models.js +118 -0
  57. package/src/global/observability/claude-models.js +31 -0
  58. package/src/global/observability/claude-usage.js +112 -0
  59. package/src/global/observability/codex-models.js +96 -0
  60. package/src/global/observability/codex-usage.js +160 -0
  61. package/src/global/observability/cursor-auth.js +88 -0
  62. package/src/global/observability/cursor-models.js +101 -0
  63. package/src/global/observability/huggingface-leaderboard.js +97 -0
  64. package/src/global/observability/opencode-models.js +101 -0
  65. package/src/global/observability/opencode-usage.js +162 -0
  66. package/src/global/paths.js +49 -2
  67. package/src/global/profile.js +23 -1
  68. package/src/global/runtime/execution-adapters/claude.js +63 -30
  69. package/src/global/runtime/execution-adapters/codex.js +9 -2
  70. package/src/global/runtime/execution-adapters/create-execution-adapter.js +6 -1
  71. package/src/global/runtime/execution-adapters/opencode.js +83 -18
  72. package/src/global/runtime/execution-worktree-manager.js +924 -0
  73. package/src/global/runtime/execution-worktree-orchestrator.js +194 -0
  74. package/src/global/runtime/execution-worktree-store.js +83 -0
  75. package/src/global/runtime/execution-worktree-types.js +45 -0
  76. package/src/global/runtime/run-events.js +38 -0
  77. package/src/global/runtime/run-manager.js +22 -6
  78. package/src/global/runtime/run-supervisor.js +41 -12
  79. package/src/global/runtime/usage-manager.js +96 -0
  80. package/src/global/runtime/usage-store.js +69 -0
  81. package/src/global/runtime/usage-types.js +62 -0
@@ -0,0 +1,41 @@
1
+ // Real subscription quota pressure — how much headroom is actually left
2
+ // on each provider's account right now (from the same usage probes
3
+ // service.js already fetches for /usage). This is a PROVIDER-level
4
+ // signal, not a per-model efficiency measurement: quota is account-wide,
5
+ // so it is modeled here as its own ProviderCapacity map, keyed by
6
+ // adapterId — never copied into the per-model capability registry as if
7
+ // it were evidence about an individual model. Two models from the same
8
+ // provider share the exact same real capacity entry; there is no way to
9
+ // represent "this model is more quota-efficient than that one" here,
10
+ // because that isn't a real, measurable thing — only the provider's
11
+ // account has quota.
12
+ //
13
+ // EFFICIENT TEAM (model-intelligence.js) resolves this separately from
14
+ // its per-model EFFICIENCY_DIMENSIONS, and deliberately checks it LAST —
15
+ // only when no real per-model signal (consumption, cost, duration, price,
16
+ // throughput) distinguishes otherwise-adequate candidates. A provider's
17
+ // spare quota must never, by itself, decide who wins a role over a model
18
+ // with genuinely better per-task economics.
19
+
20
+ /**
21
+ * @typedef {object} ProviderCapacity
22
+ * @property {string} adapterId
23
+ * @property {number} [quotaRemainingPercent]
24
+ */
25
+
26
+ /**
27
+ * Builds a real ProviderCapacity map from the same remaining-percent
28
+ * figures already computed for /usage — one entry per adapter that
29
+ * reported a real number, never a fabricated one for an adapter with no
30
+ * data.
31
+ * @param {Record<string, number|null|undefined>} remainingPercentByAdapter
32
+ * @returns {Record<string, ProviderCapacity>}
33
+ */
34
+ export function buildProviderCapacity(remainingPercentByAdapter) {
35
+ const capacity = {};
36
+ for (const [adapterId, remainingPercent] of Object.entries(remainingPercentByAdapter ?? {})) {
37
+ if (remainingPercent == null) continue;
38
+ capacity[adapterId] = { adapterId, quotaRemainingPercent: remainingPercent };
39
+ }
40
+ return capacity;
41
+ }
@@ -18,16 +18,35 @@ import {
18
18
  workSnapshotPublishSchema
19
19
  } from "./work-snapshot-tool.js";
20
20
  import { resolveMcpWorkspaceCwd } from "./resolve-mcp-workspace.js";
21
+ import {
22
+ WORKSPACE_BINDING_CODES,
23
+ resolveWorkspaceWriteBinding
24
+ } from "./workspace-binding.js";
21
25
 
22
- /** Sole MCP write tool for companion snapshots. */
26
+ /** Sole MCP write tool for companion snapshots. Bound servers only. */
23
27
  export const KAIRO_MCP_WRITE_TOOLS = Object.freeze(["kairo_publish_work_snapshot"]);
24
28
 
25
- export const KAIRO_MCP_TOOLS = Object.freeze([
29
+ export const KAIRO_MCP_READ_TOOLS = Object.freeze([
26
30
  "kairo_status", "kairo_runs", "kairo_alerts", "kairo_gentle_status",
27
- "kairo_graph_query", "kairo_graph_path", "kairo_context_summary", "kairo_fleet",
31
+ "kairo_graph_query", "kairo_graph_path", "kairo_context_summary", "kairo_fleet"
32
+ ]);
33
+
34
+ export const KAIRO_MCP_TOOLS = Object.freeze([
35
+ ...KAIRO_MCP_READ_TOOLS,
28
36
  ...KAIRO_MCP_WRITE_TOOLS
29
37
  ]);
30
38
 
39
+ export function mcpWorkspaceBinding(deps = {}) {
40
+ return resolveWorkspaceWriteBinding({
41
+ workspaceBound: deps.workspaceBound === true,
42
+ cwdExplicit: deps.cwdExplicit === true,
43
+ cwd: deps.cwd,
44
+ processCwd: deps.processCwd ?? process.cwd(),
45
+ userHome: deps.userHome,
46
+ env: deps.env ?? process.env
47
+ });
48
+ }
49
+
31
50
  const empty = z.object({});
32
51
  export const mcpSchemas = Object.freeze({
33
52
  empty,
@@ -105,11 +124,14 @@ function graphEnvelope(result) {
105
124
 
106
125
  export function createToolHandlers(deps = {}) {
107
126
  const homeDir = deps.homeDir ?? resolveHomeDir();
108
- // Cursor may spawn MCP under $HOME; prefer VSCODE_CWD / WORKSPACE_FOLDER_PATHS.
109
- const cwd = resolveMcpWorkspaceCwd({
110
- cwd: deps.cwd,
111
- env: deps.env ?? process.env
112
- });
127
+ const env = deps.env ?? process.env;
128
+ const binding = mcpWorkspaceBinding({ ...deps, env });
129
+ const cwd = binding.writable
130
+ ? binding.cwd
131
+ : resolveMcpWorkspaceCwd({
132
+ cwd: deps.cwdExplicit === true ? deps.cwd : (binding.bound ? undefined : deps.cwd),
133
+ env: binding.bound ? {} : env
134
+ });
113
135
  const listRuns = deps.listRuns ?? ((o) => listRunRecords(homeDir, o));
114
136
  const listAlertRows = deps.listAlerts ?? ((o) => listAlerts({ homeDir, ...o }));
115
137
  const listReviews = deps.listReviews ?? (() => listReviewReceipts({ homeDir, limit: 20 }));
@@ -238,20 +260,26 @@ export function createToolHandlers(deps = {}) {
238
260
  });
239
261
  }
240
262
  },
241
- kairo_publish_work_snapshot: createPublishWorkSnapshotHandler({
242
- homeDir,
243
- cwd,
244
- now: deps.now,
245
- writeAtomic: deps.writeAtomic,
246
- publishWorkSnapshot: deps.publishWorkSnapshot,
247
- mcpResult
248
- })
263
+ kairo_publish_work_snapshot: async (args = {}) => {
264
+ if (!binding.writable) {
265
+ const code = binding.code ?? WORKSPACE_BINDING_CODES.UNBOUND;
266
+ return mcpResult({ ok: false, code, data: null, diagnostics: [code], isError: true });
267
+ }
268
+ return createPublishWorkSnapshotHandler({
269
+ homeDir,
270
+ cwd: binding.cwd,
271
+ now: deps.now,
272
+ writeAtomic: deps.writeAtomic,
273
+ publishWorkSnapshot: deps.publishWorkSnapshot,
274
+ mcpResult
275
+ })(args);
276
+ }
249
277
  };
250
278
  }
251
279
 
252
280
  export function registerKairoMcpTools(registerTool, deps = {}) {
253
281
  const h = createToolHandlers(deps);
254
- for (const [name, description, inputSchema] of [
282
+ const catalog = [
255
283
  ["kairo_status", "Read-only control-plane + companion summary", empty],
256
284
  ["kairo_runs", "Read-only run list", mcpSchemas.runs],
257
285
  ["kairo_alerts", "Read-only alert list", mcpSchemas.alerts],
@@ -265,7 +293,12 @@ export function registerKairoMcpTools(registerTool, deps = {}) {
265
293
  "Publish kairo.work-snapshot/v1 for the runtime workspace (enrolls conversation)",
266
294
  mcpSchemas.workSnapshotPublish
267
295
  ]
268
- ]) registerTool(name, { description, inputSchema }, h[name]);
296
+ ];
297
+ const bound = deps.workspaceBound === true;
298
+ for (const [name, description, inputSchema] of catalog) {
299
+ if (bound ? !KAIRO_MCP_WRITE_TOOLS.includes(name) : KAIRO_MCP_WRITE_TOOLS.includes(name)) continue;
300
+ registerTool(name, { description, inputSchema }, h[name]);
301
+ }
269
302
  return h;
270
303
  }
271
304
 
@@ -18,13 +18,15 @@ export function resolveWorkSnapshotRulePath(homeDir = homedir()) {
18
18
 
19
19
  export const WORK_SNAPSHOT_RULE_BODY = `# Kairo work snapshot
20
20
 
21
- After each significant turn, publish the true work state with MCP \`kairo_publish_work_snapshot\`:
21
+ After each significant turn, publish the true work state with MCP \`kairo_publish_work_snapshot\` **only on \`kairo-workspace\`** (\`kairo mcp --workspace-bound --cwd <absolute-folder>\`; unique \`WORKSPACE_FOLDER_PATHS\` may bind):
22
22
 
23
23
  - Required: \`conversationId\`, \`provider\` (\`cursor\`), \`goal\`, \`now\`, \`next\`
24
24
  - Optional: \`progress\` (≤3), \`blockers\`, \`delegations\` (only real ones)
25
- - Workspace identity is derived by Kairo from the runtime — never send \`projectKey\`, paths, or \`cwd\`
25
+ - Workspace identity is derived by Kairo from the bound process — never send \`projectKey\`, paths, or \`cwd\`
26
26
  - Never invent work. Never send prompts, transcripts, or tool dumps
27
27
  - Reuse the same \`conversationId\` for later turns in this chat
28
+ - If the publish tool is missing, stop. The global \`kairo\` MCP does not register it. Do not retry with paths.
29
+ - On the bound server, \`workspace_ambiguous\` or \`workspace_mismatch\` also means stop. Never retry with paths.
28
30
  `;
29
31
 
30
32
  export function buildWorkSnapshotRuleFile() {
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Explicit workspace binding for MCP writes.
3
+ * Agents never supply paths. VSCODE_CWD never authorizes writes.
4
+ * Canonical `--cwd` may match process.cwd() or the unique
5
+ * WORKSPACE_FOLDER_PATHS entry (Cursor stdio has no cwd field).
6
+ */
7
+ import { homedir } from "node:os";
8
+ import { resolve } from "node:path";
9
+ import { statSync } from "node:fs";
10
+ import { canonicalizeProjectPath, projectKeyForPath } from "../next/project-key.js";
11
+ import { parseWorkspaceFolderPaths } from "./resolve-mcp-workspace.js";
12
+
13
+ export const WORKSPACE_BINDING_CODES = Object.freeze({
14
+ UNBOUND: "workspace_unbound",
15
+ AMBIGUOUS: "workspace_ambiguous",
16
+ MISMATCH: "workspace_mismatch"
17
+ });
18
+
19
+ function fail(code, bound = false) {
20
+ return { writable: false, bound, cwd: null, projectKey: null, code };
21
+ }
22
+
23
+ function isForbiddenWriteRoot(canonical, userHome) {
24
+ const root = canonicalizeProjectPath("/");
25
+ if (canonical === root || canonical === "/") return true;
26
+ return canonical === canonicalizeProjectPath(userHome);
27
+ }
28
+
29
+ function tryCanonical(pathValue) {
30
+ try {
31
+ return canonicalizeProjectPath(pathValue);
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+
37
+ function uniqueFolderCanonical(env) {
38
+ const folders = parseWorkspaceFolderPaths(env.WORKSPACE_FOLDER_PATHS);
39
+ if (folders.length > 1) return { code: WORKSPACE_BINDING_CODES.AMBIGUOUS, canonical: null };
40
+ if (folders.length === 0) return { code: null, canonical: null };
41
+ const canonical = tryCanonical(folders[0]);
42
+ return { code: canonical ? null : WORKSPACE_BINDING_CODES.UNBOUND, canonical };
43
+ }
44
+
45
+ export function resolveWorkspaceWriteBinding({
46
+ workspaceBound = false,
47
+ cwdExplicit = false,
48
+ cwd,
49
+ processCwd = process.cwd(),
50
+ userHome = homedir(),
51
+ env = process.env
52
+ } = {}) {
53
+ if (!workspaceBound) return fail(WORKSPACE_BINDING_CODES.UNBOUND);
54
+ if (!cwdExplicit || typeof cwd !== "string" || !cwd.trim()) {
55
+ return fail(WORKSPACE_BINDING_CODES.UNBOUND, true);
56
+ }
57
+
58
+ const canonical = tryCanonical(resolve(processCwd, cwd));
59
+ if (!canonical) return fail(WORKSPACE_BINDING_CODES.UNBOUND, true);
60
+ try {
61
+ if (!statSync(canonical).isDirectory()) {
62
+ return fail(WORKSPACE_BINDING_CODES.UNBOUND, true);
63
+ }
64
+ } catch {
65
+ return fail(WORKSPACE_BINDING_CODES.UNBOUND, true);
66
+ }
67
+ if (isForbiddenWriteRoot(canonical, userHome)) {
68
+ return fail(WORKSPACE_BINDING_CODES.MISMATCH, true);
69
+ }
70
+
71
+ const folder = uniqueFolderCanonical(env);
72
+ if (folder.code) return fail(folder.code, true);
73
+
74
+ const processCanonical = tryCanonical(processCwd);
75
+ const matchesProcess = processCanonical != null && canonical === processCanonical;
76
+ const matchesFolder = folder.canonical != null && canonical === folder.canonical;
77
+ if (!matchesProcess && !matchesFolder) {
78
+ return fail(WORKSPACE_BINDING_CODES.MISMATCH, true);
79
+ }
80
+
81
+ return {
82
+ writable: true,
83
+ bound: true,
84
+ cwd: canonical,
85
+ projectKey: projectKeyForPath(canonical),
86
+ code: null
87
+ };
88
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * VSIX write MCP: publish only. Global/unbound reads stay on `kairo mcp`.
3
+ */
4
+ import { McpServer } from "@modelcontextprotocol/server";
5
+ import { serveStdio } from "@modelcontextprotocol/server/stdio";
6
+ import {
7
+ createPublishWorkSnapshotHandler,
8
+ workSnapshotPublishSchema
9
+ } from "./work-snapshot-tool.js";
10
+ import {
11
+ WORKSPACE_BINDING_CODES,
12
+ resolveWorkspaceWriteBinding
13
+ } from "./workspace-binding.js";
14
+
15
+ export function parseWorkspaceMcpArgv(argv = []) {
16
+ const args = [...argv];
17
+ if (args[0] === "mcp") args.shift();
18
+ let workspaceBound = false;
19
+ let cwd;
20
+ let cwdExplicit = false;
21
+ for (let i = 0; i < args.length; i += 1) {
22
+ if (args[i] === "--workspace-bound") workspaceBound = true;
23
+ else if (args[i] === "--cwd") {
24
+ cwd = args[++i];
25
+ cwdExplicit = true;
26
+ }
27
+ }
28
+ return { workspaceBound, cwd, cwdExplicit };
29
+ }
30
+
31
+ function mcpResult({ ok, code, data = null, diagnostics = [], isError = false }) {
32
+ const structuredContent = { ok, code, data, diagnostics };
33
+ return {
34
+ content: [{ type: "text", text: JSON.stringify(structuredContent) }],
35
+ structuredContent, ...(isError ? { isError: true } : {})
36
+ };
37
+ }
38
+
39
+ export function startWorkspaceMcp(argv = process.argv.slice(2), deps = {}) {
40
+ const parsed = parseWorkspaceMcpArgv(argv);
41
+ const merged = { ...deps, ...parsed };
42
+ const binding = resolveWorkspaceWriteBinding({
43
+ workspaceBound: merged.workspaceBound === true,
44
+ cwdExplicit: merged.cwdExplicit === true,
45
+ cwd: merged.cwd,
46
+ processCwd: merged.processCwd ?? process.cwd(),
47
+ userHome: merged.userHome,
48
+ env: merged.env ?? process.env
49
+ });
50
+ const publish = createPublishWorkSnapshotHandler({
51
+ homeDir: merged.homeDir, cwd: binding.cwd, now: merged.now,
52
+ writeAtomic: merged.writeAtomic, publishWorkSnapshot: merged.publishWorkSnapshot,
53
+ mcpResult
54
+ });
55
+ const serve = deps.serveStdio ?? serveStdio;
56
+ return serve(() => {
57
+ const server = new (deps.McpServer ?? McpServer)({ name: "kairo-workspace", version: "0.8.0" });
58
+ const register = deps.registerTool ?? ((name, config, handler) => server.registerTool(name, config, handler));
59
+ register("kairo_publish_work_snapshot", {
60
+ description: "Publish kairo.work-snapshot/v1 for the runtime workspace (enrolls conversation)",
61
+ inputSchema: workSnapshotPublishSchema
62
+ }, async (args = {}) => {
63
+ if (!binding.writable) {
64
+ const code = binding.code ?? WORKSPACE_BINDING_CODES.UNBOUND;
65
+ return mcpResult({ ok: false, code, data: null, diagnostics: [code], isError: true });
66
+ }
67
+ return publish(args);
68
+ });
69
+ return server;
70
+ });
71
+ }
72
+
73
+ const invoked = process.argv[1] ?? "";
74
+ if (/(?:kairo-workspace\.cjs|workspace-mcp-entry\.js)$/.test(invoked)) void startWorkspaceMcp();
@@ -227,7 +227,14 @@ export async function runMcpCli(options = {}) {
227
227
  if (action === "serve" || action == null) {
228
228
  const { runKairoMcp } = await import("./mcp/kairo-mcp.js");
229
229
  return runKairoMcp({
230
- cwd: resolveMcpServeCwd(options),
230
+ cwd: resolveMcpServeCwd(options) ?? options.cwd,
231
+ cwdExplicit: options.cwdExplicit === true,
232
+ workspaceBound: options.workspaceBound === true,
233
+ processCwd: options.processCwd ?? process.cwd(),
234
+ homeDir: options.homeDir,
235
+ env: options.env,
236
+ registerTool: options.registerTool,
237
+ serveStdio: options.serveStdio,
231
238
  packageRoot: options.packageRoot,
232
239
  packageName: options.packageName,
233
240
  version: options.version
@@ -0,0 +1,118 @@
1
+ // Real per-model benchmark scores from Artificial Analysis's official Data
2
+ // API (verified live against https://artificialanalysis.ai/api/v2/data/llms/models
3
+ // — not scraped from their leaderboard website, which has no stable JSON
4
+ // endpoint). Fails closed like every other observability probe: no API key
5
+ // or a failed fetch never fabricates a score, it falls back to the last
6
+ // successfully cached snapshot (marked "cached", with its real age) or
7
+ // "unknown" if there's no cache either.
8
+
9
+ import { mkdir, readFile } from "node:fs/promises";
10
+ import { dirname } from "node:path";
11
+ import { harnessHomePaths } from "../paths.js";
12
+ import { writeAtomicJson } from "../runtime/write-atomic-json.js";
13
+
14
+ export const SOURCE = "artificial-analysis api v2 (data/llms/models)";
15
+ const API_URL = "https://artificialanalysis.ai/api/v2/data/llms/models";
16
+ const DEFAULT_TIMEOUT_MS = 8000;
17
+
18
+ function normalizeModel(entry) {
19
+ const evaluations = entry.evaluations ?? {};
20
+ return {
21
+ slug: entry.slug,
22
+ name: entry.name,
23
+ creator: entry.model_creator?.slug ?? null,
24
+ intelligenceIndex: evaluations.artificial_analysis_intelligence_index ?? null,
25
+ codingIndex: evaluations.artificial_analysis_coding_index ?? null,
26
+ mathIndex: evaluations.artificial_analysis_math_index ?? null,
27
+ // Real per-benchmark scores the free API already returns alongside the
28
+ // composite indices above — verified live (not documented anywhere as
29
+ // a free-tier feature, an earlier assumption here was wrong). These
30
+ // matter because the composite intelligenceIndex can blur a real,
31
+ // benchmark-specific near-tie: e.g. two models 18% apart on the
32
+ // composite can be 0.2 points apart on GPQA specifically. Real,
33
+ // 0-1 scale values as AA reports them — never rescaled or blended.
34
+ gpqa: evaluations.gpqa ?? null,
35
+ hle: evaluations.hle ?? null,
36
+ sciCode: evaluations.scicode ?? null,
37
+ mmluPro: evaluations.mmlu_pro ?? null,
38
+ liveCodeBench: evaluations.livecodebench ?? null,
39
+ ifBench: evaluations.ifbench ?? null,
40
+ terminalBenchHard: evaluations.terminalbench_hard ?? null,
41
+ terminalBenchV2: evaluations.terminalbench_v2_1 ?? null,
42
+ tau2: evaluations.tau2 ?? null,
43
+ tauBanking: evaluations.tau_banking ?? null,
44
+ // Real reported numbers, not derived scores — used to flag the
45
+ // cheapest/fastest real option among what you actually have access to,
46
+ // never blended into a single invented composite.
47
+ priceInputPerMTok: entry.pricing?.price_1m_input_tokens ?? null,
48
+ priceOutputPerMTok: entry.pricing?.price_1m_output_tokens ?? null,
49
+ outputTokensPerSecond: entry.median_output_tokens_per_second ?? null
50
+ };
51
+ }
52
+
53
+ function ageLabel(fetchedAtIso) {
54
+ const fetchedAt = new Date(fetchedAtIso ?? "").getTime();
55
+ if (!Number.isFinite(fetchedAt)) return null;
56
+ const hours = (Date.now() - fetchedAt) / 3_600_000;
57
+ if (hours < 1) return "<1h";
58
+ if (hours < 48) return `${Math.round(hours)}h`;
59
+ return `${Math.round(hours / 24)}d`;
60
+ }
61
+
62
+ async function readCache(homeDir, deps) {
63
+ const read = deps.readFile ?? readFile;
64
+ try {
65
+ const raw = await read(harnessHomePaths(homeDir).modelIntelligencePath, "utf8");
66
+ const doc = JSON.parse(raw);
67
+ if (!Array.isArray(doc?.models) || typeof doc.fetchedAt !== "string") return null;
68
+ return doc;
69
+ } catch {
70
+ return null;
71
+ }
72
+ }
73
+
74
+ async function writeCache(homeDir, doc, deps) {
75
+ const mkdirImpl = deps.mkdir ?? mkdir;
76
+ const writeJson = deps.writeAtomicJson ?? writeAtomicJson;
77
+ const path = harnessHomePaths(homeDir).modelIntelligencePath;
78
+ await mkdirImpl(dirname(path), { recursive: true });
79
+ await writeJson(path, doc);
80
+ }
81
+
82
+ function fromCache(cache, error) {
83
+ if (!cache) return { status: "unknown", source: SOURCE, fetchedAt: null, age: null, models: [], error };
84
+ return { status: "cached", source: SOURCE, fetchedAt: cache.fetchedAt, age: ageLabel(cache.fetchedAt), models: cache.models, error };
85
+ }
86
+
87
+ /**
88
+ * @param {object} [options]
89
+ * @param {string|null} [options.apiKey] - defaults to ARTIFICIAL_ANALYSIS_API_KEY; never hardcode a key
90
+ * @param {string} options.homeDir - required; the cache lives under this harness home
91
+ * @param {typeof fetch} [options.fetchImpl]
92
+ * @param {number} [options.timeoutMs]
93
+ */
94
+ export async function readArtificialAnalysisModels({
95
+ apiKey = process.env.ARTIFICIAL_ANALYSIS_API_KEY ?? null,
96
+ homeDir,
97
+ fetchImpl = fetch,
98
+ timeoutMs = DEFAULT_TIMEOUT_MS,
99
+ ...deps
100
+ } = {}) {
101
+ if (!apiKey) return fromCache(await readCache(homeDir, deps), "no API key configured");
102
+
103
+ const controller = new AbortController();
104
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
105
+ try {
106
+ const response = await fetchImpl(API_URL, { headers: { "x-api-key": apiKey }, signal: controller.signal });
107
+ if (!response.ok) throw new Error(`artificial analysis api returned ${response.status}`);
108
+ const payload = await response.json();
109
+ const models = Array.isArray(payload?.data) ? payload.data.map(normalizeModel) : [];
110
+ const fetchedAt = new Date().toISOString();
111
+ await writeCache(homeDir, { fetchedAt, models }, deps).catch(() => {});
112
+ return { status: "live", source: SOURCE, fetchedAt, age: "<1h", models, error: null };
113
+ } catch (error) {
114
+ return fromCache(await readCache(homeDir, deps), error?.message ?? String(error));
115
+ } finally {
116
+ clearTimeout(timer);
117
+ }
118
+ }
@@ -0,0 +1,31 @@
1
+ // Claude Code's CLI has no model-discovery command (`claude --help` exposes
2
+ // no `models` subcommand, unlike Codex's `model/list` RPC or OpenCode's
3
+ // `models` command) — verified by inspecting its full --help output, not
4
+ // assumed. So there is no live, per-account catalog to read.
5
+ //
6
+ // This is the documented model catalog only — current as of this file's
7
+ // last update — NOT a live entitlement check. It intentionally carries
8
+ // `status: "documented"` (never "measured") so callers can't mistake it for
9
+ // verified data the way Codex/OpenCode/Cursor's catalogs are.
10
+ const SOURCE = "documented catalog (no live discovery command exists for claude)";
11
+
12
+ const DOCUMENTED_MODELS = [
13
+ { id: "claude-opus-5", displayName: "Claude Opus 5" },
14
+ { id: "claude-sonnet-5", displayName: "Claude Sonnet 5" },
15
+ { id: "claude-haiku-4-5", displayName: "Claude Haiku 4.5" },
16
+ { id: "claude-fable-5-1", displayName: "Claude Fable 5.1" },
17
+ { id: "claude-fable-5", displayName: "Claude Fable 5" },
18
+ { id: "claude-opus-4-8", displayName: "Claude Opus 4.8" },
19
+ { id: "claude-opus-4-7", displayName: "Claude Opus 4.7" },
20
+ { id: "claude-opus-4-6", displayName: "Claude Opus 4.6" },
21
+ { id: "claude-sonnet-4-6", displayName: "Claude Sonnet 4.6" }
22
+ ];
23
+
24
+ /**
25
+ * Returns the documented Claude model catalog. Always synchronous and
26
+ * always `status: "documented"` — there is nothing to fail closed on since
27
+ * no live read is attempted.
28
+ */
29
+ export function readClaudeModels() {
30
+ return { status: "documented", source: SOURCE, models: DOCUMENTED_MODELS.slice(), error: null };
31
+ }
@@ -0,0 +1,112 @@
1
+ import { spawn as defaultSpawn } from "node:child_process";
2
+
3
+ // Claude Code's `/usage` is a local_command — intercepted client-side before
4
+ // it reaches the model, so `claude -p "/usage" --output-format json` returns
5
+ // real session/weekly percentages at zero cost (total_cost_usd: 0, all token
6
+ // counts 0). This mirrors codex-usage.js's app-server approach: a real,
7
+ // zero-cost, local read, never a fabricated quota.
8
+ const SOURCE = 'claude -p "/usage" (local_command, zero-cost)';
9
+ const DEFAULT_TIMEOUT_MS = 15_000;
10
+
11
+ function unknown(error = null) {
12
+ return {
13
+ status: "unknown",
14
+ source: SOURCE,
15
+ windows: [],
16
+ primary: null,
17
+ secondary: null,
18
+ raw: null,
19
+ error: error ? String(error) : null
20
+ };
21
+ }
22
+
23
+ /**
24
+ * Parses lines like:
25
+ * "Current session: 0% used · resets Sep 11 at 7:09pm (America/Mexico_City)"
26
+ * "Current week (all models): 7% used · resets Sep 13 at 7:59am (America/Mexico_City)"
27
+ * into normalized usage windows. Unrecognized lines are skipped, not guessed.
28
+ */
29
+ export function parseClaudeUsageText(text) {
30
+ const windows = [];
31
+ for (const line of String(text ?? "").split("\n")) {
32
+ const match = line.match(/^(.+?):\s*(\d+)%\s*used(?:\s*·\s*resets\s*(.+))?\s*$/);
33
+ if (!match) continue;
34
+ const usedPercent = Number(match[2]);
35
+ if (!Number.isFinite(usedPercent)) continue;
36
+ windows.push({
37
+ label: match[1].trim(),
38
+ usedPercent,
39
+ remainingPercent: 100 - usedPercent,
40
+ resetsAt: match[3] ? match[3].trim() : null
41
+ });
42
+ }
43
+ return windows;
44
+ }
45
+
46
+ /**
47
+ * Reads Claude Code's own real session/weekly usage percentages via its
48
+ * `/usage` local_command, without starting a model turn. Fail-closed: any
49
+ * spawn error, timeout, malformed JSON, or unparseable response yields
50
+ * `unknown`, never a fabricated percentage.
51
+ */
52
+ export async function readClaudeUsage({
53
+ spawn = defaultSpawn,
54
+ cwd = process.cwd(),
55
+ env = process.env,
56
+ timeoutMs = DEFAULT_TIMEOUT_MS
57
+ } = {}) {
58
+ let child;
59
+ try {
60
+ child = spawn("claude", ["-p", "/usage", "--output-format", "json"], {
61
+ cwd,
62
+ env,
63
+ stdio: ["ignore", "pipe", "pipe"]
64
+ });
65
+ } catch (error) {
66
+ return unknown(error?.message ?? error);
67
+ }
68
+
69
+ return new Promise((resolve) => {
70
+ let stdout = "";
71
+ let finished = false;
72
+ const timer = setTimeout(() => finish(unknown("claude -p \"/usage\" timed out")), timeoutMs);
73
+
74
+ function finish(result) {
75
+ if (finished) return;
76
+ finished = true;
77
+ clearTimeout(timer);
78
+ try { child.kill?.(); } catch { /* best effort */ }
79
+ resolve(result);
80
+ }
81
+
82
+ child.stdout?.on("data", (chunk) => { stdout += chunk; });
83
+ child.on("error", (error) => finish(unknown(error?.message ?? error)));
84
+ child.on("close", () => {
85
+ let parsed;
86
+ try {
87
+ parsed = JSON.parse(stdout);
88
+ } catch {
89
+ finish(unknown("malformed JSON from claude -p \"/usage\""));
90
+ return;
91
+ }
92
+ if (parsed?.local_command !== "usage" || typeof parsed?.result !== "string") {
93
+ finish(unknown("unexpected response shape from claude -p \"/usage\""));
94
+ return;
95
+ }
96
+ const windows = parseClaudeUsageText(parsed.result);
97
+ if (windows.length === 0) {
98
+ finish(unknown("no usage windows parsed from claude -p \"/usage\""));
99
+ return;
100
+ }
101
+ finish({
102
+ status: "measured",
103
+ source: SOURCE,
104
+ windows,
105
+ primary: windows[0] ?? null,
106
+ secondary: windows[1] ?? null,
107
+ raw: parsed.result,
108
+ error: null
109
+ });
110
+ });
111
+ });
112
+ }