xmux-bridge 1.0.39 → 1.0.41
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 +19 -57
- package/{bridge-mcp-server.js → mcp/servers/bridge.js} +90 -23
- package/{xmux-lead-mcp-server.js → mcp/servers/lead.js} +84 -22
- package/{scripts/setup_claude_mcp.js → mcp/setup/claude.js} +10 -9
- package/{scripts/setup_xmux_codex_mcp.js → mcp/setup/codex.js} +62 -20
- package/{scripts/setup_copilot_mcp.js → mcp/setup/copilot.js} +7 -8
- package/{scripts/setup_gemini_mcp.js → mcp/setup/gemini.js} +7 -8
- package/package.json +7 -13
- 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,35 +138,25 @@ 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
|
-
`XMUX_PROJECT_DIR`/`XMUX_STATE_DIR`; those values come from the
|
|
172
|
-
`xmux -n <session>` lead runtime.
|
|
173
|
-
|
|
174
|
-
Provider teammates write responses through `bridge
|
|
175
|
-
team runtime environment prepared by XMux. The
|
|
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 `mcp/servers/bridge.js`, using the
|
|
155
|
+
team runtime environment prepared by XMux. The MCP and mailbox paths are
|
|
176
156
|
implementation details behind Codex-led teammate orchestration.
|
|
177
157
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
Homebrew does not install Codex skills or repo-local plugin files; normal
|
|
181
|
-
runtime operation depends on the installed `xmux` command and
|
|
182
|
-
`XMUX_INSTALL_DIR`, not a checkout path.
|
|
183
|
-
|
|
184
|
-
The plugin skill source of truth is `plugins/xmux/skills`; the top-level
|
|
185
|
-
`skills/` directory is a mirrored distribution copy for explicit skill refresh
|
|
186
|
-
workflows. Users explicitly invoke Codex skills with `$`, for example
|
|
187
|
-
`$xmux-teams`. The official XMux skills cover agent-facing orchestration flows:
|
|
158
|
+
Users can ask for teammate work in natural language. When XMux skills are
|
|
159
|
+
available in Codex, the official skill shortcuts are:
|
|
188
160
|
|
|
189
161
|
```text
|
|
190
162
|
$xmux-teams
|
|
@@ -195,24 +167,14 @@ $xmux-diagnosis
|
|
|
195
167
|
$xmux-send-pane
|
|
196
168
|
```
|
|
197
169
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
```bash
|
|
201
|
-
pytest tests -q
|
|
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
|
-
Formula draft and distribution notes live in
|
|
209
|
-
[Homebrew distribution](docs/operations/homebrew.md).
|
|
170
|
+
Homebrew installation details live in [Homebrew installation](docs/operations/homebrew.md).
|
|
210
171
|
|
|
211
172
|
## Docs
|
|
212
173
|
|
|
213
174
|
- [Documentation index](docs/README.md)
|
|
175
|
+
- [Repository layout](docs/runtime/repository-layout.md)
|
|
214
176
|
- [Codex lead runtime](docs/runtime/codex-lead.md)
|
|
215
|
-
- [Homebrew
|
|
177
|
+
- [Homebrew installation](docs/operations/homebrew.md)
|
|
216
178
|
- [Wrapper-first debugging](docs/operations/debugging.md)
|
|
217
179
|
- [Claude teammate](docs/teammates/claude.md)
|
|
218
180
|
- [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;
|
|
@@ -305,36 +309,99 @@ function buildResponse(msg) {
|
|
|
305
309
|
|
|
306
310
|
// ── STDIO mode ────────────────────────────────────────────────────────────────
|
|
307
311
|
// CRITICAL: register stdin listener FIRST to avoid race condition.
|
|
308
|
-
// Provider CLIs send `initialize` immediately after spawn; if
|
|
312
|
+
// Provider CLIs send `initialize` immediately after spawn; if stdin isn't
|
|
309
313
|
// listening yet the message is lost and the CLI times out (Tools: none).
|
|
310
314
|
|
|
315
|
+
function writeMcpResponse(resp, framed) {
|
|
316
|
+
const body = JSON.stringify(resp);
|
|
317
|
+
if (framed) {
|
|
318
|
+
process.stdout.write(`Content-Length: ${Buffer.byteLength(body, 'utf8')}\r\n\r\n${body}`);
|
|
319
|
+
} else {
|
|
320
|
+
process.stdout.write(body + '\n');
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function handleStdioMessage(msg, framed) {
|
|
325
|
+
const resp = buildResponse(msg);
|
|
326
|
+
if (resp) writeMcpResponse(resp, framed);
|
|
327
|
+
}
|
|
328
|
+
|
|
311
329
|
function startStdio() {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
330
|
+
let buffer = Buffer.alloc(0);
|
|
331
|
+
|
|
332
|
+
function parseContentLengthHeader(header) {
|
|
333
|
+
for (const line of header.split(/\r?\n/)) {
|
|
334
|
+
const match = line.match(/^Content-Length:\s*(\d+)\s*$/i);
|
|
335
|
+
if (match) return Number.parseInt(match[1], 10);
|
|
336
|
+
}
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
315
339
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
if (!line.trim()) return;
|
|
340
|
+
function handleJsonPayload(payload, framed) {
|
|
341
|
+
if (!payload.trim()) return;
|
|
319
342
|
let msg;
|
|
320
|
-
try {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
} else {
|
|
325
|
-
messageQueue.push(msg);
|
|
343
|
+
try {
|
|
344
|
+
msg = JSON.parse(payload);
|
|
345
|
+
} catch (_) {
|
|
346
|
+
return;
|
|
326
347
|
}
|
|
348
|
+
handleStdioMessage(msg, framed);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function drain(flush = false) {
|
|
352
|
+
while (buffer.length) {
|
|
353
|
+
while (buffer[0] === 0x0a || buffer[0] === 0x0d) buffer = buffer.subarray(1);
|
|
354
|
+
if (!buffer.length) return;
|
|
355
|
+
|
|
356
|
+
const text = buffer.toString('utf8');
|
|
357
|
+
if (text.toLowerCase().startsWith('content-length:')) {
|
|
358
|
+
let headerEnd = text.indexOf('\r\n\r\n');
|
|
359
|
+
let separatorLength = 4;
|
|
360
|
+
if (headerEnd === -1) {
|
|
361
|
+
headerEnd = text.indexOf('\n\n');
|
|
362
|
+
separatorLength = 2;
|
|
363
|
+
}
|
|
364
|
+
if (headerEnd === -1) return;
|
|
365
|
+
|
|
366
|
+
const header = text.slice(0, headerEnd);
|
|
367
|
+
const contentLength = parseContentLengthHeader(header);
|
|
368
|
+
if (!Number.isFinite(contentLength) || contentLength < 0) {
|
|
369
|
+
buffer = Buffer.alloc(0);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const bodyStart = Buffer.byteLength(text.slice(0, headerEnd + separatorLength), 'utf8');
|
|
374
|
+
if (buffer.length < bodyStart + contentLength) return;
|
|
375
|
+
const body = buffer.subarray(bodyStart, bodyStart + contentLength).toString('utf8');
|
|
376
|
+
buffer = buffer.subarray(bodyStart + contentLength);
|
|
377
|
+
handleJsonPayload(body, true);
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const newline = buffer.indexOf(0x0a);
|
|
382
|
+
if (newline === -1) {
|
|
383
|
+
if (!flush) return;
|
|
384
|
+
const line = buffer.toString('utf8');
|
|
385
|
+
buffer = Buffer.alloc(0);
|
|
386
|
+
handleJsonPayload(line, false);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
const line = buffer.subarray(0, newline).toString('utf8');
|
|
390
|
+
buffer = buffer.subarray(newline + 1);
|
|
391
|
+
handleJsonPayload(line, false);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
process.stdin.on('data', (chunk) => {
|
|
396
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
397
|
+
drain(false);
|
|
327
398
|
});
|
|
328
399
|
// Parent CLI closed stdin → exit so MCP subprocess does not linger and
|
|
329
400
|
// continue accepting writes after its intended session is over.
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
const resp = buildResponse(msg);
|
|
335
|
-
if (resp) process.stdout.write(JSON.stringify(resp) + '\n');
|
|
336
|
-
}
|
|
337
|
-
messageQueue.length = 0;
|
|
401
|
+
process.stdin.on('end', () => {
|
|
402
|
+
drain(true);
|
|
403
|
+
process.exit(0);
|
|
404
|
+
});
|
|
338
405
|
}
|
|
339
406
|
|
|
340
407
|
// ── HTTP/SSE mode ─────────────────────────────────────────────────────────────
|
|
@@ -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;
|
|
@@ -451,36 +455,94 @@ function buildResponse(msg) {
|
|
|
451
455
|
return null;
|
|
452
456
|
}
|
|
453
457
|
|
|
458
|
+
function writeMcpResponse(resp, framed) {
|
|
459
|
+
const body = JSON.stringify(resp);
|
|
460
|
+
if (framed) {
|
|
461
|
+
process.stdout.write(`Content-Length: ${Buffer.byteLength(body, 'utf8')}\r\n\r\n${body}`);
|
|
462
|
+
} else {
|
|
463
|
+
process.stdout.write(body + '\n');
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function handleStdioMessage(msg, framed) {
|
|
468
|
+
const resp = buildResponse(msg);
|
|
469
|
+
if (resp) writeMcpResponse(resp, framed);
|
|
470
|
+
}
|
|
471
|
+
|
|
454
472
|
function startStdio() {
|
|
455
|
-
|
|
456
|
-
const messageQueue = [];
|
|
457
|
-
let ready = false;
|
|
473
|
+
let buffer = Buffer.alloc(0);
|
|
458
474
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
475
|
+
function parseContentLengthHeader(header) {
|
|
476
|
+
for (const line of header.split(/\r?\n/)) {
|
|
477
|
+
const match = line.match(/^Content-Length:\s*(\d+)\s*$/i);
|
|
478
|
+
if (match) return Number.parseInt(match[1], 10);
|
|
479
|
+
}
|
|
480
|
+
return null;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function handleJsonPayload(payload, framed) {
|
|
484
|
+
if (!payload.trim()) return;
|
|
462
485
|
let msg;
|
|
463
486
|
try {
|
|
464
|
-
msg = JSON.parse(
|
|
487
|
+
msg = JSON.parse(payload);
|
|
465
488
|
} catch (_) {
|
|
466
489
|
return;
|
|
467
490
|
}
|
|
491
|
+
handleStdioMessage(msg, framed);
|
|
492
|
+
}
|
|
468
493
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
494
|
+
function drain(flush = false) {
|
|
495
|
+
while (buffer.length) {
|
|
496
|
+
while (buffer[0] === 0x0a || buffer[0] === 0x0d) buffer = buffer.subarray(1);
|
|
497
|
+
if (!buffer.length) return;
|
|
498
|
+
|
|
499
|
+
const text = buffer.toString('utf8');
|
|
500
|
+
if (text.toLowerCase().startsWith('content-length:')) {
|
|
501
|
+
let headerEnd = text.indexOf('\r\n\r\n');
|
|
502
|
+
let separatorLength = 4;
|
|
503
|
+
if (headerEnd === -1) {
|
|
504
|
+
headerEnd = text.indexOf('\n\n');
|
|
505
|
+
separatorLength = 2;
|
|
506
|
+
}
|
|
507
|
+
if (headerEnd === -1) return;
|
|
508
|
+
|
|
509
|
+
const header = text.slice(0, headerEnd);
|
|
510
|
+
const contentLength = parseContentLengthHeader(header);
|
|
511
|
+
if (!Number.isFinite(contentLength) || contentLength < 0) {
|
|
512
|
+
buffer = Buffer.alloc(0);
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const bodyStart = Buffer.byteLength(text.slice(0, headerEnd + separatorLength), 'utf8');
|
|
517
|
+
if (buffer.length < bodyStart + contentLength) return;
|
|
518
|
+
const body = buffer.subarray(bodyStart, bodyStart + contentLength).toString('utf8');
|
|
519
|
+
buffer = buffer.subarray(bodyStart + contentLength);
|
|
520
|
+
handleJsonPayload(body, true);
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const newline = buffer.indexOf(0x0a);
|
|
525
|
+
if (newline === -1) {
|
|
526
|
+
if (!flush) return;
|
|
527
|
+
const line = buffer.toString('utf8');
|
|
528
|
+
buffer = Buffer.alloc(0);
|
|
529
|
+
handleJsonPayload(line, false);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
const line = buffer.subarray(0, newline).toString('utf8');
|
|
533
|
+
buffer = buffer.subarray(newline + 1);
|
|
534
|
+
handleJsonPayload(line, false);
|
|
474
535
|
}
|
|
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
536
|
}
|
|
483
|
-
|
|
537
|
+
|
|
538
|
+
process.stdin.on('data', (chunk) => {
|
|
539
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
540
|
+
drain(false);
|
|
541
|
+
});
|
|
542
|
+
process.stdin.on('end', () => {
|
|
543
|
+
drain(true);
|
|
544
|
+
process.exit(0);
|
|
545
|
+
});
|
|
484
546
|
}
|
|
485
547
|
|
|
486
548
|
startStdio();
|
|
@@ -28,7 +28,7 @@ function stableHomebrewXmuxInstallDir(installDir) {
|
|
|
28
28
|
|
|
29
29
|
const prefix = resolved.split(marker, 1)[0];
|
|
30
30
|
const candidate = path.join(prefix, "opt", "xmux", "libexec");
|
|
31
|
-
if (fs.existsSync(path.join(candidate, "xmux.zsh"))) {
|
|
31
|
+
if (fs.existsSync(path.join(candidate, "runtime", "shell", "xmux.zsh")) || fs.existsSync(path.join(candidate, "xmux.zsh"))) {
|
|
32
32
|
return candidate;
|
|
33
33
|
}
|
|
34
34
|
return resolved;
|
|
@@ -36,13 +36,14 @@ function stableHomebrewXmuxInstallDir(installDir) {
|
|
|
36
36
|
|
|
37
37
|
function stableHomebrewXmuxFilePath(filePath) {
|
|
38
38
|
const resolved = absolute(filePath);
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
39
|
+
const marker = `${path.sep}Cellar${path.sep}xmux${path.sep}`;
|
|
40
|
+
const libexecSegment = `${path.sep}libexec${path.sep}`;
|
|
41
|
+
const libexecIndex = resolved.indexOf(libexecSegment);
|
|
42
|
+
if (!resolved.includes(marker) || libexecIndex < 0) return resolved;
|
|
43
|
+
const prefix = resolved.split(marker, 1)[0];
|
|
44
|
+
const optDir = path.join(prefix, "opt", "xmux", "libexec");
|
|
45
|
+
const relativePath = resolved.slice(libexecIndex + libexecSegment.length);
|
|
46
|
+
const candidate = path.join(optDir, relativePath);
|
|
46
47
|
if (fs.existsSync(candidate)) {
|
|
47
48
|
return candidate;
|
|
48
49
|
}
|
|
@@ -72,7 +73,7 @@ function atomicWriteJson(filePath, data) {
|
|
|
72
73
|
|
|
73
74
|
function usage() {
|
|
74
75
|
process.stderr.write(
|
|
75
|
-
"usage:
|
|
76
|
+
"usage: claude.js <bridge_js> <project_dir> <outbox> <agent> <team> <state_dir> <install_dir>\n",
|
|
76
77
|
);
|
|
77
78
|
}
|
|
78
79
|
|
|
@@ -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 || "");
|
|
@@ -31,26 +32,39 @@ function abs(value) {
|
|
|
31
32
|
return path.resolve(expandUser(value));
|
|
32
33
|
}
|
|
33
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
|
+
|
|
34
44
|
function stable_homebrew_xmux_install_dir(xmuxInstallDir) {
|
|
35
45
|
const installDir = abs(xmuxInstallDir);
|
|
36
46
|
const marker = `${path.sep}Cellar${path.sep}xmux${path.sep}`;
|
|
37
47
|
if (!installDir.includes(marker) || !installDir.endsWith(`${path.sep}libexec`)) {
|
|
38
48
|
return installDir;
|
|
39
49
|
}
|
|
40
|
-
if (!
|
|
50
|
+
if (!has_xmux_runtime(installDir)) {
|
|
41
51
|
return installDir;
|
|
42
52
|
}
|
|
43
53
|
const prefix = installDir.split(marker, 1)[0];
|
|
44
54
|
const candidate = path.join(prefix, "opt", "xmux", "libexec");
|
|
45
|
-
return
|
|
55
|
+
return has_xmux_runtime(candidate) ? candidate : installDir;
|
|
46
56
|
}
|
|
47
57
|
|
|
48
58
|
function stable_homebrew_xmux_file_path(inputPath) {
|
|
49
59
|
const resolved = abs(inputPath);
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
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);
|
|
54
68
|
return fs.existsSync(candidate) ? candidate : resolved;
|
|
55
69
|
}
|
|
56
70
|
|
|
@@ -138,14 +152,15 @@ function package_spec_has_version(packageSpec) {
|
|
|
138
152
|
}
|
|
139
153
|
|
|
140
154
|
function xmux_version_from_install_dir(xmuxInstallDir) {
|
|
141
|
-
const
|
|
155
|
+
const root = abs(xmuxInstallDir);
|
|
156
|
+
const content = read_text(xmux_runtime_shell_path(root)) || read_text(path.join(root, "xmux.zsh"));
|
|
142
157
|
const match = content.match(/^XMUX_VERSION=["']([^"']+)["']/m);
|
|
143
158
|
return match ? match[1] : "";
|
|
144
159
|
}
|
|
145
160
|
|
|
146
161
|
function default_mcp_package_spec(xmuxInstallDir, packageName = "", packageVersion = "") {
|
|
147
162
|
const installPackage = read_json(path.join(abs(xmuxInstallDir), "package.json")) || {};
|
|
148
|
-
const scriptPackage = read_json(path.join(path.dirname(path.dirname(abs(__filename))), "package.json")) || {};
|
|
163
|
+
const scriptPackage = read_json(path.join(path.dirname(path.dirname(path.dirname(abs(__filename)))), "package.json")) || {};
|
|
149
164
|
const name = packageName
|
|
150
165
|
|| process.env.XMUX_MCP_NPM_PACKAGE
|
|
151
166
|
|| installPackage.name
|
|
@@ -172,21 +187,33 @@ function node_mcp_config(serverPath) {
|
|
|
172
187
|
};
|
|
173
188
|
}
|
|
174
189
|
|
|
175
|
-
function
|
|
190
|
+
function default_mcp_npx_prefix(configPath = "") {
|
|
191
|
+
const fallback = path.join(os.homedir(), DEFAULT_MCP_NPX_PREFIX);
|
|
192
|
+
if (!configPath) return fallback;
|
|
193
|
+
const configDir = path.dirname(abs(configPath));
|
|
194
|
+
if (path.basename(configDir) === ".codex") {
|
|
195
|
+
return path.join(path.dirname(configDir), DEFAULT_MCP_NPX_PREFIX);
|
|
196
|
+
}
|
|
197
|
+
return path.join(configDir, DEFAULT_MCP_NPX_PREFIX);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function npx_mcp_config(packageSpec, binName = DEFAULT_MCP_BIN, npxPrefix = "") {
|
|
201
|
+
const prefix = abs(npxPrefix || process.env.XMUX_MCP_NPX_PREFIX || default_mcp_npx_prefix());
|
|
176
202
|
return {
|
|
177
203
|
mode: "npx",
|
|
178
204
|
command: "npx",
|
|
179
|
-
args: ["-y", "-p", packageSpec, binName],
|
|
205
|
+
args: ["--prefix", prefix, "-y", "-p", packageSpec, binName],
|
|
180
206
|
package_spec: packageSpec,
|
|
181
207
|
bin: binName,
|
|
182
|
-
|
|
208
|
+
npx_prefix: prefix,
|
|
209
|
+
label: `npx --prefix ${prefix} -y -p ${packageSpec} ${binName}`,
|
|
183
210
|
};
|
|
184
211
|
}
|
|
185
212
|
|
|
186
213
|
function resolve_mcp_config(xmuxInstallDir, opts = {}) {
|
|
187
214
|
if (opts.server_path) return node_mcp_config(opts.server_path);
|
|
188
215
|
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);
|
|
216
|
+
return npx_mcp_config(packageSpec, opts.mcp_bin || DEFAULT_MCP_BIN, opts.mcp_npx_prefix || "");
|
|
190
217
|
}
|
|
191
218
|
|
|
192
219
|
function normalize_mcp_config(mcpConfigOrServerPath, xmuxInstallDir = "", opts = {}) {
|
|
@@ -198,6 +225,7 @@ function normalize_mcp_config(mcpConfigOrServerPath, xmuxInstallDir = "", opts =
|
|
|
198
225
|
server_path: mcpConfigOrServerPath.server_path || "",
|
|
199
226
|
package_spec: mcpConfigOrServerPath.package_spec || "",
|
|
200
227
|
bin: mcpConfigOrServerPath.bin || "",
|
|
228
|
+
npx_prefix: mcpConfigOrServerPath.npx_prefix || "",
|
|
201
229
|
label: mcpConfigOrServerPath.label || [
|
|
202
230
|
mcpConfigOrServerPath.command,
|
|
203
231
|
...(mcpConfigOrServerPath.args || []),
|
|
@@ -212,6 +240,12 @@ function mcp_args_toml(mcpConfig) {
|
|
|
212
240
|
return `args = [${mcpConfig.args.map(toml_quote).join(", ")}]`;
|
|
213
241
|
}
|
|
214
242
|
|
|
243
|
+
function ensure_mcp_runtime_dirs(mcpConfig) {
|
|
244
|
+
if (mcpConfig.mode === "npx" && mcpConfig.npx_prefix) {
|
|
245
|
+
fs.mkdirSync(mcpConfig.npx_prefix, { recursive: true });
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
215
249
|
function build_block(mcpConfigOrServerPath, xmuxInstallDir) {
|
|
216
250
|
const mcpConfig = normalize_mcp_config(mcpConfigOrServerPath, xmuxInstallDir);
|
|
217
251
|
const pathEnv = resolve_path_with_node();
|
|
@@ -252,7 +286,7 @@ function is_xmux_runtime_bin_path(candidatePath, currentXmuxBin) {
|
|
|
252
286
|
if (expanded === abs(currentXmuxBin)) return true;
|
|
253
287
|
if (path.basename(expanded) !== "bin") return false;
|
|
254
288
|
const installDir = path.dirname(expanded);
|
|
255
|
-
if (
|
|
289
|
+
if (has_xmux_runtime(installDir) && fs.existsSync(path.join(expanded, "xmux"))) {
|
|
256
290
|
return true;
|
|
257
291
|
}
|
|
258
292
|
if (path.basename(installDir) !== "libexec") return false;
|
|
@@ -399,7 +433,7 @@ function install_xmux_command_rule(configPath) {
|
|
|
399
433
|
let content = remove_marker_block(read_text(filePath), RULE_BEGIN, RULE_END);
|
|
400
434
|
const block = [
|
|
401
435
|
RULE_BEGIN,
|
|
402
|
-
"# Allow the scoped XMux wrapper command; XMux
|
|
436
|
+
"# Allow the scoped XMux wrapper command; user intent and XMux wrappers control operation scope.",
|
|
403
437
|
'prefix_rule(pattern=["xmux"], decision="allow")',
|
|
404
438
|
RULE_END,
|
|
405
439
|
].join("\n");
|
|
@@ -548,12 +582,12 @@ function _xmux_lead_mcp_processes_from_ps(psOutput) {
|
|
|
548
582
|
const processes = [];
|
|
549
583
|
for (const rawLine of String(psOutput || "").split(/\r?\n/)) {
|
|
550
584
|
const stripped = rawLine.trim();
|
|
551
|
-
if (!stripped.includes("xmux-lead-mcp-server.js")) continue;
|
|
585
|
+
if (!stripped.includes("mcp/servers/lead.js") && !stripped.includes("xmux-lead-mcp-server.js")) continue;
|
|
552
586
|
const match = stripped.match(/^(\d+)\s+(.*)$/);
|
|
553
587
|
if (!match) continue;
|
|
554
588
|
const [, pid, command] = match;
|
|
555
589
|
const tokens = splitShellWords(command.trim());
|
|
556
|
-
const serverPath = tokens.find((token) => token.endsWith("xmux-lead-mcp-server.js")) || "";
|
|
590
|
+
const serverPath = tokens.find((token) => token.endsWith("mcp/servers/lead.js") || token.endsWith("xmux-lead-mcp-server.js")) || "";
|
|
557
591
|
if (!serverPath) continue;
|
|
558
592
|
processes.push({ pid, command: command.trim(), server_path: abs(serverPath) });
|
|
559
593
|
}
|
|
@@ -571,7 +605,7 @@ function running_xmux_lead_mcp_processes() {
|
|
|
571
605
|
|
|
572
606
|
function _is_homebrew_xmux_mcp_server(serverPath) {
|
|
573
607
|
const normalized = abs(serverPath);
|
|
574
|
-
return normalized.endsWith("xmux-lead-mcp-server.js")
|
|
608
|
+
return (normalized.endsWith(`${path.sep}mcp${path.sep}servers${path.sep}lead.js`) || normalized.endsWith("xmux-lead-mcp-server.js"))
|
|
575
609
|
&& normalized.includes(`${path.sep}Cellar${path.sep}xmux${path.sep}`)
|
|
576
610
|
&& normalized.includes(`${path.sep}libexec${path.sep}`);
|
|
577
611
|
}
|
|
@@ -616,7 +650,7 @@ function doctor_codex(configPath, xmuxInstallDir, mcpConfigOrServerPath, skillsD
|
|
|
616
650
|
} else if (installedNames.size) {
|
|
617
651
|
notes.push(["OK", `XMux Codex skills installed under ${skills_root(configPath)}`]);
|
|
618
652
|
} else {
|
|
619
|
-
notes.push(["
|
|
653
|
+
notes.push(["OK", "optional XMux skills are not configured"]);
|
|
620
654
|
}
|
|
621
655
|
|
|
622
656
|
if (fs.existsSync(plugin_cache_path(configPath))) {
|
|
@@ -665,6 +699,7 @@ function parse_args(argv) {
|
|
|
665
699
|
mcp_package: "",
|
|
666
700
|
mcp_version: "",
|
|
667
701
|
mcp_bin: DEFAULT_MCP_BIN,
|
|
702
|
+
mcp_npx_prefix: "",
|
|
668
703
|
};
|
|
669
704
|
for (let i = 0; i < argv.length;) {
|
|
670
705
|
const arg = argv[i];
|
|
@@ -687,6 +722,7 @@ function parse_args(argv) {
|
|
|
687
722
|
"--mcp-package",
|
|
688
723
|
"--mcp-version",
|
|
689
724
|
"--mcp-bin",
|
|
725
|
+
"--mcp-npx-prefix",
|
|
690
726
|
].includes(arg) && i + 1 < argv.length) {
|
|
691
727
|
const key = arg.slice(2).replace(/-/g, "_");
|
|
692
728
|
opts[key] = expandUser(argv[i + 1]);
|
|
@@ -725,7 +761,8 @@ function resolve_config_path(opts) {
|
|
|
725
761
|
function main(argv = process.argv.slice(2)) {
|
|
726
762
|
const opts = parse_args(argv);
|
|
727
763
|
const configPath = resolve_config_path(opts);
|
|
728
|
-
|
|
764
|
+
opts.mcp_npx_prefix = abs(opts.mcp_npx_prefix || process.env.XMUX_MCP_NPX_PREFIX || default_mcp_npx_prefix(configPath));
|
|
765
|
+
const scriptInstallDir = path.dirname(path.dirname(path.dirname(abs(__filename))));
|
|
729
766
|
const rawInstallDir = abs(opts.xmux_install_dir || scriptInstallDir);
|
|
730
767
|
const xmuxInstallDir = stable_homebrew_xmux_install_dir(rawInstallDir);
|
|
731
768
|
const xmuxProjectDir = abs(opts.xmux_project_dir || default_xmux_project_dir());
|
|
@@ -736,6 +773,8 @@ function main(argv = process.argv.slice(2)) {
|
|
|
736
773
|
return doctor_codex(configPath, xmuxInstallDir, mcpConfig, opts.skills_dir, opts.quiet);
|
|
737
774
|
}
|
|
738
775
|
|
|
776
|
+
ensure_mcp_runtime_dirs(mcpConfig);
|
|
777
|
+
|
|
739
778
|
let content = remove_xmux_blocks(read_text(configPath));
|
|
740
779
|
if (opts.remove) {
|
|
741
780
|
content = remove_codex_shell_environment(content, xmuxInstallDir);
|
|
@@ -770,7 +809,9 @@ function main(argv = process.argv.slice(2)) {
|
|
|
770
809
|
console.log(" xmux_project_dir: inherited from xmux-launched Codex runtime");
|
|
771
810
|
console.log(" xmux_state_dir: inherited from xmux-launched Codex runtime");
|
|
772
811
|
if (installedSkills.length) console.log(` skills: ${installedSkills.join(", ")}`);
|
|
773
|
-
else if (opts.install_skills
|
|
812
|
+
else if (opts.install_skills && (opts.skills_dir || process.env.XMUX_CODEX_SKILLS_DIR)) {
|
|
813
|
+
console.log(" skills: no importable XMux skills found");
|
|
814
|
+
}
|
|
774
815
|
console.log(" plugin_cache: disabled; stale XMux plugin cache removed if present");
|
|
775
816
|
return 0;
|
|
776
817
|
}
|
|
@@ -788,6 +829,7 @@ module.exports = {
|
|
|
788
829
|
remove_xmux_blocks,
|
|
789
830
|
build_block,
|
|
790
831
|
default_mcp_package_spec,
|
|
832
|
+
default_mcp_npx_prefix,
|
|
791
833
|
resolve_mcp_config,
|
|
792
834
|
path_with_xmux_bin,
|
|
793
835
|
ensure_codex_shell_environment,
|