whoburnedmore 0.9.2 → 0.9.4
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 +554 -139
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import { spawn } from "node:child_process";
|
|
|
10
10
|
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "node:fs";
|
|
11
11
|
import { createRequire as createRequire4 } from "node:module";
|
|
12
12
|
import { platform as platform3 } from "node:os";
|
|
13
|
-
import { join as
|
|
13
|
+
import { join as join9 } from "node:path";
|
|
14
14
|
import { createInterface } from "node:readline/promises";
|
|
15
15
|
import pc2 from "picocolors";
|
|
16
16
|
|
|
@@ -101,7 +101,10 @@ async function post(path, body) {
|
|
|
101
101
|
res = await fetch(`${apiBase()}${path}`, {
|
|
102
102
|
method: "POST",
|
|
103
103
|
headers: { "Content-Type": "application/json" },
|
|
104
|
-
body: JSON.stringify(body)
|
|
104
|
+
body: JSON.stringify(body),
|
|
105
|
+
// Bound the request so a slow/black-holing/hostile server can't hang the CLI
|
|
106
|
+
// — or the unattended 15-minute background sync — indefinitely.
|
|
107
|
+
signal: AbortSignal.timeout(3e4)
|
|
105
108
|
});
|
|
106
109
|
} catch {
|
|
107
110
|
throw new Error(
|
|
@@ -144,7 +147,8 @@ async function anonRemove(anonKey) {
|
|
|
144
147
|
const res = await fetch(`${apiBase()}/v1/anon`, {
|
|
145
148
|
method: "DELETE",
|
|
146
149
|
headers: { "Content-Type": "application/json" },
|
|
147
|
-
body: JSON.stringify({ anonKey })
|
|
150
|
+
body: JSON.stringify({ anonKey }),
|
|
151
|
+
signal: AbortSignal.timeout(3e4)
|
|
148
152
|
});
|
|
149
153
|
if (res.status !== 200) {
|
|
150
154
|
const b = await res.json().catch(() => ({}));
|
|
@@ -167,8 +171,8 @@ import {
|
|
|
167
171
|
existsSync as existsSync2,
|
|
168
172
|
mkdirSync as mkdirSync2,
|
|
169
173
|
readFileSync as readFileSync2,
|
|
170
|
-
renameSync,
|
|
171
|
-
rmSync,
|
|
174
|
+
renameSync as renameSync2,
|
|
175
|
+
rmSync as rmSync2,
|
|
172
176
|
statSync,
|
|
173
177
|
writeFileSync as writeFileSync2
|
|
174
178
|
} from "node:fs";
|
|
@@ -182,6 +186,8 @@ import {
|
|
|
182
186
|
existsSync,
|
|
183
187
|
mkdirSync,
|
|
184
188
|
readFileSync,
|
|
189
|
+
renameSync,
|
|
190
|
+
rmSync,
|
|
185
191
|
writeFileSync
|
|
186
192
|
} from "node:fs";
|
|
187
193
|
import { homedir } from "node:os";
|
|
@@ -211,10 +217,20 @@ function loadConfig(dir = defaultConfigDir()) {
|
|
|
211
217
|
function saveConfig(dir = defaultConfigDir(), config = {}) {
|
|
212
218
|
mkdirSync(dir, { recursive: true });
|
|
213
219
|
const file = join(dir, "config.json");
|
|
214
|
-
|
|
220
|
+
const tmp = join(dir, `config.json.${process.pid}.tmp`);
|
|
215
221
|
try {
|
|
216
|
-
|
|
217
|
-
|
|
222
|
+
writeFileSync(tmp, JSON.stringify(config, null, 2), { mode: 384 });
|
|
223
|
+
try {
|
|
224
|
+
chmodSync(tmp, 384);
|
|
225
|
+
} catch {
|
|
226
|
+
}
|
|
227
|
+
renameSync(tmp, file);
|
|
228
|
+
} catch (err) {
|
|
229
|
+
try {
|
|
230
|
+
rmSync(tmp, { force: true });
|
|
231
|
+
} catch {
|
|
232
|
+
}
|
|
233
|
+
throw err;
|
|
218
234
|
}
|
|
219
235
|
}
|
|
220
236
|
function ensureAnonKey(dir = defaultConfigDir()) {
|
|
@@ -250,11 +266,55 @@ var STABLE_NPM_CANDIDATES = [
|
|
|
250
266
|
"/usr/bin/npm"
|
|
251
267
|
];
|
|
252
268
|
var LATEST_PACKAGE_SPEC = "whoburnedmore@latest";
|
|
269
|
+
var SYNC_PATH_DIRS = [
|
|
270
|
+
"/opt/homebrew/bin",
|
|
271
|
+
"/usr/local/bin",
|
|
272
|
+
"/usr/bin",
|
|
273
|
+
"/bin",
|
|
274
|
+
"/usr/sbin",
|
|
275
|
+
"/sbin"
|
|
276
|
+
];
|
|
277
|
+
function syncPathEnv(npmPath = resolveNpmPath()) {
|
|
278
|
+
const dir = dirname(npmPath);
|
|
279
|
+
const dirs = [];
|
|
280
|
+
if (dir && dir !== "." && dir !== "/" && dir !== npmPath) dirs.push(dir);
|
|
281
|
+
for (const d of SYNC_PATH_DIRS) {
|
|
282
|
+
if (!dirs.includes(d)) dirs.push(d);
|
|
283
|
+
}
|
|
284
|
+
return dirs.join(":");
|
|
285
|
+
}
|
|
286
|
+
var FORWARDED_ENV_VARS = [
|
|
287
|
+
"WHOBURNEDMORE_CONFIG_DIR",
|
|
288
|
+
// identity: where the anonKey lives
|
|
289
|
+
"WHOBURNEDMORE_API",
|
|
290
|
+
// endpoint: where submits go
|
|
291
|
+
"WHOBURNEDMORE_WEB",
|
|
292
|
+
// dashboard URL shown to the user
|
|
293
|
+
"XDG_CONFIG_HOME",
|
|
294
|
+
// base for the default config dir
|
|
295
|
+
"CLAUDE_CONFIG_DIR"
|
|
296
|
+
// a primary usage-collection source
|
|
297
|
+
];
|
|
298
|
+
function syncEnv(opts) {
|
|
299
|
+
const env = opts?.env ?? process.env;
|
|
300
|
+
const pairs = [["PATH", syncPathEnv(opts?.npmPath)]];
|
|
301
|
+
for (const key of FORWARDED_ENV_VARS) {
|
|
302
|
+
const value = env[key];
|
|
303
|
+
if (typeof value === "string" && value.length > 0 && !/[\r\n]/.test(value)) {
|
|
304
|
+
pairs.push([key, value]);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return pairs;
|
|
308
|
+
}
|
|
253
309
|
function syncLogPath() {
|
|
254
310
|
return join2(defaultConfigDir(), "sync.log");
|
|
255
311
|
}
|
|
256
|
-
function buildLaunchdPlist(commandArgs = syncCommandArgs(), logPath = syncLogPath()) {
|
|
312
|
+
function buildLaunchdPlist(commandArgs = syncCommandArgs(), logPath = syncLogPath(), envPairs = syncEnv({ npmPath: commandArgs[0] })) {
|
|
257
313
|
const programArguments = commandArgs.map((arg) => ` <string>${xmlEscape(arg)}</string>`).join("\n");
|
|
314
|
+
const envEntries = envPairs.map(
|
|
315
|
+
([k, v]) => ` <key>${xmlEscape(k)}</key>
|
|
316
|
+
<string>${xmlEscape(v)}</string>`
|
|
317
|
+
).join("\n");
|
|
258
318
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
259
319
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
260
320
|
<plist version="1.0">
|
|
@@ -265,6 +325,15 @@ function buildLaunchdPlist(commandArgs = syncCommandArgs(), logPath = syncLogPat
|
|
|
265
325
|
<array>
|
|
266
326
|
${programArguments}
|
|
267
327
|
</array>
|
|
328
|
+
<!-- launchd runs jobs with a minimal PATH (/usr/bin:/bin:/usr/sbin:/sbin)
|
|
329
|
+
that excludes Homebrew. npm's shebang (and the package bin npm exec
|
|
330
|
+
spawns) is #!/usr/bin/env node, so node must be on PATH or every tick
|
|
331
|
+
dies with "env: node: No such file or directory". We also forward the
|
|
332
|
+
user's identity/endpoint/collection env so background == foreground. -->
|
|
333
|
+
<key>EnvironmentVariables</key>
|
|
334
|
+
<dict>
|
|
335
|
+
${envEntries}
|
|
336
|
+
</dict>
|
|
268
337
|
<key>StartInterval</key>
|
|
269
338
|
<integer>${SYNC_INTERVAL_MINUTES * 60}</integer>
|
|
270
339
|
<!-- Run once right after login/reboot so a machine that was off (or asleep)
|
|
@@ -398,7 +467,10 @@ function cronSchedule(mins = SYNC_INTERVAL_MINUTES) {
|
|
|
398
467
|
}
|
|
399
468
|
function expectedLinuxCronLine(opts) {
|
|
400
469
|
const command = syncCommandArgs(opts?.npmPath).map(shellQuote).join(" ");
|
|
401
|
-
|
|
470
|
+
const envPrefix = syncEnv({ npmPath: opts?.npmPath ?? resolveNpmPath() }).map(([k, v]) => `${k}=${shellQuote(v)}`).join(" ");
|
|
471
|
+
const redirect = `>${shellQuote(opts?.logPath ?? syncLogPath())} 2>&1`;
|
|
472
|
+
const commandField = `${envPrefix} ${command} ${redirect}`.replaceAll("%", "\\%");
|
|
473
|
+
return `${cronSchedule()} ${commandField}`;
|
|
402
474
|
}
|
|
403
475
|
var SYSTEMD_UNIT = "whoburnedmore-sync";
|
|
404
476
|
function systemdUserDir() {
|
|
@@ -412,15 +484,17 @@ function systemdTimerPath() {
|
|
|
412
484
|
return join2(systemdUserDir(), `${SYSTEMD_UNIT}.timer`);
|
|
413
485
|
}
|
|
414
486
|
function systemdQuote(value) {
|
|
415
|
-
return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
|
|
487
|
+
return `"${value.replaceAll("\\", "\\\\").replaceAll("%", "%%").replaceAll('"', '\\"')}"`;
|
|
416
488
|
}
|
|
417
|
-
function buildSystemdService(commandArgs = syncCommandArgs()) {
|
|
489
|
+
function buildSystemdService(commandArgs = syncCommandArgs(), envPairs = syncEnv({ npmPath: commandArgs[0] })) {
|
|
418
490
|
const execStart = commandArgs.map(systemdQuote).join(" ");
|
|
491
|
+
const envLines = envPairs.map(([k, v]) => `Environment=${systemdQuote(`${k}=${v}`)}`).join("\n");
|
|
419
492
|
return `[Unit]
|
|
420
493
|
Description=whoburnedmore background token-usage sync
|
|
421
494
|
|
|
422
495
|
[Service]
|
|
423
496
|
Type=oneshot
|
|
497
|
+
${envLines}
|
|
424
498
|
ExecStart=${execStart}
|
|
425
499
|
`;
|
|
426
500
|
}
|
|
@@ -478,8 +552,8 @@ function tryInstallSystemd() {
|
|
|
478
552
|
{ stdio: "ignore" }
|
|
479
553
|
);
|
|
480
554
|
if (res.status !== 0) {
|
|
481
|
-
|
|
482
|
-
|
|
555
|
+
rmSync2(systemdServicePath(), { force: true });
|
|
556
|
+
rmSync2(systemdTimerPath(), { force: true });
|
|
483
557
|
return null;
|
|
484
558
|
}
|
|
485
559
|
return `systemd user timer installed, syncing every ${syncIntervalLabel()} (run \`loginctl enable-linger\` to keep syncing while logged out)`;
|
|
@@ -490,7 +564,7 @@ function uninstallAutoSync() {
|
|
|
490
564
|
const plistPath = launchAgentPath();
|
|
491
565
|
if (existsSync2(plistPath)) {
|
|
492
566
|
spawnSync("launchctl", ["unload", plistPath], { stdio: "ignore" });
|
|
493
|
-
|
|
567
|
+
rmSync2(plistPath, { force: true });
|
|
494
568
|
}
|
|
495
569
|
return "launchd agent removed";
|
|
496
570
|
}
|
|
@@ -508,8 +582,8 @@ function uninstallAutoSync() {
|
|
|
508
582
|
["--user", "disable", "--now", `${SYSTEMD_UNIT}.timer`],
|
|
509
583
|
{ stdio: "ignore" }
|
|
510
584
|
);
|
|
511
|
-
|
|
512
|
-
|
|
585
|
+
rmSync2(systemdServicePath(), { force: true });
|
|
586
|
+
rmSync2(systemdTimerPath(), { force: true });
|
|
513
587
|
spawnSync("systemctl", ["--user", "daemon-reload"], { stdio: "ignore" });
|
|
514
588
|
removed = true;
|
|
515
589
|
}
|
|
@@ -585,7 +659,7 @@ function rotateLogIfLarge(path = syncLogPath(), capBytes = 256 * 1024) {
|
|
|
585
659
|
try {
|
|
586
660
|
if (!existsSync2(path)) return false;
|
|
587
661
|
if (statSync(path).size <= capBytes) return false;
|
|
588
|
-
|
|
662
|
+
renameSync2(path, `${path}.1`);
|
|
589
663
|
return true;
|
|
590
664
|
} catch {
|
|
591
665
|
return false;
|
|
@@ -673,46 +747,23 @@ async function daemonLoop(deps) {
|
|
|
673
747
|
// src/collect.ts
|
|
674
748
|
import { execFile } from "node:child_process";
|
|
675
749
|
import { createRequire as createRequire3 } from "node:module";
|
|
676
|
-
import { dirname as dirname3, join as
|
|
750
|
+
import { dirname as dirname3, join as join8 } from "node:path";
|
|
677
751
|
import { promisify } from "node:util";
|
|
678
752
|
|
|
679
753
|
// src/attribution.ts
|
|
680
754
|
import { readFileSync as readFileSync3, readdirSync, statSync as statSync2 } from "node:fs";
|
|
681
755
|
import { homedir as homedir3 } from "node:os";
|
|
682
|
-
import {
|
|
683
|
-
|
|
684
|
-
// src/pricing.ts
|
|
685
|
-
var TABLE = [
|
|
686
|
-
{ match: /opus/i, price: { in: 15, out: 75, cacheWrite: 18.75, cacheRead: 1.5 } },
|
|
687
|
-
{ match: /sonnet/i, price: { in: 3, out: 15, cacheWrite: 3.75, cacheRead: 0.3 } },
|
|
688
|
-
{ match: /haiku/i, price: { in: 0.8, out: 4, cacheWrite: 1, cacheRead: 0.08 } },
|
|
689
|
-
{ match: /fable/i, price: { in: 15, out: 75, cacheWrite: 18.75, cacheRead: 1.5 } },
|
|
690
|
-
{ match: /gpt-4o|gpt-4\.1/i, price: { in: 2.5, out: 10, cacheWrite: 2.5, cacheRead: 1.25 } },
|
|
691
|
-
{ match: /gpt-5|o3|o4|codex/i, price: { in: 1.25, out: 10, cacheWrite: 1.25, cacheRead: 0.125 } },
|
|
692
|
-
{ match: /gemini.*flash/i, price: { in: 0.15, out: 0.6, cacheWrite: 0.15, cacheRead: 0.0375 } },
|
|
693
|
-
{ match: /gemini/i, price: { in: 1.25, out: 5, cacheWrite: 1.25, cacheRead: 0.31 } }
|
|
694
|
-
];
|
|
695
|
-
function estimateCostUSD(model, t) {
|
|
696
|
-
const row = TABLE.find((r) => r.match.test(model));
|
|
697
|
-
if (!row) return 0;
|
|
698
|
-
const p = row.price;
|
|
699
|
-
const usd = (t.inputTokens * p.in + t.outputTokens * p.out + t.cacheCreationTokens * p.cacheWrite + t.cacheReadTokens * p.cacheRead) / 1e6;
|
|
700
|
-
return usd > 0 ? usd : 0;
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
// src/attribution.ts
|
|
756
|
+
import { join as join3 } from "node:path";
|
|
704
757
|
var CLAUDE_PROJECTS = join3(homedir3(), ".claude", "projects");
|
|
705
758
|
var CODEX_SESSIONS = join3(homedir3(), ".codex", "sessions");
|
|
706
759
|
var MAX_FILES = 5e3;
|
|
707
760
|
var MAX_FILE_BYTES = 64 * 1024 * 1024;
|
|
708
761
|
var TIME_BUDGET_MS = 12e3;
|
|
709
762
|
var MAX_STATS = 300;
|
|
710
|
-
var MAX_PROJECTS = 500;
|
|
711
763
|
function createAccumulator() {
|
|
712
764
|
return {
|
|
713
765
|
tools: /* @__PURE__ */ new Map(),
|
|
714
766
|
skills: /* @__PURE__ */ new Map(),
|
|
715
|
-
projects: /* @__PURE__ */ new Map(),
|
|
716
767
|
agent: {
|
|
717
768
|
messageCount: 0,
|
|
718
769
|
subagentMessages: 0,
|
|
@@ -720,7 +771,6 @@ function createAccumulator() {
|
|
|
720
771
|
totalTokens: 0,
|
|
721
772
|
userMessageCount: 0
|
|
722
773
|
},
|
|
723
|
-
titles: /* @__PURE__ */ new Map(),
|
|
724
774
|
sessionMessages: /* @__PURE__ */ new Map()
|
|
725
775
|
};
|
|
726
776
|
}
|
|
@@ -761,9 +811,6 @@ function processRecord(rec, acc, ctx) {
|
|
|
761
811
|
sk.tokens += recTokens;
|
|
762
812
|
acc.skills.set(s, sk);
|
|
763
813
|
}
|
|
764
|
-
if (r.type === "ai-title" && typeof r.aiTitle === "string" && r.aiTitle && typeof r.sessionId === "string" && r.sessionId) {
|
|
765
|
-
acc.titles.set(r.sessionId, r.aiTitle.slice(0, 200));
|
|
766
|
-
}
|
|
767
814
|
const content = r.message?.content;
|
|
768
815
|
if (!Array.isArray(content)) return;
|
|
769
816
|
const isAssistant = r.type === "assistant" || r.message?.role === "assistant";
|
|
@@ -800,25 +847,6 @@ function processRecord(rec, acc, ctx) {
|
|
|
800
847
|
(acc.sessionMessages.get(r.sessionId) ?? 0) + 1
|
|
801
848
|
);
|
|
802
849
|
}
|
|
803
|
-
if (typeof r.cwd === "string" && r.cwd && tokens > 0) {
|
|
804
|
-
const name = basename(r.cwd).slice(0, 128) || "unknown";
|
|
805
|
-
const model = typeof r.message?.model === "string" ? r.message.model : "unknown";
|
|
806
|
-
const u = r.message?.usage;
|
|
807
|
-
const num3 = (v) => {
|
|
808
|
-
const x = Math.round(Number(v));
|
|
809
|
-
return Number.isFinite(x) && x > 0 ? x : 0;
|
|
810
|
-
};
|
|
811
|
-
const cost = estimateCostUSD(model, {
|
|
812
|
-
inputTokens: num3(u?.input_tokens),
|
|
813
|
-
outputTokens: num3(u?.output_tokens),
|
|
814
|
-
cacheCreationTokens: num3(u?.cache_creation_input_tokens),
|
|
815
|
-
cacheReadTokens: num3(u?.cache_read_input_tokens)
|
|
816
|
-
});
|
|
817
|
-
const p = acc.projects.get(name) ?? { tokens: 0, costUSD: 0 };
|
|
818
|
-
p.tokens += tokens;
|
|
819
|
-
p.costUSD += cost;
|
|
820
|
-
acc.projects.set(name, p);
|
|
821
|
-
}
|
|
822
850
|
} else if (isUser) {
|
|
823
851
|
for (const block of content) {
|
|
824
852
|
if (block && typeof block === "object" && block.type === "tool_result" && block.is_error === true) {
|
|
@@ -844,20 +872,11 @@ function toToolStats(map) {
|
|
|
844
872
|
return base;
|
|
845
873
|
});
|
|
846
874
|
}
|
|
847
|
-
function toProjectStats(map) {
|
|
848
|
-
return [...map.entries()].map(([name, v]) => ({
|
|
849
|
-
name,
|
|
850
|
-
tokens: v.tokens,
|
|
851
|
-
costUSD: Number(v.costUSD.toFixed(6))
|
|
852
|
-
})).filter((p) => p.tokens > 0).sort((a, b) => b.tokens - a.tokens).slice(0, MAX_PROJECTS);
|
|
853
|
-
}
|
|
854
875
|
function accumulatorToResult(acc) {
|
|
855
876
|
return {
|
|
856
877
|
tools: toToolStats(acc.tools),
|
|
857
878
|
skills: toSkillStats(acc.skills),
|
|
858
|
-
projects: toProjectStats(acc.projects),
|
|
859
879
|
agent: { ...acc.agent },
|
|
860
|
-
titles: acc.titles,
|
|
861
880
|
sessionMessages: acc.sessionMessages,
|
|
862
881
|
complete: true
|
|
863
882
|
};
|
|
@@ -867,7 +886,7 @@ function numTok(v) {
|
|
|
867
886
|
return Number.isFinite(x) && x > 0 ? x : 0;
|
|
868
887
|
}
|
|
869
888
|
function createCodexContext() {
|
|
870
|
-
return {
|
|
889
|
+
return { pending: [] };
|
|
871
890
|
}
|
|
872
891
|
function processCodexRecord(rec, acc, ctx) {
|
|
873
892
|
if (!rec || typeof rec !== "object") return;
|
|
@@ -875,11 +894,7 @@ function processCodexRecord(rec, acc, ctx) {
|
|
|
875
894
|
if (!r.payload || typeof r.payload !== "object") return;
|
|
876
895
|
const pl = r.payload;
|
|
877
896
|
const ptype = pl.type;
|
|
878
|
-
if (r.type === "session_meta" || r.type === "turn_context")
|
|
879
|
-
if (typeof pl.cwd === "string" && pl.cwd) ctx.cwd = pl.cwd;
|
|
880
|
-
if (typeof pl.model === "string" && pl.model) ctx.model = pl.model;
|
|
881
|
-
return;
|
|
882
|
-
}
|
|
897
|
+
if (r.type === "session_meta" || r.type === "turn_context") return;
|
|
883
898
|
if (ptype === "function_call" || ptype === "custom_tool_call" || ptype === "local_shell_call") {
|
|
884
899
|
const raw = typeof pl.name === "string" ? pl.name : ptype === "local_shell_call" ? "local_shell" : "";
|
|
885
900
|
const name = raw.slice(0, 128);
|
|
@@ -905,19 +920,6 @@ function processCodexRecord(rec, acc, ctx) {
|
|
|
905
920
|
}
|
|
906
921
|
acc.agent.messageCount += 1;
|
|
907
922
|
acc.agent.totalTokens += tokens;
|
|
908
|
-
if (ctx.cwd) {
|
|
909
|
-
const name = basename(ctx.cwd).slice(0, 128) || "unknown";
|
|
910
|
-
const cost = estimateCostUSD(ctx.model, {
|
|
911
|
-
inputTokens,
|
|
912
|
-
outputTokens,
|
|
913
|
-
cacheCreationTokens: 0,
|
|
914
|
-
cacheReadTokens
|
|
915
|
-
});
|
|
916
|
-
const proj = acc.projects.get(name) ?? { tokens: 0, costUSD: 0 };
|
|
917
|
-
proj.tokens += tokens;
|
|
918
|
-
proj.costUSD += cost;
|
|
919
|
-
acc.projects.set(name, proj);
|
|
920
|
-
}
|
|
921
923
|
const per = ctx.pending.length > 0 ? Math.floor(tokens / ctx.pending.length) : 0;
|
|
922
924
|
for (const tu of ctx.pending) {
|
|
923
925
|
const t = acc.tools.get(tu.name);
|
|
@@ -1272,6 +1274,361 @@ async function collectCursor() {
|
|
|
1272
1274
|
return { entries: [], blocks: [], found: false };
|
|
1273
1275
|
}
|
|
1274
1276
|
|
|
1277
|
+
// src/native/claude.ts
|
|
1278
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
1279
|
+
import { homedir as homedir5 } from "node:os";
|
|
1280
|
+
import { join as join6 } from "node:path";
|
|
1281
|
+
|
|
1282
|
+
// src/pricing.ts
|
|
1283
|
+
var TABLE = [
|
|
1284
|
+
{ match: /opus/i, price: { in: 15, out: 75, cacheWrite: 18.75, cacheRead: 1.5 } },
|
|
1285
|
+
{ match: /sonnet/i, price: { in: 3, out: 15, cacheWrite: 3.75, cacheRead: 0.3 } },
|
|
1286
|
+
{ match: /haiku/i, price: { in: 0.8, out: 4, cacheWrite: 1, cacheRead: 0.08 } },
|
|
1287
|
+
{ match: /fable/i, price: { in: 15, out: 75, cacheWrite: 18.75, cacheRead: 1.5 } },
|
|
1288
|
+
{ match: /gpt-4o|gpt-4\.1/i, price: { in: 2.5, out: 10, cacheWrite: 2.5, cacheRead: 1.25 } },
|
|
1289
|
+
{ match: /gpt-5|o3|o4|codex/i, price: { in: 1.25, out: 10, cacheWrite: 1.25, cacheRead: 0.125 } },
|
|
1290
|
+
{ match: /gemini.*flash/i, price: { in: 0.15, out: 0.6, cacheWrite: 0.15, cacheRead: 0.0375 } },
|
|
1291
|
+
{ match: /gemini/i, price: { in: 1.25, out: 5, cacheWrite: 1.25, cacheRead: 0.31 } }
|
|
1292
|
+
];
|
|
1293
|
+
function estimateCostUSD(model, t) {
|
|
1294
|
+
const row = TABLE.find((r) => r.match.test(model));
|
|
1295
|
+
if (!row) return 0;
|
|
1296
|
+
const p = row.price;
|
|
1297
|
+
const usd = (t.inputTokens * p.in + t.outputTokens * p.out + t.cacheCreationTokens * p.cacheWrite + t.cacheReadTokens * p.cacheRead) / 1e6;
|
|
1298
|
+
return usd > 0 ? usd : 0;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
// src/native/claude.ts
|
|
1302
|
+
function num3(n) {
|
|
1303
|
+
const v = Math.round(Number(n));
|
|
1304
|
+
return Number.isFinite(v) && v > 0 ? v : 0;
|
|
1305
|
+
}
|
|
1306
|
+
function reqTokens(r) {
|
|
1307
|
+
return r.inputTokens + r.outputTokens + r.cacheCreationTokens + r.cacheReadTokens;
|
|
1308
|
+
}
|
|
1309
|
+
function localDate(iso) {
|
|
1310
|
+
const t = Date.parse(iso);
|
|
1311
|
+
if (!Number.isFinite(t)) return null;
|
|
1312
|
+
const d = new Date(t);
|
|
1313
|
+
const y = d.getFullYear();
|
|
1314
|
+
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
1315
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
1316
|
+
return `${y}-${m}-${day}`;
|
|
1317
|
+
}
|
|
1318
|
+
var syntheticCounter = 0;
|
|
1319
|
+
function parseClaudeLine(raw) {
|
|
1320
|
+
const trimmed = raw.trim();
|
|
1321
|
+
if (!trimmed) return null;
|
|
1322
|
+
let obj;
|
|
1323
|
+
try {
|
|
1324
|
+
obj = JSON.parse(trimmed);
|
|
1325
|
+
} catch {
|
|
1326
|
+
return null;
|
|
1327
|
+
}
|
|
1328
|
+
const message = obj.message;
|
|
1329
|
+
if (!message || typeof message !== "object") return null;
|
|
1330
|
+
const usage = message.usage;
|
|
1331
|
+
if (!usage || typeof usage !== "object") return null;
|
|
1332
|
+
if (message.role !== void 0 && message.role !== "assistant") return null;
|
|
1333
|
+
const date = localDate(String(obj.timestamp ?? ""));
|
|
1334
|
+
if (!date) return null;
|
|
1335
|
+
const messageId = typeof message.id === "string" ? message.id : "";
|
|
1336
|
+
const requestId = typeof obj.requestId === "string" ? obj.requestId : "";
|
|
1337
|
+
const hasRealId = messageId !== "" || requestId !== "";
|
|
1338
|
+
const key = hasRealId ? `${messageId}|${requestId}` : `synthetic|${date}|${syntheticCounter += 1}`;
|
|
1339
|
+
return {
|
|
1340
|
+
key,
|
|
1341
|
+
hasRealId,
|
|
1342
|
+
date,
|
|
1343
|
+
model: typeof message.model === "string" ? message.model : "unknown",
|
|
1344
|
+
inputTokens: num3(usage.input_tokens),
|
|
1345
|
+
outputTokens: num3(usage.output_tokens),
|
|
1346
|
+
cacheCreationTokens: num3(usage.cache_creation_input_tokens),
|
|
1347
|
+
cacheReadTokens: num3(usage.cache_read_input_tokens)
|
|
1348
|
+
};
|
|
1349
|
+
}
|
|
1350
|
+
function accumulateClaudeLines(acc, lines) {
|
|
1351
|
+
for (const line of lines) {
|
|
1352
|
+
const r = parseClaudeLine(line);
|
|
1353
|
+
if (!r) continue;
|
|
1354
|
+
const prev = acc.get(r.key);
|
|
1355
|
+
if (!prev || reqTokens(r) > reqTokens(prev)) acc.set(r.key, r);
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
function finalizeClaudeEntries(acc) {
|
|
1359
|
+
const byDayModel = /* @__PURE__ */ new Map();
|
|
1360
|
+
for (const r of acc.values()) {
|
|
1361
|
+
const k = `${r.date}|${r.model}`;
|
|
1362
|
+
let b = byDayModel.get(k);
|
|
1363
|
+
if (!b) {
|
|
1364
|
+
b = {
|
|
1365
|
+
date: r.date,
|
|
1366
|
+
model: r.model,
|
|
1367
|
+
inputTokens: 0,
|
|
1368
|
+
outputTokens: 0,
|
|
1369
|
+
cacheCreationTokens: 0,
|
|
1370
|
+
cacheReadTokens: 0,
|
|
1371
|
+
requestCount: 0
|
|
1372
|
+
};
|
|
1373
|
+
byDayModel.set(k, b);
|
|
1374
|
+
}
|
|
1375
|
+
b.inputTokens += r.inputTokens;
|
|
1376
|
+
b.outputTokens += r.outputTokens;
|
|
1377
|
+
b.cacheCreationTokens += r.cacheCreationTokens;
|
|
1378
|
+
b.cacheReadTokens += r.cacheReadTokens;
|
|
1379
|
+
if (r.hasRealId) b.requestCount += 1;
|
|
1380
|
+
}
|
|
1381
|
+
const entries = [];
|
|
1382
|
+
for (const b of byDayModel.values()) {
|
|
1383
|
+
const tokens = b.inputTokens + b.outputTokens + b.cacheCreationTokens + b.cacheReadTokens;
|
|
1384
|
+
if (tokens === 0) continue;
|
|
1385
|
+
entries.push({
|
|
1386
|
+
date: b.date,
|
|
1387
|
+
tool: "claude",
|
|
1388
|
+
model: b.model,
|
|
1389
|
+
inputTokens: b.inputTokens,
|
|
1390
|
+
outputTokens: b.outputTokens,
|
|
1391
|
+
cacheCreationTokens: b.cacheCreationTokens,
|
|
1392
|
+
cacheReadTokens: b.cacheReadTokens,
|
|
1393
|
+
costUSD: estimateCostUSD(b.model, b),
|
|
1394
|
+
origin: "cli",
|
|
1395
|
+
verified: false,
|
|
1396
|
+
requestCount: b.requestCount
|
|
1397
|
+
});
|
|
1398
|
+
}
|
|
1399
|
+
return entries;
|
|
1400
|
+
}
|
|
1401
|
+
function resolveClaudeConfigRoots(env = process.env) {
|
|
1402
|
+
const override = env.CLAUDE_CONFIG_DIR;
|
|
1403
|
+
return override ? override.split(",").map((s) => s.trim()).filter(Boolean) : [join6(homedir5(), ".claude"), join6(homedir5(), ".config", "claude")];
|
|
1404
|
+
}
|
|
1405
|
+
function resolveClaudeProjectDirs(env = process.env) {
|
|
1406
|
+
return resolveClaudeConfigRoots(env).map((r) => join6(r, "projects"));
|
|
1407
|
+
}
|
|
1408
|
+
async function listJsonl(dir) {
|
|
1409
|
+
let dirents;
|
|
1410
|
+
try {
|
|
1411
|
+
dirents = await readdir(dir, { withFileTypes: true });
|
|
1412
|
+
} catch {
|
|
1413
|
+
return [];
|
|
1414
|
+
}
|
|
1415
|
+
const out = [];
|
|
1416
|
+
for (const d of dirents) {
|
|
1417
|
+
const full = join6(dir, d.name);
|
|
1418
|
+
if (d.isDirectory()) {
|
|
1419
|
+
out.push(...await listJsonl(full));
|
|
1420
|
+
} else if (d.isFile() && d.name.endsWith(".jsonl")) {
|
|
1421
|
+
out.push(full);
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
return out;
|
|
1425
|
+
}
|
|
1426
|
+
var NATIVE_READ_BUDGET_MS = 2e4;
|
|
1427
|
+
function* splitLines(content) {
|
|
1428
|
+
let start = 0;
|
|
1429
|
+
for (let i = 0; i < content.length; i++) {
|
|
1430
|
+
if (content[i] === "\n") {
|
|
1431
|
+
yield content.slice(start, i);
|
|
1432
|
+
start = i + 1;
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
if (start < content.length) yield content.slice(start);
|
|
1436
|
+
}
|
|
1437
|
+
async function collectClaudeNative(env = process.env, opts = {}) {
|
|
1438
|
+
const dirs = resolveClaudeProjectDirs(env);
|
|
1439
|
+
const files = [];
|
|
1440
|
+
for (const dir of dirs) files.push(...await listJsonl(dir));
|
|
1441
|
+
if (files.length === 0) return { entries: [], found: false, filesScanned: 0 };
|
|
1442
|
+
const now = opts.now ?? Date.now;
|
|
1443
|
+
const deadline = now() + (opts.budgetMs ?? NATIVE_READ_BUDGET_MS);
|
|
1444
|
+
const acc = /* @__PURE__ */ new Map();
|
|
1445
|
+
let scanned = 0;
|
|
1446
|
+
for (const f of files) {
|
|
1447
|
+
if (now() > deadline) {
|
|
1448
|
+
return { entries: [], found: false, filesScanned: scanned, timedOut: true };
|
|
1449
|
+
}
|
|
1450
|
+
let content;
|
|
1451
|
+
try {
|
|
1452
|
+
content = await readFile(f, "utf8");
|
|
1453
|
+
} catch {
|
|
1454
|
+
continue;
|
|
1455
|
+
}
|
|
1456
|
+
accumulateClaudeLines(acc, splitLines(content));
|
|
1457
|
+
scanned += 1;
|
|
1458
|
+
}
|
|
1459
|
+
return { entries: finalizeClaudeEntries(acc), found: true, filesScanned: scanned };
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
// src/native/codex.ts
|
|
1463
|
+
import { readdir as readdir2, readFile as readFile2 } from "node:fs/promises";
|
|
1464
|
+
import { homedir as homedir6 } from "node:os";
|
|
1465
|
+
import { join as join7 } from "node:path";
|
|
1466
|
+
function num4(n) {
|
|
1467
|
+
const v = Math.round(Number(n));
|
|
1468
|
+
return Number.isFinite(v) && v > 0 ? v : 0;
|
|
1469
|
+
}
|
|
1470
|
+
function localDate2(iso) {
|
|
1471
|
+
const t = Date.parse(iso);
|
|
1472
|
+
if (!Number.isFinite(t)) return null;
|
|
1473
|
+
const d = new Date(t);
|
|
1474
|
+
const y = d.getFullYear();
|
|
1475
|
+
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
1476
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
1477
|
+
return `${y}-${m}-${day}`;
|
|
1478
|
+
}
|
|
1479
|
+
function readTokenFields(payload) {
|
|
1480
|
+
const info = payload.info;
|
|
1481
|
+
const src = info?.total_token_usage ?? payload;
|
|
1482
|
+
const input = src.input_tokens;
|
|
1483
|
+
const output = src.output_tokens;
|
|
1484
|
+
if (input === void 0 && output === void 0) return null;
|
|
1485
|
+
return {
|
|
1486
|
+
input: num4(input),
|
|
1487
|
+
cached: num4(src.cached_input_tokens),
|
|
1488
|
+
output: num4(output),
|
|
1489
|
+
reasoning: num4(src.reasoning_output_tokens)
|
|
1490
|
+
};
|
|
1491
|
+
}
|
|
1492
|
+
function parseCodexRollout(lines) {
|
|
1493
|
+
let model = "unknown";
|
|
1494
|
+
let lastDate = null;
|
|
1495
|
+
let last = null;
|
|
1496
|
+
let turnCount = 0;
|
|
1497
|
+
for (const raw of lines) {
|
|
1498
|
+
const trimmed = raw.trim();
|
|
1499
|
+
if (!trimmed) continue;
|
|
1500
|
+
let obj;
|
|
1501
|
+
try {
|
|
1502
|
+
obj = JSON.parse(trimmed);
|
|
1503
|
+
} catch {
|
|
1504
|
+
continue;
|
|
1505
|
+
}
|
|
1506
|
+
const payload = obj.payload;
|
|
1507
|
+
if (!payload || typeof payload !== "object") continue;
|
|
1508
|
+
const kind = obj.type;
|
|
1509
|
+
if (kind === "session_meta" || kind === "turn_context") {
|
|
1510
|
+
if (typeof payload.model === "string" && payload.model) model = payload.model;
|
|
1511
|
+
}
|
|
1512
|
+
if (payload.type === "token_count") {
|
|
1513
|
+
const fields = readTokenFields(payload);
|
|
1514
|
+
if (!fields) continue;
|
|
1515
|
+
last = fields;
|
|
1516
|
+
turnCount += 1;
|
|
1517
|
+
const d = localDate2(String(obj.timestamp ?? ""));
|
|
1518
|
+
if (d) lastDate = d;
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
if (!last || !lastDate) return null;
|
|
1522
|
+
const cacheReadTokens = last.cached;
|
|
1523
|
+
const inputTokens = Math.max(0, last.input - last.cached);
|
|
1524
|
+
const outputTokens = last.output + last.reasoning;
|
|
1525
|
+
if (inputTokens + outputTokens + cacheReadTokens === 0) return null;
|
|
1526
|
+
return {
|
|
1527
|
+
date: lastDate,
|
|
1528
|
+
model,
|
|
1529
|
+
inputTokens,
|
|
1530
|
+
outputTokens,
|
|
1531
|
+
cacheCreationTokens: 0,
|
|
1532
|
+
cacheReadTokens,
|
|
1533
|
+
turnCount
|
|
1534
|
+
};
|
|
1535
|
+
}
|
|
1536
|
+
function accumulateCodexSession(acc, lines) {
|
|
1537
|
+
const s = parseCodexRollout(lines);
|
|
1538
|
+
if (!s) return;
|
|
1539
|
+
const k = `${s.date}|${s.model}`;
|
|
1540
|
+
let b = acc.get(k);
|
|
1541
|
+
if (!b) {
|
|
1542
|
+
b = {
|
|
1543
|
+
date: s.date,
|
|
1544
|
+
model: s.model,
|
|
1545
|
+
inputTokens: 0,
|
|
1546
|
+
outputTokens: 0,
|
|
1547
|
+
cacheCreationTokens: 0,
|
|
1548
|
+
cacheReadTokens: 0,
|
|
1549
|
+
requestCount: 0
|
|
1550
|
+
};
|
|
1551
|
+
acc.set(k, b);
|
|
1552
|
+
}
|
|
1553
|
+
b.inputTokens += s.inputTokens;
|
|
1554
|
+
b.outputTokens += s.outputTokens;
|
|
1555
|
+
b.cacheCreationTokens += s.cacheCreationTokens;
|
|
1556
|
+
b.cacheReadTokens += s.cacheReadTokens;
|
|
1557
|
+
b.requestCount += s.turnCount;
|
|
1558
|
+
}
|
|
1559
|
+
function finalizeCodexEntries(acc) {
|
|
1560
|
+
const entries = [];
|
|
1561
|
+
for (const b of acc.values()) {
|
|
1562
|
+
entries.push({
|
|
1563
|
+
date: b.date,
|
|
1564
|
+
tool: "codex",
|
|
1565
|
+
model: b.model,
|
|
1566
|
+
inputTokens: b.inputTokens,
|
|
1567
|
+
outputTokens: b.outputTokens,
|
|
1568
|
+
cacheCreationTokens: b.cacheCreationTokens,
|
|
1569
|
+
cacheReadTokens: b.cacheReadTokens,
|
|
1570
|
+
costUSD: estimateCostUSD(b.model, b),
|
|
1571
|
+
origin: "cli",
|
|
1572
|
+
verified: false,
|
|
1573
|
+
requestCount: b.requestCount
|
|
1574
|
+
});
|
|
1575
|
+
}
|
|
1576
|
+
return entries;
|
|
1577
|
+
}
|
|
1578
|
+
function resolveCodexSessionsDir(env = process.env) {
|
|
1579
|
+
const home = env.CODEX_HOME && env.CODEX_HOME.trim() ? env.CODEX_HOME.trim() : join7(homedir6(), ".codex");
|
|
1580
|
+
return join7(home, "sessions");
|
|
1581
|
+
}
|
|
1582
|
+
async function listJsonl2(dir) {
|
|
1583
|
+
let dirents;
|
|
1584
|
+
try {
|
|
1585
|
+
dirents = await readdir2(dir, { withFileTypes: true });
|
|
1586
|
+
} catch {
|
|
1587
|
+
return [];
|
|
1588
|
+
}
|
|
1589
|
+
const out = [];
|
|
1590
|
+
for (const d of dirents) {
|
|
1591
|
+
const full = join7(dir, d.name);
|
|
1592
|
+
if (d.isDirectory()) out.push(...await listJsonl2(full));
|
|
1593
|
+
else if (d.isFile() && d.name.endsWith(".jsonl")) out.push(full);
|
|
1594
|
+
}
|
|
1595
|
+
return out;
|
|
1596
|
+
}
|
|
1597
|
+
function* splitLines2(content) {
|
|
1598
|
+
let start = 0;
|
|
1599
|
+
for (let i = 0; i < content.length; i++) {
|
|
1600
|
+
if (content[i] === "\n") {
|
|
1601
|
+
yield content.slice(start, i);
|
|
1602
|
+
start = i + 1;
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
if (start < content.length) yield content.slice(start);
|
|
1606
|
+
}
|
|
1607
|
+
var NATIVE_READ_BUDGET_MS2 = 2e4;
|
|
1608
|
+
async function collectCodexNative(env = process.env, opts = {}) {
|
|
1609
|
+
const dir = resolveCodexSessionsDir(env);
|
|
1610
|
+
const files = await listJsonl2(dir);
|
|
1611
|
+
if (files.length === 0) return { entries: [], found: false, filesScanned: 0 };
|
|
1612
|
+
const now = opts.now ?? Date.now;
|
|
1613
|
+
const deadline = now() + (opts.budgetMs ?? NATIVE_READ_BUDGET_MS2);
|
|
1614
|
+
const acc = /* @__PURE__ */ new Map();
|
|
1615
|
+
let scanned = 0;
|
|
1616
|
+
for (const f of files) {
|
|
1617
|
+
if (now() > deadline) {
|
|
1618
|
+
return { entries: [], found: false, filesScanned: scanned, timedOut: true };
|
|
1619
|
+
}
|
|
1620
|
+
let content;
|
|
1621
|
+
try {
|
|
1622
|
+
content = await readFile2(f, "utf8");
|
|
1623
|
+
} catch {
|
|
1624
|
+
continue;
|
|
1625
|
+
}
|
|
1626
|
+
accumulateCodexSession(acc, splitLines2(content));
|
|
1627
|
+
scanned += 1;
|
|
1628
|
+
}
|
|
1629
|
+
return { entries: finalizeCodexEntries(acc), found: true, filesScanned: scanned };
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1275
1632
|
// src/collect.ts
|
|
1276
1633
|
var execFileAsync = promisify(execFile);
|
|
1277
1634
|
var SOURCES = [
|
|
@@ -1340,9 +1697,12 @@ function mapCcusageDaily(tool, json) {
|
|
|
1340
1697
|
};
|
|
1341
1698
|
});
|
|
1342
1699
|
} else {
|
|
1700
|
+
const modelsUsed = Array.isArray(day.modelsUsed) ? day.modelsUsed.filter(
|
|
1701
|
+
(m) => typeof m === "string" && m.length > 0
|
|
1702
|
+
) : [];
|
|
1343
1703
|
candidates = [
|
|
1344
1704
|
{
|
|
1345
|
-
model: "unknown",
|
|
1705
|
+
model: modelsUsed.length === 1 ? modelsUsed[0] : "unknown",
|
|
1346
1706
|
inputTokens: norm(day.inputTokens),
|
|
1347
1707
|
outputTokens: norm(day.outputTokens),
|
|
1348
1708
|
cacheCreationTokens: norm(day.cacheCreationTokens),
|
|
@@ -1409,7 +1769,7 @@ function resolveCcusageBin() {
|
|
|
1409
1769
|
const pkgPath = require3.resolve("ccusage/package.json");
|
|
1410
1770
|
const pkg = require3("ccusage/package.json");
|
|
1411
1771
|
const rel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.ccusage ?? "ccusage";
|
|
1412
|
-
const binPath =
|
|
1772
|
+
const binPath = join8(dirname3(pkgPath), rel);
|
|
1413
1773
|
if (/\.(c|m)?js$/.test(binPath)) {
|
|
1414
1774
|
return { cmd: process.execPath, prefixArgs: [binPath] };
|
|
1415
1775
|
}
|
|
@@ -1430,6 +1790,9 @@ function dedupeDaily(entries) {
|
|
|
1430
1790
|
prev.cacheReadTokens += e.cacheReadTokens;
|
|
1431
1791
|
prev.costUSD = Number((prev.costUSD + e.costUSD).toFixed(6));
|
|
1432
1792
|
prev.verified = prev.verified && e.verified;
|
|
1793
|
+
if (e.requestCount !== void 0 || prev.requestCount !== void 0) {
|
|
1794
|
+
prev.requestCount = (prev.requestCount ?? 0) + (e.requestCount ?? 0);
|
|
1795
|
+
}
|
|
1433
1796
|
}
|
|
1434
1797
|
return [...byKey.values()];
|
|
1435
1798
|
}
|
|
@@ -1462,7 +1825,18 @@ function dedupeBlocks(blocks) {
|
|
|
1462
1825
|
}
|
|
1463
1826
|
return [...byStart.values()];
|
|
1464
1827
|
}
|
|
1465
|
-
|
|
1828
|
+
function selectSourceEntries(source, ccusageEntries, native) {
|
|
1829
|
+
if (source === "claude" && native.claude.found && native.claude.entries.length > 0)
|
|
1830
|
+
return native.claude.entries;
|
|
1831
|
+
if (source === "codex" && native.codex.found && native.codex.entries.length > 0)
|
|
1832
|
+
return native.codex.entries;
|
|
1833
|
+
return ccusageEntries;
|
|
1834
|
+
}
|
|
1835
|
+
function ccusageClaudeEnv(env = process.env) {
|
|
1836
|
+
if (env.CLAUDE_CONFIG_DIR && env.CLAUDE_CONFIG_DIR.trim()) return env;
|
|
1837
|
+
return { ...env, CLAUDE_CONFIG_DIR: resolveClaudeConfigRoots(env).join(",") };
|
|
1838
|
+
}
|
|
1839
|
+
async function runCcusageOnce(cmd, args, env) {
|
|
1466
1840
|
try {
|
|
1467
1841
|
const { stdout } = await execFileAsync(cmd, args, {
|
|
1468
1842
|
encoding: "utf8",
|
|
@@ -1470,7 +1844,8 @@ async function runCcusageOnce(cmd, args) {
|
|
|
1470
1844
|
// A single source shouldn't be able to hang the whole run. 25s is plenty
|
|
1471
1845
|
// for a healthy local read; a hung source gets killed and (if transient)
|
|
1472
1846
|
// retried once below rather than stalling everything for minutes.
|
|
1473
|
-
timeout: 25e3
|
|
1847
|
+
timeout: 25e3,
|
|
1848
|
+
...env ? { env } : {}
|
|
1474
1849
|
});
|
|
1475
1850
|
if (!stdout) return { json: null, transient: false };
|
|
1476
1851
|
try {
|
|
@@ -1484,18 +1859,29 @@ async function runCcusageOnce(cmd, args) {
|
|
|
1484
1859
|
return { json: null, transient };
|
|
1485
1860
|
}
|
|
1486
1861
|
}
|
|
1487
|
-
async function runCcusage(cmd, args) {
|
|
1488
|
-
const first = await runCcusageOnce(cmd, args);
|
|
1862
|
+
async function runCcusage(cmd, args, env) {
|
|
1863
|
+
const first = await runCcusageOnce(cmd, args, env);
|
|
1489
1864
|
if (first.json !== null || !first.transient) return first.json;
|
|
1490
|
-
return (await runCcusageOnce(cmd, args)).json;
|
|
1865
|
+
return (await runCcusageOnce(cmd, args, env)).json;
|
|
1491
1866
|
}
|
|
1492
1867
|
var COLLECT_STAGES = SOURCES.length + 4;
|
|
1493
1868
|
async function collectAll(onProgress) {
|
|
1494
1869
|
const { cmd, prefixArgs } = resolveCcusageBin();
|
|
1495
1870
|
let done = 0;
|
|
1496
1871
|
const tick = () => onProgress?.(++done, COLLECT_STAGES, "");
|
|
1872
|
+
const nativeClaudeTask = collectClaudeNative().catch(
|
|
1873
|
+
() => ({ entries: [], found: false, filesScanned: 0 })
|
|
1874
|
+
);
|
|
1875
|
+
const nativeCodexTask = collectCodexNative().catch(
|
|
1876
|
+
() => ({ entries: [], found: false, filesScanned: 0 })
|
|
1877
|
+
);
|
|
1497
1878
|
const sourceTasks = SOURCES.map(async (source) => {
|
|
1498
|
-
const
|
|
1879
|
+
const env = source === "claude" ? ccusageClaudeEnv() : void 0;
|
|
1880
|
+
const json = await runCcusage(
|
|
1881
|
+
cmd,
|
|
1882
|
+
[...prefixArgs, source, "daily", "--json", "--offline"],
|
|
1883
|
+
env
|
|
1884
|
+
);
|
|
1499
1885
|
tick();
|
|
1500
1886
|
return { source, mapped: json ? mapCcusageDaily(source, json) : [] };
|
|
1501
1887
|
});
|
|
@@ -1519,18 +1905,22 @@ async function collectAll(onProgress) {
|
|
|
1519
1905
|
tick();
|
|
1520
1906
|
return a;
|
|
1521
1907
|
});
|
|
1522
|
-
const [sourceResults, sessions, blocks, cursor, attribution] = await Promise.all([
|
|
1908
|
+
const [sourceResults, sessions, blocks, cursor, attribution, nativeClaude, nativeCodex] = await Promise.all([
|
|
1523
1909
|
Promise.all(sourceTasks),
|
|
1524
1910
|
sessionTask,
|
|
1525
1911
|
blockTask,
|
|
1526
1912
|
cursorTask,
|
|
1527
|
-
attributionTask
|
|
1913
|
+
attributionTask,
|
|
1914
|
+
nativeClaudeTask,
|
|
1915
|
+
nativeCodexTask
|
|
1528
1916
|
]);
|
|
1917
|
+
const native = { claude: nativeClaude, codex: nativeCodex };
|
|
1529
1918
|
const entries = [];
|
|
1530
1919
|
const toolsFound = [];
|
|
1531
1920
|
for (const { source, mapped } of sourceResults) {
|
|
1532
|
-
|
|
1533
|
-
|
|
1921
|
+
const chosen = selectSourceEntries(source, mapped, native);
|
|
1922
|
+
if (chosen.length > 0) {
|
|
1923
|
+
entries.push(...chosen);
|
|
1534
1924
|
toolsFound.push(source);
|
|
1535
1925
|
}
|
|
1536
1926
|
}
|
|
@@ -1539,14 +1929,12 @@ async function collectAll(onProgress) {
|
|
|
1539
1929
|
blocks.push(...cursor.blocks);
|
|
1540
1930
|
toolsFound.push("cursor");
|
|
1541
1931
|
}
|
|
1542
|
-
const { tools, skills,
|
|
1932
|
+
const { tools, skills, agent, sessionMessages, complete } = attribution;
|
|
1543
1933
|
onProgress?.(COLLECT_STAGES, COLLECT_STAGES, "");
|
|
1544
1934
|
const dedupedSessions = dedupeSessions(sessions).map((s) => {
|
|
1545
|
-
const title = titles.get(s.sessionId);
|
|
1546
1935
|
const messageCount = sessionMessages.get(s.sessionId);
|
|
1547
1936
|
return {
|
|
1548
1937
|
...s,
|
|
1549
|
-
...title ? { title } : {},
|
|
1550
1938
|
...messageCount ? { messageCount } : {}
|
|
1551
1939
|
};
|
|
1552
1940
|
});
|
|
@@ -1555,14 +1943,13 @@ async function collectAll(onProgress) {
|
|
|
1555
1943
|
// schema: entries ≤ 20000, sessions/blocks ≤ 10000), keeping the
|
|
1556
1944
|
// highest-token rows. Without this, a power user with >10000 distinct sessions
|
|
1557
1945
|
// would have their ENTIRE submit rejected with a 400 instead of a capped one.
|
|
1558
|
-
// tools/skills
|
|
1946
|
+
// tools/skills are already bounded upstream (attribution caps).
|
|
1559
1947
|
entries: capByTokens(dedupeDaily(entries), 2e4, entryTokens),
|
|
1560
1948
|
sessions: capByTokens(dedupedSessions, 1e4, entryTokens),
|
|
1561
1949
|
blocks: capByTokens(dedupeBlocks(blocks), 1e4, (b) => b.totalTokens),
|
|
1562
1950
|
toolsFound,
|
|
1563
1951
|
tools,
|
|
1564
1952
|
skills,
|
|
1565
|
-
projects,
|
|
1566
1953
|
agent,
|
|
1567
1954
|
attributionComplete: complete
|
|
1568
1955
|
};
|
|
@@ -5747,7 +6134,18 @@ var DailyUsageEntry = external_exports.object({
|
|
|
5747
6134
|
/** Where this entry came from. Defaults to the local CLI for back-compat. */
|
|
5748
6135
|
origin: UsageOrigin.default("cli"),
|
|
5749
6136
|
/** True when the numbers come from a provider's authoritative usage API. */
|
|
5750
|
-
verified: external_exports.boolean().default(false)
|
|
6137
|
+
verified: external_exports.boolean().default(false),
|
|
6138
|
+
/**
|
|
6139
|
+
* Structural fingerprint: the number of DISTINCT provider requests
|
|
6140
|
+
* (unique message-id + request-id pairs) that summed into this entry. A real
|
|
6141
|
+
* heavy day is the product of thousands of distinct API requests; a hand-typed
|
|
6142
|
+
* fabrication has none. The server uses this as an anti-fraud signal — a
|
|
6143
|
+
* billion-token day backed by ~zero real requests is the forgery signature.
|
|
6144
|
+
* Optional + back-compat: older CLIs and the ccusage fallback path (which
|
|
6145
|
+
* cannot see request ids) omit it, and an omitted fingerprint is never
|
|
6146
|
+
* penalized.
|
|
6147
|
+
*/
|
|
6148
|
+
requestCount: external_exports.number().int().nonnegative().optional()
|
|
5751
6149
|
});
|
|
5752
6150
|
var Timestamp = external_exports.string().min(1).max(40);
|
|
5753
6151
|
var SessionEntry = external_exports.object({
|
|
@@ -5760,8 +6158,6 @@ var SessionEntry = external_exports.object({
|
|
|
5760
6158
|
cacheReadTokens: tokenCount,
|
|
5761
6159
|
costUSD: external_exports.number().nonnegative(),
|
|
5762
6160
|
lastActivity: Timestamp,
|
|
5763
|
-
/** Human-readable AI-generated session title (from transcripts). Optional. */
|
|
5764
|
-
title: external_exports.string().max(200).optional(),
|
|
5765
6161
|
/** Number of assistant messages in this session (from transcripts). Optional. */
|
|
5766
6162
|
messageCount: external_exports.number().int().nonnegative().optional()
|
|
5767
6163
|
});
|
|
@@ -5778,11 +6174,6 @@ var ToolStat = external_exports.object({
|
|
|
5778
6174
|
/** Tokens burned on turns that used this tool (turn tokens split across its tool calls). Optional. */
|
|
5779
6175
|
tokens: external_exports.number().int().nonnegative().optional()
|
|
5780
6176
|
});
|
|
5781
|
-
var ProjectStat = external_exports.object({
|
|
5782
|
-
name: external_exports.string().min(1).max(128),
|
|
5783
|
-
tokens: external_exports.number().int().nonnegative(),
|
|
5784
|
-
costUSD: external_exports.number().nonnegative()
|
|
5785
|
-
});
|
|
5786
6177
|
var AgentStat = external_exports.object({
|
|
5787
6178
|
/** Total assistant messages across transcripts. */
|
|
5788
6179
|
messageCount: external_exports.number().int().nonnegative(),
|
|
@@ -5816,15 +6207,13 @@ var SubmitPayload = external_exports.object({
|
|
|
5816
6207
|
tools: external_exports.array(ToolStat).max(300).optional(),
|
|
5817
6208
|
/** Optional skill-usage frequencies parsed from local transcripts. */
|
|
5818
6209
|
skills: external_exports.array(SkillStat).max(300).optional(),
|
|
5819
|
-
/** Optional per-project usage totals parsed from local transcripts. */
|
|
5820
|
-
projects: external_exports.array(ProjectStat).max(500).optional(),
|
|
5821
6210
|
/** Optional subagent-vs-main rollup parsed from local transcripts. */
|
|
5822
6211
|
agent: AgentStat.optional(),
|
|
5823
6212
|
/**
|
|
5824
6213
|
* Set when the transcript scan completed within its time budget, i.e. the
|
|
5825
|
-
* tool/skill/
|
|
5826
|
-
*
|
|
5827
|
-
*
|
|
6214
|
+
* tool/skill/agent rollups are a FULL snapshot. The server refreshes the
|
|
6215
|
+
* dashboard breakdowns unconditionally for a full snapshot; for a partial one
|
|
6216
|
+
* (flag absent/false) it keeps its no-shrink guard. Back-compat: omittable.
|
|
5828
6217
|
*/
|
|
5829
6218
|
attributionComplete: external_exports.boolean().optional(),
|
|
5830
6219
|
/** Optional friends-board code (from `--board=<code>`): auto-join this board on submit. */
|
|
@@ -5837,7 +6226,16 @@ var SubmitPayload = external_exports.object({
|
|
|
5837
6226
|
* never needs it, and a wrong/missing code only skips the org attach (the
|
|
5838
6227
|
* personal submit still succeeds).
|
|
5839
6228
|
*/
|
|
5840
|
-
orgCode: external_exports.string().min(1).max(64).optional()
|
|
6229
|
+
orgCode: external_exports.string().min(1).max(64).optional(),
|
|
6230
|
+
/**
|
|
6231
|
+
* The submitter's local UTC offset in minutes EAST of UTC (IST = +330,
|
|
6232
|
+
* US-Pacific DST = -420; i.e. `-new Date().getTimezoneOffset()`). ccusage dates
|
|
6233
|
+
* usage in this local zone, so the server uses the offset to compute each
|
|
6234
|
+
* member's daily/weekly leaderboard window in THEIR calendar day rather than a
|
|
6235
|
+
* UTC one. Back-compat: omittable — older CLIs don't send it and the board falls
|
|
6236
|
+
* back to its default offset. Range clamps to real zones (UTC-14..+14).
|
|
6237
|
+
*/
|
|
6238
|
+
tzOffsetMinutes: external_exports.number().int().min(-840).max(840).optional()
|
|
5841
6239
|
});
|
|
5842
6240
|
var AnonSubmitPayload = SubmitPayload.extend({
|
|
5843
6241
|
/** Client-generated secret (hex). The server stores only its hash. */
|
|
@@ -5923,7 +6321,16 @@ var OrgProvisionInput = external_exports.object({
|
|
|
5923
6321
|
ownerEmail: external_exports.string().email().max(254).optional(),
|
|
5924
6322
|
description: external_exports.string().max(2e3).optional(),
|
|
5925
6323
|
boardVisibility: OrgBoardVisibility.optional(),
|
|
5926
|
-
window: OrgWindow.optional()
|
|
6324
|
+
window: OrgWindow.optional(),
|
|
6325
|
+
/** Brand accent applied to the org homepage theming. */
|
|
6326
|
+
accentColor: HexColor.optional(),
|
|
6327
|
+
/**
|
|
6328
|
+
* Create the org UNOWNED and mint a one-time owner-claim ("admin sign-in")
|
|
6329
|
+
* token in the same call. The first person to open the link + sign in becomes
|
|
6330
|
+
* the owner; the token is then consumed. When set, an owner email/handle is
|
|
6331
|
+
* NOT required — the link is how the first admin is assigned.
|
|
6332
|
+
*/
|
|
6333
|
+
issueClaimLink: external_exports.boolean().optional()
|
|
5927
6334
|
});
|
|
5928
6335
|
var OrgSettingsInput = external_exports.object({
|
|
5929
6336
|
name: external_exports.string().min(1).max(120).optional(),
|
|
@@ -5949,18 +6356,21 @@ function formatTokens(n) {
|
|
|
5949
6356
|
function formatUSD(n) {
|
|
5950
6357
|
return `$${n.toLocaleString("en-US", { maximumFractionDigits: 2 })}`;
|
|
5951
6358
|
}
|
|
6359
|
+
function sanitizeServerText(s) {
|
|
6360
|
+
return s.replace(/[\u0000-\u001f\u007f-\u009f]/g, "");
|
|
6361
|
+
}
|
|
5952
6362
|
function submitNextStepLines(result) {
|
|
5953
6363
|
if (result.boardUrl) {
|
|
5954
6364
|
const code = result.boardCode ?? result.boardUrl.split("/").filter(Boolean).pop() ?? "";
|
|
5955
6365
|
return [
|
|
5956
|
-
` \u{1F91D} You're on the board: ${result.boardUrl}`,
|
|
6366
|
+
` \u{1F91D} You're on the board: ${sanitizeServerText(result.boardUrl)}`,
|
|
5957
6367
|
" \u2192 Open it to see who burned more.",
|
|
5958
|
-
` \u2192 Get a friend on it \u2014 have them run: npx whoburnedmore --board=${code}`,
|
|
6368
|
+
` \u2192 Get a friend on it \u2014 have them run: npx whoburnedmore --board=${sanitizeServerText(code)}`,
|
|
5959
6369
|
" \u2192 Sign in on the page and add your X to claim your spot and own your rank."
|
|
5960
6370
|
];
|
|
5961
6371
|
}
|
|
5962
6372
|
return [
|
|
5963
|
-
` Your dashboard: ${result.dashboardUrl}`,
|
|
6373
|
+
` Your dashboard: ${sanitizeServerText(result.dashboardUrl)}`,
|
|
5964
6374
|
" \u2192 Sign in and add your X on the page to get on the leaderboard and claim your rank.",
|
|
5965
6375
|
" Private until you do. Manage anytime: `npx whoburnedmore private` \xB7 `public` \xB7 `remove`."
|
|
5966
6376
|
];
|
|
@@ -5968,7 +6378,7 @@ function submitNextStepLines(result) {
|
|
|
5968
6378
|
|
|
5969
6379
|
// src/local-dashboard.ts
|
|
5970
6380
|
function esc(s) {
|
|
5971
|
-
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
6381
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
5972
6382
|
}
|
|
5973
6383
|
function renderDashboardHtml(entries, generatedAt = /* @__PURE__ */ new Date(), connect) {
|
|
5974
6384
|
const today = generatedAt.toISOString().slice(0, 10);
|
|
@@ -6181,8 +6591,12 @@ async function publishLocal(payload, deps) {
|
|
|
6181
6591
|
}
|
|
6182
6592
|
const key = deps.ensureAnonKey();
|
|
6183
6593
|
const res = await deps.anonSubmit(key, payload);
|
|
6184
|
-
deps.log(` Published \u2014 you're on the board: ${res.dashboardUrl}`);
|
|
6185
|
-
|
|
6594
|
+
deps.log(` Published \u2014 you're on the board: ${sanitizeServerText(res.dashboardUrl)}`);
|
|
6595
|
+
if (isTrustedWebUrl(res.dashboardUrl)) {
|
|
6596
|
+
deps.openBrowser(claimUrl(res.dashboardUrl, key));
|
|
6597
|
+
} else {
|
|
6598
|
+
deps.log(" (The server returned an unexpected address, so it was not auto-opened.)");
|
|
6599
|
+
}
|
|
6186
6600
|
deps.log(
|
|
6187
6601
|
" Sign in there to claim it, or `npx whoburnedmore private` to hide it again."
|
|
6188
6602
|
);
|
|
@@ -6264,7 +6678,7 @@ async function confirm(question) {
|
|
|
6264
6678
|
function showLocalDashboard(payload) {
|
|
6265
6679
|
const dir = defaultConfigDir();
|
|
6266
6680
|
mkdirSync3(dir, { recursive: true });
|
|
6267
|
-
const file =
|
|
6681
|
+
const file = join9(dir, "dashboard.html");
|
|
6268
6682
|
writeFileSync3(
|
|
6269
6683
|
file,
|
|
6270
6684
|
renderDashboardHtml(payload.entries, /* @__PURE__ */ new Date(), {
|
|
@@ -6299,7 +6713,7 @@ async function run(flags) {
|
|
|
6299
6713
|
} finally {
|
|
6300
6714
|
progress.stop();
|
|
6301
6715
|
}
|
|
6302
|
-
const { entries, sessions, blocks, tools, skills,
|
|
6716
|
+
const { entries, sessions, blocks, tools, skills, agent, attributionComplete } = collected;
|
|
6303
6717
|
if (entries.length === 0) {
|
|
6304
6718
|
console.log();
|
|
6305
6719
|
console.log(" Nothing to burn yet \u2014 no local usage found from any coding agent.");
|
|
@@ -6307,13 +6721,13 @@ async function run(flags) {
|
|
|
6307
6721
|
return;
|
|
6308
6722
|
}
|
|
6309
6723
|
const payload = { cliVersion: VERSION, entries };
|
|
6724
|
+
payload.tzOffsetMinutes = -(/* @__PURE__ */ new Date()).getTimezoneOffset();
|
|
6310
6725
|
if (sessions.length > 0) payload.sessions = sessions;
|
|
6311
6726
|
if (blocks.length > 0) payload.blocks = blocks;
|
|
6312
6727
|
if (tools.length > 0) payload.tools = tools;
|
|
6313
6728
|
if (skills.length > 0) payload.skills = skills;
|
|
6314
|
-
if (projects.length > 0) payload.projects = projects;
|
|
6315
6729
|
if (agent.messageCount > 0) payload.agent = agent;
|
|
6316
|
-
if (attributionComplete && (tools.length > 0 || skills.length > 0
|
|
6730
|
+
if (attributionComplete && (tools.length > 0 || skills.length > 0))
|
|
6317
6731
|
payload.attributionComplete = true;
|
|
6318
6732
|
applyScope(payload, flags);
|
|
6319
6733
|
if (flags.dryRun) {
|
|
@@ -6366,7 +6780,7 @@ async function run(flags) {
|
|
|
6366
6780
|
console.log(
|
|
6367
6781
|
pc2.dim(" The server returned an unexpected dashboard address, so it was NOT auto-opened. Open it yourself only if you trust it:")
|
|
6368
6782
|
);
|
|
6369
|
-
console.log(` ${baseUrl}`);
|
|
6783
|
+
console.log(` ${sanitizeServerText(baseUrl)}`);
|
|
6370
6784
|
}
|
|
6371
6785
|
}
|
|
6372
6786
|
const lines = submitNextStepLines(result);
|
|
@@ -6395,8 +6809,9 @@ async function linkServerInstall(token) {
|
|
|
6395
6809
|
}
|
|
6396
6810
|
const anonKey = ensureAnonKey();
|
|
6397
6811
|
const linked = await redeemServerInstall(token, anonKey);
|
|
6812
|
+
const handle = sanitizeServerText(linked.handle);
|
|
6398
6813
|
console.log(
|
|
6399
|
-
linked.alreadyLinked ? ` This machine is already linked to @${
|
|
6814
|
+
linked.alreadyLinked ? ` This machine is already linked to @${handle}.` : ` Linked this machine to @${handle}.`
|
|
6400
6815
|
);
|
|
6401
6816
|
if (linked.mergedDays > 0) {
|
|
6402
6817
|
console.log(pc2.dim(` Merged ${linked.mergedDays} existing usage day${linked.mergedDays === 1 ? "" : "s"} from this machine.`));
|
|
@@ -6419,7 +6834,7 @@ async function linkServerInstall(token) {
|
|
|
6419
6834
|
} catch {
|
|
6420
6835
|
console.log(pc2.dim(" Linked, but background sync could not be installed automatically. Run `npx whoburnedmore install-sync` to retry."));
|
|
6421
6836
|
}
|
|
6422
|
-
console.log(` Profile: ${linked.profileUrl}`);
|
|
6837
|
+
console.log(` Profile: ${sanitizeServerText(linked.profileUrl)}`);
|
|
6423
6838
|
}
|
|
6424
6839
|
function waitOrAbort(ms, signal) {
|
|
6425
6840
|
if (signal.aborted) return Promise.resolve();
|