whoburnedmore 0.8.7 → 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 +387 -44
  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 {};
@@ -103,6 +141,15 @@ async function anonRemove(anonKey) {
103
141
  throw new Error(b.error ?? `failed (HTTP ${res.status})`);
104
142
  }
105
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
+ }
106
153
 
107
154
  // src/autosync.ts
108
155
  import { spawnSync } from "node:child_process";
@@ -116,8 +163,7 @@ import {
116
163
  writeFileSync as writeFileSync2
117
164
  } from "node:fs";
118
165
  import { homedir as homedir2, platform } from "node:os";
119
- import { join as join2 } from "node:path";
120
- import { fileURLToPath } from "node:url";
166
+ import { dirname, join as join2, win32 } from "node:path";
121
167
 
122
168
  // src/config.ts
123
169
  import { randomBytes } from "node:crypto";
@@ -131,6 +177,8 @@ import {
131
177
  import { homedir } from "node:os";
132
178
  import { join } from "node:path";
133
179
  function defaultConfigDir() {
180
+ const override = process.env.WHOBURNEDMORE_CONFIG_DIR?.trim();
181
+ if (override) return override;
134
182
  return join(homedir(), ".config", "whoburnedmore");
135
183
  }
136
184
  function loadConfig(dir = defaultConfigDir()) {
@@ -142,6 +190,9 @@ function loadConfig(dir = defaultConfigDir()) {
142
190
  if (typeof parsed.anonKey === "string") config.anonKey = parsed.anonKey;
143
191
  if (typeof parsed.lastSyncAt === "number" && Number.isFinite(parsed.lastSyncAt))
144
192
  config.lastSyncAt = parsed.lastSyncAt;
193
+ if (typeof parsed.launchNotificationDeliveredAt === "number" && Number.isFinite(parsed.launchNotificationDeliveredAt)) {
194
+ config.launchNotificationDeliveredAt = parsed.launchNotificationDeliveredAt;
195
+ }
145
196
  return Object.keys(config).length > 0 ? config : null;
146
197
  } catch {
147
198
  return null;
@@ -167,19 +218,33 @@ function recordSync(dir = defaultConfigDir(), when = Date.now()) {
167
218
  const config = loadConfig(dir) ?? {};
168
219
  saveConfig(dir, { ...config, lastSyncAt: when });
169
220
  }
221
+ function recordLaunchNotificationDelivered(dir = defaultConfigDir(), when = Date.now()) {
222
+ const config = loadConfig(dir) ?? {};
223
+ saveConfig(dir, { ...config, launchNotificationDeliveredAt: when });
224
+ }
170
225
 
171
226
  // src/autosync.ts
172
- 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
+ }
173
231
  var LABEL = "com.whoburnedmore.sync";
174
232
  var STABLE_NODE_CANDIDATES = [
175
233
  "/opt/homebrew/bin/node",
176
234
  "/usr/local/bin/node",
177
235
  "/usr/bin/node"
178
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";
179
243
  function syncLogPath() {
180
244
  return join2(defaultConfigDir(), "sync.log");
181
245
  }
182
- 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");
183
248
  return `<?xml version="1.0" encoding="UTF-8"?>
184
249
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
185
250
  <plist version="1.0">
@@ -188,12 +253,10 @@ function buildLaunchdPlist(nodePath, scriptPath, logPath = syncLogPath()) {
188
253
  <string>${LABEL}</string>
189
254
  <key>ProgramArguments</key>
190
255
  <array>
191
- <string>${nodePath}</string>
192
- <string>${scriptPath}</string>
193
- <string>sync</string>
256
+ ${programArguments}
194
257
  </array>
195
258
  <key>StartInterval</key>
196
- <integer>${SYNC_INTERVAL_HOURS * 3600}</integer>
259
+ <integer>${SYNC_INTERVAL_MINUTES * 60}</integer>
197
260
  <!-- Run once right after login/reboot so a machine that was off (or asleep)
198
261
  through a scheduled tick catches up immediately, then keeps to the
199
262
  interval. Submits are idempotent server-side, so an extra run is safe. -->
@@ -203,9 +266,9 @@ function buildLaunchdPlist(nodePath, scriptPath, logPath = syncLogPath()) {
203
266
  <key>ProcessType</key>
204
267
  <string>Background</string>
205
268
  <key>StandardOutPath</key>
206
- <string>${logPath}</string>
269
+ <string>${xmlEscape(logPath)}</string>
207
270
  <key>StandardErrorPath</key>
208
- <string>${logPath}</string>
271
+ <string>${xmlEscape(logPath)}</string>
209
272
  </dict>
210
273
  </plist>
211
274
  `;
@@ -213,9 +276,6 @@ function buildLaunchdPlist(nodePath, scriptPath, logPath = syncLogPath()) {
213
276
  function launchAgentPath() {
214
277
  return join2(homedir2(), "Library", "LaunchAgents", `${LABEL}.plist`);
215
278
  }
216
- function cliScriptPath() {
217
- return fileURLToPath(new URL("./index.js", import.meta.url));
218
- }
219
279
  function isUsableNode(p) {
220
280
  if (!existsSync2(p)) return false;
221
281
  const res = spawnSync(p, ["-v"], { encoding: "utf8" });
@@ -232,8 +292,51 @@ function resolveNodePath(opts) {
232
292
  }
233
293
  return execPath;
234
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
+ }
235
338
  function expectedDarwinPlist() {
236
- return buildLaunchdPlist(resolveNodePath(), cliScriptPath());
339
+ return buildLaunchdPlist(syncCommandArgs());
237
340
  }
238
341
  function plistDrift(installed, expected) {
239
342
  if (installed === null) return "absent";
@@ -251,7 +354,7 @@ function installAutoSync() {
251
354
  writeFileSync2(plistPath, expectedDarwinPlist());
252
355
  spawnSync("launchctl", ["unload", plistPath], { stdio: "ignore" });
253
356
  spawnSync("launchctl", ["load", plistPath], { stdio: "ignore" });
254
- return `launchd agent installed (${plistPath}), syncing every ${SYNC_INTERVAL_HOURS}h`;
357
+ return `launchd agent installed (${plistPath}), syncing every ${syncIntervalLabel()}`;
255
358
  }
256
359
  if (os === "linux") {
257
360
  const line = expectedLinuxCronLine();
@@ -263,28 +366,32 @@ ${line}
263
366
  `.replace(/^\n+/, "");
264
367
  const res = spawnSync("crontab", ["-"], { input: next });
265
368
  if (res.status !== 0) throw new Error("could not install crontab entry");
266
- return `cron entry installed, syncing every ${SYNC_INTERVAL_HOURS}h`;
369
+ return `cron entry installed, syncing every ${syncIntervalLabel()}`;
267
370
  }
268
371
  if (os === "win32") {
269
372
  const res = spawnSync("schtasks", [
270
373
  "/Create",
271
374
  "/F",
272
375
  "/SC",
273
- "HOURLY",
376
+ "MINUTE",
274
377
  "/MO",
275
- String(SYNC_INTERVAL_HOURS),
378
+ String(SYNC_INTERVAL_MINUTES),
276
379
  "/TN",
277
380
  "whoburnedmore-sync",
278
381
  "/TR",
279
- `"${resolveNodePath()}" "${cliScriptPath()}" sync`
382
+ windowsCommandLine(syncCommandArgs())
280
383
  ]);
281
384
  if (res.status !== 0) throw new Error("could not create scheduled task");
282
- return `scheduled task installed, syncing every ${SYNC_INTERVAL_HOURS}h`;
385
+ return `scheduled task installed, syncing every ${syncIntervalLabel()}`;
283
386
  }
284
387
  throw new Error(`auto-sync is not supported on ${os}`);
285
388
  }
286
- function expectedLinuxCronLine() {
287
- 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`;
288
395
  }
289
396
  function uninstallAutoSync() {
290
397
  const os = platform();
@@ -363,6 +470,41 @@ function rotateLogIfLarge(path = syncLogPath(), capBytes = 256 * 1024) {
363
470
  return false;
364
471
  }
365
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
+ }
366
508
  function autoSyncLoaded() {
367
509
  if (platform() === "darwin") {
368
510
  const res = spawnSync("launchctl", ["list"], { encoding: "utf8" });
@@ -392,7 +534,7 @@ function printBanner() {
392
534
  // src/collect.ts
393
535
  import { execFile } from "node:child_process";
394
536
  import { createRequire as createRequire3 } from "node:module";
395
- import { dirname as dirname2, join as join6 } from "node:path";
537
+ import { dirname as dirname3, join as join6 } from "node:path";
396
538
  import { promisify } from "node:util";
397
539
 
398
540
  // src/attribution.ts
@@ -751,7 +893,7 @@ import { join as join5 } from "node:path";
751
893
  // src/tokscale.ts
752
894
  import { spawnSync as spawnSync2 } from "node:child_process";
753
895
  import { createRequire } from "node:module";
754
- import { dirname, join as join4 } from "node:path";
896
+ import { dirname as dirname2, join as join4 } from "node:path";
755
897
  var LOOKBACK_DAYS = 30;
756
898
  function num(n) {
757
899
  const v = Math.round(Number(n));
@@ -795,7 +937,7 @@ function resolveTokscaleBin() {
795
937
  const pkg = require3("tokscale/package.json");
796
938
  const rel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.tokscale ?? "";
797
939
  if (!rel) return null;
798
- const binPath = join4(dirname(pkgPath), rel);
940
+ const binPath = join4(dirname2(pkgPath), rel);
799
941
  if (/\.(c|m)?js$/.test(binPath)) {
800
942
  return { cmd: process.execPath, prefixArgs: [binPath] };
801
943
  }
@@ -1128,7 +1270,7 @@ function resolveCcusageBin() {
1128
1270
  const pkgPath = require3.resolve("ccusage/package.json");
1129
1271
  const pkg = require3("ccusage/package.json");
1130
1272
  const rel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.ccusage ?? "ccusage";
1131
- const binPath = join6(dirname2(pkgPath), rel);
1273
+ const binPath = join6(dirname3(pkgPath), rel);
1132
1274
  if (/\.(c|m)?js$/.test(binPath)) {
1133
1275
  return { cmd: process.execPath, prefixArgs: [binPath] };
1134
1276
  }
@@ -1152,6 +1294,13 @@ function dedupeDaily(entries) {
1152
1294
  }
1153
1295
  return [...byKey.values()];
1154
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
+ }
1155
1304
  function dedupeSessions(sessions) {
1156
1305
  const byId = /* @__PURE__ */ new Map();
1157
1306
  const total = (s) => s.inputTokens + s.outputTokens + s.cacheCreationTokens + s.cacheReadTokens;
@@ -1263,9 +1412,14 @@ async function collectAll(onProgress) {
1263
1412
  };
1264
1413
  });
1265
1414
  return {
1266
- entries: dedupeDaily(entries),
1267
- sessions: dedupedSessions,
1268
- 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),
1269
1423
  toolsFound,
1270
1424
  tools,
1271
1425
  skills,
@@ -1296,8 +1450,8 @@ function buildStatusReport(s) {
1296
1450
  " \u21B3 config is out of date \u2014 it will self-repair on your next run"
1297
1451
  );
1298
1452
  }
1299
- lines.push(` \u2022 Interval: every ${s.intervalHours}h`);
1300
- const staleAfterMs = s.intervalHours * 2 * 3600 * 1e3;
1453
+ lines.push(` \u2022 Interval: every ${syncIntervalLabel(s.intervalMinutes)}`);
1454
+ const staleAfterMs = s.intervalMinutes * 2 * 60 * 1e3;
1301
1455
  if (s.lastSyncAt === null) {
1302
1456
  lines.push(" \u2022 Last sync: never recorded");
1303
1457
  lines.push(" \u26A0 STALE: no successful sync recorded yet \u2014 run `npx whoburnedmore`");
@@ -1306,7 +1460,7 @@ function buildStatusReport(s) {
1306
1460
  lines.push(` \u2022 Last sync: ${ago(age)}`);
1307
1461
  if (age > staleAfterMs) {
1308
1462
  lines.push(
1309
- ` \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\`.`
1310
1464
  );
1311
1465
  } else {
1312
1466
  lines.push(" \u2713 Fresh \u2014 your dashboard is up to date.");
@@ -1328,7 +1482,7 @@ function agentStatusReport(now = Date.now()) {
1328
1482
  installed: autoSyncInstalled(),
1329
1483
  loaded: autoSyncLoaded(),
1330
1484
  drift: autoSyncDrift(),
1331
- intervalHours: SYNC_INTERVAL_HOURS,
1485
+ intervalMinutes: SYNC_INTERVAL_MINUTES,
1332
1486
  lastSyncAt: typeof cfg?.lastSyncAt === "number" ? cfg.lastSyncAt : null,
1333
1487
  now,
1334
1488
  nodePath,
@@ -5378,6 +5532,48 @@ var coerce = {
5378
5532
  };
5379
5533
  var NEVER = INVALID;
5380
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
+
5381
5577
  // ../shared/dist/index.js
5382
5578
  var DateString = external_exports.string().regex(/^\d{4}-\d{2}-\d{2}$/, "must be YYYY-MM-DD");
5383
5579
  var tokenCount = external_exports.number().int().nonnegative();
@@ -5493,7 +5689,9 @@ var SubmitPayload = external_exports.object({
5493
5689
  */
5494
5690
  attributionComplete: external_exports.boolean().optional(),
5495
5691
  /** Optional friends-board code (from `--board=<code>`): auto-join this board on submit. */
5496
- 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()
5497
5695
  });
5498
5696
  var AnonSubmitPayload = SubmitPayload.extend({
5499
5697
  /** Client-generated secret (hex). The server stores only its hash. */
@@ -5504,6 +5702,94 @@ function entryTotalTokens(e) {
5504
5702
  }
5505
5703
  var LeaderboardPeriod = external_exports.enum(["today", "7d", "30d", "all"]);
5506
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
+ });
5507
5793
 
5508
5794
  // src/output.ts
5509
5795
  function formatTokens(n) {
@@ -5815,6 +6101,7 @@ function startProgress() {
5815
6101
  };
5816
6102
  }
5817
6103
  function openBrowser(url) {
6104
+ if (!isOpenableUrl(url)) return;
5818
6105
  const os = platform3();
5819
6106
  const [cmd, args] = os === "darwin" ? ["open", [url]] : os === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
5820
6107
  spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
@@ -5880,7 +6167,7 @@ async function run(flags) {
5880
6167
  if (agent.messageCount > 0) payload.agent = agent;
5881
6168
  if (attributionComplete && (tools.length > 0 || skills.length > 0 || projects.length > 0))
5882
6169
  payload.attributionComplete = true;
5883
- if (flags.board) payload.board = flags.board;
6170
+ applyScope(payload, flags);
5884
6171
  if (flags.dryRun) {
5885
6172
  console.log(pc2.dim("\n --dry-run: this exact payload would be sent, nothing else:\n"));
5886
6173
  console.log(JSON.stringify(payload, null, 2));
@@ -5909,13 +6196,31 @@ async function run(flags) {
5909
6196
  recordSync();
5910
6197
  } catch {
5911
6198
  }
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;
5912
6209
  const target = result.boardUrl ? boardClaimUrl(result.boardUrl, result.slug, anonKey) : claimUrl(result.dashboardUrl, anonKey);
6210
+ const trusted = isTrustedWebUrl(baseUrl);
5913
6211
  if (!flags.quiet) {
5914
6212
  console.log(
5915
6213
  pc2.green(" \u2713 Synced securely.") + pc2.dim(" Only your daily totals left this machine \u2014 never your prompts, code, or file names.")
5916
6214
  );
5917
- console.log(pc2.dim(" Opening your dashboard in your browser\u2026"));
5918
- 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
+ }
5919
6224
  }
5920
6225
  const lines = submitNextStepLines(result);
5921
6226
  for (const line of lines) {
@@ -5933,10 +6238,42 @@ async function run(flags) {
5933
6238
  if (!flags.quiet) {
5934
6239
  console.log();
5935
6240
  console.log(
5936
- autoSyncInstalled() ? pc2.dim(" Background sync is on \u2014 your page updates automatically every hour (`npx whoburnedmore uninstall-sync` to stop).") : pc2.dim(" Re-run anytime to update your page.")
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.")
5937
6242
  );
5938
6243
  }
5939
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
+ }
5940
6277
  async function main() {
5941
6278
  const major = Number(process.versions.node.split(".")[0]);
5942
6279
  if (major < 20) {
@@ -5951,7 +6288,8 @@ async function main() {
5951
6288
  noSubmit: args.includes("--no-submit"),
5952
6289
  local: args.includes("--local"),
5953
6290
  quiet: command === "sync",
5954
- board: parseBoard(args)
6291
+ board: parseBoard(args),
6292
+ org: parseOrg(args)
5955
6293
  };
5956
6294
  switch (command) {
5957
6295
  case "run":
@@ -5963,6 +6301,9 @@ async function main() {
5963
6301
  await run({ ...flags, noSubmit: false, dryRun: false, local: false });
5964
6302
  break;
5965
6303
  }
6304
+ case "link":
6305
+ await linkServerInstall(parseInstallToken(args));
6306
+ break;
5966
6307
  case "status":
5967
6308
  case "doctor": {
5968
6309
  for (const line of agentStatusReport()) console.log(line);
@@ -6016,9 +6357,11 @@ function printHelp() {
6016
6357
  ${pc2.bold("usage")}
6017
6358
  npx whoburnedmore burn + land on the public leaderboard, open your dashboard
6018
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)
6019
6361
  npx whoburnedmore --local build the dashboard on your machine and open it (offline)
6020
6362
  npx whoburnedmore --dry-run print exactly what would be sent, send nothing
6021
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
6022
6365
  npx whoburnedmore private hide your dashboard from the leaderboard
6023
6366
  npx whoburnedmore public put it back on the leaderboard
6024
6367
  npx whoburnedmore remove delete your dashboard and its data
@@ -6027,7 +6370,7 @@ function printHelp() {
6027
6370
  npx whoburnedmore install-sync turn it back on after uninstalling
6028
6371
 
6029
6372
  Background sync is on by default: after your first run, your page refreshes
6030
- 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
6031
6374
  the leaderboard as an anonymous burner \u2014 sign in on whoburnedmore.com to claim
6032
6375
  it (handle + X) and own your rank, or run \`private\`/\`remove\` to pull it. Only
6033
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.7",
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"