codex-bridge-mcp 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +98 -2
- package/package.json +37 -4
- package/server.mjs +608 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 WeiqiZhou2002
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,99 @@
|
|
|
1
|
-
#
|
|
1
|
+
# codex-bridge-mcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A two-way MCP bridge that lets **Claude Code** drive the **Codex CLI**, and lets Codex talk back.
|
|
4
|
+
|
|
5
|
+
- Claude writes a plan → Codex **executes** it, or **adversarially reviews** it against the real code.
|
|
6
|
+
- While working, Codex can **ask Claude questions** (and block until answered) or **send progress / findings**.
|
|
7
|
+
- Claude can answer, send corrections mid-run, or start follow-up turns in the same Codex session.
|
|
8
|
+
- Keep several named Codex sessions per project, e.g. a `reviewer` on high effort and an `executor` on a cheaper setting.
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
Claude Code ──MCP "codex"──▶ server.mjs ──spawns──▶ codex exec --json
|
|
12
|
+
▲ │ │
|
|
13
|
+
│ ~/.codex-bridge/jobs/<id>/ │ MCP "claude_bridge"
|
|
14
|
+
└──── codex_wait ◀──────── mailbox ◀───────────────┘ (injected per job)
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Requirements
|
|
18
|
+
|
|
19
|
+
- Node.js 18+
|
|
20
|
+
- [Codex CLI](https://github.com/openai/codex), logged in (`codex login`). Tested with codex-cli 0.162.
|
|
21
|
+
- Claude Code
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
claude mcp add codex -s user -- npx -y codex-bridge-mcp
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Or from source:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
git clone https://github.com/WeiqiZhou2002/codex-bridge-mcp.git
|
|
33
|
+
cd codex-bridge-mcp && npm install
|
|
34
|
+
claude mcp add codex -s user -- node "$PWD/server.mjs"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Then start a new Claude Code session. `claude mcp list` should show `codex ✔ Connected`.
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
Just ask Claude in plain language:
|
|
42
|
+
|
|
43
|
+
> Start a Codex reviewer on xhigh and have it adversarially review `plans/plan-v3.md`.
|
|
44
|
+
|
|
45
|
+
> Start a Codex executor on high to implement the plan; it should ask me if anything is unclear.
|
|
46
|
+
|
|
47
|
+
> Tell the reviewer that finding #2 is wrong because the cache is per-request, and ask it to re-check.
|
|
48
|
+
|
|
49
|
+
### Tools Claude gets (server `codex`)
|
|
50
|
+
|
|
51
|
+
| Tool | What it does |
|
|
52
|
+
|---|---|
|
|
53
|
+
| `codex_start` | Start a job in the background. `mode`: `execute` (implement the plan, `workspace-write`), `review` (adversarial review, `read-only`), `freeform`. Takes `plan` text and/or `plan_file`, optional `name`, `model`, `effort` (`minimal`…`xhigh`), `sandbox`, `instructions`. |
|
|
54
|
+
| `codex_wait` | Block until Codex asks a question, sends a message, or finishes/fails its turn. Returns a digest of the commands it ran and files it changed. |
|
|
55
|
+
| `codex_send` | With `question_id`: answer a blocking question. While Codex is running: queue a message it receives on its next bridge call. After the turn ended: start a new turn in the same Codex session. |
|
|
56
|
+
| `codex_cancel` | Stop a running job. |
|
|
57
|
+
| `codex_jobs` | List jobs with status, name, session and cwd. |
|
|
58
|
+
|
|
59
|
+
Anywhere a `job_id` is expected you can pass the job's `name` instead; the newest job with that name wins.
|
|
60
|
+
A job's model, effort and sandbox are fixed at start and reused by every follow-up turn.
|
|
61
|
+
|
|
62
|
+
### Tools Codex gets (server `claude_bridge`, injected via `-c` per job)
|
|
63
|
+
|
|
64
|
+
| Tool | What it does |
|
|
65
|
+
|---|---|
|
|
66
|
+
| `ask_claude(question)` | Blocks until Claude answers (default timeout 30 min; on timeout Codex proceeds and states its assumption). |
|
|
67
|
+
| `message_claude(message, kind)` | Non-blocking note: `progress`, `finding`, `deviation`, `blocker`. |
|
|
68
|
+
| `check_inbox()` | Pick up corrections Claude queued. Queued messages are also appended to any bridge tool result. |
|
|
69
|
+
|
|
70
|
+
Your normal `~/.codex/config.toml` (default model, other MCP servers, etc.) still applies.
|
|
71
|
+
|
|
72
|
+
## How it works
|
|
73
|
+
|
|
74
|
+
Each job gets a directory under `~/.codex-bridge/jobs/<id>/` (override with `CODEX_BRIDGE_HOME`):
|
|
75
|
+
|
|
76
|
+
- `events.jsonl`: Codex → Claude. Questions, messages, a digest parsed from `codex exec --json`, turn results.
|
|
77
|
+
- `answers/<qid>.json`: Claude → Codex answers to `ask_claude`.
|
|
78
|
+
- `inbox.jsonl`: Claude → Codex unsolicited messages.
|
|
79
|
+
|
|
80
|
+
Follow-up turns use `codex exec resume <session_id>`. Job metadata persists on disk, so a later Claude session can keep talking to the same Codex session. Turns that were running when the previous Claude session exited are marked resumable.
|
|
81
|
+
|
|
82
|
+
## Caveats
|
|
83
|
+
|
|
84
|
+
- `execute` mode lets Codex write inside the working directory. `danger-full-access` is only used if you ask for it explicitly.
|
|
85
|
+
- In Codex's `workspace-write` sandbox, Codex usually cannot write to `.git`, so let Claude (or you) do the commits.
|
|
86
|
+
- Codex's `--json` event format is not a stable API. If a Codex upgrade changes it, the activity digest may get thinner, but final answers still come through.
|
|
87
|
+
- Two jobs in the same folder do not share context. Claude relays between them.
|
|
88
|
+
|
|
89
|
+
## Development
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
npm test
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The tests drive the server through a real MCP client against a fake `codex` binary (`test/fake-codex.mjs`), so they are fast and don't spend tokens. Set `CODEX_BIN` to point the bridge at a different Codex executable.
|
|
96
|
+
|
|
97
|
+
## License
|
|
98
|
+
|
|
99
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codex-bridge-mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"scripts": {
|
|
5
|
+
"check": "node --check server.mjs",
|
|
6
|
+
"test": "node --test test/*.test.mjs"
|
|
7
|
+
},
|
|
8
|
+
"keywords": [
|
|
9
|
+
"mcp",
|
|
10
|
+
"claude-code",
|
|
11
|
+
"codex",
|
|
12
|
+
"model-context-protocol",
|
|
13
|
+
"multi-agent",
|
|
14
|
+
"code-review"
|
|
15
|
+
],
|
|
16
|
+
"author": "WeiqiZhou2002",
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"description": "Two-way MCP bridge that lets Claude Code drive the Codex CLI (execute plans, adversarial plan review) while Codex can ask Claude questions back.",
|
|
19
|
+
"type": "module",
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
22
|
+
"zod": "^3.25.76"
|
|
23
|
+
},
|
|
24
|
+
"bin": {
|
|
25
|
+
"codex-bridge-mcp": "server.mjs"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"server.mjs",
|
|
29
|
+
"README.md",
|
|
30
|
+
"LICENSE"
|
|
31
|
+
],
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=18"
|
|
34
|
+
},
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/WeiqiZhou2002/codex-bridge-mcp.git"
|
|
38
|
+
}
|
|
39
|
+
}
|
package/server.mjs
ADDED
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// codex-bridge: a two-way MCP bridge between Claude Code and the Codex CLI.
|
|
3
|
+
//
|
|
4
|
+
// Claude side (default): node server.mjs
|
|
5
|
+
// codex_start / codex_wait / codex_send / codex_cancel / codex_jobs
|
|
6
|
+
// Codex side (injected into each `codex exec` via -c mcp_servers.claude_bridge...):
|
|
7
|
+
// node server.mjs --role codex --job <id>
|
|
8
|
+
// ask_claude / message_claude / check_inbox
|
|
9
|
+
//
|
|
10
|
+
// The two sides talk through a per-job mailbox on disk: ~/.codex-bridge/jobs/<id>/
|
|
11
|
+
// events.jsonl codex -> claude (questions, messages, progress digest, turn results)
|
|
12
|
+
// answers/<qid> claude -> codex (answers to blocking ask_claude calls)
|
|
13
|
+
// inbox.jsonl claude -> codex (unsolicited messages, delivered on Codex's next bridge call)
|
|
14
|
+
|
|
15
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
16
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
17
|
+
import { z } from "zod";
|
|
18
|
+
import { spawn } from "node:child_process";
|
|
19
|
+
import { randomBytes } from "node:crypto";
|
|
20
|
+
import fs from "node:fs";
|
|
21
|
+
import os from "node:os";
|
|
22
|
+
import path from "node:path";
|
|
23
|
+
import { fileURLToPath } from "node:url";
|
|
24
|
+
|
|
25
|
+
const SELF = fileURLToPath(import.meta.url);
|
|
26
|
+
const argv = process.argv.slice(2);
|
|
27
|
+
const argOf = (flag) => {
|
|
28
|
+
const i = argv.indexOf(flag);
|
|
29
|
+
return i >= 0 ? argv[i + 1] : undefined;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Codex does not forward arbitrary env vars to MCP servers, so the Codex side gets --home explicitly.
|
|
33
|
+
const ROOT = argOf("--home") || process.env.CODEX_BRIDGE_HOME || path.join(os.homedir(), ".codex-bridge");
|
|
34
|
+
const JOBS = path.join(ROOT, "jobs");
|
|
35
|
+
const CODEX_BIN = process.env.CODEX_BIN || "codex";
|
|
36
|
+
const VERSION = readJson(path.join(path.dirname(SELF), "package.json"), {}).version || "0.0.0";
|
|
37
|
+
|
|
38
|
+
// ---------- mailbox helpers ----------
|
|
39
|
+
|
|
40
|
+
const newId = (n = 4) => randomBytes(n).toString("hex");
|
|
41
|
+
const jobDir = (id) => path.join(JOBS, id);
|
|
42
|
+
const now = () => new Date().toISOString();
|
|
43
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
44
|
+
|
|
45
|
+
function readJson(file, fallback) {
|
|
46
|
+
try {
|
|
47
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
48
|
+
} catch {
|
|
49
|
+
return fallback;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function writeJson(file, obj) {
|
|
54
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
55
|
+
fs.writeFileSync(tmp, JSON.stringify(obj, null, 2));
|
|
56
|
+
fs.renameSync(tmp, file);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function appendLine(file, obj) {
|
|
60
|
+
fs.appendFileSync(file, JSON.stringify({ ts: now(), ...obj }) + "\n");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function readLines(file) {
|
|
64
|
+
let raw;
|
|
65
|
+
try {
|
|
66
|
+
raw = fs.readFileSync(file, "utf8");
|
|
67
|
+
} catch {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
const out = [];
|
|
71
|
+
for (const line of raw.split("\n")) {
|
|
72
|
+
if (!line.trim()) continue;
|
|
73
|
+
try {
|
|
74
|
+
out.push(JSON.parse(line));
|
|
75
|
+
} catch {
|
|
76
|
+
// a line that is mid-write; it will be picked up on the next read
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const metaPath = (id) => path.join(jobDir(id), "meta.json");
|
|
84
|
+
const eventsPath = (id) => path.join(jobDir(id), "events.jsonl");
|
|
85
|
+
const inboxPath = (id) => path.join(jobDir(id), "inbox.jsonl");
|
|
86
|
+
const answerPath = (id, qid) => path.join(jobDir(id), "answers", `${qid}.json`);
|
|
87
|
+
const inboxCursorPath = (id) => path.join(jobDir(id), "inbox.cursor");
|
|
88
|
+
|
|
89
|
+
// Claude -> Codex messages Codex has not picked up yet; marks them delivered.
|
|
90
|
+
function takeUndelivered(id) {
|
|
91
|
+
const all = readLines(inboxPath(id));
|
|
92
|
+
const fresh = all.slice(readJson(inboxCursorPath(id), 0));
|
|
93
|
+
if (fresh.length) writeJson(inboxCursorPath(id), all.length);
|
|
94
|
+
return fresh;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function loadMeta(id) {
|
|
98
|
+
const meta = readJson(metaPath(id), null);
|
|
99
|
+
if (!meta) throw new Error(`Unknown job_id: ${id}`);
|
|
100
|
+
return meta;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function allMetas() {
|
|
104
|
+
if (!fs.existsSync(JOBS)) return [];
|
|
105
|
+
return fs
|
|
106
|
+
.readdirSync(JOBS)
|
|
107
|
+
.map((d) => readJson(metaPath(d), null))
|
|
108
|
+
.filter(Boolean)
|
|
109
|
+
.sort((a, b) => String(b.created).localeCompare(String(a.created)));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Accepts a job id or a job name; a name resolves to the newest job carrying it.
|
|
113
|
+
function resolveJob(ref) {
|
|
114
|
+
if (fs.existsSync(metaPath(ref))) return ref;
|
|
115
|
+
const hit = allMetas().find((m) => m.name === ref);
|
|
116
|
+
if (!hit) throw new Error(`No job with id or name "${ref}". Use codex_jobs to list them.`);
|
|
117
|
+
return hit.id;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function pendingQuestions(id) {
|
|
121
|
+
return readLines(eventsPath(id)).filter(
|
|
122
|
+
(e) => e.type === "question" && !fs.existsSync(answerPath(id, e.qid)),
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const text = (s) => ({ content: [{ type: "text", text: s }] });
|
|
127
|
+
|
|
128
|
+
// =====================================================================
|
|
129
|
+
// Codex side
|
|
130
|
+
// =====================================================================
|
|
131
|
+
|
|
132
|
+
async function runCodexSide(jobId) {
|
|
133
|
+
if (!jobId || !fs.existsSync(jobDir(jobId))) {
|
|
134
|
+
console.error(`codex-bridge: missing or unknown --job ${jobId}`);
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
// Any messages Claude queued for Codex ride along on every bridge tool result.
|
|
138
|
+
function drainInbox() {
|
|
139
|
+
const fresh = takeUndelivered(jobId);
|
|
140
|
+
if (!fresh.length) return "";
|
|
141
|
+
return (
|
|
142
|
+
"\n\n--- New message(s) from Claude ---\n" +
|
|
143
|
+
fresh.map((m) => `[${m.ts}] ${m.text}`).join("\n\n")
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const server = new McpServer({ name: "claude_bridge", version: VERSION });
|
|
148
|
+
|
|
149
|
+
server.registerTool(
|
|
150
|
+
"ask_claude",
|
|
151
|
+
{
|
|
152
|
+
description:
|
|
153
|
+
"Ask Claude (the lead agent who wrote your plan) a question and BLOCK until Claude answers. " +
|
|
154
|
+
"Use this for ambiguities, decisions the plan does not cover, or when you believe the plan is wrong " +
|
|
155
|
+
"and want sign-off before deviating. Include enough context that Claude can answer without re-reading everything.",
|
|
156
|
+
inputSchema: {
|
|
157
|
+
question: z.string().describe("The question, with the relevant context and the options you see."),
|
|
158
|
+
timeout_sec: z
|
|
159
|
+
.number()
|
|
160
|
+
.int()
|
|
161
|
+
.min(10)
|
|
162
|
+
.max(3600)
|
|
163
|
+
.optional()
|
|
164
|
+
.describe("How long to wait for an answer (default 1800)."),
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
async ({ question, timeout_sec = 1800 }) => {
|
|
168
|
+
const qid = newId(3);
|
|
169
|
+
appendLine(eventsPath(jobId), { type: "question", qid, text: question });
|
|
170
|
+
const deadline = Date.now() + timeout_sec * 1000;
|
|
171
|
+
while (Date.now() < deadline) {
|
|
172
|
+
const ans = readJson(answerPath(jobId, qid), null);
|
|
173
|
+
if (ans) return text(`Claude answered:\n${ans.text}${drainInbox()}`);
|
|
174
|
+
await sleep(1000);
|
|
175
|
+
}
|
|
176
|
+
appendLine(eventsPath(jobId), { type: "question_timeout", qid });
|
|
177
|
+
return text(
|
|
178
|
+
`Claude did not answer within ${timeout_sec}s. Proceed with your best judgment, ` +
|
|
179
|
+
`state the assumption you made in your final summary.${drainInbox()}`,
|
|
180
|
+
);
|
|
181
|
+
},
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
server.registerTool(
|
|
185
|
+
"message_claude",
|
|
186
|
+
{
|
|
187
|
+
description:
|
|
188
|
+
"Send Claude a non-blocking message: a milestone, an important finding, a deviation from the plan, or a blocker. " +
|
|
189
|
+
"Keep it short. Returns immediately (plus any messages Claude has queued for you).",
|
|
190
|
+
inputSchema: {
|
|
191
|
+
message: z.string(),
|
|
192
|
+
kind: z.enum(["progress", "finding", "deviation", "blocker"]).optional(),
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
async ({ message, kind = "progress" }) => {
|
|
196
|
+
appendLine(eventsPath(jobId), { type: "message", kind, text: message });
|
|
197
|
+
return text(`Delivered to Claude.${drainInbox()}`);
|
|
198
|
+
},
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
server.registerTool(
|
|
202
|
+
"check_inbox",
|
|
203
|
+
{
|
|
204
|
+
description:
|
|
205
|
+
"Check whether Claude has sent you new instructions or corrections. Call this between major steps.",
|
|
206
|
+
inputSchema: {},
|
|
207
|
+
},
|
|
208
|
+
async () => text(drainInbox().trim() || "No new messages from Claude."),
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
await server.connect(new StdioServerTransport());
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// =====================================================================
|
|
215
|
+
// Claude side
|
|
216
|
+
// =====================================================================
|
|
217
|
+
|
|
218
|
+
const PREAMBLE = `You are working together with Claude, another AI coding agent who is the lead on this task.
|
|
219
|
+
You have an MCP server named "claude_bridge" with these tools:
|
|
220
|
+
- ask_claude(question): blocks until Claude answers. Use it instead of guessing when something is ambiguous,
|
|
221
|
+
when the plan does not cover a decision, or before deviating from the plan in a significant way.
|
|
222
|
+
- message_claude(message, kind): non-blocking note to Claude (milestones, important findings, blockers).
|
|
223
|
+
- check_inbox(): see if Claude has sent you corrections. Call it between major steps.
|
|
224
|
+
Do not spam Claude: a handful of messages per task is plenty.`;
|
|
225
|
+
|
|
226
|
+
const MODE_PROMPTS = {
|
|
227
|
+
execute: `${PREAMBLE}
|
|
228
|
+
|
|
229
|
+
ROLE: Executor. Claude wrote the plan below; your job is to implement it faithfully in the working directory.
|
|
230
|
+
- Read the relevant code before changing it. Follow the plan's steps in order unless a dependency forces otherwise.
|
|
231
|
+
- If a step is wrong or impossible as written, ask_claude before deviating (or, for small things, deviate and message_claude kind="deviation").
|
|
232
|
+
- Run whatever verification the plan asks for (tests, build, lint) plus anything obviously needed.
|
|
233
|
+
- Final answer: (1) what you changed, file by file; (2) verification you ran and the results, verbatim for failures;
|
|
234
|
+
(3) every deviation from the plan and why; (4) anything left undone or risky.`,
|
|
235
|
+
|
|
236
|
+
review: `${PREAMBLE}
|
|
237
|
+
|
|
238
|
+
ROLE: Adversarial reviewer. Claude wrote the plan below. Your job is to find what is wrong with it, not to praise it.
|
|
239
|
+
Do NOT modify any files. Verify claims against the actual code and environment instead of trusting the plan.
|
|
240
|
+
Look hard for: false assumptions about the code, missing steps, wrong ordering or hidden dependencies, edge cases
|
|
241
|
+
and failure modes, data loss / migration / security / concurrency risks, untestable or unverified steps,
|
|
242
|
+
scope creep, and simpler alternatives that would achieve the same goal.
|
|
243
|
+
If something about intent is genuinely unclear, ask_claude; otherwise do not ask.
|
|
244
|
+
Final answer, in this format:
|
|
245
|
+
VERDICT: approve | approve-with-changes | reject
|
|
246
|
+
FINDINGS (most severe first), each with: severity (blocker/major/minor), what is wrong, evidence (file:line or command output), concrete fix.
|
|
247
|
+
If the plan is genuinely sound, say so plainly and list only the top residual risks; do not invent problems.`,
|
|
248
|
+
|
|
249
|
+
freeform: PREAMBLE,
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const DEFAULT_SANDBOX = { execute: "workspace-write", review: "read-only", freeform: "workspace-write" };
|
|
253
|
+
|
|
254
|
+
const running = new Map(); // jobId -> ChildProcess
|
|
255
|
+
|
|
256
|
+
// Number of events already shown to Claude. Kept on disk only, so several Claude sessions sharing a job agree.
|
|
257
|
+
const getCursor = (id) => readJson(path.join(jobDir(id), "claude.cursor"), 0);
|
|
258
|
+
const setCursor = (id, n) => writeJson(path.join(jobDir(id), "claude.cursor"), n);
|
|
259
|
+
|
|
260
|
+
// Hands Claude everything it has not seen yet and marks it seen.
|
|
261
|
+
function takeUnread(id) {
|
|
262
|
+
const all = readLines(eventsPath(id));
|
|
263
|
+
const fresh = all.slice(getCursor(id));
|
|
264
|
+
setCursor(id, all.length);
|
|
265
|
+
return fresh;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function bridgeConfigArgs(jobId) {
|
|
269
|
+
const prefix = "mcp_servers.claude_bridge";
|
|
270
|
+
return [
|
|
271
|
+
"-c", `${prefix}.command=${JSON.stringify(process.execPath)}`,
|
|
272
|
+
"-c", `${prefix}.args=${JSON.stringify([SELF, "--role", "codex", "--job", jobId, "--home", ROOT])}`,
|
|
273
|
+
"-c", `${prefix}.startup_timeout_sec=30`,
|
|
274
|
+
"-c", `${prefix}.tool_timeout_sec=3700`,
|
|
275
|
+
"-c", `${prefix}.default_tools_approval_mode="approve"`,
|
|
276
|
+
];
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const IGNORED_ERROR = /ignoring \d+ unrecognized configuration setting/i;
|
|
280
|
+
|
|
281
|
+
function runTurn(jobId, prompt) {
|
|
282
|
+
const meta = loadMeta(jobId);
|
|
283
|
+
const common = [
|
|
284
|
+
"--json",
|
|
285
|
+
"--skip-git-repo-check",
|
|
286
|
+
"-c", `sandbox_mode=${JSON.stringify(meta.sandbox)}`,
|
|
287
|
+
...bridgeConfigArgs(jobId),
|
|
288
|
+
...(meta.model ? ["-m", meta.model] : []),
|
|
289
|
+
...(meta.effort ? ["-c", `model_reasoning_effort=${JSON.stringify(meta.effort)}`] : []),
|
|
290
|
+
];
|
|
291
|
+
const args = meta.session_id
|
|
292
|
+
? ["exec", "resume", ...common, meta.session_id, "-"]
|
|
293
|
+
: ["exec", ...common, "-C", meta.cwd, "-"];
|
|
294
|
+
|
|
295
|
+
meta.status = "running";
|
|
296
|
+
meta.turns = (meta.turns || 0) + 1;
|
|
297
|
+
meta.updated = now();
|
|
298
|
+
writeJson(metaPath(jobId), meta);
|
|
299
|
+
|
|
300
|
+
const ev = eventsPath(jobId);
|
|
301
|
+
const turn = meta.turns;
|
|
302
|
+
appendLine(ev, { type: "turn_started", turn });
|
|
303
|
+
|
|
304
|
+
const child = spawn(CODEX_BIN, args, { cwd: meta.cwd, stdio: ["pipe", "pipe", "pipe"], env: process.env });
|
|
305
|
+
running.set(jobId, child);
|
|
306
|
+
child.stdin.end(prompt);
|
|
307
|
+
|
|
308
|
+
let buf = "";
|
|
309
|
+
let lastMessage = null;
|
|
310
|
+
let stderrTail = "";
|
|
311
|
+
let failed = null;
|
|
312
|
+
|
|
313
|
+
child.stdout.on("data", (chunk) => {
|
|
314
|
+
buf += chunk;
|
|
315
|
+
let nl;
|
|
316
|
+
while ((nl = buf.indexOf("\n")) >= 0) {
|
|
317
|
+
const line = buf.slice(0, nl).trim();
|
|
318
|
+
buf = buf.slice(nl + 1);
|
|
319
|
+
if (!line) continue;
|
|
320
|
+
let e;
|
|
321
|
+
try {
|
|
322
|
+
e = JSON.parse(line);
|
|
323
|
+
} catch {
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
if (e.type === "thread.started" && e.thread_id) {
|
|
327
|
+
const m = loadMeta(jobId);
|
|
328
|
+
if (!m.session_id) {
|
|
329
|
+
m.session_id = e.thread_id;
|
|
330
|
+
writeJson(metaPath(jobId), m);
|
|
331
|
+
}
|
|
332
|
+
} else if (e.type === "item.completed" && e.item) {
|
|
333
|
+
const it = e.item;
|
|
334
|
+
if (it.type === "agent_message") {
|
|
335
|
+
lastMessage = it.text;
|
|
336
|
+
appendLine(ev, { type: "progress", text: `codex: ${truncate(it.text, 400)}` });
|
|
337
|
+
} else if (it.type === "command_execution") {
|
|
338
|
+
appendLine(ev, { type: "progress", text: `ran (exit ${it.exit_code ?? "?"}): ${truncate(it.command, 200)}` });
|
|
339
|
+
} else if (it.type === "file_change") {
|
|
340
|
+
const files = (it.changes || []).map((c) => `${c.kind || "edit"} ${c.path}`).join(", ");
|
|
341
|
+
appendLine(ev, { type: "progress", text: `files: ${files}` });
|
|
342
|
+
} else if (it.type === "mcp_tool_call" && it.server !== "claude_bridge") {
|
|
343
|
+
appendLine(ev, { type: "progress", text: `mcp: ${it.server}.${it.tool}` });
|
|
344
|
+
} else if (it.type === "error" && !IGNORED_ERROR.test(it.message || "")) {
|
|
345
|
+
appendLine(ev, { type: "progress", text: `error: ${truncate(it.message, 400)}` });
|
|
346
|
+
}
|
|
347
|
+
} else if (e.type === "turn.failed" || e.type === "error") {
|
|
348
|
+
failed = e.error?.message || e.message || JSON.stringify(e);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
child.stderr.on("data", (chunk) => {
|
|
353
|
+
stderrTail = (stderrTail + chunk).slice(-2000);
|
|
354
|
+
});
|
|
355
|
+
child.on("close", (code, signal) => {
|
|
356
|
+
running.delete(jobId);
|
|
357
|
+
const m = loadMeta(jobId);
|
|
358
|
+
// Messages Claude queued that Codex never picked up before the turn ended.
|
|
359
|
+
const undelivered = takeUndelivered(jobId);
|
|
360
|
+
if (m.status === "cancelled") {
|
|
361
|
+
appendLine(ev, { type: "turn_cancelled", turn });
|
|
362
|
+
} else if (code === 0 && !failed) {
|
|
363
|
+
m.status = "idle";
|
|
364
|
+
appendLine(ev, { type: "turn_done", turn, text: lastMessage ?? "(Codex produced no final message)" });
|
|
365
|
+
} else {
|
|
366
|
+
m.status = "failed";
|
|
367
|
+
appendLine(ev, {
|
|
368
|
+
type: "turn_failed",
|
|
369
|
+
turn,
|
|
370
|
+
text: `exit ${code}${signal ? ` (${signal})` : ""}: ${failed || stderrTail.trim() || "unknown error"}`,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
m.updated = now();
|
|
374
|
+
writeJson(metaPath(jobId), m);
|
|
375
|
+
|
|
376
|
+
if (!undelivered.length) return;
|
|
377
|
+
const texts = undelivered.map((u) => u.text);
|
|
378
|
+
if (m.status === "idle") {
|
|
379
|
+
appendLine(ev, { type: "auto_resume", turn: turn + 1, count: texts.length });
|
|
380
|
+
runTurn(
|
|
381
|
+
jobId,
|
|
382
|
+
`Claude sent you the following after you had already finished your last answer. Act on it:\n\n${texts.join("\n\n---\n\n")}`,
|
|
383
|
+
);
|
|
384
|
+
} else {
|
|
385
|
+
appendLine(ev, { type: "undelivered", texts });
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function truncate(s, n) {
|
|
391
|
+
s = String(s ?? "");
|
|
392
|
+
return s.length > n ? `${s.slice(0, n)}…` : s;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const WAKE = new Set(["question", "message", "turn_done", "turn_failed", "turn_cancelled", "question_timeout", "undelivered"]);
|
|
396
|
+
|
|
397
|
+
function formatEvents(events) {
|
|
398
|
+
const lines = [];
|
|
399
|
+
// The final agent message is reported in full by turn_done; drop its truncated progress copy.
|
|
400
|
+
const finals = new Set(events.filter((e) => e.type === "turn_done").map((e) => `codex: ${truncate(e.text, 400)}`));
|
|
401
|
+
const progress = events.filter((e) => e.type === "progress" && !finals.has(e.text));
|
|
402
|
+
if (progress.length) {
|
|
403
|
+
const shown = progress.slice(-25);
|
|
404
|
+
lines.push(`## Codex activity (${progress.length} events${progress.length > shown.length ? ", last 25 shown" : ""})`);
|
|
405
|
+
for (const p of shown) lines.push(`- ${p.text}`);
|
|
406
|
+
}
|
|
407
|
+
for (const e of events) {
|
|
408
|
+
if (e.type === "message") lines.push(`## Message from Codex [${e.kind}]\n${e.text}`);
|
|
409
|
+
else if (e.type === "question")
|
|
410
|
+
lines.push(`## QUESTION from Codex (question_id=${e.qid}) — Codex is blocked until you answer with codex_send\n${e.text}`);
|
|
411
|
+
else if (e.type === "question_timeout") lines.push(`## Question ${e.qid} timed out; Codex proceeded on its own judgment.`);
|
|
412
|
+
else if (e.type === "turn_done") lines.push(`## Codex finished turn ${e.turn ?? "?"}. Final answer:\n${e.text}`);
|
|
413
|
+
else if (e.type === "turn_failed") lines.push(`## Codex turn ${e.turn ?? "?"} FAILED\n${e.text}`);
|
|
414
|
+
else if (e.type === "turn_cancelled") lines.push(`## Turn ${e.turn ?? "?"} cancelled.`);
|
|
415
|
+
else if (e.type === "auto_resume")
|
|
416
|
+
lines.push(`## Codex ended the turn before reading your ${e.count} queued message(s); started turn ${e.turn} to deliver them. Call codex_wait for its answer.`);
|
|
417
|
+
else if (e.type === "undelivered")
|
|
418
|
+
lines.push(`## These queued messages never reached Codex (the turn did not finish cleanly); resend them if still relevant:\n${e.texts.map((t) => `- ${t}`).join("\n")}`);
|
|
419
|
+
}
|
|
420
|
+
return lines.join("\n\n");
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
async function waitForEvents(jobId, timeoutSec) {
|
|
424
|
+
const deadline = Date.now() + timeoutSec * 1000;
|
|
425
|
+
for (;;) {
|
|
426
|
+
const fresh = readLines(eventsPath(jobId)).slice(getCursor(jobId));
|
|
427
|
+
if (fresh.some((e) => WAKE.has(e.type)) || Date.now() >= deadline) return takeUnread(jobId);
|
|
428
|
+
await sleep(1000);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function statusLine(jobId) {
|
|
433
|
+
const m = loadMeta(jobId);
|
|
434
|
+
const pq = pendingQuestions(jobId);
|
|
435
|
+
return (
|
|
436
|
+
`job_id=${jobId}${m.name ? ` name=${m.name}` : ""} mode=${m.mode} status=${m.status} turns=${m.turns} sandbox=${m.sandbox}` +
|
|
437
|
+
(m.session_id ? ` codex_session=${m.session_id}` : "") +
|
|
438
|
+
(pq.length ? `\nPENDING QUESTIONS: ${pq.map((q) => q.qid).join(", ")}` : "")
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
async function runClaudeSide() {
|
|
443
|
+
fs.mkdirSync(JOBS, { recursive: true });
|
|
444
|
+
// Turns started by a previous Claude session died with it; their Codex sessions can still be resumed.
|
|
445
|
+
for (const m of allMetas()) {
|
|
446
|
+
if (m.status === "running" || m.status === "starting") {
|
|
447
|
+
m.status = m.session_id ? "idle" : "failed";
|
|
448
|
+
writeJson(metaPath(m.id), m);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
const server = new McpServer({ name: "codex", version: VERSION });
|
|
452
|
+
|
|
453
|
+
server.registerTool(
|
|
454
|
+
"codex_start",
|
|
455
|
+
{
|
|
456
|
+
description:
|
|
457
|
+
"Start a Codex CLI agent on a task in the background and return a job_id. " +
|
|
458
|
+
"mode=execute: Codex implements your plan (workspace-write). mode=review: Codex adversarially reviews your plan " +
|
|
459
|
+
"against the real code without editing (read-only). mode=freeform: your prompt as-is. " +
|
|
460
|
+
"Codex can ask you questions and send you messages while it works; follow up with codex_wait.",
|
|
461
|
+
inputSchema: {
|
|
462
|
+
mode: z.enum(["execute", "review", "freeform"]),
|
|
463
|
+
name: z
|
|
464
|
+
.string()
|
|
465
|
+
.optional()
|
|
466
|
+
.describe('Optional handle such as "reviewer" or "executor"; usable wherever job_id is expected. Reusing a name points it at the new job.'),
|
|
467
|
+
cwd: z.string().describe("Absolute path of the project directory Codex works in."),
|
|
468
|
+
plan: z.string().optional().describe("The plan text (or the task for freeform)."),
|
|
469
|
+
plan_file: z.string().optional().describe("Path to a plan file Codex should read instead of / in addition to `plan`."),
|
|
470
|
+
instructions: z.string().optional().describe("Extra instructions: focus areas, constraints, what not to touch."),
|
|
471
|
+
sandbox: z.enum(["read-only", "workspace-write", "danger-full-access"]).optional(),
|
|
472
|
+
model: z.string().optional().describe("Codex model override; default is the user's Codex config."),
|
|
473
|
+
effort: z.enum(["minimal", "low", "medium", "high", "xhigh"]).optional().describe("Codex reasoning effort override."),
|
|
474
|
+
wait_sec: z.number().int().min(0).max(1800).optional().describe("Block this long for the first events (default 0)."),
|
|
475
|
+
},
|
|
476
|
+
},
|
|
477
|
+
async ({ mode, name, cwd, plan, plan_file, instructions, sandbox, model, effort, wait_sec = 0 }) => {
|
|
478
|
+
if (!plan && !plan_file) throw new Error("Provide `plan` and/or `plan_file`.");
|
|
479
|
+
if (!fs.existsSync(cwd)) throw new Error(`cwd does not exist: ${cwd}`);
|
|
480
|
+
const id = newId();
|
|
481
|
+
fs.mkdirSync(path.join(jobDir(id), "answers"), { recursive: true });
|
|
482
|
+
writeJson(metaPath(id), {
|
|
483
|
+
id,
|
|
484
|
+
name,
|
|
485
|
+
mode,
|
|
486
|
+
cwd: path.resolve(cwd),
|
|
487
|
+
sandbox: sandbox || DEFAULT_SANDBOX[mode],
|
|
488
|
+
model,
|
|
489
|
+
effort,
|
|
490
|
+
status: "starting",
|
|
491
|
+
turns: 0,
|
|
492
|
+
created: now(),
|
|
493
|
+
});
|
|
494
|
+
const parts = [MODE_PROMPTS[mode]];
|
|
495
|
+
if (plan_file) parts.push(`The plan is in this file; read it fully first: ${path.resolve(cwd, plan_file)}`);
|
|
496
|
+
if (plan) parts.push(`=== PLAN (from Claude) ===\n${plan}\n=== END PLAN ===`);
|
|
497
|
+
if (instructions) parts.push(`Additional instructions from Claude:\n${instructions}`);
|
|
498
|
+
runTurn(id, parts.join("\n\n"));
|
|
499
|
+
let body = `Started.\n${statusLine(id)}\nUse codex_wait to receive Codex's questions, messages and final answer.`;
|
|
500
|
+
if (wait_sec > 0) {
|
|
501
|
+
const events = await waitForEvents(id, wait_sec);
|
|
502
|
+
body = `${statusLine(id)}\n\n${formatEvents(events) || "(no events yet)"}`;
|
|
503
|
+
}
|
|
504
|
+
return text(body);
|
|
505
|
+
},
|
|
506
|
+
);
|
|
507
|
+
|
|
508
|
+
server.registerTool(
|
|
509
|
+
"codex_wait",
|
|
510
|
+
{
|
|
511
|
+
description:
|
|
512
|
+
"Wait for news from a Codex job. Returns as soon as Codex asks a question, sends a message, or finishes/fails its turn " +
|
|
513
|
+
"(or when timeout_sec elapses), with a digest of what Codex did in the meantime.",
|
|
514
|
+
inputSchema: {
|
|
515
|
+
job_id: z.string().describe("Job id or job name."),
|
|
516
|
+
timeout_sec: z.number().int().min(1).max(1800).optional().describe("Default 300."),
|
|
517
|
+
},
|
|
518
|
+
},
|
|
519
|
+
async ({ job_id, timeout_sec = 300 }) => {
|
|
520
|
+
job_id = resolveJob(job_id);
|
|
521
|
+
const events = await waitForEvents(job_id, timeout_sec);
|
|
522
|
+
const body = formatEvents(events) || `(nothing new in ${timeout_sec}s; Codex is still working — call codex_wait again)`;
|
|
523
|
+
return text(`${statusLine(job_id)}\n\n${body}`);
|
|
524
|
+
},
|
|
525
|
+
);
|
|
526
|
+
|
|
527
|
+
server.registerTool(
|
|
528
|
+
"codex_send",
|
|
529
|
+
{
|
|
530
|
+
description:
|
|
531
|
+
"Send something to Codex. With question_id: answers Codex's blocking question. " +
|
|
532
|
+
"Without it, while Codex is running: queues a message delivered on Codex's next bridge call (corrections, extra instructions). " +
|
|
533
|
+
"Without it, after the turn ended: starts a new turn in the same Codex session (follow-ups, rebuttals to review findings, 'now fix X').",
|
|
534
|
+
inputSchema: {
|
|
535
|
+
job_id: z.string().describe("Job id or job name."),
|
|
536
|
+
message: z.string(),
|
|
537
|
+
question_id: z.string().optional(),
|
|
538
|
+
},
|
|
539
|
+
},
|
|
540
|
+
async ({ job_id, message, question_id }) => {
|
|
541
|
+
job_id = resolveJob(job_id);
|
|
542
|
+
const meta = loadMeta(job_id);
|
|
543
|
+
if (question_id) {
|
|
544
|
+
if (fs.existsSync(answerPath(job_id, question_id))) throw new Error(`Question ${question_id} was already answered.`);
|
|
545
|
+
writeJson(answerPath(job_id, question_id), { ts: now(), text: message });
|
|
546
|
+
return text(`Answer delivered; Codex will continue.\n${statusLine(job_id)}`);
|
|
547
|
+
}
|
|
548
|
+
if (running.has(job_id)) {
|
|
549
|
+
appendLine(inboxPath(job_id), { text: message });
|
|
550
|
+
return text(`Queued; Codex receives it on its next claude_bridge tool call.\n${statusLine(job_id)}`);
|
|
551
|
+
}
|
|
552
|
+
if (!meta.session_id) throw new Error("This job has no Codex session to resume.");
|
|
553
|
+
// Flush the previous turn's unread output now, so codex_wait only reports the new turn.
|
|
554
|
+
const unread = formatEvents(takeUnread(job_id));
|
|
555
|
+
runTurn(job_id, message);
|
|
556
|
+
return text(
|
|
557
|
+
`New turn started in the same Codex session.\n${statusLine(job_id)}` +
|
|
558
|
+
(unread ? `\n\n# Unread output from the previous turn (already finished)\n\n${unread}` : ""),
|
|
559
|
+
);
|
|
560
|
+
},
|
|
561
|
+
);
|
|
562
|
+
|
|
563
|
+
server.registerTool(
|
|
564
|
+
"codex_cancel",
|
|
565
|
+
{
|
|
566
|
+
description: "Stop a running Codex job.",
|
|
567
|
+
inputSchema: { job_id: z.string().describe("Job id or job name.") },
|
|
568
|
+
},
|
|
569
|
+
async ({ job_id }) => {
|
|
570
|
+
job_id = resolveJob(job_id);
|
|
571
|
+
const meta = loadMeta(job_id);
|
|
572
|
+
const child = running.get(job_id);
|
|
573
|
+
if (!child) return text(`Not running.\n${statusLine(job_id)}`);
|
|
574
|
+
meta.status = "cancelled";
|
|
575
|
+
writeJson(metaPath(job_id), meta);
|
|
576
|
+
child.kill("SIGTERM");
|
|
577
|
+
setTimeout(() => running.has(job_id) && child.kill("SIGKILL"), 5000).unref();
|
|
578
|
+
return text(`Cancelling.\n${statusLine(job_id)}`);
|
|
579
|
+
},
|
|
580
|
+
);
|
|
581
|
+
|
|
582
|
+
server.registerTool(
|
|
583
|
+
"codex_jobs",
|
|
584
|
+
{
|
|
585
|
+
description: "List Codex jobs (most recent first).",
|
|
586
|
+
inputSchema: { limit: z.number().int().min(1).max(100).optional() },
|
|
587
|
+
},
|
|
588
|
+
async ({ limit = 10 }) => {
|
|
589
|
+
const ids = allMetas()
|
|
590
|
+
.slice(0, limit)
|
|
591
|
+
.map((m) => `${m.created} ${statusLine(m.id).split("\n")[0]} cwd=${m.cwd}`);
|
|
592
|
+
return text(ids.join("\n") || "No jobs yet.");
|
|
593
|
+
},
|
|
594
|
+
);
|
|
595
|
+
|
|
596
|
+
const shutdown = () => {
|
|
597
|
+
for (const child of running.values()) child.kill("SIGTERM");
|
|
598
|
+
process.exit(0);
|
|
599
|
+
};
|
|
600
|
+
process.on("SIGINT", shutdown);
|
|
601
|
+
process.on("SIGTERM", shutdown);
|
|
602
|
+
process.stdin.on("close", shutdown);
|
|
603
|
+
|
|
604
|
+
await server.connect(new StdioServerTransport());
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
if (argOf("--role") === "codex") await runCodexSide(argOf("--job"));
|
|
608
|
+
else await runClaudeSide();
|