@minhspark/codex-mcp-bridge 1.25.1 → 1.25.3
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/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +40 -0
- package/README.md +40 -16
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/scripts/install-antigravity-bridge.mjs +40 -10
- package/scripts/install-claude-code.mjs +2 -2
- package/scripts/install-claude-desktop.mjs +12 -7
- package/scripts/install-native-relay.mjs +2 -2
- package/scripts/setup.mjs +3 -3
- package/src/antigravity-bridge.mjs +79 -25
- package/src/antigravity-deliveries.mjs +32 -8
- package/src/antigravity-ipc.mjs +18 -9
- package/src/antigravity-outbound-bridge.mjs +46 -19
- package/src/antigravity-outbound-targets.mjs +41 -6
- package/src/bridge-readiness.mjs +2 -2
- package/src/claude-bridge.mjs +38 -23
- package/src/claude-code-registration.mjs +2 -1
- package/src/claude-session-router.mjs +8 -15
- package/src/index.mjs +35 -24
- package/src/mcp-supervisor.mjs +91 -10
- package/src/native-relay-companion.mjs +23 -3
- package/src/native-relay.mjs +8 -4
- package/src/peer-protocol.mjs +9 -6
- package/src/platform.mjs +1 -0
- package/src/recipient-preflight.mjs +81 -0
- package/src/relay-wake.mjs +15 -218
- package/src/setup.mjs +38 -18
- package/src/thread-delivery.mjs +28 -21
package/src/relay-wake.mjs
CHANGED
|
@@ -1,22 +1,6 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
1
|
import net from "node:net";
|
|
3
|
-
import path from "node:path";
|
|
4
2
|
|
|
5
|
-
import { codexHome, resolveRelayThreadId } from "./native-relay.mjs";
|
|
6
|
-
import { isCodexDesktopRunning, openThreadInCodexApp } from "./platform.mjs";
|
|
7
|
-
import { captureProjectScope } from "./project-scope.mjs";
|
|
8
|
-
import { readRolloutFirstRecord } from "./rollout-reader.mjs";
|
|
9
|
-
|
|
10
|
-
const THREAD_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
11
|
-
const ROLLOUT = /^rollout-.+-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl(?:\.zst)?$/i;
|
|
12
3
|
export const RELAY_WAKE_WAIT_MS = 20000;
|
|
13
|
-
export const RELAY_WAKE_COOLDOWN_MS = 60000;
|
|
14
|
-
export const RELAY_KEEPER_INTERVAL_MS = 30000;
|
|
15
|
-
export const RELAY_KEEPER_WAIT_MS = 60000;
|
|
16
|
-
export const RELAY_KEEPER_MAX_BACKOFF_MS = 30 * 60000;
|
|
17
|
-
export const DESKTOP_RECHECK_MS = 60000;
|
|
18
|
-
const MAX_DAY_DIRECTORIES = 14;
|
|
19
|
-
const MAX_CANDIDATES = 40;
|
|
20
4
|
|
|
21
5
|
export function relayWakeEnabled(env = process.env, platform = process.platform) {
|
|
22
6
|
const value = (env.CODEX_BRIDGE_RELAY_WAKE ?? "").toLowerCase();
|
|
@@ -24,71 +8,14 @@ export function relayWakeEnabled(env = process.env, platform = process.platform)
|
|
|
24
8
|
return platform === "darwin" || platform === "win32";
|
|
25
9
|
}
|
|
26
10
|
|
|
27
|
-
function dayDirectories(sessions) {
|
|
28
|
-
const directories = [];
|
|
29
|
-
const children = (directory, pattern) => {
|
|
30
|
-
try { return fs.readdirSync(directory, { withFileTypes: true }).filter((entry) => entry.isDirectory() && pattern.test(entry.name)).map((entry) => entry.name).sort().reverse(); }
|
|
31
|
-
catch { return []; }
|
|
32
|
-
};
|
|
33
|
-
for (const year of children(sessions, /^\d{4}$/)) {
|
|
34
|
-
for (const month of children(path.join(sessions, year), /^\d{2}$/)) {
|
|
35
|
-
for (const day of children(path.join(sessions, year, month), /^\d{2}$/)) {
|
|
36
|
-
directories.push(path.join(sessions, year, month, day));
|
|
37
|
-
if (directories.length >= MAX_DAY_DIRECTORIES) return directories;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
return directories;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function sameProject(senderCwd, taskCwd) {
|
|
45
|
-
try { captureProjectScope(senderCwd, taskCwd); return true; }
|
|
46
|
-
catch { return false; }
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function relayKeeperEnabled(env = process.env, platform = process.platform) {
|
|
50
|
-
const value = (env.CODEX_BRIDGE_RELAY_KEEPER ?? "").toLowerCase();
|
|
51
|
-
if (["0", "off", "false"].includes(value)) return false;
|
|
52
|
-
if (!relayWakeEnabled(env, platform)) return false;
|
|
53
|
-
if (platform === "win32") return ["1", "on", "true"].includes(value);
|
|
54
|
-
return true;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function relayWakeStateFile(env = process.env) {
|
|
58
|
-
return path.join(codexHome(env), "bridge-relay-wake.json");
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export async function recentDesktopTaskFor(cwd, { env = process.env, sessions = path.join(codexHome(env), "sessions"), exclude = [], anyProject = false } = {}) {
|
|
62
|
-
if (!anyProject && (typeof cwd !== "string" || !cwd)) return null;
|
|
63
|
-
const excluded = new Set(exclude.filter(Boolean));
|
|
64
|
-
const candidates = [];
|
|
65
|
-
for (const directory of dayDirectories(sessions)) {
|
|
66
|
-
let entries;
|
|
67
|
-
try { entries = fs.readdirSync(directory, { withFileTypes: true }); } catch { continue; }
|
|
68
|
-
for (const entry of entries) {
|
|
69
|
-
const match = entry.isFile() ? ROLLOUT.exec(entry.name) : null;
|
|
70
|
-
if (!match || excluded.has(match[1])) continue;
|
|
71
|
-
const file = path.join(directory, entry.name);
|
|
72
|
-
try { candidates.push({ file, threadId: match[1], mtimeMs: fs.lstatSync(file).mtimeMs }); } catch {}
|
|
73
|
-
}
|
|
74
|
-
if (candidates.length >= MAX_CANDIDATES) break;
|
|
75
|
-
}
|
|
76
|
-
candidates.sort((left, right) => right.mtimeMs - left.mtimeMs);
|
|
77
|
-
for (const candidate of candidates.slice(0, MAX_CANDIDATES)) {
|
|
78
|
-
let record;
|
|
79
|
-
try { record = await readRolloutFirstRecord(candidate.file, { maxBytes: 256 * 1024 }); } catch { continue; }
|
|
80
|
-
const meta = record?.type === "session_meta" ? record.payload : null;
|
|
81
|
-
if (meta?.id !== candidate.threadId || meta.originator !== "Codex Desktop" || typeof meta.cwd !== "string") continue;
|
|
82
|
-
if (anyProject || sameProject(cwd, meta.cwd)) return candidate.threadId;
|
|
83
|
-
}
|
|
84
|
-
return null;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
11
|
export function endpointAnswers(socketPath, { timeoutMs = 1000 } = {}) {
|
|
88
12
|
if (typeof socketPath !== "string" || !socketPath) return Promise.resolve(false);
|
|
89
13
|
return new Promise((resolve) => {
|
|
90
14
|
const probe = net.connect({ path: socketPath });
|
|
15
|
+
let settled = false;
|
|
91
16
|
const done = (answer) => {
|
|
17
|
+
if (settled) return;
|
|
18
|
+
settled = true;
|
|
92
19
|
globalThis.clearTimeout(timer);
|
|
93
20
|
probe.destroy();
|
|
94
21
|
resolve(answer);
|
|
@@ -96,163 +23,33 @@ export function endpointAnswers(socketPath, { timeoutMs = 1000 } = {}) {
|
|
|
96
23
|
const timer = globalThis.setTimeout(() => done(false), timeoutMs);
|
|
97
24
|
probe.once("connect", () => done(true));
|
|
98
25
|
probe.once("error", () => done(false));
|
|
26
|
+
probe.once("close", () => done(false));
|
|
99
27
|
});
|
|
100
28
|
}
|
|
101
29
|
|
|
102
|
-
function readWakeState(file) {
|
|
103
|
-
if (!file) return {};
|
|
104
|
-
try {
|
|
105
|
-
const state = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
106
|
-
return state && typeof state === "object" && !Array.isArray(state) ? state : {};
|
|
107
|
-
} catch { return {}; }
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function writeWakeState(file, state) {
|
|
111
|
-
if (!file) return;
|
|
112
|
-
const temporary = `${file}.${process.pid}.tmp`;
|
|
113
|
-
try {
|
|
114
|
-
fs.writeFileSync(temporary, JSON.stringify(state));
|
|
115
|
-
fs.renameSync(temporary, file);
|
|
116
|
-
} catch { try { fs.rmSync(temporary, { force: true }); } catch {} }
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function acquireWakeLock(file, now, staleMs) {
|
|
120
|
-
if (!file) return () => {};
|
|
121
|
-
const lock = `${file}.lock`;
|
|
122
|
-
const claim = () => {
|
|
123
|
-
const descriptor = fs.openSync(lock, "wx");
|
|
124
|
-
try { fs.writeSync(descriptor, JSON.stringify({ pid: process.pid, at: now() })); } finally { fs.closeSync(descriptor); }
|
|
125
|
-
return () => { try { fs.rmSync(lock, { force: true }); } catch {} };
|
|
126
|
-
};
|
|
127
|
-
try { return claim(); }
|
|
128
|
-
catch (error) { if (error.code !== "EEXIST") return null; }
|
|
129
|
-
let heldSince;
|
|
130
|
-
try { heldSince = JSON.parse(fs.readFileSync(lock, "utf8")).at; } catch { heldSince = 0; }
|
|
131
|
-
if (Number.isFinite(heldSince) && now() - heldSince < staleMs) return null;
|
|
132
|
-
try { fs.rmSync(lock, { force: true }); return claim(); } catch { return null; }
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export function keeperBackoffMs(failures, { baseMs = RELAY_WAKE_COOLDOWN_MS, maxMs = RELAY_KEEPER_MAX_BACKOFF_MS } = {}) {
|
|
136
|
-
const count = Number.isInteger(failures) && failures > 0 ? Math.min(failures, 16) : 0;
|
|
137
|
-
return count === 0 ? 0 : Math.min(baseMs * 2 ** (count - 1), maxMs);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
30
|
export function createRelayWake({
|
|
141
31
|
env = process.env,
|
|
142
32
|
enabled = relayWakeEnabled(env),
|
|
143
|
-
desktopRunning = () => isCodexDesktopRunning(),
|
|
144
|
-
senderCwd = () => null,
|
|
145
|
-
findTask = (cwd, exclude) => recentDesktopTaskFor(cwd, { env, exclude }),
|
|
146
|
-
findAnyTask = (exclude) => recentDesktopTaskFor(null, { env, exclude, anyProject: true }),
|
|
147
|
-
executorThreadId = () => { try { return resolveRelayThreadId(env).threadId; } catch { return null; } },
|
|
148
|
-
open = (threadId) => openThreadInCodexApp(threadId, { activate: false }),
|
|
149
33
|
answers = endpointAnswers,
|
|
150
34
|
waitMs = RELAY_WAKE_WAIT_MS,
|
|
151
|
-
backgroundWaitMs = RELAY_KEEPER_WAIT_MS,
|
|
152
|
-
cooldownMs = RELAY_WAKE_COOLDOWN_MS,
|
|
153
|
-
desktopRecheckMs = DESKTOP_RECHECK_MS,
|
|
154
|
-
stateFile = null,
|
|
155
35
|
pollMs = 250,
|
|
156
36
|
now = Date.now,
|
|
157
37
|
sleep = (ms) => new Promise((resolve) => globalThis.setTimeout(resolve, ms)),
|
|
158
38
|
log = () => {},
|
|
159
39
|
} = {}) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
40
|
+
return async ({ socketPath, deadline }) => {
|
|
41
|
+
if (!enabled || typeof socketPath !== "string" || !socketPath) return null;
|
|
42
|
+
const until = Math.min(now() + waitMs, deadline ?? Infinity);
|
|
163
43
|
while (now() < until) {
|
|
164
|
-
if (await answers(socketPath))
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
};
|
|
169
|
-
const attempt = async ({ socketPath, threadId, deadline, background = false }) => {
|
|
170
|
-
if (!enabled) return null;
|
|
171
|
-
const state = readWakeState(stateFile);
|
|
172
|
-
if (background) {
|
|
173
|
-
if (state.desktopRunning === false && now() - (state.desktopCheckedAt ?? -Infinity) < desktopRecheckMs) return null;
|
|
174
|
-
if (now() - (state.attemptAt ?? -Infinity) < Math.max(keeperBackoffMs(state.failures), cooldownMs)) return null;
|
|
175
|
-
} else if (now() - failedAt < cooldownMs) return null;
|
|
176
|
-
const running = await desktopRunning();
|
|
177
|
-
if (stateFile) writeWakeState(stateFile, { ...readWakeState(stateFile), desktopCheckedAt: now(), desktopRunning: Boolean(running) });
|
|
178
|
-
if (!running) return null;
|
|
179
|
-
const wait = background ? backgroundWaitMs : waitMs;
|
|
180
|
-
const release = acquireWakeLock(stateFile, now, wait + 15000);
|
|
181
|
-
if (!release) {
|
|
182
|
-
if (background) return null;
|
|
183
|
-
return await waitFor(socketPath, Math.min(now() + wait, deadline ?? Infinity)) ? { threadId: null } : null;
|
|
184
|
-
}
|
|
185
|
-
try {
|
|
186
|
-
const executor = executorThreadId();
|
|
187
|
-
const target = typeof threadId === "string" && THREAD_ID.test(threadId) && threadId !== executor
|
|
188
|
-
? threadId : (await findTask(senderCwd(), [executor])) ?? (await findAnyTask([executor]));
|
|
189
|
-
const startedAt = now();
|
|
190
|
-
const failures = Number.isInteger(state.failures) ? state.failures : 0;
|
|
191
|
-
if (!target) {
|
|
192
|
-
if (background && stateFile) writeWakeState(stateFile, { ...readWakeState(stateFile), attemptAt: startedAt, failures: failures + 1 });
|
|
193
|
-
return null;
|
|
44
|
+
if (await answers(socketPath, { timeoutMs: Math.min(1000, until - now()) })) {
|
|
45
|
+
if (now() >= until) return null;
|
|
46
|
+
log("relay transport available; the original operation will verify native dispatch");
|
|
47
|
+
return { threadId: null };
|
|
194
48
|
}
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
if (stateFile) writeWakeState(stateFile, { ...readWakeState(stateFile), attemptAt: startedAt, failures: failures + 1 });
|
|
198
|
-
log(message);
|
|
199
|
-
return null;
|
|
200
|
-
};
|
|
201
|
-
try { await open(target); }
|
|
202
|
-
catch (error) { return fail(`could not open Codex task ${target} to start the native relay: ${error.message}`); }
|
|
203
|
-
if (await waitFor(socketPath, Math.min(now() + wait, deadline ?? Infinity))) {
|
|
204
|
-
if (stateFile) writeWakeState(stateFile, { ...readWakeState(stateFile), attemptAt: startedAt, failures: 0 });
|
|
205
|
-
log(`started the Codex Desktop native relay by opening task ${target} in the background`);
|
|
206
|
-
return { threadId: target };
|
|
207
|
-
}
|
|
208
|
-
return fail(`opened Codex task ${target}, but the native relay did not start within ${wait} ms`);
|
|
209
|
-
} finally {
|
|
210
|
-
release();
|
|
49
|
+
const remaining = until - now();
|
|
50
|
+
if (remaining > 0) await sleep(Math.min(pollMs, remaining));
|
|
211
51
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
if (!inFlight) return inFlight = attempt(request).finally(() => { inFlight = null; });
|
|
215
|
-
if (request.background || !request.socketPath) return inFlight;
|
|
216
|
-
const until = Math.min(now() + waitMs, request.deadline ?? Infinity);
|
|
217
|
-
return Promise.race([inFlight, waitFor(request.socketPath, until).then((ready) => ready ? { threadId: null } : null)]);
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
export function startRelayKeeper({
|
|
222
|
-
wake,
|
|
223
|
-
socketPath,
|
|
224
|
-
answers = endpointAnswers,
|
|
225
|
-
intervalMs = RELAY_KEEPER_INTERVAL_MS,
|
|
226
|
-
initialDelayMs = 3000,
|
|
227
|
-
timers = globalThis,
|
|
228
|
-
log = () => {},
|
|
229
|
-
}) {
|
|
230
|
-
let busy = false;
|
|
231
|
-
let stopped = false;
|
|
232
|
-
const tick = async () => {
|
|
233
|
-
if (busy || stopped) return null;
|
|
234
|
-
busy = true;
|
|
235
|
-
try {
|
|
236
|
-
const target = socketPath();
|
|
237
|
-
if (await answers(target)) return null;
|
|
238
|
-
return await wake({ socketPath: target, background: true });
|
|
239
|
-
} catch (error) {
|
|
240
|
-
log(`relay keeper check failed: ${error.message}`);
|
|
241
|
-
return null;
|
|
242
|
-
} finally {
|
|
243
|
-
busy = false;
|
|
244
|
-
}
|
|
245
|
-
};
|
|
246
|
-
const first = timers.setTimeout(tick, initialDelayMs);
|
|
247
|
-
const repeat = timers.setInterval(tick, intervalMs);
|
|
248
|
-
first?.unref?.();
|
|
249
|
-
repeat?.unref?.();
|
|
250
|
-
return {
|
|
251
|
-
tick,
|
|
252
|
-
stop() {
|
|
253
|
-
stopped = true;
|
|
254
|
-
timers.clearTimeout(first);
|
|
255
|
-
timers.clearInterval(repeat);
|
|
256
|
-
},
|
|
52
|
+
log("native relay recovery timed out; no task or window was opened");
|
|
53
|
+
return null;
|
|
257
54
|
};
|
|
258
55
|
}
|
package/src/setup.mjs
CHANGED
|
@@ -89,7 +89,7 @@ export function setupEnvironment(env, paths, codex) {
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
function configurationCheck(label, check, components) {
|
|
92
|
-
try { check(); components.push({ name: label, status: "configured" }); }
|
|
92
|
+
try { const details = check(); components.push({ name: label, status: "configured", ...details }); }
|
|
93
93
|
catch (error) { components.push({ name: label, status: "incomplete", detail: error.message }); }
|
|
94
94
|
}
|
|
95
95
|
|
|
@@ -99,8 +99,9 @@ function assertEntry(entry, worker, root, { caller } = {}) {
|
|
|
99
99
|
const expected = worker === "index.mjs" || worker === "claude-bridge.mjs" || worker === "native-relay-companion.mjs"
|
|
100
100
|
? [path.join(root, "src", "mcp-supervisor.mjs"), worker] : [path.join(root, "src", worker)];
|
|
101
101
|
if (JSON.stringify(args) !== JSON.stringify(expected)) throw new Error("MCP entry points to a different installation or unsupported launcher");
|
|
102
|
-
if (entry.enabled === false || entry.disabled === true) throw new Error("MCP entry is disabled; the saved choice was preserved");
|
|
103
102
|
if (caller && entry.env?.ANTIGRAVITY_BRIDGE_CALLER !== caller) throw new Error("Antigravity caller registration is missing or conflicting");
|
|
103
|
+
if (entry.enabled === false || entry.disabled === true) return { deliveryEnabled: false,
|
|
104
|
+
notices: ["This MCP registration is intentionally disabled. Its saved choice was preserved; delivery through this connector is disabled."] };
|
|
104
105
|
}
|
|
105
106
|
|
|
106
107
|
export function checkSetup({ env = process.env, platform = process.platform, root = ROOT, run = execute, resolve = resolveCodexBin } = {}) {
|
|
@@ -119,8 +120,9 @@ export function checkSetup({ env = process.env, platform = process.platform, roo
|
|
|
119
120
|
const item = inventory?.find((server) => server.name === name);
|
|
120
121
|
if (!item) throw new Error("MCP registration is missing");
|
|
121
122
|
const entry = JSON.parse(run(codex, ["mcp", "get", name, "--json"], setupEnvironment(env, paths, codex), 30000));
|
|
122
|
-
assertEntry({ ...entry.transport, enabled: entry.enabled }, worker, root, name === "antigravity-bridge" ? { caller: "codex" } : {});
|
|
123
|
+
const details = assertEntry({ ...entry.transport, enabled: entry.enabled }, worker, root, name === "antigravity-bridge" ? { caller: "codex" } : {});
|
|
123
124
|
if (name === "codex-native-relay" && !entry.transport.env_vars?.includes("CODEX_APP_TOOLS_PIPE_PATH")) throw new Error("Native pipe forwarding is missing");
|
|
125
|
+
return details;
|
|
124
126
|
}, components);
|
|
125
127
|
}
|
|
126
128
|
for (const [label, file, name, worker, caller] of [
|
|
@@ -130,14 +132,26 @@ export function checkSetup({ env = process.env, platform = process.platform, roo
|
|
|
130
132
|
["Antigravity / agent-bridge", path.join(paths.gemini, "mcp_config.json"), "agent-bridge", "antigravity-outbound-bridge.mjs"],
|
|
131
133
|
]) configurationCheck(label, () => {
|
|
132
134
|
const entry = readJson(file)?.mcpServers?.[name];
|
|
133
|
-
assertEntry(entry, worker, root, { caller });
|
|
135
|
+
const details = assertEntry(entry, worker, root, { caller });
|
|
136
|
+
if (worker === "index.mjs" && entry.env?.CODEX_BRIDGE_OPEN_IN_APP === "1") {
|
|
137
|
+
throw new Error("Managed bridge settings still allow opening Codex Desktop. Run codex-bridge-setup to apply the current silent configuration.");
|
|
138
|
+
}
|
|
134
139
|
if (name === "codex-bridge-desktop" && entry.env?.CODEX_BRIDGE_DESKTOP_TASKS === "1") {
|
|
135
|
-
|
|
140
|
+
return { ...details, nativeCodeOnly: true, sharedChatDelivery: "unsupported",
|
|
141
|
+
notices: [...(details?.notices ?? []), "The saved native Code-only policy was preserved. Ordinary Claude Desktop Chat cannot deliver through this connector; native delivery uses the registered Claude Desktop Code connector and remains unverified by setup."] };
|
|
136
142
|
}
|
|
143
|
+
return details;
|
|
144
|
+
}, components);
|
|
145
|
+
configurationCheck("Claude Desktop / background preference", () => {
|
|
146
|
+
if (readJson(paths.desktop)?.preferences?.menuBarEnabled !== true) throw new Error("The managed Claude background preference is not enabled. Run codex-bridge-setup to apply it; live activation is verified separately.");
|
|
137
147
|
}, components);
|
|
138
148
|
configurationCheck("Antigravity / codex-bridge sidecar", () => {
|
|
139
|
-
assertEntry(readJson(path.join(paths.gemini, "sidecars", "codex-bridge", "sidecar.json")), "antigravity-sidecar.mjs", root);
|
|
140
|
-
|
|
149
|
+
const details = assertEntry(readJson(path.join(paths.gemini, "sidecars", "codex-bridge", "sidecar.json")), "antigravity-sidecar.mjs", root);
|
|
150
|
+
const enabled = readJson(path.join(paths.gemini, "config.json"))?.sidecars?.["codex-bridge"]?.enabled;
|
|
151
|
+
if (typeof enabled !== "boolean") throw new Error("Antigravity sidecar enabled setting is missing or invalid");
|
|
152
|
+
if (!enabled) return { deliveryEnabled: false,
|
|
153
|
+
notices: [...(details?.notices ?? []), "The Antigravity bridge sidecar is intentionally disabled. Its saved choice was preserved; native Desktop delivery through this sidecar is disabled."] };
|
|
154
|
+
return details;
|
|
141
155
|
}, components);
|
|
142
156
|
if (native) configurationCheck("Native relay executor", () => {
|
|
143
157
|
const relay = readJson(path.join(paths.codex, "native-relay.json"));
|
|
@@ -151,7 +165,7 @@ export function checkSetup({ env = process.env, platform = process.platform, roo
|
|
|
151
165
|
|
|
152
166
|
export function runSetup({ args = [], env = process.env, platform = process.platform, root = ROOT, run = execute, resolve = resolveCodexBin } = {}) {
|
|
153
167
|
const options = setupArguments(args);
|
|
154
|
-
if (options.help) return { help: "Usage: codex-bridge-setup [--check|--dry-run] [--json] [--no-download]\nGlobal npm installation configures Claude Desktop, Claude Code, Codex and Antigravity automatically. CODEX_BRIDGE_SKIP_SETUP=1 opts out." };
|
|
168
|
+
if (options.help) return { help: "Usage: codex-bridge-setup [--check|--dry-run] [--json] [--no-download]\nUpdate the package and apply managed settings with: npm install -g @minhspark/codex-mcp-bridge@latest --allow-scripts=@minhspark/codex-mcp-bridge\nGlobal npm installation configures Claude Desktop, Claude Code, Codex and Antigravity automatically. CODEX_BRIDGE_SKIP_SETUP=1 opts out. Saved configuration and loaded app connections are verified separately." };
|
|
155
169
|
if (options.postinstall && !automaticSetupEnabled(env)) return { skipped: true, reason: "Automatic configuration runs only for a global npm install; use codex-bridge-setup for manual setup." };
|
|
156
170
|
if (options.check) return checkSetup({ env, platform, root, run, resolve });
|
|
157
171
|
const native = platform === "win32" || platform === "darwin";
|
|
@@ -166,16 +180,18 @@ export function runSetup({ args = [], env = process.env, platform = process.plat
|
|
|
166
180
|
const install = (name, script, arguments_, environment = base) => {
|
|
167
181
|
try {
|
|
168
182
|
const output = run(process.execPath, [path.join(root, "scripts", script), ...arguments_], environment, 180000);
|
|
169
|
-
let
|
|
170
|
-
try {
|
|
171
|
-
|
|
183
|
+
let details = {};
|
|
184
|
+
try { details = JSON.parse(output); } catch {}
|
|
185
|
+
const notices = details.notices ?? [];
|
|
186
|
+
components.push({ name, status: "configured", ...(notices.length ? { notices } : {}),
|
|
187
|
+
...(details.backgroundPreference ? { backgroundPreference: details.backgroundPreference } : {}) });
|
|
172
188
|
return true;
|
|
173
189
|
} catch (error) { components.push({ name, status: "incomplete", detail: error.message }); return false; }
|
|
174
190
|
};
|
|
175
|
-
const code = install(planned[0], "install-claude-code.mjs", ["--install", "--desktop-config", paths.desktop], {
|
|
191
|
+
const code = install(planned[0], "install-claude-code.mjs", ["--install", "--setup", "--desktop-config", paths.desktop], {
|
|
176
192
|
...base, CODEX_BRIDGE_DESKTOP_TASKS: native ? "1" : "0", CODEX_BRIDGE_AUTOSTART: native ? "0" : "1", CODEX_BRIDGE_THREAD_POLICY: "roots",
|
|
177
193
|
});
|
|
178
|
-
if (code) install(planned[1], "install-claude-desktop.mjs", ["--entry-name", "codex-bridge-desktop", "--quiet"]);
|
|
194
|
+
if (code) install(planned[1], "install-claude-desktop.mjs", ["--entry-name", "codex-bridge-desktop", "--quiet", "--setup"]);
|
|
179
195
|
if (native) {
|
|
180
196
|
const relay = readJson(path.join(paths.codex, "native-relay.json"));
|
|
181
197
|
install("Codex / codex-native-relay and executor", "install-native-relay.mjs", ["--setup", ...(relay?.desktopTasks === undefined ? ["--desktop-tasks"] : [])]);
|
|
@@ -185,20 +201,24 @@ export function runSetup({ args = [], env = process.env, platform = process.plat
|
|
|
185
201
|
const inspection = checkSetup({ env: base, platform, root, run, resolve: () => runtime.binary });
|
|
186
202
|
return { configured: components.every((item) => item.status === "configured") && inspection.configured, liveVerified: false,
|
|
187
203
|
cliInstalled: runtime.installed, components, verification: inspection.components,
|
|
188
|
-
nextActions: ["
|
|
189
|
-
"
|
|
190
|
-
"
|
|
204
|
+
nextActions: ["Running MCP processes may still hold earlier settings. Setup does not start apps or open conversations.",
|
|
205
|
+
"Existing compatible supervisors reload installed source when idle; configuration changes are not proof of loaded-process readiness.",
|
|
206
|
+
"Native delivery uses the same saved project and signed-in accounts in Claude Desktop Code and Codex Desktop.",
|
|
207
|
+
"codex_bridge_status, native_relay_status and agent_bridge_status report live readiness separately from this configuration check."] };
|
|
191
208
|
}
|
|
192
209
|
|
|
193
210
|
export function printSetup(result, { json = false, output = console.log } = {}) {
|
|
194
211
|
if (json) { output(JSON.stringify(result, null, 2)); return; }
|
|
195
212
|
if (result.help) { output(result.help); return; }
|
|
196
213
|
if (result.skipped) { output(result.reason); return; }
|
|
197
|
-
output(result.dryRun ? "Bridge setup preview (no changes)" : result.configured ? "Bridge configuration verified.
|
|
214
|
+
output(result.dryRun ? "Bridge setup preview (no changes)" : result.configured ? "Bridge configuration verified. Running app connections have not been verified." : "Bridge setup is incomplete. Fix the items below, then run codex-bridge-setup again.");
|
|
198
215
|
for (const item of result.components ?? []) {
|
|
199
216
|
output(`[${item.status}] ${item.name}${item.detail ? `: ${item.detail}` : ""}`);
|
|
200
217
|
for (const notice of item.notices ?? []) output(` ${notice}`);
|
|
201
218
|
}
|
|
202
|
-
for (const item of result.verification ?? [])
|
|
219
|
+
for (const item of result.verification ?? []) {
|
|
220
|
+
if (item.status !== "configured") output(`[${item.status}] ${item.name}: ${item.detail}`);
|
|
221
|
+
for (const notice of item.notices ?? []) output(`${item.name}: ${notice}`);
|
|
222
|
+
}
|
|
203
223
|
for (const action of result.nextActions ?? []) output(action);
|
|
204
224
|
}
|
package/src/thread-delivery.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import { captureCodexRolloutWatermark, inspectCodexNativeTurn, readCodexNativeTu
|
|
|
9
9
|
import { captureProjectScope, recheckProjectScope } from "./project-scope.mjs";
|
|
10
10
|
import { isRolloutChangedDuringRead } from "./rollout-segments.mjs";
|
|
11
11
|
import { requireClaudeSenderContext } from "./claude-sender-context.mjs";
|
|
12
|
+
import { bridgeDiagnostic } from "./recipient-preflight.mjs";
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Which backend puts a message into a Codex thread.
|
|
@@ -74,7 +75,7 @@ export function matchDesktopProject(projects, cwd, { canonicalize = realpathSync
|
|
|
74
75
|
return matches[0];
|
|
75
76
|
}
|
|
76
77
|
|
|
77
|
-
export const RELAY_DOWN_GUIDANCE = "The Codex Desktop native relay is not listening
|
|
78
|
+
export const RELAY_DOWN_GUIDANCE = "The Codex Desktop native relay is not listening after waiting for silent recovery. Its Desktop-owned supervisor restarts crashed relay workers without replaying requests. An unloaded native host cannot be cold-started by this bridge. Inspect codex_bridge_status and include its diagnostic in a report. Never open an app or an old, new, empty or failed chat to load a relay.";
|
|
78
79
|
|
|
79
80
|
function relayUnreachable(error) {
|
|
80
81
|
return error?.code === "RELAY_UNREACHABLE" && error.sent === false;
|
|
@@ -126,23 +127,25 @@ export class DesktopTaskDelivery {
|
|
|
126
127
|
if (scopeBinding) this.security.hardenedRoots.recheck(scopeBinding);
|
|
127
128
|
const projectScope = expectedScope ?? await this.mutationProjectScope(operation, args, { deadline });
|
|
128
129
|
if (expectedScope) recheckProjectScope(expectedScope, this.senderProjectScope(expectedScope.recipient.input));
|
|
129
|
-
const remaining = deadline === undefined ? undefined : deadline - this.now();
|
|
130
|
-
if (remaining !== undefined && remaining <= 0) throw Object.assign(new Error("The bridge's response deadline has elapsed; this operation was not sent"), { code: "DESKTOP_RESPONSE_DEADLINE" });
|
|
131
130
|
const accountContext = this.accountContext?.();
|
|
132
|
-
const send = () =>
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
131
|
+
const send = () => {
|
|
132
|
+
const remaining = deadline === undefined ? undefined : deadline - this.now();
|
|
133
|
+
if (remaining !== undefined && remaining <= 0) throw Object.assign(new Error("The bridge's response deadline has elapsed; this operation was not sent"), { code: "DESKTOP_RESPONSE_DEADLINE" });
|
|
134
|
+
return this.relay.requestDesktop(operation, args, {
|
|
135
|
+
...(remaining === undefined ? {} : { timeoutMs: remaining }),
|
|
136
|
+
...(this.beforeRequest || projectScope || scopeBinding ? { beforeSend: async () => {
|
|
137
|
+
await this.beforeRequest?.({ operation, args, phase: "write" });
|
|
138
|
+
if (projectScope) {
|
|
139
|
+
recheckProjectScope(projectScope);
|
|
140
|
+
recheckProjectScope(projectScope, await this.mutationProjectScope(operation, args, { deadline }));
|
|
141
|
+
}
|
|
142
|
+
await this.beforeRequest?.({ operation, args, phase: "dispatch" });
|
|
143
|
+
if (scopeBinding) this.security.hardenedRoots.recheck(scopeBinding);
|
|
144
|
+
} } : {}),
|
|
145
|
+
...(accountContext ? { accountContext } : {}),
|
|
146
|
+
...(creationReceiptId ? { creationReceiptId } : {}),
|
|
147
|
+
});
|
|
148
|
+
};
|
|
146
149
|
let response;
|
|
147
150
|
try {
|
|
148
151
|
response = await send();
|
|
@@ -194,13 +197,17 @@ export class DesktopTaskDelivery {
|
|
|
194
197
|
async status() {
|
|
195
198
|
const accountContext = this.accountContext?.();
|
|
196
199
|
const socketPath = this.relay.status?.({ accountContext })?.socketPath ?? this.relay.socketPath;
|
|
197
|
-
if (this.accountContext && !accountContext)
|
|
200
|
+
if (this.accountContext && !accountContext) {
|
|
201
|
+
const reason = "The current Claude and Codex accounts could not both be verified. Finish signing in and inspect the account diagnostics before sending work.";
|
|
202
|
+
return { available: false, socketPath, code: "BRIDGE_ACCOUNT_UNVERIFIED", reason, diagnostic: bridgeDiagnostic(new Error(reason), { component: "codex-bridge", operation: "codex_bridge_status", code: "BRIDGE_ACCOUNT_UNVERIFIED", readOnly: true }) };
|
|
203
|
+
}
|
|
198
204
|
try {
|
|
199
205
|
const response = await this.request("list_projects", {});
|
|
200
206
|
if (!Array.isArray(response?.projects)) throw new Error("Desktop returned no project list");
|
|
201
207
|
return { available: true, socketPath, localProjects: response.projects.filter((project) => project.projectKind === "local" && project.hostId === "local").length };
|
|
202
208
|
} catch (err) {
|
|
203
|
-
|
|
209
|
+
const diagnostic = bridgeDiagnostic(err, { component: "codex-bridge", operation: "codex_bridge_status", readOnly: true });
|
|
210
|
+
return { available: false, socketPath, code: diagnostic.code, reason: err.message, diagnostic };
|
|
204
211
|
}
|
|
205
212
|
}
|
|
206
213
|
|
|
@@ -334,11 +341,11 @@ export class DesktopTaskDelivery {
|
|
|
334
341
|
const confirmedId = response?.threadId ?? response?.conversationId;
|
|
335
342
|
threadId = confirmedId;
|
|
336
343
|
if (!threadId && (response?.status === "outcome-unknown" || response?.firstTurn?.status === "outcome-unknown")) threadId = response?.clientThreadId;
|
|
337
|
-
if (typeof confirmedId !== "string" || !confirmedId.trim() || response?.status === "outcome-unknown" || response.hostId !== "local") throw new Error(
|
|
344
|
+
if (typeof confirmedId !== "string" || !confirmedId.trim() || response?.status === "outcome-unknown" || response.hostId !== "local") throw Object.assign(new Error("Desktop creation is not confirmed; inspect the original native receipt."), { code: "CODEX_CREATION_UNCONFIRMED", deliveryState: "unknown", phase: "creation" });
|
|
338
345
|
await this.receipts.write(identity.key, { ...receipt, state: "known", threadId });
|
|
339
346
|
creationConfirmed = true;
|
|
340
347
|
this.security.registerThread(threadId);
|
|
341
|
-
if (response.firstTurn && response.firstTurn.status !== "accepted") throw new Error(`Desktop created task ${threadId}; its first turn reports ${response.firstTurn.status
|
|
348
|
+
if (response.firstTurn && response.firstTurn.status !== "accepted") throw Object.assign(new Error(`Desktop created task ${threadId}; its first turn reports ${["failed", "waitingOnApproval", "waitingOnUserInput", "outcome-unknown"].includes(response.firstTurn.status) ? response.firstTurn.status : "an unconfirmed state"}. Inspect the original native receipt.`), { code: "CODEX_FIRST_TURN_NOT_ACCEPTED", deliveryState: "sent", phase: "first_turn" });
|
|
342
349
|
const created = { threadId, name, cwd, projectId: project.projectId, projectName: project.label, backend: NATIVE_BACKEND };
|
|
343
350
|
const delivered = { ...created, previousTurnId: null, deliveryStatus: "accepted", responseObservation: {
|
|
344
351
|
operation: "create_thread", threadId, previousTurnId: null, expectedCwd: cwd,
|