@martinloop/mcp 0.5.1 → 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/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 +3 -2
- package/dist/vendor/adapters/index.js +3 -2
- package/package.json +1 -1
- package/server.json +2 -2
package/README.md
CHANGED
|
@@ -6,11 +6,31 @@ But did it actually work? Did it pass your tests? How much did it spend? What fi
|
|
|
6
6
|
|
|
7
7
|
You don't know. And that's the problem.
|
|
8
8
|
|
|
9
|
+
## One system around the coding agent
|
|
10
|
+
|
|
11
|
+
MartinLoop is the execution-control system around coding agents. The coding agent still performs the software work. MartinLoop connects the run from Definition of Done through preflight, control, verification, recovery evidence, receipts, and post-run analysis.
|
|
12
|
+
|
|
13
|
+
```text
|
|
14
|
+
DEFINE -> PREFLIGHT -> CONTROL -> VERIFY -> RECOVER -> PROVE -> ANALYZE
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The product-level flow is **Definition of Done -> Controlled Run -> Verified Handoff**.
|
|
18
|
+
|
|
19
|
+
Through MCP, compatible hosts can use the same MartinLoop lifecycle without giving up the host's own model selection. MartinLoop does not inject a hidden fallback model.
|
|
20
|
+
|
|
21
|
+
For agent-readable product context see [`../../llms.txt`](../../llms.txt), [`../../llms-full.txt`](../../llms-full.txt), and [`../../docs/for-agents.md`](../../docs/for-agents.md).
|
|
22
|
+
|
|
9
23
|
## What This Does
|
|
10
24
|
|
|
11
|
-
MartinLoop
|
|
25
|
+
MartinLoop governs AI coding-agent execution. You define what the run needs to accomplish, set execution boundaries such as budget and verifier checks, and let the configured coding agent do the work inside that contract.
|
|
26
|
+
|
|
27
|
+
When it's done, you get a receipt — not a vague summary, but a structured record: dollars spent when available, attempts made, verification results, files changed, recovery state, and what the available evidence established.
|
|
28
|
+
|
|
29
|
+
The final governed outcome is one of:
|
|
12
30
|
|
|
13
|
-
|
|
31
|
+
- `VERIFIED` when the configured evidence supports the Definition of Done
|
|
32
|
+
- `STOPPED` when a configured hard boundary ends the run
|
|
33
|
+
- `NEEDS REVIEW` when completion cannot be established from the available evidence
|
|
14
34
|
|
|
15
35
|
**One line to connect it:**
|
|
16
36
|
|
|
@@ -18,7 +38,7 @@ When it's done, you get a receipt — not a vague summary, but a structured reco
|
|
|
18
38
|
claude mcp add martin-loop -- npx -y @martinloop/mcp
|
|
19
39
|
```
|
|
20
40
|
|
|
21
|
-
That's it. Your
|
|
41
|
+
That's it. Your host can now use MartinLoop's governed run and evidence surfaces.
|
|
22
42
|
|
|
23
43
|
## Real Numbers From Real Runs
|
|
24
44
|
|
|
@@ -37,27 +57,27 @@ These aren't hypotheticals. The $28 overshoot happened in production testing. We
|
|
|
37
57
|
|
|
38
58
|
### Claude Code
|
|
39
59
|
```sh
|
|
40
|
-
claude mcp add martin-loop -- npx -y @martinloop/mcp
|
|
60
|
+
claude mcp add martin-loop -- npx -y @martinloop/mcp@0.5.3
|
|
41
61
|
```
|
|
42
62
|
|
|
43
63
|
Windows:
|
|
44
64
|
```sh
|
|
45
|
-
claude mcp add --transport stdio --scope user martin-loop -- cmd /c npx -y @martinloop/mcp
|
|
65
|
+
claude mcp add --transport stdio --scope user martin-loop -- cmd /c npx -y @martinloop/mcp@0.5.3
|
|
46
66
|
```
|
|
47
67
|
|
|
48
68
|
### Codex
|
|
49
69
|
```sh
|
|
50
|
-
codex mcp add martin-loop -- npx -y @martinloop/mcp
|
|
70
|
+
codex mcp add martin-loop -- npx -y @martinloop/mcp@0.5.3
|
|
51
71
|
```
|
|
52
72
|
|
|
53
73
|
### Gemini CLI
|
|
54
74
|
```sh
|
|
55
|
-
gemini mcp add martin-loop -- npx -y @martinloop/mcp
|
|
75
|
+
gemini mcp add martin-loop -- npx -y @martinloop/mcp@0.5.3
|
|
56
76
|
```
|
|
57
77
|
|
|
58
78
|
### Any MCP Host
|
|
59
79
|
```sh
|
|
60
|
-
npx -y @martinloop/mcp
|
|
80
|
+
npx -y @martinloop/mcp@0.5.3
|
|
61
81
|
```
|
|
62
82
|
|
|
63
83
|
## How a Governed Run Works
|
|
@@ -69,14 +89,16 @@ You: "Fix the auth bug. Budget $3. Verify: npm test"
|
|
|
69
89
|
martin_plan → scopes the task, sets constraints
|
|
70
90
|
martin_preflight → validates before any spend
|
|
71
91
|
martin_run → agent works inside budget + verifier gates
|
|
72
|
-
martin_dossier → receipt:
|
|
92
|
+
martin_dossier → receipt: spend, attempts, verifier evidence, final outcome
|
|
73
93
|
```
|
|
74
94
|
|
|
75
|
-
Every attempt
|
|
95
|
+
Every attempt can be checked by your configured verifier. Budget and policy controls stay attached to the run. If the agent drifts off-task, the scope contract can block the change. If a hard spend boundary is reached, the run stops according to policy instead of silently retrying forever.
|
|
76
96
|
|
|
77
97
|
## What Your Agent Gets
|
|
78
98
|
|
|
79
|
-
###
|
|
99
|
+
### MCP Tools
|
|
100
|
+
|
|
101
|
+
The exact tool set is discoverable from the running MCP server with `tools/list`; do not rely on a hard-coded count in documentation.
|
|
80
102
|
|
|
81
103
|
**Run the loop:**
|
|
82
104
|
`martin_doctor` `martin_plan` `martin_preflight` `martin_run` `martin_pause` `martin_continue` `martin_cancel`
|
|
@@ -87,7 +109,9 @@ Every attempt runs your verifier. Every dollar is tracked. If the agent drifts o
|
|
|
87
109
|
**Ship the work:**
|
|
88
110
|
`martin_pr_summary` `martin_create_pr` `martin_review_pr`
|
|
89
111
|
|
|
90
|
-
###
|
|
112
|
+
### Read-Only Resources
|
|
113
|
+
|
|
114
|
+
The exact resource set is discoverable from the running MCP server; this documentation names the primary public resources without treating a count as a release invariant.
|
|
91
115
|
|
|
92
116
|
Your agent can pull context without side effects:
|
|
93
117
|
|
|
@@ -106,20 +130,33 @@ npx martin-loop mcp print-config --host claude --profile github-review # + PR w
|
|
|
106
130
|
|
|
107
131
|
## What Happens When Things Go Wrong
|
|
108
132
|
|
|
109
|
-
MartinLoop
|
|
133
|
+
MartinLoop does not round failure into success. It preserves the evidence and follows the configured recovery path while budget and policy still allow useful work.
|
|
110
134
|
|
|
111
135
|
| Failure | Old behavior | Now |
|
|
112
136
|
|---|---|---|
|
|
113
137
|
| CLI not on PATH | Error message, dead stop | Searches npm global, homebrew, nvm, scoop — uses what it finds |
|
|
114
|
-
| Verifier says "command not found" | Generic "verification failed" |
|
|
138
|
+
| Verifier says "command not found" | Generic "verification failed" | Carries specific failure context into the next allowed attempt |
|
|
115
139
|
| `git restore` fails mid-rollback | Throws, leaves dirty state | Retries once, falls back to `git checkout`, cleans up |
|
|
116
140
|
| Invalid `--profile` flag | Crashes | Warns, falls back to `minimal`, keeps running |
|
|
117
141
|
|
|
142
|
+
## Codex compatibility in 0.5.3
|
|
143
|
+
|
|
144
|
+
MartinLoop no longer assumes one Codex host or one fixed Codex flag set. The CLI resolves the exact Codex executable, reads its advertised capabilities, negotiates a supported write strategy, proves that strategy before reporting launch readiness, and reuses the same contract for the governed run.
|
|
145
|
+
|
|
146
|
+
That means MCP hosts can point MartinLoop at different Codex installations without requiring a documentation-specific approval or sandbox flag to exist everywhere.
|
|
147
|
+
|
|
148
|
+
## Presentation and evidence
|
|
149
|
+
|
|
150
|
+
MCP responses are Markdown-first for humans while retaining structured content for agents and automation. The presentation layer does not rewrite governed truth.
|
|
151
|
+
|
|
152
|
+
MartinLoop Arcade is a terminal-only experience. It does not run inside MCP and cannot influence MCP evidence, budgets, verification, or final outcomes.
|
|
153
|
+
|
|
118
154
|
## Who Uses This
|
|
119
155
|
|
|
120
156
|
- **Engineers running overnight agent loops** — you need a kill switch that actually works, and a receipt in the morning
|
|
121
157
|
- **Teams with shared API budgets** — you need per-task spend caps, not org-wide prayer
|
|
122
158
|
- **Anyone reviewing agent-generated PRs** — the dossier shows what the agent tried, what passed, and what didn't
|
|
159
|
+
- **Teams tired of stitching together point tools** — you want one execution-control lifecycle around coding-agent work from preflight through post-run analysis
|
|
123
160
|
|
|
124
161
|
## Requirements
|
|
125
162
|
|
|
@@ -129,6 +166,7 @@ MartinLoop doesn't just report failures — it tries to fix them:
|
|
|
129
166
|
## Links
|
|
130
167
|
|
|
131
168
|
- [martin-loop](https://www.npmjs.com/package/martin-loop) — the standalone CLI
|
|
169
|
+
- [MartinLoop for AI Agents](../../docs/for-agents.md)
|
|
132
170
|
- [GitHub](https://github.com/Keesan12/martin-loop) — source and issues
|
|
133
171
|
- [martinloop.com](https://martinloop.com)
|
|
134
172
|
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { type CodexCapabilityProfile, type CodexPromptTransport, type CodexWriteStrategy } from "./codex-capabilities.js";
|
|
3
|
+
export interface CliCommandAvailability {
|
|
4
|
+
command: string;
|
|
5
|
+
available: boolean;
|
|
6
|
+
locator: string;
|
|
7
|
+
detail: string;
|
|
8
|
+
resolvedPath?: string;
|
|
9
|
+
candidatePaths?: string[];
|
|
10
|
+
}
|
|
11
|
+
export type CodexHostPlatform = "windows" | "linux" | "wsl" | "macos";
|
|
12
|
+
export type CodexInstallKind = "missing" | "native" | "windows_shim" | "windows_mounted_path";
|
|
13
|
+
export type CodexInvocationMode = "direct" | "cmd_shell" | "powershell";
|
|
14
|
+
export interface CodexHostDiagnosis {
|
|
15
|
+
hostPlatform: CodexHostPlatform;
|
|
16
|
+
nativeInstallValid: boolean;
|
|
17
|
+
installKind: CodexInstallKind;
|
|
18
|
+
invocationMode: CodexInvocationMode;
|
|
19
|
+
sandboxMode: "workspace-write";
|
|
20
|
+
sandboxCompatible: boolean;
|
|
21
|
+
resolvedPath?: string;
|
|
22
|
+
nativeDependencyStatus?: "unknown" | "missing";
|
|
23
|
+
nativeDependencyPackage?: string;
|
|
24
|
+
warnings: string[];
|
|
25
|
+
remediation?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface CodexProbeCandidateResult {
|
|
28
|
+
path: string;
|
|
29
|
+
installKind: CodexInstallKind;
|
|
30
|
+
invocationMode: CodexInvocationMode;
|
|
31
|
+
nativeInstallValid: boolean;
|
|
32
|
+
sandboxCompatible: boolean;
|
|
33
|
+
launchReady: boolean;
|
|
34
|
+
summary: string;
|
|
35
|
+
remediation?: string;
|
|
36
|
+
nativeDependencyStatus?: "unknown" | "missing";
|
|
37
|
+
nativeDependencyPackage?: string;
|
|
38
|
+
capabilityProfile?: CodexCapabilityProfile;
|
|
39
|
+
writeStrategy?: CodexWriteStrategy;
|
|
40
|
+
promptTransport?: CodexPromptTransport;
|
|
41
|
+
}
|
|
42
|
+
export interface CodexLaunchProbeResult {
|
|
43
|
+
ok: boolean;
|
|
44
|
+
summary: string;
|
|
45
|
+
availability: CliCommandAvailability;
|
|
46
|
+
diagnosis: CodexHostDiagnosis;
|
|
47
|
+
command: string;
|
|
48
|
+
args: string[];
|
|
49
|
+
capabilityProfile?: CodexCapabilityProfile;
|
|
50
|
+
writeStrategy?: CodexWriteStrategy;
|
|
51
|
+
promptTransport?: CodexPromptTransport;
|
|
52
|
+
exitCode?: number;
|
|
53
|
+
stdout?: string;
|
|
54
|
+
stderr?: string;
|
|
55
|
+
candidateProbeResults?: CodexProbeCandidateResult[];
|
|
56
|
+
}
|
|
57
|
+
type SpawnSyncLike = typeof spawnSync;
|
|
58
|
+
export interface CodexSandboxPreflightOk {
|
|
59
|
+
ok: true;
|
|
60
|
+
effectiveSandbox: "read-only" | "workspace-write";
|
|
61
|
+
capabilitySource: "probe";
|
|
62
|
+
writableRoot: string;
|
|
63
|
+
}
|
|
64
|
+
export interface CodexSandboxPreflightReadOnly {
|
|
65
|
+
ok: false;
|
|
66
|
+
code: "provider_sandbox_read_only";
|
|
67
|
+
requestedCapability: "workspace-write";
|
|
68
|
+
detectedCapability: "read-only";
|
|
69
|
+
effectiveSandbox: "read-only";
|
|
70
|
+
affectedPath: string;
|
|
71
|
+
writableRoot: string;
|
|
72
|
+
capabilitySource: "probe";
|
|
73
|
+
remediation: string;
|
|
74
|
+
}
|
|
75
|
+
export type CodexSandboxPreflightOutcome = CodexSandboxPreflightOk | CodexSandboxPreflightReadOnly;
|
|
76
|
+
export declare function probeFilesystemWriteCapability(directory: string): {
|
|
77
|
+
writable: true;
|
|
78
|
+
} | {
|
|
79
|
+
writable: false;
|
|
80
|
+
reason: string;
|
|
81
|
+
};
|
|
82
|
+
export declare function checkCodexSandboxPreflight(input: {
|
|
83
|
+
requestedSandbox: "read-only" | "workspace-write";
|
|
84
|
+
workingDirectory: string;
|
|
85
|
+
}): CodexSandboxPreflightOutcome;
|
|
86
|
+
export declare function resolveCliCommandAvailability(command: string, options?: {
|
|
87
|
+
platform?: NodeJS.Platform;
|
|
88
|
+
env?: NodeJS.ProcessEnv;
|
|
89
|
+
spawnSyncImpl?: SpawnSyncLike;
|
|
90
|
+
}): CliCommandAvailability;
|
|
91
|
+
export declare function detectCodexHostPlatform(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): CodexHostPlatform;
|
|
92
|
+
export declare function diagnoseCodexHost(availability: CliCommandAvailability, options?: {
|
|
93
|
+
env?: NodeJS.ProcessEnv;
|
|
94
|
+
platform?: NodeJS.Platform;
|
|
95
|
+
}): CodexHostDiagnosis;
|
|
96
|
+
export declare function probeCodexLaunch(input: {
|
|
97
|
+
workingDirectory: string;
|
|
98
|
+
availability?: CliCommandAvailability;
|
|
99
|
+
env?: NodeJS.ProcessEnv;
|
|
100
|
+
platform?: NodeJS.Platform;
|
|
101
|
+
spawnSyncImpl?: SpawnSyncLike;
|
|
102
|
+
model?: string;
|
|
103
|
+
}): CodexLaunchProbeResult;
|
|
104
|
+
export {};
|