@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,745 @@
1
+ import type { AgentSessionAggregate, AgentSessionStream } from '@/agents/types.ts';
2
+ import type { ProviderUsageData } from '@/plugins/types/provider.ts';
3
+ import type { UsageEventInsert } from '@/storage/types.ts';
4
+
5
+ /**
6
+ * Demo presets control the intensity of simulated activity.
7
+ * - light: Low activity, fewer sessions, slower token accumulation
8
+ * - normal: Moderate activity, balanced simulation (default)
9
+ * - heavy: High activity, more sessions, faster token accumulation
10
+ */
11
+ export type DemoPreset = 'light' | 'normal' | 'heavy';
12
+
13
+ export interface DemoPresetConfig {
14
+ sessionCount: number;
15
+ activityMultiplier: number;
16
+ extraProviderCount: number;
17
+ usageRange: [number, number];
18
+ idleProbability: number;
19
+ burstProbability: number;
20
+ burstMultiplier: number;
21
+ }
22
+
23
+ export const DEMO_PRESETS: Record<DemoPreset, DemoPresetConfig> = {
24
+ light: {
25
+ sessionCount: 2,
26
+ activityMultiplier: 0.5,
27
+ extraProviderCount: 1,
28
+ usageRange: [5, 35],
29
+ idleProbability: 0.6,
30
+ burstProbability: 0.05,
31
+ burstMultiplier: 3,
32
+ },
33
+ normal: {
34
+ sessionCount: 4,
35
+ activityMultiplier: 1.0,
36
+ extraProviderCount: 3,
37
+ usageRange: [8, 72],
38
+ idleProbability: 0.35,
39
+ burstProbability: 0.1,
40
+ burstMultiplier: 4,
41
+ },
42
+ heavy: {
43
+ sessionCount: 6,
44
+ activityMultiplier: 2.0,
45
+ extraProviderCount: 10,
46
+ usageRange: [25, 95],
47
+ idleProbability: 0.15,
48
+ burstProbability: 0.2,
49
+ burstMultiplier: 5,
50
+ },
51
+ };
52
+
53
+ export interface DemoSessionSeed {
54
+ sessionId: string;
55
+ sessionName?: string;
56
+ agentId: 'opencode' | 'claude-code' | 'cursor';
57
+ agentName: 'OpenCode' | 'Claude Code' | 'Cursor';
58
+ projectPath: string;
59
+ modelId: string;
60
+ providerId: string;
61
+ baseTokens: number;
62
+ baseCost: number;
63
+ inactive?: boolean;
64
+ }
65
+
66
+ const DEFAULT_SESSIONS: DemoSessionSeed[] = [
67
+ {
68
+ sessionId: 'demo-opencode-1',
69
+ sessionName: 'Implement dashboard view with real-time updates',
70
+ agentId: 'opencode',
71
+ agentName: 'OpenCode',
72
+ projectPath: '/Users/demo/workspace/tokentop',
73
+ modelId: 'claude-3-5-sonnet',
74
+ providerId: 'anthropic',
75
+ baseTokens: 3200,
76
+ baseCost: 1.24,
77
+ },
78
+ {
79
+ sessionId: 'demo-opencode-2',
80
+ sessionName: 'Fix Kubernetes deployment configuration',
81
+ agentId: 'opencode',
82
+ agentName: 'OpenCode',
83
+ projectPath: '/Users/demo/workspace/infra',
84
+ modelId: 'gpt-4.1',
85
+ providerId: 'openai',
86
+ baseTokens: 2100,
87
+ baseCost: 0.92,
88
+ },
89
+ {
90
+ sessionId: 'demo-claude-1',
91
+ sessionName: 'Add authentication flow to mobile app',
92
+ agentId: 'claude-code',
93
+ agentName: 'Claude Code',
94
+ projectPath: '/Users/demo/workspace/mobile',
95
+ modelId: 'claude-3-opus',
96
+ providerId: 'anthropic',
97
+ baseTokens: 1800,
98
+ baseCost: 0.78,
99
+ },
100
+ {
101
+ sessionId: 'demo-cursor-1',
102
+ sessionName: 'Refactor API endpoints for better performance',
103
+ agentId: 'cursor',
104
+ agentName: 'Cursor',
105
+ projectPath: '/Users/demo/workspace/webapp',
106
+ modelId: 'gemini-2.0-pro',
107
+ providerId: 'google-gemini',
108
+ baseTokens: 2600,
109
+ baseCost: 0.64,
110
+ },
111
+ {
112
+ sessionId: 'demo-opencode-old-1',
113
+ sessionName: 'Migrate legacy API to REST v2',
114
+ agentId: 'opencode',
115
+ agentName: 'OpenCode',
116
+ projectPath: '/Users/demo/workspace/legacy-api',
117
+ modelId: 'claude-3-5-sonnet',
118
+ providerId: 'anthropic',
119
+ baseTokens: 5400,
120
+ baseCost: 2.18,
121
+ inactive: true,
122
+ },
123
+ {
124
+ sessionId: 'demo-claude-old-1',
125
+ sessionName: 'Update documentation with new API examples',
126
+ agentId: 'claude-code',
127
+ agentName: 'Claude Code',
128
+ projectPath: '/Users/demo/workspace/docs-site',
129
+ modelId: 'gpt-4.1',
130
+ providerId: 'openai',
131
+ baseTokens: 3800,
132
+ baseCost: 1.56,
133
+ inactive: true,
134
+ },
135
+ ];
136
+
137
+ const PROVIDER_LIMITS: Record<string, { label: string; windowMinutes: number }> = {
138
+ anthropic: { label: 'Daily Tokens', windowMinutes: 1440 },
139
+ openai: { label: 'Daily Tokens', windowMinutes: 1440 },
140
+ 'google-gemini': { label: 'Daily Tokens', windowMinutes: 1440 },
141
+ };
142
+
143
+ const EXTRA_PROVIDERS: Array<{ id: string; label: string; balance?: string }> = [
144
+ { id: 'codex', label: 'ChatGPT Plus', balance: '$18.00' },
145
+ { id: 'github-copilot', label: 'Copilot Pro', balance: '$26.00' },
146
+ { id: 'perplexity', label: 'Perplexity Pro', balance: '$12.50' },
147
+ { id: 'antigravity', label: 'Antigravity AI', balance: '$31.00' },
148
+ { id: 'minimax', label: 'MiniMax', balance: '$22.00' },
149
+ { id: 'cohere', label: 'Cohere Enterprise', balance: '$45.00' },
150
+ { id: 'mistral', label: 'Mistral API', balance: '$15.00' },
151
+ { id: 'groq', label: 'Groq Cloud', balance: '$8.50' },
152
+ { id: 'together', label: 'Together AI', balance: '$20.00' },
153
+ { id: 'fireworks', label: 'Fireworks AI', balance: '$12.00' },
154
+ ];
155
+
156
+ function clamp(value: number, min: number, max: number): number {
157
+ return Math.max(min, Math.min(max, value));
158
+ }
159
+
160
+ function hashCombine(a: number, b: number): number {
161
+ return ((a * 2654435761) ^ (b * 1597334677)) >>> 0;
162
+ }
163
+
164
+ class DemoRng {
165
+ private seed: number;
166
+ private initialSeed: number;
167
+
168
+ constructor(seed = 1337) {
169
+ this.initialSeed = seed;
170
+ this.seed = seed;
171
+ }
172
+
173
+ next(): number {
174
+ this.seed = (this.seed * 1664525 + 1013904223) % 4294967296;
175
+ return this.seed / 4294967296;
176
+ }
177
+
178
+ range(min: number, max: number): number {
179
+ return min + (max - min) * this.next();
180
+ }
181
+
182
+ getSeed(): number {
183
+ return this.initialSeed;
184
+ }
185
+
186
+ fork(offset: number): DemoRng {
187
+ return new DemoRng(hashCombine(this.initialSeed, offset));
188
+ }
189
+ }
190
+
191
+ export interface DemoSimulatorSnapshot {
192
+ sessions: AgentSessionAggregate[];
193
+ providerUsage: Map<string, ProviderUsageData>;
194
+ usageEvents: UsageEventInsert[];
195
+ }
196
+
197
+ export interface DemoSimulatorOptions {
198
+ seed?: number;
199
+ preset?: DemoPreset;
200
+ }
201
+
202
+ export class DemoSimulator {
203
+ private rng: DemoRng;
204
+ private sessions: AgentSessionAggregate[];
205
+ private providerUsage: Map<string, ProviderUsageData>;
206
+ private lastTick: number;
207
+ private readonly startTime: number;
208
+ private readonly preset: DemoPreset;
209
+ private readonly presetConfig: DemoPresetConfig;
210
+ private readonly fixedProviderIds: string[];
211
+ private readonly fixedExtraProviders: Array<{ id: string; label: string; balance?: string }>;
212
+
213
+ constructor(options: DemoSimulatorOptions = {}) {
214
+ const seed = options.seed ?? 1337;
215
+ this.preset = options.preset ?? 'normal';
216
+ this.presetConfig = DEMO_PRESETS[this.preset];
217
+ this.rng = new DemoRng(seed);
218
+ const now = Date.now();
219
+ this.startTime = now;
220
+
221
+ const sessionCount = Math.min(this.presetConfig.sessionCount, DEFAULT_SESSIONS.length);
222
+
223
+ // Select sessions: include a mix of inactive history and active sessions per preset size.
224
+ const inactiveSessions = DEFAULT_SESSIONS.filter(s => s.inactive ?? false);
225
+ const activeSessions = DEFAULT_SESSIONS.filter(s => !(s.inactive ?? false));
226
+ const desiredInactive = Math.max(1, Math.round(sessionCount * 0.3));
227
+ let inactiveCount = Math.min(inactiveSessions.length, desiredInactive);
228
+ if (activeSessions.length > 0) {
229
+ inactiveCount = Math.min(inactiveCount, sessionCount - 1);
230
+ }
231
+ let activeCount = Math.min(activeSessions.length, sessionCount - inactiveCount);
232
+ if (activeCount + inactiveCount < sessionCount) {
233
+ inactiveCount = Math.min(inactiveSessions.length, sessionCount - activeCount);
234
+ }
235
+
236
+ const selectedSessions = [
237
+ ...activeSessions.slice(0, activeCount),
238
+ ...inactiveSessions.slice(0, inactiveCount),
239
+ ];
240
+
241
+ this.sessions = selectedSessions.map((seedSession, index) => {
242
+ const tokens = seedSession.baseTokens;
243
+ const cost = seedSession.baseCost;
244
+ const inputTokens = Math.floor(tokens * 0.6);
245
+ const outputTokens = tokens - inputTokens;
246
+ const streams: AgentSessionStream[] = [
247
+ {
248
+ providerId: seedSession.providerId,
249
+ modelId: seedSession.modelId,
250
+ tokens: { input: inputTokens, output: outputTokens },
251
+ requestCount: Math.max(1, Math.floor(tokens / 800)),
252
+ costUsd: cost,
253
+ pricingSource: 'fallback',
254
+ },
255
+ ];
256
+
257
+ const isInactive = seedSession.inactive ?? false;
258
+ const startedAt = isInactive
259
+ ? now - this.rng.range(3, 7) * 24 * 60 * 60 * 1000
260
+ : now - (index + 1) * 45 * 60 * 1000;
261
+ const lastActivityAt = isInactive
262
+ ? startedAt + this.rng.range(30, 120) * 60 * 1000
263
+ : now - this.rng.range(10_000, 50_000);
264
+
265
+ const nowDate = new Date(now);
266
+ const startOfDay = new Date(nowDate.getFullYear(), nowDate.getMonth(), nowDate.getDate()).getTime();
267
+ const dayOfWeek = nowDate.getDay();
268
+ const startOfWeek = new Date(nowDate.getFullYear(), nowDate.getMonth(), nowDate.getDate() - dayOfWeek).getTime();
269
+ const startOfMonth = new Date(nowDate.getFullYear(), nowDate.getMonth(), 1).getTime();
270
+
271
+ const costInDay = lastActivityAt >= startOfDay ? cost : 0;
272
+ const costInWeek = lastActivityAt >= startOfWeek ? cost : 0;
273
+ const costInMonth = lastActivityAt >= startOfMonth ? cost : 0;
274
+
275
+ const baseSession: Omit<AgentSessionAggregate, 'endedAt'> = {
276
+ sessionId: seedSession.sessionId,
277
+ agentId: seedSession.agentId,
278
+ agentName: seedSession.agentName,
279
+ projectPath: seedSession.projectPath,
280
+ startedAt,
281
+ lastActivityAt,
282
+ status: isInactive ? 'idle' : (index % 3 === 0 ? 'idle' : 'active'),
283
+ totals: {
284
+ input: inputTokens,
285
+ output: outputTokens,
286
+ cacheRead: Math.floor(tokens * 0.05),
287
+ cacheWrite: Math.floor(tokens * 0.02),
288
+ },
289
+ totalCostUsd: cost,
290
+ requestCount: Math.max(1, Math.floor(tokens / 700)),
291
+ streams,
292
+ costInDay,
293
+ costInWeek,
294
+ costInMonth,
295
+ };
296
+ if (seedSession.sessionName) {
297
+ baseSession.sessionName = seedSession.sessionName;
298
+ }
299
+
300
+ return isInactive
301
+ ? { ...baseSession, endedAt: lastActivityAt }
302
+ : baseSession;
303
+ });
304
+
305
+ const sessionProviderIds = new Set(selectedSessions.map(s => s.providerId));
306
+ this.fixedProviderIds = Array.from(sessionProviderIds);
307
+
308
+ const extraCount = Math.min(this.presetConfig.extraProviderCount, EXTRA_PROVIDERS.length);
309
+ this.fixedExtraProviders = EXTRA_PROVIDERS.slice(0, extraCount);
310
+
311
+ this.providerUsage = new Map();
312
+ this.lastTick = now;
313
+ this.initializeProviderUsage(now);
314
+ }
315
+
316
+ private initializeProviderUsage(now: number) {
317
+ const providerTotals = new Map<string, { tokens: number; cost: number; requests: number }>();
318
+ for (const session of this.sessions) {
319
+ for (const stream of session.streams) {
320
+ const current = providerTotals.get(stream.providerId) ?? { tokens: 0, cost: 0, requests: 0 };
321
+ const streamTokens = stream.tokens.input + stream.tokens.output;
322
+ providerTotals.set(stream.providerId, {
323
+ tokens: current.tokens + streamTokens,
324
+ cost: current.cost + (stream.costUsd ?? 0),
325
+ requests: current.requests + stream.requestCount,
326
+ });
327
+ }
328
+ }
329
+
330
+ for (const providerId of this.fixedProviderIds) {
331
+ const totals = providerTotals.get(providerId) ?? { tokens: 0, cost: 0, requests: 0 };
332
+ const limit = PROVIDER_LIMITS[providerId] ?? { label: 'Daily Tokens', windowMinutes: 1440 };
333
+ const usedPercent = clamp((totals.tokens / 50_000) * 100, 5, 98);
334
+ const limitReached = usedPercent > 95;
335
+
336
+ this.providerUsage.set(providerId, {
337
+ planType: 'Pro',
338
+ limitReached,
339
+ limits: {
340
+ primary: {
341
+ usedPercent,
342
+ label: limit.label,
343
+ windowMinutes: limit.windowMinutes,
344
+ resetsAt: now + 6 * 60 * 60 * 1000,
345
+ },
346
+ },
347
+ tokens: {
348
+ input: Math.floor(totals.tokens * 0.6),
349
+ output: Math.floor(totals.tokens * 0.4),
350
+ cacheRead: Math.floor(totals.tokens * 0.05),
351
+ cacheWrite: Math.floor(totals.tokens * 0.02),
352
+ },
353
+ cost: {
354
+ actual: {
355
+ total: totals.cost,
356
+ input: totals.cost * 0.55,
357
+ output: totals.cost * 0.45,
358
+ currency: 'USD',
359
+ },
360
+ source: 'estimated',
361
+ },
362
+ credits: {
363
+ hasCredits: true,
364
+ unlimited: false,
365
+ balance: '$42.50',
366
+ },
367
+ fetchedAt: now,
368
+ });
369
+ }
370
+
371
+ const [minUsage, maxUsage] = this.presetConfig.usageRange;
372
+ for (const provider of this.fixedExtraProviders) {
373
+ const usedPercent = clamp(this.rng.range(minUsage, maxUsage), 5, 98);
374
+ this.providerUsage.set(provider.id, {
375
+ planType: provider.label,
376
+ limitReached: usedPercent > 95,
377
+ limits: {
378
+ primary: {
379
+ usedPercent,
380
+ label: 'Monthly Tokens',
381
+ windowMinutes: 43200,
382
+ resetsAt: now + 10 * 24 * 60 * 60 * 1000,
383
+ },
384
+ },
385
+ tokens: {
386
+ input: Math.floor(usedPercent * 800),
387
+ output: Math.floor(usedPercent * 600),
388
+ },
389
+ cost: {
390
+ estimated: {
391
+ total: usedPercent * 0.08,
392
+ input: usedPercent * 0.04,
393
+ output: usedPercent * 0.04,
394
+ currency: 'USD',
395
+ },
396
+ source: 'estimated',
397
+ },
398
+ credits: {
399
+ hasCredits: true,
400
+ unlimited: false,
401
+ balance: provider.balance ?? '$25.00',
402
+ },
403
+ fetchedAt: now,
404
+ });
405
+ }
406
+ }
407
+
408
+ private updateProviderUsage(now: number, tickRng: DemoRng) {
409
+ const providerTotals = new Map<string, { tokens: number; cost: number; requests: number }>();
410
+ for (const session of this.sessions) {
411
+ for (const stream of session.streams) {
412
+ const current = providerTotals.get(stream.providerId) ?? { tokens: 0, cost: 0, requests: 0 };
413
+ const streamTokens = stream.tokens.input + stream.tokens.output;
414
+ providerTotals.set(stream.providerId, {
415
+ tokens: current.tokens + streamTokens,
416
+ cost: current.cost + (stream.costUsd ?? 0),
417
+ requests: current.requests + stream.requestCount,
418
+ });
419
+ }
420
+ }
421
+
422
+ for (const providerId of this.fixedProviderIds) {
423
+ const totals = providerTotals.get(providerId) ?? { tokens: 0, cost: 0, requests: 0 };
424
+ const limit = PROVIDER_LIMITS[providerId] ?? { label: 'Daily Tokens', windowMinutes: 1440 };
425
+ const usedPercent = clamp((totals.tokens / 50_000) * 100, 5, 98);
426
+ const limitReached = usedPercent > 95;
427
+
428
+ const existing = this.providerUsage.get(providerId);
429
+ this.providerUsage.set(providerId, {
430
+ planType: existing?.planType ?? 'Pro',
431
+ limitReached,
432
+ limits: {
433
+ primary: {
434
+ usedPercent,
435
+ label: limit.label,
436
+ windowMinutes: limit.windowMinutes,
437
+ resetsAt: existing?.limits?.primary?.resetsAt ?? now + 6 * 60 * 60 * 1000,
438
+ },
439
+ },
440
+ tokens: {
441
+ input: Math.floor(totals.tokens * 0.6),
442
+ output: Math.floor(totals.tokens * 0.4),
443
+ cacheRead: Math.floor(totals.tokens * 0.05),
444
+ cacheWrite: Math.floor(totals.tokens * 0.02),
445
+ },
446
+ cost: {
447
+ actual: {
448
+ total: totals.cost,
449
+ input: totals.cost * 0.55,
450
+ output: totals.cost * 0.45,
451
+ currency: 'USD',
452
+ },
453
+ source: 'estimated',
454
+ },
455
+ credits: existing?.credits ?? {
456
+ hasCredits: true,
457
+ unlimited: false,
458
+ balance: '$42.50',
459
+ },
460
+ fetchedAt: now,
461
+ });
462
+ }
463
+
464
+ for (const provider of this.fixedExtraProviders) {
465
+ const existing = this.providerUsage.get(provider.id);
466
+ if (!existing) continue;
467
+
468
+ const currentPercent = existing.limits?.primary?.usedPercent ?? 50;
469
+ const delta = tickRng.range(0.1, 0.5) * this.presetConfig.activityMultiplier;
470
+ const newPercent = clamp(currentPercent + delta, 5, 98);
471
+
472
+ this.providerUsage.set(provider.id, {
473
+ ...existing,
474
+ limitReached: newPercent > 95,
475
+ limits: {
476
+ primary: {
477
+ ...existing.limits!.primary!,
478
+ usedPercent: newPercent,
479
+ },
480
+ },
481
+ tokens: {
482
+ input: Math.floor(newPercent * 800),
483
+ output: Math.floor(newPercent * 600),
484
+ },
485
+ cost: {
486
+ estimated: {
487
+ total: newPercent * 0.08,
488
+ input: newPercent * 0.04,
489
+ output: newPercent * 0.04,
490
+ currency: 'USD',
491
+ },
492
+ source: 'estimated',
493
+ },
494
+ fetchedAt: now,
495
+ });
496
+ }
497
+ }
498
+
499
+ tick(now = Date.now()): DemoSimulatorSnapshot {
500
+ const dtSec = Math.max((now - this.lastTick) / 1000, 1);
501
+ this.lastTick = now;
502
+
503
+ const elapsedSec = Math.floor((now - this.startTime) / 1000);
504
+ const tickRng = this.rng.fork(elapsedSec);
505
+
506
+ const usageEvents: UsageEventInsert[] = [];
507
+ const activityMultiplier = this.presetConfig.activityMultiplier;
508
+
509
+ this.sessions = this.sessions.map((session, sessionIndex) => {
510
+ if (session.endedAt !== undefined) {
511
+ return session;
512
+ }
513
+
514
+ const sessionRng = tickRng.fork(sessionIndex);
515
+ const isIdle = sessionRng.next() < this.presetConfig.idleProbability;
516
+ if (isIdle) {
517
+ return { ...session, status: 'idle' as const };
518
+ }
519
+
520
+ const isBurst = sessionRng.next() < this.presetConfig.burstProbability;
521
+ const burstFactor = isBurst ? this.presetConfig.burstMultiplier : 1;
522
+ const activityFactor = sessionRng.range(0.4, 1.6) * burstFactor;
523
+ const tokensPerSec = session.agentId === 'opencode' ? 35 : session.agentId === 'claude-code' ? 28 : 22;
524
+ const deltaTokens = Math.floor(tokensPerSec * activityFactor * activityMultiplier * dtSec);
525
+ if (deltaTokens === 0) {
526
+ return { ...session, status: 'idle' as const };
527
+ }
528
+ const inputDelta = Math.floor(deltaTokens * 0.6);
529
+ const outputDelta = deltaTokens - inputDelta;
530
+ const costDelta = deltaTokens * 0.000015;
531
+
532
+ const updatedStreams = session.streams.map((stream) => ({
533
+ ...stream,
534
+ tokens: {
535
+ ...stream.tokens,
536
+ input: stream.tokens.input + inputDelta,
537
+ output: stream.tokens.output + outputDelta,
538
+ },
539
+ requestCount: stream.requestCount + Math.max(1, Math.floor(deltaTokens / 750)),
540
+ costUsd: (stream.costUsd ?? 0) + costDelta,
541
+ }));
542
+
543
+ usageEvents.push({
544
+ timestamp: now,
545
+ source: 'agent',
546
+ provider: updatedStreams[0]?.providerId ?? 'anthropic',
547
+ model: updatedStreams[0]?.modelId ?? 'claude-3-5-sonnet',
548
+ agentId: session.agentId,
549
+ sessionId: session.sessionId,
550
+ projectPath: session.projectPath ?? null,
551
+ inputTokens: inputDelta,
552
+ outputTokens: outputDelta,
553
+ cacheReadTokens: Math.floor(deltaTokens * 0.05),
554
+ cacheWriteTokens: Math.floor(deltaTokens * 0.02),
555
+ costUsd: costDelta,
556
+ requestCount: Math.max(1, Math.floor(deltaTokens / 700)),
557
+ pricingSource: 'fallback',
558
+ });
559
+
560
+ return {
561
+ ...session,
562
+ status: 'active' as const,
563
+ lastActivityAt: now,
564
+ totals: {
565
+ ...session.totals,
566
+ input: session.totals.input + inputDelta,
567
+ output: session.totals.output + outputDelta,
568
+ },
569
+ totalCostUsd: (session.totalCostUsd ?? 0) + costDelta,
570
+ requestCount: session.requestCount + Math.max(1, Math.floor(deltaTokens / 700)),
571
+ streams: updatedStreams,
572
+ costInDay: session.costInDay + costDelta,
573
+ costInWeek: session.costInWeek + costDelta,
574
+ costInMonth: session.costInMonth + costDelta,
575
+ } satisfies AgentSessionAggregate;
576
+ });
577
+
578
+ this.updateProviderUsage(now, tickRng);
579
+
580
+ return {
581
+ sessions: this.sessions,
582
+ providerUsage: new Map(this.providerUsage),
583
+ usageEvents,
584
+ };
585
+ }
586
+
587
+ getSeed(): number {
588
+ return this.rng.getSeed();
589
+ }
590
+
591
+ getPreset(): DemoPreset {
592
+ return this.preset;
593
+ }
594
+
595
+ getProviderIds(): string[] {
596
+ return [...this.fixedProviderIds, ...this.fixedExtraProviders.map(p => p.id)];
597
+ }
598
+
599
+ generateHistoricalCostDataByProvider(daysBack: number): Array<{ date: number; provider: string; cost: number; tokens: number; requests: number }> {
600
+ const totalDaily = this.generateHistoricalCostData(daysBack);
601
+ const result: Array<{ date: number; provider: string; cost: number; tokens: number; requests: number }> = [];
602
+
603
+ const providerShares = [
604
+ { id: 'anthropic', share: 0.50 },
605
+ { id: 'openai', share: 0.25 },
606
+ { id: 'google-gemini', share: 0.15 },
607
+ ];
608
+
609
+ const providerRng = this.rng.fork(88888);
610
+
611
+ for (let i = 0; i < totalDaily.length; i++) {
612
+ const day = totalDaily[i]!;
613
+ const dayRng = providerRng.fork(i);
614
+
615
+ let remainingCost = day.cost;
616
+
617
+ for (const provider of providerShares) {
618
+ const variance = dayRng.range(0.8, 1.2);
619
+ const cost = Math.round(day.cost * provider.share * variance * 100) / 100;
620
+ const actualCost = Math.min(Math.max(cost, 0), remainingCost);
621
+ remainingCost -= actualCost;
622
+
623
+ const tokensPerDollar = dayRng.range(15000, 25000);
624
+ const requestsPerDollar = dayRng.range(8, 15);
625
+
626
+ result.push({
627
+ date: day.date,
628
+ provider: provider.id,
629
+ cost: actualCost,
630
+ tokens: Math.floor(actualCost * tokensPerDollar),
631
+ requests: Math.floor(actualCost * requestsPerDollar),
632
+ });
633
+ }
634
+ }
635
+
636
+ return result;
637
+ }
638
+
639
+ generateHistoricalCostDataByModel(daysBack: number): Array<{ date: number; model: string; cost: number; tokens: number; requests: number }> {
640
+ const totalDaily = this.generateHistoricalCostData(daysBack);
641
+ const result: Array<{ date: number; model: string; cost: number; tokens: number; requests: number }> = [];
642
+
643
+ const modelShares = [
644
+ { id: 'claude-3-5-sonnet', share: 0.40 },
645
+ { id: 'claude-3-opus', share: 0.20 },
646
+ { id: 'gpt-4.1', share: 0.25 },
647
+ { id: 'gemini-2.0-pro', share: 0.15 },
648
+ ];
649
+
650
+ const modelRng = this.rng.fork(77777);
651
+
652
+ for (let i = 0; i < totalDaily.length; i++) {
653
+ const day = totalDaily[i]!;
654
+ const dayRng = modelRng.fork(i);
655
+
656
+ let remainingCost = day.cost;
657
+
658
+ for (const model of modelShares) {
659
+ const variance = dayRng.range(0.8, 1.2);
660
+ const cost = Math.round(day.cost * model.share * variance * 100) / 100;
661
+ const actualCost = Math.min(Math.max(cost, 0), remainingCost);
662
+ remainingCost -= actualCost;
663
+
664
+ const tokensPerDollar = dayRng.range(15000, 25000);
665
+ const requestsPerDollar = dayRng.range(8, 15);
666
+
667
+ result.push({
668
+ date: day.date,
669
+ model: model.id,
670
+ cost: actualCost,
671
+ tokens: Math.floor(actualCost * tokensPerDollar),
672
+ requests: Math.floor(actualCost * requestsPerDollar),
673
+ });
674
+ }
675
+ }
676
+
677
+ return result;
678
+ }
679
+
680
+ generateHistoricalCostDataByProject(daysBack: number): Array<{ date: number; projectPath: string; cost: number; tokens: number; requests: number }> {
681
+ const totalDaily = this.generateHistoricalCostData(daysBack);
682
+ const result: Array<{ date: number; projectPath: string; cost: number; tokens: number; requests: number }> = [];
683
+
684
+ const projectShares = [
685
+ { path: '/Users/demo/workspace/tokentop', share: 0.45 },
686
+ { path: '/Users/demo/workspace/webapp', share: 0.30 },
687
+ { path: '/Users/demo/workspace/infra', share: 0.25 },
688
+ ];
689
+
690
+ const projectRng = this.rng.fork(66666);
691
+
692
+ for (let i = 0; i < totalDaily.length; i++) {
693
+ const day = totalDaily[i]!;
694
+ const dayRng = projectRng.fork(i);
695
+
696
+ let remainingCost = day.cost;
697
+
698
+ for (const project of projectShares) {
699
+ const variance = dayRng.range(0.8, 1.2);
700
+ const cost = Math.round(day.cost * project.share * variance * 100) / 100;
701
+ const actualCost = Math.min(Math.max(cost, 0), remainingCost);
702
+ remainingCost -= actualCost;
703
+
704
+ const tokensPerDollar = dayRng.range(15000, 25000);
705
+ const requestsPerDollar = dayRng.range(8, 15);
706
+
707
+ result.push({
708
+ date: day.date,
709
+ projectPath: project.path,
710
+ cost: actualCost,
711
+ tokens: Math.floor(actualCost * tokensPerDollar),
712
+ requests: Math.floor(actualCost * requestsPerDollar),
713
+ });
714
+ }
715
+ }
716
+
717
+ return result;
718
+ }
719
+
720
+ generateHistoricalCostData(daysBack: number): Array<{ date: number; cost: number }> {
721
+ const historyRng = this.rng.fork(99999);
722
+ const now = Date.now();
723
+ const msPerDay = 24 * 60 * 60 * 1000;
724
+ const result: Array<{ date: number; cost: number }> = [];
725
+
726
+ const baseCost = this.presetConfig.activityMultiplier * 2.5;
727
+
728
+ for (let i = daysBack - 1; i >= 0; i--) {
729
+ const dayTimestamp = now - i * msPerDay;
730
+ const dayRng = historyRng.fork(i);
731
+
732
+ const isWeekend = new Date(dayTimestamp).getDay() % 6 === 0;
733
+ const weekendFactor = isWeekend ? 0.4 : 1.0;
734
+
735
+ const variance = dayRng.range(0.5, 1.5);
736
+ const spikeFactor = dayRng.next() < 0.1 ? dayRng.range(1.5, 2.5) : 1.0;
737
+
738
+ const cost = baseCost * variance * weekendFactor * spikeFactor;
739
+
740
+ result.push({ date: dayTimestamp, cost: Math.round(cost * 100) / 100 });
741
+ }
742
+
743
+ return result;
744
+ }
745
+ }