@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,445 @@
1
+ /**
2
+ * SQLite-backed metrics storage for agent session data.
3
+ *
4
+ * Uses bun:sqlite for zero-dependency, synchronous database access.
5
+ * All operations are sync — no async/await needed.
6
+ */
7
+
8
+ import { Database } from "bun:sqlite";
9
+ import type { SessionMetrics, TokenSnapshot } from "../types.ts";
10
+
11
+ export interface MetricsStore {
12
+ recordSession(metrics: SessionMetrics): void;
13
+ getRecentSessions(limit?: number): SessionMetrics[];
14
+ getSessionsByAgent(agentName: string): SessionMetrics[];
15
+ getSessionsByRun(runId: string): SessionMetrics[];
16
+ getAverageDuration(capability?: string): number;
17
+ /** Delete metrics matching the given criteria. Returns the number of rows deleted. */
18
+ purge(options: { all?: boolean; agent?: string }): number;
19
+ /** Record a token usage snapshot for a running agent. */
20
+ recordSnapshot(snapshot: TokenSnapshot): void;
21
+ /** Get the most recent snapshot per active agent (one row per agent). */
22
+ getLatestSnapshots(): TokenSnapshot[];
23
+ /** Get the timestamp of the most recent snapshot for an agent, or null. */
24
+ getLatestSnapshotTime(agentName: string): string | null;
25
+ /** Delete snapshots matching criteria. Returns number of rows deleted. */
26
+ purgeSnapshots(options: { all?: boolean; agent?: string; olderThanMs?: number }): number;
27
+ close(): void;
28
+ }
29
+
30
+ /** Row shape as stored in SQLite (snake_case columns). */
31
+ interface SessionRow {
32
+ agent_name: string;
33
+ task_id: string;
34
+ capability: string;
35
+ started_at: string;
36
+ completed_at: string | null;
37
+ duration_ms: number;
38
+ exit_code: number | null;
39
+ merge_result: string | null;
40
+ parent_agent: string | null;
41
+ input_tokens: number;
42
+ output_tokens: number;
43
+ cache_read_tokens: number;
44
+ cache_creation_tokens: number;
45
+ estimated_cost_usd: number | null;
46
+ model_used: string | null;
47
+ run_id: string | null;
48
+ }
49
+
50
+ /** Snapshot row shape as stored in SQLite (snake_case columns). */
51
+ interface SnapshotRow {
52
+ id: number;
53
+ agent_name: string;
54
+ input_tokens: number;
55
+ output_tokens: number;
56
+ cache_read_tokens: number;
57
+ cache_creation_tokens: number;
58
+ estimated_cost_usd: number | null;
59
+ model_used: string | null;
60
+ created_at: string;
61
+ }
62
+
63
+ const CREATE_TABLE = `
64
+ CREATE TABLE IF NOT EXISTS sessions (
65
+ agent_name TEXT NOT NULL,
66
+ task_id TEXT NOT NULL,
67
+ capability TEXT NOT NULL,
68
+ started_at TEXT NOT NULL,
69
+ completed_at TEXT,
70
+ duration_ms INTEGER NOT NULL DEFAULT 0,
71
+ exit_code INTEGER,
72
+ merge_result TEXT,
73
+ parent_agent TEXT,
74
+ input_tokens INTEGER NOT NULL DEFAULT 0,
75
+ output_tokens INTEGER NOT NULL DEFAULT 0,
76
+ cache_read_tokens INTEGER NOT NULL DEFAULT 0,
77
+ cache_creation_tokens INTEGER NOT NULL DEFAULT 0,
78
+ estimated_cost_usd REAL,
79
+ model_used TEXT,
80
+ run_id TEXT,
81
+ PRIMARY KEY (agent_name, task_id)
82
+ )`;
83
+
84
+ const CREATE_SNAPSHOTS_TABLE = `
85
+ CREATE TABLE IF NOT EXISTS token_snapshots (
86
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
87
+ agent_name TEXT NOT NULL,
88
+ input_tokens INTEGER NOT NULL DEFAULT 0,
89
+ output_tokens INTEGER NOT NULL DEFAULT 0,
90
+ cache_read_tokens INTEGER NOT NULL DEFAULT 0,
91
+ cache_creation_tokens INTEGER NOT NULL DEFAULT 0,
92
+ estimated_cost_usd REAL,
93
+ model_used TEXT,
94
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%f','now'))
95
+ )`;
96
+
97
+ const CREATE_SNAPSHOTS_INDEX = `
98
+ CREATE INDEX IF NOT EXISTS idx_snapshots_agent_time
99
+ ON token_snapshots(agent_name, created_at)
100
+ `;
101
+
102
+ /** Token columns added in the token instrumentation migration. */
103
+ const TOKEN_COLUMNS = [
104
+ { name: "input_tokens", ddl: "INTEGER NOT NULL DEFAULT 0" },
105
+ { name: "output_tokens", ddl: "INTEGER NOT NULL DEFAULT 0" },
106
+ { name: "cache_read_tokens", ddl: "INTEGER NOT NULL DEFAULT 0" },
107
+ { name: "cache_creation_tokens", ddl: "INTEGER NOT NULL DEFAULT 0" },
108
+ { name: "estimated_cost_usd", ddl: "REAL" },
109
+ { name: "model_used", ddl: "TEXT" },
110
+ ] as const;
111
+
112
+ /**
113
+ * Migrate an existing sessions table from bead_id to task_id column.
114
+ * Safe to call multiple times — only renames if bead_id exists and task_id does not.
115
+ */
116
+ function migrateBeadIdToTaskId(db: Database): void {
117
+ const rows = db.prepare("PRAGMA table_info(sessions)").all() as Array<{ name: string }>;
118
+ const existingColumns = new Set(rows.map((r) => r.name));
119
+ if (existingColumns.has("bead_id") && !existingColumns.has("task_id")) {
120
+ db.exec("ALTER TABLE sessions RENAME COLUMN bead_id TO task_id");
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Migrate an existing sessions table to include the run_id column.
126
+ * Safe to call multiple times — only adds the column if missing.
127
+ */
128
+ function migrateRunIdColumn(db: Database): void {
129
+ const rows = db.prepare("PRAGMA table_info(sessions)").all() as Array<{ name: string }>;
130
+ const existingColumns = new Set(rows.map((r) => r.name));
131
+ if (!existingColumns.has("run_id")) {
132
+ db.exec("ALTER TABLE sessions ADD COLUMN run_id TEXT");
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Migrate an existing sessions table to include token columns.
138
+ * Safe to call multiple times — only adds columns that are missing.
139
+ */
140
+ function migrateTokenColumns(db: Database): void {
141
+ const rows = db.prepare("PRAGMA table_info(sessions)").all() as Array<{ name: string }>;
142
+ const existingColumns = new Set(rows.map((r) => r.name));
143
+
144
+ for (const col of TOKEN_COLUMNS) {
145
+ if (!existingColumns.has(col.name)) {
146
+ db.exec(`ALTER TABLE sessions ADD COLUMN ${col.name} ${col.ddl}`);
147
+ }
148
+ }
149
+ }
150
+
151
+ /** Convert a database row (snake_case) to a SessionMetrics object (camelCase). */
152
+ function rowToMetrics(row: SessionRow): SessionMetrics {
153
+ return {
154
+ agentName: row.agent_name,
155
+ beadId: row.task_id,
156
+ capability: row.capability,
157
+ startedAt: row.started_at,
158
+ completedAt: row.completed_at,
159
+ durationMs: row.duration_ms,
160
+ exitCode: row.exit_code,
161
+ mergeResult: row.merge_result as SessionMetrics["mergeResult"],
162
+ parentAgent: row.parent_agent,
163
+ inputTokens: row.input_tokens,
164
+ outputTokens: row.output_tokens,
165
+ cacheReadTokens: row.cache_read_tokens,
166
+ cacheCreationTokens: row.cache_creation_tokens,
167
+ estimatedCostUsd: row.estimated_cost_usd,
168
+ modelUsed: row.model_used,
169
+ runId: row.run_id,
170
+ };
171
+ }
172
+
173
+ /** Convert a database snapshot row (snake_case) to a TokenSnapshot object (camelCase). */
174
+ function rowToSnapshot(row: SnapshotRow): TokenSnapshot {
175
+ return {
176
+ agentName: row.agent_name,
177
+ inputTokens: row.input_tokens,
178
+ outputTokens: row.output_tokens,
179
+ cacheReadTokens: row.cache_read_tokens,
180
+ cacheCreationTokens: row.cache_creation_tokens,
181
+ estimatedCostUsd: row.estimated_cost_usd,
182
+ modelUsed: row.model_used,
183
+ createdAt: row.created_at,
184
+ };
185
+ }
186
+
187
+ /**
188
+ * Create a new MetricsStore backed by a SQLite database at the given path.
189
+ *
190
+ * Initializes the database with WAL mode and a 5-second busy timeout.
191
+ * Creates the sessions table if it does not already exist.
192
+ * Migrates existing tables to add token columns if missing.
193
+ */
194
+ export function createMetricsStore(dbPath: string): MetricsStore {
195
+ const db = new Database(dbPath);
196
+
197
+ // Configure for concurrent access
198
+ db.exec("PRAGMA journal_mode = WAL");
199
+ db.exec("PRAGMA busy_timeout = 5000");
200
+
201
+ // Create schema
202
+ db.exec(CREATE_TABLE);
203
+ db.exec(CREATE_SNAPSHOTS_TABLE);
204
+ db.exec(CREATE_SNAPSHOTS_INDEX);
205
+
206
+ // Migrate: rename bead_id → task_id, add token columns and run_id column to existing tables
207
+ migrateBeadIdToTaskId(db);
208
+ migrateTokenColumns(db);
209
+ migrateRunIdColumn(db);
210
+
211
+ // Prepare statements for all queries
212
+ const insertStmt = db.prepare<
213
+ void,
214
+ {
215
+ $agent_name: string;
216
+ $task_id: string;
217
+ $capability: string;
218
+ $started_at: string;
219
+ $completed_at: string | null;
220
+ $duration_ms: number;
221
+ $exit_code: number | null;
222
+ $merge_result: string | null;
223
+ $parent_agent: string | null;
224
+ $input_tokens: number;
225
+ $output_tokens: number;
226
+ $cache_read_tokens: number;
227
+ $cache_creation_tokens: number;
228
+ $estimated_cost_usd: number | null;
229
+ $model_used: string | null;
230
+ $run_id: string | null;
231
+ }
232
+ >(`
233
+ INSERT OR REPLACE INTO sessions
234
+ (agent_name, task_id, capability, started_at, completed_at, duration_ms, exit_code, merge_result, parent_agent, input_tokens, output_tokens, cache_read_tokens, cache_creation_tokens, estimated_cost_usd, model_used, run_id)
235
+ VALUES
236
+ ($agent_name, $task_id, $capability, $started_at, $completed_at, $duration_ms, $exit_code, $merge_result, $parent_agent, $input_tokens, $output_tokens, $cache_read_tokens, $cache_creation_tokens, $estimated_cost_usd, $model_used, $run_id)
237
+ `);
238
+
239
+ const recentStmt = db.prepare<SessionRow, { $limit: number }>(`
240
+ SELECT * FROM sessions ORDER BY started_at DESC LIMIT $limit
241
+ `);
242
+
243
+ const byAgentStmt = db.prepare<SessionRow, { $agent_name: string }>(`
244
+ SELECT * FROM sessions WHERE agent_name = $agent_name ORDER BY started_at DESC
245
+ `);
246
+
247
+ const byRunStmt = db.prepare<SessionRow, { $run_id: string }>(`
248
+ SELECT * FROM sessions WHERE run_id = $run_id ORDER BY started_at DESC
249
+ `);
250
+
251
+ const avgDurationAllStmt = db.prepare<{ avg_duration: number | null }, Record<string, never>>(`
252
+ SELECT AVG(duration_ms) AS avg_duration FROM sessions WHERE completed_at IS NOT NULL
253
+ `);
254
+
255
+ const avgDurationByCapStmt = db.prepare<
256
+ { avg_duration: number | null },
257
+ { $capability: string }
258
+ >(`
259
+ SELECT AVG(duration_ms) AS avg_duration FROM sessions
260
+ WHERE completed_at IS NOT NULL AND capability = $capability
261
+ `);
262
+
263
+ // Snapshot prepared statements
264
+ const insertSnapshotStmt = db.prepare<
265
+ void,
266
+ {
267
+ $agent_name: string;
268
+ $input_tokens: number;
269
+ $output_tokens: number;
270
+ $cache_read_tokens: number;
271
+ $cache_creation_tokens: number;
272
+ $estimated_cost_usd: number | null;
273
+ $model_used: string | null;
274
+ $created_at: string;
275
+ }
276
+ >(`
277
+ INSERT INTO token_snapshots
278
+ (agent_name, input_tokens, output_tokens, cache_read_tokens, cache_creation_tokens, estimated_cost_usd, model_used, created_at)
279
+ VALUES
280
+ ($agent_name, $input_tokens, $output_tokens, $cache_read_tokens, $cache_creation_tokens, $estimated_cost_usd, $model_used, $created_at)
281
+ `);
282
+
283
+ const latestSnapshotsStmt = db.prepare<SnapshotRow, Record<string, never>>(`
284
+ SELECT s.*
285
+ FROM token_snapshots s
286
+ INNER JOIN (
287
+ SELECT agent_name, MAX(created_at) as max_created_at
288
+ FROM token_snapshots
289
+ GROUP BY agent_name
290
+ ) latest ON s.agent_name = latest.agent_name AND s.created_at = latest.max_created_at
291
+ `);
292
+
293
+ const latestSnapshotTimeStmt = db.prepare<
294
+ { created_at: string } | null,
295
+ { $agent_name: string }
296
+ >(`
297
+ SELECT MAX(created_at) as created_at
298
+ FROM token_snapshots
299
+ WHERE agent_name = $agent_name
300
+ `);
301
+
302
+ return {
303
+ recordSession(metrics: SessionMetrics): void {
304
+ insertStmt.run({
305
+ $agent_name: metrics.agentName,
306
+ $task_id: metrics.beadId,
307
+ $capability: metrics.capability,
308
+ $started_at: metrics.startedAt,
309
+ $completed_at: metrics.completedAt,
310
+ $duration_ms: metrics.durationMs,
311
+ $exit_code: metrics.exitCode,
312
+ $merge_result: metrics.mergeResult,
313
+ $parent_agent: metrics.parentAgent,
314
+ $input_tokens: metrics.inputTokens,
315
+ $output_tokens: metrics.outputTokens,
316
+ $cache_read_tokens: metrics.cacheReadTokens,
317
+ $cache_creation_tokens: metrics.cacheCreationTokens,
318
+ $estimated_cost_usd: metrics.estimatedCostUsd,
319
+ $model_used: metrics.modelUsed,
320
+ $run_id: metrics.runId,
321
+ });
322
+ },
323
+
324
+ getRecentSessions(limit = 20): SessionMetrics[] {
325
+ const rows = recentStmt.all({ $limit: limit });
326
+ return rows.map(rowToMetrics);
327
+ },
328
+
329
+ getSessionsByAgent(agentName: string): SessionMetrics[] {
330
+ const rows = byAgentStmt.all({ $agent_name: agentName });
331
+ return rows.map(rowToMetrics);
332
+ },
333
+
334
+ getSessionsByRun(runId: string): SessionMetrics[] {
335
+ const rows = byRunStmt.all({ $run_id: runId });
336
+ return rows.map(rowToMetrics);
337
+ },
338
+
339
+ getAverageDuration(capability?: string): number {
340
+ if (capability !== undefined) {
341
+ const row = avgDurationByCapStmt.get({ $capability: capability });
342
+ return row?.avg_duration ?? 0;
343
+ }
344
+ const row = avgDurationAllStmt.get({});
345
+ return row?.avg_duration ?? 0;
346
+ },
347
+
348
+ purge(options: { all?: boolean; agent?: string }): number {
349
+ if (options.all) {
350
+ const countRow = db
351
+ .prepare<{ cnt: number }, []>("SELECT COUNT(*) as cnt FROM sessions")
352
+ .get();
353
+ const count = countRow?.cnt ?? 0;
354
+ db.prepare("DELETE FROM sessions").run();
355
+ return count;
356
+ }
357
+
358
+ if (options.agent !== undefined) {
359
+ const countRow = db
360
+ .prepare<{ cnt: number }, { $agent: string }>(
361
+ "SELECT COUNT(*) as cnt FROM sessions WHERE agent_name = $agent",
362
+ )
363
+ .get({ $agent: options.agent });
364
+ const count = countRow?.cnt ?? 0;
365
+ db.prepare<void, { $agent: string }>("DELETE FROM sessions WHERE agent_name = $agent").run({
366
+ $agent: options.agent,
367
+ });
368
+ return count;
369
+ }
370
+
371
+ return 0;
372
+ },
373
+
374
+ recordSnapshot(snapshot: TokenSnapshot): void {
375
+ insertSnapshotStmt.run({
376
+ $agent_name: snapshot.agentName,
377
+ $input_tokens: snapshot.inputTokens,
378
+ $output_tokens: snapshot.outputTokens,
379
+ $cache_read_tokens: snapshot.cacheReadTokens,
380
+ $cache_creation_tokens: snapshot.cacheCreationTokens,
381
+ $estimated_cost_usd: snapshot.estimatedCostUsd,
382
+ $model_used: snapshot.modelUsed,
383
+ $created_at: snapshot.createdAt,
384
+ });
385
+ },
386
+
387
+ getLatestSnapshots(): TokenSnapshot[] {
388
+ const rows = latestSnapshotsStmt.all({});
389
+ return rows.map(rowToSnapshot);
390
+ },
391
+
392
+ getLatestSnapshotTime(agentName: string): string | null {
393
+ const row = latestSnapshotTimeStmt.get({ $agent_name: agentName });
394
+ return row?.created_at ?? null;
395
+ },
396
+
397
+ purgeSnapshots(options: { all?: boolean; agent?: string; olderThanMs?: number }): number {
398
+ if (options.all) {
399
+ const countRow = db
400
+ .prepare<{ cnt: number }, []>("SELECT COUNT(*) as cnt FROM token_snapshots")
401
+ .get();
402
+ const count = countRow?.cnt ?? 0;
403
+ db.prepare("DELETE FROM token_snapshots").run();
404
+ return count;
405
+ }
406
+
407
+ if (options.agent !== undefined) {
408
+ const countRow = db
409
+ .prepare<{ cnt: number }, { $agent: string }>(
410
+ "SELECT COUNT(*) as cnt FROM token_snapshots WHERE agent_name = $agent",
411
+ )
412
+ .get({ $agent: options.agent });
413
+ const count = countRow?.cnt ?? 0;
414
+ db.prepare<void, { $agent: string }>(
415
+ "DELETE FROM token_snapshots WHERE agent_name = $agent",
416
+ ).run({
417
+ $agent: options.agent,
418
+ });
419
+ return count;
420
+ }
421
+
422
+ if (options.olderThanMs !== undefined) {
423
+ const cutoffTime = new Date(Date.now() - options.olderThanMs).toISOString();
424
+ const countRow = db
425
+ .prepare<{ cnt: number }, { $cutoff: string }>(
426
+ "SELECT COUNT(*) as cnt FROM token_snapshots WHERE created_at < $cutoff",
427
+ )
428
+ .get({ $cutoff: cutoffTime });
429
+ const count = countRow?.cnt ?? 0;
430
+ db.prepare<void, { $cutoff: string }>(
431
+ "DELETE FROM token_snapshots WHERE created_at < $cutoff",
432
+ ).run({
433
+ $cutoff: cutoffTime,
434
+ });
435
+ return count;
436
+ }
437
+
438
+ return 0;
439
+ },
440
+
441
+ close(): void {
442
+ db.close();
443
+ },
444
+ };
445
+ }