tokmon 0.10.0 → 0.11.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 +206 -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
 
@@ -393,11 +490,12 @@ function tokens(value) {
393
490
  if (value >= 1e3) return `${(value / 1e3).toFixed(1)}K`;
394
491
  return String(value);
395
492
  }
396
- function time(date) {
493
+ function time(date, tz) {
397
494
  return date.toLocaleTimeString(void 0, {
398
495
  hour: "2-digit",
399
496
  minute: "2-digit",
400
- second: "2-digit"
497
+ second: "2-digit",
498
+ timeZone: tz
401
499
  });
402
500
  }
403
501
  var SHORT_MONTHS = ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
@@ -418,7 +516,8 @@ var VIEWS = ["Daily", "Weekly", "Monthly"];
418
516
  var SORTS = ["date \u2191", "date \u2193", "cost \u2191", "cost \u2193"];
419
517
  var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
420
518
  var MONTHS = ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
421
- var DEFAULT_CONFIG = { interval: 2, billingInterval: 5, clearScreen: true };
519
+ var DEFAULT_CONFIG = { interval: 2, billingInterval: 5, clearScreen: true, timezone: null };
520
+ var SETTINGS_ROWS = 4;
422
521
  var IS_TTY = process.stdin.isTTY === true;
423
522
  function App({ interval: cliInterval }) {
424
523
  const [dashboard, setDashboard] = useState(null);
@@ -431,10 +530,12 @@ function App({ interval: cliInterval }) {
431
530
  const [view, setView] = useState(0);
432
531
  const [cursor, setCursor] = useState(0);
433
532
  const [expanded, setExpanded] = useState(-1);
434
- const [sort, setSort] = useState(0);
533
+ const [sort, setSort] = useState(1);
435
534
  const [showSettings, setShowSettings] = useState(false);
436
535
  const [config2, setConfig] = useState(null);
437
536
  const [settingsCursor, setSettingsCursor] = useState(0);
537
+ const [tzEdit, setTzEdit] = useState(null);
538
+ const [tzError, setTzError] = useState(null);
438
539
  const tableLoadedOnce = useRef(false);
439
540
  const { stdout } = useStdout();
440
541
  const { exit } = useApp();
@@ -442,6 +543,7 @@ function App({ interval: cliInterval }) {
442
543
  const cols = stdout?.columns ?? 80;
443
544
  const interval2 = cliInterval ?? (config2?.interval ?? 2) * 1e3;
444
545
  const cfg = config2 ?? DEFAULT_CONFIG;
546
+ const tz = resolveTimezone(cfg.timezone);
445
547
  useEffect(() => {
446
548
  loadConfig().then((c) => {
447
549
  if (cliInterval) c = { ...c, interval: cliInterval / 1e3 };
@@ -452,7 +554,7 @@ function App({ interval: cliInterval }) {
452
554
  let active = true;
453
555
  const load = async () => {
454
556
  try {
455
- const result = await fetchDashboard();
557
+ const result = await fetchDashboard(tz);
456
558
  if (active) {
457
559
  setDashboard(result);
458
560
  setError(null);
@@ -468,7 +570,7 @@ function App({ interval: cliInterval }) {
468
570
  active = false;
469
571
  clearInterval(id);
470
572
  };
471
- }, [interval2]);
573
+ }, [interval2, tz]);
472
574
  const billingMs = cfg.billingInterval * 6e4;
473
575
  useEffect(() => {
474
576
  let active = true;
@@ -483,12 +585,16 @@ function App({ interval: cliInterval }) {
483
585
  clearInterval(id);
484
586
  };
485
587
  }, [billingMs]);
588
+ useEffect(() => {
589
+ tableLoadedOnce.current = false;
590
+ setTable(null);
591
+ }, [tz]);
486
592
  useEffect(() => {
487
593
  if (tab !== 1) return;
488
594
  if (tableLoadedOnce.current && table) return;
489
595
  let active = true;
490
596
  setTableLoading(true);
491
- fetchTable().then((result) => {
597
+ fetchTable(tz).then((result) => {
492
598
  if (active) {
493
599
  setTable(result);
494
600
  setTableLoading(false);
@@ -500,13 +606,13 @@ function App({ interval: cliInterval }) {
500
606
  return () => {
501
607
  active = false;
502
608
  };
503
- }, [tab]);
609
+ }, [tab, tz]);
504
610
  useEffect(() => {
505
611
  if (tab !== 1 || !tableLoadedOnce.current) return;
506
612
  let active = true;
507
613
  const id = setInterval(async () => {
508
614
  try {
509
- const result = await fetchTable();
615
+ const result = await fetchTable(tz);
510
616
  if (active) setTable(result);
511
617
  } catch {
512
618
  }
@@ -515,7 +621,7 @@ function App({ interval: cliInterval }) {
515
621
  active = false;
516
622
  clearInterval(id);
517
623
  };
518
- }, [tab, interval2]);
624
+ }, [tab, interval2, tz]);
519
625
  const resetView = useCallback(() => {
520
626
  setCursor(0);
521
627
  setExpanded(-1);
@@ -535,6 +641,39 @@ function App({ interval: cliInterval }) {
535
641
  };
536
642
  }, [tab]);
537
643
  useInput((input, key) => {
644
+ if (showSettings && tzEdit !== null) {
645
+ if (key.escape) {
646
+ setTzEdit(null);
647
+ setTzError(null);
648
+ return;
649
+ }
650
+ if (key.return) {
651
+ const val = tzEdit.trim();
652
+ if (val === "" || val.toLowerCase() === "system") {
653
+ updateConfig((c) => ({ ...c, timezone: null }));
654
+ setTzEdit(null);
655
+ setTzError(null);
656
+ } else if (isValidTimezone(val)) {
657
+ updateConfig((c) => ({ ...c, timezone: val }));
658
+ setTzEdit(null);
659
+ setTzError(null);
660
+ } else {
661
+ setTzError(`Invalid: ${val}`);
662
+ }
663
+ return;
664
+ }
665
+ if (key.backspace || key.delete) {
666
+ setTzEdit((s) => (s ?? "").slice(0, -1));
667
+ setTzError(null);
668
+ return;
669
+ }
670
+ if (input && !key.ctrl && !key.meta) {
671
+ setTzEdit((s) => (s ?? "") + input);
672
+ setTzError(null);
673
+ return;
674
+ }
675
+ return;
676
+ }
538
677
  if (input === "q") {
539
678
  exit();
540
679
  return;
@@ -542,7 +681,7 @@ function App({ interval: cliInterval }) {
542
681
  if (showSettings) {
543
682
  if (key.escape || input === "s") setShowSettings(false);
544
683
  if (key.upArrow) setSettingsCursor((c) => Math.max(0, c - 1));
545
- if (key.downArrow) setSettingsCursor((c) => Math.min(2, c + 1));
684
+ if (key.downArrow) setSettingsCursor((c) => Math.min(SETTINGS_ROWS - 1, c + 1));
546
685
  if (settingsCursor === 0) {
547
686
  if (key.leftArrow) updateConfig((c) => ({ ...c, interval: Math.max(1, c.interval - 1) }));
548
687
  if (key.rightArrow) updateConfig((c) => ({ ...c, interval: c.interval + 1 }));
@@ -554,6 +693,15 @@ function App({ interval: cliInterval }) {
554
693
  if (settingsCursor === 2 && (key.leftArrow || key.rightArrow || key.return)) {
555
694
  updateConfig((c) => ({ ...c, clearScreen: !c.clearScreen }));
556
695
  }
696
+ if (settingsCursor === 3) {
697
+ if (key.return) {
698
+ setTzEdit(cfg.timezone ?? "");
699
+ setTzError(null);
700
+ }
701
+ if (key.leftArrow || key.rightArrow) {
702
+ updateConfig((c) => ({ ...c, timezone: c.timezone === null ? systemTimezone() : null }));
703
+ }
704
+ }
557
705
  return;
558
706
  }
559
707
  if (input === "s") {
@@ -667,10 +815,10 @@ function App({ interval: cliInterval }) {
667
815
  /* @__PURE__ */ jsx(PeakBadge, { peak: billing.peak }),
668
816
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \xB7 " })
669
817
  ] }),
670
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: time(updated) })
818
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: time(updated, tz) })
671
819
  ] })
672
820
  ] }),
673
- showSettings ? /* @__PURE__ */ jsx(SettingsView, { config: cfg, cursor: settingsCursor }) : /* @__PURE__ */ jsxs(Fragment, { children: [
821
+ showSettings ? /* @__PURE__ */ jsx(SettingsView, { config: cfg, cursor: settingsCursor, tzEdit, tzError, resolvedTz: tz }) : /* @__PURE__ */ jsxs(Fragment, { children: [
674
822
  /* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
675
823
  /* @__PURE__ */ jsx(TabBar, { tabs: TABS, active: tab, onSelect: (i) => {
676
824
  setTab(i);
@@ -753,7 +901,9 @@ function sortRows(rows, sortIdx) {
753
901
  return sorted;
754
902
  }
755
903
  }
756
- function SettingsView({ config: config2, cursor }) {
904
+ function SettingsView({ config: config2, cursor, tzEdit, tzError, resolvedTz }) {
905
+ const editing = tzEdit !== null;
906
+ const tzDisplay = config2.timezone === null ? `System (${resolvedTz})` : config2.timezone;
757
907
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
758
908
  /* @__PURE__ */ jsx(Text, { bold: true, children: "Settings" }),
759
909
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: configLocation() }),
@@ -804,8 +954,25 @@ function SettingsView({ config: config2, cursor }) {
804
954
  /* @__PURE__ */ jsx(Text, { children: "Clear screen " }),
805
955
  /* @__PURE__ */ jsx(Text, { bold: true, color: config2.clearScreen ? "green" : "red", children: config2.clearScreen ? "on" : "off" })
806
956
  ] }),
957
+ /* @__PURE__ */ jsxs(Box, { children: [
958
+ /* @__PURE__ */ jsxs(Text, { color: cursor === 3 ? "green" : void 0, children: [
959
+ cursor === 3 ? "\u25B8" : " ",
960
+ " "
961
+ ] }),
962
+ /* @__PURE__ */ jsx(Text, { children: "Timezone " }),
963
+ editing ? /* @__PURE__ */ jsxs(Fragment, { children: [
964
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "[" }),
965
+ /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: tzEdit }),
966
+ /* @__PURE__ */ jsx(Text, { color: "cyan", children: "_" }),
967
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "]" })
968
+ ] }) : /* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: tzDisplay })
969
+ ] }),
970
+ cursor === 3 && tzError && /* @__PURE__ */ jsxs(Text, { color: "red", children: [
971
+ " ",
972
+ tzError
973
+ ] }),
807
974
  /* @__PURE__ */ jsx(Box, { height: 1 }),
808
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2191\u2193 select \u2190\u2192 adjust s/Esc close" })
975
+ 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
976
  ] });
810
977
  }
811
978
  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.0",
4
4
  "description": "Terminal dashboard for Claude Code usage and costs",
5
5
  "type": "module",
6
6
  "bin": {