tokmon 0.10.0 → 0.11.1

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 +209 -39
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -8,7 +8,7 @@ import { MouseProvider } from "@zenobius/ink-mouse";
8
8
  import { readFile, writeFile, mkdir } from "fs/promises";
9
9
  import { join } from "path";
10
10
  import { homedir } from "os";
11
- var DEFAULTS = { interval: 2, billingInterval: 5, clearScreen: true };
11
+ var DEFAULTS = { interval: 2, billingInterval: 5, clearScreen: true, timezone: null };
12
12
  function configDir() {
13
13
  if (process.platform === "win32") {
14
14
  return join(process.env.APPDATA ?? join(homedir(), "AppData", "Roaming"), "tokmon");
@@ -45,6 +45,117 @@ import { createReadStream } from "fs";
45
45
  import { createInterface } from "readline";
46
46
  import { join as join2 } from "path";
47
47
  import { homedir as homedir2 } from "os";
48
+
49
+ // src/tz.ts
50
+ function systemTimezone() {
51
+ try {
52
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
53
+ } catch {
54
+ return "UTC";
55
+ }
56
+ }
57
+ function isValidTimezone(tz) {
58
+ try {
59
+ new Intl.DateTimeFormat("en-CA", { timeZone: tz });
60
+ return true;
61
+ } catch {
62
+ return false;
63
+ }
64
+ }
65
+ function resolveTimezone(cfg) {
66
+ if (!cfg) return systemTimezone();
67
+ return isValidTimezone(cfg) ? cfg : systemTimezone();
68
+ }
69
+ var dayFmtCache = /* @__PURE__ */ new Map();
70
+ function dayFmt(tz) {
71
+ let f = dayFmtCache.get(tz);
72
+ if (!f) {
73
+ f = new Intl.DateTimeFormat("en-CA", {
74
+ timeZone: tz,
75
+ year: "numeric",
76
+ month: "2-digit",
77
+ day: "2-digit"
78
+ });
79
+ dayFmtCache.set(tz, f);
80
+ }
81
+ return f;
82
+ }
83
+ function dayKey(ts, tz) {
84
+ return dayFmt(tz).format(new Date(ts));
85
+ }
86
+ function monthKey(ts, tz) {
87
+ return dayKey(ts, tz).slice(0, 7);
88
+ }
89
+ var partsFmtCache = /* @__PURE__ */ new Map();
90
+ function partsFmt(tz) {
91
+ let f = partsFmtCache.get(tz);
92
+ if (!f) {
93
+ f = new Intl.DateTimeFormat("en-US", {
94
+ timeZone: tz,
95
+ hourCycle: "h23",
96
+ year: "numeric",
97
+ month: "2-digit",
98
+ day: "2-digit",
99
+ hour: "2-digit",
100
+ minute: "2-digit",
101
+ second: "2-digit",
102
+ weekday: "short"
103
+ });
104
+ partsFmtCache.set(tz, f);
105
+ }
106
+ return f;
107
+ }
108
+ var WEEKDAY_MAP = {
109
+ Sun: 0,
110
+ Mon: 1,
111
+ Tue: 2,
112
+ Wed: 3,
113
+ Thu: 4,
114
+ Fri: 5,
115
+ Sat: 6
116
+ };
117
+ function tzParts(ts, tz) {
118
+ const parts = partsFmt(tz).formatToParts(new Date(ts));
119
+ const get = (t) => parts.find((p) => p.type === t)?.value ?? "";
120
+ return {
121
+ y: Number(get("year")),
122
+ m: Number(get("month")),
123
+ d: Number(get("day")),
124
+ hh: Number(get("hour")),
125
+ mm: Number(get("minute")),
126
+ ss: Number(get("second")),
127
+ weekday: WEEKDAY_MAP[get("weekday")] ?? 0
128
+ };
129
+ }
130
+ function instantFromTz(y, m, d, hh, mm, ss, tz) {
131
+ const guess = Date.UTC(y, m - 1, d, hh, mm, ss);
132
+ const r = tzParts(guess, tz);
133
+ const rendered = Date.UTC(r.y, r.m - 1, r.d, r.hh, r.mm, r.ss);
134
+ const offset = rendered - guess;
135
+ return guess - offset;
136
+ }
137
+ function startOfDay(ts, tz) {
138
+ const p = tzParts(ts, tz);
139
+ return instantFromTz(p.y, p.m, p.d, 0, 0, 0, tz);
140
+ }
141
+ function startOfMonth(ts, tz) {
142
+ const p = tzParts(ts, tz);
143
+ return instantFromTz(p.y, p.m, 1, 0, 0, 0, tz);
144
+ }
145
+ function startOfWeek(ts, tz) {
146
+ const p = tzParts(ts, tz);
147
+ const offset = p.weekday === 0 ? 6 : p.weekday - 1;
148
+ return instantFromTz(p.y, p.m, p.d - offset, 0, 0, 0, tz);
149
+ }
150
+ function monthsAgoStart(ts, months, tz) {
151
+ const p = tzParts(ts, tz);
152
+ return instantFromTz(p.y, p.m - months, 1, 0, 0, 0, tz);
153
+ }
154
+ function weekKey(ts, tz) {
155
+ return dayKey(startOfWeek(ts, tz), tz);
156
+ }
157
+
158
+ // src/data.ts
48
159
  var PRICING = {
49
160
  "claude-opus-4": { i: 5e-6, o: 25e-6, cc: 625e-8, cr: 5e-7 },
50
161
  "claude-sonnet-4": { i: 3e-6, o: 15e-6, cc: 375e-8, cr: 3e-7 },
@@ -207,24 +318,11 @@ function groupBy(entries, keyFn) {
207
318
  }
208
319
  return rows.sort((a, b) => a.label.localeCompare(b.label));
209
320
  }
210
- function isoWeekLabel(ts) {
211
- const d = new Date(ts);
212
- const day = d.getDay();
213
- const mondayOffset = day === 0 ? 6 : day - 1;
214
- const monday = new Date(d);
215
- monday.setDate(d.getDate() - mondayOffset);
216
- return monday.toISOString().slice(0, 10);
217
- }
218
- function monthLabel(ts) {
219
- return new Date(ts).toISOString().slice(0, 7);
220
- }
221
- async function fetchDashboard() {
222
- const d = /* @__PURE__ */ new Date();
223
- const monthStart = new Date(d.getFullYear(), d.getMonth(), 1).getTime();
224
- const todayStart = new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
225
- const weekDay = d.getDay();
226
- const weekStart = new Date(d.getFullYear(), d.getMonth(), d.getDate() - (weekDay === 0 ? 6 : weekDay - 1)).getTime();
321
+ async function fetchDashboard(tz) {
227
322
  const now = Date.now();
323
+ const monthStart = startOfMonth(now, tz);
324
+ const todayStart = startOfDay(now, tz);
325
+ const weekStart = startOfWeek(now, tz);
228
326
  const entries = await loadEntries(monthStart);
229
327
  const todayEntries = entries.filter((e) => e.ts >= todayStart);
230
328
  let burnRate = 0;
@@ -245,14 +343,13 @@ async function fetchDashboard() {
245
343
  burnRate
246
344
  };
247
345
  }
248
- async function fetchTable() {
249
- const d = /* @__PURE__ */ new Date();
250
- const lookback = new Date(d.getFullYear(), d.getMonth() - 6, 1).getTime();
346
+ async function fetchTable(tz) {
347
+ const lookback = monthsAgoStart(Date.now(), 6, tz);
251
348
  const entries = await loadEntries(lookback);
252
349
  return {
253
- daily: groupBy(entries, (e) => new Date(e.ts).toISOString().slice(0, 10)),
254
- weekly: groupBy(entries, (e) => isoWeekLabel(e.ts)),
255
- monthly: groupBy(entries, (e) => monthLabel(e.ts))
350
+ daily: groupBy(entries, (e) => dayKey(e.ts, tz)),
351
+ weekly: groupBy(entries, (e) => weekKey(e.ts, tz)),
352
+ monthly: groupBy(entries, (e) => monthKey(e.ts, tz))
256
353
  };
257
354
  }
258
355
 
@@ -385,6 +482,9 @@ function formatReset(iso) {
385
482
 
386
483
  // src/format.ts
387
484
  function currency(value) {
485
+ if (value >= 1e4) {
486
+ return `$${value.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
487
+ }
388
488
  return `$${value.toFixed(2)}`;
389
489
  }
390
490
  function tokens(value) {
@@ -393,11 +493,12 @@ function tokens(value) {
393
493
  if (value >= 1e3) return `${(value / 1e3).toFixed(1)}K`;
394
494
  return String(value);
395
495
  }
396
- function time(date) {
496
+ function time(date, tz) {
397
497
  return date.toLocaleTimeString(void 0, {
398
498
  hour: "2-digit",
399
499
  minute: "2-digit",
400
- second: "2-digit"
500
+ second: "2-digit",
501
+ timeZone: tz
401
502
  });
402
503
  }
403
504
  var SHORT_MONTHS = ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
@@ -418,7 +519,8 @@ var VIEWS = ["Daily", "Weekly", "Monthly"];
418
519
  var SORTS = ["date \u2191", "date \u2193", "cost \u2191", "cost \u2193"];
419
520
  var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
420
521
  var MONTHS = ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
421
- var DEFAULT_CONFIG = { interval: 2, billingInterval: 5, clearScreen: true };
522
+ var DEFAULT_CONFIG = { interval: 2, billingInterval: 5, clearScreen: true, timezone: null };
523
+ var SETTINGS_ROWS = 4;
422
524
  var IS_TTY = process.stdin.isTTY === true;
423
525
  function App({ interval: cliInterval }) {
424
526
  const [dashboard, setDashboard] = useState(null);
@@ -431,10 +533,12 @@ function App({ interval: cliInterval }) {
431
533
  const [view, setView] = useState(0);
432
534
  const [cursor, setCursor] = useState(0);
433
535
  const [expanded, setExpanded] = useState(-1);
434
- const [sort, setSort] = useState(0);
536
+ const [sort, setSort] = useState(1);
435
537
  const [showSettings, setShowSettings] = useState(false);
436
538
  const [config2, setConfig] = useState(null);
437
539
  const [settingsCursor, setSettingsCursor] = useState(0);
540
+ const [tzEdit, setTzEdit] = useState(null);
541
+ const [tzError, setTzError] = useState(null);
438
542
  const tableLoadedOnce = useRef(false);
439
543
  const { stdout } = useStdout();
440
544
  const { exit } = useApp();
@@ -442,6 +546,7 @@ function App({ interval: cliInterval }) {
442
546
  const cols = stdout?.columns ?? 80;
443
547
  const interval2 = cliInterval ?? (config2?.interval ?? 2) * 1e3;
444
548
  const cfg = config2 ?? DEFAULT_CONFIG;
549
+ const tz = resolveTimezone(cfg.timezone);
445
550
  useEffect(() => {
446
551
  loadConfig().then((c) => {
447
552
  if (cliInterval) c = { ...c, interval: cliInterval / 1e3 };
@@ -452,7 +557,7 @@ function App({ interval: cliInterval }) {
452
557
  let active = true;
453
558
  const load = async () => {
454
559
  try {
455
- const result = await fetchDashboard();
560
+ const result = await fetchDashboard(tz);
456
561
  if (active) {
457
562
  setDashboard(result);
458
563
  setError(null);
@@ -468,7 +573,7 @@ function App({ interval: cliInterval }) {
468
573
  active = false;
469
574
  clearInterval(id);
470
575
  };
471
- }, [interval2]);
576
+ }, [interval2, tz]);
472
577
  const billingMs = cfg.billingInterval * 6e4;
473
578
  useEffect(() => {
474
579
  let active = true;
@@ -483,12 +588,16 @@ function App({ interval: cliInterval }) {
483
588
  clearInterval(id);
484
589
  };
485
590
  }, [billingMs]);
591
+ useEffect(() => {
592
+ tableLoadedOnce.current = false;
593
+ setTable(null);
594
+ }, [tz]);
486
595
  useEffect(() => {
487
596
  if (tab !== 1) return;
488
597
  if (tableLoadedOnce.current && table) return;
489
598
  let active = true;
490
599
  setTableLoading(true);
491
- fetchTable().then((result) => {
600
+ fetchTable(tz).then((result) => {
492
601
  if (active) {
493
602
  setTable(result);
494
603
  setTableLoading(false);
@@ -500,13 +609,13 @@ function App({ interval: cliInterval }) {
500
609
  return () => {
501
610
  active = false;
502
611
  };
503
- }, [tab]);
612
+ }, [tab, tz]);
504
613
  useEffect(() => {
505
614
  if (tab !== 1 || !tableLoadedOnce.current) return;
506
615
  let active = true;
507
616
  const id = setInterval(async () => {
508
617
  try {
509
- const result = await fetchTable();
618
+ const result = await fetchTable(tz);
510
619
  if (active) setTable(result);
511
620
  } catch {
512
621
  }
@@ -515,7 +624,7 @@ function App({ interval: cliInterval }) {
515
624
  active = false;
516
625
  clearInterval(id);
517
626
  };
518
- }, [tab, interval2]);
627
+ }, [tab, interval2, tz]);
519
628
  const resetView = useCallback(() => {
520
629
  setCursor(0);
521
630
  setExpanded(-1);
@@ -535,6 +644,39 @@ function App({ interval: cliInterval }) {
535
644
  };
536
645
  }, [tab]);
537
646
  useInput((input, key) => {
647
+ if (showSettings && tzEdit !== null) {
648
+ if (key.escape) {
649
+ setTzEdit(null);
650
+ setTzError(null);
651
+ return;
652
+ }
653
+ if (key.return) {
654
+ const val = tzEdit.trim();
655
+ if (val === "" || val.toLowerCase() === "system") {
656
+ updateConfig((c) => ({ ...c, timezone: null }));
657
+ setTzEdit(null);
658
+ setTzError(null);
659
+ } else if (isValidTimezone(val)) {
660
+ updateConfig((c) => ({ ...c, timezone: val }));
661
+ setTzEdit(null);
662
+ setTzError(null);
663
+ } else {
664
+ setTzError(`Invalid: ${val}`);
665
+ }
666
+ return;
667
+ }
668
+ if (key.backspace || key.delete) {
669
+ setTzEdit((s) => (s ?? "").slice(0, -1));
670
+ setTzError(null);
671
+ return;
672
+ }
673
+ if (input && !key.ctrl && !key.meta) {
674
+ setTzEdit((s) => (s ?? "") + input);
675
+ setTzError(null);
676
+ return;
677
+ }
678
+ return;
679
+ }
538
680
  if (input === "q") {
539
681
  exit();
540
682
  return;
@@ -542,7 +684,7 @@ function App({ interval: cliInterval }) {
542
684
  if (showSettings) {
543
685
  if (key.escape || input === "s") setShowSettings(false);
544
686
  if (key.upArrow) setSettingsCursor((c) => Math.max(0, c - 1));
545
- if (key.downArrow) setSettingsCursor((c) => Math.min(2, c + 1));
687
+ if (key.downArrow) setSettingsCursor((c) => Math.min(SETTINGS_ROWS - 1, c + 1));
546
688
  if (settingsCursor === 0) {
547
689
  if (key.leftArrow) updateConfig((c) => ({ ...c, interval: Math.max(1, c.interval - 1) }));
548
690
  if (key.rightArrow) updateConfig((c) => ({ ...c, interval: c.interval + 1 }));
@@ -554,6 +696,15 @@ function App({ interval: cliInterval }) {
554
696
  if (settingsCursor === 2 && (key.leftArrow || key.rightArrow || key.return)) {
555
697
  updateConfig((c) => ({ ...c, clearScreen: !c.clearScreen }));
556
698
  }
699
+ if (settingsCursor === 3) {
700
+ if (key.return) {
701
+ setTzEdit(cfg.timezone ?? "");
702
+ setTzError(null);
703
+ }
704
+ if (key.leftArrow || key.rightArrow) {
705
+ updateConfig((c) => ({ ...c, timezone: c.timezone === null ? systemTimezone() : null }));
706
+ }
707
+ }
557
708
  return;
558
709
  }
559
710
  if (input === "s") {
@@ -667,10 +818,10 @@ function App({ interval: cliInterval }) {
667
818
  /* @__PURE__ */ jsx(PeakBadge, { peak: billing.peak }),
668
819
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \xB7 " })
669
820
  ] }),
670
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: time(updated) })
821
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: time(updated, tz) })
671
822
  ] })
672
823
  ] }),
673
- showSettings ? /* @__PURE__ */ jsx(SettingsView, { config: cfg, cursor: settingsCursor }) : /* @__PURE__ */ jsxs(Fragment, { children: [
824
+ showSettings ? /* @__PURE__ */ jsx(SettingsView, { config: cfg, cursor: settingsCursor, tzEdit, tzError, resolvedTz: tz }) : /* @__PURE__ */ jsxs(Fragment, { children: [
674
825
  /* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
675
826
  /* @__PURE__ */ jsx(TabBar, { tabs: TABS, active: tab, onSelect: (i) => {
676
827
  setTab(i);
@@ -753,7 +904,9 @@ function sortRows(rows, sortIdx) {
753
904
  return sorted;
754
905
  }
755
906
  }
756
- function SettingsView({ config: config2, cursor }) {
907
+ function SettingsView({ config: config2, cursor, tzEdit, tzError, resolvedTz }) {
908
+ const editing = tzEdit !== null;
909
+ const tzDisplay = config2.timezone === null ? `System (${resolvedTz})` : config2.timezone;
757
910
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
758
911
  /* @__PURE__ */ jsx(Text, { bold: true, children: "Settings" }),
759
912
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: configLocation() }),
@@ -804,8 +957,25 @@ function SettingsView({ config: config2, cursor }) {
804
957
  /* @__PURE__ */ jsx(Text, { children: "Clear screen " }),
805
958
  /* @__PURE__ */ jsx(Text, { bold: true, color: config2.clearScreen ? "green" : "red", children: config2.clearScreen ? "on" : "off" })
806
959
  ] }),
960
+ /* @__PURE__ */ jsxs(Box, { children: [
961
+ /* @__PURE__ */ jsxs(Text, { color: cursor === 3 ? "green" : void 0, children: [
962
+ cursor === 3 ? "\u25B8" : " ",
963
+ " "
964
+ ] }),
965
+ /* @__PURE__ */ jsx(Text, { children: "Timezone " }),
966
+ editing ? /* @__PURE__ */ jsxs(Fragment, { children: [
967
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "[" }),
968
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: tzEdit }),
969
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: "_" }),
970
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "]" })
971
+ ] }) : /* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: tzDisplay })
972
+ ] }),
973
+ cursor === 3 && tzError && /* @__PURE__ */ jsxs(Text, { color: "red", children: [
974
+ " ",
975
+ tzError
976
+ ] }),
807
977
  /* @__PURE__ */ jsx(Box, { height: 1 }),
808
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2191\u2193 select \u2190\u2192 adjust s/Esc close" })
978
+ editing ? /* @__PURE__ */ jsx(Text, { dimColor: true, children: "type IANA name (e.g. Europe/London) \xB7 empty = System \xB7 Enter save \xB7 Esc cancel" }) : /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2191\u2193 select \u2190\u2192 adjust Enter edit tz s/Esc close" })
809
979
  ] });
810
980
  }
811
981
  function DashboardView({ data, billing }) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokmon",
3
- "version": "0.10.0",
3
+ "version": "0.11.1",
4
4
  "description": "Terminal dashboard for Claude Code usage and costs",
5
5
  "type": "module",
6
6
  "bin": {