@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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-present Nigel Bazzeghin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,273 @@
1
+ > [!CAUTION]
2
+ > **tokentop is under active development and not yet ready for general use.** APIs and configuration may change without notice. If you're interested, star the repo and check back soon.
3
+
4
+ <div align="center">
5
+
6
+ # tokentop
7
+
8
+ **htop for your AI costs**
9
+
10
+ Real-time terminal monitoring of LLM token usage and spending across providers and coding agents.
11
+
12
+ ![Tokentop Dashboard](assets/Screenshot.png)
13
+
14
+ <!-- TODO: uncomment CI badge when GitHub Actions workflow is set up -->
15
+ <!-- [![CI](https://img.shields.io/github/actions/workflow/status/tokentopapp/tokentop/ci.yml?style=flat-square&label=CI)](https://github.com/tokentopapp/tokentop/actions) -->
16
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue?style=flat-square)](LICENSE)
17
+ [![GitHub release](https://img.shields.io/github/v/release/tokentopapp/tokentop?style=flat-square)](https://github.com/tokentopapp/tokentop/releases)
18
+ [![TypeScript](https://img.shields.io/badge/TypeScript-strict-3178C6?style=flat-square&logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
19
+ [![Bun](https://img.shields.io/badge/Bun-runtime-f9f1e1?style=flat-square&logo=bun&logoColor=black)](https://bun.sh)
20
+
21
+ [Features](#features) · [Install](#installation) · [Quick Start](#quick-start) · [Keyboard Shortcuts](#keyboard-shortcuts) · [Configuration](#configuration) · [Plugins](#plugin-system)
22
+
23
+ </div>
24
+
25
+ ---
26
+
27
+ ## Why tokentop?
28
+
29
+ You're coding with AI agents all day. Claude Code, OpenCode, Cursor — they're burning through tokens and you have no idea how fast.
30
+
31
+ You check the Anthropic dashboard. It's hours behind. You check OpenAI. Different dashboard, different login. Want to see it all in one place? In real time? In your terminal, where you already live?
32
+
33
+ That's tokentop.
34
+
35
+ - See every session, every model, every dollar — **live**
36
+ - Track costs across **10 providers** from a single dashboard
37
+ - Set **budget alerts** so you don't wake up to a surprise bill
38
+ - Runs **100% locally** — your usage data never leaves your machine
39
+
40
+ <!-- TODO: Replace with VHS recording
41
+ ## Demo
42
+
43
+ > _Coming soon: VHS terminal recording_
44
+ -->
45
+
46
+ ## Features
47
+
48
+ - **Real-time dashboard** — Live token counts, costs, burn rate, and activity sparklines
49
+ - **10 providers** — Anthropic, OpenAI, Google Gemini, GitHub Copilot, Codex, Perplexity, Antigravity, MiniMax, Zai, OpenCode Zen
50
+ - **Session tracking** — See every coding agent session with model, tokens, cost, and duration
51
+ - **Budget guardrails** — Daily, weekly, and monthly limits with visual warnings at limit percentages you set
52
+ - **Smart sidebar** — Adaptive panel that breaks down spending by model, project, or agent
53
+ - **Efficiency insights** — Cache leverage, output verbosity, and cost-per-request analysis to help you spend less
54
+ - **Historical trends** — ASCII step charts showing cost patterns over 7, 30, or 90 days
55
+ - **Projects view** — See which codebase is costing you the most
56
+ - **Provider limits** — Visual gauges showing how close you are to rate limits
57
+ - **Live pricing** — Fetches current model pricing from [models.dev](https://models.dev) with local caching
58
+ - **15 built-in themes** — Dark: Tokyo Night (default), Dracula, Nord, Catppuccin Mocha, Gruvbox Dark, One Dark, Rosé Pine, Kanagawa, OpenCode, Claude Code · Light: Catppuccin Latte, Gruvbox Light, GitHub Light, Solarized Light, Rosé Pine Dawn
59
+ - **Plugin system** — Extend with custom providers, agents, themes, and notifications
60
+ - **Responsive layout** — Adapts to any terminal size; sidebar, KPI strip, header, and tables all reflow automatically from ultrawide to laptop-width
61
+ - **Demo mode** — Explore the UI with synthetic data, no API keys needed
62
+ - **Zero config** — Auto-discovers credentials from Claude Code, environment variables, and CLI auth files
63
+
64
+ ## Installation
65
+
66
+ ### Homebrew (macOS/Linux)
67
+
68
+ ```bash
69
+ brew install tokentopapp/tap/tokentop
70
+ ```
71
+
72
+ ### Install script (macOS/Linux)
73
+
74
+ ```bash
75
+ curl -fsSL https://raw.githubusercontent.com/tokentopapp/tokentop/main/scripts/install.sh | sh
76
+ ```
77
+
78
+ ### Scoop (Windows)
79
+
80
+ ```powershell
81
+ scoop bucket add tokentop https://github.com/tokentopapp/scoop-tokentop
82
+ scoop install tokentop
83
+ ```
84
+
85
+ ### Standalone binaries
86
+
87
+ Download the latest binary for your platform from [GitHub Releases](https://github.com/tokentopapp/tokentop/releases). No dependencies required.
88
+
89
+ | Platform | Binary |
90
+ |----------|--------|
91
+ | macOS (Apple Silicon) | `ttop-darwin-arm64` |
92
+ | Linux (x64) | `ttop-linux-x64` |
93
+ | Linux (ARM64) | `ttop-linux-arm64` |
94
+ | Windows (x64) | `ttop-windows-x64.exe` |
95
+
96
+ ### npm
97
+
98
+ Requires [Bun](https://bun.sh) runtime. If Bun is installed, this works out of the box. If not, it will tell you how to get it.
99
+
100
+ ```bash
101
+ bunx @tokentop/ttop
102
+ # or install globally
103
+ bun install -g @tokentop/ttop
104
+ ```
105
+
106
+ ### From source
107
+
108
+ ```bash
109
+ git clone https://github.com/tokentopapp/tokentop.git
110
+ cd tokentop
111
+ bun install
112
+ bun start
113
+ ```
114
+
115
+ ## Quick Start
116
+
117
+ ```bash
118
+ # Launch tokentop
119
+ bun start
120
+ # or, if installed globally:
121
+ ttop
122
+
123
+ # Try it without any API keys
124
+ ttop demo
125
+
126
+ # Pick a theme
127
+ ttop -t dracula
128
+
129
+ # Deterministic demo for screenshots/testing
130
+ ttop demo --seed 42 --preset heavy
131
+ ```
132
+
133
+ tokentop will automatically discover your credentials. If you use Claude Code, your OAuth tokens are picked up immediately — no configuration needed.
134
+
135
+ ## Views
136
+
137
+ tokentop has 4 main views, switchable with `1`–`4`:
138
+
139
+ | Key | View | What it shows |
140
+ |-----|------|---------------|
141
+ | `1` | **Dashboard** | KPI strip, activity sparkline, provider limits, sessions table, smart sidebar |
142
+ | `2` | **Providers** | All configured providers with connection status and usage levels |
143
+ | `3` | **Trends** | ASCII step charts of cost over 7/30/90 days |
144
+ | `4` | **Projects** | Cost and token breakdown by local project/repo |
145
+
146
+ <!-- TODO: Add VHS recordings of each view
147
+ > _Screenshots coming soon_
148
+ -->
149
+
150
+ ## Keyboard Shortcuts
151
+
152
+ ### Global
153
+
154
+ | Key | Action |
155
+ |-----|--------|
156
+ | `1`–`4` | Switch views |
157
+ | `,` | Settings |
158
+ | `:` | Command palette |
159
+ | `r` | Refresh all providers |
160
+ | `?` | Help overlay |
161
+ | `q` | Quit |
162
+
163
+ ### Dashboard
164
+
165
+ | Key | Action |
166
+ |-----|--------|
167
+ | `t` | Cycle time window (5m → 15m → 1h → 24h → 7d → 30d → all) |
168
+ | `/` | Filter sessions (by agent, model, project) |
169
+ | `s` | Cycle sort (cost, tokens) |
170
+ | `j`/`k` or `↑`/`↓` | Navigate sessions |
171
+ | `Enter` | Open session details |
172
+ | `Tab`/`Shift+Tab` | Cycle focus (sessions → limits → sidebar) |
173
+ | `i` | Toggle sidebar |
174
+ | `m`/`p`/`a` | Sidebar dimension (model / project / agent) |
175
+ | `b` | Cycle budget lock (sync with window, daily, weekly, monthly) |
176
+ | `l` | Jump to provider limits |
177
+ | `gg`/`G` | Jump to top/bottom |
178
+
179
+ ## Configuration
180
+
181
+ Config lives at `~/.config/tokentop/config.json`:
182
+
183
+ ```jsonc
184
+ {
185
+ "theme": "tokyo-night", // 15 themes — see ttop --help for full list
186
+ "colorScheme": "auto", // auto, light, dark
187
+ "refreshInterval": 60000, // polling interval (ms)
188
+ "budgets": {
189
+ "daily": 50.00,
190
+ "weekly": 200.00,
191
+ "monthly": 500.00
192
+ }
193
+ }
194
+ ```
195
+
196
+ Settings are also editable in-app — press `,` to open the settings panel.
197
+
198
+ ### Credential Discovery
199
+
200
+ tokentop finds your API credentials automatically — no manual key entry needed.
201
+
202
+ **Discovery order:**
203
+
204
+ 1. **OpenCode / Claude Code auth** — Reads OAuth tokens from `~/.local/share/opencode/auth.json` (supports Anthropic OAuth, OpenAI/Codex OAuth, GitHub tokens, and well-known API keys)
205
+ 2. **Antigravity accounts** — Picks up Google/Gemini OAuth from `~/.config/opencode/antigravity-accounts.json`
206
+ 3. **Environment variables** — `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`, etc.
207
+ 4. **External CLI auth** — Claude Code, Gemini CLI, and other tool auth files
208
+
209
+ OAuth tokens are preferred over API keys because provider usage-tracking APIs (like Anthropic's `/api/oauth/usage`) require OAuth, not API keys. If you use Claude Code or OpenCode, your tokens are picked up immediately.
210
+
211
+ ## Plugin System
212
+
213
+ tokentop is built on a plugin architecture with four extension points:
214
+
215
+ | Type | Purpose | Official | Community |
216
+ |------|---------|----------|-----------|
217
+ | **Provider** | Add a model provider | `@tokentop/provider-*` | `tokentop-provider-*` |
218
+ | **Agent** | Support a coding agent | `@tokentop/agent-*` | `tokentop-agent-*` |
219
+ | **Theme** | Custom color schemes | `@tokentop/theme-*` | `tokentop-theme-*` |
220
+ | **Notification** | Alert delivery (Slack, Discord, etc.) | `@tokentop/notification-*` | `tokentop-notification-*` |
221
+
222
+ All plugins run in a **permission sandbox** — they must declare network, filesystem, and environment access upfront. Anyone can publish community plugins to npm — no org membership needed.
223
+
224
+ See the [Plugin Guide](docs/plugins.md) for installation, configuration, and development details.
225
+
226
+ ## How It Works
227
+
228
+ tokentop combines two data sources for real-time visibility:
229
+
230
+ 1. **Provider API polling** — Fetches usage data directly from provider APIs (Anthropic, OpenAI, etc.) on a configurable interval
231
+ 2. **Local session parsing** — Watches your coding agent's session files on disk via `fs.watch`, computing token deltas in real time as you code
232
+
233
+ Costs are calculated from token counts and live pricing data from [models.dev](https://models.dev) (cached 1 hour), with built-in fallback pricing for offline use.
234
+
235
+ All data is stored in a **local SQLite database**. Nothing is sent anywhere. No telemetry, no analytics, no network calls except to the provider APIs you've already authenticated with.
236
+
237
+ ## Development
238
+
239
+ ```bash
240
+ bun install # Install dependencies
241
+ bun run dev # Dev mode with hot reload
242
+ bun test # Run tests
243
+ bun run typecheck # TypeScript check
244
+ bun run lint # ESLint
245
+ ```
246
+
247
+ ### Demo mode for development
248
+
249
+ ```bash
250
+ ttop demo # Random synthetic data
251
+ ttop demo --seed 42 # Deterministic (reproducible)
252
+ ttop demo --preset heavy # High activity simulation
253
+ ttop demo --seed 42 --preset light # Combine both
254
+ ```
255
+
256
+ ### Built with
257
+
258
+ - [Bun](https://bun.sh) — Runtime
259
+ - [OpenTUI](https://github.com/anthropics/opentui) — Terminal UI framework (React reconciler)
260
+ - [TypeScript](https://www.typescriptlang.org/) — Strict mode
261
+ - [SQLite](https://www.sqlite.org/) — Local storage via `bun:sqlite`
262
+
263
+ ## Contributing
264
+
265
+ Contributions welcome! Please open an issue first to discuss what you'd like to change.
266
+
267
+ - **Conventional commits**: `feat:`, `fix:`, `docs:`, `refactor:`
268
+ - **TypeScript strict mode** — no `any`, no `@ts-ignore`
269
+ - **Tests required** for new features
270
+
271
+ ## License
272
+
273
+ [MIT](LICENSE)
package/bin/ttop.js ADDED
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env node
2
+
3
+ // tokentop npm entry point
4
+ // Detects runtime and either loads directly (Bun) or re-launches with Bun (Node).
5
+ // Standalone binaries at: https://github.com/tokentopapp/tokentop/releases
6
+
7
+ if (typeof globalThis.Bun !== 'undefined') {
8
+ await import('../src/cli.ts');
9
+ } else {
10
+ const { execFileSync } = await import('node:child_process');
11
+ const { fileURLToPath } = await import('node:url');
12
+ const { dirname, join } = await import('node:path');
13
+
14
+ const __filename = fileURLToPath(import.meta.url);
15
+ const __dirname = dirname(__filename);
16
+ const cli = join(__dirname, '..', 'src', 'cli.ts');
17
+
18
+ try {
19
+ execFileSync('bun', ['run', cli, ...process.argv.slice(2)], {
20
+ stdio: 'inherit',
21
+ });
22
+ } catch (err) {
23
+ // If bun ran but the command itself failed, propagate the exit code
24
+ if (err.status) process.exit(err.status);
25
+
26
+ // Bun not found — show install instructions
27
+ console.error(
28
+ [
29
+ '',
30
+ 'tokentop requires the Bun runtime (https://bun.sh)',
31
+ '',
32
+ 'Install Bun:',
33
+ ' curl -fsSL https://bun.sh/install | bash',
34
+ '',
35
+ 'Or download a standalone binary (no Bun needed):',
36
+ ' https://github.com/tokentopapp/tokentop/releases',
37
+ '',
38
+ 'Homebrew (macOS/Linux):',
39
+ ' brew install tokentopapp/tap/tokentop',
40
+ '',
41
+ 'Scoop (Windows):',
42
+ ' scoop bucket add tokentop https://github.com/tokentopapp/scoop-tokentop',
43
+ ' scoop install tokentop',
44
+ '',
45
+ ].join('\n'),
46
+ );
47
+ process.exit(1);
48
+ }
49
+ }
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "@tokentop/ttop",
3
+ "version": "0.2.2",
4
+ "description": "Real-time AI token usage monitor - htop for your API costs",
5
+ "author": "Nigel Bazzeghin <nbazzeghin@gmail.com>",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "bin": {
9
+ "ttop": "./bin/ttop.js"
10
+ },
11
+ "files": [
12
+ "bin/",
13
+ "src/",
14
+ "package.json",
15
+ "README.md",
16
+ "LICENSE"
17
+ ],
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "scripts": {
22
+ "dev": "bun --hot src/cli.ts",
23
+ "start": "bun src/cli.ts",
24
+ "build": "bun build ./src/cli.ts --outdir=dist --target=bun --minify",
25
+ "typecheck": "tsc --noEmit",
26
+ "lint": "eslint src/",
27
+ "test": "bun test",
28
+ "test:watch": "bun test --watch",
29
+ "driver": "bun src/tui/driver/cli.ts",
30
+ "driver:help": "bun src/tui/driver/cli.ts --help",
31
+ "snapshot": "bun src/tui/debug/snapshot.tsx"
32
+ },
33
+ "dependencies": {
34
+ "@opentui/core": "0.1.79",
35
+ "@opentui/react": "0.1.79",
36
+ "@tokentop/agent-claude-code": "^1.1.0",
37
+ "@tokentop/agent-opencode": "^1.0.0",
38
+ "@tokentop/plugin-sdk": "^1.1.0",
39
+ "react": "^19.0.0",
40
+ "react-devtools-core": "^7.0.1",
41
+ "zod": "^3.24.0"
42
+ },
43
+ "devDependencies": {
44
+ "@types/bun": "latest",
45
+ "@types/react": "^19.0.0"
46
+ },
47
+ "keywords": [
48
+ "ai",
49
+ "llm",
50
+ "token",
51
+ "usage",
52
+ "monitor",
53
+ "tui",
54
+ "terminal",
55
+ "htop",
56
+ "anthropic",
57
+ "openai",
58
+ "claude",
59
+ "gpt"
60
+ ],
61
+ "repository": {
62
+ "type": "git",
63
+ "url": "https://github.com/tokentopapp/tokentop"
64
+ },
65
+ "engines": {
66
+ "bun": ">=1.0.0"
67
+ }
68
+ }
@@ -0,0 +1,176 @@
1
+ import type { SessionUsageData } from '@/plugins/types/agent.ts';
2
+ import {
3
+ totalTokenCount,
4
+ type AgentId,
5
+ type AgentName,
6
+ type AgentSessionAggregate,
7
+ type AgentSessionStream,
8
+ type StreamWindowedTokens,
9
+ type TokenCounts,
10
+ } from './types.ts';
11
+
12
+ const ACTIVE_THRESHOLD_MS = 2 * 60 * 1000;
13
+
14
+ interface AggregateOptions {
15
+ agentId: AgentId;
16
+ agentName: AgentName;
17
+ rows: SessionUsageData[];
18
+ now?: number;
19
+ activeThresholdMs?: number;
20
+ }
21
+
22
+ interface StreamKey {
23
+ providerId: string;
24
+ modelId: string;
25
+ }
26
+
27
+ function streamKeyToString(key: StreamKey): string {
28
+ return `${key.providerId}::${key.modelId}`;
29
+ }
30
+
31
+ function sumTokens(a: TokenCounts, b: TokenCounts): TokenCounts {
32
+ const result: TokenCounts = {
33
+ input: a.input + b.input,
34
+ output: a.output + b.output,
35
+ };
36
+
37
+ const cacheRead = (a.cacheRead ?? 0) + (b.cacheRead ?? 0);
38
+ if (cacheRead > 0) result.cacheRead = cacheRead;
39
+
40
+ const cacheWrite = (a.cacheWrite ?? 0) + (b.cacheWrite ?? 0);
41
+ if (cacheWrite > 0) result.cacheWrite = cacheWrite;
42
+
43
+ return result;
44
+ }
45
+
46
+ function computeWindowBoundaries(now: number) {
47
+ const nowDate = new Date(now);
48
+ const startOfDay = new Date(nowDate.getFullYear(), nowDate.getMonth(), nowDate.getDate()).getTime();
49
+ const dayOfWeek = nowDate.getDay();
50
+ const startOfWeek = new Date(nowDate.getFullYear(), nowDate.getMonth(), nowDate.getDate() - dayOfWeek).getTime();
51
+ const startOfMonth = new Date(nowDate.getFullYear(), nowDate.getMonth(), 1).getTime();
52
+ return { startOfDay, startOfWeek, startOfMonth };
53
+ }
54
+
55
+ interface StreamAccumulator {
56
+ key: StreamKey;
57
+ tokens: TokenCounts;
58
+ requestCount: number;
59
+ windowed: StreamWindowedTokens;
60
+ }
61
+
62
+ export function aggregateSessionUsage(options: AggregateOptions): AgentSessionAggregate[] {
63
+ const { agentId, agentName, rows, now = Date.now(), activeThresholdMs = ACTIVE_THRESHOLD_MS } = options;
64
+ const { startOfDay, startOfWeek, startOfMonth } = computeWindowBoundaries(now);
65
+
66
+ const sessionMap = new Map<string, {
67
+ sessionName?: string;
68
+ projectPath?: string;
69
+ timestamps: number[];
70
+ sessionUpdatedAt?: number;
71
+ streamMap: Map<string, StreamAccumulator>;
72
+ }>();
73
+
74
+ for (const row of rows) {
75
+ const existing = sessionMap.get(row.sessionId);
76
+ if (!existing) {
77
+ const newSession: {
78
+ sessionName?: string;
79
+ projectPath?: string;
80
+ timestamps: number[];
81
+ sessionUpdatedAt?: number;
82
+ streamMap: Map<string, StreamAccumulator>;
83
+ } = {
84
+ timestamps: [],
85
+ streamMap: new Map(),
86
+ };
87
+ if (row.sessionName) newSession.sessionName = row.sessionName;
88
+ if (row.projectPath) newSession.projectPath = row.projectPath;
89
+ if (row.sessionUpdatedAt) newSession.sessionUpdatedAt = row.sessionUpdatedAt;
90
+ sessionMap.set(row.sessionId, newSession);
91
+ }
92
+
93
+ const session = sessionMap.get(row.sessionId)!;
94
+
95
+ session.timestamps.push(row.timestamp);
96
+ if (row.sessionName && !session.sessionName) {
97
+ session.sessionName = row.sessionName;
98
+ }
99
+ if (row.projectPath && !session.projectPath) {
100
+ session.projectPath = row.projectPath;
101
+ }
102
+ if (row.sessionUpdatedAt && (!session.sessionUpdatedAt || row.sessionUpdatedAt > session.sessionUpdatedAt)) {
103
+ session.sessionUpdatedAt = row.sessionUpdatedAt;
104
+ }
105
+
106
+ const streamKey: StreamKey = { providerId: row.providerId, modelId: row.modelId };
107
+ const streamKeyStr = streamKeyToString(streamKey);
108
+
109
+ let stream = session.streamMap.get(streamKeyStr);
110
+ if (!stream) {
111
+ stream = {
112
+ key: streamKey,
113
+ tokens: { input: 0, output: 0 },
114
+ requestCount: 0,
115
+ windowed: { dayTokens: 0, weekTokens: 0, monthTokens: 0, totalTokens: 0 },
116
+ };
117
+ session.streamMap.set(streamKeyStr, stream);
118
+ }
119
+
120
+ stream.tokens = sumTokens(stream.tokens, row.tokens);
121
+ stream.requestCount += 1;
122
+
123
+ const msgTokens = totalTokenCount(row.tokens);
124
+ stream.windowed.totalTokens += msgTokens;
125
+ if (row.timestamp >= startOfDay) stream.windowed.dayTokens += msgTokens;
126
+ if (row.timestamp >= startOfWeek) stream.windowed.weekTokens += msgTokens;
127
+ if (row.timestamp >= startOfMonth) stream.windowed.monthTokens += msgTokens;
128
+ }
129
+
130
+ const results: AgentSessionAggregate[] = [];
131
+
132
+ for (const [sessionId, session] of sessionMap) {
133
+ const startedAt = Math.min(...session.timestamps);
134
+ const lastActivityAt = Math.max(...session.timestamps);
135
+ const lastSeenAt = session.sessionUpdatedAt ?? lastActivityAt;
136
+ const status = (now - lastSeenAt) <= activeThresholdMs ? 'active' : 'idle';
137
+
138
+ const streams: AgentSessionStream[] = [];
139
+ const streamWindowedTokens = new Map<string, StreamWindowedTokens>();
140
+ let totals: TokenCounts = { input: 0, output: 0 };
141
+ let totalRequestCount = 0;
142
+
143
+ for (const [streamKeyStr, stream] of session.streamMap) {
144
+ streams.push({
145
+ providerId: stream.key.providerId,
146
+ modelId: stream.key.modelId,
147
+ tokens: stream.tokens,
148
+ requestCount: stream.requestCount,
149
+ });
150
+ totals = sumTokens(totals, stream.tokens);
151
+ totalRequestCount += stream.requestCount;
152
+ streamWindowedTokens.set(streamKeyStr, stream.windowed);
153
+ }
154
+
155
+ const aggregate: AgentSessionAggregate = {
156
+ sessionId,
157
+ agentId,
158
+ agentName,
159
+ startedAt,
160
+ lastActivityAt,
161
+ status,
162
+ totals,
163
+ requestCount: totalRequestCount,
164
+ streams,
165
+ costInDay: 0,
166
+ costInWeek: 0,
167
+ costInMonth: 0,
168
+ _streamWindowedTokens: streamWindowedTokens,
169
+ };
170
+ if (session.sessionName) aggregate.sessionName = session.sessionName;
171
+ if (session.projectPath) aggregate.projectPath = session.projectPath;
172
+ results.push(aggregate);
173
+ }
174
+
175
+ return results.sort((a, b) => b.lastActivityAt - a.lastActivityAt);
176
+ }
@@ -0,0 +1,74 @@
1
+ import { getPricing, estimateCost } from '@/pricing/index.ts';
2
+ import type { AgentSessionAggregate, AgentSessionStream, StreamCostBreakdown } from './types.ts';
3
+
4
+ export async function priceStream(stream: AgentSessionStream): Promise<AgentSessionStream> {
5
+ const pricing = await getPricing(stream.providerId, stream.modelId);
6
+
7
+ if (!pricing) {
8
+ return { ...stream, pricingSource: 'unknown' };
9
+ }
10
+
11
+ const breakdown = estimateCost(stream.tokens, pricing);
12
+ const source = pricing.source === 'models.dev' ? 'models.dev' : 'fallback';
13
+
14
+ const costBreakdown: StreamCostBreakdown = {
15
+ total: breakdown.total,
16
+ input: breakdown.input ?? 0,
17
+ output: breakdown.output ?? 0,
18
+ };
19
+ if (breakdown.cacheRead) costBreakdown.cacheRead = breakdown.cacheRead;
20
+ if (breakdown.cacheWrite) costBreakdown.cacheWrite = breakdown.cacheWrite;
21
+
22
+ return {
23
+ ...stream,
24
+ costUsd: breakdown.total,
25
+ costBreakdown,
26
+ pricingSource: source,
27
+ };
28
+ }
29
+
30
+ export async function priceSession(session: AgentSessionAggregate): Promise<AgentSessionAggregate> {
31
+ const pricedStreams = await Promise.all(session.streams.map(priceStream));
32
+
33
+ const totalCostUsd = pricedStreams.reduce((sum, s) => {
34
+ return sum + (s.costUsd ?? 0);
35
+ }, 0);
36
+
37
+ const hasAnyCost = pricedStreams.some(s => s.costUsd !== undefined);
38
+
39
+ let costInDay = 0;
40
+ let costInWeek = 0;
41
+ let costInMonth = 0;
42
+
43
+ if (hasAnyCost && session._streamWindowedTokens) {
44
+ for (const stream of pricedStreams) {
45
+ const streamCost = stream.costUsd ?? 0;
46
+ if (streamCost === 0) continue;
47
+
48
+ const keyStr = `${stream.providerId}::${stream.modelId}`;
49
+ const windowed = session._streamWindowedTokens.get(keyStr);
50
+ if (!windowed || windowed.totalTokens === 0) continue;
51
+
52
+ const ratio = 1 / windowed.totalTokens;
53
+ costInDay += streamCost * windowed.dayTokens * ratio;
54
+ costInWeek += streamCost * windowed.weekTokens * ratio;
55
+ costInMonth += streamCost * windowed.monthTokens * ratio;
56
+ }
57
+ }
58
+
59
+ const result: AgentSessionAggregate = {
60
+ ...session,
61
+ streams: pricedStreams,
62
+ costInDay,
63
+ costInWeek,
64
+ costInMonth,
65
+ };
66
+ if (hasAnyCost) result.totalCostUsd = totalCostUsd;
67
+ delete result._streamWindowedTokens;
68
+
69
+ return result;
70
+ }
71
+
72
+ export async function priceSessions(sessions: AgentSessionAggregate[]): Promise<AgentSessionAggregate[]> {
73
+ return Promise.all(sessions.map(priceSession));
74
+ }
@@ -0,0 +1,12 @@
1
+ export type {
2
+ AgentName,
3
+ AgentId,
4
+ TokenCounts,
5
+ AgentSessionStream,
6
+ AgentSessionAggregate,
7
+ AgentInfo,
8
+ } from './types.ts';
9
+
10
+ export { aggregateSessionUsage } from './aggregator.ts';
11
+
12
+ export { priceStream, priceSession, priceSessions } from './costing.ts';