xmux-bridge 1.0.40 → 1.2.0
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/README.md +22 -58
- package/{bridge-mcp-server.js → mcp/servers/bridge.js} +6 -2
- package/{xmux-lead-mcp-server.js → mcp/servers/lead.js} +6 -2
- package/{scripts/setup_claude_mcp.js → mcp/setup/claude.js} +47 -22
- package/{scripts/setup_xmux_codex_mcp.js → mcp/setup/codex.js} +126 -16
- package/{scripts/setup_copilot_mcp.js → mcp/setup/copilot.js} +16 -9
- package/{scripts/setup_gemini_mcp.js → mcp/setup/gemini.js} +12 -10
- package/package.json +8 -14
- package/bin/xmux +0 -9
- package/scripts/trust_codex_project.js +0 -44
- package/scripts/trust_copilot_project.js +0 -49
- package/scripts/trust_gemini_project.js +0 -47
- package/xmux-bridge.zsh +0 -481
- package/xmux.zsh +0 -5146
package/README.md
CHANGED
|
@@ -24,11 +24,6 @@ brew tap DwvN-Lee/xmux
|
|
|
24
24
|
brew install xmux
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
Homebrew owns the stable runtime under `$(brew --prefix)/opt/xmux/libexec`.
|
|
28
|
-
The installed `xmux` command exports that path as `XMUX_INSTALL_DIR` and then
|
|
29
|
-
execs the runtime wrapper in `libexec/bin/xmux`. Ad hoc local directories, npx
|
|
30
|
-
caches, and zsh plugin directories are not part of the normal runtime path.
|
|
31
|
-
|
|
32
27
|
Configure Codex integration explicitly:
|
|
33
28
|
|
|
34
29
|
```bash
|
|
@@ -36,21 +31,8 @@ xmux setup-codex
|
|
|
36
31
|
xmux doctor-codex
|
|
37
32
|
```
|
|
38
33
|
|
|
39
|
-
|
|
40
|
-
that
|
|
41
|
-
versioned npm package, points that MCP runtime back at Homebrew with
|
|
42
|
-
`XMUX_INSTALL_DIR`, adds the installed `xmux` path to Codex shell policy,
|
|
43
|
-
installs the scoped XMux command rule, and refreshes available XMux skills
|
|
44
|
-
under `~/.codex/skills`. Runtime-only installs do not include skill source
|
|
45
|
-
files, so pass an external skill source when refreshing skills:
|
|
46
|
-
|
|
47
|
-
```bash
|
|
48
|
-
xmux setup-codex --skills-dir /path/to/xmux-skills
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
`XMUX_CODEX_SKILLS_DIR` provides the same source path for automation. Without
|
|
52
|
-
`--skills-dir` or `XMUX_CODEX_SKILLS_DIR`, `setup-codex` skips skill refresh
|
|
53
|
-
and leaves existing user-owned skills untouched.
|
|
34
|
+
`xmux setup-codex` registers XMux with Codex, and `xmux doctor-codex` checks
|
|
35
|
+
that the integration is ready.
|
|
54
36
|
|
|
55
37
|
Start the Codex lead from the target project directory:
|
|
56
38
|
|
|
@@ -148,7 +130,7 @@ Runtime state is project-local:
|
|
|
148
130
|
Runtime path environment names are now split by responsibility:
|
|
149
131
|
|
|
150
132
|
```text
|
|
151
|
-
XMUX_INSTALL_DIR # XMux
|
|
133
|
+
XMUX_INSTALL_DIR # XMux install root
|
|
152
134
|
XMUX_PROJECT_DIR # project root where Codex is working
|
|
153
135
|
XMUX_STATE_DIR # project-local runtime state, usually $XMUX_PROJECT_DIR/.codex/xmux
|
|
154
136
|
```
|
|
@@ -156,36 +138,26 @@ XMUX_STATE_DIR # project-local runtime state, usually $XMUX_PROJECT_DIR/.code
|
|
|
156
138
|
Codex uses the normal user runtime under `~/.codex`. XMux does not create an
|
|
157
139
|
isolated Codex home for a team, and Codex teammate mode is unsupported.
|
|
158
140
|
|
|
159
|
-
Agent automation uses `xmux`
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
bootstrap command remains `xmux -n <session>` after setup; ad hoc local paths
|
|
163
|
-
and shell-loading details are not part of the agent contract.
|
|
141
|
+
Agent automation uses the installed `xmux` command that `xmux setup-codex`
|
|
142
|
+
makes available to Codex. The user-facing bootstrap command remains
|
|
143
|
+
`xmux -n <session>` after setup.
|
|
164
144
|
|
|
165
145
|
The Codex lead MCP server is `xmux_lead`. `xmux setup-codex` configures it so
|
|
166
146
|
Codex can route requests, wait for teammate responses, read events, and inspect
|
|
167
147
|
team status.
|
|
168
|
-
The
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
`
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
Homebrew does not install Codex skills or repo-local plugin files; normal
|
|
182
|
-
runtime operation depends on the installed `xmux` command and
|
|
183
|
-
`XMUX_INSTALL_DIR`, not a checkout path.
|
|
184
|
-
|
|
185
|
-
The plugin skill source of truth is `plugins/xmux/skills`; the top-level
|
|
186
|
-
`skills/` directory is a mirrored distribution copy for explicit skill refresh
|
|
187
|
-
workflows. Users explicitly invoke Codex skills with `$`, for example
|
|
188
|
-
`$xmux-teams`. The official XMux skills cover agent-facing orchestration flows:
|
|
148
|
+
The installed `xmux` command owns the tmux runtime. The `xmux_lead` MCP server
|
|
149
|
+
is delivered as a versioned npm entrypoint, and Codex skills are optional
|
|
150
|
+
shortcuts for orchestrating that runtime. The MCP command is install-scoped and
|
|
151
|
+
does not pin `XMUX_PROJECT_DIR`/`XMUX_STATE_DIR`; those values come from the
|
|
152
|
+
active `xmux -n <session>` lead runtime.
|
|
153
|
+
|
|
154
|
+
Provider teammates write responses through the versioned npm `xmux-bridge`
|
|
155
|
+
entrypoint, using the team runtime environment prepared by XMux. MCP and
|
|
156
|
+
mailbox paths are implementation details behind Codex-led teammate
|
|
157
|
+
orchestration.
|
|
158
|
+
|
|
159
|
+
Users can ask for teammate work in natural language. When XMux skills are
|
|
160
|
+
available in Codex, the official skill shortcuts are:
|
|
189
161
|
|
|
190
162
|
```text
|
|
191
163
|
$xmux-teams
|
|
@@ -196,22 +168,14 @@ $xmux-diagnosis
|
|
|
196
168
|
$xmux-send-pane
|
|
197
169
|
```
|
|
198
170
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
```bash
|
|
202
|
-
zsh -n xmux.zsh
|
|
203
|
-
zsh -n xmux-bridge.zsh
|
|
204
|
-
node --check scripts/setup_xmux_codex_mcp.js
|
|
205
|
-
git diff --check
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
Homebrew distribution notes live in [Homebrew distribution](docs/operations/homebrew.md).
|
|
171
|
+
Homebrew installation details live in [Homebrew installation](docs/operations/homebrew.md).
|
|
209
172
|
|
|
210
173
|
## Docs
|
|
211
174
|
|
|
212
175
|
- [Documentation index](docs/README.md)
|
|
176
|
+
- [Repository layout](docs/runtime/repository-layout.md)
|
|
213
177
|
- [Codex lead runtime](docs/runtime/codex-lead.md)
|
|
214
|
-
- [Homebrew
|
|
178
|
+
- [Homebrew installation](docs/operations/homebrew.md)
|
|
215
179
|
- [Wrapper-first debugging](docs/operations/debugging.md)
|
|
216
180
|
- [Claude teammate](docs/teammates/claude.md)
|
|
217
181
|
- [Gemini teammate](docs/teammates/gemini.md)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* bridge
|
|
3
|
+
* mcp/servers/bridge.js
|
|
4
4
|
* Minimal MCP server for xmux.
|
|
5
5
|
* Exposes write_to_lead(text, summary?) so Claude, Gemini, and Copilot
|
|
6
6
|
* teammates can write directly to the XMux lead inbox.
|
|
@@ -140,7 +140,11 @@ function trimToCap(msgs, cap) {
|
|
|
140
140
|
function mailboxInstallBases() {
|
|
141
141
|
const seen = new Set();
|
|
142
142
|
const bases = [];
|
|
143
|
-
|
|
143
|
+
const packageRoot = path.basename(__dirname) === 'servers'
|
|
144
|
+
&& path.basename(path.dirname(__dirname)) === 'mcp'
|
|
145
|
+
? path.dirname(path.dirname(__dirname))
|
|
146
|
+
: __dirname;
|
|
147
|
+
for (const candidate of [XMUX_INSTALL_DIR, packageRoot, __dirname]) {
|
|
144
148
|
if (!candidate) continue;
|
|
145
149
|
const resolved = path.resolve(candidate);
|
|
146
150
|
if (seen.has(resolved)) continue;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* mcp/servers/lead.js
|
|
4
4
|
* Stdio-only MCP server exposing XMux lead/team mailbox tools.
|
|
5
5
|
*
|
|
6
6
|
* Mailbox persistence is delegated to the Node mailbox CLI.
|
|
@@ -138,7 +138,11 @@ function parseJsonOutput(stdout) {
|
|
|
138
138
|
function mailboxInstallBases() {
|
|
139
139
|
const seen = new Set();
|
|
140
140
|
const bases = [];
|
|
141
|
-
|
|
141
|
+
const packageRoot = path.basename(__dirname) === 'servers'
|
|
142
|
+
&& path.basename(path.dirname(__dirname)) === 'mcp'
|
|
143
|
+
? path.dirname(path.dirname(__dirname))
|
|
144
|
+
: __dirname;
|
|
145
|
+
for (const candidate of [XMUX_INSTALL_DIR, packageRoot, __dirname]) {
|
|
142
146
|
if (!candidate) continue;
|
|
143
147
|
const resolved = path.resolve(candidate);
|
|
144
148
|
if (seen.has(resolved)) continue;
|
|
@@ -6,6 +6,8 @@ const os = require("node:os");
|
|
|
6
6
|
const path = require("node:path");
|
|
7
7
|
|
|
8
8
|
const SERVER_NAME = "xmux_bridge";
|
|
9
|
+
const DEFAULT_NPM_PACKAGE = "xmux-bridge";
|
|
10
|
+
const DEFAULT_NPX_PREFIX = path.join(os.homedir(), ".cache", "xmux", "npm-prefix");
|
|
9
11
|
const LEGACY_NAMES = new Set([
|
|
10
12
|
"xmux_bridge",
|
|
11
13
|
"xmux-bridge",
|
|
@@ -28,7 +30,7 @@ function stableHomebrewXmuxInstallDir(installDir) {
|
|
|
28
30
|
|
|
29
31
|
const prefix = resolved.split(marker, 1)[0];
|
|
30
32
|
const candidate = path.join(prefix, "opt", "xmux", "libexec");
|
|
31
|
-
if (fs.existsSync(path.join(candidate, "xmux.zsh"))) {
|
|
33
|
+
if (fs.existsSync(path.join(candidate, "runtime", "shell", "xmux.zsh")) || fs.existsSync(path.join(candidate, "xmux.zsh"))) {
|
|
32
34
|
return candidate;
|
|
33
35
|
}
|
|
34
36
|
return resolved;
|
|
@@ -36,13 +38,14 @@ function stableHomebrewXmuxInstallDir(installDir) {
|
|
|
36
38
|
|
|
37
39
|
function stableHomebrewXmuxFilePath(filePath) {
|
|
38
40
|
const resolved = absolute(filePath);
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
41
|
+
const marker = `${path.sep}Cellar${path.sep}xmux${path.sep}`;
|
|
42
|
+
const libexecSegment = `${path.sep}libexec${path.sep}`;
|
|
43
|
+
const libexecIndex = resolved.indexOf(libexecSegment);
|
|
44
|
+
if (!resolved.includes(marker) || libexecIndex < 0) return resolved;
|
|
45
|
+
const prefix = resolved.split(marker, 1)[0];
|
|
46
|
+
const optDir = path.join(prefix, "opt", "xmux", "libexec");
|
|
47
|
+
const relativePath = resolved.slice(libexecIndex + libexecSegment.length);
|
|
48
|
+
const candidate = path.join(optDir, relativePath);
|
|
46
49
|
if (fs.existsSync(candidate)) {
|
|
47
50
|
return candidate;
|
|
48
51
|
}
|
|
@@ -72,7 +75,7 @@ function atomicWriteJson(filePath, data) {
|
|
|
72
75
|
|
|
73
76
|
function usage() {
|
|
74
77
|
process.stderr.write(
|
|
75
|
-
"usage:
|
|
78
|
+
"usage: claude.js <bridge_js|npx> <project_dir> <outbox> <agent> <team> <state_dir> <install_dir>\n",
|
|
76
79
|
);
|
|
77
80
|
}
|
|
78
81
|
|
|
@@ -82,7 +85,8 @@ function main(argv = process.argv.slice(2)) {
|
|
|
82
85
|
return 2;
|
|
83
86
|
}
|
|
84
87
|
|
|
85
|
-
const
|
|
88
|
+
const bridgeRef = argv[0];
|
|
89
|
+
const bridgeJs = bridgeRef === "npx" ? "" : stableHomebrewXmuxFilePath(bridgeRef);
|
|
86
90
|
const projectDir = absolute(argv[1]);
|
|
87
91
|
const outbox = absolute(argv[2]);
|
|
88
92
|
const agent = argv[3];
|
|
@@ -115,20 +119,41 @@ function main(argv = process.argv.slice(2)) {
|
|
|
115
119
|
delete servers[legacyName];
|
|
116
120
|
}
|
|
117
121
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
XMUX_OUTBOX: outbox,
|
|
126
|
-
XMUX_PROJECT_DIR: projectDir,
|
|
127
|
-
XMUX_STATE_DIR: stateDir,
|
|
128
|
-
XMUX_TEAM: team,
|
|
129
|
-
},
|
|
122
|
+
const commonEnv = {
|
|
123
|
+
XMUX_AGENT: agent,
|
|
124
|
+
XMUX_INSTALL_DIR: installDir,
|
|
125
|
+
XMUX_OUTBOX: outbox,
|
|
126
|
+
XMUX_PROJECT_DIR: projectDir,
|
|
127
|
+
XMUX_STATE_DIR: stateDir,
|
|
128
|
+
XMUX_TEAM: team,
|
|
130
129
|
};
|
|
131
130
|
|
|
131
|
+
if (bridgeRef === "npx") {
|
|
132
|
+
const packageSpec = process.env.XMUX_MCP_PACKAGE_SPEC || DEFAULT_NPM_PACKAGE;
|
|
133
|
+
const npxPrefix = process.env.XMUX_MCP_NPX_PREFIX || DEFAULT_NPX_PREFIX;
|
|
134
|
+
servers[SERVER_NAME] = {
|
|
135
|
+
type: "stdio",
|
|
136
|
+
command: "npx",
|
|
137
|
+
args: [
|
|
138
|
+
"--prefix", npxPrefix,
|
|
139
|
+
"-y",
|
|
140
|
+
"-p", packageSpec,
|
|
141
|
+
"xmux-bridge",
|
|
142
|
+
"--outbox", outbox,
|
|
143
|
+
"--agent", agent,
|
|
144
|
+
"--team", team,
|
|
145
|
+
],
|
|
146
|
+
env: commonEnv,
|
|
147
|
+
};
|
|
148
|
+
} else {
|
|
149
|
+
servers[SERVER_NAME] = {
|
|
150
|
+
type: "stdio",
|
|
151
|
+
command: "node",
|
|
152
|
+
args: [bridgeJs, "--outbox", outbox, "--agent", agent, "--team", team],
|
|
153
|
+
env: commonEnv,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
132
157
|
atomicWriteJson(configPath, config);
|
|
133
158
|
return 0;
|
|
134
159
|
}
|
|
@@ -32,26 +32,39 @@ function abs(value) {
|
|
|
32
32
|
return path.resolve(expandUser(value));
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
function xmux_runtime_shell_path(installDir) {
|
|
36
|
+
return path.join(abs(installDir), "runtime", "shell", "xmux.zsh");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function has_xmux_runtime(installDir) {
|
|
40
|
+
const root = abs(installDir);
|
|
41
|
+
return fs.existsSync(xmux_runtime_shell_path(root)) || fs.existsSync(path.join(root, "xmux.zsh"));
|
|
42
|
+
}
|
|
43
|
+
|
|
35
44
|
function stable_homebrew_xmux_install_dir(xmuxInstallDir) {
|
|
36
45
|
const installDir = abs(xmuxInstallDir);
|
|
37
46
|
const marker = `${path.sep}Cellar${path.sep}xmux${path.sep}`;
|
|
38
47
|
if (!installDir.includes(marker) || !installDir.endsWith(`${path.sep}libexec`)) {
|
|
39
48
|
return installDir;
|
|
40
49
|
}
|
|
41
|
-
if (!
|
|
50
|
+
if (!has_xmux_runtime(installDir)) {
|
|
42
51
|
return installDir;
|
|
43
52
|
}
|
|
44
53
|
const prefix = installDir.split(marker, 1)[0];
|
|
45
54
|
const candidate = path.join(prefix, "opt", "xmux", "libexec");
|
|
46
|
-
return
|
|
55
|
+
return has_xmux_runtime(candidate) ? candidate : installDir;
|
|
47
56
|
}
|
|
48
57
|
|
|
49
58
|
function stable_homebrew_xmux_file_path(inputPath) {
|
|
50
59
|
const resolved = abs(inputPath);
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
60
|
+
const marker = `${path.sep}Cellar${path.sep}xmux${path.sep}`;
|
|
61
|
+
const libexecSegment = `${path.sep}libexec${path.sep}`;
|
|
62
|
+
const libexecIndex = resolved.indexOf(libexecSegment);
|
|
63
|
+
if (!resolved.includes(marker) || libexecIndex < 0) return resolved;
|
|
64
|
+
const prefix = resolved.split(marker, 1)[0];
|
|
65
|
+
const optDir = path.join(prefix, "opt", "xmux", "libexec");
|
|
66
|
+
const relativePath = resolved.slice(libexecIndex + libexecSegment.length);
|
|
67
|
+
const candidate = path.join(optDir, relativePath);
|
|
55
68
|
return fs.existsSync(candidate) ? candidate : resolved;
|
|
56
69
|
}
|
|
57
70
|
|
|
@@ -138,15 +151,30 @@ function package_spec_has_version(packageSpec) {
|
|
|
138
151
|
return text.includes("@");
|
|
139
152
|
}
|
|
140
153
|
|
|
154
|
+
function package_name_from_spec(packageSpec) {
|
|
155
|
+
const text = String(packageSpec || "");
|
|
156
|
+
if (text.startsWith("@")) {
|
|
157
|
+
const slash = text.indexOf("/");
|
|
158
|
+
if (slash < 0) return text;
|
|
159
|
+
const scope = text.slice(0, slash);
|
|
160
|
+
const rest = text.slice(slash + 1);
|
|
161
|
+
const versionIndex = rest.indexOf("@");
|
|
162
|
+
return `${scope}/${versionIndex < 0 ? rest : rest.slice(0, versionIndex)}`;
|
|
163
|
+
}
|
|
164
|
+
const versionIndex = text.indexOf("@");
|
|
165
|
+
return versionIndex < 0 ? text : text.slice(0, versionIndex);
|
|
166
|
+
}
|
|
167
|
+
|
|
141
168
|
function xmux_version_from_install_dir(xmuxInstallDir) {
|
|
142
|
-
const
|
|
169
|
+
const root = abs(xmuxInstallDir);
|
|
170
|
+
const content = read_text(xmux_runtime_shell_path(root)) || read_text(path.join(root, "xmux.zsh"));
|
|
143
171
|
const match = content.match(/^XMUX_VERSION=["']([^"']+)["']/m);
|
|
144
172
|
return match ? match[1] : "";
|
|
145
173
|
}
|
|
146
174
|
|
|
147
175
|
function default_mcp_package_spec(xmuxInstallDir, packageName = "", packageVersion = "") {
|
|
148
176
|
const installPackage = read_json(path.join(abs(xmuxInstallDir), "package.json")) || {};
|
|
149
|
-
const scriptPackage = read_json(path.join(path.dirname(path.dirname(abs(__filename))), "package.json")) || {};
|
|
177
|
+
const scriptPackage = read_json(path.join(path.dirname(path.dirname(path.dirname(abs(__filename)))), "package.json")) || {};
|
|
150
178
|
const name = packageName
|
|
151
179
|
|| process.env.XMUX_MCP_NPM_PACKAGE
|
|
152
180
|
|| installPackage.name
|
|
@@ -232,6 +260,70 @@ function ensure_mcp_runtime_dirs(mcpConfig) {
|
|
|
232
260
|
}
|
|
233
261
|
}
|
|
234
262
|
|
|
263
|
+
function cached_package_root(mcpConfig) {
|
|
264
|
+
if (!mcpConfig || !mcpConfig.npx_prefix || !mcpConfig.package_spec) return "";
|
|
265
|
+
return path.join(abs(mcpConfig.npx_prefix), "node_modules", package_name_from_spec(mcpConfig.package_spec));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function cached_mailbox_candidates(mcpConfig) {
|
|
269
|
+
const prefix = mcpConfig && mcpConfig.npx_prefix ? abs(mcpConfig.npx_prefix) : "";
|
|
270
|
+
const root = cached_package_root(mcpConfig);
|
|
271
|
+
return [
|
|
272
|
+
prefix ? path.join(prefix, "node_modules", ".bin", "xmux-mailbox") : "",
|
|
273
|
+
root ? path.join(root, "dist", "bin", "xmux-mailbox.js") : "",
|
|
274
|
+
].filter(Boolean);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function mailbox_source(xmuxInstallDir, mcpConfig) {
|
|
278
|
+
const explicit = process.env.XMUX_MAILBOX_NODE_CLI ? abs(process.env.XMUX_MAILBOX_NODE_CLI) : "";
|
|
279
|
+
if (explicit && fs.existsSync(explicit)) return { ok: true, kind: "env", label: explicit };
|
|
280
|
+
|
|
281
|
+
for (const candidate of cached_mailbox_candidates(mcpConfig)) {
|
|
282
|
+
if (fs.existsSync(candidate)) return { ok: true, kind: "npm-cache", label: candidate };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const bundled = path.join(abs(xmuxInstallDir), "dist", "bin", "xmux-mailbox.js");
|
|
286
|
+
if (fs.existsSync(bundled)) return { ok: true, kind: "brew-bundled", label: bundled };
|
|
287
|
+
|
|
288
|
+
const npx = spawnSync("npx", ["--version"], { encoding: "utf8" });
|
|
289
|
+
if (npx.status === 0 && mcpConfig && mcpConfig.mode === "npx" && mcpConfig.package_spec) {
|
|
290
|
+
return { ok: true, kind: "npx", label: `${mcpConfig.package_spec} via ${mcpConfig.npx_prefix}` };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return { ok: false, kind: "missing", label: "no mailbox CLI source found" };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function ensure_mcp_package_cache(mcpConfig, enabled = true) {
|
|
297
|
+
if (!enabled || !mcpConfig || mcpConfig.mode !== "npx") {
|
|
298
|
+
return { status: "skipped", message: "disabled" };
|
|
299
|
+
}
|
|
300
|
+
if (!mcpConfig.npx_prefix || !mcpConfig.package_spec) {
|
|
301
|
+
return { status: "skipped", message: "missing npx package metadata" };
|
|
302
|
+
}
|
|
303
|
+
ensure_mcp_runtime_dirs(mcpConfig);
|
|
304
|
+
|
|
305
|
+
const root = cached_package_root(mcpConfig);
|
|
306
|
+
if (root && fs.existsSync(root)) {
|
|
307
|
+
return { status: "ok", message: `using existing cache at ${root}` };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const npm = spawnSync("npm", [
|
|
311
|
+
"install",
|
|
312
|
+
"--prefix",
|
|
313
|
+
abs(mcpConfig.npx_prefix),
|
|
314
|
+
"--no-save",
|
|
315
|
+
"--omit=dev",
|
|
316
|
+
mcpConfig.package_spec,
|
|
317
|
+
], { encoding: "utf8" });
|
|
318
|
+
|
|
319
|
+
if (npm.status === 0) {
|
|
320
|
+
return { status: "ok", message: `installed ${mcpConfig.package_spec} under ${mcpConfig.npx_prefix}` };
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const detail = (npm.stderr || npm.stdout || "").trim().split(/\r?\n/).slice(-2).join(" ");
|
|
324
|
+
return { status: "failed", message: detail || `npm install exited with ${npm.status}` };
|
|
325
|
+
}
|
|
326
|
+
|
|
235
327
|
function build_block(mcpConfigOrServerPath, xmuxInstallDir) {
|
|
236
328
|
const mcpConfig = normalize_mcp_config(mcpConfigOrServerPath, xmuxInstallDir);
|
|
237
329
|
const pathEnv = resolve_path_with_node();
|
|
@@ -272,7 +364,7 @@ function is_xmux_runtime_bin_path(candidatePath, currentXmuxBin) {
|
|
|
272
364
|
if (expanded === abs(currentXmuxBin)) return true;
|
|
273
365
|
if (path.basename(expanded) !== "bin") return false;
|
|
274
366
|
const installDir = path.dirname(expanded);
|
|
275
|
-
if (
|
|
367
|
+
if (has_xmux_runtime(installDir) && fs.existsSync(path.join(expanded, "xmux"))) {
|
|
276
368
|
return true;
|
|
277
369
|
}
|
|
278
370
|
if (path.basename(installDir) !== "libexec") return false;
|
|
@@ -419,7 +511,7 @@ function install_xmux_command_rule(configPath) {
|
|
|
419
511
|
let content = remove_marker_block(read_text(filePath), RULE_BEGIN, RULE_END);
|
|
420
512
|
const block = [
|
|
421
513
|
RULE_BEGIN,
|
|
422
|
-
"# Allow the scoped XMux wrapper command; XMux
|
|
514
|
+
"# Allow the scoped XMux wrapper command; user intent and XMux wrappers control operation scope.",
|
|
423
515
|
'prefix_rule(pattern=["xmux"], decision="allow")',
|
|
424
516
|
RULE_END,
|
|
425
517
|
].join("\n");
|
|
@@ -568,12 +660,12 @@ function _xmux_lead_mcp_processes_from_ps(psOutput) {
|
|
|
568
660
|
const processes = [];
|
|
569
661
|
for (const rawLine of String(psOutput || "").split(/\r?\n/)) {
|
|
570
662
|
const stripped = rawLine.trim();
|
|
571
|
-
if (!stripped.includes("xmux-lead-mcp-server.js")) continue;
|
|
663
|
+
if (!stripped.includes("mcp/servers/lead.js") && !stripped.includes("xmux-lead-mcp-server.js")) continue;
|
|
572
664
|
const match = stripped.match(/^(\d+)\s+(.*)$/);
|
|
573
665
|
if (!match) continue;
|
|
574
666
|
const [, pid, command] = match;
|
|
575
667
|
const tokens = splitShellWords(command.trim());
|
|
576
|
-
const serverPath = tokens.find((token) => token.endsWith("xmux-lead-mcp-server.js")) || "";
|
|
668
|
+
const serverPath = tokens.find((token) => token.endsWith("mcp/servers/lead.js") || token.endsWith("xmux-lead-mcp-server.js")) || "";
|
|
577
669
|
if (!serverPath) continue;
|
|
578
670
|
processes.push({ pid, command: command.trim(), server_path: abs(serverPath) });
|
|
579
671
|
}
|
|
@@ -591,7 +683,7 @@ function running_xmux_lead_mcp_processes() {
|
|
|
591
683
|
|
|
592
684
|
function _is_homebrew_xmux_mcp_server(serverPath) {
|
|
593
685
|
const normalized = abs(serverPath);
|
|
594
|
-
return normalized.endsWith("xmux-lead-mcp-server.js")
|
|
686
|
+
return (normalized.endsWith(`${path.sep}mcp${path.sep}servers${path.sep}lead.js`) || normalized.endsWith("xmux-lead-mcp-server.js"))
|
|
595
687
|
&& normalized.includes(`${path.sep}Cellar${path.sep}xmux${path.sep}`)
|
|
596
688
|
&& normalized.includes(`${path.sep}libexec${path.sep}`);
|
|
597
689
|
}
|
|
@@ -636,7 +728,7 @@ function doctor_codex(configPath, xmuxInstallDir, mcpConfigOrServerPath, skillsD
|
|
|
636
728
|
} else if (installedNames.size) {
|
|
637
729
|
notes.push(["OK", `XMux Codex skills installed under ${skills_root(configPath)}`]);
|
|
638
730
|
} else {
|
|
639
|
-
notes.push(["
|
|
731
|
+
notes.push(["OK", "optional XMux skills are not configured"]);
|
|
640
732
|
}
|
|
641
733
|
|
|
642
734
|
if (fs.existsSync(plugin_cache_path(configPath))) {
|
|
@@ -645,6 +737,10 @@ function doctor_codex(configPath, xmuxInstallDir, mcpConfigOrServerPath, skillsD
|
|
|
645
737
|
notes.push(["OK", "legacy XMux plugin cache is absent"]);
|
|
646
738
|
}
|
|
647
739
|
|
|
740
|
+
const mailbox = mailbox_source(xmuxInstallDir, mcpConfig);
|
|
741
|
+
if (mailbox.ok) notes.push(["OK", `mailbox source: ${mailbox.kind} (${mailbox.label})`]);
|
|
742
|
+
else issues.push(`mailbox source is unavailable: ${mailbox.label}`);
|
|
743
|
+
|
|
648
744
|
const staleProcesses = stale_xmux_lead_mcp_processes(mcpConfig);
|
|
649
745
|
for (const proc of staleProcesses.slice(0, 5)) {
|
|
650
746
|
notes.push([
|
|
@@ -686,6 +782,7 @@ function parse_args(argv) {
|
|
|
686
782
|
mcp_version: "",
|
|
687
783
|
mcp_bin: DEFAULT_MCP_BIN,
|
|
688
784
|
mcp_npx_prefix: "",
|
|
785
|
+
cache_mcp: true,
|
|
689
786
|
};
|
|
690
787
|
for (let i = 0; i < argv.length;) {
|
|
691
788
|
const arg = argv[i];
|
|
@@ -697,6 +794,10 @@ function parse_args(argv) {
|
|
|
697
794
|
opts.quiet = true; i += 1;
|
|
698
795
|
} else if (arg === "--without-skills") {
|
|
699
796
|
opts.install_skills = false; i += 1;
|
|
797
|
+
} else if (arg === "--cache-mcp") {
|
|
798
|
+
opts.cache_mcp = true; i += 1;
|
|
799
|
+
} else if (arg === "--no-cache-mcp") {
|
|
800
|
+
opts.cache_mcp = false; i += 1;
|
|
700
801
|
} else if ([
|
|
701
802
|
"--skills-dir",
|
|
702
803
|
"--home",
|
|
@@ -748,7 +849,7 @@ function main(argv = process.argv.slice(2)) {
|
|
|
748
849
|
const opts = parse_args(argv);
|
|
749
850
|
const configPath = resolve_config_path(opts);
|
|
750
851
|
opts.mcp_npx_prefix = abs(opts.mcp_npx_prefix || process.env.XMUX_MCP_NPX_PREFIX || default_mcp_npx_prefix(configPath));
|
|
751
|
-
const scriptInstallDir = path.dirname(path.dirname(abs(__filename)));
|
|
852
|
+
const scriptInstallDir = path.dirname(path.dirname(path.dirname(abs(__filename))));
|
|
752
853
|
const rawInstallDir = abs(opts.xmux_install_dir || scriptInstallDir);
|
|
753
854
|
const xmuxInstallDir = stable_homebrew_xmux_install_dir(rawInstallDir);
|
|
754
855
|
const xmuxProjectDir = abs(opts.xmux_project_dir || default_xmux_project_dir());
|
|
@@ -760,6 +861,10 @@ function main(argv = process.argv.slice(2)) {
|
|
|
760
861
|
}
|
|
761
862
|
|
|
762
863
|
ensure_mcp_runtime_dirs(mcpConfig);
|
|
864
|
+
const cacheResult = ensure_mcp_package_cache(mcpConfig, opts.cache_mcp);
|
|
865
|
+
if (cacheResult.status === "failed") {
|
|
866
|
+
console.error(`[WARN] XMux MCP package cache failed: ${cacheResult.message}`);
|
|
867
|
+
}
|
|
763
868
|
|
|
764
869
|
let content = remove_xmux_blocks(read_text(configPath));
|
|
765
870
|
if (opts.remove) {
|
|
@@ -791,11 +896,15 @@ function main(argv = process.argv.slice(2)) {
|
|
|
791
896
|
|
|
792
897
|
console.log(`[OK] Wrote ${SERVER_NAME} to ${configPath}`);
|
|
793
898
|
console.log(` mcp: ${mcpConfig.label}`);
|
|
899
|
+
if (cacheResult.status === "ok") console.log(` mcp_cache: ${cacheResult.message}`);
|
|
900
|
+
else if (cacheResult.status === "skipped") console.log(` mcp_cache: ${cacheResult.message}`);
|
|
794
901
|
console.log(` xmux_install_dir: ${xmuxInstallDir}`);
|
|
795
902
|
console.log(" xmux_project_dir: inherited from xmux-launched Codex runtime");
|
|
796
903
|
console.log(" xmux_state_dir: inherited from xmux-launched Codex runtime");
|
|
797
904
|
if (installedSkills.length) console.log(` skills: ${installedSkills.join(", ")}`);
|
|
798
|
-
else if (opts.install_skills
|
|
905
|
+
else if (opts.install_skills && (opts.skills_dir || process.env.XMUX_CODEX_SKILLS_DIR)) {
|
|
906
|
+
console.log(" skills: no importable XMux skills found");
|
|
907
|
+
}
|
|
799
908
|
console.log(" plugin_cache: disabled; stale XMux plugin cache removed if present");
|
|
800
909
|
return 0;
|
|
801
910
|
}
|
|
@@ -814,6 +923,7 @@ module.exports = {
|
|
|
814
923
|
build_block,
|
|
815
924
|
default_mcp_package_spec,
|
|
816
925
|
default_mcp_npx_prefix,
|
|
926
|
+
mailbox_source,
|
|
817
927
|
resolve_mcp_config,
|
|
818
928
|
path_with_xmux_bin,
|
|
819
929
|
ensure_codex_shell_environment,
|
|
@@ -6,6 +6,8 @@ const os = require("node:os");
|
|
|
6
6
|
const path = require("node:path");
|
|
7
7
|
|
|
8
8
|
const SERVER_NAME = "xmux_bridge";
|
|
9
|
+
const DEFAULT_NPM_PACKAGE = "xmux-bridge";
|
|
10
|
+
const DEFAULT_NPX_PREFIX = path.join(os.homedir(), ".cache", "xmux", "npm-prefix");
|
|
9
11
|
const LEGACY_NAMES = new Set([
|
|
10
12
|
"xmux_bridge",
|
|
11
13
|
"xmux-bridge",
|
|
@@ -18,16 +20,15 @@ const TOOLS = ["write_to_lead"];
|
|
|
18
20
|
|
|
19
21
|
function stableHomebrewXmuxFilePath(inputPath) {
|
|
20
22
|
const resolved = path.resolve(inputPath.replace(/^~(?=$|\/)/, os.homedir()));
|
|
21
|
-
const installDir = path.dirname(resolved);
|
|
22
23
|
const marker = `${path.sep}Cellar${path.sep}xmux${path.sep}`;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const prefix = installDir.split(marker, 1)[0];
|
|
24
|
+
const libexecSegment = `${path.sep}libexec${path.sep}`;
|
|
25
|
+
const libexecIndex = resolved.indexOf(libexecSegment);
|
|
26
|
+
if (!resolved.includes(marker) || libexecIndex < 0) return resolved;
|
|
27
|
+
const prefix = resolved.split(marker, 1)[0];
|
|
28
28
|
const optDir = path.join(prefix, "opt", "xmux", "libexec");
|
|
29
|
-
const
|
|
30
|
-
|
|
29
|
+
const relativePath = resolved.slice(libexecIndex + libexecSegment.length);
|
|
30
|
+
const candidate = path.join(optDir, relativePath);
|
|
31
|
+
if ((fs.existsSync(path.join(optDir, "runtime", "shell", "xmux.zsh")) || fs.existsSync(path.join(optDir, "xmux.zsh"))) && fs.existsSync(candidate)) {
|
|
31
32
|
return candidate;
|
|
32
33
|
}
|
|
33
34
|
return resolved;
|
|
@@ -58,7 +59,13 @@ function main(argv = process.argv.slice(2)) {
|
|
|
58
59
|
if (cmd.startsWith("http")) {
|
|
59
60
|
servers[SERVER_NAME] = { type: "sse", url: cmd, tools: TOOLS };
|
|
60
61
|
} else if (cmd === "npx") {
|
|
61
|
-
|
|
62
|
+
const packageSpec = process.env.XMUX_MCP_PACKAGE_SPEC || DEFAULT_NPM_PACKAGE;
|
|
63
|
+
const npxPrefix = process.env.XMUX_MCP_NPX_PREFIX || DEFAULT_NPX_PREFIX;
|
|
64
|
+
servers[SERVER_NAME] = {
|
|
65
|
+
command: "npx",
|
|
66
|
+
args: ["--prefix", npxPrefix, "-y", "-p", packageSpec, "xmux-bridge"],
|
|
67
|
+
tools: TOOLS,
|
|
68
|
+
};
|
|
62
69
|
} else {
|
|
63
70
|
servers[SERVER_NAME] = {
|
|
64
71
|
command: "node",
|
|
@@ -14,20 +14,20 @@ const LEGACY_NAMES = new Set([
|
|
|
14
14
|
"amux_bridge",
|
|
15
15
|
"amux-bridge",
|
|
16
16
|
]);
|
|
17
|
-
const
|
|
17
|
+
const DEFAULT_NPM_PACKAGE = "xmux-bridge";
|
|
18
|
+
const DEFAULT_NPX_PREFIX = path.join(os.homedir(), ".cache", "xmux", "npm-prefix");
|
|
18
19
|
|
|
19
20
|
function stableHomebrewXmuxFilePath(inputPath) {
|
|
20
21
|
const resolved = path.resolve(inputPath.replace(/^~(?=$|\/)/, os.homedir()));
|
|
21
|
-
const installDir = path.dirname(resolved);
|
|
22
22
|
const marker = `${path.sep}Cellar${path.sep}xmux${path.sep}`;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const prefix = installDir.split(marker, 1)[0];
|
|
23
|
+
const libexecSegment = `${path.sep}libexec${path.sep}`;
|
|
24
|
+
const libexecIndex = resolved.indexOf(libexecSegment);
|
|
25
|
+
if (!resolved.includes(marker) || libexecIndex < 0) return resolved;
|
|
26
|
+
const prefix = resolved.split(marker, 1)[0];
|
|
28
27
|
const optDir = path.join(prefix, "opt", "xmux", "libexec");
|
|
29
|
-
const
|
|
30
|
-
|
|
28
|
+
const relativePath = resolved.slice(libexecIndex + libexecSegment.length);
|
|
29
|
+
const candidate = path.join(optDir, relativePath);
|
|
30
|
+
if ((fs.existsSync(path.join(optDir, "runtime", "shell", "xmux.zsh")) || fs.existsSync(path.join(optDir, "xmux.zsh"))) && fs.existsSync(candidate)) {
|
|
31
31
|
return candidate;
|
|
32
32
|
}
|
|
33
33
|
return resolved;
|
|
@@ -56,9 +56,11 @@ function main(argv = process.argv.slice(2)) {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
if (cmd === "npx") {
|
|
59
|
+
const packageSpec = process.env.XMUX_MCP_PACKAGE_SPEC || DEFAULT_NPM_PACKAGE;
|
|
60
|
+
const npxPrefix = process.env.XMUX_MCP_NPX_PREFIX || DEFAULT_NPX_PREFIX;
|
|
59
61
|
servers[SERVER_NAME] = {
|
|
60
62
|
command: "npx",
|
|
61
|
-
args: ["-y",
|
|
63
|
+
args: ["--prefix", npxPrefix, "-y", "-p", packageSpec, "xmux-bridge"],
|
|
62
64
|
trust: true,
|
|
63
65
|
};
|
|
64
66
|
} else {
|