muuuuse 2.3.2 → 2.3.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
@@ -15,13 +15,16 @@ It does one job:
15
15
  The whole surface is:
16
16
 
17
17
  ```bash
18
+ muuuuse
18
19
  muuuuse 1
19
20
  muuuuse 1 flow on
20
21
  muuuuse 1 flow off
21
22
  muuuuse 1 flow off continue 5
23
+ muuuuse 1 flow on continue 3 5 6
22
24
  muuuuse 2
23
25
  muuuuse 2 flow off
24
26
  muuuuse 2 flow on continue 3
27
+ muuuuse 2 flow on continue 3 5 6
25
28
  muuuuse 3
26
29
  muuuuse 3 flow on
27
30
  muuuuse 4
@@ -48,7 +51,7 @@ Now both shells are armed. `muuuuse 1` generates the session key, `muuuuse 2` si
48
51
 
49
52
  `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.
50
53
 
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.
54
+ `continue <seat> [<seat> ...]` forwards that seat's relayed output into one or more additional armed seats without changing the signed odd/even pair law. This lets you build local loops like `1 -> 2 -> 3 -> 4 -> 1`, split chains like `2 -> 3` and `2 -> 5`, or wider local meshes while every adjacent pair still keeps its own session keypair.
52
55
 
53
56
  If you want Codex in one and Gemini in the other, start them inside the armed shells:
54
57
 
@@ -78,7 +81,7 @@ muuuuse stop
78
81
 
79
82
  - state lives under `~/.muuuuse`
80
83
  - 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
84
+ - `continue <seat> [<seat> ...]` is a separate local forwarding lane and can target any armed seat numbers
82
85
  - supported relay detection is built for Codex, Claude, and Gemini
83
86
 
84
87
  ## Install
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muuuuse",
3
- "version": "2.3.2",
3
+ "version": "2.3.4",
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
@@ -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, continueSeatIds } = parseSeatOptions(command, argv.slice(1));
60
60
  const seat = new ArmedSeat({
61
61
  cwd: process.cwd(),
62
+ continueSeatIds,
62
63
  continueSeatId,
63
64
  flowMode,
64
65
  seatId,
@@ -83,8 +84,11 @@ 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 continueSeatIds = Array.isArray(seat.continueSeatIds)
88
+ ? seat.continueSeatIds
89
+ : (seat.continueSeatId ? [seat.continueSeatId] : []);
90
+ if (continueSeatIds.length > 0) {
91
+ bits.push(`continue ${continueSeatIds.join(",")}`);
88
92
  }
89
93
  if (seat.trust) {
90
94
  bits.push(`trust ${seat.trust}`);
@@ -105,12 +109,14 @@ function renderSeatStatus(seat) {
105
109
 
106
110
  function parseSeatOptions(command, args) {
107
111
  let flowMode = "off";
108
- let continueSeatId = null;
112
+ const continueSeatIds = [];
113
+ let sawContinue = false;
114
+ let index = 0;
109
115
 
110
- for (let index = 0; index < args.length;) {
116
+ while (index < args.length) {
111
117
  const token = String(args[index] || "").trim().toLowerCase();
112
118
 
113
- if (token === "flow") {
119
+ if (token === "flow" && !sawContinue) {
114
120
  const flowToken = String(args[index + 1] || "").trim().toLowerCase();
115
121
  if (flowToken === "on" || flowToken === "off") {
116
122
  flowMode = flowToken;
@@ -121,10 +127,22 @@ function parseSeatOptions(command, args) {
121
127
  }
122
128
 
123
129
  if (token === "continue") {
124
- const targetSeatId = normalizeSeatId(args[index + 1]);
125
- if (targetSeatId) {
126
- continueSeatId = targetSeatId;
127
- index += 2;
130
+ sawContinue = true;
131
+ let consumedTargets = 0;
132
+ index += 1;
133
+
134
+ while (index < args.length) {
135
+ const targetSeatId = normalizeSeatId(args[index]);
136
+ if (!targetSeatId) {
137
+ break;
138
+ }
139
+
140
+ continueSeatIds.push(targetSeatId);
141
+ consumedTargets += 1;
142
+ index += 1;
143
+ }
144
+
145
+ if (consumedTargets > 0) {
128
146
  continue;
129
147
  }
130
148
  break;
@@ -133,27 +151,19 @@ function parseSeatOptions(command, args) {
133
151
  break;
134
152
  }
135
153
 
136
- if (args.length === 0 || (flowMode || continueSeatId !== null) && consumedAllArgs(args, flowMode, continueSeatId)) {
137
- return { flowMode, continueSeatId };
154
+ if (index === args.length) {
155
+ return {
156
+ flowMode,
157
+ continueSeatId: continueSeatIds[0] || null,
158
+ continueSeatIds,
159
+ };
138
160
  }
139
161
 
140
162
  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.`
163
+ `\`muuuuse ${command}\` accepts no extra arguments, \`flow on\` / \`flow off\`, optional \`continue <seat> [<seat> ...]\`, or both in sequence. Run it directly in the terminal you want to arm.`
142
164
  );
143
165
  }
144
166
 
145
- function consumedAllArgs(args, flowMode, continueSeatId) {
146
- const expected = [];
147
- if (flowMode !== "off" || args.includes("flow")) {
148
- expected.push("flow", flowMode);
149
- }
150
- if (continueSeatId !== null || args.includes("continue")) {
151
- expected.push("continue", String(continueSeatId));
152
- }
153
- return expected.length === args.length &&
154
- expected.every((value, index) => String(args[index]).trim().toLowerCase() === String(value).trim().toLowerCase());
155
- }
156
-
157
167
  module.exports = {
158
168
  main,
159
169
  };
package/src/runtime.js CHANGED
@@ -56,6 +56,32 @@ const CHILD_ENV_DROP_KEYS = [
56
56
  "CODEX_THREAD_ID",
57
57
  ];
58
58
 
59
+ function bestEffortEnableChildEcho(child) {
60
+ const ptsName = String(child?.ptsName || "").trim();
61
+ if (!ptsName || process.platform === "win32") {
62
+ return;
63
+ }
64
+
65
+ try {
66
+ execFileSync("stty", [
67
+ "-F",
68
+ ptsName,
69
+ "echo",
70
+ "icanon",
71
+ "isig",
72
+ "iexten",
73
+ "echoe",
74
+ "echok",
75
+ "echoke",
76
+ "echoctl",
77
+ ], {
78
+ stdio: "ignore",
79
+ });
80
+ } catch {
81
+ // Best effort only. The shell or child app may later change its own tty mode.
82
+ }
83
+ }
84
+
59
85
  function normalizeFlowMode(flowMode) {
60
86
  return String(flowMode || "").trim().toLowerCase() === "on" ? "on" : "off";
61
87
  }
@@ -65,6 +91,36 @@ function normalizeContinueSeatId(value) {
65
91
  return seatId || null;
66
92
  }
67
93
 
94
+ function normalizeContinueSeatIds(value) {
95
+ const values = Array.isArray(value) ? value : [value];
96
+ const seen = new Set();
97
+ const seatIds = [];
98
+
99
+ for (const entry of values) {
100
+ const seatId = normalizeContinueSeatId(entry);
101
+ if (!seatId || seen.has(seatId)) {
102
+ continue;
103
+ }
104
+
105
+ seen.add(seatId);
106
+ seatIds.push(seatId);
107
+ }
108
+
109
+ return seatIds;
110
+ }
111
+
112
+ function resolveContinueSeatConfig(continueSeatIds, continueSeatId) {
113
+ if (Array.isArray(continueSeatIds) && continueSeatIds.length > 0) {
114
+ return continueSeatIds;
115
+ }
116
+
117
+ if (continueSeatIds !== undefined && continueSeatIds !== null && !Array.isArray(continueSeatIds)) {
118
+ return continueSeatIds;
119
+ }
120
+
121
+ return continueSeatId;
122
+ }
123
+
68
124
  function resolveShell() {
69
125
  const shell = String(process.env.SHELL || "").trim();
70
126
  return shell || "/bin/bash";
@@ -587,9 +643,12 @@ class ArmedSeat {
587
643
  this.partnerSeatId = getPartnerSeatId(options.seatId);
588
644
  this.anchorSeatId = isAnchorSeat(options.seatId) ? options.seatId : this.partnerSeatId;
589
645
  this.flowMode = normalizeFlowMode(options.flowMode);
590
- this.continueSeatId = normalizeContinueSeatId(options.continueSeatId);
646
+ this.continueSeatIds = normalizeContinueSeatIds(
647
+ resolveContinueSeatConfig(options.continueSeatIds, options.continueSeatId)
648
+ );
649
+ this.continueSeatId = this.continueSeatIds[0] || null;
591
650
  this.cwd = normalizeWorkingPath(options.cwd);
592
- if (this.continueSeatId === this.seatId) {
651
+ if (this.continueSeatIds.includes(this.seatId)) {
593
652
  throw new Error(`\`muuuuse ${this.seatId}\` cannot continue to itself.`);
594
653
  }
595
654
  this.sessionName = resolveSessionName(this.cwd, this.seatId);
@@ -665,6 +724,7 @@ class ArmedSeat {
665
724
  partnerSeatId: this.partnerSeatId,
666
725
  sessionName: this.sessionName,
667
726
  flowMode: this.flowMode,
727
+ continueSeatIds: this.continueSeatIds,
668
728
  continueSeatId: this.continueSeatId,
669
729
  cwd: this.cwd,
670
730
  pid: process.pid,
@@ -681,6 +741,7 @@ class ArmedSeat {
681
741
  partnerSeatId: this.partnerSeatId,
682
742
  sessionName: this.sessionName,
683
743
  flowMode: this.flowMode,
744
+ continueSeatIds: this.continueSeatIds,
684
745
  continueSeatId: this.continueSeatId,
685
746
  cwd: this.cwd,
686
747
  pid: process.pid,
@@ -874,6 +935,7 @@ class ArmedSeat {
874
935
  env: childEnv,
875
936
  name: childEnv.TERM,
876
937
  });
938
+ bestEffortEnableChildEcho(this.child);
877
939
 
878
940
  this.childPid = this.child.pid;
879
941
  this.writeMeta();
@@ -1026,18 +1088,19 @@ class ArmedSeat {
1026
1088
  return Number.isFinite(requestedAtMs) && requestedAtMs > this.startedAtMs;
1027
1089
  }
1028
1090
 
1029
- findContinuationTarget() {
1030
- if (!this.continueSeatId) {
1091
+ findContinuationTarget(targetSeatId) {
1092
+ const normalizedTargetSeatId = normalizeContinueSeatId(targetSeatId);
1093
+ if (!normalizedTargetSeatId) {
1031
1094
  return null;
1032
1095
  }
1033
1096
 
1034
1097
  const candidates = listSessionNames()
1035
1098
  .map((sessionName) => {
1036
- if (!getSeatDirIfExists(sessionName, this.continueSeatId)) {
1099
+ if (!getSeatDirIfExists(sessionName, normalizedTargetSeatId)) {
1037
1100
  return null;
1038
1101
  }
1039
1102
 
1040
- const seat = buildSeatReport(sessionName, this.continueSeatId);
1103
+ const seat = buildSeatReport(sessionName, normalizedTargetSeatId);
1041
1104
  if (!seat || !matchesWorkingPath(seat.cwd, this.cwd)) {
1042
1105
  return null;
1043
1106
  }
@@ -1457,19 +1520,21 @@ class ArmedSeat {
1457
1520
  }
1458
1521
 
1459
1522
  forwardContinuation(signedEntry) {
1460
- if (!this.continueSeatId) {
1523
+ if (this.continueSeatIds.length === 0) {
1461
1524
  return;
1462
1525
  }
1463
1526
 
1464
- const target = this.findContinuationTarget();
1465
- if (!target) {
1466
- this.log(`[${this.seatId}] continue ${this.continueSeatId} unavailable`);
1467
- return;
1468
- }
1527
+ for (const continueSeatId of this.continueSeatIds) {
1528
+ const target = this.findContinuationTarget(continueSeatId);
1529
+ if (!target) {
1530
+ this.log(`[${this.seatId}] continue ${continueSeatId} unavailable`);
1531
+ continue;
1532
+ }
1469
1533
 
1470
- const continuationEntry = buildContinuationEntry(this.sessionName, target.seatId, signedEntry);
1471
- appendJsonl(target.paths.continuePath, continuationEntry);
1472
- this.log(`[${this.seatId} => ${target.seatId}] ${previewText(continuationEntry.text)}`);
1534
+ const continuationEntry = buildContinuationEntry(this.sessionName, target.seatId, signedEntry);
1535
+ appendJsonl(target.paths.continuePath, continuationEntry);
1536
+ this.log(`[${this.seatId} => ${target.seatId}] ${previewText(continuationEntry.text)}`);
1537
+ }
1473
1538
  }
1474
1539
 
1475
1540
  async tick() {
@@ -1518,8 +1583,9 @@ class ArmedSeat {
1518
1583
  this.log(`${BRAND} seat ${this.seatId} armed for ${this.sessionName}.`);
1519
1584
  this.log("Use this shell normally. Codex, Claude, and Gemini relay automatically from their local session logs.");
1520
1585
  this.log(`Seat ${this.seatId} relay mode is flow ${this.flowMode}.`);
1521
- if (this.continueSeatId) {
1522
- this.log(`Seat ${this.seatId} continues to seat ${this.continueSeatId}.`);
1586
+ if (this.continueSeatIds.length > 0) {
1587
+ const targetLabel = this.continueSeatIds.length === 1 ? "seat" : "seats";
1588
+ this.log(`Seat ${this.seatId} continues to ${targetLabel} ${this.continueSeatIds.join(", ")}.`);
1523
1589
  }
1524
1590
  if (isAnchorSeat(this.seatId)) {
1525
1591
  this.log(`Seat ${this.seatId} generated the session key and is waiting for seat ${this.partnerSeatId} to sign it.`);
@@ -1618,11 +1684,19 @@ function buildSeatReport(sessionName, seatId) {
1618
1684
  return null;
1619
1685
  }
1620
1686
 
1687
+ const continueSeatIds = normalizeContinueSeatIds(
1688
+ resolveContinueSeatConfig(
1689
+ status?.continueSeatIds ?? meta?.continueSeatIds,
1690
+ status?.continueSeatId ?? meta?.continueSeatId
1691
+ )
1692
+ );
1693
+
1621
1694
  return {
1622
1695
  seatId,
1623
1696
  state: wrapperLive ? status?.state || "running" : "orphaned_child",
1624
1697
  flowMode: status?.flowMode || meta?.flowMode || "off",
1625
- continueSeatId: status?.continueSeatId || meta?.continueSeatId || null,
1698
+ continueSeatId: continueSeatIds[0] || null,
1699
+ continueSeatIds,
1626
1700
  wrapperPid,
1627
1701
  childPid,
1628
1702
  wrapperLive,
package/src/util.js CHANGED
@@ -284,20 +284,27 @@ function usage() {
284
284
  return [
285
285
  `${BRAND} arms regular terminals in isolated odd/even pairs and relays assistant output between each pair.`,
286
286
  "",
287
- "Usage:",
288
- " muuuuse 1",
289
- " muuuuse 1 flow on",
290
- " muuuuse 1 flow off",
291
- " muuuuse 1 flow on continue 3",
292
- " muuuuse 2",
293
- " muuuuse 2 flow on",
294
- " muuuuse 2 flow off",
295
- " muuuuse 2 flow on continue 3",
296
- " muuuuse 3",
297
- " muuuuse 4",
298
- " muuuuse 4 flow on continue 1",
299
- " muuuuse stop",
287
+ "Command List:",
288
+ " muuuuse",
289
+ " Show this command list.",
290
+ "",
291
+ " muuuuse <seat>",
292
+ " Arm a seat and keep its normal odd/even partner relay behavior.",
293
+ "",
294
+ " muuuuse <seat> flow on",
295
+ " Relay commentary and final answers into that armed shell.",
296
+ "",
297
+ " muuuuse <seat> flow off",
298
+ " Relay final answers only into that armed shell.",
299
+ "",
300
+ " muuuuse <seat> flow on continue <seat> [<seat> ...]",
301
+ " Fan that seat's relayed output into one or more additional armed seats.",
302
+ "",
300
303
  " muuuuse status",
304
+ " Show all armed seats, flow mode, pair trust, and continuation targets.",
305
+ "",
306
+ " muuuuse stop",
307
+ " Stop every armed seat in the current cwd.",
301
308
  "",
302
309
  "Flow:",
303
310
  " 1. Run `muuuuse 1` in terminal one.",
@@ -305,7 +312,7 @@ function usage() {
305
312
  " 3. The odd seat generates the session key and the matching even seat signs it automatically.",
306
313
  " 4. Additional pairs work the same way: `3/4`, `5/6`, `7/8`...",
307
314
  " 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.",
315
+ " 6. Optional: add `continue <seat> [<seat> ...]` to forward that seat's relayed output into more armed seats.",
309
316
  " 7. Use those armed shells normally.",
310
317
  " 8. `flow off` sends final answers only. `flow on` keeps assistant commentary bouncing.",
311
318
  " 9. Run `muuuuse status` or `muuuuse stop` from any shell.",