whoburnedmore 0.4.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 +281 -14
  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
 
@@ -286,19 +286,231 @@ function autoSyncInstalled() {
286
286
  // src/collect.ts
287
287
  import { spawnSync as spawnSync4 } from "node:child_process";
288
288
  import { createRequire as createRequire3 } from "node:module";
289
- 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
+ }
290
502
 
291
503
  // src/cursor.ts
292
504
  import { spawnSync as spawnSync3 } from "node:child_process";
293
505
  import { existsSync as existsSync3 } from "node:fs";
294
506
  import { createRequire as createRequire2 } from "node:module";
295
- import { homedir as homedir3, platform as platform2 } from "node:os";
296
- 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";
297
509
 
298
510
  // src/tokscale.ts
299
511
  import { spawnSync as spawnSync2 } from "node:child_process";
300
512
  import { createRequire } from "node:module";
301
- import { dirname, join as join3 } from "node:path";
513
+ import { dirname, join as join4 } from "node:path";
302
514
  var LOOKBACK_DAYS = 30;
303
515
  function num(n) {
304
516
  const v = Math.round(Number(n));
@@ -342,7 +554,7 @@ function resolveTokscaleBin() {
342
554
  const pkg = require3("tokscale/package.json");
343
555
  const rel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.tokscale ?? "";
344
556
  if (!rel) return null;
345
- const binPath = join3(dirname(pkgPath), rel);
557
+ const binPath = join4(dirname(pkgPath), rel);
346
558
  if (/\.(c|m)?js$/.test(binPath)) {
347
559
  return { cmd: process.execPath, prefixArgs: [binPath] };
348
560
  }
@@ -397,9 +609,9 @@ function collectCursorViaTokscale(lookbackDays = LOOKBACK_DAYS) {
397
609
  // src/cursor.ts
398
610
  var EVENTS_URL = "https://cursor.com/api/dashboard/get-filtered-usage-events";
399
611
  function cursorDbPath() {
400
- const home = homedir3();
612
+ const home = homedir4();
401
613
  const os = platform2();
402
- 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");
403
615
  return existsSync3(p) ? p : null;
404
616
  }
405
617
  function readCursorToken(db) {
@@ -672,7 +884,7 @@ function resolveCcusageBin() {
672
884
  const pkgPath = require3.resolve("ccusage/package.json");
673
885
  const pkg = require3("ccusage/package.json");
674
886
  const rel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.ccusage ?? "ccusage";
675
- const binPath = join5(dirname2(pkgPath), rel);
887
+ const binPath = join6(dirname2(pkgPath), rel);
676
888
  if (/\.(c|m)?js$/.test(binPath)) {
677
889
  return { cmd: process.execPath, prefixArgs: [binPath] };
678
890
  }
@@ -760,11 +972,25 @@ async function collectAll() {
760
972
  blocks.push(...cursor.blocks);
761
973
  toolsFound.push("cursor");
762
974
  }
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
+ });
763
985
  return {
764
986
  entries: dedupeDaily(entries),
765
- sessions: dedupeSessions(sessions),
987
+ sessions: dedupedSessions,
766
988
  blocks: dedupeBlocks(blocks),
767
- toolsFound
989
+ toolsFound,
990
+ tools,
991
+ skills,
992
+ projects,
993
+ agent
768
994
  };
769
995
  }
770
996
 
@@ -4855,13 +5081,42 @@ var SessionEntry = external_exports.object({
4855
5081
  cacheCreationTokens: tokenCount,
4856
5082
  cacheReadTokens: tokenCount,
4857
5083
  costUSD: external_exports.number().nonnegative(),
4858
- 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()
4859
5089
  });
4860
5090
  var BlockEntry = external_exports.object({
4861
5091
  startTime: Timestamp,
4862
5092
  totalTokens: tokenCount,
4863
5093
  costUSD: external_exports.number().nonnegative()
4864
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
+ });
4865
5120
  var SubmitPayload = external_exports.object({
4866
5121
  cliVersion: external_exports.string().min(1).max(32),
4867
5122
  entries: external_exports.array(DailyUsageEntry).min(1).max(2e4),
@@ -4869,6 +5124,14 @@ var SubmitPayload = external_exports.object({
4869
5124
  sessions: external_exports.array(SessionEntry).max(1e4).optional(),
4870
5125
  /** Optional time-window rollups (ccusage blocks) for peak-hours analysis. */
4871
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(),
4872
5135
  /** Optional friends-board code (from `--board=<code>`): auto-join this board on submit. */
4873
5136
  board: external_exports.string().min(1).max(32).optional()
4874
5137
  });
@@ -5154,7 +5417,7 @@ async function confirm(question) {
5154
5417
  function showLocalDashboard(entries) {
5155
5418
  const dir = defaultConfigDir();
5156
5419
  mkdirSync3(dir, { recursive: true });
5157
- const file = join6(dir, "dashboard.html");
5420
+ const file = join7(dir, "dashboard.html");
5158
5421
  writeFileSync3(file, renderDashboardHtml(entries));
5159
5422
  console.log();
5160
5423
  console.log(` Local dashboard: ${pc2.cyan(`file://${file}`)}`);
@@ -5173,7 +5436,7 @@ async function run(flags) {
5173
5436
  } finally {
5174
5437
  stop();
5175
5438
  }
5176
- const { entries, sessions, blocks, toolsFound } = collected;
5439
+ const { entries, sessions, blocks, toolsFound, tools, skills, projects, agent } = collected;
5177
5440
  if (entries.length === 0) {
5178
5441
  console.log();
5179
5442
  console.log(" Nothing to burn yet \u2014 no local usage found from any coding agent.");
@@ -5183,6 +5446,10 @@ async function run(flags) {
5183
5446
  const payload = { cliVersion: VERSION, entries };
5184
5447
  if (sessions.length > 0) payload.sessions = sessions;
5185
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;
5186
5453
  if (flags.board) payload.board = flags.board;
5187
5454
  if (flags.dryRun) {
5188
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.4.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": {