whoburnedmore 0.9.24 → 0.9.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +784 -634
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -33,6 +33,54 @@ function applyScope(payload, flags) {
|
|
|
33
33
|
payload.orgCode = flags.orgCode;
|
|
34
34
|
return payload;
|
|
35
35
|
}
|
|
36
|
+
var VALUE_FLAGS = /* @__PURE__ */ new Set(["--board", "--org", "--token", "--pass", "--code"]);
|
|
37
|
+
var KNOWN_FLAGS = /* @__PURE__ */ new Set([
|
|
38
|
+
...VALUE_FLAGS,
|
|
39
|
+
"--dry-run",
|
|
40
|
+
"--no-submit",
|
|
41
|
+
"--local",
|
|
42
|
+
"--watch",
|
|
43
|
+
"--help",
|
|
44
|
+
"-h",
|
|
45
|
+
"--version",
|
|
46
|
+
"-v"
|
|
47
|
+
]);
|
|
48
|
+
var NO_UPLOAD_FLAGS = ["--dry-run", "--no-submit", "--local"];
|
|
49
|
+
function argumentProblem(args, command) {
|
|
50
|
+
if (command === "help" || command === "version") return null;
|
|
51
|
+
for (let i = 0; i < args.length; i++) {
|
|
52
|
+
const a = args[i];
|
|
53
|
+
if (!a.startsWith("-")) continue;
|
|
54
|
+
const name = a.includes("=") ? a.slice(0, a.indexOf("=")) : a;
|
|
55
|
+
if (!KNOWN_FLAGS.has(name)) return `Unknown option ${name}.`;
|
|
56
|
+
if (!VALUE_FLAGS.has(name) && a.includes("=")) {
|
|
57
|
+
return `${name} doesn't take a value; use ${name} on its own.`;
|
|
58
|
+
}
|
|
59
|
+
if (VALUE_FLAGS.has(name)) {
|
|
60
|
+
if (parseValueFlag([a, args[i + 1] ?? ""], name) === void 0) {
|
|
61
|
+
const example = { "--board": "CODE", "--org": "SLUG", "--token": "TOKEN" }[name] ?? "PASSWORD";
|
|
62
|
+
return `${name} needs a value, for example ${name}=${example}.`;
|
|
63
|
+
}
|
|
64
|
+
if (!a.includes("=")) i++;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const noUpload = NO_UPLOAD_FLAGS.filter((f) => args.includes(f));
|
|
68
|
+
if (args.includes("--watch")) {
|
|
69
|
+
if (command !== "run" && command !== "sync" && command !== "daemon") {
|
|
70
|
+
return `--watch only works with sync (it keeps uploading every 15 minutes).`;
|
|
71
|
+
}
|
|
72
|
+
if (noUpload.length > 0) {
|
|
73
|
+
return `--watch keeps uploading every 15 minutes, so it can't be combined with ${noUpload.join(", ")}.`;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if ((command === "sync" || command === "daemon") && noUpload.length > 0) {
|
|
77
|
+
return `${command} always uploads, so it can't be combined with ${noUpload.join(", ")}. To preview without sending, run \`npx whoburnedmore --dry-run\`.`;
|
|
78
|
+
}
|
|
79
|
+
if (command !== "run" && noUpload.length > 0) {
|
|
80
|
+
return `${noUpload.join(", ")} only works with the default command, not \`${command}\`.`;
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
36
84
|
function parseValueFlag(args, name) {
|
|
37
85
|
const prefix = `${name}=`;
|
|
38
86
|
const eq = args.find((a) => a.startsWith(prefix));
|
|
@@ -44,14 +92,13 @@ function parseValueFlag(args, name) {
|
|
|
44
92
|
return void 0;
|
|
45
93
|
}
|
|
46
94
|
function resolveCommand(args) {
|
|
47
|
-
const valueFlags = /* @__PURE__ */ new Set(["--board", "--org", "--token", "--pass", "--code"]);
|
|
48
95
|
const flags = [];
|
|
49
96
|
const words = [];
|
|
50
97
|
for (let i = 0; i < args.length; i++) {
|
|
51
98
|
const a = args[i];
|
|
52
99
|
if (a.startsWith("-")) {
|
|
53
100
|
flags.push(a);
|
|
54
|
-
if (
|
|
101
|
+
if (VALUE_FLAGS.has(a) && args[i + 1] !== void 0 && !args[i + 1].startsWith("-")) i++;
|
|
55
102
|
continue;
|
|
56
103
|
}
|
|
57
104
|
words.push(a);
|
|
@@ -64,6 +111,10 @@ function resolveCommand(args) {
|
|
|
64
111
|
}
|
|
65
112
|
return words[0] ?? "run";
|
|
66
113
|
}
|
|
114
|
+
function confirmAnswer(answer, defaultYes) {
|
|
115
|
+
const a = answer.trim();
|
|
116
|
+
return a === "" ? defaultYes : /^y(es)?$/i.test(a);
|
|
117
|
+
}
|
|
67
118
|
|
|
68
119
|
// src/http-bounds.ts
|
|
69
120
|
async function readResponseBytesCapped(response, maxBytes) {
|
|
@@ -422,6 +473,7 @@ function loadConfig(dir = defaultConfigDir()) {
|
|
|
422
473
|
if (typeof parsed.deviceBoundAt === "number" && Number.isFinite(parsed.deviceBoundAt)) {
|
|
423
474
|
config.deviceBoundAt = parsed.deviceBoundAt;
|
|
424
475
|
}
|
|
476
|
+
if (parsed.autoSyncOptOut === true) config.autoSyncOptOut = true;
|
|
425
477
|
return Object.keys(config).length > 0 ? config : null;
|
|
426
478
|
} catch {
|
|
427
479
|
return null;
|
|
@@ -448,6 +500,15 @@ function recordDeviceBound(dir = defaultConfigDir(), when = Date.now()) {
|
|
|
448
500
|
const config = loadConfig(dir) ?? {};
|
|
449
501
|
saveConfig(dir, { ...config, deviceBoundAt: when });
|
|
450
502
|
}
|
|
503
|
+
function setAutoSyncOptOut(optOut, dir = defaultConfigDir()) {
|
|
504
|
+
const config = loadConfig(dir);
|
|
505
|
+
if (!optOut && !config?.autoSyncOptOut) return;
|
|
506
|
+
const { autoSyncOptOut: _o, ...rest } = config ?? {};
|
|
507
|
+
saveConfig(dir, optOut ? { ...rest, autoSyncOptOut: true } : rest);
|
|
508
|
+
}
|
|
509
|
+
function autoSyncOptedOut(dir = defaultConfigDir()) {
|
|
510
|
+
return loadConfig(dir)?.autoSyncOptOut === true;
|
|
511
|
+
}
|
|
451
512
|
function saveAuth(dir = defaultConfigDir(), auth = { cliToken: "" }) {
|
|
452
513
|
const config = loadConfig(dir) ?? {};
|
|
453
514
|
saveConfig(dir, {
|
|
@@ -706,8 +767,13 @@ var FORWARDED_ENV_VARS = [
|
|
|
706
767
|
// dashboard URL shown to the user
|
|
707
768
|
"XDG_CONFIG_HOME",
|
|
708
769
|
// base for the default config dir
|
|
709
|
-
"CLAUDE_CONFIG_DIR"
|
|
770
|
+
"CLAUDE_CONFIG_DIR",
|
|
710
771
|
// a primary usage-collection source
|
|
772
|
+
// Usage is bucketed by local calendar day and the native parse caches record
|
|
773
|
+
// the zone: a shell TZ that differs from the system zone must reach the
|
|
774
|
+
// background job too, or the two runs bucket differently and keep
|
|
775
|
+
// invalidating each other's caches.
|
|
776
|
+
"TZ"
|
|
711
777
|
];
|
|
712
778
|
function syncEnv(opts) {
|
|
713
779
|
const env = opts?.env ?? process.env;
|
|
@@ -1153,8 +1219,7 @@ import { promisify } from "node:util";
|
|
|
1153
1219
|
|
|
1154
1220
|
// src/attribution.ts
|
|
1155
1221
|
import { readdirSync, statSync as statSync2 } from "node:fs";
|
|
1156
|
-
import {
|
|
1157
|
-
import { join as join6 } from "node:path";
|
|
1222
|
+
import { join as join7 } from "node:path";
|
|
1158
1223
|
|
|
1159
1224
|
// src/native/claude.ts
|
|
1160
1225
|
import { readdir } from "node:fs/promises";
|
|
@@ -6540,6 +6605,7 @@ function normalizeModelKey(model) {
|
|
|
6540
6605
|
/^(?:us|eu|au|jp|apac|global|anthropic|amazon|meta|openai|google|vertex|azure|mistral)\./.test(prefix) ? "" : prefix
|
|
6541
6606
|
));
|
|
6542
6607
|
k = k.replace(/:\d+$/, "").replace(/:latest$/, "");
|
|
6608
|
+
k = k.replace(/\[[^\]]*\]$/, "");
|
|
6543
6609
|
return k;
|
|
6544
6610
|
}
|
|
6545
6611
|
function lookup(key) {
|
|
@@ -6555,7 +6621,7 @@ function resolveModelPrice(model) {
|
|
|
6555
6621
|
resolveCache.set(model, resolved);
|
|
6556
6622
|
return resolved;
|
|
6557
6623
|
}
|
|
6558
|
-
function
|
|
6624
|
+
function exactCandidates(model) {
|
|
6559
6625
|
const norm2 = normalizeModelKey(model);
|
|
6560
6626
|
const candidates = [norm2];
|
|
6561
6627
|
const dateless = norm2.replace(/[-@]20\d{6}$/, "").replace(/-20\d{2}-\d{2}-\d{2}$/, "");
|
|
@@ -6563,12 +6629,19 @@ function resolveModelPriceUncached(model) {
|
|
|
6563
6629
|
candidates.push(dateless);
|
|
6564
6630
|
if (norm2.endsWith("-latest"))
|
|
6565
6631
|
candidates.push(norm2.slice(0, -"-latest".length));
|
|
6566
|
-
|
|
6567
|
-
|
|
6568
|
-
|
|
6632
|
+
return { candidates, dateless };
|
|
6633
|
+
}
|
|
6634
|
+
function lookupExact(model) {
|
|
6635
|
+
for (const c of exactCandidates(model).candidates) {
|
|
6636
|
+
const row = lookup(c);
|
|
6569
6637
|
if (row)
|
|
6570
|
-
|
|
6638
|
+
return row;
|
|
6571
6639
|
}
|
|
6640
|
+
return void 0;
|
|
6641
|
+
}
|
|
6642
|
+
function resolveModelPriceUncached(model) {
|
|
6643
|
+
const { dateless } = exactCandidates(model);
|
|
6644
|
+
let row = lookupExact(model);
|
|
6572
6645
|
if (!row) {
|
|
6573
6646
|
let base = dateless;
|
|
6574
6647
|
for (let i = 0; i < 3 && !row; i++) {
|
|
@@ -6995,12 +7068,20 @@ function nativeCachePath(reader, env = process.env) {
|
|
|
6995
7068
|
const dir = override || defaultConfigDir();
|
|
6996
7069
|
return join4(dir, `native-cache-${reader}.json`);
|
|
6997
7070
|
}
|
|
7071
|
+
function cacheTimeZone() {
|
|
7072
|
+
try {
|
|
7073
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || "unknown";
|
|
7074
|
+
} catch {
|
|
7075
|
+
return "unknown";
|
|
7076
|
+
}
|
|
7077
|
+
}
|
|
6998
7078
|
async function loadCache(path, version, maxBytes) {
|
|
6999
7079
|
try {
|
|
7000
7080
|
const read = await readTextFileCapped(path, { maxBytes });
|
|
7001
7081
|
if (!read.ok) return {};
|
|
7002
7082
|
const parsed = JSON.parse(read.content);
|
|
7003
|
-
|
|
7083
|
+
const sameZone = parsed?.tz === void 0 || parsed.tz === cacheTimeZone();
|
|
7084
|
+
if (parsed && parsed.v === version && sameZone && parsed.files && typeof parsed.files === "object") {
|
|
7004
7085
|
return parsed.files;
|
|
7005
7086
|
}
|
|
7006
7087
|
} catch {
|
|
@@ -7014,7 +7095,7 @@ function saveCache(path, version, files, maxBytes) {
|
|
|
7014
7095
|
mkdirSync3(dir, { recursive: true, mode: 448 });
|
|
7015
7096
|
chmodSync2(dir, 448);
|
|
7016
7097
|
tmp = `${path}.tmp-${process.pid}-${randomBytes3(8).toString("hex")}`;
|
|
7017
|
-
const serialized = JSON.stringify({ v: version, files });
|
|
7098
|
+
const serialized = JSON.stringify({ v: version, tz: cacheTimeZone(), files });
|
|
7018
7099
|
if (Buffer.byteLength(serialized, "utf8") > maxBytes) return;
|
|
7019
7100
|
writeFileSync4(tmp, serialized, { encoding: "utf8", flag: "wx", mode: 384 });
|
|
7020
7101
|
chmodSync2(tmp, 384);
|
|
@@ -7037,6 +7118,7 @@ async function readFilesWithCache(opts) {
|
|
|
7037
7118
|
const cached = await loadCache(opts.cachePath, opts.version, maxCacheBytes);
|
|
7038
7119
|
const fresh = {};
|
|
7039
7120
|
const itemsByFile = [];
|
|
7121
|
+
const permissionDenied = [];
|
|
7040
7122
|
let filesRead = 0;
|
|
7041
7123
|
for (const f of opts.files) {
|
|
7042
7124
|
let size;
|
|
@@ -7070,6 +7152,10 @@ async function readFilesWithCache(opts) {
|
|
|
7070
7152
|
}
|
|
7071
7153
|
if (opts.streamFile) {
|
|
7072
7154
|
const streamed = await opts.streamFile(f).catch(() => ({ ok: false, reason: "unreadable" }));
|
|
7155
|
+
if (!streamed.ok && streamed.reason === "permission-denied") {
|
|
7156
|
+
permissionDenied.push(f);
|
|
7157
|
+
continue;
|
|
7158
|
+
}
|
|
7073
7159
|
if (!streamed.ok) {
|
|
7074
7160
|
const progress = { ...cached, ...fresh };
|
|
7075
7161
|
delete progress[f];
|
|
@@ -7099,7 +7185,12 @@ async function readFilesWithCache(opts) {
|
|
|
7099
7185
|
filesRead += 1;
|
|
7100
7186
|
}
|
|
7101
7187
|
saveCache(opts.cachePath, opts.version, fresh, maxCacheBytes);
|
|
7102
|
-
return {
|
|
7188
|
+
return {
|
|
7189
|
+
itemsByFile,
|
|
7190
|
+
filesRead,
|
|
7191
|
+
timedOut: false,
|
|
7192
|
+
...permissionDenied.length > 0 ? { permissionDenied } : {}
|
|
7193
|
+
};
|
|
7103
7194
|
}
|
|
7104
7195
|
|
|
7105
7196
|
// src/native/jsonl-stream.ts
|
|
@@ -7146,7 +7237,9 @@ async function readJsonlStream(path, onLine, opts) {
|
|
|
7146
7237
|
if (pendingBytes > 0) onLine(Buffer.concat(parts, pendingBytes).toString("utf8"));
|
|
7147
7238
|
if (now() > opts.deadline) return { ok: false, reason: "timed-out" };
|
|
7148
7239
|
return { ok: true };
|
|
7149
|
-
} catch {
|
|
7240
|
+
} catch (err) {
|
|
7241
|
+
const code = err?.code;
|
|
7242
|
+
if (code === "EACCES" || code === "EPERM") return { ok: false, reason: "permission-denied" };
|
|
7150
7243
|
return { ok: false, reason: "unreadable" };
|
|
7151
7244
|
} finally {
|
|
7152
7245
|
await handle?.close().catch(() => void 0);
|
|
@@ -7196,6 +7289,7 @@ function parseClaudeLine(raw) {
|
|
|
7196
7289
|
} catch {
|
|
7197
7290
|
return null;
|
|
7198
7291
|
}
|
|
7292
|
+
if (!obj || typeof obj !== "object") return null;
|
|
7199
7293
|
const message = obj.message;
|
|
7200
7294
|
if (!message || typeof message !== "object") return null;
|
|
7201
7295
|
const usage = message.usage;
|
|
@@ -7406,7 +7500,10 @@ async function collectClaudeNative(env = process.env, opts = {}) {
|
|
|
7406
7500
|
return {
|
|
7407
7501
|
entries: finalizeClaudeEntries(acc),
|
|
7408
7502
|
found: true,
|
|
7409
|
-
filesScanned: res.filesRead
|
|
7503
|
+
filesScanned: res.filesRead,
|
|
7504
|
+
// Skipped files mean this scan is partial: keep its rows, but flag it the
|
|
7505
|
+
// way a timeout is flagged so request fingerprints are never lowered.
|
|
7506
|
+
...res.permissionDenied ? { timedOut: true, permissionDenied: res.permissionDenied } : {}
|
|
7410
7507
|
};
|
|
7411
7508
|
}
|
|
7412
7509
|
async function collectClaudeRequests(env = process.env, opts = {}) {
|
|
@@ -7418,6 +7515,7 @@ async function collectClaudeRequests(env = process.env, opts = {}) {
|
|
|
7418
7515
|
const deadline = now() + (opts.budgetMs ?? NATIVE_READ_BUDGET_MS);
|
|
7419
7516
|
const acc = /* @__PURE__ */ new Map();
|
|
7420
7517
|
const onLine = boundedLineAccumulator(acc, opts.maxRequestBytes);
|
|
7518
|
+
let skipped = false;
|
|
7421
7519
|
for (const f of files) {
|
|
7422
7520
|
if (now() > deadline) {
|
|
7423
7521
|
return { requests: [], found: false, timedOut: true };
|
|
@@ -7427,164 +7525,432 @@ async function collectClaudeRequests(env = process.env, opts = {}) {
|
|
|
7427
7525
|
deadline,
|
|
7428
7526
|
now
|
|
7429
7527
|
});
|
|
7528
|
+
if (!read.ok && read.reason === "permission-denied") {
|
|
7529
|
+
skipped = true;
|
|
7530
|
+
continue;
|
|
7531
|
+
}
|
|
7430
7532
|
if (!read.ok) {
|
|
7431
7533
|
return { requests: [], found: false, timedOut: true };
|
|
7432
7534
|
}
|
|
7433
7535
|
}
|
|
7434
|
-
return { requests: [...acc.values()], found: true };
|
|
7536
|
+
return { requests: [...acc.values()], found: true, ...skipped ? { timedOut: true } : {} };
|
|
7435
7537
|
}
|
|
7436
7538
|
|
|
7437
|
-
// src/
|
|
7438
|
-
|
|
7439
|
-
|
|
7440
|
-
|
|
7441
|
-
|
|
7442
|
-
|
|
7443
|
-
|
|
7444
|
-
function createAccumulator() {
|
|
7445
|
-
return {
|
|
7446
|
-
tools: /* @__PURE__ */ new Map(),
|
|
7447
|
-
skills: /* @__PURE__ */ new Map(),
|
|
7448
|
-
agent: {
|
|
7449
|
-
messageCount: 0,
|
|
7450
|
-
subagentMessages: 0,
|
|
7451
|
-
subagentTokens: 0,
|
|
7452
|
-
totalTokens: 0,
|
|
7453
|
-
userMessageCount: 0
|
|
7454
|
-
},
|
|
7455
|
-
sessionMessages: /* @__PURE__ */ new Map()
|
|
7456
|
-
};
|
|
7457
|
-
}
|
|
7458
|
-
function createFileContext() {
|
|
7459
|
-
return { toolNames: /* @__PURE__ */ new Map() };
|
|
7460
|
-
}
|
|
7461
|
-
function recordTokens(usage) {
|
|
7462
|
-
if (!usage || typeof usage !== "object") return 0;
|
|
7463
|
-
const u = usage;
|
|
7464
|
-
const n = (v) => {
|
|
7465
|
-
const x = Math.round(Number(v));
|
|
7466
|
-
return Number.isFinite(x) && x > 0 ? x : 0;
|
|
7467
|
-
};
|
|
7468
|
-
return n(u.input_tokens) + n(u.output_tokens) + n(u.cache_creation_input_tokens) + n(u.cache_read_input_tokens);
|
|
7539
|
+
// src/native/codex.ts
|
|
7540
|
+
import { readdir as readdir2 } from "node:fs/promises";
|
|
7541
|
+
import { homedir as homedir4 } from "node:os";
|
|
7542
|
+
import { basename, join as join6, resolve as resolve2 } from "node:path";
|
|
7543
|
+
function num2(n) {
|
|
7544
|
+
const v = Math.round(Number(n));
|
|
7545
|
+
return Number.isFinite(v) && v > 0 ? v : 0;
|
|
7469
7546
|
}
|
|
7470
|
-
function
|
|
7471
|
-
const
|
|
7472
|
-
|
|
7473
|
-
|
|
7547
|
+
function readTokenFields(payload) {
|
|
7548
|
+
const info = payload.info;
|
|
7549
|
+
const src = info?.total_token_usage ?? payload;
|
|
7550
|
+
const input = src.input_tokens;
|
|
7551
|
+
const output = src.output_tokens;
|
|
7552
|
+
if (input === void 0 && output === void 0) return null;
|
|
7553
|
+
return {
|
|
7554
|
+
input: num2(input),
|
|
7555
|
+
cached: num2(src.cached_input_tokens),
|
|
7556
|
+
output: num2(output)
|
|
7474
7557
|
};
|
|
7475
|
-
if (typeof content === "string") return isHuman(content);
|
|
7476
|
-
if (!Array.isArray(content)) return false;
|
|
7477
|
-
return content.some(
|
|
7478
|
-
(b) => b !== null && typeof b === "object" && b.type === "text" && typeof b.text === "string" && isHuman(b.text)
|
|
7479
|
-
);
|
|
7480
7558
|
}
|
|
7481
|
-
function
|
|
7482
|
-
|
|
7483
|
-
|
|
7484
|
-
|
|
7485
|
-
|
|
7486
|
-
|
|
7487
|
-
|
|
7488
|
-
|
|
7489
|
-
|
|
7490
|
-
|
|
7491
|
-
|
|
7492
|
-
|
|
7493
|
-
|
|
7494
|
-
|
|
7495
|
-
const content = r.message?.content;
|
|
7496
|
-
if (!Array.isArray(content)) return;
|
|
7497
|
-
const isAssistant = r.type === "assistant" || r.message?.role === "assistant";
|
|
7498
|
-
const isUser = r.type === "user" || r.message?.role === "user";
|
|
7499
|
-
if (isAssistant) {
|
|
7500
|
-
const toolUses = [];
|
|
7501
|
-
for (const block of content) {
|
|
7502
|
-
if (block && typeof block === "object" && block.type === "tool_use" && typeof block.name === "string") {
|
|
7503
|
-
const name = block.name.slice(0, 128);
|
|
7504
|
-
if (!name) continue;
|
|
7505
|
-
const id = block.id;
|
|
7506
|
-
toolUses.push({ name, id: typeof id === "string" ? id : void 0 });
|
|
7507
|
-
}
|
|
7508
|
-
}
|
|
7509
|
-
const perToolTokens = toolUses.length > 0 ? Math.floor(recTokens / toolUses.length) : 0;
|
|
7510
|
-
for (const tu of toolUses) {
|
|
7511
|
-
const t = acc.tools.get(tu.name) ?? { count: 0, errors: 0, tokens: 0 };
|
|
7512
|
-
t.count += 1;
|
|
7513
|
-
t.tokens += perToolTokens;
|
|
7514
|
-
acc.tools.set(tu.name, t);
|
|
7515
|
-
if (tu.id) ctx.toolNames.set(tu.id, tu.name);
|
|
7559
|
+
function inspectCodexRollout(lines) {
|
|
7560
|
+
let model = "unknown";
|
|
7561
|
+
let sawSessionMeta = false;
|
|
7562
|
+
let replayedRollout = false;
|
|
7563
|
+
const perDay = /* @__PURE__ */ new Map();
|
|
7564
|
+
let lastSeenDate = null;
|
|
7565
|
+
for (const raw of lines) {
|
|
7566
|
+
const trimmed = raw.trim();
|
|
7567
|
+
if (!trimmed) continue;
|
|
7568
|
+
let obj;
|
|
7569
|
+
try {
|
|
7570
|
+
obj = JSON.parse(trimmed);
|
|
7571
|
+
} catch {
|
|
7572
|
+
continue;
|
|
7516
7573
|
}
|
|
7517
|
-
|
|
7518
|
-
|
|
7519
|
-
|
|
7520
|
-
const
|
|
7521
|
-
if (
|
|
7522
|
-
|
|
7523
|
-
|
|
7574
|
+
if (!obj || typeof obj !== "object") continue;
|
|
7575
|
+
const payload = obj.payload;
|
|
7576
|
+
if (!payload || typeof payload !== "object") continue;
|
|
7577
|
+
const kind = obj.type;
|
|
7578
|
+
if (kind === "session_meta" && !sawSessionMeta) {
|
|
7579
|
+
sawSessionMeta = true;
|
|
7580
|
+
const source = payload.source;
|
|
7581
|
+
if (typeof payload.forked_from_id === "string" || typeof payload.parent_thread_id === "string" || source && typeof source === "object" && source.subagent !== void 0) {
|
|
7582
|
+
replayedRollout = true;
|
|
7583
|
+
}
|
|
7524
7584
|
}
|
|
7525
|
-
if (
|
|
7526
|
-
|
|
7527
|
-
r.sessionId,
|
|
7528
|
-
(acc.sessionMessages.get(r.sessionId) ?? 0) + 1
|
|
7529
|
-
);
|
|
7585
|
+
if (kind === "session_meta" || kind === "turn_context") {
|
|
7586
|
+
if (typeof payload.model === "string" && payload.model) model = payload.model;
|
|
7530
7587
|
}
|
|
7531
|
-
|
|
7532
|
-
|
|
7533
|
-
if (
|
|
7534
|
-
|
|
7535
|
-
|
|
7536
|
-
|
|
7537
|
-
|
|
7538
|
-
|
|
7539
|
-
|
|
7540
|
-
|
|
7588
|
+
if (payload.type === "token_count") {
|
|
7589
|
+
const fields = readTokenFields(payload);
|
|
7590
|
+
if (!fields) continue;
|
|
7591
|
+
const parsed = localUsageDate(Date.parse(String(obj.timestamp ?? "")));
|
|
7592
|
+
const day = parsed ?? lastSeenDate;
|
|
7593
|
+
if (!day) continue;
|
|
7594
|
+
lastSeenDate = day;
|
|
7595
|
+
const e = perDay.get(day);
|
|
7596
|
+
if (e) {
|
|
7597
|
+
e.cum = fields;
|
|
7598
|
+
e.turns += 1;
|
|
7599
|
+
} else {
|
|
7600
|
+
perDay.set(day, { cum: fields, turns: 1 });
|
|
7541
7601
|
}
|
|
7542
7602
|
}
|
|
7543
7603
|
}
|
|
7544
|
-
|
|
7545
|
-
|
|
7546
|
-
|
|
7547
|
-
|
|
7548
|
-
|
|
7549
|
-
|
|
7550
|
-
const
|
|
7551
|
-
|
|
7552
|
-
|
|
7553
|
-
|
|
7554
|
-
|
|
7555
|
-
|
|
7556
|
-
|
|
7604
|
+
if (perDay.size === 0)
|
|
7605
|
+
return { sessions: [], replaySessions: [], replayCandidateDates: [] };
|
|
7606
|
+
const dates = [...perDay.keys()].sort();
|
|
7607
|
+
const out = [];
|
|
7608
|
+
let prev = { input: 0, cached: 0, output: 0 };
|
|
7609
|
+
for (const date of dates) {
|
|
7610
|
+
const { cum, turns } = perDay.get(date);
|
|
7611
|
+
const dInput = Math.max(0, cum.input - prev.input);
|
|
7612
|
+
const dCached = Math.max(0, cum.cached - prev.cached);
|
|
7613
|
+
const dOutput = Math.max(0, cum.output - prev.output);
|
|
7614
|
+
prev = cum;
|
|
7615
|
+
const cacheReadTokens = Math.min(dCached, dInput);
|
|
7616
|
+
const inputTokens = dInput - cacheReadTokens;
|
|
7617
|
+
const outputTokens = dOutput;
|
|
7618
|
+
if (inputTokens + outputTokens + cacheReadTokens === 0) continue;
|
|
7619
|
+
out.push({
|
|
7620
|
+
date,
|
|
7621
|
+
model,
|
|
7622
|
+
inputTokens,
|
|
7623
|
+
outputTokens,
|
|
7624
|
+
cacheCreationTokens: 0,
|
|
7625
|
+
cacheReadTokens,
|
|
7626
|
+
turnCount: turns
|
|
7627
|
+
});
|
|
7628
|
+
}
|
|
7557
7629
|
return {
|
|
7558
|
-
|
|
7559
|
-
|
|
7560
|
-
|
|
7561
|
-
sessionMessages: acc.sessionMessages,
|
|
7562
|
-
complete: true
|
|
7630
|
+
sessions: replayedRollout ? [] : out,
|
|
7631
|
+
replaySessions: replayedRollout ? out : [],
|
|
7632
|
+
replayCandidateDates: replayedRollout ? dates : []
|
|
7563
7633
|
};
|
|
7564
7634
|
}
|
|
7565
|
-
function
|
|
7566
|
-
const
|
|
7567
|
-
|
|
7568
|
-
|
|
7569
|
-
|
|
7570
|
-
|
|
7571
|
-
|
|
7572
|
-
|
|
7573
|
-
|
|
7574
|
-
|
|
7575
|
-
|
|
7576
|
-
|
|
7577
|
-
|
|
7578
|
-
|
|
7579
|
-
|
|
7580
|
-
|
|
7581
|
-
|
|
7582
|
-
|
|
7583
|
-
|
|
7584
|
-
|
|
7585
|
-
|
|
7586
|
-
|
|
7587
|
-
|
|
7635
|
+
function foldCodexSessions(acc, sessions) {
|
|
7636
|
+
for (const s of sessions) {
|
|
7637
|
+
const k = `${s.date}|${s.model}`;
|
|
7638
|
+
let b = acc.get(k);
|
|
7639
|
+
if (!b) {
|
|
7640
|
+
b = {
|
|
7641
|
+
date: s.date,
|
|
7642
|
+
model: s.model,
|
|
7643
|
+
inputTokens: 0,
|
|
7644
|
+
outputTokens: 0,
|
|
7645
|
+
cacheCreationTokens: 0,
|
|
7646
|
+
cacheReadTokens: 0,
|
|
7647
|
+
requestCount: 0
|
|
7648
|
+
};
|
|
7649
|
+
acc.set(k, b);
|
|
7650
|
+
}
|
|
7651
|
+
b.inputTokens += s.inputTokens;
|
|
7652
|
+
b.outputTokens += s.outputTokens;
|
|
7653
|
+
b.cacheCreationTokens += s.cacheCreationTokens;
|
|
7654
|
+
b.cacheReadTokens += s.cacheReadTokens;
|
|
7655
|
+
b.requestCount += s.turnCount;
|
|
7656
|
+
}
|
|
7657
|
+
}
|
|
7658
|
+
function finalizeCodexEntries(acc) {
|
|
7659
|
+
const entries = [];
|
|
7660
|
+
for (const b of acc.values()) {
|
|
7661
|
+
entries.push({
|
|
7662
|
+
date: b.date,
|
|
7663
|
+
tool: "codex",
|
|
7664
|
+
model: b.model,
|
|
7665
|
+
inputTokens: b.inputTokens,
|
|
7666
|
+
outputTokens: b.outputTokens,
|
|
7667
|
+
cacheCreationTokens: b.cacheCreationTokens,
|
|
7668
|
+
cacheReadTokens: b.cacheReadTokens,
|
|
7669
|
+
costUSD: estimateCostUSD(b.model, b),
|
|
7670
|
+
origin: "cli",
|
|
7671
|
+
verified: false,
|
|
7672
|
+
requestCount: b.requestCount
|
|
7673
|
+
});
|
|
7674
|
+
}
|
|
7675
|
+
return entries;
|
|
7676
|
+
}
|
|
7677
|
+
function resolveCodexHome(env = process.env) {
|
|
7678
|
+
return env.CODEX_HOME && env.CODEX_HOME.trim() ? env.CODEX_HOME.trim() : join6(homedir4(), ".codex");
|
|
7679
|
+
}
|
|
7680
|
+
function resolveCodexSessionsDirs(env = process.env) {
|
|
7681
|
+
const home = resolve2(resolveCodexHome(env));
|
|
7682
|
+
return [join6(home, "sessions"), join6(home, "archived_sessions")];
|
|
7683
|
+
}
|
|
7684
|
+
async function listJsonl2(dir) {
|
|
7685
|
+
let dirents;
|
|
7686
|
+
try {
|
|
7687
|
+
dirents = await readdir2(dir, { withFileTypes: true });
|
|
7688
|
+
} catch {
|
|
7689
|
+
return [];
|
|
7690
|
+
}
|
|
7691
|
+
const out = [];
|
|
7692
|
+
for (const d of dirents) {
|
|
7693
|
+
const full = join6(dir, d.name);
|
|
7694
|
+
if (d.isDirectory()) out.push(...await listJsonl2(full));
|
|
7695
|
+
else if (d.isFile() && d.name.endsWith(".jsonl")) out.push(full);
|
|
7696
|
+
}
|
|
7697
|
+
return out;
|
|
7698
|
+
}
|
|
7699
|
+
function* splitLines2(content) {
|
|
7700
|
+
let start = 0;
|
|
7701
|
+
for (let i = 0; i < content.length; i++) {
|
|
7702
|
+
if (content[i] === "\n") {
|
|
7703
|
+
yield content.slice(start, i);
|
|
7704
|
+
start = i + 1;
|
|
7705
|
+
}
|
|
7706
|
+
}
|
|
7707
|
+
if (start < content.length) yield content.slice(start);
|
|
7708
|
+
}
|
|
7709
|
+
var NATIVE_READ_BUDGET_MS2 = 45e3;
|
|
7710
|
+
var CODEX_CACHE_VERSION = 7;
|
|
7711
|
+
function toCachedSession(s, kind = "usage") {
|
|
7712
|
+
return [
|
|
7713
|
+
kind,
|
|
7714
|
+
s.date,
|
|
7715
|
+
s.model,
|
|
7716
|
+
s.inputTokens,
|
|
7717
|
+
s.outputTokens,
|
|
7718
|
+
s.cacheCreationTokens,
|
|
7719
|
+
s.cacheReadTokens,
|
|
7720
|
+
s.turnCount
|
|
7721
|
+
];
|
|
7722
|
+
}
|
|
7723
|
+
function fromCachedSession(t) {
|
|
7724
|
+
return {
|
|
7725
|
+
date: t[1],
|
|
7726
|
+
model: t[2],
|
|
7727
|
+
inputTokens: t[3],
|
|
7728
|
+
outputTokens: t[4],
|
|
7729
|
+
cacheCreationTokens: t[5],
|
|
7730
|
+
cacheReadTokens: t[6],
|
|
7731
|
+
turnCount: t[7]
|
|
7732
|
+
};
|
|
7733
|
+
}
|
|
7734
|
+
async function collectCodexNative(env = process.env, opts = {}) {
|
|
7735
|
+
const dirs = resolveCodexSessionsDirs(env);
|
|
7736
|
+
const files = [];
|
|
7737
|
+
const primaryFiles = /* @__PURE__ */ new Set();
|
|
7738
|
+
const seenIdentities = /* @__PURE__ */ new Set();
|
|
7739
|
+
for (const dir of dirs) {
|
|
7740
|
+
for (const file of await listJsonl2(dir)) {
|
|
7741
|
+
const key = basename(file);
|
|
7742
|
+
files.push(file);
|
|
7743
|
+
if (!seenIdentities.has(key)) {
|
|
7744
|
+
seenIdentities.add(key);
|
|
7745
|
+
primaryFiles.add(file);
|
|
7746
|
+
}
|
|
7747
|
+
}
|
|
7748
|
+
}
|
|
7749
|
+
if (files.length === 0) return { entries: [], found: false, filesScanned: 0 };
|
|
7750
|
+
const now = opts.now ?? Date.now;
|
|
7751
|
+
const res = await readFilesWithCache({
|
|
7752
|
+
files,
|
|
7753
|
+
cachePath: opts.cachePath ?? nativeCachePath("codex", env),
|
|
7754
|
+
version: CODEX_CACHE_VERSION,
|
|
7755
|
+
parseFile: (content, path) => {
|
|
7756
|
+
const inspected = inspectCodexRollout(splitLines2(content));
|
|
7757
|
+
const primary = primaryFiles.has(path);
|
|
7758
|
+
return [
|
|
7759
|
+
// Do not pass toCachedSession directly to map: Array.map's numeric
|
|
7760
|
+
// index would be supplied as the optional `kind` argument.
|
|
7761
|
+
...inspected.sessions.map(
|
|
7762
|
+
(session) => toCachedSession(session, primary ? "usage" : "duplicate")
|
|
7763
|
+
),
|
|
7764
|
+
...inspected.replaySessions.map(
|
|
7765
|
+
(session) => toCachedSession(
|
|
7766
|
+
session,
|
|
7767
|
+
primary ? "replay" : "replay-duplicate"
|
|
7768
|
+
)
|
|
7769
|
+
)
|
|
7770
|
+
];
|
|
7771
|
+
},
|
|
7772
|
+
deadline: now() + (opts.budgetMs ?? NATIVE_READ_BUDGET_MS2),
|
|
7773
|
+
now
|
|
7774
|
+
});
|
|
7775
|
+
if (!res.itemsByFile) {
|
|
7776
|
+
return {
|
|
7777
|
+
entries: [],
|
|
7778
|
+
found: false,
|
|
7779
|
+
filesScanned: res.filesRead,
|
|
7780
|
+
timedOut: true
|
|
7781
|
+
};
|
|
7782
|
+
}
|
|
7783
|
+
const acc = /* @__PURE__ */ new Map();
|
|
7784
|
+
const legacyAcc = /* @__PURE__ */ new Map();
|
|
7785
|
+
const replayCandidateDates = /* @__PURE__ */ new Set();
|
|
7786
|
+
for (const items of res.itemsByFile) {
|
|
7787
|
+
const usage = items.filter((item) => item[0] === "usage").map(fromCachedSession);
|
|
7788
|
+
const legacy = items.map(fromCachedSession);
|
|
7789
|
+
const replay = items.filter(
|
|
7790
|
+
(item) => item[0] === "replay" || item[0] === "replay-duplicate"
|
|
7791
|
+
).map(fromCachedSession);
|
|
7792
|
+
foldCodexSessions(acc, usage);
|
|
7793
|
+
foldCodexSessions(legacyAcc, legacy);
|
|
7794
|
+
for (const session of replay) replayCandidateDates.add(session.date);
|
|
7795
|
+
}
|
|
7796
|
+
return {
|
|
7797
|
+
entries: finalizeCodexEntries(acc),
|
|
7798
|
+
found: true,
|
|
7799
|
+
filesScanned: res.filesRead,
|
|
7800
|
+
replayCandidateDates: [...replayCandidateDates].sort(),
|
|
7801
|
+
legacyEntries: finalizeCodexEntries(legacyAcc)
|
|
7802
|
+
};
|
|
7803
|
+
}
|
|
7804
|
+
|
|
7805
|
+
// src/attribution.ts
|
|
7806
|
+
var MAX_FILES = 5e3;
|
|
7807
|
+
var TIME_BUDGET_MS = NATIVE_READ_BUDGET_MS;
|
|
7808
|
+
var ATTRIBUTION_CACHE_VERSION = 1;
|
|
7809
|
+
var MAX_STATS = 300;
|
|
7810
|
+
function createAccumulator() {
|
|
7811
|
+
return {
|
|
7812
|
+
tools: /* @__PURE__ */ new Map(),
|
|
7813
|
+
skills: /* @__PURE__ */ new Map(),
|
|
7814
|
+
agent: {
|
|
7815
|
+
messageCount: 0,
|
|
7816
|
+
subagentMessages: 0,
|
|
7817
|
+
subagentTokens: 0,
|
|
7818
|
+
totalTokens: 0,
|
|
7819
|
+
userMessageCount: 0
|
|
7820
|
+
},
|
|
7821
|
+
sessionMessages: /* @__PURE__ */ new Map()
|
|
7822
|
+
};
|
|
7823
|
+
}
|
|
7824
|
+
function createFileContext() {
|
|
7825
|
+
return { toolNames: /* @__PURE__ */ new Map() };
|
|
7826
|
+
}
|
|
7827
|
+
function recordTokens(usage) {
|
|
7828
|
+
if (!usage || typeof usage !== "object") return 0;
|
|
7829
|
+
const u = usage;
|
|
7830
|
+
const n = (v) => {
|
|
7831
|
+
const x = Math.round(Number(v));
|
|
7832
|
+
return Number.isFinite(x) && x > 0 ? x : 0;
|
|
7833
|
+
};
|
|
7834
|
+
return n(u.input_tokens) + n(u.output_tokens) + n(u.cache_creation_input_tokens) + n(u.cache_read_input_tokens);
|
|
7835
|
+
}
|
|
7836
|
+
function hasHumanText(content) {
|
|
7837
|
+
const isHuman = (t) => {
|
|
7838
|
+
const s = t.trim();
|
|
7839
|
+
return s.length > 0 && !s.startsWith("<system-reminder") && !s.startsWith("<command-") && !s.startsWith("Caveat:");
|
|
7840
|
+
};
|
|
7841
|
+
if (typeof content === "string") return isHuman(content);
|
|
7842
|
+
if (!Array.isArray(content)) return false;
|
|
7843
|
+
return content.some(
|
|
7844
|
+
(b) => b !== null && typeof b === "object" && b.type === "text" && typeof b.text === "string" && isHuman(b.text)
|
|
7845
|
+
);
|
|
7846
|
+
}
|
|
7847
|
+
function processRecord(rec, acc, ctx) {
|
|
7848
|
+
if (!rec || typeof rec !== "object") return;
|
|
7849
|
+
const r = rec;
|
|
7850
|
+
const recTokens = recordTokens(r.message?.usage);
|
|
7851
|
+
if ((r.type === "user" || r.message?.role === "user") && r.isSidechain !== true && r.isMeta !== true && hasHumanText(r.message?.content)) {
|
|
7852
|
+
acc.agent.userMessageCount += 1;
|
|
7853
|
+
}
|
|
7854
|
+
if (typeof r.attributionSkill === "string" && r.attributionSkill) {
|
|
7855
|
+
const s = r.attributionSkill.slice(0, 128);
|
|
7856
|
+
const sk = acc.skills.get(s) ?? { count: 0, tokens: 0 };
|
|
7857
|
+
sk.count += 1;
|
|
7858
|
+
sk.tokens += recTokens;
|
|
7859
|
+
acc.skills.set(s, sk);
|
|
7860
|
+
}
|
|
7861
|
+
const content = r.message?.content;
|
|
7862
|
+
if (!Array.isArray(content)) return;
|
|
7863
|
+
const isAssistant = r.type === "assistant" || r.message?.role === "assistant";
|
|
7864
|
+
const isUser = r.type === "user" || r.message?.role === "user";
|
|
7865
|
+
if (isAssistant) {
|
|
7866
|
+
const toolUses = [];
|
|
7867
|
+
for (const block of content) {
|
|
7868
|
+
if (block && typeof block === "object" && block.type === "tool_use" && typeof block.name === "string") {
|
|
7869
|
+
const name = block.name.slice(0, 128);
|
|
7870
|
+
if (!name) continue;
|
|
7871
|
+
const id = block.id;
|
|
7872
|
+
toolUses.push({ name, id: typeof id === "string" ? id : void 0 });
|
|
7873
|
+
}
|
|
7874
|
+
}
|
|
7875
|
+
const perToolTokens = toolUses.length > 0 ? Math.floor(recTokens / toolUses.length) : 0;
|
|
7876
|
+
for (const tu of toolUses) {
|
|
7877
|
+
const t = acc.tools.get(tu.name) ?? { count: 0, errors: 0, tokens: 0 };
|
|
7878
|
+
t.count += 1;
|
|
7879
|
+
t.tokens += perToolTokens;
|
|
7880
|
+
acc.tools.set(tu.name, t);
|
|
7881
|
+
if (tu.id) ctx.toolNames.set(tu.id, tu.name);
|
|
7882
|
+
}
|
|
7883
|
+
const tokens = recTokens;
|
|
7884
|
+
acc.agent.messageCount += 1;
|
|
7885
|
+
acc.agent.totalTokens += tokens;
|
|
7886
|
+
const sidechain = r.isSidechain === true;
|
|
7887
|
+
if (sidechain) {
|
|
7888
|
+
acc.agent.subagentMessages += 1;
|
|
7889
|
+
acc.agent.subagentTokens += tokens;
|
|
7890
|
+
}
|
|
7891
|
+
if (typeof r.sessionId === "string" && r.sessionId) {
|
|
7892
|
+
acc.sessionMessages.set(
|
|
7893
|
+
r.sessionId,
|
|
7894
|
+
(acc.sessionMessages.get(r.sessionId) ?? 0) + 1
|
|
7895
|
+
);
|
|
7896
|
+
}
|
|
7897
|
+
} else if (isUser) {
|
|
7898
|
+
for (const block of content) {
|
|
7899
|
+
if (block && typeof block === "object" && block.type === "tool_result" && block.is_error === true) {
|
|
7900
|
+
const id = block.tool_use_id;
|
|
7901
|
+
const name = typeof id === "string" ? ctx.toolNames.get(id) : void 0;
|
|
7902
|
+
if (name) {
|
|
7903
|
+
const t = acc.tools.get(name) ?? { count: 0, errors: 0, tokens: 0 };
|
|
7904
|
+
t.errors += 1;
|
|
7905
|
+
acc.tools.set(name, t);
|
|
7906
|
+
}
|
|
7907
|
+
}
|
|
7908
|
+
}
|
|
7909
|
+
}
|
|
7910
|
+
}
|
|
7911
|
+
function toSkillStats(map) {
|
|
7912
|
+
return [...map.entries()].map(([name, v]) => ({ name, count: v.count, tokens: v.tokens })).filter((s) => s.count > 0).sort((a, b) => b.tokens - a.tokens || b.count - a.count).slice(0, MAX_STATS).map((s) => s.tokens > 0 ? s : { name: s.name, count: s.count });
|
|
7913
|
+
}
|
|
7914
|
+
function toToolStats(map) {
|
|
7915
|
+
return [...map.entries()].map(([name, v]) => ({ name, count: v.count, errors: v.errors, tokens: v.tokens })).filter((s) => s.count > 0).sort((a, b) => b.tokens - a.tokens || b.count - a.count).slice(0, MAX_STATS).map((s) => {
|
|
7916
|
+
const base = { name: s.name, count: s.count };
|
|
7917
|
+
if (s.errors > 0) base.errors = s.errors;
|
|
7918
|
+
if (s.tokens > 0) base.tokens = s.tokens;
|
|
7919
|
+
return base;
|
|
7920
|
+
});
|
|
7921
|
+
}
|
|
7922
|
+
function accumulatorToResult(acc) {
|
|
7923
|
+
return {
|
|
7924
|
+
tools: toToolStats(acc.tools),
|
|
7925
|
+
skills: toSkillStats(acc.skills),
|
|
7926
|
+
agent: { ...acc.agent },
|
|
7927
|
+
sessionMessages: acc.sessionMessages,
|
|
7928
|
+
complete: true
|
|
7929
|
+
};
|
|
7930
|
+
}
|
|
7931
|
+
function numTok(v) {
|
|
7932
|
+
const x = Math.round(Number(v));
|
|
7933
|
+
return Number.isFinite(x) && x > 0 ? x : 0;
|
|
7934
|
+
}
|
|
7935
|
+
function createCodexContext() {
|
|
7936
|
+
return { pending: [] };
|
|
7937
|
+
}
|
|
7938
|
+
function processCodexRecord(rec, acc, ctx) {
|
|
7939
|
+
if (!rec || typeof rec !== "object") return;
|
|
7940
|
+
const r = rec;
|
|
7941
|
+
if (!r.payload || typeof r.payload !== "object") return;
|
|
7942
|
+
const pl = r.payload;
|
|
7943
|
+
const ptype = pl.type;
|
|
7944
|
+
if (r.type === "session_meta" || r.type === "turn_context") return;
|
|
7945
|
+
if (ptype === "function_call" || ptype === "custom_tool_call" || ptype === "local_shell_call") {
|
|
7946
|
+
const raw = typeof pl.name === "string" ? pl.name : ptype === "local_shell_call" ? "local_shell" : "";
|
|
7947
|
+
const name = raw.slice(0, 128);
|
|
7948
|
+
if (!name) return;
|
|
7949
|
+
const id = typeof pl.call_id === "string" ? pl.call_id : void 0;
|
|
7950
|
+
ctx.pending.push({ name, id });
|
|
7951
|
+
const t = acc.tools.get(name) ?? { count: 0, errors: 0, tokens: 0 };
|
|
7952
|
+
t.count += 1;
|
|
7953
|
+
acc.tools.set(name, t);
|
|
7588
7954
|
return;
|
|
7589
7955
|
}
|
|
7590
7956
|
if (ptype === "token_count") {
|
|
@@ -7609,7 +7975,7 @@ function processCodexRecord(rec, acc, ctx) {
|
|
|
7609
7975
|
ctx.pending = [];
|
|
7610
7976
|
}
|
|
7611
7977
|
}
|
|
7612
|
-
function*
|
|
7978
|
+
function* splitLines3(content) {
|
|
7613
7979
|
let start = 0;
|
|
7614
7980
|
for (let i = 0; i < content.length; i++) {
|
|
7615
7981
|
if (content[i] === "\n") {
|
|
@@ -7659,7 +8025,7 @@ function mergeAgg(acc, agg) {
|
|
|
7659
8025
|
function parseClaudeAttrFile(content) {
|
|
7660
8026
|
const acc = createAccumulator();
|
|
7661
8027
|
const ctx = createFileContext();
|
|
7662
|
-
for (const line of
|
|
8028
|
+
for (const line of splitLines3(content)) {
|
|
7663
8029
|
if (!line) continue;
|
|
7664
8030
|
try {
|
|
7665
8031
|
processRecord(JSON.parse(line), acc, ctx);
|
|
@@ -7671,7 +8037,7 @@ function parseClaudeAttrFile(content) {
|
|
|
7671
8037
|
function parseCodexAttrFile(content) {
|
|
7672
8038
|
const acc = createAccumulator();
|
|
7673
8039
|
const ctx = createCodexContext();
|
|
7674
|
-
for (const line of
|
|
8040
|
+
for (const line of splitLines3(content)) {
|
|
7675
8041
|
if (!line) continue;
|
|
7676
8042
|
try {
|
|
7677
8043
|
processCodexRecord(JSON.parse(line), acc, ctx);
|
|
@@ -7680,13 +8046,6 @@ function parseCodexAttrFile(content) {
|
|
|
7680
8046
|
}
|
|
7681
8047
|
return [serializeAcc(acc)];
|
|
7682
8048
|
}
|
|
7683
|
-
function claudeProjectDirs() {
|
|
7684
|
-
const dirs = [CLAUDE_PROJECTS];
|
|
7685
|
-
const cfg = process.env.CLAUDE_CONFIG_DIR;
|
|
7686
|
-
if (cfg) dirs.push(join6(cfg, "projects"));
|
|
7687
|
-
dirs.push(join6(homedir4(), ".config", "claude", "projects"));
|
|
7688
|
-
return [...new Set(dirs)];
|
|
7689
|
-
}
|
|
7690
8049
|
function listTranscripts(dir) {
|
|
7691
8050
|
const out = [];
|
|
7692
8051
|
const walk = (d) => {
|
|
@@ -7699,7 +8058,7 @@ function listTranscripts(dir) {
|
|
|
7699
8058
|
}
|
|
7700
8059
|
for (const e of entries) {
|
|
7701
8060
|
if (out.length >= MAX_FILES) return;
|
|
7702
|
-
const p =
|
|
8061
|
+
const p = join7(d, e.name);
|
|
7703
8062
|
if (e.isDirectory()) walk(p);
|
|
7704
8063
|
else if (e.isFile() && e.name.endsWith(".jsonl")) {
|
|
7705
8064
|
try {
|
|
@@ -7725,7 +8084,7 @@ async function collectAttribution(env = process.env) {
|
|
|
7725
8084
|
};
|
|
7726
8085
|
try {
|
|
7727
8086
|
const claudeFiles = [];
|
|
7728
|
-
for (const dir of
|
|
8087
|
+
for (const dir of resolveClaudeProjectDirs(env)) claudeFiles.push(...listTranscripts(dir));
|
|
7729
8088
|
if (claudeFiles.length > 0) {
|
|
7730
8089
|
const res = await readFilesWithCache({
|
|
7731
8090
|
files: claudeFiles,
|
|
@@ -7736,7 +8095,7 @@ async function collectAttribution(env = process.env) {
|
|
|
7736
8095
|
});
|
|
7737
8096
|
mergeInto(res.itemsByFile);
|
|
7738
8097
|
}
|
|
7739
|
-
const codexFiles = listTranscripts(
|
|
8098
|
+
const codexFiles = resolveCodexSessionsDirs(env).flatMap((dir) => listTranscripts(dir));
|
|
7740
8099
|
if (codexFiles.length > 0) {
|
|
7741
8100
|
const res = await readFilesWithCache({
|
|
7742
8101
|
files: codexFiles,
|
|
@@ -7754,10 +8113,10 @@ async function collectAttribution(env = process.env) {
|
|
|
7754
8113
|
}
|
|
7755
8114
|
|
|
7756
8115
|
// src/continue.ts
|
|
7757
|
-
import { readdir as
|
|
8116
|
+
import { readdir as readdir3 } from "node:fs/promises";
|
|
7758
8117
|
import { homedir as homedir5 } from "node:os";
|
|
7759
|
-
import { join as
|
|
7760
|
-
function
|
|
8118
|
+
import { join as join8 } from "node:path";
|
|
8119
|
+
function num3(n) {
|
|
7761
8120
|
const v = Math.round(Number(n));
|
|
7762
8121
|
return Number.isFinite(v) && v > 0 ? v : 0;
|
|
7763
8122
|
}
|
|
@@ -7785,8 +8144,8 @@ function mapContinueRecords(records) {
|
|
|
7785
8144
|
if (ms === null) continue;
|
|
7786
8145
|
const date = localUsageDate(ms);
|
|
7787
8146
|
if (!date) continue;
|
|
7788
|
-
const inputTokens =
|
|
7789
|
-
const outputTokens =
|
|
8147
|
+
const inputTokens = num3(rec.promptTokens ?? rec.prompt_tokens);
|
|
8148
|
+
const outputTokens = num3(rec.generatedTokens ?? rec.generated_tokens);
|
|
7790
8149
|
if (inputTokens + outputTokens === 0) continue;
|
|
7791
8150
|
const key = `${date}|${model}`;
|
|
7792
8151
|
let b = byKey.get(key);
|
|
@@ -7834,7 +8193,7 @@ function parseContinueJsonl(content) {
|
|
|
7834
8193
|
}
|
|
7835
8194
|
return mapContinueRecords(records);
|
|
7836
8195
|
}
|
|
7837
|
-
async function
|
|
8196
|
+
async function listJsonl3(root, opts) {
|
|
7838
8197
|
const queue = [{ dir: root, depth: 0 }];
|
|
7839
8198
|
let queueIndex = 0;
|
|
7840
8199
|
const files = [];
|
|
@@ -7845,7 +8204,7 @@ async function listJsonl2(root, opts) {
|
|
|
7845
8204
|
const current = queue[queueIndex++];
|
|
7846
8205
|
let dirents;
|
|
7847
8206
|
try {
|
|
7848
|
-
dirents = await
|
|
8207
|
+
dirents = await readdir3(current.dir, { withFileTypes: true });
|
|
7849
8208
|
} catch {
|
|
7850
8209
|
continue;
|
|
7851
8210
|
}
|
|
@@ -7854,7 +8213,7 @@ async function listJsonl2(root, opts) {
|
|
|
7854
8213
|
if (visitedEntries > maxEntries || opts.now() > opts.deadline) {
|
|
7855
8214
|
return { files: [], aborted: true };
|
|
7856
8215
|
}
|
|
7857
|
-
const full =
|
|
8216
|
+
const full = join8(current.dir, d.name);
|
|
7858
8217
|
if (d.isDirectory()) {
|
|
7859
8218
|
if (current.depth >= 32) return { files: [], aborted: true };
|
|
7860
8219
|
queue.push({ dir: full, depth: current.depth + 1 });
|
|
@@ -7869,10 +8228,10 @@ async function listJsonl2(root, opts) {
|
|
|
7869
8228
|
var CONTINUE_CACHE_VERSION = 2;
|
|
7870
8229
|
async function collectContinue(opts = {}) {
|
|
7871
8230
|
const env = opts.env ?? process.env;
|
|
7872
|
-
const home = opts.continueDir ??
|
|
8231
|
+
const home = opts.continueDir ?? join8(env.HOME || homedir5(), ".continue");
|
|
7873
8232
|
const now = opts.now ?? Date.now;
|
|
7874
8233
|
const deadline = now() + (opts.budgetMs ?? NATIVE_READ_BUDGET_MS);
|
|
7875
|
-
const discovery = await
|
|
8234
|
+
const discovery = await listJsonl3(join8(home, "dev_data"), {
|
|
7876
8235
|
deadline,
|
|
7877
8236
|
now,
|
|
7878
8237
|
maxFiles: Math.max(1, opts.maxFiles ?? 1e4)
|
|
@@ -7936,14 +8295,14 @@ import { spawnSync as spawnSync4 } from "node:child_process";
|
|
|
7936
8295
|
import { existsSync as existsSync4, realpathSync, statSync as statSync3 } from "node:fs";
|
|
7937
8296
|
import { createRequire as createRequire3 } from "node:module";
|
|
7938
8297
|
import { homedir as homedir6, platform as platform2 } from "node:os";
|
|
7939
|
-
import { join as
|
|
8298
|
+
import { join as join10 } from "node:path";
|
|
7940
8299
|
|
|
7941
8300
|
// src/tokscale.ts
|
|
7942
8301
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
7943
8302
|
import { createRequire as createRequire2 } from "node:module";
|
|
7944
|
-
import { dirname as dirname4, join as
|
|
8303
|
+
import { dirname as dirname4, join as join9 } from "node:path";
|
|
7945
8304
|
var LOOKBACK_DAYS = 30;
|
|
7946
|
-
function
|
|
8305
|
+
function num4(n) {
|
|
7947
8306
|
const v = Math.round(Number(n));
|
|
7948
8307
|
return Number.isFinite(v) && v > 0 ? v : 0;
|
|
7949
8308
|
}
|
|
@@ -7956,10 +8315,10 @@ function mapTokscaleDay(date, json) {
|
|
|
7956
8315
|
if (!Array.isArray(entries)) return [];
|
|
7957
8316
|
const out = [];
|
|
7958
8317
|
for (const e of entries) {
|
|
7959
|
-
const inputTokens =
|
|
7960
|
-
const outputTokens =
|
|
7961
|
-
const cacheCreationTokens =
|
|
7962
|
-
const cacheReadTokens =
|
|
8318
|
+
const inputTokens = num4(e.input);
|
|
8319
|
+
const outputTokens = num4(e.output) + num4(e.reasoning);
|
|
8320
|
+
const cacheCreationTokens = num4(e.cacheWrite);
|
|
8321
|
+
const cacheReadTokens = num4(e.cacheRead);
|
|
7963
8322
|
const costUSD2 = numCost(e.cost);
|
|
7964
8323
|
const total = inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens;
|
|
7965
8324
|
if (total === 0 && costUSD2 === 0) continue;
|
|
@@ -7985,7 +8344,7 @@ function resolveTokscaleBin() {
|
|
|
7985
8344
|
const pkg = require4("tokscale/package.json");
|
|
7986
8345
|
const rel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.tokscale ?? "";
|
|
7987
8346
|
if (!rel) return null;
|
|
7988
|
-
const binPath =
|
|
8347
|
+
const binPath = join9(dirname4(pkgPath), rel);
|
|
7989
8348
|
if (/\.(c|m)?js$/.test(binPath)) {
|
|
7990
8349
|
return { cmd: process.execPath, prefixArgs: [binPath] };
|
|
7991
8350
|
}
|
|
@@ -8046,7 +8405,7 @@ var MAX_CURSOR_PAGE_BYTES = 4 * 1024 * 1024;
|
|
|
8046
8405
|
function cursorDbPath() {
|
|
8047
8406
|
const home = homedir6();
|
|
8048
8407
|
const os = platform2();
|
|
8049
|
-
const p = os === "darwin" ?
|
|
8408
|
+
const p = os === "darwin" ? join10(home, "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb") : os === "win32" ? join10(process.env.APPDATA ?? join10(home, "AppData", "Roaming"), "Cursor", "User", "globalStorage", "state.vscdb") : join10(process.env.XDG_CONFIG_HOME ?? join10(home, ".config"), "Cursor", "User", "globalStorage", "state.vscdb");
|
|
8050
8409
|
return existsSync4(p) ? p : null;
|
|
8051
8410
|
}
|
|
8052
8411
|
function trustedSqliteBinaries() {
|
|
@@ -8099,7 +8458,7 @@ function cursorCookie(token) {
|
|
|
8099
8458
|
return null;
|
|
8100
8459
|
}
|
|
8101
8460
|
}
|
|
8102
|
-
function
|
|
8461
|
+
function num5(n) {
|
|
8103
8462
|
const v = Math.round(Number(n));
|
|
8104
8463
|
return Number.isFinite(v) && v > 0 ? v : 0;
|
|
8105
8464
|
}
|
|
@@ -8114,10 +8473,10 @@ function mapCursorEvents(events) {
|
|
|
8114
8473
|
const date = localUsageDate(ms);
|
|
8115
8474
|
if (!date) continue;
|
|
8116
8475
|
const model = e.model || "cursor";
|
|
8117
|
-
const input =
|
|
8118
|
-
const output =
|
|
8119
|
-
const cacheWrite =
|
|
8120
|
-
const cacheRead =
|
|
8476
|
+
const input = num5(tu.inputTokens);
|
|
8477
|
+
const output = num5(tu.outputTokens);
|
|
8478
|
+
const cacheWrite = num5(tu.cacheWriteTokens);
|
|
8479
|
+
const cacheRead = num5(tu.cacheReadTokens);
|
|
8121
8480
|
const cost = Math.max(0, (Number(tu.totalCents) || 0) / 100);
|
|
8122
8481
|
const total = input + output + cacheWrite + cacheRead;
|
|
8123
8482
|
if (total === 0 && cost === 0) continue;
|
|
@@ -8130,349 +8489,94 @@ function mapCursorEvents(events) {
|
|
|
8130
8489
|
outputTokens: 0,
|
|
8131
8490
|
cacheCreationTokens: 0,
|
|
8132
8491
|
cacheReadTokens: 0,
|
|
8133
|
-
costUSD: 0,
|
|
8134
|
-
origin: "cli",
|
|
8135
|
-
verified: false
|
|
8136
|
-
};
|
|
8137
|
-
day.inputTokens += input;
|
|
8138
|
-
day.outputTokens += output;
|
|
8139
|
-
day.cacheCreationTokens += cacheWrite;
|
|
8140
|
-
day.cacheReadTokens += cacheRead;
|
|
8141
|
-
day.costUSD += cost;
|
|
8142
|
-
byDay.set(key, day);
|
|
8143
|
-
const hour = new Date(
|
|
8144
|
-
Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), d.getUTCHours())
|
|
8145
|
-
).toISOString();
|
|
8146
|
-
const blk = byHour.get(hour) ?? { startTime: hour, totalTokens: 0, costUSD: 0 };
|
|
8147
|
-
blk.totalTokens += total;
|
|
8148
|
-
blk.costUSD += cost;
|
|
8149
|
-
byHour.set(hour, blk);
|
|
8150
|
-
}
|
|
8151
|
-
const entries = [...byDay.values()].map((e) => ({
|
|
8152
|
-
...e,
|
|
8153
|
-
costUSD: Number(e.costUSD.toFixed(4))
|
|
8154
|
-
}));
|
|
8155
|
-
const blocks = [...byHour.values()].map((b) => ({
|
|
8156
|
-
...b,
|
|
8157
|
-
costUSD: Number(b.costUSD.toFixed(4))
|
|
8158
|
-
}));
|
|
8159
|
-
return { entries, blocks };
|
|
8160
|
-
}
|
|
8161
|
-
async function fetchCursorEvents(cookie, maxPages = 30, pageSize = 500) {
|
|
8162
|
-
const all = [];
|
|
8163
|
-
for (let page = 1; page <= maxPages; page++) {
|
|
8164
|
-
const res = await fetch(EVENTS_URL, {
|
|
8165
|
-
method: "POST",
|
|
8166
|
-
headers: {
|
|
8167
|
-
"Content-Type": "application/json",
|
|
8168
|
-
Origin: "https://cursor.com",
|
|
8169
|
-
Cookie: cookie
|
|
8170
|
-
},
|
|
8171
|
-
body: JSON.stringify({ page, pageSize }),
|
|
8172
|
-
signal: AbortSignal.timeout(2e4),
|
|
8173
|
-
redirect: "error"
|
|
8174
|
-
});
|
|
8175
|
-
if (!res.ok) {
|
|
8176
|
-
throw new Error(`cursor usage page ${page} failed (HTTP ${res.status})`);
|
|
8177
|
-
}
|
|
8178
|
-
const body = await readJsonResponseCapped(
|
|
8179
|
-
res,
|
|
8180
|
-
MAX_CURSOR_PAGE_BYTES
|
|
8181
|
-
);
|
|
8182
|
-
const batch = body.usageEventsDisplay ?? [];
|
|
8183
|
-
all.push(...batch);
|
|
8184
|
-
if (batch.length < pageSize) break;
|
|
8185
|
-
}
|
|
8186
|
-
return all;
|
|
8187
|
-
}
|
|
8188
|
-
async function collectCursor(opts) {
|
|
8189
|
-
try {
|
|
8190
|
-
const db = cursorDbPath();
|
|
8191
|
-
const token = db ? readCursorToken(db) : null;
|
|
8192
|
-
const cookie = token ? cursorCookie(token) : null;
|
|
8193
|
-
if (cookie && !opts?.offline) {
|
|
8194
|
-
const events = await fetchCursorEvents(cookie);
|
|
8195
|
-
const { entries, blocks } = mapCursorEvents(events);
|
|
8196
|
-
if (entries.length > 0) return { entries, blocks, found: true };
|
|
8197
|
-
}
|
|
8198
|
-
} catch {
|
|
8199
|
-
}
|
|
8200
|
-
try {
|
|
8201
|
-
const entries = collectCursorViaTokscale();
|
|
8202
|
-
if (entries.length > 0) return { entries, blocks: [], found: true };
|
|
8203
|
-
} catch {
|
|
8204
|
-
}
|
|
8205
|
-
return { entries: [], blocks: [], found: false };
|
|
8206
|
-
}
|
|
8207
|
-
|
|
8208
|
-
// src/native/codex.ts
|
|
8209
|
-
import { readdir as readdir3 } from "node:fs/promises";
|
|
8210
|
-
import { homedir as homedir7 } from "node:os";
|
|
8211
|
-
import { basename, join as join10, resolve as resolve2 } from "node:path";
|
|
8212
|
-
function num5(n) {
|
|
8213
|
-
const v = Math.round(Number(n));
|
|
8214
|
-
return Number.isFinite(v) && v > 0 ? v : 0;
|
|
8215
|
-
}
|
|
8216
|
-
function readTokenFields(payload) {
|
|
8217
|
-
const info = payload.info;
|
|
8218
|
-
const src = info?.total_token_usage ?? payload;
|
|
8219
|
-
const input = src.input_tokens;
|
|
8220
|
-
const output = src.output_tokens;
|
|
8221
|
-
if (input === void 0 && output === void 0) return null;
|
|
8222
|
-
return {
|
|
8223
|
-
input: num5(input),
|
|
8224
|
-
cached: num5(src.cached_input_tokens),
|
|
8225
|
-
output: num5(output)
|
|
8226
|
-
};
|
|
8227
|
-
}
|
|
8228
|
-
function inspectCodexRollout(lines) {
|
|
8229
|
-
let model = "unknown";
|
|
8230
|
-
let sawSessionMeta = false;
|
|
8231
|
-
let replayedRollout = false;
|
|
8232
|
-
const perDay = /* @__PURE__ */ new Map();
|
|
8233
|
-
let lastSeenDate = null;
|
|
8234
|
-
for (const raw of lines) {
|
|
8235
|
-
const trimmed = raw.trim();
|
|
8236
|
-
if (!trimmed) continue;
|
|
8237
|
-
let obj;
|
|
8238
|
-
try {
|
|
8239
|
-
obj = JSON.parse(trimmed);
|
|
8240
|
-
} catch {
|
|
8241
|
-
continue;
|
|
8242
|
-
}
|
|
8243
|
-
const payload = obj.payload;
|
|
8244
|
-
if (!payload || typeof payload !== "object") continue;
|
|
8245
|
-
const kind = obj.type;
|
|
8246
|
-
if (kind === "session_meta" && !sawSessionMeta) {
|
|
8247
|
-
sawSessionMeta = true;
|
|
8248
|
-
const source = payload.source;
|
|
8249
|
-
if (typeof payload.forked_from_id === "string" || typeof payload.parent_thread_id === "string" || source && typeof source === "object" && source.subagent !== void 0) {
|
|
8250
|
-
replayedRollout = true;
|
|
8251
|
-
}
|
|
8252
|
-
}
|
|
8253
|
-
if (kind === "session_meta" || kind === "turn_context") {
|
|
8254
|
-
if (typeof payload.model === "string" && payload.model) model = payload.model;
|
|
8255
|
-
}
|
|
8256
|
-
if (payload.type === "token_count") {
|
|
8257
|
-
const fields = readTokenFields(payload);
|
|
8258
|
-
if (!fields) continue;
|
|
8259
|
-
const parsed = localUsageDate(Date.parse(String(obj.timestamp ?? "")));
|
|
8260
|
-
const day = parsed ?? lastSeenDate;
|
|
8261
|
-
if (!day) continue;
|
|
8262
|
-
lastSeenDate = day;
|
|
8263
|
-
const e = perDay.get(day);
|
|
8264
|
-
if (e) {
|
|
8265
|
-
e.cum = fields;
|
|
8266
|
-
e.turns += 1;
|
|
8267
|
-
} else {
|
|
8268
|
-
perDay.set(day, { cum: fields, turns: 1 });
|
|
8269
|
-
}
|
|
8270
|
-
}
|
|
8271
|
-
}
|
|
8272
|
-
if (perDay.size === 0)
|
|
8273
|
-
return { sessions: [], replaySessions: [], replayCandidateDates: [] };
|
|
8274
|
-
const dates = [...perDay.keys()].sort();
|
|
8275
|
-
const out = [];
|
|
8276
|
-
let prev = { input: 0, cached: 0, output: 0 };
|
|
8277
|
-
for (const date of dates) {
|
|
8278
|
-
const { cum, turns } = perDay.get(date);
|
|
8279
|
-
const dInput = Math.max(0, cum.input - prev.input);
|
|
8280
|
-
const dCached = Math.max(0, cum.cached - prev.cached);
|
|
8281
|
-
const dOutput = Math.max(0, cum.output - prev.output);
|
|
8282
|
-
prev = cum;
|
|
8283
|
-
const cacheReadTokens = Math.min(dCached, dInput);
|
|
8284
|
-
const inputTokens = dInput - cacheReadTokens;
|
|
8285
|
-
const outputTokens = dOutput;
|
|
8286
|
-
if (inputTokens + outputTokens + cacheReadTokens === 0) continue;
|
|
8287
|
-
out.push({
|
|
8288
|
-
date,
|
|
8289
|
-
model,
|
|
8290
|
-
inputTokens,
|
|
8291
|
-
outputTokens,
|
|
8292
|
-
cacheCreationTokens: 0,
|
|
8293
|
-
cacheReadTokens,
|
|
8294
|
-
turnCount: turns
|
|
8295
|
-
});
|
|
8296
|
-
}
|
|
8297
|
-
return {
|
|
8298
|
-
sessions: replayedRollout ? [] : out,
|
|
8299
|
-
replaySessions: replayedRollout ? out : [],
|
|
8300
|
-
replayCandidateDates: replayedRollout ? dates : []
|
|
8301
|
-
};
|
|
8302
|
-
}
|
|
8303
|
-
function foldCodexSessions(acc, sessions) {
|
|
8304
|
-
for (const s of sessions) {
|
|
8305
|
-
const k = `${s.date}|${s.model}`;
|
|
8306
|
-
let b = acc.get(k);
|
|
8307
|
-
if (!b) {
|
|
8308
|
-
b = {
|
|
8309
|
-
date: s.date,
|
|
8310
|
-
model: s.model,
|
|
8311
|
-
inputTokens: 0,
|
|
8312
|
-
outputTokens: 0,
|
|
8313
|
-
cacheCreationTokens: 0,
|
|
8314
|
-
cacheReadTokens: 0,
|
|
8315
|
-
requestCount: 0
|
|
8316
|
-
};
|
|
8317
|
-
acc.set(k, b);
|
|
8318
|
-
}
|
|
8319
|
-
b.inputTokens += s.inputTokens;
|
|
8320
|
-
b.outputTokens += s.outputTokens;
|
|
8321
|
-
b.cacheCreationTokens += s.cacheCreationTokens;
|
|
8322
|
-
b.cacheReadTokens += s.cacheReadTokens;
|
|
8323
|
-
b.requestCount += s.turnCount;
|
|
8324
|
-
}
|
|
8325
|
-
}
|
|
8326
|
-
function finalizeCodexEntries(acc) {
|
|
8327
|
-
const entries = [];
|
|
8328
|
-
for (const b of acc.values()) {
|
|
8329
|
-
entries.push({
|
|
8330
|
-
date: b.date,
|
|
8331
|
-
tool: "codex",
|
|
8332
|
-
model: b.model,
|
|
8333
|
-
inputTokens: b.inputTokens,
|
|
8334
|
-
outputTokens: b.outputTokens,
|
|
8335
|
-
cacheCreationTokens: b.cacheCreationTokens,
|
|
8336
|
-
cacheReadTokens: b.cacheReadTokens,
|
|
8337
|
-
costUSD: estimateCostUSD(b.model, b),
|
|
8492
|
+
costUSD: 0,
|
|
8338
8493
|
origin: "cli",
|
|
8339
|
-
verified: false
|
|
8340
|
-
|
|
8341
|
-
|
|
8342
|
-
|
|
8343
|
-
|
|
8344
|
-
|
|
8345
|
-
|
|
8346
|
-
|
|
8347
|
-
|
|
8348
|
-
|
|
8349
|
-
|
|
8350
|
-
|
|
8351
|
-
|
|
8352
|
-
|
|
8353
|
-
|
|
8354
|
-
try {
|
|
8355
|
-
dirents = await readdir3(dir, { withFileTypes: true });
|
|
8356
|
-
} catch {
|
|
8357
|
-
return [];
|
|
8358
|
-
}
|
|
8359
|
-
const out = [];
|
|
8360
|
-
for (const d of dirents) {
|
|
8361
|
-
const full = join10(dir, d.name);
|
|
8362
|
-
if (d.isDirectory()) out.push(...await listJsonl3(full));
|
|
8363
|
-
else if (d.isFile() && d.name.endsWith(".jsonl")) out.push(full);
|
|
8494
|
+
verified: false
|
|
8495
|
+
};
|
|
8496
|
+
day.inputTokens += input;
|
|
8497
|
+
day.outputTokens += output;
|
|
8498
|
+
day.cacheCreationTokens += cacheWrite;
|
|
8499
|
+
day.cacheReadTokens += cacheRead;
|
|
8500
|
+
day.costUSD += cost;
|
|
8501
|
+
byDay.set(key, day);
|
|
8502
|
+
const hour = new Date(
|
|
8503
|
+
Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), d.getUTCHours())
|
|
8504
|
+
).toISOString();
|
|
8505
|
+
const blk = byHour.get(hour) ?? { startTime: hour, totalTokens: 0, costUSD: 0 };
|
|
8506
|
+
blk.totalTokens += total;
|
|
8507
|
+
blk.costUSD += cost;
|
|
8508
|
+
byHour.set(hour, blk);
|
|
8364
8509
|
}
|
|
8365
|
-
|
|
8510
|
+
const entries = [...byDay.values()].map((e) => ({
|
|
8511
|
+
...e,
|
|
8512
|
+
costUSD: Number(e.costUSD.toFixed(4))
|
|
8513
|
+
}));
|
|
8514
|
+
const blocks = [...byHour.values()].map((b) => ({
|
|
8515
|
+
...b,
|
|
8516
|
+
costUSD: Number(b.costUSD.toFixed(4))
|
|
8517
|
+
}));
|
|
8518
|
+
return { entries, blocks };
|
|
8366
8519
|
}
|
|
8367
|
-
function
|
|
8368
|
-
|
|
8369
|
-
|
|
8370
|
-
|
|
8371
|
-
|
|
8372
|
-
|
|
8520
|
+
async function fetchCursorEvents(cookie, maxPages = 30, pageSize = 500) {
|
|
8521
|
+
const all = [];
|
|
8522
|
+
let reachedEnd = false;
|
|
8523
|
+
for (let page = 1; page <= maxPages; page++) {
|
|
8524
|
+
const res = await fetch(EVENTS_URL, {
|
|
8525
|
+
method: "POST",
|
|
8526
|
+
headers: {
|
|
8527
|
+
"Content-Type": "application/json",
|
|
8528
|
+
Origin: "https://cursor.com",
|
|
8529
|
+
Cookie: cookie
|
|
8530
|
+
},
|
|
8531
|
+
body: JSON.stringify({ page, pageSize }),
|
|
8532
|
+
signal: AbortSignal.timeout(2e4),
|
|
8533
|
+
redirect: "error"
|
|
8534
|
+
});
|
|
8535
|
+
if (!res.ok) {
|
|
8536
|
+
throw new Error(`cursor usage page ${page} failed (HTTP ${res.status})`);
|
|
8537
|
+
}
|
|
8538
|
+
const body = await readJsonResponseCapped(
|
|
8539
|
+
res,
|
|
8540
|
+
MAX_CURSOR_PAGE_BYTES
|
|
8541
|
+
);
|
|
8542
|
+
const batch = body.usageEventsDisplay ?? [];
|
|
8543
|
+
all.push(...batch);
|
|
8544
|
+
if (batch.length < pageSize) {
|
|
8545
|
+
reachedEnd = true;
|
|
8546
|
+
break;
|
|
8373
8547
|
}
|
|
8374
8548
|
}
|
|
8375
|
-
|
|
8376
|
-
}
|
|
8377
|
-
var NATIVE_READ_BUDGET_MS2 = 45e3;
|
|
8378
|
-
var CODEX_CACHE_VERSION = 7;
|
|
8379
|
-
function toCachedSession(s, kind = "usage") {
|
|
8380
|
-
return [
|
|
8381
|
-
kind,
|
|
8382
|
-
s.date,
|
|
8383
|
-
s.model,
|
|
8384
|
-
s.inputTokens,
|
|
8385
|
-
s.outputTokens,
|
|
8386
|
-
s.cacheCreationTokens,
|
|
8387
|
-
s.cacheReadTokens,
|
|
8388
|
-
s.turnCount
|
|
8389
|
-
];
|
|
8549
|
+
return reachedEnd ? all : dropBoundaryDay(all);
|
|
8390
8550
|
}
|
|
8391
|
-
function
|
|
8392
|
-
|
|
8393
|
-
|
|
8394
|
-
|
|
8395
|
-
|
|
8396
|
-
outputTokens: t[4],
|
|
8397
|
-
cacheCreationTokens: t[5],
|
|
8398
|
-
cacheReadTokens: t[6],
|
|
8399
|
-
turnCount: t[7]
|
|
8400
|
-
};
|
|
8551
|
+
function dropBoundaryDay(events) {
|
|
8552
|
+
const dayOf = (e) => localUsageDate(Number(e.timestamp));
|
|
8553
|
+
const boundary = events.length > 0 ? dayOf(events[events.length - 1]) : null;
|
|
8554
|
+
if (!boundary) return events;
|
|
8555
|
+
return events.filter((e) => dayOf(e) !== boundary);
|
|
8401
8556
|
}
|
|
8402
|
-
async function
|
|
8403
|
-
|
|
8404
|
-
|
|
8405
|
-
|
|
8406
|
-
|
|
8407
|
-
|
|
8408
|
-
|
|
8409
|
-
const
|
|
8410
|
-
|
|
8411
|
-
if (!seenIdentities.has(key)) {
|
|
8412
|
-
seenIdentities.add(key);
|
|
8413
|
-
primaryFiles.add(file);
|
|
8414
|
-
}
|
|
8557
|
+
async function collectCursor(opts) {
|
|
8558
|
+
try {
|
|
8559
|
+
const db = cursorDbPath();
|
|
8560
|
+
const token = db ? readCursorToken(db) : null;
|
|
8561
|
+
const cookie = token ? cursorCookie(token) : null;
|
|
8562
|
+
if (cookie && !opts?.offline) {
|
|
8563
|
+
const events = await fetchCursorEvents(cookie);
|
|
8564
|
+
const { entries, blocks } = mapCursorEvents(events);
|
|
8565
|
+
if (entries.length > 0) return { entries, blocks, found: true };
|
|
8415
8566
|
}
|
|
8567
|
+
} catch {
|
|
8416
8568
|
}
|
|
8417
|
-
|
|
8418
|
-
|
|
8419
|
-
|
|
8420
|
-
|
|
8421
|
-
cachePath: opts.cachePath ?? nativeCachePath("codex", env),
|
|
8422
|
-
version: CODEX_CACHE_VERSION,
|
|
8423
|
-
parseFile: (content, path) => {
|
|
8424
|
-
const inspected = inspectCodexRollout(splitLines3(content));
|
|
8425
|
-
const primary = primaryFiles.has(path);
|
|
8426
|
-
return [
|
|
8427
|
-
// Do not pass toCachedSession directly to map: Array.map's numeric
|
|
8428
|
-
// index would be supplied as the optional `kind` argument.
|
|
8429
|
-
...inspected.sessions.map(
|
|
8430
|
-
(session) => toCachedSession(session, primary ? "usage" : "duplicate")
|
|
8431
|
-
),
|
|
8432
|
-
...inspected.replaySessions.map(
|
|
8433
|
-
(session) => toCachedSession(
|
|
8434
|
-
session,
|
|
8435
|
-
primary ? "replay" : "replay-duplicate"
|
|
8436
|
-
)
|
|
8437
|
-
)
|
|
8438
|
-
];
|
|
8439
|
-
},
|
|
8440
|
-
deadline: now() + (opts.budgetMs ?? NATIVE_READ_BUDGET_MS2),
|
|
8441
|
-
now
|
|
8442
|
-
});
|
|
8443
|
-
if (!res.itemsByFile) {
|
|
8444
|
-
return {
|
|
8445
|
-
entries: [],
|
|
8446
|
-
found: false,
|
|
8447
|
-
filesScanned: res.filesRead,
|
|
8448
|
-
timedOut: true
|
|
8449
|
-
};
|
|
8450
|
-
}
|
|
8451
|
-
const acc = /* @__PURE__ */ new Map();
|
|
8452
|
-
const legacyAcc = /* @__PURE__ */ new Map();
|
|
8453
|
-
const replayCandidateDates = /* @__PURE__ */ new Set();
|
|
8454
|
-
for (const items of res.itemsByFile) {
|
|
8455
|
-
const usage = items.filter((item) => item[0] === "usage").map(fromCachedSession);
|
|
8456
|
-
const legacy = items.map(fromCachedSession);
|
|
8457
|
-
const replay = items.filter(
|
|
8458
|
-
(item) => item[0] === "replay" || item[0] === "replay-duplicate"
|
|
8459
|
-
).map(fromCachedSession);
|
|
8460
|
-
foldCodexSessions(acc, usage);
|
|
8461
|
-
foldCodexSessions(legacyAcc, legacy);
|
|
8462
|
-
for (const session of replay) replayCandidateDates.add(session.date);
|
|
8569
|
+
try {
|
|
8570
|
+
const entries = collectCursorViaTokscale();
|
|
8571
|
+
if (entries.length > 0) return { entries, blocks: [], found: true };
|
|
8572
|
+
} catch {
|
|
8463
8573
|
}
|
|
8464
|
-
return {
|
|
8465
|
-
entries: finalizeCodexEntries(acc),
|
|
8466
|
-
found: true,
|
|
8467
|
-
filesScanned: res.filesRead,
|
|
8468
|
-
replayCandidateDates: [...replayCandidateDates].sort(),
|
|
8469
|
-
legacyEntries: finalizeCodexEntries(legacyAcc)
|
|
8470
|
-
};
|
|
8574
|
+
return { entries: [], blocks: [], found: false };
|
|
8471
8575
|
}
|
|
8472
8576
|
|
|
8473
8577
|
// src/native/vscode-agents.ts
|
|
8474
8578
|
import { readdir as readdir4 } from "node:fs/promises";
|
|
8475
|
-
import { homedir as
|
|
8579
|
+
import { homedir as homedir7, platform as platform3 } from "node:os";
|
|
8476
8580
|
import { join as join11 } from "node:path";
|
|
8477
8581
|
var VSCODE_AGENTS = [
|
|
8478
8582
|
{ tool: "cline", extIds: ["saoudrizwan.claude-dev"] },
|
|
@@ -8586,7 +8690,7 @@ function parseVscodeAgentMessages(tool, raw) {
|
|
|
8586
8690
|
}
|
|
8587
8691
|
function vscodeGlobalStorageRoots(env = process.env) {
|
|
8588
8692
|
const apps = ["Code", "Code - Insiders", "Cursor", "VSCodium", "Windsurf"];
|
|
8589
|
-
const home = env.HOME ||
|
|
8693
|
+
const home = env.HOME || homedir7();
|
|
8590
8694
|
const os = platform3();
|
|
8591
8695
|
const base = (app) => os === "darwin" ? join11(home, "Library", "Application Support", app, "User", "globalStorage") : os === "win32" ? join11(env.APPDATA ?? join11(home, "AppData", "Roaming"), app, "User", "globalStorage") : join11(env.XDG_CONFIG_HOME ?? join11(home, ".config"), app, "User", "globalStorage");
|
|
8592
8696
|
return apps.map(base);
|
|
@@ -8969,7 +9073,12 @@ var SOURCES = [
|
|
|
8969
9073
|
"openclaw",
|
|
8970
9074
|
"hermes",
|
|
8971
9075
|
"pi",
|
|
8972
|
-
"codebuff"
|
|
9076
|
+
"codebuff",
|
|
9077
|
+
// Read by ccusage >= 20.0.29 (Antigravity's conversations/*.db ledgers,
|
|
9078
|
+
// Grok Build CLI's sessions/**/updates.jsonl, ZCode's cli/db/db.sqlite).
|
|
9079
|
+
"antigravity",
|
|
9080
|
+
"grok",
|
|
9081
|
+
"zcode"
|
|
8973
9082
|
];
|
|
8974
9083
|
function norm(n) {
|
|
8975
9084
|
const v = Math.round(Number(n));
|
|
@@ -8979,6 +9088,15 @@ function normCost(n) {
|
|
|
8979
9088
|
const v = Number(n);
|
|
8980
9089
|
return Number.isFinite(v) && v > 0 ? v : 0;
|
|
8981
9090
|
}
|
|
9091
|
+
var PRICE_TABLE_COST_SOURCES = /* @__PURE__ */ new Set(["claude", "codex"]);
|
|
9092
|
+
function repriceCcusageCost(tool, model, tokens, ccusageCostUSD) {
|
|
9093
|
+
if (ccusageCostUSD > 0) return ccusageCostUSD;
|
|
9094
|
+
const known = model.length > 0 && model !== "unknown";
|
|
9095
|
+
if (known && PRICE_TABLE_COST_SOURCES.has(tool) && resolveModelPrice(model)) {
|
|
9096
|
+
return estimateCostUSD(model, tokens);
|
|
9097
|
+
}
|
|
9098
|
+
return 0;
|
|
9099
|
+
}
|
|
8982
9100
|
function mapCcusageDaily(tool, json, now = Date.now()) {
|
|
8983
9101
|
const daily = json?.daily;
|
|
8984
9102
|
if (!Array.isArray(daily)) return [];
|
|
@@ -9036,42 +9154,13 @@ function mapCcusageDaily(tool, json, now = Date.now()) {
|
|
|
9036
9154
|
}
|
|
9037
9155
|
for (const c of candidates) {
|
|
9038
9156
|
const tokens = c.inputTokens + c.outputTokens + c.cacheCreationTokens + c.cacheReadTokens;
|
|
9039
|
-
|
|
9040
|
-
|
|
9157
|
+
const costUSD2 = repriceCcusageCost(tool, c.model, c, c.costUSD);
|
|
9158
|
+
if (tokens === 0 && costUSD2 === 0) continue;
|
|
9159
|
+
entries.push({ date, tool, ...c, costUSD: costUSD2, origin: "cli", verified: false });
|
|
9041
9160
|
}
|
|
9042
9161
|
}
|
|
9043
9162
|
return entries;
|
|
9044
9163
|
}
|
|
9045
|
-
function mapCcusageSessions(json) {
|
|
9046
|
-
const sessions = json?.session;
|
|
9047
|
-
if (!Array.isArray(sessions)) return [];
|
|
9048
|
-
const out = [];
|
|
9049
|
-
for (const raw of sessions) {
|
|
9050
|
-
const s = raw;
|
|
9051
|
-
const sessionId = typeof s.period === "string" ? s.period : typeof s.sessionId === "string" ? s.sessionId : null;
|
|
9052
|
-
if (!sessionId) continue;
|
|
9053
|
-
const inputTokens = norm(s.inputTokens);
|
|
9054
|
-
const outputTokens = norm(s.outputTokens);
|
|
9055
|
-
const cacheCreationTokens = norm(s.cacheCreationTokens);
|
|
9056
|
-
const cacheReadTokens = norm(s.cacheReadTokens);
|
|
9057
|
-
if (inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens === 0)
|
|
9058
|
-
continue;
|
|
9059
|
-
const models = Array.isArray(s.modelsUsed) ? s.modelsUsed.filter((m) => typeof m === "string") : [];
|
|
9060
|
-
const meta = s.metadata ?? {};
|
|
9061
|
-
out.push({
|
|
9062
|
-
sessionId: sessionId.slice(0, 128),
|
|
9063
|
-
tool: typeof s.agent === "string" ? s.agent : "unknown",
|
|
9064
|
-
model: models[0] ?? "unknown",
|
|
9065
|
-
inputTokens,
|
|
9066
|
-
outputTokens,
|
|
9067
|
-
cacheCreationTokens,
|
|
9068
|
-
cacheReadTokens,
|
|
9069
|
-
costUSD: normCost(s.totalCost ?? s.costUSD),
|
|
9070
|
-
lastActivity: typeof meta.lastActivity === "string" ? meta.lastActivity : (/* @__PURE__ */ new Date()).toISOString()
|
|
9071
|
-
});
|
|
9072
|
-
}
|
|
9073
|
-
return out;
|
|
9074
|
-
}
|
|
9075
9164
|
function mapCcusageBlocks(json) {
|
|
9076
9165
|
const blocks = json?.blocks;
|
|
9077
9166
|
if (!Array.isArray(blocks)) return [];
|
|
@@ -9299,14 +9388,9 @@ async function collectAll(onProgress, opts) {
|
|
|
9299
9388
|
return { source, mapped: json ? mapCcusageDaily(source, json) : [] };
|
|
9300
9389
|
}
|
|
9301
9390
|
);
|
|
9302
|
-
const sessionTask =
|
|
9303
|
-
cmd,
|
|
9304
|
-
[...prefixArgs, "session", "--json", "--offline"],
|
|
9305
|
-
void 0,
|
|
9306
|
-
CCUSAGE_AGGREGATE_TIMEOUT_MS
|
|
9307
|
-
).then((json) => {
|
|
9391
|
+
const sessionTask = Promise.resolve().then(() => {
|
|
9308
9392
|
tick();
|
|
9309
|
-
return
|
|
9393
|
+
return [];
|
|
9310
9394
|
});
|
|
9311
9395
|
const blockTask = runCcusage(
|
|
9312
9396
|
cmd,
|
|
@@ -9325,13 +9409,19 @@ async function collectAll(onProgress, opts) {
|
|
|
9325
9409
|
tick();
|
|
9326
9410
|
return a;
|
|
9327
9411
|
});
|
|
9412
|
+
const bailed = () => ({
|
|
9413
|
+
entries: [],
|
|
9414
|
+
found: false,
|
|
9415
|
+
filesScanned: 0,
|
|
9416
|
+
timedOut: true
|
|
9417
|
+
});
|
|
9328
9418
|
const vscodeTasks = VSCODE_AGENTS.map(
|
|
9329
|
-
(a) => collectVscodeAgent({ tool: a.tool, extIds: a.extIds }).then((r) => {
|
|
9419
|
+
(a) => collectVscodeAgent({ tool: a.tool, extIds: a.extIds }).catch(bailed).then((r) => {
|
|
9330
9420
|
tick();
|
|
9331
9421
|
return { tool: a.tool, result: r };
|
|
9332
9422
|
})
|
|
9333
9423
|
);
|
|
9334
|
-
const continueTask = collectContinue().then((r) => {
|
|
9424
|
+
const continueTask = collectContinue().catch(bailed).then((r) => {
|
|
9335
9425
|
tick();
|
|
9336
9426
|
return r;
|
|
9337
9427
|
});
|
|
@@ -9455,26 +9545,30 @@ async function collectAll(onProgress, opts) {
|
|
|
9455
9545
|
tools: sanitizeToolStats(tools),
|
|
9456
9546
|
skills: sanitizeSkillStats(skills),
|
|
9457
9547
|
agent: sanitizeAgentStat(reconciled.agent),
|
|
9458
|
-
attributionComplete: complete
|
|
9548
|
+
attributionComplete: complete,
|
|
9549
|
+
...nativeClaude.permissionDenied ? { unreadableClaudeFiles: nativeClaude.permissionDenied } : {}
|
|
9459
9550
|
};
|
|
9460
9551
|
}
|
|
9461
9552
|
|
|
9462
9553
|
// src/antigravity.ts
|
|
9463
9554
|
import { existsSync as existsSync5 } from "node:fs";
|
|
9464
|
-
import { homedir as
|
|
9555
|
+
import { homedir as homedir8 } from "node:os";
|
|
9465
9556
|
import { join as join15 } from "node:path";
|
|
9466
|
-
function
|
|
9467
|
-
|
|
9557
|
+
function antigravityDataDirs(env = process.env) {
|
|
9558
|
+
const gemini = join15(env.HOME || homedir8(), ".gemini");
|
|
9559
|
+
return ["antigravity", "antigravity-cli", "antigravity-ide"].map(
|
|
9560
|
+
(name) => join15(gemini, name)
|
|
9561
|
+
);
|
|
9468
9562
|
}
|
|
9469
9563
|
function detectAntigravity(env = process.env) {
|
|
9470
|
-
return
|
|
9564
|
+
return antigravityDataDirs(env).some((dir) => existsSync5(dir));
|
|
9471
9565
|
}
|
|
9472
9566
|
function antigravityNoticeLines() {
|
|
9473
9567
|
return [
|
|
9474
|
-
" \u2022 Google Antigravity detected
|
|
9475
|
-
"
|
|
9476
|
-
"
|
|
9477
|
-
" counts
|
|
9568
|
+
" \u2022 Google Antigravity detected. Its usage is read from the local",
|
|
9569
|
+
" conversation databases (~/.gemini/antigravity*/conversations/*.db).",
|
|
9570
|
+
" Older Antigravity versions kept only encrypted logs with no token",
|
|
9571
|
+
" counts; that history can't be counted. Update Antigravity if none shows."
|
|
9478
9572
|
];
|
|
9479
9573
|
}
|
|
9480
9574
|
|
|
@@ -9512,7 +9606,7 @@ function buildStatusReport(s) {
|
|
|
9512
9606
|
lines.push(" whoburnedmore \u2014 background sync status");
|
|
9513
9607
|
lines.push("");
|
|
9514
9608
|
lines.push(
|
|
9515
|
-
s.installed ? ` \u2022 Background agent: installed${s.loaded ? " and loaded" : " but NOT loaded with the scheduler"}` : " \u2022 Background agent: NOT installed \u2014 run `npx whoburnedmore` to set it up"
|
|
9609
|
+
s.installed ? ` \u2022 Background agent: installed${s.loaded ? " and loaded" : " but NOT loaded with the scheduler"}` : s.optedOut ? " \u2022 Background agent: off (turned off with `uninstall-sync`) \u2014 run `npx whoburnedmore install-sync` to turn it back on" : " \u2022 Background agent: NOT installed \u2014 run `npx whoburnedmore` to set it up"
|
|
9516
9610
|
);
|
|
9517
9611
|
if (s.installed && s.drift !== "ok") {
|
|
9518
9612
|
lines.push(
|
|
@@ -9556,7 +9650,8 @@ function agentStatusReport(now = Date.now()) {
|
|
|
9556
9650
|
now,
|
|
9557
9651
|
nodePath,
|
|
9558
9652
|
nodePathStable: !nodePath.includes("/Cellar/"),
|
|
9559
|
-
logPath: syncLogPath()
|
|
9653
|
+
logPath: syncLogPath(),
|
|
9654
|
+
optedOut: cfg?.autoSyncOptOut === true
|
|
9560
9655
|
});
|
|
9561
9656
|
}
|
|
9562
9657
|
|
|
@@ -9616,8 +9711,12 @@ function writeLocalDashboard(dir, html) {
|
|
|
9616
9711
|
function esc(s) {
|
|
9617
9712
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
9618
9713
|
}
|
|
9714
|
+
function localDay(d) {
|
|
9715
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
9716
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
9717
|
+
}
|
|
9619
9718
|
function renderDashboardHtml(entries, generatedAt = /* @__PURE__ */ new Date(), connect) {
|
|
9620
|
-
const today = generatedAt
|
|
9719
|
+
const today = localDay(generatedAt);
|
|
9621
9720
|
const totals = {
|
|
9622
9721
|
tokens: 0,
|
|
9623
9722
|
cost: 0,
|
|
@@ -9653,7 +9752,9 @@ function renderDashboardHtml(entries, generatedAt = /* @__PURE__ */ new Date(),
|
|
|
9653
9752
|
const avgPerDay = activeDays > 0 ? totals.tokens / activeDays : 0;
|
|
9654
9753
|
const days = [];
|
|
9655
9754
|
for (let i = 59; i >= 0; i--) {
|
|
9656
|
-
const
|
|
9755
|
+
const day = new Date(generatedAt);
|
|
9756
|
+
day.setDate(day.getDate() - i);
|
|
9757
|
+
const d = localDay(day);
|
|
9657
9758
|
days.push({ date: d, tokens: byDate.get(d) ?? 0 });
|
|
9658
9759
|
}
|
|
9659
9760
|
const maxDay = Math.max(1, ...days.map((d) => d.tokens));
|
|
@@ -9761,7 +9862,7 @@ function renderDashboardHtml(entries, generatedAt = /* @__PURE__ */ new Date(),
|
|
|
9761
9862
|
<div class="flame">\u{1F525}</div>
|
|
9762
9863
|
<div>
|
|
9763
9864
|
<h1>your local burn</h1>
|
|
9764
|
-
<div class="sub">generated ${esc(generatedAt.
|
|
9865
|
+
<div class="sub">generated ${esc(`${localDay(generatedAt)} ${String(generatedAt.getHours()).padStart(2, "0")}:${String(generatedAt.getMinutes()).padStart(2, "0")}`)} \xB7 nothing left your machine</div>
|
|
9765
9866
|
</div>
|
|
9766
9867
|
</header>
|
|
9767
9868
|
${connectCta}
|
|
@@ -9810,7 +9911,7 @@ ${connectCta}
|
|
|
9810
9911
|
|
|
9811
9912
|
<div class="foot">
|
|
9812
9913
|
Re-run <code>npx whoburnedmore --local</code> to refresh this page.<br>
|
|
9813
|
-
${connect ? "
|
|
9914
|
+
${connect ? "Run the command above to sign in and put this burn on whoburnedmore.com." : "Run <code>npx whoburnedmore</code> to sign in and publish a shareable dashboard."}
|
|
9814
9915
|
</div>
|
|
9815
9916
|
</div>
|
|
9816
9917
|
</body>
|
|
@@ -9830,6 +9931,10 @@ async function publishLocal(payload, deps) {
|
|
|
9830
9931
|
return false;
|
|
9831
9932
|
}
|
|
9832
9933
|
const res = await deps.submit(auth.token, payload);
|
|
9934
|
+
if (!res) {
|
|
9935
|
+
deps.log(" Your sign-in expired and wasn't renewed \u2014 kept local. Run `npx whoburnedmore` to sign in again.");
|
|
9936
|
+
return false;
|
|
9937
|
+
}
|
|
9833
9938
|
deps.log(` Published \u2014 you're on the board: ${sanitizeServerText(res.profileUrl)}`);
|
|
9834
9939
|
if (isTrustedWebUrl(res.profileUrl)) {
|
|
9835
9940
|
deps.openBrowser(res.profileUrl);
|
|
@@ -9907,13 +10012,14 @@ function openBrowser(url) {
|
|
|
9907
10012
|
const [cmd, args] = os === "darwin" ? ["open", [url]] : os === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
9908
10013
|
spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
|
|
9909
10014
|
}
|
|
9910
|
-
async function confirm(question) {
|
|
10015
|
+
async function confirm(question, defaultYes = true) {
|
|
9911
10016
|
if (!process.stdin.isTTY) return false;
|
|
9912
10017
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
9913
|
-
const answer = (await rl.question(`${question} ${pc2.dim("[Y/n]")} `)).trim();
|
|
10018
|
+
const answer = (await rl.question(`${question} ${pc2.dim(defaultYes ? "[Y/n]" : "[y/N]")} `)).trim();
|
|
9914
10019
|
rl.close();
|
|
9915
|
-
return answer
|
|
10020
|
+
return confirmAnswer(answer, defaultYes);
|
|
9916
10021
|
}
|
|
10022
|
+
var confirmDefaultNo = (question) => confirm(question, false);
|
|
9917
10023
|
function showLocalDashboard(payload) {
|
|
9918
10024
|
const dir = defaultConfigDir();
|
|
9919
10025
|
const file = writeLocalDashboard(
|
|
@@ -9924,7 +10030,7 @@ function showLocalDashboard(payload) {
|
|
|
9924
10030
|
console.log();
|
|
9925
10031
|
console.log(` Local dashboard: ${pc2.cyan(fileUrl)}`);
|
|
9926
10032
|
console.log(pc2.dim(" Re-run `npx whoburnedmore --local` to refresh it. Nothing left your machine."));
|
|
9927
|
-
openBrowser(fileUrl);
|
|
10033
|
+
if (process.stdout.isTTY) openBrowser(fileUrl);
|
|
9928
10034
|
}
|
|
9929
10035
|
async function run(flags) {
|
|
9930
10036
|
if (!flags.quiet) {
|
|
@@ -9948,6 +10054,18 @@ async function run(flags) {
|
|
|
9948
10054
|
} finally {
|
|
9949
10055
|
progress.stop();
|
|
9950
10056
|
}
|
|
10057
|
+
const unreadable = collected.unreadableClaudeFiles ?? [];
|
|
10058
|
+
if (unreadable.length > 0 && !flags.quiet) {
|
|
10059
|
+
console.log(
|
|
10060
|
+
pc2.yellow(
|
|
10061
|
+
` \u26A0 Skipped ${unreadable.length} Claude Code transcript${unreadable.length === 1 ? "" : "s"} this account can't read (permission denied), e.g.`
|
|
10062
|
+
)
|
|
10063
|
+
);
|
|
10064
|
+
console.log(pc2.dim(` ${sanitizeServerText(unreadable[0])}`));
|
|
10065
|
+
console.log(
|
|
10066
|
+
pc2.dim(" Their usage isn't counted until you fix the file's owner or permissions (often left by `sudo claude`).")
|
|
10067
|
+
);
|
|
10068
|
+
}
|
|
9951
10069
|
const {
|
|
9952
10070
|
entries,
|
|
9953
10071
|
codexReplayTombstoneDates: codexReplayTombstoneDates2,
|
|
@@ -9993,10 +10111,13 @@ async function run(flags) {
|
|
|
9993
10111
|
showLocalDashboard(payload);
|
|
9994
10112
|
if (!flags.quiet && process.stdin.isTTY) {
|
|
9995
10113
|
await publishLocal(payload, {
|
|
9996
|
-
confirm,
|
|
9997
|
-
|
|
10114
|
+
confirm: confirmDefaultNo,
|
|
10115
|
+
// Stored token, then a silent refresh, then device sign-in — the same
|
|
10116
|
+
// order as every other signed-in command.
|
|
10117
|
+
signIn: () => requireSignedIn("--local"),
|
|
9998
10118
|
submit: async (token, p) => {
|
|
9999
|
-
const result = await
|
|
10119
|
+
const result = await submitWithAuthRecovery(token, p, true);
|
|
10120
|
+
if (!result) return null;
|
|
10000
10121
|
try {
|
|
10001
10122
|
recordSync();
|
|
10002
10123
|
} catch {
|
|
@@ -10088,27 +10209,26 @@ async function ensureSignedIn() {
|
|
|
10088
10209
|
);
|
|
10089
10210
|
return null;
|
|
10090
10211
|
}
|
|
10091
|
-
async function
|
|
10092
|
-
let result;
|
|
10212
|
+
async function submitWithAuthRecovery(token, payload, interactive) {
|
|
10093
10213
|
try {
|
|
10094
|
-
|
|
10214
|
+
return await submitFromBoundDevice(token, payload);
|
|
10095
10215
|
} catch (err) {
|
|
10096
|
-
if (err instanceof UnauthorizedError)
|
|
10097
|
-
|
|
10098
|
-
|
|
10099
|
-
|
|
10100
|
-
|
|
10101
|
-
|
|
10102
|
-
|
|
10103
|
-
|
|
10104
|
-
|
|
10105
|
-
|
|
10106
|
-
|
|
10107
|
-
}
|
|
10108
|
-
} else {
|
|
10109
|
-
throw err;
|
|
10110
|
-
}
|
|
10216
|
+
if (!(err instanceof UnauthorizedError)) throw err;
|
|
10217
|
+
const healed = await refreshCliTokenFromConfig();
|
|
10218
|
+
if (healed) {
|
|
10219
|
+
saveAuth(void 0, { cliToken: healed.token, handle: healed.handle });
|
|
10220
|
+
return await submitFromBoundDevice(healed.token, payload);
|
|
10221
|
+
}
|
|
10222
|
+
clearAuth();
|
|
10223
|
+
if (!interactive) return null;
|
|
10224
|
+
const auth = await ensureSignedIn();
|
|
10225
|
+
if (!auth) return null;
|
|
10226
|
+
return await submitFromBoundDevice(auth.token, payload);
|
|
10111
10227
|
}
|
|
10228
|
+
}
|
|
10229
|
+
async function submitSignedIn(token, payload, flags, interactive) {
|
|
10230
|
+
const result = await submitWithAuthRecovery(token, payload, interactive);
|
|
10231
|
+
if (!result) return;
|
|
10112
10232
|
try {
|
|
10113
10233
|
recordSync();
|
|
10114
10234
|
} catch {
|
|
@@ -10130,7 +10250,7 @@ async function submitSignedIn(token, payload, flags, interactive) {
|
|
|
10130
10250
|
" Verify your usage to get back on \u2014 this sends a detailed per-request breakdown (timestamps + token counts, never your prompts or code)."
|
|
10131
10251
|
)
|
|
10132
10252
|
);
|
|
10133
|
-
if (process.stdin.isTTY && await confirm(" Verify now?")) {
|
|
10253
|
+
if (process.stdin.isTTY && await confirm(" Verify now?", false)) {
|
|
10134
10254
|
await runVerify();
|
|
10135
10255
|
afterSubmitChores(flags);
|
|
10136
10256
|
return;
|
|
@@ -10156,8 +10276,10 @@ async function submitSignedIn(token, payload, flags, interactive) {
|
|
|
10156
10276
|
);
|
|
10157
10277
|
}
|
|
10158
10278
|
if (isTrustedWebUrl(baseUrl)) {
|
|
10159
|
-
|
|
10160
|
-
|
|
10279
|
+
if (process.stdout.isTTY) {
|
|
10280
|
+
console.log(pc2.dim(" Opening your dashboard in your browser\u2026"));
|
|
10281
|
+
openBrowser(baseUrl);
|
|
10282
|
+
}
|
|
10161
10283
|
} else {
|
|
10162
10284
|
console.log(
|
|
10163
10285
|
pc2.dim(" The server returned an unexpected dashboard address, so it was NOT auto-opened. Open it yourself only if you trust it:")
|
|
@@ -10202,13 +10324,16 @@ function afterSubmitChores(flags) {
|
|
|
10202
10324
|
}
|
|
10203
10325
|
return;
|
|
10204
10326
|
}
|
|
10205
|
-
|
|
10206
|
-
|
|
10207
|
-
|
|
10327
|
+
const optedOut = autoSyncOptedOut();
|
|
10328
|
+
if (!optedOut) {
|
|
10329
|
+
try {
|
|
10330
|
+
reconcileAutoSync();
|
|
10331
|
+
} catch {
|
|
10332
|
+
}
|
|
10208
10333
|
}
|
|
10209
10334
|
console.log();
|
|
10210
10335
|
console.log(
|
|
10211
|
-
autoSyncInstalled() ? pc2.dim(" Background sync is on \u2014 your page updates automatically every 15 min (`npx whoburnedmore uninstall-sync` to stop).") : pc2.dim(" Re-run anytime to update your page.")
|
|
10336
|
+
optedOut ? pc2.dim(" Background sync is off. Re-run anytime to update your page, or `npx whoburnedmore install-sync` to turn it back on.") : autoSyncInstalled() ? pc2.dim(" Background sync is on \u2014 your page updates automatically every 15 min (`npx whoburnedmore uninstall-sync` to stop).") : pc2.dim(" Re-run anytime to update your page.")
|
|
10212
10337
|
);
|
|
10213
10338
|
}
|
|
10214
10339
|
async function readServerInstallToken() {
|
|
@@ -10256,6 +10381,7 @@ async function linkServerInstall(token) {
|
|
|
10256
10381
|
quiet: true
|
|
10257
10382
|
});
|
|
10258
10383
|
try {
|
|
10384
|
+
setAutoSyncOptOut(false);
|
|
10259
10385
|
const action = reconcileAutoSync();
|
|
10260
10386
|
if (action === "installed") {
|
|
10261
10387
|
console.log(pc2.dim(" Background sync installed; this machine will refresh every 15 min."));
|
|
@@ -10330,21 +10456,8 @@ async function runVerify() {
|
|
|
10330
10456
|
" This sends a DETAILED per-request breakdown \u2014 timestamps and token counts, still never your prompts, code, or file names \u2014 so we can confirm your usage is real. It's deleted after review."
|
|
10331
10457
|
)
|
|
10332
10458
|
);
|
|
10333
|
-
const
|
|
10334
|
-
|
|
10335
|
-
if (!token) {
|
|
10336
|
-
if (!process.stdout.isTTY) {
|
|
10337
|
-
console.log(
|
|
10338
|
-
pc2.yellow(
|
|
10339
|
-
" Sign in first \u2014 run `npx whoburnedmore verify` in an interactive terminal."
|
|
10340
|
-
)
|
|
10341
|
-
);
|
|
10342
|
-
return;
|
|
10343
|
-
}
|
|
10344
|
-
const auth = await ensureSignedIn();
|
|
10345
|
-
if (!auth) return;
|
|
10346
|
-
token = auth.token;
|
|
10347
|
-
}
|
|
10459
|
+
const auth = await requireSignedIn("verify");
|
|
10460
|
+
if (!auth) return;
|
|
10348
10461
|
if (process.stdin.isTTY) {
|
|
10349
10462
|
const ok = await confirm("\n Send this breakdown to verify your usage?");
|
|
10350
10463
|
if (!ok) {
|
|
@@ -10352,8 +10465,23 @@ async function runVerify() {
|
|
|
10352
10465
|
return;
|
|
10353
10466
|
}
|
|
10354
10467
|
}
|
|
10355
|
-
console.log(pc2.dim(" Reading your local Claude Code logs\u2026"));
|
|
10356
|
-
const { requests, found, timedOut } = await collectClaudeRequests(
|
|
10468
|
+
console.log(pc2.dim(" Reading your local Claude Code logs (large histories can take a few minutes)\u2026"));
|
|
10469
|
+
const { requests, found, timedOut } = await collectClaudeRequests(process.env, {
|
|
10470
|
+
budgetMs: VERIFY_READ_BUDGET_MS
|
|
10471
|
+
});
|
|
10472
|
+
if (timedOut === true && requests.length === 0) {
|
|
10473
|
+
console.log(
|
|
10474
|
+
pc2.yellow(
|
|
10475
|
+
" Your Claude Code logs are too large to finish reading in time, so nothing was sent."
|
|
10476
|
+
)
|
|
10477
|
+
);
|
|
10478
|
+
console.log(
|
|
10479
|
+
pc2.dim(
|
|
10480
|
+
" Try again when the machine is idle, or file a written appeal at whoburnedmore.com/appeal."
|
|
10481
|
+
)
|
|
10482
|
+
);
|
|
10483
|
+
return;
|
|
10484
|
+
}
|
|
10357
10485
|
if (!found || requests.length === 0) {
|
|
10358
10486
|
console.log(
|
|
10359
10487
|
pc2.yellow(" No local Claude Code logs found on this machine to verify.")
|
|
@@ -10374,13 +10502,25 @@ async function runVerify() {
|
|
|
10374
10502
|
` Sending ${records.length} request records for analysis${truncated ? " (most recent, sampled)" : ""}\u2026`
|
|
10375
10503
|
)
|
|
10376
10504
|
);
|
|
10505
|
+
const body = {
|
|
10506
|
+
cliVersion: VERSION,
|
|
10507
|
+
requests: records,
|
|
10508
|
+
...truncated ? { truncated: true } : {}
|
|
10509
|
+
};
|
|
10510
|
+
const verifyWithRefresh = async () => {
|
|
10511
|
+
try {
|
|
10512
|
+
return await verifyUsage(auth.token, body);
|
|
10513
|
+
} catch (err) {
|
|
10514
|
+
if (!(err instanceof UnauthorizedError)) throw err;
|
|
10515
|
+
const healed = await refreshCliTokenFromConfig();
|
|
10516
|
+
if (!healed) throw err;
|
|
10517
|
+
saveAuth(void 0, { cliToken: healed.token, handle: healed.handle });
|
|
10518
|
+
return await verifyUsage(healed.token, body);
|
|
10519
|
+
}
|
|
10520
|
+
};
|
|
10377
10521
|
let result;
|
|
10378
10522
|
try {
|
|
10379
|
-
result = await
|
|
10380
|
-
cliVersion: VERSION,
|
|
10381
|
-
requests: records,
|
|
10382
|
-
...truncated ? { truncated: true } : {}
|
|
10383
|
-
});
|
|
10523
|
+
result = await verifyWithRefresh();
|
|
10384
10524
|
} catch (err) {
|
|
10385
10525
|
if (err instanceof UnauthorizedError) {
|
|
10386
10526
|
clearAuth();
|
|
@@ -10475,7 +10615,7 @@ async function runRemove() {
|
|
|
10475
10615
|
console.log(
|
|
10476
10616
|
pc2.dim(" Your local logs are untouched, and the account itself survives (delete it from your dashboard settings on the web).")
|
|
10477
10617
|
);
|
|
10478
|
-
if (!await
|
|
10618
|
+
if (!await confirmDefaultNo(" Delete your usage data?")) {
|
|
10479
10619
|
console.log(pc2.dim(" Cancelled \u2014 nothing was deleted."));
|
|
10480
10620
|
return;
|
|
10481
10621
|
}
|
|
@@ -10502,6 +10642,7 @@ async function runRemove() {
|
|
|
10502
10642
|
);
|
|
10503
10643
|
console.log(pc2.dim(" Run `npx whoburnedmore` anytime to start fresh."));
|
|
10504
10644
|
}
|
|
10645
|
+
var VERIFY_READ_BUDGET_MS = 18e4;
|
|
10505
10646
|
async function main() {
|
|
10506
10647
|
const major = Number(process.versions.node.split(".")[0]);
|
|
10507
10648
|
if (major < 20) {
|
|
@@ -10511,6 +10652,13 @@ async function main() {
|
|
|
10511
10652
|
}
|
|
10512
10653
|
const args = process.argv.slice(2);
|
|
10513
10654
|
const baseCommand = resolveCommand(args);
|
|
10655
|
+
const problem = argumentProblem(args, baseCommand);
|
|
10656
|
+
if (problem) {
|
|
10657
|
+
console.error(` ${problem}`);
|
|
10658
|
+
console.error(" Nothing was collected or sent. Run `npx whoburnedmore --help` for the options.");
|
|
10659
|
+
process.exitCode = 2;
|
|
10660
|
+
return;
|
|
10661
|
+
}
|
|
10514
10662
|
const command = args.includes("--watch") && (baseCommand === "run" || baseCommand === "sync") ? "daemon" : baseCommand;
|
|
10515
10663
|
const flags = {
|
|
10516
10664
|
dryRun: args.includes("--dry-run"),
|
|
@@ -10561,9 +10709,11 @@ async function main() {
|
|
|
10561
10709
|
break;
|
|
10562
10710
|
case "install-sync":
|
|
10563
10711
|
console.log(` ${installAutoSync()}`);
|
|
10712
|
+
setAutoSyncOptOut(false);
|
|
10564
10713
|
break;
|
|
10565
10714
|
case "uninstall-sync":
|
|
10566
10715
|
console.log(` ${uninstallAutoSync()}`);
|
|
10716
|
+
setAutoSyncOptOut(true);
|
|
10567
10717
|
break;
|
|
10568
10718
|
case "help":
|
|
10569
10719
|
printHelp();
|