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 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
- Homebrew installs the XMux CLI/runtime only. `xmux setup-codex` is the command
40
- that mutates `~/.codex`: it registers the `xmux_lead` MCP server through the
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 source/install directory
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` from the Codex shell policy PATH that
160
- `xmux setup-codex` writes to `~/.codex/config.toml`. If that wrapper is
161
- unavailable, it falls back to the explicit XMux executable. The user-facing
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 global MCP config is install-scoped: the MCP command is a versioned npm
169
- entrypoint launched with a project-neutral `npx --prefix`, while
170
- `XMUX_INSTALL_DIR` points at the Homebrew runtime that owns wrapper scripts,
171
- state discovery, and lifecycle. It does not pin
172
- `XMUX_PROJECT_DIR`/`XMUX_STATE_DIR`; those values come from the active
173
- `xmux -n <session>` lead runtime.
174
-
175
- Provider teammates write responses through `bridge-mcp-server.js`, using the
176
- team runtime environment prepared by XMux. The bridge and mailbox paths are
177
- implementation details behind Codex-led teammate orchestration.
178
-
179
- The explicit Codex setup installs available XMux skills under
180
- `~/.codex/skills` only from `--skills-dir` or `XMUX_CODEX_SKILLS_DIR`.
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
- Development verification for agent/runtime changes:
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 distribution](docs/operations/homebrew.md)
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-mcp-server.js
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
- for (const candidate of [XMUX_INSTALL_DIR, __dirname]) {
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
- * xmux-lead-mcp-server.js
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
- for (const candidate of [XMUX_INSTALL_DIR, __dirname]) {
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 installDir = path.dirname(resolved);
40
- const stableInstallDir = stableHomebrewXmuxInstallDir(installDir);
41
- if (stableInstallDir === installDir) {
42
- return resolved;
43
- }
44
-
45
- const candidate = path.join(stableInstallDir, path.basename(resolved));
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: setup_claude_mcp.js <bridge_js> <project_dir> <outbox> <agent> <team> <state_dir> <install_dir>\n",
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 bridgeJs = stableHomebrewXmuxFilePath(argv[0]);
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
- servers[SERVER_NAME] = {
119
- type: "stdio",
120
- command: "node",
121
- args: [bridgeJs, "--outbox", outbox, "--agent", agent, "--team", team],
122
- env: {
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,
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 (!fs.existsSync(path.join(installDir, "xmux.zsh"))) {
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 fs.existsSync(path.join(candidate, "xmux.zsh")) ? candidate : installDir;
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 installDir = path.dirname(resolved);
52
- const stableInstallDir = stable_homebrew_xmux_install_dir(installDir);
53
- if (stableInstallDir === installDir) return resolved;
54
- const candidate = path.join(stableInstallDir, path.basename(resolved));
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 content = read_text(path.join(abs(xmuxInstallDir), "xmux.zsh"));
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 (fs.existsSync(path.join(installDir, "xmux.zsh")) && fs.existsSync(path.join(expanded, "xmux"))) {
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 skills still control operation scope.",
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(["WARN", "no XMux skill source directory found; pass --skills-dir or set XMUX_CODEX_SKILLS_DIR"]);
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) console.log(" skills: skipped; pass --skills-dir or set XMUX_CODEX_SKILLS_DIR");
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
- if (!installDir.includes(marker) || !installDir.endsWith(`${path.sep}libexec`)) {
24
- return resolved;
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 candidate = path.join(optDir, path.basename(resolved));
30
- if (fs.existsSync(path.join(optDir, "xmux.zsh")) && fs.existsSync(candidate)) {
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
- servers[SERVER_NAME] = { command: "npx", args: ["-y", "xmux-bridge"], tools: TOOLS };
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 NPM_PIN = "xmux-bridge@^1.3.0";
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
- if (!installDir.includes(marker) || !installDir.endsWith(`${path.sep}libexec`)) {
24
- return resolved;
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 candidate = path.join(optDir, path.basename(resolved));
30
- if (fs.existsSync(path.join(optDir, "xmux.zsh")) && fs.existsSync(candidate)) {
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", NPM_PIN],
63
+ args: ["--prefix", npxPrefix, "-y", "-p", packageSpec, "xmux-bridge"],
62
64
  trust: true,
63
65
  };
64
66
  } else {