tokmon 0.6.1 → 0.7.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 +113 -92
  3. package/package.json +2 -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
@@ -15,12 +15,12 @@ function configDir() {
15
15
  const xdg = process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
16
16
  return join(xdg, "tokmon");
17
17
  }
18
- function configPath() {
18
+ function configLocation() {
19
19
  return join(configDir(), "config.json");
20
20
  }
21
21
  async function loadConfig() {
22
22
  try {
23
- const raw = await readFile(configPath(), "utf-8");
23
+ const raw = await readFile(configLocation(), "utf-8");
24
24
  const parsed = JSON.parse(raw);
25
25
  return { ...DEFAULTS, ...parsed };
26
26
  } catch {
@@ -30,14 +30,11 @@ async function loadConfig() {
30
30
  async function saveConfig(config2) {
31
31
  const dir = configDir();
32
32
  await mkdir(dir, { recursive: true });
33
- await writeFile(configPath(), JSON.stringify(config2, null, 2) + "\n");
34
- }
35
- function configLocation() {
36
- return configPath();
33
+ await writeFile(configLocation(), JSON.stringify(config2, null, 2) + "\n");
37
34
  }
38
35
 
39
36
  // src/app.tsx
40
- import { useState, useEffect, useRef } from "react";
37
+ import { useState, useEffect, useCallback, useRef } from "react";
41
38
  import { Box, Text, useInput, useStdout, useApp } from "ink";
42
39
 
43
40
  // src/data.ts
@@ -82,7 +79,7 @@ function costOf(model, u) {
82
79
  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
80
  }
84
81
  function shortModel(model) {
85
- return model.replace("claude-", "").replace("-20251001", "").replace("-20250514", "").replace("-20251101", "").replace("-20250805", "");
82
+ return model.replace("claude-", "").replace(/-\d{8}$/, "");
86
83
  }
87
84
  async function parseFile(path, since) {
88
85
  const entries = [];
@@ -110,7 +107,7 @@ async function parseFile(path, since) {
110
107
  return entries;
111
108
  }
112
109
  async function loadEntries(since) {
113
- const all = [];
110
+ const chunks = [];
114
111
  const seen = /* @__PURE__ */ new Set();
115
112
  for (const dir of getClaudeDirs()) {
116
113
  let listing;
@@ -129,17 +126,17 @@ async function loadEntries(since) {
129
126
  if (s.mtimeMs < since) return;
130
127
  const cached = fileCache.get(path);
131
128
  if (cached && cached.mtimeMs === s.mtimeMs) {
132
- all.push(...cached.data);
129
+ chunks.push(cached.data);
133
130
  return;
134
131
  }
135
132
  const data = await parseFile(path, since);
136
133
  fileCache.set(path, { mtimeMs: s.mtimeMs, data });
137
- all.push(...data);
134
+ chunks.push(data);
138
135
  } catch {
139
136
  }
140
137
  }));
141
138
  }
142
- return all;
139
+ return chunks.flat();
143
140
  }
144
141
  function sum(entries) {
145
142
  let cost = 0, tokens2 = 0;
@@ -159,29 +156,36 @@ function groupBy(entries, keyFn) {
159
156
  }
160
157
  const rows = [];
161
158
  for (const [label, group] of groups) {
162
- const models = [...new Set(group.map((e) => shortModel(e.model)))];
163
159
  let input = 0, output = 0, cacheCreate = 0, cacheRead = 0, cost = 0;
160
+ const byModel = /* @__PURE__ */ new Map();
164
161
  for (const e of group) {
165
162
  input += e.input;
166
163
  output += e.output;
167
164
  cacheCreate += e.cacheCreate;
168
165
  cacheRead += e.cacheRead;
169
166
  cost += e.cost;
170
- }
171
- const byModel = /* @__PURE__ */ new Map();
172
- for (const e of group) {
173
167
  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);
168
+ const m = byModel.get(name);
169
+ if (m) {
170
+ m.input += e.input;
171
+ m.output += e.output;
172
+ m.cacheCreate += e.cacheCreate;
173
+ m.cacheRead += e.cacheRead;
174
+ m.cost += e.cost;
175
+ } else {
176
+ byModel.set(name, {
177
+ name,
178
+ input: e.input,
179
+ output: e.output,
180
+ cacheCreate: e.cacheCreate,
181
+ cacheRead: e.cacheRead,
182
+ cost: e.cost
183
+ });
184
+ }
181
185
  }
182
186
  rows.push({
183
187
  label,
184
- models: models.sort(),
188
+ models: [...byModel.keys()].sort(),
185
189
  input,
186
190
  output,
187
191
  cacheCreate,
@@ -215,9 +219,14 @@ async function fetchDashboard() {
215
219
  const todayEntries = entries.filter((e) => e.ts >= todayStart);
216
220
  let burnRate = 0;
217
221
  if (todayEntries.length > 0) {
218
- const oldest = Math.min(...todayEntries.map((e) => e.ts));
222
+ let oldest = todayEntries[0].ts;
223
+ let totalCost = 0;
224
+ for (const e of todayEntries) {
225
+ if (e.ts < oldest) oldest = e.ts;
226
+ totalCost += e.cost;
227
+ }
219
228
  const hrs = (now - oldest) / 36e5;
220
- if (hrs > 0) burnRate = todayEntries.reduce((s, e) => s + e.cost, 0) / hrs;
229
+ if (hrs > 0) burnRate = totalCost / hrs;
221
230
  }
222
231
  return {
223
232
  today: sum(todayEntries),
@@ -326,10 +335,10 @@ function time(date) {
326
335
  second: "2-digit"
327
336
  });
328
337
  }
338
+ var SHORT_MONTHS = ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
329
339
  function shortDate(iso) {
330
340
  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, " ")}`;
341
+ return `${SHORT_MONTHS[Number(m)]} ${Number(d).toString().padStart(2, " ")}`;
333
342
  }
334
343
  function col(s, w, align = "right") {
335
344
  if (s.length > w) return s.slice(0, w - 1) + "~";
@@ -341,6 +350,11 @@ function col(s, w, align = "right") {
341
350
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
342
351
  var TABS = ["Dashboard", "Table"];
343
352
  var VIEWS = ["Daily", "Weekly", "Monthly"];
353
+ var SORTS = ["date \u2191", "date \u2193", "cost \u2191", "cost \u2193"];
354
+ var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
355
+ var MONTHS = ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
356
+ var DEFAULT_CONFIG = { interval: 2, clearScreen: true };
357
+ var IS_TTY = process.stdin.isTTY === true;
344
358
  function App({ interval: cliInterval }) {
345
359
  const [dashboard, setDashboard] = useState(null);
346
360
  const [billing, setBilling] = useState(null);
@@ -352,6 +366,7 @@ function App({ interval: cliInterval }) {
352
366
  const [view, setView] = useState(0);
353
367
  const [cursor, setCursor] = useState(0);
354
368
  const [expanded, setExpanded] = useState(-1);
369
+ const [sort, setSort] = useState(0);
355
370
  const [showSettings, setShowSettings] = useState(false);
356
371
  const [config2, setConfig] = useState(null);
357
372
  const [settingsCursor, setSettingsCursor] = useState(0);
@@ -361,7 +376,7 @@ function App({ interval: cliInterval }) {
361
376
  const rows = stdout?.rows ?? 24;
362
377
  const cols = stdout?.columns ?? 80;
363
378
  const interval2 = cliInterval ?? (config2?.interval ?? 2) * 1e3;
364
- const cfg = config2 ?? { interval: 2, clearScreen: true };
379
+ const cfg = config2 ?? DEFAULT_CONFIG;
365
380
  useEffect(() => {
366
381
  loadConfig().then((c) => {
367
382
  if (cliInterval) c = { ...c, interval: cliInterval / 1e3 };
@@ -435,30 +450,21 @@ function App({ interval: cliInterval }) {
435
450
  clearInterval(id);
436
451
  };
437
452
  }, [tab, interval2]);
438
- const isTTY = process.stdin.isTTY === true;
453
+ const resetView = useCallback(() => {
454
+ setCursor(0);
455
+ setExpanded(-1);
456
+ }, []);
439
457
  useInput((input, key) => {
440
458
  if (showSettings) {
441
459
  if (key.escape || input === "s") setShowSettings(false);
442
460
  if (key.upArrow) setSettingsCursor((c) => Math.max(0, c - 1));
443
461
  if (key.downArrow) setSettingsCursor((c) => Math.min(1, c + 1));
444
462
  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
- });
463
+ if (key.leftArrow) updateConfig((c) => ({ ...c, interval: Math.max(1, c.interval - 1) }));
464
+ if (key.rightArrow) updateConfig((c) => ({ ...c, interval: c.interval + 1 }));
455
465
  }
456
466
  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
- });
467
+ updateConfig((c) => ({ ...c, clearScreen: !c.clearScreen }));
462
468
  }
463
469
  return;
464
470
  }
@@ -472,51 +478,48 @@ function App({ interval: cliInterval }) {
472
478
  }
473
479
  if (key.tab) {
474
480
  setTab((t) => (t + 1) % TABS.length);
475
- setCursor(0);
476
- setExpanded(-1);
481
+ resetView();
477
482
  return;
478
483
  }
479
484
  if (input === "1") {
480
485
  setTab(0);
481
- setCursor(0);
482
- setExpanded(-1);
486
+ resetView();
483
487
  return;
484
488
  }
485
489
  if (input === "2") {
486
490
  setTab(1);
487
- setCursor(0);
488
- setExpanded(-1);
491
+ resetView();
489
492
  return;
490
493
  }
491
494
  if (tab === 1) {
492
495
  if (input === "d") {
493
496
  setView(0);
494
- setCursor(0);
495
- setExpanded(-1);
497
+ resetView();
496
498
  return;
497
499
  }
498
500
  if (input === "w") {
499
501
  setView(1);
500
- setCursor(0);
501
- setExpanded(-1);
502
+ resetView();
502
503
  return;
503
504
  }
504
505
  if (input === "m") {
505
506
  setView(2);
506
- setCursor(0);
507
- setExpanded(-1);
507
+ resetView();
508
508
  return;
509
509
  }
510
510
  if (key.leftArrow) {
511
511
  setView((v) => (v - 1 + VIEWS.length) % VIEWS.length);
512
- setCursor(0);
513
- setExpanded(-1);
512
+ resetView();
514
513
  return;
515
514
  }
516
515
  if (key.rightArrow) {
517
516
  setView((v) => (v + 1) % VIEWS.length);
518
- setCursor(0);
519
- setExpanded(-1);
517
+ resetView();
518
+ return;
519
+ }
520
+ if (input === "o") {
521
+ setSort((s) => (s + 1) % SORTS.length);
522
+ resetView();
520
523
  return;
521
524
  }
522
525
  if (key.return) {
@@ -530,8 +533,7 @@ function App({ interval: cliInterval }) {
530
533
  } else {
531
534
  if (key.leftArrow || key.rightArrow) {
532
535
  setTab((t) => (t + 1) % TABS.length);
533
- setCursor(0);
534
- setExpanded(-1);
536
+ resetView();
535
537
  return;
536
538
  }
537
539
  }
@@ -551,10 +553,18 @@ function App({ interval: cliInterval }) {
551
553
  setCursor((c) => input === "g" ? 0 : Math.max(0, c - Math.max(1, rows - 12)));
552
554
  return;
553
555
  }
554
- }, { isActive: isTTY });
556
+ }, { isActive: IS_TTY });
557
+ function updateConfig(fn) {
558
+ setConfig((prev) => {
559
+ const next = fn(prev ?? DEFAULT_CONFIG);
560
+ saveConfig(next);
561
+ return next;
562
+ });
563
+ }
555
564
  if (error) return /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { color: "red", children: error }) });
556
565
  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] : [];
566
+ const rawTableData = table ? [table.daily, table.weekly, table.monthly][view] : [];
567
+ const tableData = sortRows(rawTableData, sort);
558
568
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, minHeight: rows, children: [
559
569
  /* @__PURE__ */ jsxs(Box, { justifyContent: "space-between", children: [
560
570
  /* @__PURE__ */ jsxs(Box, { children: [
@@ -578,18 +588,21 @@ function App({ interval: cliInterval }) {
578
588
  /* @__PURE__ */ jsx(Box, { height: 1 }),
579
589
  tab === 0 && /* @__PURE__ */ jsx(DashboardView, { data: dashboard, billing }),
580
590
  tab === 1 && /* @__PURE__ */ jsxs(Fragment, { children: [
581
- /* @__PURE__ */ jsx(ViewBar, { views: VIEWS, active: view }),
591
+ /* @__PURE__ */ jsx(ViewBar, { views: VIEWS, active: view, sort: SORTS[sort] }),
582
592
  /* @__PURE__ */ jsx(Box, { height: 1 }),
583
593
  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
594
  ] })
585
595
  ] }),
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
- ] })
596
+ (tab === 0 || showSettings) && /* @__PURE__ */ jsx(Footer, {})
597
+ ] });
598
+ }
599
+ function Footer() {
600
+ return /* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
601
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "by " }),
602
+ /* @__PURE__ */ jsx(Text, { children: "David Ilie" }),
603
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " (" }),
604
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: "davidilie.com" }),
605
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: ") \xB7 s=settings q=quit" })
593
606
  ] });
594
607
  }
595
608
  function TabBar({ tabs, active }) {
@@ -603,16 +616,34 @@ function TabBar({ tabs, active }) {
603
616
  " "
604
617
  ] }) }, t)) });
605
618
  }
606
- function ViewBar({ views, active }) {
619
+ function ViewBar({ views, active, sort }) {
607
620
  return /* @__PURE__ */ jsxs(Box, { children: [
608
621
  views.map((v, i) => /* @__PURE__ */ jsx(Box, { marginRight: 2, children: i === active ? /* @__PURE__ */ jsxs(Text, { bold: true, color: "cyan", children: [
609
622
  "[",
610
623
  v,
611
624
  "]"
612
625
  ] }) : /* @__PURE__ */ jsx(Text, { dimColor: true, children: v }) }, v)),
613
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " d/w/m or \u2190\u2192" })
626
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " d/w/m \xB7 sort: " }),
627
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "magenta", children: sort }),
628
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " o=cycle" })
614
629
  ] });
615
630
  }
631
+ function sortRows(rows, sortIdx) {
632
+ if (rows.length === 0) return rows;
633
+ const sorted = [...rows];
634
+ switch (sortIdx) {
635
+ case 0:
636
+ return sorted.sort((a, b) => a.label.localeCompare(b.label));
637
+ case 1:
638
+ return sorted.sort((a, b) => b.label.localeCompare(a.label));
639
+ case 2:
640
+ return sorted.sort((a, b) => a.cost - b.cost);
641
+ case 3:
642
+ return sorted.sort((a, b) => b.cost - a.cost);
643
+ default:
644
+ return sorted;
645
+ }
646
+ }
616
647
  function SettingsView({ config: config2, cursor }) {
617
648
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
618
649
  /* @__PURE__ */ jsx(Text, { bold: true, children: "Settings" }),
@@ -809,26 +840,18 @@ function TableView({ rows: allRows, cursor, expanded, maxRows, wide }) {
809
840
  ] }),
810
841
  /* @__PURE__ */ jsx(Box, { height: 1 }),
811
842
  /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
812
- "\u2191\u2193 navigate \xB7 Enter detail \xB7 g top G bottom \xB7 ",
843
+ "\u2191\u2193 navigate \xB7 Enter detail \xB7 o sort \xB7 g/G top/bottom \xB7 ",
813
844
  clampedCursor + 1,
814
845
  "/",
815
846
  allRows.length
816
847
  ] }),
817
848
  /* @__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
- ] })
849
+ /* @__PURE__ */ jsx(Footer, {})
825
850
  ] });
826
851
  }
827
852
  function RowDetail({ row, indent }) {
828
- const pad = " ".repeat(indent);
829
853
  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";
854
+ const prefix = i === row.breakdown.length - 1 ? "\u2514\u2500" : "\u251C\u2500";
832
855
  return /* @__PURE__ */ jsxs(Text, { children: [
833
856
  /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
834
857
  prefix,
@@ -856,15 +879,14 @@ function RowDetail({ row, indent }) {
856
879
  }) });
857
880
  }
858
881
  function Spinner({ label }) {
859
- const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
860
882
  const [i, setI] = useState(0);
861
883
  useEffect(() => {
862
- const id = setInterval(() => setI((n) => (n + 1) % frames.length), 80);
884
+ const id = setInterval(() => setI((n) => (n + 1) % SPINNER_FRAMES.length), 80);
863
885
  return () => clearInterval(id);
864
886
  }, []);
865
887
  return /* @__PURE__ */ jsxs(Box, { children: [
866
888
  /* @__PURE__ */ jsxs(Text, { color: "green", children: [
867
- frames[i],
889
+ SPINNER_FRAMES[i],
868
890
  " "
869
891
  ] }),
870
892
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: label })
@@ -873,9 +895,8 @@ function Spinner({ label }) {
873
895
  function fmtLabel(label) {
874
896
  if (label.length === 10 && label[4] === "-") return shortDate(label);
875
897
  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)}`;
898
+ const m = label.slice(5, 7);
899
+ return `${MONTHS[Number(m)]} '${label.slice(2, 4)}`;
879
900
  }
880
901
  return shortDate(label);
881
902
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokmon",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "description": "Terminal dashboard for Claude Code usage and costs",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,6 +18,7 @@
18
18
  "react": "^18.0.0"
19
19
  },
20
20
  "devDependencies": {
21
+ "@types/node": "^25.5.2",
21
22
  "@types/react": "^18.0.0",
22
23
  "tsup": "^8.0.0",
23
24
  "tsx": "^4.0.0",