whoburnedmore 0.9.5 → 0.9.6

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 +40 -23
  2. package/dist/index.js +7 -103
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # whoburnedmore
2
2
 
3
- **who burned more tokens — you or them?** One command reads your local AI coding-agent usage logs, shows your burn, and gives you a **shareable dashboard with no sign-in required**.
3
+ **who burned more tokens — you or them?** One command reads your local AI coding-agent usage logs, adds up your burn, and puts you on the public leaderboard with a **shareable dashboard**.
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/whoburnedmore.svg?color=f97316)](https://www.npmjs.com/package/whoburnedmore)
6
6
  [![npm downloads](https://img.shields.io/npm/dm/whoburnedmore.svg?color=f97316)](https://www.npmjs.com/package/whoburnedmore)
@@ -11,7 +11,7 @@
11
11
  npx whoburnedmore
12
12
  ```
13
13
 
14
- That's the whole thing. No install, no account, no config.
14
+ That's the whole thing. No install, no config. Your **first run signs you in** — the CLI opens a page, you approve a short code, and this machine is bound to your account.
15
15
 
16
16
  ---
17
17
 
@@ -28,13 +28,15 @@ whoburnedmore · https://whoburnedmore.com
28
28
  total 2.31B tokens $2,656.91
29
29
  today 92.8M tokens
30
30
 
31
- You burned 2.31B tokens. Your shareable dashboard:
32
- https://whoburnedmore.com/d/5twjwi-jtga4r
31
+ You burned 2.31B tokens. Your dashboard:
32
+ https://whoburnedmore.com/u/<your-handle>
33
33
 
34
- Re-run anytime to update it · `npx whoburnedmore login` to claim a public handle and compete.
34
+ Re-run anytime to update it. Background sync keeps it fresh every 15 min.
35
35
  ```
36
36
 
37
- Your dashboard is **anonymous and unlisted by default** — only people you send the link to can see it. Run the command again anytime to refresh it.
37
+ Once you're signed in, your usage lands on the leaderboard under your handle. Run the command again anytime to refresh — or let the background sync do it for you.
38
+
39
+ **Want to stay off the board?** Run `npx whoburnedmore private` to hide your dashboard, or `npx whoburnedmore --local` to never upload anything at all.
38
40
 
39
41
  ## Privacy first
40
42
 
@@ -46,7 +48,7 @@ See exactly what would be sent, and send nothing:
46
48
  npx whoburnedmore --dry-run
47
49
  ```
48
50
 
49
- Or stay completely offline — `--local` builds the same dashboard as a self-contained HTML file on your machine and opens it in your browser, sending nothing anywhere:
51
+ Or stay completely offline — `--local` builds the same dashboard as a self-contained HTML file on your machine and opens it in your browser, sending nothing anywhere and never signing you in:
50
52
 
51
53
  ```bash
52
54
  npx whoburnedmore --local
@@ -56,14 +58,21 @@ npx whoburnedmore --local
56
58
 
57
59
  | Command | What it does |
58
60
  | --- | --- |
59
- | `npx whoburnedmore` | Show your burn + get a shareable dashboard (no sign-in) |
60
- | `npx whoburnedmore --local` | Build the dashboard locally and open it (fully offline) |
61
+ | `npx whoburnedmore` | Sign in, submit your burn, land on the leaderboard, open your dashboard |
62
+ | `npx whoburnedmore --board=CODE` | Compare with friends sign in and join their board |
63
+ | `npx whoburnedmore --org=SLUG` | Submit to your organization's board (companies / hackathons) |
64
+ | `npx whoburnedmore --local` | Build the dashboard locally and open it (fully offline, no sign-in) |
61
65
  | `npx whoburnedmore --dry-run` | Print exactly what would be sent, send nothing |
62
66
  | `npx whoburnedmore --no-submit` | Print local stats only, send nothing |
63
- | `npx whoburnedmore login` | Sign in to claim a public handle + join the leaderboard |
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 (every 15 min) |
66
- | `npx whoburnedmore uninstall-sync` | Remove the background sync |
67
+ | `npx whoburnedmore private` | Hide your dashboard from the leaderboard |
68
+ | `npx whoburnedmore public` | Put it back on the leaderboard |
69
+ | `npx whoburnedmore remove` | Delete your dashboard and all its data |
70
+ | `npx whoburnedmore verify` | Delisted? Re-verify your usage to get back on |
71
+ | `npx whoburnedmore link --token=TOKEN` | Bind a server / VM to your account (token from your profile) |
72
+ | `npx whoburnedmore daemon` | Keep syncing in the foreground (containers with no scheduler) |
73
+ | `npx whoburnedmore status` | Check background-sync health (last sync, staleness) |
74
+ | `npx whoburnedmore install-sync` | Turn the 15-minute background sync back on |
75
+ | `npx whoburnedmore uninstall-sync` | Turn the background sync off |
67
76
 
68
77
  ## Supported tools
69
78
 
@@ -73,29 +82,37 @@ Usage is read via [`ccusage`](https://www.npmjs.com/package/ccusage) from any of
73
82
 
74
83
  Whatever you have logs for is picked up automatically — you don't pass any flags.
75
84
 
76
- ## Claim a public handle (optional)
85
+ ## Compete with friends and teams
77
86
 
78
- Want on the public leaderboard? Sign in with **GitHub or Google**:
87
+ - **Friends:** join someone's board with `npx whoburnedmore --board=CODE` (the code comes from their board's invite).
88
+ - **Companies / hackathons:** submit to an organization board with `npx whoburnedmore --org=SLUG` (add `--pass=CODE` if the org requires a join code).
79
89
 
80
- ```bash
81
- npx whoburnedmore login
82
- ```
83
-
84
- It uses a device flow — a code appears in your terminal, you approve it in the browser, and you're done. Your submissions then link to your handle at `whoburnedmore.com/u/<handle>`. You can delete your account and every row of data anytime from your profile.
90
+ Your submissions link to your handle at `whoburnedmore.com/u/<handle>`. You can delete your account and every row of data anytime from your profile.
85
91
 
86
92
  ## Keep it live
87
93
 
88
- Want your dashboard to stay fresh without re-running by hand?
94
+ Background sync is on by default after your first run, so your dashboard stays fresh without re-running by hand. To manage it:
89
95
 
90
96
  ```bash
91
- npx whoburnedmore install-sync # background sync every 15 min (launchd / cron / scheduled task)
92
- npx whoburnedmore uninstall-sync # remove it
97
+ npx whoburnedmore status # last sync + staleness
98
+ npx whoburnedmore uninstall-sync # turn the background sync off
99
+ npx whoburnedmore install-sync # turn it back on (launchd / cron / scheduled task)
93
100
  ```
94
101
 
95
102
  The installed background job runs the latest published `whoburnedmore` package on
96
103
  each sync tick, so future CLI fixes are picked up automatically after the next
97
104
  15-minute refresh.
98
105
 
106
+ ## Servers & VMs
107
+
108
+ Generate a one-time `link` command from your profile on whoburnedmore.com and run it inside the machine to bind it to your account:
109
+
110
+ ```bash
111
+ npx whoburnedmore link --token=TOKEN
112
+ ```
113
+
114
+ On a persistent VM, background sync uses cron or a systemd user timer automatically. In a container or any host without a scheduler, run `npx whoburnedmore daemon` under your process manager instead. Set `WHOBURNEDMORE_CONFIG_DIR` to a persistent path so the machine identity survives restarts.
115
+
99
116
  ## Links
100
117
 
101
118
  - 🏆 Leaderboard — **[whoburnedmore.com](https://whoburnedmore.com)**
package/dist/index.js CHANGED
@@ -127,8 +127,10 @@ async function verifyUsage(token, payload) {
127
127
  }
128
128
  return body;
129
129
  }
130
+ var ANON_RETIRED_MESSAGE = "Anonymous mode has been retired. Run `npx whoburnedmore` and sign in, or `npx whoburnedmore link --token=\u2026` for a server/CI, to put your usage on the leaderboard.";
130
131
  async function anonSubmit(anonKey, payload) {
131
132
  const { status, body } = await post("/v1/anon/submit", { ...payload, anonKey });
133
+ if (status === 410) throw new Error(ANON_RETIRED_MESSAGE);
132
134
  if (status !== 200) {
133
135
  const err = body;
134
136
  const details = err.details?.length ? `
@@ -179,19 +181,12 @@ async function submit(token, payload) {
179
181
  function claimUrl(dashboardUrl, anonKey) {
180
182
  return `${dashboardUrl}#k=${encodeURIComponent(anonKey)}`;
181
183
  }
182
- function boardClaimUrl(boardUrl, slug, anonKey) {
183
- return `${boardUrl}#k=${encodeURIComponent(anonKey)}&u=${encodeURIComponent(slug)}`;
184
- }
185
- function resolveOpenTarget(result, anonKey) {
186
- const baseUrl = result.orgBoardUrl ?? result.boardUrl ?? result.dashboardUrl;
187
- const target = result.orgBoardUrl ? boardClaimUrl(result.orgBoardUrl, result.slug, anonKey) : result.boardUrl ? boardClaimUrl(result.boardUrl, result.slug, anonKey) : claimUrl(result.dashboardUrl, anonKey);
188
- return { baseUrl, target };
189
- }
190
184
  async function anonVisibility(anonKey, listed) {
191
185
  const { status, body } = await post(
192
186
  "/v1/anon/visibility",
193
187
  { anonKey, listed }
194
188
  );
189
+ if (status === 410) throw new Error(ANON_RETIRED_MESSAGE);
195
190
  if (status !== 200) {
196
191
  throw new Error(body.error ?? `failed (HTTP ${status})`);
197
192
  }
@@ -203,6 +198,7 @@ async function anonRemove(anonKey) {
203
198
  body: JSON.stringify({ anonKey }),
204
199
  signal: AbortSignal.timeout(3e4)
205
200
  });
201
+ if (res.status === 410) throw new Error(ANON_RETIRED_MESSAGE);
206
202
  if (res.status !== 200) {
207
203
  const b = await res.json().catch(() => ({}));
208
204
  throw new Error(b.error ?? `failed (HTTP ${res.status})`);
@@ -299,10 +295,6 @@ function recordSync(dir = defaultConfigDir(), when = Date.now()) {
299
295
  const config = loadConfig(dir) ?? {};
300
296
  saveConfig(dir, { ...config, lastSyncAt: when });
301
297
  }
302
- function recordLaunchNotificationDelivered(dir = defaultConfigDir(), when = Date.now()) {
303
- const config = loadConfig(dir) ?? {};
304
- saveConfig(dir, { ...config, launchNotificationDeliveredAt: when });
305
- }
306
298
  function saveAuth(dir = defaultConfigDir(), auth = { cliToken: "" }) {
307
299
  const config = loadConfig(dir) ?? {};
308
300
  saveConfig(dir, { ...config, cliToken: auth.cliToken, handle: auth.handle });
@@ -730,41 +722,6 @@ function rotateLogIfLarge(path = syncLogPath(), capBytes = 256 * 1024) {
730
722
  return false;
731
723
  }
732
724
  }
733
- function notifyLaunchLive(opts) {
734
- const os = opts?.platform ?? platform();
735
- const run2 = opts?.spawn ?? ((cmd, args) => spawnSync(cmd, args, { stdio: "ignore" }));
736
- const title = "whoburnedmore is live";
737
- const message = "Your dashboard is ready. Go to whoburnedmore.com";
738
- if (os === "darwin") {
739
- return run2("osascript", [
740
- "-e",
741
- `display notification "${message}" with title "${title}"`
742
- ]).status === 0;
743
- }
744
- if (os === "linux") {
745
- return run2("notify-send", [title, message]).status === 0;
746
- }
747
- if (os === "win32") {
748
- const psQuote = (value) => `'${value.replaceAll("'", "''")}'`;
749
- const script = [
750
- "Add-Type -AssemblyName System.Windows.Forms",
751
- "$n = New-Object System.Windows.Forms.NotifyIcon",
752
- "$n.Icon = [System.Drawing.SystemIcons]::Application",
753
- `$n.BalloonTipTitle = ${psQuote(title)}`,
754
- `$n.BalloonTipText = ${psQuote(message)}`,
755
- "$n.Visible = $true",
756
- "$n.ShowBalloonTip(10000)",
757
- "Start-Sleep -Seconds 2",
758
- "$n.Dispose()"
759
- ].join("; ");
760
- return run2("powershell", [
761
- "-NoProfile",
762
- "-Command",
763
- script
764
- ]).status === 0;
765
- }
766
- return false;
767
- }
768
725
  function autoSyncLoaded() {
769
726
  if (platform() === "darwin") {
770
727
  const res = spawnSync("launchctl", ["list"], { encoding: "utf8" });
@@ -6493,22 +6450,6 @@ function formatUSD(n) {
6493
6450
  function sanitizeServerText(s) {
6494
6451
  return s.replace(/[\u0000-\u001f\u007f-\u009f]/g, "");
6495
6452
  }
6496
- function submitNextStepLines(result) {
6497
- if (result.boardUrl) {
6498
- const code = result.boardCode ?? result.boardUrl.split("/").filter(Boolean).pop() ?? "";
6499
- return [
6500
- ` \u{1F91D} You're on the board: ${sanitizeServerText(result.boardUrl)}`,
6501
- " \u2192 Open it to see who burned more.",
6502
- ` \u2192 Get a friend on it \u2014 have them run: npx whoburnedmore --board=${sanitizeServerText(code)}`,
6503
- " \u2192 Sign in on the page and add your X to claim your spot and own your rank."
6504
- ];
6505
- }
6506
- return [
6507
- ` Your dashboard: ${sanitizeServerText(result.dashboardUrl)}`,
6508
- " \u2192 Sign in and add your X on the page to get on the leaderboard and claim your rank.",
6509
- " Private until you do. Manage anytime: `npx whoburnedmore private` \xB7 `public` \xB7 `remove`."
6510
- ];
6511
- }
6512
6453
  function signedInNextStepLines(result) {
6513
6454
  if (result.orgBoardUrl) {
6514
6455
  return [
@@ -6910,8 +6851,6 @@ async function run(flags) {
6910
6851
  const canSignIn = !flags.quiet && Boolean(process.stdout.isTTY);
6911
6852
  if (cfg?.cliToken) {
6912
6853
  await submitSignedIn(cfg.cliToken, payload, flags, canSignIn);
6913
- } else if (cfg?.anonKey) {
6914
- await submitWithDeviceKey(cfg.anonKey, payload, flags);
6915
6854
  } else if (canSignIn) {
6916
6855
  const auth = await ensureSignedIn();
6917
6856
  if (!auth) return;
@@ -7031,44 +6970,6 @@ async function submitSignedIn(token, payload, flags, interactive) {
7031
6970
  }
7032
6971
  afterSubmitChores(flags);
7033
6972
  }
7034
- async function submitWithDeviceKey(anonKey, payload, flags) {
7035
- const result = await anonSubmit(anonKey, payload);
7036
- try {
7037
- recordSync();
7038
- } catch {
7039
- }
7040
- try {
7041
- const config = loadConfig();
7042
- if (result.launch?.live && !config?.launchNotificationDeliveredAt) {
7043
- if (notifyLaunchLive()) {
7044
- recordLaunchNotificationDelivered();
7045
- }
7046
- }
7047
- } catch {
7048
- }
7049
- const { baseUrl, target } = resolveOpenTarget(result, anonKey);
7050
- const trusted = isTrustedWebUrl(baseUrl);
7051
- if (!flags.quiet) {
7052
- console.log(
7053
- pc2.green(" \u2713 Synced securely.") + pc2.dim(" Only your daily totals left this machine \u2014 never your prompts, code, or file names.")
7054
- );
7055
- if (trusted) {
7056
- console.log(pc2.dim(" Opening your dashboard in your browser\u2026"));
7057
- openBrowser(target);
7058
- } else {
7059
- console.log(
7060
- pc2.dim(" The server returned an unexpected dashboard address, so it was NOT auto-opened. Open it yourself only if you trust it:")
7061
- );
7062
- console.log(` ${sanitizeServerText(baseUrl)}`);
7063
- }
7064
- for (const line of submitNextStepLines(result)) {
7065
- if (line.includes("\u2192")) console.log(pc2.bold(line));
7066
- else if (line.startsWith(" Private until you do")) console.log(pc2.dim(line));
7067
- else console.log(line);
7068
- }
7069
- }
7070
- afterSubmitChores(flags);
7071
- }
7072
6973
  function afterSubmitChores(flags) {
7073
6974
  if (flags.quiet) return;
7074
6975
  try {
@@ -7086,6 +6987,9 @@ async function linkServerInstall(token) {
7086
6987
  }
7087
6988
  const anonKey = ensureAnonKey();
7088
6989
  const linked = await redeemServerInstall(token, anonKey);
6990
+ if (linked.cliToken) {
6991
+ saveAuth(void 0, { cliToken: linked.cliToken, handle: linked.handle });
6992
+ }
7089
6993
  const handle = sanitizeServerText(linked.handle);
7090
6994
  console.log(
7091
6995
  linked.alreadyLinked ? ` This machine is already linked to @${handle}.` : ` Linked this machine to @${handle}.`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whoburnedmore",
3
- "version": "0.9.5",
3
+ "version": "0.9.6",
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": {