muuuuse 1.5.0 → 2.2.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/README.md +11 -6
- package/package.json +2 -2
- package/src/agents.js +283 -40
- package/src/cli.js +20 -4
- package/src/runtime.js +397 -58
- 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": "
|
|
4
|
-
"description": "🔌Muuuuse arms two regular terminals and relays
|
|
3
|
+
"version": "2.2.0",
|
|
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
|
@@ -12,9 +12,11 @@ const {
|
|
|
12
12
|
} = require("./util");
|
|
13
13
|
|
|
14
14
|
const CODEX_ROOT = path.join(os.homedir(), ".codex", "sessions");
|
|
15
|
+
const CODEX_SNAPSHOT_ROOT = path.join(os.homedir(), ".codex", "shell_snapshots");
|
|
15
16
|
const CLAUDE_ROOT = path.join(os.homedir(), ".claude", "projects");
|
|
16
17
|
const GEMINI_ROOT = path.join(os.homedir(), ".gemini", "tmp");
|
|
17
18
|
const SESSION_START_EARLY_TOLERANCE_MS = 2 * 1000;
|
|
19
|
+
const STRICT_SINGLE_CANDIDATE_EARLY_TOLERANCE_MS = 250;
|
|
18
20
|
|
|
19
21
|
function walkFiles(rootPath, predicate, results = []) {
|
|
20
22
|
try {
|
|
@@ -51,7 +53,11 @@ function buildDetectedAgent(type, process) {
|
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
function detectAgent(processes) {
|
|
54
|
-
const ordered = [...processes].sort((left, right) =>
|
|
56
|
+
const ordered = [...processes].sort((left, right) => (
|
|
57
|
+
(left.depth ?? Number.MAX_SAFE_INTEGER) - (right.depth ?? Number.MAX_SAFE_INTEGER) ||
|
|
58
|
+
right.elapsedSeconds - left.elapsedSeconds ||
|
|
59
|
+
left.pid - right.pid
|
|
60
|
+
));
|
|
55
61
|
for (const process of ordered) {
|
|
56
62
|
if (commandMatches(process.args, "codex")) {
|
|
57
63
|
return buildDetectedAgent("codex", process);
|
|
@@ -93,6 +99,33 @@ function readFirstLines(filePath, maxLines = 20) {
|
|
|
93
99
|
}
|
|
94
100
|
}
|
|
95
101
|
|
|
102
|
+
function sortSessionCandidates(candidates) {
|
|
103
|
+
return candidates
|
|
104
|
+
.slice()
|
|
105
|
+
.sort((left, right) => {
|
|
106
|
+
const leftDiff = Number.isFinite(left.diffMs) ? left.diffMs : Number.MAX_SAFE_INTEGER;
|
|
107
|
+
const rightDiff = Number.isFinite(right.diffMs) ? right.diffMs : Number.MAX_SAFE_INTEGER;
|
|
108
|
+
return (
|
|
109
|
+
leftDiff - rightDiff ||
|
|
110
|
+
right.startedAtMs - left.startedAtMs ||
|
|
111
|
+
right.mtimeMs - left.mtimeMs ||
|
|
112
|
+
left.path.localeCompare(right.path)
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function annotateSessionCandidates(candidates, processStartedAtMs) {
|
|
118
|
+
return candidates.map((candidate) => ({
|
|
119
|
+
...candidate,
|
|
120
|
+
diffMs: Number.isFinite(processStartedAtMs) && Number.isFinite(candidate.startedAtMs)
|
|
121
|
+
? Math.abs(candidate.startedAtMs - processStartedAtMs)
|
|
122
|
+
: Number.POSITIVE_INFINITY,
|
|
123
|
+
relativeStartMs: Number.isFinite(processStartedAtMs) && Number.isFinite(candidate.startedAtMs)
|
|
124
|
+
? candidate.startedAtMs - processStartedAtMs
|
|
125
|
+
: Number.NaN,
|
|
126
|
+
}));
|
|
127
|
+
}
|
|
128
|
+
|
|
96
129
|
function selectSessionCandidatePath(candidates, currentPath, processStartedAtMs) {
|
|
97
130
|
const cwdMatches = candidates.filter((candidate) => candidate.cwd === currentPath);
|
|
98
131
|
if (cwdMatches.length === 0) {
|
|
@@ -107,12 +140,7 @@ function selectSessionCandidatePath(candidates, currentPath, processStartedAtMs)
|
|
|
107
140
|
return null;
|
|
108
141
|
}
|
|
109
142
|
|
|
110
|
-
const preciseMatches = cwdMatches
|
|
111
|
-
.map((candidate) => ({
|
|
112
|
-
...candidate,
|
|
113
|
-
diffMs: Math.abs(candidate.startedAtMs - processStartedAtMs),
|
|
114
|
-
relativeStartMs: candidate.startedAtMs - processStartedAtMs,
|
|
115
|
-
}))
|
|
143
|
+
const preciseMatches = annotateSessionCandidates(cwdMatches, processStartedAtMs)
|
|
116
144
|
.filter((candidate) => (
|
|
117
145
|
Number.isFinite(candidate.diffMs) &&
|
|
118
146
|
Number.isFinite(candidate.relativeStartMs) &&
|
|
@@ -128,29 +156,98 @@ function selectSessionCandidatePath(candidates, currentPath, processStartedAtMs)
|
|
|
128
156
|
return null;
|
|
129
157
|
}
|
|
130
158
|
|
|
131
|
-
function
|
|
132
|
-
if (!
|
|
133
|
-
return
|
|
159
|
+
function readCodexSeatClaim(sessionId) {
|
|
160
|
+
if (!sessionId) {
|
|
161
|
+
return null;
|
|
134
162
|
}
|
|
135
163
|
|
|
136
|
-
const
|
|
164
|
+
const snapshotPath = path.join(CODEX_SNAPSHOT_ROOT, `${sessionId}.sh`);
|
|
137
165
|
try {
|
|
138
|
-
const
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
return null;
|
|
145
|
-
}
|
|
146
|
-
})
|
|
147
|
-
.filter((entry) => typeof entry === "string")
|
|
148
|
-
.filter((entry) => entry.startsWith(rootPrefix));
|
|
166
|
+
const text = fs.readFileSync(snapshotPath, "utf8");
|
|
167
|
+
const seatMatch = text.match(/declare -x MUUUUSE_SEAT="([^"]+)"/);
|
|
168
|
+
const sessionMatch = text.match(/declare -x MUUUUSE_SESSION="([^"]+)"/);
|
|
169
|
+
if (!seatMatch || !sessionMatch) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
149
172
|
|
|
150
|
-
return
|
|
173
|
+
return {
|
|
174
|
+
seatId: seatMatch[1],
|
|
175
|
+
sessionName: sessionMatch[1],
|
|
176
|
+
};
|
|
151
177
|
} catch {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function selectClaimedCodexCandidatePath(candidates, options = {}) {
|
|
183
|
+
const seatId = options.seatId == null ? null : String(options.seatId);
|
|
184
|
+
const sessionName = typeof options.sessionName === "string" ? options.sessionName : null;
|
|
185
|
+
if (!seatId || !sessionName || candidates.length <= 1) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const annotated = candidates.map((candidate) => ({
|
|
190
|
+
...candidate,
|
|
191
|
+
claim: readCodexSeatClaim(candidate.sessionId),
|
|
192
|
+
}));
|
|
193
|
+
|
|
194
|
+
const exactMatches = annotated.filter((candidate) => (
|
|
195
|
+
candidate.claim?.seatId === seatId &&
|
|
196
|
+
candidate.claim?.sessionName === sessionName
|
|
197
|
+
));
|
|
198
|
+
if (exactMatches.length === 1) {
|
|
199
|
+
return exactMatches[0].path;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const otherSeatClaims = annotated.filter((candidate) => (
|
|
203
|
+
candidate.claim?.sessionName === sessionName &&
|
|
204
|
+
candidate.claim?.seatId !== seatId
|
|
205
|
+
));
|
|
206
|
+
if (otherSeatClaims.length === 0) {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const foreignPaths = new Set(otherSeatClaims.map((candidate) => candidate.path));
|
|
211
|
+
const remaining = annotated.filter((candidate) => !foreignPaths.has(candidate.path));
|
|
212
|
+
if (remaining.length === 1) {
|
|
213
|
+
return remaining[0].path;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function listOpenFilePathsForPids(pids, rootPath) {
|
|
220
|
+
const normalizedPids = [...new Set(
|
|
221
|
+
(Array.isArray(pids) ? pids : [pids])
|
|
222
|
+
.map((pid) => Number.parseInt(pid, 10))
|
|
223
|
+
.filter((pid) => Number.isInteger(pid) && pid > 0)
|
|
224
|
+
)];
|
|
225
|
+
if (normalizedPids.length === 0) {
|
|
152
226
|
return [];
|
|
153
227
|
}
|
|
228
|
+
|
|
229
|
+
const rootPrefix = path.resolve(rootPath);
|
|
230
|
+
const openPaths = new Set();
|
|
231
|
+
|
|
232
|
+
for (const pid of normalizedPids) {
|
|
233
|
+
const fdRoot = `/proc/${pid}/fd`;
|
|
234
|
+
try {
|
|
235
|
+
for (const entry of fs.readdirSync(fdRoot)) {
|
|
236
|
+
try {
|
|
237
|
+
const resolved = fs.realpathSync(path.join(fdRoot, entry));
|
|
238
|
+
if (typeof resolved === "string" && resolved.startsWith(rootPrefix)) {
|
|
239
|
+
openPaths.add(resolved);
|
|
240
|
+
}
|
|
241
|
+
} catch {
|
|
242
|
+
// Ignore descriptors that disappear while we are inspecting them.
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
} catch {
|
|
246
|
+
// Ignore pids that have already exited.
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return [...openPaths];
|
|
154
251
|
}
|
|
155
252
|
|
|
156
253
|
function selectLiveSessionCandidatePath(candidates, currentPath, captureSinceMs = null) {
|
|
@@ -173,8 +270,8 @@ function selectLiveSessionCandidatePath(candidates, currentPath, captureSinceMs
|
|
|
173
270
|
return ranked[0]?.path || null;
|
|
174
271
|
}
|
|
175
272
|
|
|
176
|
-
function readOpenSessionCandidates(
|
|
177
|
-
return
|
|
273
|
+
function readOpenSessionCandidates(pids, rootPath, reader) {
|
|
274
|
+
return listOpenFilePathsForPids(pids, rootPath)
|
|
178
275
|
.map((filePath) => reader(filePath))
|
|
179
276
|
.filter((candidate) => candidate !== null);
|
|
180
277
|
}
|
|
@@ -195,6 +292,7 @@ function readCodexCandidate(filePath) {
|
|
|
195
292
|
path: filePath,
|
|
196
293
|
cwd: entry.payload.cwd,
|
|
197
294
|
isSubagent: Boolean(entry.payload?.source?.subagent),
|
|
295
|
+
sessionId: entry.payload.id || null,
|
|
198
296
|
startedAtMs: Date.parse(entry.payload.timestamp),
|
|
199
297
|
mtimeMs: fs.statSync(filePath).mtimeMs,
|
|
200
298
|
};
|
|
@@ -203,9 +301,131 @@ function readCodexCandidate(filePath) {
|
|
|
203
301
|
}
|
|
204
302
|
}
|
|
205
303
|
|
|
304
|
+
function rankCodexCandidates(candidates, processStartedAtMs) {
|
|
305
|
+
return sortSessionCandidates(annotateSessionCandidates(candidates, processStartedAtMs));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function selectExactClaimedCodexCandidate(candidates, options = {}, processStartedAtMs = null) {
|
|
309
|
+
const seatId = options.seatId == null ? null : String(options.seatId);
|
|
310
|
+
const sessionName = typeof options.sessionName === "string" ? options.sessionName : null;
|
|
311
|
+
if (!seatId || !sessionName) {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const exactMatches = rankCodexCandidates(
|
|
316
|
+
candidates.filter((candidate) => {
|
|
317
|
+
const claim = readCodexSeatClaim(candidate.sessionId);
|
|
318
|
+
return claim?.seatId === seatId && claim?.sessionName === sessionName;
|
|
319
|
+
}),
|
|
320
|
+
processStartedAtMs
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
return exactMatches[0]?.path || null;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function filterForeignClaimedCodexCandidates(candidates, options = {}) {
|
|
327
|
+
const seatId = options.seatId == null ? null : String(options.seatId);
|
|
328
|
+
const sessionName = typeof options.sessionName === "string" ? options.sessionName : null;
|
|
329
|
+
if (!seatId || !sessionName) {
|
|
330
|
+
return candidates.slice();
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return candidates.filter((candidate) => {
|
|
334
|
+
const claim = readCodexSeatClaim(candidate.sessionId);
|
|
335
|
+
return !(claim?.sessionName === sessionName && claim?.seatId && claim.seatId !== seatId);
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function selectStrictSingleCodexCandidatePath(candidates, processStartedAtMs) {
|
|
340
|
+
if (candidates.length !== 1 || !Number.isFinite(processStartedAtMs)) {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const [candidate] = annotateSessionCandidates(candidates, processStartedAtMs);
|
|
345
|
+
if (!Number.isFinite(candidate.relativeStartMs)) {
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (
|
|
350
|
+
candidate.relativeStartMs < -STRICT_SINGLE_CANDIDATE_EARLY_TOLERANCE_MS ||
|
|
351
|
+
candidate.relativeStartMs > SESSION_MATCH_WINDOW_MS
|
|
352
|
+
) {
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return candidate.path;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function selectCodexCandidatePath(candidates, currentPath, processStartedAtMs, options = {}) {
|
|
360
|
+
const cwdMatches = candidates.filter((candidate) => candidate.cwd === currentPath);
|
|
361
|
+
if (cwdMatches.length === 0) {
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const seatId = options.seatId == null ? null : String(options.seatId);
|
|
366
|
+
const sessionName = typeof options.sessionName === "string" ? options.sessionName : null;
|
|
367
|
+
const exactClaimPath = selectExactClaimedCodexCandidate(cwdMatches, options, processStartedAtMs);
|
|
368
|
+
if (exactClaimPath) {
|
|
369
|
+
return exactClaimPath;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const foreignClaimsPresent = Boolean(
|
|
373
|
+
seatId &&
|
|
374
|
+
sessionName &&
|
|
375
|
+
cwdMatches.some((candidate) => {
|
|
376
|
+
const claim = readCodexSeatClaim(candidate.sessionId);
|
|
377
|
+
return claim?.sessionName === sessionName && claim?.seatId && claim.seatId !== seatId;
|
|
378
|
+
})
|
|
379
|
+
);
|
|
380
|
+
const allowedMatches = filterForeignClaimedCodexCandidates(cwdMatches, options);
|
|
381
|
+
if (allowedMatches.length === 0) {
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (!Number.isFinite(processStartedAtMs)) {
|
|
386
|
+
return allowedMatches.length === 1 ? allowedMatches[0].path : null;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const preciseMatches = rankCodexCandidates(
|
|
390
|
+
allowedMatches.filter((candidate) => {
|
|
391
|
+
const annotated = annotateSessionCandidates([candidate], processStartedAtMs)[0];
|
|
392
|
+
return (
|
|
393
|
+
Number.isFinite(annotated.diffMs) &&
|
|
394
|
+
Number.isFinite(annotated.relativeStartMs) &&
|
|
395
|
+
annotated.relativeStartMs >= -SESSION_START_EARLY_TOLERANCE_MS &&
|
|
396
|
+
annotated.relativeStartMs <= SESSION_MATCH_WINDOW_MS
|
|
397
|
+
);
|
|
398
|
+
}),
|
|
399
|
+
processStartedAtMs
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
const preciseClaimPath = selectClaimedCodexCandidatePath(preciseMatches, options);
|
|
403
|
+
if (preciseClaimPath) {
|
|
404
|
+
return preciseClaimPath;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const pairedSeatSelection = seatId && sessionName;
|
|
408
|
+
if (pairedSeatSelection && options.allowUnclaimedSingleCandidate === false && !foreignClaimsPresent) {
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (preciseMatches.length === 1) {
|
|
413
|
+
return selectStrictSingleCodexCandidatePath(preciseMatches, processStartedAtMs);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (allowedMatches.length === 1) {
|
|
417
|
+
return selectStrictSingleCodexCandidatePath(allowedMatches, processStartedAtMs);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
|
|
206
423
|
function selectCodexSessionFile(currentPath, processStartedAtMs, options = {}) {
|
|
207
|
-
const liveCandidates = readOpenSessionCandidates(options.pid, CODEX_ROOT, readCodexCandidate);
|
|
208
|
-
const livePath =
|
|
424
|
+
const liveCandidates = readOpenSessionCandidates(options.pids ?? options.pid, CODEX_ROOT, readCodexCandidate);
|
|
425
|
+
const livePath = selectCodexCandidatePath(liveCandidates, currentPath, processStartedAtMs, {
|
|
426
|
+
...options,
|
|
427
|
+
allowUnclaimedSingleCandidate: true,
|
|
428
|
+
});
|
|
209
429
|
if (livePath) {
|
|
210
430
|
return livePath;
|
|
211
431
|
}
|
|
@@ -214,7 +434,10 @@ function selectCodexSessionFile(currentPath, processStartedAtMs, options = {}) {
|
|
|
214
434
|
.map((filePath) => readCodexCandidate(filePath))
|
|
215
435
|
.filter((candidate) => candidate !== null);
|
|
216
436
|
|
|
217
|
-
return
|
|
437
|
+
return selectCodexCandidatePath(candidates, currentPath, processStartedAtMs, {
|
|
438
|
+
...options,
|
|
439
|
+
allowUnclaimedSingleCandidate: false,
|
|
440
|
+
});
|
|
218
441
|
}
|
|
219
442
|
|
|
220
443
|
function extractCodexAssistantText(content) {
|
|
@@ -236,14 +459,17 @@ function extractCodexAssistantText(content) {
|
|
|
236
459
|
.join("\n");
|
|
237
460
|
}
|
|
238
461
|
|
|
239
|
-
function
|
|
462
|
+
function parseCodexAssistantLine(line, options = {}) {
|
|
463
|
+
const flowMode = options.flowMode === true;
|
|
240
464
|
try {
|
|
241
465
|
const entry = JSON.parse(line);
|
|
242
466
|
if (entry?.type !== "response_item" || entry.payload?.type !== "message" || entry.payload?.role !== "assistant") {
|
|
243
467
|
return null;
|
|
244
468
|
}
|
|
245
469
|
|
|
246
|
-
|
|
470
|
+
const phase = String(entry.payload?.phase || "").trim().toLowerCase();
|
|
471
|
+
const relayablePhase = phase === "final_answer" || (flowMode && phase === "commentary");
|
|
472
|
+
if (!relayablePhase) {
|
|
247
473
|
return null;
|
|
248
474
|
}
|
|
249
475
|
|
|
@@ -262,6 +488,10 @@ function parseCodexFinalLine(line) {
|
|
|
262
488
|
}
|
|
263
489
|
}
|
|
264
490
|
|
|
491
|
+
function parseCodexFinalLine(line) {
|
|
492
|
+
return parseCodexAssistantLine(line, { flowMode: false });
|
|
493
|
+
}
|
|
494
|
+
|
|
265
495
|
function isAnswerNewEnough(answer, sinceMs = null) {
|
|
266
496
|
if (!Number.isFinite(sinceMs)) {
|
|
267
497
|
return true;
|
|
@@ -275,13 +505,13 @@ function isAnswerNewEnough(answer, sinceMs = null) {
|
|
|
275
505
|
return answerMs >= sinceMs;
|
|
276
506
|
}
|
|
277
507
|
|
|
278
|
-
function readCodexAnswers(filePath, offset, sinceMs = null) {
|
|
508
|
+
function readCodexAnswers(filePath, offset, sinceMs = null, options = {}) {
|
|
279
509
|
const { nextOffset, text } = readAppendedText(filePath, offset);
|
|
280
510
|
const answers = text
|
|
281
511
|
.split("\n")
|
|
282
512
|
.map((line) => line.trim())
|
|
283
513
|
.filter((line) => line.length > 0)
|
|
284
|
-
.map((line) =>
|
|
514
|
+
.map((line) => parseCodexAssistantLine(line, options))
|
|
285
515
|
.filter((entry) => entry !== null)
|
|
286
516
|
.filter((entry) => isAnswerNewEnough(entry, sinceMs));
|
|
287
517
|
|
|
@@ -311,7 +541,7 @@ function readClaudeCandidate(filePath) {
|
|
|
311
541
|
}
|
|
312
542
|
|
|
313
543
|
function selectClaudeSessionFile(currentPath, processStartedAtMs, options = {}) {
|
|
314
|
-
const liveCandidates = readOpenSessionCandidates(options.pid, CLAUDE_ROOT, readClaudeCandidate);
|
|
544
|
+
const liveCandidates = readOpenSessionCandidates(options.pids ?? options.pid, CLAUDE_ROOT, readClaudeCandidate);
|
|
315
545
|
const livePath = selectLiveSessionCandidatePath(liveCandidates, currentPath, options.captureSinceMs);
|
|
316
546
|
if (livePath) {
|
|
317
547
|
return livePath;
|
|
@@ -324,7 +554,8 @@ function selectClaudeSessionFile(currentPath, processStartedAtMs, options = {})
|
|
|
324
554
|
return selectSessionCandidatePath(candidates, currentPath, processStartedAtMs);
|
|
325
555
|
}
|
|
326
556
|
|
|
327
|
-
function extractClaudeAssistantText(content) {
|
|
557
|
+
function extractClaudeAssistantText(content, options = {}) {
|
|
558
|
+
const flowMode = options.flowMode === true;
|
|
328
559
|
if (!Array.isArray(content)) {
|
|
329
560
|
return "";
|
|
330
561
|
}
|
|
@@ -337,20 +568,28 @@ function extractClaudeAssistantText(content) {
|
|
|
337
568
|
if (item.type === "text" && typeof item.text === "string") {
|
|
338
569
|
return [item.text.trim()];
|
|
339
570
|
}
|
|
571
|
+
if (flowMode && item.type === "thinking" && typeof item.thinking === "string") {
|
|
572
|
+
return [item.thinking.trim()];
|
|
573
|
+
}
|
|
340
574
|
return [];
|
|
341
575
|
})
|
|
342
576
|
.filter((text) => text.length > 0)
|
|
343
577
|
.join("\n");
|
|
344
578
|
}
|
|
345
579
|
|
|
346
|
-
function
|
|
580
|
+
function parseClaudeAssistantLine(line, options = {}) {
|
|
581
|
+
const flowMode = options.flowMode === true;
|
|
347
582
|
try {
|
|
348
583
|
const entry = JSON.parse(line);
|
|
349
|
-
if (entry?.type !== "assistant" || entry.message?.role !== "assistant"
|
|
584
|
+
if (entry?.type !== "assistant" || entry.message?.role !== "assistant") {
|
|
350
585
|
return null;
|
|
351
586
|
}
|
|
352
587
|
|
|
353
|
-
|
|
588
|
+
if (!flowMode && entry.message?.stop_reason !== "end_turn") {
|
|
589
|
+
return null;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const text = sanitizeRelayText(extractClaudeAssistantText(entry.message.content, options));
|
|
354
593
|
if (!text) {
|
|
355
594
|
return null;
|
|
356
595
|
}
|
|
@@ -365,13 +604,17 @@ function parseClaudeFinalLine(line) {
|
|
|
365
604
|
}
|
|
366
605
|
}
|
|
367
606
|
|
|
368
|
-
function
|
|
607
|
+
function parseClaudeFinalLine(line) {
|
|
608
|
+
return parseClaudeAssistantLine(line, { flowMode: false });
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function readClaudeAnswers(filePath, offset, sinceMs = null, options = {}) {
|
|
369
612
|
const { nextOffset, text } = readAppendedText(filePath, offset);
|
|
370
613
|
const answers = text
|
|
371
614
|
.split("\n")
|
|
372
615
|
.map((line) => line.trim())
|
|
373
616
|
.filter((line) => line.length > 0)
|
|
374
|
-
.map((line) =>
|
|
617
|
+
.map((line) => parseClaudeAssistantLine(line, options))
|
|
375
618
|
.filter((entry) => entry !== null)
|
|
376
619
|
.filter((entry) => isAnswerNewEnough(entry, sinceMs));
|
|
377
620
|
|
|
@@ -397,7 +640,7 @@ function readGeminiCandidate(filePath) {
|
|
|
397
640
|
|
|
398
641
|
function selectGeminiSessionFile(currentPath, processStartedAtMs, options = {}) {
|
|
399
642
|
const projectHash = createHash("sha256").update(currentPath).digest("hex");
|
|
400
|
-
const liveCandidates = readOpenSessionCandidates(options.pid, GEMINI_ROOT, readGeminiCandidate)
|
|
643
|
+
const liveCandidates = readOpenSessionCandidates(options.pids ?? options.pid, GEMINI_ROOT, readGeminiCandidate)
|
|
401
644
|
.filter((candidate) => candidate.projectHash === projectHash);
|
|
402
645
|
const livePath = selectLiveSessionCandidatePath(liveCandidates, projectHash, options.captureSinceMs);
|
|
403
646
|
if (livePath) {
|
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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const fs = require("node:fs");
|
|
2
2
|
const { execFileSync } = require("node:child_process");
|
|
3
|
+
const path = require("node:path");
|
|
3
4
|
const pty = require("node-pty");
|
|
4
5
|
|
|
5
6
|
const {
|
|
@@ -36,8 +37,23 @@ const {
|
|
|
36
37
|
|
|
37
38
|
const TYPE_DELAY_MS = 70;
|
|
38
39
|
const MIRROR_SUPPRESSION_WINDOW_MS = 30 * 1000;
|
|
40
|
+
const PENDING_RELAY_CONTEXT_TTL_MS = 2 * 60 * 1000;
|
|
41
|
+
const EMITTED_ANSWER_TTL_MS = 5 * 60 * 1000;
|
|
39
42
|
const MAX_RECENT_INBOUND_RELAYS = 12;
|
|
43
|
+
const MAX_RECENT_EMITTED_ANSWERS = 48;
|
|
44
|
+
const MAX_RELAY_CHAIN_HOP = 1;
|
|
40
45
|
const STOP_FORCE_KILL_MS = 1200;
|
|
46
|
+
const SEAT_JOIN_WAIT_MS = 3000;
|
|
47
|
+
const SEAT_JOIN_POLL_MS = 60;
|
|
48
|
+
const CHILD_ENV_DROP_KEYS = [
|
|
49
|
+
"CODEX_CI",
|
|
50
|
+
"CODEX_MANAGED_BY_NPM",
|
|
51
|
+
"CODEX_THREAD_ID",
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
function normalizeFlowMode(flowMode) {
|
|
55
|
+
return String(flowMode || "").trim().toLowerCase() === "on" ? "on" : "off";
|
|
56
|
+
}
|
|
41
57
|
|
|
42
58
|
function resolveShell() {
|
|
43
59
|
const shell = String(process.env.SHELL || "").trim();
|
|
@@ -52,16 +68,139 @@ function resolveShellArgs(shellPath) {
|
|
|
52
68
|
return [];
|
|
53
69
|
}
|
|
54
70
|
|
|
55
|
-
function resolveChildTerm() {
|
|
56
|
-
const inherited = String(
|
|
71
|
+
function resolveChildTerm(sourceEnv = process.env) {
|
|
72
|
+
const inherited = String(sourceEnv.TERM || "").trim();
|
|
57
73
|
if (inherited && inherited.toLowerCase() !== "dumb") {
|
|
58
74
|
return inherited;
|
|
59
75
|
}
|
|
60
76
|
return "xterm-256color";
|
|
61
77
|
}
|
|
62
78
|
|
|
63
|
-
function
|
|
64
|
-
|
|
79
|
+
function sanitizeChildPath(pathValue, homeDir) {
|
|
80
|
+
const arg0Root = path.join(homeDir, ".codex", "tmp", "arg0");
|
|
81
|
+
const entries = String(pathValue || "")
|
|
82
|
+
.split(path.delimiter)
|
|
83
|
+
.filter(Boolean)
|
|
84
|
+
.filter((entry) => {
|
|
85
|
+
const resolved = path.resolve(entry);
|
|
86
|
+
return resolved !== arg0Root && !resolved.startsWith(`${arg0Root}${path.sep}`);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return entries.join(path.delimiter);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function buildChildEnv(seatId, sessionName, cwd, baseEnv = process.env) {
|
|
93
|
+
const env = { ...baseEnv };
|
|
94
|
+
for (const key of CHILD_ENV_DROP_KEYS) {
|
|
95
|
+
delete env[key];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const homeDir = String(env.HOME || "").trim() || process.env.HOME || "/root";
|
|
99
|
+
env.PATH = sanitizeChildPath(env.PATH, homeDir);
|
|
100
|
+
env.PWD = cwd;
|
|
101
|
+
env.TERM = resolveChildTerm(baseEnv);
|
|
102
|
+
env.MUUUUSE_SEAT = String(seatId);
|
|
103
|
+
env.MUUUUSE_SESSION = sessionName;
|
|
104
|
+
return env;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function normalizeWorkingPath(currentPath = process.cwd()) {
|
|
108
|
+
try {
|
|
109
|
+
return fs.realpathSync(currentPath);
|
|
110
|
+
} catch {
|
|
111
|
+
return path.resolve(currentPath);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function matchesWorkingPath(leftPath, rightPath) {
|
|
116
|
+
if (!leftPath || !rightPath) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return normalizeWorkingPath(leftPath) === normalizeWorkingPath(rightPath);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function createSessionName(currentPath = process.cwd()) {
|
|
124
|
+
return `${getDefaultSessionName(currentPath)}-${createId(6)}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function sleepSync(ms) {
|
|
128
|
+
if (!Number.isFinite(ms) || ms <= 0) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function findJoinableSessionName(currentPath = process.cwd()) {
|
|
136
|
+
const candidates = listSessionNames()
|
|
137
|
+
.map((sessionName) => {
|
|
138
|
+
const sessionPaths = getSessionPaths(sessionName);
|
|
139
|
+
const controller = readJson(sessionPaths.controllerPath, null);
|
|
140
|
+
const seat1Paths = getSeatPaths(sessionName, 1);
|
|
141
|
+
const seat2Paths = getSeatPaths(sessionName, 2);
|
|
142
|
+
const seat1Meta = readJson(seat1Paths.metaPath, null);
|
|
143
|
+
const seat1Status = readJson(seat1Paths.statusPath, null);
|
|
144
|
+
const seat2Meta = readJson(seat2Paths.metaPath, null);
|
|
145
|
+
const seat2Status = readJson(seat2Paths.statusPath, null);
|
|
146
|
+
const stopRequest = readJson(sessionPaths.stopPath, null);
|
|
147
|
+
|
|
148
|
+
const cwd = controller?.cwd || seat1Status?.cwd || seat1Meta?.cwd || seat2Status?.cwd || seat2Meta?.cwd || null;
|
|
149
|
+
if (!matchesWorkingPath(cwd, currentPath)) {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const seat1WrapperPid = seat1Status?.pid || seat1Meta?.pid || null;
|
|
154
|
+
const seat1ChildPid = seat1Status?.childPid || seat1Meta?.childPid || null;
|
|
155
|
+
const seat2WrapperPid = seat2Status?.pid || seat2Meta?.pid || null;
|
|
156
|
+
const seat2ChildPid = seat2Status?.childPid || seat2Meta?.childPid || null;
|
|
157
|
+
const seat1Live = isPidAlive(seat1WrapperPid) || isPidAlive(seat1ChildPid);
|
|
158
|
+
const seat2Live = isPidAlive(seat2WrapperPid) || isPidAlive(seat2ChildPid);
|
|
159
|
+
const stopRequestedAtMs = Date.parse(stopRequest?.requestedAt || "");
|
|
160
|
+
const createdAtMs = Date.parse(controller?.createdAt || seat1Meta?.startedAt || seat1Status?.updatedAt || "");
|
|
161
|
+
|
|
162
|
+
if (!seat1Live || seat2Live) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (Number.isFinite(stopRequestedAtMs) && Number.isFinite(createdAtMs) && stopRequestedAtMs > createdAtMs) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
sessionName,
|
|
172
|
+
createdAtMs,
|
|
173
|
+
};
|
|
174
|
+
})
|
|
175
|
+
.filter((entry) => entry !== null)
|
|
176
|
+
.sort((left, right) => right.createdAtMs - left.createdAtMs);
|
|
177
|
+
|
|
178
|
+
return candidates[0]?.sessionName || null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function waitForJoinableSessionName(currentPath = process.cwd(), timeoutMs = SEAT_JOIN_WAIT_MS) {
|
|
182
|
+
const deadline = Date.now() + timeoutMs;
|
|
183
|
+
while (Date.now() <= deadline) {
|
|
184
|
+
const sessionName = findJoinableSessionName(currentPath);
|
|
185
|
+
if (sessionName) {
|
|
186
|
+
return sessionName;
|
|
187
|
+
}
|
|
188
|
+
sleepSync(SEAT_JOIN_POLL_MS);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function resolveSessionName(currentPath = process.cwd(), seatId = 1) {
|
|
195
|
+
if (seatId === 1) {
|
|
196
|
+
return createSessionName(currentPath);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (seatId === 2) {
|
|
200
|
+
return waitForJoinableSessionName(currentPath);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return createSessionName(currentPath);
|
|
65
204
|
}
|
|
66
205
|
|
|
67
206
|
function parseAnswerEntries(text) {
|
|
@@ -79,6 +218,51 @@ function parseAnswerEntries(text) {
|
|
|
79
218
|
.filter((entry) => entry && entry.type === "answer" && typeof entry.text === "string");
|
|
80
219
|
}
|
|
81
220
|
|
|
221
|
+
function readSessionHeaderText(filePath, maxBytes = 16384) {
|
|
222
|
+
try {
|
|
223
|
+
const fd = fs.openSync(filePath, "r");
|
|
224
|
+
try {
|
|
225
|
+
const buffer = Buffer.alloc(maxBytes);
|
|
226
|
+
const bytesRead = fs.readSync(fd, buffer, 0, buffer.length, 0);
|
|
227
|
+
return buffer.toString("utf8", 0, bytesRead);
|
|
228
|
+
} finally {
|
|
229
|
+
fs.closeSync(fd);
|
|
230
|
+
}
|
|
231
|
+
} catch {
|
|
232
|
+
return "";
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function readSessionFileStartedAtMs(agentType, filePath) {
|
|
237
|
+
try {
|
|
238
|
+
if (agentType === "gemini") {
|
|
239
|
+
const entry = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
240
|
+
return Date.parse(entry.startTime || entry.lastUpdated || "");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const header = readSessionHeaderText(filePath);
|
|
244
|
+
const lines = header
|
|
245
|
+
.split("\n")
|
|
246
|
+
.map((line) => line.trim())
|
|
247
|
+
.filter(Boolean);
|
|
248
|
+
|
|
249
|
+
for (const line of lines) {
|
|
250
|
+
const entry = JSON.parse(line);
|
|
251
|
+
if (agentType === "codex" && entry?.type === "session_meta") {
|
|
252
|
+
return Date.parse(entry.payload?.timestamp || "");
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (agentType === "claude") {
|
|
256
|
+
return Date.parse(entry.timestamp || entry.message?.timestamp || "");
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
} catch {
|
|
260
|
+
return Number.NaN;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return Number.NaN;
|
|
264
|
+
}
|
|
265
|
+
|
|
82
266
|
function readProcessCwd(pid) {
|
|
83
267
|
if (!Number.isInteger(pid) || pid <= 0) {
|
|
84
268
|
return null;
|
|
@@ -121,20 +305,25 @@ function getChildProcesses(rootPid) {
|
|
|
121
305
|
.filter((entry) => entry !== null);
|
|
122
306
|
|
|
123
307
|
const descendants = [];
|
|
124
|
-
const queue = [rootPid];
|
|
125
|
-
const seen = new Set(
|
|
308
|
+
const queue = [{ pid: rootPid, depth: 0 }];
|
|
309
|
+
const seen = new Set([rootPid]);
|
|
126
310
|
|
|
127
311
|
while (queue.length > 0) {
|
|
128
|
-
const
|
|
312
|
+
const current = queue.shift();
|
|
313
|
+
const parentPid = current.pid;
|
|
129
314
|
for (const process of processes) {
|
|
130
315
|
if (process.ppid !== parentPid || seen.has(process.pid)) {
|
|
131
316
|
continue;
|
|
132
317
|
}
|
|
133
318
|
seen.add(process.pid);
|
|
134
|
-
queue.push(
|
|
319
|
+
queue.push({
|
|
320
|
+
pid: process.pid,
|
|
321
|
+
depth: current.depth + 1,
|
|
322
|
+
});
|
|
135
323
|
descendants.push({
|
|
136
324
|
...process,
|
|
137
325
|
cwd: readProcessCwd(process.pid),
|
|
326
|
+
depth: current.depth + 1,
|
|
138
327
|
});
|
|
139
328
|
}
|
|
140
329
|
}
|
|
@@ -145,14 +334,40 @@ function getChildProcesses(rootPid) {
|
|
|
145
334
|
}
|
|
146
335
|
}
|
|
147
336
|
|
|
148
|
-
function
|
|
337
|
+
function getProcessFamilyPids(processes, rootPid) {
|
|
338
|
+
if (!Number.isInteger(rootPid) || rootPid <= 0) {
|
|
339
|
+
return [];
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const related = new Set([rootPid]);
|
|
343
|
+
const queue = [rootPid];
|
|
344
|
+
|
|
345
|
+
while (queue.length > 0) {
|
|
346
|
+
const parentPid = queue.shift();
|
|
347
|
+
for (const process of processes) {
|
|
348
|
+
if (process.ppid !== parentPid || related.has(process.pid)) {
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
related.add(process.pid);
|
|
353
|
+
queue.push(process.pid);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return [...related];
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function resolveSessionFile(agentType, agentPid, currentPath, captureSinceMs, processStartedAtMs, seatContext = {}) {
|
|
149
361
|
if (!currentPath) {
|
|
150
362
|
return null;
|
|
151
363
|
}
|
|
152
364
|
|
|
153
365
|
const options = {
|
|
154
366
|
pid: agentPid,
|
|
367
|
+
pids: seatContext.agentPids,
|
|
155
368
|
captureSinceMs,
|
|
369
|
+
seatId: seatContext.seatId,
|
|
370
|
+
sessionName: seatContext.sessionName,
|
|
156
371
|
};
|
|
157
372
|
|
|
158
373
|
if (agentType === "codex") {
|
|
@@ -192,6 +407,8 @@ function buildAnswerSignaturePayload(sessionName, challenge, entry) {
|
|
|
192
407
|
type: "muuuuse_answer",
|
|
193
408
|
sessionName,
|
|
194
409
|
challenge,
|
|
410
|
+
chainId: entry.chainId,
|
|
411
|
+
hop: entry.hop,
|
|
195
412
|
id: entry.id,
|
|
196
413
|
seatId: entry.seatId,
|
|
197
414
|
origin: entry.origin,
|
|
@@ -219,35 +436,23 @@ function readSeatChallenge(paths, sessionName) {
|
|
|
219
436
|
}
|
|
220
437
|
|
|
221
438
|
async function sendTextAndEnter(child, text, shouldAbort = () => false) {
|
|
222
|
-
const
|
|
439
|
+
const payload = String(text || "")
|
|
440
|
+
.replace(/\r/g, "")
|
|
441
|
+
.replace(/\s*\n+\s*/g, " ")
|
|
442
|
+
.replace(/[ \t]{2,}/g, " ")
|
|
443
|
+
.trim();
|
|
223
444
|
|
|
224
|
-
|
|
445
|
+
if (payload.length > 0) {
|
|
225
446
|
if (shouldAbort() || !child) {
|
|
226
447
|
return false;
|
|
227
448
|
}
|
|
228
449
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
} catch {
|
|
234
|
-
return false;
|
|
235
|
-
}
|
|
236
|
-
await sleep(TYPE_DELAY_MS);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
if (index < lines.length - 1) {
|
|
240
|
-
if (shouldAbort()) {
|
|
241
|
-
return false;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
try {
|
|
245
|
-
child.write("\r");
|
|
246
|
-
} catch {
|
|
247
|
-
return false;
|
|
248
|
-
}
|
|
249
|
-
await sleep(TYPE_DELAY_MS);
|
|
450
|
+
try {
|
|
451
|
+
child.write(payload);
|
|
452
|
+
} catch {
|
|
453
|
+
return false;
|
|
250
454
|
}
|
|
455
|
+
await sleep(TYPE_DELAY_MS);
|
|
251
456
|
}
|
|
252
457
|
|
|
253
458
|
if (shouldAbort() || !child) {
|
|
@@ -267,8 +472,12 @@ class ArmedSeat {
|
|
|
267
472
|
constructor(options) {
|
|
268
473
|
this.seatId = options.seatId;
|
|
269
474
|
this.partnerSeatId = options.seatId === 1 ? 2 : 1;
|
|
270
|
-
this.
|
|
271
|
-
this.
|
|
475
|
+
this.flowMode = normalizeFlowMode(options.flowMode);
|
|
476
|
+
this.cwd = normalizeWorkingPath(options.cwd);
|
|
477
|
+
this.sessionName = resolveSessionName(this.cwd, this.seatId);
|
|
478
|
+
if (!this.sessionName) {
|
|
479
|
+
throw new Error("No armed `muuuuse 1` seat is waiting in this cwd. Run `muuuuse 1` first.");
|
|
480
|
+
}
|
|
272
481
|
this.sessionPaths = getSessionPaths(this.sessionName);
|
|
273
482
|
this.paths = getSeatPaths(this.sessionName, this.seatId);
|
|
274
483
|
this.partnerPaths = getSeatPaths(this.sessionName, this.partnerSeatId);
|
|
@@ -286,7 +495,10 @@ class ArmedSeat {
|
|
|
286
495
|
this.resizeCleanup = null;
|
|
287
496
|
this.forceKillTimer = null;
|
|
288
497
|
this.identity = null;
|
|
498
|
+
this.lastUserInputAtMs = 0;
|
|
499
|
+
this.pendingInboundContext = null;
|
|
289
500
|
this.recentInboundRelays = [];
|
|
501
|
+
this.recentEmittedAnswers = [];
|
|
290
502
|
this.trustState = {
|
|
291
503
|
challenge: null,
|
|
292
504
|
peerPublicKey: null,
|
|
@@ -306,6 +518,20 @@ class ArmedSeat {
|
|
|
306
518
|
};
|
|
307
519
|
}
|
|
308
520
|
|
|
521
|
+
writeController(extra = {}) {
|
|
522
|
+
const current = readJson(this.sessionPaths.controllerPath, {});
|
|
523
|
+
writeJson(this.sessionPaths.controllerPath, {
|
|
524
|
+
sessionName: this.sessionName,
|
|
525
|
+
cwd: this.cwd,
|
|
526
|
+
createdAt: current.createdAt || this.startedAt,
|
|
527
|
+
updatedAt: new Date().toISOString(),
|
|
528
|
+
seat1Pid: this.seatId === 1 ? process.pid : current.seat1Pid || null,
|
|
529
|
+
seat2Pid: this.seatId === 2 ? process.pid : current.seat2Pid || null,
|
|
530
|
+
pid: this.seatId === 1 ? process.pid : current.pid || null,
|
|
531
|
+
...extra,
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
|
|
309
535
|
log(message) {
|
|
310
536
|
process.stderr.write(`${message}\n`);
|
|
311
537
|
}
|
|
@@ -315,6 +541,7 @@ class ArmedSeat {
|
|
|
315
541
|
seatId: this.seatId,
|
|
316
542
|
partnerSeatId: this.partnerSeatId,
|
|
317
543
|
sessionName: this.sessionName,
|
|
544
|
+
flowMode: this.flowMode,
|
|
318
545
|
cwd: this.cwd,
|
|
319
546
|
pid: process.pid,
|
|
320
547
|
childPid: this.childPid,
|
|
@@ -329,6 +556,7 @@ class ArmedSeat {
|
|
|
329
556
|
seatId: this.seatId,
|
|
330
557
|
partnerSeatId: this.partnerSeatId,
|
|
331
558
|
sessionName: this.sessionName,
|
|
559
|
+
flowMode: this.flowMode,
|
|
332
560
|
cwd: this.cwd,
|
|
333
561
|
pid: process.pid,
|
|
334
562
|
childPid: this.childPid,
|
|
@@ -509,20 +737,17 @@ class ArmedSeat {
|
|
|
509
737
|
fs.rmSync(this.paths.pipePath, { force: true });
|
|
510
738
|
clearStaleStopRequest(this.sessionPaths.stopPath, this.startedAtMs);
|
|
511
739
|
this.initializeTrustMaterial();
|
|
740
|
+
this.writeController();
|
|
512
741
|
|
|
513
742
|
const shell = resolveShell();
|
|
514
743
|
const shellArgs = resolveShellArgs(shell);
|
|
744
|
+
const childEnv = buildChildEnv(this.seatId, this.sessionName, this.cwd);
|
|
515
745
|
this.child = pty.spawn(shell, shellArgs, {
|
|
516
746
|
cols: process.stdout.columns || 120,
|
|
517
747
|
rows: process.stdout.rows || 36,
|
|
518
748
|
cwd: this.cwd,
|
|
519
|
-
env:
|
|
520
|
-
|
|
521
|
-
TERM: resolveChildTerm(),
|
|
522
|
-
MUUUUSE_SEAT: String(this.seatId),
|
|
523
|
-
MUUUUSE_SESSION: this.sessionName,
|
|
524
|
-
},
|
|
525
|
-
name: resolveChildTerm(),
|
|
749
|
+
env: childEnv,
|
|
750
|
+
name: childEnv.TERM,
|
|
526
751
|
});
|
|
527
752
|
|
|
528
753
|
this.childPid = this.child.pid;
|
|
@@ -547,6 +772,8 @@ class ArmedSeat {
|
|
|
547
772
|
|
|
548
773
|
installStdinProxy() {
|
|
549
774
|
const handleData = (chunk) => {
|
|
775
|
+
this.lastUserInputAtMs = Date.now();
|
|
776
|
+
this.pendingInboundContext = null;
|
|
550
777
|
if (!this.child) {
|
|
551
778
|
return;
|
|
552
779
|
}
|
|
@@ -687,6 +914,8 @@ class ArmedSeat {
|
|
|
687
914
|
|
|
688
915
|
const payload = sanitizeRelayText(entry.text);
|
|
689
916
|
const signaturePayload = buildAnswerSignaturePayload(this.sessionName, this.trustState.challenge, {
|
|
917
|
+
chainId: entry.chainId || entry.id,
|
|
918
|
+
hop: Number.isInteger(entry.hop) ? entry.hop : 0,
|
|
690
919
|
id: entry.id,
|
|
691
920
|
seatId: entry.seatId,
|
|
692
921
|
origin: entry.origin || "unknown",
|
|
@@ -718,6 +947,14 @@ class ArmedSeat {
|
|
|
718
947
|
return;
|
|
719
948
|
}
|
|
720
949
|
|
|
950
|
+
const deliveredAtMs = Date.now();
|
|
951
|
+
this.pendingInboundContext = {
|
|
952
|
+
chainId: entry.chainId || entry.id,
|
|
953
|
+
deliveredAtMs,
|
|
954
|
+
expiresAtMs: deliveredAtMs + PENDING_RELAY_CONTEXT_TTL_MS,
|
|
955
|
+
hop: Number.isInteger(entry.hop) ? entry.hop : 0,
|
|
956
|
+
relayUsed: false,
|
|
957
|
+
};
|
|
721
958
|
this.relayCount += 1;
|
|
722
959
|
this.rememberInboundRelay(payload);
|
|
723
960
|
this.log(`[${this.partnerSeatId} -> ${this.seatId}] ${previewText(payload)}`);
|
|
@@ -749,6 +986,37 @@ class ArmedSeat {
|
|
|
749
986
|
);
|
|
750
987
|
}
|
|
751
988
|
|
|
989
|
+
pruneRecentEmittedAnswers(now = Date.now()) {
|
|
990
|
+
this.recentEmittedAnswers = this.recentEmittedAnswers.filter(
|
|
991
|
+
(entry) => now - entry.timestampMs <= EMITTED_ANSWER_TTL_MS
|
|
992
|
+
);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
hasRecentEmittedAnswer(answerKey) {
|
|
996
|
+
if (!answerKey) {
|
|
997
|
+
return false;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
this.pruneRecentEmittedAnswers();
|
|
1001
|
+
return this.recentEmittedAnswers.some((entry) => entry.key === answerKey);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
rememberEmittedAnswer(answerKey) {
|
|
1005
|
+
if (!answerKey) {
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
this.pruneRecentEmittedAnswers();
|
|
1010
|
+
this.recentEmittedAnswers.push({
|
|
1011
|
+
key: answerKey,
|
|
1012
|
+
timestampMs: Date.now(),
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
if (this.recentEmittedAnswers.length > MAX_RECENT_EMITTED_ANSWERS) {
|
|
1016
|
+
this.recentEmittedAnswers = this.recentEmittedAnswers.slice(-MAX_RECENT_EMITTED_ANSWERS);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
|
|
752
1020
|
takeMirroredInboundRelay(payload) {
|
|
753
1021
|
const normalized = sanitizeRelayText(payload);
|
|
754
1022
|
if (!normalized) {
|
|
@@ -766,8 +1034,28 @@ class ArmedSeat {
|
|
|
766
1034
|
return match;
|
|
767
1035
|
}
|
|
768
1036
|
|
|
1037
|
+
getPendingInboundContext() {
|
|
1038
|
+
const context = this.pendingInboundContext;
|
|
1039
|
+
if (!context) {
|
|
1040
|
+
return null;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
if (context.expiresAtMs <= Date.now()) {
|
|
1044
|
+
this.pendingInboundContext = null;
|
|
1045
|
+
return null;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
if (this.lastUserInputAtMs > context.deliveredAtMs) {
|
|
1049
|
+
this.pendingInboundContext = null;
|
|
1050
|
+
return null;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
return context;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
769
1056
|
collectLiveAnswers() {
|
|
770
|
-
const
|
|
1057
|
+
const childProcesses = getChildProcesses(this.childPid);
|
|
1058
|
+
const detectedAgent = detectAgent(childProcesses);
|
|
771
1059
|
if (!detectedAgent) {
|
|
772
1060
|
this.liveState = {
|
|
773
1061
|
type: null,
|
|
@@ -813,18 +1101,27 @@ class ArmedSeat {
|
|
|
813
1101
|
};
|
|
814
1102
|
}
|
|
815
1103
|
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
1104
|
+
const agentPids = getProcessFamilyPids(childProcesses, detectedAgent.pid);
|
|
1105
|
+
const resolvedSessionFile = resolveSessionFile(
|
|
1106
|
+
detectedAgent.type,
|
|
1107
|
+
detectedAgent.pid,
|
|
1108
|
+
currentPath,
|
|
1109
|
+
this.liveState.captureSinceMs,
|
|
1110
|
+
detectedAgent.processStartedAtMs,
|
|
1111
|
+
{
|
|
1112
|
+
agentPids,
|
|
1113
|
+
seatId: this.seatId,
|
|
1114
|
+
sessionName: this.sessionName,
|
|
1115
|
+
}
|
|
1116
|
+
);
|
|
824
1117
|
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
1118
|
+
if (resolvedSessionFile && resolvedSessionFile !== this.liveState.sessionFile) {
|
|
1119
|
+
this.liveState.sessionFile = resolvedSessionFile;
|
|
1120
|
+
this.liveState.offset = 0;
|
|
1121
|
+
this.liveState.lastMessageId = null;
|
|
1122
|
+
const sessionStartedAtMs = readSessionFileStartedAtMs(detectedAgent.type, resolvedSessionFile);
|
|
1123
|
+
if (Number.isFinite(sessionStartedAtMs)) {
|
|
1124
|
+
this.liveState.captureSinceMs = Math.min(this.liveState.captureSinceMs, sessionStartedAtMs);
|
|
828
1125
|
}
|
|
829
1126
|
}
|
|
830
1127
|
|
|
@@ -843,7 +1140,8 @@ class ArmedSeat {
|
|
|
843
1140
|
const result = readCodexAnswers(
|
|
844
1141
|
this.liveState.sessionFile,
|
|
845
1142
|
this.liveState.offset,
|
|
846
|
-
this.liveState.captureSinceMs
|
|
1143
|
+
this.liveState.captureSinceMs,
|
|
1144
|
+
{ flowMode: this.flowMode === "on" }
|
|
847
1145
|
);
|
|
848
1146
|
this.liveState.offset = result.nextOffset;
|
|
849
1147
|
answers.push(...result.answers);
|
|
@@ -851,7 +1149,8 @@ class ArmedSeat {
|
|
|
851
1149
|
const result = readClaudeAnswers(
|
|
852
1150
|
this.liveState.sessionFile,
|
|
853
1151
|
this.liveState.offset,
|
|
854
|
-
this.liveState.captureSinceMs
|
|
1152
|
+
this.liveState.captureSinceMs,
|
|
1153
|
+
{ flowMode: this.flowMode === "on" }
|
|
855
1154
|
);
|
|
856
1155
|
this.liveState.offset = result.nextOffset;
|
|
857
1156
|
answers.push(...result.answers);
|
|
@@ -859,7 +1158,8 @@ class ArmedSeat {
|
|
|
859
1158
|
const result = readGeminiAnswers(
|
|
860
1159
|
this.liveState.sessionFile,
|
|
861
1160
|
this.liveState.lastMessageId,
|
|
862
|
-
this.liveState.captureSinceMs
|
|
1161
|
+
this.liveState.captureSinceMs,
|
|
1162
|
+
{ flowMode: this.flowMode === "on" }
|
|
863
1163
|
);
|
|
864
1164
|
this.liveState.lastMessageId = result.lastMessageId;
|
|
865
1165
|
this.liveState.offset = result.fileSize;
|
|
@@ -895,19 +1195,39 @@ class ArmedSeat {
|
|
|
895
1195
|
return;
|
|
896
1196
|
}
|
|
897
1197
|
|
|
1198
|
+
const answerKey = buildAnswerKey(entry, payload);
|
|
1199
|
+
if (this.hasRecentEmittedAnswer(answerKey)) {
|
|
1200
|
+
this.log(`[${this.seatId}] suppressed duplicate final answer: ${previewText(payload)}`);
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
898
1204
|
const mirroredInbound = this.takeMirroredInboundRelay(payload);
|
|
899
1205
|
if (mirroredInbound) {
|
|
900
1206
|
this.log(`[${this.seatId}] suppressed mirrored relay: ${previewText(payload)}`);
|
|
901
1207
|
return;
|
|
902
1208
|
}
|
|
903
1209
|
|
|
1210
|
+
const pendingInboundContext = this.getPendingInboundContext();
|
|
1211
|
+
if (this.flowMode !== "on" && pendingInboundContext && pendingInboundContext.hop >= MAX_RELAY_CHAIN_HOP) {
|
|
1212
|
+
this.log(`[${this.seatId}] suppressed relay loop: ${previewText(payload)}`);
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
if (pendingInboundContext?.relayUsed) {
|
|
1217
|
+
this.log(`[${this.seatId}] suppressed extra queued relay output: ${previewText(payload)}`);
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
const entryId = entry.id || createId(12);
|
|
904
1222
|
const signedEntry = {
|
|
905
|
-
id:
|
|
1223
|
+
id: entryId,
|
|
906
1224
|
type: "answer",
|
|
907
1225
|
seatId: this.seatId,
|
|
908
1226
|
origin: entry.origin || "unknown",
|
|
909
1227
|
text: payload,
|
|
910
1228
|
createdAt: entry.createdAt || new Date().toISOString(),
|
|
1229
|
+
chainId: pendingInboundContext?.chainId || entry.chainId || entryId,
|
|
1230
|
+
hop: pendingInboundContext ? pendingInboundContext.hop + 1 : 0,
|
|
911
1231
|
challenge: this.trustState.challenge,
|
|
912
1232
|
publicKey: this.identity.publicKey,
|
|
913
1233
|
};
|
|
@@ -916,6 +1236,10 @@ class ArmedSeat {
|
|
|
916
1236
|
this.identity.privateKey
|
|
917
1237
|
);
|
|
918
1238
|
appendJsonl(this.paths.eventsPath, signedEntry);
|
|
1239
|
+
this.rememberEmittedAnswer(answerKey);
|
|
1240
|
+
if (pendingInboundContext) {
|
|
1241
|
+
pendingInboundContext.relayUsed = true;
|
|
1242
|
+
}
|
|
919
1243
|
|
|
920
1244
|
this.log(`[${this.seatId}] ${previewText(payload)}`);
|
|
921
1245
|
}
|
|
@@ -946,6 +1270,7 @@ class ArmedSeat {
|
|
|
946
1270
|
this.writeStatus({
|
|
947
1271
|
state: live.state,
|
|
948
1272
|
agent: live.agent,
|
|
1273
|
+
flowMode: this.flowMode,
|
|
949
1274
|
cwd: live.cwd,
|
|
950
1275
|
log: live.log,
|
|
951
1276
|
lastAnswerAt: live.lastAnswerAt,
|
|
@@ -962,7 +1287,8 @@ class ArmedSeat {
|
|
|
962
1287
|
this.installResizeHandler();
|
|
963
1288
|
|
|
964
1289
|
this.log(`${BRAND} seat ${this.seatId} armed for ${this.sessionName}.`);
|
|
965
|
-
this.log("Use this shell normally. Codex, Claude, and Gemini
|
|
1290
|
+
this.log("Use this shell normally. Codex, Claude, and Gemini relay automatically from their local session logs.");
|
|
1291
|
+
this.log(`Seat ${this.seatId} relay mode is flow ${this.flowMode}.`);
|
|
966
1292
|
if (this.seatId === 1) {
|
|
967
1293
|
this.log("Seat 1 generated the session key and is waiting for seat 2 to sign it.");
|
|
968
1294
|
} else {
|
|
@@ -1028,6 +1354,17 @@ function previewText(text, maxLength = 88) {
|
|
|
1028
1354
|
return `${compact.slice(0, maxLength - 3)}...`;
|
|
1029
1355
|
}
|
|
1030
1356
|
|
|
1357
|
+
function buildAnswerKey(entry, payload) {
|
|
1358
|
+
const origin = String(entry.origin || "unknown").trim() || "unknown";
|
|
1359
|
+
const id = typeof entry.id === "string" ? entry.id.trim() : "";
|
|
1360
|
+
if (id) {
|
|
1361
|
+
return `${origin}:${id}`;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
const createdAt = typeof entry.createdAt === "string" ? entry.createdAt : "";
|
|
1365
|
+
return `${origin}:${createdAt}:${hashText(payload)}`;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1031
1368
|
function buildSeatReport(sessionName, seatId) {
|
|
1032
1369
|
const paths = getSeatPaths(sessionName, seatId);
|
|
1033
1370
|
const daemon = readJson(paths.daemonPath, null);
|
|
@@ -1051,6 +1388,7 @@ function buildSeatReport(sessionName, seatId) {
|
|
|
1051
1388
|
return {
|
|
1052
1389
|
seatId,
|
|
1053
1390
|
state: wrapperLive ? status?.state || "running" : "orphaned_child",
|
|
1391
|
+
flowMode: status?.flowMode || meta?.flowMode || "off",
|
|
1054
1392
|
wrapperPid,
|
|
1055
1393
|
childPid,
|
|
1056
1394
|
wrapperLive,
|
|
@@ -1136,6 +1474,7 @@ function stopAllSessions() {
|
|
|
1136
1474
|
|
|
1137
1475
|
module.exports = {
|
|
1138
1476
|
ArmedSeat,
|
|
1477
|
+
buildChildEnv,
|
|
1139
1478
|
getStatusReport,
|
|
1140
1479
|
resolveSessionName,
|
|
1141
1480
|
stopAllSessions,
|
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.",
|