@minhspark/codex-mcp-bridge 1.13.2 → 1.13.3

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 CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  Follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [SemVer](https://semver.org/).
4
4
 
5
+ ## [1.13.3] - 2026-09-05
6
+
7
+ ### Fixed
8
+
9
+ - 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.
10
+ - Support `--version` and `-v` on every packaged command without starting servers, opening sockets, or mutating client configuration.
11
+ - Preserve PowerShell's `$?` and conditional `&&`/`||` behavior as well as `$LASTEXITCODE` by invoking its footer through a script alias.
12
+
13
+ ### Added
14
+
15
+ - 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.
16
+ - `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.
17
+
18
+ ### Verification
19
+
20
+ - 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.
21
+
5
22
  ## [1.13.2] - 2026-09-05
6
23
 
7
24
  ### 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 four commands on your PATH — `codex-mcp-bridge` and `claude-mcp-bridge` are the two servers, `codex-mcp-bridge-install` and `claude-mcp-bridge-install` do the wiring in the steps below. Wherever this README runs `node scripts/install-claude-desktop.mjs`, an installed copy runs `codex-mcp-bridge-install` instead.
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@minhspark/codex-mcp-bridge",
3
- "version": "1.13.2",
3
+ "version": "1.13.3",
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
  },
@@ -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,9 @@ 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
+
10
+ exitForVersionRequest(import.meta.url);
8
11
 
9
12
  const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
10
13
  const codexBin = resolveCodexBin(process.env.CODEX_EXE);
@@ -7,8 +7,11 @@ import { fileURLToPath } from "node:url";
7
7
  import { CodexAppServerClient } from "../src/app-server-client.mjs";
8
8
  import { bootstrapRelayThread, readRelayConfig, relayConfigPath, relaySocketPath, writeRelayConfig } from "../src/native-relay.mjs";
9
9
  import { IS_MACOS, IS_WINDOWS, PLATFORM_LABEL, homeDir, resolveCodexBin, spawnEnv } from "../src/platform.mjs";
10
+ import { exitForVersionRequest } from "../src/cli-version.mjs";
10
11
 
11
- const VERSION = "1.13.2";
12
+ exitForVersionRequest(import.meta.url);
13
+
14
+ const VERSION = "1.13.3";
12
15
  const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
13
16
  const entry = path.join(root, "src", "native-relay-companion.mjs");
14
17
  const serverName = process.env.CODEX_NATIVE_RELAY_NAME ?? "codex-native-relay";
@@ -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
@@ -0,0 +1,81 @@
1
+ _codex_bridge_installed_version() {
2
+ command node --input-type=module -e 'import fs from "node:fs"; const value = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); if (value.name !== "@minhspark/codex-mcp-bridge" || typeof value.version !== "string" || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(value.version)) process.exit(1); process.stdout.write(value.version);' "$1/@minhspark/codex-mcp-bridge/package.json" 2>/dev/null
3
+ }
4
+
5
+ npm() {
6
+ if [ "$#" -eq 0 ]; then command npm; return $?; fi
7
+ local bridge_command="${1-}" bridge_global=0 bridge_packages=0 bridge_supported=1
8
+ local bridge_dry_run=auto bridge_root= bridge_before= bridge_after= bridge_install_status=0
9
+ local bridge_after_root= bridge_mode=unknown bridge_before_verified=1
10
+ local -a bridge_arguments bridge_context
11
+ bridge_arguments=("$@")
12
+ bridge_context=(--global)
13
+ case "$bridge_command" in
14
+ install|i) shift ;;
15
+ *) command npm "${bridge_arguments[@]}"; return $? ;;
16
+ esac
17
+ while [ "$#" -gt 0 ]; do
18
+ case "$1" in
19
+ -g|--global|--global=true) bridge_global=1 ;;
20
+ @minhspark/codex-mcp-bridge|@minhspark/codex-mcp-bridge@?*) bridge_packages=$((bridge_packages + 1)) ;;
21
+ --dry-run|--dry-run=true) bridge_dry_run=true ;;
22
+ --no-dry-run|--dry-run=false) bridge_dry_run=false ;;
23
+ --no-fund|--no-audit|--force|--ignore-scripts|--foreground-scripts) ;;
24
+ --prefix=*)
25
+ if [ "${#bridge_context[@]}" -ne 1 ] || [ "$1" = --prefix= ]; then bridge_supported=0; break; fi
26
+ bridge_context+=("$1")
27
+ ;;
28
+ --prefix)
29
+ if [ "$#" -lt 2 ] || [ "${#bridge_context[@]}" -ne 1 ] || [ -z "$2" ]; then bridge_supported=0; break; fi
30
+ case "$2" in -*) bridge_supported=0; break ;; esac
31
+ bridge_context+=("$1" "$2")
32
+ shift
33
+ ;;
34
+ *) bridge_supported=0 ;;
35
+ esac
36
+ shift
37
+ done
38
+ if [ "$bridge_supported" -ne 1 ] || [ "$bridge_global" -ne 1 ] || [ "$bridge_packages" -ne 1 ]; then
39
+ command npm "${bridge_arguments[@]}"
40
+ return $?
41
+ fi
42
+ if [ "$bridge_dry_run" = auto ]; then
43
+ bridge_mode="$(command npm config get dry-run "${bridge_context[@]}" 2>/dev/null)" || bridge_mode=unknown
44
+ else
45
+ bridge_mode="$bridge_dry_run"
46
+ fi
47
+ if [ "$bridge_mode" = false ]; then
48
+ bridge_root="$(command npm root "${bridge_context[@]}" 2>/dev/null)" || bridge_root=
49
+ case "$bridge_root" in
50
+ /*)
51
+ if [ -e "$bridge_root/@minhspark/codex-mcp-bridge/package.json" ]; then
52
+ bridge_before="$(_codex_bridge_installed_version "$bridge_root")" || bridge_before_verified=0
53
+ fi
54
+ ;;
55
+ *) bridge_before_verified=0 ;;
56
+ esac
57
+ fi
58
+ command npm "${bridge_arguments[@]}" || bridge_install_status=$?
59
+ if [ "$bridge_install_status" -ne 0 ]; then
60
+ printf 'Failed to install: @minhspark/codex-mcp-bridge (exit code %s). See npm error above.\n' "$bridge_install_status" >&2
61
+ elif [ "$bridge_mode" = true ]; then
62
+ printf 'Dry run completed: @minhspark/codex-mcp-bridge (no changes applied).\n'
63
+ elif [ "$bridge_mode" != false ]; then
64
+ printf 'Warning: npm completed, but installation mode could not be verified.\n' >&2
65
+ else
66
+ bridge_after_root="$(command npm root "${bridge_context[@]}" 2>/dev/null)" || bridge_after_root=
67
+ case "$bridge_after_root" in
68
+ /*) bridge_after="$(_codex_bridge_installed_version "$bridge_after_root")" || bridge_after= ;;
69
+ esac
70
+ if [ -z "$bridge_after" ] || [ "$bridge_before_verified" -ne 1 ] || [ "$bridge_after_root" != "$bridge_root" ]; then
71
+ printf 'Warning: npm completed, but the installed @minhspark/codex-mcp-bridge version could not be verified.\n' >&2
72
+ elif [ "$bridge_after_root" = "$bridge_root" ] && [ "$bridge_after" = "$bridge_before" ]; then
73
+ printf 'Already up to date: @minhspark/codex-mcp-bridge v%s\n' "$bridge_after"
74
+ elif [ "$bridge_after_root" = "$bridge_root" ] && [ -n "$bridge_before" ]; then
75
+ printf 'Successfully updated: @minhspark/codex-mcp-bridge v%s -> v%s\n' "$bridge_before" "$bridge_after"
76
+ else
77
+ printf 'Successfully installed: @minhspark/codex-mcp-bridge v%s\n' "$bridge_after"
78
+ fi
79
+ fi
80
+ return "$bridge_install_status"
81
+ }
@@ -7,8 +7,11 @@ import { CodexAppServerClient } from "./app-server-client.mjs";
7
7
  import { PLATFORM_LABEL } from "./platform.mjs";
8
8
  import { PeerEndpoint, findClaudeSession, listClaudeSessions, readTranscript } from "./peer-protocol.mjs";
9
9
  import { createThreadDelivery } from "./thread-delivery.mjs";
10
+ import { exitForVersionRequest } from "./cli-version.mjs";
10
11
 
11
- const VERSION = "1.13.2";
12
+ exitForVersionRequest(import.meta.url);
13
+
14
+ const VERSION = "1.13.3";
12
15
  const FORWARD_MIN_INTERVAL_MS = 5000;
13
16
  const FORWARD_MAX_PER_SESSION = 50;
14
17
 
@@ -0,0 +1,20 @@
1
+ import { readFileSync, realpathSync, writeSync } from "node:fs";
2
+ import { fileURLToPath } from "node:url";
3
+
4
+ export function exitForVersionRequest(entrypointUrl) {
5
+ if (!process.argv.slice(2).some((arg) => arg === "--version" || arg === "-v")) return;
6
+ if (!process.argv[1]) return;
7
+
8
+ let entrypoint;
9
+ try {
10
+ entrypoint = realpathSync(process.argv[1]);
11
+ } catch (error) {
12
+ if (error.code === "ENOENT" || error.code === "ENOTDIR") return;
13
+ throw error;
14
+ }
15
+ if (entrypoint !== realpathSync(fileURLToPath(entrypointUrl))) return;
16
+
17
+ const { version } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
18
+ writeSync(process.stdout.fd, `${version}\n`);
19
+ process.exit(0);
20
+ }
package/src/index.mjs CHANGED
@@ -24,8 +24,11 @@ import { runTurn } from "./turn.mjs";
24
24
  import { BridgeSecurityPolicy } from "./security-policy.mjs";
25
25
  import { DesktopTaskDelivery, DESKTOP_TOOL_BUDGET_MS } from "./thread-delivery.mjs";
26
26
  import { desktopTasksConfigured } from "./native-relay.mjs";
27
+ import { exitForVersionRequest } from "./cli-version.mjs";
27
28
 
28
- const VERSION = "1.13.2";
29
+ exitForVersionRequest(import.meta.url);
30
+
31
+ const VERSION = "1.13.3";
29
32
  const log = (msg) => process.stderr.write(`[codex-mcp-bridge] ${msg}\n`);
30
33
 
31
34
  /**
@@ -122,17 +125,23 @@ async function delegateDesktopTask({ cwd, prompt, name, model, effort, timeoutSe
122
125
  if (openInApp ?? DEFAULT_OPEN_IN_APP) {
123
126
  try {
124
127
  await desktopTasks.open(created.threadId, { deadline });
125
- notes.push("opened in Codex Desktop while the task runs");
128
+ notes.push(created.reused ? "opened the existing task in Codex Desktop" : "opened in Codex Desktop while the task runs");
126
129
  } catch (err) {
127
- notes.push(`task was accepted; opening its page failed: ${err.message}`);
130
+ notes.push(`${created.reused ? "existing task retained" : "task was accepted"}; opening its page failed: ${err.message}`);
128
131
  }
129
132
  }
130
133
  const lines = [
131
134
  created.reused ? "Reused the existing Codex Desktop task; the prompt was not resent" : "Delegated through Codex Desktop", `threadId: ${created.threadId}`, `name: ${created.name}`,
132
- `cwd: ${created.cwd}`, `projectId: ${created.projectId}`, `project: ${created.projectName}`,
135
+ `cwd: ${created.cwd}`,
136
+ ...(created.projectAssignmentStatus === "unverified"
137
+ ? [`expected projectId: ${created.expectedProjectId}`, `expected project: ${created.expectedProjectName ?? "(unnamed)"}`, "project assignment: unverified", created.projectAssignmentNote]
138
+ : [`projectId: ${created.projectId}`, `project: ${created.projectName ?? "(unnamed)"}`, ...(created.projectAssignmentStatus === "verified" ? ["project assignment: verified in Desktop's current listing"] : [])]),
133
139
  "permissions: Codex Desktop settings; no external app-server writer",
134
- ...(created.promptChanged ? ["The edited brief was not sent. Use send_to_codex_thread with this threadId for a continuation, or a distinct title for separate work."] : []), ...notes,
140
+ ...(created.promptChanged ? [created.projectAssignmentStatus === "unverified"
141
+ ? "The edited brief was not sent. Inspect this task's project assignment before continuing it."
142
+ : "The edited brief was not sent. Use send_to_codex_thread with this threadId for a continuation, or a distinct title for separate work."] : []), ...notes,
135
143
  ];
144
+ if (created.projectAssignmentStatus === "unverified") return textResult([...lines, "status: existing task retained; project assignment needs inspection"].join("\n"));
136
145
  if (!waitForReply) return textResult([...lines, created.reused ? "status: existing task; read it to check its current progress" : "status: accepted; the task is running in Desktop"].join("\n"));
137
146
  try {
138
147
  const result = await desktopTasks.wait(created.threadId, { timeoutMs: Math.max(0, deadline - Date.now()) });
@@ -19,8 +19,11 @@ import {
19
19
  decodeNativeToolResult,
20
20
  } from "./native-relay.mjs";
21
21
  import { IS_WINDOWS, PLATFORM_LABEL } from "./platform.mjs";
22
+ import { exitForVersionRequest } from "./cli-version.mjs";
22
23
 
23
- const VERSION = "1.13.2";
24
+ exitForVersionRequest(import.meta.url);
25
+
26
+ const VERSION = "1.13.3";
24
27
  const log = (msg) => process.stderr.write(`[native-relay] ${msg}\n`);
25
28
 
26
29
  function errorResponse(code, message) {
@@ -129,7 +129,45 @@ export class DesktopTaskDelivery {
129
129
  if (thread?.id !== receipt.threadId || thread.hostId !== "local" || !thread.cwd || path.relative(cwd, realpathSync.native(thread.cwd))) throw new Error(`Existing Desktop task ${receipt.threadId} did not confirm the requested local workspace. No prompt was sent.`);
130
130
  this.security.assertCwd(thread.cwd);
131
131
  this.security.assertThread(thread.id, thread.cwd);
132
- return { threadId: receipt.threadId, name: thread.title ?? receipt.name ?? "(unnamed)", cwd, projectId: receipt.projectId, projectName: receipt.projectName, backend: NATIVE_BACKEND, reused: true, promptChanged: receipt.promptHash !== promptHash };
132
+ const assignment = await this.receiptProjectAssignment(receipt, cwd, { deadline });
133
+ return { threadId: receipt.threadId, name: thread.title ?? receipt.name ?? "(unnamed)", cwd, ...assignment, backend: NATIVE_BACKEND, reused: true, promptChanged: receipt.promptHash !== promptHash };
134
+ }
135
+
136
+ async receiptProjectAssignment(receipt, cwd, { deadline }) {
137
+ let project;
138
+ try {
139
+ const listed = await this.request("list_projects", {}, { deadline });
140
+ if (!Array.isArray(listed?.projects)) throw new Error("Desktop returned no project list");
141
+ project = matchDesktopProject(listed.projects, cwd);
142
+ if (receipt.projectId && receipt.projectId !== project.projectId) throw new Error("The saved project identity changed for this directory");
143
+ } catch (err) {
144
+ throw new Error(`Existing Desktop task ${receipt.threadId}'s saved project could not be verified: ${err.message}. No prompt was resent and no duplicate task was created. Inspect this existing task and its project settings.`, { cause: err });
145
+ }
146
+ const unverified = (reason) => ({
147
+ expectedProjectId: project.projectId,
148
+ expectedProjectName: project.label,
149
+ projectAssignmentStatus: "unverified",
150
+ projectAssignmentNote: `${reason} The task's current project assignment is unverified. Its workspace and existing ID were retained; no prompt was resent and no duplicate task was created. Inspect this task in Codex Desktop.`,
151
+ });
152
+ let snapshot;
153
+ try {
154
+ snapshot = await this.request("list_threads", { limit: 50 }, { deadline });
155
+ if (!Array.isArray(snapshot?.threads) || !Array.isArray(snapshot?.pinnedThreads)) throw new Error("Desktop returned an invalid thread list");
156
+ if (snapshot.unavailableHosts?.some((host) => host === "local" || host?.hostId === "local")) throw new Error("The local Desktop host is unavailable");
157
+ } catch (err) {
158
+ return unverified(`Desktop's recent/pinned listing could not be confirmed: ${err.message}.`);
159
+ }
160
+ const observed = [...snapshot.pinnedThreads, ...snapshot.threads].filter((thread) => thread?.id === receipt.threadId && thread.kind === "codex" && thread.hostId === "local");
161
+ if (observed.length === 0) return unverified("This task is absent from Desktop's recent/pinned listing.");
162
+ for (const thread of observed) {
163
+ if (thread.projectId !== undefined && thread.projectId !== project.projectId) throw new Error(`Existing Desktop task ${receipt.threadId}'s project assignment changed. No prompt was resent and no duplicate task was created. Inspect its assignment in Codex Desktop.`);
164
+ if (thread.cwd) {
165
+ this.security.assertCwd(thread.cwd);
166
+ if (path.relative(cwd, realpathSync.native(thread.cwd))) throw new Error(`Existing Desktop task ${receipt.threadId}'s listed workspace changed. No prompt was resent and no duplicate task was created.`);
167
+ }
168
+ }
169
+ if (observed.some((thread) => thread.projectId === undefined || !thread.cwd)) return unverified("Desktop's listing omitted this task's project or workspace metadata.");
170
+ return { projectId: project.projectId, projectName: project.label, projectAssignmentStatus: "verified" };
133
171
  }
134
172
 
135
173
  async create({ cwd, prompt, name, dedupeName = name, model, effort, deadline = this.now() + DESKTOP_TOOL_BUDGET_MS }) {