xmux-bridge 1.0.39 → 1.0.40

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
@@ -166,8 +166,9 @@ The Codex lead MCP server is `xmux_lead`. `xmux setup-codex` configures it so
166
166
  Codex can route requests, wait for teammate responses, read events, and inspect
167
167
  team status.
168
168
  The global MCP config is install-scoped: the MCP command is a versioned npm
169
- entrypoint, while `XMUX_INSTALL_DIR` points at the Homebrew runtime that owns
170
- wrapper scripts, state discovery, and lifecycle. It does not pin
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
171
172
  `XMUX_PROJECT_DIR`/`XMUX_STATE_DIR`; those values come from the active
172
173
  `xmux -n <session>` lead runtime.
173
174
 
@@ -198,15 +199,13 @@ $xmux-send-pane
198
199
  Development verification for agent/runtime changes:
199
200
 
200
201
  ```bash
201
- pytest tests -q
202
202
  zsh -n xmux.zsh
203
203
  zsh -n xmux-bridge.zsh
204
204
  node --check scripts/setup_xmux_codex_mcp.js
205
205
  git diff --check
206
206
  ```
207
207
 
208
- Formula draft and distribution notes live in
209
- [Homebrew distribution](docs/operations/homebrew.md).
208
+ Homebrew distribution notes live in [Homebrew distribution](docs/operations/homebrew.md).
210
209
 
211
210
  ## Docs
212
211
 
@@ -305,36 +305,99 @@ function buildResponse(msg) {
305
305
 
306
306
  // ── STDIO mode ────────────────────────────────────────────────────────────────
307
307
  // CRITICAL: register stdin listener FIRST to avoid race condition.
308
- // Provider CLIs send `initialize` immediately after spawn; if readline isn't
308
+ // Provider CLIs send `initialize` immediately after spawn; if stdin isn't
309
309
  // listening yet the message is lost and the CLI times out (Tools: none).
310
310
 
311
+ function writeMcpResponse(resp, framed) {
312
+ const body = JSON.stringify(resp);
313
+ if (framed) {
314
+ process.stdout.write(`Content-Length: ${Buffer.byteLength(body, 'utf8')}\r\n\r\n${body}`);
315
+ } else {
316
+ process.stdout.write(body + '\n');
317
+ }
318
+ }
319
+
320
+ function handleStdioMessage(msg, framed) {
321
+ const resp = buildResponse(msg);
322
+ if (resp) writeMcpResponse(resp, framed);
323
+ }
324
+
311
325
  function startStdio() {
312
- const readline = require('readline');
313
- const messageQueue = [];
314
- let ready = false;
326
+ let buffer = Buffer.alloc(0);
327
+
328
+ function parseContentLengthHeader(header) {
329
+ for (const line of header.split(/\r?\n/)) {
330
+ const match = line.match(/^Content-Length:\s*(\d+)\s*$/i);
331
+ if (match) return Number.parseInt(match[1], 10);
332
+ }
333
+ return null;
334
+ }
315
335
 
316
- const rl = readline.createInterface({ input: process.stdin, terminal: false });
317
- rl.on('line', (line) => {
318
- if (!line.trim()) return;
336
+ function handleJsonPayload(payload, framed) {
337
+ if (!payload.trim()) return;
319
338
  let msg;
320
- try { msg = JSON.parse(line); } catch (_) { return; }
321
- if (ready) {
322
- const resp = buildResponse(msg);
323
- if (resp) process.stdout.write(JSON.stringify(resp) + '\n');
324
- } else {
325
- messageQueue.push(msg);
339
+ try {
340
+ msg = JSON.parse(payload);
341
+ } catch (_) {
342
+ return;
326
343
  }
344
+ handleStdioMessage(msg, framed);
345
+ }
346
+
347
+ function drain(flush = false) {
348
+ while (buffer.length) {
349
+ while (buffer[0] === 0x0a || buffer[0] === 0x0d) buffer = buffer.subarray(1);
350
+ if (!buffer.length) return;
351
+
352
+ const text = buffer.toString('utf8');
353
+ if (text.toLowerCase().startsWith('content-length:')) {
354
+ let headerEnd = text.indexOf('\r\n\r\n');
355
+ let separatorLength = 4;
356
+ if (headerEnd === -1) {
357
+ headerEnd = text.indexOf('\n\n');
358
+ separatorLength = 2;
359
+ }
360
+ if (headerEnd === -1) return;
361
+
362
+ const header = text.slice(0, headerEnd);
363
+ const contentLength = parseContentLengthHeader(header);
364
+ if (!Number.isFinite(contentLength) || contentLength < 0) {
365
+ buffer = Buffer.alloc(0);
366
+ return;
367
+ }
368
+
369
+ const bodyStart = Buffer.byteLength(text.slice(0, headerEnd + separatorLength), 'utf8');
370
+ if (buffer.length < bodyStart + contentLength) return;
371
+ const body = buffer.subarray(bodyStart, bodyStart + contentLength).toString('utf8');
372
+ buffer = buffer.subarray(bodyStart + contentLength);
373
+ handleJsonPayload(body, true);
374
+ continue;
375
+ }
376
+
377
+ const newline = buffer.indexOf(0x0a);
378
+ if (newline === -1) {
379
+ if (!flush) return;
380
+ const line = buffer.toString('utf8');
381
+ buffer = Buffer.alloc(0);
382
+ handleJsonPayload(line, false);
383
+ return;
384
+ }
385
+ const line = buffer.subarray(0, newline).toString('utf8');
386
+ buffer = buffer.subarray(newline + 1);
387
+ handleJsonPayload(line, false);
388
+ }
389
+ }
390
+
391
+ process.stdin.on('data', (chunk) => {
392
+ buffer = Buffer.concat([buffer, chunk]);
393
+ drain(false);
327
394
  });
328
395
  // Parent CLI closed stdin → exit so MCP subprocess does not linger and
329
396
  // continue accepting writes after its intended session is over.
330
- rl.on('close', () => process.exit(0));
331
-
332
- ready = true;
333
- for (const msg of messageQueue) {
334
- const resp = buildResponse(msg);
335
- if (resp) process.stdout.write(JSON.stringify(resp) + '\n');
336
- }
337
- messageQueue.length = 0;
397
+ process.stdin.on('end', () => {
398
+ drain(true);
399
+ process.exit(0);
400
+ });
338
401
  }
339
402
 
340
403
  // ── HTTP/SSE mode ─────────────────────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xmux-bridge",
3
- "version": "1.0.39",
3
+ "version": "1.0.40",
4
4
  "description": "MCP stdio server for XMux teammate responses",
5
5
  "bin": {
6
6
  "xmux-bridge": "./bridge-mcp-server.js",
@@ -19,6 +19,7 @@ const LOCAL_PLUGIN_CACHE_VERSION = "local";
19
19
  const SKILL_MARKER = ".xmux-managed-skill";
20
20
  const DEFAULT_MCP_PACKAGE = "xmux-bridge";
21
21
  const DEFAULT_MCP_BIN = "xmux-lead-mcp";
22
+ const DEFAULT_MCP_NPX_PREFIX = path.join(".cache", "xmux", "npm-prefix");
22
23
 
23
24
  function expandUser(value) {
24
25
  const text = String(value || "");
@@ -172,21 +173,33 @@ function node_mcp_config(serverPath) {
172
173
  };
173
174
  }
174
175
 
175
- function npx_mcp_config(packageSpec, binName = DEFAULT_MCP_BIN) {
176
+ function default_mcp_npx_prefix(configPath = "") {
177
+ const fallback = path.join(os.homedir(), DEFAULT_MCP_NPX_PREFIX);
178
+ if (!configPath) return fallback;
179
+ const configDir = path.dirname(abs(configPath));
180
+ if (path.basename(configDir) === ".codex") {
181
+ return path.join(path.dirname(configDir), DEFAULT_MCP_NPX_PREFIX);
182
+ }
183
+ return path.join(configDir, DEFAULT_MCP_NPX_PREFIX);
184
+ }
185
+
186
+ function npx_mcp_config(packageSpec, binName = DEFAULT_MCP_BIN, npxPrefix = "") {
187
+ const prefix = abs(npxPrefix || process.env.XMUX_MCP_NPX_PREFIX || default_mcp_npx_prefix());
176
188
  return {
177
189
  mode: "npx",
178
190
  command: "npx",
179
- args: ["-y", "-p", packageSpec, binName],
191
+ args: ["--prefix", prefix, "-y", "-p", packageSpec, binName],
180
192
  package_spec: packageSpec,
181
193
  bin: binName,
182
- label: `npx -y -p ${packageSpec} ${binName}`,
194
+ npx_prefix: prefix,
195
+ label: `npx --prefix ${prefix} -y -p ${packageSpec} ${binName}`,
183
196
  };
184
197
  }
185
198
 
186
199
  function resolve_mcp_config(xmuxInstallDir, opts = {}) {
187
200
  if (opts.server_path) return node_mcp_config(opts.server_path);
188
201
  const packageSpec = default_mcp_package_spec(xmuxInstallDir, opts.mcp_package, opts.mcp_version);
189
- return npx_mcp_config(packageSpec, opts.mcp_bin || DEFAULT_MCP_BIN);
202
+ return npx_mcp_config(packageSpec, opts.mcp_bin || DEFAULT_MCP_BIN, opts.mcp_npx_prefix || "");
190
203
  }
191
204
 
192
205
  function normalize_mcp_config(mcpConfigOrServerPath, xmuxInstallDir = "", opts = {}) {
@@ -198,6 +211,7 @@ function normalize_mcp_config(mcpConfigOrServerPath, xmuxInstallDir = "", opts =
198
211
  server_path: mcpConfigOrServerPath.server_path || "",
199
212
  package_spec: mcpConfigOrServerPath.package_spec || "",
200
213
  bin: mcpConfigOrServerPath.bin || "",
214
+ npx_prefix: mcpConfigOrServerPath.npx_prefix || "",
201
215
  label: mcpConfigOrServerPath.label || [
202
216
  mcpConfigOrServerPath.command,
203
217
  ...(mcpConfigOrServerPath.args || []),
@@ -212,6 +226,12 @@ function mcp_args_toml(mcpConfig) {
212
226
  return `args = [${mcpConfig.args.map(toml_quote).join(", ")}]`;
213
227
  }
214
228
 
229
+ function ensure_mcp_runtime_dirs(mcpConfig) {
230
+ if (mcpConfig.mode === "npx" && mcpConfig.npx_prefix) {
231
+ fs.mkdirSync(mcpConfig.npx_prefix, { recursive: true });
232
+ }
233
+ }
234
+
215
235
  function build_block(mcpConfigOrServerPath, xmuxInstallDir) {
216
236
  const mcpConfig = normalize_mcp_config(mcpConfigOrServerPath, xmuxInstallDir);
217
237
  const pathEnv = resolve_path_with_node();
@@ -665,6 +685,7 @@ function parse_args(argv) {
665
685
  mcp_package: "",
666
686
  mcp_version: "",
667
687
  mcp_bin: DEFAULT_MCP_BIN,
688
+ mcp_npx_prefix: "",
668
689
  };
669
690
  for (let i = 0; i < argv.length;) {
670
691
  const arg = argv[i];
@@ -687,6 +708,7 @@ function parse_args(argv) {
687
708
  "--mcp-package",
688
709
  "--mcp-version",
689
710
  "--mcp-bin",
711
+ "--mcp-npx-prefix",
690
712
  ].includes(arg) && i + 1 < argv.length) {
691
713
  const key = arg.slice(2).replace(/-/g, "_");
692
714
  opts[key] = expandUser(argv[i + 1]);
@@ -725,6 +747,7 @@ function resolve_config_path(opts) {
725
747
  function main(argv = process.argv.slice(2)) {
726
748
  const opts = parse_args(argv);
727
749
  const configPath = resolve_config_path(opts);
750
+ opts.mcp_npx_prefix = abs(opts.mcp_npx_prefix || process.env.XMUX_MCP_NPX_PREFIX || default_mcp_npx_prefix(configPath));
728
751
  const scriptInstallDir = path.dirname(path.dirname(abs(__filename)));
729
752
  const rawInstallDir = abs(opts.xmux_install_dir || scriptInstallDir);
730
753
  const xmuxInstallDir = stable_homebrew_xmux_install_dir(rawInstallDir);
@@ -736,6 +759,8 @@ function main(argv = process.argv.slice(2)) {
736
759
  return doctor_codex(configPath, xmuxInstallDir, mcpConfig, opts.skills_dir, opts.quiet);
737
760
  }
738
761
 
762
+ ensure_mcp_runtime_dirs(mcpConfig);
763
+
739
764
  let content = remove_xmux_blocks(read_text(configPath));
740
765
  if (opts.remove) {
741
766
  content = remove_codex_shell_environment(content, xmuxInstallDir);
@@ -788,6 +813,7 @@ module.exports = {
788
813
  remove_xmux_blocks,
789
814
  build_block,
790
815
  default_mcp_package_spec,
816
+ default_mcp_npx_prefix,
791
817
  resolve_mcp_config,
792
818
  path_with_xmux_bin,
793
819
  ensure_codex_shell_environment,
@@ -451,36 +451,94 @@ function buildResponse(msg) {
451
451
  return null;
452
452
  }
453
453
 
454
+ function writeMcpResponse(resp, framed) {
455
+ const body = JSON.stringify(resp);
456
+ if (framed) {
457
+ process.stdout.write(`Content-Length: ${Buffer.byteLength(body, 'utf8')}\r\n\r\n${body}`);
458
+ } else {
459
+ process.stdout.write(body + '\n');
460
+ }
461
+ }
462
+
463
+ function handleStdioMessage(msg, framed) {
464
+ const resp = buildResponse(msg);
465
+ if (resp) writeMcpResponse(resp, framed);
466
+ }
467
+
454
468
  function startStdio() {
455
- const readline = require('readline');
456
- const messageQueue = [];
457
- let ready = false;
469
+ let buffer = Buffer.alloc(0);
458
470
 
459
- const rl = readline.createInterface({ input: process.stdin, terminal: false });
460
- rl.on('line', (line) => {
461
- if (!line.trim()) return;
471
+ function parseContentLengthHeader(header) {
472
+ for (const line of header.split(/\r?\n/)) {
473
+ const match = line.match(/^Content-Length:\s*(\d+)\s*$/i);
474
+ if (match) return Number.parseInt(match[1], 10);
475
+ }
476
+ return null;
477
+ }
478
+
479
+ function handleJsonPayload(payload, framed) {
480
+ if (!payload.trim()) return;
462
481
  let msg;
463
482
  try {
464
- msg = JSON.parse(line);
483
+ msg = JSON.parse(payload);
465
484
  } catch (_) {
466
485
  return;
467
486
  }
487
+ handleStdioMessage(msg, framed);
488
+ }
468
489
 
469
- if (ready) {
470
- const resp = buildResponse(msg);
471
- if (resp) process.stdout.write(JSON.stringify(resp) + '\n');
472
- } else {
473
- messageQueue.push(msg);
490
+ function drain(flush = false) {
491
+ while (buffer.length) {
492
+ while (buffer[0] === 0x0a || buffer[0] === 0x0d) buffer = buffer.subarray(1);
493
+ if (!buffer.length) return;
494
+
495
+ const text = buffer.toString('utf8');
496
+ if (text.toLowerCase().startsWith('content-length:')) {
497
+ let headerEnd = text.indexOf('\r\n\r\n');
498
+ let separatorLength = 4;
499
+ if (headerEnd === -1) {
500
+ headerEnd = text.indexOf('\n\n');
501
+ separatorLength = 2;
502
+ }
503
+ if (headerEnd === -1) return;
504
+
505
+ const header = text.slice(0, headerEnd);
506
+ const contentLength = parseContentLengthHeader(header);
507
+ if (!Number.isFinite(contentLength) || contentLength < 0) {
508
+ buffer = Buffer.alloc(0);
509
+ return;
510
+ }
511
+
512
+ const bodyStart = Buffer.byteLength(text.slice(0, headerEnd + separatorLength), 'utf8');
513
+ if (buffer.length < bodyStart + contentLength) return;
514
+ const body = buffer.subarray(bodyStart, bodyStart + contentLength).toString('utf8');
515
+ buffer = buffer.subarray(bodyStart + contentLength);
516
+ handleJsonPayload(body, true);
517
+ continue;
518
+ }
519
+
520
+ const newline = buffer.indexOf(0x0a);
521
+ if (newline === -1) {
522
+ if (!flush) return;
523
+ const line = buffer.toString('utf8');
524
+ buffer = Buffer.alloc(0);
525
+ handleJsonPayload(line, false);
526
+ return;
527
+ }
528
+ const line = buffer.subarray(0, newline).toString('utf8');
529
+ buffer = buffer.subarray(newline + 1);
530
+ handleJsonPayload(line, false);
474
531
  }
475
- });
476
- rl.on('close', () => process.exit(0));
477
-
478
- ready = true;
479
- for (const msg of messageQueue) {
480
- const resp = buildResponse(msg);
481
- if (resp) process.stdout.write(JSON.stringify(resp) + '\n');
482
532
  }
483
- messageQueue.length = 0;
533
+
534
+ process.stdin.on('data', (chunk) => {
535
+ buffer = Buffer.concat([buffer, chunk]);
536
+ drain(false);
537
+ });
538
+ process.stdin.on('end', () => {
539
+ drain(true);
540
+ process.exit(0);
541
+ });
484
542
  }
485
543
 
486
544
  startStdio();
package/xmux.zsh CHANGED
@@ -71,7 +71,7 @@ _xmux_refresh_home() {
71
71
 
72
72
  _xmux_refresh_paths
73
73
 
74
- XMUX_VERSION="1.0.39"
74
+ XMUX_VERSION="1.0.40"
75
75
  XMUX_LEAD_AGENT="${XMUX_LEAD_AGENT:-codex-lead}"
76
76
 
77
77
  _xmux_q() {
@@ -4208,8 +4208,8 @@ _xmux_run_codex_setup_script() {
4208
4208
 
4209
4209
  _xmux_setup_codex_usage() {
4210
4210
  cat >&2 <<'EOF'
4211
- Usage: xmux setup-codex [--skills-dir <dir>] [--without-skills] [--mcp-package <package[@version]>] [--mcp-version <version>]
4212
- xmux doctor-codex [--mcp-package <package[@version]>] [--mcp-version <version>]
4211
+ Usage: xmux setup-codex [--skills-dir <dir>] [--without-skills] [--mcp-package <package[@version]>] [--mcp-version <version>] [--mcp-npx-prefix <dir>]
4212
+ xmux doctor-codex [--mcp-package <package[@version]>] [--mcp-version <version>] [--mcp-npx-prefix <dir>]
4213
4213
  xmux remove-codex
4214
4214
  EOF
4215
4215
  }
@@ -4224,7 +4224,7 @@ _xmux_cmd_setup_codex() {
4224
4224
  setup_args+=("$arg")
4225
4225
  shift
4226
4226
  ;;
4227
- --home|--project|--skills-dir|--mcp-package|--mcp-version|--mcp-bin)
4227
+ --home|--project|--skills-dir|--mcp-package|--mcp-version|--mcp-bin|--mcp-npx-prefix)
4228
4228
  [[ $# -ge 2 ]] || { echo "error: $arg requires a value." >&2; return 1; }
4229
4229
  setup_args+=("$arg" "$2")
4230
4230
  shift 2
@@ -4253,7 +4253,7 @@ _xmux_cmd_doctor_codex() {
4253
4253
  doctor_args+=("$arg")
4254
4254
  shift
4255
4255
  ;;
4256
- --home|--project|--skills-dir|--mcp-package|--mcp-version|--mcp-bin)
4256
+ --home|--project|--skills-dir|--mcp-package|--mcp-version|--mcp-bin|--mcp-npx-prefix)
4257
4257
  [[ $# -ge 2 ]] || { echo "error: $arg requires a value." >&2; return 1; }
4258
4258
  doctor_args+=("$arg" "$2")
4259
4259
  shift 2