@minhspark/codex-mcp-bridge 1.13.2 → 1.13.4
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/CHANGELOG.md +35 -0
- package/README.md +47 -10
- package/package.json +5 -2
- package/scripts/check-claude-bridge.mjs +2 -1
- package/scripts/install-claude-desktop.mjs +3 -0
- package/scripts/install-codex-mcp.mjs +15 -15
- package/scripts/install-native-relay.mjs +38 -3
- package/scripts/install-npm-footer.mjs +99 -0
- package/scripts/npm-footer.ps1 +140 -0
- package/scripts/npm-footer.sh +81 -0
- package/src/claude-bridge.mjs +69 -19
- package/src/cli-version.mjs +20 -0
- package/src/codex-mcp-registration.mjs +25 -0
- package/src/index.mjs +46 -15
- package/src/native-relay-companion.mjs +4 -1
- package/src/peer-protocol.mjs +78 -11
- package/src/platform.mjs +55 -1
- package/src/runtime-state.mjs +34 -0
- package/src/thread-delivery.mjs +45 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,41 @@
|
|
|
2
2
|
|
|
3
3
|
Follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [SemVer](https://semver.org/).
|
|
4
4
|
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
## [1.13.4] - 2026-09-06
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Latch queued Desktop delivery deadlines when they expire, so a timed-out prompt cannot dispatch later or release the lock owned by an active send.
|
|
12
|
+
- Require and revalidate the exact intended workspace for Claude Desktop sends, expose pending recipient receipts through `read_claude_delivery`, and block no-wait retries of held or uncertain messages. Preserve pending ownership after an uncertain socket write.
|
|
13
|
+
- Detect source and Desktop-policy changes in loaded bridge processes and refuse new sends until reconnect. Diagnostics identify their separate MCP process instead of implying the Desktop connection was refreshed.
|
|
14
|
+
- Preserve the Codex MCP registration environment on upgrade, pin Desktop routing explicitly, disable its external autostart, and refuse to reset custom access or transport settings. Prefer existing Desktop tasks in MCP instructions.
|
|
15
|
+
- Enforce Desktop task mode for Claude destinations as well as Codex delivery. Exclude non-Desktop sessions from discovery, refuse CLI or unknown entrypoints before connecting, and require a unique exact target. Status reports eligible and excluded sessions; receipts identify the actual entrypoint, workspace, and session. Legacy CLI mode remains available when Desktop task mode is disabled.
|
|
16
|
+
- Register the native relay companion under a runtime Codex Desktop trusts instead of whichever Node happens to run the installer. The app authenticates the code-signing identity of any process connecting to its native tools pipe and closes the connection before reading a byte when that identity is not its own, so a companion started by a user-installed Node still created its socket and still reported itself installed while every delivery failed as `NATIVE_DELIVERY_UNCONFIRMED` — the refusal is only visible in the app's own log as `dynamic_app_tools_peer_rejected`. On macOS the installer now resolves the runtime the app ships, honours the variables the app declares for its own bundled plugin, and prints the runtime and its source; `CODEX_NATIVE_RELAY_NODE` overrides it. Windows and Linux keep the runtime running the installer, since neither was measured to enforce the check.
|
|
17
|
+
|
|
18
|
+
### Upgrade notes
|
|
19
|
+
|
|
20
|
+
- Reconnect the MCP connections in both Desktop clients after upgrading. Reinstall an existing native relay companion with the updated installer to apply runtime selection. Updating package files alone does not replace running processes or add the stale-runtime guard to an older process.
|
|
21
|
+
- In Desktop task mode, provide the independently verified absolute `expectedCwd` when sending to Claude. A held receipt still requires the recipient's approval; inspect its `msgId` with `read_claude_delivery` instead of resending. Receipt history is in memory and is not restored after a bridge restart.
|
|
22
|
+
|
|
23
|
+
## [1.13.3] - 2026-09-05
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- Verify the current saved project's identity, canonical directory, and task membership before reusing a Desktop creation receipt. Project drift is rejected; omitted recent/pinned metadata reports unverified assignment while preserving the task ID without resending or creating a duplicate.
|
|
28
|
+
- Support `--version` and `-v` on every packaged command without starting servers, opening sockets, or mutating client configuration.
|
|
29
|
+
- Preserve PowerShell's `$?` and conditional `&&`/`||` behavior as well as `$LASTEXITCODE` by invoking its footer through a script alias.
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
|
|
33
|
+
- Opt-in Bash, Zsh, and PowerShell installation footers for `npm install -g @minhspark/codex-mcp-bridge@latest`: first installation, changed version, unchanged version, failure, dry run, and unverifiable metadata are distinguished after npm's final output while preserving its exit code.
|
|
34
|
+
- `codex-npm-footer-install` backs up and updates shell profiles idempotently, refuses unmanaged npm wrappers, supports removal, and explains how to reload an already-open terminal. Package installation does not change profiles automatically.
|
|
35
|
+
|
|
36
|
+
### Verification
|
|
37
|
+
|
|
38
|
+
- Regression coverage includes same-workspace project moves, deleted/repointed/replaced projects, incomplete Desktop snapshots, deadlines and authorization, shell argument boundaries, dry-run configuration, failure exit codes, metadata validation, and profile preservation.
|
|
39
|
+
|
|
5
40
|
## [1.13.2] - 2026-09-05
|
|
6
41
|
|
|
7
42
|
### Fixed
|
package/README.md
CHANGED
|
@@ -28,6 +28,8 @@ Desktop calls share a maximum 40-second budget across creation, opening, queuein
|
|
|
28
28
|
|
|
29
29
|
Creation receipts persist in `$CODEX_HOME/bridge-task-receipts` (default `~/.codex/bridge-task-receipts`). The same canonical workspace and explicit title reuse the existing task, including after completion or a bridge restart; an edited brief is not resent. Use `send_to_codex_thread` to continue it, or a distinct title for separate work. Without an explicit title, the key uses the exact prompt hash. Receipts store hashes rather than prompt text and never grant thread permissions: `owned` policy still requires authorization after restart. Pending or uncertain receipts and abandoned creation locks fail closed without automatic expiry; inspect the actual task before repairing them.
|
|
30
30
|
|
|
31
|
+
Reusing a receipt also verifies that the saved project still has the same identity and exact directory, then checks the task's current project membership in Desktop's recent/pinned listing. A moved task or deleted/repointed project is rejected before sending or creating anything. A task omitted from that listing keeps its known ID and reports `project assignment: unverified`; stored receipt metadata is not presented as current membership. If a checkout has moved, edit the existing project's folder in Desktop, then inspect the existing task's workspace. Do not add another project or append the obsolete directory merely to satisfy a match.
|
|
32
|
+
|
|
31
33
|
The updated companion exposes a separate `-desktop-tasks` endpoint so a previous companion holding the legacy reply socket need not be killed during an upgrade. `codex_bridge_status` verifies the native connection through Desktop's local project list; `native_relay_status` reports both endpoints. Reconnect the companion after upgrading so it accepts the current native operations.
|
|
32
34
|
|
|
33
35
|
Desktop mode never contacts, starts, or falls back to an external app-server, including during status checks and thread discovery. The Claude Desktop installer records `CODEX_BRIDGE_AUTOSTART=0` in this mode. If Desktop or its companion is unavailable, the bridge reports the failure and leaves existing tasks in Desktop. `stop_codex_app_server` is a no-op in Desktop mode; stop an obsolete external service through its own launcher after confirming it has no active work.
|
|
@@ -122,7 +124,7 @@ Installing straight from the repository works the same way and needs no registry
|
|
|
122
124
|
npm install -g git+https://github.com/buidangminh23/codex-mcp-bridge.git
|
|
123
125
|
```
|
|
124
126
|
|
|
125
|
-
Either route puts
|
|
127
|
+
Either route puts the bridge servers, configuration installers, native relay, and `codex-npm-footer-install` on your PATH. Wherever this README runs `node scripts/install-claude-desktop.mjs`, an installed copy runs `codex-mcp-bridge-install` instead. Every packaged command supports `--version` and `-v` without starting a server or changing configuration.
|
|
126
128
|
|
|
127
129
|
#### Upgrading an install you already have
|
|
128
130
|
|
|
@@ -130,6 +132,33 @@ Either route puts four commands on your PATH — `codex-mcp-bridge` and `claude-
|
|
|
130
132
|
|
|
131
133
|
Re-running `codex-mcp-bridge-install` is **not** required to upgrade, and before 1.11.1 it actively hurt: it replaced the whole entry, discarding `CODEX_BRIDGE_ALLOWED_THREADS`, any hand-added `CODEX_BRIDGE_THREAD_POLICY`, and resetting `CODEX_BRIDGE_ALLOWED_ROOTS` to the install directory. From 1.11.1 it keeps what is already there — an environment variable you pass wins, the existing value is the fallback, and `--reset` gives you the defaults back.
|
|
132
134
|
|
|
135
|
+
#### Show the final npm installation result
|
|
136
|
+
|
|
137
|
+
Enable the optional shell integration once after installing the package:
|
|
138
|
+
|
|
139
|
+
```sh
|
|
140
|
+
codex-npm-footer-install --shell zsh
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Use `--shell bash` on Bash or `--shell powershell` in PowerShell 7. A source checkout can run `npm run install:footer -- --shell zsh`. The installer backs up existing profiles, preserves unrelated content, and prints a command to load the integration into the current terminal. New login and interactive Bash/Zsh shells load it automatically; PowerShell uses its current-user all-hosts profile. A terminal started before setup must run the printed reload command or open a new shell. Existing unmanaged `npm` functions are refused for review rather than overwritten. Use `--profile /absolute/path` to select a custom profile or `--remove` to remove the managed loader.
|
|
144
|
+
|
|
145
|
+
The integration leaves the command unchanged:
|
|
146
|
+
|
|
147
|
+
```sh
|
|
148
|
+
npm install -g @minhspark/codex-mcp-bridge@latest
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
| Measured result | Final line |
|
|
152
|
+
| --- | --- |
|
|
153
|
+
| No previous installation | `Successfully installed: @minhspark/codex-mcp-bridge v<version>` |
|
|
154
|
+
| Version changed | `Successfully updated: @minhspark/codex-mcp-bridge v<before> -> v<after>` |
|
|
155
|
+
| Same version after reinstalling | `Already up to date: @minhspark/codex-mcp-bridge v<version>` |
|
|
156
|
+
| npm failed | `Failed to install: ... (exit code <code>). See npm error above.` |
|
|
157
|
+
| Dry run | `Dry run completed: ... (no changes applied).` |
|
|
158
|
+
| Mode or installed metadata cannot be verified | A warning instead of a success claim |
|
|
159
|
+
|
|
160
|
+
The footer runs after npm finishes and keeps npm's output and exit code. It supports `install`/`i`, explicit global installation, one bridge package, dry-run flags, common install booleans, and a single `--prefix`. Other commands, multiple packages, unknown options, and machine-readable or silent output modes pass through without a footer. Installing the package itself does not alter shell profiles. A successful install confirms files on disk; restart the MCP client separately to load the new bridge code.
|
|
161
|
+
|
|
133
162
|
**Clone it** — right if you intend to read, test or change the code:
|
|
134
163
|
|
|
135
164
|
```bash
|
|
@@ -229,7 +258,7 @@ claude mcp remove codex-bridge --scope user
|
|
|
229
258
|
node scripts/install-codex-mcp.mjs
|
|
230
259
|
```
|
|
231
260
|
|
|
232
|
-
|
|
261
|
+
The installer updates the named entry through `codex mcp add`, preserves existing environment settings, pins the configured Desktop policy, and disables external autostart in Desktop mode. It refuses to reset custom access, timeout, or transport settings. It never infers the sender permission class from the recipient. The basic registration by hand is:
|
|
233
262
|
|
|
234
263
|
```bash
|
|
235
264
|
codex mcp add claude-bridge --env CLAUDE_BRIDGE_PEER_NAME=codex-desktop -- "$(command -v node)" "$PWD/src/claude-bridge.mjs"
|
|
@@ -243,11 +272,7 @@ codex mcp get claude-bridge
|
|
|
243
272
|
node scripts/install-codex-mcp.mjs --remove
|
|
244
273
|
```
|
|
245
274
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
```bash
|
|
249
|
-
launchctl kickstart -k gui/$UID/com.codex-mcp-bridge.app-server
|
|
250
|
-
```
|
|
275
|
+
Reconnect the MCP server in the existing Codex Desktop task, or restart the app and reopen that same task. Verify `claude_bridge_status` **from that task**: `runtime state` must be `current` and `session policy` must match the intended mode. Do not create a replacement task. `npm run check:claude` starts a separate diagnostic process and cannot prove that the app's existing MCP connection reloaded.
|
|
251
276
|
|
|
252
277
|
`CLAUDE_BRIDGE_PEER_NAME` sets the name Claude shows for this bridge in its agent list.
|
|
253
278
|
|
|
@@ -259,7 +284,7 @@ Optional, and only worth installing if you keep the bound thread **open in Codex
|
|
|
259
284
|
node scripts/install-native-relay.mjs
|
|
260
285
|
```
|
|
261
286
|
|
|
262
|
-
This registers the companion with Codex (`codex mcp add codex-native-relay -- <node> src/native-relay-companion.mjs`) and bootstraps the executor thread the native dispatch needs, writing its id to `~/.codex/native-relay.json`. The bootstrap creates one thread, unsubscribes it, and closes its connection. It leaves other clients and threads running; an idle server may retain the executor until its configured unload delay expires.
|
|
287
|
+
This registers the companion with Codex (`codex mcp add codex-native-relay -- <node> src/native-relay-companion.mjs`) and bootstraps the executor thread the native dispatch needs, writing its id to `~/.codex/native-relay.json`. The `<node>` is resolved rather than inherited: Codex Desktop authenticates the code-signing identity of any process that connects to its native tools pipe, so on macOS the installer registers the runtime the app ships and prints the `relay runtime:` line naming it and where it came from. `CODEX_NATIVE_RELAY_NODE` overrides that on any platform; Windows and Linux keep the Node running the installer, as before. The bootstrap creates one thread, unsubscribes it, and closes its connection. It leaves other clients and threads running; an idle server may retain the executor until its configured unload delay expires.
|
|
263
288
|
|
|
264
289
|
```bash
|
|
265
290
|
codex mcp get codex-native-relay
|
|
@@ -355,6 +380,7 @@ Under `owned`, a thread a human opened is **unreachable rather than merely restr
|
|
|
355
380
|
|---|---|---|
|
|
356
381
|
| `list_claude_sessions` | Lists Claude Code sessions running on this machine (name, pid, sessionId, cwd, entrypoint). | read-only |
|
|
357
382
|
| `send_to_claude_session` | Sends to a Claude inbox and waits for a correlated reply. Receipt statuses distinguish `reply_received`, `sent_unconfirmed`, `reply_timeout`, and receiver policy outcomes such as `held` or `refused`. `waitSec: 0` sends without waiting for confirmation. Prefer an exact `sessionId` because session names can change. | destructive |
|
|
383
|
+
| `read_claude_delivery` | Inspects the latest recipient receipt or correlated reply by original message ID without resending or clearing the inbox. Receipts belong to this MCP process; an unknown ID after reconnect never proves non-delivery. | read-only |
|
|
358
384
|
| `read_claude_inbox` | Reads **and clears** messages Claude pushed over on its own, including replies that arrived late. | destructive |
|
|
359
385
|
| `read_claude_transcript` | Reads a Claude session's recent conversation without sending anything. | read-only |
|
|
360
386
|
| `bind_codex_thread` | Binds a Codex thread so every message from Claude is relayed into it, **visible in the Codex desktop app**. Pass an empty string to stop. | writes |
|
|
@@ -362,7 +388,15 @@ Under `owned`, a thread a human opened is **unreachable rather than merely restr
|
|
|
362
388
|
|
|
363
389
|
Every tool declares MCP annotation hints (`readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`), because a client decides whether a call needs a human in the loop from those hints and a missing one reads as "unknown". Two are worth naming: `read_claude_inbox` empties the inbox as it reads it, so it is **not** read-only despite the name, and `claude_bridge_status` registers the peer endpoint on first call, so it writes too.
|
|
364
390
|
|
|
365
|
-
|
|
391
|
+
Desktop task mode (`CODEX_BRIDGE_DESKTOP_TASKS=1` or the shared setting written by `codex-native-relay-install --desktop-tasks`) applies in both directions. `list_claude_sessions` only offers sessions advertised by Claude Desktop (`entrypoint: claude-desktop`), and sending refuses CLI or unknown entrypoints before connecting. Targets must match a unique exact session ID, PID, or name; partial names are not accepted. Open or reconnect an existing Code session in Claude Desktop at the intended project when none is available. Do not launch a replacement CLI session: CLI and Desktop have separate conversation histories. Legacy mode still supports CLI sessions.
|
|
392
|
+
|
|
393
|
+
`claude_bridge_status` reports the destination policy, eligible session count, and excluded non-Desktop count. Send receipts include the destination's `entrypoint`, `cwd`, and `sessionId`. A correlated reply confirms receipt in that session; it does not independently prove that its conversation is visible in the Desktop UI. Desktop sends require `expectedCwd`, an absolute independently verified project directory. The bridge resolves both paths and refuses a missing, nonexistent, parent, or different directory before sending; it rechecks the destination after queueing. Reconnect the bridge's MCP connection after upgrading to load the enforcement code.
|
|
394
|
+
|
|
395
|
+
Sends to the same Claude session run in order. An outstanding message blocks all further sends to that session, including `waitSec: 0`; changing wait time must not bypass a held or uncertain delivery. Use `read_claude_delivery` with the original message ID to observe `held`, `expired`, `refused`, or a late reply. A transport error after writing began also retains pending ownership. Different destination sessions remain independent. A process restart loses in-memory receipts: retain the original ID and inspect the existing recipient conversation before any resend; never treat an unknown ID as permission to retry.
|
|
396
|
+
|
|
397
|
+
Both bridge status tools report the loaded source fingerprint and process identity. Source or Desktop-routing configuration changes make the running process stale and block new sends until reconnect, while Claude inbox/receipt reads remain available. This guard applies only after the version containing it has loaded; upgrading files cannot retrofit the guard into an older process already running.
|
|
398
|
+
|
|
399
|
+
Claude's [inbound permission controls](https://code.claude.com/docs/en/cross-session-messaging#control-inbound-messages) remain authoritative. A missing sender permission class can cause a bypassing recipient to hold the message. Do not change `CLAUDE_BRIDGE_PERMISSION_MODE` or recipient settings merely to bypass a hold/refusal. Configure a sender class only when it truthfully represents every session using that MCP entry. If the Desktop UI does not expose the pending approval, report that boundary and keep the original receipt rather than creating a CLI session.
|
|
366
400
|
|
|
367
401
|
Claude Desktop disables the CLI-native `SendMessage` tool. For Desktop targets, the bridge requests an ordinary answer in the destination conversation and reads only a completed assistant turn descended from the injected message UUID. Unrelated human prompts and sidechains are excluded. Late answers remain available through the inbox while the bridge process is running. This does not change Desktop tool permissions or the receiver's inbound policy.
|
|
368
402
|
|
|
@@ -506,6 +540,8 @@ python3 -c "import json;[print(v['properties']['method'].get('const') or v['prop
|
|
|
506
540
|
|
|
507
541
|
**The Codex app says a thread is "open in another application".** The app-server holds a writer lock while the thread is loaded. With `releaseAfterTurn: true`, the bridge unsubscribes after completion; unload can still wait for the server's idle delay or another subscriber. Open the thread after it unloads. Use `stop_codex_app_server` only when all work on that shared server may be stopped. A thread held by a different Codex window must be released there. To keep a thread open in Codex Desktop while Claude messages into it, use the [native relay](#codex-desktop-native-relay), which never takes a second writer lock.
|
|
508
542
|
|
|
543
|
+
**Desktop delivery fails on macOS while the relay reports itself installed.** The companion still creates its socket and `native_relay_status` still reports the native pipe, because Codex Desktop accepts the connection and only then closes it: it authenticates the connecting process's code-signing identity first, and a companion running under a Node build signed by anyone other than the vendor is refused. Every send then fails hard as `NATIVE_DELIVERY_UNCONFIRMED` rather than falling back, since the companion was reached. The app records the refusal as `dynamic_app_tools_peer_rejected reason=untrusted-code-signing-identity` (`~/Library/Logs/com.openai.codex/<date>/`; a process the app did not spawn is logged as `missing-code-signing-identity` instead). Re-run `scripts/install-native-relay.mjs` and check its `relay runtime:` line — it must not say `process.execPath` when Codex Desktop is installed. Point `CODEX_NATIVE_RELAY_NODE` at the runtime inside the app bundle if discovery misses it, and restart Codex Desktop so it relaunches the companion.
|
|
544
|
+
|
|
509
545
|
**`claude_bridge_status` says the delivery backend is `app-server` on Windows or macOS with the relay installed.** The `delivery:` line carries the reason: *"no companion socket at …"* means Codex Desktop has not launched the companion, so restart the app after `install-native-relay.mjs` and check `codex mcp get codex-native-relay`; *"disabled by CODEX_BRIDGE_NATIVE_RELAY=0"* means it was switched off in the MCP server's `env`; *"unavailable on Linux"* means the native relay is not supported there. A relay that is reachable but has no executor thread fails at send time instead, with `RELAY_THREAD_UNCONFIGURED` naming both `CODEX_RELAY_ID` and the file to bootstrap.
|
|
510
546
|
|
|
511
547
|
**A thread opens against the wrong directory.** The same project sits at a different absolute path on each machine: on the shared drive's letter under Windows, under its mount point when that drive is visible from macOS (**read-only** there), and in a native checkout otherwise. Since 1.4.0 the bridge picks the candidate that both **exists and is writable** on the current machine and prints a `note: cwd remapped …` line whenever it rewrites one. If nothing usable exists it fails immediately instead of opening a thread somewhere wrong. Handing Codex a read-only cwd is a reliable way to hit the freeze above: it runs a few reads, then asks for write permission and stalls.
|
|
@@ -549,6 +585,7 @@ The bridge reads these from the environment its MCP client hands it — there is
|
|
|
549
585
|
| `CODEX_NATIVE_RELAY_SOCKET` | Windows named pipe or `$CODEX_HOME/native-relay.sock` on macOS | Override the companion endpoint on both halves of the relay. |
|
|
550
586
|
| `CODEX_NATIVE_RELAY_METHOD` | `tools/call` | The undocumented Codex Desktop JSON-RPC method the companion dispatches through; override only for a verified protocol change. |
|
|
551
587
|
| `CODEX_NATIVE_RELAY_NAME` | `codex-native-relay` | The MCP server name `scripts/install-native-relay.mjs` registers with Codex. |
|
|
588
|
+
| `CODEX_NATIVE_RELAY_NODE` | the runtime Codex Desktop ships on macOS, otherwise `process.execPath` | Node runtime `scripts/install-native-relay.mjs` registers the companion under. Codex Desktop rejects a peer whose code-signing identity is not its own, so the companion has to run under a runtime the app trusts. |
|
|
552
589
|
| `CLAUDE_BRIDGE_PEER_NAME` | `codex-<pid>` | The name Claude shows for this bridge in its agent list. |
|
|
553
590
|
| `CLAUDE_BRIDGE_CWD` | the process cwd | The working directory the peer advertises. |
|
|
554
591
|
| `CLAUDE_DESKTOP_CONFIG` | auto-detected | Override the config path used by `install-claude-desktop.mjs`. |
|
|
@@ -612,7 +649,7 @@ The Codex → Claude direction has its own live check:
|
|
|
612
649
|
|
|
613
650
|
```bash
|
|
614
651
|
npm run check:claude # list the Claude sessions Codex can see
|
|
615
|
-
CLAUDE_TARGET=<sessionId> CLAUDE_WAIT=150 npm run check:claude # deliver a message and wait for the answer
|
|
652
|
+
CLAUDE_TARGET=<sessionId> CLAUDE_EXPECTED_CWD=/absolute/project CLAUDE_WAIT=150 npm run check:claude # deliver a message and wait for the answer
|
|
616
653
|
```
|
|
617
654
|
|
|
618
655
|
A reply coming back proves both directions work.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@minhspark/codex-mcp-bridge",
|
|
3
|
-
"version": "1.13.
|
|
3
|
+
"version": "1.13.4",
|
|
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",
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
"codex-mcp-bridge-install": "scripts/install-claude-desktop.mjs",
|
|
28
28
|
"claude-mcp-bridge-install": "scripts/install-codex-mcp.mjs",
|
|
29
29
|
"codex-native-relay": "src/native-relay-companion.mjs",
|
|
30
|
-
"codex-native-relay-install": "scripts/install-native-relay.mjs"
|
|
30
|
+
"codex-native-relay-install": "scripts/install-native-relay.mjs",
|
|
31
|
+
"codex-npm-footer-install": "scripts/install-npm-footer.mjs"
|
|
31
32
|
},
|
|
32
33
|
"files": [
|
|
33
34
|
"src",
|
|
@@ -53,6 +54,8 @@
|
|
|
53
54
|
"install:relay": "node scripts/install-native-relay.mjs",
|
|
54
55
|
"uninstall:relay": "node scripts/install-native-relay.mjs --remove",
|
|
55
56
|
"install:agent": "node scripts/install-launch-agent.mjs",
|
|
57
|
+
"install:footer": "node scripts/install-npm-footer.mjs",
|
|
58
|
+
"uninstall:footer": "node scripts/install-npm-footer.mjs --remove",
|
|
56
59
|
"uninstall:agent": "node scripts/install-launch-agent.mjs --uninstall",
|
|
57
60
|
"version": "node scripts/sync-version.mjs && git add src/index.mjs src/claude-bridge.mjs src/native-relay-companion.mjs scripts/install-native-relay.mjs"
|
|
58
61
|
},
|
|
@@ -12,6 +12,7 @@ const transport = new StdioClientTransport({
|
|
|
12
12
|
});
|
|
13
13
|
const client = new Client({ name: "claude-bridge-check", version: "1.0.0" });
|
|
14
14
|
try {
|
|
15
|
+
console.log("This diagnostic starts a separate MCP process; it does not reload or verify an existing Desktop task's MCP connection.");
|
|
15
16
|
await client.connect(transport);
|
|
16
17
|
|
|
17
18
|
const tools = await client.listTools();
|
|
@@ -32,7 +33,7 @@ try {
|
|
|
32
33
|
console.log(`\nsending to "${target}" (waiting ${waitSec}s)...`);
|
|
33
34
|
const sent = await client.callTool({
|
|
34
35
|
name: "send_to_claude_session",
|
|
35
|
-
arguments: { target, message, waitSec },
|
|
36
|
+
arguments: { target, message, waitSec, expectedCwd: process.env.CLAUDE_EXPECTED_CWD },
|
|
36
37
|
});
|
|
37
38
|
console.log("\n--- send_to_claude_session ---\n" + sent.content[0].text);
|
|
38
39
|
if (sent.isError) throw new Error("Claude message roundtrip failed; receipt or outcome is unconfirmed");
|
|
@@ -5,6 +5,9 @@ import { fileURLToPath } from "node:url";
|
|
|
5
5
|
|
|
6
6
|
import { IS_WINDOWS, PLATFORM_LABEL, claudeDesktopConfigPath, resolveCodexBin } from "../src/platform.mjs";
|
|
7
7
|
import { desktopTasksConfigured } from "../src/native-relay.mjs";
|
|
8
|
+
import { exitForVersionRequest } from "../src/cli-version.mjs";
|
|
9
|
+
|
|
10
|
+
exitForVersionRequest(import.meta.url);
|
|
8
11
|
|
|
9
12
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
10
13
|
const cfgPath = process.env.CLAUDE_DESKTOP_CONFIG ?? claudeDesktopConfigPath();
|
|
@@ -5,6 +5,11 @@ import path from "node:path";
|
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
|
|
7
7
|
import { PLATFORM_LABEL, resolveCodexBin, spawnEnv } from "../src/platform.mjs";
|
|
8
|
+
import { exitForVersionRequest } from "../src/cli-version.mjs";
|
|
9
|
+
import { desktopTasksConfigured } from "../src/native-relay.mjs";
|
|
10
|
+
import { codexMcpRegistration } from "../src/codex-mcp-registration.mjs";
|
|
11
|
+
|
|
12
|
+
exitForVersionRequest(import.meta.url);
|
|
8
13
|
|
|
9
14
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
10
15
|
const codexBin = resolveCodexBin(process.env.CODEX_EXE);
|
|
@@ -26,22 +31,17 @@ if (remove) {
|
|
|
26
31
|
process.exit(0);
|
|
27
32
|
}
|
|
28
33
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const args = ["mcp", "add", serverName];
|
|
36
|
-
const peerName = process.env.CLAUDE_BRIDGE_PEER_NAME;
|
|
37
|
-
if (peerName) args.push("--env", `CLAUDE_BRIDGE_PEER_NAME=${peerName}`);
|
|
38
|
-
if (permissionMode) args.push("--env", `CLAUDE_BRIDGE_PERMISSION_MODE=${permissionMode}`);
|
|
39
|
-
args.push("--", process.execPath, entry);
|
|
40
|
-
|
|
41
|
-
run(args);
|
|
34
|
+
const servers = JSON.parse(run(["mcp", "list", "--json"]));
|
|
35
|
+
if (!Array.isArray(servers)) throw new Error("Codex returned an invalid MCP inventory; existing configuration was not changed");
|
|
36
|
+
const existing = servers.some((server) => server.name === serverName)
|
|
37
|
+
? JSON.parse(run(["mcp", "get", serverName, "--json"])) : null;
|
|
38
|
+
const registration = codexMcpRegistration({ name: serverName, existing, node: process.execPath, entry, env: process.env, desktopOnly: desktopTasksConfigured() });
|
|
39
|
+
run(registration.args);
|
|
42
40
|
|
|
43
41
|
console.log(`platform: ${PLATFORM_LABEL}`);
|
|
44
42
|
console.log(`registered MCP server "${serverName}" with Codex:`);
|
|
45
|
-
console.log(
|
|
46
|
-
console.log(
|
|
43
|
+
console.log(`command: ${process.execPath} ${entry}`);
|
|
44
|
+
console.log(`environment keys: ${registration.environmentKeys.join(", ")}`);
|
|
45
|
+
console.log("\nReconnect the MCP server in the existing Codex Desktop task, or restart the app and reopen that same task. Do not create a replacement task.");
|
|
46
|
+
console.log("Verify claude_bridge_status from that task: 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.");
|
|
47
47
|
console.log(`remove: node scripts/install-codex-mcp.mjs --remove`);
|
|
@@ -6,9 +6,21 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
|
|
7
7
|
import { CodexAppServerClient } from "../src/app-server-client.mjs";
|
|
8
8
|
import { bootstrapRelayThread, readRelayConfig, relayConfigPath, relaySocketPath, writeRelayConfig } from "../src/native-relay.mjs";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
IS_MACOS,
|
|
11
|
+
IS_WINDOWS,
|
|
12
|
+
PLATFORM_LABEL,
|
|
13
|
+
hasCodexDesktopApp,
|
|
14
|
+
homeDir,
|
|
15
|
+
resolveCodexBin,
|
|
16
|
+
resolveCodexDesktopNodeBin,
|
|
17
|
+
spawnEnv,
|
|
18
|
+
} from "../src/platform.mjs";
|
|
19
|
+
import { exitForVersionRequest } from "../src/cli-version.mjs";
|
|
10
20
|
|
|
11
|
-
|
|
21
|
+
exitForVersionRequest(import.meta.url);
|
|
22
|
+
|
|
23
|
+
const VERSION = "1.13.4";
|
|
12
24
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
13
25
|
const entry = path.join(root, "src", "native-relay-companion.mjs");
|
|
14
26
|
const serverName = process.env.CODEX_NATIVE_RELAY_NAME ?? "codex-native-relay";
|
|
@@ -53,9 +65,32 @@ try {
|
|
|
53
65
|
} catch {
|
|
54
66
|
// not registered yet
|
|
55
67
|
}
|
|
56
|
-
|
|
68
|
+
/**
|
|
69
|
+
* Registering under whichever Node happens to run this installer is what made
|
|
70
|
+
* the companion unreachable on macOS: Codex Desktop rejects a peer whose
|
|
71
|
+
* code-signing identity is not the vendor's, so the relay reported itself
|
|
72
|
+
* installed while every delivery failed. Resolve a runtime the app trusts
|
|
73
|
+
* instead, and print it - a mismatch has to be visible here rather than
|
|
74
|
+
* inferred later from a delivery that never confirms.
|
|
75
|
+
*/
|
|
76
|
+
const runtime = resolveCodexDesktopNodeBin();
|
|
77
|
+
|
|
78
|
+
run(["mcp", "add", serverName, "--", runtime.path, entry]);
|
|
57
79
|
|
|
58
80
|
console.log(`platform: ${PLATFORM_LABEL}`);
|
|
81
|
+
console.log(`relay runtime: ${runtime.path} (${runtime.source})`);
|
|
82
|
+
if (runtime.source === "process.execPath" && hasCodexDesktopApp()) {
|
|
83
|
+
console.log(
|
|
84
|
+
"\nNOTE: no Codex Desktop runtime was found, so the companion is registered under this Node.",
|
|
85
|
+
);
|
|
86
|
+
console.log(
|
|
87
|
+
"Codex Desktop rejects peers whose code-signing identity is not its own (untrusted-code-signing-identity),",
|
|
88
|
+
);
|
|
89
|
+
console.log(
|
|
90
|
+
"so delivery can fail while the relay still reports itself installed. Set CODEX_NATIVE_RELAY_NODE to the",
|
|
91
|
+
);
|
|
92
|
+
console.log("runtime shipped inside Codex Desktop if that happens.");
|
|
93
|
+
}
|
|
59
94
|
console.log(`registered MCP server "${serverName}" with Codex:`);
|
|
60
95
|
console.log(run(["mcp", "get", serverName]));
|
|
61
96
|
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { execFileSync } from "node:child_process";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { exitForVersionRequest } from "../src/cli-version.mjs";
|
|
8
|
+
|
|
9
|
+
exitForVersionRequest(import.meta.url);
|
|
10
|
+
|
|
11
|
+
const begin = "# codex-mcp-bridge npm footer: begin";
|
|
12
|
+
const end = "# codex-mcp-bridge npm footer: end";
|
|
13
|
+
const args = process.argv.slice(2);
|
|
14
|
+
let shell = process.platform === "win32" ? "powershell" : path.basename(process.env.SHELL || "/bin/bash");
|
|
15
|
+
const profiles = [];
|
|
16
|
+
let remove = false;
|
|
17
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
18
|
+
const arg = args[index];
|
|
19
|
+
if (arg === "--remove") remove = true;
|
|
20
|
+
else if (["--shell", "--profile"].includes(arg) && args[index + 1]) {
|
|
21
|
+
const value = args[++index];
|
|
22
|
+
if (arg === "--shell") shell = value;
|
|
23
|
+
else profiles.push(path.resolve(value));
|
|
24
|
+
} else throw new Error(`Unsupported argument: ${arg}`);
|
|
25
|
+
}
|
|
26
|
+
if (!["bash", "zsh", "powershell"].includes(shell)) throw new Error("Choose --shell bash, zsh, or powershell.");
|
|
27
|
+
const footerDirectory = path.join(os.homedir(), ".config", "codex-mcp-bridge");
|
|
28
|
+
const footerName = shell === "powershell" ? "npm-footer.ps1" : "npm-footer.sh";
|
|
29
|
+
const footerPath = path.join(footerDirectory, footerName);
|
|
30
|
+
if (!profiles.length) {
|
|
31
|
+
if (shell === "powershell") {
|
|
32
|
+
profiles.push(execFileSync("pwsh", ["-NoProfile", "-Command", "$PROFILE.CurrentUserAllHosts"], { encoding: "utf8" }).trim());
|
|
33
|
+
} else if (shell === "zsh") {
|
|
34
|
+
const directory = process.env.ZDOTDIR || os.homedir();
|
|
35
|
+
profiles.push(path.join(directory, ".zshrc"), path.join(directory, ".zprofile"));
|
|
36
|
+
} else {
|
|
37
|
+
profiles.push(path.join(os.homedir(), ".bashrc"));
|
|
38
|
+
profiles.push([".bash_profile", ".bash_login", ".profile"].map((name) => path.join(os.homedir(), name)).find((file) => fs.existsSync(file)) || path.join(os.homedir(), ".profile"));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const quote = (value) => shell === "powershell" ? `'${value.replaceAll("'", "''")}'` : `'${value.replaceAll("'", `'\\''`)}'`;
|
|
42
|
+
const loader = shell === "powershell"
|
|
43
|
+
? `if (Test-Path -LiteralPath ${quote(footerPath)}) { . ${quote(footerPath)} }`
|
|
44
|
+
: `if [ -n "\${BASH_VERSION:-}\${ZSH_VERSION:-}" ] && [ -r ${quote(footerPath)} ]; then . ${quote(footerPath)}; fi`;
|
|
45
|
+
const edits = [...new Set(profiles)].map((file) => {
|
|
46
|
+
if (!path.isAbsolute(file)) throw new Error(`Profile path must be absolute: ${file}`);
|
|
47
|
+
if (fs.existsSync(file) && (!fs.lstatSync(file).isFile() || fs.lstatSync(file).isSymbolicLink())) throw new Error(`Refusing to replace a non-regular profile: ${file}`);
|
|
48
|
+
const before = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
|
|
49
|
+
const newline = before.includes("\r\n") || shell === "powershell" ? "\r\n" : "\n";
|
|
50
|
+
const normalized = before.replaceAll("\r\n", "\n");
|
|
51
|
+
const begins = normalized.split(begin).length - 1;
|
|
52
|
+
const ends = normalized.split(end).length - 1;
|
|
53
|
+
if (begins !== ends || begins > 1) throw new Error(`Ambiguous managed footer block: ${file}`);
|
|
54
|
+
let unmanaged = normalized;
|
|
55
|
+
if (begins) {
|
|
56
|
+
const start = normalized.indexOf(begin);
|
|
57
|
+
const finish = normalized.indexOf(end, start);
|
|
58
|
+
if (finish < start) throw new Error(`Malformed managed footer block: ${file}`);
|
|
59
|
+
unmanaged = normalized.slice(0, start) + normalized.slice(finish + end.length).replace(/^\n/, "");
|
|
60
|
+
}
|
|
61
|
+
if (!remove && /^\s*(?:function\s+(?:global:)?)?npm\s*(?:\(\s*\))?\s*\{/im.test(unmanaged)) {
|
|
62
|
+
throw new Error(`An existing npm wrapper must be reviewed before installing the footer: ${file}`);
|
|
63
|
+
}
|
|
64
|
+
const after = (remove ? unmanaged : `${begin}\n${loader}\n${end}\n${unmanaged}`).replaceAll("\n", newline);
|
|
65
|
+
return { file, before, after };
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if (!remove) {
|
|
69
|
+
const source = fs.readFileSync(fileURLToPath(new URL(`./${footerName}`, import.meta.url)), "utf8");
|
|
70
|
+
fs.mkdirSync(footerDirectory, { recursive: true, mode: 0o700 });
|
|
71
|
+
if (fs.existsSync(footerPath) && (!fs.lstatSync(footerPath).isFile() || fs.lstatSync(footerPath).isSymbolicLink())) throw new Error(`Unsafe footer destination: ${footerPath}`);
|
|
72
|
+
if (!fs.existsSync(footerPath) || fs.readFileSync(footerPath, "utf8") !== source) {
|
|
73
|
+
fs.writeFileSync(footerPath, source, { mode: 0o600 });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
for (const { file, before, after } of edits) {
|
|
77
|
+
if (before === after) { console.log(`Unchanged: ${file}`); continue; }
|
|
78
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
79
|
+
const current = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
|
|
80
|
+
if (current !== before) throw new Error(`Profile changed during installation; rerun after reviewing it: ${file}`);
|
|
81
|
+
if (fs.existsSync(file)) {
|
|
82
|
+
const backup = `${file}.bak-npm-footer-${Date.now()}-${process.pid}`;
|
|
83
|
+
fs.copyFileSync(file, backup, fs.constants.COPYFILE_EXCL);
|
|
84
|
+
console.log(`Backup: ${backup}`);
|
|
85
|
+
}
|
|
86
|
+
const temporary = `${file}.tmp-npm-footer-${process.pid}`;
|
|
87
|
+
let temporaryCreated = false;
|
|
88
|
+
try {
|
|
89
|
+
fs.writeFileSync(temporary, after, { mode: 0o600, flag: "wx" });
|
|
90
|
+
temporaryCreated = true;
|
|
91
|
+
fs.renameSync(temporary, file);
|
|
92
|
+
} finally {
|
|
93
|
+
if (temporaryCreated && fs.existsSync(temporary)) fs.unlinkSync(temporary);
|
|
94
|
+
}
|
|
95
|
+
console.log(`${remove ? "Removed" : "Installed"}: ${file}`);
|
|
96
|
+
}
|
|
97
|
+
console.log(remove
|
|
98
|
+
? "Open a new shell to use npm without the footer."
|
|
99
|
+
: `Reload the current shell with: . ${quote(footerPath)}`);
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
if ($MyInvocation.InvocationName -eq '.') {
|
|
2
|
+
Set-Alias -Name npm -Value $PSCommandPath -Scope Global
|
|
3
|
+
return
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
$npmArguments = @($args)
|
|
7
|
+
$nativeNpm = Get-Command npm -CommandType Application, ExternalScript -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
8
|
+
if (-not $nativeNpm) {
|
|
9
|
+
throw [System.Management.Automation.CommandNotFoundException]::new('The npm executable was not found on PATH.')
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
$matchesInstall = $npmArguments.Count -ge 3 -and $npmArguments[0] -cin @('install', 'i')
|
|
13
|
+
$isGlobal = $false
|
|
14
|
+
$targetCount = 0
|
|
15
|
+
$prefixArguments = @()
|
|
16
|
+
$dryRunOverride = $null
|
|
17
|
+
for ($argumentIndex = 1; $matchesInstall -and $argumentIndex -lt $npmArguments.Count; $argumentIndex++) {
|
|
18
|
+
$argument = [string]$npmArguments[$argumentIndex]
|
|
19
|
+
if ($argument -cin @('-g', '--global', '--global=true')) {
|
|
20
|
+
$isGlobal = $true
|
|
21
|
+
} elseif ($argument -cmatch '^@minhspark/codex-mcp-bridge(?:@[^\s]+)?$') {
|
|
22
|
+
$targetCount++
|
|
23
|
+
} elseif ($argument -cin @('--no-fund', '--no-audit', '--force', '--ignore-scripts', '--foreground-scripts')) {
|
|
24
|
+
continue
|
|
25
|
+
} elseif ($argument -cin @('--dry-run', '--dry-run=true')) {
|
|
26
|
+
$dryRunOverride = $true
|
|
27
|
+
} elseif ($argument -cin @('--no-dry-run', '--dry-run=false')) {
|
|
28
|
+
$dryRunOverride = $false
|
|
29
|
+
} elseif ($argument -ceq '--prefix') {
|
|
30
|
+
$argumentIndex++
|
|
31
|
+
if ($argumentIndex -ge $npmArguments.Count -or -not $npmArguments[$argumentIndex] -or [string]$npmArguments[$argumentIndex] -clike '-*' -or $prefixArguments.Count) {
|
|
32
|
+
$matchesInstall = $false
|
|
33
|
+
} else {
|
|
34
|
+
$prefixArguments = @('--prefix', [string]$npmArguments[$argumentIndex])
|
|
35
|
+
}
|
|
36
|
+
} elseif ($argument -cmatch '^--prefix=(.+)$' -and -not $prefixArguments.Count) {
|
|
37
|
+
$prefixArguments = @($argument)
|
|
38
|
+
} else {
|
|
39
|
+
$matchesInstall = $false
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (-not $matchesInstall -or -not $isGlobal -or $targetCount -ne 1) {
|
|
44
|
+
$global:LASTEXITCODE = 0
|
|
45
|
+
& $nativeNpm @npmArguments
|
|
46
|
+
exit $LASTEXITCODE
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
$PSNativeCommandUseErrorActionPreference = $false
|
|
50
|
+
$readVersion = {
|
|
51
|
+
param([string]$PackageRoot)
|
|
52
|
+
try {
|
|
53
|
+
$metadataPath = Join-Path $PackageRoot '@minhspark/codex-mcp-bridge/package.json'
|
|
54
|
+
if (-not [System.IO.File]::Exists($metadataPath)) {
|
|
55
|
+
return @{ Status = 'missing'; Version = $null }
|
|
56
|
+
}
|
|
57
|
+
$metadata = [System.IO.File]::ReadAllText($metadataPath) | ConvertFrom-Json -ErrorAction Stop
|
|
58
|
+
if ($metadata.name -cne '@minhspark/codex-mcp-bridge' -or $metadata.version -isnot [string] -or $metadata.version -cnotmatch '^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$') {
|
|
59
|
+
return @{ Status = 'unknown'; Version = $null }
|
|
60
|
+
}
|
|
61
|
+
return @{ Status = 'known'; Version = $metadata.version }
|
|
62
|
+
} catch {
|
|
63
|
+
return @{ Status = 'unknown'; Version = $null }
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
$readRoot = {
|
|
67
|
+
try {
|
|
68
|
+
$global:LASTEXITCODE = 0
|
|
69
|
+
$rootOutput = @(& $nativeNpm root --global @prefixArguments 2>$null)
|
|
70
|
+
if ($LASTEXITCODE -ne 0 -or $rootOutput.Count -ne 1) {
|
|
71
|
+
return $null
|
|
72
|
+
}
|
|
73
|
+
$packageRoot = ([string]$rootOutput[0]).Trim()
|
|
74
|
+
if (-not $packageRoot -or -not [System.IO.Path]::IsPathRooted($packageRoot)) {
|
|
75
|
+
return $null
|
|
76
|
+
}
|
|
77
|
+
return $packageRoot
|
|
78
|
+
} catch {
|
|
79
|
+
return $null
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
$dryRun = $dryRunOverride
|
|
84
|
+
if ($null -eq $dryRun) {
|
|
85
|
+
try {
|
|
86
|
+
$global:LASTEXITCODE = 0
|
|
87
|
+
$configOutput = @(& $nativeNpm config get dry-run --global @prefixArguments 2>$null)
|
|
88
|
+
if ($LASTEXITCODE -eq 0 -and $configOutput.Count -eq 1) {
|
|
89
|
+
$configValue = ([string]$configOutput[0]).Trim()
|
|
90
|
+
if ($configValue -ceq 'true') {
|
|
91
|
+
$dryRun = $true
|
|
92
|
+
} elseif ($configValue -ceq 'false') {
|
|
93
|
+
$dryRun = $false
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
} catch {
|
|
97
|
+
$dryRun = $null
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
$beforeRoot = & $readRoot
|
|
102
|
+
$before = @{ Status = 'unknown'; Version = $null }
|
|
103
|
+
if ($beforeRoot) {
|
|
104
|
+
$before = & $readVersion $beforeRoot
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
$global:LASTEXITCODE = 0
|
|
108
|
+
& $nativeNpm @npmArguments
|
|
109
|
+
$npmExitCode = $LASTEXITCODE
|
|
110
|
+
try {
|
|
111
|
+
if ($npmExitCode -ne 0) {
|
|
112
|
+
Write-Host "Failed to install: @minhspark/codex-mcp-bridge (exit code $npmExitCode). See npm error above."
|
|
113
|
+
exit $npmExitCode
|
|
114
|
+
}
|
|
115
|
+
if ($null -eq $dryRun) {
|
|
116
|
+
Write-Host 'Warning: npm completed, but installation mode could not be verified.'
|
|
117
|
+
exit $npmExitCode
|
|
118
|
+
}
|
|
119
|
+
if ($dryRun) {
|
|
120
|
+
Write-Host 'Dry run completed: @minhspark/codex-mcp-bridge (no changes applied).'
|
|
121
|
+
exit $npmExitCode
|
|
122
|
+
}
|
|
123
|
+
$afterRoot = & $readRoot
|
|
124
|
+
$after = @{ Status = 'unknown'; Version = $null }
|
|
125
|
+
if ($afterRoot) {
|
|
126
|
+
$after = & $readVersion $afterRoot
|
|
127
|
+
}
|
|
128
|
+
if ($after.Status -ne 'known' -or $before.Status -eq 'unknown' -or $beforeRoot -cne $afterRoot) {
|
|
129
|
+
Write-Host 'Warning: npm completed, but the installed @minhspark/codex-mcp-bridge version could not be verified.'
|
|
130
|
+
} elseif ($before.Status -eq 'missing') {
|
|
131
|
+
Write-Host "Successfully installed: @minhspark/codex-mcp-bridge v$($after.Version)"
|
|
132
|
+
} elseif ($before.Version -ceq $after.Version) {
|
|
133
|
+
Write-Host "Already up to date: @minhspark/codex-mcp-bridge v$($after.Version)"
|
|
134
|
+
} else {
|
|
135
|
+
Write-Host "Successfully updated: @minhspark/codex-mcp-bridge v$($before.Version) -> v$($after.Version)"
|
|
136
|
+
}
|
|
137
|
+
} finally {
|
|
138
|
+
$global:LASTEXITCODE = $npmExitCode
|
|
139
|
+
}
|
|
140
|
+
exit $npmExitCode
|