muuuuse 2.1.0 → 2.2.1
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 +11 -6
- package/package.json +2 -2
- package/src/agents.js +30 -10
- package/src/cli.js +20 -4
- package/src/runtime.js +118 -28
- package/src/util.js +9 -4
package/README.md
CHANGED
|
@@ -6,15 +6,18 @@ It does one job:
|
|
|
6
6
|
- arm terminal one with `muuuuse 1`
|
|
7
7
|
- arm terminal two with `muuuuse 2`
|
|
8
8
|
- have seat 1 generate a session key and seat 2 sign it
|
|
9
|
-
-
|
|
10
|
-
-
|
|
9
|
+
- choose per-seat relay mode with `flow on` or `flow off`
|
|
10
|
+
- watch Codex, Claude, or Gemini for local assistant output
|
|
11
|
+
- inject that output into the other armed terminal
|
|
11
12
|
- keep looping until you stop it
|
|
12
13
|
|
|
13
14
|
The whole surface is:
|
|
14
15
|
|
|
15
16
|
```bash
|
|
16
17
|
muuuuse 1
|
|
18
|
+
muuuuse 1 flow on
|
|
17
19
|
muuuuse 2
|
|
20
|
+
muuuuse 2 flow off
|
|
18
21
|
muuuuse status
|
|
19
22
|
muuuuse stop
|
|
20
23
|
```
|
|
@@ -24,17 +27,19 @@ muuuuse stop
|
|
|
24
27
|
Terminal 1:
|
|
25
28
|
|
|
26
29
|
```bash
|
|
27
|
-
muuuuse 1
|
|
30
|
+
muuuuse 1 flow on
|
|
28
31
|
```
|
|
29
32
|
|
|
30
33
|
Terminal 2:
|
|
31
34
|
|
|
32
35
|
```bash
|
|
33
|
-
muuuuse 2
|
|
36
|
+
muuuuse 2 flow off
|
|
34
37
|
```
|
|
35
38
|
|
|
36
39
|
Now both shells are armed. `muuuuse 1` generates the session key, `muuuuse 2` signs it, and only that signed pair relays. Use those shells normally.
|
|
37
40
|
|
|
41
|
+
`flow on` means that seat sends commentary and final answers. `flow off` means that seat waits for final answers only. Each seat decides what it sends out, so mixed calibration is allowed.
|
|
42
|
+
|
|
38
43
|
If you want Codex in one and Gemini in the other, start them inside the armed shells:
|
|
39
44
|
|
|
40
45
|
```bash
|
|
@@ -45,7 +50,7 @@ codex
|
|
|
45
50
|
gemini
|
|
46
51
|
```
|
|
47
52
|
|
|
48
|
-
`🔌Muuuuse` tails the local session logs for supported CLIs,
|
|
53
|
+
`🔌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.
|
|
49
54
|
|
|
50
55
|
Check the live state from any terminal:
|
|
51
56
|
|
|
@@ -64,7 +69,7 @@ muuuuse stop
|
|
|
64
69
|
- no tmux
|
|
65
70
|
- state lives under `~/.muuuuse`
|
|
66
71
|
- only the signed armed pair can exchange relay events
|
|
67
|
-
- supported
|
|
72
|
+
- supported relay detection is built for Codex, Claude, and Gemini
|
|
68
73
|
- `codeman` remains the larger transport/control layer; `muuuuse` stays local and minimal
|
|
69
74
|
|
|
70
75
|
## Install
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "muuuuse",
|
|
3
|
-
"version": "2.1
|
|
4
|
-
"description": "🔌Muuuuse arms two regular terminals and relays
|
|
3
|
+
"version": "2.2.1",
|
|
4
|
+
"description": "🔌Muuuuse arms two regular terminals and relays assistant output between them.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"bin": {
|
|
7
7
|
"muuuuse": "bin/muuse.js"
|
package/src/agents.js
CHANGED
|
@@ -459,14 +459,17 @@ function extractCodexAssistantText(content) {
|
|
|
459
459
|
.join("\n");
|
|
460
460
|
}
|
|
461
461
|
|
|
462
|
-
function
|
|
462
|
+
function parseCodexAssistantLine(line, options = {}) {
|
|
463
|
+
const flowMode = options.flowMode === true;
|
|
463
464
|
try {
|
|
464
465
|
const entry = JSON.parse(line);
|
|
465
466
|
if (entry?.type !== "response_item" || entry.payload?.type !== "message" || entry.payload?.role !== "assistant") {
|
|
466
467
|
return null;
|
|
467
468
|
}
|
|
468
469
|
|
|
469
|
-
|
|
470
|
+
const phase = String(entry.payload?.phase || "").trim().toLowerCase();
|
|
471
|
+
const relayablePhase = phase === "final_answer" || (flowMode && phase === "commentary");
|
|
472
|
+
if (!relayablePhase) {
|
|
470
473
|
return null;
|
|
471
474
|
}
|
|
472
475
|
|
|
@@ -485,6 +488,10 @@ function parseCodexFinalLine(line) {
|
|
|
485
488
|
}
|
|
486
489
|
}
|
|
487
490
|
|
|
491
|
+
function parseCodexFinalLine(line) {
|
|
492
|
+
return parseCodexAssistantLine(line, { flowMode: false });
|
|
493
|
+
}
|
|
494
|
+
|
|
488
495
|
function isAnswerNewEnough(answer, sinceMs = null) {
|
|
489
496
|
if (!Number.isFinite(sinceMs)) {
|
|
490
497
|
return true;
|
|
@@ -498,13 +505,13 @@ function isAnswerNewEnough(answer, sinceMs = null) {
|
|
|
498
505
|
return answerMs >= sinceMs;
|
|
499
506
|
}
|
|
500
507
|
|
|
501
|
-
function readCodexAnswers(filePath, offset, sinceMs = null) {
|
|
508
|
+
function readCodexAnswers(filePath, offset, sinceMs = null, options = {}) {
|
|
502
509
|
const { nextOffset, text } = readAppendedText(filePath, offset);
|
|
503
510
|
const answers = text
|
|
504
511
|
.split("\n")
|
|
505
512
|
.map((line) => line.trim())
|
|
506
513
|
.filter((line) => line.length > 0)
|
|
507
|
-
.map((line) =>
|
|
514
|
+
.map((line) => parseCodexAssistantLine(line, options))
|
|
508
515
|
.filter((entry) => entry !== null)
|
|
509
516
|
.filter((entry) => isAnswerNewEnough(entry, sinceMs));
|
|
510
517
|
|
|
@@ -547,7 +554,8 @@ function selectClaudeSessionFile(currentPath, processStartedAtMs, options = {})
|
|
|
547
554
|
return selectSessionCandidatePath(candidates, currentPath, processStartedAtMs);
|
|
548
555
|
}
|
|
549
556
|
|
|
550
|
-
function extractClaudeAssistantText(content) {
|
|
557
|
+
function extractClaudeAssistantText(content, options = {}) {
|
|
558
|
+
const flowMode = options.flowMode === true;
|
|
551
559
|
if (!Array.isArray(content)) {
|
|
552
560
|
return "";
|
|
553
561
|
}
|
|
@@ -560,20 +568,28 @@ function extractClaudeAssistantText(content) {
|
|
|
560
568
|
if (item.type === "text" && typeof item.text === "string") {
|
|
561
569
|
return [item.text.trim()];
|
|
562
570
|
}
|
|
571
|
+
if (flowMode && item.type === "thinking" && typeof item.thinking === "string") {
|
|
572
|
+
return [item.thinking.trim()];
|
|
573
|
+
}
|
|
563
574
|
return [];
|
|
564
575
|
})
|
|
565
576
|
.filter((text) => text.length > 0)
|
|
566
577
|
.join("\n");
|
|
567
578
|
}
|
|
568
579
|
|
|
569
|
-
function
|
|
580
|
+
function parseClaudeAssistantLine(line, options = {}) {
|
|
581
|
+
const flowMode = options.flowMode === true;
|
|
570
582
|
try {
|
|
571
583
|
const entry = JSON.parse(line);
|
|
572
|
-
if (entry?.type !== "assistant" || entry.message?.role !== "assistant"
|
|
584
|
+
if (entry?.type !== "assistant" || entry.message?.role !== "assistant") {
|
|
573
585
|
return null;
|
|
574
586
|
}
|
|
575
587
|
|
|
576
|
-
|
|
588
|
+
if (!flowMode && entry.message?.stop_reason !== "end_turn") {
|
|
589
|
+
return null;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const text = sanitizeRelayText(extractClaudeAssistantText(entry.message.content, options));
|
|
577
593
|
if (!text) {
|
|
578
594
|
return null;
|
|
579
595
|
}
|
|
@@ -588,13 +604,17 @@ function parseClaudeFinalLine(line) {
|
|
|
588
604
|
}
|
|
589
605
|
}
|
|
590
606
|
|
|
591
|
-
function
|
|
607
|
+
function parseClaudeFinalLine(line) {
|
|
608
|
+
return parseClaudeAssistantLine(line, { flowMode: false });
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function readClaudeAnswers(filePath, offset, sinceMs = null, options = {}) {
|
|
592
612
|
const { nextOffset, text } = readAppendedText(filePath, offset);
|
|
593
613
|
const answers = text
|
|
594
614
|
.split("\n")
|
|
595
615
|
.map((line) => line.trim())
|
|
596
616
|
.filter((line) => line.length > 0)
|
|
597
|
-
.map((line) =>
|
|
617
|
+
.map((line) => parseClaudeAssistantLine(line, options))
|
|
598
618
|
.filter((entry) => entry !== null)
|
|
599
619
|
.filter((entry) => isAnswerNewEnough(entry, sinceMs));
|
|
600
620
|
|
package/src/cli.js
CHANGED
|
@@ -55,12 +55,10 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
if (command === "1" || command === "2") {
|
|
58
|
-
|
|
59
|
-
throw new Error(`\`muuuuse ${command}\` takes no extra arguments. Run it directly in the terminal you want to arm.`);
|
|
60
|
-
}
|
|
61
|
-
|
|
58
|
+
const flowMode = parseSeatFlowMode(command, argv.slice(1));
|
|
62
59
|
const seat = new ArmedSeat({
|
|
63
60
|
cwd: process.cwd(),
|
|
61
|
+
flowMode,
|
|
64
62
|
seatId: Number(command),
|
|
65
63
|
});
|
|
66
64
|
const code = await seat.run();
|
|
@@ -74,6 +72,7 @@ function renderSeatStatus(seat) {
|
|
|
74
72
|
const bits = [
|
|
75
73
|
`seat ${seat.seatId}: ${seat.state}`,
|
|
76
74
|
`agent ${seat.agent || "idle"}`,
|
|
75
|
+
`flow ${seat.flowMode || "off"}`,
|
|
77
76
|
`relays ${seat.relayCount}`,
|
|
78
77
|
`wrapper ${seat.wrapperPid || "-"}`,
|
|
79
78
|
`child ${seat.childPid || "-"}`,
|
|
@@ -99,6 +98,23 @@ function renderSeatStatus(seat) {
|
|
|
99
98
|
return output;
|
|
100
99
|
}
|
|
101
100
|
|
|
101
|
+
function parseSeatFlowMode(command, args) {
|
|
102
|
+
if (args.length === 0) {
|
|
103
|
+
return "off";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (args.length === 2 && String(args[0]).trim().toLowerCase() === "flow") {
|
|
107
|
+
const flowMode = String(args[1]).trim().toLowerCase();
|
|
108
|
+
if (flowMode === "on" || flowMode === "off") {
|
|
109
|
+
return flowMode;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
throw new Error(
|
|
114
|
+
`\`muuuuse ${command}\` accepts either no extra arguments or \`flow on\` / \`flow off\`. Run it directly in the terminal you want to arm.`
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
102
118
|
module.exports = {
|
|
103
119
|
main,
|
|
104
120
|
};
|
package/src/runtime.js
CHANGED
|
@@ -35,7 +35,8 @@ const {
|
|
|
35
35
|
writeJson,
|
|
36
36
|
} = require("./util");
|
|
37
37
|
|
|
38
|
-
const
|
|
38
|
+
const TYPE_CHUNK_DELAY_MS = 18;
|
|
39
|
+
const TYPE_CHUNK_SIZE = 24;
|
|
39
40
|
const MIRROR_SUPPRESSION_WINDOW_MS = 30 * 1000;
|
|
40
41
|
const PENDING_RELAY_CONTEXT_TTL_MS = 2 * 60 * 1000;
|
|
41
42
|
const EMITTED_ANSWER_TTL_MS = 5 * 60 * 1000;
|
|
@@ -51,6 +52,10 @@ const CHILD_ENV_DROP_KEYS = [
|
|
|
51
52
|
"CODEX_THREAD_ID",
|
|
52
53
|
];
|
|
53
54
|
|
|
55
|
+
function normalizeFlowMode(flowMode) {
|
|
56
|
+
return String(flowMode || "").trim().toLowerCase() === "on" ? "on" : "off";
|
|
57
|
+
}
|
|
58
|
+
|
|
54
59
|
function resolveShell() {
|
|
55
60
|
const shell = String(process.env.SHELL || "").trim();
|
|
56
61
|
return shell || "/bin/bash";
|
|
@@ -214,6 +219,51 @@ function parseAnswerEntries(text) {
|
|
|
214
219
|
.filter((entry) => entry && entry.type === "answer" && typeof entry.text === "string");
|
|
215
220
|
}
|
|
216
221
|
|
|
222
|
+
function readSessionHeaderText(filePath, maxBytes = 16384) {
|
|
223
|
+
try {
|
|
224
|
+
const fd = fs.openSync(filePath, "r");
|
|
225
|
+
try {
|
|
226
|
+
const buffer = Buffer.alloc(maxBytes);
|
|
227
|
+
const bytesRead = fs.readSync(fd, buffer, 0, buffer.length, 0);
|
|
228
|
+
return buffer.toString("utf8", 0, bytesRead);
|
|
229
|
+
} finally {
|
|
230
|
+
fs.closeSync(fd);
|
|
231
|
+
}
|
|
232
|
+
} catch {
|
|
233
|
+
return "";
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function readSessionFileStartedAtMs(agentType, filePath) {
|
|
238
|
+
try {
|
|
239
|
+
if (agentType === "gemini") {
|
|
240
|
+
const entry = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
241
|
+
return Date.parse(entry.startTime || entry.lastUpdated || "");
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const header = readSessionHeaderText(filePath);
|
|
245
|
+
const lines = header
|
|
246
|
+
.split("\n")
|
|
247
|
+
.map((line) => line.trim())
|
|
248
|
+
.filter(Boolean);
|
|
249
|
+
|
|
250
|
+
for (const line of lines) {
|
|
251
|
+
const entry = JSON.parse(line);
|
|
252
|
+
if (agentType === "codex" && entry?.type === "session_meta") {
|
|
253
|
+
return Date.parse(entry.payload?.timestamp || "");
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (agentType === "claude") {
|
|
257
|
+
return Date.parse(entry.timestamp || entry.message?.timestamp || "");
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
} catch {
|
|
261
|
+
return Number.NaN;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return Number.NaN;
|
|
265
|
+
}
|
|
266
|
+
|
|
217
267
|
function readProcessCwd(pid) {
|
|
218
268
|
if (!Number.isInteger(pid) || pid <= 0) {
|
|
219
269
|
return null;
|
|
@@ -386,35 +436,56 @@ function readSeatChallenge(paths, sessionName) {
|
|
|
386
436
|
};
|
|
387
437
|
}
|
|
388
438
|
|
|
389
|
-
|
|
390
|
-
|
|
439
|
+
function normalizeRelayPayloadForTyping(text) {
|
|
440
|
+
return String(text || "")
|
|
441
|
+
.replace(/\r/g, "")
|
|
442
|
+
.replace(/\s*\n+\s*/g, " ")
|
|
443
|
+
.replace(/[ \t]{2,}/g, " ")
|
|
444
|
+
.trim();
|
|
445
|
+
}
|
|
391
446
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
447
|
+
function chunkRelayPayloadForTyping(text, chunkSize = TYPE_CHUNK_SIZE) {
|
|
448
|
+
const normalized = String(text || "");
|
|
449
|
+
if (!normalized) {
|
|
450
|
+
return [];
|
|
451
|
+
}
|
|
396
452
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
453
|
+
const size = Number.isInteger(chunkSize) && chunkSize > 0 ? chunkSize : TYPE_CHUNK_SIZE;
|
|
454
|
+
const chunks = [];
|
|
455
|
+
for (let index = 0; index < normalized.length; index += size) {
|
|
456
|
+
chunks.push(normalized.slice(index, index + size));
|
|
457
|
+
}
|
|
458
|
+
return chunks;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function stripPassiveTerminalInput(input) {
|
|
462
|
+
return String(input || "")
|
|
463
|
+
.replace(/\u001b\][\s\S]*?(?:\u0007|\u001b\\)/g, "")
|
|
464
|
+
.replace(/\u001b\[(?:I|O)/g, "")
|
|
465
|
+
.replace(/\u001b\[\d+;\d+R/g, "")
|
|
466
|
+
.replace(/\u001b\[\?[0-9;]*c/g, "")
|
|
467
|
+
.replace(/\u0000/g, "");
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function isMeaningfulTerminalInput(input) {
|
|
471
|
+
return stripPassiveTerminalInput(input).length > 0;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
async function sendTextAndEnter(child, text, shouldAbort = () => false) {
|
|
475
|
+
const payload = normalizeRelayPayloadForTyping(text);
|
|
406
476
|
|
|
407
|
-
|
|
408
|
-
|
|
477
|
+
if (payload.length > 0) {
|
|
478
|
+
for (const chunk of chunkRelayPayloadForTyping(payload)) {
|
|
479
|
+
if (shouldAbort() || !child) {
|
|
409
480
|
return false;
|
|
410
481
|
}
|
|
411
482
|
|
|
412
483
|
try {
|
|
413
|
-
child.write(
|
|
484
|
+
child.write(chunk);
|
|
414
485
|
} catch {
|
|
415
486
|
return false;
|
|
416
487
|
}
|
|
417
|
-
await sleep(
|
|
488
|
+
await sleep(TYPE_CHUNK_DELAY_MS);
|
|
418
489
|
}
|
|
419
490
|
}
|
|
420
491
|
|
|
@@ -435,6 +506,7 @@ class ArmedSeat {
|
|
|
435
506
|
constructor(options) {
|
|
436
507
|
this.seatId = options.seatId;
|
|
437
508
|
this.partnerSeatId = options.seatId === 1 ? 2 : 1;
|
|
509
|
+
this.flowMode = normalizeFlowMode(options.flowMode);
|
|
438
510
|
this.cwd = normalizeWorkingPath(options.cwd);
|
|
439
511
|
this.sessionName = resolveSessionName(this.cwd, this.seatId);
|
|
440
512
|
if (!this.sessionName) {
|
|
@@ -503,6 +575,7 @@ class ArmedSeat {
|
|
|
503
575
|
seatId: this.seatId,
|
|
504
576
|
partnerSeatId: this.partnerSeatId,
|
|
505
577
|
sessionName: this.sessionName,
|
|
578
|
+
flowMode: this.flowMode,
|
|
506
579
|
cwd: this.cwd,
|
|
507
580
|
pid: process.pid,
|
|
508
581
|
childPid: this.childPid,
|
|
@@ -517,6 +590,7 @@ class ArmedSeat {
|
|
|
517
590
|
seatId: this.seatId,
|
|
518
591
|
partnerSeatId: this.partnerSeatId,
|
|
519
592
|
sessionName: this.sessionName,
|
|
593
|
+
flowMode: this.flowMode,
|
|
520
594
|
cwd: this.cwd,
|
|
521
595
|
pid: process.pid,
|
|
522
596
|
childPid: this.childPid,
|
|
@@ -732,12 +806,15 @@ class ArmedSeat {
|
|
|
732
806
|
|
|
733
807
|
installStdinProxy() {
|
|
734
808
|
const handleData = (chunk) => {
|
|
735
|
-
|
|
736
|
-
|
|
809
|
+
const chunkText = chunk.toString("utf8");
|
|
810
|
+
if (isMeaningfulTerminalInput(chunkText)) {
|
|
811
|
+
this.lastUserInputAtMs = Date.now();
|
|
812
|
+
this.pendingInboundContext = null;
|
|
813
|
+
}
|
|
737
814
|
if (!this.child) {
|
|
738
815
|
return;
|
|
739
816
|
}
|
|
740
|
-
this.child.write(
|
|
817
|
+
this.child.write(chunkText);
|
|
741
818
|
};
|
|
742
819
|
|
|
743
820
|
const handleEnd = () => {
|
|
@@ -1079,6 +1156,10 @@ class ArmedSeat {
|
|
|
1079
1156
|
this.liveState.sessionFile = resolvedSessionFile;
|
|
1080
1157
|
this.liveState.offset = 0;
|
|
1081
1158
|
this.liveState.lastMessageId = null;
|
|
1159
|
+
const sessionStartedAtMs = readSessionFileStartedAtMs(detectedAgent.type, resolvedSessionFile);
|
|
1160
|
+
if (Number.isFinite(sessionStartedAtMs)) {
|
|
1161
|
+
this.liveState.captureSinceMs = Math.min(this.liveState.captureSinceMs, sessionStartedAtMs);
|
|
1162
|
+
}
|
|
1082
1163
|
}
|
|
1083
1164
|
|
|
1084
1165
|
if (!this.liveState.sessionFile) {
|
|
@@ -1096,7 +1177,8 @@ class ArmedSeat {
|
|
|
1096
1177
|
const result = readCodexAnswers(
|
|
1097
1178
|
this.liveState.sessionFile,
|
|
1098
1179
|
this.liveState.offset,
|
|
1099
|
-
this.liveState.captureSinceMs
|
|
1180
|
+
this.liveState.captureSinceMs,
|
|
1181
|
+
{ flowMode: this.flowMode === "on" }
|
|
1100
1182
|
);
|
|
1101
1183
|
this.liveState.offset = result.nextOffset;
|
|
1102
1184
|
answers.push(...result.answers);
|
|
@@ -1104,7 +1186,8 @@ class ArmedSeat {
|
|
|
1104
1186
|
const result = readClaudeAnswers(
|
|
1105
1187
|
this.liveState.sessionFile,
|
|
1106
1188
|
this.liveState.offset,
|
|
1107
|
-
this.liveState.captureSinceMs
|
|
1189
|
+
this.liveState.captureSinceMs,
|
|
1190
|
+
{ flowMode: this.flowMode === "on" }
|
|
1108
1191
|
);
|
|
1109
1192
|
this.liveState.offset = result.nextOffset;
|
|
1110
1193
|
answers.push(...result.answers);
|
|
@@ -1112,7 +1195,8 @@ class ArmedSeat {
|
|
|
1112
1195
|
const result = readGeminiAnswers(
|
|
1113
1196
|
this.liveState.sessionFile,
|
|
1114
1197
|
this.liveState.lastMessageId,
|
|
1115
|
-
this.liveState.captureSinceMs
|
|
1198
|
+
this.liveState.captureSinceMs,
|
|
1199
|
+
{ flowMode: this.flowMode === "on" }
|
|
1116
1200
|
);
|
|
1117
1201
|
this.liveState.lastMessageId = result.lastMessageId;
|
|
1118
1202
|
this.liveState.offset = result.fileSize;
|
|
@@ -1161,7 +1245,7 @@ class ArmedSeat {
|
|
|
1161
1245
|
}
|
|
1162
1246
|
|
|
1163
1247
|
const pendingInboundContext = this.getPendingInboundContext();
|
|
1164
|
-
if (pendingInboundContext && pendingInboundContext.hop >= MAX_RELAY_CHAIN_HOP) {
|
|
1248
|
+
if (this.flowMode !== "on" && pendingInboundContext && pendingInboundContext.hop >= MAX_RELAY_CHAIN_HOP) {
|
|
1165
1249
|
this.log(`[${this.seatId}] suppressed relay loop: ${previewText(payload)}`);
|
|
1166
1250
|
return;
|
|
1167
1251
|
}
|
|
@@ -1223,6 +1307,7 @@ class ArmedSeat {
|
|
|
1223
1307
|
this.writeStatus({
|
|
1224
1308
|
state: live.state,
|
|
1225
1309
|
agent: live.agent,
|
|
1310
|
+
flowMode: this.flowMode,
|
|
1226
1311
|
cwd: live.cwd,
|
|
1227
1312
|
log: live.log,
|
|
1228
1313
|
lastAnswerAt: live.lastAnswerAt,
|
|
@@ -1239,7 +1324,8 @@ class ArmedSeat {
|
|
|
1239
1324
|
this.installResizeHandler();
|
|
1240
1325
|
|
|
1241
1326
|
this.log(`${BRAND} seat ${this.seatId} armed for ${this.sessionName}.`);
|
|
1242
|
-
this.log("Use this shell normally. Codex, Claude, and Gemini
|
|
1327
|
+
this.log("Use this shell normally. Codex, Claude, and Gemini relay automatically from their local session logs.");
|
|
1328
|
+
this.log(`Seat ${this.seatId} relay mode is flow ${this.flowMode}.`);
|
|
1243
1329
|
if (this.seatId === 1) {
|
|
1244
1330
|
this.log("Seat 1 generated the session key and is waiting for seat 2 to sign it.");
|
|
1245
1331
|
} else {
|
|
@@ -1339,6 +1425,7 @@ function buildSeatReport(sessionName, seatId) {
|
|
|
1339
1425
|
return {
|
|
1340
1426
|
seatId,
|
|
1341
1427
|
state: wrapperLive ? status?.state || "running" : "orphaned_child",
|
|
1428
|
+
flowMode: status?.flowMode || meta?.flowMode || "off",
|
|
1342
1429
|
wrapperPid,
|
|
1343
1430
|
childPid,
|
|
1344
1431
|
wrapperLive,
|
|
@@ -1425,7 +1512,10 @@ function stopAllSessions() {
|
|
|
1425
1512
|
module.exports = {
|
|
1426
1513
|
ArmedSeat,
|
|
1427
1514
|
buildChildEnv,
|
|
1515
|
+
chunkRelayPayloadForTyping,
|
|
1428
1516
|
getStatusReport,
|
|
1517
|
+
isMeaningfulTerminalInput,
|
|
1518
|
+
normalizeRelayPayloadForTyping,
|
|
1429
1519
|
resolveSessionName,
|
|
1430
1520
|
stopAllSessions,
|
|
1431
1521
|
};
|
package/src/util.js
CHANGED
|
@@ -245,11 +245,15 @@ function listSessionNames() {
|
|
|
245
245
|
|
|
246
246
|
function usage() {
|
|
247
247
|
return [
|
|
248
|
-
`${BRAND} arms two regular terminals and relays
|
|
248
|
+
`${BRAND} arms two regular terminals and relays assistant output between them.`,
|
|
249
249
|
"",
|
|
250
250
|
"Usage:",
|
|
251
251
|
" muuuuse 1",
|
|
252
|
+
" muuuuse 1 flow on",
|
|
253
|
+
" muuuuse 1 flow off",
|
|
252
254
|
" muuuuse 2",
|
|
255
|
+
" muuuuse 2 flow on",
|
|
256
|
+
" muuuuse 2 flow off",
|
|
253
257
|
" muuuuse stop",
|
|
254
258
|
" muuuuse status",
|
|
255
259
|
"",
|
|
@@ -257,9 +261,10 @@ function usage() {
|
|
|
257
261
|
" 1. Run `muuuuse 1` in terminal one.",
|
|
258
262
|
" 2. Run `muuuuse 2` in terminal two.",
|
|
259
263
|
" 3. Seat 1 generates the session key and seat 2 signs it automatically.",
|
|
260
|
-
" 4.
|
|
261
|
-
" 5.
|
|
262
|
-
" 6.
|
|
264
|
+
" 4. Optional: arm each seat with `flow on` or `flow off`.",
|
|
265
|
+
" 5. Use those armed shells normally.",
|
|
266
|
+
" 6. `flow off` sends final answers only. `flow on` keeps assistant commentary bouncing.",
|
|
267
|
+
" 7. Run `muuuuse status` or `muuuuse stop` from any shell.",
|
|
263
268
|
"",
|
|
264
269
|
"Notes:",
|
|
265
270
|
" - No tmux.",
|