whoburnedmore 0.9.4 → 0.9.5

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 +444 -67
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -7,6 +7,7 @@ var __export = (target, all) => {
7
7
 
8
8
  // src/index.ts
9
9
  import { spawn } from "node:child_process";
10
+ import { createHash } from "node:crypto";
10
11
  import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "node:fs";
11
12
  import { createRequire as createRequire4 } from "node:module";
12
13
  import { platform as platform3 } from "node:os";
@@ -95,12 +96,14 @@ async function readJson(res) {
95
96
  };
96
97
  }
97
98
  }
98
- async function post(path, body) {
99
+ async function post(path, body, token) {
100
+ const headers = { "Content-Type": "application/json" };
101
+ if (token) headers.Authorization = `Bearer ${token}`;
99
102
  let res;
100
103
  try {
101
104
  res = await fetch(`${apiBase()}${path}`, {
102
105
  method: "POST",
103
- headers: { "Content-Type": "application/json" },
106
+ headers,
104
107
  body: JSON.stringify(body),
105
108
  // Bound the request so a slow/black-holing/hostile server can't hang the CLI
106
109
  // — or the unattended 15-minute background sync — indefinitely.
@@ -113,6 +116,17 @@ async function post(path, body) {
113
116
  }
114
117
  return { status: res.status, body: await readJson(res) };
115
118
  }
119
+ async function verifyUsage(token, payload) {
120
+ const { status, body } = await post("/v1/verify", payload, token);
121
+ if (status === 401) throw new UnauthorizedError();
122
+ if (status !== 200) {
123
+ const err = body;
124
+ const details = err.details?.length ? `
125
+ - ${err.details.join("\n - ")}` : "";
126
+ throw new Error(`${err.error ?? "verification failed"}${details}`);
127
+ }
128
+ return body;
129
+ }
116
130
  async function anonSubmit(anonKey, payload) {
117
131
  const { status, body } = await post("/v1/anon/submit", { ...payload, anonKey });
118
132
  if (status !== 200) {
@@ -123,6 +137,45 @@ async function anonSubmit(anonKey, payload) {
123
137
  }
124
138
  return body;
125
139
  }
140
+ var UnauthorizedError = class extends Error {
141
+ constructor(message = "your sign-in has expired") {
142
+ super(message);
143
+ this.name = "UnauthorizedError";
144
+ }
145
+ };
146
+ async function deviceStart() {
147
+ const { status, body } = await post(
148
+ "/v1/auth/device",
149
+ {}
150
+ );
151
+ if (status !== 200) {
152
+ throw new Error(
153
+ body.error ?? `sign-in failed (HTTP ${status})`
154
+ );
155
+ }
156
+ return body;
157
+ }
158
+ async function devicePoll(deviceCode) {
159
+ const { status, body } = await post(
160
+ "/v1/auth/device/token",
161
+ { deviceCode }
162
+ );
163
+ if (status !== 200) {
164
+ throw new Error(`sign-in failed (HTTP ${status})`);
165
+ }
166
+ return body;
167
+ }
168
+ async function submit(token, payload) {
169
+ const { status, body } = await post("/v1/submit", payload, token);
170
+ if (status === 401) throw new UnauthorizedError();
171
+ if (status !== 200) {
172
+ const err = body;
173
+ const details = err.details?.length ? `
174
+ - ${err.details.join("\n - ")}` : "";
175
+ throw new Error(`${err.error ?? `submit failed (HTTP ${status})`}${details}`);
176
+ }
177
+ return body;
178
+ }
126
179
  function claimUrl(dashboardUrl, anonKey) {
127
180
  return `${dashboardUrl}#k=${encodeURIComponent(anonKey)}`;
128
181
  }
@@ -204,6 +257,8 @@ function loadConfig(dir = defaultConfigDir()) {
204
257
  const parsed = JSON.parse(readFileSync(file, "utf8"));
205
258
  const config = {};
206
259
  if (typeof parsed.anonKey === "string") config.anonKey = parsed.anonKey;
260
+ if (typeof parsed.cliToken === "string") config.cliToken = parsed.cliToken;
261
+ if (typeof parsed.handle === "string") config.handle = parsed.handle;
207
262
  if (typeof parsed.lastSyncAt === "number" && Number.isFinite(parsed.lastSyncAt))
208
263
  config.lastSyncAt = parsed.lastSyncAt;
209
264
  if (typeof parsed.launchNotificationDeliveredAt === "number" && Number.isFinite(parsed.launchNotificationDeliveredAt)) {
@@ -248,6 +303,16 @@ function recordLaunchNotificationDelivered(dir = defaultConfigDir(), when = Date
248
303
  const config = loadConfig(dir) ?? {};
249
304
  saveConfig(dir, { ...config, launchNotificationDeliveredAt: when });
250
305
  }
306
+ function saveAuth(dir = defaultConfigDir(), auth = { cliToken: "" }) {
307
+ const config = loadConfig(dir) ?? {};
308
+ saveConfig(dir, { ...config, cliToken: auth.cliToken, handle: auth.handle });
309
+ }
310
+ function clearAuth(dir = defaultConfigDir()) {
311
+ const config = loadConfig(dir);
312
+ if (!config?.cliToken && !config?.handle) return;
313
+ const { cliToken: _t, handle: _h, ...rest } = config;
314
+ saveConfig(dir, rest);
315
+ }
251
316
 
252
317
  // src/autosync.ts
253
318
  var SYNC_INTERVAL_MINUTES = 15;
@@ -1332,6 +1397,8 @@ function parseClaudeLine(raw) {
1332
1397
  if (message.role !== void 0 && message.role !== "assistant") return null;
1333
1398
  const date = localDate(String(obj.timestamp ?? ""));
1334
1399
  if (!date) return null;
1400
+ const tsParsed = Date.parse(String(obj.timestamp ?? ""));
1401
+ const ts = Number.isFinite(tsParsed) ? tsParsed : 0;
1335
1402
  const messageId = typeof message.id === "string" ? message.id : "";
1336
1403
  const requestId = typeof obj.requestId === "string" ? obj.requestId : "";
1337
1404
  const hasRealId = messageId !== "" || requestId !== "";
@@ -1340,6 +1407,7 @@ function parseClaudeLine(raw) {
1340
1407
  key,
1341
1408
  hasRealId,
1342
1409
  date,
1410
+ ts,
1343
1411
  model: typeof message.model === "string" ? message.model : "unknown",
1344
1412
  inputTokens: num3(usage.input_tokens),
1345
1413
  outputTokens: num3(usage.output_tokens),
@@ -1458,6 +1526,28 @@ async function collectClaudeNative(env = process.env, opts = {}) {
1458
1526
  }
1459
1527
  return { entries: finalizeClaudeEntries(acc), found: true, filesScanned: scanned };
1460
1528
  }
1529
+ async function collectClaudeRequests(env = process.env, opts = {}) {
1530
+ const dirs = resolveClaudeProjectDirs(env);
1531
+ const files = [];
1532
+ for (const dir of dirs) files.push(...await listJsonl(dir));
1533
+ if (files.length === 0) return { requests: [], found: false };
1534
+ const now = opts.now ?? Date.now;
1535
+ const deadline = now() + (opts.budgetMs ?? NATIVE_READ_BUDGET_MS);
1536
+ const acc = /* @__PURE__ */ new Map();
1537
+ for (const f of files) {
1538
+ if (now() > deadline) {
1539
+ return { requests: [...acc.values()], found: true, timedOut: true };
1540
+ }
1541
+ let content;
1542
+ try {
1543
+ content = await readFile(f, "utf8");
1544
+ } catch {
1545
+ continue;
1546
+ }
1547
+ accumulateClaudeLines(acc, splitLines(content));
1548
+ }
1549
+ return { requests: [...acc.values()], found: true };
1550
+ }
1461
1551
 
1462
1552
  // src/native/codex.ts
1463
1553
  import { readdir as readdir2, readFile as readFile2 } from "node:fs/promises";
@@ -1491,9 +1581,8 @@ function readTokenFields(payload) {
1491
1581
  }
1492
1582
  function parseCodexRollout(lines) {
1493
1583
  let model = "unknown";
1494
- let lastDate = null;
1495
- let last = null;
1496
- let turnCount = 0;
1584
+ const perDay = /* @__PURE__ */ new Map();
1585
+ let lastSeenDate = null;
1497
1586
  for (const raw of lines) {
1498
1587
  const trimmed = raw.trim();
1499
1588
  if (!trimmed) continue;
@@ -1512,49 +1601,68 @@ function parseCodexRollout(lines) {
1512
1601
  if (payload.type === "token_count") {
1513
1602
  const fields = readTokenFields(payload);
1514
1603
  if (!fields) continue;
1515
- last = fields;
1516
- turnCount += 1;
1517
- const d = localDate2(String(obj.timestamp ?? ""));
1518
- if (d) lastDate = d;
1604
+ const parsed = localDate2(String(obj.timestamp ?? ""));
1605
+ const day = parsed ?? lastSeenDate;
1606
+ if (!day) continue;
1607
+ lastSeenDate = day;
1608
+ const e = perDay.get(day);
1609
+ if (e) {
1610
+ e.cum = fields;
1611
+ e.turns += 1;
1612
+ } else {
1613
+ perDay.set(day, { cum: fields, turns: 1 });
1614
+ }
1519
1615
  }
1520
1616
  }
1521
- if (!last || !lastDate) return null;
1522
- const cacheReadTokens = last.cached;
1523
- const inputTokens = Math.max(0, last.input - last.cached);
1524
- const outputTokens = last.output + last.reasoning;
1525
- if (inputTokens + outputTokens + cacheReadTokens === 0) return null;
1526
- return {
1527
- date: lastDate,
1528
- model,
1529
- inputTokens,
1530
- outputTokens,
1531
- cacheCreationTokens: 0,
1532
- cacheReadTokens,
1533
- turnCount
1534
- };
1617
+ if (perDay.size === 0) return [];
1618
+ const dates = [...perDay.keys()].sort();
1619
+ const out = [];
1620
+ let prev = { input: 0, cached: 0, output: 0, reasoning: 0 };
1621
+ for (const date of dates) {
1622
+ const { cum, turns } = perDay.get(date);
1623
+ const dInput = Math.max(0, cum.input - prev.input);
1624
+ const dCached = Math.max(0, cum.cached - prev.cached);
1625
+ const dOutput = Math.max(0, cum.output - prev.output);
1626
+ const dReasoning = Math.max(0, cum.reasoning - prev.reasoning);
1627
+ prev = cum;
1628
+ const cacheReadTokens = dCached;
1629
+ const inputTokens = Math.max(0, dInput - dCached);
1630
+ const outputTokens = dOutput + dReasoning;
1631
+ if (inputTokens + outputTokens + cacheReadTokens === 0) continue;
1632
+ out.push({
1633
+ date,
1634
+ model,
1635
+ inputTokens,
1636
+ outputTokens,
1637
+ cacheCreationTokens: 0,
1638
+ cacheReadTokens,
1639
+ turnCount: turns
1640
+ });
1641
+ }
1642
+ return out;
1535
1643
  }
1536
1644
  function accumulateCodexSession(acc, lines) {
1537
- const s = parseCodexRollout(lines);
1538
- if (!s) return;
1539
- const k = `${s.date}|${s.model}`;
1540
- let b = acc.get(k);
1541
- if (!b) {
1542
- b = {
1543
- date: s.date,
1544
- model: s.model,
1545
- inputTokens: 0,
1546
- outputTokens: 0,
1547
- cacheCreationTokens: 0,
1548
- cacheReadTokens: 0,
1549
- requestCount: 0
1550
- };
1551
- acc.set(k, b);
1645
+ for (const s of parseCodexRollout(lines)) {
1646
+ const k = `${s.date}|${s.model}`;
1647
+ let b = acc.get(k);
1648
+ if (!b) {
1649
+ b = {
1650
+ date: s.date,
1651
+ model: s.model,
1652
+ inputTokens: 0,
1653
+ outputTokens: 0,
1654
+ cacheCreationTokens: 0,
1655
+ cacheReadTokens: 0,
1656
+ requestCount: 0
1657
+ };
1658
+ acc.set(k, b);
1659
+ }
1660
+ b.inputTokens += s.inputTokens;
1661
+ b.outputTokens += s.outputTokens;
1662
+ b.cacheCreationTokens += s.cacheCreationTokens;
1663
+ b.cacheReadTokens += s.cacheReadTokens;
1664
+ b.requestCount += s.turnCount;
1552
1665
  }
1553
- b.inputTokens += s.inputTokens;
1554
- b.outputTokens += s.outputTokens;
1555
- b.cacheCreationTokens += s.cacheCreationTokens;
1556
- b.cacheReadTokens += s.cacheReadTokens;
1557
- b.requestCount += s.turnCount;
1558
1666
  }
1559
1667
  function finalizeCodexEntries(acc) {
1560
1668
  const entries = [];
@@ -6241,6 +6349,32 @@ var AnonSubmitPayload = SubmitPayload.extend({
6241
6349
  /** Client-generated secret (hex). The server stores only its hash. */
6242
6350
  anonKey: external_exports.string().min(16).max(128)
6243
6351
  });
6352
+ var VerifyRequestRecord = external_exports.object({
6353
+ /** Local calendar date (YYYY-MM-DD) this request is bucketed under. */
6354
+ date: DateString,
6355
+ /** Epoch milliseconds of the request's final (max-token) transcript line. */
6356
+ ts: external_exports.number().int().nonnegative(),
6357
+ tool: external_exports.string().min(1).max(64),
6358
+ model: external_exports.string().min(1).max(128),
6359
+ inputTokens: tokenCount,
6360
+ outputTokens: tokenCount,
6361
+ cacheCreationTokens: tokenCount,
6362
+ cacheReadTokens: tokenCount,
6363
+ /** sha256(msgId|reqId), truncated — preserves uniqueness without revealing the id. */
6364
+ reqHash: external_exports.string().min(1).max(64)
6365
+ });
6366
+ var VerifyPayload = external_exports.object({
6367
+ cliVersion: external_exports.string().min(1).max(32),
6368
+ /** The per-request skeleton (bounded/sampled — see `truncated`). */
6369
+ requests: external_exports.array(VerifyRequestRecord).min(1).max(1e5),
6370
+ /**
6371
+ * True when the local corpus exceeded the client upload cap and was sampled to
6372
+ * the most recent N requests. The server treats a truncated upload conservatively
6373
+ * (it never AUTO-passes a truncated one — it can still auto-FAIL on a physical
6374
+ * impossibility, or route to a human).
6375
+ */
6376
+ truncated: external_exports.boolean().optional()
6377
+ });
6244
6378
  function entryTotalTokens(e) {
6245
6379
  return e.inputTokens + e.outputTokens + e.cacheCreationTokens + e.cacheReadTokens;
6246
6380
  }
@@ -6375,6 +6509,26 @@ function submitNextStepLines(result) {
6375
6509
  " Private until you do. Manage anytime: `npx whoburnedmore private` \xB7 `public` \xB7 `remove`."
6376
6510
  ];
6377
6511
  }
6512
+ function signedInNextStepLines(result) {
6513
+ if (result.orgBoardUrl) {
6514
+ return [
6515
+ ` \u{1F3E2} You're on your team board: ${sanitizeServerText(result.orgBoardUrl)}`,
6516
+ " \u2192 Open it to see who burned more."
6517
+ ];
6518
+ }
6519
+ if (result.boardUrl) {
6520
+ const code = result.boardUrl.split("/").filter(Boolean).pop() ?? "";
6521
+ return [
6522
+ ` \u{1F91D} You're on the board: ${sanitizeServerText(result.boardUrl)}`,
6523
+ " \u2192 Open it to see who burned more.",
6524
+ ` \u2192 Get a friend on it \u2014 have them run: npx whoburnedmore --board=${sanitizeServerText(code)}`
6525
+ ];
6526
+ }
6527
+ return [
6528
+ ` Your dashboard: ${sanitizeServerText(result.profileUrl)}`,
6529
+ " \u2192 Open it to see your rank and share your profile."
6530
+ ];
6531
+ }
6378
6532
 
6379
6533
  // src/local-dashboard.ts
6380
6534
  function esc(s) {
@@ -6752,7 +6906,132 @@ async function run(flags) {
6752
6906
  console.log(pc2.dim(" --no-submit: skipped the dashboard."));
6753
6907
  return;
6754
6908
  }
6755
- const anonKey = ensureAnonKey();
6909
+ const cfg = loadConfig();
6910
+ const canSignIn = !flags.quiet && Boolean(process.stdout.isTTY);
6911
+ if (cfg?.cliToken) {
6912
+ await submitSignedIn(cfg.cliToken, payload, flags, canSignIn);
6913
+ } else if (cfg?.anonKey) {
6914
+ await submitWithDeviceKey(cfg.anonKey, payload, flags);
6915
+ } else if (canSignIn) {
6916
+ const auth = await ensureSignedIn();
6917
+ if (!auth) return;
6918
+ await submitSignedIn(auth.token, payload, flags, canSignIn);
6919
+ } else if (!flags.quiet) {
6920
+ console.log(pc2.yellow(" Sign in to put your usage on the leaderboard."));
6921
+ console.log(
6922
+ pc2.dim(
6923
+ " Run `npx whoburnedmore` in an interactive terminal to sign in, or `npx whoburnedmore link --token=\u2026` (from your signed-in profile) for servers/CI."
6924
+ )
6925
+ );
6926
+ } else {
6927
+ return;
6928
+ }
6929
+ }
6930
+ function sleep(ms) {
6931
+ return new Promise((resolve) => setTimeout(resolve, ms));
6932
+ }
6933
+ async function ensureSignedIn() {
6934
+ const cfg = loadConfig();
6935
+ if (cfg?.cliToken) return { token: cfg.cliToken, handle: cfg.handle ?? "" };
6936
+ let dev;
6937
+ try {
6938
+ dev = await deviceStart();
6939
+ } catch (err) {
6940
+ console.log(pc2.yellow(` Couldn't start sign-in: ${err.message}`));
6941
+ return null;
6942
+ }
6943
+ console.log();
6944
+ console.log(pc2.bold(" Sign in to whoburnedmore to put your usage on the board."));
6945
+ console.log(` 1. Opening ${pc2.cyan(sanitizeServerText(dev.verifyUrl))} \u2014 or go there yourself.`);
6946
+ console.log(` 2. Approve this code: ${pc2.bold(sanitizeServerText(dev.userCode))}`);
6947
+ if (isTrustedWebUrl(dev.verifyUrl)) openBrowser(dev.verifyUrl);
6948
+ console.log(pc2.dim(" Waiting for you to approve in the browser\u2026"));
6949
+ const deadline = Date.now() + dev.expiresInSeconds * 1e3;
6950
+ const intervalMs = Math.max(1, dev.pollIntervalSeconds) * 1e3;
6951
+ while (Date.now() < deadline) {
6952
+ await sleep(intervalMs);
6953
+ let res;
6954
+ try {
6955
+ res = await devicePoll(dev.deviceCode);
6956
+ } catch {
6957
+ continue;
6958
+ }
6959
+ if (res.status === "ok") {
6960
+ saveAuth(void 0, { cliToken: res.token, handle: res.handle });
6961
+ console.log(pc2.green(` \u2713 Signed in as @${sanitizeServerText(res.handle)}.`));
6962
+ return { token: res.token, handle: res.handle };
6963
+ }
6964
+ if (res.status === "expired") break;
6965
+ }
6966
+ console.log(
6967
+ pc2.yellow(" Sign-in timed out. Run `npx whoburnedmore` again to retry.")
6968
+ );
6969
+ return null;
6970
+ }
6971
+ async function submitSignedIn(token, payload, flags, interactive) {
6972
+ let result;
6973
+ try {
6974
+ result = await submit(token, payload);
6975
+ } catch (err) {
6976
+ if (err instanceof UnauthorizedError) {
6977
+ clearAuth();
6978
+ if (!interactive) return;
6979
+ const auth = await ensureSignedIn();
6980
+ if (!auth) return;
6981
+ result = await submit(auth.token, payload);
6982
+ } else {
6983
+ throw err;
6984
+ }
6985
+ }
6986
+ try {
6987
+ recordSync();
6988
+ } catch {
6989
+ }
6990
+ const baseUrl = result.orgBoardUrl ?? result.boardUrl ?? result.profileUrl;
6991
+ if (!flags.quiet) {
6992
+ console.log(
6993
+ pc2.green(" \u2713 Synced securely.") + pc2.dim(" Only your daily totals left this machine \u2014 never your prompts, code, or file names.")
6994
+ );
6995
+ if (result.suppressed) {
6996
+ console.log();
6997
+ console.log(
6998
+ pc2.yellow(
6999
+ " \u26A0 You're currently OFF the leaderboard \u2014 we couldn't verify these numbers."
7000
+ )
7001
+ );
7002
+ console.log(
7003
+ pc2.dim(
7004
+ " Verify your usage to get back on \u2014 this sends a detailed per-request breakdown (timestamps + token counts, never your prompts or code)."
7005
+ )
7006
+ );
7007
+ if (process.stdin.isTTY && await confirm(" Verify now?")) {
7008
+ await runVerify();
7009
+ afterSubmitChores(flags);
7010
+ return;
7011
+ }
7012
+ console.log(
7013
+ pc2.dim(
7014
+ " Run `npx whoburnedmore verify` anytime, or appeal at whoburnedmore.com/appeal."
7015
+ )
7016
+ );
7017
+ }
7018
+ if (isTrustedWebUrl(baseUrl)) {
7019
+ console.log(pc2.dim(" Opening your dashboard in your browser\u2026"));
7020
+ openBrowser(baseUrl);
7021
+ } else {
7022
+ console.log(
7023
+ pc2.dim(" The server returned an unexpected dashboard address, so it was NOT auto-opened. Open it yourself only if you trust it:")
7024
+ );
7025
+ console.log(` ${sanitizeServerText(baseUrl)}`);
7026
+ }
7027
+ for (const line of signedInNextStepLines(result)) {
7028
+ if (line.includes("\u2192")) console.log(pc2.bold(line));
7029
+ else console.log(line);
7030
+ }
7031
+ }
7032
+ afterSubmitChores(flags);
7033
+ }
7034
+ async function submitWithDeviceKey(anonKey, payload, flags) {
6756
7035
  const result = await anonSubmit(anonKey, payload);
6757
7036
  try {
6758
7037
  recordSync();
@@ -6782,26 +7061,24 @@ async function run(flags) {
6782
7061
  );
6783
7062
  console.log(` ${sanitizeServerText(baseUrl)}`);
6784
7063
  }
6785
- }
6786
- const lines = submitNextStepLines(result);
6787
- for (const line of lines) {
6788
- if (line.includes("\u2192")) console.log(pc2.bold(line));
6789
- else if (line.startsWith(" Private until you do")) {
6790
- if (!flags.quiet) console.log(pc2.dim(line));
6791
- } else console.log(line);
6792
- }
6793
- if (!flags.quiet) {
6794
- try {
6795
- reconcileAutoSync();
6796
- } catch {
7064
+ for (const line of submitNextStepLines(result)) {
7065
+ if (line.includes("\u2192")) console.log(pc2.bold(line));
7066
+ else if (line.startsWith(" Private until you do")) console.log(pc2.dim(line));
7067
+ else console.log(line);
6797
7068
  }
6798
7069
  }
6799
- if (!flags.quiet) {
6800
- console.log();
6801
- console.log(
6802
- autoSyncInstalled() ? pc2.dim(" Background sync is on \u2014 your page updates automatically every 15 min (`npx whoburnedmore uninstall-sync` to stop).") : pc2.dim(" Re-run anytime to update your page.")
6803
- );
7070
+ afterSubmitChores(flags);
7071
+ }
7072
+ function afterSubmitChores(flags) {
7073
+ if (flags.quiet) return;
7074
+ try {
7075
+ reconcileAutoSync();
7076
+ } catch {
6804
7077
  }
7078
+ console.log();
7079
+ console.log(
7080
+ autoSyncInstalled() ? pc2.dim(" Background sync is on \u2014 your page updates automatically every 15 min (`npx whoburnedmore uninstall-sync` to stop).") : pc2.dim(" Re-run anytime to update your page.")
7081
+ );
6805
7082
  }
6806
7083
  async function linkServerInstall(token) {
6807
7084
  if (!token) {
@@ -6883,6 +7160,102 @@ async function runDaemon() {
6883
7160
  console.log();
6884
7161
  console.log(pc2.dim(` Daemon stopped after ${cycles} sync cycle${cycles === 1 ? "" : "s"}.`));
6885
7162
  }
7163
+ async function runVerify() {
7164
+ printBanner();
7165
+ console.log();
7166
+ console.log(pc2.bold(" Verify your usage to get back on the leaderboard."));
7167
+ console.log(
7168
+ pc2.dim(
7169
+ " This sends a DETAILED per-request breakdown \u2014 timestamps and token counts, still never your prompts, code, or file names \u2014 so we can confirm your usage is real. It's deleted after review."
7170
+ )
7171
+ );
7172
+ const cfg = loadConfig();
7173
+ let token = cfg?.cliToken;
7174
+ if (!token) {
7175
+ if (!process.stdout.isTTY) {
7176
+ console.log(
7177
+ pc2.yellow(
7178
+ " Sign in first \u2014 run `npx whoburnedmore verify` in an interactive terminal."
7179
+ )
7180
+ );
7181
+ return;
7182
+ }
7183
+ const auth = await ensureSignedIn();
7184
+ if (!auth) return;
7185
+ token = auth.token;
7186
+ }
7187
+ if (process.stdin.isTTY) {
7188
+ const ok = await confirm("\n Send this breakdown to verify your usage?");
7189
+ if (!ok) {
7190
+ console.log(pc2.dim(" Cancelled \u2014 nothing was sent."));
7191
+ return;
7192
+ }
7193
+ }
7194
+ console.log(pc2.dim(" Reading your local Claude Code logs\u2026"));
7195
+ const { requests, found } = await collectClaudeRequests();
7196
+ if (!found || requests.length === 0) {
7197
+ console.log(
7198
+ pc2.yellow(" No local Claude Code logs found on this machine to verify.")
7199
+ );
7200
+ console.log(
7201
+ pc2.dim(
7202
+ " Run this on the machine where you actually use Claude Code, or file a written appeal at whoburnedmore.com/appeal."
7203
+ )
7204
+ );
7205
+ return;
7206
+ }
7207
+ const CAP = 5e4;
7208
+ const sorted = requests.slice().sort((a, b) => b.ts - a.ts);
7209
+ const truncated = sorted.length > CAP;
7210
+ const capped = truncated ? sorted.slice(0, CAP) : sorted;
7211
+ const records = capped.map((r) => ({
7212
+ date: r.date,
7213
+ ts: r.ts,
7214
+ tool: "claude",
7215
+ model: r.model,
7216
+ inputTokens: r.inputTokens,
7217
+ outputTokens: r.outputTokens,
7218
+ cacheCreationTokens: r.cacheCreationTokens,
7219
+ cacheReadTokens: r.cacheReadTokens,
7220
+ reqHash: createHash("sha256").update(r.key).digest("hex").slice(0, 32)
7221
+ }));
7222
+ console.log(
7223
+ pc2.dim(
7224
+ ` Sending ${records.length} request records for analysis${truncated ? " (most recent, sampled)" : ""}\u2026`
7225
+ )
7226
+ );
7227
+ let result;
7228
+ try {
7229
+ result = await verifyUsage(token, {
7230
+ cliVersion: VERSION,
7231
+ requests: records,
7232
+ ...truncated ? { truncated: true } : {}
7233
+ });
7234
+ } catch (err) {
7235
+ if (err instanceof UnauthorizedError) {
7236
+ clearAuth();
7237
+ console.log(
7238
+ pc2.yellow(
7239
+ " Your sign-in expired \u2014 run `npx whoburnedmore verify` again to retry."
7240
+ )
7241
+ );
7242
+ return;
7243
+ }
7244
+ throw err;
7245
+ }
7246
+ console.log();
7247
+ const line = ` ${sanitizeServerText(result.message)}`;
7248
+ if (result.relisted) {
7249
+ console.log(pc2.green(` \u2713${line.slice(1)}`));
7250
+ } else {
7251
+ console.log(result.verdict === "fail" ? pc2.yellow(line) : line);
7252
+ console.log(
7253
+ pc2.dim(
7254
+ " We'll relist you automatically if it checks out \u2014 re-run `npx whoburnedmore` anytime to check."
7255
+ )
7256
+ );
7257
+ }
7258
+ }
6886
7259
  async function main() {
6887
7260
  const major = Number(process.versions.node.split(".")[0]);
6888
7261
  if (major < 20) {
@@ -6918,6 +7291,9 @@ async function main() {
6918
7291
  case "daemon":
6919
7292
  await runDaemon();
6920
7293
  break;
7294
+ case "verify":
7295
+ await runVerify();
7296
+ break;
6921
7297
  case "status":
6922
7298
  case "doctor": {
6923
7299
  for (const line of agentStatusReport()) console.log(line);
@@ -6969,8 +7345,8 @@ function printHelp() {
6969
7345
  ${pc2.bold("whoburnedmore")} \u2014 who burned more tokens, you or them?
6970
7346
 
6971
7347
  ${pc2.bold("usage")}
6972
- npx whoburnedmore burn + land on the public leaderboard, open your dashboard
6973
- npx whoburnedmore --board=CODE compare with friends \u2014 join their board (no sign-in)
7348
+ npx whoburnedmore sign in, burn + land on the public leaderboard, open your dashboard
7349
+ npx whoburnedmore --board=CODE compare with friends \u2014 sign in and join their board
6974
7350
  npx whoburnedmore --org=SLUG submit to your organization's board (companies/hackathons)
6975
7351
  npx whoburnedmore --local build the dashboard on your machine and open it (offline)
6976
7352
  npx whoburnedmore --dry-run print exactly what would be sent, send nothing
@@ -6980,14 +7356,15 @@ function printHelp() {
6980
7356
  npx whoburnedmore private hide your dashboard from the leaderboard
6981
7357
  npx whoburnedmore public put it back on the leaderboard
6982
7358
  npx whoburnedmore remove delete your dashboard and its data
7359
+ npx whoburnedmore verify delisted? re-verify your usage to get back on (sends a detailed breakdown)
6983
7360
  npx whoburnedmore status check background-sync health (last sync, staleness)
6984
7361
  npx whoburnedmore uninstall-sync turn off the background sync
6985
7362
  npx whoburnedmore install-sync turn it back on after uninstalling
6986
7363
 
6987
- Background sync is on by default: after your first run, your page refreshes
6988
- automatically every 15 min (\`uninstall-sync\` to stop). Your dashboard is public on
6989
- the leaderboard as an anonymous burner \u2014 sign in on whoburnedmore.com to claim
6990
- it (handle + X) and own your rank, or run \`private\`/\`remove\` to pull it. Only
7364
+ Your first run signs you in (we open a page, you approve a short code) and binds
7365
+ this machine to your account \u2014 your usage lands on the leaderboard under your
7366
+ handle. Background sync is on by default: your page then refreshes automatically
7367
+ every 15 min (\`uninstall-sync\` to stop); run \`private\`/\`remove\` to pull it. Only
6991
7368
  daily aggregate numbers (date, tool, model, token counts, est. cost) ever leave
6992
7369
  your machine \u2014 never prompts, code, or file names. With --local, nothing leaves
6993
7370
  your machine at all.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whoburnedmore",
3
- "version": "0.9.4",
3
+ "version": "0.9.5",
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": {