@addai/node 0.30.12 → 0.30.14

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 (143) hide show
  1. package/dist/auto-update.d.ts +3 -0
  2. package/dist/auto-update.js +15 -0
  3. package/dist/capabilities.d.ts +31 -6
  4. package/dist/capabilities.js +48 -26
  5. package/dist/claude-models.d.ts +71 -0
  6. package/dist/claude-models.js +330 -0
  7. package/dist/claude-print.d.ts +15 -0
  8. package/dist/claude-print.js +16 -0
  9. package/dist/codex-models.d.ts +81 -0
  10. package/dist/codex-models.js +385 -0
  11. package/dist/codex-spawn.d.ts +14 -8
  12. package/dist/codex-spawn.js +30 -10
  13. package/dist/command-runner.js +31 -9
  14. package/dist/desktop/docker.js +2 -0
  15. package/dist/desktop/manager.d.ts +2 -1
  16. package/dist/desktop/manager.js +51 -1
  17. package/dist/desktop/provider.d.ts +1 -1
  18. package/dist/desktop/provider.js +2 -0
  19. package/dist/desktop/relay-client.js +7 -0
  20. package/dist/desktop/spec.d.ts +2 -0
  21. package/dist/direct/aios-bridge.d.ts +13 -0
  22. package/dist/direct/aios-bridge.js +177 -0
  23. package/dist/direct/aios-mcp.d.ts +2 -0
  24. package/dist/direct/aios-mcp.js +132 -0
  25. package/dist/direct/aios-tools.d.ts +351 -0
  26. package/dist/direct/aios-tools.js +120 -0
  27. package/dist/direct/board-chat-provider.d.ts +64 -0
  28. package/dist/direct/board-chat-provider.js +151 -0
  29. package/dist/direct/board-chat.d.ts +40 -0
  30. package/dist/direct/board-chat.js +414 -0
  31. package/dist/direct/config.d.ts +2 -0
  32. package/dist/direct/config.js +60 -0
  33. package/dist/direct/launch.d.ts +14 -0
  34. package/dist/direct/launch.js +121 -0
  35. package/dist/direct/protocol.d.ts +46 -0
  36. package/dist/direct/protocol.js +84 -0
  37. package/dist/direct/relay-main.d.ts +1 -0
  38. package/dist/direct/relay-main.js +21 -0
  39. package/dist/direct/relay.d.ts +26 -0
  40. package/dist/direct/relay.js +334 -0
  41. package/dist/direct/sessions.d.ts +61 -0
  42. package/dist/direct/sessions.js +368 -0
  43. package/dist/direct/supervisor.d.ts +9 -0
  44. package/dist/direct/supervisor.js +137 -0
  45. package/dist/direct/terminal.d.ts +1 -0
  46. package/dist/direct/terminal.js +106 -0
  47. package/dist/direct/worker.d.ts +1 -0
  48. package/dist/direct/worker.js +278 -0
  49. package/dist/entitybox/container-action.cjs +85 -0
  50. package/dist/entitybox/container-watchdog.cjs +70 -0
  51. package/dist/entitybox/model-entry.d.ts +3 -0
  52. package/dist/entitybox/model-entry.js +233 -0
  53. package/dist/entitybox/model-mcp-proxy.d.ts +3 -0
  54. package/dist/entitybox/model-mcp-proxy.js +85 -0
  55. package/dist/entitybox-application-boundary.d.ts +59 -0
  56. package/dist/entitybox-application-boundary.js +161 -0
  57. package/dist/entitybox-application-guard-core.d.ts +44 -0
  58. package/dist/entitybox-application-guard-core.js +164 -0
  59. package/dist/entitybox-application-guard-process.d.ts +1 -0
  60. package/dist/entitybox-application-guard-process.js +83 -0
  61. package/dist/entitybox-application-guard.d.ts +46 -0
  62. package/dist/entitybox-application-guard.js +239 -0
  63. package/dist/entitybox-capacity-health-host.d.ts +12 -0
  64. package/dist/entitybox-capacity-health-host.js +80 -0
  65. package/dist/entitybox-capacity-health-worker.d.ts +22 -0
  66. package/dist/entitybox-capacity-health-worker.js +168 -0
  67. package/dist/entitybox-capacity-host.d.ts +17 -0
  68. package/dist/entitybox-capacity-host.js +155 -0
  69. package/dist/entitybox-capacity-owner-process.d.ts +1 -0
  70. package/dist/entitybox-capacity-owner-process.js +32 -0
  71. package/dist/entitybox-capacity-owner.d.ts +16 -0
  72. package/dist/entitybox-capacity-owner.js +90 -0
  73. package/dist/entitybox-capacity-programs.d.ts +7 -0
  74. package/dist/entitybox-capacity-programs.js +49 -0
  75. package/dist/entitybox-capacity-provisioner.d.ts +82 -0
  76. package/dist/entitybox-capacity-provisioner.js +404 -0
  77. package/dist/entitybox-capacity-publication.d.ts +29 -0
  78. package/dist/entitybox-capacity-publication.js +199 -0
  79. package/dist/entitybox-capacity-registration.d.ts +36 -0
  80. package/dist/entitybox-capacity-registration.js +75 -0
  81. package/dist/entitybox-capacity-worker.d.ts +29 -0
  82. package/dist/entitybox-capacity-worker.js +113 -0
  83. package/dist/entitybox-computer-task.d.ts +69 -0
  84. package/dist/entitybox-computer-task.js +176 -0
  85. package/dist/entitybox-controller-recovery.d.ts +40 -0
  86. package/dist/entitybox-controller-recovery.js +156 -0
  87. package/dist/entitybox-desktop-action-server.d.ts +34 -0
  88. package/dist/entitybox-desktop-action-server.js +176 -0
  89. package/dist/entitybox-desktop-admission.d.ts +32 -0
  90. package/dist/entitybox-desktop-admission.js +79 -0
  91. package/dist/entitybox-display-binding.d.ts +13 -0
  92. package/dist/entitybox-display-binding.js +31 -0
  93. package/dist/entitybox-execution-container.d.ts +82 -0
  94. package/dist/entitybox-execution-container.js +450 -0
  95. package/dist/entitybox-execution-host.d.ts +40 -0
  96. package/dist/entitybox-execution-host.js +168 -0
  97. package/dist/entitybox-execution-lease.d.ts +81 -0
  98. package/dist/entitybox-execution-lease.js +332 -0
  99. package/dist/entitybox-file-inputs.d.ts +51 -0
  100. package/dist/entitybox-file-inputs.js +182 -0
  101. package/dist/entitybox-file-session.d.ts +38 -0
  102. package/dist/entitybox-file-session.js +82 -0
  103. package/dist/entitybox-host-process.d.ts +12 -0
  104. package/dist/entitybox-host-process.js +120 -0
  105. package/dist/entitybox-install.d.ts +10 -0
  106. package/dist/entitybox-install.js +40 -0
  107. package/dist/entitybox-mcp-broker.d.ts +77 -0
  108. package/dist/entitybox-mcp-broker.js +420 -0
  109. package/dist/entitybox-mcp-contract.d.ts +10 -0
  110. package/dist/entitybox-mcp-contract.js +24 -0
  111. package/dist/entitybox-mcp-guard.d.ts +3 -0
  112. package/dist/entitybox-mcp-guard.js +167 -0
  113. package/dist/entitybox-model-artifact.d.ts +9 -0
  114. package/dist/entitybox-model-artifact.js +83 -0
  115. package/dist/entitybox-native-services.d.ts +33 -0
  116. package/dist/entitybox-native-services.js +172 -0
  117. package/dist/entitybox-operation.d.ts +4 -0
  118. package/dist/entitybox-operation.js +34 -0
  119. package/dist/entitybox-provider-broker.d.ts +41 -0
  120. package/dist/entitybox-provider-broker.js +149 -0
  121. package/dist/entitybox-queue-request.d.ts +32 -0
  122. package/dist/entitybox-queue-request.js +141 -0
  123. package/dist/entitybox-recovery-host.d.ts +12 -0
  124. package/dist/entitybox-recovery-host.js +80 -0
  125. package/dist/entitybox-recovery-store.d.ts +33 -0
  126. package/dist/entitybox-recovery-store.js +150 -0
  127. package/dist/entitybox-recovery-worker.d.ts +30 -0
  128. package/dist/entitybox-recovery-worker.js +134 -0
  129. package/dist/entitybox-vault-provider.d.ts +27 -0
  130. package/dist/entitybox-vault-provider.js +127 -0
  131. package/dist/harness-registry.js +3 -1
  132. package/dist/index.js +72 -38
  133. package/dist/install.d.ts +14 -2
  134. package/dist/install.js +115 -10
  135. package/dist/model-cache.d.ts +38 -0
  136. package/dist/model-cache.js +137 -0
  137. package/dist/request-pump.d.ts +3 -0
  138. package/dist/request-pump.js +21 -7
  139. package/dist/session-runner.d.ts +8 -1
  140. package/dist/session-runner.js +48 -3
  141. package/dist/supabase-client.js +4 -1
  142. package/package.json +5 -1
  143. package/scripts/copy-assets.js +4 -2
@@ -103,6 +103,9 @@ type ProbeShape = Partial<Record<HarnessId, {
103
103
  available?: boolean;
104
104
  }>>;
105
105
  export interface AutoUpdateDeps {
106
+ /** Optional admission gate for direct sessions. Legacy callers keep the existing update path. */
107
+ quiesceDirect?: () => Promise<boolean>;
108
+ releaseDirect?: () => void;
106
109
  /** The daemon token — only used to derive this node's slot. */
107
110
  token: () => string | null;
108
111
  /** True while this node holds a child process for a live run. */
@@ -143,6 +143,21 @@ function installedHarnesses(probe) {
143
143
  /** One pass. Exported so `ainode` can be made to do it on demand later, and
144
144
  * so a test can drive it without waiting an hour. */
145
145
  async function runOnce(d) {
146
+ if (!d.quiesceDirect || shouldRun({ enabled: d.enabled(), busy: d.isBusy(), breakerOpen: d.breakerOpen() })) {
147
+ return runUpdate(d);
148
+ }
149
+ if (!await d.quiesceDirect()) {
150
+ state = { ...state, last_result: 'skipped', skipped_reason: 'busy' };
151
+ return;
152
+ }
153
+ try {
154
+ await runUpdate(d);
155
+ }
156
+ finally {
157
+ d.releaseDirect?.();
158
+ }
159
+ }
160
+ async function runUpdate(d) {
146
161
  const skip = shouldRun({ enabled: d.enabled(), busy: d.isBusy(), breakerOpen: d.breakerOpen() });
147
162
  if (skip) {
148
163
  state = { ...state, last_checked_at: new Date().toISOString(), last_result: 'skipped', skipped_reason: skip };
@@ -1,4 +1,7 @@
1
+ import { entityboxFileCapabilities } from './entitybox-file-session';
1
2
  import type { EngineInfo } from './desktop/provider';
3
+ import { type ClaudeModelEntry } from './claude-models';
4
+ import { type CodexModelEntry } from './codex-models';
2
5
  import { type AutostartState } from './autostart';
3
6
  import { type AutoUpdateState } from './auto-update';
4
7
  import { type MachineSample } from './machine-metrics';
@@ -38,16 +41,15 @@ interface FlowsCapability {
38
41
  max_concurrent: number;
39
42
  }
40
43
  interface CapabilitiesShape {
44
+ entitybox?: {
45
+ files: NonNullable<ReturnType<typeof entityboxFileCapabilities>>;
46
+ };
41
47
  /** Version of @addai/node running. Lets the studio / CodeFlows
42
48
  * UIs show "outdated daemon" hints and helps us debug field issues
43
49
  * ("which version was this row produced by?"). */
44
50
  daemon_version?: string;
45
- claude: BinaryProbe & {
46
- models?: string[];
47
- };
48
- codex: BinaryProbe & {
49
- models?: string[];
50
- };
51
+ claude: BinaryProbe & ClaudeModelFields;
52
+ codex: BinaryProbe & CodexModelFields;
51
53
  kimi: BinaryProbe & {
52
54
  models?: string[];
53
55
  };
@@ -80,6 +82,29 @@ interface CapabilitiesShape {
80
82
  * version silently changes with no explanation. */
81
83
  auto_update?: AutoUpdateState;
82
84
  }
85
+ /** What the heartbeat says about claude's models. `models` keeps its old
86
+ * shape — plain ids — because Studio and Flows read it as strings; the
87
+ * newer fields sit beside it and older readers simply never look. */
88
+ interface ClaudeModelFields {
89
+ models?: string[];
90
+ /** The CLI's own entries with their names ('Opus 5.5') and alias flags.
91
+ * Built-in extras in `models` have none, and are shown by id. */
92
+ model_catalog?: ClaudeModelEntry[];
93
+ /** 'cli' when the list came from the CLI, 'builtin' when it is the
94
+ * fallback above — so a stale picker can be told apart from a live one. */
95
+ models_source?: 'cli' | 'builtin';
96
+ }
97
+ interface CodexModelFields {
98
+ /** Asked of codex's app-server (codex-models.ts); empty until it answers.
99
+ * There is no built-in list: a blank model — codex's own default — is the
100
+ * one choice that always runs. */
101
+ models?: string[];
102
+ model_catalog?: CodexModelEntry[];
103
+ models_source?: 'cli' | 'builtin';
104
+ /** 'account' when the list is what this node's login can run, 'bundled'
105
+ * when the login was left alone and it is what the CLI ships with. */
106
+ models_scope?: 'account' | 'bundled';
107
+ }
83
108
  /** Codex's credential store: $CODEX_HOME (or ~/.codex) + /auth.json. */
84
109
  export declare function codexHome(): string;
85
110
  /**
@@ -42,11 +42,14 @@ exports.codexHome = codexHome;
42
42
  exports.codexAuthFromStore = codexAuthFromStore;
43
43
  exports.probeCapabilities = probeCapabilities;
44
44
  const engine_1 = require("./desktop/engine");
45
+ const entitybox_file_session_1 = require("./entitybox-file-session");
45
46
  const child_process_1 = require("child_process");
46
47
  const fs = __importStar(require("fs"));
47
48
  const path = __importStar(require("path"));
48
49
  const os = __importStar(require("os"));
49
50
  const claude_binary_1 = require("./claude-binary");
51
+ const claude_models_1 = require("./claude-models");
52
+ const codex_models_1 = require("./codex-models");
50
53
  const kimi_binary_1 = require("./kimi-binary");
51
54
  const gemini_binary_1 = require("./gemini-binary");
52
55
  const grok_binary_1 = require("./grok-binary");
@@ -92,25 +95,21 @@ function runCmd(cmd, args, timeoutMs = 5000) {
92
95
  }
93
96
  });
94
97
  }
95
- // Curated model lists baked into the daemon. The CLIs still expose no
96
- // non-interactive "list models" command, so this stays a constant — but
97
- // WHAT goes in it is what decides whether the UI's dropdown rots.
98
+ // Curated model lists baked into the daemon.
98
99
  //
99
- // Do not "fix" this by probing the CLI. Checked against Claude Code
100
- // 2.1.266: there is no `claude models` subcommand and no `claude config
101
- // list`. Both strings are read as a PROMPT — the CLI starts a billed
102
- // session and answers in prose. Probing that on every heartbeat would
103
- // spend money several times a minute and still return nothing parseable.
100
+ // Claude's is now the FLOOR, not the source: the CLI itself is asked which
101
+ // models it offers (claude-models.ts, over the SDK control protocol), so a new
102
+ // Claude model reaches the pickers when a node's CLI updates, not when someone
103
+ // edits this file. This list is what a node reports until that answer arrives
104
+ // or when its CLI cannot give one — and its older pinned ids are kept beside
105
+ // the CLI's answer, which (asked without a login) lists only the aliases.
104
106
  //
105
- // `claude --help` documents the real contract instead: --model takes
106
- // "an alias for the latest model (e.g. 'fable', 'opus', or 'sonnet') or
107
- // a model's full name (e.g. 'claude-fable-5')".
107
+ // Do not "fix" the fallback by running `claude models` or `claude config
108
+ // list`. Neither exists (checked to 2.1.304): both strings are read as a
109
+ // PROMPT — the CLI starts a billed session and answers in prose.
108
110
  //
109
- // So the ALIASES lead the list. They are the only entries that cannot go
110
- // stale — the CLI resolves each to whatever is current in its tier — and
111
- // staleness is exactly what went wrong before: this list was still
112
- // offering claude-opus-4-1 and claude-sonnet-4-5 long after they were
113
- // superseded, and no amount of heartbeating fixes a wrong constant.
111
+ // The ALIASES lead the list. They are the only entries that cannot go
112
+ // stale — the CLI resolves each to whatever is current in its tier.
114
113
  // Pinned ids follow, newest first, for anyone who must hold one model.
115
114
  //
116
115
  // Availability is plan-gated (these nodes sign in with claude.ai
@@ -124,20 +123,17 @@ const CLAUDE_MODELS = [
124
123
  'haiku',
125
124
  'fable',
126
125
  // Pinned ids, newest first.
126
+ 'claude-opus-5-5',
127
+ 'claude-sonnet-5-5',
128
+ 'claude-haiku-5-5',
129
+ 'claude-fable-5-1',
127
130
  'claude-opus-5',
128
131
  'claude-sonnet-5',
129
- 'claude-fable-5-1',
130
132
  'claude-opus-4-8',
131
133
  'claude-opus-4-7',
132
134
  'claude-sonnet-4-6',
133
135
  'claude-haiku-4-5',
134
136
  ];
135
- const CODEX_MODELS = [
136
- 'gpt-5',
137
- 'gpt-5-mini',
138
- 'o4',
139
- 'o4-mini',
140
- ];
141
137
  const KIMI_MODELS = [
142
138
  'kimi-k2-instruct',
143
139
  'kimi-k2-base',
@@ -178,7 +174,19 @@ async function probeClaude() {
178
174
  }
179
175
  catch { /* fall through unauthed */ }
180
176
  }
181
- return { available: true, version, models: CLAUDE_MODELS, ...authShape };
177
+ // Asked once per binary and version — the probe never sees the login, so
178
+ // an account change cannot move its answer. Never waits; see
179
+ // claude-models.ts. No default model is reported: a CLI asked without a
180
+ // login cannot say what this node's account defaults to.
181
+ const live = (0, claude_models_1.claudeModelsFor)(bin, `${bin}|${version ?? ''}`);
182
+ return {
183
+ available: true,
184
+ version,
185
+ models: live ? (0, claude_models_1.withBuiltin)(live, CLAUDE_MODELS).models : CLAUDE_MODELS,
186
+ ...(live ? { model_catalog: live.catalog } : {}),
187
+ models_source: live ? 'cli' : 'builtin',
188
+ ...authShape,
189
+ };
182
190
  }
183
191
  /** Codex's credential store: $CODEX_HOME (or ~/.codex) + /auth.json. */
184
192
  function codexHome() {
@@ -245,7 +253,18 @@ async function probeCodex() {
245
253
  else {
246
254
  authShape = codexAuthFromStore(codexHome());
247
255
  }
248
- return { available: true, version: ver.stdout, models: CODEX_MODELS, ...authShape };
256
+ // Asked once per binary and version, in the background; see codex-models.ts.
257
+ // Its efforts replace the registry's when it answers — they are per model,
258
+ // and newer codex takes xhigh and max.
259
+ const live = (0, codex_models_1.codexModelsFor)(bin, `${bin}|${ver.stdout}`);
260
+ return {
261
+ available: true,
262
+ version: ver.stdout,
263
+ models: live?.models ?? [],
264
+ ...(live ? { model_catalog: live.catalog, models_scope: live.scope, efforts: (0, codex_models_1.offeredEfforts)(live) } : {}),
265
+ models_source: live ? 'cli' : 'builtin',
266
+ ...authShape,
267
+ };
249
268
  }
250
269
  async function probeKimi() {
251
270
  const bin = (0, kimi_binary_1.findKimiBinary)();
@@ -475,7 +494,8 @@ async function probeCapabilities() {
475
494
  // login strategy) so Studio and the TUI render from one source of truth.
476
495
  const deco = (id, probe) => ({
477
496
  ...probe,
478
- efforts: harness_registry_1.HARNESSES[id].efforts,
497
+ // A list the CLI gave itself (codex) beats the registry's.
498
+ efforts: probe.efforts?.length ? probe.efforts : harness_registry_1.HARNESSES[id].efforts,
479
499
  installable: (0, harness_registry_1.isInstallable)(id),
480
500
  login_strategy: harness_registry_1.HARNESSES[id].login.strategy,
481
501
  login_methods: [
@@ -483,7 +503,9 @@ async function probeCapabilities() {
483
503
  ...(harness_registry_1.HARNESSES[id].extraLogins ?? []).map(m => m.method),
484
504
  ],
485
505
  });
506
+ const fileCapability = (0, entitybox_file_session_1.entityboxFileCapabilities)(claude);
486
507
  return {
508
+ ...(fileCapability ? { entitybox: { files: fileCapability } } : {}),
487
509
  daemon_version: readDaemonVersion(),
488
510
  autostart: (0, autostart_1.status)(),
489
511
  container_engine: containerEngine,
@@ -0,0 +1,71 @@
1
+ import { type ModelProbeResult } from './model-cache';
2
+ import { type CliInvocation } from './win';
3
+ export interface ClaudeModelEntry {
4
+ /** What goes to `--model`: an alias ('opus') or a full id. */
5
+ id: string;
6
+ /** What the CLI runs for it today — the same as `id` for a full id. */
7
+ resolved: string;
8
+ /** The CLI's own label, e.g. 'Opus 5.5'. */
9
+ name: string;
10
+ /** An alias names a tier, not a version, and moves when the CLI updates. */
11
+ alias: boolean;
12
+ /** Effort levels this model takes, when the CLI says. */
13
+ efforts?: string[];
14
+ }
15
+ export interface ClaudeModelReport {
16
+ /** Plain ids, aliases first — the shape `capabilities.claude.models` has
17
+ * always had. Studio and Flows read it as strings; keep it that way. */
18
+ models: string[];
19
+ /** The same models with names, for the UIs that know this field. */
20
+ catalog: ClaudeModelEntry[];
21
+ /** What a run with no model set gets — for a logged-OUT CLI, which is all
22
+ * the probe ever sees, so it is not reported as this node's default. */
23
+ defaultModel?: string;
24
+ }
25
+ export type ProbeResult = ModelProbeResult<ClaudeModelReport>;
26
+ /**
27
+ * Turn the `models` array of an initialize response into a report.
28
+ *
29
+ * Every alias also gets a pinned entry for what it resolves to. The CLI lists
30
+ * `opus` → claude-opus-5-5 but no `claude-opus-5-5` of its own, and without one
31
+ * nobody could hold an entity on Opus 5.5 once `opus` moves on.
32
+ *
33
+ * `default` is left out of `models`: a blank model already means the default,
34
+ * and nobody has tested what `--model default` does.
35
+ *
36
+ * Returns null when nothing usable is in there.
37
+ */
38
+ export declare function parseInitializeModels(raw: unknown): ClaudeModelReport | null;
39
+ /**
40
+ * What the heartbeat reports: the CLI's models first, then any built-in id it
41
+ * did not mention. A logged-out CLI lists only its aliases, and dropping the
42
+ * older pinned ids (Opus 4.8, Sonnet 4.6, …) from every picker would take
43
+ * options away from people who use them. The built-in extras carry no catalog
44
+ * entry — a UI shows them by id, exactly as it always has.
45
+ */
46
+ export declare function withBuiltin(report: ClaudeModelReport, builtin: readonly string[]): ClaudeModelReport;
47
+ /**
48
+ * Ask one Claude binary for its models. Never throws; resolves within
49
+ * `timeoutMs` however the CLI behaves, and leaves no process behind.
50
+ */
51
+ export declare function probeClaudeModels(bin: string, opts?: {
52
+ timeoutMs?: number;
53
+ /** Tests only: run something else in place of the CLI. */
54
+ resolve?: (bin: string, args: string[]) => CliInvocation;
55
+ /** Tests only: where the throwaway folder goes. */
56
+ dir?: string;
57
+ }): Promise<ProbeResult>;
58
+ /** Tests only. */
59
+ export declare function __resetIsolationForTests(): void;
60
+ export interface ModelCacheDeps {
61
+ probe: (bin: string) => Promise<ProbeResult>;
62
+ now: () => number;
63
+ log: (line: string) => void;
64
+ }
65
+ /** Refreshed every 6h, retried 30 min after a failure — see model-cache.ts. */
66
+ export declare function createClaudeModelCache(deps: ModelCacheDeps): {
67
+ get(bin: string, key: string): ClaudeModelReport | null;
68
+ last(): ClaudeModelReport | null;
69
+ };
70
+ /** What capabilities.ts reports for claude's models. */
71
+ export declare function claudeModelsFor(bin: string, key: string): ClaudeModelReport | null;
@@ -0,0 +1,330 @@
1
+ "use strict";
2
+ // Which models the installed Claude Code offers — asked of the CLI itself.
3
+ //
4
+ // Until 0.30.13 this was only the CLAUDE_MODELS constant in capabilities.ts,
5
+ // and a constant is only as current as the last release: Opus 5.5 shipped and
6
+ // every node went on offering Opus 5, because nobody had edited the list.
7
+ //
8
+ // The CLI has no `claude models` command (still true in 2.1.304, and anything
9
+ // it does not recognise is read as a PROMPT, which is billed). But it does
10
+ // answer the SDK control protocol: start it in stream-json mode, send the
11
+ // `initialize` control request, and the response carries `models` — the list
12
+ // its own /model picker shows, with what each alias resolves to today. No user
13
+ // message is ever sent, so nothing is billed.
14
+ //
15
+ // ⚠️ THE PROBE MUST NEVER SEE THE NODE'S LOGIN. A Claude process that starts
16
+ // with an expired access token refreshes it, which rotates the refresh token;
17
+ // one that exits a second later — as a probe does — can lose the rotated token,
18
+ // and the next process finds a dead login and wipes it. That logged popadom's
19
+ // Claude out on 2026-10-20 while this was being built. So the probe runs with
20
+ // a throwaway CLAUDE_CONFIG_DIR, `--bare` (no keychain reads — the macOS
21
+ // login lives there, not in the config dir), and no credential env vars. The
22
+ // CLI then reports `tokenSource: "none"` and still answers, with the
23
+ // account-independent list: the aliases and what they resolve to.
24
+ //
25
+ // 2.1.278 `opus[1m]` → claude-opus-5[1m], `sonnet` → claude-sonnet-5
26
+ // 2.1.304 `opus` → claude-opus-5-5, `sonnet` → claude-sonnet-5-5
27
+ //
28
+ // If a CLI ever answers WITH a login despite all that, isolation has failed:
29
+ // this process stops probing for good and says so in the log.
30
+ //
31
+ // A probe costs a CLI start (~1s) and the heartbeat runs every 30s, so the
32
+ // answer is cached per (binary, version) and the heartbeat never waits for it:
33
+ // the first beat after an install or upgrade still reports what it had, kicks
34
+ // the probe off in the background, and the next beat carries the answer. A
35
+ // probe that fails — an old CLI, a protocol change — leaves the built-in list
36
+ // in place (or the last answer that same binary gave) and is not retried for
37
+ // RETRY_AFTER_FAIL_MS, so a broken CLI never costs a spawn every 30 seconds.
38
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
39
+ if (k2 === undefined) k2 = k;
40
+ var desc = Object.getOwnPropertyDescriptor(m, k);
41
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
42
+ desc = { enumerable: true, get: function() { return m[k]; } };
43
+ }
44
+ Object.defineProperty(o, k2, desc);
45
+ }) : (function(o, m, k, k2) {
46
+ if (k2 === undefined) k2 = k;
47
+ o[k2] = m[k];
48
+ }));
49
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
50
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
51
+ }) : function(o, v) {
52
+ o["default"] = v;
53
+ });
54
+ var __importStar = (this && this.__importStar) || (function () {
55
+ var ownKeys = function(o) {
56
+ ownKeys = Object.getOwnPropertyNames || function (o) {
57
+ var ar = [];
58
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
59
+ return ar;
60
+ };
61
+ return ownKeys(o);
62
+ };
63
+ return function (mod) {
64
+ if (mod && mod.__esModule) return mod;
65
+ var result = {};
66
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
67
+ __setModuleDefault(result, mod);
68
+ return result;
69
+ };
70
+ })();
71
+ Object.defineProperty(exports, "__esModule", { value: true });
72
+ exports.parseInitializeModels = parseInitializeModels;
73
+ exports.withBuiltin = withBuiltin;
74
+ exports.probeClaudeModels = probeClaudeModels;
75
+ exports.__resetIsolationForTests = __resetIsolationForTests;
76
+ exports.createClaudeModelCache = createClaudeModelCache;
77
+ exports.claudeModelsFor = claudeModelsFor;
78
+ const child_process_1 = require("child_process");
79
+ const fs = __importStar(require("fs"));
80
+ const model_cache_1 = require("./model-cache");
81
+ const win_1 = require("./win");
82
+ // What a model id may look like before it is passed on: `claude-opus-5-5`,
83
+ // `opus[1m]`, `claude-haiku-4-5-20251001`. Anything else is dropped rather
84
+ // than shipped to every picker.
85
+ const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._:[\]-]{0,119}$/;
86
+ const MAX_ENTRIES = 64;
87
+ /** An alias carries no version number: `opus`, `sonnet`, `opus[1m]`. */
88
+ function isAlias(id) {
89
+ return !/\d/.test(id.replace(/\[[^\]]*\]$/, ''));
90
+ }
91
+ /**
92
+ * Turn the `models` array of an initialize response into a report.
93
+ *
94
+ * Every alias also gets a pinned entry for what it resolves to. The CLI lists
95
+ * `opus` → claude-opus-5-5 but no `claude-opus-5-5` of its own, and without one
96
+ * nobody could hold an entity on Opus 5.5 once `opus` moves on.
97
+ *
98
+ * `default` is left out of `models`: a blank model already means the default,
99
+ * and nobody has tested what `--model default` does.
100
+ *
101
+ * Returns null when nothing usable is in there.
102
+ */
103
+ function parseInitializeModels(raw) {
104
+ if (!Array.isArray(raw))
105
+ return null;
106
+ const aliases = [];
107
+ const pinned = [];
108
+ const derived = [];
109
+ let defaultModel;
110
+ for (const item of raw.slice(0, MAX_ENTRIES)) {
111
+ if (!item || typeof item !== 'object')
112
+ continue;
113
+ const o = item;
114
+ const id = typeof o.value === 'string' ? o.value.trim() : '';
115
+ if (!SAFE_ID.test(id))
116
+ continue;
117
+ const resolved = typeof o.resolvedModel === 'string' && SAFE_ID.test(o.resolvedModel.trim())
118
+ ? o.resolvedModel.trim() : id;
119
+ const name = typeof o.displayName === 'string' && o.displayName.trim()
120
+ ? o.displayName.trim().slice(0, 80) : id;
121
+ const efforts = Array.isArray(o.supportedEffortLevels)
122
+ ? o.supportedEffortLevels.filter((e) => typeof e === 'string' && /^[a-z]{1,20}$/.test(e))
123
+ : [];
124
+ if (id === 'default') {
125
+ if (resolved !== 'default')
126
+ defaultModel = resolved;
127
+ continue;
128
+ }
129
+ const entry = { id, resolved, name, alias: isAlias(id), ...(efforts.length ? { efforts } : {}) };
130
+ (entry.alias ? aliases : pinned).push(entry);
131
+ if (entry.alias && resolved !== id) {
132
+ derived.push({ id: resolved, resolved, name, alias: false, ...(efforts.length ? { efforts } : {}) });
133
+ }
134
+ }
135
+ // Aliases first (they never go stale), then what they point at (the newest
136
+ // models), then the CLI's own pinned list in its order.
137
+ const seen = new Set();
138
+ const catalog = [];
139
+ for (const e of [...aliases, ...derived, ...pinned]) {
140
+ if (seen.has(e.id))
141
+ continue;
142
+ seen.add(e.id);
143
+ catalog.push(e);
144
+ }
145
+ if (catalog.length === 0)
146
+ return null;
147
+ return { models: catalog.map(e => e.id), catalog, ...(defaultModel ? { defaultModel } : {}) };
148
+ }
149
+ /**
150
+ * What the heartbeat reports: the CLI's models first, then any built-in id it
151
+ * did not mention. A logged-out CLI lists only its aliases, and dropping the
152
+ * older pinned ids (Opus 4.8, Sonnet 4.6, …) from every picker would take
153
+ * options away from people who use them. The built-in extras carry no catalog
154
+ * entry — a UI shows them by id, exactly as it always has.
155
+ */
156
+ function withBuiltin(report, builtin) {
157
+ const have = new Set(report.models);
158
+ return { ...report, models: [...report.models, ...builtin.filter(id => !have.has(id))] };
159
+ }
160
+ /** Never read a login: the CLI must not see, refresh or clear one. */
161
+ const CREDENTIAL_ENV = [
162
+ 'ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN', 'CLAUDE_CODE_OAUTH_TOKEN',
163
+ 'CLAUDE_CODE_OAUTH_REFRESH_TOKEN', 'CLAUDE_CONFIG_DIR',
164
+ ];
165
+ const PROBE_ARGS = [
166
+ '--print', '--input-format', 'stream-json', '--output-format', 'stream-json', '--verbose',
167
+ // No keychain reads (the macOS login), no hooks, no plugin sync, no
168
+ // background prefetches. A CLI too old to know --bare refuses to start,
169
+ // and that node keeps the built-in list — the safe way to be wrong.
170
+ '--bare',
171
+ // No MCP servers: the handshake needs none.
172
+ '--strict-mcp-config', '--mcp-config', '{"mcpServers":{}}',
173
+ ];
174
+ const MAX_LINE = 2 * 1024 * 1024;
175
+ /** Set once a CLI answers with a login despite the isolation above. */
176
+ let isolationBroken = false;
177
+ /**
178
+ * Ask one Claude binary for its models. Never throws; resolves within
179
+ * `timeoutMs` however the CLI behaves, and leaves no process behind.
180
+ */
181
+ function probeClaudeModels(bin, opts = {}) {
182
+ return new Promise(done => {
183
+ if (isolationBroken) {
184
+ done({ ok: false, reason: 'probing is off: a CLI saw a login despite the isolation' });
185
+ return;
186
+ }
187
+ let settled = false;
188
+ let inv;
189
+ try {
190
+ inv = (opts.resolve ?? win_1.resolveCliInvocation)(bin, PROBE_ARGS);
191
+ }
192
+ catch (err) {
193
+ done({ ok: false, reason: err.message || String(err) });
194
+ return;
195
+ }
196
+ // Both the probe's cwd and its CLAUDE_CONFIG_DIR, so no project's
197
+ // settings or CLAUDE.md are read and nothing the CLI writes lands in the
198
+ // real ~/.claude.
199
+ const dir = opts.dir ?? (0, model_cache_1.makeProbeDir)();
200
+ if (!dir) {
201
+ done({ ok: false, reason: 'no folder to probe from' });
202
+ return;
203
+ }
204
+ const cleanup = () => {
205
+ if (opts.dir)
206
+ return;
207
+ try {
208
+ fs.rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 });
209
+ }
210
+ catch { /* swept later */ }
211
+ };
212
+ const env = { ...process.env, TERM: 'dumb' };
213
+ for (const k of CREDENTIAL_ENV)
214
+ delete env[k];
215
+ env.CLAUDE_CONFIG_DIR = dir;
216
+ // A probe is read-only: it must never start a self-update of the CLI.
217
+ env.DISABLE_AUTOUPDATER = '1';
218
+ let child;
219
+ try {
220
+ child = (0, child_process_1.spawn)(inv.file, inv.args, { cwd: dir, env, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
221
+ }
222
+ catch (err) {
223
+ cleanup();
224
+ done({ ok: false, reason: err.message || String(err) });
225
+ return;
226
+ }
227
+ child.once('close', cleanup);
228
+ const finish = (result) => {
229
+ if (settled)
230
+ return;
231
+ settled = true;
232
+ clearTimeout(timer);
233
+ // EOF first — in stream-json mode the CLI exits on its own when its
234
+ // input ends. The kill is the backstop for one that does not.
235
+ try {
236
+ child.stdin?.end();
237
+ }
238
+ catch { /* already closed */ }
239
+ const reap = setTimeout(() => {
240
+ if (child.exitCode === null && child.signalCode === null)
241
+ (0, win_1.killProcessTree)(child);
242
+ }, 3000);
243
+ reap.unref();
244
+ child.once('exit', () => clearTimeout(reap));
245
+ done(result);
246
+ };
247
+ const timer = setTimeout(() => {
248
+ finish({ ok: false, reason: `no answer within ${opts.timeoutMs ?? 30000}ms` });
249
+ (0, win_1.killProcessTree)(child);
250
+ }, opts.timeoutMs ?? 30000);
251
+ timer.unref();
252
+ child.on('error', err => finish({ ok: false, reason: err.message || String(err) }));
253
+ // 'close', not 'exit': close waits for stdout to drain, so an answer the
254
+ // CLI wrote just before exiting is read rather than reported missing.
255
+ child.on('close', (code, signal) => {
256
+ finish({ ok: false, reason: `exited ${code ?? signal} before answering${stderr ? `: ${stderr.trim().slice(0, 200)}` : ''}` });
257
+ });
258
+ child.stdin?.on('error', () => { });
259
+ let stderr = '';
260
+ child.stderr?.on('data', (d) => { if (stderr.length < 4096)
261
+ stderr += d.toString('utf8'); });
262
+ const requestId = `models-${process.pid}-${Date.now()}`;
263
+ let buf = '';
264
+ child.stdout?.on('data', (d) => {
265
+ buf += d.toString('utf8');
266
+ if (buf.length > MAX_LINE) {
267
+ finish({ ok: false, reason: 'oversized output' });
268
+ return;
269
+ }
270
+ let nl;
271
+ while ((nl = buf.indexOf('\n')) >= 0) {
272
+ const line = buf.slice(0, nl).trim();
273
+ buf = buf.slice(nl + 1);
274
+ if (!line.startsWith('{'))
275
+ continue;
276
+ let msg;
277
+ try {
278
+ msg = JSON.parse(line);
279
+ }
280
+ catch {
281
+ continue;
282
+ }
283
+ if (msg.type !== 'control_response')
284
+ continue;
285
+ const res = msg.response ?? {};
286
+ if (res.request_id && res.request_id !== requestId)
287
+ continue;
288
+ if (res.subtype === 'error') {
289
+ finish({ ok: false, reason: `initialize refused: ${String(res.error ?? 'unknown').slice(0, 200)}` });
290
+ return;
291
+ }
292
+ const tokenSource = res.response?.account?.tokenSource;
293
+ if (typeof tokenSource === 'string' && tokenSource !== 'none') {
294
+ isolationBroken = true;
295
+ finish({ ok: false, reason: `isolation failed — the CLI saw a login (${tokenSource}); probing is now off on this node` });
296
+ return;
297
+ }
298
+ const report = parseInitializeModels(res.response?.models);
299
+ finish(report ? { ok: true, report } : { ok: false, reason: 'initialize answered without a model list' });
300
+ return;
301
+ }
302
+ });
303
+ try {
304
+ child.stdin?.write(JSON.stringify({
305
+ type: 'control_request', request_id: requestId, request: { subtype: 'initialize' },
306
+ }) + '\n');
307
+ }
308
+ catch { /* reported via 'close' / 'error' */ }
309
+ });
310
+ }
311
+ /** Tests only. */
312
+ function __resetIsolationForTests() { isolationBroken = false; }
313
+ /** Refreshed every 6h, retried 30 min after a failure — see model-cache.ts. */
314
+ function createClaudeModelCache(deps) {
315
+ return (0, model_cache_1.createModelCache)({
316
+ ...deps,
317
+ agent: 'claude',
318
+ describe: report => report.catalog.filter(e => e.alias && e.resolved !== e.id)
319
+ .map(e => `${e.id} → ${e.resolved}`).join(', '),
320
+ });
321
+ }
322
+ const sharedCache = createClaudeModelCache({
323
+ probe: bin => probeClaudeModels(bin),
324
+ now: () => Date.now(),
325
+ log: line => console.log(line),
326
+ });
327
+ /** What capabilities.ts reports for claude's models. */
328
+ function claudeModelsFor(bin, key) {
329
+ return sharedCache.get(bin, key);
330
+ }
@@ -2,6 +2,8 @@ import type { RuntimeEvent, RuntimePermissionMode, RuntimeEffortLevel } from './
2
2
  export interface ClaudePrintInput {
3
3
  workingDirectory: string;
4
4
  prompt: string;
5
+ /** Verified original file blocks for explicitly marked EntityBox chat. */
6
+ inputBlocks?: readonly ClaudePrintInputBlock[];
5
7
  permissionMode?: RuntimePermissionMode;
6
8
  effortLevel?: RuntimeEffortLevel;
7
9
  model?: string;
@@ -13,6 +15,19 @@ export interface ClaudePrintInput {
13
15
  /** Restrict to a specific set of tools (claude only). null/undefined = inherit. */
14
16
  allowedTools?: string[];
15
17
  }
18
+ export type ClaudePrintInputBlock = {
19
+ type: 'text';
20
+ text: string;
21
+ } | {
22
+ type: 'image' | 'document';
23
+ source: {
24
+ type: 'base64';
25
+ media_type: string;
26
+ data: string;
27
+ };
28
+ };
29
+ /** Stream JSON reaches stdin only; files never become command-line arguments. */
30
+ export declare function claudePrintInputMessage(prompt: string, blocks: readonly ClaudePrintInputBlock[]): string;
16
31
  /**
17
32
  * The text to emit for one flushed block, given what came before.
18
33
  *