@minhspark/codex-mcp-bridge 1.13.8 → 1.15.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.
@@ -0,0 +1,314 @@
1
+ #!/usr/bin/env node
2
+ import { fork } from "node:child_process";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { createReleaseSnapshot, sourceRevision } from "./release-snapshot.mjs";
7
+
8
+ const ENTRIES = new Set(["index.mjs", "claude-bridge.mjs", "native-relay-companion.mjs"]);
9
+ const MAX_FRAME_BYTES = 16 * 1024 * 1024;
10
+ const MAX_QUEUE = 256;
11
+ const hasId = (message) => Object.hasOwn(message, "id");
12
+ const idKey = (id) => JSON.stringify(id);
13
+ const errorMessage = (id, message) => ({ jsonrpc: "2.0", id, error: { code: -32603, message } });
14
+
15
+ function lines(stream, receive, failed) {
16
+ let buffer = Buffer.alloc(0);
17
+ stream.on("data", (chunk) => {
18
+ buffer = Buffer.concat([buffer, chunk]);
19
+ if (buffer.length > MAX_FRAME_BYTES) { failed(new Error("MCP frame exceeds the buffer limit")); return; }
20
+ let index;
21
+ while ((index = buffer.indexOf(10)) !== -1) {
22
+ const frame = buffer.subarray(0, index).toString("utf8").trim();
23
+ buffer = buffer.subarray(index + 1);
24
+ if (!frame) continue;
25
+ try { receive(JSON.parse(frame)); }
26
+ catch (error) { failed(error); }
27
+ }
28
+ });
29
+ }
30
+
31
+ class Worker {
32
+ constructor(release, entry, env, handlers) {
33
+ this.release = release;
34
+ this.handlers = handlers;
35
+ this.rpcPending = new Map();
36
+ this.controlPending = new Map();
37
+ this.clientPending = new Map();
38
+ this.sequence = 0;
39
+ this.retiring = false;
40
+ this.closed = false;
41
+ this.process = fork(path.join(release.directory, "src", entry), [], {
42
+ execPath: process.execPath, execArgv: [], cwd: process.cwd(), windowsHide: true,
43
+ env: { ...env, CODEX_BRIDGE_WORKER: "1", CODEX_BRIDGE_STAGED: "1" },
44
+ stdio: ["pipe", "pipe", "pipe", "ipc"], serialization: "json",
45
+ });
46
+ this.process.stderr.on("data", (data) => process.stderr.write(data));
47
+ this.process.stdin.on("error", (error) => this.fail(error));
48
+ this.process.on("error", (error) => this.fail(error));
49
+ this.process.on("exit", (code, signal) => this.fail(new Error(`Bridge worker exited (${signal ?? code})`)));
50
+ this.process.on("message", (message) => {
51
+ if (message?.type !== "bridge:control-result") return;
52
+ const pending = this.controlPending.get(message.id);
53
+ if (!pending) return;
54
+ this.controlPending.delete(message.id);
55
+ clearTimeout(pending.timer);
56
+ if (message.error) pending.reject(new Error(message.error));
57
+ else pending.resolve(message.result);
58
+ });
59
+ lines(this.process.stdout, (message) => {
60
+ if (!message || message.jsonrpc !== "2.0") throw new Error("Invalid worker JSON-RPC message");
61
+ if (!message.method && hasId(message) && this.rpcPending.has(message.id)) {
62
+ const pending = this.rpcPending.get(message.id);
63
+ this.rpcPending.delete(message.id);
64
+ clearTimeout(pending.timer);
65
+ if (message.error) pending.reject(new Error(message.error.message));
66
+ else pending.resolve(message.result);
67
+ } else handlers.message(this, message);
68
+ }, (error) => this.fail(error));
69
+ }
70
+
71
+ write(message) {
72
+ if (this.closed) throw new Error("The bridge worker is unavailable");
73
+ this.process.stdin.write(`${JSON.stringify(message)}\n`);
74
+ }
75
+
76
+ rpc(method, params, timeout = 15000) {
77
+ const id = `supervisor:${++this.sequence}`;
78
+ return new Promise((resolve, reject) => {
79
+ const timer = setTimeout(() => { this.rpcPending.delete(id); reject(new Error(`Candidate ${method} timed out`)); }, timeout);
80
+ this.rpcPending.set(id, { resolve, reject, timer });
81
+ this.write({ jsonrpc: "2.0", id, method, ...(params === undefined ? {} : { params }) });
82
+ });
83
+ }
84
+
85
+ control(action, data, timeout = 15000) {
86
+ const id = ++this.sequence;
87
+ return new Promise((resolve, reject) => {
88
+ const timer = setTimeout(() => { this.controlPending.delete(id); reject(new Error(`Worker ${action} timed out`)); }, timeout);
89
+ this.controlPending.set(id, { resolve, reject, timer });
90
+ this.process.send({ type: "bridge:control", id, action, data }, (error) => {
91
+ if (!error) return;
92
+ const pending = this.controlPending.get(id);
93
+ if (pending) { clearTimeout(timer); this.controlPending.delete(id); reject(error); }
94
+ });
95
+ });
96
+ }
97
+
98
+ fail(error) {
99
+ if (this.closed) return;
100
+ this.closed = true;
101
+ for (const pending of [...this.rpcPending.values(), ...this.controlPending.values()]) {
102
+ clearTimeout(pending.timer);
103
+ pending.reject(error);
104
+ }
105
+ this.rpcPending.clear();
106
+ this.controlPending.clear();
107
+ this.handlers.exit(this, error);
108
+ }
109
+
110
+ async stop() {
111
+ this.retiring = true;
112
+ if (this.process.exitCode !== null || this.process.signalCode) return;
113
+ await new Promise((resolve) => {
114
+ const timer = setTimeout(() => { this.process.kill(); resolve(); }, 1000);
115
+ this.process.once("exit", () => { clearTimeout(timer); resolve(); });
116
+ this.process.stdin.end();
117
+ if (this.process.connected) this.process.disconnect();
118
+ });
119
+ }
120
+ }
121
+
122
+ export async function runSupervisor(entry, options = {}) {
123
+ if (!ENTRIES.has(entry)) throw new Error("Expected index.mjs, claude-bridge.mjs, or native-relay-companion.mjs");
124
+ const root = fs.realpathSync.native(options.root ?? path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."));
125
+ if (process.argv.includes("--version") || process.argv.includes("-v")) {
126
+ process.stdout.write(`${JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8")).version}\n`);
127
+ return;
128
+ }
129
+ const log = (message) => process.stderr.write(`[bridge-supervisor] ${message}\n`);
130
+ const output = (message) => process.stdout.write(`${JSON.stringify(message)}\n`);
131
+ const env = { ...process.env, CODEX_BRIDGE_SOURCE_ROOT: root };
132
+ if (entry === "native-relay-companion.mjs" && !env.CODEX_APP_TOOLS_PIPE_PATH) {
133
+ const { resolveNativeToolsPipePath } = await import("./native-relay.mjs");
134
+ const pipe = await resolveNativeToolsPipePath();
135
+ if (pipe) env.CODEX_APP_TOOLS_PIPE_PATH = pipe;
136
+ }
137
+ const pollMs = Math.max(100, Number(env.CODEX_BRIDGE_RELOAD_POLL_MS) || 1000);
138
+ const settleMs = Math.max(100, Number(env.CODEX_BRIDGE_RELOAD_SETTLE_MS) || 1500);
139
+ let active;
140
+ let initializeParams;
141
+ let initializeResult;
142
+ let ready = false;
143
+ let switching = false;
144
+ let stopping = false;
145
+ let failure = null;
146
+ let sequence = 0;
147
+ let observed = null;
148
+ let observedAt = 0;
149
+ let retryAt = 0;
150
+ let reason = null;
151
+ let reloads = 0;
152
+ const queue = [];
153
+ const requests = new Map();
154
+ const reverse = new Map();
155
+ const workers = new Set();
156
+ const diagnostics = () => ({ enabled: true, supervisorPid: process.pid, workerPid: active?.process.pid ?? null,
157
+ revision: active?.release.revision ?? null, availableRevision: observed, pending: Boolean(observed && observed !== active?.release.revision),
158
+ state: failure ? "failed" : switching ? "reloading" : reason ? "deferred" : "current", reason: failure ?? reason, reloads });
159
+
160
+ const handlers = {
161
+ message(worker, message) {
162
+ if (message.method && hasId(message)) {
163
+ const id = `bridge-server:${++sequence}`;
164
+ reverse.set(idKey(id), { worker, id: message.id });
165
+ output({ ...message, id });
166
+ return;
167
+ }
168
+ if (message.method) {
169
+ if (worker === active) output(message);
170
+ return;
171
+ }
172
+ const pending = worker.clientPending.get(message.id);
173
+ if (!pending) return;
174
+ worker.clientPending.delete(message.id);
175
+ requests.delete(idKey(pending.id));
176
+ const respond = async () => {
177
+ if (pending.method === "initialize" && !message.error) {
178
+ initializeResult = message.result;
179
+ await worker.control("activate");
180
+ ready = true;
181
+ }
182
+ if (message.result && ["codex_bridge_status", "claude_bridge_status", "native_relay_status"].includes(pending.tool)) {
183
+ message.result.structuredContent = { ...message.result.structuredContent, autoReload: diagnostics() };
184
+ message.result.content = [...(message.result.content ?? []), { type: "text", text: `automatic reload: ${diagnostics().state}\nsupervisor pid: ${process.pid}\nworker pid: ${worker.process.pid}\nreload count: ${reloads}${reason ? `\nreload detail: ${reason}` : ""}` }];
185
+ }
186
+ output({ ...message, id: pending.id });
187
+ };
188
+ void respond().catch((error) => { failure = error.message; output(errorMessage(pending.id, error.message)); });
189
+ },
190
+ exit(worker, error) {
191
+ for (const [id, pending] of worker.clientPending) {
192
+ requests.delete(idKey(pending.id));
193
+ output(errorMessage(pending.id, `${error.message}. The operation may already have been dispatched; it was not retried. Retain the original task and receipts.`));
194
+ worker.clientPending.delete(id);
195
+ }
196
+ for (const [id, pending] of reverse) if (pending.worker === worker) reverse.delete(id);
197
+ if (worker === active && !worker.retiring && !stopping) { failure = `${error.message}; automatic replay is disabled because delivery state may be unknown`; log(failure); }
198
+ },
199
+ };
200
+ const spawn = (release) => {
201
+ if (stopping) throw new Error("The MCP client disconnected before activation");
202
+ const worker = new Worker(release, entry, env, handlers);
203
+ workers.add(worker);
204
+ return worker;
205
+ };
206
+ const check = async () => {
207
+ if (switching || !ready || stopping || failure) return;
208
+ let revision;
209
+ try { revision = sourceRevision(root); }
210
+ catch (error) { reason = `Installation is not ready: ${error.message}`; return; }
211
+ if (observed !== revision) { observed = revision; observedAt = Date.now(); retryAt = 0; }
212
+ if (revision === active.release.revision) { reason = null; return; }
213
+ if (Date.now() - observedAt < settleMs || Date.now() < retryAt) { reason ??= "Waiting for a complete, stable installation"; return; }
214
+ if (active.clientPending.size || reverse.size) { reason = "A request is still running; its worker is retained"; return; }
215
+ switching = true;
216
+ let candidate;
217
+ let quiesced = false;
218
+ const previous = active;
219
+ try {
220
+ const state = await previous.control("inspect");
221
+ if (!state.reloadable) { reason = state.reason ?? "Pending deliveries prevent a safe reload"; return; }
222
+ const release = createReleaseSnapshot(root, { expectedRevision: revision });
223
+ candidate = spawn(release);
224
+ const initialized = await candidate.rpc("initialize", initializeParams);
225
+ if (initialized.protocolVersion !== initializeResult.protocolVersion || JSON.stringify(initialized.capabilities) !== JSON.stringify(initializeResult.capabilities)) {
226
+ throw new Error("The new MCP protocol or capabilities require an explicit client reconnect");
227
+ }
228
+ candidate.write({ jsonrpc: "2.0", method: "notifications/initialized" });
229
+ const catalog = await candidate.rpc("tools/list", {});
230
+ if (!Array.isArray(catalog.tools) || !catalog.tools.length || catalog.nextCursor) throw new Error("The candidate tool catalog is incomplete");
231
+ const oldCatalog = await previous.rpc("tools/list", {});
232
+ if (sourceRevision(root) !== revision) throw new Error("The installation changed before activation");
233
+ quiesced = true;
234
+ const exported = await previous.control("quiesce");
235
+ await candidate.control("restore", exported.state);
236
+ await candidate.control("activate");
237
+ if (stopping) throw new Error("The MCP client disconnected during activation");
238
+ active = candidate;
239
+ candidate = null;
240
+ reloads++;
241
+ reason = null;
242
+ if (JSON.stringify(catalog) !== JSON.stringify(oldCatalog)) output({ jsonrpc: "2.0", method: "notifications/tools/list_changed" });
243
+ await previous.stop();
244
+ workers.delete(previous);
245
+ log(`activated ${entry} revision ${revision.slice(0, 12)}; client connection retained`);
246
+ } catch (error) {
247
+ reason = `Update deferred: ${error.message}`;
248
+ retryAt = Date.now() + Math.max(pollMs, 5000);
249
+ if (candidate) { await candidate.stop(); workers.delete(candidate); }
250
+ if (quiesced && active === previous && !previous.closed) {
251
+ try { await previous.control("resume"); }
252
+ catch (resumeError) { failure = `Previous runtime could not resume: ${resumeError.message}`; }
253
+ }
254
+ log(reason);
255
+ } finally {
256
+ switching = false;
257
+ for (const message of queue.splice(0)) dispatch(message);
258
+ }
259
+ };
260
+ const dispatch = (message) => {
261
+ if (stopping) return;
262
+ if (!message.method && hasId(message)) {
263
+ const pending = reverse.get(idKey(message.id));
264
+ if (pending) { reverse.delete(idKey(message.id)); pending.worker.write({ ...message, id: pending.id }); }
265
+ return;
266
+ }
267
+ if (message.method === "notifications/cancelled") {
268
+ const pending = requests.get(idKey(message.params?.requestId));
269
+ if (pending) pending.worker.write({ ...message, params: { ...message.params, requestId: pending.workerId } });
270
+ else {
271
+ const index = queue.findIndex((queued) => hasId(queued) && idKey(queued.id) === idKey(message.params?.requestId));
272
+ if (index !== -1) { const [cancelled] = queue.splice(index, 1); output({ jsonrpc: "2.0", id: cancelled.id, error: { code: -32800, message: "Request cancelled before dispatch" } }); }
273
+ }
274
+ return;
275
+ }
276
+ if (failure) { if (hasId(message)) output(errorMessage(message.id, failure)); return; }
277
+ if (switching || !active) {
278
+ const bytes = queue.reduce((size, queued) => size + Buffer.byteLength(JSON.stringify(queued)), 0);
279
+ if (queue.length >= MAX_QUEUE || bytes + Buffer.byteLength(JSON.stringify(message)) > MAX_FRAME_BYTES) { if (hasId(message)) output(errorMessage(message.id, "Reload request queue is full; no operation was dispatched")); return; }
280
+ queue.push(message);
281
+ return;
282
+ }
283
+ if (message.method === "initialize") initializeParams = message.params;
284
+ if (hasId(message)) {
285
+ if (requests.has(idKey(message.id))) { output(errorMessage(message.id, "Duplicate active JSON-RPC request ID")); return; }
286
+ const id = `client:${++sequence}`;
287
+ active.clientPending.set(id, { id: message.id, method: message.method, tool: message.params?.name });
288
+ requests.set(idKey(message.id), { worker: active, workerId: id });
289
+ active.write({ ...message, id });
290
+ } else active.write(message);
291
+ };
292
+ const shutdown = async () => {
293
+ if (stopping) return;
294
+ stopping = true;
295
+ clearInterval(timer);
296
+ await Promise.allSettled([...workers].map((worker) => worker.stop()));
297
+ process.stdin.pause();
298
+ };
299
+ const initial = createReleaseSnapshot(root);
300
+ observed = initial.revision;
301
+ active = spawn(initial);
302
+ const timer = setInterval(() => { void check(); }, pollMs);
303
+ lines(process.stdin, (message) => {
304
+ if (!message || message.jsonrpc !== "2.0" || (!message.method && !hasId(message))) throw new Error("Invalid client JSON-RPC message");
305
+ dispatch(message);
306
+ }, (error) => { log(error.message); void shutdown(); });
307
+ process.stdin.on("end", () => { void shutdown(); });
308
+ for (const signal of ["SIGINT", "SIGTERM"]) process.once(signal, () => { void shutdown(); });
309
+ log(`supervising ${entry}; installed updates activate after pending work completes`);
310
+ }
311
+
312
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
313
+ await runSupervisor(process.argv[2]);
314
+ }