@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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-mcp-bridge",
3
- "version": "1.26.7",
3
+ "version": "1.26.9",
4
4
  "description": "Connect ChatGPT to your own Codex Desktop projects and conversations.",
5
5
  "author": {
6
6
  "name": "Bui Dang Minh"
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minhspark/codex-mcp-bridge",
3
- "version": "1.26.7",
3
+ "version": "1.26.9",
4
4
  "description": "Connect Claude, Codex, and Antigravity through MCP, with native Desktop handoffs and bridge-owned Antigravity CLI sessions.",
5
5
  "keywords": [
6
6
  "mcp",
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.7",
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 { applyStdioMcpRegistration, stdioMcpRegistration } from "../src/codex-mcp-registration.mjs";
7
- import { isMainModule, stableInstallPath, stableNodeBin } from "../src/install-paths.mjs";
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 new Error(`${label} is already configured with a different entrypoint; it was not changed`);
74
+ throw conflict("a different entrypoint");
71
75
  }
72
76
  const target = managed ? path.join(path.dirname(expected), "mcp-supervisor.mjs") : expected;
73
- if (path.resolve(script) === path.resolve(target) && !managed) return;
74
- const source = path.dirname(script);
75
- if (!path.isAbsolute(script) || script.includes("\0") || path.resolve(script) !== path.resolve(target)
76
- && (!retarget || path.basename(script) !== path.basename(target)
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
- const command = entry.command;
81
- if (typeof command !== "string" || command.includes("\0") || !/^(?:node|node\.exe)$/i.test(path.basename(command))
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) throw new Error("An Antigravity sidecar with this name exists without a matching bridge definition");
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: 30000, stdio: "pipe" });
252
- const inventory = JSON.parse(run(["mcp", "list", "--json"]));
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 source = typeof script === "string" && path.isAbsolute(script) ? path.dirname(script) : null;
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 new Error(`Desktop ${entryName} has an unrecognized or customized entrypoint; no configuration was changed`);
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 = `${cfgPath}.bak-${new Date().toISOString().slice(0, 10)}-codexbridge`;
203
+ const backup = configSibling(`bak-${new Date().toISOString().slice(0, 10)}-codexbridge-${randomUUID()}`);
198
204
  if (original) {
199
- try { fs.writeFileSync(backup, original.data, { flag: "wx", mode: 0o600 }); }
200
- catch (error) { if (error.code !== "EEXIST") throw error; }
205
+ fs.writeFileSync(backup, original.data, { flag: "wx", mode: 0o600 });
201
206
  }
202
- const temporary = `${cfgPath}.${randomUUID()}.tmp`;
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 servers = JSON.parse(run(["mcp", "list", "--json"]));
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.7";
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 servers = JSON.parse(run(["mcp", "list", "--json"]));
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 result = runSetup({ args: process.argv.slice(2) });
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.stderr.write(`Bridge setup: ${error.message}\n`);
12
- process.stderr.write("Bridge configuration was not completed. Run codex-bridge-setup to retry after resolving the reported error.\n");
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
  }
@@ -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
- function execute(command, args, options) {
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
- : `The ${phase === "verification" ? "installed setup verification" : "npm command"} did not complete. ${RETRY}`, phase, cause);
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
- for (const check of [false, true]) {
204
- onProgress(check ? "Verifying the global bridge settings and owned relay runtime." : "Applying managed settings from the installed bridge.");
205
- let setup;
206
- try { setup = JSON.parse(await command([path.join(globalRoot, "scripts", "setup.mjs"), ...(check ? ["--check"] : []), "--json"], "verification")); }
207
- catch (error) { if (error?.code) throw error; throw failure("UPDATE_SETUP_INCOMPLETE", `Global setup did not return a verifiable result. ${RETRY}`, "verification"); }
208
- if (setup?.configured !== true) throw failure("UPDATE_SETUP_INCOMPLETE", `The global bridge settings are incomplete. ${RETRY}`, "verification");
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
  }
@@ -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.7";
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 source = typeof script === "string" && path.isAbsolute(script) ? path.dirname(path.resolve(script)) : null;
45
- const ownSource = source && (source === PACKAGE_SOURCE || source === path.join(root, "src")
46
- || path.basename(source) === "src" && path.basename(path.dirname(source)).toLowerCase() === "codex-mcp-bridge");
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 packages = absoluteDirectory(path.join(env.LOCALAPPDATA || path.join(home, "AppData", "Local"), "Packages"));
46
- let entries;
47
- try {
48
- directoryStat(packages);
49
- entries = fs.readdirSync(packages, { withFileTypes: true });
50
- } catch (error) {
51
- if (!isMissing(error)) throw error;
52
- entries = [];
53
- }
54
- if (entries.length > MAX_PACKAGES) throw new Error("package scan limit");
55
- for (const entry of entries) {
56
- if (!PACKAGE_NAME.test(entry.name)) continue;
57
- const packageRoot = path.join(packages, entry.name);
58
- directoryStat(packageRoot);
59
- roots.push(path.join(packageRoot, "LocalCache", "Roaming", "Claude"));
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.7";
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
 
@@ -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.7";
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({ name: label, status: "incomplete", detail: error.message }); }
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), 30000));
124
- if (!Array.isArray(inventory)) throw new Error("Codex returned an invalid MCP inventory");
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
- const item = inventory?.find((server) => server.name === name);
130
- if (!item) throw new Error("MCP registration is missing");
131
- const entry = JSON.parse(run(codex, ["mcp", "get", name, "--json"], setupEnvironment(env, paths, codex), 30000));
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({ name, status: "incomplete", detail: error.message }); return false; }
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,