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 +29 -18
- package/package.json +2 -2
- package/src/agents.js +16 -89
- package/src/cli.js +100 -39
- package/src/runtime.js +40 -123
- package/src/util.js +14 -21
package/README.md
CHANGED
|
@@ -1,44 +1,56 @@
|
|
|
1
1
|
# 🔌Muuuuse
|
|
2
2
|
|
|
3
|
-
`🔌Muuuuse` is a tiny terminal relay
|
|
3
|
+
`🔌Muuuuse` is a tiny terminal relay.
|
|
4
4
|
|
|
5
5
|
It does one job:
|
|
6
|
-
- arm
|
|
7
|
-
-
|
|
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
|
-
-
|
|
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
|
|
18
|
-
muuuuse
|
|
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
|
-
##
|
|
33
|
+
## Flow
|
|
24
34
|
|
|
25
|
-
|
|
35
|
+
Terminal 1:
|
|
26
36
|
|
|
27
37
|
```bash
|
|
28
|
-
muuuuse
|
|
38
|
+
muuuuse 1 flow on
|
|
29
39
|
```
|
|
30
40
|
|
|
31
|
-
|
|
41
|
+
Terminal 2:
|
|
32
42
|
|
|
33
43
|
```bash
|
|
34
|
-
muuuuse
|
|
44
|
+
muuuuse 2 flow off
|
|
35
45
|
```
|
|
36
46
|
|
|
37
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
-
|
|
69
|
-
-
|
|
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.
|
|
4
|
-
"description": "🔌Muuuuse arms terminals
|
|
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
|
-
|
|
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
|
-
|
|
643
|
-
if (!flowMode && !isFinal) {
|
|
589
|
+
if (!flowMode && entry.message?.stop_reason !== "end_turn") {
|
|
644
590
|
return null;
|
|
645
591
|
}
|
|
646
592
|
|
|
647
|
-
const
|
|
648
|
-
|
|
649
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
.
|
|
691
|
-
|
|
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 =
|
|
109
|
-
if (
|
|
110
|
-
|
|
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
|
-
|
|
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
|
-
|
|
120
|
-
return { flowMode, continueTargets };
|
|
121
|
-
}
|
|
130
|
+
let index = 0;
|
|
122
131
|
|
|
123
|
-
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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 (
|
|
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
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
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
653
|
-
|
|
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((
|
|
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
|
-
|
|
1098
|
-
|
|
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,
|
|
1040
|
+
if (!getSeatDirIfExists(sessionName, seatIdToFind)) {
|
|
1110
1041
|
return null;
|
|
1111
1042
|
}
|
|
1112
1043
|
|
|
1113
|
-
const seat = buildSeatReport(sessionName,
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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.
|
|
1457
|
+
this.forwardContinuation(signedEntry);
|
|
1533
1458
|
this.rememberEmittedAnswer(answerKey);
|
|
1459
|
+
|
|
1534
1460
|
this.log(`[${this.seatId}] ${previewText(payload)}`);
|
|
1535
1461
|
}
|
|
1536
1462
|
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
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.
|
|
1610
|
-
|
|
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
|
-
|
|
1716
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
300
|
-
" muuuuse 1
|
|
301
|
-
" muuuuse 1
|
|
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
|
|
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
|
|
311
|
-
" 2.
|
|
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.
|
|
315
|
-
" 6.
|
|
316
|
-
" 7.
|
|
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
|
"",
|