tokmon 0.22.5 → 0.22.7

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.
@@ -6,7 +6,7 @@ import {
6
6
  expandHome,
7
7
  isValidTimezone,
8
8
  slugify
9
- } from "./chunk-QTHCHB7S.js";
9
+ } from "./chunk-TNIADJ3W.js";
10
10
 
11
11
  // src/providers/usage-core.ts
12
12
  import { readFile, writeFile, rename, mkdir, readdir, unlink } from "fs/promises";
@@ -188,7 +188,7 @@ function encode(mtimeMs, size, entries) {
188
188
  return { m: mtimeMs, s: size, mods, rows };
189
189
  }
190
190
  function decode(s) {
191
- const num = (x) => typeof x === "number" && Number.isFinite(x) && x >= 0 ? x : 0;
191
+ const num2 = (x) => typeof x === "number" && Number.isFinite(x) && x >= 0 ? x : 0;
192
192
  const out = [];
193
193
  for (const r of s.rows) {
194
194
  if (!Array.isArray(r) || r.length < 8) continue;
@@ -198,12 +198,12 @@ function decode(s) {
198
198
  out.push({
199
199
  ts,
200
200
  model: typeof mi === "number" && typeof s.mods[mi] === "string" ? s.mods[mi] : "unknown",
201
- input: num(r[2]),
202
- output: num(r[3]),
203
- cacheCreate: num(r[4]),
204
- cacheRead: num(r[5]),
205
- cost: num(r[6]),
206
- cacheSavings: num(r[7]),
201
+ input: num2(r[2]),
202
+ output: num2(r[3]),
203
+ cacheCreate: num2(r[4]),
204
+ cacheRead: num2(r[5]),
205
+ cost: num2(r[6]),
206
+ cacheSavings: num2(r[7]),
207
207
  id: typeof r[8] === "string" ? r[8] : void 0
208
208
  });
209
209
  }
@@ -391,13 +391,14 @@ function groupBy(entries, keyFn) {
391
391
  let input = 0, output = 0, cacheCreate = 0, cacheRead = 0, cacheSavings = 0, cost = 0, count = 0;
392
392
  const byModel = /* @__PURE__ */ new Map();
393
393
  for (const e of group) {
394
+ const n = e.count ?? 1;
394
395
  input += e.input;
395
396
  output += e.output;
396
397
  cacheCreate += e.cacheCreate;
397
398
  cacheRead += e.cacheRead;
398
399
  cacheSavings += e.cacheSavings;
399
400
  cost += e.cost;
400
- count += 1;
401
+ count += n;
401
402
  const m = byModel.get(e.model);
402
403
  if (m) {
403
404
  m.input += e.input;
@@ -406,7 +407,7 @@ function groupBy(entries, keyFn) {
406
407
  m.cacheRead += e.cacheRead;
407
408
  m.cacheSavings += e.cacheSavings;
408
409
  m.cost += e.cost;
409
- m.count += 1;
410
+ m.count += n;
410
411
  } else {
411
412
  byModel.set(e.model, {
412
413
  name: e.model,
@@ -416,7 +417,7 @@ function groupBy(entries, keyFn) {
416
417
  cacheRead: e.cacheRead,
417
418
  cacheSavings: e.cacheSavings,
418
419
  cost: e.cost,
419
- count: 1
420
+ count: n
420
421
  });
421
422
  }
422
423
  }
@@ -556,10 +557,167 @@ function col(s, w, align = "right") {
556
557
  }
557
558
  var resetIn = formatResetIn;
558
559
 
559
- // src/providers/cursor/billing.ts
560
- import { access } from "fs/promises";
560
+ // src/providers/claude/usage.ts
561
+ import { stat as fsStat, access } from "fs/promises";
562
+ import { createReadStream } from "fs";
563
+ import { createInterface } from "readline";
564
+ import { join as join2, isAbsolute } from "path";
565
+ import { homedir } from "os";
566
+ var PRICING = {
567
+ "claude-opus-4-1": { i: 15e-6, o: 75e-6, cc: 1875e-8, cr: 15e-7 },
568
+ "claude-opus-4-0": { i: 15e-6, o: 75e-6, cc: 1875e-8, cr: 15e-7 },
569
+ "claude-opus-4-20250514": { i: 15e-6, o: 75e-6, cc: 1875e-8, cr: 15e-7 },
570
+ "claude-opus-4": { i: 5e-6, o: 25e-6, cc: 625e-8, cr: 5e-7 },
571
+ "claude-3-opus": { i: 15e-6, o: 75e-6, cc: 1875e-8, cr: 15e-7 },
572
+ "claude-sonnet-4": { i: 3e-6, o: 15e-6, cc: 375e-8, cr: 3e-7 },
573
+ // intro pricing through 2026-08-31 — revert to 3/15/3.75/0.3 after.
574
+ "claude-sonnet-5": { i: 2e-6, o: 1e-5, cc: 25e-7, cr: 2e-7 },
575
+ "claude-haiku-4": { i: 1e-6, o: 5e-6, cc: 125e-8, cr: 1e-7 },
576
+ "claude-fable-5": { i: 1e-5, o: 5e-5, cc: 125e-7, cr: 1e-6 }
577
+ };
578
+ var PRICE_KEYS = Object.keys(PRICING).sort((a, b) => b.length - a.length);
579
+ var ZERO_PRICE = { i: 0, o: 0, cc: 0, cr: 0 };
580
+ function claudeConfigDirs(homeDir) {
581
+ if (homeDir) {
582
+ return [join2(homeDir, ".claude"), join2(homeDir, ".config", "claude")];
583
+ }
584
+ const home = homedir();
585
+ const dirs = [join2(home, ".claude")];
586
+ const xdg = envDir("XDG_CONFIG_HOME");
587
+ if (xdg) {
588
+ dirs.push(join2(xdg, "claude"));
589
+ }
590
+ if (process.platform !== "win32") {
591
+ dirs.push(join2(home, ".config", "claude"));
592
+ }
593
+ const appData = envDir("APPDATA");
594
+ if (appData) dirs.push(join2(appData, "claude"));
595
+ if (process.env.CLAUDE_CONFIG_DIR) {
596
+ for (const p of process.env.CLAUDE_CONFIG_DIR.split(process.platform === "win32" ? ";" : ",")) {
597
+ const t = p.trim();
598
+ if (t && isAbsolute(t)) dirs.push(t);
599
+ }
600
+ }
601
+ return [...new Set(dirs)];
602
+ }
603
+ function getClaudeDirs(homeDir) {
604
+ return claudeConfigDirs(homeDir).map((d) => join2(d, "projects"));
605
+ }
606
+ async function detectClaude(homeDir) {
607
+ for (const dir of getClaudeDirs(homeDir)) {
608
+ try {
609
+ await access(dir);
610
+ return true;
611
+ } catch {
612
+ }
613
+ }
614
+ return false;
615
+ }
616
+ function priceFor(model) {
617
+ const m = model.toLowerCase().trim();
618
+ for (const key of PRICE_KEYS) {
619
+ if (!m.startsWith(key)) continue;
620
+ const rest = m.slice(key.length);
621
+ if (rest === "" || rest[0] === "-") return PRICING[key];
622
+ }
623
+ return ZERO_PRICE;
624
+ }
625
+ function costOf(model, u, cacheCreate5m, cacheCreate1h, hasCacheCreateSplit) {
626
+ const p = priceFor(model);
627
+ const cacheCreateCost = hasCacheCreateSplit ? cacheCreate5m * p.cc + cacheCreate1h * (2 * p.i) : safeNum(u.cache_creation_input_tokens) * p.cc;
628
+ return safeNum(u.input_tokens) * p.i + safeNum(u.output_tokens) * p.o + cacheCreateCost + safeNum(u.cache_read_input_tokens) * p.cr;
629
+ }
630
+ function shortModel(model) {
631
+ return model.replace("claude-", "").replace(/-\d{8}$/, "");
632
+ }
633
+ function timestampMs(value) {
634
+ if (typeof value === "number" && Number.isFinite(value)) return value > 1e10 ? value : value * 1e3;
635
+ if (typeof value === "string" && value.trim()) return new Date(value.trim()).getTime();
636
+ return NaN;
637
+ }
638
+ async function parseFile(path) {
639
+ const entries = [];
640
+ const input = createReadStream(path);
641
+ input.on("error", () => {
642
+ });
643
+ const rl = createInterface({ input, crlfDelay: Infinity });
644
+ try {
645
+ for await (const line of rl) {
646
+ if (!line.includes('"usage"')) continue;
647
+ try {
648
+ const obj = JSON.parse(line.charCodeAt(0) === 65279 ? line.slice(1) : line);
649
+ if (obj.type !== "assistant" || !obj.message?.usage) continue;
650
+ const ts = timestampMs(obj.timestamp);
651
+ if (!Number.isFinite(ts)) continue;
652
+ const u = obj.message.usage;
653
+ const model = typeof obj.message.model === "string" && obj.message.model ? obj.message.model : "unknown";
654
+ const inputTokens = safeNum(u.input_tokens);
655
+ const output = safeNum(u.output_tokens);
656
+ const hasCacheCreateSplit = u.cache_creation?.ephemeral_5m_input_tokens !== void 0 || u.cache_creation?.ephemeral_1h_input_tokens !== void 0;
657
+ const cacheCreate5m = safeNum(u.cache_creation?.ephemeral_5m_input_tokens);
658
+ const cacheCreate1h = safeNum(u.cache_creation?.ephemeral_1h_input_tokens);
659
+ const cacheCreate = hasCacheCreateSplit ? cacheCreate5m + cacheCreate1h : safeNum(u.cache_creation_input_tokens);
660
+ const cacheRead = safeNum(u.cache_read_input_tokens);
661
+ if (inputTokens + output + cacheCreate + cacheRead === 0) continue;
662
+ const p = priceFor(model);
663
+ const msgId = obj.message?.id;
664
+ entries.push({
665
+ id: msgId ? msgId + (obj.requestId ? ":" + obj.requestId : "") : void 0,
666
+ ts,
667
+ model: shortModel(model),
668
+ cost: costOf(model, u, cacheCreate5m, cacheCreate1h, hasCacheCreateSplit),
669
+ input: inputTokens,
670
+ output,
671
+ cacheCreate,
672
+ cacheRead,
673
+ cacheSavings: cacheRead * (p.i - p.cr)
674
+ });
675
+ } catch {
676
+ }
677
+ }
678
+ } catch {
679
+ }
680
+ return entries;
681
+ }
682
+ async function loadEntries(since, homeDir) {
683
+ const files = [];
684
+ const seen = /* @__PURE__ */ new Set();
685
+ const seenIno = /* @__PURE__ */ new Set();
686
+ for (const dir of getClaudeDirs(homeDir)) {
687
+ const listing = await walkFiles(dir);
688
+ for (const f of listing) {
689
+ if (!f.endsWith(".jsonl")) continue;
690
+ const path = join2(dir, f);
691
+ if (seen.has(path)) continue;
692
+ seen.add(path);
693
+ try {
694
+ const s = await fsStat(path);
695
+ if (s.mtimeMs < since) continue;
696
+ if (s.ino && process.platform !== "win32") {
697
+ const idn = `${s.dev}:${s.ino}`;
698
+ if (seenIno.has(idn)) continue;
699
+ seenIno.add(idn);
700
+ }
701
+ files.push({ path, mtimeMs: s.mtimeMs, size: s.size });
702
+ } catch {
703
+ }
704
+ }
705
+ }
706
+ return loadCachedEntries(files, parseFile, since);
707
+ }
708
+ async function claudeDashboard(tz, homeDir) {
709
+ const entries = await loadEntries(dashboardSince(tz), homeDir);
710
+ return summarize(entries, tz);
711
+ }
712
+ async function claudeTable(tz, homeDir) {
713
+ const entries = await loadEntries(tableSince(tz), homeDir);
714
+ return tabulate(entries, tz);
715
+ }
716
+
717
+ // src/providers/claude/billing.ts
718
+ import { access as access2, readFile as readFile2 } from "fs/promises";
561
719
  import { join as join4 } from "path";
562
- import { homedir as homedir2 } from "os";
720
+ import { homedir as homedir3 } from "os";
563
721
 
564
722
  // src/http.ts
565
723
  async function readJson(res) {
@@ -580,814 +738,205 @@ function identityFields(identity) {
580
738
  };
581
739
  }
582
740
 
583
- // src/providers/_shared/jwt.ts
584
- function decodeBase64UrlJson(segment) {
585
- try {
586
- const normalized = segment.replace(/-/g, "+").replace(/_/g, "/");
587
- const padded = normalized + "=".repeat((4 - normalized.length % 4) % 4);
588
- return JSON.parse(Buffer.from(padded, "base64").toString("utf8"));
589
- } catch {
590
- return null;
591
- }
592
- }
593
- function identityFromIdToken(idToken) {
594
- if (typeof idToken !== "string" || !idToken.includes(".")) return {};
595
- const payload = decodeBase64UrlJson(idToken.split(".")[1]);
596
- if (!payload || typeof payload !== "object") return {};
597
- const email = typeof payload.email === "string" && payload.email.trim() ? payload.email.trim() : void 0;
598
- const displayName = typeof payload.name === "string" && payload.name.trim() ? payload.name.trim() : typeof payload.given_name === "string" && payload.given_name.trim() ? payload.given_name.trim() : void 0;
599
- return { email, displayName, payload };
600
- }
601
-
602
741
  // src/providers/_shared/time.ts
603
742
  function msToIso(ms) {
604
743
  return Number.isFinite(ms) && Math.abs(ms) <= 864e13 ? new Date(ms).toISOString() : null;
605
744
  }
606
745
 
607
- // src/providers/cursor/activity.ts
608
- import { join as join3 } from "path";
609
- import { homedir } from "os";
610
-
611
- // src/providers/cursor/sqlite.ts
746
+ // src/providers/_shared/keychain.ts
612
747
  import { execFile as execFileCb } from "child_process";
613
- import { accessSync, constants } from "fs";
614
- import { delimiter, join as join2 } from "path";
615
748
  import { promisify } from "util";
616
749
  var execFile = promisify(execFileCb);
617
- var nativeDb;
618
- async function getNativeDb() {
619
- if (nativeDb !== void 0) return nativeDb;
750
+ var GO_KEYRING_PREFIX = "go-keyring-base64:";
751
+ async function readMacKeychainRaw(service) {
752
+ if (process.platform !== "darwin") return null;
620
753
  try {
621
- nativeDb = (await import("sqlite")).DatabaseSync;
754
+ const { stdout } = await execFile("security", [
755
+ "find-generic-password",
756
+ "-s",
757
+ service,
758
+ "-w"
759
+ ], { timeout: 5e3 });
760
+ const raw = stdout.trim();
761
+ return raw || null;
622
762
  } catch {
623
- nativeDb = null;
624
- }
625
- return nativeDb;
626
- }
627
- function classify(msg) {
628
- if (/database is (locked|busy)|SQLITE_(BUSY|LOCKED)|EBUSY|sharing violation|resource busy|readonly/i.test(msg)) return "locked";
629
- if (/unable to open|no such file|cannot open|ENOENT/i.test(msg)) return "missing";
630
- if (/no such (function|table|column)|unknown option/i.test(msg)) return "old";
631
- return "error";
632
- }
633
- async function runSqlite(db, sql, params = []) {
634
- const DB = await getNativeDb();
635
- if (DB) {
636
- let handle;
637
- let nativeErr;
638
- try {
639
- try {
640
- handle = new DB(db, { readOnly: true, timeout: 1500 });
641
- } catch (e) {
642
- if (!(e instanceof TypeError)) throw e;
643
- handle = new DB(db, { readOnly: true });
644
- }
645
- const rows = handle.prepare(sql).all(...params);
646
- return { status: "ok", rows };
647
- } catch (e) {
648
- nativeErr = e;
649
- } finally {
650
- try {
651
- handle?.close();
652
- } catch {
653
- }
654
- }
655
- if (nativeErr) {
656
- return { status: classify(String(nativeErr?.message ?? nativeErr)), rows: [] };
657
- }
763
+ return null;
658
764
  }
659
- return runSqliteCli(db, sql, params);
660
765
  }
661
- function isExec(p) {
766
+ async function readMacKeychainFileRaw(service, keychainPath) {
767
+ if (process.platform !== "darwin") return null;
662
768
  try {
663
- accessSync(p, process.platform === "win32" ? constants.F_OK : constants.X_OK);
664
- return true;
769
+ await execFile("security", ["unlock-keychain", "-p", "", keychainPath], { timeout: 5e3 });
770
+ const { stdout } = await execFile("security", [
771
+ "find-generic-password",
772
+ "-s",
773
+ service,
774
+ "-w",
775
+ keychainPath
776
+ ], { timeout: 5e3 });
777
+ const raw = stdout.trim();
778
+ return raw || null;
665
779
  } catch {
666
- return false;
780
+ return null;
667
781
  }
668
782
  }
669
- function resolveSqliteCli() {
670
- const names = process.platform === "win32" ? ["sqlite3.exe", "sqlite3.cmd", "sqlite3.bat"] : ["sqlite3"];
671
- for (const dir of (process.env.PATH ?? "").split(delimiter).filter(Boolean)) {
672
- for (const name of names) {
673
- const path = join2(dir, name);
674
- if (isExec(path)) return path;
675
- }
676
- }
677
- return null;
783
+ function unwrapGoKeyringBase64(raw) {
784
+ if (!raw.startsWith(GO_KEYRING_PREFIX)) return raw;
785
+ return Buffer.from(raw.slice(GO_KEYRING_PREFIX.length), "base64").toString("utf-8");
678
786
  }
679
- function inlineParams(sql, params) {
680
- let i = 0;
681
- return sql.replace(/\?/g, () => {
682
- const p = params[i++];
683
- return typeof p === "number" ? String(p) : `'${String(p).replace(/'/g, "''")}'`;
684
- });
787
+
788
+ // src/providers/claude/identity.ts
789
+ import { readFileSync } from "fs";
790
+ import { join as join3 } from "path";
791
+ import { homedir as homedir2 } from "os";
792
+ function titleWords(value) {
793
+ return value.split(/[_\s-]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()).join(" ");
685
794
  }
686
- async function runSqliteCli(db, sql, params) {
687
- const sqlite = resolveSqliteCli();
688
- if (!sqlite) return { status: "missing", rows: [] };
689
- try {
690
- const { stdout } = await execFile(
691
- sqlite,
692
- ["-readonly", "-json", "-cmd", ".timeout 1500", db, inlineParams(sql, params)],
693
- { timeout: 1e4, maxBuffer: 8 << 20 }
694
- );
695
- const text = stdout.trim();
696
- if (!text) return { status: "ok", rows: [] };
697
- try {
698
- return { status: "ok", rows: JSON.parse(text) };
699
- } catch {
700
- return { status: "error", rows: [] };
701
- }
702
- } catch (e) {
703
- const err = e;
704
- if (err?.code === "ENOENT") return { status: "missing", rows: [] };
705
- return { status: classify(String(err?.stderr ?? err?.message ?? "")), rows: [] };
706
- }
795
+ function claudeOrgPlanLabel(orgType) {
796
+ if (typeof orgType !== "string" || !orgType.trim()) return null;
797
+ const normalized = orgType.trim().toLowerCase();
798
+ const stripped = normalized.startsWith("claude_") ? normalized.slice("claude_".length) : normalized;
799
+ const label = titleWords(stripped);
800
+ return label ? `Claude ${label}` : null;
707
801
  }
708
- function sqliteStatusMessage(status) {
709
- switch (status) {
710
- case "missing":
711
- return "Cursor data not found \u2014 open Cursor";
712
- case "old":
713
- return "Cursor DB unreadable";
714
- case "locked":
715
- return "Cursor DB busy \u2014 retrying next poll";
716
- default:
717
- return "Cursor data unavailable";
802
+ function readClaudeIdentity(homeDir) {
803
+ const base = homeDir ? expandHome(homeDir) : homedir2();
804
+ try {
805
+ const parsed = JSON.parse(readFileSync(join3(base, ".claude.json"), "utf-8"));
806
+ const oauth = parsed?.oauthAccount;
807
+ const email = typeof oauth?.emailAddress === "string" && oauth.emailAddress.trim() ? oauth.emailAddress.trim() : void 0;
808
+ const displayName = typeof oauth?.displayName === "string" && oauth.displayName.trim() ? oauth.displayName.trim() : void 0;
809
+ const plan = claudeOrgPlanLabel(parsed?.organizationType);
810
+ const accountUuid = typeof oauth?.accountUuid === "string" && oauth.accountUuid.trim() ? oauth.accountUuid.trim() : void 0;
811
+ return { email, displayName, plan: plan ?? void 0, accountUuid };
812
+ } catch {
813
+ return {};
718
814
  }
719
815
  }
720
816
 
721
- // src/providers/cursor/activity.ts
722
- var DAY_MS2 = 864e5;
723
- var HOUR_MS = 36e5;
724
- function trackingDb(homeDir) {
725
- return join3(homeDir ?? homedir(), ".cursor", "ai-tracking", "ai-code-tracking.db");
726
- }
727
- async function cursorActivity(tz, homeDir) {
728
- const db = trackingDb(homeDir);
817
+ // src/providers/claude/billing.ts
818
+ function parseAuth(raw) {
729
819
  try {
730
- const now = Date.now();
731
- const res = await runSqlite(
732
- db,
733
- `SELECT createdAt/${HOUR_MS} AS h, count(*) AS c FROM ai_code_hashes WHERE source!='human' AND createdAt >= ${Math.floor(now - 30 * DAY_MS2)} GROUP BY h;`
734
- );
735
- if (res.status !== "ok") return null;
736
- const byDay = /* @__PURE__ */ new Map();
737
- let month = 0;
738
- for (const row of res.rows) {
739
- const raw = Number(row.c);
740
- const n = Number.isFinite(raw) && raw > 0 ? raw : 0;
741
- const hour = Number(row.h);
742
- if (!Number.isFinite(hour)) continue;
743
- const key = dayKey(hour * HOUR_MS + HOUR_MS / 2, tz);
744
- byDay.set(key, (byDay.get(key) ?? 0) + n);
745
- month += n;
746
- }
747
- const series = lastDayKeys(now, tz, SPARK_DAYS).map((k) => byDay.get(k) ?? 0);
748
- if (month === 0 && series.every((v) => v === 0)) return null;
749
- return { series, summary: `${tokens(month)} lines` };
820
+ const creds = JSON.parse(raw);
821
+ const o = creds?.claudeAiOauth ?? creds;
822
+ const token = o?.accessToken;
823
+ if (typeof token !== "string" || !token) return null;
824
+ return {
825
+ token,
826
+ subscriptionType: typeof o.subscriptionType === "string" ? o.subscriptionType : void 0,
827
+ rateLimitTier: typeof o.rateLimitTier === "string" ? o.rateLimitTier : void 0,
828
+ expiresAt: typeof o.expiresAt === "number" && Number.isFinite(o.expiresAt) ? o.expiresAt : void 0
829
+ };
750
830
  } catch {
751
831
  return null;
752
832
  }
753
833
  }
754
-
755
- // src/providers/cursor/billing.ts
756
- var BASE = "https://api2.cursor.sh/aiserver.v1.DashboardService";
757
- var USAGE_URL = `${BASE}/GetCurrentPeriodUsage`;
758
- var PLAN_URL = `${BASE}/GetPlanInfo`;
759
- var CREDITS_URL = `${BASE}/GetCreditGrantsBalance`;
760
- var STRIPE_URL = "https://cursor.com/api/auth/stripe";
761
- function cursorStateDb(homeDir) {
762
- const base = homeDir ?? homedir2();
763
- const tail = ["Cursor", "User", "globalStorage", "state.vscdb"];
764
- if (process.platform === "darwin") {
765
- return join4(base, "Library", "Application Support", ...tail);
766
- }
767
- if (process.platform === "win32") {
768
- const roaming = homeDir ? join4(homeDir, "AppData", "Roaming") : envDir("APPDATA") ?? join4(base, "AppData", "Roaming");
769
- return join4(roaming, ...tail);
770
- }
771
- const cfg = homeDir ? join4(homeDir, ".config") : envDir("XDG_CONFIG_HOME") ?? join4(base, ".config");
772
- return join4(cfg, ...tail);
773
- }
774
- async function detectCursor(homeDir) {
775
- try {
776
- await access(cursorStateDb(homeDir));
777
- return true;
778
- } catch {
779
- return false;
834
+ async function readCredentialsFile(homeDir) {
835
+ for (const dir of claudeConfigDirs(homeDir)) {
836
+ try {
837
+ const auth = parseAuth(await readFile2(join4(dir, ".credentials.json"), "utf-8"));
838
+ if (auth) return auth;
839
+ } catch {
840
+ }
780
841
  }
842
+ return null;
781
843
  }
782
- async function readState(db, key) {
783
- const r = await runSqlite(db, "SELECT value FROM ItemTable WHERE key=? LIMIT 1;", [key]);
784
- const raw = r.status === "ok" ? r.rows[0]?.value : void 0;
785
- return { value: typeof raw === "string" && raw.trim() ? raw.trim() : null, status: r.status };
844
+ async function readMacKeychain() {
845
+ const raw = await readMacKeychainRaw("Claude Code-credentials");
846
+ return raw ? parseAuth(raw) : null;
786
847
  }
787
- function cleanStoredString(value) {
788
- if (!value) return void 0;
848
+ async function readHomeKeychain(homeDir) {
849
+ if (process.platform !== "darwin") return null;
850
+ const keychainPath = join4(homeDir, "Library", "Keychains", "login.keychain-db");
789
851
  try {
790
- const parsed = JSON.parse(value);
791
- if (typeof parsed === "string" && parsed.trim()) return parsed.trim();
852
+ await access2(keychainPath);
792
853
  } catch {
854
+ return null;
793
855
  }
794
- const trimmed = value.trim().replace(/^"|"$/g, "");
795
- return trimmed || void 0;
856
+ const raw = await readMacKeychainFileRaw("Claude Code-credentials", keychainPath);
857
+ return raw ? parseAuth(raw) : null;
796
858
  }
797
- function cursorSessionToken(accessToken) {
798
- const payload = accessToken.includes(".") ? decodeBase64UrlJson(accessToken.split(".")[1]) : null;
799
- const subject = typeof payload?.sub === "string" ? payload.sub : null;
800
- if (!subject) return null;
801
- const parts = subject.split("|");
802
- const userId = (parts.length > 1 ? parts[1] : parts[0]).trim();
803
- return userId ? `${userId}%3A%3A${accessToken}` : null;
859
+ async function authCandidates(homeDir) {
860
+ const expandedHomeDir = homeDir ? expandHome(homeDir) : void 0;
861
+ const isDefault = !expandedHomeDir || expandedHomeDir === homedir3();
862
+ const out = [];
863
+ const file = await readCredentialsFile(isDefault ? void 0 : expandedHomeDir);
864
+ const keychain = process.platform === "darwin" ? await readMacKeychain() : null;
865
+ const homeKeychain = !isDefault && expandedHomeDir ? await readHomeKeychain(expandedHomeDir) : null;
866
+ const ordered = isDefault ? [keychain && { auth: keychain, shared: true }, file && { auth: file, shared: false }] : [
867
+ homeKeychain && { auth: homeKeychain, shared: false },
868
+ file && { auth: file, shared: false },
869
+ keychain && { auth: keychain, shared: true }
870
+ ];
871
+ for (const c of ordered) if (c) out.push(c);
872
+ return out;
804
873
  }
805
- async function connectPost(url, token) {
874
+ var tokenIdentityCache = /* @__PURE__ */ new Map();
875
+ async function tokenIdentity(token) {
876
+ if (tokenIdentityCache.has(token)) return tokenIdentityCache.get(token);
806
877
  try {
807
- const res = await fetch(url, {
808
- method: "POST",
878
+ const res = await fetch("https://api.anthropic.com/api/oauth/profile", {
809
879
  headers: {
810
880
  "Authorization": `Bearer ${token}`,
811
- "Content-Type": "application/json",
812
- "Connect-Protocol-Version": "1",
881
+ "anthropic-beta": "oauth-2025-04-20",
813
882
  "User-Agent": "tokmon"
814
883
  },
815
- body: "{}",
816
884
  signal: AbortSignal.timeout(1e4)
817
885
  });
818
- if (!res.ok) return { __status: res.status };
819
- return await readJson(res);
886
+ if (res.status === 401 || res.status === 403) {
887
+ tokenIdentityCache.set(token, null);
888
+ return null;
889
+ }
890
+ if (!res.ok) return void 0;
891
+ const data = await readJson(res);
892
+ const uuid = data?.account?.uuid;
893
+ if (typeof uuid !== "string" || !uuid) return void 0;
894
+ const identity = {
895
+ accountUuid: uuid,
896
+ email: typeof data?.account?.email === "string" ? data.account.email : null
897
+ };
898
+ if (tokenIdentityCache.size > 64) tokenIdentityCache.clear();
899
+ tokenIdentityCache.set(token, identity);
900
+ return identity;
820
901
  } catch {
821
- return null;
902
+ return void 0;
822
903
  }
823
904
  }
824
- async function connectGetWithSession(url, token) {
825
- const session = cursorSessionToken(token);
826
- if (!session) return null;
827
- try {
828
- const res = await fetch(url, {
829
- headers: {
830
- "Cookie": `WorkosCursorSessionToken=${session}`,
831
- "User-Agent": "tokmon"
832
- },
833
- signal: AbortSignal.timeout(1e4)
834
- });
835
- if (!res.ok) return { __status: res.status };
836
- return await readJson(res);
837
- } catch {
838
- return null;
905
+ async function getAuth(homeDir, expectedUuid) {
906
+ const candidates = await authCandidates(homeDir);
907
+ let sharedAccountEmail;
908
+ let sawExpired = false;
909
+ let sawExpiredOwn = false;
910
+ for (const { auth, shared } of candidates) {
911
+ if (auth.expiresAt !== void 0 && auth.expiresAt < Date.now() - 6e4) {
912
+ sawExpired = true;
913
+ if (!shared) sawExpiredOwn = true;
914
+ continue;
915
+ }
916
+ if (!shared || !expectedUuid) return { auth };
917
+ const identity = await tokenIdentity(auth.token);
918
+ if (identity === void 0) return { auth };
919
+ if (identity === null) continue;
920
+ if (identity.accountUuid === expectedUuid) return { auth };
921
+ sharedAccountEmail = identity.email;
839
922
  }
923
+ if (sawExpiredOwn) return { auth: null, expired: true };
924
+ if (sharedAccountEmail !== void 0) return { auth: null, sharedAccountEmail };
925
+ return { auth: null, expired: sawExpired };
840
926
  }
841
- function onDemandSpendCents(su, limit, remaining) {
842
- for (const raw of [su.individualUsed, su.pooledUsed, su.totalSpend]) {
843
- const n = numberValue(raw);
844
- if (n !== void 0 && n > 0) return n;
845
- }
846
- const inferred = Math.max(0, limit - remaining);
847
- if (inferred > 0) return inferred;
848
- return numberValue(su.individualUsed) ?? numberValue(su.pooledUsed) ?? numberValue(su.totalSpend) ?? 0;
927
+ function planLabel(auth) {
928
+ const sub = auth.subscriptionType;
929
+ if (!sub) return null;
930
+ const base = sub.charAt(0).toUpperCase() + sub.slice(1);
931
+ const tier = (auth.rateLimitTier ?? "").match(/(\d+)x/);
932
+ return tier ? `${base} ${tier[1]}x` : base;
849
933
  }
850
- function appendCredits(metrics, creditGrants, stripe) {
851
- const stripeBalance = numberValue(stripe?.customerBalance);
852
- const stripeBalanceCents = stripeBalance !== void 0 && stripeBalance < 0 ? Math.abs(stripeBalance) : 0;
853
- const hasGrants = creditGrants?.hasCreditGrants === true;
854
- const grantTotal = hasGrants ? numberValue(creditGrants?.totalCents) ?? 0 : 0;
855
- const grantUsed = hasGrants ? numberValue(creditGrants?.usedCents) ?? 0 : 0;
856
- const hasValidGrants = hasGrants && grantTotal > 0;
857
- const total = (hasValidGrants ? grantTotal : 0) + stripeBalanceCents;
858
- if (total <= 0) return;
859
- metrics.push({ label: "Credits", used: dollars(Math.max(0, total - (hasValidGrants ? grantUsed : 0))), limit: null, format: { kind: "dollars" } });
860
- }
861
- async function cursorBilling(account, tz) {
862
- const [core, activity, spend] = await Promise.all([
863
- cursorBillingCore(account),
864
- cursorActivity(tz, account.homeDir),
865
- cursorModelSpend(account.homeDir)
866
- ]);
867
- let merged = activity;
868
- if (spend) {
869
- const lines = activity?.summary ?? "";
870
- const spendLabel = `$${Math.round(finite(spend.total))} all-time`;
871
- merged = {
872
- series: activity?.series ?? [],
873
- summary: lines ? `${lines} \xB7 ${spendLabel}` : spendLabel
874
- };
875
- }
876
- const modelSpend = spend?.models?.length ? spend.models.map((m) => ({ name: m.name, usd: finite(m.usd), requests: finite(m.requests) })) : null;
877
- return { ...core, activity: merged, modelSpend };
878
- }
879
- async function cursorBillingCore(account) {
880
- const db = cursorStateDb(account.homeDir);
881
- const [tokenRes, membershipRes, emailRes, nameRes] = await Promise.all([
882
- readState(db, "cursorAuth/accessToken"),
883
- readState(db, "cursorAuth/stripeMembershipType"),
884
- readState(db, "cursorAuth/cachedEmail"),
885
- readState(db, "cursorAuth/cachedName")
886
- ]);
887
- const token = cleanStoredString(tokenRes.value);
888
- const membership = membershipRes.value;
889
- const email = cleanStoredString(emailRes.value);
890
- const displayName = cleanStoredString(nameRes.value);
891
- const planFallback = membership ? membership.charAt(0).toUpperCase() + membership.slice(1) : null;
892
- if (!token) {
893
- const error = tokenRes.status === "ok" ? "Not signed in \u2014 open Cursor" : sqliteStatusMessage(tokenRes.status);
894
- return { plan: planFallback, metrics: [], error, ...identityFields({ email, displayName }) };
895
- }
896
- const [usage, planInfo, creditGrants, stripe] = await Promise.all([
897
- connectPost(USAGE_URL, token),
898
- connectPost(PLAN_URL, token),
899
- connectPost(CREDITS_URL, token),
900
- connectGetWithSession(STRIPE_URL, token)
901
- ]);
902
- if (!usage || usage.__status) {
903
- const expired = usage?.__status === 401 || usage?.__status === 403;
904
- return { plan: planFallback, metrics: [], error: expired ? "Token expired \u2014 re-open Cursor" : "Cursor API error", ...identityFields({ email, displayName }) };
905
- }
906
- const planName = planInfo?.planInfo?.planName ?? planFallback;
907
- const price = planInfo?.planInfo?.price;
908
- const plan = planName ? price ? `${planName} \xB7 ${price}` : planName : null;
909
- const pu = usage.planUsage ?? {};
910
- const metrics = [];
911
- const rawEnd = usage.billingCycleEnd;
912
- const endMs = numberValue(rawEnd) ?? NaN;
913
- const iso = msToIso(endMs);
914
- const resets = iso && endMs > 0 ? resetIn(iso) : null;
915
- const limit = numberValue(pu.limit);
916
- const planUsedCents = numberValue(pu.totalSpend) ?? (limit !== void 0 && numberValue(pu.remaining) !== void 0 ? limit - numberValue(pu.remaining) : void 0);
917
- const computedPercent = limit !== void 0 && limit > 0 && planUsedCents !== void 0 ? planUsedCents / limit * 100 : void 0;
918
- const totalPercentUsed = numberValue(pu.totalPercentUsed) ?? computedPercent;
919
- const su = usage.spendLimitUsage;
920
- const planLower = typeof planName === "string" ? planName.trim().toLowerCase() : "";
921
- const pooledLimit = numberValue(su?.pooledLimit) ?? 0;
922
- const isTeamAccount = planLower === "team" || String(su?.limitType ?? "").toLowerCase() === "team" || pooledLimit > 0;
923
- if (isTeamAccount && limit !== void 0 && planUsedCents !== void 0) {
924
- metrics.push({
925
- label: "Usage",
926
- used: dollars(Math.max(0, planUsedCents)),
927
- limit: dollars(limit),
928
- format: { kind: "dollars" },
929
- resetsAt: resets,
930
- primary: true
931
- });
932
- } else if (totalPercentUsed !== void 0) {
933
- metrics.push(percentMetric("Usage", totalPercentUsed, resets, true));
934
- if (limit !== void 0 && planUsedCents !== void 0) {
935
- metrics.push({
936
- label: "Spend",
937
- used: dollars(Math.max(0, planUsedCents)),
938
- limit: dollars(limit),
939
- format: { kind: "dollars" }
940
- });
941
- }
942
- }
943
- const autoPercentUsed = numberValue(pu.autoPercentUsed);
944
- if (autoPercentUsed !== void 0) {
945
- metrics.push({ label: "Auto", used: autoPercentUsed, limit: 100, format: { kind: "percent" } });
946
- }
947
- const apiPercentUsed = numberValue(pu.apiPercentUsed);
948
- if (apiPercentUsed !== void 0) {
949
- metrics.push({ label: "API", used: apiPercentUsed, limit: 100, format: { kind: "percent" } });
950
- }
951
- if (su) {
952
- const pair = numberValue(su.individualLimit) !== void 0 && numberValue(su.individualRemaining) !== void 0 ? { limit: numberValue(su.individualLimit), remaining: numberValue(su.individualRemaining) } : numberValue(su.pooledLimit) !== void 0 && numberValue(su.pooledRemaining) !== void 0 ? { limit: numberValue(su.pooledLimit), remaining: numberValue(su.pooledRemaining) } : null;
953
- const spent = onDemandSpendCents(su, pair?.limit ?? 0, pair?.remaining ?? 0);
954
- if (pair && pair.limit > 0) {
955
- metrics.push({
956
- label: "On-demand",
957
- used: dollars(spent),
958
- limit: dollars(pair.limit),
959
- format: { kind: "dollars" }
960
- });
961
- } else if (spent > 0) {
962
- metrics.push({
963
- label: "On-demand",
964
- used: dollars(spent),
965
- limit: null,
966
- format: { kind: "dollars" }
967
- });
968
- }
969
- }
970
- appendCredits(metrics, creditGrants, stripe);
971
- if (metrics.length === 0) {
972
- return { plan, metrics: [], error: usage.enabled === false ? "No active subscription" : "No usage data", ...identityFields({ email, displayName }) };
973
- }
974
- return { plan, metrics, error: null, ...identityFields({ email, displayName }) };
975
- }
976
-
977
- // src/providers/cursor/composer.ts
978
- async function cursorModelSpend(homeDir) {
979
- const db = cursorStateDb(homeDir);
980
- const sql = "SELECT mk.key AS name, sum(json_extract(mk.value,'$.costInCents')) AS cents, sum(json_extract(mk.value,'$.amount')) AS amt FROM cursorDiskKV c, json_each(c.value,'$.usageData') mk WHERE c.key LIKE 'composerData:%' AND json_valid(c.value) AND json_type(c.value,'$.usageData')='object' GROUP BY mk.key ORDER BY cents DESC;";
981
- const res = await runSqlite(db, sql);
982
- if (res.status !== "ok") return null;
983
- const models = [];
984
- let total = 0;
985
- for (const row of res.rows) {
986
- const usd = finitePositiveCoerced(row.cents) / 100;
987
- if (usd <= 0) continue;
988
- models.push({ name: String(row.name ?? ""), usd, requests: finitePositiveCoerced(row.amt) });
989
- total += usd;
990
- }
991
- if (total <= 0) return null;
992
- return { total, models };
993
- }
994
- var USAGE_SQL = "SELECT json_extract(c.value,'$.createdAt') AS createdAt, mk.key AS model, sum(json_extract(mk.value,'$.costInCents')) AS cents, sum(json_extract(mk.value,'$.amount')) AS amt FROM cursorDiskKV c, json_each(c.value,'$.usageData') mk WHERE c.key LIKE 'composerData:%' AND json_valid(c.value) AND json_type(c.value,'$.usageData')='object' AND json_extract(c.value,'$.createdAt') IS NOT NULL GROUP BY createdAt, model;";
995
- async function cursorUsageTable(tz, homeDir) {
996
- const res = await runSqlite(cursorStateDb(homeDir), USAGE_SQL);
997
- if (res.status !== "ok" || res.rows.length === 0) return null;
998
- const buckets = { daily: /* @__PURE__ */ new Map(), weekly: /* @__PURE__ */ new Map(), monthly: /* @__PURE__ */ new Map() };
999
- const put = (map, label, model, usd, reqs) => {
1000
- let row = map.get(label);
1001
- if (!row) {
1002
- row = { label, models: [], input: 0, output: 0, cacheCreate: 0, cacheRead: 0, cacheSavings: 0, total: 0, cost: 0, count: 0, breakdown: [] };
1003
- map.set(label, row);
1004
- }
1005
- row.cost += usd;
1006
- row.count += reqs;
1007
- let md = row.breakdown.find((b) => b.name === model);
1008
- if (!md) {
1009
- md = { name: model, input: 0, output: 0, cacheCreate: 0, cacheRead: 0, cacheSavings: 0, cost: 0, count: 0 };
1010
- row.breakdown.push(md);
1011
- }
1012
- md.cost += usd;
1013
- md.count += reqs;
1014
- };
1015
- for (const r of res.rows) {
1016
- const ts = Number(r.createdAt);
1017
- if (!Number.isFinite(ts) || ts <= 0) continue;
1018
- const usd = finitePositiveCoerced(r.cents) / 100;
1019
- const reqs = finitePositiveCoerced(r.amt);
1020
- if (usd <= 0 && reqs <= 0) continue;
1021
- const model = String(r.model ?? "unknown");
1022
- put(buckets.daily, dayKey(ts, tz), model, usd, reqs);
1023
- put(buckets.weekly, weekKey(ts, tz), model, usd, reqs);
1024
- put(buckets.monthly, monthKey(ts, tz), model, usd, reqs);
1025
- }
1026
- const finalize = (map) => [...map.values()].map((row) => {
1027
- row.breakdown.sort((a, b) => b.cost - a.cost);
1028
- row.models = row.breakdown.map((b) => b.name);
1029
- return row;
1030
- }).sort((a, b) => a.label.localeCompare(b.label));
1031
- const table = { daily: finalize(buckets.daily), weekly: finalize(buckets.weekly), monthly: finalize(buckets.monthly) };
1032
- return table.daily.length ? table : null;
1033
- }
1034
-
1035
- // src/providers/claude/usage.ts
1036
- import { stat as fsStat, access as access2 } from "fs/promises";
1037
- import { createReadStream } from "fs";
1038
- import { createInterface } from "readline";
1039
- import { join as join5, isAbsolute } from "path";
1040
- import { homedir as homedir3 } from "os";
1041
- var PRICING = {
1042
- "claude-opus-4-1": { i: 15e-6, o: 75e-6, cc: 1875e-8, cr: 15e-7 },
1043
- "claude-opus-4-0": { i: 15e-6, o: 75e-6, cc: 1875e-8, cr: 15e-7 },
1044
- "claude-opus-4-20250514": { i: 15e-6, o: 75e-6, cc: 1875e-8, cr: 15e-7 },
1045
- "claude-opus-4": { i: 5e-6, o: 25e-6, cc: 625e-8, cr: 5e-7 },
1046
- "claude-3-opus": { i: 15e-6, o: 75e-6, cc: 1875e-8, cr: 15e-7 },
1047
- "claude-sonnet-4": { i: 3e-6, o: 15e-6, cc: 375e-8, cr: 3e-7 },
1048
- // intro pricing through 2026-08-31 — revert to 3/15/3.75/0.3 after.
1049
- "claude-sonnet-5": { i: 2e-6, o: 1e-5, cc: 25e-7, cr: 2e-7 },
1050
- "claude-haiku-4": { i: 1e-6, o: 5e-6, cc: 125e-8, cr: 1e-7 },
1051
- "claude-fable-5": { i: 1e-5, o: 5e-5, cc: 125e-7, cr: 1e-6 }
1052
- };
1053
- var PRICE_KEYS = Object.keys(PRICING).sort((a, b) => b.length - a.length);
1054
- var ZERO_PRICE = { i: 0, o: 0, cc: 0, cr: 0 };
1055
- function claudeConfigDirs(homeDir) {
1056
- if (homeDir) {
1057
- return [join5(homeDir, ".claude"), join5(homeDir, ".config", "claude")];
1058
- }
1059
- const home = homedir3();
1060
- const dirs = [join5(home, ".claude")];
1061
- const xdg = envDir("XDG_CONFIG_HOME");
1062
- if (xdg) {
1063
- dirs.push(join5(xdg, "claude"));
1064
- }
1065
- if (process.platform !== "win32") {
1066
- dirs.push(join5(home, ".config", "claude"));
1067
- }
1068
- const appData = envDir("APPDATA");
1069
- if (appData) dirs.push(join5(appData, "claude"));
1070
- if (process.env.CLAUDE_CONFIG_DIR) {
1071
- for (const p of process.env.CLAUDE_CONFIG_DIR.split(process.platform === "win32" ? ";" : ",")) {
1072
- const t = p.trim();
1073
- if (t && isAbsolute(t)) dirs.push(t);
1074
- }
1075
- }
1076
- return [...new Set(dirs)];
1077
- }
1078
- function getClaudeDirs(homeDir) {
1079
- return claudeConfigDirs(homeDir).map((d) => join5(d, "projects"));
1080
- }
1081
- async function detectClaude(homeDir) {
1082
- for (const dir of getClaudeDirs(homeDir)) {
1083
- try {
1084
- await access2(dir);
1085
- return true;
1086
- } catch {
1087
- }
1088
- }
1089
- return false;
1090
- }
1091
- function priceFor(model) {
1092
- const m = model.toLowerCase().trim();
1093
- for (const key of PRICE_KEYS) {
1094
- if (!m.startsWith(key)) continue;
1095
- const rest = m.slice(key.length);
1096
- if (rest === "" || rest[0] === "-") return PRICING[key];
1097
- }
1098
- return ZERO_PRICE;
1099
- }
1100
- function costOf(model, u, cacheCreate5m, cacheCreate1h, hasCacheCreateSplit) {
1101
- const p = priceFor(model);
1102
- const cacheCreateCost = hasCacheCreateSplit ? cacheCreate5m * p.cc + cacheCreate1h * (2 * p.i) : safeNum(u.cache_creation_input_tokens) * p.cc;
1103
- return safeNum(u.input_tokens) * p.i + safeNum(u.output_tokens) * p.o + cacheCreateCost + safeNum(u.cache_read_input_tokens) * p.cr;
1104
- }
1105
- function shortModel(model) {
1106
- return model.replace("claude-", "").replace(/-\d{8}$/, "");
1107
- }
1108
- function timestampMs(value) {
1109
- if (typeof value === "number" && Number.isFinite(value)) return value > 1e10 ? value : value * 1e3;
1110
- if (typeof value === "string" && value.trim()) return new Date(value.trim()).getTime();
1111
- return NaN;
1112
- }
1113
- async function parseFile(path) {
1114
- const entries = [];
1115
- const input = createReadStream(path);
1116
- input.on("error", () => {
1117
- });
1118
- const rl = createInterface({ input, crlfDelay: Infinity });
1119
- try {
1120
- for await (const line of rl) {
1121
- if (!line.includes('"usage"')) continue;
1122
- try {
1123
- const obj = JSON.parse(line.charCodeAt(0) === 65279 ? line.slice(1) : line);
1124
- if (obj.type !== "assistant" || !obj.message?.usage) continue;
1125
- const ts = timestampMs(obj.timestamp);
1126
- if (!Number.isFinite(ts)) continue;
1127
- const u = obj.message.usage;
1128
- const model = typeof obj.message.model === "string" && obj.message.model ? obj.message.model : "unknown";
1129
- const inputTokens = safeNum(u.input_tokens);
1130
- const output = safeNum(u.output_tokens);
1131
- const hasCacheCreateSplit = u.cache_creation?.ephemeral_5m_input_tokens !== void 0 || u.cache_creation?.ephemeral_1h_input_tokens !== void 0;
1132
- const cacheCreate5m = safeNum(u.cache_creation?.ephemeral_5m_input_tokens);
1133
- const cacheCreate1h = safeNum(u.cache_creation?.ephemeral_1h_input_tokens);
1134
- const cacheCreate = hasCacheCreateSplit ? cacheCreate5m + cacheCreate1h : safeNum(u.cache_creation_input_tokens);
1135
- const cacheRead = safeNum(u.cache_read_input_tokens);
1136
- if (inputTokens + output + cacheCreate + cacheRead === 0) continue;
1137
- const p = priceFor(model);
1138
- const msgId = obj.message?.id;
1139
- entries.push({
1140
- id: msgId ? msgId + (obj.requestId ? ":" + obj.requestId : "") : void 0,
1141
- ts,
1142
- model: shortModel(model),
1143
- cost: costOf(model, u, cacheCreate5m, cacheCreate1h, hasCacheCreateSplit),
1144
- input: inputTokens,
1145
- output,
1146
- cacheCreate,
1147
- cacheRead,
1148
- cacheSavings: cacheRead * (p.i - p.cr)
1149
- });
1150
- } catch {
1151
- }
1152
- }
1153
- } catch {
1154
- }
1155
- return entries;
1156
- }
1157
- async function loadEntries(since, homeDir) {
1158
- const files = [];
1159
- const seen = /* @__PURE__ */ new Set();
1160
- const seenIno = /* @__PURE__ */ new Set();
1161
- for (const dir of getClaudeDirs(homeDir)) {
1162
- const listing = await walkFiles(dir);
1163
- for (const f of listing) {
1164
- if (!f.endsWith(".jsonl")) continue;
1165
- const path = join5(dir, f);
1166
- if (seen.has(path)) continue;
1167
- seen.add(path);
1168
- try {
1169
- const s = await fsStat(path);
1170
- if (s.mtimeMs < since) continue;
1171
- if (s.ino && process.platform !== "win32") {
1172
- const idn = `${s.dev}:${s.ino}`;
1173
- if (seenIno.has(idn)) continue;
1174
- seenIno.add(idn);
1175
- }
1176
- files.push({ path, mtimeMs: s.mtimeMs, size: s.size });
1177
- } catch {
1178
- }
1179
- }
1180
- }
1181
- return loadCachedEntries(files, parseFile, since);
1182
- }
1183
- async function claudeDashboard(tz, homeDir) {
1184
- const entries = await loadEntries(dashboardSince(tz), homeDir);
1185
- return summarize(entries, tz);
1186
- }
1187
- async function claudeTable(tz, homeDir) {
1188
- const entries = await loadEntries(tableSince(tz), homeDir);
1189
- return tabulate(entries, tz);
1190
- }
1191
-
1192
- // src/providers/claude/billing.ts
1193
- import { access as access3, readFile as readFile2 } from "fs/promises";
1194
- import { join as join7 } from "path";
1195
- import { homedir as homedir5 } from "os";
1196
-
1197
- // src/providers/_shared/keychain.ts
1198
- import { execFile as execFileCb2 } from "child_process";
1199
- import { promisify as promisify2 } from "util";
1200
- var execFile2 = promisify2(execFileCb2);
1201
- var GO_KEYRING_PREFIX = "go-keyring-base64:";
1202
- async function readMacKeychainRaw(service) {
1203
- if (process.platform !== "darwin") return null;
1204
- try {
1205
- const { stdout } = await execFile2("security", [
1206
- "find-generic-password",
1207
- "-s",
1208
- service,
1209
- "-w"
1210
- ], { timeout: 5e3 });
1211
- const raw = stdout.trim();
1212
- return raw || null;
1213
- } catch {
1214
- return null;
1215
- }
1216
- }
1217
- async function readMacKeychainFileRaw(service, keychainPath) {
1218
- if (process.platform !== "darwin") return null;
1219
- try {
1220
- await execFile2("security", ["unlock-keychain", "-p", "", keychainPath], { timeout: 5e3 });
1221
- const { stdout } = await execFile2("security", [
1222
- "find-generic-password",
1223
- "-s",
1224
- service,
1225
- "-w",
1226
- keychainPath
1227
- ], { timeout: 5e3 });
1228
- const raw = stdout.trim();
1229
- return raw || null;
1230
- } catch {
1231
- return null;
1232
- }
1233
- }
1234
- function unwrapGoKeyringBase64(raw) {
1235
- if (!raw.startsWith(GO_KEYRING_PREFIX)) return raw;
1236
- return Buffer.from(raw.slice(GO_KEYRING_PREFIX.length), "base64").toString("utf-8");
1237
- }
1238
-
1239
- // src/providers/claude/identity.ts
1240
- import { readFileSync } from "fs";
1241
- import { join as join6 } from "path";
1242
- import { homedir as homedir4 } from "os";
1243
- function titleWords(value) {
1244
- return value.split(/[_\s-]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()).join(" ");
1245
- }
1246
- function claudeOrgPlanLabel(orgType) {
1247
- if (typeof orgType !== "string" || !orgType.trim()) return null;
1248
- const normalized = orgType.trim().toLowerCase();
1249
- const stripped = normalized.startsWith("claude_") ? normalized.slice("claude_".length) : normalized;
1250
- const label = titleWords(stripped);
1251
- return label ? `Claude ${label}` : null;
1252
- }
1253
- function readClaudeIdentity(homeDir) {
1254
- const base = homeDir ? expandHome(homeDir) : homedir4();
1255
- try {
1256
- const parsed = JSON.parse(readFileSync(join6(base, ".claude.json"), "utf-8"));
1257
- const oauth = parsed?.oauthAccount;
1258
- const email = typeof oauth?.emailAddress === "string" && oauth.emailAddress.trim() ? oauth.emailAddress.trim() : void 0;
1259
- const displayName = typeof oauth?.displayName === "string" && oauth.displayName.trim() ? oauth.displayName.trim() : void 0;
1260
- const plan = claudeOrgPlanLabel(parsed?.organizationType);
1261
- const accountUuid = typeof oauth?.accountUuid === "string" && oauth.accountUuid.trim() ? oauth.accountUuid.trim() : void 0;
1262
- return { email, displayName, plan: plan ?? void 0, accountUuid };
1263
- } catch {
1264
- return {};
1265
- }
1266
- }
1267
-
1268
- // src/providers/claude/billing.ts
1269
- function parseAuth(raw) {
1270
- try {
1271
- const creds = JSON.parse(raw);
1272
- const o = creds?.claudeAiOauth ?? creds;
1273
- const token = o?.accessToken;
1274
- if (typeof token !== "string" || !token) return null;
1275
- return {
1276
- token,
1277
- subscriptionType: typeof o.subscriptionType === "string" ? o.subscriptionType : void 0,
1278
- rateLimitTier: typeof o.rateLimitTier === "string" ? o.rateLimitTier : void 0,
1279
- expiresAt: typeof o.expiresAt === "number" && Number.isFinite(o.expiresAt) ? o.expiresAt : void 0
1280
- };
1281
- } catch {
1282
- return null;
1283
- }
1284
- }
1285
- async function readCredentialsFile(homeDir) {
1286
- for (const dir of claudeConfigDirs(homeDir)) {
1287
- try {
1288
- const auth = parseAuth(await readFile2(join7(dir, ".credentials.json"), "utf-8"));
1289
- if (auth) return auth;
1290
- } catch {
1291
- }
1292
- }
1293
- return null;
1294
- }
1295
- async function readMacKeychain() {
1296
- const raw = await readMacKeychainRaw("Claude Code-credentials");
1297
- return raw ? parseAuth(raw) : null;
1298
- }
1299
- async function readHomeKeychain(homeDir) {
1300
- if (process.platform !== "darwin") return null;
1301
- const keychainPath = join7(homeDir, "Library", "Keychains", "login.keychain-db");
1302
- try {
1303
- await access3(keychainPath);
1304
- } catch {
1305
- return null;
1306
- }
1307
- const raw = await readMacKeychainFileRaw("Claude Code-credentials", keychainPath);
1308
- return raw ? parseAuth(raw) : null;
1309
- }
1310
- async function authCandidates(homeDir) {
1311
- const expandedHomeDir = homeDir ? expandHome(homeDir) : void 0;
1312
- const isDefault = !expandedHomeDir || expandedHomeDir === homedir5();
1313
- const out = [];
1314
- const file = await readCredentialsFile(isDefault ? void 0 : expandedHomeDir);
1315
- const keychain = process.platform === "darwin" ? await readMacKeychain() : null;
1316
- const homeKeychain = !isDefault && expandedHomeDir ? await readHomeKeychain(expandedHomeDir) : null;
1317
- const ordered = isDefault ? [keychain && { auth: keychain, shared: true }, file && { auth: file, shared: false }] : [
1318
- homeKeychain && { auth: homeKeychain, shared: false },
1319
- file && { auth: file, shared: false },
1320
- keychain && { auth: keychain, shared: true }
1321
- ];
1322
- for (const c of ordered) if (c) out.push(c);
1323
- return out;
1324
- }
1325
- var tokenIdentityCache = /* @__PURE__ */ new Map();
1326
- async function tokenIdentity(token) {
1327
- if (tokenIdentityCache.has(token)) return tokenIdentityCache.get(token);
1328
- try {
1329
- const res = await fetch("https://api.anthropic.com/api/oauth/profile", {
1330
- headers: {
1331
- "Authorization": `Bearer ${token}`,
1332
- "anthropic-beta": "oauth-2025-04-20",
1333
- "User-Agent": "tokmon"
1334
- },
1335
- signal: AbortSignal.timeout(1e4)
1336
- });
1337
- if (res.status === 401 || res.status === 403) {
1338
- tokenIdentityCache.set(token, null);
1339
- return null;
1340
- }
1341
- if (!res.ok) return void 0;
1342
- const data = await readJson(res);
1343
- const uuid = data?.account?.uuid;
1344
- if (typeof uuid !== "string" || !uuid) return void 0;
1345
- const identity = {
1346
- accountUuid: uuid,
1347
- email: typeof data?.account?.email === "string" ? data.account.email : null
1348
- };
1349
- if (tokenIdentityCache.size > 64) tokenIdentityCache.clear();
1350
- tokenIdentityCache.set(token, identity);
1351
- return identity;
1352
- } catch {
1353
- return void 0;
1354
- }
1355
- }
1356
- async function getAuth(homeDir, expectedUuid) {
1357
- const candidates = await authCandidates(homeDir);
1358
- let sharedAccountEmail;
1359
- let sawExpired = false;
1360
- let sawExpiredOwn = false;
1361
- for (const { auth, shared } of candidates) {
1362
- if (auth.expiresAt !== void 0 && auth.expiresAt < Date.now() - 6e4) {
1363
- sawExpired = true;
1364
- if (!shared) sawExpiredOwn = true;
1365
- continue;
1366
- }
1367
- if (!shared || !expectedUuid) return { auth };
1368
- const identity = await tokenIdentity(auth.token);
1369
- if (identity === void 0) return { auth };
1370
- if (identity === null) continue;
1371
- if (identity.accountUuid === expectedUuid) return { auth };
1372
- sharedAccountEmail = identity.email;
1373
- }
1374
- if (sawExpiredOwn) return { auth: null, expired: true };
1375
- if (sharedAccountEmail !== void 0) return { auth: null, sharedAccountEmail };
1376
- return { auth: null, expired: sawExpired };
1377
- }
1378
- function planLabel(auth) {
1379
- const sub = auth.subscriptionType;
1380
- if (!sub) return null;
1381
- const base = sub.charAt(0).toUpperCase() + sub.slice(1);
1382
- const tier = (auth.rateLimitTier ?? "").match(/(\d+)x/);
1383
- return tier ? `${base} ${tier[1]}x` : base;
1384
- }
1385
- var pct = (used, resets, primary) => percentMetric("", used, resets ?? null, primary);
1386
- function boolValue(value) {
1387
- if (typeof value === "boolean") return value;
1388
- if (typeof value === "number") return value !== 0;
1389
- if (typeof value === "string") return value.trim().toLowerCase() === "true" || value.trim() === "1";
1390
- return false;
934
+ var pct = (used, resets, primary) => percentMetric("", used, resets ?? null, primary);
935
+ function boolValue(value) {
936
+ if (typeof value === "boolean") return value;
937
+ if (typeof value === "number") return value !== 0;
938
+ if (typeof value === "string") return value.trim().toLowerCase() === "true" || value.trim() === "1";
939
+ return false;
1391
940
  }
1392
941
  function resetFrom(value) {
1393
942
  if (typeof value === "string" && value.trim()) return resetIn(value);
@@ -1531,11 +1080,11 @@ var claudeProvider = {
1531
1080
  };
1532
1081
 
1533
1082
  // src/providers/codex/usage.ts
1534
- import { stat as fsStat2, access as access4, open as openFile } from "fs/promises";
1083
+ import { stat as fsStat2, access as access3, open as openFile } from "fs/promises";
1535
1084
  import { createReadStream as createReadStream2 } from "fs";
1536
1085
  import { createInterface as createInterface2 } from "readline";
1537
- import { join as join8 } from "path";
1538
- import { homedir as homedir6 } from "os";
1086
+ import { join as join5 } from "path";
1087
+ import { homedir as homedir4 } from "os";
1539
1088
  var PRICING2 = {
1540
1089
  "gpt-5.5-codex": { in: 5e-6, cr: 5e-7, out: 3e-5 },
1541
1090
  "gpt-5.5": { in: 5e-6, cr: 5e-7, out: 3e-5 },
@@ -1555,23 +1104,23 @@ var PRICING2 = {
1555
1104
  var FALLBACK_PRICE = PRICING2["gpt-5.5"];
1556
1105
  var PRICE_KEYS2 = Object.keys(PRICING2).sort((a, b) => b.length - a.length);
1557
1106
  function codexHomes(homeDir) {
1558
- if (homeDir) return [.../* @__PURE__ */ new Set([join8(homeDir, ".codex"), homeDir])];
1107
+ if (homeDir) return [.../* @__PURE__ */ new Set([join5(homeDir, ".codex"), homeDir])];
1559
1108
  const homes = [];
1560
1109
  const codexHome = envDir("CODEX_HOME");
1561
1110
  if (codexHome) homes.push(codexHome);
1562
- homes.push(join8(homedir6(), ".codex"));
1563
- homes.push(join8(homedir6(), ".config", "codex"));
1111
+ homes.push(join5(homedir4(), ".codex"));
1112
+ homes.push(join5(homedir4(), ".config", "codex"));
1564
1113
  return [...new Set(homes)];
1565
1114
  }
1566
1115
  async function detectCodex(homeDir) {
1567
1116
  for (const home of codexHomes(homeDir)) {
1568
1117
  try {
1569
- await access4(join8(home, "sessions"));
1118
+ await access3(join5(home, "sessions"));
1570
1119
  return true;
1571
1120
  } catch {
1572
1121
  }
1573
1122
  try {
1574
- await access4(join8(home, "archived_sessions"));
1123
+ await access3(join5(home, "archived_sessions"));
1575
1124
  return true;
1576
1125
  } catch {
1577
1126
  }
@@ -1805,11 +1354,11 @@ async function loadEntries2(since, homeDir) {
1805
1354
  const seen = /* @__PURE__ */ new Set();
1806
1355
  const seenIno = /* @__PURE__ */ new Set();
1807
1356
  for (const home of codexHomes(homeDir)) {
1808
- for (const dir of [join8(home, "sessions"), join8(home, "archived_sessions")]) {
1357
+ for (const dir of [join5(home, "sessions"), join5(home, "archived_sessions")]) {
1809
1358
  const listing = await walkFiles(dir);
1810
1359
  for (const f of listing) {
1811
1360
  if (!f.endsWith(".jsonl")) continue;
1812
- const path = join8(dir, f);
1361
+ const path = join5(dir, f);
1813
1362
  if (seen.has(path)) continue;
1814
1363
  seen.add(path);
1815
1364
  try {
@@ -1838,11 +1387,32 @@ async function codexTable(tz, homeDir) {
1838
1387
  }
1839
1388
 
1840
1389
  // src/providers/codex/billing.ts
1841
- import { readFile as readFile3, readdir as readdir2, stat as fsStat3 } from "fs/promises";
1842
- import { createReadStream as createReadStream3 } from "fs";
1843
- import { createInterface as createInterface3 } from "readline";
1844
- import { join as join9 } from "path";
1845
- var USAGE_URL2 = "https://chatgpt.com/backend-api/wham/usage";
1390
+ import { readFile as readFile3, readdir as readdir2, stat as fsStat3 } from "fs/promises";
1391
+ import { createReadStream as createReadStream3 } from "fs";
1392
+ import { createInterface as createInterface3 } from "readline";
1393
+ import { join as join6 } from "path";
1394
+
1395
+ // src/providers/_shared/jwt.ts
1396
+ function decodeBase64UrlJson(segment) {
1397
+ try {
1398
+ const normalized = segment.replace(/-/g, "+").replace(/_/g, "/");
1399
+ const padded = normalized + "=".repeat((4 - normalized.length % 4) % 4);
1400
+ return JSON.parse(Buffer.from(padded, "base64").toString("utf8"));
1401
+ } catch {
1402
+ return null;
1403
+ }
1404
+ }
1405
+ function identityFromIdToken(idToken) {
1406
+ if (typeof idToken !== "string" || !idToken.includes(".")) return {};
1407
+ const payload = decodeBase64UrlJson(idToken.split(".")[1]);
1408
+ if (!payload || typeof payload !== "object") return {};
1409
+ const email = typeof payload.email === "string" && payload.email.trim() ? payload.email.trim() : void 0;
1410
+ const displayName = typeof payload.name === "string" && payload.name.trim() ? payload.name.trim() : typeof payload.given_name === "string" && payload.given_name.trim() ? payload.given_name.trim() : void 0;
1411
+ return { email, displayName, payload };
1412
+ }
1413
+
1414
+ // src/providers/codex/billing.ts
1415
+ var USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
1846
1416
  var RESET_CREDITS_URL = "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits";
1847
1417
  var CREDIT_USD_RATE = 0.04;
1848
1418
  function chatGptPlanLabel(planType) {
@@ -1867,258 +1437,694 @@ function codexIdentity(idToken) {
1867
1437
  }
1868
1438
  async function readAuthFile(home) {
1869
1439
  try {
1870
- const raw = await readFile3(join9(home, "auth.json"), "utf-8");
1440
+ const raw = await readFile3(join6(home, "auth.json"), "utf-8");
1441
+ const auth = JSON.parse(raw);
1442
+ const accessToken = auth?.tokens?.access_token;
1443
+ if (!accessToken) return null;
1444
+ return { accessToken, accountId: auth?.tokens?.account_id, ...codexIdentity(auth?.tokens?.id_token) };
1445
+ } catch {
1446
+ return null;
1447
+ }
1448
+ }
1449
+ async function readKeychainAuth() {
1450
+ try {
1451
+ const raw = await readMacKeychainRaw("Codex Auth");
1452
+ if (!raw) return null;
1871
1453
  const auth = JSON.parse(raw);
1872
1454
  const accessToken = auth?.tokens?.access_token;
1873
1455
  if (!accessToken) return null;
1874
1456
  return { accessToken, accountId: auth?.tokens?.account_id, ...codexIdentity(auth?.tokens?.id_token) };
1875
1457
  } catch {
1876
- return null;
1458
+ return null;
1459
+ }
1460
+ }
1461
+ async function getAuth2(homeDir) {
1462
+ for (const home of codexHomes(homeDir)) {
1463
+ const auth = await readAuthFile(home);
1464
+ if (auth) return auth;
1465
+ }
1466
+ if (!homeDir && process.platform === "darwin") return readKeychainAuth();
1467
+ return null;
1468
+ }
1469
+ function planLabel2(planType) {
1470
+ if (typeof planType !== "string" || !planType.trim()) return null;
1471
+ const p = planType.trim().toLowerCase();
1472
+ if (p === "prolite") return "Pro 5x";
1473
+ if (p === "pro") return "Pro 20x";
1474
+ return planType.charAt(0).toUpperCase() + planType.slice(1);
1475
+ }
1476
+ function resetDateMs(window) {
1477
+ if (!window) return null;
1478
+ const absolute = numberValue(window.reset_at ?? window.resets_at);
1479
+ if (absolute !== void 0) return absolute > 1e10 ? absolute : absolute * 1e3;
1480
+ const after = numberValue(window.reset_after_seconds);
1481
+ if (after !== void 0) return Date.now() + after * 1e3;
1482
+ for (const key of ["reset_at", "resets_at"]) {
1483
+ const raw = window[key];
1484
+ if (typeof raw === "string" && raw.trim()) {
1485
+ const parsed = new Date(raw).getTime();
1486
+ if (Number.isFinite(parsed)) return parsed;
1487
+ }
1488
+ }
1489
+ return null;
1490
+ }
1491
+ function resetFrom2(window) {
1492
+ const ms = resetDateMs(window);
1493
+ const iso = ms === null ? null : msToIso(ms);
1494
+ return iso ? resetIn(iso) : null;
1495
+ }
1496
+ function windowSeconds(window) {
1497
+ const seconds = numberValue(window?.limit_window_seconds ?? window?.period_seconds ?? window?.window_seconds);
1498
+ if (seconds !== void 0) return seconds;
1499
+ const minutes = numberValue(window?.limit_window_minutes ?? window?.window_minutes ?? window?.period_minutes);
1500
+ return minutes === void 0 ? void 0 : minutes * 60;
1501
+ }
1502
+ function normalizedUsedPercent(window, percent) {
1503
+ const used = numberValue(percent);
1504
+ if (used === void 0) return void 0;
1505
+ const periodSeconds = windowSeconds(window);
1506
+ const resetMs = resetDateMs(window);
1507
+ if (periodSeconds && resetMs) {
1508
+ const elapsedMs = periodSeconds * 1e3 - Math.max(0, resetMs - Date.now());
1509
+ if (elapsedMs <= 6e4 && used <= 1) return 0;
1510
+ }
1511
+ return Math.max(0, used);
1512
+ }
1513
+ function metricLabelForAdditional(window) {
1514
+ const name = String(window?.limit_name ?? window?.name ?? window?.metered_feature ?? window?.feature ?? "").toLowerCase();
1515
+ if (!name.includes("spark")) return null;
1516
+ const seconds = windowSeconds(window);
1517
+ return name.includes("week") || seconds !== void 0 && seconds >= 6 * 24 * 60 * 60 ? "Spark Weekly" : "Spark";
1518
+ }
1519
+ function additionalRateLimits(rl) {
1520
+ const raw = rl?.additional_rate_limits ?? rl?.additionalRateLimits ?? rl?.additional;
1521
+ if (Array.isArray(raw)) return raw;
1522
+ if (raw && typeof raw === "object") return Object.values(raw);
1523
+ return [];
1524
+ }
1525
+ function appendWindowMetrics(metrics, rl, headerPct) {
1526
+ const primary = rl?.primary_window ?? rl?.primary ?? null;
1527
+ const secondary = rl?.secondary_window ?? rl?.secondary ?? null;
1528
+ const primaryPct = normalizedUsedPercent(primary, primary?.used_percent ?? primary?.percent_used ?? headerPct?.("x-codex-primary-used-percent"));
1529
+ const secondaryPct = normalizedUsedPercent(secondary, secondary?.used_percent ?? secondary?.percent_used ?? headerPct?.("x-codex-secondary-used-percent"));
1530
+ if (primaryPct !== void 0) metrics.push(percentMetric("Session", primaryPct, resetFrom2(primary), true));
1531
+ if (secondaryPct !== void 0) metrics.push(percentMetric("Weekly", secondaryPct, resetFrom2(secondary)));
1532
+ for (const item of additionalRateLimits(rl)) {
1533
+ const label = metricLabelForAdditional(item);
1534
+ if (!label) continue;
1535
+ const used = normalizedUsedPercent(item, item?.used_percent ?? item?.percent_used);
1536
+ if (used === void 0) continue;
1537
+ metrics.push(percentMetric(label, used, resetFrom2(item)));
1538
+ }
1539
+ }
1540
+ function appendCredits(metrics, source) {
1541
+ const balance = numberValue(source?.credits?.balance ?? source?.credit_balance);
1542
+ if (balance !== void 0 && balance >= 0) {
1543
+ metrics.push({ label: "Credits", used: balance * CREDIT_USD_RATE, limit: null, format: { kind: "dollars" } });
1544
+ }
1545
+ }
1546
+ async function fetchResetCredits(headers) {
1547
+ try {
1548
+ const res = await fetch(RESET_CREDITS_URL, {
1549
+ headers: {
1550
+ ...headers,
1551
+ "OpenAI-Beta": "codex-1",
1552
+ "originator": "Codex Desktop"
1553
+ },
1554
+ signal: AbortSignal.timeout(1e4)
1555
+ });
1556
+ if (!res.ok) return void 0;
1557
+ const data = await readJson(res);
1558
+ return numberValue(data?.available_count ?? data?.available ?? data?.remaining);
1559
+ } catch {
1560
+ return void 0;
1561
+ }
1562
+ }
1563
+ async function liveBilling(auth) {
1564
+ try {
1565
+ const headers = {
1566
+ "Authorization": `Bearer ${auth.accessToken}`,
1567
+ "Accept": "application/json",
1568
+ "User-Agent": "tokmon"
1569
+ };
1570
+ if (auth.accountId) headers["ChatGPT-Account-Id"] = auth.accountId;
1571
+ const res = await fetch(USAGE_URL, { headers, signal: AbortSignal.timeout(1e4) });
1572
+ if (!res.ok) return null;
1573
+ const data = await readJson(res);
1574
+ if (!data) return null;
1575
+ const headerPct = (name) => {
1576
+ const h = res.headers.get(name);
1577
+ if (h === null || h.trim() === "") return void 0;
1578
+ const n = Number(h);
1579
+ return Number.isFinite(n) ? n : void 0;
1580
+ };
1581
+ const metrics = [];
1582
+ appendWindowMetrics(metrics, data.rate_limit ?? data, headerPct);
1583
+ appendCredits(metrics, data);
1584
+ const resetCredits = numberValue(data?.rate_limit_reset_credits?.available_count ?? data?.rate_limit_reset_credits?.available) ?? await fetchResetCredits(headers);
1585
+ if (resetCredits !== void 0 && resetCredits >= 0) {
1586
+ metrics.push({ label: "Resets", used: resetCredits, limit: null, format: { kind: "count", suffix: "available" } });
1587
+ }
1588
+ if (metrics.length === 0) return null;
1589
+ return { plan: auth.plan ?? planLabel2(data.plan_type), metrics, error: null, ...identityFields(auth) };
1590
+ } catch {
1591
+ return null;
1592
+ }
1593
+ }
1594
+ var SNAPSHOT_CANDIDATES = 8;
1595
+ var SNAPSHOT_STALE_MS = 24 * 36e5;
1596
+ async function newestRolloutFiles(homeDir) {
1597
+ const all = [];
1598
+ for (const home of codexHomes(homeDir)) {
1599
+ const dir = join6(home, "sessions");
1600
+ let listing;
1601
+ try {
1602
+ listing = await readdir2(dir, { recursive: true });
1603
+ } catch {
1604
+ continue;
1605
+ }
1606
+ for (const f of listing) {
1607
+ if (!f.endsWith(".jsonl") || !f.includes("rollout-")) continue;
1608
+ const path = join6(dir, f);
1609
+ try {
1610
+ const s = await fsStat3(path);
1611
+ all.push({ path, mtime: s.mtimeMs });
1612
+ } catch {
1613
+ }
1614
+ }
1615
+ }
1616
+ return all.sort((a, b) => b.mtime - a.mtime).slice(0, SNAPSHOT_CANDIDATES);
1617
+ }
1618
+ async function lastRateLimits(path) {
1619
+ let last = null;
1620
+ try {
1621
+ const input = createReadStream3(path);
1622
+ input.on("error", () => {
1623
+ });
1624
+ const rl = createInterface3({ input, crlfDelay: Infinity });
1625
+ for await (const line of rl) {
1626
+ if (!line.includes("rate_limits")) continue;
1627
+ try {
1628
+ const obj = JSON.parse(line);
1629
+ if (obj?.payload?.rate_limits) last = obj.payload.rate_limits;
1630
+ } catch {
1631
+ }
1632
+ }
1633
+ } catch {
1634
+ return null;
1635
+ }
1636
+ return last;
1637
+ }
1638
+ async function snapshotBilling(homeDir, auth = null) {
1639
+ for (const file of await newestRolloutFiles(homeDir)) {
1640
+ const last = await lastRateLimits(file.path);
1641
+ if (!last) continue;
1642
+ const metrics = [];
1643
+ appendWindowMetrics(metrics, last.rate_limit ?? last);
1644
+ appendCredits(metrics, last);
1645
+ const resetCredits = numberValue(last?.rate_limit_reset_credits?.available_count ?? last?.rate_limit_reset_credits?.available);
1646
+ if (resetCredits !== void 0 && resetCredits >= 0) {
1647
+ metrics.push({ label: "Resets", used: resetCredits, limit: null, format: { kind: "count", suffix: "available" } });
1648
+ }
1649
+ if (metrics.length === 0) continue;
1650
+ return { plan: auth?.plan ?? planLabel2(last.plan_type), metrics, error: null, asOfMs: file.mtime, ...identityFields(auth) };
1651
+ }
1652
+ return null;
1653
+ }
1654
+ async function codexBilling(account) {
1655
+ const auth = await getAuth2(account.homeDir);
1656
+ if (auth) {
1657
+ const live = await liveBilling(auth);
1658
+ if (live) return live;
1659
+ }
1660
+ const snap = await snapshotBilling(account.homeDir, auth);
1661
+ if (snap && Date.now() - snap.asOfMs < SNAPSHOT_STALE_MS) {
1662
+ const { asOfMs: _asOfMs, ...result } = snap;
1663
+ return result;
1664
+ }
1665
+ return {
1666
+ plan: auth?.plan ?? snap?.plan ?? null,
1667
+ metrics: [],
1668
+ error: auth ? "Usage API failed \u2014 run codex to refresh" : "Not logged in \u2014 run codex",
1669
+ ...identityFields(auth)
1670
+ };
1671
+ }
1672
+
1673
+ // src/providers/codex/index.ts
1674
+ var codexProvider = {
1675
+ id: "codex",
1676
+ name: "Codex",
1677
+ color: "cyan",
1678
+ hasUsage: true,
1679
+ hasBilling: true,
1680
+ detect: (homeDir) => detectCodex(homeDir),
1681
+ fetchSummary: (account, tz) => codexDashboard(tz, account.homeDir),
1682
+ fetchTable: (account, tz) => codexTable(tz, account.homeDir),
1683
+ fetchBilling: (account) => codexBilling(account)
1684
+ };
1685
+
1686
+ // src/providers/cursor/billing.ts
1687
+ import { access as access4 } from "fs/promises";
1688
+ import { join as join9 } from "path";
1689
+ import { homedir as homedir6 } from "os";
1690
+
1691
+ // src/providers/cursor/activity.ts
1692
+ import { join as join8 } from "path";
1693
+ import { homedir as homedir5 } from "os";
1694
+
1695
+ // src/providers/cursor/sqlite.ts
1696
+ import { execFile as execFileCb2 } from "child_process";
1697
+ import { accessSync, constants } from "fs";
1698
+ import { delimiter, join as join7 } from "path";
1699
+ import { promisify as promisify2 } from "util";
1700
+ var execFile2 = promisify2(execFileCb2);
1701
+ var nativeDb;
1702
+ async function getNativeDb() {
1703
+ if (nativeDb !== void 0) return nativeDb;
1704
+ try {
1705
+ nativeDb = (await import("sqlite")).DatabaseSync;
1706
+ } catch {
1707
+ nativeDb = null;
1877
1708
  }
1709
+ return nativeDb;
1878
1710
  }
1879
- async function readKeychainAuth() {
1711
+ function classify(msg) {
1712
+ if (/database is (locked|busy)|SQLITE_(BUSY|LOCKED)|EBUSY|sharing violation|resource busy|readonly/i.test(msg)) return "locked";
1713
+ if (/unable to open|no such file|cannot open|ENOENT/i.test(msg)) return "missing";
1714
+ if (/no such (function|table|column)|unknown option/i.test(msg)) return "old";
1715
+ return "error";
1716
+ }
1717
+ async function runSqlite(db, sql, params = []) {
1718
+ const DB = await getNativeDb();
1719
+ if (DB) {
1720
+ let handle;
1721
+ let nativeErr;
1722
+ try {
1723
+ try {
1724
+ handle = new DB(db, { readOnly: true, timeout: 1500 });
1725
+ } catch (e) {
1726
+ if (!(e instanceof TypeError)) throw e;
1727
+ handle = new DB(db, { readOnly: true });
1728
+ }
1729
+ const rows = handle.prepare(sql).all(...params);
1730
+ return { status: "ok", rows };
1731
+ } catch (e) {
1732
+ nativeErr = e;
1733
+ } finally {
1734
+ try {
1735
+ handle?.close();
1736
+ } catch {
1737
+ }
1738
+ }
1739
+ if (nativeErr) {
1740
+ return { status: classify(String(nativeErr?.message ?? nativeErr)), rows: [] };
1741
+ }
1742
+ }
1743
+ return runSqliteCli(db, sql, params);
1744
+ }
1745
+ function isExec(p) {
1880
1746
  try {
1881
- const raw = await readMacKeychainRaw("Codex Auth");
1882
- if (!raw) return null;
1883
- const auth = JSON.parse(raw);
1884
- const accessToken = auth?.tokens?.access_token;
1885
- if (!accessToken) return null;
1886
- return { accessToken, accountId: auth?.tokens?.account_id, ...codexIdentity(auth?.tokens?.id_token) };
1747
+ accessSync(p, process.platform === "win32" ? constants.F_OK : constants.X_OK);
1748
+ return true;
1887
1749
  } catch {
1888
- return null;
1750
+ return false;
1889
1751
  }
1890
1752
  }
1891
- async function getAuth2(homeDir) {
1892
- for (const home of codexHomes(homeDir)) {
1893
- const auth = await readAuthFile(home);
1894
- if (auth) return auth;
1753
+ function resolveSqliteCli() {
1754
+ const names = process.platform === "win32" ? ["sqlite3.exe", "sqlite3.cmd", "sqlite3.bat"] : ["sqlite3"];
1755
+ for (const dir of (process.env.PATH ?? "").split(delimiter).filter(Boolean)) {
1756
+ for (const name of names) {
1757
+ const path = join7(dir, name);
1758
+ if (isExec(path)) return path;
1759
+ }
1895
1760
  }
1896
- if (!homeDir && process.platform === "darwin") return readKeychainAuth();
1897
1761
  return null;
1898
1762
  }
1899
- function planLabel2(planType) {
1900
- if (typeof planType !== "string" || !planType.trim()) return null;
1901
- const p = planType.trim().toLowerCase();
1902
- if (p === "prolite") return "Pro 5x";
1903
- if (p === "pro") return "Pro 20x";
1904
- return planType.charAt(0).toUpperCase() + planType.slice(1);
1763
+ function inlineParams(sql, params) {
1764
+ let i = 0;
1765
+ return sql.replace(/\?/g, () => {
1766
+ const p = params[i++];
1767
+ return typeof p === "number" ? String(p) : `'${String(p).replace(/'/g, "''")}'`;
1768
+ });
1905
1769
  }
1906
- function resetDateMs(window) {
1907
- if (!window) return null;
1908
- const absolute = numberValue(window.reset_at ?? window.resets_at);
1909
- if (absolute !== void 0) return absolute > 1e10 ? absolute : absolute * 1e3;
1910
- const after = numberValue(window.reset_after_seconds);
1911
- if (after !== void 0) return Date.now() + after * 1e3;
1912
- for (const key of ["reset_at", "resets_at"]) {
1913
- const raw = window[key];
1914
- if (typeof raw === "string" && raw.trim()) {
1915
- const parsed = new Date(raw).getTime();
1916
- if (Number.isFinite(parsed)) return parsed;
1770
+ async function runSqliteCli(db, sql, params) {
1771
+ const sqlite = resolveSqliteCli();
1772
+ if (!sqlite) return { status: "missing", rows: [] };
1773
+ try {
1774
+ const { stdout } = await execFile2(
1775
+ sqlite,
1776
+ ["-readonly", "-json", "-cmd", ".timeout 1500", db, inlineParams(sql, params)],
1777
+ { timeout: 1e4, maxBuffer: 8 << 20 }
1778
+ );
1779
+ const text = stdout.trim();
1780
+ if (!text) return { status: "ok", rows: [] };
1781
+ try {
1782
+ return { status: "ok", rows: JSON.parse(text) };
1783
+ } catch {
1784
+ return { status: "error", rows: [] };
1917
1785
  }
1786
+ } catch (e) {
1787
+ const err = e;
1788
+ if (err?.code === "ENOENT") return { status: "missing", rows: [] };
1789
+ return { status: classify(String(err?.stderr ?? err?.message ?? "")), rows: [] };
1918
1790
  }
1919
- return null;
1920
1791
  }
1921
- function resetFrom2(window) {
1922
- const ms = resetDateMs(window);
1923
- const iso = ms === null ? null : msToIso(ms);
1924
- return iso ? resetIn(iso) : null;
1792
+ function sqliteStatusMessage(status) {
1793
+ switch (status) {
1794
+ case "missing":
1795
+ return "Cursor data not found \u2014 open Cursor";
1796
+ case "old":
1797
+ return "Cursor DB unreadable";
1798
+ case "locked":
1799
+ return "Cursor DB busy \u2014 retrying next poll";
1800
+ default:
1801
+ return "Cursor data unavailable";
1802
+ }
1925
1803
  }
1926
- function windowSeconds(window) {
1927
- const seconds = numberValue(window?.limit_window_seconds ?? window?.period_seconds ?? window?.window_seconds);
1928
- if (seconds !== void 0) return seconds;
1929
- const minutes = numberValue(window?.limit_window_minutes ?? window?.window_minutes ?? window?.period_minutes);
1930
- return minutes === void 0 ? void 0 : minutes * 60;
1804
+
1805
+ // src/providers/cursor/activity.ts
1806
+ var DAY_MS2 = 864e5;
1807
+ var HOUR_MS = 36e5;
1808
+ function trackingDb(homeDir) {
1809
+ return join8(homeDir ?? homedir5(), ".cursor", "ai-tracking", "ai-code-tracking.db");
1931
1810
  }
1932
- function normalizedUsedPercent(window, percent) {
1933
- const used = numberValue(percent);
1934
- if (used === void 0) return void 0;
1935
- const periodSeconds = windowSeconds(window);
1936
- const resetMs = resetDateMs(window);
1937
- if (periodSeconds && resetMs) {
1938
- const elapsedMs = periodSeconds * 1e3 - Math.max(0, resetMs - Date.now());
1939
- if (elapsedMs <= 6e4 && used <= 1) return 0;
1811
+ async function cursorActivity(tz, homeDir) {
1812
+ const db = trackingDb(homeDir);
1813
+ try {
1814
+ const now = Date.now();
1815
+ const res = await runSqlite(
1816
+ db,
1817
+ `SELECT createdAt/${HOUR_MS} AS h, count(*) AS c FROM ai_code_hashes WHERE source!='human' AND createdAt >= ${Math.floor(now - 30 * DAY_MS2)} GROUP BY h;`
1818
+ );
1819
+ if (res.status !== "ok") return null;
1820
+ const byDay = /* @__PURE__ */ new Map();
1821
+ let month = 0;
1822
+ for (const row of res.rows) {
1823
+ const raw = Number(row.c);
1824
+ const n = Number.isFinite(raw) && raw > 0 ? raw : 0;
1825
+ const hour = Number(row.h);
1826
+ if (!Number.isFinite(hour)) continue;
1827
+ const key = dayKey(hour * HOUR_MS + HOUR_MS / 2, tz);
1828
+ byDay.set(key, (byDay.get(key) ?? 0) + n);
1829
+ month += n;
1830
+ }
1831
+ const series = lastDayKeys(now, tz, SPARK_DAYS).map((k) => byDay.get(k) ?? 0);
1832
+ if (month === 0 && series.every((v) => v === 0)) return null;
1833
+ return { series, summary: `${tokens(month)} lines` };
1834
+ } catch {
1835
+ return null;
1940
1836
  }
1941
- return Math.max(0, used);
1942
1837
  }
1943
- function metricLabelForAdditional(window) {
1944
- const name = String(window?.limit_name ?? window?.name ?? window?.metered_feature ?? window?.feature ?? "").toLowerCase();
1945
- if (!name.includes("spark")) return null;
1946
- const seconds = windowSeconds(window);
1947
- return name.includes("week") || seconds !== void 0 && seconds >= 6 * 24 * 60 * 60 ? "Spark Weekly" : "Spark";
1838
+
1839
+ // src/providers/cursor/composer.ts
1840
+ async function cursorModelSpend(homeDir) {
1841
+ const db = cursorStateDb(homeDir);
1842
+ const sql = "SELECT mk.key AS name, sum(json_extract(mk.value,'$.costInCents')) AS cents, sum(json_extract(mk.value,'$.amount')) AS amt FROM cursorDiskKV c, json_each(c.value,'$.usageData') mk WHERE c.key LIKE 'composerData:%' AND json_valid(c.value) AND json_type(c.value,'$.usageData')='object' GROUP BY mk.key ORDER BY cents DESC;";
1843
+ const res = await runSqlite(db, sql);
1844
+ if (res.status !== "ok") return null;
1845
+ const models = [];
1846
+ let total = 0;
1847
+ for (const row of res.rows) {
1848
+ const usd = finitePositiveCoerced(row.cents) / 100;
1849
+ if (usd <= 0) continue;
1850
+ models.push({ name: String(row.name ?? ""), usd, requests: finitePositiveCoerced(row.amt) });
1851
+ total += usd;
1852
+ }
1853
+ if (total <= 0) return null;
1854
+ return { total, models };
1948
1855
  }
1949
- function additionalRateLimits(rl) {
1950
- const raw = rl?.additional_rate_limits ?? rl?.additionalRateLimits ?? rl?.additional;
1951
- if (Array.isArray(raw)) return raw;
1952
- if (raw && typeof raw === "object") return Object.values(raw);
1953
- return [];
1856
+ var USAGE_SQL = "SELECT json_extract(c.value,'$.createdAt') AS createdAt, mk.key AS model, sum(json_extract(mk.value,'$.costInCents')) AS cents, sum(json_extract(mk.value,'$.amount')) AS amt FROM cursorDiskKV c, json_each(c.value,'$.usageData') mk WHERE c.key LIKE 'composerData:%' AND json_valid(c.value) AND json_type(c.value,'$.usageData')='object' AND json_extract(c.value,'$.createdAt') IS NOT NULL GROUP BY createdAt, model;";
1857
+ async function cursorUsageTable(tz, homeDir) {
1858
+ const res = await runSqlite(cursorStateDb(homeDir), USAGE_SQL);
1859
+ if (res.status !== "ok" || res.rows.length === 0) return null;
1860
+ const buckets = { daily: /* @__PURE__ */ new Map(), weekly: /* @__PURE__ */ new Map(), monthly: /* @__PURE__ */ new Map() };
1861
+ const put = (map, label, model, usd, reqs) => {
1862
+ let row = map.get(label);
1863
+ if (!row) {
1864
+ row = { label, models: [], input: 0, output: 0, cacheCreate: 0, cacheRead: 0, cacheSavings: 0, total: 0, cost: 0, count: 0, breakdown: [] };
1865
+ map.set(label, row);
1866
+ }
1867
+ row.cost += usd;
1868
+ row.count += reqs;
1869
+ let md = row.breakdown.find((b) => b.name === model);
1870
+ if (!md) {
1871
+ md = { name: model, input: 0, output: 0, cacheCreate: 0, cacheRead: 0, cacheSavings: 0, cost: 0, count: 0 };
1872
+ row.breakdown.push(md);
1873
+ }
1874
+ md.cost += usd;
1875
+ md.count += reqs;
1876
+ };
1877
+ for (const r of res.rows) {
1878
+ const ts = Number(r.createdAt);
1879
+ if (!Number.isFinite(ts) || ts <= 0) continue;
1880
+ const usd = finitePositiveCoerced(r.cents) / 100;
1881
+ const reqs = finitePositiveCoerced(r.amt);
1882
+ if (usd <= 0 && reqs <= 0) continue;
1883
+ const model = String(r.model ?? "unknown");
1884
+ put(buckets.daily, dayKey(ts, tz), model, usd, reqs);
1885
+ put(buckets.weekly, weekKey(ts, tz), model, usd, reqs);
1886
+ put(buckets.monthly, monthKey(ts, tz), model, usd, reqs);
1887
+ }
1888
+ const finalize = (map) => [...map.values()].map((row) => {
1889
+ row.breakdown.sort((a, b) => b.cost - a.cost);
1890
+ row.models = row.breakdown.map((b) => b.name);
1891
+ return row;
1892
+ }).sort((a, b) => a.label.localeCompare(b.label));
1893
+ const table = { daily: finalize(buckets.daily), weekly: finalize(buckets.weekly), monthly: finalize(buckets.monthly) };
1894
+ return table.daily.length ? table : null;
1954
1895
  }
1955
- function appendWindowMetrics(metrics, rl, headerPct) {
1956
- const primary = rl?.primary_window ?? rl?.primary ?? null;
1957
- const secondary = rl?.secondary_window ?? rl?.secondary ?? null;
1958
- const primaryPct = normalizedUsedPercent(primary, primary?.used_percent ?? primary?.percent_used ?? headerPct?.("x-codex-primary-used-percent"));
1959
- const secondaryPct = normalizedUsedPercent(secondary, secondary?.used_percent ?? secondary?.percent_used ?? headerPct?.("x-codex-secondary-used-percent"));
1960
- if (primaryPct !== void 0) metrics.push(percentMetric("Session", primaryPct, resetFrom2(primary), true));
1961
- if (secondaryPct !== void 0) metrics.push(percentMetric("Weekly", secondaryPct, resetFrom2(secondary)));
1962
- for (const item of additionalRateLimits(rl)) {
1963
- const label = metricLabelForAdditional(item);
1964
- if (!label) continue;
1965
- const used = normalizedUsedPercent(item, item?.used_percent ?? item?.percent_used);
1966
- if (used === void 0) continue;
1967
- metrics.push(percentMetric(label, used, resetFrom2(item)));
1896
+
1897
+ // src/providers/cursor/billing.ts
1898
+ var BASE = "https://api2.cursor.sh/aiserver.v1.DashboardService";
1899
+ var USAGE_URL2 = `${BASE}/GetCurrentPeriodUsage`;
1900
+ var PLAN_URL = `${BASE}/GetPlanInfo`;
1901
+ var CREDITS_URL = `${BASE}/GetCreditGrantsBalance`;
1902
+ var STRIPE_URL = "https://cursor.com/api/auth/stripe";
1903
+ function cursorStateDb(homeDir) {
1904
+ const base = homeDir ?? homedir6();
1905
+ const tail = ["Cursor", "User", "globalStorage", "state.vscdb"];
1906
+ if (process.platform === "darwin") {
1907
+ return join9(base, "Library", "Application Support", ...tail);
1908
+ }
1909
+ if (process.platform === "win32") {
1910
+ const roaming = homeDir ? join9(homeDir, "AppData", "Roaming") : envDir("APPDATA") ?? join9(base, "AppData", "Roaming");
1911
+ return join9(roaming, ...tail);
1912
+ }
1913
+ const cfg = homeDir ? join9(homeDir, ".config") : envDir("XDG_CONFIG_HOME") ?? join9(base, ".config");
1914
+ return join9(cfg, ...tail);
1915
+ }
1916
+ async function detectCursor(homeDir) {
1917
+ try {
1918
+ await access4(cursorStateDb(homeDir));
1919
+ return true;
1920
+ } catch {
1921
+ return false;
1968
1922
  }
1969
1923
  }
1970
- function appendCredits2(metrics, source) {
1971
- const balance = numberValue(source?.credits?.balance ?? source?.credit_balance);
1972
- if (balance !== void 0 && balance >= 0) {
1973
- metrics.push({ label: "Credits", used: balance * CREDIT_USD_RATE, limit: null, format: { kind: "dollars" } });
1924
+ async function readState(db, key) {
1925
+ const r = await runSqlite(db, "SELECT value FROM ItemTable WHERE key=? LIMIT 1;", [key]);
1926
+ const raw = r.status === "ok" ? r.rows[0]?.value : void 0;
1927
+ return { value: typeof raw === "string" && raw.trim() ? raw.trim() : null, status: r.status };
1928
+ }
1929
+ function cleanStoredString(value) {
1930
+ if (!value) return void 0;
1931
+ try {
1932
+ const parsed = JSON.parse(value);
1933
+ if (typeof parsed === "string" && parsed.trim()) return parsed.trim();
1934
+ } catch {
1974
1935
  }
1936
+ const trimmed = value.trim().replace(/^"|"$/g, "");
1937
+ return trimmed || void 0;
1975
1938
  }
1976
- async function fetchResetCredits(headers) {
1939
+ function cursorSessionToken(accessToken) {
1940
+ const payload = accessToken.includes(".") ? decodeBase64UrlJson(accessToken.split(".")[1]) : null;
1941
+ const subject = typeof payload?.sub === "string" ? payload.sub : null;
1942
+ if (!subject) return null;
1943
+ const parts = subject.split("|");
1944
+ const userId = (parts.length > 1 ? parts[1] : parts[0]).trim();
1945
+ return userId ? `${userId}%3A%3A${accessToken}` : null;
1946
+ }
1947
+ async function connectPost(url, token) {
1977
1948
  try {
1978
- const res = await fetch(RESET_CREDITS_URL, {
1949
+ const res = await fetch(url, {
1950
+ method: "POST",
1979
1951
  headers: {
1980
- ...headers,
1981
- "OpenAI-Beta": "codex-1",
1982
- "originator": "Codex Desktop"
1952
+ "Authorization": `Bearer ${token}`,
1953
+ "Content-Type": "application/json",
1954
+ "Connect-Protocol-Version": "1",
1955
+ "User-Agent": "tokmon"
1983
1956
  },
1957
+ body: "{}",
1984
1958
  signal: AbortSignal.timeout(1e4)
1985
1959
  });
1986
- if (!res.ok) return void 0;
1987
- const data = await readJson(res);
1988
- return numberValue(data?.available_count ?? data?.available ?? data?.remaining);
1960
+ if (!res.ok) return { __status: res.status };
1961
+ return await readJson(res);
1989
1962
  } catch {
1990
- return void 0;
1963
+ return null;
1991
1964
  }
1992
1965
  }
1993
- async function liveBilling(auth) {
1966
+ async function connectGetWithSession(url, token) {
1967
+ const session = cursorSessionToken(token);
1968
+ if (!session) return null;
1994
1969
  try {
1995
- const headers = {
1996
- "Authorization": `Bearer ${auth.accessToken}`,
1997
- "Accept": "application/json",
1998
- "User-Agent": "tokmon"
1999
- };
2000
- if (auth.accountId) headers["ChatGPT-Account-Id"] = auth.accountId;
2001
- const res = await fetch(USAGE_URL2, { headers, signal: AbortSignal.timeout(1e4) });
2002
- if (!res.ok) return null;
2003
- const data = await readJson(res);
2004
- if (!data) return null;
2005
- const headerPct = (name) => {
2006
- const h = res.headers.get(name);
2007
- if (h === null || h.trim() === "") return void 0;
2008
- const n = Number(h);
2009
- return Number.isFinite(n) ? n : void 0;
2010
- };
2011
- const metrics = [];
2012
- appendWindowMetrics(metrics, data.rate_limit ?? data, headerPct);
2013
- appendCredits2(metrics, data);
2014
- const resetCredits = numberValue(data?.rate_limit_reset_credits?.available_count ?? data?.rate_limit_reset_credits?.available) ?? await fetchResetCredits(headers);
2015
- if (resetCredits !== void 0 && resetCredits >= 0) {
2016
- metrics.push({ label: "Resets", used: resetCredits, limit: null, format: { kind: "count", suffix: "available" } });
2017
- }
2018
- if (metrics.length === 0) return null;
2019
- return { plan: auth.plan ?? planLabel2(data.plan_type), metrics, error: null, ...identityFields(auth) };
1970
+ const res = await fetch(url, {
1971
+ headers: {
1972
+ "Cookie": `WorkosCursorSessionToken=${session}`,
1973
+ "User-Agent": "tokmon"
1974
+ },
1975
+ signal: AbortSignal.timeout(1e4)
1976
+ });
1977
+ if (!res.ok) return { __status: res.status };
1978
+ return await readJson(res);
2020
1979
  } catch {
2021
1980
  return null;
2022
1981
  }
2023
1982
  }
2024
- var SNAPSHOT_CANDIDATES = 8;
2025
- var SNAPSHOT_STALE_MS = 24 * 36e5;
2026
- async function newestRolloutFiles(homeDir) {
2027
- const all = [];
2028
- for (const home of codexHomes(homeDir)) {
2029
- const dir = join9(home, "sessions");
2030
- let listing;
2031
- try {
2032
- listing = await readdir2(dir, { recursive: true });
2033
- } catch {
2034
- continue;
2035
- }
2036
- for (const f of listing) {
2037
- if (!f.endsWith(".jsonl") || !f.includes("rollout-")) continue;
2038
- const path = join9(dir, f);
2039
- try {
2040
- const s = await fsStat3(path);
2041
- all.push({ path, mtime: s.mtimeMs });
2042
- } catch {
2043
- }
2044
- }
1983
+ function onDemandSpendCents(su, limit, remaining) {
1984
+ for (const raw of [su.individualUsed, su.pooledUsed, su.totalSpend]) {
1985
+ const n = numberValue(raw);
1986
+ if (n !== void 0 && n > 0) return n;
2045
1987
  }
2046
- return all.sort((a, b) => b.mtime - a.mtime).slice(0, SNAPSHOT_CANDIDATES);
1988
+ const inferred = Math.max(0, limit - remaining);
1989
+ if (inferred > 0) return inferred;
1990
+ return numberValue(su.individualUsed) ?? numberValue(su.pooledUsed) ?? numberValue(su.totalSpend) ?? 0;
2047
1991
  }
2048
- async function lastRateLimits(path) {
2049
- let last = null;
2050
- try {
2051
- const input = createReadStream3(path);
2052
- input.on("error", () => {
2053
- });
2054
- const rl = createInterface3({ input, crlfDelay: Infinity });
2055
- for await (const line of rl) {
2056
- if (!line.includes("rate_limits")) continue;
2057
- try {
2058
- const obj = JSON.parse(line);
2059
- if (obj?.payload?.rate_limits) last = obj.payload.rate_limits;
2060
- } catch {
2061
- }
2062
- }
2063
- } catch {
2064
- return null;
1992
+ function appendCredits2(metrics, creditGrants, stripe) {
1993
+ const stripeBalance = numberValue(stripe?.customerBalance);
1994
+ const stripeBalanceCents = stripeBalance !== void 0 && stripeBalance < 0 ? Math.abs(stripeBalance) : 0;
1995
+ const hasGrants = creditGrants?.hasCreditGrants === true;
1996
+ const grantTotal = hasGrants ? numberValue(creditGrants?.totalCents) ?? 0 : 0;
1997
+ const grantUsed = hasGrants ? numberValue(creditGrants?.usedCents) ?? 0 : 0;
1998
+ const hasValidGrants = hasGrants && grantTotal > 0;
1999
+ const total = (hasValidGrants ? grantTotal : 0) + stripeBalanceCents;
2000
+ if (total <= 0) return;
2001
+ metrics.push({ label: "Credits", used: dollars(Math.max(0, total - (hasValidGrants ? grantUsed : 0))), limit: null, format: { kind: "dollars" } });
2002
+ }
2003
+ async function cursorBilling(account, tz) {
2004
+ const [core, activity, spend] = await Promise.all([
2005
+ cursorBillingCore(account),
2006
+ cursorActivity(tz, account.homeDir),
2007
+ cursorModelSpend(account.homeDir)
2008
+ ]);
2009
+ let merged = activity;
2010
+ if (spend) {
2011
+ const lines = activity?.summary ?? "";
2012
+ const spendLabel = `$${Math.round(finite(spend.total))} all-time`;
2013
+ merged = {
2014
+ series: activity?.series ?? [],
2015
+ summary: lines ? `${lines} \xB7 ${spendLabel}` : spendLabel
2016
+ };
2065
2017
  }
2066
- return last;
2018
+ const modelSpend = spend?.models?.length ? spend.models.map((m) => ({ name: m.name, usd: finite(m.usd), requests: finite(m.requests) })) : null;
2019
+ return { ...core, activity: merged, modelSpend };
2067
2020
  }
2068
- async function snapshotBilling(homeDir, auth = null) {
2069
- for (const file of await newestRolloutFiles(homeDir)) {
2070
- const last = await lastRateLimits(file.path);
2071
- if (!last) continue;
2072
- const metrics = [];
2073
- appendWindowMetrics(metrics, last.rate_limit ?? last);
2074
- appendCredits2(metrics, last);
2075
- const resetCredits = numberValue(last?.rate_limit_reset_credits?.available_count ?? last?.rate_limit_reset_credits?.available);
2076
- if (resetCredits !== void 0 && resetCredits >= 0) {
2077
- metrics.push({ label: "Resets", used: resetCredits, limit: null, format: { kind: "count", suffix: "available" } });
2021
+ async function cursorBillingCore(account) {
2022
+ const db = cursorStateDb(account.homeDir);
2023
+ const [tokenRes, membershipRes, emailRes, nameRes] = await Promise.all([
2024
+ readState(db, "cursorAuth/accessToken"),
2025
+ readState(db, "cursorAuth/stripeMembershipType"),
2026
+ readState(db, "cursorAuth/cachedEmail"),
2027
+ readState(db, "cursorAuth/cachedName")
2028
+ ]);
2029
+ const token = cleanStoredString(tokenRes.value);
2030
+ const membership = membershipRes.value;
2031
+ const email = cleanStoredString(emailRes.value);
2032
+ const displayName = cleanStoredString(nameRes.value);
2033
+ const planFallback = membership ? membership.charAt(0).toUpperCase() + membership.slice(1) : null;
2034
+ if (!token) {
2035
+ const error = tokenRes.status === "ok" ? "Not signed in \u2014 open Cursor" : sqliteStatusMessage(tokenRes.status);
2036
+ return { plan: planFallback, metrics: [], error, ...identityFields({ email, displayName }) };
2037
+ }
2038
+ const [usage, planInfo, creditGrants, stripe] = await Promise.all([
2039
+ connectPost(USAGE_URL2, token),
2040
+ connectPost(PLAN_URL, token),
2041
+ connectPost(CREDITS_URL, token),
2042
+ connectGetWithSession(STRIPE_URL, token)
2043
+ ]);
2044
+ if (!usage || usage.__status) {
2045
+ const expired = usage?.__status === 401 || usage?.__status === 403;
2046
+ return { plan: planFallback, metrics: [], error: expired ? "Token expired \u2014 re-open Cursor" : "Cursor API error", ...identityFields({ email, displayName }) };
2047
+ }
2048
+ const planName = planInfo?.planInfo?.planName ?? planFallback;
2049
+ const price = planInfo?.planInfo?.price;
2050
+ const plan = planName ? price ? `${planName} \xB7 ${price}` : planName : null;
2051
+ const pu = usage.planUsage ?? {};
2052
+ const metrics = [];
2053
+ const rawEnd = usage.billingCycleEnd;
2054
+ const endMs = numberValue(rawEnd) ?? NaN;
2055
+ const iso = msToIso(endMs);
2056
+ const resets = iso && endMs > 0 ? resetIn(iso) : null;
2057
+ const limit = numberValue(pu.limit);
2058
+ const planUsedCents = numberValue(pu.totalSpend) ?? (limit !== void 0 && numberValue(pu.remaining) !== void 0 ? limit - numberValue(pu.remaining) : void 0);
2059
+ const computedPercent = limit !== void 0 && limit > 0 && planUsedCents !== void 0 ? planUsedCents / limit * 100 : void 0;
2060
+ const totalPercentUsed = numberValue(pu.totalPercentUsed) ?? computedPercent;
2061
+ const su = usage.spendLimitUsage;
2062
+ const planLower = typeof planName === "string" ? planName.trim().toLowerCase() : "";
2063
+ const pooledLimit = numberValue(su?.pooledLimit) ?? 0;
2064
+ const isTeamAccount = planLower === "team" || String(su?.limitType ?? "").toLowerCase() === "team" || pooledLimit > 0;
2065
+ if (isTeamAccount && limit !== void 0 && planUsedCents !== void 0) {
2066
+ metrics.push({
2067
+ label: "Usage",
2068
+ used: dollars(Math.max(0, planUsedCents)),
2069
+ limit: dollars(limit),
2070
+ format: { kind: "dollars" },
2071
+ resetsAt: resets,
2072
+ primary: true
2073
+ });
2074
+ } else if (totalPercentUsed !== void 0) {
2075
+ metrics.push(percentMetric("Usage", totalPercentUsed, resets, true));
2076
+ if (limit !== void 0 && planUsedCents !== void 0) {
2077
+ metrics.push({
2078
+ label: "Spend",
2079
+ used: dollars(Math.max(0, planUsedCents)),
2080
+ limit: dollars(limit),
2081
+ format: { kind: "dollars" }
2082
+ });
2078
2083
  }
2079
- if (metrics.length === 0) continue;
2080
- return { plan: auth?.plan ?? planLabel2(last.plan_type), metrics, error: null, asOfMs: file.mtime, ...identityFields(auth) };
2081
2084
  }
2082
- return null;
2083
- }
2084
- async function codexBilling(account) {
2085
- const auth = await getAuth2(account.homeDir);
2086
- if (auth) {
2087
- const live = await liveBilling(auth);
2088
- if (live) return live;
2085
+ const autoPercentUsed = numberValue(pu.autoPercentUsed);
2086
+ if (autoPercentUsed !== void 0) {
2087
+ metrics.push({ label: "Auto", used: autoPercentUsed, limit: 100, format: { kind: "percent" } });
2088
+ }
2089
+ const apiPercentUsed = numberValue(pu.apiPercentUsed);
2090
+ if (apiPercentUsed !== void 0) {
2091
+ metrics.push({ label: "API", used: apiPercentUsed, limit: 100, format: { kind: "percent" } });
2092
+ }
2093
+ if (su) {
2094
+ const pair = numberValue(su.individualLimit) !== void 0 && numberValue(su.individualRemaining) !== void 0 ? { limit: numberValue(su.individualLimit), remaining: numberValue(su.individualRemaining) } : numberValue(su.pooledLimit) !== void 0 && numberValue(su.pooledRemaining) !== void 0 ? { limit: numberValue(su.pooledLimit), remaining: numberValue(su.pooledRemaining) } : null;
2095
+ const spent = onDemandSpendCents(su, pair?.limit ?? 0, pair?.remaining ?? 0);
2096
+ if (pair && pair.limit > 0) {
2097
+ metrics.push({
2098
+ label: "On-demand",
2099
+ used: dollars(spent),
2100
+ limit: dollars(pair.limit),
2101
+ format: { kind: "dollars" }
2102
+ });
2103
+ } else if (spent > 0) {
2104
+ metrics.push({
2105
+ label: "On-demand",
2106
+ used: dollars(spent),
2107
+ limit: null,
2108
+ format: { kind: "dollars" }
2109
+ });
2110
+ }
2089
2111
  }
2090
- const snap = await snapshotBilling(account.homeDir, auth);
2091
- if (snap && Date.now() - snap.asOfMs < SNAPSHOT_STALE_MS) {
2092
- const { asOfMs: _asOfMs, ...result } = snap;
2093
- return result;
2112
+ appendCredits2(metrics, creditGrants, stripe);
2113
+ if (metrics.length === 0) {
2114
+ return { plan, metrics: [], error: usage.enabled === false ? "No active subscription" : "No usage data", ...identityFields({ email, displayName }) };
2094
2115
  }
2095
- return {
2096
- plan: auth?.plan ?? snap?.plan ?? null,
2097
- metrics: [],
2098
- error: auth ? "Usage API failed \u2014 run codex to refresh" : "Not logged in \u2014 run codex",
2099
- ...identityFields(auth)
2100
- };
2116
+ return { plan, metrics, error: null, ...identityFields({ email, displayName }) };
2101
2117
  }
2102
2118
 
2103
- // src/providers/codex/index.ts
2104
- var codexProvider = {
2105
- id: "codex",
2106
- name: "Codex",
2107
- color: "cyan",
2108
- hasUsage: true,
2109
- hasBilling: true,
2110
- detect: (homeDir) => detectCodex(homeDir),
2111
- fetchSummary: (account, tz) => codexDashboard(tz, account.homeDir),
2112
- fetchTable: (account, tz) => codexTable(tz, account.homeDir),
2113
- fetchBilling: (account) => codexBilling(account)
2114
- };
2115
-
2116
2119
  // src/providers/cursor/usage.ts
2117
2120
  var EVENTS_URL = "https://api2.cursor.sh/aiserver.v1.DashboardService/GetFilteredUsageEvents";
2118
2121
  var WINDOW_DAYS = 90;
2119
2122
  var PAGE_SIZE = 1e3;
2120
2123
  var MAX_PAGES = 12;
2124
+ var CACHE_TTL_MS = 6e4;
2121
2125
  var SKIP_KINDS = /* @__PURE__ */ new Set(["USAGE_EVENT_KIND_ABORTED_NOT_CHARGED", "USAGE_EVENT_KIND_ERRORED_NOT_CHARGED"]);
2126
+ var apiCache = /* @__PURE__ */ new Map();
2127
+ var apiInflight = /* @__PURE__ */ new Map();
2122
2128
  async function readToken(homeDir) {
2123
2129
  const r = await runSqlite(cursorStateDb(homeDir), "SELECT value FROM ItemTable WHERE key='cursorAuth/accessToken' LIMIT 1;");
2124
2130
  const raw = r.status === "ok" ? r.rows[0]?.value : void 0;
@@ -2144,135 +2150,126 @@ async function fetchPage(token, startMs, endMs, page) {
2144
2150
  return null;
2145
2151
  }
2146
2152
  }
2147
- async function cursorApiUsage(tz, homeDir) {
2148
- const token = await readToken(homeDir);
2149
- if (!token) return null;
2150
- const endMs = Date.now();
2151
- const startMs = endMs - WINDOW_DAYS * 864e5;
2152
- const events = [];
2153
- for (let page = 1; page <= MAX_PAGES; page++) {
2154
- const resp = await fetchPage(token, startMs, endMs, page);
2155
- if (!resp) return page === 1 ? null : finalizeTable(events, tz);
2156
- const batch = resp.usageEventsDisplay ?? [];
2157
- events.push(...batch);
2158
- if (batch.length < PAGE_SIZE) break;
2159
- }
2160
- return finalizeTable(events, tz);
2153
+ function eventToEntry(e) {
2154
+ if (e.kind && SKIP_KINDS.has(e.kind)) return null;
2155
+ const rawTs = e.timestamp;
2156
+ const ts = typeof rawTs === "number" ? rawTs : typeof rawTs === "string" && rawTs.trim() ? /^\d+$/.test(rawTs.trim()) ? Number(rawTs) : Date.parse(rawTs) : NaN;
2157
+ if (!Number.isFinite(ts) || ts <= 0) return null;
2158
+ const tu = e.tokenUsage ?? {};
2159
+ const input = safeNum(tu.inputTokens);
2160
+ const output = safeNum(tu.outputTokens);
2161
+ const cacheRead = safeNum(tu.cacheReadTokens);
2162
+ const charged = Number(e.chargedCents);
2163
+ const totalCents = Number(tu.totalCents);
2164
+ const cents = Number.isFinite(charged) && charged > 0 ? charged : Number.isFinite(totalCents) && totalCents > 0 ? totalCents : 0;
2165
+ const cost = cents > 0 ? cents / 100 : 0;
2166
+ if (cost <= 0 && input + output + cacheRead === 0) return null;
2167
+ return {
2168
+ ts,
2169
+ id: `${ts}|${e.model ?? ""}|${input}|${output}|${cacheRead}|${cents}`,
2170
+ model: String(e.model ?? "unknown"),
2171
+ cost,
2172
+ input,
2173
+ output,
2174
+ cacheCreate: 0,
2175
+ cacheRead,
2176
+ cacheSavings: 0,
2177
+ count: 1
2178
+ };
2161
2179
  }
2162
- function finalizeTable(events, tz) {
2163
- if (events.length === 0) return null;
2164
- const buckets = { daily: /* @__PURE__ */ new Map(), weekly: /* @__PURE__ */ new Map(), monthly: /* @__PURE__ */ new Map() };
2165
- const put = (map, label, model, usd, input, output, cacheRead) => {
2166
- let row = map.get(label);
2167
- if (!row) {
2168
- row = { label, models: [], input: 0, output: 0, cacheCreate: 0, cacheRead: 0, cacheSavings: 0, total: 0, cost: 0, count: 0, breakdown: [] };
2169
- map.set(label, row);
2180
+ async function fetchApiEntries(homeDir) {
2181
+ const cacheKey = homeDir ?? "";
2182
+ const hit = apiCache.get(cacheKey);
2183
+ if (hit && Date.now() - hit.at < CACHE_TTL_MS) return { entries: hit.entries, complete: hit.complete };
2184
+ const existing = apiInflight.get(cacheKey);
2185
+ if (existing) return existing;
2186
+ const promise = (async () => {
2187
+ const token = await readToken(homeDir);
2188
+ if (!token) return { entries: [], complete: true };
2189
+ const endMs = Date.now();
2190
+ const startMs = endMs - WINDOW_DAYS * 864e5;
2191
+ const events = [];
2192
+ let complete = true;
2193
+ for (let page = 1; page <= MAX_PAGES; page++) {
2194
+ const resp = await fetchPage(token, startMs, endMs, page);
2195
+ if (!resp) {
2196
+ complete = false;
2197
+ break;
2198
+ }
2199
+ const batch = resp.usageEventsDisplay ?? [];
2200
+ events.push(...batch);
2201
+ if (batch.length < PAGE_SIZE) break;
2202
+ if (page === MAX_PAGES) complete = false;
2170
2203
  }
2171
- row.input += input;
2172
- row.output += output;
2173
- row.cacheRead += cacheRead;
2174
- row.total += input + output + cacheRead;
2175
- row.cost += usd;
2176
- row.count += 1;
2177
- let md = row.breakdown.find((b) => b.name === model);
2178
- if (!md) {
2179
- md = { name: model, input: 0, output: 0, cacheCreate: 0, cacheRead: 0, cacheSavings: 0, cost: 0, count: 0 };
2180
- row.breakdown.push(md);
2204
+ const entries = [];
2205
+ for (const e of events) {
2206
+ const entry = eventToEntry(e);
2207
+ if (entry) entries.push(entry);
2181
2208
  }
2182
- md.input += input;
2183
- md.output += output;
2184
- md.cacheRead += cacheRead;
2185
- md.cost += usd;
2186
- md.count += 1;
2187
- };
2188
- for (const e of events) {
2189
- if (e.kind && SKIP_KINDS.has(e.kind)) continue;
2190
- const ts = Number(e.timestamp);
2191
- if (!Number.isFinite(ts) || ts <= 0) continue;
2192
- const tu = e.tokenUsage ?? {};
2193
- const input = safeNum(tu.inputTokens);
2194
- const output = safeNum(tu.outputTokens);
2195
- const cacheRead = safeNum(tu.cacheReadTokens);
2196
- const cents = Number(e.chargedCents);
2197
- const usd = Number.isFinite(cents) && cents > 0 ? cents / 100 : 0;
2198
- if (usd <= 0 && input + output + cacheRead === 0) continue;
2199
- const model = String(e.model ?? "unknown");
2200
- put(buckets.daily, dayKey(ts, tz), model, usd, input, output, cacheRead);
2201
- put(buckets.weekly, weekKey(ts, tz), model, usd, input, output, cacheRead);
2202
- put(buckets.monthly, monthKey(ts, tz), model, usd, input, output, cacheRead);
2203
- }
2204
- const sortRows = (map) => [...map.values()].map((row) => {
2205
- row.breakdown.sort((a, b) => b.cost - a.cost);
2206
- row.models = row.breakdown.map((b) => b.name);
2207
- return row;
2208
- }).sort((a, b) => a.label.localeCompare(b.label));
2209
- const table = { daily: sortRows(buckets.daily), weekly: sortRows(buckets.weekly), monthly: sortRows(buckets.monthly) };
2210
- return table.daily.length ? table : null;
2209
+ if (complete) apiCache.set(cacheKey, { at: Date.now(), entries, complete: true });
2210
+ return { entries, complete };
2211
+ })();
2212
+ apiInflight.set(cacheKey, promise);
2213
+ try {
2214
+ return await promise;
2215
+ } finally {
2216
+ apiInflight.delete(cacheKey);
2217
+ }
2211
2218
  }
2212
-
2213
- // src/providers/cursor/index.ts
2214
- var EMPTY = { daily: [], weekly: [], monthly: [] };
2215
- var overlayDaily = (lo, hi) => {
2216
- const m = new Map(lo.map((r) => [r.label, r]));
2217
- for (const r of hi) m.set(r.label, r);
2218
- return [...m.values()].sort((a, b) => a.label.localeCompare(b.label));
2219
- };
2220
- function reBucket(daily, tz, keyOf) {
2221
- const out = /* @__PURE__ */ new Map();
2222
- for (const day of daily) {
2219
+ async function fetchLocalEntries(tz, homeDir) {
2220
+ const table = await cursorUsageTable(tz, homeDir);
2221
+ if (!table) return [];
2222
+ const out = [];
2223
+ for (const day of table.daily) {
2223
2224
  const [y, mo, d] = day.label.split("-").map(Number);
2224
2225
  const ts = Date.UTC(y, mo - 1, d, 12);
2225
2226
  if (!Number.isFinite(ts)) continue;
2226
- const label = keyOf(ts, tz);
2227
- let row = out.get(label);
2228
- if (!row) {
2229
- row = { label, models: [], input: 0, output: 0, cacheCreate: 0, cacheRead: 0, cacheSavings: 0, total: 0, cost: 0, count: 0, breakdown: [] };
2230
- out.set(label, row);
2231
- }
2232
- row.input += day.input;
2233
- row.output += day.output;
2234
- row.cacheCreate += day.cacheCreate;
2235
- row.cacheRead += day.cacheRead;
2236
- row.cacheSavings += day.cacheSavings;
2237
- row.total += day.total;
2238
- row.cost += day.cost;
2239
- row.count += day.count;
2240
2227
  for (const b of day.breakdown) {
2241
- let md = row.breakdown.find((x) => x.name === b.name);
2242
- if (!md) {
2243
- md = { name: b.name, input: 0, output: 0, cacheCreate: 0, cacheRead: 0, cacheSavings: 0, cost: 0, count: 0 };
2244
- row.breakdown.push(md);
2245
- }
2246
- md.input += b.input;
2247
- md.output += b.output;
2248
- md.cacheCreate += b.cacheCreate;
2249
- md.cacheRead += b.cacheRead;
2250
- md.cacheSavings += b.cacheSavings;
2251
- md.cost += b.cost;
2252
- md.count += b.count;
2253
- }
2254
- }
2255
- return [...out.values()].map((r) => {
2256
- r.breakdown.sort((a, b) => b.cost - a.cost);
2257
- r.models = r.breakdown.map((b) => b.name);
2258
- return r;
2259
- }).sort((a, b) => a.label.localeCompare(b.label));
2228
+ out.push({
2229
+ day: day.label,
2230
+ entry: {
2231
+ ts,
2232
+ model: b.name,
2233
+ cost: b.cost,
2234
+ count: Math.max(1, b.count),
2235
+ input: 0,
2236
+ output: 0,
2237
+ cacheCreate: 0,
2238
+ cacheRead: 0,
2239
+ cacheSavings: 0
2240
+ }
2241
+ });
2242
+ }
2243
+ }
2244
+ return out;
2245
+ }
2246
+ function overlayEntries(api, local, tz) {
2247
+ if (api.length === 0) return local.map((l) => l.entry);
2248
+ if (local.length === 0) return api;
2249
+ const apiDays = new Set(api.map((e) => dayKey(e.ts, tz)));
2250
+ return [...api, ...local.filter((l) => !apiDays.has(l.day)).map((l) => l.entry)];
2251
+ }
2252
+ async function cursorEntries(since, tz, homeDir) {
2253
+ const [apiResult, local] = await Promise.all([fetchApiEntries(homeDir), fetchLocalEntries(tz, homeDir)]);
2254
+ return overlayEntries(apiResult.entries, local, tz).filter((e) => e.ts >= since);
2260
2255
  }
2261
- async function cursorTable(tz, homeDir) {
2262
- const [api, local] = await Promise.all([cursorApiUsage(tz, homeDir), cursorUsageTable(tz, homeDir)]);
2263
- if (!api && !local) return EMPTY;
2264
- const daily = overlayDaily(local?.daily ?? [], api?.daily ?? []);
2265
- if (daily.length === 0) return EMPTY;
2266
- return { daily, weekly: reBucket(daily, tz, weekKey), monthly: reBucket(daily, tz, monthKey) };
2256
+ async function cursorDashboard(tz, homeDir) {
2257
+ return summarize(await cursorEntries(dashboardSince(tz), tz, homeDir), tz);
2267
2258
  }
2259
+ async function cursorTableFull(tz, homeDir) {
2260
+ return tabulate(await cursorEntries(tableSince(tz), tz, homeDir), tz);
2261
+ }
2262
+
2263
+ // src/providers/cursor/index.ts
2268
2264
  var cursorProvider = {
2269
2265
  id: "cursor",
2270
2266
  name: "Cursor",
2271
2267
  color: "magenta",
2272
- hasUsage: false,
2268
+ hasUsage: true,
2273
2269
  hasBilling: true,
2274
2270
  detect: (homeDir) => detectCursor(homeDir),
2275
- fetchTable: (account, tz) => cursorTable(tz, account.homeDir),
2271
+ fetchSummary: (account, tz) => cursorDashboard(tz, account.homeDir),
2272
+ fetchTable: (account, tz) => cursorTableFull(tz, account.homeDir),
2276
2273
  fetchBilling: (account, tz) => cursorBilling(account, tz)
2277
2274
  };
2278
2275
 
@@ -3448,28 +3445,409 @@ var geminiProvider = {
3448
3445
  fetchBilling: (account) => geminiBilling(account)
3449
3446
  };
3450
3447
 
3448
+ // src/providers/grok/identity.ts
3449
+ import { readFileSync as readFileSync2 } from "fs";
3450
+ import { join as join17 } from "path";
3451
+ import { homedir as homedir14 } from "os";
3452
+ function grokHomes(homeDir) {
3453
+ if (homeDir) {
3454
+ const base = expandHome(homeDir);
3455
+ return [.../* @__PURE__ */ new Set([join17(base, ".grok"), base])];
3456
+ }
3457
+ const homes = [];
3458
+ const env = envDir("GROK_HOME");
3459
+ if (env) homes.push(env);
3460
+ homes.push(join17(homedir14(), ".grok"));
3461
+ return [...new Set(homes)];
3462
+ }
3463
+ function grokAuthPaths(homeDir) {
3464
+ const explicit = process.env.GROK_AUTH_PATH;
3465
+ const paths = grokHomes(homeDir).map((h) => join17(h, "auth.json"));
3466
+ return explicit ? [explicit, ...paths] : paths;
3467
+ }
3468
+ function isAuthEntry(v) {
3469
+ return !!v && typeof v === "object" && typeof v.key === "string" && !!v.key;
3470
+ }
3471
+ function readGrokAuth(homeDir) {
3472
+ const inline = process.env.GROK_AUTH?.trim();
3473
+ if (inline) {
3474
+ try {
3475
+ const parsed = JSON.parse(inline);
3476
+ const fromInline = pickAuthEntry(parsed);
3477
+ if (fromInline) return fromInline;
3478
+ } catch {
3479
+ }
3480
+ }
3481
+ for (const path of grokAuthPaths(homeDir)) {
3482
+ try {
3483
+ const parsed = JSON.parse(readFileSync2(path, "utf-8"));
3484
+ const entry = pickAuthEntry(parsed);
3485
+ if (entry) return entry;
3486
+ } catch {
3487
+ }
3488
+ }
3489
+ return null;
3490
+ }
3491
+ function pickAuthEntry(parsed) {
3492
+ if (!parsed || typeof parsed !== "object") return null;
3493
+ const map = parsed;
3494
+ if (isAuthEntry(map)) return map;
3495
+ const preferred = [
3496
+ "https://accounts.x.ai/sign-in",
3497
+ "https://auth.x.ai"
3498
+ ];
3499
+ for (const key of preferred) {
3500
+ if (isAuthEntry(map[key])) return map[key];
3501
+ }
3502
+ const entries = Object.entries(map).filter(([, v]) => isAuthEntry(v)).map(([k, v]) => ({ key: k, entry: v }));
3503
+ if (entries.length === 0) return null;
3504
+ const session = entries.filter((e) => e.key !== "xai::api_key" && !e.key.startsWith("xai::"));
3505
+ const pool = session.length ? session : entries;
3506
+ return pool.sort(
3507
+ (a, b) => Date.parse(b.entry.create_time ?? b.entry.expires_at ?? "") - Date.parse(a.entry.create_time ?? a.entry.expires_at ?? "")
3508
+ )[0]?.entry ?? null;
3509
+ }
3510
+ function readGrokIdentity(homeDir) {
3511
+ const entry = readGrokAuth(homeDir);
3512
+ if (!entry) return {};
3513
+ const payload = entry.key.includes(".") ? decodeBase64UrlJson(entry.key.split(".")[1]) : null;
3514
+ const tier = typeof payload?.tier === "number" ? payload.tier : void 0;
3515
+ const expiresAt = entry.expires_at ? Date.parse(entry.expires_at) : void 0;
3516
+ return {
3517
+ email: typeof entry.email === "string" ? entry.email : void 0,
3518
+ displayName: typeof entry.first_name === "string" ? entry.first_name : void 0,
3519
+ userId: typeof entry.user_id === "string" ? entry.user_id : void 0,
3520
+ teamId: typeof entry.team_id === "string" ? entry.team_id : void 0,
3521
+ tier,
3522
+ expiresAt: Number.isFinite(expiresAt) ? expiresAt : void 0
3523
+ };
3524
+ }
3525
+ function grokClientVersion(homeDir) {
3526
+ for (const home of grokHomes(homeDir)) {
3527
+ try {
3528
+ const v = readFileSync2(join17(home, ".metadata_version"), "utf-8").trim();
3529
+ if (v) return v;
3530
+ } catch {
3531
+ }
3532
+ }
3533
+ return "0.2.93";
3534
+ }
3535
+
3536
+ // src/providers/grok/billing.ts
3537
+ var DEFAULT_BILLING_BASE = "https://grok.com";
3538
+ var BILLING_PATH = "/billing?format=credits";
3539
+ function billingBase() {
3540
+ return DEFAULT_BILLING_BASE;
3541
+ }
3542
+ function num(v) {
3543
+ return numberValue(v) ?? (typeof v === "number" && Number.isFinite(v) ? v : void 0);
3544
+ }
3545
+ function periodMs(v) {
3546
+ if (typeof v === "number" && Number.isFinite(v)) return v > 1e10 ? v : v * 1e3;
3547
+ if (typeof v === "string" && v.trim()) {
3548
+ const t = Date.parse(v);
3549
+ return Number.isFinite(t) ? t : null;
3550
+ }
3551
+ return null;
3552
+ }
3553
+ async function grokBilling(account) {
3554
+ const identity = readGrokIdentity(account.homeDir);
3555
+ const auth = readGrokAuth(account.homeDir);
3556
+ if (!auth?.key) {
3557
+ return {
3558
+ plan: null,
3559
+ metrics: [],
3560
+ error: "Not signed in \u2014 run `grok login`",
3561
+ ...identityFields(identity)
3562
+ };
3563
+ }
3564
+ const version = grokClientVersion(account.homeDir);
3565
+ try {
3566
+ const res = await fetch(`${billingBase()}${BILLING_PATH}`, {
3567
+ headers: {
3568
+ Authorization: `Bearer ${auth.key}`,
3569
+ "x-grok-client-version": version,
3570
+ Accept: "application/json",
3571
+ "User-Agent": "tokmon"
3572
+ },
3573
+ signal: AbortSignal.timeout(12e3)
3574
+ });
3575
+ if (res.status === 401 || res.status === 403) {
3576
+ return {
3577
+ plan: null,
3578
+ metrics: [],
3579
+ error: "Billing requires grok.com auth \u2014 run `grok login`",
3580
+ ...identityFields(identity)
3581
+ };
3582
+ }
3583
+ if (!res.ok) {
3584
+ return {
3585
+ plan: null,
3586
+ metrics: [],
3587
+ error: `Billing HTTP ${res.status}`,
3588
+ ...identityFields(identity)
3589
+ };
3590
+ }
3591
+ const data = await readJson(res);
3592
+ if (!data || typeof data !== "object") {
3593
+ return { plan: null, metrics: [], error: "Empty billing response", ...identityFields(identity) };
3594
+ }
3595
+ const cfg = data.currentPeriod !== void 0 || data.creditUsagePercent !== void 0 ? data : data.config ?? data.billing ?? data.data ?? data;
3596
+ const metrics = [];
3597
+ const usagePct = num(cfg.creditUsagePercent);
3598
+ if (usagePct !== void 0) {
3599
+ const end = periodMs(cfg.billingPeriodEnd ?? cfg.currentPeriod?.end ?? cfg.currentPeriod?.billingPeriodEnd);
3600
+ metrics.push(percentMetric("Credits", finite(usagePct), end ? msToIso(end) : null, true));
3601
+ }
3602
+ const monthlyLimit = num(cfg.monthlyLimit);
3603
+ const includedUsed = num(cfg.includedUsed ?? cfg.currentPeriod?.includedUsed ?? cfg.totalUsed);
3604
+ if (monthlyLimit !== void 0 && monthlyLimit > 0 && includedUsed !== void 0) {
3605
+ metrics.push({
3606
+ label: "Included",
3607
+ used: includedUsed,
3608
+ limit: monthlyLimit,
3609
+ format: { kind: "count" },
3610
+ primary: metrics.length === 0
3611
+ });
3612
+ }
3613
+ const onDemandCap = num(cfg.onDemandCap);
3614
+ const onDemandUsed = num(cfg.onDemandUsed);
3615
+ if (onDemandCap !== void 0 && onDemandCap > 0) {
3616
+ metrics.push({
3617
+ label: "On-demand",
3618
+ used: onDemandUsed ?? 0,
3619
+ limit: onDemandCap,
3620
+ format: { kind: "dollars" }
3621
+ });
3622
+ } else if (onDemandUsed !== void 0 && onDemandUsed > 0) {
3623
+ metrics.push({
3624
+ label: "On-demand",
3625
+ used: onDemandUsed,
3626
+ limit: null,
3627
+ format: { kind: "dollars" }
3628
+ });
3629
+ }
3630
+ const prepaid = num(cfg.prepaidBalance);
3631
+ if (prepaid !== void 0) {
3632
+ metrics.push({
3633
+ label: "Credits left",
3634
+ used: prepaid,
3635
+ limit: null,
3636
+ format: { kind: "dollars" }
3637
+ });
3638
+ }
3639
+ const plan = typeof cfg.plan === "string" && cfg.plan.trim() ? cfg.plan.trim() : cfg.isUnifiedBillingUser === true ? "SuperGrok" : identity.tier !== void 0 ? `Tier ${identity.tier}` : "Grok";
3640
+ return {
3641
+ plan,
3642
+ metrics,
3643
+ error: metrics.length ? null : "No billing metrics",
3644
+ ...identityFields(identity)
3645
+ };
3646
+ } catch (err) {
3647
+ return {
3648
+ plan: null,
3649
+ metrics: [],
3650
+ error: err instanceof Error ? err.message : "Billing request failed",
3651
+ ...identityFields(identity)
3652
+ };
3653
+ }
3654
+ }
3655
+
3656
+ // src/providers/grok/usage.ts
3657
+ import { access as access10, readdir as readdir7, readFile as readFile8, stat as fsStat6 } from "fs/promises";
3658
+ import { createReadStream as createReadStream6 } from "fs";
3659
+ import { createInterface as createInterface6 } from "readline";
3660
+ import { join as join18 } from "path";
3661
+ var PRICING4 = {
3662
+ "grok-build-latest": { in: 2e-6, cr: 5e-7, out: 6e-6 },
3663
+ "grok-4.5": { in: 2e-6, cr: 5e-7, out: 6e-6 },
3664
+ "grok-composer-2.5-fast": { in: 2e-6, cr: 5e-7, out: 6e-6 },
3665
+ "grok-4.3": { in: 125e-8, cr: 2e-7, out: 25e-7 },
3666
+ "grok-4.20": { in: 125e-8, cr: 2e-7, out: 25e-7 },
3667
+ "grok-build-0.1": { in: 1e-6, cr: 2e-7, out: 2e-6 },
3668
+ "grok-code-fast-1": { in: 1e-6, cr: 2e-7, out: 2e-6 },
3669
+ "grok-code-fast": { in: 1e-6, cr: 2e-7, out: 2e-6 },
3670
+ "grok-build": { in: 1e-6, cr: 2e-7, out: 2e-6 }
3671
+ };
3672
+ var FALLBACK_PRICE2 = PRICING4["grok-4.5"];
3673
+ var PRICE_KEYS4 = Object.keys(PRICING4).sort((a, b) => b.length - a.length);
3674
+ function modelKeyMatches2(model, key) {
3675
+ let idx = model.indexOf(key);
3676
+ while (idx >= 0) {
3677
+ const before = idx === 0 ? "" : model[idx - 1];
3678
+ const rest = model.slice(idx + key.length);
3679
+ const versionContinues = rest[0] === "." && /\d/.test(rest[1] ?? "");
3680
+ if ((!before || !/[a-z0-9-]/.test(before)) && !versionContinues && (rest === "" || rest[0] === "-" || !/[a-z0-9]/.test(rest[0]))) {
3681
+ return true;
3682
+ }
3683
+ idx = model.indexOf(key, idx + key.length);
3684
+ }
3685
+ return false;
3686
+ }
3687
+ function priceFor4(model) {
3688
+ const m = model.toLowerCase().trim();
3689
+ for (const key of PRICE_KEYS4) {
3690
+ if (modelKeyMatches2(m, key)) return PRICING4[key];
3691
+ }
3692
+ return FALLBACK_PRICE2;
3693
+ }
3694
+ async function detectGrok(homeDir) {
3695
+ for (const home of grokHomes(homeDir)) {
3696
+ for (const p of [join18(home, "logs", "unified.jsonl"), join18(home, "sessions"), join18(home, "auth.json"), join18(home, "bin", "grok")]) {
3697
+ try {
3698
+ await access10(p);
3699
+ return true;
3700
+ } catch {
3701
+ }
3702
+ }
3703
+ }
3704
+ return false;
3705
+ }
3706
+ async function loadSessionModels(home) {
3707
+ const out = /* @__PURE__ */ new Map();
3708
+ const sessionsRoot = join18(home, "sessions");
3709
+ let groups;
3710
+ try {
3711
+ groups = await readdir7(sessionsRoot);
3712
+ } catch {
3713
+ return out;
3714
+ }
3715
+ for (const group of groups) {
3716
+ if (group === "session_search.sqlite" || group.startsWith(".")) continue;
3717
+ const groupDir = join18(sessionsRoot, group);
3718
+ let sessions;
3719
+ try {
3720
+ sessions = await readdir7(groupDir);
3721
+ } catch {
3722
+ continue;
3723
+ }
3724
+ for (const sid of sessions) {
3725
+ try {
3726
+ const raw = JSON.parse(await readFile8(join18(groupDir, sid, "summary.json"), "utf-8"));
3727
+ const model = typeof raw?.current_model_id === "string" && raw.current_model_id ? raw.current_model_id : null;
3728
+ if (model) out.set(sid, model);
3729
+ } catch {
3730
+ }
3731
+ }
3732
+ }
3733
+ return out;
3734
+ }
3735
+ function costOf2(model, input, cacheRead, output) {
3736
+ const p = priceFor4(model);
3737
+ const cost = input * p.in + cacheRead * p.cr + output * p.out;
3738
+ const cacheSavings = cacheRead * Math.max(0, p.in - p.cr);
3739
+ return { cost, cacheSavings };
3740
+ }
3741
+ async function parseUnifiedLog(path, models) {
3742
+ const entries = [];
3743
+ const input = createReadStream6(path);
3744
+ input.on("error", () => {
3745
+ });
3746
+ const rl = createInterface6({ input, crlfDelay: Infinity });
3747
+ try {
3748
+ for await (const line of rl) {
3749
+ if (!line.includes("shell.turn.inference_done")) continue;
3750
+ try {
3751
+ const obj = JSON.parse(line.charCodeAt(0) === 65279 ? line.slice(1) : line);
3752
+ if (obj?.msg !== "shell.turn.inference_done" || !obj.ctx) continue;
3753
+ const ts = Date.parse(String(obj.ts ?? ""));
3754
+ if (!Number.isFinite(ts)) continue;
3755
+ const sid = typeof obj.sid === "string" ? obj.sid : "unknown";
3756
+ const ctx = obj.ctx;
3757
+ const prompt = safeNum(ctx.prompt_tokens);
3758
+ const cached = safeNum(ctx.cached_prompt_tokens);
3759
+ const completion = safeNum(ctx.completion_tokens);
3760
+ const inputTokens = Math.max(0, prompt - cached);
3761
+ const cacheRead = cached;
3762
+ const output = completion;
3763
+ if (inputTokens + cacheRead + output <= 0) continue;
3764
+ const model = models.get(sid) ?? "grok-4.5";
3765
+ const { cost, cacheSavings } = costOf2(model, inputTokens, cacheRead, output);
3766
+ const loop = safeNum(ctx.loop_index);
3767
+ entries.push({
3768
+ ts,
3769
+ id: `${sid}#${obj.ts}#${loop}`,
3770
+ model,
3771
+ input: inputTokens,
3772
+ output,
3773
+ cacheCreate: 0,
3774
+ cacheRead,
3775
+ cost,
3776
+ cacheSavings
3777
+ });
3778
+ } catch {
3779
+ }
3780
+ }
3781
+ } finally {
3782
+ rl.close();
3783
+ input.destroy();
3784
+ }
3785
+ return entries;
3786
+ }
3787
+ async function loadEntries6(since, homeDir) {
3788
+ const files = [];
3789
+ const modelByHome = /* @__PURE__ */ new Map();
3790
+ for (const home of grokHomes(homeDir)) {
3791
+ const logPath = join18(home, "logs", "unified.jsonl");
3792
+ try {
3793
+ await access10(logPath);
3794
+ } catch {
3795
+ continue;
3796
+ }
3797
+ const st = await fsStat6(logPath).catch(() => null);
3798
+ if (!st) continue;
3799
+ files.push({ path: logPath, mtimeMs: st.mtimeMs, size: st.size });
3800
+ modelByHome.set(logPath, await loadSessionModels(home));
3801
+ }
3802
+ if (files.length === 0) return [];
3803
+ return loadCachedEntries(
3804
+ files,
3805
+ async (path) => parseUnifiedLog(path, modelByHome.get(path) ?? /* @__PURE__ */ new Map()),
3806
+ since
3807
+ );
3808
+ }
3809
+ async function grokDashboard(tz, homeDir) {
3810
+ return summarize(await loadEntries6(dashboardSince(tz), homeDir), tz);
3811
+ }
3812
+ async function grokTable(tz, homeDir) {
3813
+ return tabulate(await loadEntries6(tableSince(tz), homeDir), tz);
3814
+ }
3815
+
3816
+ // src/providers/grok/index.ts
3817
+ var grokProvider = {
3818
+ id: "grok",
3819
+ name: "Grok",
3820
+ color: "yellowBright",
3821
+ hasUsage: true,
3822
+ hasBilling: true,
3823
+ detect: (homeDir) => detectGrok(homeDir),
3824
+ fetchSummary: (account, tz) => grokDashboard(tz, account.homeDir),
3825
+ fetchTable: (account, tz) => grokTable(tz, account.homeDir),
3826
+ fetchBilling: (account) => grokBilling(account)
3827
+ };
3828
+
3451
3829
  // src/providers/detect.ts
3452
3830
  import { accessSync as accessSync2, constants as constants2, existsSync } from "fs";
3453
- import { join as join17, delimiter as delimiter2, isAbsolute as isAbsolute2 } from "path";
3454
- import { homedir as homedir14 } from "os";
3831
+ import { join as join19, delimiter as delimiter2, isAbsolute as isAbsolute2 } from "path";
3832
+ import { homedir as homedir15 } from "os";
3455
3833
  function searchDirs() {
3456
- const home = homedir14();
3834
+ const home = homedir15();
3457
3835
  const fromEnv = (process.env.PATH ?? "").split(delimiter2).filter(Boolean);
3458
3836
  const extra = process.platform === "win32" ? [
3459
- process.env.APPDATA && join17(process.env.APPDATA, "npm"),
3460
- process.env.LOCALAPPDATA && join17(process.env.LOCALAPPDATA, "pnpm"),
3461
- join17(home, "scoop", "shims")
3837
+ process.env.APPDATA && join19(process.env.APPDATA, "npm"),
3838
+ process.env.LOCALAPPDATA && join19(process.env.LOCALAPPDATA, "pnpm"),
3839
+ join19(home, "scoop", "shims")
3462
3840
  ] : [
3463
3841
  "/opt/homebrew/bin",
3464
3842
  "/usr/local/bin",
3465
3843
  "/usr/bin",
3466
3844
  "/bin",
3467
3845
  "/opt/local/bin",
3468
- join17(home, ".local", "bin"),
3469
- join17(home, "bin"),
3470
- join17(home, ".npm-global", "bin"),
3471
- join17(home, ".bun", "bin"),
3472
- join17(home, ".local", "share", "pnpm")
3846
+ join19(home, ".local", "bin"),
3847
+ join19(home, "bin"),
3848
+ join19(home, ".npm-global", "bin"),
3849
+ join19(home, ".bun", "bin"),
3850
+ join19(home, ".local", "share", "pnpm")
3473
3851
  ];
3474
3852
  return [.../* @__PURE__ */ new Set([...fromEnv, ...extra.filter((d) => !!d)])];
3475
3853
  }
@@ -3486,7 +3864,7 @@ function onPath(names) {
3486
3864
  for (const dir of searchDirs()) {
3487
3865
  for (const n of names) {
3488
3866
  for (const e of exts) {
3489
- if (isExec2(join17(dir, n + e))) return true;
3867
+ if (isExec2(join19(dir, n + e))) return true;
3490
3868
  }
3491
3869
  }
3492
3870
  }
@@ -3496,7 +3874,7 @@ function anyExists(paths) {
3496
3874
  return paths.some((p) => !!p && isExec2(p));
3497
3875
  }
3498
3876
  function installSignals(id) {
3499
- const home = homedir14();
3877
+ const home = homedir15();
3500
3878
  const pf = process.env.ProgramFiles;
3501
3879
  const pf86 = process.env["ProgramFiles(x86)"];
3502
3880
  const lad = process.env.LOCALAPPDATA;
@@ -3504,26 +3882,26 @@ function installSignals(id) {
3504
3882
  case "claude":
3505
3883
  return onPath(["claude"]) || anyExists([
3506
3884
  "/Applications/Claude.app",
3507
- join17(home, "Applications", "Claude.app"),
3508
- lad && join17(lad, "Programs", "claude", "Claude.exe")
3885
+ join19(home, "Applications", "Claude.app"),
3886
+ lad && join19(lad, "Programs", "claude", "Claude.exe")
3509
3887
  ]);
3510
3888
  case "codex": {
3511
3889
  const bin = process.env.CODEX_BIN;
3512
3890
  if (bin && isAbsolute2(bin) && isExec2(bin)) return true;
3513
3891
  return onPath(["codex"]) || anyExists([
3514
- lad && join17(lad, "Programs", "OpenAI", "Codex", "bin", "codex.exe"),
3515
- lad && join17(lad, "Programs", "OpenAI", "Codex", "codex.exe"),
3516
- lad && join17(lad, "Programs", "codex", "codex.exe"),
3517
- pf && join17(pf, "OpenAI", "Codex", "bin", "codex.exe")
3518
- ]) || existsSync(join17(home, ".codex", "sessions")) || existsSync(join17(home, ".codex", "auth.json"));
3892
+ lad && join19(lad, "Programs", "OpenAI", "Codex", "bin", "codex.exe"),
3893
+ lad && join19(lad, "Programs", "OpenAI", "Codex", "codex.exe"),
3894
+ lad && join19(lad, "Programs", "codex", "codex.exe"),
3895
+ pf && join19(pf, "OpenAI", "Codex", "bin", "codex.exe")
3896
+ ]) || existsSync(join19(home, ".codex", "sessions")) || existsSync(join19(home, ".codex", "auth.json"));
3519
3897
  }
3520
3898
  case "cursor":
3521
3899
  return onPath(["cursor", "cursor-agent"]) || anyExists([
3522
3900
  "/Applications/Cursor.app",
3523
- join17(home, "Applications", "Cursor.app"),
3524
- lad && join17(lad, "Programs", "cursor", "Cursor.exe"),
3525
- pf && join17(pf, "Cursor", "Cursor.exe"),
3526
- pf86 && join17(pf86, "Cursor", "Cursor.exe"),
3901
+ join19(home, "Applications", "Cursor.app"),
3902
+ lad && join19(lad, "Programs", "cursor", "Cursor.exe"),
3903
+ pf && join19(pf, "Cursor", "Cursor.exe"),
3904
+ pf86 && join19(pf86, "Cursor", "Cursor.exe"),
3527
3905
  "/opt/Cursor/cursor",
3528
3906
  "/usr/share/cursor/cursor",
3529
3907
  "/usr/bin/cursor"
@@ -3535,21 +3913,26 @@ function installSignals(id) {
3535
3913
  case "copilot": {
3536
3914
  const appData = process.env.APPDATA;
3537
3915
  return [
3538
- join17(home, ".config", "github-copilot"),
3539
- join17(home, ".copilot"),
3540
- lad && join17(lad, "github-copilot"),
3541
- appData && join17(appData, "GitHub Copilot"),
3542
- join17(home, ".local", "share", "gh", "extensions", "gh-copilot")
3916
+ join19(home, ".config", "github-copilot"),
3917
+ join19(home, ".copilot"),
3918
+ lad && join19(lad, "github-copilot"),
3919
+ appData && join19(appData, "GitHub Copilot"),
3920
+ join19(home, ".local", "share", "gh", "extensions", "gh-copilot")
3543
3921
  ].some((p) => !!p && existsSync(p));
3544
3922
  }
3545
3923
  case "antigravity":
3546
3924
  return onPath(["antigravity"]) || anyExists([
3547
3925
  "/Applications/Antigravity.app",
3548
- join17(home, "Applications", "Antigravity.app"),
3549
- lad && join17(lad, "Programs", "Antigravity", "Antigravity.exe")
3926
+ join19(home, "Applications", "Antigravity.app"),
3927
+ lad && join19(lad, "Programs", "Antigravity", "Antigravity.exe")
3550
3928
  ]);
3551
3929
  case "gemini":
3552
3930
  return onPath(["gemini"]);
3931
+ case "grok":
3932
+ return onPath(["grok"]) || anyExists([
3933
+ join19(home, ".grok", "bin", "grok"),
3934
+ join19(home, ".grok", "bin", "grok.exe")
3935
+ ]) || existsSync(join19(home, ".grok", "auth.json")) || existsSync(join19(home, ".grok", "sessions")) || existsSync(join19(home, ".grok", "logs", "unified.jsonl"));
3553
3936
  default:
3554
3937
  return false;
3555
3938
  }
@@ -3565,7 +3948,8 @@ var PROVIDERS = {
3565
3948
  opencode: opencodeProvider,
3566
3949
  copilot: copilotProvider,
3567
3950
  antigravity: antigravityProvider,
3568
- gemini: geminiProvider
3951
+ gemini: geminiProvider,
3952
+ grok: grokProvider
3569
3953
  };
3570
3954
  async function detectProviders() {
3571
3955
  const found = await Promise.all(
@@ -3582,20 +3966,20 @@ async function detectProviders() {
3582
3966
  }
3583
3967
 
3584
3968
  // src/accounts.ts
3585
- import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync3, statSync } from "fs";
3586
- import { homedir as homedir15 } from "os";
3587
- import { basename, join as join19, resolve } from "path";
3969
+ import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync4, statSync } from "fs";
3970
+ import { homedir as homedir16 } from "os";
3971
+ import { basename, join as join21, resolve } from "path";
3588
3972
 
3589
3973
  // src/providers/codex/identity.ts
3590
- import { readFileSync as readFileSync2 } from "fs";
3591
- import { join as join18 } from "path";
3974
+ import { readFileSync as readFileSync3 } from "fs";
3975
+ import { join as join20 } from "path";
3592
3976
  function codexAuthPaths(homeDir) {
3593
- return [join18(homeDir, ".codex", "auth.json"), join18(homeDir, "auth.json")];
3977
+ return [join20(homeDir, ".codex", "auth.json"), join20(homeDir, "auth.json")];
3594
3978
  }
3595
3979
  function readCodexIdentity(homeDir) {
3596
3980
  for (const path of codexAuthPaths(homeDir)) {
3597
3981
  try {
3598
- const parsed = JSON.parse(readFileSync2(path, "utf-8"));
3982
+ const parsed = JSON.parse(readFileSync3(path, "utf-8"));
3599
3983
  const { email, displayName, payload } = identityFromIdToken(parsed?.tokens?.id_token);
3600
3984
  if (!payload) continue;
3601
3985
  return { email, displayName };
@@ -3607,7 +3991,7 @@ function readCodexIdentity(homeDir) {
3607
3991
 
3608
3992
  // src/accounts.ts
3609
3993
  function accountKey(providerId, homeDir) {
3610
- return `${providerId}:${homeDir ? resolve(expandHome(homeDir)) : homedir15()}`;
3994
+ return `${providerId}:${homeDir ? resolve(expandHome(homeDir)) : homedir16()}`;
3611
3995
  }
3612
3996
  function uniqueId(base, used) {
3613
3997
  let id = slugify(base) || "account";
@@ -3627,10 +4011,10 @@ function uniqueId(base, used) {
3627
4011
  return id;
3628
4012
  }
3629
4013
  function hasClaudeState(homeDir) {
3630
- return existsSync2(join19(homeDir, ".claude.json")) || existsSync2(join19(homeDir, ".claude", ".credentials.json")) || existsSync2(join19(homeDir, ".claude", "projects")) || existsSync2(join19(homeDir, ".config", "claude", ".credentials.json")) || existsSync2(join19(homeDir, ".config", "claude", "projects"));
4014
+ return existsSync2(join21(homeDir, ".claude.json")) || existsSync2(join21(homeDir, ".claude", ".credentials.json")) || existsSync2(join21(homeDir, ".claude", "projects")) || existsSync2(join21(homeDir, ".config", "claude", ".credentials.json")) || existsSync2(join21(homeDir, ".config", "claude", "projects"));
3631
4015
  }
3632
4016
  function candidateAlternateHomes(prefix) {
3633
- const home = homedir15();
4017
+ const home = homedir16();
3634
4018
  let entries;
3635
4019
  try {
3636
4020
  entries = readdirSync(home);
@@ -3641,7 +4025,7 @@ function candidateAlternateHomes(prefix) {
3641
4025
  const pattern = new RegExp(`^\\.${prefix}[_-]`);
3642
4026
  for (const name of entries) {
3643
4027
  if (!pattern.test(name)) continue;
3644
- const path = join19(home, name);
4028
+ const path = join21(home, name);
3645
4029
  try {
3646
4030
  if (!statSync(path).isDirectory()) continue;
3647
4031
  out.push(path);
@@ -3660,7 +4044,7 @@ function labelForClaudeHome(homeDir) {
3660
4044
  function hasCodexAuth(homeDir) {
3661
4045
  for (const path of codexAuthPaths(homeDir)) {
3662
4046
  try {
3663
- const parsed = JSON.parse(readFileSync3(path, "utf-8"));
4047
+ const parsed = JSON.parse(readFileSync4(path, "utf-8"));
3664
4048
  const accessToken = parsed?.tokens?.access_token;
3665
4049
  if (typeof accessToken === "string" && accessToken.trim()) return true;
3666
4050
  } catch {
@@ -3782,7 +4166,8 @@ var PROVIDER_HEX = {
3782
4166
  pi: NAMED_HEX.blue,
3783
4167
  opencode: NAMED_HEX.yellow,
3784
4168
  antigravity: NAMED_HEX.red,
3785
- gemini: NAMED_HEX.greenBright
4169
+ gemini: NAMED_HEX.greenBright,
4170
+ grok: NAMED_HEX.yellowBright
3786
4171
  };
3787
4172
  function namedHex(name) {
3788
4173
  if (!name) return FALLBACK_HEX;
@@ -3950,7 +4335,6 @@ export {
3950
4335
  time,
3951
4336
  shortDate,
3952
4337
  col,
3953
- cursorModelSpend,
3954
4338
  PROVIDER_ORDER,
3955
4339
  PROVIDERS,
3956
4340
  detectProviders,