tokmon 0.9.0 → 0.9.2

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 +2 -1
  2. package/dist/cli.js +67 -23
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -96,7 +96,8 @@ Sort by date or cost with `o`.
96
96
 
97
97
  Press `s` to open. Persisted to `~/.config/tokmon/config.json` (macOS/Linux) or `%APPDATA%\tokmon\config.json` (Windows).
98
98
 
99
- - **Refresh interval** — adjust with `←` `→`
99
+ - **Refresh interval** — dashboard poll rate (default: 2s)
100
+ - **Billing poll** — rate limits API poll rate (default: 5m, min 1m to avoid 429s)
100
101
  - **Clear screen** — clears terminal on launch (like `watch`)
101
102
 
102
103
  ## 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");
@@ -258,29 +258,49 @@ async function fetchTable() {
258
258
 
259
259
  // src/billing.ts
260
260
  import { execFile as execFileCb } from "child_process";
261
+ import { readFile as readFile2 } from "fs/promises";
262
+ import { join as join3 } from "path";
263
+ import { homedir as homedir3 } from "os";
261
264
  import { promisify } from "util";
262
265
  var execFile = promisify(execFileCb);
266
+ function credentialsFilePath() {
267
+ const base = process.env.CLAUDE_CONFIG_DIR ?? join3(homedir3(), ".claude");
268
+ return join3(base, ".credentials.json");
269
+ }
270
+ async function readCredentialsFile() {
271
+ try {
272
+ const raw = await readFile2(credentialsFilePath(), "utf-8");
273
+ const creds = JSON.parse(raw);
274
+ return creds?.claudeAiOauth?.accessToken ?? creds?.accessToken ?? null;
275
+ } catch {
276
+ return null;
277
+ }
278
+ }
279
+ async function readMacKeychain() {
280
+ try {
281
+ const { stdout } = await execFile("security", [
282
+ "find-generic-password",
283
+ "-s",
284
+ "Claude Code-credentials",
285
+ "-w"
286
+ ], { timeout: 5e3 });
287
+ const creds = JSON.parse(stdout.trim());
288
+ return creds?.claudeAiOauth?.accessToken ?? creds?.accessToken ?? null;
289
+ } catch {
290
+ return null;
291
+ }
292
+ }
263
293
  async function getAccessToken() {
264
294
  if (process.platform === "darwin") {
265
- try {
266
- const { stdout } = await execFile("security", [
267
- "find-generic-password",
268
- "-s",
269
- "Claude Code-credentials",
270
- "-w"
271
- ], { timeout: 5e3 });
272
- const creds = JSON.parse(stdout.trim());
273
- return creds?.claudeAiOauth?.accessToken ?? null;
274
- } catch {
275
- return null;
276
- }
295
+ const token = await readMacKeychain();
296
+ if (token) return token;
277
297
  }
278
- return null;
298
+ return readCredentialsFile();
279
299
  }
280
300
  var EMPTY = { session: null, weekly: null, sonnet: null, extraUsage: null, error: null };
281
301
  async function fetchBilling() {
282
302
  const token = await getAccessToken();
283
- if (!token) return { ...EMPTY, error: "No OAuth token found (macOS Keychain)" };
303
+ if (!token) return { ...EMPTY, error: "No OAuth token \u2014 run claude and log in" };
284
304
  try {
285
305
  const res = await fetch("https://api.anthropic.com/api/oauth/usage", {
286
306
  headers: {
@@ -290,7 +310,7 @@ async function fetchBilling() {
290
310
  },
291
311
  signal: AbortSignal.timeout(1e4)
292
312
  });
293
- if (res.status === 429) return { ...EMPTY, error: "Rate limited \u2014 retrying in 2m" };
313
+ if (res.status === 429) return { ...EMPTY, error: "Rate limited \u2014 retrying next poll" };
294
314
  if (res.status === 401) return { ...EMPTY, error: "Token expired \u2014 restart Claude Code" };
295
315
  if (!res.ok) return { ...EMPTY, error: `API ${res.status}` };
296
316
  const data = await res.json();
@@ -367,7 +387,7 @@ var VIEWS = ["Daily", "Weekly", "Monthly"];
367
387
  var SORTS = ["date \u2191", "date \u2193", "cost \u2191", "cost \u2193"];
368
388
  var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
369
389
  var MONTHS = ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
370
- var DEFAULT_CONFIG = { interval: 2, clearScreen: true };
390
+ var DEFAULT_CONFIG = { interval: 2, billingInterval: 5, clearScreen: true };
371
391
  var IS_TTY = process.stdin.isTTY === true;
372
392
  function App({ interval: cliInterval }) {
373
393
  const [dashboard, setDashboard] = useState(null);
@@ -418,6 +438,7 @@ function App({ interval: cliInterval }) {
418
438
  clearInterval(id);
419
439
  };
420
440
  }, [interval2]);
441
+ const billingMs = cfg.billingInterval * 6e4;
421
442
  useEffect(() => {
422
443
  let active = true;
423
444
  const load = () => fetchBilling().then((b) => {
@@ -425,12 +446,12 @@ function App({ interval: cliInterval }) {
425
446
  }).catch(() => {
426
447
  });
427
448
  load();
428
- const id = setInterval(load, 12e4);
449
+ const id = setInterval(load, billingMs);
429
450
  return () => {
430
451
  active = false;
431
452
  clearInterval(id);
432
453
  };
433
- }, []);
454
+ }, [billingMs]);
434
455
  useEffect(() => {
435
456
  if (tab !== 1) return;
436
457
  if (tableLoadedOnce.current && table) return;
@@ -486,12 +507,16 @@ function App({ interval: cliInterval }) {
486
507
  if (showSettings) {
487
508
  if (key.escape || input === "s") setShowSettings(false);
488
509
  if (key.upArrow) setSettingsCursor((c) => Math.max(0, c - 1));
489
- if (key.downArrow) setSettingsCursor((c) => Math.min(1, c + 1));
510
+ if (key.downArrow) setSettingsCursor((c) => Math.min(2, c + 1));
490
511
  if (settingsCursor === 0) {
491
512
  if (key.leftArrow) updateConfig((c) => ({ ...c, interval: Math.max(1, c.interval - 1) }));
492
513
  if (key.rightArrow) updateConfig((c) => ({ ...c, interval: c.interval + 1 }));
493
514
  }
494
- if (settingsCursor === 1 && (key.leftArrow || key.rightArrow || key.return)) {
515
+ if (settingsCursor === 1) {
516
+ if (key.leftArrow) updateConfig((c) => ({ ...c, billingInterval: Math.max(1, c.billingInterval - 1) }));
517
+ if (key.rightArrow) updateConfig((c) => ({ ...c, billingInterval: c.billingInterval + 1 }));
518
+ }
519
+ if (settingsCursor === 2 && (key.leftArrow || key.rightArrow || key.return)) {
495
520
  updateConfig((c) => ({ ...c, clearScreen: !c.clearScreen }));
496
521
  }
497
522
  return;
@@ -701,7 +726,7 @@ function SettingsView({ config: config2, cursor }) {
701
726
  cursor === 0 ? "\u25B8" : " ",
702
727
  " "
703
728
  ] }),
704
- /* @__PURE__ */ jsx(Text, { children: "Refresh interval " }),
729
+ /* @__PURE__ */ jsx(Text, { children: "Refresh interval " }),
705
730
  /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
706
731
  "\u25C2",
707
732
  " "
@@ -720,7 +745,26 @@ function SettingsView({ config: config2, cursor }) {
720
745
  cursor === 1 ? "\u25B8" : " ",
721
746
  " "
722
747
  ] }),
723
- /* @__PURE__ */ jsx(Text, { children: "Clear screen " }),
748
+ /* @__PURE__ */ jsx(Text, { children: "Billing poll " }),
749
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
750
+ "\u25C2",
751
+ " "
752
+ ] }),
753
+ /* @__PURE__ */ jsxs(Text, { bold: true, color: "yellow", children: [
754
+ config2.billingInterval,
755
+ "m"
756
+ ] }),
757
+ /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
758
+ " ",
759
+ "\u25B8"
760
+ ] })
761
+ ] }),
762
+ /* @__PURE__ */ jsxs(Box, { children: [
763
+ /* @__PURE__ */ jsxs(Text, { color: cursor === 2 ? "green" : void 0, children: [
764
+ cursor === 2 ? "\u25B8" : " ",
765
+ " "
766
+ ] }),
767
+ /* @__PURE__ */ jsx(Text, { children: "Clear screen " }),
724
768
  /* @__PURE__ */ jsx(Text, { bold: true, color: config2.clearScreen ? "green" : "red", children: config2.clearScreen ? "on" : "off" })
725
769
  ] }),
726
770
  /* @__PURE__ */ jsx(Box, { height: 1 }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokmon",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "description": "Terminal dashboard for Claude Code usage and costs",
5
5
  "type": "module",
6
6
  "bin": {