@tokentop/ttop 0.2.2
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/LICENSE +21 -0
- package/README.md +273 -0
- package/bin/ttop.js +49 -0
- package/package.json +68 -0
- package/src/agents/aggregator.ts +176 -0
- package/src/agents/costing.ts +74 -0
- package/src/agents/index.ts +12 -0
- package/src/agents/types.ts +71 -0
- package/src/cli.ts +167 -0
- package/src/config/schema.test.ts +166 -0
- package/src/config/schema.ts +165 -0
- package/src/demo/simulator.ts +745 -0
- package/src/plugins/agents/index.ts +2 -0
- package/src/plugins/auth-sources.ts +186 -0
- package/src/plugins/lifecycle.ts +177 -0
- package/src/plugins/loader.test.ts +195 -0
- package/src/plugins/loader.ts +282 -0
- package/src/plugins/notification-bus.ts +153 -0
- package/src/plugins/notifications/index.ts +2 -0
- package/src/plugins/notifications/terminal-bell.ts +66 -0
- package/src/plugins/notifications/visual-flash.ts +57 -0
- package/src/plugins/npm-installer.ts +164 -0
- package/src/plugins/plugin-context-factory.ts +67 -0
- package/src/plugins/plugin-host.ts +205 -0
- package/src/plugins/providers/anthropic.ts +325 -0
- package/src/plugins/providers/antigravity.ts +309 -0
- package/src/plugins/providers/codex.ts +250 -0
- package/src/plugins/providers/gemini.ts +391 -0
- package/src/plugins/providers/github-copilot.ts +194 -0
- package/src/plugins/providers/index.ts +12 -0
- package/src/plugins/providers/minimax.ts +223 -0
- package/src/plugins/providers/openai-api.ts +250 -0
- package/src/plugins/providers/opencode-zen.ts +125 -0
- package/src/plugins/providers/perplexity.ts +177 -0
- package/src/plugins/providers/zai.ts +225 -0
- package/src/plugins/registry.ts +251 -0
- package/src/plugins/sandbox-guard.test.ts +191 -0
- package/src/plugins/sandbox-guard.ts +155 -0
- package/src/plugins/sandbox.ts +138 -0
- package/src/plugins/themes/catppuccin-latte.ts +45 -0
- package/src/plugins/themes/catppuccin-mocha.ts +45 -0
- package/src/plugins/themes/claude-code.ts +45 -0
- package/src/plugins/themes/dracula.ts +45 -0
- package/src/plugins/themes/github-light.ts +45 -0
- package/src/plugins/themes/gruvbox-dark.ts +45 -0
- package/src/plugins/themes/gruvbox-light.ts +45 -0
- package/src/plugins/themes/index.ts +15 -0
- package/src/plugins/themes/kanagawa.ts +45 -0
- package/src/plugins/themes/nord.ts +45 -0
- package/src/plugins/themes/one-dark.ts +45 -0
- package/src/plugins/themes/opencode.ts +47 -0
- package/src/plugins/themes/rose-pine-dawn.ts +45 -0
- package/src/plugins/themes/rose-pine.ts +45 -0
- package/src/plugins/themes/solarized-light.ts +45 -0
- package/src/plugins/themes/tokyo-night.ts +49 -0
- package/src/plugins/types/agent.ts +108 -0
- package/src/plugins/types/base.ts +267 -0
- package/src/plugins/types/index.ts +19 -0
- package/src/plugins/types/notification.ts +51 -0
- package/src/plugins/types/provider.ts +233 -0
- package/src/plugins/types/theme.ts +70 -0
- package/src/plugins/update-checker.ts +177 -0
- package/src/pricing/estimator.test.ts +181 -0
- package/src/pricing/estimator.ts +80 -0
- package/src/pricing/fallback.test.ts +71 -0
- package/src/pricing/fallback.ts +62 -0
- package/src/pricing/index.ts +66 -0
- package/src/pricing/models-dev.ts +130 -0
- package/src/storage/database.ts +613 -0
- package/src/storage/db.ts +64 -0
- package/src/storage/index.ts +66 -0
- package/src/storage/migrations/index.ts +189 -0
- package/src/storage/paths.ts +24 -0
- package/src/storage/repos/agentSessions.ts +210 -0
- package/src/storage/repos/providerSnapshots.ts +116 -0
- package/src/storage/repos/usageEvents.ts +334 -0
- package/src/storage/types.ts +288 -0
- package/src/tui/App.tsx +381 -0
- package/src/tui/components/CommandPalette.tsx +166 -0
- package/src/tui/components/DebugConsole.tsx +161 -0
- package/src/tui/components/DebugPanel.tsx +660 -0
- package/src/tui/components/FooterHints.tsx +22 -0
- package/src/tui/components/GhostProviderCard.tsx +45 -0
- package/src/tui/components/Header.tsx +125 -0
- package/src/tui/components/HelpOverlay.tsx +45 -0
- package/src/tui/components/InlineGauge.tsx +34 -0
- package/src/tui/components/InlineSparkline.tsx +78 -0
- package/src/tui/components/KpiStrip.tsx +246 -0
- package/src/tui/components/LimitGauge.tsx +186 -0
- package/src/tui/components/ModalBackdrop.tsx +42 -0
- package/src/tui/components/ProviderAggregateStrip.tsx +200 -0
- package/src/tui/components/ProviderCard.tsx +309 -0
- package/src/tui/components/ProviderDetailPanel.tsx +237 -0
- package/src/tui/components/ProviderLimitsPanel.tsx +308 -0
- package/src/tui/components/ProvidersList.tsx +380 -0
- package/src/tui/components/SessionDetailsDrawer.tsx +317 -0
- package/src/tui/components/SessionsTable.tsx +401 -0
- package/src/tui/components/SettingsModal.tsx +587 -0
- package/src/tui/components/SidebarBreakdown.tsx +87 -0
- package/src/tui/components/Skeleton.tsx +102 -0
- package/src/tui/components/SmartSidebar.tsx +644 -0
- package/src/tui/components/Sparkline.tsx +180 -0
- package/src/tui/components/Spinner.tsx +36 -0
- package/src/tui/components/StatusBar.tsx +96 -0
- package/src/tui/components/ThemePicker.tsx +234 -0
- package/src/tui/components/Toast.tsx +61 -0
- package/src/tui/components/UsageGauge.tsx +80 -0
- package/src/tui/components/index.ts +5 -0
- package/src/tui/contexts/AgentSessionContext.tsx +307 -0
- package/src/tui/contexts/ConfigContext.tsx +141 -0
- package/src/tui/contexts/DashboardRuntimeContext.tsx +102 -0
- package/src/tui/contexts/DemoModeContext.tsx +60 -0
- package/src/tui/contexts/DrawerContext.tsx +42 -0
- package/src/tui/contexts/InputContext.tsx +30 -0
- package/src/tui/contexts/LogContext.tsx +150 -0
- package/src/tui/contexts/PluginContext.tsx +424 -0
- package/src/tui/contexts/RealTimeActivityContext.tsx +119 -0
- package/src/tui/contexts/StorageContext.tsx +233 -0
- package/src/tui/contexts/ThemeContext.tsx +95 -0
- package/src/tui/contexts/TimeWindowContext.tsx +158 -0
- package/src/tui/contexts/ToastContext.tsx +40 -0
- package/src/tui/contexts/index.ts +5 -0
- package/src/tui/createApp.tsx +47 -0
- package/src/tui/debug/captureFrame.ts +262 -0
- package/src/tui/debug/snapshot.tsx +644 -0
- package/src/tui/driver/assertions.ts +279 -0
- package/src/tui/driver/cli.ts +717 -0
- package/src/tui/driver/coverage.ts +204 -0
- package/src/tui/driver/demo-snapshot.ts +66 -0
- package/src/tui/driver/diff.ts +166 -0
- package/src/tui/driver/driver.ts +270 -0
- package/src/tui/driver/index.ts +53 -0
- package/src/tui/driver/recorder.ts +303 -0
- package/src/tui/driver/test-workflow.ts +115 -0
- package/src/tui/driver/test.ts +47 -0
- package/src/tui/hooks/index.ts +20 -0
- package/src/tui/hooks/useAnimatedValue.ts +113 -0
- package/src/tui/hooks/useDashboardKeyboard.ts +341 -0
- package/src/tui/hooks/useDashboardState.ts +116 -0
- package/src/tui/hooks/useEmaActivity.ts +223 -0
- package/src/tui/hooks/useEntranceAnimation.ts +62 -0
- package/src/tui/hooks/useExitAnimation.ts +137 -0
- package/src/tui/hooks/usePulse.ts +138 -0
- package/src/tui/hooks/useSafeRenderer.ts +24 -0
- package/src/tui/hooks/useValueFlash.ts +111 -0
- package/src/tui/index.tsx +50 -0
- package/src/tui/utils/providerColor.ts +32 -0
- package/src/tui/views/Dashboard.tsx +340 -0
- package/src/tui/views/HistoricalTrendsView.tsx +1431 -0
- package/src/tui/views/ProjectsView.tsx +1277 -0
- package/src/tui/views/RealTimeDashboard.tsx +393 -0
- package/src/tui/views/SettingsView.tsx +465 -0
- package/src/tui/views/index.ts +2 -0
- package/src/types/global.d.ts +5 -0
- package/src/types/opentui-test-utils.d.ts +18 -0
- package/src/utils/clipboard.ts +118 -0
- package/src/utils/currency.test.ts +106 -0
- package/src/utils/currency.ts +79 -0
- package/src/version.ts +1 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { getDatabase } from '../db.ts';
|
|
2
|
+
import type { UsageEventInsert, TimeSeriesPoint, TimeSeriesFilters } from '../types.ts';
|
|
3
|
+
|
|
4
|
+
const INSERT_SQL = `
|
|
5
|
+
INSERT INTO usage_events (
|
|
6
|
+
timestamp, source, provider, model, agent_id, session_id, project_path,
|
|
7
|
+
input_tokens, output_tokens, cache_read_tokens, cache_write_tokens,
|
|
8
|
+
cost_usd, request_count, pricing_source
|
|
9
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
10
|
+
`;
|
|
11
|
+
|
|
12
|
+
export function insertUsageEvent(event: UsageEventInsert): number {
|
|
13
|
+
const db = getDatabase();
|
|
14
|
+
const stmt = db.prepare(INSERT_SQL);
|
|
15
|
+
|
|
16
|
+
const result = stmt.run(
|
|
17
|
+
event.timestamp,
|
|
18
|
+
event.source,
|
|
19
|
+
event.provider ?? null,
|
|
20
|
+
event.model ?? null,
|
|
21
|
+
event.agentId ?? null,
|
|
22
|
+
event.sessionId ?? null,
|
|
23
|
+
event.projectPath ?? null,
|
|
24
|
+
event.inputTokens,
|
|
25
|
+
event.outputTokens,
|
|
26
|
+
event.cacheReadTokens,
|
|
27
|
+
event.cacheWriteTokens,
|
|
28
|
+
event.costUsd,
|
|
29
|
+
event.requestCount,
|
|
30
|
+
event.pricingSource ?? null
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
return Number(result.lastInsertRowid);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function insertUsageEventBatch(events: UsageEventInsert[]): void {
|
|
37
|
+
if (events.length === 0) return;
|
|
38
|
+
|
|
39
|
+
const db = getDatabase();
|
|
40
|
+
const stmt = db.prepare(INSERT_SQL);
|
|
41
|
+
|
|
42
|
+
const insertMany = db.transaction((items: UsageEventInsert[]) => {
|
|
43
|
+
for (const e of items) {
|
|
44
|
+
stmt.run(
|
|
45
|
+
e.timestamp,
|
|
46
|
+
e.source,
|
|
47
|
+
e.provider ?? null,
|
|
48
|
+
e.model ?? null,
|
|
49
|
+
e.agentId ?? null,
|
|
50
|
+
e.sessionId ?? null,
|
|
51
|
+
e.projectPath ?? null,
|
|
52
|
+
e.inputTokens,
|
|
53
|
+
e.outputTokens,
|
|
54
|
+
e.cacheReadTokens,
|
|
55
|
+
e.cacheWriteTokens,
|
|
56
|
+
e.costUsd,
|
|
57
|
+
e.requestCount,
|
|
58
|
+
e.pricingSource ?? null
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
insertMany(events);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function queryUsageTimeSeries(
|
|
67
|
+
startMs: number,
|
|
68
|
+
endMs: number,
|
|
69
|
+
bucketMs: number,
|
|
70
|
+
filters: TimeSeriesFilters = {}
|
|
71
|
+
): TimeSeriesPoint[] {
|
|
72
|
+
const db = getDatabase();
|
|
73
|
+
|
|
74
|
+
const conditions: string[] = ['timestamp >= ?', 'timestamp <= ?'];
|
|
75
|
+
const params: (string | number)[] = [startMs, endMs];
|
|
76
|
+
|
|
77
|
+
if (filters.provider) {
|
|
78
|
+
conditions.push('provider = ?');
|
|
79
|
+
params.push(filters.provider);
|
|
80
|
+
}
|
|
81
|
+
if (filters.model) {
|
|
82
|
+
conditions.push('model = ?');
|
|
83
|
+
params.push(filters.model);
|
|
84
|
+
}
|
|
85
|
+
if (filters.agentId) {
|
|
86
|
+
conditions.push('agent_id = ?');
|
|
87
|
+
params.push(filters.agentId);
|
|
88
|
+
}
|
|
89
|
+
if (filters.sessionId) {
|
|
90
|
+
conditions.push('session_id = ?');
|
|
91
|
+
params.push(filters.sessionId);
|
|
92
|
+
}
|
|
93
|
+
if (filters.projectPath) {
|
|
94
|
+
conditions.push('project_path = ?');
|
|
95
|
+
params.push(filters.projectPath);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const sql = `
|
|
99
|
+
SELECT
|
|
100
|
+
(timestamp / ${bucketMs} * ${bucketMs}) AS bucket_start,
|
|
101
|
+
COALESCE(SUM(input_tokens + output_tokens), 0) AS tokens,
|
|
102
|
+
COALESCE(SUM(cost_usd), 0) AS cost_usd,
|
|
103
|
+
COALESCE(SUM(request_count), 0) AS request_count
|
|
104
|
+
FROM usage_events
|
|
105
|
+
WHERE ${conditions.join(' AND ')}
|
|
106
|
+
GROUP BY bucket_start
|
|
107
|
+
ORDER BY bucket_start ASC
|
|
108
|
+
`;
|
|
109
|
+
|
|
110
|
+
const rows = db.prepare(sql).all(...params) as Array<{
|
|
111
|
+
bucket_start: number;
|
|
112
|
+
tokens: number;
|
|
113
|
+
cost_usd: number;
|
|
114
|
+
request_count: number;
|
|
115
|
+
}>;
|
|
116
|
+
|
|
117
|
+
return rows.map(r => ({
|
|
118
|
+
bucketStart: r.bucket_start,
|
|
119
|
+
tokens: r.tokens,
|
|
120
|
+
costUsd: r.cost_usd,
|
|
121
|
+
requestCount: r.request_count,
|
|
122
|
+
}));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function calculateBurnRate(windowMs: number, nowMs = Date.now()): { costPerHour: number; tokensPerMinute: number } {
|
|
126
|
+
const db = getDatabase();
|
|
127
|
+
const startMs = nowMs - windowMs;
|
|
128
|
+
|
|
129
|
+
const row = db.prepare(`
|
|
130
|
+
SELECT
|
|
131
|
+
COALESCE(SUM(cost_usd), 0) AS cost,
|
|
132
|
+
COALESCE(SUM(input_tokens + output_tokens), 0) AS tokens
|
|
133
|
+
FROM usage_events
|
|
134
|
+
WHERE timestamp >= ? AND timestamp <= ?
|
|
135
|
+
`).get(startMs, nowMs) as { cost: number; tokens: number };
|
|
136
|
+
|
|
137
|
+
const hours = windowMs / (1000 * 60 * 60);
|
|
138
|
+
const minutes = windowMs / (1000 * 60);
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
costPerHour: hours > 0 ? row.cost / hours : 0,
|
|
142
|
+
tokensPerMinute: minutes > 0 ? row.tokens / minutes : 0,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function getTotalUsageInWindow(startMs: number, endMs: number): {
|
|
147
|
+
inputTokens: number;
|
|
148
|
+
outputTokens: number;
|
|
149
|
+
costUsd: number;
|
|
150
|
+
requestCount: number;
|
|
151
|
+
} {
|
|
152
|
+
const db = getDatabase();
|
|
153
|
+
|
|
154
|
+
const row = db.prepare(`
|
|
155
|
+
SELECT
|
|
156
|
+
COALESCE(SUM(input_tokens), 0) AS input_tokens,
|
|
157
|
+
COALESCE(SUM(output_tokens), 0) AS output_tokens,
|
|
158
|
+
COALESCE(SUM(cost_usd), 0) AS cost_usd,
|
|
159
|
+
COALESCE(SUM(request_count), 0) AS request_count
|
|
160
|
+
FROM usage_events
|
|
161
|
+
WHERE timestamp >= ? AND timestamp <= ?
|
|
162
|
+
`).get(startMs, endMs) as {
|
|
163
|
+
input_tokens: number;
|
|
164
|
+
output_tokens: number;
|
|
165
|
+
cost_usd: number;
|
|
166
|
+
request_count: number;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
inputTokens: row.input_tokens,
|
|
171
|
+
outputTokens: row.output_tokens,
|
|
172
|
+
costUsd: row.cost_usd,
|
|
173
|
+
requestCount: row.request_count,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export interface SessionActivityPoint {
|
|
178
|
+
timestamp: number;
|
|
179
|
+
tokens: number;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export interface ProviderDailyCost {
|
|
183
|
+
provider: string;
|
|
184
|
+
bucketStart: number;
|
|
185
|
+
costUsd: number;
|
|
186
|
+
tokens: number;
|
|
187
|
+
requestCount: number;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function queryProviderDailyCosts(
|
|
191
|
+
startMs: number,
|
|
192
|
+
endMs: number,
|
|
193
|
+
bucketMs: number
|
|
194
|
+
): ProviderDailyCost[] {
|
|
195
|
+
const db = getDatabase();
|
|
196
|
+
|
|
197
|
+
const sql = `
|
|
198
|
+
SELECT
|
|
199
|
+
provider,
|
|
200
|
+
(timestamp / ${bucketMs} * ${bucketMs}) AS bucket_start,
|
|
201
|
+
COALESCE(SUM(cost_usd), 0) AS cost_usd,
|
|
202
|
+
COALESCE(SUM(input_tokens + output_tokens), 0) AS tokens,
|
|
203
|
+
COALESCE(SUM(request_count), 0) AS request_count
|
|
204
|
+
FROM usage_events
|
|
205
|
+
WHERE timestamp >= ? AND timestamp <= ?
|
|
206
|
+
AND provider IS NOT NULL
|
|
207
|
+
GROUP BY provider, bucket_start
|
|
208
|
+
ORDER BY provider, bucket_start ASC
|
|
209
|
+
`;
|
|
210
|
+
|
|
211
|
+
const rows = db.prepare(sql).all(startMs, endMs) as Array<{
|
|
212
|
+
provider: string;
|
|
213
|
+
bucket_start: number;
|
|
214
|
+
cost_usd: number;
|
|
215
|
+
tokens: number;
|
|
216
|
+
request_count: number;
|
|
217
|
+
}>;
|
|
218
|
+
|
|
219
|
+
return rows.map(r => ({
|
|
220
|
+
provider: r.provider,
|
|
221
|
+
bucketStart: r.bucket_start,
|
|
222
|
+
costUsd: r.cost_usd,
|
|
223
|
+
tokens: r.tokens,
|
|
224
|
+
requestCount: r.request_count,
|
|
225
|
+
}));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export interface ModelDailyCost {
|
|
229
|
+
model: string;
|
|
230
|
+
bucketStart: number;
|
|
231
|
+
costUsd: number;
|
|
232
|
+
tokens: number;
|
|
233
|
+
requestCount: number;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function queryModelDailyCosts(
|
|
237
|
+
startMs: number,
|
|
238
|
+
endMs: number,
|
|
239
|
+
bucketMs: number
|
|
240
|
+
): ModelDailyCost[] {
|
|
241
|
+
const db = getDatabase();
|
|
242
|
+
|
|
243
|
+
const sql = `
|
|
244
|
+
SELECT
|
|
245
|
+
model,
|
|
246
|
+
(timestamp / ${bucketMs} * ${bucketMs}) AS bucket_start,
|
|
247
|
+
COALESCE(SUM(cost_usd), 0) AS cost_usd,
|
|
248
|
+
COALESCE(SUM(input_tokens + output_tokens), 0) AS tokens,
|
|
249
|
+
COALESCE(SUM(request_count), 0) AS request_count
|
|
250
|
+
FROM usage_events
|
|
251
|
+
WHERE timestamp >= ? AND timestamp <= ?
|
|
252
|
+
AND model IS NOT NULL
|
|
253
|
+
GROUP BY model, bucket_start
|
|
254
|
+
ORDER BY model, bucket_start ASC
|
|
255
|
+
`;
|
|
256
|
+
|
|
257
|
+
const rows = db.prepare(sql).all(startMs, endMs) as Array<{
|
|
258
|
+
model: string;
|
|
259
|
+
bucket_start: number;
|
|
260
|
+
cost_usd: number;
|
|
261
|
+
tokens: number;
|
|
262
|
+
request_count: number;
|
|
263
|
+
}>;
|
|
264
|
+
|
|
265
|
+
return rows.map(r => ({
|
|
266
|
+
model: r.model,
|
|
267
|
+
bucketStart: r.bucket_start,
|
|
268
|
+
costUsd: r.cost_usd,
|
|
269
|
+
tokens: r.tokens,
|
|
270
|
+
requestCount: r.request_count,
|
|
271
|
+
}));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export interface ProjectDailyCost {
|
|
275
|
+
projectPath: string;
|
|
276
|
+
bucketStart: number;
|
|
277
|
+
costUsd: number;
|
|
278
|
+
tokens: number;
|
|
279
|
+
requestCount: number;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export function queryProjectDailyCosts(
|
|
283
|
+
startMs: number,
|
|
284
|
+
endMs: number,
|
|
285
|
+
bucketMs: number
|
|
286
|
+
): ProjectDailyCost[] {
|
|
287
|
+
const db = getDatabase();
|
|
288
|
+
|
|
289
|
+
const sql = `
|
|
290
|
+
SELECT
|
|
291
|
+
project_path,
|
|
292
|
+
(timestamp / ${bucketMs} * ${bucketMs}) AS bucket_start,
|
|
293
|
+
COALESCE(SUM(cost_usd), 0) AS cost_usd,
|
|
294
|
+
COALESCE(SUM(input_tokens + output_tokens), 0) AS tokens,
|
|
295
|
+
COALESCE(SUM(request_count), 0) AS request_count
|
|
296
|
+
FROM usage_events
|
|
297
|
+
WHERE timestamp >= ? AND timestamp <= ?
|
|
298
|
+
AND project_path IS NOT NULL
|
|
299
|
+
GROUP BY project_path, bucket_start
|
|
300
|
+
ORDER BY project_path, bucket_start ASC
|
|
301
|
+
`;
|
|
302
|
+
|
|
303
|
+
const rows = db.prepare(sql).all(startMs, endMs) as Array<{
|
|
304
|
+
project_path: string;
|
|
305
|
+
bucket_start: number;
|
|
306
|
+
cost_usd: number;
|
|
307
|
+
tokens: number;
|
|
308
|
+
request_count: number;
|
|
309
|
+
}>;
|
|
310
|
+
|
|
311
|
+
return rows.map(r => ({
|
|
312
|
+
projectPath: r.project_path,
|
|
313
|
+
bucketStart: r.bucket_start,
|
|
314
|
+
costUsd: r.cost_usd,
|
|
315
|
+
tokens: r.tokens,
|
|
316
|
+
requestCount: r.request_count,
|
|
317
|
+
}));
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export function getSessionActivityTimeline(sessionId: string): SessionActivityPoint[] {
|
|
321
|
+
const db = getDatabase();
|
|
322
|
+
|
|
323
|
+
const rows = db.prepare(`
|
|
324
|
+
SELECT timestamp, (input_tokens + output_tokens) AS tokens
|
|
325
|
+
FROM usage_events
|
|
326
|
+
WHERE session_id = ?
|
|
327
|
+
ORDER BY timestamp ASC
|
|
328
|
+
`).all(sessionId) as Array<{ timestamp: number; tokens: number }>;
|
|
329
|
+
|
|
330
|
+
return rows.map(r => ({
|
|
331
|
+
timestamp: r.timestamp,
|
|
332
|
+
tokens: r.tokens,
|
|
333
|
+
}));
|
|
334
|
+
}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript interfaces for SQLite storage layer.
|
|
3
|
+
* Based on SQLITE_ARCHITECTURE.md specification.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// Enums & Literal Types
|
|
8
|
+
// ============================================================================
|
|
9
|
+
|
|
10
|
+
export type UsageEventSource = 'agent' | 'provider';
|
|
11
|
+
export type SessionStatus = 'active' | 'idle';
|
|
12
|
+
export type CostSource = 'api' | 'estimated';
|
|
13
|
+
export type PricingSource = 'models.dev' | 'fallback' | 'unknown';
|
|
14
|
+
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// App Runs Table
|
|
17
|
+
// ============================================================================
|
|
18
|
+
|
|
19
|
+
export interface AppRunRow {
|
|
20
|
+
id?: number;
|
|
21
|
+
startedAt: number; // ms since epoch
|
|
22
|
+
endedAt?: number | null;
|
|
23
|
+
appVersion?: string | null;
|
|
24
|
+
refreshIntervalMs?: number | null;
|
|
25
|
+
pid?: number | null;
|
|
26
|
+
hostname?: string | null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ============================================================================
|
|
30
|
+
// Provider Snapshots Table
|
|
31
|
+
// ============================================================================
|
|
32
|
+
|
|
33
|
+
export interface ProviderSnapshotRow {
|
|
34
|
+
id?: number;
|
|
35
|
+
timestamp: number; // ms since epoch
|
|
36
|
+
provider: string;
|
|
37
|
+
usedPercent?: number | null;
|
|
38
|
+
limitReached: boolean;
|
|
39
|
+
resetsAt?: number | null; // ms since epoch
|
|
40
|
+
tokensInput?: number | null;
|
|
41
|
+
tokensOutput?: number | null;
|
|
42
|
+
costUsd?: number | null;
|
|
43
|
+
costSource?: CostSource | null;
|
|
44
|
+
rawJson?: string | null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ProviderSnapshotInsert {
|
|
48
|
+
timestamp: number;
|
|
49
|
+
provider: string;
|
|
50
|
+
usedPercent?: number | null;
|
|
51
|
+
limitReached?: boolean;
|
|
52
|
+
resetsAt?: number | null;
|
|
53
|
+
tokensInput?: number | null;
|
|
54
|
+
tokensOutput?: number | null;
|
|
55
|
+
costUsd?: number | null;
|
|
56
|
+
costSource?: CostSource | null;
|
|
57
|
+
rawJson?: string | null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ============================================================================
|
|
61
|
+
// Agent Sessions Table (Logical Dimension)
|
|
62
|
+
// ============================================================================
|
|
63
|
+
|
|
64
|
+
export interface AgentSessionDim {
|
|
65
|
+
id?: number;
|
|
66
|
+
agentId: string;
|
|
67
|
+
sessionId: string;
|
|
68
|
+
projectPath?: string | null;
|
|
69
|
+
startedAt?: number | null;
|
|
70
|
+
firstSeenAt: number;
|
|
71
|
+
lastSeenAt: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface AgentSessionUpsert {
|
|
75
|
+
agentId: string;
|
|
76
|
+
sessionId: string;
|
|
77
|
+
projectPath?: string | null;
|
|
78
|
+
startedAt?: number | null;
|
|
79
|
+
firstSeenAt?: number;
|
|
80
|
+
lastSeenAt: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ============================================================================
|
|
84
|
+
// Agent Session Snapshots Table
|
|
85
|
+
// ============================================================================
|
|
86
|
+
|
|
87
|
+
export interface AgentSessionSnapshotRow {
|
|
88
|
+
id?: number;
|
|
89
|
+
timestamp: number;
|
|
90
|
+
agentSessionId: number;
|
|
91
|
+
lastActivityAt?: number | null;
|
|
92
|
+
status?: SessionStatus | null;
|
|
93
|
+
totalInputTokens: number;
|
|
94
|
+
totalOutputTokens: number;
|
|
95
|
+
totalCacheReadTokens: number;
|
|
96
|
+
totalCacheWriteTokens: number;
|
|
97
|
+
totalCostUsd: number;
|
|
98
|
+
requestCount: number;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface AgentSessionSnapshotInsert {
|
|
102
|
+
timestamp: number;
|
|
103
|
+
agentSessionId: number;
|
|
104
|
+
lastActivityAt?: number | null;
|
|
105
|
+
status?: SessionStatus | null;
|
|
106
|
+
totalInputTokens: number;
|
|
107
|
+
totalOutputTokens: number;
|
|
108
|
+
totalCacheReadTokens: number;
|
|
109
|
+
totalCacheWriteTokens: number;
|
|
110
|
+
totalCostUsd: number;
|
|
111
|
+
requestCount: number;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ============================================================================
|
|
115
|
+
// Agent Session Stream Snapshots Table
|
|
116
|
+
// ============================================================================
|
|
117
|
+
|
|
118
|
+
export interface AgentSessionStreamSnapshotRow {
|
|
119
|
+
agentSessionSnapshotId: number;
|
|
120
|
+
provider: string;
|
|
121
|
+
model: string;
|
|
122
|
+
inputTokens: number;
|
|
123
|
+
outputTokens: number;
|
|
124
|
+
cacheReadTokens: number;
|
|
125
|
+
cacheWriteTokens: number;
|
|
126
|
+
costUsd: number;
|
|
127
|
+
requestCount: number;
|
|
128
|
+
pricingSource?: PricingSource | null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ============================================================================
|
|
132
|
+
// Usage Events Table (Append-Only Deltas)
|
|
133
|
+
// ============================================================================
|
|
134
|
+
|
|
135
|
+
export interface UsageEventRow {
|
|
136
|
+
id?: number;
|
|
137
|
+
timestamp: number;
|
|
138
|
+
source: UsageEventSource;
|
|
139
|
+
provider?: string | null;
|
|
140
|
+
model?: string | null;
|
|
141
|
+
agentId?: string | null;
|
|
142
|
+
sessionId?: string | null;
|
|
143
|
+
projectPath?: string | null;
|
|
144
|
+
inputTokens: number;
|
|
145
|
+
outputTokens: number;
|
|
146
|
+
cacheReadTokens: number;
|
|
147
|
+
cacheWriteTokens: number;
|
|
148
|
+
costUsd: number;
|
|
149
|
+
requestCount: number;
|
|
150
|
+
pricingSource?: PricingSource | null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export interface UsageEventInsert {
|
|
154
|
+
timestamp: number;
|
|
155
|
+
source: UsageEventSource;
|
|
156
|
+
provider?: string | null;
|
|
157
|
+
model?: string | null;
|
|
158
|
+
agentId?: string | null;
|
|
159
|
+
sessionId?: string | null;
|
|
160
|
+
projectPath?: string | null;
|
|
161
|
+
inputTokens: number;
|
|
162
|
+
outputTokens: number;
|
|
163
|
+
cacheReadTokens: number;
|
|
164
|
+
cacheWriteTokens: number;
|
|
165
|
+
costUsd: number;
|
|
166
|
+
requestCount: number;
|
|
167
|
+
pricingSource?: PricingSource | null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ============================================================================
|
|
171
|
+
// Hourly Aggregates Table
|
|
172
|
+
// ============================================================================
|
|
173
|
+
|
|
174
|
+
export interface HourlyAggregateRow {
|
|
175
|
+
bucketStart: number; // ms since epoch, truncated to hour
|
|
176
|
+
provider?: string | null;
|
|
177
|
+
model?: string | null;
|
|
178
|
+
agentId?: string | null;
|
|
179
|
+
projectPath?: string | null;
|
|
180
|
+
inputTokens: number;
|
|
181
|
+
outputTokens: number;
|
|
182
|
+
cacheReadTokens: number;
|
|
183
|
+
cacheWriteTokens: number;
|
|
184
|
+
costUsd: number;
|
|
185
|
+
requestCount: number;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ============================================================================
|
|
189
|
+
// Daily Aggregates Table
|
|
190
|
+
// ============================================================================
|
|
191
|
+
|
|
192
|
+
export interface DailyAggregateRow {
|
|
193
|
+
date: string; // 'YYYY-MM-DD'
|
|
194
|
+
provider?: string | null;
|
|
195
|
+
model?: string | null;
|
|
196
|
+
agentId?: string | null;
|
|
197
|
+
projectPath?: string | null;
|
|
198
|
+
inputTokens: number;
|
|
199
|
+
outputTokens: number;
|
|
200
|
+
cacheReadTokens: number;
|
|
201
|
+
cacheWriteTokens: number;
|
|
202
|
+
costUsd: number;
|
|
203
|
+
requestCount: number;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ============================================================================
|
|
207
|
+
// Query Types
|
|
208
|
+
// ============================================================================
|
|
209
|
+
|
|
210
|
+
export interface TimeSeriesPoint {
|
|
211
|
+
bucketStart: number;
|
|
212
|
+
tokens: number;
|
|
213
|
+
costUsd: number;
|
|
214
|
+
requestCount: number;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export interface TimeSeriesFilters {
|
|
218
|
+
provider?: string;
|
|
219
|
+
model?: string;
|
|
220
|
+
agentId?: string;
|
|
221
|
+
sessionId?: string;
|
|
222
|
+
projectPath?: string;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export interface UsageQueryOptions {
|
|
226
|
+
startTime?: number;
|
|
227
|
+
endTime?: number;
|
|
228
|
+
provider?: string;
|
|
229
|
+
model?: string;
|
|
230
|
+
agentId?: string;
|
|
231
|
+
sessionId?: string;
|
|
232
|
+
projectPath?: string;
|
|
233
|
+
limit?: number;
|
|
234
|
+
offset?: number;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ============================================================================
|
|
238
|
+
// Delta Computation
|
|
239
|
+
// ============================================================================
|
|
240
|
+
|
|
241
|
+
export interface StreamTotals {
|
|
242
|
+
inputTokens: number;
|
|
243
|
+
outputTokens: number;
|
|
244
|
+
cacheReadTokens: number;
|
|
245
|
+
cacheWriteTokens: number;
|
|
246
|
+
costUsd: number;
|
|
247
|
+
requestCount: number;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Computes the delta between two stream totals.
|
|
252
|
+
* Returns null if delta is all zeros (no change worth recording).
|
|
253
|
+
*/
|
|
254
|
+
export function computeStreamDelta(
|
|
255
|
+
current: StreamTotals,
|
|
256
|
+
previous: StreamTotals | undefined
|
|
257
|
+
): StreamTotals | null {
|
|
258
|
+
if (!previous) {
|
|
259
|
+
// First observation - treat as a delta from zero
|
|
260
|
+
const hasNonZero =
|
|
261
|
+
current.inputTokens ||
|
|
262
|
+
current.outputTokens ||
|
|
263
|
+
current.cacheReadTokens ||
|
|
264
|
+
current.cacheWriteTokens ||
|
|
265
|
+
current.costUsd ||
|
|
266
|
+
current.requestCount;
|
|
267
|
+
return hasNonZero ? current : null;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const delta: StreamTotals = {
|
|
271
|
+
inputTokens: Math.max(0, current.inputTokens - previous.inputTokens),
|
|
272
|
+
outputTokens: Math.max(0, current.outputTokens - previous.outputTokens),
|
|
273
|
+
cacheReadTokens: Math.max(0, current.cacheReadTokens - previous.cacheReadTokens),
|
|
274
|
+
cacheWriteTokens: Math.max(0, current.cacheWriteTokens - previous.cacheWriteTokens),
|
|
275
|
+
costUsd: Math.max(0, current.costUsd - previous.costUsd),
|
|
276
|
+
requestCount: Math.max(0, current.requestCount - previous.requestCount),
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const hasNonZero =
|
|
280
|
+
delta.inputTokens ||
|
|
281
|
+
delta.outputTokens ||
|
|
282
|
+
delta.cacheReadTokens ||
|
|
283
|
+
delta.cacheWriteTokens ||
|
|
284
|
+
delta.costUsd ||
|
|
285
|
+
delta.requestCount;
|
|
286
|
+
|
|
287
|
+
return hasNonZero ? delta : null;
|
|
288
|
+
}
|