muuuuse 0.1.0 → 0.2.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 +115 -3
- package/bin/muuse.js +9 -0
- package/package.json +37 -8
- package/src/agents.js +445 -0
- package/src/cli.js +193 -0
- package/src/runtime.js +683 -0
- package/src/tmux.js +170 -0
- package/src/util.js +313 -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,117 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 🔌Muuuuse
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`muuuuse` installs one CLI name:
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
- `muuuuse`
|
|
6
|
+
|
|
7
|
+
The visible product brand is always `🔌Muuuuse`, while the terminal command examples use `muuuuse`.
|
|
8
|
+
|
|
9
|
+
## What It Does
|
|
10
|
+
|
|
11
|
+
`🔌Muuuuse` is the small local-only relay for three tmux terminals:
|
|
12
|
+
|
|
13
|
+
- seat `1` listens in one terminal
|
|
14
|
+
- seat `2` listens in another terminal
|
|
15
|
+
- seat `3` is the controller that auto-pairs them
|
|
16
|
+
|
|
17
|
+
Once seats `1` and `2` are armed, you can launch Codex, Claude, Gemini, or a deterministic script inside those two terminals. `muuuuse 3` then relays only final answers between them by injecting text plus `Enter` into the opposite seat.
|
|
18
|
+
|
|
19
|
+
Remote control is intentionally out of scope here. Use `codeman` or `codemansbot` for remote routing.
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install -g muuuuse
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Basic Flow
|
|
28
|
+
|
|
29
|
+
Terminal 1:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
muuuuse 1
|
|
33
|
+
codex -m gpt-5.4 -c model_reasoning_effort=low --dangerously-bypass-approvals-and-sandbox --no-alt-screen
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Terminal 2:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
muuuuse 2
|
|
40
|
+
claude --dangerously-skip-permissions --permission-mode bypassPermissions
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Terminal 3:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
muuuuse 3 "Start by proposing the first concrete repo task."
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
That third command auto-pairs seats `1` and `2`, then optionally drops a one-time kickoff prompt into seat `1`.
|
|
50
|
+
|
|
51
|
+
## Preset Launches
|
|
52
|
+
|
|
53
|
+
`🔌Muuuuse` does not launch the CLIs for you anymore. It arms the terminal, watches the live process, and reads only final answers from the local transcript files.
|
|
54
|
+
|
|
55
|
+
Recommended god-mode launches:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
codex -m gpt-5.4 -c model_reasoning_effort=low --dangerously-bypass-approvals-and-sandbox --no-alt-screen
|
|
59
|
+
claude --dangerously-skip-permissions --permission-mode bypassPermissions
|
|
60
|
+
gemini --approval-mode yolo --sandbox=false
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Script Mode
|
|
64
|
+
|
|
65
|
+
Turn an armed seat into a deterministic responder:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
muuuuse script
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
That stores one repeating response.
|
|
72
|
+
|
|
73
|
+
For a loop of multiple steps:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
muuuuse script 4
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
That collects four prompts and cycles them forever, one per inbound turn.
|
|
80
|
+
|
|
81
|
+
To leave script mode and go back to a live CLI listener:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
muuuuse live
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Requirements
|
|
88
|
+
|
|
89
|
+
- `tmux`
|
|
90
|
+
- `git`
|
|
91
|
+
- `npm`
|
|
92
|
+
- at least one local CLI you want to mirror: `codex`, `claude`, `gemini`, or script mode
|
|
93
|
+
|
|
94
|
+
## Doctor
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
muuuuse doctor
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
This checks:
|
|
101
|
+
|
|
102
|
+
- `git`
|
|
103
|
+
- `npm`
|
|
104
|
+
- `tmux`
|
|
105
|
+
- `codex`
|
|
106
|
+
- `claude`
|
|
107
|
+
- `gemini`
|
|
108
|
+
- `/root/npm.txt` or the fallback npm token path
|
|
109
|
+
|
|
110
|
+
## Notes
|
|
111
|
+
|
|
112
|
+
- local only
|
|
113
|
+
- auto-pair, no auth key ceremony
|
|
114
|
+
- only final answers are forwarded
|
|
115
|
+
- no verbose stream forwarding
|
|
116
|
+
- no reasoning forwarding
|
|
117
|
+
- controller exit stops the relay
|
package/bin/muuse.js
ADDED
package/package.json
CHANGED
|
@@ -1,16 +1,45 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "muuuuse",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
5
|
-
"
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "🔌Muuuuse links two local tmux terminals through a third control seat so Codex, Claude, Gemini, or scripts can bounce final answers forever.",
|
|
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
|
+
"tmux",
|
|
34
|
+
"relay",
|
|
35
|
+
"local",
|
|
36
|
+
"codex",
|
|
37
|
+
"claude",
|
|
38
|
+
"gemini"
|
|
14
39
|
],
|
|
15
|
-
"
|
|
40
|
+
"scripts": {
|
|
41
|
+
"test": "node test/cli.test.js",
|
|
42
|
+
"pack:local": "npm pack",
|
|
43
|
+
"prepublishOnly": "npm test"
|
|
44
|
+
}
|
|
16
45
|
}
|
package/src/agents.js
ADDED
|
@@ -0,0 +1,445 @@
|
|
|
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
|
+
const CODEX_ROOT = path.join(os.homedir(), ".codex", "sessions");
|
|
48
|
+
const CLAUDE_ROOT = path.join(os.homedir(), ".claude", "projects");
|
|
49
|
+
const GEMINI_ROOT = path.join(os.homedir(), ".gemini", "tmp");
|
|
50
|
+
const CODEX_SNAPSHOT_ROOT = path.join(os.homedir(), ".codex", "shell_snapshots");
|
|
51
|
+
const codexSnapshotPaneCache = new Map();
|
|
52
|
+
|
|
53
|
+
function walkFiles(rootPath, predicate, results = []) {
|
|
54
|
+
try {
|
|
55
|
+
const entries = fs.readdirSync(rootPath, { withFileTypes: true });
|
|
56
|
+
for (const entry of entries) {
|
|
57
|
+
const absolutePath = path.join(rootPath, entry.name);
|
|
58
|
+
if (entry.isDirectory()) {
|
|
59
|
+
walkFiles(absolutePath, predicate, results);
|
|
60
|
+
} else if (predicate(absolutePath)) {
|
|
61
|
+
results.push(absolutePath);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} catch (error) {
|
|
65
|
+
return results;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return results;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function commandMatches(args, command) {
|
|
72
|
+
const pattern = new RegExp(`(^|[\\\\/\\s])${command}(\\s|$)`, "i");
|
|
73
|
+
return pattern.test(args);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function detectAgent(processes) {
|
|
77
|
+
const ordered = [...processes].sort((left, right) => left.elapsedSeconds - right.elapsedSeconds);
|
|
78
|
+
for (const process of ordered) {
|
|
79
|
+
if (commandMatches(process.args, "codex")) {
|
|
80
|
+
return buildDetectedAgent("codex", process);
|
|
81
|
+
}
|
|
82
|
+
if (commandMatches(process.args, "claude")) {
|
|
83
|
+
return buildDetectedAgent("claude", process);
|
|
84
|
+
}
|
|
85
|
+
if (commandMatches(process.args, "gemini")) {
|
|
86
|
+
return buildDetectedAgent("gemini", process);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function buildDetectedAgent(type, process) {
|
|
93
|
+
return {
|
|
94
|
+
type,
|
|
95
|
+
pid: process.pid,
|
|
96
|
+
args: process.args,
|
|
97
|
+
elapsedSeconds: process.elapsedSeconds,
|
|
98
|
+
processStartedAtMs: Date.now() - process.elapsedSeconds * 1000,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function readFirstLines(filePath, maxLines = 20) {
|
|
103
|
+
const lines = [];
|
|
104
|
+
const fd = fs.openSync(filePath, "r");
|
|
105
|
+
try {
|
|
106
|
+
const buffer = Buffer.alloc(16384);
|
|
107
|
+
const bytesRead = fs.readSync(fd, buffer, 0, buffer.length, 0);
|
|
108
|
+
if (bytesRead === 0) {
|
|
109
|
+
return lines;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
for (const line of buffer.toString("utf8", 0, bytesRead).split("\n")) {
|
|
113
|
+
if (line.trim().length === 0) {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
lines.push(line.trim());
|
|
117
|
+
if (lines.length >= maxLines) {
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return lines;
|
|
122
|
+
} finally {
|
|
123
|
+
fs.closeSync(fd);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function chooseCandidate(candidates, currentPath, processStartedAtMs) {
|
|
128
|
+
const cwdMatches = candidates.filter((candidate) => candidate.cwd === currentPath);
|
|
129
|
+
if (cwdMatches.length === 0) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (processStartedAtMs !== null) {
|
|
134
|
+
const preciseMatches = cwdMatches
|
|
135
|
+
.map((candidate) => ({
|
|
136
|
+
...candidate,
|
|
137
|
+
diffMs: Math.abs(candidate.startedAtMs - processStartedAtMs),
|
|
138
|
+
}))
|
|
139
|
+
.filter((candidate) => Number.isFinite(candidate.diffMs) && candidate.diffMs <= SESSION_MATCH_WINDOW_MS)
|
|
140
|
+
.sort((left, right) => left.diffMs - right.diffMs || right.mtimeMs - left.mtimeMs);
|
|
141
|
+
|
|
142
|
+
if (preciseMatches.length > 0) {
|
|
143
|
+
return preciseMatches[0].path;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const fallback = cwdMatches.sort((left, right) => right.mtimeMs - left.mtimeMs)[0];
|
|
148
|
+
return fallback ? fallback.path : null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function extractThreadId(filePath) {
|
|
152
|
+
const match = path.basename(filePath).match(/([0-9a-f]{8}-[0-9a-f-]{27})\.jsonl$/i);
|
|
153
|
+
return match ? match[1] : null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function readCodexSnapshotPane(threadId) {
|
|
157
|
+
if (!threadId) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (codexSnapshotPaneCache.has(threadId)) {
|
|
162
|
+
return codexSnapshotPaneCache.get(threadId);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const snapshotPath = path.join(CODEX_SNAPSHOT_ROOT, `${threadId}.sh`);
|
|
166
|
+
try {
|
|
167
|
+
const contents = fs.readFileSync(snapshotPath, "utf8");
|
|
168
|
+
const match = contents.match(/declare -x TMUX_PANE="([^"]+)"/);
|
|
169
|
+
const paneId = match ? match[1] : null;
|
|
170
|
+
codexSnapshotPaneCache.set(threadId, paneId);
|
|
171
|
+
return paneId;
|
|
172
|
+
} catch (error) {
|
|
173
|
+
codexSnapshotPaneCache.set(threadId, null);
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function readCodexCandidate(filePath) {
|
|
179
|
+
try {
|
|
180
|
+
const [firstLine] = readFirstLines(filePath, 1);
|
|
181
|
+
if (!firstLine) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
const entry = JSON.parse(firstLine);
|
|
185
|
+
if (entry?.type !== "session_meta" || typeof entry.payload?.cwd !== "string") {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
path: filePath,
|
|
191
|
+
threadId: extractThreadId(filePath),
|
|
192
|
+
snapshotPaneId: readCodexSnapshotPane(extractThreadId(filePath)),
|
|
193
|
+
cwd: entry.payload.cwd,
|
|
194
|
+
startedAtMs: Date.parse(entry.payload.timestamp),
|
|
195
|
+
mtimeMs: fs.statSync(filePath).mtimeMs,
|
|
196
|
+
};
|
|
197
|
+
} catch (error) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function selectCodexSessionFile(currentPath, processStartedAtMs, paneId = null) {
|
|
203
|
+
const candidates = walkFiles(CODEX_ROOT, (filePath) => filePath.endsWith(".jsonl"))
|
|
204
|
+
.map((filePath) => readCodexCandidate(filePath))
|
|
205
|
+
.filter((candidate) => candidate !== null);
|
|
206
|
+
|
|
207
|
+
let scopedCandidates = candidates;
|
|
208
|
+
if (paneId) {
|
|
209
|
+
const exactPaneMatches = scopedCandidates.filter((candidate) => candidate.snapshotPaneId === paneId);
|
|
210
|
+
if (exactPaneMatches.length > 0) {
|
|
211
|
+
scopedCandidates = exactPaneMatches;
|
|
212
|
+
} else {
|
|
213
|
+
scopedCandidates = scopedCandidates.filter((candidate) => candidate.snapshotPaneId === null);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return chooseCandidate(scopedCandidates, currentPath, processStartedAtMs);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function extractCodexAssistantText(content) {
|
|
221
|
+
if (!Array.isArray(content)) {
|
|
222
|
+
return "";
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return content
|
|
226
|
+
.flatMap((item) => {
|
|
227
|
+
if (!item || typeof item !== "object") {
|
|
228
|
+
return [];
|
|
229
|
+
}
|
|
230
|
+
if (item.type === "output_text" && typeof item.text === "string") {
|
|
231
|
+
return [item.text.trim()];
|
|
232
|
+
}
|
|
233
|
+
return [];
|
|
234
|
+
})
|
|
235
|
+
.filter((text) => text.length > 0)
|
|
236
|
+
.join("\n");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function parseCodexFinalLine(line) {
|
|
240
|
+
try {
|
|
241
|
+
const entry = JSON.parse(line);
|
|
242
|
+
if (entry?.type !== "response_item" || entry.payload?.type !== "message" || entry.payload?.role !== "assistant") {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const text = sanitizeRelayText(extractCodexAssistantText(entry.payload.content));
|
|
247
|
+
if (!text) {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
id: entry.payload.id || hashText(line),
|
|
253
|
+
text,
|
|
254
|
+
timestamp: entry.timestamp || entry.payload.timestamp || new Date().toISOString(),
|
|
255
|
+
};
|
|
256
|
+
} catch (error) {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function readCodexAnswers(filePath, offset) {
|
|
262
|
+
const { nextOffset, text } = readAppendedText(filePath, offset);
|
|
263
|
+
const answers = text
|
|
264
|
+
.split("\n")
|
|
265
|
+
.map((line) => line.trim())
|
|
266
|
+
.filter((line) => line.length > 0)
|
|
267
|
+
.map((line) => parseCodexFinalLine(line))
|
|
268
|
+
.filter((entry) => entry !== null);
|
|
269
|
+
|
|
270
|
+
return { nextOffset, answers };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function readClaudeCandidate(filePath) {
|
|
274
|
+
try {
|
|
275
|
+
const lines = readFirstLines(filePath, 12);
|
|
276
|
+
for (const line of lines) {
|
|
277
|
+
const entry = JSON.parse(line);
|
|
278
|
+
if (typeof entry.cwd !== "string") {
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
return {
|
|
282
|
+
path: filePath,
|
|
283
|
+
cwd: entry.cwd,
|
|
284
|
+
startedAtMs: Date.parse(entry.timestamp || entry.message?.timestamp || 0),
|
|
285
|
+
mtimeMs: fs.statSync(filePath).mtimeMs,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
return null;
|
|
289
|
+
} catch (error) {
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function selectClaudeSessionFile(currentPath, processStartedAtMs) {
|
|
295
|
+
const candidates = walkFiles(CLAUDE_ROOT, (filePath) => filePath.endsWith(".jsonl"))
|
|
296
|
+
.map((filePath) => readClaudeCandidate(filePath))
|
|
297
|
+
.filter((candidate) => candidate !== null);
|
|
298
|
+
|
|
299
|
+
return chooseCandidate(candidates, currentPath, processStartedAtMs);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function extractClaudeAssistantText(content) {
|
|
303
|
+
if (!Array.isArray(content)) {
|
|
304
|
+
return "";
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return content
|
|
308
|
+
.flatMap((item) => {
|
|
309
|
+
if (!item || typeof item !== "object") {
|
|
310
|
+
return [];
|
|
311
|
+
}
|
|
312
|
+
if (item.type === "text" && typeof item.text === "string") {
|
|
313
|
+
return [item.text.trim()];
|
|
314
|
+
}
|
|
315
|
+
return [];
|
|
316
|
+
})
|
|
317
|
+
.filter((text) => text.length > 0)
|
|
318
|
+
.join("\n");
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function parseClaudeFinalLine(line) {
|
|
322
|
+
try {
|
|
323
|
+
const entry = JSON.parse(line);
|
|
324
|
+
if (entry?.type !== "assistant" || entry.message?.role !== "assistant" || entry.message?.stop_reason !== "end_turn") {
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const text = sanitizeRelayText(extractClaudeAssistantText(entry.message.content));
|
|
329
|
+
if (!text) {
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return {
|
|
334
|
+
id: entry.uuid || entry.message.id || hashText(line),
|
|
335
|
+
text,
|
|
336
|
+
timestamp: entry.timestamp || new Date().toISOString(),
|
|
337
|
+
};
|
|
338
|
+
} catch (error) {
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function readClaudeAnswers(filePath, offset) {
|
|
344
|
+
const { nextOffset, text } = readAppendedText(filePath, offset);
|
|
345
|
+
const answers = text
|
|
346
|
+
.split("\n")
|
|
347
|
+
.map((line) => line.trim())
|
|
348
|
+
.filter((line) => line.length > 0)
|
|
349
|
+
.map((line) => parseClaudeFinalLine(line))
|
|
350
|
+
.filter((entry) => entry !== null);
|
|
351
|
+
|
|
352
|
+
return { nextOffset, answers };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function readGeminiCandidate(filePath) {
|
|
356
|
+
try {
|
|
357
|
+
const raw = fs.readFileSync(filePath, "utf8");
|
|
358
|
+
const entry = JSON.parse(raw);
|
|
359
|
+
return {
|
|
360
|
+
path: filePath,
|
|
361
|
+
projectHash: entry.projectHash,
|
|
362
|
+
cwdHash: entry.projectHash,
|
|
363
|
+
startedAtMs: Date.parse(entry.startTime),
|
|
364
|
+
mtimeMs: fs.statSync(filePath).mtimeMs,
|
|
365
|
+
lastUpdatedMs: Date.parse(entry.lastUpdated),
|
|
366
|
+
};
|
|
367
|
+
} catch (error) {
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function selectGeminiSessionFile(currentPath, processStartedAtMs) {
|
|
373
|
+
const projectHash = createHash("sha256").update(currentPath).digest("hex");
|
|
374
|
+
const candidates = walkFiles(GEMINI_ROOT, (filePath) => filePath.endsWith(".json"))
|
|
375
|
+
.map((filePath) => readGeminiCandidate(filePath))
|
|
376
|
+
.filter((candidate) => candidate !== null && candidate.projectHash === projectHash);
|
|
377
|
+
|
|
378
|
+
if (candidates.length === 0) {
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (processStartedAtMs !== null) {
|
|
383
|
+
const preciseMatches = candidates
|
|
384
|
+
.map((candidate) => ({
|
|
385
|
+
...candidate,
|
|
386
|
+
diffMs: Math.abs(candidate.startedAtMs - processStartedAtMs),
|
|
387
|
+
}))
|
|
388
|
+
.filter((candidate) => Number.isFinite(candidate.diffMs) && candidate.diffMs <= SESSION_MATCH_WINDOW_MS)
|
|
389
|
+
.sort((left, right) => left.diffMs - right.diffMs || right.lastUpdatedMs - left.lastUpdatedMs);
|
|
390
|
+
|
|
391
|
+
if (preciseMatches.length > 0) {
|
|
392
|
+
return preciseMatches[0].path;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return candidates.sort((left, right) => right.lastUpdatedMs - left.lastUpdatedMs || right.mtimeMs - left.mtimeMs)[0].path;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function readGeminiAnswers(filePath, lastMessageId = null) {
|
|
400
|
+
try {
|
|
401
|
+
const entry = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
402
|
+
const messages = Array.isArray(entry.messages) ? entry.messages : [];
|
|
403
|
+
const finalMessages = messages.filter((message) => {
|
|
404
|
+
const toolCalls = Array.isArray(message.toolCalls) ? message.toolCalls : [];
|
|
405
|
+
return message.type === "gemini" && typeof message.content === "string" && message.content.trim() && toolCalls.length === 0;
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
let startIndex = 0;
|
|
409
|
+
if (lastMessageId) {
|
|
410
|
+
const previousIndex = finalMessages.findIndex((message) => message.id === lastMessageId);
|
|
411
|
+
startIndex = previousIndex === -1 ? finalMessages.length : previousIndex + 1;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const answers = finalMessages.slice(startIndex).map((message) => ({
|
|
415
|
+
id: message.id || hashText(JSON.stringify(message)),
|
|
416
|
+
text: sanitizeRelayText(message.content),
|
|
417
|
+
timestamp: message.timestamp || entry.lastUpdated || new Date().toISOString(),
|
|
418
|
+
}));
|
|
419
|
+
|
|
420
|
+
return {
|
|
421
|
+
answers: answers.filter((answer) => answer.text.length > 0),
|
|
422
|
+
lastMessageId: finalMessages.length > 0 ? finalMessages[finalMessages.length - 1].id : lastMessageId,
|
|
423
|
+
fileSize: getFileSize(filePath),
|
|
424
|
+
};
|
|
425
|
+
} catch (error) {
|
|
426
|
+
return {
|
|
427
|
+
answers: [],
|
|
428
|
+
lastMessageId,
|
|
429
|
+
fileSize: 0,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
module.exports = {
|
|
435
|
+
PRESETS,
|
|
436
|
+
detectAgent,
|
|
437
|
+
parseClaudeFinalLine,
|
|
438
|
+
parseCodexFinalLine,
|
|
439
|
+
readClaudeAnswers,
|
|
440
|
+
readCodexAnswers,
|
|
441
|
+
readGeminiAnswers,
|
|
442
|
+
selectClaudeSessionFile,
|
|
443
|
+
selectCodexSessionFile,
|
|
444
|
+
selectGeminiSessionFile,
|
|
445
|
+
};
|