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