tokmon 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +55 -32
  2. package/dist/cli.js +201 -140
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,30 +1,33 @@
1
1
  # tokmon
2
2
 
3
- Terminal dashboard for Claude Code usage and costs. Tabbed interface with auto-refresh.
3
+ Terminal dashboard for Claude Code usage, costs, and rate limits.
4
4
 
5
5
  Built with [Ink](https://github.com/vadimdemedes/ink), TypeScript.
6
6
 
7
7
  ```
8
- ◉ tokmon · 2s 01:17:09 AM
8
+ ◉ tokmon · 2s 02:48:39 AM
9
9
 
10
- Dashboard Daily ←→ or 1-2
10
+ Dashboard Table Tab/←→
11
11
 
12
12
  ┃ Claude
13
13
 
14
- ┃ Today $166.10 252.7M tokens
15
- ┃ This Week $399.79 608.2M tokens
16
- ┃ This Month $1337.03 2.2B tokens
17
-
18
- ┃ Active Block 12m remaining
14
+ ┃ Today $372.55 614.9M tokens
15
+ ┃ This Week $606.23 970.5M tokens
16
+ ┃ This Month $1543.48 2.5B tokens
19
17
 
20
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━─ 96%
18
+ Burn rate $132.65/hr
19
+
20
+ ┃ Rate Limits
21
21
 
22
- $314.37 spent · ~$328.01 proj · $65.60/hr
22
+ Session ━━━━────────────────────────── 13% resets 4h 11m
23
+ ┃ Weekly ━━━━━━━━━━━━━───────────────── 44% resets 17h 11m
24
+ ┃ Sonnet ━───────────────────────────── 3% resets 2d 20h
25
+ ┃ Extra $0.00 / $42.50 limit
23
26
 
24
27
  ──────────────────────────────────────────────────
25
- Total $1337.03
28
+ Total $1543.48
26
29
 
27
- by David Ilie (davidilie.com)
30
+ by David Ilie (davidilie.com) · s=settings q=quit
28
31
  ```
29
32
 
30
33
  ## Quick Start
@@ -45,7 +48,7 @@ pnpm dlx tokmon
45
48
  npm install -g tokmon
46
49
  ```
47
50
 
48
- Then just run `tokmon`. Press `Ctrl+C` to exit.
51
+ Then just run `tokmon`. Press `q` to quit.
49
52
 
50
53
  ## Options
51
54
 
@@ -54,45 +57,65 @@ Then just run `tokmon`. Press `Ctrl+C` to exit.
54
57
  -h, --help Show help
55
58
  ```
56
59
 
57
- ```bash
58
- tokmon -i 5 # refresh every 5 seconds
59
- ```
60
+ ## Keybindings
60
61
 
61
- ## Views
62
+ | Key | Action |
63
+ |-----|--------|
64
+ | `Tab` / `←→` | Switch between Dashboard and Table |
65
+ | `1` `2` | Jump to view |
66
+ | `d` `w` `m` | Daily / Weekly / Monthly (in Table view) |
67
+ | `↑` `↓` | Scroll table |
68
+ | `PgUp` `PgDn` | Scroll table fast |
69
+ | `s` | Settings |
70
+ | `q` | Quit |
62
71
 
63
- Navigate between views with `←` `→` arrow keys, `Tab`, or number keys `1` `2`.
72
+ ## Views
64
73
 
65
74
  | View | Description |
66
75
  |------|-------------|
67
- | **Dashboard** | Today / week / month cost summaries, active 5-hour block with burn rate |
68
- | **Daily** | Per-day breakdown table with model, token, and cost columns (scrollable with `↑` `↓`) |
76
+ | **Dashboard** | Today / week / month cost summaries, burn rate ($/hr), real-time rate limits with reset countdowns |
77
+ | **Table → Daily** | Per-day breakdown with models, tokens, and costs (6 months of history) |
78
+ | **Table → Weekly** | Grouped by ISO week |
79
+ | **Table → Monthly** | Grouped by month |
80
+
81
+ ## Rate Limits
82
+
83
+ Fetches real billing data from Anthropic's OAuth API (reads your token from macOS Keychain automatically). Shows:
84
+
85
+ - **Session** — 5-hour utilization with reset countdown
86
+ - **Weekly** — 7-day utilization with reset countdown
87
+ - **Sonnet** — Sonnet-specific limits (if applicable)
88
+ - **Extra usage** — spend vs monthly limit
89
+
90
+ Polls every 2 minutes to stay within API rate limits.
91
+
92
+ ## Settings
93
+
94
+ Press `s` to open settings. Persisted to `~/.config/tokmon/config.json` (macOS/Linux) or `%APPDATA%\tokmon\config.json` (Windows).
95
+
96
+ - **Refresh interval** — adjust with `←→`
97
+ - **Clear screen** — on/off toggle
69
98
 
70
99
  ## How It Works
71
100
 
72
- Reads Claude Code's JSONL session logs directly from `~/.claude/projects/`. Calculates costs using Claude model pricing (Opus, Sonnet, Haiku). Caches file reads by mtime so subsequent refreshes are near-instant.
101
+ Reads Claude Code's JSONL session logs directly from `~/.claude/projects/`. Calculates costs using Claude model pricing (Opus, Sonnet, Haiku). Caches file reads by mtime so refreshes are near-instant.
102
+
103
+ Dashboard loads current month only (fast). Table loads 6 months lazily on first switch.
73
104
 
74
105
  Cross-platform: supports macOS, Linux, and Windows (`%APPDATA%`, `XDG_CONFIG_HOME`, `CLAUDE_CONFIG_DIR`).
75
106
 
76
107
  ## CI/CD
77
108
 
78
- Publishes to npm automatically via GitHub Actions when a version tag is pushed:
109
+ Publishes to npm via GitHub Actions on version tags:
79
110
 
80
111
  ```bash
81
- git tag v0.2.0 && git push --tags
112
+ git tag v0.5.0 && git push --tags
82
113
  ```
83
114
 
84
115
  ## Requirements
85
116
 
86
117
  - Node.js 20+
87
- - [Claude Code](https://docs.anthropic.com/en/docs/claude-code) (generates usage data in `~/.claude/projects/`)
88
-
89
- ## Tech Stack
90
-
91
- | Tool | Purpose |
92
- |------|---------|
93
- | Ink 5 | React terminal UI |
94
- | TypeScript 5.7+ | Strict mode |
95
- | tsup | Build |
118
+ - [Claude Code](https://docs.anthropic.com/en/docs/claude-code)
96
119
 
97
120
  ## Author
98
121
 
package/dist/cli.js CHANGED
@@ -46,41 +46,6 @@ import { createReadStream } from "fs";
46
46
  import { createInterface } from "readline";
47
47
  import { join as join2 } from "path";
48
48
  import { homedir as homedir2 } from "os";
49
-
50
- // src/format.ts
51
- function currency(value) {
52
- return `$${value.toFixed(2)}`;
53
- }
54
- function tokens(value) {
55
- if (value >= 1e9) return `${(value / 1e9).toFixed(1)}B`;
56
- if (value >= 1e6) return `${(value / 1e6).toFixed(1)}M`;
57
- if (value >= 1e3) return `${(value / 1e3).toFixed(1)}K`;
58
- return String(value);
59
- }
60
- function time(date) {
61
- return date.toLocaleTimeString(void 0, {
62
- hour: "2-digit",
63
- minute: "2-digit",
64
- second: "2-digit"
65
- });
66
- }
67
- function minutes(mins) {
68
- const h = Math.floor(mins / 60);
69
- const m = Math.round(mins % 60);
70
- return h > 0 ? `${h}h ${m}m` : `${m}m`;
71
- }
72
- function shortDate(iso) {
73
- const [, m, d] = iso.split("-");
74
- const months = ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
75
- return `${months[Number(m)]} ${Number(d).toString().padStart(2, " ")}`;
76
- }
77
- function col(s, w, align = "right") {
78
- if (s.length > w) return s.slice(0, w - 1) + "~";
79
- const spaces = " ".repeat(w - s.length);
80
- return align === "right" ? spaces + s : s + spaces;
81
- }
82
-
83
- // src/data.ts
84
49
  var PRICING = {
85
50
  "claude-opus-4": { i: 5e-6, o: 25e-6, cc: 625e-8, cr: 5e-7 },
86
51
  "claude-sonnet-4": { i: 3e-6, o: 15e-6, cc: 375e-8, cr: 3e-7 },
@@ -227,47 +192,26 @@ function isoWeekLabel(ts) {
227
192
  function monthLabel(ts) {
228
193
  return new Date(ts).toISOString().slice(0, 7);
229
194
  }
230
- function findBlockStart(entries, now) {
231
- const recent = entries.filter((e) => e.ts <= now).sort((a, b) => a.ts - b.ts);
232
- if (recent.length === 0) return 0;
233
- const GAP = 30 * 6e4;
234
- let blockStart = recent[0].ts;
235
- for (let i = 1; i < recent.length; i++) {
236
- if (recent[i].ts - recent[i - 1].ts > GAP) {
237
- blockStart = recent[i].ts;
238
- }
239
- }
240
- if (now - blockStart > 5 * 36e5) return 0;
241
- return blockStart;
242
- }
243
195
  async function fetchDashboard() {
244
- const now = Date.now();
245
196
  const d = /* @__PURE__ */ new Date();
246
197
  const monthStart = new Date(d.getFullYear(), d.getMonth(), 1).getTime();
247
198
  const todayStart = new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
248
199
  const weekDay = d.getDay();
249
200
  const weekStart = new Date(d.getFullYear(), d.getMonth(), d.getDate() - (weekDay === 0 ? 6 : weekDay - 1)).getTime();
201
+ const now = Date.now();
250
202
  const entries = await loadEntries(monthStart);
251
- let block = null;
252
- const blockStart = findBlockStart(entries, now);
253
- if (blockStart > 0) {
254
- const blockEnd = blockStart + 5 * 36e5;
255
- const blockEntries = entries.filter((e) => e.ts >= blockStart && e.ts < blockEnd);
256
- if (blockEntries.length > 0) {
257
- const spent = blockEntries.reduce((s, e) => s + e.cost, 0);
258
- const elapsedMs = now - blockStart;
259
- const elapsedHrs = elapsedMs / 36e5;
260
- const burnRate = elapsedHrs > 0 ? spent / elapsedHrs : 0;
261
- const remainMs = Math.max(0, blockEnd - now);
262
- const percent = Math.min(100, elapsedMs / (5 * 36e5) * 100);
263
- block = { spent, projected: burnRate * 5, burnRate, percent, remaining: minutes(remainMs / 6e4) };
264
- }
203
+ const todayEntries = entries.filter((e) => e.ts >= todayStart);
204
+ let burnRate = 0;
205
+ if (todayEntries.length > 0) {
206
+ const oldest = Math.min(...todayEntries.map((e) => e.ts));
207
+ const hrs = (now - oldest) / 36e5;
208
+ if (hrs > 0) burnRate = todayEntries.reduce((s, e) => s + e.cost, 0) / hrs;
265
209
  }
266
210
  return {
267
- today: sum(entries.filter((e) => e.ts >= todayStart)),
211
+ today: sum(todayEntries),
268
212
  week: sum(entries.filter((e) => e.ts >= weekStart)),
269
213
  month: sum(entries.filter((e) => e.ts >= monthStart)),
270
- block
214
+ burnRate
271
215
  };
272
216
  }
273
217
  async function fetchTable() {
@@ -281,12 +225,113 @@ async function fetchTable() {
281
225
  };
282
226
  }
283
227
 
228
+ // src/billing.ts
229
+ import { execFile as execFileCb } from "child_process";
230
+ import { promisify } from "util";
231
+ var execFile = promisify(execFileCb);
232
+ async function getAccessToken() {
233
+ if (process.platform === "darwin") {
234
+ try {
235
+ const { stdout } = await execFile("security", [
236
+ "find-generic-password",
237
+ "-s",
238
+ "Claude Code-credentials",
239
+ "-w"
240
+ ], { timeout: 5e3 });
241
+ const creds = JSON.parse(stdout.trim());
242
+ return creds?.claudeAiOauth?.accessToken ?? null;
243
+ } catch {
244
+ return null;
245
+ }
246
+ }
247
+ return null;
248
+ }
249
+ async function fetchBilling() {
250
+ const token = await getAccessToken();
251
+ if (!token) return null;
252
+ try {
253
+ const res = await fetch("https://api.anthropic.com/api/oauth/usage", {
254
+ headers: {
255
+ "Authorization": `Bearer ${token}`,
256
+ "anthropic-beta": "oauth-2025-04-20",
257
+ "User-Agent": "tokmon"
258
+ },
259
+ signal: AbortSignal.timeout(1e4)
260
+ });
261
+ if (!res.ok) return null;
262
+ const data = await res.json();
263
+ return {
264
+ session: data.five_hour ? {
265
+ utilization: data.five_hour.utilization,
266
+ resetsAt: formatReset(data.five_hour.resets_at)
267
+ } : null,
268
+ weekly: data.seven_day ? {
269
+ utilization: data.seven_day.utilization,
270
+ resetsAt: formatReset(data.seven_day.resets_at)
271
+ } : null,
272
+ sonnet: data.seven_day_sonnet ? {
273
+ utilization: data.seven_day_sonnet.utilization,
274
+ resetsAt: formatReset(data.seven_day_sonnet.resets_at)
275
+ } : null,
276
+ extraUsage: data.extra_usage?.is_enabled ? {
277
+ limit: data.extra_usage.monthly_limit / 100,
278
+ used: data.extra_usage.used_credits / 100
279
+ } : null
280
+ };
281
+ } catch {
282
+ return null;
283
+ }
284
+ }
285
+ function formatReset(iso) {
286
+ const d = new Date(iso);
287
+ const now = /* @__PURE__ */ new Date();
288
+ const diff = d.getTime() - now.getTime();
289
+ if (diff <= 0) return "now";
290
+ const mins = Math.round(diff / 6e4);
291
+ if (mins < 60) return `${mins}m`;
292
+ const hrs = Math.floor(mins / 60);
293
+ const m = mins % 60;
294
+ if (hrs < 24) return `${hrs}h ${m}m`;
295
+ const days = Math.floor(hrs / 24);
296
+ const h = hrs % 24;
297
+ return `${days}d ${h}h`;
298
+ }
299
+
300
+ // src/format.ts
301
+ function currency(value) {
302
+ return `$${value.toFixed(2)}`;
303
+ }
304
+ function tokens(value) {
305
+ if (value >= 1e9) return `${(value / 1e9).toFixed(1)}B`;
306
+ if (value >= 1e6) return `${(value / 1e6).toFixed(1)}M`;
307
+ if (value >= 1e3) return `${(value / 1e3).toFixed(1)}K`;
308
+ return String(value);
309
+ }
310
+ function time(date) {
311
+ return date.toLocaleTimeString(void 0, {
312
+ hour: "2-digit",
313
+ minute: "2-digit",
314
+ second: "2-digit"
315
+ });
316
+ }
317
+ function shortDate(iso) {
318
+ const [, m, d] = iso.split("-");
319
+ const months = ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
320
+ return `${months[Number(m)]} ${Number(d).toString().padStart(2, " ")}`;
321
+ }
322
+ function col(s, w, align = "right") {
323
+ if (s.length > w) return s.slice(0, w - 1) + "~";
324
+ const spaces = " ".repeat(w - s.length);
325
+ return align === "right" ? spaces + s : s + spaces;
326
+ }
327
+
284
328
  // src/app.tsx
285
329
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
286
330
  var TABS = ["Dashboard", "Table"];
287
331
  var VIEWS = ["Daily", "Weekly", "Monthly"];
288
332
  function App({ interval: cliInterval }) {
289
333
  const [dashboard, setDashboard] = useState(null);
334
+ const [billing, setBilling] = useState(null);
290
335
  const [table, setTable] = useState(null);
291
336
  const [tableLoading, setTableLoading] = useState(false);
292
337
  const [error, setError] = useState(null);
@@ -299,9 +344,11 @@ function App({ interval: cliInterval }) {
299
344
  const [settingsCursor, setSettingsCursor] = useState(0);
300
345
  const tableLoadedOnce = useRef(false);
301
346
  const { stdout } = useStdout();
347
+ const { exit } = useApp();
302
348
  const rows = stdout?.rows ?? 24;
303
349
  const cols = stdout?.columns ?? 80;
304
350
  const interval2 = cliInterval ?? (config2?.interval ?? 2) * 1e3;
351
+ const cfg = config2 ?? { interval: 2, clearScreen: true };
305
352
  useEffect(() => {
306
353
  loadConfig().then((c) => {
307
354
  if (cliInterval) c = { ...c, interval: cliInterval / 1e3 };
@@ -329,6 +376,19 @@ function App({ interval: cliInterval }) {
329
376
  clearInterval(id);
330
377
  };
331
378
  }, [interval2]);
379
+ useEffect(() => {
380
+ let active = true;
381
+ const load = () => fetchBilling().then((b) => {
382
+ if (active && b) setBilling(b);
383
+ }).catch(() => {
384
+ });
385
+ load();
386
+ const id = setInterval(load, 12e4);
387
+ return () => {
388
+ active = false;
389
+ clearInterval(id);
390
+ };
391
+ }, []);
332
392
  useEffect(() => {
333
393
  if (tab !== 1) return;
334
394
  if (tableLoadedOnce.current && table) return;
@@ -362,36 +422,29 @@ function App({ interval: cliInterval }) {
362
422
  clearInterval(id);
363
423
  };
364
424
  }, [tab, interval2]);
365
- const { exit } = useApp();
366
425
  const isTTY = process.stdin.isTTY === true;
367
- const settingsItems = 2;
368
- const cfg = config2 ?? { interval: 2, clearScreen: true };
369
426
  useInput((input, key) => {
370
427
  if (showSettings) {
371
428
  if (key.escape || input === "s") setShowSettings(false);
372
429
  if (key.upArrow) setSettingsCursor((c) => Math.max(0, c - 1));
373
- if (key.downArrow) setSettingsCursor((c) => Math.min(settingsItems - 1, c + 1));
430
+ if (key.downArrow) setSettingsCursor((c) => Math.min(1, c + 1));
374
431
  if (settingsCursor === 0) {
375
- if (key.leftArrow) {
376
- setConfig((c) => {
377
- const next = { ...c, interval: Math.max(1, c.interval - 1) };
378
- saveConfig(next);
379
- return next;
380
- });
381
- }
382
- if (key.rightArrow) {
383
- setConfig((c) => {
384
- const next = { ...c, interval: c.interval + 1 };
385
- saveConfig(next);
386
- return next;
387
- });
388
- }
432
+ if (key.leftArrow) setConfig((c) => {
433
+ const n = { ...c, interval: Math.max(1, c.interval - 1) };
434
+ saveConfig(n);
435
+ return n;
436
+ });
437
+ if (key.rightArrow) setConfig((c) => {
438
+ const n = { ...c, interval: c.interval + 1 };
439
+ saveConfig(n);
440
+ return n;
441
+ });
389
442
  }
390
443
  if (settingsCursor === 1 && (key.leftArrow || key.rightArrow || key.return)) {
391
444
  setConfig((c) => {
392
- const next = { ...c, clearScreen: !c.clearScreen };
393
- saveConfig(next);
394
- return next;
445
+ const n = { ...c, clearScreen: !c.clearScreen };
446
+ saveConfig(n);
447
+ return n;
395
448
  });
396
449
  }
397
450
  return;
@@ -481,7 +534,7 @@ function App({ interval: cliInterval }) {
481
534
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: " Tab/\u2190\u2192" })
482
535
  ] }),
483
536
  /* @__PURE__ */ jsx(Box, { height: 1 }),
484
- tab === 0 && /* @__PURE__ */ jsx(DashboardView, { data: dashboard }),
537
+ tab === 0 && /* @__PURE__ */ jsx(DashboardView, { data: dashboard, billing }),
485
538
  tab === 1 && /* @__PURE__ */ jsxs(Fragment, { children: [
486
539
  /* @__PURE__ */ jsx(ViewBar, { views: VIEWS, active: view }),
487
540
  /* @__PURE__ */ jsx(Box, { height: 1 }),
@@ -493,8 +546,7 @@ function App({ interval: cliInterval }) {
493
546
  /* @__PURE__ */ jsx(Text, { children: "David Ilie" }),
494
547
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: " (" }),
495
548
  /* @__PURE__ */ jsx(Text, { color: "cyan", children: "davidilie.com" }),
496
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: ") \xB7 " }),
497
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "s=settings q=quit" })
549
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: ") \xB7 s=settings q=quit" })
498
550
  ] })
499
551
  ] });
500
552
  }
@@ -555,7 +607,7 @@ function SettingsView({ config: config2, cursor }) {
555
607
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2191\u2193 select \u2190\u2192 adjust s/Esc close" })
556
608
  ] });
557
609
  }
558
- function DashboardView({ data }) {
610
+ function DashboardView({ data, billing }) {
559
611
  return /* @__PURE__ */ jsxs(Fragment, { children: [
560
612
  /* @__PURE__ */ jsxs(
561
613
  Box,
@@ -572,13 +624,51 @@ function DashboardView({ data }) {
572
624
  /* @__PURE__ */ jsx(Box, { height: 1 }),
573
625
  /* @__PURE__ */ jsx(SummaryRow, { label: "Today", summary: data.today }),
574
626
  /* @__PURE__ */ jsx(SummaryRow, { label: "This Week", summary: data.week }),
575
- /* @__PURE__ */ jsx(SummaryRow, { label: "This Month", summary: data.month })
627
+ /* @__PURE__ */ jsx(SummaryRow, { label: "This Month", summary: data.month }),
628
+ data.burnRate > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
629
+ /* @__PURE__ */ jsx(Box, { height: 1 }),
630
+ /* @__PURE__ */ jsxs(Box, { children: [
631
+ /* @__PURE__ */ jsx(Box, { width: 14, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Burn rate" }) }),
632
+ /* @__PURE__ */ jsx(Box, { width: 12, justifyContent: "flex-end", children: /* @__PURE__ */ jsx(Text, { color: "red", children: currency(data.burnRate) }) }),
633
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "/hr" })
634
+ ] })
635
+ ] })
576
636
  ]
577
637
  }
578
638
  ),
579
- data.block && /* @__PURE__ */ jsxs(Fragment, { children: [
639
+ billing && /* @__PURE__ */ jsxs(Fragment, { children: [
580
640
  /* @__PURE__ */ jsx(Box, { height: 1 }),
581
- /* @__PURE__ */ jsx(BlockView, { block: data.block })
641
+ /* @__PURE__ */ jsxs(
642
+ Box,
643
+ {
644
+ flexDirection: "column",
645
+ paddingLeft: 1,
646
+ borderStyle: "bold",
647
+ borderColor: "yellow",
648
+ borderRight: false,
649
+ borderTop: false,
650
+ borderBottom: false,
651
+ children: [
652
+ /* @__PURE__ */ jsx(Text, { bold: true, children: "Rate Limits" }),
653
+ /* @__PURE__ */ jsx(Box, { height: 1 }),
654
+ billing.session && /* @__PURE__ */ jsx(LimitBar, { label: "Session", pct: billing.session.utilization, resets: billing.session.resetsAt }),
655
+ billing.weekly && /* @__PURE__ */ jsx(LimitBar, { label: "Weekly", pct: billing.weekly.utilization, resets: billing.weekly.resetsAt }),
656
+ billing.sonnet && /* @__PURE__ */ jsx(LimitBar, { label: "Sonnet", pct: billing.sonnet.utilization, resets: billing.sonnet.resetsAt }),
657
+ billing.extraUsage && /* @__PURE__ */ jsxs(Box, { children: [
658
+ /* @__PURE__ */ jsx(Box, { width: 10, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Extra" }) }),
659
+ /* @__PURE__ */ jsxs(Text, { color: "yellow", children: [
660
+ "$",
661
+ billing.extraUsage.used.toFixed(2)
662
+ ] }),
663
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
664
+ " / $",
665
+ billing.extraUsage.limit.toFixed(2),
666
+ " limit"
667
+ ] })
668
+ ] })
669
+ ]
670
+ }
671
+ )
582
672
  ] }),
583
673
  /* @__PURE__ */ jsx(Box, { height: 1 }),
584
674
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(50) }),
@@ -588,46 +678,24 @@ function DashboardView({ data }) {
588
678
  ] })
589
679
  ] });
590
680
  }
591
- function BlockView({ block }) {
592
- return /* @__PURE__ */ jsxs(
593
- Box,
594
- {
595
- flexDirection: "column",
596
- paddingLeft: 1,
597
- borderStyle: "bold",
598
- borderColor: "yellow",
599
- borderRight: false,
600
- borderTop: false,
601
- borderBottom: false,
602
- children: [
603
- /* @__PURE__ */ jsxs(Box, { children: [
604
- /* @__PURE__ */ jsx(Text, { bold: true, children: "Active Block" }),
605
- /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
606
- " ",
607
- block.remaining,
608
- " remaining"
609
- ] })
610
- ] }),
611
- /* @__PURE__ */ jsx(Box, { height: 1 }),
612
- /* @__PURE__ */ jsxs(Box, { children: [
613
- /* @__PURE__ */ jsx(ProgressBar, { percent: block.percent, width: 36 }),
614
- /* @__PURE__ */ jsx(Text, { children: " " }),
615
- /* @__PURE__ */ jsxs(Text, { bold: true, children: [
616
- Math.round(block.percent),
617
- "%"
618
- ] })
619
- ] }),
620
- /* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
621
- /* @__PURE__ */ jsx(Text, { color: "yellow", children: currency(block.spent) }),
622
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " spent \xB7 ~" }),
623
- /* @__PURE__ */ jsx(Text, { children: currency(block.projected) }),
624
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " proj \xB7 " }),
625
- /* @__PURE__ */ jsx(Text, { color: "red", children: currency(block.burnRate) }),
626
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "/hr" })
627
- ] })
628
- ]
629
- }
630
- );
681
+ function LimitBar({ label, pct, resets }) {
682
+ const width = 30;
683
+ const filled = Math.round(pct / 100 * width);
684
+ const color = pct >= 80 ? "red" : pct >= 50 ? "yellow" : "green";
685
+ return /* @__PURE__ */ jsxs(Box, { children: [
686
+ /* @__PURE__ */ jsx(Box, { width: 10, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: label }) }),
687
+ /* @__PURE__ */ jsx(Text, { color, children: "\u2501".repeat(filled) }),
688
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(width - filled) }),
689
+ /* @__PURE__ */ jsx(Text, { children: " " }),
690
+ /* @__PURE__ */ jsxs(Text, { bold: true, children: [
691
+ Math.round(pct),
692
+ "%"
693
+ ] }),
694
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
695
+ " resets ",
696
+ resets
697
+ ] })
698
+ ] });
631
699
  }
632
700
  function SummaryRow({ label, summary }) {
633
701
  return /* @__PURE__ */ jsxs(Box, { children: [
@@ -639,13 +707,6 @@ function SummaryRow({ label, summary }) {
639
707
  ] }) })
640
708
  ] });
641
709
  }
642
- function ProgressBar({ percent, width = 36 }) {
643
- const filled = Math.round(percent / 100 * width);
644
- return /* @__PURE__ */ jsxs(Text, { children: [
645
- /* @__PURE__ */ jsx(Text, { color: "greenBright", children: "\u2501".repeat(filled) }),
646
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(width - filled) })
647
- ] });
648
- }
649
710
  function TableView({ rows: allRows, scroll, maxRows, wide }) {
650
711
  const W = wide ? { label: 10, models: 18, input: 8, output: 8, cc: 8, cr: 9, total: 9, cost: 10 } : { label: 8, models: 14, input: 7, output: 7, cc: 7, cr: 8, total: 0, cost: 9 };
651
712
  const lineW = W.label + W.models + W.input + W.output + W.cc + W.cr + W.total + W.cost;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokmon",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "Terminal dashboard for Claude Code usage and costs",
5
5
  "type": "module",
6
6
  "bin": {