@dhrupo/codex-claude-bridge 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,160 @@
1
+ # codex-claude-bridge
2
+
3
+ Use Claude Code from inside Codex.
4
+
5
+ This package exposes Claude Code as MCP tools that Codex can call, and also ships a small `codex-claude` command for common flows.
6
+
7
+ ## What You Get
8
+
9
+ - `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
11
+ - Three MCP tools:
12
+ - `claude_ask`
13
+ - `claude_review`
14
+ - `claude_delegate`
15
+
16
+ ## Requirements
17
+
18
+ - Node.js `>=18.18.0`
19
+ - `codex` installed and authenticated
20
+ - `claude` installed and authenticated
21
+
22
+ Check both CLIs first:
23
+
24
+ ```bash
25
+ codex --version
26
+ claude --version
27
+ claude auth status
28
+ ```
29
+
30
+ ## Install
31
+
32
+ Install the package:
33
+
34
+ ```bash
35
+ npm install -g @dhrupo/codex-claude-bridge
36
+ ```
37
+
38
+ Register the MCP server in Codex:
39
+
40
+ ```bash
41
+ codex-claude install
42
+ ```
43
+
44
+ That adds a global Codex MCP server named `claude-bridge`.
45
+
46
+ You can also register it manually:
47
+
48
+ ```bash
49
+ codex mcp add claude-bridge -- codex-claude-mcp
50
+ ```
51
+
52
+ Or without a global npm install:
53
+
54
+ ```bash
55
+ codex mcp add claude-bridge -- npx -y @dhrupo/codex-claude-bridge
56
+ ```
57
+
58
+ Confirm registration:
59
+
60
+ ```bash
61
+ codex mcp list
62
+ ```
63
+
64
+ ## Usage
65
+
66
+ ### Run Through The Helper Command
67
+
68
+ Ask Claude a question through Codex:
69
+
70
+ ```bash
71
+ codex-claude ask "Summarize the current repository."
72
+ ```
73
+
74
+ Run a review:
75
+
76
+ ```bash
77
+ codex-claude review
78
+ codex-claude review "Review the current workspace for bugs and missing tests."
79
+ ```
80
+
81
+ Delegate an implementation task:
82
+
83
+ ```bash
84
+ codex-claude delegate "Fix the failing tests with the smallest safe patch."
85
+ ```
86
+
87
+ If you are outside a git repository, add:
88
+
89
+ ```bash
90
+ --skip-git-repo-check
91
+ ```
92
+
93
+ Example:
94
+
95
+ ```bash
96
+ codex-claude ask --cwd /path/to/project --skip-git-repo-check "Reply with exactly OK."
97
+ ```
98
+
99
+ ### Run Through Codex Directly
100
+
101
+ Once installed, Codex can call these tools directly:
102
+
103
+ - `claude_ask`: read-only prompting
104
+ - `claude_review`: stricter review pass
105
+ - `claude_delegate`: writable implementation/delegation flow
106
+
107
+ Example `codex exec` prompt:
108
+
109
+ ```bash
110
+ codex exec --cd /path/to/project --skip-git-repo-check \
111
+ 'Use the MCP tool `claude_ask` exactly once. Return only the tool result. Tool arguments: {"prompt":"Summarize this repo.","cwd":"/path/to/project","outputFormat":"text"}'
112
+ ```
113
+
114
+ ## How It Works
115
+
116
+ - Codex discovers the `claude-bridge` MCP server
117
+ - Codex calls one of the bridge tools
118
+ - The bridge runs the local `claude --print` command
119
+ - The Claude response is returned back through MCP to Codex
120
+
121
+ This is an MCP bridge, not a native Codex slash-command plugin.
122
+
123
+ ## Development
124
+
125
+ Install dependencies:
126
+
127
+ ```bash
128
+ npm install
129
+ ```
130
+
131
+ Run tests:
132
+
133
+ ```bash
134
+ npm test
135
+ ```
136
+
137
+ Start the MCP server directly:
138
+
139
+ ```bash
140
+ npm start
141
+ ```
142
+
143
+ ## Troubleshooting
144
+
145
+ If the bridge works but Claude returns an auth or access error, test Claude directly first:
146
+
147
+ ```bash
148
+ claude -p --output-format text "Reply with exactly OK."
149
+ ```
150
+
151
+ If that fails, fix Claude access before testing through Codex.
152
+
153
+ If Codex is outside a git repo, use `--skip-git-repo-check`.
154
+
155
+ If needed, remove and re-add the MCP server:
156
+
157
+ ```bash
158
+ codex mcp remove claude-bridge
159
+ codex mcp add claude-bridge -- codex-claude-mcp
160
+ ```
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { startServer } from "../src/server.mjs";
4
+
5
+ startServer().catch((error) => {
6
+ console.error(error instanceof Error ? error.stack || error.message : String(error));
7
+ process.exitCode = 1;
8
+ });
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { runCodexClaudeCli } from "../src/codex-claude-command.mjs";
4
+
5
+ runCodexClaudeCli(process.argv.slice(2)).catch((error) => {
6
+ console.error(error instanceof Error ? error.message : String(error));
7
+ process.exitCode = 1;
8
+ });
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@dhrupo/codex-claude-bridge",
3
+ "version": "0.1.0",
4
+ "description": "Expose Claude Code as MCP tools for Codex.",
5
+ "type": "module",
6
+ "bin": {
7
+ "codex-claude": "./bin/codex-claude.mjs",
8
+ "codex-claude-mcp": "./bin/codex-claude-mcp.mjs"
9
+ },
10
+ "engines": {
11
+ "node": ">=18.18.0"
12
+ },
13
+ "scripts": {
14
+ "test": "node --test tests/*.test.mjs",
15
+ "start": "node ./bin/codex-claude-mcp.mjs",
16
+ "check": "node --test tests/*.test.mjs"
17
+ },
18
+ "keywords": [
19
+ "codex",
20
+ "claude",
21
+ "mcp",
22
+ "anthropic",
23
+ "openai"
24
+ ],
25
+ "license": "Apache-2.0",
26
+ "dependencies": {
27
+ "@modelcontextprotocol/sdk": "^1.29.0",
28
+ "zod": "^4.1.12"
29
+ }
30
+ }
@@ -0,0 +1,102 @@
1
+ import { spawn } from "node:child_process";
2
+
3
+ function pushListOption(args, flag, values) {
4
+ if (!Array.isArray(values) || values.length === 0) {
5
+ return;
6
+ }
7
+ args.push(flag, values.join(" "));
8
+ }
9
+
10
+ export function buildClaudeArgs(input, defaults = {}) {
11
+ const args = ["--print"];
12
+ const outputFormat = input.outputFormat || defaults.outputFormat || "text";
13
+ const permissionMode = input.permissionMode || defaults.permissionMode;
14
+
15
+ args.push("--output-format", outputFormat);
16
+
17
+ if (input.model) {
18
+ args.push("--model", input.model);
19
+ }
20
+ if (permissionMode) {
21
+ args.push("--permission-mode", permissionMode);
22
+ }
23
+ if (typeof input.maxBudgetUsd === "number") {
24
+ args.push("--max-budget-usd", String(input.maxBudgetUsd));
25
+ }
26
+ if (input.appendSystemPrompt) {
27
+ args.push("--append-system-prompt", input.appendSystemPrompt);
28
+ }
29
+
30
+ pushListOption(args, "--allowedTools", input.allowedTools);
31
+ pushListOption(args, "--disallowedTools", input.disallowedTools);
32
+
33
+ args.push(input.prompt);
34
+ return args;
35
+ }
36
+
37
+ function normalizeStdout(stdout, outputFormat) {
38
+ if (outputFormat === "json") {
39
+ try {
40
+ return JSON.stringify(JSON.parse(stdout), null, 2);
41
+ } catch {
42
+ return stdout.trim();
43
+ }
44
+ }
45
+ return stdout.trim();
46
+ }
47
+
48
+ export async function runClaude(input, options = {}) {
49
+ const command = options.command || "claude";
50
+ const cwd = input.cwd || options.cwd || process.cwd();
51
+ const args = buildClaudeArgs(input, options.defaults);
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
+ });
101
+ });
102
+ }
@@ -0,0 +1,145 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+
4
+ import { DEFAULT_MCP_SERVER_NAME } from "./constants.mjs";
5
+ import { buildCodexExecArgs, buildCodexMcpAddArgs, runCodex } from "./codex-cli.mjs";
6
+
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+ const serverScript = path.resolve(__dirname, "../bin/codex-claude-mcp.mjs");
9
+
10
+ function usage() {
11
+ return [
12
+ "Usage:",
13
+ " 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>"
17
+ ].join("\n");
18
+ }
19
+
20
+ function parseCommand(argv) {
21
+ if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
22
+ return { command: "help" };
23
+ }
24
+
25
+ const [command, ...rest] = argv;
26
+ const options = {
27
+ command,
28
+ cwd: undefined,
29
+ model: undefined,
30
+ skipGitRepoCheck: false,
31
+ serverName: DEFAULT_MCP_SERVER_NAME,
32
+ prompt: ""
33
+ };
34
+
35
+ for (let index = 0; index < rest.length; index += 1) {
36
+ const token = rest[index];
37
+ if (token === "--cwd") {
38
+ index += 1;
39
+ options.cwd = rest[index];
40
+ continue;
41
+ }
42
+ if (token === "--model") {
43
+ index += 1;
44
+ options.model = rest[index];
45
+ continue;
46
+ }
47
+ if (token === "--name") {
48
+ index += 1;
49
+ options.serverName = rest[index];
50
+ continue;
51
+ }
52
+ if (token === "--skip-git-repo-check") {
53
+ options.skipGitRepoCheck = true;
54
+ continue;
55
+ }
56
+ options.prompt = rest.slice(index).join(" ");
57
+ break;
58
+ }
59
+
60
+ return options;
61
+ }
62
+
63
+ export function buildCodexClaudePrompt(input) {
64
+ const toolNameByCommand = {
65
+ ask: "claude_ask",
66
+ review: "claude_review",
67
+ delegate: "claude_delegate"
68
+ };
69
+
70
+ const toolName = toolNameByCommand[input.command];
71
+ const prompt =
72
+ input.prompt ||
73
+ (input.command === "review"
74
+ ? "Review the current repository for bugs, regressions, and missing tests."
75
+ : "");
76
+
77
+ if (!toolName || !prompt) {
78
+ throw new Error(usage());
79
+ }
80
+
81
+ const call = {
82
+ prompt,
83
+ cwd: input.cwd || process.cwd(),
84
+ outputFormat: "text"
85
+ };
86
+
87
+ return [
88
+ `Use the MCP tool \`${toolName}\` exactly once.`,
89
+ "Do not solve this task yourself.",
90
+ "Return only the tool result with no extra framing.",
91
+ "",
92
+ `Tool arguments: ${JSON.stringify(call)}`
93
+ ].join("\n");
94
+ }
95
+
96
+ async function installBridge(input) {
97
+ const args = buildCodexMcpAddArgs({
98
+ serverName: input.serverName,
99
+ serverScript
100
+ });
101
+
102
+ return runCodex(args, {
103
+ cwd: input.cwd || process.cwd()
104
+ });
105
+ }
106
+
107
+ async function runViaCodex(input) {
108
+ const args = buildCodexExecArgs({
109
+ cwd: input.cwd,
110
+ model: input.model,
111
+ skipGitRepoCheck: input.skipGitRepoCheck,
112
+ prompt: buildCodexClaudePrompt(input)
113
+ });
114
+
115
+ return runCodex(args, {
116
+ cwd: input.cwd || process.cwd()
117
+ });
118
+ }
119
+
120
+ export async function runCodexClaudeCli(argv) {
121
+ const input = parseCommand(argv);
122
+
123
+ if (input.command === "help") {
124
+ console.log(usage());
125
+ return;
126
+ }
127
+
128
+ if (input.command === "install") {
129
+ const result = await installBridge(input);
130
+ if (result.stdout) {
131
+ console.log(result.stdout);
132
+ }
133
+ return;
134
+ }
135
+
136
+ if (input.command === "ask" || input.command === "review" || input.command === "delegate") {
137
+ const result = await runViaCodex(input);
138
+ if (result.stdout) {
139
+ console.log(result.stdout);
140
+ }
141
+ return;
142
+ }
143
+
144
+ throw new Error(usage());
145
+ }
@@ -0,0 +1,78 @@
1
+ import { spawn } from "node:child_process";
2
+
3
+ export function buildCodexExecArgs(input) {
4
+ const args = ["exec"];
5
+
6
+ if (input.cwd) {
7
+ args.push("--cd", input.cwd);
8
+ }
9
+ if (input.model) {
10
+ args.push("--model", input.model);
11
+ }
12
+ if (input.sandbox) {
13
+ args.push("--sandbox", input.sandbox);
14
+ }
15
+ if (input.skipGitRepoCheck) {
16
+ args.push("--skip-git-repo-check");
17
+ }
18
+
19
+ args.push(input.prompt);
20
+ return args;
21
+ }
22
+
23
+ export function buildCodexMcpAddArgs(input) {
24
+ return [
25
+ "mcp",
26
+ "add",
27
+ input.serverName,
28
+ "--",
29
+ "node",
30
+ input.serverScript
31
+ ];
32
+ }
33
+
34
+ export async function runCodex(args, options = {}) {
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
+ });
77
+ });
78
+ }
@@ -0,0 +1,3 @@
1
+ export const PACKAGE_NAME = "codex-claude-bridge";
2
+ export const PACKAGE_VERSION = "0.1.0";
3
+ export const DEFAULT_MCP_SERVER_NAME = "claude-bridge";
package/src/schema.mjs ADDED
@@ -0,0 +1,87 @@
1
+ export const CLAUDE_COMMON_INPUT = {
2
+ cwd: {
3
+ type: "string",
4
+ description: "Working directory for the Claude Code run. Defaults to the current process directory."
5
+ },
6
+ model: {
7
+ type: "string",
8
+ description: "Optional Claude model or alias, for example `sonnet` or `claude-sonnet-4-6`."
9
+ },
10
+ permissionMode: {
11
+ type: "string",
12
+ enum: ["default", "acceptEdits", "auto", "dontAsk", "bypassPermissions", "plan"],
13
+ description: "Claude Code permission mode."
14
+ },
15
+ allowedTools: {
16
+ type: "array",
17
+ items: { type: "string" },
18
+ description: "Optional allow-list passed to `--allowedTools`."
19
+ },
20
+ disallowedTools: {
21
+ type: "array",
22
+ items: { type: "string" },
23
+ description: "Optional deny-list passed to `--disallowedTools`."
24
+ },
25
+ appendSystemPrompt: {
26
+ type: "string",
27
+ description: "Extra system guidance appended to Claude's default system prompt."
28
+ },
29
+ maxBudgetUsd: {
30
+ type: "number",
31
+ description: "Optional API budget cap for the run."
32
+ }
33
+ };
34
+
35
+ export const ASK_TOOL_SCHEMA = {
36
+ type: "object",
37
+ additionalProperties: false,
38
+ required: ["prompt"],
39
+ properties: {
40
+ prompt: {
41
+ type: "string",
42
+ description: "Task or question for Claude Code."
43
+ },
44
+ outputFormat: {
45
+ type: "string",
46
+ enum: ["text", "json"],
47
+ description: "Preferred result format. Defaults to `text`."
48
+ },
49
+ ...CLAUDE_COMMON_INPUT
50
+ }
51
+ };
52
+
53
+ export const REVIEW_TOOL_SCHEMA = {
54
+ type: "object",
55
+ additionalProperties: false,
56
+ required: ["prompt"],
57
+ properties: {
58
+ prompt: {
59
+ type: "string",
60
+ description: "Review instructions. Example: `Review the current diff for bugs and regressions.`"
61
+ },
62
+ outputFormat: {
63
+ type: "string",
64
+ enum: ["text", "json"],
65
+ description: "Preferred result format. Defaults to `text`."
66
+ },
67
+ ...CLAUDE_COMMON_INPUT
68
+ }
69
+ };
70
+
71
+ export const DELEGATE_TOOL_SCHEMA = {
72
+ type: "object",
73
+ additionalProperties: false,
74
+ required: ["prompt"],
75
+ properties: {
76
+ prompt: {
77
+ type: "string",
78
+ description: "Implementation task for Claude Code."
79
+ },
80
+ outputFormat: {
81
+ type: "string",
82
+ enum: ["text", "json"],
83
+ description: "Preferred result format. Defaults to `text`."
84
+ },
85
+ ...CLAUDE_COMMON_INPUT
86
+ }
87
+ };
package/src/server.mjs ADDED
@@ -0,0 +1,97 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { z } from "zod";
4
+
5
+ import { PACKAGE_NAME, PACKAGE_VERSION } from "./constants.mjs";
6
+ import { ASK_TOOL_SCHEMA, DELEGATE_TOOL_SCHEMA, REVIEW_TOOL_SCHEMA } from "./schema.mjs";
7
+ import { handleClaudeAsk, handleClaudeDelegate, handleClaudeReview } from "./tools.mjs";
8
+
9
+ function jsonSchemaToZod(shape) {
10
+ const entries = Object.entries(shape.properties);
11
+ const built = {};
12
+
13
+ for (const [key, definition] of entries) {
14
+ let node;
15
+ if (definition.enum) {
16
+ node = z.enum(definition.enum);
17
+ } else if (definition.type === "string") {
18
+ node = z.string();
19
+ } else if (definition.type === "number") {
20
+ node = z.number();
21
+ } else if (definition.type === "array" && definition.items?.type === "string") {
22
+ node = z.array(z.string());
23
+ } else {
24
+ node = z.any();
25
+ }
26
+
27
+ if (!shape.required?.includes(key)) {
28
+ node = node.optional();
29
+ }
30
+ built[key] = node;
31
+ }
32
+
33
+ return z.object(built);
34
+ }
35
+
36
+ export async function createServer() {
37
+ const server = new McpServer({
38
+ name: PACKAGE_NAME,
39
+ version: PACKAGE_VERSION
40
+ });
41
+
42
+ server.registerTool(
43
+ "claude_ask",
44
+ {
45
+ title: "Claude Ask",
46
+ description: "Send a read-only prompt to Claude Code and return the result.",
47
+ inputSchema: jsonSchemaToZod(ASK_TOOL_SCHEMA),
48
+ annotations: {
49
+ readOnlyHint: true,
50
+ destructiveHint: false,
51
+ idempotentHint: true,
52
+ openWorldHint: false
53
+ }
54
+ },
55
+ handleClaudeAsk
56
+ );
57
+
58
+ server.registerTool(
59
+ "claude_review",
60
+ {
61
+ title: "Claude Review",
62
+ description: "Run Claude Code as a reviewer against the current workspace.",
63
+ inputSchema: jsonSchemaToZod(REVIEW_TOOL_SCHEMA),
64
+ annotations: {
65
+ readOnlyHint: true,
66
+ destructiveHint: false,
67
+ idempotentHint: true,
68
+ openWorldHint: false
69
+ }
70
+ },
71
+ handleClaudeReview
72
+ );
73
+
74
+ server.registerTool(
75
+ "claude_delegate",
76
+ {
77
+ title: "Claude Delegate",
78
+ description: "Delegate an implementation task to Claude Code, allowing file edits by default.",
79
+ inputSchema: jsonSchemaToZod(DELEGATE_TOOL_SCHEMA),
80
+ annotations: {
81
+ readOnlyHint: false,
82
+ destructiveHint: false,
83
+ idempotentHint: false,
84
+ openWorldHint: false
85
+ }
86
+ },
87
+ handleClaudeDelegate
88
+ );
89
+
90
+ return server;
91
+ }
92
+
93
+ export async function startServer() {
94
+ const server = await createServer();
95
+ const transport = new StdioServerTransport();
96
+ await server.connect(transport);
97
+ }
package/src/tools.mjs ADDED
@@ -0,0 +1,79 @@
1
+ import { runClaude } from "./claude-cli.mjs";
2
+
3
+ function formatResult(title, result) {
4
+ const lines = [
5
+ `# ${title}`,
6
+ "",
7
+ `Command: \`claude ${result.args.map((part) => (/\s/.test(part) ? JSON.stringify(part) : part)).join(" ")}\``,
8
+ `Working directory: \`${result.cwd}\``,
9
+ ""
10
+ ];
11
+
12
+ if (result.stdout) {
13
+ lines.push("Output:", result.outputFormat === "json" ? "```json" : "```text", result.stdout, "```");
14
+ } else {
15
+ lines.push("Output:", "```text", "", "```");
16
+ }
17
+
18
+ if (result.stderr) {
19
+ lines.push("", "Stderr:", "```text", result.stderr, "```");
20
+ }
21
+
22
+ return lines.join("\n");
23
+ }
24
+
25
+ function toTextContent(text) {
26
+ return {
27
+ content: [
28
+ {
29
+ type: "text",
30
+ text
31
+ }
32
+ ]
33
+ };
34
+ }
35
+
36
+ export async function handleClaudeAsk(input) {
37
+ const result = await runClaude(input, {
38
+ defaults: {
39
+ outputFormat: input.outputFormat || "text",
40
+ permissionMode: input.permissionMode || "default"
41
+ }
42
+ });
43
+
44
+ return toTextContent(formatResult("Claude Ask", result));
45
+ }
46
+
47
+ export async function handleClaudeReview(input) {
48
+ const reviewInput = {
49
+ ...input,
50
+ appendSystemPrompt:
51
+ input.appendSystemPrompt ||
52
+ "Act as a strict code reviewer. Prioritize bugs, regressions, security issues, and missing tests."
53
+ };
54
+
55
+ const result = await runClaude(reviewInput, {
56
+ defaults: {
57
+ outputFormat: input.outputFormat || "text",
58
+ permissionMode: input.permissionMode || "default"
59
+ }
60
+ });
61
+
62
+ return toTextContent(formatResult("Claude Review", result));
63
+ }
64
+
65
+ export async function handleClaudeDelegate(input) {
66
+ const delegateInput = {
67
+ ...input,
68
+ permissionMode: input.permissionMode || "acceptEdits"
69
+ };
70
+
71
+ const result = await runClaude(delegateInput, {
72
+ defaults: {
73
+ outputFormat: input.outputFormat || "text",
74
+ permissionMode: delegateInput.permissionMode
75
+ }
76
+ });
77
+
78
+ return toTextContent(formatResult("Claude Delegate", result));
79
+ }
@@ -0,0 +1,57 @@
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
+ });
@@ -0,0 +1,52 @@
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
+ });
@@ -0,0 +1,12 @@
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
+ });
@@ -0,0 +1,19 @@
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
+ });