whoburnedmore 0.2.0 → 0.3.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 +282 -27
  2. package/package.json +4 -3
package/dist/index.js CHANGED
@@ -8,9 +8,9 @@ var __export = (target, all) => {
8
8
  // src/index.ts
9
9
  import { spawn } from "node:child_process";
10
10
  import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "node:fs";
11
- import { createRequire as createRequire2 } from "node:module";
12
- import { platform as platform2 } from "node:os";
13
- import { join as join4 } from "node:path";
11
+ import { createRequire as createRequire4 } from "node:module";
12
+ import { platform as platform3 } from "node:os";
13
+ import { join as join6 } from "node:path";
14
14
  import { createInterface } from "node:readline/promises";
15
15
  import pc2 from "picocolors";
16
16
 
@@ -266,9 +266,259 @@ function autoSyncInstalled() {
266
266
  }
267
267
 
268
268
  // src/collect.ts
269
+ import { spawnSync as spawnSync4 } from "node:child_process";
270
+ import { createRequire as createRequire3 } from "node:module";
271
+ import { dirname as dirname2, join as join5 } from "node:path";
272
+
273
+ // src/cursor.ts
274
+ import { spawnSync as spawnSync3 } from "node:child_process";
275
+ import { existsSync as existsSync3 } from "node:fs";
276
+ 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";
279
+
280
+ // src/tokscale.ts
269
281
  import { spawnSync as spawnSync2 } from "node:child_process";
270
282
  import { createRequire } from "node:module";
271
283
  import { dirname, join as join3 } from "node:path";
284
+ var LOOKBACK_DAYS = 30;
285
+ function num(n) {
286
+ const v = Math.round(Number(n));
287
+ return Number.isFinite(v) && v > 0 ? v : 0;
288
+ }
289
+ function numCost(n) {
290
+ const v = Number(n);
291
+ return Number.isFinite(v) && v > 0 ? v : 0;
292
+ }
293
+ function mapTokscaleDay(date, json) {
294
+ const entries = json?.entries;
295
+ if (!Array.isArray(entries)) return [];
296
+ const out = [];
297
+ for (const e of entries) {
298
+ const inputTokens = num(e.input);
299
+ const outputTokens = num(e.output) + num(e.reasoning);
300
+ const cacheCreationTokens = num(e.cacheWrite);
301
+ const cacheReadTokens = num(e.cacheRead);
302
+ const costUSD = numCost(e.cost);
303
+ const total = inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens;
304
+ if (total === 0 && costUSD === 0) continue;
305
+ out.push({
306
+ date,
307
+ tool: "cursor",
308
+ model: typeof e.model === "string" && e.model ? e.model : "cursor",
309
+ inputTokens,
310
+ outputTokens,
311
+ cacheCreationTokens,
312
+ cacheReadTokens,
313
+ costUSD,
314
+ origin: "cli",
315
+ verified: false
316
+ });
317
+ }
318
+ return out;
319
+ }
320
+ function resolveTokscaleBin() {
321
+ try {
322
+ const require3 = createRequire(import.meta.url);
323
+ const pkgPath = require3.resolve("tokscale/package.json");
324
+ const pkg = require3("tokscale/package.json");
325
+ const rel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.tokscale ?? "";
326
+ if (!rel) return null;
327
+ const binPath = join3(dirname(pkgPath), rel);
328
+ if (/\.(c|m)?js$/.test(binPath)) {
329
+ return { cmd: process.execPath, prefixArgs: [binPath] };
330
+ }
331
+ return { cmd: binPath, prefixArgs: [] };
332
+ } catch {
333
+ return null;
334
+ }
335
+ }
336
+ function runTokscaleDay(bin, day) {
337
+ const res = spawnSync2(
338
+ bin.cmd,
339
+ [
340
+ ...bin.prefixArgs,
341
+ "--client",
342
+ "cursor",
343
+ "--json",
344
+ "--since",
345
+ day,
346
+ "--until",
347
+ day,
348
+ "--group-by",
349
+ "model",
350
+ "--no-spinner"
351
+ ],
352
+ { encoding: "utf8", maxBuffer: 32 * 1024 * 1024, timeout: 6e4 }
353
+ );
354
+ if (res.status !== 0 || !res.stdout) return null;
355
+ try {
356
+ return JSON.parse(res.stdout);
357
+ } catch {
358
+ return null;
359
+ }
360
+ }
361
+ function collectCursorViaTokscale(lookbackDays = LOOKBACK_DAYS) {
362
+ const bin = resolveTokscaleBin();
363
+ if (!bin) return [];
364
+ const today = /* @__PURE__ */ new Date();
365
+ const day = (offset) => new Date(today.getTime() - offset * 864e5).toISOString().slice(0, 10);
366
+ if (mapTokscaleDay(day(0), runTokscaleDay(bin, day(0))).length === 0) {
367
+ if (mapTokscaleDay(day(1), runTokscaleDay(bin, day(1))).length === 0) {
368
+ return [];
369
+ }
370
+ }
371
+ const out = [];
372
+ for (let i = 0; i < lookbackDays; i++) {
373
+ const d = day(i);
374
+ out.push(...mapTokscaleDay(d, runTokscaleDay(bin, d)));
375
+ }
376
+ return out;
377
+ }
378
+
379
+ // src/cursor.ts
380
+ var EVENTS_URL = "https://cursor.com/api/dashboard/get-filtered-usage-events";
381
+ function cursorDbPath() {
382
+ const home = homedir3();
383
+ 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");
385
+ return existsSync3(p) ? p : null;
386
+ }
387
+ function readCursorToken(db) {
388
+ const require3 = createRequire2(import.meta.url);
389
+ try {
390
+ const { DatabaseSync } = require3("node:sqlite");
391
+ const d = new DatabaseSync(db, { readOnly: true });
392
+ const row = d.prepare("SELECT value FROM ItemTable WHERE key = ?").get("cursorAuth/accessToken");
393
+ d.close();
394
+ if (row?.value) return String(row.value);
395
+ } catch {
396
+ }
397
+ try {
398
+ const res = spawnSync3(
399
+ "sqlite3",
400
+ [db, "SELECT value FROM ItemTable WHERE key='cursorAuth/accessToken';"],
401
+ { encoding: "utf8", timeout: 1e4 }
402
+ );
403
+ const out = res.stdout?.trim();
404
+ if (res.status === 0 && out) return out;
405
+ } catch {
406
+ }
407
+ return null;
408
+ }
409
+ function cursorCookie(token) {
410
+ try {
411
+ const part = token.split(".")[1];
412
+ if (!part) return null;
413
+ const json = JSON.parse(
414
+ Buffer.from(part, "base64url").toString("utf8")
415
+ );
416
+ if (!json.sub) return null;
417
+ return `WorkosCursorSessionToken=${json.sub}%3A%3A${token}`;
418
+ } catch {
419
+ return null;
420
+ }
421
+ }
422
+ function num2(n) {
423
+ const v = Math.round(Number(n));
424
+ return Number.isFinite(v) && v > 0 ? v : 0;
425
+ }
426
+ function mapCursorEvents(events) {
427
+ const byDay = /* @__PURE__ */ new Map();
428
+ const byHour = /* @__PURE__ */ new Map();
429
+ for (const e of events) {
430
+ const tu = e.tokenUsage;
431
+ const ms = Number(e.timestamp);
432
+ if (!tu || !Number.isFinite(ms)) continue;
433
+ const d = new Date(ms);
434
+ const date = d.toISOString().slice(0, 10);
435
+ const model = e.model || "cursor";
436
+ const input = num2(tu.inputTokens);
437
+ const output = num2(tu.outputTokens);
438
+ const cacheWrite = num2(tu.cacheWriteTokens);
439
+ const cacheRead = num2(tu.cacheReadTokens);
440
+ const cost = Math.max(0, (Number(tu.totalCents) || 0) / 100);
441
+ const total = input + output + cacheWrite + cacheRead;
442
+ if (total === 0 && cost === 0) continue;
443
+ const key = `${date}|${model}`;
444
+ const day = byDay.get(key) ?? {
445
+ date,
446
+ tool: "cursor",
447
+ model,
448
+ inputTokens: 0,
449
+ outputTokens: 0,
450
+ cacheCreationTokens: 0,
451
+ cacheReadTokens: 0,
452
+ costUSD: 0,
453
+ origin: "cli",
454
+ verified: false
455
+ };
456
+ day.inputTokens += input;
457
+ day.outputTokens += output;
458
+ day.cacheCreationTokens += cacheWrite;
459
+ day.cacheReadTokens += cacheRead;
460
+ day.costUSD += cost;
461
+ byDay.set(key, day);
462
+ const hour = new Date(
463
+ Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), d.getUTCHours())
464
+ ).toISOString();
465
+ const blk = byHour.get(hour) ?? { startTime: hour, totalTokens: 0, costUSD: 0 };
466
+ blk.totalTokens += total;
467
+ blk.costUSD += cost;
468
+ byHour.set(hour, blk);
469
+ }
470
+ const entries = [...byDay.values()].map((e) => ({
471
+ ...e,
472
+ costUSD: Number(e.costUSD.toFixed(4))
473
+ }));
474
+ const blocks = [...byHour.values()].map((b) => ({
475
+ ...b,
476
+ costUSD: Number(b.costUSD.toFixed(4))
477
+ }));
478
+ return { entries, blocks };
479
+ }
480
+ async function fetchCursorEvents(cookie, maxPages = 30, pageSize = 500) {
481
+ const all = [];
482
+ for (let page = 1; page <= maxPages; page++) {
483
+ const res = await fetch(EVENTS_URL, {
484
+ method: "POST",
485
+ headers: {
486
+ "Content-Type": "application/json",
487
+ Origin: "https://cursor.com",
488
+ Cookie: cookie
489
+ },
490
+ body: JSON.stringify({ page, pageSize }),
491
+ signal: AbortSignal.timeout(2e4)
492
+ });
493
+ if (!res.ok) break;
494
+ const body = await res.json();
495
+ const batch = body.usageEventsDisplay ?? [];
496
+ all.push(...batch);
497
+ if (batch.length < pageSize) break;
498
+ }
499
+ return all;
500
+ }
501
+ async function collectCursor() {
502
+ try {
503
+ const db = cursorDbPath();
504
+ const token = db ? readCursorToken(db) : null;
505
+ const cookie = token ? cursorCookie(token) : null;
506
+ if (cookie) {
507
+ const events = await fetchCursorEvents(cookie);
508
+ const { entries, blocks } = mapCursorEvents(events);
509
+ if (entries.length > 0) return { entries, blocks, found: true };
510
+ }
511
+ } catch {
512
+ }
513
+ try {
514
+ const entries = collectCursorViaTokscale();
515
+ if (entries.length > 0) return { entries, blocks: [], found: true };
516
+ } catch {
517
+ }
518
+ return { entries: [], blocks: [], found: false };
519
+ }
520
+
521
+ // src/collect.ts
272
522
  var SOURCES = [
273
523
  "claude",
274
524
  "codex",
@@ -400,18 +650,18 @@ function mapCcusageBlocks(json) {
400
650
  return out;
401
651
  }
402
652
  function resolveCcusageBin() {
403
- const require3 = createRequire(import.meta.url);
653
+ const require3 = createRequire3(import.meta.url);
404
654
  const pkgPath = require3.resolve("ccusage/package.json");
405
655
  const pkg = require3("ccusage/package.json");
406
656
  const rel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.ccusage ?? "ccusage";
407
- const binPath = join3(dirname(pkgPath), rel);
657
+ const binPath = join5(dirname2(pkgPath), rel);
408
658
  if (/\.(c|m)?js$/.test(binPath)) {
409
659
  return { cmd: process.execPath, prefixArgs: [binPath] };
410
660
  }
411
661
  return { cmd: binPath, prefixArgs: [] };
412
662
  }
413
663
  function runCcusage(cmd, args) {
414
- const res = spawnSync2(cmd, args, {
664
+ const res = spawnSync4(cmd, args, {
415
665
  encoding: "utf8",
416
666
  maxBuffer: 64 * 1024 * 1024,
417
667
  timeout: 12e4
@@ -423,7 +673,7 @@ function runCcusage(cmd, args) {
423
673
  return null;
424
674
  }
425
675
  }
426
- function collectAll() {
676
+ async function collectAll() {
427
677
  const { cmd, prefixArgs } = resolveCcusageBin();
428
678
  const entries = [];
429
679
  const toolsFound = [];
@@ -446,6 +696,12 @@ function collectAll() {
446
696
  const blockJson = runCcusage(cmd, [...prefixArgs, "blocks", "--json", "--offline"]);
447
697
  const sessions = sessionJson ? mapCcusageSessions(sessionJson) : [];
448
698
  const blocks = blockJson ? mapCcusageBlocks(blockJson) : [];
699
+ const cursor = await collectCursor();
700
+ if (cursor.found) {
701
+ entries.push(...cursor.entries);
702
+ blocks.push(...cursor.blocks);
703
+ toolsFound.push("cursor");
704
+ }
449
705
  return { entries, sessions, blocks, toolsFound };
450
706
  }
451
707
 
@@ -4778,10 +5034,10 @@ async function publishLocal(payload, deps) {
4778
5034
  }
4779
5035
 
4780
5036
  // src/index.ts
4781
- var require2 = createRequire2(import.meta.url);
5037
+ var require2 = createRequire4(import.meta.url);
4782
5038
  var VERSION = require2("../package.json").version;
4783
5039
  function openBrowser(url) {
4784
- const os = platform2();
5040
+ const os = platform3();
4785
5041
  const [cmd, args] = os === "darwin" ? ["open", [url]] : os === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
4786
5042
  spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
4787
5043
  }
@@ -4816,7 +5072,7 @@ async function confirm(question) {
4816
5072
  function showLocalDashboard(entries) {
4817
5073
  const dir = defaultConfigDir();
4818
5074
  mkdirSync3(dir, { recursive: true });
4819
- const file = join4(dir, "dashboard.html");
5075
+ const file = join6(dir, "dashboard.html");
4820
5076
  writeFileSync3(file, renderDashboardHtml(entries));
4821
5077
  console.log();
4822
5078
  console.log(` Local dashboard: ${pc2.cyan(`file://${file}`)}`);
@@ -4826,8 +5082,9 @@ function showLocalDashboard(entries) {
4826
5082
  async function run(flags) {
4827
5083
  if (!flags.quiet) {
4828
5084
  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"));
4829
5086
  }
4830
- const { entries, sessions, blocks, toolsFound } = collectAll();
5087
+ const { entries, sessions, blocks, toolsFound } = await collectAll();
4831
5088
  if (entries.length === 0) {
4832
5089
  console.log();
4833
5090
  console.log(" Nothing to burn yet \u2014 no local usage found from any coding agent.");
@@ -4864,6 +5121,10 @@ async function run(flags) {
4864
5121
  const config = loadConfig();
4865
5122
  if (config?.token) {
4866
5123
  const result = await submitUsage(config.token, payload);
5124
+ if (!flags.quiet) {
5125
+ console.log(pc2.dim(" Opening your dashboard in your browser\u2026"));
5126
+ openBrowser(result.boardUrl ?? result.profileUrl);
5127
+ }
4867
5128
  console.log(
4868
5129
  ` Submitted ${pc2.bold(String(result.upserted))} day-entries from ${toolsFound.join(", ")}.`
4869
5130
  );
@@ -4876,10 +5137,14 @@ async function run(flags) {
4876
5137
  if (result.boardUrl) {
4877
5138
  console.log(` \u{1F91D} You're on the friends board: ${pc2.cyan(result.boardUrl)}`);
4878
5139
  }
4879
- if (!flags.quiet) openBrowser(result.boardUrl ?? result.profileUrl);
4880
5140
  } else {
4881
5141
  const anonKey = ensureAnonKey();
4882
5142
  const result = await anonSubmit(anonKey, payload);
5143
+ const target = result.boardUrl ?? claimUrl(result.dashboardUrl, anonKey);
5144
+ if (!flags.quiet) {
5145
+ console.log(pc2.dim(" Opening your dashboard in your browser\u2026"));
5146
+ openBrowser(target);
5147
+ }
4883
5148
  console.log(
4884
5149
  ` Submitted ${pc2.bold(String(result.upserted))} day-entries from ${toolsFound.join(", ")}.`
4885
5150
  );
@@ -4889,19 +5154,14 @@ async function run(flags) {
4889
5154
  );
4890
5155
  console.log(` ${pc2.cyan(result.boardUrl)}`);
4891
5156
  console.log(pc2.dim(` Your dashboard: ${result.dashboardUrl}`));
4892
- if (!flags.quiet) {
4893
- openBrowser(result.boardUrl);
4894
- console.log(pc2.dim(" Opened the board \u2014 see how you stack up. Re-run anytime to update."));
4895
- }
4896
5157
  } else {
4897
5158
  console.log(
4898
5159
  ` You burned ${pc2.bold(formatTokens(result.totalTokens))} tokens \u2014 you're on the public leaderboard:`
4899
5160
  );
4900
5161
  console.log(` ${pc2.cyan(result.dashboardUrl)}`);
4901
5162
  if (!flags.quiet) {
4902
- openBrowser(claimUrl(result.dashboardUrl, anonKey));
4903
5163
  console.log(
4904
- pc2.dim(" Opened your dashboard \u2014 claim it (name + X) to own your rank, or make it private / remove it.")
5164
+ pc2.dim(" Claim it (name + X) to own your rank, or make it private / remove it.")
4905
5165
  );
4906
5166
  console.log(
4907
5167
  pc2.dim(" Manage anytime: `npx whoburnedmore private` \xB7 `npx whoburnedmore public` \xB7 `npx whoburnedmore remove`.")
@@ -4910,15 +5170,10 @@ async function run(flags) {
4910
5170
  }
4911
5171
  }
4912
5172
  console.log();
4913
- if (!flags.quiet && !autoSyncInstalled()) {
4914
- if (await confirm(" Keep your stats live? Install background sync (every 3h)?")) {
4915
- console.log(` ${installAutoSync()}`);
4916
- console.log(pc2.dim(" Your page now updates in the background. Stop anytime with `npx whoburnedmore uninstall-sync`."));
4917
- } else {
4918
- console.log(pc2.dim(" OK \u2014 re-run `npx whoburnedmore` anytime, or `npx whoburnedmore install-sync` later."));
4919
- }
4920
- } else if (!flags.quiet && autoSyncInstalled()) {
4921
- console.log(pc2.dim(" Background sync is on \u2014 your page keeps updating automatically. Stop with `npx whoburnedmore uninstall-sync`."));
5173
+ if (!flags.quiet) {
5174
+ console.log(
5175
+ autoSyncInstalled() ? pc2.dim(" Background sync is on \u2014 your page keeps updating automatically (`npx whoburnedmore uninstall-sync` to stop).") : pc2.dim(" Re-run anytime to update \xB7 `npx whoburnedmore install-sync` to keep it live in the background.")
5176
+ );
4922
5177
  }
4923
5178
  }
4924
5179
  async function main() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whoburnedmore",
3
- "version": "0.2.0",
3
+ "version": "0.3.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": {
@@ -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",
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",
14
14
  "test": "vitest run",
15
15
  "lint": "tsc -p tsconfig.json --noEmit",
16
16
  "prepublishOnly": "pnpm run build",
@@ -20,7 +20,8 @@
20
20
  },
21
21
  "dependencies": {
22
22
  "ccusage": "20.0.9",
23
- "picocolors": "^1.1.1"
23
+ "picocolors": "^1.1.1",
24
+ "tokscale": "^1.2.7"
24
25
  },
25
26
  "devDependencies": {
26
27
  "@types/node": "^22.10.0",