whoburnedmore 0.5.0 → 0.6.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 +109 -140
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -29,6 +29,9 @@ function parseBoard(args) {
29
29
  function apiBase() {
30
30
  return process.env.WHOBURNEDMORE_API ?? "https://api.whoburnedmore.com";
31
31
  }
32
+ function webBase() {
33
+ return process.env.WHOBURNEDMORE_WEB ?? "https://whoburnedmore.com";
34
+ }
32
35
  async function readJson(res) {
33
36
  const text = await res.text();
34
37
  if (!text) return {};
@@ -40,15 +43,12 @@ async function readJson(res) {
40
43
  };
41
44
  }
42
45
  }
43
- async function post(path, body, token) {
46
+ async function post(path, body) {
44
47
  let res;
45
48
  try {
46
49
  res = await fetch(`${apiBase()}${path}`, {
47
50
  method: "POST",
48
- headers: {
49
- "Content-Type": "application/json",
50
- ...token ? { Authorization: `Bearer ${token}` } : {}
51
- },
51
+ headers: { "Content-Type": "application/json" },
52
52
  body: JSON.stringify(body)
53
53
  });
54
54
  } catch {
@@ -58,34 +58,6 @@ async function post(path, body, token) {
58
58
  }
59
59
  return { status: res.status, body: await readJson(res) };
60
60
  }
61
- async function deviceStart() {
62
- const { status, body } = await post("/v1/auth/device", {});
63
- if (status !== 200) throw new Error(`device auth failed (HTTP ${status})`);
64
- return body;
65
- }
66
- async function devicePoll(deviceCode) {
67
- const { body } = await post("/v1/auth/device/token", {
68
- deviceCode
69
- });
70
- return body;
71
- }
72
- async function submitUsage(token, payload) {
73
- const { status, body } = await post(
74
- "/v1/submit",
75
- payload,
76
- token
77
- );
78
- if (status === 401) {
79
- throw new Error("session expired \u2014 run `npx whoburnedmore login` again");
80
- }
81
- if (status !== 200) {
82
- const err = body;
83
- const details = err.details?.length ? `
84
- - ${err.details.join("\n - ")}` : "";
85
- throw new Error(`${err.error ?? `submit failed (HTTP ${status})`}${details}`);
86
- }
87
- return body;
88
- }
89
61
  async function anonSubmit(anonKey, payload) {
90
62
  const { status, body } = await post("/v1/anon/submit", { ...payload, anonKey });
91
63
  if (status !== 200) {
@@ -122,14 +94,14 @@ async function anonRemove(anonKey) {
122
94
 
123
95
  // src/autosync.ts
124
96
  import { spawnSync } from "node:child_process";
125
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
97
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
126
98
  import { homedir as homedir2, platform } from "node:os";
127
99
  import { join as join2 } from "node:path";
128
100
  import { fileURLToPath } from "node:url";
129
101
 
130
102
  // src/config.ts
131
103
  import { randomBytes } from "node:crypto";
132
- import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
104
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
133
105
  import { homedir } from "node:os";
134
106
  import { join } from "node:path";
135
107
  function defaultConfigDir() {
@@ -141,8 +113,6 @@ function loadConfig(dir = defaultConfigDir()) {
141
113
  try {
142
114
  const parsed = JSON.parse(readFileSync(file, "utf8"));
143
115
  const config = {};
144
- if (typeof parsed.token === "string") config.token = parsed.token;
145
- if (typeof parsed.handle === "string") config.handle = parsed.handle;
146
116
  if (typeof parsed.anonKey === "string") config.anonKey = parsed.anonKey;
147
117
  return Object.keys(config).length > 0 ? config : null;
148
118
  } catch {
@@ -154,9 +124,6 @@ function saveConfig(dir = defaultConfigDir(), config = {}) {
154
124
  const file = join(dir, "config.json");
155
125
  writeFileSync(file, JSON.stringify(config, null, 2), { mode: 384 });
156
126
  }
157
- function clearConfig(dir = defaultConfigDir()) {
158
- rmSync(join(dir, "config.json"), { force: true });
159
- }
160
127
  function ensureAnonKey(dir = defaultConfigDir()) {
161
128
  const config = loadConfig(dir) ?? {};
162
129
  if (config.anonKey) return config.anonKey;
@@ -250,7 +217,7 @@ function uninstallAutoSync() {
250
217
  const plistPath = launchAgentPath();
251
218
  if (existsSync2(plistPath)) {
252
219
  spawnSync("launchctl", ["unload", plistPath], { stdio: "ignore" });
253
- rmSync2(plistPath, { force: true });
220
+ rmSync(plistPath, { force: true });
254
221
  }
255
222
  return "launchd agent removed";
256
223
  }
@@ -349,9 +316,13 @@ function recordTokens(usage) {
349
316
  function processRecord(rec, acc, ctx) {
350
317
  if (!rec || typeof rec !== "object") return;
351
318
  const r = rec;
319
+ const recTokens = recordTokens(r.message?.usage);
352
320
  if (typeof r.attributionSkill === "string" && r.attributionSkill) {
353
321
  const s = r.attributionSkill.slice(0, 128);
354
- acc.skills.set(s, (acc.skills.get(s) ?? 0) + 1);
322
+ const sk = acc.skills.get(s) ?? { count: 0, tokens: 0 };
323
+ sk.count += 1;
324
+ sk.tokens += recTokens;
325
+ acc.skills.set(s, sk);
355
326
  }
356
327
  if (r.type === "ai-title" && typeof r.aiTitle === "string" && r.aiTitle && typeof r.sessionId === "string" && r.sessionId) {
357
328
  acc.titles.set(r.sessionId, r.aiTitle.slice(0, 200));
@@ -361,18 +332,24 @@ function processRecord(rec, acc, ctx) {
361
332
  const isAssistant = r.type === "assistant" || r.message?.role === "assistant";
362
333
  const isUser = r.type === "user" || r.message?.role === "user";
363
334
  if (isAssistant) {
335
+ const toolUses = [];
364
336
  for (const block of content) {
365
337
  if (block && typeof block === "object" && block.type === "tool_use" && typeof block.name === "string") {
366
338
  const name = block.name.slice(0, 128);
367
339
  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
340
  const id = block.id;
372
- if (typeof id === "string" && id) ctx.toolNames.set(id, name);
341
+ toolUses.push({ name, id: typeof id === "string" ? id : void 0 });
373
342
  }
374
343
  }
375
- const tokens = recordTokens(r.message?.usage);
344
+ const perToolTokens = toolUses.length > 0 ? Math.floor(recTokens / toolUses.length) : 0;
345
+ for (const tu of toolUses) {
346
+ const t = acc.tools.get(tu.name) ?? { count: 0, errors: 0, tokens: 0 };
347
+ t.count += 1;
348
+ t.tokens += perToolTokens;
349
+ acc.tools.set(tu.name, t);
350
+ if (tu.id) ctx.toolNames.set(tu.id, tu.name);
351
+ }
352
+ const tokens = recTokens;
376
353
  acc.agent.messageCount += 1;
377
354
  acc.agent.totalTokens += tokens;
378
355
  const sidechain = r.isSidechain === true;
@@ -411,7 +388,7 @@ function processRecord(rec, acc, ctx) {
411
388
  const id = block.tool_use_id;
412
389
  const name = typeof id === "string" ? ctx.toolNames.get(id) : void 0;
413
390
  if (name) {
414
- const t = acc.tools.get(name) ?? { count: 0, errors: 0 };
391
+ const t = acc.tools.get(name) ?? { count: 0, errors: 0, tokens: 0 };
415
392
  t.errors += 1;
416
393
  acc.tools.set(name, t);
417
394
  }
@@ -420,10 +397,15 @@ function processRecord(rec, acc, ctx) {
420
397
  }
421
398
  }
422
399
  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);
400
+ return [...map.entries()].map(([name, v]) => ({ name, count: v.count, tokens: v.tokens })).filter((s) => s.count > 0).sort((a, b) => b.tokens - a.tokens || b.count - a.count).slice(0, MAX_STATS).map((s) => s.tokens > 0 ? s : { name: s.name, count: s.count });
424
401
  }
425
402
  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 });
403
+ return [...map.entries()].map(([name, v]) => ({ name, count: v.count, errors: v.errors, tokens: v.tokens })).filter((s) => s.count > 0).sort((a, b) => b.tokens - a.tokens || b.count - a.count).slice(0, MAX_STATS).map((s) => {
404
+ const base = { name: s.name, count: s.count };
405
+ if (s.errors > 0) base.errors = s.errors;
406
+ if (s.tokens > 0) base.tokens = s.tokens;
407
+ return base;
408
+ });
427
409
  }
428
410
  function toProjectStats(map) {
429
411
  return [...map.entries()].map(([name, v]) => ({
@@ -5096,7 +5078,9 @@ var ToolStat = external_exports.object({
5096
5078
  name: external_exports.string().min(1).max(128),
5097
5079
  count: external_exports.number().int().nonnegative(),
5098
5080
  /** How many of those calls returned an error/interrupt (tool reliability). Optional. */
5099
- errors: external_exports.number().int().nonnegative().optional()
5081
+ errors: external_exports.number().int().nonnegative().optional(),
5082
+ /** Tokens burned on turns that used this tool (turn tokens split across its tool calls). Optional. */
5083
+ tokens: external_exports.number().int().nonnegative().optional()
5100
5084
  });
5101
5085
  var ProjectStat = external_exports.object({
5102
5086
  name: external_exports.string().min(1).max(128),
@@ -5115,7 +5099,9 @@ var AgentStat = external_exports.object({
5115
5099
  });
5116
5100
  var SkillStat = external_exports.object({
5117
5101
  name: external_exports.string().min(1).max(128),
5118
- count: external_exports.number().int().nonnegative()
5102
+ count: external_exports.number().int().nonnegative(),
5103
+ /** Tokens burned in records produced while this skill was active. Optional. */
5104
+ tokens: external_exports.number().int().nonnegative().optional()
5119
5105
  });
5120
5106
  var SubmitPayload = external_exports.object({
5121
5107
  cliVersion: external_exports.string().min(1).max(32),
@@ -5196,7 +5182,7 @@ function printSummary(entries) {
5196
5182
  function esc(s) {
5197
5183
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
5198
5184
  }
5199
- function renderDashboardHtml(entries, generatedAt = /* @__PURE__ */ new Date()) {
5185
+ function renderDashboardHtml(entries, generatedAt = /* @__PURE__ */ new Date(), connect) {
5200
5186
  const today = generatedAt.toISOString().slice(0, 10);
5201
5187
  const totals = {
5202
5188
  tokens: 0,
@@ -5248,6 +5234,18 @@ function renderDashboardHtml(entries, generatedAt = /* @__PURE__ */ new Date())
5248
5234
  ([model, a]) => `<tr><td class="mono">${esc(model)}</td><td class="num">${esc(formatTokens(a.tokens))}</td><td class="num">${esc(formatUSD(a.cost))}</td></tr>`
5249
5235
  ).join("");
5250
5236
  const stat = (label, value, accent = false) => `<div class="card"><div class="label">${label}</div><div class="value${accent ? " accent" : ""}">${esc(value)}</div></div>`;
5237
+ const connectCta = connect ? `
5238
+ <form class="connect" method="POST" action="${esc(connect.webBaseUrl)}/connect">
5239
+ <input type="hidden" name="payload" value="${Buffer.from(JSON.stringify(connect.payload)).toString("base64")}">
5240
+ <div class="connect-row">
5241
+ <div>
5242
+ <div class="connect-title">Connect your account</div>
5243
+ <div class="connect-sub">Save this dashboard to your account and claim your spot on the public leaderboard. The local numbers above become your starting point \u2014 nothing has left your machine yet.</div>
5244
+ </div>
5245
+ <button type="submit">Connect your account \u2192</button>
5246
+ </div>
5247
+ <div class="connect-note">After connecting, run <code>npx whoburnedmore</code> (no flag) once so it keeps syncing automatically in the background.</div>
5248
+ </form>` : "";
5251
5249
  return `<!doctype html>
5252
5250
  <html lang="en">
5253
5251
  <head>
@@ -5285,13 +5283,22 @@ function renderDashboardHtml(entries, generatedAt = /* @__PURE__ */ new Date())
5285
5283
  td.mono { font-family: ui-monospace, monospace; font-size: 12px; }
5286
5284
  .foot { color: #78716c; font-size: 12px; margin-top: 32px; }
5287
5285
  .foot code { color: #d6d3d1; }
5286
+ .connect { display: block; margin: 24px 0 4px; background: linear-gradient(135deg, rgba(234,88,12,.16), rgba(249,115,22,.06)); border: 1px solid rgba(234,88,12,.5); border-radius: 14px; padding: 18px 20px; }
5287
+ .connect-row { display: flex; flex-direction: column; gap: 14px; align-items: flex-start; }
5288
+ @media (min-width: 640px) { .connect-row { flex-direction: row; align-items: center; justify-content: space-between; } }
5289
+ .connect-title { font-size: 16px; font-weight: 700; }
5290
+ .connect-sub { color: #d6d3d1; font-size: 13px; margin-top: 4px; max-width: 60ch; }
5291
+ .connect button { flex-shrink: 0; cursor: pointer; border: 0; border-radius: 10px; background: #ea580c; color: #fff; font-size: 14px; font-weight: 600; padding: 11px 18px; font-family: inherit; transition: background .15s; }
5292
+ .connect button:hover { background: #f97316; }
5293
+ .connect-note { color: #a8a29e; font-size: 12px; margin-top: 14px; padding-top: 12px; border-top: 1px solid rgba(234,88,12,.25); }
5294
+ .connect-note code { color: #fed7aa; background: rgba(0,0,0,.25); padding: 1px 6px; border-radius: 5px; }
5288
5295
  </style>
5289
5296
  </head>
5290
5297
  <body>
5291
5298
  <div class="wrap">
5292
5299
  <h1>who burned more<span class="q">?</span></h1>
5293
5300
  <div class="sub">your local burn report \xB7 generated ${esc(generatedAt.toISOString().slice(0, 16).replace("T", " "))} \xB7 nothing left your machine</div>
5294
-
5301
+ ${connectCta}
5295
5302
  <div class="grid">
5296
5303
  ${stat("total tokens", formatTokens(totals.tokens), true)}
5297
5304
  ${stat("est. cost", formatUSD(totals.cost))}
@@ -5335,7 +5342,7 @@ function renderDashboardHtml(entries, generatedAt = /* @__PURE__ */ new Date())
5335
5342
 
5336
5343
  <div class="foot">
5337
5344
  Re-run <code>npx whoburnedmore --local</code> to refresh this page.<br>
5338
- Run <code>npx whoburnedmore</code> (no flag) to get a shareable dashboard at whoburnedmore.com \u2014 no sign-in.
5345
+ ${connect ? "Use \u201CConnect your account\u201D above to save it to whoburnedmore.com and join the leaderboard." : "Run <code>npx whoburnedmore</code> (no flag) to get a shareable dashboard at whoburnedmore.com \u2014 no sign-in."}
5339
5346
  </div>
5340
5347
  </div>
5341
5348
  </body>
@@ -5386,27 +5393,6 @@ function openBrowser(url) {
5386
5393
  const [cmd, args] = os === "darwin" ? ["open", [url]] : os === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
5387
5394
  spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
5388
5395
  }
5389
- async function login() {
5390
- const device = await deviceStart();
5391
- console.log();
5392
- console.log(` Opening ${pc2.cyan(device.verifyUrl)}`);
5393
- console.log(` Your code: ${pc2.bold(pc2.yellow(device.userCode))}`);
5394
- console.log(pc2.dim(" Sign in with Google or GitHub and approve this device."));
5395
- openBrowser(device.verifyUrl);
5396
- const deadline = Date.now() + device.expiresInSeconds * 1e3;
5397
- while (Date.now() < deadline) {
5398
- await new Promise((r) => setTimeout(r, device.pollIntervalSeconds * 1e3));
5399
- const poll = await devicePoll(device.deviceCode);
5400
- if (poll.status === "ok") {
5401
- const config = { token: poll.token, handle: poll.handle };
5402
- saveConfig(void 0, config);
5403
- console.log(` Signed in as ${pc2.bold(poll.handle)} \u2713`);
5404
- return config;
5405
- }
5406
- if (poll.status === "expired") break;
5407
- }
5408
- throw new Error("login timed out \u2014 run `npx whoburnedmore` to try again");
5409
- }
5410
5396
  async function confirm(question) {
5411
5397
  if (!process.stdin.isTTY) return false;
5412
5398
  const rl = createInterface({ input: process.stdin, output: process.stdout });
@@ -5414,11 +5400,17 @@ async function confirm(question) {
5414
5400
  rl.close();
5415
5401
  return answer === "" || /^y(es)?$/i.test(answer);
5416
5402
  }
5417
- function showLocalDashboard(entries) {
5403
+ function showLocalDashboard(payload) {
5418
5404
  const dir = defaultConfigDir();
5419
5405
  mkdirSync3(dir, { recursive: true });
5420
5406
  const file = join7(dir, "dashboard.html");
5421
- writeFileSync3(file, renderDashboardHtml(entries));
5407
+ writeFileSync3(
5408
+ file,
5409
+ renderDashboardHtml(payload.entries, /* @__PURE__ */ new Date(), {
5410
+ payload,
5411
+ webBaseUrl: webBase()
5412
+ })
5413
+ );
5422
5414
  console.log();
5423
5415
  console.log(` Local dashboard: ${pc2.cyan(`file://${file}`)}`);
5424
5416
  console.log(pc2.dim(" Re-run `npx whoburnedmore --local` to refresh it. Nothing left your machine."));
@@ -5458,7 +5450,7 @@ async function run(flags) {
5458
5450
  }
5459
5451
  if (!flags.quiet) printSummary(entries);
5460
5452
  if (flags.local) {
5461
- showLocalDashboard(entries);
5453
+ showLocalDashboard(payload);
5462
5454
  if (!flags.quiet && process.stdin.isTTY) {
5463
5455
  await publishLocal(payload, {
5464
5456
  confirm,
@@ -5474,61 +5466,46 @@ async function run(flags) {
5474
5466
  console.log(pc2.dim(" --no-submit: skipped the dashboard."));
5475
5467
  return;
5476
5468
  }
5477
- const config = loadConfig();
5478
- if (config?.token) {
5479
- const result = await submitUsage(config.token, payload);
5480
- if (!flags.quiet) {
5481
- console.log(pc2.dim(" Opening your dashboard in your browser\u2026"));
5482
- openBrowser(result.boardUrl ?? result.profileUrl);
5483
- }
5469
+ const anonKey = ensureAnonKey();
5470
+ const result = await anonSubmit(anonKey, payload);
5471
+ const target = result.boardUrl ?? claimUrl(result.dashboardUrl, anonKey);
5472
+ if (!flags.quiet) {
5473
+ console.log(pc2.dim(" Opening your dashboard in your browser\u2026"));
5474
+ openBrowser(target);
5475
+ }
5476
+ console.log(
5477
+ ` Submitted ${pc2.bold(String(result.upserted))} day-entries from ${toolsFound.join(", ")}.`
5478
+ );
5479
+ if (result.boardUrl) {
5484
5480
  console.log(
5485
- ` Submitted ${pc2.bold(String(result.upserted))} day-entries from ${toolsFound.join(", ")}.`
5481
+ ` You burned ${pc2.bold(formatTokens(result.totalTokens))} tokens \u2014 \u{1F91D} you're on the friends board:`
5486
5482
  );
5487
- if (result.rank) {
5488
- console.log(
5489
- ` You are ${pc2.bold(pc2.yellow(`#${result.rank}`))} with ${pc2.bold(formatTokens(result.totalTokens))} tokens burned.`
5490
- );
5491
- }
5492
- console.log(` ${pc2.cyan(result.profileUrl)}`);
5493
- if (result.boardUrl) {
5494
- console.log(` \u{1F91D} You're on the friends board: ${pc2.cyan(result.boardUrl)}`);
5495
- }
5483
+ console.log(` ${pc2.cyan(result.boardUrl)}`);
5484
+ console.log(pc2.dim(` Your dashboard: ${result.dashboardUrl}`));
5496
5485
  } else {
5497
- const anonKey = ensureAnonKey();
5498
- const result = await anonSubmit(anonKey, payload);
5499
- const target = result.boardUrl ?? claimUrl(result.dashboardUrl, anonKey);
5500
- if (!flags.quiet) {
5501
- console.log(pc2.dim(" Opening your dashboard in your browser\u2026"));
5502
- openBrowser(target);
5503
- }
5504
5486
  console.log(
5505
- ` Submitted ${pc2.bold(String(result.upserted))} day-entries from ${toolsFound.join(", ")}.`
5487
+ ` You burned ${pc2.bold(formatTokens(result.totalTokens))} tokens \u2014 you're on the public leaderboard:`
5506
5488
  );
5507
- if (result.boardUrl) {
5489
+ console.log(` ${pc2.cyan(result.dashboardUrl)}`);
5490
+ if (!flags.quiet) {
5508
5491
  console.log(
5509
- ` You burned ${pc2.bold(formatTokens(result.totalTokens))} tokens \u2014 \u{1F91D} you're on the friends board:`
5492
+ pc2.dim(" Claim it (name + X) on the web to own your rank, or make it private / remove it.")
5510
5493
  );
5511
- console.log(` ${pc2.cyan(result.boardUrl)}`);
5512
- console.log(pc2.dim(` Your dashboard: ${result.dashboardUrl}`));
5513
- } else {
5514
5494
  console.log(
5515
- ` You burned ${pc2.bold(formatTokens(result.totalTokens))} tokens \u2014 you're on the public leaderboard:`
5495
+ pc2.dim(" Manage anytime: `npx whoburnedmore private` \xB7 `npx whoburnedmore public` \xB7 `npx whoburnedmore remove`.")
5516
5496
  );
5517
- console.log(` ${pc2.cyan(result.dashboardUrl)}`);
5518
- if (!flags.quiet) {
5519
- console.log(
5520
- pc2.dim(" Claim it (name + X) to own your rank, or make it private / remove it.")
5521
- );
5522
- console.log(
5523
- pc2.dim(" Manage anytime: `npx whoburnedmore private` \xB7 `npx whoburnedmore public` \xB7 `npx whoburnedmore remove`.")
5524
- );
5525
- }
5526
5497
  }
5527
5498
  }
5528
- console.log();
5499
+ if (!flags.quiet && !autoSyncInstalled()) {
5500
+ try {
5501
+ installAutoSync();
5502
+ } catch {
5503
+ }
5504
+ }
5529
5505
  if (!flags.quiet) {
5506
+ console.log();
5530
5507
  console.log(
5531
- 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.")
5508
+ autoSyncInstalled() ? pc2.dim(" Background sync is on \u2014 your page updates automatically every 3h (`npx whoburnedmore uninstall-sync` to stop).") : pc2.dim(" Re-run anytime to update your page.")
5532
5509
  );
5533
5510
  }
5534
5511
  }
@@ -5557,14 +5534,6 @@ async function main() {
5557
5534
  await run({ ...flags, noSubmit: false, dryRun: false, local: false });
5558
5535
  break;
5559
5536
  }
5560
- case "login":
5561
- await login();
5562
- break;
5563
- case "logout":
5564
- clearConfig();
5565
- console.log(" Logged out. Your leaderboard data is untouched.");
5566
- console.log(pc2.dim(" Delete your data anytime from your profile page."));
5567
- break;
5568
5537
  case "private":
5569
5538
  case "public": {
5570
5539
  const cfg = loadConfig();
@@ -5619,19 +5588,19 @@ function printHelp() {
5619
5588
  npx whoburnedmore --local build the dashboard on your machine and open it (offline)
5620
5589
  npx whoburnedmore --dry-run print exactly what would be sent, send nothing
5621
5590
  npx whoburnedmore --no-submit print local stats only, send nothing
5622
- npx whoburnedmore private hide your anonymous dashboard from the leaderboard
5591
+ npx whoburnedmore private hide your dashboard from the leaderboard
5623
5592
  npx whoburnedmore public put it back on the leaderboard
5624
- npx whoburnedmore remove delete your anonymous dashboard and its data
5625
- npx whoburnedmore login sign in to claim a public handle + join the leaderboard
5626
- npx whoburnedmore logout forget the local token
5627
- npx whoburnedmore install-sync keep your dashboard live (background sync, 3h)
5628
- npx whoburnedmore uninstall-sync remove background sync
5593
+ npx whoburnedmore remove delete your dashboard and its data
5594
+ npx whoburnedmore uninstall-sync turn off the background sync
5595
+ npx whoburnedmore install-sync turn it back on after uninstalling
5629
5596
 
5630
- By default your dashboard is public on the leaderboard as an anonymous burner \u2014
5631
- claim it (sign in for a handle + X) to own your rank, or run \`private\`/\`remove\`
5632
- to pull it. Only daily aggregate numbers (date, tool, model, token counts, est.
5633
- cost) ever leave your machine \u2014 never prompts, code, or file names. With --local,
5634
- nothing leaves your machine at all.
5597
+ Background sync is on by default: after your first run, your page refreshes
5598
+ automatically every 3h (\`uninstall-sync\` to stop). Your dashboard is public on
5599
+ the leaderboard as an anonymous burner \u2014 sign in on whoburnedmore.com to claim
5600
+ it (handle + X) and own your rank, or run \`private\`/\`remove\` to pull it. Only
5601
+ daily aggregate numbers (date, tool, model, token counts, est. cost) ever leave
5602
+ your machine \u2014 never prompts, code, or file names. With --local, nothing leaves
5603
+ your machine at all.
5635
5604
  `);
5636
5605
  }
5637
5606
  main().catch((err) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whoburnedmore",
3
- "version": "0.5.0",
3
+ "version": "0.6.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": {