tokmon 0.1.0 → 0.3.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 +51 -17
  2. package/dist/cli.js +435 -77
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,52 +1,86 @@
1
1
  # tokmon
2
2
 
3
- Terminal dashboard for Claude Code usage and costs. Refreshes every 2 seconds like `watch`.
3
+ Terminal dashboard for Claude Code usage and costs. Tabbed interface with auto-refresh.
4
4
 
5
5
  Built with [Ink](https://github.com/vadimdemedes/ink), TypeScript.
6
6
 
7
7
  ```
8
- ◉ tokmon · refreshing every 2s
8
+ ◉ tokmon · 2s 01:17:09 AM
9
+
10
+ Dashboard Daily ←→ or 1-2
9
11
 
10
12
  ┃ Claude
11
13
 
12
- ┃ Today $122.78 179.9M tokens
13
- ┃ This Week $356.47 535.4M tokens
14
- ┃ This Month $1293.71 2.1B tokens
14
+ ┃ Today $166.10 252.7M tokens
15
+ ┃ This Week $399.79 608.2M tokens
16
+ ┃ This Month $1337.03 2.2B tokens
15
17
 
16
- ┃ Active Block 35m remaining
18
+ ┃ Active Block 12m remaining
17
19
 
18
- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━──── 88%
20
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━─ 96%
19
21
 
20
- ┃ $271.05 spent · ~$306.63 proj · $61.33/hr
22
+ ┃ $314.37 spent · ~$328.01 proj · $65.60/hr
21
23
 
22
24
  ──────────────────────────────────────────────────
23
- Total $1293.71 12:54:49 AM
25
+ Total $1337.03
26
+
27
+ by David Ilie (davidilie.com)
28
+ ```
29
+
30
+ ## Quick Start
31
+
32
+ ```bash
33
+ npx tokmon
34
+ ```
35
+
36
+ Or with pnpm:
37
+
38
+ ```bash
39
+ pnpm dlx tokmon
24
40
  ```
25
41
 
26
- ## Install
42
+ ### Global Install
27
43
 
28
44
  ```bash
29
45
  npm install -g tokmon
30
46
  ```
31
47
 
32
- ## Usage
48
+ Then just run `tokmon`. Press `Ctrl+C` to exit.
49
+
50
+ ## Options
51
+
52
+ ```
53
+ -i, --interval <seconds> Refresh interval in seconds (default: 2)
54
+ -h, --help Show help
55
+ ```
33
56
 
34
57
  ```bash
35
- tokmon
58
+ tokmon -i 5 # refresh every 5 seconds
36
59
  ```
37
60
 
38
- Press `Ctrl+C` to exit.
61
+ ## Views
39
62
 
40
- ## What It Shows
63
+ Navigate between views with `←` `→` arrow keys, `Tab`, or number keys `1` `2`.
41
64
 
42
- - **Today / This Week / This Month** — cost and token totals from Claude Code JSONL logs
43
- - **Active Block** — current 5-hour window with burn rate, projected cost, and time remaining
44
- - Auto-refreshes every 2 seconds with mtime-based file caching
65
+ | View | Description |
66
+ |------|-------------|
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 `↑` `↓`) |
45
69
 
46
70
  ## How It Works
47
71
 
48
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.
49
73
 
74
+ Cross-platform: supports macOS, Linux, and Windows (`%APPDATA%`, `XDG_CONFIG_HOME`, `CLAUDE_CONFIG_DIR`).
75
+
76
+ ## CI/CD
77
+
78
+ Publishes to npm automatically via GitHub Actions when a version tag is pushed:
79
+
80
+ ```bash
81
+ git tag v0.2.0 && git push --tags
82
+ ```
83
+
50
84
  ## Requirements
51
85
 
52
86
  - Node.js 20+
package/dist/cli.js CHANGED
@@ -5,7 +5,7 @@ import { render } from "ink";
5
5
 
6
6
  // src/app.tsx
7
7
  import { useState, useEffect } from "react";
8
- import { Box, Text } from "ink";
8
+ import { Box, Text, useInput, useStdout } from "ink";
9
9
 
10
10
  // src/data.ts
11
11
  import { readdir, stat as fsStat } from "fs/promises";
@@ -36,6 +36,16 @@ function minutes(mins) {
36
36
  const m = Math.round(mins % 60);
37
37
  return h > 0 ? `${h}h ${m}m` : `${m}m`;
38
38
  }
39
+ function shortDate(iso) {
40
+ const [, m, d] = iso.split("-");
41
+ const months = ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
42
+ return `${months[Number(m)]} ${Number(d).toString().padStart(2, " ")}`;
43
+ }
44
+ function col(s, w, align = "right") {
45
+ if (s.length > w) return s.slice(0, w - 1) + "~";
46
+ const spaces = " ".repeat(w - s.length);
47
+ return align === "right" ? spaces + s : s + spaces;
48
+ }
39
49
 
40
50
  // src/data.ts
41
51
  var PRICING = {
@@ -73,6 +83,9 @@ function costOf(model, u) {
73
83
  const p = priceFor(model);
74
84
  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;
75
85
  }
86
+ function shortModel(model) {
87
+ return model.replace("claude-", "").replace("-20251001", "").replace("-20250514", "").replace("-20251101", "").replace("-20250805", "");
88
+ }
76
89
  async function parseFile(path, since) {
77
90
  const entries = [];
78
91
  const rl = createInterface({ input: createReadStream(path), crlfDelay: Infinity });
@@ -86,8 +99,12 @@ async function parseFile(path, since) {
86
99
  const u = obj.message.usage;
87
100
  entries.push({
88
101
  ts,
102
+ model: obj.message.model ?? "unknown",
89
103
  cost: costOf(obj.message.model ?? "", u),
90
- tokens: (u.input_tokens ?? 0) + (u.output_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0) + (u.cache_read_input_tokens ?? 0)
104
+ input: u.input_tokens ?? 0,
105
+ output: u.output_tokens ?? 0,
106
+ cacheCreate: u.cache_creation_input_tokens ?? 0,
107
+ cacheRead: u.cache_read_input_tokens ?? 0
91
108
  });
92
109
  } catch {
93
110
  }
@@ -130,18 +147,62 @@ function sum(entries) {
130
147
  let cost = 0, tokens2 = 0;
131
148
  for (const e of entries) {
132
149
  cost += e.cost;
133
- tokens2 += e.tokens;
150
+ tokens2 += e.input + e.output + e.cacheCreate + e.cacheRead;
134
151
  }
135
152
  return { cost, tokens: tokens2 };
136
153
  }
137
- async function fetchUsage() {
154
+ function groupBy(entries, keyFn) {
155
+ const groups = /* @__PURE__ */ new Map();
156
+ for (const e of entries) {
157
+ const key = keyFn(e);
158
+ const arr = groups.get(key);
159
+ if (arr) arr.push(e);
160
+ else groups.set(key, [e]);
161
+ }
162
+ const rows = [];
163
+ for (const [label, group] of groups) {
164
+ const models = [...new Set(group.map((e) => shortModel(e.model)))];
165
+ let input = 0, output = 0, cacheCreate = 0, cacheRead = 0, cost = 0;
166
+ for (const e of group) {
167
+ input += e.input;
168
+ output += e.output;
169
+ cacheCreate += e.cacheCreate;
170
+ cacheRead += e.cacheRead;
171
+ cost += e.cost;
172
+ }
173
+ rows.push({
174
+ label,
175
+ models: models.sort(),
176
+ input,
177
+ output,
178
+ cacheCreate,
179
+ cacheRead,
180
+ total: input + output + cacheCreate + cacheRead,
181
+ cost
182
+ });
183
+ }
184
+ return rows.sort((a, b) => a.label.localeCompare(b.label));
185
+ }
186
+ function isoWeekLabel(ts) {
187
+ const d = new Date(ts);
188
+ const day = d.getDay();
189
+ const mondayOffset = day === 0 ? 6 : day - 1;
190
+ const monday = new Date(d);
191
+ monday.setDate(d.getDate() - mondayOffset);
192
+ return monday.toISOString().slice(0, 10);
193
+ }
194
+ function monthLabel(ts) {
195
+ return new Date(ts).toISOString().slice(0, 7);
196
+ }
197
+ async function fetchData() {
138
198
  const now = Date.now();
139
199
  const d = /* @__PURE__ */ new Date();
200
+ const lookback = new Date(d.getFullYear(), d.getMonth() - 6, 1).getTime();
140
201
  const monthStart = new Date(d.getFullYear(), d.getMonth(), 1).getTime();
141
202
  const todayStart = new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
142
203
  const weekDay = d.getDay();
143
204
  const weekStart = new Date(d.getFullYear(), d.getMonth(), d.getDate() - (weekDay === 0 ? 6 : weekDay - 1)).getTime();
144
- const entries = await loadEntries(monthStart);
205
+ const entries = await loadEntries(lookback);
145
206
  const fiveHoursAgo = now - 5 * 36e5;
146
207
  const blockEntries = entries.filter((e) => e.ts >= fiveHoursAgo);
147
208
  let block = null;
@@ -154,26 +215,173 @@ async function fetchUsage() {
154
215
  const percent = Math.min(100, (now - oldest) / (5 * 36e5) * 100);
155
216
  block = { spent, projected: burnRate * 5, burnRate, percent, remaining: minutes(remainMs / 6e4) };
156
217
  }
218
+ const daily = groupBy(entries, (e) => new Date(e.ts).toISOString().slice(0, 10));
219
+ const weekly = groupBy(entries, (e) => isoWeekLabel(e.ts));
220
+ const monthly = groupBy(entries, (e) => monthLabel(e.ts));
157
221
  return {
158
222
  today: sum(entries.filter((e) => e.ts >= todayStart)),
159
223
  week: sum(entries.filter((e) => e.ts >= weekStart)),
160
- month: sum(entries),
161
- block
224
+ month: sum(entries.filter((e) => e.ts >= monthStart)),
225
+ block,
226
+ daily,
227
+ weekly,
228
+ monthly
162
229
  };
163
230
  }
164
231
 
232
+ // src/config.ts
233
+ import { readFile, writeFile, mkdir } from "fs/promises";
234
+ import { join as join2 } from "path";
235
+ import { homedir as homedir2 } from "os";
236
+ var DEFAULTS = { interval: 2, clearScreen: true };
237
+ function configDir() {
238
+ if (process.platform === "win32") {
239
+ return join2(process.env.APPDATA ?? join2(homedir2(), "AppData", "Roaming"), "tokmon");
240
+ }
241
+ const xdg = process.env.XDG_CONFIG_HOME ?? join2(homedir2(), ".config");
242
+ return join2(xdg, "tokmon");
243
+ }
244
+ function configPath() {
245
+ return join2(configDir(), "config.json");
246
+ }
247
+ async function loadConfig() {
248
+ try {
249
+ const raw = await readFile(configPath(), "utf-8");
250
+ const parsed = JSON.parse(raw);
251
+ return { ...DEFAULTS, ...parsed };
252
+ } catch {
253
+ return { ...DEFAULTS };
254
+ }
255
+ }
256
+ async function saveConfig(config) {
257
+ const dir = configDir();
258
+ await mkdir(dir, { recursive: true });
259
+ await writeFile(configPath(), JSON.stringify(config, null, 2) + "\n");
260
+ }
261
+ function configLocation() {
262
+ return configPath();
263
+ }
264
+
165
265
  // src/app.tsx
166
266
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
167
- var INTERVAL = 2e3;
168
- function App() {
267
+ var TABS = ["Dashboard", "Table"];
268
+ var VIEWS = ["Daily", "Weekly", "Monthly"];
269
+ function App({ interval: cliInterval }) {
169
270
  const [data, setData] = useState(null);
170
271
  const [error, setError] = useState(null);
171
272
  const [updated, setUpdated] = useState(/* @__PURE__ */ new Date());
273
+ const [tab, setTab] = useState(0);
274
+ const [view, setView] = useState(0);
275
+ const [scroll, setScroll] = useState(0);
276
+ const [showSettings, setShowSettings] = useState(false);
277
+ const [config, setConfig] = useState(null);
278
+ const [settingsCursor, setSettingsCursor] = useState(0);
279
+ const { stdout } = useStdout();
280
+ const rows = stdout?.rows ?? 24;
281
+ const cols = stdout?.columns ?? 80;
282
+ const interval2 = cliInterval ?? (config?.interval ?? 2) * 1e3;
283
+ useEffect(() => {
284
+ loadConfig().then((c) => {
285
+ if (cliInterval) c = { ...c, interval: cliInterval / 1e3 };
286
+ setConfig(c);
287
+ if (c.clearScreen && stdout) stdout.write("\x1B[2J\x1B[H");
288
+ });
289
+ }, []);
290
+ const isTTY = process.stdin.isTTY === true;
291
+ const settingsItems = 2;
292
+ const cfg = config ?? { interval: 2, clearScreen: true };
293
+ useInput((input, key) => {
294
+ if (showSettings) {
295
+ if (key.escape || input === "s") setShowSettings(false);
296
+ if (key.upArrow) setSettingsCursor((c) => Math.max(0, c - 1));
297
+ if (key.downArrow) setSettingsCursor((c) => Math.min(settingsItems - 1, c + 1));
298
+ if (settingsCursor === 0) {
299
+ if (key.leftArrow) {
300
+ setConfig((c) => {
301
+ const next = { ...c, interval: Math.max(1, c.interval - 1) };
302
+ saveConfig(next);
303
+ return next;
304
+ });
305
+ }
306
+ if (key.rightArrow) {
307
+ setConfig((c) => {
308
+ const next = { ...c, interval: c.interval + 1 };
309
+ saveConfig(next);
310
+ return next;
311
+ });
312
+ }
313
+ }
314
+ if (settingsCursor === 1 && (key.leftArrow || key.rightArrow || key.return)) {
315
+ setConfig((c) => {
316
+ const next = { ...c, clearScreen: !c.clearScreen };
317
+ saveConfig(next);
318
+ return next;
319
+ });
320
+ }
321
+ return;
322
+ }
323
+ if (input === "s") {
324
+ setShowSettings(true);
325
+ return;
326
+ }
327
+ if (key.tab) {
328
+ setTab((t) => (t + 1) % TABS.length);
329
+ setScroll(0);
330
+ return;
331
+ }
332
+ if (input === "1") {
333
+ setTab(0);
334
+ setScroll(0);
335
+ return;
336
+ }
337
+ if (input === "2") {
338
+ setTab(1);
339
+ setScroll(0);
340
+ return;
341
+ }
342
+ if (tab === 1) {
343
+ if (input === "d") {
344
+ setView(0);
345
+ setScroll(0);
346
+ return;
347
+ }
348
+ if (input === "w") {
349
+ setView(1);
350
+ setScroll(0);
351
+ return;
352
+ }
353
+ if (input === "m") {
354
+ setView(2);
355
+ setScroll(0);
356
+ return;
357
+ }
358
+ if (key.leftArrow) {
359
+ setView((v) => (v - 1 + VIEWS.length) % VIEWS.length);
360
+ setScroll(0);
361
+ return;
362
+ }
363
+ if (key.rightArrow) {
364
+ setView((v) => (v + 1) % VIEWS.length);
365
+ setScroll(0);
366
+ return;
367
+ }
368
+ } else {
369
+ if (key.leftArrow || key.rightArrow) {
370
+ setTab((t) => (t + 1) % TABS.length);
371
+ setScroll(0);
372
+ return;
373
+ }
374
+ }
375
+ if (key.upArrow) setScroll((s) => Math.max(0, s - 1));
376
+ if (key.downArrow) setScroll((s) => s + 1);
377
+ if (key.pageDown) setScroll((s) => s + Math.max(1, rows - 12));
378
+ if (key.pageUp) setScroll((s) => Math.max(0, s - Math.max(1, rows - 12)));
379
+ }, { isActive: isTTY });
172
380
  useEffect(() => {
173
381
  let active = true;
174
382
  const load = async () => {
175
383
  try {
176
- const result = await fetchUsage();
384
+ const result = await fetchData();
177
385
  if (active) {
178
386
  setData(result);
179
387
  setError(null);
@@ -184,62 +392,143 @@ function App() {
184
392
  }
185
393
  };
186
394
  load();
187
- const id = setInterval(load, INTERVAL);
395
+ const id = setInterval(load, interval2);
188
396
  return () => {
189
397
  active = false;
190
398
  clearInterval(id);
191
399
  };
192
- }, []);
400
+ }, [interval2]);
193
401
  if (error) return /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { color: "red", children: error }) });
194
402
  if (!data) return /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Loading..." }) });
403
+ const tableData = [data.daily, data.weekly, data.monthly][view];
195
404
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [
196
- /* @__PURE__ */ jsx(Header, {}),
197
- /* @__PURE__ */ jsx(Box, { height: 1 }),
198
- /* @__PURE__ */ jsx(UsageSection, { color: "green", title: "Claude", data }),
199
- data.block && /* @__PURE__ */ jsxs(Fragment, { children: [
405
+ /* @__PURE__ */ jsxs(Box, { justifyContent: "space-between", children: [
406
+ /* @__PURE__ */ jsxs(Box, { children: [
407
+ /* @__PURE__ */ jsxs(Text, { bold: true, color: "greenBright", children: [
408
+ "\u25C9",
409
+ " tokmon"
410
+ ] }),
411
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
412
+ " \xB7 ",
413
+ cliInterval ? cliInterval / 1e3 : cfg.interval,
414
+ "s"
415
+ ] })
416
+ ] }),
417
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: time(updated) })
418
+ ] }),
419
+ showSettings ? /* @__PURE__ */ jsx(SettingsView, { config: cfg, cursor: settingsCursor }) : /* @__PURE__ */ jsxs(Fragment, { children: [
420
+ /* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
421
+ /* @__PURE__ */ jsx(TabBar, { tabs: TABS, active: tab }),
422
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " Tab s=settings" })
423
+ ] }),
200
424
  /* @__PURE__ */ jsx(Box, { height: 1 }),
201
- /* @__PURE__ */ jsx(BlockSection, { block: data.block })
425
+ tab === 0 && /* @__PURE__ */ jsx(DashboardView, { data }),
426
+ tab === 1 && /* @__PURE__ */ jsxs(Fragment, { children: [
427
+ /* @__PURE__ */ jsx(ViewBar, { views: VIEWS, active: view }),
428
+ /* @__PURE__ */ jsx(Box, { height: 1 }),
429
+ /* @__PURE__ */ jsx(TableView, { rows: tableData, scroll, maxRows: rows - 12, wide: cols > 90 })
430
+ ] })
202
431
  ] }),
203
- /* @__PURE__ */ jsx(Box, { height: 1 }),
204
- /* @__PURE__ */ jsx(Divider, {}),
205
- /* @__PURE__ */ jsx(Footer, { cost: data.month.cost, updated })
432
+ /* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
433
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "by " }),
434
+ /* @__PURE__ */ jsx(Text, { children: "David Ilie" }),
435
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " (" }),
436
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: "davidilie.com" }),
437
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: ")" })
438
+ ] })
206
439
  ] });
207
440
  }
208
- function Header() {
441
+ function TabBar({ tabs, active }) {
442
+ return /* @__PURE__ */ jsx(Box, { children: tabs.map((t, i) => /* @__PURE__ */ jsx(Box, { marginRight: 1, children: i === active ? /* @__PURE__ */ jsxs(Text, { bold: true, inverse: true, children: [
443
+ " ",
444
+ t,
445
+ " "
446
+ ] }) : /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
447
+ " ",
448
+ t,
449
+ " "
450
+ ] }) }, t)) });
451
+ }
452
+ function ViewBar({ views, active }) {
209
453
  return /* @__PURE__ */ jsxs(Box, { children: [
210
- /* @__PURE__ */ jsxs(Text, { bold: true, color: "greenBright", children: [
211
- "\u25C9",
212
- " tokmon"
454
+ views.map((v, i) => /* @__PURE__ */ jsx(Box, { marginRight: 2, children: i === active ? /* @__PURE__ */ jsxs(Text, { bold: true, color: "cyan", children: [
455
+ "[",
456
+ v,
457
+ "]"
458
+ ] }) : /* @__PURE__ */ jsx(Text, { dimColor: true, children: v }) }, v)),
459
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " d/w/m or \u2190\u2192" })
460
+ ] });
461
+ }
462
+ function SettingsView({ config, cursor }) {
463
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
464
+ /* @__PURE__ */ jsx(Text, { bold: true, children: "Settings" }),
465
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: configLocation() }),
466
+ /* @__PURE__ */ jsx(Box, { height: 1 }),
467
+ /* @__PURE__ */ jsxs(Box, { children: [
468
+ /* @__PURE__ */ jsxs(Text, { color: cursor === 0 ? "green" : void 0, children: [
469
+ cursor === 0 ? "\u25B8" : " ",
470
+ " "
471
+ ] }),
472
+ /* @__PURE__ */ jsx(Text, { children: "Refresh interval " }),
473
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
474
+ "\u25C2",
475
+ " "
476
+ ] }),
477
+ /* @__PURE__ */ jsxs(Text, { bold: true, color: "yellow", children: [
478
+ config.interval,
479
+ "s"
480
+ ] }),
481
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
482
+ " ",
483
+ "\u25B8"
484
+ ] })
213
485
  ] }),
214
- /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
215
- " \xB7 refreshing every ",
216
- INTERVAL / 1e3,
217
- "s"
218
- ] })
486
+ /* @__PURE__ */ jsxs(Box, { children: [
487
+ /* @__PURE__ */ jsxs(Text, { color: cursor === 1 ? "green" : void 0, children: [
488
+ cursor === 1 ? "\u25B8" : " ",
489
+ " "
490
+ ] }),
491
+ /* @__PURE__ */ jsx(Text, { children: "Clear screen " }),
492
+ /* @__PURE__ */ jsx(Text, { bold: true, color: config.clearScreen ? "green" : "red", children: config.clearScreen ? "on" : "off" })
493
+ ] }),
494
+ /* @__PURE__ */ jsx(Box, { height: 1 }),
495
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2191\u2193 select \u2190\u2192 adjust s/Esc close" })
219
496
  ] });
220
497
  }
221
- function UsageSection({ color, title, data }) {
222
- return /* @__PURE__ */ jsxs(
223
- Box,
224
- {
225
- flexDirection: "column",
226
- paddingLeft: 1,
227
- borderStyle: "bold",
228
- borderColor: color,
229
- borderRight: false,
230
- borderTop: false,
231
- borderBottom: false,
232
- children: [
233
- /* @__PURE__ */ jsx(Text, { bold: true, children: title }),
234
- /* @__PURE__ */ jsx(Box, { height: 1 }),
235
- /* @__PURE__ */ jsx(Row, { label: "Today", summary: data.today }),
236
- /* @__PURE__ */ jsx(Row, { label: "This Week", summary: data.week }),
237
- /* @__PURE__ */ jsx(Row, { label: "This Month", summary: data.month })
238
- ]
239
- }
240
- );
498
+ function DashboardView({ data }) {
499
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
500
+ /* @__PURE__ */ jsxs(
501
+ Box,
502
+ {
503
+ flexDirection: "column",
504
+ paddingLeft: 1,
505
+ borderStyle: "bold",
506
+ borderColor: "green",
507
+ borderRight: false,
508
+ borderTop: false,
509
+ borderBottom: false,
510
+ children: [
511
+ /* @__PURE__ */ jsx(Text, { bold: true, children: "Claude" }),
512
+ /* @__PURE__ */ jsx(Box, { height: 1 }),
513
+ /* @__PURE__ */ jsx(SummaryRow, { label: "Today", summary: data.today }),
514
+ /* @__PURE__ */ jsx(SummaryRow, { label: "This Week", summary: data.week }),
515
+ /* @__PURE__ */ jsx(SummaryRow, { label: "This Month", summary: data.month })
516
+ ]
517
+ }
518
+ ),
519
+ data.block && /* @__PURE__ */ jsxs(Fragment, { children: [
520
+ /* @__PURE__ */ jsx(Box, { height: 1 }),
521
+ /* @__PURE__ */ jsx(BlockView, { block: data.block })
522
+ ] }),
523
+ /* @__PURE__ */ jsx(Box, { height: 1 }),
524
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(50) }),
525
+ /* @__PURE__ */ jsxs(Box, { width: 50, children: [
526
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Total " }),
527
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "yellowBright", children: currency(data.month.cost) })
528
+ ] })
529
+ ] });
241
530
  }
242
- function BlockSection({ block }) {
531
+ function BlockView({ block }) {
243
532
  return /* @__PURE__ */ jsxs(
244
533
  Box,
245
534
  {
@@ -261,7 +550,7 @@ function BlockSection({ block }) {
261
550
  ] }),
262
551
  /* @__PURE__ */ jsx(Box, { height: 1 }),
263
552
  /* @__PURE__ */ jsxs(Box, { children: [
264
- /* @__PURE__ */ jsx(Bar, { percent: block.percent, width: 36 }),
553
+ /* @__PURE__ */ jsx(ProgressBar, { percent: block.percent, width: 36 }),
265
554
  /* @__PURE__ */ jsx(Text, { children: " " }),
266
555
  /* @__PURE__ */ jsxs(Text, { bold: true, children: [
267
556
  Math.round(block.percent),
@@ -270,12 +559,9 @@ function BlockSection({ block }) {
270
559
  ] }),
271
560
  /* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
272
561
  /* @__PURE__ */ jsx(Text, { color: "yellow", children: currency(block.spent) }),
273
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " spent" }),
274
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \xB7 " }),
275
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "~" }),
562
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " spent \xB7 ~" }),
276
563
  /* @__PURE__ */ jsx(Text, { children: currency(block.projected) }),
277
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " proj" }),
278
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \xB7 " }),
564
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " proj \xB7 " }),
279
565
  /* @__PURE__ */ jsx(Text, { color: "red", children: currency(block.burnRate) }),
280
566
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "/hr" })
281
567
  ] })
@@ -283,7 +569,7 @@ function BlockSection({ block }) {
283
569
  }
284
570
  );
285
571
  }
286
- function Row({ label, summary }) {
572
+ function SummaryRow({ label, summary }) {
287
573
  return /* @__PURE__ */ jsxs(Box, { children: [
288
574
  /* @__PURE__ */ jsx(Box, { width: 14, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: label }) }),
289
575
  /* @__PURE__ */ jsx(Box, { width: 12, justifyContent: "flex-end", children: /* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: currency(summary.cost) }) }),
@@ -293,36 +579,108 @@ function Row({ label, summary }) {
293
579
  ] }) })
294
580
  ] });
295
581
  }
296
- function Bar({ percent, width = 36 }) {
582
+ function ProgressBar({ percent, width = 36 }) {
297
583
  const filled = Math.round(percent / 100 * width);
298
584
  return /* @__PURE__ */ jsxs(Text, { children: [
299
585
  /* @__PURE__ */ jsx(Text, { color: "greenBright", children: "\u2501".repeat(filled) }),
300
586
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(width - filled) })
301
587
  ] });
302
588
  }
303
- function Divider() {
304
- return /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(50) });
305
- }
306
- function Footer({ cost, updated }) {
307
- return /* @__PURE__ */ jsxs(Fragment, { children: [
308
- /* @__PURE__ */ jsxs(Box, { justifyContent: "space-between", width: 50, children: [
309
- /* @__PURE__ */ jsxs(Box, { children: [
310
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Total " }),
311
- /* @__PURE__ */ jsx(Text, { bold: true, color: "yellowBright", children: currency(cost) })
312
- ] }),
313
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: time(updated) })
589
+ function TableView({ rows: allRows, scroll, maxRows, wide }) {
590
+ 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 };
591
+ const lineW = W.label + W.models + W.input + W.output + W.cc + W.cr + W.total + W.cost;
592
+ const totals = { input: 0, output: 0, cacheCreate: 0, cacheRead: 0, cost: 0 };
593
+ for (const r of allRows) {
594
+ totals.input += r.input;
595
+ totals.output += r.output;
596
+ totals.cacheCreate += r.cacheCreate;
597
+ totals.cacheRead += r.cacheRead;
598
+ totals.cost += r.cost;
599
+ }
600
+ const clampedScroll = Math.min(scroll, Math.max(0, allRows.length - maxRows));
601
+ const visible = allRows.slice(clampedScroll, clampedScroll + maxRows);
602
+ const more = allRows.length - clampedScroll - maxRows;
603
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
604
+ /* @__PURE__ */ jsxs(Text, { children: [
605
+ /* @__PURE__ */ jsx(Text, { bold: true, children: col("Date", W.label, "left") }),
606
+ /* @__PURE__ */ jsx(Text, { bold: true, children: col("Models", W.models, "left") }),
607
+ /* @__PURE__ */ jsx(Text, { bold: true, children: col("Input", W.input) }),
608
+ /* @__PURE__ */ jsx(Text, { bold: true, children: col("Output", W.output) }),
609
+ /* @__PURE__ */ jsx(Text, { bold: true, children: col("CchCrt", W.cc) }),
610
+ /* @__PURE__ */ jsx(Text, { bold: true, children: col("CchRd", W.cr) }),
611
+ W.total > 0 && /* @__PURE__ */ jsx(Text, { bold: true, children: col("Total", W.total) }),
612
+ /* @__PURE__ */ jsx(Text, { bold: true, children: col("Cost", W.cost) })
314
613
  ] }),
315
- /* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
316
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "by " }),
317
- /* @__PURE__ */ jsx(Text, { children: "David Ilie" }),
318
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " (" }),
319
- /* @__PURE__ */ jsx(Text, { color: "cyan", children: "davidilie.com" }),
320
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: ")" })
321
- ] })
614
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(lineW) }),
615
+ visible.map((r) => /* @__PURE__ */ jsxs(Text, { children: [
616
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: col(fmtLabel(r.label), W.label, "left") }),
617
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: col(r.models.join(", "), W.models, "left") }),
618
+ /* @__PURE__ */ jsx(Text, { children: col(tokens(r.input), W.input) }),
619
+ /* @__PURE__ */ jsx(Text, { children: col(tokens(r.output), W.output) }),
620
+ /* @__PURE__ */ jsx(Text, { children: col(tokens(r.cacheCreate), W.cc) }),
621
+ /* @__PURE__ */ jsx(Text, { children: col(tokens(r.cacheRead), W.cr) }),
622
+ W.total > 0 && /* @__PURE__ */ jsx(Text, { children: col(tokens(r.total), W.total) }),
623
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: col(currency(r.cost), W.cost) })
624
+ ] }, r.label)),
625
+ more > 0 && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
626
+ " \u2193 ",
627
+ more,
628
+ " more"
629
+ ] }),
630
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(lineW) }),
631
+ /* @__PURE__ */ jsxs(Text, { children: [
632
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "greenBright", children: col("Total", W.label, "left") }),
633
+ /* @__PURE__ */ jsx(Text, { children: col("", W.models, "left") }),
634
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: col(tokens(totals.input), W.input) }),
635
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: col(tokens(totals.output), W.output) }),
636
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: col(tokens(totals.cacheCreate), W.cc) }),
637
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: col(tokens(totals.cacheRead), W.cr) }),
638
+ W.total > 0 && /* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: col(tokens(totals.input + totals.output + totals.cacheCreate + totals.cacheRead), W.total) }),
639
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "yellowBright", children: col(currency(totals.cost), W.cost) })
640
+ ] }),
641
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
642
+ "\u2191\u2193 PgUp/Dn scroll \xB7 ",
643
+ allRows.length,
644
+ " rows \xB7 ",
645
+ clampedScroll + 1,
646
+ "-",
647
+ Math.min(clampedScroll + maxRows, allRows.length)
648
+ ] }) })
322
649
  ] });
323
650
  }
651
+ function fmtLabel(label) {
652
+ if (label.length === 10 && label[4] === "-") return shortDate(label);
653
+ if (label.length === 7 && label[4] === "-") {
654
+ const [, m] = label.split("-");
655
+ const months = ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
656
+ return `${months[Number(m)]} '${label.slice(2, 4)}`;
657
+ }
658
+ return shortDate(label);
659
+ }
324
660
 
325
661
  // src/cli.tsx
326
662
  import { jsx as jsx2 } from "react/jsx-runtime";
327
- var { waitUntilExit } = render(/* @__PURE__ */ jsx2(App, {}));
663
+ var args = process.argv.slice(2);
664
+ var interval;
665
+ for (let i = 0; i < args.length; i++) {
666
+ if ((args[i] === "--interval" || args[i] === "-i") && args[i + 1]) {
667
+ interval = Math.max(500, Number(args[i + 1]) * 1e3);
668
+ i++;
669
+ }
670
+ if (args[i] === "--help" || args[i] === "-h") {
671
+ console.log("tokmon - Terminal dashboard for Claude Code usage\n");
672
+ console.log("Usage: tokmon [options]\n");
673
+ console.log("Options:");
674
+ console.log(" -i, --interval <seconds> Refresh interval (default: from config or 2)");
675
+ console.log(" -h, --help Show this help\n");
676
+ console.log("Keybindings:");
677
+ console.log(" Tab / \u2190\u2192 Switch views");
678
+ console.log(" \u2191\u2193 Scroll table");
679
+ console.log(" 1-2 Jump to view");
680
+ console.log(" s Settings");
681
+ console.log(" Ctrl+C Quit");
682
+ process.exit(0);
683
+ }
684
+ }
685
+ var { waitUntilExit } = render(/* @__PURE__ */ jsx2(App, { interval }));
328
686
  await waitUntilExit();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokmon",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Terminal dashboard for Claude Code usage and costs",
5
5
  "type": "module",
6
6
  "bin": {