@minhspark/codex-mcp-bridge 1.24.0 → 1.25.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.
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +36 -0
- package/README.md +550 -28
- package/assets/README.md +2 -1
- package/assets/agent-pets.svg +47 -1
- package/docs/new-macos-setup.md +85 -37
- package/docs/new-windows-setup.md +101 -42
- package/docs/project-onboarding.md +64 -9
- package/package.json +5 -2
- package/plugin.json +1 -1
- package/scripts/install-antigravity-bridge.mjs +31 -17
- package/scripts/install-claude-code.mjs +58 -43
- package/scripts/install-claude-desktop.mjs +60 -32
- package/scripts/install-codex-mcp.mjs +1 -1
- package/scripts/install-native-relay.mjs +3 -2
- package/scripts/setup.mjs +14 -0
- package/src/app-server-client.mjs +2 -1
- package/src/claude-bridge.mjs +18 -11
- package/src/claude-code-registration.mjs +94 -0
- package/src/claude-sender-context.mjs +22 -2
- package/src/codex-bridge-launcher.mjs +8 -1
- package/src/codex-mcp-registration.mjs +1 -0
- package/src/codex-native-response.mjs +6 -2
- package/src/desktop-account-context.mjs +18 -0
- package/src/desktop-read-tools.mjs +3 -2
- package/src/index.mjs +5 -3
- package/src/native-relay-companion.mjs +1 -1
- package/src/peer-protocol.mjs +23 -5
- package/src/platform.mjs +57 -5
- package/src/setup.mjs +204 -0
- package/src/thread-delivery.mjs +35 -1
package/src/peer-protocol.mjs
CHANGED
|
@@ -875,9 +875,7 @@ export class PeerEndpoint {
|
|
|
875
875
|
this.log(`ignored uncorrelated inbound peer message from ${message.fromSocket ?? "?"} in hardened mode`);
|
|
876
876
|
return;
|
|
877
877
|
}
|
|
878
|
-
|
|
879
|
-
const record = { ...message, receivedAt: Date.now(), sequence: ++this.messageSequence };
|
|
880
|
-
this.inbox.push(record);
|
|
878
|
+
const record = { ...message };
|
|
881
879
|
if (key && correlated && pending.targetSocket === record.fromSocket) {
|
|
882
880
|
const sent = this.sentMessages.get(key);
|
|
883
881
|
if (sent) {
|
|
@@ -891,6 +889,13 @@ export class PeerEndpoint {
|
|
|
891
889
|
}
|
|
892
890
|
this.#removePendingReply(record.fromSocket, key);
|
|
893
891
|
}
|
|
892
|
+
this.#publishMessage(record);
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
#publishMessage(record) {
|
|
896
|
+
this.activityRevision += 1;
|
|
897
|
+
Object.assign(record, { receivedAt: Date.now(), sequence: ++this.messageSequence });
|
|
898
|
+
this.inbox.push(record);
|
|
894
899
|
this.log(`inbox <- ${record.fromSocket ?? "?"}: ${record.text.length} characters`);
|
|
895
900
|
for (const listener of [...this.listeners]) {
|
|
896
901
|
try { listener(record); }
|
|
@@ -911,9 +916,22 @@ export class PeerEndpoint {
|
|
|
911
916
|
const outcome = readTranscriptOutcome(sessionId, cwd, msgId);
|
|
912
917
|
if (outcome?.reply) this.#receiveMessage({ ...outcome.reply, fromSocket: pending.targetSocket });
|
|
913
918
|
else if (outcome?.status === "interrupted") {
|
|
914
|
-
this.activityRevision += 1;
|
|
915
919
|
this.deliveryReceipts.set(msgId, { ...outcome, fromSocket: pending.targetSocket, observedAt: Date.now() });
|
|
916
920
|
this.#removePendingReply(pending.targetSocket, msgId);
|
|
921
|
+
const sent = this.sentMessages.get(msgId);
|
|
922
|
+
// A terminal observation is a bridge notification, never a Claude reply.
|
|
923
|
+
// Retain the send's original routing and scope for inbox filtering and
|
|
924
|
+
// forwarding; do not infer either from the currently active task.
|
|
925
|
+
this.#publishMessage({
|
|
926
|
+
kind: "delivery_status", source: "bridge", status: "interrupted",
|
|
927
|
+
msgId: `interrupted:${msgId}`, inReplyTo: msgId, fromSocket: pending.targetSocket,
|
|
928
|
+
replyThreadId: sent?.replyThreadId ?? null, accountContext: sent?.accountContext ?? null,
|
|
929
|
+
cwd: sent?.transcriptSession?.cwd ?? null, senderCwd: sent?.scopeBindings?.sender?.path ?? null,
|
|
930
|
+
scopeBindings: sent?.scopeBindings ?? null,
|
|
931
|
+
outcome: "unknown", waitEnded: true, taskCancelled: false, retrySafe: false,
|
|
932
|
+
reasonCode: outcome.reasonCode, interruptedBy: outcome.interruptedBy, backgroundTaskIds: outcome.backgroundTaskIds,
|
|
933
|
+
text: `[Bridge delivery status: ${outcome.reasonCode}]\nMessage id: ${msgId}\n${outcome.reason}`,
|
|
934
|
+
});
|
|
917
935
|
this.log(`reply observation interrupted for ${msgId}; task outcome remains unknown`);
|
|
918
936
|
}
|
|
919
937
|
}
|
|
@@ -1096,7 +1114,7 @@ export class PeerEndpoint {
|
|
|
1096
1114
|
*/
|
|
1097
1115
|
waitForReply(fromSocket, { timeoutMs = 120000, since = Date.now(), afterSequence = null, msgId } = {}) {
|
|
1098
1116
|
const expectsTranscript = Boolean(msgId && this.sentMessages.get(msgId)?.transcriptSession);
|
|
1099
|
-
const matches = (record) => record.fromSocket === fromSocket
|
|
1117
|
+
const matches = (record) => record.kind !== "delivery_status" && record.fromSocket === fromSocket
|
|
1100
1118
|
&& (!expectsTranscript || (record.source === "transcript" && record.inReplyTo === msgId))
|
|
1101
1119
|
&& (!record.inReplyTo || !msgId || record.inReplyTo === msgId)
|
|
1102
1120
|
&& (afterSequence === null ? record.receivedAt >= since : record.sequence > afterSequence);
|
package/src/platform.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execFile, execFileSync } from "node:child_process";
|
|
2
2
|
import { accessSync, constants, existsSync, readdirSync, realpathSync, statSync } from "node:fs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
3
4
|
import os from "node:os";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import { fileURLToPath } from "node:url";
|
|
@@ -168,16 +169,67 @@ function pathCandidates() {
|
|
|
168
169
|
* ~/.local/bin, ~/.npm-global/bin or Homebrew. Probe every known install
|
|
169
170
|
* location for the running platform before giving up on PATH resolution.
|
|
170
171
|
*/
|
|
171
|
-
export function
|
|
172
|
+
export function resolvePackagedCodexBin({
|
|
173
|
+
platform = process.platform,
|
|
174
|
+
arch = process.arch,
|
|
175
|
+
resolvePackage = (specifier, from) => createRequire(from).resolve(specifier),
|
|
176
|
+
exists = isRunnable,
|
|
177
|
+
} = {}) {
|
|
178
|
+
const packagePlatform = platform === "android" ? "linux" : platform;
|
|
179
|
+
const target = {
|
|
180
|
+
"linux-x64": "x86_64-unknown-linux-musl",
|
|
181
|
+
"linux-arm64": "aarch64-unknown-linux-musl",
|
|
182
|
+
"darwin-x64": "x86_64-apple-darwin",
|
|
183
|
+
"darwin-arm64": "aarch64-apple-darwin",
|
|
184
|
+
"win32-x64": "x86_64-pc-windows-msvc",
|
|
185
|
+
"win32-arm64": "aarch64-pc-windows-msvc",
|
|
186
|
+
}[`${packagePlatform}-${arch}`];
|
|
187
|
+
if (!target) return null;
|
|
188
|
+
|
|
189
|
+
let codexManifest;
|
|
190
|
+
try { codexManifest = resolvePackage("@openai/codex/package.json", import.meta.url); } catch {}
|
|
191
|
+
if (typeof codexManifest !== "string" || !path.isAbsolute(codexManifest)) codexManifest = null;
|
|
192
|
+
const packageName = `@openai/codex-${packagePlatform}-${arch}/package.json`;
|
|
193
|
+
const roots = [];
|
|
194
|
+
for (const anchor of [codexManifest, import.meta.url].filter(Boolean)) {
|
|
195
|
+
try {
|
|
196
|
+
const manifest = resolvePackage(packageName, anchor);
|
|
197
|
+
if (typeof manifest === "string" && path.isAbsolute(manifest)) roots.push(path.dirname(manifest));
|
|
198
|
+
} catch {}
|
|
199
|
+
}
|
|
200
|
+
if (codexManifest) roots.push(path.dirname(codexManifest));
|
|
201
|
+
for (const root of new Set(roots)) {
|
|
202
|
+
const candidate = path.join(root, "vendor", target, "bin", platform === "win32" ? "codex.exe" : "codex");
|
|
203
|
+
if (exists(candidate)) return candidate;
|
|
204
|
+
}
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function resolveCodexBin(explicit, {
|
|
209
|
+
env = process.env,
|
|
210
|
+
platform = process.platform,
|
|
211
|
+
arch = process.arch,
|
|
212
|
+
candidates: discovered,
|
|
213
|
+
runnable = isRunnable,
|
|
214
|
+
packaged = resolvePackagedCodexBin,
|
|
215
|
+
} = {}) {
|
|
172
216
|
const candidates = [
|
|
173
217
|
explicit,
|
|
174
|
-
|
|
175
|
-
...(
|
|
176
|
-
|
|
218
|
+
env.CODEX_BIN,
|
|
219
|
+
...(discovered ?? [
|
|
220
|
+
...(IS_WINDOWS ? windowsCodexCandidates() : IS_MACOS ? macosCodexCandidates() : unixCodexCandidates()),
|
|
221
|
+
...pathCandidates(),
|
|
222
|
+
]),
|
|
177
223
|
].filter(Boolean);
|
|
178
224
|
|
|
225
|
+
const deferredShim = (candidate) => platform === "win32" && /\.(?:cmd|bat)$/i.test(candidate);
|
|
226
|
+
for (const candidate of candidates) {
|
|
227
|
+
if (!deferredShim(candidate) && runnable(candidate)) return candidate;
|
|
228
|
+
}
|
|
229
|
+
const bundled = packaged({ platform, arch });
|
|
230
|
+
if (bundled) return bundled;
|
|
179
231
|
for (const candidate of candidates) {
|
|
180
|
-
if (
|
|
232
|
+
if (deferredShim(candidate) && runnable(candidate)) return candidate;
|
|
181
233
|
}
|
|
182
234
|
return "codex";
|
|
183
235
|
}
|
package/src/setup.mjs
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { resolveCodexBin, resolvePackagedCodexBin } from "./platform.mjs";
|
|
8
|
+
import { readRegularConfigFile } from "./project-policy.mjs";
|
|
9
|
+
import { stableInstallPath } from "./install-paths.mjs";
|
|
10
|
+
import { resolveClaudeDesktopConfigRoot } from "./desktop-account-context.mjs";
|
|
11
|
+
|
|
12
|
+
const ROOT = stableInstallPath(path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."));
|
|
13
|
+
const CODEX_PACKAGE = "@openai/codex@0.166.0";
|
|
14
|
+
const MAX_CONFIG = 16 * 1024 * 1024;
|
|
15
|
+
|
|
16
|
+
export function setupArguments(args) {
|
|
17
|
+
const allowed = new Set(["--postinstall", "--check", "--dry-run", "--json", "--no-download", "--help"]);
|
|
18
|
+
if (args.some((arg) => !allowed.has(arg))) throw new Error("Unknown setup option; use codex-bridge-setup --help");
|
|
19
|
+
if (args.includes("--check") && args.includes("--dry-run")) throw new Error("Use --check or --dry-run, not both");
|
|
20
|
+
return Object.fromEntries([...allowed].map((arg) => [arg.slice(2).replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()), args.includes(arg)]));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function automaticSetupEnabled(env = process.env) {
|
|
24
|
+
return env.npm_config_global === "true" && env.CODEX_BRIDGE_SKIP_SETUP !== "1";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function setupPaths(env = process.env, platform = process.platform) {
|
|
28
|
+
const home = env.HOME || env.USERPROFILE || os.homedir();
|
|
29
|
+
const codeDirectory = env.CLAUDE_CONFIG_DIR || home;
|
|
30
|
+
const configRoot = env.XDG_CONFIG_HOME || path.join(home, ".config");
|
|
31
|
+
const desktop = env.CLAUDE_DESKTOP_CONFIG || path.join(resolveClaudeDesktopConfigRoot({ env, platform }), "claude_desktop_config.json");
|
|
32
|
+
return { home, code: path.join(codeDirectory, ".claude.json"), desktop,
|
|
33
|
+
codex: env.CODEX_HOME || path.join(home, ".codex"),
|
|
34
|
+
gemini: path.join(home, ".gemini", "config"), tools: path.join(configRoot, "GptClaudeBridge", "tools") };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function readJson(file) {
|
|
38
|
+
const text = readRegularConfigFile(file, { missing: true, maxBytes: MAX_CONFIG });
|
|
39
|
+
if (text === null) return null;
|
|
40
|
+
const value = JSON.parse(text.replace(/^\uFEFF/, ""));
|
|
41
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Expected a JSON object: ${file}`);
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function execute(command, args, env, timeout = 120000) {
|
|
46
|
+
const result = spawnSync(command, args, { env, encoding: "utf8", windowsHide: true, shell: false, timeout,
|
|
47
|
+
maxBuffer: 4 * 1024 * 1024, stdio: ["ignore", "pipe", "pipe"] });
|
|
48
|
+
if (result.error || result.status !== 0) {
|
|
49
|
+
const lines = String(result.stderr || "").split(/\r?\n/).filter((line) => /^Error:|^(?:Expected|Refusing|Invalid|Unknown|Shared|Claude|Codex|Configuration|The existing|Missing|Unsafe)/.test(line)
|
|
50
|
+
&& !/Command failed|--env|password|secret|token|authorization/i.test(line));
|
|
51
|
+
const detail = lines.slice(0, 3).join(" ") || result.error?.code || `exit ${result.status}`;
|
|
52
|
+
throw new Error(`${path.basename(command)} ${path.basename(args[0] ?? "")}: ${detail}`);
|
|
53
|
+
}
|
|
54
|
+
return result.stdout || "";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function usableBinary(binary, platform) {
|
|
58
|
+
return typeof binary === "string" && path.isAbsolute(binary) && fs.existsSync(binary)
|
|
59
|
+
&& fs.statSync(binary).isFile() && (platform !== "win32" || /\.(exe|com)$/i.test(binary));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function managedBinary(paths, platform) {
|
|
63
|
+
return resolvePackagedCodexBin({ platform, resolvePackage: (specifier, from) => createRequire(
|
|
64
|
+
specifier === "@openai/codex/package.json" ? path.join(paths.tools, "package.json") : from,
|
|
65
|
+
).resolve(specifier) });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function ensureSetupCodex({ env, paths, platform, download = true, run = execute, resolve = resolveCodexBin }) {
|
|
69
|
+
const existing = resolve(env.CODEX_EXE, { env, platform });
|
|
70
|
+
if (usableBinary(existing, platform)) return { binary: existing, installed: false };
|
|
71
|
+
const managed = managedBinary(paths, platform);
|
|
72
|
+
if (usableBinary(managed, platform)) return { binary: managed, installed: false };
|
|
73
|
+
if (!download) throw new Error("Codex CLI was not found. Rerun setup without --no-download to install it automatically.");
|
|
74
|
+
const candidates = [env.npm_execpath, path.join(path.dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js")];
|
|
75
|
+
const npm = candidates.find((file) => file && path.isAbsolute(file) && fs.existsSync(file) && /(?:npm-cli|npm)\.js$/i.test(file));
|
|
76
|
+
if (!npm) throw new Error("Codex CLI is missing and npm could not be located. Rerun the global npm install command to finish setup.");
|
|
77
|
+
fs.mkdirSync(paths.tools, { recursive: true, mode: 0o700 });
|
|
78
|
+
run(process.execPath, [npm, "install", "--global=false", "--prefix", paths.tools, "--ignore-scripts", "--no-audit", "--no-fund", "--no-package-lock", "--no-save", CODEX_PACKAGE],
|
|
79
|
+
{ ...env, npm_config_global: "false", CODEX_BRIDGE_SKIP_SETUP: "1" }, 180000);
|
|
80
|
+
const binary = managedBinary(paths, platform);
|
|
81
|
+
if (!usableBinary(binary, platform)) throw new Error("The Codex CLI download did not provide a supported native executable. Check npm optional dependencies and rerun setup.");
|
|
82
|
+
return { binary, installed: true };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function setupEnvironment(env, paths, codex) {
|
|
86
|
+
const result = Object.fromEntries(Object.entries(env).filter(([name]) => !/^(?:CODEX_BRIDGE_|CLAUDE_BRIDGE_|CODEX_NATIVE_RELAY_|CODEX_RELAY_ID$|CODEX_THREAD_ID$|CODEX_APP_TOOLS_PIPE_PATH$)/.test(name)));
|
|
87
|
+
return { ...result, HOME: paths.home, USERPROFILE: paths.home, CODEX_HOME: paths.codex,
|
|
88
|
+
CLAUDE_DESKTOP_CONFIG: paths.desktop, CODEX_EXE: codex, CODEX_BIN: codex };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function configurationCheck(label, check, components) {
|
|
92
|
+
try { check(); components.push({ name: label, status: "configured" }); }
|
|
93
|
+
catch (error) { components.push({ name: label, status: "incomplete", detail: error.message }); }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function assertEntry(entry, worker, root, { caller } = {}) {
|
|
97
|
+
if (!entry || typeof entry.command !== "string" || !fs.existsSync(entry.command)) throw new Error("MCP command is missing or unavailable");
|
|
98
|
+
const args = entry.args ?? [];
|
|
99
|
+
const expected = worker === "index.mjs" || worker === "claude-bridge.mjs" || worker === "native-relay-companion.mjs"
|
|
100
|
+
? [path.join(root, "src", "mcp-supervisor.mjs"), worker] : [path.join(root, "src", worker)];
|
|
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
|
+
if (caller && entry.env?.ANTIGRAVITY_BRIDGE_CALLER !== caller) throw new Error("Antigravity caller registration is missing or conflicting");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function checkSetup({ env = process.env, platform = process.platform, root = ROOT, run = execute, resolve = resolveCodexBin } = {}) {
|
|
107
|
+
const paths = setupPaths(env, platform);
|
|
108
|
+
const components = [];
|
|
109
|
+
let codex;
|
|
110
|
+
let inventory;
|
|
111
|
+
configurationCheck("Codex CLI", () => {
|
|
112
|
+
codex = ensureSetupCodex({ env, paths, platform, download: false, run, resolve }).binary;
|
|
113
|
+
inventory = JSON.parse(run(codex, ["mcp", "list", "--json"], setupEnvironment(env, paths, codex), 30000));
|
|
114
|
+
if (!Array.isArray(inventory)) throw new Error("Codex returned an invalid MCP inventory");
|
|
115
|
+
}, components);
|
|
116
|
+
const native = platform === "win32" || platform === "darwin";
|
|
117
|
+
for (const [name, worker] of [["claude-bridge", "claude-bridge.mjs"], ...(native ? [["codex-native-relay", "native-relay-companion.mjs"]] : []), ["antigravity-bridge", "antigravity-bridge.mjs"]]) {
|
|
118
|
+
configurationCheck(`Codex / ${name}`, () => {
|
|
119
|
+
const item = inventory?.find((server) => server.name === name);
|
|
120
|
+
if (!item) throw new Error("MCP registration is missing");
|
|
121
|
+
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
|
+
if (name === "codex-native-relay" && !entry.transport.env_vars?.includes("CODEX_APP_TOOLS_PIPE_PATH")) throw new Error("Native pipe forwarding is missing");
|
|
124
|
+
}, components);
|
|
125
|
+
}
|
|
126
|
+
for (const [label, file, name, worker, caller] of [
|
|
127
|
+
["Claude Code / codex-bridge", paths.code, "codex-bridge", "index.mjs"],
|
|
128
|
+
["Claude Desktop / codex-bridge-desktop", paths.desktop, "codex-bridge-desktop", "index.mjs"],
|
|
129
|
+
["Claude Code / antigravity-bridge", paths.code, "antigravity-bridge", "antigravity-bridge.mjs", "claude"],
|
|
130
|
+
["Antigravity / agent-bridge", path.join(paths.gemini, "mcp_config.json"), "agent-bridge", "antigravity-outbound-bridge.mjs"],
|
|
131
|
+
]) configurationCheck(label, () => {
|
|
132
|
+
const entry = readJson(file)?.mcpServers?.[name];
|
|
133
|
+
assertEntry(entry, worker, root, { caller });
|
|
134
|
+
if (name === "codex-bridge-desktop" && entry.env?.CODEX_BRIDGE_DESKTOP_TASKS === "1") {
|
|
135
|
+
throw new Error("Shared Desktop chat cannot use native Code routing. Its saved choice was preserved; explicitly set CODEX_BRIDGE_DESKTOP_TASKS=0 for the local app-server route and CODEX_BRIDGE_AUTOSTART=1 if the bridge should start that server.");
|
|
136
|
+
}
|
|
137
|
+
}, components);
|
|
138
|
+
configurationCheck("Antigravity / codex-bridge sidecar", () => {
|
|
139
|
+
assertEntry(readJson(path.join(paths.gemini, "sidecars", "codex-bridge", "sidecar.json")), "antigravity-sidecar.mjs", root);
|
|
140
|
+
if (readJson(path.join(paths.gemini, "config.json"))?.sidecars?.["codex-bridge"]?.enabled !== true) throw new Error("Antigravity sidecar is not enabled");
|
|
141
|
+
}, components);
|
|
142
|
+
if (native) configurationCheck("Native relay executor", () => {
|
|
143
|
+
const relay = readJson(path.join(paths.codex, "native-relay.json"));
|
|
144
|
+
if (!relay?.relayThreadId) throw new Error("Native relay executor has not been initialized");
|
|
145
|
+
}, components);
|
|
146
|
+
configurationCheck("Claude registration names", () => {
|
|
147
|
+
if (readJson(paths.desktop)?.mcpServers?.["codex-bridge"]) throw new Error("Shared codex-bridge shadows the Claude Code registration");
|
|
148
|
+
}, components);
|
|
149
|
+
return { configured: components.every((item) => item.status === "configured"), liveVerified: false, components };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function runSetup({ args = [], env = process.env, platform = process.platform, root = ROOT, run = execute, resolve = resolveCodexBin } = {}) {
|
|
153
|
+
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." };
|
|
155
|
+
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
|
+
if (options.check) return checkSetup({ env, platform, root, run, resolve });
|
|
157
|
+
const native = platform === "win32" || platform === "darwin";
|
|
158
|
+
const paths = setupPaths(env, platform);
|
|
159
|
+
const planned = ["Claude Code / codex-bridge", "Claude Desktop / codex-bridge-desktop", ...(native ? ["Codex / codex-native-relay and executor"] : []), "Codex / claude-bridge", "Antigravity / all components"];
|
|
160
|
+
if (options.dryRun) return { dryRun: true, configured: false, liveVerified: false, components: planned.map((name) => ({ name, status: "planned" })), files: [paths.code, paths.desktop, path.join(paths.codex, "config.toml"), paths.gemini] };
|
|
161
|
+
if (env.SUDO_USER && env.SUDO_USER !== "root" && typeof process.getuid === "function" && process.getuid() === 0) throw new Error("Run the global install without sudo in the account that runs the apps; setup will not write another account's configuration.");
|
|
162
|
+
const runtime = ensureSetupCodex({ env, paths, platform, download: !options.noDownload, run, resolve });
|
|
163
|
+
fs.mkdirSync(paths.codex, { recursive: true, mode: 0o700 });
|
|
164
|
+
const base = setupEnvironment(env, paths, runtime.binary);
|
|
165
|
+
const components = [];
|
|
166
|
+
const install = (name, script, arguments_, environment = base) => {
|
|
167
|
+
try {
|
|
168
|
+
const output = run(process.execPath, [path.join(root, "scripts", script), ...arguments_], environment, 180000);
|
|
169
|
+
let notices = [];
|
|
170
|
+
try { notices = JSON.parse(output).notices ?? []; } catch {}
|
|
171
|
+
components.push({ name, status: "configured", ...(notices.length ? { notices } : {}) });
|
|
172
|
+
return true;
|
|
173
|
+
} catch (error) { components.push({ name, status: "incomplete", detail: error.message }); return false; }
|
|
174
|
+
};
|
|
175
|
+
const code = install(planned[0], "install-claude-code.mjs", ["--install", "--desktop-config", paths.desktop], {
|
|
176
|
+
...base, CODEX_BRIDGE_DESKTOP_TASKS: native ? "1" : "0", CODEX_BRIDGE_AUTOSTART: native ? "0" : "1", CODEX_BRIDGE_THREAD_POLICY: "roots",
|
|
177
|
+
});
|
|
178
|
+
if (code) install(planned[1], "install-claude-desktop.mjs", ["--entry-name", "codex-bridge-desktop", "--quiet"]);
|
|
179
|
+
if (native) {
|
|
180
|
+
const relay = readJson(path.join(paths.codex, "native-relay.json"));
|
|
181
|
+
install("Codex / codex-native-relay and executor", "install-native-relay.mjs", ["--setup", ...(relay?.desktopTasks === undefined ? ["--desktop-tasks"] : [])]);
|
|
182
|
+
}
|
|
183
|
+
install("Codex / claude-bridge", "install-codex-mcp.mjs", ["--setup"]);
|
|
184
|
+
if (code) install("Antigravity / all components", "install-antigravity-bridge.mjs", ["--all", "--setup"]);
|
|
185
|
+
const inspection = checkSetup({ env: base, platform, root, run, resolve: () => runtime.binary });
|
|
186
|
+
return { configured: components.every((item) => item.status === "configured") && inspection.configured, liveVerified: false,
|
|
187
|
+
cliInstalled: runtime.installed, components, verification: inspection.components,
|
|
188
|
+
nextActions: ["Open or reconnect Claude, Codex and Antigravity after active work finishes so they load the new entries.",
|
|
189
|
+
"Use the same saved project in Claude Desktop Code and Codex Desktop. Keep the apps signed in.",
|
|
190
|
+
"Verify codex_bridge_status in Claude Code, native_relay_status in Codex, and agent_bridge_status in Antigravity. Shared Claude Desktop chat uses the local app-server route."] };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function printSetup(result, { json = false, output = console.log } = {}) {
|
|
194
|
+
if (json) { output(JSON.stringify(result, null, 2)); return; }
|
|
195
|
+
if (result.help) { output(result.help); return; }
|
|
196
|
+
if (result.skipped) { output(result.reason); return; }
|
|
197
|
+
output(result.dryRun ? "Bridge setup preview (no changes)" : result.configured ? "Bridge configuration verified. Live connections still require the apps to load it." : "Bridge setup is incomplete. Fix the items below, then run codex-bridge-setup again.");
|
|
198
|
+
for (const item of result.components ?? []) {
|
|
199
|
+
output(`[${item.status}] ${item.name}${item.detail ? `: ${item.detail}` : ""}`);
|
|
200
|
+
for (const notice of item.notices ?? []) output(` ${notice}`);
|
|
201
|
+
}
|
|
202
|
+
for (const item of result.verification ?? []) if (item.status !== "configured") output(`[${item.status}] ${item.name}: ${item.detail}`);
|
|
203
|
+
for (const action of result.nextActions ?? []) output(action);
|
|
204
|
+
}
|
package/src/thread-delivery.mjs
CHANGED
|
@@ -27,9 +27,11 @@ import { requireClaudeSenderContext } from "./claude-sender-context.mjs";
|
|
|
27
27
|
export const NATIVE_BACKEND = "codex-desktop-native";
|
|
28
28
|
export const APP_SERVER_BACKEND = "app-server";
|
|
29
29
|
export const DESKTOP_TOOL_BUDGET_MS = 40000;
|
|
30
|
+
export const OBSERVATION_UNAVAILABLE_GUIDANCE = "The response could not be verified; this does not establish that delivery or the task failed. Inspect the existing task and this receipt; do not resend the prompt.";
|
|
30
31
|
export const WORKSPACE_UNASSIGNED = "DESKTOP_WORKSPACE_UNASSIGNED";
|
|
31
32
|
export const UNASSIGNED_WORKSPACE_GRACE_MS = 120000;
|
|
32
33
|
const RELEASE_STATUSES = new Set(["completed", "interrupted", "failed"]);
|
|
34
|
+
const UNMATCHED_REPLY_GRACE_MS = 3000;
|
|
33
35
|
const METADATA_READ_OPTIONS = { includeOutputs: false, maxOutputCharsPerItem: 1 };
|
|
34
36
|
|
|
35
37
|
function unavailableWaitRenderer(error) {
|
|
@@ -572,6 +574,7 @@ export class DesktopTaskDelivery {
|
|
|
572
574
|
let readSnapshots = false;
|
|
573
575
|
let deferredReason = null;
|
|
574
576
|
let deferredTurn = null;
|
|
577
|
+
let unmatchedTerminal = null;
|
|
575
578
|
const readSnapshot = async () => {
|
|
576
579
|
const result = await this.request("read_thread", { threadId, hostId: "local", turnLimit: 1, ...METADATA_READ_OPTIONS }, { deadline: startedAt + timeoutMs });
|
|
577
580
|
const thread = result?.thread;
|
|
@@ -614,6 +617,7 @@ export class DesktopTaskDelivery {
|
|
|
614
617
|
} catch (err) {
|
|
615
618
|
if (deadlineError(err)) return expired();
|
|
616
619
|
if (err.code === WORKSPACE_UNASSIGNED) {
|
|
620
|
+
unmatchedTerminal = null;
|
|
617
621
|
await pause();
|
|
618
622
|
continue;
|
|
619
623
|
}
|
|
@@ -639,13 +643,15 @@ export class DesktopTaskDelivery {
|
|
|
639
643
|
const observed = await observe(null, true);
|
|
640
644
|
if (!observed) return expired();
|
|
641
645
|
if (observed === observeLater) {
|
|
646
|
+
unmatchedTerminal = null;
|
|
642
647
|
await pause();
|
|
643
648
|
continue;
|
|
644
649
|
}
|
|
645
650
|
turnId = observed.turnId ?? null;
|
|
646
651
|
if (observed.status !== "pending") {
|
|
652
|
+
const nativeFailure = turnId && turn?.id === turnId && ["failed", "interrupted"].includes(turn.status) ? turn.status : null;
|
|
647
653
|
return {
|
|
648
|
-
threadId, turnId, status: observed.status === "unavailable" ? "
|
|
654
|
+
threadId, turnId, status: observed.status === "unavailable" ? nativeFailure ?? "observation_unavailable" : "completed",
|
|
649
655
|
responseStatus: observed.status, observationStatus: observed.status,
|
|
650
656
|
text: observed.status === "completed" ? observed.text : "",
|
|
651
657
|
...(observed.reason ? { observationReason: observed.reason } : {}),
|
|
@@ -656,6 +662,34 @@ export class DesktopTaskDelivery {
|
|
|
656
662
|
};
|
|
657
663
|
}
|
|
658
664
|
if (needsAttention) return attention();
|
|
665
|
+
if (turnId === null && observed.dispatchObserved !== true && threadStatus === "idle" &&
|
|
666
|
+
typeof turn?.id === "string" && turn.id && turn.id !== previousTurnId && RELEASE_STATUSES.has(turn.status)) {
|
|
667
|
+
if (unmatchedTerminal?.id !== turn.id || unmatchedTerminal.status !== turn.status) {
|
|
668
|
+
unmatchedTerminal = { id: turn.id, status: turn.status, since: this.now(), polls: 1 };
|
|
669
|
+
} else unmatchedTerminal.polls++;
|
|
670
|
+
if (unmatchedTerminal.polls >= 3 && this.now() - unmatchedTerminal.since >= UNMATCHED_REPLY_GRACE_MS) {
|
|
671
|
+
let snapshot;
|
|
672
|
+
try { snapshot = (await readSnapshot()).polls[0]; }
|
|
673
|
+
catch (error) {
|
|
674
|
+
if (deadlineError(error)) return expired();
|
|
675
|
+
if (error.code === WORKSPACE_UNASSIGNED) {
|
|
676
|
+
unmatchedTerminal = null;
|
|
677
|
+
await pause();
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
680
|
+
throw error;
|
|
681
|
+
}
|
|
682
|
+
const status = typeof snapshot.thread.status === "string" ? snapshot.thread.status : snapshot.thread.status?.type;
|
|
683
|
+
if (status === "idle" && snapshot.latestTurn?.id === unmatchedTerminal.id && snapshot.latestTurn.status === unmatchedTerminal.status) {
|
|
684
|
+
return {
|
|
685
|
+
threadId, turnId: null, status: "observation_unavailable", responseStatus: "unavailable", observationStatus: "unavailable",
|
|
686
|
+
observationReason: "Codex Desktop reports an idle task, but its latest finished turn could not be correlated to this delivery after the rollout grace period. Inspect the existing task and receipt; do not resend the prompt.",
|
|
687
|
+
text: "", assistantItems: [], replySha256: null, responseSource: null, activity: [], errors: [], durationMs: this.now() - startedAt,
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
unmatchedTerminal = null;
|
|
691
|
+
}
|
|
692
|
+
} else unmatchedTerminal = null;
|
|
659
693
|
if (this.now() - startedAt >= timeoutMs) return expired();
|
|
660
694
|
await this.sleep(Math.min(1500, timeoutMs - (this.now() - startedAt)));
|
|
661
695
|
continue;
|