muuuuse 2.3.5 → 4.0.0

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.5",
3
+ "version": "4.0.0",
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/agents.js CHANGED
@@ -555,8 +555,7 @@ function selectClaudeSessionFile(currentPath, processStartedAtMs, options = {})
555
555
  return selectSessionCandidatePath(candidates, currentPath, processStartedAtMs);
556
556
  }
557
557
 
558
- function extractClaudeAssistantText(content, options = {}) {
559
- const flowMode = options.flowMode === true;
558
+ function extractClaudeAssistantText(content) {
560
559
  if (!Array.isArray(content)) {
561
560
  return "";
562
561
  }
@@ -569,7 +568,23 @@ function extractClaudeAssistantText(content, options = {}) {
569
568
  if (item.type === "text" && typeof item.text === "string") {
570
569
  return [item.text.trim()];
571
570
  }
572
- if (flowMode && item.type === "thinking" && typeof item.thinking === "string") {
571
+ return [];
572
+ })
573
+ .filter((text) => text.length > 0)
574
+ .join("\n");
575
+ }
576
+
577
+ function extractClaudeThinkingText(content) {
578
+ if (!Array.isArray(content)) {
579
+ return "";
580
+ }
581
+
582
+ return content
583
+ .flatMap((item) => {
584
+ if (!item || typeof item !== "object") {
585
+ return [];
586
+ }
587
+ if (item.type === "thinking" && typeof item.thinking === "string") {
573
588
  return [item.thinking.trim()];
574
589
  }
575
590
  return [];
@@ -586,28 +601,46 @@ function parseClaudeAssistantLine(line, options = {}) {
586
601
  return null;
587
602
  }
588
603
 
589
- if (!flowMode && entry.message?.stop_reason !== "end_turn") {
604
+ const isFinal = entry.message?.stop_reason === "end_turn";
605
+ if (!flowMode && !isFinal) {
590
606
  return null;
591
607
  }
592
608
 
593
- const text = sanitizeRelayText(extractClaudeAssistantText(entry.message.content, options));
594
- if (!text) {
595
- return null;
609
+ const id = entry.uuid || entry.message.id || hashText(line);
610
+ const timestamp = entry.timestamp || new Date().toISOString();
611
+ const results = [];
612
+
613
+ if (flowMode) {
614
+ const thinking = sanitizeRelayText(extractClaudeThinkingText(entry.message.content));
615
+ if (thinking) {
616
+ results.push({
617
+ id: `${id}-thinking`,
618
+ text: thinking,
619
+ phase: "commentary",
620
+ timestamp,
621
+ });
622
+ }
596
623
  }
597
624
 
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
- };
625
+ const text = sanitizeRelayText(extractClaudeAssistantText(entry.message.content));
626
+ if (text) {
627
+ results.push({
628
+ id,
629
+ text,
630
+ phase: isFinal ? "final_answer" : "commentary",
631
+ timestamp,
632
+ });
633
+ }
634
+
635
+ return results.length > 0 ? results : null;
604
636
  } catch {
605
637
  return null;
606
638
  }
607
639
  }
608
640
 
609
641
  function parseClaudeFinalLine(line) {
610
- return parseClaudeAssistantLine(line, { flowMode: false });
642
+ const results = parseClaudeAssistantLine(line, { flowMode: false });
643
+ return Array.isArray(results) ? results[0] || null : results;
611
644
  }
612
645
 
613
646
  function readClaudeAnswers(filePath, offset, sinceMs = null, options = {}) {
@@ -616,8 +649,10 @@ function readClaudeAnswers(filePath, offset, sinceMs = null, options = {}) {
616
649
  .split("\n")
617
650
  .map((line) => line.trim())
618
651
  .filter((line) => line.length > 0)
619
- .map((line) => parseClaudeAssistantLine(line, options))
620
- .filter((entry) => entry !== null)
652
+ .flatMap((line) => {
653
+ const result = parseClaudeAssistantLine(line, options);
654
+ return Array.isArray(result) ? result : result ? [result] : [];
655
+ })
621
656
  .filter((entry) => isAnswerNewEnough(entry, sinceMs));
622
657
 
623
658
  return { nextOffset, answers };
package/src/cli.js CHANGED
@@ -56,11 +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, continueTargets } = parseSeatOptions(command, argv.slice(1));
59
+ const { flowMode, continueTargets } = parseSeatOptions(command, argv.slice(1));
60
60
  const seat = new ArmedSeat({
61
61
  cwd: process.cwd(),
62
62
  continueTargets,
63
- continueSeatId,
64
63
  flowMode,
65
64
  seatId,
66
65
  });
@@ -116,101 +115,55 @@ function renderLinkTargets(seat) {
116
115
  for (const target of Array.isArray(seat.continueTargets) ? seat.continueTargets : []) {
117
116
  targets.push(target);
118
117
  }
119
-
120
- return targets
121
- .map((target) => `${target.targetSeatId}:${target.flowMode}`)
122
- .join(", ");
118
+ if (targets.length === 0) {
119
+ return "";
120
+ }
121
+ return targets.map((target) => `${target.targetSeatId}:${target.flowMode}`).join(", ");
123
122
  }
124
123
 
125
124
  function parseSeatOptions(command, args) {
126
125
  const seatId = normalizeSeatId(command);
127
126
  let flowMode = "off";
128
- let continueSeatId = null;
129
127
  let continueTargets = [];
130
- let index = 0;
131
-
132
- while (index < args.length) {
133
- const token = String(args[index] || "").trim().toLowerCase();
128
+ if (args.length === 0) {
129
+ return { flowMode, continueTargets };
130
+ }
134
131
 
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;
132
+ if (String(args[0] || "").trim().toLowerCase() === "link") {
133
+ const parsedLinks = parseLinkTargets(args.slice(1), seatId, flowMode);
134
+ if (parsedLinks.consumed === args.length - 1 && parsedLinks.consumed > 0) {
135
+ return {
136
+ flowMode: parsedLinks.flowMode,
137
+ continueTargets: parsedLinks.continueTargets,
138
+ };
143
139
  }
144
-
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;
140
+ } else {
141
+ let index = 0;
142
+
143
+ const flowToken = String(args[index] || "").trim().toLowerCase();
144
+ const flowModeToken = String(args[index + 1] || "").trim().toLowerCase();
145
+ if (flowToken === "flow" && (flowModeToken === "on" || flowModeToken === "off")) {
146
+ flowMode = flowModeToken;
147
+ index += 2;
154
148
  }
155
149
 
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
+ const continueToken = String(args[index] || "").trim().toLowerCase();
151
+ const targetSeatId = normalizeSeatId(args[index + 1]);
152
+ if (continueToken === "continue" && targetSeatId) {
153
+ continueTargets = [{ targetSeatId, flowMode }];
154
+ index += 2;
166
155
  }
167
156
 
168
- break;
169
- }
170
-
171
- if (index === args.length) {
172
- return { flowMode, continueSeatId, continueTargets };
157
+ if (index === args.length) {
158
+ return { flowMode, continueTargets };
159
+ }
173
160
  }
174
161
 
175
162
  throw new Error(
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.`
163
+ `\`muuuuse ${command}\` accepts no extra arguments or \`link <seat> flow on [<seat> flow off ...]\`. Run it directly in the terminal you want to arm.`
177
164
  );
178
165
  }
179
166
 
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
167
  function parseLinkTargets(args, seatId, defaultFlowMode) {
215
168
  const partnerSeatId = seatId ? getPartnerSeatId(seatId) : null;
216
169
  const continueTargets = [];
@@ -263,4 +216,5 @@ function upsertTarget(targets, nextTarget) {
263
216
 
264
217
  module.exports = {
265
218
  main,
219
+ parseSeatOptions,
266
220
  };
package/src/runtime.js CHANGED
@@ -42,6 +42,7 @@ 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;
45
46
  const MIRROR_SUPPRESSION_WINDOW_MS = 30 * 1000;
46
47
  const PENDING_RELAY_CONTEXT_TTL_MS = 2 * 60 * 1000;
47
48
  const EMITTED_ANSWER_TTL_MS = 5 * 60 * 1000;
@@ -620,6 +621,14 @@ async function sendTextAndEnter(child, text, shouldAbort = () => false) {
620
621
  return false;
621
622
  }
622
623
 
624
+ // Some TUIs treat fast, chunked relay typing as paste input and suppress an
625
+ // immediate Enter. A short settle delay keeps submit behavior reliable.
626
+ await sleep(TYPE_SUBMIT_DELAY_MS);
627
+
628
+ if (shouldAbort() || !child) {
629
+ return false;
630
+ }
631
+
623
632
  try {
624
633
  child.write("\r");
625
634
  } catch {
@@ -641,7 +650,6 @@ class ArmedSeat {
641
650
  ),
642
651
  this.flowMode
643
652
  );
644
- this.continueSeatId = this.continueTargets[0]?.targetSeatId || normalizeContinueSeatId(options.continueSeatId);
645
653
  this.cwd = normalizeWorkingPath(options.cwd);
646
654
  if (this.continueTargets.some((target) => target.targetSeatId === this.seatId)) {
647
655
  throw new Error(`\`muuuuse ${this.seatId}\` cannot continue to itself.`);
@@ -719,7 +727,6 @@ class ArmedSeat {
719
727
  partnerSeatId: this.partnerSeatId,
720
728
  sessionName: this.sessionName,
721
729
  flowMode: this.flowMode,
722
- continueSeatId: this.continueSeatId,
723
730
  continueTargets: this.continueTargets,
724
731
  cwd: this.cwd,
725
732
  pid: process.pid,
@@ -736,7 +743,6 @@ class ArmedSeat {
736
743
  partnerSeatId: this.partnerSeatId,
737
744
  sessionName: this.sessionName,
738
745
  flowMode: this.flowMode,
739
- continueSeatId: this.continueSeatId,
740
746
  continueTargets: this.continueTargets,
741
747
  cwd: this.cwd,
742
748
  pid: process.pid,
@@ -1516,11 +1522,16 @@ class ArmedSeat {
1516
1522
  buildAnswerSignaturePayload(this.sessionName, this.trustState.challenge, signedEntry),
1517
1523
  this.identity.privateKey
1518
1524
  );
1519
- appendJsonl(this.paths.eventsPath, signedEntry);
1525
+
1526
+ // Only write to partner events if local flow mode accepts this phase.
1527
+ // Commentary is gated here; continue targets filter independently.
1528
+ if (shouldAcceptInboundEntry(this.flowMode, signedEntry)) {
1529
+ appendJsonl(this.paths.eventsPath, signedEntry);
1530
+ this.log(`[${this.seatId}] ${previewText(payload)}`);
1531
+ }
1532
+
1520
1533
  this.forwardContinuation(signedEntry);
1521
1534
  this.rememberEmittedAnswer(answerKey);
1522
-
1523
- this.log(`[${this.seatId}] ${previewText(payload)}`);
1524
1535
  }
1525
1536
 
1526
1537
  forwardContinuation(signedEntry) {
@@ -1529,6 +1540,10 @@ class ArmedSeat {
1529
1540
  }
1530
1541
 
1531
1542
  for (const targetEntry of this.continueTargets) {
1543
+ if (!shouldAcceptInboundEntry(targetEntry.flowMode, signedEntry)) {
1544
+ continue;
1545
+ }
1546
+
1532
1547
  const target = this.findContinuationTarget(targetEntry.targetSeatId);
1533
1548
  if (!target) {
1534
1549
  this.log(`[${this.seatId}] continue ${targetEntry.targetSeatId} unavailable`);
@@ -1697,7 +1712,6 @@ function buildSeatReport(sessionName, seatId) {
1697
1712
  partnerSeatId: status?.partnerSeatId || meta?.partnerSeatId || getPartnerSeatId(seatId),
1698
1713
  state: wrapperLive ? status?.state || "running" : "orphaned_child",
1699
1714
  flowMode: status?.flowMode || meta?.flowMode || "off",
1700
- continueSeatId: status?.continueSeatId || meta?.continueSeatId || null,
1701
1715
  continueTargets: normalizeContinueTargets(
1702
1716
  status?.continueTargets || meta?.continueTargets || (
1703
1717
  (status?.continueSeatId || meta?.continueSeatId)
package/src/util.js CHANGED
@@ -287,10 +287,10 @@ function usage() {
287
287
  "Usage:",
288
288
  " muuuuse",
289
289
  " muuuuse 1",
290
+ " muuuuse 1 link 2 flow off",
290
291
  " muuuuse 1 link 2 flow on",
291
292
  " muuuuse 1 link 2 flow on 3 flow off",
292
293
  " muuuuse 2",
293
- " muuuuse 2 link 1 flow on",
294
294
  " muuuuse 3",
295
295
  " muuuuse 4",
296
296
  " muuuuse 4 link 3 flow off 1 flow on",
@@ -298,13 +298,13 @@ function usage() {
298
298
  " muuuuse status",
299
299
  "",
300
300
  "Flow:",
301
- " 1. Run `muuuuse 1` in terminal one.",
302
- " 2. Run `muuuuse 2` in terminal two.",
301
+ " 1. Run `muuuuse 1` in terminal one, then `muuuuse 2` in terminal two.",
302
+ " 2. Bare seats default to final-only pair relay.",
303
303
  " 3. The odd seat generates the session key and the matching even seat signs it automatically.",
304
304
  " 4. Additional pairs work the same way: `3/4`, `5/6`, `7/8`...",
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.",
305
+ " 5. Use `link <seat> flow on [<seat> flow off ...]` to set each outbound route.",
306
+ " 6. Include the odd/even partner in `link` to set normal pair flow.",
307
+ " 7. Any extra linked seats receive routed copies with their own flow mode.",
308
308
  " 8. `flow off` sends final answers only. `flow on` keeps assistant commentary bouncing.",
309
309
  " 9. Run `muuuuse status` or `muuuuse stop` from any shell.",
310
310
  "",