@minhspark/codex-mcp-bridge 1.26.7 → 1.26.9
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 +41 -0
- package/docs/new-windows-setup.md +51 -0
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/scripts/install-antigravity-bridge.mjs +29 -23
- package/scripts/install-claude-desktop.mjs +16 -11
- package/scripts/install-codex-mcp.mjs +2 -5
- package/scripts/install-native-relay.mjs +4 -7
- package/scripts/setup.mjs +8 -4
- package/scripts/update.mjs +27 -15
- package/src/claude-bridge.mjs +1 -1
- package/src/claude-code-registration.mjs +7 -8
- package/src/codex-mcp-registration.mjs +32 -1
- package/src/desktop-account-context.mjs +54 -17
- package/src/index.mjs +1 -1
- package/src/install-paths.mjs +35 -0
- package/src/native-relay-companion.mjs +1 -1
- package/src/setup.mjs +72 -10
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,47 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [SemVer](ht
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [1.26.9] - 2026-11-03
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Shorten the "Applying managed settings" step of `npx @minhspark/codex-mcp-bridge@latest`. Each Codex `mcp` command takes several seconds to start on a machine with many MCP servers and plugins, and one update ran about sixteen of them. The Codex installers now read their own entry with one `codex mcp get` instead of listing every server first, the verification reads all three registrations from a single `codex mcp list`, and the updater no longer runs a second, separate verification process after setup has already verified the saved settings.
|
|
12
|
+
- Show which component the updater is configuring while managed settings are applied, instead of a single line that looked frozen for a minute or more.
|
|
13
|
+
- Stop reporting `MCP registration is missing` for `claude-bridge`, `codex-native-relay` and `antigravity-bridge` when Codex CLI was merely too slow to answer. A slow inventory now says so, and the three registrations are reported as not checked. Codex `mcp` commands used by setup may take up to 120 seconds instead of 30, so a busy computer no longer fails an otherwise complete update.
|
|
14
|
+
|
|
15
|
+
### Verification
|
|
16
|
+
|
|
17
|
+
- Regression coverage checks that verification makes exactly one Codex inventory call, that only Codex's own "No MCP server named" error counts as an absent entry, that a timed-out inventory is reported as unchecked, and that the updater runs setup once and relays only its progress lines.
|
|
18
|
+
|
|
19
|
+
### Upgrade
|
|
20
|
+
|
|
21
|
+
```powershell
|
|
22
|
+
npx.cmd -y @minhspark/codex-mcp-bridge@latest
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
This command runs the 1.26.9 updater itself, so the shorter sequence applies to this update.
|
|
26
|
+
|
|
27
|
+
## [1.26.8] - 2026-11-03
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- Discover Claude Desktop's migrated Windows MSIX data under `%LOCALAPPDATA%\Claude-Data\claude_*`. Ignore old roots containing only migration markers, including an alias to a discovered retired root, without deleting data or guessing between active profiles. Explicit data-root overrides remain authoritative.
|
|
32
|
+
- Retarget verified older bridge source registrations during managed setup, including renamed checkouts, while preserving custom environment values, project restrictions and disabled settings. Changed configuration files receive unique backups, including repeated Desktop migrations on the same day. Custom launchers, extra arguments, unrelated packages and unavailable old source remain protected and receive specific recovery guidance.
|
|
33
|
+
- Restore a missing Antigravity sidecar definition when only its ordinary enable/disable registration remains. Preserve a deliberately disabled sidecar and refuse custom orphan settings.
|
|
34
|
+
- Include the failed setup components and safe recovery guidance in updater errors instead of reporting only `EXIT_1`. Detailed local setup failures remain available through `codex-bridge-setup --check --json`; raw child output and private configuration are excluded from the updater summary.
|
|
35
|
+
|
|
36
|
+
### Verification
|
|
37
|
+
|
|
38
|
+
- Regression coverage includes migrated MSIX roots, retired aliases, multiple profiles, verified legacy entrypoints, exact configuration backups, concurrent configuration changes, disabled sidecars and bounded error summaries. Native delivery on the reporter's Windows 11 / Claude Desktop 4.4212 environment has not been exercised.
|
|
39
|
+
|
|
40
|
+
### Upgrade
|
|
41
|
+
|
|
42
|
+
```powershell
|
|
43
|
+
npx.cmd -y @minhspark/codex-mcp-bridge@latest
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
After active work finishes, reconnect the affected MCP clients or restart their host apps so existing direct processes load the updated registrations.
|
|
47
|
+
|
|
7
48
|
## [1.26.7] - 2026-11-03
|
|
8
49
|
|
|
9
50
|
### Fixed
|
|
@@ -114,6 +114,57 @@ incomplete.
|
|
|
114
114
|
has no bridge tools, inspect and reconnect `codex-bridge` using `/mcp` before
|
|
115
115
|
attempting delivery.
|
|
116
116
|
|
|
117
|
+
### Claude Desktop MSIX data migration
|
|
118
|
+
|
|
119
|
+
Discovery includes `%LOCALAPPDATA%\Claude-Data\claude_*` as well as
|
|
120
|
+
`%APPDATA%\Claude` and the older
|
|
121
|
+
`%LOCALAPPDATA%\Packages\Claude_*\LocalCache\Roaming\Claude` layout.
|
|
122
|
+
Directories containing only regular `.user-data-left*` migration markers are
|
|
123
|
+
ignored. An old directory link is ignored only when it points to another
|
|
124
|
+
discovered marker-only directory. A marker beside real data does not make that
|
|
125
|
+
data disposable, and setup never deletes these directories.
|
|
126
|
+
|
|
127
|
+
If multiple directories still contain account data, setup refuses to guess.
|
|
128
|
+
Select the actual directory used by the signed-in app for that invocation:
|
|
129
|
+
|
|
130
|
+
```powershell
|
|
131
|
+
$env:CLAUDE_DESKTOP_USER_DATA = 'C:\Users\YourUser\AppData\Local\Claude-Data\claude_YourPackageId'
|
|
132
|
+
codex-bridge-setup.cmd --check --json
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Replace the example with the existing directory; do not create an empty
|
|
136
|
+
directory to bypass discovery. Explicit data-root and config-file overrides
|
|
137
|
+
remain authoritative. Configuration discovery without account state does not
|
|
138
|
+
establish a signed-in account or prove native delivery readiness.
|
|
139
|
+
|
|
140
|
+
### Older source registrations and incomplete setup
|
|
141
|
+
|
|
142
|
+
Managed setup can retarget a recognized older source installation, including a
|
|
143
|
+
renamed checkout. Another installation must have the bridge's exact package
|
|
144
|
+
identity and regular source files, with supported Node arguments. Custom
|
|
145
|
+
launchers, extra arguments, unrelated packages and missing old source cannot
|
|
146
|
+
be adopted automatically. Inspect the named registration before replacing its
|
|
147
|
+
`command` and `args`; preserve its environment, project restrictions and
|
|
148
|
+
disabled settings. Existing changed configuration files receive backups.
|
|
149
|
+
|
|
150
|
+
If only Antigravity's `sidecars["codex-bridge"]` setting remains after its
|
|
151
|
+
`sidecar.json` was removed, setup restores the definition when that setting is
|
|
152
|
+
empty or contains only a boolean `enabled`. A saved `false` stays disabled.
|
|
153
|
+
Custom residual settings still require inspection; deleting `sidecar.json`
|
|
154
|
+
alone does not remove the registration in `.gemini\config\config.json`.
|
|
155
|
+
|
|
156
|
+
The updater names recognized failed components and provides static recovery
|
|
157
|
+
guidance. For the detailed local report, run:
|
|
158
|
+
|
|
159
|
+
```powershell
|
|
160
|
+
codex-bridge-setup.cmd --json
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
This command applies setup again; `--check --json` only inspects it. Review and
|
|
164
|
+
redact local details before sharing them. After a successful migration, let
|
|
165
|
+
active work finish and reconnect the affected MCP clients so direct processes
|
|
166
|
+
load the new registration. A saved configuration change does not restart them.
|
|
167
|
+
|
|
117
168
|
Local `npm install` and `npm ci` do not configure apps. For a deliberate
|
|
118
169
|
package-only installation, a direct global npm install can use
|
|
119
170
|
`CODEX_BRIDGE_SKIP_SETUP=1` or `--ignore-scripts`. This differs from the
|
package/package.json
CHANGED
package/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
3
|
"name": "codex-mcp-bridge",
|
|
4
|
-
"version": "1.26.
|
|
4
|
+
"version": "1.26.9",
|
|
5
5
|
"description": "Connect ChatGPT to your own Codex Desktop projects and conversations.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Bui Dang Minh",
|
|
@@ -3,8 +3,10 @@ import { execFileSync } from "node:child_process";
|
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { applyStdioMcpRegistration, CODEX_MCP_TIMEOUT_MS, readCodexMcpServer, stdioMcpRegistration } from "../src/codex-mcp-registration.mjs";
|
|
8
|
+
import { bridgeEntrypointConflict, isBridgeSource, isMainModule, isNodeLauncher, stableInstallPath, stableNodeBin } from "../src/install-paths.mjs";
|
|
9
|
+
import { backupRegularConfigFile } from "../src/project-policy.mjs";
|
|
8
10
|
import { homeDir, resolveCodexBin, spawnEnv } from "../src/platform.mjs";
|
|
9
11
|
import { exitForVersionRequest } from "../src/cli-version.mjs";
|
|
10
12
|
import { createReleaseSnapshot, snapshotRoot } from "../src/release-snapshot.mjs";
|
|
@@ -60,27 +62,25 @@ export function validateCodexExecutable(executable, { platform = process.platfor
|
|
|
60
62
|
return resolved;
|
|
61
63
|
}
|
|
62
64
|
|
|
63
|
-
function ownEntry(entry, expected, label, retarget = false, supervised = false) {
|
|
65
|
+
function ownEntry(entry, expected, label, retarget = false, supervised = false, node = stableNodeBin()) {
|
|
64
66
|
if (entry === undefined || entry === null) return;
|
|
65
67
|
object(entry, label);
|
|
66
68
|
const script = entry.args?.[0];
|
|
67
69
|
const managed = supervised && Array.isArray(entry.args) && entry.args.length === 2
|
|
68
70
|
&& typeof script === "string" && path.basename(script) === "mcp-supervisor.mjs" && entry.args[1] === path.basename(expected);
|
|
71
|
+
const commandArgs = supervised ? [path.join(path.dirname(expected), "mcp-supervisor.mjs"), path.basename(expected)] : [expected];
|
|
72
|
+
const conflict = detail => bridgeEntrypointConflict(label, node, commandArgs, detail);
|
|
69
73
|
if (!Array.isArray(entry.args) || !managed && entry.args.length !== 1 || typeof script !== "string") {
|
|
70
|
-
throw
|
|
74
|
+
throw conflict("a different entrypoint");
|
|
71
75
|
}
|
|
72
76
|
const target = managed ? path.join(path.dirname(expected), "mcp-supervisor.mjs") : expected;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|| path.basename(source) !== "src" || path.basename(path.dirname(source)).toLowerCase() !== "codex-mcp-bridge")) {
|
|
78
|
-
throw new Error(`${label} is already configured with a different entrypoint; it was not changed`);
|
|
77
|
+
const current = path.isAbsolute(script) && path.relative(target, script) === "";
|
|
78
|
+
if (!current && (!retarget || path.basename(script) !== path.basename(target)
|
|
79
|
+
|| !isBridgeSource(script, [], [path.basename(expected)])) || script.includes("\0")) {
|
|
80
|
+
throw conflict("a different entrypoint");
|
|
79
81
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|| !path.isAbsolute(command) && command !== path.basename(command)) {
|
|
83
|
-
throw new Error(`${label} is already configured with a different launcher; it was not changed`);
|
|
82
|
+
if (!isNodeLauncher(entry.command)) {
|
|
83
|
+
throw conflict("a different launcher");
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
|
|
@@ -105,7 +105,7 @@ export function planAntigravityInstallation({ home = homeDir(), codeConfigDir =
|
|
|
105
105
|
let codex = null;
|
|
106
106
|
if (targets.includes("codex")) {
|
|
107
107
|
if (codexExisting && codexExisting.transport?.type !== "stdio") throw new Error("The existing Antigravity MCP transport is not stdio");
|
|
108
|
-
ownEntry(codexExisting?.transport, entry, "Codex Antigravity bridge", retarget, true);
|
|
108
|
+
ownEntry(codexExisting?.transport, entry, "Codex Antigravity bridge", retarget, true, node);
|
|
109
109
|
const existingCaller = codexExisting?.transport?.env?.ANTIGRAVITY_BRIDGE_CALLER;
|
|
110
110
|
if (existingCaller !== undefined && existingCaller !== "codex") throw new Error("The existing Codex bridge has a conflicting caller identity");
|
|
111
111
|
codex = remove ? { remove: Boolean(codexExisting) } : stdioMcpRegistration({ name: NAME, existing: codexExisting, node, entry: supervisorEntry, entryArgs: [path.basename(entry)], envOverrides: { ANTIGRAVITY_BRIDGE_CALLER: "codex" } });
|
|
@@ -116,7 +116,7 @@ export function planAntigravityInstallation({ home = homeDir(), codeConfigDir =
|
|
|
116
116
|
const value = structuredClone(original.value);
|
|
117
117
|
if (value.mcpServers !== undefined) object(value.mcpServers, "Claude mcpServers");
|
|
118
118
|
const existing = value.mcpServers?.[NAME];
|
|
119
|
-
ownEntry(existing, entry, "Claude Antigravity bridge", retarget, true);
|
|
119
|
+
ownEntry(existing, entry, "Claude Antigravity bridge", retarget, true, node);
|
|
120
120
|
if (existing?.type !== undefined && existing.type !== "stdio") throw new Error("The existing Claude bridge transport is not stdio");
|
|
121
121
|
if (existing?.env !== undefined) object(existing.env, "Claude bridge environment");
|
|
122
122
|
if (existing?.env?.ANTIGRAVITY_BRIDGE_CALLER !== undefined && existing.env.ANTIGRAVITY_BRIDGE_CALLER !== "claude") throw new Error("The existing Claude bridge has a conflicting caller identity");
|
|
@@ -135,7 +135,7 @@ export function planAntigravityInstallation({ home = homeDir(), codeConfigDir =
|
|
|
135
135
|
const mcp = structuredClone(originalMcp.value);
|
|
136
136
|
if (mcp.mcpServers !== undefined) object(mcp.mcpServers, "Antigravity mcpServers");
|
|
137
137
|
const outbound = mcp.mcpServers?.[OUTBOUND];
|
|
138
|
-
ownEntry(outbound, outboundEntry, "Antigravity outbound bridge", retarget, true);
|
|
138
|
+
ownEntry(outbound, outboundEntry, "Antigravity outbound bridge", retarget, true, node);
|
|
139
139
|
if (outbound?.type !== undefined && outbound.type !== "stdio" || outbound?.url !== undefined || outbound?.serverUrl !== undefined || outbound?.httpUrl !== undefined) throw new Error("The existing Antigravity outbound bridge transport is not stdio");
|
|
140
140
|
if (outbound?.env !== undefined) object(outbound.env, "Antigravity outbound bridge environment");
|
|
141
141
|
if (Object.keys(outbound?.env ?? {}).some((name) => /^(?:CLAUDE|CODEX_|ANTIGRAVITY_BRIDGE_CALLER$)/i.test(name))) throw new Error("The existing Antigravity outbound bridge contains a foreign caller environment; review it before installation");
|
|
@@ -151,12 +151,17 @@ export function planAntigravityInstallation({ home = homeDir(), codeConfigDir =
|
|
|
151
151
|
const mcpEdit = changed(originalMcp, mcp);
|
|
152
152
|
if (mcpEdit) edits.push(mcpEdit);
|
|
153
153
|
const originalSidecar = snapshot(path.join(configRoot, "sidecars", SIDECAR, "sidecar.json"));
|
|
154
|
-
if (originalSidecar.text !== null) ownEntry(originalSidecar.value, sidecarEntry, "Antigravity sidecar", retarget);
|
|
154
|
+
if (originalSidecar.text !== null) ownEntry(originalSidecar.value, sidecarEntry, "Antigravity sidecar", retarget, false, node);
|
|
155
155
|
const originalConfig = snapshot(path.join(configRoot, "config.json"));
|
|
156
156
|
const config = structuredClone(originalConfig.value);
|
|
157
157
|
if (config.sidecars !== undefined) object(config.sidecars, "Antigravity sidecars");
|
|
158
158
|
if (config.sidecars?.[SIDECAR] !== undefined) object(config.sidecars[SIDECAR], "Antigravity bridge sidecar settings");
|
|
159
|
-
if (originalSidecar.text === null && config.sidecars?.[SIDECAR] !== undefined)
|
|
159
|
+
if (originalSidecar.text === null && config.sidecars?.[SIDECAR] !== undefined) {
|
|
160
|
+
const settings = config.sidecars[SIDECAR];
|
|
161
|
+
const plain = Object.keys(settings).every(key => key === "enabled") && (settings.enabled === undefined || typeof settings.enabled === "boolean");
|
|
162
|
+
if (!setup || remove || !plain) throw new Error('SETUP_SIDECAR_DEFINITION_MISSING: Antigravity config.json still contains sidecars["codex-bridge"] without sidecar.json. Restore the previous bridge definition, or back up config.json and remove only the stale sidecars["codex-bridge"] entry before rerunning setup.');
|
|
163
|
+
notices.push("Restored the missing bridge sidecar definition and preserved its saved enabled preference.");
|
|
164
|
+
}
|
|
160
165
|
let definition = null;
|
|
161
166
|
if (remove) { if (config.sidecars) delete config.sidecars[SIDECAR]; }
|
|
162
167
|
else {
|
|
@@ -202,8 +207,11 @@ function assertCurrent(edit) {
|
|
|
202
207
|
export function applyJsonEdits(edits) {
|
|
203
208
|
for (const edit of edits) assertCurrent(edit);
|
|
204
209
|
const completed = [];
|
|
210
|
+
const suffix = randomUUID();
|
|
205
211
|
try {
|
|
206
212
|
for (const edit of edits) {
|
|
213
|
+
assertCurrent(edit);
|
|
214
|
+
if (edit.text !== null) backupRegularConfigFile(edit.file, `${edit.file}.backup-${suffix}`, edit.text, 16 * 1024 * 1024);
|
|
207
215
|
assertCurrent(edit);
|
|
208
216
|
if (edit.next === null) fs.unlinkSync(edit.file);
|
|
209
217
|
else {
|
|
@@ -248,10 +256,8 @@ export function runAntigravityInstaller(args = process.argv.slice(2), { prepareR
|
|
|
248
256
|
let codexExisting = null;
|
|
249
257
|
if (options.targets.includes("codex")) {
|
|
250
258
|
const codexBin = validateCodexExecutable(resolveCodexBin(process.env.CODEX_EXE));
|
|
251
|
-
run = (command) => execFileSync(codexBin, command, { encoding: "utf8", env: spawnEnv(), windowsHide: true, shell: false, timeout:
|
|
252
|
-
|
|
253
|
-
if (!Array.isArray(inventory)) throw new Error("Codex returned an invalid MCP inventory");
|
|
254
|
-
if (inventory.some((item) => item.name === NAME)) codexExisting = JSON.parse(run(["mcp", "get", NAME, "--json"]));
|
|
259
|
+
run = (command) => execFileSync(codexBin, command, { encoding: "utf8", env: spawnEnv(), windowsHide: true, shell: false, timeout: CODEX_MCP_TIMEOUT_MS, stdio: "pipe" });
|
|
260
|
+
codexExisting = readCodexMcpServer(run, NAME);
|
|
255
261
|
}
|
|
256
262
|
const plan = planAntigravityInstallation({ ...options, home, root, node, codexExisting });
|
|
257
263
|
if (!options.remove) {
|
|
@@ -10,12 +10,12 @@ import { desktopTasksConfigured } from "../src/native-relay.mjs";
|
|
|
10
10
|
import { exitForVersionRequest } from "../src/cli-version.mjs";
|
|
11
11
|
import { createReleaseSnapshot, snapshotRoot } from "../src/release-snapshot.mjs";
|
|
12
12
|
import { enableCodexFullAccess, fullAccessEnabled } from "../src/codex-full-access.mjs";
|
|
13
|
-
import { stableInstallPath, stableNodeBin } from "../src/install-paths.mjs";
|
|
13
|
+
import { bridgeEntrypointConflict, isBridgeSource, isNodeLauncher, stableInstallPath, stableNodeBin } from "../src/install-paths.mjs";
|
|
14
14
|
|
|
15
15
|
exitForVersionRequest(import.meta.url);
|
|
16
16
|
|
|
17
17
|
const root = stableInstallPath(path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."));
|
|
18
|
-
const cfgPath = process.env.CLAUDE_DESKTOP_CONFIG ?? claudeDesktopConfigPath();
|
|
18
|
+
const cfgPath = path.resolve(process.env.CLAUDE_DESKTOP_CONFIG ?? claudeDesktopConfigPath());
|
|
19
19
|
const entryOption = process.argv.indexOf("--entry-name");
|
|
20
20
|
const requestedEntryName = entryOption < 0 ? undefined : process.argv[entryOption + 1];
|
|
21
21
|
if (entryOption >= 0 && !["codex-bridge", "codex-bridge-desktop"].includes(requestedEntryName)) throw new Error("--entry-name must be codex-bridge or codex-bridge-desktop");
|
|
@@ -113,14 +113,12 @@ const entryName = requestedEntryName ?? bridgeEntries[0] ?? "codex-bridge";
|
|
|
113
113
|
if ((requestedEntryName || setup) && cfg.mcpServers[entryName] !== undefined) {
|
|
114
114
|
const existing = cfg.mcpServers[entryName];
|
|
115
115
|
const script = existing?.args?.[0];
|
|
116
|
-
const
|
|
117
|
-
const ownedSource = source && (sourceDirectories.has(source)
|
|
118
|
-
|| path.basename(source) === "src" && path.basename(path.dirname(source)).toLowerCase() === "codex-mcp-bridge");
|
|
116
|
+
const ownedSource = isBridgeSource(script, sourceDirectories, ["index.mjs"]);
|
|
119
117
|
const supervised = existing?.args?.length === 2 && path.basename(script ?? "") === "mcp-supervisor.mjs" && existing.args[1] === "index.mjs";
|
|
120
118
|
const direct = existing?.args?.length === 1 && ["index.mjs", "codex-bridge-launcher.mjs"].includes(path.basename(script ?? ""));
|
|
121
|
-
if (!existing || typeof existing !== "object" || Array.isArray(existing) || !ownedSource || !(supervised || direct)
|
|
119
|
+
if (!existing || typeof existing !== "object" || Array.isArray(existing) || !ownedSource || !(supervised || direct) || !isNodeLauncher(existing.command)
|
|
122
120
|
|| existing.type !== undefined && existing.type !== "stdio" || ["url", "httpUrl", "serverUrl"].some((key) => existing[key] !== undefined)) {
|
|
123
|
-
throw
|
|
121
|
+
throw bridgeEntrypointConflict(`Desktop ${entryName}`, nodeBin, [path.join(root, "src", "mcp-supervisor.mjs"), "index.mjs"]);
|
|
124
122
|
}
|
|
125
123
|
if (existing.env !== undefined && (!existing.env || typeof existing.env !== "object" || Array.isArray(existing.env)
|
|
126
124
|
|| Object.values(existing.env).some((value) => typeof value !== "string"))) throw new Error(`Desktop ${entryName} environment must contain string values`);
|
|
@@ -193,13 +191,20 @@ if (setup) cfg.preferences = { ...cfg.preferences, menuBarEnabled: true };
|
|
|
193
191
|
createReleaseSnapshot(root, { cache: snapshotRoot({ ...process.env, ...cfg.mcpServers[entryName].env }) });
|
|
194
192
|
|
|
195
193
|
const changed = original === null || JSON.stringify(JSON.parse(original.data.toString("utf8"))) !== JSON.stringify(cfg);
|
|
194
|
+
function configSibling(suffix) {
|
|
195
|
+
const directory = path.resolve(path.dirname(cfgPath));
|
|
196
|
+
const file = path.resolve(directory, `${path.basename(cfgPath)}.${suffix}`);
|
|
197
|
+
const relative = path.relative(directory, file);
|
|
198
|
+
if (path.isAbsolute(relative) || relative === ".." || relative.startsWith(`..${path.sep}`) || path.dirname(file) !== directory) throw new Error("Unsafe Desktop configuration sibling path");
|
|
199
|
+
return file;
|
|
200
|
+
}
|
|
201
|
+
|
|
196
202
|
if (changed) {
|
|
197
|
-
const backup =
|
|
203
|
+
const backup = configSibling(`bak-${new Date().toISOString().slice(0, 10)}-codexbridge-${randomUUID()}`);
|
|
198
204
|
if (original) {
|
|
199
|
-
|
|
200
|
-
catch (error) { if (error.code !== "EEXIST") throw error; }
|
|
205
|
+
fs.writeFileSync(backup, original.data, { flag: "wx", mode: 0o600 });
|
|
201
206
|
}
|
|
202
|
-
const temporary = `${
|
|
207
|
+
const temporary = configSibling(`${randomUUID()}.tmp`);
|
|
203
208
|
try {
|
|
204
209
|
fs.writeFileSync(temporary, `${JSON.stringify(cfg, null, 2)}\n`, { flag: "wx", mode: 0o600 });
|
|
205
210
|
const current = readConfig();
|
|
@@ -7,7 +7,7 @@ import { fileURLToPath } from "node:url";
|
|
|
7
7
|
import { PLATFORM_LABEL, resolveCodexBin, spawnEnv } from "../src/platform.mjs";
|
|
8
8
|
import { exitForVersionRequest } from "../src/cli-version.mjs";
|
|
9
9
|
import { desktopTasksConfigured } from "../src/native-relay.mjs";
|
|
10
|
-
import { applyStdioMcpRegistration, codexMcpRegistration } from "../src/codex-mcp-registration.mjs";
|
|
10
|
+
import { applyStdioMcpRegistration, codexMcpRegistration, readCodexMcpServer } from "../src/codex-mcp-registration.mjs";
|
|
11
11
|
import { createReleaseSnapshot, snapshotRoot } from "../src/release-snapshot.mjs";
|
|
12
12
|
import { codexAccessPaths, enableCodexFullAccess, fullAccessEnabled } from "../src/codex-full-access.mjs";
|
|
13
13
|
import { stableInstallPath, stableNodeBin } from "../src/install-paths.mjs";
|
|
@@ -37,10 +37,7 @@ if (remove) {
|
|
|
37
37
|
process.exit(0);
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
const
|
|
41
|
-
if (!Array.isArray(servers)) throw new Error("Codex returned an invalid MCP inventory; existing configuration was not changed");
|
|
42
|
-
const existing = servers.some((server) => server.name === serverName)
|
|
43
|
-
? JSON.parse(run(["mcp", "get", serverName, "--json"])) : null;
|
|
40
|
+
const existing = readCodexMcpServer(run, serverName);
|
|
44
41
|
const registration = codexMcpRegistration({ name: serverName, existing, node: nodeBin, entry, entryArgs, env: process.env, desktopOnly: desktopTasksConfigured() });
|
|
45
42
|
createReleaseSnapshot(root, { cache: snapshotRoot({ ...process.env, ...registration.environment }) });
|
|
46
43
|
const applied = applyStdioMcpRegistration({ name: serverName, registration, existing, run, configPath: codexAccessPaths().config });
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
spawnEnv,
|
|
18
18
|
} from "../src/platform.mjs";
|
|
19
19
|
import { exitForVersionRequest } from "../src/cli-version.mjs";
|
|
20
|
-
import { applyStdioMcpRegistration, stdioMcpRegistration } from "../src/codex-mcp-registration.mjs";
|
|
20
|
+
import { applyStdioMcpRegistration, readCodexMcpServer, stdioMcpRegistration } from "../src/codex-mcp-registration.mjs";
|
|
21
21
|
import { createReleaseSnapshot, snapshotRoot } from "../src/release-snapshot.mjs";
|
|
22
22
|
import { codexAccessPaths, enableCodexFullAccess, fullAccessEnabled } from "../src/codex-full-access.mjs";
|
|
23
23
|
import { stableInstallPath } from "../src/install-paths.mjs";
|
|
@@ -25,7 +25,7 @@ import { removeRelayFile } from "../src/native-relay-files.mjs";
|
|
|
25
25
|
|
|
26
26
|
exitForVersionRequest(import.meta.url);
|
|
27
27
|
|
|
28
|
-
const VERSION = "1.26.
|
|
28
|
+
const VERSION = "1.26.9";
|
|
29
29
|
const root = stableInstallPath(path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."));
|
|
30
30
|
const entry = path.join(root, "src", "mcp-supervisor.mjs");
|
|
31
31
|
const entryArgs = ["native-relay-companion.mjs"];
|
|
@@ -69,10 +69,7 @@ if (!IS_MACOS && !IS_WINDOWS) {
|
|
|
69
69
|
console.log("claude-bridge will keep using the app-server path here.");
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
const
|
|
73
|
-
if (!Array.isArray(servers)) throw new Error("Codex returned an invalid MCP inventory; existing configuration was not changed");
|
|
74
|
-
const existingServer = servers.some((server) => server.name === serverName)
|
|
75
|
-
? JSON.parse(run(["mcp", "get", serverName, "--json"])) : null;
|
|
72
|
+
const existingServer = readCodexMcpServer(run, serverName);
|
|
76
73
|
/**
|
|
77
74
|
* Registering under whichever Node happens to run this installer is what made
|
|
78
75
|
* the companion unreachable on macOS: Codex Desktop rejects a peer whose
|
|
@@ -105,7 +102,7 @@ if (runtime.source === "process.execPath" && hasCodexDesktopApp()) {
|
|
|
105
102
|
console.log("runtime shipped inside Codex Desktop if that happens.");
|
|
106
103
|
}
|
|
107
104
|
console.log(`registered MCP server "${serverName}" with Codex (${applied.mode === "retarget" ? "existing entry retargeted in place; its other settings were kept" : "codex mcp add"}):`);
|
|
108
|
-
console.log(run(["mcp", "get", serverName]));
|
|
105
|
+
if (!process.argv.includes("--setup")) console.log(run(["mcp", "get", serverName]));
|
|
109
106
|
if (applied.notice) console.log(`NOTE: ${applied.notice}`);
|
|
110
107
|
else if (ensureEnvVars.length) console.log(`env_vars includes ${ensureEnvVars.join(", ")}; the temporary pipe path itself is never saved.`);
|
|
111
108
|
|
package/scripts/setup.mjs
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { exitForVersionRequest } from "../src/cli-version.mjs";
|
|
3
|
-
import { printSetup, runSetup } from "../src/setup.mjs";
|
|
3
|
+
import { printSetup, runSetup, setupFailureResult } from "../src/setup.mjs";
|
|
4
4
|
|
|
5
5
|
exitForVersionRequest(import.meta.url);
|
|
6
6
|
try {
|
|
7
|
-
const
|
|
7
|
+
const onProgress = process.argv.includes("--progress") ? (step) => process.stderr.write(`setup-progress: ${step}\n`) : undefined;
|
|
8
|
+
const result = runSetup({ args: process.argv.slice(2), onProgress });
|
|
8
9
|
printSetup(result, { json: process.argv.includes("--json") });
|
|
9
10
|
if (result.configured === false && !result.dryRun) process.exitCode = 1;
|
|
10
11
|
} catch (error) {
|
|
11
|
-
process.
|
|
12
|
-
|
|
12
|
+
if (process.argv.includes("--json")) printSetup(setupFailureResult(error), { json: true });
|
|
13
|
+
else {
|
|
14
|
+
process.stderr.write(`Bridge setup: ${error.message}\n`);
|
|
15
|
+
process.stderr.write("Bridge configuration was not completed. Run codex-bridge-setup to retry after resolving the reported error.\n");
|
|
16
|
+
}
|
|
13
17
|
process.exitCode = 1;
|
|
14
18
|
}
|
package/scripts/update.mjs
CHANGED
|
@@ -12,6 +12,7 @@ import { isMainModule } from "../src/install-paths.mjs";
|
|
|
12
12
|
import { readRegularConfigFile } from "../src/project-policy.mjs";
|
|
13
13
|
import { exitForVersionRequest } from "../src/cli-version.mjs";
|
|
14
14
|
import { bridgeDiagnostic, withBridgeDiagnostic } from "../src/recipient-preflight.mjs";
|
|
15
|
+
import { SETUP_INSPECTION, setupFailureSummary } from "../src/setup.mjs";
|
|
15
16
|
|
|
16
17
|
const PACKAGE = "@minhspark/codex-mcp-bridge";
|
|
17
18
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
@@ -64,9 +65,18 @@ export function resolveUpdateNpm(env = process.env) {
|
|
|
64
65
|
throw failure("UPDATE_NPM_NOT_FOUND", "A supported npm CLI could not be located. Run this updater through npm exec.", "preflight");
|
|
65
66
|
}
|
|
66
67
|
|
|
67
|
-
|
|
68
|
+
const SETUP_PROGRESS = /^setup-progress: ([^\u0000-\u001f\u007f]{1,120})$/;
|
|
69
|
+
|
|
70
|
+
function execute(command, args, { onStderrLine, ...options }) {
|
|
68
71
|
return new Promise(resolve => {
|
|
69
|
-
execFile(command, args, options, (error, stdout, stderr) => resolve({ status: error ? 1 : 0, error, stdout, stderr }));
|
|
72
|
+
const child = execFile(command, args, options, (error, stdout, stderr) => resolve({ status: error ? 1 : 0, error, stdout, stderr }));
|
|
73
|
+
if (!onStderrLine || !child.stderr) return;
|
|
74
|
+
let pending = "";
|
|
75
|
+
child.stderr.on("data", chunk => {
|
|
76
|
+
const lines = (pending + chunk).split(/\r?\n/);
|
|
77
|
+
pending = lines.pop();
|
|
78
|
+
for (const line of lines) onStderrLine(line);
|
|
79
|
+
});
|
|
70
80
|
});
|
|
71
81
|
}
|
|
72
82
|
|
|
@@ -86,7 +96,7 @@ function neutralDirectory(roots, env) {
|
|
|
86
96
|
throw failure("UPDATE_DIRECTORY_UNAVAILABLE", "No working directory outside the package installation is available.", "preflight");
|
|
87
97
|
}
|
|
88
98
|
|
|
89
|
-
function childFailure(result, phase) {
|
|
99
|
+
function childFailure(result, phase, setupCommand = false) {
|
|
90
100
|
const match = /npm\s+(?:ERR!|error)\s+code\s+([A-Z][A-Z0-9_]{0,79}|\d{1,10})\b/.exec(String(result?.stderr ?? ""));
|
|
91
101
|
const signal = result?.error?.signal ?? result?.signal;
|
|
92
102
|
const value = match?.[1] ?? result?.error?.code ?? (signal ? null : result?.status ?? result?.exitCode);
|
|
@@ -96,11 +106,13 @@ function childFailure(result, phase) {
|
|
|
96
106
|
: /^SIG[A-Z0-9]{1,20}$/.test(signal ?? "") ? `SIGNAL_${signal}` : null;
|
|
97
107
|
const cause = code ? Object.assign(new Error("The child command reported a failure."), { code }) : undefined;
|
|
98
108
|
const permissionDenied = phase === "installation" && ["EPERM", "EACCES"].includes(code);
|
|
109
|
+
const setupSummary = setupCommand && phase === "verification" && !signal && !timedOut ? setupFailureSummary(result?.stdout) : null;
|
|
99
110
|
return failure(permissionDenied ? "UPDATE_INSTALL_PERMISSION" : code === "EBUSY" ? "UPDATE_PACKAGE_BUSY" : timedOut ? "UPDATE_COMMAND_TIMEOUT" : phase === "verification" ? "UPDATE_SETUP_INCOMPLETE" : "UPDATE_NPM_FAILED",
|
|
100
111
|
permissionDenied ? "npm could not write to the installation directory. Run codex-bridge-update --user to install and configure the bridge in your user account without administrator rights. Existing work and receipts were preserved."
|
|
101
112
|
: timedOut ? `The update command exceeded its deadline. ${RETRY}`
|
|
102
113
|
: code === "EBUSY" ? `The installed package is still held open by another process. No process was stopped. ${RETRY}`
|
|
103
|
-
:
|
|
114
|
+
: setupSummary ? `The global bridge settings are incomplete. ${setupSummary} ${RETRY}`
|
|
115
|
+
: `The ${phase === "verification" ? "installed setup verification" : "npm command"} did not complete. ${setupCommand ? `${SETUP_INSPECTION} ` : ""}${RETRY}`, phase, cause);
|
|
104
116
|
}
|
|
105
117
|
|
|
106
118
|
function serviceQuiescent(config) {
|
|
@@ -138,13 +150,13 @@ export async function runUpdate({ args = [], env = process.env, platform = proce
|
|
|
138
150
|
const prefixArgs = userPrefix ? ["--prefix", userPrefix] : [];
|
|
139
151
|
let cwd = neutralDirectory([root], env);
|
|
140
152
|
let childEnv = Object.fromEntries(Object.entries(env).filter(([name]) => !/^AGENT_RELAY_(?!DIRECTORY$)/i.test(name)));
|
|
141
|
-
const command = async (arguments_, phase, timeout = 600000, environment = childEnv) => {
|
|
153
|
+
const command = async (arguments_, phase, timeout = 600000, environment = childEnv, setupCommand = false, onStderrLine) => {
|
|
142
154
|
let result;
|
|
143
155
|
try {
|
|
144
156
|
result = await run(process.execPath, arguments_, { env: environment, cwd, encoding: "utf8", windowsHide: true,
|
|
145
|
-
shell: false, timeout, maxBuffer: 8 * 1024 * 1024, stdio: ["ignore", "pipe", "pipe"] });
|
|
146
|
-
} catch (error) { throw childFailure({ error, stderr: error.stderr }, phase); }
|
|
147
|
-
if (result?.error || (result?.status ?? result?.exitCode ?? 0) !== 0) throw childFailure(result, phase);
|
|
157
|
+
shell: false, timeout, maxBuffer: 8 * 1024 * 1024, stdio: ["ignore", "pipe", "pipe"], ...(onStderrLine ? { onStderrLine } : {}) });
|
|
158
|
+
} catch (error) { throw childFailure({ error, stdout: error.stdout, stderr: error.stderr }, phase, setupCommand); }
|
|
159
|
+
if (result?.error || (result?.status ?? result?.exitCode ?? 0) !== 0) throw childFailure(result, phase, setupCommand);
|
|
148
160
|
return String(result?.stdout ?? "");
|
|
149
161
|
};
|
|
150
162
|
const modules = (await command([npm, "root", "--global", ...prefixArgs], "preflight", 30000)).trim();
|
|
@@ -200,13 +212,13 @@ export async function runUpdate({ args = [], env = process.env, platform = proce
|
|
|
200
212
|
await command([npm, "install", "--global", "--prefix", prefix, `${PACKAGE}@${version}`, "--ignore-scripts"],
|
|
201
213
|
"installation", 600000, { ...childEnv, CODEX_BRIDGE_SKIP_SETUP: "1" });
|
|
202
214
|
if (packageVersion(globalRoot) !== version) throw failure("UPDATE_VERSION_MISMATCH", "The global package version does not match the updater version. Rerun the same update command.", "verification");
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
215
|
+
onProgress("Applying and verifying managed settings from the installed bridge. Each step can take up to a minute while Codex CLI reads its configuration.");
|
|
216
|
+
const setupProgress = line => { const step = SETUP_PROGRESS.exec(line)?.[1]; if (step) onProgress(` ${step}`); };
|
|
217
|
+
let setup;
|
|
218
|
+
try { setup = JSON.parse(await command([path.join(globalRoot, "scripts", "setup.mjs"), "--json", "--progress"], "verification", 600000, childEnv, true, setupProgress)); }
|
|
219
|
+
catch (error) { if (error?.code) throw error; throw failure("UPDATE_SETUP_INCOMPLETE", `Global setup did not return a verifiable result. ${SETUP_INSPECTION} ${RETRY}`, "verification"); }
|
|
220
|
+
if (setup?.configured !== true) throw failure("UPDATE_SETUP_INCOMPLETE", `The global bridge settings are incomplete. ${setupFailureSummary(setup) ?? SETUP_INSPECTION} ${RETRY}`, "verification");
|
|
221
|
+
onProgress("Verifying the owned relay runtime.");
|
|
210
222
|
const service = await awaitService(globalRoot, false, "verification");
|
|
211
223
|
return { updated: true, configured: true, version, previousVersion, migrated, service, opensApps: false, opensChats: false };
|
|
212
224
|
}
|
package/src/claude-bridge.mjs
CHANGED
|
@@ -31,7 +31,7 @@ import { BACKGROUND_WORKER_GUIDANCE, registerBackgroundTools } from "./backgroun
|
|
|
31
31
|
|
|
32
32
|
exitForVersionRequest(import.meta.url);
|
|
33
33
|
|
|
34
|
-
const VERSION = "1.26.
|
|
34
|
+
const VERSION = "1.26.9";
|
|
35
35
|
void import("./telemetry.mjs").then(({ startUsageReporting }) => startUsageReporting({ version: VERSION })).catch(() => {});
|
|
36
36
|
const FORWARD_MIN_INTERVAL_MS = 5000;
|
|
37
37
|
const FORWARD_MAX_PER_SESSION = 50;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { bridgeEntrypointConflict, isBridgeSource, isNodeLauncher } from "./install-paths.mjs";
|
|
4
5
|
|
|
5
6
|
const PACKAGE_SOURCE = path.dirname(fileURLToPath(import.meta.url));
|
|
6
7
|
|
|
@@ -31,7 +32,7 @@ function configurationObject(value, label) {
|
|
|
31
32
|
return value;
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
function initialBridgeEntry(entry, root, label) {
|
|
35
|
+
function initialBridgeEntry(entry, root, node, label) {
|
|
35
36
|
configurationObject(entry, label);
|
|
36
37
|
if (entry.type !== undefined && entry.type !== "stdio" || ["url", "serverUrl", "httpUrl"].some((key) => entry[key] !== undefined)) {
|
|
37
38
|
throw new Error(`${label} has a conflicting transport; no configuration was changed`);
|
|
@@ -41,11 +42,9 @@ function initialBridgeEntry(entry, root, label) {
|
|
|
41
42
|
const leaf = typeof script === "string" ? path.basename(script) : null;
|
|
42
43
|
const supervised = args?.length === 2 && leaf === "mcp-supervisor.mjs" && args[1] === "index.mjs";
|
|
43
44
|
const direct = args?.length === 1 && ["index.mjs", "codex-bridge-launcher.mjs"].includes(leaf);
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (!Array.isArray(args) || !(supervised || direct) || !ownSource) {
|
|
48
|
-
throw new Error(`${label} is already configured with an unrecognized or customized entrypoint; no configuration was changed`);
|
|
45
|
+
const ownSource = isBridgeSource(script, [PACKAGE_SOURCE, path.join(root, "src")], ["index.mjs"]);
|
|
46
|
+
if (!Array.isArray(args) || !(supervised || direct) || !ownSource || !isNodeLauncher(entry.command)) {
|
|
47
|
+
throw bridgeEntrypointConflict(label, node, [path.join(root, "src", "mcp-supervisor.mjs"), "index.mjs"]);
|
|
49
48
|
}
|
|
50
49
|
if (entry.env !== undefined) {
|
|
51
50
|
configurationObject(entry.env, `${label} environment`);
|
|
@@ -67,7 +66,7 @@ export function planInitialCodeRegistration({ code, desktop, root, node, environ
|
|
|
67
66
|
const sharedNative = desktop.mcpServers?.[name] ?? desktop.mcpServers?.[`${name}-desktop`];
|
|
68
67
|
const inheritedShared = originalCode === undefined && sharedNative?.env?.CODEX_BRIDGE_DESKTOP_TASKS === "1";
|
|
69
68
|
const source = inheritedShared ? sharedNative : originalCode;
|
|
70
|
-
if (source !== undefined) initialBridgeEntry(source, root, `Claude Code ${name}`);
|
|
69
|
+
if (source !== undefined) initialBridgeEntry(source, root, node, `Claude Code ${name}`);
|
|
71
70
|
const defaults = Object.fromEntries(INITIAL_ENVIRONMENT_KEYS.filter((key) => environment[key] !== undefined).map((key) => [key, environment[key]]));
|
|
72
71
|
if (Object.values(defaults).some((value) => typeof value !== "string")) throw new Error("Bridge environment defaults must be strings");
|
|
73
72
|
const scopeKeys = ["CODEX_BRIDGE_THREAD_POLICY", "CODEX_BRIDGE_ALLOWED_ROOTS", "CODEX_BRIDGE_PROJECT_POLICY", "CODEX_BRIDGE_ALLOWED_THREADS", "CODEX_BRIDGE_HARDENED"];
|
|
@@ -94,7 +93,7 @@ export function planInitialCodeRegistration({ code, desktop, root, node, environ
|
|
|
94
93
|
if (JSON.stringify(originalCode) !== JSON.stringify(replacement)) changes.push({ kind: originalCode ? "update-user-code-entry" : "create-user-code-entry", name });
|
|
95
94
|
const shared = updatedDesktop.mcpServers?.[name];
|
|
96
95
|
if (shared !== undefined) {
|
|
97
|
-
initialBridgeEntry(shared, root, `Shared Desktop ${name}`);
|
|
96
|
+
initialBridgeEntry(shared, root, node, `Shared Desktop ${name}`);
|
|
98
97
|
const destination = `${name}-desktop`;
|
|
99
98
|
if (Object.hasOwn(updatedDesktop.mcpServers, destination)) {
|
|
100
99
|
throw new Error(`Shared Desktop ${destination} already exists; resolve the name collision before installation`);
|
|
@@ -1,9 +1,32 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
3
4
|
import { retargetMcpServerToml } from "./codex-mcp-toml.mjs";
|
|
5
|
+
import { backupRegularConfigFile } from "./project-policy.mjs";
|
|
4
6
|
|
|
5
7
|
const PRESERVED_FIELDS = ["enabled", "disabled_reason", "enabled_tools", "disabled_tools", "startup_timeout_sec", "tool_timeout_sec"];
|
|
6
8
|
|
|
9
|
+
export const CODEX_MCP_TIMEOUT_MS = 120000;
|
|
10
|
+
|
|
11
|
+
function missingServerError(error, name) {
|
|
12
|
+
const output = `${error?.stderr ?? ""}\n${error?.stdout ?? ""}\n${error?.message ?? ""}`;
|
|
13
|
+
return output.includes(`No MCP server named '${name}' found`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function readCodexMcpServer(run, name) {
|
|
17
|
+
let output;
|
|
18
|
+
try { output = run(["mcp", "get", name, "--json"]); }
|
|
19
|
+
catch (error) {
|
|
20
|
+
if (missingServerError(error, name)) return null;
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
const entry = JSON.parse(output);
|
|
24
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry) || entry.name !== name) {
|
|
25
|
+
throw new Error(`Codex returned an invalid ${name} entry; existing configuration was not changed`);
|
|
26
|
+
}
|
|
27
|
+
return entry;
|
|
28
|
+
}
|
|
29
|
+
|
|
7
30
|
function sameEnvironment(left = {}, right = {}) {
|
|
8
31
|
const keys = (value) => Object.keys(value ?? {}).sort();
|
|
9
32
|
return JSON.stringify(keys(left)) === JSON.stringify(keys(right)) && keys(left).every((key) => left[key] === right[key]);
|
|
@@ -99,12 +122,19 @@ function verificationErrors(before, after, registration, ensureEnvVars) {
|
|
|
99
122
|
|
|
100
123
|
export function applyStdioMcpRegistration({ name, registration, existing, run, configPath, ensureEnvVars = [] }) {
|
|
101
124
|
const original = readConfigSnapshot(configPath);
|
|
125
|
+
let backedUp = false;
|
|
126
|
+
const ensureBackup = () => {
|
|
127
|
+
if (!original || backedUp) return;
|
|
128
|
+
backupRegularConfigFile(original.path, `${original.path}.backup-${randomUUID()}`, original.contents, 16 * 1024 * 1024);
|
|
129
|
+
backedUp = true;
|
|
130
|
+
};
|
|
102
131
|
const target = { command: registration.command, args: registration.commandArgs, ensureEnvVars };
|
|
103
132
|
const editInPlace = () => {
|
|
104
133
|
const current = readConfigSnapshot(configPath);
|
|
105
134
|
if (!current) throw new Error("The Codex configuration file does not exist");
|
|
106
135
|
const next = retargetMcpServerToml(current.contents, name, target);
|
|
107
136
|
if (next === current.contents) return false;
|
|
137
|
+
ensureBackup();
|
|
108
138
|
writeConfigAtomically(current, next);
|
|
109
139
|
return true;
|
|
110
140
|
};
|
|
@@ -120,11 +150,12 @@ export function applyStdioMcpRegistration({ name, registration, existing, run, c
|
|
|
120
150
|
mutated = edited;
|
|
121
151
|
envVarsAdded = true;
|
|
122
152
|
} catch (error) {
|
|
123
|
-
if (!registration.args) throw new Error(`${error.message}. ${registration.manual}`);
|
|
153
|
+
if (!registration.args || backedUp) throw new Error(`${error.message}. ${registration.manual}`);
|
|
124
154
|
mode = "add";
|
|
125
155
|
}
|
|
126
156
|
}
|
|
127
157
|
if (mode === "add") {
|
|
158
|
+
ensureBackup();
|
|
128
159
|
run(registration.args);
|
|
129
160
|
mutated = true;
|
|
130
161
|
if (!envVarsAdded) {
|
|
@@ -33,6 +33,31 @@ function directoryStat(directory) {
|
|
|
33
33
|
return stat;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
function retiredRoot(root, candidates) {
|
|
37
|
+
let initial;
|
|
38
|
+
try { initial = fs.lstatSync(root, { bigint: true }); }
|
|
39
|
+
catch (error) { if (isMissing(error)) return false; throw error; }
|
|
40
|
+
let directory = root;
|
|
41
|
+
let destination;
|
|
42
|
+
if (initial.isSymbolicLink()) {
|
|
43
|
+
destination = fs.realpathSync.native(root);
|
|
44
|
+
directory = candidates.find((candidate) => {
|
|
45
|
+
if (candidate === root) return false;
|
|
46
|
+
try {
|
|
47
|
+
directoryStat(candidate);
|
|
48
|
+
return fs.realpathSync.native(candidate).toLowerCase() === destination.toLowerCase();
|
|
49
|
+
} catch (error) { if (isMissing(error) || error.message === "invalid directory") return false; throw error; }
|
|
50
|
+
});
|
|
51
|
+
if (!directory) throw new Error("invalid directory");
|
|
52
|
+
}
|
|
53
|
+
const before = directoryStat(directory);
|
|
54
|
+
const entries = fs.readdirSync(directory, { withFileTypes: true });
|
|
55
|
+
const retired = entries.length > 0 && entries.every((entry) => entry.isFile() && /^\.user-data-left(?:\.|$)/i.test(entry.name));
|
|
56
|
+
if (!sameVersion(before, directoryStat(directory)) || !sameVersion(initial, fs.lstatSync(root, { bigint: true }))
|
|
57
|
+
|| destination !== undefined && fs.realpathSync.native(root) !== destination) throw new Error("root changed");
|
|
58
|
+
return retired;
|
|
59
|
+
}
|
|
60
|
+
|
|
36
61
|
function candidateRoots({ platform, env, root }) {
|
|
37
62
|
const selected = root !== undefined ? root : env.CLAUDE_DESKTOP_USER_DATA;
|
|
38
63
|
if (selected !== undefined) return [absoluteDirectory(selected)];
|
|
@@ -42,30 +67,42 @@ function candidateRoots({ platform, env, root }) {
|
|
|
42
67
|
: env.XDG_CONFIG_HOME || path.join(home, ".config");
|
|
43
68
|
const roots = [absoluteDirectory(path.join(config, "Claude"))];
|
|
44
69
|
if (platform === "win32") {
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
70
|
+
const local = absoluteDirectory(env.LOCALAPPDATA || path.join(home, "AppData", "Local"));
|
|
71
|
+
for (const [name, suffix] of [["Packages", ["LocalCache", "Roaming", "Claude"]], ["Claude-Data", []]]) {
|
|
72
|
+
const parent = path.resolve(local, name);
|
|
73
|
+
const relative = path.relative(local, parent);
|
|
74
|
+
if (path.isAbsolute(relative) || relative === ".." || relative.startsWith(`..${path.sep}`)) throw new Error("invalid package directory");
|
|
75
|
+
let entries;
|
|
76
|
+
try {
|
|
77
|
+
directoryStat(parent);
|
|
78
|
+
entries = fs.readdirSync(parent, { withFileTypes: true });
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if (!isMissing(error)) throw error;
|
|
81
|
+
entries = [];
|
|
82
|
+
}
|
|
83
|
+
if (entries.length > MAX_PACKAGES) throw new Error("package scan limit");
|
|
84
|
+
for (const entry of entries) {
|
|
85
|
+
if (!PACKAGE_NAME.test(entry.name)) continue;
|
|
86
|
+
const packageRoot = path.join(parent, entry.name);
|
|
87
|
+
directoryStat(packageRoot);
|
|
88
|
+
roots.push(path.join(packageRoot, ...suffix));
|
|
89
|
+
}
|
|
60
90
|
}
|
|
61
91
|
}
|
|
62
92
|
const unique = [...new Map(roots.map((value) => [platform === "win32" ? value.toLowerCase() : value, value])).values()].sort();
|
|
63
93
|
if (unique.length > MAX_ROOTS) throw new Error("root scan limit");
|
|
64
|
-
return unique;
|
|
94
|
+
if (platform !== "win32") return unique;
|
|
95
|
+
const active = unique.filter((candidate) => !retiredRoot(candidate, unique));
|
|
96
|
+
if (active.length === unique.length) return active;
|
|
97
|
+
return active.filter((candidate) => {
|
|
98
|
+
try { directoryStat(candidate); return true; }
|
|
99
|
+
catch (error) { if (isMissing(error)) return false; throw error; }
|
|
100
|
+
});
|
|
65
101
|
}
|
|
66
102
|
|
|
67
103
|
export function resolveClaudeDesktopConfigRoot({ platform = process.platform, env = process.env } = {}) {
|
|
68
104
|
const roots = candidateRoots({ platform, env });
|
|
105
|
+
if (!roots.length) throw Object.assign(new Error("No active Claude Desktop data root remains after migration. Start Claude Desktop or set CLAUDE_DESKTOP_USER_DATA to its active data directory, then rerun setup."), { code: "CLAUDE_DESKTOP_DATA_ROOT_UNAVAILABLE" });
|
|
69
106
|
if (roots.length === 1) return roots[0];
|
|
70
107
|
const account = readClaudeAccountContext({ platform, env });
|
|
71
108
|
if (account.status === "verified" || account.status === "signed_out") return account.root;
|
|
@@ -79,7 +116,7 @@ export function resolveClaudeDesktopConfigRoot({ platform = process.platform, en
|
|
|
79
116
|
const ordinary = path.resolve(env.APPDATA || path.join(home, "AppData", "Roaming"), "Claude");
|
|
80
117
|
return roots.find((root) => root.toLowerCase() !== ordinary.toLowerCase());
|
|
81
118
|
}
|
|
82
|
-
throw new Error("Multiple Claude Desktop data roots exist. Set CLAUDE_DESKTOP_USER_DATA to the application's active data directory, then rerun setup.");
|
|
119
|
+
throw Object.assign(new Error("Multiple Claude Desktop data roots exist. Set CLAUDE_DESKTOP_USER_DATA to the application's active data directory, then rerun setup."), { code: "CLAUDE_DESKTOP_DATA_ROOT_AMBIGUOUS" });
|
|
83
120
|
}
|
|
84
121
|
|
|
85
122
|
function readConfig(root) {
|
package/src/index.mjs
CHANGED
|
@@ -44,7 +44,7 @@ import { BACKGROUND_WORKER_GUIDANCE, registerBackgroundTools } from "./backgroun
|
|
|
44
44
|
|
|
45
45
|
exitForVersionRequest(import.meta.url);
|
|
46
46
|
|
|
47
|
-
const VERSION = "1.26.
|
|
47
|
+
const VERSION = "1.26.9";
|
|
48
48
|
void import("./telemetry.mjs").then(({ startUsageReporting }) => startUsageReporting({ version: VERSION })).catch(() => {});
|
|
49
49
|
const log = (msg) => process.stderr.write(`[codex-mcp-bridge] ${msg}\n`);
|
|
50
50
|
|
package/src/install-paths.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { readRegularConfigFile } from "./project-policy.mjs";
|
|
4
5
|
|
|
5
6
|
const KEG_PATH = /^(\/.*?)\/Cellar\/([^/]+)\/[^/]+(\/.*)?$/;
|
|
6
7
|
|
|
@@ -36,6 +37,40 @@ export function stableNodeBin(env = process.env) {
|
|
|
36
37
|
return stableInstallPath(process.execPath, env);
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
export function isNodeLauncher(command) {
|
|
41
|
+
return typeof command === "string" && !command.includes("\0")
|
|
42
|
+
&& /^(?:node|node\.exe)$/i.test(path.basename(command))
|
|
43
|
+
&& (path.isAbsolute(command) || command === path.basename(command));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function isBridgeSource(script, sources = [], workers = []) {
|
|
47
|
+
if (typeof script !== "string" || !path.isAbsolute(script) || script.includes("\0")) return false;
|
|
48
|
+
const source = path.dirname(path.resolve(script));
|
|
49
|
+
if ([...sources].some(value => path.relative(value, source) === "")) return true;
|
|
50
|
+
if (path.basename(source) !== "src") return false;
|
|
51
|
+
const root = path.dirname(source);
|
|
52
|
+
try {
|
|
53
|
+
for (const directory of [root, source]) {
|
|
54
|
+
const stat = fs.lstatSync(directory);
|
|
55
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) return false;
|
|
56
|
+
}
|
|
57
|
+
const metadata = JSON.parse(readRegularConfigFile(path.join(root, "package.json"), { maxBytes: 128 * 1024 }));
|
|
58
|
+
if (metadata?.name !== "@minhspark/codex-mcp-bridge" || typeof metadata.version !== "string"
|
|
59
|
+
|| !/^\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.+-]+)?$/.test(metadata.version)) return false;
|
|
60
|
+
for (const name of new Set([path.basename(script), ...workers])) {
|
|
61
|
+
if (typeof name !== "string" || path.basename(name) !== name) return false;
|
|
62
|
+
const stat = fs.lstatSync(path.join(source, name));
|
|
63
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1) return false;
|
|
64
|
+
}
|
|
65
|
+
return true;
|
|
66
|
+
} catch { return false; }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function bridgeEntrypointConflict(label, node, args, detail = "an unrecognized or customized entrypoint") {
|
|
70
|
+
const code = "SETUP_ENTRYPOINT_CONFLICT";
|
|
71
|
+
return Object.assign(new Error(`${code}: ${label} has ${detail}; no configuration was changed. Restore the previous bridge package so setup can verify its package.json, or back up this registration and update only command=${JSON.stringify(node)} and args=${JSON.stringify(args)}. Preserve its other settings, then rerun setup.`), { code });
|
|
72
|
+
}
|
|
73
|
+
|
|
39
74
|
/**
|
|
40
75
|
* True when `entry` names the running main module, however it was reached.
|
|
41
76
|
* Node resolves the main module with the JavaScript realpath, which follows
|
|
@@ -43,7 +43,7 @@ import { remoteCodexSocketPath, remoteNativeAdapter, remoteRequestResponse } fro
|
|
|
43
43
|
|
|
44
44
|
exitForVersionRequest(import.meta.url);
|
|
45
45
|
|
|
46
|
-
const VERSION = "1.26.
|
|
46
|
+
const VERSION = "1.26.9";
|
|
47
47
|
const log = (msg) => process.stderr.write(`[native-relay] ${msg}\n`);
|
|
48
48
|
|
|
49
49
|
function errorResponse(code, message, sent, details) {
|
package/src/setup.mjs
CHANGED
|
@@ -8,13 +8,64 @@ import { resolveCodexBin, resolvePackagedCodexBin } from "./platform.mjs";
|
|
|
8
8
|
import { readRegularConfigFile } from "./project-policy.mjs";
|
|
9
9
|
import { stableInstallPath } from "./install-paths.mjs";
|
|
10
10
|
import { resolveClaudeDesktopConfigRoot } from "./desktop-account-context.mjs";
|
|
11
|
+
import { CODEX_MCP_TIMEOUT_MS } from "./codex-mcp-registration.mjs";
|
|
11
12
|
|
|
12
13
|
const ROOT = stableInstallPath(path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."));
|
|
13
14
|
const CODEX_PACKAGE = "@openai/codex@0.166.0";
|
|
14
15
|
const MAX_CONFIG = 16 * 1024 * 1024;
|
|
16
|
+
export const SETUP_INSPECTION = "Inspect locally with codex-bridge-setup --check --json.";
|
|
17
|
+
const SETUP_COMPONENTS = new Set([
|
|
18
|
+
"Setup preflight", "Claude Desktop data directory", "Codex CLI", "Codex / claude-bridge",
|
|
19
|
+
"Codex / codex-native-relay", "Codex / codex-native-relay and executor", "Codex / antigravity-bridge",
|
|
20
|
+
"Claude Code / codex-bridge", "Claude Desktop / codex-bridge-desktop", "Claude Code / antigravity-bridge",
|
|
21
|
+
"Claude Desktop / background preference", "Antigravity / agent-bridge", "Antigravity / codex-bridge sidecar",
|
|
22
|
+
"Antigravity / all components", "Native relay executor", "Claude registration names", "Independent background relay",
|
|
23
|
+
]);
|
|
24
|
+
const SETUP_FAILURES = Object.freeze({
|
|
25
|
+
CLAUDE_DESKTOP_DATA_ROOT_AMBIGUOUS: "Multiple active directories; set CLAUDE_DESKTOP_USER_DATA to the active Claude data directory.",
|
|
26
|
+
CLAUDE_DESKTOP_DATA_ROOT_UNAVAILABLE: "Data directory unavailable; check access and the CLAUDE_DESKTOP_USER_DATA override.",
|
|
27
|
+
SETUP_ENTRYPOINT_CONFLICT: "Existing entrypoint could not be verified; review the preserved registration before migration.",
|
|
28
|
+
SETUP_SIDECAR_DEFINITION_MISSING: "Sidecar settings remain without a verified definition; review both sidecar configuration files.",
|
|
29
|
+
SETUP_COMPONENT_INCOMPLETE: "Configuration could not be completed or verified.",
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
function failureCode(error) {
|
|
33
|
+
return typeof error?.code === "string" && /^[A-Z][A-Z0-9_]{0,79}$/.test(error.code) ? error.code : "SETUP_COMPONENT_INCOMPLETE";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function failedComponent(name, error) {
|
|
37
|
+
return { name, status: "incomplete", code: failureCode(error), detail: error.message };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function setupFailureResult(error) {
|
|
41
|
+
const selected = failureCode(error);
|
|
42
|
+
const code = Object.hasOwn(SETUP_FAILURES, selected) ? selected : "SETUP_COMPONENT_INCOMPLETE";
|
|
43
|
+
const name = code === "CLAUDE_DESKTOP_DATA_ROOT_AMBIGUOUS" || code === "CLAUDE_DESKTOP_DATA_ROOT_UNAVAILABLE"
|
|
44
|
+
? "Claude Desktop data directory" : "Setup preflight";
|
|
45
|
+
return { configured: false, liveVerified: false, components: [{ name, status: "incomplete", code, detail: SETUP_FAILURES[code] }], nextActions: [SETUP_INSPECTION] };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function setupFailureSummary(value) {
|
|
49
|
+
if (typeof value === "string") {
|
|
50
|
+
if (Buffer.byteLength(value) > 128 * 1024) return null;
|
|
51
|
+
try { value = JSON.parse(value); } catch { return null; }
|
|
52
|
+
}
|
|
53
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || value.configured !== false) return null;
|
|
54
|
+
const failures = new Map();
|
|
55
|
+
for (const items of [value.components, value.verification]) {
|
|
56
|
+
if (!Array.isArray(items)) continue;
|
|
57
|
+
for (const item of items.slice(0, 128)) {
|
|
58
|
+
if (!item || item.status !== "incomplete" || !SETUP_COMPONENTS.has(item.name) || failures.has(item.name)) continue;
|
|
59
|
+
const code = typeof item.code === "string" && Object.hasOwn(SETUP_FAILURES, item.code) ? item.code : "SETUP_COMPONENT_INCOMPLETE";
|
|
60
|
+
failures.set(item.name, `${item.name}: ${SETUP_FAILURES[code]}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (!failures.size) return null;
|
|
64
|
+
return `${SETUP_INSPECTION} ${[...failures.values()].slice(0, 6).join(" ")}${failures.size > 6 ? " Additional incomplete components are listed by that command." : ""}`;
|
|
65
|
+
}
|
|
15
66
|
|
|
16
67
|
export function setupArguments(args) {
|
|
17
|
-
const allowed = new Set(["--postinstall", "--check", "--dry-run", "--json", "--no-download", "--help"]);
|
|
68
|
+
const allowed = new Set(["--postinstall", "--check", "--dry-run", "--json", "--no-download", "--progress", "--help"]);
|
|
18
69
|
if (args.some((arg) => !allowed.has(arg))) throw new Error("Unknown setup option; use codex-bridge-setup --help");
|
|
19
70
|
if (args.includes("--check") && args.includes("--dry-run")) throw new Error("Use --check or --dry-run, not both");
|
|
20
71
|
return Object.fromEntries([...allowed].map((arg) => [arg.slice(2).replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()), args.includes(arg)]));
|
|
@@ -56,6 +107,7 @@ export function setupProcessFailure(command, args, result) {
|
|
|
56
107
|
try { structured = JSON.parse(String(result.stdout || "")); } catch {}
|
|
57
108
|
const validCode = value => typeof value === "string" && /^[A-Z][A-Z0-9_]{0,79}$/.test(value);
|
|
58
109
|
const codes = [structured?.code, structured?.service?.code, structured?.permissions?.code, structured?.diagnostic?.code,
|
|
110
|
+
...String(result.stderr || "").split(/\r?\n/).map(line => /^(?:Error: )?([A-Z][A-Z0-9_]{0,79}):/.exec(line)?.[1]).filter(code => Object.hasOwn(SETUP_FAILURES, code ?? "")),
|
|
59
111
|
...String(result.stderr || "").split(/\r?\n/).map(line => /^([A-Z][A-Z0-9_]{0,79}) \| /.exec(line)?.[1])].filter(validCode);
|
|
60
112
|
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)
|
|
61
113
|
&& !/Command failed|--env|password|secret|token|authorization/i.test(line));
|
|
@@ -97,9 +149,16 @@ export function setupEnvironment(env, paths, codex) {
|
|
|
97
149
|
CLAUDE_DESKTOP_CONFIG: paths.desktop, CODEX_EXE: codex, CODEX_BIN: codex };
|
|
98
150
|
}
|
|
99
151
|
|
|
152
|
+
function codexInventoryFailure(error) {
|
|
153
|
+
const detail = error?.message?.includes("ETIMEDOUT") || error?.code === "ETIMEDOUT"
|
|
154
|
+
? `Codex CLI did not list its MCP servers within ${CODEX_MCP_TIMEOUT_MS / 1000} seconds; the computer may be busy. Rerun the check when it is idle`
|
|
155
|
+
: error?.message ?? String(error);
|
|
156
|
+
return Object.assign(new Error(detail), error?.code ? { code: error.code } : {});
|
|
157
|
+
}
|
|
158
|
+
|
|
100
159
|
function configurationCheck(label, check, components) {
|
|
101
160
|
try { const details = check(); components.push({ name: label, status: "configured", ...details }); }
|
|
102
|
-
catch (error) { components.push(
|
|
161
|
+
catch (error) { components.push(failedComponent(label, error)); }
|
|
103
162
|
}
|
|
104
163
|
|
|
105
164
|
function assertEntry(entry, worker, root, { caller } = {}) {
|
|
@@ -107,7 +166,7 @@ function assertEntry(entry, worker, root, { caller } = {}) {
|
|
|
107
166
|
const args = entry.args ?? [];
|
|
108
167
|
const expected = ["index.mjs", "claude-bridge.mjs", "native-relay-companion.mjs", "antigravity-bridge.mjs", "antigravity-outbound-bridge.mjs"].includes(worker)
|
|
109
168
|
? [path.join(root, "src", "mcp-supervisor.mjs"), worker] : [path.join(root, "src", worker)];
|
|
110
|
-
if (JSON.stringify(args) !== JSON.stringify(expected)) throw new Error("MCP entry points to a different installation or unsupported launcher");
|
|
169
|
+
if (JSON.stringify(args) !== JSON.stringify(expected)) throw Object.assign(new Error("MCP entry points to a different installation or unsupported launcher"), { code: "SETUP_ENTRYPOINT_CONFLICT" });
|
|
111
170
|
if (caller && entry.env?.ANTIGRAVITY_BRIDGE_CALLER !== caller) throw new Error("Antigravity caller registration is missing or conflicting");
|
|
112
171
|
if (entry.enabled === false || entry.disabled === true) return { deliveryEnabled: false,
|
|
113
172
|
notices: ["This MCP registration is intentionally disabled. Its saved choice was preserved; delivery through this connector is disabled."] };
|
|
@@ -120,15 +179,16 @@ export function checkSetup({ env = process.env, platform = process.platform, roo
|
|
|
120
179
|
let inventory;
|
|
121
180
|
configurationCheck("Codex CLI", () => {
|
|
122
181
|
codex = ensureSetupCodex({ env, paths, platform, download: false, run, resolve }).binary;
|
|
123
|
-
inventory = JSON.parse(run(codex, ["mcp", "list", "--json"], setupEnvironment(env, paths, codex),
|
|
124
|
-
|
|
182
|
+
try { inventory = JSON.parse(run(codex, ["mcp", "list", "--json"], setupEnvironment(env, paths, codex), CODEX_MCP_TIMEOUT_MS)); }
|
|
183
|
+
catch (error) { throw codexInventoryFailure(error); }
|
|
184
|
+
if (!Array.isArray(inventory)) { inventory = undefined; throw new Error("Codex returned an invalid MCP inventory"); }
|
|
125
185
|
}, components);
|
|
126
186
|
const native = platform === "win32" || platform === "darwin";
|
|
127
187
|
for (const [name, worker] of [["claude-bridge", "claude-bridge.mjs"], ...(native ? [["codex-native-relay", "native-relay-companion.mjs"]] : []), ["antigravity-bridge", "antigravity-bridge.mjs"]]) {
|
|
128
188
|
configurationCheck(`Codex / ${name}`, () => {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
189
|
+
if (!inventory) throw new Error("Not checked because Codex CLI did not report its MCP servers; the registration may still be present");
|
|
190
|
+
const entry = inventory.find((server) => server?.name === name);
|
|
191
|
+
if (!entry) throw new Error("MCP registration is missing");
|
|
132
192
|
const details = assertEntry({ ...entry.transport, enabled: entry.enabled }, worker, root, name === "antigravity-bridge" ? { caller: "codex" } : {});
|
|
133
193
|
if (name === "codex-native-relay" && !entry.transport.env_vars?.includes("CODEX_APP_TOOLS_PIPE_PATH")) throw new Error("Native pipe forwarding is missing");
|
|
134
194
|
return details;
|
|
@@ -177,7 +237,7 @@ export function checkSetup({ env = process.env, platform = process.platform, roo
|
|
|
177
237
|
return { configured: components.every((item) => item.status === "configured"), liveVerified: false, components };
|
|
178
238
|
}
|
|
179
239
|
|
|
180
|
-
export function runSetup({ args = [], env = process.env, platform = process.platform, root = ROOT, run = execute, resolve = resolveCodexBin } = {}) {
|
|
240
|
+
export function runSetup({ args = [], env = process.env, platform = process.platform, root = ROOT, run = execute, resolve = resolveCodexBin, onProgress = () => {} } = {}) {
|
|
181
241
|
const options = setupArguments(args);
|
|
182
242
|
if (options.help) return { help: `Usage: codex-bridge-setup [--check|--dry-run] [--json] [--no-download]\nUpdate the package and apply managed settings in an interactive terminal with: ${platform === "win32" ? "npx.cmd" : "npx"} -y @minhspark/codex-mcp-bridge@latest\nThe updater installs the package with lifecycle scripts disabled, then directly configures Claude Desktop, Claude Code, Codex, Antigravity and the independent worker relay. CODEX_BRIDGE_SKIP_SETUP=1 opts out. Saved configuration, background relay health and loaded native app connections are verified separately.` };
|
|
183
243
|
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." };
|
|
@@ -192,6 +252,7 @@ export function runSetup({ args = [], env = process.env, platform = process.plat
|
|
|
192
252
|
const base = setupEnvironment(env, paths, runtime.binary);
|
|
193
253
|
const components = [];
|
|
194
254
|
const install = (name, script, arguments_, environment = base) => {
|
|
255
|
+
onProgress(name);
|
|
195
256
|
try {
|
|
196
257
|
const output = run(process.execPath, [path.join(root, "scripts", script), ...arguments_], environment, 180000);
|
|
197
258
|
let details = {};
|
|
@@ -201,7 +262,7 @@ export function runSetup({ args = [], env = process.env, platform = process.plat
|
|
|
201
262
|
...(typeof details.liveVerified === "boolean" ? { liveVerified: details.liveVerified, transportReady: details.transportReady === true } : {}),
|
|
202
263
|
...(details.backgroundPreference ? { backgroundPreference: details.backgroundPreference } : {}) });
|
|
203
264
|
return true;
|
|
204
|
-
} catch (error) { components.push(
|
|
265
|
+
} catch (error) { components.push(failedComponent(name, error)); return false; }
|
|
205
266
|
};
|
|
206
267
|
const code = install(planned[0], "install-claude-code.mjs", ["--install", "--setup", "--desktop-config", paths.desktop], {
|
|
207
268
|
...base, CODEX_BRIDGE_DESKTOP_TASKS: native ? "1" : "0", CODEX_BRIDGE_AUTOSTART: native ? "0" : "1", CODEX_BRIDGE_THREAD_POLICY: "roots",
|
|
@@ -214,6 +275,7 @@ export function runSetup({ args = [], env = process.env, platform = process.plat
|
|
|
214
275
|
install("Codex / claude-bridge", "install-codex-mcp.mjs", ["--setup"]);
|
|
215
276
|
if (code) install("Antigravity / all components", "install-antigravity-bridge.mjs", ["--all", "--setup"]);
|
|
216
277
|
install("Independent background relay", "setup-background-relay.mjs", ["--json"]);
|
|
278
|
+
onProgress("Verifying saved settings");
|
|
217
279
|
const inspection = checkSetup({ env: base, platform, root, run, resolve: () => runtime.binary });
|
|
218
280
|
return { configured: components.every((item) => item.status === "configured") && inspection.configured, liveVerified: false,
|
|
219
281
|
cliInstalled: runtime.installed, components, verification: inspection.components,
|