tokens-metric 0.4.13 → 0.4.14

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 CHANGED
@@ -1,19 +1,53 @@
1
1
  # tokens-metric
2
2
 
3
- Real-time token usage meter for [Claude Code](https://claude.com/claude-code) — a terminal UI plus a one-line statusline you can wire into Claude Code itself.
3
+ Real-time token usage meter for [Claude Code](https://claude.com/claude-code) and [OpenAI Codex CLI](https://github.com/openai/codex) — a terminal UI plus a one-line statusline you can wire into Claude Code itself.
4
4
 
5
- It tails the transcripts Claude Code writes under `~/.claude/projects/**/*.jsonl`, aggregates the `usage` field of each message in-memory, and renders it live. No API calls, no telemetry.
5
+ It tails the transcripts both tools write to disk, aggregates usage in-memory, and renders it live. No API calls, no telemetry.
6
+
7
+ | Source | Transcript path |
8
+ |--------|----------------|
9
+ | Claude Code | `~/.claude/projects/**/*.jsonl` |
10
+ | Codex CLI | `~/.codex/sessions/YYYY/MM/DD/*.jsonl` |
6
11
 
7
12
  ## What it shows
8
13
 
9
- ### TUI (tab navigation)
14
+ ### Always-visible status bars
15
+
16
+ One bar per active source, always on screen regardless of which panel is open:
17
+
18
+ ```
19
+ ● sonnet-4-6 · 33.4M tok · ~$23.21 · 351 msgs · since 17:23 ● live 2s ago
20
+ ● codex · 236M tok · ~$165.35 · 896 msgs · since 18:19 ○ idle 3m ago
21
+ ```
22
+
23
+ ### [1] Breakdown
24
+
25
+ - Input / output / cache-write / cache-read bars with percentages and per-category cost
26
+ - Cache hit ratio with quality rating (excellent / degraded / poor)
27
+ - Context window fill gauge for the last turn
28
+ - Per-model totals when multiple models were used in a session
29
+ - 32-second activity sparkline with peak and avg rate
30
+ - **30-minute timeline chart** — vertical bar chart, one bucket per minute, stacked cyan (Claude) / magenta (Codex), oldest bars dimmed, current minute highlighted
31
+
32
+ ### [2] History
33
+
34
+ Today / 7-day / 30-day aggregate: tokens, estimated cost, session count. Dual bar chart (Claude vs Codex) for the last 7 days. Data persisted in `~/.tokens-metric/history.json` so totals survive transcript rotation.
35
+
36
+ ### [3] Sessions
37
+
38
+ Today's sessions sorted by start time. Navigate with `↑↓` — the selected session expands an inline detail panel showing per-category token bars, percentages, and cost breakdown.
39
+
40
+ ### [4] Transcripts
10
41
 
11
- - **Session status bar** always visible: model, total tokens, estimated cost, message count, time since session start, mini activity sparkline, live/idle indicator.
12
- - **[1] Breakdown** — input / output / cache-write / cache-read token bars with percentages and per-category cost estimates, cache hit ratio, per-model totals, and a 32-second activity sparkline with peak/avg rate.
13
- - **[2] History** — today / 7-day / 30-day aggregate: tokens, estimated cost, session count, top model. Data is persisted locally in `~/.tokens-metric/history.json` so historical totals survive transcript rotation.
14
- - **[3] Sessions** — today's sessions sorted by start time: project path, model, tokens, cost, duration, active indicator.
15
- - **[4] Transcripts** — last five transcript files with recency timestamps.
16
- - **Update notifier** checks npm once every 24 h (cached) and shows a banner when a newer version is available.
42
+ Last five transcript files with recency timestamps.
43
+
44
+ ### Header
45
+
46
+ ```
47
+ Claude Code detected · ● Codex detected · 4 sessions · 3 projects today
48
+ ```
49
+
50
+ Codex detection dot is green when `~/.codex/` exists, red when not installed.
17
51
 
18
52
  ### Statusline
19
53
 
@@ -31,7 +65,7 @@ Or run without installing:
31
65
  npx tokens-metric
32
66
  ```
33
67
 
34
- Requires **Node 18+** and an existing Claude Code installation.
68
+ Requires **Node 18+**. Claude Code and/or Codex CLI must be installed for data to appear.
35
69
 
36
70
  ## Usage
37
71
 
@@ -46,15 +80,16 @@ tokens-metric
46
80
  | `←` / `→` | Move cursor between tabs |
47
81
  | `Enter` | Open / collapse the focused tab |
48
82
  | `1` – `4` | Jump directly to a tab and open it |
83
+ | `↑` / `↓` | Navigate sessions (while Sessions tab is open) |
49
84
  | `Esc` | Collapse the open panel |
50
85
  | `q` / `Ctrl-C` | Quit |
51
86
 
52
87
  ### Privacy defaults
53
88
 
54
- Starting in v0.2.0 the TUI masks identifying information by default so screenshots can be shared safely:
89
+ Paths are masked by default so screenshots can be shared safely:
55
90
 
56
- - `cwd` paths are shown as `~/…` instead of `/Users/<you>/…`.
57
- - The user ID is masked to `●●●●●●●●`.
91
+ - `cwd` paths shown as `~/…` instead of `/Users/<you>/…`
92
+ - User IDs masked to `●●●●●●●●`
58
93
 
59
94
  Pass `--reveal` to show everything unmasked:
60
95
 
@@ -81,30 +116,41 @@ Output looks like:
81
116
  🏢 team opus-4-7 │ in 117 · out 43.5k · cache 3.21M │ Σ 3.25M · ~$12.29 API-eq
82
117
  ```
83
118
 
119
+ ## Pricing
120
+
121
+ Costs are estimated at **API-equivalent pricing** — a reference figure, not what you pay on Pro/Max/Team subscriptions.
122
+
123
+ | Provider | Models |
124
+ |----------|--------|
125
+ | Anthropic | claude-opus-4, claude-sonnet-4, claude-haiku-3.5, and prior generations |
126
+ | OpenAI | o4-mini, o3, o3-mini, gpt-4o, gpt-4o-mini (via Codex CLI) |
127
+
128
+ Prices are hardcoded in `src/core/format.ts`. Update the package when providers change their rates.
129
+
84
130
  ## Honest limitations
85
131
 
86
- 1. **Plan tier is heuristic.** We read flags Claude Code writes locally (e.g. `opusProMigrationComplete`). Anthropic can rename these at any release; the detector falls back to `unknown` instead of breaking.
87
- 2. **Pro vs. Max are not distinguishable locally.** Both look identical from the config file.
88
- 3. **The USD figure is API-equivalent pricing, not what you actually pay.** On Pro/Max/Team you pay a flat subscription the dollar number is purely a reference for what the same tokens would cost on the API.
89
- 4. **Prices are hardcoded.** See `src/core/format.ts`. If Anthropic updates pricing, the numbers drift until you bump the package.
90
- 5. **The transcript format is not a public API.** It works today; it may shift. The parser is intentionally tolerant of unexpected shapes.
132
+ 1. **Plan tier is heuristic.** We read flags Claude Code writes locally. Anthropic can rename these at any release; the detector falls back to `unknown`.
133
+ 2. **Pro vs. Max are not distinguishable locally.** Both look identical from config.
134
+ 3. **The transcript format is not a public API.** It works today; it may shift. The parser is intentionally tolerant of unexpected shapes.
135
+ 4. **Codex CLI model key is always `codex`.** The JSONL does not include a specific model name, so all Codex usage is priced at o4-mini rates.
91
136
 
92
137
  ## How it works
93
138
 
94
139
  ```
95
- ~/.claude/projects/<encoded-cwd>/<session-id>.jsonl
96
-
97
-
98
- src/core/parser.ts reads + aggregates usage per message
99
- src/core/tailer.ts watches the active file with fs.watch
100
- src/core/detect.ts reads ~/.claude.json for auth + plan hints
101
- src/core/history.ts per-day aggregation with mtime-based cache
102
- src/core/history-store.ts persists daily aggregates to ~/.tokens-metric/
103
- src/core/updater.ts npm version check, 24h cache
104
-
105
- ┌────────┴────────┐
106
- ▼ ▼
107
- src/tui (Ink) src/statusline
140
+ ~/.claude/projects/<encoded-cwd>/<session>.jsonl ~/.codex/sessions/YYYY/MM/DD/<session>.jsonl
141
+
142
+ └──────────────┬───────────────────────────┘
143
+
144
+ src/core/parser.ts reads + aggregates usage per message
145
+ src/core/tailer.ts watches active files with fs.watch
146
+ src/core/detect.ts auth, plan hints, Codex detection
147
+ src/core/history.ts per-day aggregation, mtime cache
148
+ src/core/history-store persists daily totals to ~/.tokens-metric/
149
+ src/core/updater.ts npm version check, 24h cache
150
+
151
+ ┌────────────┴────────────┐
152
+ ▼ ▼
153
+ src/tui (Ink) src/statusline
108
154
  ```
109
155
 
110
156
  ## Development
@@ -120,11 +166,7 @@ npm run build # builds dist/
120
166
 
121
167
  ## Roadmap
122
168
 
123
- - **Windows support** — Claude Code on Windows stores transcripts in a different location (`%APPDATA%\Claude\`). This requires abstracting `claudeHome()` in `detect.ts` to resolve the correct path per OS, and handling Windows path separators in project folder names.
124
-
125
- - **Multi-provider support (Codex, Gemini, etc.)** — The pricing table in `format.ts` currently covers Claude models only. Adding other providers means extending the table with `gpt-*`, `gemini-*`, `o1-*` prefixes, investigating whether those agents produce compatible `.jsonl` transcripts, and likely allowing the user to point to additional transcript folders.
126
-
127
- - **Context window usage** — Show how much of the active session's context window is consumed. Claude Code may log this in the transcript; if not, it can be inferred per model (e.g. claude-3.5-sonnet = 200k tokens). Would appear as a progress bar in the session status bar or breakdown panel.
169
+ - **Windows support** — Claude Code on Windows stores transcripts under `%APPDATA%\Claude\`. Requires abstracting `claudeHome()` in `detect.ts` and handling Windows path separators.
128
170
 
129
171
  ## License
130
172
 
package/dist/tui/index.js CHANGED
@@ -357,10 +357,12 @@ function BarRow({ label, value, max, total, color, cost, }) {
357
357
  return (_jsxs(Text, { children: [_jsx(Text, { bold: true, children: label }), _jsx(Text, { color: color, children: bar(value / max, BAR_WIDTH) }), _jsxs(Text, { children: [" ", fmtNumber(value).padStart(7, ' ')] }), _jsx(Text, { dimColor: true, children: ` ${pct.toFixed(1).padStart(5, ' ')}%` }), cost !== null && _jsx(Text, { dimColor: true, children: ` ~${fmtUSD(cost)}` })] }));
358
358
  }
359
359
  // ── 30-minute timeline chart ─────────────────────────────────────────────────
360
+ const PARTIAL_BLOCKS = [' ', '▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
360
361
  function TimelineChart({ claudeTimeline, codexTimeline, }) {
361
362
  const combined = claudeTimeline.map((c, i) => c + (codexTimeline[i] ?? 0));
362
- const max = Math.max(1, ...combined);
363
- const hasData = combined.some((v) => v > 0);
363
+ const maxRaw = Math.max(...combined);
364
+ const hasData = maxRaw > 0;
365
+ const max = hasData ? maxRaw : 1;
364
366
  const N = claudeTimeline.length;
365
367
  const fmtY = (v) => {
366
368
  if (v >= 1_000_000)
@@ -369,7 +371,10 @@ function TimelineChart({ claudeTimeline, codexTimeline, }) {
369
371
  return `${Math.round(v / 1_000)}k`;
370
372
  return String(Math.round(v));
371
373
  };
374
+ // Y axis labels: only when there is real data
372
375
  const yLabel = (r) => {
376
+ if (!hasData)
377
+ return ' ';
373
378
  if (r === TIMELINE_ROWS - 1)
374
379
  return fmtY(max).padStart(5);
375
380
  if (r === Math.floor(TIMELINE_ROWS / 2))
@@ -377,15 +382,26 @@ function TimelineChart({ claudeTimeline, codexTimeline, }) {
377
382
  return ' ';
378
383
  };
379
384
  return (_jsxs(Box, { borderStyle: "round", borderColor: "yellow", paddingX: 1, flexDirection: "column", children: [_jsxs(Text, { bold: true, color: "yellow", children: ['activity ', _jsx(Text, { bold: false, dimColor: true, children: "\u00B7 last 30m" })] }), Array.from({ length: TIMELINE_ROWS }, (_, i) => {
380
- const r = TIMELINE_ROWS - 1 - i;
381
- const threshold = (r / TIMELINE_ROWS) * max;
385
+ const r = TIMELINE_ROWS - 1 - i; // r=ROWS-1 at top, r=0 at bottom
382
386
  return (_jsxs(Text, { children: [_jsxs(Text, { dimColor: true, children: [yLabel(r), " \u2502"] }), combined.map((v, c) => {
383
- const filled = v > threshold;
384
- const isCodex = filled && (codexTimeline[c] ?? 0) > threshold;
385
- return (_jsx(Text, { color: filled ? (isCodex ? 'magenta' : 'cyan') : undefined, children: filled ? '█' : ' ' }, c));
387
+ // fractional bar heights in row units
388
+ const totalH = (v / max) * TIMELINE_ROWS;
389
+ const codexH = ((codexTimeline[c] ?? 0) / max) * TIMELINE_ROWS;
390
+ if (totalH <= r)
391
+ return _jsx(Text, { children: " " }, c);
392
+ // smooth top using partial block chars
393
+ const char = totalH >= r + 1
394
+ ? '█'
395
+ : (PARTIAL_BLOCKS[Math.max(1, Math.round((totalH - r) * 8))] ?? '█');
396
+ // stacked color: codex fills from bottom, claude on top
397
+ const isCodex = codexH > r + 0.5;
398
+ // oldest third dimmed, current minute (rightmost) bright
399
+ const isCurrentMinute = c === N - 1;
400
+ const isOld = c < Math.floor(N / 3);
401
+ return (_jsx(Text, { color: isCodex ? 'magenta' : 'cyan', dimColor: isOld && !isCurrentMinute, bold: isCurrentMinute, children: char }, c));
386
402
  })] }, r));
387
403
  }), _jsx(Text, { dimColor: true, children: ' └' + '─'.repeat(N) }), _jsx(Text, { dimColor: true, children: ` -30m${' '.repeat(Math.max(0, N - 7))}now` }), _jsx(Box, { marginTop: 1, children: hasData
388
- ? _jsxs(Text, { dimColor: true, children: ["peak ", _jsx(Text, { color: "white", children: fmtY(max) }), "/min \u00B7 ", _jsx(Text, { color: "cyan", children: "\u2588" }), " claude ", _jsx(Text, { color: "magenta", children: "\u2588" }), " codex"] })
404
+ ? _jsxs(Text, { dimColor: true, children: ['peak ', _jsx(Text, { color: "white", children: fmtY(max) }), '/min · ', _jsx(Text, { color: "cyan", children: '█' }), ' claude ', _jsx(Text, { color: "magenta", children: '█' }), ' codex'] })
389
405
  : _jsx(Text, { dimColor: true, children: "filling\u2026 (1 bar/min)" }) })] }));
390
406
  }
391
407
  // ── History panel ────────────────────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokens-metric",
3
- "version": "0.4.13",
3
+ "version": "0.4.14",
4
4
  "description": "Real-time token usage meter for Claude Code — statusline + Ink TUI.",
5
5
  "type": "module",
6
6
  "bin": {