whoburnedmore 0.8.0 → 0.8.3

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 +136 -73
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -24,6 +24,15 @@ function parseBoard(args) {
24
24
  }
25
25
  return void 0;
26
26
  }
27
+ function resolveCommand(args) {
28
+ if (args.includes("--help") || args.includes("-h") || args.includes("help")) {
29
+ return "help";
30
+ }
31
+ if (args.includes("--version") || args.includes("-v") || args.includes("version")) {
32
+ return "version";
33
+ }
34
+ return args.find((a) => !a.startsWith("-")) ?? "run";
35
+ }
27
36
 
28
37
  // src/api.ts
29
38
  function apiBase() {
@@ -101,7 +110,13 @@ import { fileURLToPath } from "node:url";
101
110
 
102
111
  // src/config.ts
103
112
  import { randomBytes } from "node:crypto";
104
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
113
+ import {
114
+ chmodSync,
115
+ existsSync,
116
+ mkdirSync,
117
+ readFileSync,
118
+ writeFileSync
119
+ } from "node:fs";
105
120
  import { homedir } from "node:os";
106
121
  import { join } from "node:path";
107
122
  function defaultConfigDir() {
@@ -123,6 +138,10 @@ function saveConfig(dir = defaultConfigDir(), config = {}) {
123
138
  mkdirSync(dir, { recursive: true });
124
139
  const file = join(dir, "config.json");
125
140
  writeFileSync(file, JSON.stringify(config, null, 2), { mode: 384 });
141
+ try {
142
+ chmodSync(file, 384);
143
+ } catch {
144
+ }
126
145
  }
127
146
  function ensureAnonKey(dir = defaultConfigDir()) {
128
147
  const config = loadConfig(dir) ?? {};
@@ -258,29 +277,27 @@ function autoSyncInstalled() {
258
277
 
259
278
  // src/banner.ts
260
279
  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];
280
+ var WORD = "whoburnedmore";
281
+ var SHADES = [226, 220, 214, 208, 202, 196];
282
+ function paintWordmark() {
283
+ if (!pc.isColorSupported) return `${WORD}?`;
284
+ const letters = [...WORD].map((ch, i) => `\x1B[1;38;5;${SHADES[i % SHADES.length]}m${ch}`).join("");
285
+ return `${letters}\x1B[1;38;5;208m?\x1B[0m`;
286
+ }
269
287
  function printBanner() {
270
- const color = pc.isColorSupported;
288
+ const rule = pc.dim("\u2500".repeat(30));
271
289
  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?")}`);
290
+ console.log(` \u{1F525} ${paintWordmark()}`);
291
+ console.log(` ${rule}`);
292
+ console.log(` ${pc.dim("who burned more \u2014 you, or them?")}`);
277
293
  console.log();
278
294
  }
279
295
 
280
296
  // src/collect.ts
281
- import { spawnSync as spawnSync4 } from "node:child_process";
297
+ import { execFile } from "node:child_process";
282
298
  import { createRequire as createRequire3 } from "node:module";
283
299
  import { dirname as dirname2, join as join6 } from "node:path";
300
+ import { promisify } from "node:util";
284
301
 
285
302
  // src/attribution.ts
286
303
  import { readFileSync as readFileSync2, readdirSync, statSync } from "node:fs";
@@ -311,7 +328,7 @@ var CLAUDE_PROJECTS = join3(homedir3(), ".claude", "projects");
311
328
  var CODEX_SESSIONS = join3(homedir3(), ".codex", "sessions");
312
329
  var MAX_FILES = 5e3;
313
330
  var MAX_FILE_BYTES = 64 * 1024 * 1024;
314
- var TIME_BUDGET_MS = 3e4;
331
+ var TIME_BUDGET_MS = 12e3;
315
332
  var MAX_STATS = 300;
316
333
  var MAX_PROJECTS = 500;
317
334
  function createAccumulator() {
@@ -563,10 +580,17 @@ function listTranscripts(dir) {
563
580
  walk(dir);
564
581
  return out.sort((a, b) => b.mtime - a.mtime).slice(0, MAX_FILES).map((f) => f.path);
565
582
  }
566
- function collectAttribution() {
583
+ async function collectAttribution() {
567
584
  const acc = createAccumulator();
568
585
  const deadline = Date.now() + TIME_BUDGET_MS;
569
586
  let complete = true;
587
+ let sinceYield = 0;
588
+ const breathe = async () => {
589
+ if (++sinceYield >= 8) {
590
+ sinceYield = 0;
591
+ await new Promise((r) => setImmediate(r));
592
+ }
593
+ };
570
594
  try {
571
595
  for (const dir of claudeProjectDirs()) {
572
596
  for (const file of listTranscripts(dir)) {
@@ -582,6 +606,7 @@ function collectAttribution() {
582
606
  } catch {
583
607
  }
584
608
  }
609
+ await breathe();
585
610
  }
586
611
  }
587
612
  for (const file of listTranscripts(CODEX_SESSIONS)) {
@@ -597,6 +622,7 @@ function collectAttribution() {
597
622
  } catch {
598
623
  }
599
624
  }
625
+ await breathe();
600
626
  }
601
627
  } catch {
602
628
  complete = false;
@@ -855,6 +881,7 @@ async function collectCursor() {
855
881
  }
856
882
 
857
883
  // src/collect.ts
884
+ var execFileAsync = promisify(execFile);
858
885
  var SOURCES = [
859
886
  "claude",
860
887
  "codex",
@@ -1036,64 +1063,85 @@ function dedupeBlocks(blocks) {
1036
1063
  }
1037
1064
  return [...byStart.values()];
1038
1065
  }
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 };
1066
+ async function runCcusageOnce(cmd, args) {
1047
1067
  try {
1048
- return { json: JSON.parse(res.stdout), transient: false };
1049
- } catch {
1050
- return { json: null, transient: false };
1068
+ const { stdout } = await execFileAsync(cmd, args, {
1069
+ encoding: "utf8",
1070
+ maxBuffer: 64 * 1024 * 1024,
1071
+ // A single source shouldn't be able to hang the whole run. 25s is plenty
1072
+ // for a healthy local read; a hung source gets killed and (if transient)
1073
+ // retried once below rather than stalling everything for minutes.
1074
+ timeout: 25e3
1075
+ });
1076
+ if (!stdout) return { json: null, transient: false };
1077
+ try {
1078
+ return { json: JSON.parse(stdout), transient: false };
1079
+ } catch {
1080
+ return { json: null, transient: false };
1081
+ }
1082
+ } catch (err) {
1083
+ const e = err;
1084
+ const transient = e.killed === true || e.signal != null || typeof e.code === "string";
1085
+ return { json: null, transient };
1051
1086
  }
1052
1087
  }
1053
- function runCcusage(cmd, args) {
1054
- const first = runCcusageOnce(cmd, args);
1088
+ async function runCcusage(cmd, args) {
1089
+ const first = await runCcusageOnce(cmd, args);
1055
1090
  if (first.json !== null || !first.transient) return first.json;
1056
- return runCcusageOnce(cmd, args).json;
1091
+ return (await runCcusageOnce(cmd, args)).json;
1057
1092
  }
1058
1093
  var COLLECT_STAGES = SOURCES.length + 4;
1059
1094
  async function collectAll(onProgress) {
1060
1095
  const { cmd, prefixArgs } = resolveCcusageBin();
1096
+ let done = 0;
1097
+ const tick = () => onProgress?.(++done, COLLECT_STAGES, "");
1098
+ const sourceTasks = SOURCES.map(async (source) => {
1099
+ const json = await runCcusage(cmd, [...prefixArgs, source, "daily", "--json", "--offline"]);
1100
+ tick();
1101
+ return { source, mapped: json ? mapCcusageDaily(source, json) : [] };
1102
+ });
1103
+ const sessionTask = runCcusage(cmd, [...prefixArgs, "session", "--json", "--offline"]).then(
1104
+ (json) => {
1105
+ tick();
1106
+ return json ? mapCcusageSessions(json) : [];
1107
+ }
1108
+ );
1109
+ const blockTask = runCcusage(cmd, [...prefixArgs, "blocks", "--json", "--offline"]).then(
1110
+ (json) => {
1111
+ tick();
1112
+ return json ? mapCcusageBlocks(json) : [];
1113
+ }
1114
+ );
1115
+ const cursorTask = collectCursor().then((c) => {
1116
+ tick();
1117
+ return c;
1118
+ });
1119
+ const attributionTask = collectAttribution().then((a) => {
1120
+ tick();
1121
+ return a;
1122
+ });
1123
+ const [sourceResults, sessions, blocks, cursor, attribution] = await Promise.all([
1124
+ Promise.all(sourceTasks),
1125
+ sessionTask,
1126
+ blockTask,
1127
+ cursorTask,
1128
+ attributionTask
1129
+ ]);
1061
1130
  const entries = [];
1062
1131
  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);
1132
+ for (const { source, mapped } of sourceResults) {
1076
1133
  if (mapped.length > 0) {
1077
1134
  entries.push(...mapped);
1078
1135
  toolsFound.push(source);
1079
1136
  }
1080
1137
  }
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
1138
  if (cursor.found) {
1090
1139
  entries.push(...cursor.entries);
1091
1140
  blocks.push(...cursor.blocks);
1092
1141
  toolsFound.push("cursor");
1093
1142
  }
1094
- tick("reading agent transcripts");
1095
- const { tools, skills, projects, agent, titles, sessionMessages, complete } = collectAttribution();
1096
- onProgress?.(done, COLLECT_STAGES, "done");
1143
+ const { tools, skills, projects, agent, titles, sessionMessages, complete } = attribution;
1144
+ onProgress?.(COLLECT_STAGES, COLLECT_STAGES, "");
1097
1145
  const dedupedSessions = dedupeSessions(sessions).map((s) => {
1098
1146
  const title = titles.get(s.sessionId);
1099
1147
  const messageCount = sessionMessages.get(s.sessionId);
@@ -5551,6 +5599,16 @@ async function publishLocal(payload, deps) {
5551
5599
  // src/index.ts
5552
5600
  var require2 = createRequire4(import.meta.url);
5553
5601
  var VERSION = require2("../package.json").version;
5602
+ var LOADING_VIBES = [
5603
+ "counting up your token usage, right here on your machine\u2026",
5604
+ "tallying tokens across every coding agent you use\u2026",
5605
+ "adding up cache reads, writes & all the burn\u2026",
5606
+ "tokens & totals only \u2014 never your prompts or code\u2026",
5607
+ "summing up your usage, model by model\u2026",
5608
+ "working out what all that burn cost you \u{1F525}",
5609
+ "crunching your daily token totals\u2026",
5610
+ "almost there \u2014 adding it all up\u2026"
5611
+ ];
5554
5612
  function startProgress() {
5555
5613
  if (!process.stdout.isTTY) {
5556
5614
  let lastLogged = -1;
@@ -5559,7 +5617,7 @@ function startProgress() {
5559
5617
  const pct = Math.round(done / total * 100);
5560
5618
  if (pct >= lastLogged + 25 || pct === 100 && lastLogged < 100) {
5561
5619
  lastLogged = pct;
5562
- console.log(pc3.dim(` reading local usage\u2026 ${pct}%`));
5620
+ console.log(pc3.dim(` counting local token usage\u2026 ${pct}%`));
5563
5621
  }
5564
5622
  },
5565
5623
  stop: () => {
@@ -5569,22 +5627,29 @@ function startProgress() {
5569
5627
  const width = 24;
5570
5628
  let target = 0;
5571
5629
  let shown = 0;
5572
- let label = "starting\u2026";
5630
+ let ticks = 0;
5573
5631
  process.stdout.write("\x1B[?25l");
5632
+ const vibe = () => {
5633
+ if (shown >= 0.9) return LOADING_VIBES[LOADING_VIBES.length - 1];
5634
+ const i = Math.floor(ticks / 18) % (LOADING_VIBES.length - 1);
5635
+ return LOADING_VIBES[i];
5636
+ };
5574
5637
  const render = () => {
5638
+ ticks++;
5575
5639
  shown += (target - shown) * 0.3;
5576
5640
  if (target - shown < 4e-3) shown = target;
5577
5641
  const filled = Math.round(shown * width);
5578
5642
  const bar = pc3.yellow("\u2588".repeat(filled)) + pc3.dim("\u2591".repeat(width - filled));
5579
5643
  const pct = String(Math.round(shown * 100)).padStart(3);
5580
- process.stdout.write(`\r ${bar} ${pct}% ${pc3.dim(label)}\x1B[K`);
5644
+ process.stdout.write(`\r ${bar} ${pct}% ${pc3.dim(vibe())}\x1B[K`);
5581
5645
  };
5582
5646
  render();
5583
5647
  const timer = setInterval(render, 60);
5584
5648
  return {
5585
- onProgress: (done, total, l) => {
5649
+ // Stage labels are no longer surfaced (the narration is friendlier); we only
5650
+ // consume the done/total fraction to drive the bar.
5651
+ onProgress: (done, total) => {
5586
5652
  target = total > 0 ? done / total : 0;
5587
- label = l;
5588
5653
  },
5589
5654
  stop: () => {
5590
5655
  clearInterval(timer);
@@ -5623,15 +5688,16 @@ function showLocalDashboard(payload) {
5623
5688
  async function run(flags) {
5624
5689
  if (!flags.quiet) {
5625
5690
  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;
5691
+ console.log(pc3.dim(` whoburnedmore v${VERSION} \xB7 ${flags.local ? "local mode" : apiBase()}`));
5692
+ if (!flags.dryRun && !flags.noSubmit && !flags.local) {
5693
+ console.log(
5694
+ pc3.dim(" Counting your token usage and posting your rank \u2014 only daily totals leave your machine, never your prompts or code.")
5695
+ );
5696
+ console.log(
5697
+ pc3.dim(" (`--local` keeps it fully offline \xB7 `private`/`remove` pull it anytime \xB7 details: whoburnedmore.com/trust)")
5698
+ );
5634
5699
  }
5700
+ console.log();
5635
5701
  }
5636
5702
  const progress = flags.quiet ? { onProgress: void 0, stop: () => {
5637
5703
  } } : startProgress();
@@ -5732,7 +5798,7 @@ async function main() {
5732
5798
  return;
5733
5799
  }
5734
5800
  const args = process.argv.slice(2);
5735
- const command = args.find((a) => !a.startsWith("-")) ?? (args.includes("--version") || args.includes("-v") ? "version" : "run");
5801
+ const command = resolveCommand(args);
5736
5802
  const flags = {
5737
5803
  dryRun: args.includes("--dry-run"),
5738
5804
  noSubmit: args.includes("--no-submit"),
@@ -5779,12 +5845,9 @@ async function main() {
5779
5845
  console.log(` ${uninstallAutoSync()}`);
5780
5846
  break;
5781
5847
  case "help":
5782
- case "--help":
5783
5848
  printHelp();
5784
5849
  break;
5785
5850
  case "version":
5786
- case "--version":
5787
- case "-v":
5788
5851
  console.log(VERSION);
5789
5852
  break;
5790
5853
  default:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whoburnedmore",
3
- "version": "0.8.0",
3
+ "version": "0.8.3",
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": {