whoburnedmore 0.8.9 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +236 -28
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -21,12 +21,17 @@ function parseBoard(args) {
21
21
  function parseOrg(args) {
22
22
  return parseValueFlag(args, "--org");
23
23
  }
24
+ function parsePass(args) {
25
+ return parseValueFlag(args, "--pass") ?? parseValueFlag(args, "--code");
26
+ }
24
27
  function parseInstallToken(args) {
25
28
  return parseValueFlag(args, "--token");
26
29
  }
27
30
  function applyScope(payload, flags) {
28
31
  if (flags.board) payload.board = flags.board;
29
32
  if (flags.org) payload.org = flags.org;
33
+ if (flags.org && flags.orgCode)
34
+ payload.orgCode = flags.orgCode;
30
35
  return payload;
31
36
  }
32
37
  function parseValueFlag(args, name) {
@@ -46,7 +51,7 @@ function resolveCommand(args) {
46
51
  if (args.includes("--version") || args.includes("-v") || args.includes("version")) {
47
52
  return "version";
48
53
  }
49
- const valueFlags = /* @__PURE__ */ new Set(["--board", "--org", "--token"]);
54
+ const valueFlags = /* @__PURE__ */ new Set(["--board", "--org", "--token", "--pass", "--code"]);
50
55
  for (let i = 0; i < args.length; i++) {
51
56
  const a = args[i];
52
57
  if (a.startsWith("-")) {
@@ -121,6 +126,11 @@ function claimUrl(dashboardUrl, anonKey) {
121
126
  function boardClaimUrl(boardUrl, slug, anonKey) {
122
127
  return `${boardUrl}#k=${encodeURIComponent(anonKey)}&u=${encodeURIComponent(slug)}`;
123
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
+ }
124
134
  async function anonVisibility(anonKey, listed) {
125
135
  const { status, body } = await post(
126
136
  "/v1/anon/visibility",
@@ -357,16 +367,13 @@ function installAutoSync() {
357
367
  return `launchd agent installed (${plistPath}), syncing every ${syncIntervalLabel()}`;
358
368
  }
359
369
  if (os === "linux") {
360
- const line = expectedLinuxCronLine();
361
- const current = spawnSync("crontab", ["-l"], { encoding: "utf8" });
362
- const existing = current.status === 0 ? current.stdout : "";
363
- const kept = existing.split("\n").filter((l) => !l.includes("whoburnedmore")).join("\n");
364
- const next = `${kept.trimEnd()}
365
- ${line}
366
- `.replace(/^\n+/, "");
367
- const res = spawnSync("crontab", ["-"], { input: next });
368
- if (res.status !== 0) throw new Error("could not install crontab entry");
369
- return `cron entry installed, syncing every ${syncIntervalLabel()}`;
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
+ );
370
377
  }
371
378
  if (os === "win32") {
372
379
  const res = spawnSync("schtasks", [
@@ -393,6 +400,90 @@ function expectedLinuxCronLine(opts) {
393
400
  const command = syncCommandArgs(opts?.npmPath).map(shellQuote).join(" ");
394
401
  return `${cronSchedule()} ${command} >${shellQuote(opts?.logPath ?? syncLogPath())} 2>&1`;
395
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)`;
486
+ }
396
487
  function uninstallAutoSync() {
397
488
  const os = platform();
398
489
  if (os === "darwin") {
@@ -404,12 +495,25 @@ function uninstallAutoSync() {
404
495
  return "launchd agent removed";
405
496
  }
406
497
  if (os === "linux") {
498
+ let removed = false;
407
499
  const current = spawnSync("crontab", ["-l"], { encoding: "utf8" });
408
- if (current.status === 0 && current.stdout.includes("whoburnedmore")) {
500
+ if (!current.error && current.status === 0 && current.stdout.includes("whoburnedmore")) {
409
501
  const next = current.stdout.split("\n").filter((l) => !l.includes("whoburnedmore")).join("\n");
410
502
  spawnSync("crontab", ["-"], { input: next });
503
+ removed = true;
504
+ }
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;
411
515
  }
412
- return "cron entry removed";
516
+ return removed ? "background sync removed" : "nothing to remove";
413
517
  }
414
518
  if (os === "win32") {
415
519
  spawnSync("schtasks", ["/Delete", "/F", "/TN", "whoburnedmore-sync"]);
@@ -420,8 +524,7 @@ function uninstallAutoSync() {
420
524
  function autoSyncInstalled() {
421
525
  if (platform() === "darwin") return existsSync2(launchAgentPath());
422
526
  if (platform() === "linux") {
423
- const current = spawnSync("crontab", ["-l"], { encoding: "utf8" });
424
- return current.status === 0 && current.stdout.includes("whoburnedmore");
527
+ return linuxSyncMechanism() !== "none";
425
528
  }
426
529
  if (platform() === "win32") {
427
530
  const res = spawnSync("schtasks", ["/Query", "/TN", "whoburnedmore-sync"], {
@@ -431,22 +534,40 @@ function autoSyncInstalled() {
431
534
  }
432
535
  return false;
433
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
+ }
434
549
  function readInstalledAgent() {
435
550
  if (platform() === "darwin") {
436
551
  const p = launchAgentPath();
437
552
  return existsSync2(p) ? readFileSync2(p, "utf8") : null;
438
553
  }
439
554
  if (platform() === "linux") {
440
- const current = spawnSync("crontab", ["-l"], { encoding: "utf8" });
441
- if (current.status !== 0) return null;
442
- const line = current.stdout.split("\n").find((l) => l.includes("whoburnedmore"));
443
- 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;
444
563
  }
445
564
  return null;
446
565
  }
447
566
  function expectedAgent() {
448
567
  if (platform() === "darwin") return expectedDarwinPlist();
449
- if (platform() === "linux") return expectedLinuxCronLine();
568
+ if (platform() === "linux") {
569
+ return linuxSyncMechanism() === "systemd" ? expectedSystemd() : expectedLinuxCronLine();
570
+ }
450
571
  return null;
451
572
  }
452
573
  function autoSyncDrift() {
@@ -531,6 +652,24 @@ function printBanner() {
531
652
  console.log();
532
653
  }
533
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
+
534
673
  // src/collect.ts
535
674
  import { execFile } from "node:child_process";
536
675
  import { createRequire as createRequire3 } from "node:module";
@@ -5691,7 +5830,14 @@ var SubmitPayload = external_exports.object({
5691
5830
  /** Optional friends-board code (from `--board=<code>`): auto-join this board on submit. */
5692
5831
  board: external_exports.string().min(1).max(32).optional(),
5693
5832
  /** Optional organization slug (from `--org=<slug>`): auto-join this org on submit. */
5694
- org: external_exports.string().min(2).max(32).optional()
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()
5695
5841
  });
5696
5842
  var AnonSubmitPayload = SubmitPayload.extend({
5697
5843
  /** Client-generated secret (hex). The server stores only its hash. */
@@ -5740,13 +5886,15 @@ var RESERVED_SLUGS = /* @__PURE__ */ new Set([
5740
5886
  ]);
5741
5887
  var SLUG_RE = /^[a-z0-9](?:[a-z0-9-]{0,30}[a-z0-9])?$/;
5742
5888
  function isValidSlug(slug) {
5743
- return typeof slug === "string" && slug.length >= 2 && slug.length <= 32 && SLUG_RE.test(slug) && !RESERVED_SLUGS.has(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);
5744
5892
  }
5745
5893
  var OrgSlug = external_exports.string().min(2).max(32).refine(isValidSlug, "invalid or reserved slug");
5746
5894
  var OrgWindow = external_exports.object({
5747
5895
  startDate: DateString.nullable().optional(),
5748
5896
  endDate: DateString.nullable().optional()
5749
- });
5897
+ }).refine((w) => !(w.startDate && w.endDate) || w.endDate >= w.startDate, { message: "endDate must be on or after startDate", path: ["endDate"] });
5750
5898
  var OrgJoinPolicy = external_exports.object({
5751
5899
  allowCodeJoin: external_exports.boolean().default(true),
5752
5900
  allowDomainJoin: external_exports.boolean().default(false),
@@ -6205,8 +6353,7 @@ async function run(flags) {
6205
6353
  }
6206
6354
  } catch {
6207
6355
  }
6208
- const baseUrl = result.boardUrl ?? result.dashboardUrl;
6209
- const target = result.boardUrl ? boardClaimUrl(result.boardUrl, result.slug, anonKey) : claimUrl(result.dashboardUrl, anonKey);
6356
+ const { baseUrl, target } = resolveOpenTarget(result, anonKey);
6210
6357
  const trusted = isTrustedWebUrl(baseUrl);
6211
6358
  if (!flags.quiet) {
6212
6359
  console.log(
@@ -6274,6 +6421,53 @@ async function linkServerInstall(token) {
6274
6421
  }
6275
6422
  console.log(` Profile: ${linked.profileUrl}`);
6276
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
+ }
6277
6471
  async function main() {
6278
6472
  const major = Number(process.versions.node.split(".")[0]);
6279
6473
  if (major < 20) {
@@ -6282,14 +6476,16 @@ async function main() {
6282
6476
  return;
6283
6477
  }
6284
6478
  const args = process.argv.slice(2);
6285
- const command = resolveCommand(args);
6479
+ const baseCommand = resolveCommand(args);
6480
+ const command = args.includes("--watch") && (baseCommand === "run" || baseCommand === "sync") ? "daemon" : baseCommand;
6286
6481
  const flags = {
6287
6482
  dryRun: args.includes("--dry-run"),
6288
6483
  noSubmit: args.includes("--no-submit"),
6289
6484
  local: args.includes("--local"),
6290
6485
  quiet: command === "sync",
6291
6486
  board: parseBoard(args),
6292
- org: parseOrg(args)
6487
+ org: parseOrg(args),
6488
+ orgCode: parsePass(args)
6293
6489
  };
6294
6490
  switch (command) {
6295
6491
  case "run":
@@ -6304,6 +6500,9 @@ async function main() {
6304
6500
  case "link":
6305
6501
  await linkServerInstall(parseInstallToken(args));
6306
6502
  break;
6503
+ case "daemon":
6504
+ await runDaemon();
6505
+ break;
6307
6506
  case "status":
6308
6507
  case "doctor": {
6309
6508
  for (const line of agentStatusReport()) console.log(line);
@@ -6361,7 +6560,8 @@ function printHelp() {
6361
6560
  npx whoburnedmore --local build the dashboard on your machine and open it (offline)
6362
6561
  npx whoburnedmore --dry-run print exactly what would be sent, send nothing
6363
6562
  npx whoburnedmore --no-submit collect locally, send nothing (no dashboard)
6364
- npx whoburnedmore link --token=TOKEN link this server to your signed-in account
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)
6365
6565
  npx whoburnedmore private hide your dashboard from the leaderboard
6366
6566
  npx whoburnedmore public put it back on the leaderboard
6367
6567
  npx whoburnedmore remove delete your dashboard and its data
@@ -6376,6 +6576,14 @@ function printHelp() {
6376
6576
  daily aggregate numbers (date, tool, model, token counts, est. cost) ever leave
6377
6577
  your machine \u2014 never prompts, code, or file names. With --local, nothing leaves
6378
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.
6379
6587
  `);
6380
6588
  }
6381
6589
  main().catch((err) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whoburnedmore",
3
- "version": "0.8.9",
3
+ "version": "0.9.2",
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": {