muuuuse 5.0.2 → 5.5.4

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
@@ -1,44 +1,56 @@
1
1
  # 🔌Muuuuse
2
2
 
3
- `🔌Muuuuse` is a tiny terminal relay with flexible routing.
3
+ `🔌Muuuuse` is a tiny terminal relay.
4
4
 
5
5
  It does one job:
6
- - arm any number of terminals with `muuuuse <N>`
7
- - each seat can route to any other seats with individual flow modes
6
+ - arm terminal one with `muuuuse 1`
7
+ - arm terminal two with `muuuuse 2`
8
+ - have seat 1 generate a session key and seat 2 sign it
9
+ - additional isolated pairs work the same way: `3/4`, `5/6`, `7/8`, ...
10
+ - choose per-seat relay mode with `flow on` or `flow off`
8
11
  - watch Codex, Claude, or Gemini for local assistant output
9
- - relay that output to configured targets with per-target flow control
12
+ - inject that output into the other armed terminal
10
13
  - keep looping until you stop it
11
14
 
12
15
  The whole surface is:
13
16
 
14
17
  ```bash
15
18
  muuuuse 1
19
+ muuuuse 1 flow on
20
+ muuuuse 1 flow off
21
+ muuuuse 1 flow off continue 5
16
22
  muuuuse 2
17
- muuuuse 3 link 1 flow on 2 flow off
18
- muuuuse 4 link 3 flow on
23
+ muuuuse 2 flow off
24
+ muuuuse 2 flow on continue 3
25
+ muuuuse 3
26
+ muuuuse 3 flow on
27
+ muuuuse 4
28
+ muuuuse 4 flow off continue 1
19
29
  muuuuse status
20
30
  muuuuse stop
21
31
  ```
22
32
 
23
- ## Routing
33
+ ## Flow
24
34
 
25
- Any seat can route to any targets. Syntax:
35
+ Terminal 1:
26
36
 
27
37
  ```bash
28
- muuuuse <seat> link <target1> flow <on|off> [<target2> flow <on|off> ...]
38
+ muuuuse 1 flow on
29
39
  ```
30
40
 
31
- Example: Seat 3 routes to seat 1 with full output (flow on) and seat 4 with final answers only (flow off):
41
+ Terminal 2:
32
42
 
33
43
  ```bash
34
- muuuuse 3 link 1 flow on 4 flow off
44
+ muuuuse 2 flow off
35
45
  ```
36
46
 
37
- Each seat defines its own routing targets. When that seat gets an answer from Claude/Codex/Gemini, it sends to all configured targets with their respective flow modes.
47
+ Now both shells are armed. `muuuuse 1` generates the session key, `muuuuse 2` signs it, and only that signed pair relays. Every odd/even adjacent pair works the same way in parallel: `3/4`, `5/6`, `7/8`, and so on. Use those shells normally.
38
48
 
39
- `flow on` means relay both thinking/commentary and final answers. `flow off` means relay final answers only.
49
+ `flow on` means that seat relays commentary and final answers. `flow off` means that seat relays and accepts final answers only. Mixed calibration is allowed per seat.
40
50
 
41
- If you want Codex in one and Gemini in another, start them inside the armed shells:
51
+ `continue <seat>` forwards that seat's relayed output into another armed seat without changing the signed odd/even pair law. This lets you build local loops like `1 -> 2 -> 3 -> 4 -> 1` while every adjacent pair still keeps its own session keypair.
52
+
53
+ If you want Codex in one and Gemini in the other, start them inside the armed shells:
42
54
 
43
55
  ```bash
44
56
  codex
@@ -48,7 +60,7 @@ codex
48
60
  gemini
49
61
  ```
50
62
 
51
- `🔌Muuuuse` tails the local session logs for supported CLIs, relays according to each target's flow mode, types that output into the target seat, and then sends Enter as a separate keystroke.
63
+ `🔌Muuuuse` tails the local session logs for supported CLIs, relays according to each seat's flow mode, types that output into the other seat, and then sends Enter as a separate keystroke.
52
64
 
53
65
  Check the live state from any terminal:
54
66
 
@@ -65,9 +77,8 @@ muuuuse stop
65
77
  ## Notes
66
78
 
67
79
  - state lives under `~/.muuuuse`
68
- - each seat defines its own routing targets independently
69
- - any seat can route to any other seat number
70
- - flow modes are per-target (not per-seat)
80
+ - only the signed armed pair can exchange relay events
81
+ - `continue <seat>` is a separate local forwarding lane and can target any armed seat number
71
82
  - supported relay detection is built for Codex, Claude, and Gemini
72
83
 
73
84
  ## Install
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "muuuuse",
3
- "version": "5.0.2",
4
- "description": "🔌Muuuuse arms terminals with flexible routing to any targets - pure message relay graph.",
3
+ "version": "5.5.4",
4
+ "description": "🔌Muuuuse arms regular terminals in isolated pairs and can continue relay output into any other armed seat.",
5
5
  "type": "commonjs",
6
6
  "bin": {
7
7
  "muuuuse": "bin/muuse.js"
package/src/agents.js CHANGED
@@ -69,44 +69,6 @@ function detectAgent(processes) {
69
69
  return buildDetectedAgent("gemini", process);
70
70
  }
71
71
  }
72
-
73
- // Fallback: if no agent process found, check for Claude Code running in a separate terminal
74
- // (Claude Code's session updates even if it's not a child process of this shell)
75
- if (processes.length > 0) {
76
- // Use the bash/shell process as reference for timing
77
- const shellProcess = processes[0];
78
- const now = Date.now();
79
- const recentThreshold = now - 2 * 60 * 1000; // Last 2 minutes
80
-
81
- const CLAUDE_ROOT = path.join(os.homedir(), ".claude", "projects");
82
- if (fs.existsSync(CLAUDE_ROOT)) {
83
- try {
84
- const sessionFiles = walkFiles(CLAUDE_ROOT, (f) => f.endsWith(".jsonl"))
85
- .map(filePath => {
86
- try {
87
- const stat = fs.statSync(filePath);
88
- return { filePath, mtimeMs: stat.mtimeMs };
89
- } catch {
90
- return null;
91
- }
92
- })
93
- .filter(e => e && e.mtimeMs > recentThreshold)
94
- .sort((a, b) => b.mtimeMs - a.mtimeMs)[0];
95
-
96
- if (sessionFiles) {
97
- return {
98
- type: "claude",
99
- pid: process.pid || shellProcess.pid,
100
- args: "claude-code-terminal-session",
101
- cwd: shellProcess.cwd || null,
102
- elapsedSeconds: Math.round((Date.now() - shellProcess.startedAtMs) / 1000),
103
- processStartedAtMs: Date.now() - (shellProcess.elapsedSeconds ?? 0) * 1000,
104
- };
105
- }
106
- } catch {}
107
- }
108
- }
109
-
110
72
  return null;
111
73
  }
112
74
 
@@ -593,7 +555,8 @@ function selectClaudeSessionFile(currentPath, processStartedAtMs, options = {})
593
555
  return selectSessionCandidatePath(candidates, currentPath, processStartedAtMs);
594
556
  }
595
557
 
596
- function extractClaudeAssistantText(content) {
558
+ function extractClaudeAssistantText(content, options = {}) {
559
+ const flowMode = options.flowMode === true;
597
560
  if (!Array.isArray(content)) {
598
561
  return "";
599
562
  }
@@ -606,23 +569,7 @@ function extractClaudeAssistantText(content) {
606
569
  if (item.type === "text" && typeof item.text === "string") {
607
570
  return [item.text.trim()];
608
571
  }
609
- return [];
610
- })
611
- .filter((text) => text.length > 0)
612
- .join("\n");
613
- }
614
-
615
- function extractClaudeThinkingText(content) {
616
- if (!Array.isArray(content)) {
617
- return "";
618
- }
619
-
620
- return content
621
- .flatMap((item) => {
622
- if (!item || typeof item !== "object") {
623
- return [];
624
- }
625
- if (item.type === "thinking" && typeof item.thinking === "string") {
572
+ if (flowMode && item.type === "thinking" && typeof item.thinking === "string") {
626
573
  return [item.thinking.trim()];
627
574
  }
628
575
  return [];
@@ -639,46 +586,28 @@ function parseClaudeAssistantLine(line, options = {}) {
639
586
  return null;
640
587
  }
641
588
 
642
- const isFinal = entry.message?.stop_reason === "end_turn";
643
- if (!flowMode && !isFinal) {
589
+ if (!flowMode && entry.message?.stop_reason !== "end_turn") {
644
590
  return null;
645
591
  }
646
592
 
647
- const id = entry.uuid || entry.message.id || hashText(line);
648
- const timestamp = entry.timestamp || new Date().toISOString();
649
- const results = [];
650
-
651
- if (flowMode) {
652
- const thinking = sanitizeRelayText(extractClaudeThinkingText(entry.message.content));
653
- if (thinking) {
654
- results.push({
655
- id: `${id}-thinking`,
656
- text: thinking,
657
- phase: "commentary",
658
- timestamp,
659
- });
660
- }
661
- }
662
-
663
- const text = sanitizeRelayText(extractClaudeAssistantText(entry.message.content));
664
- if (text) {
665
- results.push({
666
- id,
667
- text,
668
- phase: isFinal ? "final_answer" : "commentary",
669
- timestamp,
670
- });
593
+ const text = sanitizeRelayText(extractClaudeAssistantText(entry.message.content, options));
594
+ if (!text) {
595
+ return null;
671
596
  }
672
597
 
673
- return results.length > 0 ? results : null;
598
+ return {
599
+ id: entry.uuid || entry.message.id || hashText(line),
600
+ text,
601
+ phase: flowMode && entry.message?.stop_reason !== "end_turn" ? "commentary" : "final_answer",
602
+ timestamp: entry.timestamp || new Date().toISOString(),
603
+ };
674
604
  } catch {
675
605
  return null;
676
606
  }
677
607
  }
678
608
 
679
609
  function parseClaudeFinalLine(line) {
680
- const results = parseClaudeAssistantLine(line, { flowMode: false });
681
- return Array.isArray(results) ? results[0] || null : results;
610
+ return parseClaudeAssistantLine(line, { flowMode: false });
682
611
  }
683
612
 
684
613
  function readClaudeAnswers(filePath, offset, sinceMs = null, options = {}) {
@@ -687,10 +616,8 @@ function readClaudeAnswers(filePath, offset, sinceMs = null, options = {}) {
687
616
  .split("\n")
688
617
  .map((line) => line.trim())
689
618
  .filter((line) => line.length > 0)
690
- .flatMap((line) => {
691
- const result = parseClaudeAssistantLine(line, options);
692
- return Array.isArray(result) ? result : result ? [result] : [];
693
- })
619
+ .map((line) => parseClaudeAssistantLine(line, options))
620
+ .filter((entry) => entry !== null)
694
621
  .filter((entry) => isAnswerNewEnough(entry, sinceMs));
695
622
 
696
623
  return { nextOffset, answers };
package/src/cli.js CHANGED
@@ -56,10 +56,11 @@ async function main(argv = process.argv.slice(2)) {
56
56
 
57
57
  const seatId = normalizeSeatId(command);
58
58
  if (seatId) {
59
- const { flowMode, continueTargets } = parseSeatOptions(command, argv.slice(1));
59
+ const { flowMode, continueSeatId, continueTargets } = parseSeatOptions(command, argv.slice(1));
60
60
  const seat = new ArmedSeat({
61
61
  cwd: process.cwd(),
62
62
  continueTargets,
63
+ continueSeatId,
63
64
  flowMode,
64
65
  seatId,
65
66
  });
@@ -105,60 +106,117 @@ function renderSeatStatus(seat) {
105
106
  }
106
107
 
107
108
  function renderLinkTargets(seat) {
108
- const targets = Array.isArray(seat.continueTargets) ? seat.continueTargets : [];
109
- if (targets.length === 0) {
110
- return "";
109
+ const targets = [];
110
+ if (seat.partnerSeatId) {
111
+ targets.push({
112
+ targetSeatId: seat.partnerSeatId,
113
+ flowMode: seat.flowMode || "off",
114
+ });
115
+ }
116
+ for (const target of Array.isArray(seat.continueTargets) ? seat.continueTargets : []) {
117
+ targets.push(target);
111
118
  }
112
- return targets.map((target) => `${target.targetSeatId}:${target.flowMode}`).join(", ");
119
+
120
+ return targets
121
+ .map((target) => `${target.targetSeatId}:${target.flowMode}`)
122
+ .join(", ");
113
123
  }
114
124
 
115
125
  function parseSeatOptions(command, args) {
116
126
  const seatId = normalizeSeatId(command);
117
127
  let flowMode = "off";
128
+ let continueSeatId = null;
118
129
  let continueTargets = [];
119
- if (args.length === 0) {
120
- return { flowMode, continueTargets };
121
- }
130
+ let index = 0;
122
131
 
123
- if (String(args[0] || "").trim().toLowerCase() === "link") {
124
- const parsedLinks = parseLinkTargets(args.slice(1), seatId, flowMode);
125
- if (parsedLinks.consumed === args.length - 1 && parsedLinks.consumed > 0) {
126
- return {
127
- flowMode: parsedLinks.flowMode,
128
- continueTargets: parsedLinks.continueTargets,
129
- };
130
- }
131
- } else {
132
- let index = 0;
133
-
134
- const flowToken = String(args[index] || "").trim().toLowerCase();
135
- const flowModeToken = String(args[index + 1] || "").trim().toLowerCase();
136
- if (flowToken === "flow" && (flowModeToken === "on" || flowModeToken === "off")) {
137
- flowMode = flowModeToken;
138
- index += 2;
132
+ while (index < args.length) {
133
+ const token = String(args[index] || "").trim().toLowerCase();
134
+
135
+ if (token === "flow") {
136
+ const flowToken = String(args[index + 1] || "").trim().toLowerCase();
137
+ if (flowToken === "on" || flowToken === "off") {
138
+ flowMode = flowToken;
139
+ index += 2;
140
+ continue;
141
+ }
142
+ break;
139
143
  }
140
144
 
141
- const continueToken = String(args[index] || "").trim().toLowerCase();
142
- const targetSeatId = normalizeSeatId(args[index + 1]);
143
- if (continueToken === "continue" && targetSeatId) {
144
- continueTargets = [{ targetSeatId, flowMode }];
145
- index += 2;
145
+ if (token === "continue") {
146
+ const parsedTargets = parseContinueTargets(args.slice(index + 1), flowMode);
147
+ if (parsedTargets.targets.length > 0) {
148
+ continueTargets = mergeTargets(continueTargets, parsedTargets.targets);
149
+ continueSeatId = continueTargets[0].targetSeatId;
150
+ index += 1 + parsedTargets.consumed;
151
+ continue;
152
+ }
153
+ break;
146
154
  }
147
155
 
148
- if (index === args.length) {
149
- return { flowMode, continueTargets };
156
+ if (token === "link") {
157
+ const parsedLinks = parseLinkTargets(args.slice(index + 1), seatId, flowMode);
158
+ if (parsedLinks.consumed > 0) {
159
+ flowMode = parsedLinks.flowMode;
160
+ continueTargets = mergeTargets(continueTargets, parsedLinks.continueTargets);
161
+ continueSeatId = continueTargets[0]?.targetSeatId || null;
162
+ index += 1 + parsedLinks.consumed;
163
+ continue;
164
+ }
165
+ break;
150
166
  }
167
+
168
+ break;
169
+ }
170
+
171
+ if (index === args.length) {
172
+ return { flowMode, continueSeatId, continueTargets };
151
173
  }
152
174
 
153
175
  throw new Error(
154
- `\`muuuuse ${command}\` accepts no extra arguments or \`link <seat> flow on [<seat> flow off ...]\`. Run it directly in the terminal you want to arm.`
176
+ `\`muuuuse ${command}\` accepts no extra arguments, \`flow on\` / \`flow off\`, optional \`continue <seat>\`, or \`link <seat> flow on [<seat> flow off ...]\`. Run it directly in the terminal you want to arm.`
155
177
  );
156
178
  }
157
179
 
158
- function parseLinkTargets(args, seatId, defaultFlowMode) {
180
+ function mergeTargets(existingTargets, nextTargets) {
181
+ const merged = [];
182
+ for (const target of Array.isArray(existingTargets) ? existingTargets : []) {
183
+ upsertTarget(merged, target);
184
+ }
185
+ for (const target of Array.isArray(nextTargets) ? nextTargets : []) {
186
+ upsertTarget(merged, target);
187
+ }
188
+
189
+ return merged;
190
+ }
191
+
192
+ function parseContinueTargets(args, defaultFlowMode) {
159
193
  const targets = [];
160
194
  let consumed = 0;
161
195
 
196
+ while (consumed < args.length) {
197
+ const targetSeatId = normalizeSeatId(args[consumed]);
198
+ if (!targetSeatId) {
199
+ break;
200
+ }
201
+
202
+ const nextFlowMode = parseFlowModeToken(args[consumed + 1], args[consumed + 2]);
203
+ const target = {
204
+ targetSeatId,
205
+ flowMode: nextFlowMode || defaultFlowMode,
206
+ };
207
+ upsertTarget(targets, target);
208
+ consumed += nextFlowMode ? 3 : 1;
209
+ }
210
+
211
+ return { consumed, targets };
212
+ }
213
+
214
+ function parseLinkTargets(args, seatId, defaultFlowMode) {
215
+ const partnerSeatId = seatId ? getPartnerSeatId(seatId) : null;
216
+ const continueTargets = [];
217
+ let flowMode = defaultFlowMode;
218
+ let consumed = 0;
219
+
162
220
  while (consumed < args.length) {
163
221
  const targetSeatId = normalizeSeatId(args[consumed]);
164
222
  if (!targetSeatId) {
@@ -170,15 +228,19 @@ function parseLinkTargets(args, seatId, defaultFlowMode) {
170
228
  break;
171
229
  }
172
230
 
173
- upsertTarget(targets, {
174
- targetSeatId,
175
- flowMode: targetFlowMode,
176
- });
231
+ if (targetSeatId === partnerSeatId) {
232
+ flowMode = targetFlowMode;
233
+ } else {
234
+ upsertTarget(continueTargets, {
235
+ targetSeatId,
236
+ flowMode: targetFlowMode,
237
+ });
238
+ }
177
239
 
178
240
  consumed += 3;
179
241
  }
180
242
 
181
- return { consumed, continueTargets: targets, flowMode: defaultFlowMode };
243
+ return { consumed, continueTargets, flowMode };
182
244
  }
183
245
 
184
246
  function parseFlowModeToken(flowToken, modeToken) {
@@ -201,5 +263,4 @@ function upsertTarget(targets, nextTarget) {
201
263
 
202
264
  module.exports = {
203
265
  main,
204
- parseSeatOptions,
205
266
  };
package/src/runtime.js CHANGED
@@ -42,7 +42,6 @@ const {
42
42
 
43
43
  const TYPE_CHUNK_DELAY_MS = 18;
44
44
  const TYPE_CHUNK_SIZE = 24;
45
- const TYPE_SUBMIT_DELAY_MS = 60;
46
45
  const MIRROR_SUPPRESSION_WINDOW_MS = 30 * 1000;
47
46
  const PENDING_RELAY_CONTEXT_TTL_MS = 2 * 60 * 1000;
48
47
  const EMITTED_ANSWER_TTL_MS = 5 * 60 * 1000;
@@ -57,32 +56,6 @@ const CHILD_ENV_DROP_KEYS = [
57
56
  "CODEX_THREAD_ID",
58
57
  ];
59
58
 
60
- function bestEffortEnableChildEcho(child) {
61
- const ptsName = String(child?.ptsName || "").trim();
62
- if (!ptsName || process.platform === "win32") {
63
- return;
64
- }
65
-
66
- try {
67
- execFileSync("stty", [
68
- "-F",
69
- ptsName,
70
- "echo",
71
- "icanon",
72
- "isig",
73
- "iexten",
74
- "echoe",
75
- "echok",
76
- "echoke",
77
- "echoctl",
78
- ], {
79
- stdio: "ignore",
80
- });
81
- } catch {
82
- // Best effort only. The shell or child app may later change its own tty mode.
83
- }
84
- }
85
-
86
59
  function normalizeFlowMode(flowMode) {
87
60
  return String(flowMode || "").trim().toLowerCase() === "on" ? "on" : "off";
88
61
  }
@@ -92,26 +65,6 @@ function normalizeContinueSeatId(value) {
92
65
  return seatId || null;
93
66
  }
94
67
 
95
- function normalizeContinueTargets(targets, defaultFlowMode = "off") {
96
- const normalized = [];
97
- const seen = new Set();
98
-
99
- for (const entry of Array.isArray(targets) ? targets : []) {
100
- const targetSeatId = normalizeSeatId(entry?.targetSeatId ?? entry?.seatId ?? entry);
101
- if (!targetSeatId || seen.has(targetSeatId)) {
102
- continue;
103
- }
104
-
105
- seen.add(targetSeatId);
106
- normalized.push({
107
- targetSeatId,
108
- flowMode: normalizeFlowMode(entry?.flowMode ?? entry?.flow ?? defaultFlowMode),
109
- });
110
- }
111
-
112
- return normalized;
113
- }
114
-
115
68
  function resolveShell() {
116
69
  const shell = String(process.env.SHELL || "").trim();
117
70
  return shell || "/bin/bash";
@@ -222,12 +175,7 @@ function findJoinableSessionName(currentPath = process.cwd(), seatId = 2) {
222
175
  const stopRequestedAtMs = Date.parse(stopRequest?.requestedAt || "");
223
176
  const createdAtMs = Date.parse(controller?.createdAt || anchorMeta?.startedAt || anchorStatus?.updatedAt || "");
224
177
 
225
- // Accept session if: controller exists AND (anchor is live OR anchor has written meta/status files).
226
- // This handles the startup race: anchor might not have a live PID yet, but if it wrote files, it's initialized.
227
- const controllerExists = controller !== null;
228
- const anchorInitialized = anchorMeta !== null || anchorStatus !== null;
229
- const anchorReady = anchorLive || anchorInitialized;
230
- if (!controllerExists || !anchorReady || seatLive) {
178
+ if (!anchorLive || seatLive) {
231
179
  return null;
232
180
  }
233
181
 
@@ -497,7 +445,6 @@ function buildAnswerSignaturePayload(sessionName, challenge, entry) {
497
445
  seatId: entry.seatId,
498
446
  origin: entry.origin,
499
447
  phase: entry.phase || "final_answer",
500
- flowMode: entry.flowMode || "off",
501
448
  createdAt: entry.createdAt,
502
449
  text: entry.text,
503
450
  });
@@ -517,7 +464,6 @@ function buildContinuationEntry(sourceSessionName, targetSeatId, entry) {
517
464
  chainId: entry.chainId,
518
465
  hop: entry.hop,
519
466
  sourceAnswerId: entry.id,
520
- flowMode: entry.flowMode || null,
521
467
  publicKey: entry.publicKey || null,
522
468
  signature: entry.signature || null,
523
469
  };
@@ -626,14 +572,6 @@ async function sendTextAndEnter(child, text, shouldAbort = () => false) {
626
572
  return false;
627
573
  }
628
574
 
629
- // Some TUIs treat fast, chunked relay typing as paste input and suppress an
630
- // immediate Enter. A short settle delay keeps submit behavior reliable.
631
- await sleep(TYPE_SUBMIT_DELAY_MS);
632
-
633
- if (shouldAbort() || !child) {
634
- return false;
635
- }
636
-
637
575
  try {
638
576
  child.write("\r");
639
577
  } catch {
@@ -649,14 +587,10 @@ class ArmedSeat {
649
587
  this.partnerSeatId = getPartnerSeatId(options.seatId);
650
588
  this.anchorSeatId = isAnchorSeat(options.seatId) ? options.seatId : this.partnerSeatId;
651
589
  this.flowMode = normalizeFlowMode(options.flowMode);
652
- this.continueTargets = normalizeContinueTargets(
653
- options.continueTargets || (
654
- options.continueSeatId ? [{ targetSeatId: options.continueSeatId, flowMode: options.flowMode }] : []
655
- ),
656
- this.flowMode
657
- );
590
+ this.continueSeatId = normalizeContinueSeatId(options.continueSeatId);
591
+ this.continueTargets = Array.isArray(options.continueTargets) ? options.continueTargets : [];
658
592
  this.cwd = normalizeWorkingPath(options.cwd);
659
- if (this.continueTargets.some((target) => target.targetSeatId === this.seatId)) {
593
+ if (this.continueSeatId === this.seatId || this.continueTargets.some((t) => t.targetSeatId === this.seatId)) {
660
594
  throw new Error(`\`muuuuse ${this.seatId}\` cannot continue to itself.`);
661
595
  }
662
596
  this.sessionName = resolveSessionName(this.cwd, this.seatId);
@@ -732,6 +666,7 @@ class ArmedSeat {
732
666
  partnerSeatId: this.partnerSeatId,
733
667
  sessionName: this.sessionName,
734
668
  flowMode: this.flowMode,
669
+ continueSeatId: this.continueSeatId,
735
670
  continueTargets: this.continueTargets,
736
671
  cwd: this.cwd,
737
672
  pid: process.pid,
@@ -748,6 +683,7 @@ class ArmedSeat {
748
683
  partnerSeatId: this.partnerSeatId,
749
684
  sessionName: this.sessionName,
750
685
  flowMode: this.flowMode,
686
+ continueSeatId: this.continueSeatId,
751
687
  continueTargets: this.continueTargets,
752
688
  cwd: this.cwd,
753
689
  pid: process.pid,
@@ -941,7 +877,6 @@ class ArmedSeat {
941
877
  env: childEnv,
942
878
  name: childEnv.TERM,
943
879
  });
944
- bestEffortEnableChildEcho(this.child);
945
880
 
946
881
  this.childPid = this.child.pid;
947
882
  this.writeMeta();
@@ -1094,23 +1029,19 @@ class ArmedSeat {
1094
1029
  return Number.isFinite(requestedAtMs) && requestedAtMs > this.startedAtMs;
1095
1030
  }
1096
1031
 
1097
- shouldCaptureCommentary() {
1098
- return this.flowMode === "on" || this.continueTargets.some((target) => target.flowMode === "on");
1099
- }
1100
-
1101
- findContinuationTarget(targetSeatId) {
1102
- const normalizedTargetSeatId = normalizeSeatId(targetSeatId);
1103
- if (!normalizedTargetSeatId) {
1032
+ findContinuationTarget(targetSeatId = null) {
1033
+ const seatIdToFind = targetSeatId || this.continueSeatId;
1034
+ if (!seatIdToFind) {
1104
1035
  return null;
1105
1036
  }
1106
1037
 
1107
1038
  const candidates = listSessionNames()
1108
1039
  .map((sessionName) => {
1109
- if (!getSeatDirIfExists(sessionName, normalizedTargetSeatId)) {
1040
+ if (!getSeatDirIfExists(sessionName, seatIdToFind)) {
1110
1041
  return null;
1111
1042
  }
1112
1043
 
1113
- const seat = buildSeatReport(sessionName, normalizedTargetSeatId);
1044
+ const seat = buildSeatReport(sessionName, seatIdToFind);
1114
1045
  if (!seat || !matchesWorkingPath(seat.cwd, this.cwd)) {
1115
1046
  return null;
1116
1047
  }
@@ -1151,8 +1082,7 @@ class ArmedSeat {
1151
1082
  return;
1152
1083
  }
1153
1084
 
1154
- const inboundFlowMode = normalizeFlowMode(entry.flowMode || this.flowMode);
1155
- if (!shouldAcceptInboundEntry(inboundFlowMode, entry)) {
1085
+ if (!shouldAcceptInboundEntry(this.flowMode, entry)) {
1156
1086
  continue;
1157
1087
  }
1158
1088
 
@@ -1164,7 +1094,6 @@ class ArmedSeat {
1164
1094
  seatId: entry.seatId,
1165
1095
  origin: entry.origin || "unknown",
1166
1096
  phase: getRelayPhase(entry),
1167
- flowMode: inboundFlowMode,
1168
1097
  createdAt: entry.createdAt,
1169
1098
  text: payload,
1170
1099
  });
@@ -1220,8 +1149,7 @@ class ArmedSeat {
1220
1149
  return;
1221
1150
  }
1222
1151
 
1223
- const continueFlowMode = normalizeFlowMode(entry.flowMode || this.flowMode);
1224
- if (!shouldAcceptInboundEntry(continueFlowMode, entry)) {
1152
+ if (!shouldAcceptInboundEntry(this.flowMode, entry)) {
1225
1153
  continue;
1226
1154
  }
1227
1155
 
@@ -1433,13 +1361,12 @@ class ArmedSeat {
1433
1361
  }
1434
1362
 
1435
1363
  const answers = [];
1436
- const captureCommentary = this.shouldCaptureCommentary();
1437
1364
  if (detectedAgent.type === "codex") {
1438
1365
  const result = readCodexAnswers(
1439
1366
  this.liveState.sessionFile,
1440
1367
  this.liveState.offset,
1441
1368
  this.liveState.captureSinceMs,
1442
- { flowMode: captureCommentary }
1369
+ { flowMode: this.flowMode === "on" }
1443
1370
  );
1444
1371
  this.liveState.offset = result.nextOffset;
1445
1372
  answers.push(...result.answers);
@@ -1448,7 +1375,7 @@ class ArmedSeat {
1448
1375
  this.liveState.sessionFile,
1449
1376
  this.liveState.offset,
1450
1377
  this.liveState.captureSinceMs,
1451
- { flowMode: captureCommentary }
1378
+ { flowMode: this.flowMode === "on" }
1452
1379
  );
1453
1380
  this.liveState.offset = result.nextOffset;
1454
1381
  answers.push(...result.answers);
@@ -1457,7 +1384,7 @@ class ArmedSeat {
1457
1384
  this.liveState.sessionFile,
1458
1385
  this.liveState.lastMessageId,
1459
1386
  this.liveState.captureSinceMs,
1460
- { flowMode: captureCommentary }
1387
+ { flowMode: this.flowMode === "on" }
1461
1388
  );
1462
1389
  this.liveState.lastMessageId = result.lastMessageId;
1463
1390
  this.liveState.offset = result.fileSize;
@@ -1515,7 +1442,6 @@ class ArmedSeat {
1515
1442
  seatId: this.seatId,
1516
1443
  origin: entry.origin || "unknown",
1517
1444
  phase: entry.phase || "final_answer",
1518
- flowMode: this.flowMode,
1519
1445
  text: payload,
1520
1446
  createdAt: entry.createdAt || new Date().toISOString(),
1521
1447
  chainId: pendingInboundContext?.chainId || entry.chainId || entryId,
@@ -1527,35 +1453,38 @@ class ArmedSeat {
1527
1453
  buildAnswerSignaturePayload(this.sessionName, this.trustState.challenge, signedEntry),
1528
1454
  this.identity.privateKey
1529
1455
  );
1530
-
1531
1456
  appendJsonl(this.paths.eventsPath, signedEntry);
1532
- this.routeToTargets(signedEntry);
1457
+ this.forwardContinuation(signedEntry);
1533
1458
  this.rememberEmittedAnswer(answerKey);
1459
+
1534
1460
  this.log(`[${this.seatId}] ${previewText(payload)}`);
1535
1461
  }
1536
1462
 
1537
- routeToTargets(signedEntry) {
1538
- if (this.continueTargets.length === 0) {
1539
- return;
1463
+ forwardContinuation(signedEntry) {
1464
+ // Route to legacy single continueSeatId if set
1465
+ if (this.continueSeatId) {
1466
+ const target = this.findContinuationTarget();
1467
+ if (!target) {
1468
+ this.log(`[${this.seatId}] continue ${this.continueSeatId} unavailable`);
1469
+ return;
1470
+ }
1471
+
1472
+ const continuationEntry = buildContinuationEntry(this.sessionName, target.seatId, signedEntry);
1473
+ appendJsonl(target.paths.continuePath, continuationEntry);
1474
+ this.log(`[${this.seatId} => ${target.seatId}] ${previewText(continuationEntry.text)}`);
1540
1475
  }
1541
1476
 
1477
+ // Route to all continueTargets with per-target flow modes
1542
1478
  for (const targetEntry of this.continueTargets) {
1543
- if (!shouldAcceptInboundEntry(targetEntry.flowMode, signedEntry)) {
1544
- continue;
1545
- }
1546
-
1547
1479
  const target = this.findContinuationTarget(targetEntry.targetSeatId);
1548
1480
  if (!target) {
1549
1481
  this.log(`[${this.seatId}] target ${targetEntry.targetSeatId} unavailable`);
1550
1482
  continue;
1551
1483
  }
1552
1484
 
1553
- const continuationEntry = buildContinuationEntry(this.sessionName, target.seatId, {
1554
- ...signedEntry,
1555
- flowMode: targetEntry.flowMode,
1556
- });
1485
+ const continuationEntry = buildContinuationEntry(this.sessionName, target.seatId, signedEntry);
1557
1486
  appendJsonl(target.paths.continuePath, continuationEntry);
1558
- this.log(`[${this.seatId} => ${target.seatId} ${targetEntry.flowMode}] ${previewText(continuationEntry.text)}`);
1487
+ this.log(`[${this.seatId} => ${target.seatId} (${targetEntry.flowMode})] ${previewText(continuationEntry.text)}`);
1559
1488
  }
1560
1489
  }
1561
1490
 
@@ -1605,10 +1534,12 @@ class ArmedSeat {
1605
1534
  this.log(`${BRAND} seat ${this.seatId} armed for ${this.sessionName}.`);
1606
1535
  this.log("Use this shell normally. Codex, Claude, and Gemini relay automatically from their local session logs.");
1607
1536
  this.log(`Seat ${this.seatId} relay mode is flow ${this.flowMode}.`);
1537
+ if (this.continueSeatId) {
1538
+ this.log(`Seat ${this.seatId} continues to seat ${this.continueSeatId}.`);
1539
+ }
1608
1540
  if (this.continueTargets.length > 0) {
1609
- this.log(
1610
- `Seat ${this.seatId} continues to ${this.continueTargets.map((target) => `seat ${target.targetSeatId} (${target.flowMode})`).join(", ")}.`
1611
- );
1541
+ const targets = this.continueTargets.map((t) => `${t.targetSeatId} (${t.flowMode})`).join(", ");
1542
+ this.log(`Seat ${this.seatId} links to ${targets}.`);
1612
1543
  }
1613
1544
  if (isAnchorSeat(this.seatId)) {
1614
1545
  this.log(`Seat ${this.seatId} generated the session key and is waiting for seat ${this.partnerSeatId} to sign it.`);
@@ -1709,17 +1640,10 @@ function buildSeatReport(sessionName, seatId) {
1709
1640
 
1710
1641
  return {
1711
1642
  seatId,
1712
- partnerSeatId: status?.partnerSeatId || meta?.partnerSeatId || getPartnerSeatId(seatId),
1713
1643
  state: wrapperLive ? status?.state || "running" : "orphaned_child",
1714
1644
  flowMode: status?.flowMode || meta?.flowMode || "off",
1715
- continueTargets: normalizeContinueTargets(
1716
- status?.continueTargets || meta?.continueTargets || (
1717
- (status?.continueSeatId || meta?.continueSeatId)
1718
- ? [{ targetSeatId: status?.continueSeatId || meta?.continueSeatId, flowMode: status?.flowMode || meta?.flowMode || "off" }]
1719
- : []
1720
- ),
1721
- status?.flowMode || meta?.flowMode || "off"
1722
- ),
1645
+ continueSeatId: status?.continueSeatId || meta?.continueSeatId || null,
1646
+ continueTargets: status?.continueTargets || meta?.continueTargets || [],
1723
1647
  wrapperPid,
1724
1648
  childPid,
1725
1649
  wrapperLive,
@@ -1795,13 +1719,6 @@ function stopAllSessions() {
1795
1719
  signalPid(seat.wrapperPid, "SIGTERM");
1796
1720
  }
1797
1721
  }
1798
-
1799
- // Clean up session directory so stale data doesn't bleed into next session.
1800
- try {
1801
- fs.rmSync(sessionPaths.dir, { recursive: true, force: true });
1802
- } catch {
1803
- // Best-effort cleanup.
1804
- }
1805
1722
  }
1806
1723
 
1807
1724
  return {
package/src/util.js CHANGED
@@ -9,16 +9,7 @@ const fs = require("node:fs");
9
9
  const os = require("node:os");
10
10
  const path = require("node:path");
11
11
 
12
- function getBrand() {
13
- try {
14
- const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, "../package.json"), "utf8"));
15
- return `🔌Muuuuse v${pkg.version}`;
16
- } catch {
17
- return "🔌Muuuuse";
18
- }
19
- }
20
-
21
- const BRAND = getBrand();
12
+ const BRAND = "🔌Muuuuse";
22
13
  const POLL_MS = 220;
23
14
  const MAX_RELAY_CHARS = 4000;
24
15
  const SESSION_MATCH_WINDOW_MS = 5 * 60 * 1000;
@@ -291,29 +282,31 @@ function listSessionNames() {
291
282
 
292
283
  function usage() {
293
284
  return [
294
- `${BRAND} arms terminals with flexible routing to any targets - pure message relay graph.`,
285
+ `${BRAND} arms regular terminals in isolated odd/even pairs and relays assistant output between each pair.`,
295
286
  "",
296
287
  "Usage:",
297
- " muuuuse",
298
288
  " muuuuse 1",
299
- " muuuuse 1 link 2 flow off",
300
- " muuuuse 1 link 2 flow on",
301
- " muuuuse 1 link 2 flow on 3 flow off",
289
+ " muuuuse 1 flow on",
290
+ " muuuuse 1 flow off",
291
+ " muuuuse 1 flow on continue 3",
302
292
  " muuuuse 2",
293
+ " muuuuse 2 flow on",
294
+ " muuuuse 2 flow off",
295
+ " muuuuse 2 flow on continue 3",
303
296
  " muuuuse 3",
304
297
  " muuuuse 4",
305
- " muuuuse 4 link 3 flow off 1 flow on",
298
+ " muuuuse 4 flow on continue 1",
306
299
  " muuuuse stop",
307
300
  " muuuuse status",
308
301
  "",
309
302
  "Flow:",
310
- " 1. Run `muuuuse 1` in terminal one, then `muuuuse 2` in terminal two.",
311
- " 2. Bare seats default to final-only pair relay.",
303
+ " 1. Run `muuuuse 1` in terminal one.",
304
+ " 2. Run `muuuuse 2` in terminal two.",
312
305
  " 3. The odd seat generates the session key and the matching even seat signs it automatically.",
313
306
  " 4. Additional pairs work the same way: `3/4`, `5/6`, `7/8`...",
314
- " 5. Use `link <seat> flow on [<seat> flow off ...]` to set each outbound route.",
315
- " 6. Include the odd/even partner in `link` to set normal pair flow.",
316
- " 7. Any extra linked seats receive routed copies with their own flow mode.",
307
+ " 5. Optional: arm each seat with `flow on` or `flow off`.",
308
+ " 6. Optional: add `continue <seat>` to forward that seat's relayed output into another armed seat.",
309
+ " 7. Use those armed shells normally.",
317
310
  " 8. `flow off` sends final answers only. `flow on` keeps assistant commentary bouncing.",
318
311
  " 9. Run `muuuuse status` or `muuuuse stop` from any shell.",
319
312
  "",