tokmon 0.22.5 → 0.22.7
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 +4 -5
- package/dist/{bootstrap-ink-TWFRIEBW.js → bootstrap-ink-OHPQZKGB.js} +56 -172
- package/dist/{chunk-FO7IUYCM.js → chunk-EMQL5P5J.js} +2 -2
- package/dist/{chunk-AQNFQRWV.js → chunk-I4USANOF.js} +1527 -1143
- package/dist/{chunk-QTHCHB7S.js → chunk-TNIADJ3W.js} +3 -2
- package/dist/cli.js +5 -5
- package/dist/{config-64N7KBZ7.js → config-WLRQHLVJ.js} +1 -1
- package/dist/{daemon-ATXHNYTK.js → daemon-IP3UCTPB.js} +3 -3
- package/dist/server-GA2FKYDQ.js +9 -0
- package/dist/web/assets/{breakdown-Bj0BAVYf.js → breakdown-DU2j1sQ8.js} +1 -1
- package/dist/web/assets/{index-Dziiggw_.js → index-ygZyXDLT.js} +20 -20
- package/dist/web/assets/{timeline-BQpqTU-a.js → timeline-CO2B2c-0.js} +1 -1
- package/dist/web/index.html +1 -1
- package/package.json +2 -2
- package/dist/server-56C7MMEZ.js +0 -9
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
expandHome,
|
|
7
7
|
isValidTimezone,
|
|
8
8
|
slugify
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-TNIADJ3W.js";
|
|
10
10
|
|
|
11
11
|
// src/providers/usage-core.ts
|
|
12
12
|
import { readFile, writeFile, rename, mkdir, readdir, unlink } from "fs/promises";
|
|
@@ -188,7 +188,7 @@ function encode(mtimeMs, size, entries) {
|
|
|
188
188
|
return { m: mtimeMs, s: size, mods, rows };
|
|
189
189
|
}
|
|
190
190
|
function decode(s) {
|
|
191
|
-
const
|
|
191
|
+
const num2 = (x) => typeof x === "number" && Number.isFinite(x) && x >= 0 ? x : 0;
|
|
192
192
|
const out = [];
|
|
193
193
|
for (const r of s.rows) {
|
|
194
194
|
if (!Array.isArray(r) || r.length < 8) continue;
|
|
@@ -198,12 +198,12 @@ function decode(s) {
|
|
|
198
198
|
out.push({
|
|
199
199
|
ts,
|
|
200
200
|
model: typeof mi === "number" && typeof s.mods[mi] === "string" ? s.mods[mi] : "unknown",
|
|
201
|
-
input:
|
|
202
|
-
output:
|
|
203
|
-
cacheCreate:
|
|
204
|
-
cacheRead:
|
|
205
|
-
cost:
|
|
206
|
-
cacheSavings:
|
|
201
|
+
input: num2(r[2]),
|
|
202
|
+
output: num2(r[3]),
|
|
203
|
+
cacheCreate: num2(r[4]),
|
|
204
|
+
cacheRead: num2(r[5]),
|
|
205
|
+
cost: num2(r[6]),
|
|
206
|
+
cacheSavings: num2(r[7]),
|
|
207
207
|
id: typeof r[8] === "string" ? r[8] : void 0
|
|
208
208
|
});
|
|
209
209
|
}
|
|
@@ -391,13 +391,14 @@ function groupBy(entries, keyFn) {
|
|
|
391
391
|
let input = 0, output = 0, cacheCreate = 0, cacheRead = 0, cacheSavings = 0, cost = 0, count = 0;
|
|
392
392
|
const byModel = /* @__PURE__ */ new Map();
|
|
393
393
|
for (const e of group) {
|
|
394
|
+
const n = e.count ?? 1;
|
|
394
395
|
input += e.input;
|
|
395
396
|
output += e.output;
|
|
396
397
|
cacheCreate += e.cacheCreate;
|
|
397
398
|
cacheRead += e.cacheRead;
|
|
398
399
|
cacheSavings += e.cacheSavings;
|
|
399
400
|
cost += e.cost;
|
|
400
|
-
count +=
|
|
401
|
+
count += n;
|
|
401
402
|
const m = byModel.get(e.model);
|
|
402
403
|
if (m) {
|
|
403
404
|
m.input += e.input;
|
|
@@ -406,7 +407,7 @@ function groupBy(entries, keyFn) {
|
|
|
406
407
|
m.cacheRead += e.cacheRead;
|
|
407
408
|
m.cacheSavings += e.cacheSavings;
|
|
408
409
|
m.cost += e.cost;
|
|
409
|
-
m.count +=
|
|
410
|
+
m.count += n;
|
|
410
411
|
} else {
|
|
411
412
|
byModel.set(e.model, {
|
|
412
413
|
name: e.model,
|
|
@@ -416,7 +417,7 @@ function groupBy(entries, keyFn) {
|
|
|
416
417
|
cacheRead: e.cacheRead,
|
|
417
418
|
cacheSavings: e.cacheSavings,
|
|
418
419
|
cost: e.cost,
|
|
419
|
-
count:
|
|
420
|
+
count: n
|
|
420
421
|
});
|
|
421
422
|
}
|
|
422
423
|
}
|
|
@@ -556,10 +557,167 @@ function col(s, w, align = "right") {
|
|
|
556
557
|
}
|
|
557
558
|
var resetIn = formatResetIn;
|
|
558
559
|
|
|
559
|
-
// src/providers/
|
|
560
|
-
import { access } from "fs/promises";
|
|
560
|
+
// src/providers/claude/usage.ts
|
|
561
|
+
import { stat as fsStat, access } from "fs/promises";
|
|
562
|
+
import { createReadStream } from "fs";
|
|
563
|
+
import { createInterface } from "readline";
|
|
564
|
+
import { join as join2, isAbsolute } from "path";
|
|
565
|
+
import { homedir } from "os";
|
|
566
|
+
var PRICING = {
|
|
567
|
+
"claude-opus-4-1": { i: 15e-6, o: 75e-6, cc: 1875e-8, cr: 15e-7 },
|
|
568
|
+
"claude-opus-4-0": { i: 15e-6, o: 75e-6, cc: 1875e-8, cr: 15e-7 },
|
|
569
|
+
"claude-opus-4-20250514": { i: 15e-6, o: 75e-6, cc: 1875e-8, cr: 15e-7 },
|
|
570
|
+
"claude-opus-4": { i: 5e-6, o: 25e-6, cc: 625e-8, cr: 5e-7 },
|
|
571
|
+
"claude-3-opus": { i: 15e-6, o: 75e-6, cc: 1875e-8, cr: 15e-7 },
|
|
572
|
+
"claude-sonnet-4": { i: 3e-6, o: 15e-6, cc: 375e-8, cr: 3e-7 },
|
|
573
|
+
// intro pricing through 2026-08-31 — revert to 3/15/3.75/0.3 after.
|
|
574
|
+
"claude-sonnet-5": { i: 2e-6, o: 1e-5, cc: 25e-7, cr: 2e-7 },
|
|
575
|
+
"claude-haiku-4": { i: 1e-6, o: 5e-6, cc: 125e-8, cr: 1e-7 },
|
|
576
|
+
"claude-fable-5": { i: 1e-5, o: 5e-5, cc: 125e-7, cr: 1e-6 }
|
|
577
|
+
};
|
|
578
|
+
var PRICE_KEYS = Object.keys(PRICING).sort((a, b) => b.length - a.length);
|
|
579
|
+
var ZERO_PRICE = { i: 0, o: 0, cc: 0, cr: 0 };
|
|
580
|
+
function claudeConfigDirs(homeDir) {
|
|
581
|
+
if (homeDir) {
|
|
582
|
+
return [join2(homeDir, ".claude"), join2(homeDir, ".config", "claude")];
|
|
583
|
+
}
|
|
584
|
+
const home = homedir();
|
|
585
|
+
const dirs = [join2(home, ".claude")];
|
|
586
|
+
const xdg = envDir("XDG_CONFIG_HOME");
|
|
587
|
+
if (xdg) {
|
|
588
|
+
dirs.push(join2(xdg, "claude"));
|
|
589
|
+
}
|
|
590
|
+
if (process.platform !== "win32") {
|
|
591
|
+
dirs.push(join2(home, ".config", "claude"));
|
|
592
|
+
}
|
|
593
|
+
const appData = envDir("APPDATA");
|
|
594
|
+
if (appData) dirs.push(join2(appData, "claude"));
|
|
595
|
+
if (process.env.CLAUDE_CONFIG_DIR) {
|
|
596
|
+
for (const p of process.env.CLAUDE_CONFIG_DIR.split(process.platform === "win32" ? ";" : ",")) {
|
|
597
|
+
const t = p.trim();
|
|
598
|
+
if (t && isAbsolute(t)) dirs.push(t);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
return [...new Set(dirs)];
|
|
602
|
+
}
|
|
603
|
+
function getClaudeDirs(homeDir) {
|
|
604
|
+
return claudeConfigDirs(homeDir).map((d) => join2(d, "projects"));
|
|
605
|
+
}
|
|
606
|
+
async function detectClaude(homeDir) {
|
|
607
|
+
for (const dir of getClaudeDirs(homeDir)) {
|
|
608
|
+
try {
|
|
609
|
+
await access(dir);
|
|
610
|
+
return true;
|
|
611
|
+
} catch {
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
return false;
|
|
615
|
+
}
|
|
616
|
+
function priceFor(model) {
|
|
617
|
+
const m = model.toLowerCase().trim();
|
|
618
|
+
for (const key of PRICE_KEYS) {
|
|
619
|
+
if (!m.startsWith(key)) continue;
|
|
620
|
+
const rest = m.slice(key.length);
|
|
621
|
+
if (rest === "" || rest[0] === "-") return PRICING[key];
|
|
622
|
+
}
|
|
623
|
+
return ZERO_PRICE;
|
|
624
|
+
}
|
|
625
|
+
function costOf(model, u, cacheCreate5m, cacheCreate1h, hasCacheCreateSplit) {
|
|
626
|
+
const p = priceFor(model);
|
|
627
|
+
const cacheCreateCost = hasCacheCreateSplit ? cacheCreate5m * p.cc + cacheCreate1h * (2 * p.i) : safeNum(u.cache_creation_input_tokens) * p.cc;
|
|
628
|
+
return safeNum(u.input_tokens) * p.i + safeNum(u.output_tokens) * p.o + cacheCreateCost + safeNum(u.cache_read_input_tokens) * p.cr;
|
|
629
|
+
}
|
|
630
|
+
function shortModel(model) {
|
|
631
|
+
return model.replace("claude-", "").replace(/-\d{8}$/, "");
|
|
632
|
+
}
|
|
633
|
+
function timestampMs(value) {
|
|
634
|
+
if (typeof value === "number" && Number.isFinite(value)) return value > 1e10 ? value : value * 1e3;
|
|
635
|
+
if (typeof value === "string" && value.trim()) return new Date(value.trim()).getTime();
|
|
636
|
+
return NaN;
|
|
637
|
+
}
|
|
638
|
+
async function parseFile(path) {
|
|
639
|
+
const entries = [];
|
|
640
|
+
const input = createReadStream(path);
|
|
641
|
+
input.on("error", () => {
|
|
642
|
+
});
|
|
643
|
+
const rl = createInterface({ input, crlfDelay: Infinity });
|
|
644
|
+
try {
|
|
645
|
+
for await (const line of rl) {
|
|
646
|
+
if (!line.includes('"usage"')) continue;
|
|
647
|
+
try {
|
|
648
|
+
const obj = JSON.parse(line.charCodeAt(0) === 65279 ? line.slice(1) : line);
|
|
649
|
+
if (obj.type !== "assistant" || !obj.message?.usage) continue;
|
|
650
|
+
const ts = timestampMs(obj.timestamp);
|
|
651
|
+
if (!Number.isFinite(ts)) continue;
|
|
652
|
+
const u = obj.message.usage;
|
|
653
|
+
const model = typeof obj.message.model === "string" && obj.message.model ? obj.message.model : "unknown";
|
|
654
|
+
const inputTokens = safeNum(u.input_tokens);
|
|
655
|
+
const output = safeNum(u.output_tokens);
|
|
656
|
+
const hasCacheCreateSplit = u.cache_creation?.ephemeral_5m_input_tokens !== void 0 || u.cache_creation?.ephemeral_1h_input_tokens !== void 0;
|
|
657
|
+
const cacheCreate5m = safeNum(u.cache_creation?.ephemeral_5m_input_tokens);
|
|
658
|
+
const cacheCreate1h = safeNum(u.cache_creation?.ephemeral_1h_input_tokens);
|
|
659
|
+
const cacheCreate = hasCacheCreateSplit ? cacheCreate5m + cacheCreate1h : safeNum(u.cache_creation_input_tokens);
|
|
660
|
+
const cacheRead = safeNum(u.cache_read_input_tokens);
|
|
661
|
+
if (inputTokens + output + cacheCreate + cacheRead === 0) continue;
|
|
662
|
+
const p = priceFor(model);
|
|
663
|
+
const msgId = obj.message?.id;
|
|
664
|
+
entries.push({
|
|
665
|
+
id: msgId ? msgId + (obj.requestId ? ":" + obj.requestId : "") : void 0,
|
|
666
|
+
ts,
|
|
667
|
+
model: shortModel(model),
|
|
668
|
+
cost: costOf(model, u, cacheCreate5m, cacheCreate1h, hasCacheCreateSplit),
|
|
669
|
+
input: inputTokens,
|
|
670
|
+
output,
|
|
671
|
+
cacheCreate,
|
|
672
|
+
cacheRead,
|
|
673
|
+
cacheSavings: cacheRead * (p.i - p.cr)
|
|
674
|
+
});
|
|
675
|
+
} catch {
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
} catch {
|
|
679
|
+
}
|
|
680
|
+
return entries;
|
|
681
|
+
}
|
|
682
|
+
async function loadEntries(since, homeDir) {
|
|
683
|
+
const files = [];
|
|
684
|
+
const seen = /* @__PURE__ */ new Set();
|
|
685
|
+
const seenIno = /* @__PURE__ */ new Set();
|
|
686
|
+
for (const dir of getClaudeDirs(homeDir)) {
|
|
687
|
+
const listing = await walkFiles(dir);
|
|
688
|
+
for (const f of listing) {
|
|
689
|
+
if (!f.endsWith(".jsonl")) continue;
|
|
690
|
+
const path = join2(dir, f);
|
|
691
|
+
if (seen.has(path)) continue;
|
|
692
|
+
seen.add(path);
|
|
693
|
+
try {
|
|
694
|
+
const s = await fsStat(path);
|
|
695
|
+
if (s.mtimeMs < since) continue;
|
|
696
|
+
if (s.ino && process.platform !== "win32") {
|
|
697
|
+
const idn = `${s.dev}:${s.ino}`;
|
|
698
|
+
if (seenIno.has(idn)) continue;
|
|
699
|
+
seenIno.add(idn);
|
|
700
|
+
}
|
|
701
|
+
files.push({ path, mtimeMs: s.mtimeMs, size: s.size });
|
|
702
|
+
} catch {
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
return loadCachedEntries(files, parseFile, since);
|
|
707
|
+
}
|
|
708
|
+
async function claudeDashboard(tz, homeDir) {
|
|
709
|
+
const entries = await loadEntries(dashboardSince(tz), homeDir);
|
|
710
|
+
return summarize(entries, tz);
|
|
711
|
+
}
|
|
712
|
+
async function claudeTable(tz, homeDir) {
|
|
713
|
+
const entries = await loadEntries(tableSince(tz), homeDir);
|
|
714
|
+
return tabulate(entries, tz);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// src/providers/claude/billing.ts
|
|
718
|
+
import { access as access2, readFile as readFile2 } from "fs/promises";
|
|
561
719
|
import { join as join4 } from "path";
|
|
562
|
-
import { homedir as
|
|
720
|
+
import { homedir as homedir3 } from "os";
|
|
563
721
|
|
|
564
722
|
// src/http.ts
|
|
565
723
|
async function readJson(res) {
|
|
@@ -580,814 +738,205 @@ function identityFields(identity) {
|
|
|
580
738
|
};
|
|
581
739
|
}
|
|
582
740
|
|
|
583
|
-
// src/providers/_shared/jwt.ts
|
|
584
|
-
function decodeBase64UrlJson(segment) {
|
|
585
|
-
try {
|
|
586
|
-
const normalized = segment.replace(/-/g, "+").replace(/_/g, "/");
|
|
587
|
-
const padded = normalized + "=".repeat((4 - normalized.length % 4) % 4);
|
|
588
|
-
return JSON.parse(Buffer.from(padded, "base64").toString("utf8"));
|
|
589
|
-
} catch {
|
|
590
|
-
return null;
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
function identityFromIdToken(idToken) {
|
|
594
|
-
if (typeof idToken !== "string" || !idToken.includes(".")) return {};
|
|
595
|
-
const payload = decodeBase64UrlJson(idToken.split(".")[1]);
|
|
596
|
-
if (!payload || typeof payload !== "object") return {};
|
|
597
|
-
const email = typeof payload.email === "string" && payload.email.trim() ? payload.email.trim() : void 0;
|
|
598
|
-
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;
|
|
599
|
-
return { email, displayName, payload };
|
|
600
|
-
}
|
|
601
|
-
|
|
602
741
|
// src/providers/_shared/time.ts
|
|
603
742
|
function msToIso(ms) {
|
|
604
743
|
return Number.isFinite(ms) && Math.abs(ms) <= 864e13 ? new Date(ms).toISOString() : null;
|
|
605
744
|
}
|
|
606
745
|
|
|
607
|
-
// src/providers/
|
|
608
|
-
import { join as join3 } from "path";
|
|
609
|
-
import { homedir } from "os";
|
|
610
|
-
|
|
611
|
-
// src/providers/cursor/sqlite.ts
|
|
746
|
+
// src/providers/_shared/keychain.ts
|
|
612
747
|
import { execFile as execFileCb } from "child_process";
|
|
613
|
-
import { accessSync, constants } from "fs";
|
|
614
|
-
import { delimiter, join as join2 } from "path";
|
|
615
748
|
import { promisify } from "util";
|
|
616
749
|
var execFile = promisify(execFileCb);
|
|
617
|
-
var
|
|
618
|
-
async function
|
|
619
|
-
if (
|
|
750
|
+
var GO_KEYRING_PREFIX = "go-keyring-base64:";
|
|
751
|
+
async function readMacKeychainRaw(service) {
|
|
752
|
+
if (process.platform !== "darwin") return null;
|
|
620
753
|
try {
|
|
621
|
-
|
|
754
|
+
const { stdout } = await execFile("security", [
|
|
755
|
+
"find-generic-password",
|
|
756
|
+
"-s",
|
|
757
|
+
service,
|
|
758
|
+
"-w"
|
|
759
|
+
], { timeout: 5e3 });
|
|
760
|
+
const raw = stdout.trim();
|
|
761
|
+
return raw || null;
|
|
622
762
|
} catch {
|
|
623
|
-
|
|
624
|
-
}
|
|
625
|
-
return nativeDb;
|
|
626
|
-
}
|
|
627
|
-
function classify(msg) {
|
|
628
|
-
if (/database is (locked|busy)|SQLITE_(BUSY|LOCKED)|EBUSY|sharing violation|resource busy|readonly/i.test(msg)) return "locked";
|
|
629
|
-
if (/unable to open|no such file|cannot open|ENOENT/i.test(msg)) return "missing";
|
|
630
|
-
if (/no such (function|table|column)|unknown option/i.test(msg)) return "old";
|
|
631
|
-
return "error";
|
|
632
|
-
}
|
|
633
|
-
async function runSqlite(db, sql, params = []) {
|
|
634
|
-
const DB = await getNativeDb();
|
|
635
|
-
if (DB) {
|
|
636
|
-
let handle;
|
|
637
|
-
let nativeErr;
|
|
638
|
-
try {
|
|
639
|
-
try {
|
|
640
|
-
handle = new DB(db, { readOnly: true, timeout: 1500 });
|
|
641
|
-
} catch (e) {
|
|
642
|
-
if (!(e instanceof TypeError)) throw e;
|
|
643
|
-
handle = new DB(db, { readOnly: true });
|
|
644
|
-
}
|
|
645
|
-
const rows = handle.prepare(sql).all(...params);
|
|
646
|
-
return { status: "ok", rows };
|
|
647
|
-
} catch (e) {
|
|
648
|
-
nativeErr = e;
|
|
649
|
-
} finally {
|
|
650
|
-
try {
|
|
651
|
-
handle?.close();
|
|
652
|
-
} catch {
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
if (nativeErr) {
|
|
656
|
-
return { status: classify(String(nativeErr?.message ?? nativeErr)), rows: [] };
|
|
657
|
-
}
|
|
763
|
+
return null;
|
|
658
764
|
}
|
|
659
|
-
return runSqliteCli(db, sql, params);
|
|
660
765
|
}
|
|
661
|
-
function
|
|
766
|
+
async function readMacKeychainFileRaw(service, keychainPath) {
|
|
767
|
+
if (process.platform !== "darwin") return null;
|
|
662
768
|
try {
|
|
663
|
-
|
|
664
|
-
|
|
769
|
+
await execFile("security", ["unlock-keychain", "-p", "", keychainPath], { timeout: 5e3 });
|
|
770
|
+
const { stdout } = await execFile("security", [
|
|
771
|
+
"find-generic-password",
|
|
772
|
+
"-s",
|
|
773
|
+
service,
|
|
774
|
+
"-w",
|
|
775
|
+
keychainPath
|
|
776
|
+
], { timeout: 5e3 });
|
|
777
|
+
const raw = stdout.trim();
|
|
778
|
+
return raw || null;
|
|
665
779
|
} catch {
|
|
666
|
-
return
|
|
780
|
+
return null;
|
|
667
781
|
}
|
|
668
782
|
}
|
|
669
|
-
function
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
for (const name of names) {
|
|
673
|
-
const path = join2(dir, name);
|
|
674
|
-
if (isExec(path)) return path;
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
return null;
|
|
783
|
+
function unwrapGoKeyringBase64(raw) {
|
|
784
|
+
if (!raw.startsWith(GO_KEYRING_PREFIX)) return raw;
|
|
785
|
+
return Buffer.from(raw.slice(GO_KEYRING_PREFIX.length), "base64").toString("utf-8");
|
|
678
786
|
}
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
787
|
+
|
|
788
|
+
// src/providers/claude/identity.ts
|
|
789
|
+
import { readFileSync } from "fs";
|
|
790
|
+
import { join as join3 } from "path";
|
|
791
|
+
import { homedir as homedir2 } from "os";
|
|
792
|
+
function titleWords(value) {
|
|
793
|
+
return value.split(/[_\s-]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()).join(" ");
|
|
685
794
|
}
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
["-readonly", "-json", "-cmd", ".timeout 1500", db, inlineParams(sql, params)],
|
|
693
|
-
{ timeout: 1e4, maxBuffer: 8 << 20 }
|
|
694
|
-
);
|
|
695
|
-
const text = stdout.trim();
|
|
696
|
-
if (!text) return { status: "ok", rows: [] };
|
|
697
|
-
try {
|
|
698
|
-
return { status: "ok", rows: JSON.parse(text) };
|
|
699
|
-
} catch {
|
|
700
|
-
return { status: "error", rows: [] };
|
|
701
|
-
}
|
|
702
|
-
} catch (e) {
|
|
703
|
-
const err = e;
|
|
704
|
-
if (err?.code === "ENOENT") return { status: "missing", rows: [] };
|
|
705
|
-
return { status: classify(String(err?.stderr ?? err?.message ?? "")), rows: [] };
|
|
706
|
-
}
|
|
795
|
+
function claudeOrgPlanLabel(orgType) {
|
|
796
|
+
if (typeof orgType !== "string" || !orgType.trim()) return null;
|
|
797
|
+
const normalized = orgType.trim().toLowerCase();
|
|
798
|
+
const stripped = normalized.startsWith("claude_") ? normalized.slice("claude_".length) : normalized;
|
|
799
|
+
const label = titleWords(stripped);
|
|
800
|
+
return label ? `Claude ${label}` : null;
|
|
707
801
|
}
|
|
708
|
-
function
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
802
|
+
function readClaudeIdentity(homeDir) {
|
|
803
|
+
const base = homeDir ? expandHome(homeDir) : homedir2();
|
|
804
|
+
try {
|
|
805
|
+
const parsed = JSON.parse(readFileSync(join3(base, ".claude.json"), "utf-8"));
|
|
806
|
+
const oauth = parsed?.oauthAccount;
|
|
807
|
+
const email = typeof oauth?.emailAddress === "string" && oauth.emailAddress.trim() ? oauth.emailAddress.trim() : void 0;
|
|
808
|
+
const displayName = typeof oauth?.displayName === "string" && oauth.displayName.trim() ? oauth.displayName.trim() : void 0;
|
|
809
|
+
const plan = claudeOrgPlanLabel(parsed?.organizationType);
|
|
810
|
+
const accountUuid = typeof oauth?.accountUuid === "string" && oauth.accountUuid.trim() ? oauth.accountUuid.trim() : void 0;
|
|
811
|
+
return { email, displayName, plan: plan ?? void 0, accountUuid };
|
|
812
|
+
} catch {
|
|
813
|
+
return {};
|
|
718
814
|
}
|
|
719
815
|
}
|
|
720
816
|
|
|
721
|
-
// src/providers/
|
|
722
|
-
|
|
723
|
-
var HOUR_MS = 36e5;
|
|
724
|
-
function trackingDb(homeDir) {
|
|
725
|
-
return join3(homeDir ?? homedir(), ".cursor", "ai-tracking", "ai-code-tracking.db");
|
|
726
|
-
}
|
|
727
|
-
async function cursorActivity(tz, homeDir) {
|
|
728
|
-
const db = trackingDb(homeDir);
|
|
817
|
+
// src/providers/claude/billing.ts
|
|
818
|
+
function parseAuth(raw) {
|
|
729
819
|
try {
|
|
730
|
-
const
|
|
731
|
-
const
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
const n = Number.isFinite(raw) && raw > 0 ? raw : 0;
|
|
741
|
-
const hour = Number(row.h);
|
|
742
|
-
if (!Number.isFinite(hour)) continue;
|
|
743
|
-
const key = dayKey(hour * HOUR_MS + HOUR_MS / 2, tz);
|
|
744
|
-
byDay.set(key, (byDay.get(key) ?? 0) + n);
|
|
745
|
-
month += n;
|
|
746
|
-
}
|
|
747
|
-
const series = lastDayKeys(now, tz, SPARK_DAYS).map((k) => byDay.get(k) ?? 0);
|
|
748
|
-
if (month === 0 && series.every((v) => v === 0)) return null;
|
|
749
|
-
return { series, summary: `${tokens(month)} lines` };
|
|
820
|
+
const creds = JSON.parse(raw);
|
|
821
|
+
const o = creds?.claudeAiOauth ?? creds;
|
|
822
|
+
const token = o?.accessToken;
|
|
823
|
+
if (typeof token !== "string" || !token) return null;
|
|
824
|
+
return {
|
|
825
|
+
token,
|
|
826
|
+
subscriptionType: typeof o.subscriptionType === "string" ? o.subscriptionType : void 0,
|
|
827
|
+
rateLimitTier: typeof o.rateLimitTier === "string" ? o.rateLimitTier : void 0,
|
|
828
|
+
expiresAt: typeof o.expiresAt === "number" && Number.isFinite(o.expiresAt) ? o.expiresAt : void 0
|
|
829
|
+
};
|
|
750
830
|
} catch {
|
|
751
831
|
return null;
|
|
752
832
|
}
|
|
753
833
|
}
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
function cursorStateDb(homeDir) {
|
|
762
|
-
const base = homeDir ?? homedir2();
|
|
763
|
-
const tail = ["Cursor", "User", "globalStorage", "state.vscdb"];
|
|
764
|
-
if (process.platform === "darwin") {
|
|
765
|
-
return join4(base, "Library", "Application Support", ...tail);
|
|
766
|
-
}
|
|
767
|
-
if (process.platform === "win32") {
|
|
768
|
-
const roaming = homeDir ? join4(homeDir, "AppData", "Roaming") : envDir("APPDATA") ?? join4(base, "AppData", "Roaming");
|
|
769
|
-
return join4(roaming, ...tail);
|
|
770
|
-
}
|
|
771
|
-
const cfg = homeDir ? join4(homeDir, ".config") : envDir("XDG_CONFIG_HOME") ?? join4(base, ".config");
|
|
772
|
-
return join4(cfg, ...tail);
|
|
773
|
-
}
|
|
774
|
-
async function detectCursor(homeDir) {
|
|
775
|
-
try {
|
|
776
|
-
await access(cursorStateDb(homeDir));
|
|
777
|
-
return true;
|
|
778
|
-
} catch {
|
|
779
|
-
return false;
|
|
834
|
+
async function readCredentialsFile(homeDir) {
|
|
835
|
+
for (const dir of claudeConfigDirs(homeDir)) {
|
|
836
|
+
try {
|
|
837
|
+
const auth = parseAuth(await readFile2(join4(dir, ".credentials.json"), "utf-8"));
|
|
838
|
+
if (auth) return auth;
|
|
839
|
+
} catch {
|
|
840
|
+
}
|
|
780
841
|
}
|
|
842
|
+
return null;
|
|
781
843
|
}
|
|
782
|
-
async function
|
|
783
|
-
const
|
|
784
|
-
|
|
785
|
-
return { value: typeof raw === "string" && raw.trim() ? raw.trim() : null, status: r.status };
|
|
844
|
+
async function readMacKeychain() {
|
|
845
|
+
const raw = await readMacKeychainRaw("Claude Code-credentials");
|
|
846
|
+
return raw ? parseAuth(raw) : null;
|
|
786
847
|
}
|
|
787
|
-
function
|
|
788
|
-
if (
|
|
848
|
+
async function readHomeKeychain(homeDir) {
|
|
849
|
+
if (process.platform !== "darwin") return null;
|
|
850
|
+
const keychainPath = join4(homeDir, "Library", "Keychains", "login.keychain-db");
|
|
789
851
|
try {
|
|
790
|
-
|
|
791
|
-
if (typeof parsed === "string" && parsed.trim()) return parsed.trim();
|
|
852
|
+
await access2(keychainPath);
|
|
792
853
|
} catch {
|
|
854
|
+
return null;
|
|
793
855
|
}
|
|
794
|
-
const
|
|
795
|
-
return
|
|
856
|
+
const raw = await readMacKeychainFileRaw("Claude Code-credentials", keychainPath);
|
|
857
|
+
return raw ? parseAuth(raw) : null;
|
|
796
858
|
}
|
|
797
|
-
function
|
|
798
|
-
const
|
|
799
|
-
const
|
|
800
|
-
|
|
801
|
-
const
|
|
802
|
-
const
|
|
803
|
-
|
|
859
|
+
async function authCandidates(homeDir) {
|
|
860
|
+
const expandedHomeDir = homeDir ? expandHome(homeDir) : void 0;
|
|
861
|
+
const isDefault = !expandedHomeDir || expandedHomeDir === homedir3();
|
|
862
|
+
const out = [];
|
|
863
|
+
const file = await readCredentialsFile(isDefault ? void 0 : expandedHomeDir);
|
|
864
|
+
const keychain = process.platform === "darwin" ? await readMacKeychain() : null;
|
|
865
|
+
const homeKeychain = !isDefault && expandedHomeDir ? await readHomeKeychain(expandedHomeDir) : null;
|
|
866
|
+
const ordered = isDefault ? [keychain && { auth: keychain, shared: true }, file && { auth: file, shared: false }] : [
|
|
867
|
+
homeKeychain && { auth: homeKeychain, shared: false },
|
|
868
|
+
file && { auth: file, shared: false },
|
|
869
|
+
keychain && { auth: keychain, shared: true }
|
|
870
|
+
];
|
|
871
|
+
for (const c of ordered) if (c) out.push(c);
|
|
872
|
+
return out;
|
|
804
873
|
}
|
|
805
|
-
|
|
874
|
+
var tokenIdentityCache = /* @__PURE__ */ new Map();
|
|
875
|
+
async function tokenIdentity(token) {
|
|
876
|
+
if (tokenIdentityCache.has(token)) return tokenIdentityCache.get(token);
|
|
806
877
|
try {
|
|
807
|
-
const res = await fetch(
|
|
808
|
-
method: "POST",
|
|
878
|
+
const res = await fetch("https://api.anthropic.com/api/oauth/profile", {
|
|
809
879
|
headers: {
|
|
810
880
|
"Authorization": `Bearer ${token}`,
|
|
811
|
-
"
|
|
812
|
-
"Connect-Protocol-Version": "1",
|
|
881
|
+
"anthropic-beta": "oauth-2025-04-20",
|
|
813
882
|
"User-Agent": "tokmon"
|
|
814
883
|
},
|
|
815
|
-
body: "{}",
|
|
816
884
|
signal: AbortSignal.timeout(1e4)
|
|
817
885
|
});
|
|
818
|
-
if (
|
|
819
|
-
|
|
886
|
+
if (res.status === 401 || res.status === 403) {
|
|
887
|
+
tokenIdentityCache.set(token, null);
|
|
888
|
+
return null;
|
|
889
|
+
}
|
|
890
|
+
if (!res.ok) return void 0;
|
|
891
|
+
const data = await readJson(res);
|
|
892
|
+
const uuid = data?.account?.uuid;
|
|
893
|
+
if (typeof uuid !== "string" || !uuid) return void 0;
|
|
894
|
+
const identity = {
|
|
895
|
+
accountUuid: uuid,
|
|
896
|
+
email: typeof data?.account?.email === "string" ? data.account.email : null
|
|
897
|
+
};
|
|
898
|
+
if (tokenIdentityCache.size > 64) tokenIdentityCache.clear();
|
|
899
|
+
tokenIdentityCache.set(token, identity);
|
|
900
|
+
return identity;
|
|
820
901
|
} catch {
|
|
821
|
-
return
|
|
902
|
+
return void 0;
|
|
822
903
|
}
|
|
823
904
|
}
|
|
824
|
-
async function
|
|
825
|
-
const
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
}
|
|
835
|
-
if (!
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
905
|
+
async function getAuth(homeDir, expectedUuid) {
|
|
906
|
+
const candidates = await authCandidates(homeDir);
|
|
907
|
+
let sharedAccountEmail;
|
|
908
|
+
let sawExpired = false;
|
|
909
|
+
let sawExpiredOwn = false;
|
|
910
|
+
for (const { auth, shared } of candidates) {
|
|
911
|
+
if (auth.expiresAt !== void 0 && auth.expiresAt < Date.now() - 6e4) {
|
|
912
|
+
sawExpired = true;
|
|
913
|
+
if (!shared) sawExpiredOwn = true;
|
|
914
|
+
continue;
|
|
915
|
+
}
|
|
916
|
+
if (!shared || !expectedUuid) return { auth };
|
|
917
|
+
const identity = await tokenIdentity(auth.token);
|
|
918
|
+
if (identity === void 0) return { auth };
|
|
919
|
+
if (identity === null) continue;
|
|
920
|
+
if (identity.accountUuid === expectedUuid) return { auth };
|
|
921
|
+
sharedAccountEmail = identity.email;
|
|
839
922
|
}
|
|
923
|
+
if (sawExpiredOwn) return { auth: null, expired: true };
|
|
924
|
+
if (sharedAccountEmail !== void 0) return { auth: null, sharedAccountEmail };
|
|
925
|
+
return { auth: null, expired: sawExpired };
|
|
840
926
|
}
|
|
841
|
-
function
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
if (inferred > 0) return inferred;
|
|
848
|
-
return numberValue(su.individualUsed) ?? numberValue(su.pooledUsed) ?? numberValue(su.totalSpend) ?? 0;
|
|
927
|
+
function planLabel(auth) {
|
|
928
|
+
const sub = auth.subscriptionType;
|
|
929
|
+
if (!sub) return null;
|
|
930
|
+
const base = sub.charAt(0).toUpperCase() + sub.slice(1);
|
|
931
|
+
const tier = (auth.rateLimitTier ?? "").match(/(\d+)x/);
|
|
932
|
+
return tier ? `${base} ${tier[1]}x` : base;
|
|
849
933
|
}
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
const hasValidGrants = hasGrants && grantTotal > 0;
|
|
857
|
-
const total = (hasValidGrants ? grantTotal : 0) + stripeBalanceCents;
|
|
858
|
-
if (total <= 0) return;
|
|
859
|
-
metrics.push({ label: "Credits", used: dollars(Math.max(0, total - (hasValidGrants ? grantUsed : 0))), limit: null, format: { kind: "dollars" } });
|
|
860
|
-
}
|
|
861
|
-
async function cursorBilling(account, tz) {
|
|
862
|
-
const [core, activity, spend] = await Promise.all([
|
|
863
|
-
cursorBillingCore(account),
|
|
864
|
-
cursorActivity(tz, account.homeDir),
|
|
865
|
-
cursorModelSpend(account.homeDir)
|
|
866
|
-
]);
|
|
867
|
-
let merged = activity;
|
|
868
|
-
if (spend) {
|
|
869
|
-
const lines = activity?.summary ?? "";
|
|
870
|
-
const spendLabel = `$${Math.round(finite(spend.total))} all-time`;
|
|
871
|
-
merged = {
|
|
872
|
-
series: activity?.series ?? [],
|
|
873
|
-
summary: lines ? `${lines} \xB7 ${spendLabel}` : spendLabel
|
|
874
|
-
};
|
|
875
|
-
}
|
|
876
|
-
const modelSpend = spend?.models?.length ? spend.models.map((m) => ({ name: m.name, usd: finite(m.usd), requests: finite(m.requests) })) : null;
|
|
877
|
-
return { ...core, activity: merged, modelSpend };
|
|
878
|
-
}
|
|
879
|
-
async function cursorBillingCore(account) {
|
|
880
|
-
const db = cursorStateDb(account.homeDir);
|
|
881
|
-
const [tokenRes, membershipRes, emailRes, nameRes] = await Promise.all([
|
|
882
|
-
readState(db, "cursorAuth/accessToken"),
|
|
883
|
-
readState(db, "cursorAuth/stripeMembershipType"),
|
|
884
|
-
readState(db, "cursorAuth/cachedEmail"),
|
|
885
|
-
readState(db, "cursorAuth/cachedName")
|
|
886
|
-
]);
|
|
887
|
-
const token = cleanStoredString(tokenRes.value);
|
|
888
|
-
const membership = membershipRes.value;
|
|
889
|
-
const email = cleanStoredString(emailRes.value);
|
|
890
|
-
const displayName = cleanStoredString(nameRes.value);
|
|
891
|
-
const planFallback = membership ? membership.charAt(0).toUpperCase() + membership.slice(1) : null;
|
|
892
|
-
if (!token) {
|
|
893
|
-
const error = tokenRes.status === "ok" ? "Not signed in \u2014 open Cursor" : sqliteStatusMessage(tokenRes.status);
|
|
894
|
-
return { plan: planFallback, metrics: [], error, ...identityFields({ email, displayName }) };
|
|
895
|
-
}
|
|
896
|
-
const [usage, planInfo, creditGrants, stripe] = await Promise.all([
|
|
897
|
-
connectPost(USAGE_URL, token),
|
|
898
|
-
connectPost(PLAN_URL, token),
|
|
899
|
-
connectPost(CREDITS_URL, token),
|
|
900
|
-
connectGetWithSession(STRIPE_URL, token)
|
|
901
|
-
]);
|
|
902
|
-
if (!usage || usage.__status) {
|
|
903
|
-
const expired = usage?.__status === 401 || usage?.__status === 403;
|
|
904
|
-
return { plan: planFallback, metrics: [], error: expired ? "Token expired \u2014 re-open Cursor" : "Cursor API error", ...identityFields({ email, displayName }) };
|
|
905
|
-
}
|
|
906
|
-
const planName = planInfo?.planInfo?.planName ?? planFallback;
|
|
907
|
-
const price = planInfo?.planInfo?.price;
|
|
908
|
-
const plan = planName ? price ? `${planName} \xB7 ${price}` : planName : null;
|
|
909
|
-
const pu = usage.planUsage ?? {};
|
|
910
|
-
const metrics = [];
|
|
911
|
-
const rawEnd = usage.billingCycleEnd;
|
|
912
|
-
const endMs = numberValue(rawEnd) ?? NaN;
|
|
913
|
-
const iso = msToIso(endMs);
|
|
914
|
-
const resets = iso && endMs > 0 ? resetIn(iso) : null;
|
|
915
|
-
const limit = numberValue(pu.limit);
|
|
916
|
-
const planUsedCents = numberValue(pu.totalSpend) ?? (limit !== void 0 && numberValue(pu.remaining) !== void 0 ? limit - numberValue(pu.remaining) : void 0);
|
|
917
|
-
const computedPercent = limit !== void 0 && limit > 0 && planUsedCents !== void 0 ? planUsedCents / limit * 100 : void 0;
|
|
918
|
-
const totalPercentUsed = numberValue(pu.totalPercentUsed) ?? computedPercent;
|
|
919
|
-
const su = usage.spendLimitUsage;
|
|
920
|
-
const planLower = typeof planName === "string" ? planName.trim().toLowerCase() : "";
|
|
921
|
-
const pooledLimit = numberValue(su?.pooledLimit) ?? 0;
|
|
922
|
-
const isTeamAccount = planLower === "team" || String(su?.limitType ?? "").toLowerCase() === "team" || pooledLimit > 0;
|
|
923
|
-
if (isTeamAccount && limit !== void 0 && planUsedCents !== void 0) {
|
|
924
|
-
metrics.push({
|
|
925
|
-
label: "Usage",
|
|
926
|
-
used: dollars(Math.max(0, planUsedCents)),
|
|
927
|
-
limit: dollars(limit),
|
|
928
|
-
format: { kind: "dollars" },
|
|
929
|
-
resetsAt: resets,
|
|
930
|
-
primary: true
|
|
931
|
-
});
|
|
932
|
-
} else if (totalPercentUsed !== void 0) {
|
|
933
|
-
metrics.push(percentMetric("Usage", totalPercentUsed, resets, true));
|
|
934
|
-
if (limit !== void 0 && planUsedCents !== void 0) {
|
|
935
|
-
metrics.push({
|
|
936
|
-
label: "Spend",
|
|
937
|
-
used: dollars(Math.max(0, planUsedCents)),
|
|
938
|
-
limit: dollars(limit),
|
|
939
|
-
format: { kind: "dollars" }
|
|
940
|
-
});
|
|
941
|
-
}
|
|
942
|
-
}
|
|
943
|
-
const autoPercentUsed = numberValue(pu.autoPercentUsed);
|
|
944
|
-
if (autoPercentUsed !== void 0) {
|
|
945
|
-
metrics.push({ label: "Auto", used: autoPercentUsed, limit: 100, format: { kind: "percent" } });
|
|
946
|
-
}
|
|
947
|
-
const apiPercentUsed = numberValue(pu.apiPercentUsed);
|
|
948
|
-
if (apiPercentUsed !== void 0) {
|
|
949
|
-
metrics.push({ label: "API", used: apiPercentUsed, limit: 100, format: { kind: "percent" } });
|
|
950
|
-
}
|
|
951
|
-
if (su) {
|
|
952
|
-
const pair = numberValue(su.individualLimit) !== void 0 && numberValue(su.individualRemaining) !== void 0 ? { limit: numberValue(su.individualLimit), remaining: numberValue(su.individualRemaining) } : numberValue(su.pooledLimit) !== void 0 && numberValue(su.pooledRemaining) !== void 0 ? { limit: numberValue(su.pooledLimit), remaining: numberValue(su.pooledRemaining) } : null;
|
|
953
|
-
const spent = onDemandSpendCents(su, pair?.limit ?? 0, pair?.remaining ?? 0);
|
|
954
|
-
if (pair && pair.limit > 0) {
|
|
955
|
-
metrics.push({
|
|
956
|
-
label: "On-demand",
|
|
957
|
-
used: dollars(spent),
|
|
958
|
-
limit: dollars(pair.limit),
|
|
959
|
-
format: { kind: "dollars" }
|
|
960
|
-
});
|
|
961
|
-
} else if (spent > 0) {
|
|
962
|
-
metrics.push({
|
|
963
|
-
label: "On-demand",
|
|
964
|
-
used: dollars(spent),
|
|
965
|
-
limit: null,
|
|
966
|
-
format: { kind: "dollars" }
|
|
967
|
-
});
|
|
968
|
-
}
|
|
969
|
-
}
|
|
970
|
-
appendCredits(metrics, creditGrants, stripe);
|
|
971
|
-
if (metrics.length === 0) {
|
|
972
|
-
return { plan, metrics: [], error: usage.enabled === false ? "No active subscription" : "No usage data", ...identityFields({ email, displayName }) };
|
|
973
|
-
}
|
|
974
|
-
return { plan, metrics, error: null, ...identityFields({ email, displayName }) };
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
// src/providers/cursor/composer.ts
|
|
978
|
-
async function cursorModelSpend(homeDir) {
|
|
979
|
-
const db = cursorStateDb(homeDir);
|
|
980
|
-
const sql = "SELECT mk.key AS name, sum(json_extract(mk.value,'$.costInCents')) AS cents, sum(json_extract(mk.value,'$.amount')) AS amt FROM cursorDiskKV c, json_each(c.value,'$.usageData') mk WHERE c.key LIKE 'composerData:%' AND json_valid(c.value) AND json_type(c.value,'$.usageData')='object' GROUP BY mk.key ORDER BY cents DESC;";
|
|
981
|
-
const res = await runSqlite(db, sql);
|
|
982
|
-
if (res.status !== "ok") return null;
|
|
983
|
-
const models = [];
|
|
984
|
-
let total = 0;
|
|
985
|
-
for (const row of res.rows) {
|
|
986
|
-
const usd = finitePositiveCoerced(row.cents) / 100;
|
|
987
|
-
if (usd <= 0) continue;
|
|
988
|
-
models.push({ name: String(row.name ?? ""), usd, requests: finitePositiveCoerced(row.amt) });
|
|
989
|
-
total += usd;
|
|
990
|
-
}
|
|
991
|
-
if (total <= 0) return null;
|
|
992
|
-
return { total, models };
|
|
993
|
-
}
|
|
994
|
-
var USAGE_SQL = "SELECT json_extract(c.value,'$.createdAt') AS createdAt, mk.key AS model, sum(json_extract(mk.value,'$.costInCents')) AS cents, sum(json_extract(mk.value,'$.amount')) AS amt FROM cursorDiskKV c, json_each(c.value,'$.usageData') mk WHERE c.key LIKE 'composerData:%' AND json_valid(c.value) AND json_type(c.value,'$.usageData')='object' AND json_extract(c.value,'$.createdAt') IS NOT NULL GROUP BY createdAt, model;";
|
|
995
|
-
async function cursorUsageTable(tz, homeDir) {
|
|
996
|
-
const res = await runSqlite(cursorStateDb(homeDir), USAGE_SQL);
|
|
997
|
-
if (res.status !== "ok" || res.rows.length === 0) return null;
|
|
998
|
-
const buckets = { daily: /* @__PURE__ */ new Map(), weekly: /* @__PURE__ */ new Map(), monthly: /* @__PURE__ */ new Map() };
|
|
999
|
-
const put = (map, label, model, usd, reqs) => {
|
|
1000
|
-
let row = map.get(label);
|
|
1001
|
-
if (!row) {
|
|
1002
|
-
row = { label, models: [], input: 0, output: 0, cacheCreate: 0, cacheRead: 0, cacheSavings: 0, total: 0, cost: 0, count: 0, breakdown: [] };
|
|
1003
|
-
map.set(label, row);
|
|
1004
|
-
}
|
|
1005
|
-
row.cost += usd;
|
|
1006
|
-
row.count += reqs;
|
|
1007
|
-
let md = row.breakdown.find((b) => b.name === model);
|
|
1008
|
-
if (!md) {
|
|
1009
|
-
md = { name: model, input: 0, output: 0, cacheCreate: 0, cacheRead: 0, cacheSavings: 0, cost: 0, count: 0 };
|
|
1010
|
-
row.breakdown.push(md);
|
|
1011
|
-
}
|
|
1012
|
-
md.cost += usd;
|
|
1013
|
-
md.count += reqs;
|
|
1014
|
-
};
|
|
1015
|
-
for (const r of res.rows) {
|
|
1016
|
-
const ts = Number(r.createdAt);
|
|
1017
|
-
if (!Number.isFinite(ts) || ts <= 0) continue;
|
|
1018
|
-
const usd = finitePositiveCoerced(r.cents) / 100;
|
|
1019
|
-
const reqs = finitePositiveCoerced(r.amt);
|
|
1020
|
-
if (usd <= 0 && reqs <= 0) continue;
|
|
1021
|
-
const model = String(r.model ?? "unknown");
|
|
1022
|
-
put(buckets.daily, dayKey(ts, tz), model, usd, reqs);
|
|
1023
|
-
put(buckets.weekly, weekKey(ts, tz), model, usd, reqs);
|
|
1024
|
-
put(buckets.monthly, monthKey(ts, tz), model, usd, reqs);
|
|
1025
|
-
}
|
|
1026
|
-
const finalize = (map) => [...map.values()].map((row) => {
|
|
1027
|
-
row.breakdown.sort((a, b) => b.cost - a.cost);
|
|
1028
|
-
row.models = row.breakdown.map((b) => b.name);
|
|
1029
|
-
return row;
|
|
1030
|
-
}).sort((a, b) => a.label.localeCompare(b.label));
|
|
1031
|
-
const table = { daily: finalize(buckets.daily), weekly: finalize(buckets.weekly), monthly: finalize(buckets.monthly) };
|
|
1032
|
-
return table.daily.length ? table : null;
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
// src/providers/claude/usage.ts
|
|
1036
|
-
import { stat as fsStat, access as access2 } from "fs/promises";
|
|
1037
|
-
import { createReadStream } from "fs";
|
|
1038
|
-
import { createInterface } from "readline";
|
|
1039
|
-
import { join as join5, isAbsolute } from "path";
|
|
1040
|
-
import { homedir as homedir3 } from "os";
|
|
1041
|
-
var PRICING = {
|
|
1042
|
-
"claude-opus-4-1": { i: 15e-6, o: 75e-6, cc: 1875e-8, cr: 15e-7 },
|
|
1043
|
-
"claude-opus-4-0": { i: 15e-6, o: 75e-6, cc: 1875e-8, cr: 15e-7 },
|
|
1044
|
-
"claude-opus-4-20250514": { i: 15e-6, o: 75e-6, cc: 1875e-8, cr: 15e-7 },
|
|
1045
|
-
"claude-opus-4": { i: 5e-6, o: 25e-6, cc: 625e-8, cr: 5e-7 },
|
|
1046
|
-
"claude-3-opus": { i: 15e-6, o: 75e-6, cc: 1875e-8, cr: 15e-7 },
|
|
1047
|
-
"claude-sonnet-4": { i: 3e-6, o: 15e-6, cc: 375e-8, cr: 3e-7 },
|
|
1048
|
-
// intro pricing through 2026-08-31 — revert to 3/15/3.75/0.3 after.
|
|
1049
|
-
"claude-sonnet-5": { i: 2e-6, o: 1e-5, cc: 25e-7, cr: 2e-7 },
|
|
1050
|
-
"claude-haiku-4": { i: 1e-6, o: 5e-6, cc: 125e-8, cr: 1e-7 },
|
|
1051
|
-
"claude-fable-5": { i: 1e-5, o: 5e-5, cc: 125e-7, cr: 1e-6 }
|
|
1052
|
-
};
|
|
1053
|
-
var PRICE_KEYS = Object.keys(PRICING).sort((a, b) => b.length - a.length);
|
|
1054
|
-
var ZERO_PRICE = { i: 0, o: 0, cc: 0, cr: 0 };
|
|
1055
|
-
function claudeConfigDirs(homeDir) {
|
|
1056
|
-
if (homeDir) {
|
|
1057
|
-
return [join5(homeDir, ".claude"), join5(homeDir, ".config", "claude")];
|
|
1058
|
-
}
|
|
1059
|
-
const home = homedir3();
|
|
1060
|
-
const dirs = [join5(home, ".claude")];
|
|
1061
|
-
const xdg = envDir("XDG_CONFIG_HOME");
|
|
1062
|
-
if (xdg) {
|
|
1063
|
-
dirs.push(join5(xdg, "claude"));
|
|
1064
|
-
}
|
|
1065
|
-
if (process.platform !== "win32") {
|
|
1066
|
-
dirs.push(join5(home, ".config", "claude"));
|
|
1067
|
-
}
|
|
1068
|
-
const appData = envDir("APPDATA");
|
|
1069
|
-
if (appData) dirs.push(join5(appData, "claude"));
|
|
1070
|
-
if (process.env.CLAUDE_CONFIG_DIR) {
|
|
1071
|
-
for (const p of process.env.CLAUDE_CONFIG_DIR.split(process.platform === "win32" ? ";" : ",")) {
|
|
1072
|
-
const t = p.trim();
|
|
1073
|
-
if (t && isAbsolute(t)) dirs.push(t);
|
|
1074
|
-
}
|
|
1075
|
-
}
|
|
1076
|
-
return [...new Set(dirs)];
|
|
1077
|
-
}
|
|
1078
|
-
function getClaudeDirs(homeDir) {
|
|
1079
|
-
return claudeConfigDirs(homeDir).map((d) => join5(d, "projects"));
|
|
1080
|
-
}
|
|
1081
|
-
async function detectClaude(homeDir) {
|
|
1082
|
-
for (const dir of getClaudeDirs(homeDir)) {
|
|
1083
|
-
try {
|
|
1084
|
-
await access2(dir);
|
|
1085
|
-
return true;
|
|
1086
|
-
} catch {
|
|
1087
|
-
}
|
|
1088
|
-
}
|
|
1089
|
-
return false;
|
|
1090
|
-
}
|
|
1091
|
-
function priceFor(model) {
|
|
1092
|
-
const m = model.toLowerCase().trim();
|
|
1093
|
-
for (const key of PRICE_KEYS) {
|
|
1094
|
-
if (!m.startsWith(key)) continue;
|
|
1095
|
-
const rest = m.slice(key.length);
|
|
1096
|
-
if (rest === "" || rest[0] === "-") return PRICING[key];
|
|
1097
|
-
}
|
|
1098
|
-
return ZERO_PRICE;
|
|
1099
|
-
}
|
|
1100
|
-
function costOf(model, u, cacheCreate5m, cacheCreate1h, hasCacheCreateSplit) {
|
|
1101
|
-
const p = priceFor(model);
|
|
1102
|
-
const cacheCreateCost = hasCacheCreateSplit ? cacheCreate5m * p.cc + cacheCreate1h * (2 * p.i) : safeNum(u.cache_creation_input_tokens) * p.cc;
|
|
1103
|
-
return safeNum(u.input_tokens) * p.i + safeNum(u.output_tokens) * p.o + cacheCreateCost + safeNum(u.cache_read_input_tokens) * p.cr;
|
|
1104
|
-
}
|
|
1105
|
-
function shortModel(model) {
|
|
1106
|
-
return model.replace("claude-", "").replace(/-\d{8}$/, "");
|
|
1107
|
-
}
|
|
1108
|
-
function timestampMs(value) {
|
|
1109
|
-
if (typeof value === "number" && Number.isFinite(value)) return value > 1e10 ? value : value * 1e3;
|
|
1110
|
-
if (typeof value === "string" && value.trim()) return new Date(value.trim()).getTime();
|
|
1111
|
-
return NaN;
|
|
1112
|
-
}
|
|
1113
|
-
async function parseFile(path) {
|
|
1114
|
-
const entries = [];
|
|
1115
|
-
const input = createReadStream(path);
|
|
1116
|
-
input.on("error", () => {
|
|
1117
|
-
});
|
|
1118
|
-
const rl = createInterface({ input, crlfDelay: Infinity });
|
|
1119
|
-
try {
|
|
1120
|
-
for await (const line of rl) {
|
|
1121
|
-
if (!line.includes('"usage"')) continue;
|
|
1122
|
-
try {
|
|
1123
|
-
const obj = JSON.parse(line.charCodeAt(0) === 65279 ? line.slice(1) : line);
|
|
1124
|
-
if (obj.type !== "assistant" || !obj.message?.usage) continue;
|
|
1125
|
-
const ts = timestampMs(obj.timestamp);
|
|
1126
|
-
if (!Number.isFinite(ts)) continue;
|
|
1127
|
-
const u = obj.message.usage;
|
|
1128
|
-
const model = typeof obj.message.model === "string" && obj.message.model ? obj.message.model : "unknown";
|
|
1129
|
-
const inputTokens = safeNum(u.input_tokens);
|
|
1130
|
-
const output = safeNum(u.output_tokens);
|
|
1131
|
-
const hasCacheCreateSplit = u.cache_creation?.ephemeral_5m_input_tokens !== void 0 || u.cache_creation?.ephemeral_1h_input_tokens !== void 0;
|
|
1132
|
-
const cacheCreate5m = safeNum(u.cache_creation?.ephemeral_5m_input_tokens);
|
|
1133
|
-
const cacheCreate1h = safeNum(u.cache_creation?.ephemeral_1h_input_tokens);
|
|
1134
|
-
const cacheCreate = hasCacheCreateSplit ? cacheCreate5m + cacheCreate1h : safeNum(u.cache_creation_input_tokens);
|
|
1135
|
-
const cacheRead = safeNum(u.cache_read_input_tokens);
|
|
1136
|
-
if (inputTokens + output + cacheCreate + cacheRead === 0) continue;
|
|
1137
|
-
const p = priceFor(model);
|
|
1138
|
-
const msgId = obj.message?.id;
|
|
1139
|
-
entries.push({
|
|
1140
|
-
id: msgId ? msgId + (obj.requestId ? ":" + obj.requestId : "") : void 0,
|
|
1141
|
-
ts,
|
|
1142
|
-
model: shortModel(model),
|
|
1143
|
-
cost: costOf(model, u, cacheCreate5m, cacheCreate1h, hasCacheCreateSplit),
|
|
1144
|
-
input: inputTokens,
|
|
1145
|
-
output,
|
|
1146
|
-
cacheCreate,
|
|
1147
|
-
cacheRead,
|
|
1148
|
-
cacheSavings: cacheRead * (p.i - p.cr)
|
|
1149
|
-
});
|
|
1150
|
-
} catch {
|
|
1151
|
-
}
|
|
1152
|
-
}
|
|
1153
|
-
} catch {
|
|
1154
|
-
}
|
|
1155
|
-
return entries;
|
|
1156
|
-
}
|
|
1157
|
-
async function loadEntries(since, homeDir) {
|
|
1158
|
-
const files = [];
|
|
1159
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1160
|
-
const seenIno = /* @__PURE__ */ new Set();
|
|
1161
|
-
for (const dir of getClaudeDirs(homeDir)) {
|
|
1162
|
-
const listing = await walkFiles(dir);
|
|
1163
|
-
for (const f of listing) {
|
|
1164
|
-
if (!f.endsWith(".jsonl")) continue;
|
|
1165
|
-
const path = join5(dir, f);
|
|
1166
|
-
if (seen.has(path)) continue;
|
|
1167
|
-
seen.add(path);
|
|
1168
|
-
try {
|
|
1169
|
-
const s = await fsStat(path);
|
|
1170
|
-
if (s.mtimeMs < since) continue;
|
|
1171
|
-
if (s.ino && process.platform !== "win32") {
|
|
1172
|
-
const idn = `${s.dev}:${s.ino}`;
|
|
1173
|
-
if (seenIno.has(idn)) continue;
|
|
1174
|
-
seenIno.add(idn);
|
|
1175
|
-
}
|
|
1176
|
-
files.push({ path, mtimeMs: s.mtimeMs, size: s.size });
|
|
1177
|
-
} catch {
|
|
1178
|
-
}
|
|
1179
|
-
}
|
|
1180
|
-
}
|
|
1181
|
-
return loadCachedEntries(files, parseFile, since);
|
|
1182
|
-
}
|
|
1183
|
-
async function claudeDashboard(tz, homeDir) {
|
|
1184
|
-
const entries = await loadEntries(dashboardSince(tz), homeDir);
|
|
1185
|
-
return summarize(entries, tz);
|
|
1186
|
-
}
|
|
1187
|
-
async function claudeTable(tz, homeDir) {
|
|
1188
|
-
const entries = await loadEntries(tableSince(tz), homeDir);
|
|
1189
|
-
return tabulate(entries, tz);
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
// src/providers/claude/billing.ts
|
|
1193
|
-
import { access as access3, readFile as readFile2 } from "fs/promises";
|
|
1194
|
-
import { join as join7 } from "path";
|
|
1195
|
-
import { homedir as homedir5 } from "os";
|
|
1196
|
-
|
|
1197
|
-
// src/providers/_shared/keychain.ts
|
|
1198
|
-
import { execFile as execFileCb2 } from "child_process";
|
|
1199
|
-
import { promisify as promisify2 } from "util";
|
|
1200
|
-
var execFile2 = promisify2(execFileCb2);
|
|
1201
|
-
var GO_KEYRING_PREFIX = "go-keyring-base64:";
|
|
1202
|
-
async function readMacKeychainRaw(service) {
|
|
1203
|
-
if (process.platform !== "darwin") return null;
|
|
1204
|
-
try {
|
|
1205
|
-
const { stdout } = await execFile2("security", [
|
|
1206
|
-
"find-generic-password",
|
|
1207
|
-
"-s",
|
|
1208
|
-
service,
|
|
1209
|
-
"-w"
|
|
1210
|
-
], { timeout: 5e3 });
|
|
1211
|
-
const raw = stdout.trim();
|
|
1212
|
-
return raw || null;
|
|
1213
|
-
} catch {
|
|
1214
|
-
return null;
|
|
1215
|
-
}
|
|
1216
|
-
}
|
|
1217
|
-
async function readMacKeychainFileRaw(service, keychainPath) {
|
|
1218
|
-
if (process.platform !== "darwin") return null;
|
|
1219
|
-
try {
|
|
1220
|
-
await execFile2("security", ["unlock-keychain", "-p", "", keychainPath], { timeout: 5e3 });
|
|
1221
|
-
const { stdout } = await execFile2("security", [
|
|
1222
|
-
"find-generic-password",
|
|
1223
|
-
"-s",
|
|
1224
|
-
service,
|
|
1225
|
-
"-w",
|
|
1226
|
-
keychainPath
|
|
1227
|
-
], { timeout: 5e3 });
|
|
1228
|
-
const raw = stdout.trim();
|
|
1229
|
-
return raw || null;
|
|
1230
|
-
} catch {
|
|
1231
|
-
return null;
|
|
1232
|
-
}
|
|
1233
|
-
}
|
|
1234
|
-
function unwrapGoKeyringBase64(raw) {
|
|
1235
|
-
if (!raw.startsWith(GO_KEYRING_PREFIX)) return raw;
|
|
1236
|
-
return Buffer.from(raw.slice(GO_KEYRING_PREFIX.length), "base64").toString("utf-8");
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
// src/providers/claude/identity.ts
|
|
1240
|
-
import { readFileSync } from "fs";
|
|
1241
|
-
import { join as join6 } from "path";
|
|
1242
|
-
import { homedir as homedir4 } from "os";
|
|
1243
|
-
function titleWords(value) {
|
|
1244
|
-
return value.split(/[_\s-]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()).join(" ");
|
|
1245
|
-
}
|
|
1246
|
-
function claudeOrgPlanLabel(orgType) {
|
|
1247
|
-
if (typeof orgType !== "string" || !orgType.trim()) return null;
|
|
1248
|
-
const normalized = orgType.trim().toLowerCase();
|
|
1249
|
-
const stripped = normalized.startsWith("claude_") ? normalized.slice("claude_".length) : normalized;
|
|
1250
|
-
const label = titleWords(stripped);
|
|
1251
|
-
return label ? `Claude ${label}` : null;
|
|
1252
|
-
}
|
|
1253
|
-
function readClaudeIdentity(homeDir) {
|
|
1254
|
-
const base = homeDir ? expandHome(homeDir) : homedir4();
|
|
1255
|
-
try {
|
|
1256
|
-
const parsed = JSON.parse(readFileSync(join6(base, ".claude.json"), "utf-8"));
|
|
1257
|
-
const oauth = parsed?.oauthAccount;
|
|
1258
|
-
const email = typeof oauth?.emailAddress === "string" && oauth.emailAddress.trim() ? oauth.emailAddress.trim() : void 0;
|
|
1259
|
-
const displayName = typeof oauth?.displayName === "string" && oauth.displayName.trim() ? oauth.displayName.trim() : void 0;
|
|
1260
|
-
const plan = claudeOrgPlanLabel(parsed?.organizationType);
|
|
1261
|
-
const accountUuid = typeof oauth?.accountUuid === "string" && oauth.accountUuid.trim() ? oauth.accountUuid.trim() : void 0;
|
|
1262
|
-
return { email, displayName, plan: plan ?? void 0, accountUuid };
|
|
1263
|
-
} catch {
|
|
1264
|
-
return {};
|
|
1265
|
-
}
|
|
1266
|
-
}
|
|
1267
|
-
|
|
1268
|
-
// src/providers/claude/billing.ts
|
|
1269
|
-
function parseAuth(raw) {
|
|
1270
|
-
try {
|
|
1271
|
-
const creds = JSON.parse(raw);
|
|
1272
|
-
const o = creds?.claudeAiOauth ?? creds;
|
|
1273
|
-
const token = o?.accessToken;
|
|
1274
|
-
if (typeof token !== "string" || !token) return null;
|
|
1275
|
-
return {
|
|
1276
|
-
token,
|
|
1277
|
-
subscriptionType: typeof o.subscriptionType === "string" ? o.subscriptionType : void 0,
|
|
1278
|
-
rateLimitTier: typeof o.rateLimitTier === "string" ? o.rateLimitTier : void 0,
|
|
1279
|
-
expiresAt: typeof o.expiresAt === "number" && Number.isFinite(o.expiresAt) ? o.expiresAt : void 0
|
|
1280
|
-
};
|
|
1281
|
-
} catch {
|
|
1282
|
-
return null;
|
|
1283
|
-
}
|
|
1284
|
-
}
|
|
1285
|
-
async function readCredentialsFile(homeDir) {
|
|
1286
|
-
for (const dir of claudeConfigDirs(homeDir)) {
|
|
1287
|
-
try {
|
|
1288
|
-
const auth = parseAuth(await readFile2(join7(dir, ".credentials.json"), "utf-8"));
|
|
1289
|
-
if (auth) return auth;
|
|
1290
|
-
} catch {
|
|
1291
|
-
}
|
|
1292
|
-
}
|
|
1293
|
-
return null;
|
|
1294
|
-
}
|
|
1295
|
-
async function readMacKeychain() {
|
|
1296
|
-
const raw = await readMacKeychainRaw("Claude Code-credentials");
|
|
1297
|
-
return raw ? parseAuth(raw) : null;
|
|
1298
|
-
}
|
|
1299
|
-
async function readHomeKeychain(homeDir) {
|
|
1300
|
-
if (process.platform !== "darwin") return null;
|
|
1301
|
-
const keychainPath = join7(homeDir, "Library", "Keychains", "login.keychain-db");
|
|
1302
|
-
try {
|
|
1303
|
-
await access3(keychainPath);
|
|
1304
|
-
} catch {
|
|
1305
|
-
return null;
|
|
1306
|
-
}
|
|
1307
|
-
const raw = await readMacKeychainFileRaw("Claude Code-credentials", keychainPath);
|
|
1308
|
-
return raw ? parseAuth(raw) : null;
|
|
1309
|
-
}
|
|
1310
|
-
async function authCandidates(homeDir) {
|
|
1311
|
-
const expandedHomeDir = homeDir ? expandHome(homeDir) : void 0;
|
|
1312
|
-
const isDefault = !expandedHomeDir || expandedHomeDir === homedir5();
|
|
1313
|
-
const out = [];
|
|
1314
|
-
const file = await readCredentialsFile(isDefault ? void 0 : expandedHomeDir);
|
|
1315
|
-
const keychain = process.platform === "darwin" ? await readMacKeychain() : null;
|
|
1316
|
-
const homeKeychain = !isDefault && expandedHomeDir ? await readHomeKeychain(expandedHomeDir) : null;
|
|
1317
|
-
const ordered = isDefault ? [keychain && { auth: keychain, shared: true }, file && { auth: file, shared: false }] : [
|
|
1318
|
-
homeKeychain && { auth: homeKeychain, shared: false },
|
|
1319
|
-
file && { auth: file, shared: false },
|
|
1320
|
-
keychain && { auth: keychain, shared: true }
|
|
1321
|
-
];
|
|
1322
|
-
for (const c of ordered) if (c) out.push(c);
|
|
1323
|
-
return out;
|
|
1324
|
-
}
|
|
1325
|
-
var tokenIdentityCache = /* @__PURE__ */ new Map();
|
|
1326
|
-
async function tokenIdentity(token) {
|
|
1327
|
-
if (tokenIdentityCache.has(token)) return tokenIdentityCache.get(token);
|
|
1328
|
-
try {
|
|
1329
|
-
const res = await fetch("https://api.anthropic.com/api/oauth/profile", {
|
|
1330
|
-
headers: {
|
|
1331
|
-
"Authorization": `Bearer ${token}`,
|
|
1332
|
-
"anthropic-beta": "oauth-2025-04-20",
|
|
1333
|
-
"User-Agent": "tokmon"
|
|
1334
|
-
},
|
|
1335
|
-
signal: AbortSignal.timeout(1e4)
|
|
1336
|
-
});
|
|
1337
|
-
if (res.status === 401 || res.status === 403) {
|
|
1338
|
-
tokenIdentityCache.set(token, null);
|
|
1339
|
-
return null;
|
|
1340
|
-
}
|
|
1341
|
-
if (!res.ok) return void 0;
|
|
1342
|
-
const data = await readJson(res);
|
|
1343
|
-
const uuid = data?.account?.uuid;
|
|
1344
|
-
if (typeof uuid !== "string" || !uuid) return void 0;
|
|
1345
|
-
const identity = {
|
|
1346
|
-
accountUuid: uuid,
|
|
1347
|
-
email: typeof data?.account?.email === "string" ? data.account.email : null
|
|
1348
|
-
};
|
|
1349
|
-
if (tokenIdentityCache.size > 64) tokenIdentityCache.clear();
|
|
1350
|
-
tokenIdentityCache.set(token, identity);
|
|
1351
|
-
return identity;
|
|
1352
|
-
} catch {
|
|
1353
|
-
return void 0;
|
|
1354
|
-
}
|
|
1355
|
-
}
|
|
1356
|
-
async function getAuth(homeDir, expectedUuid) {
|
|
1357
|
-
const candidates = await authCandidates(homeDir);
|
|
1358
|
-
let sharedAccountEmail;
|
|
1359
|
-
let sawExpired = false;
|
|
1360
|
-
let sawExpiredOwn = false;
|
|
1361
|
-
for (const { auth, shared } of candidates) {
|
|
1362
|
-
if (auth.expiresAt !== void 0 && auth.expiresAt < Date.now() - 6e4) {
|
|
1363
|
-
sawExpired = true;
|
|
1364
|
-
if (!shared) sawExpiredOwn = true;
|
|
1365
|
-
continue;
|
|
1366
|
-
}
|
|
1367
|
-
if (!shared || !expectedUuid) return { auth };
|
|
1368
|
-
const identity = await tokenIdentity(auth.token);
|
|
1369
|
-
if (identity === void 0) return { auth };
|
|
1370
|
-
if (identity === null) continue;
|
|
1371
|
-
if (identity.accountUuid === expectedUuid) return { auth };
|
|
1372
|
-
sharedAccountEmail = identity.email;
|
|
1373
|
-
}
|
|
1374
|
-
if (sawExpiredOwn) return { auth: null, expired: true };
|
|
1375
|
-
if (sharedAccountEmail !== void 0) return { auth: null, sharedAccountEmail };
|
|
1376
|
-
return { auth: null, expired: sawExpired };
|
|
1377
|
-
}
|
|
1378
|
-
function planLabel(auth) {
|
|
1379
|
-
const sub = auth.subscriptionType;
|
|
1380
|
-
if (!sub) return null;
|
|
1381
|
-
const base = sub.charAt(0).toUpperCase() + sub.slice(1);
|
|
1382
|
-
const tier = (auth.rateLimitTier ?? "").match(/(\d+)x/);
|
|
1383
|
-
return tier ? `${base} ${tier[1]}x` : base;
|
|
1384
|
-
}
|
|
1385
|
-
var pct = (used, resets, primary) => percentMetric("", used, resets ?? null, primary);
|
|
1386
|
-
function boolValue(value) {
|
|
1387
|
-
if (typeof value === "boolean") return value;
|
|
1388
|
-
if (typeof value === "number") return value !== 0;
|
|
1389
|
-
if (typeof value === "string") return value.trim().toLowerCase() === "true" || value.trim() === "1";
|
|
1390
|
-
return false;
|
|
934
|
+
var pct = (used, resets, primary) => percentMetric("", used, resets ?? null, primary);
|
|
935
|
+
function boolValue(value) {
|
|
936
|
+
if (typeof value === "boolean") return value;
|
|
937
|
+
if (typeof value === "number") return value !== 0;
|
|
938
|
+
if (typeof value === "string") return value.trim().toLowerCase() === "true" || value.trim() === "1";
|
|
939
|
+
return false;
|
|
1391
940
|
}
|
|
1392
941
|
function resetFrom(value) {
|
|
1393
942
|
if (typeof value === "string" && value.trim()) return resetIn(value);
|
|
@@ -1531,11 +1080,11 @@ var claudeProvider = {
|
|
|
1531
1080
|
};
|
|
1532
1081
|
|
|
1533
1082
|
// src/providers/codex/usage.ts
|
|
1534
|
-
import { stat as fsStat2, access as
|
|
1083
|
+
import { stat as fsStat2, access as access3, open as openFile } from "fs/promises";
|
|
1535
1084
|
import { createReadStream as createReadStream2 } from "fs";
|
|
1536
1085
|
import { createInterface as createInterface2 } from "readline";
|
|
1537
|
-
import { join as
|
|
1538
|
-
import { homedir as
|
|
1086
|
+
import { join as join5 } from "path";
|
|
1087
|
+
import { homedir as homedir4 } from "os";
|
|
1539
1088
|
var PRICING2 = {
|
|
1540
1089
|
"gpt-5.5-codex": { in: 5e-6, cr: 5e-7, out: 3e-5 },
|
|
1541
1090
|
"gpt-5.5": { in: 5e-6, cr: 5e-7, out: 3e-5 },
|
|
@@ -1555,23 +1104,23 @@ var PRICING2 = {
|
|
|
1555
1104
|
var FALLBACK_PRICE = PRICING2["gpt-5.5"];
|
|
1556
1105
|
var PRICE_KEYS2 = Object.keys(PRICING2).sort((a, b) => b.length - a.length);
|
|
1557
1106
|
function codexHomes(homeDir) {
|
|
1558
|
-
if (homeDir) return [.../* @__PURE__ */ new Set([
|
|
1107
|
+
if (homeDir) return [.../* @__PURE__ */ new Set([join5(homeDir, ".codex"), homeDir])];
|
|
1559
1108
|
const homes = [];
|
|
1560
1109
|
const codexHome = envDir("CODEX_HOME");
|
|
1561
1110
|
if (codexHome) homes.push(codexHome);
|
|
1562
|
-
homes.push(
|
|
1563
|
-
homes.push(
|
|
1111
|
+
homes.push(join5(homedir4(), ".codex"));
|
|
1112
|
+
homes.push(join5(homedir4(), ".config", "codex"));
|
|
1564
1113
|
return [...new Set(homes)];
|
|
1565
1114
|
}
|
|
1566
1115
|
async function detectCodex(homeDir) {
|
|
1567
1116
|
for (const home of codexHomes(homeDir)) {
|
|
1568
1117
|
try {
|
|
1569
|
-
await
|
|
1118
|
+
await access3(join5(home, "sessions"));
|
|
1570
1119
|
return true;
|
|
1571
1120
|
} catch {
|
|
1572
1121
|
}
|
|
1573
1122
|
try {
|
|
1574
|
-
await
|
|
1123
|
+
await access3(join5(home, "archived_sessions"));
|
|
1575
1124
|
return true;
|
|
1576
1125
|
} catch {
|
|
1577
1126
|
}
|
|
@@ -1805,11 +1354,11 @@ async function loadEntries2(since, homeDir) {
|
|
|
1805
1354
|
const seen = /* @__PURE__ */ new Set();
|
|
1806
1355
|
const seenIno = /* @__PURE__ */ new Set();
|
|
1807
1356
|
for (const home of codexHomes(homeDir)) {
|
|
1808
|
-
for (const dir of [
|
|
1357
|
+
for (const dir of [join5(home, "sessions"), join5(home, "archived_sessions")]) {
|
|
1809
1358
|
const listing = await walkFiles(dir);
|
|
1810
1359
|
for (const f of listing) {
|
|
1811
1360
|
if (!f.endsWith(".jsonl")) continue;
|
|
1812
|
-
const path =
|
|
1361
|
+
const path = join5(dir, f);
|
|
1813
1362
|
if (seen.has(path)) continue;
|
|
1814
1363
|
seen.add(path);
|
|
1815
1364
|
try {
|
|
@@ -1838,11 +1387,32 @@ async function codexTable(tz, homeDir) {
|
|
|
1838
1387
|
}
|
|
1839
1388
|
|
|
1840
1389
|
// src/providers/codex/billing.ts
|
|
1841
|
-
import { readFile as readFile3, readdir as readdir2, stat as fsStat3 } from "fs/promises";
|
|
1842
|
-
import { createReadStream as createReadStream3 } from "fs";
|
|
1843
|
-
import { createInterface as createInterface3 } from "readline";
|
|
1844
|
-
import { join as
|
|
1845
|
-
|
|
1390
|
+
import { readFile as readFile3, readdir as readdir2, stat as fsStat3 } from "fs/promises";
|
|
1391
|
+
import { createReadStream as createReadStream3 } from "fs";
|
|
1392
|
+
import { createInterface as createInterface3 } from "readline";
|
|
1393
|
+
import { join as join6 } from "path";
|
|
1394
|
+
|
|
1395
|
+
// src/providers/_shared/jwt.ts
|
|
1396
|
+
function decodeBase64UrlJson(segment) {
|
|
1397
|
+
try {
|
|
1398
|
+
const normalized = segment.replace(/-/g, "+").replace(/_/g, "/");
|
|
1399
|
+
const padded = normalized + "=".repeat((4 - normalized.length % 4) % 4);
|
|
1400
|
+
return JSON.parse(Buffer.from(padded, "base64").toString("utf8"));
|
|
1401
|
+
} catch {
|
|
1402
|
+
return null;
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
function identityFromIdToken(idToken) {
|
|
1406
|
+
if (typeof idToken !== "string" || !idToken.includes(".")) return {};
|
|
1407
|
+
const payload = decodeBase64UrlJson(idToken.split(".")[1]);
|
|
1408
|
+
if (!payload || typeof payload !== "object") return {};
|
|
1409
|
+
const email = typeof payload.email === "string" && payload.email.trim() ? payload.email.trim() : void 0;
|
|
1410
|
+
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;
|
|
1411
|
+
return { email, displayName, payload };
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
// src/providers/codex/billing.ts
|
|
1415
|
+
var USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
1846
1416
|
var RESET_CREDITS_URL = "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits";
|
|
1847
1417
|
var CREDIT_USD_RATE = 0.04;
|
|
1848
1418
|
function chatGptPlanLabel(planType) {
|
|
@@ -1867,258 +1437,694 @@ function codexIdentity(idToken) {
|
|
|
1867
1437
|
}
|
|
1868
1438
|
async function readAuthFile(home) {
|
|
1869
1439
|
try {
|
|
1870
|
-
const raw = await readFile3(
|
|
1440
|
+
const raw = await readFile3(join6(home, "auth.json"), "utf-8");
|
|
1441
|
+
const auth = JSON.parse(raw);
|
|
1442
|
+
const accessToken = auth?.tokens?.access_token;
|
|
1443
|
+
if (!accessToken) return null;
|
|
1444
|
+
return { accessToken, accountId: auth?.tokens?.account_id, ...codexIdentity(auth?.tokens?.id_token) };
|
|
1445
|
+
} catch {
|
|
1446
|
+
return null;
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
async function readKeychainAuth() {
|
|
1450
|
+
try {
|
|
1451
|
+
const raw = await readMacKeychainRaw("Codex Auth");
|
|
1452
|
+
if (!raw) return null;
|
|
1871
1453
|
const auth = JSON.parse(raw);
|
|
1872
1454
|
const accessToken = auth?.tokens?.access_token;
|
|
1873
1455
|
if (!accessToken) return null;
|
|
1874
1456
|
return { accessToken, accountId: auth?.tokens?.account_id, ...codexIdentity(auth?.tokens?.id_token) };
|
|
1875
1457
|
} catch {
|
|
1876
|
-
return null;
|
|
1458
|
+
return null;
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
async function getAuth2(homeDir) {
|
|
1462
|
+
for (const home of codexHomes(homeDir)) {
|
|
1463
|
+
const auth = await readAuthFile(home);
|
|
1464
|
+
if (auth) return auth;
|
|
1465
|
+
}
|
|
1466
|
+
if (!homeDir && process.platform === "darwin") return readKeychainAuth();
|
|
1467
|
+
return null;
|
|
1468
|
+
}
|
|
1469
|
+
function planLabel2(planType) {
|
|
1470
|
+
if (typeof planType !== "string" || !planType.trim()) return null;
|
|
1471
|
+
const p = planType.trim().toLowerCase();
|
|
1472
|
+
if (p === "prolite") return "Pro 5x";
|
|
1473
|
+
if (p === "pro") return "Pro 20x";
|
|
1474
|
+
return planType.charAt(0).toUpperCase() + planType.slice(1);
|
|
1475
|
+
}
|
|
1476
|
+
function resetDateMs(window) {
|
|
1477
|
+
if (!window) return null;
|
|
1478
|
+
const absolute = numberValue(window.reset_at ?? window.resets_at);
|
|
1479
|
+
if (absolute !== void 0) return absolute > 1e10 ? absolute : absolute * 1e3;
|
|
1480
|
+
const after = numberValue(window.reset_after_seconds);
|
|
1481
|
+
if (after !== void 0) return Date.now() + after * 1e3;
|
|
1482
|
+
for (const key of ["reset_at", "resets_at"]) {
|
|
1483
|
+
const raw = window[key];
|
|
1484
|
+
if (typeof raw === "string" && raw.trim()) {
|
|
1485
|
+
const parsed = new Date(raw).getTime();
|
|
1486
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
return null;
|
|
1490
|
+
}
|
|
1491
|
+
function resetFrom2(window) {
|
|
1492
|
+
const ms = resetDateMs(window);
|
|
1493
|
+
const iso = ms === null ? null : msToIso(ms);
|
|
1494
|
+
return iso ? resetIn(iso) : null;
|
|
1495
|
+
}
|
|
1496
|
+
function windowSeconds(window) {
|
|
1497
|
+
const seconds = numberValue(window?.limit_window_seconds ?? window?.period_seconds ?? window?.window_seconds);
|
|
1498
|
+
if (seconds !== void 0) return seconds;
|
|
1499
|
+
const minutes = numberValue(window?.limit_window_minutes ?? window?.window_minutes ?? window?.period_minutes);
|
|
1500
|
+
return minutes === void 0 ? void 0 : minutes * 60;
|
|
1501
|
+
}
|
|
1502
|
+
function normalizedUsedPercent(window, percent) {
|
|
1503
|
+
const used = numberValue(percent);
|
|
1504
|
+
if (used === void 0) return void 0;
|
|
1505
|
+
const periodSeconds = windowSeconds(window);
|
|
1506
|
+
const resetMs = resetDateMs(window);
|
|
1507
|
+
if (periodSeconds && resetMs) {
|
|
1508
|
+
const elapsedMs = periodSeconds * 1e3 - Math.max(0, resetMs - Date.now());
|
|
1509
|
+
if (elapsedMs <= 6e4 && used <= 1) return 0;
|
|
1510
|
+
}
|
|
1511
|
+
return Math.max(0, used);
|
|
1512
|
+
}
|
|
1513
|
+
function metricLabelForAdditional(window) {
|
|
1514
|
+
const name = String(window?.limit_name ?? window?.name ?? window?.metered_feature ?? window?.feature ?? "").toLowerCase();
|
|
1515
|
+
if (!name.includes("spark")) return null;
|
|
1516
|
+
const seconds = windowSeconds(window);
|
|
1517
|
+
return name.includes("week") || seconds !== void 0 && seconds >= 6 * 24 * 60 * 60 ? "Spark Weekly" : "Spark";
|
|
1518
|
+
}
|
|
1519
|
+
function additionalRateLimits(rl) {
|
|
1520
|
+
const raw = rl?.additional_rate_limits ?? rl?.additionalRateLimits ?? rl?.additional;
|
|
1521
|
+
if (Array.isArray(raw)) return raw;
|
|
1522
|
+
if (raw && typeof raw === "object") return Object.values(raw);
|
|
1523
|
+
return [];
|
|
1524
|
+
}
|
|
1525
|
+
function appendWindowMetrics(metrics, rl, headerPct) {
|
|
1526
|
+
const primary = rl?.primary_window ?? rl?.primary ?? null;
|
|
1527
|
+
const secondary = rl?.secondary_window ?? rl?.secondary ?? null;
|
|
1528
|
+
const primaryPct = normalizedUsedPercent(primary, primary?.used_percent ?? primary?.percent_used ?? headerPct?.("x-codex-primary-used-percent"));
|
|
1529
|
+
const secondaryPct = normalizedUsedPercent(secondary, secondary?.used_percent ?? secondary?.percent_used ?? headerPct?.("x-codex-secondary-used-percent"));
|
|
1530
|
+
if (primaryPct !== void 0) metrics.push(percentMetric("Session", primaryPct, resetFrom2(primary), true));
|
|
1531
|
+
if (secondaryPct !== void 0) metrics.push(percentMetric("Weekly", secondaryPct, resetFrom2(secondary)));
|
|
1532
|
+
for (const item of additionalRateLimits(rl)) {
|
|
1533
|
+
const label = metricLabelForAdditional(item);
|
|
1534
|
+
if (!label) continue;
|
|
1535
|
+
const used = normalizedUsedPercent(item, item?.used_percent ?? item?.percent_used);
|
|
1536
|
+
if (used === void 0) continue;
|
|
1537
|
+
metrics.push(percentMetric(label, used, resetFrom2(item)));
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
function appendCredits(metrics, source) {
|
|
1541
|
+
const balance = numberValue(source?.credits?.balance ?? source?.credit_balance);
|
|
1542
|
+
if (balance !== void 0 && balance >= 0) {
|
|
1543
|
+
metrics.push({ label: "Credits", used: balance * CREDIT_USD_RATE, limit: null, format: { kind: "dollars" } });
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
async function fetchResetCredits(headers) {
|
|
1547
|
+
try {
|
|
1548
|
+
const res = await fetch(RESET_CREDITS_URL, {
|
|
1549
|
+
headers: {
|
|
1550
|
+
...headers,
|
|
1551
|
+
"OpenAI-Beta": "codex-1",
|
|
1552
|
+
"originator": "Codex Desktop"
|
|
1553
|
+
},
|
|
1554
|
+
signal: AbortSignal.timeout(1e4)
|
|
1555
|
+
});
|
|
1556
|
+
if (!res.ok) return void 0;
|
|
1557
|
+
const data = await readJson(res);
|
|
1558
|
+
return numberValue(data?.available_count ?? data?.available ?? data?.remaining);
|
|
1559
|
+
} catch {
|
|
1560
|
+
return void 0;
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
async function liveBilling(auth) {
|
|
1564
|
+
try {
|
|
1565
|
+
const headers = {
|
|
1566
|
+
"Authorization": `Bearer ${auth.accessToken}`,
|
|
1567
|
+
"Accept": "application/json",
|
|
1568
|
+
"User-Agent": "tokmon"
|
|
1569
|
+
};
|
|
1570
|
+
if (auth.accountId) headers["ChatGPT-Account-Id"] = auth.accountId;
|
|
1571
|
+
const res = await fetch(USAGE_URL, { headers, signal: AbortSignal.timeout(1e4) });
|
|
1572
|
+
if (!res.ok) return null;
|
|
1573
|
+
const data = await readJson(res);
|
|
1574
|
+
if (!data) return null;
|
|
1575
|
+
const headerPct = (name) => {
|
|
1576
|
+
const h = res.headers.get(name);
|
|
1577
|
+
if (h === null || h.trim() === "") return void 0;
|
|
1578
|
+
const n = Number(h);
|
|
1579
|
+
return Number.isFinite(n) ? n : void 0;
|
|
1580
|
+
};
|
|
1581
|
+
const metrics = [];
|
|
1582
|
+
appendWindowMetrics(metrics, data.rate_limit ?? data, headerPct);
|
|
1583
|
+
appendCredits(metrics, data);
|
|
1584
|
+
const resetCredits = numberValue(data?.rate_limit_reset_credits?.available_count ?? data?.rate_limit_reset_credits?.available) ?? await fetchResetCredits(headers);
|
|
1585
|
+
if (resetCredits !== void 0 && resetCredits >= 0) {
|
|
1586
|
+
metrics.push({ label: "Resets", used: resetCredits, limit: null, format: { kind: "count", suffix: "available" } });
|
|
1587
|
+
}
|
|
1588
|
+
if (metrics.length === 0) return null;
|
|
1589
|
+
return { plan: auth.plan ?? planLabel2(data.plan_type), metrics, error: null, ...identityFields(auth) };
|
|
1590
|
+
} catch {
|
|
1591
|
+
return null;
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
var SNAPSHOT_CANDIDATES = 8;
|
|
1595
|
+
var SNAPSHOT_STALE_MS = 24 * 36e5;
|
|
1596
|
+
async function newestRolloutFiles(homeDir) {
|
|
1597
|
+
const all = [];
|
|
1598
|
+
for (const home of codexHomes(homeDir)) {
|
|
1599
|
+
const dir = join6(home, "sessions");
|
|
1600
|
+
let listing;
|
|
1601
|
+
try {
|
|
1602
|
+
listing = await readdir2(dir, { recursive: true });
|
|
1603
|
+
} catch {
|
|
1604
|
+
continue;
|
|
1605
|
+
}
|
|
1606
|
+
for (const f of listing) {
|
|
1607
|
+
if (!f.endsWith(".jsonl") || !f.includes("rollout-")) continue;
|
|
1608
|
+
const path = join6(dir, f);
|
|
1609
|
+
try {
|
|
1610
|
+
const s = await fsStat3(path);
|
|
1611
|
+
all.push({ path, mtime: s.mtimeMs });
|
|
1612
|
+
} catch {
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
return all.sort((a, b) => b.mtime - a.mtime).slice(0, SNAPSHOT_CANDIDATES);
|
|
1617
|
+
}
|
|
1618
|
+
async function lastRateLimits(path) {
|
|
1619
|
+
let last = null;
|
|
1620
|
+
try {
|
|
1621
|
+
const input = createReadStream3(path);
|
|
1622
|
+
input.on("error", () => {
|
|
1623
|
+
});
|
|
1624
|
+
const rl = createInterface3({ input, crlfDelay: Infinity });
|
|
1625
|
+
for await (const line of rl) {
|
|
1626
|
+
if (!line.includes("rate_limits")) continue;
|
|
1627
|
+
try {
|
|
1628
|
+
const obj = JSON.parse(line);
|
|
1629
|
+
if (obj?.payload?.rate_limits) last = obj.payload.rate_limits;
|
|
1630
|
+
} catch {
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
} catch {
|
|
1634
|
+
return null;
|
|
1635
|
+
}
|
|
1636
|
+
return last;
|
|
1637
|
+
}
|
|
1638
|
+
async function snapshotBilling(homeDir, auth = null) {
|
|
1639
|
+
for (const file of await newestRolloutFiles(homeDir)) {
|
|
1640
|
+
const last = await lastRateLimits(file.path);
|
|
1641
|
+
if (!last) continue;
|
|
1642
|
+
const metrics = [];
|
|
1643
|
+
appendWindowMetrics(metrics, last.rate_limit ?? last);
|
|
1644
|
+
appendCredits(metrics, last);
|
|
1645
|
+
const resetCredits = numberValue(last?.rate_limit_reset_credits?.available_count ?? last?.rate_limit_reset_credits?.available);
|
|
1646
|
+
if (resetCredits !== void 0 && resetCredits >= 0) {
|
|
1647
|
+
metrics.push({ label: "Resets", used: resetCredits, limit: null, format: { kind: "count", suffix: "available" } });
|
|
1648
|
+
}
|
|
1649
|
+
if (metrics.length === 0) continue;
|
|
1650
|
+
return { plan: auth?.plan ?? planLabel2(last.plan_type), metrics, error: null, asOfMs: file.mtime, ...identityFields(auth) };
|
|
1651
|
+
}
|
|
1652
|
+
return null;
|
|
1653
|
+
}
|
|
1654
|
+
async function codexBilling(account) {
|
|
1655
|
+
const auth = await getAuth2(account.homeDir);
|
|
1656
|
+
if (auth) {
|
|
1657
|
+
const live = await liveBilling(auth);
|
|
1658
|
+
if (live) return live;
|
|
1659
|
+
}
|
|
1660
|
+
const snap = await snapshotBilling(account.homeDir, auth);
|
|
1661
|
+
if (snap && Date.now() - snap.asOfMs < SNAPSHOT_STALE_MS) {
|
|
1662
|
+
const { asOfMs: _asOfMs, ...result } = snap;
|
|
1663
|
+
return result;
|
|
1664
|
+
}
|
|
1665
|
+
return {
|
|
1666
|
+
plan: auth?.plan ?? snap?.plan ?? null,
|
|
1667
|
+
metrics: [],
|
|
1668
|
+
error: auth ? "Usage API failed \u2014 run codex to refresh" : "Not logged in \u2014 run codex",
|
|
1669
|
+
...identityFields(auth)
|
|
1670
|
+
};
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
// src/providers/codex/index.ts
|
|
1674
|
+
var codexProvider = {
|
|
1675
|
+
id: "codex",
|
|
1676
|
+
name: "Codex",
|
|
1677
|
+
color: "cyan",
|
|
1678
|
+
hasUsage: true,
|
|
1679
|
+
hasBilling: true,
|
|
1680
|
+
detect: (homeDir) => detectCodex(homeDir),
|
|
1681
|
+
fetchSummary: (account, tz) => codexDashboard(tz, account.homeDir),
|
|
1682
|
+
fetchTable: (account, tz) => codexTable(tz, account.homeDir),
|
|
1683
|
+
fetchBilling: (account) => codexBilling(account)
|
|
1684
|
+
};
|
|
1685
|
+
|
|
1686
|
+
// src/providers/cursor/billing.ts
|
|
1687
|
+
import { access as access4 } from "fs/promises";
|
|
1688
|
+
import { join as join9 } from "path";
|
|
1689
|
+
import { homedir as homedir6 } from "os";
|
|
1690
|
+
|
|
1691
|
+
// src/providers/cursor/activity.ts
|
|
1692
|
+
import { join as join8 } from "path";
|
|
1693
|
+
import { homedir as homedir5 } from "os";
|
|
1694
|
+
|
|
1695
|
+
// src/providers/cursor/sqlite.ts
|
|
1696
|
+
import { execFile as execFileCb2 } from "child_process";
|
|
1697
|
+
import { accessSync, constants } from "fs";
|
|
1698
|
+
import { delimiter, join as join7 } from "path";
|
|
1699
|
+
import { promisify as promisify2 } from "util";
|
|
1700
|
+
var execFile2 = promisify2(execFileCb2);
|
|
1701
|
+
var nativeDb;
|
|
1702
|
+
async function getNativeDb() {
|
|
1703
|
+
if (nativeDb !== void 0) return nativeDb;
|
|
1704
|
+
try {
|
|
1705
|
+
nativeDb = (await import("sqlite")).DatabaseSync;
|
|
1706
|
+
} catch {
|
|
1707
|
+
nativeDb = null;
|
|
1877
1708
|
}
|
|
1709
|
+
return nativeDb;
|
|
1878
1710
|
}
|
|
1879
|
-
|
|
1711
|
+
function classify(msg) {
|
|
1712
|
+
if (/database is (locked|busy)|SQLITE_(BUSY|LOCKED)|EBUSY|sharing violation|resource busy|readonly/i.test(msg)) return "locked";
|
|
1713
|
+
if (/unable to open|no such file|cannot open|ENOENT/i.test(msg)) return "missing";
|
|
1714
|
+
if (/no such (function|table|column)|unknown option/i.test(msg)) return "old";
|
|
1715
|
+
return "error";
|
|
1716
|
+
}
|
|
1717
|
+
async function runSqlite(db, sql, params = []) {
|
|
1718
|
+
const DB = await getNativeDb();
|
|
1719
|
+
if (DB) {
|
|
1720
|
+
let handle;
|
|
1721
|
+
let nativeErr;
|
|
1722
|
+
try {
|
|
1723
|
+
try {
|
|
1724
|
+
handle = new DB(db, { readOnly: true, timeout: 1500 });
|
|
1725
|
+
} catch (e) {
|
|
1726
|
+
if (!(e instanceof TypeError)) throw e;
|
|
1727
|
+
handle = new DB(db, { readOnly: true });
|
|
1728
|
+
}
|
|
1729
|
+
const rows = handle.prepare(sql).all(...params);
|
|
1730
|
+
return { status: "ok", rows };
|
|
1731
|
+
} catch (e) {
|
|
1732
|
+
nativeErr = e;
|
|
1733
|
+
} finally {
|
|
1734
|
+
try {
|
|
1735
|
+
handle?.close();
|
|
1736
|
+
} catch {
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
if (nativeErr) {
|
|
1740
|
+
return { status: classify(String(nativeErr?.message ?? nativeErr)), rows: [] };
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
return runSqliteCli(db, sql, params);
|
|
1744
|
+
}
|
|
1745
|
+
function isExec(p) {
|
|
1880
1746
|
try {
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
const auth = JSON.parse(raw);
|
|
1884
|
-
const accessToken = auth?.tokens?.access_token;
|
|
1885
|
-
if (!accessToken) return null;
|
|
1886
|
-
return { accessToken, accountId: auth?.tokens?.account_id, ...codexIdentity(auth?.tokens?.id_token) };
|
|
1747
|
+
accessSync(p, process.platform === "win32" ? constants.F_OK : constants.X_OK);
|
|
1748
|
+
return true;
|
|
1887
1749
|
} catch {
|
|
1888
|
-
return
|
|
1750
|
+
return false;
|
|
1889
1751
|
}
|
|
1890
1752
|
}
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1753
|
+
function resolveSqliteCli() {
|
|
1754
|
+
const names = process.platform === "win32" ? ["sqlite3.exe", "sqlite3.cmd", "sqlite3.bat"] : ["sqlite3"];
|
|
1755
|
+
for (const dir of (process.env.PATH ?? "").split(delimiter).filter(Boolean)) {
|
|
1756
|
+
for (const name of names) {
|
|
1757
|
+
const path = join7(dir, name);
|
|
1758
|
+
if (isExec(path)) return path;
|
|
1759
|
+
}
|
|
1895
1760
|
}
|
|
1896
|
-
if (!homeDir && process.platform === "darwin") return readKeychainAuth();
|
|
1897
1761
|
return null;
|
|
1898
1762
|
}
|
|
1899
|
-
function
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1763
|
+
function inlineParams(sql, params) {
|
|
1764
|
+
let i = 0;
|
|
1765
|
+
return sql.replace(/\?/g, () => {
|
|
1766
|
+
const p = params[i++];
|
|
1767
|
+
return typeof p === "number" ? String(p) : `'${String(p).replace(/'/g, "''")}'`;
|
|
1768
|
+
});
|
|
1905
1769
|
}
|
|
1906
|
-
function
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1770
|
+
async function runSqliteCli(db, sql, params) {
|
|
1771
|
+
const sqlite = resolveSqliteCli();
|
|
1772
|
+
if (!sqlite) return { status: "missing", rows: [] };
|
|
1773
|
+
try {
|
|
1774
|
+
const { stdout } = await execFile2(
|
|
1775
|
+
sqlite,
|
|
1776
|
+
["-readonly", "-json", "-cmd", ".timeout 1500", db, inlineParams(sql, params)],
|
|
1777
|
+
{ timeout: 1e4, maxBuffer: 8 << 20 }
|
|
1778
|
+
);
|
|
1779
|
+
const text = stdout.trim();
|
|
1780
|
+
if (!text) return { status: "ok", rows: [] };
|
|
1781
|
+
try {
|
|
1782
|
+
return { status: "ok", rows: JSON.parse(text) };
|
|
1783
|
+
} catch {
|
|
1784
|
+
return { status: "error", rows: [] };
|
|
1917
1785
|
}
|
|
1786
|
+
} catch (e) {
|
|
1787
|
+
const err = e;
|
|
1788
|
+
if (err?.code === "ENOENT") return { status: "missing", rows: [] };
|
|
1789
|
+
return { status: classify(String(err?.stderr ?? err?.message ?? "")), rows: [] };
|
|
1918
1790
|
}
|
|
1919
|
-
return null;
|
|
1920
1791
|
}
|
|
1921
|
-
function
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1792
|
+
function sqliteStatusMessage(status) {
|
|
1793
|
+
switch (status) {
|
|
1794
|
+
case "missing":
|
|
1795
|
+
return "Cursor data not found \u2014 open Cursor";
|
|
1796
|
+
case "old":
|
|
1797
|
+
return "Cursor DB unreadable";
|
|
1798
|
+
case "locked":
|
|
1799
|
+
return "Cursor DB busy \u2014 retrying next poll";
|
|
1800
|
+
default:
|
|
1801
|
+
return "Cursor data unavailable";
|
|
1802
|
+
}
|
|
1925
1803
|
}
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1804
|
+
|
|
1805
|
+
// src/providers/cursor/activity.ts
|
|
1806
|
+
var DAY_MS2 = 864e5;
|
|
1807
|
+
var HOUR_MS = 36e5;
|
|
1808
|
+
function trackingDb(homeDir) {
|
|
1809
|
+
return join8(homeDir ?? homedir5(), ".cursor", "ai-tracking", "ai-code-tracking.db");
|
|
1931
1810
|
}
|
|
1932
|
-
function
|
|
1933
|
-
const
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1811
|
+
async function cursorActivity(tz, homeDir) {
|
|
1812
|
+
const db = trackingDb(homeDir);
|
|
1813
|
+
try {
|
|
1814
|
+
const now = Date.now();
|
|
1815
|
+
const res = await runSqlite(
|
|
1816
|
+
db,
|
|
1817
|
+
`SELECT createdAt/${HOUR_MS} AS h, count(*) AS c FROM ai_code_hashes WHERE source!='human' AND createdAt >= ${Math.floor(now - 30 * DAY_MS2)} GROUP BY h;`
|
|
1818
|
+
);
|
|
1819
|
+
if (res.status !== "ok") return null;
|
|
1820
|
+
const byDay = /* @__PURE__ */ new Map();
|
|
1821
|
+
let month = 0;
|
|
1822
|
+
for (const row of res.rows) {
|
|
1823
|
+
const raw = Number(row.c);
|
|
1824
|
+
const n = Number.isFinite(raw) && raw > 0 ? raw : 0;
|
|
1825
|
+
const hour = Number(row.h);
|
|
1826
|
+
if (!Number.isFinite(hour)) continue;
|
|
1827
|
+
const key = dayKey(hour * HOUR_MS + HOUR_MS / 2, tz);
|
|
1828
|
+
byDay.set(key, (byDay.get(key) ?? 0) + n);
|
|
1829
|
+
month += n;
|
|
1830
|
+
}
|
|
1831
|
+
const series = lastDayKeys(now, tz, SPARK_DAYS).map((k) => byDay.get(k) ?? 0);
|
|
1832
|
+
if (month === 0 && series.every((v) => v === 0)) return null;
|
|
1833
|
+
return { series, summary: `${tokens(month)} lines` };
|
|
1834
|
+
} catch {
|
|
1835
|
+
return null;
|
|
1940
1836
|
}
|
|
1941
|
-
return Math.max(0, used);
|
|
1942
1837
|
}
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
const
|
|
1947
|
-
|
|
1838
|
+
|
|
1839
|
+
// src/providers/cursor/composer.ts
|
|
1840
|
+
async function cursorModelSpend(homeDir) {
|
|
1841
|
+
const db = cursorStateDb(homeDir);
|
|
1842
|
+
const sql = "SELECT mk.key AS name, sum(json_extract(mk.value,'$.costInCents')) AS cents, sum(json_extract(mk.value,'$.amount')) AS amt FROM cursorDiskKV c, json_each(c.value,'$.usageData') mk WHERE c.key LIKE 'composerData:%' AND json_valid(c.value) AND json_type(c.value,'$.usageData')='object' GROUP BY mk.key ORDER BY cents DESC;";
|
|
1843
|
+
const res = await runSqlite(db, sql);
|
|
1844
|
+
if (res.status !== "ok") return null;
|
|
1845
|
+
const models = [];
|
|
1846
|
+
let total = 0;
|
|
1847
|
+
for (const row of res.rows) {
|
|
1848
|
+
const usd = finitePositiveCoerced(row.cents) / 100;
|
|
1849
|
+
if (usd <= 0) continue;
|
|
1850
|
+
models.push({ name: String(row.name ?? ""), usd, requests: finitePositiveCoerced(row.amt) });
|
|
1851
|
+
total += usd;
|
|
1852
|
+
}
|
|
1853
|
+
if (total <= 0) return null;
|
|
1854
|
+
return { total, models };
|
|
1948
1855
|
}
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
if (
|
|
1953
|
-
|
|
1856
|
+
var USAGE_SQL = "SELECT json_extract(c.value,'$.createdAt') AS createdAt, mk.key AS model, sum(json_extract(mk.value,'$.costInCents')) AS cents, sum(json_extract(mk.value,'$.amount')) AS amt FROM cursorDiskKV c, json_each(c.value,'$.usageData') mk WHERE c.key LIKE 'composerData:%' AND json_valid(c.value) AND json_type(c.value,'$.usageData')='object' AND json_extract(c.value,'$.createdAt') IS NOT NULL GROUP BY createdAt, model;";
|
|
1857
|
+
async function cursorUsageTable(tz, homeDir) {
|
|
1858
|
+
const res = await runSqlite(cursorStateDb(homeDir), USAGE_SQL);
|
|
1859
|
+
if (res.status !== "ok" || res.rows.length === 0) return null;
|
|
1860
|
+
const buckets = { daily: /* @__PURE__ */ new Map(), weekly: /* @__PURE__ */ new Map(), monthly: /* @__PURE__ */ new Map() };
|
|
1861
|
+
const put = (map, label, model, usd, reqs) => {
|
|
1862
|
+
let row = map.get(label);
|
|
1863
|
+
if (!row) {
|
|
1864
|
+
row = { label, models: [], input: 0, output: 0, cacheCreate: 0, cacheRead: 0, cacheSavings: 0, total: 0, cost: 0, count: 0, breakdown: [] };
|
|
1865
|
+
map.set(label, row);
|
|
1866
|
+
}
|
|
1867
|
+
row.cost += usd;
|
|
1868
|
+
row.count += reqs;
|
|
1869
|
+
let md = row.breakdown.find((b) => b.name === model);
|
|
1870
|
+
if (!md) {
|
|
1871
|
+
md = { name: model, input: 0, output: 0, cacheCreate: 0, cacheRead: 0, cacheSavings: 0, cost: 0, count: 0 };
|
|
1872
|
+
row.breakdown.push(md);
|
|
1873
|
+
}
|
|
1874
|
+
md.cost += usd;
|
|
1875
|
+
md.count += reqs;
|
|
1876
|
+
};
|
|
1877
|
+
for (const r of res.rows) {
|
|
1878
|
+
const ts = Number(r.createdAt);
|
|
1879
|
+
if (!Number.isFinite(ts) || ts <= 0) continue;
|
|
1880
|
+
const usd = finitePositiveCoerced(r.cents) / 100;
|
|
1881
|
+
const reqs = finitePositiveCoerced(r.amt);
|
|
1882
|
+
if (usd <= 0 && reqs <= 0) continue;
|
|
1883
|
+
const model = String(r.model ?? "unknown");
|
|
1884
|
+
put(buckets.daily, dayKey(ts, tz), model, usd, reqs);
|
|
1885
|
+
put(buckets.weekly, weekKey(ts, tz), model, usd, reqs);
|
|
1886
|
+
put(buckets.monthly, monthKey(ts, tz), model, usd, reqs);
|
|
1887
|
+
}
|
|
1888
|
+
const finalize = (map) => [...map.values()].map((row) => {
|
|
1889
|
+
row.breakdown.sort((a, b) => b.cost - a.cost);
|
|
1890
|
+
row.models = row.breakdown.map((b) => b.name);
|
|
1891
|
+
return row;
|
|
1892
|
+
}).sort((a, b) => a.label.localeCompare(b.label));
|
|
1893
|
+
const table = { daily: finalize(buckets.daily), weekly: finalize(buckets.weekly), monthly: finalize(buckets.monthly) };
|
|
1894
|
+
return table.daily.length ? table : null;
|
|
1954
1895
|
}
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1896
|
+
|
|
1897
|
+
// src/providers/cursor/billing.ts
|
|
1898
|
+
var BASE = "https://api2.cursor.sh/aiserver.v1.DashboardService";
|
|
1899
|
+
var USAGE_URL2 = `${BASE}/GetCurrentPeriodUsage`;
|
|
1900
|
+
var PLAN_URL = `${BASE}/GetPlanInfo`;
|
|
1901
|
+
var CREDITS_URL = `${BASE}/GetCreditGrantsBalance`;
|
|
1902
|
+
var STRIPE_URL = "https://cursor.com/api/auth/stripe";
|
|
1903
|
+
function cursorStateDb(homeDir) {
|
|
1904
|
+
const base = homeDir ?? homedir6();
|
|
1905
|
+
const tail = ["Cursor", "User", "globalStorage", "state.vscdb"];
|
|
1906
|
+
if (process.platform === "darwin") {
|
|
1907
|
+
return join9(base, "Library", "Application Support", ...tail);
|
|
1908
|
+
}
|
|
1909
|
+
if (process.platform === "win32") {
|
|
1910
|
+
const roaming = homeDir ? join9(homeDir, "AppData", "Roaming") : envDir("APPDATA") ?? join9(base, "AppData", "Roaming");
|
|
1911
|
+
return join9(roaming, ...tail);
|
|
1912
|
+
}
|
|
1913
|
+
const cfg = homeDir ? join9(homeDir, ".config") : envDir("XDG_CONFIG_HOME") ?? join9(base, ".config");
|
|
1914
|
+
return join9(cfg, ...tail);
|
|
1915
|
+
}
|
|
1916
|
+
async function detectCursor(homeDir) {
|
|
1917
|
+
try {
|
|
1918
|
+
await access4(cursorStateDb(homeDir));
|
|
1919
|
+
return true;
|
|
1920
|
+
} catch {
|
|
1921
|
+
return false;
|
|
1968
1922
|
}
|
|
1969
1923
|
}
|
|
1970
|
-
function
|
|
1971
|
-
const
|
|
1972
|
-
|
|
1973
|
-
|
|
1924
|
+
async function readState(db, key) {
|
|
1925
|
+
const r = await runSqlite(db, "SELECT value FROM ItemTable WHERE key=? LIMIT 1;", [key]);
|
|
1926
|
+
const raw = r.status === "ok" ? r.rows[0]?.value : void 0;
|
|
1927
|
+
return { value: typeof raw === "string" && raw.trim() ? raw.trim() : null, status: r.status };
|
|
1928
|
+
}
|
|
1929
|
+
function cleanStoredString(value) {
|
|
1930
|
+
if (!value) return void 0;
|
|
1931
|
+
try {
|
|
1932
|
+
const parsed = JSON.parse(value);
|
|
1933
|
+
if (typeof parsed === "string" && parsed.trim()) return parsed.trim();
|
|
1934
|
+
} catch {
|
|
1974
1935
|
}
|
|
1936
|
+
const trimmed = value.trim().replace(/^"|"$/g, "");
|
|
1937
|
+
return trimmed || void 0;
|
|
1975
1938
|
}
|
|
1976
|
-
|
|
1939
|
+
function cursorSessionToken(accessToken) {
|
|
1940
|
+
const payload = accessToken.includes(".") ? decodeBase64UrlJson(accessToken.split(".")[1]) : null;
|
|
1941
|
+
const subject = typeof payload?.sub === "string" ? payload.sub : null;
|
|
1942
|
+
if (!subject) return null;
|
|
1943
|
+
const parts = subject.split("|");
|
|
1944
|
+
const userId = (parts.length > 1 ? parts[1] : parts[0]).trim();
|
|
1945
|
+
return userId ? `${userId}%3A%3A${accessToken}` : null;
|
|
1946
|
+
}
|
|
1947
|
+
async function connectPost(url, token) {
|
|
1977
1948
|
try {
|
|
1978
|
-
const res = await fetch(
|
|
1949
|
+
const res = await fetch(url, {
|
|
1950
|
+
method: "POST",
|
|
1979
1951
|
headers: {
|
|
1980
|
-
|
|
1981
|
-
"
|
|
1982
|
-
"
|
|
1952
|
+
"Authorization": `Bearer ${token}`,
|
|
1953
|
+
"Content-Type": "application/json",
|
|
1954
|
+
"Connect-Protocol-Version": "1",
|
|
1955
|
+
"User-Agent": "tokmon"
|
|
1983
1956
|
},
|
|
1957
|
+
body: "{}",
|
|
1984
1958
|
signal: AbortSignal.timeout(1e4)
|
|
1985
1959
|
});
|
|
1986
|
-
if (!res.ok) return
|
|
1987
|
-
|
|
1988
|
-
return numberValue(data?.available_count ?? data?.available ?? data?.remaining);
|
|
1960
|
+
if (!res.ok) return { __status: res.status };
|
|
1961
|
+
return await readJson(res);
|
|
1989
1962
|
} catch {
|
|
1990
|
-
return
|
|
1963
|
+
return null;
|
|
1991
1964
|
}
|
|
1992
1965
|
}
|
|
1993
|
-
async function
|
|
1966
|
+
async function connectGetWithSession(url, token) {
|
|
1967
|
+
const session = cursorSessionToken(token);
|
|
1968
|
+
if (!session) return null;
|
|
1994
1969
|
try {
|
|
1995
|
-
const
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
if (!res.ok) return
|
|
2003
|
-
|
|
2004
|
-
if (!data) return null;
|
|
2005
|
-
const headerPct = (name) => {
|
|
2006
|
-
const h = res.headers.get(name);
|
|
2007
|
-
if (h === null || h.trim() === "") return void 0;
|
|
2008
|
-
const n = Number(h);
|
|
2009
|
-
return Number.isFinite(n) ? n : void 0;
|
|
2010
|
-
};
|
|
2011
|
-
const metrics = [];
|
|
2012
|
-
appendWindowMetrics(metrics, data.rate_limit ?? data, headerPct);
|
|
2013
|
-
appendCredits2(metrics, data);
|
|
2014
|
-
const resetCredits = numberValue(data?.rate_limit_reset_credits?.available_count ?? data?.rate_limit_reset_credits?.available) ?? await fetchResetCredits(headers);
|
|
2015
|
-
if (resetCredits !== void 0 && resetCredits >= 0) {
|
|
2016
|
-
metrics.push({ label: "Resets", used: resetCredits, limit: null, format: { kind: "count", suffix: "available" } });
|
|
2017
|
-
}
|
|
2018
|
-
if (metrics.length === 0) return null;
|
|
2019
|
-
return { plan: auth.plan ?? planLabel2(data.plan_type), metrics, error: null, ...identityFields(auth) };
|
|
1970
|
+
const res = await fetch(url, {
|
|
1971
|
+
headers: {
|
|
1972
|
+
"Cookie": `WorkosCursorSessionToken=${session}`,
|
|
1973
|
+
"User-Agent": "tokmon"
|
|
1974
|
+
},
|
|
1975
|
+
signal: AbortSignal.timeout(1e4)
|
|
1976
|
+
});
|
|
1977
|
+
if (!res.ok) return { __status: res.status };
|
|
1978
|
+
return await readJson(res);
|
|
2020
1979
|
} catch {
|
|
2021
1980
|
return null;
|
|
2022
1981
|
}
|
|
2023
1982
|
}
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
for (const home of codexHomes(homeDir)) {
|
|
2029
|
-
const dir = join9(home, "sessions");
|
|
2030
|
-
let listing;
|
|
2031
|
-
try {
|
|
2032
|
-
listing = await readdir2(dir, { recursive: true });
|
|
2033
|
-
} catch {
|
|
2034
|
-
continue;
|
|
2035
|
-
}
|
|
2036
|
-
for (const f of listing) {
|
|
2037
|
-
if (!f.endsWith(".jsonl") || !f.includes("rollout-")) continue;
|
|
2038
|
-
const path = join9(dir, f);
|
|
2039
|
-
try {
|
|
2040
|
-
const s = await fsStat3(path);
|
|
2041
|
-
all.push({ path, mtime: s.mtimeMs });
|
|
2042
|
-
} catch {
|
|
2043
|
-
}
|
|
2044
|
-
}
|
|
1983
|
+
function onDemandSpendCents(su, limit, remaining) {
|
|
1984
|
+
for (const raw of [su.individualUsed, su.pooledUsed, su.totalSpend]) {
|
|
1985
|
+
const n = numberValue(raw);
|
|
1986
|
+
if (n !== void 0 && n > 0) return n;
|
|
2045
1987
|
}
|
|
2046
|
-
|
|
1988
|
+
const inferred = Math.max(0, limit - remaining);
|
|
1989
|
+
if (inferred > 0) return inferred;
|
|
1990
|
+
return numberValue(su.individualUsed) ?? numberValue(su.pooledUsed) ?? numberValue(su.totalSpend) ?? 0;
|
|
2047
1991
|
}
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
1992
|
+
function appendCredits2(metrics, creditGrants, stripe) {
|
|
1993
|
+
const stripeBalance = numberValue(stripe?.customerBalance);
|
|
1994
|
+
const stripeBalanceCents = stripeBalance !== void 0 && stripeBalance < 0 ? Math.abs(stripeBalance) : 0;
|
|
1995
|
+
const hasGrants = creditGrants?.hasCreditGrants === true;
|
|
1996
|
+
const grantTotal = hasGrants ? numberValue(creditGrants?.totalCents) ?? 0 : 0;
|
|
1997
|
+
const grantUsed = hasGrants ? numberValue(creditGrants?.usedCents) ?? 0 : 0;
|
|
1998
|
+
const hasValidGrants = hasGrants && grantTotal > 0;
|
|
1999
|
+
const total = (hasValidGrants ? grantTotal : 0) + stripeBalanceCents;
|
|
2000
|
+
if (total <= 0) return;
|
|
2001
|
+
metrics.push({ label: "Credits", used: dollars(Math.max(0, total - (hasValidGrants ? grantUsed : 0))), limit: null, format: { kind: "dollars" } });
|
|
2002
|
+
}
|
|
2003
|
+
async function cursorBilling(account, tz) {
|
|
2004
|
+
const [core, activity, spend] = await Promise.all([
|
|
2005
|
+
cursorBillingCore(account),
|
|
2006
|
+
cursorActivity(tz, account.homeDir),
|
|
2007
|
+
cursorModelSpend(account.homeDir)
|
|
2008
|
+
]);
|
|
2009
|
+
let merged = activity;
|
|
2010
|
+
if (spend) {
|
|
2011
|
+
const lines = activity?.summary ?? "";
|
|
2012
|
+
const spendLabel = `$${Math.round(finite(spend.total))} all-time`;
|
|
2013
|
+
merged = {
|
|
2014
|
+
series: activity?.series ?? [],
|
|
2015
|
+
summary: lines ? `${lines} \xB7 ${spendLabel}` : spendLabel
|
|
2016
|
+
};
|
|
2065
2017
|
}
|
|
2066
|
-
|
|
2018
|
+
const modelSpend = spend?.models?.length ? spend.models.map((m) => ({ name: m.name, usd: finite(m.usd), requests: finite(m.requests) })) : null;
|
|
2019
|
+
return { ...core, activity: merged, modelSpend };
|
|
2067
2020
|
}
|
|
2068
|
-
async function
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2021
|
+
async function cursorBillingCore(account) {
|
|
2022
|
+
const db = cursorStateDb(account.homeDir);
|
|
2023
|
+
const [tokenRes, membershipRes, emailRes, nameRes] = await Promise.all([
|
|
2024
|
+
readState(db, "cursorAuth/accessToken"),
|
|
2025
|
+
readState(db, "cursorAuth/stripeMembershipType"),
|
|
2026
|
+
readState(db, "cursorAuth/cachedEmail"),
|
|
2027
|
+
readState(db, "cursorAuth/cachedName")
|
|
2028
|
+
]);
|
|
2029
|
+
const token = cleanStoredString(tokenRes.value);
|
|
2030
|
+
const membership = membershipRes.value;
|
|
2031
|
+
const email = cleanStoredString(emailRes.value);
|
|
2032
|
+
const displayName = cleanStoredString(nameRes.value);
|
|
2033
|
+
const planFallback = membership ? membership.charAt(0).toUpperCase() + membership.slice(1) : null;
|
|
2034
|
+
if (!token) {
|
|
2035
|
+
const error = tokenRes.status === "ok" ? "Not signed in \u2014 open Cursor" : sqliteStatusMessage(tokenRes.status);
|
|
2036
|
+
return { plan: planFallback, metrics: [], error, ...identityFields({ email, displayName }) };
|
|
2037
|
+
}
|
|
2038
|
+
const [usage, planInfo, creditGrants, stripe] = await Promise.all([
|
|
2039
|
+
connectPost(USAGE_URL2, token),
|
|
2040
|
+
connectPost(PLAN_URL, token),
|
|
2041
|
+
connectPost(CREDITS_URL, token),
|
|
2042
|
+
connectGetWithSession(STRIPE_URL, token)
|
|
2043
|
+
]);
|
|
2044
|
+
if (!usage || usage.__status) {
|
|
2045
|
+
const expired = usage?.__status === 401 || usage?.__status === 403;
|
|
2046
|
+
return { plan: planFallback, metrics: [], error: expired ? "Token expired \u2014 re-open Cursor" : "Cursor API error", ...identityFields({ email, displayName }) };
|
|
2047
|
+
}
|
|
2048
|
+
const planName = planInfo?.planInfo?.planName ?? planFallback;
|
|
2049
|
+
const price = planInfo?.planInfo?.price;
|
|
2050
|
+
const plan = planName ? price ? `${planName} \xB7 ${price}` : planName : null;
|
|
2051
|
+
const pu = usage.planUsage ?? {};
|
|
2052
|
+
const metrics = [];
|
|
2053
|
+
const rawEnd = usage.billingCycleEnd;
|
|
2054
|
+
const endMs = numberValue(rawEnd) ?? NaN;
|
|
2055
|
+
const iso = msToIso(endMs);
|
|
2056
|
+
const resets = iso && endMs > 0 ? resetIn(iso) : null;
|
|
2057
|
+
const limit = numberValue(pu.limit);
|
|
2058
|
+
const planUsedCents = numberValue(pu.totalSpend) ?? (limit !== void 0 && numberValue(pu.remaining) !== void 0 ? limit - numberValue(pu.remaining) : void 0);
|
|
2059
|
+
const computedPercent = limit !== void 0 && limit > 0 && planUsedCents !== void 0 ? planUsedCents / limit * 100 : void 0;
|
|
2060
|
+
const totalPercentUsed = numberValue(pu.totalPercentUsed) ?? computedPercent;
|
|
2061
|
+
const su = usage.spendLimitUsage;
|
|
2062
|
+
const planLower = typeof planName === "string" ? planName.trim().toLowerCase() : "";
|
|
2063
|
+
const pooledLimit = numberValue(su?.pooledLimit) ?? 0;
|
|
2064
|
+
const isTeamAccount = planLower === "team" || String(su?.limitType ?? "").toLowerCase() === "team" || pooledLimit > 0;
|
|
2065
|
+
if (isTeamAccount && limit !== void 0 && planUsedCents !== void 0) {
|
|
2066
|
+
metrics.push({
|
|
2067
|
+
label: "Usage",
|
|
2068
|
+
used: dollars(Math.max(0, planUsedCents)),
|
|
2069
|
+
limit: dollars(limit),
|
|
2070
|
+
format: { kind: "dollars" },
|
|
2071
|
+
resetsAt: resets,
|
|
2072
|
+
primary: true
|
|
2073
|
+
});
|
|
2074
|
+
} else if (totalPercentUsed !== void 0) {
|
|
2075
|
+
metrics.push(percentMetric("Usage", totalPercentUsed, resets, true));
|
|
2076
|
+
if (limit !== void 0 && planUsedCents !== void 0) {
|
|
2077
|
+
metrics.push({
|
|
2078
|
+
label: "Spend",
|
|
2079
|
+
used: dollars(Math.max(0, planUsedCents)),
|
|
2080
|
+
limit: dollars(limit),
|
|
2081
|
+
format: { kind: "dollars" }
|
|
2082
|
+
});
|
|
2078
2083
|
}
|
|
2079
|
-
if (metrics.length === 0) continue;
|
|
2080
|
-
return { plan: auth?.plan ?? planLabel2(last.plan_type), metrics, error: null, asOfMs: file.mtime, ...identityFields(auth) };
|
|
2081
2084
|
}
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2085
|
+
const autoPercentUsed = numberValue(pu.autoPercentUsed);
|
|
2086
|
+
if (autoPercentUsed !== void 0) {
|
|
2087
|
+
metrics.push({ label: "Auto", used: autoPercentUsed, limit: 100, format: { kind: "percent" } });
|
|
2088
|
+
}
|
|
2089
|
+
const apiPercentUsed = numberValue(pu.apiPercentUsed);
|
|
2090
|
+
if (apiPercentUsed !== void 0) {
|
|
2091
|
+
metrics.push({ label: "API", used: apiPercentUsed, limit: 100, format: { kind: "percent" } });
|
|
2092
|
+
}
|
|
2093
|
+
if (su) {
|
|
2094
|
+
const pair = numberValue(su.individualLimit) !== void 0 && numberValue(su.individualRemaining) !== void 0 ? { limit: numberValue(su.individualLimit), remaining: numberValue(su.individualRemaining) } : numberValue(su.pooledLimit) !== void 0 && numberValue(su.pooledRemaining) !== void 0 ? { limit: numberValue(su.pooledLimit), remaining: numberValue(su.pooledRemaining) } : null;
|
|
2095
|
+
const spent = onDemandSpendCents(su, pair?.limit ?? 0, pair?.remaining ?? 0);
|
|
2096
|
+
if (pair && pair.limit > 0) {
|
|
2097
|
+
metrics.push({
|
|
2098
|
+
label: "On-demand",
|
|
2099
|
+
used: dollars(spent),
|
|
2100
|
+
limit: dollars(pair.limit),
|
|
2101
|
+
format: { kind: "dollars" }
|
|
2102
|
+
});
|
|
2103
|
+
} else if (spent > 0) {
|
|
2104
|
+
metrics.push({
|
|
2105
|
+
label: "On-demand",
|
|
2106
|
+
used: dollars(spent),
|
|
2107
|
+
limit: null,
|
|
2108
|
+
format: { kind: "dollars" }
|
|
2109
|
+
});
|
|
2110
|
+
}
|
|
2089
2111
|
}
|
|
2090
|
-
|
|
2091
|
-
if (
|
|
2092
|
-
|
|
2093
|
-
return result;
|
|
2112
|
+
appendCredits2(metrics, creditGrants, stripe);
|
|
2113
|
+
if (metrics.length === 0) {
|
|
2114
|
+
return { plan, metrics: [], error: usage.enabled === false ? "No active subscription" : "No usage data", ...identityFields({ email, displayName }) };
|
|
2094
2115
|
}
|
|
2095
|
-
return {
|
|
2096
|
-
plan: auth?.plan ?? snap?.plan ?? null,
|
|
2097
|
-
metrics: [],
|
|
2098
|
-
error: auth ? "Usage API failed \u2014 run codex to refresh" : "Not logged in \u2014 run codex",
|
|
2099
|
-
...identityFields(auth)
|
|
2100
|
-
};
|
|
2116
|
+
return { plan, metrics, error: null, ...identityFields({ email, displayName }) };
|
|
2101
2117
|
}
|
|
2102
2118
|
|
|
2103
|
-
// src/providers/codex/index.ts
|
|
2104
|
-
var codexProvider = {
|
|
2105
|
-
id: "codex",
|
|
2106
|
-
name: "Codex",
|
|
2107
|
-
color: "cyan",
|
|
2108
|
-
hasUsage: true,
|
|
2109
|
-
hasBilling: true,
|
|
2110
|
-
detect: (homeDir) => detectCodex(homeDir),
|
|
2111
|
-
fetchSummary: (account, tz) => codexDashboard(tz, account.homeDir),
|
|
2112
|
-
fetchTable: (account, tz) => codexTable(tz, account.homeDir),
|
|
2113
|
-
fetchBilling: (account) => codexBilling(account)
|
|
2114
|
-
};
|
|
2115
|
-
|
|
2116
2119
|
// src/providers/cursor/usage.ts
|
|
2117
2120
|
var EVENTS_URL = "https://api2.cursor.sh/aiserver.v1.DashboardService/GetFilteredUsageEvents";
|
|
2118
2121
|
var WINDOW_DAYS = 90;
|
|
2119
2122
|
var PAGE_SIZE = 1e3;
|
|
2120
2123
|
var MAX_PAGES = 12;
|
|
2124
|
+
var CACHE_TTL_MS = 6e4;
|
|
2121
2125
|
var SKIP_KINDS = /* @__PURE__ */ new Set(["USAGE_EVENT_KIND_ABORTED_NOT_CHARGED", "USAGE_EVENT_KIND_ERRORED_NOT_CHARGED"]);
|
|
2126
|
+
var apiCache = /* @__PURE__ */ new Map();
|
|
2127
|
+
var apiInflight = /* @__PURE__ */ new Map();
|
|
2122
2128
|
async function readToken(homeDir) {
|
|
2123
2129
|
const r = await runSqlite(cursorStateDb(homeDir), "SELECT value FROM ItemTable WHERE key='cursorAuth/accessToken' LIMIT 1;");
|
|
2124
2130
|
const raw = r.status === "ok" ? r.rows[0]?.value : void 0;
|
|
@@ -2144,135 +2150,126 @@ async function fetchPage(token, startMs, endMs, page) {
|
|
|
2144
2150
|
return null;
|
|
2145
2151
|
}
|
|
2146
2152
|
}
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
const
|
|
2151
|
-
|
|
2152
|
-
const
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2153
|
+
function eventToEntry(e) {
|
|
2154
|
+
if (e.kind && SKIP_KINDS.has(e.kind)) return null;
|
|
2155
|
+
const rawTs = e.timestamp;
|
|
2156
|
+
const ts = typeof rawTs === "number" ? rawTs : typeof rawTs === "string" && rawTs.trim() ? /^\d+$/.test(rawTs.trim()) ? Number(rawTs) : Date.parse(rawTs) : NaN;
|
|
2157
|
+
if (!Number.isFinite(ts) || ts <= 0) return null;
|
|
2158
|
+
const tu = e.tokenUsage ?? {};
|
|
2159
|
+
const input = safeNum(tu.inputTokens);
|
|
2160
|
+
const output = safeNum(tu.outputTokens);
|
|
2161
|
+
const cacheRead = safeNum(tu.cacheReadTokens);
|
|
2162
|
+
const charged = Number(e.chargedCents);
|
|
2163
|
+
const totalCents = Number(tu.totalCents);
|
|
2164
|
+
const cents = Number.isFinite(charged) && charged > 0 ? charged : Number.isFinite(totalCents) && totalCents > 0 ? totalCents : 0;
|
|
2165
|
+
const cost = cents > 0 ? cents / 100 : 0;
|
|
2166
|
+
if (cost <= 0 && input + output + cacheRead === 0) return null;
|
|
2167
|
+
return {
|
|
2168
|
+
ts,
|
|
2169
|
+
id: `${ts}|${e.model ?? ""}|${input}|${output}|${cacheRead}|${cents}`,
|
|
2170
|
+
model: String(e.model ?? "unknown"),
|
|
2171
|
+
cost,
|
|
2172
|
+
input,
|
|
2173
|
+
output,
|
|
2174
|
+
cacheCreate: 0,
|
|
2175
|
+
cacheRead,
|
|
2176
|
+
cacheSavings: 0,
|
|
2177
|
+
count: 1
|
|
2178
|
+
};
|
|
2161
2179
|
}
|
|
2162
|
-
function
|
|
2163
|
-
|
|
2164
|
-
const
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2180
|
+
async function fetchApiEntries(homeDir) {
|
|
2181
|
+
const cacheKey = homeDir ?? "";
|
|
2182
|
+
const hit = apiCache.get(cacheKey);
|
|
2183
|
+
if (hit && Date.now() - hit.at < CACHE_TTL_MS) return { entries: hit.entries, complete: hit.complete };
|
|
2184
|
+
const existing = apiInflight.get(cacheKey);
|
|
2185
|
+
if (existing) return existing;
|
|
2186
|
+
const promise = (async () => {
|
|
2187
|
+
const token = await readToken(homeDir);
|
|
2188
|
+
if (!token) return { entries: [], complete: true };
|
|
2189
|
+
const endMs = Date.now();
|
|
2190
|
+
const startMs = endMs - WINDOW_DAYS * 864e5;
|
|
2191
|
+
const events = [];
|
|
2192
|
+
let complete = true;
|
|
2193
|
+
for (let page = 1; page <= MAX_PAGES; page++) {
|
|
2194
|
+
const resp = await fetchPage(token, startMs, endMs, page);
|
|
2195
|
+
if (!resp) {
|
|
2196
|
+
complete = false;
|
|
2197
|
+
break;
|
|
2198
|
+
}
|
|
2199
|
+
const batch = resp.usageEventsDisplay ?? [];
|
|
2200
|
+
events.push(...batch);
|
|
2201
|
+
if (batch.length < PAGE_SIZE) break;
|
|
2202
|
+
if (page === MAX_PAGES) complete = false;
|
|
2170
2203
|
}
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
row.cost += usd;
|
|
2176
|
-
row.count += 1;
|
|
2177
|
-
let md = row.breakdown.find((b) => b.name === model);
|
|
2178
|
-
if (!md) {
|
|
2179
|
-
md = { name: model, input: 0, output: 0, cacheCreate: 0, cacheRead: 0, cacheSavings: 0, cost: 0, count: 0 };
|
|
2180
|
-
row.breakdown.push(md);
|
|
2204
|
+
const entries = [];
|
|
2205
|
+
for (const e of events) {
|
|
2206
|
+
const entry = eventToEntry(e);
|
|
2207
|
+
if (entry) entries.push(entry);
|
|
2181
2208
|
}
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
if (!Number.isFinite(ts) || ts <= 0) continue;
|
|
2192
|
-
const tu = e.tokenUsage ?? {};
|
|
2193
|
-
const input = safeNum(tu.inputTokens);
|
|
2194
|
-
const output = safeNum(tu.outputTokens);
|
|
2195
|
-
const cacheRead = safeNum(tu.cacheReadTokens);
|
|
2196
|
-
const cents = Number(e.chargedCents);
|
|
2197
|
-
const usd = Number.isFinite(cents) && cents > 0 ? cents / 100 : 0;
|
|
2198
|
-
if (usd <= 0 && input + output + cacheRead === 0) continue;
|
|
2199
|
-
const model = String(e.model ?? "unknown");
|
|
2200
|
-
put(buckets.daily, dayKey(ts, tz), model, usd, input, output, cacheRead);
|
|
2201
|
-
put(buckets.weekly, weekKey(ts, tz), model, usd, input, output, cacheRead);
|
|
2202
|
-
put(buckets.monthly, monthKey(ts, tz), model, usd, input, output, cacheRead);
|
|
2203
|
-
}
|
|
2204
|
-
const sortRows = (map) => [...map.values()].map((row) => {
|
|
2205
|
-
row.breakdown.sort((a, b) => b.cost - a.cost);
|
|
2206
|
-
row.models = row.breakdown.map((b) => b.name);
|
|
2207
|
-
return row;
|
|
2208
|
-
}).sort((a, b) => a.label.localeCompare(b.label));
|
|
2209
|
-
const table = { daily: sortRows(buckets.daily), weekly: sortRows(buckets.weekly), monthly: sortRows(buckets.monthly) };
|
|
2210
|
-
return table.daily.length ? table : null;
|
|
2209
|
+
if (complete) apiCache.set(cacheKey, { at: Date.now(), entries, complete: true });
|
|
2210
|
+
return { entries, complete };
|
|
2211
|
+
})();
|
|
2212
|
+
apiInflight.set(cacheKey, promise);
|
|
2213
|
+
try {
|
|
2214
|
+
return await promise;
|
|
2215
|
+
} finally {
|
|
2216
|
+
apiInflight.delete(cacheKey);
|
|
2217
|
+
}
|
|
2211
2218
|
}
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
const
|
|
2217
|
-
for (const r of hi) m.set(r.label, r);
|
|
2218
|
-
return [...m.values()].sort((a, b) => a.label.localeCompare(b.label));
|
|
2219
|
-
};
|
|
2220
|
-
function reBucket(daily, tz, keyOf) {
|
|
2221
|
-
const out = /* @__PURE__ */ new Map();
|
|
2222
|
-
for (const day of daily) {
|
|
2219
|
+
async function fetchLocalEntries(tz, homeDir) {
|
|
2220
|
+
const table = await cursorUsageTable(tz, homeDir);
|
|
2221
|
+
if (!table) return [];
|
|
2222
|
+
const out = [];
|
|
2223
|
+
for (const day of table.daily) {
|
|
2223
2224
|
const [y, mo, d] = day.label.split("-").map(Number);
|
|
2224
2225
|
const ts = Date.UTC(y, mo - 1, d, 12);
|
|
2225
2226
|
if (!Number.isFinite(ts)) continue;
|
|
2226
|
-
const label = keyOf(ts, tz);
|
|
2227
|
-
let row = out.get(label);
|
|
2228
|
-
if (!row) {
|
|
2229
|
-
row = { label, models: [], input: 0, output: 0, cacheCreate: 0, cacheRead: 0, cacheSavings: 0, total: 0, cost: 0, count: 0, breakdown: [] };
|
|
2230
|
-
out.set(label, row);
|
|
2231
|
-
}
|
|
2232
|
-
row.input += day.input;
|
|
2233
|
-
row.output += day.output;
|
|
2234
|
-
row.cacheCreate += day.cacheCreate;
|
|
2235
|
-
row.cacheRead += day.cacheRead;
|
|
2236
|
-
row.cacheSavings += day.cacheSavings;
|
|
2237
|
-
row.total += day.total;
|
|
2238
|
-
row.cost += day.cost;
|
|
2239
|
-
row.count += day.count;
|
|
2240
2227
|
for (const b of day.breakdown) {
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2228
|
+
out.push({
|
|
2229
|
+
day: day.label,
|
|
2230
|
+
entry: {
|
|
2231
|
+
ts,
|
|
2232
|
+
model: b.name,
|
|
2233
|
+
cost: b.cost,
|
|
2234
|
+
count: Math.max(1, b.count),
|
|
2235
|
+
input: 0,
|
|
2236
|
+
output: 0,
|
|
2237
|
+
cacheCreate: 0,
|
|
2238
|
+
cacheRead: 0,
|
|
2239
|
+
cacheSavings: 0
|
|
2240
|
+
}
|
|
2241
|
+
});
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
return out;
|
|
2245
|
+
}
|
|
2246
|
+
function overlayEntries(api, local, tz) {
|
|
2247
|
+
if (api.length === 0) return local.map((l) => l.entry);
|
|
2248
|
+
if (local.length === 0) return api;
|
|
2249
|
+
const apiDays = new Set(api.map((e) => dayKey(e.ts, tz)));
|
|
2250
|
+
return [...api, ...local.filter((l) => !apiDays.has(l.day)).map((l) => l.entry)];
|
|
2251
|
+
}
|
|
2252
|
+
async function cursorEntries(since, tz, homeDir) {
|
|
2253
|
+
const [apiResult, local] = await Promise.all([fetchApiEntries(homeDir), fetchLocalEntries(tz, homeDir)]);
|
|
2254
|
+
return overlayEntries(apiResult.entries, local, tz).filter((e) => e.ts >= since);
|
|
2260
2255
|
}
|
|
2261
|
-
async function
|
|
2262
|
-
|
|
2263
|
-
if (!api && !local) return EMPTY;
|
|
2264
|
-
const daily = overlayDaily(local?.daily ?? [], api?.daily ?? []);
|
|
2265
|
-
if (daily.length === 0) return EMPTY;
|
|
2266
|
-
return { daily, weekly: reBucket(daily, tz, weekKey), monthly: reBucket(daily, tz, monthKey) };
|
|
2256
|
+
async function cursorDashboard(tz, homeDir) {
|
|
2257
|
+
return summarize(await cursorEntries(dashboardSince(tz), tz, homeDir), tz);
|
|
2267
2258
|
}
|
|
2259
|
+
async function cursorTableFull(tz, homeDir) {
|
|
2260
|
+
return tabulate(await cursorEntries(tableSince(tz), tz, homeDir), tz);
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
// src/providers/cursor/index.ts
|
|
2268
2264
|
var cursorProvider = {
|
|
2269
2265
|
id: "cursor",
|
|
2270
2266
|
name: "Cursor",
|
|
2271
2267
|
color: "magenta",
|
|
2272
|
-
hasUsage:
|
|
2268
|
+
hasUsage: true,
|
|
2273
2269
|
hasBilling: true,
|
|
2274
2270
|
detect: (homeDir) => detectCursor(homeDir),
|
|
2275
|
-
|
|
2271
|
+
fetchSummary: (account, tz) => cursorDashboard(tz, account.homeDir),
|
|
2272
|
+
fetchTable: (account, tz) => cursorTableFull(tz, account.homeDir),
|
|
2276
2273
|
fetchBilling: (account, tz) => cursorBilling(account, tz)
|
|
2277
2274
|
};
|
|
2278
2275
|
|
|
@@ -3448,28 +3445,409 @@ var geminiProvider = {
|
|
|
3448
3445
|
fetchBilling: (account) => geminiBilling(account)
|
|
3449
3446
|
};
|
|
3450
3447
|
|
|
3448
|
+
// src/providers/grok/identity.ts
|
|
3449
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
3450
|
+
import { join as join17 } from "path";
|
|
3451
|
+
import { homedir as homedir14 } from "os";
|
|
3452
|
+
function grokHomes(homeDir) {
|
|
3453
|
+
if (homeDir) {
|
|
3454
|
+
const base = expandHome(homeDir);
|
|
3455
|
+
return [.../* @__PURE__ */ new Set([join17(base, ".grok"), base])];
|
|
3456
|
+
}
|
|
3457
|
+
const homes = [];
|
|
3458
|
+
const env = envDir("GROK_HOME");
|
|
3459
|
+
if (env) homes.push(env);
|
|
3460
|
+
homes.push(join17(homedir14(), ".grok"));
|
|
3461
|
+
return [...new Set(homes)];
|
|
3462
|
+
}
|
|
3463
|
+
function grokAuthPaths(homeDir) {
|
|
3464
|
+
const explicit = process.env.GROK_AUTH_PATH;
|
|
3465
|
+
const paths = grokHomes(homeDir).map((h) => join17(h, "auth.json"));
|
|
3466
|
+
return explicit ? [explicit, ...paths] : paths;
|
|
3467
|
+
}
|
|
3468
|
+
function isAuthEntry(v) {
|
|
3469
|
+
return !!v && typeof v === "object" && typeof v.key === "string" && !!v.key;
|
|
3470
|
+
}
|
|
3471
|
+
function readGrokAuth(homeDir) {
|
|
3472
|
+
const inline = process.env.GROK_AUTH?.trim();
|
|
3473
|
+
if (inline) {
|
|
3474
|
+
try {
|
|
3475
|
+
const parsed = JSON.parse(inline);
|
|
3476
|
+
const fromInline = pickAuthEntry(parsed);
|
|
3477
|
+
if (fromInline) return fromInline;
|
|
3478
|
+
} catch {
|
|
3479
|
+
}
|
|
3480
|
+
}
|
|
3481
|
+
for (const path of grokAuthPaths(homeDir)) {
|
|
3482
|
+
try {
|
|
3483
|
+
const parsed = JSON.parse(readFileSync2(path, "utf-8"));
|
|
3484
|
+
const entry = pickAuthEntry(parsed);
|
|
3485
|
+
if (entry) return entry;
|
|
3486
|
+
} catch {
|
|
3487
|
+
}
|
|
3488
|
+
}
|
|
3489
|
+
return null;
|
|
3490
|
+
}
|
|
3491
|
+
function pickAuthEntry(parsed) {
|
|
3492
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
3493
|
+
const map = parsed;
|
|
3494
|
+
if (isAuthEntry(map)) return map;
|
|
3495
|
+
const preferred = [
|
|
3496
|
+
"https://accounts.x.ai/sign-in",
|
|
3497
|
+
"https://auth.x.ai"
|
|
3498
|
+
];
|
|
3499
|
+
for (const key of preferred) {
|
|
3500
|
+
if (isAuthEntry(map[key])) return map[key];
|
|
3501
|
+
}
|
|
3502
|
+
const entries = Object.entries(map).filter(([, v]) => isAuthEntry(v)).map(([k, v]) => ({ key: k, entry: v }));
|
|
3503
|
+
if (entries.length === 0) return null;
|
|
3504
|
+
const session = entries.filter((e) => e.key !== "xai::api_key" && !e.key.startsWith("xai::"));
|
|
3505
|
+
const pool = session.length ? session : entries;
|
|
3506
|
+
return pool.sort(
|
|
3507
|
+
(a, b) => Date.parse(b.entry.create_time ?? b.entry.expires_at ?? "") - Date.parse(a.entry.create_time ?? a.entry.expires_at ?? "")
|
|
3508
|
+
)[0]?.entry ?? null;
|
|
3509
|
+
}
|
|
3510
|
+
function readGrokIdentity(homeDir) {
|
|
3511
|
+
const entry = readGrokAuth(homeDir);
|
|
3512
|
+
if (!entry) return {};
|
|
3513
|
+
const payload = entry.key.includes(".") ? decodeBase64UrlJson(entry.key.split(".")[1]) : null;
|
|
3514
|
+
const tier = typeof payload?.tier === "number" ? payload.tier : void 0;
|
|
3515
|
+
const expiresAt = entry.expires_at ? Date.parse(entry.expires_at) : void 0;
|
|
3516
|
+
return {
|
|
3517
|
+
email: typeof entry.email === "string" ? entry.email : void 0,
|
|
3518
|
+
displayName: typeof entry.first_name === "string" ? entry.first_name : void 0,
|
|
3519
|
+
userId: typeof entry.user_id === "string" ? entry.user_id : void 0,
|
|
3520
|
+
teamId: typeof entry.team_id === "string" ? entry.team_id : void 0,
|
|
3521
|
+
tier,
|
|
3522
|
+
expiresAt: Number.isFinite(expiresAt) ? expiresAt : void 0
|
|
3523
|
+
};
|
|
3524
|
+
}
|
|
3525
|
+
function grokClientVersion(homeDir) {
|
|
3526
|
+
for (const home of grokHomes(homeDir)) {
|
|
3527
|
+
try {
|
|
3528
|
+
const v = readFileSync2(join17(home, ".metadata_version"), "utf-8").trim();
|
|
3529
|
+
if (v) return v;
|
|
3530
|
+
} catch {
|
|
3531
|
+
}
|
|
3532
|
+
}
|
|
3533
|
+
return "0.2.93";
|
|
3534
|
+
}
|
|
3535
|
+
|
|
3536
|
+
// src/providers/grok/billing.ts
|
|
3537
|
+
var DEFAULT_BILLING_BASE = "https://grok.com";
|
|
3538
|
+
var BILLING_PATH = "/billing?format=credits";
|
|
3539
|
+
function billingBase() {
|
|
3540
|
+
return DEFAULT_BILLING_BASE;
|
|
3541
|
+
}
|
|
3542
|
+
function num(v) {
|
|
3543
|
+
return numberValue(v) ?? (typeof v === "number" && Number.isFinite(v) ? v : void 0);
|
|
3544
|
+
}
|
|
3545
|
+
function periodMs(v) {
|
|
3546
|
+
if (typeof v === "number" && Number.isFinite(v)) return v > 1e10 ? v : v * 1e3;
|
|
3547
|
+
if (typeof v === "string" && v.trim()) {
|
|
3548
|
+
const t = Date.parse(v);
|
|
3549
|
+
return Number.isFinite(t) ? t : null;
|
|
3550
|
+
}
|
|
3551
|
+
return null;
|
|
3552
|
+
}
|
|
3553
|
+
async function grokBilling(account) {
|
|
3554
|
+
const identity = readGrokIdentity(account.homeDir);
|
|
3555
|
+
const auth = readGrokAuth(account.homeDir);
|
|
3556
|
+
if (!auth?.key) {
|
|
3557
|
+
return {
|
|
3558
|
+
plan: null,
|
|
3559
|
+
metrics: [],
|
|
3560
|
+
error: "Not signed in \u2014 run `grok login`",
|
|
3561
|
+
...identityFields(identity)
|
|
3562
|
+
};
|
|
3563
|
+
}
|
|
3564
|
+
const version = grokClientVersion(account.homeDir);
|
|
3565
|
+
try {
|
|
3566
|
+
const res = await fetch(`${billingBase()}${BILLING_PATH}`, {
|
|
3567
|
+
headers: {
|
|
3568
|
+
Authorization: `Bearer ${auth.key}`,
|
|
3569
|
+
"x-grok-client-version": version,
|
|
3570
|
+
Accept: "application/json",
|
|
3571
|
+
"User-Agent": "tokmon"
|
|
3572
|
+
},
|
|
3573
|
+
signal: AbortSignal.timeout(12e3)
|
|
3574
|
+
});
|
|
3575
|
+
if (res.status === 401 || res.status === 403) {
|
|
3576
|
+
return {
|
|
3577
|
+
plan: null,
|
|
3578
|
+
metrics: [],
|
|
3579
|
+
error: "Billing requires grok.com auth \u2014 run `grok login`",
|
|
3580
|
+
...identityFields(identity)
|
|
3581
|
+
};
|
|
3582
|
+
}
|
|
3583
|
+
if (!res.ok) {
|
|
3584
|
+
return {
|
|
3585
|
+
plan: null,
|
|
3586
|
+
metrics: [],
|
|
3587
|
+
error: `Billing HTTP ${res.status}`,
|
|
3588
|
+
...identityFields(identity)
|
|
3589
|
+
};
|
|
3590
|
+
}
|
|
3591
|
+
const data = await readJson(res);
|
|
3592
|
+
if (!data || typeof data !== "object") {
|
|
3593
|
+
return { plan: null, metrics: [], error: "Empty billing response", ...identityFields(identity) };
|
|
3594
|
+
}
|
|
3595
|
+
const cfg = data.currentPeriod !== void 0 || data.creditUsagePercent !== void 0 ? data : data.config ?? data.billing ?? data.data ?? data;
|
|
3596
|
+
const metrics = [];
|
|
3597
|
+
const usagePct = num(cfg.creditUsagePercent);
|
|
3598
|
+
if (usagePct !== void 0) {
|
|
3599
|
+
const end = periodMs(cfg.billingPeriodEnd ?? cfg.currentPeriod?.end ?? cfg.currentPeriod?.billingPeriodEnd);
|
|
3600
|
+
metrics.push(percentMetric("Credits", finite(usagePct), end ? msToIso(end) : null, true));
|
|
3601
|
+
}
|
|
3602
|
+
const monthlyLimit = num(cfg.monthlyLimit);
|
|
3603
|
+
const includedUsed = num(cfg.includedUsed ?? cfg.currentPeriod?.includedUsed ?? cfg.totalUsed);
|
|
3604
|
+
if (monthlyLimit !== void 0 && monthlyLimit > 0 && includedUsed !== void 0) {
|
|
3605
|
+
metrics.push({
|
|
3606
|
+
label: "Included",
|
|
3607
|
+
used: includedUsed,
|
|
3608
|
+
limit: monthlyLimit,
|
|
3609
|
+
format: { kind: "count" },
|
|
3610
|
+
primary: metrics.length === 0
|
|
3611
|
+
});
|
|
3612
|
+
}
|
|
3613
|
+
const onDemandCap = num(cfg.onDemandCap);
|
|
3614
|
+
const onDemandUsed = num(cfg.onDemandUsed);
|
|
3615
|
+
if (onDemandCap !== void 0 && onDemandCap > 0) {
|
|
3616
|
+
metrics.push({
|
|
3617
|
+
label: "On-demand",
|
|
3618
|
+
used: onDemandUsed ?? 0,
|
|
3619
|
+
limit: onDemandCap,
|
|
3620
|
+
format: { kind: "dollars" }
|
|
3621
|
+
});
|
|
3622
|
+
} else if (onDemandUsed !== void 0 && onDemandUsed > 0) {
|
|
3623
|
+
metrics.push({
|
|
3624
|
+
label: "On-demand",
|
|
3625
|
+
used: onDemandUsed,
|
|
3626
|
+
limit: null,
|
|
3627
|
+
format: { kind: "dollars" }
|
|
3628
|
+
});
|
|
3629
|
+
}
|
|
3630
|
+
const prepaid = num(cfg.prepaidBalance);
|
|
3631
|
+
if (prepaid !== void 0) {
|
|
3632
|
+
metrics.push({
|
|
3633
|
+
label: "Credits left",
|
|
3634
|
+
used: prepaid,
|
|
3635
|
+
limit: null,
|
|
3636
|
+
format: { kind: "dollars" }
|
|
3637
|
+
});
|
|
3638
|
+
}
|
|
3639
|
+
const plan = typeof cfg.plan === "string" && cfg.plan.trim() ? cfg.plan.trim() : cfg.isUnifiedBillingUser === true ? "SuperGrok" : identity.tier !== void 0 ? `Tier ${identity.tier}` : "Grok";
|
|
3640
|
+
return {
|
|
3641
|
+
plan,
|
|
3642
|
+
metrics,
|
|
3643
|
+
error: metrics.length ? null : "No billing metrics",
|
|
3644
|
+
...identityFields(identity)
|
|
3645
|
+
};
|
|
3646
|
+
} catch (err) {
|
|
3647
|
+
return {
|
|
3648
|
+
plan: null,
|
|
3649
|
+
metrics: [],
|
|
3650
|
+
error: err instanceof Error ? err.message : "Billing request failed",
|
|
3651
|
+
...identityFields(identity)
|
|
3652
|
+
};
|
|
3653
|
+
}
|
|
3654
|
+
}
|
|
3655
|
+
|
|
3656
|
+
// src/providers/grok/usage.ts
|
|
3657
|
+
import { access as access10, readdir as readdir7, readFile as readFile8, stat as fsStat6 } from "fs/promises";
|
|
3658
|
+
import { createReadStream as createReadStream6 } from "fs";
|
|
3659
|
+
import { createInterface as createInterface6 } from "readline";
|
|
3660
|
+
import { join as join18 } from "path";
|
|
3661
|
+
var PRICING4 = {
|
|
3662
|
+
"grok-build-latest": { in: 2e-6, cr: 5e-7, out: 6e-6 },
|
|
3663
|
+
"grok-4.5": { in: 2e-6, cr: 5e-7, out: 6e-6 },
|
|
3664
|
+
"grok-composer-2.5-fast": { in: 2e-6, cr: 5e-7, out: 6e-6 },
|
|
3665
|
+
"grok-4.3": { in: 125e-8, cr: 2e-7, out: 25e-7 },
|
|
3666
|
+
"grok-4.20": { in: 125e-8, cr: 2e-7, out: 25e-7 },
|
|
3667
|
+
"grok-build-0.1": { in: 1e-6, cr: 2e-7, out: 2e-6 },
|
|
3668
|
+
"grok-code-fast-1": { in: 1e-6, cr: 2e-7, out: 2e-6 },
|
|
3669
|
+
"grok-code-fast": { in: 1e-6, cr: 2e-7, out: 2e-6 },
|
|
3670
|
+
"grok-build": { in: 1e-6, cr: 2e-7, out: 2e-6 }
|
|
3671
|
+
};
|
|
3672
|
+
var FALLBACK_PRICE2 = PRICING4["grok-4.5"];
|
|
3673
|
+
var PRICE_KEYS4 = Object.keys(PRICING4).sort((a, b) => b.length - a.length);
|
|
3674
|
+
function modelKeyMatches2(model, key) {
|
|
3675
|
+
let idx = model.indexOf(key);
|
|
3676
|
+
while (idx >= 0) {
|
|
3677
|
+
const before = idx === 0 ? "" : model[idx - 1];
|
|
3678
|
+
const rest = model.slice(idx + key.length);
|
|
3679
|
+
const versionContinues = rest[0] === "." && /\d/.test(rest[1] ?? "");
|
|
3680
|
+
if ((!before || !/[a-z0-9-]/.test(before)) && !versionContinues && (rest === "" || rest[0] === "-" || !/[a-z0-9]/.test(rest[0]))) {
|
|
3681
|
+
return true;
|
|
3682
|
+
}
|
|
3683
|
+
idx = model.indexOf(key, idx + key.length);
|
|
3684
|
+
}
|
|
3685
|
+
return false;
|
|
3686
|
+
}
|
|
3687
|
+
function priceFor4(model) {
|
|
3688
|
+
const m = model.toLowerCase().trim();
|
|
3689
|
+
for (const key of PRICE_KEYS4) {
|
|
3690
|
+
if (modelKeyMatches2(m, key)) return PRICING4[key];
|
|
3691
|
+
}
|
|
3692
|
+
return FALLBACK_PRICE2;
|
|
3693
|
+
}
|
|
3694
|
+
async function detectGrok(homeDir) {
|
|
3695
|
+
for (const home of grokHomes(homeDir)) {
|
|
3696
|
+
for (const p of [join18(home, "logs", "unified.jsonl"), join18(home, "sessions"), join18(home, "auth.json"), join18(home, "bin", "grok")]) {
|
|
3697
|
+
try {
|
|
3698
|
+
await access10(p);
|
|
3699
|
+
return true;
|
|
3700
|
+
} catch {
|
|
3701
|
+
}
|
|
3702
|
+
}
|
|
3703
|
+
}
|
|
3704
|
+
return false;
|
|
3705
|
+
}
|
|
3706
|
+
async function loadSessionModels(home) {
|
|
3707
|
+
const out = /* @__PURE__ */ new Map();
|
|
3708
|
+
const sessionsRoot = join18(home, "sessions");
|
|
3709
|
+
let groups;
|
|
3710
|
+
try {
|
|
3711
|
+
groups = await readdir7(sessionsRoot);
|
|
3712
|
+
} catch {
|
|
3713
|
+
return out;
|
|
3714
|
+
}
|
|
3715
|
+
for (const group of groups) {
|
|
3716
|
+
if (group === "session_search.sqlite" || group.startsWith(".")) continue;
|
|
3717
|
+
const groupDir = join18(sessionsRoot, group);
|
|
3718
|
+
let sessions;
|
|
3719
|
+
try {
|
|
3720
|
+
sessions = await readdir7(groupDir);
|
|
3721
|
+
} catch {
|
|
3722
|
+
continue;
|
|
3723
|
+
}
|
|
3724
|
+
for (const sid of sessions) {
|
|
3725
|
+
try {
|
|
3726
|
+
const raw = JSON.parse(await readFile8(join18(groupDir, sid, "summary.json"), "utf-8"));
|
|
3727
|
+
const model = typeof raw?.current_model_id === "string" && raw.current_model_id ? raw.current_model_id : null;
|
|
3728
|
+
if (model) out.set(sid, model);
|
|
3729
|
+
} catch {
|
|
3730
|
+
}
|
|
3731
|
+
}
|
|
3732
|
+
}
|
|
3733
|
+
return out;
|
|
3734
|
+
}
|
|
3735
|
+
function costOf2(model, input, cacheRead, output) {
|
|
3736
|
+
const p = priceFor4(model);
|
|
3737
|
+
const cost = input * p.in + cacheRead * p.cr + output * p.out;
|
|
3738
|
+
const cacheSavings = cacheRead * Math.max(0, p.in - p.cr);
|
|
3739
|
+
return { cost, cacheSavings };
|
|
3740
|
+
}
|
|
3741
|
+
async function parseUnifiedLog(path, models) {
|
|
3742
|
+
const entries = [];
|
|
3743
|
+
const input = createReadStream6(path);
|
|
3744
|
+
input.on("error", () => {
|
|
3745
|
+
});
|
|
3746
|
+
const rl = createInterface6({ input, crlfDelay: Infinity });
|
|
3747
|
+
try {
|
|
3748
|
+
for await (const line of rl) {
|
|
3749
|
+
if (!line.includes("shell.turn.inference_done")) continue;
|
|
3750
|
+
try {
|
|
3751
|
+
const obj = JSON.parse(line.charCodeAt(0) === 65279 ? line.slice(1) : line);
|
|
3752
|
+
if (obj?.msg !== "shell.turn.inference_done" || !obj.ctx) continue;
|
|
3753
|
+
const ts = Date.parse(String(obj.ts ?? ""));
|
|
3754
|
+
if (!Number.isFinite(ts)) continue;
|
|
3755
|
+
const sid = typeof obj.sid === "string" ? obj.sid : "unknown";
|
|
3756
|
+
const ctx = obj.ctx;
|
|
3757
|
+
const prompt = safeNum(ctx.prompt_tokens);
|
|
3758
|
+
const cached = safeNum(ctx.cached_prompt_tokens);
|
|
3759
|
+
const completion = safeNum(ctx.completion_tokens);
|
|
3760
|
+
const inputTokens = Math.max(0, prompt - cached);
|
|
3761
|
+
const cacheRead = cached;
|
|
3762
|
+
const output = completion;
|
|
3763
|
+
if (inputTokens + cacheRead + output <= 0) continue;
|
|
3764
|
+
const model = models.get(sid) ?? "grok-4.5";
|
|
3765
|
+
const { cost, cacheSavings } = costOf2(model, inputTokens, cacheRead, output);
|
|
3766
|
+
const loop = safeNum(ctx.loop_index);
|
|
3767
|
+
entries.push({
|
|
3768
|
+
ts,
|
|
3769
|
+
id: `${sid}#${obj.ts}#${loop}`,
|
|
3770
|
+
model,
|
|
3771
|
+
input: inputTokens,
|
|
3772
|
+
output,
|
|
3773
|
+
cacheCreate: 0,
|
|
3774
|
+
cacheRead,
|
|
3775
|
+
cost,
|
|
3776
|
+
cacheSavings
|
|
3777
|
+
});
|
|
3778
|
+
} catch {
|
|
3779
|
+
}
|
|
3780
|
+
}
|
|
3781
|
+
} finally {
|
|
3782
|
+
rl.close();
|
|
3783
|
+
input.destroy();
|
|
3784
|
+
}
|
|
3785
|
+
return entries;
|
|
3786
|
+
}
|
|
3787
|
+
async function loadEntries6(since, homeDir) {
|
|
3788
|
+
const files = [];
|
|
3789
|
+
const modelByHome = /* @__PURE__ */ new Map();
|
|
3790
|
+
for (const home of grokHomes(homeDir)) {
|
|
3791
|
+
const logPath = join18(home, "logs", "unified.jsonl");
|
|
3792
|
+
try {
|
|
3793
|
+
await access10(logPath);
|
|
3794
|
+
} catch {
|
|
3795
|
+
continue;
|
|
3796
|
+
}
|
|
3797
|
+
const st = await fsStat6(logPath).catch(() => null);
|
|
3798
|
+
if (!st) continue;
|
|
3799
|
+
files.push({ path: logPath, mtimeMs: st.mtimeMs, size: st.size });
|
|
3800
|
+
modelByHome.set(logPath, await loadSessionModels(home));
|
|
3801
|
+
}
|
|
3802
|
+
if (files.length === 0) return [];
|
|
3803
|
+
return loadCachedEntries(
|
|
3804
|
+
files,
|
|
3805
|
+
async (path) => parseUnifiedLog(path, modelByHome.get(path) ?? /* @__PURE__ */ new Map()),
|
|
3806
|
+
since
|
|
3807
|
+
);
|
|
3808
|
+
}
|
|
3809
|
+
async function grokDashboard(tz, homeDir) {
|
|
3810
|
+
return summarize(await loadEntries6(dashboardSince(tz), homeDir), tz);
|
|
3811
|
+
}
|
|
3812
|
+
async function grokTable(tz, homeDir) {
|
|
3813
|
+
return tabulate(await loadEntries6(tableSince(tz), homeDir), tz);
|
|
3814
|
+
}
|
|
3815
|
+
|
|
3816
|
+
// src/providers/grok/index.ts
|
|
3817
|
+
var grokProvider = {
|
|
3818
|
+
id: "grok",
|
|
3819
|
+
name: "Grok",
|
|
3820
|
+
color: "yellowBright",
|
|
3821
|
+
hasUsage: true,
|
|
3822
|
+
hasBilling: true,
|
|
3823
|
+
detect: (homeDir) => detectGrok(homeDir),
|
|
3824
|
+
fetchSummary: (account, tz) => grokDashboard(tz, account.homeDir),
|
|
3825
|
+
fetchTable: (account, tz) => grokTable(tz, account.homeDir),
|
|
3826
|
+
fetchBilling: (account) => grokBilling(account)
|
|
3827
|
+
};
|
|
3828
|
+
|
|
3451
3829
|
// src/providers/detect.ts
|
|
3452
3830
|
import { accessSync as accessSync2, constants as constants2, existsSync } from "fs";
|
|
3453
|
-
import { join as
|
|
3454
|
-
import { homedir as
|
|
3831
|
+
import { join as join19, delimiter as delimiter2, isAbsolute as isAbsolute2 } from "path";
|
|
3832
|
+
import { homedir as homedir15 } from "os";
|
|
3455
3833
|
function searchDirs() {
|
|
3456
|
-
const home =
|
|
3834
|
+
const home = homedir15();
|
|
3457
3835
|
const fromEnv = (process.env.PATH ?? "").split(delimiter2).filter(Boolean);
|
|
3458
3836
|
const extra = process.platform === "win32" ? [
|
|
3459
|
-
process.env.APPDATA &&
|
|
3460
|
-
process.env.LOCALAPPDATA &&
|
|
3461
|
-
|
|
3837
|
+
process.env.APPDATA && join19(process.env.APPDATA, "npm"),
|
|
3838
|
+
process.env.LOCALAPPDATA && join19(process.env.LOCALAPPDATA, "pnpm"),
|
|
3839
|
+
join19(home, "scoop", "shims")
|
|
3462
3840
|
] : [
|
|
3463
3841
|
"/opt/homebrew/bin",
|
|
3464
3842
|
"/usr/local/bin",
|
|
3465
3843
|
"/usr/bin",
|
|
3466
3844
|
"/bin",
|
|
3467
3845
|
"/opt/local/bin",
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3846
|
+
join19(home, ".local", "bin"),
|
|
3847
|
+
join19(home, "bin"),
|
|
3848
|
+
join19(home, ".npm-global", "bin"),
|
|
3849
|
+
join19(home, ".bun", "bin"),
|
|
3850
|
+
join19(home, ".local", "share", "pnpm")
|
|
3473
3851
|
];
|
|
3474
3852
|
return [.../* @__PURE__ */ new Set([...fromEnv, ...extra.filter((d) => !!d)])];
|
|
3475
3853
|
}
|
|
@@ -3486,7 +3864,7 @@ function onPath(names) {
|
|
|
3486
3864
|
for (const dir of searchDirs()) {
|
|
3487
3865
|
for (const n of names) {
|
|
3488
3866
|
for (const e of exts) {
|
|
3489
|
-
if (isExec2(
|
|
3867
|
+
if (isExec2(join19(dir, n + e))) return true;
|
|
3490
3868
|
}
|
|
3491
3869
|
}
|
|
3492
3870
|
}
|
|
@@ -3496,7 +3874,7 @@ function anyExists(paths) {
|
|
|
3496
3874
|
return paths.some((p) => !!p && isExec2(p));
|
|
3497
3875
|
}
|
|
3498
3876
|
function installSignals(id) {
|
|
3499
|
-
const home =
|
|
3877
|
+
const home = homedir15();
|
|
3500
3878
|
const pf = process.env.ProgramFiles;
|
|
3501
3879
|
const pf86 = process.env["ProgramFiles(x86)"];
|
|
3502
3880
|
const lad = process.env.LOCALAPPDATA;
|
|
@@ -3504,26 +3882,26 @@ function installSignals(id) {
|
|
|
3504
3882
|
case "claude":
|
|
3505
3883
|
return onPath(["claude"]) || anyExists([
|
|
3506
3884
|
"/Applications/Claude.app",
|
|
3507
|
-
|
|
3508
|
-
lad &&
|
|
3885
|
+
join19(home, "Applications", "Claude.app"),
|
|
3886
|
+
lad && join19(lad, "Programs", "claude", "Claude.exe")
|
|
3509
3887
|
]);
|
|
3510
3888
|
case "codex": {
|
|
3511
3889
|
const bin = process.env.CODEX_BIN;
|
|
3512
3890
|
if (bin && isAbsolute2(bin) && isExec2(bin)) return true;
|
|
3513
3891
|
return onPath(["codex"]) || anyExists([
|
|
3514
|
-
lad &&
|
|
3515
|
-
lad &&
|
|
3516
|
-
lad &&
|
|
3517
|
-
pf &&
|
|
3518
|
-
]) || existsSync(
|
|
3892
|
+
lad && join19(lad, "Programs", "OpenAI", "Codex", "bin", "codex.exe"),
|
|
3893
|
+
lad && join19(lad, "Programs", "OpenAI", "Codex", "codex.exe"),
|
|
3894
|
+
lad && join19(lad, "Programs", "codex", "codex.exe"),
|
|
3895
|
+
pf && join19(pf, "OpenAI", "Codex", "bin", "codex.exe")
|
|
3896
|
+
]) || existsSync(join19(home, ".codex", "sessions")) || existsSync(join19(home, ".codex", "auth.json"));
|
|
3519
3897
|
}
|
|
3520
3898
|
case "cursor":
|
|
3521
3899
|
return onPath(["cursor", "cursor-agent"]) || anyExists([
|
|
3522
3900
|
"/Applications/Cursor.app",
|
|
3523
|
-
|
|
3524
|
-
lad &&
|
|
3525
|
-
pf &&
|
|
3526
|
-
pf86 &&
|
|
3901
|
+
join19(home, "Applications", "Cursor.app"),
|
|
3902
|
+
lad && join19(lad, "Programs", "cursor", "Cursor.exe"),
|
|
3903
|
+
pf && join19(pf, "Cursor", "Cursor.exe"),
|
|
3904
|
+
pf86 && join19(pf86, "Cursor", "Cursor.exe"),
|
|
3527
3905
|
"/opt/Cursor/cursor",
|
|
3528
3906
|
"/usr/share/cursor/cursor",
|
|
3529
3907
|
"/usr/bin/cursor"
|
|
@@ -3535,21 +3913,26 @@ function installSignals(id) {
|
|
|
3535
3913
|
case "copilot": {
|
|
3536
3914
|
const appData = process.env.APPDATA;
|
|
3537
3915
|
return [
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
lad &&
|
|
3541
|
-
appData &&
|
|
3542
|
-
|
|
3916
|
+
join19(home, ".config", "github-copilot"),
|
|
3917
|
+
join19(home, ".copilot"),
|
|
3918
|
+
lad && join19(lad, "github-copilot"),
|
|
3919
|
+
appData && join19(appData, "GitHub Copilot"),
|
|
3920
|
+
join19(home, ".local", "share", "gh", "extensions", "gh-copilot")
|
|
3543
3921
|
].some((p) => !!p && existsSync(p));
|
|
3544
3922
|
}
|
|
3545
3923
|
case "antigravity":
|
|
3546
3924
|
return onPath(["antigravity"]) || anyExists([
|
|
3547
3925
|
"/Applications/Antigravity.app",
|
|
3548
|
-
|
|
3549
|
-
lad &&
|
|
3926
|
+
join19(home, "Applications", "Antigravity.app"),
|
|
3927
|
+
lad && join19(lad, "Programs", "Antigravity", "Antigravity.exe")
|
|
3550
3928
|
]);
|
|
3551
3929
|
case "gemini":
|
|
3552
3930
|
return onPath(["gemini"]);
|
|
3931
|
+
case "grok":
|
|
3932
|
+
return onPath(["grok"]) || anyExists([
|
|
3933
|
+
join19(home, ".grok", "bin", "grok"),
|
|
3934
|
+
join19(home, ".grok", "bin", "grok.exe")
|
|
3935
|
+
]) || existsSync(join19(home, ".grok", "auth.json")) || existsSync(join19(home, ".grok", "sessions")) || existsSync(join19(home, ".grok", "logs", "unified.jsonl"));
|
|
3553
3936
|
default:
|
|
3554
3937
|
return false;
|
|
3555
3938
|
}
|
|
@@ -3565,7 +3948,8 @@ var PROVIDERS = {
|
|
|
3565
3948
|
opencode: opencodeProvider,
|
|
3566
3949
|
copilot: copilotProvider,
|
|
3567
3950
|
antigravity: antigravityProvider,
|
|
3568
|
-
gemini: geminiProvider
|
|
3951
|
+
gemini: geminiProvider,
|
|
3952
|
+
grok: grokProvider
|
|
3569
3953
|
};
|
|
3570
3954
|
async function detectProviders() {
|
|
3571
3955
|
const found = await Promise.all(
|
|
@@ -3582,20 +3966,20 @@ async function detectProviders() {
|
|
|
3582
3966
|
}
|
|
3583
3967
|
|
|
3584
3968
|
// src/accounts.ts
|
|
3585
|
-
import { existsSync as existsSync2, readdirSync, readFileSync as
|
|
3586
|
-
import { homedir as
|
|
3587
|
-
import { basename, join as
|
|
3969
|
+
import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync4, statSync } from "fs";
|
|
3970
|
+
import { homedir as homedir16 } from "os";
|
|
3971
|
+
import { basename, join as join21, resolve } from "path";
|
|
3588
3972
|
|
|
3589
3973
|
// src/providers/codex/identity.ts
|
|
3590
|
-
import { readFileSync as
|
|
3591
|
-
import { join as
|
|
3974
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
3975
|
+
import { join as join20 } from "path";
|
|
3592
3976
|
function codexAuthPaths(homeDir) {
|
|
3593
|
-
return [
|
|
3977
|
+
return [join20(homeDir, ".codex", "auth.json"), join20(homeDir, "auth.json")];
|
|
3594
3978
|
}
|
|
3595
3979
|
function readCodexIdentity(homeDir) {
|
|
3596
3980
|
for (const path of codexAuthPaths(homeDir)) {
|
|
3597
3981
|
try {
|
|
3598
|
-
const parsed = JSON.parse(
|
|
3982
|
+
const parsed = JSON.parse(readFileSync3(path, "utf-8"));
|
|
3599
3983
|
const { email, displayName, payload } = identityFromIdToken(parsed?.tokens?.id_token);
|
|
3600
3984
|
if (!payload) continue;
|
|
3601
3985
|
return { email, displayName };
|
|
@@ -3607,7 +3991,7 @@ function readCodexIdentity(homeDir) {
|
|
|
3607
3991
|
|
|
3608
3992
|
// src/accounts.ts
|
|
3609
3993
|
function accountKey(providerId, homeDir) {
|
|
3610
|
-
return `${providerId}:${homeDir ? resolve(expandHome(homeDir)) :
|
|
3994
|
+
return `${providerId}:${homeDir ? resolve(expandHome(homeDir)) : homedir16()}`;
|
|
3611
3995
|
}
|
|
3612
3996
|
function uniqueId(base, used) {
|
|
3613
3997
|
let id = slugify(base) || "account";
|
|
@@ -3627,10 +4011,10 @@ function uniqueId(base, used) {
|
|
|
3627
4011
|
return id;
|
|
3628
4012
|
}
|
|
3629
4013
|
function hasClaudeState(homeDir) {
|
|
3630
|
-
return existsSync2(
|
|
4014
|
+
return existsSync2(join21(homeDir, ".claude.json")) || existsSync2(join21(homeDir, ".claude", ".credentials.json")) || existsSync2(join21(homeDir, ".claude", "projects")) || existsSync2(join21(homeDir, ".config", "claude", ".credentials.json")) || existsSync2(join21(homeDir, ".config", "claude", "projects"));
|
|
3631
4015
|
}
|
|
3632
4016
|
function candidateAlternateHomes(prefix) {
|
|
3633
|
-
const home =
|
|
4017
|
+
const home = homedir16();
|
|
3634
4018
|
let entries;
|
|
3635
4019
|
try {
|
|
3636
4020
|
entries = readdirSync(home);
|
|
@@ -3641,7 +4025,7 @@ function candidateAlternateHomes(prefix) {
|
|
|
3641
4025
|
const pattern = new RegExp(`^\\.${prefix}[_-]`);
|
|
3642
4026
|
for (const name of entries) {
|
|
3643
4027
|
if (!pattern.test(name)) continue;
|
|
3644
|
-
const path =
|
|
4028
|
+
const path = join21(home, name);
|
|
3645
4029
|
try {
|
|
3646
4030
|
if (!statSync(path).isDirectory()) continue;
|
|
3647
4031
|
out.push(path);
|
|
@@ -3660,7 +4044,7 @@ function labelForClaudeHome(homeDir) {
|
|
|
3660
4044
|
function hasCodexAuth(homeDir) {
|
|
3661
4045
|
for (const path of codexAuthPaths(homeDir)) {
|
|
3662
4046
|
try {
|
|
3663
|
-
const parsed = JSON.parse(
|
|
4047
|
+
const parsed = JSON.parse(readFileSync4(path, "utf-8"));
|
|
3664
4048
|
const accessToken = parsed?.tokens?.access_token;
|
|
3665
4049
|
if (typeof accessToken === "string" && accessToken.trim()) return true;
|
|
3666
4050
|
} catch {
|
|
@@ -3782,7 +4166,8 @@ var PROVIDER_HEX = {
|
|
|
3782
4166
|
pi: NAMED_HEX.blue,
|
|
3783
4167
|
opencode: NAMED_HEX.yellow,
|
|
3784
4168
|
antigravity: NAMED_HEX.red,
|
|
3785
|
-
gemini: NAMED_HEX.greenBright
|
|
4169
|
+
gemini: NAMED_HEX.greenBright,
|
|
4170
|
+
grok: NAMED_HEX.yellowBright
|
|
3786
4171
|
};
|
|
3787
4172
|
function namedHex(name) {
|
|
3788
4173
|
if (!name) return FALLBACK_HEX;
|
|
@@ -3950,7 +4335,6 @@ export {
|
|
|
3950
4335
|
time,
|
|
3951
4336
|
shortDate,
|
|
3952
4337
|
col,
|
|
3953
|
-
cursorModelSpend,
|
|
3954
4338
|
PROVIDER_ORDER,
|
|
3955
4339
|
PROVIDERS,
|
|
3956
4340
|
detectProviders,
|