whoburnedmore 0.9.25 → 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.
Files changed (2) hide show
  1. package/dist/index.js +137 -59
  2. package/package.json +1 -1
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 (valueFlags.has(a)) i++;
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) {
@@ -716,8 +767,13 @@ var FORWARDED_ENV_VARS = [
716
767
  // dashboard URL shown to the user
717
768
  "XDG_CONFIG_HOME",
718
769
  // base for the default config dir
719
- "CLAUDE_CONFIG_DIR"
770
+ "CLAUDE_CONFIG_DIR",
720
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"
721
777
  ];
722
778
  function syncEnv(opts) {
723
779
  const env = opts?.env ?? process.env;
@@ -6549,6 +6605,7 @@ function normalizeModelKey(model) {
6549
6605
  /^(?:us|eu|au|jp|apac|global|anthropic|amazon|meta|openai|google|vertex|azure|mistral)\./.test(prefix) ? "" : prefix
6550
6606
  ));
6551
6607
  k = k.replace(/:\d+$/, "").replace(/:latest$/, "");
6608
+ k = k.replace(/\[[^\]]*\]$/, "");
6552
6609
  return k;
6553
6610
  }
6554
6611
  function lookup(key) {
@@ -6564,7 +6621,7 @@ function resolveModelPrice(model) {
6564
6621
  resolveCache.set(model, resolved);
6565
6622
  return resolved;
6566
6623
  }
6567
- function resolveModelPriceUncached(model) {
6624
+ function exactCandidates(model) {
6568
6625
  const norm2 = normalizeModelKey(model);
6569
6626
  const candidates = [norm2];
6570
6627
  const dateless = norm2.replace(/[-@]20\d{6}$/, "").replace(/-20\d{2}-\d{2}-\d{2}$/, "");
@@ -6572,12 +6629,19 @@ function resolveModelPriceUncached(model) {
6572
6629
  candidates.push(dateless);
6573
6630
  if (norm2.endsWith("-latest"))
6574
6631
  candidates.push(norm2.slice(0, -"-latest".length));
6575
- let row;
6576
- for (const c of candidates) {
6577
- row = lookup(c);
6632
+ return { candidates, dateless };
6633
+ }
6634
+ function lookupExact(model) {
6635
+ for (const c of exactCandidates(model).candidates) {
6636
+ const row = lookup(c);
6578
6637
  if (row)
6579
- break;
6638
+ return row;
6580
6639
  }
6640
+ return void 0;
6641
+ }
6642
+ function resolveModelPriceUncached(model) {
6643
+ const { dateless } = exactCandidates(model);
6644
+ let row = lookupExact(model);
6581
6645
  if (!row) {
6582
6646
  let base = dateless;
6583
6647
  for (let i = 0; i < 3 && !row; i++) {
@@ -7225,6 +7289,7 @@ function parseClaudeLine(raw) {
7225
7289
  } catch {
7226
7290
  return null;
7227
7291
  }
7292
+ if (!obj || typeof obj !== "object") return null;
7228
7293
  const message = obj.message;
7229
7294
  if (!message || typeof message !== "object") return null;
7230
7295
  const usage = message.usage;
@@ -7506,6 +7571,7 @@ function inspectCodexRollout(lines) {
7506
7571
  } catch {
7507
7572
  continue;
7508
7573
  }
7574
+ if (!obj || typeof obj !== "object") continue;
7509
7575
  const payload = obj.payload;
7510
7576
  if (!payload || typeof payload !== "object") continue;
7511
7577
  const kind = obj.type;
@@ -8453,6 +8519,7 @@ function mapCursorEvents(events) {
8453
8519
  }
8454
8520
  async function fetchCursorEvents(cookie, maxPages = 30, pageSize = 500) {
8455
8521
  const all = [];
8522
+ let reachedEnd = false;
8456
8523
  for (let page = 1; page <= maxPages; page++) {
8457
8524
  const res = await fetch(EVENTS_URL, {
8458
8525
  method: "POST",
@@ -8474,9 +8541,18 @@ async function fetchCursorEvents(cookie, maxPages = 30, pageSize = 500) {
8474
8541
  );
8475
8542
  const batch = body.usageEventsDisplay ?? [];
8476
8543
  all.push(...batch);
8477
- if (batch.length < pageSize) break;
8544
+ if (batch.length < pageSize) {
8545
+ reachedEnd = true;
8546
+ break;
8547
+ }
8478
8548
  }
8479
- return all;
8549
+ return reachedEnd ? all : dropBoundaryDay(all);
8550
+ }
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);
8480
8556
  }
8481
8557
  async function collectCursor(opts) {
8482
8558
  try {
@@ -9013,6 +9089,14 @@ function normCost(n) {
9013
9089
  return Number.isFinite(v) && v > 0 ? v : 0;
9014
9090
  }
9015
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
+ }
9016
9100
  function mapCcusageDaily(tool, json, now = Date.now()) {
9017
9101
  const daily = json?.daily;
9018
9102
  if (!Array.isArray(daily)) return [];
@@ -9070,43 +9154,13 @@ function mapCcusageDaily(tool, json, now = Date.now()) {
9070
9154
  }
9071
9155
  for (const c of candidates) {
9072
9156
  const tokens = c.inputTokens + c.outputTokens + c.cacheCreationTokens + c.cacheReadTokens;
9073
- if (tokens === 0 && c.costUSD === 0) continue;
9074
- const costUSD2 = c.costUSD === 0 && PRICE_TABLE_COST_SOURCES.has(tool) ? estimateCostUSD(c.model, c) : c.costUSD;
9157
+ const costUSD2 = repriceCcusageCost(tool, c.model, c, c.costUSD);
9158
+ if (tokens === 0 && costUSD2 === 0) continue;
9075
9159
  entries.push({ date, tool, ...c, costUSD: costUSD2, origin: "cli", verified: false });
9076
9160
  }
9077
9161
  }
9078
9162
  return entries;
9079
9163
  }
9080
- function mapCcusageSessions(json) {
9081
- const sessions = json?.session;
9082
- if (!Array.isArray(sessions)) return [];
9083
- const out = [];
9084
- for (const raw of sessions) {
9085
- const s = raw;
9086
- const sessionId = typeof s.period === "string" ? s.period : typeof s.sessionId === "string" ? s.sessionId : null;
9087
- if (!sessionId) continue;
9088
- const inputTokens = norm(s.inputTokens);
9089
- const outputTokens = norm(s.outputTokens);
9090
- const cacheCreationTokens = norm(s.cacheCreationTokens);
9091
- const cacheReadTokens = norm(s.cacheReadTokens);
9092
- if (inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens === 0)
9093
- continue;
9094
- const models = Array.isArray(s.modelsUsed) ? s.modelsUsed.filter((m) => typeof m === "string") : [];
9095
- const meta = s.metadata ?? {};
9096
- out.push({
9097
- sessionId: sessionId.slice(0, 128),
9098
- tool: typeof s.agent === "string" ? s.agent : "unknown",
9099
- model: models[0] ?? "unknown",
9100
- inputTokens,
9101
- outputTokens,
9102
- cacheCreationTokens,
9103
- cacheReadTokens,
9104
- costUSD: normCost(s.totalCost ?? s.costUSD),
9105
- lastActivity: typeof meta.lastActivity === "string" ? meta.lastActivity : (/* @__PURE__ */ new Date()).toISOString()
9106
- });
9107
- }
9108
- return out;
9109
- }
9110
9164
  function mapCcusageBlocks(json) {
9111
9165
  const blocks = json?.blocks;
9112
9166
  if (!Array.isArray(blocks)) return [];
@@ -9334,14 +9388,9 @@ async function collectAll(onProgress, opts) {
9334
9388
  return { source, mapped: json ? mapCcusageDaily(source, json) : [] };
9335
9389
  }
9336
9390
  );
9337
- const sessionTask = runCcusage(
9338
- cmd,
9339
- [...prefixArgs, "session", "--json", "--offline"],
9340
- void 0,
9341
- CCUSAGE_AGGREGATE_TIMEOUT_MS
9342
- ).then((json) => {
9391
+ const sessionTask = Promise.resolve().then(() => {
9343
9392
  tick();
9344
- return json ? mapCcusageSessions(json) : [];
9393
+ return [];
9345
9394
  });
9346
9395
  const blockTask = runCcusage(
9347
9396
  cmd,
@@ -9662,8 +9711,12 @@ function writeLocalDashboard(dir, html) {
9662
9711
  function esc(s) {
9663
9712
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
9664
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
+ }
9665
9718
  function renderDashboardHtml(entries, generatedAt = /* @__PURE__ */ new Date(), connect) {
9666
- const today = generatedAt.toISOString().slice(0, 10);
9719
+ const today = localDay(generatedAt);
9667
9720
  const totals = {
9668
9721
  tokens: 0,
9669
9722
  cost: 0,
@@ -9699,7 +9752,9 @@ function renderDashboardHtml(entries, generatedAt = /* @__PURE__ */ new Date(),
9699
9752
  const avgPerDay = activeDays > 0 ? totals.tokens / activeDays : 0;
9700
9753
  const days = [];
9701
9754
  for (let i = 59; i >= 0; i--) {
9702
- const d = new Date(generatedAt.getTime() - i * 864e5).toISOString().slice(0, 10);
9755
+ const day = new Date(generatedAt);
9756
+ day.setDate(day.getDate() - i);
9757
+ const d = localDay(day);
9703
9758
  days.push({ date: d, tokens: byDate.get(d) ?? 0 });
9704
9759
  }
9705
9760
  const maxDay = Math.max(1, ...days.map((d) => d.tokens));
@@ -9807,7 +9862,7 @@ function renderDashboardHtml(entries, generatedAt = /* @__PURE__ */ new Date(),
9807
9862
  <div class="flame">\u{1F525}</div>
9808
9863
  <div>
9809
9864
  <h1>your local burn</h1>
9810
- <div class="sub">generated ${esc(generatedAt.toISOString().slice(0, 16).replace("T", " "))} \xB7 nothing left your machine</div>
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>
9811
9866
  </div>
9812
9867
  </header>
9813
9868
  ${connectCta}
@@ -9960,11 +10015,11 @@ function openBrowser(url) {
9960
10015
  async function confirm(question, defaultYes = true) {
9961
10016
  if (!process.stdin.isTTY) return false;
9962
10017
  const rl = createInterface({ input: process.stdin, output: process.stdout });
9963
- const hint = defaultYes ? "[Y/n]" : "[y/N]";
9964
- const answer = (await rl.question(`${question} ${pc2.dim(hint)} `)).trim();
10018
+ const answer = (await rl.question(`${question} ${pc2.dim(defaultYes ? "[Y/n]" : "[y/N]")} `)).trim();
9965
10019
  rl.close();
9966
- return answer === "" ? defaultYes : /^y(es)?$/i.test(answer);
10020
+ return confirmAnswer(answer, defaultYes);
9967
10021
  }
10022
+ var confirmDefaultNo = (question) => confirm(question, false);
9968
10023
  function showLocalDashboard(payload) {
9969
10024
  const dir = defaultConfigDir();
9970
10025
  const file = writeLocalDashboard(
@@ -10056,7 +10111,7 @@ async function run(flags) {
10056
10111
  showLocalDashboard(payload);
10057
10112
  if (!flags.quiet && process.stdin.isTTY) {
10058
10113
  await publishLocal(payload, {
10059
- confirm,
10114
+ confirm: confirmDefaultNo,
10060
10115
  // Stored token, then a silent refresh, then device sign-in — the same
10061
10116
  // order as every other signed-in command.
10062
10117
  signIn: () => requireSignedIn("--local"),
@@ -10410,8 +10465,23 @@ async function runVerify() {
10410
10465
  return;
10411
10466
  }
10412
10467
  }
10413
- console.log(pc2.dim(" Reading your local Claude Code logs\u2026"));
10414
- 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
+ }
10415
10485
  if (!found || requests.length === 0) {
10416
10486
  console.log(
10417
10487
  pc2.yellow(" No local Claude Code logs found on this machine to verify.")
@@ -10545,7 +10615,7 @@ async function runRemove() {
10545
10615
  console.log(
10546
10616
  pc2.dim(" Your local logs are untouched, and the account itself survives (delete it from your dashboard settings on the web).")
10547
10617
  );
10548
- if (!await confirm(" Delete your usage data?")) {
10618
+ if (!await confirmDefaultNo(" Delete your usage data?")) {
10549
10619
  console.log(pc2.dim(" Cancelled \u2014 nothing was deleted."));
10550
10620
  return;
10551
10621
  }
@@ -10572,6 +10642,7 @@ async function runRemove() {
10572
10642
  );
10573
10643
  console.log(pc2.dim(" Run `npx whoburnedmore` anytime to start fresh."));
10574
10644
  }
10645
+ var VERIFY_READ_BUDGET_MS = 18e4;
10575
10646
  async function main() {
10576
10647
  const major = Number(process.versions.node.split(".")[0]);
10577
10648
  if (major < 20) {
@@ -10581,6 +10652,13 @@ async function main() {
10581
10652
  }
10582
10653
  const args = process.argv.slice(2);
10583
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
+ }
10584
10662
  const command = args.includes("--watch") && (baseCommand === "run" || baseCommand === "sync") ? "daemon" : baseCommand;
10585
10663
  const flags = {
10586
10664
  dryRun: args.includes("--dry-run"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whoburnedmore",
3
- "version": "0.9.25",
3
+ "version": "0.9.26",
4
4
  "description": "Find out who burned more — submit your AI coding-agent token usage to the public leaderboard at whoburnedmore.com",
5
5
  "type": "module",
6
6
  "bin": {