tokenleak 0.1.0 → 0.3.0
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/README.md +259 -82
- package/package.json +1 -1
- package/tokenleak.js +437 -48
package/README.md
CHANGED
|
@@ -1,160 +1,337 @@
|
|
|
1
1
|
# Tokenleak
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
See where your AI tokens actually go. Tokenleak reads local usage logs from **Claude Code**, **Codex**, and **Open Code**, then renders heatmaps, dashboards, and shareable cards — all from your terminal.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Install
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- Terminal dashboard with ANSI colours and Unicode block characters
|
|
9
|
-
- SVG and PNG image export for sharing
|
|
10
|
-
- JSON export for downstream tooling
|
|
11
|
-
- Streak tracking (current and longest)
|
|
12
|
-
- Cost estimation with per-model pricing
|
|
13
|
-
- Rolling 30-day window statistics
|
|
14
|
-
- Day-of-week usage breakdown
|
|
15
|
-
- Multi-provider support with automatic detection
|
|
16
|
-
- Configuration file support (`~/.tokenleakrc`)
|
|
17
|
-
|
|
18
|
-
## Quick Start
|
|
7
|
+
Tokenleak requires [Bun](https://bun.sh) (v1.0+).
|
|
19
8
|
|
|
20
9
|
```bash
|
|
21
|
-
# Install globally with bun
|
|
22
10
|
bun install -g tokenleak
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
After installing, run `tokenleak` in your terminal. It will automatically detect which AI coding tools you have installed and display your usage.
|
|
14
|
+
|
|
15
|
+
### From source
|
|
23
16
|
|
|
24
|
-
|
|
17
|
+
```bash
|
|
25
18
|
git clone https://github.com/ya-nsh/tokenleak.git
|
|
26
19
|
cd tokenleak
|
|
27
20
|
bun install
|
|
28
21
|
bun run build
|
|
22
|
+
bun run bundle
|
|
29
23
|
|
|
30
|
-
# Run
|
|
31
|
-
bun
|
|
24
|
+
# Run directly
|
|
25
|
+
bun dist/tokenleak.js
|
|
32
26
|
```
|
|
33
27
|
|
|
34
28
|
## Usage
|
|
35
29
|
|
|
36
30
|
```bash
|
|
37
|
-
#
|
|
31
|
+
# Show a terminal dashboard of your token usage (default)
|
|
38
32
|
tokenleak
|
|
39
33
|
|
|
40
|
-
# JSON
|
|
34
|
+
# Output as JSON
|
|
41
35
|
tokenleak --format json
|
|
42
36
|
|
|
43
|
-
# SVG heatmap
|
|
37
|
+
# Export an SVG heatmap
|
|
44
38
|
tokenleak --format svg --output usage.svg
|
|
45
39
|
|
|
46
|
-
# PNG
|
|
40
|
+
# Export a PNG image
|
|
47
41
|
tokenleak --format png --output usage.png
|
|
48
42
|
|
|
49
|
-
#
|
|
50
|
-
tokenleak
|
|
43
|
+
# Save to a file (format is inferred from the extension)
|
|
44
|
+
tokenleak -o report.json
|
|
45
|
+
tokenleak -o heatmap.svg
|
|
46
|
+
tokenleak -o card.png
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Date filtering
|
|
50
|
+
|
|
51
|
+
By default, Tokenleak shows the last **90 days** of usage.
|
|
51
52
|
|
|
52
|
-
|
|
53
|
-
|
|
53
|
+
```bash
|
|
54
|
+
# Last 30 days
|
|
55
|
+
tokenleak --days 30
|
|
56
|
+
|
|
57
|
+
# Specific date range
|
|
58
|
+
tokenleak --since 2025-06-01 --until 2025-12-31
|
|
59
|
+
|
|
60
|
+
# Everything since a date (until defaults to today)
|
|
61
|
+
tokenleak --since 2025-01-01
|
|
62
|
+
|
|
63
|
+
# --since takes priority over --days when both are provided
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Provider filtering
|
|
54
67
|
|
|
55
|
-
|
|
68
|
+
Tokenleak auto-detects all installed providers. You can filter to specific ones:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
# Only Claude Code
|
|
56
72
|
tokenleak --provider claude-code
|
|
57
73
|
|
|
58
|
-
#
|
|
59
|
-
tokenleak --
|
|
74
|
+
# Only Codex
|
|
75
|
+
tokenleak --provider codex
|
|
60
76
|
|
|
61
|
-
#
|
|
77
|
+
# Multiple providers (comma-separated)
|
|
78
|
+
tokenleak --provider claude-code,codex
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Compare mode
|
|
82
|
+
|
|
83
|
+
Compare your usage across two time periods to see how your token consumption has changed:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# Auto-compare: splits your date range in half
|
|
87
|
+
# (e.g. with --days 60, compares last 30 days vs. previous 30 days)
|
|
88
|
+
tokenleak --compare auto
|
|
89
|
+
|
|
90
|
+
# Compare against a specific previous period
|
|
91
|
+
tokenleak --compare 2025-01-01..2025-03-31
|
|
92
|
+
|
|
93
|
+
# Compare outputs deltas for total tokens, cost, streaks, etc.
|
|
94
|
+
tokenleak --compare auto --format json
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Compare mode outputs a JSON object with `currentPeriod`, `previousPeriod`, and `deltas` showing the difference between the two periods (positive = increase, negative = decrease).
|
|
98
|
+
|
|
99
|
+
### Themes
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
# Dark theme (default)
|
|
103
|
+
tokenleak --theme dark
|
|
104
|
+
|
|
105
|
+
# Light theme
|
|
62
106
|
tokenleak --theme light
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Terminal options
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
# Set terminal width (affects heatmap and dashboard layout)
|
|
113
|
+
tokenleak --width 120
|
|
63
114
|
|
|
64
|
-
# Disable ANSI colours
|
|
115
|
+
# Disable ANSI colours (useful for piping output)
|
|
65
116
|
tokenleak --no-color
|
|
66
117
|
|
|
67
|
-
#
|
|
68
|
-
tokenleak --
|
|
118
|
+
# Hide the insights panel
|
|
119
|
+
tokenleak --no-insights
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Sharing
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
# Copy rendered output to clipboard
|
|
126
|
+
tokenleak --format json --clipboard
|
|
127
|
+
|
|
128
|
+
# Open the output file in your default application after saving
|
|
129
|
+
tokenleak -o usage.svg --open
|
|
130
|
+
|
|
131
|
+
# Upload to a GitHub Gist (requires gh CLI to be authenticated)
|
|
132
|
+
tokenleak --format json --upload gist
|
|
69
133
|
```
|
|
70
134
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
| Flag | Alias | Description |
|
|
74
|
-
|
|
75
|
-
| `--format` | `-f` | Output format: `json`, `svg`, `png`, `terminal` |
|
|
76
|
-
| `--theme` | `-t` | Colour theme: `dark`, `light` |
|
|
77
|
-
| `--since` | `-s` | Start date (`YYYY-MM-DD`) |
|
|
78
|
-
| `--until` | `-u` | End date (`YYYY-MM-DD`)
|
|
79
|
-
| `--days` | `-d` | Number of days to look back
|
|
80
|
-
| `--output` | `-o` | Output file path |
|
|
81
|
-
| `--width` | `-w` | Terminal width
|
|
82
|
-
| `--no-color` | |
|
|
83
|
-
| `--no-insights` | | Hide the insights panel |
|
|
84
|
-
| `--compare` | | Compare two date ranges
|
|
85
|
-
| `--provider` | `-p` | Filter to specific provider(s), comma-separated |
|
|
86
|
-
| `--
|
|
87
|
-
| `--
|
|
88
|
-
|
|
89
|
-
|
|
135
|
+
## All flags
|
|
136
|
+
|
|
137
|
+
| Flag | Alias | Default | Description |
|
|
138
|
+
|------|-------|---------|-------------|
|
|
139
|
+
| `--format` | `-f` | `terminal` | Output format: `json`, `svg`, `png`, `terminal` |
|
|
140
|
+
| `--theme` | `-t` | `dark` | Colour theme: `dark`, `light` |
|
|
141
|
+
| `--since` | `-s` | | Start date (`YYYY-MM-DD`). Overrides `--days` |
|
|
142
|
+
| `--until` | `-u` | today | End date (`YYYY-MM-DD`) |
|
|
143
|
+
| `--days` | `-d` | `90` | Number of days to look back |
|
|
144
|
+
| `--output` | `-o` | stdout | Output file path. Format is inferred from extension |
|
|
145
|
+
| `--width` | `-w` | `80` | Terminal width for dashboard layout |
|
|
146
|
+
| `--no-color` | | `false` | Strip ANSI escape codes from terminal output |
|
|
147
|
+
| `--no-insights` | | `false` | Hide the insights panel |
|
|
148
|
+
| `--compare` | | | Compare two date ranges. Use `auto` or `YYYY-MM-DD..YYYY-MM-DD` |
|
|
149
|
+
| `--provider` | `-p` | all | Filter to specific provider(s), comma-separated |
|
|
150
|
+
| `--clipboard` | | `false` | Copy output to clipboard after rendering |
|
|
151
|
+
| `--open` | | `false` | Open output file in default app (requires `--output`) |
|
|
152
|
+
| `--upload` | | | Upload output to a service. Supported: `gist` |
|
|
153
|
+
| `--version` | | | Print version number |
|
|
154
|
+
| `--help` | | | Print usage information |
|
|
155
|
+
|
|
156
|
+
## Supported providers
|
|
90
157
|
|
|
91
158
|
### Claude Code
|
|
92
159
|
|
|
93
|
-
Reads JSONL conversation logs from the Claude Code
|
|
160
|
+
Reads JSONL conversation logs from the Claude Code projects directory. Each assistant message with a `usage` field is parsed for input/output/cache token counts.
|
|
94
161
|
|
|
95
|
-
|
|
96
|
-
|
|
162
|
+
| | |
|
|
163
|
+
|---|---|
|
|
164
|
+
| **Data location** | `~/.claude/projects/*/*.jsonl` |
|
|
165
|
+
| **Override** | Set `CLAUDE_CONFIG_DIR` environment variable |
|
|
166
|
+
| **Provider name** | `claude-code` |
|
|
97
167
|
|
|
98
168
|
### Codex
|
|
99
169
|
|
|
100
|
-
Reads JSONL session logs from the Codex
|
|
170
|
+
Reads JSONL session logs from the Codex sessions directory. Parses `response` events for token usage with cumulative delta extraction.
|
|
101
171
|
|
|
102
|
-
|
|
103
|
-
|
|
172
|
+
| | |
|
|
173
|
+
|---|---|
|
|
174
|
+
| **Data location** | `~/.codex/sessions/*.jsonl` |
|
|
175
|
+
| **Override** | Set `CODEX_HOME` environment variable |
|
|
176
|
+
| **Provider name** | `codex` |
|
|
104
177
|
|
|
105
178
|
### Open Code
|
|
106
179
|
|
|
107
|
-
Reads usage data from the Open Code SQLite database
|
|
180
|
+
Reads usage data from the Open Code SQLite database. Falls back to legacy JSON session files if no database is found.
|
|
181
|
+
|
|
182
|
+
| | |
|
|
183
|
+
|---|---|
|
|
184
|
+
| **Data location** | `~/.opencode/sessions.db` (primary) or `~/.opencode/sessions/*.json` (fallback) |
|
|
185
|
+
| **Provider name** | `open-code` |
|
|
186
|
+
|
|
187
|
+
## Output formats
|
|
188
|
+
|
|
189
|
+
### `terminal` (default)
|
|
190
|
+
|
|
191
|
+
A full-width dashboard rendered in your terminal with:
|
|
192
|
+
|
|
193
|
+
- GitHub-style heatmap using Unicode block characters (`░▒▓█`)
|
|
194
|
+
- Stats panel: current streak, longest streak, total tokens, total cost, 30-day rolling totals, daily averages, cache hit rate
|
|
195
|
+
- Day-of-week breakdown showing which days you code most
|
|
196
|
+
- Top models ranked by token usage
|
|
197
|
+
- Insights: peak day, most active day, top model, top provider
|
|
198
|
+
|
|
199
|
+
Falls back to a compact one-liner when terminal width is under 40 characters.
|
|
200
|
+
|
|
201
|
+
### `json`
|
|
202
|
+
|
|
203
|
+
Structured JSON output containing:
|
|
204
|
+
|
|
205
|
+
```jsonc
|
|
206
|
+
{
|
|
207
|
+
"schemaVersion": 1,
|
|
208
|
+
"generated": "2025-12-01T00:00:00.000Z",
|
|
209
|
+
"dateRange": { "since": "2025-09-01", "until": "2025-12-01" },
|
|
210
|
+
"providers": [
|
|
211
|
+
{
|
|
212
|
+
"name": "claude-code",
|
|
213
|
+
"displayName": "Claude Code",
|
|
214
|
+
"daily": [
|
|
215
|
+
{
|
|
216
|
+
"date": "2025-11-30",
|
|
217
|
+
"inputTokens": 15000,
|
|
218
|
+
"outputTokens": 5000,
|
|
219
|
+
"cacheReadTokens": 2000,
|
|
220
|
+
"cacheWriteTokens": 500,
|
|
221
|
+
"totalTokens": 22500,
|
|
222
|
+
"cost": 0.0825
|
|
223
|
+
}
|
|
224
|
+
// ...
|
|
225
|
+
],
|
|
226
|
+
"models": [
|
|
227
|
+
{ "model": "claude-sonnet-4", "inputTokens": 10000, "outputTokens": 3000, "totalTokens": 13000, "cost": 0.075 }
|
|
228
|
+
],
|
|
229
|
+
"totalTokens": 22500,
|
|
230
|
+
"totalCost": 0.0825
|
|
231
|
+
}
|
|
232
|
+
],
|
|
233
|
+
"aggregated": {
|
|
234
|
+
"currentStreak": 12,
|
|
235
|
+
"longestStreak": 45,
|
|
236
|
+
"totalTokens": 1500000,
|
|
237
|
+
"totalCost": 52.50,
|
|
238
|
+
// ... rolling windows, peaks, averages, day-of-week, top models
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### `svg`
|
|
108
244
|
|
|
109
|
-
-
|
|
245
|
+
A self-contained SVG image with:
|
|
110
246
|
|
|
111
|
-
|
|
247
|
+
- Heatmap grid (7 rows x N weeks) with quantile-based colour intensity
|
|
248
|
+
- Month labels and day-of-week labels
|
|
249
|
+
- Stats panel and insights panel
|
|
250
|
+
- Supports `dark` and `light` themes
|
|
112
251
|
|
|
113
|
-
|
|
252
|
+
### `png`
|
|
253
|
+
|
|
254
|
+
Same layout as SVG, rendered to a PNG image via [sharp](https://sharp.pixelplumbing.com/). Useful for embedding in documents or sharing on platforms that don't support SVG.
|
|
255
|
+
|
|
256
|
+
## Configuration file
|
|
257
|
+
|
|
258
|
+
Create `~/.tokenleakrc` to set persistent defaults:
|
|
114
259
|
|
|
115
260
|
```json
|
|
116
261
|
{
|
|
117
262
|
"format": "terminal",
|
|
118
263
|
"theme": "dark",
|
|
119
|
-
"days":
|
|
264
|
+
"days": 90,
|
|
120
265
|
"width": 120,
|
|
121
266
|
"noColor": false,
|
|
122
267
|
"noInsights": false
|
|
123
268
|
}
|
|
124
269
|
```
|
|
125
270
|
|
|
126
|
-
CLI flags
|
|
271
|
+
**Priority order** (highest wins): CLI flags > environment variables > config file > built-in defaults.
|
|
272
|
+
|
|
273
|
+
All fields are optional. Only include the ones you want to override.
|
|
127
274
|
|
|
128
|
-
## Environment
|
|
275
|
+
## Environment variables
|
|
129
276
|
|
|
130
277
|
| Variable | Default | Description |
|
|
131
278
|
|----------|---------|-------------|
|
|
132
|
-
| `
|
|
133
|
-
| `
|
|
134
|
-
| `
|
|
279
|
+
| `TOKENLEAK_FORMAT` | `terminal` | Default output format |
|
|
280
|
+
| `TOKENLEAK_THEME` | `dark` | Default colour theme |
|
|
281
|
+
| `TOKENLEAK_DAYS` | `90` | Default lookback period in days |
|
|
282
|
+
| `TOKENLEAK_MAX_JSONL_RECORD_BYTES` | `10485760` (10 MB) | Max size of a single JSONL record before it is rejected |
|
|
135
283
|
| `CLAUDE_CONFIG_DIR` | `~/.claude` | Claude Code configuration directory |
|
|
136
284
|
| `CODEX_HOME` | `~/.codex` | Codex home directory |
|
|
137
285
|
|
|
138
|
-
##
|
|
286
|
+
## What Tokenleak tracks
|
|
287
|
+
|
|
288
|
+
Tokenleak reads your **local** log files only. It never sends data anywhere (unless you explicitly use `--upload`).
|
|
289
|
+
|
|
290
|
+
For each day of usage, it tracks:
|
|
291
|
+
|
|
292
|
+
- **Input tokens** — tokens sent to the model
|
|
293
|
+
- **Output tokens** — tokens generated by the model
|
|
294
|
+
- **Cache read tokens** — tokens served from prompt cache
|
|
295
|
+
- **Cache write tokens** — tokens written to prompt cache
|
|
296
|
+
- **Cost** — estimated USD cost based on per-model pricing
|
|
297
|
+
|
|
298
|
+
It then computes:
|
|
299
|
+
|
|
300
|
+
- **Streaks** — consecutive days with any token usage
|
|
301
|
+
- **Rolling 30-day totals** — tokens and cost over a sliding window
|
|
302
|
+
- **Peak day** — the single day with the highest token usage
|
|
303
|
+
- **Day-of-week breakdown** — which days of the week you use AI most
|
|
304
|
+
- **Cache hit rate** — percentage of input tokens served from cache
|
|
305
|
+
- **Top models** — models ranked by total token consumption
|
|
306
|
+
- **Daily averages** — mean tokens and cost per day
|
|
307
|
+
|
|
308
|
+
### Supported models and pricing
|
|
309
|
+
|
|
310
|
+
Tokenleak includes pricing for these model families:
|
|
311
|
+
|
|
312
|
+
| Family | Models |
|
|
313
|
+
|--------|--------|
|
|
314
|
+
| Claude 3 | `claude-3-haiku`, `claude-3-sonnet`, `claude-3-opus` |
|
|
315
|
+
| Claude 3.5 | `claude-3.5-haiku`, `claude-3.5-sonnet` |
|
|
316
|
+
| Claude 4 | `claude-sonnet-4`, `claude-opus-4` |
|
|
317
|
+
| GPT-4o | `gpt-4o`, `gpt-4o-mini` |
|
|
318
|
+
| o-series | `o1`, `o1-mini`, `o3`, `o3-mini`, `o4-mini` |
|
|
139
319
|
|
|
140
|
-
|
|
141
|
-
|--------|-------------|
|
|
142
|
-
| `terminal` | ANSI dashboard with heatmap and stats, rendered in the shell |
|
|
143
|
-
| `json` | Structured JSON export with daily data, insights, and aggregated stats |
|
|
144
|
-
| `svg` | SVG heatmap with stats and insights panels |
|
|
145
|
-
| `png` | PNG heatmap rendered via `@napi-rs/canvas` |
|
|
320
|
+
Model names with date suffixes (e.g. `claude-sonnet-4-20250514`) are automatically normalised. Unknown models show `$0.00` cost but tokens are still tracked.
|
|
146
321
|
|
|
147
|
-
## Project
|
|
322
|
+
## Project structure
|
|
148
323
|
|
|
149
324
|
```
|
|
150
325
|
tokenleak/
|
|
151
326
|
packages/
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
327
|
+
core/ Shared types, constants, aggregation engine
|
|
328
|
+
registry/ Provider parsers and model pricing
|
|
329
|
+
renderers/ JSON, SVG, PNG, and terminal output
|
|
330
|
+
cli/ CLI entrypoint and config handling
|
|
331
|
+
scripts/
|
|
332
|
+
build-npm.ts Bundles CLI for npm publishing
|
|
333
|
+
dist/
|
|
334
|
+
tokenleak.js Bundled CLI (generated)
|
|
158
335
|
```
|
|
159
336
|
|
|
160
337
|
## Contributing
|
package/package.json
CHANGED
package/tokenleak.js
CHANGED
|
@@ -695,7 +695,7 @@ function computePreviousPeriod(current) {
|
|
|
695
695
|
};
|
|
696
696
|
}
|
|
697
697
|
// packages/core/dist/index.js
|
|
698
|
-
var VERSION = "0.
|
|
698
|
+
var VERSION = "0.2.0";
|
|
699
699
|
|
|
700
700
|
// packages/registry/dist/models/normalizer.js
|
|
701
701
|
var DATE_SUFFIX_PATTERN = /-\d{8}$/;
|
|
@@ -735,18 +735,48 @@ var MODEL_PRICING = {
|
|
|
735
735
|
cacheRead: 0.3,
|
|
736
736
|
cacheWrite: 3.75
|
|
737
737
|
},
|
|
738
|
+
"claude-haiku-4-5": {
|
|
739
|
+
input: 0.8,
|
|
740
|
+
output: 4,
|
|
741
|
+
cacheRead: 0.08,
|
|
742
|
+
cacheWrite: 1
|
|
743
|
+
},
|
|
744
|
+
"claude-sonnet-4-5": {
|
|
745
|
+
input: 3,
|
|
746
|
+
output: 15,
|
|
747
|
+
cacheRead: 0.3,
|
|
748
|
+
cacheWrite: 3.75
|
|
749
|
+
},
|
|
750
|
+
"claude-opus-4-5": {
|
|
751
|
+
input: 15,
|
|
752
|
+
output: 75,
|
|
753
|
+
cacheRead: 1.5,
|
|
754
|
+
cacheWrite: 18.75
|
|
755
|
+
},
|
|
738
756
|
"claude-sonnet-4": {
|
|
739
757
|
input: 3,
|
|
740
758
|
output: 15,
|
|
741
759
|
cacheRead: 0.3,
|
|
742
760
|
cacheWrite: 3.75
|
|
743
761
|
},
|
|
762
|
+
"claude-sonnet-4-6": {
|
|
763
|
+
input: 3,
|
|
764
|
+
output: 15,
|
|
765
|
+
cacheRead: 0.3,
|
|
766
|
+
cacheWrite: 3.75
|
|
767
|
+
},
|
|
744
768
|
"claude-opus-4": {
|
|
745
769
|
input: 15,
|
|
746
770
|
output: 75,
|
|
747
771
|
cacheRead: 1.5,
|
|
748
772
|
cacheWrite: 18.75
|
|
749
773
|
},
|
|
774
|
+
"claude-opus-4-6": {
|
|
775
|
+
input: 15,
|
|
776
|
+
output: 75,
|
|
777
|
+
cacheRead: 1.5,
|
|
778
|
+
cacheWrite: 18.75
|
|
779
|
+
},
|
|
750
780
|
"gpt-4o": {
|
|
751
781
|
input: 2.5,
|
|
752
782
|
output: 10,
|
|
@@ -867,17 +897,17 @@ async function* splitJsonlRecords(filePath) {
|
|
|
867
897
|
let lineNumber = 0;
|
|
868
898
|
for (const line of lines) {
|
|
869
899
|
lineNumber++;
|
|
870
|
-
if (line.trim() === "") {
|
|
900
|
+
if (line.trim() === "" || /^\x00+$/.test(line) || !/[^\x00]/.test(line)) {
|
|
871
901
|
continue;
|
|
872
902
|
}
|
|
873
903
|
const byteLength = new TextEncoder().encode(line).byteLength;
|
|
874
904
|
if (byteLength > maxBytes) {
|
|
875
|
-
|
|
905
|
+
continue;
|
|
876
906
|
}
|
|
877
907
|
try {
|
|
878
908
|
yield JSON.parse(line);
|
|
879
909
|
} catch {
|
|
880
|
-
|
|
910
|
+
continue;
|
|
881
911
|
}
|
|
882
912
|
}
|
|
883
913
|
}
|
|
@@ -1027,11 +1057,15 @@ class ClaudeCodeProvider {
|
|
|
1027
1057
|
const files = collectJsonlFiles(this.baseDir);
|
|
1028
1058
|
const allRecords = [];
|
|
1029
1059
|
for (const file of files) {
|
|
1030
|
-
|
|
1031
|
-
const
|
|
1032
|
-
|
|
1033
|
-
|
|
1060
|
+
try {
|
|
1061
|
+
for await (const record of splitJsonlRecords(file)) {
|
|
1062
|
+
const usage = extractUsage(record);
|
|
1063
|
+
if (usage !== null && isInRange(usage.date, range)) {
|
|
1064
|
+
allRecords.push(usage);
|
|
1065
|
+
}
|
|
1034
1066
|
}
|
|
1067
|
+
} catch {
|
|
1068
|
+
continue;
|
|
1035
1069
|
}
|
|
1036
1070
|
}
|
|
1037
1071
|
const daily = buildDailyUsage(allRecords);
|
|
@@ -1120,41 +1154,45 @@ class CodexProvider {
|
|
|
1120
1154
|
}
|
|
1121
1155
|
for (const file of files) {
|
|
1122
1156
|
const filePath = join2(this.sessionsDir, file);
|
|
1123
|
-
|
|
1124
|
-
const
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
dailyMap.
|
|
1157
|
+
try {
|
|
1158
|
+
for await (const record of splitJsonlRecords(filePath)) {
|
|
1159
|
+
const event = parseResponseEvent(record);
|
|
1160
|
+
if (!event) {
|
|
1161
|
+
continue;
|
|
1162
|
+
}
|
|
1163
|
+
const date = extractDate(event.timestamp);
|
|
1164
|
+
if (!date || !isInRange2(date, range)) {
|
|
1165
|
+
continue;
|
|
1166
|
+
}
|
|
1167
|
+
const normalizedModel = normalizeModelName(compactModelDateSuffix(event.model));
|
|
1168
|
+
const inputTokens = event.usage.input_tokens;
|
|
1169
|
+
const outputTokens = event.usage.output_tokens;
|
|
1170
|
+
const cacheReadTokens = 0;
|
|
1171
|
+
const cacheWriteTokens = 0;
|
|
1172
|
+
const cost = estimateCost(normalizedModel, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens);
|
|
1173
|
+
if (!dailyMap.has(date)) {
|
|
1174
|
+
dailyMap.set(date, new Map);
|
|
1175
|
+
}
|
|
1176
|
+
const modelMap = dailyMap.get(date);
|
|
1177
|
+
if (!modelMap.has(normalizedModel)) {
|
|
1178
|
+
modelMap.set(normalizedModel, {
|
|
1179
|
+
model: normalizedModel,
|
|
1180
|
+
inputTokens: 0,
|
|
1181
|
+
outputTokens: 0,
|
|
1182
|
+
cacheReadTokens: 0,
|
|
1183
|
+
cacheWriteTokens: 0,
|
|
1184
|
+
totalTokens: 0,
|
|
1185
|
+
cost: 0
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
const breakdown = modelMap.get(normalizedModel);
|
|
1189
|
+
breakdown.inputTokens += inputTokens;
|
|
1190
|
+
breakdown.outputTokens += outputTokens;
|
|
1191
|
+
breakdown.totalTokens += inputTokens + outputTokens;
|
|
1192
|
+
breakdown.cost += cost;
|
|
1140
1193
|
}
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
modelMap.set(normalizedModel, {
|
|
1144
|
-
model: normalizedModel,
|
|
1145
|
-
inputTokens: 0,
|
|
1146
|
-
outputTokens: 0,
|
|
1147
|
-
cacheReadTokens: 0,
|
|
1148
|
-
cacheWriteTokens: 0,
|
|
1149
|
-
totalTokens: 0,
|
|
1150
|
-
cost: 0
|
|
1151
|
-
});
|
|
1152
|
-
}
|
|
1153
|
-
const breakdown = modelMap.get(normalizedModel);
|
|
1154
|
-
breakdown.inputTokens += inputTokens;
|
|
1155
|
-
breakdown.outputTokens += outputTokens;
|
|
1156
|
-
breakdown.totalTokens += inputTokens + outputTokens;
|
|
1157
|
-
breakdown.cost += cost;
|
|
1194
|
+
} catch {
|
|
1195
|
+
continue;
|
|
1158
1196
|
}
|
|
1159
1197
|
}
|
|
1160
1198
|
const daily = [...dailyMap.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([date, modelMap]) => {
|
|
@@ -1613,9 +1651,10 @@ function buildInsights(stats, providers) {
|
|
|
1613
1651
|
if (stats.topModels.length > 0) {
|
|
1614
1652
|
const top = stats.topModels[0];
|
|
1615
1653
|
if (top) {
|
|
1654
|
+
const pct = top.percentage < 1 ? top.percentage * 100 : top.percentage;
|
|
1616
1655
|
items.push({
|
|
1617
1656
|
label: "Top Model",
|
|
1618
|
-
value: `${top.model} (${
|
|
1657
|
+
value: `${top.model} (${pct.toFixed(1)}%)`
|
|
1619
1658
|
});
|
|
1620
1659
|
}
|
|
1621
1660
|
}
|
|
@@ -1710,7 +1749,8 @@ function renderModelChart(topModels2, theme) {
|
|
|
1710
1749
|
if (barWidth > 0) {
|
|
1711
1750
|
children.push(rect(BAR_LABEL_WIDTH, y, barWidth, BAR_HEIGHT, theme.accentSecondary, 3));
|
|
1712
1751
|
}
|
|
1713
|
-
const
|
|
1752
|
+
const pct = entry.percentage < 1 ? entry.percentage * 100 : entry.percentage;
|
|
1753
|
+
const valueStr = `${formatNumber(entry.tokens)} (${pct.toFixed(1)}%)`;
|
|
1714
1754
|
children.push(text(BAR_LABEL_WIDTH + barAreaWidth + 8, y + BAR_HEIGHT - 4, valueStr, {
|
|
1715
1755
|
fill: theme.foreground,
|
|
1716
1756
|
"font-size": FONT_SIZE_SMALL,
|
|
@@ -1723,13 +1763,15 @@ function renderModelChart(topModels2, theme) {
|
|
|
1723
1763
|
}
|
|
1724
1764
|
|
|
1725
1765
|
// packages/renderers/dist/svg/svg-renderer.js
|
|
1766
|
+
var MIN_SVG_WIDTH = 520;
|
|
1767
|
+
|
|
1726
1768
|
class SvgRenderer {
|
|
1727
1769
|
format = "svg";
|
|
1728
1770
|
async render(output, options) {
|
|
1729
1771
|
const theme = getTheme(options.theme);
|
|
1730
|
-
const contentWidth = options.width - PADDING * 2;
|
|
1731
1772
|
let y = PADDING;
|
|
1732
1773
|
const sections = [];
|
|
1774
|
+
const sectionWidths = [];
|
|
1733
1775
|
sections.push(group([
|
|
1734
1776
|
text(PADDING, y + FONT_SIZE_TITLE + 4, "Tokenleak", {
|
|
1735
1777
|
fill: theme.foreground,
|
|
@@ -1767,6 +1809,7 @@ class SvgRenderer {
|
|
|
1767
1809
|
endDate: output.dateRange.until
|
|
1768
1810
|
});
|
|
1769
1811
|
sections.push(group([heatmap.svg], `translate(${PADDING}, ${y})`));
|
|
1812
|
+
sectionWidths.push(heatmap.width);
|
|
1770
1813
|
y += heatmap.height + SECTION_GAP;
|
|
1771
1814
|
}
|
|
1772
1815
|
sections.push(text(PADDING, y, "Statistics", {
|
|
@@ -1778,6 +1821,7 @@ class SvgRenderer {
|
|
|
1778
1821
|
y += 16;
|
|
1779
1822
|
const stats = renderStatsPanel(output.aggregated, theme);
|
|
1780
1823
|
sections.push(group([stats.svg], `translate(${PADDING}, ${y})`));
|
|
1824
|
+
sectionWidths.push(stats.width);
|
|
1781
1825
|
y += stats.height + SECTION_GAP;
|
|
1782
1826
|
if (output.aggregated.dayOfWeek.length > 0) {
|
|
1783
1827
|
sections.push(text(PADDING, y, "Day of Week", {
|
|
@@ -1789,6 +1833,7 @@ class SvgRenderer {
|
|
|
1789
1833
|
y += 16;
|
|
1790
1834
|
const dowChart = renderDayOfWeekChart(output.aggregated.dayOfWeek, theme);
|
|
1791
1835
|
sections.push(group([dowChart.svg], `translate(${PADDING}, ${y})`));
|
|
1836
|
+
sectionWidths.push(dowChart.width);
|
|
1792
1837
|
y += dowChart.height + SECTION_GAP;
|
|
1793
1838
|
}
|
|
1794
1839
|
if (output.aggregated.topModels.length > 0) {
|
|
@@ -1801,6 +1846,7 @@ class SvgRenderer {
|
|
|
1801
1846
|
y += 16;
|
|
1802
1847
|
const modelChart = renderModelChart(output.aggregated.topModels, theme);
|
|
1803
1848
|
sections.push(group([modelChart.svg], `translate(${PADDING}, ${y})`));
|
|
1849
|
+
sectionWidths.push(modelChart.width);
|
|
1804
1850
|
y += modelChart.height + SECTION_GAP;
|
|
1805
1851
|
}
|
|
1806
1852
|
if (options.showInsights) {
|
|
@@ -1813,12 +1859,15 @@ class SvgRenderer {
|
|
|
1813
1859
|
y += 16;
|
|
1814
1860
|
const insights = renderInsightsPanel(output.aggregated, output.providers, theme);
|
|
1815
1861
|
sections.push(group([insights.svg], `translate(${PADDING}, ${y})`));
|
|
1862
|
+
sectionWidths.push(insights.width);
|
|
1816
1863
|
y += insights.height + SECTION_GAP;
|
|
1817
1864
|
}
|
|
1818
1865
|
const totalHeight = y + PADDING;
|
|
1866
|
+
const maxContentWidth = sectionWidths.length > 0 ? Math.max(...sectionWidths) : MIN_SVG_WIDTH - PADDING * 2;
|
|
1867
|
+
const svgWidth = Math.max(maxContentWidth + PADDING * 2, MIN_SVG_WIDTH);
|
|
1819
1868
|
const svgContent = [
|
|
1820
|
-
`<svg xmlns="http://www.w3.org/2000/svg" width="${
|
|
1821
|
-
rect(0, 0,
|
|
1869
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="${svgWidth}" height="${totalHeight}" viewBox="0 0 ${svgWidth} ${totalHeight}">`,
|
|
1870
|
+
rect(0, 0, svgWidth, totalHeight, theme.background, 8),
|
|
1822
1871
|
...sections,
|
|
1823
1872
|
"</svg>"
|
|
1824
1873
|
].join(`
|
|
@@ -1849,6 +1898,340 @@ var CODES = {
|
|
|
1849
1898
|
dim: `${ESC}2m`,
|
|
1850
1899
|
reset: `${ESC}0m`
|
|
1851
1900
|
};
|
|
1901
|
+
function colorize(text2, color, noColor2) {
|
|
1902
|
+
if (noColor2) {
|
|
1903
|
+
return text2;
|
|
1904
|
+
}
|
|
1905
|
+
return `${CODES[color]}${text2}${CODES.reset}`;
|
|
1906
|
+
}
|
|
1907
|
+
var HEATMAP_BLOCKS = {
|
|
1908
|
+
FULL: "\u2588",
|
|
1909
|
+
DARK: "\u2593",
|
|
1910
|
+
MEDIUM: "\u2592",
|
|
1911
|
+
LIGHT: "\u2591",
|
|
1912
|
+
EMPTY: " "
|
|
1913
|
+
};
|
|
1914
|
+
function intensityBlock(value, max) {
|
|
1915
|
+
if (max <= 0 || value <= 0)
|
|
1916
|
+
return HEATMAP_BLOCKS.EMPTY;
|
|
1917
|
+
const ratio = value / max;
|
|
1918
|
+
if (ratio >= 0.75)
|
|
1919
|
+
return HEATMAP_BLOCKS.FULL;
|
|
1920
|
+
if (ratio >= 0.5)
|
|
1921
|
+
return HEATMAP_BLOCKS.DARK;
|
|
1922
|
+
if (ratio >= 0.25)
|
|
1923
|
+
return HEATMAP_BLOCKS.MEDIUM;
|
|
1924
|
+
return HEATMAP_BLOCKS.LIGHT;
|
|
1925
|
+
}
|
|
1926
|
+
function intensityColor(value, max) {
|
|
1927
|
+
if (max <= 0 || value <= 0)
|
|
1928
|
+
return "dim";
|
|
1929
|
+
const ratio = value / max;
|
|
1930
|
+
if (ratio >= 0.75)
|
|
1931
|
+
return "green";
|
|
1932
|
+
if (ratio >= 0.5)
|
|
1933
|
+
return "yellow";
|
|
1934
|
+
if (ratio >= 0.25)
|
|
1935
|
+
return "cyan";
|
|
1936
|
+
return "dim";
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
// packages/renderers/dist/terminal/heatmap.js
|
|
1940
|
+
var DAY_LABELS3 = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
1941
|
+
var MONTH_LABELS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
1942
|
+
var DAY_LABEL_WIDTH2 = 4;
|
|
1943
|
+
var LEGEND_TEXT = "Less";
|
|
1944
|
+
var LEGEND_TEXT_MORE = "More";
|
|
1945
|
+
function buildUsageMap(daily) {
|
|
1946
|
+
const map = new Map;
|
|
1947
|
+
for (const entry of daily) {
|
|
1948
|
+
map.set(entry.date, (map.get(entry.date) ?? 0) + entry.totalTokens);
|
|
1949
|
+
}
|
|
1950
|
+
return map;
|
|
1951
|
+
}
|
|
1952
|
+
function renderTerminalHeatmap(daily, options) {
|
|
1953
|
+
if (daily.length === 0) {
|
|
1954
|
+
return " No usage data available.";
|
|
1955
|
+
}
|
|
1956
|
+
const usageMap = buildUsageMap(daily);
|
|
1957
|
+
const maxTokens = Math.max(...usageMap.values(), 0);
|
|
1958
|
+
const dates = daily.map((d) => d.date).sort();
|
|
1959
|
+
const startDate = new Date(dates[0]);
|
|
1960
|
+
const endDate = new Date(dates[dates.length - 1]);
|
|
1961
|
+
const alignedStart = new Date(startDate);
|
|
1962
|
+
alignedStart.setDate(alignedStart.getDate() - alignedStart.getDay());
|
|
1963
|
+
const weeks = [];
|
|
1964
|
+
const current = new Date(alignedStart);
|
|
1965
|
+
while (current <= endDate) {
|
|
1966
|
+
const week = [];
|
|
1967
|
+
for (let d = 0;d < 7; d++) {
|
|
1968
|
+
week.push(new Date(current));
|
|
1969
|
+
current.setDate(current.getDate() + 1);
|
|
1970
|
+
}
|
|
1971
|
+
weeks.push(week);
|
|
1972
|
+
}
|
|
1973
|
+
const availableWidth = options.width - DAY_LABEL_WIDTH2;
|
|
1974
|
+
const maxWeeks = Math.min(weeks.length, availableWidth);
|
|
1975
|
+
const displayWeeks = weeks.slice(Math.max(0, weeks.length - maxWeeks));
|
|
1976
|
+
const lines = [];
|
|
1977
|
+
let monthHeader = " ".repeat(DAY_LABEL_WIDTH2);
|
|
1978
|
+
let lastMonth = -1;
|
|
1979
|
+
for (const week of displayWeeks) {
|
|
1980
|
+
const month = week[0].getMonth();
|
|
1981
|
+
if (month !== lastMonth) {
|
|
1982
|
+
monthHeader += MONTH_LABELS[month];
|
|
1983
|
+
lastMonth = month;
|
|
1984
|
+
const labelLen = MONTH_LABELS[month].length;
|
|
1985
|
+
} else {
|
|
1986
|
+
monthHeader += " ";
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
if (monthHeader.length > options.width) {
|
|
1990
|
+
monthHeader = monthHeader.slice(0, options.width);
|
|
1991
|
+
}
|
|
1992
|
+
lines.push(monthHeader);
|
|
1993
|
+
for (let dayIdx = 0;dayIdx < 7; dayIdx++) {
|
|
1994
|
+
const label = dayIdx % 2 === 1 ? DAY_LABELS3[dayIdx] : " ";
|
|
1995
|
+
let line = label + " ";
|
|
1996
|
+
line = line.slice(0, DAY_LABEL_WIDTH2);
|
|
1997
|
+
for (const week of displayWeeks) {
|
|
1998
|
+
const date = week[dayIdx];
|
|
1999
|
+
if (!date || date > endDate || date < startDate) {
|
|
2000
|
+
line += " ";
|
|
2001
|
+
continue;
|
|
2002
|
+
}
|
|
2003
|
+
const dateStr = formatDate(date);
|
|
2004
|
+
const tokens = usageMap.get(dateStr) ?? 0;
|
|
2005
|
+
const block = intensityBlock(tokens, maxTokens);
|
|
2006
|
+
const color = intensityColor(tokens, maxTokens);
|
|
2007
|
+
line += colorize(block, color, options.noColor);
|
|
2008
|
+
}
|
|
2009
|
+
lines.push(line);
|
|
2010
|
+
}
|
|
2011
|
+
const legendBlocks = [
|
|
2012
|
+
HEATMAP_BLOCKS.EMPTY,
|
|
2013
|
+
HEATMAP_BLOCKS.LIGHT,
|
|
2014
|
+
HEATMAP_BLOCKS.MEDIUM,
|
|
2015
|
+
HEATMAP_BLOCKS.DARK,
|
|
2016
|
+
HEATMAP_BLOCKS.FULL
|
|
2017
|
+
];
|
|
2018
|
+
const legend = `${" ".repeat(DAY_LABEL_WIDTH2)}${LEGEND_TEXT} ${legendBlocks.join("")} ${LEGEND_TEXT_MORE}`;
|
|
2019
|
+
lines.push(legend);
|
|
2020
|
+
return lines.join(`
|
|
2021
|
+
`);
|
|
2022
|
+
}
|
|
2023
|
+
function formatDate(date) {
|
|
2024
|
+
const y = date.getFullYear();
|
|
2025
|
+
const m = String(date.getMonth() + 1).padStart(2, "0");
|
|
2026
|
+
const d = String(date.getDate()).padStart(2, "0");
|
|
2027
|
+
return `${y}-${m}-${d}`;
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
// packages/renderers/dist/terminal/dashboard.js
|
|
2031
|
+
var BOX_H = "\u2500";
|
|
2032
|
+
var BOX_V = "\u2502";
|
|
2033
|
+
var BOX_TL = "\u250C";
|
|
2034
|
+
var BOX_TR = "\u2510";
|
|
2035
|
+
var BOX_BL = "\u2514";
|
|
2036
|
+
var BOX_BR = "\u2518";
|
|
2037
|
+
var DAY_NAMES2 = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
2038
|
+
var BAR_CHAR = "\u2588";
|
|
2039
|
+
var MAX_BAR_LENGTH = 20;
|
|
2040
|
+
function formatTokens(count) {
|
|
2041
|
+
if (count >= 1e6) {
|
|
2042
|
+
return `${(count / 1e6).toFixed(1)}M`;
|
|
2043
|
+
}
|
|
2044
|
+
if (count >= 1000) {
|
|
2045
|
+
return `${(count / 1000).toFixed(0)}K`;
|
|
2046
|
+
}
|
|
2047
|
+
return String(count);
|
|
2048
|
+
}
|
|
2049
|
+
function formatCost2(cost) {
|
|
2050
|
+
return `$${cost.toFixed(2)}`;
|
|
2051
|
+
}
|
|
2052
|
+
function formatPercent2(rate) {
|
|
2053
|
+
return `${(rate * 100).toFixed(1)}%`;
|
|
2054
|
+
}
|
|
2055
|
+
function divider(width) {
|
|
2056
|
+
return BOX_H.repeat(width);
|
|
2057
|
+
}
|
|
2058
|
+
function boxedHeader(title, width, noColor2) {
|
|
2059
|
+
const inner = width - 2;
|
|
2060
|
+
const padded = ` ${title} `;
|
|
2061
|
+
const remaining = Math.max(0, inner - padded.length);
|
|
2062
|
+
const left = Math.floor(remaining / 2);
|
|
2063
|
+
const right = remaining - left;
|
|
2064
|
+
const content = `${BOX_H.repeat(left)}${padded}${BOX_H.repeat(right)}`;
|
|
2065
|
+
const top = `${BOX_TL}${BOX_H.repeat(inner)}${BOX_TR}`;
|
|
2066
|
+
const headerLine = `${BOX_V}${colorize(content, "bold", noColor2)}${BOX_V}`;
|
|
2067
|
+
const bottom = `${BOX_BL}${BOX_H.repeat(inner)}${BOX_BR}`;
|
|
2068
|
+
return [top, headerLine, bottom].join(`
|
|
2069
|
+
`);
|
|
2070
|
+
}
|
|
2071
|
+
function dayBar(tokens, maxTokens, noColor2) {
|
|
2072
|
+
if (maxTokens <= 0)
|
|
2073
|
+
return "";
|
|
2074
|
+
const length = Math.round(tokens / maxTokens * MAX_BAR_LENGTH);
|
|
2075
|
+
const bar = BAR_CHAR.repeat(length);
|
|
2076
|
+
return colorize(bar, "green", noColor2);
|
|
2077
|
+
}
|
|
2078
|
+
function renderStats(stats, width, noColor2) {
|
|
2079
|
+
const lines = [];
|
|
2080
|
+
const labelWidth = 20;
|
|
2081
|
+
const entries = [
|
|
2082
|
+
["Current Streak", `${stats.currentStreak}d`],
|
|
2083
|
+
["Longest Streak", `${stats.longestStreak}d`],
|
|
2084
|
+
["Total Tokens", formatTokens(stats.totalTokens)],
|
|
2085
|
+
["Total Cost", formatCost2(stats.totalCost)],
|
|
2086
|
+
["30d Tokens", formatTokens(stats.rolling30dTokens)],
|
|
2087
|
+
["30d Cost", formatCost2(stats.rolling30dCost)],
|
|
2088
|
+
["7d Tokens", formatTokens(stats.rolling7dTokens)],
|
|
2089
|
+
["7d Cost", formatCost2(stats.rolling7dCost)],
|
|
2090
|
+
["Avg Daily Tokens", formatTokens(stats.averageDailyTokens)],
|
|
2091
|
+
["Avg Daily Cost", formatCost2(stats.averageDailyCost)],
|
|
2092
|
+
["Cache Hit Rate", formatPercent2(stats.cacheHitRate)],
|
|
2093
|
+
["Active Days", `${stats.activeDays} / ${stats.totalDays}`]
|
|
2094
|
+
];
|
|
2095
|
+
if (stats.peakDay) {
|
|
2096
|
+
entries.push(["Peak Day", `${stats.peakDay.date} (${formatTokens(stats.peakDay.tokens)})`]);
|
|
2097
|
+
}
|
|
2098
|
+
for (const [label, value] of entries) {
|
|
2099
|
+
const line = ` ${label.padEnd(labelWidth)} ${colorize(value, "cyan", noColor2)}`;
|
|
2100
|
+
lines.push(line.length > width ? line.slice(0, width) : line);
|
|
2101
|
+
}
|
|
2102
|
+
return lines.join(`
|
|
2103
|
+
`);
|
|
2104
|
+
}
|
|
2105
|
+
function renderDayOfWeek(stats, width, noColor2) {
|
|
2106
|
+
const lines = [];
|
|
2107
|
+
const maxTokens = Math.max(...stats.dayOfWeek.map((d) => d.tokens), 0);
|
|
2108
|
+
for (const entry of stats.dayOfWeek) {
|
|
2109
|
+
const label = DAY_NAMES2[entry.day] ?? `Day${entry.day}`;
|
|
2110
|
+
const bar = dayBar(entry.tokens, maxTokens, noColor2);
|
|
2111
|
+
const tokenStr = formatTokens(entry.tokens);
|
|
2112
|
+
const line = ` ${label} ${bar} ${tokenStr}`;
|
|
2113
|
+
lines.push(line.length > width ? line.slice(0, width) : line);
|
|
2114
|
+
}
|
|
2115
|
+
return lines.join(`
|
|
2116
|
+
`);
|
|
2117
|
+
}
|
|
2118
|
+
function renderTopModels(stats, width, noColor2) {
|
|
2119
|
+
const lines = [];
|
|
2120
|
+
for (const model of stats.topModels.slice(0, 5)) {
|
|
2121
|
+
const pct = formatPercent2(model.percentage);
|
|
2122
|
+
const tokens = formatTokens(model.tokens);
|
|
2123
|
+
const line = ` ${colorize(model.model, "yellow", noColor2)} ${tokens} ${pct}`;
|
|
2124
|
+
lines.push(line.length > width ? line.slice(0, width) : line);
|
|
2125
|
+
}
|
|
2126
|
+
return lines.join(`
|
|
2127
|
+
`);
|
|
2128
|
+
}
|
|
2129
|
+
function renderInsights(stats, noColor2) {
|
|
2130
|
+
const insights = [];
|
|
2131
|
+
if (stats.currentStreak > 7) {
|
|
2132
|
+
insights.push(`You have a ${stats.currentStreak}-day coding streak going!`);
|
|
2133
|
+
}
|
|
2134
|
+
if (stats.cacheHitRate > 0.5) {
|
|
2135
|
+
insights.push(`Cache hit rate is ${formatPercent2(stats.cacheHitRate)} - good cache reuse.`);
|
|
2136
|
+
}
|
|
2137
|
+
if (stats.cacheHitRate < 0.1 && stats.totalTokens > 0) {
|
|
2138
|
+
insights.push("Cache hit rate is low - consider enabling prompt caching.");
|
|
2139
|
+
}
|
|
2140
|
+
if (stats.peakDay) {
|
|
2141
|
+
insights.push(`Peak usage was on ${stats.peakDay.date} with ${formatTokens(stats.peakDay.tokens)} tokens.`);
|
|
2142
|
+
}
|
|
2143
|
+
if (insights.length === 0)
|
|
2144
|
+
return "";
|
|
2145
|
+
return insights.map((i) => ` ${colorize("*", "green", noColor2)} ${i}`).join(`
|
|
2146
|
+
`);
|
|
2147
|
+
}
|
|
2148
|
+
function renderProviderSection(provider, stats, width, noColor2, showInsights) {
|
|
2149
|
+
const sections = [];
|
|
2150
|
+
sections.push(boxedHeader(provider.displayName, width, noColor2));
|
|
2151
|
+
sections.push("");
|
|
2152
|
+
sections.push(colorize(" Heatmap", "bold", noColor2));
|
|
2153
|
+
sections.push(renderTerminalHeatmap(provider.daily, { width, noColor: noColor2 }));
|
|
2154
|
+
sections.push("");
|
|
2155
|
+
sections.push(colorize(" Stats", "bold", noColor2));
|
|
2156
|
+
sections.push(renderStats(stats, width, noColor2));
|
|
2157
|
+
if (stats.dayOfWeek.length > 0) {
|
|
2158
|
+
sections.push("");
|
|
2159
|
+
sections.push(colorize(" Day of Week", "bold", noColor2));
|
|
2160
|
+
sections.push(renderDayOfWeek(stats, width, noColor2));
|
|
2161
|
+
}
|
|
2162
|
+
if (stats.topModels.length > 0) {
|
|
2163
|
+
sections.push("");
|
|
2164
|
+
sections.push(colorize(" Top Models", "bold", noColor2));
|
|
2165
|
+
sections.push(renderTopModels(stats, width, noColor2));
|
|
2166
|
+
}
|
|
2167
|
+
if (showInsights) {
|
|
2168
|
+
const insightsText = renderInsights(stats, noColor2);
|
|
2169
|
+
if (insightsText) {
|
|
2170
|
+
sections.push("");
|
|
2171
|
+
sections.push(colorize(" Insights", "bold", noColor2));
|
|
2172
|
+
sections.push(insightsText);
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
return sections.join(`
|
|
2176
|
+
`);
|
|
2177
|
+
}
|
|
2178
|
+
function renderDashboard(output, options) {
|
|
2179
|
+
const width = options.width;
|
|
2180
|
+
const noColor2 = options.noColor;
|
|
2181
|
+
const sections = [];
|
|
2182
|
+
sections.push(boxedHeader("Tokenleak", width, noColor2));
|
|
2183
|
+
sections.push("");
|
|
2184
|
+
if (output.providers.length === 0) {
|
|
2185
|
+
sections.push(" No provider data available.");
|
|
2186
|
+
return sections.join(`
|
|
2187
|
+
`);
|
|
2188
|
+
}
|
|
2189
|
+
for (let i = 0;i < output.providers.length; i++) {
|
|
2190
|
+
const provider = output.providers[i];
|
|
2191
|
+
sections.push(renderProviderSection(provider, output.aggregated, width, noColor2, options.showInsights));
|
|
2192
|
+
if (i < output.providers.length - 1) {
|
|
2193
|
+
sections.push("");
|
|
2194
|
+
sections.push(divider(width));
|
|
2195
|
+
sections.push("");
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
if (output.providers.length > 1) {
|
|
2199
|
+
sections.push("");
|
|
2200
|
+
sections.push(divider(width));
|
|
2201
|
+
sections.push("");
|
|
2202
|
+
sections.push(boxedHeader("Overall", width, noColor2));
|
|
2203
|
+
sections.push("");
|
|
2204
|
+
sections.push(renderStats(output.aggregated, width, noColor2));
|
|
2205
|
+
}
|
|
2206
|
+
return sections.join(`
|
|
2207
|
+
`);
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
// packages/renderers/dist/terminal/oneliner.js
|
|
2211
|
+
function renderOneliner(output, _options) {
|
|
2212
|
+
const streak = output.aggregated.currentStreak;
|
|
2213
|
+
const tokens = formatTokens(output.aggregated.totalTokens);
|
|
2214
|
+
const cost = formatCost2(output.aggregated.totalCost);
|
|
2215
|
+
const providerCount = output.providers.length;
|
|
2216
|
+
return `\uD83D\uDD25 ${streak}d streak | ${tokens} tokens | ${cost} | ${providerCount} provider${providerCount !== 1 ? "s" : ""}`;
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
// packages/renderers/dist/terminal/terminal-renderer.js
|
|
2220
|
+
var MIN_DASHBOARD_WIDTH = 40;
|
|
2221
|
+
|
|
2222
|
+
class TerminalRenderer {
|
|
2223
|
+
format = "terminal";
|
|
2224
|
+
async render(output, options) {
|
|
2225
|
+
const effectiveOptions = {
|
|
2226
|
+
...options,
|
|
2227
|
+
noColor: options.noColor
|
|
2228
|
+
};
|
|
2229
|
+
if (effectiveOptions.width < MIN_DASHBOARD_WIDTH) {
|
|
2230
|
+
return renderOneliner(output, effectiveOptions);
|
|
2231
|
+
}
|
|
2232
|
+
return renderDashboard(output, effectiveOptions);
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
1852
2235
|
// packages/cli/src/config.ts
|
|
1853
2236
|
import { readFileSync as readFileSync2 } from "fs";
|
|
1854
2237
|
import { join as join4 } from "path";
|
|
@@ -2129,8 +2512,14 @@ function getRenderer(format) {
|
|
|
2129
2512
|
switch (format) {
|
|
2130
2513
|
case "json":
|
|
2131
2514
|
return new JsonRenderer;
|
|
2515
|
+
case "svg":
|
|
2516
|
+
return new SvgRenderer;
|
|
2517
|
+
case "terminal":
|
|
2518
|
+
return new TerminalRenderer;
|
|
2519
|
+
case "png":
|
|
2520
|
+
return new PngRenderer;
|
|
2132
2521
|
default:
|
|
2133
|
-
throw new TokenleakError(`Format "${format}" is not
|
|
2522
|
+
throw new TokenleakError(`Format "${format}" is not supported. Available formats: json, svg, png, terminal`);
|
|
2134
2523
|
}
|
|
2135
2524
|
}
|
|
2136
2525
|
async function loadAndAggregate(range, providers) {
|