tokmon 0.9.1 → 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.
- package/README.md +3 -1
- package/dist/cli.js +114 -35
- 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
|
|
|
@@ -96,7 +97,8 @@ Sort by date or cost with `o`.
|
|
|
96
97
|
|
|
97
98
|
Press `s` to open. Persisted to `~/.config/tokmon/config.json` (macOS/Linux) or `%APPDATA%\tokmon\config.json` (Windows).
|
|
98
99
|
|
|
99
|
-
- **Refresh interval** —
|
|
100
|
+
- **Refresh interval** — dashboard poll rate (default: 2s)
|
|
101
|
+
- **Billing poll** — rate limits API poll rate (default: 5m, min 1m to avoid 429s)
|
|
100
102
|
- **Clear screen** — clears terminal on launch (like `watch`)
|
|
101
103
|
|
|
102
104
|
## How It Works
|
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, clearScreen: true };
|
|
11
|
+
var DEFAULTS = { interval: 2, billingInterval: 5, clearScreen: true };
|
|
12
12
|
function configDir() {
|
|
13
13
|
if (process.platform === "win32") {
|
|
14
14
|
return join(process.env.APPDATA ?? join(homedir(), "AppData", "Roaming"), "tokmon");
|
|
@@ -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 {
|
|
314
|
-
if (res.status === 401) return {
|
|
315
|
-
if (!res.ok) return {
|
|
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}` };
|
|
324
|
+
const data = await res.json();
|
|
325
|
+
return {
|
|
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
|
+
}
|
|
344
|
+
};
|
|
345
|
+
} catch {
|
|
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;
|
|
316
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;
|
|
317
362
|
return {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
|
363
|
+
state,
|
|
364
|
+
label: state === "peak" ? "Peak" : state === "weekend" ? "Weekend" : "Off-Peak",
|
|
365
|
+
minutesUntilChange: typeof data.minutesUntilChange === "number" ? data.minutesUntilChange : null
|
|
335
366
|
};
|
|
336
367
|
} catch {
|
|
337
|
-
return
|
|
368
|
+
return null;
|
|
338
369
|
}
|
|
339
370
|
}
|
|
340
371
|
function formatReset(iso) {
|
|
@@ -387,7 +418,7 @@ var VIEWS = ["Daily", "Weekly", "Monthly"];
|
|
|
387
418
|
var SORTS = ["date \u2191", "date \u2193", "cost \u2191", "cost \u2193"];
|
|
388
419
|
var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
389
420
|
var MONTHS = ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
390
|
-
var DEFAULT_CONFIG = { interval: 2, clearScreen: true };
|
|
421
|
+
var DEFAULT_CONFIG = { interval: 2, billingInterval: 5, clearScreen: true };
|
|
391
422
|
var IS_TTY = process.stdin.isTTY === true;
|
|
392
423
|
function App({ interval: cliInterval }) {
|
|
393
424
|
const [dashboard, setDashboard] = useState(null);
|
|
@@ -438,6 +469,7 @@ function App({ interval: cliInterval }) {
|
|
|
438
469
|
clearInterval(id);
|
|
439
470
|
};
|
|
440
471
|
}, [interval2]);
|
|
472
|
+
const billingMs = cfg.billingInterval * 6e4;
|
|
441
473
|
useEffect(() => {
|
|
442
474
|
let active = true;
|
|
443
475
|
const load = () => fetchBilling().then((b) => {
|
|
@@ -445,12 +477,12 @@ function App({ interval: cliInterval }) {
|
|
|
445
477
|
}).catch(() => {
|
|
446
478
|
});
|
|
447
479
|
load();
|
|
448
|
-
const id = setInterval(load,
|
|
480
|
+
const id = setInterval(load, billingMs);
|
|
449
481
|
return () => {
|
|
450
482
|
active = false;
|
|
451
483
|
clearInterval(id);
|
|
452
484
|
};
|
|
453
|
-
}, []);
|
|
485
|
+
}, [billingMs]);
|
|
454
486
|
useEffect(() => {
|
|
455
487
|
if (tab !== 1) return;
|
|
456
488
|
if (tableLoadedOnce.current && table) return;
|
|
@@ -503,23 +535,27 @@ function App({ interval: cliInterval }) {
|
|
|
503
535
|
};
|
|
504
536
|
}, [tab]);
|
|
505
537
|
useInput((input, key) => {
|
|
538
|
+
if (input === "q") {
|
|
539
|
+
exit();
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
506
542
|
if (showSettings) {
|
|
507
543
|
if (key.escape || input === "s") setShowSettings(false);
|
|
508
544
|
if (key.upArrow) setSettingsCursor((c) => Math.max(0, c - 1));
|
|
509
|
-
if (key.downArrow) setSettingsCursor((c) => Math.min(
|
|
545
|
+
if (key.downArrow) setSettingsCursor((c) => Math.min(2, c + 1));
|
|
510
546
|
if (settingsCursor === 0) {
|
|
511
547
|
if (key.leftArrow) updateConfig((c) => ({ ...c, interval: Math.max(1, c.interval - 1) }));
|
|
512
548
|
if (key.rightArrow) updateConfig((c) => ({ ...c, interval: c.interval + 1 }));
|
|
513
549
|
}
|
|
514
|
-
if (settingsCursor === 1
|
|
550
|
+
if (settingsCursor === 1) {
|
|
551
|
+
if (key.leftArrow) updateConfig((c) => ({ ...c, billingInterval: Math.max(1, c.billingInterval - 1) }));
|
|
552
|
+
if (key.rightArrow) updateConfig((c) => ({ ...c, billingInterval: c.billingInterval + 1 }));
|
|
553
|
+
}
|
|
554
|
+
if (settingsCursor === 2 && (key.leftArrow || key.rightArrow || key.return)) {
|
|
515
555
|
updateConfig((c) => ({ ...c, clearScreen: !c.clearScreen }));
|
|
516
556
|
}
|
|
517
557
|
return;
|
|
518
558
|
}
|
|
519
|
-
if (input === "q") {
|
|
520
|
-
exit();
|
|
521
|
-
return;
|
|
522
|
-
}
|
|
523
559
|
if (input === "s") {
|
|
524
560
|
setShowSettings(true);
|
|
525
561
|
return;
|
|
@@ -626,7 +662,13 @@ function App({ interval: cliInterval }) {
|
|
|
626
662
|
"s"
|
|
627
663
|
] })
|
|
628
664
|
] }),
|
|
629
|
-
/* @__PURE__ */
|
|
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
|
+
] })
|
|
630
672
|
] }),
|
|
631
673
|
showSettings ? /* @__PURE__ */ jsx(SettingsView, { config: cfg, cursor: settingsCursor }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
632
674
|
/* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
|
|
@@ -721,7 +763,7 @@ function SettingsView({ config: config2, cursor }) {
|
|
|
721
763
|
cursor === 0 ? "\u25B8" : " ",
|
|
722
764
|
" "
|
|
723
765
|
] }),
|
|
724
|
-
/* @__PURE__ */ jsx(Text, { children: "Refresh interval
|
|
766
|
+
/* @__PURE__ */ jsx(Text, { children: "Refresh interval " }),
|
|
725
767
|
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
726
768
|
"\u25C2",
|
|
727
769
|
" "
|
|
@@ -740,7 +782,26 @@ function SettingsView({ config: config2, cursor }) {
|
|
|
740
782
|
cursor === 1 ? "\u25B8" : " ",
|
|
741
783
|
" "
|
|
742
784
|
] }),
|
|
743
|
-
/* @__PURE__ */ jsx(Text, { children: "
|
|
785
|
+
/* @__PURE__ */ jsx(Text, { children: "Billing poll " }),
|
|
786
|
+
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
787
|
+
"\u25C2",
|
|
788
|
+
" "
|
|
789
|
+
] }),
|
|
790
|
+
/* @__PURE__ */ jsxs(Text, { bold: true, color: "yellow", children: [
|
|
791
|
+
config2.billingInterval,
|
|
792
|
+
"m"
|
|
793
|
+
] }),
|
|
794
|
+
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
795
|
+
" ",
|
|
796
|
+
"\u25B8"
|
|
797
|
+
] })
|
|
798
|
+
] }),
|
|
799
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
800
|
+
/* @__PURE__ */ jsxs(Text, { color: cursor === 2 ? "green" : void 0, children: [
|
|
801
|
+
cursor === 2 ? "\u25B8" : " ",
|
|
802
|
+
" "
|
|
803
|
+
] }),
|
|
804
|
+
/* @__PURE__ */ jsx(Text, { children: "Clear screen " }),
|
|
744
805
|
/* @__PURE__ */ jsx(Text, { bold: true, color: config2.clearScreen ? "green" : "red", children: config2.clearScreen ? "on" : "off" })
|
|
745
806
|
] }),
|
|
746
807
|
/* @__PURE__ */ jsx(Box, { height: 1 }),
|
|
@@ -812,6 +873,24 @@ function DashboardView({ data, billing }) {
|
|
|
812
873
|
)
|
|
813
874
|
] });
|
|
814
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
|
+
}
|
|
815
894
|
function LimitBar({ label, pct, resets }) {
|
|
816
895
|
const width = 30;
|
|
817
896
|
const filled = Math.round(pct / 100 * width);
|