@doxbrix/doxloop 0.1.5 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (357) hide show
  1. package/CHANGELOG.md +1145 -0
  2. package/README.md +422 -227
  3. package/assets/doxbrix-preview.css +77 -15
  4. package/contracts/agent-events-v1.schema.json +1 -0
  5. package/contracts/coverage-v1.schema.json +59 -0
  6. package/contracts/drift-v1.schema.json +1 -0
  7. package/contracts/evaluation-v1.schema.json +62 -0
  8. package/contracts/quality-codes-v1.json +1 -0
  9. package/contracts/quality-report-v1.schema.json +1 -0
  10. package/contracts/validation-v1.schema.json +1 -0
  11. package/dist/agent-failure.d.ts +36 -0
  12. package/dist/agent-failure.js +98 -0
  13. package/dist/agent-isolation.d.ts +17 -0
  14. package/dist/agent-isolation.js +94 -0
  15. package/dist/agent-log.d.ts +197 -0
  16. package/dist/agent-log.js +1094 -0
  17. package/dist/agent-process.d.ts +63 -0
  18. package/dist/agent-process.js +128 -0
  19. package/dist/agent-reply.d.ts +47 -0
  20. package/dist/agent-reply.js +315 -0
  21. package/dist/agents.d.ts +24 -5
  22. package/dist/agents.js +60 -9
  23. package/dist/api-coverage.d.ts +11 -0
  24. package/dist/api-coverage.js +72 -0
  25. package/dist/application-probe.d.ts +28 -0
  26. package/dist/application-probe.js +93 -0
  27. package/dist/application-url.d.ts +14 -0
  28. package/dist/application-url.js +26 -0
  29. package/dist/args.js +15 -0
  30. package/dist/artifact-deploy.d.ts +9 -0
  31. package/dist/artifact-deploy.js +28 -8
  32. package/dist/assets.d.ts +72 -0
  33. package/dist/assets.js +382 -0
  34. package/dist/auth.d.ts +10 -0
  35. package/dist/auth.js +38 -9
  36. package/dist/author.d.ts +206 -4
  37. package/dist/author.js +1650 -52
  38. package/dist/authoring-batches.d.ts +190 -0
  39. package/dist/authoring-batches.js +489 -0
  40. package/dist/authoring-postpass.d.ts +79 -0
  41. package/dist/authoring-postpass.js +1289 -0
  42. package/dist/authoring-progress.d.ts +95 -0
  43. package/dist/authoring-progress.js +269 -0
  44. package/dist/autosync.d.ts +56 -0
  45. package/dist/autosync.js +442 -0
  46. package/dist/batch-artifacts.d.ts +81 -0
  47. package/dist/batch-artifacts.js +412 -0
  48. package/dist/batch-limits.d.ts +37 -0
  49. package/dist/batch-limits.js +80 -0
  50. package/dist/branding.d.ts +41 -0
  51. package/dist/branding.js +229 -0
  52. package/dist/bundle-upload.d.ts +26 -0
  53. package/dist/bundle-upload.js +99 -0
  54. package/dist/capture-auth.d.ts +95 -0
  55. package/dist/capture-auth.js +204 -0
  56. package/dist/capture.d.ts +48 -0
  57. package/dist/capture.js +20 -1
  58. package/dist/cli.js +839 -32
  59. package/dist/content-links.d.ts +10 -0
  60. package/dist/content-links.js +49 -0
  61. package/dist/contract-validation.d.ts +4 -0
  62. package/dist/contract-validation.js +25 -0
  63. package/dist/coverage-actions.d.ts +8 -0
  64. package/dist/coverage-actions.js +71 -0
  65. package/dist/coverage-resolutions.d.ts +16 -0
  66. package/dist/coverage-resolutions.js +58 -0
  67. package/dist/db.d.ts +37 -0
  68. package/dist/db.js +288 -0
  69. package/dist/demo.d.ts +12 -0
  70. package/dist/demo.js +122 -0
  71. package/dist/deploy-credentials.d.ts +7 -0
  72. package/dist/deploy-credentials.js +76 -0
  73. package/dist/deploy-targets/github-pages.d.ts +3 -0
  74. package/dist/deploy-targets/github-pages.js +100 -0
  75. package/dist/deploy-targets/index.d.ts +8 -0
  76. package/dist/deploy-targets/index.js +78 -0
  77. package/dist/deploy-targets/netlify.d.ts +3 -0
  78. package/dist/deploy-targets/netlify.js +49 -0
  79. package/dist/deploy-targets/types.d.ts +32 -0
  80. package/dist/deploy-targets/types.js +2 -0
  81. package/dist/deploy-targets/vercel.d.ts +3 -0
  82. package/dist/deploy-targets/vercel.js +95 -0
  83. package/dist/deploy.d.ts +9 -0
  84. package/dist/deploy.js +87 -13
  85. package/dist/deterministic-capture.d.ts +90 -0
  86. package/dist/deterministic-capture.js +435 -0
  87. package/dist/direct-edit.d.ts +40 -0
  88. package/dist/direct-edit.js +159 -0
  89. package/dist/docs-crawl.d.ts +86 -0
  90. package/dist/docs-crawl.js +536 -0
  91. package/dist/docs-site.d.ts +49 -0
  92. package/dist/docs-site.js +233 -0
  93. package/dist/doctor.js +8 -0
  94. package/dist/documentation-collections.d.ts +23 -0
  95. package/dist/documentation-collections.js +191 -0
  96. package/dist/documentation-plan.d.ts +253 -0
  97. package/dist/documentation-plan.js +2563 -0
  98. package/dist/doxbrix-build.d.ts +19 -0
  99. package/dist/doxbrix-build.js +167 -0
  100. package/dist/doxbrix-markdown.d.ts +9 -0
  101. package/dist/doxbrix-markdown.js +115 -14
  102. package/dist/drift.d.ts +10 -0
  103. package/dist/drift.js +164 -0
  104. package/dist/evaluation.d.ts +46 -0
  105. package/dist/evaluation.js +113 -0
  106. package/dist/evidence-pack.d.ts +47 -0
  107. package/dist/evidence-pack.js +358 -0
  108. package/dist/evidence.d.ts +25 -0
  109. package/dist/evidence.js +175 -0
  110. package/dist/fs.d.ts +8 -2
  111. package/dist/fs.js +41 -11
  112. package/dist/generator-api.d.ts +89 -0
  113. package/dist/generator-preflight.d.ts +27 -0
  114. package/dist/generator-preflight.js +105 -0
  115. package/dist/generator-runtime.d.ts +7 -0
  116. package/dist/generator-runtime.js +17 -1
  117. package/dist/generators.d.ts +24 -2
  118. package/dist/generators.js +54 -1
  119. package/dist/git-delivery.d.ts +17 -0
  120. package/dist/git-delivery.js +123 -0
  121. package/dist/globs.d.ts +16 -0
  122. package/dist/globs.js +65 -0
  123. package/dist/glossary.d.ts +26 -0
  124. package/dist/glossary.js +179 -0
  125. package/dist/history.d.ts +106 -0
  126. package/dist/history.js +600 -0
  127. package/dist/html-markdown.d.ts +46 -0
  128. package/dist/html-markdown.js +423 -0
  129. package/dist/interactive.js +3 -0
  130. package/dist/job-events.d.ts +74 -0
  131. package/dist/job-events.js +377 -0
  132. package/dist/keep-awake.d.ts +50 -0
  133. package/dist/keep-awake.js +123 -0
  134. package/dist/local-source-snapshot.d.ts +20 -0
  135. package/dist/local-source-snapshot.js +61 -0
  136. package/dist/mintlify-detect.d.ts +3 -0
  137. package/dist/mintlify-detect.js +18 -0
  138. package/dist/mintlify-import.d.ts +75 -0
  139. package/dist/mintlify-import.js +190 -0
  140. package/dist/navigation.d.ts +98 -0
  141. package/dist/navigation.js +310 -0
  142. package/dist/openapi.d.ts +60 -0
  143. package/dist/openapi.js +439 -0
  144. package/dist/page-editor-bridge.d.ts +3 -0
  145. package/dist/page-editor-bridge.js +109 -0
  146. package/dist/page-editor-preview.d.ts +10 -0
  147. package/dist/page-editor-preview.js +55 -0
  148. package/dist/page-extension.d.ts +9 -0
  149. package/dist/page-extension.js +15 -0
  150. package/dist/page-metadata.d.ts +28 -0
  151. package/dist/page-metadata.js +166 -0
  152. package/dist/page-operations.d.ts +34 -0
  153. package/dist/page-operations.js +215 -0
  154. package/dist/page-routes.d.ts +4 -0
  155. package/dist/page-routes.js +61 -0
  156. package/dist/pages.d.ts +20 -0
  157. package/dist/pages.js +184 -0
  158. package/dist/plan-generator.d.ts +3 -0
  159. package/dist/plan-generator.js +21 -0
  160. package/dist/plan-navigation.d.ts +11 -0
  161. package/dist/plan-navigation.js +30 -0
  162. package/dist/planning-captures.d.ts +20 -0
  163. package/dist/planning-captures.js +143 -0
  164. package/dist/planning-research.d.ts +135 -0
  165. package/dist/planning-research.js +472 -0
  166. package/dist/planning-triage.d.ts +23 -0
  167. package/dist/planning-triage.js +131 -0
  168. package/dist/preview.d.ts +24 -0
  169. package/dist/preview.js +280 -29
  170. package/dist/project-detect.d.ts +36 -0
  171. package/dist/project-detect.js +251 -0
  172. package/dist/project-import.d.ts +54 -0
  173. package/dist/project-import.js +157 -0
  174. package/dist/project-lock.d.ts +6 -0
  175. package/dist/project-lock.js +96 -0
  176. package/dist/project-registry.d.ts +25 -0
  177. package/dist/project-registry.js +79 -0
  178. package/dist/project.d.ts +35 -3
  179. package/dist/project.js +388 -38
  180. package/dist/prompts.d.ts +9 -0
  181. package/dist/prompts.js +25 -0
  182. package/dist/proposal-replay.d.ts +32 -0
  183. package/dist/proposal-replay.js +99 -0
  184. package/dist/quality-claims.d.ts +8 -0
  185. package/dist/quality-claims.js +168 -0
  186. package/dist/quality-config.d.ts +5 -0
  187. package/dist/quality-config.js +84 -0
  188. package/dist/quality-contract.d.ts +37 -0
  189. package/dist/quality-contract.js +46 -0
  190. package/dist/quality-examples.d.ts +4 -0
  191. package/dist/quality-examples.js +233 -0
  192. package/dist/quality-gates.d.ts +16 -0
  193. package/dist/quality-gates.js +192 -0
  194. package/dist/quality-links.d.ts +7 -0
  195. package/dist/quality-links.js +149 -0
  196. package/dist/quality-lint.d.ts +6 -0
  197. package/dist/quality-lint.js +124 -0
  198. package/dist/quality-rendered.d.ts +11 -0
  199. package/dist/quality-rendered.js +222 -0
  200. package/dist/quality-schema.d.ts +3 -0
  201. package/dist/quality-schema.js +71 -0
  202. package/dist/release-notes.d.ts +44 -0
  203. package/dist/release-notes.js +183 -0
  204. package/dist/remote-monitor.d.ts +16 -0
  205. package/dist/remote-monitor.js +74 -0
  206. package/dist/remote-source.d.ts +34 -0
  207. package/dist/remote-source.js +426 -0
  208. package/dist/review-diff.d.ts +82 -0
  209. package/dist/review-diff.js +400 -0
  210. package/dist/review-learning.d.ts +11 -0
  211. package/dist/review-learning.js +60 -0
  212. package/dist/review-render.d.ts +38 -0
  213. package/dist/review-render.js +224 -0
  214. package/dist/review-report.d.ts +9 -0
  215. package/dist/review-report.js +89 -0
  216. package/dist/review-ui.d.ts +14 -0
  217. package/dist/review-ui.js +1248 -0
  218. package/dist/schedule.d.ts +78 -0
  219. package/dist/schedule.js +480 -0
  220. package/dist/screen-capture-provider.d.ts +52 -0
  221. package/dist/screen-capture-provider.js +218 -0
  222. package/dist/screenshot-workflow.d.ts +167 -0
  223. package/dist/screenshot-workflow.js +1237 -0
  224. package/dist/settings.d.ts +1 -1
  225. package/dist/settings.js +95 -7
  226. package/dist/site-export.d.ts +18 -0
  227. package/dist/site-export.js +87 -0
  228. package/dist/source-connectors.d.ts +33 -0
  229. package/dist/source-connectors.js +268 -0
  230. package/dist/source-discovery.d.ts +132 -0
  231. package/dist/source-discovery.js +823 -0
  232. package/dist/source-intelligence.d.ts +9 -0
  233. package/dist/source-intelligence.js +306 -0
  234. package/dist/sync-review.d.ts +28 -0
  235. package/dist/sync-review.js +264 -0
  236. package/dist/sync-runs.d.ts +192 -0
  237. package/dist/sync-runs.js +2244 -0
  238. package/dist/sync.d.ts +35 -0
  239. package/dist/sync.js +298 -32
  240. package/dist/text-diff.d.ts +9 -0
  241. package/dist/text-diff.js +59 -0
  242. package/dist/types.d.ts +946 -1
  243. package/dist/ui/assets/doxloop-logo-light-De7Nx7j7.png +0 -0
  244. package/dist/ui/assets/index-BHBYU2aG.css +1 -0
  245. package/dist/ui/assets/index-Cq3RPQiC.js +33 -0
  246. package/dist/ui/index.html +18 -0
  247. package/dist/ui-server.d.ts +83 -0
  248. package/dist/ui-server.js +3532 -0
  249. package/dist/usage-budget.d.ts +28 -0
  250. package/dist/usage-budget.js +90 -0
  251. package/dist/validation.d.ts +25 -1
  252. package/dist/validation.js +312 -26
  253. package/dist/workspace-tools.d.ts +54 -0
  254. package/dist/workspace-tools.js +123 -0
  255. package/docs/agent-compatibility.md +49 -32
  256. package/docs/ci-and-automation.md +105 -66
  257. package/docs/doxbrix-http-api.md +8 -1
  258. package/docs/existing-documentation.md +80 -0
  259. package/docs/generation-performance.md +108 -0
  260. package/docs/generator-authoring.md +68 -5
  261. package/docs/generator-selection.md +50 -13
  262. package/docs/mintlify-import.md +71 -0
  263. package/docs/openapi-security.md +25 -0
  264. package/docs/project-format.md +331 -36
  265. package/docs/release-quality.md +158 -0
  266. package/docs/releasing.md +72 -0
  267. package/docs/review-workflows.md +51 -0
  268. package/docs/security-model.md +102 -39
  269. package/docs/troubleshooting.md +226 -93
  270. package/package.json +46 -15
  271. package/scripts/test-auto-screenshot.mjs +172 -0
  272. package/skills/doxloop-authoring/SKILL.md +242 -361
  273. package/skills/doxloop-authoring/references/existing-documentation.md +94 -0
  274. package/skills/doxloop-authoring/references/navigation-architecture.md +18 -7
  275. package/skills/doxloop-authoring/references/page-depth.md +169 -0
  276. package/skills/doxloop-authoring/references/project-format.md +106 -7
  277. package/skills/doxloop-authoring/references/quality.md +10 -0
  278. package/skills/doxloop-authoring/references/screenshot-manifest.md +113 -0
  279. package/skills/doxloop-authoring/references/screenshots.md +149 -235
  280. package/skills/doxloop-authoring/references/workflows.md +76 -0
  281. package/skills/doxloop-doxbrix/SKILL.md +38 -22
  282. package/skills/doxloop-doxbrix/references/api-endpoints.md +15 -14
  283. package/skills/doxloop-doxbrix/references/components.md +30 -3
  284. package/skills/doxloop-doxbrix/references/manifest.md +3 -2
  285. package/vendor/doxbrix-import/LICENSE +202 -0
  286. package/vendor/doxbrix-import/README.md +17 -0
  287. package/vendor/doxbrix-import/UPSTREAM.json +47 -0
  288. package/vendor/doxbrix-import/dist/docs/frontmatter.d.ts +13 -0
  289. package/vendor/doxbrix-import/dist/docs/frontmatter.js +83 -0
  290. package/vendor/doxbrix-import/dist/docs/import.d.ts +21 -0
  291. package/vendor/doxbrix-import/dist/docs/import.js +147 -0
  292. package/vendor/doxbrix-import/dist/docs/manifest.d.ts +163 -0
  293. package/vendor/doxbrix-import/dist/docs/manifest.js +64 -0
  294. package/vendor/doxbrix-import/dist/docs/project.d.ts +25 -0
  295. package/vendor/doxbrix-import/dist/docs/project.js +77 -0
  296. package/vendor/doxbrix-import/dist/docs/starter.d.ts +4 -0
  297. package/vendor/doxbrix-import/dist/docs/starter.js +11 -0
  298. package/vendor/doxbrix-import/dist/importer.d.ts +227 -0
  299. package/vendor/doxbrix-import/dist/importer.js +1567 -0
  300. package/vendor/doxbrix-import/dist/mintlify-openapi.d.ts +37 -0
  301. package/vendor/doxbrix-import/dist/mintlify-openapi.js +305 -0
  302. package/vendor/doxbrix-import/dist/safe-path.d.ts +9 -0
  303. package/vendor/doxbrix-import/dist/safe-path.js +47 -0
  304. package/dist/agents.d.ts.map +0 -1
  305. package/dist/agents.js.map +0 -1
  306. package/dist/args.d.ts.map +0 -1
  307. package/dist/args.js.map +0 -1
  308. package/dist/artifact-deploy.d.ts.map +0 -1
  309. package/dist/artifact-deploy.js.map +0 -1
  310. package/dist/auth.d.ts.map +0 -1
  311. package/dist/auth.js.map +0 -1
  312. package/dist/author.d.ts.map +0 -1
  313. package/dist/author.js.map +0 -1
  314. package/dist/capture.d.ts.map +0 -1
  315. package/dist/capture.js.map +0 -1
  316. package/dist/cli.d.ts.map +0 -1
  317. package/dist/cli.js.map +0 -1
  318. package/dist/deploy.d.ts.map +0 -1
  319. package/dist/deploy.js.map +0 -1
  320. package/dist/deployment-visibility.d.ts.map +0 -1
  321. package/dist/deployment-visibility.js.map +0 -1
  322. package/dist/doctor.d.ts.map +0 -1
  323. package/dist/doctor.js.map +0 -1
  324. package/dist/doxbrix-markdown.d.ts.map +0 -1
  325. package/dist/doxbrix-markdown.js.map +0 -1
  326. package/dist/errors.d.ts.map +0 -1
  327. package/dist/errors.js.map +0 -1
  328. package/dist/fs.d.ts.map +0 -1
  329. package/dist/fs.js.map +0 -1
  330. package/dist/generator-api.d.ts.map +0 -1
  331. package/dist/generator-api.js.map +0 -1
  332. package/dist/generator-manager.d.ts.map +0 -1
  333. package/dist/generator-manager.js.map +0 -1
  334. package/dist/generator-runtime.d.ts.map +0 -1
  335. package/dist/generator-runtime.js.map +0 -1
  336. package/dist/generators.d.ts.map +0 -1
  337. package/dist/generators.js.map +0 -1
  338. package/dist/interactive.d.ts.map +0 -1
  339. package/dist/interactive.js.map +0 -1
  340. package/dist/preview.d.ts.map +0 -1
  341. package/dist/preview.js.map +0 -1
  342. package/dist/progress.d.ts.map +0 -1
  343. package/dist/progress.js.map +0 -1
  344. package/dist/project.d.ts.map +0 -1
  345. package/dist/project.js.map +0 -1
  346. package/dist/prompts.d.ts.map +0 -1
  347. package/dist/prompts.js.map +0 -1
  348. package/dist/settings.d.ts.map +0 -1
  349. package/dist/settings.js.map +0 -1
  350. package/dist/sync.d.ts.map +0 -1
  351. package/dist/sync.js.map +0 -1
  352. package/dist/types.d.ts.map +0 -1
  353. package/dist/types.js.map +0 -1
  354. package/dist/validation.d.ts.map +0 -1
  355. package/dist/validation.js.map +0 -1
  356. package/dist/version.d.ts.map +0 -1
  357. package/dist/version.js.map +0 -1
@@ -0,0 +1,1094 @@
1
+ /** Quiet time before a formatter names what the agent is doing. */
2
+ export const AGENT_LOG_HEARTBEAT_MS = 30_000;
3
+ /** Tool calls faster than this get no duration suffix; the log's timestamps already cover them. */
4
+ const TOOL_DURATION_THRESHOLD_MS = 1_000;
5
+ /**
6
+ * Tracks the phase an agent session is in — waiting for a response, thinking,
7
+ * writing, running a tool — so a quiet stretch can be named. Every line the
8
+ * formatter writes resets the quiet timer; a phase that outlasts the interval
9
+ * produces one heartbeat line per interval.
10
+ */
11
+ class ActivityPulse {
12
+ now;
13
+ intervalMs;
14
+ phase;
15
+ lastLineAt;
16
+ constructor(now, intervalMs) {
17
+ this.now = now;
18
+ this.intervalMs = intervalMs;
19
+ this.lastLineAt = now();
20
+ }
21
+ begin(description, options = {}) {
22
+ this.phase = { description, detail: options.detail, startedAt: options.startedAt ?? this.now() };
23
+ }
24
+ end() {
25
+ this.phase = undefined;
26
+ }
27
+ /** Emitted lines reset the quiet timer; passes them through for chaining. */
28
+ mark(lines) {
29
+ if (lines.length > 0)
30
+ this.lastLineAt = this.now();
31
+ return lines;
32
+ }
33
+ lines(now = this.now()) {
34
+ if (!this.phase || now - this.lastLineAt < this.intervalMs)
35
+ return [];
36
+ this.lastLineAt = now;
37
+ const detail = this.phase.detail?.();
38
+ return [`… ${this.phase.description}${detail ? ` (${detail})` : ''} · ${formatLogDuration(now - this.phase.startedAt)}`];
39
+ }
40
+ }
41
+ /** A finished tool call's duration, shown only once it is long enough to matter. */
42
+ function toolDurationSuffix(startedAt, now) {
43
+ if (startedAt === undefined)
44
+ return '';
45
+ const elapsed = now - startedAt;
46
+ return elapsed >= TOOL_DURATION_THRESHOLD_MS ? ` · ${formatLogDuration(elapsed)}` : '';
47
+ }
48
+ function charactersSoFar(count) {
49
+ return count > 0 ? `${formatTokenCount(count)} characters so far` : undefined;
50
+ }
51
+ export function createAgentLogFormatter(agent, options = {}) {
52
+ if (agent === 'codex')
53
+ return new CodexStreamLogFormatter(options);
54
+ if (agent === 'gemini')
55
+ return new GeminiStreamLogFormatter(options);
56
+ return new ClaudeStreamLogFormatter(options);
57
+ }
58
+ /** Split a chunked stream into complete lines, keeping a partial tail. */
59
+ class LineBuffer {
60
+ buffer = '';
61
+ push(chunk) {
62
+ this.buffer += chunk.toString();
63
+ const lines = this.buffer.split(/\r?\n/);
64
+ this.buffer = lines.pop() ?? '';
65
+ return lines;
66
+ }
67
+ finish() {
68
+ const remaining = this.buffer;
69
+ this.buffer = '';
70
+ return remaining ? [remaining] : [];
71
+ }
72
+ }
73
+ const ZERO_TOKENS = { input: 0, output: 0, cacheRead: 0, cacheCreation: 0 };
74
+ function addTokens(a, b) {
75
+ return {
76
+ input: a.input + b.input,
77
+ output: a.output + b.output,
78
+ cacheRead: a.cacheRead + b.cacheRead,
79
+ cacheCreation: a.cacheCreation + b.cacheCreation,
80
+ };
81
+ }
82
+ /** Input + cache read + cache creation: the context one request was served with. */
83
+ function contextTokens(counts) {
84
+ return counts.input + counts.cacheRead + counts.cacheCreation;
85
+ }
86
+ /**
87
+ * Claude's usage shape (`input_tokens`, `output_tokens`,
88
+ * `cache_read_input_tokens`, `cache_creation_input_tokens`), shared by
89
+ * assistant messages and the result event's cumulative `usage`.
90
+ */
91
+ function claudeTokens(usage) {
92
+ if (!usage)
93
+ return undefined;
94
+ const input = numberField(usage, 'input_tokens');
95
+ const output = numberField(usage, 'output_tokens');
96
+ if (input === undefined && output === undefined)
97
+ return undefined;
98
+ return {
99
+ input: input ?? 0,
100
+ output: output ?? 0,
101
+ cacheRead: numberField(usage, 'cache_read_input_tokens') ?? 0,
102
+ cacheCreation: numberField(usage, 'cache_creation_input_tokens') ?? 0,
103
+ };
104
+ }
105
+ /**
106
+ * Tracks the wall-clock span of a stream so a session that never reports a
107
+ * duration (a crash, a kill) still contributes elapsed time to the usage.
108
+ */
109
+ class StreamClock {
110
+ startedAt;
111
+ lastAt = 0;
112
+ touch() {
113
+ const now = Date.now();
114
+ this.startedAt ??= now;
115
+ this.lastAt = now;
116
+ }
117
+ elapsedMs() {
118
+ return this.startedAt === undefined ? 0 : Math.max(0, this.lastAt - this.startedAt);
119
+ }
120
+ }
121
+ function buildUsage(tokens, fields) {
122
+ return {
123
+ inputTokens: tokens.input,
124
+ outputTokens: tokens.output,
125
+ cacheReadTokens: tokens.cacheRead,
126
+ cacheCreationTokens: tokens.cacheCreation,
127
+ totalTokens: tokens.input + tokens.output + tokens.cacheRead + tokens.cacheCreation,
128
+ ...(fields.costUsd !== undefined ? { costUsd: fields.costUsd } : {}),
129
+ turns: fields.turns,
130
+ sessions: 1,
131
+ maxContextTokens: fields.maxContextTokens,
132
+ durationMs: fields.durationMs,
133
+ };
134
+ }
135
+ /**
136
+ * Combine the usage of several agent sessions (a run that resumed, or the
137
+ * plan and authoring phases of one request) into one total. Counters,
138
+ * sessions, turns, and durations add up; the context high-water mark is the
139
+ * largest seen; cost is present when any part reported one.
140
+ */
141
+ export function mergeAgentUsage(...parts) {
142
+ const present = parts.filter((part) => part !== undefined);
143
+ if (present.length === 0)
144
+ return undefined;
145
+ const merged = {
146
+ inputTokens: 0,
147
+ outputTokens: 0,
148
+ cacheReadTokens: 0,
149
+ cacheCreationTokens: 0,
150
+ totalTokens: 0,
151
+ turns: 0,
152
+ sessions: 0,
153
+ maxContextTokens: 0,
154
+ durationMs: 0,
155
+ };
156
+ let cost;
157
+ for (const part of present) {
158
+ merged.inputTokens += part.inputTokens;
159
+ merged.outputTokens += part.outputTokens;
160
+ merged.cacheReadTokens += part.cacheReadTokens;
161
+ merged.cacheCreationTokens += part.cacheCreationTokens;
162
+ merged.totalTokens += part.totalTokens;
163
+ merged.turns += part.turns;
164
+ merged.sessions += part.sessions;
165
+ merged.maxContextTokens = Math.max(merged.maxContextTokens, part.maxContextTokens);
166
+ merged.durationMs += part.durationMs;
167
+ if (part.costUsd !== undefined)
168
+ cost = (cost ?? 0) + part.costUsd;
169
+ }
170
+ if (cost !== undefined)
171
+ merged.costUsd = cost;
172
+ return merged;
173
+ }
174
+ /** `1234` → `1.2k`, `1_234_567` → `1.2M`; whole thousands drop the `.0`. */
175
+ export function formatTokenCount(count) {
176
+ const scaled = (value, suffix) => {
177
+ const text = value.toFixed(1);
178
+ return `${text.endsWith('.0') ? text.slice(0, -2) : text}${suffix}`;
179
+ };
180
+ if (count >= 1_000_000)
181
+ return scaled(count / 1_000_000, 'M');
182
+ if (count >= 1_000)
183
+ return scaled(count / 1_000, 'k');
184
+ return String(Math.round(count));
185
+ }
186
+ /** Share of the context the model read from cache, as a whole percentage, or undefined when nothing was cached. */
187
+ function cachedPercent(usage) {
188
+ if (usage.cacheReadTokens === 0 && usage.cacheCreationTokens === 0)
189
+ return undefined;
190
+ const context = usage.inputTokens + usage.cacheReadTokens + usage.cacheCreationTokens;
191
+ return context > 0 ? Math.round((usage.cacheReadTokens / context) * 100) : undefined;
192
+ }
193
+ function formatUsageCost(cost) {
194
+ return cost > 0 && cost < 0.01 ? '<$0.01' : `$${cost.toFixed(2)}`;
195
+ }
196
+ /**
197
+ * One line for the UI: `1.2M tokens · 98% cached · $1.42 · 3 sessions`. Cost
198
+ * is omitted when the agent never reported one; sessions when there was one.
199
+ */
200
+ export function formatAgentUsage(usage) {
201
+ const parts = [`${formatTokenCount(usage.totalTokens)} tokens`];
202
+ const cached = cachedPercent(usage);
203
+ if (cached !== undefined)
204
+ parts.push(`${cached}% cached`);
205
+ if (usage.costUsd !== undefined)
206
+ parts.push(formatUsageCost(usage.costUsd));
207
+ if (usage.sessions !== 1)
208
+ parts.push(`${usage.sessions} sessions`);
209
+ return parts.join(' · ');
210
+ }
211
+ /** The usage tail of a finished/stopped summary line: `1.2M tokens (98% cached) · $1.42`. */
212
+ function usageSummary(usage) {
213
+ if (!usage || usage.totalTokens === 0)
214
+ return '';
215
+ const cached = cachedPercent(usage);
216
+ const cost = usage.costUsd !== undefined ? ` · ${formatUsageCost(usage.costUsd)}` : '';
217
+ return ` · ${formatTokenCount(usage.totalTokens)} tokens${cached !== undefined ? ` (${cached}% cached)` : ''}${cost}`;
218
+ }
219
+ /** Convert Claude Code's JSONL stream into concise, durable UI log lines. */
220
+ export class ClaudeStreamLogFormatter {
221
+ buffer = '';
222
+ blocks = new Map();
223
+ tools = new Map();
224
+ now;
225
+ pulse;
226
+ streamedContent = false;
227
+ lastText = '';
228
+ /** Why Claude stopped without a successful result, once its result event arrives. */
229
+ stopReason;
230
+ /** True when the stop was a transient API failure rather than a task outcome. */
231
+ transientFailure = false;
232
+ /** Claude's session id from its init event, so a cut-off session can be resumed. */
233
+ sessionId;
234
+ /** Called for every tool call Claude makes, with the parsed input when it is readable. */
235
+ onToolCall;
236
+ /**
237
+ * Usage per assistant message id. With `--include-partial-messages` the same
238
+ * assistant message is emitted once per content block, each carrying that
239
+ * message's usage, so the last report per id wins instead of adding up.
240
+ */
241
+ messageUsage = new Map();
242
+ /** Assistant messages that carried usage but no id; nothing to de-duplicate on. */
243
+ anonymousUsage = ZERO_TOKENS;
244
+ maxContextTokens = 0;
245
+ /** The result event's cumulative usage, preferred over the running sum once it arrives. */
246
+ resultTokens;
247
+ resultTurns;
248
+ resultDurationMs;
249
+ resultCostUsd;
250
+ clock = new StreamClock();
251
+ constructor(options = {}) {
252
+ this.now = options.now ?? Date.now;
253
+ this.pulse = new ActivityPulse(this.now, options.heartbeatMs ?? AGENT_LOG_HEARTBEAT_MS);
254
+ }
255
+ heartbeat(now = this.now()) {
256
+ return this.pulse.lines(now);
257
+ }
258
+ get usage() {
259
+ const seen = this.messageUsage.size > 0 || contextTokens(this.anonymousUsage) + this.anonymousUsage.output > 0;
260
+ if (!seen && !this.resultTokens && this.resultTurns === undefined)
261
+ return undefined;
262
+ let running = this.anonymousUsage;
263
+ for (const counts of this.messageUsage.values())
264
+ running = addTokens(running, counts);
265
+ return buildUsage(this.resultTokens ?? running, {
266
+ turns: this.resultTurns ?? this.messageUsage.size,
267
+ maxContextTokens: this.maxContextTokens,
268
+ durationMs: this.resultDurationMs ?? this.clock.elapsedMs(),
269
+ costUsd: this.resultCostUsd,
270
+ });
271
+ }
272
+ push(chunk) {
273
+ this.buffer += chunk.toString();
274
+ const lines = this.buffer.split(/\r?\n/);
275
+ this.buffer = lines.pop() ?? '';
276
+ const written = this.pulse.mark(lines.flatMap((line) => this.formatLine(line)));
277
+ // A stream busy with thinking or a long reply prints nothing for minutes;
278
+ // the deltas still arrive, so the heartbeat is checked here as well.
279
+ return [...written, ...this.pulse.lines()];
280
+ }
281
+ finish() {
282
+ const remaining = this.buffer;
283
+ this.buffer = '';
284
+ return this.pulse.mark(remaining ? this.formatLine(remaining) : []);
285
+ }
286
+ formatLine(line) {
287
+ if (!line.trim())
288
+ return [];
289
+ let message;
290
+ try {
291
+ const parsed = JSON.parse(line);
292
+ const record = jsonRecord(parsed);
293
+ if (!record)
294
+ return [line];
295
+ message = record;
296
+ }
297
+ catch {
298
+ return [line];
299
+ }
300
+ const type = stringField(message, 'type');
301
+ this.clock.touch();
302
+ if (type === 'stream_event')
303
+ return this.formatStreamEvent(jsonRecord(message.event));
304
+ if (type === 'assistant')
305
+ this.recordAssistantUsage(jsonRecord(message.message));
306
+ if (type === 'assistant' && !this.streamedContent) {
307
+ return this.formatAssistantMessage(jsonRecord(message.message));
308
+ }
309
+ if (type === 'user')
310
+ return this.formatToolResults(jsonRecord(message.message));
311
+ if (type === 'result')
312
+ return this.formatResult(message);
313
+ if (type === 'system')
314
+ return this.formatSystem(message);
315
+ return [];
316
+ }
317
+ formatStreamEvent(event) {
318
+ if (!event)
319
+ return [];
320
+ const eventType = stringField(event, 'type');
321
+ if (eventType === 'message_start') {
322
+ this.blocks.clear();
323
+ return [];
324
+ }
325
+ const index = numberField(event, 'index');
326
+ if (index === undefined)
327
+ return [];
328
+ if (eventType === 'content_block_start') {
329
+ const content = jsonRecord(event.content_block);
330
+ if (!content)
331
+ return [];
332
+ const contentType = stringField(content, 'type');
333
+ if (contentType === 'text') {
334
+ const block = { type: 'text', text: stringField(content, 'text') ?? '' };
335
+ this.blocks.set(index, block);
336
+ this.streamedContent = true;
337
+ this.pulse.begin('Claude is writing its reply', { detail: () => charactersSoFar(block.text.length) });
338
+ }
339
+ else if (contentType === 'tool_use') {
340
+ const name = stringField(content, 'name') ?? 'tool';
341
+ const block = {
342
+ type: 'tool',
343
+ ...(stringField(content, 'id') ? { id: stringField(content, 'id') } : {}),
344
+ name,
345
+ json: '',
346
+ ...(jsonRecord(content.input) ? { input: jsonRecord(content.input) } : {}),
347
+ };
348
+ this.blocks.set(index, block);
349
+ this.streamedContent = true;
350
+ this.pulse.begin(`Claude is preparing a ${name} call`, { detail: () => charactersSoFar(block.json.length) });
351
+ }
352
+ else if (contentType === 'thinking' || contentType === 'redacted_thinking') {
353
+ const block = { type: 'thinking', characters: 0 };
354
+ this.blocks.set(index, block);
355
+ this.pulse.begin('Claude is thinking', { detail: () => charactersSoFar(block.characters) });
356
+ }
357
+ return [];
358
+ }
359
+ if (eventType === 'content_block_delta') {
360
+ const block = this.blocks.get(index);
361
+ const delta = jsonRecord(event.delta);
362
+ if (!block || !delta)
363
+ return [];
364
+ if (block.type === 'text' && stringField(delta, 'type') === 'text_delta') {
365
+ block.text += stringField(delta, 'text') ?? '';
366
+ }
367
+ else if (block.type === 'tool' && stringField(delta, 'type') === 'input_json_delta') {
368
+ block.json += stringField(delta, 'partial_json') ?? '';
369
+ }
370
+ else if (block.type === 'thinking' && stringField(delta, 'type') === 'thinking_delta') {
371
+ block.characters += (stringField(delta, 'thinking') ?? '').length;
372
+ }
373
+ return [];
374
+ }
375
+ if (eventType !== 'content_block_stop')
376
+ return [];
377
+ const block = this.blocks.get(index);
378
+ this.blocks.delete(index);
379
+ if (!block)
380
+ return [];
381
+ if (block.type === 'thinking') {
382
+ this.awaitNextStep();
383
+ return [];
384
+ }
385
+ if (block.type === 'text') {
386
+ this.awaitNextStep();
387
+ return this.textLines(block.text);
388
+ }
389
+ let input = block.input ?? {};
390
+ if (block.json) {
391
+ try {
392
+ input = jsonRecord(JSON.parse(block.json)) ?? input;
393
+ }
394
+ catch {
395
+ // A malformed partial tool input still has a useful tool name.
396
+ }
397
+ }
398
+ const activity = formatClaudeToolActivity(block.name, input);
399
+ this.beginToolCall(block.id, activity);
400
+ this.onToolCall?.(block.name, input);
401
+ return [`→ ${activity}`];
402
+ }
403
+ /** Claude Code runs the tool as soon as its call is complete; the result arrives as a user message. */
404
+ beginToolCall(id, activity) {
405
+ const startedAt = this.now();
406
+ if (id)
407
+ this.tools.set(id, { activity, startedAt });
408
+ this.pulse.begin(`waiting for ${activity} to finish`, { startedAt });
409
+ }
410
+ /** A block or tool result just ended: the next event is either another tool result or Claude's own. */
411
+ awaitNextStep() {
412
+ const pending = [...this.tools.values()].at(-1);
413
+ if (pending)
414
+ this.pulse.begin(`waiting for ${pending.activity} to finish`, { startedAt: pending.startedAt });
415
+ else
416
+ this.pulse.begin("waiting for Claude's next step");
417
+ }
418
+ recordAssistantUsage(message) {
419
+ const counts = message ? claudeTokens(jsonRecord(message.usage)) : undefined;
420
+ if (!counts)
421
+ return;
422
+ this.maxContextTokens = Math.max(this.maxContextTokens, contextTokens(counts));
423
+ const id = message ? stringField(message, 'id') : undefined;
424
+ if (id)
425
+ this.messageUsage.set(id, counts);
426
+ else
427
+ this.anonymousUsage = addTokens(this.anonymousUsage, counts);
428
+ }
429
+ /**
430
+ * The result event's totals: its `usage` object when present, else the sum
431
+ * of its per-model `modelUsage` map, else nothing (the running sum stands).
432
+ */
433
+ recordResultUsage(message) {
434
+ this.resultTurns = numberField(message, 'num_turns') ?? this.resultTurns;
435
+ this.resultDurationMs = numberField(message, 'duration_ms') ?? this.resultDurationMs;
436
+ this.resultCostUsd = numberField(message, 'total_cost_usd') ?? this.resultCostUsd;
437
+ const totals = claudeTokens(jsonRecord(message.usage));
438
+ if (totals) {
439
+ this.resultTokens = totals;
440
+ return;
441
+ }
442
+ const perModel = jsonRecord(message.modelUsage);
443
+ if (!perModel)
444
+ return;
445
+ let summed;
446
+ let cost;
447
+ for (const entry of Object.values(perModel)) {
448
+ const record = jsonRecord(entry);
449
+ if (!record)
450
+ continue;
451
+ summed = addTokens(summed ?? ZERO_TOKENS, {
452
+ input: numberField(record, 'inputTokens') ?? 0,
453
+ output: numberField(record, 'outputTokens') ?? 0,
454
+ cacheRead: numberField(record, 'cacheReadInputTokens') ?? 0,
455
+ cacheCreation: numberField(record, 'cacheCreationInputTokens') ?? 0,
456
+ });
457
+ const modelCost = numberField(record, 'costUSD');
458
+ if (modelCost !== undefined)
459
+ cost = (cost ?? 0) + modelCost;
460
+ }
461
+ if (summed)
462
+ this.resultTokens = summed;
463
+ if (this.resultCostUsd === undefined && cost !== undefined)
464
+ this.resultCostUsd = cost;
465
+ }
466
+ formatAssistantMessage(message) {
467
+ const content = message?.content;
468
+ if (!Array.isArray(content))
469
+ return [];
470
+ const lines = [];
471
+ for (const rawBlock of content) {
472
+ const block = jsonRecord(rawBlock);
473
+ if (!block)
474
+ continue;
475
+ const type = stringField(block, 'type');
476
+ if (type === 'text') {
477
+ lines.push(...this.textLines(stringField(block, 'text') ?? ''));
478
+ }
479
+ else if (type === 'tool_use') {
480
+ const name = stringField(block, 'name') ?? 'tool';
481
+ const input = jsonRecord(block.input) ?? {};
482
+ const activity = formatClaudeToolActivity(name, input);
483
+ this.beginToolCall(stringField(block, 'id'), activity);
484
+ this.onToolCall?.(name, input);
485
+ lines.push(`→ ${activity}`);
486
+ }
487
+ }
488
+ return lines;
489
+ }
490
+ formatToolResults(message) {
491
+ const content = message?.content;
492
+ if (!Array.isArray(content))
493
+ return [];
494
+ const lines = [];
495
+ for (const rawBlock of content) {
496
+ const block = jsonRecord(rawBlock);
497
+ if (!block || stringField(block, 'type') !== 'tool_result')
498
+ continue;
499
+ const id = stringField(block, 'tool_use_id');
500
+ const pending = id ? this.tools.get(id) : undefined;
501
+ if (id)
502
+ this.tools.delete(id);
503
+ const activity = pending?.activity ?? 'Tool action';
504
+ const duration = toolDurationSuffix(pending?.startedAt, this.now());
505
+ if (block.is_error === true) {
506
+ const detail = compactClaudeValue(block.content);
507
+ lines.push(`✗ ${activity} failed${detail ? `: ${detail}` : ''}${duration}`);
508
+ }
509
+ else {
510
+ lines.push(`✓ ${activity}${duration}`);
511
+ }
512
+ }
513
+ this.awaitNextStep();
514
+ return lines;
515
+ }
516
+ formatSystem(message) {
517
+ const subtype = stringField(message, 'subtype');
518
+ if (subtype === 'init') {
519
+ const model = stringField(message, 'model');
520
+ this.sessionId = stringField(message, 'session_id') ?? this.sessionId;
521
+ this.pulse.begin("waiting for Claude's first response");
522
+ return [`Claude session started${model ? ` · ${model}` : ''}`];
523
+ }
524
+ if (subtype === 'api_retry') {
525
+ const attempt = numberField(message, 'attempt');
526
+ const maximum = numberField(message, 'max_retries');
527
+ const delay = numberField(message, 'retry_delay_ms');
528
+ return [
529
+ `Claude API retry${attempt ? ` ${attempt}${maximum ? `/${maximum}` : ''}` : ''}${delay ? ` in ${Math.ceil(delay / 1000)}s` : ''}`,
530
+ ];
531
+ }
532
+ if (subtype === 'compact_boundary')
533
+ return ['Claude compacted its working context.'];
534
+ return [];
535
+ }
536
+ formatResult(message) {
537
+ const subtype = stringField(message, 'subtype');
538
+ const success = subtype === 'success' && message.is_error !== true;
539
+ const turns = numberField(message, 'num_turns');
540
+ const duration = numberField(message, 'duration_ms');
541
+ const result = stringField(message, 'result');
542
+ this.recordResultUsage(message);
543
+ this.pulse.end();
544
+ // A result event that is an error but has no `errors` list carries the
545
+ // failure in its text (or in the last assistant text), such as
546
+ // "API Error: Server error mid-response".
547
+ if (success) {
548
+ this.stopReason = undefined;
549
+ this.transientFailure = false;
550
+ }
551
+ const stop = success ? undefined : claudeStopReason(subtype, turns, message, result ?? this.lastText);
552
+ if (stop) {
553
+ this.stopReason = stop.reason;
554
+ this.transientFailure = stop.transient;
555
+ }
556
+ const reason = stop?.reason;
557
+ const summary = `Claude ${success ? 'finished' : 'stopped'}${turns !== undefined ? ` · ${turns} turns` : ''}${duration !== undefined ? ` · ${formatLogDuration(duration)}` : ''}${usageSummary(this.usage)}${reason ? ` · ${reason}` : ''}`;
558
+ return [...(result ? this.textLines(result) : []), summary];
559
+ }
560
+ textLines(text) {
561
+ const normalized = text.trim();
562
+ if (!normalized || normalized === this.lastText)
563
+ return [];
564
+ this.lastText = normalized;
565
+ return normalized.split(/\r?\n/).filter(Boolean);
566
+ }
567
+ }
568
+ /**
569
+ * A bare "exited with status 1" hides why a run ended. Claude's result event
570
+ * names the cause, and the turn limit in particular needs to be visible: it
571
+ * looks like an agent failure but is a Doxloop budget. A transient API
572
+ * failure is flagged so the runner can resume the session rather than fail.
573
+ */
574
+ function claudeStopReason(subtype, turns, message, errorText) {
575
+ const reported = Array.isArray(message.errors)
576
+ ? message.errors
577
+ .filter((item) => typeof item === 'string' && item.trim().length > 0)
578
+ .map((item) => compactLogText(item))
579
+ : [];
580
+ if (subtype === 'error_max_turns') {
581
+ return {
582
+ reason: `reached its ${turns !== undefined ? `${turns}-turn` : 'turn'} limit before finishing; retry the stage to continue from the preserved workspace`,
583
+ transient: false,
584
+ };
585
+ }
586
+ if (subtype === 'error_max_budget_usd') {
587
+ return {
588
+ reason: 'reached the configured spending cap before finishing; raise "Maximum Claude spend" under Monitoring → Advanced watch scope and budgets, or retry the stage to continue from the preserved workspace',
589
+ transient: false,
590
+ };
591
+ }
592
+ const detail = reported.length > 0 ? reported.join('; ') : errorText?.trim() ? compactLogText(errorText) : '';
593
+ if (detail && isTransientClaudeApiFailure(detail)) {
594
+ return { reason: `stopped because its API request failed: ${detail}`, transient: true };
595
+ }
596
+ if (detail)
597
+ return { reason: `stopped with an error: ${detail}`, transient: false };
598
+ if (subtype === 'error_during_execution')
599
+ return { reason: 'stopped with an error during execution', transient: false };
600
+ return { reason: subtype ? `stopped with result "${subtype}"` : 'stopped without a result', transient: false };
601
+ }
602
+ /**
603
+ * Failures that come from the Claude API or the network rather than from the
604
+ * task: the session is intact and continues where it stopped when resumed.
605
+ */
606
+ export function isTransientClaudeApiFailure(text) {
607
+ return /\bAPI Error\b|server error|mid-response|overloaded|internal server error|rate limit|too many requests|\b(?:429|500|502|503|504|529)\b|ECONNRESET|ECONNREFUSED|ETIMEDOUT|EAI_AGAIN|socket hang up|fetch failed|network error|stream (?:closed|ended|error)|connection (?:error|reset|closed)/i.test(text);
608
+ }
609
+ function writeClaudeLogLines(lines) {
610
+ for (const line of lines)
611
+ process.stdout.write(`${line}\n`);
612
+ }
613
+ function formatClaudeToolActivity(name, input) {
614
+ const path = stringField(input, 'file_path') ?? stringField(input, 'path');
615
+ const pattern = stringField(input, 'pattern');
616
+ const description = stringField(input, 'description');
617
+ const command = stringField(input, 'command');
618
+ const query = stringField(input, 'query');
619
+ const url = stringField(input, 'url');
620
+ const prompt = stringField(input, 'prompt');
621
+ if (name === 'Read')
622
+ return `Reading ${compactLogText(path ?? 'a file')}`;
623
+ if (name === 'Write')
624
+ return `Writing ${compactLogText(path ?? 'a file')}`;
625
+ if (name === 'Edit' || name === 'MultiEdit')
626
+ return `Editing ${compactLogText(path ?? 'a file')}`;
627
+ if (name === 'Glob')
628
+ return `Finding files matching ${compactLogText(pattern ?? '*')}`;
629
+ if (name === 'Grep')
630
+ return `Searching${pattern ? ` for ${compactLogText(pattern)}` : ''}${path ? ` in ${compactLogText(path)}` : ''}`;
631
+ if (name === 'Bash')
632
+ return `Running ${compactLogText(description ?? command ?? 'a command')}`;
633
+ if (name === 'WebFetch')
634
+ return `Fetching ${compactLogText(url ?? 'a web page')}`;
635
+ if (name === 'WebSearch')
636
+ return `Searching the web${query ? ` for ${compactLogText(query)}` : ''}`;
637
+ if (name === 'Skill')
638
+ return `Loading skill ${compactLogText(stringField(input, 'skill') ?? 'instructions')}`;
639
+ if (name === 'ToolSearch')
640
+ return `Looking up tools${query ? ` matching ${compactLogText(query)}` : ''}`;
641
+ if (name === 'Agent' || name === 'Task')
642
+ return `Starting subtask${description || prompt ? `: ${compactLogText(description ?? prompt ?? '')}` : ''}`;
643
+ return `Using ${compactLogText(name)}`;
644
+ }
645
+ function compactClaudeValue(value) {
646
+ if (typeof value === 'string')
647
+ return compactLogText(value);
648
+ if (!Array.isArray(value))
649
+ return '';
650
+ return compactLogText(value
651
+ .map((item) => stringField(jsonRecord(item) ?? {}, 'text') ?? '')
652
+ .filter(Boolean)
653
+ .join(' '));
654
+ }
655
+ /**
656
+ * Codex `exec --json` emits one JSON object per line: thread and turn
657
+ * lifecycle events plus `item.started` / `item.completed` for each message,
658
+ * command, file change, and MCP call.
659
+ */
660
+ export class CodexStreamLogFormatter {
661
+ lines = new LineBuffer();
662
+ /** Items Codex has started, by id, with the time they started. */
663
+ startedItems = new Map();
664
+ now;
665
+ pulse;
666
+ lastText = '';
667
+ stopReason;
668
+ onToolCall;
669
+ /** Sum of every `turn.completed` usage, or the last cumulative `token_count` total when the stream carries one. */
670
+ tokens;
671
+ turns = 0;
672
+ maxContextTokens = 0;
673
+ clock = new StreamClock();
674
+ constructor(options = {}) {
675
+ this.now = options.now ?? Date.now;
676
+ this.pulse = new ActivityPulse(this.now, options.heartbeatMs ?? AGENT_LOG_HEARTBEAT_MS);
677
+ }
678
+ heartbeat(now = this.now()) {
679
+ return this.pulse.lines(now);
680
+ }
681
+ get usage() {
682
+ if (!this.tokens)
683
+ return undefined;
684
+ return buildUsage(this.tokens, {
685
+ turns: this.turns,
686
+ maxContextTokens: this.maxContextTokens,
687
+ durationMs: this.clock.elapsedMs(),
688
+ });
689
+ }
690
+ /**
691
+ * Codex counts cached prompt tokens inside `input_tokens`; Claude reports
692
+ * them separately. Split them out so "input" means uncached input for every
693
+ * agent and the four counters add up to the total either way.
694
+ */
695
+ static tokensOf(usage) {
696
+ if (!usage)
697
+ return undefined;
698
+ const input = numberField(usage, 'input_tokens');
699
+ const output = numberField(usage, 'output_tokens');
700
+ if (input === undefined && output === undefined)
701
+ return undefined;
702
+ const cached = Math.min(numberField(usage, 'cached_input_tokens') ?? 0, input ?? 0);
703
+ return { input: (input ?? 0) - cached, output: output ?? 0, cacheRead: cached, cacheCreation: 0 };
704
+ }
705
+ push(chunk) {
706
+ const written = this.pulse.mark(this.lines.push(chunk).flatMap((line) => this.formatLine(line)));
707
+ return [...written, ...this.pulse.lines()];
708
+ }
709
+ finish() {
710
+ return this.pulse.mark(this.lines.finish().flatMap((line) => this.formatLine(line)));
711
+ }
712
+ formatLine(line) {
713
+ if (!line.trim())
714
+ return [];
715
+ let message;
716
+ try {
717
+ const record = jsonRecord(JSON.parse(line));
718
+ if (!record)
719
+ return [line];
720
+ message = record;
721
+ }
722
+ catch {
723
+ return [line];
724
+ }
725
+ const type = stringField(message, 'type');
726
+ this.clock.touch();
727
+ if (type === 'thread.started') {
728
+ this.pulse.begin("waiting for Codex's first response");
729
+ return ['Codex session started'];
730
+ }
731
+ if (type === 'turn.failed') {
732
+ const error = jsonRecord(message.error);
733
+ const detail = error ? stringField(error, 'message') : undefined;
734
+ this.stopReason = `stopped with an error${detail ? `: ${compactLogText(detail)}` : ''}`;
735
+ return [`Codex ${this.stopReason}`];
736
+ }
737
+ if (type === 'error') {
738
+ const detail = stringField(message, 'message');
739
+ this.stopReason = `stopped with an error${detail ? `: ${compactLogText(detail)}` : ''}`;
740
+ return [`Codex ${this.stopReason}`];
741
+ }
742
+ if (type === 'turn.completed') {
743
+ const counts = CodexStreamLogFormatter.tokensOf(jsonRecord(message.usage));
744
+ this.turns += 1;
745
+ // A turn's usage is the sum over every request in the turn, not one
746
+ // request's context, so it says nothing about context size; only a
747
+ // token_count event's last_token_usage does.
748
+ if (counts)
749
+ this.tokens = addTokens(this.tokens ?? ZERO_TOKENS, counts);
750
+ this.pulse.end();
751
+ return [`Codex finished · ${this.turns} turn${this.turns === 1 ? '' : 's'}${usageSummary(this.usage)}`];
752
+ }
753
+ if (type === 'token_count' || type === 'thread.token_usage') {
754
+ // Codex core's cumulative counter: `info.total_token_usage` is the
755
+ // session total so far and `info.last_token_usage` the latest request.
756
+ const info = jsonRecord(message.info) ?? jsonRecord(message.usage) ?? message;
757
+ const total = CodexStreamLogFormatter.tokensOf(jsonRecord(info.total_token_usage) ?? jsonRecord(info.total));
758
+ const last = CodexStreamLogFormatter.tokensOf(jsonRecord(info.last_token_usage) ?? jsonRecord(info.last));
759
+ if (total)
760
+ this.tokens = total;
761
+ if (last)
762
+ this.maxContextTokens = Math.max(this.maxContextTokens, contextTokens(last));
763
+ return [];
764
+ }
765
+ if (type !== 'item.started' && type !== 'item.completed')
766
+ return [];
767
+ const item = jsonRecord(message.item);
768
+ if (!item)
769
+ return [];
770
+ return type === 'item.started' ? this.itemStarted(item) : this.itemCompleted(item);
771
+ }
772
+ itemStarted(item) {
773
+ const id = stringField(item, 'id');
774
+ const startedAt = this.now();
775
+ if (id)
776
+ this.startedItems.set(id, startedAt);
777
+ const activity = this.activity(item);
778
+ if (!activity)
779
+ return [];
780
+ this.reportTool(item);
781
+ this.pulse.begin(`waiting for ${activity} to finish`, { startedAt });
782
+ return [`→ ${activity}`];
783
+ }
784
+ itemCompleted(item) {
785
+ const kind = stringField(item, 'type');
786
+ const id = stringField(item, 'id');
787
+ const startedAt = id !== undefined ? this.startedItems.get(id) : undefined;
788
+ const started = startedAt !== undefined;
789
+ if (id)
790
+ this.startedItems.delete(id);
791
+ this.pulse.begin("waiting for Codex's next step");
792
+ if (kind === 'agent_message')
793
+ return this.textLines(stringField(item, 'text') ?? '');
794
+ if (kind === 'reasoning')
795
+ return [];
796
+ if (kind === 'error')
797
+ return [`✗ ${compactLogText(stringField(item, 'message') ?? 'Codex reported an error')}`];
798
+ if (kind === 'todo_list') {
799
+ const items = Array.isArray(item.items) ? item.items : [];
800
+ const done = items.filter((entry) => jsonRecord(entry)?.completed === true).length;
801
+ return [`Plan: ${done} of ${items.length} steps done`];
802
+ }
803
+ const activity = this.activity(item);
804
+ if (!activity)
805
+ return [];
806
+ if (!started)
807
+ this.reportTool(item);
808
+ const status = stringField(item, 'status');
809
+ const exitCode = numberField(item, 'exit_code');
810
+ const failed = status === 'failed' || status === 'declined' || (exitCode !== undefined && exitCode !== 0);
811
+ const duration = toolDurationSuffix(startedAt, this.now());
812
+ return [failed ? `✗ ${activity} failed${exitCode !== undefined && exitCode !== 0 ? ` (exit ${exitCode})` : ''}${duration}` : `✓ ${activity}${duration}`];
813
+ }
814
+ activity(item) {
815
+ const kind = stringField(item, 'type');
816
+ if (kind === 'command_execution')
817
+ return `Running ${compactLogText(stringField(item, 'command') ?? 'a command')}`;
818
+ if (kind === 'file_change') {
819
+ const changes = Array.isArray(item.changes) ? item.changes.flatMap((change) => {
820
+ const record = jsonRecord(change);
821
+ return record && stringField(record, 'path') ? [stringField(record, 'path')] : [];
822
+ }) : [];
823
+ return changes.length > 0 ? `Editing ${compactLogText(changes.join(', '))}` : 'Editing files';
824
+ }
825
+ if (kind === 'mcp_tool_call') {
826
+ return `Using ${compactLogText(`${stringField(item, 'server') ?? 'mcp'} ${stringField(item, 'tool') ?? 'tool'}`)}`;
827
+ }
828
+ if (kind === 'web_search')
829
+ return `Searching the web${stringField(item, 'query') ? ` for ${compactLogText(stringField(item, 'query'))}` : ''}`;
830
+ return undefined;
831
+ }
832
+ /** Report the call in Claude's tool vocabulary so one classifier fits all agents. */
833
+ reportTool(item) {
834
+ if (!this.onToolCall)
835
+ return;
836
+ const kind = stringField(item, 'type');
837
+ if (kind === 'command_execution') {
838
+ this.onToolCall('Bash', { command: stringField(item, 'command') ?? '' });
839
+ }
840
+ else if (kind === 'mcp_tool_call') {
841
+ const server = stringField(item, 'server') ?? 'mcp';
842
+ const tool = stringField(item, 'tool') ?? 'tool';
843
+ this.onToolCall(`mcp__${server}__${tool}`, jsonRecord(item.arguments) ?? {});
844
+ }
845
+ else if (kind === 'file_change') {
846
+ const changes = Array.isArray(item.changes) ? item.changes : [];
847
+ for (const change of changes) {
848
+ const record = jsonRecord(change);
849
+ const path = record ? stringField(record, 'path') : undefined;
850
+ if (path)
851
+ this.onToolCall('Write', { file_path: path });
852
+ }
853
+ }
854
+ }
855
+ textLines(text) {
856
+ const normalized = text.trim();
857
+ if (!normalized || normalized === this.lastText)
858
+ return [];
859
+ this.lastText = normalized;
860
+ return normalized.split(/\r?\n/).filter(Boolean);
861
+ }
862
+ }
863
+ /**
864
+ * Gemini CLI `--output-format stream-json` emits init, message, tool_use,
865
+ * tool_result, error, and result events. Assistant text arrives as deltas
866
+ * that are joined until the next event of another kind.
867
+ */
868
+ export class GeminiStreamLogFormatter {
869
+ lines = new LineBuffer();
870
+ tools = new Map();
871
+ now;
872
+ pulse;
873
+ assistantText = '';
874
+ lastText = '';
875
+ stopReason;
876
+ onToolCall;
877
+ /** Set from the result event's `stats` when it carries token counts; Gemini reports nothing per message. */
878
+ usage;
879
+ clock = new StreamClock();
880
+ constructor(options = {}) {
881
+ this.now = options.now ?? Date.now;
882
+ this.pulse = new ActivityPulse(this.now, options.heartbeatMs ?? AGENT_LOG_HEARTBEAT_MS);
883
+ }
884
+ heartbeat(now = this.now()) {
885
+ return this.pulse.lines(now);
886
+ }
887
+ push(chunk) {
888
+ const written = this.pulse.mark(this.lines.push(chunk).flatMap((line) => this.formatLine(line)));
889
+ return [...written, ...this.pulse.lines()];
890
+ }
891
+ finish() {
892
+ return this.pulse.mark([...this.lines.finish().flatMap((line) => this.formatLine(line)), ...this.flushText()]);
893
+ }
894
+ formatLine(line) {
895
+ if (!line.trim())
896
+ return [];
897
+ let message;
898
+ try {
899
+ const record = jsonRecord(JSON.parse(line));
900
+ if (!record)
901
+ return [line];
902
+ message = record;
903
+ }
904
+ catch {
905
+ return [line];
906
+ }
907
+ const type = stringField(message, 'type');
908
+ this.clock.touch();
909
+ if (type === 'message') {
910
+ if (stringField(message, 'role') !== 'assistant')
911
+ return [];
912
+ const content = stringField(message, 'content') ?? '';
913
+ if (message.delta === true) {
914
+ this.assistantText += content;
915
+ return [];
916
+ }
917
+ return [...this.flushText(), ...this.textLines(content)];
918
+ }
919
+ const pending = this.flushText();
920
+ if (type === 'init') {
921
+ const model = stringField(message, 'model');
922
+ this.pulse.begin("waiting for Gemini's first response");
923
+ return [...pending, `Gemini session started${model ? ` · ${model}` : ''}`];
924
+ }
925
+ if (type === 'tool_use') {
926
+ const name = stringField(message, 'tool_name') ?? 'tool';
927
+ const parameters = jsonRecord(message.parameters) ?? {};
928
+ const activity = formatGeminiToolActivity(name, parameters);
929
+ const id = stringField(message, 'tool_id');
930
+ const startedAt = this.now();
931
+ if (id)
932
+ this.tools.set(id, { activity, startedAt });
933
+ this.reportTool(name, parameters);
934
+ this.pulse.begin(`waiting for ${activity} to finish`, { startedAt });
935
+ return [...pending, `→ ${activity}`];
936
+ }
937
+ if (type === 'tool_result') {
938
+ const id = stringField(message, 'tool_id');
939
+ const call = id ? this.tools.get(id) : undefined;
940
+ const activity = call?.activity ?? 'Tool action';
941
+ if (id)
942
+ this.tools.delete(id);
943
+ const duration = toolDurationSuffix(call?.startedAt, this.now());
944
+ this.pulse.begin("waiting for Gemini's next step");
945
+ if (stringField(message, 'status') === 'error') {
946
+ const detail = stringField(message, 'output') ?? stringField(message, 'error');
947
+ return [...pending, `✗ ${activity} failed${detail ? `: ${compactLogText(detail)}` : ''}${duration}`];
948
+ }
949
+ return [...pending, `✓ ${activity}${duration}`];
950
+ }
951
+ if (type === 'error') {
952
+ const detail = stringField(message, 'message');
953
+ const fatal = stringField(message, 'severity') !== 'warning';
954
+ if (fatal)
955
+ this.stopReason = `stopped with an error${detail ? `: ${compactLogText(detail)}` : ''}`;
956
+ return [...pending, `${fatal ? '✗' : '!'} ${compactLogText(detail ?? 'Gemini reported an error')}`];
957
+ }
958
+ if (type === 'result') {
959
+ const success = stringField(message, 'status') === 'success';
960
+ const stats = jsonRecord(message.stats);
961
+ const duration = stats ? numberField(stats, 'duration_ms') : undefined;
962
+ const calls = stats ? numberField(stats, 'tool_calls') : undefined;
963
+ this.usage = geminiUsage(stats, duration ?? this.clock.elapsedMs()) ?? this.usage;
964
+ this.pulse.end();
965
+ if (!success && !this.stopReason) {
966
+ const detail = jsonRecord(message.error);
967
+ const reason = detail ? stringField(detail, 'message') : stringField(message, 'error');
968
+ this.stopReason = reason ? `stopped with an error: ${compactLogText(reason)}` : 'stopped without a successful result';
969
+ }
970
+ return [
971
+ ...pending,
972
+ `Gemini ${success ? 'finished' : 'stopped'}${calls !== undefined ? ` · ${calls} tool calls` : ''}${duration !== undefined ? ` · ${formatLogDuration(duration)}` : ''}${usageSummary(this.usage)}${!success && this.stopReason ? ` · ${this.stopReason}` : ''}`,
973
+ ];
974
+ }
975
+ return pending;
976
+ }
977
+ reportTool(name, parameters) {
978
+ if (!this.onToolCall)
979
+ return;
980
+ if (name === 'run_shell_command' || name === 'ShellTool') {
981
+ this.onToolCall('Bash', { command: stringField(parameters, 'command') ?? '' });
982
+ return;
983
+ }
984
+ if (name === 'write_file' || name === 'WriteFile' || name === 'replace' || name === 'Edit') {
985
+ this.onToolCall(name === 'write_file' || name === 'WriteFile' ? 'Write' : 'Edit', { file_path: stringField(parameters, 'file_path') ?? stringField(parameters, 'path') ?? '' });
986
+ return;
987
+ }
988
+ this.onToolCall(name, parameters);
989
+ }
990
+ flushText() {
991
+ const text = this.assistantText;
992
+ this.assistantText = '';
993
+ return this.textLines(text);
994
+ }
995
+ textLines(text) {
996
+ const normalized = text.trim();
997
+ if (!normalized || normalized === this.lastText)
998
+ return [];
999
+ this.lastText = normalized;
1000
+ return normalized.split(/\r?\n/).filter(Boolean);
1001
+ }
1002
+ }
1003
+ /**
1004
+ * Gemini's result `stats` either carry flat token counts (`input_tokens`,
1005
+ * `output_tokens`, `cached_tokens`) or a per-model map whose `tokens` object
1006
+ * has `prompt`, `candidates`, and `cached`. Anything else leaves usage unset.
1007
+ */
1008
+ function geminiUsage(stats, durationMs) {
1009
+ if (!stats)
1010
+ return undefined;
1011
+ let counts;
1012
+ const input = numberField(stats, 'input_tokens') ?? numberField(stats, 'prompt_tokens');
1013
+ const output = numberField(stats, 'output_tokens') ?? numberField(stats, 'candidates_tokens');
1014
+ if (input !== undefined || output !== undefined) {
1015
+ const cached = Math.min(numberField(stats, 'cached_tokens') ?? numberField(stats, 'cached_content_tokens') ?? 0, input ?? 0);
1016
+ counts = { input: (input ?? 0) - cached, output: output ?? 0, cacheRead: cached, cacheCreation: 0 };
1017
+ }
1018
+ else {
1019
+ const models = jsonRecord(stats.models);
1020
+ for (const entry of Object.values(models ?? {})) {
1021
+ const tokens = jsonRecord(jsonRecord(entry)?.tokens);
1022
+ if (!tokens)
1023
+ continue;
1024
+ const prompt = numberField(tokens, 'prompt') ?? 0;
1025
+ const cached = Math.min(numberField(tokens, 'cached') ?? 0, prompt);
1026
+ counts = addTokens(counts ?? ZERO_TOKENS, {
1027
+ input: prompt - cached,
1028
+ output: (numberField(tokens, 'candidates') ?? 0) + (numberField(tokens, 'thoughts') ?? 0),
1029
+ cacheRead: cached,
1030
+ cacheCreation: 0,
1031
+ });
1032
+ }
1033
+ }
1034
+ if (!counts)
1035
+ return undefined;
1036
+ return buildUsage(counts, {
1037
+ turns: numberField(stats, 'turns') ?? numberField(stats, 'tool_calls') ?? 0,
1038
+ // Gemini reports session totals only, so the largest context is unknown; the total input is an upper bound.
1039
+ maxContextTokens: contextTokens(counts),
1040
+ durationMs,
1041
+ });
1042
+ }
1043
+ function formatGeminiToolActivity(name, parameters) {
1044
+ const path = stringField(parameters, 'file_path') ?? stringField(parameters, 'path') ?? stringField(parameters, 'absolute_path');
1045
+ const command = stringField(parameters, 'command');
1046
+ const pattern = stringField(parameters, 'pattern');
1047
+ const query = stringField(parameters, 'query');
1048
+ const url = stringField(parameters, 'url') ?? stringField(parameters, 'prompt');
1049
+ if (name === 'read_file' || name === 'ReadFile')
1050
+ return `Reading ${compactLogText(path ?? 'a file')}`;
1051
+ if (name === 'read_many_files' || name === 'ReadManyFiles')
1052
+ return 'Reading several files';
1053
+ if (name === 'write_file' || name === 'WriteFile')
1054
+ return `Writing ${compactLogText(path ?? 'a file')}`;
1055
+ if (name === 'replace' || name === 'Edit')
1056
+ return `Editing ${compactLogText(path ?? 'a file')}`;
1057
+ if (name === 'run_shell_command' || name === 'ShellTool')
1058
+ return `Running ${compactLogText(stringField(parameters, 'description') ?? command ?? 'a command')}`;
1059
+ if (name === 'glob' || name === 'FindFiles')
1060
+ return `Finding files matching ${compactLogText(pattern ?? '*')}`;
1061
+ if (name === 'search_file_content' || name === 'grep_search' || name === 'SearchText')
1062
+ return `Searching${pattern ? ` for ${compactLogText(pattern)}` : ''}${path ? ` in ${compactLogText(path)}` : ''}`;
1063
+ if (name === 'list_directory' || name === 'ReadFolder')
1064
+ return `Listing ${compactLogText(path ?? 'a folder')}`;
1065
+ if (name === 'web_fetch' || name === 'WebFetch')
1066
+ return `Fetching ${compactLogText(url ?? 'a web page')}`;
1067
+ if (name === 'google_web_search' || name === 'GoogleSearch')
1068
+ return `Searching the web${query ? ` for ${compactLogText(query)}` : ''}`;
1069
+ if (name === 'save_memory')
1070
+ return 'Saving a note to memory';
1071
+ return `Using ${compactLogText(name)}`;
1072
+ }
1073
+ export function compactLogText(value, maximum = 240) {
1074
+ const compact = value.replace(/\s+/g, ' ').trim();
1075
+ return compact.length > maximum ? `${compact.slice(0, maximum - 1)}…` : compact;
1076
+ }
1077
+ export function formatLogDuration(milliseconds) {
1078
+ const seconds = Math.max(0, Math.round(milliseconds / 1000));
1079
+ const minutes = Math.floor(seconds / 60);
1080
+ const remaining = seconds % 60;
1081
+ return minutes > 0 ? `${minutes}m ${remaining}s` : `${remaining}s`;
1082
+ }
1083
+ export function jsonRecord(value) {
1084
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
1085
+ ? value
1086
+ : undefined;
1087
+ }
1088
+ export function stringField(record, key) {
1089
+ return typeof record[key] === 'string' ? record[key] : undefined;
1090
+ }
1091
+ export function numberField(record, key) {
1092
+ return typeof record[key] === 'number' ? record[key] : undefined;
1093
+ }
1094
+ //# sourceMappingURL=agent-log.js.map