@martinloop/mcp 0.5.0 → 0.5.3

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 (51) hide show
  1. package/README.md +52 -14
  2. package/dist/resources.js +15 -3
  3. package/dist/server.js +40 -8
  4. package/dist/tools/doctor.js +5 -4
  5. package/dist/tools/get-status.d.ts +2 -0
  6. package/dist/tools/get-status.js +1 -0
  7. package/dist/tools/preflight.js +1 -1
  8. package/dist/tools/run-loop.d.ts +6 -1
  9. package/dist/tools/run-loop.js +22 -7
  10. package/dist/tools/tool-errors.js +1 -1
  11. package/dist/tools/tool-response.js +9 -2
  12. package/dist/tools/tool-support.d.ts +3 -2
  13. package/dist/tools/tool-support.js +5 -3
  14. package/dist/vendor/adapters/claude-cli.d.ts +2 -2
  15. package/dist/vendor/adapters/claude-cli.js +59 -34
  16. package/dist/vendor/adapters/codex-capabilities.d.ts +60 -0
  17. package/dist/vendor/adapters/codex-capabilities.js +237 -0
  18. package/dist/vendor/adapters/codex-cli.d.ts +22 -0
  19. package/dist/vendor/adapters/codex-cli.js +62 -0
  20. package/dist/vendor/adapters/codex-host.d.ts +104 -0
  21. package/dist/vendor/adapters/codex-host.js +577 -0
  22. package/dist/vendor/adapters/codex-launcher.d.ts +7 -135
  23. package/dist/vendor/adapters/codex-launcher.js +17 -766
  24. package/dist/vendor/adapters/index.d.ts +4 -3
  25. package/dist/vendor/adapters/index.js +4 -3
  26. package/dist/vendor/adapters/openai-compatible.d.ts +2 -3
  27. package/dist/vendor/adapters/openai-compatible.js +33 -7
  28. package/dist/vendor/adapters/verifier-only.d.ts +9 -0
  29. package/dist/vendor/adapters/verifier-only.js +68 -0
  30. package/dist/vendor/contracts/index.d.ts +16 -6
  31. package/dist/vendor/contracts/index.js +1 -1
  32. package/dist/vendor/contracts/verified-handoff.d.ts +6 -0
  33. package/dist/vendor/contracts/verified-handoff.js +5 -0
  34. package/dist/vendor/core/grounding.d.ts +2 -1
  35. package/dist/vendor/core/grounding.js +40 -5
  36. package/dist/vendor/core/index.d.ts +4 -6
  37. package/dist/vendor/core/index.js +25 -19
  38. package/dist/vendor/core/persistence/runs-reader.d.ts +2 -0
  39. package/dist/vendor/core/policy.js +6 -6
  40. package/dist/vendor/core/red-blue/red-phase.d.ts +2 -1
  41. package/dist/vendor/core/red-blue/red-phase.js +3 -3
  42. package/dist/vendor/core/red-blue/risk-tiers.d.ts +2 -4
  43. package/dist/vendor/core/red-blue/risk-tiers.js +1 -3
  44. package/dist/vendor/core/routing.d.ts +0 -42
  45. package/dist/vendor/core/routing.js +4 -154
  46. package/dist/vendor/core/verified-handoff.d.ts +6 -1
  47. package/dist/vendor/core/verified-handoff.js +28 -0
  48. package/package.json +6 -2
  49. package/server.json +2 -2
  50. package/dist/vendor/adapters/stub-direct-provider.d.ts +0 -8
  51. package/dist/vendor/adapters/stub-direct-provider.js +0 -12
@@ -68,7 +68,22 @@ function calculateUsageCost(usage, pricing) {
68
68
  (usage.cacheCreationInputTokens / 1000) * (pricing.cacheCreationInputPer1K ?? pricing.inputPer1K) +
69
69
  (usage.outputTokens / 1000) * pricing.outputPer1K);
70
70
  }
71
- function extractUsage(parsed, modelLabel) {
71
+ const EMBEDDED_PRICING_VERSION = "embedded-v1";
72
+ function extractClaudeObservedModel(stdout) {
73
+ for (const line of stdout.split(/\r?\n/u)) {
74
+ try {
75
+ const event = JSON.parse(line);
76
+ if (event.type === "system" && event.subtype === "init" && typeof event.model === "string") {
77
+ return event.model;
78
+ }
79
+ }
80
+ catch {
81
+ // Ignore non-JSON stream fragments.
82
+ }
83
+ }
84
+ return undefined;
85
+ }
86
+ function extractUsage(parsed, modelLabel, modelSource) {
72
87
  if (!parsed?.usage) {
73
88
  return normalizeUsage({
74
89
  actualUsd: 0,
@@ -102,15 +117,24 @@ function extractUsage(parsed, modelLabel) {
102
117
  tokensIn,
103
118
  tokensOut,
104
119
  cachedInputTokens,
105
- provenance: hasAuthoritativeCost ? "actual" : pricingResolution.pricing ? "estimated" : "unavailable",
120
+ provenance: hasAuthoritativeCost ? "actual" : pricingResolution.pricing ? "calculated" : "unavailable",
106
121
  providerSettlement: {
107
122
  providerId: "claude",
108
- model: modelLabel ?? "claude",
123
+ ...(modelLabel ? { model: modelLabel } : {}),
109
124
  transport: "cli",
110
125
  source: "claude_json",
111
126
  inputTokens: promptTokens + cacheCreationInputTokens,
112
127
  cachedInputTokens,
128
+ cacheCreationInputTokens,
113
129
  outputTokens: tokensOut,
130
+ billingMode: "unknown",
131
+ modelSource,
132
+ pricingSource: hasAuthoritativeCost
133
+ ? "provider_reported_total"
134
+ : pricingResolution.pricing
135
+ ? "static_catalog"
136
+ : "none",
137
+ ...(pricingResolution.pricing ? { pricingVersion: EMBEDDED_PRICING_VERSION } : {}),
114
138
  rawUsageAvailable: true,
115
139
  settledAt: new Date().toISOString()
116
140
  }
@@ -152,7 +176,7 @@ function extractCodexJsonlResult(stdout, modelLabel) {
152
176
  provenance: "unavailable",
153
177
  providerSettlement: {
154
178
  providerId: "codex",
155
- model: modelLabel ?? "codex",
179
+ ...(modelLabel ? { model: modelLabel } : {}),
156
180
  transport: "cli",
157
181
  source: "unavailable",
158
182
  inputTokens: 0,
@@ -169,8 +193,8 @@ function extractCodexJsonlResult(stdout, modelLabel) {
169
193
  const reasoningOutputTokens = latestTurnCompleted.usage.reasoning_output_tokens ?? 0;
170
194
  const tokensIn = promptTokens + cachedInputTokens;
171
195
  const tokensOut = outputTokens + reasoningOutputTokens;
172
- const pricing = (modelLabel ? MODEL_PRICING[modelLabel] : undefined) ??
173
- MODEL_PRICING["codex"] ??
196
+ const exactPricing = modelLabel ? MODEL_PRICING[modelLabel] : undefined;
197
+ const pricing = exactPricing ?? MODEL_PRICING["codex"] ??
174
198
  { inputPer1K: BLENDED_INPUT_COST_PER_1K, outputPer1K: BLENDED_OUTPUT_COST_PER_1K };
175
199
  const actualUsd = (promptTokens / 1000) * pricing.inputPer1K +
176
200
  (cachedInputTokens / 1000) * (pricing.cachedInputPer1K ?? pricing.inputPer1K) +
@@ -183,16 +207,20 @@ function extractCodexJsonlResult(stdout, modelLabel) {
183
207
  tokensOut,
184
208
  cachedInputTokens,
185
209
  reasoningTokensOut: reasoningOutputTokens,
186
- provenance: "actual",
210
+ provenance: exactPricing ? "calculated" : "estimated",
187
211
  providerSettlement: {
188
212
  providerId: "codex",
189
- model: modelLabel ?? "codex",
213
+ ...(modelLabel ? { model: modelLabel } : {}),
190
214
  transport: "cli",
191
215
  source: "codex_jsonl",
192
216
  inputTokens: promptTokens,
193
217
  cachedInputTokens,
194
218
  outputTokens,
195
219
  reasoningOutputTokens,
220
+ billingMode: "unknown",
221
+ modelSource: modelLabel ? "explicit_override" : "agent_default",
222
+ pricingSource: exactPricing ? "static_catalog" : "blended_fallback",
223
+ pricingVersion: EMBEDDED_PRICING_VERSION,
196
224
  rawUsageAvailable: true,
197
225
  settledAt: new Date().toISOString()
198
226
  }
@@ -228,7 +256,7 @@ function extractGeminiJsonResult(stdout, modelLabel) {
228
256
  provenance: "unavailable",
229
257
  providerSettlement: {
230
258
  providerId: "gemini",
231
- model: modelLabel ?? "flash",
259
+ ...(modelLabel ? { model: modelLabel } : {}),
232
260
  transport: "cli",
233
261
  source: "unavailable",
234
262
  inputTokens: 0,
@@ -254,16 +282,20 @@ function extractGeminiJsonResult(stdout, modelLabel) {
254
282
  tokensOut,
255
283
  cachedInputTokens,
256
284
  reasoningTokensOut: reasoningOutputTokens,
257
- provenance: "actual",
285
+ provenance: "estimated",
258
286
  providerSettlement: {
259
287
  providerId: "gemini",
260
- model: modelLabel ?? "flash",
288
+ ...(modelLabel ? { model: modelLabel } : {}),
261
289
  transport: "cli",
262
290
  source: "gemini_json",
263
291
  inputTokens: promptTokens,
264
292
  cachedInputTokens,
265
293
  outputTokens,
266
294
  reasoningOutputTokens,
295
+ billingMode: "unknown",
296
+ modelSource: modelLabel ? "explicit_override" : "agent_default",
297
+ pricingSource: "blended_fallback",
298
+ pricingVersion: EMBEDDED_PRICING_VERSION,
267
299
  rawUsageAvailable: true,
268
300
  settledAt: new Date().toISOString()
269
301
  }
@@ -497,7 +529,7 @@ export function createAgentCliAdapter(options) {
497
529
  label: options.label ?? `${options.command} CLI adapter`,
498
530
  metadata: {
499
531
  providerId: options.command,
500
- model: options.model ?? options.command,
532
+ ...(options.model ? { model: options.model } : {}),
501
533
  transport: "cli",
502
534
  capabilities: createAdapterCapabilities({
503
535
  preflight: true,
@@ -665,8 +697,15 @@ export function createAgentCliAdapter(options) {
665
697
  parsed?.result ??
666
698
  agentResult.stdout.trim();
667
699
  const summary = truncate(agentText, 2000);
700
+ const observedClaudeModel = options.streamingUsageCap
701
+ ? extractClaudeObservedModel(agentResult.stdout)
702
+ : undefined;
668
703
  const usage = parsed?.usage
669
- ? extractUsage(parsed, options.model)
704
+ ? extractUsage(parsed, observedClaudeModel ?? options.model, observedClaudeModel
705
+ ? "provider_reported"
706
+ : options.model
707
+ ? "explicit_override"
708
+ : "agent_default")
670
709
  : codexJsonlResult?.usage ??
671
710
  geminiJsonResult?.usage ??
672
711
  normalizeUsage({
@@ -678,7 +717,7 @@ export function createAgentCliAdapter(options) {
678
717
  providerSettlement: options.command === "codex"
679
718
  ? {
680
719
  providerId: "codex",
681
- model: options.model ?? "codex",
720
+ ...(options.model ? { model: options.model } : {}),
682
721
  transport: "cli",
683
722
  source: "estimated_fallback",
684
723
  inputTokens: estimatedUsage.tokensIn,
@@ -689,7 +728,7 @@ export function createAgentCliAdapter(options) {
689
728
  : options.command === "gemini"
690
729
  ? {
691
730
  providerId: "gemini",
692
- model: options.model ?? "flash",
731
+ ...(options.model ? { model: options.model } : {}),
693
732
  transport: "cli",
694
733
  source: "estimated_fallback",
695
734
  inputTokens: estimatedUsage.tokensIn,
@@ -830,18 +869,6 @@ export function createAgentCliAdapter(options) {
830
869
  ...(classHint ? { classHint } : {})
831
870
  }
832
871
  };
833
- },
834
- /**
835
- * Return a new adapter instance with a different model.
836
- * Used by run-martin.ts when a change_model intervention fires.
837
- * Model escalation order: haiku → sonnet → opus (cheapest-first, escalate on repeated failure).
838
- */
839
- withModel(newModel) {
840
- return createAgentCliAdapter({
841
- ...options,
842
- model: newModel,
843
- adapterIdSuffix: `${options.adapterIdSuffix ?? options.command}:${newModel}`
844
- });
845
872
  }
846
873
  };
847
874
  return adapter;
@@ -870,7 +897,7 @@ export function createClaudeCliAdapter(options = {}) {
870
897
  return createAgentCliAdapter({
871
898
  command: "claude",
872
899
  adapterIdSuffix: "claude",
873
- model: options.model ?? "claude-sonnet-4-6",
900
+ model: options.model,
874
901
  label: options.label ?? "Claude CLI adapter",
875
902
  workingDirectory: options.workingDirectory,
876
903
  timeoutMs: options.timeoutMs,
@@ -921,7 +948,7 @@ export function createCodexCliAdapter(options = {}) {
921
948
  return createAgentCliAdapter({
922
949
  command,
923
950
  adapterIdSuffix: "codex",
924
- model: options.model ?? "codex",
951
+ model: options.model,
925
952
  label: options.label ?? "Codex CLI adapter",
926
953
  workingDirectory,
927
954
  timeoutMs: options.timeoutMs,
@@ -942,7 +969,7 @@ export function createCodexCliAdapter(options = {}) {
942
969
  // Pre-configured: Gemini CLI
943
970
  // ---------------------------------------------------------------------------
944
971
  /**
945
- * Spawns `gemini --model <model> --prompt "" --approval-mode <mode> --output-format json [...]`.
972
+ * Spawns `gemini [--model <explicit-model>] --prompt "" --approval-mode <mode> --output-format json [...]`.
946
973
  *
947
974
  * The prompt is delivered via stdin while forcing headless mode with `--prompt ""`,
948
975
  * which keeps large MartinLoop prompts off the command line on Windows.
@@ -951,13 +978,12 @@ export function createCodexCliAdapter(options = {}) {
951
978
  * npm install -g @google/gemini-cli
952
979
  */
953
980
  export function createGeminiCliAdapter(options = {}) {
954
- const model = options.model ?? "flash";
955
981
  const approvalMode = options.approvalMode ?? "yolo";
956
982
  const extraArgs = options.extraArgs ?? [];
957
983
  return createAgentCliAdapter({
958
984
  command: "gemini",
959
985
  adapterIdSuffix: "gemini",
960
- model,
986
+ model: options.model,
961
987
  label: options.label ?? "Gemini CLI adapter",
962
988
  workingDirectory: options.workingDirectory,
963
989
  timeoutMs: options.timeoutMs,
@@ -965,8 +991,7 @@ export function createGeminiCliAdapter(options = {}) {
965
991
  supportsJsonOutput: false,
966
992
  spawnImpl: options.spawnImpl,
967
993
  argsBuilder: () => [
968
- "--model",
969
- model,
994
+ ...(options.model ? ["--model", options.model] : []),
970
995
  "--prompt",
971
996
  "",
972
997
  "--approval-mode",
@@ -0,0 +1,60 @@
1
+ import { spawnSync } from "node:child_process";
2
+ export type CodexFlagScope = "global" | "exec";
3
+ export type CodexPromptTransport = "stdin-dash" | "argv";
4
+ export type CodexWriteStrategy = "sandbox+approval" | "sandbox" | "automation" | "approval" | "default";
5
+ export interface CodexCapabilityFlag {
6
+ flag: string;
7
+ scope: CodexFlagScope;
8
+ }
9
+ export interface CodexSandboxCapability extends CodexCapabilityFlag {
10
+ values: string[];
11
+ }
12
+ export interface CodexApprovalCapability extends CodexCapabilityFlag {
13
+ semantics: "approval-policy" | "automation-mode";
14
+ value?: string;
15
+ }
16
+ export interface CodexCapabilityProfile {
17
+ binaryPath: string;
18
+ supportsExec: boolean;
19
+ probeSucceeded: boolean;
20
+ probeError?: string;
21
+ model?: CodexCapabilityFlag;
22
+ cwd?: CodexCapabilityFlag;
23
+ sandbox?: CodexSandboxCapability;
24
+ /** Preferred compatibility alias. `automation` and `approvalPolicy` are authoritative. */
25
+ approval?: CodexApprovalCapability;
26
+ automation?: CodexApprovalCapability;
27
+ approvalPolicy?: CodexApprovalCapability;
28
+ json?: CodexCapabilityFlag;
29
+ color?: CodexCapabilityFlag & {
30
+ neverValue?: string;
31
+ };
32
+ userConfigIsolation?: CodexCapabilityFlag;
33
+ promptTransport: CodexPromptTransport;
34
+ promptTransports?: CodexPromptTransport[];
35
+ selectedWriteStrategy?: CodexWriteStrategy;
36
+ }
37
+ export interface CodexExecArgsOptions {
38
+ command?: string;
39
+ workingDirectory: string;
40
+ sandbox?: "read-only" | "workspace-write" | "danger-full-access";
41
+ model?: string;
42
+ extraArgs?: string[];
43
+ mode?: "prompt" | "probe";
44
+ prompt?: string;
45
+ capabilityProfile?: CodexCapabilityProfile;
46
+ promptTransport?: CodexPromptTransport;
47
+ writeStrategy?: CodexWriteStrategy;
48
+ }
49
+ type SpawnSyncLike = typeof spawnSync;
50
+ export declare function clearCodexCapabilityCacheForTests(): void;
51
+ export declare function cacheCodexCapabilityProfile(profile: CodexCapabilityProfile, platform?: NodeJS.Platform): CodexCapabilityProfile;
52
+ export declare function probeCodexCapabilities(binaryPath: string, options?: {
53
+ platform?: NodeJS.Platform;
54
+ spawnSyncImpl?: SpawnSyncLike;
55
+ cache?: boolean;
56
+ }): CodexCapabilityProfile;
57
+ export declare function codexWriteStrategies(profile: CodexCapabilityProfile): CodexWriteStrategy[];
58
+ export declare function buildCodexExecArgs(options: CodexExecArgsOptions): string[];
59
+ export declare function buildCodexStdin(profile: CodexCapabilityProfile, prompt: string, transport?: CodexPromptTransport): string | undefined;
60
+ export {};
@@ -0,0 +1,237 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { extname } from "node:path";
3
+ import { createSpawnPlan, resolveNpmShimScript } from "./cli-bridge.js";
4
+ const capabilityCache = new Map();
5
+ function escapeRegex(value) {
6
+ return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
7
+ }
8
+ function flagPattern(flag) {
9
+ return new RegExp(`(?:^|\\s)${escapeRegex(flag)}(?=\\s|[=,<\\[]|$)`, "imu");
10
+ }
11
+ function locateFlag(globalHelp, execHelp, candidates) {
12
+ for (const flag of candidates) {
13
+ if (flagPattern(flag).test(execHelp))
14
+ return { flag, scope: "exec" };
15
+ }
16
+ for (const flag of candidates) {
17
+ if (flagPattern(flag).test(globalHelp))
18
+ return { flag, scope: "global" };
19
+ }
20
+ return undefined;
21
+ }
22
+ function flagContext(help, flag) {
23
+ const lines = help.split(/\r?\n/u);
24
+ const index = lines.findIndex((line) => line.includes(flag));
25
+ if (index < 0)
26
+ return "";
27
+ return lines.slice(Math.max(0, index - 1), Math.min(lines.length, index + 3)).join("\n");
28
+ }
29
+ function buildInjectedSpawnPlan(binaryPath, args, platform) {
30
+ if (platform !== "win32")
31
+ return { command: binaryPath, args };
32
+ const extension = extname(binaryPath).toLowerCase();
33
+ if (extension === ".cmd" || extension === ".bat" || extension === ".ps1") {
34
+ const script = resolveNpmShimScript(binaryPath);
35
+ if (script)
36
+ return { command: process.execPath, args: [script, ...args] };
37
+ if (extension === ".ps1") {
38
+ return {
39
+ command: "powershell.exe",
40
+ args: ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", binaryPath, ...args]
41
+ };
42
+ }
43
+ return {
44
+ command: process.env.ComSpec || "cmd.exe",
45
+ args: ["/d", "/c", binaryPath, ...args]
46
+ };
47
+ }
48
+ return { command: binaryPath, args };
49
+ }
50
+ function runHelpProbe(input) {
51
+ const plan = input.injected
52
+ ? buildInjectedSpawnPlan(input.binaryPath, input.args, input.platform)
53
+ : createSpawnPlan(input.binaryPath, input.args, process.cwd(), false);
54
+ const result = input.spawnSyncImpl(plan.command, plan.args, {
55
+ encoding: "utf8",
56
+ stdio: ["ignore", "pipe", "pipe"],
57
+ timeout: 8_000
58
+ });
59
+ return {
60
+ text: `${result.stdout ?? ""}\n${result.stderr ?? ""}`,
61
+ status: result.status,
62
+ ...(result.error ? { error: result.error.message } : {})
63
+ };
64
+ }
65
+ function parsePromptTransports(execHelp) {
66
+ const transports = [];
67
+ const stdinAdvertised = /\bstdin\b/iu.test(execHelp) || /(?:^|\s)-(?:\s|,|$).*prompt/imu.test(execHelp);
68
+ const argvAdvertised = /(?:\[|<)PROMPT(?:\]|>)/iu.test(execHelp) || /\bPROMPT\b/iu.test(execHelp);
69
+ if (stdinAdvertised)
70
+ transports.push("stdin-dash");
71
+ if (argvAdvertised || !stdinAdvertised)
72
+ transports.push("argv");
73
+ return [...new Set(transports)];
74
+ }
75
+ export function clearCodexCapabilityCacheForTests() {
76
+ capabilityCache.clear();
77
+ }
78
+ export function cacheCodexCapabilityProfile(profile, platform = process.platform) {
79
+ capabilityCache.set(`${platform}:${profile.binaryPath}`, profile);
80
+ return profile;
81
+ }
82
+ export function probeCodexCapabilities(binaryPath, options = {}) {
83
+ const platform = options.platform ?? process.platform;
84
+ const key = `${platform}:${binaryPath}`;
85
+ const useCache = options.cache ?? options.spawnSyncImpl === undefined;
86
+ if (useCache) {
87
+ const cached = capabilityCache.get(key);
88
+ if (cached)
89
+ return cached;
90
+ }
91
+ const spawnSyncImpl = options.spawnSyncImpl ?? spawnSync;
92
+ const injected = options.spawnSyncImpl !== undefined;
93
+ const globalProbe = runHelpProbe({
94
+ binaryPath,
95
+ args: ["--help"],
96
+ platform,
97
+ spawnSyncImpl,
98
+ injected
99
+ });
100
+ const execProbe = runHelpProbe({
101
+ binaryPath,
102
+ args: ["exec", "--help"],
103
+ platform,
104
+ spawnSyncImpl,
105
+ injected
106
+ });
107
+ const globalHelp = globalProbe.text;
108
+ const execHelp = execProbe.text;
109
+ const supportsExec = execProbe.status === 0 &&
110
+ !/(unknown command|unrecognized subcommand|unexpected argument ['"]?exec)/iu.test(execHelp);
111
+ const sandboxFlag = locateFlag(globalHelp, execHelp, ["--sandbox", "-s"]);
112
+ const sandboxHelp = sandboxFlag
113
+ ? `${flagContext(globalHelp, sandboxFlag.flag)}\n${flagContext(execHelp, sandboxFlag.flag)}`
114
+ : "";
115
+ const sandboxValues = ["read-only", "workspace-write", "danger-full-access"].filter((value) => new RegExp(`\\b${escapeRegex(value)}\\b`, "iu").test(sandboxHelp));
116
+ const automationFlag = locateFlag(globalHelp, execHelp, ["--approve-for-me", "--full-auto"]);
117
+ const automation = automationFlag
118
+ ? { ...automationFlag, semantics: "automation-mode" }
119
+ : undefined;
120
+ const approvalFlag = locateFlag(globalHelp, execHelp, ["--ask-for-approval", "-a"]);
121
+ const approvalHelp = approvalFlag
122
+ ? `${flagContext(globalHelp, approvalFlag.flag)}\n${flagContext(execHelp, approvalFlag.flag)}`
123
+ : "";
124
+ const approvalPolicy = approvalFlag && /\bnever\b/iu.test(approvalHelp)
125
+ ? { ...approvalFlag, semantics: "approval-policy", value: "never" }
126
+ : undefined;
127
+ const colorFlag = locateFlag(globalHelp, execHelp, ["--color"]);
128
+ const colorHelp = colorFlag
129
+ ? `${flagContext(globalHelp, colorFlag.flag)}\n${flagContext(execHelp, colorFlag.flag)}`
130
+ : "";
131
+ const promptTransports = parsePromptTransports(execHelp);
132
+ const probeError = globalProbe.error ?? execProbe.error;
133
+ const profile = {
134
+ binaryPath,
135
+ supportsExec,
136
+ probeSucceeded: supportsExec && `${globalHelp}\n${execHelp}`.trim().length > 0,
137
+ ...(probeError ? { probeError } : {}),
138
+ ...(locateFlag(globalHelp, execHelp, ["--model", "-m"])
139
+ ? { model: locateFlag(globalHelp, execHelp, ["--model", "-m"]) }
140
+ : {}),
141
+ ...(locateFlag(globalHelp, execHelp, ["--cd", "--cwd", "--working-dir", "-C"])
142
+ ? { cwd: locateFlag(globalHelp, execHelp, ["--cd", "--cwd", "--working-dir", "-C"]) }
143
+ : {}),
144
+ ...(sandboxFlag ? { sandbox: { ...sandboxFlag, values: sandboxValues } } : {}),
145
+ ...(automation ? { automation, approval: automation } : {}),
146
+ ...(approvalPolicy
147
+ ? {
148
+ approvalPolicy,
149
+ ...(!automation ? { approval: approvalPolicy } : {})
150
+ }
151
+ : {}),
152
+ ...(locateFlag(globalHelp, execHelp, ["--json"])
153
+ ? { json: locateFlag(globalHelp, execHelp, ["--json"]) }
154
+ : {}),
155
+ ...(colorFlag
156
+ ? {
157
+ color: {
158
+ ...colorFlag,
159
+ ...(/\bnever\b/iu.test(colorHelp) ? { neverValue: "never" } : {})
160
+ }
161
+ }
162
+ : {}),
163
+ ...(locateFlag(globalHelp, execHelp, ["--ignore-user-config", "--no-user-config"])
164
+ ? { userConfigIsolation: locateFlag(globalHelp, execHelp, ["--ignore-user-config", "--no-user-config"]) }
165
+ : {}),
166
+ promptTransport: promptTransports[0] ?? "argv",
167
+ promptTransports
168
+ };
169
+ if (useCache)
170
+ cacheCodexCapabilityProfile(profile, platform);
171
+ return profile;
172
+ }
173
+ function pushCapabilityArg(globalArgs, execArgs, capability, ...values) {
174
+ (capability.scope === "global" ? globalArgs : execArgs).push(capability.flag, ...values);
175
+ }
176
+ export function codexWriteStrategies(profile) {
177
+ const strategies = [];
178
+ const hasWorkspaceWrite = profile.sandbox?.values.includes("workspace-write") === true;
179
+ if (hasWorkspaceWrite && profile.approvalPolicy?.value === "never")
180
+ strategies.push("sandbox+approval");
181
+ if (hasWorkspaceWrite)
182
+ strategies.push("sandbox");
183
+ if (profile.automation)
184
+ strategies.push("automation");
185
+ if (profile.approvalPolicy?.value === "never")
186
+ strategies.push("approval");
187
+ strategies.push("default");
188
+ return [...new Set(strategies)];
189
+ }
190
+ export function buildCodexExecArgs(options) {
191
+ const profile = options.capabilityProfile ?? probeCodexCapabilities(options.command ?? "codex");
192
+ if (!profile.supportsExec) {
193
+ throw new Error(`Resolved Codex binary ${profile.binaryPath} does not advertise a usable exec subcommand.`);
194
+ }
195
+ const globalArgs = [];
196
+ const execArgs = [];
197
+ const requestedSandbox = options.sandbox ?? "workspace-write";
198
+ const strategy = options.writeStrategy ?? profile.selectedWriteStrategy ?? codexWriteStrategies(profile)[0] ?? "default";
199
+ const promptTransport = options.promptTransport ?? profile.promptTransport;
200
+ if (profile.userConfigIsolation)
201
+ pushCapabilityArg(globalArgs, execArgs, profile.userConfigIsolation);
202
+ if (profile.cwd)
203
+ pushCapabilityArg(globalArgs, execArgs, profile.cwd, options.workingDirectory);
204
+ if (requestedSandbox === "workspace-write") {
205
+ if ((strategy === "sandbox" || strategy === "sandbox+approval") && profile.sandbox?.values.includes("workspace-write")) {
206
+ pushCapabilityArg(globalArgs, execArgs, profile.sandbox, "workspace-write");
207
+ }
208
+ if (strategy === "automation" && profile.automation) {
209
+ pushCapabilityArg(globalArgs, execArgs, profile.automation);
210
+ }
211
+ if ((strategy === "approval" || strategy === "sandbox+approval") && profile.approvalPolicy?.value) {
212
+ pushCapabilityArg(globalArgs, execArgs, profile.approvalPolicy, profile.approvalPolicy.value);
213
+ }
214
+ }
215
+ else {
216
+ if (!profile.sandbox?.values.includes(requestedSandbox)) {
217
+ throw new Error(`Resolved Codex binary ${profile.binaryPath} does not advertise requested sandbox mode ${requestedSandbox}.`);
218
+ }
219
+ pushCapabilityArg(globalArgs, execArgs, profile.sandbox, requestedSandbox);
220
+ }
221
+ if (profile.json)
222
+ pushCapabilityArg(globalArgs, execArgs, profile.json);
223
+ if (profile.color?.neverValue)
224
+ pushCapabilityArg(globalArgs, execArgs, profile.color, profile.color.neverValue);
225
+ if (options.model) {
226
+ if (!profile.model) {
227
+ throw new Error(`Resolved Codex binary ${profile.binaryPath} does not advertise a model override flag.`);
228
+ }
229
+ pushCapabilityArg(globalArgs, execArgs, profile.model, options.model);
230
+ }
231
+ const prompt = options.prompt ?? "";
232
+ const promptArgs = promptTransport === "stdin-dash" ? ["-"] : prompt ? [prompt] : [];
233
+ return [...globalArgs, "exec", ...execArgs, ...(options.extraArgs ?? []), ...promptArgs];
234
+ }
235
+ export function buildCodexStdin(profile, prompt, transport = profile.promptTransport) {
236
+ return transport === "stdin-dash" ? prompt : undefined;
237
+ }
@@ -0,0 +1,22 @@
1
+ import { type CodexCliAdapterOptions as LegacyCodexCliAdapterOptions } from "./claude-cli.js";
2
+ import { type CodexCapabilityProfile } from "./codex-launcher.js";
3
+ export interface CodexCliAdapterOptions extends Omit<LegacyCodexCliAdapterOptions, "command"> {
4
+ /** Exact resolved executable selected by the Codex launch probe. */
5
+ command?: string;
6
+ /**
7
+ * Optional pre-probed capability profile. Production callers normally omit
8
+ * this because probeCodexCapabilities caches profiles by exact binary path
9
+ * for the process lifetime. Exposed for deterministic integrations/tests.
10
+ */
11
+ capabilityProfile?: CodexCapabilityProfile;
12
+ }
13
+ /**
14
+ * Capability-driven Codex CLI adapter.
15
+ *
16
+ * Provider identity remains `codex` even when doctor/preflight selected an
17
+ * absolute native binary or npm shim. The selected binary's cached capability
18
+ * profile builds both the real argv and stdin transport, while the spawn router
19
+ * sends only the Codex subprocess to that exact executable. Git/verifier
20
+ * subprocesses retain their normal commands.
21
+ */
22
+ export declare function createCodexCliAdapter(options?: CodexCliAdapterOptions): import("../core/index.js").MartinAdapter;
@@ -0,0 +1,62 @@
1
+ import { spawn } from "node:child_process";
2
+ import { createAgentCliAdapter } from "./claude-cli.js";
3
+ import { buildCodexExecArgs, buildCodexStdin, probeCodexCapabilities } from "./codex-launcher.js";
4
+ import { createSpawnPlan } from "./cli-bridge.js";
5
+ function createCodexSpawnRouter(input) {
6
+ if (input.injectedSpawn) {
7
+ return (command, args = [], options) => input.injectedSpawn?.(command === "codex" ? input.selectedBinary : command, args, options);
8
+ }
9
+ return (command, args = [], options) => {
10
+ const executable = command === "codex" ? input.selectedBinary : command;
11
+ const cwd = typeof options?.cwd === "string" ? options.cwd : input.workingDirectory;
12
+ const plan = createSpawnPlan(executable, [...args], cwd, false);
13
+ return spawn(plan.command, plan.args, options ?? {});
14
+ };
15
+ }
16
+ /**
17
+ * Capability-driven Codex CLI adapter.
18
+ *
19
+ * Provider identity remains `codex` even when doctor/preflight selected an
20
+ * absolute native binary or npm shim. The selected binary's cached capability
21
+ * profile builds both the real argv and stdin transport, while the spawn router
22
+ * sends only the Codex subprocess to that exact executable. Git/verifier
23
+ * subprocesses retain their normal commands.
24
+ */
25
+ export function createCodexCliAdapter(options = {}) {
26
+ const workingDirectory = options.workingDirectory ?? process.cwd();
27
+ const selectedBinary = options.command ?? "codex";
28
+ const capabilityProfile = options.capabilityProfile ?? probeCodexCapabilities(selectedBinary);
29
+ const sandbox = options.sandbox ?? "workspace-write";
30
+ const extraArgs = options.extraArgs ?? [];
31
+ const launchModel = options.model;
32
+ const spawnImpl = createCodexSpawnRouter({
33
+ selectedBinary,
34
+ workingDirectory,
35
+ ...(options.spawnImpl ? { injectedSpawn: options.spawnImpl } : {})
36
+ });
37
+ return createAgentCliAdapter({
38
+ // Keep semantic provider identity stable for usage parsing, pricing,
39
+ // diagnostics, and adapter metadata. The spawn router maps this to the
40
+ // exact selected binary at process launch time.
41
+ command: "codex",
42
+ adapterIdSuffix: "codex",
43
+ model: options.model,
44
+ label: options.label ?? "Codex CLI adapter",
45
+ workingDirectory,
46
+ timeoutMs: options.timeoutMs,
47
+ verifyTimeoutMs: options.verifyTimeoutMs,
48
+ supportsJsonOutput: false,
49
+ spawnImpl,
50
+ argsBuilder: (prompt) => buildCodexExecArgs({
51
+ command: selectedBinary,
52
+ workingDirectory,
53
+ sandbox,
54
+ ...(launchModel ? { model: launchModel } : {}),
55
+ extraArgs,
56
+ mode: "prompt",
57
+ prompt,
58
+ capabilityProfile
59
+ }),
60
+ stdinBuilder: (prompt) => buildCodexStdin(capabilityProfile, prompt)
61
+ });
62
+ }