whoburnedmore 0.8.5 → 0.8.9

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 (3) hide show
  1. package/README.md +6 -2
  2. package/dist/index.js +391 -45
  3. package/package.json +3 -2
package/README.md CHANGED
@@ -62,7 +62,7 @@ npx whoburnedmore --local
62
62
  | `npx whoburnedmore --no-submit` | Print local stats only, send nothing |
63
63
  | `npx whoburnedmore login` | Sign in to claim a public handle + join the leaderboard |
64
64
  | `npx whoburnedmore logout` | Forget the local token (your data is untouched) |
65
- | `npx whoburnedmore install-sync` | Keep your dashboard live with a background sync (hourly) |
65
+ | `npx whoburnedmore install-sync` | Keep your dashboard live with a background sync (every 15 min) |
66
66
  | `npx whoburnedmore uninstall-sync` | Remove the background sync |
67
67
 
68
68
  ## Supported tools
@@ -88,10 +88,14 @@ It uses a device flow — a code appears in your terminal, you approve it in the
88
88
  Want your dashboard to stay fresh without re-running by hand?
89
89
 
90
90
  ```bash
91
- npx whoburnedmore install-sync # background sync hourly (launchd / cron / scheduled task)
91
+ npx whoburnedmore install-sync # background sync every 15 min (launchd / cron / scheduled task)
92
92
  npx whoburnedmore uninstall-sync # remove it
93
93
  ```
94
94
 
95
+ The installed background job runs the latest published `whoburnedmore` package on
96
+ each sync tick, so future CLI fixes are picked up automatically after the next
97
+ 15-minute refresh.
98
+
95
99
  ## Links
96
100
 
97
101
  - 🏆 Leaderboard — **[whoburnedmore.com](https://whoburnedmore.com)**
package/dist/index.js CHANGED
@@ -16,9 +16,24 @@ import pc2 from "picocolors";
16
16
 
17
17
  // src/args.ts
18
18
  function parseBoard(args) {
19
- const eq = args.find((a) => a.startsWith("--board="));
20
- if (eq) return eq.slice("--board=".length).trim() || void 0;
21
- const i = args.indexOf("--board");
19
+ return parseValueFlag(args, "--board");
20
+ }
21
+ function parseOrg(args) {
22
+ return parseValueFlag(args, "--org");
23
+ }
24
+ function parseInstallToken(args) {
25
+ return parseValueFlag(args, "--token");
26
+ }
27
+ function applyScope(payload, flags) {
28
+ if (flags.board) payload.board = flags.board;
29
+ if (flags.org) payload.org = flags.org;
30
+ return payload;
31
+ }
32
+ function parseValueFlag(args, name) {
33
+ const prefix = `${name}=`;
34
+ const eq = args.find((a) => a.startsWith(prefix));
35
+ if (eq) return eq.slice(prefix.length).trim() || void 0;
36
+ const i = args.indexOf(name);
22
37
  if (i !== -1 && args[i + 1] && !args[i + 1].startsWith("-")) {
23
38
  return args[i + 1].trim() || void 0;
24
39
  }
@@ -31,7 +46,16 @@ function resolveCommand(args) {
31
46
  if (args.includes("--version") || args.includes("-v") || args.includes("version")) {
32
47
  return "version";
33
48
  }
34
- return args.find((a) => !a.startsWith("-")) ?? "run";
49
+ const valueFlags = /* @__PURE__ */ new Set(["--board", "--org", "--token"]);
50
+ for (let i = 0; i < args.length; i++) {
51
+ const a = args[i];
52
+ if (a.startsWith("-")) {
53
+ if (valueFlags.has(a)) i++;
54
+ continue;
55
+ }
56
+ return a;
57
+ }
58
+ return "run";
35
59
  }
36
60
 
37
61
  // src/api.ts
@@ -41,6 +65,20 @@ function apiBase() {
41
65
  function webBase() {
42
66
  return process.env.WHOBURNEDMORE_WEB ?? "https://whoburnedmore.com";
43
67
  }
68
+ function isTrustedWebUrl(url) {
69
+ let u;
70
+ let base;
71
+ try {
72
+ u = new URL(url);
73
+ base = new URL(webBase());
74
+ } catch {
75
+ return false;
76
+ }
77
+ return (u.protocol === "https:" || u.protocol === "http:") && u.protocol === base.protocol && u.host === base.host;
78
+ }
79
+ function isOpenableUrl(url) {
80
+ return /^(https?|file):\/\//.test(url);
81
+ }
44
82
  async function readJson(res) {
45
83
  const text = await res.text();
46
84
  if (!text) return {};
@@ -80,6 +118,9 @@ async function anonSubmit(anonKey, payload) {
80
118
  function claimUrl(dashboardUrl, anonKey) {
81
119
  return `${dashboardUrl}#k=${encodeURIComponent(anonKey)}`;
82
120
  }
121
+ function boardClaimUrl(boardUrl, slug, anonKey) {
122
+ return `${boardUrl}#k=${encodeURIComponent(anonKey)}&u=${encodeURIComponent(slug)}`;
123
+ }
83
124
  async function anonVisibility(anonKey, listed) {
84
125
  const { status, body } = await post(
85
126
  "/v1/anon/visibility",
@@ -100,6 +141,15 @@ async function anonRemove(anonKey) {
100
141
  throw new Error(b.error ?? `failed (HTTP ${res.status})`);
101
142
  }
102
143
  }
144
+ async function redeemServerInstall(token, anonKey) {
145
+ const { status, body } = await post("/v1/server-install/redeem", { token, anonKey });
146
+ if (status !== 200) {
147
+ throw new Error(
148
+ body.error ?? `server install failed (HTTP ${status})`
149
+ );
150
+ }
151
+ return body;
152
+ }
103
153
 
104
154
  // src/autosync.ts
105
155
  import { spawnSync } from "node:child_process";
@@ -113,8 +163,7 @@ import {
113
163
  writeFileSync as writeFileSync2
114
164
  } from "node:fs";
115
165
  import { homedir as homedir2, platform } from "node:os";
116
- import { join as join2 } from "node:path";
117
- import { fileURLToPath } from "node:url";
166
+ import { dirname, join as join2, win32 } from "node:path";
118
167
 
119
168
  // src/config.ts
120
169
  import { randomBytes } from "node:crypto";
@@ -128,6 +177,8 @@ import {
128
177
  import { homedir } from "node:os";
129
178
  import { join } from "node:path";
130
179
  function defaultConfigDir() {
180
+ const override = process.env.WHOBURNEDMORE_CONFIG_DIR?.trim();
181
+ if (override) return override;
131
182
  return join(homedir(), ".config", "whoburnedmore");
132
183
  }
133
184
  function loadConfig(dir = defaultConfigDir()) {
@@ -139,6 +190,9 @@ function loadConfig(dir = defaultConfigDir()) {
139
190
  if (typeof parsed.anonKey === "string") config.anonKey = parsed.anonKey;
140
191
  if (typeof parsed.lastSyncAt === "number" && Number.isFinite(parsed.lastSyncAt))
141
192
  config.lastSyncAt = parsed.lastSyncAt;
193
+ if (typeof parsed.launchNotificationDeliveredAt === "number" && Number.isFinite(parsed.launchNotificationDeliveredAt)) {
194
+ config.launchNotificationDeliveredAt = parsed.launchNotificationDeliveredAt;
195
+ }
142
196
  return Object.keys(config).length > 0 ? config : null;
143
197
  } catch {
144
198
  return null;
@@ -164,19 +218,33 @@ function recordSync(dir = defaultConfigDir(), when = Date.now()) {
164
218
  const config = loadConfig(dir) ?? {};
165
219
  saveConfig(dir, { ...config, lastSyncAt: when });
166
220
  }
221
+ function recordLaunchNotificationDelivered(dir = defaultConfigDir(), when = Date.now()) {
222
+ const config = loadConfig(dir) ?? {};
223
+ saveConfig(dir, { ...config, launchNotificationDeliveredAt: when });
224
+ }
167
225
 
168
226
  // src/autosync.ts
169
- var SYNC_INTERVAL_HOURS = 1;
227
+ var SYNC_INTERVAL_MINUTES = 15;
228
+ function syncIntervalLabel(mins = SYNC_INTERVAL_MINUTES) {
229
+ return mins % 60 === 0 ? `${mins / 60}h` : `${mins}m`;
230
+ }
170
231
  var LABEL = "com.whoburnedmore.sync";
171
232
  var STABLE_NODE_CANDIDATES = [
172
233
  "/opt/homebrew/bin/node",
173
234
  "/usr/local/bin/node",
174
235
  "/usr/bin/node"
175
236
  ];
237
+ var STABLE_NPM_CANDIDATES = [
238
+ "/opt/homebrew/bin/npm",
239
+ "/usr/local/bin/npm",
240
+ "/usr/bin/npm"
241
+ ];
242
+ var LATEST_PACKAGE_SPEC = "whoburnedmore@latest";
176
243
  function syncLogPath() {
177
244
  return join2(defaultConfigDir(), "sync.log");
178
245
  }
179
- function buildLaunchdPlist(nodePath, scriptPath, logPath = syncLogPath()) {
246
+ function buildLaunchdPlist(commandArgs = syncCommandArgs(), logPath = syncLogPath()) {
247
+ const programArguments = commandArgs.map((arg) => ` <string>${xmlEscape(arg)}</string>`).join("\n");
180
248
  return `<?xml version="1.0" encoding="UTF-8"?>
181
249
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
182
250
  <plist version="1.0">
@@ -185,12 +253,10 @@ function buildLaunchdPlist(nodePath, scriptPath, logPath = syncLogPath()) {
185
253
  <string>${LABEL}</string>
186
254
  <key>ProgramArguments</key>
187
255
  <array>
188
- <string>${nodePath}</string>
189
- <string>${scriptPath}</string>
190
- <string>sync</string>
256
+ ${programArguments}
191
257
  </array>
192
258
  <key>StartInterval</key>
193
- <integer>${SYNC_INTERVAL_HOURS * 3600}</integer>
259
+ <integer>${SYNC_INTERVAL_MINUTES * 60}</integer>
194
260
  <!-- Run once right after login/reboot so a machine that was off (or asleep)
195
261
  through a scheduled tick catches up immediately, then keeps to the
196
262
  interval. Submits are idempotent server-side, so an extra run is safe. -->
@@ -200,9 +266,9 @@ function buildLaunchdPlist(nodePath, scriptPath, logPath = syncLogPath()) {
200
266
  <key>ProcessType</key>
201
267
  <string>Background</string>
202
268
  <key>StandardOutPath</key>
203
- <string>${logPath}</string>
269
+ <string>${xmlEscape(logPath)}</string>
204
270
  <key>StandardErrorPath</key>
205
- <string>${logPath}</string>
271
+ <string>${xmlEscape(logPath)}</string>
206
272
  </dict>
207
273
  </plist>
208
274
  `;
@@ -210,9 +276,6 @@ function buildLaunchdPlist(nodePath, scriptPath, logPath = syncLogPath()) {
210
276
  function launchAgentPath() {
211
277
  return join2(homedir2(), "Library", "LaunchAgents", `${LABEL}.plist`);
212
278
  }
213
- function cliScriptPath() {
214
- return fileURLToPath(new URL("./index.js", import.meta.url));
215
- }
216
279
  function isUsableNode(p) {
217
280
  if (!existsSync2(p)) return false;
218
281
  const res = spawnSync(p, ["-v"], { encoding: "utf8" });
@@ -229,8 +292,51 @@ function resolveNodePath(opts) {
229
292
  }
230
293
  return execPath;
231
294
  }
295
+ function isUsableNpm(p) {
296
+ if (!existsSync2(p)) return false;
297
+ const res = spawnSync(p, ["--version"], { encoding: "utf8" });
298
+ return res.status === 0;
299
+ }
300
+ function resolveNpmPath(opts) {
301
+ const candidates = opts?.candidates ?? STABLE_NPM_CANDIDATES;
302
+ const check = opts?.check ?? isUsableNpm;
303
+ const execPath = opts?.execPath ?? process.execPath;
304
+ const os = opts?.platform ?? platform();
305
+ for (const c of candidates) {
306
+ if (check(c)) return c;
307
+ }
308
+ const sibling = os === "win32" ? win32.join(win32.dirname(execPath), "npm.cmd") : join2(dirname(execPath), "npm");
309
+ if (check(sibling)) return sibling;
310
+ return os === "win32" ? "npm.cmd" : "npm";
311
+ }
312
+ function syncCommandArgs(npmPath = resolveNpmPath()) {
313
+ return [
314
+ npmPath,
315
+ "exec",
316
+ "--yes",
317
+ "--ignore-scripts",
318
+ "--package",
319
+ LATEST_PACKAGE_SPEC,
320
+ "--",
321
+ "whoburnedmore",
322
+ "sync"
323
+ ];
324
+ }
325
+ function xmlEscape(value) {
326
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;").replaceAll(">", "&gt;");
327
+ }
328
+ function shellQuote(value) {
329
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
330
+ }
331
+ function windowsQuote(value) {
332
+ const escaped = value.replace(/(\\*)"/g, '$1$1\\"').replace(/\\+$/g, (slashes) => `${slashes}${slashes}`);
333
+ return `"${escaped}"`;
334
+ }
335
+ function windowsCommandLine(args) {
336
+ return args.map(windowsQuote).join(" ");
337
+ }
232
338
  function expectedDarwinPlist() {
233
- return buildLaunchdPlist(resolveNodePath(), cliScriptPath());
339
+ return buildLaunchdPlist(syncCommandArgs());
234
340
  }
235
341
  function plistDrift(installed, expected) {
236
342
  if (installed === null) return "absent";
@@ -248,7 +354,7 @@ function installAutoSync() {
248
354
  writeFileSync2(plistPath, expectedDarwinPlist());
249
355
  spawnSync("launchctl", ["unload", plistPath], { stdio: "ignore" });
250
356
  spawnSync("launchctl", ["load", plistPath], { stdio: "ignore" });
251
- return `launchd agent installed (${plistPath}), syncing every ${SYNC_INTERVAL_HOURS}h`;
357
+ return `launchd agent installed (${plistPath}), syncing every ${syncIntervalLabel()}`;
252
358
  }
253
359
  if (os === "linux") {
254
360
  const line = expectedLinuxCronLine();
@@ -260,28 +366,32 @@ ${line}
260
366
  `.replace(/^\n+/, "");
261
367
  const res = spawnSync("crontab", ["-"], { input: next });
262
368
  if (res.status !== 0) throw new Error("could not install crontab entry");
263
- return `cron entry installed, syncing every ${SYNC_INTERVAL_HOURS}h`;
369
+ return `cron entry installed, syncing every ${syncIntervalLabel()}`;
264
370
  }
265
371
  if (os === "win32") {
266
372
  const res = spawnSync("schtasks", [
267
373
  "/Create",
268
374
  "/F",
269
375
  "/SC",
270
- "HOURLY",
376
+ "MINUTE",
271
377
  "/MO",
272
- String(SYNC_INTERVAL_HOURS),
378
+ String(SYNC_INTERVAL_MINUTES),
273
379
  "/TN",
274
380
  "whoburnedmore-sync",
275
381
  "/TR",
276
- `"${resolveNodePath()}" "${cliScriptPath()}" sync`
382
+ windowsCommandLine(syncCommandArgs())
277
383
  ]);
278
384
  if (res.status !== 0) throw new Error("could not create scheduled task");
279
- return `scheduled task installed, syncing every ${SYNC_INTERVAL_HOURS}h`;
385
+ return `scheduled task installed, syncing every ${syncIntervalLabel()}`;
280
386
  }
281
387
  throw new Error(`auto-sync is not supported on ${os}`);
282
388
  }
283
- function expectedLinuxCronLine() {
284
- return `0 */${SYNC_INTERVAL_HOURS} * * * "${resolveNodePath()}" "${cliScriptPath()}" sync >"${syncLogPath()}" 2>&1`;
389
+ function cronSchedule(mins = SYNC_INTERVAL_MINUTES) {
390
+ return mins % 60 === 0 ? `0 */${mins / 60} * * *` : `*/${mins} * * * *`;
391
+ }
392
+ function expectedLinuxCronLine(opts) {
393
+ const command = syncCommandArgs(opts?.npmPath).map(shellQuote).join(" ");
394
+ return `${cronSchedule()} ${command} >${shellQuote(opts?.logPath ?? syncLogPath())} 2>&1`;
285
395
  }
286
396
  function uninstallAutoSync() {
287
397
  const os = platform();
@@ -360,6 +470,41 @@ function rotateLogIfLarge(path = syncLogPath(), capBytes = 256 * 1024) {
360
470
  return false;
361
471
  }
362
472
  }
473
+ function notifyLaunchLive(opts) {
474
+ const os = opts?.platform ?? platform();
475
+ const run2 = opts?.spawn ?? ((cmd, args) => spawnSync(cmd, args, { stdio: "ignore" }));
476
+ const title = "whoburnedmore is live";
477
+ const message = "Your dashboard is ready. Go to whoburnedmore.com";
478
+ if (os === "darwin") {
479
+ return run2("osascript", [
480
+ "-e",
481
+ `display notification "${message}" with title "${title}"`
482
+ ]).status === 0;
483
+ }
484
+ if (os === "linux") {
485
+ return run2("notify-send", [title, message]).status === 0;
486
+ }
487
+ if (os === "win32") {
488
+ const psQuote = (value) => `'${value.replaceAll("'", "''")}'`;
489
+ const script = [
490
+ "Add-Type -AssemblyName System.Windows.Forms",
491
+ "$n = New-Object System.Windows.Forms.NotifyIcon",
492
+ "$n.Icon = [System.Drawing.SystemIcons]::Application",
493
+ `$n.BalloonTipTitle = ${psQuote(title)}`,
494
+ `$n.BalloonTipText = ${psQuote(message)}`,
495
+ "$n.Visible = $true",
496
+ "$n.ShowBalloonTip(10000)",
497
+ "Start-Sleep -Seconds 2",
498
+ "$n.Dispose()"
499
+ ].join("; ");
500
+ return run2("powershell", [
501
+ "-NoProfile",
502
+ "-Command",
503
+ script
504
+ ]).status === 0;
505
+ }
506
+ return false;
507
+ }
363
508
  function autoSyncLoaded() {
364
509
  if (platform() === "darwin") {
365
510
  const res = spawnSync("launchctl", ["list"], { encoding: "utf8" });
@@ -389,7 +534,7 @@ function printBanner() {
389
534
  // src/collect.ts
390
535
  import { execFile } from "node:child_process";
391
536
  import { createRequire as createRequire3 } from "node:module";
392
- import { dirname as dirname2, join as join6 } from "node:path";
537
+ import { dirname as dirname3, join as join6 } from "node:path";
393
538
  import { promisify } from "node:util";
394
539
 
395
540
  // src/attribution.ts
@@ -748,7 +893,7 @@ import { join as join5 } from "node:path";
748
893
  // src/tokscale.ts
749
894
  import { spawnSync as spawnSync2 } from "node:child_process";
750
895
  import { createRequire } from "node:module";
751
- import { dirname, join as join4 } from "node:path";
896
+ import { dirname as dirname2, join as join4 } from "node:path";
752
897
  var LOOKBACK_DAYS = 30;
753
898
  function num(n) {
754
899
  const v = Math.round(Number(n));
@@ -792,7 +937,7 @@ function resolveTokscaleBin() {
792
937
  const pkg = require3("tokscale/package.json");
793
938
  const rel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.tokscale ?? "";
794
939
  if (!rel) return null;
795
- const binPath = join4(dirname(pkgPath), rel);
940
+ const binPath = join4(dirname2(pkgPath), rel);
796
941
  if (/\.(c|m)?js$/.test(binPath)) {
797
942
  return { cmd: process.execPath, prefixArgs: [binPath] };
798
943
  }
@@ -1125,7 +1270,7 @@ function resolveCcusageBin() {
1125
1270
  const pkgPath = require3.resolve("ccusage/package.json");
1126
1271
  const pkg = require3("ccusage/package.json");
1127
1272
  const rel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.ccusage ?? "ccusage";
1128
- const binPath = join6(dirname2(pkgPath), rel);
1273
+ const binPath = join6(dirname3(pkgPath), rel);
1129
1274
  if (/\.(c|m)?js$/.test(binPath)) {
1130
1275
  return { cmd: process.execPath, prefixArgs: [binPath] };
1131
1276
  }
@@ -1149,6 +1294,13 @@ function dedupeDaily(entries) {
1149
1294
  }
1150
1295
  return [...byKey.values()];
1151
1296
  }
1297
+ function entryTokens(e) {
1298
+ return e.inputTokens + e.outputTokens + e.cacheCreationTokens + e.cacheReadTokens;
1299
+ }
1300
+ function capByTokens(rows, max, tokens) {
1301
+ if (rows.length <= max) return rows;
1302
+ return [...rows].sort((a, b) => tokens(b) - tokens(a)).slice(0, max);
1303
+ }
1152
1304
  function dedupeSessions(sessions) {
1153
1305
  const byId = /* @__PURE__ */ new Map();
1154
1306
  const total = (s) => s.inputTokens + s.outputTokens + s.cacheCreationTokens + s.cacheReadTokens;
@@ -1260,9 +1412,14 @@ async function collectAll(onProgress) {
1260
1412
  };
1261
1413
  });
1262
1414
  return {
1263
- entries: dedupeDaily(entries),
1264
- sessions: dedupedSessions,
1265
- blocks: dedupeBlocks(blocks),
1415
+ // Cap each array to the server's accepted maximum (the shared SubmitPayload
1416
+ // schema: entries ≤ 20000, sessions/blocks ≤ 10000), keeping the
1417
+ // highest-token rows. Without this, a power user with >10000 distinct sessions
1418
+ // would have their ENTIRE submit rejected with a 400 instead of a capped one.
1419
+ // tools/skills/projects are already bounded upstream (attribution caps).
1420
+ entries: capByTokens(dedupeDaily(entries), 2e4, entryTokens),
1421
+ sessions: capByTokens(dedupedSessions, 1e4, entryTokens),
1422
+ blocks: capByTokens(dedupeBlocks(blocks), 1e4, (b) => b.totalTokens),
1266
1423
  toolsFound,
1267
1424
  tools,
1268
1425
  skills,
@@ -1293,8 +1450,8 @@ function buildStatusReport(s) {
1293
1450
  " \u21B3 config is out of date \u2014 it will self-repair on your next run"
1294
1451
  );
1295
1452
  }
1296
- lines.push(` \u2022 Interval: every ${s.intervalHours}h`);
1297
- const staleAfterMs = s.intervalHours * 2 * 3600 * 1e3;
1453
+ lines.push(` \u2022 Interval: every ${syncIntervalLabel(s.intervalMinutes)}`);
1454
+ const staleAfterMs = s.intervalMinutes * 2 * 60 * 1e3;
1298
1455
  if (s.lastSyncAt === null) {
1299
1456
  lines.push(" \u2022 Last sync: never recorded");
1300
1457
  lines.push(" \u26A0 STALE: no successful sync recorded yet \u2014 run `npx whoburnedmore`");
@@ -1303,7 +1460,7 @@ function buildStatusReport(s) {
1303
1460
  lines.push(` \u2022 Last sync: ${ago(age)}`);
1304
1461
  if (age > staleAfterMs) {
1305
1462
  lines.push(
1306
- ` \u26A0 STALE: last sync was over ${s.intervalHours * 2}h ago \u2014 your dashboard may be behind. Run \`npx whoburnedmore\`.`
1463
+ ` \u26A0 STALE: last sync was over ${syncIntervalLabel(s.intervalMinutes * 2)} ago \u2014 your dashboard may be behind. Run \`npx whoburnedmore\`.`
1307
1464
  );
1308
1465
  } else {
1309
1466
  lines.push(" \u2713 Fresh \u2014 your dashboard is up to date.");
@@ -1325,7 +1482,7 @@ function agentStatusReport(now = Date.now()) {
1325
1482
  installed: autoSyncInstalled(),
1326
1483
  loaded: autoSyncLoaded(),
1327
1484
  drift: autoSyncDrift(),
1328
- intervalHours: SYNC_INTERVAL_HOURS,
1485
+ intervalMinutes: SYNC_INTERVAL_MINUTES,
1329
1486
  lastSyncAt: typeof cfg?.lastSyncAt === "number" ? cfg.lastSyncAt : null,
1330
1487
  now,
1331
1488
  nodePath,
@@ -5375,6 +5532,48 @@ var coerce = {
5375
5532
  };
5376
5533
  var NEVER = INVALID;
5377
5534
 
5535
+ // ../shared/dist/tenant.js
5536
+ var RESERVED_SUBDOMAINS = /* @__PURE__ */ new Set([
5537
+ "www",
5538
+ "api",
5539
+ "app",
5540
+ "admin",
5541
+ "mail",
5542
+ "email",
5543
+ "cdn",
5544
+ "static",
5545
+ "assets",
5546
+ "blog",
5547
+ "docs",
5548
+ "status",
5549
+ "help",
5550
+ "support",
5551
+ "ingest",
5552
+ "vercel",
5553
+ "preview",
5554
+ "staging",
5555
+ "dev",
5556
+ "test"
5557
+ ]);
5558
+
5559
+ // ../shared/dist/launch-gate.js
5560
+ var LaunchAccessMode = external_exports.enum(["full", "invited", "waitlisted"]);
5561
+ var LaunchStatusResponse = external_exports.object({
5562
+ mode: LaunchAccessMode,
5563
+ launchAt: external_exports.string().datetime(),
5564
+ now: external_exports.string().datetime(),
5565
+ remainingSeconds: external_exports.number().int().nonnegative(),
5566
+ live: external_exports.boolean()
5567
+ });
5568
+ var LaunchRedeemRequest = external_exports.object({
5569
+ code: external_exports.string().trim().min(2).max(64)
5570
+ });
5571
+ var LaunchRedeemResponse = external_exports.object({
5572
+ ok: external_exports.literal(true),
5573
+ mode: LaunchAccessMode,
5574
+ expiresAt: external_exports.string().datetime().nullable()
5575
+ });
5576
+
5378
5577
  // ../shared/dist/index.js
5379
5578
  var DateString = external_exports.string().regex(/^\d{4}-\d{2}-\d{2}$/, "must be YYYY-MM-DD");
5380
5579
  var tokenCount = external_exports.number().int().nonnegative();
@@ -5490,7 +5689,9 @@ var SubmitPayload = external_exports.object({
5490
5689
  */
5491
5690
  attributionComplete: external_exports.boolean().optional(),
5492
5691
  /** Optional friends-board code (from `--board=<code>`): auto-join this board on submit. */
5493
- board: external_exports.string().min(1).max(32).optional()
5692
+ board: external_exports.string().min(1).max(32).optional(),
5693
+ /** Optional organization slug (from `--org=<slug>`): auto-join this org on submit. */
5694
+ org: external_exports.string().min(2).max(32).optional()
5494
5695
  });
5495
5696
  var AnonSubmitPayload = SubmitPayload.extend({
5496
5697
  /** Client-generated secret (hex). The server stores only its hash. */
@@ -5501,6 +5702,94 @@ function entryTotalTokens(e) {
5501
5702
  }
5502
5703
  var LeaderboardPeriod = external_exports.enum(["today", "7d", "30d", "all"]);
5503
5704
  var LeaderboardMetric = external_exports.enum(["tokens", "cost"]);
5705
+ var OrgType = external_exports.enum(["company", "hackathon", "hackerhouse"]);
5706
+ var MemberRole = external_exports.enum(["owner", "admin", "member"]);
5707
+ var OrgBoardVisibility = external_exports.enum(["public", "members"]);
5708
+ var HexColor = external_exports.string().regex(/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/, "must be a hex color like #f97316");
5709
+ var RESERVED_SLUGS = /* @__PURE__ */ new Set([
5710
+ ...RESERVED_SUBDOMAINS,
5711
+ "o",
5712
+ "d",
5713
+ "u",
5714
+ "boards",
5715
+ "board",
5716
+ "claim",
5717
+ "signin",
5718
+ "signout",
5719
+ "login",
5720
+ "logout",
5721
+ "dashboard",
5722
+ "install",
5723
+ "for-teams",
5724
+ "teams",
5725
+ "about",
5726
+ "contact",
5727
+ "trust",
5728
+ "cli",
5729
+ "feedback",
5730
+ "guides",
5731
+ "guide",
5732
+ "join",
5733
+ "leaderboard",
5734
+ "settings",
5735
+ "account",
5736
+ "me",
5737
+ "new",
5738
+ "robots",
5739
+ "sitemap"
5740
+ ]);
5741
+ var SLUG_RE = /^[a-z0-9](?:[a-z0-9-]{0,30}[a-z0-9])?$/;
5742
+ function isValidSlug(slug) {
5743
+ return typeof slug === "string" && slug.length >= 2 && slug.length <= 32 && SLUG_RE.test(slug) && !RESERVED_SLUGS.has(slug);
5744
+ }
5745
+ var OrgSlug = external_exports.string().min(2).max(32).refine(isValidSlug, "invalid or reserved slug");
5746
+ var OrgWindow = external_exports.object({
5747
+ startDate: DateString.nullable().optional(),
5748
+ endDate: DateString.nullable().optional()
5749
+ });
5750
+ var OrgJoinPolicy = external_exports.object({
5751
+ allowCodeJoin: external_exports.boolean().default(true),
5752
+ allowDomainJoin: external_exports.boolean().default(false),
5753
+ /** Verified email domains that auto-join, e.g. ["acme.com"]. */
5754
+ emailDomains: external_exports.array(external_exports.string().min(3).max(253).toLowerCase()).max(20).default([])
5755
+ });
5756
+ var OrgApplicationInput = external_exports.object({
5757
+ type: OrgType,
5758
+ orgName: external_exports.string().min(1).max(120),
5759
+ desiredSlug: external_exports.string().min(2).max(32).optional(),
5760
+ contactName: external_exports.string().min(1).max(120),
5761
+ contactEmail: external_exports.string().email().max(254),
5762
+ website: external_exports.string().url().max(300).optional(),
5763
+ /** Rough headcount / attendee estimate, free text. */
5764
+ size: external_exports.string().max(60).optional(),
5765
+ message: external_exports.string().max(2e3).optional()
5766
+ });
5767
+ var OrgProvisionInput = external_exports.object({
5768
+ /** When provisioning straight from an application. */
5769
+ applicationId: external_exports.string().min(1).max(64).optional(),
5770
+ slug: OrgSlug,
5771
+ name: external_exports.string().min(1).max(120),
5772
+ type: OrgType,
5773
+ /** Handle (or email) of the user who becomes Owner. */
5774
+ ownerHandle: external_exports.string().min(1).max(120).optional(),
5775
+ ownerEmail: external_exports.string().email().max(254).optional(),
5776
+ description: external_exports.string().max(2e3).optional(),
5777
+ boardVisibility: OrgBoardVisibility.optional(),
5778
+ window: OrgWindow.optional()
5779
+ });
5780
+ var OrgSettingsInput = external_exports.object({
5781
+ name: external_exports.string().min(1).max(120).optional(),
5782
+ description: external_exports.string().max(2e3).nullable().optional(),
5783
+ accentColor: HexColor.optional(),
5784
+ logoUrl: external_exports.string().url().max(500).nullable().optional(),
5785
+ boardVisibility: OrgBoardVisibility.optional(),
5786
+ window: OrgWindow.optional(),
5787
+ joinPolicy: OrgJoinPolicy.partial().optional()
5788
+ });
5789
+ var OrgJoinInput = external_exports.object({
5790
+ /** Required only when joining via a code; domain/admin joins omit it. */
5791
+ code: external_exports.string().min(1).max(64).optional()
5792
+ });
5504
5793
 
5505
5794
  // src/output.ts
5506
5795
  function formatTokens(n) {
@@ -5812,6 +6101,7 @@ function startProgress() {
5812
6101
  };
5813
6102
  }
5814
6103
  function openBrowser(url) {
6104
+ if (!isOpenableUrl(url)) return;
5815
6105
  const os = platform3();
5816
6106
  const [cmd, args] = os === "darwin" ? ["open", [url]] : os === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
5817
6107
  spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
@@ -5877,7 +6167,7 @@ async function run(flags) {
5877
6167
  if (agent.messageCount > 0) payload.agent = agent;
5878
6168
  if (attributionComplete && (tools.length > 0 || skills.length > 0 || projects.length > 0))
5879
6169
  payload.attributionComplete = true;
5880
- if (flags.board) payload.board = flags.board;
6170
+ applyScope(payload, flags);
5881
6171
  if (flags.dryRun) {
5882
6172
  console.log(pc2.dim("\n --dry-run: this exact payload would be sent, nothing else:\n"));
5883
6173
  console.log(JSON.stringify(payload, null, 2));
@@ -5906,13 +6196,31 @@ async function run(flags) {
5906
6196
  recordSync();
5907
6197
  } catch {
5908
6198
  }
5909
- const target = result.boardUrl ?? claimUrl(result.dashboardUrl, anonKey);
6199
+ try {
6200
+ const config = loadConfig();
6201
+ if (result.launch?.live && !config?.launchNotificationDeliveredAt) {
6202
+ if (notifyLaunchLive()) {
6203
+ recordLaunchNotificationDelivered();
6204
+ }
6205
+ }
6206
+ } catch {
6207
+ }
6208
+ const baseUrl = result.boardUrl ?? result.dashboardUrl;
6209
+ const target = result.boardUrl ? boardClaimUrl(result.boardUrl, result.slug, anonKey) : claimUrl(result.dashboardUrl, anonKey);
6210
+ const trusted = isTrustedWebUrl(baseUrl);
5910
6211
  if (!flags.quiet) {
5911
6212
  console.log(
5912
6213
  pc2.green(" \u2713 Synced securely.") + pc2.dim(" Only your daily totals left this machine \u2014 never your prompts, code, or file names.")
5913
6214
  );
5914
- console.log(pc2.dim(" Opening your dashboard in your browser\u2026"));
5915
- openBrowser(target);
6215
+ if (trusted) {
6216
+ console.log(pc2.dim(" Opening your dashboard in your browser\u2026"));
6217
+ openBrowser(target);
6218
+ } else {
6219
+ console.log(
6220
+ pc2.dim(" The server returned an unexpected dashboard address, so it was NOT auto-opened. Open it yourself only if you trust it:")
6221
+ );
6222
+ console.log(` ${baseUrl}`);
6223
+ }
5916
6224
  }
5917
6225
  const lines = submitNextStepLines(result);
5918
6226
  for (const line of lines) {
@@ -5930,10 +6238,42 @@ async function run(flags) {
5930
6238
  if (!flags.quiet) {
5931
6239
  console.log();
5932
6240
  console.log(
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.")
6241
+ autoSyncInstalled() ? pc2.dim(" Background sync is on \u2014 your page updates automatically every 15 min (`npx whoburnedmore uninstall-sync` to stop).") : pc2.dim(" Re-run anytime to update your page.")
5934
6242
  );
5935
6243
  }
5936
6244
  }
6245
+ async function linkServerInstall(token) {
6246
+ if (!token) {
6247
+ throw new Error("missing install token \u2014 use `npx whoburnedmore link --token=<token>`");
6248
+ }
6249
+ const anonKey = ensureAnonKey();
6250
+ const linked = await redeemServerInstall(token, anonKey);
6251
+ console.log(
6252
+ linked.alreadyLinked ? ` This machine is already linked to @${linked.handle}.` : ` Linked this machine to @${linked.handle}.`
6253
+ );
6254
+ if (linked.mergedDays > 0) {
6255
+ console.log(pc2.dim(` Merged ${linked.mergedDays} existing usage day${linked.mergedDays === 1 ? "" : "s"} from this machine.`));
6256
+ }
6257
+ await run({
6258
+ dryRun: false,
6259
+ noSubmit: false,
6260
+ local: false,
6261
+ quiet: true
6262
+ });
6263
+ try {
6264
+ const action = reconcileAutoSync();
6265
+ if (action === "installed") {
6266
+ console.log(pc2.dim(" Background sync installed; this machine will refresh every 15 min."));
6267
+ } else if (action === "reinstalled") {
6268
+ console.log(pc2.dim(" Background sync repaired; this machine will refresh every 15 min."));
6269
+ } else {
6270
+ console.log(pc2.dim(" Background sync is already configured."));
6271
+ }
6272
+ } catch {
6273
+ console.log(pc2.dim(" Linked, but background sync could not be installed automatically. Run `npx whoburnedmore install-sync` to retry."));
6274
+ }
6275
+ console.log(` Profile: ${linked.profileUrl}`);
6276
+ }
5937
6277
  async function main() {
5938
6278
  const major = Number(process.versions.node.split(".")[0]);
5939
6279
  if (major < 20) {
@@ -5948,7 +6288,8 @@ async function main() {
5948
6288
  noSubmit: args.includes("--no-submit"),
5949
6289
  local: args.includes("--local"),
5950
6290
  quiet: command === "sync",
5951
- board: parseBoard(args)
6291
+ board: parseBoard(args),
6292
+ org: parseOrg(args)
5952
6293
  };
5953
6294
  switch (command) {
5954
6295
  case "run":
@@ -5960,6 +6301,9 @@ async function main() {
5960
6301
  await run({ ...flags, noSubmit: false, dryRun: false, local: false });
5961
6302
  break;
5962
6303
  }
6304
+ case "link":
6305
+ await linkServerInstall(parseInstallToken(args));
6306
+ break;
5963
6307
  case "status":
5964
6308
  case "doctor": {
5965
6309
  for (const line of agentStatusReport()) console.log(line);
@@ -6013,9 +6357,11 @@ function printHelp() {
6013
6357
  ${pc2.bold("usage")}
6014
6358
  npx whoburnedmore burn + land on the public leaderboard, open your dashboard
6015
6359
  npx whoburnedmore --board=CODE compare with friends \u2014 join their board (no sign-in)
6360
+ npx whoburnedmore --org=SLUG submit to your organization's board (companies/hackathons)
6016
6361
  npx whoburnedmore --local build the dashboard on your machine and open it (offline)
6017
6362
  npx whoburnedmore --dry-run print exactly what would be sent, send nothing
6018
6363
  npx whoburnedmore --no-submit collect locally, send nothing (no dashboard)
6364
+ npx whoburnedmore link --token=TOKEN link this server to your signed-in account
6019
6365
  npx whoburnedmore private hide your dashboard from the leaderboard
6020
6366
  npx whoburnedmore public put it back on the leaderboard
6021
6367
  npx whoburnedmore remove delete your dashboard and its data
@@ -6024,7 +6370,7 @@ function printHelp() {
6024
6370
  npx whoburnedmore install-sync turn it back on after uninstalling
6025
6371
 
6026
6372
  Background sync is on by default: after your first run, your page refreshes
6027
- automatically every hour (\`uninstall-sync\` to stop). Your dashboard is public on
6373
+ automatically every 15 min (\`uninstall-sync\` to stop). Your dashboard is public on
6028
6374
  the leaderboard as an anonymous burner \u2014 sign in on whoburnedmore.com to claim
6029
6375
  it (handle + X) and own your rank, or run \`private\`/\`remove\` to pull it. Only
6030
6376
  daily aggregate numbers (date, tool, model, token counts, est. cost) ever leave
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whoburnedmore",
3
- "version": "0.8.5",
3
+ "version": "0.8.9",
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": {
@@ -13,7 +13,8 @@
13
13
  "build": "rm -rf dist && esbuild src/index.ts --bundle --platform=node --format=esm --target=node20 --outfile=dist/index.js --external:ccusage --external:picocolors --external:tokscale",
14
14
  "test": "vitest run",
15
15
  "lint": "tsc -p tsconfig.json --noEmit",
16
- "prepublishOnly": "pnpm run build",
16
+ "smoke:package": "node scripts/smoke-package.mjs",
17
+ "prepublishOnly": "pnpm run lint && pnpm run test && pnpm run build && pnpm run smoke:package",
17
18
  "release:patch": "npm version patch && npm publish",
18
19
  "release:minor": "npm version minor && npm publish",
19
20
  "release:major": "npm version major && npm publish"