@foothill/agent-move 1.0.9 → 1.0.10
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 +46 -9
- package/package.json +1 -1
- package/packages/client/dist/assets/{BufferResource-Ddjob236.js → BufferResource-Dfd5uHKt.js} +1 -1
- package/packages/client/dist/assets/{CanvasRenderer-B0w6SYyW.js → CanvasRenderer-7Cv6xZVP.js} +1 -1
- package/packages/client/dist/assets/{Filter-NcMGuiK-.js → Filter-CBX7EB7j.js} +1 -1
- package/packages/client/dist/assets/{RenderTargetSystem-DgAzY5_U.js → RenderTargetSystem-ko-v73NG.js} +1 -1
- package/packages/client/dist/assets/{WebGLRenderer-DUWXDPIX.js → WebGLRenderer-vhPQEPUG.js} +1 -1
- package/packages/client/dist/assets/{WebGPURenderer-C1HbrllR.js → WebGPURenderer-Dwywvwqe.js} +1 -1
- package/packages/client/dist/assets/{browserAll-CaF1Fl0O.js → browserAll-QyCAT8_K.js} +1 -1
- package/packages/client/dist/assets/index-BPJtz4FL.js +722 -0
- package/packages/client/dist/assets/{webworkerAll-BJ6UhC7r.js → webworkerAll-hM-gNP7L.js} +1 -1
- package/packages/client/dist/index.html +1 -1
- package/packages/server/dist/config.d.ts +4 -0
- package/packages/server/dist/config.d.ts.map +1 -1
- package/packages/server/dist/config.js +5 -1
- package/packages/server/dist/config.js.map +1 -1
- package/packages/server/dist/index.d.ts.map +1 -1
- package/packages/server/dist/index.js +790 -77
- package/packages/server/dist/index.js.map +1 -1
- package/packages/server/dist/state/activity-processor.d.ts +1 -1
- package/packages/server/dist/state/activity-processor.d.ts.map +1 -1
- package/packages/server/dist/state/activity-processor.js +1 -1
- package/packages/server/dist/state/activity-processor.js.map +1 -1
- package/packages/server/dist/state/agent-state-manager.d.ts +1 -2
- package/packages/server/dist/state/agent-state-manager.d.ts.map +1 -1
- package/packages/server/dist/state/agent-state-manager.js +87 -2
- package/packages/server/dist/state/agent-state-manager.js.map +1 -1
- package/packages/server/dist/state/role-resolver.d.ts +1 -2
- package/packages/server/dist/state/role-resolver.d.ts.map +1 -1
- package/packages/server/dist/state/role-resolver.js.map +1 -1
- package/packages/server/dist/state/task-graph-manager.d.ts +12 -0
- package/packages/server/dist/state/task-graph-manager.d.ts.map +1 -1
- package/packages/server/dist/state/task-graph-manager.js +80 -0
- package/packages/server/dist/state/task-graph-manager.js.map +1 -1
- package/packages/server/dist/watcher/claude/claude-paths.d.ts +18 -0
- package/packages/server/dist/watcher/claude/claude-paths.d.ts.map +1 -0
- package/packages/server/dist/watcher/{claude-paths.js → claude/claude-paths.js} +47 -55
- package/packages/server/dist/watcher/claude/claude-paths.js.map +1 -0
- package/packages/server/dist/watcher/{file-watcher.d.ts → claude/claude-watcher.d.ts} +3 -3
- package/packages/server/dist/watcher/claude/claude-watcher.d.ts.map +1 -0
- package/packages/server/dist/watcher/{file-watcher.js → claude/claude-watcher.js} +59 -65
- package/packages/server/dist/watcher/claude/claude-watcher.js.map +1 -0
- package/packages/server/dist/watcher/claude/jsonl-parser.d.ts +6 -0
- package/packages/server/dist/watcher/claude/jsonl-parser.d.ts.map +1 -0
- package/packages/server/dist/watcher/{jsonl-parser.js → claude/jsonl-parser.js} +1 -1
- package/packages/server/dist/watcher/claude/jsonl-parser.js.map +1 -0
- package/packages/server/dist/watcher/codex/codex-parser.d.ts +30 -0
- package/packages/server/dist/watcher/codex/codex-parser.d.ts.map +1 -0
- package/packages/server/dist/watcher/codex/codex-parser.js +326 -0
- package/packages/server/dist/watcher/codex/codex-parser.js.map +1 -0
- package/packages/server/dist/watcher/codex/codex-paths.d.ts +35 -0
- package/packages/server/dist/watcher/codex/codex-paths.d.ts.map +1 -0
- package/packages/server/dist/watcher/codex/codex-paths.js +46 -0
- package/packages/server/dist/watcher/codex/codex-paths.js.map +1 -0
- package/packages/server/dist/watcher/codex/codex-watcher.d.ts +42 -0
- package/packages/server/dist/watcher/codex/codex-watcher.d.ts.map +1 -0
- package/packages/server/dist/watcher/codex/codex-watcher.js +577 -0
- package/packages/server/dist/watcher/codex/codex-watcher.js.map +1 -0
- package/packages/server/dist/watcher/opencode/opencode-parser.d.ts +1 -1
- package/packages/server/dist/watcher/opencode/opencode-parser.d.ts.map +1 -1
- package/packages/server/dist/watcher/opencode/opencode-parser.js +31 -2
- package/packages/server/dist/watcher/opencode/opencode-paths.d.ts +1 -1
- package/packages/server/dist/watcher/opencode/opencode-paths.d.ts.map +1 -1
- package/packages/server/dist/watcher/opencode/opencode-paths.js +1 -0
- package/packages/server/dist/watcher/opencode/opencode-paths.js.map +1 -1
- package/packages/server/dist/watcher/opencode/opencode-watcher.d.ts.map +1 -1
- package/packages/server/dist/watcher/opencode/opencode-watcher.js +48 -10
- package/packages/server/dist/watcher/opencode/opencode-watcher.js.map +1 -1
- package/packages/server/dist/watcher/path-utils.d.ts +10 -0
- package/packages/server/dist/watcher/path-utils.d.ts.map +1 -0
- package/packages/server/dist/watcher/path-utils.js +38 -0
- package/packages/server/dist/watcher/path-utils.js.map +1 -0
- package/packages/server/dist/watcher/pi/pi-parser.d.ts +19 -0
- package/packages/server/dist/watcher/pi/pi-parser.d.ts.map +1 -0
- package/packages/server/dist/watcher/pi/pi-parser.js +307 -0
- package/packages/server/dist/watcher/pi/pi-parser.js.map +1 -0
- package/packages/server/dist/watcher/pi/pi-paths.d.ts +28 -0
- package/packages/server/dist/watcher/pi/pi-paths.d.ts.map +1 -0
- package/packages/server/dist/watcher/pi/pi-paths.js +86 -0
- package/packages/server/dist/watcher/pi/pi-paths.js.map +1 -0
- package/packages/server/dist/watcher/pi/pi-watcher.d.ts +36 -0
- package/packages/server/dist/watcher/pi/pi-watcher.d.ts.map +1 -0
- package/packages/server/dist/watcher/pi/pi-watcher.js +593 -0
- package/packages/server/dist/watcher/pi/pi-watcher.js.map +1 -0
- package/packages/server/dist/watcher/session-scanner.d.ts +9 -3
- package/packages/server/dist/watcher/session-scanner.d.ts.map +1 -1
- package/packages/server/dist/watcher/session-scanner.js +11 -9
- package/packages/server/dist/watcher/session-scanner.js.map +1 -1
- package/packages/server/dist/watcher/types.d.ts +30 -0
- package/packages/server/dist/watcher/types.d.ts.map +1 -0
- package/packages/server/dist/watcher/types.js +14 -0
- package/packages/server/dist/watcher/types.js.map +1 -0
- package/packages/shared/dist/constants/colors.d.ts +1 -1
- package/packages/shared/dist/constants/colors.js +1 -1
- package/packages/shared/dist/constants/colors.js.map +1 -1
- package/packages/shared/dist/constants/tools.d.ts.map +1 -1
- package/packages/shared/dist/constants/tools.js +30 -1
- package/packages/shared/dist/constants/tools.js.map +1 -1
- package/packages/shared/dist/index.d.ts +1 -1
- package/packages/shared/dist/index.d.ts.map +1 -1
- package/packages/shared/dist/types/agent.d.ts +3 -0
- package/packages/shared/dist/types/agent.d.ts.map +1 -1
- package/packages/client/dist/assets/index-Dh8yWoLP.js +0 -711
- package/packages/server/dist/watcher/claude-paths.d.ts +0 -32
- package/packages/server/dist/watcher/claude-paths.d.ts.map +0 -1
- package/packages/server/dist/watcher/claude-paths.js.map +0 -1
- package/packages/server/dist/watcher/file-watcher.d.ts.map +0 -1
- package/packages/server/dist/watcher/file-watcher.js.map +0 -1
- package/packages/server/dist/watcher/jsonl-parser.d.ts +0 -21
- package/packages/server/dist/watcher/jsonl-parser.d.ts.map +0 -1
- package/packages/server/dist/watcher/jsonl-parser.js.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// dist/index.js
|
|
2
2
|
import { fileURLToPath } from "url";
|
|
3
|
-
import { dirname, join as
|
|
3
|
+
import { dirname as dirname2, join as join10 } from "path";
|
|
4
4
|
import Fastify from "fastify";
|
|
5
5
|
import cors from "@fastify/cors";
|
|
6
6
|
import websocket from "@fastify/websocket";
|
|
@@ -10,8 +10,8 @@ import { join } from "path";
|
|
|
10
10
|
import chokidar from "chokidar";
|
|
11
11
|
import { stat as stat2, open } from "fs/promises";
|
|
12
12
|
import { join as join4, basename } from "path";
|
|
13
|
-
import { existsSync } from "fs";
|
|
14
13
|
import { join as join2 } from "path";
|
|
14
|
+
import { existsSync } from "fs";
|
|
15
15
|
import { readdir, stat } from "fs/promises";
|
|
16
16
|
import { join as join3 } from "path";
|
|
17
17
|
import chokidar2 from "chokidar";
|
|
@@ -19,6 +19,18 @@ import Database from "better-sqlite3";
|
|
|
19
19
|
import { homedir as homedir2 } from "os";
|
|
20
20
|
import { join as join5 } from "path";
|
|
21
21
|
import { existsSync as existsSync2 } from "fs";
|
|
22
|
+
import chokidar3 from "chokidar";
|
|
23
|
+
import { stat as stat3, open as open2 } from "fs/promises";
|
|
24
|
+
import { join as join7, basename as basename2, dirname } from "path";
|
|
25
|
+
import { homedir as homedir3 } from "os";
|
|
26
|
+
import { join as join6 } from "path";
|
|
27
|
+
import { existsSync as existsSync3 } from "fs";
|
|
28
|
+
import chokidar4 from "chokidar";
|
|
29
|
+
import { stat as stat4, open as open3, readdir as readdir2 } from "fs/promises";
|
|
30
|
+
import { join as join9, basename as basename4 } from "path";
|
|
31
|
+
import { homedir as homedir4 } from "os";
|
|
32
|
+
import { join as join8, basename as basename3 } from "path";
|
|
33
|
+
import { existsSync as existsSync4 } from "fs";
|
|
22
34
|
import { EventEmitter as EventEmitter2 } from "events";
|
|
23
35
|
import { EventEmitter } from "events";
|
|
24
36
|
import { execSync } from "child_process";
|
|
@@ -35,7 +47,11 @@ var config = {
|
|
|
35
47
|
activeThresholdMs: 10 * 60 * 1e3,
|
|
36
48
|
// 10 minutes
|
|
37
49
|
/** Enable OpenCode session watching (auto-detected if storage dir exists) */
|
|
38
|
-
enableOpenCode: process.env.AGENT_MOVE_OPENCODE !== "false"
|
|
50
|
+
enableOpenCode: process.env.AGENT_MOVE_OPENCODE !== "false",
|
|
51
|
+
/** Enable pi coding agent session watching (auto-detected if sessions dir exists) */
|
|
52
|
+
enablePi: process.env.AGENT_MOVE_PI !== "false",
|
|
53
|
+
/** Enable Codex CLI session watching (auto-detected if sessions dir exists) */
|
|
54
|
+
enableCodex: process.env.AGENT_MOVE_CODEX !== "false"
|
|
39
55
|
};
|
|
40
56
|
var JsonlParser = class {
|
|
41
57
|
parseLine(line) {
|
|
@@ -128,6 +144,38 @@ var JsonlParser = class {
|
|
|
128
144
|
return null;
|
|
129
145
|
}
|
|
130
146
|
};
|
|
147
|
+
function resolveEncodedPath(root, segments) {
|
|
148
|
+
try {
|
|
149
|
+
const parts = segments.split("-").filter(Boolean);
|
|
150
|
+
let currentPath = root;
|
|
151
|
+
let lastName = "";
|
|
152
|
+
let i = 0;
|
|
153
|
+
while (i < parts.length) {
|
|
154
|
+
let found = false;
|
|
155
|
+
const maxLen = Math.min(parts.length - i, 6);
|
|
156
|
+
for (let len = 1; len <= maxLen; len++) {
|
|
157
|
+
const segment = parts.slice(i, i + len).join("-");
|
|
158
|
+
for (const prefix of ["", "."]) {
|
|
159
|
+
const testPath = join2(currentPath, prefix + segment);
|
|
160
|
+
if (existsSync(testPath)) {
|
|
161
|
+
currentPath = testPath;
|
|
162
|
+
lastName = prefix + segment;
|
|
163
|
+
i += len;
|
|
164
|
+
found = true;
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (found)
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
if (!found)
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
return lastName || null;
|
|
175
|
+
} catch {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
131
179
|
var ClaudePaths = class {
|
|
132
180
|
/**
|
|
133
181
|
* Parse a JSONL session file path to extract project info.
|
|
@@ -140,6 +188,7 @@ var ClaudePaths = class {
|
|
|
140
188
|
const projectsIdx = parts.indexOf("projects");
|
|
141
189
|
if (projectsIdx === -1 || projectsIdx + 1 >= parts.length) {
|
|
142
190
|
return {
|
|
191
|
+
agentType: "claude",
|
|
143
192
|
projectPath: "unknown",
|
|
144
193
|
projectName: "Unknown",
|
|
145
194
|
isSubagent: false,
|
|
@@ -153,6 +202,7 @@ var ClaudePaths = class {
|
|
|
153
202
|
const isSubagent = depthAfterProject > 1;
|
|
154
203
|
const parentSessionId = isSubagent ? parts[projectsIdx + 2] : null;
|
|
155
204
|
return {
|
|
205
|
+
agentType: "claude",
|
|
156
206
|
projectPath: encodedProjectName,
|
|
157
207
|
projectName,
|
|
158
208
|
isSubagent,
|
|
@@ -166,80 +216,37 @@ var ClaudePaths = class {
|
|
|
166
216
|
* e.g., "C--projects-fts-temp-agent-move" → "agent-move"
|
|
167
217
|
*/
|
|
168
218
|
decodeProjectName(encoded) {
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
219
|
+
const driveMatch = encoded.match(/^([A-Za-z])--(.*)/);
|
|
220
|
+
const unixMatch = !driveMatch && encoded.match(/^-(.*)/);
|
|
221
|
+
if (driveMatch) {
|
|
222
|
+
const resolved = resolveEncodedPath(driveMatch[1] + ":/", driveMatch[2]);
|
|
223
|
+
if (resolved)
|
|
224
|
+
return resolved;
|
|
225
|
+
} else if (unixMatch) {
|
|
226
|
+
const resolved = resolveEncodedPath("/", unixMatch[1]);
|
|
227
|
+
if (resolved)
|
|
228
|
+
return resolved;
|
|
229
|
+
}
|
|
172
230
|
const parts = encoded.split("-").filter((p) => p.length > 0);
|
|
173
231
|
if (parts.length <= 2)
|
|
174
232
|
return parts.join("/");
|
|
175
233
|
return parts.slice(-2).join("/");
|
|
176
234
|
}
|
|
177
|
-
/**
|
|
178
|
-
* Greedily resolve the encoded path against the filesystem.
|
|
179
|
-
* Tries each dash-segment as a directory, joining multiple segments
|
|
180
|
-
* when a single one doesn't exist (to handle dashes in folder names).
|
|
181
|
-
*/
|
|
182
|
-
resolveToFolderName(encoded) {
|
|
183
|
-
try {
|
|
184
|
-
let root;
|
|
185
|
-
let rest;
|
|
186
|
-
const driveMatch = encoded.match(/^([A-Za-z])--(.*)/);
|
|
187
|
-
const unixMatch = !driveMatch && encoded.match(/^-(.*)/);
|
|
188
|
-
if (driveMatch) {
|
|
189
|
-
root = driveMatch[1] + ":/";
|
|
190
|
-
rest = driveMatch[2];
|
|
191
|
-
} else if (unixMatch) {
|
|
192
|
-
root = "/";
|
|
193
|
-
rest = unixMatch[1];
|
|
194
|
-
} else {
|
|
195
|
-
return null;
|
|
196
|
-
}
|
|
197
|
-
const parts = rest.split("-").filter(Boolean);
|
|
198
|
-
let currentPath = root;
|
|
199
|
-
let lastName = "";
|
|
200
|
-
let i = 0;
|
|
201
|
-
while (i < parts.length) {
|
|
202
|
-
let found = false;
|
|
203
|
-
const maxLen = Math.min(parts.length - i, 6);
|
|
204
|
-
for (let len = 1; len <= maxLen; len++) {
|
|
205
|
-
const segment = parts.slice(i, i + len).join("-");
|
|
206
|
-
for (const prefix of ["", "."]) {
|
|
207
|
-
const testPath = join2(currentPath, prefix + segment);
|
|
208
|
-
if (existsSync(testPath)) {
|
|
209
|
-
currentPath = testPath;
|
|
210
|
-
lastName = prefix + segment;
|
|
211
|
-
i += len;
|
|
212
|
-
found = true;
|
|
213
|
-
break;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
if (found)
|
|
217
|
-
break;
|
|
218
|
-
}
|
|
219
|
-
if (!found)
|
|
220
|
-
break;
|
|
221
|
-
}
|
|
222
|
-
return lastName || null;
|
|
223
|
-
} catch {
|
|
224
|
-
return null;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
235
|
};
|
|
228
236
|
var claudePaths = new ClaudePaths();
|
|
229
237
|
var SessionScanner = class {
|
|
230
|
-
|
|
231
|
-
constructor(
|
|
232
|
-
this.
|
|
238
|
+
rootDir;
|
|
239
|
+
constructor(rootDir) {
|
|
240
|
+
this.rootDir = rootDir;
|
|
233
241
|
}
|
|
234
|
-
/** Find
|
|
242
|
+
/** Find the most recently modified JSONL per project subdirectory */
|
|
235
243
|
async scan() {
|
|
236
244
|
const results = [];
|
|
237
|
-
const projectsDir = join3(this.claudeHome, "projects");
|
|
238
245
|
try {
|
|
239
|
-
const projects = await readdir(
|
|
246
|
+
const projects = await readdir(this.rootDir);
|
|
240
247
|
const now = Date.now();
|
|
241
248
|
for (const project of projects) {
|
|
242
|
-
const projectDir = join3(
|
|
249
|
+
const projectDir = join3(this.rootDir, project);
|
|
243
250
|
try {
|
|
244
251
|
const projectStat = await stat(projectDir);
|
|
245
252
|
if (!projectStat.isDirectory())
|
|
@@ -269,7 +276,6 @@ var SessionScanner = class {
|
|
|
269
276
|
}
|
|
270
277
|
}
|
|
271
278
|
} catch {
|
|
272
|
-
console.log("No projects directory found \u2014 will wait for new sessions");
|
|
273
279
|
}
|
|
274
280
|
return results;
|
|
275
281
|
}
|
|
@@ -287,7 +293,7 @@ var FileWatcher = class {
|
|
|
287
293
|
this.stateManager = stateManager;
|
|
288
294
|
}
|
|
289
295
|
async start() {
|
|
290
|
-
const scanner = new SessionScanner(this.claudeHome);
|
|
296
|
+
const scanner = new SessionScanner(join4(this.claudeHome, "projects"));
|
|
291
297
|
const existingFiles = await scanner.scan();
|
|
292
298
|
for (const file of existingFiles) {
|
|
293
299
|
await this.processFile(file);
|
|
@@ -358,6 +364,16 @@ var FileWatcher = class {
|
|
|
358
364
|
}
|
|
359
365
|
}
|
|
360
366
|
};
|
|
367
|
+
function createFallbackSession(agentType, name) {
|
|
368
|
+
return {
|
|
369
|
+
agentType,
|
|
370
|
+
projectPath: name,
|
|
371
|
+
projectName: name,
|
|
372
|
+
isSubagent: false,
|
|
373
|
+
projectDir: name,
|
|
374
|
+
parentSessionId: null
|
|
375
|
+
};
|
|
376
|
+
}
|
|
361
377
|
function getOpenCodeDbPath() {
|
|
362
378
|
const home = homedir2();
|
|
363
379
|
const candidates = [
|
|
@@ -376,6 +392,7 @@ function parseOpenCodeSession(row) {
|
|
|
376
392
|
const segments = row.directory.replace(/\\/g, "/").split("/").filter(Boolean);
|
|
377
393
|
const projectName = segments[segments.length - 1] || "opencode";
|
|
378
394
|
return {
|
|
395
|
+
agentType: "opencode",
|
|
379
396
|
// Use the actual directory as projectPath so getGitBranch() gets a valid cwd.
|
|
380
397
|
// projectDir uses the project_id hash to group agents belonging to the same project.
|
|
381
398
|
projectPath: row.directory || row.project_id,
|
|
@@ -435,7 +452,7 @@ function getZoneForTool(toolName) {
|
|
|
435
452
|
return TOOL_ZONE_MAP[toolName] ?? "thinking";
|
|
436
453
|
}
|
|
437
454
|
var TOOL_NAME_MAP = {
|
|
438
|
-
// OpenCode lowercase → canonical PascalCase
|
|
455
|
+
// OpenCode / pi lowercase → canonical PascalCase
|
|
439
456
|
read: "Read",
|
|
440
457
|
write: "Write",
|
|
441
458
|
edit: "Edit",
|
|
@@ -446,7 +463,36 @@ var TOOL_NAME_MAP = {
|
|
|
446
463
|
websearch: "WebSearch",
|
|
447
464
|
webfetch: "WebFetch",
|
|
448
465
|
todoread: "TodoRead",
|
|
449
|
-
todowrite: "TodoWrite"
|
|
466
|
+
todowrite: "TodoWrite",
|
|
467
|
+
// pi-specific tool names
|
|
468
|
+
"edit-diff": "Patch",
|
|
469
|
+
find: "Glob",
|
|
470
|
+
ls: "Bash",
|
|
471
|
+
truncate: "Write",
|
|
472
|
+
// Codex CLI tool names
|
|
473
|
+
shell_command: "Bash",
|
|
474
|
+
exec_command: "Bash",
|
|
475
|
+
read_file: "Read",
|
|
476
|
+
apply_patch: "Patch",
|
|
477
|
+
list_dir: "Bash",
|
|
478
|
+
grep_files: "Grep",
|
|
479
|
+
web_search: "WebSearch",
|
|
480
|
+
js_repl: "Bash",
|
|
481
|
+
js_repl_reset: "Bash",
|
|
482
|
+
spawn_agent: "Agent",
|
|
483
|
+
send_input: "Agent",
|
|
484
|
+
wait: "Agent",
|
|
485
|
+
close_agent: "Agent",
|
|
486
|
+
resume_agent: "Agent",
|
|
487
|
+
spawn_agents_on_csv: "Agent",
|
|
488
|
+
report_agent_job_result: "Agent",
|
|
489
|
+
request_user_input: "AskUserQuestion",
|
|
490
|
+
request_permissions: "AskUserQuestion",
|
|
491
|
+
update_plan: "TodoWrite",
|
|
492
|
+
view_image: "Read",
|
|
493
|
+
image_generation: "Write",
|
|
494
|
+
write_stdin: "Bash",
|
|
495
|
+
search_apps: "WebSearch"
|
|
450
496
|
};
|
|
451
497
|
function normalizeToolName(name) {
|
|
452
498
|
return TOOL_NAME_MAP[name] ?? name;
|
|
@@ -907,13 +953,597 @@ var OpenCodeWatcher = class {
|
|
|
907
953
|
return id.startsWith("oc:") ? id : `oc:${id}`;
|
|
908
954
|
}
|
|
909
955
|
fallbackSession() {
|
|
910
|
-
return
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
956
|
+
return createFallbackSession("opencode", "opencode");
|
|
957
|
+
}
|
|
958
|
+
};
|
|
959
|
+
var PiParser = class {
|
|
960
|
+
/**
|
|
961
|
+
* Parse a single JSONL line from a pi session file into a raw object.
|
|
962
|
+
* Returns the parsed JSON, or null on parse error.
|
|
963
|
+
*/
|
|
964
|
+
parseRaw(line) {
|
|
965
|
+
try {
|
|
966
|
+
return JSON.parse(line);
|
|
967
|
+
} catch {
|
|
968
|
+
return null;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* Extract a ParsedActivity from a pre-parsed JSONL entry.
|
|
973
|
+
* Returns null for non-actionable entries (session header, user messages, etc.).
|
|
974
|
+
*/
|
|
975
|
+
parseEntry(entry) {
|
|
976
|
+
if (entry.type !== "message")
|
|
977
|
+
return null;
|
|
978
|
+
const msg = entry.message;
|
|
979
|
+
if (!msg || msg.role !== "assistant")
|
|
980
|
+
return null;
|
|
981
|
+
return this.parseAssistantMessage(msg);
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* Check if a pre-parsed entry is a session header.
|
|
985
|
+
*/
|
|
986
|
+
isSessionHeader(entry) {
|
|
987
|
+
return entry.type === "session";
|
|
988
|
+
}
|
|
989
|
+
parseAssistantMessage(msg) {
|
|
990
|
+
const content = msg.content;
|
|
991
|
+
if (!Array.isArray(content))
|
|
992
|
+
return null;
|
|
993
|
+
for (const block of content) {
|
|
994
|
+
if (block.type === "toolCall") {
|
|
995
|
+
const tool = block;
|
|
996
|
+
return {
|
|
997
|
+
type: "tool_use",
|
|
998
|
+
toolName: normalizeToolName(tool.name),
|
|
999
|
+
toolInput: normalizeToolInput(tool.arguments ?? {}),
|
|
1000
|
+
model: msg.model,
|
|
1001
|
+
inputTokens: msg.usage?.input,
|
|
1002
|
+
outputTokens: msg.usage?.output,
|
|
1003
|
+
cacheReadTokens: msg.usage?.cacheRead,
|
|
1004
|
+
cacheCreationTokens: msg.usage?.cacheWrite
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
for (const block of content) {
|
|
1009
|
+
if (block.type === "text") {
|
|
1010
|
+
const text = block.text?.trim() ?? "";
|
|
1011
|
+
if (text.length > 0 && text.length < 200) {
|
|
1012
|
+
return {
|
|
1013
|
+
type: "text",
|
|
1014
|
+
text,
|
|
1015
|
+
model: msg.model,
|
|
1016
|
+
inputTokens: msg.usage?.input,
|
|
1017
|
+
outputTokens: msg.usage?.output,
|
|
1018
|
+
cacheReadTokens: msg.usage?.cacheRead,
|
|
1019
|
+
cacheCreationTokens: msg.usage?.cacheWrite
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
for (const block of content) {
|
|
1025
|
+
if (block.type === "thinking") {
|
|
1026
|
+
const thinking = block.thinking?.trim() ?? "";
|
|
1027
|
+
return {
|
|
1028
|
+
type: "tool_use",
|
|
1029
|
+
toolName: "thinking",
|
|
1030
|
+
toolInput: thinking.length > 0 ? { thought: thinking.slice(0, 120) } : void 0,
|
|
1031
|
+
model: msg.model,
|
|
1032
|
+
inputTokens: msg.usage?.input,
|
|
1033
|
+
outputTokens: msg.usage?.output,
|
|
1034
|
+
cacheReadTokens: msg.usage?.cacheRead,
|
|
1035
|
+
cacheCreationTokens: msg.usage?.cacheWrite
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
if (msg.usage && (msg.usage.input || msg.usage.output)) {
|
|
1040
|
+
return {
|
|
1041
|
+
type: "token_usage",
|
|
1042
|
+
inputTokens: msg.usage.input,
|
|
1043
|
+
outputTokens: msg.usage.output,
|
|
1044
|
+
cacheReadTokens: msg.usage.cacheRead,
|
|
1045
|
+
cacheCreationTokens: msg.usage.cacheWrite,
|
|
1046
|
+
model: msg.model
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
return null;
|
|
1050
|
+
}
|
|
1051
|
+
};
|
|
1052
|
+
function getPiSessionsDir() {
|
|
1053
|
+
const candidate = join6(homedir3(), ".pi", "agent", "sessions");
|
|
1054
|
+
return existsSync3(candidate) ? candidate : null;
|
|
1055
|
+
}
|
|
1056
|
+
function decodePiProjectDir(encoded) {
|
|
1057
|
+
let inner = encoded;
|
|
1058
|
+
if (inner.startsWith("--") && inner.endsWith("--")) {
|
|
1059
|
+
inner = inner.slice(2, -2);
|
|
1060
|
+
}
|
|
1061
|
+
const driveMatch = inner.match(/^([A-Za-z])--(.*)/);
|
|
1062
|
+
if (driveMatch) {
|
|
1063
|
+
const resolved = resolveEncodedPath(driveMatch[1] + ":/", driveMatch[2]);
|
|
1064
|
+
if (resolved)
|
|
1065
|
+
return resolved;
|
|
1066
|
+
} else {
|
|
1067
|
+
const resolved = resolveEncodedPath("/", inner);
|
|
1068
|
+
if (resolved)
|
|
1069
|
+
return resolved;
|
|
1070
|
+
}
|
|
1071
|
+
const parts = inner.split("-").filter(Boolean);
|
|
1072
|
+
if (parts.length <= 2)
|
|
1073
|
+
return parts.join("/");
|
|
1074
|
+
return parts.slice(-2).join("-");
|
|
1075
|
+
}
|
|
1076
|
+
function parsePiSessionInfo(header, dirName) {
|
|
1077
|
+
const projectName = decodePiProjectDir(dirName);
|
|
1078
|
+
return {
|
|
1079
|
+
agentType: "pi",
|
|
1080
|
+
projectPath: header.cwd || dirName,
|
|
1081
|
+
projectName,
|
|
1082
|
+
isSubagent: !!header.parentSession,
|
|
1083
|
+
projectDir: dirName,
|
|
1084
|
+
parentSessionId: header.parentSession ? extractSessionIdFromPath(header.parentSession) : null
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
function extractSessionIdFromPath(filePath) {
|
|
1088
|
+
const match = filePath.match(/([^/\\]+)\.jsonl$/);
|
|
1089
|
+
if (!match)
|
|
1090
|
+
return null;
|
|
1091
|
+
const parts = match[1].split("_");
|
|
1092
|
+
const uuid = parts[parts.length - 1];
|
|
1093
|
+
return uuid ? `pi:${uuid}` : null;
|
|
1094
|
+
}
|
|
1095
|
+
var PiWatcher = class {
|
|
1096
|
+
stateManager;
|
|
1097
|
+
watcher = null;
|
|
1098
|
+
byteOffsets = /* @__PURE__ */ new Map();
|
|
1099
|
+
parser = new PiParser();
|
|
1100
|
+
/** Per-file lock to prevent concurrent processFile calls */
|
|
1101
|
+
fileLocks = /* @__PURE__ */ new Map();
|
|
1102
|
+
/** Cached session info per file (parsed from session header) */
|
|
1103
|
+
sessionInfoCache = /* @__PURE__ */ new Map();
|
|
1104
|
+
constructor(stateManager) {
|
|
1105
|
+
this.stateManager = stateManager;
|
|
1106
|
+
}
|
|
1107
|
+
async start() {
|
|
1108
|
+
const sessionsDir = getPiSessionsDir();
|
|
1109
|
+
if (!sessionsDir) {
|
|
1110
|
+
console.log("[pi] No sessions directory found \u2014 pi not installed or not yet used");
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
console.log(`[pi] Sessions directory found at ${sessionsDir}`);
|
|
1114
|
+
const scanner = new SessionScanner(sessionsDir);
|
|
1115
|
+
const existingFiles = await scanner.scan();
|
|
1116
|
+
for (const file of existingFiles) {
|
|
1117
|
+
await this.processFile(file);
|
|
1118
|
+
}
|
|
1119
|
+
const pattern = join7(sessionsDir, "**", "*.jsonl");
|
|
1120
|
+
this.watcher = chokidar3.watch(pattern, {
|
|
1121
|
+
persistent: true,
|
|
1122
|
+
ignoreInitial: true,
|
|
1123
|
+
awaitWriteFinish: { stabilityThreshold: 200, pollInterval: 50 }
|
|
1124
|
+
});
|
|
1125
|
+
this.watcher.on("add", (filePath) => {
|
|
1126
|
+
console.log(`[pi] New session file: ${filePath}`);
|
|
1127
|
+
this.processFile(filePath);
|
|
1128
|
+
});
|
|
1129
|
+
this.watcher.on("change", (filePath) => {
|
|
1130
|
+
this.processFile(filePath);
|
|
1131
|
+
});
|
|
1132
|
+
console.log(`[pi] Watching for JSONL files in ${sessionsDir}`);
|
|
1133
|
+
}
|
|
1134
|
+
stop() {
|
|
1135
|
+
this.watcher?.close();
|
|
1136
|
+
this.byteOffsets.clear();
|
|
1137
|
+
this.fileLocks.clear();
|
|
1138
|
+
this.sessionInfoCache.clear();
|
|
1139
|
+
}
|
|
1140
|
+
processFile(filePath) {
|
|
1141
|
+
const prev = this.fileLocks.get(filePath) ?? Promise.resolve();
|
|
1142
|
+
const next = prev.then(() => this.doProcessFile(filePath)).catch(() => {
|
|
1143
|
+
}).finally(() => {
|
|
1144
|
+
if (this.fileLocks.get(filePath) === next) {
|
|
1145
|
+
this.fileLocks.delete(filePath);
|
|
1146
|
+
}
|
|
1147
|
+
});
|
|
1148
|
+
this.fileLocks.set(filePath, next);
|
|
1149
|
+
}
|
|
1150
|
+
async doProcessFile(filePath) {
|
|
1151
|
+
try {
|
|
1152
|
+
const fileStats = await stat3(filePath);
|
|
1153
|
+
const currentOffset = this.byteOffsets.get(filePath) ?? 0;
|
|
1154
|
+
if (fileStats.size <= currentOffset)
|
|
1155
|
+
return;
|
|
1156
|
+
const handle = await open2(filePath, "r");
|
|
1157
|
+
try {
|
|
1158
|
+
const buffer = Buffer.alloc(fileStats.size - currentOffset);
|
|
1159
|
+
await handle.read(buffer, 0, buffer.length, currentOffset);
|
|
1160
|
+
this.byteOffsets.set(filePath, fileStats.size);
|
|
1161
|
+
const newContent = buffer.toString("utf-8");
|
|
1162
|
+
const lines = newContent.split("\n").filter((l) => l.trim());
|
|
1163
|
+
const sessionId = this.extractSessionId(filePath);
|
|
1164
|
+
let sessionInfo = this.sessionInfoCache.get(filePath);
|
|
1165
|
+
let hadParsedActivity = false;
|
|
1166
|
+
for (const line of lines) {
|
|
1167
|
+
const raw = this.parser.parseRaw(line);
|
|
1168
|
+
if (!raw)
|
|
1169
|
+
continue;
|
|
1170
|
+
if (!sessionInfo && this.parser.isSessionHeader(raw)) {
|
|
1171
|
+
const dirName = this.getProjectDirName(filePath);
|
|
1172
|
+
sessionInfo = parsePiSessionInfo(raw, dirName);
|
|
1173
|
+
this.sessionInfoCache.set(filePath, sessionInfo);
|
|
1174
|
+
continue;
|
|
1175
|
+
}
|
|
1176
|
+
const parsed = this.parser.parseEntry(raw);
|
|
1177
|
+
if (parsed) {
|
|
1178
|
+
hadParsedActivity = true;
|
|
1179
|
+
if (!sessionInfo) {
|
|
1180
|
+
sessionInfo = this.buildFallbackSession(filePath);
|
|
1181
|
+
this.sessionInfoCache.set(filePath, sessionInfo);
|
|
1182
|
+
}
|
|
1183
|
+
this.stateManager.processMessage(sessionId, parsed, sessionInfo);
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
if (!hadParsedActivity && lines.length > 0) {
|
|
1187
|
+
this.stateManager.heartbeat(sessionId);
|
|
1188
|
+
}
|
|
1189
|
+
} finally {
|
|
1190
|
+
await handle.close();
|
|
1191
|
+
}
|
|
1192
|
+
} catch (err) {
|
|
1193
|
+
if (err.code !== "ENOENT") {
|
|
1194
|
+
console.error(`[pi] Error processing ${filePath}:`, err);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
/**
|
|
1199
|
+
* Extract a prefixed session ID from a pi session file path.
|
|
1200
|
+
* Filename format: {timestamp}_{uuid}.jsonl
|
|
1201
|
+
*/
|
|
1202
|
+
extractSessionId(filePath) {
|
|
1203
|
+
const name = basename2(filePath, ".jsonl");
|
|
1204
|
+
return `pi:${name}`;
|
|
1205
|
+
}
|
|
1206
|
+
buildFallbackSession(filePath) {
|
|
1207
|
+
const dirName = this.getProjectDirName(filePath);
|
|
1208
|
+
const name = dirName.replace(/^--|--$/g, "") || "pi";
|
|
1209
|
+
return createFallbackSession("pi", name);
|
|
1210
|
+
}
|
|
1211
|
+
/**
|
|
1212
|
+
* Get the encoded project directory name from a session file path.
|
|
1213
|
+
* Path: .../sessions/--encoded-path--/{timestamp}_{uuid}.jsonl
|
|
1214
|
+
*/
|
|
1215
|
+
getProjectDirName(filePath) {
|
|
1216
|
+
const dir = dirname(filePath).replace(/\\/g, "/");
|
|
1217
|
+
const parts = dir.split("/");
|
|
1218
|
+
return parts[parts.length - 1] || "unknown";
|
|
1219
|
+
}
|
|
1220
|
+
};
|
|
1221
|
+
var CodexParser = class {
|
|
1222
|
+
/**
|
|
1223
|
+
* Parse a single JSONL line into the envelope structure.
|
|
1224
|
+
*/
|
|
1225
|
+
parseRaw(line) {
|
|
1226
|
+
try {
|
|
1227
|
+
const obj = JSON.parse(line);
|
|
1228
|
+
if (obj && typeof obj.type === "string") {
|
|
1229
|
+
return obj;
|
|
1230
|
+
}
|
|
1231
|
+
return null;
|
|
1232
|
+
} catch {
|
|
1233
|
+
return null;
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
/**
|
|
1237
|
+
* Try to extract session_meta payload. Returns null if not a session_meta envelope.
|
|
1238
|
+
*/
|
|
1239
|
+
tryGetSessionMeta(envelope) {
|
|
1240
|
+
if (envelope.type !== "session_meta")
|
|
1241
|
+
return null;
|
|
1242
|
+
return envelope.payload;
|
|
1243
|
+
}
|
|
1244
|
+
/**
|
|
1245
|
+
* Try to extract model from a turn_context envelope. Returns null otherwise.
|
|
1246
|
+
*/
|
|
1247
|
+
tryGetModel(envelope) {
|
|
1248
|
+
if (envelope.type !== "turn_context")
|
|
1249
|
+
return null;
|
|
1250
|
+
const model = envelope.payload?.model;
|
|
1251
|
+
return model ?? null;
|
|
1252
|
+
}
|
|
1253
|
+
/**
|
|
1254
|
+
* Parse a Codex JSONL envelope into a ParsedActivity.
|
|
1255
|
+
* Model is passed in from the watcher (tracked per-file).
|
|
1256
|
+
* Returns null for non-actionable entries.
|
|
1257
|
+
*/
|
|
1258
|
+
parseEntry(envelope, model) {
|
|
1259
|
+
if (envelope.type === "response_item") {
|
|
1260
|
+
return this.parseResponseItem(envelope.payload, model);
|
|
1261
|
+
}
|
|
1262
|
+
if (envelope.type === "event_msg") {
|
|
1263
|
+
return this.parseEventMsg(envelope.payload, model);
|
|
1264
|
+
}
|
|
1265
|
+
return null;
|
|
1266
|
+
}
|
|
1267
|
+
parseResponseItem(payload, model) {
|
|
1268
|
+
const itemType = payload.type;
|
|
1269
|
+
if (itemType === "function_call") {
|
|
1270
|
+
const fc = payload;
|
|
1271
|
+
let toolInput = {};
|
|
1272
|
+
try {
|
|
1273
|
+
toolInput = JSON.parse(fc.arguments);
|
|
1274
|
+
} catch {
|
|
1275
|
+
}
|
|
1276
|
+
return {
|
|
1277
|
+
type: "tool_use",
|
|
1278
|
+
toolName: normalizeToolName(fc.name),
|
|
1279
|
+
toolInput: normalizeToolInput(toolInput),
|
|
1280
|
+
model: model ?? void 0
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
1283
|
+
if (itemType.endsWith("_call") && itemType !== "function_call") {
|
|
1284
|
+
const nativeName = itemType.replace(/_call$/, "");
|
|
1285
|
+
const toolInput = {};
|
|
1286
|
+
const action = payload.action;
|
|
1287
|
+
if (action?.query)
|
|
1288
|
+
toolInput.query = action.query;
|
|
1289
|
+
return {
|
|
1290
|
+
type: "tool_use",
|
|
1291
|
+
toolName: normalizeToolName(nativeName),
|
|
1292
|
+
toolInput,
|
|
1293
|
+
model: model ?? void 0
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
return null;
|
|
1297
|
+
}
|
|
1298
|
+
parseEventMsg(payload, model) {
|
|
1299
|
+
const eventType = payload.type;
|
|
1300
|
+
if (eventType === "token_count") {
|
|
1301
|
+
const info = payload.info;
|
|
1302
|
+
if (!info?.last_token_usage)
|
|
1303
|
+
return null;
|
|
1304
|
+
const usage = info.last_token_usage;
|
|
1305
|
+
return {
|
|
1306
|
+
type: "token_usage",
|
|
1307
|
+
inputTokens: usage.input_tokens,
|
|
1308
|
+
outputTokens: (usage.output_tokens ?? 0) + (usage.reasoning_output_tokens ?? 0),
|
|
1309
|
+
cacheReadTokens: usage.cached_input_tokens,
|
|
1310
|
+
model: model ?? void 0
|
|
1311
|
+
};
|
|
1312
|
+
}
|
|
1313
|
+
if (eventType === "agent_message") {
|
|
1314
|
+
const text = payload.message?.trim();
|
|
1315
|
+
if (text && text.length > 0 && text.length < 200) {
|
|
1316
|
+
return { type: "text", text, model: model ?? void 0 };
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
if (eventType === "agent_reasoning") {
|
|
1320
|
+
const text = payload.text?.trim();
|
|
1321
|
+
if (!text)
|
|
1322
|
+
return null;
|
|
1323
|
+
return {
|
|
1324
|
+
type: "tool_use",
|
|
1325
|
+
toolName: "thinking",
|
|
1326
|
+
toolInput: { thought: text.slice(0, 120) },
|
|
1327
|
+
model: model ?? void 0
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
return null;
|
|
1331
|
+
}
|
|
1332
|
+
};
|
|
1333
|
+
function getCodexSessionsDir() {
|
|
1334
|
+
const candidate = join8(homedir4(), ".codex", "sessions");
|
|
1335
|
+
return existsSync4(candidate) ? candidate : null;
|
|
1336
|
+
}
|
|
1337
|
+
var UUID_RE = /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/;
|
|
1338
|
+
function extractCodexSessionId(filePath) {
|
|
1339
|
+
const name = basename3(filePath, ".jsonl");
|
|
1340
|
+
const match = name.match(UUID_RE);
|
|
1341
|
+
if (match) {
|
|
1342
|
+
return `codex:${match[1]}`;
|
|
1343
|
+
}
|
|
1344
|
+
return `codex:${name}`;
|
|
1345
|
+
}
|
|
1346
|
+
function parseCodexSessionInfo(meta) {
|
|
1347
|
+
const cwd = meta.cwd || "codex";
|
|
1348
|
+
const parts = cwd.replace(/\\/g, "/").split("/").filter(Boolean);
|
|
1349
|
+
const projectName = parts[parts.length - 1] || "codex";
|
|
1350
|
+
return {
|
|
1351
|
+
agentType: "codex",
|
|
1352
|
+
projectPath: cwd,
|
|
1353
|
+
projectName,
|
|
1354
|
+
isSubagent: false,
|
|
1355
|
+
projectDir: cwd,
|
|
1356
|
+
parentSessionId: null
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
function createCodexSubagentSession(parentSessionId, parentInfo) {
|
|
1360
|
+
return {
|
|
1361
|
+
agentType: "codex",
|
|
1362
|
+
projectPath: parentInfo.projectPath,
|
|
1363
|
+
projectName: parentInfo.projectName,
|
|
1364
|
+
isSubagent: true,
|
|
1365
|
+
projectDir: parentInfo.projectDir,
|
|
1366
|
+
parentSessionId
|
|
1367
|
+
};
|
|
1368
|
+
}
|
|
1369
|
+
var CodexWatcher = class {
|
|
1370
|
+
stateManager;
|
|
1371
|
+
watcher = null;
|
|
1372
|
+
byteOffsets = /* @__PURE__ */ new Map();
|
|
1373
|
+
parser = new CodexParser();
|
|
1374
|
+
/** Per-file lock to prevent concurrent processFile calls */
|
|
1375
|
+
fileLocks = /* @__PURE__ */ new Map();
|
|
1376
|
+
/** Cached session info per file (parsed from session_meta) */
|
|
1377
|
+
sessionInfoCache = /* @__PURE__ */ new Map();
|
|
1378
|
+
/** Per-file model tracking (from turn_context events) */
|
|
1379
|
+
fileModels = /* @__PURE__ */ new Map();
|
|
1380
|
+
/** Deduplication of spawn_agent call IDs already processed */
|
|
1381
|
+
seenSubagentCalls = /* @__PURE__ */ new Set();
|
|
1382
|
+
/** Counter for generating unique subagent session IDs */
|
|
1383
|
+
subagentCounter = 0;
|
|
1384
|
+
constructor(stateManager) {
|
|
1385
|
+
this.stateManager = stateManager;
|
|
1386
|
+
}
|
|
1387
|
+
async start() {
|
|
1388
|
+
const sessionsDir = getCodexSessionsDir();
|
|
1389
|
+
if (!sessionsDir) {
|
|
1390
|
+
console.log("[codex] No sessions directory found \u2014 Codex CLI not installed or not yet used");
|
|
1391
|
+
return;
|
|
1392
|
+
}
|
|
1393
|
+
console.log(`[codex] Sessions directory found at ${sessionsDir}`);
|
|
1394
|
+
const existingFiles = await this.scanDeep(sessionsDir);
|
|
1395
|
+
for (const file of existingFiles) {
|
|
1396
|
+
await this.processFile(file);
|
|
1397
|
+
}
|
|
1398
|
+
const pattern = join9(sessionsDir, "**", "*.jsonl");
|
|
1399
|
+
const usePolling = process.platform === "win32";
|
|
1400
|
+
this.watcher = chokidar4.watch(pattern, {
|
|
1401
|
+
persistent: true,
|
|
1402
|
+
ignoreInitial: true,
|
|
1403
|
+
usePolling,
|
|
1404
|
+
interval: usePolling ? 500 : void 0,
|
|
1405
|
+
awaitWriteFinish: usePolling ? false : { stabilityThreshold: 200, pollInterval: 50 }
|
|
1406
|
+
});
|
|
1407
|
+
this.watcher.on("add", (filePath) => {
|
|
1408
|
+
console.log(`[codex] New session file: ${filePath}`);
|
|
1409
|
+
this.processFile(filePath);
|
|
1410
|
+
});
|
|
1411
|
+
this.watcher.on("change", (filePath) => {
|
|
1412
|
+
this.processFile(filePath);
|
|
1413
|
+
});
|
|
1414
|
+
console.log(`[codex] Watching for JSONL files in ${sessionsDir} (polling: ${usePolling})`);
|
|
1415
|
+
}
|
|
1416
|
+
stop() {
|
|
1417
|
+
this.watcher?.close();
|
|
1418
|
+
this.byteOffsets.clear();
|
|
1419
|
+
this.fileLocks.clear();
|
|
1420
|
+
this.sessionInfoCache.clear();
|
|
1421
|
+
this.fileModels.clear();
|
|
1422
|
+
this.seenSubagentCalls.clear();
|
|
1423
|
+
}
|
|
1424
|
+
/**
|
|
1425
|
+
* Recursively scan for recently-modified JSONL files under the sessions dir.
|
|
1426
|
+
* Codex nests files as sessions/YYYY/MM/DD/rollout-*.jsonl.
|
|
1427
|
+
*/
|
|
1428
|
+
async scanDeep(dir) {
|
|
1429
|
+
const results = [];
|
|
1430
|
+
const now = Date.now();
|
|
1431
|
+
const walk = async (current) => {
|
|
1432
|
+
try {
|
|
1433
|
+
const entries = await readdir2(current, { withFileTypes: true });
|
|
1434
|
+
for (const entry of entries) {
|
|
1435
|
+
const full = join9(current, entry.name);
|
|
1436
|
+
if (entry.isDirectory()) {
|
|
1437
|
+
await walk(full);
|
|
1438
|
+
} else if (entry.name.endsWith(".jsonl")) {
|
|
1439
|
+
try {
|
|
1440
|
+
const s = await stat4(full);
|
|
1441
|
+
if (now - s.mtimeMs < config.activeThresholdMs) {
|
|
1442
|
+
results.push(full);
|
|
1443
|
+
}
|
|
1444
|
+
} catch {
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
} catch {
|
|
1449
|
+
}
|
|
916
1450
|
};
|
|
1451
|
+
await walk(dir);
|
|
1452
|
+
return results;
|
|
1453
|
+
}
|
|
1454
|
+
processFile(filePath) {
|
|
1455
|
+
const prev = this.fileLocks.get(filePath) ?? Promise.resolve();
|
|
1456
|
+
const next = prev.then(() => this.doProcessFile(filePath)).catch(() => {
|
|
1457
|
+
}).finally(() => {
|
|
1458
|
+
if (this.fileLocks.get(filePath) === next) {
|
|
1459
|
+
this.fileLocks.delete(filePath);
|
|
1460
|
+
}
|
|
1461
|
+
});
|
|
1462
|
+
this.fileLocks.set(filePath, next);
|
|
1463
|
+
}
|
|
1464
|
+
async doProcessFile(filePath) {
|
|
1465
|
+
try {
|
|
1466
|
+
const fileStats = await stat4(filePath);
|
|
1467
|
+
const currentOffset = this.byteOffsets.get(filePath) ?? 0;
|
|
1468
|
+
if (fileStats.size <= currentOffset)
|
|
1469
|
+
return;
|
|
1470
|
+
const handle = await open3(filePath, "r");
|
|
1471
|
+
try {
|
|
1472
|
+
const buffer = Buffer.alloc(fileStats.size - currentOffset);
|
|
1473
|
+
await handle.read(buffer, 0, buffer.length, currentOffset);
|
|
1474
|
+
this.byteOffsets.set(filePath, fileStats.size);
|
|
1475
|
+
const newContent = buffer.toString("utf-8");
|
|
1476
|
+
const lines = newContent.split("\n").filter((l) => l.trim());
|
|
1477
|
+
const sessionId = extractCodexSessionId(filePath);
|
|
1478
|
+
let sessionInfo = this.sessionInfoCache.get(filePath);
|
|
1479
|
+
let hadParsedActivity = false;
|
|
1480
|
+
for (const line of lines) {
|
|
1481
|
+
const envelope = this.parser.parseRaw(line);
|
|
1482
|
+
if (!envelope)
|
|
1483
|
+
continue;
|
|
1484
|
+
if (!sessionInfo) {
|
|
1485
|
+
const meta = this.parser.tryGetSessionMeta(envelope);
|
|
1486
|
+
if (meta) {
|
|
1487
|
+
sessionInfo = parseCodexSessionInfo(meta);
|
|
1488
|
+
this.sessionInfoCache.set(filePath, sessionInfo);
|
|
1489
|
+
continue;
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
const model = this.parser.tryGetModel(envelope);
|
|
1493
|
+
if (model) {
|
|
1494
|
+
this.fileModels.set(filePath, model);
|
|
1495
|
+
continue;
|
|
1496
|
+
}
|
|
1497
|
+
const currentModel = this.fileModels.get(filePath) ?? null;
|
|
1498
|
+
const parsed = this.parser.parseEntry(envelope, currentModel);
|
|
1499
|
+
if (parsed) {
|
|
1500
|
+
hadParsedActivity = true;
|
|
1501
|
+
if (!sessionInfo) {
|
|
1502
|
+
sessionInfo = this.buildFallbackSession(filePath);
|
|
1503
|
+
this.sessionInfoCache.set(filePath, sessionInfo);
|
|
1504
|
+
}
|
|
1505
|
+
if (parsed.type === "tool_use" && parsed.toolName === "Agent") {
|
|
1506
|
+
this.handleSubagentSpawn(sessionId, sessionInfo, parsed, envelope.payload);
|
|
1507
|
+
}
|
|
1508
|
+
this.stateManager.processMessage(sessionId, parsed, sessionInfo);
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
if (!hadParsedActivity && lines.length > 0) {
|
|
1512
|
+
this.stateManager.heartbeat(sessionId);
|
|
1513
|
+
}
|
|
1514
|
+
} finally {
|
|
1515
|
+
await handle.close();
|
|
1516
|
+
}
|
|
1517
|
+
} catch (err) {
|
|
1518
|
+
if (err.code !== "ENOENT") {
|
|
1519
|
+
console.error(`[codex] Error processing ${filePath}:`, err);
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
/**
|
|
1524
|
+
* When a spawn_agent tool call is detected, create a synthetic subagent session
|
|
1525
|
+
* so the visualization shows the child agent.
|
|
1526
|
+
*/
|
|
1527
|
+
handleSubagentSpawn(parentSessionId, parentInfo, parsed, payload) {
|
|
1528
|
+
const callId = payload.call_id;
|
|
1529
|
+
if (!callId || this.seenSubagentCalls.has(callId))
|
|
1530
|
+
return;
|
|
1531
|
+
this.subagentCounter++;
|
|
1532
|
+
this.seenSubagentCalls.add(callId);
|
|
1533
|
+
const subSessionId = `codex:sub-${parentSessionId.replace("codex:", "")}-${this.subagentCounter}`;
|
|
1534
|
+
const subInfo = createCodexSubagentSession(parentSessionId, parentInfo);
|
|
1535
|
+
const agentName = parsed.toolInput?.agent_type || parsed.toolInput?.message?.slice(0, 30) || `agent-${this.subagentCounter}`;
|
|
1536
|
+
this.stateManager.processMessage(subSessionId, {
|
|
1537
|
+
type: "tool_use",
|
|
1538
|
+
toolName: "thinking",
|
|
1539
|
+
toolInput: { thought: `Subagent: ${agentName}` },
|
|
1540
|
+
model: parsed.model,
|
|
1541
|
+
agentName
|
|
1542
|
+
}, subInfo);
|
|
1543
|
+
}
|
|
1544
|
+
buildFallbackSession(filePath) {
|
|
1545
|
+
const name = basename4(filePath, ".jsonl");
|
|
1546
|
+
return createFallbackSession("codex", name);
|
|
917
1547
|
}
|
|
918
1548
|
};
|
|
919
1549
|
var COOLDOWN_MS = 6e4;
|
|
@@ -1169,6 +1799,9 @@ var TaskGraphManager = class {
|
|
|
1169
1799
|
if (toolName === "TaskUpdate") {
|
|
1170
1800
|
return this.handleUpdate(agentId, agentName, toolInput, projectName, root);
|
|
1171
1801
|
}
|
|
1802
|
+
if (toolName === "TodoWrite" || toolName === "update_plan") {
|
|
1803
|
+
return this.handlePlanUpdate(agentId, agentName, toolInput, projectName, root);
|
|
1804
|
+
}
|
|
1172
1805
|
return false;
|
|
1173
1806
|
}
|
|
1174
1807
|
handleCreate(agentId, agentName, toolInput, projectName, root) {
|
|
@@ -1274,6 +1907,83 @@ var TaskGraphManager = class {
|
|
|
1274
1907
|
}
|
|
1275
1908
|
return changed;
|
|
1276
1909
|
}
|
|
1910
|
+
/**
|
|
1911
|
+
* Handle plan/todo updates from multiple formats:
|
|
1912
|
+
* - Codex update_plan: {plan: [{step: "...", status: "pending"|"in_progress"|"completed"}, ...]}
|
|
1913
|
+
* - Claude Code / OpenCode TodoWrite: {todos: [{id, content, status}, ...]}
|
|
1914
|
+
* Each call replaces the full plan — we sync to create/update tasks accordingly.
|
|
1915
|
+
*/
|
|
1916
|
+
handlePlanUpdate(agentId, agentName, toolInput, projectName, root) {
|
|
1917
|
+
const input = toolInput;
|
|
1918
|
+
if (!input)
|
|
1919
|
+
return false;
|
|
1920
|
+
const steps = this.extractPlanSteps(input);
|
|
1921
|
+
if (steps.length === 0)
|
|
1922
|
+
return false;
|
|
1923
|
+
let changed = false;
|
|
1924
|
+
const existingBySubject = /* @__PURE__ */ new Map();
|
|
1925
|
+
for (const [key, task] of this.tasks) {
|
|
1926
|
+
if (key.startsWith(root + "::")) {
|
|
1927
|
+
existingBySubject.set(task.subject, task);
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
for (const { subject, status } of steps) {
|
|
1931
|
+
const existing = existingBySubject.get(subject);
|
|
1932
|
+
if (existing) {
|
|
1933
|
+
if (existing.status !== status) {
|
|
1934
|
+
existing.status = status;
|
|
1935
|
+
changed = true;
|
|
1936
|
+
}
|
|
1937
|
+
} else {
|
|
1938
|
+
const count = (this.counters.get(root) ?? 0) + 1;
|
|
1939
|
+
this.counters.set(root, count);
|
|
1940
|
+
const shortId = String(count);
|
|
1941
|
+
const key = this.scopedKey(root, shortId);
|
|
1942
|
+
const node = {
|
|
1943
|
+
id: shortId,
|
|
1944
|
+
subject,
|
|
1945
|
+
status,
|
|
1946
|
+
owner: void 0,
|
|
1947
|
+
agentId,
|
|
1948
|
+
agentName,
|
|
1949
|
+
projectName,
|
|
1950
|
+
blocks: [],
|
|
1951
|
+
blockedBy: [],
|
|
1952
|
+
timestamp: Date.now(),
|
|
1953
|
+
_rootKey: key
|
|
1954
|
+
};
|
|
1955
|
+
this.tasks.set(key, node);
|
|
1956
|
+
existingBySubject.set(subject, node);
|
|
1957
|
+
changed = true;
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
return changed;
|
|
1961
|
+
}
|
|
1962
|
+
/**
|
|
1963
|
+
* Extract plan steps from various tool input formats into a common shape.
|
|
1964
|
+
*/
|
|
1965
|
+
extractPlanSteps(input) {
|
|
1966
|
+
const planSteps = input.plan;
|
|
1967
|
+
if (Array.isArray(planSteps) && planSteps.length > 0) {
|
|
1968
|
+
return planSteps.filter((s) => s.step?.trim()).map((s) => ({ subject: s.step.trim(), status: this.normalizePlanStatus(s.status) }));
|
|
1969
|
+
}
|
|
1970
|
+
const todos = input.todos;
|
|
1971
|
+
if (Array.isArray(todos) && todos.length > 0) {
|
|
1972
|
+
return todos.filter((t) => t.content?.trim()).map((t) => ({ subject: t.content.trim(), status: this.normalizePlanStatus(t.status) }));
|
|
1973
|
+
}
|
|
1974
|
+
return [];
|
|
1975
|
+
}
|
|
1976
|
+
normalizePlanStatus(status) {
|
|
1977
|
+
if (!status)
|
|
1978
|
+
return "pending";
|
|
1979
|
+
if (status === "in_progress" || status === "in-progress")
|
|
1980
|
+
return "in_progress";
|
|
1981
|
+
if (status === "completed" || status === "done")
|
|
1982
|
+
return "completed";
|
|
1983
|
+
if (status === "deleted" || status === "cancelled")
|
|
1984
|
+
return "deleted";
|
|
1985
|
+
return "pending";
|
|
1986
|
+
}
|
|
1277
1987
|
/**
|
|
1278
1988
|
* Mark a task as completed (hook-sourced — TaskCompleted event).
|
|
1279
1989
|
* Returns true if the status actually changed.
|
|
@@ -1495,7 +2205,7 @@ function processToolUseActivity(deps, agent, activity, now) {
|
|
|
1495
2205
|
anomalyDetector.setAgentName(agentId, agent.agentName ?? agent.projectName ?? agentId.slice(0, 10));
|
|
1496
2206
|
anomalyDetector.checkToolUse(agentId, toolName);
|
|
1497
2207
|
toolChainTracker.recordToolUse(agentId, toolName);
|
|
1498
|
-
if (toolName === "TaskCreate" || toolName === "TaskUpdate") {
|
|
2208
|
+
if (toolName === "TaskCreate" || toolName === "TaskUpdate" || toolName === "TodoWrite") {
|
|
1499
2209
|
const graphChanged = taskGraphManager.processToolUse(agentId, agent.agentName ?? agentId.slice(0, 10), toolName, activity.toolInput, agent.projectName, agent.rootSessionId);
|
|
1500
2210
|
if (graphChanged) {
|
|
1501
2211
|
emit("taskgraph:changed", { data: taskGraphManager.getSnapshot(), timestamp: Date.now() });
|
|
@@ -1919,6 +2629,7 @@ var AgentStateManager = class extends EventEmitter2 {
|
|
|
1919
2629
|
agent = {
|
|
1920
2630
|
id: sessionId,
|
|
1921
2631
|
sessionId,
|
|
2632
|
+
agentType: sessionInfo.agentType,
|
|
1922
2633
|
rootSessionId,
|
|
1923
2634
|
projectPath: sessionInfo.projectPath,
|
|
1924
2635
|
projectName: sessionInfo.projectName,
|
|
@@ -2682,12 +3393,12 @@ function buildResponse(decision) {
|
|
|
2682
3393
|
}
|
|
2683
3394
|
};
|
|
2684
3395
|
}
|
|
2685
|
-
var __dirname =
|
|
3396
|
+
var __dirname = dirname2(fileURLToPath(import.meta.url));
|
|
2686
3397
|
async function main() {
|
|
2687
3398
|
const app = Fastify({ logger: { level: "info" } });
|
|
2688
3399
|
await app.register(cors, { origin: true });
|
|
2689
3400
|
await app.register(websocket);
|
|
2690
|
-
const clientDist =
|
|
3401
|
+
const clientDist = join10(__dirname, "..", "..", "client", "dist");
|
|
2691
3402
|
await app.register(fastifyStatic, {
|
|
2692
3403
|
root: clientDist,
|
|
2693
3404
|
prefix: "/",
|
|
@@ -2719,7 +3430,9 @@ async function main() {
|
|
|
2719
3430
|
});
|
|
2720
3431
|
const watchers = [
|
|
2721
3432
|
new FileWatcher(config.claudeHome, stateManager),
|
|
2722
|
-
...config.enableOpenCode ? [new OpenCodeWatcher(stateManager)] : []
|
|
3433
|
+
...config.enableOpenCode ? [new OpenCodeWatcher(stateManager)] : [],
|
|
3434
|
+
...config.enablePi ? [new PiWatcher(stateManager)] : [],
|
|
3435
|
+
...config.enableCodex ? [new CodexWatcher(stateManager)] : []
|
|
2723
3436
|
];
|
|
2724
3437
|
for (const w of watchers) {
|
|
2725
3438
|
await w.start();
|