tokmon 0.2.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 (2) hide show
  1. package/dist/cli.js +167 -70
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -97,11 +97,10 @@ async function parseFile(path, since) {
97
97
  const ts = new Date(obj.timestamp ?? 0).getTime();
98
98
  if (ts < since) continue;
99
99
  const u = obj.message.usage;
100
- const model = obj.message.model ?? "unknown";
101
100
  entries.push({
102
101
  ts,
103
- model,
104
- cost: costOf(model, u),
102
+ model: obj.message.model ?? "unknown",
103
+ cost: costOf(obj.message.model ?? "", u),
105
104
  input: u.input_tokens ?? 0,
106
105
  output: u.output_tokens ?? 0,
107
106
  cacheCreate: u.cache_creation_input_tokens ?? 0,
@@ -152,19 +151,19 @@ function sum(entries) {
152
151
  }
153
152
  return { cost, tokens: tokens2 };
154
153
  }
155
- function buildDaily(entries) {
156
- const byDate = /* @__PURE__ */ new Map();
154
+ function groupBy(entries, keyFn) {
155
+ const groups = /* @__PURE__ */ new Map();
157
156
  for (const e of entries) {
158
- const date = new Date(e.ts).toISOString().slice(0, 10);
159
- const arr = byDate.get(date);
157
+ const key = keyFn(e);
158
+ const arr = groups.get(key);
160
159
  if (arr) arr.push(e);
161
- else byDate.set(date, [e]);
160
+ else groups.set(key, [e]);
162
161
  }
163
162
  const rows = [];
164
- for (const [date, dayEntries] of byDate) {
165
- const models = [...new Set(dayEntries.map((e) => shortModel(e.model)))];
163
+ for (const [label, group] of groups) {
164
+ const models = [...new Set(group.map((e) => shortModel(e.model)))];
166
165
  let input = 0, output = 0, cacheCreate = 0, cacheRead = 0, cost = 0;
167
- for (const e of dayEntries) {
166
+ for (const e of group) {
168
167
  input += e.input;
169
168
  output += e.output;
170
169
  cacheCreate += e.cacheCreate;
@@ -172,7 +171,7 @@ function buildDaily(entries) {
172
171
  cost += e.cost;
173
172
  }
174
173
  rows.push({
175
- date,
174
+ label,
176
175
  models: models.sort(),
177
176
  input,
178
177
  output,
@@ -182,16 +181,28 @@ function buildDaily(entries) {
182
181
  cost
183
182
  });
184
183
  }
185
- return rows.sort((a, b) => a.date.localeCompare(b.date));
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);
186
196
  }
187
197
  async function fetchData() {
188
198
  const now = Date.now();
189
199
  const d = /* @__PURE__ */ new Date();
200
+ const lookback = new Date(d.getFullYear(), d.getMonth() - 6, 1).getTime();
190
201
  const monthStart = new Date(d.getFullYear(), d.getMonth(), 1).getTime();
191
202
  const todayStart = new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
192
203
  const weekDay = d.getDay();
193
204
  const weekStart = new Date(d.getFullYear(), d.getMonth(), d.getDate() - (weekDay === 0 ? 6 : weekDay - 1)).getTime();
194
- const entries = await loadEntries(monthStart);
205
+ const entries = await loadEntries(lookback);
195
206
  const fiveHoursAgo = now - 5 * 36e5;
196
207
  const blockEntries = entries.filter((e) => e.ts >= fiveHoursAgo);
197
208
  let block = null;
@@ -204,12 +215,17 @@ async function fetchData() {
204
215
  const percent = Math.min(100, (now - oldest) / (5 * 36e5) * 100);
205
216
  block = { spent, projected: burnRate * 5, burnRate, percent, remaining: minutes(remainMs / 6e4) };
206
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));
207
221
  return {
208
222
  today: sum(entries.filter((e) => e.ts >= todayStart)),
209
223
  week: sum(entries.filter((e) => e.ts >= weekStart)),
210
- month: sum(entries),
224
+ month: sum(entries.filter((e) => e.ts >= monthStart)),
211
225
  block,
212
- daily: buildDaily(entries)
226
+ daily,
227
+ weekly,
228
+ monthly
213
229
  };
214
230
  }
215
231
 
@@ -217,7 +233,7 @@ async function fetchData() {
217
233
  import { readFile, writeFile, mkdir } from "fs/promises";
218
234
  import { join as join2 } from "path";
219
235
  import { homedir as homedir2 } from "os";
220
- var DEFAULTS = { interval: 2 };
236
+ var DEFAULTS = { interval: 2, clearScreen: true };
221
237
  function configDir() {
222
238
  if (process.platform === "win32") {
223
239
  return join2(process.env.APPDATA ?? join2(homedir2(), "AppData", "Roaming"), "tokmon");
@@ -248,41 +264,56 @@ function configLocation() {
248
264
 
249
265
  // src/app.tsx
250
266
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
251
- var TABS = ["Dashboard", "Daily"];
252
- function App({ interval: initialInterval }) {
267
+ var TABS = ["Dashboard", "Table"];
268
+ var VIEWS = ["Daily", "Weekly", "Monthly"];
269
+ function App({ interval: cliInterval }) {
253
270
  const [data, setData] = useState(null);
254
271
  const [error, setError] = useState(null);
255
272
  const [updated, setUpdated] = useState(/* @__PURE__ */ new Date());
256
273
  const [tab, setTab] = useState(0);
274
+ const [view, setView] = useState(0);
257
275
  const [scroll, setScroll] = useState(0);
258
276
  const [showSettings, setShowSettings] = useState(false);
259
- const [config, setConfig] = useState({ interval: (initialInterval ?? 2e3) / 1e3 });
277
+ const [config, setConfig] = useState(null);
260
278
  const [settingsCursor, setSettingsCursor] = useState(0);
261
279
  const { stdout } = useStdout();
262
280
  const rows = stdout?.rows ?? 24;
263
- const interval2 = config.interval * 1e3;
281
+ const cols = stdout?.columns ?? 80;
282
+ const interval2 = cliInterval ?? (config?.interval ?? 2) * 1e3;
264
283
  useEffect(() => {
265
284
  loadConfig().then((c) => {
266
- if (!initialInterval) setConfig(c);
267
- else setConfig({ ...c, interval: initialInterval / 1e3 });
285
+ if (cliInterval) c = { ...c, interval: cliInterval / 1e3 };
286
+ setConfig(c);
287
+ if (c.clearScreen && stdout) stdout.write("\x1B[2J\x1B[H");
268
288
  });
269
289
  }, []);
270
290
  const isTTY = process.stdin.isTTY === true;
291
+ const settingsItems = 2;
292
+ const cfg = config ?? { interval: 2, clearScreen: true };
271
293
  useInput((input, key) => {
272
294
  if (showSettings) {
273
295
  if (key.escape || input === "s") setShowSettings(false);
274
296
  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
- });
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
+ }
282
313
  }
283
- if (key.rightArrow && settingsCursor === 0) {
314
+ if (settingsCursor === 1 && (key.leftArrow || key.rightArrow || key.return)) {
284
315
  setConfig((c) => {
285
- const next = { ...c, interval: c.interval + 1 };
316
+ const next = { ...c, clearScreen: !c.clearScreen };
286
317
  saveConfig(next);
287
318
  return next;
288
319
  });
@@ -293,24 +324,58 @@ function App({ interval: initialInterval }) {
293
324
  setShowSettings(true);
294
325
  return;
295
326
  }
296
- if (key.tab || key.rightArrow) {
327
+ if (key.tab) {
297
328
  setTab((t) => (t + 1) % TABS.length);
298
329
  setScroll(0);
330
+ return;
299
331
  }
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
332
  if (input === "1") {
307
333
  setTab(0);
308
334
  setScroll(0);
335
+ return;
309
336
  }
310
337
  if (input === "2") {
311
338
  setTab(1);
312
339
  setScroll(0);
340
+ return;
313
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)));
314
379
  }, { isActive: isTTY });
315
380
  useEffect(() => {
316
381
  let active = true;
@@ -335,6 +400,7 @@ function App({ interval: initialInterval }) {
335
400
  }, [interval2]);
336
401
  if (error) return /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { color: "red", children: error }) });
337
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];
338
404
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [
339
405
  /* @__PURE__ */ jsxs(Box, { justifyContent: "space-between", children: [
340
406
  /* @__PURE__ */ jsxs(Box, { children: [
@@ -344,20 +410,24 @@ function App({ interval: initialInterval }) {
344
410
  ] }),
345
411
  /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
346
412
  " \xB7 ",
347
- config.interval,
413
+ cliInterval ? cliInterval / 1e3 : cfg.interval,
348
414
  "s"
349
415
  ] })
350
416
  ] }),
351
417
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: time(updated) })
352
418
  ] }),
353
- showSettings ? /* @__PURE__ */ jsx(SettingsView, { config, cursor: settingsCursor }) : /* @__PURE__ */ jsxs(Fragment, { children: [
419
+ showSettings ? /* @__PURE__ */ jsx(SettingsView, { config: cfg, cursor: settingsCursor }) : /* @__PURE__ */ jsxs(Fragment, { children: [
354
420
  /* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
355
421
  /* @__PURE__ */ jsx(TabBar, { tabs: TABS, active: tab }),
356
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " Tab/\u2190\u2192 s=settings" })
422
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " Tab s=settings" })
357
423
  ] }),
358
424
  /* @__PURE__ */ jsx(Box, { height: 1 }),
359
- TABS[tab] === "Dashboard" && /* @__PURE__ */ jsx(DashboardView, { data }),
360
- TABS[tab] === "Daily" && /* @__PURE__ */ jsx(DailyView, { daily: data.daily, scroll, maxRows: rows - 10 })
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
+ ] })
361
431
  ] }),
362
432
  /* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
363
433
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "by " }),
@@ -379,19 +449,26 @@ function TabBar({ tabs, active }) {
379
449
  " "
380
450
  ] }) }, t)) });
381
451
  }
452
+ function ViewBar({ views, active }) {
453
+ return /* @__PURE__ */ jsxs(Box, { children: [
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
+ }
382
462
  function SettingsView({ config, cursor }) {
383
463
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
384
464
  /* @__PURE__ */ jsx(Text, { bold: true, children: "Settings" }),
385
- /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
386
- "Saved to ",
387
- configLocation()
388
- ] }),
465
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: configLocation() }),
389
466
  /* @__PURE__ */ jsx(Box, { height: 1 }),
390
467
  /* @__PURE__ */ jsxs(Box, { children: [
391
- cursor === 0 ? /* @__PURE__ */ jsxs(Text, { color: "green", children: [
392
- "\u25B8",
468
+ /* @__PURE__ */ jsxs(Text, { color: cursor === 0 ? "green" : void 0, children: [
469
+ cursor === 0 ? "\u25B8" : " ",
393
470
  " "
394
- ] }) : /* @__PURE__ */ jsx(Text, { children: " " }),
471
+ ] }),
395
472
  /* @__PURE__ */ jsx(Text, { children: "Refresh interval " }),
396
473
  /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
397
474
  "\u25C2",
@@ -404,11 +481,18 @@ function SettingsView({ config, cursor }) {
404
481
  /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
405
482
  " ",
406
483
  "\u25B8"
484
+ ] })
485
+ ] }),
486
+ /* @__PURE__ */ jsxs(Box, { children: [
487
+ /* @__PURE__ */ jsxs(Text, { color: cursor === 1 ? "green" : void 0, children: [
488
+ cursor === 1 ? "\u25B8" : " ",
489
+ " "
407
490
  ] }),
408
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " (\u2190\u2192 to adjust)" })
491
+ /* @__PURE__ */ jsx(Text, { children: "Clear screen " }),
492
+ /* @__PURE__ */ jsx(Text, { bold: true, color: config.clearScreen ? "green" : "red", children: config.clearScreen ? "on" : "off" })
409
493
  ] }),
410
494
  /* @__PURE__ */ jsx(Box, { height: 1 }),
411
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Press s or Esc to close" })
495
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2191\u2193 select \u2190\u2192 adjust s/Esc close" })
412
496
  ] });
413
497
  }
414
498
  function DashboardView({ data }) {
@@ -438,10 +522,10 @@ function DashboardView({ data }) {
438
522
  ] }),
439
523
  /* @__PURE__ */ jsx(Box, { height: 1 }),
440
524
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(50) }),
441
- /* @__PURE__ */ jsx(Box, { justifyContent: "space-between", width: 50, children: /* @__PURE__ */ jsxs(Box, { children: [
525
+ /* @__PURE__ */ jsxs(Box, { width: 50, children: [
442
526
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Total " }),
443
527
  /* @__PURE__ */ jsx(Text, { bold: true, color: "yellowBright", children: currency(data.month.cost) })
444
- ] }) })
528
+ ] })
445
529
  ] });
446
530
  }
447
531
  function BlockView({ block }) {
@@ -502,39 +586,42 @@ function ProgressBar({ percent, width = 36 }) {
502
586
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(width - filled) })
503
587
  ] });
504
588
  }
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;
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;
508
592
  const totals = { input: 0, output: 0, cacheCreate: 0, cacheRead: 0, cost: 0 };
509
- for (const r of daily) {
593
+ for (const r of allRows) {
510
594
  totals.input += r.input;
511
595
  totals.output += r.output;
512
596
  totals.cacheCreate += r.cacheCreate;
513
597
  totals.cacheRead += r.cacheRead;
514
598
  totals.cost += r.cost;
515
599
  }
516
- const visible = daily.slice(scroll, scroll + maxRows);
517
- const more = daily.length - scroll - maxRows;
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;
518
603
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
519
604
  /* @__PURE__ */ jsxs(Text, { children: [
520
- /* @__PURE__ */ jsx(Text, { bold: true, children: col("Date", W.date, "left") }),
605
+ /* @__PURE__ */ jsx(Text, { bold: true, children: col("Date", W.label, "left") }),
521
606
  /* @__PURE__ */ jsx(Text, { bold: true, children: col("Models", W.models, "left") }),
522
607
  /* @__PURE__ */ jsx(Text, { bold: true, children: col("Input", W.input) }),
523
608
  /* @__PURE__ */ jsx(Text, { bold: true, children: col("Output", W.output) }),
524
609
  /* @__PURE__ */ jsx(Text, { bold: true, children: col("CchCrt", W.cc) }),
525
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) }),
526
612
  /* @__PURE__ */ jsx(Text, { bold: true, children: col("Cost", W.cost) })
527
613
  ] }),
528
614
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(lineW) }),
529
615
  visible.map((r) => /* @__PURE__ */ jsxs(Text, { children: [
530
- /* @__PURE__ */ jsx(Text, { color: "cyan", children: col(shortDate(r.date), W.date, "left") }),
616
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: col(fmtLabel(r.label), W.label, "left") }),
531
617
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: col(r.models.join(", "), W.models, "left") }),
532
618
  /* @__PURE__ */ jsx(Text, { children: col(tokens(r.input), W.input) }),
533
619
  /* @__PURE__ */ jsx(Text, { children: col(tokens(r.output), W.output) }),
534
620
  /* @__PURE__ */ jsx(Text, { children: col(tokens(r.cacheCreate), W.cc) }),
535
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) }),
536
623
  /* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: col(currency(r.cost), W.cost) })
537
- ] }, r.date)),
624
+ ] }, r.label)),
538
625
  more > 0 && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
539
626
  " \u2193 ",
540
627
  more,
@@ -542,24 +629,34 @@ function DailyView({ daily, scroll, maxRows }) {
542
629
  ] }),
543
630
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(lineW) }),
544
631
  /* @__PURE__ */ jsxs(Text, { children: [
545
- /* @__PURE__ */ jsx(Text, { bold: true, color: "greenBright", children: col("Total", W.date, "left") }),
632
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "greenBright", children: col("Total", W.label, "left") }),
546
633
  /* @__PURE__ */ jsx(Text, { children: col("", W.models, "left") }),
547
634
  /* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: col(tokens(totals.input), W.input) }),
548
635
  /* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: col(tokens(totals.output), W.output) }),
549
636
  /* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: col(tokens(totals.cacheCreate), W.cc) }),
550
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) }),
551
639
  /* @__PURE__ */ jsx(Text, { bold: true, color: "yellowBright", children: col(currency(totals.cost), W.cost) })
552
640
  ] }),
553
641
  /* @__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,
642
+ "\u2191\u2193 PgUp/Dn scroll \xB7 ",
643
+ allRows.length,
644
+ " rows \xB7 ",
645
+ clampedScroll + 1,
558
646
  "-",
559
- Math.min(scroll + maxRows, daily.length)
647
+ Math.min(clampedScroll + maxRows, allRows.length)
560
648
  ] }) })
561
649
  ] });
562
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
+ }
563
660
 
564
661
  // src/cli.tsx
565
662
  import { jsx as jsx2 } from "react/jsx-runtime";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokmon",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Terminal dashboard for Claude Code usage and costs",
5
5
  "type": "module",
6
6
  "bin": {