muuuuse 1.3.2 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,105 +1,72 @@
1
1
  # 🔌Muuuuse
2
2
 
3
- `muuuuse` installs one CLI:
3
+ `🔌Muuuuse` is a tiny no-tmux terminal relay.
4
4
 
5
- - `muuuuse`
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
6
11
 
7
- The public brand stays `🔌Muuuuse`. The terminal command stays `muuuuse`.
8
-
9
- ## What It Is Now
10
-
11
- `🔌Muuuuse` no longer expects you to arm a terminal first and launch something later.
12
-
13
- The main flow is:
12
+ The whole surface is:
14
13
 
15
14
  ```bash
16
- muuuuse 1 <program...>
17
- muuuuse 2 <program...>
15
+ muuuuse 1
16
+ muuuuse 2
17
+ muuuuse status
18
18
  muuuuse stop
19
19
  ```
20
20
 
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
-
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
-
25
- ## Install
26
-
27
- ```bash
28
- npm install -g muuuuse
29
- ```
30
-
31
- ## Fastest AI Flow
21
+ ## Flow
32
22
 
33
23
  Terminal 1:
34
24
 
35
25
  ```bash
36
- muuuuse 1 codex
26
+ muuuuse 1
37
27
  ```
38
28
 
39
29
  Terminal 2:
40
30
 
41
31
  ```bash
42
- muuuuse 2 gemini
32
+ muuuuse 2
43
33
  ```
44
34
 
45
- Terminal 3:
46
-
47
- ```bash
48
- muuuuse stop
49
- ```
50
-
51
- Known presets expand to recommended flags automatically:
52
-
53
- - `codex`
54
- - `claude`
55
- - `gemini`
35
+ Now both shells are armed. Use them normally.
56
36
 
57
- So `muuuuse 1 codex` launches the fuller Codex command, not just bare `codex`.
58
-
59
- ## Generic Program Flow
60
-
61
- This is not AI-only. Any local program can be wrapped directly.
62
-
63
- Example:
37
+ If you want Codex in one and Gemini in the other, start them inside the armed shells:
64
38
 
65
39
  ```bash
66
- muuuuse 1 bash -lc 'while read line; do printf "left: %s\n\n" "$line"; done'
67
- muuuuse 2 bash -lc 'while read line; do printf "right: %s\n\n" "$line"; done'
40
+ codex
68
41
  ```
69
42
 
70
- Type into one seat and the other seat will receive the relayed block.
71
-
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
-
74
- ## Sessions
75
-
76
- Seats auto-pair by current working directory by default.
77
-
78
- If you want an explicit lane name, use:
79
-
80
43
  ```bash
81
- muuuuse 1 --session demo codex
82
- muuuuse 2 --session demo gemini
83
- muuuuse stop --session demo
44
+ gemini
84
45
  ```
85
46
 
86
- You can also inspect the lane:
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.
48
+
49
+ Check the live state from any terminal:
87
50
 
88
51
  ```bash
89
52
  muuuuse status
90
53
  ```
91
54
 
92
- ## Doctor
55
+ Stop the loop from any terminal, including one of the armed shells once it is back at a prompt:
93
56
 
94
57
  ```bash
95
- muuuuse doctor
58
+ muuuuse stop
96
59
  ```
97
60
 
98
- This checks the local runtime plus common agent binaries if you use them.
99
-
100
61
  ## Notes
101
62
 
102
- - local only
103
- - no tmux requirement for the main path
104
- - no remote control surface here; that belongs to `codeman`
105
- - best with programs that naturally produce turn-shaped output
63
+ - no tmux
64
+ - state lives under `~/.muuuuse`
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.3.2",
4
- "description": "🔌Muuuuse wraps two local programs and bounces final blocks between them from the terminal you launched.",
3
+ "version": "1.4.1",
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"
@@ -34,7 +34,6 @@
34
34
  "relay",
35
35
  "local",
36
36
  "automation",
37
- "script",
38
37
  "codex",
39
38
  "claude",
40
39
  "gemini"
package/src/agents.js CHANGED
@@ -4,79 +4,17 @@ const os = require("node:os");
4
4
  const path = require("node:path");
5
5
 
6
6
  const {
7
- SESSION_MATCH_WINDOW_MS,
8
7
  getFileSize,
9
8
  hashText,
10
9
  readAppendedText,
11
10
  sanitizeRelayText,
11
+ SESSION_MATCH_WINDOW_MS,
12
12
  } = require("./util");
13
13
 
14
- const PRESETS = {
15
- codex: {
16
- label: "Codex",
17
- command: [
18
- "codex",
19
- "-m",
20
- "gpt-5.4",
21
- "-c",
22
- "model_reasoning_effort=low",
23
- "--dangerously-bypass-approvals-and-sandbox",
24
- "--no-alt-screen",
25
- ],
26
- },
27
- claude: {
28
- label: "Claude Code",
29
- command: [
30
- "claude",
31
- "--dangerously-skip-permissions",
32
- "--permission-mode",
33
- "bypassPermissions",
34
- ],
35
- },
36
- gemini: {
37
- label: "Gemini CLI",
38
- command: [
39
- "gemini",
40
- "--approval-mode",
41
- "yolo",
42
- "--sandbox=false",
43
- ],
44
- },
45
- };
46
-
47
- function expandPresetCommand(commandTokens, usePresets = true) {
48
- if (!usePresets || !Array.isArray(commandTokens) || commandTokens.length !== 1) {
49
- return Array.isArray(commandTokens) ? [...commandTokens] : [];
50
- }
51
-
52
- const preset = PRESETS[String(commandTokens[0] || "").toLowerCase()];
53
- return preset ? [...preset.command] : [...commandTokens];
54
- }
55
-
56
- function detectAgentTypeFromCommand(commandTokens) {
57
- const executable = path.basename(String(commandTokens?.[0] || "")).toLowerCase();
58
- if (!executable) {
59
- return null;
60
- }
61
-
62
- if (executable === "codex") {
63
- return "codex";
64
- }
65
- if (executable === "claude") {
66
- return "claude";
67
- }
68
- if (executable === "gemini") {
69
- return "gemini";
70
- }
71
- return null;
72
- }
73
-
74
14
  const CODEX_ROOT = path.join(os.homedir(), ".codex", "sessions");
75
15
  const CLAUDE_ROOT = path.join(os.homedir(), ".claude", "projects");
76
16
  const GEMINI_ROOT = path.join(os.homedir(), ".gemini", "tmp");
77
- const CODEX_SNAPSHOT_ROOT = path.join(os.homedir(), ".codex", "shell_snapshots");
78
- const codexSnapshotPaneCache = new Map();
79
- const codexSnapshotExportsCache = new Map();
17
+ const SESSION_START_EARLY_TOLERANCE_MS = 2 * 1000;
80
18
 
81
19
  function walkFiles(rootPath, predicate, results = []) {
82
20
  try {
@@ -89,7 +27,7 @@ function walkFiles(rootPath, predicate, results = []) {
89
27
  results.push(absolutePath);
90
28
  }
91
29
  }
92
- } catch (error) {
30
+ } catch {
93
31
  return results;
94
32
  }
95
33
 
@@ -101,6 +39,17 @@ function commandMatches(args, command) {
101
39
  return pattern.test(args);
102
40
  }
103
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
+
104
53
  function detectAgent(processes) {
105
54
  const ordered = [...processes].sort((left, right) => left.elapsedSeconds - right.elapsedSeconds);
106
55
  for (const process of ordered) {
@@ -117,19 +66,10 @@ function detectAgent(processes) {
117
66
  return null;
118
67
  }
119
68
 
120
- function buildDetectedAgent(type, process) {
121
- return {
122
- type,
123
- pid: process.pid,
124
- args: process.args,
125
- elapsedSeconds: process.elapsedSeconds,
126
- processStartedAtMs: Date.now() - process.elapsedSeconds * 1000,
127
- };
128
- }
129
-
130
69
  function readFirstLines(filePath, maxLines = 20) {
131
70
  const lines = [];
132
71
  const fd = fs.openSync(filePath, "r");
72
+
133
73
  try {
134
74
  const buffer = Buffer.alloc(16384);
135
75
  const bytesRead = fs.readSync(fd, buffer, 0, buffer.length, 0);
@@ -146,13 +86,14 @@ function readFirstLines(filePath, maxLines = 20) {
146
86
  break;
147
87
  }
148
88
  }
89
+
149
90
  return lines;
150
91
  } finally {
151
92
  fs.closeSync(fd);
152
93
  }
153
94
  }
154
95
 
155
- function chooseCandidate(candidates, currentPath, processStartedAtMs) {
96
+ function selectSessionCandidatePath(candidates, currentPath, processStartedAtMs) {
156
97
  const cwdMatches = candidates.filter((candidate) => candidate.cwd === currentPath);
157
98
  if (cwdMatches.length === 0) {
158
99
  return null;
@@ -162,91 +103,29 @@ function chooseCandidate(candidates, currentPath, processStartedAtMs) {
162
103
  return cwdMatches[0].path;
163
104
  }
164
105
 
165
- if (processStartedAtMs !== null) {
166
- const preciseMatches = cwdMatches
167
- .map((candidate) => ({
168
- ...candidate,
169
- diffMs: Math.abs(candidate.startedAtMs - processStartedAtMs),
170
- }))
171
- .filter((candidate) => Number.isFinite(candidate.diffMs) && candidate.diffMs <= SESSION_MATCH_WINDOW_MS)
172
- .sort((left, right) => left.diffMs - right.diffMs || right.mtimeMs - left.mtimeMs);
173
-
174
- if (preciseMatches.length === 1) {
175
- return preciseMatches[0].path;
176
- }
177
- }
178
-
179
- return null;
180
- }
181
-
182
- function extractThreadId(filePath) {
183
- const match = path.basename(filePath).match(/([0-9a-f]{8}-[0-9a-f-]{27})\.jsonl$/i);
184
- return match ? match[1] : null;
185
- }
186
-
187
- function readCodexSnapshotPane(threadId) {
188
- if (!threadId) {
189
- return null;
190
- }
191
-
192
- if (codexSnapshotPaneCache.has(threadId)) {
193
- return codexSnapshotPaneCache.get(threadId);
194
- }
195
-
196
- const snapshotPath = path.join(CODEX_SNAPSHOT_ROOT, `${threadId}.sh`);
197
- try {
198
- const contents = fs.readFileSync(snapshotPath, "utf8");
199
- const match = contents.match(/declare -x TMUX_PANE="([^"]+)"/);
200
- const paneId = match ? match[1] : null;
201
- codexSnapshotPaneCache.set(threadId, paneId);
202
- return paneId;
203
- } catch (error) {
204
- codexSnapshotPaneCache.set(threadId, null);
106
+ if (!Number.isFinite(processStartedAtMs)) {
205
107
  return null;
206
108
  }
207
- }
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
109
 
239
- function snapshotEnvMatches(exportsMap, expectedEnv = null) {
240
- if (!expectedEnv || typeof expectedEnv !== "object") {
241
- return true;
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;
242
126
  }
243
127
 
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
- });
128
+ return null;
250
129
  }
251
130
 
252
131
  function readCodexCandidate(filePath) {
@@ -255,6 +134,7 @@ function readCodexCandidate(filePath) {
255
134
  if (!firstLine) {
256
135
  return null;
257
136
  }
137
+
258
138
  const entry = JSON.parse(firstLine);
259
139
  if (entry?.type !== "session_meta" || typeof entry.payload?.cwd !== "string") {
260
140
  return null;
@@ -262,46 +142,21 @@ function readCodexCandidate(filePath) {
262
142
 
263
143
  return {
264
144
  path: filePath,
265
- threadId: extractThreadId(filePath),
266
- snapshotExports: readCodexSnapshotExports(extractThreadId(filePath)),
267
- snapshotPaneId: readCodexSnapshotPane(extractThreadId(filePath)),
268
145
  cwd: entry.payload.cwd,
269
146
  startedAtMs: Date.parse(entry.payload.timestamp),
270
147
  mtimeMs: fs.statSync(filePath).mtimeMs,
271
148
  };
272
- } catch (error) {
149
+ } catch {
273
150
  return null;
274
151
  }
275
152
  }
276
153
 
277
- function selectCodexSessionFile(currentPath, processStartedAtMs, options = {}) {
278
- const paneId = options.paneId || null;
279
- const snapshotEnv = options.snapshotEnv || null;
154
+ function selectCodexSessionFile(currentPath, processStartedAtMs) {
280
155
  const candidates = walkFiles(CODEX_ROOT, (filePath) => filePath.endsWith(".jsonl"))
281
156
  .map((filePath) => readCodexCandidate(filePath))
282
157
  .filter((candidate) => candidate !== null);
283
158
 
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
-
295
- if (paneId) {
296
- const exactPaneMatches = scopedCandidates.filter((candidate) => candidate.snapshotPaneId === paneId);
297
- if (exactPaneMatches.length > 0) {
298
- scopedCandidates = exactPaneMatches;
299
- } else {
300
- scopedCandidates = scopedCandidates.filter((candidate) => candidate.snapshotPaneId === null);
301
- }
302
- }
303
-
304
- return chooseCandidate(scopedCandidates, currentPath, processStartedAtMs);
159
+ return selectSessionCandidatePath(candidates, currentPath, processStartedAtMs);
305
160
  }
306
161
 
307
162
  function extractCodexAssistantText(content) {
@@ -330,6 +185,10 @@ function parseCodexFinalLine(line) {
330
185
  return null;
331
186
  }
332
187
 
188
+ if (entry.payload?.phase !== "final_answer") {
189
+ return null;
190
+ }
191
+
333
192
  const text = sanitizeRelayText(extractCodexAssistantText(entry.payload.content));
334
193
  if (!text) {
335
194
  return null;
@@ -340,19 +199,33 @@ function parseCodexFinalLine(line) {
340
199
  text,
341
200
  timestamp: entry.timestamp || entry.payload.timestamp || new Date().toISOString(),
342
201
  };
343
- } catch (error) {
202
+ } catch {
344
203
  return null;
345
204
  }
346
205
  }
347
206
 
348
- function readCodexAnswers(filePath, offset) {
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) {
349
221
  const { nextOffset, text } = readAppendedText(filePath, offset);
350
222
  const answers = text
351
223
  .split("\n")
352
224
  .map((line) => line.trim())
353
225
  .filter((line) => line.length > 0)
354
226
  .map((line) => parseCodexFinalLine(line))
355
- .filter((entry) => entry !== null);
227
+ .filter((entry) => entry !== null)
228
+ .filter((entry) => isAnswerNewEnough(entry, sinceMs));
356
229
 
357
230
  return { nextOffset, answers };
358
231
  }
@@ -365,6 +238,7 @@ function readClaudeCandidate(filePath) {
365
238
  if (typeof entry.cwd !== "string") {
366
239
  continue;
367
240
  }
241
+
368
242
  return {
369
243
  path: filePath,
370
244
  cwd: entry.cwd,
@@ -373,7 +247,7 @@ function readClaudeCandidate(filePath) {
373
247
  };
374
248
  }
375
249
  return null;
376
- } catch (error) {
250
+ } catch {
377
251
  return null;
378
252
  }
379
253
  }
@@ -383,7 +257,7 @@ function selectClaudeSessionFile(currentPath, processStartedAtMs) {
383
257
  .map((filePath) => readClaudeCandidate(filePath))
384
258
  .filter((candidate) => candidate !== null);
385
259
 
386
- return chooseCandidate(candidates, currentPath, processStartedAtMs);
260
+ return selectSessionCandidatePath(candidates, currentPath, processStartedAtMs);
387
261
  }
388
262
 
389
263
  function extractClaudeAssistantText(content) {
@@ -422,19 +296,20 @@ function parseClaudeFinalLine(line) {
422
296
  text,
423
297
  timestamp: entry.timestamp || new Date().toISOString(),
424
298
  };
425
- } catch (error) {
299
+ } catch {
426
300
  return null;
427
301
  }
428
302
  }
429
303
 
430
- function readClaudeAnswers(filePath, offset) {
304
+ function readClaudeAnswers(filePath, offset, sinceMs = null) {
431
305
  const { nextOffset, text } = readAppendedText(filePath, offset);
432
306
  const answers = text
433
307
  .split("\n")
434
308
  .map((line) => line.trim())
435
309
  .filter((line) => line.length > 0)
436
310
  .map((line) => parseClaudeFinalLine(line))
437
- .filter((entry) => entry !== null);
311
+ .filter((entry) => entry !== null)
312
+ .filter((entry) => isAnswerNewEnough(entry, sinceMs));
438
313
 
439
314
  return { nextOffset, answers };
440
315
  }
@@ -446,12 +321,12 @@ function readGeminiCandidate(filePath) {
446
321
  return {
447
322
  path: filePath,
448
323
  projectHash: entry.projectHash,
449
- cwdHash: entry.projectHash,
324
+ cwd: entry.projectHash,
450
325
  startedAtMs: Date.parse(entry.startTime),
451
326
  mtimeMs: fs.statSync(filePath).mtimeMs,
452
327
  lastUpdatedMs: Date.parse(entry.lastUpdated),
453
328
  };
454
- } catch (error) {
329
+ } catch {
455
330
  return null;
456
331
  }
457
332
  }
@@ -462,32 +337,10 @@ function selectGeminiSessionFile(currentPath, processStartedAtMs) {
462
337
  .map((filePath) => readGeminiCandidate(filePath))
463
338
  .filter((candidate) => candidate !== null && candidate.projectHash === projectHash);
464
339
 
465
- if (candidates.length === 0) {
466
- return null;
467
- }
468
-
469
- if (processStartedAtMs !== null) {
470
- const preciseMatches = candidates
471
- .map((candidate) => ({
472
- ...candidate,
473
- diffMs: Math.abs(candidate.startedAtMs - processStartedAtMs),
474
- }))
475
- .filter((candidate) => Number.isFinite(candidate.diffMs) && candidate.diffMs <= SESSION_MATCH_WINDOW_MS)
476
- .sort((left, right) => left.diffMs - right.diffMs || right.lastUpdatedMs - left.lastUpdatedMs);
477
-
478
- if (preciseMatches.length === 1) {
479
- return preciseMatches[0].path;
480
- }
481
- }
482
-
483
- if (candidates.length === 1) {
484
- return candidates[0].path;
485
- }
486
-
487
- return null;
340
+ return selectSessionCandidatePath(candidates, projectHash, processStartedAtMs);
488
341
  }
489
342
 
490
- function readGeminiAnswers(filePath, lastMessageId = null) {
343
+ function readGeminiAnswers(filePath, lastMessageId = null, sinceMs = null) {
491
344
  try {
492
345
  const entry = JSON.parse(fs.readFileSync(filePath, "utf8"));
493
346
  const messages = Array.isArray(entry.messages) ? entry.messages : [];
@@ -509,11 +362,13 @@ function readGeminiAnswers(filePath, lastMessageId = null) {
509
362
  }));
510
363
 
511
364
  return {
512
- answers: answers.filter((answer) => answer.text.length > 0),
365
+ answers: answers
366
+ .filter((answer) => answer.text.length > 0)
367
+ .filter((answer) => isAnswerNewEnough(answer, sinceMs)),
513
368
  lastMessageId: finalMessages.length > 0 ? finalMessages[finalMessages.length - 1].id : lastMessageId,
514
369
  fileSize: getFileSize(filePath),
515
370
  };
516
- } catch (error) {
371
+ } catch {
517
372
  return {
518
373
  answers: [],
519
374
  lastMessageId,
@@ -523,16 +378,13 @@ function readGeminiAnswers(filePath, lastMessageId = null) {
523
378
  }
524
379
 
525
380
  module.exports = {
526
- PRESETS,
527
- chooseCandidate,
528
381
  detectAgent,
529
- detectAgentTypeFromCommand,
530
- expandPresetCommand,
531
382
  parseClaudeFinalLine,
532
383
  parseCodexFinalLine,
533
384
  readClaudeAnswers,
534
385
  readCodexAnswers,
535
386
  readGeminiAnswers,
387
+ selectSessionCandidatePath,
536
388
  selectClaudeSessionFile,
537
389
  selectCodexSessionFile,
538
390
  selectGeminiSessionFile,