tokmon 0.9.2 → 0.10.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 +82 -27
  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
@@ -297,10 +297,18 @@ async function getAccessToken() {
297
297
  }
298
298
  return readCredentialsFile();
299
299
  }
300
- var EMPTY = { session: null, weekly: null, sonnet: null, extraUsage: null, error: null };
300
+ var EMPTY = { session: null, weekly: null, sonnet: null, extraUsage: null, peak: null, error: null };
301
301
  async function fetchBilling() {
302
302
  const token = await getAccessToken();
303
303
  if (!token) return { ...EMPTY, error: "No OAuth token \u2014 run claude and log in" };
304
+ const [usageRes, peak] = await Promise.all([
305
+ fetchUsage(token),
306
+ fetchPeakStatus()
307
+ ]);
308
+ if ("error" in usageRes) return { ...EMPTY, peak, error: usageRes.error };
309
+ return { ...usageRes.data, peak, error: null };
310
+ }
311
+ async function fetchUsage(token) {
304
312
  try {
305
313
  const res = await fetch("https://api.anthropic.com/api/oauth/usage", {
306
314
  headers: {
@@ -310,31 +318,54 @@ async function fetchBilling() {
310
318
  },
311
319
  signal: AbortSignal.timeout(1e4)
312
320
  });
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}` };
321
+ if (res.status === 429) return { error: "Rate limited \u2014 retrying next poll" };
322
+ if (res.status === 401) return { error: "Token expired \u2014 restart Claude Code" };
323
+ if (!res.ok) return { error: `API ${res.status}` };
316
324
  const data = await res.json();
317
325
  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
326
+ data: {
327
+ session: data.five_hour ? {
328
+ utilization: data.five_hour.utilization,
329
+ resetsAt: formatReset(data.five_hour.resets_at)
330
+ } : null,
331
+ weekly: data.seven_day ? {
332
+ utilization: data.seven_day.utilization,
333
+ resetsAt: formatReset(data.seven_day.resets_at)
334
+ } : null,
335
+ sonnet: data.seven_day_sonnet ? {
336
+ utilization: data.seven_day_sonnet.utilization,
337
+ resetsAt: formatReset(data.seven_day_sonnet.resets_at)
338
+ } : null,
339
+ extraUsage: data.extra_usage?.is_enabled ? {
340
+ limit: data.extra_usage.monthly_limit / 100,
341
+ used: data.extra_usage.used_credits / 100
342
+ } : null
343
+ }
335
344
  };
336
345
  } catch {
337
- return { ...EMPTY, error: "Network error" };
346
+ return { error: "Network error" };
347
+ }
348
+ }
349
+ async function fetchPeakStatus() {
350
+ try {
351
+ const res = await fetch("https://promoclock.co/api/status", {
352
+ headers: { "Accept": "application/json", "User-Agent": "tokmon" },
353
+ signal: AbortSignal.timeout(3e3)
354
+ });
355
+ if (!res.ok) return null;
356
+ const data = await res.json();
357
+ let state;
358
+ if (data.isPeak === true || data.status === "peak") state = "peak";
359
+ else if (data.isWeekend === true || data.status === "weekend") state = "weekend";
360
+ else if (data.isOffPeak === true || data.status === "off_peak" || data.status === "off-peak") state = "off-peak";
361
+ else return null;
362
+ return {
363
+ state,
364
+ label: state === "peak" ? "Peak" : state === "weekend" ? "Weekend" : "Off-Peak",
365
+ minutesUntilChange: typeof data.minutesUntilChange === "number" ? data.minutesUntilChange : null
366
+ };
367
+ } catch {
368
+ return null;
338
369
  }
339
370
  }
340
371
  function formatReset(iso) {
@@ -504,6 +535,10 @@ function App({ interval: cliInterval }) {
504
535
  };
505
536
  }, [tab]);
506
537
  useInput((input, key) => {
538
+ if (input === "q") {
539
+ exit();
540
+ return;
541
+ }
507
542
  if (showSettings) {
508
543
  if (key.escape || input === "s") setShowSettings(false);
509
544
  if (key.upArrow) setSettingsCursor((c) => Math.max(0, c - 1));
@@ -521,10 +556,6 @@ function App({ interval: cliInterval }) {
521
556
  }
522
557
  return;
523
558
  }
524
- if (input === "q") {
525
- exit();
526
- return;
527
- }
528
559
  if (input === "s") {
529
560
  setShowSettings(true);
530
561
  return;
@@ -631,7 +662,13 @@ function App({ interval: cliInterval }) {
631
662
  "s"
632
663
  ] })
633
664
  ] }),
634
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: time(updated) })
665
+ /* @__PURE__ */ jsxs(Box, { children: [
666
+ billing?.peak && /* @__PURE__ */ jsxs(Fragment, { children: [
667
+ /* @__PURE__ */ jsx(PeakBadge, { peak: billing.peak }),
668
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: " \xB7 " })
669
+ ] }),
670
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: time(updated) })
671
+ ] })
635
672
  ] }),
636
673
  showSettings ? /* @__PURE__ */ jsx(SettingsView, { config: cfg, cursor: settingsCursor }) : /* @__PURE__ */ jsxs(Fragment, { children: [
637
674
  /* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
@@ -836,6 +873,24 @@ function DashboardView({ data, billing }) {
836
873
  )
837
874
  ] });
838
875
  }
876
+ function PeakBadge({ peak }) {
877
+ const color = peak.state === "peak" ? "red" : "green";
878
+ return /* @__PURE__ */ jsxs(Box, { children: [
879
+ /* @__PURE__ */ jsx(Text, { color, children: "\u25CF " }),
880
+ /* @__PURE__ */ jsx(Text, { bold: true, color, children: peak.label }),
881
+ peak.minutesUntilChange !== null && peak.minutesUntilChange > 0 && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
882
+ " (",
883
+ fmtMinutes(peak.minutesUntilChange),
884
+ ")"
885
+ ] })
886
+ ] });
887
+ }
888
+ function fmtMinutes(mins) {
889
+ if (mins < 60) return `${mins}m`;
890
+ const h = Math.floor(mins / 60);
891
+ const m = mins % 60;
892
+ return m === 0 ? `${h}h` : `${h}h ${m}m`;
893
+ }
839
894
  function LimitBar({ label, pct, resets }) {
840
895
  const width = 30;
841
896
  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.10.0",
4
4
  "description": "Terminal dashboard for Claude Code usage and costs",
5
5
  "type": "module",
6
6
  "bin": {