tokmon 0.4.0 → 0.5.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 +184 -140
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -46,41 +46,6 @@ import { createReadStream } from "fs";
46
46
  import { createInterface } from "readline";
47
47
  import { join as join2 } from "path";
48
48
  import { homedir as homedir2 } from "os";
49
-
50
- // src/format.ts
51
- function currency(value) {
52
- return `$${value.toFixed(2)}`;
53
- }
54
- function tokens(value) {
55
- if (value >= 1e9) return `${(value / 1e9).toFixed(1)}B`;
56
- if (value >= 1e6) return `${(value / 1e6).toFixed(1)}M`;
57
- if (value >= 1e3) return `${(value / 1e3).toFixed(1)}K`;
58
- return String(value);
59
- }
60
- function time(date) {
61
- return date.toLocaleTimeString(void 0, {
62
- hour: "2-digit",
63
- minute: "2-digit",
64
- second: "2-digit"
65
- });
66
- }
67
- function minutes(mins) {
68
- const h = Math.floor(mins / 60);
69
- const m = Math.round(mins % 60);
70
- return h > 0 ? `${h}h ${m}m` : `${m}m`;
71
- }
72
- function shortDate(iso) {
73
- const [, m, d] = iso.split("-");
74
- const months = ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
75
- return `${months[Number(m)]} ${Number(d).toString().padStart(2, " ")}`;
76
- }
77
- function col(s, w, align = "right") {
78
- if (s.length > w) return s.slice(0, w - 1) + "~";
79
- const spaces = " ".repeat(w - s.length);
80
- return align === "right" ? spaces + s : s + spaces;
81
- }
82
-
83
- // src/data.ts
84
49
  var PRICING = {
85
50
  "claude-opus-4": { i: 5e-6, o: 25e-6, cc: 625e-8, cr: 5e-7 },
86
51
  "claude-sonnet-4": { i: 3e-6, o: 15e-6, cc: 375e-8, cr: 3e-7 },
@@ -227,47 +192,17 @@ function isoWeekLabel(ts) {
227
192
  function monthLabel(ts) {
228
193
  return new Date(ts).toISOString().slice(0, 7);
229
194
  }
230
- function findBlockStart(entries, now) {
231
- const recent = entries.filter((e) => e.ts <= now).sort((a, b) => a.ts - b.ts);
232
- if (recent.length === 0) return 0;
233
- const GAP = 30 * 6e4;
234
- let blockStart = recent[0].ts;
235
- for (let i = 1; i < recent.length; i++) {
236
- if (recent[i].ts - recent[i - 1].ts > GAP) {
237
- blockStart = recent[i].ts;
238
- }
239
- }
240
- if (now - blockStart > 5 * 36e5) return 0;
241
- return blockStart;
242
- }
243
195
  async function fetchDashboard() {
244
- const now = Date.now();
245
196
  const d = /* @__PURE__ */ new Date();
246
197
  const monthStart = new Date(d.getFullYear(), d.getMonth(), 1).getTime();
247
198
  const todayStart = new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
248
199
  const weekDay = d.getDay();
249
200
  const weekStart = new Date(d.getFullYear(), d.getMonth(), d.getDate() - (weekDay === 0 ? 6 : weekDay - 1)).getTime();
250
201
  const entries = await loadEntries(monthStart);
251
- let block = null;
252
- const blockStart = findBlockStart(entries, now);
253
- if (blockStart > 0) {
254
- const blockEnd = blockStart + 5 * 36e5;
255
- const blockEntries = entries.filter((e) => e.ts >= blockStart && e.ts < blockEnd);
256
- if (blockEntries.length > 0) {
257
- const spent = blockEntries.reduce((s, e) => s + e.cost, 0);
258
- const elapsedMs = now - blockStart;
259
- const elapsedHrs = elapsedMs / 36e5;
260
- const burnRate = elapsedHrs > 0 ? spent / elapsedHrs : 0;
261
- const remainMs = Math.max(0, blockEnd - now);
262
- const percent = Math.min(100, elapsedMs / (5 * 36e5) * 100);
263
- block = { spent, projected: burnRate * 5, burnRate, percent, remaining: minutes(remainMs / 6e4) };
264
- }
265
- }
266
202
  return {
267
203
  today: sum(entries.filter((e) => e.ts >= todayStart)),
268
204
  week: sum(entries.filter((e) => e.ts >= weekStart)),
269
- month: sum(entries.filter((e) => e.ts >= monthStart)),
270
- block
205
+ month: sum(entries.filter((e) => e.ts >= monthStart))
271
206
  };
272
207
  }
273
208
  async function fetchTable() {
@@ -281,12 +216,113 @@ async function fetchTable() {
281
216
  };
282
217
  }
283
218
 
219
+ // src/billing.ts
220
+ import { execFile as execFileCb } from "child_process";
221
+ import { promisify } from "util";
222
+ var execFile = promisify(execFileCb);
223
+ async function getAccessToken() {
224
+ if (process.platform === "darwin") {
225
+ try {
226
+ const { stdout } = await execFile("security", [
227
+ "find-generic-password",
228
+ "-s",
229
+ "Claude Code-credentials",
230
+ "-w"
231
+ ], { timeout: 5e3 });
232
+ const creds = JSON.parse(stdout.trim());
233
+ return creds?.claudeAiOauth?.accessToken ?? null;
234
+ } catch {
235
+ return null;
236
+ }
237
+ }
238
+ return null;
239
+ }
240
+ async function fetchBilling() {
241
+ const token = await getAccessToken();
242
+ if (!token) return null;
243
+ try {
244
+ const res = await fetch("https://api.anthropic.com/api/oauth/usage", {
245
+ headers: {
246
+ "Authorization": `Bearer ${token}`,
247
+ "anthropic-beta": "oauth-2025-04-20",
248
+ "User-Agent": "tokmon"
249
+ },
250
+ signal: AbortSignal.timeout(1e4)
251
+ });
252
+ if (!res.ok) return null;
253
+ const data = await res.json();
254
+ return {
255
+ session: data.five_hour ? {
256
+ utilization: data.five_hour.utilization,
257
+ resetsAt: formatReset(data.five_hour.resets_at)
258
+ } : null,
259
+ weekly: data.seven_day ? {
260
+ utilization: data.seven_day.utilization,
261
+ resetsAt: formatReset(data.seven_day.resets_at)
262
+ } : null,
263
+ sonnet: data.seven_day_sonnet ? {
264
+ utilization: data.seven_day_sonnet.utilization,
265
+ resetsAt: formatReset(data.seven_day_sonnet.resets_at)
266
+ } : null,
267
+ extraUsage: data.extra_usage?.is_enabled ? {
268
+ limit: data.extra_usage.monthly_limit / 100,
269
+ used: data.extra_usage.used_credits / 100
270
+ } : null
271
+ };
272
+ } catch {
273
+ return null;
274
+ }
275
+ }
276
+ function formatReset(iso) {
277
+ const d = new Date(iso);
278
+ const now = /* @__PURE__ */ new Date();
279
+ const diff = d.getTime() - now.getTime();
280
+ if (diff <= 0) return "now";
281
+ const mins = Math.round(diff / 6e4);
282
+ if (mins < 60) return `${mins}m`;
283
+ const hrs = Math.floor(mins / 60);
284
+ const m = mins % 60;
285
+ if (hrs < 24) return `${hrs}h ${m}m`;
286
+ const days = Math.floor(hrs / 24);
287
+ const h = hrs % 24;
288
+ return `${days}d ${h}h`;
289
+ }
290
+
291
+ // src/format.ts
292
+ function currency(value) {
293
+ return `$${value.toFixed(2)}`;
294
+ }
295
+ function tokens(value) {
296
+ if (value >= 1e9) return `${(value / 1e9).toFixed(1)}B`;
297
+ if (value >= 1e6) return `${(value / 1e6).toFixed(1)}M`;
298
+ if (value >= 1e3) return `${(value / 1e3).toFixed(1)}K`;
299
+ return String(value);
300
+ }
301
+ function time(date) {
302
+ return date.toLocaleTimeString(void 0, {
303
+ hour: "2-digit",
304
+ minute: "2-digit",
305
+ second: "2-digit"
306
+ });
307
+ }
308
+ function shortDate(iso) {
309
+ const [, m, d] = iso.split("-");
310
+ const months = ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
311
+ return `${months[Number(m)]} ${Number(d).toString().padStart(2, " ")}`;
312
+ }
313
+ function col(s, w, align = "right") {
314
+ if (s.length > w) return s.slice(0, w - 1) + "~";
315
+ const spaces = " ".repeat(w - s.length);
316
+ return align === "right" ? spaces + s : s + spaces;
317
+ }
318
+
284
319
  // src/app.tsx
285
320
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
286
321
  var TABS = ["Dashboard", "Table"];
287
322
  var VIEWS = ["Daily", "Weekly", "Monthly"];
288
323
  function App({ interval: cliInterval }) {
289
324
  const [dashboard, setDashboard] = useState(null);
325
+ const [billing, setBilling] = useState(null);
290
326
  const [table, setTable] = useState(null);
291
327
  const [tableLoading, setTableLoading] = useState(false);
292
328
  const [error, setError] = useState(null);
@@ -299,9 +335,11 @@ function App({ interval: cliInterval }) {
299
335
  const [settingsCursor, setSettingsCursor] = useState(0);
300
336
  const tableLoadedOnce = useRef(false);
301
337
  const { stdout } = useStdout();
338
+ const { exit } = useApp();
302
339
  const rows = stdout?.rows ?? 24;
303
340
  const cols = stdout?.columns ?? 80;
304
341
  const interval2 = cliInterval ?? (config2?.interval ?? 2) * 1e3;
342
+ const cfg = config2 ?? { interval: 2, clearScreen: true };
305
343
  useEffect(() => {
306
344
  loadConfig().then((c) => {
307
345
  if (cliInterval) c = { ...c, interval: cliInterval / 1e3 };
@@ -329,6 +367,19 @@ function App({ interval: cliInterval }) {
329
367
  clearInterval(id);
330
368
  };
331
369
  }, [interval2]);
370
+ useEffect(() => {
371
+ let active = true;
372
+ const load = () => fetchBilling().then((b) => {
373
+ if (active && b) setBilling(b);
374
+ }).catch(() => {
375
+ });
376
+ load();
377
+ const id = setInterval(load, 12e4);
378
+ return () => {
379
+ active = false;
380
+ clearInterval(id);
381
+ };
382
+ }, []);
332
383
  useEffect(() => {
333
384
  if (tab !== 1) return;
334
385
  if (tableLoadedOnce.current && table) return;
@@ -362,36 +413,29 @@ function App({ interval: cliInterval }) {
362
413
  clearInterval(id);
363
414
  };
364
415
  }, [tab, interval2]);
365
- const { exit } = useApp();
366
416
  const isTTY = process.stdin.isTTY === true;
367
- const settingsItems = 2;
368
- const cfg = config2 ?? { interval: 2, clearScreen: true };
369
417
  useInput((input, key) => {
370
418
  if (showSettings) {
371
419
  if (key.escape || input === "s") setShowSettings(false);
372
420
  if (key.upArrow) setSettingsCursor((c) => Math.max(0, c - 1));
373
- if (key.downArrow) setSettingsCursor((c) => Math.min(settingsItems - 1, c + 1));
421
+ if (key.downArrow) setSettingsCursor((c) => Math.min(1, c + 1));
374
422
  if (settingsCursor === 0) {
375
- if (key.leftArrow) {
376
- setConfig((c) => {
377
- const next = { ...c, interval: Math.max(1, c.interval - 1) };
378
- saveConfig(next);
379
- return next;
380
- });
381
- }
382
- if (key.rightArrow) {
383
- setConfig((c) => {
384
- const next = { ...c, interval: c.interval + 1 };
385
- saveConfig(next);
386
- return next;
387
- });
388
- }
423
+ if (key.leftArrow) setConfig((c) => {
424
+ const n = { ...c, interval: Math.max(1, c.interval - 1) };
425
+ saveConfig(n);
426
+ return n;
427
+ });
428
+ if (key.rightArrow) setConfig((c) => {
429
+ const n = { ...c, interval: c.interval + 1 };
430
+ saveConfig(n);
431
+ return n;
432
+ });
389
433
  }
390
434
  if (settingsCursor === 1 && (key.leftArrow || key.rightArrow || key.return)) {
391
435
  setConfig((c) => {
392
- const next = { ...c, clearScreen: !c.clearScreen };
393
- saveConfig(next);
394
- return next;
436
+ const n = { ...c, clearScreen: !c.clearScreen };
437
+ saveConfig(n);
438
+ return n;
395
439
  });
396
440
  }
397
441
  return;
@@ -481,7 +525,7 @@ function App({ interval: cliInterval }) {
481
525
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: " Tab/\u2190\u2192" })
482
526
  ] }),
483
527
  /* @__PURE__ */ jsx(Box, { height: 1 }),
484
- tab === 0 && /* @__PURE__ */ jsx(DashboardView, { data: dashboard }),
528
+ tab === 0 && /* @__PURE__ */ jsx(DashboardView, { data: dashboard, billing }),
485
529
  tab === 1 && /* @__PURE__ */ jsxs(Fragment, { children: [
486
530
  /* @__PURE__ */ jsx(ViewBar, { views: VIEWS, active: view }),
487
531
  /* @__PURE__ */ jsx(Box, { height: 1 }),
@@ -493,8 +537,7 @@ function App({ interval: cliInterval }) {
493
537
  /* @__PURE__ */ jsx(Text, { children: "David Ilie" }),
494
538
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: " (" }),
495
539
  /* @__PURE__ */ jsx(Text, { color: "cyan", children: "davidilie.com" }),
496
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: ") \xB7 " }),
497
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "s=settings q=quit" })
540
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: ") \xB7 s=settings q=quit" })
498
541
  ] })
499
542
  ] });
500
543
  }
@@ -555,7 +598,7 @@ function SettingsView({ config: config2, cursor }) {
555
598
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2191\u2193 select \u2190\u2192 adjust s/Esc close" })
556
599
  ] });
557
600
  }
558
- function DashboardView({ data }) {
601
+ function DashboardView({ data, billing }) {
559
602
  return /* @__PURE__ */ jsxs(Fragment, { children: [
560
603
  /* @__PURE__ */ jsxs(
561
604
  Box,
@@ -576,9 +619,39 @@ function DashboardView({ data }) {
576
619
  ]
577
620
  }
578
621
  ),
579
- data.block && /* @__PURE__ */ jsxs(Fragment, { children: [
622
+ billing && /* @__PURE__ */ jsxs(Fragment, { children: [
580
623
  /* @__PURE__ */ jsx(Box, { height: 1 }),
581
- /* @__PURE__ */ jsx(BlockView, { block: data.block })
624
+ /* @__PURE__ */ jsxs(
625
+ Box,
626
+ {
627
+ flexDirection: "column",
628
+ paddingLeft: 1,
629
+ borderStyle: "bold",
630
+ borderColor: "yellow",
631
+ borderRight: false,
632
+ borderTop: false,
633
+ borderBottom: false,
634
+ children: [
635
+ /* @__PURE__ */ jsx(Text, { bold: true, children: "Rate Limits" }),
636
+ /* @__PURE__ */ jsx(Box, { height: 1 }),
637
+ billing.session && /* @__PURE__ */ jsx(LimitBar, { label: "Session", pct: billing.session.utilization, resets: billing.session.resetsAt }),
638
+ billing.weekly && /* @__PURE__ */ jsx(LimitBar, { label: "Weekly", pct: billing.weekly.utilization, resets: billing.weekly.resetsAt }),
639
+ billing.sonnet && /* @__PURE__ */ jsx(LimitBar, { label: "Sonnet", pct: billing.sonnet.utilization, resets: billing.sonnet.resetsAt }),
640
+ billing.extraUsage && /* @__PURE__ */ jsxs(Box, { children: [
641
+ /* @__PURE__ */ jsx(Box, { width: 10, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Extra" }) }),
642
+ /* @__PURE__ */ jsxs(Text, { color: "yellow", children: [
643
+ "$",
644
+ billing.extraUsage.used.toFixed(2)
645
+ ] }),
646
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
647
+ " / $",
648
+ billing.extraUsage.limit.toFixed(2),
649
+ " limit"
650
+ ] })
651
+ ] })
652
+ ]
653
+ }
654
+ )
582
655
  ] }),
583
656
  /* @__PURE__ */ jsx(Box, { height: 1 }),
584
657
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(50) }),
@@ -588,46 +661,24 @@ function DashboardView({ data }) {
588
661
  ] })
589
662
  ] });
590
663
  }
591
- function BlockView({ block }) {
592
- return /* @__PURE__ */ jsxs(
593
- Box,
594
- {
595
- flexDirection: "column",
596
- paddingLeft: 1,
597
- borderStyle: "bold",
598
- borderColor: "yellow",
599
- borderRight: false,
600
- borderTop: false,
601
- borderBottom: false,
602
- children: [
603
- /* @__PURE__ */ jsxs(Box, { children: [
604
- /* @__PURE__ */ jsx(Text, { bold: true, children: "Active Block" }),
605
- /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
606
- " ",
607
- block.remaining,
608
- " remaining"
609
- ] })
610
- ] }),
611
- /* @__PURE__ */ jsx(Box, { height: 1 }),
612
- /* @__PURE__ */ jsxs(Box, { children: [
613
- /* @__PURE__ */ jsx(ProgressBar, { percent: block.percent, width: 36 }),
614
- /* @__PURE__ */ jsx(Text, { children: " " }),
615
- /* @__PURE__ */ jsxs(Text, { bold: true, children: [
616
- Math.round(block.percent),
617
- "%"
618
- ] })
619
- ] }),
620
- /* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
621
- /* @__PURE__ */ jsx(Text, { color: "yellow", children: currency(block.spent) }),
622
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " spent \xB7 ~" }),
623
- /* @__PURE__ */ jsx(Text, { children: currency(block.projected) }),
624
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " proj \xB7 " }),
625
- /* @__PURE__ */ jsx(Text, { color: "red", children: currency(block.burnRate) }),
626
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "/hr" })
627
- ] })
628
- ]
629
- }
630
- );
664
+ function LimitBar({ label, pct, resets }) {
665
+ const width = 30;
666
+ const filled = Math.round(pct / 100 * width);
667
+ const color = pct >= 80 ? "red" : pct >= 50 ? "yellow" : "green";
668
+ return /* @__PURE__ */ jsxs(Box, { children: [
669
+ /* @__PURE__ */ jsx(Box, { width: 10, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: label }) }),
670
+ /* @__PURE__ */ jsx(Text, { color, children: "\u2501".repeat(filled) }),
671
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(width - filled) }),
672
+ /* @__PURE__ */ jsx(Text, { children: " " }),
673
+ /* @__PURE__ */ jsxs(Text, { bold: true, children: [
674
+ Math.round(pct),
675
+ "%"
676
+ ] }),
677
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
678
+ " resets ",
679
+ resets
680
+ ] })
681
+ ] });
631
682
  }
632
683
  function SummaryRow({ label, summary }) {
633
684
  return /* @__PURE__ */ jsxs(Box, { children: [
@@ -639,13 +690,6 @@ function SummaryRow({ label, summary }) {
639
690
  ] }) })
640
691
  ] });
641
692
  }
642
- function ProgressBar({ percent, width = 36 }) {
643
- const filled = Math.round(percent / 100 * width);
644
- return /* @__PURE__ */ jsxs(Text, { children: [
645
- /* @__PURE__ */ jsx(Text, { color: "greenBright", children: "\u2501".repeat(filled) }),
646
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(width - filled) })
647
- ] });
648
- }
649
693
  function TableView({ rows: allRows, scroll, maxRows, wide }) {
650
694
  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 };
651
695
  const lineW = W.label + W.models + W.input + W.output + W.cc + W.cr + W.total + W.cost;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokmon",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Terminal dashboard for Claude Code usage and costs",
5
5
  "type": "module",
6
6
  "bin": {