muuuuse 1.4.0 → 1.4.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 +25 -38
- package/package.json +2 -2
- package/src/agents.js +391 -0
- package/src/cli.js +51 -5
- package/src/runtime.js +603 -157
- package/src/util.js +22 -3
package/README.md
CHANGED
|
@@ -1,36 +1,32 @@
|
|
|
1
1
|
# 🔌Muuuuse
|
|
2
2
|
|
|
3
|
-
`🔌Muuuuse` is a
|
|
3
|
+
`🔌Muuuuse` is a tiny no-tmux terminal relay.
|
|
4
4
|
|
|
5
|
-
It does one
|
|
6
|
-
- arm
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
5
|
+
It does one job:
|
|
6
|
+
- arm terminal one with `muuuuse 1`
|
|
7
|
+
- arm terminal two with `muuuuse 2`
|
|
8
|
+
- watch Codex, Claude, or Gemini for real final answers
|
|
9
|
+
- inject that final answer into the other armed terminal
|
|
10
|
+
- keep looping until you stop it
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
The whole surface is:
|
|
12
13
|
|
|
13
14
|
```bash
|
|
14
15
|
muuuuse 1
|
|
15
16
|
muuuuse 2
|
|
17
|
+
muuuuse status
|
|
16
18
|
muuuuse stop
|
|
17
19
|
```
|
|
18
20
|
|
|
19
|
-
No tmux.
|
|
20
|
-
No program arguments.
|
|
21
|
-
No status command.
|
|
22
|
-
No doctor command.
|
|
23
|
-
No preset logic.
|
|
24
|
-
|
|
25
21
|
## Flow
|
|
26
22
|
|
|
27
|
-
|
|
23
|
+
Terminal 1:
|
|
28
24
|
|
|
29
25
|
```bash
|
|
30
26
|
muuuuse 1
|
|
31
27
|
```
|
|
32
28
|
|
|
33
|
-
|
|
29
|
+
Terminal 2:
|
|
34
30
|
|
|
35
31
|
```bash
|
|
36
32
|
muuuuse 2
|
|
@@ -48,38 +44,29 @@ codex
|
|
|
48
44
|
gemini
|
|
49
45
|
```
|
|
50
46
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
When the running program rings the terminal bell, `🔌Muuuuse` takes the final output block for that turn and injects it into the partner seat.
|
|
47
|
+
`🔌Muuuuse` tails the local session logs for supported CLIs, detects the final answer, types that answer into the other seat, and then sends Enter as a separate keystroke.
|
|
54
48
|
|
|
55
|
-
|
|
49
|
+
Check the live state from any terminal:
|
|
56
50
|
|
|
57
51
|
```bash
|
|
58
|
-
muuuuse
|
|
52
|
+
muuuuse status
|
|
59
53
|
```
|
|
60
54
|
|
|
61
|
-
|
|
55
|
+
Stop the loop from any terminal, including one of the armed shells once it is back at a prompt:
|
|
62
56
|
|
|
63
57
|
```bash
|
|
64
|
-
|
|
58
|
+
muuuuse stop
|
|
65
59
|
```
|
|
66
60
|
|
|
67
|
-
## What Counts As A Relay
|
|
68
|
-
|
|
69
|
-
`🔌Muuuuse` watches the armed terminal output for BEL (`\u0007`).
|
|
70
|
-
|
|
71
|
-
That BEL marks the end of a turn.
|
|
72
|
-
|
|
73
|
-
When it sees BEL, it:
|
|
74
|
-
1. grabs the final output block since the last submitted input
|
|
75
|
-
2. cleans the block
|
|
76
|
-
3. appends it to the seat event log
|
|
77
|
-
4. injects it into the other armed terminal followed by Enter
|
|
78
|
-
|
|
79
61
|
## Notes
|
|
80
62
|
|
|
81
|
-
-
|
|
82
|
-
- seat pairing defaults by current working directory
|
|
63
|
+
- no tmux
|
|
83
64
|
- state lives under `~/.muuuuse`
|
|
84
|
-
-
|
|
85
|
-
-
|
|
65
|
+
- supported final-answer detection is built for Codex, Claude, and Gemini
|
|
66
|
+
- `codeman` remains the larger transport/control layer; `muuuuse` stays local and minimal
|
|
67
|
+
|
|
68
|
+
## Install
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
npm install -g muuuuse
|
|
72
|
+
```
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "muuuuse",
|
|
3
|
-
"version": "1.4.
|
|
4
|
-
"description": "🔌Muuuuse arms two
|
|
3
|
+
"version": "1.4.2",
|
|
4
|
+
"description": "🔌Muuuuse arms two regular terminals and relays final answers between them.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"bin": {
|
|
7
7
|
"muuuuse": "bin/muuse.js"
|
package/src/agents.js
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
const { createHash } = require("node:crypto");
|
|
2
|
+
const fs = require("node:fs");
|
|
3
|
+
const os = require("node:os");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
getFileSize,
|
|
8
|
+
hashText,
|
|
9
|
+
readAppendedText,
|
|
10
|
+
sanitizeRelayText,
|
|
11
|
+
SESSION_MATCH_WINDOW_MS,
|
|
12
|
+
} = require("./util");
|
|
13
|
+
|
|
14
|
+
const CODEX_ROOT = path.join(os.homedir(), ".codex", "sessions");
|
|
15
|
+
const CLAUDE_ROOT = path.join(os.homedir(), ".claude", "projects");
|
|
16
|
+
const GEMINI_ROOT = path.join(os.homedir(), ".gemini", "tmp");
|
|
17
|
+
const SESSION_START_EARLY_TOLERANCE_MS = 2 * 1000;
|
|
18
|
+
|
|
19
|
+
function walkFiles(rootPath, predicate, results = []) {
|
|
20
|
+
try {
|
|
21
|
+
const entries = fs.readdirSync(rootPath, { withFileTypes: true });
|
|
22
|
+
for (const entry of entries) {
|
|
23
|
+
const absolutePath = path.join(rootPath, entry.name);
|
|
24
|
+
if (entry.isDirectory()) {
|
|
25
|
+
walkFiles(absolutePath, predicate, results);
|
|
26
|
+
} else if (predicate(absolutePath)) {
|
|
27
|
+
results.push(absolutePath);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
} catch {
|
|
31
|
+
return results;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return results;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function commandMatches(args, command) {
|
|
38
|
+
const pattern = new RegExp(`(^|[\\\\/\\s])${command}(\\s|$)`, "i");
|
|
39
|
+
return pattern.test(args);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function buildDetectedAgent(type, process) {
|
|
43
|
+
return {
|
|
44
|
+
type,
|
|
45
|
+
pid: process.pid,
|
|
46
|
+
args: process.args,
|
|
47
|
+
cwd: process.cwd || null,
|
|
48
|
+
elapsedSeconds: process.elapsedSeconds,
|
|
49
|
+
processStartedAtMs: Date.now() - process.elapsedSeconds * 1000,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function detectAgent(processes) {
|
|
54
|
+
const ordered = [...processes].sort((left, right) => left.elapsedSeconds - right.elapsedSeconds);
|
|
55
|
+
for (const process of ordered) {
|
|
56
|
+
if (commandMatches(process.args, "codex")) {
|
|
57
|
+
return buildDetectedAgent("codex", process);
|
|
58
|
+
}
|
|
59
|
+
if (commandMatches(process.args, "claude")) {
|
|
60
|
+
return buildDetectedAgent("claude", process);
|
|
61
|
+
}
|
|
62
|
+
if (commandMatches(process.args, "gemini")) {
|
|
63
|
+
return buildDetectedAgent("gemini", process);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function readFirstLines(filePath, maxLines = 20) {
|
|
70
|
+
const lines = [];
|
|
71
|
+
const fd = fs.openSync(filePath, "r");
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const buffer = Buffer.alloc(16384);
|
|
75
|
+
const bytesRead = fs.readSync(fd, buffer, 0, buffer.length, 0);
|
|
76
|
+
if (bytesRead === 0) {
|
|
77
|
+
return lines;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
for (const line of buffer.toString("utf8", 0, bytesRead).split("\n")) {
|
|
81
|
+
if (line.trim().length === 0) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
lines.push(line.trim());
|
|
85
|
+
if (lines.length >= maxLines) {
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return lines;
|
|
91
|
+
} finally {
|
|
92
|
+
fs.closeSync(fd);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function selectSessionCandidatePath(candidates, currentPath, processStartedAtMs) {
|
|
97
|
+
const cwdMatches = candidates.filter((candidate) => candidate.cwd === currentPath);
|
|
98
|
+
if (cwdMatches.length === 0) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (cwdMatches.length === 1) {
|
|
103
|
+
return cwdMatches[0].path;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!Number.isFinite(processStartedAtMs)) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const preciseMatches = cwdMatches
|
|
111
|
+
.map((candidate) => ({
|
|
112
|
+
...candidate,
|
|
113
|
+
diffMs: Math.abs(candidate.startedAtMs - processStartedAtMs),
|
|
114
|
+
relativeStartMs: candidate.startedAtMs - processStartedAtMs,
|
|
115
|
+
}))
|
|
116
|
+
.filter((candidate) => (
|
|
117
|
+
Number.isFinite(candidate.diffMs) &&
|
|
118
|
+
Number.isFinite(candidate.relativeStartMs) &&
|
|
119
|
+
candidate.relativeStartMs >= -SESSION_START_EARLY_TOLERANCE_MS &&
|
|
120
|
+
candidate.relativeStartMs <= SESSION_MATCH_WINDOW_MS
|
|
121
|
+
))
|
|
122
|
+
.sort((left, right) => left.diffMs - right.diffMs || right.mtimeMs - left.mtimeMs);
|
|
123
|
+
|
|
124
|
+
if (preciseMatches.length === 1) {
|
|
125
|
+
return preciseMatches[0].path;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function readCodexCandidate(filePath) {
|
|
132
|
+
try {
|
|
133
|
+
const [firstLine] = readFirstLines(filePath, 1);
|
|
134
|
+
if (!firstLine) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const entry = JSON.parse(firstLine);
|
|
139
|
+
if (entry?.type !== "session_meta" || typeof entry.payload?.cwd !== "string") {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
path: filePath,
|
|
145
|
+
cwd: entry.payload.cwd,
|
|
146
|
+
startedAtMs: Date.parse(entry.payload.timestamp),
|
|
147
|
+
mtimeMs: fs.statSync(filePath).mtimeMs,
|
|
148
|
+
};
|
|
149
|
+
} catch {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function selectCodexSessionFile(currentPath, processStartedAtMs) {
|
|
155
|
+
const candidates = walkFiles(CODEX_ROOT, (filePath) => filePath.endsWith(".jsonl"))
|
|
156
|
+
.map((filePath) => readCodexCandidate(filePath))
|
|
157
|
+
.filter((candidate) => candidate !== null);
|
|
158
|
+
|
|
159
|
+
return selectSessionCandidatePath(candidates, currentPath, processStartedAtMs);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function extractCodexAssistantText(content) {
|
|
163
|
+
if (!Array.isArray(content)) {
|
|
164
|
+
return "";
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return content
|
|
168
|
+
.flatMap((item) => {
|
|
169
|
+
if (!item || typeof item !== "object") {
|
|
170
|
+
return [];
|
|
171
|
+
}
|
|
172
|
+
if (item.type === "output_text" && typeof item.text === "string") {
|
|
173
|
+
return [item.text.trim()];
|
|
174
|
+
}
|
|
175
|
+
return [];
|
|
176
|
+
})
|
|
177
|
+
.filter((text) => text.length > 0)
|
|
178
|
+
.join("\n");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function parseCodexFinalLine(line) {
|
|
182
|
+
try {
|
|
183
|
+
const entry = JSON.parse(line);
|
|
184
|
+
if (entry?.type !== "response_item" || entry.payload?.type !== "message" || entry.payload?.role !== "assistant") {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (entry.payload?.phase !== "final_answer") {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const text = sanitizeRelayText(extractCodexAssistantText(entry.payload.content));
|
|
193
|
+
if (!text) {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
id: entry.payload.id || hashText(line),
|
|
199
|
+
text,
|
|
200
|
+
timestamp: entry.timestamp || entry.payload.timestamp || new Date().toISOString(),
|
|
201
|
+
};
|
|
202
|
+
} catch {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function isAnswerNewEnough(answer, sinceMs = null) {
|
|
208
|
+
if (!Number.isFinite(sinceMs)) {
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const answerMs = Date.parse(answer?.timestamp || "");
|
|
213
|
+
if (!Number.isFinite(answerMs)) {
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return answerMs >= sinceMs;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function readCodexAnswers(filePath, offset, sinceMs = null) {
|
|
221
|
+
const { nextOffset, text } = readAppendedText(filePath, offset);
|
|
222
|
+
const answers = text
|
|
223
|
+
.split("\n")
|
|
224
|
+
.map((line) => line.trim())
|
|
225
|
+
.filter((line) => line.length > 0)
|
|
226
|
+
.map((line) => parseCodexFinalLine(line))
|
|
227
|
+
.filter((entry) => entry !== null)
|
|
228
|
+
.filter((entry) => isAnswerNewEnough(entry, sinceMs));
|
|
229
|
+
|
|
230
|
+
return { nextOffset, answers };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function readClaudeCandidate(filePath) {
|
|
234
|
+
try {
|
|
235
|
+
const lines = readFirstLines(filePath, 12);
|
|
236
|
+
for (const line of lines) {
|
|
237
|
+
const entry = JSON.parse(line);
|
|
238
|
+
if (typeof entry.cwd !== "string") {
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
path: filePath,
|
|
244
|
+
cwd: entry.cwd,
|
|
245
|
+
startedAtMs: Date.parse(entry.timestamp || entry.message?.timestamp || 0),
|
|
246
|
+
mtimeMs: fs.statSync(filePath).mtimeMs,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
return null;
|
|
250
|
+
} catch {
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function selectClaudeSessionFile(currentPath, processStartedAtMs) {
|
|
256
|
+
const candidates = walkFiles(CLAUDE_ROOT, (filePath) => filePath.endsWith(".jsonl"))
|
|
257
|
+
.map((filePath) => readClaudeCandidate(filePath))
|
|
258
|
+
.filter((candidate) => candidate !== null);
|
|
259
|
+
|
|
260
|
+
return selectSessionCandidatePath(candidates, currentPath, processStartedAtMs);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function extractClaudeAssistantText(content) {
|
|
264
|
+
if (!Array.isArray(content)) {
|
|
265
|
+
return "";
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return content
|
|
269
|
+
.flatMap((item) => {
|
|
270
|
+
if (!item || typeof item !== "object") {
|
|
271
|
+
return [];
|
|
272
|
+
}
|
|
273
|
+
if (item.type === "text" && typeof item.text === "string") {
|
|
274
|
+
return [item.text.trim()];
|
|
275
|
+
}
|
|
276
|
+
return [];
|
|
277
|
+
})
|
|
278
|
+
.filter((text) => text.length > 0)
|
|
279
|
+
.join("\n");
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function parseClaudeFinalLine(line) {
|
|
283
|
+
try {
|
|
284
|
+
const entry = JSON.parse(line);
|
|
285
|
+
if (entry?.type !== "assistant" || entry.message?.role !== "assistant" || entry.message?.stop_reason !== "end_turn") {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const text = sanitizeRelayText(extractClaudeAssistantText(entry.message.content));
|
|
290
|
+
if (!text) {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
id: entry.uuid || entry.message.id || hashText(line),
|
|
296
|
+
text,
|
|
297
|
+
timestamp: entry.timestamp || new Date().toISOString(),
|
|
298
|
+
};
|
|
299
|
+
} catch {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function readClaudeAnswers(filePath, offset, sinceMs = null) {
|
|
305
|
+
const { nextOffset, text } = readAppendedText(filePath, offset);
|
|
306
|
+
const answers = text
|
|
307
|
+
.split("\n")
|
|
308
|
+
.map((line) => line.trim())
|
|
309
|
+
.filter((line) => line.length > 0)
|
|
310
|
+
.map((line) => parseClaudeFinalLine(line))
|
|
311
|
+
.filter((entry) => entry !== null)
|
|
312
|
+
.filter((entry) => isAnswerNewEnough(entry, sinceMs));
|
|
313
|
+
|
|
314
|
+
return { nextOffset, answers };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function readGeminiCandidate(filePath) {
|
|
318
|
+
try {
|
|
319
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
320
|
+
const entry = JSON.parse(raw);
|
|
321
|
+
return {
|
|
322
|
+
path: filePath,
|
|
323
|
+
projectHash: entry.projectHash,
|
|
324
|
+
cwd: entry.projectHash,
|
|
325
|
+
startedAtMs: Date.parse(entry.startTime),
|
|
326
|
+
mtimeMs: fs.statSync(filePath).mtimeMs,
|
|
327
|
+
lastUpdatedMs: Date.parse(entry.lastUpdated),
|
|
328
|
+
};
|
|
329
|
+
} catch {
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function selectGeminiSessionFile(currentPath, processStartedAtMs) {
|
|
335
|
+
const projectHash = createHash("sha256").update(currentPath).digest("hex");
|
|
336
|
+
const candidates = walkFiles(GEMINI_ROOT, (filePath) => filePath.endsWith(".json"))
|
|
337
|
+
.map((filePath) => readGeminiCandidate(filePath))
|
|
338
|
+
.filter((candidate) => candidate !== null && candidate.projectHash === projectHash);
|
|
339
|
+
|
|
340
|
+
return selectSessionCandidatePath(candidates, projectHash, processStartedAtMs);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function readGeminiAnswers(filePath, lastMessageId = null, sinceMs = null) {
|
|
344
|
+
try {
|
|
345
|
+
const entry = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
346
|
+
const messages = Array.isArray(entry.messages) ? entry.messages : [];
|
|
347
|
+
const finalMessages = messages.filter((message) => {
|
|
348
|
+
const toolCalls = Array.isArray(message.toolCalls) ? message.toolCalls : [];
|
|
349
|
+
return message.type === "gemini" && typeof message.content === "string" && message.content.trim() && toolCalls.length === 0;
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
let startIndex = 0;
|
|
353
|
+
if (lastMessageId) {
|
|
354
|
+
const previousIndex = finalMessages.findIndex((message) => message.id === lastMessageId);
|
|
355
|
+
startIndex = previousIndex === -1 ? finalMessages.length : previousIndex + 1;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const answers = finalMessages.slice(startIndex).map((message) => ({
|
|
359
|
+
id: message.id || hashText(JSON.stringify(message)),
|
|
360
|
+
text: sanitizeRelayText(message.content),
|
|
361
|
+
timestamp: message.timestamp || entry.lastUpdated || new Date().toISOString(),
|
|
362
|
+
}));
|
|
363
|
+
|
|
364
|
+
return {
|
|
365
|
+
answers: answers
|
|
366
|
+
.filter((answer) => answer.text.length > 0)
|
|
367
|
+
.filter((answer) => isAnswerNewEnough(answer, sinceMs)),
|
|
368
|
+
lastMessageId: finalMessages.length > 0 ? finalMessages[finalMessages.length - 1].id : lastMessageId,
|
|
369
|
+
fileSize: getFileSize(filePath),
|
|
370
|
+
};
|
|
371
|
+
} catch {
|
|
372
|
+
return {
|
|
373
|
+
answers: [],
|
|
374
|
+
lastMessageId,
|
|
375
|
+
fileSize: 0,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
module.exports = {
|
|
381
|
+
detectAgent,
|
|
382
|
+
parseClaudeFinalLine,
|
|
383
|
+
parseCodexFinalLine,
|
|
384
|
+
readClaudeAnswers,
|
|
385
|
+
readCodexAnswers,
|
|
386
|
+
readGeminiAnswers,
|
|
387
|
+
selectSessionCandidatePath,
|
|
388
|
+
selectClaudeSessionFile,
|
|
389
|
+
selectCodexSessionFile,
|
|
390
|
+
selectGeminiSessionFile,
|
|
391
|
+
};
|
package/src/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const { BRAND, usage } = require("./util");
|
|
2
|
-
const { ArmedSeat, stopAllSessions } = require("./runtime");
|
|
2
|
+
const { ArmedSeat, getStatusReport, stopAllSessions } = require("./runtime");
|
|
3
3
|
|
|
4
4
|
async function main(argv = process.argv.slice(2)) {
|
|
5
5
|
if (argv.length === 0 || argv.includes("--help") || argv.includes("-h")) {
|
|
@@ -24,7 +24,31 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
24
24
|
for (const session of result.sessions) {
|
|
25
25
|
process.stdout.write(`${session.sessionName}\n`);
|
|
26
26
|
for (const seat of session.seats) {
|
|
27
|
-
process.stdout.write(`seat ${seat.seatId}:
|
|
27
|
+
process.stdout.write(`seat ${seat.seatId}: ${seat.state} · agent ${seat.agent || "idle"} · relays ${seat.relayCount}\n`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (command === "status") {
|
|
34
|
+
if (argv.length > 1) {
|
|
35
|
+
throw new Error("`muuuuse status` takes no extra arguments.");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const report = getStatusReport();
|
|
39
|
+
if (report.sessions.length === 0) {
|
|
40
|
+
process.stdout.write(`${BRAND} no armed seats found.\n`);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
process.stdout.write(`${BRAND} status\n`);
|
|
45
|
+
for (const session of report.sessions) {
|
|
46
|
+
process.stdout.write(`\n${session.sessionName}\n`);
|
|
47
|
+
if (session.stopRequestedAt) {
|
|
48
|
+
process.stdout.write(`stop requested: ${session.stopRequestedAt}\n`);
|
|
49
|
+
}
|
|
50
|
+
for (const seat of session.seats) {
|
|
51
|
+
process.stdout.write(renderSeatStatus(seat));
|
|
28
52
|
}
|
|
29
53
|
}
|
|
30
54
|
return;
|
|
@@ -32,7 +56,7 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
32
56
|
|
|
33
57
|
if (command === "1" || command === "2") {
|
|
34
58
|
if (argv.length > 1) {
|
|
35
|
-
throw new Error(`\`muuuuse ${command}\` no
|
|
59
|
+
throw new Error(`\`muuuuse ${command}\` takes no extra arguments. Run it directly in the terminal you want to arm.`);
|
|
36
60
|
}
|
|
37
61
|
|
|
38
62
|
const seat = new ArmedSeat({
|
|
@@ -46,8 +70,30 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
46
70
|
throw new Error(`Unknown command '${command}'.`);
|
|
47
71
|
}
|
|
48
72
|
|
|
49
|
-
function
|
|
50
|
-
|
|
73
|
+
function renderSeatStatus(seat) {
|
|
74
|
+
const bits = [
|
|
75
|
+
`seat ${seat.seatId}: ${seat.state}`,
|
|
76
|
+
`agent ${seat.agent || "idle"}`,
|
|
77
|
+
`relays ${seat.relayCount}`,
|
|
78
|
+
`wrapper ${seat.wrapperPid || "-"}`,
|
|
79
|
+
`child ${seat.childPid || "-"}`,
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
if (seat.partnerLive) {
|
|
83
|
+
bits.push("peer live");
|
|
84
|
+
}
|
|
85
|
+
if (seat.lastAnswerAt) {
|
|
86
|
+
bits.push(`last answer ${seat.lastAnswerAt}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let output = `${bits.join(" · ")}\n`;
|
|
90
|
+
if (seat.cwd) {
|
|
91
|
+
output += `cwd: ${seat.cwd}\n`;
|
|
92
|
+
}
|
|
93
|
+
if (seat.log) {
|
|
94
|
+
output += `log: ${seat.log}\n`;
|
|
95
|
+
}
|
|
96
|
+
return output;
|
|
51
97
|
}
|
|
52
98
|
|
|
53
99
|
module.exports = {
|