whoburnedmore 0.9.10 → 0.9.12

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 +156 -80
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -96,15 +96,15 @@ async function readJson(res) {
96
96
  };
97
97
  }
98
98
  }
99
- async function post(path, body, token) {
99
+ async function send(method, path, body, token) {
100
100
  const headers = { "Content-Type": "application/json" };
101
101
  if (token) headers.Authorization = `Bearer ${token}`;
102
102
  let res;
103
103
  try {
104
104
  res = await fetch(`${apiBase()}${path}`, {
105
- method: "POST",
105
+ method,
106
106
  headers,
107
- body: JSON.stringify(body),
107
+ ...body === void 0 ? {} : { body: JSON.stringify(body) },
108
108
  // Bound the request so a slow/black-holing/hostile server can't hang the CLI
109
109
  // — or the unattended 15-minute background sync — indefinitely.
110
110
  signal: AbortSignal.timeout(3e4)
@@ -116,6 +116,9 @@ async function post(path, body, token) {
116
116
  }
117
117
  return { status: res.status, body: await readJson(res) };
118
118
  }
119
+ async function post(path, body, token) {
120
+ return send("POST", path, body, token);
121
+ }
119
122
  async function verifyUsage(token, payload) {
120
123
  const { status, body } = await post("/v1/verify", payload, token);
121
124
  if (status === 401) throw new UnauthorizedError();
@@ -127,18 +130,6 @@ async function verifyUsage(token, payload) {
127
130
  }
128
131
  return body;
129
132
  }
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.";
131
- async function anonSubmit(anonKey, payload) {
132
- const { status, body } = await post("/v1/anon/submit", { ...payload, anonKey });
133
- if (status === 410) throw new Error(ANON_RETIRED_MESSAGE);
134
- if (status !== 200) {
135
- const err = body;
136
- const details = err.details?.length ? `
137
- - ${err.details.join("\n - ")}` : "";
138
- throw new Error(`${err.error ?? `submit failed (HTTP ${status})`}${details}`);
139
- }
140
- return body;
141
- }
142
133
  var UnauthorizedError = class extends Error {
143
134
  constructor(message = "your sign-in has expired") {
144
135
  super(message);
@@ -207,31 +198,21 @@ async function submit(token, payload) {
207
198
  }
208
199
  return body;
209
200
  }
210
- function claimUrl(dashboardUrl, anonKey) {
211
- return `${dashboardUrl}#k=${encodeURIComponent(anonKey)}`;
212
- }
213
- async function anonVisibility(anonKey, listed) {
214
- const { status, body } = await post(
215
- "/v1/anon/visibility",
216
- { anonKey, listed }
217
- );
218
- if (status === 410) throw new Error(ANON_RETIRED_MESSAGE);
201
+ async function setVisibility(token, listed) {
202
+ const { status, body } = await post("/v1/cli/visibility", { listed }, token);
203
+ if (status === 401) throw new UnauthorizedError();
219
204
  if (status !== 200) {
220
205
  throw new Error(body.error ?? `failed (HTTP ${status})`);
221
206
  }
207
+ return { listed: body.listed === true, eligible: body.eligible === true };
222
208
  }
223
- async function anonRemove(anonKey) {
224
- const res = await fetch(`${apiBase()}/v1/anon`, {
225
- method: "DELETE",
226
- headers: { "Content-Type": "application/json" },
227
- body: JSON.stringify({ anonKey }),
228
- signal: AbortSignal.timeout(3e4)
229
- });
230
- if (res.status === 410) throw new Error(ANON_RETIRED_MESSAGE);
231
- if (res.status !== 200) {
232
- const b = await res.json().catch(() => ({}));
233
- throw new Error(b.error ?? `failed (HTTP ${res.status})`);
209
+ async function removeUsage(token) {
210
+ const { status, body } = await send("DELETE", "/v1/cli/usage", void 0, token);
211
+ if (status === 401) throw new UnauthorizedError();
212
+ if (status !== 200) {
213
+ throw new Error(body.error ?? `failed (HTTP ${status})`);
234
214
  }
215
+ return { deletedDays: body.deletedDays ?? 0 };
235
216
  }
236
217
  async function redeemServerInstall(token, anonKey) {
237
218
  const { status, body } = await post("/v1/server-install/redeem", { token, anonKey });
@@ -8004,17 +7985,16 @@ function renderDashboardHtml(entries, generatedAt = /* @__PURE__ */ new Date(),
8004
7985
  (p) => `<div class="tbcell"><div class="tblabel"><span class="dot" style="background:${p.color}"></span>${p.label}</div><div class="tbval">${esc(formatTokens(p.value))}</div><div class="tbpct">${Math.round(p.value / tbSum * 100)}%</div></div>`
8005
7986
  ).join("");
8006
7987
  const connectCta = connect ? `
8007
- <form class="connect" method="POST" action="${esc(connect.webBaseUrl)}/connect">
8008
- <input type="hidden" name="payload" value="${Buffer.from(JSON.stringify(connect.payload)).toString("base64")}">
7988
+ <div class="connect">
8009
7989
  <div class="connect-row">
8010
7990
  <div>
8011
- <div class="connect-title">Connect your account</div>
8012
- <div class="connect-sub">Save this dashboard to your account and claim your spot on the public leaderboard. The local numbers above become your starting point \u2014 nothing has left your machine yet.</div>
7991
+ <div class="connect-title">Claim your spot on the public leaderboard</div>
7992
+ <div class="connect-sub">Run the command below in your terminal \u2014 it signs you in and puts these numbers on <a href="${esc(connect.webBaseUrl)}">whoburnedmore.com</a> under your handle. Nothing has left your machine yet.</div>
8013
7993
  </div>
8014
- <button type="submit">Connect your account \u2192</button>
7994
+ <code class="connect-cmd">npx whoburnedmore</code>
8015
7995
  </div>
8016
- <div class="connect-note">After connecting, run <code>npx whoburnedmore</code> (no flag) once so it keeps syncing automatically in the background.</div>
8017
- </form>` : "";
7996
+ <div class="connect-note">Only daily totals are submitted \u2014 never your prompts or code. Background sync keeps your page fresh afterwards.</div>
7997
+ </div>` : "";
8018
7998
  return `<!doctype html>
8019
7999
  <html lang="en">
8020
8000
  <head>
@@ -8074,8 +8054,8 @@ function renderDashboardHtml(entries, generatedAt = /* @__PURE__ */ new Date(),
8074
8054
  @media (min-width: 640px) { .connect-row { flex-direction: row; align-items: center; justify-content: space-between; } }
8075
8055
  .connect-title { font-size: 16px; font-weight: 700; }
8076
8056
  .connect-sub { color: #d6d3d1; font-size: 13px; margin-top: 4px; max-width: 60ch; }
8077
- .connect button { flex-shrink: 0; cursor: pointer; border: 0; border-radius: 10px; background: #ea580c; color: #fff; font-size: 14px; font-weight: 600; padding: 11px 18px; font-family: inherit; transition: background .15s; }
8078
- .connect button:hover { background: #f97316; }
8057
+ .connect-cmd { flex-shrink: 0; display: inline-block; border-radius: 10px; background: rgba(0,0,0,.35); border: 1px solid rgba(234,88,12,.5); color: #fed7aa; font-size: 14px; font-weight: 600; padding: 11px 18px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; user-select: all; }
8058
+ .connect-sub a { color: #fed7aa; }
8079
8059
  .connect-note { color: #a8a29e; font-size: 12px; margin-top: 14px; padding-top: 12px; border-top: 1px solid rgba(234,88,12,.25); }
8080
8060
  .connect-note code { color: #fed7aa; background: rgba(0,0,0,.25); padding: 1px 6px; border-radius: 5px; }
8081
8061
  </style>
@@ -8145,20 +8125,24 @@ ${connectCta}
8145
8125
 
8146
8126
  // src/publish.ts
8147
8127
  async function publishLocal(payload, deps) {
8148
- if (!await deps.confirm(" Publish this to the public leaderboard?")) {
8128
+ if (!await deps.confirm(" Publish this to the public leaderboard? (You'll sign in first.)")) {
8149
8129
  deps.log(" Kept local. Nothing left your machine.");
8150
8130
  return false;
8151
8131
  }
8152
- const key = deps.ensureAnonKey();
8153
- const res = await deps.anonSubmit(key, payload);
8154
- deps.log(` Published \u2014 you're on the board: ${sanitizeServerText(res.dashboardUrl)}`);
8155
- if (isTrustedWebUrl(res.dashboardUrl)) {
8156
- deps.openBrowser(claimUrl(res.dashboardUrl, key));
8132
+ const auth = await deps.signIn();
8133
+ if (!auth) {
8134
+ deps.log(" Sign-in didn't complete \u2014 kept local. Nothing left your machine.");
8135
+ return false;
8136
+ }
8137
+ const res = await deps.submit(auth.token, payload);
8138
+ deps.log(` Published \u2014 you're on the board: ${sanitizeServerText(res.profileUrl)}`);
8139
+ if (isTrustedWebUrl(res.profileUrl)) {
8140
+ deps.openBrowser(res.profileUrl);
8157
8141
  } else {
8158
8142
  deps.log(" (The server returned an unexpected address, so it was not auto-opened.)");
8159
8143
  }
8160
8144
  deps.log(
8161
- " Sign in there to claim it, or `npx whoburnedmore private` to hide it again."
8145
+ " Manage anytime: `npx whoburnedmore private` hides you, `remove` deletes your data."
8162
8146
  );
8163
8147
  return true;
8164
8148
  }
@@ -8241,10 +8225,7 @@ function showLocalDashboard(payload) {
8241
8225
  const file = join10(dir, "dashboard.html");
8242
8226
  writeFileSync3(
8243
8227
  file,
8244
- renderDashboardHtml(payload.entries, /* @__PURE__ */ new Date(), {
8245
- payload,
8246
- webBaseUrl: webBase()
8247
- })
8228
+ renderDashboardHtml(payload.entries, /* @__PURE__ */ new Date(), { webBaseUrl: webBase() })
8248
8229
  );
8249
8230
  console.log();
8250
8231
  console.log(` Local dashboard: ${pc2.cyan(`file://${file}`)}`);
@@ -8300,8 +8281,23 @@ async function run(flags) {
8300
8281
  if (!flags.quiet && process.stdin.isTTY) {
8301
8282
  await publishLocal(payload, {
8302
8283
  confirm,
8303
- ensureAnonKey,
8304
- anonSubmit,
8284
+ signIn: ensureSignedIn,
8285
+ submit: async (token, p) => {
8286
+ const result = await submit(token, p);
8287
+ try {
8288
+ recordSync();
8289
+ } catch {
8290
+ }
8291
+ try {
8292
+ const cfgNow = loadConfig();
8293
+ if (!cfgNow?.deviceBoundAt) {
8294
+ const key = ensureAnonKey();
8295
+ if (await bindDeviceKey(token, key)) recordDeviceBound();
8296
+ }
8297
+ } catch {
8298
+ }
8299
+ return result;
8300
+ },
8305
8301
  openBrowser,
8306
8302
  log: (line) => console.log(pc2.dim(line))
8307
8303
  });
@@ -8667,6 +8663,102 @@ async function runVerify() {
8667
8663
  );
8668
8664
  }
8669
8665
  }
8666
+ async function requireSignedIn(commandName) {
8667
+ const cfg = loadConfig();
8668
+ if (cfg?.cliToken) return { token: cfg.cliToken, handle: cfg.handle ?? "" };
8669
+ const healed = await refreshCliTokenFromConfig();
8670
+ if (healed) {
8671
+ saveAuth(void 0, { cliToken: healed.token, handle: healed.handle });
8672
+ return healed;
8673
+ }
8674
+ if (process.stdout.isTTY) return ensureSignedIn();
8675
+ console.log(
8676
+ pc2.yellow(
8677
+ ` Sign in first \u2014 run \`npx whoburnedmore ${commandName}\` in an interactive terminal.`
8678
+ )
8679
+ );
8680
+ return null;
8681
+ }
8682
+ async function runVisibility(listed) {
8683
+ const auth = await requireSignedIn(listed ? "public" : "private");
8684
+ if (!auth) return;
8685
+ let res;
8686
+ try {
8687
+ res = await setVisibility(auth.token, listed);
8688
+ } catch (err) {
8689
+ if (err instanceof UnauthorizedError) {
8690
+ clearAuth();
8691
+ console.log(
8692
+ pc2.yellow(
8693
+ ` Your sign-in expired \u2014 run \`npx whoburnedmore ${listed ? "public" : "private"}\` again to retry.`
8694
+ )
8695
+ );
8696
+ return;
8697
+ }
8698
+ throw err;
8699
+ }
8700
+ if (!listed) {
8701
+ console.log(" \u2713 You're off the public leaderboard. `npx whoburnedmore public` puts you back.");
8702
+ return;
8703
+ }
8704
+ if (res.listed) {
8705
+ console.log(" \u2713 You're back on the public leaderboard.");
8706
+ } else if (!res.eligible) {
8707
+ console.log(
8708
+ pc2.yellow(" You're signed in, but not on the leaderboard yet \u2014 it lists accounts with a social handle.")
8709
+ );
8710
+ console.log(
8711
+ pc2.dim(" Add your X, GitHub or Instagram on your profile at whoburnedmore.com, then you're on.")
8712
+ );
8713
+ } else {
8714
+ console.log(" \u2713 Leaderboard listing is on.");
8715
+ }
8716
+ }
8717
+ async function runRemove() {
8718
+ if (!process.stdin.isTTY) {
8719
+ console.log(
8720
+ pc2.yellow(" `remove` deletes data, so it needs an interactive terminal to confirm.")
8721
+ );
8722
+ console.log(pc2.dim(" Run `npx whoburnedmore remove` in a real terminal."));
8723
+ process.exitCode = 1;
8724
+ return;
8725
+ }
8726
+ const auth = await requireSignedIn("remove");
8727
+ if (!auth) return;
8728
+ const who = auth.handle ? `@${sanitizeServerText(auth.handle)}` : "your account";
8729
+ console.log(
8730
+ ` This deletes ALL usage data stored for ${who} on whoburnedmore.com and turns off this machine's background sync.`
8731
+ );
8732
+ console.log(
8733
+ pc2.dim(" Your local logs are untouched, and the account itself survives (delete it from your dashboard settings on the web).")
8734
+ );
8735
+ if (!await confirm(" Delete your usage data?")) {
8736
+ console.log(pc2.dim(" Cancelled \u2014 nothing was deleted."));
8737
+ return;
8738
+ }
8739
+ let res;
8740
+ try {
8741
+ res = await removeUsage(auth.token);
8742
+ } catch (err) {
8743
+ if (err instanceof UnauthorizedError) {
8744
+ clearAuth();
8745
+ console.log(
8746
+ pc2.yellow(" Your sign-in expired \u2014 run `npx whoburnedmore remove` again to retry.")
8747
+ );
8748
+ return;
8749
+ }
8750
+ throw err;
8751
+ }
8752
+ try {
8753
+ uninstallAutoSync();
8754
+ } catch {
8755
+ }
8756
+ clearAuth();
8757
+ console.log(
8758
+ pc2.green(` \u2713 Deleted your usage data (${res.deletedDays} day${res.deletedDays === 1 ? "" : "s"}) and turned off background sync.`)
8759
+ );
8760
+ console.log(pc2.dim(" Run `npx whoburnedmore` anytime to start fresh."));
8761
+ }
8670
8762
  async function main() {
8671
8763
  const major = Number(process.versions.node.split(".")[0]);
8672
8764
  if (major < 20) {
@@ -8711,28 +8803,12 @@ async function main() {
8711
8803
  break;
8712
8804
  }
8713
8805
  case "private":
8714
- case "public": {
8715
- const cfg = loadConfig();
8716
- if (!cfg?.anonKey) {
8717
- console.log(" No anonymous dashboard on this machine \u2014 run `npx whoburnedmore` first.");
8718
- break;
8719
- }
8720
- await anonVisibility(cfg.anonKey, command === "public");
8721
- console.log(
8722
- command === "public" ? " Your dashboard is public on the leaderboard again." : " Your dashboard is now private \u2014 off the public leaderboard."
8723
- );
8806
+ case "public":
8807
+ await runVisibility(command === "public");
8724
8808
  break;
8725
- }
8726
- case "remove": {
8727
- const cfg = loadConfig();
8728
- if (!cfg?.anonKey) {
8729
- console.log(" No anonymous dashboard on this machine \u2014 nothing to remove.");
8730
- break;
8731
- }
8732
- await anonRemove(cfg.anonKey);
8733
- console.log(" Removed your anonymous dashboard and all its data.");
8809
+ case "remove":
8810
+ await runRemove();
8734
8811
  break;
8735
- }
8736
8812
  case "install-sync":
8737
8813
  console.log(` ${installAutoSync()}`);
8738
8814
  break;
@@ -8764,9 +8840,9 @@ function printHelp() {
8764
8840
  npx whoburnedmore --no-submit collect locally, send nothing (no dashboard)
8765
8841
  npx whoburnedmore link --token=TOKEN link this server/VM to your signed-in account
8766
8842
  npx whoburnedmore daemon keep syncing in the foreground (VMs/containers with no cron)
8767
- npx whoburnedmore private hide your dashboard from the leaderboard
8768
- npx whoburnedmore public put it back on the leaderboard
8769
- npx whoburnedmore remove delete your dashboard and its data
8843
+ npx whoburnedmore private take yourself off the public leaderboard
8844
+ npx whoburnedmore public put yourself back on it
8845
+ npx whoburnedmore remove delete your usage data and stop background sync
8770
8846
  npx whoburnedmore verify delisted? re-verify your usage to get back on (sends a detailed breakdown)
8771
8847
  npx whoburnedmore status check background-sync health (last sync, staleness)
8772
8848
  npx whoburnedmore uninstall-sync turn off the background sync
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whoburnedmore",
3
- "version": "0.9.10",
3
+ "version": "0.9.12",
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": {