whoburnedmore 0.9.19 → 0.9.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +883 -244
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -7,10 +7,9 @@ var __export = (target, all) => {
7
7
 
8
8
  // src/index.ts
9
9
  import { spawn } from "node:child_process";
10
- import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "node:fs";
11
- import { createRequire as createRequire4 } from "node:module";
10
+ import { createRequire as createRequire5 } from "node:module";
12
11
  import { platform as platform4 } from "node:os";
13
- import { join as join15 } from "node:path";
12
+ import { pathToFileURL } from "node:url";
14
13
  import { createInterface } from "node:readline/promises";
15
14
  import pc2 from "picocolors";
16
15
 
@@ -24,8 +23,8 @@ function parseOrg(args) {
24
23
  function parsePass(args) {
25
24
  return parseValueFlag(args, "--pass") ?? parseValueFlag(args, "--code");
26
25
  }
27
- function parseInstallToken(args) {
28
- return parseValueFlag(args, "--token");
26
+ function hasUnsafeInstallTokenArg(args) {
27
+ return args.some((arg) => arg === "--token" || arg.startsWith("--token="));
29
28
  }
30
29
  function applyScope(payload, flags) {
31
30
  if (flags.board) payload.board = flags.board;
@@ -66,9 +65,65 @@ function resolveCommand(args) {
66
65
  return words[0] ?? "run";
67
66
  }
68
67
 
68
+ // src/http-bounds.ts
69
+ async function readResponseBytesCapped(response, maxBytes) {
70
+ const limit = Math.max(1, maxBytes);
71
+ const declared = Number(response.headers.get("content-length"));
72
+ if (Number.isFinite(declared) && declared > limit) {
73
+ throw new Error("response body too large");
74
+ }
75
+ if (!response.body) throw new Error("response body missing");
76
+ const reader = response.body.getReader();
77
+ const chunks = [];
78
+ let total = 0;
79
+ try {
80
+ while (true) {
81
+ const { done, value } = await reader.read();
82
+ if (done) break;
83
+ if (!value) continue;
84
+ total += value.byteLength;
85
+ if (total > limit) {
86
+ await reader.cancel("response body too large").catch(() => void 0);
87
+ throw new Error("response body too large");
88
+ }
89
+ chunks.push(value);
90
+ }
91
+ } finally {
92
+ reader.releaseLock();
93
+ }
94
+ const bytes = new Uint8Array(total);
95
+ let offset = 0;
96
+ for (const chunk of chunks) {
97
+ bytes.set(chunk, offset);
98
+ offset += chunk.byteLength;
99
+ }
100
+ return bytes;
101
+ }
102
+ async function readTextResponseCapped(response, maxBytes) {
103
+ return new TextDecoder("utf-8", { fatal: true }).decode(
104
+ await readResponseBytesCapped(response, maxBytes)
105
+ );
106
+ }
107
+ async function readJsonResponseCapped(response, maxBytes) {
108
+ return JSON.parse(await readTextResponseCapped(response, maxBytes));
109
+ }
110
+
69
111
  // src/api.ts
112
+ var DEFAULT_API_BASE = "https://api.whoburnedmore.com";
113
+ var MAX_SERVER_RESPONSE_BYTES = 2 * 1024 * 1024;
70
114
  function apiBase() {
71
- return process.env.WHOBURNEDMORE_API ?? "https://api.whoburnedmore.com";
115
+ const raw = process.env.WHOBURNEDMORE_API?.trim() || DEFAULT_API_BASE;
116
+ try {
117
+ const url = new URL(raw);
118
+ const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
119
+ const trustedTransport = url.protocol === "https:" || url.protocol === "http:" && loopback;
120
+ if (!trustedTransport || url.username || url.password || url.pathname !== "" && url.pathname !== "/" || url.search || url.hash) {
121
+ throw new Error("untrusted");
122
+ }
123
+ return url.origin;
124
+ } catch {
125
+ throw new Error("WHOBURNEDMORE_API must be a trusted HTTPS or loopback HTTP origin");
126
+ }
72
127
  }
73
128
  function webBase() {
74
129
  return process.env.WHOBURNEDMORE_WEB ?? "https://whoburnedmore.com";
@@ -88,7 +143,7 @@ function isOpenableUrl(url) {
88
143
  return /^(https?|file):\/\//.test(url);
89
144
  }
90
145
  async function readJson(res) {
91
- const text = await res.text();
146
+ const text = await readTextResponseCapped(res, MAX_SERVER_RESPONSE_BYTES);
92
147
  if (!text) return {};
93
148
  try {
94
149
  return JSON.parse(text);
@@ -109,7 +164,8 @@ async function send(method, path, body, token) {
109
164
  ...body === void 0 ? {} : { body: JSON.stringify(body) },
110
165
  // Bound the request so a slow/black-holing/hostile server can't hang the CLI
111
166
  // — or the unattended 15-minute background sync — indefinitely.
112
- signal: AbortSignal.timeout(3e4)
167
+ signal: AbortSignal.timeout(3e4),
168
+ redirect: "error"
113
169
  });
114
170
  } catch {
115
171
  throw new Error(
@@ -160,9 +216,9 @@ async function devicePoll(deviceCode) {
160
216
  }
161
217
  return body;
162
218
  }
163
- async function refreshCliToken(anonKey) {
219
+ async function refreshCliToken(refreshToken) {
164
220
  try {
165
- const { status, body } = await post("/v1/auth/cli/refresh", { anonKey });
221
+ const { status, body } = await post("/v1/auth/cli/refresh", { refreshToken });
166
222
  if (status === 200 && typeof body.token === "string" && body.token) {
167
223
  return { token: body.token, handle: body.handle ?? "" };
168
224
  }
@@ -172,14 +228,20 @@ async function refreshCliToken(anonKey) {
172
228
  }
173
229
  async function bindDeviceKey(token, anonKey) {
174
230
  try {
175
- const { status } = await post(
231
+ const { status, body } = await post(
176
232
  "/v1/me/devices/bind",
177
233
  { anonKey },
178
234
  token
179
235
  );
180
- return status === 200 || status === 409;
236
+ if (status === 200) {
237
+ return {
238
+ definitive: true,
239
+ ...typeof body.refreshToken === "string" && body.refreshToken ? { refreshToken: body.refreshToken } : {}
240
+ };
241
+ }
242
+ return { definitive: status === 409 };
181
243
  } catch {
182
- return false;
244
+ return { definitive: false };
183
245
  }
184
246
  }
185
247
  async function submit(token, payload) {
@@ -228,31 +290,112 @@ async function redeemServerInstall(token, anonKey) {
228
290
 
229
291
  // src/autosync.ts
230
292
  import { spawnSync } from "node:child_process";
293
+ import { createRequire } from "node:module";
231
294
  import {
232
295
  existsSync as existsSync2,
233
296
  mkdirSync as mkdirSync2,
234
- readFileSync as readFileSync2,
297
+ readFileSync,
235
298
  renameSync as renameSync2,
236
299
  rmSync as rmSync2,
237
300
  statSync,
238
301
  writeFileSync as writeFileSync2
239
302
  } from "node:fs";
240
303
  import { homedir as homedir2, platform } from "node:os";
241
- import { dirname, join as join2, posix, win32 } from "node:path";
304
+ import { dirname as dirname2, join as join2, posix, win32 } from "node:path";
242
305
 
243
306
  // src/config.ts
307
+ import { createHash, randomBytes as randomBytes2 } from "node:crypto";
308
+ import {
309
+ existsSync
310
+ } from "node:fs";
311
+ import { homedir } from "node:os";
312
+ import { join } from "node:path";
313
+
314
+ // src/safe-local-file.ts
244
315
  import { randomBytes } from "node:crypto";
245
316
  import {
246
317
  chmodSync,
247
- existsSync,
318
+ closeSync,
319
+ constants,
320
+ fstatSync,
248
321
  mkdirSync,
249
- readFileSync,
322
+ openSync,
323
+ readSync,
250
324
  renameSync,
251
325
  rmSync,
252
326
  writeFileSync
253
327
  } from "node:fs";
254
- import { homedir } from "node:os";
255
- import { join } from "node:path";
328
+ import { dirname } from "node:path";
329
+ function readTextFileSyncCapped(path, maxBytes) {
330
+ const limit = Math.max(1, maxBytes);
331
+ let fd = null;
332
+ try {
333
+ fd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
334
+ const metadata = fstatSync(fd);
335
+ if (!metadata.isFile() || metadata.size > limit) return null;
336
+ const chunks = [];
337
+ let total = 0;
338
+ while (true) {
339
+ const allowance = Math.min(64 * 1024, limit + 1 - total);
340
+ if (allowance <= 0) return null;
341
+ const chunk = Buffer.allocUnsafe(allowance);
342
+ const count = readSync(fd, chunk, 0, allowance, null);
343
+ if (count === 0) break;
344
+ total += count;
345
+ if (total > limit) return null;
346
+ chunks.push(chunk.subarray(0, count));
347
+ }
348
+ return Buffer.concat(chunks, total).toString("utf8");
349
+ } catch {
350
+ return null;
351
+ } finally {
352
+ if (fd !== null) {
353
+ try {
354
+ closeSync(fd);
355
+ } catch {
356
+ }
357
+ }
358
+ }
359
+ }
360
+ function writePrivateFileAtomic(path, content, maxBytes) {
361
+ if (Buffer.byteLength(content, "utf8") > Math.max(1, maxBytes)) return false;
362
+ const dir = dirname(path);
363
+ let tmp = null;
364
+ try {
365
+ mkdirSync(dir, { recursive: true, mode: 448 });
366
+ try {
367
+ chmodSync(dir, 448);
368
+ } catch {
369
+ }
370
+ tmp = `${path}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`;
371
+ writeFileSync(tmp, content, { encoding: "utf8", flag: "wx", mode: 384 });
372
+ try {
373
+ chmodSync(tmp, 384);
374
+ } catch {
375
+ }
376
+ renameSync(tmp, path);
377
+ tmp = null;
378
+ return true;
379
+ } catch {
380
+ return false;
381
+ } finally {
382
+ if (tmp) {
383
+ try {
384
+ rmSync(tmp, { force: true });
385
+ } catch {
386
+ }
387
+ }
388
+ }
389
+ }
390
+
391
+ // src/config.ts
392
+ var MAX_CONFIG_BYTES = 1024 * 1024;
393
+ function deviceKeyHash(anonKey) {
394
+ return createHash("sha256").update(anonKey).digest("hex");
395
+ }
396
+ function needsDeviceBind(config) {
397
+ return !config.deviceBoundAt || !config.refreshToken;
398
+ }
256
399
  function defaultConfigDir() {
257
400
  const override = process.env.WHOBURNEDMORE_CONFIG_DIR?.trim();
258
401
  if (override) return override;
@@ -262,10 +405,13 @@ function loadConfig(dir = defaultConfigDir()) {
262
405
  const file = join(dir, "config.json");
263
406
  if (!existsSync(file)) return null;
264
407
  try {
265
- const parsed = JSON.parse(readFileSync(file, "utf8"));
408
+ const content = readTextFileSyncCapped(file, MAX_CONFIG_BYTES);
409
+ if (content === null) return null;
410
+ const parsed = JSON.parse(content);
266
411
  const config = {};
267
412
  if (typeof parsed.anonKey === "string") config.anonKey = parsed.anonKey;
268
413
  if (typeof parsed.cliToken === "string") config.cliToken = parsed.cliToken;
414
+ if (typeof parsed.refreshToken === "string") config.refreshToken = parsed.refreshToken;
269
415
  if (typeof parsed.handle === "string") config.handle = parsed.handle;
270
416
  if (typeof parsed.lastSyncAt === "number" && Number.isFinite(parsed.lastSyncAt))
271
417
  config.lastSyncAt = parsed.lastSyncAt;
@@ -281,28 +427,15 @@ function loadConfig(dir = defaultConfigDir()) {
281
427
  }
282
428
  }
283
429
  function saveConfig(dir = defaultConfigDir(), config = {}) {
284
- mkdirSync(dir, { recursive: true });
285
430
  const file = join(dir, "config.json");
286
- const tmp = join(dir, `config.json.${process.pid}.tmp`);
287
- try {
288
- writeFileSync(tmp, JSON.stringify(config, null, 2), { mode: 384 });
289
- try {
290
- chmodSync(tmp, 384);
291
- } catch {
292
- }
293
- renameSync(tmp, file);
294
- } catch (err) {
295
- try {
296
- rmSync(tmp, { force: true });
297
- } catch {
298
- }
299
- throw err;
431
+ if (!writePrivateFileAtomic(file, JSON.stringify(config, null, 2), MAX_CONFIG_BYTES)) {
432
+ throw new Error("could not save private CLI configuration");
300
433
  }
301
434
  }
302
435
  function ensureAnonKey(dir = defaultConfigDir()) {
303
436
  const config = loadConfig(dir) ?? {};
304
437
  if (config.anonKey) return config.anonKey;
305
- const anonKey = randomBytes(32).toString("hex");
438
+ const anonKey = randomBytes2(32).toString("hex");
306
439
  saveConfig(dir, { ...config, anonKey });
307
440
  return anonKey;
308
441
  }
@@ -316,7 +449,12 @@ function recordDeviceBound(dir = defaultConfigDir(), when = Date.now()) {
316
449
  }
317
450
  function saveAuth(dir = defaultConfigDir(), auth = { cliToken: "" }) {
318
451
  const config = loadConfig(dir) ?? {};
319
- saveConfig(dir, { ...config, cliToken: auth.cliToken, handle: auth.handle });
452
+ saveConfig(dir, {
453
+ ...config,
454
+ cliToken: auth.cliToken,
455
+ handle: auth.handle,
456
+ ...auth.refreshToken ? { refreshToken: auth.refreshToken } : {}
457
+ });
320
458
  }
321
459
  function clearAuth(dir = defaultConfigDir()) {
322
460
  const config = loadConfig(dir);
@@ -341,7 +479,9 @@ var STABLE_NPM_CANDIDATES = [
341
479
  "/usr/local/bin/npm",
342
480
  "/usr/bin/npm"
343
481
  ];
344
- var LATEST_PACKAGE_SPEC = "whoburnedmore@latest";
482
+ var require2 = createRequire(import.meta.url);
483
+ var CLI_VERSION = require2("../package.json").version;
484
+ var SCHEDULED_PACKAGE_SPEC = `whoburnedmore@${CLI_VERSION}`;
345
485
  var SYNC_PATH_DIRS = [
346
486
  "/opt/homebrew/bin",
347
487
  "/usr/local/bin",
@@ -351,7 +491,7 @@ var SYNC_PATH_DIRS = [
351
491
  "/sbin"
352
492
  ];
353
493
  function syncPathEnv(npmPath = resolveNpmPath()) {
354
- const dir = dirname(npmPath);
494
+ const dir = dirname2(npmPath);
355
495
  const dirs = [];
356
496
  if (dir && dir !== "." && dir !== "/" && dir !== npmPath) dirs.push(dir);
357
497
  for (const d of SYNC_PATH_DIRS) {
@@ -481,7 +621,7 @@ function syncCommandArgs(npmPath = resolveNpmPath()) {
481
621
  "--yes",
482
622
  "--ignore-scripts",
483
623
  "--package",
484
- LATEST_PACKAGE_SPEC,
624
+ SCHEDULED_PACKAGE_SPEC,
485
625
  "--",
486
626
  "whoburnedmore",
487
627
  "sync"
@@ -696,8 +836,8 @@ function autoSyncInstalled() {
696
836
  }
697
837
  function readInstalledSystemd() {
698
838
  try {
699
- return `${readFileSync2(systemdServicePath(), "utf8")}
700
- ${readFileSync2(systemdTimerPath(), "utf8")}`;
839
+ return `${readFileSync(systemdServicePath(), "utf8")}
840
+ ${readFileSync(systemdTimerPath(), "utf8")}`;
701
841
  } catch {
702
842
  return null;
703
843
  }
@@ -709,7 +849,7 @@ ${buildSystemdTimer()}`;
709
849
  function readInstalledAgent() {
710
850
  if (platform() === "darwin") {
711
851
  const p = launchAgentPath();
712
- return existsSync2(p) ? readFileSync2(p, "utf8") : null;
852
+ return existsSync2(p) ? readFileSync(p, "utf8") : null;
713
853
  }
714
854
  if (platform() === "linux") {
715
855
  const mech = linuxSyncMechanism();
@@ -797,7 +937,7 @@ async function daemonLoop(deps) {
797
937
 
798
938
  // src/collect.ts
799
939
  import { execFile } from "node:child_process";
800
- import { createRequire as createRequire3 } from "node:module";
940
+ import { createRequire as createRequire4 } from "node:module";
801
941
  import { dirname as dirname6, join as join13 } from "node:path";
802
942
  import { promisify } from "node:util";
803
943
 
@@ -807,7 +947,7 @@ import { homedir as homedir4 } from "node:os";
807
947
  import { join as join5 } from "node:path";
808
948
 
809
949
  // src/native/claude.ts
810
- import { readdir, readFile as readFile2 } from "node:fs/promises";
950
+ import { readdir } from "node:fs/promises";
811
951
  import { homedir as homedir3 } from "node:os";
812
952
  import { join as join4 } from "node:path";
813
953
 
@@ -6248,7 +6388,12 @@ function estimateCostUSD(model, t) {
6248
6388
 
6249
6389
  // ../shared/dist/index.js
6250
6390
  var DateString = external_exports.string().regex(/^\d{4}-\d{2}-\d{2}$/, "must be YYYY-MM-DD");
6251
- var tokenCount = external_exports.number().int().nonnegative();
6391
+ var MAX_TOKEN_COUNT = 1e11;
6392
+ var MAX_ROLLUP_COUNT = 1e9;
6393
+ var MAX_COST_USD = 1e5;
6394
+ var tokenCount = external_exports.number().int().nonnegative().max(MAX_TOKEN_COUNT);
6395
+ var rollupCount = external_exports.number().int().nonnegative().max(MAX_ROLLUP_COUNT);
6396
+ var costUSD = external_exports.number().nonnegative().max(MAX_COST_USD);
6252
6397
  var ConnectorProvider = external_exports.enum([
6253
6398
  "anthropic-api",
6254
6399
  "openai-api",
@@ -6278,7 +6423,7 @@ var DailyUsageEntry = external_exports.object({
6278
6423
  cacheCreationTokens: tokenCount,
6279
6424
  cacheReadTokens: tokenCount,
6280
6425
  /** Estimated cost in USD for this entry. */
6281
- costUSD: external_exports.number().nonnegative(),
6426
+ costUSD,
6282
6427
  /** Where this entry came from. Defaults to the local CLI for back-compat. */
6283
6428
  origin: UsageOrigin.default("cli"),
6284
6429
  /** True when the numbers come from a provider's authoritative usage API. */
@@ -6293,7 +6438,7 @@ var DailyUsageEntry = external_exports.object({
6293
6438
  * cannot see request ids) omit it, and an omitted fingerprint is never
6294
6439
  * penalized.
6295
6440
  */
6296
- requestCount: external_exports.number().int().nonnegative().optional()
6441
+ requestCount: rollupCount.optional()
6297
6442
  });
6298
6443
  var Timestamp = external_exports.string().min(1).max(40);
6299
6444
  var SessionEntry = external_exports.object({
@@ -6304,49 +6449,76 @@ var SessionEntry = external_exports.object({
6304
6449
  outputTokens: tokenCount,
6305
6450
  cacheCreationTokens: tokenCount,
6306
6451
  cacheReadTokens: tokenCount,
6307
- costUSD: external_exports.number().nonnegative(),
6452
+ costUSD,
6308
6453
  lastActivity: Timestamp,
6309
6454
  /** Number of assistant messages in this session (from transcripts). Optional. */
6310
- messageCount: external_exports.number().int().nonnegative().optional()
6455
+ messageCount: rollupCount.optional()
6311
6456
  });
6312
6457
  var BlockEntry = external_exports.object({
6313
6458
  startTime: Timestamp,
6314
6459
  totalTokens: tokenCount,
6315
- costUSD: external_exports.number().nonnegative()
6460
+ costUSD
6316
6461
  });
6317
6462
  var ToolStat = external_exports.object({
6318
6463
  name: external_exports.string().min(1).max(128),
6319
- count: external_exports.number().int().nonnegative(),
6464
+ count: rollupCount,
6320
6465
  /** How many of those calls returned an error/interrupt (tool reliability). Optional. */
6321
- errors: external_exports.number().int().nonnegative().optional(),
6466
+ errors: rollupCount.optional(),
6322
6467
  /** Tokens burned on turns that used this tool (turn tokens split across its tool calls). Optional. */
6323
- tokens: external_exports.number().int().nonnegative().optional()
6468
+ tokens: tokenCount.optional()
6324
6469
  });
6325
6470
  var AgentStat = external_exports.object({
6326
6471
  /** Total assistant messages across transcripts. */
6327
- messageCount: external_exports.number().int().nonnegative(),
6472
+ messageCount: rollupCount,
6328
6473
  /** Assistant messages that ran inside a subagent sidechain. */
6329
- subagentMessages: external_exports.number().int().nonnegative(),
6474
+ subagentMessages: rollupCount,
6330
6475
  /** Tokens spent inside subagent sidechains. */
6331
- subagentTokens: external_exports.number().int().nonnegative(),
6476
+ subagentTokens: tokenCount,
6332
6477
  /** Total tokens observed across transcripts (denominator for the share). */
6333
- totalTokens: external_exports.number().int().nonnegative(),
6478
+ totalTokens: tokenCount,
6334
6479
  /**
6335
6480
  * Messages the human actually sent (their prompts) — non-sidechain user turns
6336
6481
  * carrying real text, NOT tool results or injected/meta turns. Denominator for
6337
6482
  * "avg cost per message". Optional (back-compat with older CLIs).
6338
6483
  */
6339
- userMessageCount: external_exports.number().int().nonnegative().optional()
6484
+ userMessageCount: rollupCount.optional()
6340
6485
  });
6341
6486
  var SkillStat = external_exports.object({
6342
6487
  name: external_exports.string().min(1).max(128),
6343
- count: external_exports.number().int().nonnegative(),
6488
+ count: rollupCount,
6344
6489
  /** Tokens burned in records produced while this skill was active. Optional. */
6345
- tokens: external_exports.number().int().nonnegative().optional()
6490
+ tokens: tokenCount.optional()
6491
+ });
6492
+ var CodexReplayPriorRow = external_exports.object({
6493
+ model: external_exports.string().min(1).max(128),
6494
+ inputTokens: tokenCount,
6495
+ outputTokens: tokenCount,
6496
+ cacheCreationTokens: tokenCount,
6497
+ cacheReadTokens: tokenCount
6498
+ });
6499
+ var CodexReplayPriorScope = external_exports.object({
6500
+ date: DateString,
6501
+ rows: external_exports.array(CodexReplayPriorRow).min(1).max(100)
6346
6502
  });
6347
6503
  var SubmitPayload = external_exports.object({
6348
6504
  cliVersion: external_exports.string().min(1).max(32),
6349
6505
  entries: external_exports.array(DailyUsageEntry).min(1).max(2e4),
6506
+ /**
6507
+ * Dates observed only inside replayed Codex fork/subagent rollouts and absent
6508
+ * from a successful replay-aware parse. The API may remove an old
6509
+ * self-reported Codex scope for these targeted dates only when the account has
6510
+ * one confirmed device and the accompanying prior scope matches exactly.
6511
+ * Omitted on parser failure, timeout, capped payloads, and older clients.
6512
+ */
6513
+ codexReplayTombstoneDates: external_exports.array(DateString).max(5e3).optional(),
6514
+ /**
6515
+ * Compare-and-swap proof for destructive Codex correction. Each scope is the
6516
+ * exact model/token snapshot the legacy native reader would have submitted.
6517
+ * The API corrects only when its stored scope matches these rows exactly.
6518
+ */
6519
+ codexReplayPriorScopes: external_exports.array(CodexReplayPriorScope).max(5e3).optional(),
6520
+ /** sha256 of this machine's bound secret; required by the API for correction authority. */
6521
+ deviceKeyHash: external_exports.string().regex(/^[a-f0-9]{64}$/).optional(),
6350
6522
  /** Optional per-conversation rollups (ccusage session). Back-compat: omittable. */
6351
6523
  sessions: external_exports.array(SessionEntry).max(1e4).optional(),
6352
6524
  /** Optional time-window rollups (ccusage blocks) for peak-hours analysis. */
@@ -6420,6 +6592,7 @@ function entryTotalTokens(e) {
6420
6592
  }
6421
6593
  var LeaderboardPeriod = external_exports.enum(["today", "7d", "30d", "all"]);
6422
6594
  var LeaderboardMetric = external_exports.enum(["tokens", "cost"]);
6595
+ var InsightsPeriod = external_exports.enum(["7d", "30d", "all"]);
6423
6596
  var OrgType = external_exports.enum(["company", "hackathon", "hackerhouse"]);
6424
6597
  var MemberRole = external_exports.enum(["owner", "admin", "member"]);
6425
6598
  var OrgBoardVisibility = external_exports.enum(["public", "members"]);
@@ -6558,18 +6731,63 @@ var OrgJoinInput = external_exports.object({
6558
6731
  });
6559
6732
 
6560
6733
  // src/native/file-cache.ts
6561
- import { mkdirSync as mkdirSync3, renameSync as renameSync3, writeFileSync as writeFileSync3 } from "node:fs";
6562
- import { readFile, stat } from "node:fs/promises";
6563
- import { dirname as dirname2, join as join3 } from "node:path";
6734
+ import {
6735
+ chmodSync as chmodSync2,
6736
+ constants as constants2,
6737
+ mkdirSync as mkdirSync3,
6738
+ renameSync as renameSync3,
6739
+ unlinkSync,
6740
+ writeFileSync as writeFileSync3
6741
+ } from "node:fs";
6742
+ import { open, stat } from "node:fs/promises";
6743
+ import { randomBytes as randomBytes3 } from "node:crypto";
6744
+ import { dirname as dirname3, join as join3 } from "node:path";
6745
+ var MAX_NATIVE_FILE_BYTES = 64 * 1024 * 1024;
6746
+ var MAX_NATIVE_CACHE_BYTES = 128 * 1024 * 1024;
6747
+ var READ_CHUNK_BYTES = 64 * 1024;
6748
+ async function readTextFileCapped(path, opts = {}) {
6749
+ const maxBytes = Math.max(1, opts.maxBytes ?? MAX_NATIVE_FILE_BYTES);
6750
+ const deadline = opts.deadline ?? Number.POSITIVE_INFINITY;
6751
+ const now = opts.now ?? Date.now;
6752
+ let handle = null;
6753
+ try {
6754
+ handle = await open(
6755
+ path,
6756
+ constants2.O_RDONLY | (constants2.O_NOFOLLOW ?? 0)
6757
+ );
6758
+ const metadata = await handle.stat();
6759
+ if (!metadata.isFile()) return { ok: false, reason: "unreadable" };
6760
+ if (metadata.size > maxBytes) return { ok: false, reason: "too-large" };
6761
+ const chunks = [];
6762
+ let total = 0;
6763
+ while (true) {
6764
+ if (now() > deadline) return { ok: false, reason: "timed-out" };
6765
+ const allowance = Math.min(READ_CHUNK_BYTES, maxBytes + 1 - total);
6766
+ if (allowance <= 0) return { ok: false, reason: "too-large" };
6767
+ const chunk = Buffer.allocUnsafe(allowance);
6768
+ const { bytesRead } = await handle.read(chunk, 0, allowance, null);
6769
+ if (bytesRead === 0) break;
6770
+ total += bytesRead;
6771
+ if (total > maxBytes) return { ok: false, reason: "too-large" };
6772
+ chunks.push(chunk.subarray(0, bytesRead));
6773
+ }
6774
+ return { ok: true, content: Buffer.concat(chunks, total).toString("utf8") };
6775
+ } catch {
6776
+ return { ok: false, reason: "unreadable" };
6777
+ } finally {
6778
+ await handle?.close().catch(() => void 0);
6779
+ }
6780
+ }
6564
6781
  function nativeCachePath(reader, env = process.env) {
6565
6782
  const override = env.WHOBURNEDMORE_CONFIG_DIR?.trim();
6566
6783
  const dir = override || defaultConfigDir();
6567
6784
  return join3(dir, `native-cache-${reader}.json`);
6568
6785
  }
6569
- async function loadCache(path, version) {
6786
+ async function loadCache(path, version, maxBytes) {
6570
6787
  try {
6571
- const raw = await readFile(path, "utf8");
6572
- const parsed = JSON.parse(raw);
6788
+ const read = await readTextFileCapped(path, { maxBytes });
6789
+ if (!read.ok) return {};
6790
+ const parsed = JSON.parse(read.content);
6573
6791
  if (parsed && parsed.v === version && parsed.files && typeof parsed.files === "object") {
6574
6792
  return parsed.files;
6575
6793
  }
@@ -6577,18 +6795,34 @@ async function loadCache(path, version) {
6577
6795
  }
6578
6796
  return {};
6579
6797
  }
6580
- function saveCache(path, version, files) {
6798
+ function saveCache(path, version, files, maxBytes) {
6799
+ let tmp = null;
6581
6800
  try {
6582
- mkdirSync3(dirname2(path), { recursive: true });
6583
- const tmp = `${path}.tmp-${process.pid}`;
6584
- writeFileSync3(tmp, JSON.stringify({ v: version, files }));
6801
+ const dir = dirname3(path);
6802
+ mkdirSync3(dir, { recursive: true, mode: 448 });
6803
+ chmodSync2(dir, 448);
6804
+ tmp = `${path}.tmp-${process.pid}-${randomBytes3(8).toString("hex")}`;
6805
+ const serialized = JSON.stringify({ v: version, files });
6806
+ if (Buffer.byteLength(serialized, "utf8") > maxBytes) return;
6807
+ writeFileSync3(tmp, serialized, { encoding: "utf8", flag: "wx", mode: 384 });
6808
+ chmodSync2(tmp, 384);
6585
6809
  renameSync3(tmp, path);
6810
+ tmp = null;
6586
6811
  } catch {
6812
+ } finally {
6813
+ if (tmp) {
6814
+ try {
6815
+ unlinkSync(tmp);
6816
+ } catch {
6817
+ }
6818
+ }
6587
6819
  }
6588
6820
  }
6589
6821
  async function readFilesWithCache(opts) {
6590
6822
  const now = opts.now ?? Date.now;
6591
- const cached = await loadCache(opts.cachePath, opts.version);
6823
+ const maxFileBytes = Math.max(1, opts.maxFileBytes ?? MAX_NATIVE_FILE_BYTES);
6824
+ const maxCacheBytes = Math.max(1, opts.maxCacheBytes ?? MAX_NATIVE_CACHE_BYTES);
6825
+ const cached = await loadCache(opts.cachePath, opts.version, maxCacheBytes);
6592
6826
  const fresh = {};
6593
6827
  const itemsByFile = [];
6594
6828
  let filesRead = 0;
@@ -6602,6 +6836,9 @@ async function readFilesWithCache(opts) {
6602
6836
  } catch {
6603
6837
  continue;
6604
6838
  }
6839
+ if (size > maxFileBytes) {
6840
+ continue;
6841
+ }
6605
6842
  const hit = cached[f];
6606
6843
  if (hit && hit.size === size && hit.mtimeMs === mtimeMs) {
6607
6844
  fresh[f] = hit;
@@ -6609,21 +6846,27 @@ async function readFilesWithCache(opts) {
6609
6846
  continue;
6610
6847
  }
6611
6848
  if (now() > opts.deadline) {
6612
- saveCache(opts.cachePath, opts.version, { ...cached, ...fresh });
6849
+ saveCache(opts.cachePath, opts.version, { ...cached, ...fresh }, maxCacheBytes);
6613
6850
  return { itemsByFile: null, filesRead, timedOut: true };
6614
6851
  }
6615
- let content;
6616
- try {
6617
- content = await readFile(f, "utf8");
6618
- } catch {
6852
+ const read = await readTextFileCapped(f, {
6853
+ maxBytes: maxFileBytes,
6854
+ deadline: opts.deadline,
6855
+ now
6856
+ });
6857
+ if (!read.ok) {
6858
+ if (read.reason === "timed-out") {
6859
+ saveCache(opts.cachePath, opts.version, { ...cached, ...fresh }, maxCacheBytes);
6860
+ return { itemsByFile: null, filesRead, timedOut: true };
6861
+ }
6619
6862
  continue;
6620
6863
  }
6621
- const items = opts.parseFile(content, f);
6864
+ const items = opts.parseFile(read.content, f);
6622
6865
  fresh[f] = { size, mtimeMs, items };
6623
6866
  itemsByFile.push(items);
6624
6867
  filesRead += 1;
6625
6868
  }
6626
- saveCache(opts.cachePath, opts.version, fresh);
6869
+ saveCache(opts.cachePath, opts.version, fresh, maxCacheBytes);
6627
6870
  return { itemsByFile, filesRead, timedOut: false };
6628
6871
  }
6629
6872
 
@@ -6866,13 +7109,18 @@ async function collectClaudeRequests(env = process.env, opts = {}) {
6866
7109
  if (now() > deadline) {
6867
7110
  return { requests: [...acc.values()], found: true, timedOut: true };
6868
7111
  }
6869
- let content;
6870
- try {
6871
- content = await readFile2(f, "utf8");
6872
- } catch {
7112
+ const read = await readTextFileCapped(f, {
7113
+ maxBytes: opts.maxFileBytes ?? MAX_NATIVE_FILE_BYTES,
7114
+ deadline,
7115
+ now
7116
+ });
7117
+ if (!read.ok) {
7118
+ if (read.reason === "timed-out") {
7119
+ return { requests: [...acc.values()], found: true, timedOut: true };
7120
+ }
6873
7121
  continue;
6874
7122
  }
6875
- accumulateClaudeLines(acc, splitLines(content));
7123
+ accumulateClaudeLines(acc, splitLines(read.content));
6876
7124
  }
6877
7125
  return { requests: [...acc.values()], found: true };
6878
7126
  }
@@ -7277,28 +7525,54 @@ function parseContinueJsonl(content) {
7277
7525
  }
7278
7526
  return mapContinueRecords(records);
7279
7527
  }
7280
- async function listJsonl2(dir) {
7281
- let dirents;
7282
- try {
7283
- dirents = await readdir2(dir, { withFileTypes: true });
7284
- } catch {
7285
- return [];
7286
- }
7287
- const out = [];
7288
- for (const d of dirents) {
7289
- const full = join6(dir, d.name);
7290
- if (d.isDirectory()) out.push(...await listJsonl2(full));
7291
- else if (d.isFile() && d.name === "tokensGenerated.jsonl") out.push(full);
7528
+ async function listJsonl2(root, opts) {
7529
+ const queue = [{ dir: root, depth: 0 }];
7530
+ let queueIndex = 0;
7531
+ const files = [];
7532
+ let visitedEntries = 0;
7533
+ const maxEntries = Math.max(1e3, opts.maxFiles * 20);
7534
+ while (queueIndex < queue.length) {
7535
+ if (opts.now() > opts.deadline) return { files: [], aborted: true };
7536
+ const current = queue[queueIndex++];
7537
+ let dirents;
7538
+ try {
7539
+ dirents = await readdir2(current.dir, { withFileTypes: true });
7540
+ } catch {
7541
+ continue;
7542
+ }
7543
+ for (const d of dirents) {
7544
+ visitedEntries += 1;
7545
+ if (visitedEntries > maxEntries || opts.now() > opts.deadline) {
7546
+ return { files: [], aborted: true };
7547
+ }
7548
+ const full = join6(current.dir, d.name);
7549
+ if (d.isDirectory()) {
7550
+ if (current.depth >= 32) return { files: [], aborted: true };
7551
+ queue.push({ dir: full, depth: current.depth + 1 });
7552
+ } else if (d.isFile() && d.name === "tokensGenerated.jsonl") {
7553
+ files.push(full);
7554
+ if (files.length > opts.maxFiles) return { files: [], aborted: true };
7555
+ }
7556
+ }
7292
7557
  }
7293
- return out;
7558
+ return { files, aborted: false };
7294
7559
  }
7295
7560
  var CONTINUE_CACHE_VERSION = 2;
7296
7561
  async function collectContinue(opts = {}) {
7297
7562
  const env = opts.env ?? process.env;
7298
7563
  const home = opts.continueDir ?? join6(env.HOME || homedir5(), ".continue");
7299
- const files = await listJsonl2(join6(home, "dev_data"));
7300
- if (files.length === 0) return { entries: [], found: false, filesScanned: 0 };
7301
7564
  const now = opts.now ?? Date.now;
7565
+ const deadline = now() + (opts.budgetMs ?? NATIVE_READ_BUDGET_MS);
7566
+ const discovery = await listJsonl2(join6(home, "dev_data"), {
7567
+ deadline,
7568
+ now,
7569
+ maxFiles: Math.max(1, opts.maxFiles ?? 1e4)
7570
+ });
7571
+ if (discovery.aborted) {
7572
+ return { entries: [], found: false, filesScanned: 0, timedOut: true };
7573
+ }
7574
+ const files = discovery.files;
7575
+ if (files.length === 0) return { entries: [], found: false, filesScanned: 0 };
7302
7576
  const res = await readFilesWithCache({
7303
7577
  files,
7304
7578
  cachePath: opts.cachePath ?? nativeCachePath("continue", env),
@@ -7311,7 +7585,7 @@ async function collectContinue(opts = {}) {
7311
7585
  e.costUSD,
7312
7586
  e.requestCount ?? 0
7313
7587
  ]),
7314
- deadline: now() + (opts.budgetMs ?? NATIVE_READ_BUDGET_MS),
7588
+ deadline,
7315
7589
  now
7316
7590
  });
7317
7591
  if (!res.itemsByFile) {
@@ -7350,15 +7624,15 @@ async function collectContinue(opts = {}) {
7350
7624
 
7351
7625
  // src/cursor.ts
7352
7626
  import { spawnSync as spawnSync3 } from "node:child_process";
7353
- import { existsSync as existsSync3 } from "node:fs";
7354
- import { createRequire as createRequire2 } from "node:module";
7627
+ import { existsSync as existsSync3, realpathSync, statSync as statSync3 } from "node:fs";
7628
+ import { createRequire as createRequire3 } from "node:module";
7355
7629
  import { homedir as homedir6, platform as platform2 } from "node:os";
7356
7630
  import { join as join8 } from "node:path";
7357
7631
 
7358
7632
  // src/tokscale.ts
7359
7633
  import { spawnSync as spawnSync2 } from "node:child_process";
7360
- import { createRequire } from "node:module";
7361
- import { dirname as dirname3, join as join7 } from "node:path";
7634
+ import { createRequire as createRequire2 } from "node:module";
7635
+ import { dirname as dirname4, join as join7 } from "node:path";
7362
7636
  var LOOKBACK_DAYS = 30;
7363
7637
  function num3(n) {
7364
7638
  const v = Math.round(Number(n));
@@ -7377,9 +7651,9 @@ function mapTokscaleDay(date, json) {
7377
7651
  const outputTokens = num3(e.output) + num3(e.reasoning);
7378
7652
  const cacheCreationTokens = num3(e.cacheWrite);
7379
7653
  const cacheReadTokens = num3(e.cacheRead);
7380
- const costUSD = numCost(e.cost);
7654
+ const costUSD2 = numCost(e.cost);
7381
7655
  const total = inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens;
7382
- if (total === 0 && costUSD === 0) continue;
7656
+ if (total === 0 && costUSD2 === 0) continue;
7383
7657
  out.push({
7384
7658
  date,
7385
7659
  tool: "cursor",
@@ -7388,7 +7662,7 @@ function mapTokscaleDay(date, json) {
7388
7662
  outputTokens,
7389
7663
  cacheCreationTokens,
7390
7664
  cacheReadTokens,
7391
- costUSD,
7665
+ costUSD: costUSD2,
7392
7666
  origin: "cli",
7393
7667
  verified: false
7394
7668
  });
@@ -7397,12 +7671,12 @@ function mapTokscaleDay(date, json) {
7397
7671
  }
7398
7672
  function resolveTokscaleBin() {
7399
7673
  try {
7400
- const require3 = createRequire(import.meta.url);
7401
- const pkgPath = require3.resolve("tokscale/package.json");
7402
- const pkg = require3("tokscale/package.json");
7674
+ const require4 = createRequire2(import.meta.url);
7675
+ const pkgPath = require4.resolve("tokscale/package.json");
7676
+ const pkg = require4("tokscale/package.json");
7403
7677
  const rel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.tokscale ?? "";
7404
7678
  if (!rel) return null;
7405
- const binPath = join7(dirname3(pkgPath), rel);
7679
+ const binPath = join7(dirname4(pkgPath), rel);
7406
7680
  if (/\.(c|m)?js$/.test(binPath)) {
7407
7681
  return { cmd: process.execPath, prefixArgs: [binPath] };
7408
7682
  }
@@ -7459,31 +7733,47 @@ function collectCursorViaTokscale(lookbackDays = LOOKBACK_DAYS) {
7459
7733
 
7460
7734
  // src/cursor.ts
7461
7735
  var EVENTS_URL = "https://cursor.com/api/dashboard/get-filtered-usage-events";
7736
+ var MAX_CURSOR_PAGE_BYTES = 4 * 1024 * 1024;
7462
7737
  function cursorDbPath() {
7463
7738
  const home = homedir6();
7464
7739
  const os = platform2();
7465
7740
  const p = os === "darwin" ? join8(home, "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb") : os === "win32" ? join8(process.env.APPDATA ?? join8(home, "AppData", "Roaming"), "Cursor", "User", "globalStorage", "state.vscdb") : join8(process.env.XDG_CONFIG_HOME ?? join8(home, ".config"), "Cursor", "User", "globalStorage", "state.vscdb");
7466
7741
  return existsSync3(p) ? p : null;
7467
7742
  }
7743
+ function trustedSqliteBinaries() {
7744
+ const candidates = platform2() === "darwin" ? ["/usr/bin/sqlite3", "/opt/homebrew/bin/sqlite3", "/usr/local/bin/sqlite3"] : platform2() === "linux" ? ["/usr/bin/sqlite3", "/usr/local/bin/sqlite3"] : [];
7745
+ const trusted = [];
7746
+ for (const candidate of candidates) {
7747
+ try {
7748
+ const resolved = realpathSync(candidate);
7749
+ const stat2 = statSync3(resolved);
7750
+ if (stat2.isFile() && (stat2.mode & 18) === 0) trusted.push(resolved);
7751
+ } catch {
7752
+ }
7753
+ }
7754
+ return [...new Set(trusted)];
7755
+ }
7468
7756
  function readCursorToken(db) {
7469
- const require3 = createRequire2(import.meta.url);
7757
+ const require4 = createRequire3(import.meta.url);
7470
7758
  try {
7471
- const { DatabaseSync } = require3("node:sqlite");
7759
+ const { DatabaseSync } = require4("node:sqlite");
7472
7760
  const d = new DatabaseSync(db, { readOnly: true });
7473
7761
  const row = d.prepare("SELECT value FROM ItemTable WHERE key = ?").get("cursorAuth/accessToken");
7474
7762
  d.close();
7475
7763
  if (row?.value) return String(row.value);
7476
7764
  } catch {
7477
7765
  }
7478
- try {
7479
- const res = spawnSync3(
7480
- "sqlite3",
7481
- [db, "SELECT value FROM ItemTable WHERE key='cursorAuth/accessToken';"],
7482
- { encoding: "utf8", timeout: 1e4 }
7483
- );
7484
- const out = res.stdout?.trim();
7485
- if (res.status === 0 && out) return out;
7486
- } catch {
7766
+ for (const sqlite3 of trustedSqliteBinaries()) {
7767
+ try {
7768
+ const res = spawnSync3(
7769
+ sqlite3,
7770
+ [db, "SELECT value FROM ItemTable WHERE key='cursorAuth/accessToken';"],
7771
+ { encoding: "utf8", timeout: 1e4 }
7772
+ );
7773
+ const out = res.stdout?.trim();
7774
+ if (res.status === 0 && out) return out;
7775
+ } catch {
7776
+ }
7487
7777
  }
7488
7778
  return null;
7489
7779
  }
@@ -7570,24 +7860,28 @@ async function fetchCursorEvents(cookie, maxPages = 30, pageSize = 500) {
7570
7860
  Cookie: cookie
7571
7861
  },
7572
7862
  body: JSON.stringify({ page, pageSize }),
7573
- signal: AbortSignal.timeout(2e4)
7863
+ signal: AbortSignal.timeout(2e4),
7864
+ redirect: "error"
7574
7865
  });
7575
7866
  if (!res.ok) {
7576
7867
  throw new Error(`cursor usage page ${page} failed (HTTP ${res.status})`);
7577
7868
  }
7578
- const body = await res.json();
7869
+ const body = await readJsonResponseCapped(
7870
+ res,
7871
+ MAX_CURSOR_PAGE_BYTES
7872
+ );
7579
7873
  const batch = body.usageEventsDisplay ?? [];
7580
7874
  all.push(...batch);
7581
7875
  if (batch.length < pageSize) break;
7582
7876
  }
7583
7877
  return all;
7584
7878
  }
7585
- async function collectCursor() {
7879
+ async function collectCursor(opts) {
7586
7880
  try {
7587
7881
  const db = cursorDbPath();
7588
7882
  const token = db ? readCursorToken(db) : null;
7589
7883
  const cookie = token ? cursorCookie(token) : null;
7590
- if (cookie) {
7884
+ if (cookie && !opts?.offline) {
7591
7885
  const events = await fetchCursorEvents(cookie);
7592
7886
  const { entries, blocks } = mapCursorEvents(events);
7593
7887
  if (entries.length > 0) return { entries, blocks, found: true };
@@ -7605,7 +7899,7 @@ async function collectCursor() {
7605
7899
  // src/native/codex.ts
7606
7900
  import { readdir as readdir3 } from "node:fs/promises";
7607
7901
  import { homedir as homedir7 } from "node:os";
7608
- import { join as join9, resolve } from "node:path";
7902
+ import { basename, join as join9, resolve } from "node:path";
7609
7903
  function num5(n) {
7610
7904
  const v = Math.round(Number(n));
7611
7905
  return Number.isFinite(v) && v > 0 ? v : 0;
@@ -7622,8 +7916,10 @@ function readTokenFields(payload) {
7622
7916
  output: num5(output)
7623
7917
  };
7624
7918
  }
7625
- function parseCodexRollout(lines) {
7919
+ function inspectCodexRollout(lines) {
7626
7920
  let model = "unknown";
7921
+ let sawSessionMeta = false;
7922
+ let replayedRollout = false;
7627
7923
  const perDay = /* @__PURE__ */ new Map();
7628
7924
  let lastSeenDate = null;
7629
7925
  for (const raw of lines) {
@@ -7638,6 +7934,13 @@ function parseCodexRollout(lines) {
7638
7934
  const payload = obj.payload;
7639
7935
  if (!payload || typeof payload !== "object") continue;
7640
7936
  const kind = obj.type;
7937
+ if (kind === "session_meta" && !sawSessionMeta) {
7938
+ sawSessionMeta = true;
7939
+ const source = payload.source;
7940
+ if (typeof payload.forked_from_id === "string" || typeof payload.parent_thread_id === "string" || source && typeof source === "object" && source.subagent !== void 0) {
7941
+ replayedRollout = true;
7942
+ }
7943
+ }
7641
7944
  if (kind === "session_meta" || kind === "turn_context") {
7642
7945
  if (typeof payload.model === "string" && payload.model) model = payload.model;
7643
7946
  }
@@ -7657,7 +7960,8 @@ function parseCodexRollout(lines) {
7657
7960
  }
7658
7961
  }
7659
7962
  }
7660
- if (perDay.size === 0) return [];
7963
+ if (perDay.size === 0)
7964
+ return { sessions: [], replaySessions: [], replayCandidateDates: [] };
7661
7965
  const dates = [...perDay.keys()].sort();
7662
7966
  const out = [];
7663
7967
  let prev = { input: 0, cached: 0, output: 0 };
@@ -7667,8 +7971,8 @@ function parseCodexRollout(lines) {
7667
7971
  const dCached = Math.max(0, cum.cached - prev.cached);
7668
7972
  const dOutput = Math.max(0, cum.output - prev.output);
7669
7973
  prev = cum;
7670
- const cacheReadTokens = dCached;
7671
- const inputTokens = Math.max(0, dInput - dCached);
7974
+ const cacheReadTokens = Math.min(dCached, dInput);
7975
+ const inputTokens = dInput - cacheReadTokens;
7672
7976
  const outputTokens = dOutput;
7673
7977
  if (inputTokens + outputTokens + cacheReadTokens === 0) continue;
7674
7978
  out.push({
@@ -7681,7 +7985,11 @@ function parseCodexRollout(lines) {
7681
7985
  turnCount: turns
7682
7986
  });
7683
7987
  }
7684
- return out;
7988
+ return {
7989
+ sessions: replayedRollout ? [] : out,
7990
+ replaySessions: replayedRollout ? out : [],
7991
+ replayCandidateDates: replayedRollout ? dates : []
7992
+ };
7685
7993
  }
7686
7994
  function foldCodexSessions(acc, sessions) {
7687
7995
  for (const s of sessions) {
@@ -7729,7 +8037,7 @@ function resolveCodexHome(env = process.env) {
7729
8037
  return env.CODEX_HOME && env.CODEX_HOME.trim() ? env.CODEX_HOME.trim() : join9(homedir7(), ".codex");
7730
8038
  }
7731
8039
  function resolveCodexSessionsDirs(env = process.env) {
7732
- const home = resolveCodexHome(env);
8040
+ const home = resolve(resolveCodexHome(env));
7733
8041
  return [join9(home, "sessions"), join9(home, "archived_sessions")];
7734
8042
  }
7735
8043
  async function listJsonl3(dir) {
@@ -7758,9 +8066,10 @@ function* splitLines3(content) {
7758
8066
  if (start < content.length) yield content.slice(start);
7759
8067
  }
7760
8068
  var NATIVE_READ_BUDGET_MS2 = 45e3;
7761
- var CODEX_CACHE_VERSION = 2;
7762
- function toCachedSession(s) {
8069
+ var CODEX_CACHE_VERSION = 7;
8070
+ function toCachedSession(s, kind = "usage") {
7763
8071
  return [
8072
+ kind,
7764
8073
  s.date,
7765
8074
  s.model,
7766
8075
  s.inputTokens,
@@ -7772,25 +8081,53 @@ function toCachedSession(s) {
7772
8081
  }
7773
8082
  function fromCachedSession(t) {
7774
8083
  return {
7775
- date: t[0],
7776
- model: t[1],
7777
- inputTokens: t[2],
7778
- outputTokens: t[3],
7779
- cacheCreationTokens: t[4],
7780
- cacheReadTokens: t[5],
7781
- turnCount: t[6]
8084
+ date: t[1],
8085
+ model: t[2],
8086
+ inputTokens: t[3],
8087
+ outputTokens: t[4],
8088
+ cacheCreationTokens: t[5],
8089
+ cacheReadTokens: t[6],
8090
+ turnCount: t[7]
7782
8091
  };
7783
8092
  }
7784
8093
  async function collectCodexNative(env = process.env, opts = {}) {
7785
8094
  const dirs = resolveCodexSessionsDirs(env);
7786
- const files = (await Promise.all(dirs.map(listJsonl3))).flat();
8095
+ const files = [];
8096
+ const primaryFiles = /* @__PURE__ */ new Set();
8097
+ const seenIdentities = /* @__PURE__ */ new Set();
8098
+ for (const dir of dirs) {
8099
+ for (const file of await listJsonl3(dir)) {
8100
+ const key = basename(file);
8101
+ files.push(file);
8102
+ if (!seenIdentities.has(key)) {
8103
+ seenIdentities.add(key);
8104
+ primaryFiles.add(file);
8105
+ }
8106
+ }
8107
+ }
7787
8108
  if (files.length === 0) return { entries: [], found: false, filesScanned: 0 };
7788
8109
  const now = opts.now ?? Date.now;
7789
8110
  const res = await readFilesWithCache({
7790
8111
  files,
7791
8112
  cachePath: opts.cachePath ?? nativeCachePath("codex", env),
7792
8113
  version: CODEX_CACHE_VERSION,
7793
- parseFile: (content) => parseCodexRollout(splitLines3(content)).map(toCachedSession),
8114
+ parseFile: (content, path) => {
8115
+ const inspected = inspectCodexRollout(splitLines3(content));
8116
+ const primary = primaryFiles.has(path);
8117
+ return [
8118
+ // Do not pass toCachedSession directly to map: Array.map's numeric
8119
+ // index would be supplied as the optional `kind` argument.
8120
+ ...inspected.sessions.map(
8121
+ (session) => toCachedSession(session, primary ? "usage" : "duplicate")
8122
+ ),
8123
+ ...inspected.replaySessions.map(
8124
+ (session) => toCachedSession(
8125
+ session,
8126
+ primary ? "replay" : "replay-duplicate"
8127
+ )
8128
+ )
8129
+ ];
8130
+ },
7794
8131
  deadline: now() + (opts.budgetMs ?? NATIVE_READ_BUDGET_MS2),
7795
8132
  now
7796
8133
  });
@@ -7803,13 +8140,24 @@ async function collectCodexNative(env = process.env, opts = {}) {
7803
8140
  };
7804
8141
  }
7805
8142
  const acc = /* @__PURE__ */ new Map();
8143
+ const legacyAcc = /* @__PURE__ */ new Map();
8144
+ const replayCandidateDates = /* @__PURE__ */ new Set();
7806
8145
  for (const items of res.itemsByFile) {
7807
- foldCodexSessions(acc, items.map(fromCachedSession));
8146
+ const usage = items.filter((item) => item[0] === "usage").map(fromCachedSession);
8147
+ const legacy = items.map(fromCachedSession);
8148
+ const replay = items.filter(
8149
+ (item) => item[0] === "replay" || item[0] === "replay-duplicate"
8150
+ ).map(fromCachedSession);
8151
+ foldCodexSessions(acc, usage);
8152
+ foldCodexSessions(legacyAcc, legacy);
8153
+ for (const session of replay) replayCandidateDates.add(session.date);
7808
8154
  }
7809
8155
  return {
7810
8156
  entries: finalizeCodexEntries(acc),
7811
8157
  found: true,
7812
- filesScanned: res.filesRead
8158
+ filesScanned: res.filesRead,
8159
+ replayCandidateDates: [...replayCandidateDates].sort(),
8160
+ legacyEntries: finalizeCodexEntries(legacyAcc)
7813
8161
  };
7814
8162
  }
7815
8163
 
@@ -8015,16 +8363,20 @@ async function collectVscodeAgent(opts) {
8015
8363
  }
8016
8364
 
8017
8365
  // src/pricing-live.ts
8018
- import { mkdir, readFile as readFile3, rename, writeFile } from "node:fs/promises";
8019
- import { dirname as dirname4, join as join11 } from "node:path";
8366
+ import { chmod, mkdir, rename, unlink, writeFile } from "node:fs/promises";
8367
+ import { randomBytes as randomBytes4 } from "node:crypto";
8368
+ import { dirname as dirname5, join as join11 } from "node:path";
8020
8369
  var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
8021
8370
  var FETCH_TIMEOUT_MS = 5e3;
8371
+ var MAX_PRICING_BYTES = 32 * 1024 * 1024;
8022
8372
  function pricingCachePath(dir = defaultConfigDir()) {
8023
8373
  return join11(dir, "pricing-cache.json");
8024
8374
  }
8025
8375
  async function readCache(path) {
8026
8376
  try {
8027
- const parsed = JSON.parse(await readFile3(path, "utf8"));
8377
+ const read = await readTextFileCapped(path, { maxBytes: MAX_PRICING_BYTES });
8378
+ if (!read.ok) return null;
8379
+ const parsed = JSON.parse(read.content);
8028
8380
  if (typeof parsed?.fetchedAt === "number" && parsed.table && typeof parsed.table === "object") {
8029
8381
  return parsed;
8030
8382
  }
@@ -8035,10 +8387,11 @@ async function readCache(path) {
8035
8387
  async function fetchLiveTable(url) {
8036
8388
  try {
8037
8389
  const res = await fetch(url, {
8038
- signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
8390
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
8391
+ redirect: "error"
8039
8392
  });
8040
8393
  if (!res.ok) return null;
8041
- const table = litellmToTable(await res.json());
8394
+ const table = litellmToTable(await readJsonResponseCapped(res, MAX_PRICING_BYTES));
8042
8395
  return Object.keys(table).length > 0 ? table : null;
8043
8396
  } catch {
8044
8397
  return null;
@@ -8056,10 +8409,20 @@ async function loadLivePricing(env = process.env, now = Date.now, cachePath = pr
8056
8409
  if (live) {
8057
8410
  setLivePricing(live);
8058
8411
  try {
8059
- await mkdir(dirname4(path), { recursive: true });
8060
- const tmp = `${path}.${process.pid}.tmp`;
8061
- await writeFile(tmp, JSON.stringify({ fetchedAt: now(), table: live }));
8062
- await rename(tmp, path);
8412
+ const dir = dirname5(path);
8413
+ await mkdir(dir, { recursive: true, mode: 448 });
8414
+ await chmod(dir, 448);
8415
+ const serialized = JSON.stringify({ fetchedAt: now(), table: live });
8416
+ if (Buffer.byteLength(serialized, "utf8") <= MAX_PRICING_BYTES) {
8417
+ const tmp = `${path}.${process.pid}.${randomBytes4(8).toString("hex")}.tmp`;
8418
+ try {
8419
+ await writeFile(tmp, serialized, { encoding: "utf8", flag: "wx", mode: 384 });
8420
+ await chmod(tmp, 384);
8421
+ await rename(tmp, path);
8422
+ } finally {
8423
+ await unlink(tmp).catch(() => void 0);
8424
+ }
8425
+ }
8063
8426
  } catch {
8064
8427
  }
8065
8428
  return "live";
@@ -8072,8 +8435,8 @@ async function loadLivePricing(env = process.env, now = Date.now, cachePath = pr
8072
8435
  }
8073
8436
 
8074
8437
  // src/provenance-store.ts
8075
- import { mkdirSync as mkdirSync4, readFileSync as readFileSync3, renameSync as renameSync4, writeFileSync as writeFileSync4 } from "node:fs";
8076
- import { dirname as dirname5, join as join12 } from "node:path";
8438
+ import { join as join12 } from "node:path";
8439
+ var MAX_PROVENANCE_STORE_BYTES = 4 * 1024 * 1024;
8077
8440
  var PROVENANCE_STORE_VERSION = 1;
8078
8441
  var KEY_SEP = "|";
8079
8442
  var keyOf = (date, tool) => `${date}${KEY_SEP}${tool}`;
@@ -8087,7 +8450,9 @@ function provenanceStorePath(env = process.env) {
8087
8450
  }
8088
8451
  function loadProvenanceStore(path) {
8089
8452
  try {
8090
- const parsed = JSON.parse(readFileSync3(path, "utf8"));
8453
+ const content = readTextFileSyncCapped(path, MAX_PROVENANCE_STORE_BYTES);
8454
+ if (content === null) return null;
8455
+ const parsed = JSON.parse(content);
8091
8456
  if (parsed && parsed.v === PROVENANCE_STORE_VERSION && parsed.req && typeof parsed.req === "object") {
8092
8457
  return {
8093
8458
  v: PROVENANCE_STORE_VERSION,
@@ -8101,10 +8466,7 @@ function loadProvenanceStore(path) {
8101
8466
  }
8102
8467
  function saveProvenanceStore(path, store) {
8103
8468
  try {
8104
- mkdirSync4(dirname5(path), { recursive: true });
8105
- const tmp = `${path}.tmp-${process.pid}`;
8106
- writeFileSync4(tmp, JSON.stringify(store));
8107
- renameSync4(tmp, path);
8469
+ writePrivateFileAtomic(path, JSON.stringify(store), MAX_PROVENANCE_STORE_BYTES);
8108
8470
  } catch {
8109
8471
  }
8110
8472
  }
@@ -8158,12 +8520,131 @@ function reconcileProvenance(entries, agent, complete, store) {
8158
8520
  };
8159
8521
  }
8160
8522
 
8523
+ // src/wire-sanitize.ts
8524
+ import { createHash as createHash2 } from "node:crypto";
8525
+ var MACHINE_IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:/@+~-]*$/;
8526
+ function sanitizeMachineLabel(value, kind, maxLength) {
8527
+ const raw = typeof value === "string" ? value.trim() : "";
8528
+ if (raw.length > 0 && raw.length <= maxLength && MACHINE_IDENTIFIER.test(raw)) {
8529
+ return raw;
8530
+ }
8531
+ const digest = createHash2("sha256").update(`${kind}\0`).update(raw).digest("hex").slice(0, 12);
8532
+ return `${kind}-${digest}`;
8533
+ }
8534
+ function sanitizeDailyEntries(rows) {
8535
+ const safe = [];
8536
+ for (const raw of rows) {
8537
+ if (!raw || typeof raw !== "object") continue;
8538
+ const row = raw;
8539
+ const parsed = DailyUsageEntry.safeParse({
8540
+ ...row,
8541
+ tool: sanitizeMachineLabel(row.tool, "agent", 64),
8542
+ model: sanitizeMachineLabel(row.model, "model", 128),
8543
+ // Only a server-side connector may assert stronger provenance.
8544
+ origin: "cli",
8545
+ verified: false
8546
+ });
8547
+ if (parsed.success) safe.push(parsed.data);
8548
+ }
8549
+ return safe;
8550
+ }
8551
+ function sanitizeSessions(rows) {
8552
+ const safe = [];
8553
+ for (const raw of rows) {
8554
+ if (!raw || typeof raw !== "object") continue;
8555
+ const row = raw;
8556
+ const sessionId = createHash2("sha256").update("session\0").update(String(row.sessionId ?? "")).digest("hex");
8557
+ const parsed = SessionEntry.safeParse({
8558
+ ...row,
8559
+ sessionId: `local-${sessionId.slice(0, 24)}`,
8560
+ tool: sanitizeMachineLabel(row.tool, "agent", 64),
8561
+ model: sanitizeMachineLabel(row.model, "model", 128)
8562
+ });
8563
+ if (parsed.success) safe.push(parsed.data);
8564
+ }
8565
+ return safe;
8566
+ }
8567
+ function sanitizeBlocks(rows) {
8568
+ return rows.flatMap((row) => {
8569
+ const parsed = BlockEntry.safeParse(row);
8570
+ return parsed.success ? [parsed.data] : [];
8571
+ });
8572
+ }
8573
+ function sanitizeToolStats(rows) {
8574
+ return rows.flatMap((raw) => {
8575
+ if (!raw || typeof raw !== "object") return [];
8576
+ const row = raw;
8577
+ const parsed = ToolStat.safeParse({
8578
+ ...row,
8579
+ name: sanitizeMachineLabel(row.name, "tool", 128)
8580
+ });
8581
+ return parsed.success ? [parsed.data] : [];
8582
+ });
8583
+ }
8584
+ function sanitizeSkillStats(rows) {
8585
+ return rows.flatMap((raw) => {
8586
+ if (!raw || typeof raw !== "object") return [];
8587
+ const row = raw;
8588
+ const parsed = SkillStat.safeParse({
8589
+ ...row,
8590
+ name: sanitizeMachineLabel(row.name, "skill", 128)
8591
+ });
8592
+ return parsed.success ? [parsed.data] : [];
8593
+ });
8594
+ }
8595
+ var EMPTY_AGENT = {
8596
+ messageCount: 0,
8597
+ subagentMessages: 0,
8598
+ subagentTokens: 0,
8599
+ totalTokens: 0,
8600
+ userMessageCount: 0
8601
+ };
8602
+ function sanitizeAgentStat(value) {
8603
+ const parsed = AgentStat.safeParse(value);
8604
+ return parsed.success ? parsed.data : { ...EMPTY_AGENT };
8605
+ }
8606
+ function sanitizeCodexReplayScopes(scopes) {
8607
+ return scopes.flatMap((raw) => {
8608
+ if (!raw || typeof raw !== "object") return [];
8609
+ const scope = raw;
8610
+ const rows = Array.isArray(scope.rows) ? scope.rows.map((row) => {
8611
+ if (!row || typeof row !== "object") return row;
8612
+ const value = row;
8613
+ return {
8614
+ ...value,
8615
+ model: sanitizeMachineLabel(value.model, "model", 128)
8616
+ };
8617
+ }) : scope.rows;
8618
+ const parsed = CodexReplayPriorScope.safeParse({ ...scope, rows });
8619
+ return parsed.success ? [parsed.data] : [];
8620
+ });
8621
+ }
8622
+
8161
8623
  // src/collect.ts
8162
8624
  var execFileAsync = promisify(execFile);
8163
8625
  var NATIVE_COVERED_SOURCES = /* @__PURE__ */ new Set(["claude", "codex"]);
8164
8626
  var CCUSAGE_TIMEOUT_MS = 25e3;
8165
8627
  var CCUSAGE_FALLBACK_TIMEOUT_MS = NATIVE_READ_BUDGET_MS;
8166
8628
  var CCUSAGE_AGGREGATE_TIMEOUT_MS = 18e4;
8629
+ var CCUSAGE_MAX_CONCURRENCY = 4;
8630
+ var CCUSAGE_MAX_BUFFER_BYTES = 32 * 1024 * 1024;
8631
+ async function mapConcurrent(items, concurrency, fn) {
8632
+ const out = new Array(items.length);
8633
+ let next = 0;
8634
+ const worker = async () => {
8635
+ while (true) {
8636
+ const index = next++;
8637
+ if (index >= items.length) return;
8638
+ out[index] = await fn(items[index], index);
8639
+ }
8640
+ };
8641
+ const workers = Math.min(
8642
+ items.length,
8643
+ Math.max(1, Math.floor(concurrency) || 1)
8644
+ );
8645
+ await Promise.all(Array.from({ length: workers }, () => worker()));
8646
+ return out;
8647
+ }
8167
8648
  var SOURCES = [
8168
8649
  "claude",
8169
8650
  "codex",
@@ -8291,16 +8772,16 @@ function mapCcusageBlocks(json) {
8291
8772
  if (b.isGap === true) continue;
8292
8773
  if (typeof b.startTime !== "string") continue;
8293
8774
  const totalTokens = norm(b.totalTokens);
8294
- const costUSD = normCost(b.costUSD);
8295
- if (totalTokens === 0 && costUSD === 0) continue;
8296
- out.push({ startTime: b.startTime, totalTokens, costUSD });
8775
+ const costUSD2 = normCost(b.costUSD);
8776
+ if (totalTokens === 0 && costUSD2 === 0) continue;
8777
+ out.push({ startTime: b.startTime, totalTokens, costUSD: costUSD2 });
8297
8778
  }
8298
8779
  return out;
8299
8780
  }
8300
8781
  function resolveCcusageBin() {
8301
- const require3 = createRequire3(import.meta.url);
8302
- const pkgPath = require3.resolve("ccusage/package.json");
8303
- const pkg = require3("ccusage/package.json");
8782
+ const require4 = createRequire4(import.meta.url);
8783
+ const pkgPath = require4.resolve("ccusage/package.json");
8784
+ const pkg = require4("ccusage/package.json");
8304
8785
  const rel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.ccusage ?? "ccusage";
8305
8786
  const binPath = join13(dirname6(pkgPath), rel);
8306
8787
  if (/\.(c|m)?js$/.test(binPath)) {
@@ -8308,6 +8789,68 @@ function resolveCcusageBin() {
8308
8789
  }
8309
8790
  return { cmd: binPath, prefixArgs: [] };
8310
8791
  }
8792
+ function codexReplayTombstoneDates(native, correctedEntries, replayReadSucceeded) {
8793
+ if (!replayReadSucceeded || native.timedOut) return [];
8794
+ const correctedDates = new Set(
8795
+ correctedEntries.filter((entry) => entry.tool === "codex").map((entry) => entry.date)
8796
+ );
8797
+ return [...new Set(native.replayCandidateDates ?? [])].filter((date) => !correctedDates.has(date)).sort().slice(0, 5e3);
8798
+ }
8799
+ function codexRowsByDate(entries) {
8800
+ const grouped = /* @__PURE__ */ new Map();
8801
+ for (const entry of entries) {
8802
+ if (entry.tool !== "codex" || entry.origin !== "cli" || entry.verified === true)
8803
+ continue;
8804
+ const byModel = grouped.get(entry.date) ?? /* @__PURE__ */ new Map();
8805
+ const row = byModel.get(entry.model) ?? {
8806
+ model: entry.model,
8807
+ inputTokens: 0,
8808
+ outputTokens: 0,
8809
+ cacheCreationTokens: 0,
8810
+ cacheReadTokens: 0
8811
+ };
8812
+ row.inputTokens += entry.inputTokens;
8813
+ row.outputTokens += entry.outputTokens;
8814
+ row.cacheCreationTokens += entry.cacheCreationTokens;
8815
+ row.cacheReadTokens += entry.cacheReadTokens;
8816
+ byModel.set(entry.model, row);
8817
+ grouped.set(entry.date, byModel);
8818
+ }
8819
+ return new Map(
8820
+ [...grouped].map(([date, byModel]) => [
8821
+ date,
8822
+ [...byModel.values()].sort((a, b) => a.model.localeCompare(b.model))
8823
+ ])
8824
+ );
8825
+ }
8826
+ function codexRowsEqual(left, right) {
8827
+ return JSON.stringify(left ?? []) === JSON.stringify(right ?? []);
8828
+ }
8829
+ function codexReplayCorrectionMetadata(native, correctedEntries, replayReadSucceeded) {
8830
+ if (!replayReadSucceeded || native.timedOut || !native.legacyEntries) {
8831
+ return { tombstoneDates: [], priorScopes: [] };
8832
+ }
8833
+ const tombstoneDates = codexReplayTombstoneDates(
8834
+ native,
8835
+ correctedEntries,
8836
+ replayReadSucceeded
8837
+ );
8838
+ const legacyByDate = codexRowsByDate(native.legacyEntries);
8839
+ const correctedByDate = codexRowsByDate(correctedEntries);
8840
+ const changedDates = new Set(tombstoneDates);
8841
+ for (const [date, rows] of legacyByDate) {
8842
+ if (!codexRowsEqual(rows, correctedByDate.get(date))) changedDates.add(date);
8843
+ }
8844
+ const priorScopes = [...changedDates].sort().flatMap((date) => {
8845
+ const rows = legacyByDate.get(date);
8846
+ return rows && rows.length > 0 && rows.length <= 100 ? [{ date, rows }] : [];
8847
+ }).slice(0, 5e3);
8848
+ const provedDates = new Set(priorScopes.map((scope) => scope.date));
8849
+ return {
8850
+ tombstoneDates: tombstoneDates.filter((date) => provedDates.has(date)),
8851
+ priorScopes
8852
+ };
8853
+ }
8311
8854
  function dedupeDaily(entries) {
8312
8855
  const byKey = /* @__PURE__ */ new Map();
8313
8856
  for (const e of entries) {
@@ -8361,8 +8904,7 @@ function dedupeBlocks(blocks) {
8361
8904
  function selectSourceEntries(source, ccusageEntries, native) {
8362
8905
  if (source === "claude" && nativeReaderWon(native.claude))
8363
8906
  return native.claude.entries;
8364
- if (source === "codex" && nativeReaderWon(native.codex))
8365
- return native.codex.entries;
8907
+ if (source === "codex") return ccusageEntries;
8366
8908
  return ccusageEntries;
8367
8909
  }
8368
8910
  function nativeReaderWon(result) {
@@ -8370,7 +8912,7 @@ function nativeReaderWon(result) {
8370
8912
  }
8371
8913
  function ccusageFallbackSources(native) {
8372
8914
  return SOURCES.filter(
8373
- (s) => NATIVE_COVERED_SOURCES.has(s) && !nativeReaderWon(s === "claude" ? native.claude : native.codex)
8915
+ (s) => s === "codex" || s === "claude" && !nativeReaderWon(native.claude)
8374
8916
  );
8375
8917
  }
8376
8918
  function ccusageClaudeEnv(env = process.env) {
@@ -8386,7 +8928,7 @@ async function runCcusageOnce(cmd, args, env, timeoutMs = CCUSAGE_TIMEOUT_MS) {
8386
8928
  try {
8387
8929
  const { stdout } = await execFileAsync(cmd, args, {
8388
8930
  encoding: "utf8",
8389
- maxBuffer: 64 * 1024 * 1024,
8931
+ maxBuffer: CCUSAGE_MAX_BUFFER_BYTES,
8390
8932
  // A single source shouldn't be able to hang the whole run: a hung source
8391
8933
  // gets killed and (if transient) retried once below rather than stalling
8392
8934
  // everything for minutes. The claude/codex fallback passes a longer cap —
@@ -8413,8 +8955,10 @@ var COLLECT_STAGES = SOURCES.length + 4 + VSCODE_AGENTS.length + 1;
8413
8955
  function isAuthoritativeScan(attributionComplete, ...fingerprintReaders) {
8414
8956
  return attributionComplete && fingerprintReaders.every((reader) => reader.timedOut !== true);
8415
8957
  }
8416
- async function collectAll(onProgress) {
8417
- await loadLivePricing().catch(() => {
8958
+ async function collectAll(onProgress, opts) {
8959
+ await loadLivePricing(
8960
+ opts?.offline ? { ...process.env, WHOBURNEDMORE_PRICING_OFFLINE: "1" } : process.env
8961
+ ).catch(() => {
8418
8962
  });
8419
8963
  const { cmd, prefixArgs } = resolveCcusageBin();
8420
8964
  let done = 0;
@@ -8425,22 +8969,26 @@ async function collectAll(onProgress) {
8425
8969
  const nativeCodexTask = collectCodexNative().catch(
8426
8970
  () => ({ entries: [], found: false, filesScanned: 0, timedOut: true })
8427
8971
  );
8428
- const sourceTasks = SOURCES.map(async (source) => {
8429
- if (NATIVE_COVERED_SOURCES.has(source)) {
8430
- await (source === "claude" ? nativeClaudeTask : nativeCodexTask);
8972
+ const sourceTasks = mapConcurrent(
8973
+ SOURCES,
8974
+ CCUSAGE_MAX_CONCURRENCY,
8975
+ async (source) => {
8976
+ if (NATIVE_COVERED_SOURCES.has(source)) {
8977
+ await (source === "claude" ? nativeClaudeTask : nativeCodexTask);
8978
+ tick();
8979
+ return { source, mapped: [] };
8980
+ }
8981
+ const json = await runCcusage(cmd, [
8982
+ ...prefixArgs,
8983
+ source,
8984
+ "daily",
8985
+ "--json",
8986
+ "--offline"
8987
+ ]);
8431
8988
  tick();
8432
- return { source, mapped: [] };
8989
+ return { source, mapped: json ? mapCcusageDaily(source, json) : [] };
8433
8990
  }
8434
- const json = await runCcusage(cmd, [
8435
- ...prefixArgs,
8436
- source,
8437
- "daily",
8438
- "--json",
8439
- "--offline"
8440
- ]);
8441
- tick();
8442
- return { source, mapped: json ? mapCcusageDaily(source, json) : [] };
8443
- });
8991
+ );
8444
8992
  const sessionTask = runCcusage(
8445
8993
  cmd,
8446
8994
  [...prefixArgs, "session", "--json", "--offline"],
@@ -8459,7 +9007,7 @@ async function collectAll(onProgress) {
8459
9007
  tick();
8460
9008
  return json ? mapCcusageBlocks(json) : [];
8461
9009
  });
8462
- const cursorTask = collectCursor().then((c) => {
9010
+ const cursorTask = collectCursor({ offline: opts?.offline }).then((c) => {
8463
9011
  tick();
8464
9012
  return c;
8465
9013
  });
@@ -8488,7 +9036,7 @@ async function collectAll(onProgress) {
8488
9036
  vscodeResults,
8489
9037
  continueResult
8490
9038
  ] = await Promise.all([
8491
- Promise.all(sourceTasks),
9039
+ sourceTasks,
8492
9040
  sessionTask,
8493
9041
  blockTask,
8494
9042
  cursorTask,
@@ -8500,6 +9048,7 @@ async function collectAll(onProgress) {
8500
9048
  ]);
8501
9049
  const native = { claude: nativeClaude, codex: nativeCodex };
8502
9050
  const fallbacks = /* @__PURE__ */ new Map();
9051
+ let codexReplayReadSucceeded = false;
8503
9052
  for (const source of ccusageFallbackSources(native)) {
8504
9053
  const json = await runCcusage(
8505
9054
  cmd,
@@ -8508,8 +9057,15 @@ async function collectAll(onProgress) {
8508
9057
  source === "claude" ? ccusageClaudeEnv() : void 0,
8509
9058
  CCUSAGE_FALLBACK_TIMEOUT_MS
8510
9059
  );
9060
+ if (source === "codex") codexReplayReadSucceeded = json !== null;
8511
9061
  fallbacks.set(source, json ? mapCcusageDaily(source, json) : []);
8512
9062
  }
9063
+ const codexReplayEntries = fallbacks.get("codex") ?? [];
9064
+ const replayCorrection = codexReplayCorrectionMetadata(
9065
+ nativeCodex,
9066
+ codexReplayEntries,
9067
+ codexReplayReadSucceeded
9068
+ );
8513
9069
  const entries = [];
8514
9070
  const toolsFound = [];
8515
9071
  for (const { source, mapped } of sourceResults) {
@@ -8536,13 +9092,16 @@ async function collectAll(onProgress) {
8536
9092
  }
8537
9093
  const { tools, skills, agent, sessionMessages, complete } = attribution;
8538
9094
  onProgress?.(COLLECT_STAGES, COLLECT_STAGES, "");
8539
- const dedupedSessions = dedupeSessions(sessions).map((s) => {
9095
+ const localSessions = dedupeSessions(sessions).map((s) => {
8540
9096
  const messageCount = sessionMessages.get(s.sessionId);
8541
9097
  return {
8542
9098
  ...s,
8543
9099
  ...messageCount ? { messageCount } : {}
8544
9100
  };
8545
9101
  });
9102
+ const dedupedSessions = sanitizeSessions(localSessions);
9103
+ const boundaryEntries = sanitizeDailyEntries(entries);
9104
+ const boundaryAgent = sanitizeAgentStat(agent);
8546
9105
  const scanComplete = isAuthoritativeScan(
8547
9106
  complete,
8548
9107
  nativeClaude,
@@ -8552,25 +9111,40 @@ async function collectAll(onProgress) {
8552
9111
  );
8553
9112
  const storePath = provenanceStorePath();
8554
9113
  const reconciled = reconcileProvenance(
8555
- dedupeDaily(entries),
8556
- agent,
9114
+ dedupeDaily(boundaryEntries),
9115
+ boundaryAgent,
8557
9116
  scanComplete,
8558
9117
  loadProvenanceStore(storePath)
8559
9118
  );
9119
+ const reconciledEntries = sanitizeDailyEntries(reconciled.entries);
8560
9120
  saveProvenanceStore(storePath, reconciled.store);
9121
+ const cappedEntries = capByTokens(reconciledEntries, 2e4, entryTokens2);
9122
+ const payloadReplayCorrection = boundaryEntries.length === entries.length && reconciledEntries.length === reconciled.entries.length && cappedEntries.length === reconciledEntries.length ? replayCorrection : { tombstoneDates: [], priorScopes: [] };
9123
+ const safeReplayScopes = sanitizeCodexReplayScopes(
9124
+ payloadReplayCorrection.priorScopes
9125
+ );
9126
+ const safeReplayDates = new Set(safeReplayScopes.map((scope) => scope.date));
8561
9127
  return {
8562
9128
  // Cap each array to the server's accepted maximum (the shared SubmitPayload
8563
9129
  // schema: entries ≤ 20000, sessions/blocks ≤ 10000), keeping the
8564
9130
  // highest-token rows. Without this, a power user with >10000 distinct sessions
8565
9131
  // would have their ENTIRE submit rejected with a 400 instead of a capped one.
8566
9132
  // tools/skills are already bounded upstream (attribution caps).
8567
- entries: capByTokens(reconciled.entries, 2e4, entryTokens2),
9133
+ entries: cappedEntries,
9134
+ codexReplayTombstoneDates: payloadReplayCorrection.tombstoneDates.filter(
9135
+ (date) => safeReplayDates.has(date)
9136
+ ),
9137
+ codexReplayPriorScopes: safeReplayScopes,
8568
9138
  sessions: capByTokens(dedupedSessions, 1e4, entryTokens2),
8569
- blocks: capByTokens(dedupeBlocks(blocks), 1e4, (b) => b.totalTokens),
9139
+ blocks: capByTokens(
9140
+ dedupeBlocks(sanitizeBlocks(blocks)),
9141
+ 1e4,
9142
+ (b) => b.totalTokens
9143
+ ),
8570
9144
  toolsFound,
8571
- tools,
8572
- skills,
8573
- agent: reconciled.agent,
9145
+ tools: sanitizeToolStats(tools),
9146
+ skills: sanitizeSkillStats(skills),
9147
+ agent: sanitizeAgentStat(reconciled.agent),
8574
9148
  attributionComplete: complete
8575
9149
  };
8576
9150
  }
@@ -8595,7 +9169,7 @@ function antigravityNoticeLines() {
8595
9169
  }
8596
9170
 
8597
9171
  // src/verify-upload.ts
8598
- import { createHash } from "node:crypto";
9172
+ import { createHash as createHash3 } from "node:crypto";
8599
9173
  function prepareVerifyUpload(requests, scanTimedOut, cap = 5e4) {
8600
9174
  const sorted = requests.slice().sort((a, b) => b.ts - a.ts);
8601
9175
  const truncated = scanTimedOut || sorted.length > cap;
@@ -8609,7 +9183,7 @@ function prepareVerifyUpload(requests, scanTimedOut, cap = 5e4) {
8609
9183
  outputTokens: r.outputTokens,
8610
9184
  cacheCreationTokens: r.cacheCreationTokens,
8611
9185
  cacheReadTokens: r.cacheReadTokens,
8612
- reqHash: createHash("sha256").update(r.key).digest("hex").slice(0, 32)
9186
+ reqHash: createHash3("sha256").update(r.key).digest("hex").slice(0, 32)
8613
9187
  }));
8614
9188
  return { records, truncated };
8615
9189
  }
@@ -8676,6 +9250,9 @@ function agentStatusReport(now = Date.now()) {
8676
9250
  });
8677
9251
  }
8678
9252
 
9253
+ // src/local-dashboard.ts
9254
+ import { join as join15 } from "node:path";
9255
+
8679
9256
  // src/output.ts
8680
9257
  function formatTokens(n) {
8681
9258
  if (n >= 1e9) return `${(n / 1e9).toFixed(2)}B`;
@@ -8704,6 +9281,13 @@ function signedInNextStepLines(result) {
8704
9281
  ` \u2192 Get a friend on it \u2014 have them run: npx whoburnedmore --board=${sanitizeServerText(code)}`
8705
9282
  ];
8706
9283
  }
9284
+ if (result.needsSocial) {
9285
+ return [
9286
+ ` Your dashboard: ${sanitizeServerText(result.profileUrl)}`,
9287
+ " \u2192 You're synced, but not on the public leaderboard yet.",
9288
+ " \u2192 Add a social handle (X, GitHub, or Instagram) on your dashboard to appear."
9289
+ ];
9290
+ }
8707
9291
  return [
8708
9292
  ` Your dashboard: ${sanitizeServerText(result.profileUrl)}`,
8709
9293
  " \u2192 Open it to see your rank and share your profile."
@@ -8711,6 +9295,14 @@ function signedInNextStepLines(result) {
8711
9295
  }
8712
9296
 
8713
9297
  // src/local-dashboard.ts
9298
+ var MAX_LOCAL_DASHBOARD_BYTES = 16 * 1024 * 1024;
9299
+ function writeLocalDashboard(dir, html) {
9300
+ const file = join15(dir, "dashboard.html");
9301
+ if (!writePrivateFileAtomic(file, html, MAX_LOCAL_DASHBOARD_BYTES)) {
9302
+ throw new Error("could not write the private local dashboard");
9303
+ }
9304
+ return file;
9305
+ }
8714
9306
  function esc(s) {
8715
9307
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
8716
9308
  }
@@ -8941,8 +9533,8 @@ async function publishLocal(payload, deps) {
8941
9533
  }
8942
9534
 
8943
9535
  // src/index.ts
8944
- var require2 = createRequire4(import.meta.url);
8945
- var VERSION = require2("../package.json").version;
9536
+ var require3 = createRequire5(import.meta.url);
9537
+ var VERSION = require3("../package.json").version;
8946
9538
  var LOADING_VIBES = [
8947
9539
  "counting up your token usage, right here on your machine\u2026",
8948
9540
  "tallying tokens across every coding agent you use\u2026",
@@ -9014,16 +9606,15 @@ async function confirm(question) {
9014
9606
  }
9015
9607
  function showLocalDashboard(payload) {
9016
9608
  const dir = defaultConfigDir();
9017
- mkdirSync5(dir, { recursive: true });
9018
- const file = join15(dir, "dashboard.html");
9019
- writeFileSync5(
9020
- file,
9609
+ const file = writeLocalDashboard(
9610
+ dir,
9021
9611
  renderDashboardHtml(payload.entries, /* @__PURE__ */ new Date(), { webBaseUrl: webBase() })
9022
9612
  );
9613
+ const fileUrl = pathToFileURL(file).href;
9023
9614
  console.log();
9024
- console.log(` Local dashboard: ${pc2.cyan(`file://${file}`)}`);
9615
+ console.log(` Local dashboard: ${pc2.cyan(fileUrl)}`);
9025
9616
  console.log(pc2.dim(" Re-run `npx whoburnedmore --local` to refresh it. Nothing left your machine."));
9026
- openBrowser(`file://${file}`);
9617
+ openBrowser(fileUrl);
9027
9618
  }
9028
9619
  async function run(flags) {
9029
9620
  if (!flags.quiet) {
@@ -9043,11 +9634,20 @@ async function run(flags) {
9043
9634
  } } : startProgress();
9044
9635
  let collected;
9045
9636
  try {
9046
- collected = await collectAll(progress.onProgress);
9637
+ collected = await collectAll(progress.onProgress, { offline: flags.local });
9047
9638
  } finally {
9048
9639
  progress.stop();
9049
9640
  }
9050
- const { entries, sessions, blocks, tools, skills, agent, attributionComplete } = collected;
9641
+ const {
9642
+ entries,
9643
+ codexReplayTombstoneDates: codexReplayTombstoneDates2,
9644
+ codexReplayPriorScopes,
9645
+ blocks,
9646
+ tools,
9647
+ skills,
9648
+ agent,
9649
+ attributionComplete
9650
+ } = collected;
9051
9651
  if (entries.length === 0) {
9052
9652
  console.log();
9053
9653
  console.log(" Nothing to burn yet \u2014 no local usage found from any coding agent.");
@@ -9059,8 +9659,14 @@ async function run(flags) {
9059
9659
  return;
9060
9660
  }
9061
9661
  const payload = { cliVersion: VERSION, entries };
9662
+ if (codexReplayTombstoneDates2.length > 0)
9663
+ payload.codexReplayTombstoneDates = codexReplayTombstoneDates2;
9664
+ if (codexReplayPriorScopes.length > 0)
9665
+ payload.codexReplayPriorScopes = codexReplayPriorScopes;
9666
+ const configuredMachineKey = loadConfig()?.anonKey;
9667
+ if (configuredMachineKey)
9668
+ payload.deviceKeyHash = deviceKeyHash(configuredMachineKey);
9062
9669
  payload.tzOffsetMinutes = -(/* @__PURE__ */ new Date()).getTimezoneOffset();
9063
- if (sessions.length > 0) payload.sessions = sessions;
9064
9670
  if (blocks.length > 0) payload.blocks = blocks;
9065
9671
  if (tools.length > 0) payload.tools = tools;
9066
9672
  if (skills.length > 0) payload.skills = skills;
@@ -9080,19 +9686,11 @@ async function run(flags) {
9080
9686
  confirm,
9081
9687
  signIn: ensureSignedIn,
9082
9688
  submit: async (token, p) => {
9083
- const result = await submit(token, p);
9689
+ const result = await submitFromBoundDevice(token, p);
9084
9690
  try {
9085
9691
  recordSync();
9086
9692
  } catch {
9087
9693
  }
9088
- try {
9089
- const cfgNow = loadConfig();
9090
- if (!cfgNow?.deviceBoundAt) {
9091
- const key = ensureAnonKey();
9092
- if (await bindDeviceKey(token, key)) recordDeviceBound();
9093
- }
9094
- } catch {
9095
- }
9096
9694
  return result;
9097
9695
  },
9098
9696
  openBrowser,
@@ -9111,7 +9709,7 @@ async function run(flags) {
9111
9709
  await submitSignedIn(cfg.cliToken, payload, flags, canSignIn);
9112
9710
  return;
9113
9711
  }
9114
- const healed = cfg?.anonKey ? await refreshCliToken(cfg.anonKey) : null;
9712
+ const healed = cfg?.refreshToken ? await refreshCliToken(cfg.refreshToken) : null;
9115
9713
  if (healed) {
9116
9714
  saveAuth(void 0, { cliToken: healed.token, handle: healed.handle });
9117
9715
  await submitSignedIn(healed.token, payload, flags, canSignIn);
@@ -9123,7 +9721,7 @@ async function run(flags) {
9123
9721
  console.log(pc2.yellow(" Sign in to put your usage on the leaderboard."));
9124
9722
  console.log(
9125
9723
  pc2.dim(
9126
- " Run `npx whoburnedmore` in an interactive terminal to sign in, or `npx whoburnedmore link --token=\u2026` (from your signed-in profile) for servers/CI."
9724
+ " Run `npx whoburnedmore` in an interactive terminal to sign in, or generate a one-time `npx whoburnedmore link` code from your profile for servers/CI."
9127
9725
  )
9128
9726
  );
9129
9727
  } else {
@@ -9135,8 +9733,8 @@ function sleep(ms) {
9135
9733
  }
9136
9734
  async function refreshCliTokenFromConfig() {
9137
9735
  const cfg = loadConfig();
9138
- if (!cfg?.anonKey) return null;
9139
- return refreshCliToken(cfg.anonKey);
9736
+ if (!cfg?.refreshToken) return null;
9737
+ return refreshCliToken(cfg.refreshToken);
9140
9738
  }
9141
9739
  async function ensureSignedIn() {
9142
9740
  const cfg = loadConfig();
@@ -9165,7 +9763,11 @@ async function ensureSignedIn() {
9165
9763
  continue;
9166
9764
  }
9167
9765
  if (res.status === "ok") {
9168
- saveAuth(void 0, { cliToken: res.token, handle: res.handle });
9766
+ saveAuth(void 0, {
9767
+ cliToken: res.token,
9768
+ handle: res.handle,
9769
+ refreshToken: res.refreshToken
9770
+ });
9169
9771
  console.log(pc2.green(` \u2713 Signed in as @${sanitizeServerText(res.handle)}.`));
9170
9772
  return { token: res.token, handle: res.handle };
9171
9773
  }
@@ -9177,24 +9779,21 @@ async function ensureSignedIn() {
9177
9779
  return null;
9178
9780
  }
9179
9781
  async function submitSignedIn(token, payload, flags, interactive) {
9180
- let activeToken = token;
9181
9782
  let result;
9182
9783
  try {
9183
- result = await submit(token, payload);
9784
+ result = await submitFromBoundDevice(token, payload);
9184
9785
  } catch (err) {
9185
9786
  if (err instanceof UnauthorizedError) {
9186
9787
  const healed = await refreshCliTokenFromConfig();
9187
9788
  if (healed) {
9188
9789
  saveAuth(void 0, { cliToken: healed.token, handle: healed.handle });
9189
- activeToken = healed.token;
9190
- result = await submit(healed.token, payload);
9790
+ result = await submitFromBoundDevice(healed.token, payload);
9191
9791
  } else {
9192
9792
  clearAuth();
9193
9793
  if (!interactive) return;
9194
9794
  const auth = await ensureSignedIn();
9195
9795
  if (!auth) return;
9196
- activeToken = auth.token;
9197
- result = await submit(auth.token, payload);
9796
+ result = await submitFromBoundDevice(auth.token, payload);
9198
9797
  }
9199
9798
  } else {
9200
9799
  throw err;
@@ -9204,14 +9803,6 @@ async function submitSignedIn(token, payload, flags, interactive) {
9204
9803
  recordSync();
9205
9804
  } catch {
9206
9805
  }
9207
- try {
9208
- const cfgNow = loadConfig();
9209
- if (!cfgNow?.deviceBoundAt) {
9210
- const key = ensureAnonKey();
9211
- if (await bindDeviceKey(activeToken, key)) recordDeviceBound();
9212
- }
9213
- } catch {
9214
- }
9215
9806
  const baseUrl = result.orgBoardUrl ?? result.boardUrl ?? result.profileUrl;
9216
9807
  if (!flags.quiet) {
9217
9808
  console.log(
@@ -9270,6 +9861,29 @@ async function submitSignedIn(token, payload, flags, interactive) {
9270
9861
  }
9271
9862
  afterSubmitChores(flags);
9272
9863
  }
9864
+ async function submitFromBoundDevice(token, payload) {
9865
+ let machineKey = loadConfig()?.anonKey;
9866
+ try {
9867
+ const cfg = loadConfig();
9868
+ if (!cfg || needsDeviceBind(cfg)) {
9869
+ machineKey = ensureAnonKey();
9870
+ const bound = await bindDeviceKey(token, machineKey);
9871
+ if (bound.refreshToken) {
9872
+ saveAuth(void 0, {
9873
+ cliToken: token,
9874
+ handle: cfg?.handle,
9875
+ refreshToken: bound.refreshToken
9876
+ });
9877
+ }
9878
+ if (bound.definitive) recordDeviceBound();
9879
+ }
9880
+ } catch {
9881
+ }
9882
+ if (machineKey) {
9883
+ payload.deviceKeyHash = deviceKeyHash(machineKey);
9884
+ }
9885
+ return submit(token, payload);
9886
+ }
9273
9887
  function afterSubmitChores(flags) {
9274
9888
  if (flags.quiet) {
9275
9889
  try {
@@ -9287,14 +9901,36 @@ function afterSubmitChores(flags) {
9287
9901
  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.")
9288
9902
  );
9289
9903
  }
9904
+ async function readServerInstallToken() {
9905
+ const fromEnvironment = process.env.WHOBURNEDMORE_INSTALL_TOKEN?.trim();
9906
+ if (fromEnvironment) return fromEnvironment;
9907
+ if (process.stdin.isTTY) {
9908
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
9909
+ const value2 = (await rl.question(" Paste the one-time link code: ")).trim();
9910
+ rl.close();
9911
+ return value2 || void 0;
9912
+ }
9913
+ let value = "";
9914
+ for await (const chunk of process.stdin) {
9915
+ value += String(chunk);
9916
+ if (value.length > 512) {
9917
+ throw new Error("install code input is too large");
9918
+ }
9919
+ }
9920
+ return value.trim() || void 0;
9921
+ }
9290
9922
  async function linkServerInstall(token) {
9291
9923
  if (!token) {
9292
- throw new Error("missing install token \u2014 use `npx whoburnedmore link --token=<token>`");
9924
+ throw new Error("missing install code \u2014 generate one from your signed-in profile, then paste it when prompted");
9293
9925
  }
9294
9926
  const anonKey = ensureAnonKey();
9295
9927
  const linked = await redeemServerInstall(token, anonKey);
9296
9928
  if (linked.cliToken) {
9297
- saveAuth(void 0, { cliToken: linked.cliToken, handle: linked.handle });
9929
+ saveAuth(void 0, {
9930
+ cliToken: linked.cliToken,
9931
+ handle: linked.handle,
9932
+ refreshToken: linked.refreshToken
9933
+ });
9298
9934
  }
9299
9935
  const handle = sanitizeServerText(linked.handle);
9300
9936
  console.log(
@@ -9367,7 +10003,7 @@ async function runDaemon() {
9367
10003
  await run({ dryRun: false, noSubmit: false, local: false, quiet: true });
9368
10004
  if (!loadConfig()?.cliToken) {
9369
10005
  throw new Error(
9370
- "not linked \u2014 nothing submitted (run `npx whoburnedmore link --token=\u2026` from your signed-in profile first)"
10006
+ "not linked \u2014 nothing submitted (generate a one-time link code from your signed-in profile first)"
9371
10007
  );
9372
10008
  }
9373
10009
  }
@@ -9586,7 +10222,10 @@ async function main() {
9586
10222
  break;
9587
10223
  }
9588
10224
  case "link":
9589
- await linkServerInstall(parseInstallToken(args));
10225
+ if (hasUnsafeInstallTokenArg(args)) {
10226
+ throw new Error("`--token` is no longer accepted because command-line secrets leak into shell history and process listings; run `npx whoburnedmore link` and paste the code when prompted");
10227
+ }
10228
+ await linkServerInstall(await readServerInstallToken());
9590
10229
  break;
9591
10230
  case "daemon":
9592
10231
  await runDaemon();
@@ -9639,7 +10278,7 @@ function printHelp() {
9639
10278
  npx whoburnedmore --local build the dashboard on your machine and open it (offline)
9640
10279
  npx whoburnedmore --dry-run print exactly what would be sent, send nothing
9641
10280
  npx whoburnedmore --no-submit collect locally, send nothing (no dashboard)
9642
- npx whoburnedmore link --token=TOKEN link this server/VM to your signed-in account
10281
+ npx whoburnedmore link link this server/VM (prompts for a one-time code)
9643
10282
  npx whoburnedmore daemon keep syncing in the foreground (VMs/containers with no cron)
9644
10283
  npx whoburnedmore private take yourself off the public leaderboard
9645
10284
  npx whoburnedmore public put yourself back on it