@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 +17 -0
- package/README.md +30 -1
- package/package.json +5 -2
- package/scripts/install-claude-desktop.mjs +3 -0
- package/scripts/install-codex-mcp.mjs +3 -0
- package/scripts/install-native-relay.mjs +4 -1
- 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 +4 -1
- package/src/cli-version.mjs +20 -0
- package/src/index.mjs +14 -5
- package/src/native-relay-companion.mjs +4 -1
- package/src/thread-delivery.mjs +39 -1
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
|
|
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.
|
|
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
|
-
|
|
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
|
+
}
|
package/src/claude-bridge.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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(
|
|
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}`,
|
|
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 ? [
|
|
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
|
-
|
|
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) {
|
package/src/thread-delivery.mjs
CHANGED
|
@@ -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
|
-
|
|
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 }) {
|