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