tokmon 0.3.4 → 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 +185 -128
  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 },
@@ -228,30 +193,16 @@ function monthLabel(ts) {
228
193
  return new Date(ts).toISOString().slice(0, 7);
229
194
  }
230
195
  async function fetchDashboard() {
231
- const now = Date.now();
232
196
  const d = /* @__PURE__ */ new Date();
233
197
  const monthStart = new Date(d.getFullYear(), d.getMonth(), 1).getTime();
234
198
  const todayStart = new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
235
199
  const weekDay = d.getDay();
236
200
  const weekStart = new Date(d.getFullYear(), d.getMonth(), d.getDate() - (weekDay === 0 ? 6 : weekDay - 1)).getTime();
237
201
  const entries = await loadEntries(monthStart);
238
- const fiveHoursAgo = now - 5 * 36e5;
239
- const blockEntries = entries.filter((e) => e.ts >= fiveHoursAgo);
240
- let block = null;
241
- if (blockEntries.length > 0) {
242
- const spent = blockEntries.reduce((s, e) => s + e.cost, 0);
243
- const oldest = Math.min(...blockEntries.map((e) => e.ts));
244
- const elapsedHrs = (now - oldest) / 36e5;
245
- const burnRate = elapsedHrs > 0 ? spent / elapsedHrs : 0;
246
- const remainMs = Math.max(0, oldest + 5 * 36e5 - now);
247
- const percent = Math.min(100, (now - oldest) / (5 * 36e5) * 100);
248
- block = { spent, projected: burnRate * 5, burnRate, percent, remaining: minutes(remainMs / 6e4) };
249
- }
250
202
  return {
251
203
  today: sum(entries.filter((e) => e.ts >= todayStart)),
252
204
  week: sum(entries.filter((e) => e.ts >= weekStart)),
253
- month: sum(entries.filter((e) => e.ts >= monthStart)),
254
- block
205
+ month: sum(entries.filter((e) => e.ts >= monthStart))
255
206
  };
256
207
  }
257
208
  async function fetchTable() {
@@ -265,12 +216,113 @@ async function fetchTable() {
265
216
  };
266
217
  }
267
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
+
268
319
  // src/app.tsx
269
320
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
270
321
  var TABS = ["Dashboard", "Table"];
271
322
  var VIEWS = ["Daily", "Weekly", "Monthly"];
272
323
  function App({ interval: cliInterval }) {
273
324
  const [dashboard, setDashboard] = useState(null);
325
+ const [billing, setBilling] = useState(null);
274
326
  const [table, setTable] = useState(null);
275
327
  const [tableLoading, setTableLoading] = useState(false);
276
328
  const [error, setError] = useState(null);
@@ -283,18 +335,17 @@ function App({ interval: cliInterval }) {
283
335
  const [settingsCursor, setSettingsCursor] = useState(0);
284
336
  const tableLoadedOnce = useRef(false);
285
337
  const { stdout } = useStdout();
338
+ const { exit } = useApp();
286
339
  const rows = stdout?.rows ?? 24;
287
340
  const cols = stdout?.columns ?? 80;
288
341
  const interval2 = cliInterval ?? (config2?.interval ?? 2) * 1e3;
342
+ const cfg = config2 ?? { interval: 2, clearScreen: true };
289
343
  useEffect(() => {
290
344
  loadConfig().then((c) => {
291
345
  if (cliInterval) c = { ...c, interval: cliInterval / 1e3 };
292
346
  setConfig(c);
293
347
  });
294
348
  }, []);
295
- useEffect(() => {
296
- if (stdout) stdout.write("\x1B[2J\x1B[H");
297
- }, [tab, view, showSettings]);
298
349
  useEffect(() => {
299
350
  let active = true;
300
351
  const load = async () => {
@@ -316,6 +367,19 @@ function App({ interval: cliInterval }) {
316
367
  clearInterval(id);
317
368
  };
318
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
+ }, []);
319
383
  useEffect(() => {
320
384
  if (tab !== 1) return;
321
385
  if (tableLoadedOnce.current && table) return;
@@ -349,36 +413,29 @@ function App({ interval: cliInterval }) {
349
413
  clearInterval(id);
350
414
  };
351
415
  }, [tab, interval2]);
352
- const { exit } = useApp();
353
416
  const isTTY = process.stdin.isTTY === true;
354
- const settingsItems = 2;
355
- const cfg = config2 ?? { interval: 2, clearScreen: true };
356
417
  useInput((input, key) => {
357
418
  if (showSettings) {
358
419
  if (key.escape || input === "s") setShowSettings(false);
359
420
  if (key.upArrow) setSettingsCursor((c) => Math.max(0, c - 1));
360
- if (key.downArrow) setSettingsCursor((c) => Math.min(settingsItems - 1, c + 1));
421
+ if (key.downArrow) setSettingsCursor((c) => Math.min(1, c + 1));
361
422
  if (settingsCursor === 0) {
362
- if (key.leftArrow) {
363
- setConfig((c) => {
364
- const next = { ...c, interval: Math.max(1, c.interval - 1) };
365
- saveConfig(next);
366
- return next;
367
- });
368
- }
369
- if (key.rightArrow) {
370
- setConfig((c) => {
371
- const next = { ...c, interval: c.interval + 1 };
372
- saveConfig(next);
373
- return next;
374
- });
375
- }
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
+ });
376
433
  }
377
434
  if (settingsCursor === 1 && (key.leftArrow || key.rightArrow || key.return)) {
378
435
  setConfig((c) => {
379
- const next = { ...c, clearScreen: !c.clearScreen };
380
- saveConfig(next);
381
- return next;
436
+ const n = { ...c, clearScreen: !c.clearScreen };
437
+ saveConfig(n);
438
+ return n;
382
439
  });
383
440
  }
384
441
  return;
@@ -447,7 +504,7 @@ function App({ interval: cliInterval }) {
447
504
  if (error) return /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { color: "red", children: error }) });
448
505
  if (!dashboard) return /* @__PURE__ */ jsx(Box, { padding: 1, children: /* @__PURE__ */ jsx(Text, { dimColor: true, children: "Loading..." }) });
449
506
  const tableData = table ? [table.daily, table.weekly, table.monthly][view] : [];
450
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [
507
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, minHeight: rows, children: [
451
508
  /* @__PURE__ */ jsxs(Box, { justifyContent: "space-between", children: [
452
509
  /* @__PURE__ */ jsxs(Box, { children: [
453
510
  /* @__PURE__ */ jsxs(Text, { bold: true, color: "greenBright", children: [
@@ -468,7 +525,7 @@ function App({ interval: cliInterval }) {
468
525
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: " Tab/\u2190\u2192" })
469
526
  ] }),
470
527
  /* @__PURE__ */ jsx(Box, { height: 1 }),
471
- tab === 0 && /* @__PURE__ */ jsx(DashboardView, { data: dashboard }),
528
+ tab === 0 && /* @__PURE__ */ jsx(DashboardView, { data: dashboard, billing }),
472
529
  tab === 1 && /* @__PURE__ */ jsxs(Fragment, { children: [
473
530
  /* @__PURE__ */ jsx(ViewBar, { views: VIEWS, active: view }),
474
531
  /* @__PURE__ */ jsx(Box, { height: 1 }),
@@ -480,8 +537,7 @@ function App({ interval: cliInterval }) {
480
537
  /* @__PURE__ */ jsx(Text, { children: "David Ilie" }),
481
538
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: " (" }),
482
539
  /* @__PURE__ */ jsx(Text, { color: "cyan", children: "davidilie.com" }),
483
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: ") \xB7 " }),
484
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "s=settings q=quit" })
540
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: ") \xB7 s=settings q=quit" })
485
541
  ] })
486
542
  ] });
487
543
  }
@@ -542,7 +598,7 @@ function SettingsView({ config: config2, cursor }) {
542
598
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2191\u2193 select \u2190\u2192 adjust s/Esc close" })
543
599
  ] });
544
600
  }
545
- function DashboardView({ data }) {
601
+ function DashboardView({ data, billing }) {
546
602
  return /* @__PURE__ */ jsxs(Fragment, { children: [
547
603
  /* @__PURE__ */ jsxs(
548
604
  Box,
@@ -563,9 +619,39 @@ function DashboardView({ data }) {
563
619
  ]
564
620
  }
565
621
  ),
566
- data.block && /* @__PURE__ */ jsxs(Fragment, { children: [
622
+ billing && /* @__PURE__ */ jsxs(Fragment, { children: [
567
623
  /* @__PURE__ */ jsx(Box, { height: 1 }),
568
- /* @__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
+ )
569
655
  ] }),
570
656
  /* @__PURE__ */ jsx(Box, { height: 1 }),
571
657
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(50) }),
@@ -575,46 +661,24 @@ function DashboardView({ data }) {
575
661
  ] })
576
662
  ] });
577
663
  }
578
- function BlockView({ block }) {
579
- return /* @__PURE__ */ jsxs(
580
- Box,
581
- {
582
- flexDirection: "column",
583
- paddingLeft: 1,
584
- borderStyle: "bold",
585
- borderColor: "yellow",
586
- borderRight: false,
587
- borderTop: false,
588
- borderBottom: false,
589
- children: [
590
- /* @__PURE__ */ jsxs(Box, { children: [
591
- /* @__PURE__ */ jsx(Text, { bold: true, children: "Active Block" }),
592
- /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
593
- " ",
594
- block.remaining,
595
- " remaining"
596
- ] })
597
- ] }),
598
- /* @__PURE__ */ jsx(Box, { height: 1 }),
599
- /* @__PURE__ */ jsxs(Box, { children: [
600
- /* @__PURE__ */ jsx(ProgressBar, { percent: block.percent, width: 36 }),
601
- /* @__PURE__ */ jsx(Text, { children: " " }),
602
- /* @__PURE__ */ jsxs(Text, { bold: true, children: [
603
- Math.round(block.percent),
604
- "%"
605
- ] })
606
- ] }),
607
- /* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
608
- /* @__PURE__ */ jsx(Text, { color: "yellow", children: currency(block.spent) }),
609
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " spent \xB7 ~" }),
610
- /* @__PURE__ */ jsx(Text, { children: currency(block.projected) }),
611
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: " proj \xB7 " }),
612
- /* @__PURE__ */ jsx(Text, { color: "red", children: currency(block.burnRate) }),
613
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "/hr" })
614
- ] })
615
- ]
616
- }
617
- );
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
+ ] });
618
682
  }
619
683
  function SummaryRow({ label, summary }) {
620
684
  return /* @__PURE__ */ jsxs(Box, { children: [
@@ -626,13 +690,6 @@ function SummaryRow({ label, summary }) {
626
690
  ] }) })
627
691
  ] });
628
692
  }
629
- function ProgressBar({ percent, width = 36 }) {
630
- const filled = Math.round(percent / 100 * width);
631
- return /* @__PURE__ */ jsxs(Text, { children: [
632
- /* @__PURE__ */ jsx(Text, { color: "greenBright", children: "\u2501".repeat(filled) }),
633
- /* @__PURE__ */ jsx(Text, { dimColor: true, children: "\u2500".repeat(width - filled) })
634
- ] });
635
- }
636
693
  function TableView({ rows: allRows, scroll, maxRows, wide }) {
637
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 };
638
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.3.4",
3
+ "version": "0.5.0",
4
4
  "description": "Terminal dashboard for Claude Code usage and costs",
5
5
  "type": "module",
6
6
  "bin": {