@jini-ai/agent-runtime 0.3.6 → 0.3.8

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 (220) hide show
  1. package/dist/claude-code-models.d.ts +63 -0
  2. package/dist/claude-code-models.js +252 -0
  3. package/dist/defs/aider.d.ts +1 -0
  4. package/dist/defs/aider.js +10 -1
  5. package/dist/defs/claude.d.ts +1 -1
  6. package/dist/defs/claude.js +25 -10
  7. package/dist/defs/opencode.d.ts +1 -0
  8. package/dist/defs/opencode.js +7 -2
  9. package/dist/defs/pi.d.ts +1 -0
  10. package/dist/defs/pi.js +12 -4
  11. package/dist/detection.d.ts +20 -0
  12. package/dist/detection.js +37 -3
  13. package/dist/providers/connection-test.js +1 -1
  14. package/dist/providers/model-catalog.js +1 -1
  15. package/dist/types.d.ts +1 -1
  16. package/package.json +5 -4
  17. package/dist/acp-model-probe.d.ts.map +0 -1
  18. package/dist/acp-model-probe.js.map +0 -1
  19. package/dist/agent-protocol/acp/account-failure.d.ts.map +0 -1
  20. package/dist/agent-protocol/acp/account-failure.js.map +0 -1
  21. package/dist/agent-protocol/acp/constants.d.ts.map +0 -1
  22. package/dist/agent-protocol/acp/constants.js.map +0 -1
  23. package/dist/agent-protocol/acp/index.d.ts.map +0 -1
  24. package/dist/agent-protocol/acp/index.js.map +0 -1
  25. package/dist/agent-protocol/acp/json.d.ts.map +0 -1
  26. package/dist/agent-protocol/acp/json.js.map +0 -1
  27. package/dist/agent-protocol/acp/models.d.ts.map +0 -1
  28. package/dist/agent-protocol/acp/models.js.map +0 -1
  29. package/dist/agent-protocol/acp/rpc.d.ts.map +0 -1
  30. package/dist/agent-protocol/acp/rpc.js.map +0 -1
  31. package/dist/agent-protocol/acp/session-params.d.ts.map +0 -1
  32. package/dist/agent-protocol/acp/session-params.js.map +0 -1
  33. package/dist/agent-protocol/acp/session.d.ts.map +0 -1
  34. package/dist/agent-protocol/acp/session.js.map +0 -1
  35. package/dist/agent-protocol/acp/text-suppression.d.ts.map +0 -1
  36. package/dist/agent-protocol/acp/text-suppression.js.map +0 -1
  37. package/dist/agent-protocol/acp/types.d.ts.map +0 -1
  38. package/dist/agent-protocol/acp/types.js.map +0 -1
  39. package/dist/agent-protocol/acp/updates.d.ts.map +0 -1
  40. package/dist/agent-protocol/acp/updates.js.map +0 -1
  41. package/dist/agent-protocol/core/index.d.ts.map +0 -1
  42. package/dist/agent-protocol/core/index.js.map +0 -1
  43. package/dist/agent-protocol/core/json-line-stream.d.ts.map +0 -1
  44. package/dist/agent-protocol/core/json-line-stream.js.map +0 -1
  45. package/dist/agent-protocol/index.d.ts.map +0 -1
  46. package/dist/agent-protocol/index.js.map +0 -1
  47. package/dist/agent-protocol/pi-rpc/events.d.ts.map +0 -1
  48. package/dist/agent-protocol/pi-rpc/events.js.map +0 -1
  49. package/dist/agent-protocol/pi-rpc/index.d.ts.map +0 -1
  50. package/dist/agent-protocol/pi-rpc/index.js.map +0 -1
  51. package/dist/agent-protocol/pi-rpc/internal.d.ts.map +0 -1
  52. package/dist/agent-protocol/pi-rpc/internal.js.map +0 -1
  53. package/dist/agent-protocol/pi-rpc/models.d.ts.map +0 -1
  54. package/dist/agent-protocol/pi-rpc/models.js.map +0 -1
  55. package/dist/agent-protocol/pi-rpc/session.d.ts.map +0 -1
  56. package/dist/agent-protocol/pi-rpc/session.js.map +0 -1
  57. package/dist/amr-model-cache.d.ts.map +0 -1
  58. package/dist/amr-model-cache.js.map +0 -1
  59. package/dist/amr-profile-resolver.d.ts.map +0 -1
  60. package/dist/amr-profile-resolver.js.map +0 -1
  61. package/dist/anthropic-live-models.d.ts.map +0 -1
  62. package/dist/anthropic-live-models.js.map +0 -1
  63. package/dist/artifact-taxonomy.d.ts.map +0 -1
  64. package/dist/artifact-taxonomy.js.map +0 -1
  65. package/dist/auth.d.ts.map +0 -1
  66. package/dist/auth.js.map +0 -1
  67. package/dist/capabilities.d.ts.map +0 -1
  68. package/dist/capabilities.js.map +0 -1
  69. package/dist/claude-stream.d.ts.map +0 -1
  70. package/dist/claude-stream.js.map +0 -1
  71. package/dist/copilot-stream.d.ts.map +0 -1
  72. package/dist/copilot-stream.js.map +0 -1
  73. package/dist/defs/aider.d.ts.map +0 -1
  74. package/dist/defs/aider.js.map +0 -1
  75. package/dist/defs/amp.d.ts.map +0 -1
  76. package/dist/defs/amp.js.map +0 -1
  77. package/dist/defs/amr.d.ts.map +0 -1
  78. package/dist/defs/amr.js.map +0 -1
  79. package/dist/defs/antigravity.d.ts.map +0 -1
  80. package/dist/defs/antigravity.js.map +0 -1
  81. package/dist/defs/claude.d.ts.map +0 -1
  82. package/dist/defs/claude.js.map +0 -1
  83. package/dist/defs/codebuddy.d.ts.map +0 -1
  84. package/dist/defs/codebuddy.js.map +0 -1
  85. package/dist/defs/codex.d.ts.map +0 -1
  86. package/dist/defs/codex.js.map +0 -1
  87. package/dist/defs/copilot.d.ts.map +0 -1
  88. package/dist/defs/copilot.js.map +0 -1
  89. package/dist/defs/cursor-agent.d.ts.map +0 -1
  90. package/dist/defs/cursor-agent.js.map +0 -1
  91. package/dist/defs/deepseek.d.ts.map +0 -1
  92. package/dist/defs/deepseek.js.map +0 -1
  93. package/dist/defs/devin.d.ts.map +0 -1
  94. package/dist/defs/devin.js.map +0 -1
  95. package/dist/defs/grok-build.d.ts.map +0 -1
  96. package/dist/defs/grok-build.js.map +0 -1
  97. package/dist/defs/hermes.d.ts.map +0 -1
  98. package/dist/defs/hermes.js.map +0 -1
  99. package/dist/defs/index.d.ts.map +0 -1
  100. package/dist/defs/index.js.map +0 -1
  101. package/dist/defs/kilo.d.ts.map +0 -1
  102. package/dist/defs/kilo.js.map +0 -1
  103. package/dist/defs/kimi.d.ts.map +0 -1
  104. package/dist/defs/kimi.js.map +0 -1
  105. package/dist/defs/kiro.d.ts.map +0 -1
  106. package/dist/defs/kiro.js.map +0 -1
  107. package/dist/defs/mimo.d.ts.map +0 -1
  108. package/dist/defs/mimo.js.map +0 -1
  109. package/dist/defs/opencode.d.ts.map +0 -1
  110. package/dist/defs/opencode.js.map +0 -1
  111. package/dist/defs/pi.d.ts.map +0 -1
  112. package/dist/defs/pi.js.map +0 -1
  113. package/dist/defs/qoder.d.ts.map +0 -1
  114. package/dist/defs/qoder.js.map +0 -1
  115. package/dist/defs/qwen.d.ts.map +0 -1
  116. package/dist/defs/qwen.js.map +0 -1
  117. package/dist/defs/reasonix.d.ts.map +0 -1
  118. package/dist/defs/reasonix.js.map +0 -1
  119. package/dist/defs/shared.d.ts.map +0 -1
  120. package/dist/defs/shared.js.map +0 -1
  121. package/dist/defs/trae-cli.d.ts.map +0 -1
  122. package/dist/defs/trae-cli.js.map +0 -1
  123. package/dist/defs/vibe.d.ts.map +0 -1
  124. package/dist/defs/vibe.js.map +0 -1
  125. package/dist/detection.d.ts.map +0 -1
  126. package/dist/detection.js.map +0 -1
  127. package/dist/diagnostics.d.ts.map +0 -1
  128. package/dist/diagnostics.js.map +0 -1
  129. package/dist/env.d.ts.map +0 -1
  130. package/dist/env.js.map +0 -1
  131. package/dist/executables.d.ts.map +0 -1
  132. package/dist/executables.js.map +0 -1
  133. package/dist/index.d.ts.map +0 -1
  134. package/dist/index.js.map +0 -1
  135. package/dist/invocation.d.ts.map +0 -1
  136. package/dist/invocation.js.map +0 -1
  137. package/dist/json-event-stream.d.ts.map +0 -1
  138. package/dist/json-event-stream.js.map +0 -1
  139. package/dist/launch.d.ts.map +0 -1
  140. package/dist/launch.js.map +0 -1
  141. package/dist/log-file.d.ts.map +0 -1
  142. package/dist/log-file.js.map +0 -1
  143. package/dist/mcp.d.ts.map +0 -1
  144. package/dist/mcp.js.map +0 -1
  145. package/dist/metadata.d.ts.map +0 -1
  146. package/dist/metadata.js.map +0 -1
  147. package/dist/mmd-routes.d.ts.map +0 -1
  148. package/dist/mmd-routes.js.map +0 -1
  149. package/dist/model-registry.d.ts.map +0 -1
  150. package/dist/model-registry.js.map +0 -1
  151. package/dist/models.d.ts.map +0 -1
  152. package/dist/models.js.map +0 -1
  153. package/dist/opencode-log.d.ts.map +0 -1
  154. package/dist/opencode-log.js.map +0 -1
  155. package/dist/paths.d.ts.map +0 -1
  156. package/dist/paths.js.map +0 -1
  157. package/dist/pi-models.d.ts.map +0 -1
  158. package/dist/pi-models.js.map +0 -1
  159. package/dist/prompt-augmenter.d.ts.map +0 -1
  160. package/dist/prompt-augmenter.js.map +0 -1
  161. package/dist/prompt-budget.d.ts.map +0 -1
  162. package/dist/prompt-budget.js.map +0 -1
  163. package/dist/prompt-file.d.ts.map +0 -1
  164. package/dist/prompt-file.js.map +0 -1
  165. package/dist/providers/aihubmix.d.ts.map +0 -1
  166. package/dist/providers/aihubmix.js.map +0 -1
  167. package/dist/providers/anthropic-messages.d.ts.map +0 -1
  168. package/dist/providers/anthropic-messages.js.map +0 -1
  169. package/dist/providers/azure-chat.d.ts.map +0 -1
  170. package/dist/providers/azure-chat.js.map +0 -1
  171. package/dist/providers/connection-guard.d.ts.map +0 -1
  172. package/dist/providers/connection-guard.js.map +0 -1
  173. package/dist/providers/connection-test.d.ts.map +0 -1
  174. package/dist/providers/connection-test.js.map +0 -1
  175. package/dist/providers/elevenlabs.d.ts.map +0 -1
  176. package/dist/providers/elevenlabs.js.map +0 -1
  177. package/dist/providers/google-messages.d.ts.map +0 -1
  178. package/dist/providers/google-messages.js.map +0 -1
  179. package/dist/providers/google.d.ts.map +0 -1
  180. package/dist/providers/google.js.map +0 -1
  181. package/dist/providers/index.d.ts.map +0 -1
  182. package/dist/providers/index.js.map +0 -1
  183. package/dist/providers/model-catalog.d.ts.map +0 -1
  184. package/dist/providers/model-catalog.js.map +0 -1
  185. package/dist/providers/oauth-callback-server.d.ts.map +0 -1
  186. package/dist/providers/oauth-callback-server.js.map +0 -1
  187. package/dist/providers/oauth-credentials.d.ts.map +0 -1
  188. package/dist/providers/oauth-credentials.js.map +0 -1
  189. package/dist/providers/oauth-provider.d.ts.map +0 -1
  190. package/dist/providers/oauth-provider.js.map +0 -1
  191. package/dist/providers/oauth-tokens.d.ts.map +0 -1
  192. package/dist/providers/oauth-tokens.js.map +0 -1
  193. package/dist/providers/ollama-chat.d.ts.map +0 -1
  194. package/dist/providers/ollama-chat.js.map +0 -1
  195. package/dist/providers/openai-chat.d.ts.map +0 -1
  196. package/dist/providers/openai-chat.js.map +0 -1
  197. package/dist/providers/pkce.d.ts.map +0 -1
  198. package/dist/providers/pkce.js.map +0 -1
  199. package/dist/providers/sse-decode.d.ts.map +0 -1
  200. package/dist/providers/sse-decode.js.map +0 -1
  201. package/dist/providers/token-params.d.ts.map +0 -1
  202. package/dist/providers/token-params.js.map +0 -1
  203. package/dist/providers/turn-end-guard.d.ts.map +0 -1
  204. package/dist/providers/turn-end-guard.js.map +0 -1
  205. package/dist/providers/types.d.ts.map +0 -1
  206. package/dist/providers/types.js.map +0 -1
  207. package/dist/qoder-stream.d.ts.map +0 -1
  208. package/dist/qoder-stream.js.map +0 -1
  209. package/dist/registry.d.ts.map +0 -1
  210. package/dist/registry.js.map +0 -1
  211. package/dist/resolution.d.ts.map +0 -1
  212. package/dist/resolution.js.map +0 -1
  213. package/dist/role-marker-guard.d.ts.map +0 -1
  214. package/dist/role-marker-guard.js.map +0 -1
  215. package/dist/telemetry-sink.d.ts.map +0 -1
  216. package/dist/telemetry-sink.js.map +0 -1
  217. package/dist/terminal-launch.d.ts.map +0 -1
  218. package/dist/terminal-launch.js.map +0 -1
  219. package/dist/types.d.ts.map +0 -1
  220. package/dist/types.js.map +0 -1
@@ -0,0 +1,63 @@
1
+ import type { RuntimeEnv, RuntimeModelOption } from './types.js';
2
+ /** The single stdin line that asks the CLI for its initialize payload (which carries `models`). */
3
+ export declare const CLAUDE_CODE_INITIALIZE_REQUEST = "{\"type\":\"control_request\",\"request_id\":\"jini-model-probe\",\"request\":{\"subtype\":\"initialize\"}}\n";
4
+ /**
5
+ * argv for the initialize probe.
6
+ *
7
+ * `--no-session-persistence`: no transcript is written. `--strict-mcp-config` with no `--mcp-config`:
8
+ * none of the operator's MCP servers are started. `disableAllHooks`: the operator's SessionStart
9
+ * hooks do not fire for a metadata probe (verified: without it, two user hooks ran). User settings
10
+ * are otherwise kept, because they can change what the picker offers (e.g. `availableModels`).
11
+ */
12
+ export declare const CLAUDE_CODE_INITIALIZE_ARGS: readonly string[];
13
+ /** Strips the bracketed context-window variant marker (`claude-fable-5-1[1m]`) —
14
+ * `models.ts#sanitizeCustomModel` rejects brackets, and `--model` accepts the bare name. */
15
+ export declare function normalizeClaudePickerModelId(raw: string): string;
16
+ /** Outcome of parsing `~/.claude.json`. The failure reasons are distinct so the freshness guard can
17
+ * say WHY it skipped; the runtime treats every failure as "nothing to add". */
18
+ export type ClaudeCodePickerCacheParse = {
19
+ readonly ok: true;
20
+ readonly ids: readonly string[];
21
+ } | {
22
+ readonly ok: false;
23
+ readonly reason: 'invalid-json' | 'no-cache' | 'no-usable-values';
24
+ };
25
+ /**
26
+ * Parses the raw text of `~/.claude.json` down to the model ids in `additionalModelOptionsCache`.
27
+ *
28
+ * @returns Normalized ids (bracket suffix stripped), or a typed reason. Never throws.
29
+ * @complexity O(n) over the cache entries (plus the JSON parse of the whole file).
30
+ */
31
+ export declare function parseClaudeCodePickerCache(raw: string): ClaudeCodePickerCacheParse;
32
+ /**
33
+ * Parses the CLI's stream-json stdout for the initialize `control_response` and returns its model
34
+ * rows (aliases and concrete ids, bracket suffixes stripped, deduped, `default` excluded).
35
+ *
36
+ * @returns The rows, or `null` when no well-formed response with at least one usable model exists.
37
+ * @complexity O(s + n) over the stdout length and the n models listed.
38
+ */
39
+ export declare function parseClaudeInitializeModels(stdout: string): RuntimeModelOption[] | null;
40
+ /** Where Claude Code keeps `.claude.json` for this env: `$CLAUDE_CONFIG_DIR` when set, else HOME. */
41
+ export declare function resolveClaudeConfigPath(env: RuntimeEnv): string;
42
+ /** The two I/O edges, injectable so tests never spawn a real CLI or read the real home dir. */
43
+ export interface ClaudeCodeModelIo {
44
+ /** Runs the initialize probe; resolves to whatever stdout was captured, or `null`. Never rejects. */
45
+ runInitialize(bin: string, env: RuntimeEnv): Promise<string | null>;
46
+ /** Reads a file as UTF-8, or `null` when missing/unreadable. Never rejects. */
47
+ readConfigFile(path: string): Promise<string | null>;
48
+ }
49
+ /** Test-only: install fake I/O (or `null` to restore the real one) and clear the process cache. */
50
+ export declare function setClaudeCodeModelIoForTesting(io: ClaudeCodeModelIo | null): void;
51
+ /**
52
+ * Returns the `claude` model list with the CLI's own picker catalog merged in, or `null` when there
53
+ * is nothing to merge. Needs no API key.
54
+ *
55
+ * @param bin - The resolved `claude` (or fork) binary detection found.
56
+ * @param env - The composed environment for the agent — what a spawn would actually see.
57
+ * @param fallbackModels - The def's static list; always fully present, in order, in a non-null result.
58
+ * @param now - Injectable clock for TTL tests.
59
+ * @complexity O(f + l) plus, at most once per {@link CLAUDE_CODE_MODEL_CACHE_TTL_MS} per cache key,
60
+ * one CLI spawn bounded by {@link CLAUDE_CODE_INIT_TIMEOUT_MS} and one file read.
61
+ */
62
+ export declare function loadClaudeCodeModels(bin: string, env: RuntimeEnv, fallbackModels: readonly RuntimeModelOption[], now?: () => number): Promise<RuntimeModelOption[] | null>;
63
+ //# sourceMappingURL=claude-code-models.d.ts.map
@@ -0,0 +1,252 @@
1
+ /**
2
+ * @module claude-code-models
3
+ *
4
+ * Credential-free live model discovery for the `claude` runtime def — the `claude` analogue of
5
+ * `codex debug models`.
6
+ *
7
+ * **Why this exists.** `claude` has no list-models subcommand, so without an `ANTHROPIC_API_KEY` the
8
+ * picker used to render `defs/claude.ts`'s hand-written `CLAUDE_FALLBACK_MODELS` and nothing else. A
9
+ * subscription-only install therefore never saw a new model (e.g. `claude-opus-5-5`) until someone
10
+ * edited that array and republished.
11
+ *
12
+ * **The two sources, both needing no API key** (verified live against Claude Code 2.1.280):
13
+ *
14
+ * 1. The CLI's own `/model` picker list, via the stream-json control protocol. Writing ONE
15
+ * `{"type":"control_request","request":{"subtype":"initialize"}}` line to
16
+ * `claude -p --input-format stream-json --output-format stream-json --verbose` and closing stdin
17
+ * makes the CLI answer with a `control_response` whose `response.models` is exactly the rows its
18
+ * interactive `/model` picker renders — the binary's built-in models PLUS the server-fetched
19
+ * "additional" options — then exit 0. No user message is ever sent, so no model call is made
20
+ * and nothing is billed; it authenticates with whatever the CLI itself uses (subscription OAuth
21
+ * included). This is the same `initialize` handshake the Claude Agent SDK's `supportedModels()`
22
+ * rides on.
23
+ * 2. `~/.claude.json`'s `additionalModelOptionsCache` — the server-fetched "additional" picker rows
24
+ * Claude Code persists on every session start's bootstrap fetch (headless `-p` runs included;
25
+ * it only rewrites the file when the payload changed). Only the server's extras, NOT the
26
+ * built-in rows, so it is strictly weaker than (1); it is read only when (1) yields nothing — an
27
+ * older CLI without the control protocol, a fork bin (`openclaude`), a timeout.
28
+ *
29
+ * **Enrichment, never a gate.** Every failure resolves to `null` ("nothing to add"), and a non-null
30
+ * result is always `mergeLiveModels(fallback, live)` — the static list is fully present, in order.
31
+ *
32
+ * The cache-file parser ({@link parseClaudeCodePickerCache}) is shared with
33
+ * `scripts/check-model-fallback-freshness.ts` so the freshness guard and the runtime can never
34
+ * disagree about what that cache says.
35
+ */
36
+ import { execFile } from 'node:child_process';
37
+ import { createHash } from 'node:crypto';
38
+ import { readFile } from 'node:fs/promises';
39
+ import os from 'node:os';
40
+ import { join } from 'node:path';
41
+ import { createCommandInvocation } from '@jini-ai/platform';
42
+ import { mergeLiveModels } from './anthropic-live-models.js';
43
+ import { sanitizeCustomModel } from './models.js';
44
+ /** A spawn costs ~2.5s and the picker list only moves when Claude Code updates or the server's
45
+ * "additional" options change, so one answer is trusted for this long per process. Failures are
46
+ * cached for the same window so a broken CLI is not re-spawned on every detection pass. */
47
+ const CLAUDE_CODE_MODEL_CACHE_TTL_MS = 30 * 60_000;
48
+ /** Measured ~2.5s on a warm machine (the CLI performs its bootstrap fetch before answering). */
49
+ const CLAUDE_CODE_INIT_TIMEOUT_MS = 8_000;
50
+ /** The single stdin line that asks the CLI for its initialize payload (which carries `models`). */
51
+ export const CLAUDE_CODE_INITIALIZE_REQUEST = '{"type":"control_request","request_id":"jini-model-probe","request":{"subtype":"initialize"}}\n';
52
+ /**
53
+ * argv for the initialize probe.
54
+ *
55
+ * `--no-session-persistence`: no transcript is written. `--strict-mcp-config` with no `--mcp-config`:
56
+ * none of the operator's MCP servers are started. `disableAllHooks`: the operator's SessionStart
57
+ * hooks do not fire for a metadata probe (verified: without it, two user hooks ran). User settings
58
+ * are otherwise kept, because they can change what the picker offers (e.g. `availableModels`).
59
+ */
60
+ export const CLAUDE_CODE_INITIALIZE_ARGS = [
61
+ '-p',
62
+ '--input-format', 'stream-json',
63
+ '--output-format', 'stream-json',
64
+ '--verbose',
65
+ '--no-session-persistence',
66
+ '--strict-mcp-config',
67
+ '--settings', '{"disableAllHooks":true}',
68
+ ];
69
+ /** Strips the bracketed context-window variant marker (`claude-fable-5-1[1m]`) —
70
+ * `models.ts#sanitizeCustomModel` rejects brackets, and `--model` accepts the bare name. */
71
+ export function normalizeClaudePickerModelId(raw) {
72
+ return raw.replace(/\[[^\]]*\]$/, '').trim();
73
+ }
74
+ /**
75
+ * Parses the raw text of `~/.claude.json` down to the model ids in `additionalModelOptionsCache`.
76
+ *
77
+ * @returns Normalized ids (bracket suffix stripped), or a typed reason. Never throws.
78
+ * @complexity O(n) over the cache entries (plus the JSON parse of the whole file).
79
+ */
80
+ export function parseClaudeCodePickerCache(raw) {
81
+ let parsed;
82
+ try {
83
+ parsed = JSON.parse(raw);
84
+ }
85
+ catch {
86
+ return { ok: false, reason: 'invalid-json' };
87
+ }
88
+ const cached = parsed && typeof parsed === 'object'
89
+ ? parsed.additionalModelOptionsCache
90
+ : undefined;
91
+ if (!Array.isArray(cached) || cached.length === 0)
92
+ return { ok: false, reason: 'no-cache' };
93
+ const ids = cached
94
+ .map((entry) => (entry && typeof entry === 'object' ? entry.value : null))
95
+ .filter((value) => typeof value === 'string')
96
+ .map(normalizeClaudePickerModelId)
97
+ .filter((id) => id.length > 0);
98
+ return ids.length > 0 ? { ok: true, ids } : { ok: false, reason: 'no-usable-values' };
99
+ }
100
+ /** Pulls `response.response.models` out of the one `control_response` line, or `null`. */
101
+ function findInitializeModels(stdout) {
102
+ for (const line of stdout.split('\n')) {
103
+ if (!line.includes('control_response'))
104
+ continue;
105
+ try {
106
+ const msg = JSON.parse(line);
107
+ if (msg.type !== 'control_response' || msg.response?.subtype !== 'success')
108
+ continue;
109
+ const models = msg.response.response?.models;
110
+ if (Array.isArray(models))
111
+ return models;
112
+ }
113
+ catch {
114
+ // Not a JSON line (or a truncated one on timeout) — keep scanning.
115
+ }
116
+ }
117
+ return null;
118
+ }
119
+ /** The picker rows one initialize entry contributes: its selection value (an alias like `sonnet`,
120
+ * or a full id) and the concrete model it resolves to. `default` is the sentinel the fallback list
121
+ * already carries; anything `sanitizeCustomModel` would refuse is dropped rather than rendered. */
122
+ function initializeEntryRows(entry) {
123
+ const rows = [];
124
+ const displayName = typeof entry.displayName === 'string' ? entry.displayName : '';
125
+ for (const raw of [entry.value, entry.resolvedModel]) {
126
+ if (typeof raw !== 'string')
127
+ continue;
128
+ const id = normalizeClaudePickerModelId(raw);
129
+ if (id === 'default' || sanitizeCustomModel(id) !== id)
130
+ continue;
131
+ const isAlias = !id.startsWith('claude-');
132
+ rows.push({ id, label: isAlias && displayName ? `${displayName} (alias)` : id });
133
+ }
134
+ return rows;
135
+ }
136
+ /**
137
+ * Parses the CLI's stream-json stdout for the initialize `control_response` and returns its model
138
+ * rows (aliases and concrete ids, bracket suffixes stripped, deduped, `default` excluded).
139
+ *
140
+ * @returns The rows, or `null` when no well-formed response with at least one usable model exists.
141
+ * @complexity O(s + n) over the stdout length and the n models listed.
142
+ */
143
+ export function parseClaudeInitializeModels(stdout) {
144
+ const models = findInitializeModels(String(stdout || ''));
145
+ if (!models)
146
+ return null;
147
+ const seen = new Set();
148
+ const out = [];
149
+ for (const raw of models) {
150
+ if (!raw || typeof raw !== 'object')
151
+ continue;
152
+ for (const row of initializeEntryRows(raw)) {
153
+ if (seen.has(row.id))
154
+ continue;
155
+ seen.add(row.id);
156
+ out.push(row);
157
+ }
158
+ }
159
+ return out.length > 0 ? out : null;
160
+ }
161
+ /** Where Claude Code keeps `.claude.json` for this env: `$CLAUDE_CONFIG_DIR` when set, else HOME. */
162
+ export function resolveClaudeConfigPath(env) {
163
+ const configDir = typeof env.CLAUDE_CONFIG_DIR === 'string' ? env.CLAUDE_CONFIG_DIR.trim() : '';
164
+ if (configDir)
165
+ return join(configDir, '.claude.json');
166
+ const home = typeof env.HOME === 'string' && env.HOME.trim() ? env.HOME.trim() : os.homedir();
167
+ return join(home, '.claude.json');
168
+ }
169
+ function runInitializeProbe(bin, env) {
170
+ return new Promise((resolve) => {
171
+ try {
172
+ const invocation = createCommandInvocation({ command: bin, args: [...CLAUDE_CODE_INITIALIZE_ARGS], env });
173
+ const child = execFile(invocation.command, invocation.args, {
174
+ env: env,
175
+ // Neutral cwd, same reason as `invocation.ts#execAgentFile`: a metadata probe must never
176
+ // pick up (or write into) a project directory.
177
+ cwd: os.tmpdir(),
178
+ timeout: CLAUDE_CODE_INIT_TIMEOUT_MS,
179
+ maxBuffer: 8 * 1024 * 1024,
180
+ windowsVerbatimArguments: invocation.windowsVerbatimArguments,
181
+ },
182
+ // A timeout or non-zero exit still hands back what was captured; the parser decides.
183
+ (_err, stdout) => resolve(typeof stdout === 'string' ? stdout : String(stdout ?? '')));
184
+ child.stdin?.on('error', () => { });
185
+ child.stdin?.end(CLAUDE_CODE_INITIALIZE_REQUEST);
186
+ }
187
+ catch {
188
+ resolve(null);
189
+ }
190
+ });
191
+ }
192
+ const defaultIo = {
193
+ runInitialize: runInitializeProbe,
194
+ readConfigFile: (path) => readFile(path, 'utf8').catch(() => null),
195
+ };
196
+ let activeIo = defaultIo;
197
+ const cache = new Map();
198
+ /** Test-only: install fake I/O (or `null` to restore the real one) and clear the process cache. */
199
+ export function setClaudeCodeModelIoForTesting(io) {
200
+ activeIo = io ?? defaultIo;
201
+ cache.clear();
202
+ }
203
+ /** One cache slot per binary + config location + auth-affecting env, fingerprinted so no secret is
204
+ * retained as a `Map` key. Two accounts on one host must not share a picker list. */
205
+ function cacheKeyFor(bin, env) {
206
+ const auth = ['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_BASE_URL', 'CLAUDE_CODE_OAUTH_TOKEN']
207
+ .map((name) => (typeof env[name] === 'string' ? env[name] : ''))
208
+ .join('\u0000');
209
+ const fingerprint = createHash('sha256').update(auth).digest('hex').slice(0, 16);
210
+ return `${bin}\u0000${resolveClaudeConfigPath(env)}\u0000${fingerprint}`;
211
+ }
212
+ /** Source (1), then source (2). Never rejects. */
213
+ async function discoverClaudeCodeModels(bin, env) {
214
+ try {
215
+ const stdout = await activeIo.runInitialize(bin, env);
216
+ const fromCli = stdout ? parseClaudeInitializeModels(stdout) : null;
217
+ if (fromCli)
218
+ return fromCli;
219
+ const raw = await activeIo.readConfigFile(resolveClaudeConfigPath(env));
220
+ const cached = raw === null ? null : parseClaudeCodePickerCache(raw);
221
+ if (!cached?.ok)
222
+ return null;
223
+ return cached.ids
224
+ .filter((id) => sanitizeCustomModel(id) === id)
225
+ .map((id) => ({ id, label: id }));
226
+ }
227
+ catch {
228
+ return null;
229
+ }
230
+ }
231
+ /**
232
+ * Returns the `claude` model list with the CLI's own picker catalog merged in, or `null` when there
233
+ * is nothing to merge. Needs no API key.
234
+ *
235
+ * @param bin - The resolved `claude` (or fork) binary detection found.
236
+ * @param env - The composed environment for the agent — what a spawn would actually see.
237
+ * @param fallbackModels - The def's static list; always fully present, in order, in a non-null result.
238
+ * @param now - Injectable clock for TTL tests.
239
+ * @complexity O(f + l) plus, at most once per {@link CLAUDE_CODE_MODEL_CACHE_TTL_MS} per cache key,
240
+ * one CLI spawn bounded by {@link CLAUDE_CODE_INIT_TIMEOUT_MS} and one file read.
241
+ */
242
+ export async function loadClaudeCodeModels(bin, env, fallbackModels, now = Date.now) {
243
+ const key = cacheKeyFor(bin, env);
244
+ const cached = cache.get(key);
245
+ const pending = cached && cached.expiresAt > now() ? cached.value : null;
246
+ const value = pending ?? discoverClaudeCodeModels(bin, env);
247
+ if (!pending)
248
+ cache.set(key, { expiresAt: now() + CLAUDE_CODE_MODEL_CACHE_TTL_MS, value });
249
+ const live = await value;
250
+ return live && live.length > 0 ? mergeLiveModels(fallbackModels, live) : null;
251
+ }
252
+ //# sourceMappingURL=claude-code-models.js.map
@@ -4,6 +4,7 @@ export declare const aiderAgentDef: {
4
4
  bin: string;
5
5
  versionArgs: string[];
6
6
  fallbackModels: import("../types.js").RuntimeModelOption[];
7
+ fallbackModelsAssertedAt: string;
7
8
  buildArgs: (prompt: string, _imagePaths: string[], _extra: string[] | undefined, options?: import("../types.js").RuntimeBuildOptions | undefined) => string[];
8
9
  maxPromptArgBytes: number;
9
10
  streamFormat: string;
@@ -14,10 +14,19 @@ export const aiderAgentDef = {
14
14
  fallbackModels: [
15
15
  DEFAULT_MODEL_OPTION,
16
16
  { id: 'sonnet', label: 'sonnet' },
17
- { id: 'gpt-4o', label: 'gpt-4o' },
17
+ // `gpt-4o` is stale — OpenAI's GPT-5.6 family replaced the old flagship/mini/nano naming
18
+ // with named tiers (sol/terra/luna); `sol` is the flagship `gpt-4o` mapped to, bare (no
19
+ // `openai/` prefix) to match this list's existing LiteLLM-provider-default spelling. See
20
+ // `packages/ui/src/features/execution/constants.ts`'s OpenAI preset and
21
+ // `scripts/check-model-fallback-freshness.ts`.
22
+ { id: 'gpt-5.6-sol', label: 'gpt-5.6-sol' },
18
23
  { id: 'deepseek/deepseek-chat', label: 'deepseek/deepseek-chat' },
19
24
  { id: 'gemini/gemini-2.0-flash', label: 'gemini/gemini-2.0-flash' },
20
25
  ],
26
+ // Asserted against `packages/ui/src/features/execution/constants.ts`'s current OpenAI preset
27
+ // (gpt-5.6-sol/terra/luna). See `RuntimeAgentDef.fallbackModelsAssertedAt` and
28
+ // `scripts/check-model-fallback-freshness.ts`.
29
+ fallbackModelsAssertedAt: '2026-09-16',
21
30
  // Aider's one-shot mode requires the prompt as `--message <text>` on
22
31
  // argv; neither `--message` nor `--message-file` accept `-` as a stdin
23
32
  // sentinel (it is treated as a literal filename), so we cannot pipe
@@ -17,7 +17,7 @@ export declare const claudeAgentDef: {
17
17
  };
18
18
  fallbackModels: import("../types.js").RuntimeModelOption[];
19
19
  fallbackModelsAssertedAt: string;
20
- fetchModels: (_resolvedBin: string, env: import("../types.js").RuntimeEnv) => Promise<import("../types.js").RuntimeModelOption[] | null>;
20
+ fetchModels: (resolvedBin: string, env: import("../types.js").RuntimeEnv) => Promise<import("../types.js").RuntimeModelOption[] | null>;
21
21
  reasoningOptions: {
22
22
  id: string;
23
23
  label: string;
@@ -30,7 +30,8 @@
30
30
  import { agentCapabilities } from '../capabilities.js';
31
31
  import { buildClaudeMcpConfigArgs, DEFAULT_MODEL_OPTION } from './shared.js';
32
32
  import { loadMmdRouteModels } from '../mmd-routes.js';
33
- import { loadAnthropicLiveModels } from '../anthropic-live-models.js';
33
+ import { loadAnthropicLiveModels, mergeLiveModels } from '../anthropic-live-models.js';
34
+ import { loadClaudeCodeModels } from '../claude-code-models.js';
34
35
  /**
35
36
  * The levels `claude --effort` accepts, as the CLI itself reports them when
36
37
  * given an unknown one. Anything outside this set is dropped rather than
@@ -43,8 +44,9 @@ const CLAUDE_EFFORT_LEVELS = new Set([
43
44
  ]);
44
45
  /**
45
46
  * The list a picker renders when nothing live is available — no `~/.config/mms/model-routes.json`,
46
- * no `ANTHROPIC_API_KEY` in the agent's environment, or a failed live call. Not the primary source:
47
- * see `fetchModels` below.
47
+ * the CLI's own picker catalog unreadable (see `claude-code-models.ts`), and no `ANTHROPIC_API_KEY`
48
+ * in the agent's environment or a failed live call. Not the primary source: see `fetchModels` below.
49
+ * Every live source is unioned ON TOP of this list, so it is also the floor that never shrinks.
48
50
  *
49
51
  * Ordered current-first, then the still-active-but-superseded generations, which are KEPT rather
50
52
  * than dropped — an installed CLI may be pinned to one via its own config, and removing a working
@@ -113,11 +115,14 @@ export const claudeAgentDef = {
113
115
  // 1. Local mmd/MMS routes, when `~/.config/mms/model-routes.json` exists — an explicit local
114
116
  // routing config is the operator's own authoritative statement of what this CLI can reach,
115
117
  // including proxy-backed Claude-compatible models the vendor API knows nothing about.
116
- // 2. The account's live Anthropic catalog, when `ANTHROPIC_API_KEY` is in this agent's
117
- // environment. Merged ON TOP of the static list, never in place of it.
118
- // 3. The static list, for the ordinary Local-CLI case: subscription-authenticated `claude`,
119
- // no API key anywhere. This is the common path and it must stay zero-latency, which is why
120
- // step 2 makes no network call at all when no key is resolvable.
118
+ // 2. Otherwise, the UNION of two live sources, run in parallel:
119
+ // a. The CLI's own `/model` picker catalog — NO credential needed (subscription auth
120
+ // works). See `claude-code-models.ts`: the stream-json `initialize` control request,
121
+ // falling back to `~/.claude.json`'s `additionalModelOptionsCache`. Cached per process.
122
+ // b. The account's live Anthropic catalog, when `ANTHROPIC_API_KEY` is in this agent's
123
+ // environment (makes no network call at all without a key).
124
+ // Both are merged ON TOP of the static list, never in place of it; neither shadows the other.
125
+ // 3. The static list, when neither live source answers.
121
126
  //
122
127
  // Returning `null` from any step is "nothing to add", and `detection.ts#fetchModels` renders
123
128
  // `fallbackModels` for it — so a live-discovery failure can never empty or shrink the picker.
@@ -126,8 +131,18 @@ export const claudeAgentDef = {
126
131
  // `~/.claude.json`'s server-fetched `additionalModelOptionsCache`. See
127
132
  // `RuntimeAgentDef.fallbackModelsAssertedAt` and `scripts/check-model-fallback-freshness.ts`.
128
133
  fallbackModelsAssertedAt: '2026-09-05',
129
- fetchModels: async (_resolvedBin, env) => (await loadMmdRouteModels(env, CLAUDE_FALLBACK_MODELS))
130
- ?? (await loadAnthropicLiveModels(env, CLAUDE_FALLBACK_MODELS)),
134
+ fetchModels: async (resolvedBin, env) => {
135
+ const routed = await loadMmdRouteModels(env, CLAUDE_FALLBACK_MODELS);
136
+ if (routed)
137
+ return routed;
138
+ const [fromCli, fromApi] = await Promise.all([
139
+ loadClaudeCodeModels(resolvedBin, env, CLAUDE_FALLBACK_MODELS),
140
+ loadAnthropicLiveModels(env, CLAUDE_FALLBACK_MODELS),
141
+ ]);
142
+ if (!fromCli)
143
+ return fromApi;
144
+ return fromApi ? mergeLiveModels(fromCli, fromApi) : fromCli;
145
+ },
131
146
  // `claude --effort <level>`. The set differs from codex's on both ends — it
132
147
  // has `max` and has no `none`/`minimal` — so it is spelled out rather than
133
148
  // shared, and `CLAUDE_EFFORT_LEVELS` below is what `buildArgs` validates
@@ -16,6 +16,7 @@ export declare const opencodeAgentDef: {
16
16
  timeoutMs: number;
17
17
  };
18
18
  fallbackModels: import("../types.js").RuntimeModelOption[];
19
+ fallbackModelsAssertedAt: string;
19
20
  buildArgs: (_prompt: string, _imagePaths: string[], _extra: string[] | undefined, options?: import("../types.js").RuntimeBuildOptions | undefined, runtimeContext?: import("../types.js").RuntimeContext | undefined) => string[];
20
21
  promptViaStdin: true;
21
22
  resumesSessionViaCli: true;
@@ -26,12 +26,17 @@ export const opencodeAgentDef = {
26
26
  fallbackModels: [
27
27
  DEFAULT_MODEL_OPTION,
28
28
  {
29
- id: 'anthropic/claude-sonnet-4-5',
30
- label: 'anthropic/claude-sonnet-4-5',
29
+ id: 'anthropic/claude-sonnet-5',
30
+ label: 'anthropic/claude-sonnet-5',
31
31
  },
32
32
  { id: 'openai/gpt-5', label: 'openai/gpt-5' },
33
33
  { id: 'google/gemini-2.5-pro', label: 'google/gemini-2.5-pro' },
34
34
  ],
35
+ // Checked against `packages/ui/src/features/execution/constants.ts`'s current OpenAI preset —
36
+ // `gpt-5` is a distinct, still-current generation from the newer gpt-5.6 sol/terra/luna tier
37
+ // names, so it is not stale. See `RuntimeAgentDef.fallbackModelsAssertedAt` and
38
+ // `scripts/check-model-fallback-freshness.ts`.
39
+ fallbackModelsAssertedAt: '2026-09-16',
35
40
  // OpenCode's CLI help currently exposes model selection and session
36
41
  // controls, but not an explicit per-run reasoning / effort flag. Keep
37
42
  // `reasoningOptions` undefined and do not synthesize argv for
package/dist/defs/pi.d.ts CHANGED
@@ -6,6 +6,7 @@ export declare const piAgentDef: {
6
6
  versionProbeTimeoutMs: number;
7
7
  fetchModels: (resolvedBin: string, env: import("../types.js").RuntimeEnv) => Promise<import("../types.js").RuntimeModelOption[] | null>;
8
8
  fallbackModels: import("../types.js").RuntimeModelOption[];
9
+ fallbackModelsAssertedAt: string;
9
10
  reasoningOptions: {
10
11
  id: string;
11
12
  label: string;
package/dist/defs/pi.js CHANGED
@@ -34,15 +34,23 @@ export const piAgentDef = {
34
34
  fallbackModels: [
35
35
  DEFAULT_MODEL_OPTION,
36
36
  {
37
- id: 'anthropic/claude-sonnet-4-5',
38
- label: 'Claude Sonnet 4.5 (anthropic)',
37
+ id: 'anthropic/claude-sonnet-5',
38
+ label: 'Claude Sonnet 5 (anthropic)',
39
39
  },
40
- { id: 'anthropic/claude-opus-4-5', label: 'Claude Opus 4.5 (anthropic)' },
40
+ { id: 'anthropic/claude-opus-5', label: 'Claude Opus 5 (anthropic)' },
41
41
  { id: 'openai/gpt-5', label: 'GPT-5 (openai)' },
42
- { id: 'openai/o4-mini', label: 'o4-mini (openai)' },
42
+ // `o4-mini` is stale — OpenAI's GPT-5.6 family replaced the old flagship/mini/nano naming
43
+ // with named tiers (sol/terra/luna); `terra` is the mid tier `o4-mini` mapped to. See
44
+ // `packages/ui/src/features/execution/constants.ts`'s OpenAI preset and
45
+ // `scripts/check-model-fallback-freshness.ts`.
46
+ { id: 'openai/gpt-5.6-terra', label: 'GPT-5.6 Terra (openai)' },
43
47
  { id: 'google/gemini-2.5-pro', label: 'Gemini 2.5 Pro (google)' },
44
48
  { id: 'google/gemini-2.5-flash', label: 'Gemini 2.5 Flash (google)' },
45
49
  ],
50
+ // Asserted against `packages/ui/src/features/execution/constants.ts`'s current OpenAI preset
51
+ // (gpt-5.6-sol/terra/luna). See `RuntimeAgentDef.fallbackModelsAssertedAt` and
52
+ // `scripts/check-model-fallback-freshness.ts`.
53
+ fallbackModelsAssertedAt: '2026-09-16',
46
54
  // Thinking level presets mapped to pi's --thinking flag.
47
55
  reasoningOptions: [
48
56
  { id: 'default', label: 'Default' },
@@ -1,5 +1,24 @@
1
1
  import { type AmrProfileResolver } from './amr-profile-resolver.js';
2
2
  import type { AgentDiagnostic, DetectedAgent, RuntimeAgentDef, RuntimeModelSource, RuntimeModelOption } from './types.js';
3
+ type FetchedRuntimeModels = {
4
+ models: RuntimeModelOption[];
5
+ source: RuntimeModelSource;
6
+ };
7
+ /** Result of {@link probeAgentModels}: the models to surface and where they came from. */
8
+ export type ProbedAgentModels = FetchedRuntimeModels;
9
+ /**
10
+ * The model half of {@link detectAgents} for ONE def, without the `--version`/`--help`/auth probes:
11
+ * resolves the launch path, builds the same probe env, runs the def's `fetchModels`/`listModels`,
12
+ * and refreshes the live-model validation cache exactly like `detectAgents` does.
13
+ *
14
+ * For hosts that do their own cheaper availability check but still want the live model catalog —
15
+ * without this, such a host can only ever show `fallbackModels`.
16
+ *
17
+ * @returns `source: 'live'` when the CLI supplied a list; otherwise the def's `fallbackModels` with
18
+ * `source: 'fallback'` (including when the binary is not found). Never rejects.
19
+ * @complexity At most one model-listing spawn, bounded by the def's own timeout.
20
+ */
21
+ export declare function probeAgentModels(def: RuntimeAgentDef, configuredEnv?: Record<string, string>, amrProfileResolver?: AmrProfileResolver): Promise<ProbedAgentModels>;
3
22
  export declare function detectAgents(configuredEnvByAgent?: Record<string, Record<string, string>>, amrProfileResolver?: AmrProfileResolver): Promise<(Omit<RuntimeAgentDef, "buildArgs" | "listModels" | "fetchModels" | "deriveReasoningOptions" | "fallbackModels" | "fallbackModelsAssertedAt" | "helpArgs" | "capabilityFlags" | "fallbackBins" | "versionProbeTimeoutMs" | "maxPromptArgBytes" | "env" | "inactivityTimeoutMs" | "authProbe" | "needsAgentLogFile" | "stdoutPolicy" | "runtimeLock"> & {
4
23
  models: RuntimeModelOption[];
5
24
  modelsSource: RuntimeModelSource;
@@ -11,4 +30,5 @@ export declare function detectAgents(configuredEnvByAgent?: Record<string, Recor
11
30
  diagnostics?: AgentDiagnostic[];
12
31
  })[]>;
13
32
  export declare function detectAgentsStream(configuredEnvByAgent?: Record<string, Record<string, string>>, amrProfileResolver?: AmrProfileResolver): AsyncGenerator<DetectedAgent>;
33
+ export {};
14
34
  //# sourceMappingURL=detection.d.ts.map
package/dist/detection.js CHANGED
@@ -169,6 +169,37 @@ function derivedReasoningOverride(def, models) {
169
169
  const derived = def.deriveReasoningOptions?.(models);
170
170
  return derived && derived.length > 0 ? { reasoningOptions: derived } : {};
171
171
  }
172
+ /** The env every probe of one def spawns with — the same env a real run would see. */
173
+ function probeEnvForLaunch(def, configuredEnv, launch) {
174
+ return applyAgentLaunchEnv(spawnEnvForAgent(def.id, { ...process.env, ...(def.env || {}) }, configuredEnv, undefined, {}), launch);
175
+ }
176
+ /**
177
+ * The model half of {@link detectAgents} for ONE def, without the `--version`/`--help`/auth probes:
178
+ * resolves the launch path, builds the same probe env, runs the def's `fetchModels`/`listModels`,
179
+ * and refreshes the live-model validation cache exactly like `detectAgents` does.
180
+ *
181
+ * For hosts that do their own cheaper availability check but still want the live model catalog —
182
+ * without this, such a host can only ever show `fallbackModels`.
183
+ *
184
+ * @returns `source: 'live'` when the CLI supplied a list; otherwise the def's `fallbackModels` with
185
+ * `source: 'fallback'` (including when the binary is not found). Never rejects.
186
+ * @complexity At most one model-listing spawn, bounded by the def's own timeout.
187
+ */
188
+ export async function probeAgentModels(def, configuredEnv = {}, amrProfileResolver = noopAmrProfileResolver) {
189
+ try {
190
+ const launch = resolveAgentLaunch(def, configuredEnv);
191
+ if (!launch.launchPath)
192
+ return { models: def.fallbackModels, source: 'fallback' };
193
+ const probeEnv = probeEnvForLaunch(def, configuredEnv, launch);
194
+ const fetched = await fetchModels(def, launch.launchPath, probeEnv);
195
+ const result = withRememberedAmrModels(def, probeEnv, fetched, amrProfileResolver);
196
+ rememberDetectedModelList(def, configuredEnv, result.models, amrProfileResolver);
197
+ return result;
198
+ }
199
+ catch {
200
+ return { models: def.fallbackModels, source: 'fallback' };
201
+ }
202
+ }
172
203
  async function probe(def, configuredEnv = {}, amrProfileResolver) {
173
204
  // Detection must probe the exact path the runtime will spawn, not just
174
205
  // the PATH-visible shim. This is load-bearing under nvm/fnm/mise: the
@@ -182,7 +213,7 @@ async function probe(def, configuredEnv = {}, amrProfileResolver) {
182
213
  if (!launch.selectedPath || !launch.launchPath) {
183
214
  return unavailableAgent(def, [buildExecutableDiagnostic(def, configuredEnv)]);
184
215
  }
185
- const probeEnv = applyAgentLaunchEnv(spawnEnvForAgent(def.id, { ...process.env, ...(def.env || {}) }, configuredEnv, undefined, {}), launch);
216
+ const probeEnv = probeEnvForLaunch(def, configuredEnv, launch);
186
217
  const outcome = await probeVersionAtPath(def, launch.launchPath, probeEnv);
187
218
  if (outcome.kind === 'not-invocable') {
188
219
  return unavailableAgent(def, [buildNotInvocableDiagnostic(def, launch, outcome.cause)]);
@@ -250,12 +281,15 @@ async function safeProbe(def, configuredEnv = {}, amrProfileResolver) {
250
281
  }
251
282
  }
252
283
  function rememberDetectedLiveModels(def, configuredEnv, agent, amrProfileResolver) {
253
- if (def.id === 'amr' && agent.models.length === 0)
284
+ rememberDetectedModelList(def, configuredEnv, agent.models, amrProfileResolver);
285
+ }
286
+ function rememberDetectedModelList(def, configuredEnv, models, amrProfileResolver) {
287
+ if (def.id === 'amr' && models.length === 0)
254
288
  return;
255
289
  const scope = def.id === 'amr'
256
290
  ? amrModelScopeFromEnv({ ...process.env, ...(def.env || {}), ...configuredEnv }, amrProfileResolver)
257
291
  : null;
258
- rememberLiveModels(agent.id, agent.models, scope);
292
+ rememberLiveModels(def.id, models, scope);
259
293
  }
260
294
  export async function detectAgents(configuredEnvByAgent = {}, amrProfileResolver = noopAmrProfileResolver) {
261
295
  const results = await Promise.all(AGENT_DEFS.map((def) => safeProbe(def, configuredEnvByAgent?.[def.id] ?? {}, amrProfileResolver)));
@@ -287,7 +287,7 @@ export async function testProviderConnection(input) {
287
287
  kind: 'auth_failed',
288
288
  latencyMs: Date.now() - start,
289
289
  model,
290
- detail: 'No API key — connection test needs the key from this browser.',
290
+ detail: 'No API key saved. Save one to test the connection.',
291
291
  };
292
292
  }
293
293
  const validated = await validateBaseUrlResolved(input.baseUrl, input.dnsLookup ?? defaultDnsLookup);
@@ -32,7 +32,7 @@ function missingApiKeyResponse(startedAt) {
32
32
  ok: false,
33
33
  kind: 'auth_failed',
34
34
  latencyMs: Date.now() - startedAt,
35
- detail: 'No API key — model discovery needs the key from this browser.',
35
+ detail: 'No API key saved. Save one to load models.',
36
36
  };
37
37
  }
38
38
  const BEDROCK_MODEL_OPTIONS = [
package/dist/types.d.ts CHANGED
@@ -81,7 +81,7 @@ export type RuntimeBuildOptions = {
81
81
  * expresses a deny-list — today, `claude`'s own `--disallowedTools` (verified against installed
82
82
  * Claude Code 2.1.263: `--disallowedTools, --disallowed-tools <tools...>`, "Comma or
83
83
  * space-separated list of tool names to deny"). This is a `@jini-ai/agent-runtime`-level
84
- * *mechanism* only: no product's tool policy is baked in here (not Tovu's Bash-forbid opinion,
84
+ * *mechanism* only: no product's tool policy is baked in here (not one host's Bash-forbid opinion,
85
85
  * not anyone else's) — the caller decides which names to pass. A def whose CLI has no equivalent
86
86
  * flag simply ignores this field, same as `systemPromptOverlay` on a def with no overlay
87
87
  * delivery. `undefined`/empty means no restriction — byte-identical to today's behavior (every