tokenleak 0.1.0 → 0.2.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 +421 -43
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]) => {
|
|
@@ -1849,6 +1887,340 @@ var CODES = {
|
|
|
1849
1887
|
dim: `${ESC}2m`,
|
|
1850
1888
|
reset: `${ESC}0m`
|
|
1851
1889
|
};
|
|
1890
|
+
function colorize(text2, color, noColor2) {
|
|
1891
|
+
if (noColor2) {
|
|
1892
|
+
return text2;
|
|
1893
|
+
}
|
|
1894
|
+
return `${CODES[color]}${text2}${CODES.reset}`;
|
|
1895
|
+
}
|
|
1896
|
+
var HEATMAP_BLOCKS = {
|
|
1897
|
+
FULL: "\u2588",
|
|
1898
|
+
DARK: "\u2593",
|
|
1899
|
+
MEDIUM: "\u2592",
|
|
1900
|
+
LIGHT: "\u2591",
|
|
1901
|
+
EMPTY: " "
|
|
1902
|
+
};
|
|
1903
|
+
function intensityBlock(value, max) {
|
|
1904
|
+
if (max <= 0 || value <= 0)
|
|
1905
|
+
return HEATMAP_BLOCKS.EMPTY;
|
|
1906
|
+
const ratio = value / max;
|
|
1907
|
+
if (ratio >= 0.75)
|
|
1908
|
+
return HEATMAP_BLOCKS.FULL;
|
|
1909
|
+
if (ratio >= 0.5)
|
|
1910
|
+
return HEATMAP_BLOCKS.DARK;
|
|
1911
|
+
if (ratio >= 0.25)
|
|
1912
|
+
return HEATMAP_BLOCKS.MEDIUM;
|
|
1913
|
+
return HEATMAP_BLOCKS.LIGHT;
|
|
1914
|
+
}
|
|
1915
|
+
function intensityColor(value, max) {
|
|
1916
|
+
if (max <= 0 || value <= 0)
|
|
1917
|
+
return "dim";
|
|
1918
|
+
const ratio = value / max;
|
|
1919
|
+
if (ratio >= 0.75)
|
|
1920
|
+
return "green";
|
|
1921
|
+
if (ratio >= 0.5)
|
|
1922
|
+
return "yellow";
|
|
1923
|
+
if (ratio >= 0.25)
|
|
1924
|
+
return "cyan";
|
|
1925
|
+
return "dim";
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
// packages/renderers/dist/terminal/heatmap.js
|
|
1929
|
+
var DAY_LABELS3 = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
1930
|
+
var MONTH_LABELS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
1931
|
+
var DAY_LABEL_WIDTH2 = 4;
|
|
1932
|
+
var LEGEND_TEXT = "Less";
|
|
1933
|
+
var LEGEND_TEXT_MORE = "More";
|
|
1934
|
+
function buildUsageMap(daily) {
|
|
1935
|
+
const map = new Map;
|
|
1936
|
+
for (const entry of daily) {
|
|
1937
|
+
map.set(entry.date, (map.get(entry.date) ?? 0) + entry.totalTokens);
|
|
1938
|
+
}
|
|
1939
|
+
return map;
|
|
1940
|
+
}
|
|
1941
|
+
function renderTerminalHeatmap(daily, options) {
|
|
1942
|
+
if (daily.length === 0) {
|
|
1943
|
+
return " No usage data available.";
|
|
1944
|
+
}
|
|
1945
|
+
const usageMap = buildUsageMap(daily);
|
|
1946
|
+
const maxTokens = Math.max(...usageMap.values(), 0);
|
|
1947
|
+
const dates = daily.map((d) => d.date).sort();
|
|
1948
|
+
const startDate = new Date(dates[0]);
|
|
1949
|
+
const endDate = new Date(dates[dates.length - 1]);
|
|
1950
|
+
const alignedStart = new Date(startDate);
|
|
1951
|
+
alignedStart.setDate(alignedStart.getDate() - alignedStart.getDay());
|
|
1952
|
+
const weeks = [];
|
|
1953
|
+
const current = new Date(alignedStart);
|
|
1954
|
+
while (current <= endDate) {
|
|
1955
|
+
const week = [];
|
|
1956
|
+
for (let d = 0;d < 7; d++) {
|
|
1957
|
+
week.push(new Date(current));
|
|
1958
|
+
current.setDate(current.getDate() + 1);
|
|
1959
|
+
}
|
|
1960
|
+
weeks.push(week);
|
|
1961
|
+
}
|
|
1962
|
+
const availableWidth = options.width - DAY_LABEL_WIDTH2;
|
|
1963
|
+
const maxWeeks = Math.min(weeks.length, availableWidth);
|
|
1964
|
+
const displayWeeks = weeks.slice(Math.max(0, weeks.length - maxWeeks));
|
|
1965
|
+
const lines = [];
|
|
1966
|
+
let monthHeader = " ".repeat(DAY_LABEL_WIDTH2);
|
|
1967
|
+
let lastMonth = -1;
|
|
1968
|
+
for (const week of displayWeeks) {
|
|
1969
|
+
const month = week[0].getMonth();
|
|
1970
|
+
if (month !== lastMonth) {
|
|
1971
|
+
monthHeader += MONTH_LABELS[month];
|
|
1972
|
+
lastMonth = month;
|
|
1973
|
+
const labelLen = MONTH_LABELS[month].length;
|
|
1974
|
+
} else {
|
|
1975
|
+
monthHeader += " ";
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
if (monthHeader.length > options.width) {
|
|
1979
|
+
monthHeader = monthHeader.slice(0, options.width);
|
|
1980
|
+
}
|
|
1981
|
+
lines.push(monthHeader);
|
|
1982
|
+
for (let dayIdx = 0;dayIdx < 7; dayIdx++) {
|
|
1983
|
+
const label = dayIdx % 2 === 1 ? DAY_LABELS3[dayIdx] : " ";
|
|
1984
|
+
let line = label + " ";
|
|
1985
|
+
line = line.slice(0, DAY_LABEL_WIDTH2);
|
|
1986
|
+
for (const week of displayWeeks) {
|
|
1987
|
+
const date = week[dayIdx];
|
|
1988
|
+
if (!date || date > endDate || date < startDate) {
|
|
1989
|
+
line += " ";
|
|
1990
|
+
continue;
|
|
1991
|
+
}
|
|
1992
|
+
const dateStr = formatDate(date);
|
|
1993
|
+
const tokens = usageMap.get(dateStr) ?? 0;
|
|
1994
|
+
const block = intensityBlock(tokens, maxTokens);
|
|
1995
|
+
const color = intensityColor(tokens, maxTokens);
|
|
1996
|
+
line += colorize(block, color, options.noColor);
|
|
1997
|
+
}
|
|
1998
|
+
lines.push(line);
|
|
1999
|
+
}
|
|
2000
|
+
const legendBlocks = [
|
|
2001
|
+
HEATMAP_BLOCKS.EMPTY,
|
|
2002
|
+
HEATMAP_BLOCKS.LIGHT,
|
|
2003
|
+
HEATMAP_BLOCKS.MEDIUM,
|
|
2004
|
+
HEATMAP_BLOCKS.DARK,
|
|
2005
|
+
HEATMAP_BLOCKS.FULL
|
|
2006
|
+
];
|
|
2007
|
+
const legend = `${" ".repeat(DAY_LABEL_WIDTH2)}${LEGEND_TEXT} ${legendBlocks.join("")} ${LEGEND_TEXT_MORE}`;
|
|
2008
|
+
lines.push(legend);
|
|
2009
|
+
return lines.join(`
|
|
2010
|
+
`);
|
|
2011
|
+
}
|
|
2012
|
+
function formatDate(date) {
|
|
2013
|
+
const y = date.getFullYear();
|
|
2014
|
+
const m = String(date.getMonth() + 1).padStart(2, "0");
|
|
2015
|
+
const d = String(date.getDate()).padStart(2, "0");
|
|
2016
|
+
return `${y}-${m}-${d}`;
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
// packages/renderers/dist/terminal/dashboard.js
|
|
2020
|
+
var BOX_H = "\u2500";
|
|
2021
|
+
var BOX_V = "\u2502";
|
|
2022
|
+
var BOX_TL = "\u250C";
|
|
2023
|
+
var BOX_TR = "\u2510";
|
|
2024
|
+
var BOX_BL = "\u2514";
|
|
2025
|
+
var BOX_BR = "\u2518";
|
|
2026
|
+
var DAY_NAMES2 = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
2027
|
+
var BAR_CHAR = "\u2588";
|
|
2028
|
+
var MAX_BAR_LENGTH = 20;
|
|
2029
|
+
function formatTokens(count) {
|
|
2030
|
+
if (count >= 1e6) {
|
|
2031
|
+
return `${(count / 1e6).toFixed(1)}M`;
|
|
2032
|
+
}
|
|
2033
|
+
if (count >= 1000) {
|
|
2034
|
+
return `${(count / 1000).toFixed(0)}K`;
|
|
2035
|
+
}
|
|
2036
|
+
return String(count);
|
|
2037
|
+
}
|
|
2038
|
+
function formatCost2(cost) {
|
|
2039
|
+
return `$${cost.toFixed(2)}`;
|
|
2040
|
+
}
|
|
2041
|
+
function formatPercent2(rate) {
|
|
2042
|
+
return `${(rate * 100).toFixed(1)}%`;
|
|
2043
|
+
}
|
|
2044
|
+
function divider(width) {
|
|
2045
|
+
return BOX_H.repeat(width);
|
|
2046
|
+
}
|
|
2047
|
+
function boxedHeader(title, width, noColor2) {
|
|
2048
|
+
const inner = width - 2;
|
|
2049
|
+
const padded = ` ${title} `;
|
|
2050
|
+
const remaining = Math.max(0, inner - padded.length);
|
|
2051
|
+
const left = Math.floor(remaining / 2);
|
|
2052
|
+
const right = remaining - left;
|
|
2053
|
+
const content = `${BOX_H.repeat(left)}${padded}${BOX_H.repeat(right)}`;
|
|
2054
|
+
const top = `${BOX_TL}${BOX_H.repeat(inner)}${BOX_TR}`;
|
|
2055
|
+
const headerLine = `${BOX_V}${colorize(content, "bold", noColor2)}${BOX_V}`;
|
|
2056
|
+
const bottom = `${BOX_BL}${BOX_H.repeat(inner)}${BOX_BR}`;
|
|
2057
|
+
return [top, headerLine, bottom].join(`
|
|
2058
|
+
`);
|
|
2059
|
+
}
|
|
2060
|
+
function dayBar(tokens, maxTokens, noColor2) {
|
|
2061
|
+
if (maxTokens <= 0)
|
|
2062
|
+
return "";
|
|
2063
|
+
const length = Math.round(tokens / maxTokens * MAX_BAR_LENGTH);
|
|
2064
|
+
const bar = BAR_CHAR.repeat(length);
|
|
2065
|
+
return colorize(bar, "green", noColor2);
|
|
2066
|
+
}
|
|
2067
|
+
function renderStats(stats, width, noColor2) {
|
|
2068
|
+
const lines = [];
|
|
2069
|
+
const labelWidth = 20;
|
|
2070
|
+
const entries = [
|
|
2071
|
+
["Current Streak", `${stats.currentStreak}d`],
|
|
2072
|
+
["Longest Streak", `${stats.longestStreak}d`],
|
|
2073
|
+
["Total Tokens", formatTokens(stats.totalTokens)],
|
|
2074
|
+
["Total Cost", formatCost2(stats.totalCost)],
|
|
2075
|
+
["30d Tokens", formatTokens(stats.rolling30dTokens)],
|
|
2076
|
+
["30d Cost", formatCost2(stats.rolling30dCost)],
|
|
2077
|
+
["7d Tokens", formatTokens(stats.rolling7dTokens)],
|
|
2078
|
+
["7d Cost", formatCost2(stats.rolling7dCost)],
|
|
2079
|
+
["Avg Daily Tokens", formatTokens(stats.averageDailyTokens)],
|
|
2080
|
+
["Avg Daily Cost", formatCost2(stats.averageDailyCost)],
|
|
2081
|
+
["Cache Hit Rate", formatPercent2(stats.cacheHitRate)],
|
|
2082
|
+
["Active Days", `${stats.activeDays} / ${stats.totalDays}`]
|
|
2083
|
+
];
|
|
2084
|
+
if (stats.peakDay) {
|
|
2085
|
+
entries.push(["Peak Day", `${stats.peakDay.date} (${formatTokens(stats.peakDay.tokens)})`]);
|
|
2086
|
+
}
|
|
2087
|
+
for (const [label, value] of entries) {
|
|
2088
|
+
const line = ` ${label.padEnd(labelWidth)} ${colorize(value, "cyan", noColor2)}`;
|
|
2089
|
+
lines.push(line.length > width ? line.slice(0, width) : line);
|
|
2090
|
+
}
|
|
2091
|
+
return lines.join(`
|
|
2092
|
+
`);
|
|
2093
|
+
}
|
|
2094
|
+
function renderDayOfWeek(stats, width, noColor2) {
|
|
2095
|
+
const lines = [];
|
|
2096
|
+
const maxTokens = Math.max(...stats.dayOfWeek.map((d) => d.tokens), 0);
|
|
2097
|
+
for (const entry of stats.dayOfWeek) {
|
|
2098
|
+
const label = DAY_NAMES2[entry.day] ?? `Day${entry.day}`;
|
|
2099
|
+
const bar = dayBar(entry.tokens, maxTokens, noColor2);
|
|
2100
|
+
const tokenStr = formatTokens(entry.tokens);
|
|
2101
|
+
const line = ` ${label} ${bar} ${tokenStr}`;
|
|
2102
|
+
lines.push(line.length > width ? line.slice(0, width) : line);
|
|
2103
|
+
}
|
|
2104
|
+
return lines.join(`
|
|
2105
|
+
`);
|
|
2106
|
+
}
|
|
2107
|
+
function renderTopModels(stats, width, noColor2) {
|
|
2108
|
+
const lines = [];
|
|
2109
|
+
for (const model of stats.topModels.slice(0, 5)) {
|
|
2110
|
+
const pct = formatPercent2(model.percentage / 100);
|
|
2111
|
+
const tokens = formatTokens(model.tokens);
|
|
2112
|
+
const line = ` ${colorize(model.model, "yellow", noColor2)} ${tokens} ${pct}`;
|
|
2113
|
+
lines.push(line.length > width ? line.slice(0, width) : line);
|
|
2114
|
+
}
|
|
2115
|
+
return lines.join(`
|
|
2116
|
+
`);
|
|
2117
|
+
}
|
|
2118
|
+
function renderInsights(stats, noColor2) {
|
|
2119
|
+
const insights = [];
|
|
2120
|
+
if (stats.currentStreak > 7) {
|
|
2121
|
+
insights.push(`You have a ${stats.currentStreak}-day coding streak going!`);
|
|
2122
|
+
}
|
|
2123
|
+
if (stats.cacheHitRate > 0.5) {
|
|
2124
|
+
insights.push(`Cache hit rate is ${formatPercent2(stats.cacheHitRate)} - good cache reuse.`);
|
|
2125
|
+
}
|
|
2126
|
+
if (stats.cacheHitRate < 0.1 && stats.totalTokens > 0) {
|
|
2127
|
+
insights.push("Cache hit rate is low - consider enabling prompt caching.");
|
|
2128
|
+
}
|
|
2129
|
+
if (stats.peakDay) {
|
|
2130
|
+
insights.push(`Peak usage was on ${stats.peakDay.date} with ${formatTokens(stats.peakDay.tokens)} tokens.`);
|
|
2131
|
+
}
|
|
2132
|
+
if (insights.length === 0)
|
|
2133
|
+
return "";
|
|
2134
|
+
return insights.map((i) => ` ${colorize("*", "green", noColor2)} ${i}`).join(`
|
|
2135
|
+
`);
|
|
2136
|
+
}
|
|
2137
|
+
function renderProviderSection(provider, stats, width, noColor2, showInsights) {
|
|
2138
|
+
const sections = [];
|
|
2139
|
+
sections.push(boxedHeader(provider.displayName, width, noColor2));
|
|
2140
|
+
sections.push("");
|
|
2141
|
+
sections.push(colorize(" Heatmap", "bold", noColor2));
|
|
2142
|
+
sections.push(renderTerminalHeatmap(provider.daily, { width, noColor: noColor2 }));
|
|
2143
|
+
sections.push("");
|
|
2144
|
+
sections.push(colorize(" Stats", "bold", noColor2));
|
|
2145
|
+
sections.push(renderStats(stats, width, noColor2));
|
|
2146
|
+
if (stats.dayOfWeek.length > 0) {
|
|
2147
|
+
sections.push("");
|
|
2148
|
+
sections.push(colorize(" Day of Week", "bold", noColor2));
|
|
2149
|
+
sections.push(renderDayOfWeek(stats, width, noColor2));
|
|
2150
|
+
}
|
|
2151
|
+
if (stats.topModels.length > 0) {
|
|
2152
|
+
sections.push("");
|
|
2153
|
+
sections.push(colorize(" Top Models", "bold", noColor2));
|
|
2154
|
+
sections.push(renderTopModels(stats, width, noColor2));
|
|
2155
|
+
}
|
|
2156
|
+
if (showInsights) {
|
|
2157
|
+
const insightsText = renderInsights(stats, noColor2);
|
|
2158
|
+
if (insightsText) {
|
|
2159
|
+
sections.push("");
|
|
2160
|
+
sections.push(colorize(" Insights", "bold", noColor2));
|
|
2161
|
+
sections.push(insightsText);
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
return sections.join(`
|
|
2165
|
+
`);
|
|
2166
|
+
}
|
|
2167
|
+
function renderDashboard(output, options) {
|
|
2168
|
+
const width = options.width;
|
|
2169
|
+
const noColor2 = options.noColor;
|
|
2170
|
+
const sections = [];
|
|
2171
|
+
sections.push(boxedHeader("Tokenleak", width, noColor2));
|
|
2172
|
+
sections.push("");
|
|
2173
|
+
if (output.providers.length === 0) {
|
|
2174
|
+
sections.push(" No provider data available.");
|
|
2175
|
+
return sections.join(`
|
|
2176
|
+
`);
|
|
2177
|
+
}
|
|
2178
|
+
for (let i = 0;i < output.providers.length; i++) {
|
|
2179
|
+
const provider = output.providers[i];
|
|
2180
|
+
sections.push(renderProviderSection(provider, output.aggregated, width, noColor2, options.showInsights));
|
|
2181
|
+
if (i < output.providers.length - 1) {
|
|
2182
|
+
sections.push("");
|
|
2183
|
+
sections.push(divider(width));
|
|
2184
|
+
sections.push("");
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
if (output.providers.length > 1) {
|
|
2188
|
+
sections.push("");
|
|
2189
|
+
sections.push(divider(width));
|
|
2190
|
+
sections.push("");
|
|
2191
|
+
sections.push(boxedHeader("Overall", width, noColor2));
|
|
2192
|
+
sections.push("");
|
|
2193
|
+
sections.push(renderStats(output.aggregated, width, noColor2));
|
|
2194
|
+
}
|
|
2195
|
+
return sections.join(`
|
|
2196
|
+
`);
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
// packages/renderers/dist/terminal/oneliner.js
|
|
2200
|
+
function renderOneliner(output, _options) {
|
|
2201
|
+
const streak = output.aggregated.currentStreak;
|
|
2202
|
+
const tokens = formatTokens(output.aggregated.totalTokens);
|
|
2203
|
+
const cost = formatCost2(output.aggregated.totalCost);
|
|
2204
|
+
const providerCount = output.providers.length;
|
|
2205
|
+
return `\uD83D\uDD25 ${streak}d streak | ${tokens} tokens | ${cost} | ${providerCount} provider${providerCount !== 1 ? "s" : ""}`;
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
// packages/renderers/dist/terminal/terminal-renderer.js
|
|
2209
|
+
var MIN_DASHBOARD_WIDTH = 40;
|
|
2210
|
+
|
|
2211
|
+
class TerminalRenderer {
|
|
2212
|
+
format = "terminal";
|
|
2213
|
+
async render(output, options) {
|
|
2214
|
+
const effectiveOptions = {
|
|
2215
|
+
...options,
|
|
2216
|
+
noColor: options.noColor
|
|
2217
|
+
};
|
|
2218
|
+
if (effectiveOptions.width < MIN_DASHBOARD_WIDTH) {
|
|
2219
|
+
return renderOneliner(output, effectiveOptions);
|
|
2220
|
+
}
|
|
2221
|
+
return renderDashboard(output, effectiveOptions);
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
1852
2224
|
// packages/cli/src/config.ts
|
|
1853
2225
|
import { readFileSync as readFileSync2 } from "fs";
|
|
1854
2226
|
import { join as join4 } from "path";
|
|
@@ -2129,8 +2501,14 @@ function getRenderer(format) {
|
|
|
2129
2501
|
switch (format) {
|
|
2130
2502
|
case "json":
|
|
2131
2503
|
return new JsonRenderer;
|
|
2504
|
+
case "svg":
|
|
2505
|
+
return new SvgRenderer;
|
|
2506
|
+
case "terminal":
|
|
2507
|
+
return new TerminalRenderer;
|
|
2508
|
+
case "png":
|
|
2509
|
+
return new PngRenderer;
|
|
2132
2510
|
default:
|
|
2133
|
-
throw new TokenleakError(`Format "${format}" is not
|
|
2511
|
+
throw new TokenleakError(`Format "${format}" is not supported. Available formats: json, svg, png, terminal`);
|
|
2134
2512
|
}
|
|
2135
2513
|
}
|
|
2136
2514
|
async function loadAndAggregate(range, providers) {
|