whoburnedmore 0.3.0 → 0.5.0

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 +379 -23
  2. 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 join6 } from "node:path";
13
+ import { join as join7 } from "node:path";
14
14
  import { createInterface } from "node:readline/promises";
15
15
  import pc2 from "picocolors";
16
16
 
@@ -29,16 +29,34 @@ function parseBoard(args) {
29
29
  function apiBase() {
30
30
  return process.env.WHOBURNEDMORE_API ?? "https://api.whoburnedmore.com";
31
31
  }
32
+ async function readJson(res) {
33
+ const text = await res.text();
34
+ if (!text) return {};
35
+ try {
36
+ return JSON.parse(text);
37
+ } catch {
38
+ return {
39
+ error: res.status >= 500 ? "the leaderboard server is temporarily unavailable \u2014 try again in a minute" : `unexpected response from the server (HTTP ${res.status})`
40
+ };
41
+ }
42
+ }
32
43
  async function post(path, body, token) {
33
- const res = await fetch(`${apiBase()}${path}`, {
34
- method: "POST",
35
- headers: {
36
- "Content-Type": "application/json",
37
- ...token ? { Authorization: `Bearer ${token}` } : {}
38
- },
39
- body: JSON.stringify(body)
40
- });
41
- return { status: res.status, body: await res.json() };
44
+ let res;
45
+ try {
46
+ res = await fetch(`${apiBase()}${path}`, {
47
+ method: "POST",
48
+ headers: {
49
+ "Content-Type": "application/json",
50
+ ...token ? { Authorization: `Bearer ${token}` } : {}
51
+ },
52
+ body: JSON.stringify(body)
53
+ });
54
+ } catch {
55
+ throw new Error(
56
+ "couldn't reach the leaderboard server \u2014 check your connection and try again"
57
+ );
58
+ }
59
+ return { status: res.status, body: await readJson(res) };
42
60
  }
43
61
  async function deviceStart() {
44
62
  const { status, body } = await post("/v1/auth/device", {});
@@ -268,19 +286,231 @@ function autoSyncInstalled() {
268
286
  // src/collect.ts
269
287
  import { spawnSync as spawnSync4 } from "node:child_process";
270
288
  import { createRequire as createRequire3 } from "node:module";
271
- import { dirname as dirname2, join as join5 } from "node:path";
289
+ import { dirname as dirname2, join as join6 } from "node:path";
290
+
291
+ // src/attribution.ts
292
+ import { readFileSync as readFileSync2, readdirSync, statSync } from "node:fs";
293
+ import { homedir as homedir3 } from "node:os";
294
+ import { basename, join as join3 } from "node:path";
295
+
296
+ // src/pricing.ts
297
+ var TABLE = [
298
+ { match: /opus/i, price: { in: 15, out: 75, cacheWrite: 18.75, cacheRead: 1.5 } },
299
+ { match: /sonnet/i, price: { in: 3, out: 15, cacheWrite: 3.75, cacheRead: 0.3 } },
300
+ { match: /haiku/i, price: { in: 0.8, out: 4, cacheWrite: 1, cacheRead: 0.08 } },
301
+ { match: /fable/i, price: { in: 15, out: 75, cacheWrite: 18.75, cacheRead: 1.5 } },
302
+ { match: /gpt-4o|gpt-4\.1/i, price: { in: 2.5, out: 10, cacheWrite: 2.5, cacheRead: 1.25 } },
303
+ { match: /gpt-5|o3|o4|codex/i, price: { in: 1.25, out: 10, cacheWrite: 1.25, cacheRead: 0.125 } },
304
+ { match: /gemini.*flash/i, price: { in: 0.15, out: 0.6, cacheWrite: 0.15, cacheRead: 0.0375 } },
305
+ { match: /gemini/i, price: { in: 1.25, out: 5, cacheWrite: 1.25, cacheRead: 0.31 } }
306
+ ];
307
+ function estimateCostUSD(model, t) {
308
+ const row = TABLE.find((r) => r.match.test(model));
309
+ if (!row) return 0;
310
+ const p = row.price;
311
+ const usd = (t.inputTokens * p.in + t.outputTokens * p.out + t.cacheCreationTokens * p.cacheWrite + t.cacheReadTokens * p.cacheRead) / 1e6;
312
+ return usd > 0 ? usd : 0;
313
+ }
314
+
315
+ // src/attribution.ts
316
+ var CLAUDE_PROJECTS = join3(homedir3(), ".claude", "projects");
317
+ var MAX_FILES = 5e3;
318
+ var MAX_FILE_BYTES = 64 * 1024 * 1024;
319
+ var TIME_BUDGET_MS = 3e4;
320
+ var MAX_STATS = 300;
321
+ var MAX_PROJECTS = 500;
322
+ function createAccumulator() {
323
+ return {
324
+ tools: /* @__PURE__ */ new Map(),
325
+ skills: /* @__PURE__ */ new Map(),
326
+ projects: /* @__PURE__ */ new Map(),
327
+ agent: {
328
+ messageCount: 0,
329
+ subagentMessages: 0,
330
+ subagentTokens: 0,
331
+ totalTokens: 0
332
+ },
333
+ titles: /* @__PURE__ */ new Map(),
334
+ sessionMessages: /* @__PURE__ */ new Map()
335
+ };
336
+ }
337
+ function createFileContext() {
338
+ return { toolNames: /* @__PURE__ */ new Map() };
339
+ }
340
+ function recordTokens(usage) {
341
+ if (!usage || typeof usage !== "object") return 0;
342
+ const u = usage;
343
+ const n = (v) => {
344
+ const x = Math.round(Number(v));
345
+ return Number.isFinite(x) && x > 0 ? x : 0;
346
+ };
347
+ return n(u.input_tokens) + n(u.output_tokens) + n(u.cache_creation_input_tokens) + n(u.cache_read_input_tokens);
348
+ }
349
+ function processRecord(rec, acc, ctx) {
350
+ if (!rec || typeof rec !== "object") return;
351
+ const r = rec;
352
+ if (typeof r.attributionSkill === "string" && r.attributionSkill) {
353
+ const s = r.attributionSkill.slice(0, 128);
354
+ acc.skills.set(s, (acc.skills.get(s) ?? 0) + 1);
355
+ }
356
+ if (r.type === "ai-title" && typeof r.aiTitle === "string" && r.aiTitle && typeof r.sessionId === "string" && r.sessionId) {
357
+ acc.titles.set(r.sessionId, r.aiTitle.slice(0, 200));
358
+ }
359
+ const content = r.message?.content;
360
+ if (!Array.isArray(content)) return;
361
+ const isAssistant = r.type === "assistant" || r.message?.role === "assistant";
362
+ const isUser = r.type === "user" || r.message?.role === "user";
363
+ if (isAssistant) {
364
+ for (const block of content) {
365
+ if (block && typeof block === "object" && block.type === "tool_use" && typeof block.name === "string") {
366
+ const name = block.name.slice(0, 128);
367
+ if (!name) continue;
368
+ const t = acc.tools.get(name) ?? { count: 0, errors: 0 };
369
+ t.count += 1;
370
+ acc.tools.set(name, t);
371
+ const id = block.id;
372
+ if (typeof id === "string" && id) ctx.toolNames.set(id, name);
373
+ }
374
+ }
375
+ const tokens = recordTokens(r.message?.usage);
376
+ acc.agent.messageCount += 1;
377
+ acc.agent.totalTokens += tokens;
378
+ const sidechain = r.isSidechain === true;
379
+ if (sidechain) {
380
+ acc.agent.subagentMessages += 1;
381
+ acc.agent.subagentTokens += tokens;
382
+ }
383
+ if (typeof r.sessionId === "string" && r.sessionId) {
384
+ acc.sessionMessages.set(
385
+ r.sessionId,
386
+ (acc.sessionMessages.get(r.sessionId) ?? 0) + 1
387
+ );
388
+ }
389
+ if (typeof r.cwd === "string" && r.cwd && tokens > 0) {
390
+ const name = basename(r.cwd).slice(0, 128) || "unknown";
391
+ const model = typeof r.message?.model === "string" ? r.message.model : "unknown";
392
+ const u = r.message?.usage;
393
+ const num3 = (v) => {
394
+ const x = Math.round(Number(v));
395
+ return Number.isFinite(x) && x > 0 ? x : 0;
396
+ };
397
+ const cost = estimateCostUSD(model, {
398
+ inputTokens: num3(u?.input_tokens),
399
+ outputTokens: num3(u?.output_tokens),
400
+ cacheCreationTokens: num3(u?.cache_creation_input_tokens),
401
+ cacheReadTokens: num3(u?.cache_read_input_tokens)
402
+ });
403
+ const p = acc.projects.get(name) ?? { tokens: 0, costUSD: 0 };
404
+ p.tokens += tokens;
405
+ p.costUSD += cost;
406
+ acc.projects.set(name, p);
407
+ }
408
+ } else if (isUser) {
409
+ for (const block of content) {
410
+ if (block && typeof block === "object" && block.type === "tool_result" && block.is_error === true) {
411
+ const id = block.tool_use_id;
412
+ const name = typeof id === "string" ? ctx.toolNames.get(id) : void 0;
413
+ if (name) {
414
+ const t = acc.tools.get(name) ?? { count: 0, errors: 0 };
415
+ t.errors += 1;
416
+ acc.tools.set(name, t);
417
+ }
418
+ }
419
+ }
420
+ }
421
+ }
422
+ function toSkillStats(map) {
423
+ return [...map.entries()].map(([name, count]) => ({ name, count })).filter((s) => s.count > 0).sort((a, b) => b.count - a.count).slice(0, MAX_STATS);
424
+ }
425
+ function toToolStats(map) {
426
+ return [...map.entries()].map(([name, v]) => ({ name, count: v.count, errors: v.errors })).filter((s) => s.count > 0).sort((a, b) => b.count - a.count).slice(0, MAX_STATS).map((s) => s.errors > 0 ? s : { name: s.name, count: s.count });
427
+ }
428
+ function toProjectStats(map) {
429
+ return [...map.entries()].map(([name, v]) => ({
430
+ name,
431
+ tokens: v.tokens,
432
+ costUSD: Number(v.costUSD.toFixed(6))
433
+ })).filter((p) => p.tokens > 0).sort((a, b) => b.tokens - a.tokens).slice(0, MAX_PROJECTS);
434
+ }
435
+ function accumulatorToResult(acc) {
436
+ return {
437
+ tools: toToolStats(acc.tools),
438
+ skills: toSkillStats(acc.skills),
439
+ projects: toProjectStats(acc.projects),
440
+ agent: { ...acc.agent },
441
+ titles: acc.titles,
442
+ sessionMessages: acc.sessionMessages
443
+ };
444
+ }
445
+ function listTranscripts(dir) {
446
+ const out = [];
447
+ const walk = (d) => {
448
+ if (out.length >= MAX_FILES) return;
449
+ let entries;
450
+ try {
451
+ entries = readdirSync(d, { withFileTypes: true });
452
+ } catch {
453
+ return;
454
+ }
455
+ for (const e of entries) {
456
+ if (out.length >= MAX_FILES) return;
457
+ const p = join3(d, e.name);
458
+ if (e.isDirectory()) walk(p);
459
+ else if (e.isFile() && e.name.endsWith(".jsonl")) {
460
+ try {
461
+ out.push({ path: p, mtime: statSync(p).mtimeMs });
462
+ } catch {
463
+ }
464
+ }
465
+ }
466
+ };
467
+ walk(dir);
468
+ return out.sort((a, b) => b.mtime - a.mtime).slice(0, MAX_FILES).map((f) => f.path);
469
+ }
470
+ function collectAttribution() {
471
+ const acc = createAccumulator();
472
+ const deadline = Date.now() + TIME_BUDGET_MS;
473
+ try {
474
+ for (const file of listTranscripts(CLAUDE_PROJECTS)) {
475
+ if (Date.now() > deadline) break;
476
+ let size = 0;
477
+ try {
478
+ size = statSync(file).size;
479
+ } catch {
480
+ continue;
481
+ }
482
+ if (size > MAX_FILE_BYTES) continue;
483
+ let text;
484
+ try {
485
+ text = readFileSync2(file, "utf8");
486
+ } catch {
487
+ continue;
488
+ }
489
+ const ctx = createFileContext();
490
+ for (const line of text.split("\n")) {
491
+ if (!line) continue;
492
+ try {
493
+ processRecord(JSON.parse(line), acc, ctx);
494
+ } catch {
495
+ }
496
+ }
497
+ }
498
+ } catch {
499
+ }
500
+ return accumulatorToResult(acc);
501
+ }
272
502
 
273
503
  // src/cursor.ts
274
504
  import { spawnSync as spawnSync3 } from "node:child_process";
275
505
  import { existsSync as existsSync3 } from "node:fs";
276
506
  import { createRequire as createRequire2 } from "node:module";
277
- import { homedir as homedir3, platform as platform2 } from "node:os";
278
- import { join as join4 } from "node:path";
507
+ import { homedir as homedir4, platform as platform2 } from "node:os";
508
+ import { join as join5 } from "node:path";
279
509
 
280
510
  // src/tokscale.ts
281
511
  import { spawnSync as spawnSync2 } from "node:child_process";
282
512
  import { createRequire } from "node:module";
283
- import { dirname, join as join3 } from "node:path";
513
+ import { dirname, join as join4 } from "node:path";
284
514
  var LOOKBACK_DAYS = 30;
285
515
  function num(n) {
286
516
  const v = Math.round(Number(n));
@@ -324,7 +554,7 @@ function resolveTokscaleBin() {
324
554
  const pkg = require3("tokscale/package.json");
325
555
  const rel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.tokscale ?? "";
326
556
  if (!rel) return null;
327
- const binPath = join3(dirname(pkgPath), rel);
557
+ const binPath = join4(dirname(pkgPath), rel);
328
558
  if (/\.(c|m)?js$/.test(binPath)) {
329
559
  return { cmd: process.execPath, prefixArgs: [binPath] };
330
560
  }
@@ -379,9 +609,9 @@ function collectCursorViaTokscale(lookbackDays = LOOKBACK_DAYS) {
379
609
  // src/cursor.ts
380
610
  var EVENTS_URL = "https://cursor.com/api/dashboard/get-filtered-usage-events";
381
611
  function cursorDbPath() {
382
- const home = homedir3();
612
+ const home = homedir4();
383
613
  const os = platform2();
384
- const p = os === "darwin" ? join4(home, "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb") : os === "win32" ? join4(process.env.APPDATA ?? join4(home, "AppData", "Roaming"), "Cursor", "User", "globalStorage", "state.vscdb") : join4(process.env.XDG_CONFIG_HOME ?? join4(home, ".config"), "Cursor", "User", "globalStorage", "state.vscdb");
614
+ const p = os === "darwin" ? join5(home, "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb") : os === "win32" ? join5(process.env.APPDATA ?? join5(home, "AppData", "Roaming"), "Cursor", "User", "globalStorage", "state.vscdb") : join5(process.env.XDG_CONFIG_HOME ?? join5(home, ".config"), "Cursor", "User", "globalStorage", "state.vscdb");
385
615
  return existsSync3(p) ? p : null;
386
616
  }
387
617
  function readCursorToken(db) {
@@ -654,12 +884,52 @@ function resolveCcusageBin() {
654
884
  const pkgPath = require3.resolve("ccusage/package.json");
655
885
  const pkg = require3("ccusage/package.json");
656
886
  const rel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.ccusage ?? "ccusage";
657
- const binPath = join5(dirname2(pkgPath), rel);
887
+ const binPath = join6(dirname2(pkgPath), rel);
658
888
  if (/\.(c|m)?js$/.test(binPath)) {
659
889
  return { cmd: process.execPath, prefixArgs: [binPath] };
660
890
  }
661
891
  return { cmd: binPath, prefixArgs: [] };
662
892
  }
893
+ function dedupeDaily(entries) {
894
+ const byKey = /* @__PURE__ */ new Map();
895
+ for (const e of entries) {
896
+ const key = `${e.date}|${e.tool}|${e.model}|${e.origin}`;
897
+ const prev = byKey.get(key);
898
+ if (!prev) {
899
+ byKey.set(key, { ...e });
900
+ continue;
901
+ }
902
+ prev.inputTokens += e.inputTokens;
903
+ prev.outputTokens += e.outputTokens;
904
+ prev.cacheCreationTokens += e.cacheCreationTokens;
905
+ prev.cacheReadTokens += e.cacheReadTokens;
906
+ prev.costUSD = Number((prev.costUSD + e.costUSD).toFixed(6));
907
+ prev.verified = prev.verified && e.verified;
908
+ }
909
+ return [...byKey.values()];
910
+ }
911
+ function dedupeSessions(sessions) {
912
+ const byId = /* @__PURE__ */ new Map();
913
+ const total = (s) => s.inputTokens + s.outputTokens + s.cacheCreationTokens + s.cacheReadTokens;
914
+ for (const s of sessions) {
915
+ const prev = byId.get(s.sessionId);
916
+ if (!prev || total(s) >= total(prev)) byId.set(s.sessionId, s);
917
+ }
918
+ return [...byId.values()];
919
+ }
920
+ function dedupeBlocks(blocks) {
921
+ const byStart = /* @__PURE__ */ new Map();
922
+ for (const b of blocks) {
923
+ const prev = byStart.get(b.startTime);
924
+ if (!prev) {
925
+ byStart.set(b.startTime, { ...b });
926
+ continue;
927
+ }
928
+ prev.totalTokens += b.totalTokens;
929
+ prev.costUSD = Number((prev.costUSD + b.costUSD).toFixed(6));
930
+ }
931
+ return [...byStart.values()];
932
+ }
663
933
  function runCcusage(cmd, args) {
664
934
  const res = spawnSync4(cmd, args, {
665
935
  encoding: "utf8",
@@ -702,7 +972,26 @@ async function collectAll() {
702
972
  blocks.push(...cursor.blocks);
703
973
  toolsFound.push("cursor");
704
974
  }
705
- return { entries, sessions, blocks, toolsFound };
975
+ const { tools, skills, projects, agent, titles, sessionMessages } = collectAttribution();
976
+ const dedupedSessions = dedupeSessions(sessions).map((s) => {
977
+ const title = titles.get(s.sessionId);
978
+ const messageCount = sessionMessages.get(s.sessionId);
979
+ return {
980
+ ...s,
981
+ ...title ? { title } : {},
982
+ ...messageCount ? { messageCount } : {}
983
+ };
984
+ });
985
+ return {
986
+ entries: dedupeDaily(entries),
987
+ sessions: dedupedSessions,
988
+ blocks: dedupeBlocks(blocks),
989
+ toolsFound,
990
+ tools,
991
+ skills,
992
+ projects,
993
+ agent
994
+ };
706
995
  }
707
996
 
708
997
  // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
@@ -4792,13 +5081,42 @@ var SessionEntry = external_exports.object({
4792
5081
  cacheCreationTokens: tokenCount,
4793
5082
  cacheReadTokens: tokenCount,
4794
5083
  costUSD: external_exports.number().nonnegative(),
4795
- lastActivity: Timestamp
5084
+ lastActivity: Timestamp,
5085
+ /** Human-readable AI-generated session title (from transcripts). Optional. */
5086
+ title: external_exports.string().max(200).optional(),
5087
+ /** Number of assistant messages in this session (from transcripts). Optional. */
5088
+ messageCount: external_exports.number().int().nonnegative().optional()
4796
5089
  });
4797
5090
  var BlockEntry = external_exports.object({
4798
5091
  startTime: Timestamp,
4799
5092
  totalTokens: tokenCount,
4800
5093
  costUSD: external_exports.number().nonnegative()
4801
5094
  });
5095
+ var ToolStat = external_exports.object({
5096
+ name: external_exports.string().min(1).max(128),
5097
+ count: external_exports.number().int().nonnegative(),
5098
+ /** How many of those calls returned an error/interrupt (tool reliability). Optional. */
5099
+ errors: external_exports.number().int().nonnegative().optional()
5100
+ });
5101
+ var ProjectStat = external_exports.object({
5102
+ name: external_exports.string().min(1).max(128),
5103
+ tokens: external_exports.number().int().nonnegative(),
5104
+ costUSD: external_exports.number().nonnegative()
5105
+ });
5106
+ var AgentStat = external_exports.object({
5107
+ /** Total assistant messages across transcripts. */
5108
+ messageCount: external_exports.number().int().nonnegative(),
5109
+ /** Assistant messages that ran inside a subagent sidechain. */
5110
+ subagentMessages: external_exports.number().int().nonnegative(),
5111
+ /** Tokens spent inside subagent sidechains. */
5112
+ subagentTokens: external_exports.number().int().nonnegative(),
5113
+ /** Total tokens observed across transcripts (denominator for the share). */
5114
+ totalTokens: external_exports.number().int().nonnegative()
5115
+ });
5116
+ var SkillStat = external_exports.object({
5117
+ name: external_exports.string().min(1).max(128),
5118
+ count: external_exports.number().int().nonnegative()
5119
+ });
4802
5120
  var SubmitPayload = external_exports.object({
4803
5121
  cliVersion: external_exports.string().min(1).max(32),
4804
5122
  entries: external_exports.array(DailyUsageEntry).min(1).max(2e4),
@@ -4806,6 +5124,14 @@ var SubmitPayload = external_exports.object({
4806
5124
  sessions: external_exports.array(SessionEntry).max(1e4).optional(),
4807
5125
  /** Optional time-window rollups (ccusage blocks) for peak-hours analysis. */
4808
5126
  blocks: external_exports.array(BlockEntry).max(1e4).optional(),
5127
+ /** Optional tool-call frequencies parsed from local transcripts (names + counts). */
5128
+ tools: external_exports.array(ToolStat).max(300).optional(),
5129
+ /** Optional skill-usage frequencies parsed from local transcripts. */
5130
+ skills: external_exports.array(SkillStat).max(300).optional(),
5131
+ /** Optional per-project usage totals parsed from local transcripts. */
5132
+ projects: external_exports.array(ProjectStat).max(500).optional(),
5133
+ /** Optional subagent-vs-main rollup parsed from local transcripts. */
5134
+ agent: AgentStat.optional(),
4809
5135
  /** Optional friends-board code (from `--board=<code>`): auto-join this board on submit. */
4810
5136
  board: external_exports.string().min(1).max(32).optional()
4811
5137
  });
@@ -5036,6 +5362,25 @@ async function publishLocal(payload, deps) {
5036
5362
  // src/index.ts
5037
5363
  var require2 = createRequire4(import.meta.url);
5038
5364
  var VERSION = require2("../package.json").version;
5365
+ function startSpinner(label) {
5366
+ if (!process.stdout.isTTY) {
5367
+ console.log(pc2.dim(` ${label}`));
5368
+ return () => {
5369
+ };
5370
+ }
5371
+ const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
5372
+ let i = 0;
5373
+ process.stdout.write("\x1B[?25l");
5374
+ const timer = setInterval(() => {
5375
+ i = (i + 1) % frames.length;
5376
+ process.stdout.write(`\r ${pc2.yellow(frames[i])} ${pc2.dim(label)}`);
5377
+ }, 80);
5378
+ return () => {
5379
+ clearInterval(timer);
5380
+ process.stdout.write("\r\x1B[2K");
5381
+ process.stdout.write("\x1B[?25h");
5382
+ };
5383
+ }
5039
5384
  function openBrowser(url) {
5040
5385
  const os = platform3();
5041
5386
  const [cmd, args] = os === "darwin" ? ["open", [url]] : os === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
@@ -5072,7 +5417,7 @@ async function confirm(question) {
5072
5417
  function showLocalDashboard(entries) {
5073
5418
  const dir = defaultConfigDir();
5074
5419
  mkdirSync3(dir, { recursive: true });
5075
- const file = join6(dir, "dashboard.html");
5420
+ const file = join7(dir, "dashboard.html");
5076
5421
  writeFileSync3(file, renderDashboardHtml(entries));
5077
5422
  console.log();
5078
5423
  console.log(` Local dashboard: ${pc2.cyan(`file://${file}`)}`);
@@ -5082,9 +5427,16 @@ function showLocalDashboard(entries) {
5082
5427
  async function run(flags) {
5083
5428
  if (!flags.quiet) {
5084
5429
  console.log(pc2.dim(`whoburnedmore v${VERSION} \xB7 ${flags.local ? "local mode" : apiBase()}`));
5085
- console.log(pc2.dim(" Calculating your burn from local usage\u2026"));
5086
5430
  }
5087
- const { entries, sessions, blocks, toolsFound } = await collectAll();
5431
+ const stop = flags.quiet ? () => {
5432
+ } : startSpinner("Calculating your burn from local usage\u2026");
5433
+ let collected;
5434
+ try {
5435
+ collected = await collectAll();
5436
+ } finally {
5437
+ stop();
5438
+ }
5439
+ const { entries, sessions, blocks, toolsFound, tools, skills, projects, agent } = collected;
5088
5440
  if (entries.length === 0) {
5089
5441
  console.log();
5090
5442
  console.log(" Nothing to burn yet \u2014 no local usage found from any coding agent.");
@@ -5094,6 +5446,10 @@ async function run(flags) {
5094
5446
  const payload = { cliVersion: VERSION, entries };
5095
5447
  if (sessions.length > 0) payload.sessions = sessions;
5096
5448
  if (blocks.length > 0) payload.blocks = blocks;
5449
+ if (tools.length > 0) payload.tools = tools;
5450
+ if (skills.length > 0) payload.skills = skills;
5451
+ if (projects.length > 0) payload.projects = projects;
5452
+ if (agent.messageCount > 0) payload.agent = agent;
5097
5453
  if (flags.board) payload.board = flags.board;
5098
5454
  if (flags.dryRun) {
5099
5455
  console.log(pc2.dim("\n --dry-run: this exact payload would be sent, nothing else:\n"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whoburnedmore",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
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": {