@minhspark/codex-mcp-bridge 1.10.0 → 1.11.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/CHANGELOG.md CHANGED
@@ -2,6 +2,45 @@
2
2
 
3
3
  Follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [SemVer](https://semver.org/).
4
4
 
5
+ ## [1.11.0] - 2026-08-23
6
+
7
+ ### Added
8
+
9
+ - `CODEX_BRIDGE_THREAD_POLICY` selects what authorizes a thread: `owned` (unchanged default) or `roots`.
10
+
11
+ Under the only behaviour that existed before, a thread opened in the Codex app or the VS Code extension
12
+ was not merely restricted - it was unreachable. Codex assigns the thread ID at the moment it opens, so
13
+ the ID cannot have been listed in `CODEX_BRIDGE_ALLOWED_THREADS` beforehand, and the bridge-owned set
14
+ lives in memory and empties on every MCP server restart. The listing showed those threads, because
15
+ listing is gated on the workspace, and then every send into one answered `not authorized`. On a machine
16
+ with ten live threads, nine of them were unreachable and the tenth only because the bridge had just
17
+ created it.
18
+
19
+ `roots` grants on the workspace instead of the ID: a thread already working inside a directory named in
20
+ `CODEX_BRIDGE_ALLOWED_ROOTS` is reachable. That is the same containment every tool already enforces on
21
+ the `cwd` it is handed, applied to the `cwd` the thread reports. It is opt-in so an existing install
22
+ cannot widen silently on upgrade, and an unknown workspace fails closed exactly like one outside the
23
+ roots.
24
+
25
+ ### Fixed
26
+
27
+ - Authorization now happens **before** `thread/resume`. Attaching takes the per-thread writer lock away
28
+ from whoever else has the thread open, so deciding afterwards would have locked a thread on its way to
29
+ being refused.
30
+ - `openInApp` no longer raises a thread on screen before that thread is known to be in scope - a refusal
31
+ that leaked which threads exist.
32
+ - `codex_bridge_status` printed the approval policy under a `thread policy` label. Two different settings:
33
+ one was misreported, the other invisible. It now names both.
34
+ - The `NOT AUTHORIZED` line in `list_codex_threads` pointed only at `CODEX_BRIDGE_ALLOWED_THREADS`, which
35
+ is the option that cannot work for a thread a human just opened. It now names the policy as well.
36
+
37
+ ## [1.10.1] - 2026-08-20
38
+
39
+ ### Fixed
40
+
41
+ - **`claude_bridge_status` advertised `claude-bridge 1.3.0` inside a 1.10.0 package.** Seven releases of drift, and the only thing it could tell a bug reporter was a wrong answer about which build was running. The stale constant was the symptom: `scripts/sync-version.mjs` named `src/index.mjs` and the repo-hygiene test checked that same single file, so nothing in the release path ever looked at the second entry point. The sync script now walks both, the `version` lifecycle stages both, and the test **discovers** every `src/*.mjs` declaring `const VERSION` rather than naming one - a third bridge added later is covered without editing any of this again. Confirmed red on the previous tree: `src/claude-bridge.mjs declares 1.3.0, package.json says 1.10.0`.
42
+ - README said Windows was supported but not covered by CI, while `windows-latest` has been in the matrix since 1.10.0. A README that undersells its own coverage reads as a warning, and it is the first thing anyone evaluating the bridge sees. What stays true is narrower and is now what it says: the Codex to Claude tests skip on Windows because they need unix sockets, and the rest of the suite runs there.
43
+
5
44
  ## [1.10.0] - 2026-08-20
6
45
 
7
46
  ### Added
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Bui Dang Minh
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.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bui Dang Minh
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,5 +1,9 @@
1
1
  # codex-mcp-bridge
2
2
 
3
+ [![npm](https://img.shields.io/npm/v/@minhspark/codex-mcp-bridge?logo=npm&color=CB3837)](https://www.npmjs.com/package/@minhspark/codex-mcp-bridge)
4
+ [![CI](https://github.com/buidangminh23/codex-mcp-bridge/actions/workflows/ci.yml/badge.svg)](https://github.com/buidangminh23/codex-mcp-bridge/actions/workflows/ci.yml)
5
+ [![node](https://img.shields.io/node/v/@minhspark/codex-mcp-bridge?logo=node.js&color=5FA04E)](https://nodejs.org)
6
+ [![license](https://img.shields.io/npm/l/@minhspark/codex-mcp-bridge)](LICENSE)
3
7
  [![M8ven Verified](https://m8ven.ai/badge/mcp/buidangminh23-codex-mcp-bridge-1ke8t1?variant=verified)](https://m8ven.ai/mcp/buidangminh23-codex-mcp-bridge-1ke8t1)
4
8
 
5
9
  A **two-way** bridge between Claude and Codex: Claude pushes prompts into a **live Codex thread**, and Codex messages back into a **running Claude Code session**. Each side sees the other's sessions and follows the conversation inside its own app. Runs on **macOS, Windows and Linux** (the Codex → Claude direction needs unix sockets, so macOS/Linux only).
@@ -258,7 +262,16 @@ On macOS and Linux the `codex` launcher is a Node script with a `#!/usr/bin/env
258
262
 
259
263
  `send_to_codex_thread` also accepts `timeoutSec` (default 240), `cwd`, `model`, `effort`, and `openInApp` (macOS — surface the thread in the desktop app before sending). A timeout does **not** cancel the turn: the bridge returns what it collected plus the `turnId`; keep reading with `read_codex_thread` or stop it with `interrupt_codex_turn`.
260
264
 
261
- Thread operations are deny-by-default. Set `CODEX_BRIDGE_ALLOWED_THREADS` to a comma-separated list of exact thread IDs you want this MCP server to access; threads created by `start_codex_thread` are authorized for the lifetime of this bridge process. Set `CODEX_BRIDGE_ALLOWED_ROOTS` to the absolute project directories the bridge may use. The installer defaults that root to this repository, so change it when delegating into another project.
265
+ Thread operations are deny-by-default, and `CODEX_BRIDGE_THREAD_POLICY` decides what counts as permission:
266
+
267
+ | Policy | A thread is reachable when | Use it when |
268
+ |---|---|---|
269
+ | `owned` *(default)* | the bridge created it with `start_codex_thread`, or its exact ID is listed in `CODEX_BRIDGE_ALLOWED_THREADS` | the bridge drives threads it opens itself |
270
+ | `roots` | it is working inside a directory named in `CODEX_BRIDGE_ALLOWED_ROOTS` | you open threads in the Codex app or VS Code and want Claude to talk to them |
271
+
272
+ Under `owned`, a thread a human opened is **unreachable rather than merely restricted**: Codex assigns its ID at the moment it opens, so the ID cannot have been allowlisted beforehand, and the bridge-owned set lives in memory and empties whenever the MCP server restarts. If every live thread answers `NOT AUTHORIZED`, that is the cause — switch to `roots`.
273
+
274
+ `roots` does not remove a gate; it moves it from the ID to the workspace, which is the containment every tool already applies to the `cwd` it is handed. The bridge resolves a thread's workspace with a read **before** attaching, so a thread outside every root is refused without ever taking its writer lock. Set `CODEX_BRIDGE_ALLOWED_ROOTS` to the absolute project directories the bridge may use — the installer defaults that root to this repository, so change it when delegating into another project. A root as broad as `/` or `C:\` makes `roots` mean *every thread on this machine*.
262
275
 
263
276
  ## Tools — `claude-bridge` (runs inside Codex)
264
277
 
@@ -360,6 +373,7 @@ The bridge reads these from the environment its MCP client hands it — there is
360
373
  | `CODEX_APP_SERVER_URL` | `ws://127.0.0.1:8791` | Shared **loopback-only** app-server endpoint. Non-loopback endpoints are rejected because this bridge does not implement remote WebSocket authentication. |
361
374
  | `CODEX_BIN` | auto-detected | Path to `codex` used for autostart. |
362
375
  | `CODEX_BRIDGE_AUTOSTART` | `1` | `0` = never spawn an app-server; one must already be running. |
376
+ | `CODEX_BRIDGE_THREAD_POLICY` | `owned` | What authorizes a thread: `owned` (created by this bridge, or listed in `CODEX_BRIDGE_ALLOWED_THREADS`) or `roots` (working inside `CODEX_BRIDGE_ALLOWED_ROOTS`). Default is unchanged on upgrade, so an existing install never widens by itself. |
363
377
  | `CODEX_BRIDGE_ALLOWED_THREADS` | empty | Exact comma-separated thread IDs permitted for read/send/interrupt/open/list. Empty means no pre-existing thread access. |
364
378
  | `CODEX_BRIDGE_ALLOWED_ROOTS` | empty (installer sets its repo root) | Absolute project directories permitted for `cwd`, separated by `:` (`;` on Windows). |
365
379
  | `CODEX_BRIDGE_APPROVAL` | `deny` | How to answer approval requests from Codex. `approve` is ignored unless `CODEX_BRIDGE_AUTO_APPROVE_ACK=1` is also set. |
@@ -415,7 +429,7 @@ Runs the whole suite with `node --test`. It needs no Codex install, no login and
415
429
  | `test/platform.test.mjs` | binary resolution, the PATH handed to child processes, per-OS config paths and cwd remapping |
416
430
  | `test/repo-hygiene.test.mjs` | no environment file or build output is ever tracked, versions do not drift, documentation stays in English |
417
431
 
418
- GitHub Actions runs the same command on every push and pull request, across Node 22 and 24 on Linux and macOS (`.github/workflows/ci.yml`). Windows is supported by the bridge but not covered by CI: the Codex → Claude direction needs unix sockets, so those tests skip there anyway.
432
+ GitHub Actions runs the same command on every push and pull request, across Node 22 and 24 on Linux, macOS and Windows (`.github/workflows/ci.yml`). The Codex → Claude direction needs unix sockets, so those tests skip on Windows; the rest of the suite runs there like anywhere else.
419
433
 
420
434
  Two checks need a real Codex and are not part of `npm test`:
421
435
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minhspark/codex-mcp-bridge",
3
- "version": "1.10.0",
3
+ "version": "1.11.0",
4
4
  "description": "Two-way MCP bridge between Claude and Codex: prompts into a live Codex thread, messages into a running Claude Code session.",
5
5
  "keywords": [
6
6
  "mcp",
@@ -50,7 +50,7 @@
50
50
  "uninstall:codex": "node scripts/install-codex-mcp.mjs --remove",
51
51
  "install:agent": "node scripts/install-launch-agent.mjs",
52
52
  "uninstall:agent": "node scripts/install-launch-agent.mjs --uninstall",
53
- "version": "node scripts/sync-version.mjs && git add src/index.mjs"
53
+ "version": "node scripts/sync-version.mjs && git add src/index.mjs src/claude-bridge.mjs"
54
54
  },
55
55
  "engines": {
56
56
  "node": ">=22"
@@ -1,37 +1,37 @@
1
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
- import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
3
- import path from "node:path";
4
- import { fileURLToPath } from "node:url";
5
-
6
- const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
7
- const transport = new StdioClientTransport({
8
- command: process.execPath,
9
- args: [path.join(root, "src", "claude-bridge.mjs")],
10
- stderr: "inherit",
11
- });
12
- const client = new Client({ name: "claude-bridge-check", version: "1.0.0" });
13
- await client.connect(transport);
14
-
15
- const tools = await client.listTools();
16
- console.log("tools:", tools.tools.map((t) => t.name).join(", "));
17
-
18
- const status = await client.callTool({ name: "claude_bridge_status", arguments: {} });
19
- console.log("\n--- claude_bridge_status ---\n" + status.content[0].text);
20
-
21
- const listed = await client.callTool({ name: "list_claude_sessions", arguments: {} });
22
- console.log("\n--- list_claude_sessions ---\n" + listed.content[0].text);
23
-
24
- const target = process.env.CLAUDE_TARGET;
25
- if (target) {
26
- const message = process.env.CLAUDE_MESSAGE ?? "Ping from the Codex side of the bridge.";
27
- const waitSec = Number(process.env.CLAUDE_WAIT ?? 180);
28
- console.log(`\nsending to "${target}" (waiting ${waitSec}s)...`);
29
- const sent = await client.callTool({
30
- name: "send_to_claude_session",
31
- arguments: { target, message, waitSec },
32
- });
33
- console.log("\n--- send_to_claude_session ---\n" + sent.content[0].text);
34
- }
35
-
36
- await client.close();
37
- process.exit(0);
1
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
7
+ const transport = new StdioClientTransport({
8
+ command: process.execPath,
9
+ args: [path.join(root, "src", "claude-bridge.mjs")],
10
+ stderr: "inherit",
11
+ });
12
+ const client = new Client({ name: "claude-bridge-check", version: "1.0.0" });
13
+ await client.connect(transport);
14
+
15
+ const tools = await client.listTools();
16
+ console.log("tools:", tools.tools.map((t) => t.name).join(", "));
17
+
18
+ const status = await client.callTool({ name: "claude_bridge_status", arguments: {} });
19
+ console.log("\n--- claude_bridge_status ---\n" + status.content[0].text);
20
+
21
+ const listed = await client.callTool({ name: "list_claude_sessions", arguments: {} });
22
+ console.log("\n--- list_claude_sessions ---\n" + listed.content[0].text);
23
+
24
+ const target = process.env.CLAUDE_TARGET;
25
+ if (target) {
26
+ const message = process.env.CLAUDE_MESSAGE ?? "Ping from the Codex side of the bridge.";
27
+ const waitSec = Number(process.env.CLAUDE_WAIT ?? 180);
28
+ console.log(`\nsending to "${target}" (waiting ${waitSec}s)...`);
29
+ const sent = await client.callTool({
30
+ name: "send_to_claude_session",
31
+ arguments: { target, message, waitSec },
32
+ });
33
+ console.log("\n--- send_to_claude_session ---\n" + sent.content[0].text);
34
+ }
35
+
36
+ await client.close();
37
+ process.exit(0);
@@ -1,93 +1,93 @@
1
- import { execFileSync } from "node:child_process";
2
- import fs from "node:fs";
3
- import os from "node:os";
4
- import path from "node:path";
5
-
6
- import { IS_MACOS, LAUNCH_AGENT_LABEL, launchAgentPath, resolveCodexBin, spawnEnv } from "../src/platform.mjs";
7
-
8
- if (!IS_MACOS) {
9
- throw new Error("install-launch-agent.mjs is macOS-only. On Windows use a Startup shortcut or a scheduled task.");
10
- }
11
-
12
- const plistPath = launchAgentPath();
13
- const uninstall = process.argv.includes("--uninstall");
14
- const domain = `gui/${process.getuid()}`;
15
-
16
- function bootout() {
17
- try {
18
- execFileSync("launchctl", ["bootout", `${domain}/${LAUNCH_AGENT_LABEL}`], { stdio: "pipe" });
19
- return true;
20
- } catch {
21
- return false;
22
- }
23
- }
24
-
25
- if (uninstall) {
26
- const wasLoaded = bootout();
27
- if (fs.existsSync(plistPath)) fs.rmSync(plistPath);
28
- console.log(`removed ${plistPath}${wasLoaded ? " (agent unloaded)" : ""}`);
29
- process.exit(0);
30
- }
31
-
32
- const codexBin = resolveCodexBin(process.env.CODEX_EXE);
33
- if (!path.isAbsolute(codexBin) || !fs.existsSync(codexBin)) {
34
- throw new Error(`codex binary not found (resolved to "${codexBin}"). Set CODEX_EXE to its absolute path.`);
35
- }
36
-
37
- const url = process.env.CODEX_APP_SERVER_URL ?? "ws://127.0.0.1:8791";
38
- const logDir = path.join(os.homedir(), "Library", "Logs", "codex-mcp-bridge");
39
- fs.mkdirSync(logDir, { recursive: true });
40
- fs.mkdirSync(path.dirname(plistPath), { recursive: true });
41
-
42
- const escapeXml = (value) =>
43
- value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
44
-
45
- const plist = `<?xml version="1.0" encoding="UTF-8"?>
46
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
47
- <plist version="1.0">
48
- <dict>
49
- <key>Label</key>
50
- <string>${LAUNCH_AGENT_LABEL}</string>
51
- <key>ProgramArguments</key>
52
- <array>
53
- <string>${escapeXml(codexBin)}</string>
54
- <string>app-server</string>
55
- <string>--listen</string>
56
- <string>${escapeXml(url)}</string>
57
- </array>
58
- <key>EnvironmentVariables</key>
59
- <dict>
60
- <key>PATH</key>
61
- <string>${escapeXml(spawnEnv().PATH)}</string>
62
- <key>HOME</key>
63
- <string>${escapeXml(os.homedir())}</string>
64
- </dict>
65
- <key>RunAtLoad</key>
66
- <true/>
67
- <key>KeepAlive</key>
68
- <dict>
69
- <key>SuccessfulExit</key>
70
- <false/>
71
- </dict>
72
- <key>ThrottleInterval</key>
73
- <integer>10</integer>
74
- <key>ProcessType</key>
75
- <string>Background</string>
76
- <key>StandardOutPath</key>
77
- <string>${escapeXml(path.join(logDir, "app-server.out.log"))}</string>
78
- <key>StandardErrorPath</key>
79
- <string>${escapeXml(path.join(logDir, "app-server.err.log"))}</string>
80
- </dict>
81
- </plist>
82
- `;
83
-
84
- fs.writeFileSync(plistPath, plist, "utf8");
85
- bootout();
86
- execFileSync("launchctl", ["bootstrap", domain, plistPath], { stdio: "inherit" });
87
- execFileSync("launchctl", ["enable", `${domain}/${LAUNCH_AGENT_LABEL}`], { stdio: "pipe" });
88
-
89
- console.log(`installed ${plistPath}`);
90
- console.log(`endpoint: ${url}`);
91
- console.log(`logs: ${logDir}`);
92
- console.log(`status: launchctl print ${domain}/${LAUNCH_AGENT_LABEL} | head -20`);
93
- console.log(`remove: node scripts/install-launch-agent.mjs --uninstall`);
1
+ import { execFileSync } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+
6
+ import { IS_MACOS, LAUNCH_AGENT_LABEL, launchAgentPath, resolveCodexBin, spawnEnv } from "../src/platform.mjs";
7
+
8
+ if (!IS_MACOS) {
9
+ throw new Error("install-launch-agent.mjs is macOS-only. On Windows use a Startup shortcut or a scheduled task.");
10
+ }
11
+
12
+ const plistPath = launchAgentPath();
13
+ const uninstall = process.argv.includes("--uninstall");
14
+ const domain = `gui/${process.getuid()}`;
15
+
16
+ function bootout() {
17
+ try {
18
+ execFileSync("launchctl", ["bootout", `${domain}/${LAUNCH_AGENT_LABEL}`], { stdio: "pipe" });
19
+ return true;
20
+ } catch {
21
+ return false;
22
+ }
23
+ }
24
+
25
+ if (uninstall) {
26
+ const wasLoaded = bootout();
27
+ if (fs.existsSync(plistPath)) fs.rmSync(plistPath);
28
+ console.log(`removed ${plistPath}${wasLoaded ? " (agent unloaded)" : ""}`);
29
+ process.exit(0);
30
+ }
31
+
32
+ const codexBin = resolveCodexBin(process.env.CODEX_EXE);
33
+ if (!path.isAbsolute(codexBin) || !fs.existsSync(codexBin)) {
34
+ throw new Error(`codex binary not found (resolved to "${codexBin}"). Set CODEX_EXE to its absolute path.`);
35
+ }
36
+
37
+ const url = process.env.CODEX_APP_SERVER_URL ?? "ws://127.0.0.1:8791";
38
+ const logDir = path.join(os.homedir(), "Library", "Logs", "codex-mcp-bridge");
39
+ fs.mkdirSync(logDir, { recursive: true });
40
+ fs.mkdirSync(path.dirname(plistPath), { recursive: true });
41
+
42
+ const escapeXml = (value) =>
43
+ value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
44
+
45
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
46
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
47
+ <plist version="1.0">
48
+ <dict>
49
+ <key>Label</key>
50
+ <string>${LAUNCH_AGENT_LABEL}</string>
51
+ <key>ProgramArguments</key>
52
+ <array>
53
+ <string>${escapeXml(codexBin)}</string>
54
+ <string>app-server</string>
55
+ <string>--listen</string>
56
+ <string>${escapeXml(url)}</string>
57
+ </array>
58
+ <key>EnvironmentVariables</key>
59
+ <dict>
60
+ <key>PATH</key>
61
+ <string>${escapeXml(spawnEnv().PATH)}</string>
62
+ <key>HOME</key>
63
+ <string>${escapeXml(os.homedir())}</string>
64
+ </dict>
65
+ <key>RunAtLoad</key>
66
+ <true/>
67
+ <key>KeepAlive</key>
68
+ <dict>
69
+ <key>SuccessfulExit</key>
70
+ <false/>
71
+ </dict>
72
+ <key>ThrottleInterval</key>
73
+ <integer>10</integer>
74
+ <key>ProcessType</key>
75
+ <string>Background</string>
76
+ <key>StandardOutPath</key>
77
+ <string>${escapeXml(path.join(logDir, "app-server.out.log"))}</string>
78
+ <key>StandardErrorPath</key>
79
+ <string>${escapeXml(path.join(logDir, "app-server.err.log"))}</string>
80
+ </dict>
81
+ </plist>
82
+ `;
83
+
84
+ fs.writeFileSync(plistPath, plist, "utf8");
85
+ bootout();
86
+ execFileSync("launchctl", ["bootstrap", domain, plistPath], { stdio: "inherit" });
87
+ execFileSync("launchctl", ["enable", `${domain}/${LAUNCH_AGENT_LABEL}`], { stdio: "pipe" });
88
+
89
+ console.log(`installed ${plistPath}`);
90
+ console.log(`endpoint: ${url}`);
91
+ console.log(`logs: ${logDir}`);
92
+ console.log(`status: launchctl print ${domain}/${LAUNCH_AGENT_LABEL} | head -20`);
93
+ console.log(`remove: node scripts/install-launch-agent.mjs --uninstall`);
package/scripts/smoke.mjs CHANGED
@@ -1,50 +1,50 @@
1
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
- import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
3
- import { fileURLToPath } from "node:url";
4
- import path from "node:path";
5
-
6
- const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
7
- const cwdForTest = process.env.SMOKE_CWD ?? root;
8
-
9
- const transport = new StdioClientTransport({
10
- command: process.execPath,
11
- args: [path.join(root, "src", "index.mjs")],
12
- env: { ...process.env, CODEX_BRIDGE_ALLOWED_ROOTS: process.env.CODEX_BRIDGE_ALLOWED_ROOTS ?? cwdForTest },
13
- stderr: "inherit",
14
- });
15
- const client = new Client({ name: "smoke", version: "1.0.0" });
16
- await client.connect(transport);
17
-
18
- const tools = await client.listTools();
19
- console.log("TOOLS:", tools.tools.map((t) => t.name).join(", "));
20
-
21
- const listed = await client.callTool({ name: "list_codex_threads", arguments: { limit: 3 } });
22
- console.log("\n--- list_codex_threads ---\n" + listed.content[0].text.slice(0, 900));
23
-
24
- const created = await client.callTool({
25
- name: "start_codex_thread",
26
- arguments: { cwd: cwdForTest },
27
- });
28
- console.log("\n--- start_codex_thread ---\n" + created.content[0].text);
29
- const threadId = created.content[0].text.match(/threadId: ([0-9a-f-]+)/)?.[1];
30
- if (!threadId) throw new Error("could not parse threadId");
31
-
32
- const first = await client.callTool({
33
- name: "send_to_codex_thread",
34
- arguments: { threadId, prompt: "Remember the codeword PAPAYA. Reply with only: SAVED", timeoutSec: 180 },
35
- });
36
- console.log("\n--- send #1 ---\n" + first.content[0].text);
37
-
38
- const second = await client.callTool({
39
- name: "send_to_codex_thread",
40
- arguments: { threadId, prompt: "What codeword did I give you? Reply with only that word.", timeoutSec: 180 },
41
- });
42
- console.log("\n--- send #2 (same thread, must recall) ---\n" + second.content[0].text);
43
-
44
- const read = await client.callTool({ name: "read_codex_thread", arguments: { threadId, limit: 6 } });
45
- console.log("\n--- read_codex_thread ---\n" + read.content[0].text.slice(0, 900));
46
-
47
- const ok = /PAPAYA/i.test(second.content[0].text);
48
- console.log("\nRESULT:", ok ? "PASS - thread continuity works" : "FAIL - Codex did not recall the codeword");
49
- await client.close();
50
- process.exit(ok ? 0 : 1);
1
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
3
+ import { fileURLToPath } from "node:url";
4
+ import path from "node:path";
5
+
6
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
7
+ const cwdForTest = process.env.SMOKE_CWD ?? root;
8
+
9
+ const transport = new StdioClientTransport({
10
+ command: process.execPath,
11
+ args: [path.join(root, "src", "index.mjs")],
12
+ env: { ...process.env, CODEX_BRIDGE_ALLOWED_ROOTS: process.env.CODEX_BRIDGE_ALLOWED_ROOTS ?? cwdForTest },
13
+ stderr: "inherit",
14
+ });
15
+ const client = new Client({ name: "smoke", version: "1.0.0" });
16
+ await client.connect(transport);
17
+
18
+ const tools = await client.listTools();
19
+ console.log("TOOLS:", tools.tools.map((t) => t.name).join(", "));
20
+
21
+ const listed = await client.callTool({ name: "list_codex_threads", arguments: { limit: 3 } });
22
+ console.log("\n--- list_codex_threads ---\n" + listed.content[0].text.slice(0, 900));
23
+
24
+ const created = await client.callTool({
25
+ name: "start_codex_thread",
26
+ arguments: { cwd: cwdForTest },
27
+ });
28
+ console.log("\n--- start_codex_thread ---\n" + created.content[0].text);
29
+ const threadId = created.content[0].text.match(/threadId: ([0-9a-f-]+)/)?.[1];
30
+ if (!threadId) throw new Error("could not parse threadId");
31
+
32
+ const first = await client.callTool({
33
+ name: "send_to_codex_thread",
34
+ arguments: { threadId, prompt: "Remember the codeword PAPAYA. Reply with only: SAVED", timeoutSec: 180 },
35
+ });
36
+ console.log("\n--- send #1 ---\n" + first.content[0].text);
37
+
38
+ const second = await client.callTool({
39
+ name: "send_to_codex_thread",
40
+ arguments: { threadId, prompt: "What codeword did I give you? Reply with only that word.", timeoutSec: 180 },
41
+ });
42
+ console.log("\n--- send #2 (same thread, must recall) ---\n" + second.content[0].text);
43
+
44
+ const read = await client.callTool({ name: "read_codex_thread", arguments: { threadId, limit: 6 } });
45
+ console.log("\n--- read_codex_thread ---\n" + read.content[0].text.slice(0, 900));
46
+
47
+ const ok = /PAPAYA/i.test(second.content[0].text);
48
+ console.log("\nRESULT:", ok ? "PASS - thread continuity works" : "FAIL - Codex did not recall the codeword");
49
+ await client.close();
50
+ process.exit(ok ? 0 : 1);
@@ -4,22 +4,29 @@ import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
 
6
6
  /**
7
- * `codex_bridge_status` reports a version constant that lives in the source,
8
- * and `npm version` only ever rewrites package.json. Left to a human the two
7
+ * Both status tools report a version constant that lives in the source, and
8
+ * `npm version` only ever rewrites package.json. Left to a human the two
9
9
  * drift, and a drifting version turns every bug report into a guess about
10
10
  * which build is actually running - so this runs from the `version` lifecycle
11
11
  * script, between the bump and the commit npm makes for it.
12
+ *
13
+ * Every entry point carrying its own constant belongs in this list. One that
14
+ * is missing here is not bumped by any release, which is exactly how
15
+ * claude-bridge sat at 1.3.0 while the package shipped 1.10.0.
12
16
  */
13
17
  const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
14
- const entry = path.join(root, "src", "index.mjs");
18
+ const entries = [path.join("src", "index.mjs"), path.join("src", "claude-bridge.mjs")];
15
19
  const { version } = JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
16
20
 
17
- const before = fs.readFileSync(entry, "utf8");
18
- const after = before.replace(/^const VERSION = "[^"]+";$/m, `const VERSION = "${version}";`);
21
+ for (const entry of entries) {
22
+ const file = path.join(root, entry);
23
+ const before = fs.readFileSync(file, "utf8");
24
+ const after = before.replace(/^const VERSION = "[^"]+";$/m, `const VERSION = "${version}";`);
19
25
 
20
- if (!after.includes(`const VERSION = "${version}";`)) {
21
- throw new Error(`could not find the VERSION constant in ${entry}`);
22
- }
26
+ if (!after.includes(`const VERSION = "${version}";`)) {
27
+ throw new Error(`could not find the VERSION constant in ${entry}`);
28
+ }
23
29
 
24
- if (before !== after) fs.writeFileSync(entry, after);
25
- console.log(`src/index.mjs VERSION -> ${version}`);
30
+ if (before !== after) fs.writeFileSync(file, after);
31
+ console.log(`${entry.split(path.sep).join("/")} VERSION -> ${version}`);
32
+ }