@katyella/legio 0.1.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 (219) hide show
  1. package/CHANGELOG.md +422 -0
  2. package/LICENSE +21 -0
  3. package/README.md +555 -0
  4. package/agents/builder.md +141 -0
  5. package/agents/coordinator.md +351 -0
  6. package/agents/cto.md +196 -0
  7. package/agents/gateway.md +276 -0
  8. package/agents/lead.md +281 -0
  9. package/agents/merger.md +156 -0
  10. package/agents/monitor.md +212 -0
  11. package/agents/reviewer.md +142 -0
  12. package/agents/scout.md +131 -0
  13. package/agents/supervisor.md +416 -0
  14. package/bin/legio.mjs +38 -0
  15. package/package.json +77 -0
  16. package/src/agents/checkpoint.test.ts +88 -0
  17. package/src/agents/checkpoint.ts +102 -0
  18. package/src/agents/hooks-deployer.test.ts +1820 -0
  19. package/src/agents/hooks-deployer.ts +574 -0
  20. package/src/agents/identity.test.ts +614 -0
  21. package/src/agents/identity.ts +385 -0
  22. package/src/agents/lifecycle.test.ts +202 -0
  23. package/src/agents/lifecycle.ts +184 -0
  24. package/src/agents/manifest.test.ts +558 -0
  25. package/src/agents/manifest.ts +297 -0
  26. package/src/agents/overlay.test.ts +592 -0
  27. package/src/agents/overlay.ts +316 -0
  28. package/src/beads/client.test.ts +210 -0
  29. package/src/beads/client.ts +227 -0
  30. package/src/beads/molecules.test.ts +320 -0
  31. package/src/beads/molecules.ts +209 -0
  32. package/src/commands/agents.test.ts +325 -0
  33. package/src/commands/agents.ts +286 -0
  34. package/src/commands/clean.test.ts +730 -0
  35. package/src/commands/clean.ts +653 -0
  36. package/src/commands/completions.test.ts +346 -0
  37. package/src/commands/completions.ts +950 -0
  38. package/src/commands/coordinator.test.ts +1524 -0
  39. package/src/commands/coordinator.ts +880 -0
  40. package/src/commands/costs.test.ts +1015 -0
  41. package/src/commands/costs.ts +473 -0
  42. package/src/commands/dashboard.test.ts +94 -0
  43. package/src/commands/dashboard.ts +607 -0
  44. package/src/commands/doctor.test.ts +295 -0
  45. package/src/commands/doctor.ts +213 -0
  46. package/src/commands/down.test.ts +308 -0
  47. package/src/commands/down.ts +124 -0
  48. package/src/commands/errors.test.ts +648 -0
  49. package/src/commands/errors.ts +255 -0
  50. package/src/commands/feed.test.ts +579 -0
  51. package/src/commands/feed.ts +368 -0
  52. package/src/commands/gateway.test.ts +698 -0
  53. package/src/commands/gateway.ts +419 -0
  54. package/src/commands/group.test.ts +262 -0
  55. package/src/commands/group.ts +539 -0
  56. package/src/commands/hooks.test.ts +292 -0
  57. package/src/commands/hooks.ts +210 -0
  58. package/src/commands/init.test.ts +211 -0
  59. package/src/commands/init.ts +622 -0
  60. package/src/commands/inspect.test.ts +670 -0
  61. package/src/commands/inspect.ts +455 -0
  62. package/src/commands/log.test.ts +1556 -0
  63. package/src/commands/log.ts +752 -0
  64. package/src/commands/logs.test.ts +379 -0
  65. package/src/commands/logs.ts +544 -0
  66. package/src/commands/mail.test.ts +1726 -0
  67. package/src/commands/mail.ts +926 -0
  68. package/src/commands/merge.test.ts +676 -0
  69. package/src/commands/merge.ts +374 -0
  70. package/src/commands/metrics.test.ts +444 -0
  71. package/src/commands/metrics.ts +150 -0
  72. package/src/commands/monitor.test.ts +151 -0
  73. package/src/commands/monitor.ts +394 -0
  74. package/src/commands/nudge.test.ts +230 -0
  75. package/src/commands/nudge.ts +373 -0
  76. package/src/commands/prime.test.ts +467 -0
  77. package/src/commands/prime.ts +386 -0
  78. package/src/commands/replay.test.ts +742 -0
  79. package/src/commands/replay.ts +367 -0
  80. package/src/commands/run.test.ts +443 -0
  81. package/src/commands/run.ts +365 -0
  82. package/src/commands/server.test.ts +626 -0
  83. package/src/commands/server.ts +298 -0
  84. package/src/commands/sling.test.ts +810 -0
  85. package/src/commands/sling.ts +700 -0
  86. package/src/commands/spec.test.ts +206 -0
  87. package/src/commands/spec.ts +171 -0
  88. package/src/commands/status.test.ts +276 -0
  89. package/src/commands/status.ts +339 -0
  90. package/src/commands/stop.test.ts +357 -0
  91. package/src/commands/stop.ts +119 -0
  92. package/src/commands/supervisor.test.ts +186 -0
  93. package/src/commands/supervisor.ts +544 -0
  94. package/src/commands/trace.test.ts +746 -0
  95. package/src/commands/trace.ts +332 -0
  96. package/src/commands/up.test.ts +597 -0
  97. package/src/commands/up.ts +275 -0
  98. package/src/commands/watch.test.ts +152 -0
  99. package/src/commands/watch.ts +238 -0
  100. package/src/commands/worktree.test.ts +648 -0
  101. package/src/commands/worktree.ts +266 -0
  102. package/src/config.test.ts +496 -0
  103. package/src/config.ts +616 -0
  104. package/src/doctor/agents.test.ts +448 -0
  105. package/src/doctor/agents.ts +396 -0
  106. package/src/doctor/config-check.test.ts +184 -0
  107. package/src/doctor/config-check.ts +185 -0
  108. package/src/doctor/consistency.test.ts +645 -0
  109. package/src/doctor/consistency.ts +294 -0
  110. package/src/doctor/databases.test.ts +284 -0
  111. package/src/doctor/databases.ts +211 -0
  112. package/src/doctor/dependencies.test.ts +150 -0
  113. package/src/doctor/dependencies.ts +179 -0
  114. package/src/doctor/logs.test.ts +244 -0
  115. package/src/doctor/logs.ts +295 -0
  116. package/src/doctor/merge-queue.test.ts +210 -0
  117. package/src/doctor/merge-queue.ts +144 -0
  118. package/src/doctor/structure.test.ts +285 -0
  119. package/src/doctor/structure.ts +195 -0
  120. package/src/doctor/types.ts +37 -0
  121. package/src/doctor/version.test.ts +130 -0
  122. package/src/doctor/version.ts +131 -0
  123. package/src/e2e/chat-flow.test.ts +346 -0
  124. package/src/e2e/init-sling-lifecycle.test.ts +288 -0
  125. package/src/errors.test.ts +21 -0
  126. package/src/errors.ts +246 -0
  127. package/src/events/store.test.ts +660 -0
  128. package/src/events/store.ts +344 -0
  129. package/src/events/tool-filter.test.ts +330 -0
  130. package/src/events/tool-filter.ts +126 -0
  131. package/src/global-setup.ts +14 -0
  132. package/src/index.ts +339 -0
  133. package/src/insights/analyzer.test.ts +466 -0
  134. package/src/insights/analyzer.ts +203 -0
  135. package/src/logging/color.test.ts +118 -0
  136. package/src/logging/color.ts +71 -0
  137. package/src/logging/logger.test.ts +812 -0
  138. package/src/logging/logger.ts +266 -0
  139. package/src/logging/reporter.test.ts +258 -0
  140. package/src/logging/reporter.ts +109 -0
  141. package/src/logging/sanitizer.test.ts +190 -0
  142. package/src/logging/sanitizer.ts +57 -0
  143. package/src/mail/broadcast.test.ts +203 -0
  144. package/src/mail/broadcast.ts +92 -0
  145. package/src/mail/client.test.ts +873 -0
  146. package/src/mail/client.ts +236 -0
  147. package/src/mail/store.test.ts +815 -0
  148. package/src/mail/store.ts +402 -0
  149. package/src/merge/queue.test.ts +449 -0
  150. package/src/merge/queue.ts +262 -0
  151. package/src/merge/resolver.test.ts +1453 -0
  152. package/src/merge/resolver.ts +759 -0
  153. package/src/metrics/store.test.ts +1167 -0
  154. package/src/metrics/store.ts +511 -0
  155. package/src/metrics/summary.test.ts +397 -0
  156. package/src/metrics/summary.ts +178 -0
  157. package/src/metrics/transcript.test.ts +643 -0
  158. package/src/metrics/transcript.ts +351 -0
  159. package/src/mulch/client.test.ts +547 -0
  160. package/src/mulch/client.ts +416 -0
  161. package/src/server/audit-store.test.ts +384 -0
  162. package/src/server/audit-store.ts +257 -0
  163. package/src/server/headless.test.ts +180 -0
  164. package/src/server/headless.ts +151 -0
  165. package/src/server/index.test.ts +241 -0
  166. package/src/server/index.ts +317 -0
  167. package/src/server/public/app.js +187 -0
  168. package/src/server/public/apple-touch-icon.png +0 -0
  169. package/src/server/public/components/agent-badge.js +37 -0
  170. package/src/server/public/components/data-table.js +114 -0
  171. package/src/server/public/components/gateway-chat.js +256 -0
  172. package/src/server/public/components/issue-card.js +96 -0
  173. package/src/server/public/components/layout.js +88 -0
  174. package/src/server/public/components/message-bubble.js +120 -0
  175. package/src/server/public/components/stat-card.js +26 -0
  176. package/src/server/public/components/terminal-panel.js +140 -0
  177. package/src/server/public/favicon-16.png +0 -0
  178. package/src/server/public/favicon-32.png +0 -0
  179. package/src/server/public/favicon.ico +0 -0
  180. package/src/server/public/favicon.png +0 -0
  181. package/src/server/public/index.html +64 -0
  182. package/src/server/public/lib/api.js +35 -0
  183. package/src/server/public/lib/markdown.js +8 -0
  184. package/src/server/public/lib/preact-setup.js +8 -0
  185. package/src/server/public/lib/state.js +99 -0
  186. package/src/server/public/lib/utils.js +309 -0
  187. package/src/server/public/lib/ws.js +79 -0
  188. package/src/server/public/views/chat.js +983 -0
  189. package/src/server/public/views/costs.js +692 -0
  190. package/src/server/public/views/dashboard.js +781 -0
  191. package/src/server/public/views/gateway-chat.js +622 -0
  192. package/src/server/public/views/inspect.js +399 -0
  193. package/src/server/public/views/issues.js +470 -0
  194. package/src/server/public/views/setup.js +94 -0
  195. package/src/server/public/views/task-detail.js +422 -0
  196. package/src/server/routes.test.ts +3816 -0
  197. package/src/server/routes.ts +1964 -0
  198. package/src/server/websocket.test.ts +288 -0
  199. package/src/server/websocket.ts +196 -0
  200. package/src/sessions/compat.test.ts +109 -0
  201. package/src/sessions/compat.ts +17 -0
  202. package/src/sessions/store.test.ts +969 -0
  203. package/src/sessions/store.ts +480 -0
  204. package/src/test-helpers.test.ts +97 -0
  205. package/src/test-helpers.ts +143 -0
  206. package/src/types.ts +708 -0
  207. package/src/watchdog/daemon.test.ts +1233 -0
  208. package/src/watchdog/daemon.ts +533 -0
  209. package/src/watchdog/health.test.ts +371 -0
  210. package/src/watchdog/health.ts +248 -0
  211. package/src/watchdog/triage.test.ts +162 -0
  212. package/src/watchdog/triage.ts +193 -0
  213. package/src/worktree/manager.test.ts +444 -0
  214. package/src/worktree/manager.ts +224 -0
  215. package/src/worktree/tmux.test.ts +1238 -0
  216. package/src/worktree/tmux.ts +644 -0
  217. package/templates/CLAUDE.md.tmpl +89 -0
  218. package/templates/hooks.json.tmpl +132 -0
  219. package/templates/overlay.md.tmpl +79 -0
@@ -0,0 +1,351 @@
1
+ import { readFile } from "node:fs/promises";
2
+
3
+ /**
4
+ * Parser for Claude Code transcript JSONL files.
5
+ *
6
+ * Extracts token usage data from assistant-type entries in transcript files
7
+ * at ~/.claude/projects/{project-slug}/{session-id}.jsonl.
8
+ *
9
+ * Each assistant entry contains per-turn usage:
10
+ * {
11
+ * "type": "assistant",
12
+ * "message": {
13
+ * "model": "claude-opus-4-6",
14
+ * "usage": {
15
+ * "input_tokens": 3,
16
+ * "output_tokens": 9,
17
+ * "cache_read_input_tokens": 19401,
18
+ * "cache_creation_input_tokens": 9918
19
+ * }
20
+ * }
21
+ * }
22
+ */
23
+
24
+ interface TranscriptUsage {
25
+ inputTokens: number;
26
+ outputTokens: number;
27
+ cacheReadTokens: number;
28
+ cacheCreationTokens: number;
29
+ modelUsed: string | null;
30
+ }
31
+
32
+ /** A single extracted text message from a transcript entry. */
33
+ interface TranscriptMessage {
34
+ role: "user" | "assistant";
35
+ text: string;
36
+ }
37
+
38
+ /** Pricing per million tokens (USD). */
39
+ interface ModelPricing {
40
+ inputPerMTok: number;
41
+ outputPerMTok: number;
42
+ cacheReadPerMTok: number;
43
+ cacheCreationPerMTok: number;
44
+ }
45
+
46
+ /** Hardcoded pricing for known Claude models. */
47
+ const MODEL_PRICING: Record<string, ModelPricing> = {
48
+ // Opus 4 / 4.1 — legacy pricing
49
+ "opus-legacy": {
50
+ inputPerMTok: 15,
51
+ outputPerMTok: 75,
52
+ cacheReadPerMTok: 1.5, // 10% of input
53
+ cacheCreationPerMTok: 3.75, // 25% of input
54
+ },
55
+ // Opus 4.5+ (claude-opus-4-5, claude-opus-4-6, ...) — reduced pricing
56
+ "opus-new": {
57
+ inputPerMTok: 5,
58
+ outputPerMTok: 25,
59
+ cacheReadPerMTok: 0.5, // 10% of input
60
+ cacheCreationPerMTok: 1.25, // 25% of input
61
+ },
62
+ sonnet: {
63
+ inputPerMTok: 3,
64
+ outputPerMTok: 15,
65
+ cacheReadPerMTok: 0.3, // 10% of input
66
+ cacheCreationPerMTok: 0.75, // 25% of input
67
+ },
68
+ // Haiku 3.x / 4.0-4.4 — legacy pricing
69
+ "haiku-legacy": {
70
+ inputPerMTok: 0.8,
71
+ outputPerMTok: 4,
72
+ cacheReadPerMTok: 0.08, // 10% of input
73
+ cacheCreationPerMTok: 0.2, // 25% of input
74
+ },
75
+ // Haiku 4.5+ (claude-haiku-4-5-20251001, ...) — updated pricing
76
+ "haiku-new": {
77
+ inputPerMTok: 1,
78
+ outputPerMTok: 5,
79
+ cacheReadPerMTok: 0.1, // 10% of input
80
+ cacheCreationPerMTok: 0.25, // 25% of input
81
+ },
82
+ };
83
+
84
+ /**
85
+ * Determine the pricing tier for a given model string.
86
+ * Parses the major.minor version suffix (e.g. "opus-4-6") to select the right tier:
87
+ * - Opus 4.5+ -> opus-new ($5/$25), Opus < 4.5 -> opus-legacy ($15/$75)
88
+ * - Haiku 4.5+ -> haiku-new ($1/$5), Haiku < 4.5 -> haiku-legacy ($0.80/$4)
89
+ * - Sonnet -> sonnet ($3/$15)
90
+ * Returns null if unrecognized.
91
+ */
92
+ function getPricingForModel(model: string): ModelPricing | null {
93
+ const lower = model.toLowerCase();
94
+
95
+ if (lower.includes("opus")) {
96
+ // Detect major.minor version from pattern like "opus-4-6" or "opus-4-5"
97
+ const m = lower.match(/opus-(\d+)-(\d+)/);
98
+ if (m && m[1] !== undefined && m[2] !== undefined) {
99
+ const major = parseInt(m[1], 10);
100
+ const minor = parseInt(m[2], 10);
101
+ if (major > 4 || (major === 4 && minor >= 5)) {
102
+ return MODEL_PRICING["opus-new"] ?? null;
103
+ }
104
+ }
105
+ return MODEL_PRICING["opus-legacy"] ?? null;
106
+ }
107
+
108
+ if (lower.includes("sonnet")) return MODEL_PRICING.sonnet ?? null;
109
+
110
+ if (lower.includes("haiku")) {
111
+ // Detect major.minor version from pattern like "haiku-4-5" or "haiku-3-5"
112
+ const m = lower.match(/haiku-(\d+)-(\d+)/);
113
+ if (m && m[1] !== undefined && m[2] !== undefined) {
114
+ const major = parseInt(m[1], 10);
115
+ const minor = parseInt(m[2], 10);
116
+ if (major > 4 || (major === 4 && minor >= 5)) {
117
+ return MODEL_PRICING["haiku-new"] ?? null;
118
+ }
119
+ }
120
+ return MODEL_PRICING["haiku-legacy"] ?? null;
121
+ }
122
+
123
+ return null;
124
+ }
125
+
126
+ /**
127
+ * Calculate the estimated cost in USD for a given usage and model.
128
+ * Returns null if the model is unrecognized.
129
+ */
130
+ export function estimateCost(usage: TranscriptUsage): number | null {
131
+ if (usage.modelUsed === null) return null;
132
+
133
+ const pricing = getPricingForModel(usage.modelUsed);
134
+ if (pricing === null) return null;
135
+
136
+ const inputCost = (usage.inputTokens / 1_000_000) * pricing.inputPerMTok;
137
+ const outputCost = (usage.outputTokens / 1_000_000) * pricing.outputPerMTok;
138
+ const cacheReadCost = (usage.cacheReadTokens / 1_000_000) * pricing.cacheReadPerMTok;
139
+ const cacheCreationCost = (usage.cacheCreationTokens / 1_000_000) * pricing.cacheCreationPerMTok;
140
+
141
+ return inputCost + outputCost + cacheReadCost + cacheCreationCost;
142
+ }
143
+
144
+ /**
145
+ * Narrow an unknown value to determine if it looks like a transcript assistant entry.
146
+ * Returns the usage fields if valid, or null otherwise.
147
+ */
148
+ function extractUsageFromEntry(entry: unknown): {
149
+ inputTokens: number;
150
+ outputTokens: number;
151
+ cacheReadTokens: number;
152
+ cacheCreationTokens: number;
153
+ model: string | undefined;
154
+ } | null {
155
+ if (typeof entry !== "object" || entry === null) return null;
156
+
157
+ const obj = entry as Record<string, unknown>;
158
+ if (obj.type !== "assistant") return null;
159
+
160
+ const message = obj.message;
161
+ if (typeof message !== "object" || message === null) return null;
162
+
163
+ const msg = message as Record<string, unknown>;
164
+ const usage = msg.usage;
165
+ if (typeof usage !== "object" || usage === null) return null;
166
+
167
+ const u = usage as Record<string, unknown>;
168
+
169
+ return {
170
+ inputTokens: typeof u.input_tokens === "number" ? u.input_tokens : 0,
171
+ outputTokens: typeof u.output_tokens === "number" ? u.output_tokens : 0,
172
+ cacheReadTokens: typeof u.cache_read_input_tokens === "number" ? u.cache_read_input_tokens : 0,
173
+ cacheCreationTokens:
174
+ typeof u.cache_creation_input_tokens === "number" ? u.cache_creation_input_tokens : 0,
175
+ model: typeof msg.model === "string" ? msg.model : undefined,
176
+ };
177
+ }
178
+
179
+ /**
180
+ * Extract the first text block content from an assistant transcript entry.
181
+ * Returns the text string, or null if the entry is not an assistant turn
182
+ * or contains no text content block.
183
+ */
184
+ export function extractAssistantText(entry: unknown): string | null {
185
+ if (typeof entry !== "object" || entry === null) return null;
186
+
187
+ const obj = entry as Record<string, unknown>;
188
+ if (obj.type !== "assistant") return null;
189
+
190
+ const message = obj.message;
191
+ if (typeof message !== "object" || message === null) return null;
192
+
193
+ const msg = message as Record<string, unknown>;
194
+ const content = msg.content;
195
+
196
+ if (Array.isArray(content)) {
197
+ for (const block of content) {
198
+ if (typeof block === "object" && block !== null) {
199
+ const b = block as Record<string, unknown>;
200
+ if (b.type === "text" && typeof b.text === "string" && b.text.length > 0) {
201
+ return b.text;
202
+ }
203
+ }
204
+ }
205
+ }
206
+
207
+ return null;
208
+ }
209
+
210
+ /**
211
+ * Extract text content from a human transcript entry.
212
+ * Returns the first non-empty text block content, or null.
213
+ */
214
+ function extractHumanText(entry: unknown): string | null {
215
+ if (typeof entry !== "object" || entry === null) return null;
216
+
217
+ const obj = entry as Record<string, unknown>;
218
+ if (obj.type !== "human") return null;
219
+
220
+ const message = obj.message;
221
+ if (typeof message !== "object" || message === null) return null;
222
+
223
+ const msg = message as Record<string, unknown>;
224
+ const content = msg.content;
225
+
226
+ // Content may be a string or an array of blocks
227
+ if (typeof content === "string" && content.length > 0) {
228
+ return content;
229
+ }
230
+
231
+ if (Array.isArray(content)) {
232
+ for (const block of content) {
233
+ if (typeof block === "object" && block !== null) {
234
+ const b = block as Record<string, unknown>;
235
+ if (b.type === "text" && typeof b.text === "string" && b.text.length > 0) {
236
+ return b.text;
237
+ }
238
+ } else if (typeof block === "string" && block.length > 0) {
239
+ return block;
240
+ }
241
+ }
242
+ }
243
+
244
+ return null;
245
+ }
246
+
247
+ /**
248
+ * Parse a Claude Code transcript JSONL file and extract text messages.
249
+ *
250
+ * Reads from a given line offset (for incremental parsing across calls).
251
+ * Returns extracted messages with role ("user" | "assistant") and the
252
+ * next line index to use for the subsequent call.
253
+ *
254
+ * @param transcriptPath - Absolute path to the transcript JSONL file
255
+ * @param fromLine - 0-based line index to start reading from (default 0)
256
+ * @returns Messages found and the next line index
257
+ */
258
+ export async function parseTranscriptTexts(
259
+ transcriptPath: string,
260
+ fromLine = 0,
261
+ ): Promise<{ messages: TranscriptMessage[]; nextLine: number }> {
262
+ const fileText = await readFile(transcriptPath, "utf-8");
263
+ const lines = fileText.split("\n");
264
+
265
+ // When a JSONL file ends with \n, split() produces a trailing empty element.
266
+ // Exclude it from the watermark so that appended lines at that position are
267
+ // correctly processed on the next incremental call.
268
+ const lastLine = lines[lines.length - 1] ?? "";
269
+ const nextLine = lastLine.trim().length === 0 ? lines.length - 1 : lines.length;
270
+
271
+ const messages: TranscriptMessage[] = [];
272
+
273
+ for (let i = fromLine; i < lines.length; i++) {
274
+ const trimmed = lines[i]?.trim() ?? "";
275
+ if (trimmed.length === 0) continue;
276
+
277
+ let parsed: unknown;
278
+ try {
279
+ parsed = JSON.parse(trimmed);
280
+ } catch {
281
+ continue;
282
+ }
283
+
284
+ if (typeof parsed !== "object" || parsed === null) continue;
285
+ const obj = parsed as Record<string, unknown>;
286
+
287
+ if (obj.type === "assistant") {
288
+ const text = extractAssistantText(parsed);
289
+ if (text !== null) {
290
+ messages.push({ role: "assistant", text });
291
+ }
292
+ } else if (obj.type === "human") {
293
+ const text = extractHumanText(parsed);
294
+ if (text !== null) {
295
+ messages.push({ role: "user", text });
296
+ }
297
+ }
298
+ }
299
+
300
+ return { messages, nextLine };
301
+ }
302
+
303
+ /**
304
+ * Parse a Claude Code transcript JSONL file and aggregate token usage.
305
+ *
306
+ * Reads the file line by line, extracting usage data from each assistant
307
+ * entry. Returns aggregated totals and the model from the first assistant turn.
308
+ *
309
+ * @param transcriptPath - Absolute path to the transcript JSONL file
310
+ * @returns Aggregated usage data across all assistant turns
311
+ */
312
+ export async function parseTranscriptUsage(transcriptPath: string): Promise<TranscriptUsage> {
313
+ const text = await readFile(transcriptPath, "utf-8");
314
+ const lines = text.split("\n");
315
+
316
+ const result: TranscriptUsage = {
317
+ inputTokens: 0,
318
+ outputTokens: 0,
319
+ cacheReadTokens: 0,
320
+ cacheCreationTokens: 0,
321
+ modelUsed: null,
322
+ };
323
+
324
+ for (const line of lines) {
325
+ const trimmed = line.trim();
326
+ if (trimmed.length === 0) continue;
327
+
328
+ let parsed: unknown;
329
+ try {
330
+ parsed = JSON.parse(trimmed);
331
+ } catch {
332
+ // Skip malformed lines
333
+ continue;
334
+ }
335
+
336
+ const usage = extractUsageFromEntry(parsed);
337
+ if (usage === null) continue;
338
+
339
+ result.inputTokens += usage.inputTokens;
340
+ result.outputTokens += usage.outputTokens;
341
+ result.cacheReadTokens += usage.cacheReadTokens;
342
+ result.cacheCreationTokens += usage.cacheCreationTokens;
343
+
344
+ // Capture model from first assistant turn
345
+ if (result.modelUsed === null && usage.model !== undefined) {
346
+ result.modelUsed = usage.model;
347
+ }
348
+ }
349
+
350
+ return result;
351
+ }