@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.
- package/CHANGELOG.md +35 -0
- package/README.md +47 -10
- package/package.json +5 -2
- package/scripts/check-claude-bridge.mjs +2 -1
- package/scripts/install-claude-desktop.mjs +3 -0
- package/scripts/install-codex-mcp.mjs +15 -15
- package/scripts/install-native-relay.mjs +38 -3
- package/scripts/install-npm-footer.mjs +99 -0
- package/scripts/npm-footer.ps1 +140 -0
- package/scripts/npm-footer.sh +81 -0
- package/src/claude-bridge.mjs +69 -19
- package/src/cli-version.mjs +20 -0
- package/src/codex-mcp-registration.mjs +25 -0
- package/src/index.mjs +46 -15
- package/src/native-relay-companion.mjs +4 -1
- package/src/peer-protocol.mjs +78 -11
- package/src/platform.mjs +55 -1
- package/src/runtime-state.mjs +34 -0
- package/src/thread-delivery.mjs +45 -3
|
@@ -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
|
+
}
|
package/src/claude-bridge.mjs
CHANGED
|
@@ -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
|
-
|
|
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) =>
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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}
|
|
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
|
-
`
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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}`,
|
|
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 ? [
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|