muuuuse 2.3.3 → 2.3.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muuuuse",
3
- "version": "2.3.3",
3
+ "version": "2.3.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": {
package/src/cli.js CHANGED
@@ -1,4 +1,4 @@
1
- const { BRAND, normalizeSeatId, usage } = require("./util");
1
+ const { BRAND, getPartnerSeatId, normalizeSeatId, usage } = require("./util");
2
2
  const { ArmedSeat, getStatusReport, stopAllSessions } = require("./runtime");
3
3
 
4
4
  async function main(argv = process.argv.slice(2)) {
@@ -56,9 +56,10 @@ async function main(argv = process.argv.slice(2)) {
56
56
 
57
57
  const seatId = normalizeSeatId(command);
58
58
  if (seatId) {
59
- const { flowMode, continueSeatId } = 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
+ continueTargets,
62
63
  continueSeatId,
63
64
  flowMode,
64
65
  seatId,
@@ -83,8 +84,9 @@ function renderSeatStatus(seat) {
83
84
  if (seat.partnerLive) {
84
85
  bits.push("peer live");
85
86
  }
86
- if (seat.continueSeatId) {
87
- bits.push(`continue ${seat.continueSeatId}`);
87
+ const renderedLinks = renderLinkTargets(seat);
88
+ if (renderedLinks) {
89
+ bits.push(`link ${renderedLinks}`);
88
90
  }
89
91
  if (seat.trust) {
90
92
  bits.push(`trust ${seat.trust}`);
@@ -103,11 +105,31 @@ function renderSeatStatus(seat) {
103
105
  return output;
104
106
  }
105
107
 
108
+ function renderLinkTargets(seat) {
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);
118
+ }
119
+
120
+ return targets
121
+ .map((target) => `${target.targetSeatId}:${target.flowMode}`)
122
+ .join(", ");
123
+ }
124
+
106
125
  function parseSeatOptions(command, args) {
126
+ const seatId = normalizeSeatId(command);
107
127
  let flowMode = "off";
108
128
  let continueSeatId = null;
129
+ let continueTargets = [];
130
+ let index = 0;
109
131
 
110
- for (let index = 0; index < args.length;) {
132
+ while (index < args.length) {
111
133
  const token = String(args[index] || "").trim().toLowerCase();
112
134
 
113
135
  if (token === "flow") {
@@ -121,10 +143,23 @@ function parseSeatOptions(command, args) {
121
143
  }
122
144
 
123
145
  if (token === "continue") {
124
- const targetSeatId = normalizeSeatId(args[index + 1]);
125
- if (targetSeatId) {
126
- continueSeatId = targetSeatId;
127
- index += 2;
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;
154
+ }
155
+
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;
128
163
  continue;
129
164
  }
130
165
  break;
@@ -133,25 +168,97 @@ function parseSeatOptions(command, args) {
133
168
  break;
134
169
  }
135
170
 
136
- if (args.length === 0 || (flowMode || continueSeatId !== null) && consumedAllArgs(args, flowMode, continueSeatId)) {
137
- return { flowMode, continueSeatId };
171
+ if (index === args.length) {
172
+ return { flowMode, continueSeatId, continueTargets };
138
173
  }
139
174
 
140
175
  throw new Error(
141
- `\`muuuuse ${command}\` accepts no extra arguments, \`flow on\` / \`flow off\`, optional \`continue <seat>\`, or both in sequence. 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.`
142
177
  );
143
178
  }
144
179
 
145
- function consumedAllArgs(args, flowMode, continueSeatId) {
146
- const expected = [];
147
- if (flowMode !== "off" || args.includes("flow")) {
148
- expected.push("flow", flowMode);
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) {
193
+ const targets = [];
194
+ let consumed = 0;
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
+
220
+ while (consumed < args.length) {
221
+ const targetSeatId = normalizeSeatId(args[consumed]);
222
+ if (!targetSeatId) {
223
+ break;
224
+ }
225
+
226
+ const targetFlowMode = parseFlowModeToken(args[consumed + 1], args[consumed + 2]);
227
+ if (!targetFlowMode) {
228
+ break;
229
+ }
230
+
231
+ if (targetSeatId === partnerSeatId) {
232
+ flowMode = targetFlowMode;
233
+ } else {
234
+ upsertTarget(continueTargets, {
235
+ targetSeatId,
236
+ flowMode: targetFlowMode,
237
+ });
238
+ }
239
+
240
+ consumed += 3;
241
+ }
242
+
243
+ return { consumed, continueTargets, flowMode };
244
+ }
245
+
246
+ function parseFlowModeToken(flowToken, modeToken) {
247
+ const normalizedFlowToken = String(flowToken || "").trim().toLowerCase();
248
+ const normalizedModeToken = String(modeToken || "").trim().toLowerCase();
249
+ if (normalizedFlowToken === "flow" && (normalizedModeToken === "on" || normalizedModeToken === "off")) {
250
+ return normalizedModeToken;
149
251
  }
150
- if (continueSeatId !== null || args.includes("continue")) {
151
- expected.push("continue", String(continueSeatId));
252
+ return null;
253
+ }
254
+
255
+ function upsertTarget(targets, nextTarget) {
256
+ const existingIndex = targets.findIndex((entry) => entry.targetSeatId === nextTarget.targetSeatId);
257
+ if (existingIndex >= 0) {
258
+ targets[existingIndex] = nextTarget;
259
+ return;
152
260
  }
153
- return expected.length === args.length &&
154
- expected.every((value, index) => String(args[index]).trim().toLowerCase() === String(value).trim().toLowerCase());
261
+ targets.push(nextTarget);
155
262
  }
156
263
 
157
264
  module.exports = {
package/src/runtime.js CHANGED
@@ -91,6 +91,26 @@ function normalizeContinueSeatId(value) {
91
91
  return seatId || null;
92
92
  }
93
93
 
94
+ function normalizeContinueTargets(targets, defaultFlowMode = "off") {
95
+ const normalized = [];
96
+ const seen = new Set();
97
+
98
+ for (const entry of Array.isArray(targets) ? targets : []) {
99
+ const targetSeatId = normalizeSeatId(entry?.targetSeatId ?? entry?.seatId ?? entry);
100
+ if (!targetSeatId || seen.has(targetSeatId)) {
101
+ continue;
102
+ }
103
+
104
+ seen.add(targetSeatId);
105
+ normalized.push({
106
+ targetSeatId,
107
+ flowMode: normalizeFlowMode(entry?.flowMode ?? entry?.flow ?? defaultFlowMode),
108
+ });
109
+ }
110
+
111
+ return normalized;
112
+ }
113
+
94
114
  function resolveShell() {
95
115
  const shell = String(process.env.SHELL || "").trim();
96
116
  return shell || "/bin/bash";
@@ -471,6 +491,7 @@ function buildAnswerSignaturePayload(sessionName, challenge, entry) {
471
491
  seatId: entry.seatId,
472
492
  origin: entry.origin,
473
493
  phase: entry.phase || "final_answer",
494
+ flowMode: entry.flowMode || "off",
474
495
  createdAt: entry.createdAt,
475
496
  text: entry.text,
476
497
  });
@@ -490,6 +511,7 @@ function buildContinuationEntry(sourceSessionName, targetSeatId, entry) {
490
511
  chainId: entry.chainId,
491
512
  hop: entry.hop,
492
513
  sourceAnswerId: entry.id,
514
+ flowMode: entry.flowMode || null,
493
515
  publicKey: entry.publicKey || null,
494
516
  signature: entry.signature || null,
495
517
  };
@@ -613,9 +635,15 @@ class ArmedSeat {
613
635
  this.partnerSeatId = getPartnerSeatId(options.seatId);
614
636
  this.anchorSeatId = isAnchorSeat(options.seatId) ? options.seatId : this.partnerSeatId;
615
637
  this.flowMode = normalizeFlowMode(options.flowMode);
616
- this.continueSeatId = normalizeContinueSeatId(options.continueSeatId);
638
+ this.continueTargets = normalizeContinueTargets(
639
+ options.continueTargets || (
640
+ options.continueSeatId ? [{ targetSeatId: options.continueSeatId, flowMode: options.flowMode }] : []
641
+ ),
642
+ this.flowMode
643
+ );
644
+ this.continueSeatId = this.continueTargets[0]?.targetSeatId || normalizeContinueSeatId(options.continueSeatId);
617
645
  this.cwd = normalizeWorkingPath(options.cwd);
618
- if (this.continueSeatId === this.seatId) {
646
+ if (this.continueTargets.some((target) => target.targetSeatId === this.seatId)) {
619
647
  throw new Error(`\`muuuuse ${this.seatId}\` cannot continue to itself.`);
620
648
  }
621
649
  this.sessionName = resolveSessionName(this.cwd, this.seatId);
@@ -692,6 +720,7 @@ class ArmedSeat {
692
720
  sessionName: this.sessionName,
693
721
  flowMode: this.flowMode,
694
722
  continueSeatId: this.continueSeatId,
723
+ continueTargets: this.continueTargets,
695
724
  cwd: this.cwd,
696
725
  pid: process.pid,
697
726
  childPid: this.childPid,
@@ -708,6 +737,7 @@ class ArmedSeat {
708
737
  sessionName: this.sessionName,
709
738
  flowMode: this.flowMode,
710
739
  continueSeatId: this.continueSeatId,
740
+ continueTargets: this.continueTargets,
711
741
  cwd: this.cwd,
712
742
  pid: process.pid,
713
743
  childPid: this.childPid,
@@ -1053,18 +1083,23 @@ class ArmedSeat {
1053
1083
  return Number.isFinite(requestedAtMs) && requestedAtMs > this.startedAtMs;
1054
1084
  }
1055
1085
 
1056
- findContinuationTarget() {
1057
- if (!this.continueSeatId) {
1086
+ shouldCaptureCommentary() {
1087
+ return this.flowMode === "on" || this.continueTargets.some((target) => target.flowMode === "on");
1088
+ }
1089
+
1090
+ findContinuationTarget(targetSeatId) {
1091
+ const normalizedTargetSeatId = normalizeSeatId(targetSeatId);
1092
+ if (!normalizedTargetSeatId) {
1058
1093
  return null;
1059
1094
  }
1060
1095
 
1061
1096
  const candidates = listSessionNames()
1062
1097
  .map((sessionName) => {
1063
- if (!getSeatDirIfExists(sessionName, this.continueSeatId)) {
1098
+ if (!getSeatDirIfExists(sessionName, normalizedTargetSeatId)) {
1064
1099
  return null;
1065
1100
  }
1066
1101
 
1067
- const seat = buildSeatReport(sessionName, this.continueSeatId);
1102
+ const seat = buildSeatReport(sessionName, normalizedTargetSeatId);
1068
1103
  if (!seat || !matchesWorkingPath(seat.cwd, this.cwd)) {
1069
1104
  return null;
1070
1105
  }
@@ -1105,7 +1140,8 @@ class ArmedSeat {
1105
1140
  return;
1106
1141
  }
1107
1142
 
1108
- if (!shouldAcceptInboundEntry(this.flowMode, entry)) {
1143
+ const inboundFlowMode = normalizeFlowMode(entry.flowMode || this.flowMode);
1144
+ if (!shouldAcceptInboundEntry(inboundFlowMode, entry)) {
1109
1145
  continue;
1110
1146
  }
1111
1147
 
@@ -1117,6 +1153,7 @@ class ArmedSeat {
1117
1153
  seatId: entry.seatId,
1118
1154
  origin: entry.origin || "unknown",
1119
1155
  phase: getRelayPhase(entry),
1156
+ flowMode: inboundFlowMode,
1120
1157
  createdAt: entry.createdAt,
1121
1158
  text: payload,
1122
1159
  });
@@ -1172,7 +1209,8 @@ class ArmedSeat {
1172
1209
  return;
1173
1210
  }
1174
1211
 
1175
- if (!shouldAcceptInboundEntry(this.flowMode, entry)) {
1212
+ const continueFlowMode = normalizeFlowMode(entry.flowMode || this.flowMode);
1213
+ if (!shouldAcceptInboundEntry(continueFlowMode, entry)) {
1176
1214
  continue;
1177
1215
  }
1178
1216
 
@@ -1384,12 +1422,13 @@ class ArmedSeat {
1384
1422
  }
1385
1423
 
1386
1424
  const answers = [];
1425
+ const captureCommentary = this.shouldCaptureCommentary();
1387
1426
  if (detectedAgent.type === "codex") {
1388
1427
  const result = readCodexAnswers(
1389
1428
  this.liveState.sessionFile,
1390
1429
  this.liveState.offset,
1391
1430
  this.liveState.captureSinceMs,
1392
- { flowMode: this.flowMode === "on" }
1431
+ { flowMode: captureCommentary }
1393
1432
  );
1394
1433
  this.liveState.offset = result.nextOffset;
1395
1434
  answers.push(...result.answers);
@@ -1398,7 +1437,7 @@ class ArmedSeat {
1398
1437
  this.liveState.sessionFile,
1399
1438
  this.liveState.offset,
1400
1439
  this.liveState.captureSinceMs,
1401
- { flowMode: this.flowMode === "on" }
1440
+ { flowMode: captureCommentary }
1402
1441
  );
1403
1442
  this.liveState.offset = result.nextOffset;
1404
1443
  answers.push(...result.answers);
@@ -1407,7 +1446,7 @@ class ArmedSeat {
1407
1446
  this.liveState.sessionFile,
1408
1447
  this.liveState.lastMessageId,
1409
1448
  this.liveState.captureSinceMs,
1410
- { flowMode: this.flowMode === "on" }
1449
+ { flowMode: captureCommentary }
1411
1450
  );
1412
1451
  this.liveState.lastMessageId = result.lastMessageId;
1413
1452
  this.liveState.offset = result.fileSize;
@@ -1465,6 +1504,7 @@ class ArmedSeat {
1465
1504
  seatId: this.seatId,
1466
1505
  origin: entry.origin || "unknown",
1467
1506
  phase: entry.phase || "final_answer",
1507
+ flowMode: this.flowMode,
1468
1508
  text: payload,
1469
1509
  createdAt: entry.createdAt || new Date().toISOString(),
1470
1510
  chainId: pendingInboundContext?.chainId || entry.chainId || entryId,
@@ -1484,19 +1524,24 @@ class ArmedSeat {
1484
1524
  }
1485
1525
 
1486
1526
  forwardContinuation(signedEntry) {
1487
- if (!this.continueSeatId) {
1527
+ if (this.continueTargets.length === 0) {
1488
1528
  return;
1489
1529
  }
1490
1530
 
1491
- const target = this.findContinuationTarget();
1492
- if (!target) {
1493
- this.log(`[${this.seatId}] continue ${this.continueSeatId} unavailable`);
1494
- return;
1495
- }
1531
+ for (const targetEntry of this.continueTargets) {
1532
+ const target = this.findContinuationTarget(targetEntry.targetSeatId);
1533
+ if (!target) {
1534
+ this.log(`[${this.seatId}] continue ${targetEntry.targetSeatId} unavailable`);
1535
+ continue;
1536
+ }
1496
1537
 
1497
- const continuationEntry = buildContinuationEntry(this.sessionName, target.seatId, signedEntry);
1498
- appendJsonl(target.paths.continuePath, continuationEntry);
1499
- this.log(`[${this.seatId} => ${target.seatId}] ${previewText(continuationEntry.text)}`);
1538
+ const continuationEntry = buildContinuationEntry(this.sessionName, target.seatId, {
1539
+ ...signedEntry,
1540
+ flowMode: targetEntry.flowMode,
1541
+ });
1542
+ appendJsonl(target.paths.continuePath, continuationEntry);
1543
+ this.log(`[${this.seatId} => ${target.seatId} ${targetEntry.flowMode}] ${previewText(continuationEntry.text)}`);
1544
+ }
1500
1545
  }
1501
1546
 
1502
1547
  async tick() {
@@ -1545,8 +1590,10 @@ class ArmedSeat {
1545
1590
  this.log(`${BRAND} seat ${this.seatId} armed for ${this.sessionName}.`);
1546
1591
  this.log("Use this shell normally. Codex, Claude, and Gemini relay automatically from their local session logs.");
1547
1592
  this.log(`Seat ${this.seatId} relay mode is flow ${this.flowMode}.`);
1548
- if (this.continueSeatId) {
1549
- this.log(`Seat ${this.seatId} continues to seat ${this.continueSeatId}.`);
1593
+ if (this.continueTargets.length > 0) {
1594
+ this.log(
1595
+ `Seat ${this.seatId} continues to ${this.continueTargets.map((target) => `seat ${target.targetSeatId} (${target.flowMode})`).join(", ")}.`
1596
+ );
1550
1597
  }
1551
1598
  if (isAnchorSeat(this.seatId)) {
1552
1599
  this.log(`Seat ${this.seatId} generated the session key and is waiting for seat ${this.partnerSeatId} to sign it.`);
@@ -1647,9 +1694,18 @@ function buildSeatReport(sessionName, seatId) {
1647
1694
 
1648
1695
  return {
1649
1696
  seatId,
1697
+ partnerSeatId: status?.partnerSeatId || meta?.partnerSeatId || getPartnerSeatId(seatId),
1650
1698
  state: wrapperLive ? status?.state || "running" : "orphaned_child",
1651
1699
  flowMode: status?.flowMode || meta?.flowMode || "off",
1652
1700
  continueSeatId: status?.continueSeatId || meta?.continueSeatId || null,
1701
+ continueTargets: normalizeContinueTargets(
1702
+ status?.continueTargets || meta?.continueTargets || (
1703
+ (status?.continueSeatId || meta?.continueSeatId)
1704
+ ? [{ targetSeatId: status?.continueSeatId || meta?.continueSeatId, flowMode: status?.flowMode || meta?.flowMode || "off" }]
1705
+ : []
1706
+ ),
1707
+ status?.flowMode || meta?.flowMode || "off"
1708
+ ),
1653
1709
  wrapperPid,
1654
1710
  childPid,
1655
1711
  wrapperLive,
package/src/util.js CHANGED
@@ -285,17 +285,15 @@ function usage() {
285
285
  `${BRAND} arms regular terminals in isolated odd/even pairs and relays assistant output between each pair.`,
286
286
  "",
287
287
  "Usage:",
288
+ " muuuuse",
288
289
  " muuuuse 1",
289
- " muuuuse 1 flow on",
290
- " muuuuse 1 flow off",
291
- " muuuuse 1 flow on continue 3",
290
+ " muuuuse 1 link 2 flow on",
291
+ " muuuuse 1 link 2 flow on 3 flow off",
292
292
  " muuuuse 2",
293
- " muuuuse 2 flow on",
294
- " muuuuse 2 flow off",
295
- " muuuuse 2 flow on continue 3",
293
+ " muuuuse 2 link 1 flow on",
296
294
  " muuuuse 3",
297
295
  " muuuuse 4",
298
- " muuuuse 4 flow on continue 1",
296
+ " muuuuse 4 link 3 flow off 1 flow on",
299
297
  " muuuuse stop",
300
298
  " muuuuse status",
301
299
  "",
@@ -304,9 +302,9 @@ function usage() {
304
302
  " 2. Run `muuuuse 2` in terminal two.",
305
303
  " 3. The odd seat generates the session key and the matching even seat signs it automatically.",
306
304
  " 4. Additional pairs work the same way: `3/4`, `5/6`, `7/8`...",
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.",
305
+ " 5. Use `link <seat> flow on [<seat> flow off ...]` to set each outbound path.",
306
+ " 6. Linking the odd/even partner sets the normal pair flow. Extra linked seats become continuations.",
307
+ " 7. Legacy `flow on` / `flow off` and `continue` still parse, but `link` is the main command shape.",
310
308
  " 8. `flow off` sends final answers only. `flow on` keeps assistant commentary bouncing.",
311
309
  " 9. Run `muuuuse status` or `muuuuse stop` from any shell.",
312
310
  "",