@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,66 @@
1
+ export { PATHS } from './paths.ts';
2
+
3
+ export {
4
+ initDatabase,
5
+ getDatabase,
6
+ closeDatabase,
7
+ isDatabaseInitialized,
8
+ getAppRunId,
9
+ } from './db.ts';
10
+
11
+ export {
12
+ insertProviderSnapshot,
13
+ insertProviderSnapshotBatch,
14
+ queryProviderSnapshots,
15
+ getLatestProviderSnapshot,
16
+ } from './repos/providerSnapshots.ts';
17
+
18
+ export {
19
+ insertUsageEvent,
20
+ insertUsageEventBatch,
21
+ queryUsageTimeSeries,
22
+ queryProviderDailyCosts,
23
+ queryModelDailyCosts,
24
+ queryProjectDailyCosts,
25
+ calculateBurnRate,
26
+ getTotalUsageInWindow,
27
+ getSessionActivityTimeline,
28
+ } from './repos/usageEvents.ts';
29
+
30
+ export type { SessionActivityPoint, ProviderDailyCost, ModelDailyCost, ProjectDailyCost } from './repos/usageEvents.ts';
31
+
32
+ export {
33
+ upsertAgentSession,
34
+ insertAgentSessionSnapshot,
35
+ getAgentSession,
36
+ getRecentSessions,
37
+ getSessionsByProject,
38
+ getLatestStreamTotalsForAllSessions,
39
+ } from './repos/agentSessions.ts';
40
+
41
+ export type { LatestStreamTotals } from './repos/agentSessions.ts';
42
+
43
+ export type {
44
+ UsageEventSource,
45
+ SessionStatus,
46
+ CostSource,
47
+ PricingSource,
48
+ AppRunRow,
49
+ ProviderSnapshotRow,
50
+ ProviderSnapshotInsert,
51
+ AgentSessionDim,
52
+ AgentSessionUpsert,
53
+ AgentSessionSnapshotRow,
54
+ AgentSessionSnapshotInsert,
55
+ AgentSessionStreamSnapshotRow,
56
+ UsageEventRow,
57
+ UsageEventInsert,
58
+ HourlyAggregateRow,
59
+ DailyAggregateRow,
60
+ TimeSeriesPoint,
61
+ TimeSeriesFilters,
62
+ UsageQueryOptions,
63
+ StreamTotals,
64
+ } from './types.ts';
65
+
66
+ export { computeStreamDelta } from './types.ts';
@@ -0,0 +1,189 @@
1
+ import type { Database } from 'bun:sqlite';
2
+
3
+ export interface Migration {
4
+ version: number;
5
+ up: (db: Database) => void;
6
+ }
7
+
8
+ export const migrations: Migration[] = [
9
+ {
10
+ version: 1,
11
+ up: (db: Database) => {
12
+ db.exec(`
13
+ CREATE TABLE IF NOT EXISTS meta (
14
+ key TEXT PRIMARY KEY,
15
+ value TEXT NOT NULL
16
+ );
17
+
18
+ CREATE TABLE IF NOT EXISTS app_runs (
19
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
20
+ started_at INTEGER NOT NULL,
21
+ ended_at INTEGER,
22
+ app_version TEXT,
23
+ refresh_interval_ms INTEGER,
24
+ pid INTEGER,
25
+ hostname TEXT
26
+ );
27
+
28
+ CREATE TABLE IF NOT EXISTS provider_snapshots (
29
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
30
+ timestamp INTEGER NOT NULL,
31
+ provider TEXT NOT NULL,
32
+ used_percent REAL,
33
+ limit_reached INTEGER NOT NULL DEFAULT 0,
34
+ resets_at INTEGER,
35
+ tokens_input INTEGER,
36
+ tokens_output INTEGER,
37
+ cost_usd REAL,
38
+ cost_source TEXT,
39
+ raw_json TEXT,
40
+ created_at INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)
41
+ );
42
+
43
+ CREATE INDEX IF NOT EXISTS idx_provider_snapshots_provider_ts
44
+ ON provider_snapshots(provider, timestamp);
45
+
46
+ CREATE INDEX IF NOT EXISTS idx_provider_snapshots_ts
47
+ ON provider_snapshots(timestamp);
48
+
49
+ CREATE TABLE IF NOT EXISTS agent_sessions (
50
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
51
+ agent_id TEXT NOT NULL,
52
+ session_id TEXT NOT NULL,
53
+ project_path TEXT,
54
+ started_at INTEGER,
55
+ first_seen_at INTEGER NOT NULL,
56
+ last_seen_at INTEGER NOT NULL,
57
+ UNIQUE(agent_id, session_id)
58
+ );
59
+
60
+ CREATE INDEX IF NOT EXISTS idx_agent_sessions_last_seen
61
+ ON agent_sessions(last_seen_at);
62
+
63
+ CREATE INDEX IF NOT EXISTS idx_agent_sessions_project
64
+ ON agent_sessions(project_path);
65
+
66
+ CREATE TABLE IF NOT EXISTS agent_session_snapshots (
67
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
68
+ timestamp INTEGER NOT NULL,
69
+ agent_session_id INTEGER NOT NULL REFERENCES agent_sessions(id) ON DELETE CASCADE,
70
+ last_activity_at INTEGER,
71
+ status TEXT,
72
+ total_input_tokens INTEGER NOT NULL DEFAULT 0,
73
+ total_output_tokens INTEGER NOT NULL DEFAULT 0,
74
+ total_cache_read_tokens INTEGER NOT NULL DEFAULT 0,
75
+ total_cache_write_tokens INTEGER NOT NULL DEFAULT 0,
76
+ total_cost_usd REAL NOT NULL DEFAULT 0,
77
+ request_count INTEGER NOT NULL DEFAULT 0
78
+ );
79
+
80
+ CREATE INDEX IF NOT EXISTS idx_agent_session_snapshots_session_ts
81
+ ON agent_session_snapshots(agent_session_id, timestamp);
82
+
83
+ CREATE INDEX IF NOT EXISTS idx_agent_session_snapshots_ts
84
+ ON agent_session_snapshots(timestamp);
85
+
86
+ CREATE TABLE IF NOT EXISTS agent_session_stream_snapshots (
87
+ agent_session_snapshot_id INTEGER NOT NULL REFERENCES agent_session_snapshots(id) ON DELETE CASCADE,
88
+ provider TEXT NOT NULL,
89
+ model TEXT NOT NULL,
90
+ input_tokens INTEGER NOT NULL DEFAULT 0,
91
+ output_tokens INTEGER NOT NULL DEFAULT 0,
92
+ cache_read_tokens INTEGER NOT NULL DEFAULT 0,
93
+ cache_write_tokens INTEGER NOT NULL DEFAULT 0,
94
+ cost_usd REAL NOT NULL DEFAULT 0,
95
+ request_count INTEGER NOT NULL DEFAULT 0,
96
+ pricing_source TEXT,
97
+ PRIMARY KEY (agent_session_snapshot_id, provider, model)
98
+ );
99
+
100
+ CREATE INDEX IF NOT EXISTS idx_agent_stream_provider_model
101
+ ON agent_session_stream_snapshots(provider, model);
102
+
103
+ CREATE TABLE IF NOT EXISTS usage_events (
104
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
105
+ timestamp INTEGER NOT NULL,
106
+ source TEXT NOT NULL,
107
+ provider TEXT,
108
+ model TEXT,
109
+ agent_id TEXT,
110
+ session_id TEXT,
111
+ project_path TEXT,
112
+ input_tokens INTEGER NOT NULL DEFAULT 0,
113
+ output_tokens INTEGER NOT NULL DEFAULT 0,
114
+ cache_read_tokens INTEGER NOT NULL DEFAULT 0,
115
+ cache_write_tokens INTEGER NOT NULL DEFAULT 0,
116
+ cost_usd REAL NOT NULL DEFAULT 0,
117
+ request_count INTEGER NOT NULL DEFAULT 0,
118
+ pricing_source TEXT,
119
+ created_at INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)
120
+ );
121
+
122
+ CREATE INDEX IF NOT EXISTS idx_usage_events_ts ON usage_events(timestamp);
123
+ CREATE INDEX IF NOT EXISTS idx_usage_events_provider_ts ON usage_events(provider, timestamp);
124
+ CREATE INDEX IF NOT EXISTS idx_usage_events_provider_model_ts ON usage_events(provider, model, timestamp);
125
+ CREATE INDEX IF NOT EXISTS idx_usage_events_agent_ts ON usage_events(agent_id, timestamp);
126
+ CREATE INDEX IF NOT EXISTS idx_usage_events_session_ts ON usage_events(session_id, timestamp);
127
+ CREATE INDEX IF NOT EXISTS idx_usage_events_project_ts ON usage_events(project_path, timestamp);
128
+
129
+ CREATE TABLE IF NOT EXISTS hourly_aggregates (
130
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
131
+ bucket_start INTEGER NOT NULL,
132
+ provider TEXT NOT NULL DEFAULT '',
133
+ model TEXT NOT NULL DEFAULT '',
134
+ agent_id TEXT NOT NULL DEFAULT '',
135
+ project_path TEXT NOT NULL DEFAULT '',
136
+ input_tokens INTEGER NOT NULL DEFAULT 0,
137
+ output_tokens INTEGER NOT NULL DEFAULT 0,
138
+ cache_read_tokens INTEGER NOT NULL DEFAULT 0,
139
+ cache_write_tokens INTEGER NOT NULL DEFAULT 0,
140
+ cost_usd REAL NOT NULL DEFAULT 0,
141
+ request_count INTEGER NOT NULL DEFAULT 0,
142
+ UNIQUE (bucket_start, provider, model, agent_id, project_path)
143
+ );
144
+
145
+ CREATE INDEX IF NOT EXISTS idx_hourly_aggregates_bucket ON hourly_aggregates(bucket_start);
146
+
147
+ CREATE TABLE IF NOT EXISTS daily_aggregates (
148
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
149
+ date TEXT NOT NULL,
150
+ provider TEXT NOT NULL DEFAULT '',
151
+ model TEXT NOT NULL DEFAULT '',
152
+ agent_id TEXT NOT NULL DEFAULT '',
153
+ project_path TEXT NOT NULL DEFAULT '',
154
+ input_tokens INTEGER NOT NULL DEFAULT 0,
155
+ output_tokens INTEGER NOT NULL DEFAULT 0,
156
+ cache_read_tokens INTEGER NOT NULL DEFAULT 0,
157
+ cache_write_tokens INTEGER NOT NULL DEFAULT 0,
158
+ cost_usd REAL NOT NULL DEFAULT 0,
159
+ request_count INTEGER NOT NULL DEFAULT 0,
160
+ UNIQUE (date, provider, model, agent_id, project_path)
161
+ );
162
+
163
+ CREATE INDEX IF NOT EXISTS idx_daily_aggregates_date ON daily_aggregates(date);
164
+ `);
165
+ },
166
+ },
167
+ ];
168
+
169
+ export function getCurrentVersion(db: Database): number {
170
+ const result = db.prepare('PRAGMA user_version').get() as { user_version: number } | null;
171
+ return result?.user_version ?? 0;
172
+ }
173
+
174
+ export function setVersion(db: Database, version: number): void {
175
+ db.exec(`PRAGMA user_version = ${version}`);
176
+ }
177
+
178
+ export function applyMigrations(db: Database): void {
179
+ const currentVersion = getCurrentVersion(db);
180
+
181
+ for (const migration of migrations) {
182
+ if (migration.version > currentVersion) {
183
+ db.transaction(() => {
184
+ migration.up(db);
185
+ setVersion(db, migration.version);
186
+ })();
187
+ }
188
+ }
189
+ }
@@ -0,0 +1,24 @@
1
+ import * as path from 'path';
2
+ import * as os from 'os';
3
+
4
+ const HOME = os.homedir();
5
+
6
+ export const PATHS = {
7
+ config: {
8
+ dir: path.join(HOME, '.config/tokentop'),
9
+ file: path.join(HOME, '.config/tokentop/config.json'),
10
+ fileJsonc: path.join(HOME, '.config/tokentop/config.jsonc'),
11
+ plugins: path.join(HOME, '.config/tokentop/plugins'),
12
+ },
13
+ data: {
14
+ dir: path.join(HOME, '.local/share/tokentop'),
15
+ database: path.join(HOME, '.local/share/tokentop/usage.db'),
16
+ sessions: path.join(HOME, '.local/share/tokentop/sessions'),
17
+ cache: path.join(HOME, '.local/share/tokentop/cache'),
18
+ logs: path.join(HOME, '.local/share/tokentop/logs'),
19
+ },
20
+ cache: {
21
+ dir: path.join(HOME, '.cache/tokentop'),
22
+ nodeModules: path.join(HOME, '.cache/tokentop/node_modules'),
23
+ },
24
+ } as const;
@@ -0,0 +1,210 @@
1
+ import { getDatabase } from '../db.ts';
2
+ import type {
3
+ AgentSessionDim,
4
+ AgentSessionUpsert,
5
+ AgentSessionSnapshotInsert,
6
+ AgentSessionStreamSnapshotRow,
7
+ } from '../types.ts';
8
+
9
+ const UPSERT_SESSION_SQL = `
10
+ INSERT INTO agent_sessions (agent_id, session_id, project_path, started_at, first_seen_at, last_seen_at)
11
+ VALUES (?, ?, ?, ?, ?, ?)
12
+ ON CONFLICT(agent_id, session_id) DO UPDATE SET
13
+ project_path = COALESCE(excluded.project_path, agent_sessions.project_path),
14
+ started_at = COALESCE(excluded.started_at, agent_sessions.started_at),
15
+ last_seen_at = excluded.last_seen_at
16
+ RETURNING id
17
+ `;
18
+
19
+ const INSERT_SNAPSHOT_SQL = `
20
+ INSERT INTO agent_session_snapshots (
21
+ timestamp, agent_session_id, last_activity_at, status,
22
+ total_input_tokens, total_output_tokens, total_cache_read_tokens, total_cache_write_tokens,
23
+ total_cost_usd, request_count
24
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
25
+ `;
26
+
27
+ const INSERT_STREAM_SQL = `
28
+ INSERT INTO agent_session_stream_snapshots (
29
+ agent_session_snapshot_id, provider, model,
30
+ input_tokens, output_tokens, cache_read_tokens, cache_write_tokens,
31
+ cost_usd, request_count, pricing_source
32
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
33
+ `;
34
+
35
+ export function upsertAgentSession(session: AgentSessionUpsert): number {
36
+ const db = getDatabase();
37
+ const now = Date.now();
38
+
39
+ const row = db.prepare(UPSERT_SESSION_SQL).get(
40
+ session.agentId,
41
+ session.sessionId,
42
+ session.projectPath ?? null,
43
+ session.startedAt ?? null,
44
+ session.firstSeenAt ?? now,
45
+ session.lastSeenAt
46
+ ) as { id: number };
47
+
48
+ return row.id;
49
+ }
50
+
51
+ export function insertAgentSessionSnapshot(
52
+ snapshot: AgentSessionSnapshotInsert,
53
+ streams: Omit<AgentSessionStreamSnapshotRow, 'agentSessionSnapshotId'>[]
54
+ ): number {
55
+ const db = getDatabase();
56
+
57
+ const insertSnapshot = db.prepare(INSERT_SNAPSHOT_SQL);
58
+ const insertStream = db.prepare(INSERT_STREAM_SQL);
59
+
60
+ let snapshotId = 0;
61
+
62
+ db.transaction(() => {
63
+ const result = insertSnapshot.run(
64
+ snapshot.timestamp,
65
+ snapshot.agentSessionId,
66
+ snapshot.lastActivityAt ?? null,
67
+ snapshot.status ?? null,
68
+ snapshot.totalInputTokens,
69
+ snapshot.totalOutputTokens,
70
+ snapshot.totalCacheReadTokens,
71
+ snapshot.totalCacheWriteTokens,
72
+ snapshot.totalCostUsd,
73
+ snapshot.requestCount
74
+ );
75
+
76
+ snapshotId = Number(result.lastInsertRowid);
77
+
78
+ for (const stream of streams) {
79
+ insertStream.run(
80
+ snapshotId,
81
+ stream.provider,
82
+ stream.model,
83
+ stream.inputTokens,
84
+ stream.outputTokens,
85
+ stream.cacheReadTokens,
86
+ stream.cacheWriteTokens,
87
+ stream.costUsd,
88
+ stream.requestCount,
89
+ stream.pricingSource ?? null
90
+ );
91
+ }
92
+ })();
93
+
94
+ return snapshotId;
95
+ }
96
+
97
+ export function getAgentSession(agentId: string, sessionId: string): AgentSessionDim | null {
98
+ const db = getDatabase();
99
+
100
+ const row = db.prepare(`
101
+ SELECT * FROM agent_sessions WHERE agent_id = ? AND session_id = ?
102
+ `).get(agentId, sessionId) as DbSessionRow | null;
103
+
104
+ return row ? mapSessionRow(row) : null;
105
+ }
106
+
107
+ export function getRecentSessions(limit = 50): AgentSessionDim[] {
108
+ const db = getDatabase();
109
+
110
+ const rows = db.prepare(`
111
+ SELECT * FROM agent_sessions
112
+ ORDER BY last_seen_at DESC
113
+ LIMIT ?
114
+ `).all(limit) as DbSessionRow[];
115
+
116
+ return rows.map(mapSessionRow);
117
+ }
118
+
119
+ export function getSessionsByProject(projectPath: string, limit = 50): AgentSessionDim[] {
120
+ const db = getDatabase();
121
+
122
+ const rows = db.prepare(`
123
+ SELECT * FROM agent_sessions
124
+ WHERE project_path = ?
125
+ ORDER BY last_seen_at DESC
126
+ LIMIT ?
127
+ `).all(projectPath, limit) as DbSessionRow[];
128
+
129
+ return rows.map(mapSessionRow);
130
+ }
131
+
132
+ interface DbSessionRow {
133
+ id: number;
134
+ agent_id: string;
135
+ session_id: string;
136
+ project_path: string | null;
137
+ started_at: number | null;
138
+ first_seen_at: number;
139
+ last_seen_at: number;
140
+ }
141
+
142
+ function mapSessionRow(row: DbSessionRow): AgentSessionDim {
143
+ return {
144
+ id: row.id,
145
+ agentId: row.agent_id,
146
+ sessionId: row.session_id,
147
+ projectPath: row.project_path,
148
+ startedAt: row.started_at,
149
+ firstSeenAt: row.first_seen_at,
150
+ lastSeenAt: row.last_seen_at,
151
+ };
152
+ }
153
+
154
+ export interface LatestStreamTotals {
155
+ agentId: string;
156
+ sessionId: string;
157
+ provider: string;
158
+ model: string;
159
+ inputTokens: number;
160
+ outputTokens: number;
161
+ cacheReadTokens: number;
162
+ cacheWriteTokens: number;
163
+ costUsd: number;
164
+ requestCount: number;
165
+ }
166
+
167
+ export function getLatestStreamTotalsForAllSessions(): LatestStreamTotals[] {
168
+ const db = getDatabase();
169
+
170
+ const rows = db.prepare(`
171
+ SELECT
172
+ s.agent_id, s.session_id,
173
+ ss.provider, ss.model,
174
+ ss.input_tokens, ss.output_tokens,
175
+ ss.cache_read_tokens, ss.cache_write_tokens,
176
+ ss.cost_usd, ss.request_count
177
+ FROM agent_session_stream_snapshots ss
178
+ JOIN agent_session_snapshots snap ON snap.id = ss.agent_session_snapshot_id
179
+ JOIN agent_sessions s ON s.id = snap.agent_session_id
180
+ WHERE snap.id IN (
181
+ SELECT MAX(snap2.id)
182
+ FROM agent_session_snapshots snap2
183
+ GROUP BY snap2.agent_session_id
184
+ )
185
+ `).all() as Array<{
186
+ agent_id: string;
187
+ session_id: string;
188
+ provider: string;
189
+ model: string;
190
+ input_tokens: number;
191
+ output_tokens: number;
192
+ cache_read_tokens: number;
193
+ cache_write_tokens: number;
194
+ cost_usd: number;
195
+ request_count: number;
196
+ }>;
197
+
198
+ return rows.map(r => ({
199
+ agentId: r.agent_id,
200
+ sessionId: r.session_id,
201
+ provider: r.provider,
202
+ model: r.model,
203
+ inputTokens: r.input_tokens,
204
+ outputTokens: r.output_tokens,
205
+ cacheReadTokens: r.cache_read_tokens,
206
+ cacheWriteTokens: r.cache_write_tokens,
207
+ costUsd: r.cost_usd,
208
+ requestCount: r.request_count,
209
+ }));
210
+ }
@@ -0,0 +1,116 @@
1
+ import { getDatabase } from '../db.ts';
2
+ import type { ProviderSnapshotInsert, ProviderSnapshotRow, CostSource } from '../types.ts';
3
+
4
+ const INSERT_SQL = `
5
+ INSERT INTO provider_snapshots (
6
+ timestamp, provider, used_percent, limit_reached, resets_at,
7
+ tokens_input, tokens_output, cost_usd, cost_source, raw_json
8
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
9
+ `;
10
+
11
+ const QUERY_BY_PROVIDER_SQL = `
12
+ SELECT * FROM provider_snapshots
13
+ WHERE provider = ? AND timestamp >= ? AND timestamp <= ?
14
+ ORDER BY timestamp DESC
15
+ LIMIT ?
16
+ `;
17
+
18
+ const QUERY_LATEST_SQL = `
19
+ SELECT * FROM provider_snapshots
20
+ WHERE provider = ?
21
+ ORDER BY timestamp DESC
22
+ LIMIT 1
23
+ `;
24
+
25
+ export function insertProviderSnapshot(snapshot: ProviderSnapshotInsert): number {
26
+ const db = getDatabase();
27
+ const stmt = db.prepare(INSERT_SQL);
28
+
29
+ const result = stmt.run(
30
+ snapshot.timestamp,
31
+ snapshot.provider,
32
+ snapshot.usedPercent ?? null,
33
+ snapshot.limitReached ? 1 : 0,
34
+ snapshot.resetsAt ?? null,
35
+ snapshot.tokensInput ?? null,
36
+ snapshot.tokensOutput ?? null,
37
+ snapshot.costUsd ?? null,
38
+ snapshot.costSource ?? null,
39
+ snapshot.rawJson ?? null
40
+ );
41
+
42
+ return Number(result.lastInsertRowid);
43
+ }
44
+
45
+ export function insertProviderSnapshotBatch(snapshots: ProviderSnapshotInsert[]): void {
46
+ if (snapshots.length === 0) return;
47
+
48
+ const db = getDatabase();
49
+ const stmt = db.prepare(INSERT_SQL);
50
+
51
+ const insertMany = db.transaction((items: ProviderSnapshotInsert[]) => {
52
+ for (const s of items) {
53
+ stmt.run(
54
+ s.timestamp,
55
+ s.provider,
56
+ s.usedPercent ?? null,
57
+ s.limitReached ? 1 : 0,
58
+ s.resetsAt ?? null,
59
+ s.tokensInput ?? null,
60
+ s.tokensOutput ?? null,
61
+ s.costUsd ?? null,
62
+ s.costSource ?? null,
63
+ s.rawJson ?? null
64
+ );
65
+ }
66
+ });
67
+
68
+ insertMany(snapshots);
69
+ }
70
+
71
+ export function queryProviderSnapshots(
72
+ provider: string,
73
+ startMs: number,
74
+ endMs: number,
75
+ limit = 100
76
+ ): ProviderSnapshotRow[] {
77
+ const db = getDatabase();
78
+ const rows = db.prepare(QUERY_BY_PROVIDER_SQL).all(provider, startMs, endMs, limit) as DbRow[];
79
+ return rows.map(mapRow);
80
+ }
81
+
82
+ export function getLatestProviderSnapshot(provider: string): ProviderSnapshotRow | null {
83
+ const db = getDatabase();
84
+ const row = db.prepare(QUERY_LATEST_SQL).get(provider) as DbRow | null;
85
+ return row ? mapRow(row) : null;
86
+ }
87
+
88
+ interface DbRow {
89
+ id: number;
90
+ timestamp: number;
91
+ provider: string;
92
+ used_percent: number | null;
93
+ limit_reached: number;
94
+ resets_at: number | null;
95
+ tokens_input: number | null;
96
+ tokens_output: number | null;
97
+ cost_usd: number | null;
98
+ cost_source: string | null;
99
+ raw_json: string | null;
100
+ }
101
+
102
+ function mapRow(row: DbRow): ProviderSnapshotRow {
103
+ return {
104
+ id: row.id,
105
+ timestamp: row.timestamp,
106
+ provider: row.provider,
107
+ usedPercent: row.used_percent,
108
+ limitReached: row.limit_reached === 1,
109
+ resetsAt: row.resets_at,
110
+ tokensInput: row.tokens_input,
111
+ tokensOutput: row.tokens_output,
112
+ costUsd: row.cost_usd,
113
+ costSource: row.cost_source as CostSource | null,
114
+ rawJson: row.raw_json,
115
+ };
116
+ }