@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,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
+ }