@mono-agent/agent-app 0.6.2 → 0.8.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 (211) hide show
  1. package/README.md +210 -19
  2. package/dist/adapter-send-proxy.d.ts +18 -0
  3. package/dist/adapter-send-proxy.d.ts.map +1 -0
  4. package/dist/adapter-send-proxy.js +229 -0
  5. package/dist/adapter-send-proxy.js.map +1 -0
  6. package/dist/adapter-send-tools-main.js +62 -4
  7. package/dist/adapter-send-tools-main.js.map +1 -1
  8. package/dist/adapter-send-tools.d.ts +33 -6
  9. package/dist/adapter-send-tools.d.ts.map +1 -1
  10. package/dist/adapter-send-tools.js +485 -87
  11. package/dist/adapter-send-tools.js.map +1 -1
  12. package/dist/app-config.d.ts.map +1 -1
  13. package/dist/app-config.js +8 -0
  14. package/dist/app-config.js.map +1 -1
  15. package/dist/app.d.ts +2 -0
  16. package/dist/app.d.ts.map +1 -1
  17. package/dist/app.js +528 -135
  18. package/dist/app.js.map +1 -1
  19. package/dist/broadcast-recorder.d.ts.map +1 -1
  20. package/dist/broadcast-recorder.js +24 -2
  21. package/dist/broadcast-recorder.js.map +1 -1
  22. package/dist/channels.d.ts.map +1 -1
  23. package/dist/channels.js +16 -6
  24. package/dist/channels.js.map +1 -1
  25. package/dist/cli.d.ts +76 -4
  26. package/dist/cli.d.ts.map +1 -1
  27. package/dist/cli.js +2102 -102
  28. package/dist/cli.js.map +1 -1
  29. package/dist/config-reference.d.ts.map +1 -1
  30. package/dist/config-reference.js +195 -6
  31. package/dist/config-reference.js.map +1 -1
  32. package/dist/configuration-proposal-main.d.ts +3 -0
  33. package/dist/configuration-proposal-main.d.ts.map +1 -0
  34. package/dist/configuration-proposal-main.js +13 -0
  35. package/dist/configuration-proposal-main.js.map +1 -0
  36. package/dist/configuration-proposal-tool.d.ts +26 -0
  37. package/dist/configuration-proposal-tool.d.ts.map +1 -0
  38. package/dist/configuration-proposal-tool.js +139 -0
  39. package/dist/configuration-proposal-tool.js.map +1 -0
  40. package/dist/configured-agent.d.ts +12 -0
  41. package/dist/configured-agent.d.ts.map +1 -1
  42. package/dist/configured-agent.js +150 -18
  43. package/dist/configured-agent.js.map +1 -1
  44. package/dist/consumer-contract.d.ts +3 -0
  45. package/dist/consumer-contract.d.ts.map +1 -1
  46. package/dist/consumer-contract.js +44 -1
  47. package/dist/consumer-contract.js.map +1 -1
  48. package/dist/doctor.d.ts +27 -5
  49. package/dist/doctor.d.ts.map +1 -1
  50. package/dist/doctor.js +1197 -101
  51. package/dist/doctor.js.map +1 -1
  52. package/dist/first-run-managed-memory.d.ts +39 -0
  53. package/dist/first-run-managed-memory.d.ts.map +1 -0
  54. package/dist/first-run-managed-memory.js +521 -0
  55. package/dist/first-run-managed-memory.js.map +1 -0
  56. package/dist/first-run-readiness.d.ts +111 -0
  57. package/dist/first-run-readiness.d.ts.map +1 -0
  58. package/dist/first-run-readiness.js +757 -0
  59. package/dist/first-run-readiness.js.map +1 -0
  60. package/dist/index.d.ts +8 -3
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +4 -1
  63. package/dist/index.js.map +1 -1
  64. package/dist/init.d.ts +88 -3
  65. package/dist/init.d.ts.map +1 -1
  66. package/dist/init.js +1212 -23
  67. package/dist/init.js.map +1 -1
  68. package/dist/interaction-bridge.d.ts +25 -1
  69. package/dist/interaction-bridge.d.ts.map +1 -1
  70. package/dist/interaction-bridge.js +281 -19
  71. package/dist/interaction-bridge.js.map +1 -1
  72. package/dist/local-configuration.d.ts +56 -0
  73. package/dist/local-configuration.d.ts.map +1 -0
  74. package/dist/local-configuration.js +914 -0
  75. package/dist/local-configuration.js.map +1 -0
  76. package/dist/memory-command.d.ts +4 -0
  77. package/dist/memory-command.d.ts.map +1 -1
  78. package/dist/memory-command.js +751 -27
  79. package/dist/memory-command.js.map +1 -1
  80. package/dist/memory-recall.d.ts +38 -10
  81. package/dist/memory-recall.d.ts.map +1 -1
  82. package/dist/memory-recall.js +82 -25
  83. package/dist/memory-recall.js.map +1 -1
  84. package/dist/memory-retrieval.d.ts +66 -0
  85. package/dist/memory-retrieval.d.ts.map +1 -0
  86. package/dist/memory-retrieval.js +332 -0
  87. package/dist/memory-retrieval.js.map +1 -0
  88. package/dist/modules/base.d.ts +6 -5
  89. package/dist/modules/base.d.ts.map +1 -1
  90. package/dist/modules/base.js +11 -7
  91. package/dist/modules/base.js.map +1 -1
  92. package/dist/modules/catalog.d.ts.map +1 -1
  93. package/dist/modules/catalog.js +54 -20
  94. package/dist/modules/catalog.js.map +1 -1
  95. package/dist/modules/index.d.ts +2 -2
  96. package/dist/modules/index.d.ts.map +1 -1
  97. package/dist/modules/index.js +1 -1
  98. package/dist/modules/index.js.map +1 -1
  99. package/dist/modules/known-tools.d.ts +3 -0
  100. package/dist/modules/known-tools.d.ts.map +1 -1
  101. package/dist/modules/known-tools.js +11 -1
  102. package/dist/modules/known-tools.js.map +1 -1
  103. package/dist/modules/types.d.ts +20 -6
  104. package/dist/modules/types.d.ts.map +1 -1
  105. package/dist/modules/types.js.map +1 -1
  106. package/dist/package-version.d.ts +3 -0
  107. package/dist/package-version.d.ts.map +1 -0
  108. package/dist/package-version.js +14 -0
  109. package/dist/package-version.js.map +1 -0
  110. package/dist/pi-auth-store-inspection.d.ts +21 -0
  111. package/dist/pi-auth-store-inspection.d.ts.map +1 -0
  112. package/dist/pi-auth-store-inspection.js +120 -0
  113. package/dist/pi-auth-store-inspection.js.map +1 -0
  114. package/dist/pi-oauth-login-main.d.ts +3 -0
  115. package/dist/pi-oauth-login-main.d.ts.map +1 -0
  116. package/dist/pi-oauth-login-main.js +14 -0
  117. package/dist/pi-oauth-login-main.js.map +1 -0
  118. package/dist/pi-oauth-login.d.ts +22 -0
  119. package/dist/pi-oauth-login.d.ts.map +1 -0
  120. package/dist/pi-oauth-login.js +79 -0
  121. package/dist/pi-oauth-login.js.map +1 -0
  122. package/dist/project-skills.d.ts +40 -0
  123. package/dist/project-skills.d.ts.map +1 -0
  124. package/dist/project-skills.js +573 -0
  125. package/dist/project-skills.js.map +1 -0
  126. package/dist/provider-setup.d.ts +117 -2
  127. package/dist/provider-setup.d.ts.map +1 -1
  128. package/dist/provider-setup.js +1505 -49
  129. package/dist/provider-setup.js.map +1 -1
  130. package/dist/readiness-probe-worker.d.ts +12 -0
  131. package/dist/readiness-probe-worker.d.ts.map +1 -0
  132. package/dist/readiness-probe-worker.js +276 -0
  133. package/dist/readiness-probe-worker.js.map +1 -0
  134. package/dist/readiness-probe.d.ts +100 -0
  135. package/dist/readiness-probe.d.ts.map +1 -0
  136. package/dist/readiness-probe.js +780 -0
  137. package/dist/readiness-probe.js.map +1 -0
  138. package/dist/request-model-override.d.ts +43 -1
  139. package/dist/request-model-override.d.ts.map +1 -1
  140. package/dist/request-model-override.js +178 -19
  141. package/dist/request-model-override.js.map +1 -1
  142. package/dist/run-history.d.ts +32 -0
  143. package/dist/run-history.d.ts.map +1 -0
  144. package/dist/run-history.js +859 -0
  145. package/dist/run-history.js.map +1 -0
  146. package/dist/runs-health.d.ts.map +1 -1
  147. package/dist/runs-health.js +12 -4
  148. package/dist/runs-health.js.map +1 -1
  149. package/dist/runtime-option-extensions.d.ts +14 -0
  150. package/dist/runtime-option-extensions.d.ts.map +1 -0
  151. package/dist/runtime-option-extensions.js +98 -0
  152. package/dist/runtime-option-extensions.js.map +1 -0
  153. package/dist/runtime-routes.d.ts +9 -0
  154. package/dist/runtime-routes.d.ts.map +1 -0
  155. package/dist/runtime-routes.js +13 -0
  156. package/dist/runtime-routes.js.map +1 -0
  157. package/dist/sandbox-manager.d.ts +69 -0
  158. package/dist/sandbox-manager.d.ts.map +1 -0
  159. package/dist/sandbox-manager.js +856 -0
  160. package/dist/sandbox-manager.js.map +1 -0
  161. package/dist/supermemory-plugin.d.ts +54 -0
  162. package/dist/supermemory-plugin.d.ts.map +1 -0
  163. package/dist/supermemory-plugin.js +136 -0
  164. package/dist/supermemory-plugin.js.map +1 -0
  165. package/dist/tui-command.d.ts +15 -0
  166. package/dist/tui-command.d.ts.map +1 -1
  167. package/dist/tui-command.js +46 -6
  168. package/dist/tui-command.js.map +1 -1
  169. package/dist/web-command.d.ts +8 -0
  170. package/dist/web-command.d.ts.map +1 -1
  171. package/dist/web-command.js +174 -17
  172. package/dist/web-command.js.map +1 -1
  173. package/dist/wizard/answers.d.ts +31 -4
  174. package/dist/wizard/answers.d.ts.map +1 -1
  175. package/dist/wizard/answers.js +145 -16
  176. package/dist/wizard/answers.js.map +1 -1
  177. package/dist/wizard/from-flags.d.ts +7 -0
  178. package/dist/wizard/from-flags.d.ts.map +1 -1
  179. package/dist/wizard/from-flags.js +36 -2
  180. package/dist/wizard/from-flags.js.map +1 -1
  181. package/dist/wizard/index.d.ts +4 -4
  182. package/dist/wizard/index.d.ts.map +1 -1
  183. package/dist/wizard/index.js +2 -2
  184. package/dist/wizard/index.js.map +1 -1
  185. package/dist/wizard/model-discovery.d.ts +33 -2
  186. package/dist/wizard/model-discovery.d.ts.map +1 -1
  187. package/dist/wizard/model-discovery.js +826 -108
  188. package/dist/wizard/model-discovery.js.map +1 -1
  189. package/dist/wizard/presets.d.ts +1 -1
  190. package/dist/wizard/presets.d.ts.map +1 -1
  191. package/dist/wizard/presets.js +7 -12
  192. package/dist/wizard/presets.js.map +1 -1
  193. package/dist/wizard/prompts.d.ts +56 -9
  194. package/dist/wizard/prompts.d.ts.map +1 -1
  195. package/dist/wizard/prompts.js +172 -20
  196. package/dist/wizard/prompts.js.map +1 -1
  197. package/dist/wizard/run.d.ts +59 -3
  198. package/dist/wizard/run.d.ts.map +1 -1
  199. package/dist/wizard/run.js +1133 -191
  200. package/dist/wizard/run.js.map +1 -1
  201. package/package.json +21 -16
  202. package/resources/srt/package-lock.json +66 -0
  203. package/resources/srt/package.json +9 -0
  204. package/schema/mono-agent.config.schema.json +381 -17
  205. package/skills/mono-agent-composer/SKILL.md +4 -4
  206. package/skills/mono-agent-composer/references/config-blueprint.md +5 -4
  207. package/skills/mono-agent-composer/references/discovery-questions.md +8 -8
  208. package/skills/mono-agent-composer/references/feature-coverage.md +8 -6
  209. package/skills/mono-agent-composer/references/package-map.md +1 -1
  210. package/skills/mono-agent-composer/references/playbooks.md +8 -7
  211. package/skills/mono-agent-composer/references/validation.md +1 -1
@@ -1,43 +1,94 @@
1
- import { readFile } from "node:fs/promises";
2
- import { homedir } from "node:os";
1
+ import { spawn } from "node:child_process";
2
+ import { chmod, mkdir, mkdtemp, open, rm } from "node:fs/promises";
3
+ import { homedir, tmpdir } from "node:os";
3
4
  import { join } from "node:path";
4
- import { execFile as execFileCallback } from "node:child_process";
5
- import { promisify } from "node:util";
6
- const execFile = promisify(execFileCallback);
5
+ import { createInterface } from "node:readline";
6
+ import { getSupportedThinkingLevels } from "@earendil-works/pi-ai";
7
+ import { getOAuthProviders } from "@earendil-works/pi-ai/oauth";
8
+ import { builtinModels } from "@earendil-works/pi-ai/providers/all";
9
+ import { discoverClaudeSdkModels } from "@mono-agent/runtime-adapter";
10
+ import { inspectPiAuthStore as inspectDefaultPiAuthStore, } from "../pi-auth-store-inspection.js";
11
+ import { credentialNeutralProviderStatusEnvironment, hasDurableProviderEnvironmentCredential, runBoundedProviderCommand, } from "../provider-setup.js";
12
+ /** Cloud Pi providers that guided onboarding can authenticate and prove end to end. */
13
+ export const GUIDED_PI_PROVIDER_IDS = [
14
+ "anthropic",
15
+ "github-copilot",
16
+ "openai-codex",
17
+ "opencode-go",
18
+ ];
19
+ const GUIDED_PI_PROVIDERS = new Set(GUIDED_PI_PROVIDER_IDS);
20
+ const GUIDED_LOCAL_PI_PROVIDERS = new Set(["ollama", "lmstudio"]);
21
+ /** Reject known unsupported remote Pi integrations while leaving custom local ids available. */
22
+ export function guidedPiProviderProblem(provider) {
23
+ if (GUIDED_PI_PROVIDERS.has(provider) || GUIDED_LOCAL_PI_PROVIDERS.has(provider)) {
24
+ return undefined;
25
+ }
26
+ return builtinModels().getProvider(provider) === undefined
27
+ ? "Custom Pi providers require a hand-authored providers.local[] entry. Guided init supports discovered Ollama and LM Studio routes."
28
+ : "Guided init supports Pi Anthropic, GitHub Copilot, OpenAI Codex, OpenCode-Go, Ollama, and LM Studio. Configure other Pi providers manually.";
29
+ }
30
+ function providerDiscoveryCommand(opts) {
31
+ if (opts.execFile !== undefined) {
32
+ return (file, args, commandOptions) => opts.execFile(file, args, {
33
+ ...commandOptions,
34
+ env: commandOptions.env ?? safeDiscoveryProcessEnv(),
35
+ ...(opts.abortSignal === undefined ? {} : { abortSignal: opts.abortSignal }),
36
+ });
37
+ }
38
+ return (file, args, commandOptions) => runBoundedProviderCommand(file, args, {
39
+ ...commandOptions,
40
+ cwd: process.cwd(),
41
+ env: commandOptions.env ?? safeDiscoveryProcessEnv(),
42
+ ...(opts.abortSignal === undefined ? {} : { abortSignal: opts.abortSignal }),
43
+ });
44
+ }
7
45
  const DEFAULT_DISCOVERY_TIMEOUT_MS = 1200;
46
+ const DEFAULT_OPENCODE_DISCOVERY_TIMEOUT_MS = 5000;
8
47
  const PI_OPENAI_CODEX_PROVIDER = "openai-codex";
9
- const PI_OPENAI_CODEX = "pi:openai-codex:gpt-5.5";
10
- const DIRECT_CODEX = "codex:gpt-5.5";
48
+ const OPENAI_CODEX_MODELS = [
49
+ { id: "gpt-5.6-terra", name: "GPT-5.6 Terra", minimumCodexCliVersion: [0, 144, 0] },
50
+ { id: "gpt-5.6-sol", name: "GPT-5.6 Sol", minimumCodexCliVersion: [0, 144, 0] },
51
+ ];
11
52
  export const STATIC_MODEL_CANDIDATES = [
12
- {
13
- value: PI_OPENAI_CODEX,
14
- label: "Pi OpenAI-Codex GPT-5.5",
15
- hint: "default; auth setup available",
16
- source: "pi",
53
+ ...OPENAI_CODEX_MODELS.map(staticDirectCodexCandidate),
54
+ ...[
55
+ ["claude-sonnet-5", "Claude Sonnet 5", ["low", "medium", "high", "xhigh", "max"]],
56
+ ["claude-opus-4-8[1m]", "Claude Opus 4.8 (1M context)", ["low", "medium", "high", "xhigh", "max"]],
57
+ ["claude-haiku-4-5-20251001", "Claude Haiku 4.5", []],
58
+ ].map(([model, label, efforts]) => ({
59
+ value: `claude:${model}`,
60
+ label: label,
61
+ hint: "SDK-versioned cached catalog; sign-in required",
62
+ source: "claude",
63
+ availability: "catalog_available",
64
+ authState: "auth_required",
65
+ supportedEfforts: efforts,
17
66
  setupRequired: true,
18
- defaultEffort: "medium",
19
- },
20
- { value: DIRECT_CODEX, label: "Codex GPT-5.5", source: "codex", defaultEffort: "medium" },
21
- { value: "claude:claude-sonnet-4-6", label: "Claude Sonnet 4.6", source: "claude", defaultEffort: "medium" },
22
- { value: "pi:ollama:llama3.1:8b", label: "Ollama llama3.1:8b", hint: "fully local", source: "ollama", defaultEffort: "none" },
67
+ })),
23
68
  ];
24
69
  export async function discoverWizardModelCandidates(opts = {}) {
25
70
  const timeoutMs = opts.timeoutMs ?? DEFAULT_DISCOVERY_TIMEOUT_MS;
26
- const [pi, opencode, ollama, lmstudio] = await Promise.all([
27
- discoverPiOpenAiCodex({ ...opts, timeoutMs }),
28
- discoverOpenCodeModels({ ...opts, timeoutMs }),
71
+ const [codex, pi, claude, opencode, ollama, lmstudio] = await Promise.all([
72
+ discoverDirectCodex({ ...opts, timeoutMs }),
73
+ discoverPiModels({ ...opts, timeoutMs }),
74
+ discoverClaudeModels({ ...opts, timeoutMs }),
75
+ discoverOpenCodeModels({
76
+ ...opts,
77
+ timeoutMs: opts.timeoutMs ?? DEFAULT_OPENCODE_DISCOVERY_TIMEOUT_MS,
78
+ }),
29
79
  discoverOllamaModels({ ...opts, timeoutMs }),
30
80
  discoverLmStudioModels({ ...opts, timeoutMs }),
31
81
  ]);
32
82
  return {
33
83
  candidates: rankWizardModelCandidates([
34
- ...STATIC_MODEL_CANDIDATES,
84
+ ...codex.candidates,
35
85
  ...pi.candidates,
86
+ ...claude.candidates,
36
87
  ...opencode.candidates,
37
88
  ...ollama.candidates,
38
89
  ...lmstudio.candidates,
39
90
  ]),
40
- statuses: [pi.status, opencode.status, ollama.status, lmstudio.status],
91
+ statuses: [codex.status, pi.status, claude.status, opencode.status, ollama.status, lmstudio.status],
41
92
  };
42
93
  }
43
94
  export function rankWizardModelCandidates(candidates) {
@@ -58,9 +109,6 @@ export function defaultEffortForModelRef(modelRef, reasoning) {
58
109
  if (reasoning === false) {
59
110
  return "none";
60
111
  }
61
- if (modelRef.startsWith("claude:") || modelRef.startsWith("codex:") || modelRef.startsWith("pi:openai-codex:")) {
62
- return "medium";
63
- }
64
112
  if (!modelRef.startsWith("pi:")) {
65
113
  return undefined;
66
114
  }
@@ -71,66 +119,402 @@ export function defaultEffortForModelRef(modelRef, reasoning) {
71
119
  }
72
120
  return undefined;
73
121
  }
74
- async function discoverPiOpenAiCodex(opts) {
75
- const read = opts.readFile ?? readFile;
76
- const authPath = opts.piAuthPath ?? join(homedir(), ".pi", "agent", "auth.json");
122
+ /** Detect the direct Codex CLI and its account catalog without claiming readiness. */
123
+ async function discoverDirectCodex(opts) {
124
+ const run = providerDiscoveryCommand(opts);
125
+ const verified = new Set(opts.verifiedModelRefs ?? []);
126
+ let version = "";
77
127
  try {
78
- const auth = parseJsonObject(await read(authPath, "utf8"));
79
- const providers = readPiAuthProviderMap(auth);
80
- if (providers[PI_OPENAI_CODEX_PROVIDER] !== undefined) {
81
- return {
82
- candidates: [piOpenAiCodexCandidate("authenticated")],
83
- status: { provider: "Pi", status: "detected", detail: "OpenAI-Codex credentials found" },
84
- };
128
+ const result = await run("codex", ["--version"], { timeout: opts.timeoutMs });
129
+ version = firstOutputLine(result.stdout);
130
+ }
131
+ catch {
132
+ return {
133
+ candidates: directCodexCandidates(curatedCodexCatalog(), "install-required", "", verified),
134
+ status: {
135
+ provider: "Codex",
136
+ status: "setup_available",
137
+ detail: "CLI not found; install Codex and sign in before the readiness check",
138
+ },
139
+ };
140
+ }
141
+ let catalog = curatedCodexCatalog();
142
+ try {
143
+ // Injected command runners are normally deterministic unit-test seams; do
144
+ // not escape them by starting a real app-server unless a model-list seam
145
+ // was also supplied.
146
+ const discoveredCatalog = opts.codexModelList !== undefined
147
+ ? [...await opts.codexModelList()]
148
+ : opts.execFile === undefined
149
+ ? await requestCodexModelList(opts.timeoutMs, codexModelDiscoveryEnvironment(opts.persistedEnv), opts.abortSignal)
150
+ : catalog;
151
+ if (discoveredCatalog.length > 0)
152
+ catalog = discoveredCatalog;
153
+ }
154
+ catch {
155
+ // The exact curated Sol/Terra fallback remains selectable offline.
156
+ }
157
+ const durableCredential = hasDurableProviderEnvironmentCredential("codex:gpt-5.6-terra", opts.persistedEnv ?? {});
158
+ const shellOnlyCredential = !durableCredential
159
+ && hasDurableProviderEnvironmentCredential("codex:gpt-5.6-terra", process.env);
160
+ try {
161
+ if (!durableCredential) {
162
+ await run("codex", ["login", "status"], {
163
+ timeout: opts.timeoutMs,
164
+ env: credentialNeutralProviderStatusEnvironment(process.env, opts.persistedEnv),
165
+ });
85
166
  }
167
+ const candidates = directCodexCandidates(catalog, "credential-detected", version, verified);
168
+ const setupModels = candidates.filter((candidate) => candidate.setupRequired === true);
169
+ const setupDetails = [...new Set(setupModels.map((candidate) => candidate.hint ?? `${candidate.label} setup required`))];
86
170
  return {
87
- candidates: [piOpenAiCodexCandidate("setup-required")],
88
- status: { provider: "Pi", status: "setup_available", detail: "OpenAI-Codex credentials not found; auth setup available" },
171
+ candidates,
172
+ status: {
173
+ provider: "Codex",
174
+ status: setupModels.length === 0 ? "detected" : "setup_available",
175
+ detail: `${version.length > 0 ? `${version}; ` : ""}${durableCredential ? "durable OPENAI_API_KEY detected" : "sign-in detected"} (live readiness not yet verified)${setupModels.length === 0
176
+ ? ""
177
+ : `; ${setupDetails.join("; ")}`}`,
178
+ },
89
179
  };
90
180
  }
91
- catch (error) {
92
- if (isMissingFileError(error)) {
93
- return {
94
- candidates: [piOpenAiCodexCandidate("setup-required")],
95
- status: { provider: "Pi", status: "setup_available", detail: "auth store not found; OpenAI-Codex auth setup available" },
181
+ catch {
182
+ return {
183
+ candidates: directCodexCandidates(catalog, "login-required", version, verified),
184
+ status: {
185
+ provider: "Codex",
186
+ status: "setup_available",
187
+ detail: `${version.length > 0 ? `${version}; ` : "CLI installed; "}${shellOnlyCredential
188
+ ? "durable sign-in required; shell-only OPENAI_API_KEY ignored"
189
+ : "sign-in required"}`,
190
+ },
191
+ };
192
+ }
193
+ }
194
+ async function discoverPiModels(opts) {
195
+ const inspectAuthStore = opts.inspectPiAuthStore ?? inspectDefaultPiAuthStore;
196
+ const authPath = opts.piAuthPath ?? join(homedir(), ".pi", "agent", "auth.json");
197
+ let credentialProviders = new Set();
198
+ let status;
199
+ try {
200
+ const inspection = await inspectAuthStore(authPath);
201
+ if (inspection.status === "missing") {
202
+ status = { provider: "Pi", status: "setup_available", detail: "auth store not found; provider setup is available" };
203
+ }
204
+ else if (inspection.status === "unsafe") {
205
+ status = {
206
+ provider: "Pi",
207
+ status: "unavailable",
208
+ detail: `auth store rejected as unsafe (${inspection.reason}); catalog remains available`,
96
209
  };
97
210
  }
98
- return { candidates: [], status: { provider: "Pi", status: "unavailable", detail: "auth store unreadable" } };
211
+ else {
212
+ const providers = readPiAuthProviderMap(inspection.auth);
213
+ credentialProviders = new Set(Object.entries(providers)
214
+ .filter(([provider, credential]) => GUIDED_PI_PROVIDERS.has(provider) && hasUsablePiCredential(credential))
215
+ .map(([provider]) => provider));
216
+ status = credentialProviders.size > 0
217
+ ? {
218
+ provider: "Pi",
219
+ status: "detected",
220
+ detail: `${credentialProviders.size} provider credential entr${credentialProviders.size === 1 ? "y" : "ies"} detected (not yet verified)`,
221
+ }
222
+ : { provider: "Pi", status: "setup_available", detail: "credential store is empty; setup is available" };
223
+ }
224
+ }
225
+ catch {
226
+ status = { provider: "Pi", status: "unavailable", detail: "auth store unreadable; catalog remains available" };
227
+ }
228
+ const preEnvironmentStatus = status;
229
+ for (const provider of GUIDED_PI_PROVIDER_IDS) {
230
+ if (hasDurableProviderEnvironmentCredential(`pi:${provider}:credential-check`, opts.persistedEnv ?? {})) {
231
+ credentialProviders.add(provider);
232
+ }
99
233
  }
234
+ if (credentialProviders.size > 0 && preEnvironmentStatus.status !== "detected") {
235
+ status = {
236
+ provider: "Pi",
237
+ status: "detected",
238
+ detail: `${credentialProviders.size} provider credential source${credentialProviders.size === 1 ? "" : "s"} detected (not yet verified); ${preEnvironmentStatus.detail}`,
239
+ };
240
+ }
241
+ else if (credentialProviders.size === 0
242
+ && hasDurableProviderEnvironmentCredential("pi:opencode-go:credential-check", process.env)) {
243
+ status = {
244
+ provider: "Pi",
245
+ status: "setup_available",
246
+ detail: `${preEnvironmentStatus.detail}; shell-only OPENCODE_API_KEY ignored until persisted in the agent .env or Pi auth store`,
247
+ };
248
+ }
249
+ const verified = new Set(opts.verifiedModelRefs ?? []);
250
+ const oauthProviders = new Set(getOAuthProviders().map((provider) => provider.id));
251
+ const models = builtinModels();
252
+ const providerNames = new Map(models.getProviders().map((provider) => [provider.id, provider.name]));
253
+ const candidates = models.getModels()
254
+ .filter((model) => GUIDED_PI_PROVIDERS.has(model.provider))
255
+ .map((model) => {
256
+ const value = `pi:${model.provider}:${model.id}`;
257
+ const supportedEfforts = normalizeEfforts(getSupportedThinkingLevels(model));
258
+ const authState = verified.has(value)
259
+ ? "verified"
260
+ : credentialProviders.has(model.provider)
261
+ ? "credential_detected"
262
+ : "auth_required";
263
+ const defaultEffort = exactDefaultEffortWhenUnambiguous(supportedEfforts);
264
+ const shellOnlyCredential = authState === "auth_required"
265
+ && hasDurableProviderEnvironmentCredential(value, process.env);
266
+ const providerLabel = model.provider === "opencode-go"
267
+ ? "OpenCode-Go"
268
+ : providerNames.get(model.provider) ?? model.provider;
269
+ return {
270
+ value,
271
+ label: `Pi ${providerLabel} · ${model.name || model.id}`,
272
+ hint: authState === "verified"
273
+ ? "verified by live readiness"
274
+ : authState === "credential_detected"
275
+ ? "credential detected; live readiness pending"
276
+ : shellOnlyCredential
277
+ ? "shell-only credential ignored; persist it in the agent .env or provider store"
278
+ : oauthProviders.has(model.provider)
279
+ ? "OAuth setup available"
280
+ : "API key or provider environment required",
281
+ source: "pi",
282
+ availability: "catalog_available",
283
+ authState,
284
+ supportedEfforts,
285
+ ...(defaultEffort === undefined ? {} : { defaultEffort }),
286
+ ...(authState === "verified" ? { discovered: true } : {}),
287
+ ...(authState === "auth_required" ? { setupRequired: true } : {}),
288
+ };
289
+ });
290
+ return { candidates, status };
100
291
  }
101
292
  function readPiAuthProviderMap(auth) {
102
- const nestedProviders = parseJsonObject(auth.providers);
103
- return { ...nestedProviders, ...auth };
293
+ const nestedProviders = auth.providers === undefined ? {} : parseJsonObject(auth.providers);
294
+ const { providers: _providers, ...topLevelProviders } = auth;
295
+ return { ...nestedProviders, ...topLevelProviders };
296
+ }
297
+ function hasUsablePiCredential(value) {
298
+ if (!isRecord(value))
299
+ return false;
300
+ if (value.type === "oauth") {
301
+ return isCredentialString(value.access) || isCredentialString(value.refresh);
302
+ }
303
+ return value.type === "api_key" && isCredentialString(value.key);
304
+ }
305
+ function isCredentialString(value) {
306
+ return typeof value === "string"
307
+ && value.trim().length > 0
308
+ && value.length <= 65_536
309
+ && !value.includes("\0");
310
+ }
311
+ async function discoverClaudeModels(opts) {
312
+ const run = providerDiscoveryCommand(opts);
313
+ const verified = new Set(opts.verifiedModelRefs ?? []);
314
+ let credentialDetected = hasDurableProviderEnvironmentCredential("claude:claude-sonnet-5", opts.persistedEnv ?? {});
315
+ const durableCredential = credentialDetected;
316
+ const shellOnlyCredential = !durableCredential
317
+ && hasDurableProviderEnvironmentCredential("claude:claude-sonnet-5", process.env);
318
+ if (!credentialDetected) {
319
+ try {
320
+ await run("claude", ["auth", "status", "--json"], {
321
+ timeout: opts.timeoutMs,
322
+ env: credentialNeutralProviderStatusEnvironment(process.env, opts.persistedEnv),
323
+ });
324
+ credentialDetected = true;
325
+ }
326
+ catch {
327
+ // Catalog discovery is deliberately independent from account state.
328
+ }
329
+ }
330
+ let catalog = [];
331
+ try {
332
+ catalog = opts.claudeModelList !== undefined
333
+ ? await opts.claudeModelList()
334
+ : opts.execFile === undefined
335
+ ? await discoverClaudeSdkModels({ timeoutMs: Math.max(opts.timeoutMs, 2_000) })
336
+ : await discoverClaudeSdkModels({ timeoutMs: 1 });
337
+ }
338
+ catch {
339
+ // Static SDK-versioned rows remain available through STATIC_MODEL_CANDIDATES.
340
+ }
341
+ const candidates = catalog.map((model) => {
342
+ const value = model.reference;
343
+ const authState = verified.has(value)
344
+ ? "verified"
345
+ : credentialDetected
346
+ ? "credential_detected"
347
+ : "auth_required";
348
+ const supportedEfforts = normalizeEfforts(model.supportedEfforts);
349
+ return {
350
+ value,
351
+ label: model.displayName,
352
+ hint: `${model.source === "discovered" ? "discovered from Claude SDK" : "SDK-versioned cached catalog"}; ${authState === "verified" ? "verified" : authState === "credential_detected" ? "credential detected; live readiness pending" : "sign-in required"}`,
353
+ source: "claude",
354
+ availability: "catalog_available",
355
+ authState,
356
+ supportedEfforts,
357
+ ...(authState === "verified" ? { discovered: true } : {}),
358
+ ...(authState === "auth_required" ? { setupRequired: true } : {}),
359
+ };
360
+ });
361
+ return {
362
+ candidates,
363
+ status: credentialDetected
364
+ ? {
365
+ provider: "Claude",
366
+ status: "detected",
367
+ detail: `${durableCredential ? "durable provider credential" : "sign-in"} detected (live readiness not yet verified)`,
368
+ }
369
+ : {
370
+ provider: "Claude",
371
+ status: "setup_available",
372
+ detail: `${shellOnlyCredential
373
+ ? "durable sign-in required; shell-only Claude credential ignored"
374
+ : "sign-in required"}; catalog remains available`,
375
+ },
376
+ };
104
377
  }
105
378
  async function discoverOpenCodeModels(opts) {
106
- const run = opts.execFile ?? execFile;
379
+ const run = providerDiscoveryCommand(opts);
380
+ let isolation;
107
381
  try {
108
- const { stdout } = await run("opencode", ["models", "--json"], { timeout: opts.timeoutMs });
109
- const models = parseModelEntries(stdout);
382
+ isolation = await createOpenCodeDiscoveryIsolation();
383
+ const { stdout } = await run("opencode", ["models", "opencode-go", "--pure"], {
384
+ timeout: opts.timeoutMs,
385
+ env: isolation.env,
386
+ });
387
+ const bundledModelIds = new Set(builtinModels().getModels()
388
+ .filter((model) => model.provider === "opencode-go")
389
+ .map((model) => model.id));
390
+ const parsedModels = parseOpenCodeGoModels(stdout);
391
+ const models = parsedModels.filter((model) => bundledModelIds.has(model));
392
+ const unsupportedCount = parsedModels.length - models.length;
110
393
  return {
111
394
  candidates: models.map((model) => {
112
- const value = model.id.startsWith("pi:") ? model.id : `pi:opencode-go:${model.id}`;
113
- const defaultEffort = defaultEffortForModelRef(value, model.reasoning);
395
+ const value = `pi:opencode-go:${model}`;
114
396
  return {
115
397
  value,
116
- label: `OpenCode ${displayModelName(model.id)}`,
117
- hint: "discovered from opencode",
398
+ label: `OpenCode-Go ${displayModelName(model)}`,
399
+ hint: "discovered from opencode; OpenCode-Go API key required",
118
400
  source: "opencode",
119
401
  discovered: true,
120
- ...(defaultEffort === undefined ? {} : { defaultEffort }),
402
+ availability: "catalog_available",
403
+ authState: "auth_required",
404
+ setupRequired: true,
405
+ supportedEfforts: [],
121
406
  };
122
407
  }),
123
408
  status: models.length > 0
124
- ? { provider: "OpenCode", status: "detected", detail: `${models.length} model(s) found` }
125
- : { provider: "OpenCode", status: "unavailable", detail: "no models returned" },
409
+ ? {
410
+ provider: "OpenCode-Go",
411
+ status: "detected",
412
+ detail: `${models.length} runtime-supported model${models.length === 1 ? "" : "s"} found${unsupportedCount === 0
413
+ ? ""
414
+ : `; ${unsupportedCount} unsupported CLI row${unsupportedCount === 1 ? "" : "s"} ignored`}`,
415
+ }
416
+ : parsedModels.length > 0
417
+ ? {
418
+ provider: "OpenCode-Go",
419
+ status: "unavailable",
420
+ detail: `no runtime-supported models; ${parsedModels.length} unsupported CLI row${parsedModels.length === 1 ? "" : "s"} ignored`,
421
+ }
422
+ : { provider: "OpenCode-Go", status: "unavailable", detail: "no models returned" },
126
423
  };
127
424
  }
128
425
  catch {
129
- return { candidates: [], status: { provider: "OpenCode", status: "unavailable", detail: "`opencode models --json` unavailable" } };
426
+ return { candidates: [], status: { provider: "OpenCode-Go", status: "unavailable", detail: "`opencode models opencode-go --pure` unavailable" } };
427
+ }
428
+ finally {
429
+ await isolation?.cleanup();
430
+ }
431
+ }
432
+ async function createOpenCodeDiscoveryIsolation() {
433
+ const root = await mkdtemp(join(tmpdir(), "mono-agent-opencode-discovery-"));
434
+ try {
435
+ await chmod(root, 0o700);
436
+ const home = await createPrivateDirectory(root, "home");
437
+ const config = await createPrivateDirectory(root, "config");
438
+ const opencodeConfig = await createPrivateDirectory(config, "opencode");
439
+ if (process.platform !== "win32")
440
+ await chmod(opencodeConfig, 0o500);
441
+ const data = await createPrivateDirectory(root, "data");
442
+ const state = await createPrivateDirectory(root, "state");
443
+ const cache = await createPrivateDirectory(root, "cache");
444
+ const opencodeData = await createPrivateDirectory(data, "opencode");
445
+ const database = join(opencodeData, "opencode.db");
446
+ const handle = await open(database, "wx", 0o600);
447
+ await handle.close();
448
+ await chmod(database, 0o600);
449
+ const env = safeDiscoveryProcessEnv();
450
+ Object.assign(env, {
451
+ OPENCODE_TEST_HOME: home,
452
+ XDG_CONFIG_HOME: config,
453
+ XDG_CONFIG_DIRS: config,
454
+ XDG_DATA_HOME: data,
455
+ XDG_DATA_DIRS: data,
456
+ XDG_STATE_HOME: state,
457
+ XDG_CACHE_HOME: cache,
458
+ OPENCODE_DB: database,
459
+ OPENCODE_CONFIG_CONTENT: JSON.stringify({ share: "disabled", autoshare: false }),
460
+ OPENCODE_DISABLE_PROJECT_CONFIG: "true",
461
+ OPENCODE_DISABLE_AUTOUPDATE: "true",
462
+ OPENCODE_DISABLE_SHARE: "true",
463
+ OPENCODE_AUTO_SHARE: "false",
464
+ OPENCODE_DISABLE_EXTERNAL_SKILLS: "true",
465
+ OPENCODE_DISABLE_LSP_DOWNLOAD: "true",
466
+ });
467
+ return {
468
+ env,
469
+ cleanup: async () => {
470
+ await rm(root, { recursive: true, force: true }).catch(() => undefined);
471
+ },
472
+ };
473
+ }
474
+ catch (error) {
475
+ await rm(root, { recursive: true, force: true }).catch(() => undefined);
476
+ throw error;
130
477
  }
131
478
  }
479
+ function safeDiscoveryProcessEnv(source = process.env) {
480
+ const env = {};
481
+ for (const key of [
482
+ "PATH",
483
+ "LANG",
484
+ "LC_ALL",
485
+ "LC_CTYPE",
486
+ "SHELL",
487
+ "TMPDIR",
488
+ "TMP",
489
+ "TEMP",
490
+ "SystemRoot",
491
+ "WINDIR",
492
+ "ComSpec",
493
+ "PATHEXT",
494
+ ]) {
495
+ const value = source[key];
496
+ if (typeof value === "string" && value.length > 0)
497
+ env[key] = value;
498
+ }
499
+ return env;
500
+ }
501
+ /** Exact non-secret environment used by live Codex model/list discovery. */
502
+ export function codexModelDiscoveryEnvironment(persistedEnv = {}, shellEnv = process.env) {
503
+ const neutral = credentialNeutralProviderStatusEnvironment(shellEnv, persistedEnv);
504
+ return {
505
+ ...safeDiscoveryProcessEnv(shellEnv),
506
+ HOME: neutral.HOME ?? homedir(),
507
+ ...(neutral.CODEX_HOME === undefined ? {} : { CODEX_HOME: neutral.CODEX_HOME }),
508
+ };
509
+ }
510
+ async function createPrivateDirectory(parent, name) {
511
+ const path = join(parent, name);
512
+ await mkdir(path, { mode: 0o700 });
513
+ await chmod(path, 0o700);
514
+ return path;
515
+ }
132
516
  async function discoverOllamaModels(opts) {
133
- const run = opts.execFile ?? execFile;
517
+ const run = providerDiscoveryCommand(opts);
134
518
  try {
135
519
  const { stdout } = await run("ollama", ["list"], { timeout: opts.timeoutMs });
136
520
  const models = parseOllamaList(stdout);
@@ -143,7 +527,9 @@ async function discoverOllamaModels(opts) {
143
527
  hint: "discovered locally",
144
528
  source: "ollama",
145
529
  discovered: true,
146
- defaultEffort: defaultEffortForModelRef(value) ?? "none",
530
+ availability: "catalog_available",
531
+ authState: "not_required",
532
+ supportedEfforts: [],
147
533
  };
148
534
  }),
149
535
  status: models.length > 0
@@ -159,6 +545,8 @@ async function discoverLmStudioModels(opts) {
159
545
  const fetchImpl = opts.fetch ?? fetch;
160
546
  const controller = new AbortController();
161
547
  const timer = setTimeout(() => controller.abort(), opts.timeoutMs);
548
+ const onAbort = () => controller.abort();
549
+ opts.abortSignal?.addEventListener("abort", onAbort, { once: true });
162
550
  try {
163
551
  const response = await fetchImpl("http://localhost:1234/v1/models", { signal: controller.signal });
164
552
  if (!response.ok) {
@@ -175,7 +563,10 @@ async function discoverLmStudioModels(opts) {
175
563
  hint: "discovered locally",
176
564
  source: "lmstudio",
177
565
  discovered: true,
178
- defaultEffort: defaultEffortForModelRef(value, model.reasoning) ?? "none",
566
+ availability: "catalog_available",
567
+ authState: "not_required",
568
+ supportedEfforts: model.reasoning === false ? ["none"] : [],
569
+ ...(model.reasoning === false ? { defaultEffort: "none" } : {}),
179
570
  };
180
571
  }),
181
572
  status: models.length > 0
@@ -188,31 +579,68 @@ async function discoverLmStudioModels(opts) {
188
579
  }
189
580
  finally {
190
581
  clearTimeout(timer);
582
+ opts.abortSignal?.removeEventListener("abort", onAbort);
191
583
  }
192
584
  }
193
585
  function mergeCandidate(left, right) {
194
- const { setupRequired: leftSetupRequired, defaultEffort: leftDefaultEffort, ...leftRest } = left;
195
- const { setupRequired: rightSetupRequired, defaultEffort: rightDefaultEffort, ...rightRest } = right;
196
- const hint = right.discovered === true ? right.hint ?? left.hint : left.hint ?? right.hint;
586
+ const { setupRequired: leftSetupRequired, defaultEffort: leftDefaultEffort, supportedEfforts: leftSupportedEfforts, ...leftRest } = left;
587
+ const { setupRequired: rightSetupRequired, defaultEffort: rightDefaultEffort, supportedEfforts: rightSupportedEfforts, ...rightRest } = right;
588
+ const rightHasDiscoveryState = right.discovered === true
589
+ || right.setupRequired === true
590
+ || right.availability !== undefined
591
+ || right.authState !== undefined;
592
+ const preserveBuiltinPiMetadata = left.source === "pi"
593
+ && (right.source === "opencode" || right.source === "ollama" || right.source === "lmstudio");
594
+ const hint = preserveBuiltinPiMetadata
595
+ ? left.hint
596
+ : rightHasDiscoveryState ? right.hint ?? left.hint : left.hint ?? right.hint;
197
597
  const discovered = left.discovered === true || right.discovered === true;
198
- const defaultEffort = rightDefaultEffort ?? leftDefaultEffort;
598
+ const rightReplacesCatalogMetadata = right.availability === "catalog_available"
599
+ && left.source === right.source;
600
+ const defaultEffort = preserveBuiltinPiMetadata
601
+ ? leftDefaultEffort
602
+ : rightReplacesCatalogMetadata
603
+ ? rightDefaultEffort
604
+ : rightDefaultEffort ?? leftDefaultEffort;
605
+ const authState = preserveBuiltinPiMetadata
606
+ ? left.authState
607
+ : strongerAuthState(left.authState, right.authState);
608
+ const setupRequired = preserveBuiltinPiMetadata
609
+ ? leftSetupRequired === true || authState === "auth_required"
610
+ : rightSetupRequired === true || authState === "auth_required"
611
+ || (right.authState === undefined && !discovered && leftSetupRequired === true);
199
612
  return {
200
613
  ...leftRest,
201
614
  ...rightRest,
615
+ ...(preserveBuiltinPiMetadata ? { source: left.source } : {}),
616
+ ...(authState === undefined ? {} : { authState }),
617
+ supportedEfforts: preserveBuiltinPiMetadata
618
+ ? [...(leftSupportedEfforts ?? [])]
619
+ : [...(rightSupportedEfforts ?? leftSupportedEfforts ?? [])],
202
620
  ...(hint === undefined ? {} : { hint }),
203
621
  ...(defaultEffort === undefined ? {} : { defaultEffort }),
204
622
  discovered,
205
- ...(discovered || !(leftSetupRequired === true || rightSetupRequired === true) ? {} : { setupRequired: true }),
623
+ ...(setupRequired ? { setupRequired: true } : {}),
206
624
  };
207
625
  }
208
626
  function rank(candidate) {
209
- if (candidate.value === PI_OPENAI_CODEX) {
210
- return 0;
211
- }
212
- if (candidate.value === DIRECT_CODEX) {
213
- return 10;
214
- }
215
- if (candidate.value === "claude:claude-sonnet-4-6") {
627
+ const authRank = candidate.authState === "verified"
628
+ ? 0
629
+ : candidate.authState === "credential_detected" || candidate.authState === "not_required"
630
+ ? 1
631
+ : 2;
632
+ return authRank * 1_000 + modelRank(candidate);
633
+ }
634
+ function modelRank(candidate) {
635
+ if (candidate.providerDefault === true)
636
+ return -100;
637
+ const directCodexRank = OPENAI_CODEX_MODELS.findIndex((model) => candidate.value === directCodexRef(model));
638
+ if (directCodexRank >= 0)
639
+ return directCodexRank;
640
+ const piOpenAiCodexRank = OPENAI_CODEX_MODELS.findIndex((model) => candidate.value === piOpenAiCodexRef(model));
641
+ if (piOpenAiCodexRank >= 0)
642
+ return 10 + piOpenAiCodexRank;
643
+ if (candidate.source === "claude") {
216
644
  return 20;
217
645
  }
218
646
  if (candidate.source === "opencode") {
@@ -226,17 +654,323 @@ function rank(candidate) {
226
654
  }
227
655
  return 90;
228
656
  }
229
- function piOpenAiCodexCandidate(state) {
657
+ function directCodexRef(model) {
658
+ return `codex:${model.id}`;
659
+ }
660
+ function piOpenAiCodexRef(model) {
661
+ return `pi:${PI_OPENAI_CODEX_PROVIDER}:${model.id}`;
662
+ }
663
+ function staticDirectCodexCandidate(model) {
664
+ return {
665
+ value: directCodexRef(model),
666
+ label: `Codex ${model.name}`,
667
+ ...(model.minimumCodexCliVersion === undefined
668
+ ? {}
669
+ : { hint: `requires Codex CLI ${formatVersion(model.minimumCodexCliVersion)}+` }),
670
+ source: "codex",
671
+ availability: "catalog_available",
672
+ authState: "auth_required",
673
+ // Exact effort support/defaults come from Codex app-server model/list.
674
+ // Do not fabricate provider metadata when the live catalog is unavailable.
675
+ supportedEfforts: [],
676
+ providerDefault: model.id === "gpt-5.6-terra",
677
+ };
678
+ }
679
+ function directCodexCandidates(catalog, state, version = "", verified = new Set()) {
680
+ return catalog.map((model) => directCodexCandidate(model, state, version, verified));
681
+ }
682
+ function directCodexCandidate(model, state, version = "", verified = new Set()) {
683
+ const reference = `codex:${model.id}`;
684
+ const curated = OPENAI_CODEX_MODELS.find((entry) => entry.id === model.id);
685
+ const authState = verified.has(reference)
686
+ ? "verified"
687
+ : state === "credential-detected"
688
+ ? "credential_detected"
689
+ : "auth_required";
690
+ if (state === "install-required") {
691
+ const minimum = curated?.minimumCodexCliVersion;
692
+ return {
693
+ value: reference,
694
+ label: `Codex ${model.displayName}`,
695
+ hint: minimum === undefined
696
+ ? "install Codex CLI and sign in"
697
+ : `install Codex CLI ${formatVersion(minimum)}+ and sign in`,
698
+ source: "codex",
699
+ setupRequired: true,
700
+ availability: "catalog_available",
701
+ authState,
702
+ supportedEfforts: model.supportedEfforts,
703
+ ...(model.isDefault === undefined ? {} : { providerDefault: model.isDefault }),
704
+ ...(model.defaultEffort === undefined ? {} : { defaultEffort: model.defaultEffort }),
705
+ };
706
+ }
707
+ const prerequisites = [];
708
+ const minimum = curated?.minimumCodexCliVersion;
709
+ if (minimum !== undefined && !codexVersionMeetsMinimum(version, minimum)) {
710
+ prerequisites.push(parseCodexVersion(version) === undefined
711
+ ? `Codex CLI ${formatVersion(minimum)}+ required; installed version could not be verified`
712
+ : `update Codex CLI to ${formatVersion(minimum)}+ (found ${version})`);
713
+ }
714
+ if (state === "login-required")
715
+ prerequisites.push("Codex sign-in required");
716
+ if (prerequisites.length === 0) {
717
+ return {
718
+ value: reference,
719
+ label: `Codex ${model.displayName}`,
720
+ hint: authState === "verified"
721
+ ? `${version.length > 0 ? `${version}; ` : ""}verified by live readiness`
722
+ : `${version.length > 0 ? `${version}; ` : ""}sign-in detected; live readiness pending`,
723
+ source: "codex",
724
+ discovered: true,
725
+ availability: "catalog_available",
726
+ authState,
727
+ supportedEfforts: model.supportedEfforts,
728
+ ...(model.isDefault === undefined ? {} : { providerDefault: model.isDefault }),
729
+ ...(model.defaultEffort === undefined ? {} : { defaultEffort: model.defaultEffort }),
730
+ };
731
+ }
230
732
  return {
231
- value: PI_OPENAI_CODEX,
232
- label: "Pi OpenAI-Codex GPT-5.5",
233
- hint: state === "authenticated"
234
- ? "recommended when Pi auth is configured"
235
- : "auth setup available",
236
- source: "pi",
237
- defaultEffort: "medium",
238
- ...(state === "authenticated" ? { discovered: true } : { setupRequired: true }),
733
+ value: reference,
734
+ label: `Codex ${model.displayName}`,
735
+ hint: prerequisites.join("; "),
736
+ source: "codex",
737
+ setupRequired: true,
738
+ availability: "catalog_available",
739
+ authState,
740
+ supportedEfforts: model.supportedEfforts,
741
+ ...(model.isDefault === undefined ? {} : { providerDefault: model.isDefault }),
742
+ ...(model.defaultEffort === undefined ? {} : { defaultEffort: model.defaultEffort }),
743
+ };
744
+ }
745
+ function curatedCodexCatalog() {
746
+ return OPENAI_CODEX_MODELS.map((model) => ({
747
+ id: model.id,
748
+ displayName: model.name,
749
+ supportedEfforts: [],
750
+ // Offline product fallback only; live model/list replaces this flag.
751
+ isDefault: model.id === "gpt-5.6-terra",
752
+ }));
753
+ }
754
+ function strongerAuthState(left, right) {
755
+ const order = {
756
+ auth_required: 0,
757
+ not_required: 1,
758
+ credential_detected: 2,
759
+ verified: 3,
239
760
  };
761
+ if (left === undefined)
762
+ return right ?? "auth_required";
763
+ if (right === undefined)
764
+ return left;
765
+ return order[right] > order[left] ? right : left;
766
+ }
767
+ async function requestCodexModelList(timeoutMs, environment, abortSignal) {
768
+ const child = spawn("codex", ["app-server", "--listen", "stdio://", "-c", "project_doc_max_bytes=0"], {
769
+ stdio: ["pipe", "pipe", "ignore"],
770
+ env: { ...environment },
771
+ });
772
+ const lines = createInterface({ input: child.stdout });
773
+ return await new Promise((resolveResult, rejectResult) => {
774
+ let settled = false;
775
+ let timer;
776
+ const ignoreStdinError = () => undefined;
777
+ child.stdin.on("error", ignoreStdinError);
778
+ const childClosedWithin = async (milliseconds) => {
779
+ if (child.exitCode !== null || child.signalCode !== null)
780
+ return true;
781
+ return await new Promise((resolveClosed) => {
782
+ const onClose = () => {
783
+ clearTimeout(closeTimer);
784
+ resolveClosed(true);
785
+ };
786
+ const closeTimer = setTimeout(() => {
787
+ child.off("close", onClose);
788
+ resolveClosed(false);
789
+ }, milliseconds);
790
+ closeTimer.unref?.();
791
+ child.once("close", onClose);
792
+ });
793
+ };
794
+ const stopChild = async () => {
795
+ lines.removeAllListeners("line");
796
+ lines.close();
797
+ try {
798
+ child.stdin.end();
799
+ }
800
+ catch { /* best effort */ }
801
+ if (await childClosedWithin(25))
802
+ return;
803
+ try {
804
+ child.kill("SIGTERM");
805
+ }
806
+ catch { /* best effort */ }
807
+ if (await childClosedWithin(250))
808
+ return;
809
+ try {
810
+ child.kill("SIGKILL");
811
+ }
812
+ catch { /* best effort */ }
813
+ if (!await childClosedWithin(250)) {
814
+ child.stdin.destroy();
815
+ child.stdout.destroy();
816
+ child.unref();
817
+ }
818
+ };
819
+ let settle;
820
+ const onChildError = () => void settle(new Error("Codex app-server is unavailable."));
821
+ const onChildClose = () => void settle(new Error("Codex app-server closed before returning models."));
822
+ const onAbort = () => {
823
+ const error = new Error("Codex model catalog discovery was interrupted.");
824
+ error.name = "AbortError";
825
+ void settle(error);
826
+ };
827
+ settle = async (error, models = []) => {
828
+ if (settled)
829
+ return;
830
+ settled = true;
831
+ clearTimeout(timer);
832
+ child.off("error", onChildError);
833
+ child.off("close", onChildClose);
834
+ abortSignal?.removeEventListener("abort", onAbort);
835
+ await stopChild();
836
+ child.stdin.off("error", ignoreStdinError);
837
+ if (error === undefined)
838
+ resolveResult(models);
839
+ else
840
+ rejectResult(error);
841
+ };
842
+ const writeMessage = (message) => {
843
+ if (settled || child.stdin.destroyed || child.stdin.writableEnded)
844
+ return;
845
+ try {
846
+ child.stdin.write(`${JSON.stringify(message)}\n`);
847
+ }
848
+ catch { /* bounded shutdown handles the process */ }
849
+ };
850
+ timer = setTimeout(() => void settle(new Error("Codex model catalog discovery timed out.")), Math.max(250, Math.min(10_000, timeoutMs)));
851
+ timer.unref?.();
852
+ child.once("error", onChildError);
853
+ child.once("close", onChildClose);
854
+ abortSignal?.addEventListener("abort", onAbort, { once: true });
855
+ lines.on("line", (line) => {
856
+ let message;
857
+ try {
858
+ message = JSON.parse(line);
859
+ }
860
+ catch {
861
+ return;
862
+ }
863
+ if (!isRecord(message))
864
+ return;
865
+ if (typeof message.method === "string" && (typeof message.id === "string" || typeof message.id === "number")) {
866
+ writeMessage({
867
+ id: message.id,
868
+ error: { code: -32601, message: "Model catalog discovery does not support server requests." },
869
+ });
870
+ return;
871
+ }
872
+ if (message.id === 1 && isRecord(message.result)) {
873
+ writeMessage({
874
+ id: 2,
875
+ method: "model/list",
876
+ params: { includeHidden: false, limit: 1_000 },
877
+ });
878
+ return;
879
+ }
880
+ if (message.id !== 2)
881
+ return;
882
+ if (!isRecord(message.result) || !Array.isArray(message.result.data)) {
883
+ void settle(new Error("Codex app-server returned an invalid model catalog."));
884
+ return;
885
+ }
886
+ void settle(undefined, normalizeCodexCatalog(message.result.data));
887
+ });
888
+ writeMessage({
889
+ id: 1,
890
+ method: "initialize",
891
+ params: {
892
+ clientInfo: { name: "mono-agent", title: "mono-agent", version: "0" },
893
+ capabilities: { experimentalApi: true },
894
+ },
895
+ });
896
+ if (abortSignal?.aborted === true)
897
+ onAbort();
898
+ });
899
+ }
900
+ function normalizeCodexCatalog(rows) {
901
+ const result = [];
902
+ const seen = new Set();
903
+ for (const raw of rows.slice(0, 1_000)) {
904
+ if (!isRecord(raw))
905
+ continue;
906
+ const id = typeof raw.id === "string" ? raw.id.trim() : "";
907
+ if (id.length === 0 || id.length > 160 || seen.has(id))
908
+ continue;
909
+ const supportedRows = Array.isArray(raw.supportedReasoningEfforts)
910
+ ? raw.supportedReasoningEfforts.map((entry) => isRecord(entry) ? entry.reasoningEffort : entry)
911
+ : [];
912
+ const supportedEfforts = normalizeEfforts(supportedRows);
913
+ const normalizedDefault = normalizeEffort(raw.defaultReasoningEffort);
914
+ seen.add(id);
915
+ result.push({
916
+ id,
917
+ displayName: boundedCatalogText(raw.displayName, 160) || id,
918
+ supportedEfforts,
919
+ ...(normalizedDefault === undefined ? {} : { defaultEffort: normalizedDefault }),
920
+ ...(typeof raw.isDefault === "boolean" ? { isDefault: raw.isDefault } : {}),
921
+ });
922
+ }
923
+ return result;
924
+ }
925
+ const SUPPORTED_EFFORTS = new Set([
926
+ "none",
927
+ "minimal",
928
+ "low",
929
+ "medium",
930
+ "high",
931
+ "xhigh",
932
+ "max",
933
+ "ultra",
934
+ ]);
935
+ function normalizeEffort(value) {
936
+ const normalized = value === "off" ? "none" : typeof value === "string" ? value : "";
937
+ return SUPPORTED_EFFORTS.has(normalized) ? normalized : undefined;
938
+ }
939
+ function normalizeEfforts(values) {
940
+ return [...new Set(values.map(normalizeEffort).filter((value) => value !== undefined))];
941
+ }
942
+ function exactDefaultEffortWhenUnambiguous(supportedEfforts) {
943
+ return supportedEfforts.length === 1 ? supportedEfforts[0] : undefined;
944
+ }
945
+ function boundedCatalogText(value, limit) {
946
+ const normalized = typeof value === "string"
947
+ ? value.replace(/[\u0000-\u001f\u007f]/gu, " ").replace(/\s+/gu, " ").trim()
948
+ : "";
949
+ return normalized.length <= limit ? normalized : `${normalized.slice(0, Math.max(0, limit - 1))}…`;
950
+ }
951
+ function parseCodexVersion(version) {
952
+ const match = /(?:^|\s)v?(\d+)\.(\d+)\.(\d+)(?:[-+\s]|$)/u.exec(version);
953
+ if (match === null)
954
+ return undefined;
955
+ const parts = match.slice(1, 4).map(Number);
956
+ if (parts.some((part) => !Number.isSafeInteger(part) || part < 0))
957
+ return undefined;
958
+ return [parts[0], parts[1], parts[2]];
959
+ }
960
+ function codexVersionMeetsMinimum(version, minimum) {
961
+ const parsed = parseCodexVersion(version);
962
+ if (parsed === undefined)
963
+ return false;
964
+ for (let index = 0; index < minimum.length; index += 1) {
965
+ const installedPart = parsed[index] ?? 0;
966
+ const minimumPart = minimum[index] ?? 0;
967
+ if (installedPart !== minimumPart)
968
+ return installedPart > minimumPart;
969
+ }
970
+ return true;
971
+ }
972
+ function formatVersion(version) {
973
+ return version.join(".");
240
974
  }
241
975
  function parseOllamaList(stdout) {
242
976
  const lines = stdout.trim().split(/\r?\n/u).filter(Boolean);
@@ -248,29 +982,13 @@ function parseOllamaList(stdout) {
248
982
  .map((line) => line.trim().split(/\s+/u)[0])
249
983
  .filter((name) => name !== undefined && name.length > 0);
250
984
  }
251
- function parseModelEntries(stdout) {
252
- try {
253
- const parsed = JSON.parse(stdout);
254
- if (Array.isArray(parsed)) {
255
- return parsed.map(modelEntryFromUnknown).filter(isModelEntry);
256
- }
257
- if (isRecord(parsed)) {
258
- for (const key of ["models", "data"]) {
259
- const value = parsed[key];
260
- if (Array.isArray(value)) {
261
- return value.map(modelEntryFromUnknown).filter(isModelEntry);
262
- }
263
- }
264
- }
265
- }
266
- catch {
267
- // Fall through to tolerant line parsing.
268
- }
985
+ function parseOpenCodeGoModels(stdout) {
986
+ const prefix = "opencode-go/";
269
987
  return stdout
270
988
  .split(/\r?\n/u)
271
- .map((line) => line.trim().split(/\s+/u)[0])
272
- .filter(isNonEmptyString)
273
- .map((id) => ({ id }));
989
+ .map((line) => line.trim())
990
+ .filter((line) => line.startsWith(prefix) && line.length > prefix.length)
991
+ .map((line) => line.slice(prefix.length));
274
992
  }
275
993
  function parseOpenAiModelEntriesBody(body) {
276
994
  if (!isRecord(body) || !Array.isArray(body.data)) {
@@ -300,7 +1018,13 @@ function displayModelName(model) {
300
1018
  }
301
1019
  function parseJsonObject(value) {
302
1020
  const parsed = typeof value === "string" ? JSON.parse(value) : value;
303
- return isRecord(parsed) ? parsed : {};
1021
+ if (!isRecord(parsed)) {
1022
+ throw new Error("Expected a JSON object.");
1023
+ }
1024
+ return parsed;
1025
+ }
1026
+ function firstOutputLine(value) {
1027
+ return value.split(/\r?\n/u).map((line) => line.trim()).find((line) => line.length > 0)?.slice(0, 120) ?? "";
304
1028
  }
305
1029
  function readReasoningCapability(value) {
306
1030
  for (const field of ["reasoning", "supportsReasoning", "supports_reasoning", "thinking", "supportsThinking", "supports_thinking"]) {
@@ -331,15 +1055,9 @@ function localModelDefaultEffort(model) {
331
1055
  ? "medium"
332
1056
  : "none";
333
1057
  }
334
- function isMissingFileError(error) {
335
- return isRecord(error) && (error.code === "ENOENT" || error.code === "ENOTDIR");
336
- }
337
1058
  function isRecord(value) {
338
1059
  return typeof value === "object" && value !== null && !Array.isArray(value);
339
1060
  }
340
- function isNonEmptyString(value) {
341
- return value !== undefined && value.length > 0;
342
- }
343
1061
  function isModelEntry(value) {
344
1062
  return value !== undefined;
345
1063
  }