xmux-bridge 1.0.39

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 ADDED
@@ -0,0 +1,219 @@
1
+ # XMux
2
+
3
+ XMux is a Codex-led tmux teammate runtime. The single user-facing command is
4
+ `xmux`; Codex is always the lead, and supported teammates are Claude, Gemini,
5
+ and Copilot.
6
+
7
+ <table>
8
+ <tr>
9
+ <th>Create</th>
10
+ <th>Shutdown</th>
11
+ </tr>
12
+ <tr>
13
+ <td><img src="docs/screenshots/team-create.png" alt="XMux team creation" width="100%"></td>
14
+ <td><img src="docs/screenshots/team-shutdown.png" alt="XMux team shutdown" width="100%"></td>
15
+ </tr>
16
+ </table>
17
+
18
+ ## How to Use
19
+
20
+ Install XMux with Homebrew:
21
+
22
+ ```bash
23
+ brew tap DwvN-Lee/xmux
24
+ brew install xmux
25
+ ```
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
+ Configure Codex integration explicitly:
33
+
34
+ ```bash
35
+ xmux setup-codex
36
+ xmux doctor-codex
37
+ ```
38
+
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.
54
+
55
+ Start the Codex lead from the target project directory:
56
+
57
+ ```bash
58
+ xmux -n refactor
59
+ ```
60
+
61
+ XMux displays short names with the project prefix, such as `XMux/refactor`.
62
+ Raw `tmux ls` may show a slash-free internal session key; use `xmux sessions`
63
+ and `xmux attach XMux/refactor` for user-facing runtime operations.
64
+
65
+ This is the only command users normally need to run directly. After the lead is
66
+ open, ask Codex for teammate work in natural language. For example:
67
+
68
+ - "Use Gemini and Copilot to review this change."
69
+ - "Ask Claude to look for edge cases before implementation."
70
+ - "Ask Copilot for a repository-aware implementation check."
71
+
72
+ Codex then manages the XMux lifecycle through hidden agent-facing commands such as:
73
+
74
+ ```bash
75
+ xmux teamStatus
76
+ xmux teammateAdd -t refactor claude gemini copilot
77
+ xmux teammateShutdown -t refactor gemini-worker
78
+ xmux teamShutdown -t refactor --reason manual-shutdown
79
+ ```
80
+
81
+ Those commands are hidden from the default `xmux --help` output. Use
82
+ `xmux help agent` when agent-facing lifecycle syntax is needed for automation
83
+ or troubleshooting.
84
+
85
+ To start a detached or scripted team outside an interactive lead session, Codex
86
+ automation can use:
87
+
88
+ ```bash
89
+ xmux teamCreate -t refactor-team -n refactor claude gemini copilot
90
+ ```
91
+
92
+ XMux is agent friendly: when the user explicitly asks to use teammates, the
93
+ Codex lead may create the scoped team, attach requested teammates, send mailbox
94
+ requests, wait for responses, and perform bounded retries without asking the
95
+ user to approve each XMux step. Runtime permission prompts are only for the
96
+ tooling boundary, such as tmux access from a sandboxed process.
97
+
98
+ Inspect and operate a team when debugging:
99
+
100
+ ```bash
101
+ xmux teamStatus -t refactor
102
+ xmux doctor -t refactor --log-lines 0
103
+ xmux teammateStatus -t refactor
104
+ xmux paneInfo gemini-worker -t refactor
105
+ xmux teammateShutdown -t refactor gemini-worker
106
+ xmux teamShutdown -t refactor --reason manual-shutdown
107
+ ```
108
+
109
+ Lower-level diagnostics are also hidden from the default help. Use
110
+ `xmux help debug` for the full troubleshooting surface.
111
+
112
+ `xmux teammateShutdown` keeps the team live. `xmux teamShutdown` is team-wide
113
+ and archives the team state while preserving inboxes, requests, request ids,
114
+ and events. Lead `/exit` triggers shutdown/archive by default; start with
115
+ `--keep-team-on-lead-exit` to leave teammates running for debugging.
116
+
117
+ Unsupported legacy paths fail explicitly because Codex is the XMux lead, not a
118
+ teammate:
119
+
120
+ ```bash
121
+ xmux codex
122
+ xmux start --codex
123
+ xmux start -c
124
+ ```
125
+
126
+ ## Agent-Managed Internals
127
+
128
+ This section describes the runtime work handled by Codex and XMux automation.
129
+ Users normally do not run these steps directly.
130
+
131
+ Runtime state is project-local:
132
+
133
+ ```text
134
+ <project>/.codex/xmux/
135
+ teams/<team>/
136
+ team.json
137
+ inboxes/
138
+ requests/
139
+ events.jsonl
140
+ archive/<timestamp>-<team>/
141
+ archive.json
142
+ team.json
143
+ inboxes/
144
+ requests/
145
+ events.jsonl
146
+ ```
147
+
148
+ Runtime path environment names are now split by responsibility:
149
+
150
+ ```text
151
+ XMUX_INSTALL_DIR # XMux source/install directory
152
+ XMUX_PROJECT_DIR # project root where Codex is working
153
+ XMUX_STATE_DIR # project-local runtime state, usually $XMUX_PROJECT_DIR/.codex/xmux
154
+ ```
155
+
156
+ Codex uses the normal user runtime under `~/.codex`. XMux does not create an
157
+ isolated Codex home for a team, and Codex teammate mode is unsupported.
158
+
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.
164
+
165
+ The Codex lead MCP server is `xmux_lead`. `xmux setup-codex` configures it so
166
+ Codex can route requests, wait for teammate responses, read events, and inspect
167
+ team status.
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
171
+ `XMUX_PROJECT_DIR`/`XMUX_STATE_DIR`; those values come from the active
172
+ `xmux -n <session>` lead runtime.
173
+
174
+ Provider teammates write responses through `bridge-mcp-server.js`, using the
175
+ team runtime environment prepared by XMux. The bridge and mailbox paths are
176
+ implementation details behind Codex-led teammate orchestration.
177
+
178
+ The explicit Codex setup installs available XMux skills under
179
+ `~/.codex/skills` only from `--skills-dir` or `XMUX_CODEX_SKILLS_DIR`.
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:
188
+
189
+ ```text
190
+ $xmux-teams
191
+ $xmux-claude
192
+ $xmux-gemini
193
+ $xmux-copilot
194
+ $xmux-diagnosis
195
+ $xmux-send-pane
196
+ ```
197
+
198
+ Development verification for agent/runtime changes:
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).
210
+
211
+ ## Docs
212
+
213
+ - [Documentation index](docs/README.md)
214
+ - [Codex lead runtime](docs/runtime/codex-lead.md)
215
+ - [Homebrew distribution](docs/operations/homebrew.md)
216
+ - [Wrapper-first debugging](docs/operations/debugging.md)
217
+ - [Claude teammate](docs/teammates/claude.md)
218
+ - [Gemini teammate](docs/teammates/gemini.md)
219
+ - [Copilot teammate](docs/teammates/copilot.md)
package/bin/xmux ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env zsh
2
+ set -euo pipefail
3
+
4
+ script_path="${0:A}"
5
+ script_dir="${script_path:h}"
6
+ install_dir="${script_dir:h}"
7
+
8
+ source "$install_dir/xmux.zsh"
9
+ xmux "$@"
@@ -0,0 +1,407 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * bridge-mcp-server.js
4
+ * Minimal MCP server for xmux.
5
+ * Exposes write_to_lead(text, summary?) so Claude, Gemini, and Copilot
6
+ * teammates can write directly to the XMux lead inbox.
7
+ *
8
+ * Modes:
9
+ * stdio (default): JSON-RPC over stdin/stdout
10
+ * HTTP/SSE: --http <port> → GET /sse + POST /messages
11
+ *
12
+ * Config resolution order (first wins):
13
+ * 1. CLI args: --outbox <path> --agent <name> --team <name>
14
+ * 2. Env vars: XMUX_OUTBOX, XMUX_AGENT, XMUX_TEAM
15
+ *
16
+ * If neither is provided the server exits immediately. A previous
17
+ * `.bridge-<agent>.env` mtime-scan fallback was removed: it caused
18
+ * standalone CLI sessions (notably Gemini launched outside xmux-gemini)
19
+ * to silently adopt an unrelated active team's outbox + agent identity
20
+ * and forge write_to_lead entries into the wrong outbox. See
21
+ * docs/investigations/orphan-env-fallback-2026-04-20.md.
22
+ */
23
+
24
+ 'use strict';
25
+
26
+ const fs = require('fs');
27
+ const path = require('path');
28
+ const crypto = require('crypto');
29
+ const { spawnSync } = require('child_process');
30
+
31
+ // ── Parse ALL CLI args first (determines mode) ───────────────────────────────
32
+
33
+ let AGENT_NAME = '';
34
+ let OUTBOX = '';
35
+ let HTTP_PORT = 0; // 0 = stdio mode
36
+ let XMUX_TEAM = '';
37
+ let XMUX_INSTALL_DIR = '';
38
+
39
+ const cliArgs = process.argv.slice(2);
40
+ for (let i = 0; i < cliArgs.length; i++) {
41
+ if (cliArgs[i] === '--outbox' && cliArgs[i + 1]) OUTBOX = cliArgs[++i];
42
+ if (cliArgs[i] === '--agent' && cliArgs[i + 1]) AGENT_NAME = cliArgs[++i];
43
+ if (cliArgs[i] === '--team' && cliArgs[i + 1]) XMUX_TEAM = cliArgs[++i];
44
+ if (cliArgs[i] === '--http' && cliArgs[i + 1]) HTTP_PORT = parseInt(cliArgs[++i], 10);
45
+ }
46
+
47
+ // ── Env vars ─────────────────────────────────────────────────────────────────
48
+
49
+ if (!OUTBOX) OUTBOX = process.env.XMUX_OUTBOX || '';
50
+ if (!AGENT_NAME) AGENT_NAME = process.env.XMUX_AGENT || '';
51
+ if (!XMUX_TEAM) XMUX_TEAM = process.env.XMUX_TEAM || '';
52
+ XMUX_INSTALL_DIR = process.env.XMUX_INSTALL_DIR
53
+ ? path.resolve(process.env.XMUX_INSTALL_DIR)
54
+ : '';
55
+
56
+ // ── Fail fast: reject spawns that never got a team identity ─────────────────
57
+ // A standalone CLI (e.g. user runs `gemini` in ~/Desktop) can spawn this
58
+ // MCP server via a generic `xmux-bridge` entry in its settings. Without
59
+ // explicit OUTBOX/AGENT we refuse to serve — previously a mtime-based
60
+ // .bridge-<agent>.env fallback would pick the most-recently-active team
61
+ // and the standalone CLI's write_to_lead calls ended up in that team's
62
+ // outbox under the wrong `from` (the orphan-env bug, 2026-04-20).
63
+
64
+ if (!OUTBOX || !AGENT_NAME) {
65
+ process.stderr.write(
66
+ '[xmux-bridge] fatal: no team identity configured.\n' +
67
+ ' Provide --outbox <path> --agent <name> OR env XMUX_OUTBOX/XMUX_AGENT.\n' +
68
+ ' Standalone CLI sessions not launched via xmux claude, xmux gemini, or xmux copilot must not\n' +
69
+ ' register xmux-bridge as an MCP server — remove the entry from that tool\'s\n' +
70
+ ' settings if you see this message.\n'
71
+ );
72
+ process.exit(1);
73
+ }
74
+
75
+ // ── Path validation ──────────────────────────────────────────────────────────
76
+
77
+ function validateOutboxPath(p) {
78
+ const resolved = path.resolve(p);
79
+ const home = process.env.HOME || '';
80
+ const stateDir = process.env.XMUX_STATE_DIR || path.resolve(home, '.codex', 'xmux');
81
+ const allowedBases = [
82
+ path.resolve(stateDir),
83
+ ];
84
+ return allowedBases.some(base => resolved.startsWith(base + path.sep)) &&
85
+ resolved.endsWith('.json') && !p.includes('..');
86
+ }
87
+
88
+ // ── Outbox write ─────────────────────────────────────────────────────────────
89
+
90
+ function nowTs() {
91
+ return new Date().toISOString().replace(/(\.\d{3})\d*Z/, '$1Z');
92
+ }
93
+
94
+ function atomicWrite(filePath, data) {
95
+ const dir = path.dirname(path.resolve(filePath));
96
+ const tmp = path.join(dir, `.tmp-${crypto.randomBytes(6).toString('hex')}.json`);
97
+ fs.writeFileSync(tmp, JSON.stringify(data, null, 2), 'utf-8');
98
+ fs.renameSync(tmp, filePath);
99
+ }
100
+
101
+ // mkdir-based cross-process file lock. Coordinates with the Node mailbox
102
+ // runtime that uses the same `<path>.lock.d` mutex.
103
+ // Prevents lost updates when mailbox writers concurrently read-modify-write
104
+ // the same lead inbox JSON file.
105
+ function withLock(targetPath, fn) {
106
+ const lockPath = targetPath + '.lock.d';
107
+ let acquired = false;
108
+ for (let i = 0; i < 200; i++) {
109
+ try {
110
+ fs.mkdirSync(lockPath);
111
+ acquired = true;
112
+ break;
113
+ } catch (e) {
114
+ if (e.code !== 'EEXIST') throw e;
115
+ const start = Date.now();
116
+ while (Date.now() - start < 25) {} // busy-wait 25ms
117
+ }
118
+ }
119
+ if (!acquired) throw new Error(`could not acquire lock on ${targetPath}`);
120
+ try {
121
+ return fn();
122
+ } finally {
123
+ try { fs.rmdirSync(lockPath); } catch (_) {}
124
+ }
125
+ }
126
+
127
+ // Cap with read-message preference: when over cap, drop oldest READ
128
+ // message first; only drop oldest unread as a last resort. Prevents
129
+ // the double-push (response + idle_notification) from silently losing
130
+ // unread messages at the 50-cap boundary.
131
+ function trimToCap(msgs, cap) {
132
+ while (msgs.length > cap) {
133
+ const idx = msgs.findIndex(m => m.read);
134
+ if (idx >= 0) msgs.splice(idx, 1);
135
+ else msgs.shift();
136
+ }
137
+ return msgs;
138
+ }
139
+
140
+ function mailboxInstallBases() {
141
+ const seen = new Set();
142
+ const bases = [];
143
+ for (const candidate of [XMUX_INSTALL_DIR, __dirname]) {
144
+ if (!candidate) continue;
145
+ const resolved = path.resolve(candidate);
146
+ if (seen.has(resolved)) continue;
147
+ seen.add(resolved);
148
+ bases.push(resolved);
149
+ }
150
+ return bases;
151
+ }
152
+
153
+ function resolveMailboxBackend() {
154
+ for (const base of mailboxInstallBases()) {
155
+ const nodeCli = path.join(base, 'dist', 'bin', 'xmux-mailbox.js');
156
+ if (fs.existsSync(nodeCli)) {
157
+ return {
158
+ kind: 'node',
159
+ command: process.execPath || 'node',
160
+ prefixArgs: [nodeCli],
161
+ };
162
+ }
163
+ }
164
+ return null;
165
+ }
166
+
167
+ function writeToXMuxMailbox(text, summary, requestId, status) {
168
+ const backend = resolveMailboxBackend();
169
+ if (!backend) return null;
170
+ if (!XMUX_TEAM) return null;
171
+
172
+ const args = [
173
+ ...backend.prefixArgs,
174
+ 'write-response',
175
+ XMUX_TEAM,
176
+ '--from',
177
+ AGENT_NAME,
178
+ '--text',
179
+ text,
180
+ '--status',
181
+ status || 'done',
182
+ ];
183
+ if (summary) args.push('--summary', summary);
184
+ if (requestId) args.push('--request-id', requestId);
185
+
186
+ const result = spawnSync(backend.command, args, {
187
+ env: { ...process.env },
188
+ encoding: 'utf8',
189
+ maxBuffer: 1024 * 1024,
190
+ });
191
+ if (result.error) return `error: ${result.error.message || result.error}`;
192
+ if (result.status !== 0) {
193
+ const detail = String(result.stderr || result.stdout || '').trim();
194
+ return `error: xmux mailbox write failed${detail ? `: ${detail}` : ''}`;
195
+ }
196
+ return 'ok: response delivered to lead';
197
+ }
198
+
199
+ function writeToLeadImpl(text, summary, requestId, status) {
200
+ if (!OUTBOX) return 'error: XMUX_OUTBOX not set';
201
+ if (!validateOutboxPath(OUTBOX)) return 'error: XMUX_OUTBOX path is invalid or outside allowed directory';
202
+ if (!AGENT_NAME) return 'error: AGENT_NAME not set (pass --agent or set XMUX_AGENT)';
203
+
204
+ const xmuxResult = writeToXMuxMailbox(text, summary, requestId, status);
205
+ if (xmuxResult !== null) return xmuxResult;
206
+
207
+ try {
208
+ return withLock(OUTBOX, () => {
209
+ let msgs = [];
210
+ try { msgs = JSON.parse(fs.readFileSync(OUTBOX, 'utf-8')); } catch (_) { msgs = []; }
211
+ const ts1 = nowTs();
212
+ const entry = { from: AGENT_NAME, text, timestamp: ts1, read: false };
213
+ if (summary) entry.summary = summary;
214
+ if (requestId) entry.request_id = requestId;
215
+ if (status) entry.status = status;
216
+ msgs.push(entry);
217
+ trimToCap(msgs, 50);
218
+ const ts2 = nowTs();
219
+ const idlePayload = JSON.stringify({
220
+ type: 'idle_notification', from: AGENT_NAME, idleReason: 'available', timestamp: ts2,
221
+ });
222
+ msgs.push({ from: AGENT_NAME, text: idlePayload, timestamp: ts2, read: false });
223
+ trimToCap(msgs, 50);
224
+ atomicWrite(OUTBOX, msgs);
225
+ return 'ok: response delivered to lead';
226
+ });
227
+ } catch (exc) {
228
+ return `error: ${exc}`;
229
+ }
230
+ }
231
+
232
+ // ── Tool schema ───────────────────────────────────────────────────────────────
233
+
234
+ const TOOL_SCHEMA = {
235
+ name: 'write_to_lead',
236
+ description:
237
+ 'Send your completed response to the Codex lead through the XMux mailbox.',
238
+ inputSchema: {
239
+ type: 'object',
240
+ properties: {
241
+ text: { type: 'string', description: 'Your full response text.' },
242
+ summary: { type: 'string', description: 'Optional short summary (first sentence, < 60 chars).' },
243
+ request_id: { type: 'string', description: 'Optional XMux request id this response completes.' },
244
+ status: { type: 'string', description: 'Optional response status, defaults to done.' },
245
+ },
246
+ required: ['text'],
247
+ },
248
+ };
249
+
250
+ // ── MCP request handler (returns response object or null) ─────────────────────
251
+
252
+ function buildResponse(msg) {
253
+ const method = msg.method || '';
254
+ const id = msg.id !== undefined ? msg.id : null;
255
+
256
+ if (method === 'initialize') {
257
+ const params = msg.params || {};
258
+ return {
259
+ jsonrpc: '2.0', id,
260
+ result: {
261
+ protocolVersion: params.protocolVersion || '2024-11-05',
262
+ capabilities: { tools: {} },
263
+ serverInfo: { name: 'xmux-bridge', version: '0.3.0' },
264
+ },
265
+ };
266
+ }
267
+
268
+ if (method === 'notifications/initialized' || method === 'initialized') {
269
+ return null;
270
+ }
271
+
272
+ if (method === 'tools/list') {
273
+ return { jsonrpc: '2.0', id, result: { tools: [TOOL_SCHEMA] } };
274
+ }
275
+
276
+ if (method === 'resources/list') {
277
+ return { jsonrpc: '2.0', id, result: { resources: [] } };
278
+ }
279
+
280
+ if (method === 'resources/templates/list') {
281
+ return { jsonrpc: '2.0', id, result: { resourceTemplates: [] } };
282
+ }
283
+
284
+ if (method === 'tools/call') {
285
+ const params = msg.params || {};
286
+ if (params.name === 'write_to_lead') {
287
+ const args = params.arguments || {};
288
+ const result = writeToLeadImpl(
289
+ args.text || '',
290
+ args.summary || '',
291
+ args.request_id || args.requestId || '',
292
+ args.status || 'done',
293
+ );
294
+ return { jsonrpc: '2.0', id, result: { content: [{ type: 'text', text: result }] } };
295
+ }
296
+ return { jsonrpc: '2.0', id, error: { code: -32601, message: 'Unknown tool' } };
297
+ }
298
+
299
+ if (id !== null) {
300
+ return { jsonrpc: '2.0', id, error: { code: -32601, message: `Unknown method: ${method}` } };
301
+ }
302
+
303
+ return null;
304
+ }
305
+
306
+ // ── STDIO mode ────────────────────────────────────────────────────────────────
307
+ // CRITICAL: register stdin listener FIRST to avoid race condition.
308
+ // Provider CLIs send `initialize` immediately after spawn; if readline isn't
309
+ // listening yet the message is lost and the CLI times out (Tools: none).
310
+
311
+ function startStdio() {
312
+ const readline = require('readline');
313
+ const messageQueue = [];
314
+ let ready = false;
315
+
316
+ const rl = readline.createInterface({ input: process.stdin, terminal: false });
317
+ rl.on('line', (line) => {
318
+ if (!line.trim()) return;
319
+ 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);
326
+ }
327
+ });
328
+ // Parent CLI closed stdin → exit so MCP subprocess does not linger and
329
+ // 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;
338
+ }
339
+
340
+ // ── HTTP/SSE mode ─────────────────────────────────────────────────────────────
341
+ // Implements the MCP HTTP+SSE transport:
342
+ // GET /sse → SSE stream; first event is `endpoint` with POST URL
343
+ // POST /messages?sessionId=<id> → JSON-RPC request; response via SSE `message` event
344
+
345
+ function startHttpServer(port) {
346
+ const http = require('http');
347
+ const sessions = new Map(); // sessionId → SSE response
348
+
349
+ const server = http.createServer((req, res) => {
350
+ res.setHeader('Access-Control-Allow-Origin', '*');
351
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
352
+
353
+ if (req.method === 'OPTIONS') {
354
+ res.writeHead(204);
355
+ res.end();
356
+ return;
357
+ }
358
+
359
+ const reqUrl = new URL(req.url, `http://127.0.0.1:${port}`);
360
+
361
+ if (req.method === 'GET' && reqUrl.pathname === '/sse') {
362
+ const sessionId = crypto.randomBytes(8).toString('hex');
363
+ res.writeHead(200, {
364
+ 'Content-Type': 'text/event-stream',
365
+ 'Cache-Control': 'no-cache',
366
+ 'Connection': 'keep-alive',
367
+ });
368
+ sessions.set(sessionId, res);
369
+ res.write(`event: endpoint\ndata: /messages?sessionId=${sessionId}\n\n`);
370
+ req.on('close', () => sessions.delete(sessionId));
371
+ return;
372
+ }
373
+
374
+ if (req.method === 'POST' && reqUrl.pathname === '/messages') {
375
+ const sessionId = reqUrl.searchParams.get('sessionId');
376
+ const sseRes = sessions.get(sessionId);
377
+ let body = '';
378
+ req.on('data', chunk => body += chunk);
379
+ req.on('end', () => {
380
+ res.writeHead(202);
381
+ res.end();
382
+ let msg;
383
+ try { msg = JSON.parse(body); } catch (_) { return; }
384
+ const resp = buildResponse(msg);
385
+ if (resp && sseRes) {
386
+ sseRes.write(`event: message\ndata: ${JSON.stringify(resp)}\n\n`);
387
+ }
388
+ });
389
+ return;
390
+ }
391
+
392
+ res.writeHead(404);
393
+ res.end();
394
+ });
395
+
396
+ server.listen(port, '127.0.0.1', () => {
397
+ process.stderr.write(`[xmux-bridge] HTTP MCP server on http://127.0.0.1:${port}/sse\n`);
398
+ });
399
+ }
400
+
401
+ // ── Start ─────────────────────────────────────────────────────────────────────
402
+
403
+ if (HTTP_PORT > 0) {
404
+ startHttpServer(HTTP_PORT);
405
+ } else {
406
+ startStdio();
407
+ }
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { main } = require('../mailbox/cli');
5
+
6
+ process.exit(main(process.argv.slice(2)));
@@ -0,0 +1,3 @@
1
+ 'use strict';
2
+
3
+ module.exports = require('../../src/mailbox/cli');
@@ -0,0 +1,3 @@
1
+ 'use strict';
2
+
3
+ module.exports = require('../../src/mailbox/core');
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "xmux-bridge",
3
+ "version": "1.0.39",
4
+ "description": "MCP stdio server for XMux teammate responses",
5
+ "bin": {
6
+ "xmux-bridge": "./bridge-mcp-server.js",
7
+ "xmux-lead-mcp": "./xmux-lead-mcp-server.js",
8
+ "xmux-mailbox": "./dist/bin/xmux-mailbox.js",
9
+ "xmux": "./bin/xmux",
10
+ "xmux-bridge-relay": "./xmux-bridge.zsh"
11
+ },
12
+ "files": [
13
+ "bridge-mcp-server.js",
14
+ "xmux-lead-mcp-server.js",
15
+ "xmux-bridge.zsh",
16
+ "bin/xmux",
17
+ "xmux.zsh",
18
+ "scripts/*.js",
19
+ "dist/bin/xmux-mailbox.js",
20
+ "dist/mailbox",
21
+ "src/runtime",
22
+ "src/mailbox"
23
+ ],
24
+ "keywords": [
25
+ "mcp",
26
+ "claude",
27
+ "gemini",
28
+ "copilot",
29
+ "tmux",
30
+ "teammate"
31
+ ],
32
+ "author": "DwvN-Lee",
33
+ "license": "MIT",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "https://github.com/DwvN-Lee/XMux.git"
37
+ }
38
+ }