whoburnedmore 0.9.17 → 0.9.19

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 +98 -46
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -238,7 +238,7 @@ import {
238
238
  writeFileSync as writeFileSync2
239
239
  } from "node:fs";
240
240
  import { homedir as homedir2, platform } from "node:os";
241
- import { dirname, join as join2, win32 } from "node:path";
241
+ import { dirname, join as join2, posix, win32 } from "node:path";
242
242
 
243
243
  // src/config.ts
244
244
  import { randomBytes } from "node:crypto";
@@ -417,9 +417,19 @@ ${envEntries}
417
417
  interval. Submits are idempotent server-side, so an extra run is safe. -->
418
418
  <key>RunAtLoad</key>
419
419
  <true/>
420
- <!-- Be a good citizen: macOS schedules this with background priority. -->
420
+ <!-- Standard, NOT Background. ProcessType=Background opts the job into
421
+ macOS I/O throttling, which is disastrous for a job whose work is almost
422
+ entirely reading thousands of transcript files: a full collect measured
423
+ ~51s unthrottled vs ~197s throttled on the same machine. That blows past
424
+ the collector's own budgets (25s per ccusage child, 45s for the native
425
+ reader), so every source times out at once, entries comes back empty and
426
+ the tick submits nothing \u2014 it logged "Nothing to burn yet" on 206 of 473
427
+ runs (~44%) until this was changed. Nice=0 keeps CPU priority neighbourly
428
+ without throttling the reads. -->
421
429
  <key>ProcessType</key>
422
- <string>Background</string>
430
+ <string>Standard</string>
431
+ <key>Nice</key>
432
+ <integer>0</integer>
423
433
  <key>StandardOutPath</key>
424
434
  <string>${xmlEscape(logPath)}</string>
425
435
  <key>StandardErrorPath</key>
@@ -460,7 +470,7 @@ function resolveNpmPath(opts) {
460
470
  for (const c of candidates) {
461
471
  if (check(c)) return c;
462
472
  }
463
- const sibling = os === "win32" ? win32.join(win32.dirname(execPath), "npm.cmd") : join2(dirname(execPath), "npm");
473
+ const sibling = os === "win32" ? win32.join(win32.dirname(execPath), "npm.cmd") : posix.join(posix.dirname(execPath), "npm");
464
474
  if (check(sibling)) return sibling;
465
475
  return os === "win32" ? "npm.cmd" : "npm";
466
476
  }
@@ -7595,7 +7605,7 @@ async function collectCursor() {
7595
7605
  // src/native/codex.ts
7596
7606
  import { readdir as readdir3 } from "node:fs/promises";
7597
7607
  import { homedir as homedir7 } from "node:os";
7598
- import { join as join9 } from "node:path";
7608
+ import { join as join9, resolve } from "node:path";
7599
7609
  function num5(n) {
7600
7610
  const v = Math.round(Number(n));
7601
7611
  return Number.isFinite(v) && v > 0 ? v : 0;
@@ -7715,9 +7725,12 @@ function finalizeCodexEntries(acc) {
7715
7725
  }
7716
7726
  return entries;
7717
7727
  }
7718
- function resolveCodexSessionsDir(env = process.env) {
7719
- const home = env.CODEX_HOME && env.CODEX_HOME.trim() ? env.CODEX_HOME.trim() : join9(homedir7(), ".codex");
7720
- return join9(home, "sessions");
7728
+ function resolveCodexHome(env = process.env) {
7729
+ return env.CODEX_HOME && env.CODEX_HOME.trim() ? env.CODEX_HOME.trim() : join9(homedir7(), ".codex");
7730
+ }
7731
+ function resolveCodexSessionsDirs(env = process.env) {
7732
+ const home = resolveCodexHome(env);
7733
+ return [join9(home, "sessions"), join9(home, "archived_sessions")];
7721
7734
  }
7722
7735
  async function listJsonl3(dir) {
7723
7736
  let dirents;
@@ -7769,8 +7782,8 @@ function fromCachedSession(t) {
7769
7782
  };
7770
7783
  }
7771
7784
  async function collectCodexNative(env = process.env, opts = {}) {
7772
- const dir = resolveCodexSessionsDir(env);
7773
- const files = await listJsonl3(dir);
7785
+ const dirs = resolveCodexSessionsDirs(env);
7786
+ const files = (await Promise.all(dirs.map(listJsonl3))).flat();
7774
7787
  if (files.length === 0) return { entries: [], found: false, filesScanned: 0 };
7775
7788
  const now = opts.now ?? Date.now;
7776
7789
  const res = await readFilesWithCache({
@@ -8147,6 +8160,10 @@ function reconcileProvenance(entries, agent, complete, store) {
8147
8160
 
8148
8161
  // src/collect.ts
8149
8162
  var execFileAsync = promisify(execFile);
8163
+ var NATIVE_COVERED_SOURCES = /* @__PURE__ */ new Set(["claude", "codex"]);
8164
+ var CCUSAGE_TIMEOUT_MS = 25e3;
8165
+ var CCUSAGE_FALLBACK_TIMEOUT_MS = NATIVE_READ_BUDGET_MS;
8166
+ var CCUSAGE_AGGREGATE_TIMEOUT_MS = 18e4;
8150
8167
  var SOURCES = [
8151
8168
  "claude",
8152
8169
  "codex",
@@ -8342,25 +8359,39 @@ function dedupeBlocks(blocks) {
8342
8359
  return [...byStart.values()];
8343
8360
  }
8344
8361
  function selectSourceEntries(source, ccusageEntries, native) {
8345
- if (source === "claude" && native.claude.found && native.claude.entries.length > 0)
8362
+ if (source === "claude" && nativeReaderWon(native.claude))
8346
8363
  return native.claude.entries;
8347
- if (source === "codex" && native.codex.found && native.codex.entries.length > 0)
8364
+ if (source === "codex" && nativeReaderWon(native.codex))
8348
8365
  return native.codex.entries;
8349
8366
  return ccusageEntries;
8350
8367
  }
8368
+ function nativeReaderWon(result) {
8369
+ return result.found && result.entries.length > 0;
8370
+ }
8371
+ function ccusageFallbackSources(native) {
8372
+ return SOURCES.filter(
8373
+ (s) => NATIVE_COVERED_SOURCES.has(s) && !nativeReaderWon(s === "claude" ? native.claude : native.codex)
8374
+ );
8375
+ }
8351
8376
  function ccusageClaudeEnv(env = process.env) {
8352
8377
  if (env.CLAUDE_CONFIG_DIR && env.CLAUDE_CONFIG_DIR.trim()) return env;
8353
8378
  return { ...env, CLAUDE_CONFIG_DIR: resolveClaudeConfigRoots(env).join(",") };
8354
8379
  }
8355
- async function runCcusageOnce(cmd, args, env) {
8380
+ function isRetryableCcusageFailure(err) {
8381
+ const e = err;
8382
+ if (e.killed === true) return false;
8383
+ return e.signal != null || typeof e.code === "string";
8384
+ }
8385
+ async function runCcusageOnce(cmd, args, env, timeoutMs = CCUSAGE_TIMEOUT_MS) {
8356
8386
  try {
8357
8387
  const { stdout } = await execFileAsync(cmd, args, {
8358
8388
  encoding: "utf8",
8359
8389
  maxBuffer: 64 * 1024 * 1024,
8360
- // A single source shouldn't be able to hang the whole run. 25s is plenty
8361
- // for a healthy local read; a hung source gets killed and (if transient)
8362
- // retried once below rather than stalling everything for minutes.
8363
- timeout: 25e3,
8390
+ // A single source shouldn't be able to hang the whole run: a hung source
8391
+ // gets killed and (if transient) retried once below rather than stalling
8392
+ // everything for minutes. The claude/codex fallback passes a longer cap —
8393
+ // see CCUSAGE_FALLBACK_TIMEOUT_MS.
8394
+ timeout: timeoutMs,
8364
8395
  ...env ? { env } : {}
8365
8396
  });
8366
8397
  if (!stdout) return { json: null, transient: false };
@@ -8370,15 +8401,13 @@ async function runCcusageOnce(cmd, args, env) {
8370
8401
  return { json: null, transient: false };
8371
8402
  }
8372
8403
  } catch (err) {
8373
- const e = err;
8374
- const transient = e.killed === true || e.signal != null || typeof e.code === "string";
8375
- return { json: null, transient };
8404
+ return { json: null, transient: isRetryableCcusageFailure(err) };
8376
8405
  }
8377
8406
  }
8378
- async function runCcusage(cmd, args, env) {
8379
- const first = await runCcusageOnce(cmd, args, env);
8407
+ async function runCcusage(cmd, args, env, timeoutMs = CCUSAGE_TIMEOUT_MS) {
8408
+ const first = await runCcusageOnce(cmd, args, env, timeoutMs);
8380
8409
  if (first.json !== null || !first.transient) return first.json;
8381
- return (await runCcusageOnce(cmd, args, env)).json;
8410
+ return (await runCcusageOnce(cmd, args, env, timeoutMs)).json;
8382
8411
  }
8383
8412
  var COLLECT_STAGES = SOURCES.length + 4 + VSCODE_AGENTS.length + 1;
8384
8413
  function isAuthoritativeScan(attributionComplete, ...fingerprintReaders) {
@@ -8397,27 +8426,39 @@ async function collectAll(onProgress) {
8397
8426
  () => ({ entries: [], found: false, filesScanned: 0, timedOut: true })
8398
8427
  );
8399
8428
  const sourceTasks = SOURCES.map(async (source) => {
8400
- const env = source === "claude" ? ccusageClaudeEnv() : void 0;
8401
- const json = await runCcusage(
8402
- cmd,
8403
- [...prefixArgs, source, "daily", "--json", "--offline"],
8404
- env
8405
- );
8429
+ if (NATIVE_COVERED_SOURCES.has(source)) {
8430
+ await (source === "claude" ? nativeClaudeTask : nativeCodexTask);
8431
+ tick();
8432
+ return { source, mapped: [] };
8433
+ }
8434
+ const json = await runCcusage(cmd, [
8435
+ ...prefixArgs,
8436
+ source,
8437
+ "daily",
8438
+ "--json",
8439
+ "--offline"
8440
+ ]);
8406
8441
  tick();
8407
8442
  return { source, mapped: json ? mapCcusageDaily(source, json) : [] };
8408
8443
  });
8409
- const sessionTask = runCcusage(cmd, [...prefixArgs, "session", "--json", "--offline"]).then(
8410
- (json) => {
8411
- tick();
8412
- return json ? mapCcusageSessions(json) : [];
8413
- }
8414
- );
8415
- const blockTask = runCcusage(cmd, [...prefixArgs, "blocks", "--json", "--offline"]).then(
8416
- (json) => {
8417
- tick();
8418
- return json ? mapCcusageBlocks(json) : [];
8419
- }
8420
- );
8444
+ const sessionTask = runCcusage(
8445
+ cmd,
8446
+ [...prefixArgs, "session", "--json", "--offline"],
8447
+ void 0,
8448
+ CCUSAGE_AGGREGATE_TIMEOUT_MS
8449
+ ).then((json) => {
8450
+ tick();
8451
+ return json ? mapCcusageSessions(json) : [];
8452
+ });
8453
+ const blockTask = runCcusage(
8454
+ cmd,
8455
+ [...prefixArgs, "blocks", "--json", "--offline"],
8456
+ void 0,
8457
+ CCUSAGE_AGGREGATE_TIMEOUT_MS
8458
+ ).then((json) => {
8459
+ tick();
8460
+ return json ? mapCcusageBlocks(json) : [];
8461
+ });
8421
8462
  const cursorTask = collectCursor().then((c) => {
8422
8463
  tick();
8423
8464
  return c;
@@ -8458,10 +8499,21 @@ async function collectAll(onProgress) {
8458
8499
  continueTask
8459
8500
  ]);
8460
8501
  const native = { claude: nativeClaude, codex: nativeCodex };
8502
+ const fallbacks = /* @__PURE__ */ new Map();
8503
+ for (const source of ccusageFallbackSources(native)) {
8504
+ const json = await runCcusage(
8505
+ cmd,
8506
+ [...prefixArgs, source, "daily", "--json", "--offline"],
8507
+ // For Claude, force ccusage to scan both config roots (dual-dir hardening).
8508
+ source === "claude" ? ccusageClaudeEnv() : void 0,
8509
+ CCUSAGE_FALLBACK_TIMEOUT_MS
8510
+ );
8511
+ fallbacks.set(source, json ? mapCcusageDaily(source, json) : []);
8512
+ }
8461
8513
  const entries = [];
8462
8514
  const toolsFound = [];
8463
8515
  for (const { source, mapped } of sourceResults) {
8464
- const chosen = selectSourceEntries(source, mapped, native);
8516
+ const chosen = selectSourceEntries(source, fallbacks.get(source) ?? mapped, native);
8465
8517
  if (chosen.length > 0) {
8466
8518
  entries.push(...chosen);
8467
8519
  toolsFound.push(source);
@@ -9079,7 +9131,7 @@ async function run(flags) {
9079
9131
  }
9080
9132
  }
9081
9133
  function sleep(ms) {
9082
- return new Promise((resolve) => setTimeout(resolve, ms));
9134
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
9083
9135
  }
9084
9136
  async function refreshCliTokenFromConfig() {
9085
9137
  const cfg = loadConfig();
@@ -9273,14 +9325,14 @@ async function linkServerInstall(token) {
9273
9325
  }
9274
9326
  function waitOrAbort(ms, signal) {
9275
9327
  if (signal.aborted) return Promise.resolve();
9276
- return new Promise((resolve) => {
9328
+ return new Promise((resolve2) => {
9277
9329
  const onAbort = () => {
9278
9330
  clearTimeout(timer);
9279
- resolve();
9331
+ resolve2();
9280
9332
  };
9281
9333
  const timer = setTimeout(() => {
9282
9334
  signal.removeEventListener("abort", onAbort);
9283
- resolve();
9335
+ resolve2();
9284
9336
  }, ms);
9285
9337
  signal.addEventListener("abort", onAbort, { once: true });
9286
9338
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whoburnedmore",
3
- "version": "0.9.17",
3
+ "version": "0.9.19",
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": {
@@ -10,7 +10,7 @@
10
10
  "dist/index.js"
11
11
  ],
12
12
  "scripts": {
13
- "build": "rm -rf dist && esbuild src/index.ts --bundle --platform=node --format=esm --target=node20 --outfile=dist/index.js --external:ccusage --external:picocolors --external:tokscale",
13
+ "build": "node scripts/clean.mjs && esbuild src/index.ts --bundle --platform=node --format=esm --target=node20 --outfile=dist/index.js --external:ccusage --external:picocolors --external:tokscale",
14
14
  "test": "vitest run",
15
15
  "lint": "tsc -p tsconfig.json --noEmit",
16
16
  "smoke:package": "node scripts/smoke-package.mjs",
@@ -20,7 +20,7 @@
20
20
  "release:major": "npm version major && npm publish"
21
21
  },
22
22
  "dependencies": {
23
- "ccusage": "20.0.9",
23
+ "ccusage": "20.0.19",
24
24
  "picocolors": "^1.1.1"
25
25
  },
26
26
  "devDependencies": {