whoburnedmore 0.8.3 → 0.8.7

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 +258 -104
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ import { createRequire as createRequire4 } from "node:module";
12
12
  import { platform as platform3 } from "node:os";
13
13
  import { join as join7 } from "node:path";
14
14
  import { createInterface } from "node:readline/promises";
15
- import pc3 from "picocolors";
15
+ import pc2 from "picocolors";
16
16
 
17
17
  // src/args.ts
18
18
  function parseBoard(args) {
@@ -80,6 +80,9 @@ async function anonSubmit(anonKey, payload) {
80
80
  function claimUrl(dashboardUrl, anonKey) {
81
81
  return `${dashboardUrl}#k=${encodeURIComponent(anonKey)}`;
82
82
  }
83
+ function boardClaimUrl(boardUrl, slug, anonKey) {
84
+ return `${boardUrl}#k=${encodeURIComponent(anonKey)}&u=${encodeURIComponent(slug)}`;
85
+ }
83
86
  async function anonVisibility(anonKey, listed) {
84
87
  const { status, body } = await post(
85
88
  "/v1/anon/visibility",
@@ -103,7 +106,15 @@ async function anonRemove(anonKey) {
103
106
 
104
107
  // src/autosync.ts
105
108
  import { spawnSync } from "node:child_process";
106
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
109
+ import {
110
+ existsSync as existsSync2,
111
+ mkdirSync as mkdirSync2,
112
+ readFileSync as readFileSync2,
113
+ renameSync,
114
+ rmSync,
115
+ statSync,
116
+ writeFileSync as writeFileSync2
117
+ } from "node:fs";
107
118
  import { homedir as homedir2, platform } from "node:os";
108
119
  import { join as join2 } from "node:path";
109
120
  import { fileURLToPath } from "node:url";
@@ -129,6 +140,8 @@ function loadConfig(dir = defaultConfigDir()) {
129
140
  const parsed = JSON.parse(readFileSync(file, "utf8"));
130
141
  const config = {};
131
142
  if (typeof parsed.anonKey === "string") config.anonKey = parsed.anonKey;
143
+ if (typeof parsed.lastSyncAt === "number" && Number.isFinite(parsed.lastSyncAt))
144
+ config.lastSyncAt = parsed.lastSyncAt;
132
145
  return Object.keys(config).length > 0 ? config : null;
133
146
  } catch {
134
147
  return null;
@@ -150,10 +163,19 @@ function ensureAnonKey(dir = defaultConfigDir()) {
150
163
  saveConfig(dir, { ...config, anonKey });
151
164
  return anonKey;
152
165
  }
166
+ function recordSync(dir = defaultConfigDir(), when = Date.now()) {
167
+ const config = loadConfig(dir) ?? {};
168
+ saveConfig(dir, { ...config, lastSyncAt: when });
169
+ }
153
170
 
154
171
  // src/autosync.ts
155
172
  var SYNC_INTERVAL_HOURS = 1;
156
173
  var LABEL = "com.whoburnedmore.sync";
174
+ var STABLE_NODE_CANDIDATES = [
175
+ "/opt/homebrew/bin/node",
176
+ "/usr/local/bin/node",
177
+ "/usr/bin/node"
178
+ ];
157
179
  function syncLogPath() {
158
180
  return join2(defaultConfigDir(), "sync.log");
159
181
  }
@@ -194,28 +216,53 @@ function launchAgentPath() {
194
216
  function cliScriptPath() {
195
217
  return fileURLToPath(new URL("./index.js", import.meta.url));
196
218
  }
219
+ function isUsableNode(p) {
220
+ if (!existsSync2(p)) return false;
221
+ const res = spawnSync(p, ["-v"], { encoding: "utf8" });
222
+ if (res.status !== 0 || typeof res.stdout !== "string") return false;
223
+ const major = Number(res.stdout.trim().replace(/^v/, "").split(".")[0]);
224
+ return Number.isFinite(major) && major >= 20;
225
+ }
226
+ function resolveNodePath(opts) {
227
+ const candidates = opts?.candidates ?? STABLE_NODE_CANDIDATES;
228
+ const check = opts?.check ?? isUsableNode;
229
+ const execPath = opts?.execPath ?? process.execPath;
230
+ for (const c of candidates) {
231
+ if (check(c)) return c;
232
+ }
233
+ return execPath;
234
+ }
235
+ function expectedDarwinPlist() {
236
+ return buildLaunchdPlist(resolveNodePath(), cliScriptPath());
237
+ }
238
+ function plistDrift(installed, expected) {
239
+ if (installed === null) return "absent";
240
+ return installed.trim() === expected.trim() ? "ok" : "drift";
241
+ }
242
+ function reconcileAction(state) {
243
+ return state === "ok" ? "noop" : "install";
244
+ }
197
245
  function installAutoSync() {
198
246
  const os = platform();
199
247
  mkdirSync2(defaultConfigDir(), { recursive: true });
200
248
  if (os === "darwin") {
201
249
  const plistPath = launchAgentPath();
202
250
  mkdirSync2(join2(homedir2(), "Library", "LaunchAgents"), { recursive: true });
203
- writeFileSync2(plistPath, buildLaunchdPlist(process.execPath, cliScriptPath()));
251
+ writeFileSync2(plistPath, expectedDarwinPlist());
204
252
  spawnSync("launchctl", ["unload", plistPath], { stdio: "ignore" });
205
253
  spawnSync("launchctl", ["load", plistPath], { stdio: "ignore" });
206
254
  return `launchd agent installed (${plistPath}), syncing every ${SYNC_INTERVAL_HOURS}h`;
207
255
  }
208
256
  if (os === "linux") {
209
- const line = `0 */${SYNC_INTERVAL_HOURS} * * * "${process.execPath}" "${cliScriptPath()}" sync >"${syncLogPath()}" 2>&1`;
257
+ const line = expectedLinuxCronLine();
210
258
  const current = spawnSync("crontab", ["-l"], { encoding: "utf8" });
211
259
  const existing = current.status === 0 ? current.stdout : "";
212
- if (!existing.includes("whoburnedmore")) {
213
- const next = `${existing.trimEnd()}
260
+ const kept = existing.split("\n").filter((l) => !l.includes("whoburnedmore")).join("\n");
261
+ const next = `${kept.trimEnd()}
214
262
  ${line}
215
- `;
216
- const res = spawnSync("crontab", ["-"], { input: next });
217
- if (res.status !== 0) throw new Error("could not install crontab entry");
218
- }
263
+ `.replace(/^\n+/, "");
264
+ const res = spawnSync("crontab", ["-"], { input: next });
265
+ if (res.status !== 0) throw new Error("could not install crontab entry");
219
266
  return `cron entry installed, syncing every ${SYNC_INTERVAL_HOURS}h`;
220
267
  }
221
268
  if (os === "win32") {
@@ -229,13 +276,16 @@ ${line}
229
276
  "/TN",
230
277
  "whoburnedmore-sync",
231
278
  "/TR",
232
- `"${process.execPath}" "${cliScriptPath()}" sync`
279
+ `"${resolveNodePath()}" "${cliScriptPath()}" sync`
233
280
  ]);
234
281
  if (res.status !== 0) throw new Error("could not create scheduled task");
235
282
  return `scheduled task installed, syncing every ${SYNC_INTERVAL_HOURS}h`;
236
283
  }
237
284
  throw new Error(`auto-sync is not supported on ${os}`);
238
285
  }
286
+ function expectedLinuxCronLine() {
287
+ return `0 */${SYNC_INTERVAL_HOURS} * * * "${resolveNodePath()}" "${cliScriptPath()}" sync >"${syncLogPath()}" 2>&1`;
288
+ }
239
289
  function uninstallAutoSync() {
240
290
  const os = platform();
241
291
  if (os === "darwin") {
@@ -274,6 +324,52 @@ function autoSyncInstalled() {
274
324
  }
275
325
  return false;
276
326
  }
327
+ function readInstalledAgent() {
328
+ if (platform() === "darwin") {
329
+ const p = launchAgentPath();
330
+ return existsSync2(p) ? readFileSync2(p, "utf8") : null;
331
+ }
332
+ if (platform() === "linux") {
333
+ const current = spawnSync("crontab", ["-l"], { encoding: "utf8" });
334
+ if (current.status !== 0) return null;
335
+ const line = current.stdout.split("\n").find((l) => l.includes("whoburnedmore"));
336
+ return line ?? null;
337
+ }
338
+ return null;
339
+ }
340
+ function expectedAgent() {
341
+ if (platform() === "darwin") return expectedDarwinPlist();
342
+ if (platform() === "linux") return expectedLinuxCronLine();
343
+ return null;
344
+ }
345
+ function autoSyncDrift() {
346
+ const expected = expectedAgent();
347
+ if (expected === null) return autoSyncInstalled() ? "ok" : "absent";
348
+ return plistDrift(readInstalledAgent(), expected);
349
+ }
350
+ function reconcileAutoSync() {
351
+ const state = autoSyncDrift();
352
+ if (reconcileAction(state) === "noop") return "noop";
353
+ installAutoSync();
354
+ return state === "absent" ? "installed" : "reinstalled";
355
+ }
356
+ function rotateLogIfLarge(path = syncLogPath(), capBytes = 256 * 1024) {
357
+ try {
358
+ if (!existsSync2(path)) return false;
359
+ if (statSync(path).size <= capBytes) return false;
360
+ renameSync(path, `${path}.1`);
361
+ return true;
362
+ } catch {
363
+ return false;
364
+ }
365
+ }
366
+ function autoSyncLoaded() {
367
+ if (platform() === "darwin") {
368
+ const res = spawnSync("launchctl", ["list"], { encoding: "utf8" });
369
+ return res.status === 0 && res.stdout.includes(LABEL);
370
+ }
371
+ return autoSyncInstalled();
372
+ }
277
373
 
278
374
  // src/banner.ts
279
375
  import pc from "picocolors";
@@ -300,7 +396,7 @@ import { dirname as dirname2, join as join6 } from "node:path";
300
396
  import { promisify } from "node:util";
301
397
 
302
398
  // src/attribution.ts
303
- import { readFileSync as readFileSync2, readdirSync, statSync } from "node:fs";
399
+ import { readFileSync as readFileSync3, readdirSync, statSync as statSync2 } from "node:fs";
304
400
  import { homedir as homedir3 } from "node:os";
305
401
  import { basename, join as join3 } from "node:path";
306
402
 
@@ -340,7 +436,8 @@ function createAccumulator() {
340
436
  messageCount: 0,
341
437
  subagentMessages: 0,
342
438
  subagentTokens: 0,
343
- totalTokens: 0
439
+ totalTokens: 0,
440
+ userMessageCount: 0
344
441
  },
345
442
  titles: /* @__PURE__ */ new Map(),
346
443
  sessionMessages: /* @__PURE__ */ new Map()
@@ -358,10 +455,24 @@ function recordTokens(usage) {
358
455
  };
359
456
  return n(u.input_tokens) + n(u.output_tokens) + n(u.cache_creation_input_tokens) + n(u.cache_read_input_tokens);
360
457
  }
458
+ function hasHumanText(content) {
459
+ const isHuman = (t) => {
460
+ const s = t.trim();
461
+ return s.length > 0 && !s.startsWith("<system-reminder") && !s.startsWith("<command-") && !s.startsWith("Caveat:");
462
+ };
463
+ if (typeof content === "string") return isHuman(content);
464
+ if (!Array.isArray(content)) return false;
465
+ return content.some(
466
+ (b) => b !== null && typeof b === "object" && b.type === "text" && typeof b.text === "string" && isHuman(b.text)
467
+ );
468
+ }
361
469
  function processRecord(rec, acc, ctx) {
362
470
  if (!rec || typeof rec !== "object") return;
363
471
  const r = rec;
364
472
  const recTokens = recordTokens(r.message?.usage);
473
+ if ((r.type === "user" || r.message?.role === "user") && r.isSidechain !== true && r.isMeta !== true && hasHumanText(r.message?.content)) {
474
+ acc.agent.userMessageCount += 1;
475
+ }
365
476
  if (typeof r.attributionSkill === "string" && r.attributionSkill) {
366
477
  const s = r.attributionSkill.slice(0, 128);
367
478
  const sk = acc.skills.get(s) ?? { count: 0, tokens: 0 };
@@ -537,13 +648,13 @@ function processCodexRecord(rec, acc, ctx) {
537
648
  function readLines(file) {
538
649
  let size = 0;
539
650
  try {
540
- size = statSync(file).size;
651
+ size = statSync2(file).size;
541
652
  } catch {
542
653
  return [];
543
654
  }
544
655
  if (size > MAX_FILE_BYTES) return [];
545
656
  try {
546
- return readFileSync2(file, "utf8").split("\n");
657
+ return readFileSync3(file, "utf8").split("\n");
547
658
  } catch {
548
659
  return [];
549
660
  }
@@ -571,7 +682,7 @@ function listTranscripts(dir) {
571
682
  if (e.isDirectory()) walk(p);
572
683
  else if (e.isFile() && e.name.endsWith(".jsonl")) {
573
684
  try {
574
- out.push({ path: p, mtime: statSync(p).mtimeMs });
685
+ out.push({ path: p, mtime: statSync2(p).mtimeMs });
575
686
  } catch {
576
687
  }
577
688
  }
@@ -1164,6 +1275,68 @@ async function collectAll(onProgress) {
1164
1275
  };
1165
1276
  }
1166
1277
 
1278
+ // src/status.ts
1279
+ function ago(ms) {
1280
+ const mins = Math.round(ms / 6e4);
1281
+ if (mins < 1) return "just now";
1282
+ if (mins < 60) return `${mins}m ago`;
1283
+ const hrs = Math.round(mins / 60);
1284
+ if (hrs < 48) return `${hrs}h ago`;
1285
+ return `${Math.round(hrs / 24)}d ago`;
1286
+ }
1287
+ function buildStatusReport(s) {
1288
+ const lines = [];
1289
+ lines.push(" whoburnedmore \u2014 background sync status");
1290
+ lines.push("");
1291
+ lines.push(
1292
+ s.installed ? ` \u2022 Background agent: installed${s.loaded ? " and loaded" : " but NOT loaded with the scheduler"}` : " \u2022 Background agent: NOT installed \u2014 run `npx whoburnedmore` to set it up"
1293
+ );
1294
+ if (s.installed && s.drift !== "ok") {
1295
+ lines.push(
1296
+ " \u21B3 config is out of date \u2014 it will self-repair on your next run"
1297
+ );
1298
+ }
1299
+ lines.push(` \u2022 Interval: every ${s.intervalHours}h`);
1300
+ const staleAfterMs = s.intervalHours * 2 * 3600 * 1e3;
1301
+ if (s.lastSyncAt === null) {
1302
+ lines.push(" \u2022 Last sync: never recorded");
1303
+ lines.push(" \u26A0 STALE: no successful sync recorded yet \u2014 run `npx whoburnedmore`");
1304
+ } else {
1305
+ const age = s.now - s.lastSyncAt;
1306
+ lines.push(` \u2022 Last sync: ${ago(age)}`);
1307
+ if (age > staleAfterMs) {
1308
+ lines.push(
1309
+ ` \u26A0 STALE: last sync was over ${s.intervalHours * 2}h ago \u2014 your dashboard may be behind. Run \`npx whoburnedmore\`.`
1310
+ );
1311
+ } else {
1312
+ lines.push(" \u2713 Fresh \u2014 your dashboard is up to date.");
1313
+ }
1314
+ }
1315
+ lines.push(` \u2022 Node: ${s.nodePath}`);
1316
+ if (!s.nodePathStable) {
1317
+ lines.push(
1318
+ " \u26A0 that node path is version-pinned and may break on a node upgrade \u2014 a run will re-point it to a stable path"
1319
+ );
1320
+ }
1321
+ lines.push(` \u2022 Log: ${s.logPath}`);
1322
+ return lines;
1323
+ }
1324
+ function agentStatusReport(now = Date.now()) {
1325
+ const cfg = loadConfig();
1326
+ const nodePath = resolveNodePath();
1327
+ return buildStatusReport({
1328
+ installed: autoSyncInstalled(),
1329
+ loaded: autoSyncLoaded(),
1330
+ drift: autoSyncDrift(),
1331
+ intervalHours: SYNC_INTERVAL_HOURS,
1332
+ lastSyncAt: typeof cfg?.lastSyncAt === "number" ? cfg.lastSyncAt : null,
1333
+ now,
1334
+ nodePath,
1335
+ nodePathStable: !nodePath.includes("/Cellar/"),
1336
+ logPath: syncLogPath()
1337
+ });
1338
+ }
1339
+
1167
1340
  // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
1168
1341
  var external_exports = {};
1169
1342
  __export(external_exports, {
@@ -5283,7 +5456,13 @@ var AgentStat = external_exports.object({
5283
5456
  /** Tokens spent inside subagent sidechains. */
5284
5457
  subagentTokens: external_exports.number().int().nonnegative(),
5285
5458
  /** Total tokens observed across transcripts (denominator for the share). */
5286
- totalTokens: external_exports.number().int().nonnegative()
5459
+ totalTokens: external_exports.number().int().nonnegative(),
5460
+ /**
5461
+ * Messages the human actually sent (their prompts) — non-sidechain user turns
5462
+ * carrying real text, NOT tool results or injected/meta turns. Denominator for
5463
+ * "avg cost per message". Optional (back-compat with older CLIs).
5464
+ */
5465
+ userMessageCount: external_exports.number().int().nonnegative().optional()
5287
5466
  });
5288
5467
  var SkillStat = external_exports.object({
5289
5468
  name: external_exports.string().min(1).max(128),
@@ -5327,7 +5506,6 @@ var LeaderboardPeriod = external_exports.enum(["today", "7d", "30d", "all"]);
5327
5506
  var LeaderboardMetric = external_exports.enum(["tokens", "cost"]);
5328
5507
 
5329
5508
  // src/output.ts
5330
- import pc2 from "picocolors";
5331
5509
  function formatTokens(n) {
5332
5510
  if (n >= 1e9) return `${(n / 1e9).toFixed(2)}B`;
5333
5511
  if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
@@ -5337,40 +5515,21 @@ function formatTokens(n) {
5337
5515
  function formatUSD(n) {
5338
5516
  return `$${n.toLocaleString("en-US", { maximumFractionDigits: 2 })}`;
5339
5517
  }
5340
- function printSummary(entries) {
5341
- const byTool = /* @__PURE__ */ new Map();
5342
- let totalTokens = 0;
5343
- let totalCost = 0;
5344
- let todayTokens = 0;
5345
- const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
5346
- for (const e of entries) {
5347
- const tokens = entryTotalTokens(e);
5348
- totalTokens += tokens;
5349
- totalCost += e.costUSD;
5350
- if (e.date === today) todayTokens += tokens;
5351
- const agg = byTool.get(e.tool) ?? { tokens: 0, cost: 0, days: /* @__PURE__ */ new Set() };
5352
- agg.tokens += tokens;
5353
- agg.cost += e.costUSD;
5354
- agg.days.add(e.date);
5355
- byTool.set(e.tool, agg);
5356
- }
5357
- console.log();
5358
- console.log(pc2.bold(pc2.yellow(" \u{1F525} your burn report")));
5359
- console.log();
5360
- const rows = [...byTool.entries()].sort((a, b) => b[1].tokens - a[1].tokens);
5361
- for (const [tool, agg] of rows) {
5362
- console.log(
5363
- ` ${pc2.cyan(tool.padEnd(10))} ${formatTokens(agg.tokens).padStart(9)} tokens ${formatUSD(agg.cost).padStart(10)} ${String(agg.days.size).padStart(4)} days`
5364
- );
5365
- }
5366
- console.log(pc2.dim(" " + "\u2500".repeat(46)));
5367
- console.log(
5368
- ` ${pc2.bold("total".padEnd(10))} ${pc2.bold(formatTokens(totalTokens).padStart(9))} tokens ${pc2.bold(formatUSD(totalCost).padStart(10))}`
5369
- );
5370
- if (todayTokens > 0) {
5371
- console.log(` ${pc2.dim("today".padEnd(10))} ${formatTokens(todayTokens).padStart(9)} tokens`);
5372
- }
5373
- console.log();
5518
+ function submitNextStepLines(result) {
5519
+ if (result.boardUrl) {
5520
+ const code = result.boardCode ?? result.boardUrl.split("/").filter(Boolean).pop() ?? "";
5521
+ return [
5522
+ ` \u{1F91D} You're on the board: ${result.boardUrl}`,
5523
+ " \u2192 Open it to see who burned more.",
5524
+ ` \u2192 Get a friend on it \u2014 have them run: npx whoburnedmore --board=${code}`,
5525
+ " \u2192 Sign in on the page and add your X to claim your spot and own your rank."
5526
+ ];
5527
+ }
5528
+ return [
5529
+ ` Your dashboard: ${result.dashboardUrl}`,
5530
+ " \u2192 Sign in and add your X on the page to get on the leaderboard and claim your rank.",
5531
+ " Private until you do. Manage anytime: `npx whoburnedmore private` \xB7 `public` \xB7 `remove`."
5532
+ ];
5374
5533
  }
5375
5534
 
5376
5535
  // src/local-dashboard.ts
@@ -5611,14 +5770,12 @@ var LOADING_VIBES = [
5611
5770
  ];
5612
5771
  function startProgress() {
5613
5772
  if (!process.stdout.isTTY) {
5614
- let lastLogged = -1;
5773
+ let announced = false;
5615
5774
  return {
5616
- onProgress: (done, total) => {
5617
- const pct = Math.round(done / total * 100);
5618
- if (pct >= lastLogged + 25 || pct === 100 && lastLogged < 100) {
5619
- lastLogged = pct;
5620
- console.log(pc3.dim(` counting local token usage\u2026 ${pct}%`));
5621
- }
5775
+ onProgress: () => {
5776
+ if (announced) return;
5777
+ announced = true;
5778
+ console.log(pc2.dim(" Counting your token usage\u2026"));
5622
5779
  },
5623
5780
  stop: () => {
5624
5781
  }
@@ -5639,9 +5796,9 @@ function startProgress() {
5639
5796
  shown += (target - shown) * 0.3;
5640
5797
  if (target - shown < 4e-3) shown = target;
5641
5798
  const filled = Math.round(shown * width);
5642
- const bar = pc3.yellow("\u2588".repeat(filled)) + pc3.dim("\u2591".repeat(width - filled));
5799
+ const bar = pc2.yellow("\u2588".repeat(filled)) + pc2.dim("\u2591".repeat(width - filled));
5643
5800
  const pct = String(Math.round(shown * 100)).padStart(3);
5644
- process.stdout.write(`\r ${bar} ${pct}% ${pc3.dim(vibe())}\x1B[K`);
5801
+ process.stdout.write(`\r ${bar} ${pct}% ${pc2.dim(vibe())}\x1B[K`);
5645
5802
  };
5646
5803
  render();
5647
5804
  const timer = setInterval(render, 60);
@@ -5665,7 +5822,7 @@ function openBrowser(url) {
5665
5822
  async function confirm(question) {
5666
5823
  if (!process.stdin.isTTY) return false;
5667
5824
  const rl = createInterface({ input: process.stdin, output: process.stdout });
5668
- const answer = (await rl.question(`${question} ${pc3.dim("[Y/n]")} `)).trim();
5825
+ const answer = (await rl.question(`${question} ${pc2.dim("[Y/n]")} `)).trim();
5669
5826
  rl.close();
5670
5827
  return answer === "" || /^y(es)?$/i.test(answer);
5671
5828
  }
@@ -5681,20 +5838,20 @@ function showLocalDashboard(payload) {
5681
5838
  })
5682
5839
  );
5683
5840
  console.log();
5684
- console.log(` Local dashboard: ${pc3.cyan(`file://${file}`)}`);
5685
- console.log(pc3.dim(" Re-run `npx whoburnedmore --local` to refresh it. Nothing left your machine."));
5841
+ console.log(` Local dashboard: ${pc2.cyan(`file://${file}`)}`);
5842
+ console.log(pc2.dim(" Re-run `npx whoburnedmore --local` to refresh it. Nothing left your machine."));
5686
5843
  openBrowser(`file://${file}`);
5687
5844
  }
5688
5845
  async function run(flags) {
5689
5846
  if (!flags.quiet) {
5690
5847
  printBanner();
5691
- console.log(pc3.dim(` whoburnedmore v${VERSION} \xB7 ${flags.local ? "local mode" : apiBase()}`));
5848
+ console.log(pc2.dim(` whoburnedmore v${VERSION} \xB7 ${flags.local ? "local mode" : apiBase()}`));
5692
5849
  if (!flags.dryRun && !flags.noSubmit && !flags.local) {
5693
5850
  console.log(
5694
- pc3.dim(" Counting your token usage and posting your rank \u2014 only daily totals leave your machine, never your prompts or code.")
5851
+ pc2.dim(" Counting your token usage and posting your rank \u2014 only daily totals leave your machine, never your prompts or code.")
5695
5852
  );
5696
5853
  console.log(
5697
- pc3.dim(" (`--local` keeps it fully offline \xB7 `private`/`remove` pull it anytime \xB7 details: whoburnedmore.com/trust)")
5854
+ pc2.dim(" (`--local` keeps it fully offline \xB7 `private`/`remove` pull it anytime \xB7 details: whoburnedmore.com/trust)")
5698
5855
  );
5699
5856
  }
5700
5857
  console.log();
@@ -5707,11 +5864,11 @@ async function run(flags) {
5707
5864
  } finally {
5708
5865
  progress.stop();
5709
5866
  }
5710
- const { entries, sessions, blocks, toolsFound, tools, skills, projects, agent, attributionComplete } = collected;
5867
+ const { entries, sessions, blocks, tools, skills, projects, agent, attributionComplete } = collected;
5711
5868
  if (entries.length === 0) {
5712
5869
  console.log();
5713
5870
  console.log(" Nothing to burn yet \u2014 no local usage found from any coding agent.");
5714
- console.log(pc3.dim(" Use Claude Code, Codex, Gemini CLI (or friends) and come back."));
5871
+ console.log(pc2.dim(" Use Claude Code, Codex, Gemini CLI (or friends) and come back."));
5715
5872
  return;
5716
5873
  }
5717
5874
  const payload = { cliVersion: VERSION, entries };
@@ -5725,11 +5882,10 @@ async function run(flags) {
5725
5882
  payload.attributionComplete = true;
5726
5883
  if (flags.board) payload.board = flags.board;
5727
5884
  if (flags.dryRun) {
5728
- console.log(pc3.dim("\n --dry-run: this exact payload would be sent, nothing else:\n"));
5885
+ console.log(pc2.dim("\n --dry-run: this exact payload would be sent, nothing else:\n"));
5729
5886
  console.log(JSON.stringify(payload, null, 2));
5730
5887
  return;
5731
5888
  }
5732
- if (!flags.quiet) printSummary(entries);
5733
5889
  if (flags.local) {
5734
5890
  showLocalDashboard(payload);
5735
5891
  if (!flags.quiet && process.stdin.isTTY) {
@@ -5738,55 +5894,46 @@ async function run(flags) {
5738
5894
  ensureAnonKey,
5739
5895
  anonSubmit,
5740
5896
  openBrowser,
5741
- log: (line) => console.log(pc3.dim(line))
5897
+ log: (line) => console.log(pc2.dim(line))
5742
5898
  });
5743
5899
  }
5744
5900
  return;
5745
5901
  }
5746
5902
  if (flags.noSubmit) {
5747
- console.log(pc3.dim(" --no-submit: skipped the dashboard."));
5903
+ console.log(pc2.dim(" --no-submit: skipped the dashboard."));
5748
5904
  return;
5749
5905
  }
5750
5906
  const anonKey = ensureAnonKey();
5751
5907
  const result = await anonSubmit(anonKey, payload);
5752
- const target = result.boardUrl ?? claimUrl(result.dashboardUrl, anonKey);
5753
- if (!flags.quiet) {
5754
- console.log(pc3.dim(" Opening your dashboard in your browser\u2026"));
5755
- openBrowser(target);
5908
+ try {
5909
+ recordSync();
5910
+ } catch {
5756
5911
  }
5757
- console.log(
5758
- ` Submitted ${pc3.bold(String(result.upserted))} day-entries from ${toolsFound.join(", ")}.`
5759
- );
5760
- if (result.boardUrl) {
5761
- console.log(
5762
- ` You burned ${pc3.bold(formatTokens(result.totalTokens))} tokens \u2014 \u{1F91D} you're on the friends board:`
5763
- );
5764
- console.log(` ${pc3.cyan(result.boardUrl)}`);
5765
- console.log(pc3.dim(` Your dashboard: ${result.dashboardUrl}`));
5766
- } else {
5912
+ const target = result.boardUrl ? boardClaimUrl(result.boardUrl, result.slug, anonKey) : claimUrl(result.dashboardUrl, anonKey);
5913
+ if (!flags.quiet) {
5767
5914
  console.log(
5768
- ` You burned ${pc3.bold(formatTokens(result.totalTokens))} tokens \u2014 you're on the public leaderboard:`
5915
+ pc2.green(" \u2713 Synced securely.") + pc2.dim(" Only your daily totals left this machine \u2014 never your prompts, code, or file names.")
5769
5916
  );
5770
- console.log(` ${pc3.cyan(result.dashboardUrl)}`);
5771
- if (!flags.quiet) {
5772
- console.log(
5773
- pc3.dim(" Claim it (name + X) on the web to own your rank, or make it private / remove it.")
5774
- );
5775
- console.log(
5776
- pc3.dim(" Manage anytime: `npx whoburnedmore private` \xB7 `npx whoburnedmore public` \xB7 `npx whoburnedmore remove`.")
5777
- );
5778
- }
5917
+ console.log(pc2.dim(" Opening your dashboard in your browser\u2026"));
5918
+ openBrowser(target);
5919
+ }
5920
+ const lines = submitNextStepLines(result);
5921
+ for (const line of lines) {
5922
+ if (line.includes("\u2192")) console.log(pc2.bold(line));
5923
+ else if (line.startsWith(" Private until you do")) {
5924
+ if (!flags.quiet) console.log(pc2.dim(line));
5925
+ } else console.log(line);
5779
5926
  }
5780
- if (!flags.quiet && !autoSyncInstalled()) {
5927
+ if (!flags.quiet) {
5781
5928
  try {
5782
- installAutoSync();
5929
+ reconcileAutoSync();
5783
5930
  } catch {
5784
5931
  }
5785
5932
  }
5786
5933
  if (!flags.quiet) {
5787
5934
  console.log();
5788
5935
  console.log(
5789
- autoSyncInstalled() ? pc3.dim(" Background sync is on \u2014 your page updates automatically every hour (`npx whoburnedmore uninstall-sync` to stop).") : pc3.dim(" Re-run anytime to update your page.")
5936
+ autoSyncInstalled() ? pc2.dim(" Background sync is on \u2014 your page updates automatically every hour (`npx whoburnedmore uninstall-sync` to stop).") : pc2.dim(" Re-run anytime to update your page.")
5790
5937
  );
5791
5938
  }
5792
5939
  }
@@ -5812,9 +5959,15 @@ async function main() {
5812
5959
  break;
5813
5960
  case "sync": {
5814
5961
  if (!loadConfig()) return;
5962
+ rotateLogIfLarge();
5815
5963
  await run({ ...flags, noSubmit: false, dryRun: false, local: false });
5816
5964
  break;
5817
5965
  }
5966
+ case "status":
5967
+ case "doctor": {
5968
+ for (const line of agentStatusReport()) console.log(line);
5969
+ break;
5970
+ }
5818
5971
  case "private":
5819
5972
  case "public": {
5820
5973
  const cfg = loadConfig();
@@ -5858,17 +6011,18 @@ async function main() {
5858
6011
  }
5859
6012
  function printHelp() {
5860
6013
  console.log(`
5861
- ${pc3.bold("whoburnedmore")} \u2014 who burned more tokens, you or them?
6014
+ ${pc2.bold("whoburnedmore")} \u2014 who burned more tokens, you or them?
5862
6015
 
5863
- ${pc3.bold("usage")}
6016
+ ${pc2.bold("usage")}
5864
6017
  npx whoburnedmore burn + land on the public leaderboard, open your dashboard
5865
6018
  npx whoburnedmore --board=CODE compare with friends \u2014 join their board (no sign-in)
5866
6019
  npx whoburnedmore --local build the dashboard on your machine and open it (offline)
5867
6020
  npx whoburnedmore --dry-run print exactly what would be sent, send nothing
5868
- npx whoburnedmore --no-submit print local stats only, send nothing
6021
+ npx whoburnedmore --no-submit collect locally, send nothing (no dashboard)
5869
6022
  npx whoburnedmore private hide your dashboard from the leaderboard
5870
6023
  npx whoburnedmore public put it back on the leaderboard
5871
6024
  npx whoburnedmore remove delete your dashboard and its data
6025
+ npx whoburnedmore status check background-sync health (last sync, staleness)
5872
6026
  npx whoburnedmore uninstall-sync turn off the background sync
5873
6027
  npx whoburnedmore install-sync turn it back on after uninstalling
5874
6028
 
@@ -5882,7 +6036,7 @@ function printHelp() {
5882
6036
  `);
5883
6037
  }
5884
6038
  main().catch((err) => {
5885
- console.error(pc3.red(`
6039
+ console.error(pc2.red(`
5886
6040
  ${err.message}
5887
6041
  `));
5888
6042
  process.exitCode = 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whoburnedmore",
3
- "version": "0.8.3",
3
+ "version": "0.8.7",
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": {