@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.
- package/CHANGELOG.md +422 -0
- package/LICENSE +21 -0
- package/README.md +555 -0
- package/agents/builder.md +141 -0
- package/agents/coordinator.md +351 -0
- package/agents/cto.md +196 -0
- package/agents/gateway.md +276 -0
- package/agents/lead.md +281 -0
- package/agents/merger.md +156 -0
- package/agents/monitor.md +212 -0
- package/agents/reviewer.md +142 -0
- package/agents/scout.md +131 -0
- package/agents/supervisor.md +416 -0
- package/bin/legio.mjs +38 -0
- package/package.json +77 -0
- package/src/agents/checkpoint.test.ts +88 -0
- package/src/agents/checkpoint.ts +102 -0
- package/src/agents/hooks-deployer.test.ts +1820 -0
- package/src/agents/hooks-deployer.ts +574 -0
- package/src/agents/identity.test.ts +614 -0
- package/src/agents/identity.ts +385 -0
- package/src/agents/lifecycle.test.ts +202 -0
- package/src/agents/lifecycle.ts +184 -0
- package/src/agents/manifest.test.ts +558 -0
- package/src/agents/manifest.ts +297 -0
- package/src/agents/overlay.test.ts +592 -0
- package/src/agents/overlay.ts +316 -0
- package/src/beads/client.test.ts +210 -0
- package/src/beads/client.ts +227 -0
- package/src/beads/molecules.test.ts +320 -0
- package/src/beads/molecules.ts +209 -0
- package/src/commands/agents.test.ts +325 -0
- package/src/commands/agents.ts +286 -0
- package/src/commands/clean.test.ts +730 -0
- package/src/commands/clean.ts +653 -0
- package/src/commands/completions.test.ts +346 -0
- package/src/commands/completions.ts +950 -0
- package/src/commands/coordinator.test.ts +1524 -0
- package/src/commands/coordinator.ts +880 -0
- package/src/commands/costs.test.ts +1015 -0
- package/src/commands/costs.ts +473 -0
- package/src/commands/dashboard.test.ts +94 -0
- package/src/commands/dashboard.ts +607 -0
- package/src/commands/doctor.test.ts +295 -0
- package/src/commands/doctor.ts +213 -0
- package/src/commands/down.test.ts +308 -0
- package/src/commands/down.ts +124 -0
- package/src/commands/errors.test.ts +648 -0
- package/src/commands/errors.ts +255 -0
- package/src/commands/feed.test.ts +579 -0
- package/src/commands/feed.ts +368 -0
- package/src/commands/gateway.test.ts +698 -0
- package/src/commands/gateway.ts +419 -0
- package/src/commands/group.test.ts +262 -0
- package/src/commands/group.ts +539 -0
- package/src/commands/hooks.test.ts +292 -0
- package/src/commands/hooks.ts +210 -0
- package/src/commands/init.test.ts +211 -0
- package/src/commands/init.ts +622 -0
- package/src/commands/inspect.test.ts +670 -0
- package/src/commands/inspect.ts +455 -0
- package/src/commands/log.test.ts +1556 -0
- package/src/commands/log.ts +752 -0
- package/src/commands/logs.test.ts +379 -0
- package/src/commands/logs.ts +544 -0
- package/src/commands/mail.test.ts +1726 -0
- package/src/commands/mail.ts +926 -0
- package/src/commands/merge.test.ts +676 -0
- package/src/commands/merge.ts +374 -0
- package/src/commands/metrics.test.ts +444 -0
- package/src/commands/metrics.ts +150 -0
- package/src/commands/monitor.test.ts +151 -0
- package/src/commands/monitor.ts +394 -0
- package/src/commands/nudge.test.ts +230 -0
- package/src/commands/nudge.ts +373 -0
- package/src/commands/prime.test.ts +467 -0
- package/src/commands/prime.ts +386 -0
- package/src/commands/replay.test.ts +742 -0
- package/src/commands/replay.ts +367 -0
- package/src/commands/run.test.ts +443 -0
- package/src/commands/run.ts +365 -0
- package/src/commands/server.test.ts +626 -0
- package/src/commands/server.ts +298 -0
- package/src/commands/sling.test.ts +810 -0
- package/src/commands/sling.ts +700 -0
- package/src/commands/spec.test.ts +206 -0
- package/src/commands/spec.ts +171 -0
- package/src/commands/status.test.ts +276 -0
- package/src/commands/status.ts +339 -0
- package/src/commands/stop.test.ts +357 -0
- package/src/commands/stop.ts +119 -0
- package/src/commands/supervisor.test.ts +186 -0
- package/src/commands/supervisor.ts +544 -0
- package/src/commands/trace.test.ts +746 -0
- package/src/commands/trace.ts +332 -0
- package/src/commands/up.test.ts +597 -0
- package/src/commands/up.ts +275 -0
- package/src/commands/watch.test.ts +152 -0
- package/src/commands/watch.ts +238 -0
- package/src/commands/worktree.test.ts +648 -0
- package/src/commands/worktree.ts +266 -0
- package/src/config.test.ts +496 -0
- package/src/config.ts +616 -0
- package/src/doctor/agents.test.ts +448 -0
- package/src/doctor/agents.ts +396 -0
- package/src/doctor/config-check.test.ts +184 -0
- package/src/doctor/config-check.ts +185 -0
- package/src/doctor/consistency.test.ts +645 -0
- package/src/doctor/consistency.ts +294 -0
- package/src/doctor/databases.test.ts +284 -0
- package/src/doctor/databases.ts +211 -0
- package/src/doctor/dependencies.test.ts +150 -0
- package/src/doctor/dependencies.ts +179 -0
- package/src/doctor/logs.test.ts +244 -0
- package/src/doctor/logs.ts +295 -0
- package/src/doctor/merge-queue.test.ts +210 -0
- package/src/doctor/merge-queue.ts +144 -0
- package/src/doctor/structure.test.ts +285 -0
- package/src/doctor/structure.ts +195 -0
- package/src/doctor/types.ts +37 -0
- package/src/doctor/version.test.ts +130 -0
- package/src/doctor/version.ts +131 -0
- package/src/e2e/chat-flow.test.ts +346 -0
- package/src/e2e/init-sling-lifecycle.test.ts +288 -0
- package/src/errors.test.ts +21 -0
- package/src/errors.ts +246 -0
- package/src/events/store.test.ts +660 -0
- package/src/events/store.ts +344 -0
- package/src/events/tool-filter.test.ts +330 -0
- package/src/events/tool-filter.ts +126 -0
- package/src/global-setup.ts +14 -0
- package/src/index.ts +339 -0
- package/src/insights/analyzer.test.ts +466 -0
- package/src/insights/analyzer.ts +203 -0
- package/src/logging/color.test.ts +118 -0
- package/src/logging/color.ts +71 -0
- package/src/logging/logger.test.ts +812 -0
- package/src/logging/logger.ts +266 -0
- package/src/logging/reporter.test.ts +258 -0
- package/src/logging/reporter.ts +109 -0
- package/src/logging/sanitizer.test.ts +190 -0
- package/src/logging/sanitizer.ts +57 -0
- package/src/mail/broadcast.test.ts +203 -0
- package/src/mail/broadcast.ts +92 -0
- package/src/mail/client.test.ts +873 -0
- package/src/mail/client.ts +236 -0
- package/src/mail/store.test.ts +815 -0
- package/src/mail/store.ts +402 -0
- package/src/merge/queue.test.ts +449 -0
- package/src/merge/queue.ts +262 -0
- package/src/merge/resolver.test.ts +1453 -0
- package/src/merge/resolver.ts +759 -0
- package/src/metrics/store.test.ts +1167 -0
- package/src/metrics/store.ts +511 -0
- package/src/metrics/summary.test.ts +397 -0
- package/src/metrics/summary.ts +178 -0
- package/src/metrics/transcript.test.ts +643 -0
- package/src/metrics/transcript.ts +351 -0
- package/src/mulch/client.test.ts +547 -0
- package/src/mulch/client.ts +416 -0
- package/src/server/audit-store.test.ts +384 -0
- package/src/server/audit-store.ts +257 -0
- package/src/server/headless.test.ts +180 -0
- package/src/server/headless.ts +151 -0
- package/src/server/index.test.ts +241 -0
- package/src/server/index.ts +317 -0
- package/src/server/public/app.js +187 -0
- package/src/server/public/apple-touch-icon.png +0 -0
- package/src/server/public/components/agent-badge.js +37 -0
- package/src/server/public/components/data-table.js +114 -0
- package/src/server/public/components/gateway-chat.js +256 -0
- package/src/server/public/components/issue-card.js +96 -0
- package/src/server/public/components/layout.js +88 -0
- package/src/server/public/components/message-bubble.js +120 -0
- package/src/server/public/components/stat-card.js +26 -0
- package/src/server/public/components/terminal-panel.js +140 -0
- package/src/server/public/favicon-16.png +0 -0
- package/src/server/public/favicon-32.png +0 -0
- package/src/server/public/favicon.ico +0 -0
- package/src/server/public/favicon.png +0 -0
- package/src/server/public/index.html +64 -0
- package/src/server/public/lib/api.js +35 -0
- package/src/server/public/lib/markdown.js +8 -0
- package/src/server/public/lib/preact-setup.js +8 -0
- package/src/server/public/lib/state.js +99 -0
- package/src/server/public/lib/utils.js +309 -0
- package/src/server/public/lib/ws.js +79 -0
- package/src/server/public/views/chat.js +983 -0
- package/src/server/public/views/costs.js +692 -0
- package/src/server/public/views/dashboard.js +781 -0
- package/src/server/public/views/gateway-chat.js +622 -0
- package/src/server/public/views/inspect.js +399 -0
- package/src/server/public/views/issues.js +470 -0
- package/src/server/public/views/setup.js +94 -0
- package/src/server/public/views/task-detail.js +422 -0
- package/src/server/routes.test.ts +3816 -0
- package/src/server/routes.ts +1964 -0
- package/src/server/websocket.test.ts +288 -0
- package/src/server/websocket.ts +196 -0
- package/src/sessions/compat.test.ts +109 -0
- package/src/sessions/compat.ts +17 -0
- package/src/sessions/store.test.ts +969 -0
- package/src/sessions/store.ts +480 -0
- package/src/test-helpers.test.ts +97 -0
- package/src/test-helpers.ts +143 -0
- package/src/types.ts +708 -0
- package/src/watchdog/daemon.test.ts +1233 -0
- package/src/watchdog/daemon.ts +533 -0
- package/src/watchdog/health.test.ts +371 -0
- package/src/watchdog/health.ts +248 -0
- package/src/watchdog/triage.test.ts +162 -0
- package/src/watchdog/triage.ts +193 -0
- package/src/worktree/manager.test.ts +444 -0
- package/src/worktree/manager.ts +224 -0
- package/src/worktree/tmux.test.ts +1238 -0
- package/src/worktree/tmux.ts +644 -0
- package/templates/CLAUDE.md.tmpl +89 -0
- package/templates/hooks.json.tmpl +132 -0
- 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
|
+
}
|