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