whoburnedmore 0.8.1 → 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 +275 -108
  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) {
@@ -24,6 +24,15 @@ function parseBoard(args) {
24
24
  }
25
25
  return void 0;
26
26
  }
27
+ function resolveCommand(args) {
28
+ if (args.includes("--help") || args.includes("-h") || args.includes("help")) {
29
+ return "help";
30
+ }
31
+ if (args.includes("--version") || args.includes("-v") || args.includes("version")) {
32
+ return "version";
33
+ }
34
+ return args.find((a) => !a.startsWith("-")) ?? "run";
35
+ }
27
36
 
28
37
  // src/api.ts
29
38
  function apiBase() {
@@ -94,14 +103,28 @@ async function anonRemove(anonKey) {
94
103
 
95
104
  // src/autosync.ts
96
105
  import { spawnSync } from "node:child_process";
97
- 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";
98
115
  import { homedir as homedir2, platform } from "node:os";
99
116
  import { join as join2 } from "node:path";
100
117
  import { fileURLToPath } from "node:url";
101
118
 
102
119
  // src/config.ts
103
120
  import { randomBytes } from "node:crypto";
104
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
121
+ import {
122
+ chmodSync,
123
+ existsSync,
124
+ mkdirSync,
125
+ readFileSync,
126
+ writeFileSync
127
+ } from "node:fs";
105
128
  import { homedir } from "node:os";
106
129
  import { join } from "node:path";
107
130
  function defaultConfigDir() {
@@ -114,6 +137,8 @@ function loadConfig(dir = defaultConfigDir()) {
114
137
  const parsed = JSON.parse(readFileSync(file, "utf8"));
115
138
  const config = {};
116
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;
117
142
  return Object.keys(config).length > 0 ? config : null;
118
143
  } catch {
119
144
  return null;
@@ -123,6 +148,10 @@ function saveConfig(dir = defaultConfigDir(), config = {}) {
123
148
  mkdirSync(dir, { recursive: true });
124
149
  const file = join(dir, "config.json");
125
150
  writeFileSync(file, JSON.stringify(config, null, 2), { mode: 384 });
151
+ try {
152
+ chmodSync(file, 384);
153
+ } catch {
154
+ }
126
155
  }
127
156
  function ensureAnonKey(dir = defaultConfigDir()) {
128
157
  const config = loadConfig(dir) ?? {};
@@ -131,10 +160,19 @@ function ensureAnonKey(dir = defaultConfigDir()) {
131
160
  saveConfig(dir, { ...config, anonKey });
132
161
  return anonKey;
133
162
  }
163
+ function recordSync(dir = defaultConfigDir(), when = Date.now()) {
164
+ const config = loadConfig(dir) ?? {};
165
+ saveConfig(dir, { ...config, lastSyncAt: when });
166
+ }
134
167
 
135
168
  // src/autosync.ts
136
169
  var SYNC_INTERVAL_HOURS = 1;
137
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
+ ];
138
176
  function syncLogPath() {
139
177
  return join2(defaultConfigDir(), "sync.log");
140
178
  }
@@ -175,28 +213,53 @@ function launchAgentPath() {
175
213
  function cliScriptPath() {
176
214
  return fileURLToPath(new URL("./index.js", import.meta.url));
177
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
+ }
178
242
  function installAutoSync() {
179
243
  const os = platform();
180
244
  mkdirSync2(defaultConfigDir(), { recursive: true });
181
245
  if (os === "darwin") {
182
246
  const plistPath = launchAgentPath();
183
247
  mkdirSync2(join2(homedir2(), "Library", "LaunchAgents"), { recursive: true });
184
- writeFileSync2(plistPath, buildLaunchdPlist(process.execPath, cliScriptPath()));
248
+ writeFileSync2(plistPath, expectedDarwinPlist());
185
249
  spawnSync("launchctl", ["unload", plistPath], { stdio: "ignore" });
186
250
  spawnSync("launchctl", ["load", plistPath], { stdio: "ignore" });
187
251
  return `launchd agent installed (${plistPath}), syncing every ${SYNC_INTERVAL_HOURS}h`;
188
252
  }
189
253
  if (os === "linux") {
190
- const line = `0 */${SYNC_INTERVAL_HOURS} * * * "${process.execPath}" "${cliScriptPath()}" sync >"${syncLogPath()}" 2>&1`;
254
+ const line = expectedLinuxCronLine();
191
255
  const current = spawnSync("crontab", ["-l"], { encoding: "utf8" });
192
256
  const existing = current.status === 0 ? current.stdout : "";
193
- if (!existing.includes("whoburnedmore")) {
194
- const next = `${existing.trimEnd()}
257
+ const kept = existing.split("\n").filter((l) => !l.includes("whoburnedmore")).join("\n");
258
+ const next = `${kept.trimEnd()}
195
259
  ${line}
196
- `;
197
- const res = spawnSync("crontab", ["-"], { input: next });
198
- if (res.status !== 0) throw new Error("could not install crontab entry");
199
- }
260
+ `.replace(/^\n+/, "");
261
+ const res = spawnSync("crontab", ["-"], { input: next });
262
+ if (res.status !== 0) throw new Error("could not install crontab entry");
200
263
  return `cron entry installed, syncing every ${SYNC_INTERVAL_HOURS}h`;
201
264
  }
202
265
  if (os === "win32") {
@@ -210,13 +273,16 @@ ${line}
210
273
  "/TN",
211
274
  "whoburnedmore-sync",
212
275
  "/TR",
213
- `"${process.execPath}" "${cliScriptPath()}" sync`
276
+ `"${resolveNodePath()}" "${cliScriptPath()}" sync`
214
277
  ]);
215
278
  if (res.status !== 0) throw new Error("could not create scheduled task");
216
279
  return `scheduled task installed, syncing every ${SYNC_INTERVAL_HOURS}h`;
217
280
  }
218
281
  throw new Error(`auto-sync is not supported on ${os}`);
219
282
  }
283
+ function expectedLinuxCronLine() {
284
+ return `0 */${SYNC_INTERVAL_HOURS} * * * "${resolveNodePath()}" "${cliScriptPath()}" sync >"${syncLogPath()}" 2>&1`;
285
+ }
220
286
  function uninstallAutoSync() {
221
287
  const os = platform();
222
288
  if (os === "darwin") {
@@ -255,6 +321,52 @@ function autoSyncInstalled() {
255
321
  }
256
322
  return false;
257
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
+ }
258
370
 
259
371
  // src/banner.ts
260
372
  import pc from "picocolors";
@@ -281,7 +393,7 @@ import { dirname as dirname2, join as join6 } from "node:path";
281
393
  import { promisify } from "node:util";
282
394
 
283
395
  // src/attribution.ts
284
- import { readFileSync as readFileSync2, readdirSync, statSync } from "node:fs";
396
+ import { readFileSync as readFileSync3, readdirSync, statSync as statSync2 } from "node:fs";
285
397
  import { homedir as homedir3 } from "node:os";
286
398
  import { basename, join as join3 } from "node:path";
287
399
 
@@ -321,7 +433,8 @@ function createAccumulator() {
321
433
  messageCount: 0,
322
434
  subagentMessages: 0,
323
435
  subagentTokens: 0,
324
- totalTokens: 0
436
+ totalTokens: 0,
437
+ userMessageCount: 0
325
438
  },
326
439
  titles: /* @__PURE__ */ new Map(),
327
440
  sessionMessages: /* @__PURE__ */ new Map()
@@ -339,10 +452,24 @@ function recordTokens(usage) {
339
452
  };
340
453
  return n(u.input_tokens) + n(u.output_tokens) + n(u.cache_creation_input_tokens) + n(u.cache_read_input_tokens);
341
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
+ }
342
466
  function processRecord(rec, acc, ctx) {
343
467
  if (!rec || typeof rec !== "object") return;
344
468
  const r = rec;
345
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
+ }
346
473
  if (typeof r.attributionSkill === "string" && r.attributionSkill) {
347
474
  const s = r.attributionSkill.slice(0, 128);
348
475
  const sk = acc.skills.get(s) ?? { count: 0, tokens: 0 };
@@ -518,13 +645,13 @@ function processCodexRecord(rec, acc, ctx) {
518
645
  function readLines(file) {
519
646
  let size = 0;
520
647
  try {
521
- size = statSync(file).size;
648
+ size = statSync2(file).size;
522
649
  } catch {
523
650
  return [];
524
651
  }
525
652
  if (size > MAX_FILE_BYTES) return [];
526
653
  try {
527
- return readFileSync2(file, "utf8").split("\n");
654
+ return readFileSync3(file, "utf8").split("\n");
528
655
  } catch {
529
656
  return [];
530
657
  }
@@ -552,7 +679,7 @@ function listTranscripts(dir) {
552
679
  if (e.isDirectory()) walk(p);
553
680
  else if (e.isFile() && e.name.endsWith(".jsonl")) {
554
681
  try {
555
- out.push({ path: p, mtime: statSync(p).mtimeMs });
682
+ out.push({ path: p, mtime: statSync2(p).mtimeMs });
556
683
  } catch {
557
684
  }
558
685
  }
@@ -1145,6 +1272,68 @@ async function collectAll(onProgress) {
1145
1272
  };
1146
1273
  }
1147
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
+
1148
1337
  // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
1149
1338
  var external_exports = {};
1150
1339
  __export(external_exports, {
@@ -5264,7 +5453,13 @@ var AgentStat = external_exports.object({
5264
5453
  /** Tokens spent inside subagent sidechains. */
5265
5454
  subagentTokens: external_exports.number().int().nonnegative(),
5266
5455
  /** Total tokens observed across transcripts (denominator for the share). */
5267
- 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()
5268
5463
  });
5269
5464
  var SkillStat = external_exports.object({
5270
5465
  name: external_exports.string().min(1).max(128),
@@ -5308,7 +5503,6 @@ var LeaderboardPeriod = external_exports.enum(["today", "7d", "30d", "all"]);
5308
5503
  var LeaderboardMetric = external_exports.enum(["tokens", "cost"]);
5309
5504
 
5310
5505
  // src/output.ts
5311
- import pc2 from "picocolors";
5312
5506
  function formatTokens(n) {
5313
5507
  if (n >= 1e9) return `${(n / 1e9).toFixed(2)}B`;
5314
5508
  if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
@@ -5318,40 +5512,21 @@ function formatTokens(n) {
5318
5512
  function formatUSD(n) {
5319
5513
  return `$${n.toLocaleString("en-US", { maximumFractionDigits: 2 })}`;
5320
5514
  }
5321
- function printSummary(entries) {
5322
- const byTool = /* @__PURE__ */ new Map();
5323
- let totalTokens = 0;
5324
- let totalCost = 0;
5325
- let todayTokens = 0;
5326
- const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
5327
- for (const e of entries) {
5328
- const tokens = entryTotalTokens(e);
5329
- totalTokens += tokens;
5330
- totalCost += e.costUSD;
5331
- if (e.date === today) todayTokens += tokens;
5332
- const agg = byTool.get(e.tool) ?? { tokens: 0, cost: 0, days: /* @__PURE__ */ new Set() };
5333
- agg.tokens += tokens;
5334
- agg.cost += e.costUSD;
5335
- agg.days.add(e.date);
5336
- byTool.set(e.tool, agg);
5337
- }
5338
- console.log();
5339
- console.log(pc2.bold(pc2.yellow(" \u{1F525} your burn report")));
5340
- console.log();
5341
- const rows = [...byTool.entries()].sort((a, b) => b[1].tokens - a[1].tokens);
5342
- for (const [tool, agg] of rows) {
5343
- console.log(
5344
- ` ${pc2.cyan(tool.padEnd(10))} ${formatTokens(agg.tokens).padStart(9)} tokens ${formatUSD(agg.cost).padStart(10)} ${String(agg.days.size).padStart(4)} days`
5345
- );
5346
- }
5347
- console.log(pc2.dim(" " + "\u2500".repeat(46)));
5348
- console.log(
5349
- ` ${pc2.bold("total".padEnd(10))} ${pc2.bold(formatTokens(totalTokens).padStart(9))} tokens ${pc2.bold(formatUSD(totalCost).padStart(10))}`
5350
- );
5351
- if (todayTokens > 0) {
5352
- console.log(` ${pc2.dim("today".padEnd(10))} ${formatTokens(todayTokens).padStart(9)} tokens`);
5353
- }
5354
- 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
+ ];
5355
5530
  }
5356
5531
 
5357
5532
  // src/local-dashboard.ts
@@ -5592,14 +5767,12 @@ var LOADING_VIBES = [
5592
5767
  ];
5593
5768
  function startProgress() {
5594
5769
  if (!process.stdout.isTTY) {
5595
- let lastLogged = -1;
5770
+ let announced = false;
5596
5771
  return {
5597
- onProgress: (done, total) => {
5598
- const pct = Math.round(done / total * 100);
5599
- if (pct >= lastLogged + 25 || pct === 100 && lastLogged < 100) {
5600
- lastLogged = pct;
5601
- console.log(pc3.dim(` counting local token usage\u2026 ${pct}%`));
5602
- }
5772
+ onProgress: () => {
5773
+ if (announced) return;
5774
+ announced = true;
5775
+ console.log(pc2.dim(" Counting your token usage\u2026"));
5603
5776
  },
5604
5777
  stop: () => {
5605
5778
  }
@@ -5620,9 +5793,9 @@ function startProgress() {
5620
5793
  shown += (target - shown) * 0.3;
5621
5794
  if (target - shown < 4e-3) shown = target;
5622
5795
  const filled = Math.round(shown * width);
5623
- 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));
5624
5797
  const pct = String(Math.round(shown * 100)).padStart(3);
5625
- process.stdout.write(`\r ${bar} ${pct}% ${pc3.dim(vibe())}\x1B[K`);
5798
+ process.stdout.write(`\r ${bar} ${pct}% ${pc2.dim(vibe())}\x1B[K`);
5626
5799
  };
5627
5800
  render();
5628
5801
  const timer = setInterval(render, 60);
@@ -5646,7 +5819,7 @@ function openBrowser(url) {
5646
5819
  async function confirm(question) {
5647
5820
  if (!process.stdin.isTTY) return false;
5648
5821
  const rl = createInterface({ input: process.stdin, output: process.stdout });
5649
- const answer = (await rl.question(`${question} ${pc3.dim("[Y/n]")} `)).trim();
5822
+ const answer = (await rl.question(`${question} ${pc2.dim("[Y/n]")} `)).trim();
5650
5823
  rl.close();
5651
5824
  return answer === "" || /^y(es)?$/i.test(answer);
5652
5825
  }
@@ -5662,20 +5835,20 @@ function showLocalDashboard(payload) {
5662
5835
  })
5663
5836
  );
5664
5837
  console.log();
5665
- console.log(` Local dashboard: ${pc3.cyan(`file://${file}`)}`);
5666
- 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."));
5667
5840
  openBrowser(`file://${file}`);
5668
5841
  }
5669
5842
  async function run(flags) {
5670
5843
  if (!flags.quiet) {
5671
5844
  printBanner();
5672
- 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()}`));
5673
5846
  if (!flags.dryRun && !flags.noSubmit && !flags.local) {
5674
5847
  console.log(
5675
- 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.")
5676
5849
  );
5677
5850
  console.log(
5678
- 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)")
5679
5852
  );
5680
5853
  }
5681
5854
  console.log();
@@ -5688,11 +5861,11 @@ async function run(flags) {
5688
5861
  } finally {
5689
5862
  progress.stop();
5690
5863
  }
5691
- const { entries, sessions, blocks, toolsFound, tools, skills, projects, agent, attributionComplete } = collected;
5864
+ const { entries, sessions, blocks, tools, skills, projects, agent, attributionComplete } = collected;
5692
5865
  if (entries.length === 0) {
5693
5866
  console.log();
5694
5867
  console.log(" Nothing to burn yet \u2014 no local usage found from any coding agent.");
5695
- 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."));
5696
5869
  return;
5697
5870
  }
5698
5871
  const payload = { cliVersion: VERSION, entries };
@@ -5706,11 +5879,10 @@ async function run(flags) {
5706
5879
  payload.attributionComplete = true;
5707
5880
  if (flags.board) payload.board = flags.board;
5708
5881
  if (flags.dryRun) {
5709
- 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"));
5710
5883
  console.log(JSON.stringify(payload, null, 2));
5711
5884
  return;
5712
5885
  }
5713
- if (!flags.quiet) printSummary(entries);
5714
5886
  if (flags.local) {
5715
5887
  showLocalDashboard(payload);
5716
5888
  if (!flags.quiet && process.stdin.isTTY) {
@@ -5719,55 +5891,46 @@ async function run(flags) {
5719
5891
  ensureAnonKey,
5720
5892
  anonSubmit,
5721
5893
  openBrowser,
5722
- log: (line) => console.log(pc3.dim(line))
5894
+ log: (line) => console.log(pc2.dim(line))
5723
5895
  });
5724
5896
  }
5725
5897
  return;
5726
5898
  }
5727
5899
  if (flags.noSubmit) {
5728
- console.log(pc3.dim(" --no-submit: skipped the dashboard."));
5900
+ console.log(pc2.dim(" --no-submit: skipped the dashboard."));
5729
5901
  return;
5730
5902
  }
5731
5903
  const anonKey = ensureAnonKey();
5732
5904
  const result = await anonSubmit(anonKey, payload);
5905
+ try {
5906
+ recordSync();
5907
+ } catch {
5908
+ }
5733
5909
  const target = result.boardUrl ?? claimUrl(result.dashboardUrl, anonKey);
5734
5910
  if (!flags.quiet) {
5735
- console.log(pc3.dim(" Opening your dashboard in your browser\u2026"));
5736
- openBrowser(target);
5737
- }
5738
- console.log(
5739
- ` Submitted ${pc3.bold(String(result.upserted))} day-entries from ${toolsFound.join(", ")}.`
5740
- );
5741
- if (result.boardUrl) {
5742
5911
  console.log(
5743
- ` You burned ${pc3.bold(formatTokens(result.totalTokens))} tokens \u2014 \u{1F91D} you're on the friends board:`
5912
+ pc2.green(" \u2713 Synced securely.") + pc2.dim(" Only your daily totals left this machine \u2014 never your prompts, code, or file names.")
5744
5913
  );
5745
- console.log(` ${pc3.cyan(result.boardUrl)}`);
5746
- console.log(pc3.dim(` Your dashboard: ${result.dashboardUrl}`));
5747
- } else {
5748
- console.log(
5749
- ` You burned ${pc3.bold(formatTokens(result.totalTokens))} tokens \u2014 you're on the public leaderboard:`
5750
- );
5751
- console.log(` ${pc3.cyan(result.dashboardUrl)}`);
5752
- if (!flags.quiet) {
5753
- console.log(
5754
- pc3.dim(" Claim it (name + X) on the web to own your rank, or make it private / remove it.")
5755
- );
5756
- console.log(
5757
- pc3.dim(" Manage anytime: `npx whoburnedmore private` \xB7 `npx whoburnedmore public` \xB7 `npx whoburnedmore remove`.")
5758
- );
5759
- }
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);
5760
5923
  }
5761
- if (!flags.quiet && !autoSyncInstalled()) {
5924
+ if (!flags.quiet) {
5762
5925
  try {
5763
- installAutoSync();
5926
+ reconcileAutoSync();
5764
5927
  } catch {
5765
5928
  }
5766
5929
  }
5767
5930
  if (!flags.quiet) {
5768
5931
  console.log();
5769
5932
  console.log(
5770
- 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.")
5771
5934
  );
5772
5935
  }
5773
5936
  }
@@ -5779,7 +5942,7 @@ async function main() {
5779
5942
  return;
5780
5943
  }
5781
5944
  const args = process.argv.slice(2);
5782
- const command = args.find((a) => !a.startsWith("-")) ?? (args.includes("--version") || args.includes("-v") ? "version" : "run");
5945
+ const command = resolveCommand(args);
5783
5946
  const flags = {
5784
5947
  dryRun: args.includes("--dry-run"),
5785
5948
  noSubmit: args.includes("--no-submit"),
@@ -5793,9 +5956,15 @@ async function main() {
5793
5956
  break;
5794
5957
  case "sync": {
5795
5958
  if (!loadConfig()) return;
5959
+ rotateLogIfLarge();
5796
5960
  await run({ ...flags, noSubmit: false, dryRun: false, local: false });
5797
5961
  break;
5798
5962
  }
5963
+ case "status":
5964
+ case "doctor": {
5965
+ for (const line of agentStatusReport()) console.log(line);
5966
+ break;
5967
+ }
5799
5968
  case "private":
5800
5969
  case "public": {
5801
5970
  const cfg = loadConfig();
@@ -5826,12 +5995,9 @@ async function main() {
5826
5995
  console.log(` ${uninstallAutoSync()}`);
5827
5996
  break;
5828
5997
  case "help":
5829
- case "--help":
5830
5998
  printHelp();
5831
5999
  break;
5832
6000
  case "version":
5833
- case "--version":
5834
- case "-v":
5835
6001
  console.log(VERSION);
5836
6002
  break;
5837
6003
  default:
@@ -5842,17 +6008,18 @@ async function main() {
5842
6008
  }
5843
6009
  function printHelp() {
5844
6010
  console.log(`
5845
- ${pc3.bold("whoburnedmore")} \u2014 who burned more tokens, you or them?
6011
+ ${pc2.bold("whoburnedmore")} \u2014 who burned more tokens, you or them?
5846
6012
 
5847
- ${pc3.bold("usage")}
6013
+ ${pc2.bold("usage")}
5848
6014
  npx whoburnedmore burn + land on the public leaderboard, open your dashboard
5849
6015
  npx whoburnedmore --board=CODE compare with friends \u2014 join their board (no sign-in)
5850
6016
  npx whoburnedmore --local build the dashboard on your machine and open it (offline)
5851
6017
  npx whoburnedmore --dry-run print exactly what would be sent, send nothing
5852
- npx whoburnedmore --no-submit print local stats only, send nothing
6018
+ npx whoburnedmore --no-submit collect locally, send nothing (no dashboard)
5853
6019
  npx whoburnedmore private hide your dashboard from the leaderboard
5854
6020
  npx whoburnedmore public put it back on the leaderboard
5855
6021
  npx whoburnedmore remove delete your dashboard and its data
6022
+ npx whoburnedmore status check background-sync health (last sync, staleness)
5856
6023
  npx whoburnedmore uninstall-sync turn off the background sync
5857
6024
  npx whoburnedmore install-sync turn it back on after uninstalling
5858
6025
 
@@ -5866,7 +6033,7 @@ function printHelp() {
5866
6033
  `);
5867
6034
  }
5868
6035
  main().catch((err) => {
5869
- console.error(pc3.red(`
6036
+ console.error(pc2.red(`
5870
6037
  ${err.message}
5871
6038
  `));
5872
6039
  process.exitCode = 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whoburnedmore",
3
- "version": "0.8.1",
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": {