tokmon 0.9.2 → 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 (3) hide show
  1. package/README.md +1 -0
  2. package/dist/cli.js +287 -65
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -73,6 +73,7 @@ Then just run `tokmon`. Press `q` to quit.
73
73
  - **Today / This Week / This Month** — cost and token summaries
74
74
  - **Burn rate** — current $/hr
75
75
  - **Rate Limits** — real-time session (5h), weekly (7d), and Sonnet utilization with reset countdowns, fetched from Anthropic's OAuth API
76
+ - **Peak / Off-Peak badge** — shown in the header, fetched from [promoclock.co](https://promoclock.co) (peak hours drain session limits faster)
76
77
 
77
78
  ### Table
78
79
 
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
 
@@ -297,10 +394,18 @@ async function getAccessToken() {
297
394
  }
298
395
  return readCredentialsFile();
299
396
  }
300
- var EMPTY = { session: null, weekly: null, sonnet: null, extraUsage: null, error: null };
397
+ var EMPTY = { session: null, weekly: null, sonnet: null, extraUsage: null, peak: null, error: null };
301
398
  async function fetchBilling() {
302
399
  const token = await getAccessToken();
303
400
  if (!token) return { ...EMPTY, error: "No OAuth token \u2014 run claude and log in" };
401
+ const [usageRes, peak] = await Promise.all([
402
+ fetchUsage(token),
403
+ fetchPeakStatus()
404
+ ]);
405
+ if ("error" in usageRes) return { ...EMPTY, peak, error: usageRes.error };
406
+ return { ...usageRes.data, peak, error: null };
407
+ }
408
+ async function fetchUsage(token) {
304
409
  try {
305
410
  const res = await fetch("https://api.anthropic.com/api/oauth/usage", {
306
411
  headers: {
@@ -310,31 +415,54 @@ async function fetchBilling() {
310
415
  },
311
416
  signal: AbortSignal.timeout(1e4)
312
417
  });
313
- if (res.status === 429) return { ...EMPTY, error: "Rate limited \u2014 retrying next poll" };
314
- if (res.status === 401) return { ...EMPTY, error: "Token expired \u2014 restart Claude Code" };
315
- if (!res.ok) return { ...EMPTY, error: `API ${res.status}` };
418
+ if (res.status === 429) return { error: "Rate limited \u2014 retrying next poll" };
419
+ if (res.status === 401) return { error: "Token expired \u2014 restart Claude Code" };
420
+ if (!res.ok) return { error: `API ${res.status}` };
316
421
  const data = await res.json();
317
422
  return {
318
- session: data.five_hour ? {
319
- utilization: data.five_hour.utilization,
320
- resetsAt: formatReset(data.five_hour.resets_at)
321
- } : null,
322
- weekly: data.seven_day ? {
323
- utilization: data.seven_day.utilization,
324
- resetsAt: formatReset(data.seven_day.resets_at)
325
- } : null,
326
- sonnet: data.seven_day_sonnet ? {
327
- utilization: data.seven_day_sonnet.utilization,
328
- resetsAt: formatReset(data.seven_day_sonnet.resets_at)
329
- } : null,
330
- extraUsage: data.extra_usage?.is_enabled ? {
331
- limit: data.extra_usage.monthly_limit / 100,
332
- used: data.extra_usage.used_credits / 100
333
- } : null,
334
- error: null
423
+ data: {
424
+ session: data.five_hour ? {
425
+ utilization: data.five_hour.utilization,
426
+ resetsAt: formatReset(data.five_hour.resets_at)
427
+ } : null,
428
+ weekly: data.seven_day ? {
429
+ utilization: data.seven_day.utilization,
430
+ resetsAt: formatReset(data.seven_day.resets_at)
431
+ } : null,
432
+ sonnet: data.seven_day_sonnet ? {
433
+ utilization: data.seven_day_sonnet.utilization,
434
+ resetsAt: formatReset(data.seven_day_sonnet.resets_at)
435
+ } : null,
436
+ extraUsage: data.extra_usage?.is_enabled ? {
437
+ limit: data.extra_usage.monthly_limit / 100,
438
+ used: data.extra_usage.used_credits / 100
439
+ } : null
440
+ }
335
441
  };
336
442
  } catch {
337
- return { ...EMPTY, error: "Network error" };
443
+ return { error: "Network error" };
444
+ }
445
+ }
446
+ async function fetchPeakStatus() {
447
+ try {
448
+ const res = await fetch("https://promoclock.co/api/status", {
449
+ headers: { "Accept": "application/json", "User-Agent": "tokmon" },
450
+ signal: AbortSignal.timeout(3e3)
451
+ });
452
+ if (!res.ok) return null;
453
+ const data = await res.json();
454
+ let state;
455
+ if (data.isPeak === true || data.status === "peak") state = "peak";
456
+ else if (data.isWeekend === true || data.status === "weekend") state = "weekend";
457
+ else if (data.isOffPeak === true || data.status === "off_peak" || data.status === "off-peak") state = "off-peak";
458
+ else return null;
459
+ return {
460
+ state,
461
+ label: state === "peak" ? "Peak" : state === "weekend" ? "Weekend" : "Off-Peak",
462
+ minutesUntilChange: typeof data.minutesUntilChange === "number" ? data.minutesUntilChange : null
463
+ };
464
+ } catch {
465
+ return null;
338
466
  }
339
467
  }
340
468
  function formatReset(iso) {
@@ -362,11 +490,12 @@ function tokens(value) {
362
490
  if (value >= 1e3) return `${(value / 1e3).toFixed(1)}K`;
363
491
  return String(value);
364
492
  }
365
- function time(date) {
493
+ function time(date, tz) {
366
494
  return date.toLocaleTimeString(void 0, {
367
495
  hour: "2-digit",
368
496
  minute: "2-digit",
369
- second: "2-digit"
497
+ second: "2-digit",
498
+ timeZone: tz
370
499
  });
371
500
  }
372
501
  var SHORT_MONTHS = ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
@@ -387,7 +516,8 @@ var VIEWS = ["Daily", "Weekly", "Monthly"];
387
516
  var SORTS = ["date \u2191", "date \u2193", "cost \u2191", "cost \u2193"];
388
517
  var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
389
518
  var MONTHS = ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
390
- 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;
391
521
  var IS_TTY = process.stdin.isTTY === true;
392
522
  function App({ interval: cliInterval }) {
393
523
  const [dashboard, setDashboard] = useState(null);
@@ -400,10 +530,12 @@ function App({ interval: cliInterval }) {
400
530
  const [view, setView] = useState(0);
401
531
  const [cursor, setCursor] = useState(0);
402
532
  const [expanded, setExpanded] = useState(-1);
403
- const [sort, setSort] = useState(0);
533
+ const [sort, setSort] = useState(1);
404
534
  const [showSettings, setShowSettings] = useState(false);
405
535
  const [config2, setConfig] = useState(null);
406
536
  const [settingsCursor, setSettingsCursor] = useState(0);
537
+ const [tzEdit, setTzEdit] = useState(null);
538
+ const [tzError, setTzError] = useState(null);
407
539
  const tableLoadedOnce = useRef(false);
408
540
  const { stdout } = useStdout();
409
541
  const { exit } = useApp();
@@ -411,6 +543,7 @@ function App({ interval: cliInterval }) {
411
543
  const cols = stdout?.columns ?? 80;
412
544
  const interval2 = cliInterval ?? (config2?.interval ?? 2) * 1e3;
413
545
  const cfg = config2 ?? DEFAULT_CONFIG;
546
+ const tz = resolveTimezone(cfg.timezone);
414
547
  useEffect(() => {
415
548
  loadConfig().then((c) => {
416
549
  if (cliInterval) c = { ...c, interval: cliInterval / 1e3 };
@@ -421,7 +554,7 @@ function App({ interval: cliInterval }) {
421
554
  let active = true;
422
555
  const load = async () => {
423
556
  try {
424
- const result = await fetchDashboard();
557
+ const result = await fetchDashboard(tz);
425
558
  if (active) {
426
559
  setDashboard(result);
427
560
  setError(null);
@@ -437,7 +570,7 @@ function App({ interval: cliInterval }) {
437
570
  active = false;
438
571
  clearInterval(id);
439
572
  };
440
- }, [interval2]);
573
+ }, [interval2, tz]);
441
574
  const billingMs = cfg.billingInterval * 6e4;
442
575
  useEffect(() => {
443
576
  let active = true;
@@ -452,12 +585,16 @@ function App({ interval: cliInterval }) {
452
585
  clearInterval(id);
453
586
  };
454
587
  }, [billingMs]);
588
+ useEffect(() => {
589
+ tableLoadedOnce.current = false;
590
+ setTable(null);
591
+ }, [tz]);
455
592
  useEffect(() => {
456
593
  if (tab !== 1) return;
457
594
  if (tableLoadedOnce.current && table) return;
458
595
  let active = true;
459
596
  setTableLoading(true);
460
- fetchTable().then((result) => {
597
+ fetchTable(tz).then((result) => {
461
598
  if (active) {
462
599
  setTable(result);
463
600
  setTableLoading(false);
@@ -469,13 +606,13 @@ function App({ interval: cliInterval }) {
469
606
  return () => {
470
607
  active = false;
471
608
  };
472
- }, [tab]);
609
+ }, [tab, tz]);
473
610
  useEffect(() => {
474
611
  if (tab !== 1 || !tableLoadedOnce.current) return;
475
612
  let active = true;
476
613
  const id = setInterval(async () => {
477
614
  try {
478
- const result = await fetchTable();
615
+ const result = await fetchTable(tz);
479
616
  if (active) setTable(result);
480
617
  } catch {
481
618
  }
@@ -484,7 +621,7 @@ function App({ interval: cliInterval }) {
484
621
  active = false;
485
622
  clearInterval(id);
486
623
  };
487
- }, [tab, interval2]);
624
+ }, [tab, interval2, tz]);
488
625
  const resetView = useCallback(() => {
489
626
  setCursor(0);
490
627
  setExpanded(-1);
@@ -504,10 +641,47 @@ function App({ interval: cliInterval }) {
504
641
  };
505
642
  }, [tab]);
506
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
+ }
677
+ if (input === "q") {
678
+ exit();
679
+ return;
680
+ }
507
681
  if (showSettings) {
508
682
  if (key.escape || input === "s") setShowSettings(false);
509
683
  if (key.upArrow) setSettingsCursor((c) => Math.max(0, c - 1));
510
- if (key.downArrow) setSettingsCursor((c) => Math.min(2, c + 1));
684
+ if (key.downArrow) setSettingsCursor((c) => Math.min(SETTINGS_ROWS - 1, c + 1));
511
685
  if (settingsCursor === 0) {
512
686
  if (key.leftArrow) updateConfig((c) => ({ ...c, interval: Math.max(1, c.interval - 1) }));
513
687
  if (key.rightArrow) updateConfig((c) => ({ ...c, interval: c.interval + 1 }));
@@ -519,10 +693,15 @@ function App({ interval: cliInterval }) {
519
693
  if (settingsCursor === 2 && (key.leftArrow || key.rightArrow || key.return)) {
520
694
  updateConfig((c) => ({ ...c, clearScreen: !c.clearScreen }));
521
695
  }
522
- return;
523
- }
524
- if (input === "q") {
525
- exit();
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
+ }
526
705
  return;
527
706
  }
528
707
  if (input === "s") {
@@ -631,9 +810,15 @@ function App({ interval: cliInterval }) {
631
810
  "s"
632
811
  ] })
633
812
  ] }),
634
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: time(updated) })
813
+ /* @__PURE__ */ jsxs(Box, { children: [
814
+ billing?.peak && /* @__PURE__ */ jsxs(Fragment, { children: [
815
+ /* @__PURE__ */ jsx(PeakBadge, { peak: billing.peak }),
816
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \xB7 " })
817
+ ] }),
818
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: time(updated, tz) })
819
+ ] })
635
820
  ] }),
636
- 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: [
637
822
  /* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
638
823
  /* @__PURE__ */ jsx(TabBar, { tabs: TABS, active: tab, onSelect: (i) => {
639
824
  setTab(i);
@@ -716,7 +901,9 @@ function sortRows(rows, sortIdx) {
716
901
  return sorted;
717
902
  }
718
903
  }
719
- 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;
720
907
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
721
908
  /* @__PURE__ */ jsx(Text, { bold: true, children: "Settings" }),
722
909
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: configLocation() }),
@@ -767,8 +954,25 @@ function SettingsView({ config: config2, cursor }) {
767
954
  /* @__PURE__ */ jsx(Text, { children: "Clear screen " }),
768
955
  /* @__PURE__ */ jsx(Text, { bold: true, color: config2.clearScreen ? "green" : "red", children: config2.clearScreen ? "on" : "off" })
769
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
+ ] }),
770
974
  /* @__PURE__ */ jsx(Box, { height: 1 }),
771
- /* @__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" })
772
976
  ] });
773
977
  }
774
978
  function DashboardView({ data, billing }) {
@@ -836,6 +1040,24 @@ function DashboardView({ data, billing }) {
836
1040
  )
837
1041
  ] });
838
1042
  }
1043
+ function PeakBadge({ peak }) {
1044
+ const color = peak.state === "peak" ? "red" : "green";
1045
+ return /* @__PURE__ */ jsxs(Box, { children: [
1046
+ /* @__PURE__ */ jsx(Text, { color, children: "\u25CF " }),
1047
+ /* @__PURE__ */ jsx(Text, { bold: true, color, children: peak.label }),
1048
+ peak.minutesUntilChange !== null && peak.minutesUntilChange > 0 && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
1049
+ " (",
1050
+ fmtMinutes(peak.minutesUntilChange),
1051
+ ")"
1052
+ ] })
1053
+ ] });
1054
+ }
1055
+ function fmtMinutes(mins) {
1056
+ if (mins < 60) return `${mins}m`;
1057
+ const h = Math.floor(mins / 60);
1058
+ const m = mins % 60;
1059
+ return m === 0 ? `${h}h` : `${h}h ${m}m`;
1060
+ }
839
1061
  function LimitBar({ label, pct, resets }) {
840
1062
  const width = 30;
841
1063
  const filled = Math.round(pct / 100 * width);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokmon",
3
- "version": "0.9.2",
3
+ "version": "0.11.0",
4
4
  "description": "Terminal dashboard for Claude Code usage and costs",
5
5
  "type": "module",
6
6
  "bin": {