@dhrupo/codex-claude-bridge 0.1.0 → 0.1.1

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 CHANGED
@@ -1,17 +1,25 @@
1
+ [![npm version](https://img.shields.io/npm/v/%40dhrupo%2Fcodex-claude-bridge)](https://www.npmjs.com/package/@dhrupo/codex-claude-bridge)
2
+ [![CI](https://github.com/dhrupo/codex-claude-bridge/actions/workflows/ci.yml/badge.svg)](https://github.com/dhrupo/codex-claude-bridge/actions/workflows/ci.yml)
3
+ [![npm publish](https://github.com/dhrupo/codex-claude-bridge/actions/workflows/publish.yml/badge.svg)](https://github.com/dhrupo/codex-claude-bridge/actions/workflows/publish.yml)
4
+
1
5
  # codex-claude-bridge
2
6
 
3
7
  Use Claude Code from inside Codex.
4
8
 
5
- This package exposes Claude Code as MCP tools that Codex can call, and also ships a small `codex-claude` command for common flows.
9
+ `@dhrupo/codex-claude-bridge` exposes Claude Code as MCP tools that Codex can call, and also ships a `codex-claude` helper command for common workflows.
6
10
 
7
11
  ## What You Get
8
12
 
9
13
  - `codex-claude-mcp`: an MCP server that wraps the local `claude` CLI
10
- - `codex-claude`: a helper command that drives Codex to call the Claude MCP tools
14
+ - `codex-claude`: a helper command that asks Codex to call Claude tools
11
15
  - Three MCP tools:
12
16
  - `claude_ask`
13
17
  - `claude_review`
14
18
  - `claude_delegate`
19
+ - Extra helper workflows:
20
+ - `codex-claude status`
21
+ - `codex-claude doctor`
22
+ - `codex-claude compare`
15
23
 
16
24
  ## Requirements
17
25
 
@@ -19,7 +27,7 @@ This package exposes Claude Code as MCP tools that Codex can call, and also ship
19
27
  - `codex` installed and authenticated
20
28
  - `claude` installed and authenticated
21
29
 
22
- Check both CLIs first:
30
+ Quick checks:
23
31
 
24
32
  ```bash
25
33
  codex --version
@@ -35,37 +43,34 @@ Install the package:
35
43
  npm install -g @dhrupo/codex-claude-bridge
36
44
  ```
37
45
 
38
- Register the MCP server in Codex:
46
+ Register the MCP bridge in Codex:
39
47
 
40
48
  ```bash
41
49
  codex-claude install
42
50
  ```
43
51
 
44
- That adds a global Codex MCP server named `claude-bridge`.
52
+ That creates a global Codex MCP server named `claude-bridge`.
45
53
 
46
- You can also register it manually:
54
+ Manual alternatives:
47
55
 
48
56
  ```bash
49
57
  codex mcp add claude-bridge -- codex-claude-mcp
50
- ```
51
-
52
- Or without a global npm install:
53
-
54
- ```bash
55
58
  codex mcp add claude-bridge -- npx -y @dhrupo/codex-claude-bridge
56
59
  ```
57
60
 
58
- Confirm registration:
61
+ Verify:
59
62
 
60
63
  ```bash
61
64
  codex mcp list
65
+ codex-claude status
66
+ codex-claude doctor
62
67
  ```
63
68
 
64
69
  ## Usage
65
70
 
66
- ### Run Through The Helper Command
71
+ ### Helper Commands
67
72
 
68
- Ask Claude a question through Codex:
73
+ Ask Claude through Codex:
69
74
 
70
75
  ```bash
71
76
  codex-claude ask "Summarize the current repository."
@@ -78,12 +83,40 @@ codex-claude review
78
83
  codex-claude review "Review the current workspace for bugs and missing tests."
79
84
  ```
80
85
 
86
+ Run a review and compare Claude vs Codex side by side:
87
+
88
+ ```bash
89
+ codex-claude review --compare "Review the current workspace for bugs and missing tests."
90
+ ```
91
+
81
92
  Delegate an implementation task:
82
93
 
83
94
  ```bash
84
95
  codex-claude delegate "Fix the failing tests with the smallest safe patch."
85
96
  ```
86
97
 
98
+ Compare Claude and Codex on the same prompt in one readable report:
99
+
100
+ ```bash
101
+ codex-claude compare "Review this architecture and suggest the safest refactor."
102
+ ```
103
+
104
+ Use separate model choices when comparing:
105
+
106
+ ```bash
107
+ codex-claude compare \
108
+ --codex-model gpt-5.4 \
109
+ --claude-model sonnet \
110
+ "Summarize the tradeoffs in this module."
111
+ ```
112
+
113
+ Write reports to disk:
114
+
115
+ ```bash
116
+ codex-claude compare --out compare.md "Review this change and compare results."
117
+ codex-claude status --json --out status.json
118
+ ```
119
+
87
120
  If you are outside a git repository, add:
88
121
 
89
122
  ```bash
@@ -96,7 +129,16 @@ Example:
96
129
  codex-claude ask --cwd /path/to/project --skip-git-repo-check "Reply with exactly OK."
97
130
  ```
98
131
 
99
- ### Run Through Codex Directly
132
+ You can also pass through Claude-focused options when using `ask`, `review`, or `delegate`:
133
+
134
+ ```bash
135
+ codex-claude review \
136
+ --model sonnet \
137
+ --permission-mode dontAsk \
138
+ --append-system-prompt "Focus on reliability and regression risk."
139
+ ```
140
+
141
+ ### Direct Codex MCP Usage
100
142
 
101
143
  Once installed, Codex can call these tools directly:
102
144
 
@@ -104,7 +146,7 @@ Once installed, Codex can call these tools directly:
104
146
  - `claude_review`: stricter review pass
105
147
  - `claude_delegate`: writable implementation/delegation flow
106
148
 
107
- Example `codex exec` prompt:
149
+ Example:
108
150
 
109
151
  ```bash
110
152
  codex exec --cd /path/to/project --skip-git-repo-check \
@@ -116,10 +158,34 @@ codex exec --cd /path/to/project --skip-git-repo-check \
116
158
  - Codex discovers the `claude-bridge` MCP server
117
159
  - Codex calls one of the bridge tools
118
160
  - The bridge runs the local `claude --print` command
119
- - The Claude response is returned back through MCP to Codex
161
+ - Claude output is returned through MCP back to Codex
120
162
 
121
163
  This is an MCP bridge, not a native Codex slash-command plugin.
122
164
 
165
+ ## Status And Comparison
166
+
167
+ `codex-claude status` checks:
168
+
169
+ - whether `codex` is installed
170
+ - whether `claude` is installed
171
+ - whether Claude auth is available
172
+ - whether the `claude-bridge` MCP server is registered in Codex
173
+
174
+ `codex-claude doctor` adds a simple pass/fail diagnostic summary and suggested fixes.
175
+
176
+ `codex-claude compare` runs both tools on the same prompt:
177
+
178
+ - Claude runs directly through `claude --print`
179
+ - Codex runs directly through `codex exec`
180
+ - the command returns a single report with:
181
+ - Claude output
182
+ - Codex output
183
+ - a line-by-line diff block for quick human review
184
+
185
+ `--json` returns machine-readable report data for `status`, `doctor`, and `compare`.
186
+
187
+ `--out <file>` writes the rendered or JSON report to disk and also prints it to stdout.
188
+
123
189
  ## Development
124
190
 
125
191
  Install dependencies:
@@ -140,6 +206,20 @@ Start the MCP server directly:
140
206
  npm start
141
207
  ```
142
208
 
209
+ ## Publishing
210
+
211
+ This package is published on npm as:
212
+
213
+ ```bash
214
+ @dhrupo/codex-claude-bridge
215
+ ```
216
+
217
+ The repo includes a GitHub Actions publish workflow. To publish a new version:
218
+
219
+ 1. Bump `package.json`
220
+ 2. Commit and push to `main`
221
+ 3. Create and push a matching tag like `v0.1.1`
222
+
143
223
  ## Troubleshooting
144
224
 
145
225
  If the bridge works but Claude returns an auth or access error, test Claude directly first:
package/package.json CHANGED
@@ -1,11 +1,16 @@
1
1
  {
2
2
  "name": "@dhrupo/codex-claude-bridge",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Expose Claude Code as MCP tools for Codex.",
5
5
  "type": "module",
6
+ "files": [
7
+ "bin",
8
+ "src",
9
+ "README.md"
10
+ ],
6
11
  "bin": {
7
- "codex-claude": "./bin/codex-claude.mjs",
8
- "codex-claude-mcp": "./bin/codex-claude-mcp.mjs"
12
+ "codex-claude": "bin/codex-claude.mjs",
13
+ "codex-claude-mcp": "bin/codex-claude-mcp.mjs"
9
14
  },
10
15
  "engines": {
11
16
  "node": ">=18.18.0"
@@ -1,4 +1,4 @@
1
- import { spawn } from "node:child_process";
1
+ import { runCommand } from "./process-utils.mjs";
2
2
 
3
3
  function pushListOption(args, flag, values) {
4
4
  if (!Array.isArray(values) || values.length === 0) {
@@ -50,53 +50,19 @@ export async function runClaude(input, options = {}) {
50
50
  const cwd = input.cwd || options.cwd || process.cwd();
51
51
  const args = buildClaudeArgs(input, options.defaults);
52
52
  const outputFormat = input.outputFormat || options.defaults?.outputFormat || "text";
53
-
54
- return new Promise((resolve, reject) => {
55
- const child = spawn(command, args, {
56
- cwd,
57
- env: {
58
- ...process.env,
59
- ...options.env
60
- },
61
- stdio: ["ignore", "pipe", "pipe"]
62
- });
63
-
64
- let stdout = "";
65
- let stderr = "";
66
-
67
- child.stdout.on("data", (chunk) => {
68
- stdout += String(chunk);
69
- });
70
-
71
- child.stderr.on("data", (chunk) => {
72
- stderr += String(chunk);
73
- });
74
-
75
- child.on("error", (error) => {
76
- reject(
77
- new Error(`Failed to start Claude CLI. Ensure \`claude\` is installed and on PATH. ${error.message}`)
78
- );
79
- });
80
-
81
- child.on("close", (code) => {
82
- const trimmedStdout = stdout.trim();
83
- const trimmedStderr = stderr.trim();
84
-
85
- if (code !== 0) {
86
- reject(
87
- new Error(trimmedStderr || trimmedStdout || `Claude CLI exited with status ${code ?? "unknown"}.`)
88
- );
89
- return;
90
- }
91
-
92
- resolve({
93
- command,
94
- cwd,
95
- args,
96
- outputFormat,
97
- stdout: normalizeStdout(trimmedStdout, outputFormat),
98
- stderr: trimmedStderr
99
- });
100
- });
53
+ const result = await runCommand(command, args, {
54
+ cwd,
55
+ env: options.env
56
+ }).catch((error) => {
57
+ throw new Error(`Failed to start Claude CLI. Ensure \`claude\` is installed and on PATH. ${error.message}`);
101
58
  });
59
+
60
+ return {
61
+ command,
62
+ cwd,
63
+ args,
64
+ outputFormat,
65
+ stdout: normalizeStdout(result.stdout, outputFormat),
66
+ stderr: result.stderr
67
+ };
102
68
  }
@@ -1,8 +1,12 @@
1
1
  import path from "node:path";
2
+ import { writeFile } from "node:fs/promises";
2
3
  import { fileURLToPath } from "node:url";
3
4
 
4
5
  import { DEFAULT_MCP_SERVER_NAME } from "./constants.mjs";
6
+ import { runClaude } from "./claude-cli.mjs";
5
7
  import { buildCodexExecArgs, buildCodexMcpAddArgs, runCodex } from "./codex-cli.mjs";
8
+ import { runCommand } from "./process-utils.mjs";
9
+ import { formatCompareReport, formatDoctorReport, formatStatusReport, toJsonReport } from "./render.mjs";
6
10
 
7
11
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
12
  const serverScript = path.resolve(__dirname, "../bin/codex-claude-mcp.mjs");
@@ -11,9 +15,12 @@ function usage() {
11
15
  return [
12
16
  "Usage:",
13
17
  " codex-claude install [--name <server-name>]",
14
- " codex-claude ask [--cwd <dir>] [--model <model>] [--skip-git-repo-check] <prompt>",
15
- " codex-claude review [--cwd <dir>] [--model <model>] [--skip-git-repo-check] [prompt]",
16
- " codex-claude delegate [--cwd <dir>] [--model <model>] [--skip-git-repo-check] <prompt>"
18
+ " codex-claude status [--name <server-name>] [--json] [--out <file>]",
19
+ " codex-claude doctor [--name <server-name>] [--json] [--out <file>]",
20
+ " codex-claude ask [--cwd <dir>] [--model <model>] [--permission-mode <mode>] [--skip-git-repo-check] <prompt>",
21
+ " codex-claude review [--compare] [--cwd <dir>] [--model <model>] [--permission-mode <mode>] [--skip-git-repo-check] [prompt]",
22
+ " codex-claude delegate [--cwd <dir>] [--model <model>] [--permission-mode <mode>] [--skip-git-repo-check] <prompt>",
23
+ " codex-claude compare [--cwd <dir>] [--codex-model <model>] [--claude-model <model>] [--skip-git-repo-check] [--json] [--out <file>] <prompt>"
17
24
  ].join("\n");
18
25
  }
19
26
 
@@ -27,6 +34,15 @@ function parseCommand(argv) {
27
34
  command,
28
35
  cwd: undefined,
29
36
  model: undefined,
37
+ claudeModel: undefined,
38
+ codexModel: undefined,
39
+ permissionMode: undefined,
40
+ appendSystemPrompt: undefined,
41
+ outputFormat: "text",
42
+ maxBudgetUsd: undefined,
43
+ json: false,
44
+ out: undefined,
45
+ compare: false,
30
46
  skipGitRepoCheck: false,
31
47
  serverName: DEFAULT_MCP_SERVER_NAME,
32
48
  prompt: ""
@@ -44,15 +60,58 @@ function parseCommand(argv) {
44
60
  options.model = rest[index];
45
61
  continue;
46
62
  }
63
+ if (token === "--claude-model") {
64
+ index += 1;
65
+ options.claudeModel = rest[index];
66
+ continue;
67
+ }
68
+ if (token === "--codex-model") {
69
+ index += 1;
70
+ options.codexModel = rest[index];
71
+ continue;
72
+ }
47
73
  if (token === "--name") {
48
74
  index += 1;
49
75
  options.serverName = rest[index];
50
76
  continue;
51
77
  }
78
+ if (token === "--permission-mode") {
79
+ index += 1;
80
+ options.permissionMode = rest[index];
81
+ continue;
82
+ }
83
+ if (token === "--append-system-prompt") {
84
+ index += 1;
85
+ options.appendSystemPrompt = rest[index];
86
+ continue;
87
+ }
88
+ if (token === "--output-format") {
89
+ index += 1;
90
+ options.outputFormat = rest[index];
91
+ continue;
92
+ }
93
+ if (token === "--max-budget-usd") {
94
+ index += 1;
95
+ options.maxBudgetUsd = Number(rest[index]);
96
+ continue;
97
+ }
52
98
  if (token === "--skip-git-repo-check") {
53
99
  options.skipGitRepoCheck = true;
54
100
  continue;
55
101
  }
102
+ if (token === "--json") {
103
+ options.json = true;
104
+ continue;
105
+ }
106
+ if (token === "--compare") {
107
+ options.compare = true;
108
+ continue;
109
+ }
110
+ if (token === "--out") {
111
+ index += 1;
112
+ options.out = rest[index];
113
+ continue;
114
+ }
56
115
  options.prompt = rest.slice(index).join(" ");
57
116
  break;
58
117
  }
@@ -81,9 +140,22 @@ export function buildCodexClaudePrompt(input) {
81
140
  const call = {
82
141
  prompt,
83
142
  cwd: input.cwd || process.cwd(),
84
- outputFormat: "text"
143
+ outputFormat: input.outputFormat || "text"
85
144
  };
86
145
 
146
+ if (input.model) {
147
+ call.model = input.model;
148
+ }
149
+ if (input.permissionMode) {
150
+ call.permissionMode = input.permissionMode;
151
+ }
152
+ if (input.appendSystemPrompt) {
153
+ call.appendSystemPrompt = input.appendSystemPrompt;
154
+ }
155
+ if (typeof input.maxBudgetUsd === "number" && !Number.isNaN(input.maxBudgetUsd)) {
156
+ call.maxBudgetUsd = input.maxBudgetUsd;
157
+ }
158
+
87
159
  return [
88
160
  `Use the MCP tool \`${toolName}\` exactly once.`,
89
161
  "Do not solve this task yourself.",
@@ -93,21 +165,40 @@ export function buildCodexClaudePrompt(input) {
93
165
  ].join("\n");
94
166
  }
95
167
 
96
- async function installBridge(input) {
168
+ async function installBridge(input, options = {}) {
169
+ const cwd = input.cwd || process.cwd();
170
+ const codexBinary = options.codexCommand || "codex";
171
+ const existing = await runCommand(codexBinary, ["mcp", "list"], {
172
+ cwd,
173
+ rejectOnNonZero: false
174
+ });
175
+
176
+ const serverName = input.serverName || DEFAULT_MCP_SERVER_NAME;
177
+ const alreadyInstalled = (existing.stdout || "")
178
+ .split("\n")
179
+ .some((line) => line.trim().startsWith(serverName));
180
+
181
+ if (alreadyInstalled) {
182
+ await runCommand(codexBinary, ["mcp", "remove", serverName], {
183
+ cwd,
184
+ rejectOnNonZero: false
185
+ });
186
+ }
187
+
97
188
  const args = buildCodexMcpAddArgs({
98
- serverName: input.serverName,
189
+ serverName,
99
190
  serverScript
100
191
  });
101
192
 
102
193
  return runCodex(args, {
103
- cwd: input.cwd || process.cwd()
194
+ cwd
104
195
  });
105
196
  }
106
197
 
107
198
  async function runViaCodex(input) {
108
199
  const args = buildCodexExecArgs({
109
200
  cwd: input.cwd,
110
- model: input.model,
201
+ model: input.codexModel || input.model,
111
202
  skipGitRepoCheck: input.skipGitRepoCheck,
112
203
  prompt: buildCodexClaudePrompt(input)
113
204
  });
@@ -117,6 +208,156 @@ async function runViaCodex(input) {
117
208
  });
118
209
  }
119
210
 
211
+ export async function collectStatus(input, options = {}) {
212
+ const cwd = input.cwd || process.cwd();
213
+ const serverName = input.serverName || DEFAULT_MCP_SERVER_NAME;
214
+ const notes = [];
215
+
216
+ const codexVersion = await runCommand(options.codexCommand || "codex", ["--version"], {
217
+ cwd,
218
+ rejectOnNonZero: false
219
+ });
220
+ const claudeVersion = await runCommand(options.claudeCommand || "claude", ["--version"], {
221
+ cwd,
222
+ rejectOnNonZero: false
223
+ });
224
+ const claudeAuth = await runCommand(options.claudeCommand || "claude", ["auth", "status"], {
225
+ cwd,
226
+ rejectOnNonZero: false
227
+ });
228
+ const mcpList = await runCommand(options.codexCommand || "codex", ["mcp", "list"], {
229
+ cwd,
230
+ rejectOnNonZero: false
231
+ });
232
+
233
+ if (codexVersion.code !== 0) {
234
+ notes.push("Codex CLI is not available or not executable.");
235
+ }
236
+ if (claudeVersion.code !== 0) {
237
+ notes.push("Claude CLI is not available or not executable.");
238
+ }
239
+ if (claudeAuth.code !== 0) {
240
+ notes.push("Claude auth check failed. Run `claude auth login`.");
241
+ }
242
+
243
+ const mcpLine = (mcpList.stdout || "")
244
+ .split("\n")
245
+ .find((line) => line.trim().startsWith(serverName));
246
+
247
+ return {
248
+ codex: {
249
+ version: codexVersion.stdout || codexVersion.stderr || "missing"
250
+ },
251
+ claude: {
252
+ version: claudeVersion.stdout || claudeVersion.stderr || "missing",
253
+ auth: claudeAuth.stdout || claudeAuth.stderr || "unknown"
254
+ },
255
+ mcp: {
256
+ serverName,
257
+ installed: Boolean(mcpLine),
258
+ command: mcpLine || ""
259
+ },
260
+ notes
261
+ };
262
+ }
263
+
264
+ export async function collectDoctorReport(input, options = {}) {
265
+ const status = await collectStatus(input, options);
266
+ const checks = [
267
+ {
268
+ name: "codex cli",
269
+ ok: !/missing|not available/i.test(status.codex.version),
270
+ detail: status.codex.version
271
+ },
272
+ {
273
+ name: "claude cli",
274
+ ok: !/missing|not available/i.test(status.claude.version),
275
+ detail: status.claude.version
276
+ },
277
+ {
278
+ name: "claude auth",
279
+ ok: /loggedIn|logged in|subscriptionType/i.test(status.claude.auth),
280
+ detail: status.claude.auth.split("\n")[0]
281
+ },
282
+ {
283
+ name: "mcp server",
284
+ ok: status.mcp.installed,
285
+ detail: status.mcp.serverName
286
+ }
287
+ ];
288
+
289
+ const nextSteps = [];
290
+ if (!checks[0].ok) {
291
+ nextSteps.push("Install or fix the Codex CLI, then rerun `codex-claude doctor`.");
292
+ }
293
+ if (!checks[1].ok) {
294
+ nextSteps.push("Install or fix the Claude CLI, then rerun `codex-claude doctor`.");
295
+ }
296
+ if (!checks[2].ok) {
297
+ nextSteps.push("Run `claude auth login` and verify direct Claude access with `claude -p --output-format text \"Reply with exactly OK.\"`.");
298
+ }
299
+ if (!checks[3].ok) {
300
+ nextSteps.push("Run `codex-claude install` to register the MCP bridge in Codex.");
301
+ }
302
+
303
+ return {
304
+ ok: checks.every((check) => check.ok),
305
+ checks,
306
+ nextSteps,
307
+ status
308
+ };
309
+ }
310
+
311
+ export async function compareCodexAndClaude(input) {
312
+ const cwd = input.cwd || process.cwd();
313
+ const prompt = input.prompt;
314
+
315
+ if (!prompt) {
316
+ throw new Error(usage());
317
+ }
318
+
319
+ const [claudeResult, codexResult] = await Promise.allSettled([
320
+ runClaude({
321
+ prompt,
322
+ cwd,
323
+ outputFormat: "text",
324
+ model: input.claudeModel || input.model,
325
+ permissionMode: input.permissionMode,
326
+ appendSystemPrompt: input.appendSystemPrompt,
327
+ maxBudgetUsd: input.maxBudgetUsd
328
+ }),
329
+ runCodex(
330
+ buildCodexExecArgs({
331
+ cwd,
332
+ model: input.codexModel || input.model,
333
+ skipGitRepoCheck: input.skipGitRepoCheck,
334
+ prompt
335
+ }),
336
+ { cwd }
337
+ )
338
+ ]);
339
+
340
+ return {
341
+ prompt,
342
+ cwd,
343
+ claudeOutput:
344
+ claudeResult.status === "fulfilled"
345
+ ? claudeResult.value.stdout
346
+ : `ERROR: ${claudeResult.reason instanceof Error ? claudeResult.reason.message : String(claudeResult.reason)}`,
347
+ codexOutput:
348
+ codexResult.status === "fulfilled"
349
+ ? codexResult.value.stdout
350
+ : `ERROR: ${codexResult.reason instanceof Error ? codexResult.reason.message : String(codexResult.reason)}`
351
+ };
352
+ }
353
+
354
+ async function emitOutput(output, input) {
355
+ if (input.out) {
356
+ await writeFile(path.resolve(input.cwd || process.cwd(), input.out), output, "utf8");
357
+ }
358
+ console.log(output);
359
+ }
360
+
120
361
  export async function runCodexClaudeCli(argv) {
121
362
  const input = parseCommand(argv);
122
363
 
@@ -133,11 +374,41 @@ export async function runCodexClaudeCli(argv) {
133
374
  return;
134
375
  }
135
376
 
377
+ if (input.command === "status") {
378
+ const report = await collectStatus(input);
379
+ const output = input.json ? toJsonReport(report) : formatStatusReport(report);
380
+ await emitOutput(output, input);
381
+ return;
382
+ }
383
+
384
+ if (input.command === "doctor") {
385
+ const report = await collectDoctorReport(input);
386
+ const output = input.json ? toJsonReport(report) : formatDoctorReport(report);
387
+ await emitOutput(output, input);
388
+ return;
389
+ }
390
+
391
+ if (input.command === "compare") {
392
+ const report = await compareCodexAndClaude(input);
393
+ const output = input.json ? toJsonReport(report) : formatCompareReport(report);
394
+ await emitOutput(output, input);
395
+ return;
396
+ }
397
+
398
+ if (input.command === "review" && input.compare) {
399
+ const report = await compareCodexAndClaude({
400
+ ...input,
401
+ prompt:
402
+ input.prompt || "Review the current repository for bugs, regressions, and missing tests."
403
+ });
404
+ const output = input.json ? toJsonReport(report) : formatCompareReport(report);
405
+ await emitOutput(output, input);
406
+ return;
407
+ }
408
+
136
409
  if (input.command === "ask" || input.command === "review" || input.command === "delegate") {
137
410
  const result = await runViaCodex(input);
138
- if (result.stdout) {
139
- console.log(result.stdout);
140
- }
411
+ await emitOutput(result.stdout || "", input);
141
412
  return;
142
413
  }
143
414
 
package/src/codex-cli.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { spawn } from "node:child_process";
1
+ import { runCommand } from "./process-utils.mjs";
2
2
 
3
3
  export function buildCodexExecArgs(input) {
4
4
  const args = ["exec"];
@@ -33,46 +33,10 @@ export function buildCodexMcpAddArgs(input) {
33
33
 
34
34
  export async function runCodex(args, options = {}) {
35
35
  const command = options.command || "codex";
36
- const cwd = options.cwd || process.cwd();
37
-
38
- return new Promise((resolve, reject) => {
39
- const child = spawn(command, args, {
40
- cwd,
41
- env: {
42
- ...process.env,
43
- ...options.env
44
- },
45
- stdio: ["ignore", "pipe", "pipe"]
46
- });
47
-
48
- let stdout = "";
49
- let stderr = "";
50
-
51
- child.stdout.on("data", (chunk) => {
52
- stdout += String(chunk);
53
- });
54
-
55
- child.stderr.on("data", (chunk) => {
56
- stderr += String(chunk);
57
- });
58
-
59
- child.on("error", (error) => {
60
- reject(new Error(`Failed to start Codex CLI. Ensure \`codex\` is installed and on PATH. ${error.message}`));
61
- });
62
-
63
- child.on("close", (code) => {
64
- if (code !== 0) {
65
- reject(new Error(stderr.trim() || stdout.trim() || `Codex CLI exited with status ${code ?? "unknown"}.`));
66
- return;
67
- }
68
-
69
- resolve({
70
- command,
71
- args,
72
- cwd,
73
- stdout: stdout.trim(),
74
- stderr: stderr.trim()
75
- });
76
- });
36
+ return runCommand(command, args, {
37
+ cwd: options.cwd,
38
+ env: options.env
39
+ }).catch((error) => {
40
+ throw new Error(`Failed to start Codex CLI. Ensure \`codex\` is installed and on PATH. ${error.message}`);
77
41
  });
78
42
  }
@@ -0,0 +1,53 @@
1
+ import { spawn } from "node:child_process";
2
+
3
+ export async function runCommand(command, args, options = {}) {
4
+ if (typeof globalThis.__runCommandForTest === "function") {
5
+ return globalThis.__runCommandForTest(command, args, options);
6
+ }
7
+
8
+ const cwd = options.cwd || process.cwd();
9
+
10
+ return new Promise((resolve, reject) => {
11
+ const child = spawn(command, args, {
12
+ cwd,
13
+ env: {
14
+ ...process.env,
15
+ ...options.env
16
+ },
17
+ stdio: ["ignore", "pipe", "pipe"]
18
+ });
19
+
20
+ let stdout = "";
21
+ let stderr = "";
22
+
23
+ child.stdout.on("data", (chunk) => {
24
+ stdout += String(chunk);
25
+ });
26
+
27
+ child.stderr.on("data", (chunk) => {
28
+ stderr += String(chunk);
29
+ });
30
+
31
+ child.on("error", (error) => {
32
+ reject(new Error(`Failed to start \`${command}\`. ${error.message}`));
33
+ });
34
+
35
+ child.on("close", (code) => {
36
+ const result = {
37
+ command,
38
+ args,
39
+ cwd,
40
+ code: code ?? 1,
41
+ stdout: stdout.trim(),
42
+ stderr: stderr.trim()
43
+ };
44
+
45
+ if (result.code !== 0 && options.rejectOnNonZero !== false) {
46
+ reject(new Error(result.stderr || result.stdout || `\`${command}\` exited with status ${result.code}.`));
47
+ return;
48
+ }
49
+
50
+ resolve(result);
51
+ });
52
+ });
53
+ }
package/src/render.mjs ADDED
@@ -0,0 +1,104 @@
1
+ function normalizeForDiff(text) {
2
+ return String(text || "")
3
+ .replace(/\r\n/g, "\n")
4
+ .replace(/[ \t]+$/gm, "")
5
+ .replace(/\n{3,}/g, "\n\n")
6
+ .trim();
7
+ }
8
+
9
+ function makeSection(title, body) {
10
+ return [`## ${title}`, "", body || "_No output_", ""].join("\n");
11
+ }
12
+
13
+ function diffLines(left, right) {
14
+ const leftLines = normalizeForDiff(left).split("\n");
15
+ const rightLines = normalizeForDiff(right).split("\n");
16
+ const max = Math.max(leftLines.length, rightLines.length);
17
+ const lines = [];
18
+
19
+ for (let index = 0; index < max; index += 1) {
20
+ const leftLine = leftLines[index];
21
+ const rightLine = rightLines[index];
22
+ if (leftLine === rightLine) {
23
+ if (leftLine !== undefined) {
24
+ lines.push(` ${leftLine}`);
25
+ }
26
+ continue;
27
+ }
28
+ if (leftLine !== undefined) {
29
+ lines.push(`- ${leftLine}`);
30
+ }
31
+ if (rightLine !== undefined) {
32
+ lines.push(`+ ${rightLine}`);
33
+ }
34
+ }
35
+
36
+ return lines.join("\n").trim() || "Outputs are identical.";
37
+ }
38
+
39
+ export function formatStatusReport(report) {
40
+ const lines = [
41
+ "# codex-claude status",
42
+ "",
43
+ `- codex cli: ${report.codex.version}`,
44
+ `- claude cli: ${report.claude.version}`,
45
+ `- claude auth: ${report.claude.auth}`,
46
+ `- mcp server (${report.mcp.serverName}): ${report.mcp.installed ? "installed" : "missing"}`
47
+ ];
48
+
49
+ if (report.mcp.installed && report.mcp.command) {
50
+ lines.push(`- mcp command: ${report.mcp.command}`);
51
+ }
52
+
53
+ if (report.notes.length) {
54
+ lines.push("", "Notes:");
55
+ for (const note of report.notes) {
56
+ lines.push(`- ${note}`);
57
+ }
58
+ }
59
+
60
+ return `${lines.join("\n")}\n`;
61
+ }
62
+
63
+ export function toJsonReport(report) {
64
+ return JSON.stringify(report, null, 2);
65
+ }
66
+
67
+ export function formatCompareReport(report) {
68
+ const lines = [
69
+ "# codex-claude compare",
70
+ "",
71
+ `Prompt: ${report.prompt}`,
72
+ `Working directory: \`${report.cwd}\``,
73
+ ""
74
+ ];
75
+
76
+ lines.push(makeSection("Claude", ["```text", report.claudeOutput, "```"].join("\n")));
77
+ lines.push(makeSection("Codex", ["```text", report.codexOutput, "```"].join("\n")));
78
+ lines.push(makeSection("Diff", ["```diff", diffLines(report.claudeOutput, report.codexOutput), "```"].join("\n")));
79
+
80
+ return lines.join("\n").trimEnd();
81
+ }
82
+
83
+ export function formatDoctorReport(report) {
84
+ const lines = [
85
+ "# codex-claude doctor",
86
+ "",
87
+ `- overall: ${report.ok ? "ok" : "needs attention"}`,
88
+ ""
89
+ ];
90
+
91
+ lines.push("Checks:");
92
+ for (const check of report.checks) {
93
+ lines.push(`- ${check.name}: ${check.ok ? "ok" : "failed"}${check.detail ? ` (${check.detail})` : ""}`);
94
+ }
95
+
96
+ if (report.nextSteps.length) {
97
+ lines.push("", "Next steps:");
98
+ for (const step of report.nextSteps) {
99
+ lines.push(`- ${step}`);
100
+ }
101
+ }
102
+
103
+ return `${lines.join("\n")}\n`;
104
+ }
@@ -1,57 +0,0 @@
1
- import test from "node:test";
2
- import assert from "node:assert/strict";
3
-
4
- import { buildClaudeArgs } from "../src/claude-cli.mjs";
5
-
6
- test("buildClaudeArgs creates the expected Claude CLI invocation", () => {
7
- const args = buildClaudeArgs({
8
- prompt: "Review the current diff",
9
- outputFormat: "json",
10
- model: "sonnet",
11
- permissionMode: "dontAsk",
12
- allowedTools: ["Read", "Bash(git:*)"],
13
- disallowedTools: ["Edit"],
14
- appendSystemPrompt: "Be strict.",
15
- maxBudgetUsd: 1.5
16
- });
17
-
18
- assert.deepEqual(args, [
19
- "--print",
20
- "--output-format",
21
- "json",
22
- "--model",
23
- "sonnet",
24
- "--permission-mode",
25
- "dontAsk",
26
- "--max-budget-usd",
27
- "1.5",
28
- "--append-system-prompt",
29
- "Be strict.",
30
- "--allowedTools",
31
- "Read Bash(git:*)",
32
- "--disallowedTools",
33
- "Edit",
34
- "Review the current diff"
35
- ]);
36
- });
37
-
38
- test("buildClaudeArgs falls back to text output when unspecified", () => {
39
- const args = buildClaudeArgs(
40
- {
41
- prompt: "Summarize the repo"
42
- },
43
- {
44
- outputFormat: "text",
45
- permissionMode: "default"
46
- }
47
- );
48
-
49
- assert.deepEqual(args, [
50
- "--print",
51
- "--output-format",
52
- "text",
53
- "--permission-mode",
54
- "default",
55
- "Summarize the repo"
56
- ]);
57
- });
@@ -1,52 +0,0 @@
1
- import test from "node:test";
2
- import assert from "node:assert/strict";
3
-
4
- import { buildCodexExecArgs, buildCodexMcpAddArgs } from "../src/codex-cli.mjs";
5
- import { buildCodexClaudePrompt } from "../src/codex-claude-command.mjs";
6
-
7
- test("buildCodexExecArgs creates the expected codex exec invocation", () => {
8
- const args = buildCodexExecArgs({
9
- cwd: "/repo",
10
- model: "gpt-5.4",
11
- skipGitRepoCheck: true,
12
- prompt: "Use the tool."
13
- });
14
-
15
- assert.deepEqual(args, [
16
- "exec",
17
- "--cd",
18
- "/repo",
19
- "--model",
20
- "gpt-5.4",
21
- "--skip-git-repo-check",
22
- "Use the tool."
23
- ]);
24
- });
25
-
26
- test("buildCodexMcpAddArgs registers the local bridge server", () => {
27
- const args = buildCodexMcpAddArgs({
28
- serverName: "claude-bridge",
29
- serverScript: "/tmp/codex-claude-mcp.mjs"
30
- });
31
-
32
- assert.deepEqual(args, [
33
- "mcp",
34
- "add",
35
- "claude-bridge",
36
- "--",
37
- "node",
38
- "/tmp/codex-claude-mcp.mjs"
39
- ]);
40
- });
41
-
42
- test("buildCodexClaudePrompt targets the review tool with a default review request", () => {
43
- const prompt = buildCodexClaudePrompt({
44
- command: "review",
45
- cwd: "/repo"
46
- });
47
-
48
- assert.match(prompt, /claude_review/);
49
- assert.match(prompt, /Return only the tool result/);
50
- assert.match(prompt, /Review the current repository for bugs, regressions, and missing tests/);
51
- assert.match(prompt, /"cwd":"\/repo"/);
52
- });
@@ -1,12 +0,0 @@
1
- import test from "node:test";
2
- import assert from "node:assert/strict";
3
-
4
- import { createServer } from "../src/server.mjs";
5
-
6
- test("createServer registers the expected Claude bridge tools", async () => {
7
- const server = await createServer();
8
- assert.deepEqual(
9
- Object.keys(server._registeredTools).sort(),
10
- ["claude_ask", "claude_delegate", "claude_review"]
11
- );
12
- });
@@ -1,19 +0,0 @@
1
- import test from "node:test";
2
- import assert from "node:assert/strict";
3
-
4
- import { handleClaudeAsk } from "../src/tools.mjs";
5
-
6
- test("handleClaudeAsk renders a Codex-friendly text block", async () => {
7
- const originalPath = process.env.PATH;
8
- process.env.PATH = "/tmp";
9
-
10
- const response = await handleClaudeAsk({
11
- prompt: "Summarize",
12
- outputFormat: "text"
13
- }).catch((error) => error);
14
-
15
- process.env.PATH = originalPath;
16
-
17
- assert.equal(response instanceof Error, true);
18
- assert.match(response.message, /Failed to start Claude CLI|Ensure `claude` is installed/);
19
- });