@minhspark/codex-mcp-bridge 1.24.1 → 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 +24 -0
- package/README.md +328 -33
- 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 +4 -1
- 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 +13 -6
- 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/desktop-account-context.mjs +18 -0
- package/src/index.mjs +1 -1
- package/src/native-relay-companion.mjs +1 -1
- package/src/platform.mjs +57 -5
- package/src/setup.mjs +204 -0
- package/src/thread-delivery.mjs +32 -0
package/src/index.mjs
CHANGED
|
@@ -41,7 +41,7 @@ import { createRelayWake, relayKeeperEnabled, relayWakeStateFile, startRelayKeep
|
|
|
41
41
|
|
|
42
42
|
exitForVersionRequest(import.meta.url);
|
|
43
43
|
|
|
44
|
-
const VERSION = "1.
|
|
44
|
+
const VERSION = "1.25.0";
|
|
45
45
|
void import("./telemetry.mjs").then(({ startUsageReporting }) => startUsageReporting({ version: VERSION })).catch(() => {});
|
|
46
46
|
const log = (msg) => process.stderr.write(`[codex-mcp-bridge] ${msg}\n`);
|
|
47
47
|
|
|
@@ -42,7 +42,7 @@ import { remoteCodexSocketPath, remoteNativeAdapter, remoteRequestResponse } fro
|
|
|
42
42
|
|
|
43
43
|
exitForVersionRequest(import.meta.url);
|
|
44
44
|
|
|
45
|
-
const VERSION = "1.
|
|
45
|
+
const VERSION = "1.25.0";
|
|
46
46
|
const log = (msg) => process.stderr.write(`[native-relay] ${msg}\n`);
|
|
47
47
|
|
|
48
48
|
function errorResponse(code, message, sent, details) {
|
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
|
@@ -31,6 +31,7 @@ export const OBSERVATION_UNAVAILABLE_GUIDANCE = "The response could not be verif
|
|
|
31
31
|
export const WORKSPACE_UNASSIGNED = "DESKTOP_WORKSPACE_UNASSIGNED";
|
|
32
32
|
export const UNASSIGNED_WORKSPACE_GRACE_MS = 120000;
|
|
33
33
|
const RELEASE_STATUSES = new Set(["completed", "interrupted", "failed"]);
|
|
34
|
+
const UNMATCHED_REPLY_GRACE_MS = 3000;
|
|
34
35
|
const METADATA_READ_OPTIONS = { includeOutputs: false, maxOutputCharsPerItem: 1 };
|
|
35
36
|
|
|
36
37
|
function unavailableWaitRenderer(error) {
|
|
@@ -573,6 +574,7 @@ export class DesktopTaskDelivery {
|
|
|
573
574
|
let readSnapshots = false;
|
|
574
575
|
let deferredReason = null;
|
|
575
576
|
let deferredTurn = null;
|
|
577
|
+
let unmatchedTerminal = null;
|
|
576
578
|
const readSnapshot = async () => {
|
|
577
579
|
const result = await this.request("read_thread", { threadId, hostId: "local", turnLimit: 1, ...METADATA_READ_OPTIONS }, { deadline: startedAt + timeoutMs });
|
|
578
580
|
const thread = result?.thread;
|
|
@@ -615,6 +617,7 @@ export class DesktopTaskDelivery {
|
|
|
615
617
|
} catch (err) {
|
|
616
618
|
if (deadlineError(err)) return expired();
|
|
617
619
|
if (err.code === WORKSPACE_UNASSIGNED) {
|
|
620
|
+
unmatchedTerminal = null;
|
|
618
621
|
await pause();
|
|
619
622
|
continue;
|
|
620
623
|
}
|
|
@@ -640,6 +643,7 @@ export class DesktopTaskDelivery {
|
|
|
640
643
|
const observed = await observe(null, true);
|
|
641
644
|
if (!observed) return expired();
|
|
642
645
|
if (observed === observeLater) {
|
|
646
|
+
unmatchedTerminal = null;
|
|
643
647
|
await pause();
|
|
644
648
|
continue;
|
|
645
649
|
}
|
|
@@ -658,6 +662,34 @@ export class DesktopTaskDelivery {
|
|
|
658
662
|
};
|
|
659
663
|
}
|
|
660
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;
|
|
661
693
|
if (this.now() - startedAt >= timeoutMs) return expired();
|
|
662
694
|
await this.sleep(Math.min(1500, timeoutMs - (this.now() - startedAt)));
|
|
663
695
|
continue;
|