@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.
Files changed (159) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +273 -0
  3. package/bin/ttop.js +49 -0
  4. package/package.json +68 -0
  5. package/src/agents/aggregator.ts +176 -0
  6. package/src/agents/costing.ts +74 -0
  7. package/src/agents/index.ts +12 -0
  8. package/src/agents/types.ts +71 -0
  9. package/src/cli.ts +167 -0
  10. package/src/config/schema.test.ts +166 -0
  11. package/src/config/schema.ts +165 -0
  12. package/src/demo/simulator.ts +745 -0
  13. package/src/plugins/agents/index.ts +2 -0
  14. package/src/plugins/auth-sources.ts +186 -0
  15. package/src/plugins/lifecycle.ts +177 -0
  16. package/src/plugins/loader.test.ts +195 -0
  17. package/src/plugins/loader.ts +282 -0
  18. package/src/plugins/notification-bus.ts +153 -0
  19. package/src/plugins/notifications/index.ts +2 -0
  20. package/src/plugins/notifications/terminal-bell.ts +66 -0
  21. package/src/plugins/notifications/visual-flash.ts +57 -0
  22. package/src/plugins/npm-installer.ts +164 -0
  23. package/src/plugins/plugin-context-factory.ts +67 -0
  24. package/src/plugins/plugin-host.ts +205 -0
  25. package/src/plugins/providers/anthropic.ts +325 -0
  26. package/src/plugins/providers/antigravity.ts +309 -0
  27. package/src/plugins/providers/codex.ts +250 -0
  28. package/src/plugins/providers/gemini.ts +391 -0
  29. package/src/plugins/providers/github-copilot.ts +194 -0
  30. package/src/plugins/providers/index.ts +12 -0
  31. package/src/plugins/providers/minimax.ts +223 -0
  32. package/src/plugins/providers/openai-api.ts +250 -0
  33. package/src/plugins/providers/opencode-zen.ts +125 -0
  34. package/src/plugins/providers/perplexity.ts +177 -0
  35. package/src/plugins/providers/zai.ts +225 -0
  36. package/src/plugins/registry.ts +251 -0
  37. package/src/plugins/sandbox-guard.test.ts +191 -0
  38. package/src/plugins/sandbox-guard.ts +155 -0
  39. package/src/plugins/sandbox.ts +138 -0
  40. package/src/plugins/themes/catppuccin-latte.ts +45 -0
  41. package/src/plugins/themes/catppuccin-mocha.ts +45 -0
  42. package/src/plugins/themes/claude-code.ts +45 -0
  43. package/src/plugins/themes/dracula.ts +45 -0
  44. package/src/plugins/themes/github-light.ts +45 -0
  45. package/src/plugins/themes/gruvbox-dark.ts +45 -0
  46. package/src/plugins/themes/gruvbox-light.ts +45 -0
  47. package/src/plugins/themes/index.ts +15 -0
  48. package/src/plugins/themes/kanagawa.ts +45 -0
  49. package/src/plugins/themes/nord.ts +45 -0
  50. package/src/plugins/themes/one-dark.ts +45 -0
  51. package/src/plugins/themes/opencode.ts +47 -0
  52. package/src/plugins/themes/rose-pine-dawn.ts +45 -0
  53. package/src/plugins/themes/rose-pine.ts +45 -0
  54. package/src/plugins/themes/solarized-light.ts +45 -0
  55. package/src/plugins/themes/tokyo-night.ts +49 -0
  56. package/src/plugins/types/agent.ts +108 -0
  57. package/src/plugins/types/base.ts +267 -0
  58. package/src/plugins/types/index.ts +19 -0
  59. package/src/plugins/types/notification.ts +51 -0
  60. package/src/plugins/types/provider.ts +233 -0
  61. package/src/plugins/types/theme.ts +70 -0
  62. package/src/plugins/update-checker.ts +177 -0
  63. package/src/pricing/estimator.test.ts +181 -0
  64. package/src/pricing/estimator.ts +80 -0
  65. package/src/pricing/fallback.test.ts +71 -0
  66. package/src/pricing/fallback.ts +62 -0
  67. package/src/pricing/index.ts +66 -0
  68. package/src/pricing/models-dev.ts +130 -0
  69. package/src/storage/database.ts +613 -0
  70. package/src/storage/db.ts +64 -0
  71. package/src/storage/index.ts +66 -0
  72. package/src/storage/migrations/index.ts +189 -0
  73. package/src/storage/paths.ts +24 -0
  74. package/src/storage/repos/agentSessions.ts +210 -0
  75. package/src/storage/repos/providerSnapshots.ts +116 -0
  76. package/src/storage/repos/usageEvents.ts +334 -0
  77. package/src/storage/types.ts +288 -0
  78. package/src/tui/App.tsx +381 -0
  79. package/src/tui/components/CommandPalette.tsx +166 -0
  80. package/src/tui/components/DebugConsole.tsx +161 -0
  81. package/src/tui/components/DebugPanel.tsx +660 -0
  82. package/src/tui/components/FooterHints.tsx +22 -0
  83. package/src/tui/components/GhostProviderCard.tsx +45 -0
  84. package/src/tui/components/Header.tsx +125 -0
  85. package/src/tui/components/HelpOverlay.tsx +45 -0
  86. package/src/tui/components/InlineGauge.tsx +34 -0
  87. package/src/tui/components/InlineSparkline.tsx +78 -0
  88. package/src/tui/components/KpiStrip.tsx +246 -0
  89. package/src/tui/components/LimitGauge.tsx +186 -0
  90. package/src/tui/components/ModalBackdrop.tsx +42 -0
  91. package/src/tui/components/ProviderAggregateStrip.tsx +200 -0
  92. package/src/tui/components/ProviderCard.tsx +309 -0
  93. package/src/tui/components/ProviderDetailPanel.tsx +237 -0
  94. package/src/tui/components/ProviderLimitsPanel.tsx +308 -0
  95. package/src/tui/components/ProvidersList.tsx +380 -0
  96. package/src/tui/components/SessionDetailsDrawer.tsx +317 -0
  97. package/src/tui/components/SessionsTable.tsx +401 -0
  98. package/src/tui/components/SettingsModal.tsx +587 -0
  99. package/src/tui/components/SidebarBreakdown.tsx +87 -0
  100. package/src/tui/components/Skeleton.tsx +102 -0
  101. package/src/tui/components/SmartSidebar.tsx +644 -0
  102. package/src/tui/components/Sparkline.tsx +180 -0
  103. package/src/tui/components/Spinner.tsx +36 -0
  104. package/src/tui/components/StatusBar.tsx +96 -0
  105. package/src/tui/components/ThemePicker.tsx +234 -0
  106. package/src/tui/components/Toast.tsx +61 -0
  107. package/src/tui/components/UsageGauge.tsx +80 -0
  108. package/src/tui/components/index.ts +5 -0
  109. package/src/tui/contexts/AgentSessionContext.tsx +307 -0
  110. package/src/tui/contexts/ConfigContext.tsx +141 -0
  111. package/src/tui/contexts/DashboardRuntimeContext.tsx +102 -0
  112. package/src/tui/contexts/DemoModeContext.tsx +60 -0
  113. package/src/tui/contexts/DrawerContext.tsx +42 -0
  114. package/src/tui/contexts/InputContext.tsx +30 -0
  115. package/src/tui/contexts/LogContext.tsx +150 -0
  116. package/src/tui/contexts/PluginContext.tsx +424 -0
  117. package/src/tui/contexts/RealTimeActivityContext.tsx +119 -0
  118. package/src/tui/contexts/StorageContext.tsx +233 -0
  119. package/src/tui/contexts/ThemeContext.tsx +95 -0
  120. package/src/tui/contexts/TimeWindowContext.tsx +158 -0
  121. package/src/tui/contexts/ToastContext.tsx +40 -0
  122. package/src/tui/contexts/index.ts +5 -0
  123. package/src/tui/createApp.tsx +47 -0
  124. package/src/tui/debug/captureFrame.ts +262 -0
  125. package/src/tui/debug/snapshot.tsx +644 -0
  126. package/src/tui/driver/assertions.ts +279 -0
  127. package/src/tui/driver/cli.ts +717 -0
  128. package/src/tui/driver/coverage.ts +204 -0
  129. package/src/tui/driver/demo-snapshot.ts +66 -0
  130. package/src/tui/driver/diff.ts +166 -0
  131. package/src/tui/driver/driver.ts +270 -0
  132. package/src/tui/driver/index.ts +53 -0
  133. package/src/tui/driver/recorder.ts +303 -0
  134. package/src/tui/driver/test-workflow.ts +115 -0
  135. package/src/tui/driver/test.ts +47 -0
  136. package/src/tui/hooks/index.ts +20 -0
  137. package/src/tui/hooks/useAnimatedValue.ts +113 -0
  138. package/src/tui/hooks/useDashboardKeyboard.ts +341 -0
  139. package/src/tui/hooks/useDashboardState.ts +116 -0
  140. package/src/tui/hooks/useEmaActivity.ts +223 -0
  141. package/src/tui/hooks/useEntranceAnimation.ts +62 -0
  142. package/src/tui/hooks/useExitAnimation.ts +137 -0
  143. package/src/tui/hooks/usePulse.ts +138 -0
  144. package/src/tui/hooks/useSafeRenderer.ts +24 -0
  145. package/src/tui/hooks/useValueFlash.ts +111 -0
  146. package/src/tui/index.tsx +50 -0
  147. package/src/tui/utils/providerColor.ts +32 -0
  148. package/src/tui/views/Dashboard.tsx +340 -0
  149. package/src/tui/views/HistoricalTrendsView.tsx +1431 -0
  150. package/src/tui/views/ProjectsView.tsx +1277 -0
  151. package/src/tui/views/RealTimeDashboard.tsx +393 -0
  152. package/src/tui/views/SettingsView.tsx +465 -0
  153. package/src/tui/views/index.ts +2 -0
  154. package/src/types/global.d.ts +5 -0
  155. package/src/types/opentui-test-utils.d.ts +18 -0
  156. package/src/utils/clipboard.ts +118 -0
  157. package/src/utils/currency.test.ts +106 -0
  158. package/src/utils/currency.ts +79 -0
  159. package/src/version.ts +1 -0
@@ -0,0 +1,613 @@
1
+ import { Database } from 'bun:sqlite';
2
+ import * as fs from 'fs/promises';
3
+ import { PATHS } from './paths.ts';
4
+
5
+ let db: Database | null = null;
6
+
7
+ const SCHEMA = `
8
+ CREATE TABLE IF NOT EXISTS usage (
9
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
10
+ timestamp INTEGER NOT NULL,
11
+ provider TEXT NOT NULL,
12
+ model TEXT NOT NULL,
13
+ agent TEXT,
14
+ session_id TEXT,
15
+ input_tokens INTEGER NOT NULL DEFAULT 0,
16
+ output_tokens INTEGER NOT NULL DEFAULT 0,
17
+ cache_read_tokens INTEGER DEFAULT 0,
18
+ cache_write_tokens INTEGER DEFAULT 0,
19
+ cost_usd REAL NOT NULL DEFAULT 0,
20
+ project_path TEXT,
21
+ created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
22
+ );
23
+
24
+ CREATE INDEX IF NOT EXISTS idx_usage_timestamp ON usage(timestamp);
25
+ CREATE INDEX IF NOT EXISTS idx_usage_provider ON usage(provider, timestamp);
26
+ CREATE INDEX IF NOT EXISTS idx_usage_model ON usage(model, timestamp);
27
+ CREATE INDEX IF NOT EXISTS idx_usage_agent ON usage(agent, timestamp);
28
+ CREATE INDEX IF NOT EXISTS idx_usage_session ON usage(session_id);
29
+
30
+ CREATE TABLE IF NOT EXISTS provider_snapshots (
31
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
32
+ timestamp INTEGER NOT NULL,
33
+ provider TEXT NOT NULL,
34
+ used_percent REAL,
35
+ limit_reached INTEGER DEFAULT 0,
36
+ tokens_input INTEGER,
37
+ tokens_output INTEGER,
38
+ cost_usd REAL,
39
+ raw_json TEXT,
40
+ created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
41
+ );
42
+
43
+ CREATE INDEX IF NOT EXISTS idx_snapshots_provider ON provider_snapshots(provider, timestamp);
44
+
45
+ CREATE TABLE IF NOT EXISTS daily_aggregates (
46
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
47
+ date TEXT NOT NULL,
48
+ provider TEXT NOT NULL,
49
+ model TEXT,
50
+ total_input_tokens INTEGER NOT NULL DEFAULT 0,
51
+ total_output_tokens INTEGER NOT NULL DEFAULT 0,
52
+ total_cache_read INTEGER DEFAULT 0,
53
+ total_cache_write INTEGER DEFAULT 0,
54
+ total_cost_usd REAL NOT NULL DEFAULT 0,
55
+ request_count INTEGER NOT NULL DEFAULT 0,
56
+ UNIQUE(date, provider, model)
57
+ );
58
+
59
+ CREATE INDEX IF NOT EXISTS idx_daily_date ON daily_aggregates(date);
60
+ CREATE INDEX IF NOT EXISTS idx_daily_provider ON daily_aggregates(provider, date);
61
+
62
+ CREATE TABLE IF NOT EXISTS plugin_storage (
63
+ plugin_id TEXT NOT NULL,
64
+ key TEXT NOT NULL,
65
+ value TEXT NOT NULL,
66
+ updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
67
+ PRIMARY KEY (plugin_id, key)
68
+ );
69
+
70
+ CREATE INDEX IF NOT EXISTS idx_plugin_storage_plugin ON plugin_storage(plugin_id);
71
+ `;
72
+
73
+ export async function initDatabase(): Promise<Database> {
74
+ if (db) return db;
75
+
76
+ await fs.mkdir(PATHS.data.dir, { recursive: true });
77
+
78
+ db = new Database(PATHS.data.database, { create: true });
79
+ db.exec('PRAGMA journal_mode = WAL');
80
+ db.exec('PRAGMA synchronous = NORMAL');
81
+ db.exec(SCHEMA);
82
+
83
+ return db;
84
+ }
85
+
86
+ export function getDatabase(): Database {
87
+ if (!db) {
88
+ throw new Error('Database not initialized. Call initDatabase() first.');
89
+ }
90
+ return db;
91
+ }
92
+
93
+ export function closeDatabase(): void {
94
+ if (db) {
95
+ db.close();
96
+ db = null;
97
+ }
98
+ }
99
+
100
+ export interface UsageRecord {
101
+ timestamp: number;
102
+ provider: string;
103
+ model: string;
104
+ agent?: string | undefined;
105
+ sessionId?: string | undefined;
106
+ inputTokens: number;
107
+ outputTokens: number;
108
+ cacheReadTokens?: number | undefined;
109
+ cacheWriteTokens?: number | undefined;
110
+ costUsd: number;
111
+ projectPath?: string | undefined;
112
+ }
113
+
114
+ export interface ProviderSnapshot {
115
+ timestamp: number;
116
+ provider: string;
117
+ usedPercent?: number | undefined;
118
+ limitReached?: boolean | undefined;
119
+ tokensInput?: number | undefined;
120
+ tokensOutput?: number | undefined;
121
+ costUsd?: number | undefined;
122
+ rawJson?: string | undefined;
123
+ }
124
+
125
+ export interface DailyAggregate {
126
+ date: string;
127
+ provider: string;
128
+ model?: string | undefined;
129
+ totalInputTokens: number;
130
+ totalOutputTokens: number;
131
+ totalCacheRead: number;
132
+ totalCacheWrite: number;
133
+ totalCostUsd: number;
134
+ requestCount: number;
135
+ }
136
+
137
+ export interface UsageQuery {
138
+ startTime?: number;
139
+ endTime?: number;
140
+ provider?: string;
141
+ model?: string;
142
+ agent?: string;
143
+ sessionId?: string;
144
+ limit?: number;
145
+ offset?: number;
146
+ }
147
+
148
+ export function insertUsage(record: UsageRecord): void {
149
+ const db = getDatabase();
150
+ const stmt = db.prepare(`
151
+ INSERT INTO usage (
152
+ timestamp, provider, model, agent, session_id,
153
+ input_tokens, output_tokens, cache_read_tokens, cache_write_tokens,
154
+ cost_usd, project_path
155
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
156
+ `);
157
+
158
+ stmt.run(
159
+ record.timestamp,
160
+ record.provider,
161
+ record.model,
162
+ record.agent ?? null,
163
+ record.sessionId ?? null,
164
+ record.inputTokens,
165
+ record.outputTokens,
166
+ record.cacheReadTokens ?? 0,
167
+ record.cacheWriteTokens ?? 0,
168
+ record.costUsd,
169
+ record.projectPath ?? null
170
+ );
171
+ }
172
+
173
+ export function insertUsageBatch(records: UsageRecord[]): void {
174
+ const db = getDatabase();
175
+ const stmt = db.prepare(`
176
+ INSERT INTO usage (
177
+ timestamp, provider, model, agent, session_id,
178
+ input_tokens, output_tokens, cache_read_tokens, cache_write_tokens,
179
+ cost_usd, project_path
180
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
181
+ `);
182
+
183
+ const transaction = db.transaction((records: UsageRecord[]) => {
184
+ for (const record of records) {
185
+ stmt.run(
186
+ record.timestamp,
187
+ record.provider,
188
+ record.model,
189
+ record.agent ?? null,
190
+ record.sessionId ?? null,
191
+ record.inputTokens,
192
+ record.outputTokens,
193
+ record.cacheReadTokens ?? 0,
194
+ record.cacheWriteTokens ?? 0,
195
+ record.costUsd,
196
+ record.projectPath ?? null
197
+ );
198
+ }
199
+ });
200
+
201
+ transaction(records);
202
+ }
203
+
204
+ export function insertProviderSnapshot(snapshot: ProviderSnapshot): void {
205
+ const db = getDatabase();
206
+ const stmt = db.prepare(`
207
+ INSERT INTO provider_snapshots (
208
+ timestamp, provider, used_percent, limit_reached,
209
+ tokens_input, tokens_output, cost_usd, raw_json
210
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
211
+ `);
212
+
213
+ stmt.run(
214
+ snapshot.timestamp,
215
+ snapshot.provider,
216
+ snapshot.usedPercent ?? null,
217
+ snapshot.limitReached ? 1 : 0,
218
+ snapshot.tokensInput ?? null,
219
+ snapshot.tokensOutput ?? null,
220
+ snapshot.costUsd ?? null,
221
+ snapshot.rawJson ?? null
222
+ );
223
+ }
224
+
225
+ export function queryUsage(query: UsageQuery): UsageRecord[] {
226
+ const db = getDatabase();
227
+ const conditions: string[] = [];
228
+ const params: (string | number)[] = [];
229
+
230
+ if (query.startTime !== undefined) {
231
+ conditions.push('timestamp >= ?');
232
+ params.push(query.startTime);
233
+ }
234
+ if (query.endTime !== undefined) {
235
+ conditions.push('timestamp <= ?');
236
+ params.push(query.endTime);
237
+ }
238
+ if (query.provider) {
239
+ conditions.push('provider = ?');
240
+ params.push(query.provider);
241
+ }
242
+ if (query.model) {
243
+ conditions.push('model = ?');
244
+ params.push(query.model);
245
+ }
246
+ if (query.agent) {
247
+ conditions.push('agent = ?');
248
+ params.push(query.agent);
249
+ }
250
+ if (query.sessionId) {
251
+ conditions.push('session_id = ?');
252
+ params.push(query.sessionId);
253
+ }
254
+
255
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
256
+ const limitClause = query.limit ? `LIMIT ${query.limit}` : '';
257
+ const offsetClause = query.offset ? `OFFSET ${query.offset}` : '';
258
+
259
+ const sql = `
260
+ SELECT * FROM usage
261
+ ${whereClause}
262
+ ORDER BY timestamp DESC
263
+ ${limitClause} ${offsetClause}
264
+ `;
265
+
266
+ const rows = db.prepare(sql).all(...params) as Array<{
267
+ timestamp: number;
268
+ provider: string;
269
+ model: string;
270
+ agent: string | null;
271
+ session_id: string | null;
272
+ input_tokens: number;
273
+ output_tokens: number;
274
+ cache_read_tokens: number;
275
+ cache_write_tokens: number;
276
+ cost_usd: number;
277
+ project_path: string | null;
278
+ }>;
279
+
280
+ return rows.map((row) => ({
281
+ timestamp: row.timestamp,
282
+ provider: row.provider,
283
+ model: row.model,
284
+ agent: row.agent ?? undefined,
285
+ sessionId: row.session_id ?? undefined,
286
+ inputTokens: row.input_tokens,
287
+ outputTokens: row.output_tokens,
288
+ cacheReadTokens: row.cache_read_tokens,
289
+ cacheWriteTokens: row.cache_write_tokens,
290
+ costUsd: row.cost_usd,
291
+ projectPath: row.project_path ?? undefined,
292
+ }));
293
+ }
294
+
295
+ export interface UsageSummary {
296
+ totalInputTokens: number;
297
+ totalOutputTokens: number;
298
+ totalCacheRead: number;
299
+ totalCacheWrite: number;
300
+ totalCost: number;
301
+ requestCount: number;
302
+ }
303
+
304
+ export function getUsageSummary(startTime: number, endTime: number): UsageSummary {
305
+ const db = getDatabase();
306
+ const row = db.prepare(`
307
+ SELECT
308
+ COALESCE(SUM(input_tokens), 0) as total_input,
309
+ COALESCE(SUM(output_tokens), 0) as total_output,
310
+ COALESCE(SUM(cache_read_tokens), 0) as total_cache_read,
311
+ COALESCE(SUM(cache_write_tokens), 0) as total_cache_write,
312
+ COALESCE(SUM(cost_usd), 0) as total_cost,
313
+ COUNT(*) as request_count
314
+ FROM usage
315
+ WHERE timestamp >= ? AND timestamp <= ?
316
+ `).get(startTime, endTime) as {
317
+ total_input: number;
318
+ total_output: number;
319
+ total_cache_read: number;
320
+ total_cache_write: number;
321
+ total_cost: number;
322
+ request_count: number;
323
+ };
324
+
325
+ return {
326
+ totalInputTokens: row.total_input,
327
+ totalOutputTokens: row.total_output,
328
+ totalCacheRead: row.total_cache_read,
329
+ totalCacheWrite: row.total_cache_write,
330
+ totalCost: row.total_cost,
331
+ requestCount: row.request_count,
332
+ };
333
+ }
334
+
335
+ export interface ProviderSummary {
336
+ provider: string;
337
+ totalInputTokens: number;
338
+ totalOutputTokens: number;
339
+ totalCost: number;
340
+ requestCount: number;
341
+ }
342
+
343
+ export function getUsageByProvider(startTime: number, endTime: number): ProviderSummary[] {
344
+ const db = getDatabase();
345
+ const rows = db.prepare(`
346
+ SELECT
347
+ provider,
348
+ COALESCE(SUM(input_tokens), 0) as total_input,
349
+ COALESCE(SUM(output_tokens), 0) as total_output,
350
+ COALESCE(SUM(cost_usd), 0) as total_cost,
351
+ COUNT(*) as request_count
352
+ FROM usage
353
+ WHERE timestamp >= ? AND timestamp <= ?
354
+ GROUP BY provider
355
+ ORDER BY total_cost DESC
356
+ `).all(startTime, endTime) as Array<{
357
+ provider: string;
358
+ total_input: number;
359
+ total_output: number;
360
+ total_cost: number;
361
+ request_count: number;
362
+ }>;
363
+
364
+ return rows.map((row) => ({
365
+ provider: row.provider,
366
+ totalInputTokens: row.total_input,
367
+ totalOutputTokens: row.total_output,
368
+ totalCost: row.total_cost,
369
+ requestCount: row.request_count,
370
+ }));
371
+ }
372
+
373
+ export interface ModelSummary {
374
+ model: string;
375
+ provider: string;
376
+ totalInputTokens: number;
377
+ totalOutputTokens: number;
378
+ totalCost: number;
379
+ requestCount: number;
380
+ }
381
+
382
+ export function getUsageByModel(startTime: number, endTime: number): ModelSummary[] {
383
+ const db = getDatabase();
384
+ const rows = db.prepare(`
385
+ SELECT
386
+ model,
387
+ provider,
388
+ COALESCE(SUM(input_tokens), 0) as total_input,
389
+ COALESCE(SUM(output_tokens), 0) as total_output,
390
+ COALESCE(SUM(cost_usd), 0) as total_cost,
391
+ COUNT(*) as request_count
392
+ FROM usage
393
+ WHERE timestamp >= ? AND timestamp <= ?
394
+ GROUP BY model, provider
395
+ ORDER BY total_cost DESC
396
+ `).all(startTime, endTime) as Array<{
397
+ model: string;
398
+ provider: string;
399
+ total_input: number;
400
+ total_output: number;
401
+ total_cost: number;
402
+ request_count: number;
403
+ }>;
404
+
405
+ return rows.map((row) => ({
406
+ model: row.model,
407
+ provider: row.provider,
408
+ totalInputTokens: row.total_input,
409
+ totalOutputTokens: row.total_output,
410
+ totalCost: row.total_cost,
411
+ requestCount: row.request_count,
412
+ }));
413
+ }
414
+
415
+ export interface TimeSeriesPoint {
416
+ timestamp: number;
417
+ totalTokens: number;
418
+ totalCost: number;
419
+ requestCount: number;
420
+ }
421
+
422
+ export function getUsageTimeSeries(
423
+ startTime: number,
424
+ endTime: number,
425
+ bucketMinutes: number = 5
426
+ ): TimeSeriesPoint[] {
427
+ const db = getDatabase();
428
+ const bucketSeconds = bucketMinutes * 60;
429
+
430
+ const rows = db.prepare(`
431
+ SELECT
432
+ (timestamp / ? * ?) as bucket_time,
433
+ COALESCE(SUM(input_tokens + output_tokens), 0) as total_tokens,
434
+ COALESCE(SUM(cost_usd), 0) as total_cost,
435
+ COUNT(*) as request_count
436
+ FROM usage
437
+ WHERE timestamp >= ? AND timestamp <= ?
438
+ GROUP BY bucket_time
439
+ ORDER BY bucket_time ASC
440
+ `).all(bucketSeconds, bucketSeconds, startTime, endTime) as Array<{
441
+ bucket_time: number;
442
+ total_tokens: number;
443
+ total_cost: number;
444
+ request_count: number;
445
+ }>;
446
+
447
+ return rows.map((row) => ({
448
+ timestamp: row.bucket_time,
449
+ totalTokens: row.total_tokens,
450
+ totalCost: row.total_cost,
451
+ requestCount: row.request_count,
452
+ }));
453
+ }
454
+
455
+ export function getProviderSnapshots(
456
+ provider: string,
457
+ startTime: number,
458
+ endTime: number,
459
+ limit: number = 100
460
+ ): ProviderSnapshot[] {
461
+ const db = getDatabase();
462
+ const rows = db.prepare(`
463
+ SELECT * FROM provider_snapshots
464
+ WHERE provider = ? AND timestamp >= ? AND timestamp <= ?
465
+ ORDER BY timestamp DESC
466
+ LIMIT ?
467
+ `).all(provider, startTime, endTime, limit) as Array<{
468
+ timestamp: number;
469
+ provider: string;
470
+ used_percent: number | null;
471
+ limit_reached: number;
472
+ tokens_input: number | null;
473
+ tokens_output: number | null;
474
+ cost_usd: number | null;
475
+ raw_json: string | null;
476
+ }>;
477
+
478
+ return rows.map((row) => ({
479
+ timestamp: row.timestamp,
480
+ provider: row.provider,
481
+ usedPercent: row.used_percent ?? undefined,
482
+ limitReached: row.limit_reached === 1,
483
+ tokensInput: row.tokens_input ?? undefined,
484
+ tokensOutput: row.tokens_output ?? undefined,
485
+ costUsd: row.cost_usd ?? undefined,
486
+ rawJson: row.raw_json ?? undefined,
487
+ }));
488
+ }
489
+
490
+ export function upsertDailyAggregate(aggregate: DailyAggregate): void {
491
+ const db = getDatabase();
492
+ const stmt = db.prepare(`
493
+ INSERT INTO daily_aggregates (
494
+ date, provider, model,
495
+ total_input_tokens, total_output_tokens,
496
+ total_cache_read, total_cache_write,
497
+ total_cost_usd, request_count
498
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
499
+ ON CONFLICT(date, provider, model) DO UPDATE SET
500
+ total_input_tokens = total_input_tokens + excluded.total_input_tokens,
501
+ total_output_tokens = total_output_tokens + excluded.total_output_tokens,
502
+ total_cache_read = total_cache_read + excluded.total_cache_read,
503
+ total_cache_write = total_cache_write + excluded.total_cache_write,
504
+ total_cost_usd = total_cost_usd + excluded.total_cost_usd,
505
+ request_count = request_count + excluded.request_count
506
+ `);
507
+
508
+ stmt.run(
509
+ aggregate.date,
510
+ aggregate.provider,
511
+ aggregate.model ?? null,
512
+ aggregate.totalInputTokens,
513
+ aggregate.totalOutputTokens,
514
+ aggregate.totalCacheRead,
515
+ aggregate.totalCacheWrite,
516
+ aggregate.totalCostUsd,
517
+ aggregate.requestCount
518
+ );
519
+ }
520
+
521
+ export function getDailyAggregates(
522
+ startDate: string,
523
+ endDate: string,
524
+ provider?: string
525
+ ): DailyAggregate[] {
526
+ const db = getDatabase();
527
+ const conditions = ['date >= ?', 'date <= ?'];
528
+ const params: string[] = [startDate, endDate];
529
+
530
+ if (provider) {
531
+ conditions.push('provider = ?');
532
+ params.push(provider);
533
+ }
534
+
535
+ const rows = db.prepare(`
536
+ SELECT * FROM daily_aggregates
537
+ WHERE ${conditions.join(' AND ')}
538
+ ORDER BY date DESC, total_cost_usd DESC
539
+ `).all(...params) as Array<{
540
+ date: string;
541
+ provider: string;
542
+ model: string | null;
543
+ total_input_tokens: number;
544
+ total_output_tokens: number;
545
+ total_cache_read: number;
546
+ total_cache_write: number;
547
+ total_cost_usd: number;
548
+ request_count: number;
549
+ }>;
550
+
551
+ return rows.map((row) => ({
552
+ date: row.date,
553
+ provider: row.provider,
554
+ model: row.model ?? undefined,
555
+ totalInputTokens: row.total_input_tokens,
556
+ totalOutputTokens: row.total_output_tokens,
557
+ totalCacheRead: row.total_cache_read,
558
+ totalCacheWrite: row.total_cache_write,
559
+ totalCostUsd: row.total_cost_usd,
560
+ requestCount: row.request_count,
561
+ }));
562
+ }
563
+
564
+ export function getTodaySummary(): UsageSummary {
565
+ const now = Date.now();
566
+ const startOfDay = new Date();
567
+ startOfDay.setHours(0, 0, 0, 0);
568
+ return getUsageSummary(startOfDay.getTime(), now);
569
+ }
570
+
571
+ export function getThisMonthSummary(): UsageSummary {
572
+ const now = Date.now();
573
+ const startOfMonth = new Date();
574
+ startOfMonth.setDate(1);
575
+ startOfMonth.setHours(0, 0, 0, 0);
576
+ return getUsageSummary(startOfMonth.getTime(), now);
577
+ }
578
+
579
+ // ---------------------------------------------------------------------------
580
+ // Plugin KV storage
581
+ // ---------------------------------------------------------------------------
582
+
583
+ export function pluginStorageGet(pluginId: string, key: string): string | null {
584
+ const db = getDatabase();
585
+ const row = db.prepare(
586
+ 'SELECT value FROM plugin_storage WHERE plugin_id = ? AND key = ?',
587
+ ).get(pluginId, key) as { value: string } | null;
588
+ return row?.value ?? null;
589
+ }
590
+
591
+ export function pluginStorageSet(pluginId: string, key: string, value: string): void {
592
+ const db = getDatabase();
593
+ db.prepare(
594
+ `INSERT INTO plugin_storage (plugin_id, key, value, updated_at)
595
+ VALUES (?, ?, ?, ?)
596
+ ON CONFLICT(plugin_id, key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`,
597
+ ).run(pluginId, key, value, Math.floor(Date.now() / 1000));
598
+ }
599
+
600
+ export function pluginStorageDelete(pluginId: string, key: string): void {
601
+ const db = getDatabase();
602
+ db.prepare(
603
+ 'DELETE FROM plugin_storage WHERE plugin_id = ? AND key = ?',
604
+ ).run(pluginId, key);
605
+ }
606
+
607
+ export function pluginStorageHas(pluginId: string, key: string): boolean {
608
+ const db = getDatabase();
609
+ const row = db.prepare(
610
+ 'SELECT 1 FROM plugin_storage WHERE plugin_id = ? AND key = ?',
611
+ ).get(pluginId, key);
612
+ return row !== null && row !== undefined;
613
+ }
@@ -0,0 +1,64 @@
1
+ import { Database } from 'bun:sqlite';
2
+ import * as fs from 'fs/promises';
3
+ import { PATHS } from './paths.ts';
4
+ import { applyMigrations } from './migrations/index.ts';
5
+
6
+ let db: Database | null = null;
7
+ let currentAppRunId: number | null = null;
8
+
9
+ export async function initDatabase(): Promise<Database> {
10
+ if (db) return db;
11
+
12
+ await fs.mkdir(PATHS.data.dir, { recursive: true });
13
+
14
+ db = new Database(PATHS.data.database, { create: true });
15
+
16
+ db.exec('PRAGMA journal_mode = WAL');
17
+ db.exec('PRAGMA synchronous = NORMAL');
18
+ db.exec('PRAGMA foreign_keys = ON');
19
+ db.exec('PRAGMA busy_timeout = 2000');
20
+
21
+ applyMigrations(db);
22
+
23
+ const stmt = db.prepare(`
24
+ INSERT INTO app_runs (started_at, app_version, pid, hostname)
25
+ VALUES (?, ?, ?, ?)
26
+ `);
27
+
28
+ const result = stmt.run(
29
+ Date.now(),
30
+ process.env.npm_package_version ?? '0.0.0',
31
+ process.pid,
32
+ require('os').hostname()
33
+ );
34
+
35
+ currentAppRunId = Number(result.lastInsertRowid);
36
+
37
+ return db;
38
+ }
39
+
40
+ export function getDatabase(): Database {
41
+ if (!db) {
42
+ throw new Error('Database not initialized. Call initDatabase() first.');
43
+ }
44
+ return db;
45
+ }
46
+
47
+ export function getAppRunId(): number | null {
48
+ return currentAppRunId;
49
+ }
50
+
51
+ export function closeDatabase(): void {
52
+ if (db && currentAppRunId) {
53
+ db.prepare('UPDATE app_runs SET ended_at = ? WHERE id = ?').run(Date.now(), currentAppRunId);
54
+ }
55
+ if (db) {
56
+ db.close();
57
+ db = null;
58
+ currentAppRunId = null;
59
+ }
60
+ }
61
+
62
+ export function isDatabaseInitialized(): boolean {
63
+ return db !== null;
64
+ }