@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.
- package/LICENSE +21 -0
- package/README.md +273 -0
- package/bin/ttop.js +49 -0
- package/package.json +68 -0
- package/src/agents/aggregator.ts +176 -0
- package/src/agents/costing.ts +74 -0
- package/src/agents/index.ts +12 -0
- package/src/agents/types.ts +71 -0
- package/src/cli.ts +167 -0
- package/src/config/schema.test.ts +166 -0
- package/src/config/schema.ts +165 -0
- package/src/demo/simulator.ts +745 -0
- package/src/plugins/agents/index.ts +2 -0
- package/src/plugins/auth-sources.ts +186 -0
- package/src/plugins/lifecycle.ts +177 -0
- package/src/plugins/loader.test.ts +195 -0
- package/src/plugins/loader.ts +282 -0
- package/src/plugins/notification-bus.ts +153 -0
- package/src/plugins/notifications/index.ts +2 -0
- package/src/plugins/notifications/terminal-bell.ts +66 -0
- package/src/plugins/notifications/visual-flash.ts +57 -0
- package/src/plugins/npm-installer.ts +164 -0
- package/src/plugins/plugin-context-factory.ts +67 -0
- package/src/plugins/plugin-host.ts +205 -0
- package/src/plugins/providers/anthropic.ts +325 -0
- package/src/plugins/providers/antigravity.ts +309 -0
- package/src/plugins/providers/codex.ts +250 -0
- package/src/plugins/providers/gemini.ts +391 -0
- package/src/plugins/providers/github-copilot.ts +194 -0
- package/src/plugins/providers/index.ts +12 -0
- package/src/plugins/providers/minimax.ts +223 -0
- package/src/plugins/providers/openai-api.ts +250 -0
- package/src/plugins/providers/opencode-zen.ts +125 -0
- package/src/plugins/providers/perplexity.ts +177 -0
- package/src/plugins/providers/zai.ts +225 -0
- package/src/plugins/registry.ts +251 -0
- package/src/plugins/sandbox-guard.test.ts +191 -0
- package/src/plugins/sandbox-guard.ts +155 -0
- package/src/plugins/sandbox.ts +138 -0
- package/src/plugins/themes/catppuccin-latte.ts +45 -0
- package/src/plugins/themes/catppuccin-mocha.ts +45 -0
- package/src/plugins/themes/claude-code.ts +45 -0
- package/src/plugins/themes/dracula.ts +45 -0
- package/src/plugins/themes/github-light.ts +45 -0
- package/src/plugins/themes/gruvbox-dark.ts +45 -0
- package/src/plugins/themes/gruvbox-light.ts +45 -0
- package/src/plugins/themes/index.ts +15 -0
- package/src/plugins/themes/kanagawa.ts +45 -0
- package/src/plugins/themes/nord.ts +45 -0
- package/src/plugins/themes/one-dark.ts +45 -0
- package/src/plugins/themes/opencode.ts +47 -0
- package/src/plugins/themes/rose-pine-dawn.ts +45 -0
- package/src/plugins/themes/rose-pine.ts +45 -0
- package/src/plugins/themes/solarized-light.ts +45 -0
- package/src/plugins/themes/tokyo-night.ts +49 -0
- package/src/plugins/types/agent.ts +108 -0
- package/src/plugins/types/base.ts +267 -0
- package/src/plugins/types/index.ts +19 -0
- package/src/plugins/types/notification.ts +51 -0
- package/src/plugins/types/provider.ts +233 -0
- package/src/plugins/types/theme.ts +70 -0
- package/src/plugins/update-checker.ts +177 -0
- package/src/pricing/estimator.test.ts +181 -0
- package/src/pricing/estimator.ts +80 -0
- package/src/pricing/fallback.test.ts +71 -0
- package/src/pricing/fallback.ts +62 -0
- package/src/pricing/index.ts +66 -0
- package/src/pricing/models-dev.ts +130 -0
- package/src/storage/database.ts +613 -0
- package/src/storage/db.ts +64 -0
- package/src/storage/index.ts +66 -0
- package/src/storage/migrations/index.ts +189 -0
- package/src/storage/paths.ts +24 -0
- package/src/storage/repos/agentSessions.ts +210 -0
- package/src/storage/repos/providerSnapshots.ts +116 -0
- package/src/storage/repos/usageEvents.ts +334 -0
- package/src/storage/types.ts +288 -0
- package/src/tui/App.tsx +381 -0
- package/src/tui/components/CommandPalette.tsx +166 -0
- package/src/tui/components/DebugConsole.tsx +161 -0
- package/src/tui/components/DebugPanel.tsx +660 -0
- package/src/tui/components/FooterHints.tsx +22 -0
- package/src/tui/components/GhostProviderCard.tsx +45 -0
- package/src/tui/components/Header.tsx +125 -0
- package/src/tui/components/HelpOverlay.tsx +45 -0
- package/src/tui/components/InlineGauge.tsx +34 -0
- package/src/tui/components/InlineSparkline.tsx +78 -0
- package/src/tui/components/KpiStrip.tsx +246 -0
- package/src/tui/components/LimitGauge.tsx +186 -0
- package/src/tui/components/ModalBackdrop.tsx +42 -0
- package/src/tui/components/ProviderAggregateStrip.tsx +200 -0
- package/src/tui/components/ProviderCard.tsx +309 -0
- package/src/tui/components/ProviderDetailPanel.tsx +237 -0
- package/src/tui/components/ProviderLimitsPanel.tsx +308 -0
- package/src/tui/components/ProvidersList.tsx +380 -0
- package/src/tui/components/SessionDetailsDrawer.tsx +317 -0
- package/src/tui/components/SessionsTable.tsx +401 -0
- package/src/tui/components/SettingsModal.tsx +587 -0
- package/src/tui/components/SidebarBreakdown.tsx +87 -0
- package/src/tui/components/Skeleton.tsx +102 -0
- package/src/tui/components/SmartSidebar.tsx +644 -0
- package/src/tui/components/Sparkline.tsx +180 -0
- package/src/tui/components/Spinner.tsx +36 -0
- package/src/tui/components/StatusBar.tsx +96 -0
- package/src/tui/components/ThemePicker.tsx +234 -0
- package/src/tui/components/Toast.tsx +61 -0
- package/src/tui/components/UsageGauge.tsx +80 -0
- package/src/tui/components/index.ts +5 -0
- package/src/tui/contexts/AgentSessionContext.tsx +307 -0
- package/src/tui/contexts/ConfigContext.tsx +141 -0
- package/src/tui/contexts/DashboardRuntimeContext.tsx +102 -0
- package/src/tui/contexts/DemoModeContext.tsx +60 -0
- package/src/tui/contexts/DrawerContext.tsx +42 -0
- package/src/tui/contexts/InputContext.tsx +30 -0
- package/src/tui/contexts/LogContext.tsx +150 -0
- package/src/tui/contexts/PluginContext.tsx +424 -0
- package/src/tui/contexts/RealTimeActivityContext.tsx +119 -0
- package/src/tui/contexts/StorageContext.tsx +233 -0
- package/src/tui/contexts/ThemeContext.tsx +95 -0
- package/src/tui/contexts/TimeWindowContext.tsx +158 -0
- package/src/tui/contexts/ToastContext.tsx +40 -0
- package/src/tui/contexts/index.ts +5 -0
- package/src/tui/createApp.tsx +47 -0
- package/src/tui/debug/captureFrame.ts +262 -0
- package/src/tui/debug/snapshot.tsx +644 -0
- package/src/tui/driver/assertions.ts +279 -0
- package/src/tui/driver/cli.ts +717 -0
- package/src/tui/driver/coverage.ts +204 -0
- package/src/tui/driver/demo-snapshot.ts +66 -0
- package/src/tui/driver/diff.ts +166 -0
- package/src/tui/driver/driver.ts +270 -0
- package/src/tui/driver/index.ts +53 -0
- package/src/tui/driver/recorder.ts +303 -0
- package/src/tui/driver/test-workflow.ts +115 -0
- package/src/tui/driver/test.ts +47 -0
- package/src/tui/hooks/index.ts +20 -0
- package/src/tui/hooks/useAnimatedValue.ts +113 -0
- package/src/tui/hooks/useDashboardKeyboard.ts +341 -0
- package/src/tui/hooks/useDashboardState.ts +116 -0
- package/src/tui/hooks/useEmaActivity.ts +223 -0
- package/src/tui/hooks/useEntranceAnimation.ts +62 -0
- package/src/tui/hooks/useExitAnimation.ts +137 -0
- package/src/tui/hooks/usePulse.ts +138 -0
- package/src/tui/hooks/useSafeRenderer.ts +24 -0
- package/src/tui/hooks/useValueFlash.ts +111 -0
- package/src/tui/index.tsx +50 -0
- package/src/tui/utils/providerColor.ts +32 -0
- package/src/tui/views/Dashboard.tsx +340 -0
- package/src/tui/views/HistoricalTrendsView.tsx +1431 -0
- package/src/tui/views/ProjectsView.tsx +1277 -0
- package/src/tui/views/RealTimeDashboard.tsx +393 -0
- package/src/tui/views/SettingsView.tsx +465 -0
- package/src/tui/views/index.ts +2 -0
- package/src/types/global.d.ts +5 -0
- package/src/types/opentui-test-utils.d.ts +18 -0
- package/src/utils/clipboard.ts +118 -0
- package/src/utils/currency.test.ts +106 -0
- package/src/utils/currency.ts +79 -0
- 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
|
+

|
|
13
|
+
|
|
14
|
+
<!-- TODO: uncomment CI badge when GitHub Actions workflow is set up -->
|
|
15
|
+
<!-- [](https://github.com/tokentopapp/tokentop/actions) -->
|
|
16
|
+
[](LICENSE)
|
|
17
|
+
[](https://github.com/tokentopapp/tokentop/releases)
|
|
18
|
+
[](https://www.typescriptlang.org/)
|
|
19
|
+
[](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';
|