@minhspark/codex-mcp-bridge 1.22.6 → 1.22.7

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.22.6",
3
+ "version": "1.22.7",
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,28 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [SemVer](ht
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [1.22.7] - 2026-10-20
8
+
9
+ ### Added
10
+
11
+ - Install with Homebrew: `brew install buidangminh23/tap/codex-mcp-bridge`. The formula in [buidangminh23/homebrew-tap](https://github.com/buidangminh23/homebrew-tap) follows each npm release.
12
+
13
+ ### Fixed
14
+
15
+ - Record Homebrew's stable `opt` paths instead of a versioned Cellar keg when the Claude Desktop, Codex and native relay installers and `bridge-projects register-manager` write a client configuration, for both the bridge and Homebrew's Node.js. A keg path broke every registration as soon as `brew upgrade` removed the old keg. Paths outside a Homebrew keg are written exactly as before.
16
+ - Start the supervisor when its path reaches the installation through a symbolic link, such as Homebrew's `opt` alias. The direct-execution check compared the unresolved command-line path with the resolved module path, so the process exited at once without serving MCP.
17
+ - Follow a Homebrew upgrade in a running supervisor: it watches the `opt` alias, reloads the new keg like any other installed update, and forks workers with the stable Node.js path, instead of reporting "Installation is not ready" after the old keg is removed.
18
+
19
+ ### Verification
20
+
21
+ - 9 new regression tests: Cellar to `opt` mapping, a custom `HOMEBREW_PREFIX`, the main-module check through a symlink and a respelled path, a running supervisor following a bridge keg switch, and one following a Node.js keg replacement. The supervisor tests fail on v1.22.6 and pass here.
22
+ - End to end on macOS with a local tap: two packed versions installed with brew, the three installers run against an isolated home, and every recorded path was `opt/codex-mcp-bridge` and `opt/node` (the relay keeps Codex Desktop's Node). A bridge launched from the recorded Claude Desktop entry kept its supervisor through `brew upgrade` and `brew cleanup` and reloaded the new keg; fresh launches from the recorded paths answered `initialize`.
23
+ - An independent adversarial review confirmed two defects in the first draft, a main-module regression for respelled paths and a worker Node path resolved too late, and both were fixed before merge.
24
+
25
+ ### Limitations
26
+
27
+ - Registrations written from a Homebrew keg by an earlier version still name that keg; run the installers once after upgrading. `codex-native-relay-install` still refuses to rewrite an entry that has `env_vars`, so edit that entry's path by hand.
28
+
7
29
  ## [1.22.6] - 2026-10-20
8
30
 
9
31
  ### Fixed
package/README.md CHANGED
@@ -27,6 +27,14 @@ npm install -g @minhspark/codex-mcp-bridge@latest
27
27
 
28
28
  Requires **Node.js 22+**. In Windows PowerShell, use `npm.cmd` instead of `npm`. Run the same command to update; reconnect MCP servers if their settings or installation paths changed.
29
29
 
30
+ On macOS or Linux you can install with [Homebrew](https://github.com/buidangminh23/homebrew-tap) instead, which brings its own Node.js:
31
+
32
+ ```bash
33
+ brew install buidangminh23/tap/codex-mcp-bridge
34
+ ```
35
+
36
+ Update with `brew upgrade codex-mcp-bridge`. The installers record Homebrew's stable `opt` paths, so registrations keep working after an upgrade and running bridges reload the new version. Keep one install channel: whichever of npm or Homebrew comes first on `PATH` provides the commands.
37
+
30
38
  [Windows](#windows-powershell) · [macOS](#macos-terminal) · [Linux / WSL](#linux--wsl-bash) · [ChatGPT plugin](#chatgpt-plugin)
31
39
 
32
40
  ### Windows (PowerShell)
@@ -93,6 +101,15 @@ claude mcp add --scope user codex-bridge \
93
101
  -- "$(command -v node)" "$bridge_root/src/mcp-supervisor.mjs" index.mjs
94
102
  ```
95
103
 
104
+ For a Homebrew install, use the stable paths instead of `npm root -g` and `command -v node`:
105
+
106
+ ```bash
107
+ bridge_root="$(brew --prefix)/opt/codex-mcp-bridge/libexec/lib/node_modules/@minhspark/codex-mcp-bridge"
108
+ node_bin="$(brew --prefix)/opt/node/bin/node"
109
+ ```
110
+
111
+ and end the command with `-- "$node_bin" "$bridge_root/src/mcp-supervisor.mjs" index.mjs`.
112
+
96
113
  </details>
97
114
 
98
115
  Separate multiple allowed directories with `;` on Windows or `:` on macOS/Linux.
@@ -178,7 +195,7 @@ Dispatch expires after two minutes; operation history remains stored. Disconnect
178
195
 
179
196
  ## Troubleshooting
180
197
 
181
- - **Command missing:** open a new terminal and check Node.js 22+ and the global npm directory on PATH.
198
+ - **Command missing:** open a new terminal and check Node.js 22+ and the global npm directory (or Homebrew's `bin`) on PATH.
182
199
  - **PowerShell blocks scripts:** use `npm.cmd`, installer `.cmd` launchers, and `codex.exe`.
183
200
  - **No sessions or wrong project:** open the intended Claude **Code** session and use the exact project directory saved in Codex Desktop.
184
201
  - **Relay unavailable:** check [pipe forwarding](#native-relay-pipe-forwarding), reconnect, and run `native_relay_status` in the task.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minhspark/codex-mcp-bridge",
3
- "version": "1.22.6",
3
+ "version": "1.22.7",
4
4
  "description": "Two-way MCP bridge between Claude and Codex: prompts into a live Codex thread, messages into a running Claude Code session.",
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.22.6",
4
+ "version": "1.22.7",
5
5
  "description": "Connect ChatGPT to your own Codex Desktop projects and conversations.",
6
6
  "author": {
7
7
  "name": "Bui Dang Minh",
@@ -9,10 +9,11 @@ import { desktopTasksConfigured } from "../src/native-relay.mjs";
9
9
  import { exitForVersionRequest } from "../src/cli-version.mjs";
10
10
  import { createReleaseSnapshot, snapshotRoot } from "../src/release-snapshot.mjs";
11
11
  import { enableCodexFullAccess, fullAccessEnabled } from "../src/codex-full-access.mjs";
12
+ import { stableInstallPath, stableNodeBin } from "../src/install-paths.mjs";
12
13
 
13
14
  exitForVersionRequest(import.meta.url);
14
15
 
15
- const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
16
+ const root = stableInstallPath(path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."));
16
17
  const cfgPath = process.env.CLAUDE_DESKTOP_CONFIG ?? claudeDesktopConfigPath();
17
18
  const reset = process.argv.includes("--reset");
18
19
  const forceDesktopTasks = process.argv.includes("--desktop-tasks");
@@ -29,7 +30,7 @@ if (fullAccess) enableCodexFullAccess();
29
30
  */
30
31
  const defaultRoots = "*";
31
32
 
32
- const nodeBin = process.env.NODE_EXE ?? process.execPath;
33
+ const nodeBin = process.env.NODE_EXE ?? stableNodeBin();
33
34
  const codexBin = resolveCodexBin(process.env.CODEX_EXE);
34
35
 
35
36
  if (!fs.existsSync(nodeBin)) throw new Error(`node executable not found: ${nodeBin}`);
@@ -10,10 +10,12 @@ import { desktopTasksConfigured } from "../src/native-relay.mjs";
10
10
  import { codexMcpRegistration } from "../src/codex-mcp-registration.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
14
 
14
15
  exitForVersionRequest(import.meta.url);
15
16
 
16
- const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
17
+ const root = stableInstallPath(path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."));
18
+ const nodeBin = process.env.NODE_EXE ?? stableNodeBin();
17
19
  const codexBin = resolveCodexBin(process.env.CODEX_EXE);
18
20
  const serverName = process.env.CLAUDE_BRIDGE_NAME ?? "claude-bridge";
19
21
  const entry = path.join(root, "src", "mcp-supervisor.mjs");
@@ -39,13 +41,13 @@ const servers = JSON.parse(run(["mcp", "list", "--json"]));
39
41
  if (!Array.isArray(servers)) throw new Error("Codex returned an invalid MCP inventory; existing configuration was not changed");
40
42
  const existing = servers.some((server) => server.name === serverName)
41
43
  ? JSON.parse(run(["mcp", "get", serverName, "--json"])) : null;
42
- const registration = codexMcpRegistration({ name: serverName, existing, node: process.execPath, entry, entryArgs, env: process.env, desktopOnly: desktopTasksConfigured() });
44
+ const registration = codexMcpRegistration({ name: serverName, existing, node: nodeBin, entry, entryArgs, env: process.env, desktopOnly: desktopTasksConfigured() });
43
45
  createReleaseSnapshot(root, { cache: snapshotRoot({ ...process.env, ...registration.environment }) });
44
46
  run(registration.args);
45
47
 
46
48
  console.log(`platform: ${PLATFORM_LABEL}`);
47
49
  console.log(`registered MCP server "${serverName}" with Codex:`);
48
- console.log(`command: ${process.execPath} ${entry} ${entryArgs.join(" ")}`);
50
+ console.log(`command: ${nodeBin} ${entry} ${entryArgs.join(" ")}`);
49
51
  console.log(`environment keys: ${registration.environmentKeys.join(", ")}`);
50
52
  console.log("\nReconnect claude-bridge once in the existing Codex Desktop task to load the supervisor. Subsequent compatible installed-source updates reload automatically when the worker is safely idle. Do not create a replacement task.");
51
53
  console.log("Verify claude_bridge_status from that task: autoReload must be enabled, runtime state must be current, and the session policy must match this installation. A separate diagnostic process does not verify the app's loaded MCP process.");
@@ -20,11 +20,12 @@ import { exitForVersionRequest } from "../src/cli-version.mjs";
20
20
  import { stdioMcpRegistration } from "../src/codex-mcp-registration.mjs";
21
21
  import { createReleaseSnapshot, snapshotRoot } from "../src/release-snapshot.mjs";
22
22
  import { enableCodexFullAccess, fullAccessEnabled } from "../src/codex-full-access.mjs";
23
+ import { stableInstallPath } from "../src/install-paths.mjs";
23
24
 
24
25
  exitForVersionRequest(import.meta.url);
25
26
 
26
- const VERSION = "1.22.6";
27
- const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
27
+ const VERSION = "1.22.7";
28
+ const root = stableInstallPath(path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."));
28
29
  const entry = path.join(root, "src", "mcp-supervisor.mjs");
29
30
  const entryArgs = ["native-relay-companion.mjs"];
30
31
  const serverName = process.env.CODEX_NATIVE_RELAY_NAME ?? "codex-native-relay";
@@ -30,7 +30,7 @@ import { discoverClaudeProject } from "./claude-project-discovery.mjs";
30
30
 
31
31
  exitForVersionRequest(import.meta.url);
32
32
 
33
- const VERSION = "1.22.6";
33
+ const VERSION = "1.22.7";
34
34
  void import("./telemetry.mjs").then(({ startUsageReporting }) => startUsageReporting({ version: VERSION })).catch(() => {});
35
35
  const FORWARD_MIN_INTERVAL_MS = 5000;
36
36
  const FORWARD_MAX_PER_SESSION = 50;
package/src/index.mjs CHANGED
@@ -40,7 +40,7 @@ import { PROJECT_SETUP_TOOLS, registerProjectOnboardingTools } from "./project-o
40
40
 
41
41
  exitForVersionRequest(import.meta.url);
42
42
 
43
- const VERSION = "1.22.6";
43
+ const VERSION = "1.22.7";
44
44
  void import("./telemetry.mjs").then(({ startUsageReporting }) => startUsageReporting({ version: VERSION })).catch(() => {});
45
45
  const log = (msg) => process.stderr.write(`[codex-mcp-bridge] ${msg}\n`);
46
46
 
@@ -0,0 +1,54 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const KEG_PATH = /^(\/.*?)\/Cellar\/([^/]+)\/[^/]+(\/.*)?$/;
6
+
7
+ function resolved(target) {
8
+ try { return fs.realpathSync.native(target); } catch { return null; }
9
+ }
10
+
11
+ /**
12
+ * Homebrew installs every version of a formula into its own Cellar keg and
13
+ * deletes the old keg on upgrade, while <prefix>/opt/<formula> is retargeted to
14
+ * the current one. A path that outlives this process - written into a client
15
+ * configuration, or watched for an upgrade - therefore has to name the opt
16
+ * alias. A keg path is rewritten only when the alias resolves to the same file
17
+ * right now; every other path is returned unchanged.
18
+ */
19
+ export function stableInstallPath(target, env = process.env) {
20
+ if (process.platform === "win32" || typeof target !== "string") return target;
21
+ const match = KEG_PATH.exec(target);
22
+ if (!match) return target;
23
+ const real = resolved(target);
24
+ if (!real) return target;
25
+ const [, base, formula, rest = ""] = match;
26
+ const prefixes = [...new Set([env.HOMEBREW_PREFIX, base].filter((prefix) => prefix && path.isAbsolute(prefix)))];
27
+ for (const prefix of prefixes) {
28
+ const alias = path.join(prefix, "opt", formula) + rest;
29
+ if (resolved(alias) === real) return alias;
30
+ }
31
+ return target;
32
+ }
33
+
34
+ /** The node binary to record for later launches. */
35
+ export function stableNodeBin(env = process.env) {
36
+ return stableInstallPath(process.execPath, env);
37
+ }
38
+
39
+ /**
40
+ * True when `entry` names the running main module, however it was reached.
41
+ * Node resolves the main module with the JavaScript realpath, which follows
42
+ * symlinks but keeps the spelling of the path as typed (case, 8.3 names,
43
+ * substituted drives), so that is the form compared first.
44
+ */
45
+ export function isMainModule(entry, moduleUrl) {
46
+ if (!entry) return false;
47
+ const file = path.resolve(entry);
48
+ const module = fileURLToPath(moduleUrl);
49
+ let typed;
50
+ try { typed = fs.realpathSync(file); } catch { typed = file; }
51
+ if (typed === module) return true;
52
+ const canonical = resolved(file);
53
+ return canonical !== null && canonical === resolved(module);
54
+ }
@@ -5,7 +5,9 @@ import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { createReleaseSnapshot, sourceRevision } from "./release-snapshot.mjs";
7
7
  import { desktopTasksConfigured } from "./native-relay.mjs";
8
+ import { isMainModule, stableInstallPath } from "./install-paths.mjs";
8
9
 
10
+ const WORKER_NODE = stableInstallPath(process.execPath);
9
11
  const ENTRIES = new Set(["index.mjs", "claude-bridge.mjs", "native-relay-companion.mjs"]);
10
12
  const MAX_FRAME_BYTES = 16 * 1024 * 1024;
11
13
  const MAX_QUEUE = 256;
@@ -40,7 +42,7 @@ class Worker {
40
42
  this.retiring = false;
41
43
  this.closed = false;
42
44
  this.process = fork(path.join(release.directory, "src", entry), [], {
43
- execPath: process.execPath, execArgv: [], cwd: process.cwd(), windowsHide: true,
45
+ execPath: WORKER_NODE, execArgv: [], cwd: process.cwd(), windowsHide: true,
44
46
  env: { ...env, CODEX_BRIDGE_WORKER: "1", CODEX_BRIDGE_STAGED: "1" },
45
47
  stdio: ["pipe", "pipe", "pipe", "ipc"], serialization: "json",
46
48
  });
@@ -133,14 +135,16 @@ class Worker {
133
135
 
134
136
  export async function runSupervisor(entry, options = {}) {
135
137
  if (!ENTRIES.has(entry)) throw new Error("Expected index.mjs, claude-bridge.mjs, or native-relay-companion.mjs");
136
- const root = fs.realpathSync.native(options.root ?? path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."));
138
+ const installRoot = stableInstallPath(fs.realpathSync.native(options.root ?? path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..")));
139
+ const currentRoot = () => fs.realpathSync.native(installRoot);
140
+ const root = currentRoot();
137
141
  if (process.argv.includes("--version") || process.argv.includes("-v")) {
138
142
  process.stdout.write(`${JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8")).version}\n`);
139
143
  return;
140
144
  }
141
145
  const log = (message) => process.stderr.write(`[bridge-supervisor] ${message}\n`);
142
146
  const output = (message) => process.stdout.write(`${JSON.stringify(message)}\n`);
143
- const env = { ...process.env, CODEX_BRIDGE_SOURCE_ROOT: root };
147
+ const env = { ...process.env, CODEX_BRIDGE_SOURCE_ROOT: installRoot };
144
148
  const routingConfiguration = () => entry === "native-relay-companion.mjs" ? null : desktopTasksConfigured(env);
145
149
  if (entry === "native-relay-companion.mjs" && !env.CODEX_APP_TOOLS_PIPE_PATH) {
146
150
  const { resolveNativeToolsPipePath } = await import("./native-relay.mjs");
@@ -226,9 +230,10 @@ export async function runSupervisor(entry, options = {}) {
226
230
  };
227
231
  const check = async () => {
228
232
  if (switching || !ready || stopping || failure) return;
233
+ let source;
229
234
  let revision;
230
235
  let routing;
231
- try { revision = sourceRevision(root); routing = routingConfiguration(); }
236
+ try { source = currentRoot(); revision = sourceRevision(source); routing = routingConfiguration(); }
232
237
  catch (error) { reason = `Installation is not ready: ${error.message}`; return; }
233
238
  if (observed !== revision || observedRouting !== routing) { observed = revision; observedRouting = routing; observedAt = Date.now(); retryAt = 0; }
234
239
  if (revision === active.release.revision && routing === active.routingConfiguration) { reason = null; return; }
@@ -241,7 +246,7 @@ export async function runSupervisor(entry, options = {}) {
241
246
  try {
242
247
  const state = await previous.control("inspect");
243
248
  if (!state.reloadable) { reason = state.reason ?? "Pending deliveries prevent a safe reload"; return; }
244
- const release = createReleaseSnapshot(root, { expectedRevision: revision });
249
+ const release = createReleaseSnapshot(source, { expectedRevision: revision });
245
250
  candidate = spawn(release, routing);
246
251
  const initialized = await candidate.rpc("initialize", initializeParams);
247
252
  if (initialized.protocolVersion !== initializeResult.protocolVersion || JSON.stringify(initialized.capabilities) !== JSON.stringify(initializeResult.capabilities)) {
@@ -251,7 +256,7 @@ export async function runSupervisor(entry, options = {}) {
251
256
  const catalog = await candidate.rpc("tools/list", {});
252
257
  if (!Array.isArray(catalog.tools) || !catalog.tools.length || catalog.nextCursor) throw new Error("The candidate tool catalog is incomplete");
253
258
  const oldCatalog = await previous.rpc("tools/list", {});
254
- if (sourceRevision(root) !== revision) throw new Error("The installation changed before activation");
259
+ if (sourceRevision(currentRoot()) !== revision) throw new Error("The installation changed before activation");
255
260
  if (routingConfiguration() !== routing) throw new Error("The routing configuration changed before activation");
256
261
  quiesced = true;
257
262
  const exported = await previous.control("quiesce");
@@ -339,6 +344,6 @@ export async function runSupervisor(entry, options = {}) {
339
344
  log(`supervising ${entry}; installed updates activate after pending work completes`);
340
345
  }
341
346
 
342
- if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
347
+ if (isMainModule(process.argv[1], import.meta.url)) {
343
348
  await runSupervisor(process.argv[2]);
344
349
  }
@@ -38,7 +38,7 @@ import { NativeCreationReceipts, creationIdValid } from "./native-creation-recei
38
38
 
39
39
  exitForVersionRequest(import.meta.url);
40
40
 
41
- const VERSION = "1.22.6";
41
+ const VERSION = "1.22.7";
42
42
  const log = (msg) => process.stderr.write(`[native-relay] ${msg}\n`);
43
43
 
44
44
  function errorResponse(code, message, sent, details) {
package/src/platform.mjs CHANGED
@@ -5,6 +5,8 @@ import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { promisify } from "node:util";
7
7
 
8
+ import { stableNodeBin } from "./install-paths.mjs";
9
+
8
10
  const execFileAsync = promisify(execFile);
9
11
 
10
12
  export const IS_MACOS = process.platform === "darwin";
@@ -230,7 +232,7 @@ export function resolveCodexDesktopNodeBin(
230
232
  ]) {
231
233
  if (candidate && isRunnable(candidate)) return { path: candidate, source };
232
234
  }
233
- return { path: process.execPath, source: "process.execPath" };
235
+ return { path: stableNodeBin(env), source: "process.execPath" };
234
236
  }
235
237
 
236
238
  /**
@@ -393,7 +395,7 @@ export function spawnEnv(extra = {}) {
393
395
  const systemDirs = IS_WINDOWS
394
396
  ? []
395
397
  : ["/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin", "/usr/sbin", "/sbin"];
396
- const merged = [path.dirname(process.execPath), ...systemDirs, ...(process.env.PATH ?? "").split(separator)]
398
+ const merged = [path.dirname(stableNodeBin()), ...systemDirs, ...(process.env.PATH ?? "").split(separator)]
397
399
  .filter(Boolean)
398
400
  .filter((dir, index, all) => all.indexOf(dir) === index)
399
401
  .join(separator);
@@ -4,15 +4,16 @@ import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { randomUUID } from "node:crypto";
6
6
  import { readRegularConfigFile, backupRegularConfigFile } from "./project-policy.mjs";
7
+ import { stableInstallPath, stableNodeBin } from "./install-paths.mjs";
7
8
 
8
9
  // Optional UI adapter discovery. Neither bridge startup nor policy evaluation reads
9
10
  // this file. Removing the card extension or this descriptor cannot stop messaging.
10
- export function registerProjectManager({ policyFile, env = process.env, executable = process.execPath } = {}) {
11
+ export function registerProjectManager({ policyFile, env = process.env, executable = stableNodeBin(env) } = {}) {
11
12
  if (!path.isAbsolute(policyFile ?? "")) throw new Error("Manager policy path must be absolute");
12
13
  const home = env.HOME ?? env.USERPROFILE ?? os.homedir();
13
14
  const file = path.join(home, ".config", "GptClaudeBridge", "management.json");
14
15
  const value = { version: 1, node: executable,
15
- script: fileURLToPath(new URL("../scripts/bridge-projects.mjs", import.meta.url)),
16
+ script: stableInstallPath(fileURLToPath(new URL("../scripts/bridge-projects.mjs", import.meta.url)), env),
16
17
  policyFile, configFile: env.CLAUDE_CONFIG_DIR ? path.join(env.CLAUDE_CONFIG_DIR, ".claude.json") : path.join(home, ".claude.json") };
17
18
  if (!Object.values(value).filter(v => typeof v === "string").every(v => path.isAbsolute(v))) throw new Error("Manager paths must be absolute");
18
19
  fs.mkdirSync(path.dirname(file), { recursive: true });