tokmon 0.19.8 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,134 +1,15 @@
1
1
  #!/usr/bin/env node
2
-
3
- // src/config.ts
4
- import { readFile, writeFile, mkdir, rename } from "fs/promises";
5
- import { join, isAbsolute } from "path";
6
- import { homedir } from "os";
7
- function envDir(name) {
8
- const v = process.env[name];
9
- return v && v.trim() && isAbsolute(v.trim()) ? v.trim() : void 0;
10
- }
11
- var DEFAULTS = {
12
- interval: 2,
13
- billingInterval: 5,
14
- clearScreen: true,
15
- timezone: null,
16
- accounts: [],
17
- activeAccountId: null,
18
- disabledProviders: [],
19
- onboarded: false,
20
- dashboardLayout: "grid",
21
- defaultFocus: "all",
22
- ascii: "auto",
23
- knownProviders: []
24
- };
25
- var LEGACY_KNOWN = ["claude", "codex", "cursor"];
26
- var ACCENT_COLORS = ["cyan", "magenta", "green", "yellow", "blue", "red"];
27
- function configDir() {
28
- if (process.platform === "win32") {
29
- return join(envDir("APPDATA") ?? join(homedir(), "AppData", "Roaming"), "tokmon");
30
- }
31
- return join(envDir("XDG_CONFIG_HOME") ?? join(homedir(), ".config"), "tokmon");
32
- }
33
- function configLocation() {
34
- return join(configDir(), "config.json");
35
- }
36
- function cacheDir() {
37
- if (process.platform === "win32") {
38
- return join(envDir("LOCALAPPDATA") ?? envDir("APPDATA") ?? join(homedir(), "AppData", "Local"), "tokmon", "cache");
39
- }
40
- if (process.platform === "darwin") {
41
- return join(homedir(), "Library", "Caches", "tokmon");
42
- }
43
- return join(envDir("XDG_CACHE_HOME") ?? join(homedir(), ".cache"), "tokmon");
44
- }
45
- var PROVIDER_IDS = ["claude", "codex", "cursor", "pi", "opencode", "copilot", "antigravity", "gemini"];
46
- function clampNum(v, fallback, min) {
47
- return typeof v === "number" && Number.isFinite(v) && v >= min ? v : fallback;
48
- }
49
- async function loadConfig() {
50
- let raw;
51
- try {
52
- raw = await readFile(configLocation(), "utf-8");
53
- } catch {
54
- return { ...DEFAULTS };
55
- }
56
- let parsed;
57
- try {
58
- parsed = JSON.parse(raw);
59
- } catch {
60
- try {
61
- await writeFile(configLocation() + ".bak", raw);
62
- } catch {
63
- }
64
- return { ...DEFAULTS };
65
- }
66
- try {
67
- const accounts = (Array.isArray(parsed.accounts) ? parsed.accounts : []).map((a) => ({ ...a, providerId: a.providerId ?? "claude" })).filter((a) => typeof a?.id === "string" && typeof a?.name === "string" && PROVIDER_IDS.includes(a.providerId));
68
- return {
69
- ...DEFAULTS,
70
- ...parsed,
71
- interval: clampNum(parsed.interval, DEFAULTS.interval, 1),
72
- billingInterval: clampNum(parsed.billingInterval, DEFAULTS.billingInterval, 1),
73
- clearScreen: typeof parsed.clearScreen === "boolean" ? parsed.clearScreen : DEFAULTS.clearScreen,
74
- timezone: typeof parsed.timezone === "string" && parsed.timezone.trim() ? parsed.timezone : null,
75
- accounts,
76
- activeAccountId: typeof parsed.activeAccountId === "string" ? parsed.activeAccountId : null,
77
- disabledProviders: (Array.isArray(parsed.disabledProviders) ? parsed.disabledProviders : []).filter((p) => PROVIDER_IDS.includes(p)),
78
- onboarded: parsed.onboarded === true,
79
- dashboardLayout: parsed.dashboardLayout === "single" ? "single" : "grid",
80
- defaultFocus: parsed.defaultFocus === "last" ? "last" : "all",
81
- ascii: parsed.ascii === "on" ? "on" : parsed.ascii === "off" ? "off" : "auto",
82
- knownProviders: Array.isArray(parsed.knownProviders) ? parsed.knownProviders.filter((p) => PROVIDER_IDS.includes(p)) : parsed.onboarded === true ? [...LEGACY_KNOWN] : []
83
- };
84
- } catch {
85
- return { ...DEFAULTS };
86
- }
87
- }
88
- var saveQueue = Promise.resolve();
89
- function saveConfig(config) {
90
- saveQueue = saveQueue.then(async () => {
91
- try {
92
- const dir = configDir();
93
- await mkdir(dir, { recursive: true });
94
- const tmp = join(dir, `config.json.${process.pid}.tmp`);
95
- await writeFile(tmp, JSON.stringify(config, null, 2) + "\n");
96
- await rename(tmp, configLocation());
97
- } catch {
98
- }
99
- });
100
- return saveQueue;
101
- }
102
- function slugify(value) {
103
- return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 48);
104
- }
105
- function generateAccountId(name, existing) {
106
- const base = slugify(name) || "account";
107
- const taken = new Set(existing.map((a) => a.id));
108
- if (!taken.has(base)) return base;
109
- for (let i = 2; i < 1e3; i++) {
110
- const candidate = `${base}_${i}`;
111
- if (!taken.has(candidate)) return candidate;
112
- }
113
- return `${base}_${Date.now()}`;
114
- }
115
- function pickAccentColor(existing) {
116
- const used = new Set(existing.map((a) => a.color).filter(Boolean));
117
- for (const c of ACCENT_COLORS) {
118
- if (!used.has(c)) return c;
119
- }
120
- return ACCENT_COLORS[existing.length % ACCENT_COLORS.length];
121
- }
122
- function expandHome(p) {
123
- if (!p) return homedir();
124
- if (p === "~" || p === "~/" || p === "~\\") return homedir();
125
- if (p.startsWith("~/") || p.startsWith("~\\")) return join(homedir(), p.slice(2));
126
- return p;
127
- }
2
+ import {
3
+ cacheDir,
4
+ envDir,
5
+ expandHome,
6
+ isValidTimezone,
7
+ slugify
8
+ } from "./chunk-MVMPQJ5S.js";
128
9
 
129
10
  // src/providers/usage-core.ts
130
- import { readFile as readFile2, writeFile as writeFile2, rename as rename2, mkdir as mkdir2 } from "fs/promises";
131
- import { join as join2 } from "path";
11
+ import { readFile, writeFile, rename, mkdir } from "fs/promises";
12
+ import { join } from "path";
132
13
 
133
14
  // src/tz.ts
134
15
  function systemTimezone() {
@@ -138,14 +19,6 @@ function systemTimezone() {
138
19
  return "UTC";
139
20
  }
140
21
  }
141
- function isValidTimezone(tz) {
142
- try {
143
- new Intl.DateTimeFormat("en-CA", { timeZone: tz });
144
- return true;
145
- } catch {
146
- return false;
147
- }
148
- }
149
22
  function resolveTimezone(cfg) {
150
23
  if (!cfg) return systemTimezone();
151
24
  return isValidTimezone(cfg) ? cfg : systemTimezone();
@@ -250,7 +123,7 @@ var diskLoaded = false;
250
123
  var dirty = false;
251
124
  var flushTimer = null;
252
125
  function cacheFile() {
253
- return join2(cacheDir(), `usage-v${CACHE_VERSION}.json`);
126
+ return join(cacheDir(), `usage-v${CACHE_VERSION}.json`);
254
127
  }
255
128
  function encode(mtimeMs, size, entries) {
256
129
  const mods = [];
@@ -292,7 +165,7 @@ async function ensureDiskLoaded() {
292
165
  if (diskLoaded) return;
293
166
  diskLoaded = true;
294
167
  try {
295
- const obj = JSON.parse(await readFile2(cacheFile(), "utf-8"));
168
+ const obj = JSON.parse(await readFile(cacheFile(), "utf-8"));
296
169
  for (const [path, s] of Object.entries(obj)) {
297
170
  if (s && typeof s.m === "number" && Array.isArray(s.rows) && Array.isArray(s.mods)) {
298
171
  memCache.set(path, { mtimeMs: s.m, size: typeof s.s === "number" ? s.s : -1, entries: decode(s) });
@@ -311,10 +184,10 @@ async function flushDisk() {
311
184
  }
312
185
  }
313
186
  try {
314
- await mkdir2(cacheDir(), { recursive: true });
187
+ await mkdir(cacheDir(), { recursive: true });
315
188
  const tmp = `${cacheFile()}.${process.pid}.tmp`;
316
- await writeFile2(tmp, JSON.stringify(obj));
317
- await rename2(tmp, cacheFile());
189
+ await writeFile(tmp, JSON.stringify(obj));
190
+ await rename(tmp, cacheFile());
318
191
  dirty = false;
319
192
  } catch {
320
193
  }
@@ -356,10 +229,34 @@ async function loadCachedEntries(files, parse, since) {
356
229
  function safeNum(v) {
357
230
  return typeof v === "number" && Number.isFinite(v) && v > 0 ? Math.floor(v) : 0;
358
231
  }
232
+ function dashboardSince(tz) {
233
+ const now = Date.now();
234
+ return Math.min(startOfMonth(now, tz), startOfWeek(now, tz), now - SPARK_DAYS * DAY_MS);
235
+ }
236
+ function tableSince(tz) {
237
+ return monthsAgoStart(Date.now(), 6, tz);
238
+ }
239
+ function finitePositive(v) {
240
+ return typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0;
241
+ }
242
+ function cleanEntry(e) {
243
+ return {
244
+ ...e,
245
+ ts: finitePositive(e.ts),
246
+ cost: finitePositive(e.cost),
247
+ input: finitePositive(e.input),
248
+ output: finitePositive(e.output),
249
+ cacheCreate: finitePositive(e.cacheCreate),
250
+ cacheRead: finitePositive(e.cacheRead),
251
+ cacheSavings: finitePositive(e.cacheSavings)
252
+ };
253
+ }
359
254
  function dedupe(entries) {
360
255
  const seen = /* @__PURE__ */ new Set();
361
256
  const out = [];
362
- for (const e of entries) {
257
+ for (const raw of entries) {
258
+ const e = cleanEntry(raw);
259
+ if (e.ts <= 0) continue;
363
260
  const k = e.id ?? `${e.ts} ${e.model} ${e.input} ${e.output} ${e.cacheCreate} ${e.cacheRead}`;
364
261
  if (seen.has(k)) continue;
365
262
  seen.add(k);
@@ -384,7 +281,8 @@ function summarize(entries, tz) {
384
281
  s.cacheRead += e.cacheRead;
385
282
  s.cacheSavings += e.cacheSavings;
386
283
  };
387
- for (const e of entries) {
284
+ for (const raw of entries) {
285
+ const e = cleanEntry(raw);
388
286
  if (e.ts >= monthStart) add(month, e);
389
287
  if (e.ts >= weekStart) add(week, e);
390
288
  if (e.ts >= todayStart) {
@@ -396,14 +294,16 @@ function summarize(entries, tz) {
396
294
  byDay.set(dk, (byDay.get(dk) ?? 0) + e.cost);
397
295
  }
398
296
  const hrs = Math.max((now - oldestToday) / 36e5, 1 / 60);
399
- const burnRate = hadToday ? today.cost / hrs : 0;
297
+ const rawBurnRate = hadToday ? today.cost / hrs : 0;
298
+ const burnRate = Number.isFinite(rawBurnRate) ? rawBurnRate : 0;
400
299
  const series = [];
401
300
  for (let i = SPARK_DAYS - 1; i >= 0; i--) series.push(byDay.get(dayKey(now - i * DAY_MS, tz)) ?? 0);
402
301
  return { today, week, month, burnRate, series };
403
302
  }
404
303
  function groupBy(entries, keyFn) {
405
304
  const groups = /* @__PURE__ */ new Map();
406
- for (const e of entries) {
305
+ for (const raw of entries) {
306
+ const e = cleanEntry(raw);
407
307
  const key = keyFn(e);
408
308
  const arr = groups.get(key);
409
309
  if (arr) arr.push(e);
@@ -511,23 +411,34 @@ function mergeTables(list) {
511
411
  monthly: mergeRows(list.map((t) => t.monthly))
512
412
  };
513
413
  }
514
-
515
- // src/format.ts
516
- function currency(value) {
517
- if (!Number.isFinite(value) || value <= 0) return "$0.00";
518
- if (value >= 1e4) {
519
- return `$${value.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
520
- }
521
- return `$${value.toFixed(2)}`;
414
+ function coalesceTables(list) {
415
+ if (list.length === 0) return { daily: [], weekly: [], monthly: [] };
416
+ if (list.length === 1) return list[0];
417
+ return mergeTables(list);
522
418
  }
523
- function tokens(value) {
524
- const v = Number.isFinite(value) && value > 0 ? value : 0;
525
- if (v >= 1e9) return `${(v / 1e9).toFixed(1)}B`;
526
- if (v >= 1e6) return `${(v / 1e6).toFixed(1)}M`;
527
- if (v >= 1e3) return `${(v / 1e3).toFixed(1)}K`;
528
- return String(Math.floor(v));
529
- }
530
- function time(date, tz) {
419
+
420
+ // src/shared/format.ts
421
+ var MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
422
+ function formatCurrency(value, opts = {}) {
423
+ if (!Number.isFinite(value)) return "$0.00";
424
+ const sign = opts.sign && value > 0 ? "+" : "";
425
+ const abs = Math.abs(value);
426
+ if (abs >= 1e5) return `${sign}$${(value / 1e3).toFixed(0)}k`;
427
+ if (abs >= 1e4) return `${sign}$${(value / 1e3).toFixed(1)}k`;
428
+ if (abs >= 1) return `${sign}$${value.toFixed(2)}`;
429
+ if (abs >= 0.01) return `${sign}$${value.toFixed(3)}`;
430
+ if (abs === 0) return "$0.00";
431
+ return `${sign}$${value.toFixed(4)}`;
432
+ }
433
+ function formatTokens(value) {
434
+ if (!Number.isFinite(value) || value === 0) return "0";
435
+ const abs = Math.abs(value);
436
+ if (abs >= 1e9) return `${(value / 1e9).toFixed(2)}B`;
437
+ if (abs >= 1e6) return `${(value / 1e6).toFixed(2)}M`;
438
+ if (abs >= 1e3) return `${(value / 1e3).toFixed(1)}k`;
439
+ return String(Math.round(value));
440
+ }
441
+ function formatTime(date, tz) {
531
442
  return date.toLocaleTimeString(void 0, {
532
443
  hour: "2-digit",
533
444
  minute: "2-digit",
@@ -535,33 +446,43 @@ function time(date, tz) {
535
446
  timeZone: tz
536
447
  });
537
448
  }
538
- var SHORT_MONTHS = ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
449
+ function formatShortDate(label, opts = {}) {
450
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(label);
451
+ if (!match) return label;
452
+ const day = Number(match[3]);
453
+ return `${MONTHS[Number(match[2]) - 1]} ${opts.padDay ? day.toString().padStart(2, " ") : day}`;
454
+ }
455
+ function formatResetIn(iso, now = Date.now()) {
456
+ const diff = new Date(iso).getTime() - now;
457
+ if (!Number.isFinite(diff) || diff <= 0) return "now";
458
+ const minutes = Math.round(diff / 6e4);
459
+ if (minutes < 60) return `${minutes}m`;
460
+ const hours = Math.floor(minutes / 60);
461
+ const mins = minutes % 60;
462
+ if (hours < 24) return `${hours}h ${mins}m`;
463
+ const days = Math.floor(hours / 24);
464
+ const hrs = hours % 24;
465
+ return `${days}d ${hrs}h`;
466
+ }
467
+
468
+ // src/format.ts
469
+ var currency = formatCurrency;
470
+ var tokens = formatTokens;
471
+ var time = formatTime;
539
472
  function shortDate(iso) {
540
- const [, m, d] = iso.split("-");
541
- return `${SHORT_MONTHS[Number(m)]} ${Number(d).toString().padStart(2, " ")}`;
473
+ return formatShortDate(iso, { padDay: true });
542
474
  }
543
475
  function col(s, w, align = "right") {
544
476
  if (s.length > w) return s.slice(0, w - 1) + "~";
545
477
  const spaces = " ".repeat(w - s.length);
546
478
  return align === "right" ? spaces + s : s + spaces;
547
479
  }
548
- function resetIn(iso) {
549
- const diff = new Date(iso).getTime() - Date.now();
550
- if (!Number.isFinite(diff) || diff <= 0) return "now";
551
- const mins = Math.round(diff / 6e4);
552
- if (mins < 60) return `${mins}m`;
553
- const hrs = Math.floor(mins / 60);
554
- const m = mins % 60;
555
- if (hrs < 24) return `${hrs}h ${m}m`;
556
- const days = Math.floor(hrs / 24);
557
- const h = hrs % 24;
558
- return `${days}d ${h}h`;
559
- }
480
+ var resetIn = formatResetIn;
560
481
 
561
482
  // src/providers/cursor/billing.ts
562
483
  import { access } from "fs/promises";
563
- import { join as join4 } from "path";
564
- import { homedir as homedir3 } from "os";
484
+ import { join as join3 } from "path";
485
+ import { homedir as homedir2 } from "os";
565
486
 
566
487
  // src/http.ts
567
488
  async function readJson(res) {
@@ -574,9 +495,28 @@ async function readJson(res) {
574
495
  }
575
496
  }
576
497
 
498
+ // src/providers/_shared/metric.ts
499
+ var finite = (value, fallback = 0) => typeof value === "number" && Number.isFinite(value) ? value : fallback;
500
+ function percentMetric(label, used, resetsAt, primary) {
501
+ return {
502
+ label,
503
+ used: finite(used),
504
+ limit: 100,
505
+ format: { kind: "percent" },
506
+ resetsAt,
507
+ ...primary === void 0 ? {} : { primary }
508
+ };
509
+ }
510
+ var dollars = (cents) => finite(cents) / 100;
511
+
512
+ // src/providers/_shared/time.ts
513
+ function msToIso(ms) {
514
+ return Number.isFinite(ms) && Math.abs(ms) <= 864e13 ? new Date(ms).toISOString() : null;
515
+ }
516
+
577
517
  // src/providers/cursor/activity.ts
578
- import { join as join3 } from "path";
579
- import { homedir as homedir2 } from "os";
518
+ import { join as join2 } from "path";
519
+ import { homedir } from "os";
580
520
 
581
521
  // src/providers/cursor/sqlite.ts
582
522
  import { execFile as execFileCb } from "child_process";
@@ -659,7 +599,7 @@ function sqliteStatusMessage(status) {
659
599
  // src/providers/cursor/activity.ts
660
600
  var DAY_MS2 = 864e5;
661
601
  function trackingDb(homeDir) {
662
- return join3(homeDir ?? homedir2(), ".cursor", "ai-tracking", "ai-code-tracking.db");
602
+ return join2(homeDir ?? homedir(), ".cursor", "ai-tracking", "ai-code-tracking.db");
663
603
  }
664
604
  function localDayKey(ms) {
665
605
  const d = new Date(ms);
@@ -677,7 +617,8 @@ async function cursorActivity(homeDir) {
677
617
  const byDay = /* @__PURE__ */ new Map();
678
618
  let month = 0;
679
619
  for (const row of res.rows) {
680
- const n = Number(row.c) || 0;
620
+ const raw = Number(row.c);
621
+ const n = Number.isFinite(raw) && raw > 0 ? raw : 0;
681
622
  byDay.set(String(row.d), n);
682
623
  month += n;
683
624
  }
@@ -694,18 +635,19 @@ async function cursorActivity(homeDir) {
694
635
  var BASE = "https://api2.cursor.sh/aiserver.v1.DashboardService";
695
636
  var USAGE_URL = `${BASE}/GetCurrentPeriodUsage`;
696
637
  var PLAN_URL = `${BASE}/GetPlanInfo`;
638
+ var finiteNumber = (value) => typeof value === "number" && Number.isFinite(value);
697
639
  function cursorStateDb(homeDir) {
698
- const base = homeDir ?? homedir3();
640
+ const base = homeDir ?? homedir2();
699
641
  const tail = ["Cursor", "User", "globalStorage", "state.vscdb"];
700
642
  if (process.platform === "darwin") {
701
- return join4(base, "Library", "Application Support", ...tail);
643
+ return join3(base, "Library", "Application Support", ...tail);
702
644
  }
703
645
  if (process.platform === "win32") {
704
- const roaming = homeDir ? join4(homeDir, "AppData", "Roaming") : envDir("APPDATA") ?? join4(base, "AppData", "Roaming");
705
- return join4(roaming, ...tail);
646
+ const roaming = homeDir ? join3(homeDir, "AppData", "Roaming") : envDir("APPDATA") ?? join3(base, "AppData", "Roaming");
647
+ return join3(roaming, ...tail);
706
648
  }
707
- const cfg = homeDir ? join4(homeDir, ".config") : envDir("XDG_CONFIG_HOME") ?? join4(base, ".config");
708
- return join4(cfg, ...tail);
649
+ const cfg = homeDir ? join3(homeDir, ".config") : envDir("XDG_CONFIG_HOME") ?? join3(base, ".config");
650
+ return join3(cfg, ...tail);
709
651
  }
710
652
  async function detectCursor(homeDir) {
711
653
  try {
@@ -720,6 +662,22 @@ async function readState(db, key) {
720
662
  const raw = r.status === "ok" ? r.rows[0]?.value : void 0;
721
663
  return { value: typeof raw === "string" && raw.trim() ? raw.trim() : null, status: r.status };
722
664
  }
665
+ function cleanStoredString(value) {
666
+ if (!value) return void 0;
667
+ try {
668
+ const parsed = JSON.parse(value);
669
+ if (typeof parsed === "string" && parsed.trim()) return parsed.trim();
670
+ } catch {
671
+ }
672
+ const trimmed = value.trim().replace(/^"|"$/g, "");
673
+ return trimmed || void 0;
674
+ }
675
+ function identityFields(email, displayName) {
676
+ return {
677
+ email: email ?? null,
678
+ displayName: displayName ?? null
679
+ };
680
+ }
723
681
  async function connectPost(url, token) {
724
682
  try {
725
683
  const res = await fetch(url, {
@@ -739,7 +697,6 @@ async function connectPost(url, token) {
739
697
  return null;
740
698
  }
741
699
  }
742
- var dollars = (cents) => cents / 100;
743
700
  async function cursorBilling(account) {
744
701
  const [core, activity, spend] = await Promise.all([
745
702
  cursorBillingCore(account),
@@ -749,27 +706,31 @@ async function cursorBilling(account) {
749
706
  let merged = activity;
750
707
  if (spend) {
751
708
  const lines = activity?.summary ?? "";
752
- const spendLabel = `$${Math.round(spend.total)} all-time`;
709
+ const spendLabel = `$${Math.round(finite(spend.total))} all-time`;
753
710
  merged = {
754
711
  series: activity?.series ?? [],
755
712
  summary: lines ? `${lines} \xB7 ${spendLabel}` : spendLabel
756
713
  };
757
714
  }
758
- const modelSpend = spend?.models?.length ? spend.models.slice(0, 6).map((m) => ({ name: m.name, usd: m.usd, requests: m.requests })) : null;
715
+ const modelSpend = spend?.models?.length ? spend.models.map((m) => ({ name: m.name, usd: finite(m.usd), requests: finite(m.requests) })) : null;
759
716
  return { ...core, activity: merged, modelSpend };
760
717
  }
761
718
  async function cursorBillingCore(account) {
762
719
  const db = cursorStateDb(account.homeDir);
763
- const [tokenRes, membershipRes] = await Promise.all([
720
+ const [tokenRes, membershipRes, emailRes, nameRes] = await Promise.all([
764
721
  readState(db, "cursorAuth/accessToken"),
765
- readState(db, "cursorAuth/stripeMembershipType")
722
+ readState(db, "cursorAuth/stripeMembershipType"),
723
+ readState(db, "cursorAuth/cachedEmail"),
724
+ readState(db, "cursorAuth/cachedName")
766
725
  ]);
767
726
  const token = tokenRes.value;
768
727
  const membership = membershipRes.value;
728
+ const email = cleanStoredString(emailRes.value);
729
+ const displayName = cleanStoredString(nameRes.value);
769
730
  const planFallback = membership ? membership.charAt(0).toUpperCase() + membership.slice(1) : null;
770
731
  if (!token) {
771
732
  const error = tokenRes.status === "ok" ? "Not signed in \u2014 open Cursor" : sqliteStatusMessage(tokenRes.status);
772
- return { plan: planFallback, metrics: [], error };
733
+ return { plan: planFallback, metrics: [], error, ...identityFields(email, displayName) };
773
734
  }
774
735
  const [usage, planInfo] = await Promise.all([
775
736
  connectPost(USAGE_URL, token),
@@ -777,7 +738,7 @@ async function cursorBillingCore(account) {
777
738
  ]);
778
739
  if (!usage || usage.__status) {
779
740
  const expired = usage?.__status === 401 || usage?.__status === 403;
780
- return { plan: planFallback, metrics: [], error: expired ? "Token expired \u2014 re-open Cursor" : "Cursor API error" };
741
+ return { plan: planFallback, metrics: [], error: expired ? "Token expired \u2014 re-open Cursor" : "Cursor API error", ...identityFields(email, displayName) };
781
742
  }
782
743
  const planName = planInfo?.planInfo?.planName ?? planFallback;
783
744
  const price = planInfo?.planInfo?.price;
@@ -786,50 +747,49 @@ async function cursorBillingCore(account) {
786
747
  const metrics = [];
787
748
  const rawEnd = usage.billingCycleEnd;
788
749
  const endMs = typeof rawEnd === "string" && rawEnd.trim() ? Number(rawEnd) : NaN;
789
- const resets = Number.isFinite(endMs) && endMs > 0 && endMs <= 864e13 ? resetIn(new Date(endMs).toISOString()) : null;
790
- if (typeof pu.totalPercentUsed === "number" && typeof pu.limit === "number") {
791
- metrics.push({
792
- label: "Usage",
793
- used: pu.totalPercentUsed,
794
- limit: 100,
795
- format: { kind: "percent" },
796
- resetsAt: resets,
797
- primary: true
798
- });
799
- const spentCents = typeof pu.totalSpend === "number" ? pu.totalSpend : pu.limit - (pu.remaining ?? 0);
800
- metrics.push({
801
- label: "Spend",
802
- used: dollars(spentCents),
803
- limit: dollars(pu.limit),
804
- format: { kind: "dollars" }
805
- });
750
+ const iso = msToIso(endMs);
751
+ const resets = iso && endMs > 0 ? resetIn(iso) : null;
752
+ if (finiteNumber(pu.totalPercentUsed) && finiteNumber(pu.limit)) {
753
+ metrics.push(percentMetric("Usage", pu.totalPercentUsed, resets, true));
754
+ const spentCents = finiteNumber(pu.totalSpend) ? pu.totalSpend : finiteNumber(pu.remaining) ? pu.limit - pu.remaining : pu.limit * (pu.totalPercentUsed / 100);
755
+ if (Number.isFinite(spentCents)) {
756
+ metrics.push({
757
+ label: "Spend",
758
+ used: dollars(spentCents),
759
+ limit: dollars(pu.limit),
760
+ format: { kind: "dollars" }
761
+ });
762
+ }
806
763
  }
807
- if (pu.autoPercentUsed) {
764
+ if (typeof pu.autoPercentUsed === "number" && Number.isFinite(pu.autoPercentUsed)) {
808
765
  metrics.push({ label: "Auto", used: pu.autoPercentUsed, limit: 100, format: { kind: "percent" } });
809
766
  }
810
- if (pu.apiPercentUsed) {
767
+ if (typeof pu.apiPercentUsed === "number" && Number.isFinite(pu.apiPercentUsed)) {
811
768
  metrics.push({ label: "API", used: pu.apiPercentUsed, limit: 100, format: { kind: "percent" } });
812
769
  }
813
770
  const su = usage.spendLimitUsage;
814
771
  if (su) {
815
- const limitCents = su.individualLimit ?? su.pooledLimit ?? 0;
816
- const remainingCents = su.individualRemaining ?? su.pooledRemaining ?? 0;
817
- if (limitCents > 0) {
772
+ const pair = finiteNumber(su.individualLimit) && finiteNumber(su.individualRemaining) ? { limit: su.individualLimit, remaining: su.individualRemaining } : finiteNumber(su.pooledLimit) && finiteNumber(su.pooledRemaining) ? { limit: su.pooledLimit, remaining: su.pooledRemaining } : null;
773
+ if (pair && pair.limit > 0) {
818
774
  metrics.push({
819
775
  label: "On-demand",
820
- used: dollars(limitCents - remainingCents),
821
- limit: dollars(limitCents),
776
+ used: dollars(pair.limit - pair.remaining),
777
+ limit: dollars(pair.limit),
822
778
  format: { kind: "dollars" }
823
779
  });
824
780
  }
825
781
  }
826
782
  if (metrics.length === 0) {
827
- return { plan, metrics: [], error: usage.enabled === false ? "No active subscription" : "No usage data" };
783
+ return { plan, metrics: [], error: usage.enabled === false ? "No active subscription" : "No usage data", ...identityFields(email, displayName) };
828
784
  }
829
- return { plan, metrics, error: null };
785
+ return { plan, metrics, error: null, ...identityFields(email, displayName) };
830
786
  }
831
787
 
832
788
  // src/providers/cursor/composer.ts
789
+ var finiteNonNegative = (value) => {
790
+ const n = Number(value);
791
+ return Number.isFinite(n) && n > 0 ? n : 0;
792
+ };
833
793
  async function cursorModelSpend(homeDir) {
834
794
  const db = cursorStateDb(homeDir);
835
795
  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;";
@@ -838,9 +798,9 @@ async function cursorModelSpend(homeDir) {
838
798
  const models = [];
839
799
  let total = 0;
840
800
  for (const row of res.rows) {
841
- const usd = (Number(row.cents) || 0) / 100;
801
+ const usd = finiteNonNegative(row.cents) / 100;
842
802
  if (usd <= 0) continue;
843
- models.push({ name: String(row.name ?? ""), usd, requests: Number(row.amt) || 0 });
803
+ models.push({ name: String(row.name ?? ""), usd, requests: finiteNonNegative(row.amt) });
844
804
  total += usd;
845
805
  }
846
806
  if (total <= 0) return null;
@@ -870,8 +830,8 @@ async function cursorUsageTable(tz, homeDir) {
870
830
  for (const r of res.rows) {
871
831
  const ts = Number(r.createdAt);
872
832
  if (!Number.isFinite(ts) || ts <= 0) continue;
873
- const usd = (Number(r.cents) || 0) / 100;
874
- const reqs = Number(r.amt) || 0;
833
+ const usd = finiteNonNegative(r.cents) / 100;
834
+ const reqs = finiteNonNegative(r.amt);
875
835
  if (usd <= 0 && reqs <= 0) continue;
876
836
  const model = String(r.model ?? "unknown");
877
837
  put(buckets.daily, dayKey(ts, tz), model, usd, reqs);
@@ -891,8 +851,8 @@ async function cursorUsageTable(tz, homeDir) {
891
851
  import { readdir, stat as fsStat, access as access2 } from "fs/promises";
892
852
  import { createReadStream } from "fs";
893
853
  import { createInterface } from "readline";
894
- import { join as join5, isAbsolute as isAbsolute2 } from "path";
895
- import { homedir as homedir4 } from "os";
854
+ import { join as join4, isAbsolute } from "path";
855
+ import { homedir as homedir3 } from "os";
896
856
  var PRICING = {
897
857
  "claude-opus-4-1": { i: 15e-6, o: 75e-6, cc: 1875e-8, cr: 15e-7 },
898
858
  "claude-opus-4-0": { i: 15e-6, o: 75e-6, cc: 1875e-8, cr: 15e-7 },
@@ -904,31 +864,31 @@ var PRICING = {
904
864
  "claude-fable-5": { i: 1e-5, o: 5e-5, cc: 125e-7, cr: 1e-6 }
905
865
  };
906
866
  var PRICE_KEYS = Object.keys(PRICING).sort((a, b) => b.length - a.length);
907
- var FALLBACK = PRICING["claude-opus-4"];
867
+ var ZERO_PRICE = { i: 0, o: 0, cc: 0, cr: 0 };
908
868
  function claudeConfigDirs(homeDir) {
909
869
  if (homeDir) {
910
- return [join5(homeDir, ".claude"), join5(homeDir, ".config", "claude")];
870
+ return [join4(homeDir, ".claude"), join4(homeDir, ".config", "claude")];
911
871
  }
912
- const home = homedir4();
913
- const dirs = [join5(home, ".claude")];
872
+ const home = homedir3();
873
+ const dirs = [join4(home, ".claude")];
914
874
  const xdg = envDir("XDG_CONFIG_HOME");
915
875
  if (xdg) {
916
- dirs.push(join5(xdg, "claude"));
876
+ dirs.push(join4(xdg, "claude"));
917
877
  } else if (process.platform !== "win32") {
918
- dirs.push(join5(home, ".config", "claude"));
878
+ dirs.push(join4(home, ".config", "claude"));
919
879
  }
920
880
  const appData = envDir("APPDATA");
921
- if (appData) dirs.push(join5(appData, "claude"));
881
+ if (appData) dirs.push(join4(appData, "claude"));
922
882
  if (process.env.CLAUDE_CONFIG_DIR) {
923
883
  for (const p of process.env.CLAUDE_CONFIG_DIR.split(process.platform === "win32" ? ";" : ",")) {
924
884
  const t = p.trim();
925
- if (t && isAbsolute2(t)) dirs.push(t);
885
+ if (t && isAbsolute(t)) dirs.push(t);
926
886
  }
927
887
  }
928
888
  return [...new Set(dirs)];
929
889
  }
930
890
  function getClaudeDirs(homeDir) {
931
- return claudeConfigDirs(homeDir).map((d) => join5(d, "projects"));
891
+ return claudeConfigDirs(homeDir).map((d) => join4(d, "projects"));
932
892
  }
933
893
  async function detectClaude(homeDir) {
934
894
  for (const dir of getClaudeDirs(homeDir)) {
@@ -941,12 +901,13 @@ async function detectClaude(homeDir) {
941
901
  return false;
942
902
  }
943
903
  function priceFor(model) {
904
+ const m = model.toLowerCase().trim();
944
905
  for (const key of PRICE_KEYS) {
945
- if (!model.startsWith(key)) continue;
946
- const rest = model.slice(key.length);
906
+ if (!m.startsWith(key)) continue;
907
+ const rest = m.slice(key.length);
947
908
  if (rest === "" || rest[0] === "-") return PRICING[key];
948
909
  }
949
- return FALLBACK;
910
+ return ZERO_PRICE;
950
911
  }
951
912
  function costOf(model, u) {
952
913
  const p = priceFor(model);
@@ -1003,7 +964,7 @@ async function loadEntries(since, homeDir) {
1003
964
  }
1004
965
  for (const f of listing) {
1005
966
  if (!f.endsWith(".jsonl")) continue;
1006
- const path = join5(dir, f);
967
+ const path = join4(dir, f);
1007
968
  if (seen.has(path)) continue;
1008
969
  seen.add(path);
1009
970
  try {
@@ -1022,23 +983,74 @@ async function loadEntries(since, homeDir) {
1022
983
  return loadCachedEntries(files, parseFile, since);
1023
984
  }
1024
985
  async function claudeDashboard(tz, homeDir) {
1025
- const now = Date.now();
1026
- const since = Math.min(startOfMonth(now, tz), startOfWeek(now, tz), now - SPARK_DAYS * 864e5);
1027
- const entries = await loadEntries(since, homeDir);
986
+ const entries = await loadEntries(dashboardSince(tz), homeDir);
1028
987
  return summarize(entries, tz);
1029
988
  }
1030
989
  async function claudeTable(tz, homeDir) {
1031
- const entries = await loadEntries(monthsAgoStart(Date.now(), 6, tz), homeDir);
990
+ const entries = await loadEntries(tableSince(tz), homeDir);
1032
991
  return tabulate(entries, tz);
1033
992
  }
1034
993
 
1035
994
  // src/providers/claude/billing.ts
995
+ import { readFile as readFile2 } from "fs/promises";
996
+ import { join as join5 } from "path";
997
+ import { homedir as homedir4 } from "os";
998
+
999
+ // src/providers/_shared/keychain.ts
1036
1000
  import { execFile as execFileCb2 } from "child_process";
1037
- import { readFile as readFile3 } from "fs/promises";
1038
- import { join as join6 } from "path";
1039
- import { homedir as homedir5 } from "os";
1040
1001
  import { promisify as promisify2 } from "util";
1041
1002
  var execFile2 = promisify2(execFileCb2);
1003
+ var GO_KEYRING_PREFIX = "go-keyring-base64:";
1004
+ async function readMacKeychainRaw(service) {
1005
+ if (process.platform !== "darwin") return null;
1006
+ try {
1007
+ const { stdout } = await execFile2("security", [
1008
+ "find-generic-password",
1009
+ "-s",
1010
+ service,
1011
+ "-w"
1012
+ ], { timeout: 5e3 });
1013
+ const raw = stdout.trim();
1014
+ return raw || null;
1015
+ } catch {
1016
+ return null;
1017
+ }
1018
+ }
1019
+ function unwrapGoKeyringBase64(raw) {
1020
+ if (!raw.startsWith(GO_KEYRING_PREFIX)) return raw;
1021
+ return Buffer.from(raw.slice(GO_KEYRING_PREFIX.length), "base64").toString("utf-8");
1022
+ }
1023
+
1024
+ // src/providers/claude/billing.ts
1025
+ function titleWords(value) {
1026
+ return value.split(/[_\s-]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()).join(" ");
1027
+ }
1028
+ function claudeOrgPlanLabel(orgType) {
1029
+ if (typeof orgType !== "string" || !orgType.trim()) return null;
1030
+ const normalized = orgType.trim().toLowerCase();
1031
+ const stripped = normalized.startsWith("claude_") ? normalized.slice("claude_".length) : normalized;
1032
+ const label = titleWords(stripped);
1033
+ return label ? `Claude ${label}` : null;
1034
+ }
1035
+ async function readClaudeIdentity(homeDir) {
1036
+ const base = homeDir ? expandHome(homeDir) : homedir4();
1037
+ try {
1038
+ const parsed = JSON.parse(await readFile2(join5(base, ".claude.json"), "utf-8"));
1039
+ const oauth = parsed?.oauthAccount;
1040
+ const email = typeof oauth?.emailAddress === "string" && oauth.emailAddress.trim() ? oauth.emailAddress.trim() : void 0;
1041
+ const displayName = typeof oauth?.displayName === "string" && oauth.displayName.trim() ? oauth.displayName.trim() : void 0;
1042
+ const plan = claudeOrgPlanLabel(parsed?.organizationType);
1043
+ return { email, displayName, plan: plan ?? void 0 };
1044
+ } catch {
1045
+ return {};
1046
+ }
1047
+ }
1048
+ function identityFields2(identity) {
1049
+ return {
1050
+ email: identity.email ?? null,
1051
+ displayName: identity.displayName ?? null
1052
+ };
1053
+ }
1042
1054
  function parseAuth(raw) {
1043
1055
  try {
1044
1056
  const creds = JSON.parse(raw);
@@ -1057,7 +1069,7 @@ function parseAuth(raw) {
1057
1069
  async function readCredentialsFile(homeDir) {
1058
1070
  for (const dir of claudeConfigDirs(homeDir)) {
1059
1071
  try {
1060
- const auth = parseAuth(await readFile3(join6(dir, ".credentials.json"), "utf-8"));
1072
+ const auth = parseAuth(await readFile2(join5(dir, ".credentials.json"), "utf-8"));
1061
1073
  if (auth) return auth;
1062
1074
  } catch {
1063
1075
  }
@@ -1065,25 +1077,23 @@ async function readCredentialsFile(homeDir) {
1065
1077
  return null;
1066
1078
  }
1067
1079
  async function readMacKeychain() {
1068
- try {
1069
- const { stdout } = await execFile2("security", [
1070
- "find-generic-password",
1071
- "-s",
1072
- "Claude Code-credentials",
1073
- "-w"
1074
- ], { timeout: 5e3 });
1075
- return parseAuth(stdout.trim());
1076
- } catch {
1077
- return null;
1078
- }
1080
+ const raw = await readMacKeychainRaw("Claude Code-credentials");
1081
+ return raw ? parseAuth(raw) : null;
1079
1082
  }
1080
1083
  async function getAuth(homeDir) {
1081
- const isDefault = !homeDir || homeDir === homedir5();
1082
- if (isDefault && process.platform === "darwin") {
1083
- const auth = await readMacKeychain();
1084
- if (auth) return auth;
1084
+ const expandedHomeDir = homeDir ? expandHome(homeDir) : void 0;
1085
+ const isDefault = !expandedHomeDir || expandedHomeDir === homedir4();
1086
+ if (isDefault) {
1087
+ if (process.platform === "darwin") {
1088
+ const auth = await readMacKeychain();
1089
+ if (auth) return auth;
1090
+ }
1091
+ return readCredentialsFile(void 0);
1085
1092
  }
1086
- return readCredentialsFile(homeDir);
1093
+ const fileAuth = await readCredentialsFile(expandedHomeDir);
1094
+ if (fileAuth) return fileAuth;
1095
+ if (process.platform === "darwin") return readMacKeychain();
1096
+ return null;
1087
1097
  }
1088
1098
  function planLabel(auth) {
1089
1099
  const sub = auth.subscriptionType;
@@ -1092,11 +1102,19 @@ function planLabel(auth) {
1092
1102
  const tier = (auth.rateLimitTier ?? "").match(/(\d+)x/);
1093
1103
  return tier ? `${base} ${tier[1]}x` : base;
1094
1104
  }
1095
- var pct = (used, resets, primary) => ({ label: "", used, limit: 100, format: { kind: "percent" }, resetsAt: resets ?? null, primary });
1105
+ var pct = (used, resets, primary) => percentMetric("", used, resets ?? null, primary);
1106
+ function usageMetric(label, window, primary) {
1107
+ if (!window || typeof window.utilization !== "number" || !Number.isFinite(window.utilization)) return null;
1108
+ const resets = typeof window.resets_at === "string" && window.resets_at.trim() ? resetIn(window.resets_at) : null;
1109
+ return { ...pct(window.utilization, resets, primary), label };
1110
+ }
1096
1111
  async function claudeBilling(account) {
1097
- const auth = await getAuth(account.homeDir);
1098
- if (!auth) return { plan: null, metrics: [], error: "No OAuth token \u2014 run claude and log in" };
1099
- const plan = planLabel(auth);
1112
+ const [auth, identity] = await Promise.all([
1113
+ getAuth(account.homeDir),
1114
+ readClaudeIdentity(account.homeDir)
1115
+ ]);
1116
+ if (!auth) return { plan: identity.plan ?? null, metrics: [], error: "No OAuth token \u2014 run claude and log in", ...identityFields2(identity) };
1117
+ const plan = identity.plan ?? planLabel(auth);
1100
1118
  try {
1101
1119
  const res = await fetch("https://api.anthropic.com/api/oauth/usage", {
1102
1120
  headers: {
@@ -1106,32 +1124,30 @@ async function claudeBilling(account) {
1106
1124
  },
1107
1125
  signal: AbortSignal.timeout(1e4)
1108
1126
  });
1109
- if (res.status === 429) return { plan, metrics: [], error: "Rate limited \u2014 retrying next poll" };
1110
- if (res.status === 401) return { plan, metrics: [], error: "Token expired \u2014 restart Claude Code" };
1111
- if (!res.ok) return { plan, metrics: [], error: `API ${res.status}` };
1127
+ if (res.status === 429) return { plan, metrics: [], error: "Rate limited \u2014 retrying next poll", ...identityFields2(identity) };
1128
+ if (res.status === 401) return { plan, metrics: [], error: "Token expired \u2014 restart Claude Code", ...identityFields2(identity) };
1129
+ if (!res.ok) return { plan, metrics: [], error: `API ${res.status}`, ...identityFields2(identity) };
1112
1130
  const data = await readJson(res);
1113
- if (!data) return { plan, metrics: [], error: "Unexpected API response" };
1131
+ if (!data) return { plan, metrics: [], error: "Unexpected API response", ...identityFields2(identity) };
1114
1132
  const metrics = [];
1115
- if (data.five_hour) {
1116
- metrics.push({ ...pct(data.five_hour.utilization, resetIn(data.five_hour.resets_at), true), label: "5h" });
1117
- }
1118
- if (data.seven_day) {
1119
- metrics.push({ ...pct(data.seven_day.utilization, resetIn(data.seven_day.resets_at)), label: "Week" });
1120
- }
1121
- if (data.seven_day_sonnet) {
1122
- metrics.push({ ...pct(data.seven_day_sonnet.utilization), label: "Sonnet" });
1123
- }
1133
+ const fiveHour = usageMetric("5h", data.five_hour, true);
1134
+ if (fiveHour) metrics.push(fiveHour);
1135
+ const sevenDay = usageMetric("Week", data.seven_day);
1136
+ if (sevenDay) metrics.push(sevenDay);
1137
+ const sevenDaySonnet = usageMetric("Sonnet", data.seven_day_sonnet);
1138
+ if (sevenDaySonnet) metrics.push(sevenDaySonnet);
1124
1139
  if (data.extra_usage?.is_enabled) {
1140
+ const monthlyLimit = data.extra_usage.monthly_limit;
1125
1141
  metrics.push({
1126
1142
  label: "Extra",
1127
- used: (data.extra_usage.used_credits ?? 0) / 100,
1128
- limit: data.extra_usage.monthly_limit != null ? data.extra_usage.monthly_limit / 100 : null,
1143
+ used: finite(data.extra_usage.used_credits) / 100,
1144
+ limit: typeof monthlyLimit === "number" && Number.isFinite(monthlyLimit) ? monthlyLimit / 100 : null,
1129
1145
  format: { kind: "dollars", currency: data.extra_usage.currency ?? "USD" }
1130
1146
  });
1131
1147
  }
1132
- return { plan, metrics, error: null };
1148
+ return { plan, metrics, error: null, ...identityFields2(identity) };
1133
1149
  } catch {
1134
- return { plan, metrics: [], error: "Network error" };
1150
+ return { plan, metrics: [], error: "Network error", ...identityFields2(identity) };
1135
1151
  }
1136
1152
  }
1137
1153
 
@@ -1152,8 +1168,8 @@ var claudeProvider = {
1152
1168
  import { readdir as readdir2, stat as fsStat2, access as access3 } from "fs/promises";
1153
1169
  import { createReadStream as createReadStream2 } from "fs";
1154
1170
  import { createInterface as createInterface2 } from "readline";
1155
- import { join as join7 } from "path";
1156
- import { homedir as homedir6 } from "os";
1171
+ import { join as join6 } from "path";
1172
+ import { homedir as homedir5 } from "os";
1157
1173
  var PRICING2 = {
1158
1174
  "gpt-5-codex": { in: 125e-8, cr: 125e-9, out: 1e-5 },
1159
1175
  "gpt-5-mini": { in: 25e-8, cr: 25e-9, out: 2e-6 },
@@ -1161,33 +1177,45 @@ var PRICING2 = {
1161
1177
  "gpt-5": { in: 125e-8, cr: 125e-9, out: 1e-5 },
1162
1178
  "o4-mini": { in: 11e-7, cr: 275e-9, out: 44e-7 }
1163
1179
  };
1164
- var FALLBACK2 = PRICING2["gpt-5-codex"];
1180
+ var ZERO_PRICE2 = { in: 0, cr: 0, out: 0 };
1165
1181
  var PRICE_KEYS2 = Object.keys(PRICING2).sort((a, b) => b.length - a.length);
1166
1182
  function codexHomes(homeDir) {
1167
- if (homeDir) return [join7(homeDir, ".codex")];
1183
+ if (homeDir) return [join6(homeDir, ".codex")];
1168
1184
  const homes = [];
1169
1185
  const codexHome = envDir("CODEX_HOME");
1170
1186
  if (codexHome) homes.push(codexHome);
1171
- homes.push(join7(homedir6(), ".codex"));
1172
- homes.push(join7(homedir6(), ".config", "codex"));
1187
+ homes.push(join6(homedir5(), ".codex"));
1188
+ homes.push(join6(homedir5(), ".config", "codex"));
1173
1189
  return [...new Set(homes)];
1174
1190
  }
1175
1191
  async function detectCodex(homeDir) {
1176
1192
  for (const home of codexHomes(homeDir)) {
1177
1193
  try {
1178
- await access3(join7(home, "sessions"));
1194
+ await access3(join6(home, "sessions"));
1179
1195
  return true;
1180
1196
  } catch {
1181
1197
  }
1182
1198
  }
1183
1199
  return false;
1184
1200
  }
1201
+ function modelKeyMatches(model, key) {
1202
+ let idx = model.indexOf(key);
1203
+ while (idx >= 0) {
1204
+ const before = idx === 0 ? "" : model[idx - 1];
1205
+ const rest = model.slice(idx + key.length);
1206
+ if ((!before || !/[a-z0-9-]/.test(before)) && (rest === "" || rest[0] === "-" || !/[a-z0-9]/.test(rest[0]))) {
1207
+ return true;
1208
+ }
1209
+ idx = model.indexOf(key, idx + key.length);
1210
+ }
1211
+ return false;
1212
+ }
1185
1213
  function priceFor2(model) {
1186
- const m = model.toLowerCase();
1214
+ const m = model.toLowerCase().trim();
1187
1215
  for (const key of PRICE_KEYS2) {
1188
- if (m.startsWith(key) || m.includes(key)) return PRICING2[key];
1216
+ if (modelKeyMatches(m, key)) return PRICING2[key];
1189
1217
  }
1190
- return FALLBACK2;
1218
+ return ZERO_PRICE2;
1191
1219
  }
1192
1220
  function extractModel(obj) {
1193
1221
  const p = obj?.payload ?? obj;
@@ -1208,7 +1236,7 @@ function eventSig(last, total) {
1208
1236
  }
1209
1237
  async function parseFile2(path) {
1210
1238
  const entries = [];
1211
- let model = "gpt-5-codex";
1239
+ let model = "unknown";
1212
1240
  let prevTotal = null;
1213
1241
  let prevSig = null;
1214
1242
  const rl = createInterface2({ input: createReadStream2(path), crlfDelay: Infinity });
@@ -1265,7 +1293,7 @@ async function loadEntries2(since, homeDir) {
1265
1293
  const seen = /* @__PURE__ */ new Set();
1266
1294
  const seenIno = /* @__PURE__ */ new Set();
1267
1295
  for (const home of codexHomes(homeDir)) {
1268
- const dir = join7(home, "sessions");
1296
+ const dir = join6(home, "sessions");
1269
1297
  let listing;
1270
1298
  try {
1271
1299
  listing = await readdir2(dir, { recursive: true });
@@ -1274,7 +1302,7 @@ async function loadEntries2(since, homeDir) {
1274
1302
  }
1275
1303
  for (const f of listing) {
1276
1304
  if (!f.endsWith(".jsonl") || !f.includes("rollout-")) continue;
1277
- const path = join7(dir, f);
1305
+ const path = join6(dir, f);
1278
1306
  if (seen.has(path)) continue;
1279
1307
  seen.add(path);
1280
1308
  try {
@@ -1293,49 +1321,78 @@ async function loadEntries2(since, homeDir) {
1293
1321
  return loadCachedEntries(files, parseFile2, since);
1294
1322
  }
1295
1323
  async function codexDashboard(tz, homeDir) {
1296
- const now = Date.now();
1297
- const since = Math.min(startOfMonth(now, tz), startOfWeek(now, tz), now - SPARK_DAYS * 864e5);
1298
- const entries = await loadEntries2(since, homeDir);
1324
+ const entries = await loadEntries2(dashboardSince(tz), homeDir);
1299
1325
  return summarize(entries, tz);
1300
1326
  }
1301
1327
  async function codexTable(tz, homeDir) {
1302
- const entries = await loadEntries2(monthsAgoStart(Date.now(), 6, tz), homeDir);
1328
+ const entries = await loadEntries2(tableSince(tz), homeDir);
1303
1329
  return tabulate(entries, tz);
1304
1330
  }
1305
1331
 
1306
1332
  // src/providers/codex/billing.ts
1307
- import { execFile as execFileCb3 } from "child_process";
1308
- import { readFile as readFile4, readdir as readdir3, stat as fsStat3 } from "fs/promises";
1333
+ import { readFile as readFile3, readdir as readdir3, stat as fsStat3 } from "fs/promises";
1309
1334
  import { createReadStream as createReadStream3 } from "fs";
1310
1335
  import { createInterface as createInterface3 } from "readline";
1311
- import { join as join8 } from "path";
1312
- import { promisify as promisify3 } from "util";
1313
- var execFile3 = promisify3(execFileCb3);
1336
+ import { join as join7 } from "path";
1314
1337
  var USAGE_URL2 = "https://chatgpt.com/backend-api/wham/usage";
1315
1338
  var CREDIT_USD_RATE = 0.04;
1339
+ function decodeBase64UrlJson(segment) {
1340
+ try {
1341
+ const normalized = segment.replace(/-/g, "+").replace(/_/g, "/");
1342
+ const padded = normalized + "=".repeat((4 - normalized.length % 4) % 4);
1343
+ return JSON.parse(Buffer.from(padded, "base64").toString("utf8"));
1344
+ } catch {
1345
+ return null;
1346
+ }
1347
+ }
1348
+ function chatGptPlanLabel(planType) {
1349
+ if (typeof planType !== "string" || !planType.trim()) return null;
1350
+ const p = planType.trim().toLowerCase();
1351
+ const labels = {
1352
+ free: "ChatGPT Free",
1353
+ plus: "ChatGPT Plus",
1354
+ pro: "ChatGPT Pro",
1355
+ team: "ChatGPT Team",
1356
+ enterprise: "ChatGPT Enterprise",
1357
+ edu: "ChatGPT Edu"
1358
+ };
1359
+ if (labels[p]) return labels[p];
1360
+ return `ChatGPT ${p.charAt(0).toUpperCase()}${p.slice(1)}`;
1361
+ }
1362
+ function identityFromIdToken(idToken) {
1363
+ if (typeof idToken !== "string" || !idToken.includes(".")) return {};
1364
+ const payload = decodeBase64UrlJson(idToken.split(".")[1]);
1365
+ if (!payload || typeof payload !== "object") return {};
1366
+ const email = typeof payload.email === "string" && payload.email.trim() ? payload.email.trim() : void 0;
1367
+ 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;
1368
+ const plan = chatGptPlanLabel(payload["https://api.openai.com/auth"]?.chatgpt_plan_type);
1369
+ return { email, displayName, plan: plan ?? void 0 };
1370
+ }
1371
+ function identityFields3(auth) {
1372
+ return {
1373
+ email: auth?.email ?? null,
1374
+ displayName: auth?.displayName ?? null
1375
+ };
1376
+ }
1316
1377
  async function readAuthFile(home) {
1317
1378
  try {
1318
- const raw = await readFile4(join8(home, "auth.json"), "utf-8");
1379
+ const raw = await readFile3(join7(home, "auth.json"), "utf-8");
1319
1380
  const auth = JSON.parse(raw);
1320
1381
  const accessToken = auth?.tokens?.access_token;
1321
1382
  if (!accessToken) return null;
1322
- return { accessToken, accountId: auth?.tokens?.account_id };
1383
+ return { accessToken, accountId: auth?.tokens?.account_id, ...identityFromIdToken(auth?.tokens?.id_token) };
1323
1384
  } catch {
1324
1385
  return null;
1325
1386
  }
1326
1387
  }
1327
1388
  async function readKeychainAuth() {
1328
1389
  try {
1329
- const { stdout } = await execFile3("security", [
1330
- "find-generic-password",
1331
- "-s",
1332
- "Codex Auth",
1333
- "-w"
1334
- ], { timeout: 5e3 });
1335
- const auth = JSON.parse(stdout.trim());
1390
+ const raw = await readMacKeychainRaw("Codex Auth");
1391
+ if (!raw) return null;
1392
+ const auth = JSON.parse(raw);
1336
1393
  const accessToken = auth?.tokens?.access_token;
1337
1394
  if (!accessToken) return null;
1338
- return { accessToken, accountId: auth?.tokens?.account_id };
1395
+ return { accessToken, accountId: auth?.tokens?.account_id, ...identityFromIdToken(auth?.tokens?.id_token) };
1339
1396
  } catch {
1340
1397
  return null;
1341
1398
  }
@@ -1355,20 +1412,14 @@ function planLabel2(planType) {
1355
1412
  if (p === "pro") return "Pro 20x";
1356
1413
  return planType.charAt(0).toUpperCase() + planType.slice(1);
1357
1414
  }
1358
- function isoOrNull(ms) {
1359
- return Number.isFinite(ms) && Math.abs(ms) <= 864e13 ? new Date(ms).toISOString() : null;
1360
- }
1361
1415
  function resetFrom(window) {
1362
1416
  if (!window) return null;
1363
1417
  let iso = null;
1364
- if (typeof window.reset_at === "number") iso = isoOrNull(window.reset_at * 1e3);
1365
- else if (typeof window.resets_at === "number") iso = isoOrNull(window.resets_at * 1e3);
1366
- else if (typeof window.reset_after_seconds === "number") iso = isoOrNull(Date.now() + window.reset_after_seconds * 1e3);
1418
+ if (typeof window.reset_at === "number") iso = msToIso(window.reset_at * 1e3);
1419
+ else if (typeof window.resets_at === "number") iso = msToIso(window.resets_at * 1e3);
1420
+ else if (typeof window.reset_after_seconds === "number") iso = msToIso(Date.now() + window.reset_after_seconds * 1e3);
1367
1421
  return iso ? resetIn(iso) : null;
1368
1422
  }
1369
- function percentMetric(label, used, resets, primary) {
1370
- return { label, used, limit: 100, format: { kind: "percent" }, resetsAt: resets, primary };
1371
- }
1372
1423
  async function liveBilling(auth) {
1373
1424
  try {
1374
1425
  const headers = {
@@ -1393,14 +1444,14 @@ async function liveBilling(auth) {
1393
1444
  };
1394
1445
  const primaryPct = headerPct("x-codex-primary-used-percent") ?? primary?.used_percent;
1395
1446
  const secondaryPct = headerPct("x-codex-secondary-used-percent") ?? secondary?.used_percent;
1396
- if (typeof primaryPct === "number") metrics.push(percentMetric("5h", primaryPct, resetFrom(primary), true));
1397
- if (typeof secondaryPct === "number") metrics.push(percentMetric("Week", secondaryPct, resetFrom(secondary)));
1447
+ if (typeof primaryPct === "number" && Number.isFinite(primaryPct)) metrics.push(percentMetric("5h", primaryPct, resetFrom(primary), true));
1448
+ if (typeof secondaryPct === "number" && Number.isFinite(secondaryPct)) metrics.push(percentMetric("Week", secondaryPct, resetFrom(secondary)));
1398
1449
  const balance = data?.credits?.balance;
1399
- if (typeof balance === "number" && balance >= 0) {
1450
+ if (typeof balance === "number" && Number.isFinite(balance) && balance >= 0) {
1400
1451
  metrics.push({ label: "Credits", used: balance * CREDIT_USD_RATE, limit: null, format: { kind: "dollars" } });
1401
1452
  }
1402
1453
  if (metrics.length === 0) return null;
1403
- return { plan: planLabel2(data.plan_type), metrics, error: null };
1454
+ return { plan: auth.plan ?? planLabel2(data.plan_type), metrics, error: null, ...identityFields3(auth) };
1404
1455
  } catch {
1405
1456
  return null;
1406
1457
  }
@@ -1408,7 +1459,7 @@ async function liveBilling(auth) {
1408
1459
  async function newestRolloutFile(homeDir) {
1409
1460
  let best = null;
1410
1461
  for (const home of codexHomes(homeDir)) {
1411
- const dir = join8(home, "sessions");
1462
+ const dir = join7(home, "sessions");
1412
1463
  let listing;
1413
1464
  try {
1414
1465
  listing = await readdir3(dir, { recursive: true });
@@ -1417,7 +1468,7 @@ async function newestRolloutFile(homeDir) {
1417
1468
  }
1418
1469
  for (const f of listing) {
1419
1470
  if (!f.endsWith(".jsonl") || !f.includes("rollout-")) continue;
1420
- const path = join8(dir, f);
1471
+ const path = join7(dir, f);
1421
1472
  try {
1422
1473
  const s = await fsStat3(path);
1423
1474
  if (!best || s.mtimeMs > best.mtime) best = { path, mtime: s.mtimeMs };
@@ -1427,7 +1478,7 @@ async function newestRolloutFile(homeDir) {
1427
1478
  }
1428
1479
  return best?.path ?? null;
1429
1480
  }
1430
- async function snapshotBilling(homeDir) {
1481
+ async function snapshotBilling(homeDir, auth = null) {
1431
1482
  const path = await newestRolloutFile(homeDir);
1432
1483
  if (!path) return null;
1433
1484
  let last = null;
@@ -1446,18 +1497,18 @@ async function snapshotBilling(homeDir) {
1446
1497
  }
1447
1498
  if (!last) return null;
1448
1499
  const metrics = [];
1449
- if (typeof last.primary?.used_percent === "number") {
1500
+ if (typeof last.primary?.used_percent === "number" && Number.isFinite(last.primary.used_percent)) {
1450
1501
  metrics.push(percentMetric("5h", last.primary.used_percent, resetFrom(last.primary), true));
1451
1502
  }
1452
- if (typeof last.secondary?.used_percent === "number") {
1503
+ if (typeof last.secondary?.used_percent === "number" && Number.isFinite(last.secondary.used_percent)) {
1453
1504
  metrics.push(percentMetric("Week", last.secondary.used_percent, resetFrom(last.secondary)));
1454
1505
  }
1455
1506
  const balance = last?.credits?.balance;
1456
- if (typeof balance === "number" && balance >= 0) {
1507
+ if (typeof balance === "number" && Number.isFinite(balance) && balance >= 0) {
1457
1508
  metrics.push({ label: "Credits", used: balance * CREDIT_USD_RATE, limit: null, format: { kind: "dollars" } });
1458
1509
  }
1459
1510
  if (metrics.length === 0) return null;
1460
- return { plan: planLabel2(last.plan_type), metrics, error: null };
1511
+ return { plan: auth?.plan ?? planLabel2(last.plan_type), metrics, error: null, ...identityFields3(auth) };
1461
1512
  }
1462
1513
  async function codexBilling(account) {
1463
1514
  const auth = await getAuth2(account.homeDir);
@@ -1465,12 +1516,13 @@ async function codexBilling(account) {
1465
1516
  const live = await liveBilling(auth);
1466
1517
  if (live) return live;
1467
1518
  }
1468
- const snap = await snapshotBilling(account.homeDir);
1519
+ const snap = await snapshotBilling(account.homeDir, auth);
1469
1520
  if (snap) return snap;
1470
1521
  return {
1471
- plan: null,
1522
+ plan: auth?.plan ?? null,
1472
1523
  metrics: [],
1473
- error: auth ? "Usage API failed \u2014 run codex to refresh" : "Not logged in \u2014 run codex"
1524
+ error: auth ? "Usage API failed \u2014 run codex to refresh" : "Not logged in \u2014 run codex",
1525
+ ...identityFields3(auth)
1474
1526
  };
1475
1527
  }
1476
1528
 
@@ -1595,7 +1647,9 @@ function reBucket(daily, tz, keyOf) {
1595
1647
  const out = /* @__PURE__ */ new Map();
1596
1648
  for (const day of daily) {
1597
1649
  const [y, mo, d] = day.label.split("-").map(Number);
1598
- const label = keyOf(Date.UTC(y, mo - 1, d, 12), tz);
1650
+ const ts = Date.UTC(y, mo - 1, d, 12);
1651
+ if (!Number.isFinite(ts)) continue;
1652
+ const label = keyOf(ts, tz);
1599
1653
  let row = out.get(label);
1600
1654
  if (!row) {
1601
1655
  row = { label, models: [], input: 0, output: 0, cacheCreate: 0, cacheRead: 0, cacheSavings: 0, total: 0, cost: 0, count: 0, breakdown: [] };
@@ -1641,7 +1695,6 @@ var cursorProvider = {
1641
1695
  id: "cursor",
1642
1696
  name: "Cursor",
1643
1697
  color: "magenta",
1644
- // hasUsage: false — TUI keys its dedicated Cursor spend-table off this flag directly.
1645
1698
  hasUsage: false,
1646
1699
  hasBilling: true,
1647
1700
  detect: (homeDir) => detectCursor(homeDir),
@@ -1653,10 +1706,10 @@ var cursorProvider = {
1653
1706
  import { readdir as readdir4, stat as fsStat4, access as access4 } from "fs/promises";
1654
1707
  import { createReadStream as createReadStream4 } from "fs";
1655
1708
  import { createInterface as createInterface4 } from "readline";
1656
- import { join as join9 } from "path";
1657
- import { homedir as homedir7 } from "os";
1709
+ import { join as join8 } from "path";
1710
+ import { homedir as homedir6 } from "os";
1658
1711
  function piSessionsDir(homeDir) {
1659
- return join9(homeDir ?? homedir7(), ".pi", "agent", "sessions");
1712
+ return join8(homeDir ?? homedir6(), ".pi", "agent", "sessions");
1660
1713
  }
1661
1714
  async function detectPi(homeDir) {
1662
1715
  try {
@@ -1666,9 +1719,6 @@ async function detectPi(homeDir) {
1666
1719
  return false;
1667
1720
  }
1668
1721
  }
1669
- function pos(v) {
1670
- return typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0;
1671
- }
1672
1722
  async function parseFile3(path) {
1673
1723
  const entries = [];
1674
1724
  const rl = createInterface4({ input: createReadStream4(path), crlfDelay: Infinity });
@@ -1689,13 +1739,13 @@ async function parseFile3(path) {
1689
1739
  const cacheCreate = safeNum(u.cacheWrite);
1690
1740
  if (input + output + cacheRead + cacheCreate === 0) continue;
1691
1741
  const c = u.cost ?? {};
1692
- const costInput = pos(c.input);
1693
- const cacheSavings = input > 0 && cacheRead > 0 ? Math.max(0, cacheRead * (costInput / input) - pos(c.cacheRead)) : 0;
1742
+ const costInput = finitePositive(c.input);
1743
+ const cacheSavings = input > 0 && cacheRead > 0 ? Math.max(0, cacheRead * (costInput / input) - finitePositive(c.cacheRead)) : 0;
1694
1744
  const model = typeof msg.responseModel === "string" && msg.responseModel || typeof msg.model === "string" && msg.model || "unknown";
1695
1745
  entries.push({
1696
1746
  ts,
1697
1747
  model,
1698
- cost: pos(c.total),
1748
+ cost: finitePositive(c.total),
1699
1749
  input,
1700
1750
  output,
1701
1751
  cacheCreate,
@@ -1719,7 +1769,7 @@ async function loadEntries3(since, homeDir) {
1719
1769
  }
1720
1770
  for (const f of listing) {
1721
1771
  if (!f.endsWith(".jsonl")) continue;
1722
- const path = join9(dir, f);
1772
+ const path = join8(dir, f);
1723
1773
  try {
1724
1774
  const s = await fsStat4(path);
1725
1775
  if (s.mtimeMs < since) continue;
@@ -1735,12 +1785,10 @@ async function loadEntries3(since, homeDir) {
1735
1785
  return loadCachedEntries(files, parseFile3, since);
1736
1786
  }
1737
1787
  async function piDashboard(tz, homeDir) {
1738
- const now = Date.now();
1739
- const since = Math.min(startOfMonth(now, tz), startOfWeek(now, tz), now - SPARK_DAYS * 864e5);
1740
- return summarize(await loadEntries3(since, homeDir), tz);
1788
+ return summarize(await loadEntries3(dashboardSince(tz), homeDir), tz);
1741
1789
  }
1742
1790
  async function piTable(tz, homeDir) {
1743
- return tabulate(await loadEntries3(monthsAgoStart(Date.now(), 6, tz), homeDir), tz);
1791
+ return tabulate(await loadEntries3(tableSince(tz), homeDir), tz);
1744
1792
  }
1745
1793
 
1746
1794
  // src/providers/pi/index.ts
@@ -1757,17 +1805,17 @@ var piProvider = {
1757
1805
 
1758
1806
  // src/providers/opencode/usage.ts
1759
1807
  import { access as access5 } from "fs/promises";
1760
- import { join as join10 } from "path";
1761
- import { homedir as homedir8 } from "os";
1808
+ import { join as join9 } from "path";
1809
+ import { homedir as homedir7 } from "os";
1762
1810
  function opencodeDbPaths(homeDir) {
1763
- const base = homeDir ?? homedir8();
1811
+ const base = homeDir ?? homedir7();
1764
1812
  const paths = [];
1765
- if (!homeDir && process.env.XDG_DATA_HOME) paths.push(join10(process.env.XDG_DATA_HOME, "opencode", "opencode.db"));
1766
- paths.push(join10(base, ".local", "share", "opencode", "opencode.db"));
1767
- if (process.platform === "darwin") paths.push(join10(base, "Library", "Application Support", "opencode", "opencode.db"));
1813
+ if (!homeDir && process.env.XDG_DATA_HOME) paths.push(join9(process.env.XDG_DATA_HOME, "opencode", "opencode.db"));
1814
+ paths.push(join9(base, ".local", "share", "opencode", "opencode.db"));
1815
+ if (process.platform === "darwin") paths.push(join9(base, "Library", "Application Support", "opencode", "opencode.db"));
1768
1816
  if (process.platform === "win32") {
1769
- const lad = homeDir ? join10(homeDir, "AppData", "Local") : process.env.LOCALAPPDATA;
1770
- if (lad) paths.push(join10(lad, "opencode", "opencode.db"));
1817
+ const lad = homeDir ? join9(homeDir, "AppData", "Local") : process.env.LOCALAPPDATA;
1818
+ if (lad) paths.push(join9(lad, "opencode", "opencode.db"));
1771
1819
  }
1772
1820
  return [...new Set(paths)];
1773
1821
  }
@@ -1784,7 +1832,6 @@ async function findDb(homeDir) {
1784
1832
  async function detectOpencode(homeDir) {
1785
1833
  return await findDb(homeDir) !== null;
1786
1834
  }
1787
- var pos2 = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0;
1788
1835
  async function loadEntries4(since, homeDir) {
1789
1836
  const db = await findDb(homeDir);
1790
1837
  if (!db) return [];
@@ -1793,17 +1840,17 @@ async function loadEntries4(since, homeDir) {
1793
1840
  if (res.status !== "ok") return [];
1794
1841
  const entries = [];
1795
1842
  for (const row of res.rows) {
1796
- const ts = pos2(row.ts);
1843
+ const ts = finitePositive(row.ts);
1797
1844
  if (!ts) continue;
1798
- const input = pos2(row.input);
1799
- const output = pos2(row.output);
1800
- const cacheRead = pos2(row.cacheRead);
1801
- const cacheCreate = pos2(row.cacheWrite);
1845
+ const input = finitePositive(row.input);
1846
+ const output = finitePositive(row.output);
1847
+ const cacheRead = finitePositive(row.cacheRead);
1848
+ const cacheCreate = finitePositive(row.cacheWrite);
1802
1849
  if (input + output + cacheRead + cacheCreate === 0) continue;
1803
1850
  entries.push({
1804
1851
  ts,
1805
1852
  model: typeof row.model === "string" && row.model ? row.model : "unknown",
1806
- cost: pos2(row.cost),
1853
+ cost: finitePositive(row.cost),
1807
1854
  input,
1808
1855
  output,
1809
1856
  cacheCreate,
@@ -1814,12 +1861,10 @@ async function loadEntries4(since, homeDir) {
1814
1861
  return entries;
1815
1862
  }
1816
1863
  async function opencodeDashboard(tz, homeDir) {
1817
- const now = Date.now();
1818
- const since = Math.min(startOfMonth(now, tz), startOfWeek(now, tz), now - SPARK_DAYS * 864e5);
1819
- return summarize(await loadEntries4(since, homeDir), tz);
1864
+ return summarize(await loadEntries4(dashboardSince(tz), homeDir), tz);
1820
1865
  }
1821
1866
  async function opencodeTable(tz, homeDir) {
1822
- return tabulate(await loadEntries4(monthsAgoStart(Date.now(), 6, tz), homeDir), tz);
1867
+ return tabulate(await loadEntries4(tableSince(tz), homeDir), tz);
1823
1868
  }
1824
1869
 
1825
1870
  // src/providers/opencode/index.ts
@@ -1835,29 +1880,28 @@ var opencodeProvider = {
1835
1880
  };
1836
1881
 
1837
1882
  // src/providers/copilot/billing.ts
1838
- import { execFile as execFileCb4 } from "child_process";
1839
- import { access as access6, readFile as readFile5, readdir as readdir5 } from "fs/promises";
1840
- import { join as join11 } from "path";
1841
- import { homedir as homedir9 } from "os";
1842
- import { promisify as promisify4 } from "util";
1843
- var execFile4 = promisify4(execFileCb4);
1883
+ import { execFile as execFileCb3 } from "child_process";
1884
+ import { access as access6, readFile as readFile4, readdir as readdir5 } from "fs/promises";
1885
+ import { join as join10 } from "path";
1886
+ import { homedir as homedir8 } from "os";
1887
+ import { promisify as promisify3 } from "util";
1888
+ var execFile3 = promisify3(execFileCb3);
1844
1889
  var USAGE_URL3 = "https://api.github.com/copilot_internal/user";
1845
1890
  var GH_KEYCHAIN_SERVICE = "gh:github.com";
1846
- var GO_KEYRING_PREFIX = "go-keyring-base64:";
1847
1891
  function ghConfigDir(homeDir) {
1848
1892
  if (!homeDir) {
1849
1893
  const explicit = process.env.GH_CONFIG_DIR;
1850
1894
  if (explicit && explicit.trim()) return explicit.trim();
1851
1895
  if (process.platform === "win32") {
1852
- return join11(envDir("APPDATA") ?? join11(homedir9(), "AppData", "Roaming"), "GitHub CLI");
1896
+ return join10(envDir("APPDATA") ?? join10(homedir8(), "AppData", "Roaming"), "GitHub CLI");
1853
1897
  }
1854
1898
  const xdg = envDir("XDG_CONFIG_HOME");
1855
- return xdg ? join11(xdg, "gh") : join11(homedir9(), ".config", "gh");
1899
+ return xdg ? join10(xdg, "gh") : join10(homedir8(), ".config", "gh");
1856
1900
  }
1857
- return process.platform === "win32" ? join11(homeDir, "AppData", "Roaming", "GitHub CLI") : join11(homeDir, ".config", "gh");
1901
+ return process.platform === "win32" ? join10(homeDir, "AppData", "Roaming", "GitHub CLI") : join10(homeDir, ".config", "gh");
1858
1902
  }
1859
1903
  function ghHostsPath(homeDir) {
1860
- return join11(ghConfigDir(homeDir), "hosts.yml");
1904
+ return join10(ghConfigDir(homeDir), "hosts.yml");
1861
1905
  }
1862
1906
  async function detectCopilot(homeDir) {
1863
1907
  try {
@@ -1866,7 +1910,7 @@ async function detectCopilot(homeDir) {
1866
1910
  } catch {
1867
1911
  }
1868
1912
  try {
1869
- await execFile4("gh", ["--version"], { timeout: 3e3 });
1913
+ await execFile3("gh", ["--version"], { timeout: 3e3 });
1870
1914
  return true;
1871
1915
  } catch {
1872
1916
  return false;
@@ -1902,40 +1946,25 @@ function tokenFromHostsYaml(raw) {
1902
1946
  }
1903
1947
  async function loadTokenFromHosts(homeDir) {
1904
1948
  try {
1905
- const token = tokenFromHostsYaml(await readFile5(ghHostsPath(homeDir), "utf-8"));
1949
+ const token = tokenFromHostsYaml(await readFile4(ghHostsPath(homeDir), "utf-8"));
1906
1950
  return token ? { token, source: "gh-hosts" } : null;
1907
1951
  } catch {
1908
1952
  return null;
1909
1953
  }
1910
1954
  }
1911
1955
  async function readMacKeychainService(service) {
1912
- if (process.platform !== "darwin") return null;
1913
- try {
1914
- const { stdout } = await execFile4("security", [
1915
- "find-generic-password",
1916
- "-s",
1917
- service,
1918
- "-w"
1919
- ], { timeout: 5e3 });
1920
- const raw = stdout.trim();
1921
- if (!raw) return null;
1922
- if (raw.startsWith(GO_KEYRING_PREFIX)) {
1923
- return Buffer.from(raw.slice(GO_KEYRING_PREFIX.length), "base64").toString("utf-8");
1924
- }
1925
- return raw;
1926
- } catch {
1927
- return null;
1928
- }
1956
+ const raw = await readMacKeychainRaw(service);
1957
+ return raw ? unwrapGoKeyringBase64(raw) : null;
1929
1958
  }
1930
1959
  async function loadTokenFromGhKeychain() {
1931
1960
  const token = await readMacKeychainService(GH_KEYCHAIN_SERVICE);
1932
1961
  return token ? { token, source: "gh-keychain" } : null;
1933
1962
  }
1934
1963
  function vscodeUserDir(homeDir) {
1935
- const home = homeDir ?? homedir9();
1936
- if (process.platform === "darwin") return join11(home, "Library", "Application Support", "Code", "User");
1937
- if (process.platform === "win32") return join11(home, "AppData", "Roaming", "Code", "User");
1938
- return join11(home, ".config", "Code", "User");
1964
+ const home = homeDir ?? homedir8();
1965
+ if (process.platform === "darwin") return join10(home, "Library", "Application Support", "Code", "User");
1966
+ if (process.platform === "win32") return join10(home, "AppData", "Roaming", "Code", "User");
1967
+ return join10(home, ".config", "Code", "User");
1939
1968
  }
1940
1969
  function tokenFromText(raw) {
1941
1970
  const patterns = [
@@ -1952,21 +1981,21 @@ function tokenFromText(raw) {
1952
1981
  async function loadTokenFromVsCode(homeDir) {
1953
1982
  const userDir = vscodeUserDir(homeDir);
1954
1983
  const candidates = [
1955
- join11(userDir, "globalStorage", "github.copilot-chat", "auth.json"),
1956
- join11(userDir, "globalStorage", "github.copilot", "auth.json"),
1957
- join11(userDir, "globalStorage", "state.vscdb")
1984
+ join10(userDir, "globalStorage", "github.copilot-chat", "auth.json"),
1985
+ join10(userDir, "globalStorage", "github.copilot", "auth.json"),
1986
+ join10(userDir, "globalStorage", "state.vscdb")
1958
1987
  ];
1959
1988
  try {
1960
- for (const dirent of await readdir5(join11(userDir, "globalStorage"), { withFileTypes: true })) {
1989
+ for (const dirent of await readdir5(join10(userDir, "globalStorage"), { withFileTypes: true })) {
1961
1990
  if (dirent.isDirectory() && dirent.name.toLowerCase().includes("github")) {
1962
- candidates.push(join11(userDir, "globalStorage", dirent.name, "auth.json"));
1991
+ candidates.push(join10(userDir, "globalStorage", dirent.name, "auth.json"));
1963
1992
  }
1964
1993
  }
1965
1994
  } catch {
1966
1995
  }
1967
1996
  for (const path of candidates) {
1968
1997
  try {
1969
- const token = tokenFromText(await readFile5(path, "utf-8"));
1998
+ const token = tokenFromText(await readFile4(path, "utf-8"));
1970
1999
  if (token) return { token, source: "vscode" };
1971
2000
  } catch {
1972
2001
  }
@@ -1983,13 +2012,20 @@ function resetDate(value) {
1983
2012
  return typeof value === "string" && value.trim() ? resetIn(value) : null;
1984
2013
  }
1985
2014
  function percentMetric2(label, snapshot, reset, primary) {
1986
- if (!snapshot || typeof snapshot.percent_remaining !== "number") return null;
2015
+ if (!snapshot || typeof snapshot.percent_remaining !== "number" || !Number.isFinite(snapshot.percent_remaining)) return null;
1987
2016
  if (snapshot.unlimited === true || snapshot.entitlement === 0) return null;
1988
2017
  const used = Math.min(100, Math.max(0, 100 - snapshot.percent_remaining));
1989
- return { label, used, limit: 100, format: { kind: "percent" }, resetsAt: reset, primary };
2018
+ return {
2019
+ label,
2020
+ used,
2021
+ limit: 100,
2022
+ format: { kind: "percent" },
2023
+ resetsAt: reset,
2024
+ ...primary === void 0 ? {} : { primary }
2025
+ };
1990
2026
  }
1991
2027
  function countMetric(label, remaining, total, reset) {
1992
- if (typeof remaining !== "number" || typeof total !== "number" || total <= 0) return null;
2028
+ if (typeof remaining !== "number" || typeof total !== "number" || !Number.isFinite(remaining) || !Number.isFinite(total) || total <= 0) return null;
1993
2029
  return {
1994
2030
  label,
1995
2031
  used: Math.max(0, total - remaining),
@@ -2060,14 +2096,14 @@ var copilotProvider = {
2060
2096
 
2061
2097
  // src/providers/antigravity/billing.ts
2062
2098
  import { access as access7, readdir as readdir7 } from "fs/promises";
2063
- import { join as join13 } from "path";
2064
- import { homedir as homedir11 } from "os";
2099
+ import { join as join12 } from "path";
2100
+ import { homedir as homedir10 } from "os";
2065
2101
 
2066
2102
  // src/providers/cloud-code.ts
2067
- import { readFile as readFile6, readdir as readdir6, realpath, stat } from "fs/promises";
2103
+ import { readFile as readFile5, readdir as readdir6, realpath, stat } from "fs/promises";
2068
2104
  import { spawnSync } from "child_process";
2069
- import { homedir as homedir10 } from "os";
2070
- import { dirname, join as join12 } from "path";
2105
+ import { homedir as homedir9 } from "os";
2106
+ import { dirname, join as join11 } from "path";
2071
2107
  var CLOUD_CODE_URLS = [
2072
2108
  "https://daily-cloudcode-pa.googleapis.com",
2073
2109
  "https://cloudcode-pa.googleapis.com"
@@ -2095,42 +2131,42 @@ var CC_MODEL_BLACKLIST = {
2095
2131
  function readVarint(bytes, start) {
2096
2132
  let value = 0;
2097
2133
  let shift = 0;
2098
- let pos3 = start;
2099
- while (pos3 < bytes.length) {
2100
- const b = bytes[pos3++];
2134
+ let pos = start;
2135
+ while (pos < bytes.length) {
2136
+ const b = bytes[pos++];
2101
2137
  value += (b & 127) * Math.pow(2, shift);
2102
- if ((b & 128) === 0) return { value, pos: pos3 };
2138
+ if ((b & 128) === 0) return { value, pos };
2103
2139
  shift += 7;
2104
2140
  }
2105
2141
  return null;
2106
2142
  }
2107
2143
  function readFields(bytes) {
2108
2144
  const fields = {};
2109
- let pos3 = 0;
2110
- while (pos3 < bytes.length) {
2111
- const tag = readVarint(bytes, pos3);
2145
+ let pos = 0;
2146
+ while (pos < bytes.length) {
2147
+ const tag = readVarint(bytes, pos);
2112
2148
  if (!tag) break;
2113
- pos3 = tag.pos;
2149
+ pos = tag.pos;
2114
2150
  const fieldNum = Math.floor(tag.value / 8);
2115
2151
  const wireType = tag.value % 8;
2116
2152
  if (wireType === 0) {
2117
- const val = readVarint(bytes, pos3);
2153
+ const val = readVarint(bytes, pos);
2118
2154
  if (!val) break;
2119
2155
  fields[fieldNum] = { type: 0, value: val.value };
2120
- pos3 = val.pos;
2156
+ pos = val.pos;
2121
2157
  } else if (wireType === 1) {
2122
- if (pos3 + 8 > bytes.length) break;
2123
- pos3 += 8;
2158
+ if (pos + 8 > bytes.length) break;
2159
+ pos += 8;
2124
2160
  } else if (wireType === 2) {
2125
- const len = readVarint(bytes, pos3);
2161
+ const len = readVarint(bytes, pos);
2126
2162
  if (!len) break;
2127
- pos3 = len.pos;
2128
- if (pos3 + len.value > bytes.length) break;
2129
- fields[fieldNum] = { type: 2, data: bytes.slice(pos3, pos3 + len.value) };
2130
- pos3 += len.value;
2163
+ pos = len.pos;
2164
+ if (pos + len.value > bytes.length) break;
2165
+ fields[fieldNum] = { type: 2, data: bytes.slice(pos, pos + len.value) };
2166
+ pos += len.value;
2131
2167
  } else if (wireType === 5) {
2132
- if (pos3 + 4 > bytes.length) break;
2133
- pos3 += 4;
2168
+ if (pos + 4 > bytes.length) break;
2169
+ pos += 4;
2134
2170
  } else {
2135
2171
  break;
2136
2172
  }
@@ -2193,7 +2229,7 @@ function geminiBundleCandidates() {
2193
2229
  const candidates = [];
2194
2230
  const addBundle = (nodeModulesRoot) => {
2195
2231
  if (!nodeModulesRoot) return;
2196
- candidates.push(join12(nodeModulesRoot, "@google", "gemini-cli", "bundle"));
2232
+ candidates.push(join11(nodeModulesRoot, "@google", "gemini-cli", "bundle"));
2197
2233
  };
2198
2234
  try {
2199
2235
  const which = spawnSync("command", ["-v", "gemini"], { encoding: "utf8", timeout: 5e3 });
@@ -2201,22 +2237,22 @@ function geminiBundleCandidates() {
2201
2237
  if (resolved) candidates.push(resolved);
2202
2238
  } catch {
2203
2239
  }
2204
- const home = homedir10();
2240
+ const home = homedir9();
2205
2241
  addBundle("/opt/homebrew/lib/node_modules");
2206
2242
  addBundle("/usr/local/lib/node_modules");
2207
- addBundle(join12(home, ".local", "share", "node_modules"));
2208
- addBundle(join12(home, ".bun", "install", "global", "node_modules"));
2243
+ addBundle(join11(home, ".local", "share", "node_modules"));
2244
+ addBundle(join11(home, ".bun", "install", "global", "node_modules"));
2209
2245
  try {
2210
2246
  const prefix = spawnSync("npm", ["config", "get", "prefix"], { encoding: "utf8", timeout: 5e3 });
2211
2247
  const root = typeof prefix.stdout === "string" ? prefix.stdout.trim() : "";
2212
- if (root && root !== "undefined") addBundle(join12(root, "lib", "node_modules"));
2248
+ if (root && root !== "undefined") addBundle(join11(root, "lib", "node_modules"));
2213
2249
  } catch {
2214
2250
  }
2215
2251
  return [...new Set(candidates.filter(Boolean))];
2216
2252
  }
2217
2253
  async function resolveBundleDir(candidate) {
2218
2254
  try {
2219
- if (candidate.endsWith(`${join12("@google", "gemini-cli", "bundle")}`)) {
2255
+ if (candidate.endsWith(`${join11("@google", "gemini-cli", "bundle")}`)) {
2220
2256
  return candidate;
2221
2257
  }
2222
2258
  const real = await realpath(candidate);
@@ -2234,11 +2270,11 @@ async function scanBundleDir(dir) {
2234
2270
  }
2235
2271
  const targets = entries.filter((name) => name === "gemini.js" || name.startsWith("chunk-") && name.endsWith(".js"));
2236
2272
  for (const name of targets) {
2237
- const filePath = join12(dir, name);
2273
+ const filePath = join11(dir, name);
2238
2274
  try {
2239
2275
  const info = await stat(filePath);
2240
2276
  if (!info.isFile() || info.size > MAX_BUNDLE_READ) continue;
2241
- const contents = await readFile6(filePath, "utf8");
2277
+ const contents = await readFile5(filePath, "utf8");
2242
2278
  if (!contents.includes("OAUTH_CLIENT_SECRET")) continue;
2243
2279
  const match = GOOGLE_OAUTH_CLIENT_REGEX.exec(contents);
2244
2280
  if (match) return { clientId: match[1], clientSecret: match[2] };
@@ -2320,14 +2356,19 @@ function readPlan(loadData) {
2320
2356
  if (!raw) return null;
2321
2357
  return raw.replace(/^Gemini Code Assist (?:in|for)\s+/i, "").replace(/^Gemini Code Assist$/i, "Code Assist");
2322
2358
  }
2359
+ function readRemainingFraction(value) {
2360
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
2361
+ }
2323
2362
  function parseBuckets(data) {
2324
2363
  if (!Array.isArray(data?.buckets)) return [];
2325
2364
  return data.buckets.flatMap((bucket) => {
2326
2365
  const modelId = typeof bucket?.modelId === "string" ? bucket.modelId.trim() : "";
2327
2366
  if (!modelId) return [];
2367
+ const remainingFraction = readRemainingFraction(bucket.remainingFraction);
2368
+ if (remainingFraction === null) return [];
2328
2369
  return [{
2329
2370
  modelId,
2330
- remainingFraction: typeof bucket.remainingFraction === "number" ? bucket.remainingFraction : 0,
2371
+ remainingFraction,
2331
2372
  resetTime: typeof bucket.resetTime === "string" ? bucket.resetTime : void 0
2332
2373
  }];
2333
2374
  });
@@ -2343,9 +2384,11 @@ function parseModelBuckets(data) {
2343
2384
  const displayName = typeof model.displayName === "string" && model.displayName.trim() || typeof model.label === "string" && model.label.trim() || "";
2344
2385
  if (!displayName) return [];
2345
2386
  const quotaInfo = model.quotaInfo;
2387
+ const remainingFraction = readRemainingFraction(quotaInfo?.remainingFraction);
2388
+ if (remainingFraction === null) return [];
2346
2389
  return [{
2347
2390
  modelId: displayName,
2348
- remainingFraction: typeof quotaInfo?.remainingFraction === "number" ? quotaInfo.remainingFraction : 0,
2391
+ remainingFraction,
2349
2392
  resetTime: typeof quotaInfo?.resetTime === "string" ? quotaInfo.resetTime : void 0
2350
2393
  }];
2351
2394
  });
@@ -2393,12 +2436,18 @@ function normalizeLabel(label) {
2393
2436
  }
2394
2437
  function poolLabel(label) {
2395
2438
  const lower = normalizeLabel(label).toLowerCase();
2439
+ if (lower.includes("claude")) return "Claude";
2396
2440
  if (lower.includes("gemini") && lower.includes("pro")) return "Pro";
2397
2441
  if (lower.includes("gemini") && lower.includes("flash")) return "Flash";
2398
- return "Claude";
2442
+ if (lower.includes("gemini")) return "Gemini";
2443
+ return normalizeLabel(label) || "Other";
2399
2444
  }
2400
2445
  function sortKey(label) {
2401
2446
  const lower = label.toLowerCase();
2447
+ if (lower === "pro") return `0a_${label}`;
2448
+ if (lower === "flash") return `0b_${label}`;
2449
+ if (lower === "gemini") return `0c_${label}`;
2450
+ if (lower === "claude") return `1b_${label}`;
2402
2451
  if (lower.includes("gemini") && lower.includes("pro")) return `0a_${label}`;
2403
2452
  if (lower.includes("gemini")) return `0b_${label}`;
2404
2453
  if (lower.includes("claude") && lower.includes("opus")) return `1a_${label}`;
@@ -2408,6 +2457,7 @@ function sortKey(label) {
2408
2457
  function cloudCodeBucketsToMetrics(buckets) {
2409
2458
  const pooled = /* @__PURE__ */ new Map();
2410
2459
  for (const bucket of buckets) {
2460
+ if (!Number.isFinite(bucket.remainingFraction)) continue;
2411
2461
  const label = poolLabel(bucket.modelId);
2412
2462
  const existing = pooled.get(label);
2413
2463
  if (!existing || bucket.remainingFraction < existing.remainingFraction) {
@@ -2446,33 +2496,33 @@ async function firstExisting(paths) {
2446
2496
  return paths[0];
2447
2497
  }
2448
2498
  async function antigravityStateDb(homeDir) {
2449
- const base = homeDir ?? homedir11();
2499
+ const base = homeDir ?? homedir10();
2450
2500
  const tail = ["User", "globalStorage", "state.vscdb"];
2451
2501
  if (process.platform === "darwin") {
2452
- const support = join13(base, "Library", "Application Support");
2502
+ const support = join12(base, "Library", "Application Support");
2453
2503
  const exact = [
2454
- join13(support, "Antigravity IDE", ...tail),
2455
- join13(support, "Antigravity", ...tail)
2504
+ join12(support, "Antigravity IDE", ...tail),
2505
+ join12(support, "Antigravity", ...tail)
2456
2506
  ];
2457
2507
  try {
2458
2508
  const entries = await readdir7(support, { withFileTypes: true });
2459
- const matches = entries.filter((e) => e.isDirectory() && e.name.includes("Antigravity")).map((e) => join13(support, e.name, ...tail));
2509
+ const matches = entries.filter((e) => e.isDirectory() && e.name.includes("Antigravity")).map((e) => join12(support, e.name, ...tail));
2460
2510
  return firstExisting([...exact, ...matches]);
2461
2511
  } catch {
2462
2512
  return firstExisting(exact);
2463
2513
  }
2464
2514
  }
2465
2515
  if (process.platform === "win32") {
2466
- const roaming = homeDir ? join13(homeDir, "AppData", "Roaming") : envDir("APPDATA") ?? join13(base, "AppData", "Roaming");
2516
+ const roaming = homeDir ? join12(homeDir, "AppData", "Roaming") : envDir("APPDATA") ?? join12(base, "AppData", "Roaming");
2467
2517
  return firstExisting([
2468
- join13(roaming, "Antigravity IDE", ...tail),
2469
- join13(roaming, "Antigravity", ...tail)
2518
+ join12(roaming, "Antigravity IDE", ...tail),
2519
+ join12(roaming, "Antigravity", ...tail)
2470
2520
  ]);
2471
2521
  }
2472
- const cfg = homeDir ? join13(homeDir, ".config") : envDir("XDG_CONFIG_HOME") ?? join13(base, ".config");
2522
+ const cfg = homeDir ? join12(homeDir, ".config") : envDir("XDG_CONFIG_HOME") ?? join12(base, ".config");
2473
2523
  return firstExisting([
2474
- join13(cfg, "Antigravity IDE", ...tail),
2475
- join13(cfg, "Antigravity", ...tail)
2524
+ join12(cfg, "Antigravity IDE", ...tail),
2525
+ join12(cfg, "Antigravity", ...tail)
2476
2526
  ]);
2477
2527
  }
2478
2528
  async function detectAntigravity(homeDir) {
@@ -2503,11 +2553,11 @@ var antigravityProvider = {
2503
2553
  };
2504
2554
 
2505
2555
  // src/providers/gemini/billing.ts
2506
- import { access as access8, readFile as readFile7 } from "fs/promises";
2507
- import { join as join14 } from "path";
2508
- import { homedir as homedir12 } from "os";
2556
+ import { access as access8, readFile as readFile6 } from "fs/promises";
2557
+ import { join as join13 } from "path";
2558
+ import { homedir as homedir11 } from "os";
2509
2559
  function geminiCredsPath(homeDir) {
2510
- return join14(homeDir ?? homedir12(), ".gemini", "oauth_creds.json");
2560
+ return join13(homeDir ?? homedir11(), ".gemini", "oauth_creds.json");
2511
2561
  }
2512
2562
  async function detectGemini(homeDir) {
2513
2563
  try {
@@ -2517,9 +2567,24 @@ async function detectGemini(homeDir) {
2517
2567
  return false;
2518
2568
  }
2519
2569
  }
2570
+ function decodeBase64UrlJson2(segment) {
2571
+ try {
2572
+ const normalized = segment.replace(/-/g, "+").replace(/_/g, "/");
2573
+ const padded = normalized + "=".repeat((4 - normalized.length % 4) % 4);
2574
+ return JSON.parse(Buffer.from(padded, "base64").toString("utf8"));
2575
+ } catch {
2576
+ return null;
2577
+ }
2578
+ }
2579
+ function geminiIdentity(creds) {
2580
+ const tokenPayload = typeof creds?.id_token === "string" && creds.id_token.includes(".") ? decodeBase64UrlJson2(creds.id_token.split(".")[1]) : null;
2581
+ const email = typeof creds?.email === "string" && creds.email.trim() || typeof tokenPayload?.email === "string" && tokenPayload.email.trim() || null;
2582
+ const displayName = typeof creds?.displayName === "string" && creds.displayName.trim() || typeof creds?.name === "string" && creds.name.trim() || typeof tokenPayload?.name === "string" && tokenPayload.name.trim() || null;
2583
+ return { email, displayName };
2584
+ }
2520
2585
  async function readGeminiCreds(path) {
2521
2586
  try {
2522
- return JSON.parse(await readFile7(path, "utf8"));
2587
+ return JSON.parse(await readFile6(path, "utf8"));
2523
2588
  } catch {
2524
2589
  return null;
2525
2590
  }
@@ -2527,16 +2592,17 @@ async function readGeminiCreds(path) {
2527
2592
  async function geminiBilling(account) {
2528
2593
  try {
2529
2594
  const creds = await readGeminiCreds(geminiCredsPath(account.homeDir));
2595
+ const identity = geminiIdentity(creds);
2530
2596
  const accessToken = typeof creds?.access_token === "string" ? creds.access_token.trim() : "";
2531
2597
  const refreshToken = typeof creds?.refresh_token === "string" ? creds.refresh_token.trim() : null;
2532
- if (!creds || !accessToken && !refreshToken) return { plan: null, metrics: [], error: "Not signed in \u2014 run gemini" };
2598
+ if (!creds || !accessToken && !refreshToken) return { plan: null, metrics: [], error: "Not signed in \u2014 run gemini", ...identity };
2533
2599
  const quota = await fetchCloudCodeQuota({
2534
2600
  accessToken,
2535
2601
  refreshToken,
2536
2602
  expirySeconds: typeof creds.expiry_date === "number" ? Math.floor(creds.expiry_date / 1e3) : null
2537
2603
  }, "Token expired \u2014 run gemini");
2538
- if (!quota.ok) return { plan: quota.plan, metrics: [], error: quota.error };
2539
- return { plan: quota.plan, metrics: cloudCodeBucketsToMetrics(quota.buckets), error: null };
2604
+ if (!quota.ok) return { plan: quota.plan, metrics: [], error: quota.error, ...identity };
2605
+ return { plan: quota.plan, metrics: cloudCodeBucketsToMetrics(quota.buckets), error: null, ...identity };
2540
2606
  } catch {
2541
2607
  return { plan: null, metrics: [], error: "Gemini billing unavailable" };
2542
2608
  }
@@ -2555,26 +2621,26 @@ var geminiProvider = {
2555
2621
 
2556
2622
  // src/providers/detect.ts
2557
2623
  import { accessSync, constants } from "fs";
2558
- import { join as join15, delimiter, isAbsolute as isAbsolute3 } from "path";
2559
- import { homedir as homedir13 } from "os";
2624
+ import { join as join14, delimiter, isAbsolute as isAbsolute2 } from "path";
2625
+ import { homedir as homedir12 } from "os";
2560
2626
  function searchDirs() {
2561
- const home = homedir13();
2627
+ const home = homedir12();
2562
2628
  const fromEnv = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
2563
2629
  const extra = process.platform === "win32" ? [
2564
- process.env.APPDATA && join15(process.env.APPDATA, "npm"),
2565
- process.env.LOCALAPPDATA && join15(process.env.LOCALAPPDATA, "pnpm"),
2566
- join15(home, "scoop", "shims")
2630
+ process.env.APPDATA && join14(process.env.APPDATA, "npm"),
2631
+ process.env.LOCALAPPDATA && join14(process.env.LOCALAPPDATA, "pnpm"),
2632
+ join14(home, "scoop", "shims")
2567
2633
  ] : [
2568
2634
  "/opt/homebrew/bin",
2569
2635
  "/usr/local/bin",
2570
2636
  "/usr/bin",
2571
2637
  "/bin",
2572
2638
  "/opt/local/bin",
2573
- join15(home, ".local", "bin"),
2574
- join15(home, "bin"),
2575
- join15(home, ".npm-global", "bin"),
2576
- join15(home, ".bun", "bin"),
2577
- join15(home, ".local", "share", "pnpm")
2639
+ join14(home, ".local", "bin"),
2640
+ join14(home, "bin"),
2641
+ join14(home, ".npm-global", "bin"),
2642
+ join14(home, ".bun", "bin"),
2643
+ join14(home, ".local", "share", "pnpm")
2578
2644
  ];
2579
2645
  return [.../* @__PURE__ */ new Set([...fromEnv, ...extra.filter((d) => !!d)])];
2580
2646
  }
@@ -2591,7 +2657,7 @@ function onPath(names) {
2591
2657
  for (const dir of searchDirs()) {
2592
2658
  for (const n of names) {
2593
2659
  for (const e of exts) {
2594
- if (isExec(join15(dir, n + e))) return true;
2660
+ if (isExec(join14(dir, n + e))) return true;
2595
2661
  }
2596
2662
  }
2597
2663
  }
@@ -2601,7 +2667,7 @@ function anyExists(paths) {
2601
2667
  return paths.some((p) => !!p && isExec(p));
2602
2668
  }
2603
2669
  function installSignals(id) {
2604
- const home = homedir13();
2670
+ const home = homedir12();
2605
2671
  const pf = process.env.ProgramFiles;
2606
2672
  const pf86 = process.env["ProgramFiles(x86)"];
2607
2673
  const lad = process.env.LOCALAPPDATA;
@@ -2609,21 +2675,21 @@ function installSignals(id) {
2609
2675
  case "claude":
2610
2676
  return onPath(["claude"]) || anyExists([
2611
2677
  "/Applications/Claude.app",
2612
- join15(home, "Applications", "Claude.app"),
2613
- lad && join15(lad, "Programs", "claude", "Claude.exe")
2678
+ join14(home, "Applications", "Claude.app"),
2679
+ lad && join14(lad, "Programs", "claude", "Claude.exe")
2614
2680
  ]);
2615
2681
  case "codex": {
2616
2682
  const bin = process.env.CODEX_BIN;
2617
- if (bin && isAbsolute3(bin) && isExec(bin)) return true;
2683
+ if (bin && isAbsolute2(bin) && isExec(bin)) return true;
2618
2684
  return onPath(["codex"]);
2619
2685
  }
2620
2686
  case "cursor":
2621
2687
  return onPath(["cursor", "cursor-agent"]) || anyExists([
2622
2688
  "/Applications/Cursor.app",
2623
- join15(home, "Applications", "Cursor.app"),
2624
- lad && join15(lad, "Programs", "cursor", "Cursor.exe"),
2625
- pf && join15(pf, "Cursor", "Cursor.exe"),
2626
- pf86 && join15(pf86, "Cursor", "Cursor.exe"),
2689
+ join14(home, "Applications", "Cursor.app"),
2690
+ lad && join14(lad, "Programs", "cursor", "Cursor.exe"),
2691
+ pf && join14(pf, "Cursor", "Cursor.exe"),
2692
+ pf86 && join14(pf86, "Cursor", "Cursor.exe"),
2627
2693
  "/opt/Cursor/cursor",
2628
2694
  "/usr/share/cursor/cursor",
2629
2695
  "/usr/bin/cursor"
@@ -2637,8 +2703,8 @@ function installSignals(id) {
2637
2703
  case "antigravity":
2638
2704
  return onPath(["antigravity"]) || anyExists([
2639
2705
  "/Applications/Antigravity.app",
2640
- join15(home, "Applications", "Antigravity.app"),
2641
- lad && join15(lad, "Programs", "Antigravity", "Antigravity.exe")
2706
+ join14(home, "Applications", "Antigravity.app"),
2707
+ lad && join14(lad, "Programs", "Antigravity", "Antigravity.exe")
2642
2708
  ]);
2643
2709
  case "gemini":
2644
2710
  return onPath(["gemini"]);
@@ -2675,24 +2741,120 @@ async function detectProviders() {
2675
2741
  }
2676
2742
 
2677
2743
  // src/accounts.ts
2744
+ import { existsSync, readdirSync, readFileSync, statSync } from "fs";
2745
+ import { homedir as homedir13 } from "os";
2746
+ import { basename, join as join15, resolve } from "path";
2747
+ function accountKey(providerId, homeDir) {
2748
+ return `${providerId}:${homeDir ? resolve(expandHome(homeDir)) : homedir13()}`;
2749
+ }
2750
+ function uniqueId(base, used) {
2751
+ let id = slugify(base) || "account";
2752
+ if (!used.has(id)) {
2753
+ used.add(id);
2754
+ return id;
2755
+ }
2756
+ for (let i = 2; i < 1e3; i++) {
2757
+ const next = `${id}_${i}`;
2758
+ if (!used.has(next)) {
2759
+ used.add(next);
2760
+ return next;
2761
+ }
2762
+ }
2763
+ id = `${id}_${Date.now()}`;
2764
+ used.add(id);
2765
+ return id;
2766
+ }
2767
+ function readClaudeIdentity2(homeDir) {
2768
+ try {
2769
+ const parsed = JSON.parse(readFileSync(join15(homeDir, ".claude.json"), "utf-8"));
2770
+ const oauth = parsed?.oauthAccount;
2771
+ return {
2772
+ email: typeof oauth?.emailAddress === "string" && oauth.emailAddress.trim() ? oauth.emailAddress.trim() : void 0,
2773
+ displayName: typeof oauth?.displayName === "string" && oauth.displayName.trim() ? oauth.displayName.trim() : void 0
2774
+ };
2775
+ } catch {
2776
+ return {};
2777
+ }
2778
+ }
2779
+ function hasClaudeState(homeDir) {
2780
+ return existsSync(join15(homeDir, ".claude.json")) || existsSync(join15(homeDir, ".claude", ".credentials.json")) || existsSync(join15(homeDir, ".claude", "projects")) || existsSync(join15(homeDir, ".config", "claude", ".credentials.json")) || existsSync(join15(homeDir, ".config", "claude", "projects"));
2781
+ }
2782
+ function candidateAlternateHomes() {
2783
+ const home = homedir13();
2784
+ let entries;
2785
+ try {
2786
+ entries = readdirSync(home);
2787
+ } catch {
2788
+ return [];
2789
+ }
2790
+ const out = [];
2791
+ for (const name of entries) {
2792
+ if (!/^\.claude[_-]/.test(name)) continue;
2793
+ const path = join15(home, name);
2794
+ try {
2795
+ if (!statSync(path).isDirectory()) continue;
2796
+ out.push(path);
2797
+ } catch {
2798
+ }
2799
+ }
2800
+ return out.sort();
2801
+ }
2802
+ function labelForClaudeHome(homeDir) {
2803
+ const identity = readClaudeIdentity2(homeDir);
2804
+ if (identity.email) return `Claude ${identity.email}`;
2805
+ if (identity.displayName) return `Claude ${identity.displayName}`;
2806
+ const raw = basename(homeDir).replace(/^\.claude[_-]?/, "").replace(/[_-]+/g, " ").trim();
2807
+ return raw ? `Claude ${raw}` : "Claude";
2808
+ }
2809
+ function discoverClaudeAccounts(usedIds) {
2810
+ const provider = PROVIDERS.claude;
2811
+ const out = [];
2812
+ for (const homeDir of candidateAlternateHomes()) {
2813
+ if (!hasClaudeState(homeDir)) continue;
2814
+ const suffix = basename(homeDir).replace(/^\.claude[_-]?/, "") || basename(homeDir);
2815
+ out.push({
2816
+ id: uniqueId(`claude_${suffix}`, usedIds),
2817
+ providerId: "claude",
2818
+ name: labelForClaudeHome(homeDir),
2819
+ color: provider.color,
2820
+ homeDir
2821
+ });
2822
+ }
2823
+ return out;
2824
+ }
2825
+ function discoverProviderAccounts(providerId, usedIds) {
2826
+ if (providerId === "claude") return discoverClaudeAccounts(usedIds);
2827
+ return [];
2828
+ }
2678
2829
  function buildAccounts(config, detected) {
2679
2830
  const out = [];
2831
+ const usedIds = new Set(config.accounts.map((a) => a.id));
2832
+ const seenKeys = /* @__PURE__ */ new Set();
2833
+ const add = (account) => {
2834
+ const key = accountKey(account.providerId, account.homeDir);
2835
+ if (seenKeys.has(key)) return;
2836
+ seenKeys.add(key);
2837
+ out.push(account);
2838
+ };
2680
2839
  for (const pid of PROVIDER_ORDER) {
2681
2840
  if (config.disabledProviders.includes(pid)) continue;
2682
2841
  const provider = PROVIDERS[pid];
2683
2842
  const configured = config.accounts.filter((a) => a.providerId === pid);
2684
- if (configured.length > 0) {
2685
- for (const a of configured) {
2686
- out.push({
2687
- id: a.id,
2688
- providerId: pid,
2689
- name: a.name,
2690
- color: a.color || provider.color,
2691
- homeDir: a.homeDir && a.homeDir !== "~" ? expandHome(a.homeDir) : void 0
2692
- });
2693
- }
2694
- } else if (detected.includes(pid)) {
2695
- out.push({ id: pid, providerId: pid, name: provider.name, color: provider.color, homeDir: void 0 });
2843
+ for (const a of configured) {
2844
+ add({
2845
+ id: a.id,
2846
+ providerId: pid,
2847
+ name: a.name,
2848
+ color: a.color || provider.color,
2849
+ homeDir: a.homeDir && a.homeDir !== "~" ? expandHome(a.homeDir) : void 0
2850
+ });
2851
+ }
2852
+ const discovered = discoverProviderAccounts(pid, usedIds);
2853
+ if (detected.includes(pid)) {
2854
+ add({ id: pid, providerId: pid, name: provider.name, color: provider.color, homeDir: void 0 });
2855
+ }
2856
+ for (const account of discovered) {
2857
+ add(account);
2696
2858
  }
2697
2859
  }
2698
2860
  return out;
@@ -2706,19 +2868,146 @@ function accountsByProvider(accounts) {
2706
2868
  return groups;
2707
2869
  }
2708
2870
 
2871
+ // src/json-safe.ts
2872
+ function toJsonSafe(value) {
2873
+ const serialized = JSON.stringify(value);
2874
+ return serialized === void 0 ? null : JSON.parse(serialized);
2875
+ }
2876
+
2877
+ // src/peak.ts
2878
+ async function fetchPeak() {
2879
+ try {
2880
+ const res = await fetch("https://promoclock.co/api/status", {
2881
+ headers: { "Accept": "application/json", "User-Agent": "tokmon" },
2882
+ signal: AbortSignal.timeout(3e3)
2883
+ });
2884
+ if (!res.ok) return null;
2885
+ const data = await readJson(res);
2886
+ if (!data) return null;
2887
+ let state;
2888
+ if (data.isPeak === true || data.status === "peak") state = "peak";
2889
+ else if (data.isWeekend === true || data.status === "weekend") state = "weekend";
2890
+ else if (data.isOffPeak === true || data.status === "off_peak" || data.status === "off-peak") state = "off-peak";
2891
+ else return null;
2892
+ return {
2893
+ state,
2894
+ label: state === "peak" ? "Peak" : state === "weekend" ? "Weekend" : "Off-Peak",
2895
+ minutesUntilChange: typeof data.minutesUntilChange === "number" ? data.minutesUntilChange : null
2896
+ };
2897
+ } catch {
2898
+ return null;
2899
+ }
2900
+ }
2901
+
2902
+ // src/rpc/contract.ts
2903
+ import { Schema, SchemaGetter } from "effect";
2904
+ import * as Rpc from "effect/unstable/rpc/Rpc";
2905
+ import * as RpcGroup from "effect/unstable/rpc/RpcGroup";
2906
+ var TOKMON_WS_PATH = "/ws";
2907
+ var TOKMON_WS_METHODS = {
2908
+ getConfig: "tokmon.getConfig",
2909
+ setConfig: "tokmon.setConfig",
2910
+ refresh: "tokmon.refresh",
2911
+ browseFs: "tokmon.browseFs",
2912
+ snapshot: "tokmon.snapshot",
2913
+ config: "tokmon.config"
2914
+ };
2915
+ var RefreshScopeSchema = Schema.Literals([
2916
+ "all",
2917
+ "summary",
2918
+ "table",
2919
+ "billing",
2920
+ "peak"
2921
+ ]);
2922
+ var PROVIDER_IDS = [
2923
+ "claude",
2924
+ "codex",
2925
+ "cursor",
2926
+ "copilot",
2927
+ "pi",
2928
+ "opencode",
2929
+ "antigravity",
2930
+ "gemini"
2931
+ ];
2932
+ var ProviderIdSchema = Schema.Literals(PROVIDER_IDS);
2933
+ var AccountSchema = Schema.Struct({
2934
+ id: Schema.String,
2935
+ providerId: ProviderIdSchema,
2936
+ name: Schema.String,
2937
+ homeDir: Schema.String,
2938
+ color: Schema.optionalKey(Schema.String)
2939
+ });
2940
+ var jsonSafePassthrough = () => Schema.Unknown.pipe(Schema.encode({
2941
+ decode: SchemaGetter.transform((value) => value),
2942
+ encode: SchemaGetter.transform((value) => toJsonSafe(value))
2943
+ }));
2944
+ var ConfigSchema = Schema.Struct({
2945
+ interval: Schema.Number,
2946
+ billingInterval: Schema.Number,
2947
+ clearScreen: Schema.Boolean,
2948
+ timezone: Schema.NullOr(Schema.String),
2949
+ accounts: Schema.mutable(Schema.Array(AccountSchema)),
2950
+ activeAccountId: Schema.NullOr(Schema.String),
2951
+ disabledProviders: Schema.mutable(Schema.Array(ProviderIdSchema)),
2952
+ onboarded: Schema.Boolean,
2953
+ dashboardLayout: Schema.Literals(["grid", "single"]),
2954
+ defaultFocus: Schema.Literals(["all", "last"]),
2955
+ ascii: Schema.Literals(["auto", "on", "off"]),
2956
+ knownProviders: Schema.mutable(Schema.Array(ProviderIdSchema))
2957
+ });
2958
+ var ConfigResultSchema = jsonSafePassthrough();
2959
+ var FsEntrySchema = Schema.Struct({
2960
+ name: Schema.String,
2961
+ path: Schema.String,
2962
+ dir: Schema.Boolean
2963
+ });
2964
+ var FsListingSchema = Schema.Struct({
2965
+ path: Schema.String,
2966
+ parent: Schema.NullOr(Schema.String),
2967
+ entries: Schema.Array(FsEntrySchema)
2968
+ });
2969
+ var WebSnapshotSchema = jsonSafePassthrough();
2970
+ var EmptyPayloadSchema = Schema.Struct({});
2971
+ var GetConfigRpc = Rpc.make(TOKMON_WS_METHODS.getConfig, {
2972
+ payload: EmptyPayloadSchema,
2973
+ success: ConfigResultSchema
2974
+ });
2975
+ var SetConfigRpc = Rpc.make(TOKMON_WS_METHODS.setConfig, {
2976
+ payload: ConfigSchema,
2977
+ success: ConfigResultSchema
2978
+ });
2979
+ var RefreshRpc = Rpc.make(TOKMON_WS_METHODS.refresh, {
2980
+ payload: Schema.Struct({ scope: RefreshScopeSchema }),
2981
+ success: Schema.Void
2982
+ });
2983
+ var BrowseFsRpc = Rpc.make(TOKMON_WS_METHODS.browseFs, {
2984
+ payload: Schema.Struct({ path: Schema.String }),
2985
+ success: FsListingSchema
2986
+ });
2987
+ var SnapshotRpc = Rpc.make(TOKMON_WS_METHODS.snapshot, {
2988
+ payload: EmptyPayloadSchema,
2989
+ success: WebSnapshotSchema,
2990
+ stream: true
2991
+ });
2992
+ var ConfigRpc = Rpc.make(TOKMON_WS_METHODS.config, {
2993
+ payload: EmptyPayloadSchema,
2994
+ success: ConfigResultSchema,
2995
+ stream: true
2996
+ });
2997
+ var TokmonRpcGroup = RpcGroup.make(
2998
+ GetConfigRpc,
2999
+ SetConfigRpc,
3000
+ RefreshRpc,
3001
+ BrowseFsRpc,
3002
+ SnapshotRpc,
3003
+ ConfigRpc
3004
+ );
3005
+
2709
3006
  export {
2710
- configLocation,
2711
- cacheDir,
2712
- loadConfig,
2713
- saveConfig,
2714
- generateAccountId,
2715
- pickAccentColor,
2716
3007
  systemTimezone,
2717
- isValidTimezone,
2718
3008
  resolveTimezone,
2719
3009
  flushDisk,
2720
- mergeTables,
2721
- readJson,
3010
+ coalesceTables,
2722
3011
  currency,
2723
3012
  tokens,
2724
3013
  time,
@@ -2729,5 +3018,10 @@ export {
2729
3018
  PROVIDERS,
2730
3019
  detectProviders,
2731
3020
  buildAccounts,
2732
- accountsByProvider
3021
+ accountsByProvider,
3022
+ toJsonSafe,
3023
+ fetchPeak,
3024
+ TOKMON_WS_PATH,
3025
+ TOKMON_WS_METHODS,
3026
+ TokmonRpcGroup
2733
3027
  };