muuuuse 1.4.2 → 2.1.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 +3 -1
- package/package.json +1 -1
- package/src/agents.js +306 -11
- package/src/cli.js +3 -0
- package/src/runtime.js +593 -39
- package/src/util.js +70 -4
package/README.md
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
It does one job:
|
|
6
6
|
- arm terminal one with `muuuuse 1`
|
|
7
7
|
- arm terminal two with `muuuuse 2`
|
|
8
|
+
- have seat 1 generate a session key and seat 2 sign it
|
|
8
9
|
- watch Codex, Claude, or Gemini for real final answers
|
|
9
10
|
- inject that final answer into the other armed terminal
|
|
10
11
|
- keep looping until you stop it
|
|
@@ -32,7 +33,7 @@ Terminal 2:
|
|
|
32
33
|
muuuuse 2
|
|
33
34
|
```
|
|
34
35
|
|
|
35
|
-
Now both shells are armed. Use
|
|
36
|
+
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.
|
|
36
37
|
|
|
37
38
|
If you want Codex in one and Gemini in the other, start them inside the armed shells:
|
|
38
39
|
|
|
@@ -62,6 +63,7 @@ muuuuse stop
|
|
|
62
63
|
|
|
63
64
|
- no tmux
|
|
64
65
|
- state lives under `~/.muuuuse`
|
|
66
|
+
- only the signed armed pair can exchange relay events
|
|
65
67
|
- supported final-answer detection is built for Codex, Claude, and Gemini
|
|
66
68
|
- `codeman` remains the larger transport/control layer; `muuuuse` stays local and minimal
|
|
67
69
|
|
package/package.json
CHANGED
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,6 +156,126 @@ function selectSessionCandidatePath(candidates, currentPath, processStartedAtMs)
|
|
|
128
156
|
return null;
|
|
129
157
|
}
|
|
130
158
|
|
|
159
|
+
function readCodexSeatClaim(sessionId) {
|
|
160
|
+
if (!sessionId) {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const snapshotPath = path.join(CODEX_SNAPSHOT_ROOT, `${sessionId}.sh`);
|
|
165
|
+
try {
|
|
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
|
+
}
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
seatId: seatMatch[1],
|
|
175
|
+
sessionName: sessionMatch[1],
|
|
176
|
+
};
|
|
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) {
|
|
226
|
+
return [];
|
|
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];
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function selectLiveSessionCandidatePath(candidates, currentPath, captureSinceMs = null) {
|
|
254
|
+
const cwdMatches = candidates.filter((candidate) => candidate.cwd === currentPath);
|
|
255
|
+
if (cwdMatches.length === 0) {
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const primary = cwdMatches.some((candidate) => candidate.isSubagent === false)
|
|
260
|
+
? cwdMatches.filter((candidate) => candidate.isSubagent === false)
|
|
261
|
+
: cwdMatches;
|
|
262
|
+
|
|
263
|
+
const recent = Number.isFinite(captureSinceMs)
|
|
264
|
+
? primary.filter((candidate) => Number.isFinite(candidate.mtimeMs) && candidate.mtimeMs >= captureSinceMs - SESSION_START_EARLY_TOLERANCE_MS)
|
|
265
|
+
: primary;
|
|
266
|
+
const ranked = (recent.length > 0 ? recent : primary)
|
|
267
|
+
.slice()
|
|
268
|
+
.sort((left, right) => right.mtimeMs - left.mtimeMs || right.startedAtMs - left.startedAtMs);
|
|
269
|
+
|
|
270
|
+
return ranked[0]?.path || null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function readOpenSessionCandidates(pids, rootPath, reader) {
|
|
274
|
+
return listOpenFilePathsForPids(pids, rootPath)
|
|
275
|
+
.map((filePath) => reader(filePath))
|
|
276
|
+
.filter((candidate) => candidate !== null);
|
|
277
|
+
}
|
|
278
|
+
|
|
131
279
|
function readCodexCandidate(filePath) {
|
|
132
280
|
try {
|
|
133
281
|
const [firstLine] = readFirstLines(filePath, 1);
|
|
@@ -143,6 +291,8 @@ function readCodexCandidate(filePath) {
|
|
|
143
291
|
return {
|
|
144
292
|
path: filePath,
|
|
145
293
|
cwd: entry.payload.cwd,
|
|
294
|
+
isSubagent: Boolean(entry.payload?.source?.subagent),
|
|
295
|
+
sessionId: entry.payload.id || null,
|
|
146
296
|
startedAtMs: Date.parse(entry.payload.timestamp),
|
|
147
297
|
mtimeMs: fs.statSync(filePath).mtimeMs,
|
|
148
298
|
};
|
|
@@ -151,12 +301,143 @@ function readCodexCandidate(filePath) {
|
|
|
151
301
|
}
|
|
152
302
|
}
|
|
153
303
|
|
|
154
|
-
function
|
|
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
|
+
|
|
423
|
+
function selectCodexSessionFile(currentPath, processStartedAtMs, options = {}) {
|
|
424
|
+
const liveCandidates = readOpenSessionCandidates(options.pids ?? options.pid, CODEX_ROOT, readCodexCandidate);
|
|
425
|
+
const livePath = selectCodexCandidatePath(liveCandidates, currentPath, processStartedAtMs, {
|
|
426
|
+
...options,
|
|
427
|
+
allowUnclaimedSingleCandidate: true,
|
|
428
|
+
});
|
|
429
|
+
if (livePath) {
|
|
430
|
+
return livePath;
|
|
431
|
+
}
|
|
432
|
+
|
|
155
433
|
const candidates = walkFiles(CODEX_ROOT, (filePath) => filePath.endsWith(".jsonl"))
|
|
156
434
|
.map((filePath) => readCodexCandidate(filePath))
|
|
157
435
|
.filter((candidate) => candidate !== null);
|
|
158
436
|
|
|
159
|
-
return
|
|
437
|
+
return selectCodexCandidatePath(candidates, currentPath, processStartedAtMs, {
|
|
438
|
+
...options,
|
|
439
|
+
allowUnclaimedSingleCandidate: false,
|
|
440
|
+
});
|
|
160
441
|
}
|
|
161
442
|
|
|
162
443
|
function extractCodexAssistantText(content) {
|
|
@@ -252,7 +533,13 @@ function readClaudeCandidate(filePath) {
|
|
|
252
533
|
}
|
|
253
534
|
}
|
|
254
535
|
|
|
255
|
-
function selectClaudeSessionFile(currentPath, processStartedAtMs) {
|
|
536
|
+
function selectClaudeSessionFile(currentPath, processStartedAtMs, options = {}) {
|
|
537
|
+
const liveCandidates = readOpenSessionCandidates(options.pids ?? options.pid, CLAUDE_ROOT, readClaudeCandidate);
|
|
538
|
+
const livePath = selectLiveSessionCandidatePath(liveCandidates, currentPath, options.captureSinceMs);
|
|
539
|
+
if (livePath) {
|
|
540
|
+
return livePath;
|
|
541
|
+
}
|
|
542
|
+
|
|
256
543
|
const candidates = walkFiles(CLAUDE_ROOT, (filePath) => filePath.endsWith(".jsonl"))
|
|
257
544
|
.map((filePath) => readClaudeCandidate(filePath))
|
|
258
545
|
.filter((candidate) => candidate !== null);
|
|
@@ -331,8 +618,15 @@ function readGeminiCandidate(filePath) {
|
|
|
331
618
|
}
|
|
332
619
|
}
|
|
333
620
|
|
|
334
|
-
function selectGeminiSessionFile(currentPath, processStartedAtMs) {
|
|
621
|
+
function selectGeminiSessionFile(currentPath, processStartedAtMs, options = {}) {
|
|
335
622
|
const projectHash = createHash("sha256").update(currentPath).digest("hex");
|
|
623
|
+
const liveCandidates = readOpenSessionCandidates(options.pids ?? options.pid, GEMINI_ROOT, readGeminiCandidate)
|
|
624
|
+
.filter((candidate) => candidate.projectHash === projectHash);
|
|
625
|
+
const livePath = selectLiveSessionCandidatePath(liveCandidates, projectHash, options.captureSinceMs);
|
|
626
|
+
if (livePath) {
|
|
627
|
+
return livePath;
|
|
628
|
+
}
|
|
629
|
+
|
|
336
630
|
const candidates = walkFiles(GEMINI_ROOT, (filePath) => filePath.endsWith(".json"))
|
|
337
631
|
.map((filePath) => readGeminiCandidate(filePath))
|
|
338
632
|
.filter((candidate) => candidate !== null && candidate.projectHash === projectHash);
|
|
@@ -384,6 +678,7 @@ module.exports = {
|
|
|
384
678
|
readClaudeAnswers,
|
|
385
679
|
readCodexAnswers,
|
|
386
680
|
readGeminiAnswers,
|
|
681
|
+
selectLiveSessionCandidatePath,
|
|
387
682
|
selectSessionCandidatePath,
|
|
388
683
|
selectClaudeSessionFile,
|
|
389
684
|
selectCodexSessionFile,
|
package/src/cli.js
CHANGED