@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.
- package/README.md +52 -14
- package/dist/resources.js +15 -3
- package/dist/server.js +40 -8
- package/dist/tools/doctor.js +5 -4
- package/dist/tools/get-status.d.ts +2 -0
- package/dist/tools/get-status.js +1 -0
- package/dist/tools/preflight.js +1 -1
- package/dist/tools/run-loop.d.ts +6 -1
- package/dist/tools/run-loop.js +22 -7
- package/dist/tools/tool-errors.js +1 -1
- package/dist/tools/tool-response.js +9 -2
- package/dist/tools/tool-support.d.ts +3 -2
- package/dist/tools/tool-support.js +5 -3
- package/dist/vendor/adapters/claude-cli.d.ts +2 -2
- package/dist/vendor/adapters/claude-cli.js +59 -34
- package/dist/vendor/adapters/codex-capabilities.d.ts +60 -0
- package/dist/vendor/adapters/codex-capabilities.js +237 -0
- package/dist/vendor/adapters/codex-cli.d.ts +22 -0
- package/dist/vendor/adapters/codex-cli.js +62 -0
- package/dist/vendor/adapters/codex-host.d.ts +104 -0
- package/dist/vendor/adapters/codex-host.js +577 -0
- package/dist/vendor/adapters/codex-launcher.d.ts +7 -135
- package/dist/vendor/adapters/codex-launcher.js +17 -766
- package/dist/vendor/adapters/index.d.ts +4 -3
- package/dist/vendor/adapters/index.js +4 -3
- package/dist/vendor/adapters/openai-compatible.d.ts +2 -3
- package/dist/vendor/adapters/openai-compatible.js +33 -7
- package/dist/vendor/adapters/verifier-only.d.ts +9 -0
- package/dist/vendor/adapters/verifier-only.js +68 -0
- package/dist/vendor/contracts/index.d.ts +16 -6
- package/dist/vendor/contracts/index.js +1 -1
- package/dist/vendor/contracts/verified-handoff.d.ts +6 -0
- package/dist/vendor/contracts/verified-handoff.js +5 -0
- package/dist/vendor/core/grounding.d.ts +2 -1
- package/dist/vendor/core/grounding.js +40 -5
- package/dist/vendor/core/index.d.ts +4 -6
- package/dist/vendor/core/index.js +25 -19
- package/dist/vendor/core/persistence/runs-reader.d.ts +2 -0
- package/dist/vendor/core/policy.js +6 -6
- package/dist/vendor/core/red-blue/red-phase.d.ts +2 -1
- package/dist/vendor/core/red-blue/red-phase.js +3 -3
- package/dist/vendor/core/red-blue/risk-tiers.d.ts +2 -4
- package/dist/vendor/core/red-blue/risk-tiers.js +1 -3
- package/dist/vendor/core/routing.d.ts +0 -42
- package/dist/vendor/core/routing.js +4 -154
- package/dist/vendor/core/verified-handoff.d.ts +6 -1
- package/dist/vendor/core/verified-handoff.js +28 -0
- package/package.json +6 -2
- package/server.json +2 -2
- package/dist/vendor/adapters/stub-direct-provider.d.ts +0 -8
- 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
|
-
|
|
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 ? "
|
|
120
|
+
provenance: hasAuthoritativeCost ? "actual" : pricingResolution.pricing ? "calculated" : "unavailable",
|
|
106
121
|
providerSettlement: {
|
|
107
122
|
providerId: "claude",
|
|
108
|
-
model: modelLabel
|
|
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
|
|
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
|
|
173
|
-
|
|
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: "
|
|
210
|
+
provenance: exactPricing ? "calculated" : "estimated",
|
|
187
211
|
providerSettlement: {
|
|
188
212
|
providerId: "codex",
|
|
189
|
-
model: modelLabel
|
|
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
|
|
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: "
|
|
285
|
+
provenance: "estimated",
|
|
258
286
|
providerSettlement: {
|
|
259
287
|
providerId: "gemini",
|
|
260
|
-
model: modelLabel
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
+
}
|