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