@mono-agent/agent-app 0.6.1 → 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 -17
  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 +41 -8
  9. package/dist/adapter-send-tools.d.ts.map +1 -1
  10. package/dist/adapter-send-tools.js +563 -108
  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 +532 -134
  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 +18 -7
  24. package/dist/channels.js.map +1 -1
  25. package/dist/cli.d.ts +77 -4
  26. package/dist/cli.d.ts.map +1 -1
  27. package/dist/cli.js +2106 -99
  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 +1198 -96
  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 +32 -2
  69. package/dist/interaction-bridge.d.ts.map +1 -1
  70. package/dist/interaction-bridge.js +309 -28
  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 +136 -5
  127. package/dist/provider-setup.d.ts.map +1 -1
  128. package/dist/provider-setup.js +1552 -33
  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 +38 -3
  186. package/dist/wizard/model-discovery.d.ts.map +1 -1
  187. package/dist/wizard/model-discovery.js +927 -102
  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 +66 -11
  194. package/dist/wizard/prompts.d.ts.map +1 -1
  195. package/dist/wizard/prompts.js +190 -20
  196. package/dist/wizard/prompts.js.map +1 -1
  197. package/dist/wizard/run.d.ts +60 -3
  198. package/dist/wizard/run.d.ts.map +1 -1
  199. package/dist/wizard/run.js +1161 -145
  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,34 +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;
8
- const PI_OPENAI_CODEX = "pi:openai-codex:gpt-5.5";
9
- const DIRECT_CODEX = "codex:gpt-5.5";
46
+ const DEFAULT_OPENCODE_DISCOVERY_TIMEOUT_MS = 5000;
47
+ const PI_OPENAI_CODEX_PROVIDER = "openai-codex";
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
+ ];
10
52
  export const STATIC_MODEL_CANDIDATES = [
11
- { value: "claude:claude-sonnet-4-6", label: "Claude Sonnet 4.6", hint: "default", source: "claude" },
12
- { value: DIRECT_CODEX, label: "Codex GPT-5.5", source: "codex" },
13
- { value: "pi:ollama:llama3.1:8b", label: "Ollama llama3.1:8b", hint: "fully local", source: "ollama" },
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,
66
+ setupRequired: true,
67
+ })),
14
68
  ];
15
69
  export async function discoverWizardModelCandidates(opts = {}) {
16
70
  const timeoutMs = opts.timeoutMs ?? DEFAULT_DISCOVERY_TIMEOUT_MS;
17
- const [pi, opencode, ollama, lmstudio] = await Promise.all([
18
- discoverPiOpenAiCodex({ ...opts, timeoutMs }),
19
- 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
+ }),
20
79
  discoverOllamaModels({ ...opts, timeoutMs }),
21
80
  discoverLmStudioModels({ ...opts, timeoutMs }),
22
81
  ]);
23
82
  return {
24
83
  candidates: rankWizardModelCandidates([
25
- ...STATIC_MODEL_CANDIDATES,
84
+ ...codex.candidates,
26
85
  ...pi.candidates,
86
+ ...claude.candidates,
27
87
  ...opencode.candidates,
28
88
  ...ollama.candidates,
29
89
  ...lmstudio.candidates,
30
90
  ]),
31
- statuses: [pi.status, opencode.status, ollama.status, lmstudio.status],
91
+ statuses: [codex.status, pi.status, claude.status, opencode.status, ollama.status, lmstudio.status],
32
92
  };
33
93
  }
34
94
  export function rankWizardModelCandidates(candidates) {
@@ -42,71 +102,436 @@ export function rankWizardModelCandidates(candidates) {
42
102
  export function formatModelDiscoveryStatus(statuses) {
43
103
  return statuses.map((status) => `${status.provider}: ${status.detail}`).join("\n");
44
104
  }
45
- async function discoverPiOpenAiCodex(opts) {
46
- const read = opts.readFile ?? readFile;
105
+ export function defaultEffortForModelRef(modelRef, reasoning) {
106
+ if (reasoning === true) {
107
+ return "medium";
108
+ }
109
+ if (reasoning === false) {
110
+ return "none";
111
+ }
112
+ if (!modelRef.startsWith("pi:")) {
113
+ return undefined;
114
+ }
115
+ const [, provider, ...modelParts] = modelRef.split(":");
116
+ const model = modelParts.join(":");
117
+ if (provider === "opencode-go" || provider === "ollama" || provider === "lmstudio") {
118
+ return localModelDefaultEffort(model);
119
+ }
120
+ return undefined;
121
+ }
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 = "";
127
+ try {
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
+ });
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`))];
170
+ return {
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
+ },
179
+ };
180
+ }
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;
47
196
  const authPath = opts.piAuthPath ?? join(homedir(), ".pi", "agent", "auth.json");
197
+ let credentialProviders = new Set();
198
+ let status;
48
199
  try {
49
- const auth = parseJsonObject(await read(authPath, "utf8"));
50
- const providers = readPiAuthProviderMap(auth);
51
- if (providers["openai-codex"] !== undefined) {
52
- return {
53
- candidates: [
54
- {
55
- value: PI_OPENAI_CODEX,
56
- label: "Pi OpenAI-Codex GPT-5.5",
57
- hint: "recommended when Pi auth is configured",
58
- source: "pi",
59
- discovered: true,
60
- },
61
- ],
62
- status: { provider: "Pi", status: "detected", detail: "OpenAI-Codex credentials found" },
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`,
63
209
  };
64
210
  }
65
- return { candidates: [], status: { provider: "Pi", status: "unavailable", detail: "OpenAI-Codex credentials not found" } };
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
+ }
66
224
  }
67
225
  catch {
68
- return { candidates: [], status: { provider: "Pi", status: "unavailable", detail: "auth store not found" } };
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
+ }
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
+ };
69
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 };
70
291
  }
71
292
  function readPiAuthProviderMap(auth) {
72
- const nestedProviders = parseJsonObject(auth.providers);
73
- 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
+ };
74
377
  }
75
378
  async function discoverOpenCodeModels(opts) {
76
- const run = opts.execFile ?? execFile;
379
+ const run = providerDiscoveryCommand(opts);
380
+ let isolation;
77
381
  try {
78
- const { stdout } = await run("opencode", ["models", "--json"], { timeout: opts.timeoutMs });
79
- const models = parseModelNames(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;
80
393
  return {
81
- candidates: models.map((model) => ({
82
- value: model.startsWith("pi:") ? model : `pi:opencode-go:${model}`,
83
- label: `OpenCode ${displayModelName(model)}`,
84
- hint: "discovered from opencode",
85
- source: "opencode",
86
- discovered: true,
87
- })),
394
+ candidates: models.map((model) => {
395
+ const value = `pi:opencode-go:${model}`;
396
+ return {
397
+ value,
398
+ label: `OpenCode-Go ${displayModelName(model)}`,
399
+ hint: "discovered from opencode; OpenCode-Go API key required",
400
+ source: "opencode",
401
+ discovered: true,
402
+ availability: "catalog_available",
403
+ authState: "auth_required",
404
+ setupRequired: true,
405
+ supportedEfforts: [],
406
+ };
407
+ }),
88
408
  status: models.length > 0
89
- ? { provider: "OpenCode", status: "detected", detail: `${models.length} model(s) found` }
90
- : { 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" },
91
423
  };
92
424
  }
93
425
  catch {
94
- 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
+ };
95
473
  }
474
+ catch (error) {
475
+ await rm(root, { recursive: true, force: true }).catch(() => undefined);
476
+ throw error;
477
+ }
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;
96
515
  }
97
516
  async function discoverOllamaModels(opts) {
98
- const run = opts.execFile ?? execFile;
517
+ const run = providerDiscoveryCommand(opts);
99
518
  try {
100
519
  const { stdout } = await run("ollama", ["list"], { timeout: opts.timeoutMs });
101
520
  const models = parseOllamaList(stdout);
102
521
  return {
103
- candidates: models.map((model) => ({
104
- value: `pi:ollama:${model}`,
105
- label: `Ollama ${model}`,
106
- hint: "discovered locally",
107
- source: "ollama",
108
- discovered: true,
109
- })),
522
+ candidates: models.map((model) => {
523
+ const value = `pi:ollama:${model}`;
524
+ return {
525
+ value,
526
+ label: `Ollama ${model}`,
527
+ hint: "discovered locally",
528
+ source: "ollama",
529
+ discovered: true,
530
+ availability: "catalog_available",
531
+ authState: "not_required",
532
+ supportedEfforts: [],
533
+ };
534
+ }),
110
535
  status: models.length > 0
111
536
  ? { provider: "Ollama", status: "detected", detail: `${models.length} model(s) found` }
112
537
  : { provider: "Ollama", status: "unavailable", detail: "no local models returned" },
@@ -120,21 +545,30 @@ async function discoverLmStudioModels(opts) {
120
545
  const fetchImpl = opts.fetch ?? fetch;
121
546
  const controller = new AbortController();
122
547
  const timer = setTimeout(() => controller.abort(), opts.timeoutMs);
548
+ const onAbort = () => controller.abort();
549
+ opts.abortSignal?.addEventListener("abort", onAbort, { once: true });
123
550
  try {
124
551
  const response = await fetchImpl("http://localhost:1234/v1/models", { signal: controller.signal });
125
552
  if (!response.ok) {
126
553
  return { candidates: [], status: { provider: "LM Studio", status: "unavailable", detail: "server did not return models" } };
127
554
  }
128
555
  const body = await response.json();
129
- const models = parseOpenAiModelsBody(body);
556
+ const models = parseOpenAiModelEntriesBody(body);
130
557
  return {
131
- candidates: models.map((model) => ({
132
- value: `pi:lmstudio:${model}`,
133
- label: `LM Studio ${model}`,
134
- hint: "discovered locally",
135
- source: "lmstudio",
136
- discovered: true,
137
- })),
558
+ candidates: models.map((model) => {
559
+ const value = `pi:lmstudio:${model.id}`;
560
+ return {
561
+ value,
562
+ label: `LM Studio ${model.id}`,
563
+ hint: "discovered locally",
564
+ source: "lmstudio",
565
+ discovered: true,
566
+ availability: "catalog_available",
567
+ authState: "not_required",
568
+ supportedEfforts: model.reasoning === false ? ["none"] : [],
569
+ ...(model.reasoning === false ? { defaultEffort: "none" } : {}),
570
+ };
571
+ }),
138
572
  status: models.length > 0
139
573
  ? { provider: "LM Studio", status: "detected", detail: `${models.length} model(s) found` }
140
574
  : { provider: "LM Studio", status: "unavailable", detail: "no models returned" },
@@ -145,25 +579,68 @@ async function discoverLmStudioModels(opts) {
145
579
  }
146
580
  finally {
147
581
  clearTimeout(timer);
582
+ opts.abortSignal?.removeEventListener("abort", onAbort);
148
583
  }
149
584
  }
150
585
  function mergeCandidate(left, right) {
151
- 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;
597
+ const discovered = left.discovered === true || right.discovered === true;
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);
152
612
  return {
153
- ...left,
154
- ...right,
613
+ ...leftRest,
614
+ ...rightRest,
615
+ ...(preserveBuiltinPiMetadata ? { source: left.source } : {}),
616
+ ...(authState === undefined ? {} : { authState }),
617
+ supportedEfforts: preserveBuiltinPiMetadata
618
+ ? [...(leftSupportedEfforts ?? [])]
619
+ : [...(rightSupportedEfforts ?? leftSupportedEfforts ?? [])],
155
620
  ...(hint === undefined ? {} : { hint }),
156
- discovered: left.discovered === true || right.discovered === true,
621
+ ...(defaultEffort === undefined ? {} : { defaultEffort }),
622
+ discovered,
623
+ ...(setupRequired ? { setupRequired: true } : {}),
157
624
  };
158
625
  }
159
626
  function rank(candidate) {
160
- if (candidate.value === "claude:claude-sonnet-4-6") {
161
- return 0;
162
- }
163
- if (candidate.value === PI_OPENAI_CODEX) {
164
- return 10;
165
- }
166
- if (candidate.value === DIRECT_CODEX) {
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") {
167
644
  return 20;
168
645
  }
169
646
  if (candidate.source === "opencode") {
@@ -177,6 +654,324 @@ function rank(candidate) {
177
654
  }
178
655
  return 90;
179
656
  }
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
+ }
732
+ return {
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,
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(".");
974
+ }
180
975
  function parseOllamaList(stdout) {
181
976
  const lines = stdout.trim().split(/\r?\n/u).filter(Boolean);
182
977
  if (lines.length === 0) {
@@ -187,53 +982,83 @@ function parseOllamaList(stdout) {
187
982
  .map((line) => line.trim().split(/\s+/u)[0])
188
983
  .filter((name) => name !== undefined && name.length > 0);
189
984
  }
190
- function parseModelNames(stdout) {
191
- try {
192
- const parsed = JSON.parse(stdout);
193
- if (Array.isArray(parsed)) {
194
- return parsed.map(modelNameFromUnknown).filter(isNonEmptyString);
195
- }
196
- if (isRecord(parsed)) {
197
- for (const key of ["models", "data"]) {
198
- const value = parsed[key];
199
- if (Array.isArray(value)) {
200
- return value.map(modelNameFromUnknown).filter(isNonEmptyString);
201
- }
202
- }
203
- }
204
- }
205
- catch {
206
- // Fall through to tolerant line parsing.
207
- }
208
- return stdout.split(/\r?\n/u).map((line) => line.trim().split(/\s+/u)[0]).filter(isNonEmptyString);
985
+ function parseOpenCodeGoModels(stdout) {
986
+ const prefix = "opencode-go/";
987
+ return stdout
988
+ .split(/\r?\n/u)
989
+ .map((line) => line.trim())
990
+ .filter((line) => line.startsWith(prefix) && line.length > prefix.length)
991
+ .map((line) => line.slice(prefix.length));
209
992
  }
210
- function parseOpenAiModelsBody(body) {
993
+ function parseOpenAiModelEntriesBody(body) {
211
994
  if (!isRecord(body) || !Array.isArray(body.data)) {
212
995
  return [];
213
996
  }
214
- return body.data.map(modelNameFromUnknown).filter(isNonEmptyString);
997
+ return body.data.map(modelEntryFromUnknown).filter(isModelEntry);
215
998
  }
216
- function modelNameFromUnknown(value) {
999
+ function modelEntryFromUnknown(value) {
217
1000
  if (typeof value === "string") {
218
- return value;
1001
+ return { id: value };
219
1002
  }
220
1003
  if (!isRecord(value)) {
221
1004
  return undefined;
222
1005
  }
223
1006
  const raw = value.id ?? value.name ?? value.model;
224
- return typeof raw === "string" ? raw : undefined;
1007
+ if (typeof raw !== "string" || raw.length === 0) {
1008
+ return undefined;
1009
+ }
1010
+ const reasoning = readReasoningCapability(value);
1011
+ return {
1012
+ id: raw,
1013
+ ...(reasoning === undefined ? {} : { reasoning }),
1014
+ };
225
1015
  }
226
1016
  function displayModelName(model) {
227
1017
  return model.startsWith("pi:") ? model.split(":").slice(2).join(":") : model;
228
1018
  }
229
1019
  function parseJsonObject(value) {
230
1020
  const parsed = typeof value === "string" ? JSON.parse(value) : value;
231
- 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) ?? "";
1028
+ }
1029
+ function readReasoningCapability(value) {
1030
+ for (const field of ["reasoning", "supportsReasoning", "supports_reasoning", "thinking", "supportsThinking", "supports_thinking"]) {
1031
+ const result = booleanCapability(value[field]);
1032
+ if (result !== undefined) {
1033
+ return result;
1034
+ }
1035
+ }
1036
+ for (const field of ["capabilities", "features"]) {
1037
+ const nested = value[field];
1038
+ const result = Array.isArray(nested) ? arrayCapability(nested) : isRecord(nested) ? readReasoningCapability(nested) : undefined;
1039
+ if (result !== undefined) {
1040
+ return result;
1041
+ }
1042
+ }
1043
+ return undefined;
1044
+ }
1045
+ function booleanCapability(value) {
1046
+ return typeof value === "boolean" ? value : undefined;
1047
+ }
1048
+ function arrayCapability(value) {
1049
+ return value.some((entry) => typeof entry === "string" && /^(reasoning|thinking)$/iu.test(entry)) ? true : undefined;
1050
+ }
1051
+ function localModelDefaultEffort(model) {
1052
+ const normalized = model.toLowerCase();
1053
+ return ["gpt-oss", "qwen3", "qwq", "deepseek-r1", "reasoning", "thinking"].some((token) => normalized.includes(token))
1054
+ || /(?:^|[-_:/.\s])o[1345](?:$|[-_:/.\s])/u.test(normalized)
1055
+ ? "medium"
1056
+ : "none";
232
1057
  }
233
1058
  function isRecord(value) {
234
1059
  return typeof value === "object" && value !== null && !Array.isArray(value);
235
1060
  }
236
- function isNonEmptyString(value) {
237
- return value !== undefined && value.length > 0;
1061
+ function isModelEntry(value) {
1062
+ return value !== undefined;
238
1063
  }
239
1064
  //# sourceMappingURL=model-discovery.js.map