tokmon 0.1.0 → 0.2.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 +338 -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 });
@@ -84,10 +97,15 @@ async function parseFile(path, since) {
84
97
  const ts = new Date(obj.timestamp ?? 0).getTime();
85
98
  if (ts < since) continue;
86
99
  const u = obj.message.usage;
100
+ const model = obj.message.model ?? "unknown";
87
101
  entries.push({
88
102
  ts,
89
- 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)
103
+ model,
104
+ cost: costOf(model, u),
105
+ input: u.input_tokens ?? 0,
106
+ output: u.output_tokens ?? 0,
107
+ cacheCreate: u.cache_creation_input_tokens ?? 0,
108
+ cacheRead: u.cache_read_input_tokens ?? 0
91
109
  });
92
110
  } catch {
93
111
  }
@@ -130,11 +148,43 @@ function sum(entries) {
130
148
  let cost = 0, tokens2 = 0;
131
149
  for (const e of entries) {
132
150
  cost += e.cost;
133
- tokens2 += e.tokens;
151
+ tokens2 += e.input + e.output + e.cacheCreate + e.cacheRead;
134
152
  }
135
153
  return { cost, tokens: tokens2 };
136
154
  }
137
- async function fetchUsage() {
155
+ function buildDaily(entries) {
156
+ const byDate = /* @__PURE__ */ new Map();
157
+ for (const e of entries) {
158
+ const date = new Date(e.ts).toISOString().slice(0, 10);
159
+ const arr = byDate.get(date);
160
+ if (arr) arr.push(e);
161
+ else byDate.set(date, [e]);
162
+ }
163
+ const rows = [];
164
+ for (const [date, dayEntries] of byDate) {
165
+ const models = [...new Set(dayEntries.map((e) => shortModel(e.model)))];
166
+ let input = 0, output = 0, cacheCreate = 0, cacheRead = 0, cost = 0;
167
+ for (const e of dayEntries) {
168
+ input += e.input;
169
+ output += e.output;
170
+ cacheCreate += e.cacheCreate;
171
+ cacheRead += e.cacheRead;
172
+ cost += e.cost;
173
+ }
174
+ rows.push({
175
+ date,
176
+ models: models.sort(),
177
+ input,
178
+ output,
179
+ cacheCreate,
180
+ cacheRead,
181
+ total: input + output + cacheCreate + cacheRead,
182
+ cost
183
+ });
184
+ }
185
+ return rows.sort((a, b) => a.date.localeCompare(b.date));
186
+ }
187
+ async function fetchData() {
138
188
  const now = Date.now();
139
189
  const d = /* @__PURE__ */ new Date();
140
190
  const monthStart = new Date(d.getFullYear(), d.getMonth(), 1).getTime();
@@ -158,22 +208,115 @@ async function fetchUsage() {
158
208
  today: sum(entries.filter((e) => e.ts >= todayStart)),
159
209
  week: sum(entries.filter((e) => e.ts >= weekStart)),
160
210
  month: sum(entries),
161
- block
211
+ block,
212
+ daily: buildDaily(entries)
162
213
  };
163
214
  }
164
215
 
216
+ // src/config.ts
217
+ import { readFile, writeFile, mkdir } from "fs/promises";
218
+ import { join as join2 } from "path";
219
+ import { homedir as homedir2 } from "os";
220
+ var DEFAULTS = { interval: 2 };
221
+ function configDir() {
222
+ if (process.platform === "win32") {
223
+ return join2(process.env.APPDATA ?? join2(homedir2(), "AppData", "Roaming"), "tokmon");
224
+ }
225
+ const xdg = process.env.XDG_CONFIG_HOME ?? join2(homedir2(), ".config");
226
+ return join2(xdg, "tokmon");
227
+ }
228
+ function configPath() {
229
+ return join2(configDir(), "config.json");
230
+ }
231
+ async function loadConfig() {
232
+ try {
233
+ const raw = await readFile(configPath(), "utf-8");
234
+ const parsed = JSON.parse(raw);
235
+ return { ...DEFAULTS, ...parsed };
236
+ } catch {
237
+ return { ...DEFAULTS };
238
+ }
239
+ }
240
+ async function saveConfig(config) {
241
+ const dir = configDir();
242
+ await mkdir(dir, { recursive: true });
243
+ await writeFile(configPath(), JSON.stringify(config, null, 2) + "\n");
244
+ }
245
+ function configLocation() {
246
+ return configPath();
247
+ }
248
+
165
249
  // src/app.tsx
166
250
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
167
- var INTERVAL = 2e3;
168
- function App() {
251
+ var TABS = ["Dashboard", "Daily"];
252
+ function App({ interval: initialInterval }) {
169
253
  const [data, setData] = useState(null);
170
254
  const [error, setError] = useState(null);
171
255
  const [updated, setUpdated] = useState(/* @__PURE__ */ new Date());
256
+ const [tab, setTab] = useState(0);
257
+ const [scroll, setScroll] = useState(0);
258
+ const [showSettings, setShowSettings] = useState(false);
259
+ const [config, setConfig] = useState({ interval: (initialInterval ?? 2e3) / 1e3 });
260
+ const [settingsCursor, setSettingsCursor] = useState(0);
261
+ const { stdout } = useStdout();
262
+ const rows = stdout?.rows ?? 24;
263
+ const interval2 = config.interval * 1e3;
264
+ useEffect(() => {
265
+ loadConfig().then((c) => {
266
+ if (!initialInterval) setConfig(c);
267
+ else setConfig({ ...c, interval: initialInterval / 1e3 });
268
+ });
269
+ }, []);
270
+ const isTTY = process.stdin.isTTY === true;
271
+ useInput((input, key) => {
272
+ if (showSettings) {
273
+ if (key.escape || input === "s") setShowSettings(false);
274
+ if (key.upArrow) setSettingsCursor((c) => Math.max(0, c - 1));
275
+ if (key.downArrow) setSettingsCursor((c) => Math.min(0, c + 1));
276
+ if (key.leftArrow && settingsCursor === 0) {
277
+ setConfig((c) => {
278
+ const next = { ...c, interval: Math.max(1, c.interval - 1) };
279
+ saveConfig(next);
280
+ return next;
281
+ });
282
+ }
283
+ if (key.rightArrow && settingsCursor === 0) {
284
+ setConfig((c) => {
285
+ const next = { ...c, interval: c.interval + 1 };
286
+ saveConfig(next);
287
+ return next;
288
+ });
289
+ }
290
+ return;
291
+ }
292
+ if (input === "s") {
293
+ setShowSettings(true);
294
+ return;
295
+ }
296
+ if (key.tab || key.rightArrow) {
297
+ setTab((t) => (t + 1) % TABS.length);
298
+ setScroll(0);
299
+ }
300
+ if (key.leftArrow) {
301
+ setTab((t) => (t - 1 + TABS.length) % TABS.length);
302
+ setScroll(0);
303
+ }
304
+ if (key.upArrow) setScroll((s) => Math.max(0, s - 1));
305
+ if (key.downArrow) setScroll((s) => s + 1);
306
+ if (input === "1") {
307
+ setTab(0);
308
+ setScroll(0);
309
+ }
310
+ if (input === "2") {
311
+ setTab(1);
312
+ setScroll(0);
313
+ }
314
+ }, { isActive: isTTY });
172
315
  useEffect(() => {
173
316
  let active = true;
174
317
  const load = async () => {
175
318
  try {
176
- const result = await fetchUsage();
319
+ const result = await fetchData();
177
320
  if (active) {
178
321
  setData(result);
179
322
  setError(null);
@@ -184,62 +327,124 @@ function App() {
184
327
  }
185
328
  };
186
329
  load();
187
- const id = setInterval(load, INTERVAL);
330
+ const id = setInterval(load, interval2);
188
331
  return () => {
189
332
  active = false;
190
333
  clearInterval(id);
191
334
  };
192
- }, []);
335
+ }, [interval2]);
193
336
  if (error) return /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { color: "red", children: error }) });
194
337
  if (!data) return /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Loading..." }) });
195
338
  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: [
339
+ /* @__PURE__ */ jsxs(Box, { justifyContent: "space-between", children: [
340
+ /* @__PURE__ */ jsxs(Box, { children: [
341
+ /* @__PURE__ */ jsxs(Text, { bold: true, color: "greenBright", children: [
342
+ "\u25C9",
343
+ " tokmon"
344
+ ] }),
345
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
346
+ " \xB7 ",
347
+ config.interval,
348
+ "s"
349
+ ] })
350
+ ] }),
351
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: time(updated) })
352
+ ] }),
353
+ showSettings ? /* @__PURE__ */ jsx(SettingsView, { config, cursor: settingsCursor }) : /* @__PURE__ */ jsxs(Fragment, { children: [
354
+ /* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
355
+ /* @__PURE__ */ jsx(TabBar, { tabs: TABS, active: tab }),
356
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " Tab/\u2190\u2192 s=settings" })
357
+ ] }),
200
358
  /* @__PURE__ */ jsx(Box, { height: 1 }),
201
- /* @__PURE__ */ jsx(BlockSection, { block: data.block })
359
+ TABS[tab] === "Dashboard" && /* @__PURE__ */ jsx(DashboardView, { data }),
360
+ TABS[tab] === "Daily" && /* @__PURE__ */ jsx(DailyView, { daily: data.daily, scroll, maxRows: rows - 10 })
202
361
  ] }),
203
- /* @__PURE__ */ jsx(Box, { height: 1 }),
204
- /* @__PURE__ */ jsx(Divider, {}),
205
- /* @__PURE__ */ jsx(Footer, { cost: data.month.cost, updated })
362
+ /* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
363
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "by " }),
364
+ /* @__PURE__ */ jsx(Text, { children: "David Ilie" }),
365
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " (" }),
366
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: "davidilie.com" }),
367
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: ")" })
368
+ ] })
206
369
  ] });
207
370
  }
208
- function Header() {
209
- return /* @__PURE__ */ jsxs(Box, { children: [
210
- /* @__PURE__ */ jsxs(Text, { bold: true, color: "greenBright", children: [
211
- "\u25C9",
212
- " tokmon"
213
- ] }),
371
+ function TabBar({ tabs, active }) {
372
+ 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: [
373
+ " ",
374
+ t,
375
+ " "
376
+ ] }) : /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
377
+ " ",
378
+ t,
379
+ " "
380
+ ] }) }, t)) });
381
+ }
382
+ function SettingsView({ config, cursor }) {
383
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
384
+ /* @__PURE__ */ jsx(Text, { bold: true, children: "Settings" }),
214
385
  /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
215
- " \xB7 refreshing every ",
216
- INTERVAL / 1e3,
217
- "s"
218
- ] })
386
+ "Saved to ",
387
+ configLocation()
388
+ ] }),
389
+ /* @__PURE__ */ jsx(Box, { height: 1 }),
390
+ /* @__PURE__ */ jsxs(Box, { children: [
391
+ cursor === 0 ? /* @__PURE__ */ jsxs(Text, { color: "green", children: [
392
+ "\u25B8",
393
+ " "
394
+ ] }) : /* @__PURE__ */ jsx(Text, { children: " " }),
395
+ /* @__PURE__ */ jsx(Text, { children: "Refresh interval " }),
396
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
397
+ "\u25C2",
398
+ " "
399
+ ] }),
400
+ /* @__PURE__ */ jsxs(Text, { bold: true, color: "yellow", children: [
401
+ config.interval,
402
+ "s"
403
+ ] }),
404
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
405
+ " ",
406
+ "\u25B8"
407
+ ] }),
408
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " (\u2190\u2192 to adjust)" })
409
+ ] }),
410
+ /* @__PURE__ */ jsx(Box, { height: 1 }),
411
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Press s or Esc to close" })
219
412
  ] });
220
413
  }
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
- );
414
+ function DashboardView({ data }) {
415
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
416
+ /* @__PURE__ */ jsxs(
417
+ Box,
418
+ {
419
+ flexDirection: "column",
420
+ paddingLeft: 1,
421
+ borderStyle: "bold",
422
+ borderColor: "green",
423
+ borderRight: false,
424
+ borderTop: false,
425
+ borderBottom: false,
426
+ children: [
427
+ /* @__PURE__ */ jsx(Text, { bold: true, children: "Claude" }),
428
+ /* @__PURE__ */ jsx(Box, { height: 1 }),
429
+ /* @__PURE__ */ jsx(SummaryRow, { label: "Today", summary: data.today }),
430
+ /* @__PURE__ */ jsx(SummaryRow, { label: "This Week", summary: data.week }),
431
+ /* @__PURE__ */ jsx(SummaryRow, { label: "This Month", summary: data.month })
432
+ ]
433
+ }
434
+ ),
435
+ data.block && /* @__PURE__ */ jsxs(Fragment, { children: [
436
+ /* @__PURE__ */ jsx(Box, { height: 1 }),
437
+ /* @__PURE__ */ jsx(BlockView, { block: data.block })
438
+ ] }),
439
+ /* @__PURE__ */ jsx(Box, { height: 1 }),
440
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(50) }),
441
+ /* @__PURE__ */ jsx(Box, { justifyContent: "space-between", width: 50, children: /* @__PURE__ */ jsxs(Box, { children: [
442
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Total " }),
443
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "yellowBright", children: currency(data.month.cost) })
444
+ ] }) })
445
+ ] });
241
446
  }
242
- function BlockSection({ block }) {
447
+ function BlockView({ block }) {
243
448
  return /* @__PURE__ */ jsxs(
244
449
  Box,
245
450
  {
@@ -261,7 +466,7 @@ function BlockSection({ block }) {
261
466
  ] }),
262
467
  /* @__PURE__ */ jsx(Box, { height: 1 }),
263
468
  /* @__PURE__ */ jsxs(Box, { children: [
264
- /* @__PURE__ */ jsx(Bar, { percent: block.percent, width: 36 }),
469
+ /* @__PURE__ */ jsx(ProgressBar, { percent: block.percent, width: 36 }),
265
470
  /* @__PURE__ */ jsx(Text, { children: " " }),
266
471
  /* @__PURE__ */ jsxs(Text, { bold: true, children: [
267
472
  Math.round(block.percent),
@@ -270,12 +475,9 @@ function BlockSection({ block }) {
270
475
  ] }),
271
476
  /* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
272
477
  /* @__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: "~" }),
478
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " spent \xB7 ~" }),
276
479
  /* @__PURE__ */ jsx(Text, { children: currency(block.projected) }),
277
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " proj" }),
278
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \xB7 " }),
480
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " proj \xB7 " }),
279
481
  /* @__PURE__ */ jsx(Text, { color: "red", children: currency(block.burnRate) }),
280
482
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "/hr" })
281
483
  ] })
@@ -283,7 +485,7 @@ function BlockSection({ block }) {
283
485
  }
284
486
  );
285
487
  }
286
- function Row({ label, summary }) {
488
+ function SummaryRow({ label, summary }) {
287
489
  return /* @__PURE__ */ jsxs(Box, { children: [
288
490
  /* @__PURE__ */ jsx(Box, { width: 14, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: label }) }),
289
491
  /* @__PURE__ */ jsx(Box, { width: 12, justifyContent: "flex-end", children: /* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: currency(summary.cost) }) }),
@@ -293,36 +495,95 @@ function Row({ label, summary }) {
293
495
  ] }) })
294
496
  ] });
295
497
  }
296
- function Bar({ percent, width = 36 }) {
498
+ function ProgressBar({ percent, width = 36 }) {
297
499
  const filled = Math.round(percent / 100 * width);
298
500
  return /* @__PURE__ */ jsxs(Text, { children: [
299
501
  /* @__PURE__ */ jsx(Text, { color: "greenBright", children: "\u2501".repeat(filled) }),
300
502
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(width - filled) })
301
503
  ] });
302
504
  }
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) })
505
+ function DailyView({ daily, scroll, maxRows }) {
506
+ const W = { date: 7, models: 16, input: 8, output: 8, cc: 8, cr: 8, cost: 10 };
507
+ const lineW = W.date + W.models + W.input + W.output + W.cc + W.cr + W.cost;
508
+ const totals = { input: 0, output: 0, cacheCreate: 0, cacheRead: 0, cost: 0 };
509
+ for (const r of daily) {
510
+ totals.input += r.input;
511
+ totals.output += r.output;
512
+ totals.cacheCreate += r.cacheCreate;
513
+ totals.cacheRead += r.cacheRead;
514
+ totals.cost += r.cost;
515
+ }
516
+ const visible = daily.slice(scroll, scroll + maxRows);
517
+ const more = daily.length - scroll - maxRows;
518
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
519
+ /* @__PURE__ */ jsxs(Text, { children: [
520
+ /* @__PURE__ */ jsx(Text, { bold: true, children: col("Date", W.date, "left") }),
521
+ /* @__PURE__ */ jsx(Text, { bold: true, children: col("Models", W.models, "left") }),
522
+ /* @__PURE__ */ jsx(Text, { bold: true, children: col("Input", W.input) }),
523
+ /* @__PURE__ */ jsx(Text, { bold: true, children: col("Output", W.output) }),
524
+ /* @__PURE__ */ jsx(Text, { bold: true, children: col("CchCrt", W.cc) }),
525
+ /* @__PURE__ */ jsx(Text, { bold: true, children: col("CchRd", W.cr) }),
526
+ /* @__PURE__ */ jsx(Text, { bold: true, children: col("Cost", W.cost) })
314
527
  ] }),
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
- ] })
528
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(lineW) }),
529
+ visible.map((r) => /* @__PURE__ */ jsxs(Text, { children: [
530
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: col(shortDate(r.date), W.date, "left") }),
531
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: col(r.models.join(", "), W.models, "left") }),
532
+ /* @__PURE__ */ jsx(Text, { children: col(tokens(r.input), W.input) }),
533
+ /* @__PURE__ */ jsx(Text, { children: col(tokens(r.output), W.output) }),
534
+ /* @__PURE__ */ jsx(Text, { children: col(tokens(r.cacheCreate), W.cc) }),
535
+ /* @__PURE__ */ jsx(Text, { children: col(tokens(r.cacheRead), W.cr) }),
536
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: col(currency(r.cost), W.cost) })
537
+ ] }, r.date)),
538
+ more > 0 && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
539
+ " \u2193 ",
540
+ more,
541
+ " more"
542
+ ] }),
543
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(lineW) }),
544
+ /* @__PURE__ */ jsxs(Text, { children: [
545
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "greenBright", children: col("Total", W.date, "left") }),
546
+ /* @__PURE__ */ jsx(Text, { children: col("", W.models, "left") }),
547
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: col(tokens(totals.input), W.input) }),
548
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: col(tokens(totals.output), W.output) }),
549
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: col(tokens(totals.cacheCreate), W.cc) }),
550
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: col(tokens(totals.cacheRead), W.cr) }),
551
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "yellowBright", children: col(currency(totals.cost), W.cost) })
552
+ ] }),
553
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
554
+ "\u2191\u2193 scroll \xB7 ",
555
+ daily.length,
556
+ " days \xB7 ",
557
+ scroll + 1,
558
+ "-",
559
+ Math.min(scroll + maxRows, daily.length)
560
+ ] }) })
322
561
  ] });
323
562
  }
324
563
 
325
564
  // src/cli.tsx
326
565
  import { jsx as jsx2 } from "react/jsx-runtime";
327
- var { waitUntilExit } = render(/* @__PURE__ */ jsx2(App, {}));
566
+ var args = process.argv.slice(2);
567
+ var interval;
568
+ for (let i = 0; i < args.length; i++) {
569
+ if ((args[i] === "--interval" || args[i] === "-i") && args[i + 1]) {
570
+ interval = Math.max(500, Number(args[i + 1]) * 1e3);
571
+ i++;
572
+ }
573
+ if (args[i] === "--help" || args[i] === "-h") {
574
+ console.log("tokmon - Terminal dashboard for Claude Code usage\n");
575
+ console.log("Usage: tokmon [options]\n");
576
+ console.log("Options:");
577
+ console.log(" -i, --interval <seconds> Refresh interval (default: from config or 2)");
578
+ console.log(" -h, --help Show this help\n");
579
+ console.log("Keybindings:");
580
+ console.log(" Tab / \u2190\u2192 Switch views");
581
+ console.log(" \u2191\u2193 Scroll table");
582
+ console.log(" 1-2 Jump to view");
583
+ console.log(" s Settings");
584
+ console.log(" Ctrl+C Quit");
585
+ process.exit(0);
586
+ }
587
+ }
588
+ var { waitUntilExit } = render(/* @__PURE__ */ jsx2(App, { interval }));
328
589
  await waitUntilExit();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokmon",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Terminal dashboard for Claude Code usage and costs",
5
5
  "type": "module",
6
6
  "bin": {