whoburnedmore 0.8.0 → 0.8.1

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 +115 -68
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -258,29 +258,27 @@ function autoSyncInstalled() {
258
258
 
259
259
  // src/banner.ts
260
260
  import pc from "picocolors";
261
- var ART = [
262
- "\u2588\u2588\u2588\u2588\u2588 \u2588 \u2588 \u2588\u2588\u2588\u2588\u2588 \u2588 \u2588",
263
- "\u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588\u2588 \u2588",
264
- "\u2588\u2588\u2588\u2588\u2588 \u2588 \u2588 \u2588\u2588\u2588\u2588 \u2588 \u2588 \u2588",
265
- "\u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588 \u2588\u2588",
266
- "\u2588\u2588\u2588\u2588\u2588 \u2588\u2588\u2588\u2588\u2588 \u2588 \u2588 \u2588 \u2588"
267
- ];
268
- var SHADES = [226, 220, 214, 208, 202];
261
+ var WORD = "whoburnedmore";
262
+ var SHADES = [226, 220, 214, 208, 202, 196];
263
+ function paintWordmark() {
264
+ if (!pc.isColorSupported) return `${WORD}?`;
265
+ const letters = [...WORD].map((ch, i) => `\x1B[1;38;5;${SHADES[i % SHADES.length]}m${ch}`).join("");
266
+ return `${letters}\x1B[1;38;5;208m?\x1B[0m`;
267
+ }
269
268
  function printBanner() {
270
- const color = pc.isColorSupported;
269
+ const rule = pc.dim("\u2500".repeat(30));
271
270
  console.log();
272
- ART.forEach((row, i) => {
273
- console.log(" " + (color ? `\x1B[1;38;5;${SHADES[i]}m${row}\x1B[0m` : row));
274
- });
275
- const mark = color ? `\x1B[1;38;5;208m\u{1F525} whoburnedmore\x1B[0m` : "\u{1F525} whoburnedmore";
276
- console.log(` ${mark} ${pc.dim("\xB7 who burned more?")}`);
271
+ console.log(` \u{1F525} ${paintWordmark()}`);
272
+ console.log(` ${rule}`);
273
+ console.log(` ${pc.dim("who burned more \u2014 you, or them?")}`);
277
274
  console.log();
278
275
  }
279
276
 
280
277
  // src/collect.ts
281
- import { spawnSync as spawnSync4 } from "node:child_process";
278
+ import { execFile } from "node:child_process";
282
279
  import { createRequire as createRequire3 } from "node:module";
283
280
  import { dirname as dirname2, join as join6 } from "node:path";
281
+ import { promisify } from "node:util";
284
282
 
285
283
  // src/attribution.ts
286
284
  import { readFileSync as readFileSync2, readdirSync, statSync } from "node:fs";
@@ -311,7 +309,7 @@ var CLAUDE_PROJECTS = join3(homedir3(), ".claude", "projects");
311
309
  var CODEX_SESSIONS = join3(homedir3(), ".codex", "sessions");
312
310
  var MAX_FILES = 5e3;
313
311
  var MAX_FILE_BYTES = 64 * 1024 * 1024;
314
- var TIME_BUDGET_MS = 3e4;
312
+ var TIME_BUDGET_MS = 12e3;
315
313
  var MAX_STATS = 300;
316
314
  var MAX_PROJECTS = 500;
317
315
  function createAccumulator() {
@@ -563,10 +561,17 @@ function listTranscripts(dir) {
563
561
  walk(dir);
564
562
  return out.sort((a, b) => b.mtime - a.mtime).slice(0, MAX_FILES).map((f) => f.path);
565
563
  }
566
- function collectAttribution() {
564
+ async function collectAttribution() {
567
565
  const acc = createAccumulator();
568
566
  const deadline = Date.now() + TIME_BUDGET_MS;
569
567
  let complete = true;
568
+ let sinceYield = 0;
569
+ const breathe = async () => {
570
+ if (++sinceYield >= 8) {
571
+ sinceYield = 0;
572
+ await new Promise((r) => setImmediate(r));
573
+ }
574
+ };
570
575
  try {
571
576
  for (const dir of claudeProjectDirs()) {
572
577
  for (const file of listTranscripts(dir)) {
@@ -582,6 +587,7 @@ function collectAttribution() {
582
587
  } catch {
583
588
  }
584
589
  }
590
+ await breathe();
585
591
  }
586
592
  }
587
593
  for (const file of listTranscripts(CODEX_SESSIONS)) {
@@ -597,6 +603,7 @@ function collectAttribution() {
597
603
  } catch {
598
604
  }
599
605
  }
606
+ await breathe();
600
607
  }
601
608
  } catch {
602
609
  complete = false;
@@ -855,6 +862,7 @@ async function collectCursor() {
855
862
  }
856
863
 
857
864
  // src/collect.ts
865
+ var execFileAsync = promisify(execFile);
858
866
  var SOURCES = [
859
867
  "claude",
860
868
  "codex",
@@ -1036,64 +1044,85 @@ function dedupeBlocks(blocks) {
1036
1044
  }
1037
1045
  return [...byStart.values()];
1038
1046
  }
1039
- function runCcusageOnce(cmd, args) {
1040
- const res = spawnSync4(cmd, args, {
1041
- encoding: "utf8",
1042
- maxBuffer: 64 * 1024 * 1024,
1043
- timeout: 12e4
1044
- });
1045
- const transient = res.signal != null || res.error != null;
1046
- if (res.status !== 0 || !res.stdout) return { json: null, transient };
1047
+ async function runCcusageOnce(cmd, args) {
1047
1048
  try {
1048
- return { json: JSON.parse(res.stdout), transient: false };
1049
- } catch {
1050
- return { json: null, transient: false };
1049
+ const { stdout } = await execFileAsync(cmd, args, {
1050
+ encoding: "utf8",
1051
+ maxBuffer: 64 * 1024 * 1024,
1052
+ // A single source shouldn't be able to hang the whole run. 25s is plenty
1053
+ // for a healthy local read; a hung source gets killed and (if transient)
1054
+ // retried once below rather than stalling everything for minutes.
1055
+ timeout: 25e3
1056
+ });
1057
+ if (!stdout) return { json: null, transient: false };
1058
+ try {
1059
+ return { json: JSON.parse(stdout), transient: false };
1060
+ } catch {
1061
+ return { json: null, transient: false };
1062
+ }
1063
+ } catch (err) {
1064
+ const e = err;
1065
+ const transient = e.killed === true || e.signal != null || typeof e.code === "string";
1066
+ return { json: null, transient };
1051
1067
  }
1052
1068
  }
1053
- function runCcusage(cmd, args) {
1054
- const first = runCcusageOnce(cmd, args);
1069
+ async function runCcusage(cmd, args) {
1070
+ const first = await runCcusageOnce(cmd, args);
1055
1071
  if (first.json !== null || !first.transient) return first.json;
1056
- return runCcusageOnce(cmd, args).json;
1072
+ return (await runCcusageOnce(cmd, args)).json;
1057
1073
  }
1058
1074
  var COLLECT_STAGES = SOURCES.length + 4;
1059
1075
  async function collectAll(onProgress) {
1060
1076
  const { cmd, prefixArgs } = resolveCcusageBin();
1077
+ let done = 0;
1078
+ const tick = () => onProgress?.(++done, COLLECT_STAGES, "");
1079
+ const sourceTasks = SOURCES.map(async (source) => {
1080
+ const json = await runCcusage(cmd, [...prefixArgs, source, "daily", "--json", "--offline"]);
1081
+ tick();
1082
+ return { source, mapped: json ? mapCcusageDaily(source, json) : [] };
1083
+ });
1084
+ const sessionTask = runCcusage(cmd, [...prefixArgs, "session", "--json", "--offline"]).then(
1085
+ (json) => {
1086
+ tick();
1087
+ return json ? mapCcusageSessions(json) : [];
1088
+ }
1089
+ );
1090
+ const blockTask = runCcusage(cmd, [...prefixArgs, "blocks", "--json", "--offline"]).then(
1091
+ (json) => {
1092
+ tick();
1093
+ return json ? mapCcusageBlocks(json) : [];
1094
+ }
1095
+ );
1096
+ const cursorTask = collectCursor().then((c) => {
1097
+ tick();
1098
+ return c;
1099
+ });
1100
+ const attributionTask = collectAttribution().then((a) => {
1101
+ tick();
1102
+ return a;
1103
+ });
1104
+ const [sourceResults, sessions, blocks, cursor, attribution] = await Promise.all([
1105
+ Promise.all(sourceTasks),
1106
+ sessionTask,
1107
+ blockTask,
1108
+ cursorTask,
1109
+ attributionTask
1110
+ ]);
1061
1111
  const entries = [];
1062
1112
  const toolsFound = [];
1063
- let done = 0;
1064
- const tick = (label) => onProgress?.(done++, COLLECT_STAGES, label);
1065
- for (const source of SOURCES) {
1066
- tick(`reading ${source}`);
1067
- const json = runCcusage(cmd, [
1068
- ...prefixArgs,
1069
- source,
1070
- "daily",
1071
- "--json",
1072
- "--offline"
1073
- ]);
1074
- if (!json) continue;
1075
- const mapped = mapCcusageDaily(source, json);
1113
+ for (const { source, mapped } of sourceResults) {
1076
1114
  if (mapped.length > 0) {
1077
1115
  entries.push(...mapped);
1078
1116
  toolsFound.push(source);
1079
1117
  }
1080
1118
  }
1081
- tick("reading conversations");
1082
- const sessionJson = runCcusage(cmd, [...prefixArgs, "session", "--json", "--offline"]);
1083
- tick("reading active blocks");
1084
- const blockJson = runCcusage(cmd, [...prefixArgs, "blocks", "--json", "--offline"]);
1085
- const sessions = sessionJson ? mapCcusageSessions(sessionJson) : [];
1086
- const blocks = blockJson ? mapCcusageBlocks(blockJson) : [];
1087
- tick("reading cursor");
1088
- const cursor = await collectCursor();
1089
1119
  if (cursor.found) {
1090
1120
  entries.push(...cursor.entries);
1091
1121
  blocks.push(...cursor.blocks);
1092
1122
  toolsFound.push("cursor");
1093
1123
  }
1094
- tick("reading agent transcripts");
1095
- const { tools, skills, projects, agent, titles, sessionMessages, complete } = collectAttribution();
1096
- onProgress?.(done, COLLECT_STAGES, "done");
1124
+ const { tools, skills, projects, agent, titles, sessionMessages, complete } = attribution;
1125
+ onProgress?.(COLLECT_STAGES, COLLECT_STAGES, "");
1097
1126
  const dedupedSessions = dedupeSessions(sessions).map((s) => {
1098
1127
  const title = titles.get(s.sessionId);
1099
1128
  const messageCount = sessionMessages.get(s.sessionId);
@@ -5551,6 +5580,16 @@ async function publishLocal(payload, deps) {
5551
5580
  // src/index.ts
5552
5581
  var require2 = createRequire4(import.meta.url);
5553
5582
  var VERSION = require2("../package.json").version;
5583
+ var LOADING_VIBES = [
5584
+ "counting up your token usage, right here on your machine\u2026",
5585
+ "tallying tokens across every coding agent you use\u2026",
5586
+ "adding up cache reads, writes & all the burn\u2026",
5587
+ "tokens & totals only \u2014 never your prompts or code\u2026",
5588
+ "summing up your usage, model by model\u2026",
5589
+ "working out what all that burn cost you \u{1F525}",
5590
+ "crunching your daily token totals\u2026",
5591
+ "almost there \u2014 adding it all up\u2026"
5592
+ ];
5554
5593
  function startProgress() {
5555
5594
  if (!process.stdout.isTTY) {
5556
5595
  let lastLogged = -1;
@@ -5559,7 +5598,7 @@ function startProgress() {
5559
5598
  const pct = Math.round(done / total * 100);
5560
5599
  if (pct >= lastLogged + 25 || pct === 100 && lastLogged < 100) {
5561
5600
  lastLogged = pct;
5562
- console.log(pc3.dim(` reading local usage\u2026 ${pct}%`));
5601
+ console.log(pc3.dim(` counting local token usage\u2026 ${pct}%`));
5563
5602
  }
5564
5603
  },
5565
5604
  stop: () => {
@@ -5569,22 +5608,29 @@ function startProgress() {
5569
5608
  const width = 24;
5570
5609
  let target = 0;
5571
5610
  let shown = 0;
5572
- let label = "starting\u2026";
5611
+ let ticks = 0;
5573
5612
  process.stdout.write("\x1B[?25l");
5613
+ const vibe = () => {
5614
+ if (shown >= 0.9) return LOADING_VIBES[LOADING_VIBES.length - 1];
5615
+ const i = Math.floor(ticks / 18) % (LOADING_VIBES.length - 1);
5616
+ return LOADING_VIBES[i];
5617
+ };
5574
5618
  const render = () => {
5619
+ ticks++;
5575
5620
  shown += (target - shown) * 0.3;
5576
5621
  if (target - shown < 4e-3) shown = target;
5577
5622
  const filled = Math.round(shown * width);
5578
5623
  const bar = pc3.yellow("\u2588".repeat(filled)) + pc3.dim("\u2591".repeat(width - filled));
5579
5624
  const pct = String(Math.round(shown * 100)).padStart(3);
5580
- process.stdout.write(`\r ${bar} ${pct}% ${pc3.dim(label)}\x1B[K`);
5625
+ process.stdout.write(`\r ${bar} ${pct}% ${pc3.dim(vibe())}\x1B[K`);
5581
5626
  };
5582
5627
  render();
5583
5628
  const timer = setInterval(render, 60);
5584
5629
  return {
5585
- onProgress: (done, total, l) => {
5630
+ // Stage labels are no longer surfaced (the narration is friendlier); we only
5631
+ // consume the done/total fraction to drive the bar.
5632
+ onProgress: (done, total) => {
5586
5633
  target = total > 0 ? done / total : 0;
5587
- label = l;
5588
5634
  },
5589
5635
  stop: () => {
5590
5636
  clearInterval(timer);
@@ -5623,15 +5669,16 @@ function showLocalDashboard(payload) {
5623
5669
  async function run(flags) {
5624
5670
  if (!flags.quiet) {
5625
5671
  printBanner();
5626
- console.log(pc3.dim(`whoburnedmore v${VERSION} \xB7 ${flags.local ? "local mode" : apiBase()}`));
5627
- }
5628
- if (!flags.quiet && !flags.dryRun && !flags.noSubmit && !flags.local && process.stdin.isTTY) {
5629
- const ok = await confirm(" Read your local usage and post your rank to the leaderboard?");
5630
- if (!ok) {
5631
- console.log(pc3.dim(" No worries \u2014 nothing read, nothing sent."));
5632
- console.log(pc3.dim(" Tip: `--local` builds a private dashboard, `--dry-run` previews the payload."));
5633
- return;
5672
+ console.log(pc3.dim(` whoburnedmore v${VERSION} \xB7 ${flags.local ? "local mode" : apiBase()}`));
5673
+ if (!flags.dryRun && !flags.noSubmit && !flags.local) {
5674
+ console.log(
5675
+ pc3.dim(" Counting your token usage and posting your rank \u2014 only daily totals leave your machine, never your prompts or code.")
5676
+ );
5677
+ console.log(
5678
+ pc3.dim(" (`--local` keeps it fully offline \xB7 `private`/`remove` pull it anytime \xB7 details: whoburnedmore.com/trust)")
5679
+ );
5634
5680
  }
5681
+ console.log();
5635
5682
  }
5636
5683
  const progress = flags.quiet ? { onProgress: void 0, stop: () => {
5637
5684
  } } : startProgress();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whoburnedmore",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
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": {