whoburnedmore 0.9.10 → 0.9.14

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 +379 -138
  2. package/package.json +3 -4
package/dist/index.js CHANGED
@@ -8,10 +8,10 @@ var __export = (target, all) => {
8
8
  // src/index.ts
9
9
  import { spawn } from "node:child_process";
10
10
  import { createHash } from "node:crypto";
11
- import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "node:fs";
11
+ import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "node:fs";
12
12
  import { createRequire as createRequire4 } from "node:module";
13
13
  import { platform as platform3 } from "node:os";
14
- import { join as join10 } from "node:path";
14
+ import { join as join11 } from "node:path";
15
15
  import { createInterface } from "node:readline/promises";
16
16
  import pc2 from "picocolors";
17
17
 
@@ -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 });
@@ -805,7 +786,7 @@ async function daemonLoop(deps) {
805
786
  // src/collect.ts
806
787
  import { execFile } from "node:child_process";
807
788
  import { createRequire as createRequire3 } from "node:module";
808
- import { dirname as dirname4, join as join9 } from "node:path";
789
+ import { dirname as dirname5, join as join10 } from "node:path";
809
790
  import { promisify } from "node:util";
810
791
 
811
792
  // src/attribution.ts
@@ -1174,7 +1155,10 @@ function collectCursorViaTokscale(lookbackDays = LOOKBACK_DAYS) {
1174
1155
  const bin = resolveTokscaleBin();
1175
1156
  if (!bin) return [];
1176
1157
  const today = /* @__PURE__ */ new Date();
1177
- const day = (offset) => new Date(today.getTime() - offset * 864e5).toISOString().slice(0, 10);
1158
+ const day = (offset) => {
1159
+ const d = new Date(today.getTime() - offset * 864e5);
1160
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
1161
+ };
1178
1162
  if (mapTokscaleDay(day(0), runTokscaleDay(bin, day(0))).length === 0) {
1179
1163
  if (mapTokscaleDay(day(1), runTokscaleDay(bin, day(1))).length === 0) {
1180
1164
  return [];
@@ -1243,7 +1227,7 @@ function mapCursorEvents(events) {
1243
1227
  const ms = Number(e.timestamp);
1244
1228
  if (!tu || !Number.isFinite(ms)) continue;
1245
1229
  const d = new Date(ms);
1246
- const date = d.toISOString().slice(0, 10);
1230
+ const date = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
1247
1231
  const model = e.model || "cursor";
1248
1232
  const input = num2(tu.inputTokens);
1249
1233
  const output = num2(tu.outputTokens);
@@ -1333,9 +1317,9 @@ async function collectCursor() {
1333
1317
  }
1334
1318
 
1335
1319
  // src/native/claude.ts
1336
- import { readdir, readFile } from "node:fs/promises";
1320
+ import { readdir, readFile as readFile2 } from "node:fs/promises";
1337
1321
  import { homedir as homedir5 } from "node:os";
1338
- import { join as join6 } from "node:path";
1322
+ import { join as join7 } from "node:path";
1339
1323
 
1340
1324
  // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
1341
1325
  var external_exports = {};
@@ -6780,7 +6764,8 @@ var ConnectorProvider = external_exports.enum([
6780
6764
  "openai-api",
6781
6765
  "openrouter",
6782
6766
  "google-api",
6783
- "cursor"
6767
+ "cursor",
6768
+ "azure-foundry"
6784
6769
  ]);
6785
6770
  var UsageOrigin = external_exports.enum([
6786
6771
  "cli",
@@ -6789,6 +6774,7 @@ var UsageOrigin = external_exports.enum([
6789
6774
  "openrouter",
6790
6775
  "google-api",
6791
6776
  "cursor",
6777
+ "azure-foundry",
6792
6778
  "import"
6793
6779
  ]);
6794
6780
  var DailyUsageEntry = external_exports.object({
@@ -7081,6 +7067,76 @@ var OrgJoinInput = external_exports.object({
7081
7067
  code: external_exports.string().min(1).max(64).optional()
7082
7068
  });
7083
7069
 
7070
+ // src/native/file-cache.ts
7071
+ import { mkdirSync as mkdirSync3, renameSync as renameSync3, writeFileSync as writeFileSync3 } from "node:fs";
7072
+ import { readFile, stat } from "node:fs/promises";
7073
+ import { dirname as dirname3, join as join6 } from "node:path";
7074
+ function nativeCachePath(reader, env = process.env) {
7075
+ const override = env.WHOBURNEDMORE_CONFIG_DIR?.trim();
7076
+ const dir = override || defaultConfigDir();
7077
+ return join6(dir, `native-cache-${reader}.json`);
7078
+ }
7079
+ async function loadCache(path, version) {
7080
+ try {
7081
+ const raw = await readFile(path, "utf8");
7082
+ const parsed = JSON.parse(raw);
7083
+ if (parsed && parsed.v === version && parsed.files && typeof parsed.files === "object") {
7084
+ return parsed.files;
7085
+ }
7086
+ } catch {
7087
+ }
7088
+ return {};
7089
+ }
7090
+ function saveCache(path, version, files) {
7091
+ try {
7092
+ mkdirSync3(dirname3(path), { recursive: true });
7093
+ const tmp = `${path}.tmp-${process.pid}`;
7094
+ writeFileSync3(tmp, JSON.stringify({ v: version, files }));
7095
+ renameSync3(tmp, path);
7096
+ } catch {
7097
+ }
7098
+ }
7099
+ async function readFilesWithCache(opts) {
7100
+ const now = opts.now ?? Date.now;
7101
+ const cached = await loadCache(opts.cachePath, opts.version);
7102
+ const fresh = {};
7103
+ const itemsByFile = [];
7104
+ let filesRead = 0;
7105
+ for (const f of opts.files) {
7106
+ let size;
7107
+ let mtimeMs;
7108
+ try {
7109
+ const s = await stat(f);
7110
+ size = s.size;
7111
+ mtimeMs = s.mtimeMs;
7112
+ } catch {
7113
+ continue;
7114
+ }
7115
+ const hit = cached[f];
7116
+ if (hit && hit.size === size && hit.mtimeMs === mtimeMs) {
7117
+ fresh[f] = hit;
7118
+ itemsByFile.push(hit.items);
7119
+ continue;
7120
+ }
7121
+ if (now() > opts.deadline) {
7122
+ saveCache(opts.cachePath, opts.version, { ...cached, ...fresh });
7123
+ return { itemsByFile: null, filesRead, timedOut: true };
7124
+ }
7125
+ let content;
7126
+ try {
7127
+ content = await readFile(f, "utf8");
7128
+ } catch {
7129
+ continue;
7130
+ }
7131
+ const items = opts.parseFile(content, f);
7132
+ fresh[f] = { size, mtimeMs, items };
7133
+ itemsByFile.push(items);
7134
+ filesRead += 1;
7135
+ }
7136
+ saveCache(opts.cachePath, opts.version, fresh);
7137
+ return { itemsByFile, filesRead, timedOut: false };
7138
+ }
7139
+
7084
7140
  // src/native/claude.ts
7085
7141
  function num3(n) {
7086
7142
  const v = Math.round(Number(n));
@@ -7186,10 +7242,10 @@ function finalizeClaudeEntries(acc) {
7186
7242
  }
7187
7243
  function resolveClaudeConfigRoots(env = process.env) {
7188
7244
  const override = env.CLAUDE_CONFIG_DIR;
7189
- return override ? override.split(",").map((s) => s.trim()).filter(Boolean) : [join6(homedir5(), ".claude"), join6(homedir5(), ".config", "claude")];
7245
+ return override ? override.split(",").map((s) => s.trim()).filter(Boolean) : [join7(homedir5(), ".claude"), join7(homedir5(), ".config", "claude")];
7190
7246
  }
7191
7247
  function resolveClaudeProjectDirs(env = process.env) {
7192
- return resolveClaudeConfigRoots(env).map((r) => join6(r, "projects"));
7248
+ return resolveClaudeConfigRoots(env).map((r) => join7(r, "projects"));
7193
7249
  }
7194
7250
  async function listJsonl(dir) {
7195
7251
  let dirents;
@@ -7200,7 +7256,7 @@ async function listJsonl(dir) {
7200
7256
  }
7201
7257
  const out = [];
7202
7258
  for (const d of dirents) {
7203
- const full = join6(dir, d.name);
7259
+ const full = join7(dir, d.name);
7204
7260
  if (d.isDirectory()) {
7205
7261
  out.push(...await listJsonl(full));
7206
7262
  } else if (d.isFile() && d.name.endsWith(".jsonl")) {
@@ -7209,7 +7265,43 @@ async function listJsonl(dir) {
7209
7265
  }
7210
7266
  return out;
7211
7267
  }
7212
- var NATIVE_READ_BUDGET_MS = 2e4;
7268
+ var NATIVE_READ_BUDGET_MS = 45e3;
7269
+ var CLAUDE_CACHE_VERSION = 1;
7270
+ function toCached(r) {
7271
+ return [
7272
+ r.key,
7273
+ r.hasRealId ? 1 : 0,
7274
+ r.date,
7275
+ r.model,
7276
+ r.inputTokens,
7277
+ r.outputTokens,
7278
+ r.cacheCreationTokens,
7279
+ r.cacheReadTokens
7280
+ ];
7281
+ }
7282
+ function fromCached(t) {
7283
+ return {
7284
+ key: t[0],
7285
+ hasRealId: t[1] === 1,
7286
+ date: t[2],
7287
+ ts: 0,
7288
+ model: t[3],
7289
+ inputTokens: t[4],
7290
+ outputTokens: t[5],
7291
+ cacheCreationTokens: t[6],
7292
+ cacheReadTokens: t[7]
7293
+ };
7294
+ }
7295
+ function parseFileToCache(content, path) {
7296
+ const acc = /* @__PURE__ */ new Map();
7297
+ accumulateClaudeLines(acc, splitLines(content));
7298
+ let synIndex = 0;
7299
+ return [...acc.values()].map(
7300
+ (r) => toCached(
7301
+ r.hasRealId ? r : { ...r, key: `synthetic|${path}|${synIndex += 1}` }
7302
+ )
7303
+ );
7304
+ }
7213
7305
  function* splitLines(content) {
7214
7306
  let start = 0;
7215
7307
  for (let i = 0; i < content.length; i++) {
@@ -7226,23 +7318,35 @@ async function collectClaudeNative(env = process.env, opts = {}) {
7226
7318
  for (const dir of dirs) files.push(...await listJsonl(dir));
7227
7319
  if (files.length === 0) return { entries: [], found: false, filesScanned: 0 };
7228
7320
  const now = opts.now ?? Date.now;
7229
- const deadline = now() + (opts.budgetMs ?? NATIVE_READ_BUDGET_MS);
7321
+ const res = await readFilesWithCache({
7322
+ files,
7323
+ cachePath: opts.cachePath ?? nativeCachePath("claude", env),
7324
+ version: CLAUDE_CACHE_VERSION,
7325
+ parseFile: parseFileToCache,
7326
+ deadline: now() + (opts.budgetMs ?? NATIVE_READ_BUDGET_MS),
7327
+ now
7328
+ });
7329
+ if (!res.itemsByFile) {
7330
+ return {
7331
+ entries: [],
7332
+ found: false,
7333
+ filesScanned: res.filesRead,
7334
+ timedOut: true
7335
+ };
7336
+ }
7230
7337
  const acc = /* @__PURE__ */ new Map();
7231
- let scanned = 0;
7232
- for (const f of files) {
7233
- if (now() > deadline) {
7234
- return { entries: [], found: false, filesScanned: scanned, timedOut: true };
7338
+ for (const items of res.itemsByFile) {
7339
+ for (const t of items) {
7340
+ const r = fromCached(t);
7341
+ const prev = acc.get(r.key);
7342
+ if (!prev || reqTokens(r) > reqTokens(prev)) acc.set(r.key, r);
7235
7343
  }
7236
- let content;
7237
- try {
7238
- content = await readFile(f, "utf8");
7239
- } catch {
7240
- continue;
7241
- }
7242
- accumulateClaudeLines(acc, splitLines(content));
7243
- scanned += 1;
7244
7344
  }
7245
- return { entries: finalizeClaudeEntries(acc), found: true, filesScanned: scanned };
7345
+ return {
7346
+ entries: finalizeClaudeEntries(acc),
7347
+ found: true,
7348
+ filesScanned: res.filesRead
7349
+ };
7246
7350
  }
7247
7351
  async function collectClaudeRequests(env = process.env, opts = {}) {
7248
7352
  const dirs = resolveClaudeProjectDirs(env);
@@ -7258,7 +7362,7 @@ async function collectClaudeRequests(env = process.env, opts = {}) {
7258
7362
  }
7259
7363
  let content;
7260
7364
  try {
7261
- content = await readFile(f, "utf8");
7365
+ content = await readFile2(f, "utf8");
7262
7366
  } catch {
7263
7367
  continue;
7264
7368
  }
@@ -7268,9 +7372,9 @@ async function collectClaudeRequests(env = process.env, opts = {}) {
7268
7372
  }
7269
7373
 
7270
7374
  // src/native/codex.ts
7271
- import { readdir as readdir2, readFile as readFile2 } from "node:fs/promises";
7375
+ import { readdir as readdir2 } from "node:fs/promises";
7272
7376
  import { homedir as homedir6 } from "node:os";
7273
- import { join as join7 } from "node:path";
7377
+ import { join as join8 } from "node:path";
7274
7378
  function num4(n) {
7275
7379
  const v = Math.round(Number(n));
7276
7380
  return Number.isFinite(v) && v > 0 ? v : 0;
@@ -7359,8 +7463,8 @@ function parseCodexRollout(lines) {
7359
7463
  }
7360
7464
  return out;
7361
7465
  }
7362
- function accumulateCodexSession(acc, lines) {
7363
- for (const s of parseCodexRollout(lines)) {
7466
+ function foldCodexSessions(acc, sessions) {
7467
+ for (const s of sessions) {
7364
7468
  const k = `${s.date}|${s.model}`;
7365
7469
  let b = acc.get(k);
7366
7470
  if (!b) {
@@ -7402,8 +7506,8 @@ function finalizeCodexEntries(acc) {
7402
7506
  return entries;
7403
7507
  }
7404
7508
  function resolveCodexSessionsDir(env = process.env) {
7405
- const home = env.CODEX_HOME && env.CODEX_HOME.trim() ? env.CODEX_HOME.trim() : join7(homedir6(), ".codex");
7406
- return join7(home, "sessions");
7509
+ const home = env.CODEX_HOME && env.CODEX_HOME.trim() ? env.CODEX_HOME.trim() : join8(homedir6(), ".codex");
7510
+ return join8(home, "sessions");
7407
7511
  }
7408
7512
  async function listJsonl2(dir) {
7409
7513
  let dirents;
@@ -7414,7 +7518,7 @@ async function listJsonl2(dir) {
7414
7518
  }
7415
7519
  const out = [];
7416
7520
  for (const d of dirents) {
7417
- const full = join7(dir, d.name);
7521
+ const full = join8(dir, d.name);
7418
7522
  if (d.isDirectory()) out.push(...await listJsonl2(full));
7419
7523
  else if (d.isFile() && d.name.endsWith(".jsonl")) out.push(full);
7420
7524
  }
@@ -7430,38 +7534,69 @@ function* splitLines2(content) {
7430
7534
  }
7431
7535
  if (start < content.length) yield content.slice(start);
7432
7536
  }
7433
- var NATIVE_READ_BUDGET_MS2 = 2e4;
7537
+ var NATIVE_READ_BUDGET_MS2 = 45e3;
7538
+ var CODEX_CACHE_VERSION = 1;
7539
+ function toCachedSession(s) {
7540
+ return [
7541
+ s.date,
7542
+ s.model,
7543
+ s.inputTokens,
7544
+ s.outputTokens,
7545
+ s.cacheCreationTokens,
7546
+ s.cacheReadTokens,
7547
+ s.turnCount
7548
+ ];
7549
+ }
7550
+ function fromCachedSession(t) {
7551
+ return {
7552
+ date: t[0],
7553
+ model: t[1],
7554
+ inputTokens: t[2],
7555
+ outputTokens: t[3],
7556
+ cacheCreationTokens: t[4],
7557
+ cacheReadTokens: t[5],
7558
+ turnCount: t[6]
7559
+ };
7560
+ }
7434
7561
  async function collectCodexNative(env = process.env, opts = {}) {
7435
7562
  const dir = resolveCodexSessionsDir(env);
7436
7563
  const files = await listJsonl2(dir);
7437
7564
  if (files.length === 0) return { entries: [], found: false, filesScanned: 0 };
7438
7565
  const now = opts.now ?? Date.now;
7439
- const deadline = now() + (opts.budgetMs ?? NATIVE_READ_BUDGET_MS2);
7566
+ const res = await readFilesWithCache({
7567
+ files,
7568
+ cachePath: opts.cachePath ?? nativeCachePath("codex", env),
7569
+ version: CODEX_CACHE_VERSION,
7570
+ parseFile: (content) => parseCodexRollout(splitLines2(content)).map(toCachedSession),
7571
+ deadline: now() + (opts.budgetMs ?? NATIVE_READ_BUDGET_MS2),
7572
+ now
7573
+ });
7574
+ if (!res.itemsByFile) {
7575
+ return {
7576
+ entries: [],
7577
+ found: false,
7578
+ filesScanned: res.filesRead,
7579
+ timedOut: true
7580
+ };
7581
+ }
7440
7582
  const acc = /* @__PURE__ */ new Map();
7441
- let scanned = 0;
7442
- for (const f of files) {
7443
- if (now() > deadline) {
7444
- return { entries: [], found: false, filesScanned: scanned, timedOut: true };
7445
- }
7446
- let content;
7447
- try {
7448
- content = await readFile2(f, "utf8");
7449
- } catch {
7450
- continue;
7451
- }
7452
- accumulateCodexSession(acc, splitLines2(content));
7453
- scanned += 1;
7583
+ for (const items of res.itemsByFile) {
7584
+ foldCodexSessions(acc, items.map(fromCachedSession));
7454
7585
  }
7455
- return { entries: finalizeCodexEntries(acc), found: true, filesScanned: scanned };
7586
+ return {
7587
+ entries: finalizeCodexEntries(acc),
7588
+ found: true,
7589
+ filesScanned: res.filesRead
7590
+ };
7456
7591
  }
7457
7592
 
7458
7593
  // src/pricing-live.ts
7459
7594
  import { mkdir, readFile as readFile3, rename, writeFile } from "node:fs/promises";
7460
- import { dirname as dirname3, join as join8 } from "node:path";
7595
+ import { dirname as dirname4, join as join9 } from "node:path";
7461
7596
  var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
7462
7597
  var FETCH_TIMEOUT_MS = 5e3;
7463
7598
  function pricingCachePath(dir = defaultConfigDir()) {
7464
- return join8(dir, "pricing-cache.json");
7599
+ return join9(dir, "pricing-cache.json");
7465
7600
  }
7466
7601
  async function readCache(path) {
7467
7602
  try {
@@ -7497,7 +7632,7 @@ async function loadLivePricing(env = process.env, now = Date.now, cachePath = pr
7497
7632
  if (live) {
7498
7633
  setLivePricing(live);
7499
7634
  try {
7500
- await mkdir(dirname3(path), { recursive: true });
7635
+ await mkdir(dirname4(path), { recursive: true });
7501
7636
  const tmp = `${path}.${process.pid}.tmp`;
7502
7637
  await writeFile(tmp, JSON.stringify({ fetchedAt: now(), table: live }));
7503
7638
  await rename(tmp, path);
@@ -7652,7 +7787,7 @@ function resolveCcusageBin() {
7652
7787
  const pkgPath = require3.resolve("ccusage/package.json");
7653
7788
  const pkg = require3("ccusage/package.json");
7654
7789
  const rel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.ccusage ?? "ccusage";
7655
- const binPath = join9(dirname4(pkgPath), rel);
7790
+ const binPath = join10(dirname5(pkgPath), rel);
7656
7791
  if (/\.(c|m)?js$/.test(binPath)) {
7657
7792
  return { cmd: process.execPath, prefixArgs: [binPath] };
7658
7793
  }
@@ -8004,17 +8139,16 @@ function renderDashboardHtml(entries, generatedAt = /* @__PURE__ */ new Date(),
8004
8139
  (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
8140
  ).join("");
8006
8141
  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")}">
8142
+ <div class="connect">
8009
8143
  <div class="connect-row">
8010
8144
  <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>
8145
+ <div class="connect-title">Claim your spot on the public leaderboard</div>
8146
+ <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
8147
  </div>
8014
- <button type="submit">Connect your account \u2192</button>
8148
+ <code class="connect-cmd">npx whoburnedmore</code>
8015
8149
  </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>` : "";
8150
+ <div class="connect-note">Only daily totals are submitted \u2014 never your prompts or code. Background sync keeps your page fresh afterwards.</div>
8151
+ </div>` : "";
8018
8152
  return `<!doctype html>
8019
8153
  <html lang="en">
8020
8154
  <head>
@@ -8074,8 +8208,8 @@ function renderDashboardHtml(entries, generatedAt = /* @__PURE__ */ new Date(),
8074
8208
  @media (min-width: 640px) { .connect-row { flex-direction: row; align-items: center; justify-content: space-between; } }
8075
8209
  .connect-title { font-size: 16px; font-weight: 700; }
8076
8210
  .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; }
8211
+ .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; }
8212
+ .connect-sub a { color: #fed7aa; }
8079
8213
  .connect-note { color: #a8a29e; font-size: 12px; margin-top: 14px; padding-top: 12px; border-top: 1px solid rgba(234,88,12,.25); }
8080
8214
  .connect-note code { color: #fed7aa; background: rgba(0,0,0,.25); padding: 1px 6px; border-radius: 5px; }
8081
8215
  </style>
@@ -8145,20 +8279,24 @@ ${connectCta}
8145
8279
 
8146
8280
  // src/publish.ts
8147
8281
  async function publishLocal(payload, deps) {
8148
- if (!await deps.confirm(" Publish this to the public leaderboard?")) {
8282
+ if (!await deps.confirm(" Publish this to the public leaderboard? (You'll sign in first.)")) {
8149
8283
  deps.log(" Kept local. Nothing left your machine.");
8150
8284
  return false;
8151
8285
  }
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));
8286
+ const auth = await deps.signIn();
8287
+ if (!auth) {
8288
+ deps.log(" Sign-in didn't complete \u2014 kept local. Nothing left your machine.");
8289
+ return false;
8290
+ }
8291
+ const res = await deps.submit(auth.token, payload);
8292
+ deps.log(` Published \u2014 you're on the board: ${sanitizeServerText(res.profileUrl)}`);
8293
+ if (isTrustedWebUrl(res.profileUrl)) {
8294
+ deps.openBrowser(res.profileUrl);
8157
8295
  } else {
8158
8296
  deps.log(" (The server returned an unexpected address, so it was not auto-opened.)");
8159
8297
  }
8160
8298
  deps.log(
8161
- " Sign in there to claim it, or `npx whoburnedmore private` to hide it again."
8299
+ " Manage anytime: `npx whoburnedmore private` hides you, `remove` deletes your data."
8162
8300
  );
8163
8301
  return true;
8164
8302
  }
@@ -8237,14 +8375,11 @@ async function confirm(question) {
8237
8375
  }
8238
8376
  function showLocalDashboard(payload) {
8239
8377
  const dir = defaultConfigDir();
8240
- mkdirSync3(dir, { recursive: true });
8241
- const file = join10(dir, "dashboard.html");
8242
- writeFileSync3(
8378
+ mkdirSync4(dir, { recursive: true });
8379
+ const file = join11(dir, "dashboard.html");
8380
+ writeFileSync4(
8243
8381
  file,
8244
- renderDashboardHtml(payload.entries, /* @__PURE__ */ new Date(), {
8245
- payload,
8246
- webBaseUrl: webBase()
8247
- })
8382
+ renderDashboardHtml(payload.entries, /* @__PURE__ */ new Date(), { webBaseUrl: webBase() })
8248
8383
  );
8249
8384
  console.log();
8250
8385
  console.log(` Local dashboard: ${pc2.cyan(`file://${file}`)}`);
@@ -8300,8 +8435,23 @@ async function run(flags) {
8300
8435
  if (!flags.quiet && process.stdin.isTTY) {
8301
8436
  await publishLocal(payload, {
8302
8437
  confirm,
8303
- ensureAnonKey,
8304
- anonSubmit,
8438
+ signIn: ensureSignedIn,
8439
+ submit: async (token, p) => {
8440
+ const result = await submit(token, p);
8441
+ try {
8442
+ recordSync();
8443
+ } catch {
8444
+ }
8445
+ try {
8446
+ const cfgNow = loadConfig();
8447
+ if (!cfgNow?.deviceBoundAt) {
8448
+ const key = ensureAnonKey();
8449
+ if (await bindDeviceKey(token, key)) recordDeviceBound();
8450
+ }
8451
+ } catch {
8452
+ }
8453
+ return result;
8454
+ },
8305
8455
  openBrowser,
8306
8456
  log: (line) => console.log(pc2.dim(line))
8307
8457
  });
@@ -8478,7 +8628,13 @@ async function submitSignedIn(token, payload, flags, interactive) {
8478
8628
  afterSubmitChores(flags);
8479
8629
  }
8480
8630
  function afterSubmitChores(flags) {
8481
- if (flags.quiet) return;
8631
+ if (flags.quiet) {
8632
+ try {
8633
+ if (autoSyncInstalled()) reconcileAutoSync();
8634
+ } catch {
8635
+ }
8636
+ return;
8637
+ }
8482
8638
  try {
8483
8639
  reconcileAutoSync();
8484
8640
  } catch {
@@ -8566,6 +8722,11 @@ async function runDaemon() {
8566
8722
  runOnce: async () => {
8567
8723
  rotateLogIfLarge();
8568
8724
  await run({ dryRun: false, noSubmit: false, local: false, quiet: true });
8725
+ if (!loadConfig()?.cliToken) {
8726
+ throw new Error(
8727
+ "not linked \u2014 nothing submitted (run `npx whoburnedmore link --token=\u2026` from your signed-in profile first)"
8728
+ );
8729
+ }
8569
8730
  }
8570
8731
  });
8571
8732
  console.log();
@@ -8667,6 +8828,102 @@ async function runVerify() {
8667
8828
  );
8668
8829
  }
8669
8830
  }
8831
+ async function requireSignedIn(commandName) {
8832
+ const cfg = loadConfig();
8833
+ if (cfg?.cliToken) return { token: cfg.cliToken, handle: cfg.handle ?? "" };
8834
+ const healed = await refreshCliTokenFromConfig();
8835
+ if (healed) {
8836
+ saveAuth(void 0, { cliToken: healed.token, handle: healed.handle });
8837
+ return healed;
8838
+ }
8839
+ if (process.stdout.isTTY) return ensureSignedIn();
8840
+ console.log(
8841
+ pc2.yellow(
8842
+ ` Sign in first \u2014 run \`npx whoburnedmore ${commandName}\` in an interactive terminal.`
8843
+ )
8844
+ );
8845
+ return null;
8846
+ }
8847
+ async function runVisibility(listed) {
8848
+ const auth = await requireSignedIn(listed ? "public" : "private");
8849
+ if (!auth) return;
8850
+ let res;
8851
+ try {
8852
+ res = await setVisibility(auth.token, listed);
8853
+ } catch (err) {
8854
+ if (err instanceof UnauthorizedError) {
8855
+ clearAuth();
8856
+ console.log(
8857
+ pc2.yellow(
8858
+ ` Your sign-in expired \u2014 run \`npx whoburnedmore ${listed ? "public" : "private"}\` again to retry.`
8859
+ )
8860
+ );
8861
+ return;
8862
+ }
8863
+ throw err;
8864
+ }
8865
+ if (!listed) {
8866
+ console.log(" \u2713 You're off the public leaderboard. `npx whoburnedmore public` puts you back.");
8867
+ return;
8868
+ }
8869
+ if (res.listed) {
8870
+ console.log(" \u2713 You're back on the public leaderboard.");
8871
+ } else if (!res.eligible) {
8872
+ console.log(
8873
+ pc2.yellow(" You're signed in, but not on the leaderboard yet \u2014 it lists accounts with a social handle.")
8874
+ );
8875
+ console.log(
8876
+ pc2.dim(" Add your X, GitHub or Instagram on your profile at whoburnedmore.com, then you're on.")
8877
+ );
8878
+ } else {
8879
+ console.log(" \u2713 Leaderboard listing is on.");
8880
+ }
8881
+ }
8882
+ async function runRemove() {
8883
+ if (!process.stdin.isTTY) {
8884
+ console.log(
8885
+ pc2.yellow(" `remove` deletes data, so it needs an interactive terminal to confirm.")
8886
+ );
8887
+ console.log(pc2.dim(" Run `npx whoburnedmore remove` in a real terminal."));
8888
+ process.exitCode = 1;
8889
+ return;
8890
+ }
8891
+ const auth = await requireSignedIn("remove");
8892
+ if (!auth) return;
8893
+ const who = auth.handle ? `@${sanitizeServerText(auth.handle)}` : "your account";
8894
+ console.log(
8895
+ ` This deletes ALL usage data stored for ${who} on whoburnedmore.com and turns off this machine's background sync.`
8896
+ );
8897
+ console.log(
8898
+ pc2.dim(" Your local logs are untouched, and the account itself survives (delete it from your dashboard settings on the web).")
8899
+ );
8900
+ if (!await confirm(" Delete your usage data?")) {
8901
+ console.log(pc2.dim(" Cancelled \u2014 nothing was deleted."));
8902
+ return;
8903
+ }
8904
+ let res;
8905
+ try {
8906
+ res = await removeUsage(auth.token);
8907
+ } catch (err) {
8908
+ if (err instanceof UnauthorizedError) {
8909
+ clearAuth();
8910
+ console.log(
8911
+ pc2.yellow(" Your sign-in expired \u2014 run `npx whoburnedmore remove` again to retry.")
8912
+ );
8913
+ return;
8914
+ }
8915
+ throw err;
8916
+ }
8917
+ try {
8918
+ uninstallAutoSync();
8919
+ } catch {
8920
+ }
8921
+ clearAuth();
8922
+ console.log(
8923
+ pc2.green(` \u2713 Deleted your usage data (${res.deletedDays} day${res.deletedDays === 1 ? "" : "s"}) and turned off background sync.`)
8924
+ );
8925
+ console.log(pc2.dim(" Run `npx whoburnedmore` anytime to start fresh."));
8926
+ }
8670
8927
  async function main() {
8671
8928
  const major = Number(process.versions.node.split(".")[0]);
8672
8929
  if (major < 20) {
@@ -8711,28 +8968,12 @@ async function main() {
8711
8968
  break;
8712
8969
  }
8713
8970
  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
- );
8971
+ case "public":
8972
+ await runVisibility(command === "public");
8724
8973
  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.");
8974
+ case "remove":
8975
+ await runRemove();
8734
8976
  break;
8735
- }
8736
8977
  case "install-sync":
8737
8978
  console.log(` ${installAutoSync()}`);
8738
8979
  break;
@@ -8764,9 +9005,9 @@ function printHelp() {
8764
9005
  npx whoburnedmore --no-submit collect locally, send nothing (no dashboard)
8765
9006
  npx whoburnedmore link --token=TOKEN link this server/VM to your signed-in account
8766
9007
  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
9008
+ npx whoburnedmore private take yourself off the public leaderboard
9009
+ npx whoburnedmore public put yourself back on it
9010
+ npx whoburnedmore remove delete your usage data and stop background sync
8770
9011
  npx whoburnedmore verify delisted? re-verify your usage to get back on (sends a detailed breakdown)
8771
9012
  npx whoburnedmore status check background-sync health (last sync, staleness)
8772
9013
  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.14",
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": {
@@ -38,11 +38,10 @@
38
38
  "homepage": "https://whoburnedmore.com",
39
39
  "repository": {
40
40
  "type": "git",
41
- "url": "git+https://github.com/amiinwani/whoburnedmore.git",
42
- "directory": "packages/cli"
41
+ "url": "git+https://github.com/arhxam/whoburnedmore.git"
43
42
  },
44
43
  "bugs": {
45
- "url": "https://github.com/amiinwani/whoburnedmore/issues"
44
+ "url": "https://github.com/arhxam/whoburnedmore/issues"
46
45
  },
47
46
  "keywords": [
48
47
  "ai",