muuuuse 1.3.0 → 1.3.2
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 +6 -6
- package/package.json +1 -1
- package/src/agents.js +71 -6
- package/src/cli.js +76 -23
- package/src/runtime.js +101 -14
- package/src/util.js +4 -4
package/README.md
CHANGED
|
@@ -15,12 +15,12 @@ The main flow is:
|
|
|
15
15
|
```bash
|
|
16
16
|
muuuuse 1 <program...>
|
|
17
17
|
muuuuse 2 <program...>
|
|
18
|
-
muuuuse
|
|
18
|
+
muuuuse stop
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
Seat `1` and seat `2` each launch and own a real local program under a PTY wrapper. Once both seats are alive in the same lane, they automatically bounce final blocks between each other by typing the partner answer plus `Enter` into the wrapped program.
|
|
22
22
|
|
|
23
|
-
`muuuuse
|
|
23
|
+
`muuuuse stop` is the real cleanup command. With no flags it force-stops every tracked lane. Use `--session <name>` if you only want one explicit lane.
|
|
24
24
|
|
|
25
25
|
## Install
|
|
26
26
|
|
|
@@ -45,7 +45,7 @@ muuuuse 2 gemini
|
|
|
45
45
|
Terminal 3:
|
|
46
46
|
|
|
47
47
|
```bash
|
|
48
|
-
muuuuse
|
|
48
|
+
muuuuse stop
|
|
49
49
|
```
|
|
50
50
|
|
|
51
51
|
Known presets expand to recommended flags automatically:
|
|
@@ -69,7 +69,7 @@ muuuuse 2 bash -lc 'while read line; do printf "right: %s\n\n" "$line"; done'
|
|
|
69
69
|
|
|
70
70
|
Type into one seat and the other seat will receive the relayed block.
|
|
71
71
|
|
|
72
|
-
For Codex, Claude, and Gemini, `🔌Muuuuse`
|
|
72
|
+
For Codex, Claude, and Gemini, `🔌Muuuuse` waits for their structured final-answer logs instead of relaying transient screen chatter. For anything else, it first looks for an explicit `(answer)` block and otherwise falls back to the last stable output block after a turn goes idle.
|
|
73
73
|
|
|
74
74
|
## Sessions
|
|
75
75
|
|
|
@@ -80,13 +80,13 @@ If you want an explicit lane name, use:
|
|
|
80
80
|
```bash
|
|
81
81
|
muuuuse 1 --session demo codex
|
|
82
82
|
muuuuse 2 --session demo gemini
|
|
83
|
-
muuuuse
|
|
83
|
+
muuuuse stop --session demo
|
|
84
84
|
```
|
|
85
85
|
|
|
86
86
|
You can also inspect the lane:
|
|
87
87
|
|
|
88
88
|
```bash
|
|
89
|
-
muuuuse
|
|
89
|
+
muuuuse status
|
|
90
90
|
```
|
|
91
91
|
|
|
92
92
|
## Doctor
|
package/package.json
CHANGED
package/src/agents.js
CHANGED
|
@@ -76,6 +76,7 @@ const CLAUDE_ROOT = path.join(os.homedir(), ".claude", "projects");
|
|
|
76
76
|
const GEMINI_ROOT = path.join(os.homedir(), ".gemini", "tmp");
|
|
77
77
|
const CODEX_SNAPSHOT_ROOT = path.join(os.homedir(), ".codex", "shell_snapshots");
|
|
78
78
|
const codexSnapshotPaneCache = new Map();
|
|
79
|
+
const codexSnapshotExportsCache = new Map();
|
|
79
80
|
|
|
80
81
|
function walkFiles(rootPath, predicate, results = []) {
|
|
81
82
|
try {
|
|
@@ -157,6 +158,10 @@ function chooseCandidate(candidates, currentPath, processStartedAtMs) {
|
|
|
157
158
|
return null;
|
|
158
159
|
}
|
|
159
160
|
|
|
161
|
+
if (cwdMatches.length === 1) {
|
|
162
|
+
return cwdMatches[0].path;
|
|
163
|
+
}
|
|
164
|
+
|
|
160
165
|
if (processStartedAtMs !== null) {
|
|
161
166
|
const preciseMatches = cwdMatches
|
|
162
167
|
.map((candidate) => ({
|
|
@@ -166,13 +171,12 @@ function chooseCandidate(candidates, currentPath, processStartedAtMs) {
|
|
|
166
171
|
.filter((candidate) => Number.isFinite(candidate.diffMs) && candidate.diffMs <= SESSION_MATCH_WINDOW_MS)
|
|
167
172
|
.sort((left, right) => left.diffMs - right.diffMs || right.mtimeMs - left.mtimeMs);
|
|
168
173
|
|
|
169
|
-
if (preciseMatches.length
|
|
174
|
+
if (preciseMatches.length === 1) {
|
|
170
175
|
return preciseMatches[0].path;
|
|
171
176
|
}
|
|
172
177
|
}
|
|
173
178
|
|
|
174
|
-
|
|
175
|
-
return fallback ? fallback.path : null;
|
|
179
|
+
return null;
|
|
176
180
|
}
|
|
177
181
|
|
|
178
182
|
function extractThreadId(filePath) {
|
|
@@ -202,6 +206,49 @@ function readCodexSnapshotPane(threadId) {
|
|
|
202
206
|
}
|
|
203
207
|
}
|
|
204
208
|
|
|
209
|
+
function readCodexSnapshotExports(threadId) {
|
|
210
|
+
if (!threadId) {
|
|
211
|
+
return {};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (codexSnapshotExportsCache.has(threadId)) {
|
|
215
|
+
return codexSnapshotExportsCache.get(threadId);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const snapshotPath = path.join(CODEX_SNAPSHOT_ROOT, `${threadId}.sh`);
|
|
219
|
+
try {
|
|
220
|
+
const contents = fs.readFileSync(snapshotPath, "utf8");
|
|
221
|
+
const exportsMap = {};
|
|
222
|
+
const pattern = /^declare -x ([A-Z0-9_]+)="((?:[^"\\]|\\.)*)"$/gm;
|
|
223
|
+
|
|
224
|
+
for (const match of contents.matchAll(pattern)) {
|
|
225
|
+
const [, key = "", rawValue = ""] = match;
|
|
226
|
+
exportsMap[key] = rawValue
|
|
227
|
+
.replace(/\\"/g, "\"")
|
|
228
|
+
.replace(/\\\\/g, "\\");
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
codexSnapshotExportsCache.set(threadId, exportsMap);
|
|
232
|
+
return exportsMap;
|
|
233
|
+
} catch (error) {
|
|
234
|
+
codexSnapshotExportsCache.set(threadId, {});
|
|
235
|
+
return {};
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function snapshotEnvMatches(exportsMap, expectedEnv = null) {
|
|
240
|
+
if (!expectedEnv || typeof expectedEnv !== "object") {
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return Object.entries(expectedEnv).every(([key, value]) => {
|
|
245
|
+
if (value === undefined || value === null) {
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
return exportsMap[key] === String(value);
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
205
252
|
function readCodexCandidate(filePath) {
|
|
206
253
|
try {
|
|
207
254
|
const [firstLine] = readFirstLines(filePath, 1);
|
|
@@ -216,6 +263,7 @@ function readCodexCandidate(filePath) {
|
|
|
216
263
|
return {
|
|
217
264
|
path: filePath,
|
|
218
265
|
threadId: extractThreadId(filePath),
|
|
266
|
+
snapshotExports: readCodexSnapshotExports(extractThreadId(filePath)),
|
|
219
267
|
snapshotPaneId: readCodexSnapshotPane(extractThreadId(filePath)),
|
|
220
268
|
cwd: entry.payload.cwd,
|
|
221
269
|
startedAtMs: Date.parse(entry.payload.timestamp),
|
|
@@ -226,12 +274,24 @@ function readCodexCandidate(filePath) {
|
|
|
226
274
|
}
|
|
227
275
|
}
|
|
228
276
|
|
|
229
|
-
function selectCodexSessionFile(currentPath, processStartedAtMs,
|
|
277
|
+
function selectCodexSessionFile(currentPath, processStartedAtMs, options = {}) {
|
|
278
|
+
const paneId = options.paneId || null;
|
|
279
|
+
const snapshotEnv = options.snapshotEnv || null;
|
|
230
280
|
const candidates = walkFiles(CODEX_ROOT, (filePath) => filePath.endsWith(".jsonl"))
|
|
231
281
|
.map((filePath) => readCodexCandidate(filePath))
|
|
232
282
|
.filter((candidate) => candidate !== null);
|
|
233
283
|
|
|
234
284
|
let scopedCandidates = candidates;
|
|
285
|
+
if (snapshotEnv) {
|
|
286
|
+
const exactEnvMatches = scopedCandidates.filter((candidate) => snapshotEnvMatches(candidate.snapshotExports, snapshotEnv));
|
|
287
|
+
if (exactEnvMatches.length === 1) {
|
|
288
|
+
return exactEnvMatches[0].path;
|
|
289
|
+
}
|
|
290
|
+
if (exactEnvMatches.length > 1) {
|
|
291
|
+
scopedCandidates = exactEnvMatches;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
235
295
|
if (paneId) {
|
|
236
296
|
const exactPaneMatches = scopedCandidates.filter((candidate) => candidate.snapshotPaneId === paneId);
|
|
237
297
|
if (exactPaneMatches.length > 0) {
|
|
@@ -415,12 +475,16 @@ function selectGeminiSessionFile(currentPath, processStartedAtMs) {
|
|
|
415
475
|
.filter((candidate) => Number.isFinite(candidate.diffMs) && candidate.diffMs <= SESSION_MATCH_WINDOW_MS)
|
|
416
476
|
.sort((left, right) => left.diffMs - right.diffMs || right.lastUpdatedMs - left.lastUpdatedMs);
|
|
417
477
|
|
|
418
|
-
if (preciseMatches.length
|
|
478
|
+
if (preciseMatches.length === 1) {
|
|
419
479
|
return preciseMatches[0].path;
|
|
420
480
|
}
|
|
421
481
|
}
|
|
422
482
|
|
|
423
|
-
|
|
483
|
+
if (candidates.length === 1) {
|
|
484
|
+
return candidates[0].path;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return null;
|
|
424
488
|
}
|
|
425
489
|
|
|
426
490
|
function readGeminiAnswers(filePath, lastMessageId = null) {
|
|
@@ -460,6 +524,7 @@ function readGeminiAnswers(filePath, lastMessageId = null) {
|
|
|
460
524
|
|
|
461
525
|
module.exports = {
|
|
462
526
|
PRESETS,
|
|
527
|
+
chooseCandidate,
|
|
463
528
|
detectAgent,
|
|
464
529
|
detectAgentTypeFromCommand,
|
|
465
530
|
expandPresetCommand,
|
package/src/cli.js
CHANGED
|
@@ -7,10 +7,11 @@ const {
|
|
|
7
7
|
} = require("./util");
|
|
8
8
|
const {
|
|
9
9
|
SeatProcess,
|
|
10
|
+
readAllSessionStatuses,
|
|
10
11
|
readSessionStatus,
|
|
11
12
|
resolveProgramTokens,
|
|
12
13
|
resolveSessionName,
|
|
13
|
-
|
|
14
|
+
stopSessions,
|
|
14
15
|
} = require("./runtime");
|
|
15
16
|
|
|
16
17
|
async function main(argv = process.argv.slice(2)) {
|
|
@@ -26,6 +27,51 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
26
27
|
return;
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
if (command === "stop") {
|
|
31
|
+
const { flags } = parseFlags(argv.slice(1));
|
|
32
|
+
const result = await stopSessions(flags.session || null);
|
|
33
|
+
if (result.sessions.length === 0) {
|
|
34
|
+
process.stdout.write(`${BRAND} no live sessions found.\n`);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (flags.session) {
|
|
39
|
+
process.stdout.write(`${BRAND} stop requested for session ${flags.session}.\n`);
|
|
40
|
+
} else {
|
|
41
|
+
process.stdout.write(`${BRAND} stop requested for all sessions.\n`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
for (const session of result.sessions) {
|
|
45
|
+
process.stdout.write(`${session.sessionName}\n`);
|
|
46
|
+
for (const seat of session.seats) {
|
|
47
|
+
process.stdout.write(
|
|
48
|
+
`seat ${seat.seatId}: wrapper ${describeStopResult(seat.wrapperStopped, seat.wrapperForced)}`
|
|
49
|
+
+ `, child ${describeStopResult(seat.childStopped, seat.childForced)}\n`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (command === "status") {
|
|
57
|
+
const { flags } = parseFlags(argv.slice(1));
|
|
58
|
+
if (flags.session) {
|
|
59
|
+
printSessionStatus(readSessionStatus(flags.session));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const sessions = readAllSessionStatuses();
|
|
64
|
+
if (sessions.length === 0) {
|
|
65
|
+
process.stdout.write(`${BRAND} no tracked sessions.\n`);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
for (const session of sessions) {
|
|
70
|
+
printSessionStatus(session);
|
|
71
|
+
}
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
29
75
|
if (command === "1" || command === "2") {
|
|
30
76
|
const { positionals, flags } = parseFlags(argv.slice(1));
|
|
31
77
|
const sessionName = resolveSessionName(flags.session, process.cwd());
|
|
@@ -43,38 +89,21 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
43
89
|
|
|
44
90
|
if (command === "3") {
|
|
45
91
|
const { positionals, flags } = parseFlags(argv.slice(1));
|
|
46
|
-
const sessionName = resolveSessionName(flags.session, process.cwd());
|
|
47
92
|
const action = String(positionals[0] || "status").toLowerCase();
|
|
48
93
|
|
|
49
94
|
if (action === "stop") {
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
for (const seat of result.seats) {
|
|
53
|
-
process.stdout.write(
|
|
54
|
-
`seat ${seat.seatId}: wrapper ${seat.wrapperStopped ? "signaled" : "idle"}`
|
|
55
|
-
+ `, child ${seat.childStopped ? "signaled" : "idle"}\n`
|
|
56
|
-
);
|
|
57
|
-
}
|
|
95
|
+
const forwarded = ["stop", ...argv.slice(1).filter((token) => token !== "stop")];
|
|
96
|
+
await main(forwarded);
|
|
58
97
|
return;
|
|
59
98
|
}
|
|
60
99
|
|
|
61
100
|
if (action === "status") {
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
for (const seat of status.seats) {
|
|
65
|
-
if (!seat.status) {
|
|
66
|
-
process.stdout.write(`seat ${seat.seatId}: idle\n`);
|
|
67
|
-
continue;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const state = seat.status.state || "unknown";
|
|
71
|
-
const program = Array.isArray(seat.status.command) ? seat.status.command.join(" ") : "";
|
|
72
|
-
process.stdout.write(`seat ${seat.seatId}: ${state}${program ? ` (${program})` : ""}\n`);
|
|
73
|
-
}
|
|
101
|
+
const forwarded = ["status", ...argv.slice(1).filter((token) => token !== "status")];
|
|
102
|
+
await main(forwarded);
|
|
74
103
|
return;
|
|
75
104
|
}
|
|
76
105
|
|
|
77
|
-
throw new Error(`Unknown seat 3 action '${action}'. Try \`muuuuse
|
|
106
|
+
throw new Error(`Unknown seat 3 action '${action}'. Try \`muuuuse stop\` or \`muuuuse status\`.`);
|
|
78
107
|
}
|
|
79
108
|
|
|
80
109
|
throw new Error(`Unknown command '${command}'.`);
|
|
@@ -114,6 +143,30 @@ function checkBinary(command, versionArgs, required) {
|
|
|
114
143
|
return { label: command, ok: true, detail: version, required };
|
|
115
144
|
}
|
|
116
145
|
|
|
146
|
+
function describeStopResult(signaled, forced) {
|
|
147
|
+
if (forced) {
|
|
148
|
+
return "killed";
|
|
149
|
+
}
|
|
150
|
+
if (signaled) {
|
|
151
|
+
return "signaled";
|
|
152
|
+
}
|
|
153
|
+
return "idle";
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function printSessionStatus(status) {
|
|
157
|
+
process.stdout.write(`${BRAND} session ${status.sessionName}\n`);
|
|
158
|
+
for (const seat of status.seats) {
|
|
159
|
+
if (!seat.status) {
|
|
160
|
+
process.stdout.write(`seat ${seat.seatId}: idle\n`);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const state = seat.status.state || "unknown";
|
|
165
|
+
const program = Array.isArray(seat.status.command) ? seat.status.command.join(" ") : "";
|
|
166
|
+
process.stdout.write(`seat ${seat.seatId}: ${state}${program ? ` (${program})` : ""}\n`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
117
170
|
module.exports = {
|
|
118
171
|
main,
|
|
119
172
|
runDoctor,
|
package/src/runtime.js
CHANGED
|
@@ -20,6 +20,7 @@ const {
|
|
|
20
20
|
getDefaultSessionName,
|
|
21
21
|
getFileSize,
|
|
22
22
|
getSeatPaths,
|
|
23
|
+
getStateRoot,
|
|
23
24
|
hashText,
|
|
24
25
|
isPidAlive,
|
|
25
26
|
readAppendedText,
|
|
@@ -31,7 +32,6 @@ const {
|
|
|
31
32
|
} = require("./util");
|
|
32
33
|
|
|
33
34
|
const GENERIC_IDLE_MS = 900;
|
|
34
|
-
const GENERIC_FALLBACK_DELAY_MS = 4000;
|
|
35
35
|
|
|
36
36
|
function resolveSessionName(sessionOverride, currentPath = process.cwd()) {
|
|
37
37
|
return sessionOverride || getDefaultSessionName(currentPath);
|
|
@@ -79,9 +79,9 @@ function parseAnswerEntries(text) {
|
|
|
79
79
|
.filter((entry) => entry && entry.type === "answer" && typeof entry.text === "string");
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
function resolveSessionFile(agentType, currentPath, processStartedAtMs) {
|
|
82
|
+
function resolveSessionFile(agentType, currentPath, processStartedAtMs, options = {}) {
|
|
83
83
|
if (agentType === "codex") {
|
|
84
|
-
return selectCodexSessionFile(currentPath, processStartedAtMs);
|
|
84
|
+
return selectCodexSessionFile(currentPath, processStartedAtMs, options);
|
|
85
85
|
}
|
|
86
86
|
if (agentType === "claude") {
|
|
87
87
|
return selectClaudeSessionFile(currentPath, processStartedAtMs);
|
|
@@ -161,6 +161,11 @@ function extractGenericAnswer(rawText, lastInputText) {
|
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
+
const markerAnswer = extractMarkedAnswer(candidate);
|
|
165
|
+
if (markerAnswer) {
|
|
166
|
+
return markerAnswer;
|
|
167
|
+
}
|
|
168
|
+
|
|
164
169
|
const blocks = candidate
|
|
165
170
|
.split(/\n{2,}/)
|
|
166
171
|
.map((block) => block.trim())
|
|
@@ -173,6 +178,18 @@ function extractGenericAnswer(rawText, lastInputText) {
|
|
|
173
178
|
return sanitizeRelayText(blocks[blocks.length - 1]);
|
|
174
179
|
}
|
|
175
180
|
|
|
181
|
+
function extractMarkedAnswer(content) {
|
|
182
|
+
const lines = String(content || "").split("\n");
|
|
183
|
+
const answerIndex = lines.findIndex((line) => line.trim().startsWith("(answer)"));
|
|
184
|
+
if (answerIndex === -1) {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const answerLines = lines.slice(answerIndex);
|
|
189
|
+
answerLines[0] = answerLines[0].trim().replace(/^\(answer\)\s*/, "");
|
|
190
|
+
return sanitizeRelayText(answerLines.join("\n"));
|
|
191
|
+
}
|
|
192
|
+
|
|
176
193
|
class SeatProcess {
|
|
177
194
|
constructor(options) {
|
|
178
195
|
this.seatId = options.seatId;
|
|
@@ -196,6 +213,8 @@ class SeatProcess {
|
|
|
196
213
|
this.stopped = false;
|
|
197
214
|
this.stdinCleanup = null;
|
|
198
215
|
this.resizeCleanup = null;
|
|
216
|
+
this.childToken = createId(16);
|
|
217
|
+
this.processStartedAtMs = null;
|
|
199
218
|
|
|
200
219
|
this.sessionState = {
|
|
201
220
|
file: null,
|
|
@@ -217,6 +236,7 @@ class SeatProcess {
|
|
|
217
236
|
cwd: this.cwd,
|
|
218
237
|
pid: process.pid,
|
|
219
238
|
childPid: this.childPid,
|
|
239
|
+
childToken: this.childToken,
|
|
220
240
|
agentType: this.agentType,
|
|
221
241
|
command: this.commandTokens,
|
|
222
242
|
commandLine: formatCommand(this.commandTokens),
|
|
@@ -232,6 +252,7 @@ class SeatProcess {
|
|
|
232
252
|
cwd: this.cwd,
|
|
233
253
|
pid: process.pid,
|
|
234
254
|
childPid: this.childPid,
|
|
255
|
+
childToken: this.childToken,
|
|
235
256
|
agentType: this.agentType,
|
|
236
257
|
command: this.commandTokens,
|
|
237
258
|
relayCount: this.relayCount,
|
|
@@ -245,6 +266,7 @@ class SeatProcess {
|
|
|
245
266
|
this.stopped = true;
|
|
246
267
|
};
|
|
247
268
|
process.once("SIGINT", stop);
|
|
269
|
+
process.once("SIGHUP", stop);
|
|
248
270
|
process.once("SIGTERM", stop);
|
|
249
271
|
}
|
|
250
272
|
|
|
@@ -260,15 +282,22 @@ class SeatProcess {
|
|
|
260
282
|
this.genericTracker.noteTurnStart("");
|
|
261
283
|
}
|
|
262
284
|
};
|
|
285
|
+
const handleEnd = () => {
|
|
286
|
+
this.stopped = true;
|
|
287
|
+
};
|
|
263
288
|
|
|
264
289
|
if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
|
|
265
290
|
process.stdin.setRawMode(true);
|
|
266
291
|
}
|
|
267
292
|
process.stdin.resume();
|
|
268
293
|
process.stdin.on("data", handleData);
|
|
294
|
+
process.stdin.on("close", handleEnd);
|
|
295
|
+
process.stdin.on("end", handleEnd);
|
|
269
296
|
|
|
270
297
|
this.stdinCleanup = () => {
|
|
271
298
|
process.stdin.off("data", handleData);
|
|
299
|
+
process.stdin.off("close", handleEnd);
|
|
300
|
+
process.stdin.off("end", handleEnd);
|
|
272
301
|
if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
|
|
273
302
|
process.stdin.setRawMode(false);
|
|
274
303
|
}
|
|
@@ -307,6 +336,7 @@ class SeatProcess {
|
|
|
307
336
|
cwd: this.cwd,
|
|
308
337
|
env: {
|
|
309
338
|
...process.env,
|
|
339
|
+
MUUUUSE_CHILD_TOKEN: this.childToken,
|
|
310
340
|
MUUUUSE_SEAT: String(this.seatId),
|
|
311
341
|
MUUUUSE_SESSION: this.sessionName,
|
|
312
342
|
},
|
|
@@ -315,6 +345,7 @@ class SeatProcess {
|
|
|
315
345
|
});
|
|
316
346
|
|
|
317
347
|
this.childPid = this.child.pid;
|
|
348
|
+
this.processStartedAtMs = Date.now();
|
|
318
349
|
this.writeMeta();
|
|
319
350
|
this.writeStatus({
|
|
320
351
|
partnerSeatId: this.partnerSeatId,
|
|
@@ -352,15 +383,7 @@ class SeatProcess {
|
|
|
352
383
|
}
|
|
353
384
|
|
|
354
385
|
shouldUseGenericCapture() {
|
|
355
|
-
|
|
356
|
-
return true;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
if (this.sessionState.file) {
|
|
360
|
-
return false;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
return Date.now() - this.startedAtMs >= GENERIC_FALLBACK_DELAY_MS;
|
|
386
|
+
return !this.agentType;
|
|
364
387
|
}
|
|
365
388
|
|
|
366
389
|
pullPartnerEvents() {
|
|
@@ -401,7 +424,15 @@ class SeatProcess {
|
|
|
401
424
|
return;
|
|
402
425
|
}
|
|
403
426
|
|
|
404
|
-
const sessionFile = resolveSessionFile(this.agentType, this.cwd, this.
|
|
427
|
+
const sessionFile = resolveSessionFile(this.agentType, this.cwd, this.processStartedAtMs, {
|
|
428
|
+
snapshotEnv: this.agentType === "codex"
|
|
429
|
+
? {
|
|
430
|
+
MUUUUSE_CHILD_TOKEN: this.childToken,
|
|
431
|
+
MUUUUSE_SEAT: String(this.seatId),
|
|
432
|
+
MUUUUSE_SESSION: this.sessionName,
|
|
433
|
+
}
|
|
434
|
+
: null,
|
|
435
|
+
});
|
|
405
436
|
if (!sessionFile) {
|
|
406
437
|
return;
|
|
407
438
|
}
|
|
@@ -506,7 +537,7 @@ class SeatProcess {
|
|
|
506
537
|
|
|
507
538
|
this.log(`${BRAND} seat ${this.seatId} started in session ${this.sessionName}.`);
|
|
508
539
|
this.log(`Command: ${formatCommand(this.commandTokens)}`);
|
|
509
|
-
this.log(`Stop
|
|
540
|
+
this.log(`Stop everything from another terminal with: muuuuse stop`);
|
|
510
541
|
|
|
511
542
|
try {
|
|
512
543
|
while (!this.stopped) {
|
|
@@ -598,6 +629,56 @@ function stopSession(sessionName) {
|
|
|
598
629
|
};
|
|
599
630
|
}
|
|
600
631
|
|
|
632
|
+
function listSessionNames() {
|
|
633
|
+
const sessionsRoot = path.join(getStateRoot(), "sessions");
|
|
634
|
+
try {
|
|
635
|
+
return fs.readdirSync(sessionsRoot, { withFileTypes: true })
|
|
636
|
+
.filter((entry) => entry.isDirectory())
|
|
637
|
+
.map((entry) => entry.name)
|
|
638
|
+
.sort();
|
|
639
|
+
} catch (error) {
|
|
640
|
+
return [];
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
async function stopSessions(sessionName = null) {
|
|
645
|
+
const sessionNames = sessionName ? [sessionName] : listSessionNames();
|
|
646
|
+
const sessionResults = sessionNames.map((name) => stopSession(name));
|
|
647
|
+
|
|
648
|
+
await sleep(200);
|
|
649
|
+
|
|
650
|
+
for (const sessionResult of sessionResults) {
|
|
651
|
+
for (const seat of sessionResult.seats) {
|
|
652
|
+
if (seat.wrapperPid && isPidAlive(seat.wrapperPid)) {
|
|
653
|
+
try {
|
|
654
|
+
process.kill(seat.wrapperPid, "SIGKILL");
|
|
655
|
+
seat.wrapperForced = true;
|
|
656
|
+
} catch (error) {
|
|
657
|
+
seat.wrapperForced = false;
|
|
658
|
+
}
|
|
659
|
+
} else {
|
|
660
|
+
seat.wrapperForced = false;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
if (seat.childPid && isPidAlive(seat.childPid)) {
|
|
664
|
+
try {
|
|
665
|
+
process.kill(seat.childPid, "SIGKILL");
|
|
666
|
+
seat.childForced = true;
|
|
667
|
+
} catch (error) {
|
|
668
|
+
seat.childForced = false;
|
|
669
|
+
}
|
|
670
|
+
} else {
|
|
671
|
+
seat.childForced = false;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
return {
|
|
677
|
+
sessionName,
|
|
678
|
+
sessions: sessionResults,
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
|
|
601
682
|
function readSessionStatus(sessionName) {
|
|
602
683
|
return {
|
|
603
684
|
sessionName,
|
|
@@ -612,11 +693,17 @@ function readSessionStatus(sessionName) {
|
|
|
612
693
|
};
|
|
613
694
|
}
|
|
614
695
|
|
|
696
|
+
function readAllSessionStatuses() {
|
|
697
|
+
return listSessionNames().map((sessionName) => readSessionStatus(sessionName));
|
|
698
|
+
}
|
|
699
|
+
|
|
615
700
|
module.exports = {
|
|
616
701
|
SeatProcess,
|
|
617
702
|
formatCommand,
|
|
703
|
+
readAllSessionStatuses,
|
|
618
704
|
readSessionStatus,
|
|
619
705
|
resolveProgramTokens,
|
|
620
706
|
resolveSessionName,
|
|
621
707
|
stopSession,
|
|
708
|
+
stopSessions,
|
|
622
709
|
};
|
package/src/util.js
CHANGED
|
@@ -261,19 +261,19 @@ function usage() {
|
|
|
261
261
|
"Usage:",
|
|
262
262
|
" muuuuse 1 <program...>",
|
|
263
263
|
" muuuuse 2 <program...>",
|
|
264
|
-
" muuuuse
|
|
265
|
-
" muuuuse
|
|
264
|
+
" muuuuse stop",
|
|
265
|
+
" muuuuse status",
|
|
266
266
|
" muuuuse doctor",
|
|
267
267
|
"",
|
|
268
268
|
"Examples:",
|
|
269
269
|
" muuuuse 1 codex",
|
|
270
270
|
" muuuuse 2 gemini",
|
|
271
|
-
" muuuuse
|
|
271
|
+
" muuuuse stop",
|
|
272
272
|
" muuuuse 1 bash -lc 'while read line; do printf \"script one: %s\\n\\n\" \"$line\"; done'",
|
|
273
273
|
"",
|
|
274
274
|
"Notes:",
|
|
275
275
|
" - Seats auto-pair by current working directory by default.",
|
|
276
|
-
" - Use `--session <name>` on seats and
|
|
276
|
+
" - Use `--session <name>` on seats and stop/status if you want an explicit shared lane.",
|
|
277
277
|
" - Known presets (`codex`, `claude`, `gemini`) expand to recommended launch flags.",
|
|
278
278
|
" - Any other program runs as-is inside the current terminal under a PTY wrapper.",
|
|
279
279
|
].join("\n");
|