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 +33 -66
- package/package.json +2 -3
- package/src/agents.js +76 -224
- package/src/cli.js +47 -119
- package/src/runtime.js +625 -373
- package/src/util.js +43 -123
- package/src/tmux.js +0 -170
package/README.md
CHANGED
|
@@ -1,105 +1,72 @@
|
|
|
1
1
|
# 🔌Muuuuse
|
|
2
2
|
|
|
3
|
-
`
|
|
3
|
+
`🔌Muuuuse` is a tiny no-tmux terminal relay.
|
|
4
4
|
|
|
5
|
-
|
|
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
|
|
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
|
|
17
|
-
muuuuse 2
|
|
15
|
+
muuuuse 1
|
|
16
|
+
muuuuse 2
|
|
17
|
+
muuuuse status
|
|
18
18
|
muuuuse stop
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
|
|
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
|
|
26
|
+
muuuuse 1
|
|
37
27
|
```
|
|
38
28
|
|
|
39
29
|
Terminal 2:
|
|
40
30
|
|
|
41
31
|
```bash
|
|
42
|
-
muuuuse 2
|
|
32
|
+
muuuuse 2
|
|
43
33
|
```
|
|
44
34
|
|
|
45
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
82
|
-
muuuuse 2 --session demo gemini
|
|
83
|
-
muuuuse stop --session demo
|
|
44
|
+
gemini
|
|
84
45
|
```
|
|
85
46
|
|
|
86
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
-
|
|
103
|
-
-
|
|
104
|
-
-
|
|
105
|
-
-
|
|
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.
|
|
4
|
-
"description": "🔌Muuuuse
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
|
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
|
|
149
|
+
} catch {
|
|
273
150
|
return null;
|
|
274
151
|
}
|
|
275
152
|
}
|
|
276
153
|
|
|
277
|
-
function selectCodexSessionFile(currentPath, processStartedAtMs
|
|
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
|
-
|
|
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
|
|
202
|
+
} catch {
|
|
344
203
|
return null;
|
|
345
204
|
}
|
|
346
205
|
}
|
|
347
206
|
|
|
348
|
-
function
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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,
|