tokmon 0.6.1 → 0.8.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.
Files changed (3) hide show
  1. package/README.md +57 -28
  2. package/dist/cli.js +158 -117
  3. package/package.json +3 -1
package/README.md CHANGED
@@ -35,57 +35,86 @@ Then just run `tokmon`. Press `q` to quit.
35
35
 
36
36
  ## Keybindings
37
37
 
38
+ ### Global
39
+
38
40
  | Key | Action |
39
41
  |-----|--------|
40
- | `Tab` / `←→` | Switch between Dashboard and Table |
41
- | `1` `2` | Jump to view |
42
- | `d` `w` `m` | Daily / Weekly / Monthly (in Table view) |
43
- | `↑` `↓` | Scroll table |
44
- | `PgUp` `PgDn` | Scroll table fast |
45
- | `s` | Settings |
42
+ | `Tab` | Cycle between Dashboard and Table |
43
+ | `1` `2` | Jump to Dashboard / Table |
44
+ | `s` | Open settings |
46
45
  | `q` | Quit |
47
46
 
47
+ ### Table View
48
+
49
+ | Key | Action |
50
+ |-----|--------|
51
+ | `d` `w` `m` | Switch to Daily / Weekly / Monthly |
52
+ | `←` `→` | Cycle sub-view |
53
+ | `↑` `↓` | Move cursor / navigate rows |
54
+ | `Enter` | Expand row — per-model cost breakdown |
55
+ | `Esc` | Collapse expanded row |
56
+ | `o` | Cycle sort: date ↑, date ↓, cost ↑, cost ↓ |
57
+ | `g` | Jump to top |
58
+ | `G` | Jump to bottom |
59
+ | `PgUp` `PgDn` | Page scroll |
60
+
61
+ ### Settings
62
+
63
+ | Key | Action |
64
+ |-----|--------|
65
+ | `↑` `↓` | Select option |
66
+ | `←` `→` | Adjust value |
67
+ | `s` / `Esc` | Close |
68
+
48
69
  ## Views
49
70
 
50
- | View | Description |
51
- |------|-------------|
52
- | **Dashboard** | Today / week / month cost summaries, burn rate ($/hr), real-time rate limits with reset countdowns |
53
- | **Table → Daily** | Per-day breakdown with models, tokens, and costs (6 months of history) |
54
- | **Table → Weekly** | Grouped by ISO week |
55
- | **Table → Monthly** | Grouped by month |
71
+ ### Dashboard
72
+
73
+ - **Today / This Week / This Month** cost and token summaries
74
+ - **Burn rate** current $/hr
75
+ - **Rate Limits** real-time session (5h), weekly (7d), and Sonnet utilization with reset countdowns, fetched from Anthropic's OAuth API
56
76
 
57
- ## Rate Limits
77
+ ### Table
58
78
 
59
- Fetches real billing data from Anthropic's OAuth API (reads your token from macOS Keychain automatically). Shows:
79
+ Interactive table with 3 sub-views:
60
80
 
61
- - **Session** — 5-hour utilization with reset countdown
62
- - **Weekly** — 7-day utilization with reset countdown
63
- - **Sonnet** — Sonnet-specific limits (if applicable)
64
- - **Extra usage** — spend vs monthly limit
81
+ - **Daily** — per-day breakdown (6 months of history)
82
+ - **Weekly** — grouped by ISO week
83
+ - **Monthly** — grouped by month
65
84
 
66
- Polls every 2 minutes to stay within API rate limits.
85
+ Each row shows models used, input/output/cache tokens, and cost. Press `Enter` on any row to expand a per-model breakdown:
86
+
87
+ ```
88
+ ▸ Apr 7 haiku-4-5, op~ 7.6K 487.0K 10.1M 1.1B $603.89
89
+ ├─ opus-4-6 7.5K 485.0K 10.0M 1.1B $601.50
90
+ └─ haiku-4-5 100 2.0K 100K 5.0M $2.39
91
+ ```
92
+
93
+ Sort by date or cost with `o`.
67
94
 
68
95
  ## Settings
69
96
 
70
- Press `s` to open settings. Persisted to `~/.config/tokmon/config.json` (macOS/Linux) or `%APPDATA%\tokmon\config.json` (Windows).
97
+ Press `s` to open. Persisted to `~/.config/tokmon/config.json` (macOS/Linux) or `%APPDATA%\tokmon\config.json` (Windows).
71
98
 
72
- - **Refresh interval** — adjust with `←→`
73
- - **Clear screen** — on/off toggle
99
+ - **Refresh interval** — adjust with `←` `→`
100
+ - **Clear screen** — clears terminal on launch (like `watch`)
74
101
 
75
102
  ## How It Works
76
103
 
77
- 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.
78
-
79
- Dashboard loads current month only (fast). Table loads 6 months lazily on first switch.
104
+ - Reads Claude Code's JSONL session logs from `~/.claude/projects/`
105
+ - Calculates costs using Claude model pricing (Opus, Sonnet, Haiku)
106
+ - Caches file reads by mtime subsequent refreshes are near-instant
107
+ - Dashboard loads current month only (fast). Table loads 6 months lazily.
108
+ - Rate limits fetched from Anthropic OAuth API every 2 minutes (token from macOS Keychain)
80
109
 
81
- Cross-platform: supports macOS, Linux, and Windows (`%APPDATA%`, `XDG_CONFIG_HOME`, `CLAUDE_CONFIG_DIR`).
110
+ Cross-platform: macOS, Linux, Windows.
82
111
 
83
112
  ## CI/CD
84
113
 
85
- Publishes to npm via GitHub Actions on version tags:
114
+ Publishes to npm and GitHub Packages via GitHub Actions on version tags:
86
115
 
87
116
  ```bash
88
- git tag v0.5.0 && git push --tags
117
+ git tag v0.7.0 && git push --tags
89
118
  ```
90
119
 
91
120
  ## Requirements
package/dist/cli.js CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  // src/cli.tsx
4
4
  import { render } from "ink";
5
+ import { MouseProvider as MouseProvider2 } from "@zenobius/ink-mouse";
5
6
 
6
7
  // src/config.ts
7
8
  import { readFile, writeFile, mkdir } from "fs/promises";
@@ -15,12 +16,12 @@ function configDir() {
15
16
  const xdg = process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
16
17
  return join(xdg, "tokmon");
17
18
  }
18
- function configPath() {
19
+ function configLocation() {
19
20
  return join(configDir(), "config.json");
20
21
  }
21
22
  async function loadConfig() {
22
23
  try {
23
- const raw = await readFile(configPath(), "utf-8");
24
+ const raw = await readFile(configLocation(), "utf-8");
24
25
  const parsed = JSON.parse(raw);
25
26
  return { ...DEFAULTS, ...parsed };
26
27
  } catch {
@@ -30,15 +31,13 @@ async function loadConfig() {
30
31
  async function saveConfig(config2) {
31
32
  const dir = configDir();
32
33
  await mkdir(dir, { recursive: true });
33
- await writeFile(configPath(), JSON.stringify(config2, null, 2) + "\n");
34
- }
35
- function configLocation() {
36
- return configPath();
34
+ await writeFile(configLocation(), JSON.stringify(config2, null, 2) + "\n");
37
35
  }
38
36
 
39
37
  // src/app.tsx
40
- import { useState, useEffect, useRef } from "react";
38
+ import { useState, useEffect, useCallback, useRef } from "react";
41
39
  import { Box, Text, useInput, useStdout, useApp } from "ink";
40
+ import { useMouse } from "@zenobius/ink-mouse";
42
41
 
43
42
  // src/data.ts
44
43
  import { readdir, stat as fsStat } from "fs/promises";
@@ -82,7 +81,7 @@ function costOf(model, u) {
82
81
  return (u.input_tokens ?? 0) * p.i + (u.output_tokens ?? 0) * p.o + (u.cache_creation_input_tokens ?? 0) * p.cc + (u.cache_read_input_tokens ?? 0) * p.cr;
83
82
  }
84
83
  function shortModel(model) {
85
- return model.replace("claude-", "").replace("-20251001", "").replace("-20250514", "").replace("-20251101", "").replace("-20250805", "");
84
+ return model.replace("claude-", "").replace(/-\d{8}$/, "");
86
85
  }
87
86
  async function parseFile(path, since) {
88
87
  const entries = [];
@@ -110,7 +109,7 @@ async function parseFile(path, since) {
110
109
  return entries;
111
110
  }
112
111
  async function loadEntries(since) {
113
- const all = [];
112
+ const chunks = [];
114
113
  const seen = /* @__PURE__ */ new Set();
115
114
  for (const dir of getClaudeDirs()) {
116
115
  let listing;
@@ -129,17 +128,17 @@ async function loadEntries(since) {
129
128
  if (s.mtimeMs < since) return;
130
129
  const cached = fileCache.get(path);
131
130
  if (cached && cached.mtimeMs === s.mtimeMs) {
132
- all.push(...cached.data);
131
+ chunks.push(cached.data);
133
132
  return;
134
133
  }
135
134
  const data = await parseFile(path, since);
136
135
  fileCache.set(path, { mtimeMs: s.mtimeMs, data });
137
- all.push(...data);
136
+ chunks.push(data);
138
137
  } catch {
139
138
  }
140
139
  }));
141
140
  }
142
- return all;
141
+ return chunks.flat();
143
142
  }
144
143
  function sum(entries) {
145
144
  let cost = 0, tokens2 = 0;
@@ -159,29 +158,36 @@ function groupBy(entries, keyFn) {
159
158
  }
160
159
  const rows = [];
161
160
  for (const [label, group] of groups) {
162
- const models = [...new Set(group.map((e) => shortModel(e.model)))];
163
161
  let input = 0, output = 0, cacheCreate = 0, cacheRead = 0, cost = 0;
162
+ const byModel = /* @__PURE__ */ new Map();
164
163
  for (const e of group) {
165
164
  input += e.input;
166
165
  output += e.output;
167
166
  cacheCreate += e.cacheCreate;
168
167
  cacheRead += e.cacheRead;
169
168
  cost += e.cost;
170
- }
171
- const byModel = /* @__PURE__ */ new Map();
172
- for (const e of group) {
173
169
  const name = shortModel(e.model);
174
- const m = byModel.get(name) ?? { name, input: 0, output: 0, cacheCreate: 0, cacheRead: 0, cost: 0 };
175
- m.input += e.input;
176
- m.output += e.output;
177
- m.cacheCreate += e.cacheCreate;
178
- m.cacheRead += e.cacheRead;
179
- m.cost += e.cost;
180
- byModel.set(name, m);
170
+ const m = byModel.get(name);
171
+ if (m) {
172
+ m.input += e.input;
173
+ m.output += e.output;
174
+ m.cacheCreate += e.cacheCreate;
175
+ m.cacheRead += e.cacheRead;
176
+ m.cost += e.cost;
177
+ } else {
178
+ byModel.set(name, {
179
+ name,
180
+ input: e.input,
181
+ output: e.output,
182
+ cacheCreate: e.cacheCreate,
183
+ cacheRead: e.cacheRead,
184
+ cost: e.cost
185
+ });
186
+ }
181
187
  }
182
188
  rows.push({
183
189
  label,
184
- models: models.sort(),
190
+ models: [...byModel.keys()].sort(),
185
191
  input,
186
192
  output,
187
193
  cacheCreate,
@@ -215,9 +221,14 @@ async function fetchDashboard() {
215
221
  const todayEntries = entries.filter((e) => e.ts >= todayStart);
216
222
  let burnRate = 0;
217
223
  if (todayEntries.length > 0) {
218
- const oldest = Math.min(...todayEntries.map((e) => e.ts));
224
+ let oldest = todayEntries[0].ts;
225
+ let totalCost = 0;
226
+ for (const e of todayEntries) {
227
+ if (e.ts < oldest) oldest = e.ts;
228
+ totalCost += e.cost;
229
+ }
219
230
  const hrs = (now - oldest) / 36e5;
220
- if (hrs > 0) burnRate = todayEntries.reduce((s, e) => s + e.cost, 0) / hrs;
231
+ if (hrs > 0) burnRate = totalCost / hrs;
221
232
  }
222
233
  return {
223
234
  today: sum(todayEntries),
@@ -258,9 +269,10 @@ async function getAccessToken() {
258
269
  }
259
270
  return null;
260
271
  }
272
+ var EMPTY = { session: null, weekly: null, sonnet: null, extraUsage: null, error: null };
261
273
  async function fetchBilling() {
262
274
  const token = await getAccessToken();
263
- if (!token) return null;
275
+ if (!token) return { ...EMPTY, error: "No OAuth token found (macOS Keychain)" };
264
276
  try {
265
277
  const res = await fetch("https://api.anthropic.com/api/oauth/usage", {
266
278
  headers: {
@@ -270,7 +282,9 @@ async function fetchBilling() {
270
282
  },
271
283
  signal: AbortSignal.timeout(1e4)
272
284
  });
273
- if (!res.ok) return null;
285
+ if (res.status === 429) return { ...EMPTY, error: "Rate limited \u2014 retrying in 2m" };
286
+ if (res.status === 401) return { ...EMPTY, error: "Token expired \u2014 restart Claude Code" };
287
+ if (!res.ok) return { ...EMPTY, error: `API ${res.status}` };
274
288
  const data = await res.json();
275
289
  return {
276
290
  session: data.five_hour ? {
@@ -288,10 +302,11 @@ async function fetchBilling() {
288
302
  extraUsage: data.extra_usage?.is_enabled ? {
289
303
  limit: data.extra_usage.monthly_limit / 100,
290
304
  used: data.extra_usage.used_credits / 100
291
- } : null
305
+ } : null,
306
+ error: null
292
307
  };
293
308
  } catch {
294
- return null;
309
+ return { ...EMPTY, error: "Network error" };
295
310
  }
296
311
  }
297
312
  function formatReset(iso) {
@@ -326,10 +341,10 @@ function time(date) {
326
341
  second: "2-digit"
327
342
  });
328
343
  }
344
+ var SHORT_MONTHS = ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
329
345
  function shortDate(iso) {
330
346
  const [, m, d] = iso.split("-");
331
- const months = ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
332
- return `${months[Number(m)]} ${Number(d).toString().padStart(2, " ")}`;
347
+ return `${SHORT_MONTHS[Number(m)]} ${Number(d).toString().padStart(2, " ")}`;
333
348
  }
334
349
  function col(s, w, align = "right") {
335
350
  if (s.length > w) return s.slice(0, w - 1) + "~";
@@ -341,6 +356,11 @@ function col(s, w, align = "right") {
341
356
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
342
357
  var TABS = ["Dashboard", "Table"];
343
358
  var VIEWS = ["Daily", "Weekly", "Monthly"];
359
+ var SORTS = ["date \u2191", "date \u2193", "cost \u2191", "cost \u2193"];
360
+ var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
361
+ var MONTHS = ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
362
+ var DEFAULT_CONFIG = { interval: 2, clearScreen: true };
363
+ var IS_TTY = process.stdin.isTTY === true;
344
364
  function App({ interval: cliInterval }) {
345
365
  const [dashboard, setDashboard] = useState(null);
346
366
  const [billing, setBilling] = useState(null);
@@ -352,6 +372,7 @@ function App({ interval: cliInterval }) {
352
372
  const [view, setView] = useState(0);
353
373
  const [cursor, setCursor] = useState(0);
354
374
  const [expanded, setExpanded] = useState(-1);
375
+ const [sort, setSort] = useState(0);
355
376
  const [showSettings, setShowSettings] = useState(false);
356
377
  const [config2, setConfig] = useState(null);
357
378
  const [settingsCursor, setSettingsCursor] = useState(0);
@@ -361,7 +382,7 @@ function App({ interval: cliInterval }) {
361
382
  const rows = stdout?.rows ?? 24;
362
383
  const cols = stdout?.columns ?? 80;
363
384
  const interval2 = cliInterval ?? (config2?.interval ?? 2) * 1e3;
364
- const cfg = config2 ?? { interval: 2, clearScreen: true };
385
+ const cfg = config2 ?? DEFAULT_CONFIG;
365
386
  useEffect(() => {
366
387
  loadConfig().then((c) => {
367
388
  if (cliInterval) c = { ...c, interval: cliInterval / 1e3 };
@@ -392,7 +413,7 @@ function App({ interval: cliInterval }) {
392
413
  useEffect(() => {
393
414
  let active = true;
394
415
  const load = () => fetchBilling().then((b) => {
395
- if (active && b) setBilling(b);
416
+ if (active) setBilling(b);
396
417
  }).catch(() => {
397
418
  });
398
419
  load();
@@ -435,30 +456,35 @@ function App({ interval: cliInterval }) {
435
456
  clearInterval(id);
436
457
  };
437
458
  }, [tab, interval2]);
438
- const isTTY = process.stdin.isTTY === true;
459
+ const resetView = useCallback(() => {
460
+ setCursor(0);
461
+ setExpanded(-1);
462
+ }, []);
463
+ const mouse = useMouse();
464
+ useEffect(() => {
465
+ if (!IS_TTY) return;
466
+ mouse.enable();
467
+ const onScroll = (_pos, dir) => {
468
+ if (tab === 1) {
469
+ setCursor((c) => dir === "scrollup" ? Math.max(0, c - 3) : c + 3);
470
+ }
471
+ };
472
+ mouse.events.on("scroll", onScroll);
473
+ return () => {
474
+ mouse.events.off("scroll", onScroll);
475
+ };
476
+ }, [tab]);
439
477
  useInput((input, key) => {
440
478
  if (showSettings) {
441
479
  if (key.escape || input === "s") setShowSettings(false);
442
480
  if (key.upArrow) setSettingsCursor((c) => Math.max(0, c - 1));
443
481
  if (key.downArrow) setSettingsCursor((c) => Math.min(1, c + 1));
444
482
  if (settingsCursor === 0) {
445
- if (key.leftArrow) setConfig((c) => {
446
- const n = { ...c, interval: Math.max(1, c.interval - 1) };
447
- saveConfig(n);
448
- return n;
449
- });
450
- if (key.rightArrow) setConfig((c) => {
451
- const n = { ...c, interval: c.interval + 1 };
452
- saveConfig(n);
453
- return n;
454
- });
483
+ if (key.leftArrow) updateConfig((c) => ({ ...c, interval: Math.max(1, c.interval - 1) }));
484
+ if (key.rightArrow) updateConfig((c) => ({ ...c, interval: c.interval + 1 }));
455
485
  }
456
486
  if (settingsCursor === 1 && (key.leftArrow || key.rightArrow || key.return)) {
457
- setConfig((c) => {
458
- const n = { ...c, clearScreen: !c.clearScreen };
459
- saveConfig(n);
460
- return n;
461
- });
487
+ updateConfig((c) => ({ ...c, clearScreen: !c.clearScreen }));
462
488
  }
463
489
  return;
464
490
  }
@@ -472,51 +498,48 @@ function App({ interval: cliInterval }) {
472
498
  }
473
499
  if (key.tab) {
474
500
  setTab((t) => (t + 1) % TABS.length);
475
- setCursor(0);
476
- setExpanded(-1);
501
+ resetView();
477
502
  return;
478
503
  }
479
504
  if (input === "1") {
480
505
  setTab(0);
481
- setCursor(0);
482
- setExpanded(-1);
506
+ resetView();
483
507
  return;
484
508
  }
485
509
  if (input === "2") {
486
510
  setTab(1);
487
- setCursor(0);
488
- setExpanded(-1);
511
+ resetView();
489
512
  return;
490
513
  }
491
514
  if (tab === 1) {
492
515
  if (input === "d") {
493
516
  setView(0);
494
- setCursor(0);
495
- setExpanded(-1);
517
+ resetView();
496
518
  return;
497
519
  }
498
520
  if (input === "w") {
499
521
  setView(1);
500
- setCursor(0);
501
- setExpanded(-1);
522
+ resetView();
502
523
  return;
503
524
  }
504
525
  if (input === "m") {
505
526
  setView(2);
506
- setCursor(0);
507
- setExpanded(-1);
527
+ resetView();
508
528
  return;
509
529
  }
510
530
  if (key.leftArrow) {
511
531
  setView((v) => (v - 1 + VIEWS.length) % VIEWS.length);
512
- setCursor(0);
513
- setExpanded(-1);
532
+ resetView();
514
533
  return;
515
534
  }
516
535
  if (key.rightArrow) {
517
536
  setView((v) => (v + 1) % VIEWS.length);
518
- setCursor(0);
519
- setExpanded(-1);
537
+ resetView();
538
+ return;
539
+ }
540
+ if (input === "o") {
541
+ setSort((s) => (s + 1) % SORTS.length);
542
+ resetView();
520
543
  return;
521
544
  }
522
545
  if (key.return) {
@@ -530,8 +553,7 @@ function App({ interval: cliInterval }) {
530
553
  } else {
531
554
  if (key.leftArrow || key.rightArrow) {
532
555
  setTab((t) => (t + 1) % TABS.length);
533
- setCursor(0);
534
- setExpanded(-1);
556
+ resetView();
535
557
  return;
536
558
  }
537
559
  }
@@ -551,10 +573,18 @@ function App({ interval: cliInterval }) {
551
573
  setCursor((c) => input === "g" ? 0 : Math.max(0, c - Math.max(1, rows - 12)));
552
574
  return;
553
575
  }
554
- }, { isActive: isTTY });
576
+ }, { isActive: IS_TTY });
577
+ function updateConfig(fn) {
578
+ setConfig((prev) => {
579
+ const next = fn(prev ?? DEFAULT_CONFIG);
580
+ saveConfig(next);
581
+ return next;
582
+ });
583
+ }
555
584
  if (error) return /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { color: "red", children: error }) });
556
585
  if (!dashboard) return /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Loading..." }) });
557
- const tableData = table ? [table.daily, table.weekly, table.monthly][view] : [];
586
+ const rawTableData = table ? [table.daily, table.weekly, table.monthly][view] : [];
587
+ const tableData = sortRows(rawTableData, sort);
558
588
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, minHeight: rows, children: [
559
589
  /* @__PURE__ */ jsxs(Box, { justifyContent: "space-between", children: [
560
590
  /* @__PURE__ */ jsxs(Box, { children: [
@@ -578,18 +608,21 @@ function App({ interval: cliInterval }) {
578
608
  /* @__PURE__ */ jsx(Box, { height: 1 }),
579
609
  tab === 0 && /* @__PURE__ */ jsx(DashboardView, { data: dashboard, billing }),
580
610
  tab === 1 && /* @__PURE__ */ jsxs(Fragment, { children: [
581
- /* @__PURE__ */ jsx(ViewBar, { views: VIEWS, active: view }),
611
+ /* @__PURE__ */ jsx(ViewBar, { views: VIEWS, active: view, sort: SORTS[sort] }),
582
612
  /* @__PURE__ */ jsx(Box, { height: 1 }),
583
613
  tableLoading && !table ? /* @__PURE__ */ jsx(Spinner, { label: "Loading 6 months of history" }) : /* @__PURE__ */ jsx(TableView, { rows: tableData, cursor, expanded, maxRows: rows - 12, wide: cols > 90 })
584
614
  ] })
585
615
  ] }),
586
- (tab === 0 || showSettings) && /* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
587
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "by " }),
588
- /* @__PURE__ */ jsx(Text, { children: "David Ilie" }),
589
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " (" }),
590
- /* @__PURE__ */ jsx(Text, { color: "cyan", children: "davidilie.com" }),
591
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: ") \xB7 s=settings q=quit" })
592
- ] })
616
+ (tab === 0 || showSettings) && /* @__PURE__ */ jsx(Footer, {})
617
+ ] });
618
+ }
619
+ function Footer() {
620
+ return /* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
621
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "by " }),
622
+ /* @__PURE__ */ jsx(Text, { children: "David Ilie" }),
623
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " (" }),
624
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: "davidilie.com" }),
625
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: ") \xB7 s=settings q=quit" })
593
626
  ] });
594
627
  }
595
628
  function TabBar({ tabs, active }) {
@@ -603,16 +636,34 @@ function TabBar({ tabs, active }) {
603
636
  " "
604
637
  ] }) }, t)) });
605
638
  }
606
- function ViewBar({ views, active }) {
639
+ function ViewBar({ views, active, sort }) {
607
640
  return /* @__PURE__ */ jsxs(Box, { children: [
608
641
  views.map((v, i) => /* @__PURE__ */ jsx(Box, { marginRight: 2, children: i === active ? /* @__PURE__ */ jsxs(Text, { bold: true, color: "cyan", children: [
609
642
  "[",
610
643
  v,
611
644
  "]"
612
645
  ] }) : /* @__PURE__ */ jsx(Text, { dimColor: true, children: v }) }, v)),
613
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " d/w/m or \u2190\u2192" })
646
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " d/w/m \xB7 sort: " }),
647
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "magenta", children: sort }),
648
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " o=cycle" })
614
649
  ] });
615
650
  }
651
+ function sortRows(rows, sortIdx) {
652
+ if (rows.length === 0) return rows;
653
+ const sorted = [...rows];
654
+ switch (sortIdx) {
655
+ case 0:
656
+ return sorted.sort((a, b) => a.label.localeCompare(b.label));
657
+ case 1:
658
+ return sorted.sort((a, b) => b.label.localeCompare(a.label));
659
+ case 2:
660
+ return sorted.sort((a, b) => a.cost - b.cost);
661
+ case 3:
662
+ return sorted.sort((a, b) => b.cost - a.cost);
663
+ default:
664
+ return sorted;
665
+ }
666
+ }
616
667
  function SettingsView({ config: config2, cursor }) {
617
668
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
618
669
  /* @__PURE__ */ jsx(Text, { bold: true, children: "Settings" }),
@@ -678,21 +729,21 @@ function DashboardView({ data, billing }) {
678
729
  ]
679
730
  }
680
731
  ),
681
- billing && /* @__PURE__ */ jsxs(Fragment, { children: [
682
- /* @__PURE__ */ jsx(Box, { height: 1 }),
683
- /* @__PURE__ */ jsxs(
684
- Box,
685
- {
686
- flexDirection: "column",
687
- paddingLeft: 1,
688
- borderStyle: "bold",
689
- borderColor: "yellow",
690
- borderRight: false,
691
- borderTop: false,
692
- borderBottom: false,
693
- children: [
694
- /* @__PURE__ */ jsx(Text, { bold: true, children: "Rate Limits" }),
695
- /* @__PURE__ */ jsx(Box, { height: 1 }),
732
+ /* @__PURE__ */ jsx(Box, { height: 1 }),
733
+ /* @__PURE__ */ jsxs(
734
+ Box,
735
+ {
736
+ flexDirection: "column",
737
+ paddingLeft: 1,
738
+ borderStyle: "bold",
739
+ borderColor: billing?.error ? "red" : "yellow",
740
+ borderRight: false,
741
+ borderTop: false,
742
+ borderBottom: false,
743
+ children: [
744
+ /* @__PURE__ */ jsx(Text, { bold: true, children: "Rate Limits" }),
745
+ /* @__PURE__ */ jsx(Box, { height: 1 }),
746
+ billing?.error ? /* @__PURE__ */ jsx(Text, { color: "red", children: billing.error }) : billing?.session || billing?.weekly ? /* @__PURE__ */ jsxs(Fragment, { children: [
696
747
  billing.session && /* @__PURE__ */ jsx(LimitBar, { label: "Session", pct: billing.session.utilization, resets: billing.session.resetsAt }),
697
748
  billing.weekly && /* @__PURE__ */ jsx(LimitBar, { label: "Weekly", pct: billing.weekly.utilization, resets: billing.weekly.resetsAt }),
698
749
  billing.sonnet && /* @__PURE__ */ jsx(LimitBar, { label: "Sonnet", pct: billing.sonnet.utilization, resets: billing.sonnet.resetsAt }),
@@ -708,10 +759,10 @@ function DashboardView({ data, billing }) {
708
759
  " limit"
709
760
  ] })
710
761
  ] })
711
- ]
712
- }
713
- )
714
- ] })
762
+ ] }) : /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Fetching..." })
763
+ ]
764
+ }
765
+ )
715
766
  ] });
716
767
  }
717
768
  function LimitBar({ label, pct, resets }) {
@@ -809,26 +860,18 @@ function TableView({ rows: allRows, cursor, expanded, maxRows, wide }) {
809
860
  ] }),
810
861
  /* @__PURE__ */ jsx(Box, { height: 1 }),
811
862
  /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
812
- "\u2191\u2193 navigate \xB7 Enter detail \xB7 g top G bottom \xB7 ",
863
+ "\u2191\u2193 navigate \xB7 Enter detail \xB7 o sort \xB7 g/G top/bottom \xB7 ",
813
864
  clampedCursor + 1,
814
865
  "/",
815
866
  allRows.length
816
867
  ] }),
817
868
  /* @__PURE__ */ jsx(Box, { height: 1 }),
818
- /* @__PURE__ */ jsxs(Box, { children: [
819
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "by " }),
820
- /* @__PURE__ */ jsx(Text, { children: "David Ilie" }),
821
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " (" }),
822
- /* @__PURE__ */ jsx(Text, { color: "cyan", children: "davidilie.com" }),
823
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: ") \xB7 s=settings q=quit" })
824
- ] })
869
+ /* @__PURE__ */ jsx(Footer, {})
825
870
  ] });
826
871
  }
827
872
  function RowDetail({ row, indent }) {
828
- const pad = " ".repeat(indent);
829
873
  return /* @__PURE__ */ jsx(Box, { flexDirection: "column", paddingLeft: indent, marginY: 0, children: row.breakdown.map((m, i) => {
830
- const last = i === row.breakdown.length - 1;
831
- const prefix = last ? "\u2514\u2500" : "\u251C\u2500";
874
+ const prefix = i === row.breakdown.length - 1 ? "\u2514\u2500" : "\u251C\u2500";
832
875
  return /* @__PURE__ */ jsxs(Text, { children: [
833
876
  /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
834
877
  prefix,
@@ -856,15 +899,14 @@ function RowDetail({ row, indent }) {
856
899
  }) });
857
900
  }
858
901
  function Spinner({ label }) {
859
- const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
860
902
  const [i, setI] = useState(0);
861
903
  useEffect(() => {
862
- const id = setInterval(() => setI((n) => (n + 1) % frames.length), 80);
904
+ const id = setInterval(() => setI((n) => (n + 1) % SPINNER_FRAMES.length), 80);
863
905
  return () => clearInterval(id);
864
906
  }, []);
865
907
  return /* @__PURE__ */ jsxs(Box, { children: [
866
908
  /* @__PURE__ */ jsxs(Text, { color: "green", children: [
867
- frames[i],
909
+ SPINNER_FRAMES[i],
868
910
  " "
869
911
  ] }),
870
912
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: label })
@@ -873,9 +915,8 @@ function Spinner({ label }) {
873
915
  function fmtLabel(label) {
874
916
  if (label.length === 10 && label[4] === "-") return shortDate(label);
875
917
  if (label.length === 7 && label[4] === "-") {
876
- const [, m] = label.split("-");
877
- const months = ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
878
- return `${months[Number(m)]} '${label.slice(2, 4)}`;
918
+ const m = label.slice(5, 7);
919
+ return `${MONTHS[Number(m)]} '${label.slice(2, 4)}`;
879
920
  }
880
921
  return shortDate(label);
881
922
  }
@@ -908,5 +949,5 @@ var config = await loadConfig();
908
949
  if (config.clearScreen && process.stdout.isTTY) {
909
950
  process.stdout.write("\x1B[2J\x1B[H");
910
951
  }
911
- var { waitUntilExit } = render(/* @__PURE__ */ jsx2(App, { interval }));
952
+ var { waitUntilExit } = render(/* @__PURE__ */ jsx2(MouseProvider2, { children: /* @__PURE__ */ jsx2(App, { interval }) }));
912
953
  await waitUntilExit();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokmon",
3
- "version": "0.6.1",
3
+ "version": "0.8.0",
4
4
  "description": "Terminal dashboard for Claude Code usage and costs",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,10 +14,12 @@
14
14
  "dev": "tsx src/cli.tsx"
15
15
  },
16
16
  "dependencies": {
17
+ "@zenobius/ink-mouse": "^1.0.3",
17
18
  "ink": "^5.0.0",
18
19
  "react": "^18.0.0"
19
20
  },
20
21
  "devDependencies": {
22
+ "@types/node": "^25.5.2",
21
23
  "@types/react": "^18.0.0",
22
24
  "tsup": "^8.0.0",
23
25
  "tsx": "^4.0.0",