whoburnedmore 0.9.2 → 0.9.5

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 +957 -165
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -7,10 +7,11 @@ var __export = (target, all) => {
7
7
 
8
8
  // src/index.ts
9
9
  import { spawn } from "node:child_process";
10
+ import { createHash } from "node:crypto";
10
11
  import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "node:fs";
11
12
  import { createRequire as createRequire4 } from "node:module";
12
13
  import { platform as platform3 } from "node:os";
13
- import { join as join7 } from "node:path";
14
+ import { join as join9 } from "node:path";
14
15
  import { createInterface } from "node:readline/promises";
15
16
  import pc2 from "picocolors";
16
17
 
@@ -95,13 +96,18 @@ async function readJson(res) {
95
96
  };
96
97
  }
97
98
  }
98
- async function post(path, body) {
99
+ async function post(path, body, token) {
100
+ const headers = { "Content-Type": "application/json" };
101
+ if (token) headers.Authorization = `Bearer ${token}`;
99
102
  let res;
100
103
  try {
101
104
  res = await fetch(`${apiBase()}${path}`, {
102
105
  method: "POST",
103
- headers: { "Content-Type": "application/json" },
104
- body: JSON.stringify(body)
106
+ headers,
107
+ body: JSON.stringify(body),
108
+ // Bound the request so a slow/black-holing/hostile server can't hang the CLI
109
+ // — or the unattended 15-minute background sync — indefinitely.
110
+ signal: AbortSignal.timeout(3e4)
105
111
  });
106
112
  } catch {
107
113
  throw new Error(
@@ -110,6 +116,17 @@ async function post(path, body) {
110
116
  }
111
117
  return { status: res.status, body: await readJson(res) };
112
118
  }
119
+ async function verifyUsage(token, payload) {
120
+ const { status, body } = await post("/v1/verify", payload, token);
121
+ if (status === 401) throw new UnauthorizedError();
122
+ if (status !== 200) {
123
+ const err = body;
124
+ const details = err.details?.length ? `
125
+ - ${err.details.join("\n - ")}` : "";
126
+ throw new Error(`${err.error ?? "verification failed"}${details}`);
127
+ }
128
+ return body;
129
+ }
113
130
  async function anonSubmit(anonKey, payload) {
114
131
  const { status, body } = await post("/v1/anon/submit", { ...payload, anonKey });
115
132
  if (status !== 200) {
@@ -120,6 +137,45 @@ async function anonSubmit(anonKey, payload) {
120
137
  }
121
138
  return body;
122
139
  }
140
+ var UnauthorizedError = class extends Error {
141
+ constructor(message = "your sign-in has expired") {
142
+ super(message);
143
+ this.name = "UnauthorizedError";
144
+ }
145
+ };
146
+ async function deviceStart() {
147
+ const { status, body } = await post(
148
+ "/v1/auth/device",
149
+ {}
150
+ );
151
+ if (status !== 200) {
152
+ throw new Error(
153
+ body.error ?? `sign-in failed (HTTP ${status})`
154
+ );
155
+ }
156
+ return body;
157
+ }
158
+ async function devicePoll(deviceCode) {
159
+ const { status, body } = await post(
160
+ "/v1/auth/device/token",
161
+ { deviceCode }
162
+ );
163
+ if (status !== 200) {
164
+ throw new Error(`sign-in failed (HTTP ${status})`);
165
+ }
166
+ return body;
167
+ }
168
+ async function submit(token, payload) {
169
+ const { status, body } = await post("/v1/submit", payload, token);
170
+ if (status === 401) throw new UnauthorizedError();
171
+ if (status !== 200) {
172
+ const err = body;
173
+ const details = err.details?.length ? `
174
+ - ${err.details.join("\n - ")}` : "";
175
+ throw new Error(`${err.error ?? `submit failed (HTTP ${status})`}${details}`);
176
+ }
177
+ return body;
178
+ }
123
179
  function claimUrl(dashboardUrl, anonKey) {
124
180
  return `${dashboardUrl}#k=${encodeURIComponent(anonKey)}`;
125
181
  }
@@ -144,7 +200,8 @@ async function anonRemove(anonKey) {
144
200
  const res = await fetch(`${apiBase()}/v1/anon`, {
145
201
  method: "DELETE",
146
202
  headers: { "Content-Type": "application/json" },
147
- body: JSON.stringify({ anonKey })
203
+ body: JSON.stringify({ anonKey }),
204
+ signal: AbortSignal.timeout(3e4)
148
205
  });
149
206
  if (res.status !== 200) {
150
207
  const b = await res.json().catch(() => ({}));
@@ -167,8 +224,8 @@ import {
167
224
  existsSync as existsSync2,
168
225
  mkdirSync as mkdirSync2,
169
226
  readFileSync as readFileSync2,
170
- renameSync,
171
- rmSync,
227
+ renameSync as renameSync2,
228
+ rmSync as rmSync2,
172
229
  statSync,
173
230
  writeFileSync as writeFileSync2
174
231
  } from "node:fs";
@@ -182,6 +239,8 @@ import {
182
239
  existsSync,
183
240
  mkdirSync,
184
241
  readFileSync,
242
+ renameSync,
243
+ rmSync,
185
244
  writeFileSync
186
245
  } from "node:fs";
187
246
  import { homedir } from "node:os";
@@ -198,6 +257,8 @@ function loadConfig(dir = defaultConfigDir()) {
198
257
  const parsed = JSON.parse(readFileSync(file, "utf8"));
199
258
  const config = {};
200
259
  if (typeof parsed.anonKey === "string") config.anonKey = parsed.anonKey;
260
+ if (typeof parsed.cliToken === "string") config.cliToken = parsed.cliToken;
261
+ if (typeof parsed.handle === "string") config.handle = parsed.handle;
201
262
  if (typeof parsed.lastSyncAt === "number" && Number.isFinite(parsed.lastSyncAt))
202
263
  config.lastSyncAt = parsed.lastSyncAt;
203
264
  if (typeof parsed.launchNotificationDeliveredAt === "number" && Number.isFinite(parsed.launchNotificationDeliveredAt)) {
@@ -211,10 +272,20 @@ function loadConfig(dir = defaultConfigDir()) {
211
272
  function saveConfig(dir = defaultConfigDir(), config = {}) {
212
273
  mkdirSync(dir, { recursive: true });
213
274
  const file = join(dir, "config.json");
214
- writeFileSync(file, JSON.stringify(config, null, 2), { mode: 384 });
275
+ const tmp = join(dir, `config.json.${process.pid}.tmp`);
215
276
  try {
216
- chmodSync(file, 384);
217
- } catch {
277
+ writeFileSync(tmp, JSON.stringify(config, null, 2), { mode: 384 });
278
+ try {
279
+ chmodSync(tmp, 384);
280
+ } catch {
281
+ }
282
+ renameSync(tmp, file);
283
+ } catch (err) {
284
+ try {
285
+ rmSync(tmp, { force: true });
286
+ } catch {
287
+ }
288
+ throw err;
218
289
  }
219
290
  }
220
291
  function ensureAnonKey(dir = defaultConfigDir()) {
@@ -232,6 +303,16 @@ function recordLaunchNotificationDelivered(dir = defaultConfigDir(), when = Date
232
303
  const config = loadConfig(dir) ?? {};
233
304
  saveConfig(dir, { ...config, launchNotificationDeliveredAt: when });
234
305
  }
306
+ function saveAuth(dir = defaultConfigDir(), auth = { cliToken: "" }) {
307
+ const config = loadConfig(dir) ?? {};
308
+ saveConfig(dir, { ...config, cliToken: auth.cliToken, handle: auth.handle });
309
+ }
310
+ function clearAuth(dir = defaultConfigDir()) {
311
+ const config = loadConfig(dir);
312
+ if (!config?.cliToken && !config?.handle) return;
313
+ const { cliToken: _t, handle: _h, ...rest } = config;
314
+ saveConfig(dir, rest);
315
+ }
235
316
 
236
317
  // src/autosync.ts
237
318
  var SYNC_INTERVAL_MINUTES = 15;
@@ -250,11 +331,55 @@ var STABLE_NPM_CANDIDATES = [
250
331
  "/usr/bin/npm"
251
332
  ];
252
333
  var LATEST_PACKAGE_SPEC = "whoburnedmore@latest";
334
+ var SYNC_PATH_DIRS = [
335
+ "/opt/homebrew/bin",
336
+ "/usr/local/bin",
337
+ "/usr/bin",
338
+ "/bin",
339
+ "/usr/sbin",
340
+ "/sbin"
341
+ ];
342
+ function syncPathEnv(npmPath = resolveNpmPath()) {
343
+ const dir = dirname(npmPath);
344
+ const dirs = [];
345
+ if (dir && dir !== "." && dir !== "/" && dir !== npmPath) dirs.push(dir);
346
+ for (const d of SYNC_PATH_DIRS) {
347
+ if (!dirs.includes(d)) dirs.push(d);
348
+ }
349
+ return dirs.join(":");
350
+ }
351
+ var FORWARDED_ENV_VARS = [
352
+ "WHOBURNEDMORE_CONFIG_DIR",
353
+ // identity: where the anonKey lives
354
+ "WHOBURNEDMORE_API",
355
+ // endpoint: where submits go
356
+ "WHOBURNEDMORE_WEB",
357
+ // dashboard URL shown to the user
358
+ "XDG_CONFIG_HOME",
359
+ // base for the default config dir
360
+ "CLAUDE_CONFIG_DIR"
361
+ // a primary usage-collection source
362
+ ];
363
+ function syncEnv(opts) {
364
+ const env = opts?.env ?? process.env;
365
+ const pairs = [["PATH", syncPathEnv(opts?.npmPath)]];
366
+ for (const key of FORWARDED_ENV_VARS) {
367
+ const value = env[key];
368
+ if (typeof value === "string" && value.length > 0 && !/[\r\n]/.test(value)) {
369
+ pairs.push([key, value]);
370
+ }
371
+ }
372
+ return pairs;
373
+ }
253
374
  function syncLogPath() {
254
375
  return join2(defaultConfigDir(), "sync.log");
255
376
  }
256
- function buildLaunchdPlist(commandArgs = syncCommandArgs(), logPath = syncLogPath()) {
377
+ function buildLaunchdPlist(commandArgs = syncCommandArgs(), logPath = syncLogPath(), envPairs = syncEnv({ npmPath: commandArgs[0] })) {
257
378
  const programArguments = commandArgs.map((arg) => ` <string>${xmlEscape(arg)}</string>`).join("\n");
379
+ const envEntries = envPairs.map(
380
+ ([k, v]) => ` <key>${xmlEscape(k)}</key>
381
+ <string>${xmlEscape(v)}</string>`
382
+ ).join("\n");
258
383
  return `<?xml version="1.0" encoding="UTF-8"?>
259
384
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
260
385
  <plist version="1.0">
@@ -265,6 +390,15 @@ function buildLaunchdPlist(commandArgs = syncCommandArgs(), logPath = syncLogPat
265
390
  <array>
266
391
  ${programArguments}
267
392
  </array>
393
+ <!-- launchd runs jobs with a minimal PATH (/usr/bin:/bin:/usr/sbin:/sbin)
394
+ that excludes Homebrew. npm's shebang (and the package bin npm exec
395
+ spawns) is #!/usr/bin/env node, so node must be on PATH or every tick
396
+ dies with "env: node: No such file or directory". We also forward the
397
+ user's identity/endpoint/collection env so background == foreground. -->
398
+ <key>EnvironmentVariables</key>
399
+ <dict>
400
+ ${envEntries}
401
+ </dict>
268
402
  <key>StartInterval</key>
269
403
  <integer>${SYNC_INTERVAL_MINUTES * 60}</integer>
270
404
  <!-- Run once right after login/reboot so a machine that was off (or asleep)
@@ -398,7 +532,10 @@ function cronSchedule(mins = SYNC_INTERVAL_MINUTES) {
398
532
  }
399
533
  function expectedLinuxCronLine(opts) {
400
534
  const command = syncCommandArgs(opts?.npmPath).map(shellQuote).join(" ");
401
- return `${cronSchedule()} ${command} >${shellQuote(opts?.logPath ?? syncLogPath())} 2>&1`;
535
+ const envPrefix = syncEnv({ npmPath: opts?.npmPath ?? resolveNpmPath() }).map(([k, v]) => `${k}=${shellQuote(v)}`).join(" ");
536
+ const redirect = `>${shellQuote(opts?.logPath ?? syncLogPath())} 2>&1`;
537
+ const commandField = `${envPrefix} ${command} ${redirect}`.replaceAll("%", "\\%");
538
+ return `${cronSchedule()} ${commandField}`;
402
539
  }
403
540
  var SYSTEMD_UNIT = "whoburnedmore-sync";
404
541
  function systemdUserDir() {
@@ -412,15 +549,17 @@ function systemdTimerPath() {
412
549
  return join2(systemdUserDir(), `${SYSTEMD_UNIT}.timer`);
413
550
  }
414
551
  function systemdQuote(value) {
415
- return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
552
+ return `"${value.replaceAll("\\", "\\\\").replaceAll("%", "%%").replaceAll('"', '\\"')}"`;
416
553
  }
417
- function buildSystemdService(commandArgs = syncCommandArgs()) {
554
+ function buildSystemdService(commandArgs = syncCommandArgs(), envPairs = syncEnv({ npmPath: commandArgs[0] })) {
418
555
  const execStart = commandArgs.map(systemdQuote).join(" ");
556
+ const envLines = envPairs.map(([k, v]) => `Environment=${systemdQuote(`${k}=${v}`)}`).join("\n");
419
557
  return `[Unit]
420
558
  Description=whoburnedmore background token-usage sync
421
559
 
422
560
  [Service]
423
561
  Type=oneshot
562
+ ${envLines}
424
563
  ExecStart=${execStart}
425
564
  `;
426
565
  }
@@ -478,8 +617,8 @@ function tryInstallSystemd() {
478
617
  { stdio: "ignore" }
479
618
  );
480
619
  if (res.status !== 0) {
481
- rmSync(systemdServicePath(), { force: true });
482
- rmSync(systemdTimerPath(), { force: true });
620
+ rmSync2(systemdServicePath(), { force: true });
621
+ rmSync2(systemdTimerPath(), { force: true });
483
622
  return null;
484
623
  }
485
624
  return `systemd user timer installed, syncing every ${syncIntervalLabel()} (run \`loginctl enable-linger\` to keep syncing while logged out)`;
@@ -490,7 +629,7 @@ function uninstallAutoSync() {
490
629
  const plistPath = launchAgentPath();
491
630
  if (existsSync2(plistPath)) {
492
631
  spawnSync("launchctl", ["unload", plistPath], { stdio: "ignore" });
493
- rmSync(plistPath, { force: true });
632
+ rmSync2(plistPath, { force: true });
494
633
  }
495
634
  return "launchd agent removed";
496
635
  }
@@ -508,8 +647,8 @@ function uninstallAutoSync() {
508
647
  ["--user", "disable", "--now", `${SYSTEMD_UNIT}.timer`],
509
648
  { stdio: "ignore" }
510
649
  );
511
- rmSync(systemdServicePath(), { force: true });
512
- rmSync(systemdTimerPath(), { force: true });
650
+ rmSync2(systemdServicePath(), { force: true });
651
+ rmSync2(systemdTimerPath(), { force: true });
513
652
  spawnSync("systemctl", ["--user", "daemon-reload"], { stdio: "ignore" });
514
653
  removed = true;
515
654
  }
@@ -585,7 +724,7 @@ function rotateLogIfLarge(path = syncLogPath(), capBytes = 256 * 1024) {
585
724
  try {
586
725
  if (!existsSync2(path)) return false;
587
726
  if (statSync(path).size <= capBytes) return false;
588
- renameSync(path, `${path}.1`);
727
+ renameSync2(path, `${path}.1`);
589
728
  return true;
590
729
  } catch {
591
730
  return false;
@@ -673,46 +812,23 @@ async function daemonLoop(deps) {
673
812
  // src/collect.ts
674
813
  import { execFile } from "node:child_process";
675
814
  import { createRequire as createRequire3 } from "node:module";
676
- import { dirname as dirname3, join as join6 } from "node:path";
815
+ import { dirname as dirname3, join as join8 } from "node:path";
677
816
  import { promisify } from "node:util";
678
817
 
679
818
  // src/attribution.ts
680
819
  import { readFileSync as readFileSync3, readdirSync, statSync as statSync2 } from "node:fs";
681
820
  import { homedir as homedir3 } from "node:os";
682
- import { basename, join as join3 } from "node:path";
683
-
684
- // src/pricing.ts
685
- var TABLE = [
686
- { match: /opus/i, price: { in: 15, out: 75, cacheWrite: 18.75, cacheRead: 1.5 } },
687
- { match: /sonnet/i, price: { in: 3, out: 15, cacheWrite: 3.75, cacheRead: 0.3 } },
688
- { match: /haiku/i, price: { in: 0.8, out: 4, cacheWrite: 1, cacheRead: 0.08 } },
689
- { match: /fable/i, price: { in: 15, out: 75, cacheWrite: 18.75, cacheRead: 1.5 } },
690
- { match: /gpt-4o|gpt-4\.1/i, price: { in: 2.5, out: 10, cacheWrite: 2.5, cacheRead: 1.25 } },
691
- { match: /gpt-5|o3|o4|codex/i, price: { in: 1.25, out: 10, cacheWrite: 1.25, cacheRead: 0.125 } },
692
- { match: /gemini.*flash/i, price: { in: 0.15, out: 0.6, cacheWrite: 0.15, cacheRead: 0.0375 } },
693
- { match: /gemini/i, price: { in: 1.25, out: 5, cacheWrite: 1.25, cacheRead: 0.31 } }
694
- ];
695
- function estimateCostUSD(model, t) {
696
- const row = TABLE.find((r) => r.match.test(model));
697
- if (!row) return 0;
698
- const p = row.price;
699
- const usd = (t.inputTokens * p.in + t.outputTokens * p.out + t.cacheCreationTokens * p.cacheWrite + t.cacheReadTokens * p.cacheRead) / 1e6;
700
- return usd > 0 ? usd : 0;
701
- }
702
-
703
- // src/attribution.ts
821
+ import { join as join3 } from "node:path";
704
822
  var CLAUDE_PROJECTS = join3(homedir3(), ".claude", "projects");
705
823
  var CODEX_SESSIONS = join3(homedir3(), ".codex", "sessions");
706
824
  var MAX_FILES = 5e3;
707
825
  var MAX_FILE_BYTES = 64 * 1024 * 1024;
708
826
  var TIME_BUDGET_MS = 12e3;
709
827
  var MAX_STATS = 300;
710
- var MAX_PROJECTS = 500;
711
828
  function createAccumulator() {
712
829
  return {
713
830
  tools: /* @__PURE__ */ new Map(),
714
831
  skills: /* @__PURE__ */ new Map(),
715
- projects: /* @__PURE__ */ new Map(),
716
832
  agent: {
717
833
  messageCount: 0,
718
834
  subagentMessages: 0,
@@ -720,7 +836,6 @@ function createAccumulator() {
720
836
  totalTokens: 0,
721
837
  userMessageCount: 0
722
838
  },
723
- titles: /* @__PURE__ */ new Map(),
724
839
  sessionMessages: /* @__PURE__ */ new Map()
725
840
  };
726
841
  }
@@ -761,9 +876,6 @@ function processRecord(rec, acc, ctx) {
761
876
  sk.tokens += recTokens;
762
877
  acc.skills.set(s, sk);
763
878
  }
764
- if (r.type === "ai-title" && typeof r.aiTitle === "string" && r.aiTitle && typeof r.sessionId === "string" && r.sessionId) {
765
- acc.titles.set(r.sessionId, r.aiTitle.slice(0, 200));
766
- }
767
879
  const content = r.message?.content;
768
880
  if (!Array.isArray(content)) return;
769
881
  const isAssistant = r.type === "assistant" || r.message?.role === "assistant";
@@ -800,25 +912,6 @@ function processRecord(rec, acc, ctx) {
800
912
  (acc.sessionMessages.get(r.sessionId) ?? 0) + 1
801
913
  );
802
914
  }
803
- if (typeof r.cwd === "string" && r.cwd && tokens > 0) {
804
- const name = basename(r.cwd).slice(0, 128) || "unknown";
805
- const model = typeof r.message?.model === "string" ? r.message.model : "unknown";
806
- const u = r.message?.usage;
807
- const num3 = (v) => {
808
- const x = Math.round(Number(v));
809
- return Number.isFinite(x) && x > 0 ? x : 0;
810
- };
811
- const cost = estimateCostUSD(model, {
812
- inputTokens: num3(u?.input_tokens),
813
- outputTokens: num3(u?.output_tokens),
814
- cacheCreationTokens: num3(u?.cache_creation_input_tokens),
815
- cacheReadTokens: num3(u?.cache_read_input_tokens)
816
- });
817
- const p = acc.projects.get(name) ?? { tokens: 0, costUSD: 0 };
818
- p.tokens += tokens;
819
- p.costUSD += cost;
820
- acc.projects.set(name, p);
821
- }
822
915
  } else if (isUser) {
823
916
  for (const block of content) {
824
917
  if (block && typeof block === "object" && block.type === "tool_result" && block.is_error === true) {
@@ -844,20 +937,11 @@ function toToolStats(map) {
844
937
  return base;
845
938
  });
846
939
  }
847
- function toProjectStats(map) {
848
- return [...map.entries()].map(([name, v]) => ({
849
- name,
850
- tokens: v.tokens,
851
- costUSD: Number(v.costUSD.toFixed(6))
852
- })).filter((p) => p.tokens > 0).sort((a, b) => b.tokens - a.tokens).slice(0, MAX_PROJECTS);
853
- }
854
940
  function accumulatorToResult(acc) {
855
941
  return {
856
942
  tools: toToolStats(acc.tools),
857
943
  skills: toSkillStats(acc.skills),
858
- projects: toProjectStats(acc.projects),
859
944
  agent: { ...acc.agent },
860
- titles: acc.titles,
861
945
  sessionMessages: acc.sessionMessages,
862
946
  complete: true
863
947
  };
@@ -867,7 +951,7 @@ function numTok(v) {
867
951
  return Number.isFinite(x) && x > 0 ? x : 0;
868
952
  }
869
953
  function createCodexContext() {
870
- return { cwd: "", model: "unknown", pending: [] };
954
+ return { pending: [] };
871
955
  }
872
956
  function processCodexRecord(rec, acc, ctx) {
873
957
  if (!rec || typeof rec !== "object") return;
@@ -875,11 +959,7 @@ function processCodexRecord(rec, acc, ctx) {
875
959
  if (!r.payload || typeof r.payload !== "object") return;
876
960
  const pl = r.payload;
877
961
  const ptype = pl.type;
878
- if (r.type === "session_meta" || r.type === "turn_context") {
879
- if (typeof pl.cwd === "string" && pl.cwd) ctx.cwd = pl.cwd;
880
- if (typeof pl.model === "string" && pl.model) ctx.model = pl.model;
881
- return;
882
- }
962
+ if (r.type === "session_meta" || r.type === "turn_context") return;
883
963
  if (ptype === "function_call" || ptype === "custom_tool_call" || ptype === "local_shell_call") {
884
964
  const raw = typeof pl.name === "string" ? pl.name : ptype === "local_shell_call" ? "local_shell" : "";
885
965
  const name = raw.slice(0, 128);
@@ -905,19 +985,6 @@ function processCodexRecord(rec, acc, ctx) {
905
985
  }
906
986
  acc.agent.messageCount += 1;
907
987
  acc.agent.totalTokens += tokens;
908
- if (ctx.cwd) {
909
- const name = basename(ctx.cwd).slice(0, 128) || "unknown";
910
- const cost = estimateCostUSD(ctx.model, {
911
- inputTokens,
912
- outputTokens,
913
- cacheCreationTokens: 0,
914
- cacheReadTokens
915
- });
916
- const proj = acc.projects.get(name) ?? { tokens: 0, costUSD: 0 };
917
- proj.tokens += tokens;
918
- proj.costUSD += cost;
919
- acc.projects.set(name, proj);
920
- }
921
988
  const per = ctx.pending.length > 0 ? Math.floor(tokens / ctx.pending.length) : 0;
922
989
  for (const tu of ctx.pending) {
923
990
  const t = acc.tools.get(tu.name);
@@ -1272,6 +1339,404 @@ async function collectCursor() {
1272
1339
  return { entries: [], blocks: [], found: false };
1273
1340
  }
1274
1341
 
1342
+ // src/native/claude.ts
1343
+ import { readdir, readFile } from "node:fs/promises";
1344
+ import { homedir as homedir5 } from "node:os";
1345
+ import { join as join6 } from "node:path";
1346
+
1347
+ // src/pricing.ts
1348
+ var TABLE = [
1349
+ { match: /opus/i, price: { in: 15, out: 75, cacheWrite: 18.75, cacheRead: 1.5 } },
1350
+ { match: /sonnet/i, price: { in: 3, out: 15, cacheWrite: 3.75, cacheRead: 0.3 } },
1351
+ { match: /haiku/i, price: { in: 0.8, out: 4, cacheWrite: 1, cacheRead: 0.08 } },
1352
+ { match: /fable/i, price: { in: 15, out: 75, cacheWrite: 18.75, cacheRead: 1.5 } },
1353
+ { match: /gpt-4o|gpt-4\.1/i, price: { in: 2.5, out: 10, cacheWrite: 2.5, cacheRead: 1.25 } },
1354
+ { match: /gpt-5|o3|o4|codex/i, price: { in: 1.25, out: 10, cacheWrite: 1.25, cacheRead: 0.125 } },
1355
+ { match: /gemini.*flash/i, price: { in: 0.15, out: 0.6, cacheWrite: 0.15, cacheRead: 0.0375 } },
1356
+ { match: /gemini/i, price: { in: 1.25, out: 5, cacheWrite: 1.25, cacheRead: 0.31 } }
1357
+ ];
1358
+ function estimateCostUSD(model, t) {
1359
+ const row = TABLE.find((r) => r.match.test(model));
1360
+ if (!row) return 0;
1361
+ const p = row.price;
1362
+ const usd = (t.inputTokens * p.in + t.outputTokens * p.out + t.cacheCreationTokens * p.cacheWrite + t.cacheReadTokens * p.cacheRead) / 1e6;
1363
+ return usd > 0 ? usd : 0;
1364
+ }
1365
+
1366
+ // src/native/claude.ts
1367
+ function num3(n) {
1368
+ const v = Math.round(Number(n));
1369
+ return Number.isFinite(v) && v > 0 ? v : 0;
1370
+ }
1371
+ function reqTokens(r) {
1372
+ return r.inputTokens + r.outputTokens + r.cacheCreationTokens + r.cacheReadTokens;
1373
+ }
1374
+ function localDate(iso) {
1375
+ const t = Date.parse(iso);
1376
+ if (!Number.isFinite(t)) return null;
1377
+ const d = new Date(t);
1378
+ const y = d.getFullYear();
1379
+ const m = String(d.getMonth() + 1).padStart(2, "0");
1380
+ const day = String(d.getDate()).padStart(2, "0");
1381
+ return `${y}-${m}-${day}`;
1382
+ }
1383
+ var syntheticCounter = 0;
1384
+ function parseClaudeLine(raw) {
1385
+ const trimmed = raw.trim();
1386
+ if (!trimmed) return null;
1387
+ let obj;
1388
+ try {
1389
+ obj = JSON.parse(trimmed);
1390
+ } catch {
1391
+ return null;
1392
+ }
1393
+ const message = obj.message;
1394
+ if (!message || typeof message !== "object") return null;
1395
+ const usage = message.usage;
1396
+ if (!usage || typeof usage !== "object") return null;
1397
+ if (message.role !== void 0 && message.role !== "assistant") return null;
1398
+ const date = localDate(String(obj.timestamp ?? ""));
1399
+ if (!date) return null;
1400
+ const tsParsed = Date.parse(String(obj.timestamp ?? ""));
1401
+ const ts = Number.isFinite(tsParsed) ? tsParsed : 0;
1402
+ const messageId = typeof message.id === "string" ? message.id : "";
1403
+ const requestId = typeof obj.requestId === "string" ? obj.requestId : "";
1404
+ const hasRealId = messageId !== "" || requestId !== "";
1405
+ const key = hasRealId ? `${messageId}|${requestId}` : `synthetic|${date}|${syntheticCounter += 1}`;
1406
+ return {
1407
+ key,
1408
+ hasRealId,
1409
+ date,
1410
+ ts,
1411
+ model: typeof message.model === "string" ? message.model : "unknown",
1412
+ inputTokens: num3(usage.input_tokens),
1413
+ outputTokens: num3(usage.output_tokens),
1414
+ cacheCreationTokens: num3(usage.cache_creation_input_tokens),
1415
+ cacheReadTokens: num3(usage.cache_read_input_tokens)
1416
+ };
1417
+ }
1418
+ function accumulateClaudeLines(acc, lines) {
1419
+ for (const line of lines) {
1420
+ const r = parseClaudeLine(line);
1421
+ if (!r) continue;
1422
+ const prev = acc.get(r.key);
1423
+ if (!prev || reqTokens(r) > reqTokens(prev)) acc.set(r.key, r);
1424
+ }
1425
+ }
1426
+ function finalizeClaudeEntries(acc) {
1427
+ const byDayModel = /* @__PURE__ */ new Map();
1428
+ for (const r of acc.values()) {
1429
+ const k = `${r.date}|${r.model}`;
1430
+ let b = byDayModel.get(k);
1431
+ if (!b) {
1432
+ b = {
1433
+ date: r.date,
1434
+ model: r.model,
1435
+ inputTokens: 0,
1436
+ outputTokens: 0,
1437
+ cacheCreationTokens: 0,
1438
+ cacheReadTokens: 0,
1439
+ requestCount: 0
1440
+ };
1441
+ byDayModel.set(k, b);
1442
+ }
1443
+ b.inputTokens += r.inputTokens;
1444
+ b.outputTokens += r.outputTokens;
1445
+ b.cacheCreationTokens += r.cacheCreationTokens;
1446
+ b.cacheReadTokens += r.cacheReadTokens;
1447
+ if (r.hasRealId) b.requestCount += 1;
1448
+ }
1449
+ const entries = [];
1450
+ for (const b of byDayModel.values()) {
1451
+ const tokens = b.inputTokens + b.outputTokens + b.cacheCreationTokens + b.cacheReadTokens;
1452
+ if (tokens === 0) continue;
1453
+ entries.push({
1454
+ date: b.date,
1455
+ tool: "claude",
1456
+ model: b.model,
1457
+ inputTokens: b.inputTokens,
1458
+ outputTokens: b.outputTokens,
1459
+ cacheCreationTokens: b.cacheCreationTokens,
1460
+ cacheReadTokens: b.cacheReadTokens,
1461
+ costUSD: estimateCostUSD(b.model, b),
1462
+ origin: "cli",
1463
+ verified: false,
1464
+ requestCount: b.requestCount
1465
+ });
1466
+ }
1467
+ return entries;
1468
+ }
1469
+ function resolveClaudeConfigRoots(env = process.env) {
1470
+ const override = env.CLAUDE_CONFIG_DIR;
1471
+ return override ? override.split(",").map((s) => s.trim()).filter(Boolean) : [join6(homedir5(), ".claude"), join6(homedir5(), ".config", "claude")];
1472
+ }
1473
+ function resolveClaudeProjectDirs(env = process.env) {
1474
+ return resolveClaudeConfigRoots(env).map((r) => join6(r, "projects"));
1475
+ }
1476
+ async function listJsonl(dir) {
1477
+ let dirents;
1478
+ try {
1479
+ dirents = await readdir(dir, { withFileTypes: true });
1480
+ } catch {
1481
+ return [];
1482
+ }
1483
+ const out = [];
1484
+ for (const d of dirents) {
1485
+ const full = join6(dir, d.name);
1486
+ if (d.isDirectory()) {
1487
+ out.push(...await listJsonl(full));
1488
+ } else if (d.isFile() && d.name.endsWith(".jsonl")) {
1489
+ out.push(full);
1490
+ }
1491
+ }
1492
+ return out;
1493
+ }
1494
+ var NATIVE_READ_BUDGET_MS = 2e4;
1495
+ function* splitLines(content) {
1496
+ let start = 0;
1497
+ for (let i = 0; i < content.length; i++) {
1498
+ if (content[i] === "\n") {
1499
+ yield content.slice(start, i);
1500
+ start = i + 1;
1501
+ }
1502
+ }
1503
+ if (start < content.length) yield content.slice(start);
1504
+ }
1505
+ async function collectClaudeNative(env = process.env, opts = {}) {
1506
+ const dirs = resolveClaudeProjectDirs(env);
1507
+ const files = [];
1508
+ for (const dir of dirs) files.push(...await listJsonl(dir));
1509
+ if (files.length === 0) return { entries: [], found: false, filesScanned: 0 };
1510
+ const now = opts.now ?? Date.now;
1511
+ const deadline = now() + (opts.budgetMs ?? NATIVE_READ_BUDGET_MS);
1512
+ const acc = /* @__PURE__ */ new Map();
1513
+ let scanned = 0;
1514
+ for (const f of files) {
1515
+ if (now() > deadline) {
1516
+ return { entries: [], found: false, filesScanned: scanned, timedOut: true };
1517
+ }
1518
+ let content;
1519
+ try {
1520
+ content = await readFile(f, "utf8");
1521
+ } catch {
1522
+ continue;
1523
+ }
1524
+ accumulateClaudeLines(acc, splitLines(content));
1525
+ scanned += 1;
1526
+ }
1527
+ return { entries: finalizeClaudeEntries(acc), found: true, filesScanned: scanned };
1528
+ }
1529
+ async function collectClaudeRequests(env = process.env, opts = {}) {
1530
+ const dirs = resolveClaudeProjectDirs(env);
1531
+ const files = [];
1532
+ for (const dir of dirs) files.push(...await listJsonl(dir));
1533
+ if (files.length === 0) return { requests: [], found: false };
1534
+ const now = opts.now ?? Date.now;
1535
+ const deadline = now() + (opts.budgetMs ?? NATIVE_READ_BUDGET_MS);
1536
+ const acc = /* @__PURE__ */ new Map();
1537
+ for (const f of files) {
1538
+ if (now() > deadline) {
1539
+ return { requests: [...acc.values()], found: true, timedOut: true };
1540
+ }
1541
+ let content;
1542
+ try {
1543
+ content = await readFile(f, "utf8");
1544
+ } catch {
1545
+ continue;
1546
+ }
1547
+ accumulateClaudeLines(acc, splitLines(content));
1548
+ }
1549
+ return { requests: [...acc.values()], found: true };
1550
+ }
1551
+
1552
+ // src/native/codex.ts
1553
+ import { readdir as readdir2, readFile as readFile2 } from "node:fs/promises";
1554
+ import { homedir as homedir6 } from "node:os";
1555
+ import { join as join7 } from "node:path";
1556
+ function num4(n) {
1557
+ const v = Math.round(Number(n));
1558
+ return Number.isFinite(v) && v > 0 ? v : 0;
1559
+ }
1560
+ function localDate2(iso) {
1561
+ const t = Date.parse(iso);
1562
+ if (!Number.isFinite(t)) return null;
1563
+ const d = new Date(t);
1564
+ const y = d.getFullYear();
1565
+ const m = String(d.getMonth() + 1).padStart(2, "0");
1566
+ const day = String(d.getDate()).padStart(2, "0");
1567
+ return `${y}-${m}-${day}`;
1568
+ }
1569
+ function readTokenFields(payload) {
1570
+ const info = payload.info;
1571
+ const src = info?.total_token_usage ?? payload;
1572
+ const input = src.input_tokens;
1573
+ const output = src.output_tokens;
1574
+ if (input === void 0 && output === void 0) return null;
1575
+ return {
1576
+ input: num4(input),
1577
+ cached: num4(src.cached_input_tokens),
1578
+ output: num4(output),
1579
+ reasoning: num4(src.reasoning_output_tokens)
1580
+ };
1581
+ }
1582
+ function parseCodexRollout(lines) {
1583
+ let model = "unknown";
1584
+ const perDay = /* @__PURE__ */ new Map();
1585
+ let lastSeenDate = null;
1586
+ for (const raw of lines) {
1587
+ const trimmed = raw.trim();
1588
+ if (!trimmed) continue;
1589
+ let obj;
1590
+ try {
1591
+ obj = JSON.parse(trimmed);
1592
+ } catch {
1593
+ continue;
1594
+ }
1595
+ const payload = obj.payload;
1596
+ if (!payload || typeof payload !== "object") continue;
1597
+ const kind = obj.type;
1598
+ if (kind === "session_meta" || kind === "turn_context") {
1599
+ if (typeof payload.model === "string" && payload.model) model = payload.model;
1600
+ }
1601
+ if (payload.type === "token_count") {
1602
+ const fields = readTokenFields(payload);
1603
+ if (!fields) continue;
1604
+ const parsed = localDate2(String(obj.timestamp ?? ""));
1605
+ const day = parsed ?? lastSeenDate;
1606
+ if (!day) continue;
1607
+ lastSeenDate = day;
1608
+ const e = perDay.get(day);
1609
+ if (e) {
1610
+ e.cum = fields;
1611
+ e.turns += 1;
1612
+ } else {
1613
+ perDay.set(day, { cum: fields, turns: 1 });
1614
+ }
1615
+ }
1616
+ }
1617
+ if (perDay.size === 0) return [];
1618
+ const dates = [...perDay.keys()].sort();
1619
+ const out = [];
1620
+ let prev = { input: 0, cached: 0, output: 0, reasoning: 0 };
1621
+ for (const date of dates) {
1622
+ const { cum, turns } = perDay.get(date);
1623
+ const dInput = Math.max(0, cum.input - prev.input);
1624
+ const dCached = Math.max(0, cum.cached - prev.cached);
1625
+ const dOutput = Math.max(0, cum.output - prev.output);
1626
+ const dReasoning = Math.max(0, cum.reasoning - prev.reasoning);
1627
+ prev = cum;
1628
+ const cacheReadTokens = dCached;
1629
+ const inputTokens = Math.max(0, dInput - dCached);
1630
+ const outputTokens = dOutput + dReasoning;
1631
+ if (inputTokens + outputTokens + cacheReadTokens === 0) continue;
1632
+ out.push({
1633
+ date,
1634
+ model,
1635
+ inputTokens,
1636
+ outputTokens,
1637
+ cacheCreationTokens: 0,
1638
+ cacheReadTokens,
1639
+ turnCount: turns
1640
+ });
1641
+ }
1642
+ return out;
1643
+ }
1644
+ function accumulateCodexSession(acc, lines) {
1645
+ for (const s of parseCodexRollout(lines)) {
1646
+ const k = `${s.date}|${s.model}`;
1647
+ let b = acc.get(k);
1648
+ if (!b) {
1649
+ b = {
1650
+ date: s.date,
1651
+ model: s.model,
1652
+ inputTokens: 0,
1653
+ outputTokens: 0,
1654
+ cacheCreationTokens: 0,
1655
+ cacheReadTokens: 0,
1656
+ requestCount: 0
1657
+ };
1658
+ acc.set(k, b);
1659
+ }
1660
+ b.inputTokens += s.inputTokens;
1661
+ b.outputTokens += s.outputTokens;
1662
+ b.cacheCreationTokens += s.cacheCreationTokens;
1663
+ b.cacheReadTokens += s.cacheReadTokens;
1664
+ b.requestCount += s.turnCount;
1665
+ }
1666
+ }
1667
+ function finalizeCodexEntries(acc) {
1668
+ const entries = [];
1669
+ for (const b of acc.values()) {
1670
+ entries.push({
1671
+ date: b.date,
1672
+ tool: "codex",
1673
+ model: b.model,
1674
+ inputTokens: b.inputTokens,
1675
+ outputTokens: b.outputTokens,
1676
+ cacheCreationTokens: b.cacheCreationTokens,
1677
+ cacheReadTokens: b.cacheReadTokens,
1678
+ costUSD: estimateCostUSD(b.model, b),
1679
+ origin: "cli",
1680
+ verified: false,
1681
+ requestCount: b.requestCount
1682
+ });
1683
+ }
1684
+ return entries;
1685
+ }
1686
+ function resolveCodexSessionsDir(env = process.env) {
1687
+ const home = env.CODEX_HOME && env.CODEX_HOME.trim() ? env.CODEX_HOME.trim() : join7(homedir6(), ".codex");
1688
+ return join7(home, "sessions");
1689
+ }
1690
+ async function listJsonl2(dir) {
1691
+ let dirents;
1692
+ try {
1693
+ dirents = await readdir2(dir, { withFileTypes: true });
1694
+ } catch {
1695
+ return [];
1696
+ }
1697
+ const out = [];
1698
+ for (const d of dirents) {
1699
+ const full = join7(dir, d.name);
1700
+ if (d.isDirectory()) out.push(...await listJsonl2(full));
1701
+ else if (d.isFile() && d.name.endsWith(".jsonl")) out.push(full);
1702
+ }
1703
+ return out;
1704
+ }
1705
+ function* splitLines2(content) {
1706
+ let start = 0;
1707
+ for (let i = 0; i < content.length; i++) {
1708
+ if (content[i] === "\n") {
1709
+ yield content.slice(start, i);
1710
+ start = i + 1;
1711
+ }
1712
+ }
1713
+ if (start < content.length) yield content.slice(start);
1714
+ }
1715
+ var NATIVE_READ_BUDGET_MS2 = 2e4;
1716
+ async function collectCodexNative(env = process.env, opts = {}) {
1717
+ const dir = resolveCodexSessionsDir(env);
1718
+ const files = await listJsonl2(dir);
1719
+ if (files.length === 0) return { entries: [], found: false, filesScanned: 0 };
1720
+ const now = opts.now ?? Date.now;
1721
+ const deadline = now() + (opts.budgetMs ?? NATIVE_READ_BUDGET_MS2);
1722
+ const acc = /* @__PURE__ */ new Map();
1723
+ let scanned = 0;
1724
+ for (const f of files) {
1725
+ if (now() > deadline) {
1726
+ return { entries: [], found: false, filesScanned: scanned, timedOut: true };
1727
+ }
1728
+ let content;
1729
+ try {
1730
+ content = await readFile2(f, "utf8");
1731
+ } catch {
1732
+ continue;
1733
+ }
1734
+ accumulateCodexSession(acc, splitLines2(content));
1735
+ scanned += 1;
1736
+ }
1737
+ return { entries: finalizeCodexEntries(acc), found: true, filesScanned: scanned };
1738
+ }
1739
+
1275
1740
  // src/collect.ts
1276
1741
  var execFileAsync = promisify(execFile);
1277
1742
  var SOURCES = [
@@ -1340,9 +1805,12 @@ function mapCcusageDaily(tool, json) {
1340
1805
  };
1341
1806
  });
1342
1807
  } else {
1808
+ const modelsUsed = Array.isArray(day.modelsUsed) ? day.modelsUsed.filter(
1809
+ (m) => typeof m === "string" && m.length > 0
1810
+ ) : [];
1343
1811
  candidates = [
1344
1812
  {
1345
- model: "unknown",
1813
+ model: modelsUsed.length === 1 ? modelsUsed[0] : "unknown",
1346
1814
  inputTokens: norm(day.inputTokens),
1347
1815
  outputTokens: norm(day.outputTokens),
1348
1816
  cacheCreationTokens: norm(day.cacheCreationTokens),
@@ -1409,7 +1877,7 @@ function resolveCcusageBin() {
1409
1877
  const pkgPath = require3.resolve("ccusage/package.json");
1410
1878
  const pkg = require3("ccusage/package.json");
1411
1879
  const rel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.ccusage ?? "ccusage";
1412
- const binPath = join6(dirname3(pkgPath), rel);
1880
+ const binPath = join8(dirname3(pkgPath), rel);
1413
1881
  if (/\.(c|m)?js$/.test(binPath)) {
1414
1882
  return { cmd: process.execPath, prefixArgs: [binPath] };
1415
1883
  }
@@ -1430,6 +1898,9 @@ function dedupeDaily(entries) {
1430
1898
  prev.cacheReadTokens += e.cacheReadTokens;
1431
1899
  prev.costUSD = Number((prev.costUSD + e.costUSD).toFixed(6));
1432
1900
  prev.verified = prev.verified && e.verified;
1901
+ if (e.requestCount !== void 0 || prev.requestCount !== void 0) {
1902
+ prev.requestCount = (prev.requestCount ?? 0) + (e.requestCount ?? 0);
1903
+ }
1433
1904
  }
1434
1905
  return [...byKey.values()];
1435
1906
  }
@@ -1462,7 +1933,18 @@ function dedupeBlocks(blocks) {
1462
1933
  }
1463
1934
  return [...byStart.values()];
1464
1935
  }
1465
- async function runCcusageOnce(cmd, args) {
1936
+ function selectSourceEntries(source, ccusageEntries, native) {
1937
+ if (source === "claude" && native.claude.found && native.claude.entries.length > 0)
1938
+ return native.claude.entries;
1939
+ if (source === "codex" && native.codex.found && native.codex.entries.length > 0)
1940
+ return native.codex.entries;
1941
+ return ccusageEntries;
1942
+ }
1943
+ function ccusageClaudeEnv(env = process.env) {
1944
+ if (env.CLAUDE_CONFIG_DIR && env.CLAUDE_CONFIG_DIR.trim()) return env;
1945
+ return { ...env, CLAUDE_CONFIG_DIR: resolveClaudeConfigRoots(env).join(",") };
1946
+ }
1947
+ async function runCcusageOnce(cmd, args, env) {
1466
1948
  try {
1467
1949
  const { stdout } = await execFileAsync(cmd, args, {
1468
1950
  encoding: "utf8",
@@ -1470,7 +1952,8 @@ async function runCcusageOnce(cmd, args) {
1470
1952
  // A single source shouldn't be able to hang the whole run. 25s is plenty
1471
1953
  // for a healthy local read; a hung source gets killed and (if transient)
1472
1954
  // retried once below rather than stalling everything for minutes.
1473
- timeout: 25e3
1955
+ timeout: 25e3,
1956
+ ...env ? { env } : {}
1474
1957
  });
1475
1958
  if (!stdout) return { json: null, transient: false };
1476
1959
  try {
@@ -1484,18 +1967,29 @@ async function runCcusageOnce(cmd, args) {
1484
1967
  return { json: null, transient };
1485
1968
  }
1486
1969
  }
1487
- async function runCcusage(cmd, args) {
1488
- const first = await runCcusageOnce(cmd, args);
1970
+ async function runCcusage(cmd, args, env) {
1971
+ const first = await runCcusageOnce(cmd, args, env);
1489
1972
  if (first.json !== null || !first.transient) return first.json;
1490
- return (await runCcusageOnce(cmd, args)).json;
1973
+ return (await runCcusageOnce(cmd, args, env)).json;
1491
1974
  }
1492
1975
  var COLLECT_STAGES = SOURCES.length + 4;
1493
1976
  async function collectAll(onProgress) {
1494
1977
  const { cmd, prefixArgs } = resolveCcusageBin();
1495
1978
  let done = 0;
1496
1979
  const tick = () => onProgress?.(++done, COLLECT_STAGES, "");
1980
+ const nativeClaudeTask = collectClaudeNative().catch(
1981
+ () => ({ entries: [], found: false, filesScanned: 0 })
1982
+ );
1983
+ const nativeCodexTask = collectCodexNative().catch(
1984
+ () => ({ entries: [], found: false, filesScanned: 0 })
1985
+ );
1497
1986
  const sourceTasks = SOURCES.map(async (source) => {
1498
- const json = await runCcusage(cmd, [...prefixArgs, source, "daily", "--json", "--offline"]);
1987
+ const env = source === "claude" ? ccusageClaudeEnv() : void 0;
1988
+ const json = await runCcusage(
1989
+ cmd,
1990
+ [...prefixArgs, source, "daily", "--json", "--offline"],
1991
+ env
1992
+ );
1499
1993
  tick();
1500
1994
  return { source, mapped: json ? mapCcusageDaily(source, json) : [] };
1501
1995
  });
@@ -1519,18 +2013,22 @@ async function collectAll(onProgress) {
1519
2013
  tick();
1520
2014
  return a;
1521
2015
  });
1522
- const [sourceResults, sessions, blocks, cursor, attribution] = await Promise.all([
2016
+ const [sourceResults, sessions, blocks, cursor, attribution, nativeClaude, nativeCodex] = await Promise.all([
1523
2017
  Promise.all(sourceTasks),
1524
2018
  sessionTask,
1525
2019
  blockTask,
1526
2020
  cursorTask,
1527
- attributionTask
2021
+ attributionTask,
2022
+ nativeClaudeTask,
2023
+ nativeCodexTask
1528
2024
  ]);
2025
+ const native = { claude: nativeClaude, codex: nativeCodex };
1529
2026
  const entries = [];
1530
2027
  const toolsFound = [];
1531
2028
  for (const { source, mapped } of sourceResults) {
1532
- if (mapped.length > 0) {
1533
- entries.push(...mapped);
2029
+ const chosen = selectSourceEntries(source, mapped, native);
2030
+ if (chosen.length > 0) {
2031
+ entries.push(...chosen);
1534
2032
  toolsFound.push(source);
1535
2033
  }
1536
2034
  }
@@ -1539,14 +2037,12 @@ async function collectAll(onProgress) {
1539
2037
  blocks.push(...cursor.blocks);
1540
2038
  toolsFound.push("cursor");
1541
2039
  }
1542
- const { tools, skills, projects, agent, titles, sessionMessages, complete } = attribution;
2040
+ const { tools, skills, agent, sessionMessages, complete } = attribution;
1543
2041
  onProgress?.(COLLECT_STAGES, COLLECT_STAGES, "");
1544
2042
  const dedupedSessions = dedupeSessions(sessions).map((s) => {
1545
- const title = titles.get(s.sessionId);
1546
2043
  const messageCount = sessionMessages.get(s.sessionId);
1547
2044
  return {
1548
2045
  ...s,
1549
- ...title ? { title } : {},
1550
2046
  ...messageCount ? { messageCount } : {}
1551
2047
  };
1552
2048
  });
@@ -1555,14 +2051,13 @@ async function collectAll(onProgress) {
1555
2051
  // schema: entries ≤ 20000, sessions/blocks ≤ 10000), keeping the
1556
2052
  // highest-token rows. Without this, a power user with >10000 distinct sessions
1557
2053
  // would have their ENTIRE submit rejected with a 400 instead of a capped one.
1558
- // tools/skills/projects are already bounded upstream (attribution caps).
2054
+ // tools/skills are already bounded upstream (attribution caps).
1559
2055
  entries: capByTokens(dedupeDaily(entries), 2e4, entryTokens),
1560
2056
  sessions: capByTokens(dedupedSessions, 1e4, entryTokens),
1561
2057
  blocks: capByTokens(dedupeBlocks(blocks), 1e4, (b) => b.totalTokens),
1562
2058
  toolsFound,
1563
2059
  tools,
1564
2060
  skills,
1565
- projects,
1566
2061
  agent,
1567
2062
  attributionComplete: complete
1568
2063
  };
@@ -5747,7 +6242,18 @@ var DailyUsageEntry = external_exports.object({
5747
6242
  /** Where this entry came from. Defaults to the local CLI for back-compat. */
5748
6243
  origin: UsageOrigin.default("cli"),
5749
6244
  /** True when the numbers come from a provider's authoritative usage API. */
5750
- verified: external_exports.boolean().default(false)
6245
+ verified: external_exports.boolean().default(false),
6246
+ /**
6247
+ * Structural fingerprint: the number of DISTINCT provider requests
6248
+ * (unique message-id + request-id pairs) that summed into this entry. A real
6249
+ * heavy day is the product of thousands of distinct API requests; a hand-typed
6250
+ * fabrication has none. The server uses this as an anti-fraud signal — a
6251
+ * billion-token day backed by ~zero real requests is the forgery signature.
6252
+ * Optional + back-compat: older CLIs and the ccusage fallback path (which
6253
+ * cannot see request ids) omit it, and an omitted fingerprint is never
6254
+ * penalized.
6255
+ */
6256
+ requestCount: external_exports.number().int().nonnegative().optional()
5751
6257
  });
5752
6258
  var Timestamp = external_exports.string().min(1).max(40);
5753
6259
  var SessionEntry = external_exports.object({
@@ -5760,8 +6266,6 @@ var SessionEntry = external_exports.object({
5760
6266
  cacheReadTokens: tokenCount,
5761
6267
  costUSD: external_exports.number().nonnegative(),
5762
6268
  lastActivity: Timestamp,
5763
- /** Human-readable AI-generated session title (from transcripts). Optional. */
5764
- title: external_exports.string().max(200).optional(),
5765
6269
  /** Number of assistant messages in this session (from transcripts). Optional. */
5766
6270
  messageCount: external_exports.number().int().nonnegative().optional()
5767
6271
  });
@@ -5778,11 +6282,6 @@ var ToolStat = external_exports.object({
5778
6282
  /** Tokens burned on turns that used this tool (turn tokens split across its tool calls). Optional. */
5779
6283
  tokens: external_exports.number().int().nonnegative().optional()
5780
6284
  });
5781
- var ProjectStat = external_exports.object({
5782
- name: external_exports.string().min(1).max(128),
5783
- tokens: external_exports.number().int().nonnegative(),
5784
- costUSD: external_exports.number().nonnegative()
5785
- });
5786
6285
  var AgentStat = external_exports.object({
5787
6286
  /** Total assistant messages across transcripts. */
5788
6287
  messageCount: external_exports.number().int().nonnegative(),
@@ -5816,15 +6315,13 @@ var SubmitPayload = external_exports.object({
5816
6315
  tools: external_exports.array(ToolStat).max(300).optional(),
5817
6316
  /** Optional skill-usage frequencies parsed from local transcripts. */
5818
6317
  skills: external_exports.array(SkillStat).max(300).optional(),
5819
- /** Optional per-project usage totals parsed from local transcripts. */
5820
- projects: external_exports.array(ProjectStat).max(500).optional(),
5821
6318
  /** Optional subagent-vs-main rollup parsed from local transcripts. */
5822
6319
  agent: AgentStat.optional(),
5823
6320
  /**
5824
6321
  * Set when the transcript scan completed within its time budget, i.e. the
5825
- * tool/skill/project/agent rollups are a FULL snapshot. The server refreshes
5826
- * the dashboard breakdowns unconditionally for a full snapshot; for a partial
5827
- * one (flag absent/false) it keeps its no-shrink guard. Back-compat: omittable.
6322
+ * tool/skill/agent rollups are a FULL snapshot. The server refreshes the
6323
+ * dashboard breakdowns unconditionally for a full snapshot; for a partial one
6324
+ * (flag absent/false) it keeps its no-shrink guard. Back-compat: omittable.
5828
6325
  */
5829
6326
  attributionComplete: external_exports.boolean().optional(),
5830
6327
  /** Optional friends-board code (from `--board=<code>`): auto-join this board on submit. */
@@ -5837,12 +6334,47 @@ var SubmitPayload = external_exports.object({
5837
6334
  * never needs it, and a wrong/missing code only skips the org attach (the
5838
6335
  * personal submit still succeeds).
5839
6336
  */
5840
- orgCode: external_exports.string().min(1).max(64).optional()
6337
+ orgCode: external_exports.string().min(1).max(64).optional(),
6338
+ /**
6339
+ * The submitter's local UTC offset in minutes EAST of UTC (IST = +330,
6340
+ * US-Pacific DST = -420; i.e. `-new Date().getTimezoneOffset()`). ccusage dates
6341
+ * usage in this local zone, so the server uses the offset to compute each
6342
+ * member's daily/weekly leaderboard window in THEIR calendar day rather than a
6343
+ * UTC one. Back-compat: omittable — older CLIs don't send it and the board falls
6344
+ * back to its default offset. Range clamps to real zones (UTC-14..+14).
6345
+ */
6346
+ tzOffsetMinutes: external_exports.number().int().min(-840).max(840).optional()
5841
6347
  });
5842
6348
  var AnonSubmitPayload = SubmitPayload.extend({
5843
6349
  /** Client-generated secret (hex). The server stores only its hash. */
5844
6350
  anonKey: external_exports.string().min(16).max(128)
5845
6351
  });
6352
+ var VerifyRequestRecord = external_exports.object({
6353
+ /** Local calendar date (YYYY-MM-DD) this request is bucketed under. */
6354
+ date: DateString,
6355
+ /** Epoch milliseconds of the request's final (max-token) transcript line. */
6356
+ ts: external_exports.number().int().nonnegative(),
6357
+ tool: external_exports.string().min(1).max(64),
6358
+ model: external_exports.string().min(1).max(128),
6359
+ inputTokens: tokenCount,
6360
+ outputTokens: tokenCount,
6361
+ cacheCreationTokens: tokenCount,
6362
+ cacheReadTokens: tokenCount,
6363
+ /** sha256(msgId|reqId), truncated — preserves uniqueness without revealing the id. */
6364
+ reqHash: external_exports.string().min(1).max(64)
6365
+ });
6366
+ var VerifyPayload = external_exports.object({
6367
+ cliVersion: external_exports.string().min(1).max(32),
6368
+ /** The per-request skeleton (bounded/sampled — see `truncated`). */
6369
+ requests: external_exports.array(VerifyRequestRecord).min(1).max(1e5),
6370
+ /**
6371
+ * True when the local corpus exceeded the client upload cap and was sampled to
6372
+ * the most recent N requests. The server treats a truncated upload conservatively
6373
+ * (it never AUTO-passes a truncated one — it can still auto-FAIL on a physical
6374
+ * impossibility, or route to a human).
6375
+ */
6376
+ truncated: external_exports.boolean().optional()
6377
+ });
5846
6378
  function entryTotalTokens(e) {
5847
6379
  return e.inputTokens + e.outputTokens + e.cacheCreationTokens + e.cacheReadTokens;
5848
6380
  }
@@ -5923,7 +6455,16 @@ var OrgProvisionInput = external_exports.object({
5923
6455
  ownerEmail: external_exports.string().email().max(254).optional(),
5924
6456
  description: external_exports.string().max(2e3).optional(),
5925
6457
  boardVisibility: OrgBoardVisibility.optional(),
5926
- window: OrgWindow.optional()
6458
+ window: OrgWindow.optional(),
6459
+ /** Brand accent applied to the org homepage theming. */
6460
+ accentColor: HexColor.optional(),
6461
+ /**
6462
+ * Create the org UNOWNED and mint a one-time owner-claim ("admin sign-in")
6463
+ * token in the same call. The first person to open the link + sign in becomes
6464
+ * the owner; the token is then consumed. When set, an owner email/handle is
6465
+ * NOT required — the link is how the first admin is assigned.
6466
+ */
6467
+ issueClaimLink: external_exports.boolean().optional()
5927
6468
  });
5928
6469
  var OrgSettingsInput = external_exports.object({
5929
6470
  name: external_exports.string().min(1).max(120).optional(),
@@ -5949,26 +6490,49 @@ function formatTokens(n) {
5949
6490
  function formatUSD(n) {
5950
6491
  return `$${n.toLocaleString("en-US", { maximumFractionDigits: 2 })}`;
5951
6492
  }
6493
+ function sanitizeServerText(s) {
6494
+ return s.replace(/[\u0000-\u001f\u007f-\u009f]/g, "");
6495
+ }
5952
6496
  function submitNextStepLines(result) {
5953
6497
  if (result.boardUrl) {
5954
6498
  const code = result.boardCode ?? result.boardUrl.split("/").filter(Boolean).pop() ?? "";
5955
6499
  return [
5956
- ` \u{1F91D} You're on the board: ${result.boardUrl}`,
6500
+ ` \u{1F91D} You're on the board: ${sanitizeServerText(result.boardUrl)}`,
5957
6501
  " \u2192 Open it to see who burned more.",
5958
- ` \u2192 Get a friend on it \u2014 have them run: npx whoburnedmore --board=${code}`,
6502
+ ` \u2192 Get a friend on it \u2014 have them run: npx whoburnedmore --board=${sanitizeServerText(code)}`,
5959
6503
  " \u2192 Sign in on the page and add your X to claim your spot and own your rank."
5960
6504
  ];
5961
6505
  }
5962
6506
  return [
5963
- ` Your dashboard: ${result.dashboardUrl}`,
6507
+ ` Your dashboard: ${sanitizeServerText(result.dashboardUrl)}`,
5964
6508
  " \u2192 Sign in and add your X on the page to get on the leaderboard and claim your rank.",
5965
6509
  " Private until you do. Manage anytime: `npx whoburnedmore private` \xB7 `public` \xB7 `remove`."
5966
6510
  ];
5967
6511
  }
6512
+ function signedInNextStepLines(result) {
6513
+ if (result.orgBoardUrl) {
6514
+ return [
6515
+ ` \u{1F3E2} You're on your team board: ${sanitizeServerText(result.orgBoardUrl)}`,
6516
+ " \u2192 Open it to see who burned more."
6517
+ ];
6518
+ }
6519
+ if (result.boardUrl) {
6520
+ const code = result.boardUrl.split("/").filter(Boolean).pop() ?? "";
6521
+ return [
6522
+ ` \u{1F91D} You're on the board: ${sanitizeServerText(result.boardUrl)}`,
6523
+ " \u2192 Open it to see who burned more.",
6524
+ ` \u2192 Get a friend on it \u2014 have them run: npx whoburnedmore --board=${sanitizeServerText(code)}`
6525
+ ];
6526
+ }
6527
+ return [
6528
+ ` Your dashboard: ${sanitizeServerText(result.profileUrl)}`,
6529
+ " \u2192 Open it to see your rank and share your profile."
6530
+ ];
6531
+ }
5968
6532
 
5969
6533
  // src/local-dashboard.ts
5970
6534
  function esc(s) {
5971
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
6535
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
5972
6536
  }
5973
6537
  function renderDashboardHtml(entries, generatedAt = /* @__PURE__ */ new Date(), connect) {
5974
6538
  const today = generatedAt.toISOString().slice(0, 10);
@@ -6181,8 +6745,12 @@ async function publishLocal(payload, deps) {
6181
6745
  }
6182
6746
  const key = deps.ensureAnonKey();
6183
6747
  const res = await deps.anonSubmit(key, payload);
6184
- deps.log(` Published \u2014 you're on the board: ${res.dashboardUrl}`);
6185
- deps.openBrowser(claimUrl(res.dashboardUrl, key));
6748
+ deps.log(` Published \u2014 you're on the board: ${sanitizeServerText(res.dashboardUrl)}`);
6749
+ if (isTrustedWebUrl(res.dashboardUrl)) {
6750
+ deps.openBrowser(claimUrl(res.dashboardUrl, key));
6751
+ } else {
6752
+ deps.log(" (The server returned an unexpected address, so it was not auto-opened.)");
6753
+ }
6186
6754
  deps.log(
6187
6755
  " Sign in there to claim it, or `npx whoburnedmore private` to hide it again."
6188
6756
  );
@@ -6264,7 +6832,7 @@ async function confirm(question) {
6264
6832
  function showLocalDashboard(payload) {
6265
6833
  const dir = defaultConfigDir();
6266
6834
  mkdirSync3(dir, { recursive: true });
6267
- const file = join7(dir, "dashboard.html");
6835
+ const file = join9(dir, "dashboard.html");
6268
6836
  writeFileSync3(
6269
6837
  file,
6270
6838
  renderDashboardHtml(payload.entries, /* @__PURE__ */ new Date(), {
@@ -6299,7 +6867,7 @@ async function run(flags) {
6299
6867
  } finally {
6300
6868
  progress.stop();
6301
6869
  }
6302
- const { entries, sessions, blocks, tools, skills, projects, agent, attributionComplete } = collected;
6870
+ const { entries, sessions, blocks, tools, skills, agent, attributionComplete } = collected;
6303
6871
  if (entries.length === 0) {
6304
6872
  console.log();
6305
6873
  console.log(" Nothing to burn yet \u2014 no local usage found from any coding agent.");
@@ -6307,13 +6875,13 @@ async function run(flags) {
6307
6875
  return;
6308
6876
  }
6309
6877
  const payload = { cliVersion: VERSION, entries };
6878
+ payload.tzOffsetMinutes = -(/* @__PURE__ */ new Date()).getTimezoneOffset();
6310
6879
  if (sessions.length > 0) payload.sessions = sessions;
6311
6880
  if (blocks.length > 0) payload.blocks = blocks;
6312
6881
  if (tools.length > 0) payload.tools = tools;
6313
6882
  if (skills.length > 0) payload.skills = skills;
6314
- if (projects.length > 0) payload.projects = projects;
6315
6883
  if (agent.messageCount > 0) payload.agent = agent;
6316
- if (attributionComplete && (tools.length > 0 || skills.length > 0 || projects.length > 0))
6884
+ if (attributionComplete && (tools.length > 0 || skills.length > 0))
6317
6885
  payload.attributionComplete = true;
6318
6886
  applyScope(payload, flags);
6319
6887
  if (flags.dryRun) {
@@ -6338,7 +6906,132 @@ async function run(flags) {
6338
6906
  console.log(pc2.dim(" --no-submit: skipped the dashboard."));
6339
6907
  return;
6340
6908
  }
6341
- const anonKey = ensureAnonKey();
6909
+ const cfg = loadConfig();
6910
+ const canSignIn = !flags.quiet && Boolean(process.stdout.isTTY);
6911
+ if (cfg?.cliToken) {
6912
+ await submitSignedIn(cfg.cliToken, payload, flags, canSignIn);
6913
+ } else if (cfg?.anonKey) {
6914
+ await submitWithDeviceKey(cfg.anonKey, payload, flags);
6915
+ } else if (canSignIn) {
6916
+ const auth = await ensureSignedIn();
6917
+ if (!auth) return;
6918
+ await submitSignedIn(auth.token, payload, flags, canSignIn);
6919
+ } else if (!flags.quiet) {
6920
+ console.log(pc2.yellow(" Sign in to put your usage on the leaderboard."));
6921
+ console.log(
6922
+ pc2.dim(
6923
+ " Run `npx whoburnedmore` in an interactive terminal to sign in, or `npx whoburnedmore link --token=\u2026` (from your signed-in profile) for servers/CI."
6924
+ )
6925
+ );
6926
+ } else {
6927
+ return;
6928
+ }
6929
+ }
6930
+ function sleep(ms) {
6931
+ return new Promise((resolve) => setTimeout(resolve, ms));
6932
+ }
6933
+ async function ensureSignedIn() {
6934
+ const cfg = loadConfig();
6935
+ if (cfg?.cliToken) return { token: cfg.cliToken, handle: cfg.handle ?? "" };
6936
+ let dev;
6937
+ try {
6938
+ dev = await deviceStart();
6939
+ } catch (err) {
6940
+ console.log(pc2.yellow(` Couldn't start sign-in: ${err.message}`));
6941
+ return null;
6942
+ }
6943
+ console.log();
6944
+ console.log(pc2.bold(" Sign in to whoburnedmore to put your usage on the board."));
6945
+ console.log(` 1. Opening ${pc2.cyan(sanitizeServerText(dev.verifyUrl))} \u2014 or go there yourself.`);
6946
+ console.log(` 2. Approve this code: ${pc2.bold(sanitizeServerText(dev.userCode))}`);
6947
+ if (isTrustedWebUrl(dev.verifyUrl)) openBrowser(dev.verifyUrl);
6948
+ console.log(pc2.dim(" Waiting for you to approve in the browser\u2026"));
6949
+ const deadline = Date.now() + dev.expiresInSeconds * 1e3;
6950
+ const intervalMs = Math.max(1, dev.pollIntervalSeconds) * 1e3;
6951
+ while (Date.now() < deadline) {
6952
+ await sleep(intervalMs);
6953
+ let res;
6954
+ try {
6955
+ res = await devicePoll(dev.deviceCode);
6956
+ } catch {
6957
+ continue;
6958
+ }
6959
+ if (res.status === "ok") {
6960
+ saveAuth(void 0, { cliToken: res.token, handle: res.handle });
6961
+ console.log(pc2.green(` \u2713 Signed in as @${sanitizeServerText(res.handle)}.`));
6962
+ return { token: res.token, handle: res.handle };
6963
+ }
6964
+ if (res.status === "expired") break;
6965
+ }
6966
+ console.log(
6967
+ pc2.yellow(" Sign-in timed out. Run `npx whoburnedmore` again to retry.")
6968
+ );
6969
+ return null;
6970
+ }
6971
+ async function submitSignedIn(token, payload, flags, interactive) {
6972
+ let result;
6973
+ try {
6974
+ result = await submit(token, payload);
6975
+ } catch (err) {
6976
+ if (err instanceof UnauthorizedError) {
6977
+ clearAuth();
6978
+ if (!interactive) return;
6979
+ const auth = await ensureSignedIn();
6980
+ if (!auth) return;
6981
+ result = await submit(auth.token, payload);
6982
+ } else {
6983
+ throw err;
6984
+ }
6985
+ }
6986
+ try {
6987
+ recordSync();
6988
+ } catch {
6989
+ }
6990
+ const baseUrl = result.orgBoardUrl ?? result.boardUrl ?? result.profileUrl;
6991
+ if (!flags.quiet) {
6992
+ console.log(
6993
+ pc2.green(" \u2713 Synced securely.") + pc2.dim(" Only your daily totals left this machine \u2014 never your prompts, code, or file names.")
6994
+ );
6995
+ if (result.suppressed) {
6996
+ console.log();
6997
+ console.log(
6998
+ pc2.yellow(
6999
+ " \u26A0 You're currently OFF the leaderboard \u2014 we couldn't verify these numbers."
7000
+ )
7001
+ );
7002
+ console.log(
7003
+ pc2.dim(
7004
+ " Verify your usage to get back on \u2014 this sends a detailed per-request breakdown (timestamps + token counts, never your prompts or code)."
7005
+ )
7006
+ );
7007
+ if (process.stdin.isTTY && await confirm(" Verify now?")) {
7008
+ await runVerify();
7009
+ afterSubmitChores(flags);
7010
+ return;
7011
+ }
7012
+ console.log(
7013
+ pc2.dim(
7014
+ " Run `npx whoburnedmore verify` anytime, or appeal at whoburnedmore.com/appeal."
7015
+ )
7016
+ );
7017
+ }
7018
+ if (isTrustedWebUrl(baseUrl)) {
7019
+ console.log(pc2.dim(" Opening your dashboard in your browser\u2026"));
7020
+ openBrowser(baseUrl);
7021
+ } else {
7022
+ console.log(
7023
+ pc2.dim(" The server returned an unexpected dashboard address, so it was NOT auto-opened. Open it yourself only if you trust it:")
7024
+ );
7025
+ console.log(` ${sanitizeServerText(baseUrl)}`);
7026
+ }
7027
+ for (const line of signedInNextStepLines(result)) {
7028
+ if (line.includes("\u2192")) console.log(pc2.bold(line));
7029
+ else console.log(line);
7030
+ }
7031
+ }
7032
+ afterSubmitChores(flags);
7033
+ }
7034
+ async function submitWithDeviceKey(anonKey, payload, flags) {
6342
7035
  const result = await anonSubmit(anonKey, payload);
6343
7036
  try {
6344
7037
  recordSync();
@@ -6366,28 +7059,26 @@ async function run(flags) {
6366
7059
  console.log(
6367
7060
  pc2.dim(" The server returned an unexpected dashboard address, so it was NOT auto-opened. Open it yourself only if you trust it:")
6368
7061
  );
6369
- console.log(` ${baseUrl}`);
7062
+ console.log(` ${sanitizeServerText(baseUrl)}`);
6370
7063
  }
6371
- }
6372
- const lines = submitNextStepLines(result);
6373
- for (const line of lines) {
6374
- if (line.includes("\u2192")) console.log(pc2.bold(line));
6375
- else if (line.startsWith(" Private until you do")) {
6376
- if (!flags.quiet) console.log(pc2.dim(line));
6377
- } else console.log(line);
6378
- }
6379
- if (!flags.quiet) {
6380
- try {
6381
- reconcileAutoSync();
6382
- } catch {
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);
6383
7068
  }
6384
7069
  }
6385
- if (!flags.quiet) {
6386
- console.log();
6387
- console.log(
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.")
6389
- );
7070
+ afterSubmitChores(flags);
7071
+ }
7072
+ function afterSubmitChores(flags) {
7073
+ if (flags.quiet) return;
7074
+ try {
7075
+ reconcileAutoSync();
7076
+ } catch {
6390
7077
  }
7078
+ console.log();
7079
+ console.log(
7080
+ 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.")
7081
+ );
6391
7082
  }
6392
7083
  async function linkServerInstall(token) {
6393
7084
  if (!token) {
@@ -6395,8 +7086,9 @@ async function linkServerInstall(token) {
6395
7086
  }
6396
7087
  const anonKey = ensureAnonKey();
6397
7088
  const linked = await redeemServerInstall(token, anonKey);
7089
+ const handle = sanitizeServerText(linked.handle);
6398
7090
  console.log(
6399
- linked.alreadyLinked ? ` This machine is already linked to @${linked.handle}.` : ` Linked this machine to @${linked.handle}.`
7091
+ linked.alreadyLinked ? ` This machine is already linked to @${handle}.` : ` Linked this machine to @${handle}.`
6400
7092
  );
6401
7093
  if (linked.mergedDays > 0) {
6402
7094
  console.log(pc2.dim(` Merged ${linked.mergedDays} existing usage day${linked.mergedDays === 1 ? "" : "s"} from this machine.`));
@@ -6419,7 +7111,7 @@ async function linkServerInstall(token) {
6419
7111
  } catch {
6420
7112
  console.log(pc2.dim(" Linked, but background sync could not be installed automatically. Run `npx whoburnedmore install-sync` to retry."));
6421
7113
  }
6422
- console.log(` Profile: ${linked.profileUrl}`);
7114
+ console.log(` Profile: ${sanitizeServerText(linked.profileUrl)}`);
6423
7115
  }
6424
7116
  function waitOrAbort(ms, signal) {
6425
7117
  if (signal.aborted) return Promise.resolve();
@@ -6468,6 +7160,102 @@ async function runDaemon() {
6468
7160
  console.log();
6469
7161
  console.log(pc2.dim(` Daemon stopped after ${cycles} sync cycle${cycles === 1 ? "" : "s"}.`));
6470
7162
  }
7163
+ async function runVerify() {
7164
+ printBanner();
7165
+ console.log();
7166
+ console.log(pc2.bold(" Verify your usage to get back on the leaderboard."));
7167
+ console.log(
7168
+ pc2.dim(
7169
+ " This sends a DETAILED per-request breakdown \u2014 timestamps and token counts, still never your prompts, code, or file names \u2014 so we can confirm your usage is real. It's deleted after review."
7170
+ )
7171
+ );
7172
+ const cfg = loadConfig();
7173
+ let token = cfg?.cliToken;
7174
+ if (!token) {
7175
+ if (!process.stdout.isTTY) {
7176
+ console.log(
7177
+ pc2.yellow(
7178
+ " Sign in first \u2014 run `npx whoburnedmore verify` in an interactive terminal."
7179
+ )
7180
+ );
7181
+ return;
7182
+ }
7183
+ const auth = await ensureSignedIn();
7184
+ if (!auth) return;
7185
+ token = auth.token;
7186
+ }
7187
+ if (process.stdin.isTTY) {
7188
+ const ok = await confirm("\n Send this breakdown to verify your usage?");
7189
+ if (!ok) {
7190
+ console.log(pc2.dim(" Cancelled \u2014 nothing was sent."));
7191
+ return;
7192
+ }
7193
+ }
7194
+ console.log(pc2.dim(" Reading your local Claude Code logs\u2026"));
7195
+ const { requests, found } = await collectClaudeRequests();
7196
+ if (!found || requests.length === 0) {
7197
+ console.log(
7198
+ pc2.yellow(" No local Claude Code logs found on this machine to verify.")
7199
+ );
7200
+ console.log(
7201
+ pc2.dim(
7202
+ " Run this on the machine where you actually use Claude Code, or file a written appeal at whoburnedmore.com/appeal."
7203
+ )
7204
+ );
7205
+ return;
7206
+ }
7207
+ const CAP = 5e4;
7208
+ const sorted = requests.slice().sort((a, b) => b.ts - a.ts);
7209
+ const truncated = sorted.length > CAP;
7210
+ const capped = truncated ? sorted.slice(0, CAP) : sorted;
7211
+ const records = capped.map((r) => ({
7212
+ date: r.date,
7213
+ ts: r.ts,
7214
+ tool: "claude",
7215
+ model: r.model,
7216
+ inputTokens: r.inputTokens,
7217
+ outputTokens: r.outputTokens,
7218
+ cacheCreationTokens: r.cacheCreationTokens,
7219
+ cacheReadTokens: r.cacheReadTokens,
7220
+ reqHash: createHash("sha256").update(r.key).digest("hex").slice(0, 32)
7221
+ }));
7222
+ console.log(
7223
+ pc2.dim(
7224
+ ` Sending ${records.length} request records for analysis${truncated ? " (most recent, sampled)" : ""}\u2026`
7225
+ )
7226
+ );
7227
+ let result;
7228
+ try {
7229
+ result = await verifyUsage(token, {
7230
+ cliVersion: VERSION,
7231
+ requests: records,
7232
+ ...truncated ? { truncated: true } : {}
7233
+ });
7234
+ } catch (err) {
7235
+ if (err instanceof UnauthorizedError) {
7236
+ clearAuth();
7237
+ console.log(
7238
+ pc2.yellow(
7239
+ " Your sign-in expired \u2014 run `npx whoburnedmore verify` again to retry."
7240
+ )
7241
+ );
7242
+ return;
7243
+ }
7244
+ throw err;
7245
+ }
7246
+ console.log();
7247
+ const line = ` ${sanitizeServerText(result.message)}`;
7248
+ if (result.relisted) {
7249
+ console.log(pc2.green(` \u2713${line.slice(1)}`));
7250
+ } else {
7251
+ console.log(result.verdict === "fail" ? pc2.yellow(line) : line);
7252
+ console.log(
7253
+ pc2.dim(
7254
+ " We'll relist you automatically if it checks out \u2014 re-run `npx whoburnedmore` anytime to check."
7255
+ )
7256
+ );
7257
+ }
7258
+ }
6471
7259
  async function main() {
6472
7260
  const major = Number(process.versions.node.split(".")[0]);
6473
7261
  if (major < 20) {
@@ -6503,6 +7291,9 @@ async function main() {
6503
7291
  case "daemon":
6504
7292
  await runDaemon();
6505
7293
  break;
7294
+ case "verify":
7295
+ await runVerify();
7296
+ break;
6506
7297
  case "status":
6507
7298
  case "doctor": {
6508
7299
  for (const line of agentStatusReport()) console.log(line);
@@ -6554,8 +7345,8 @@ function printHelp() {
6554
7345
  ${pc2.bold("whoburnedmore")} \u2014 who burned more tokens, you or them?
6555
7346
 
6556
7347
  ${pc2.bold("usage")}
6557
- npx whoburnedmore burn + land on the public leaderboard, open your dashboard
6558
- npx whoburnedmore --board=CODE compare with friends \u2014 join their board (no sign-in)
7348
+ npx whoburnedmore sign in, burn + land on the public leaderboard, open your dashboard
7349
+ npx whoburnedmore --board=CODE compare with friends \u2014 sign in and join their board
6559
7350
  npx whoburnedmore --org=SLUG submit to your organization's board (companies/hackathons)
6560
7351
  npx whoburnedmore --local build the dashboard on your machine and open it (offline)
6561
7352
  npx whoburnedmore --dry-run print exactly what would be sent, send nothing
@@ -6565,14 +7356,15 @@ function printHelp() {
6565
7356
  npx whoburnedmore private hide your dashboard from the leaderboard
6566
7357
  npx whoburnedmore public put it back on the leaderboard
6567
7358
  npx whoburnedmore remove delete your dashboard and its data
7359
+ npx whoburnedmore verify delisted? re-verify your usage to get back on (sends a detailed breakdown)
6568
7360
  npx whoburnedmore status check background-sync health (last sync, staleness)
6569
7361
  npx whoburnedmore uninstall-sync turn off the background sync
6570
7362
  npx whoburnedmore install-sync turn it back on after uninstalling
6571
7363
 
6572
- Background sync is on by default: after your first run, your page refreshes
6573
- automatically every 15 min (\`uninstall-sync\` to stop). Your dashboard is public on
6574
- the leaderboard as an anonymous burner \u2014 sign in on whoburnedmore.com to claim
6575
- it (handle + X) and own your rank, or run \`private\`/\`remove\` to pull it. Only
7364
+ Your first run signs you in (we open a page, you approve a short code) and binds
7365
+ this machine to your account \u2014 your usage lands on the leaderboard under your
7366
+ handle. Background sync is on by default: your page then refreshes automatically
7367
+ every 15 min (\`uninstall-sync\` to stop); run \`private\`/\`remove\` to pull it. Only
6576
7368
  daily aggregate numbers (date, tool, model, token counts, est. cost) ever leave
6577
7369
  your machine \u2014 never prompts, code, or file names. With --local, nothing leaves
6578
7370
  your machine at all.