whoburnedmore 0.8.7 → 0.9.1

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 +615 -64
  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,29 @@ 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 parsePass(args) {
25
+ return parseValueFlag(args, "--pass") ?? parseValueFlag(args, "--code");
26
+ }
27
+ function parseInstallToken(args) {
28
+ return parseValueFlag(args, "--token");
29
+ }
30
+ function applyScope(payload, flags) {
31
+ if (flags.board) payload.board = flags.board;
32
+ if (flags.org) payload.org = flags.org;
33
+ if (flags.org && flags.orgCode)
34
+ payload.orgCode = flags.orgCode;
35
+ return payload;
36
+ }
37
+ function parseValueFlag(args, name) {
38
+ const prefix = `${name}=`;
39
+ const eq = args.find((a) => a.startsWith(prefix));
40
+ if (eq) return eq.slice(prefix.length).trim() || void 0;
41
+ const i = args.indexOf(name);
22
42
  if (i !== -1 && args[i + 1] && !args[i + 1].startsWith("-")) {
23
43
  return args[i + 1].trim() || void 0;
24
44
  }
@@ -31,7 +51,16 @@ function resolveCommand(args) {
31
51
  if (args.includes("--version") || args.includes("-v") || args.includes("version")) {
32
52
  return "version";
33
53
  }
34
- return args.find((a) => !a.startsWith("-")) ?? "run";
54
+ const valueFlags = /* @__PURE__ */ new Set(["--board", "--org", "--token", "--pass", "--code"]);
55
+ for (let i = 0; i < args.length; i++) {
56
+ const a = args[i];
57
+ if (a.startsWith("-")) {
58
+ if (valueFlags.has(a)) i++;
59
+ continue;
60
+ }
61
+ return a;
62
+ }
63
+ return "run";
35
64
  }
36
65
 
37
66
  // src/api.ts
@@ -41,6 +70,20 @@ function apiBase() {
41
70
  function webBase() {
42
71
  return process.env.WHOBURNEDMORE_WEB ?? "https://whoburnedmore.com";
43
72
  }
73
+ function isTrustedWebUrl(url) {
74
+ let u;
75
+ let base;
76
+ try {
77
+ u = new URL(url);
78
+ base = new URL(webBase());
79
+ } catch {
80
+ return false;
81
+ }
82
+ return (u.protocol === "https:" || u.protocol === "http:") && u.protocol === base.protocol && u.host === base.host;
83
+ }
84
+ function isOpenableUrl(url) {
85
+ return /^(https?|file):\/\//.test(url);
86
+ }
44
87
  async function readJson(res) {
45
88
  const text = await res.text();
46
89
  if (!text) return {};
@@ -83,6 +126,11 @@ function claimUrl(dashboardUrl, anonKey) {
83
126
  function boardClaimUrl(boardUrl, slug, anonKey) {
84
127
  return `${boardUrl}#k=${encodeURIComponent(anonKey)}&u=${encodeURIComponent(slug)}`;
85
128
  }
129
+ function resolveOpenTarget(result, anonKey) {
130
+ const baseUrl = result.orgBoardUrl ?? result.boardUrl ?? result.dashboardUrl;
131
+ const target = result.orgBoardUrl ? boardClaimUrl(result.orgBoardUrl, result.slug, anonKey) : result.boardUrl ? boardClaimUrl(result.boardUrl, result.slug, anonKey) : claimUrl(result.dashboardUrl, anonKey);
132
+ return { baseUrl, target };
133
+ }
86
134
  async function anonVisibility(anonKey, listed) {
87
135
  const { status, body } = await post(
88
136
  "/v1/anon/visibility",
@@ -103,6 +151,15 @@ async function anonRemove(anonKey) {
103
151
  throw new Error(b.error ?? `failed (HTTP ${res.status})`);
104
152
  }
105
153
  }
154
+ async function redeemServerInstall(token, anonKey) {
155
+ const { status, body } = await post("/v1/server-install/redeem", { token, anonKey });
156
+ if (status !== 200) {
157
+ throw new Error(
158
+ body.error ?? `server install failed (HTTP ${status})`
159
+ );
160
+ }
161
+ return body;
162
+ }
106
163
 
107
164
  // src/autosync.ts
108
165
  import { spawnSync } from "node:child_process";
@@ -116,8 +173,7 @@ import {
116
173
  writeFileSync as writeFileSync2
117
174
  } from "node:fs";
118
175
  import { homedir as homedir2, platform } from "node:os";
119
- import { join as join2 } from "node:path";
120
- import { fileURLToPath } from "node:url";
176
+ import { dirname, join as join2, win32 } from "node:path";
121
177
 
122
178
  // src/config.ts
123
179
  import { randomBytes } from "node:crypto";
@@ -131,6 +187,8 @@ import {
131
187
  import { homedir } from "node:os";
132
188
  import { join } from "node:path";
133
189
  function defaultConfigDir() {
190
+ const override = process.env.WHOBURNEDMORE_CONFIG_DIR?.trim();
191
+ if (override) return override;
134
192
  return join(homedir(), ".config", "whoburnedmore");
135
193
  }
136
194
  function loadConfig(dir = defaultConfigDir()) {
@@ -142,6 +200,9 @@ function loadConfig(dir = defaultConfigDir()) {
142
200
  if (typeof parsed.anonKey === "string") config.anonKey = parsed.anonKey;
143
201
  if (typeof parsed.lastSyncAt === "number" && Number.isFinite(parsed.lastSyncAt))
144
202
  config.lastSyncAt = parsed.lastSyncAt;
203
+ if (typeof parsed.launchNotificationDeliveredAt === "number" && Number.isFinite(parsed.launchNotificationDeliveredAt)) {
204
+ config.launchNotificationDeliveredAt = parsed.launchNotificationDeliveredAt;
205
+ }
145
206
  return Object.keys(config).length > 0 ? config : null;
146
207
  } catch {
147
208
  return null;
@@ -167,19 +228,33 @@ function recordSync(dir = defaultConfigDir(), when = Date.now()) {
167
228
  const config = loadConfig(dir) ?? {};
168
229
  saveConfig(dir, { ...config, lastSyncAt: when });
169
230
  }
231
+ function recordLaunchNotificationDelivered(dir = defaultConfigDir(), when = Date.now()) {
232
+ const config = loadConfig(dir) ?? {};
233
+ saveConfig(dir, { ...config, launchNotificationDeliveredAt: when });
234
+ }
170
235
 
171
236
  // src/autosync.ts
172
- var SYNC_INTERVAL_HOURS = 1;
237
+ var SYNC_INTERVAL_MINUTES = 15;
238
+ function syncIntervalLabel(mins = SYNC_INTERVAL_MINUTES) {
239
+ return mins % 60 === 0 ? `${mins / 60}h` : `${mins}m`;
240
+ }
173
241
  var LABEL = "com.whoburnedmore.sync";
174
242
  var STABLE_NODE_CANDIDATES = [
175
243
  "/opt/homebrew/bin/node",
176
244
  "/usr/local/bin/node",
177
245
  "/usr/bin/node"
178
246
  ];
247
+ var STABLE_NPM_CANDIDATES = [
248
+ "/opt/homebrew/bin/npm",
249
+ "/usr/local/bin/npm",
250
+ "/usr/bin/npm"
251
+ ];
252
+ var LATEST_PACKAGE_SPEC = "whoburnedmore@latest";
179
253
  function syncLogPath() {
180
254
  return join2(defaultConfigDir(), "sync.log");
181
255
  }
182
- function buildLaunchdPlist(nodePath, scriptPath, logPath = syncLogPath()) {
256
+ function buildLaunchdPlist(commandArgs = syncCommandArgs(), logPath = syncLogPath()) {
257
+ const programArguments = commandArgs.map((arg) => ` <string>${xmlEscape(arg)}</string>`).join("\n");
183
258
  return `<?xml version="1.0" encoding="UTF-8"?>
184
259
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
185
260
  <plist version="1.0">
@@ -188,12 +263,10 @@ function buildLaunchdPlist(nodePath, scriptPath, logPath = syncLogPath()) {
188
263
  <string>${LABEL}</string>
189
264
  <key>ProgramArguments</key>
190
265
  <array>
191
- <string>${nodePath}</string>
192
- <string>${scriptPath}</string>
193
- <string>sync</string>
266
+ ${programArguments}
194
267
  </array>
195
268
  <key>StartInterval</key>
196
- <integer>${SYNC_INTERVAL_HOURS * 3600}</integer>
269
+ <integer>${SYNC_INTERVAL_MINUTES * 60}</integer>
197
270
  <!-- Run once right after login/reboot so a machine that was off (or asleep)
198
271
  through a scheduled tick catches up immediately, then keeps to the
199
272
  interval. Submits are idempotent server-side, so an extra run is safe. -->
@@ -203,9 +276,9 @@ function buildLaunchdPlist(nodePath, scriptPath, logPath = syncLogPath()) {
203
276
  <key>ProcessType</key>
204
277
  <string>Background</string>
205
278
  <key>StandardOutPath</key>
206
- <string>${logPath}</string>
279
+ <string>${xmlEscape(logPath)}</string>
207
280
  <key>StandardErrorPath</key>
208
- <string>${logPath}</string>
281
+ <string>${xmlEscape(logPath)}</string>
209
282
  </dict>
210
283
  </plist>
211
284
  `;
@@ -213,9 +286,6 @@ function buildLaunchdPlist(nodePath, scriptPath, logPath = syncLogPath()) {
213
286
  function launchAgentPath() {
214
287
  return join2(homedir2(), "Library", "LaunchAgents", `${LABEL}.plist`);
215
288
  }
216
- function cliScriptPath() {
217
- return fileURLToPath(new URL("./index.js", import.meta.url));
218
- }
219
289
  function isUsableNode(p) {
220
290
  if (!existsSync2(p)) return false;
221
291
  const res = spawnSync(p, ["-v"], { encoding: "utf8" });
@@ -232,8 +302,51 @@ function resolveNodePath(opts) {
232
302
  }
233
303
  return execPath;
234
304
  }
305
+ function isUsableNpm(p) {
306
+ if (!existsSync2(p)) return false;
307
+ const res = spawnSync(p, ["--version"], { encoding: "utf8" });
308
+ return res.status === 0;
309
+ }
310
+ function resolveNpmPath(opts) {
311
+ const candidates = opts?.candidates ?? STABLE_NPM_CANDIDATES;
312
+ const check = opts?.check ?? isUsableNpm;
313
+ const execPath = opts?.execPath ?? process.execPath;
314
+ const os = opts?.platform ?? platform();
315
+ for (const c of candidates) {
316
+ if (check(c)) return c;
317
+ }
318
+ const sibling = os === "win32" ? win32.join(win32.dirname(execPath), "npm.cmd") : join2(dirname(execPath), "npm");
319
+ if (check(sibling)) return sibling;
320
+ return os === "win32" ? "npm.cmd" : "npm";
321
+ }
322
+ function syncCommandArgs(npmPath = resolveNpmPath()) {
323
+ return [
324
+ npmPath,
325
+ "exec",
326
+ "--yes",
327
+ "--ignore-scripts",
328
+ "--package",
329
+ LATEST_PACKAGE_SPEC,
330
+ "--",
331
+ "whoburnedmore",
332
+ "sync"
333
+ ];
334
+ }
335
+ function xmlEscape(value) {
336
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;").replaceAll(">", "&gt;");
337
+ }
338
+ function shellQuote(value) {
339
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
340
+ }
341
+ function windowsQuote(value) {
342
+ const escaped = value.replace(/(\\*)"/g, '$1$1\\"').replace(/\\+$/g, (slashes) => `${slashes}${slashes}`);
343
+ return `"${escaped}"`;
344
+ }
345
+ function windowsCommandLine(args) {
346
+ return args.map(windowsQuote).join(" ");
347
+ }
235
348
  function expectedDarwinPlist() {
236
- return buildLaunchdPlist(resolveNodePath(), cliScriptPath());
349
+ return buildLaunchdPlist(syncCommandArgs());
237
350
  }
238
351
  function plistDrift(installed, expected) {
239
352
  if (installed === null) return "absent";
@@ -251,40 +364,125 @@ function installAutoSync() {
251
364
  writeFileSync2(plistPath, expectedDarwinPlist());
252
365
  spawnSync("launchctl", ["unload", plistPath], { stdio: "ignore" });
253
366
  spawnSync("launchctl", ["load", plistPath], { stdio: "ignore" });
254
- return `launchd agent installed (${plistPath}), syncing every ${SYNC_INTERVAL_HOURS}h`;
367
+ return `launchd agent installed (${plistPath}), syncing every ${syncIntervalLabel()}`;
255
368
  }
256
369
  if (os === "linux") {
257
- const line = expectedLinuxCronLine();
258
- const current = spawnSync("crontab", ["-l"], { encoding: "utf8" });
259
- const existing = current.status === 0 ? current.stdout : "";
260
- const kept = existing.split("\n").filter((l) => !l.includes("whoburnedmore")).join("\n");
261
- const next = `${kept.trimEnd()}
262
- ${line}
263
- `.replace(/^\n+/, "");
264
- const res = spawnSync("crontab", ["-"], { input: next });
265
- if (res.status !== 0) throw new Error("could not install crontab entry");
266
- return `cron entry installed, syncing every ${SYNC_INTERVAL_HOURS}h`;
370
+ const viaCron = tryInstallCron();
371
+ if (viaCron) return viaCron;
372
+ const viaSystemd = tryInstallSystemd();
373
+ if (viaSystemd) return viaSystemd;
374
+ throw new Error(
375
+ "could not install background sync: no usable crontab or systemd user timer. Run `whoburnedmore daemon` under your process manager (systemd service, Docker CMD, pm2 or nohup) to keep syncing."
376
+ );
267
377
  }
268
378
  if (os === "win32") {
269
379
  const res = spawnSync("schtasks", [
270
380
  "/Create",
271
381
  "/F",
272
382
  "/SC",
273
- "HOURLY",
383
+ "MINUTE",
274
384
  "/MO",
275
- String(SYNC_INTERVAL_HOURS),
385
+ String(SYNC_INTERVAL_MINUTES),
276
386
  "/TN",
277
387
  "whoburnedmore-sync",
278
388
  "/TR",
279
- `"${resolveNodePath()}" "${cliScriptPath()}" sync`
389
+ windowsCommandLine(syncCommandArgs())
280
390
  ]);
281
391
  if (res.status !== 0) throw new Error("could not create scheduled task");
282
- return `scheduled task installed, syncing every ${SYNC_INTERVAL_HOURS}h`;
392
+ return `scheduled task installed, syncing every ${syncIntervalLabel()}`;
283
393
  }
284
394
  throw new Error(`auto-sync is not supported on ${os}`);
285
395
  }
286
- function expectedLinuxCronLine() {
287
- return `0 */${SYNC_INTERVAL_HOURS} * * * "${resolveNodePath()}" "${cliScriptPath()}" sync >"${syncLogPath()}" 2>&1`;
396
+ function cronSchedule(mins = SYNC_INTERVAL_MINUTES) {
397
+ return mins % 60 === 0 ? `0 */${mins / 60} * * *` : `*/${mins} * * * *`;
398
+ }
399
+ function expectedLinuxCronLine(opts) {
400
+ const command = syncCommandArgs(opts?.npmPath).map(shellQuote).join(" ");
401
+ return `${cronSchedule()} ${command} >${shellQuote(opts?.logPath ?? syncLogPath())} 2>&1`;
402
+ }
403
+ var SYSTEMD_UNIT = "whoburnedmore-sync";
404
+ function systemdUserDir() {
405
+ const base = process.env.XDG_CONFIG_HOME?.trim() || join2(homedir2(), ".config");
406
+ return join2(base, "systemd", "user");
407
+ }
408
+ function systemdServicePath() {
409
+ return join2(systemdUserDir(), `${SYSTEMD_UNIT}.service`);
410
+ }
411
+ function systemdTimerPath() {
412
+ return join2(systemdUserDir(), `${SYSTEMD_UNIT}.timer`);
413
+ }
414
+ function systemdQuote(value) {
415
+ return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
416
+ }
417
+ function buildSystemdService(commandArgs = syncCommandArgs()) {
418
+ const execStart = commandArgs.map(systemdQuote).join(" ");
419
+ return `[Unit]
420
+ Description=whoburnedmore background token-usage sync
421
+
422
+ [Service]
423
+ Type=oneshot
424
+ ExecStart=${execStart}
425
+ `;
426
+ }
427
+ function buildSystemdTimer(mins = SYNC_INTERVAL_MINUTES) {
428
+ return `[Unit]
429
+ Description=whoburnedmore background token-usage sync timer
430
+
431
+ [Timer]
432
+ OnBootSec=1min
433
+ OnUnitActiveSec=${mins}min
434
+ Persistent=true
435
+
436
+ [Install]
437
+ WantedBy=timers.target
438
+ `;
439
+ }
440
+ function binaryExists(cmd, probeArgs = ["--version"]) {
441
+ const res = spawnSync(cmd, probeArgs, { stdio: "ignore" });
442
+ return !res.error;
443
+ }
444
+ function linuxSyncMechanism() {
445
+ const cron = spawnSync("crontab", ["-l"], { encoding: "utf8" });
446
+ if (!cron.error && cron.status === 0 && cron.stdout.includes("whoburnedmore")) {
447
+ return "cron";
448
+ }
449
+ if (existsSync2(systemdTimerPath())) return "systemd";
450
+ return "none";
451
+ }
452
+ function tryInstallCron() {
453
+ if (!binaryExists("crontab", ["-l"])) return null;
454
+ const line = expectedLinuxCronLine();
455
+ const current = spawnSync("crontab", ["-l"], { encoding: "utf8" });
456
+ const existing = current.status === 0 ? current.stdout : "";
457
+ const kept = existing.split("\n").filter((l) => !l.includes("whoburnedmore")).join("\n");
458
+ const next = `${kept.trimEnd()}
459
+ ${line}
460
+ `.replace(/^\n+/, "");
461
+ const res = spawnSync("crontab", ["-"], { input: next });
462
+ if (res.status !== 0) return null;
463
+ return `cron entry installed, syncing every ${syncIntervalLabel()}`;
464
+ }
465
+ function tryInstallSystemd() {
466
+ if (!binaryExists("systemctl", ["--user", "--version"])) return null;
467
+ try {
468
+ mkdirSync2(systemdUserDir(), { recursive: true });
469
+ writeFileSync2(systemdServicePath(), buildSystemdService());
470
+ writeFileSync2(systemdTimerPath(), buildSystemdTimer());
471
+ } catch {
472
+ return null;
473
+ }
474
+ spawnSync("systemctl", ["--user", "daemon-reload"], { stdio: "ignore" });
475
+ const res = spawnSync(
476
+ "systemctl",
477
+ ["--user", "enable", "--now", `${SYSTEMD_UNIT}.timer`],
478
+ { stdio: "ignore" }
479
+ );
480
+ if (res.status !== 0) {
481
+ rmSync(systemdServicePath(), { force: true });
482
+ rmSync(systemdTimerPath(), { force: true });
483
+ return null;
484
+ }
485
+ return `systemd user timer installed, syncing every ${syncIntervalLabel()} (run \`loginctl enable-linger\` to keep syncing while logged out)`;
288
486
  }
289
487
  function uninstallAutoSync() {
290
488
  const os = platform();
@@ -297,12 +495,25 @@ function uninstallAutoSync() {
297
495
  return "launchd agent removed";
298
496
  }
299
497
  if (os === "linux") {
498
+ let removed = false;
300
499
  const current = spawnSync("crontab", ["-l"], { encoding: "utf8" });
301
- if (current.status === 0 && current.stdout.includes("whoburnedmore")) {
500
+ if (!current.error && current.status === 0 && current.stdout.includes("whoburnedmore")) {
302
501
  const next = current.stdout.split("\n").filter((l) => !l.includes("whoburnedmore")).join("\n");
303
502
  spawnSync("crontab", ["-"], { input: next });
503
+ removed = true;
304
504
  }
305
- return "cron entry removed";
505
+ if (existsSync2(systemdTimerPath())) {
506
+ spawnSync(
507
+ "systemctl",
508
+ ["--user", "disable", "--now", `${SYSTEMD_UNIT}.timer`],
509
+ { stdio: "ignore" }
510
+ );
511
+ rmSync(systemdServicePath(), { force: true });
512
+ rmSync(systemdTimerPath(), { force: true });
513
+ spawnSync("systemctl", ["--user", "daemon-reload"], { stdio: "ignore" });
514
+ removed = true;
515
+ }
516
+ return removed ? "background sync removed" : "nothing to remove";
306
517
  }
307
518
  if (os === "win32") {
308
519
  spawnSync("schtasks", ["/Delete", "/F", "/TN", "whoburnedmore-sync"]);
@@ -313,8 +524,7 @@ function uninstallAutoSync() {
313
524
  function autoSyncInstalled() {
314
525
  if (platform() === "darwin") return existsSync2(launchAgentPath());
315
526
  if (platform() === "linux") {
316
- const current = spawnSync("crontab", ["-l"], { encoding: "utf8" });
317
- return current.status === 0 && current.stdout.includes("whoburnedmore");
527
+ return linuxSyncMechanism() !== "none";
318
528
  }
319
529
  if (platform() === "win32") {
320
530
  const res = spawnSync("schtasks", ["/Query", "/TN", "whoburnedmore-sync"], {
@@ -324,22 +534,40 @@ function autoSyncInstalled() {
324
534
  }
325
535
  return false;
326
536
  }
537
+ function readInstalledSystemd() {
538
+ try {
539
+ return `${readFileSync2(systemdServicePath(), "utf8")}
540
+ ${readFileSync2(systemdTimerPath(), "utf8")}`;
541
+ } catch {
542
+ return null;
543
+ }
544
+ }
545
+ function expectedSystemd() {
546
+ return `${buildSystemdService()}
547
+ ${buildSystemdTimer()}`;
548
+ }
327
549
  function readInstalledAgent() {
328
550
  if (platform() === "darwin") {
329
551
  const p = launchAgentPath();
330
552
  return existsSync2(p) ? readFileSync2(p, "utf8") : null;
331
553
  }
332
554
  if (platform() === "linux") {
333
- const current = spawnSync("crontab", ["-l"], { encoding: "utf8" });
334
- if (current.status !== 0) return null;
335
- const line = current.stdout.split("\n").find((l) => l.includes("whoburnedmore"));
336
- return line ?? null;
555
+ const mech = linuxSyncMechanism();
556
+ if (mech === "cron") {
557
+ const current = spawnSync("crontab", ["-l"], { encoding: "utf8" });
558
+ if (current.error || current.status !== 0) return null;
559
+ return current.stdout.split("\n").find((l) => l.includes("whoburnedmore")) ?? null;
560
+ }
561
+ if (mech === "systemd") return readInstalledSystemd();
562
+ return null;
337
563
  }
338
564
  return null;
339
565
  }
340
566
  function expectedAgent() {
341
567
  if (platform() === "darwin") return expectedDarwinPlist();
342
- if (platform() === "linux") return expectedLinuxCronLine();
568
+ if (platform() === "linux") {
569
+ return linuxSyncMechanism() === "systemd" ? expectedSystemd() : expectedLinuxCronLine();
570
+ }
343
571
  return null;
344
572
  }
345
573
  function autoSyncDrift() {
@@ -363,6 +591,41 @@ function rotateLogIfLarge(path = syncLogPath(), capBytes = 256 * 1024) {
363
591
  return false;
364
592
  }
365
593
  }
594
+ function notifyLaunchLive(opts) {
595
+ const os = opts?.platform ?? platform();
596
+ const run2 = opts?.spawn ?? ((cmd, args) => spawnSync(cmd, args, { stdio: "ignore" }));
597
+ const title = "whoburnedmore is live";
598
+ const message = "Your dashboard is ready. Go to whoburnedmore.com";
599
+ if (os === "darwin") {
600
+ return run2("osascript", [
601
+ "-e",
602
+ `display notification "${message}" with title "${title}"`
603
+ ]).status === 0;
604
+ }
605
+ if (os === "linux") {
606
+ return run2("notify-send", [title, message]).status === 0;
607
+ }
608
+ if (os === "win32") {
609
+ const psQuote = (value) => `'${value.replaceAll("'", "''")}'`;
610
+ const script = [
611
+ "Add-Type -AssemblyName System.Windows.Forms",
612
+ "$n = New-Object System.Windows.Forms.NotifyIcon",
613
+ "$n.Icon = [System.Drawing.SystemIcons]::Application",
614
+ `$n.BalloonTipTitle = ${psQuote(title)}`,
615
+ `$n.BalloonTipText = ${psQuote(message)}`,
616
+ "$n.Visible = $true",
617
+ "$n.ShowBalloonTip(10000)",
618
+ "Start-Sleep -Seconds 2",
619
+ "$n.Dispose()"
620
+ ].join("; ");
621
+ return run2("powershell", [
622
+ "-NoProfile",
623
+ "-Command",
624
+ script
625
+ ]).status === 0;
626
+ }
627
+ return false;
628
+ }
366
629
  function autoSyncLoaded() {
367
630
  if (platform() === "darwin") {
368
631
  const res = spawnSync("launchctl", ["list"], { encoding: "utf8" });
@@ -389,10 +652,28 @@ function printBanner() {
389
652
  console.log();
390
653
  }
391
654
 
655
+ // src/daemon.ts
656
+ async function daemonLoop(deps) {
657
+ let cycles = 0;
658
+ while (!deps.isStopped()) {
659
+ cycles++;
660
+ try {
661
+ await deps.runOnce();
662
+ deps.log("synced");
663
+ } catch (err) {
664
+ const message = err instanceof Error ? err.message : String(err);
665
+ deps.log(`sync failed: ${message} \u2014 retrying next cycle`);
666
+ }
667
+ if (deps.isStopped()) break;
668
+ await deps.wait(deps.intervalMs);
669
+ }
670
+ return cycles;
671
+ }
672
+
392
673
  // src/collect.ts
393
674
  import { execFile } from "node:child_process";
394
675
  import { createRequire as createRequire3 } from "node:module";
395
- import { dirname as dirname2, join as join6 } from "node:path";
676
+ import { dirname as dirname3, join as join6 } from "node:path";
396
677
  import { promisify } from "node:util";
397
678
 
398
679
  // src/attribution.ts
@@ -751,7 +1032,7 @@ import { join as join5 } from "node:path";
751
1032
  // src/tokscale.ts
752
1033
  import { spawnSync as spawnSync2 } from "node:child_process";
753
1034
  import { createRequire } from "node:module";
754
- import { dirname, join as join4 } from "node:path";
1035
+ import { dirname as dirname2, join as join4 } from "node:path";
755
1036
  var LOOKBACK_DAYS = 30;
756
1037
  function num(n) {
757
1038
  const v = Math.round(Number(n));
@@ -795,7 +1076,7 @@ function resolveTokscaleBin() {
795
1076
  const pkg = require3("tokscale/package.json");
796
1077
  const rel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.tokscale ?? "";
797
1078
  if (!rel) return null;
798
- const binPath = join4(dirname(pkgPath), rel);
1079
+ const binPath = join4(dirname2(pkgPath), rel);
799
1080
  if (/\.(c|m)?js$/.test(binPath)) {
800
1081
  return { cmd: process.execPath, prefixArgs: [binPath] };
801
1082
  }
@@ -1128,7 +1409,7 @@ function resolveCcusageBin() {
1128
1409
  const pkgPath = require3.resolve("ccusage/package.json");
1129
1410
  const pkg = require3("ccusage/package.json");
1130
1411
  const rel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.ccusage ?? "ccusage";
1131
- const binPath = join6(dirname2(pkgPath), rel);
1412
+ const binPath = join6(dirname3(pkgPath), rel);
1132
1413
  if (/\.(c|m)?js$/.test(binPath)) {
1133
1414
  return { cmd: process.execPath, prefixArgs: [binPath] };
1134
1415
  }
@@ -1152,6 +1433,13 @@ function dedupeDaily(entries) {
1152
1433
  }
1153
1434
  return [...byKey.values()];
1154
1435
  }
1436
+ function entryTokens(e) {
1437
+ return e.inputTokens + e.outputTokens + e.cacheCreationTokens + e.cacheReadTokens;
1438
+ }
1439
+ function capByTokens(rows, max, tokens) {
1440
+ if (rows.length <= max) return rows;
1441
+ return [...rows].sort((a, b) => tokens(b) - tokens(a)).slice(0, max);
1442
+ }
1155
1443
  function dedupeSessions(sessions) {
1156
1444
  const byId = /* @__PURE__ */ new Map();
1157
1445
  const total = (s) => s.inputTokens + s.outputTokens + s.cacheCreationTokens + s.cacheReadTokens;
@@ -1263,9 +1551,14 @@ async function collectAll(onProgress) {
1263
1551
  };
1264
1552
  });
1265
1553
  return {
1266
- entries: dedupeDaily(entries),
1267
- sessions: dedupedSessions,
1268
- blocks: dedupeBlocks(blocks),
1554
+ // Cap each array to the server's accepted maximum (the shared SubmitPayload
1555
+ // schema: entries ≤ 20000, sessions/blocks ≤ 10000), keeping the
1556
+ // highest-token rows. Without this, a power user with >10000 distinct sessions
1557
+ // would have their ENTIRE submit rejected with a 400 instead of a capped one.
1558
+ // tools/skills/projects are already bounded upstream (attribution caps).
1559
+ entries: capByTokens(dedupeDaily(entries), 2e4, entryTokens),
1560
+ sessions: capByTokens(dedupedSessions, 1e4, entryTokens),
1561
+ blocks: capByTokens(dedupeBlocks(blocks), 1e4, (b) => b.totalTokens),
1269
1562
  toolsFound,
1270
1563
  tools,
1271
1564
  skills,
@@ -1296,8 +1589,8 @@ function buildStatusReport(s) {
1296
1589
  " \u21B3 config is out of date \u2014 it will self-repair on your next run"
1297
1590
  );
1298
1591
  }
1299
- lines.push(` \u2022 Interval: every ${s.intervalHours}h`);
1300
- const staleAfterMs = s.intervalHours * 2 * 3600 * 1e3;
1592
+ lines.push(` \u2022 Interval: every ${syncIntervalLabel(s.intervalMinutes)}`);
1593
+ const staleAfterMs = s.intervalMinutes * 2 * 60 * 1e3;
1301
1594
  if (s.lastSyncAt === null) {
1302
1595
  lines.push(" \u2022 Last sync: never recorded");
1303
1596
  lines.push(" \u26A0 STALE: no successful sync recorded yet \u2014 run `npx whoburnedmore`");
@@ -1306,7 +1599,7 @@ function buildStatusReport(s) {
1306
1599
  lines.push(` \u2022 Last sync: ${ago(age)}`);
1307
1600
  if (age > staleAfterMs) {
1308
1601
  lines.push(
1309
- ` \u26A0 STALE: last sync was over ${s.intervalHours * 2}h ago \u2014 your dashboard may be behind. Run \`npx whoburnedmore\`.`
1602
+ ` \u26A0 STALE: last sync was over ${syncIntervalLabel(s.intervalMinutes * 2)} ago \u2014 your dashboard may be behind. Run \`npx whoburnedmore\`.`
1310
1603
  );
1311
1604
  } else {
1312
1605
  lines.push(" \u2713 Fresh \u2014 your dashboard is up to date.");
@@ -1328,7 +1621,7 @@ function agentStatusReport(now = Date.now()) {
1328
1621
  installed: autoSyncInstalled(),
1329
1622
  loaded: autoSyncLoaded(),
1330
1623
  drift: autoSyncDrift(),
1331
- intervalHours: SYNC_INTERVAL_HOURS,
1624
+ intervalMinutes: SYNC_INTERVAL_MINUTES,
1332
1625
  lastSyncAt: typeof cfg?.lastSyncAt === "number" ? cfg.lastSyncAt : null,
1333
1626
  now,
1334
1627
  nodePath,
@@ -5378,6 +5671,48 @@ var coerce = {
5378
5671
  };
5379
5672
  var NEVER = INVALID;
5380
5673
 
5674
+ // ../shared/dist/tenant.js
5675
+ var RESERVED_SUBDOMAINS = /* @__PURE__ */ new Set([
5676
+ "www",
5677
+ "api",
5678
+ "app",
5679
+ "admin",
5680
+ "mail",
5681
+ "email",
5682
+ "cdn",
5683
+ "static",
5684
+ "assets",
5685
+ "blog",
5686
+ "docs",
5687
+ "status",
5688
+ "help",
5689
+ "support",
5690
+ "ingest",
5691
+ "vercel",
5692
+ "preview",
5693
+ "staging",
5694
+ "dev",
5695
+ "test"
5696
+ ]);
5697
+
5698
+ // ../shared/dist/launch-gate.js
5699
+ var LaunchAccessMode = external_exports.enum(["full", "invited", "waitlisted"]);
5700
+ var LaunchStatusResponse = external_exports.object({
5701
+ mode: LaunchAccessMode,
5702
+ launchAt: external_exports.string().datetime(),
5703
+ now: external_exports.string().datetime(),
5704
+ remainingSeconds: external_exports.number().int().nonnegative(),
5705
+ live: external_exports.boolean()
5706
+ });
5707
+ var LaunchRedeemRequest = external_exports.object({
5708
+ code: external_exports.string().trim().min(2).max(64)
5709
+ });
5710
+ var LaunchRedeemResponse = external_exports.object({
5711
+ ok: external_exports.literal(true),
5712
+ mode: LaunchAccessMode,
5713
+ expiresAt: external_exports.string().datetime().nullable()
5714
+ });
5715
+
5381
5716
  // ../shared/dist/index.js
5382
5717
  var DateString = external_exports.string().regex(/^\d{4}-\d{2}-\d{2}$/, "must be YYYY-MM-DD");
5383
5718
  var tokenCount = external_exports.number().int().nonnegative();
@@ -5493,7 +5828,16 @@ var SubmitPayload = external_exports.object({
5493
5828
  */
5494
5829
  attributionComplete: external_exports.boolean().optional(),
5495
5830
  /** Optional friends-board code (from `--board=<code>`): auto-join this board on submit. */
5496
- board: external_exports.string().min(1).max(32).optional()
5831
+ board: external_exports.string().min(1).max(32).optional(),
5832
+ /** Optional organization slug (from `--org=<slug>`): auto-join this org on submit. */
5833
+ org: external_exports.string().min(2).max(32).optional(),
5834
+ /**
5835
+ * Optional org join password (from `--pass=<code>` / `--code=<code>`): required
5836
+ * to attach a CLI run to an `org`. Back-compat: omittable — a run with no org
5837
+ * never needs it, and a wrong/missing code only skips the org attach (the
5838
+ * personal submit still succeeds).
5839
+ */
5840
+ orgCode: external_exports.string().min(1).max(64).optional()
5497
5841
  });
5498
5842
  var AnonSubmitPayload = SubmitPayload.extend({
5499
5843
  /** Client-generated secret (hex). The server stores only its hash. */
@@ -5504,6 +5848,96 @@ function entryTotalTokens(e) {
5504
5848
  }
5505
5849
  var LeaderboardPeriod = external_exports.enum(["today", "7d", "30d", "all"]);
5506
5850
  var LeaderboardMetric = external_exports.enum(["tokens", "cost"]);
5851
+ var OrgType = external_exports.enum(["company", "hackathon", "hackerhouse"]);
5852
+ var MemberRole = external_exports.enum(["owner", "admin", "member"]);
5853
+ var OrgBoardVisibility = external_exports.enum(["public", "members"]);
5854
+ var HexColor = external_exports.string().regex(/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/, "must be a hex color like #f97316");
5855
+ var RESERVED_SLUGS = /* @__PURE__ */ new Set([
5856
+ ...RESERVED_SUBDOMAINS,
5857
+ "o",
5858
+ "d",
5859
+ "u",
5860
+ "boards",
5861
+ "board",
5862
+ "claim",
5863
+ "signin",
5864
+ "signout",
5865
+ "login",
5866
+ "logout",
5867
+ "dashboard",
5868
+ "install",
5869
+ "for-teams",
5870
+ "teams",
5871
+ "about",
5872
+ "contact",
5873
+ "trust",
5874
+ "cli",
5875
+ "feedback",
5876
+ "guides",
5877
+ "guide",
5878
+ "join",
5879
+ "leaderboard",
5880
+ "settings",
5881
+ "account",
5882
+ "me",
5883
+ "new",
5884
+ "robots",
5885
+ "sitemap"
5886
+ ]);
5887
+ var SLUG_RE = /^[a-z0-9](?:[a-z0-9-]{0,30}[a-z0-9])?$/;
5888
+ function isValidSlug(slug) {
5889
+ return typeof slug === "string" && slug.length >= 2 && slug.length <= 32 && SLUG_RE.test(slug) && // Reject consecutive hyphens — the regex above permits "a--b", which makes
5890
+ // for confusing subdomains and is never a deliberate slug.
5891
+ !slug.includes("--") && !RESERVED_SLUGS.has(slug);
5892
+ }
5893
+ var OrgSlug = external_exports.string().min(2).max(32).refine(isValidSlug, "invalid or reserved slug");
5894
+ var OrgWindow = external_exports.object({
5895
+ startDate: DateString.nullable().optional(),
5896
+ endDate: DateString.nullable().optional()
5897
+ }).refine((w) => !(w.startDate && w.endDate) || w.endDate >= w.startDate, { message: "endDate must be on or after startDate", path: ["endDate"] });
5898
+ var OrgJoinPolicy = external_exports.object({
5899
+ allowCodeJoin: external_exports.boolean().default(true),
5900
+ allowDomainJoin: external_exports.boolean().default(false),
5901
+ /** Verified email domains that auto-join, e.g. ["acme.com"]. */
5902
+ emailDomains: external_exports.array(external_exports.string().min(3).max(253).toLowerCase()).max(20).default([])
5903
+ });
5904
+ var OrgApplicationInput = external_exports.object({
5905
+ type: OrgType,
5906
+ orgName: external_exports.string().min(1).max(120),
5907
+ desiredSlug: external_exports.string().min(2).max(32).optional(),
5908
+ contactName: external_exports.string().min(1).max(120),
5909
+ contactEmail: external_exports.string().email().max(254),
5910
+ website: external_exports.string().url().max(300).optional(),
5911
+ /** Rough headcount / attendee estimate, free text. */
5912
+ size: external_exports.string().max(60).optional(),
5913
+ message: external_exports.string().max(2e3).optional()
5914
+ });
5915
+ var OrgProvisionInput = external_exports.object({
5916
+ /** When provisioning straight from an application. */
5917
+ applicationId: external_exports.string().min(1).max(64).optional(),
5918
+ slug: OrgSlug,
5919
+ name: external_exports.string().min(1).max(120),
5920
+ type: OrgType,
5921
+ /** Handle (or email) of the user who becomes Owner. */
5922
+ ownerHandle: external_exports.string().min(1).max(120).optional(),
5923
+ ownerEmail: external_exports.string().email().max(254).optional(),
5924
+ description: external_exports.string().max(2e3).optional(),
5925
+ boardVisibility: OrgBoardVisibility.optional(),
5926
+ window: OrgWindow.optional()
5927
+ });
5928
+ var OrgSettingsInput = external_exports.object({
5929
+ name: external_exports.string().min(1).max(120).optional(),
5930
+ description: external_exports.string().max(2e3).nullable().optional(),
5931
+ accentColor: HexColor.optional(),
5932
+ logoUrl: external_exports.string().url().max(500).nullable().optional(),
5933
+ boardVisibility: OrgBoardVisibility.optional(),
5934
+ window: OrgWindow.optional(),
5935
+ joinPolicy: OrgJoinPolicy.partial().optional()
5936
+ });
5937
+ var OrgJoinInput = external_exports.object({
5938
+ /** Required only when joining via a code; domain/admin joins omit it. */
5939
+ code: external_exports.string().min(1).max(64).optional()
5940
+ });
5507
5941
 
5508
5942
  // src/output.ts
5509
5943
  function formatTokens(n) {
@@ -5815,6 +6249,7 @@ function startProgress() {
5815
6249
  };
5816
6250
  }
5817
6251
  function openBrowser(url) {
6252
+ if (!isOpenableUrl(url)) return;
5818
6253
  const os = platform3();
5819
6254
  const [cmd, args] = os === "darwin" ? ["open", [url]] : os === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
5820
6255
  spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
@@ -5880,7 +6315,7 @@ async function run(flags) {
5880
6315
  if (agent.messageCount > 0) payload.agent = agent;
5881
6316
  if (attributionComplete && (tools.length > 0 || skills.length > 0 || projects.length > 0))
5882
6317
  payload.attributionComplete = true;
5883
- if (flags.board) payload.board = flags.board;
6318
+ applyScope(payload, flags);
5884
6319
  if (flags.dryRun) {
5885
6320
  console.log(pc2.dim("\n --dry-run: this exact payload would be sent, nothing else:\n"));
5886
6321
  console.log(JSON.stringify(payload, null, 2));
@@ -5909,13 +6344,30 @@ async function run(flags) {
5909
6344
  recordSync();
5910
6345
  } catch {
5911
6346
  }
5912
- const target = result.boardUrl ? boardClaimUrl(result.boardUrl, result.slug, anonKey) : claimUrl(result.dashboardUrl, anonKey);
6347
+ try {
6348
+ const config = loadConfig();
6349
+ if (result.launch?.live && !config?.launchNotificationDeliveredAt) {
6350
+ if (notifyLaunchLive()) {
6351
+ recordLaunchNotificationDelivered();
6352
+ }
6353
+ }
6354
+ } catch {
6355
+ }
6356
+ const { baseUrl, target } = resolveOpenTarget(result, anonKey);
6357
+ const trusted = isTrustedWebUrl(baseUrl);
5913
6358
  if (!flags.quiet) {
5914
6359
  console.log(
5915
6360
  pc2.green(" \u2713 Synced securely.") + pc2.dim(" Only your daily totals left this machine \u2014 never your prompts, code, or file names.")
5916
6361
  );
5917
- console.log(pc2.dim(" Opening your dashboard in your browser\u2026"));
5918
- openBrowser(target);
6362
+ if (trusted) {
6363
+ console.log(pc2.dim(" Opening your dashboard in your browser\u2026"));
6364
+ openBrowser(target);
6365
+ } else {
6366
+ console.log(
6367
+ pc2.dim(" The server returned an unexpected dashboard address, so it was NOT auto-opened. Open it yourself only if you trust it:")
6368
+ );
6369
+ console.log(` ${baseUrl}`);
6370
+ }
5919
6371
  }
5920
6372
  const lines = submitNextStepLines(result);
5921
6373
  for (const line of lines) {
@@ -5933,10 +6385,89 @@ async function run(flags) {
5933
6385
  if (!flags.quiet) {
5934
6386
  console.log();
5935
6387
  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.")
6388
+ 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
6389
  );
5938
6390
  }
5939
6391
  }
6392
+ async function linkServerInstall(token) {
6393
+ if (!token) {
6394
+ throw new Error("missing install token \u2014 use `npx whoburnedmore link --token=<token>`");
6395
+ }
6396
+ const anonKey = ensureAnonKey();
6397
+ const linked = await redeemServerInstall(token, anonKey);
6398
+ console.log(
6399
+ linked.alreadyLinked ? ` This machine is already linked to @${linked.handle}.` : ` Linked this machine to @${linked.handle}.`
6400
+ );
6401
+ if (linked.mergedDays > 0) {
6402
+ console.log(pc2.dim(` Merged ${linked.mergedDays} existing usage day${linked.mergedDays === 1 ? "" : "s"} from this machine.`));
6403
+ }
6404
+ await run({
6405
+ dryRun: false,
6406
+ noSubmit: false,
6407
+ local: false,
6408
+ quiet: true
6409
+ });
6410
+ try {
6411
+ const action = reconcileAutoSync();
6412
+ if (action === "installed") {
6413
+ console.log(pc2.dim(" Background sync installed; this machine will refresh every 15 min."));
6414
+ } else if (action === "reinstalled") {
6415
+ console.log(pc2.dim(" Background sync repaired; this machine will refresh every 15 min."));
6416
+ } else {
6417
+ console.log(pc2.dim(" Background sync is already configured."));
6418
+ }
6419
+ } catch {
6420
+ console.log(pc2.dim(" Linked, but background sync could not be installed automatically. Run `npx whoburnedmore install-sync` to retry."));
6421
+ }
6422
+ console.log(` Profile: ${linked.profileUrl}`);
6423
+ }
6424
+ function waitOrAbort(ms, signal) {
6425
+ if (signal.aborted) return Promise.resolve();
6426
+ return new Promise((resolve) => {
6427
+ const onAbort = () => {
6428
+ clearTimeout(timer);
6429
+ resolve();
6430
+ };
6431
+ const timer = setTimeout(() => {
6432
+ signal.removeEventListener("abort", onAbort);
6433
+ resolve();
6434
+ }, ms);
6435
+ signal.addEventListener("abort", onAbort, { once: true });
6436
+ });
6437
+ }
6438
+ async function runDaemon() {
6439
+ ensureAnonKey();
6440
+ const controller = new AbortController();
6441
+ const onSignal = () => controller.abort();
6442
+ process.on("SIGINT", onSignal);
6443
+ process.on("SIGTERM", onSignal);
6444
+ console.log(pc2.bold(" whoburnedmore daemon"));
6445
+ console.log(
6446
+ pc2.dim(
6447
+ ` Syncing in the foreground every ${syncIntervalLabel()} \u2014 run this under systemd, a Docker CMD, pm2 or nohup to keep a server on the leaderboard. Ctrl-C to stop.`
6448
+ )
6449
+ );
6450
+ if (!process.env.WHOBURNEDMORE_CONFIG_DIR) {
6451
+ console.log(
6452
+ pc2.dim(
6453
+ " Tip: set WHOBURNEDMORE_CONFIG_DIR to a persistent path so this machine's identity survives container/VM restarts."
6454
+ )
6455
+ );
6456
+ }
6457
+ console.log();
6458
+ const cycles = await daemonLoop({
6459
+ intervalMs: SYNC_INTERVAL_MINUTES * 6e4,
6460
+ isStopped: () => controller.signal.aborted,
6461
+ log: (line) => console.log(` ${pc2.dim((/* @__PURE__ */ new Date()).toISOString())} ${line}`),
6462
+ wait: (ms) => waitOrAbort(ms, controller.signal),
6463
+ runOnce: async () => {
6464
+ rotateLogIfLarge();
6465
+ await run({ dryRun: false, noSubmit: false, local: false, quiet: true });
6466
+ }
6467
+ });
6468
+ console.log();
6469
+ console.log(pc2.dim(` Daemon stopped after ${cycles} sync cycle${cycles === 1 ? "" : "s"}.`));
6470
+ }
5940
6471
  async function main() {
5941
6472
  const major = Number(process.versions.node.split(".")[0]);
5942
6473
  if (major < 20) {
@@ -5945,13 +6476,16 @@ async function main() {
5945
6476
  return;
5946
6477
  }
5947
6478
  const args = process.argv.slice(2);
5948
- const command = resolveCommand(args);
6479
+ const baseCommand = resolveCommand(args);
6480
+ const command = args.includes("--watch") && (baseCommand === "run" || baseCommand === "sync") ? "daemon" : baseCommand;
5949
6481
  const flags = {
5950
6482
  dryRun: args.includes("--dry-run"),
5951
6483
  noSubmit: args.includes("--no-submit"),
5952
6484
  local: args.includes("--local"),
5953
6485
  quiet: command === "sync",
5954
- board: parseBoard(args)
6486
+ board: parseBoard(args),
6487
+ org: parseOrg(args),
6488
+ orgCode: parsePass(args)
5955
6489
  };
5956
6490
  switch (command) {
5957
6491
  case "run":
@@ -5963,6 +6497,12 @@ async function main() {
5963
6497
  await run({ ...flags, noSubmit: false, dryRun: false, local: false });
5964
6498
  break;
5965
6499
  }
6500
+ case "link":
6501
+ await linkServerInstall(parseInstallToken(args));
6502
+ break;
6503
+ case "daemon":
6504
+ await runDaemon();
6505
+ break;
5966
6506
  case "status":
5967
6507
  case "doctor": {
5968
6508
  for (const line of agentStatusReport()) console.log(line);
@@ -6016,9 +6556,12 @@ function printHelp() {
6016
6556
  ${pc2.bold("usage")}
6017
6557
  npx whoburnedmore burn + land on the public leaderboard, open your dashboard
6018
6558
  npx whoburnedmore --board=CODE compare with friends \u2014 join their board (no sign-in)
6559
+ npx whoburnedmore --org=SLUG submit to your organization's board (companies/hackathons)
6019
6560
  npx whoburnedmore --local build the dashboard on your machine and open it (offline)
6020
6561
  npx whoburnedmore --dry-run print exactly what would be sent, send nothing
6021
6562
  npx whoburnedmore --no-submit collect locally, send nothing (no dashboard)
6563
+ npx whoburnedmore link --token=TOKEN link this server/VM to your signed-in account
6564
+ npx whoburnedmore daemon keep syncing in the foreground (VMs/containers with no cron)
6022
6565
  npx whoburnedmore private hide your dashboard from the leaderboard
6023
6566
  npx whoburnedmore public put it back on the leaderboard
6024
6567
  npx whoburnedmore remove delete your dashboard and its data
@@ -6027,12 +6570,20 @@ function printHelp() {
6027
6570
  npx whoburnedmore install-sync turn it back on after uninstalling
6028
6571
 
6029
6572
  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
6573
+ automatically every 15 min (\`uninstall-sync\` to stop). Your dashboard is public on
6031
6574
  the leaderboard as an anonymous burner \u2014 sign in on whoburnedmore.com to claim
6032
6575
  it (handle + X) and own your rank, or run \`private\`/\`remove\` to pull it. Only
6033
6576
  daily aggregate numbers (date, tool, model, token counts, est. cost) ever leave
6034
6577
  your machine \u2014 never prompts, code, or file names. With --local, nothing leaves
6035
6578
  your machine at all.
6579
+
6580
+ ${pc2.bold("servers & VMs")}
6581
+ Generate a one-time \`link\` command from your profile on whoburnedmore.com and
6582
+ run it inside the VM to bind that machine to your account. On a persistent VM
6583
+ background sync uses cron or a systemd user timer automatically; in a container
6584
+ or any host without a scheduler, run \`whoburnedmore daemon\` under your process
6585
+ manager instead. Set WHOBURNEDMORE_CONFIG_DIR to a persistent path so the
6586
+ machine identity survives restarts. See docs/SERVER-VM-SETUP.md.
6036
6587
  `);
6037
6588
  }
6038
6589
  main().catch((err) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whoburnedmore",
3
- "version": "0.8.7",
3
+ "version": "0.9.1",
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"