agmon 0.2.1 → 0.2.2

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 (152) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +3 -3
  3. package/.next/fallback-build-manifest.json +3 -3
  4. package/.next/prerender-manifest.json +8 -8
  5. package/.next/server/app/_global-error.html +1 -1
  6. package/.next/server/app/_global-error.rsc +1 -1
  7. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  8. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  9. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  10. package/.next/server/app/_not-found/page.js.nft.json +1 -1
  11. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  12. package/.next/server/app/_not-found.html +1 -1
  13. package/.next/server/app/_not-found.rsc +16 -15
  14. package/.next/server/app/_not-found.segments/_full.segment.rsc +16 -15
  15. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +8 -7
  16. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  17. package/.next/server/app/api/media/route.js.nft.json +1 -1
  18. package/.next/server/app/api/reports/tokens/route.js.nft.json +1 -1
  19. package/.next/server/app/api/sessions/[id]/transcript/route.js +1 -1
  20. package/.next/server/app/api/sessions/[id]/transcript/route.js.nft.json +1 -1
  21. package/.next/server/app/api/traces/stream/route.js +1 -1
  22. package/.next/server/app/api/traces/stream/route.js.nft.json +1 -1
  23. package/.next/server/app/api/version/route.js.nft.json +1 -1
  24. package/.next/server/app/api/workspace/file-content/route.js.nft.json +1 -1
  25. package/.next/server/app/api/workspace/files/route.js.nft.json +1 -1
  26. package/.next/server/app/chat/page.js.nft.json +1 -1
  27. package/.next/server/app/chat/page_client-reference-manifest.js +1 -1
  28. package/.next/server/app/chat.html +1 -1
  29. package/.next/server/app/chat.rsc +4 -4
  30. package/.next/server/app/chat.segments/_full.segment.rsc +4 -4
  31. package/.next/server/app/chat.segments/_tree.segment.rsc +2 -2
  32. package/.next/server/app/chat.segments/chat/__PAGE__.segment.rsc +6 -5
  33. package/.next/server/app/index.html +1 -1
  34. package/.next/server/app/index.rsc +5 -5
  35. package/.next/server/app/index.segments/__PAGE__.segment.rsc +7 -6
  36. package/.next/server/app/index.segments/_full.segment.rsc +5 -5
  37. package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
  38. package/.next/server/app/page.js.nft.json +1 -1
  39. package/.next/server/app/page_client-reference-manifest.js +1 -1
  40. package/.next/server/app/reports/page.js.nft.json +1 -1
  41. package/.next/server/app/reports/page_client-reference-manifest.js +1 -1
  42. package/.next/server/app/reports.html +1 -1
  43. package/.next/server/app/reports.rsc +2 -2
  44. package/.next/server/app/reports.segments/_full.segment.rsc +2 -2
  45. package/.next/server/app/reports.segments/_tree.segment.rsc +2 -2
  46. package/.next/server/app/reports.segments/reports/__PAGE__.segment.rsc +4 -3
  47. package/.next/server/app/store/page.js.nft.json +1 -1
  48. package/.next/server/app/store/page_client-reference-manifest.js +1 -1
  49. package/.next/server/app/store.html +1 -1
  50. package/.next/server/app/store.rsc +2 -2
  51. package/.next/server/app/store.segments/_full.segment.rsc +2 -2
  52. package/.next/server/app/store.segments/_tree.segment.rsc +2 -2
  53. package/.next/server/app/store.segments/store/__PAGE__.segment.rsc +4 -3
  54. package/.next/server/chunks/[root-of-the-server]__0dw97_pahi8uf._.js +3 -0
  55. package/.next/server/chunks/[root-of-the-server]__0dw97_pahi8uf._.js.map +1 -0
  56. package/.next/server/chunks/_0ao-_t4jkb46h._.js +2 -2
  57. package/.next/server/chunks/_0ao-_t4jkb46h._.js.map +1 -1
  58. package/.next/server/chunks/_1-8ipqazoyrrk._.js +1 -1
  59. package/.next/server/chunks/_1-8ipqazoyrrk._.js.map +1 -1
  60. package/.next/server/chunks/ssr/[root-of-the-server]__1jwplflwix8wi._.js +1 -1
  61. package/.next/server/chunks/ssr/[root-of-the-server]__1jwplflwix8wi._.js.map +1 -1
  62. package/.next/server/chunks/ssr/[root-of-the-server]__1kt7h7bz59mdn._.js +1 -1
  63. package/.next/server/chunks/ssr/[root-of-the-server]__1kt7h7bz59mdn._.js.map +1 -1
  64. package/.next/server/chunks/ssr/[root-of-the-server]__1otsvjy81s8my._.js +1 -1
  65. package/.next/server/chunks/ssr/[root-of-the-server]__1otsvjy81s8my._.js.map +1 -1
  66. package/.next/server/chunks/ssr/{_0--937zhi743v._.js → _0zel-n0h--cnl._.js} +1 -1
  67. package/.next/server/chunks/ssr/{node_modules_0jla9ugf1y6rb._.js → node_modules_01avo0bde3tpo._.js} +1 -1
  68. package/.next/server/chunks/ssr/{node_modules_0sjdrli5douui._.js → node_modules_01l0208e5kqic._.js} +1 -1
  69. package/.next/server/chunks/ssr/{node_modules_0lqovp8j8ld76._.js → node_modules_033x29yqle_sj._.js} +1 -1
  70. package/.next/server/chunks/ssr/{node_modules_05i69rc9pwcdx._.js → node_modules_03wbg__aslvui._.js} +1 -1
  71. package/.next/server/chunks/ssr/{node_modules_1ltys5mwtcv-n._.js → node_modules_08-bqkrz7sh0l._.js} +1 -1
  72. package/.next/server/chunks/ssr/{node_modules_09v49jhajmvl4._.js → node_modules_089w9jm7s2qa3._.js} +2 -2
  73. package/.next/server/chunks/ssr/{node_modules_0z1e37smavebg._.js → node_modules_0cy47-s63xjzh._.js} +2 -2
  74. package/.next/server/chunks/ssr/{node_modules_0dw0vozejrnml._.js → node_modules_0czh0jcnsm5e6._.js} +2 -2
  75. package/.next/server/chunks/ssr/{node_modules_0e3njj436xau-._.js → node_modules_0fv321j1nslkb._.js} +1 -1
  76. package/.next/server/chunks/ssr/{node_modules_0d2ios01ot1in._.js → node_modules_0g-wb7h-hsdwc._.js} +1 -1
  77. package/.next/server/chunks/ssr/{node_modules_1l46or7mgko0z._.js → node_modules_0oz_zzlh8exwx._.js} +1 -1
  78. package/.next/server/chunks/ssr/{node_modules_0b8zi3pbl-y9n._.js → node_modules_0so2agzqd9p8l._.js} +2 -2
  79. package/.next/server/chunks/ssr/{node_modules_1wew4nn7i1uup._.js → node_modules_0w2w61b8bh769._.js} +1 -1
  80. package/.next/server/chunks/ssr/{node_modules_07bjxeq9dfgn1._.js → node_modules_0wd6szix_zku7._.js} +2 -2
  81. package/.next/server/chunks/ssr/{node_modules_1aqaelh-8fme6._.js → node_modules_0wp7omf0xblv8._.js} +1 -1
  82. package/.next/server/chunks/ssr/{node_modules_0xh8tnq1niblb._.js → node_modules_10c51o2qv6mbi._.js} +1 -1
  83. package/.next/server/chunks/ssr/{node_modules_1d4sreyq0ubqg._.js → node_modules_19vqq7rvf-_ar._.js} +2 -2
  84. package/.next/server/chunks/ssr/{node_modules_1e_nc2eyg4djz._.js → node_modules_1hu83gui7_nj2._.js} +1 -1
  85. package/.next/server/chunks/ssr/{node_modules_1zbmc81s-4zsm._.js → node_modules_1j8hpoiygrs5e._.js} +1 -1
  86. package/.next/server/chunks/ssr/{node_modules_1pquu5wprusnc._.js → node_modules_1n4kig4mj6n4s._.js} +2 -2
  87. package/.next/server/chunks/ssr/{node_modules_1st9dwe66yx93._.js → node_modules_1s8oxeqa5_i3k._.js} +1 -1
  88. package/.next/server/chunks/ssr/{node_modules_01p9ctt-n2dxp._.js → node_modules_1xz1dstlo5ri-._.js} +1 -1
  89. package/.next/server/chunks/ssr/{node_modules_katex_dist_katex_mjs_0kqo9zl9cgwbo._.js → node_modules_katex_dist_katex_mjs_067omoj5szuys._.js} +1 -1
  90. package/.next/server/chunks/ssr/src_app_page_1vqfk0-j72fp6.js +1 -1
  91. package/.next/server/chunks/ssr/src_app_page_1vqfk0-j72fp6.js.map +1 -1
  92. package/.next/server/middleware-build-manifest.js +3 -3
  93. package/.next/server/pages/404.html +1 -1
  94. package/.next/server/pages/500.html +1 -1
  95. package/.next/server/preview-props.json +3 -3
  96. package/.next/server/server-reference-manifest.js +1 -1
  97. package/.next/server/server-reference-manifest.json +1 -1
  98. package/.next/static/chunks/0jo6fy2c_gmqn.js +1 -0
  99. package/.next/static/chunks/2q9cwykv6ox1t.css +1 -0
  100. package/.next/static/chunks/{3og0e9qcff393.js → 2zf44j8sjo0w0.js} +4 -4
  101. package/.next/trace +1 -1
  102. package/.next/trace-build +1 -1
  103. package/package.json +1 -1
  104. package/src/app/api/traces/stream/route.js +41 -5
  105. package/src/app/layout.js +2 -1
  106. package/src/app/page.js +17 -1
  107. package/src/components/chat/SessionChatView.js +1 -1
  108. package/src/components/factory/office-canvas.js +32 -1
  109. package/src/components/layout/BottomPanel.jsx +6 -6
  110. package/src/components/layout/LeftSidebar.jsx +62 -4
  111. package/src/components/layout/StatusBar.jsx +1 -1
  112. package/src/components/layout/TitleBar.jsx +71 -1
  113. package/src/components/layout/VSCodeWorkbench.jsx +21 -0
  114. package/src/lib/mockTraces.js +13 -11
  115. package/src/lib/parsers/codexParser.js +373 -0
  116. package/src/lib/parsers/transcriptParser.js +35 -3
  117. package/src/lib/useFactoryTraces.js +70 -5
  118. package/src/lib/watchers/antigravityWatcher.js +15 -1
  119. package/src/lib/watchers/claudeWatcher.js +64 -53
  120. package/src/lib/watchers/codexWatcher.js +482 -0
  121. package/src/lib/watchers/watcherManager.js +46 -30
  122. package/.next/server/chunks/src_lib_watchers_antigravityWatcher_0w1-atg39y__p.js +0 -3
  123. package/.next/server/chunks/src_lib_watchers_antigravityWatcher_0w1-atg39y__p.js.map +0 -1
  124. package/.next/static/chunks/17jorrh_pfel_.js +0 -1
  125. package/.next/static/chunks/25l0z48vlvdu4.css +0 -1
  126. /package/.next/server/chunks/ssr/{_0--937zhi743v._.js.map → _0zel-n0h--cnl._.js.map} +0 -0
  127. /package/.next/server/chunks/ssr/{node_modules_0jla9ugf1y6rb._.js.map → node_modules_01avo0bde3tpo._.js.map} +0 -0
  128. /package/.next/server/chunks/ssr/{node_modules_0sjdrli5douui._.js.map → node_modules_01l0208e5kqic._.js.map} +0 -0
  129. /package/.next/server/chunks/ssr/{node_modules_0lqovp8j8ld76._.js.map → node_modules_033x29yqle_sj._.js.map} +0 -0
  130. /package/.next/server/chunks/ssr/{node_modules_05i69rc9pwcdx._.js.map → node_modules_03wbg__aslvui._.js.map} +0 -0
  131. /package/.next/server/chunks/ssr/{node_modules_1ltys5mwtcv-n._.js.map → node_modules_08-bqkrz7sh0l._.js.map} +0 -0
  132. /package/.next/server/chunks/ssr/{node_modules_09v49jhajmvl4._.js.map → node_modules_089w9jm7s2qa3._.js.map} +0 -0
  133. /package/.next/server/chunks/ssr/{node_modules_0z1e37smavebg._.js.map → node_modules_0cy47-s63xjzh._.js.map} +0 -0
  134. /package/.next/server/chunks/ssr/{node_modules_0dw0vozejrnml._.js.map → node_modules_0czh0jcnsm5e6._.js.map} +0 -0
  135. /package/.next/server/chunks/ssr/{node_modules_0e3njj436xau-._.js.map → node_modules_0fv321j1nslkb._.js.map} +0 -0
  136. /package/.next/server/chunks/ssr/{node_modules_0d2ios01ot1in._.js.map → node_modules_0g-wb7h-hsdwc._.js.map} +0 -0
  137. /package/.next/server/chunks/ssr/{node_modules_0xh8tnq1niblb._.js.map → node_modules_0oz_zzlh8exwx._.js.map} +0 -0
  138. /package/.next/server/chunks/ssr/{node_modules_0b8zi3pbl-y9n._.js.map → node_modules_0so2agzqd9p8l._.js.map} +0 -0
  139. /package/.next/server/chunks/ssr/{node_modules_1wew4nn7i1uup._.js.map → node_modules_0w2w61b8bh769._.js.map} +0 -0
  140. /package/.next/server/chunks/ssr/{node_modules_07bjxeq9dfgn1._.js.map → node_modules_0wd6szix_zku7._.js.map} +0 -0
  141. /package/.next/server/chunks/ssr/{node_modules_1aqaelh-8fme6._.js.map → node_modules_0wp7omf0xblv8._.js.map} +0 -0
  142. /package/.next/server/chunks/ssr/{node_modules_1d4sreyq0ubqg._.js.map → node_modules_10c51o2qv6mbi._.js.map} +0 -0
  143. /package/.next/server/chunks/ssr/{node_modules_1l46or7mgko0z._.js.map → node_modules_19vqq7rvf-_ar._.js.map} +0 -0
  144. /package/.next/server/chunks/ssr/{node_modules_1e_nc2eyg4djz._.js.map → node_modules_1hu83gui7_nj2._.js.map} +0 -0
  145. /package/.next/server/chunks/ssr/{node_modules_1zbmc81s-4zsm._.js.map → node_modules_1j8hpoiygrs5e._.js.map} +0 -0
  146. /package/.next/server/chunks/ssr/{node_modules_1pquu5wprusnc._.js.map → node_modules_1n4kig4mj6n4s._.js.map} +0 -0
  147. /package/.next/server/chunks/ssr/{node_modules_1st9dwe66yx93._.js.map → node_modules_1s8oxeqa5_i3k._.js.map} +0 -0
  148. /package/.next/server/chunks/ssr/{node_modules_01p9ctt-n2dxp._.js.map → node_modules_1xz1dstlo5ri-._.js.map} +0 -0
  149. /package/.next/server/chunks/ssr/{node_modules_katex_dist_katex_mjs_0kqo9zl9cgwbo._.js.map → node_modules_katex_dist_katex_mjs_067omoj5szuys._.js.map} +0 -0
  150. /package/.next/static/{1y6Da0YgnPr2_6ncpWrbV → dGN2VKPk5TJgezxS3JvRc}/_buildManifest.js +0 -0
  151. /package/.next/static/{1y6Da0YgnPr2_6ncpWrbV → dGN2VKPk5TJgezxS3JvRc}/_clientMiddlewareManifest.js +0 -0
  152. /package/.next/static/{1y6Da0YgnPr2_6ncpWrbV → dGN2VKPk5TJgezxS3JvRc}/_ssgManifest.js +0 -0
@@ -368,6 +368,8 @@ export async function getClaudeTraces(maxAgeMs = 24 * 60 * 60 * 1000) {
368
368
  // Catches all project-level active & historical sessions within the time window
369
369
  try {
370
370
  const projectFolders = await fs.readdir(projectsDir).catch(() => []);
371
+ const projectCandidates = [];
372
+
371
373
  for (const pf of projectFolders) {
372
374
  const folderPath = path.join(projectsDir, pf);
373
375
  let files = [];
@@ -387,60 +389,69 @@ export async function getClaudeTraces(maxAgeMs = 24 * 60 * 60 * 1000) {
387
389
  const stat = await fs.stat(transcriptPath);
388
390
  const mtime = stat.mtimeMs;
389
391
  if (now - mtime > maxAgeMs) continue;
390
-
391
392
  seenSessionIds.add(fileSessionId);
392
- const parsed = await parseClaudeTranscript(transcriptPath);
393
-
394
- const isRecent = (now - mtime) < 90 * 1000;
395
- const isPending = (now - mtime) < 15 * 60 * 1000;
396
- const state = isRecent ? "streaming" : isPending ? "idle" : "done";
397
-
398
- const { clientType, label } = classifyClaudeEntrypoint(...(parsed.entrypoints || []));
399
- const workspaceName = parsed.cwd
400
- ? path.basename(parsed.cwd)
401
- : (extractWorkspaceFromFolder(pf) || fallbackWorkspace);
402
-
403
- const elapsedMs = Math.max(0, (parsed.lastTimestamp || mtime) - stat.birthtimeMs);
404
-
405
- const sessionTitle =
406
- parsed.sessionTitle ||
407
- (parsed.slug ? parsed.slug.replace(/-/g, " ") : null) ||
408
- workspaceName;
409
-
410
- const model = parsed.model || null;
411
- const connectionId = `${label} (${parsed.slug || fileSessionId.slice(0, 6)})`;
412
-
413
- traces.push(normalizeTrace({
414
- traceId: `claude-${fileSessionId.slice(0, 6)}`,
415
- cli: "claude",
416
- clientType,
417
- source: clientType,
418
- connectionId,
419
- account: workspaceName,
420
- model,
421
- provider: detectProvider(model, clientType),
422
- state,
423
- startedAt: stat.birthtimeMs || null,
424
- elapsedMs: stat.birthtimeMs ? elapsedMs : null,
425
- tokens: {
426
- input: parsed.totalIn || null,
427
- output: parsed.totalOut || null,
428
- cached: parsed.totalCached || null,
429
- },
430
- cost: null,
431
- status: parsed.isLooping ? "error" : null,
432
- tools: Array.from(parsed.toolSet || []),
433
- activeTool: parsed.activeTool || null,
434
- currentCommand: parsed.activeCommand || null,
435
- sessionTitle,
436
- lastText: parsed.lastText || null,
437
- lastTextRole: parsed.lastTextRole || null,
438
- isLooping: parsed.isLooping || false,
439
- logs: (parsed.recentLogs || []).slice(-25),
440
- }));
441
- } catch {
442
- // ignore unreadable project file
443
- }
393
+ projectCandidates.push({ transcriptPath, mtime, stat, fileSessionId, pf });
394
+ } catch {}
395
+ }
396
+ }
397
+
398
+ // Sort by mtime descending (most recent first) and cap to top 100
399
+ projectCandidates.sort((a, b) => b.mtime - a.mtime);
400
+ const toProcess = projectCandidates.slice(0, 100);
401
+
402
+ for (const { transcriptPath, mtime, stat, fileSessionId, pf } of toProcess) {
403
+ try {
404
+ const parsed = await parseClaudeTranscript(transcriptPath);
405
+
406
+ const isRecent = (now - mtime) < 90 * 1000;
407
+ const isPending = (now - mtime) < 15 * 60 * 1000;
408
+ const state = isRecent ? "streaming" : isPending ? "idle" : "done";
409
+
410
+ const { clientType, label } = classifyClaudeEntrypoint(...(parsed.entrypoints || []));
411
+ const workspaceName = parsed.cwd
412
+ ? path.basename(parsed.cwd)
413
+ : (extractWorkspaceFromFolder(pf) || fallbackWorkspace);
414
+
415
+ const elapsedMs = Math.max(0, (parsed.lastTimestamp || mtime) - stat.birthtimeMs);
416
+
417
+ const sessionTitle =
418
+ parsed.sessionTitle ||
419
+ (parsed.slug ? parsed.slug.replace(/-/g, " ") : null) ||
420
+ workspaceName;
421
+
422
+ const model = parsed.model || null;
423
+ const connectionId = `${label} (${parsed.slug || fileSessionId.slice(0, 6)})`;
424
+
425
+ traces.push(normalizeTrace({
426
+ traceId: `claude-${fileSessionId.slice(0, 6)}`,
427
+ cli: "claude",
428
+ clientType,
429
+ source: clientType,
430
+ connectionId,
431
+ account: workspaceName,
432
+ model,
433
+ provider: detectProvider(model, clientType),
434
+ state,
435
+ startedAt: stat.birthtimeMs || null,
436
+ elapsedMs: stat.birthtimeMs ? elapsedMs : null,
437
+ tokens: {
438
+ input: parsed.totalIn || null,
439
+ output: parsed.totalOut || null,
440
+ cached: parsed.totalCached || null,
441
+ },
442
+ cost: null,
443
+ status: parsed.isLooping ? "error" : null,
444
+ tools: Array.from(parsed.toolSet || []),
445
+ activeTool: parsed.activeTool || null,
446
+ currentCommand: parsed.activeCommand || null,
447
+ sessionTitle,
448
+ lastText: parsed.lastText || null,
449
+ lastTextRole: parsed.lastTextRole || null,
450
+ isLooping: parsed.isLooping || false,
451
+ logs: (parsed.recentLogs || []).slice(-25),
452
+ }));
453
+ } catch {
454
+ // ignore unreadable project file
444
455
  }
445
456
  }
446
457
  } catch {
@@ -0,0 +1,482 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import os from "node:os";
4
+ import { exec } from "node:child_process";
5
+ import { promisify } from "node:util";
6
+ import { normalizeTrace } from "../traceContract.js";
7
+
8
+ const execAsync = promisify(exec);
9
+
10
+ /**
11
+ * Codex (OpenAI) stores one rollout file per session under
12
+ * ~/.codex/sessions/<YYYY>/<MM>/<DD>/rollout-<timestamp>-<uuid>.jsonl
13
+ *
14
+ * Rollout entries are `{ type, timestamp, payload }` where `type` is one of
15
+ * `session_meta`, `turn_context`, `response_item`, `event_msg`, `world_state`.
16
+ */
17
+
18
+ function compactText(value, maxLength = 160) {
19
+ const text = String(value || "").replace(/\s+/g, " ").trim();
20
+ return text ? text.slice(0, maxLength) : null;
21
+ }
22
+
23
+ function textFromContent(content) {
24
+ if (typeof content === "string") return compactText(content);
25
+ if (!Array.isArray(content)) return null;
26
+ const joined = content
27
+ .map((block) => (typeof block?.text === "string" ? block.text : ""))
28
+ .filter(Boolean)
29
+ .join(" ");
30
+ return compactText(joined);
31
+ }
32
+
33
+ /**
34
+ * Normalizes tool name and command/args from OpenAI Codex into a standard tool type.
35
+ */
36
+ export function normalizeCodexTool(name, args = {}) {
37
+ const n = String(name || "").toLowerCase();
38
+ const cmd = String(args.cmd || args.command || "").trim();
39
+ const cmdLower = cmd.toLowerCase();
40
+
41
+ if (n === "exec_command" || n === "local_shell_call" || n === "bash" || n === "shell") {
42
+ if (cmdLower.startsWith("docker") || cmdLower.includes("docker exec")) {
43
+ return { type: "docker", detail: cmd.length > 50 ? `${cmd.slice(0, 47)}...` : cmd };
44
+ }
45
+ if (cmdLower.startsWith("git ") || cmdLower.startsWith("gh ")) {
46
+ return { type: "git", detail: cmd.length > 50 ? `${cmd.slice(0, 47)}...` : cmd };
47
+ }
48
+ if (cmdLower.includes("gitnexus")) {
49
+ return { type: "gitnexus", detail: cmd.length > 50 ? `${cmd.slice(0, 47)}...` : cmd };
50
+ }
51
+ if (cmdLower.startsWith("rg ") || cmdLower.startsWith("grep ")) {
52
+ return { type: "search", detail: cmd.length > 50 ? `${cmd.slice(0, 47)}...` : cmd };
53
+ }
54
+ if (/^(cat|sed|head|tail|ls|find)\b/.test(cmdLower)) {
55
+ return { type: "read", detail: cmd.length > 50 ? `${cmd.slice(0, 47)}...` : cmd };
56
+ }
57
+ return { type: "bash", detail: cmd.length > 50 ? `${cmd.slice(0, 47)}...` : cmd };
58
+ }
59
+
60
+ if (n === "apply_patch" || n === "patch" || n.includes("write") || n.includes("edit") || n.includes("replace")) {
61
+ const file = args.file_path || args.path || args.TargetFile || "";
62
+ return { type: "edit", detail: file ? `Edit: ${path.basename(file)}` : "Edit file" };
63
+ }
64
+
65
+ if (n.includes("read") || n.includes("view") || n.includes("glob")) {
66
+ const file = args.file_path || args.path || args.AbsolutePath || "";
67
+ return { type: "read", detail: file ? `Read: ${path.basename(file)}` : "Read file" };
68
+ }
69
+
70
+ if (n.includes("search") || n.includes("grep") || n.includes("find")) {
71
+ return { type: "search", detail: args.pattern || args.query || "Search" };
72
+ }
73
+
74
+ if (n.includes("spawn_agent") || n.includes("followup_task") || n.includes("agent")) {
75
+ return { type: "agent", detail: "Subagent Task" };
76
+ }
77
+
78
+ if (n.includes("browser") || n.includes("playwright")) {
79
+ return { type: "browser", detail: "Browser automation" };
80
+ }
81
+
82
+ if (n.includes("web") || n.includes("fetch")) {
83
+ return { type: "web", detail: "Web fetch" };
84
+ }
85
+
86
+ return { type: "mcp", detail: n };
87
+ }
88
+
89
+ /**
90
+ * Classifies Codex surface into:
91
+ * - Desktop App: `clientType: "desktop", label: "Codex Desktop"`
92
+ * - CLI: `clientType: "cli", label: "Codex CLI"`
93
+ * - VS Code Extension: `clientType: "extension", label: "Codex Extension"`
94
+ *
95
+ * @returns {{ clientType: string, label: string }}
96
+ */
97
+ export function classifyCodexSession(originator, source) {
98
+ const org = String(originator || "").toLowerCase();
99
+ const src = String(source || "").toLowerCase();
100
+
101
+ if (
102
+ org.includes("desktop") ||
103
+ org === "codex_app" ||
104
+ org === "codex-app" ||
105
+ org === "codex_app_server" ||
106
+ src === "desktop" ||
107
+ src === "app"
108
+ ) {
109
+ return { clientType: "desktop", label: "Codex Desktop" };
110
+ }
111
+
112
+ if (org.includes("vscode") || src === "vscode") {
113
+ return { clientType: "extension", label: "Codex Extension" };
114
+ }
115
+
116
+ return { clientType: "cli", label: "Codex CLI" };
117
+ }
118
+
119
+ function extractWorkspace(cwd) {
120
+ return cwd ? path.basename(cwd) : null;
121
+ }
122
+
123
+ /** Scans ~/.codex/sessions/<YYYY>/<MM>/<DD> directories for .jsonl rollout files within maxAgeMs */
124
+ async function scanCodexRolloutFiles(sessionsRoot, maxAgeMs, now) {
125
+ const files = [];
126
+ let years;
127
+ try {
128
+ years = await fs.readdir(sessionsRoot, { withFileTypes: true });
129
+ } catch {
130
+ return files;
131
+ }
132
+
133
+ const yearDirs = years.filter((d) => d.isDirectory()).map((d) => d.name).sort().reverse();
134
+ for (const year of yearDirs) {
135
+ const yearPath = path.join(sessionsRoot, year);
136
+ let months;
137
+ try {
138
+ months = await fs.readdir(yearPath, { withFileTypes: true });
139
+ } catch {
140
+ continue;
141
+ }
142
+ const monthDirs = months.filter((d) => d.isDirectory()).map((d) => d.name).sort().reverse();
143
+
144
+ for (const month of monthDirs) {
145
+ const monthPath = path.join(yearPath, month);
146
+ let days;
147
+ try {
148
+ days = await fs.readdir(monthPath, { withFileTypes: true });
149
+ } catch {
150
+ continue;
151
+ }
152
+ const dayDirs = days.filter((d) => d.isDirectory()).map((d) => d.name).sort().reverse();
153
+
154
+ for (const day of dayDirs) {
155
+ if (Number.isFinite(maxAgeMs)) {
156
+ const dayDate = new Date(`${year}-${month}-${day}T23:59:59Z`).getTime();
157
+ if (now - dayDate > maxAgeMs) continue;
158
+ }
159
+
160
+ const dayPath = path.join(monthPath, day);
161
+ let entries;
162
+ try {
163
+ entries = await fs.readdir(dayPath);
164
+ } catch {
165
+ continue;
166
+ }
167
+
168
+ for (const name of entries) {
169
+ if (name.endsWith(".jsonl")) files.push(path.join(dayPath, name));
170
+ }
171
+ }
172
+ }
173
+ }
174
+
175
+ return files;
176
+ }
177
+
178
+ async function getRunningCodexProcesses() {
179
+ try {
180
+ const { stdout } = await execAsync("pgrep -l -i codex");
181
+ const lines = stdout.trim().split("\n").filter(Boolean);
182
+ return lines.map((line) => {
183
+ const [pidStr, ...rest] = line.trim().split(/\s+/);
184
+ return { pid: Number(pidStr), name: rest.join(" ").toLowerCase() };
185
+ });
186
+ } catch {
187
+ return [];
188
+ }
189
+ }
190
+
191
+ export async function parseCodexRollout(filePath) {
192
+ const raw = await fs.readFile(filePath, "utf-8");
193
+
194
+ const result = {
195
+ meta: null,
196
+ model: null,
197
+ cwd: null,
198
+ sessionTitle: null,
199
+ lastText: null,
200
+ lastTextRole: null,
201
+ toolSet: new Set(),
202
+ toolInvocations: [],
203
+ recentLogs: [],
204
+ activeTool: null,
205
+ currentCommand: null,
206
+ tokens: null,
207
+ startedAt: null,
208
+ lastTimestamp: null,
209
+ openTurn: false,
210
+ error: null,
211
+ isLooping: false,
212
+ };
213
+
214
+ for (const line of raw.split("\n")) {
215
+ if (!line.trim()) continue;
216
+ let entry;
217
+ try {
218
+ entry = JSON.parse(line);
219
+ } catch {
220
+ continue;
221
+ }
222
+
223
+ const ts = entry.timestamp ? new Date(entry.timestamp).getTime() : null;
224
+ if (ts && (!result.lastTimestamp || ts > result.lastTimestamp)) result.lastTimestamp = ts;
225
+
226
+ const payload = entry.payload || (entry.type === "message" || entry.role ? entry : null);
227
+ if (!payload) continue;
228
+
229
+ const entryType = entry.type || payload.type;
230
+
231
+ if (entryType === "session_meta") {
232
+ result.meta = payload;
233
+ result.cwd = payload.cwd || null;
234
+ result.startedAt = payload.timestamp ? new Date(payload.timestamp).getTime() : null;
235
+ continue;
236
+ }
237
+
238
+ if (entryType === "turn_context") {
239
+ if (payload.model) result.model = payload.model;
240
+ if (payload.cwd && !result.cwd) result.cwd = payload.cwd;
241
+ continue;
242
+ }
243
+
244
+ if (entryType === "response_item" || entryType === "message") {
245
+ const type = payload.type || (payload.role ? "message" : null);
246
+
247
+ if (type === "message") {
248
+ const role = payload.role;
249
+ if (role !== "user" && role !== "assistant") continue;
250
+
251
+ // Filter out synthetic injected instructions
252
+ const kinds = payload.internal_chat_message_metadata_passthrough?.content_item_kinds;
253
+ if (role === "user" && Array.isArray(kinds) && !kinds.includes("user.text")) continue;
254
+
255
+ const text = textFromContent(payload.content);
256
+ if (!text) continue;
257
+
258
+ if (!result.cwd) {
259
+ const mCwd = text.match(/<cwd>(.*?)<\/cwd>/);
260
+ if (mCwd) result.cwd = mCwd[1];
261
+ }
262
+ if (role === "user" && /^(# AGENTS\.md instructions|<INSTRUCTIONS>|<permissions instructions>|<model_switch>)/i.test(text)) continue;
263
+
264
+ if (role === "user") {
265
+ result.sessionTitle ||= compactText(text, 80);
266
+ result.lastText = text;
267
+ result.lastTextRole = "user";
268
+ result.recentLogs.push({
269
+ timestamp: entry.timestamp || new Date().toISOString(),
270
+ type: "prompt",
271
+ summary: "User Prompt",
272
+ detail: compactText(text, 100),
273
+ });
274
+ } else {
275
+ result.lastText = text;
276
+ result.lastTextRole = "assistant";
277
+ }
278
+ continue;
279
+ }
280
+
281
+ if (type === "function_call" || type === "local_shell_call" || type === "custom_tool_call") {
282
+ let args = {};
283
+ if (typeof payload.arguments === "string") {
284
+ try {
285
+ args = JSON.parse(payload.arguments);
286
+ } catch {
287
+ args = {};
288
+ }
289
+ } else if (payload.arguments && typeof payload.arguments === "object") {
290
+ args = payload.arguments;
291
+ }
292
+
293
+ const name = payload.name || payload.tool || "tool";
294
+ const { type: toolType, detail } = normalizeCodexTool(name, args);
295
+
296
+ result.toolSet.add(toolType || name.toLowerCase());
297
+ result.activeTool = toolType;
298
+ result.currentCommand = detail || name;
299
+
300
+ const entryTime = ts || Date.now();
301
+ result.toolInvocations.push({
302
+ name,
303
+ detail: detail || name,
304
+ time: entryTime,
305
+ });
306
+
307
+ result.recentLogs.push({
308
+ timestamp: entry.timestamp || new Date().toISOString(),
309
+ type: toolType || "tool",
310
+ summary: name,
311
+ detail: detail || name,
312
+ });
313
+ continue;
314
+ }
315
+
316
+ if (type === "function_call_output") {
317
+ const outText = typeof payload.output === "string" ? payload.output : JSON.stringify(payload.output || "");
318
+ if (outText.includes("error:") || outText.includes("failed") || outText.includes("Error:")) {
319
+ result.recentLogs.push({
320
+ timestamp: entry.timestamp || new Date().toISOString(),
321
+ type: "error",
322
+ summary: "Tool Error",
323
+ detail: compactText(outText, 100),
324
+ });
325
+ }
326
+ }
327
+ continue;
328
+ }
329
+
330
+ if (entry.type === "event_msg") {
331
+ if (payload.type === "token_count") {
332
+ const usage = payload.info?.total_token_usage;
333
+ if (usage && Number.isFinite(usage.total_tokens)) {
334
+ result.tokens = usage;
335
+ }
336
+ continue;
337
+ }
338
+ if (payload.type === "task_started") {
339
+ result.openTurn = true;
340
+ result.error = null;
341
+ continue;
342
+ }
343
+ if (payload.type === "task_complete") {
344
+ result.openTurn = false;
345
+ if (payload.error?.message) {
346
+ result.error = String(payload.error.message).slice(0, 200);
347
+ result.recentLogs.push({
348
+ timestamp: entry.timestamp || new Date().toISOString(),
349
+ type: "error",
350
+ summary: "Task Complete Error",
351
+ detail: result.error,
352
+ });
353
+ }
354
+ }
355
+ }
356
+ }
357
+
358
+ // Loop detection: 5 consecutive identical tool calls within 60s
359
+ if (result.toolInvocations.length >= 5) {
360
+ const lastN = result.toolInvocations.slice(-10);
361
+ for (let s = 0; s <= lastN.length - 5; s++) {
362
+ const window = lastN.slice(s, s + 5);
363
+ const targetKey = `${window[0].name}:${window[0].detail}`;
364
+ const sameCall = window.every((w) => `${w.name}:${w.detail}` === targetKey);
365
+ const timeSpan = (window[window.length - 1].time || 0) - (window[0].time || 0);
366
+ if (sameCall && (timeSpan <= 60000 || timeSpan === 0)) {
367
+ result.isLooping = true;
368
+ break;
369
+ }
370
+ }
371
+ }
372
+
373
+ // Cap recent logs to max 25 entries for performance
374
+ if (result.recentLogs.length > 25) {
375
+ result.recentLogs = result.recentLogs.slice(-25);
376
+ }
377
+
378
+ return result;
379
+ }
380
+
381
+ export async function getCodexTraces(maxAgeMs = 24 * 60 * 60 * 1000) {
382
+ const now = Date.now();
383
+ const sessionsRoot = path.join(os.homedir(), ".codex", "sessions");
384
+ const traces = [];
385
+
386
+ let yearDirs;
387
+ try {
388
+ yearDirs = await fs.readdir(sessionsRoot, { withFileTypes: true });
389
+ } catch {
390
+ return traces;
391
+ }
392
+
393
+ const candidateFiles = await scanCodexRolloutFiles(sessionsRoot, maxAgeMs, now);
394
+ const runningProcs = await getRunningCodexProcesses();
395
+ const hasRunningCodex = runningProcs.length > 0;
396
+
397
+ // Stat candidate files and sort by mtime descending (most recent first)
398
+ const candidateStats = [];
399
+ for (const filePath of candidateFiles) {
400
+ try {
401
+ const stat = await fs.stat(filePath);
402
+ const ageMs = now - stat.mtimeMs;
403
+ if (ageMs > maxAgeMs) continue;
404
+ candidateStats.push({ filePath, stat, ageMs, mtime: stat.mtimeMs });
405
+ } catch {}
406
+ }
407
+
408
+ candidateStats.sort((a, b) => b.mtime - a.mtime);
409
+ const toProcess = candidateStats.slice(0, 100);
410
+
411
+ for (const { filePath, stat, ageMs } of toProcess) {
412
+ try {
413
+ const parsed = await parseCodexRollout(filePath);
414
+ // Skip files that carry no cwd, model, tokens, or title
415
+ if (!parsed.cwd && !parsed.model && !parsed.tokens && !parsed.sessionTitle) continue;
416
+
417
+ const rawBase = path.basename(filePath, ".jsonl");
418
+ const cleanBase = rawBase.replace(/^rollout-[\d-T]+-/, "");
419
+ const sessionId = parsed.meta?.session_id || parsed.meta?.id || cleanBase;
420
+ const { clientType, label } = classifyCodexSession(parsed.meta?.originator, parsed.meta?.source);
421
+ const workspace = extractWorkspace(parsed.cwd) || label;
422
+
423
+ let state = "done";
424
+ if (parsed.isLooping) {
425
+ state = "streaming";
426
+ } else if (parsed.openTurn) {
427
+ state = "streaming";
428
+ } else if (parsed.error && ageMs > 15 * 60 * 1000) {
429
+ state = "error";
430
+ } else if (hasRunningCodex && ageMs < 15 * 60 * 1000) {
431
+ state = ageMs < 45_000 ? "streaming" : "idle";
432
+ } else if (ageMs < 60_000) {
433
+ state = "idle";
434
+ } else if (parsed.error) {
435
+ state = "error";
436
+ }
437
+
438
+ const usage = parsed.tokens;
439
+ const elapsedMs =
440
+ parsed.startedAt && parsed.lastTimestamp
441
+ ? Math.max(0, parsed.lastTimestamp - parsed.startedAt)
442
+ : null;
443
+
444
+ traces.push(normalizeTrace({
445
+ traceId: `codex-${sessionId.slice(0, 8)}`,
446
+ cli: "codex",
447
+ clientType,
448
+ source: clientType,
449
+ connectionId: `${label} (${sessionId.slice(0, 6)})`,
450
+ account: workspace,
451
+ model: parsed.model || null,
452
+ provider: "openai (codex)",
453
+ state,
454
+ startedAt: parsed.startedAt,
455
+ elapsedMs,
456
+ tokens: usage
457
+ ? {
458
+ input: usage.input_tokens ?? null,
459
+ output: usage.output_tokens ?? null,
460
+ cached: usage.cached_input_tokens ?? null,
461
+ }
462
+ : { input: null, output: null, cached: null },
463
+ cost: null,
464
+ status: parsed.error ? "error" : null,
465
+ tools: Array.from(parsed.toolSet),
466
+ activeTool: parsed.activeTool || (parsed.toolSet.size > 0 ? Array.from(parsed.toolSet)[0] : null),
467
+ currentCommand: parsed.currentCommand || null,
468
+ sessionTitle: parsed.sessionTitle || workspace,
469
+ lastText: parsed.lastText,
470
+ lastTextRole: parsed.lastTextRole,
471
+ error: parsed.error,
472
+ isLooping: parsed.isLooping,
473
+ logs: parsed.recentLogs,
474
+ }));
475
+ } catch {
476
+ // unreadable rollout
477
+ }
478
+ }
479
+
480
+ traces.sort((a, b) => (b.startedAt || 0) - (a.startedAt || 0));
481
+ return traces;
482
+ }