@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.
Files changed (111) hide show
  1. package/README.md +46 -9
  2. package/package.json +1 -1
  3. package/packages/client/dist/assets/{BufferResource-Ddjob236.js → BufferResource-Dfd5uHKt.js} +1 -1
  4. package/packages/client/dist/assets/{CanvasRenderer-B0w6SYyW.js → CanvasRenderer-7Cv6xZVP.js} +1 -1
  5. package/packages/client/dist/assets/{Filter-NcMGuiK-.js → Filter-CBX7EB7j.js} +1 -1
  6. package/packages/client/dist/assets/{RenderTargetSystem-DgAzY5_U.js → RenderTargetSystem-ko-v73NG.js} +1 -1
  7. package/packages/client/dist/assets/{WebGLRenderer-DUWXDPIX.js → WebGLRenderer-vhPQEPUG.js} +1 -1
  8. package/packages/client/dist/assets/{WebGPURenderer-C1HbrllR.js → WebGPURenderer-Dwywvwqe.js} +1 -1
  9. package/packages/client/dist/assets/{browserAll-CaF1Fl0O.js → browserAll-QyCAT8_K.js} +1 -1
  10. package/packages/client/dist/assets/index-BPJtz4FL.js +722 -0
  11. package/packages/client/dist/assets/{webworkerAll-BJ6UhC7r.js → webworkerAll-hM-gNP7L.js} +1 -1
  12. package/packages/client/dist/index.html +1 -1
  13. package/packages/server/dist/config.d.ts +4 -0
  14. package/packages/server/dist/config.d.ts.map +1 -1
  15. package/packages/server/dist/config.js +5 -1
  16. package/packages/server/dist/config.js.map +1 -1
  17. package/packages/server/dist/index.d.ts.map +1 -1
  18. package/packages/server/dist/index.js +790 -77
  19. package/packages/server/dist/index.js.map +1 -1
  20. package/packages/server/dist/state/activity-processor.d.ts +1 -1
  21. package/packages/server/dist/state/activity-processor.d.ts.map +1 -1
  22. package/packages/server/dist/state/activity-processor.js +1 -1
  23. package/packages/server/dist/state/activity-processor.js.map +1 -1
  24. package/packages/server/dist/state/agent-state-manager.d.ts +1 -2
  25. package/packages/server/dist/state/agent-state-manager.d.ts.map +1 -1
  26. package/packages/server/dist/state/agent-state-manager.js +87 -2
  27. package/packages/server/dist/state/agent-state-manager.js.map +1 -1
  28. package/packages/server/dist/state/role-resolver.d.ts +1 -2
  29. package/packages/server/dist/state/role-resolver.d.ts.map +1 -1
  30. package/packages/server/dist/state/role-resolver.js.map +1 -1
  31. package/packages/server/dist/state/task-graph-manager.d.ts +12 -0
  32. package/packages/server/dist/state/task-graph-manager.d.ts.map +1 -1
  33. package/packages/server/dist/state/task-graph-manager.js +80 -0
  34. package/packages/server/dist/state/task-graph-manager.js.map +1 -1
  35. package/packages/server/dist/watcher/claude/claude-paths.d.ts +18 -0
  36. package/packages/server/dist/watcher/claude/claude-paths.d.ts.map +1 -0
  37. package/packages/server/dist/watcher/{claude-paths.js → claude/claude-paths.js} +47 -55
  38. package/packages/server/dist/watcher/claude/claude-paths.js.map +1 -0
  39. package/packages/server/dist/watcher/{file-watcher.d.ts → claude/claude-watcher.d.ts} +3 -3
  40. package/packages/server/dist/watcher/claude/claude-watcher.d.ts.map +1 -0
  41. package/packages/server/dist/watcher/{file-watcher.js → claude/claude-watcher.js} +59 -65
  42. package/packages/server/dist/watcher/claude/claude-watcher.js.map +1 -0
  43. package/packages/server/dist/watcher/claude/jsonl-parser.d.ts +6 -0
  44. package/packages/server/dist/watcher/claude/jsonl-parser.d.ts.map +1 -0
  45. package/packages/server/dist/watcher/{jsonl-parser.js → claude/jsonl-parser.js} +1 -1
  46. package/packages/server/dist/watcher/claude/jsonl-parser.js.map +1 -0
  47. package/packages/server/dist/watcher/codex/codex-parser.d.ts +30 -0
  48. package/packages/server/dist/watcher/codex/codex-parser.d.ts.map +1 -0
  49. package/packages/server/dist/watcher/codex/codex-parser.js +326 -0
  50. package/packages/server/dist/watcher/codex/codex-parser.js.map +1 -0
  51. package/packages/server/dist/watcher/codex/codex-paths.d.ts +35 -0
  52. package/packages/server/dist/watcher/codex/codex-paths.d.ts.map +1 -0
  53. package/packages/server/dist/watcher/codex/codex-paths.js +46 -0
  54. package/packages/server/dist/watcher/codex/codex-paths.js.map +1 -0
  55. package/packages/server/dist/watcher/codex/codex-watcher.d.ts +42 -0
  56. package/packages/server/dist/watcher/codex/codex-watcher.d.ts.map +1 -0
  57. package/packages/server/dist/watcher/codex/codex-watcher.js +577 -0
  58. package/packages/server/dist/watcher/codex/codex-watcher.js.map +1 -0
  59. package/packages/server/dist/watcher/opencode/opencode-parser.d.ts +1 -1
  60. package/packages/server/dist/watcher/opencode/opencode-parser.d.ts.map +1 -1
  61. package/packages/server/dist/watcher/opencode/opencode-parser.js +31 -2
  62. package/packages/server/dist/watcher/opencode/opencode-paths.d.ts +1 -1
  63. package/packages/server/dist/watcher/opencode/opencode-paths.d.ts.map +1 -1
  64. package/packages/server/dist/watcher/opencode/opencode-paths.js +1 -0
  65. package/packages/server/dist/watcher/opencode/opencode-paths.js.map +1 -1
  66. package/packages/server/dist/watcher/opencode/opencode-watcher.d.ts.map +1 -1
  67. package/packages/server/dist/watcher/opencode/opencode-watcher.js +48 -10
  68. package/packages/server/dist/watcher/opencode/opencode-watcher.js.map +1 -1
  69. package/packages/server/dist/watcher/path-utils.d.ts +10 -0
  70. package/packages/server/dist/watcher/path-utils.d.ts.map +1 -0
  71. package/packages/server/dist/watcher/path-utils.js +38 -0
  72. package/packages/server/dist/watcher/path-utils.js.map +1 -0
  73. package/packages/server/dist/watcher/pi/pi-parser.d.ts +19 -0
  74. package/packages/server/dist/watcher/pi/pi-parser.d.ts.map +1 -0
  75. package/packages/server/dist/watcher/pi/pi-parser.js +307 -0
  76. package/packages/server/dist/watcher/pi/pi-parser.js.map +1 -0
  77. package/packages/server/dist/watcher/pi/pi-paths.d.ts +28 -0
  78. package/packages/server/dist/watcher/pi/pi-paths.d.ts.map +1 -0
  79. package/packages/server/dist/watcher/pi/pi-paths.js +86 -0
  80. package/packages/server/dist/watcher/pi/pi-paths.js.map +1 -0
  81. package/packages/server/dist/watcher/pi/pi-watcher.d.ts +36 -0
  82. package/packages/server/dist/watcher/pi/pi-watcher.d.ts.map +1 -0
  83. package/packages/server/dist/watcher/pi/pi-watcher.js +593 -0
  84. package/packages/server/dist/watcher/pi/pi-watcher.js.map +1 -0
  85. package/packages/server/dist/watcher/session-scanner.d.ts +9 -3
  86. package/packages/server/dist/watcher/session-scanner.d.ts.map +1 -1
  87. package/packages/server/dist/watcher/session-scanner.js +11 -9
  88. package/packages/server/dist/watcher/session-scanner.js.map +1 -1
  89. package/packages/server/dist/watcher/types.d.ts +30 -0
  90. package/packages/server/dist/watcher/types.d.ts.map +1 -0
  91. package/packages/server/dist/watcher/types.js +14 -0
  92. package/packages/server/dist/watcher/types.js.map +1 -0
  93. package/packages/shared/dist/constants/colors.d.ts +1 -1
  94. package/packages/shared/dist/constants/colors.js +1 -1
  95. package/packages/shared/dist/constants/colors.js.map +1 -1
  96. package/packages/shared/dist/constants/tools.d.ts.map +1 -1
  97. package/packages/shared/dist/constants/tools.js +30 -1
  98. package/packages/shared/dist/constants/tools.js.map +1 -1
  99. package/packages/shared/dist/index.d.ts +1 -1
  100. package/packages/shared/dist/index.d.ts.map +1 -1
  101. package/packages/shared/dist/types/agent.d.ts +3 -0
  102. package/packages/shared/dist/types/agent.d.ts.map +1 -1
  103. package/packages/client/dist/assets/index-Dh8yWoLP.js +0 -711
  104. package/packages/server/dist/watcher/claude-paths.d.ts +0 -32
  105. package/packages/server/dist/watcher/claude-paths.d.ts.map +0 -1
  106. package/packages/server/dist/watcher/claude-paths.js.map +0 -1
  107. package/packages/server/dist/watcher/file-watcher.d.ts.map +0 -1
  108. package/packages/server/dist/watcher/file-watcher.js.map +0 -1
  109. package/packages/server/dist/watcher/jsonl-parser.d.ts +0 -21
  110. package/packages/server/dist/watcher/jsonl-parser.d.ts.map +0 -1
  111. 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 join6 } from "path";
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 resolved = this.resolveToFolderName(encoded);
170
- if (resolved)
171
- return resolved;
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
- claudeHome;
231
- constructor(claudeHome) {
232
- this.claudeHome = claudeHome;
238
+ rootDir;
239
+ constructor(rootDir) {
240
+ this.rootDir = rootDir;
233
241
  }
234
- /** Find all recently active JSONL session files */
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(projectsDir);
246
+ const projects = await readdir(this.rootDir);
240
247
  const now = Date.now();
241
248
  for (const project of projects) {
242
- const projectDir = join3(projectsDir, project);
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
- projectPath: "opencode",
912
- projectName: "opencode",
913
- isSubagent: false,
914
- projectDir: "opencode",
915
- parentSessionId: null
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 = dirname(fileURLToPath(import.meta.url));
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 = join6(__dirname, "..", "..", "client", "dist");
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();