@minhspark/codex-mcp-bridge 1.13.2 → 1.13.4

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.
@@ -0,0 +1,81 @@
1
+ _codex_bridge_installed_version() {
2
+ command node --input-type=module -e 'import fs from "node:fs"; const value = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); if (value.name !== "@minhspark/codex-mcp-bridge" || typeof value.version !== "string" || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(value.version)) process.exit(1); process.stdout.write(value.version);' "$1/@minhspark/codex-mcp-bridge/package.json" 2>/dev/null
3
+ }
4
+
5
+ npm() {
6
+ if [ "$#" -eq 0 ]; then command npm; return $?; fi
7
+ local bridge_command="${1-}" bridge_global=0 bridge_packages=0 bridge_supported=1
8
+ local bridge_dry_run=auto bridge_root= bridge_before= bridge_after= bridge_install_status=0
9
+ local bridge_after_root= bridge_mode=unknown bridge_before_verified=1
10
+ local -a bridge_arguments bridge_context
11
+ bridge_arguments=("$@")
12
+ bridge_context=(--global)
13
+ case "$bridge_command" in
14
+ install|i) shift ;;
15
+ *) command npm "${bridge_arguments[@]}"; return $? ;;
16
+ esac
17
+ while [ "$#" -gt 0 ]; do
18
+ case "$1" in
19
+ -g|--global|--global=true) bridge_global=1 ;;
20
+ @minhspark/codex-mcp-bridge|@minhspark/codex-mcp-bridge@?*) bridge_packages=$((bridge_packages + 1)) ;;
21
+ --dry-run|--dry-run=true) bridge_dry_run=true ;;
22
+ --no-dry-run|--dry-run=false) bridge_dry_run=false ;;
23
+ --no-fund|--no-audit|--force|--ignore-scripts|--foreground-scripts) ;;
24
+ --prefix=*)
25
+ if [ "${#bridge_context[@]}" -ne 1 ] || [ "$1" = --prefix= ]; then bridge_supported=0; break; fi
26
+ bridge_context+=("$1")
27
+ ;;
28
+ --prefix)
29
+ if [ "$#" -lt 2 ] || [ "${#bridge_context[@]}" -ne 1 ] || [ -z "$2" ]; then bridge_supported=0; break; fi
30
+ case "$2" in -*) bridge_supported=0; break ;; esac
31
+ bridge_context+=("$1" "$2")
32
+ shift
33
+ ;;
34
+ *) bridge_supported=0 ;;
35
+ esac
36
+ shift
37
+ done
38
+ if [ "$bridge_supported" -ne 1 ] || [ "$bridge_global" -ne 1 ] || [ "$bridge_packages" -ne 1 ]; then
39
+ command npm "${bridge_arguments[@]}"
40
+ return $?
41
+ fi
42
+ if [ "$bridge_dry_run" = auto ]; then
43
+ bridge_mode="$(command npm config get dry-run "${bridge_context[@]}" 2>/dev/null)" || bridge_mode=unknown
44
+ else
45
+ bridge_mode="$bridge_dry_run"
46
+ fi
47
+ if [ "$bridge_mode" = false ]; then
48
+ bridge_root="$(command npm root "${bridge_context[@]}" 2>/dev/null)" || bridge_root=
49
+ case "$bridge_root" in
50
+ /*)
51
+ if [ -e "$bridge_root/@minhspark/codex-mcp-bridge/package.json" ]; then
52
+ bridge_before="$(_codex_bridge_installed_version "$bridge_root")" || bridge_before_verified=0
53
+ fi
54
+ ;;
55
+ *) bridge_before_verified=0 ;;
56
+ esac
57
+ fi
58
+ command npm "${bridge_arguments[@]}" || bridge_install_status=$?
59
+ if [ "$bridge_install_status" -ne 0 ]; then
60
+ printf 'Failed to install: @minhspark/codex-mcp-bridge (exit code %s). See npm error above.\n' "$bridge_install_status" >&2
61
+ elif [ "$bridge_mode" = true ]; then
62
+ printf 'Dry run completed: @minhspark/codex-mcp-bridge (no changes applied).\n'
63
+ elif [ "$bridge_mode" != false ]; then
64
+ printf 'Warning: npm completed, but installation mode could not be verified.\n' >&2
65
+ else
66
+ bridge_after_root="$(command npm root "${bridge_context[@]}" 2>/dev/null)" || bridge_after_root=
67
+ case "$bridge_after_root" in
68
+ /*) bridge_after="$(_codex_bridge_installed_version "$bridge_after_root")" || bridge_after= ;;
69
+ esac
70
+ if [ -z "$bridge_after" ] || [ "$bridge_before_verified" -ne 1 ] || [ "$bridge_after_root" != "$bridge_root" ]; then
71
+ printf 'Warning: npm completed, but the installed @minhspark/codex-mcp-bridge version could not be verified.\n' >&2
72
+ elif [ "$bridge_after_root" = "$bridge_root" ] && [ "$bridge_after" = "$bridge_before" ]; then
73
+ printf 'Already up to date: @minhspark/codex-mcp-bridge v%s\n' "$bridge_after"
74
+ elif [ "$bridge_after_root" = "$bridge_root" ] && [ -n "$bridge_before" ]; then
75
+ printf 'Successfully updated: @minhspark/codex-mcp-bridge v%s -> v%s\n' "$bridge_before" "$bridge_after"
76
+ else
77
+ printf 'Successfully installed: @minhspark/codex-mcp-bridge v%s\n' "$bridge_after"
78
+ fi
79
+ fi
80
+ return "$bridge_install_status"
81
+ }
@@ -5,16 +5,23 @@ import { z } from "zod";
5
5
 
6
6
  import { CodexAppServerClient } from "./app-server-client.mjs";
7
7
  import { PLATFORM_LABEL } from "./platform.mjs";
8
- import { PeerEndpoint, findClaudeSession, listClaudeSessions, readTranscript } from "./peer-protocol.mjs";
8
+ import { PeerEndpoint, assertClaudeSessionCwd, findClaudeSession, listClaudeSessions, readTranscript } from "./peer-protocol.mjs";
9
9
  import { createThreadDelivery } from "./thread-delivery.mjs";
10
+ import { exitForVersionRequest } from "./cli-version.mjs";
11
+ import { desktopTasksConfigured } from "./native-relay.mjs";
12
+ import { createRuntimeState } from "./runtime-state.mjs";
10
13
 
11
- const VERSION = "1.13.2";
14
+ exitForVersionRequest(import.meta.url);
15
+
16
+ const VERSION = "1.13.4";
12
17
  const FORWARD_MIN_INTERVAL_MS = 5000;
13
18
  const FORWARD_MAX_PER_SESSION = 50;
14
19
 
15
20
  const log = (msg) => process.stderr.write(`[claude-bridge] ${msg}\n`);
16
21
 
17
22
  const defaultPeerName = process.env.CLAUDE_BRIDGE_PEER_NAME ?? `codex-${process.pid}`;
23
+ const desktopOnly = desktopTasksConfigured();
24
+ const runtime = createRuntimeState({ configuration: desktopTasksConfigured });
18
25
 
19
26
  const peer = new PeerEndpoint({
20
27
  name: defaultPeerName,
@@ -33,7 +40,7 @@ const codex = new CodexAppServerClient({
33
40
  * other thread through the shared one. `claude-bridge` never picks between
34
41
  * them - see `thread-delivery.mjs`.
35
42
  */
36
- const delivery = createThreadDelivery({ codex, log });
43
+ const delivery = createThreadDelivery({ codex, log, desktopOnly });
37
44
 
38
45
  const forwarding = {
39
46
  threadId: process.env.CODEX_THREAD_ID ?? null,
@@ -46,7 +53,12 @@ const textResult = (text, isError = false) => ({
46
53
  ...(isError ? { isError: true } : {}),
47
54
  });
48
55
 
49
- const failure = (err) => textResult(`Claude bridge error: ${err?.message ?? String(err)}`, true);
56
+ const failure = (err) => ({
57
+ ...textResult(`Claude bridge error: ${err?.message ?? String(err)}${err?.msgId ? `\nMessage id: ${err.msgId}; inspect read_claude_delivery before any resend.` : ""}`, true),
58
+ ...(err?.msgId ? { structuredContent: { receipt: peer.readDelivery(err.msgId) } } : {}),
59
+ });
60
+
61
+ const missingDesktopSession = "No live Claude Desktop session with an exact matching ID or name and a messaging endpoint. Open or reconnect an existing Code session in Claude Desktop for the intended project. CLI sessions are excluded; do not launch a replacement CLI session.";
50
62
 
51
63
  function formatSessionRow(s) {
52
64
  const started = s.startedAt ? new Date(s.startedAt).toISOString().replace("T", " ").slice(0, 16) : "?";
@@ -72,6 +84,7 @@ async function forwardToCodexThread(record) {
72
84
  forwarding.lastAt = now;
73
85
  forwarding.count += 1;
74
86
  try {
87
+ runtime.assertCurrent();
75
88
  const { backend } = await delivery.deliver(
76
89
  forwarding.threadId,
77
90
  `[message from Claude session ${record.fromSocket ?? "?"}]\n\n${record.text}`,
@@ -93,7 +106,10 @@ const server = new McpServer(
93
106
  "Talk to a live Claude Code session from Codex. list_claude_sessions finds the session, " +
94
107
  "send_to_claude_session sends to its peer transport and waits for a reply to confirm receipt. " +
95
108
  "This bridge registers itself as a peer, so Claude sees it in its own agent list and can " +
96
- "message back; bind_codex_thread relays those messages into a Codex thread.",
109
+ "message back; bind_codex_thread relays those messages into a Codex thread. " +
110
+ "In Desktop-only mode, both destinations must belong to their Desktop apps. " +
111
+ "Use an existing session with the exact project directory and sessionId; never launch a CLI session " +
112
+ "or an external app-server as a substitute. A receipt confirms a reply, not visual verification in the app.",
97
113
  },
98
114
  );
99
115
 
@@ -103,7 +119,7 @@ server.registerTool(
103
119
  title: "List live Claude Code sessions",
104
120
  description:
105
121
  "List Claude Code sessions running on this machine (name, pid, sessionId, cwd, how it was started). " +
106
- "Use it to pick the session to talk to.",
122
+ "In Desktop-only mode, only Claude Desktop Code sessions are listed. Match the exact project cwd and sessionId before sending.",
107
123
  inputSchema: {
108
124
  includeDead: z.boolean().optional().describe("Also list sessions whose process is gone (default false)"),
109
125
  },
@@ -115,10 +131,10 @@ server.registerTool(
115
131
  async ({ includeDead }) => {
116
132
  try {
117
133
  const sessions = listClaudeSessions({ includeDead: includeDead ?? false }).filter(
118
- (s) => s.pid !== process.pid,
134
+ (s) => s.pid !== process.pid && (!desktopOnly || s.entrypoint === "claude-desktop"),
119
135
  );
120
- if (!sessions.length) return textResult("No live Claude Code session found.");
121
- return textResult(`${sessions.length} Claude session(s):\n\n${sessions.map(formatSessionRow).join("\n")}`);
136
+ if (!sessions.length) return textResult(desktopOnly ? missingDesktopSession : "No live Claude Code session found.");
137
+ return textResult(`${sessions.length} Claude${desktopOnly ? " Desktop" : ""} session(s):\n\n${sessions.map(formatSessionRow).join("\n")}`);
122
138
  } catch (err) {
123
139
  return failure(err);
124
140
  }
@@ -132,11 +148,13 @@ server.registerTool(
132
148
  description:
133
149
  "Send a message to a running Claude Code session's peer transport and wait for its reply. " +
134
150
  "A socket write alone does not confirm that Claude received the message. Set waitSec to 0 to send without confirmation. " +
135
- "A waited send is refused while earlier messages to that session still await replies; " +
136
- "wait for those replies and read_claude_inbox before trying again.",
151
+ "Every send is refused while earlier messages to that session still await replies, including waitSec 0; " +
152
+ "wait for those replies and read_claude_inbox before trying again. Desktop-only mode refuses CLI or unknown " +
153
+ "entrypoints, partial names, ambiguous targets, and a missing or mismatched expectedCwd before sending. It never creates a replacement session.",
137
154
  inputSchema: {
138
155
  target: z.string().describe("Session name, pid or sessionId from list_claude_sessions"),
139
156
  message: z.string().describe("The message text to deliver"),
157
+ expectedCwd: z.string().optional().describe("Exact absolute project directory independently verified by the caller; required in Desktop-only mode"),
140
158
  waitSec: z
141
159
  .number()
142
160
  .int()
@@ -152,26 +170,36 @@ server.registerTool(
152
170
  openWorldHint: true,
153
171
  },
154
172
  },
155
- async ({ target, message, waitSec }) => {
173
+ async ({ target, message, waitSec, expectedCwd }) => {
156
174
  try {
175
+ runtime.assertCurrent();
176
+ const session = findClaudeSession(target, { desktopOnly });
177
+ if (!session) return textResult(desktopOnly ? missingDesktopSession : `No live Claude session matches "${target}".`, true);
178
+ if (desktopOnly || expectedCwd !== undefined) assertClaudeSessionCwd(session, expectedCwd);
157
179
  await peer.start();
158
- const session = findClaudeSession(target);
159
- if (!session) return textResult(`No live Claude session matches "${target}".`, true);
160
180
 
161
181
  const wait = waitSec ?? 180;
162
182
  const desktop = session.entrypoint === "claude-desktop";
163
183
  const text = desktop ? `${message}\n\n[Bridge response routing: reply with ordinary text in this conversation. The bridge reads the response associated with this message from the local transcript; no cross-session reply tool is needed.]` : message;
164
184
  const { msgId, reply, delivery } = await peer.sendAndWait(session.socket, text, {
165
185
  timeoutMs: wait * 1000,
186
+ beforeSend: () => {
187
+ runtime.assertCurrent();
188
+ const current = findClaudeSession(session.sessionId ?? String(session.pid), { desktopOnly });
189
+ if (!current || current.pid !== session.pid || current.socket !== session.socket) {
190
+ throw new Error("The Claude destination changed while this message was queued. No message was sent; inspect the existing Desktop session.");
191
+ }
192
+ if (desktopOnly || expectedCwd !== undefined) assertClaudeSessionCwd(current, expectedCwd);
193
+ },
166
194
  ...(desktop ? { transcriptSession: session } : {}),
167
195
  });
168
196
  const status = reply ? "reply_received" : delivery?.status ?? (wait === 0 ? "sent_unconfirmed" : "reply_timeout");
169
- const receipt = { status, msgId, target: session.name ?? String(session.pid), sessionId: session.sessionId, waitSec: wait, ...(reply ? { source: reply.source ?? "peer" } : {}) };
197
+ const receipt = { status, msgId, target: session.name ?? String(session.pid), sessionId: session.sessionId, cwd: session.cwd, entrypoint: session.entrypoint, waitSec: wait, ...(reply ? { source: reply.source ?? "peer" } : {}) };
170
198
  const result = (text, isError = false) => ({ ...textResult(text, isError), structuredContent: { receipt } });
171
- const targetLabel = `${session.name ?? session.pid} (pid ${session.pid}, session ${session.sessionId ?? "?"})`;
199
+ const targetLabel = `${session.name ?? session.pid} (pid ${session.pid}, session ${session.sessionId ?? "?"}, via ${session.entrypoint ?? "unknown"}, cwd ${session.cwd ?? "?"})`;
172
200
 
173
201
  if (!reply && delivery && delivery.status !== "delivered") {
174
- return result(`Claude inbox reported ${delivery.status} for ${targetLabel}.\n${delivery.reason}\nMessage id: ${msgId}`, true);
202
+ return result(`Claude inbox reported ${delivery.status} for ${targetLabel}.\n${delivery.reason}\nMessage id: ${msgId}\nInspect read_claude_delivery with this message ID. Do not resend, change the sender permission class, or alter recipient permissions to bypass this receipt.`, true);
175
203
  }
176
204
 
177
205
  if (wait === 0) {
@@ -192,6 +220,21 @@ server.registerTool(
192
220
  },
193
221
  );
194
222
 
223
+ server.registerTool(
224
+ "read_claude_delivery",
225
+ {
226
+ title: "Inspect a Claude message receipt without resending",
227
+ description: "Read the latest recipient control receipt or correlated reply for a message sent by this MCP process. Unknown IDs do not prove non-delivery; retain the original receipt after a reconnect and inspect the existing Claude session before any resend.",
228
+ inputSchema: { msgId: z.string().describe("The original message ID returned by send_to_claude_session") },
229
+ annotations: { readOnlyHint: true, openWorldHint: false },
230
+ },
231
+ async ({ msgId }) => {
232
+ const receipt = peer.readDelivery(msgId);
233
+ if (!receipt) return textResult("This MCP process has no receipt for that message ID. It may belong to a previous process; do not infer failure or resend. Inspect the original Claude Desktop session.", true);
234
+ return { ...textResult(JSON.stringify(receipt, null, 2)), structuredContent: { receipt } };
235
+ },
236
+ );
237
+
195
238
  server.registerTool(
196
239
  "read_claude_inbox",
197
240
  {
@@ -307,18 +350,25 @@ server.registerTool(
307
350
  try {
308
351
  await peer.start();
309
352
  const sessions = listClaudeSessions().filter((s) => s.pid !== process.pid);
353
+ const eligible = sessions.filter((session) => !desktopOnly || session.entrypoint === "claude-desktop");
310
354
  const lines = [
311
355
  `platform: ${PLATFORM_LABEL} (${process.platform}/${process.arch})`,
312
356
  `bridge: claude-bridge ${VERSION}`,
313
357
  `peer name: ${peer.name} (Claude sees this in its agent list)`,
314
358
  `peer socket: ${peer.socketPath}`,
315
359
  `sender mode: ${peer.permissionMode ?? "unknown (recipient may hold messages for approval)"}`,
316
- `live sessions: ${sessions.length}`,
360
+ `session policy: ${desktopOnly ? "desktop-only" : "all Claude Code entrypoints"}`,
361
+ `live sessions: ${eligible.length}`,
362
+ `excluded: ${sessions.length - eligible.length} non-Desktop session(s)`,
317
363
  `relay thread: ${forwarding.threadId ?? "(none - use bind_codex_thread)"}`,
318
364
  `delivery: ${delivery.describe()}`,
319
365
  `inbox: ${peer.inbox.length} pending message(s)`,
366
+ `outstanding: ${peer.pendingMessages.size} message(s) awaiting receipt or reply`,
367
+ ...[...peer.pendingMessages.keys()].map((id) => `pending message: ${id} (${peer.readDelivery(id)?.status ?? "sent_unconfirmed"})`),
320
368
  ];
321
- return textResult(lines.join("\n"));
369
+ const state = runtime.status();
370
+ lines.push(`runtime pid: ${state.pid}`, `loaded source: ${state.revision}`, `disk source: ${state.diskRevision ?? "unreadable"}`, `runtime state: ${state.current ? "current" : `STALE - ${state.reason}; reconnect this MCP server in the existing task`}`);
371
+ return { ...textResult(lines.join("\n"), !state.current), structuredContent: { runtime: state } };
322
372
  } catch (err) {
323
373
  return failure(err);
324
374
  }
@@ -0,0 +1,20 @@
1
+ import { readFileSync, realpathSync, writeSync } from "node:fs";
2
+ import { fileURLToPath } from "node:url";
3
+
4
+ export function exitForVersionRequest(entrypointUrl) {
5
+ if (!process.argv.slice(2).some((arg) => arg === "--version" || arg === "-v")) return;
6
+ if (!process.argv[1]) return;
7
+
8
+ let entrypoint;
9
+ try {
10
+ entrypoint = realpathSync(process.argv[1]);
11
+ } catch (error) {
12
+ if (error.code === "ENOENT" || error.code === "ENOTDIR") return;
13
+ throw error;
14
+ }
15
+ if (entrypoint !== realpathSync(fileURLToPath(entrypointUrl))) return;
16
+
17
+ const { version } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
18
+ writeSync(process.stdout.fd, `${version}\n`);
19
+ process.exit(0);
20
+ }
@@ -0,0 +1,25 @@
1
+ export function codexMcpRegistration({ name, existing, node, entry, env, desktopOnly }) {
2
+ if (existing) {
3
+ const customized = ["enabled_tools", "disabled_tools", "startup_timeout_sec", "tool_timeout_sec", "disabled_reason"]
4
+ .some((key) => existing[key] != null && (!Array.isArray(existing[key]) || existing[key].length > 0));
5
+ if (existing.enabled === false || customized || existing.transport?.type !== "stdio" || existing.transport?.cwd || existing.transport?.env_vars?.length) {
6
+ throw new Error("The existing MCP entry has custom access, timeout, or transport settings. Keep that entry and update only its command/args using Codex settings; the installer will not remove or reset it.");
7
+ }
8
+ }
9
+ const values = { ...existing?.transport?.env };
10
+ for (const key of ["CLAUDE_BRIDGE_PEER_NAME", "CLAUDE_BRIDGE_PERMISSION_MODE", "CODEX_BRIDGE_DESKTOP_TASKS"]) {
11
+ if (env[key] !== undefined) values[key] = env[key];
12
+ }
13
+ values.CODEX_BRIDGE_DESKTOP_TASKS ??= desktopOnly ? "1" : "0";
14
+ if (!["0", "1"].includes(values.CODEX_BRIDGE_DESKTOP_TASKS)) throw new Error("CODEX_BRIDGE_DESKTOP_TASKS must be 0 or 1");
15
+ if (values.CLAUDE_BRIDGE_PERMISSION_MODE !== undefined && !["bypass", "prompting"].includes(values.CLAUDE_BRIDGE_PERMISSION_MODE)) {
16
+ throw new Error("CLAUDE_BRIDGE_PERMISSION_MODE must be bypass or prompting; never infer it from the recipient");
17
+ }
18
+ if (values.CODEX_BRIDGE_DESKTOP_TASKS === "1") values.CODEX_BRIDGE_AUTOSTART = "0";
19
+ const args = ["mcp", "add", name];
20
+ for (const [key, value] of Object.entries(values).sort(([a], [b]) => a.localeCompare(b))) {
21
+ if (typeof value !== "string") throw new Error(`MCP environment value ${key} must be a string`);
22
+ args.push("--env", `${key}=${value}`);
23
+ }
24
+ return { args: [...args, "--", node, entry], environmentKeys: Object.keys(values).sort() };
25
+ }
package/src/index.mjs CHANGED
@@ -24,8 +24,12 @@ import { runTurn } from "./turn.mjs";
24
24
  import { BridgeSecurityPolicy } from "./security-policy.mjs";
25
25
  import { DesktopTaskDelivery, DESKTOP_TOOL_BUDGET_MS } from "./thread-delivery.mjs";
26
26
  import { desktopTasksConfigured } from "./native-relay.mjs";
27
+ import { exitForVersionRequest } from "./cli-version.mjs";
28
+ import { createRuntimeState } from "./runtime-state.mjs";
27
29
 
28
- const VERSION = "1.13.2";
30
+ exitForVersionRequest(import.meta.url);
31
+
32
+ const VERSION = "1.13.4";
29
33
  const log = (msg) => process.stderr.write(`[codex-mcp-bridge] ${msg}\n`);
30
34
 
31
35
  /**
@@ -45,6 +49,7 @@ const TERMINAL_TURN_STATUSES = new Set(["completed", "interrupted", "failed"]);
45
49
  const RELEASE_TURN_STATUSES = TERMINAL_TURN_STATUSES;
46
50
  const security = new BridgeSecurityPolicy();
47
51
  const desktopTasksEnabled = desktopTasksConfigured();
52
+ const runtime = createRuntimeState({ configuration: desktopTasksConfigured });
48
53
  const desktopTasks = new DesktopTaskDelivery({ security });
49
54
 
50
55
  const client = desktopTasksEnabled ? null : new CodexAppServerClient({
@@ -122,17 +127,23 @@ async function delegateDesktopTask({ cwd, prompt, name, model, effort, timeoutSe
122
127
  if (openInApp ?? DEFAULT_OPEN_IN_APP) {
123
128
  try {
124
129
  await desktopTasks.open(created.threadId, { deadline });
125
- notes.push("opened in Codex Desktop while the task runs");
130
+ notes.push(created.reused ? "opened the existing task in Codex Desktop" : "opened in Codex Desktop while the task runs");
126
131
  } catch (err) {
127
- notes.push(`task was accepted; opening its page failed: ${err.message}`);
132
+ notes.push(`${created.reused ? "existing task retained" : "task was accepted"}; opening its page failed: ${err.message}`);
128
133
  }
129
134
  }
130
135
  const lines = [
131
136
  created.reused ? "Reused the existing Codex Desktop task; the prompt was not resent" : "Delegated through Codex Desktop", `threadId: ${created.threadId}`, `name: ${created.name}`,
132
- `cwd: ${created.cwd}`, `projectId: ${created.projectId}`, `project: ${created.projectName}`,
137
+ `cwd: ${created.cwd}`,
138
+ ...(created.projectAssignmentStatus === "unverified"
139
+ ? [`expected projectId: ${created.expectedProjectId}`, `expected project: ${created.expectedProjectName ?? "(unnamed)"}`, "project assignment: unverified", created.projectAssignmentNote]
140
+ : [`projectId: ${created.projectId}`, `project: ${created.projectName ?? "(unnamed)"}`, ...(created.projectAssignmentStatus === "verified" ? ["project assignment: verified in Desktop's current listing"] : [])]),
133
141
  "permissions: Codex Desktop settings; no external app-server writer",
134
- ...(created.promptChanged ? ["The edited brief was not sent. Use send_to_codex_thread with this threadId for a continuation, or a distinct title for separate work."] : []), ...notes,
142
+ ...(created.promptChanged ? [created.projectAssignmentStatus === "unverified"
143
+ ? "The edited brief was not sent. Inspect this task's project assignment before continuing it."
144
+ : "The edited brief was not sent. Use send_to_codex_thread with this threadId for a continuation, or a distinct title for separate work."] : []), ...notes,
135
145
  ];
146
+ if (created.projectAssignmentStatus === "unverified") return textResult([...lines, "status: existing task retained; project assignment needs inspection"].join("\n"));
136
147
  if (!waitForReply) return textResult([...lines, created.reused ? "status: existing task; read it to check its current progress" : "status: accepted; the task is running in Desktop"].join("\n"));
137
148
  try {
138
149
  const result = await desktopTasks.wait(created.threadId, { timeoutMs: Math.max(0, deadline - Date.now()) });
@@ -270,7 +281,8 @@ const server = new McpServer(
270
281
  { name: "codex-bridge", version: VERSION },
271
282
  {
272
283
  instructions:
273
- "Bridge Claude work into Codex. Prefer delegate_to_codex: it creates a named Codex thread at the " +
284
+ "Bridge Claude work into Codex. Prefer an existing task: inspect its exact threadId and project, then use send_to_codex_thread. " +
285
+ "Only use delegate_to_codex or start_codex_thread when the user explicitly requests a new task at the " +
274
286
  "requested cwd. With Desktop tasks enabled it assigns the exact saved project and starts visibly in " +
275
287
  "Codex Desktop using Desktop permissions. Otherwise it releases the bridge writer lock and opens the exact thread in " +
276
288
  "Codex Desktop. Use send_to_codex_thread only when an existing threadId is intentional; use " +
@@ -278,7 +290,26 @@ const server = new McpServer(
278
290
  },
279
291
  );
280
292
 
281
- server.registerTool(
293
+ function registerTool(name, definition, handler) {
294
+ server.registerTool(name, definition, async (...args) => {
295
+ try {
296
+ if (!definition.annotations?.readOnlyHint) runtime.assertCurrent();
297
+ if (name === "codex_bridge_status") {
298
+ const state = runtime.status();
299
+ if (!state.current) return { ...failure(new Error(`${state.reason}; reconnect this MCP server in the existing task.`)), structuredContent: { runtime: state } };
300
+ const result = await handler(...args);
301
+ result.content.push({ type: "text", text: `runtime pid: ${state.pid}\nloaded source: ${state.revision}\nruntime state: current` });
302
+ result.structuredContent = { ...result.structuredContent, runtime: state };
303
+ return result;
304
+ }
305
+ return await handler(...args);
306
+ } catch (err) {
307
+ return failure(err);
308
+ }
309
+ });
310
+ }
311
+
312
+ registerTool(
282
313
  "delegate_to_codex",
283
314
  {
284
315
  title: "Delegate work to a new Codex session",
@@ -369,7 +400,7 @@ server.registerTool(
369
400
  },
370
401
  );
371
402
 
372
- server.registerTool(
403
+ registerTool(
373
404
  "send_to_codex_thread",
374
405
  {
375
406
  title: "Send a prompt to a Codex thread",
@@ -494,7 +525,7 @@ server.registerTool(
494
525
  },
495
526
  );
496
527
 
497
- server.registerTool(
528
+ registerTool(
498
529
  "list_codex_threads",
499
530
  {
500
531
  title: "List Codex threads",
@@ -580,7 +611,7 @@ server.registerTool(
580
611
  },
581
612
  );
582
613
 
583
- server.registerTool(
614
+ registerTool(
584
615
  "start_codex_thread",
585
616
  {
586
617
  title: "Start a new Codex thread",
@@ -622,7 +653,7 @@ server.registerTool(
622
653
  },
623
654
  );
624
655
 
625
- server.registerTool(
656
+ registerTool(
626
657
  "read_codex_thread",
627
658
  {
628
659
  title: "Read a Codex thread",
@@ -669,7 +700,7 @@ server.registerTool(
669
700
  },
670
701
  );
671
702
 
672
- server.registerTool(
703
+ registerTool(
673
704
  "interrupt_codex_turn",
674
705
  {
675
706
  title: "Interrupt a Codex turn",
@@ -703,7 +734,7 @@ server.registerTool(
703
734
  },
704
735
  );
705
736
 
706
- server.registerTool(
737
+ registerTool(
707
738
  "open_codex_thread",
708
739
  {
709
740
  title: "Open a Codex thread in the desktop app",
@@ -746,7 +777,7 @@ server.registerTool(
746
777
  },
747
778
  );
748
779
 
749
- server.registerTool(
780
+ registerTool(
750
781
  "stop_codex_app_server",
751
782
  {
752
783
  title: "Stop the shared Codex app-server",
@@ -780,7 +811,7 @@ server.registerTool(
780
811
  },
781
812
  );
782
813
 
783
- server.registerTool(
814
+ registerTool(
784
815
  "codex_bridge_status",
785
816
  {
786
817
  title: "Check the Codex bridge environment",
@@ -19,8 +19,11 @@ import {
19
19
  decodeNativeToolResult,
20
20
  } from "./native-relay.mjs";
21
21
  import { IS_WINDOWS, PLATFORM_LABEL } from "./platform.mjs";
22
+ import { exitForVersionRequest } from "./cli-version.mjs";
22
23
 
23
- const VERSION = "1.13.2";
24
+ exitForVersionRequest(import.meta.url);
25
+
26
+ const VERSION = "1.13.4";
24
27
  const log = (msg) => process.stderr.write(`[native-relay] ${msg}\n`);
25
28
 
26
29
  function errorResponse(code, message) {