muuuuse 0.1.0 → 1.3.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/LICENSE +21 -0
- package/README.md +103 -3
- package/bin/muuse.js +9 -0
- package/package.json +42 -8
- package/src/agents.js +474 -0
- package/src/cli.js +120 -0
- package/src/runtime.js +622 -0
- package/src/tmux.js +170 -0
- package/src/util.js +310 -0
- package/index.js +0 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 lmtlssss
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,5 +1,105 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 🔌Muuuuse
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`muuuuse` installs one CLI:
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
- `muuuuse`
|
|
6
|
+
|
|
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:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
muuuuse 1 <program...>
|
|
17
|
+
muuuuse 2 <program...>
|
|
18
|
+
muuuuse 3 stop
|
|
19
|
+
```
|
|
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 3 stop` is the cleanup command for that lane.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install -g muuuuse
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Fastest AI Flow
|
|
32
|
+
|
|
33
|
+
Terminal 1:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
muuuuse 1 codex
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Terminal 2:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
muuuuse 2 gemini
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Terminal 3:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
muuuuse 3 stop
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Known presets expand to recommended flags automatically:
|
|
52
|
+
|
|
53
|
+
- `codex`
|
|
54
|
+
- `claude`
|
|
55
|
+
- `gemini`
|
|
56
|
+
|
|
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:
|
|
64
|
+
|
|
65
|
+
```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'
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Type into one seat and the other seat will receive the relayed block.
|
|
71
|
+
|
|
72
|
+
For Codex, Claude, and Gemini, `🔌Muuuuse` prefers their structured session logs. For anything else, it 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
|
+
```bash
|
|
81
|
+
muuuuse 1 --session demo codex
|
|
82
|
+
muuuuse 2 --session demo gemini
|
|
83
|
+
muuuuse 3 --session demo stop
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
You can also inspect the lane:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
muuuuse 3 status
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Doctor
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
muuuuse doctor
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
This checks the local runtime plus common agent binaries if you use them.
|
|
99
|
+
|
|
100
|
+
## Notes
|
|
101
|
+
|
|
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
|
package/bin/muuse.js
ADDED
package/package.json
CHANGED
|
@@ -1,16 +1,50 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "muuuuse",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
5
|
-
"
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"description": "🔌Muuuuse wraps two local programs and bounces final blocks between them from the terminal you launched.",
|
|
5
|
+
"type": "commonjs",
|
|
6
|
+
"bin": {
|
|
7
|
+
"muuuuse": "bin/muuse.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"src",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=18"
|
|
17
|
+
},
|
|
18
|
+
"homepage": "https://muuuuse.lmtlssss.fun",
|
|
6
19
|
"repository": {
|
|
7
20
|
"type": "git",
|
|
8
|
-
"url": "https://github.com/lmtlssss/muuuuse.git"
|
|
21
|
+
"url": "git+https://github.com/lmtlssss/muuuuse.git"
|
|
9
22
|
},
|
|
10
|
-
"
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/lmtlssss/muuuuse/issues"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"license": "MIT",
|
|
11
30
|
"keywords": [
|
|
12
|
-
"
|
|
13
|
-
"
|
|
31
|
+
"terminal",
|
|
32
|
+
"ai",
|
|
33
|
+
"pty",
|
|
34
|
+
"relay",
|
|
35
|
+
"local",
|
|
36
|
+
"automation",
|
|
37
|
+
"script",
|
|
38
|
+
"codex",
|
|
39
|
+
"claude",
|
|
40
|
+
"gemini"
|
|
14
41
|
],
|
|
15
|
-
"
|
|
42
|
+
"scripts": {
|
|
43
|
+
"test": "node test/cli.test.js",
|
|
44
|
+
"pack:local": "npm pack",
|
|
45
|
+
"prepublishOnly": "npm test"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"node-pty": "^1.1.0"
|
|
49
|
+
}
|
|
16
50
|
}
|
package/src/agents.js
ADDED
|
@@ -0,0 +1,474 @@
|
|
|
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
|
+
SESSION_MATCH_WINDOW_MS,
|
|
8
|
+
getFileSize,
|
|
9
|
+
hashText,
|
|
10
|
+
readAppendedText,
|
|
11
|
+
sanitizeRelayText,
|
|
12
|
+
} = require("./util");
|
|
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
|
+
const CODEX_ROOT = path.join(os.homedir(), ".codex", "sessions");
|
|
75
|
+
const CLAUDE_ROOT = path.join(os.homedir(), ".claude", "projects");
|
|
76
|
+
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
|
+
|
|
80
|
+
function walkFiles(rootPath, predicate, results = []) {
|
|
81
|
+
try {
|
|
82
|
+
const entries = fs.readdirSync(rootPath, { withFileTypes: true });
|
|
83
|
+
for (const entry of entries) {
|
|
84
|
+
const absolutePath = path.join(rootPath, entry.name);
|
|
85
|
+
if (entry.isDirectory()) {
|
|
86
|
+
walkFiles(absolutePath, predicate, results);
|
|
87
|
+
} else if (predicate(absolutePath)) {
|
|
88
|
+
results.push(absolutePath);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
} catch (error) {
|
|
92
|
+
return results;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return results;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function commandMatches(args, command) {
|
|
99
|
+
const pattern = new RegExp(`(^|[\\\\/\\s])${command}(\\s|$)`, "i");
|
|
100
|
+
return pattern.test(args);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function detectAgent(processes) {
|
|
104
|
+
const ordered = [...processes].sort((left, right) => left.elapsedSeconds - right.elapsedSeconds);
|
|
105
|
+
for (const process of ordered) {
|
|
106
|
+
if (commandMatches(process.args, "codex")) {
|
|
107
|
+
return buildDetectedAgent("codex", process);
|
|
108
|
+
}
|
|
109
|
+
if (commandMatches(process.args, "claude")) {
|
|
110
|
+
return buildDetectedAgent("claude", process);
|
|
111
|
+
}
|
|
112
|
+
if (commandMatches(process.args, "gemini")) {
|
|
113
|
+
return buildDetectedAgent("gemini", process);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function buildDetectedAgent(type, process) {
|
|
120
|
+
return {
|
|
121
|
+
type,
|
|
122
|
+
pid: process.pid,
|
|
123
|
+
args: process.args,
|
|
124
|
+
elapsedSeconds: process.elapsedSeconds,
|
|
125
|
+
processStartedAtMs: Date.now() - process.elapsedSeconds * 1000,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function readFirstLines(filePath, maxLines = 20) {
|
|
130
|
+
const lines = [];
|
|
131
|
+
const fd = fs.openSync(filePath, "r");
|
|
132
|
+
try {
|
|
133
|
+
const buffer = Buffer.alloc(16384);
|
|
134
|
+
const bytesRead = fs.readSync(fd, buffer, 0, buffer.length, 0);
|
|
135
|
+
if (bytesRead === 0) {
|
|
136
|
+
return lines;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
for (const line of buffer.toString("utf8", 0, bytesRead).split("\n")) {
|
|
140
|
+
if (line.trim().length === 0) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
lines.push(line.trim());
|
|
144
|
+
if (lines.length >= maxLines) {
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return lines;
|
|
149
|
+
} finally {
|
|
150
|
+
fs.closeSync(fd);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function chooseCandidate(candidates, currentPath, processStartedAtMs) {
|
|
155
|
+
const cwdMatches = candidates.filter((candidate) => candidate.cwd === currentPath);
|
|
156
|
+
if (cwdMatches.length === 0) {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (processStartedAtMs !== null) {
|
|
161
|
+
const preciseMatches = cwdMatches
|
|
162
|
+
.map((candidate) => ({
|
|
163
|
+
...candidate,
|
|
164
|
+
diffMs: Math.abs(candidate.startedAtMs - processStartedAtMs),
|
|
165
|
+
}))
|
|
166
|
+
.filter((candidate) => Number.isFinite(candidate.diffMs) && candidate.diffMs <= SESSION_MATCH_WINDOW_MS)
|
|
167
|
+
.sort((left, right) => left.diffMs - right.diffMs || right.mtimeMs - left.mtimeMs);
|
|
168
|
+
|
|
169
|
+
if (preciseMatches.length > 0) {
|
|
170
|
+
return preciseMatches[0].path;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const fallback = cwdMatches.sort((left, right) => right.mtimeMs - left.mtimeMs)[0];
|
|
175
|
+
return fallback ? fallback.path : null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function extractThreadId(filePath) {
|
|
179
|
+
const match = path.basename(filePath).match(/([0-9a-f]{8}-[0-9a-f-]{27})\.jsonl$/i);
|
|
180
|
+
return match ? match[1] : null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function readCodexSnapshotPane(threadId) {
|
|
184
|
+
if (!threadId) {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (codexSnapshotPaneCache.has(threadId)) {
|
|
189
|
+
return codexSnapshotPaneCache.get(threadId);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const snapshotPath = path.join(CODEX_SNAPSHOT_ROOT, `${threadId}.sh`);
|
|
193
|
+
try {
|
|
194
|
+
const contents = fs.readFileSync(snapshotPath, "utf8");
|
|
195
|
+
const match = contents.match(/declare -x TMUX_PANE="([^"]+)"/);
|
|
196
|
+
const paneId = match ? match[1] : null;
|
|
197
|
+
codexSnapshotPaneCache.set(threadId, paneId);
|
|
198
|
+
return paneId;
|
|
199
|
+
} catch (error) {
|
|
200
|
+
codexSnapshotPaneCache.set(threadId, null);
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function readCodexCandidate(filePath) {
|
|
206
|
+
try {
|
|
207
|
+
const [firstLine] = readFirstLines(filePath, 1);
|
|
208
|
+
if (!firstLine) {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
const entry = JSON.parse(firstLine);
|
|
212
|
+
if (entry?.type !== "session_meta" || typeof entry.payload?.cwd !== "string") {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
path: filePath,
|
|
218
|
+
threadId: extractThreadId(filePath),
|
|
219
|
+
snapshotPaneId: readCodexSnapshotPane(extractThreadId(filePath)),
|
|
220
|
+
cwd: entry.payload.cwd,
|
|
221
|
+
startedAtMs: Date.parse(entry.payload.timestamp),
|
|
222
|
+
mtimeMs: fs.statSync(filePath).mtimeMs,
|
|
223
|
+
};
|
|
224
|
+
} catch (error) {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function selectCodexSessionFile(currentPath, processStartedAtMs, paneId = null) {
|
|
230
|
+
const candidates = walkFiles(CODEX_ROOT, (filePath) => filePath.endsWith(".jsonl"))
|
|
231
|
+
.map((filePath) => readCodexCandidate(filePath))
|
|
232
|
+
.filter((candidate) => candidate !== null);
|
|
233
|
+
|
|
234
|
+
let scopedCandidates = candidates;
|
|
235
|
+
if (paneId) {
|
|
236
|
+
const exactPaneMatches = scopedCandidates.filter((candidate) => candidate.snapshotPaneId === paneId);
|
|
237
|
+
if (exactPaneMatches.length > 0) {
|
|
238
|
+
scopedCandidates = exactPaneMatches;
|
|
239
|
+
} else {
|
|
240
|
+
scopedCandidates = scopedCandidates.filter((candidate) => candidate.snapshotPaneId === null);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return chooseCandidate(scopedCandidates, currentPath, processStartedAtMs);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function extractCodexAssistantText(content) {
|
|
248
|
+
if (!Array.isArray(content)) {
|
|
249
|
+
return "";
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return content
|
|
253
|
+
.flatMap((item) => {
|
|
254
|
+
if (!item || typeof item !== "object") {
|
|
255
|
+
return [];
|
|
256
|
+
}
|
|
257
|
+
if (item.type === "output_text" && typeof item.text === "string") {
|
|
258
|
+
return [item.text.trim()];
|
|
259
|
+
}
|
|
260
|
+
return [];
|
|
261
|
+
})
|
|
262
|
+
.filter((text) => text.length > 0)
|
|
263
|
+
.join("\n");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function parseCodexFinalLine(line) {
|
|
267
|
+
try {
|
|
268
|
+
const entry = JSON.parse(line);
|
|
269
|
+
if (entry?.type !== "response_item" || entry.payload?.type !== "message" || entry.payload?.role !== "assistant") {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const text = sanitizeRelayText(extractCodexAssistantText(entry.payload.content));
|
|
274
|
+
if (!text) {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
id: entry.payload.id || hashText(line),
|
|
280
|
+
text,
|
|
281
|
+
timestamp: entry.timestamp || entry.payload.timestamp || new Date().toISOString(),
|
|
282
|
+
};
|
|
283
|
+
} catch (error) {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function readCodexAnswers(filePath, offset) {
|
|
289
|
+
const { nextOffset, text } = readAppendedText(filePath, offset);
|
|
290
|
+
const answers = text
|
|
291
|
+
.split("\n")
|
|
292
|
+
.map((line) => line.trim())
|
|
293
|
+
.filter((line) => line.length > 0)
|
|
294
|
+
.map((line) => parseCodexFinalLine(line))
|
|
295
|
+
.filter((entry) => entry !== null);
|
|
296
|
+
|
|
297
|
+
return { nextOffset, answers };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function readClaudeCandidate(filePath) {
|
|
301
|
+
try {
|
|
302
|
+
const lines = readFirstLines(filePath, 12);
|
|
303
|
+
for (const line of lines) {
|
|
304
|
+
const entry = JSON.parse(line);
|
|
305
|
+
if (typeof entry.cwd !== "string") {
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
return {
|
|
309
|
+
path: filePath,
|
|
310
|
+
cwd: entry.cwd,
|
|
311
|
+
startedAtMs: Date.parse(entry.timestamp || entry.message?.timestamp || 0),
|
|
312
|
+
mtimeMs: fs.statSync(filePath).mtimeMs,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
return null;
|
|
316
|
+
} catch (error) {
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function selectClaudeSessionFile(currentPath, processStartedAtMs) {
|
|
322
|
+
const candidates = walkFiles(CLAUDE_ROOT, (filePath) => filePath.endsWith(".jsonl"))
|
|
323
|
+
.map((filePath) => readClaudeCandidate(filePath))
|
|
324
|
+
.filter((candidate) => candidate !== null);
|
|
325
|
+
|
|
326
|
+
return chooseCandidate(candidates, currentPath, processStartedAtMs);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function extractClaudeAssistantText(content) {
|
|
330
|
+
if (!Array.isArray(content)) {
|
|
331
|
+
return "";
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return content
|
|
335
|
+
.flatMap((item) => {
|
|
336
|
+
if (!item || typeof item !== "object") {
|
|
337
|
+
return [];
|
|
338
|
+
}
|
|
339
|
+
if (item.type === "text" && typeof item.text === "string") {
|
|
340
|
+
return [item.text.trim()];
|
|
341
|
+
}
|
|
342
|
+
return [];
|
|
343
|
+
})
|
|
344
|
+
.filter((text) => text.length > 0)
|
|
345
|
+
.join("\n");
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function parseClaudeFinalLine(line) {
|
|
349
|
+
try {
|
|
350
|
+
const entry = JSON.parse(line);
|
|
351
|
+
if (entry?.type !== "assistant" || entry.message?.role !== "assistant" || entry.message?.stop_reason !== "end_turn") {
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const text = sanitizeRelayText(extractClaudeAssistantText(entry.message.content));
|
|
356
|
+
if (!text) {
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return {
|
|
361
|
+
id: entry.uuid || entry.message.id || hashText(line),
|
|
362
|
+
text,
|
|
363
|
+
timestamp: entry.timestamp || new Date().toISOString(),
|
|
364
|
+
};
|
|
365
|
+
} catch (error) {
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function readClaudeAnswers(filePath, offset) {
|
|
371
|
+
const { nextOffset, text } = readAppendedText(filePath, offset);
|
|
372
|
+
const answers = text
|
|
373
|
+
.split("\n")
|
|
374
|
+
.map((line) => line.trim())
|
|
375
|
+
.filter((line) => line.length > 0)
|
|
376
|
+
.map((line) => parseClaudeFinalLine(line))
|
|
377
|
+
.filter((entry) => entry !== null);
|
|
378
|
+
|
|
379
|
+
return { nextOffset, answers };
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function readGeminiCandidate(filePath) {
|
|
383
|
+
try {
|
|
384
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
385
|
+
const entry = JSON.parse(raw);
|
|
386
|
+
return {
|
|
387
|
+
path: filePath,
|
|
388
|
+
projectHash: entry.projectHash,
|
|
389
|
+
cwdHash: entry.projectHash,
|
|
390
|
+
startedAtMs: Date.parse(entry.startTime),
|
|
391
|
+
mtimeMs: fs.statSync(filePath).mtimeMs,
|
|
392
|
+
lastUpdatedMs: Date.parse(entry.lastUpdated),
|
|
393
|
+
};
|
|
394
|
+
} catch (error) {
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function selectGeminiSessionFile(currentPath, processStartedAtMs) {
|
|
400
|
+
const projectHash = createHash("sha256").update(currentPath).digest("hex");
|
|
401
|
+
const candidates = walkFiles(GEMINI_ROOT, (filePath) => filePath.endsWith(".json"))
|
|
402
|
+
.map((filePath) => readGeminiCandidate(filePath))
|
|
403
|
+
.filter((candidate) => candidate !== null && candidate.projectHash === projectHash);
|
|
404
|
+
|
|
405
|
+
if (candidates.length === 0) {
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (processStartedAtMs !== null) {
|
|
410
|
+
const preciseMatches = candidates
|
|
411
|
+
.map((candidate) => ({
|
|
412
|
+
...candidate,
|
|
413
|
+
diffMs: Math.abs(candidate.startedAtMs - processStartedAtMs),
|
|
414
|
+
}))
|
|
415
|
+
.filter((candidate) => Number.isFinite(candidate.diffMs) && candidate.diffMs <= SESSION_MATCH_WINDOW_MS)
|
|
416
|
+
.sort((left, right) => left.diffMs - right.diffMs || right.lastUpdatedMs - left.lastUpdatedMs);
|
|
417
|
+
|
|
418
|
+
if (preciseMatches.length > 0) {
|
|
419
|
+
return preciseMatches[0].path;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return candidates.sort((left, right) => right.lastUpdatedMs - left.lastUpdatedMs || right.mtimeMs - left.mtimeMs)[0].path;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function readGeminiAnswers(filePath, lastMessageId = null) {
|
|
427
|
+
try {
|
|
428
|
+
const entry = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
429
|
+
const messages = Array.isArray(entry.messages) ? entry.messages : [];
|
|
430
|
+
const finalMessages = messages.filter((message) => {
|
|
431
|
+
const toolCalls = Array.isArray(message.toolCalls) ? message.toolCalls : [];
|
|
432
|
+
return message.type === "gemini" && typeof message.content === "string" && message.content.trim() && toolCalls.length === 0;
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
let startIndex = 0;
|
|
436
|
+
if (lastMessageId) {
|
|
437
|
+
const previousIndex = finalMessages.findIndex((message) => message.id === lastMessageId);
|
|
438
|
+
startIndex = previousIndex === -1 ? finalMessages.length : previousIndex + 1;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const answers = finalMessages.slice(startIndex).map((message) => ({
|
|
442
|
+
id: message.id || hashText(JSON.stringify(message)),
|
|
443
|
+
text: sanitizeRelayText(message.content),
|
|
444
|
+
timestamp: message.timestamp || entry.lastUpdated || new Date().toISOString(),
|
|
445
|
+
}));
|
|
446
|
+
|
|
447
|
+
return {
|
|
448
|
+
answers: answers.filter((answer) => answer.text.length > 0),
|
|
449
|
+
lastMessageId: finalMessages.length > 0 ? finalMessages[finalMessages.length - 1].id : lastMessageId,
|
|
450
|
+
fileSize: getFileSize(filePath),
|
|
451
|
+
};
|
|
452
|
+
} catch (error) {
|
|
453
|
+
return {
|
|
454
|
+
answers: [],
|
|
455
|
+
lastMessageId,
|
|
456
|
+
fileSize: 0,
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
module.exports = {
|
|
462
|
+
PRESETS,
|
|
463
|
+
detectAgent,
|
|
464
|
+
detectAgentTypeFromCommand,
|
|
465
|
+
expandPresetCommand,
|
|
466
|
+
parseClaudeFinalLine,
|
|
467
|
+
parseCodexFinalLine,
|
|
468
|
+
readClaudeAnswers,
|
|
469
|
+
readCodexAnswers,
|
|
470
|
+
readGeminiAnswers,
|
|
471
|
+
selectClaudeSessionFile,
|
|
472
|
+
selectCodexSessionFile,
|
|
473
|
+
selectGeminiSessionFile,
|
|
474
|
+
};
|