tokmon 0.14.5 → 0.15.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.
Files changed (37) hide show
  1. package/dist/chunk-Z7JLP2Y2.js +564 -0
  2. package/dist/chunk-ZPH4754N.js +2542 -0
  3. package/dist/cli.js +204 -2622
  4. package/dist/server-NO7JYH7U.js +8 -0
  5. package/dist/web/assets/DepartureMono-Regular-2BZob_Zz.woff2 +0 -0
  6. package/dist/web/assets/index-Blfaml-c.js +113 -0
  7. package/dist/web/assets/index-DxSkJ-XD.css +1 -0
  8. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  9. package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  10. package/dist/web/assets/jetbrains-mono-cyrillic-500-normal-DJqRU3vO.woff +0 -0
  11. package/dist/web/assets/jetbrains-mono-cyrillic-500-normal-DmUKJPL_.woff2 +0 -0
  12. package/dist/web/assets/jetbrains-mono-cyrillic-700-normal-BWTpRfYl.woff2 +0 -0
  13. package/dist/web/assets/jetbrains-mono-cyrillic-700-normal-CEoEElIJ.woff +0 -0
  14. package/dist/web/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  15. package/dist/web/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  16. package/dist/web/assets/jetbrains-mono-greek-500-normal-D7SFKleX.woff +0 -0
  17. package/dist/web/assets/jetbrains-mono-greek-500-normal-JpySY46c.woff2 +0 -0
  18. package/dist/web/assets/jetbrains-mono-greek-700-normal-C6CZE3T8.woff2 +0 -0
  19. package/dist/web/assets/jetbrains-mono-greek-700-normal-DEigVDxa.woff +0 -0
  20. package/dist/web/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  21. package/dist/web/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  22. package/dist/web/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
  23. package/dist/web/assets/jetbrains-mono-latin-500-normal-CJOVTJB7.woff +0 -0
  24. package/dist/web/assets/jetbrains-mono-latin-700-normal-BYuf6tUa.woff2 +0 -0
  25. package/dist/web/assets/jetbrains-mono-latin-700-normal-D3wTyLJW.woff +0 -0
  26. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  27. package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  28. package/dist/web/assets/jetbrains-mono-latin-ext-500-normal-Cut-4mMH.woff2 +0 -0
  29. package/dist/web/assets/jetbrains-mono-latin-ext-500-normal-ckzbgY84.woff +0 -0
  30. package/dist/web/assets/jetbrains-mono-latin-ext-700-normal-CZipNAKV.woff2 +0 -0
  31. package/dist/web/assets/jetbrains-mono-latin-ext-700-normal-CxPITLHs.woff +0 -0
  32. package/dist/web/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  33. package/dist/web/assets/jetbrains-mono-vietnamese-500-normal-DNRqzVM1.woff +0 -0
  34. package/dist/web/assets/jetbrains-mono-vietnamese-700-normal-BDLVIk2r.woff +0 -0
  35. package/dist/web/index.html +20 -0
  36. package/dist/web-KQUELAT7.js +110 -0
  37. package/package.json +7 -3
@@ -0,0 +1,2542 @@
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
+ }
128
+
129
+ // 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";
132
+
133
+ // src/tz.ts
134
+ function systemTimezone() {
135
+ try {
136
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
137
+ } catch {
138
+ return "UTC";
139
+ }
140
+ }
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
+ function resolveTimezone(cfg) {
150
+ if (!cfg) return systemTimezone();
151
+ return isValidTimezone(cfg) ? cfg : systemTimezone();
152
+ }
153
+ var dayFmtCache = /* @__PURE__ */ new Map();
154
+ function dayFmt(tz) {
155
+ let f = dayFmtCache.get(tz);
156
+ if (!f) {
157
+ f = new Intl.DateTimeFormat("en-CA", {
158
+ timeZone: tz,
159
+ year: "numeric",
160
+ month: "2-digit",
161
+ day: "2-digit"
162
+ });
163
+ dayFmtCache.set(tz, f);
164
+ }
165
+ return f;
166
+ }
167
+ function dayKey(ts, tz) {
168
+ return dayFmt(tz).format(new Date(ts));
169
+ }
170
+ function monthKey(ts, tz) {
171
+ return dayKey(ts, tz).slice(0, 7);
172
+ }
173
+ var partsFmtCache = /* @__PURE__ */ new Map();
174
+ function partsFmt(tz) {
175
+ let f = partsFmtCache.get(tz);
176
+ if (!f) {
177
+ f = new Intl.DateTimeFormat("en-US", {
178
+ timeZone: tz,
179
+ hourCycle: "h23",
180
+ year: "numeric",
181
+ month: "2-digit",
182
+ day: "2-digit",
183
+ hour: "2-digit",
184
+ minute: "2-digit",
185
+ second: "2-digit",
186
+ weekday: "short"
187
+ });
188
+ partsFmtCache.set(tz, f);
189
+ }
190
+ return f;
191
+ }
192
+ var WEEKDAY_MAP = {
193
+ Sun: 0,
194
+ Mon: 1,
195
+ Tue: 2,
196
+ Wed: 3,
197
+ Thu: 4,
198
+ Fri: 5,
199
+ Sat: 6
200
+ };
201
+ function tzParts(ts, tz) {
202
+ const parts = partsFmt(tz).formatToParts(new Date(ts));
203
+ const get = (t) => parts.find((p) => p.type === t)?.value ?? "";
204
+ return {
205
+ y: Number(get("year")),
206
+ m: Number(get("month")),
207
+ d: Number(get("day")),
208
+ hh: Number(get("hour")),
209
+ mm: Number(get("minute")),
210
+ ss: Number(get("second")),
211
+ weekday: WEEKDAY_MAP[get("weekday")] ?? 0
212
+ };
213
+ }
214
+ function instantFromTz(y, m, d, hh, mm, ss, tz) {
215
+ const guess = Date.UTC(y, m - 1, d, hh, mm, ss);
216
+ const r = tzParts(guess, tz);
217
+ const rendered = Date.UTC(r.y, r.m - 1, r.d, r.hh, r.mm, r.ss);
218
+ const offset = rendered - guess;
219
+ return guess - offset;
220
+ }
221
+ function startOfDay(ts, tz) {
222
+ const p = tzParts(ts, tz);
223
+ return instantFromTz(p.y, p.m, p.d, 0, 0, 0, tz);
224
+ }
225
+ function startOfMonth(ts, tz) {
226
+ const p = tzParts(ts, tz);
227
+ return instantFromTz(p.y, p.m, 1, 0, 0, 0, tz);
228
+ }
229
+ function startOfWeek(ts, tz) {
230
+ const p = tzParts(ts, tz);
231
+ const offset = p.weekday === 0 ? 6 : p.weekday - 1;
232
+ return instantFromTz(p.y, p.m, p.d - offset, 0, 0, 0, tz);
233
+ }
234
+ function monthsAgoStart(ts, months, tz) {
235
+ const p = tzParts(ts, tz);
236
+ return instantFromTz(p.y, p.m - months, 1, 0, 0, 0, tz);
237
+ }
238
+ function weekKey(ts, tz) {
239
+ return dayKey(startOfWeek(ts, tz), tz);
240
+ }
241
+
242
+ // src/providers/usage-core.ts
243
+ var SPARK_DAYS = 14;
244
+ var DAY_MS = 864e5;
245
+ var CACHE_VERSION = 4;
246
+ var STABLE_AGE_MS = 5 * 6e4;
247
+ var PRUNE_AGE_MS = 200 * DAY_MS;
248
+ var memCache = /* @__PURE__ */ new Map();
249
+ var diskLoaded = false;
250
+ var dirty = false;
251
+ var flushTimer = null;
252
+ function cacheFile() {
253
+ return join2(cacheDir(), `usage-v${CACHE_VERSION}.json`);
254
+ }
255
+ function encode(mtimeMs, size, entries) {
256
+ const mods = [];
257
+ const idx = /* @__PURE__ */ new Map();
258
+ const rows = entries.map((e) => {
259
+ let mi = idx.get(e.model);
260
+ if (mi === void 0) {
261
+ mi = mods.length;
262
+ mods.push(e.model);
263
+ idx.set(e.model, mi);
264
+ }
265
+ return [e.ts, mi, e.input, e.output, e.cacheCreate, e.cacheRead, e.cost, e.cacheSavings, e.id ?? 0];
266
+ });
267
+ return { m: mtimeMs, s: size, mods, rows };
268
+ }
269
+ function decode(s) {
270
+ const num = (x) => typeof x === "number" && Number.isFinite(x) && x >= 0 ? x : 0;
271
+ const out = [];
272
+ for (const r of s.rows) {
273
+ if (!Array.isArray(r) || r.length < 8) continue;
274
+ const ts = r[0];
275
+ if (typeof ts !== "number" || !Number.isFinite(ts)) continue;
276
+ const mi = r[1];
277
+ out.push({
278
+ ts,
279
+ model: typeof mi === "number" && typeof s.mods[mi] === "string" ? s.mods[mi] : "unknown",
280
+ input: num(r[2]),
281
+ output: num(r[3]),
282
+ cacheCreate: num(r[4]),
283
+ cacheRead: num(r[5]),
284
+ cost: num(r[6]),
285
+ cacheSavings: num(r[7]),
286
+ id: typeof r[8] === "string" ? r[8] : void 0
287
+ });
288
+ }
289
+ return out;
290
+ }
291
+ async function ensureDiskLoaded() {
292
+ if (diskLoaded) return;
293
+ diskLoaded = true;
294
+ try {
295
+ const obj = JSON.parse(await readFile2(cacheFile(), "utf-8"));
296
+ for (const [path, s] of Object.entries(obj)) {
297
+ if (s && typeof s.m === "number" && Array.isArray(s.rows) && Array.isArray(s.mods)) {
298
+ memCache.set(path, { mtimeMs: s.m, size: typeof s.s === "number" ? s.s : -1, entries: decode(s) });
299
+ }
300
+ }
301
+ } catch {
302
+ }
303
+ }
304
+ async function flushDisk() {
305
+ if (!dirty) return;
306
+ const now = Date.now();
307
+ const obj = {};
308
+ for (const [path, v] of memCache) {
309
+ if (now - v.mtimeMs > STABLE_AGE_MS && now - v.mtimeMs < PRUNE_AGE_MS) {
310
+ obj[path] = encode(v.mtimeMs, v.size, v.entries);
311
+ }
312
+ }
313
+ try {
314
+ await mkdir2(cacheDir(), { recursive: true });
315
+ const tmp = `${cacheFile()}.${process.pid}.tmp`;
316
+ await writeFile2(tmp, JSON.stringify(obj));
317
+ await rename2(tmp, cacheFile());
318
+ dirty = false;
319
+ } catch {
320
+ }
321
+ }
322
+ function scheduleFlush() {
323
+ if (flushTimer) return;
324
+ flushTimer = setTimeout(() => {
325
+ flushTimer = null;
326
+ void flushDisk();
327
+ }, 4e3);
328
+ flushTimer.unref?.();
329
+ }
330
+ async function mapLimit(items, limit, fn) {
331
+ let i = 0;
332
+ const worker = async () => {
333
+ while (i < items.length) await fn(items[i++]);
334
+ };
335
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
336
+ }
337
+ async function loadCachedEntries(files, parse, since) {
338
+ await ensureDiskLoaded();
339
+ const chunks = [];
340
+ await mapLimit(files, 8, async (f) => {
341
+ try {
342
+ let c = memCache.get(f.path);
343
+ if (!c || c.mtimeMs !== f.mtimeMs || c.size !== f.size) {
344
+ const entries = await parse(f.path);
345
+ c = { mtimeMs: f.mtimeMs, size: f.size, entries };
346
+ memCache.set(f.path, c);
347
+ if (Date.now() - f.mtimeMs > STABLE_AGE_MS) dirty = true;
348
+ }
349
+ chunks.push(c.entries);
350
+ } catch {
351
+ }
352
+ });
353
+ if (dirty) scheduleFlush();
354
+ return dedupe(chunks.flat().filter((e) => e.ts >= since));
355
+ }
356
+ function safeNum(v) {
357
+ return typeof v === "number" && Number.isFinite(v) && v > 0 ? Math.floor(v) : 0;
358
+ }
359
+ function dedupe(entries) {
360
+ const seen = /* @__PURE__ */ new Set();
361
+ const out = [];
362
+ for (const e of entries) {
363
+ const k = e.id ?? `${e.ts} ${e.model} ${e.input} ${e.output} ${e.cacheCreate} ${e.cacheRead}`;
364
+ if (seen.has(k)) continue;
365
+ seen.add(k);
366
+ out.push(e);
367
+ }
368
+ return out;
369
+ }
370
+ function summarize(entries, tz) {
371
+ const now = Date.now();
372
+ const todayStart = startOfDay(now, tz);
373
+ const weekStart = startOfWeek(now, tz);
374
+ const monthStart = startOfMonth(now, tz);
375
+ const today = { cost: 0, tokens: 0, cacheRead: 0, cacheSavings: 0 };
376
+ const week = { cost: 0, tokens: 0, cacheRead: 0, cacheSavings: 0 };
377
+ const month = { cost: 0, tokens: 0, cacheRead: 0, cacheSavings: 0 };
378
+ const byDay = /* @__PURE__ */ new Map();
379
+ let oldestToday = now;
380
+ let hadToday = false;
381
+ const add = (s, e) => {
382
+ s.cost += e.cost;
383
+ s.tokens += e.input + e.output + e.cacheCreate + e.cacheRead;
384
+ s.cacheRead += e.cacheRead;
385
+ s.cacheSavings += e.cacheSavings;
386
+ };
387
+ for (const e of entries) {
388
+ if (e.ts >= monthStart) add(month, e);
389
+ if (e.ts >= weekStart) add(week, e);
390
+ if (e.ts >= todayStart) {
391
+ add(today, e);
392
+ hadToday = true;
393
+ if (e.ts < oldestToday) oldestToday = e.ts;
394
+ }
395
+ const dk = dayKey(e.ts, tz);
396
+ byDay.set(dk, (byDay.get(dk) ?? 0) + e.cost);
397
+ }
398
+ const hrs = Math.max((now - oldestToday) / 36e5, 1 / 60);
399
+ const burnRate = hadToday ? today.cost / hrs : 0;
400
+ const series = [];
401
+ for (let i = SPARK_DAYS - 1; i >= 0; i--) series.push(byDay.get(dayKey(now - i * DAY_MS, tz)) ?? 0);
402
+ return { today, week, month, burnRate, series };
403
+ }
404
+ function groupBy(entries, keyFn) {
405
+ const groups = /* @__PURE__ */ new Map();
406
+ for (const e of entries) {
407
+ const key = keyFn(e);
408
+ const arr = groups.get(key);
409
+ if (arr) arr.push(e);
410
+ else groups.set(key, [e]);
411
+ }
412
+ const rows = [];
413
+ for (const [label, group] of groups) {
414
+ let input = 0, output = 0, cacheCreate = 0, cacheRead = 0, cacheSavings = 0, cost = 0, count = 0;
415
+ const byModel = /* @__PURE__ */ new Map();
416
+ for (const e of group) {
417
+ input += e.input;
418
+ output += e.output;
419
+ cacheCreate += e.cacheCreate;
420
+ cacheRead += e.cacheRead;
421
+ cacheSavings += e.cacheSavings;
422
+ cost += e.cost;
423
+ count += 1;
424
+ const m = byModel.get(e.model);
425
+ if (m) {
426
+ m.input += e.input;
427
+ m.output += e.output;
428
+ m.cacheCreate += e.cacheCreate;
429
+ m.cacheRead += e.cacheRead;
430
+ m.cacheSavings += e.cacheSavings;
431
+ m.cost += e.cost;
432
+ m.count += 1;
433
+ } else {
434
+ byModel.set(e.model, {
435
+ name: e.model,
436
+ input: e.input,
437
+ output: e.output,
438
+ cacheCreate: e.cacheCreate,
439
+ cacheRead: e.cacheRead,
440
+ cacheSavings: e.cacheSavings,
441
+ cost: e.cost,
442
+ count: 1
443
+ });
444
+ }
445
+ }
446
+ rows.push({
447
+ label,
448
+ models: [...byModel.keys()].sort(),
449
+ input,
450
+ output,
451
+ cacheCreate,
452
+ cacheRead,
453
+ cacheSavings,
454
+ total: input + output + cacheCreate + cacheRead,
455
+ cost,
456
+ count,
457
+ breakdown: [...byModel.values()].sort((a, b) => b.cost - a.cost)
458
+ });
459
+ }
460
+ return rows.sort((a, b) => a.label.localeCompare(b.label));
461
+ }
462
+ function tabulate(entries, tz) {
463
+ return {
464
+ daily: groupBy(entries, (e) => dayKey(e.ts, tz)),
465
+ weekly: groupBy(entries, (e) => weekKey(e.ts, tz)),
466
+ monthly: groupBy(entries, (e) => monthKey(e.ts, tz))
467
+ };
468
+ }
469
+ function mergeRows(groups) {
470
+ const byLabel = /* @__PURE__ */ new Map();
471
+ for (const rows of groups) {
472
+ for (const r of rows) {
473
+ const ex = byLabel.get(r.label);
474
+ if (!ex) {
475
+ byLabel.set(r.label, { ...r, models: [...r.models], breakdown: r.breakdown.map((m) => ({ ...m })) });
476
+ continue;
477
+ }
478
+ ex.input += r.input;
479
+ ex.output += r.output;
480
+ ex.cacheCreate += r.cacheCreate;
481
+ ex.cacheRead += r.cacheRead;
482
+ ex.cacheSavings += r.cacheSavings;
483
+ ex.total += r.total;
484
+ ex.cost += r.cost;
485
+ ex.count += r.count;
486
+ const bd = new Map(ex.breakdown.map((m) => [m.name, m]));
487
+ for (const m of r.breakdown) {
488
+ const e = bd.get(m.name);
489
+ if (e) {
490
+ e.input += m.input;
491
+ e.output += m.output;
492
+ e.cacheCreate += m.cacheCreate;
493
+ e.cacheRead += m.cacheRead;
494
+ e.cacheSavings += m.cacheSavings;
495
+ e.cost += m.cost;
496
+ e.count += m.count;
497
+ } else {
498
+ bd.set(m.name, { ...m });
499
+ }
500
+ }
501
+ ex.breakdown = [...bd.values()].sort((a, b) => b.cost - a.cost);
502
+ ex.models = [...bd.keys()].sort();
503
+ }
504
+ }
505
+ return [...byLabel.values()].sort((a, b) => a.label.localeCompare(b.label));
506
+ }
507
+ function mergeTables(list) {
508
+ return {
509
+ daily: mergeRows(list.map((t) => t.daily)),
510
+ weekly: mergeRows(list.map((t) => t.weekly)),
511
+ monthly: mergeRows(list.map((t) => t.monthly))
512
+ };
513
+ }
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)}`;
522
+ }
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) {
531
+ return date.toLocaleTimeString(void 0, {
532
+ hour: "2-digit",
533
+ minute: "2-digit",
534
+ second: "2-digit",
535
+ timeZone: tz
536
+ });
537
+ }
538
+ var SHORT_MONTHS = ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
539
+ function shortDate(iso) {
540
+ const [, m, d] = iso.split("-");
541
+ return `${SHORT_MONTHS[Number(m)]} ${Number(d).toString().padStart(2, " ")}`;
542
+ }
543
+ function col(s, w, align = "right") {
544
+ if (s.length > w) return s.slice(0, w - 1) + "~";
545
+ const spaces = " ".repeat(w - s.length);
546
+ return align === "right" ? spaces + s : s + spaces;
547
+ }
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
+ }
560
+
561
+ // src/providers/cursor/billing.ts
562
+ import { access } from "fs/promises";
563
+ import { join as join4 } from "path";
564
+ import { homedir as homedir3 } from "os";
565
+
566
+ // src/http.ts
567
+ async function readJson(res) {
568
+ const type = (res.headers.get("content-type") ?? "").toLowerCase();
569
+ if (type && !type.includes("json")) return null;
570
+ try {
571
+ return await res.json();
572
+ } catch {
573
+ return null;
574
+ }
575
+ }
576
+
577
+ // src/providers/cursor/activity.ts
578
+ import { join as join3 } from "path";
579
+ import { homedir as homedir2 } from "os";
580
+
581
+ // src/providers/cursor/sqlite.ts
582
+ import { execFile as execFileCb } from "child_process";
583
+ import { promisify } from "util";
584
+ var execFile = promisify(execFileCb);
585
+ var nativeDb;
586
+ async function getNativeDb() {
587
+ if (nativeDb !== void 0) return nativeDb;
588
+ try {
589
+ nativeDb = (await import("sqlite")).DatabaseSync;
590
+ } catch {
591
+ nativeDb = null;
592
+ }
593
+ return nativeDb;
594
+ }
595
+ function classify(msg) {
596
+ if (/unable to open|no such file|cannot open|ENOENT/i.test(msg)) return "missing";
597
+ if (/database is (locked|busy)|readonly/i.test(msg)) return "locked";
598
+ if (/no such (function|table|column)|unknown option/i.test(msg)) return "old";
599
+ return "error";
600
+ }
601
+ async function runSqlite(db, sql, params = []) {
602
+ const DB = await getNativeDb();
603
+ if (DB) {
604
+ let handle;
605
+ try {
606
+ handle = new DB(db, { readOnly: true, timeout: 1500 });
607
+ const rows = handle.prepare(sql).all(...params);
608
+ return { status: "ok", rows };
609
+ } catch {
610
+ } finally {
611
+ try {
612
+ handle?.close();
613
+ } catch {
614
+ }
615
+ }
616
+ }
617
+ return runSqliteCli(db, sql, params);
618
+ }
619
+ function inlineParams(sql, params) {
620
+ let i = 0;
621
+ return sql.replace(/\?/g, () => {
622
+ const p = params[i++];
623
+ return typeof p === "number" ? String(p) : `'${String(p).replace(/'/g, "''")}'`;
624
+ });
625
+ }
626
+ async function runSqliteCli(db, sql, params) {
627
+ try {
628
+ const { stdout } = await execFile(
629
+ "sqlite3",
630
+ ["-readonly", "-json", "-cmd", ".timeout 1500", db, inlineParams(sql, params)],
631
+ { timeout: 1e4, maxBuffer: 8 << 20 }
632
+ );
633
+ const text = stdout.trim();
634
+ if (!text) return { status: "ok", rows: [] };
635
+ try {
636
+ return { status: "ok", rows: JSON.parse(text) };
637
+ } catch {
638
+ return { status: "error", rows: [] };
639
+ }
640
+ } catch (e) {
641
+ const err = e;
642
+ if (err?.code === "ENOENT") return { status: "missing", rows: [] };
643
+ return { status: classify(String(err?.stderr ?? err?.message ?? "")), rows: [] };
644
+ }
645
+ }
646
+ function sqliteStatusMessage(status) {
647
+ switch (status) {
648
+ case "missing":
649
+ return "Cursor data not found \u2014 open Cursor";
650
+ case "old":
651
+ return "Cursor DB unreadable";
652
+ case "locked":
653
+ return "Cursor DB busy \u2014 retrying next poll";
654
+ default:
655
+ return "Cursor data unavailable";
656
+ }
657
+ }
658
+
659
+ // src/providers/cursor/activity.ts
660
+ var DAY_MS2 = 864e5;
661
+ function trackingDb(homeDir) {
662
+ return join3(homeDir ?? homedir2(), ".cursor", "ai-tracking", "ai-code-tracking.db");
663
+ }
664
+ function localDayKey(ms) {
665
+ const d = new Date(ms);
666
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
667
+ }
668
+ async function cursorActivity(homeDir) {
669
+ const db = trackingDb(homeDir);
670
+ try {
671
+ const now = Date.now();
672
+ const res = await runSqlite(
673
+ db,
674
+ `SELECT date(createdAt/1000,'unixepoch','localtime') AS d, count(*) AS c FROM ai_code_hashes WHERE source!='human' AND createdAt >= ${Math.floor(now - 30 * DAY_MS2)} GROUP BY d;`
675
+ );
676
+ if (res.status !== "ok") return null;
677
+ const byDay = /* @__PURE__ */ new Map();
678
+ let month = 0;
679
+ for (const row of res.rows) {
680
+ const n = Number(row.c) || 0;
681
+ byDay.set(String(row.d), n);
682
+ month += n;
683
+ }
684
+ const series = [];
685
+ for (let i = SPARK_DAYS - 1; i >= 0; i--) series.push(byDay.get(localDayKey(now - i * DAY_MS2)) ?? 0);
686
+ if (month === 0 && series.every((v) => v === 0)) return null;
687
+ return { series, summary: `${tokens(month)} lines` };
688
+ } catch {
689
+ return null;
690
+ }
691
+ }
692
+
693
+ // src/providers/cursor/billing.ts
694
+ var BASE = "https://api2.cursor.sh/aiserver.v1.DashboardService";
695
+ var USAGE_URL = `${BASE}/GetCurrentPeriodUsage`;
696
+ var PLAN_URL = `${BASE}/GetPlanInfo`;
697
+ function cursorStateDb(homeDir) {
698
+ const base = homeDir ?? homedir3();
699
+ const tail = ["Cursor", "User", "globalStorage", "state.vscdb"];
700
+ if (process.platform === "darwin") {
701
+ return join4(base, "Library", "Application Support", ...tail);
702
+ }
703
+ if (process.platform === "win32") {
704
+ const roaming = homeDir ? join4(homeDir, "AppData", "Roaming") : envDir("APPDATA") ?? join4(base, "AppData", "Roaming");
705
+ return join4(roaming, ...tail);
706
+ }
707
+ const cfg = homeDir ? join4(homeDir, ".config") : envDir("XDG_CONFIG_HOME") ?? join4(base, ".config");
708
+ return join4(cfg, ...tail);
709
+ }
710
+ async function detectCursor(homeDir) {
711
+ try {
712
+ await access(cursorStateDb(homeDir));
713
+ return true;
714
+ } catch {
715
+ return false;
716
+ }
717
+ }
718
+ async function readState(db, key) {
719
+ const r = await runSqlite(db, "SELECT value FROM ItemTable WHERE key=? LIMIT 1;", [key]);
720
+ const raw = r.status === "ok" ? r.rows[0]?.value : void 0;
721
+ return { value: typeof raw === "string" && raw.trim() ? raw.trim() : null, status: r.status };
722
+ }
723
+ async function connectPost(url, token) {
724
+ try {
725
+ const res = await fetch(url, {
726
+ method: "POST",
727
+ headers: {
728
+ "Authorization": `Bearer ${token}`,
729
+ "Content-Type": "application/json",
730
+ "Connect-Protocol-Version": "1",
731
+ "User-Agent": "tokmon"
732
+ },
733
+ body: "{}",
734
+ signal: AbortSignal.timeout(1e4)
735
+ });
736
+ if (!res.ok) return { __status: res.status };
737
+ return await readJson(res);
738
+ } catch {
739
+ return null;
740
+ }
741
+ }
742
+ var dollars = (cents) => cents / 100;
743
+ async function cursorBilling(account) {
744
+ const [core, activity, spend] = await Promise.all([
745
+ cursorBillingCore(account),
746
+ cursorActivity(account.homeDir),
747
+ cursorModelSpend(account.homeDir)
748
+ ]);
749
+ let merged = activity;
750
+ if (spend) {
751
+ const lines = activity?.summary ?? "";
752
+ const spendLabel = `$${Math.round(spend.total)} all-time`;
753
+ merged = {
754
+ series: activity?.series ?? [],
755
+ summary: lines ? `${lines} \xB7 ${spendLabel}` : spendLabel
756
+ };
757
+ }
758
+ return { ...core, activity: merged };
759
+ }
760
+ async function cursorBillingCore(account) {
761
+ const db = cursorStateDb(account.homeDir);
762
+ const [tokenRes, membershipRes] = await Promise.all([
763
+ readState(db, "cursorAuth/accessToken"),
764
+ readState(db, "cursorAuth/stripeMembershipType")
765
+ ]);
766
+ const token = tokenRes.value;
767
+ const membership = membershipRes.value;
768
+ const planFallback = membership ? membership.charAt(0).toUpperCase() + membership.slice(1) : null;
769
+ if (!token) {
770
+ const error = tokenRes.status === "ok" ? "Not signed in \u2014 open Cursor" : sqliteStatusMessage(tokenRes.status);
771
+ return { plan: planFallback, metrics: [], error };
772
+ }
773
+ const [usage, planInfo] = await Promise.all([
774
+ connectPost(USAGE_URL, token),
775
+ connectPost(PLAN_URL, token)
776
+ ]);
777
+ if (!usage || usage.__status) {
778
+ const expired = usage?.__status === 401 || usage?.__status === 403;
779
+ return { plan: planFallback, metrics: [], error: expired ? "Token expired \u2014 re-open Cursor" : "Cursor API error" };
780
+ }
781
+ const planName = planInfo?.planInfo?.planName ?? planFallback;
782
+ const price = planInfo?.planInfo?.price;
783
+ const plan = planName ? price ? `${planName} \xB7 ${price}` : planName : null;
784
+ const pu = usage.planUsage ?? {};
785
+ const metrics = [];
786
+ const rawEnd = usage.billingCycleEnd;
787
+ const endMs = typeof rawEnd === "string" && rawEnd.trim() ? Number(rawEnd) : NaN;
788
+ const resets = Number.isFinite(endMs) && endMs > 0 && endMs <= 864e13 ? resetIn(new Date(endMs).toISOString()) : null;
789
+ if (typeof pu.totalPercentUsed === "number" && typeof pu.limit === "number") {
790
+ metrics.push({
791
+ label: "Usage",
792
+ used: pu.totalPercentUsed,
793
+ limit: 100,
794
+ format: { kind: "percent" },
795
+ resetsAt: resets,
796
+ primary: true
797
+ });
798
+ const spentCents = typeof pu.totalSpend === "number" ? pu.totalSpend : pu.limit - (pu.remaining ?? 0);
799
+ metrics.push({
800
+ label: "Spend",
801
+ used: dollars(spentCents),
802
+ limit: dollars(pu.limit),
803
+ format: { kind: "dollars" }
804
+ });
805
+ }
806
+ if (pu.autoPercentUsed) {
807
+ metrics.push({ label: "Auto", used: pu.autoPercentUsed, limit: 100, format: { kind: "percent" } });
808
+ }
809
+ if (pu.apiPercentUsed) {
810
+ metrics.push({ label: "API", used: pu.apiPercentUsed, limit: 100, format: { kind: "percent" } });
811
+ }
812
+ const su = usage.spendLimitUsage;
813
+ if (su) {
814
+ const limitCents = su.individualLimit ?? su.pooledLimit ?? 0;
815
+ const remainingCents = su.individualRemaining ?? su.pooledRemaining ?? 0;
816
+ if (limitCents > 0) {
817
+ metrics.push({
818
+ label: "On-demand",
819
+ used: dollars(limitCents - remainingCents),
820
+ limit: dollars(limitCents),
821
+ format: { kind: "dollars" }
822
+ });
823
+ }
824
+ }
825
+ if (metrics.length === 0) {
826
+ return { plan, metrics: [], error: usage.enabled === false ? "No active subscription" : "No usage data" };
827
+ }
828
+ return { plan, metrics, error: null };
829
+ }
830
+
831
+ // src/providers/cursor/composer.ts
832
+ async function cursorModelSpend(homeDir) {
833
+ const db = cursorStateDb(homeDir);
834
+ const sql = "SELECT mk.key AS name, sum(json_extract(mk.value,'$.costInCents')) AS cents, sum(json_extract(mk.value,'$.amount')) AS amt FROM cursorDiskKV c, json_each(c.value,'$.usageData') mk WHERE c.key LIKE 'composerData:%' AND json_valid(c.value) AND json_type(c.value,'$.usageData')='object' GROUP BY mk.key ORDER BY cents DESC;";
835
+ const res = await runSqlite(db, sql);
836
+ if (res.status !== "ok") return null;
837
+ const models = [];
838
+ let total = 0;
839
+ for (const row of res.rows) {
840
+ const usd = (Number(row.cents) || 0) / 100;
841
+ if (usd <= 0) continue;
842
+ models.push({ name: String(row.name ?? ""), usd, requests: Number(row.amt) || 0 });
843
+ total += usd;
844
+ }
845
+ if (total <= 0) return null;
846
+ return { total, models };
847
+ }
848
+
849
+ // src/providers/claude/usage.ts
850
+ import { readdir, stat as fsStat, access as access2 } from "fs/promises";
851
+ import { createReadStream } from "fs";
852
+ import { createInterface } from "readline";
853
+ import { join as join5, isAbsolute as isAbsolute2 } from "path";
854
+ import { homedir as homedir4 } from "os";
855
+ var PRICING = {
856
+ "claude-opus-4-1": { i: 15e-6, o: 75e-6, cc: 1875e-8, cr: 15e-7 },
857
+ "claude-opus-4-0": { i: 15e-6, o: 75e-6, cc: 1875e-8, cr: 15e-7 },
858
+ "claude-opus-4-20250514": { i: 15e-6, o: 75e-6, cc: 1875e-8, cr: 15e-7 },
859
+ "claude-opus-4": { i: 5e-6, o: 25e-6, cc: 625e-8, cr: 5e-7 },
860
+ "claude-3-opus": { i: 15e-6, o: 75e-6, cc: 1875e-8, cr: 15e-7 },
861
+ "claude-sonnet-4": { i: 3e-6, o: 15e-6, cc: 375e-8, cr: 3e-7 },
862
+ "claude-haiku-4": { i: 1e-6, o: 5e-6, cc: 125e-8, cr: 1e-7 },
863
+ "claude-fable-5": { i: 1e-5, o: 5e-5, cc: 125e-7, cr: 1e-6 }
864
+ };
865
+ var PRICE_KEYS = Object.keys(PRICING).sort((a, b) => b.length - a.length);
866
+ var FALLBACK = PRICING["claude-opus-4"];
867
+ function claudeConfigDirs(homeDir) {
868
+ if (homeDir) {
869
+ return [join5(homeDir, ".claude"), join5(homeDir, ".config", "claude")];
870
+ }
871
+ const home = homedir4();
872
+ const dirs = [join5(home, ".claude")];
873
+ const xdg = envDir("XDG_CONFIG_HOME");
874
+ if (xdg) {
875
+ dirs.push(join5(xdg, "claude"));
876
+ } else if (process.platform !== "win32") {
877
+ dirs.push(join5(home, ".config", "claude"));
878
+ }
879
+ const appData = envDir("APPDATA");
880
+ if (appData) dirs.push(join5(appData, "claude"));
881
+ if (process.env.CLAUDE_CONFIG_DIR) {
882
+ for (const p of process.env.CLAUDE_CONFIG_DIR.split(process.platform === "win32" ? ";" : ",")) {
883
+ const t = p.trim();
884
+ if (t && isAbsolute2(t)) dirs.push(t);
885
+ }
886
+ }
887
+ return [...new Set(dirs)];
888
+ }
889
+ function getClaudeDirs(homeDir) {
890
+ return claudeConfigDirs(homeDir).map((d) => join5(d, "projects"));
891
+ }
892
+ async function detectClaude(homeDir) {
893
+ for (const dir of getClaudeDirs(homeDir)) {
894
+ try {
895
+ await access2(dir);
896
+ return true;
897
+ } catch {
898
+ }
899
+ }
900
+ return false;
901
+ }
902
+ function priceFor(model) {
903
+ for (const key of PRICE_KEYS) {
904
+ if (!model.startsWith(key)) continue;
905
+ const rest = model.slice(key.length);
906
+ if (rest === "" || rest[0] === "-") return PRICING[key];
907
+ }
908
+ return FALLBACK;
909
+ }
910
+ function costOf(model, u) {
911
+ const p = priceFor(model);
912
+ return safeNum(u.input_tokens) * p.i + safeNum(u.output_tokens) * p.o + safeNum(u.cache_creation_input_tokens) * p.cc + safeNum(u.cache_read_input_tokens) * p.cr;
913
+ }
914
+ function shortModel(model) {
915
+ return model.replace("claude-", "").replace(/-\d{8}$/, "");
916
+ }
917
+ async function parseFile(path) {
918
+ const entries = [];
919
+ const rl = createInterface({ input: createReadStream(path), crlfDelay: Infinity });
920
+ for await (const line of rl) {
921
+ if (!line.includes('"usage"')) continue;
922
+ try {
923
+ const obj = JSON.parse(line.charCodeAt(0) === 65279 ? line.slice(1) : line);
924
+ if (obj.type !== "assistant" || !obj.message?.usage) continue;
925
+ const ts = new Date(obj.timestamp ?? 0).getTime();
926
+ if (!Number.isFinite(ts)) continue;
927
+ const u = obj.message.usage;
928
+ const model = typeof obj.message.model === "string" && obj.message.model ? obj.message.model : "unknown";
929
+ const input = safeNum(u.input_tokens);
930
+ const output = safeNum(u.output_tokens);
931
+ const cacheCreate = safeNum(u.cache_creation_input_tokens);
932
+ const cacheRead = safeNum(u.cache_read_input_tokens);
933
+ if (input + output + cacheCreate + cacheRead === 0) continue;
934
+ const p = priceFor(model);
935
+ const msgId = obj.message?.id;
936
+ entries.push({
937
+ id: msgId ? msgId + (obj.requestId ? ":" + obj.requestId : "") : void 0,
938
+ ts,
939
+ model: shortModel(model),
940
+ cost: costOf(model, u),
941
+ input,
942
+ output,
943
+ cacheCreate,
944
+ cacheRead,
945
+ cacheSavings: cacheRead * (p.i - p.cr)
946
+ });
947
+ } catch {
948
+ }
949
+ }
950
+ return entries;
951
+ }
952
+ async function loadEntries(since, homeDir) {
953
+ const files = [];
954
+ const seen = /* @__PURE__ */ new Set();
955
+ const seenIno = /* @__PURE__ */ new Set();
956
+ for (const dir of getClaudeDirs(homeDir)) {
957
+ let listing;
958
+ try {
959
+ listing = await readdir(dir, { recursive: true });
960
+ } catch {
961
+ continue;
962
+ }
963
+ for (const f of listing) {
964
+ if (!f.endsWith(".jsonl")) continue;
965
+ const path = join5(dir, f);
966
+ if (seen.has(path)) continue;
967
+ seen.add(path);
968
+ try {
969
+ const s = await fsStat(path);
970
+ if (s.mtimeMs < since) continue;
971
+ if (s.ino && process.platform !== "win32") {
972
+ const idn = `${s.dev}:${s.ino}`;
973
+ if (seenIno.has(idn)) continue;
974
+ seenIno.add(idn);
975
+ }
976
+ files.push({ path, mtimeMs: s.mtimeMs, size: s.size });
977
+ } catch {
978
+ }
979
+ }
980
+ }
981
+ return loadCachedEntries(files, parseFile, since);
982
+ }
983
+ async function claudeDashboard(tz, homeDir) {
984
+ const now = Date.now();
985
+ const since = Math.min(startOfMonth(now, tz), startOfWeek(now, tz), now - SPARK_DAYS * 864e5);
986
+ const entries = await loadEntries(since, homeDir);
987
+ return summarize(entries, tz);
988
+ }
989
+ async function claudeTable(tz, homeDir) {
990
+ const entries = await loadEntries(monthsAgoStart(Date.now(), 6, tz), homeDir);
991
+ return tabulate(entries, tz);
992
+ }
993
+
994
+ // src/providers/claude/billing.ts
995
+ import { execFile as execFileCb2 } from "child_process";
996
+ import { readFile as readFile3 } from "fs/promises";
997
+ import { join as join6 } from "path";
998
+ import { homedir as homedir5 } from "os";
999
+ import { promisify as promisify2 } from "util";
1000
+ var execFile2 = promisify2(execFileCb2);
1001
+ function parseAuth(raw) {
1002
+ try {
1003
+ const creds = JSON.parse(raw);
1004
+ const o = creds?.claudeAiOauth ?? creds;
1005
+ const token = o?.accessToken;
1006
+ if (typeof token !== "string" || !token) return null;
1007
+ return {
1008
+ token,
1009
+ subscriptionType: typeof o.subscriptionType === "string" ? o.subscriptionType : void 0,
1010
+ rateLimitTier: typeof o.rateLimitTier === "string" ? o.rateLimitTier : void 0
1011
+ };
1012
+ } catch {
1013
+ return null;
1014
+ }
1015
+ }
1016
+ async function readCredentialsFile(homeDir) {
1017
+ for (const dir of claudeConfigDirs(homeDir)) {
1018
+ try {
1019
+ const auth = parseAuth(await readFile3(join6(dir, ".credentials.json"), "utf-8"));
1020
+ if (auth) return auth;
1021
+ } catch {
1022
+ }
1023
+ }
1024
+ return null;
1025
+ }
1026
+ async function readMacKeychain() {
1027
+ try {
1028
+ const { stdout } = await execFile2("security", [
1029
+ "find-generic-password",
1030
+ "-s",
1031
+ "Claude Code-credentials",
1032
+ "-w"
1033
+ ], { timeout: 5e3 });
1034
+ return parseAuth(stdout.trim());
1035
+ } catch {
1036
+ return null;
1037
+ }
1038
+ }
1039
+ async function getAuth(homeDir) {
1040
+ const isDefault = !homeDir || homeDir === homedir5();
1041
+ if (isDefault && process.platform === "darwin") {
1042
+ const auth = await readMacKeychain();
1043
+ if (auth) return auth;
1044
+ }
1045
+ return readCredentialsFile(homeDir);
1046
+ }
1047
+ function planLabel(auth) {
1048
+ const sub = auth.subscriptionType;
1049
+ if (!sub) return null;
1050
+ const base = sub.charAt(0).toUpperCase() + sub.slice(1);
1051
+ const tier = (auth.rateLimitTier ?? "").match(/(\d+)x/);
1052
+ return tier ? `${base} ${tier[1]}x` : base;
1053
+ }
1054
+ var pct = (used, resets, primary) => ({ label: "", used, limit: 100, format: { kind: "percent" }, resetsAt: resets ?? null, primary });
1055
+ async function claudeBilling(account) {
1056
+ const auth = await getAuth(account.homeDir);
1057
+ if (!auth) return { plan: null, metrics: [], error: "No OAuth token \u2014 run claude and log in" };
1058
+ const plan = planLabel(auth);
1059
+ try {
1060
+ const res = await fetch("https://api.anthropic.com/api/oauth/usage", {
1061
+ headers: {
1062
+ "Authorization": `Bearer ${auth.token}`,
1063
+ "anthropic-beta": "oauth-2025-04-20",
1064
+ "User-Agent": "tokmon"
1065
+ },
1066
+ signal: AbortSignal.timeout(1e4)
1067
+ });
1068
+ if (res.status === 429) return { plan, metrics: [], error: "Rate limited \u2014 retrying next poll" };
1069
+ if (res.status === 401) return { plan, metrics: [], error: "Token expired \u2014 restart Claude Code" };
1070
+ if (!res.ok) return { plan, metrics: [], error: `API ${res.status}` };
1071
+ const data = await readJson(res);
1072
+ if (!data) return { plan, metrics: [], error: "Unexpected API response" };
1073
+ const metrics = [];
1074
+ if (data.five_hour) {
1075
+ metrics.push({ ...pct(data.five_hour.utilization, resetIn(data.five_hour.resets_at), true), label: "5h" });
1076
+ }
1077
+ if (data.seven_day) {
1078
+ metrics.push({ ...pct(data.seven_day.utilization, resetIn(data.seven_day.resets_at)), label: "Week" });
1079
+ }
1080
+ if (data.seven_day_sonnet) {
1081
+ metrics.push({ ...pct(data.seven_day_sonnet.utilization), label: "Sonnet" });
1082
+ }
1083
+ if (data.extra_usage?.is_enabled) {
1084
+ metrics.push({
1085
+ label: "Extra",
1086
+ used: (data.extra_usage.used_credits ?? 0) / 100,
1087
+ limit: data.extra_usage.monthly_limit != null ? data.extra_usage.monthly_limit / 100 : null,
1088
+ format: { kind: "dollars", currency: data.extra_usage.currency ?? "USD" }
1089
+ });
1090
+ }
1091
+ return { plan, metrics, error: null };
1092
+ } catch {
1093
+ return { plan, metrics: [], error: "Network error" };
1094
+ }
1095
+ }
1096
+
1097
+ // src/providers/claude/index.ts
1098
+ var claudeProvider = {
1099
+ id: "claude",
1100
+ name: "Claude",
1101
+ color: "green",
1102
+ hasUsage: true,
1103
+ hasBilling: true,
1104
+ detect: (homeDir) => detectClaude(homeDir),
1105
+ fetchSummary: (account, tz) => claudeDashboard(tz, account.homeDir),
1106
+ fetchTable: (account, tz) => claudeTable(tz, account.homeDir),
1107
+ fetchBilling: (account) => claudeBilling(account)
1108
+ };
1109
+
1110
+ // src/providers/codex/usage.ts
1111
+ import { readdir as readdir2, stat as fsStat2, access as access3 } from "fs/promises";
1112
+ import { createReadStream as createReadStream2 } from "fs";
1113
+ import { createInterface as createInterface2 } from "readline";
1114
+ import { join as join7 } from "path";
1115
+ import { homedir as homedir6 } from "os";
1116
+ var PRICING2 = {
1117
+ "gpt-5-codex": { in: 125e-8, cr: 125e-9, out: 1e-5 },
1118
+ "gpt-5-mini": { in: 25e-8, cr: 25e-9, out: 2e-6 },
1119
+ "gpt-5-nano": { in: 5e-8, cr: 5e-9, out: 4e-7 },
1120
+ "gpt-5": { in: 125e-8, cr: 125e-9, out: 1e-5 },
1121
+ "o4-mini": { in: 11e-7, cr: 275e-9, out: 44e-7 }
1122
+ };
1123
+ var FALLBACK2 = PRICING2["gpt-5-codex"];
1124
+ var PRICE_KEYS2 = Object.keys(PRICING2).sort((a, b) => b.length - a.length);
1125
+ function codexHomes(homeDir) {
1126
+ if (homeDir) return [join7(homeDir, ".codex")];
1127
+ const homes = [];
1128
+ const codexHome = envDir("CODEX_HOME");
1129
+ if (codexHome) homes.push(codexHome);
1130
+ homes.push(join7(homedir6(), ".codex"));
1131
+ homes.push(join7(homedir6(), ".config", "codex"));
1132
+ return [...new Set(homes)];
1133
+ }
1134
+ async function detectCodex(homeDir) {
1135
+ for (const home of codexHomes(homeDir)) {
1136
+ try {
1137
+ await access3(join7(home, "sessions"));
1138
+ return true;
1139
+ } catch {
1140
+ }
1141
+ }
1142
+ return false;
1143
+ }
1144
+ function priceFor2(model) {
1145
+ const m = model.toLowerCase();
1146
+ for (const key of PRICE_KEYS2) {
1147
+ if (m.startsWith(key) || m.includes(key)) return PRICING2[key];
1148
+ }
1149
+ return FALLBACK2;
1150
+ }
1151
+ function extractModel(obj) {
1152
+ const p = obj?.payload ?? obj;
1153
+ return p?.model || p?.collaboration_mode?.settings?.model || p?.model_slug || p?.config?.model || p?.info?.model || null;
1154
+ }
1155
+ function subtractClamped(cur, prev) {
1156
+ const sub = (a, b) => Math.max(0, (a ?? 0) - (b ?? 0));
1157
+ return {
1158
+ input_tokens: sub(cur.input_tokens, prev?.input_tokens),
1159
+ cached_input_tokens: sub(cur.cached_input_tokens, prev?.cached_input_tokens),
1160
+ output_tokens: sub(cur.output_tokens, prev?.output_tokens),
1161
+ reasoning_output_tokens: sub(cur.reasoning_output_tokens, prev?.reasoning_output_tokens)
1162
+ };
1163
+ }
1164
+ function eventSig(last, total) {
1165
+ const f = (x) => x ? `${x.input_tokens ?? 0},${x.cached_input_tokens ?? 0},${x.output_tokens ?? 0},${x.reasoning_output_tokens ?? 0}` : "-";
1166
+ return `${f(last)}|${f(total)}`;
1167
+ }
1168
+ async function parseFile2(path) {
1169
+ const entries = [];
1170
+ let model = "gpt-5-codex";
1171
+ let prevTotal = null;
1172
+ let prevSig = null;
1173
+ const rl = createInterface2({ input: createReadStream2(path), crlfDelay: Infinity });
1174
+ for await (const rawLine of rl) {
1175
+ if (!rawLine.includes("token_count") && !rawLine.includes("turn_context")) continue;
1176
+ try {
1177
+ const line = rawLine.charCodeAt(0) === 65279 ? rawLine.slice(1) : rawLine;
1178
+ const obj = JSON.parse(line);
1179
+ const payloadType = obj?.payload?.type ?? obj?.type;
1180
+ if (payloadType === "turn_context") {
1181
+ const m = extractModel(obj);
1182
+ if (typeof m === "string" && m.trim()) model = m;
1183
+ continue;
1184
+ }
1185
+ if (payloadType !== "token_count") continue;
1186
+ const info = obj?.payload?.info;
1187
+ const total = info?.total_token_usage;
1188
+ const last = info?.last_token_usage;
1189
+ const sig = eventSig(last, total);
1190
+ if (sig === prevSig) continue;
1191
+ prevSig = sig;
1192
+ let d = last;
1193
+ if (!d && total) {
1194
+ const reset = !!prevTotal && (total.input_tokens ?? 0) < (prevTotal.input_tokens ?? 0);
1195
+ d = reset ? total : subtractClamped(total, prevTotal);
1196
+ }
1197
+ if (total) prevTotal = total;
1198
+ if (!d) continue;
1199
+ const ts = new Date(obj.timestamp ?? obj?.payload?.timestamp ?? 0).getTime();
1200
+ if (!Number.isFinite(ts)) continue;
1201
+ const inputTotal = safeNum(d.input_tokens);
1202
+ const cached = Math.min(safeNum(d.cached_input_tokens), inputTotal);
1203
+ const input = inputTotal - cached;
1204
+ const output = safeNum(d.output_tokens);
1205
+ if (input + output + cached === 0) continue;
1206
+ const p = priceFor2(model);
1207
+ entries.push({
1208
+ ts,
1209
+ model,
1210
+ cost: input * p.in + cached * p.cr + output * p.out,
1211
+ input,
1212
+ output,
1213
+ cacheCreate: 0,
1214
+ cacheRead: cached,
1215
+ cacheSavings: cached * (p.in - p.cr)
1216
+ });
1217
+ } catch {
1218
+ }
1219
+ }
1220
+ return entries;
1221
+ }
1222
+ async function loadEntries2(since, homeDir) {
1223
+ const files = [];
1224
+ const seen = /* @__PURE__ */ new Set();
1225
+ const seenIno = /* @__PURE__ */ new Set();
1226
+ for (const home of codexHomes(homeDir)) {
1227
+ const dir = join7(home, "sessions");
1228
+ let listing;
1229
+ try {
1230
+ listing = await readdir2(dir, { recursive: true });
1231
+ } catch {
1232
+ continue;
1233
+ }
1234
+ for (const f of listing) {
1235
+ if (!f.endsWith(".jsonl") || !f.includes("rollout-")) continue;
1236
+ const path = join7(dir, f);
1237
+ if (seen.has(path)) continue;
1238
+ seen.add(path);
1239
+ try {
1240
+ const s = await fsStat2(path);
1241
+ if (s.mtimeMs < since) continue;
1242
+ if (s.ino && process.platform !== "win32") {
1243
+ const idn = `${s.dev}:${s.ino}`;
1244
+ if (seenIno.has(idn)) continue;
1245
+ seenIno.add(idn);
1246
+ }
1247
+ files.push({ path, mtimeMs: s.mtimeMs, size: s.size });
1248
+ } catch {
1249
+ }
1250
+ }
1251
+ }
1252
+ return loadCachedEntries(files, parseFile2, since);
1253
+ }
1254
+ async function codexDashboard(tz, homeDir) {
1255
+ const now = Date.now();
1256
+ const since = Math.min(startOfMonth(now, tz), startOfWeek(now, tz), now - SPARK_DAYS * 864e5);
1257
+ const entries = await loadEntries2(since, homeDir);
1258
+ return summarize(entries, tz);
1259
+ }
1260
+ async function codexTable(tz, homeDir) {
1261
+ const entries = await loadEntries2(monthsAgoStart(Date.now(), 6, tz), homeDir);
1262
+ return tabulate(entries, tz);
1263
+ }
1264
+
1265
+ // src/providers/codex/billing.ts
1266
+ import { execFile as execFileCb3 } from "child_process";
1267
+ import { readFile as readFile4, readdir as readdir3, stat as fsStat3 } from "fs/promises";
1268
+ import { createReadStream as createReadStream3 } from "fs";
1269
+ import { createInterface as createInterface3 } from "readline";
1270
+ import { join as join8 } from "path";
1271
+ import { promisify as promisify3 } from "util";
1272
+ var execFile3 = promisify3(execFileCb3);
1273
+ var USAGE_URL2 = "https://chatgpt.com/backend-api/wham/usage";
1274
+ var CREDIT_USD_RATE = 0.04;
1275
+ async function readAuthFile(home) {
1276
+ try {
1277
+ const raw = await readFile4(join8(home, "auth.json"), "utf-8");
1278
+ const auth = JSON.parse(raw);
1279
+ const accessToken = auth?.tokens?.access_token;
1280
+ if (!accessToken) return null;
1281
+ return { accessToken, accountId: auth?.tokens?.account_id };
1282
+ } catch {
1283
+ return null;
1284
+ }
1285
+ }
1286
+ async function readKeychainAuth() {
1287
+ try {
1288
+ const { stdout } = await execFile3("security", [
1289
+ "find-generic-password",
1290
+ "-s",
1291
+ "Codex Auth",
1292
+ "-w"
1293
+ ], { timeout: 5e3 });
1294
+ const auth = JSON.parse(stdout.trim());
1295
+ const accessToken = auth?.tokens?.access_token;
1296
+ if (!accessToken) return null;
1297
+ return { accessToken, accountId: auth?.tokens?.account_id };
1298
+ } catch {
1299
+ return null;
1300
+ }
1301
+ }
1302
+ async function getAuth2(homeDir) {
1303
+ for (const home of codexHomes(homeDir)) {
1304
+ const auth = await readAuthFile(home);
1305
+ if (auth) return auth;
1306
+ }
1307
+ if (process.platform === "darwin") return readKeychainAuth();
1308
+ return null;
1309
+ }
1310
+ function planLabel2(planType) {
1311
+ if (typeof planType !== "string" || !planType.trim()) return null;
1312
+ const p = planType.trim().toLowerCase();
1313
+ if (p === "prolite") return "Pro 5x";
1314
+ if (p === "pro") return "Pro 20x";
1315
+ return planType.charAt(0).toUpperCase() + planType.slice(1);
1316
+ }
1317
+ function isoOrNull(ms) {
1318
+ return Number.isFinite(ms) && Math.abs(ms) <= 864e13 ? new Date(ms).toISOString() : null;
1319
+ }
1320
+ function resetFrom(window) {
1321
+ if (!window) return null;
1322
+ let iso = null;
1323
+ if (typeof window.reset_at === "number") iso = isoOrNull(window.reset_at * 1e3);
1324
+ else if (typeof window.resets_at === "number") iso = isoOrNull(window.resets_at * 1e3);
1325
+ else if (typeof window.reset_after_seconds === "number") iso = isoOrNull(Date.now() + window.reset_after_seconds * 1e3);
1326
+ return iso ? resetIn(iso) : null;
1327
+ }
1328
+ function percentMetric(label, used, resets, primary) {
1329
+ return { label, used, limit: 100, format: { kind: "percent" }, resetsAt: resets, primary };
1330
+ }
1331
+ async function liveBilling(auth) {
1332
+ try {
1333
+ const headers = {
1334
+ "Authorization": `Bearer ${auth.accessToken}`,
1335
+ "Accept": "application/json",
1336
+ "User-Agent": "tokmon"
1337
+ };
1338
+ if (auth.accountId) headers["ChatGPT-Account-Id"] = auth.accountId;
1339
+ const res = await fetch(USAGE_URL2, { headers, signal: AbortSignal.timeout(1e4) });
1340
+ if (!res.ok) return null;
1341
+ const data = await readJson(res);
1342
+ if (!data) return null;
1343
+ const metrics = [];
1344
+ const rl = data.rate_limit ?? null;
1345
+ const primary = rl?.primary_window ?? null;
1346
+ const secondary = rl?.secondary_window ?? null;
1347
+ const headerPct = (name) => {
1348
+ const h = res.headers.get(name);
1349
+ if (h === null || h.trim() === "") return void 0;
1350
+ const n = Number(h);
1351
+ return Number.isFinite(n) ? n : void 0;
1352
+ };
1353
+ const primaryPct = headerPct("x-codex-primary-used-percent") ?? primary?.used_percent;
1354
+ const secondaryPct = headerPct("x-codex-secondary-used-percent") ?? secondary?.used_percent;
1355
+ if (typeof primaryPct === "number") metrics.push(percentMetric("5h", primaryPct, resetFrom(primary), true));
1356
+ if (typeof secondaryPct === "number") metrics.push(percentMetric("Week", secondaryPct, resetFrom(secondary)));
1357
+ const balance = data?.credits?.balance;
1358
+ if (typeof balance === "number" && balance >= 0) {
1359
+ metrics.push({ label: "Credits", used: balance * CREDIT_USD_RATE, limit: null, format: { kind: "dollars" } });
1360
+ }
1361
+ if (metrics.length === 0) return null;
1362
+ return { plan: planLabel2(data.plan_type), metrics, error: null };
1363
+ } catch {
1364
+ return null;
1365
+ }
1366
+ }
1367
+ async function newestRolloutFile(homeDir) {
1368
+ let best = null;
1369
+ for (const home of codexHomes(homeDir)) {
1370
+ const dir = join8(home, "sessions");
1371
+ let listing;
1372
+ try {
1373
+ listing = await readdir3(dir, { recursive: true });
1374
+ } catch {
1375
+ continue;
1376
+ }
1377
+ for (const f of listing) {
1378
+ if (!f.endsWith(".jsonl") || !f.includes("rollout-")) continue;
1379
+ const path = join8(dir, f);
1380
+ try {
1381
+ const s = await fsStat3(path);
1382
+ if (!best || s.mtimeMs > best.mtime) best = { path, mtime: s.mtimeMs };
1383
+ } catch {
1384
+ }
1385
+ }
1386
+ }
1387
+ return best?.path ?? null;
1388
+ }
1389
+ async function snapshotBilling(homeDir) {
1390
+ const path = await newestRolloutFile(homeDir);
1391
+ if (!path) return null;
1392
+ let last = null;
1393
+ try {
1394
+ const rl = createInterface3({ input: createReadStream3(path), crlfDelay: Infinity });
1395
+ for await (const line of rl) {
1396
+ if (!line.includes("rate_limits")) continue;
1397
+ try {
1398
+ const obj = JSON.parse(line);
1399
+ if (obj?.payload?.rate_limits) last = obj.payload.rate_limits;
1400
+ } catch {
1401
+ }
1402
+ }
1403
+ } catch {
1404
+ return null;
1405
+ }
1406
+ if (!last) return null;
1407
+ const metrics = [];
1408
+ if (typeof last.primary?.used_percent === "number") {
1409
+ metrics.push(percentMetric("5h", last.primary.used_percent, resetFrom(last.primary), true));
1410
+ }
1411
+ if (typeof last.secondary?.used_percent === "number") {
1412
+ metrics.push(percentMetric("Week", last.secondary.used_percent, resetFrom(last.secondary)));
1413
+ }
1414
+ const balance = last?.credits?.balance;
1415
+ if (typeof balance === "number" && balance >= 0) {
1416
+ metrics.push({ label: "Credits", used: balance * CREDIT_USD_RATE, limit: null, format: { kind: "dollars" } });
1417
+ }
1418
+ if (metrics.length === 0) return null;
1419
+ return { plan: planLabel2(last.plan_type), metrics, error: null };
1420
+ }
1421
+ async function codexBilling(account) {
1422
+ const auth = await getAuth2(account.homeDir);
1423
+ if (auth) {
1424
+ const live = await liveBilling(auth);
1425
+ if (live) return live;
1426
+ }
1427
+ const snap = await snapshotBilling(account.homeDir);
1428
+ if (snap) return snap;
1429
+ return {
1430
+ plan: null,
1431
+ metrics: [],
1432
+ error: auth ? "Usage API failed \u2014 run codex to refresh" : "Not logged in \u2014 run codex"
1433
+ };
1434
+ }
1435
+
1436
+ // src/providers/codex/index.ts
1437
+ var codexProvider = {
1438
+ id: "codex",
1439
+ name: "Codex",
1440
+ color: "cyan",
1441
+ hasUsage: true,
1442
+ hasBilling: true,
1443
+ detect: (homeDir) => detectCodex(homeDir),
1444
+ fetchSummary: (account, tz) => codexDashboard(tz, account.homeDir),
1445
+ fetchTable: (account, tz) => codexTable(tz, account.homeDir),
1446
+ fetchBilling: (account) => codexBilling(account)
1447
+ };
1448
+
1449
+ // src/providers/cursor/index.ts
1450
+ var cursorProvider = {
1451
+ id: "cursor",
1452
+ name: "Cursor",
1453
+ color: "magenta",
1454
+ hasUsage: false,
1455
+ // Cursor exposes spend/limits, not a token history
1456
+ hasBilling: true,
1457
+ detect: (homeDir) => detectCursor(homeDir),
1458
+ fetchBilling: (account) => cursorBilling(account)
1459
+ };
1460
+
1461
+ // src/providers/pi/usage.ts
1462
+ import { readdir as readdir4, stat as fsStat4, access as access4 } from "fs/promises";
1463
+ import { createReadStream as createReadStream4 } from "fs";
1464
+ import { createInterface as createInterface4 } from "readline";
1465
+ import { join as join9 } from "path";
1466
+ import { homedir as homedir7 } from "os";
1467
+ function piSessionsDir(homeDir) {
1468
+ return join9(homeDir ?? homedir7(), ".pi", "agent", "sessions");
1469
+ }
1470
+ async function detectPi(homeDir) {
1471
+ try {
1472
+ await access4(piSessionsDir(homeDir));
1473
+ return true;
1474
+ } catch {
1475
+ return false;
1476
+ }
1477
+ }
1478
+ function pos(v) {
1479
+ return typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0;
1480
+ }
1481
+ async function parseFile3(path) {
1482
+ const entries = [];
1483
+ const rl = createInterface4({ input: createReadStream4(path), crlfDelay: Infinity });
1484
+ for await (const rawLine of rl) {
1485
+ if (!rawLine.includes('"usage"')) continue;
1486
+ try {
1487
+ const line = rawLine.charCodeAt(0) === 65279 ? rawLine.slice(1) : rawLine;
1488
+ const obj = JSON.parse(line);
1489
+ if (obj?.type !== "message") continue;
1490
+ const msg = obj.message;
1491
+ if (msg?.role !== "assistant" || !msg?.usage) continue;
1492
+ const u = msg.usage;
1493
+ const ts = new Date(obj.timestamp ?? msg.timestamp ?? 0).getTime();
1494
+ if (!Number.isFinite(ts)) continue;
1495
+ const input = safeNum(u.input);
1496
+ const output = safeNum(u.output);
1497
+ const cacheRead = safeNum(u.cacheRead);
1498
+ const cacheCreate = safeNum(u.cacheWrite);
1499
+ if (input + output + cacheRead + cacheCreate === 0) continue;
1500
+ const c = u.cost ?? {};
1501
+ const costInput = pos(c.input);
1502
+ const cacheSavings = input > 0 && cacheRead > 0 ? Math.max(0, cacheRead * (costInput / input) - pos(c.cacheRead)) : 0;
1503
+ const model = typeof msg.responseModel === "string" && msg.responseModel || typeof msg.model === "string" && msg.model || "unknown";
1504
+ entries.push({
1505
+ ts,
1506
+ model,
1507
+ cost: pos(c.total),
1508
+ input,
1509
+ output,
1510
+ cacheCreate,
1511
+ cacheRead,
1512
+ cacheSavings
1513
+ });
1514
+ } catch {
1515
+ }
1516
+ }
1517
+ return entries;
1518
+ }
1519
+ async function loadEntries3(since, homeDir) {
1520
+ const dir = piSessionsDir(homeDir);
1521
+ const files = [];
1522
+ const seenIno = /* @__PURE__ */ new Set();
1523
+ let listing;
1524
+ try {
1525
+ listing = await readdir4(dir, { recursive: true });
1526
+ } catch {
1527
+ return [];
1528
+ }
1529
+ for (const f of listing) {
1530
+ if (!f.endsWith(".jsonl")) continue;
1531
+ const path = join9(dir, f);
1532
+ try {
1533
+ const s = await fsStat4(path);
1534
+ if (s.mtimeMs < since) continue;
1535
+ if (s.ino && process.platform !== "win32") {
1536
+ const idn = `${s.dev}:${s.ino}`;
1537
+ if (seenIno.has(idn)) continue;
1538
+ seenIno.add(idn);
1539
+ }
1540
+ files.push({ path, mtimeMs: s.mtimeMs, size: s.size });
1541
+ } catch {
1542
+ }
1543
+ }
1544
+ return loadCachedEntries(files, parseFile3, since);
1545
+ }
1546
+ async function piDashboard(tz, homeDir) {
1547
+ const now = Date.now();
1548
+ const since = Math.min(startOfMonth(now, tz), startOfWeek(now, tz), now - SPARK_DAYS * 864e5);
1549
+ return summarize(await loadEntries3(since, homeDir), tz);
1550
+ }
1551
+ async function piTable(tz, homeDir) {
1552
+ return tabulate(await loadEntries3(monthsAgoStart(Date.now(), 6, tz), homeDir), tz);
1553
+ }
1554
+
1555
+ // src/providers/pi/index.ts
1556
+ var piProvider = {
1557
+ id: "pi",
1558
+ name: "Pi",
1559
+ color: "blue",
1560
+ hasUsage: true,
1561
+ hasBilling: false,
1562
+ detect: (homeDir) => detectPi(homeDir),
1563
+ fetchSummary: (account, tz) => piDashboard(tz, account.homeDir),
1564
+ fetchTable: (account, tz) => piTable(tz, account.homeDir)
1565
+ };
1566
+
1567
+ // src/providers/opencode/usage.ts
1568
+ import { access as access5 } from "fs/promises";
1569
+ import { join as join10 } from "path";
1570
+ import { homedir as homedir8 } from "os";
1571
+ function opencodeDbPaths(homeDir) {
1572
+ const base = homeDir ?? homedir8();
1573
+ const paths = [];
1574
+ if (!homeDir && process.env.XDG_DATA_HOME) paths.push(join10(process.env.XDG_DATA_HOME, "opencode", "opencode.db"));
1575
+ paths.push(join10(base, ".local", "share", "opencode", "opencode.db"));
1576
+ if (process.platform === "darwin") paths.push(join10(base, "Library", "Application Support", "opencode", "opencode.db"));
1577
+ if (process.platform === "win32") {
1578
+ const lad = homeDir ? join10(homeDir, "AppData", "Local") : process.env.LOCALAPPDATA;
1579
+ if (lad) paths.push(join10(lad, "opencode", "opencode.db"));
1580
+ }
1581
+ return [...new Set(paths)];
1582
+ }
1583
+ async function findDb(homeDir) {
1584
+ for (const p of opencodeDbPaths(homeDir)) {
1585
+ try {
1586
+ await access5(p);
1587
+ return p;
1588
+ } catch {
1589
+ }
1590
+ }
1591
+ return null;
1592
+ }
1593
+ async function detectOpencode(homeDir) {
1594
+ return await findDb(homeDir) !== null;
1595
+ }
1596
+ var pos2 = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0;
1597
+ async function loadEntries4(since, homeDir) {
1598
+ const db = await findDb(homeDir);
1599
+ if (!db) return [];
1600
+ const sql = "SELECT time_created AS ts, json_extract(data,'$.modelID') AS model, json_extract(data,'$.cost') AS cost, json_extract(data,'$.tokens.input') AS input, json_extract(data,'$.tokens.output') AS output, json_extract(data,'$.tokens.reasoning') AS reasoning, json_extract(data,'$.tokens.cache.read') AS cacheRead, json_extract(data,'$.tokens.cache.write') AS cacheWrite FROM message WHERE json_valid(data) AND json_extract(data,'$.role')='assistant' AND json_type(data,'$.tokens')='object' AND time_created >= ?;";
1601
+ const res = await runSqlite(db, sql, [Math.floor(since)]);
1602
+ if (res.status !== "ok") return [];
1603
+ const entries = [];
1604
+ for (const row of res.rows) {
1605
+ const ts = pos2(row.ts);
1606
+ if (!ts) continue;
1607
+ const input = pos2(row.input);
1608
+ const output = pos2(row.output);
1609
+ const cacheRead = pos2(row.cacheRead);
1610
+ const cacheCreate = pos2(row.cacheWrite);
1611
+ if (input + output + cacheRead + cacheCreate === 0) continue;
1612
+ entries.push({
1613
+ ts,
1614
+ model: typeof row.model === "string" && row.model ? row.model : "unknown",
1615
+ cost: pos2(row.cost),
1616
+ input,
1617
+ output,
1618
+ cacheCreate,
1619
+ cacheRead,
1620
+ cacheSavings: 0
1621
+ });
1622
+ }
1623
+ return entries;
1624
+ }
1625
+ async function opencodeDashboard(tz, homeDir) {
1626
+ const now = Date.now();
1627
+ const since = Math.min(startOfMonth(now, tz), startOfWeek(now, tz), now - SPARK_DAYS * 864e5);
1628
+ return summarize(await loadEntries4(since, homeDir), tz);
1629
+ }
1630
+ async function opencodeTable(tz, homeDir) {
1631
+ return tabulate(await loadEntries4(monthsAgoStart(Date.now(), 6, tz), homeDir), tz);
1632
+ }
1633
+
1634
+ // src/providers/opencode/index.ts
1635
+ var opencodeProvider = {
1636
+ id: "opencode",
1637
+ name: "opencode",
1638
+ color: "yellow",
1639
+ hasUsage: true,
1640
+ hasBilling: false,
1641
+ detect: (homeDir) => detectOpencode(homeDir),
1642
+ fetchSummary: (account, tz) => opencodeDashboard(tz, account.homeDir),
1643
+ fetchTable: (account, tz) => opencodeTable(tz, account.homeDir)
1644
+ };
1645
+
1646
+ // src/providers/copilot/billing.ts
1647
+ import { execFile as execFileCb4 } from "child_process";
1648
+ import { access as access6, readFile as readFile5, readdir as readdir5 } from "fs/promises";
1649
+ import { join as join11 } from "path";
1650
+ import { homedir as homedir9 } from "os";
1651
+ import { promisify as promisify4 } from "util";
1652
+ var execFile4 = promisify4(execFileCb4);
1653
+ var USAGE_URL3 = "https://api.github.com/copilot_internal/user";
1654
+ var GH_KEYCHAIN_SERVICE = "gh:github.com";
1655
+ var GO_KEYRING_PREFIX = "go-keyring-base64:";
1656
+ function ghConfigDir(homeDir) {
1657
+ if (!homeDir) {
1658
+ const explicit = process.env.GH_CONFIG_DIR;
1659
+ if (explicit && explicit.trim()) return explicit.trim();
1660
+ if (process.platform === "win32") {
1661
+ return join11(envDir("APPDATA") ?? join11(homedir9(), "AppData", "Roaming"), "GitHub CLI");
1662
+ }
1663
+ const xdg = envDir("XDG_CONFIG_HOME");
1664
+ return xdg ? join11(xdg, "gh") : join11(homedir9(), ".config", "gh");
1665
+ }
1666
+ return process.platform === "win32" ? join11(homeDir, "AppData", "Roaming", "GitHub CLI") : join11(homeDir, ".config", "gh");
1667
+ }
1668
+ function ghHostsPath(homeDir) {
1669
+ return join11(ghConfigDir(homeDir), "hosts.yml");
1670
+ }
1671
+ async function detectCopilot(homeDir) {
1672
+ try {
1673
+ await access6(ghHostsPath(homeDir));
1674
+ return true;
1675
+ } catch {
1676
+ }
1677
+ try {
1678
+ await execFile4("gh", ["--version"], { timeout: 3e3 });
1679
+ return true;
1680
+ } catch {
1681
+ return false;
1682
+ }
1683
+ }
1684
+ function unquoteYamlValue(value) {
1685
+ const trimmed = value.trim();
1686
+ if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
1687
+ return trimmed.slice(1, -1);
1688
+ }
1689
+ return trimmed;
1690
+ }
1691
+ function tokenFromHostsYaml(raw) {
1692
+ const lines = raw.split(/\r?\n/);
1693
+ let inGithub = false;
1694
+ let githubIndent = -1;
1695
+ for (const line of lines) {
1696
+ const match = line.match(/^(\s*)([^:#][^:]*):\s*(.*)$/);
1697
+ if (!match) continue;
1698
+ const indent = match[1].length;
1699
+ const key = match[2].trim();
1700
+ const value = match[3].trim();
1701
+ if (indent === 0) {
1702
+ inGithub = key === "github.com";
1703
+ githubIndent = inGithub ? indent : -1;
1704
+ continue;
1705
+ }
1706
+ if (inGithub && indent > githubIndent && key === "oauth_token" && value) {
1707
+ return unquoteYamlValue(value);
1708
+ }
1709
+ }
1710
+ return null;
1711
+ }
1712
+ async function loadTokenFromHosts(homeDir) {
1713
+ try {
1714
+ const token = tokenFromHostsYaml(await readFile5(ghHostsPath(homeDir), "utf-8"));
1715
+ return token ? { token, source: "gh-hosts" } : null;
1716
+ } catch {
1717
+ return null;
1718
+ }
1719
+ }
1720
+ async function readMacKeychainService(service) {
1721
+ if (process.platform !== "darwin") return null;
1722
+ try {
1723
+ const { stdout } = await execFile4("security", [
1724
+ "find-generic-password",
1725
+ "-s",
1726
+ service,
1727
+ "-w"
1728
+ ], { timeout: 5e3 });
1729
+ const raw = stdout.trim();
1730
+ if (!raw) return null;
1731
+ if (raw.startsWith(GO_KEYRING_PREFIX)) {
1732
+ return Buffer.from(raw.slice(GO_KEYRING_PREFIX.length), "base64").toString("utf-8");
1733
+ }
1734
+ return raw;
1735
+ } catch {
1736
+ return null;
1737
+ }
1738
+ }
1739
+ async function loadTokenFromGhKeychain() {
1740
+ const token = await readMacKeychainService(GH_KEYCHAIN_SERVICE);
1741
+ return token ? { token, source: "gh-keychain" } : null;
1742
+ }
1743
+ function vscodeUserDir(homeDir) {
1744
+ const home = homeDir ?? homedir9();
1745
+ if (process.platform === "darwin") return join11(home, "Library", "Application Support", "Code", "User");
1746
+ if (process.platform === "win32") return join11(home, "AppData", "Roaming", "Code", "User");
1747
+ return join11(home, ".config", "Code", "User");
1748
+ }
1749
+ function tokenFromText(raw) {
1750
+ const patterns = [
1751
+ /github\.com[^A-Za-z0-9_]+oauth_token[^A-Za-z0-9_]+([A-Za-z0-9_]{20,})/i,
1752
+ /github\.com[^A-Za-z0-9_]+(gh[opusr]_[A-Za-z0-9_]{20,})/i,
1753
+ /\b(gh[opusr]_[A-Za-z0-9_]{20,})\b/
1754
+ ];
1755
+ for (const pattern of patterns) {
1756
+ const token = raw.match(pattern)?.[1];
1757
+ if (token) return token;
1758
+ }
1759
+ return null;
1760
+ }
1761
+ async function loadTokenFromVsCode(homeDir) {
1762
+ const userDir = vscodeUserDir(homeDir);
1763
+ const candidates = [
1764
+ join11(userDir, "globalStorage", "github.copilot-chat", "auth.json"),
1765
+ join11(userDir, "globalStorage", "github.copilot", "auth.json"),
1766
+ join11(userDir, "globalStorage", "state.vscdb")
1767
+ ];
1768
+ try {
1769
+ for (const dirent of await readdir5(join11(userDir, "globalStorage"), { withFileTypes: true })) {
1770
+ if (dirent.isDirectory() && dirent.name.toLowerCase().includes("github")) {
1771
+ candidates.push(join11(userDir, "globalStorage", dirent.name, "auth.json"));
1772
+ }
1773
+ }
1774
+ } catch {
1775
+ }
1776
+ for (const path of candidates) {
1777
+ try {
1778
+ const token = tokenFromText(await readFile5(path, "utf-8"));
1779
+ if (token) return { token, source: "vscode" };
1780
+ } catch {
1781
+ }
1782
+ }
1783
+ return null;
1784
+ }
1785
+ async function loadToken(homeDir) {
1786
+ return await loadTokenFromHosts(homeDir) || await loadTokenFromGhKeychain() || await loadTokenFromVsCode(homeDir);
1787
+ }
1788
+ function redactToken(token) {
1789
+ return token.length <= 4 ? "****" : `****${token.slice(-4)}`;
1790
+ }
1791
+ function resetDate(value) {
1792
+ return typeof value === "string" && value.trim() ? resetIn(value) : null;
1793
+ }
1794
+ function percentMetric2(label, snapshot, reset, primary) {
1795
+ if (!snapshot || typeof snapshot.percent_remaining !== "number") return null;
1796
+ if (snapshot.unlimited === true || snapshot.entitlement === 0) return null;
1797
+ const used = Math.min(100, Math.max(0, 100 - snapshot.percent_remaining));
1798
+ return { label, used, limit: 100, format: { kind: "percent" }, resetsAt: reset, primary };
1799
+ }
1800
+ function countMetric(label, remaining, total, reset) {
1801
+ if (typeof remaining !== "number" || typeof total !== "number" || total <= 0) return null;
1802
+ return {
1803
+ label,
1804
+ used: Math.max(0, total - remaining),
1805
+ limit: total,
1806
+ format: { kind: "count" },
1807
+ resetsAt: reset
1808
+ };
1809
+ }
1810
+ async function fetchUsage(token) {
1811
+ try {
1812
+ const res = await fetch(USAGE_URL3, {
1813
+ headers: {
1814
+ "Authorization": `token ${token}`,
1815
+ "Accept": "application/json",
1816
+ "Editor-Version": "vscode/1.96.2",
1817
+ "Editor-Plugin-Version": "copilot-chat/0.26.7",
1818
+ "User-Agent": "GitHubCopilotChat/0.26.7",
1819
+ "X-Github-Api-Version": "2025-04-01"
1820
+ },
1821
+ signal: AbortSignal.timeout(1e4)
1822
+ });
1823
+ if (!res.ok) return { data: null, status: res.status };
1824
+ return { data: await readJson(res), status: res.status };
1825
+ } catch {
1826
+ return { data: null, status: null };
1827
+ }
1828
+ }
1829
+ async function copilotBilling(account) {
1830
+ const cred = await loadToken(account.homeDir);
1831
+ if (!cred) return { plan: null, metrics: [], error: "Not logged in \u2014 run gh auth login" };
1832
+ const { data, status } = await fetchUsage(cred.token);
1833
+ if (!data) {
1834
+ if (status === 401 || status === 403) {
1835
+ return { plan: null, metrics: [], error: `Token invalid (${redactToken(cred.token)}) \u2014 run gh auth login` };
1836
+ }
1837
+ if (status) return { plan: null, metrics: [], error: `Copilot API ${status}` };
1838
+ return { plan: null, metrics: [], error: "Network error" };
1839
+ }
1840
+ const plan = typeof data.copilot_plan === "string" && data.copilot_plan.trim() ? data.copilot_plan : null;
1841
+ const metrics = [];
1842
+ const quotaReset = resetDate(data.quota_reset_date);
1843
+ const snapshots = data.quota_snapshots;
1844
+ const premium = percentMetric2("Premium", snapshots?.premium_interactions, quotaReset, true);
1845
+ if (premium) metrics.push(premium);
1846
+ const chat = percentMetric2("Chat", snapshots?.chat, quotaReset);
1847
+ if (chat) metrics.push(chat);
1848
+ if (data.limited_user_quotas && data.monthly_quotas) {
1849
+ const reset = resetDate(data.limited_user_reset_date);
1850
+ const limitedChat = countMetric("Chat", data.limited_user_quotas.chat, data.monthly_quotas.chat, reset);
1851
+ if (limitedChat) metrics.push(limitedChat);
1852
+ const completions = countMetric("Completions", data.limited_user_quotas.completions, data.monthly_quotas.completions, reset);
1853
+ if (completions) metrics.push(completions);
1854
+ }
1855
+ if (metrics.length === 0) return { plan, metrics: [], error: "No usage data" };
1856
+ return { plan, metrics, error: null };
1857
+ }
1858
+
1859
+ // src/providers/copilot/index.ts
1860
+ var copilotProvider = {
1861
+ id: "copilot",
1862
+ name: "Copilot",
1863
+ color: "white",
1864
+ hasUsage: false,
1865
+ hasBilling: true,
1866
+ detect: (homeDir) => detectCopilot(homeDir),
1867
+ fetchBilling: (account) => copilotBilling(account)
1868
+ };
1869
+
1870
+ // src/providers/antigravity/billing.ts
1871
+ import { access as access7, readdir as readdir7 } from "fs/promises";
1872
+ import { join as join13 } from "path";
1873
+ import { homedir as homedir11 } from "os";
1874
+
1875
+ // src/providers/cloud-code.ts
1876
+ import { readFile as readFile6, readdir as readdir6, realpath, stat } from "fs/promises";
1877
+ import { spawnSync } from "child_process";
1878
+ import { homedir as homedir10 } from "os";
1879
+ import { dirname, join as join12 } from "path";
1880
+ var CLOUD_CODE_URLS = [
1881
+ "https://daily-cloudcode-pa.googleapis.com",
1882
+ "https://cloudcode-pa.googleapis.com"
1883
+ ];
1884
+ var LOAD_CODE_ASSIST_PATH = "/v1internal:loadCodeAssist";
1885
+ var FETCH_MODELS_PATH = "/v1internal:fetchAvailableModels";
1886
+ var RETRIEVE_QUOTA_PATH = "/v1internal:retrieveUserQuota";
1887
+ var GOOGLE_OAUTH_URL = "https://oauth2.googleapis.com/token";
1888
+ var GOOGLE_OAUTH_CLIENT_REGEX = /OAUTH_CLIENT_ID\s*=\s*["']([0-9]{6,}-[a-z0-9]+\.apps\.googleusercontent\.com)["']\s*;?\s*(?:var|const|let)?\s*OAUTH_CLIENT_SECRET\s*=\s*["'](GOCSPX-[A-Za-z0-9_-]+)["']/s;
1889
+ var MAX_BUNDLE_READ = 32 * 1024 * 1024;
1890
+ var cachedClient;
1891
+ var OAUTH_TOKEN_KEY = "antigravityUnifiedStateSync.oauthToken";
1892
+ var OAUTH_TOKEN_SENTINEL = "oauthTokenInfoSentinelKey";
1893
+ var CC_MODEL_BLACKLIST = {
1894
+ MODEL_CHAT_20706: true,
1895
+ MODEL_CHAT_23310: true,
1896
+ MODEL_GOOGLE_GEMINI_2_5_FLASH: true,
1897
+ MODEL_GOOGLE_GEMINI_2_5_FLASH_THINKING: true,
1898
+ MODEL_GOOGLE_GEMINI_2_5_FLASH_LITE: true,
1899
+ MODEL_GOOGLE_GEMINI_2_5_PRO: true,
1900
+ MODEL_PLACEHOLDER_M19: true,
1901
+ MODEL_PLACEHOLDER_M9: true,
1902
+ MODEL_PLACEHOLDER_M12: true
1903
+ };
1904
+ function readVarint(bytes, start) {
1905
+ let value = 0;
1906
+ let shift = 0;
1907
+ let pos3 = start;
1908
+ while (pos3 < bytes.length) {
1909
+ const b = bytes[pos3++];
1910
+ value += (b & 127) * Math.pow(2, shift);
1911
+ if ((b & 128) === 0) return { value, pos: pos3 };
1912
+ shift += 7;
1913
+ }
1914
+ return null;
1915
+ }
1916
+ function readFields(bytes) {
1917
+ const fields = {};
1918
+ let pos3 = 0;
1919
+ while (pos3 < bytes.length) {
1920
+ const tag = readVarint(bytes, pos3);
1921
+ if (!tag) break;
1922
+ pos3 = tag.pos;
1923
+ const fieldNum = Math.floor(tag.value / 8);
1924
+ const wireType = tag.value % 8;
1925
+ if (wireType === 0) {
1926
+ const val = readVarint(bytes, pos3);
1927
+ if (!val) break;
1928
+ fields[fieldNum] = { type: 0, value: val.value };
1929
+ pos3 = val.pos;
1930
+ } else if (wireType === 1) {
1931
+ if (pos3 + 8 > bytes.length) break;
1932
+ pos3 += 8;
1933
+ } else if (wireType === 2) {
1934
+ const len = readVarint(bytes, pos3);
1935
+ if (!len) break;
1936
+ pos3 = len.pos;
1937
+ if (pos3 + len.value > bytes.length) break;
1938
+ fields[fieldNum] = { type: 2, data: bytes.slice(pos3, pos3 + len.value) };
1939
+ pos3 += len.value;
1940
+ } else if (wireType === 5) {
1941
+ if (pos3 + 4 > bytes.length) break;
1942
+ pos3 += 4;
1943
+ } else {
1944
+ break;
1945
+ }
1946
+ }
1947
+ return fields;
1948
+ }
1949
+ function utf8(data) {
1950
+ return Buffer.from(data).toString("utf8");
1951
+ }
1952
+ function decodeBase64(text) {
1953
+ try {
1954
+ return Buffer.from(text, "base64");
1955
+ } catch {
1956
+ return null;
1957
+ }
1958
+ }
1959
+ function unwrapKeyringBase64(raw) {
1960
+ const text = raw.trim();
1961
+ if (!text.startsWith("go-keyring-base64:")) return text;
1962
+ const decoded = decodeBase64(text.slice("go-keyring-base64:".length));
1963
+ return decoded ? utf8(decoded).trim() : text;
1964
+ }
1965
+ function unwrapOAuthSentinel(base64Text) {
1966
+ const outerBytes = decodeBase64(unwrapKeyringBase64(base64Text));
1967
+ if (!outerBytes) return null;
1968
+ const outer = readFields(outerBytes);
1969
+ if (outer[1]?.type !== 2 || !outer[1].data) return null;
1970
+ const wrapper = readFields(outer[1].data);
1971
+ const sentinel = wrapper[1]?.type === 2 && wrapper[1].data ? utf8(wrapper[1].data) : null;
1972
+ const payload = wrapper[2]?.type === 2 ? wrapper[2].data : null;
1973
+ if (sentinel !== OAUTH_TOKEN_SENTINEL || !payload) return null;
1974
+ const payloadFields = readFields(payload);
1975
+ if (payloadFields[1]?.type !== 2 || !payloadFields[1].data) return null;
1976
+ const innerText = utf8(payloadFields[1].data).trim();
1977
+ return innerText ? decodeBase64(innerText) : null;
1978
+ }
1979
+ async function readAntigravityOAuthToken(db) {
1980
+ const r = await runSqlite(db, "SELECT value FROM ItemTable WHERE key=? LIMIT 1;", [OAUTH_TOKEN_KEY]);
1981
+ if (r.status !== "ok") return { token: null, status: r.status };
1982
+ const raw = r.rows[0]?.value;
1983
+ if (typeof raw !== "string" || !raw.trim()) return { token: null, status: "ok" };
1984
+ const inner = unwrapOAuthSentinel(raw);
1985
+ if (!inner) return { token: null, status: "ok" };
1986
+ const fields = readFields(inner);
1987
+ const accessToken = fields[1]?.type === 2 && fields[1].data ? utf8(fields[1].data) : null;
1988
+ const refreshToken = fields[3]?.type === 2 && fields[3].data ? utf8(fields[3].data) : null;
1989
+ let expirySeconds = null;
1990
+ if (fields[4]?.type === 2 && fields[4].data) {
1991
+ const ts = readFields(fields[4].data);
1992
+ expirySeconds = ts[1]?.type === 0 && typeof ts[1].value === "number" ? ts[1].value : null;
1993
+ }
1994
+ if (!accessToken && !refreshToken) return { token: null, status: "ok" };
1995
+ return { token: { accessToken, refreshToken, expirySeconds }, status: "ok" };
1996
+ }
1997
+ function redact(token) {
1998
+ if (!token) return "none";
1999
+ return `...${token.slice(-4)}`;
2000
+ }
2001
+ function geminiBundleCandidates() {
2002
+ const candidates = [];
2003
+ const addBundle = (nodeModulesRoot) => {
2004
+ if (!nodeModulesRoot) return;
2005
+ candidates.push(join12(nodeModulesRoot, "@google", "gemini-cli", "bundle"));
2006
+ };
2007
+ try {
2008
+ const which = spawnSync("command", ["-v", "gemini"], { encoding: "utf8", timeout: 5e3 });
2009
+ const resolved = typeof which.stdout === "string" ? which.stdout.trim().split("\n")[0]?.trim() : "";
2010
+ if (resolved) candidates.push(resolved);
2011
+ } catch {
2012
+ }
2013
+ const home = homedir10();
2014
+ addBundle("/opt/homebrew/lib/node_modules");
2015
+ addBundle("/usr/local/lib/node_modules");
2016
+ addBundle(join12(home, ".local", "share", "node_modules"));
2017
+ addBundle(join12(home, ".bun", "install", "global", "node_modules"));
2018
+ try {
2019
+ const prefix = spawnSync("npm", ["config", "get", "prefix"], { encoding: "utf8", timeout: 5e3 });
2020
+ const root = typeof prefix.stdout === "string" ? prefix.stdout.trim() : "";
2021
+ if (root && root !== "undefined") addBundle(join12(root, "lib", "node_modules"));
2022
+ } catch {
2023
+ }
2024
+ return [...new Set(candidates.filter(Boolean))];
2025
+ }
2026
+ async function resolveBundleDir(candidate) {
2027
+ try {
2028
+ if (candidate.endsWith(`${join12("@google", "gemini-cli", "bundle")}`)) {
2029
+ return candidate;
2030
+ }
2031
+ const real = await realpath(candidate);
2032
+ return dirname(real);
2033
+ } catch {
2034
+ return null;
2035
+ }
2036
+ }
2037
+ async function scanBundleDir(dir) {
2038
+ let entries;
2039
+ try {
2040
+ entries = await readdir6(dir);
2041
+ } catch {
2042
+ return null;
2043
+ }
2044
+ const targets = entries.filter((name) => name === "gemini.js" || name.startsWith("chunk-") && name.endsWith(".js"));
2045
+ for (const name of targets) {
2046
+ const filePath = join12(dir, name);
2047
+ try {
2048
+ const info = await stat(filePath);
2049
+ if (!info.isFile() || info.size > MAX_BUNDLE_READ) continue;
2050
+ const contents = await readFile6(filePath, "utf8");
2051
+ if (!contents.includes("OAUTH_CLIENT_SECRET")) continue;
2052
+ const match = GOOGLE_OAUTH_CLIENT_REGEX.exec(contents);
2053
+ if (match) return { clientId: match[1], clientSecret: match[2] };
2054
+ } catch {
2055
+ }
2056
+ }
2057
+ return null;
2058
+ }
2059
+ async function discoverGoogleOAuthClient() {
2060
+ try {
2061
+ for (const candidate of geminiBundleCandidates()) {
2062
+ const dir = await resolveBundleDir(candidate);
2063
+ if (!dir) continue;
2064
+ const found = await scanBundleDir(dir);
2065
+ if (found) return found;
2066
+ }
2067
+ } catch {
2068
+ }
2069
+ return null;
2070
+ }
2071
+ async function resolveGoogleClient() {
2072
+ const envId = process.env.TOKMON_GOOGLE_CLIENT_ID?.trim();
2073
+ const envSecret = process.env.TOKMON_GOOGLE_CLIENT_SECRET?.trim();
2074
+ if (envId && envSecret) return { clientId: envId, clientSecret: envSecret };
2075
+ if (cachedClient === void 0) cachedClient = await discoverGoogleOAuthClient();
2076
+ return cachedClient;
2077
+ }
2078
+ async function refreshAccessToken(refreshToken) {
2079
+ if (!refreshToken) return null;
2080
+ const client = await resolveGoogleClient();
2081
+ if (!client) return null;
2082
+ const body = new URLSearchParams({
2083
+ client_id: client.clientId,
2084
+ client_secret: client.clientSecret,
2085
+ refresh_token: refreshToken,
2086
+ grant_type: "refresh_token"
2087
+ });
2088
+ try {
2089
+ const res = await fetch(GOOGLE_OAUTH_URL, {
2090
+ method: "POST",
2091
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
2092
+ body,
2093
+ signal: AbortSignal.timeout(15e3)
2094
+ });
2095
+ if (!res.ok) return null;
2096
+ const json = await readJson(res);
2097
+ return typeof json?.access_token === "string" && json.access_token.trim() ? json.access_token.trim() : null;
2098
+ } catch {
2099
+ return null;
2100
+ }
2101
+ }
2102
+ async function requestCloudCodeJson(path, token, body) {
2103
+ for (const base of CLOUD_CODE_URLS) {
2104
+ try {
2105
+ const res = await fetch(`${base}${path}`, {
2106
+ method: "POST",
2107
+ headers: {
2108
+ Accept: "application/json",
2109
+ "Content-Type": "application/json",
2110
+ Authorization: `Bearer ${token}`,
2111
+ "User-Agent": "agy"
2112
+ },
2113
+ body: JSON.stringify(body ?? {}),
2114
+ signal: AbortSignal.timeout(15e3)
2115
+ });
2116
+ if (res.status === 401 || res.status === 403) return { _authFailed: true };
2117
+ if (!res.ok) continue;
2118
+ const json = await readJson(res);
2119
+ if (json && typeof json === "object") return json;
2120
+ } catch {
2121
+ }
2122
+ }
2123
+ return null;
2124
+ }
2125
+ function readPlan(loadData) {
2126
+ const paid = typeof loadData?.paidTier?.name === "string" ? loadData.paidTier.name.trim() : "";
2127
+ const current = typeof loadData?.currentTier?.name === "string" ? loadData.currentTier.name.trim() : "";
2128
+ const raw = paid || current;
2129
+ if (!raw) return null;
2130
+ return raw.replace(/^Gemini Code Assist (?:in|for)\s+/i, "").replace(/^Gemini Code Assist$/i, "Code Assist");
2131
+ }
2132
+ function parseBuckets(data) {
2133
+ if (!Array.isArray(data?.buckets)) return [];
2134
+ return data.buckets.flatMap((bucket) => {
2135
+ const modelId = typeof bucket?.modelId === "string" ? bucket.modelId.trim() : "";
2136
+ if (!modelId) return [];
2137
+ return [{
2138
+ modelId,
2139
+ remainingFraction: typeof bucket.remainingFraction === "number" ? bucket.remainingFraction : 0,
2140
+ resetTime: typeof bucket.resetTime === "string" ? bucket.resetTime : void 0
2141
+ }];
2142
+ });
2143
+ }
2144
+ function parseModelBuckets(data) {
2145
+ const models = data?.models;
2146
+ if (!models || typeof models !== "object") return [];
2147
+ return Object.keys(models).flatMap((key) => {
2148
+ const model = models[key];
2149
+ if (!model || typeof model !== "object" || model.isInternal) return [];
2150
+ const modelId = typeof model.model === "string" && model.model.trim() ? model.model.trim() : key;
2151
+ if (CC_MODEL_BLACKLIST[modelId]) return [];
2152
+ const displayName = typeof model.displayName === "string" && model.displayName.trim() || typeof model.label === "string" && model.label.trim() || "";
2153
+ if (!displayName) return [];
2154
+ const quotaInfo = model.quotaInfo;
2155
+ return [{
2156
+ modelId: displayName,
2157
+ remainingFraction: typeof quotaInfo?.remainingFraction === "number" ? quotaInfo.remainingFraction : 0,
2158
+ resetTime: typeof quotaInfo?.resetTime === "string" ? quotaInfo.resetTime : void 0
2159
+ }];
2160
+ });
2161
+ }
2162
+ async function fetchWithAccessToken(accessToken) {
2163
+ const loadData = await requestCloudCodeJson(LOAD_CODE_ASSIST_PATH, accessToken, {});
2164
+ if (!loadData) return { ok: false, plan: null, error: "Cloud Code API error" };
2165
+ if ("_authFailed" in loadData) return { ok: false, plan: null, error: "Token expired" };
2166
+ const plan = readPlan(loadData);
2167
+ const project = typeof loadData.cloudaicompanionProject === "string" && loadData.cloudaicompanionProject.trim() ? loadData.cloudaicompanionProject.trim() : null;
2168
+ let quotaData = project ? await requestCloudCodeJson(RETRIEVE_QUOTA_PATH, accessToken, { project }) : null;
2169
+ if (!quotaData || "_authFailed" in quotaData) {
2170
+ quotaData = await requestCloudCodeJson(RETRIEVE_QUOTA_PATH, accessToken, {});
2171
+ }
2172
+ if (!quotaData) return { ok: false, plan, error: "Cloud Code quota unavailable" };
2173
+ if ("_authFailed" in quotaData) return { ok: false, plan, error: "Token expired" };
2174
+ let buckets = parseBuckets(quotaData);
2175
+ if (buckets.length === 0) {
2176
+ const modelData = await requestCloudCodeJson(FETCH_MODELS_PATH, accessToken, {});
2177
+ if (modelData && !("_authFailed" in modelData)) buckets = parseModelBuckets(modelData);
2178
+ if (modelData && "_authFailed" in modelData) return { ok: false, plan, error: "Token expired" };
2179
+ }
2180
+ if (buckets.length === 0) return { ok: false, plan, error: "No quota data" };
2181
+ return { ok: true, plan, buckets };
2182
+ }
2183
+ async function fetchCloudCodeQuota(token, expiredMessage = "Token expired") {
2184
+ const nowSec = Math.floor(Date.now() / 1e3);
2185
+ let accessToken = token.accessToken?.trim() || null;
2186
+ if (accessToken && token.expirySeconds && token.expirySeconds <= nowSec) {
2187
+ accessToken = await refreshAccessToken(token.refreshToken);
2188
+ if (!accessToken) return { ok: false, plan: null, error: expiredMessage };
2189
+ }
2190
+ if (!accessToken) {
2191
+ accessToken = await refreshAccessToken(token.refreshToken);
2192
+ if (!accessToken) return { ok: false, plan: null, error: `Missing credentials (${redact(token.accessToken)})` };
2193
+ }
2194
+ const result = await fetchWithAccessToken(accessToken);
2195
+ if (result.ok || result.error !== "Token expired") return result;
2196
+ const refreshed = await refreshAccessToken(token.refreshToken);
2197
+ if (!refreshed) return { ok: false, plan: result.plan, error: expiredMessage };
2198
+ return fetchWithAccessToken(refreshed);
2199
+ }
2200
+ function normalizeLabel(label) {
2201
+ return label.replace(/\s*\([^)]*\)\s*$/, "").trim();
2202
+ }
2203
+ function poolLabel(label) {
2204
+ const lower = normalizeLabel(label).toLowerCase();
2205
+ if (lower.includes("gemini") && lower.includes("pro")) return "Pro";
2206
+ if (lower.includes("gemini") && lower.includes("flash")) return "Flash";
2207
+ return "Claude";
2208
+ }
2209
+ function sortKey(label) {
2210
+ const lower = label.toLowerCase();
2211
+ if (lower.includes("gemini") && lower.includes("pro")) return `0a_${label}`;
2212
+ if (lower.includes("gemini")) return `0b_${label}`;
2213
+ if (lower.includes("claude") && lower.includes("opus")) return `1a_${label}`;
2214
+ if (lower.includes("claude")) return `1b_${label}`;
2215
+ return `2_${label}`;
2216
+ }
2217
+ function cloudCodeBucketsToMetrics(buckets) {
2218
+ const pooled = /* @__PURE__ */ new Map();
2219
+ for (const bucket of buckets) {
2220
+ const label = poolLabel(bucket.modelId);
2221
+ const existing = pooled.get(label);
2222
+ if (!existing || bucket.remainingFraction < existing.remainingFraction) {
2223
+ pooled.set(label, { ...bucket, modelId: label });
2224
+ }
2225
+ }
2226
+ return [...pooled.values()].sort((a, b) => sortKey(a.modelId).localeCompare(sortKey(b.modelId))).map((bucket, i) => {
2227
+ const clamped = Math.max(0, Math.min(1, bucket.remainingFraction));
2228
+ return {
2229
+ label: bucket.modelId,
2230
+ used: Math.round((1 - clamped) * 100),
2231
+ limit: 100,
2232
+ format: { kind: "percent" },
2233
+ resetsAt: bucket.resetTime ? resetIn(bucket.resetTime) : null,
2234
+ primary: i === 0
2235
+ };
2236
+ });
2237
+ }
2238
+ function cloudCodeSqliteError(status) {
2239
+ return status === "ok" ? "Not signed in \u2014 open Antigravity" : sqliteStatusMessage(status).replace(/Cursor/g, "Antigravity");
2240
+ }
2241
+
2242
+ // src/providers/antigravity/billing.ts
2243
+ async function exists(path) {
2244
+ try {
2245
+ await access7(path);
2246
+ return true;
2247
+ } catch {
2248
+ return false;
2249
+ }
2250
+ }
2251
+ async function firstExisting(paths) {
2252
+ for (const path of paths) {
2253
+ if (await exists(path)) return path;
2254
+ }
2255
+ return paths[0];
2256
+ }
2257
+ async function antigravityStateDb(homeDir) {
2258
+ const base = homeDir ?? homedir11();
2259
+ const tail = ["User", "globalStorage", "state.vscdb"];
2260
+ if (process.platform === "darwin") {
2261
+ const support = join13(base, "Library", "Application Support");
2262
+ const exact = [
2263
+ join13(support, "Antigravity IDE", ...tail),
2264
+ join13(support, "Antigravity", ...tail)
2265
+ ];
2266
+ try {
2267
+ const entries = await readdir7(support, { withFileTypes: true });
2268
+ const matches = entries.filter((e) => e.isDirectory() && e.name.includes("Antigravity")).map((e) => join13(support, e.name, ...tail));
2269
+ return firstExisting([...exact, ...matches]);
2270
+ } catch {
2271
+ return firstExisting(exact);
2272
+ }
2273
+ }
2274
+ if (process.platform === "win32") {
2275
+ const roaming = homeDir ? join13(homeDir, "AppData", "Roaming") : envDir("APPDATA") ?? join13(base, "AppData", "Roaming");
2276
+ return firstExisting([
2277
+ join13(roaming, "Antigravity IDE", ...tail),
2278
+ join13(roaming, "Antigravity", ...tail)
2279
+ ]);
2280
+ }
2281
+ const cfg = homeDir ? join13(homeDir, ".config") : envDir("XDG_CONFIG_HOME") ?? join13(base, ".config");
2282
+ return firstExisting([
2283
+ join13(cfg, "Antigravity IDE", ...tail),
2284
+ join13(cfg, "Antigravity", ...tail)
2285
+ ]);
2286
+ }
2287
+ async function detectAntigravity(homeDir) {
2288
+ return exists(await antigravityStateDb(homeDir));
2289
+ }
2290
+ async function antigravityBilling(account) {
2291
+ try {
2292
+ const db = await antigravityStateDb(account.homeDir);
2293
+ const { token, status } = await readAntigravityOAuthToken(db);
2294
+ if (!token) return { plan: null, metrics: [], error: cloudCodeSqliteError(status) };
2295
+ const quota = await fetchCloudCodeQuota(token, "Token expired \u2014 open Antigravity");
2296
+ if (!quota.ok) return { plan: quota.plan, metrics: [], error: quota.error };
2297
+ return { plan: quota.plan, metrics: cloudCodeBucketsToMetrics(quota.buckets), error: null };
2298
+ } catch {
2299
+ return { plan: null, metrics: [], error: "Antigravity billing unavailable" };
2300
+ }
2301
+ }
2302
+
2303
+ // src/providers/antigravity/index.ts
2304
+ var antigravityProvider = {
2305
+ id: "antigravity",
2306
+ name: "Antigravity",
2307
+ color: "red",
2308
+ hasUsage: false,
2309
+ hasBilling: true,
2310
+ detect: (homeDir) => detectAntigravity(homeDir),
2311
+ fetchBilling: (account) => antigravityBilling(account)
2312
+ };
2313
+
2314
+ // src/providers/gemini/billing.ts
2315
+ import { access as access8, readFile as readFile7 } from "fs/promises";
2316
+ import { join as join14 } from "path";
2317
+ import { homedir as homedir12 } from "os";
2318
+ function geminiCredsPath(homeDir) {
2319
+ return join14(homeDir ?? homedir12(), ".gemini", "oauth_creds.json");
2320
+ }
2321
+ async function detectGemini(homeDir) {
2322
+ try {
2323
+ await access8(geminiCredsPath(homeDir));
2324
+ return true;
2325
+ } catch {
2326
+ return false;
2327
+ }
2328
+ }
2329
+ async function readGeminiCreds(path) {
2330
+ try {
2331
+ return JSON.parse(await readFile7(path, "utf8"));
2332
+ } catch {
2333
+ return null;
2334
+ }
2335
+ }
2336
+ async function geminiBilling(account) {
2337
+ try {
2338
+ const creds = await readGeminiCreds(geminiCredsPath(account.homeDir));
2339
+ const accessToken = typeof creds?.access_token === "string" ? creds.access_token.trim() : "";
2340
+ const refreshToken = typeof creds?.refresh_token === "string" ? creds.refresh_token.trim() : null;
2341
+ if (!creds || !accessToken && !refreshToken) return { plan: null, metrics: [], error: "Not signed in \u2014 run gemini" };
2342
+ const quota = await fetchCloudCodeQuota({
2343
+ accessToken,
2344
+ refreshToken,
2345
+ expirySeconds: typeof creds.expiry_date === "number" ? Math.floor(creds.expiry_date / 1e3) : null
2346
+ }, "Token expired \u2014 run gemini");
2347
+ if (!quota.ok) return { plan: quota.plan, metrics: [], error: quota.error };
2348
+ return { plan: quota.plan, metrics: cloudCodeBucketsToMetrics(quota.buckets), error: null };
2349
+ } catch {
2350
+ return { plan: null, metrics: [], error: "Gemini billing unavailable" };
2351
+ }
2352
+ }
2353
+
2354
+ // src/providers/gemini/index.ts
2355
+ var geminiProvider = {
2356
+ id: "gemini",
2357
+ name: "Gemini",
2358
+ color: "greenBright",
2359
+ hasUsage: false,
2360
+ hasBilling: true,
2361
+ detect: (homeDir) => detectGemini(homeDir),
2362
+ fetchBilling: (account) => geminiBilling(account)
2363
+ };
2364
+
2365
+ // src/providers/detect.ts
2366
+ import { accessSync, constants } from "fs";
2367
+ import { join as join15, delimiter, isAbsolute as isAbsolute3 } from "path";
2368
+ import { homedir as homedir13 } from "os";
2369
+ function searchDirs() {
2370
+ const home = homedir13();
2371
+ const fromEnv = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
2372
+ const extra = process.platform === "win32" ? [
2373
+ process.env.APPDATA && join15(process.env.APPDATA, "npm"),
2374
+ process.env.LOCALAPPDATA && join15(process.env.LOCALAPPDATA, "pnpm"),
2375
+ join15(home, "scoop", "shims")
2376
+ ] : [
2377
+ "/opt/homebrew/bin",
2378
+ "/usr/local/bin",
2379
+ "/usr/bin",
2380
+ "/bin",
2381
+ "/opt/local/bin",
2382
+ join15(home, ".local", "bin"),
2383
+ join15(home, "bin"),
2384
+ join15(home, ".npm-global", "bin"),
2385
+ join15(home, ".bun", "bin"),
2386
+ join15(home, ".local", "share", "pnpm")
2387
+ ];
2388
+ return [.../* @__PURE__ */ new Set([...fromEnv, ...extra.filter((d) => !!d)])];
2389
+ }
2390
+ function isExec(p) {
2391
+ try {
2392
+ accessSync(p, process.platform === "win32" ? constants.F_OK : constants.X_OK);
2393
+ return true;
2394
+ } catch {
2395
+ return false;
2396
+ }
2397
+ }
2398
+ function onPath(names) {
2399
+ const exts = process.platform === "win32" ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";").map((e) => e.toLowerCase()).concat("") : [""];
2400
+ for (const dir of searchDirs()) {
2401
+ for (const n of names) {
2402
+ for (const e of exts) {
2403
+ if (isExec(join15(dir, n + e))) return true;
2404
+ }
2405
+ }
2406
+ }
2407
+ return false;
2408
+ }
2409
+ function anyExists(paths) {
2410
+ return paths.some((p) => !!p && isExec(p));
2411
+ }
2412
+ function installSignals(id) {
2413
+ const home = homedir13();
2414
+ const pf = process.env.ProgramFiles;
2415
+ const pf86 = process.env["ProgramFiles(x86)"];
2416
+ const lad = process.env.LOCALAPPDATA;
2417
+ switch (id) {
2418
+ case "claude":
2419
+ return onPath(["claude"]) || anyExists([
2420
+ "/Applications/Claude.app",
2421
+ join15(home, "Applications", "Claude.app"),
2422
+ lad && join15(lad, "Programs", "claude", "Claude.exe")
2423
+ ]);
2424
+ case "codex": {
2425
+ const bin = process.env.CODEX_BIN;
2426
+ if (bin && isAbsolute3(bin) && isExec(bin)) return true;
2427
+ return onPath(["codex"]);
2428
+ }
2429
+ case "cursor":
2430
+ return onPath(["cursor", "cursor-agent"]) || anyExists([
2431
+ "/Applications/Cursor.app",
2432
+ join15(home, "Applications", "Cursor.app"),
2433
+ lad && join15(lad, "Programs", "cursor", "Cursor.exe"),
2434
+ pf && join15(pf, "Cursor", "Cursor.exe"),
2435
+ pf86 && join15(pf86, "Cursor", "Cursor.exe"),
2436
+ "/opt/Cursor/cursor",
2437
+ "/usr/share/cursor/cursor",
2438
+ "/usr/bin/cursor"
2439
+ ]);
2440
+ case "pi":
2441
+ return onPath(["pi"]);
2442
+ case "opencode":
2443
+ return onPath(["opencode"]);
2444
+ case "copilot":
2445
+ return onPath(["gh"]);
2446
+ case "antigravity":
2447
+ return onPath(["antigravity"]) || anyExists([
2448
+ "/Applications/Antigravity.app",
2449
+ join15(home, "Applications", "Antigravity.app"),
2450
+ lad && join15(lad, "Programs", "Antigravity", "Antigravity.exe")
2451
+ ]);
2452
+ case "gemini":
2453
+ return onPath(["gemini"]);
2454
+ default:
2455
+ return false;
2456
+ }
2457
+ }
2458
+
2459
+ // src/providers/index.ts
2460
+ var PROVIDER_ORDER = ["claude", "codex", "cursor", "copilot", "pi", "opencode", "antigravity", "gemini"];
2461
+ var PROVIDERS = {
2462
+ claude: claudeProvider,
2463
+ codex: codexProvider,
2464
+ cursor: cursorProvider,
2465
+ pi: piProvider,
2466
+ opencode: opencodeProvider,
2467
+ copilot: copilotProvider,
2468
+ antigravity: antigravityProvider,
2469
+ gemini: geminiProvider
2470
+ };
2471
+ var ALL_PROVIDERS = PROVIDER_ORDER.map((id) => PROVIDERS[id]);
2472
+ async function detectProviders() {
2473
+ const found = await Promise.all(
2474
+ PROVIDER_ORDER.map(async (id) => {
2475
+ try {
2476
+ if (installSignals(id)) return id;
2477
+ return await PROVIDERS[id].detect() ? id : null;
2478
+ } catch {
2479
+ return null;
2480
+ }
2481
+ })
2482
+ );
2483
+ return found.filter((id) => id !== null);
2484
+ }
2485
+
2486
+ // src/accounts.ts
2487
+ function buildAccounts(config, detected) {
2488
+ const out = [];
2489
+ for (const pid of PROVIDER_ORDER) {
2490
+ if (config.disabledProviders.includes(pid)) continue;
2491
+ const provider = PROVIDERS[pid];
2492
+ const configured = config.accounts.filter((a) => a.providerId === pid);
2493
+ if (configured.length > 0) {
2494
+ for (const a of configured) {
2495
+ out.push({
2496
+ id: a.id,
2497
+ providerId: pid,
2498
+ name: a.name,
2499
+ color: a.color || provider.color,
2500
+ homeDir: a.homeDir && a.homeDir !== "~" ? expandHome(a.homeDir) : void 0
2501
+ });
2502
+ }
2503
+ } else if (detected.includes(pid)) {
2504
+ out.push({ id: pid, providerId: pid, name: provider.name, color: provider.color, homeDir: void 0 });
2505
+ }
2506
+ }
2507
+ return out;
2508
+ }
2509
+ function accountsByProvider(accounts) {
2510
+ const groups = [];
2511
+ for (const pid of PROVIDER_ORDER) {
2512
+ const list = accounts.filter((a) => a.providerId === pid);
2513
+ if (list.length > 0) groups.push({ provider: pid, accounts: list });
2514
+ }
2515
+ return groups;
2516
+ }
2517
+
2518
+ export {
2519
+ configLocation,
2520
+ cacheDir,
2521
+ loadConfig,
2522
+ saveConfig,
2523
+ generateAccountId,
2524
+ pickAccentColor,
2525
+ systemTimezone,
2526
+ isValidTimezone,
2527
+ resolveTimezone,
2528
+ flushDisk,
2529
+ mergeTables,
2530
+ readJson,
2531
+ currency,
2532
+ tokens,
2533
+ time,
2534
+ shortDate,
2535
+ col,
2536
+ cursorModelSpend,
2537
+ PROVIDER_ORDER,
2538
+ PROVIDERS,
2539
+ detectProviders,
2540
+ buildAccounts,
2541
+ accountsByProvider
2542
+ };