claude2codex 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 +62 -0
- package/README.zh-CN.md +62 -0
- package/dist/server.js +638 -0
- package/package.json +41 -0
package/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# c2c-codex — Claude↔Codex Contract Bridge (MCP server)
|
|
2
|
+
|
|
3
|
+
*[中文](./README.zh-CN.md)*
|
|
4
|
+
|
|
5
|
+
Claude Code is the **architect/reviewer** (see `../CLAUDE.md`); Codex is the **implementer** (see `../AGENTS.md`). This MCP server is the courier between them: it turns Goal/Delta Contracts into Codex runs and returns the structured handoff for Claude to review. It is **language-agnostic** — all verification commands come from the contract's Success Conditions, never from this server.
|
|
6
|
+
|
|
7
|
+
## How it works
|
|
8
|
+
|
|
9
|
+
Each job spawns `codex app-server` (Codex's JSON-RPC interface, stdio/JSONL) and drives it:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
initialize
|
|
13
|
+
→ thread/start (or thread/resume for rework)
|
|
14
|
+
→ thread/goal/set objective = the Goal Contract, distilled ← this is /goal
|
|
15
|
+
→ turn/start input = the full rendered contract
|
|
16
|
+
→ ... Codex's goal-continuation loop runs turns until the goal is achieved ...
|
|
17
|
+
→ job done when goal status is terminal (complete / budget_limited),
|
|
18
|
+
or the thread goes quiet after a turn with the goal still active
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Setting the thread goal is the programmatic equivalent of the `/goal` slash command in the Codex TUI: Codex audits its own work against the objective after each turn and keeps going ("Goal achieved" / "Goal unmet") until it passes or hits the token budget.
|
|
22
|
+
|
|
23
|
+
## Tools
|
|
24
|
+
|
|
25
|
+
| Tool | Purpose |
|
|
26
|
+
|---|---|
|
|
27
|
+
| `codex_implement` | Start a job from a Goal Contract (`goal`, `constraints[]`, `success_conditions[]`, optional `token_budget`, `cwd`). Returns `job_id` immediately. |
|
|
28
|
+
| `codex_status` | Poll a job: state, thread id, goal status/tokens, turn count, transcript tail. |
|
|
29
|
+
| `codex_result` | Fetch a finished job: final message + parsed handoff (Changed Files / Validation / Success Conditions / Risks & Deviations). A missing section ⇒ `handoff.valid: false` — per CLAUDE.md that is itself a review failure. |
|
|
30
|
+
| `codex_rework` | Resume the same Codex thread with a Delta Contract (`findings[]`, `failed_conditions[]`). The existing thread goal re-engages. |
|
|
31
|
+
|
|
32
|
+
## Configuration (env vars)
|
|
33
|
+
|
|
34
|
+
| Var | Default | Meaning |
|
|
35
|
+
|---|---|---|
|
|
36
|
+
| `CODEX_BIN` | `codex` | Codex executable |
|
|
37
|
+
| `CODEX_ARGS` | `app-server` | Args (space-separated) |
|
|
38
|
+
| `CODEX_CWD` | server cwd | Default working directory for jobs |
|
|
39
|
+
| `CODEX_MODEL` | (codex default) | Model override passed to `thread/start` |
|
|
40
|
+
| `CODEX_APPROVAL_POLICY` | `never` | Codex approval policy (autonomous worker) |
|
|
41
|
+
| `CODEX_PERMISSIONS` | (unset) | Codex permissions profile passthrough |
|
|
42
|
+
| `CODEX_JOB_TIMEOUT_MS` | `1800000` | Hard cap per job (30 min) |
|
|
43
|
+
| `CODEX_QUIET_MS` | `30000` | Quiet window after a turn before declaring a still-active goal finished |
|
|
44
|
+
| `GOAL_OBJECTIVE_MAX` | `2000` | Max length of the `thread/goal/set` objective string |
|
|
45
|
+
|
|
46
|
+
## Run / test
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
bun install
|
|
50
|
+
bun test # e2e against test/mock-codex.ts (no real Codex needed)
|
|
51
|
+
bunx tsc --noEmit # typecheck
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Registered for Claude Code in `../.mcp.json`. The server itself runs on Bun (Node ≥ 22.18 also works — the TS is erasable-syntax only).
|
|
55
|
+
|
|
56
|
+
## Status / caveats
|
|
57
|
+
|
|
58
|
+
- **Verified live** against `codex` 0.144.6 (2026-07-19): `test/live-smoke.ts` drove a real contract through the MCP server — `thread/goal/set` accepted, goal tracked `tokensUsed` and went `active → complete`, Codex self-verified the success condition, and the structured handoff parsed valid. Also covered by `bun test` against `test/mock-codex.ts` (no real Codex or model spend needed).
|
|
59
|
+
- Requires a reasonably current Codex CLI (`npm i -g @openai/codex@latest`). Note: a stale install can fail with an instant `zsh: killed` after a macOS update — reinstalling fixes it.
|
|
60
|
+
- If Codex's goals feature is unavailable in a build ("goals feature is disabled"), `thread/goal/set` failure degrades the job gracefully to single-turn mode and notes it in the transcript.
|
|
61
|
+
- Goals require a persisted thread; `thread/start` defaults are used (no ephemeral threads).
|
|
62
|
+
- Server→client requests (approvals) are declined automatically — jobs run with `approvalPolicy: never`; tune `CODEX_PERMISSIONS` if Codex needs broader sandbox rights.
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# c2c-codex — Claude↔Codex 契约桥接器(MCP 服务器)
|
|
2
|
+
|
|
3
|
+
*[English](./README.md)*
|
|
4
|
+
|
|
5
|
+
Claude Code 是**架构师/审查者**(见 `../CLAUDE.md`);Codex 是**实现者**(见 `../AGENTS.md`)。这个 MCP 服务器是两者之间的信使:它把 Goal/Delta Contract(目标/差量契约)转化为 Codex 的运行任务,并把结构化的交接结果返回给 Claude 审查。它**与具体语言无关**——所有验证命令都来自契约中的 Success Conditions(成功条件),而不是这个服务器本身。
|
|
6
|
+
|
|
7
|
+
## 工作原理
|
|
8
|
+
|
|
9
|
+
每个任务都会启动一个 `codex app-server`(Codex 的 JSON-RPC 接口,基于 stdio/JSONL)并驱动它:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
initialize
|
|
13
|
+
→ thread/start (或 rework 时用 thread/resume)
|
|
14
|
+
→ thread/goal/set objective = 提炼后的 Goal Contract ← 相当于 /goal
|
|
15
|
+
→ turn/start input = 完整渲染后的契约内容
|
|
16
|
+
→ ... Codex 的目标持续循环会不断运行 turn,直到目标达成 ...
|
|
17
|
+
→ 当目标状态终止(complete / budget_limited),
|
|
18
|
+
或线程在某次 turn 后陷入沉默但目标仍处于 active 状态时,任务结束
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
设置线程目标(thread goal)在编程层面等价于 Codex TUI 里的 `/goal` 斜杠命令:Codex 会在每次 turn 之后对照目标自我审核("Goal achieved" / "Goal unmet"),并持续运行直到通过审核或达到 token 预算上限。
|
|
22
|
+
|
|
23
|
+
## 工具列表
|
|
24
|
+
|
|
25
|
+
| 工具 | 用途 |
|
|
26
|
+
|---|---|
|
|
27
|
+
| `codex_implement` | 依据 Goal Contract(`goal`、`constraints[]`、`success_conditions[]`,可选 `token_budget`、`cwd`)启动一个任务,立即返回 `job_id`。 |
|
|
28
|
+
| `codex_status` | 查询任务状态:state、thread id、goal 状态/token 消耗、turn 计数、transcript 尾部内容。 |
|
|
29
|
+
| `codex_result` | 获取已完成任务的结果:最终消息 + 解析出的交接内容(Changed Files / Validation / Success Conditions / Risks & Deviations)。缺失任何一部分 ⇒ `handoff.valid: false`——依据 CLAUDE.md,这本身就算审查失败。 |
|
|
30
|
+
| `codex_rework` | 用 Delta Contract(`findings[]`、`failed_conditions[]`)恢复同一个 Codex 线程。原有的线程目标会重新生效。 |
|
|
31
|
+
|
|
32
|
+
## 配置(环境变量)
|
|
33
|
+
|
|
34
|
+
| 变量 | 默认值 | 含义 |
|
|
35
|
+
|---|---|---|
|
|
36
|
+
| `CODEX_BIN` | `codex` | Codex 可执行文件 |
|
|
37
|
+
| `CODEX_ARGS` | `app-server` | 参数(空格分隔) |
|
|
38
|
+
| `CODEX_CWD` | 服务器自身 cwd | 任务的默认工作目录 |
|
|
39
|
+
| `CODEX_MODEL` | (Codex 默认值) | 传给 `thread/start` 的模型覆盖项 |
|
|
40
|
+
| `CODEX_APPROVAL_POLICY` | `never` | Codex 审批策略(自主执行模式) |
|
|
41
|
+
| `CODEX_PERMISSIONS` | (未设置) | Codex 权限配置透传 |
|
|
42
|
+
| `CODEX_JOB_TIMEOUT_MS` | `1800000` | 单个任务的硬性超时上限(30 分钟) |
|
|
43
|
+
| `CODEX_QUIET_MS` | `30000` | 一次 turn 结束后、判定"目标仍处于 active 但已完成"之前的静默等待时间 |
|
|
44
|
+
| `GOAL_OBJECTIVE_MAX` | `2000` | `thread/goal/set` objective 字符串的最大长度 |
|
|
45
|
+
|
|
46
|
+
## 运行 / 测试
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
bun install
|
|
50
|
+
bun test # 针对 test/mock-codex.ts 做端到端测试(不需要真实 Codex)
|
|
51
|
+
bunx tsc --noEmit # 类型检查
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
已在 `../.mcp.json` 中为 Claude Code 注册。服务器本身运行在 Bun 上(Node ≥ 22.18 也可以——TS 代码只使用可擦除语法)。
|
|
55
|
+
|
|
56
|
+
## 状态 / 注意事项
|
|
57
|
+
|
|
58
|
+
- **已通过真实环境验证**:针对 `codex` 0.144.6(2026-07-19)——`test/live-smoke.ts` 驱动了一个真实契约走完整个 MCP 服务器流程:`thread/goal/set` 被接受,目标追踪了 `tokensUsed` 并经历 `active → complete`,Codex 自我验证了成功条件,交接内容解析有效。同时也被 `bun test`(基于 `test/mock-codex.ts`)覆盖,无需真实 Codex 或模型开销。
|
|
59
|
+
- 需要较新版本的 Codex CLI(`npm i -g @openai/codex@latest`)。注意:macOS 更新后,过旧的安装可能会立即 `zsh: killed`,重新安装即可解决。
|
|
60
|
+
- 如果某个构建版本中 Codex 的 goals 功能不可用("goals feature is disabled"),`thread/goal/set` 失败会优雅降级为单轮(single-turn)模式,并在 transcript 中记录说明。
|
|
61
|
+
- Goals 功能依赖持久化的线程;使用 `thread/start` 的默认设置(不使用临时线程)。
|
|
62
|
+
- 服务器→客户端的请求(审批类)会被自动拒绝——任务以 `approvalPolicy: never` 运行;如果 Codex 需要更宽松的沙箱权限,可调整 `CODEX_PERMISSIONS`。
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/server.ts
|
|
4
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
|
|
8
|
+
// src/contracts.ts
|
|
9
|
+
import { readFile } from "fs/promises";
|
|
10
|
+
import { join } from "path";
|
|
11
|
+
var PROTOCOL_INSTRUCTIONS = `## Protocol Instructions
|
|
12
|
+
|
|
13
|
+
### Execution Rules
|
|
14
|
+
- Implement exactly what the contract specifies. Do not decide or expand what to build.
|
|
15
|
+
- Think before coding: surface confusion and tradeoffs instead of assuming.
|
|
16
|
+
- Prefer the minimum code that solves the problem. Make surgical changes and do no extra work.
|
|
17
|
+
- Follow the target project's declared toolchain. Add tests whenever a Success Condition requires them.
|
|
18
|
+
|
|
19
|
+
### Self-Verification
|
|
20
|
+
Before declaring completion:
|
|
21
|
+
1. Run the project's own typecheck/build and test commands, plus every command named in the Success Conditions. All must pass.
|
|
22
|
+
2. Walk through every Success Condition and confirm it is met. If one cannot be met, say so explicitly and do not declare completion.
|
|
23
|
+
3. Re-read the Constraints and confirm none were violated, including all "Do not modify" lists.
|
|
24
|
+
|
|
25
|
+
### Required Handoff
|
|
26
|
+
End every task with exactly these sections:
|
|
27
|
+
|
|
28
|
+
### Changed Files
|
|
29
|
+
- path \u2014 one-line reason
|
|
30
|
+
|
|
31
|
+
### Validation
|
|
32
|
+
- Commands run and their results.
|
|
33
|
+
|
|
34
|
+
### Success Conditions
|
|
35
|
+
- [x]/[ ] Each condition from the contract, with evidence.
|
|
36
|
+
|
|
37
|
+
### Risks & Deviations
|
|
38
|
+
- Known risks, assumptions, and deviations, or "none".
|
|
39
|
+
|
|
40
|
+
### Rework
|
|
41
|
+
For a Delta Contract, fix only its review findings and failed Success Conditions. Do not revisit work that passed review. Repeat the full Self-Verification and Required Handoff.`;
|
|
42
|
+
async function optionalAgentsInstructions(cwd) {
|
|
43
|
+
try {
|
|
44
|
+
return await readFile(join(cwd, "AGENTS.md"), "utf8");
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (error?.code === "ENOENT") return void 0;
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function renderGoalContract(c, cwd = process.cwd()) {
|
|
51
|
+
const agents = await optionalAgentsInstructions(cwd);
|
|
52
|
+
const lines = [
|
|
53
|
+
PROTOCOL_INSTRUCTIONS,
|
|
54
|
+
...agents ? ["", "## Project Instructions", "", agents.trim()] : [],
|
|
55
|
+
"",
|
|
56
|
+
"## Goal Contract",
|
|
57
|
+
"",
|
|
58
|
+
"### Goal",
|
|
59
|
+
c.goal.trim(),
|
|
60
|
+
"",
|
|
61
|
+
"### Constraints",
|
|
62
|
+
...c.constraints.map((x) => `- ${x}`),
|
|
63
|
+
"",
|
|
64
|
+
"### Success Conditions",
|
|
65
|
+
...c.success_conditions.map((x) => `- [ ] ${x}`)
|
|
66
|
+
];
|
|
67
|
+
return lines.join("\n");
|
|
68
|
+
}
|
|
69
|
+
function renderDeltaContract(d) {
|
|
70
|
+
const lines = [
|
|
71
|
+
PROTOCOL_INSTRUCTIONS,
|
|
72
|
+
"",
|
|
73
|
+
"## Delta Contract",
|
|
74
|
+
"",
|
|
75
|
+
"### Findings",
|
|
76
|
+
...d.findings.map((x) => `- ${x}`),
|
|
77
|
+
"",
|
|
78
|
+
"### Failed Success Conditions",
|
|
79
|
+
...d.failed_conditions.map((x) => `- [ ] ${x}`),
|
|
80
|
+
"",
|
|
81
|
+
"### Constraints",
|
|
82
|
+
"- Original constraints still apply. Fix only the findings above; do not touch work that passed.",
|
|
83
|
+
...(d.constraints ?? []).map((x) => `- ${x}`)
|
|
84
|
+
];
|
|
85
|
+
return lines.join("\n");
|
|
86
|
+
}
|
|
87
|
+
function renderObjective(c, maxLen) {
|
|
88
|
+
const conditions = c.success_conditions.map((x) => `(${x})`).join(" ");
|
|
89
|
+
const text = `Fulfill this Goal Contract: ${c.goal.trim()} The goal is achieved only when every Success Condition is verified: ${conditions} and a structured handoff (Changed Files / Validation / Success Conditions / Risks & Deviations) has been delivered.`;
|
|
90
|
+
return text.length <= maxLen ? text : text.slice(0, maxLen - 1) + "\u2026";
|
|
91
|
+
}
|
|
92
|
+
var HANDOFF_SECTIONS = [
|
|
93
|
+
"Changed Files",
|
|
94
|
+
"Validation",
|
|
95
|
+
"Success Conditions",
|
|
96
|
+
"Risks & Deviations"
|
|
97
|
+
];
|
|
98
|
+
function parseHandoff(text) {
|
|
99
|
+
const sections = {};
|
|
100
|
+
const headingRe = /^#{2,4}\s+(.+?):?\s*$/gm;
|
|
101
|
+
const found = [];
|
|
102
|
+
let m;
|
|
103
|
+
while ((m = headingRe.exec(text)) !== null) {
|
|
104
|
+
found.push({ name: m[1].trim(), start: m.index, contentStart: m.index + m[0].length });
|
|
105
|
+
}
|
|
106
|
+
for (let i = 0; i < found.length; i++) {
|
|
107
|
+
const end = i + 1 < found.length ? found[i + 1].start : text.length;
|
|
108
|
+
const canonical = HANDOFF_SECTIONS.find(
|
|
109
|
+
(s) => s.toLowerCase() === found[i].name.toLowerCase()
|
|
110
|
+
);
|
|
111
|
+
if (canonical) sections[canonical] = text.slice(found[i].contentStart, end).trim();
|
|
112
|
+
}
|
|
113
|
+
const missing = HANDOFF_SECTIONS.filter((s) => !(s in sections));
|
|
114
|
+
return { valid: missing.length === 0, missing: [...missing], sections };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/jobs.ts
|
|
118
|
+
import { randomUUID } from "crypto";
|
|
119
|
+
|
|
120
|
+
// src/codex-client.ts
|
|
121
|
+
import { spawn } from "child_process";
|
|
122
|
+
import { createInterface } from "readline";
|
|
123
|
+
var CodexAppServer = class {
|
|
124
|
+
proc;
|
|
125
|
+
nextId = 1;
|
|
126
|
+
pending = /* @__PURE__ */ new Map();
|
|
127
|
+
opts;
|
|
128
|
+
stderrTail = [];
|
|
129
|
+
exited = false;
|
|
130
|
+
constructor(opts) {
|
|
131
|
+
this.opts = opts;
|
|
132
|
+
this.proc = spawn(opts.bin, opts.args, {
|
|
133
|
+
cwd: opts.cwd,
|
|
134
|
+
env: { ...process.env, ...opts.env },
|
|
135
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
136
|
+
});
|
|
137
|
+
const rl = createInterface({ input: this.proc.stdout });
|
|
138
|
+
rl.on("line", (line) => this.handleLine(line));
|
|
139
|
+
const rlErr = createInterface({ input: this.proc.stderr });
|
|
140
|
+
rlErr.on("line", (line) => {
|
|
141
|
+
this.stderrTail.push(line);
|
|
142
|
+
if (this.stderrTail.length > 50) this.stderrTail.shift();
|
|
143
|
+
opts.onLog?.(line);
|
|
144
|
+
});
|
|
145
|
+
this.proc.on("exit", (code) => {
|
|
146
|
+
this.exited = true;
|
|
147
|
+
for (const [, p] of this.pending) {
|
|
148
|
+
clearTimeout(p.timer);
|
|
149
|
+
p.reject(new Error(`codex app-server exited (code ${code}). stderr: ${this.stderrTail.slice(-10).join(" | ")}`));
|
|
150
|
+
}
|
|
151
|
+
this.pending.clear();
|
|
152
|
+
opts.onExit?.(code);
|
|
153
|
+
});
|
|
154
|
+
this.proc.on("error", (err) => {
|
|
155
|
+
this.exited = true;
|
|
156
|
+
for (const [, p] of this.pending) {
|
|
157
|
+
clearTimeout(p.timer);
|
|
158
|
+
p.reject(new Error(`failed to spawn ${opts.bin}: ${err.message}`));
|
|
159
|
+
}
|
|
160
|
+
this.pending.clear();
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
handleLine(line) {
|
|
164
|
+
const trimmed = line.trim();
|
|
165
|
+
if (!trimmed) return;
|
|
166
|
+
let msg;
|
|
167
|
+
try {
|
|
168
|
+
msg = JSON.parse(trimmed);
|
|
169
|
+
} catch {
|
|
170
|
+
this.opts.onLog?.(`unparseable line from codex: ${trimmed.slice(0, 200)}`);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (msg.id !== void 0 && msg.method === void 0) {
|
|
174
|
+
const p = this.pending.get(msg.id);
|
|
175
|
+
if (!p) return;
|
|
176
|
+
this.pending.delete(msg.id);
|
|
177
|
+
clearTimeout(p.timer);
|
|
178
|
+
if (msg.error) {
|
|
179
|
+
p.reject(new Error(`${msg.error.code ?? ""} ${msg.error.message ?? JSON.stringify(msg.error)}`.trim()));
|
|
180
|
+
} else {
|
|
181
|
+
p.resolve(msg.result);
|
|
182
|
+
}
|
|
183
|
+
} else if (msg.id !== void 0 && msg.method !== void 0) {
|
|
184
|
+
this.opts.onLog?.(`declining server request: ${msg.method}`);
|
|
185
|
+
this.send({ id: msg.id, error: { code: -32601, message: `client does not handle ${msg.method}` } });
|
|
186
|
+
} else if (msg.method !== void 0) {
|
|
187
|
+
this.opts.onNotification?.(msg.method, msg.params);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
send(obj) {
|
|
191
|
+
if (this.exited) return;
|
|
192
|
+
this.proc.stdin.write(JSON.stringify(obj) + "\n");
|
|
193
|
+
}
|
|
194
|
+
request(method, params, timeoutMs = 6e4) {
|
|
195
|
+
const id = this.nextId++;
|
|
196
|
+
return new Promise((resolve, reject) => {
|
|
197
|
+
const timer = setTimeout(() => {
|
|
198
|
+
this.pending.delete(id);
|
|
199
|
+
reject(new Error(`request ${method} timed out after ${timeoutMs}ms`));
|
|
200
|
+
}, timeoutMs);
|
|
201
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
202
|
+
this.send({ id, method, ...params !== void 0 ? { params } : {} });
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
notify(method, params) {
|
|
206
|
+
this.send({ method, ...params !== void 0 ? { params } : {} });
|
|
207
|
+
}
|
|
208
|
+
async initialize() {
|
|
209
|
+
const result = await this.request("initialize", {
|
|
210
|
+
clientInfo: { name: "claude2codex", title: "Claude\u2194Codex Contract Bridge", version: "0.1.0" },
|
|
211
|
+
capabilities: { experimentalApi: true }
|
|
212
|
+
});
|
|
213
|
+
this.notify("initialized", {});
|
|
214
|
+
return result;
|
|
215
|
+
}
|
|
216
|
+
kill() {
|
|
217
|
+
if (!this.exited) this.proc.kill("SIGTERM");
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
// src/jobs.ts
|
|
222
|
+
var jobs = /* @__PURE__ */ new Map();
|
|
223
|
+
function getJob(id) {
|
|
224
|
+
return jobs.get(id);
|
|
225
|
+
}
|
|
226
|
+
function startJob(opts) {
|
|
227
|
+
const job = {
|
|
228
|
+
id: randomUUID().slice(0, 8),
|
|
229
|
+
kind: opts.kind,
|
|
230
|
+
state: "starting",
|
|
231
|
+
goalSet: false,
|
|
232
|
+
turns: 0,
|
|
233
|
+
transcript: [],
|
|
234
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
235
|
+
};
|
|
236
|
+
jobs.set(job.id, job);
|
|
237
|
+
void runJob(job, opts).catch((err) => {
|
|
238
|
+
finish(job, "error", void 0, String(err?.message ?? err));
|
|
239
|
+
});
|
|
240
|
+
return job;
|
|
241
|
+
}
|
|
242
|
+
function log(job, kind, detail) {
|
|
243
|
+
job.transcript.push({ at: (/* @__PURE__ */ new Date()).toISOString(), kind, detail: detail.slice(0, 500) });
|
|
244
|
+
if (job.transcript.length > 200) job.transcript.splice(0, job.transcript.length - 200);
|
|
245
|
+
}
|
|
246
|
+
function finish(job, state, client, error) {
|
|
247
|
+
if (job.state === "done" || job.state === "error" || job.state === "timeout") return;
|
|
248
|
+
job.state = state;
|
|
249
|
+
job.error = error;
|
|
250
|
+
job.endedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
251
|
+
if (job.finalMessage) job.handoff = parseHandoff(job.finalMessage);
|
|
252
|
+
client?.kill();
|
|
253
|
+
}
|
|
254
|
+
function extractText(item) {
|
|
255
|
+
if (typeof item?.text === "string") return item.text;
|
|
256
|
+
if (typeof item?.message === "string") return item.message;
|
|
257
|
+
if (typeof item?.content === "string") return item.content;
|
|
258
|
+
if (Array.isArray(item?.content)) {
|
|
259
|
+
const parts = item.content.map((c) => typeof c === "string" ? c : c?.text).filter((x) => typeof x === "string");
|
|
260
|
+
if (parts.length) return parts.join("\n");
|
|
261
|
+
}
|
|
262
|
+
return void 0;
|
|
263
|
+
}
|
|
264
|
+
function extractGoal(params) {
|
|
265
|
+
const g = params?.goal ?? params ?? {};
|
|
266
|
+
return {
|
|
267
|
+
status: g.status,
|
|
268
|
+
tokensUsed: g.tokensUsed ?? g.tokens_used,
|
|
269
|
+
tokenBudget: g.tokenBudget ?? g.token_budget,
|
|
270
|
+
timeUsedSeconds: g.timeUsedSeconds ?? g.time_used_seconds
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
var TERMINAL_GOAL_STATUSES = /* @__PURE__ */ new Set(["complete", "budget_limited", "budgetLimited"]);
|
|
274
|
+
async function runJob(job, opts) {
|
|
275
|
+
const cfg = opts.config;
|
|
276
|
+
let activeTurn = false;
|
|
277
|
+
let quietTimer;
|
|
278
|
+
let settle;
|
|
279
|
+
const doneSignal = new Promise((res) => settle = res);
|
|
280
|
+
const progress = (event, message) => opts.onProgress?.({ jobId: job.id, event, message });
|
|
281
|
+
const client = new CodexAppServer({
|
|
282
|
+
bin: cfg.bin,
|
|
283
|
+
args: cfg.args,
|
|
284
|
+
cwd: cfg.cwd,
|
|
285
|
+
onLog: (line) => log(job, "log", line),
|
|
286
|
+
onExit: (code) => {
|
|
287
|
+
if (job.state === "starting" || job.state === "running") {
|
|
288
|
+
finish(job, "error", void 0, `codex app-server exited early (code ${code})`);
|
|
289
|
+
}
|
|
290
|
+
settle();
|
|
291
|
+
},
|
|
292
|
+
onNotification: (method, params) => handleNotification(method, params)
|
|
293
|
+
});
|
|
294
|
+
function armQuietTimer() {
|
|
295
|
+
clearTimeout(quietTimer);
|
|
296
|
+
quietTimer = setTimeout(() => {
|
|
297
|
+
log(job, "quiet", `no goal continuation within ${cfg.quietMs}ms; treating turn as final`);
|
|
298
|
+
finish(job, "done", client);
|
|
299
|
+
settle();
|
|
300
|
+
}, cfg.quietMs);
|
|
301
|
+
}
|
|
302
|
+
function handleNotification(method, params) {
|
|
303
|
+
const p = params ?? {};
|
|
304
|
+
switch (method) {
|
|
305
|
+
case "turn/started": {
|
|
306
|
+
activeTurn = true;
|
|
307
|
+
job.turns += 1;
|
|
308
|
+
clearTimeout(quietTimer);
|
|
309
|
+
log(job, "turn", `turn ${job.turns} started`);
|
|
310
|
+
progress("turn_started", `turn ${job.turns} started`);
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
case "item/completed": {
|
|
314
|
+
const item = p.item ?? p;
|
|
315
|
+
const type = String(item?.type ?? "item");
|
|
316
|
+
if (/agent.?message/i.test(type)) {
|
|
317
|
+
const text = extractText(item);
|
|
318
|
+
if (text) {
|
|
319
|
+
job.finalMessage = text;
|
|
320
|
+
log(job, "message", text.slice(0, 200));
|
|
321
|
+
progress("agent_message", text.slice(0, 200));
|
|
322
|
+
}
|
|
323
|
+
} else if (/command/i.test(type)) {
|
|
324
|
+
log(job, "command", String(item?.command ?? item?.detail ?? ""));
|
|
325
|
+
} else {
|
|
326
|
+
log(job, "item", type);
|
|
327
|
+
}
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
case "thread/goal/updated": {
|
|
331
|
+
job.goal = extractGoal(p);
|
|
332
|
+
log(job, "goal", `status=${job.goal.status} tokensUsed=${job.goal.tokensUsed ?? "?"}`);
|
|
333
|
+
progress("goal_updated", `goal status=${job.goal.status ?? "unknown"}`);
|
|
334
|
+
if (job.goal.status && TERMINAL_GOAL_STATUSES.has(job.goal.status) && !activeTurn) {
|
|
335
|
+
finish(job, "done", client);
|
|
336
|
+
settle();
|
|
337
|
+
}
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
case "turn/completed":
|
|
341
|
+
case "turn/failed": {
|
|
342
|
+
activeTurn = false;
|
|
343
|
+
const status = p.turn?.status ?? (method === "turn/failed" ? "failed" : "completed");
|
|
344
|
+
log(job, "turn", `turn ended (${status})`);
|
|
345
|
+
progress("turn_ended", `turn ended (${status})`);
|
|
346
|
+
void onTurnEnded();
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
349
|
+
default:
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
async function onTurnEnded() {
|
|
354
|
+
if (!job.goalSet) {
|
|
355
|
+
finish(job, "done", client);
|
|
356
|
+
settle();
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (job.goal?.status && TERMINAL_GOAL_STATUSES.has(job.goal.status)) {
|
|
360
|
+
finish(job, "done", client);
|
|
361
|
+
settle();
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
try {
|
|
365
|
+
const res = await client.request("thread/goal/get", { threadId: job.threadId }, 15e3);
|
|
366
|
+
if (res?.goal) {
|
|
367
|
+
job.goal = extractGoal(res);
|
|
368
|
+
if (job.goal.status && TERMINAL_GOAL_STATUSES.has(job.goal.status)) {
|
|
369
|
+
finish(job, "done", client);
|
|
370
|
+
settle();
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
} catch (err) {
|
|
375
|
+
log(job, "log", `thread/goal/get failed: ${err?.message ?? err}`);
|
|
376
|
+
}
|
|
377
|
+
if (!activeTurn) armQuietTimer();
|
|
378
|
+
}
|
|
379
|
+
const overallTimer = setTimeout(() => {
|
|
380
|
+
finish(job, "timeout", client, `job exceeded ${cfg.jobTimeoutMs}ms`);
|
|
381
|
+
settle();
|
|
382
|
+
}, cfg.jobTimeoutMs);
|
|
383
|
+
try {
|
|
384
|
+
await client.initialize();
|
|
385
|
+
const threadParams = {
|
|
386
|
+
cwd: cfg.cwd,
|
|
387
|
+
approvalPolicy: cfg.approvalPolicy,
|
|
388
|
+
...cfg.model ? { model: cfg.model } : {},
|
|
389
|
+
...cfg.permissions ? { permissions: cfg.permissions } : {},
|
|
390
|
+
...opts.reasoningEffort ? { config: { model_reasoning_effort: opts.reasoningEffort } } : {}
|
|
391
|
+
};
|
|
392
|
+
let threadRes;
|
|
393
|
+
if (opts.resumeThreadId) {
|
|
394
|
+
threadRes = await client.request("thread/resume", { threadId: opts.resumeThreadId, ...threadParams });
|
|
395
|
+
job.threadId = threadRes?.thread?.id ?? opts.resumeThreadId;
|
|
396
|
+
job.goalSet = true;
|
|
397
|
+
} else {
|
|
398
|
+
threadRes = await client.request("thread/start", { ...threadParams, sessionStartSource: "startup" });
|
|
399
|
+
job.threadId = threadRes?.thread?.id ?? threadRes?.id;
|
|
400
|
+
}
|
|
401
|
+
if (!job.threadId) throw new Error(`could not determine thread id from ${JSON.stringify(threadRes).slice(0, 300)}`);
|
|
402
|
+
log(job, "thread", `thread ${job.threadId} ${opts.resumeThreadId ? "resumed" : "started"}`);
|
|
403
|
+
if (opts.objective) {
|
|
404
|
+
try {
|
|
405
|
+
const goalRes = await client.request("thread/goal/set", {
|
|
406
|
+
threadId: job.threadId,
|
|
407
|
+
objective: opts.objective,
|
|
408
|
+
...opts.tokenBudget ? { tokenBudget: opts.tokenBudget } : {}
|
|
409
|
+
});
|
|
410
|
+
job.goalSet = true;
|
|
411
|
+
job.goal = extractGoal(goalRes);
|
|
412
|
+
log(job, "goal", `goal set (status=${job.goal.status ?? "active"})`);
|
|
413
|
+
progress("goal_updated", `goal status=${job.goal.status ?? "active"}`);
|
|
414
|
+
} catch (err) {
|
|
415
|
+
log(job, "goal", `thread/goal/set failed (${err?.message ?? err}); continuing without goal loop`);
|
|
416
|
+
job.goalSet = false;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
job.state = "running";
|
|
420
|
+
await client.request("turn/start", {
|
|
421
|
+
threadId: job.threadId,
|
|
422
|
+
input: [{ type: "text", text: opts.prompt }],
|
|
423
|
+
...opts.reasoningEffort ? { effort: opts.reasoningEffort } : {}
|
|
424
|
+
});
|
|
425
|
+
await doneSignal;
|
|
426
|
+
} finally {
|
|
427
|
+
clearTimeout(overallTimer);
|
|
428
|
+
clearTimeout(quietTimer);
|
|
429
|
+
client.kill();
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// src/config.ts
|
|
434
|
+
async function readCodexConfig(cfg) {
|
|
435
|
+
const client = new CodexAppServer({ bin: cfg.bin, args: cfg.args, cwd: cfg.cwd });
|
|
436
|
+
try {
|
|
437
|
+
const initialized = await client.initialize();
|
|
438
|
+
const result = await client.request("config/read", {}, 15e3);
|
|
439
|
+
const current = result?.config ?? result ?? {};
|
|
440
|
+
return {
|
|
441
|
+
model: current.model ?? cfg.model ?? null,
|
|
442
|
+
default_effort: current.model_reasoning_effort ?? current.reasoning_effort ?? null,
|
|
443
|
+
version: initialized?.userAgent ?? initialized?.user_agent ?? "unknown",
|
|
444
|
+
config: current
|
|
445
|
+
};
|
|
446
|
+
} finally {
|
|
447
|
+
client.kill();
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// src/server.ts
|
|
452
|
+
function envInt(name, fallback) {
|
|
453
|
+
const v = process.env[name];
|
|
454
|
+
const n = v ? Number.parseInt(v, 10) : NaN;
|
|
455
|
+
return Number.isFinite(n) ? n : fallback;
|
|
456
|
+
}
|
|
457
|
+
function config(cwd) {
|
|
458
|
+
return {
|
|
459
|
+
bin: process.env.CODEX_BIN ?? "codex",
|
|
460
|
+
args: (process.env.CODEX_ARGS ?? "app-server").split(" ").filter(Boolean),
|
|
461
|
+
cwd: cwd ?? process.env.CODEX_CWD ?? process.cwd(),
|
|
462
|
+
model: process.env.CODEX_MODEL || void 0,
|
|
463
|
+
approvalPolicy: process.env.CODEX_APPROVAL_POLICY ?? "never",
|
|
464
|
+
permissions: process.env.CODEX_PERMISSIONS || void 0,
|
|
465
|
+
jobTimeoutMs: envInt("CODEX_JOB_TIMEOUT_MS", 18e5),
|
|
466
|
+
quietMs: envInt("CODEX_QUIET_MS", 3e4)
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
var OBJECTIVE_MAX = envInt("GOAL_OBJECTIVE_MAX", 2e3);
|
|
470
|
+
function jobSummary(job, transcriptTail = 15) {
|
|
471
|
+
return {
|
|
472
|
+
job_id: job.id,
|
|
473
|
+
kind: job.kind,
|
|
474
|
+
state: job.state,
|
|
475
|
+
thread_id: job.threadId ?? null,
|
|
476
|
+
goal: job.goal ?? null,
|
|
477
|
+
turns: job.turns,
|
|
478
|
+
started_at: job.startedAt,
|
|
479
|
+
ended_at: job.endedAt ?? null,
|
|
480
|
+
error: job.error ?? null,
|
|
481
|
+
transcript_tail: job.transcript.slice(-transcriptTail)
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
function textResult(obj, isError = false) {
|
|
485
|
+
return {
|
|
486
|
+
content: [{ type: "text", text: JSON.stringify(obj, null, 2) }],
|
|
487
|
+
isError
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
var server = new McpServer({ name: "c2c-codex", version: "0.1.0" });
|
|
491
|
+
var reasoningEffort = z.enum(["low", "medium", "high", "xhigh", "max", "ultra"]);
|
|
492
|
+
function progressReporter() {
|
|
493
|
+
let sequence = 0;
|
|
494
|
+
return (event) => {
|
|
495
|
+
sequence += 1;
|
|
496
|
+
void server.server.notification({
|
|
497
|
+
method: "notifications/progress",
|
|
498
|
+
params: {
|
|
499
|
+
progressToken: event.jobId,
|
|
500
|
+
progress: sequence,
|
|
501
|
+
message: JSON.stringify(event)
|
|
502
|
+
}
|
|
503
|
+
}).catch(() => void 0);
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
server.registerTool(
|
|
507
|
+
"codex_implement",
|
|
508
|
+
{
|
|
509
|
+
title: "Delegate a Goal Contract to Codex",
|
|
510
|
+
description: "Start a Codex implementation job for a Goal Contract. Sets the contract as a Codex thread goal (the /goal loop keeps Codex iterating until the objective is achieved or the token budget is hit) and sends the full contract as the first turn. Returns a job_id immediately; poll codex_status, then fetch codex_result when done. Language-agnostic: verification commands come from the contract's Success Conditions, not from this server.",
|
|
511
|
+
inputSchema: {
|
|
512
|
+
goal: z.string().min(1).describe("What the code must do when this task is done."),
|
|
513
|
+
constraints: z.array(z.string()).describe("Technical boundaries: files to touch, patterns to follow, 'Do not modify' lists."),
|
|
514
|
+
success_conditions: z.array(z.string()).min(1).describe("Checkable criteria proving the goal is met. At least one must be a runnable command/test."),
|
|
515
|
+
token_budget: z.number().int().positive().optional().describe("Optional token budget for the Codex goal loop (thread/goal/set tokenBudget)."),
|
|
516
|
+
cwd: z.string().optional().describe("Working directory for Codex. Defaults to the project directory."),
|
|
517
|
+
reasoning_effort: reasoningEffort.optional().describe("Optional Codex reasoning effort; omitted uses the Codex default.")
|
|
518
|
+
}
|
|
519
|
+
},
|
|
520
|
+
async (input) => {
|
|
521
|
+
const contract = {
|
|
522
|
+
goal: input.goal,
|
|
523
|
+
constraints: input.constraints,
|
|
524
|
+
success_conditions: input.success_conditions
|
|
525
|
+
};
|
|
526
|
+
const jobConfig = config(input.cwd);
|
|
527
|
+
const job = startJob({
|
|
528
|
+
kind: "implement",
|
|
529
|
+
prompt: await renderGoalContract(contract, jobConfig.cwd),
|
|
530
|
+
objective: renderObjective(contract, OBJECTIVE_MAX),
|
|
531
|
+
tokenBudget: input.token_budget,
|
|
532
|
+
config: jobConfig,
|
|
533
|
+
reasoningEffort: input.reasoning_effort,
|
|
534
|
+
onProgress: progressReporter()
|
|
535
|
+
});
|
|
536
|
+
return textResult({
|
|
537
|
+
job_id: job.id,
|
|
538
|
+
state: job.state,
|
|
539
|
+
note: "Job started. Poll codex_status until state is 'done', then call codex_result."
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
);
|
|
543
|
+
server.registerTool(
|
|
544
|
+
"codex_rework",
|
|
545
|
+
{
|
|
546
|
+
title: "Send a Delta Contract to Codex",
|
|
547
|
+
description: "Resume the Codex thread from a previous implement job and hand it a Delta Contract (review findings + failed success conditions). The existing thread goal re-engages, so the /goal loop continues until the objective passes. Returns a new job_id.",
|
|
548
|
+
inputSchema: {
|
|
549
|
+
job_id: z.string().optional().describe("The original implement job's id (preferred; resolves the thread to resume)."),
|
|
550
|
+
thread_id: z.string().optional().describe("Codex thread id, if the job registry is gone (e.g. after a restart)."),
|
|
551
|
+
findings: z.array(z.string()).min(1).describe("What is wrong, with file/line references."),
|
|
552
|
+
failed_conditions: z.array(z.string()).describe("The specific Success Conditions from the original contract that did not pass."),
|
|
553
|
+
constraints: z.array(z.string()).optional().describe("Additional constraints beyond the originals."),
|
|
554
|
+
cwd: z.string().optional().describe("Working directory for Codex. Defaults to the project directory."),
|
|
555
|
+
reasoning_effort: reasoningEffort.optional().describe("Optional Codex reasoning effort; omitted uses the Codex default.")
|
|
556
|
+
}
|
|
557
|
+
},
|
|
558
|
+
async (input) => {
|
|
559
|
+
let threadId = input.thread_id;
|
|
560
|
+
if (!threadId && input.job_id) {
|
|
561
|
+
const prev = getJob(input.job_id);
|
|
562
|
+
if (!prev) return textResult({ error: `unknown job_id ${input.job_id}` }, true);
|
|
563
|
+
threadId = prev.threadId;
|
|
564
|
+
}
|
|
565
|
+
if (!threadId) {
|
|
566
|
+
return textResult({ error: "provide job_id (of the implement job) or thread_id" }, true);
|
|
567
|
+
}
|
|
568
|
+
const job = startJob({
|
|
569
|
+
kind: "rework",
|
|
570
|
+
prompt: renderDeltaContract({
|
|
571
|
+
findings: input.findings,
|
|
572
|
+
failed_conditions: input.failed_conditions,
|
|
573
|
+
constraints: input.constraints
|
|
574
|
+
}),
|
|
575
|
+
resumeThreadId: threadId,
|
|
576
|
+
config: config(input.cwd),
|
|
577
|
+
reasoningEffort: input.reasoning_effort,
|
|
578
|
+
onProgress: progressReporter()
|
|
579
|
+
});
|
|
580
|
+
return textResult({
|
|
581
|
+
job_id: job.id,
|
|
582
|
+
resumed_thread_id: threadId,
|
|
583
|
+
state: job.state,
|
|
584
|
+
note: "Rework job started. Poll codex_status until state is 'done', then call codex_result."
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
);
|
|
588
|
+
server.registerTool(
|
|
589
|
+
"codex_config",
|
|
590
|
+
{
|
|
591
|
+
title: "Read current Codex configuration",
|
|
592
|
+
description: "Read-only view of the Codex CLI model, default reasoning effort, version, and current configuration.",
|
|
593
|
+
inputSchema: {
|
|
594
|
+
cwd: z.string().optional().describe("Directory whose Codex configuration should be resolved.")
|
|
595
|
+
},
|
|
596
|
+
annotations: { readOnlyHint: true }
|
|
597
|
+
},
|
|
598
|
+
async (input) => textResult(await readCodexConfig(config(input.cwd)))
|
|
599
|
+
);
|
|
600
|
+
server.registerTool(
|
|
601
|
+
"codex_status",
|
|
602
|
+
{
|
|
603
|
+
title: "Check a Codex job",
|
|
604
|
+
description: "Status of a running or finished Codex job: state, thread id, goal state (status/tokens), turn count, and a tail of the activity transcript.",
|
|
605
|
+
inputSchema: {
|
|
606
|
+
job_id: z.string().describe("Job id returned by codex_implement or codex_rework.")
|
|
607
|
+
}
|
|
608
|
+
},
|
|
609
|
+
async (input) => {
|
|
610
|
+
const job = getJob(input.job_id);
|
|
611
|
+
if (!job) return textResult({ error: `unknown job_id ${input.job_id}` }, true);
|
|
612
|
+
return textResult(jobSummary(job));
|
|
613
|
+
}
|
|
614
|
+
);
|
|
615
|
+
server.registerTool(
|
|
616
|
+
"codex_result",
|
|
617
|
+
{
|
|
618
|
+
title: "Fetch a finished Codex job's handoff",
|
|
619
|
+
description: "Final result of a finished job: Codex's final message, the parsed structured handoff (Changed Files / Validation / Success Conditions / Risks & Deviations \u2014 a missing section is itself a review failure), and final goal state. Errors if the job is still running.",
|
|
620
|
+
inputSchema: {
|
|
621
|
+
job_id: z.string().describe("Job id returned by codex_implement or codex_rework.")
|
|
622
|
+
}
|
|
623
|
+
},
|
|
624
|
+
async (input) => {
|
|
625
|
+
const job = getJob(input.job_id);
|
|
626
|
+
if (!job) return textResult({ error: `unknown job_id ${input.job_id}` }, true);
|
|
627
|
+
if (job.state === "starting" || job.state === "running") {
|
|
628
|
+
return textResult({ error: `job ${job.id} is still ${job.state}; poll codex_status` }, true);
|
|
629
|
+
}
|
|
630
|
+
return textResult({
|
|
631
|
+
...jobSummary(job, 5),
|
|
632
|
+
final_message: job.finalMessage ?? null,
|
|
633
|
+
handoff: job.handoff ?? null
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
);
|
|
637
|
+
var transport = new StdioServerTransport();
|
|
638
|
+
await server.connect(transport);
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "claude2codex",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "MCP server bridging Claude (architect/reviewer) and Codex (implementer) via Goal/Delta Contracts and Codex's thread-goal loop.",
|
|
6
|
+
"bin": {
|
|
7
|
+
"claude2codex": "dist/server.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"keywords": [
|
|
13
|
+
"mcp",
|
|
14
|
+
"codex",
|
|
15
|
+
"claude",
|
|
16
|
+
"ai-agents"
|
|
17
|
+
],
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/PrestoOverture/c2c.git",
|
|
21
|
+
"directory": "mcp"
|
|
22
|
+
},
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"start": "bun src/server.ts",
|
|
29
|
+
"build": "tsup",
|
|
30
|
+
"test": "bun test"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
34
|
+
"zod": "^4.4.3"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"bun-types": "^1.3.14",
|
|
38
|
+
"tsup": "^8.5.0",
|
|
39
|
+
"typescript": "^7.0.2"
|
|
40
|
+
}
|
|
41
|
+
}
|