tokmon 0.19.8 → 0.20.1

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