@minhspark/codex-mcp-bridge 1.13.0 → 1.13.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/CHANGELOG.md CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  Follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [SemVer](https://semver.org/).
4
4
 
5
+ ## [1.13.1] - 2026-09-05
6
+
7
+ ### Fixed
8
+
9
+ - Make Desktop task mode independent of the external app-server for status, thread discovery, diagnostics, and fallback delivery. A missing native relay reports an actionable failure without starting another process or taking a task's writer lock.
10
+ - Verify native connectivity through Desktop projects and list its authorized local recent/pinned Codex tasks. Explain the snapshot coverage and reject unsupported `loadedOnly` queries instead of returning external-server state.
11
+ - Persist external autostart off when installing Claude Desktop in Desktop task mode, while preserving explicit legacy mode and existing permission settings. Reconnect the native companion after upgrading to enable the new allowlisted thread-list operation.
12
+
13
+ ### Verification
14
+
15
+ - Windows suite: 300 passed, 0 failed, 8 platform-specific skips. Added absent-relay, zero-external-connection, native-list authorization, and overlapping-send regressions.
16
+ - Live Windows checks returned bridge 1.13.1, eight native local projects, recent Desktop tasks, and an existing task's history while port 8791 remained stopped.
17
+
5
18
  ## [1.13.0] - 2026-09-05
6
19
 
7
20
  ### Added
package/README.md CHANGED
@@ -24,7 +24,11 @@ Call `delegate_to_codex` with `cwd`, `prompt`, and optionally `name`. The bridge
24
24
 
25
25
  `start_codex_thread` requires `prompt` in Desktop mode and returns after acceptance. Use `delegate_to_codex` to also wait for the reply. `send_to_codex_thread` continues the existing Desktop task without attaching another writer; its cwd cannot be changed. On timeout, the task continues. Inspect it in Desktop and use its Stop button to interrupt it. Quota failures and approval/input requests remain visible; delivery does not bypass them. An uncertain creation or send is never automatically repeated through another backend.
26
26
 
27
- The updated companion exposes a separate `-desktop-tasks` endpoint so a previous companion holding the legacy reply socket need not be killed during an upgrade. `codex_bridge_status` reports the selected mode; `native_relay_status` reports both endpoints.
27
+ The updated companion exposes a separate `-desktop-tasks` endpoint so a previous companion holding the legacy reply socket need not be killed during an upgrade. `codex_bridge_status` verifies the native connection through Desktop's local project list; `native_relay_status` reports both endpoints. Reconnect the companion after upgrading so it accepts the current native operations.
28
+
29
+ Desktop mode never contacts, starts, or falls back to an external app-server, including during status checks and thread discovery. The Claude Desktop installer records `CODEX_BRIDGE_AUTOSTART=0` in this mode. If Desktop or its companion is unavailable, the bridge reports the failure and leaves existing tasks in Desktop. `stop_codex_app_server` is a no-op in Desktop mode; stop an obsolete external service through its own launcher after confirming it has no active work.
30
+
31
+ `list_codex_threads` lists authorized local Codex tasks from Desktop's latest 50 non-pinned tasks and all pinned tasks, then applies workspace/title filters and the requested result limit. This snapshot does not cover the complete archive. Desktop does not expose the external server's `loadedOnly` state; requesting it returns an explanation without contacting that server.
28
32
 
29
33
  ## Architecture
30
34
 
@@ -46,7 +50,7 @@ both bridges ───────┤ named pipe / unix socket
46
50
  └────────────────────────────────────────────────────────────────────┘
47
51
  ```
48
52
 
49
- - The app-server is a **singleton per port**. The bridge probes `http://127.0.0.1:8791/readyz`; if nothing answers it spawns a detached `codex app-server --listen ws://127.0.0.1:8791`, which keeps running after the bridge exits.
53
+ - In app-server mode, the app-server is a **singleton per port**. The bridge probes `http://127.0.0.1:8791/readyz`; with autostart enabled, if nothing answers it spawns a detached `codex app-server --listen ws://127.0.0.1:8791`, which keeps running after the bridge exits. Desktop mode does not use this path.
50
54
  - Every client pointed at the same URL shares **one app-server**, so `thread/resume` with a `threadId` rejoins the running thread instead of opening a new session.
51
55
  - The bridge keeps exactly one WebSocket, calls `initialize` once, and routes notifications by `threadId`, so parallel threads never bleed into each other.
52
56
  - In app-server mode, `delegate_to_codex` starts the thread at the supplied `cwd`, names it, sends the prompt, and unsubscribes that thread after a terminal turn. It opens `codex://threads/<id>` only after unload is confirmed; other threads on the shared app-server keep running. Desktop mode instead creates and assigns the task through the app immediately.
@@ -314,7 +318,7 @@ On macOS and Linux the `codex` launcher is a Node script with a `#!/usr/bin/env
314
318
  |---|---|---|
315
319
  | `delegate_to_codex` | Creates a named Codex thread at the requested project `cwd`, sends Claude's prompt, returns the reply, releases the bridge writer lock, and opens the exact session in Codex Desktop when enabled. | destructive |
316
320
  | `send_to_codex_thread` | Sends a prompt as a user turn into `threadId`, waits for `turn/completed`, returns Codex's reply plus an activity trail (commands run, files changed). | destructive |
317
- | `list_codex_threads` | Lists threads (id, title, cwd, last update, status) so you can pick the **exact** `threadId`. `loadedOnly: true` shows only threads live inside the app-server. Windows and macOS rows carry a `codex://threads/<id>` deep link. | read-only |
321
+ | `list_codex_threads` | Lists threads (id, title, cwd, last update, status) so you can pick the **exact** `threadId`. Desktop mode uses recent/pinned authorized local tasks; app-server mode supports `loadedOnly: true`. Windows and macOS rows carry a `codex://threads/<id>` deep link. | read-only |
318
322
  | `start_codex_thread` | Opens a new Codex thread at a permitted `cwd`, optionally names it, and returns its `threadId`; the bridge applies its configured safe sandbox and approval policy. | writes |
319
323
  | `read_codex_thread` | Reads the recent conversation without sending anything. | read-only |
320
324
  | `interrupt_codex_turn` | Stops a turn that is still running. | destructive |
@@ -520,7 +524,7 @@ The bridge reads these from the environment its MCP client hands it — there is
520
524
  |---|---|---|
521
525
  | `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. |
522
526
  | `CODEX_BIN` | auto-detected | Path to `codex` used for autostart. |
523
- | `CODEX_BRIDGE_AUTOSTART` | `1` | `0` = never spawn an app-server; one must already be running. |
527
+ | `CODEX_BRIDGE_AUTOSTART` | `1` in app-server mode | `0` = never spawn an external app-server. Always off in Desktop mode, which does not need one. |
524
528
  | `CODEX_BRIDGE_THREAD_POLICY` | `owned` in the direct server; installer writes `roots` for new v1.11.2 entries | What authorizes a thread: `owned` (created by this bridge, or listed in `CODEX_BRIDGE_ALLOWED_THREADS`) or `roots` (working inside `CODEX_BRIDGE_ALLOWED_ROOTS`). Existing config values are preserved on upgrade. |
525
529
  | `CODEX_BRIDGE_ALLOWED_THREADS` | empty | Exact comma-separated thread IDs permitted for read/send/interrupt/open/list; `*` explicitly permits every thread ID. Under `roots`, the workspace check still runs. |
526
530
  | `CODEX_BRIDGE_ALLOWED_ROOTS` | `*` in the v1.11.2 installer | Absolute project directories permitted for `cwd`, separated by `:` (`;` on Windows); `*` means every usable workspace. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minhspark/codex-mcp-bridge",
3
- "version": "1.13.0",
3
+ "version": "1.13.1",
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,6 +50,7 @@ cfg.mcpServers = cfg.mcpServers ?? {};
50
50
  */
51
51
  const previousEnv = (reset ? {} : cfg.mcpServers["codex-bridge"]?.env) ?? {};
52
52
  const settled = (name, fallback) => process.env[name] ?? previousEnv[name] ?? fallback;
53
+ const desktopMode = settled("CODEX_BRIDGE_DESKTOP_TASKS", desktopTasksConfigured() ? "1" : "0");
53
54
 
54
55
  const kept = Object.keys(previousEnv).filter(
55
56
  (name) => process.env[name] === undefined && name !== "CODEX_BIN",
@@ -86,7 +87,8 @@ cfg.mcpServers["codex-bridge"] = {
86
87
  CODEX_BRIDGE_SANDBOX: settled("CODEX_BRIDGE_SANDBOX", "workspace-write"),
87
88
  CODEX_BRIDGE_OPEN_IN_APP: settled("CODEX_BRIDGE_OPEN_IN_APP", IS_WINDOWS ? "1" : "0"),
88
89
  CODEX_BRIDGE_RELEASE_AFTER_TURN: settled("CODEX_BRIDGE_RELEASE_AFTER_TURN", IS_WINDOWS ? "1" : "0"),
89
- CODEX_BRIDGE_DESKTOP_TASKS: settled("CODEX_BRIDGE_DESKTOP_TASKS", desktopTasksConfigured() ? "1" : "0"),
90
+ CODEX_BRIDGE_DESKTOP_TASKS: desktopMode,
91
+ CODEX_BRIDGE_AUTOSTART: desktopMode === "1" ? "0" : settled("CODEX_BRIDGE_AUTOSTART", "1"),
90
92
  ...(process.env.CODEX_BRIDGE_ALLOWED_THREADS !== undefined
91
93
  ? { CODEX_BRIDGE_ALLOWED_THREADS: process.env.CODEX_BRIDGE_ALLOWED_THREADS }
92
94
  : {}),
@@ -8,7 +8,7 @@ import { CodexAppServerClient } from "../src/app-server-client.mjs";
8
8
  import { bootstrapRelayThread, readRelayConfig, relayConfigPath, relaySocketPath, writeRelayConfig } from "../src/native-relay.mjs";
9
9
  import { IS_MACOS, IS_WINDOWS, PLATFORM_LABEL, homeDir, resolveCodexBin, spawnEnv } from "../src/platform.mjs";
10
10
 
11
- const VERSION = "1.13.0";
11
+ const VERSION = "1.13.1";
12
12
  const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
13
13
  const entry = path.join(root, "src", "native-relay-companion.mjs");
14
14
  const serverName = process.env.CODEX_NATIVE_RELAY_NAME ?? "codex-native-relay";
@@ -8,7 +8,7 @@ import { PLATFORM_LABEL } from "./platform.mjs";
8
8
  import { PeerEndpoint, findClaudeSession, listClaudeSessions, readTranscript } from "./peer-protocol.mjs";
9
9
  import { createThreadDelivery } from "./thread-delivery.mjs";
10
10
 
11
- const VERSION = "1.13.0";
11
+ const VERSION = "1.13.1";
12
12
  const FORWARD_MIN_INTERVAL_MS = 5000;
13
13
  const FORWARD_MAX_PER_SESSION = 50;
14
14
 
package/src/index.mjs CHANGED
@@ -25,7 +25,7 @@ import { BridgeSecurityPolicy } from "./security-policy.mjs";
25
25
  import { DesktopTaskDelivery } from "./thread-delivery.mjs";
26
26
  import { desktopTasksConfigured } from "./native-relay.mjs";
27
27
 
28
- const VERSION = "1.13.0";
28
+ const VERSION = "1.13.1";
29
29
  const log = (msg) => process.stderr.write(`[codex-mcp-bridge] ${msg}\n`);
30
30
 
31
31
  /**
@@ -47,7 +47,7 @@ const security = new BridgeSecurityPolicy();
47
47
  const desktopTasksEnabled = desktopTasksConfigured();
48
48
  const desktopTasks = new DesktopTaskDelivery({ security });
49
49
 
50
- const client = new CodexAppServerClient({
50
+ const client = desktopTasksEnabled ? null : new CodexAppServerClient({
51
51
  clientInfo: { name: "codex-mcp-bridge", title: "Codex MCP Bridge", version: VERSION },
52
52
  log,
53
53
  });
@@ -212,9 +212,9 @@ async function finishDesktopHandoff({ threadId, result, openInApp, releaseAfterT
212
212
  }
213
213
 
214
214
  function formatThreadRow(t) {
215
- const title = t.name || (t.preview ?? "").replace(/\s+/g, " ").slice(0, 70) || "(no title)";
215
+ const title = t.title || t.name || (t.preview ?? "").replace(/\s+/g, " ").slice(0, 70) || "(no title)";
216
216
  const updated = t.updatedAt ? new Date(t.updatedAt * 1000).toISOString().replace("T", " ").slice(0, 16) : "?";
217
- const status = t.status?.type ?? "?";
217
+ const status = typeof t.status === "string" ? t.status : t.status?.type ?? "?";
218
218
  const deepLink = supportsCodexThreadLinks() ? `\n open: ${codexThreadUrl(t.id)}` : "";
219
219
  const authorized = security.isThreadAuthorized(t.id, t.cwd)
220
220
  ? ""
@@ -408,7 +408,7 @@ server.registerTool(
408
408
  },
409
409
  },
410
410
  async ({ threadId, prompt, timeoutSec, cwd, model, effort, name, openInApp, releaseAfterTurn }) => {
411
- return client.withThread(threadId, async () => {
411
+ return (desktopTasksEnabled ? desktopTasks : client).withThread(threadId, async () => {
412
412
  const notes = [];
413
413
  const shouldOpen = openInApp ?? DEFAULT_OPEN_IN_APP;
414
414
  const shouldRelease = releaseAfterTurn ?? DEFAULT_RELEASE_AFTER_TURN;
@@ -513,6 +513,11 @@ server.registerTool(
513
513
  },
514
514
  async ({ limit, cwd, searchTerm, loadedOnly }) => {
515
515
  try {
516
+ if (desktopTasksEnabled) {
517
+ const workspace = cwd ? resolveWorkspacePath(cwd) : null;
518
+ const { rows, coverage } = await desktopTasks.list({ limit, cwd: workspace?.path, searchTerm, loadedOnly });
519
+ return textResult(`${rows.length} Codex thread(s) via Codex Desktop:\n${coverage}\n\n${rows.length ? rows.map(formatThreadRow).join("\n") : "No matching authorized local Codex tasks in this snapshot."}`);
520
+ }
516
521
  const params = { limit: limit ?? 15 };
517
522
  if (cwd) {
518
523
  const workspace = resolveWorkspacePath(cwd);
@@ -754,6 +759,7 @@ server.registerTool(
754
759
  },
755
760
  async () => {
756
761
  try {
762
+ if (desktopTasksEnabled) return textResult("Desktop-only mode does not manage an external app-server. Codex Desktop and its running tasks were left unchanged.");
757
763
  const result = await client.stopServer();
758
764
  if (result.stillListening) {
759
765
  return textResult("The app-server is still listening after the stop request; its thread writer locks are not confirmed released.", true);
@@ -784,6 +790,22 @@ server.registerTool(
784
790
  },
785
791
  async () => {
786
792
  const summary = security.summary();
793
+ if (desktopTasksEnabled) {
794
+ const native = await desktopTasks.status();
795
+ return textResult([
796
+ `platform: ${PLATFORM_LABEL} (${process.platform}/${process.arch})`,
797
+ `bridge version: ${VERSION}`,
798
+ `node: ${process.version} at ${process.execPath}`,
799
+ "desktop tasks: enabled; Desktop permissions, exact saved project, immediate visibility",
800
+ `native relay: ${native.available ? "available; verified through Codex Desktop" : "unavailable"}`,
801
+ `native endpoint: ${native.socketPath}`,
802
+ ...(native.available ? [`local projects: ${native.localProjects}`] : [`reason: ${native.reason}`]),
803
+ "app-server: disabled in Desktop-only mode; no external endpoint is contacted",
804
+ "autostart: off; external app-server fallback is disabled",
805
+ `security: thread policy ${security.threadPolicy}, ${summary.allowAllRoots ? "all directories" : `${summary.allowedRoots.length} allowed root(s)`}; task permissions belong to Codex Desktop`,
806
+ `claude desktop config: ${claudeDesktopConfigPath()}`,
807
+ ].join("\n"), !native.available);
808
+ }
787
809
  const up = await client.isServerUp();
788
810
  let liveThreads = null;
789
811
  if (up) {
@@ -836,4 +858,4 @@ server.registerTool(
836
858
 
837
859
  const transport = new StdioServerTransport();
838
860
  await server.connect(transport);
839
- log(`ready on ${PLATFORM_LABEL} (app-server endpoint: ${client.url}, codex: ${client.codexBin})`);
861
+ log(desktopTasksEnabled ? `ready on ${PLATFORM_LABEL} (Codex Desktop only; external app-server disabled)` : `ready on ${PLATFORM_LABEL} (app-server endpoint: ${client.url}, codex: ${client.codexBin})`);
@@ -20,7 +20,7 @@ import {
20
20
  } from "./native-relay.mjs";
21
21
  import { IS_WINDOWS, PLATFORM_LABEL } from "./platform.mjs";
22
22
 
23
- const VERSION = "1.13.0";
23
+ const VERSION = "1.13.1";
24
24
  const log = (msg) => process.stderr.write(`[native-relay] ${msg}\n`);
25
25
 
26
26
  function errorResponse(code, message) {
@@ -178,6 +178,9 @@ export function validateDesktopOperation(operation, args) {
178
178
  case "list_projects":
179
179
  valid = exactObject(args, []);
180
180
  break;
181
+ case "list_threads":
182
+ valid = exactObject(args, ["limit"]) && optionalInteger(args.limit, 1, 50);
183
+ break;
181
184
  case "create_thread":
182
185
  valid = exactObject(args, ["prompt", "target", "model", "thinking", "title"]) &&
183
186
  nonempty(args.prompt) && optionalText(args.title) && modelSettings() &&
@@ -1,4 +1,4 @@
1
- import { NativeDesktopRelay, desktopTaskSocketPath } from "./native-relay.mjs";
1
+ import { NativeDesktopRelay, desktopTaskSocketPath, desktopTasksConfigured } from "./native-relay.mjs";
2
2
  import { runTurn } from "./turn.mjs";
3
3
  import { realpathSync } from "node:fs";
4
4
  import path from "node:path";
@@ -45,11 +45,59 @@ export class DesktopTaskDelivery {
45
45
  this.security = security;
46
46
  this.sleep = sleep;
47
47
  this.now = now;
48
+ this.threadOperations = new Map();
48
49
  }
49
50
 
50
51
  async request(operation, args) {
51
- const response = await this.relay.requestDesktop(operation, args);
52
- return response.result;
52
+ try {
53
+ const response = await this.relay.requestDesktop(operation, args);
54
+ return response.result;
55
+ } catch (err) {
56
+ throw new Error(`Codex Desktop operation ${operation} failed: ${err.message}. Desktop-only mode will not start or use an external app-server. Open Codex Desktop and reconnect its native relay, then inspect the existing task before retrying a send.`, { cause: err });
57
+ }
58
+ }
59
+
60
+ async withThread(threadId, operation) {
61
+ const previous = this.threadOperations.get(threadId) ?? Promise.resolve();
62
+ const current = previous.catch(() => {}).then(operation);
63
+ this.threadOperations.set(threadId, current);
64
+ try {
65
+ return await current;
66
+ } finally {
67
+ if (this.threadOperations.get(threadId) === current) this.threadOperations.delete(threadId);
68
+ }
69
+ }
70
+
71
+ async status() {
72
+ try {
73
+ const response = await this.request("list_projects", {});
74
+ if (!Array.isArray(response?.projects)) throw new Error("Desktop returned no project list");
75
+ return { available: true, socketPath: this.relay.socketPath, localProjects: response.projects.filter((project) => project.projectKind === "local" && project.hostId === "local").length };
76
+ } catch (err) {
77
+ return { available: false, socketPath: this.relay.socketPath, reason: err.message };
78
+ }
79
+ }
80
+
81
+ async list({ limit = 15, cwd, searchTerm, loadedOnly = false } = {}) {
82
+ if (loadedOnly) throw new Error("Codex Desktop does not expose a loaded-only thread list. Omit loadedOnly to list its recent and pinned local tasks; no external app-server was contacted.");
83
+ if (cwd) this.security.assertCwd(cwd);
84
+ const response = await this.request("list_threads", { limit: 50 });
85
+ if (!Array.isArray(response?.threads) || !Array.isArray(response?.pinnedThreads)) throw new Error("Codex Desktop returned an invalid thread list");
86
+ if (response.unavailableHosts?.some((host) => host === "local" || host?.hostId === "local")) throw new Error("The local Codex Desktop host is unavailable; its thread list could not be confirmed");
87
+ const seen = new Set();
88
+ const rows = [...response.pinnedThreads, ...response.threads].filter((thread) => {
89
+ if (thread.kind !== "codex" || thread.hostId !== "local" || !thread.id || !thread.cwd || seen.has(thread.id)) return false;
90
+ seen.add(thread.id);
91
+ if (!this.security.isThreadAuthorized(thread.id, thread.cwd)) return false;
92
+ try {
93
+ this.security.assertCwd(thread.cwd);
94
+ if (cwd && path.relative(realpathSync.native(cwd), realpathSync.native(thread.cwd))) return false;
95
+ } catch {
96
+ return false;
97
+ }
98
+ return !searchTerm || String(thread.title ?? "").toLowerCase().includes(searchTerm.toLowerCase());
99
+ }).slice(0, limit);
100
+ return { rows, coverage: "Codex Desktop's latest 50 non-pinned tasks and all pinned tasks; local Codex workspaces only" };
53
101
  }
54
102
 
55
103
  async create({ cwd, prompt, name, model, effort }) {
@@ -144,6 +192,7 @@ export function createThreadDelivery({
144
192
  relay = new NativeDesktopRelay(),
145
193
  log = () => {},
146
194
  timeoutMs = 240000,
195
+ desktopOnly = desktopTasksConfigured(),
147
196
  releaseAfterTurn =
148
197
  process.env.CODEX_BRIDGE_RELEASE_AFTER_TURN !== undefined
149
198
  ? process.env.CODEX_BRIDGE_RELEASE_AFTER_TURN === "1"
@@ -168,6 +217,7 @@ export function createThreadDelivery({
168
217
  return { backend: NATIVE_BACKEND, threadId, ack };
169
218
  } catch (err) {
170
219
  if (err.reachedCompanion || err.code !== "RELAY_UNREACHABLE") throw err;
220
+ if (desktopOnly) throw new Error(`Codex Desktop relay is unavailable: ${err.message}. Desktop-only mode will not start or use an external app-server.`);
171
221
  log(`native relay unreachable (${err.message}); falling back to the app-server path`);
172
222
  }
173
223
  } else if (status.reason !== reportedUnavailable) {
@@ -175,6 +225,7 @@ export function createThreadDelivery({
175
225
  log(`native relay not in use: ${status.reason}`);
176
226
  }
177
227
 
228
+ if (desktopOnly) throw new Error(`Codex Desktop relay is unavailable: ${status.reason ?? "no native acknowledgement"}. Desktop-only mode will not start or use an external app-server.`);
178
229
  if (!codex) throw new Error("No Codex app-server client is configured to deliver this message");
179
230
  const send = async () => {
180
231
  await codex.ensureThreadAttached(threadId);
@@ -200,7 +251,7 @@ export function createThreadDelivery({
200
251
  const status = relay.status();
201
252
  return status.enabled
202
253
  ? `${NATIVE_BACKEND} via ${status.socketPath}`
203
- : `${APP_SERVER_BACKEND} (${status.reason})`;
254
+ : desktopOnly ? `${NATIVE_BACKEND} unavailable (${status.reason}); external app-server disabled` : `${APP_SERVER_BACKEND} (${status.reason})`;
204
255
  }
205
256
 
206
257
  return { deliver, describe };