@os-eco/overstory-cli 0.6.1

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 (170) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +381 -0
  3. package/agents/builder.md +137 -0
  4. package/agents/coordinator.md +263 -0
  5. package/agents/lead.md +301 -0
  6. package/agents/merger.md +160 -0
  7. package/agents/monitor.md +214 -0
  8. package/agents/reviewer.md +140 -0
  9. package/agents/scout.md +119 -0
  10. package/agents/supervisor.md +423 -0
  11. package/package.json +47 -0
  12. package/src/agents/checkpoint.test.ts +88 -0
  13. package/src/agents/checkpoint.ts +101 -0
  14. package/src/agents/hooks-deployer.test.ts +2040 -0
  15. package/src/agents/hooks-deployer.ts +607 -0
  16. package/src/agents/identity.test.ts +603 -0
  17. package/src/agents/identity.ts +384 -0
  18. package/src/agents/lifecycle.test.ts +196 -0
  19. package/src/agents/lifecycle.ts +183 -0
  20. package/src/agents/manifest.test.ts +746 -0
  21. package/src/agents/manifest.ts +354 -0
  22. package/src/agents/overlay.test.ts +676 -0
  23. package/src/agents/overlay.ts +308 -0
  24. package/src/beads/client.test.ts +217 -0
  25. package/src/beads/client.ts +202 -0
  26. package/src/beads/molecules.test.ts +338 -0
  27. package/src/beads/molecules.ts +198 -0
  28. package/src/commands/agents.test.ts +322 -0
  29. package/src/commands/agents.ts +287 -0
  30. package/src/commands/clean.test.ts +670 -0
  31. package/src/commands/clean.ts +618 -0
  32. package/src/commands/completions.test.ts +342 -0
  33. package/src/commands/completions.ts +887 -0
  34. package/src/commands/coordinator.test.ts +1530 -0
  35. package/src/commands/coordinator.ts +733 -0
  36. package/src/commands/costs.test.ts +1119 -0
  37. package/src/commands/costs.ts +564 -0
  38. package/src/commands/dashboard.test.ts +308 -0
  39. package/src/commands/dashboard.ts +838 -0
  40. package/src/commands/doctor.test.ts +294 -0
  41. package/src/commands/doctor.ts +213 -0
  42. package/src/commands/errors.test.ts +647 -0
  43. package/src/commands/errors.ts +248 -0
  44. package/src/commands/feed.test.ts +578 -0
  45. package/src/commands/feed.ts +361 -0
  46. package/src/commands/group.test.ts +262 -0
  47. package/src/commands/group.ts +511 -0
  48. package/src/commands/hooks.test.ts +458 -0
  49. package/src/commands/hooks.ts +253 -0
  50. package/src/commands/init.test.ts +347 -0
  51. package/src/commands/init.ts +650 -0
  52. package/src/commands/inspect.test.ts +670 -0
  53. package/src/commands/inspect.ts +431 -0
  54. package/src/commands/log.test.ts +1454 -0
  55. package/src/commands/log.ts +724 -0
  56. package/src/commands/logs.test.ts +379 -0
  57. package/src/commands/logs.ts +546 -0
  58. package/src/commands/mail.test.ts +1270 -0
  59. package/src/commands/mail.ts +771 -0
  60. package/src/commands/merge.test.ts +670 -0
  61. package/src/commands/merge.ts +355 -0
  62. package/src/commands/metrics.test.ts +444 -0
  63. package/src/commands/metrics.ts +143 -0
  64. package/src/commands/monitor.test.ts +191 -0
  65. package/src/commands/monitor.ts +390 -0
  66. package/src/commands/nudge.test.ts +230 -0
  67. package/src/commands/nudge.ts +372 -0
  68. package/src/commands/prime.test.ts +470 -0
  69. package/src/commands/prime.ts +381 -0
  70. package/src/commands/replay.test.ts +741 -0
  71. package/src/commands/replay.ts +360 -0
  72. package/src/commands/run.test.ts +431 -0
  73. package/src/commands/run.ts +351 -0
  74. package/src/commands/sling.test.ts +657 -0
  75. package/src/commands/sling.ts +661 -0
  76. package/src/commands/spec.test.ts +203 -0
  77. package/src/commands/spec.ts +168 -0
  78. package/src/commands/status.test.ts +430 -0
  79. package/src/commands/status.ts +398 -0
  80. package/src/commands/stop.test.ts +420 -0
  81. package/src/commands/stop.ts +151 -0
  82. package/src/commands/supervisor.test.ts +187 -0
  83. package/src/commands/supervisor.ts +535 -0
  84. package/src/commands/trace.test.ts +745 -0
  85. package/src/commands/trace.ts +325 -0
  86. package/src/commands/watch.test.ts +145 -0
  87. package/src/commands/watch.ts +247 -0
  88. package/src/commands/worktree.test.ts +786 -0
  89. package/src/commands/worktree.ts +311 -0
  90. package/src/config.test.ts +822 -0
  91. package/src/config.ts +829 -0
  92. package/src/doctor/agents.test.ts +454 -0
  93. package/src/doctor/agents.ts +396 -0
  94. package/src/doctor/config-check.test.ts +190 -0
  95. package/src/doctor/config-check.ts +183 -0
  96. package/src/doctor/consistency.test.ts +651 -0
  97. package/src/doctor/consistency.ts +294 -0
  98. package/src/doctor/databases.test.ts +290 -0
  99. package/src/doctor/databases.ts +218 -0
  100. package/src/doctor/dependencies.test.ts +184 -0
  101. package/src/doctor/dependencies.ts +175 -0
  102. package/src/doctor/logs.test.ts +251 -0
  103. package/src/doctor/logs.ts +295 -0
  104. package/src/doctor/merge-queue.test.ts +216 -0
  105. package/src/doctor/merge-queue.ts +144 -0
  106. package/src/doctor/structure.test.ts +291 -0
  107. package/src/doctor/structure.ts +198 -0
  108. package/src/doctor/types.ts +37 -0
  109. package/src/doctor/version.test.ts +136 -0
  110. package/src/doctor/version.ts +129 -0
  111. package/src/e2e/init-sling-lifecycle.test.ts +277 -0
  112. package/src/errors.ts +217 -0
  113. package/src/events/store.test.ts +660 -0
  114. package/src/events/store.ts +369 -0
  115. package/src/events/tool-filter.test.ts +330 -0
  116. package/src/events/tool-filter.ts +126 -0
  117. package/src/index.ts +316 -0
  118. package/src/insights/analyzer.test.ts +466 -0
  119. package/src/insights/analyzer.ts +203 -0
  120. package/src/logging/color.test.ts +142 -0
  121. package/src/logging/color.ts +71 -0
  122. package/src/logging/logger.test.ts +813 -0
  123. package/src/logging/logger.ts +266 -0
  124. package/src/logging/reporter.test.ts +259 -0
  125. package/src/logging/reporter.ts +109 -0
  126. package/src/logging/sanitizer.test.ts +190 -0
  127. package/src/logging/sanitizer.ts +57 -0
  128. package/src/mail/broadcast.test.ts +203 -0
  129. package/src/mail/broadcast.ts +92 -0
  130. package/src/mail/client.test.ts +773 -0
  131. package/src/mail/client.ts +223 -0
  132. package/src/mail/store.test.ts +705 -0
  133. package/src/mail/store.ts +387 -0
  134. package/src/merge/queue.test.ts +359 -0
  135. package/src/merge/queue.ts +231 -0
  136. package/src/merge/resolver.test.ts +1345 -0
  137. package/src/merge/resolver.ts +645 -0
  138. package/src/metrics/store.test.ts +667 -0
  139. package/src/metrics/store.ts +445 -0
  140. package/src/metrics/summary.test.ts +398 -0
  141. package/src/metrics/summary.ts +178 -0
  142. package/src/metrics/transcript.test.ts +356 -0
  143. package/src/metrics/transcript.ts +175 -0
  144. package/src/mulch/client.test.ts +671 -0
  145. package/src/mulch/client.ts +332 -0
  146. package/src/sessions/compat.test.ts +280 -0
  147. package/src/sessions/compat.ts +104 -0
  148. package/src/sessions/store.test.ts +873 -0
  149. package/src/sessions/store.ts +494 -0
  150. package/src/test-helpers.test.ts +124 -0
  151. package/src/test-helpers.ts +126 -0
  152. package/src/tracker/beads.ts +56 -0
  153. package/src/tracker/factory.test.ts +80 -0
  154. package/src/tracker/factory.ts +64 -0
  155. package/src/tracker/seeds.ts +182 -0
  156. package/src/tracker/types.ts +52 -0
  157. package/src/types.ts +724 -0
  158. package/src/watchdog/daemon.test.ts +1975 -0
  159. package/src/watchdog/daemon.ts +671 -0
  160. package/src/watchdog/health.test.ts +431 -0
  161. package/src/watchdog/health.ts +264 -0
  162. package/src/watchdog/triage.test.ts +164 -0
  163. package/src/watchdog/triage.ts +179 -0
  164. package/src/worktree/manager.test.ts +439 -0
  165. package/src/worktree/manager.ts +198 -0
  166. package/src/worktree/tmux.test.ts +1009 -0
  167. package/src/worktree/tmux.ts +509 -0
  168. package/templates/CLAUDE.md.tmpl +89 -0
  169. package/templates/hooks.json.tmpl +105 -0
  170. package/templates/overlay.md.tmpl +81 -0
@@ -0,0 +1,564 @@
1
+ /**
2
+ * CLI command: overstory costs [--agent <name>] [--run <id>] [--by-capability] [--last <n>] [--self] [--json]
3
+ *
4
+ * Shows token/cost analysis and breakdown for agent sessions.
5
+ * Data source: metrics.db via createMetricsStore().
6
+ * Use --self to parse the current orchestrator session's transcript directly.
7
+ */
8
+
9
+ import { readdir, stat } from "node:fs/promises";
10
+ import { join } from "node:path";
11
+ import { loadConfig } from "../config.ts";
12
+ import { ValidationError } from "../errors.ts";
13
+ import { color } from "../logging/color.ts";
14
+ import { createMetricsStore } from "../metrics/store.ts";
15
+ import { estimateCost, parseTranscriptUsage } from "../metrics/transcript.ts";
16
+ import { openSessionStore } from "../sessions/compat.ts";
17
+ import type { SessionMetrics } from "../types.ts";
18
+
19
+ /**
20
+ * Parse a named flag value from args.
21
+ */
22
+ function getFlag(args: string[], flag: string): string | undefined {
23
+ const idx = args.indexOf(flag);
24
+ if (idx === -1 || idx + 1 >= args.length) {
25
+ return undefined;
26
+ }
27
+ return args[idx + 1];
28
+ }
29
+
30
+ function hasFlag(args: string[], flag: string): boolean {
31
+ return args.includes(flag);
32
+ }
33
+
34
+ /** Format a number with thousands separators (e.g., 12345 -> "12,345"). */
35
+ function formatNumber(n: number): string {
36
+ return n.toLocaleString("en-US");
37
+ }
38
+
39
+ /** Format a cost value as "$X.XX". Returns "$0.00" for null/undefined. */
40
+ function formatCost(cost: number | null): string {
41
+ if (cost === null || cost === undefined) {
42
+ return "$0.00";
43
+ }
44
+ return `$${cost.toFixed(2)}`;
45
+ }
46
+
47
+ /** Right-pad a string to the given width. */
48
+ function padRight(str: string, width: number): string {
49
+ return str.length >= width ? str : str + " ".repeat(width - str.length);
50
+ }
51
+
52
+ /** Left-pad a string to the given width. */
53
+ function padLeft(str: string, width: number): string {
54
+ return str.length >= width ? str : " ".repeat(width - str.length) + str;
55
+ }
56
+
57
+ /**
58
+ * Discover the orchestrator's Claude Code transcript JSONL file.
59
+ *
60
+ * Scans ~/.claude/projects/{project-key}/ for JSONL files and returns
61
+ * the most recently modified one, corresponding to the current orchestrator session.
62
+ *
63
+ * @param projectRoot - Absolute path to the project root
64
+ * @returns Absolute path to the most recent transcript, or null if none found
65
+ */
66
+ async function discoverOrchestratorTranscript(projectRoot: string): Promise<string | null> {
67
+ const homeDir = process.env.HOME ?? "";
68
+ if (homeDir.length === 0) return null;
69
+
70
+ const projectKey = projectRoot.replace(/\//g, "-");
71
+ const projectDir = join(homeDir, ".claude", "projects", projectKey);
72
+
73
+ let entries: string[];
74
+ try {
75
+ entries = await readdir(projectDir);
76
+ } catch {
77
+ return null;
78
+ }
79
+
80
+ const jsonlFiles = entries.filter((e) => e.endsWith(".jsonl"));
81
+ if (jsonlFiles.length === 0) return null;
82
+
83
+ let bestPath: string | null = null;
84
+ let bestMtime = 0;
85
+
86
+ for (const file of jsonlFiles) {
87
+ const filePath = join(projectDir, file);
88
+ try {
89
+ const fileStat = await stat(filePath);
90
+ if (fileStat.mtimeMs > bestMtime) {
91
+ bestMtime = fileStat.mtimeMs;
92
+ bestPath = filePath;
93
+ }
94
+ } catch {
95
+ // Skip files we cannot stat
96
+ }
97
+ }
98
+
99
+ return bestPath;
100
+ }
101
+
102
+ /** Aggregate totals from a list of SessionMetrics. */
103
+ interface Totals {
104
+ inputTokens: number;
105
+ outputTokens: number;
106
+ cacheTokens: number;
107
+ costUsd: number;
108
+ }
109
+
110
+ function computeTotals(sessions: SessionMetrics[]): Totals {
111
+ let inputTokens = 0;
112
+ let outputTokens = 0;
113
+ let cacheTokens = 0;
114
+ let costUsd = 0;
115
+ for (const s of sessions) {
116
+ inputTokens += s.inputTokens;
117
+ outputTokens += s.outputTokens;
118
+ cacheTokens += s.cacheReadTokens + s.cacheCreationTokens;
119
+ costUsd += s.estimatedCostUsd ?? 0;
120
+ }
121
+ return { inputTokens, outputTokens, cacheTokens, costUsd };
122
+ }
123
+
124
+ /** Group SessionMetrics by capability. */
125
+ interface CapabilityGroup {
126
+ capability: string;
127
+ sessions: SessionMetrics[];
128
+ totals: Totals;
129
+ }
130
+
131
+ function groupByCapability(sessions: SessionMetrics[]): CapabilityGroup[] {
132
+ const groups = new Map<string, SessionMetrics[]>();
133
+ for (const s of sessions) {
134
+ const existing = groups.get(s.capability);
135
+ if (existing) {
136
+ existing.push(s);
137
+ } else {
138
+ groups.set(s.capability, [s]);
139
+ }
140
+ }
141
+ const result: CapabilityGroup[] = [];
142
+ for (const [capability, capSessions] of groups) {
143
+ result.push({
144
+ capability,
145
+ sessions: capSessions,
146
+ totals: computeTotals(capSessions),
147
+ });
148
+ }
149
+ // Sort by cost descending
150
+ result.sort((a, b) => b.totals.costUsd - a.totals.costUsd);
151
+ return result;
152
+ }
153
+
154
+ /** Print the standard per-agent cost summary table. */
155
+ function printCostSummary(sessions: SessionMetrics[]): void {
156
+ const w = process.stdout.write.bind(process.stdout);
157
+ const separator = "\u2500".repeat(70);
158
+
159
+ w(`${color.bold}Cost Summary${color.reset}\n`);
160
+ w(`${"=".repeat(70)}\n`);
161
+
162
+ if (sessions.length === 0) {
163
+ w(`${color.dim}No session data found.${color.reset}\n`);
164
+ return;
165
+ }
166
+
167
+ w(
168
+ `${padRight("Agent", 19)}${padRight("Capability", 12)}` +
169
+ `${padLeft("Input", 10)}${padLeft("Output", 10)}` +
170
+ `${padLeft("Cache", 10)}${padLeft("Cost", 10)}\n`,
171
+ );
172
+ w(`${color.dim}${separator}${color.reset}\n`);
173
+
174
+ for (const s of sessions) {
175
+ const cacheTotal = s.cacheReadTokens + s.cacheCreationTokens;
176
+ w(
177
+ `${padRight(s.agentName, 19)}${padRight(s.capability, 12)}` +
178
+ `${padLeft(formatNumber(s.inputTokens), 10)}` +
179
+ `${padLeft(formatNumber(s.outputTokens), 10)}` +
180
+ `${padLeft(formatNumber(cacheTotal), 10)}` +
181
+ `${padLeft(formatCost(s.estimatedCostUsd), 10)}\n`,
182
+ );
183
+ }
184
+
185
+ const totals = computeTotals(sessions);
186
+ w(`${color.dim}${separator}${color.reset}\n`);
187
+ w(
188
+ `${color.green}${color.bold}${padRight("Total", 31)}` +
189
+ `${padLeft(formatNumber(totals.inputTokens), 10)}` +
190
+ `${padLeft(formatNumber(totals.outputTokens), 10)}` +
191
+ `${padLeft(formatNumber(totals.cacheTokens), 10)}` +
192
+ `${padLeft(formatCost(totals.costUsd), 10)}${color.reset}\n`,
193
+ );
194
+ }
195
+
196
+ /** Print the capability-grouped cost table. */
197
+ function printByCapability(sessions: SessionMetrics[]): void {
198
+ const w = process.stdout.write.bind(process.stdout);
199
+ const separator = "\u2500".repeat(70);
200
+
201
+ w(`${color.bold}Cost by Capability${color.reset}\n`);
202
+ w(`${"=".repeat(70)}\n`);
203
+
204
+ if (sessions.length === 0) {
205
+ w(`${color.dim}No session data found.${color.reset}\n`);
206
+ return;
207
+ }
208
+
209
+ w(
210
+ `${padRight("Capability", 14)}${padLeft("Sessions", 10)}` +
211
+ `${padLeft("Input", 10)}${padLeft("Output", 10)}` +
212
+ `${padLeft("Cache", 10)}${padLeft("Cost", 10)}\n`,
213
+ );
214
+ w(`${color.dim}${separator}${color.reset}\n`);
215
+
216
+ const groups = groupByCapability(sessions);
217
+
218
+ for (const group of groups) {
219
+ w(
220
+ `${padRight(group.capability, 14)}` +
221
+ `${padLeft(formatNumber(group.sessions.length), 10)}` +
222
+ `${padLeft(formatNumber(group.totals.inputTokens), 10)}` +
223
+ `${padLeft(formatNumber(group.totals.outputTokens), 10)}` +
224
+ `${padLeft(formatNumber(group.totals.cacheTokens), 10)}` +
225
+ `${padLeft(formatCost(group.totals.costUsd), 10)}\n`,
226
+ );
227
+ }
228
+
229
+ const totals = computeTotals(sessions);
230
+ w(`${color.dim}${separator}${color.reset}\n`);
231
+ w(
232
+ `${color.green}${color.bold}${padRight("Total", 14)}` +
233
+ `${padLeft(formatNumber(sessions.length), 10)}` +
234
+ `${padLeft(formatNumber(totals.inputTokens), 10)}` +
235
+ `${padLeft(formatNumber(totals.outputTokens), 10)}` +
236
+ `${padLeft(formatNumber(totals.cacheTokens), 10)}` +
237
+ `${padLeft(formatCost(totals.costUsd), 10)}${color.reset}\n`,
238
+ );
239
+ }
240
+
241
+ const COSTS_HELP = `overstory costs -- Token/cost analysis and breakdown
242
+
243
+ Usage: overstory costs [options]
244
+
245
+ Options:
246
+ --live Show real-time token usage for active agents
247
+ --self Show cost for the current orchestrator session
248
+ --agent <name> Filter by agent name
249
+ --run <id> Filter by run ID
250
+ --by-capability Group results by capability with subtotals
251
+ --last <n> Number of recent sessions (default: 20)
252
+ --json Output as JSON
253
+ --help, -h Show this help`;
254
+
255
+ /**
256
+ * Entry point for `overstory costs [--agent <name>] [--run <id>] [--by-capability] [--last <n>] [--self] [--json]`.
257
+ */
258
+ export async function costsCommand(args: string[]): Promise<void> {
259
+ if (args.includes("--help") || args.includes("-h")) {
260
+ process.stdout.write(`${COSTS_HELP}\n`);
261
+ return;
262
+ }
263
+
264
+ const json = hasFlag(args, "--json");
265
+ const live = hasFlag(args, "--live");
266
+ const self = hasFlag(args, "--self");
267
+ const byCapability = hasFlag(args, "--by-capability");
268
+ const agentName = getFlag(args, "--agent");
269
+ const runId = getFlag(args, "--run");
270
+ const lastStr = getFlag(args, "--last");
271
+
272
+ if (lastStr !== undefined) {
273
+ const parsed = Number.parseInt(lastStr, 10);
274
+ if (Number.isNaN(parsed) || parsed < 1) {
275
+ throw new ValidationError("--last must be a positive integer", {
276
+ field: "last",
277
+ value: lastStr,
278
+ });
279
+ }
280
+ }
281
+
282
+ const last = lastStr ? Number.parseInt(lastStr, 10) : 20;
283
+
284
+ const cwd = process.cwd();
285
+ const config = await loadConfig(cwd);
286
+ const overstoryDir = join(config.project.root, ".overstory");
287
+
288
+ // Handle --self flag (early return for self-scan)
289
+ if (self) {
290
+ const transcriptPath = await discoverOrchestratorTranscript(config.project.root);
291
+ if (!transcriptPath) {
292
+ if (json) {
293
+ process.stdout.write(
294
+ JSON.stringify({ error: "no_transcript", message: "No orchestrator transcript found" }) +
295
+ "\n",
296
+ );
297
+ } else {
298
+ process.stdout.write(
299
+ "No orchestrator transcript found.\nExpected at: ~/.claude/projects/{project-key}/*.jsonl\n",
300
+ );
301
+ }
302
+ return;
303
+ }
304
+
305
+ const usage = await parseTranscriptUsage(transcriptPath);
306
+ const cost = estimateCost(usage);
307
+ const cacheTotal = usage.cacheReadTokens + usage.cacheCreationTokens;
308
+
309
+ if (json) {
310
+ process.stdout.write(
311
+ `${JSON.stringify({
312
+ source: "self",
313
+ transcriptPath,
314
+ model: usage.modelUsed,
315
+ inputTokens: usage.inputTokens,
316
+ outputTokens: usage.outputTokens,
317
+ cacheReadTokens: usage.cacheReadTokens,
318
+ cacheCreationTokens: usage.cacheCreationTokens,
319
+ estimatedCostUsd: cost,
320
+ })}\n`,
321
+ );
322
+ } else {
323
+ const w = process.stdout.write.bind(process.stdout);
324
+ const separator = "\u2500".repeat(70);
325
+
326
+ w(`${color.bold}Orchestrator Session Cost${color.reset}\n`);
327
+ w(`${"=".repeat(70)}\n`);
328
+ w(`${padRight("Model:", 12)}${usage.modelUsed ?? "unknown"}\n`);
329
+ w(`${padRight("Transcript:", 12)}${transcriptPath}\n`);
330
+ w(`${color.dim}${separator}${color.reset}\n`);
331
+ w(`${padRight("Input tokens:", 22)}${padLeft(formatNumber(usage.inputTokens), 12)}\n`);
332
+ w(`${padRight("Output tokens:", 22)}${padLeft(formatNumber(usage.outputTokens), 12)}\n`);
333
+ w(`${padRight("Cache tokens:", 22)}${padLeft(formatNumber(cacheTotal), 12)}\n`);
334
+ w(`${color.dim}${separator}${color.reset}\n`);
335
+ w(
336
+ `${color.green}${color.bold}${padRight("Estimated cost:", 22)}${padLeft(formatCost(cost), 12)}${color.reset}\n`,
337
+ );
338
+ }
339
+ return;
340
+ }
341
+
342
+ // Handle --live flag (early return for live view)
343
+ if (live) {
344
+ const metricsDbPath = join(overstoryDir, "metrics.db");
345
+ const metricsFile = Bun.file(metricsDbPath);
346
+ if (!(await metricsFile.exists())) {
347
+ if (json) {
348
+ process.stdout.write(
349
+ `${JSON.stringify({ agents: [], totals: { inputTokens: 0, outputTokens: 0, cacheTokens: 0, costUsd: 0, burnRatePerMin: 0, tokensPerMin: 0 } })}\n`,
350
+ );
351
+ } else {
352
+ process.stdout.write(
353
+ "No live data available. Token snapshots begin after first tool call.\n",
354
+ );
355
+ }
356
+ return;
357
+ }
358
+
359
+ const metricsStore = createMetricsStore(metricsDbPath);
360
+ const { store: sessionStore } = openSessionStore(overstoryDir);
361
+
362
+ try {
363
+ const snapshots = metricsStore.getLatestSnapshots();
364
+ if (snapshots.length === 0) {
365
+ if (json) {
366
+ process.stdout.write(
367
+ `${JSON.stringify({ agents: [], totals: { inputTokens: 0, outputTokens: 0, cacheTokens: 0, costUsd: 0, burnRatePerMin: 0, tokensPerMin: 0 } })}\n`,
368
+ );
369
+ } else {
370
+ process.stdout.write(
371
+ "No live data available. Token snapshots begin after first tool call.\n",
372
+ );
373
+ }
374
+ return;
375
+ }
376
+
377
+ // Get active sessions to join with snapshots
378
+ const activeSessions = sessionStore.getActive();
379
+
380
+ // Filter snapshots by agent if --agent is provided
381
+ const filteredSnapshots = agentName
382
+ ? snapshots.filter((s) => s.agentName === agentName)
383
+ : snapshots;
384
+
385
+ // Build agent data with session info
386
+ interface LiveAgentData {
387
+ agentName: string;
388
+ capability: string;
389
+ inputTokens: number;
390
+ outputTokens: number;
391
+ cacheReadTokens: number;
392
+ cacheCreationTokens: number;
393
+ estimatedCostUsd: number;
394
+ modelUsed: string | null;
395
+ snapshotAt: string;
396
+ sessionStartedAt: string;
397
+ elapsedMs: number;
398
+ }
399
+
400
+ const agentData: LiveAgentData[] = [];
401
+ const now = Date.now();
402
+
403
+ for (const snapshot of filteredSnapshots) {
404
+ const session = activeSessions.find((s) => s.agentName === snapshot.agentName);
405
+ if (!session) continue; // Skip inactive agents
406
+
407
+ const startedAt = new Date(session.startedAt).getTime();
408
+ const elapsedMs = now - startedAt;
409
+
410
+ agentData.push({
411
+ agentName: snapshot.agentName,
412
+ capability: session.capability,
413
+ inputTokens: snapshot.inputTokens,
414
+ outputTokens: snapshot.outputTokens,
415
+ cacheReadTokens: snapshot.cacheReadTokens,
416
+ cacheCreationTokens: snapshot.cacheCreationTokens,
417
+ estimatedCostUsd: snapshot.estimatedCostUsd ?? 0,
418
+ modelUsed: snapshot.modelUsed,
419
+ snapshotAt: snapshot.createdAt,
420
+ sessionStartedAt: session.startedAt,
421
+ elapsedMs,
422
+ });
423
+ }
424
+
425
+ // Compute totals
426
+ let totalInput = 0;
427
+ let totalOutput = 0;
428
+ let totalCacheRead = 0;
429
+ let totalCacheCreate = 0;
430
+ let totalCost = 0;
431
+ let totalElapsedMs = 0;
432
+
433
+ for (const agent of agentData) {
434
+ totalInput += agent.inputTokens;
435
+ totalOutput += agent.outputTokens;
436
+ totalCacheRead += agent.cacheReadTokens;
437
+ totalCacheCreate += agent.cacheCreationTokens;
438
+ totalCost += agent.estimatedCostUsd;
439
+ totalElapsedMs += agent.elapsedMs;
440
+ }
441
+
442
+ const avgElapsedMs = agentData.length > 0 ? totalElapsedMs / agentData.length : 0;
443
+ const totalCacheTokens = totalCacheRead + totalCacheCreate;
444
+ const totalTokens = totalInput + totalOutput;
445
+ const burnRatePerMin = avgElapsedMs > 0 ? totalCost / (avgElapsedMs / 60_000) : 0;
446
+ const tokensPerMin = avgElapsedMs > 0 ? totalTokens / (avgElapsedMs / 60_000) : 0;
447
+
448
+ if (json) {
449
+ process.stdout.write(
450
+ `${JSON.stringify({
451
+ agents: agentData,
452
+ totals: {
453
+ inputTokens: totalInput,
454
+ outputTokens: totalOutput,
455
+ cacheTokens: totalCacheTokens,
456
+ costUsd: totalCost,
457
+ burnRatePerMin,
458
+ tokensPerMin,
459
+ },
460
+ })}\n`,
461
+ );
462
+ } else {
463
+ const w = process.stdout.write.bind(process.stdout);
464
+ const separator = "\u2500".repeat(70);
465
+
466
+ w(`${color.bold}Live Token Usage (${agentData.length} active agents)${color.reset}\n`);
467
+ w(`${"=".repeat(70)}\n`);
468
+ w(
469
+ `${padRight("Agent", 19)}${padRight("Capability", 12)}` +
470
+ `${padLeft("Input", 10)}${padLeft("Output", 10)}` +
471
+ `${padLeft("Cache", 10)}${padLeft("Cost", 10)}\n`,
472
+ );
473
+ w(`${color.dim}${separator}${color.reset}\n`);
474
+
475
+ for (const agent of agentData) {
476
+ const cacheTotal = agent.cacheReadTokens + agent.cacheCreationTokens;
477
+ w(
478
+ `${padRight(agent.agentName, 19)}${padRight(agent.capability, 12)}` +
479
+ `${padLeft(formatNumber(agent.inputTokens), 10)}` +
480
+ `${padLeft(formatNumber(agent.outputTokens), 10)}` +
481
+ `${padLeft(formatNumber(cacheTotal), 10)}` +
482
+ `${padLeft(formatCost(agent.estimatedCostUsd), 10)}\n`,
483
+ );
484
+ }
485
+
486
+ w(`${color.dim}${separator}${color.reset}\n`);
487
+ w(
488
+ `${color.green}${color.bold}${padRight("Total", 31)}` +
489
+ `${padLeft(formatNumber(totalInput), 10)}` +
490
+ `${padLeft(formatNumber(totalOutput), 10)}` +
491
+ `${padLeft(formatNumber(totalCacheTokens), 10)}` +
492
+ `${padLeft(formatCost(totalCost), 10)}${color.reset}\n\n`,
493
+ );
494
+
495
+ // Format elapsed time
496
+ const totalElapsedSec = Math.floor(avgElapsedMs / 1000);
497
+ const minutes = Math.floor(totalElapsedSec / 60);
498
+ const seconds = totalElapsedSec % 60;
499
+ const elapsedStr = `${minutes}m ${seconds}s`;
500
+
501
+ w(
502
+ `Burn rate: ${formatCost(burnRatePerMin)}/min | ` +
503
+ `${formatNumber(Math.floor(tokensPerMin))} tokens/min | ` +
504
+ `Elapsed: ${elapsedStr}\n`,
505
+ );
506
+ }
507
+ } finally {
508
+ metricsStore.close();
509
+ sessionStore.close();
510
+ }
511
+ return;
512
+ }
513
+
514
+ // Check if metrics.db exists
515
+ const metricsDbPath = join(overstoryDir, "metrics.db");
516
+ const metricsFile = Bun.file(metricsDbPath);
517
+ if (!(await metricsFile.exists())) {
518
+ if (json) {
519
+ process.stdout.write("[]\n");
520
+ } else {
521
+ process.stdout.write("No metrics data yet.\n");
522
+ }
523
+ return;
524
+ }
525
+
526
+ const metricsStore = createMetricsStore(metricsDbPath);
527
+
528
+ try {
529
+ let sessions: SessionMetrics[];
530
+
531
+ if (agentName !== undefined) {
532
+ sessions = metricsStore.getSessionsByAgent(agentName);
533
+ } else if (runId !== undefined) {
534
+ sessions = metricsStore.getSessionsByRun(runId);
535
+ } else {
536
+ sessions = metricsStore.getRecentSessions(last);
537
+ }
538
+
539
+ if (json) {
540
+ if (byCapability) {
541
+ const groups = groupByCapability(sessions);
542
+ const grouped: Record<string, { sessions: SessionMetrics[]; totals: Totals }> = {};
543
+ for (const group of groups) {
544
+ grouped[group.capability] = {
545
+ sessions: group.sessions,
546
+ totals: group.totals,
547
+ };
548
+ }
549
+ process.stdout.write(`${JSON.stringify(grouped)}\n`);
550
+ } else {
551
+ process.stdout.write(`${JSON.stringify(sessions)}\n`);
552
+ }
553
+ return;
554
+ }
555
+
556
+ if (byCapability) {
557
+ printByCapability(sessions);
558
+ } else {
559
+ printCostSummary(sessions);
560
+ }
561
+ } finally {
562
+ metricsStore.close();
563
+ }
564
+ }