whoburnedmore 0.9.20 → 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 +864 -236
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -7,10 +7,8 @@ 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";
14
12
  import { pathToFileURL } from "node:url";
15
13
  import { createInterface } from "node:readline/promises";
16
14
  import pc2 from "picocolors";
@@ -25,8 +23,8 @@ function parseOrg(args) {
25
23
  function parsePass(args) {
26
24
  return parseValueFlag(args, "--pass") ?? parseValueFlag(args, "--code");
27
25
  }
28
- function parseInstallToken(args) {
29
- return parseValueFlag(args, "--token");
26
+ function hasUnsafeInstallTokenArg(args) {
27
+ return args.some((arg) => arg === "--token" || arg.startsWith("--token="));
30
28
  }
31
29
  function applyScope(payload, flags) {
32
30
  if (flags.board) payload.board = flags.board;
@@ -67,9 +65,65 @@ function resolveCommand(args) {
67
65
  return words[0] ?? "run";
68
66
  }
69
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
+
70
111
  // src/api.ts
112
+ var DEFAULT_API_BASE = "https://api.whoburnedmore.com";
113
+ var MAX_SERVER_RESPONSE_BYTES = 2 * 1024 * 1024;
71
114
  function apiBase() {
72
- 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
+ }
73
127
  }
74
128
  function webBase() {
75
129
  return process.env.WHOBURNEDMORE_WEB ?? "https://whoburnedmore.com";
@@ -89,7 +143,7 @@ function isOpenableUrl(url) {
89
143
  return /^(https?|file):\/\//.test(url);
90
144
  }
91
145
  async function readJson(res) {
92
- const text = await res.text();
146
+ const text = await readTextResponseCapped(res, MAX_SERVER_RESPONSE_BYTES);
93
147
  if (!text) return {};
94
148
  try {
95
149
  return JSON.parse(text);
@@ -110,7 +164,8 @@ async function send(method, path, body, token) {
110
164
  ...body === void 0 ? {} : { body: JSON.stringify(body) },
111
165
  // Bound the request so a slow/black-holing/hostile server can't hang the CLI
112
166
  // — or the unattended 15-minute background sync — indefinitely.
113
- signal: AbortSignal.timeout(3e4)
167
+ signal: AbortSignal.timeout(3e4),
168
+ redirect: "error"
114
169
  });
115
170
  } catch {
116
171
  throw new Error(
@@ -161,9 +216,9 @@ async function devicePoll(deviceCode) {
161
216
  }
162
217
  return body;
163
218
  }
164
- async function refreshCliToken(anonKey) {
219
+ async function refreshCliToken(refreshToken) {
165
220
  try {
166
- const { status, body } = await post("/v1/auth/cli/refresh", { anonKey });
221
+ const { status, body } = await post("/v1/auth/cli/refresh", { refreshToken });
167
222
  if (status === 200 && typeof body.token === "string" && body.token) {
168
223
  return { token: body.token, handle: body.handle ?? "" };
169
224
  }
@@ -173,14 +228,20 @@ async function refreshCliToken(anonKey) {
173
228
  }
174
229
  async function bindDeviceKey(token, anonKey) {
175
230
  try {
176
- const { status } = await post(
231
+ const { status, body } = await post(
177
232
  "/v1/me/devices/bind",
178
233
  { anonKey },
179
234
  token
180
235
  );
181
- 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 };
182
243
  } catch {
183
- return false;
244
+ return { definitive: false };
184
245
  }
185
246
  }
186
247
  async function submit(token, payload) {
@@ -229,31 +290,112 @@ async function redeemServerInstall(token, anonKey) {
229
290
 
230
291
  // src/autosync.ts
231
292
  import { spawnSync } from "node:child_process";
293
+ import { createRequire } from "node:module";
232
294
  import {
233
295
  existsSync as existsSync2,
234
296
  mkdirSync as mkdirSync2,
235
- readFileSync as readFileSync2,
297
+ readFileSync,
236
298
  renameSync as renameSync2,
237
299
  rmSync as rmSync2,
238
300
  statSync,
239
301
  writeFileSync as writeFileSync2
240
302
  } from "node:fs";
241
303
  import { homedir as homedir2, platform } from "node:os";
242
- import { dirname, join as join2, posix, win32 } from "node:path";
304
+ import { dirname as dirname2, join as join2, posix, win32 } from "node:path";
243
305
 
244
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
245
315
  import { randomBytes } from "node:crypto";
246
316
  import {
247
317
  chmodSync,
248
- existsSync,
318
+ closeSync,
319
+ constants,
320
+ fstatSync,
249
321
  mkdirSync,
250
- readFileSync,
322
+ openSync,
323
+ readSync,
251
324
  renameSync,
252
325
  rmSync,
253
326
  writeFileSync
254
327
  } from "node:fs";
255
- import { homedir } from "node:os";
256
- 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
+ }
257
399
  function defaultConfigDir() {
258
400
  const override = process.env.WHOBURNEDMORE_CONFIG_DIR?.trim();
259
401
  if (override) return override;
@@ -263,10 +405,13 @@ function loadConfig(dir = defaultConfigDir()) {
263
405
  const file = join(dir, "config.json");
264
406
  if (!existsSync(file)) return null;
265
407
  try {
266
- 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);
267
411
  const config = {};
268
412
  if (typeof parsed.anonKey === "string") config.anonKey = parsed.anonKey;
269
413
  if (typeof parsed.cliToken === "string") config.cliToken = parsed.cliToken;
414
+ if (typeof parsed.refreshToken === "string") config.refreshToken = parsed.refreshToken;
270
415
  if (typeof parsed.handle === "string") config.handle = parsed.handle;
271
416
  if (typeof parsed.lastSyncAt === "number" && Number.isFinite(parsed.lastSyncAt))
272
417
  config.lastSyncAt = parsed.lastSyncAt;
@@ -282,28 +427,15 @@ function loadConfig(dir = defaultConfigDir()) {
282
427
  }
283
428
  }
284
429
  function saveConfig(dir = defaultConfigDir(), config = {}) {
285
- mkdirSync(dir, { recursive: true });
286
430
  const file = join(dir, "config.json");
287
- const tmp = join(dir, `config.json.${process.pid}.tmp`);
288
- try {
289
- writeFileSync(tmp, JSON.stringify(config, null, 2), { mode: 384 });
290
- try {
291
- chmodSync(tmp, 384);
292
- } catch {
293
- }
294
- renameSync(tmp, file);
295
- } catch (err) {
296
- try {
297
- rmSync(tmp, { force: true });
298
- } catch {
299
- }
300
- throw err;
431
+ if (!writePrivateFileAtomic(file, JSON.stringify(config, null, 2), MAX_CONFIG_BYTES)) {
432
+ throw new Error("could not save private CLI configuration");
301
433
  }
302
434
  }
303
435
  function ensureAnonKey(dir = defaultConfigDir()) {
304
436
  const config = loadConfig(dir) ?? {};
305
437
  if (config.anonKey) return config.anonKey;
306
- const anonKey = randomBytes(32).toString("hex");
438
+ const anonKey = randomBytes2(32).toString("hex");
307
439
  saveConfig(dir, { ...config, anonKey });
308
440
  return anonKey;
309
441
  }
@@ -317,7 +449,12 @@ function recordDeviceBound(dir = defaultConfigDir(), when = Date.now()) {
317
449
  }
318
450
  function saveAuth(dir = defaultConfigDir(), auth = { cliToken: "" }) {
319
451
  const config = loadConfig(dir) ?? {};
320
- 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
+ });
321
458
  }
322
459
  function clearAuth(dir = defaultConfigDir()) {
323
460
  const config = loadConfig(dir);
@@ -342,7 +479,9 @@ var STABLE_NPM_CANDIDATES = [
342
479
  "/usr/local/bin/npm",
343
480
  "/usr/bin/npm"
344
481
  ];
345
- 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}`;
346
485
  var SYNC_PATH_DIRS = [
347
486
  "/opt/homebrew/bin",
348
487
  "/usr/local/bin",
@@ -352,7 +491,7 @@ var SYNC_PATH_DIRS = [
352
491
  "/sbin"
353
492
  ];
354
493
  function syncPathEnv(npmPath = resolveNpmPath()) {
355
- const dir = dirname(npmPath);
494
+ const dir = dirname2(npmPath);
356
495
  const dirs = [];
357
496
  if (dir && dir !== "." && dir !== "/" && dir !== npmPath) dirs.push(dir);
358
497
  for (const d of SYNC_PATH_DIRS) {
@@ -482,7 +621,7 @@ function syncCommandArgs(npmPath = resolveNpmPath()) {
482
621
  "--yes",
483
622
  "--ignore-scripts",
484
623
  "--package",
485
- LATEST_PACKAGE_SPEC,
624
+ SCHEDULED_PACKAGE_SPEC,
486
625
  "--",
487
626
  "whoburnedmore",
488
627
  "sync"
@@ -697,8 +836,8 @@ function autoSyncInstalled() {
697
836
  }
698
837
  function readInstalledSystemd() {
699
838
  try {
700
- return `${readFileSync2(systemdServicePath(), "utf8")}
701
- ${readFileSync2(systemdTimerPath(), "utf8")}`;
839
+ return `${readFileSync(systemdServicePath(), "utf8")}
840
+ ${readFileSync(systemdTimerPath(), "utf8")}`;
702
841
  } catch {
703
842
  return null;
704
843
  }
@@ -710,7 +849,7 @@ ${buildSystemdTimer()}`;
710
849
  function readInstalledAgent() {
711
850
  if (platform() === "darwin") {
712
851
  const p = launchAgentPath();
713
- return existsSync2(p) ? readFileSync2(p, "utf8") : null;
852
+ return existsSync2(p) ? readFileSync(p, "utf8") : null;
714
853
  }
715
854
  if (platform() === "linux") {
716
855
  const mech = linuxSyncMechanism();
@@ -798,7 +937,7 @@ async function daemonLoop(deps) {
798
937
 
799
938
  // src/collect.ts
800
939
  import { execFile } from "node:child_process";
801
- import { createRequire as createRequire3 } from "node:module";
940
+ import { createRequire as createRequire4 } from "node:module";
802
941
  import { dirname as dirname6, join as join13 } from "node:path";
803
942
  import { promisify } from "node:util";
804
943
 
@@ -808,7 +947,7 @@ import { homedir as homedir4 } from "node:os";
808
947
  import { join as join5 } from "node:path";
809
948
 
810
949
  // src/native/claude.ts
811
- import { readdir, readFile as readFile2 } from "node:fs/promises";
950
+ import { readdir } from "node:fs/promises";
812
951
  import { homedir as homedir3 } from "node:os";
813
952
  import { join as join4 } from "node:path";
814
953
 
@@ -6249,7 +6388,12 @@ function estimateCostUSD(model, t) {
6249
6388
 
6250
6389
  // ../shared/dist/index.js
6251
6390
  var DateString = external_exports.string().regex(/^\d{4}-\d{2}-\d{2}$/, "must be YYYY-MM-DD");
6252
- 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);
6253
6397
  var ConnectorProvider = external_exports.enum([
6254
6398
  "anthropic-api",
6255
6399
  "openai-api",
@@ -6279,7 +6423,7 @@ var DailyUsageEntry = external_exports.object({
6279
6423
  cacheCreationTokens: tokenCount,
6280
6424
  cacheReadTokens: tokenCount,
6281
6425
  /** Estimated cost in USD for this entry. */
6282
- costUSD: external_exports.number().nonnegative(),
6426
+ costUSD,
6283
6427
  /** Where this entry came from. Defaults to the local CLI for back-compat. */
6284
6428
  origin: UsageOrigin.default("cli"),
6285
6429
  /** True when the numbers come from a provider's authoritative usage API. */
@@ -6294,7 +6438,7 @@ var DailyUsageEntry = external_exports.object({
6294
6438
  * cannot see request ids) omit it, and an omitted fingerprint is never
6295
6439
  * penalized.
6296
6440
  */
6297
- requestCount: external_exports.number().int().nonnegative().optional()
6441
+ requestCount: rollupCount.optional()
6298
6442
  });
6299
6443
  var Timestamp = external_exports.string().min(1).max(40);
6300
6444
  var SessionEntry = external_exports.object({
@@ -6305,49 +6449,76 @@ var SessionEntry = external_exports.object({
6305
6449
  outputTokens: tokenCount,
6306
6450
  cacheCreationTokens: tokenCount,
6307
6451
  cacheReadTokens: tokenCount,
6308
- costUSD: external_exports.number().nonnegative(),
6452
+ costUSD,
6309
6453
  lastActivity: Timestamp,
6310
6454
  /** Number of assistant messages in this session (from transcripts). Optional. */
6311
- messageCount: external_exports.number().int().nonnegative().optional()
6455
+ messageCount: rollupCount.optional()
6312
6456
  });
6313
6457
  var BlockEntry = external_exports.object({
6314
6458
  startTime: Timestamp,
6315
6459
  totalTokens: tokenCount,
6316
- costUSD: external_exports.number().nonnegative()
6460
+ costUSD
6317
6461
  });
6318
6462
  var ToolStat = external_exports.object({
6319
6463
  name: external_exports.string().min(1).max(128),
6320
- count: external_exports.number().int().nonnegative(),
6464
+ count: rollupCount,
6321
6465
  /** How many of those calls returned an error/interrupt (tool reliability). Optional. */
6322
- errors: external_exports.number().int().nonnegative().optional(),
6466
+ errors: rollupCount.optional(),
6323
6467
  /** Tokens burned on turns that used this tool (turn tokens split across its tool calls). Optional. */
6324
- tokens: external_exports.number().int().nonnegative().optional()
6468
+ tokens: tokenCount.optional()
6325
6469
  });
6326
6470
  var AgentStat = external_exports.object({
6327
6471
  /** Total assistant messages across transcripts. */
6328
- messageCount: external_exports.number().int().nonnegative(),
6472
+ messageCount: rollupCount,
6329
6473
  /** Assistant messages that ran inside a subagent sidechain. */
6330
- subagentMessages: external_exports.number().int().nonnegative(),
6474
+ subagentMessages: rollupCount,
6331
6475
  /** Tokens spent inside subagent sidechains. */
6332
- subagentTokens: external_exports.number().int().nonnegative(),
6476
+ subagentTokens: tokenCount,
6333
6477
  /** Total tokens observed across transcripts (denominator for the share). */
6334
- totalTokens: external_exports.number().int().nonnegative(),
6478
+ totalTokens: tokenCount,
6335
6479
  /**
6336
6480
  * Messages the human actually sent (their prompts) — non-sidechain user turns
6337
6481
  * carrying real text, NOT tool results or injected/meta turns. Denominator for
6338
6482
  * "avg cost per message". Optional (back-compat with older CLIs).
6339
6483
  */
6340
- userMessageCount: external_exports.number().int().nonnegative().optional()
6484
+ userMessageCount: rollupCount.optional()
6341
6485
  });
6342
6486
  var SkillStat = external_exports.object({
6343
6487
  name: external_exports.string().min(1).max(128),
6344
- count: external_exports.number().int().nonnegative(),
6488
+ count: rollupCount,
6345
6489
  /** Tokens burned in records produced while this skill was active. Optional. */
6346
- 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)
6347
6502
  });
6348
6503
  var SubmitPayload = external_exports.object({
6349
6504
  cliVersion: external_exports.string().min(1).max(32),
6350
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(),
6351
6522
  /** Optional per-conversation rollups (ccusage session). Back-compat: omittable. */
6352
6523
  sessions: external_exports.array(SessionEntry).max(1e4).optional(),
6353
6524
  /** Optional time-window rollups (ccusage blocks) for peak-hours analysis. */
@@ -6421,6 +6592,7 @@ function entryTotalTokens(e) {
6421
6592
  }
6422
6593
  var LeaderboardPeriod = external_exports.enum(["today", "7d", "30d", "all"]);
6423
6594
  var LeaderboardMetric = external_exports.enum(["tokens", "cost"]);
6595
+ var InsightsPeriod = external_exports.enum(["7d", "30d", "all"]);
6424
6596
  var OrgType = external_exports.enum(["company", "hackathon", "hackerhouse"]);
6425
6597
  var MemberRole = external_exports.enum(["owner", "admin", "member"]);
6426
6598
  var OrgBoardVisibility = external_exports.enum(["public", "members"]);
@@ -6559,18 +6731,63 @@ var OrgJoinInput = external_exports.object({
6559
6731
  });
6560
6732
 
6561
6733
  // src/native/file-cache.ts
6562
- import { mkdirSync as mkdirSync3, renameSync as renameSync3, writeFileSync as writeFileSync3 } from "node:fs";
6563
- import { readFile, stat } from "node:fs/promises";
6564
- 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
+ }
6565
6781
  function nativeCachePath(reader, env = process.env) {
6566
6782
  const override = env.WHOBURNEDMORE_CONFIG_DIR?.trim();
6567
6783
  const dir = override || defaultConfigDir();
6568
6784
  return join3(dir, `native-cache-${reader}.json`);
6569
6785
  }
6570
- async function loadCache(path, version) {
6786
+ async function loadCache(path, version, maxBytes) {
6571
6787
  try {
6572
- const raw = await readFile(path, "utf8");
6573
- const parsed = JSON.parse(raw);
6788
+ const read = await readTextFileCapped(path, { maxBytes });
6789
+ if (!read.ok) return {};
6790
+ const parsed = JSON.parse(read.content);
6574
6791
  if (parsed && parsed.v === version && parsed.files && typeof parsed.files === "object") {
6575
6792
  return parsed.files;
6576
6793
  }
@@ -6578,18 +6795,34 @@ async function loadCache(path, version) {
6578
6795
  }
6579
6796
  return {};
6580
6797
  }
6581
- function saveCache(path, version, files) {
6798
+ function saveCache(path, version, files, maxBytes) {
6799
+ let tmp = null;
6582
6800
  try {
6583
- mkdirSync3(dirname2(path), { recursive: true });
6584
- const tmp = `${path}.tmp-${process.pid}`;
6585
- 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);
6586
6809
  renameSync3(tmp, path);
6810
+ tmp = null;
6587
6811
  } catch {
6812
+ } finally {
6813
+ if (tmp) {
6814
+ try {
6815
+ unlinkSync(tmp);
6816
+ } catch {
6817
+ }
6818
+ }
6588
6819
  }
6589
6820
  }
6590
6821
  async function readFilesWithCache(opts) {
6591
6822
  const now = opts.now ?? Date.now;
6592
- 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);
6593
6826
  const fresh = {};
6594
6827
  const itemsByFile = [];
6595
6828
  let filesRead = 0;
@@ -6603,6 +6836,9 @@ async function readFilesWithCache(opts) {
6603
6836
  } catch {
6604
6837
  continue;
6605
6838
  }
6839
+ if (size > maxFileBytes) {
6840
+ continue;
6841
+ }
6606
6842
  const hit = cached[f];
6607
6843
  if (hit && hit.size === size && hit.mtimeMs === mtimeMs) {
6608
6844
  fresh[f] = hit;
@@ -6610,21 +6846,27 @@ async function readFilesWithCache(opts) {
6610
6846
  continue;
6611
6847
  }
6612
6848
  if (now() > opts.deadline) {
6613
- saveCache(opts.cachePath, opts.version, { ...cached, ...fresh });
6849
+ saveCache(opts.cachePath, opts.version, { ...cached, ...fresh }, maxCacheBytes);
6614
6850
  return { itemsByFile: null, filesRead, timedOut: true };
6615
6851
  }
6616
- let content;
6617
- try {
6618
- content = await readFile(f, "utf8");
6619
- } 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
+ }
6620
6862
  continue;
6621
6863
  }
6622
- const items = opts.parseFile(content, f);
6864
+ const items = opts.parseFile(read.content, f);
6623
6865
  fresh[f] = { size, mtimeMs, items };
6624
6866
  itemsByFile.push(items);
6625
6867
  filesRead += 1;
6626
6868
  }
6627
- saveCache(opts.cachePath, opts.version, fresh);
6869
+ saveCache(opts.cachePath, opts.version, fresh, maxCacheBytes);
6628
6870
  return { itemsByFile, filesRead, timedOut: false };
6629
6871
  }
6630
6872
 
@@ -6867,13 +7109,18 @@ async function collectClaudeRequests(env = process.env, opts = {}) {
6867
7109
  if (now() > deadline) {
6868
7110
  return { requests: [...acc.values()], found: true, timedOut: true };
6869
7111
  }
6870
- let content;
6871
- try {
6872
- content = await readFile2(f, "utf8");
6873
- } 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
+ }
6874
7121
  continue;
6875
7122
  }
6876
- accumulateClaudeLines(acc, splitLines(content));
7123
+ accumulateClaudeLines(acc, splitLines(read.content));
6877
7124
  }
6878
7125
  return { requests: [...acc.values()], found: true };
6879
7126
  }
@@ -7278,28 +7525,54 @@ function parseContinueJsonl(content) {
7278
7525
  }
7279
7526
  return mapContinueRecords(records);
7280
7527
  }
7281
- async function listJsonl2(dir) {
7282
- let dirents;
7283
- try {
7284
- dirents = await readdir2(dir, { withFileTypes: true });
7285
- } catch {
7286
- return [];
7287
- }
7288
- const out = [];
7289
- for (const d of dirents) {
7290
- const full = join6(dir, d.name);
7291
- if (d.isDirectory()) out.push(...await listJsonl2(full));
7292
- 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
+ }
7293
7557
  }
7294
- return out;
7558
+ return { files, aborted: false };
7295
7559
  }
7296
7560
  var CONTINUE_CACHE_VERSION = 2;
7297
7561
  async function collectContinue(opts = {}) {
7298
7562
  const env = opts.env ?? process.env;
7299
7563
  const home = opts.continueDir ?? join6(env.HOME || homedir5(), ".continue");
7300
- const files = await listJsonl2(join6(home, "dev_data"));
7301
- if (files.length === 0) return { entries: [], found: false, filesScanned: 0 };
7302
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 };
7303
7576
  const res = await readFilesWithCache({
7304
7577
  files,
7305
7578
  cachePath: opts.cachePath ?? nativeCachePath("continue", env),
@@ -7312,7 +7585,7 @@ async function collectContinue(opts = {}) {
7312
7585
  e.costUSD,
7313
7586
  e.requestCount ?? 0
7314
7587
  ]),
7315
- deadline: now() + (opts.budgetMs ?? NATIVE_READ_BUDGET_MS),
7588
+ deadline,
7316
7589
  now
7317
7590
  });
7318
7591
  if (!res.itemsByFile) {
@@ -7351,15 +7624,15 @@ async function collectContinue(opts = {}) {
7351
7624
 
7352
7625
  // src/cursor.ts
7353
7626
  import { spawnSync as spawnSync3 } from "node:child_process";
7354
- import { existsSync as existsSync3 } from "node:fs";
7355
- 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";
7356
7629
  import { homedir as homedir6, platform as platform2 } from "node:os";
7357
7630
  import { join as join8 } from "node:path";
7358
7631
 
7359
7632
  // src/tokscale.ts
7360
7633
  import { spawnSync as spawnSync2 } from "node:child_process";
7361
- import { createRequire } from "node:module";
7362
- 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";
7363
7636
  var LOOKBACK_DAYS = 30;
7364
7637
  function num3(n) {
7365
7638
  const v = Math.round(Number(n));
@@ -7378,9 +7651,9 @@ function mapTokscaleDay(date, json) {
7378
7651
  const outputTokens = num3(e.output) + num3(e.reasoning);
7379
7652
  const cacheCreationTokens = num3(e.cacheWrite);
7380
7653
  const cacheReadTokens = num3(e.cacheRead);
7381
- const costUSD = numCost(e.cost);
7654
+ const costUSD2 = numCost(e.cost);
7382
7655
  const total = inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens;
7383
- if (total === 0 && costUSD === 0) continue;
7656
+ if (total === 0 && costUSD2 === 0) continue;
7384
7657
  out.push({
7385
7658
  date,
7386
7659
  tool: "cursor",
@@ -7389,7 +7662,7 @@ function mapTokscaleDay(date, json) {
7389
7662
  outputTokens,
7390
7663
  cacheCreationTokens,
7391
7664
  cacheReadTokens,
7392
- costUSD,
7665
+ costUSD: costUSD2,
7393
7666
  origin: "cli",
7394
7667
  verified: false
7395
7668
  });
@@ -7398,12 +7671,12 @@ function mapTokscaleDay(date, json) {
7398
7671
  }
7399
7672
  function resolveTokscaleBin() {
7400
7673
  try {
7401
- const require3 = createRequire(import.meta.url);
7402
- const pkgPath = require3.resolve("tokscale/package.json");
7403
- 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");
7404
7677
  const rel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.tokscale ?? "";
7405
7678
  if (!rel) return null;
7406
- const binPath = join7(dirname3(pkgPath), rel);
7679
+ const binPath = join7(dirname4(pkgPath), rel);
7407
7680
  if (/\.(c|m)?js$/.test(binPath)) {
7408
7681
  return { cmd: process.execPath, prefixArgs: [binPath] };
7409
7682
  }
@@ -7460,31 +7733,47 @@ function collectCursorViaTokscale(lookbackDays = LOOKBACK_DAYS) {
7460
7733
 
7461
7734
  // src/cursor.ts
7462
7735
  var EVENTS_URL = "https://cursor.com/api/dashboard/get-filtered-usage-events";
7736
+ var MAX_CURSOR_PAGE_BYTES = 4 * 1024 * 1024;
7463
7737
  function cursorDbPath() {
7464
7738
  const home = homedir6();
7465
7739
  const os = platform2();
7466
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");
7467
7741
  return existsSync3(p) ? p : null;
7468
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
+ }
7469
7756
  function readCursorToken(db) {
7470
- const require3 = createRequire2(import.meta.url);
7757
+ const require4 = createRequire3(import.meta.url);
7471
7758
  try {
7472
- const { DatabaseSync } = require3("node:sqlite");
7759
+ const { DatabaseSync } = require4("node:sqlite");
7473
7760
  const d = new DatabaseSync(db, { readOnly: true });
7474
7761
  const row = d.prepare("SELECT value FROM ItemTable WHERE key = ?").get("cursorAuth/accessToken");
7475
7762
  d.close();
7476
7763
  if (row?.value) return String(row.value);
7477
7764
  } catch {
7478
7765
  }
7479
- try {
7480
- const res = spawnSync3(
7481
- "sqlite3",
7482
- [db, "SELECT value FROM ItemTable WHERE key='cursorAuth/accessToken';"],
7483
- { encoding: "utf8", timeout: 1e4 }
7484
- );
7485
- const out = res.stdout?.trim();
7486
- if (res.status === 0 && out) return out;
7487
- } 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
+ }
7488
7777
  }
7489
7778
  return null;
7490
7779
  }
@@ -7571,12 +7860,16 @@ async function fetchCursorEvents(cookie, maxPages = 30, pageSize = 500) {
7571
7860
  Cookie: cookie
7572
7861
  },
7573
7862
  body: JSON.stringify({ page, pageSize }),
7574
- signal: AbortSignal.timeout(2e4)
7863
+ signal: AbortSignal.timeout(2e4),
7864
+ redirect: "error"
7575
7865
  });
7576
7866
  if (!res.ok) {
7577
7867
  throw new Error(`cursor usage page ${page} failed (HTTP ${res.status})`);
7578
7868
  }
7579
- const body = await res.json();
7869
+ const body = await readJsonResponseCapped(
7870
+ res,
7871
+ MAX_CURSOR_PAGE_BYTES
7872
+ );
7580
7873
  const batch = body.usageEventsDisplay ?? [];
7581
7874
  all.push(...batch);
7582
7875
  if (batch.length < pageSize) break;
@@ -7606,7 +7899,7 @@ async function collectCursor(opts) {
7606
7899
  // src/native/codex.ts
7607
7900
  import { readdir as readdir3 } from "node:fs/promises";
7608
7901
  import { homedir as homedir7 } from "node:os";
7609
- import { join as join9, resolve } from "node:path";
7902
+ import { basename, join as join9, resolve } from "node:path";
7610
7903
  function num5(n) {
7611
7904
  const v = Math.round(Number(n));
7612
7905
  return Number.isFinite(v) && v > 0 ? v : 0;
@@ -7623,8 +7916,10 @@ function readTokenFields(payload) {
7623
7916
  output: num5(output)
7624
7917
  };
7625
7918
  }
7626
- function parseCodexRollout(lines) {
7919
+ function inspectCodexRollout(lines) {
7627
7920
  let model = "unknown";
7921
+ let sawSessionMeta = false;
7922
+ let replayedRollout = false;
7628
7923
  const perDay = /* @__PURE__ */ new Map();
7629
7924
  let lastSeenDate = null;
7630
7925
  for (const raw of lines) {
@@ -7639,6 +7934,13 @@ function parseCodexRollout(lines) {
7639
7934
  const payload = obj.payload;
7640
7935
  if (!payload || typeof payload !== "object") continue;
7641
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
+ }
7642
7944
  if (kind === "session_meta" || kind === "turn_context") {
7643
7945
  if (typeof payload.model === "string" && payload.model) model = payload.model;
7644
7946
  }
@@ -7658,7 +7960,8 @@ function parseCodexRollout(lines) {
7658
7960
  }
7659
7961
  }
7660
7962
  }
7661
- if (perDay.size === 0) return [];
7963
+ if (perDay.size === 0)
7964
+ return { sessions: [], replaySessions: [], replayCandidateDates: [] };
7662
7965
  const dates = [...perDay.keys()].sort();
7663
7966
  const out = [];
7664
7967
  let prev = { input: 0, cached: 0, output: 0 };
@@ -7668,8 +7971,8 @@ function parseCodexRollout(lines) {
7668
7971
  const dCached = Math.max(0, cum.cached - prev.cached);
7669
7972
  const dOutput = Math.max(0, cum.output - prev.output);
7670
7973
  prev = cum;
7671
- const cacheReadTokens = dCached;
7672
- const inputTokens = Math.max(0, dInput - dCached);
7974
+ const cacheReadTokens = Math.min(dCached, dInput);
7975
+ const inputTokens = dInput - cacheReadTokens;
7673
7976
  const outputTokens = dOutput;
7674
7977
  if (inputTokens + outputTokens + cacheReadTokens === 0) continue;
7675
7978
  out.push({
@@ -7682,7 +7985,11 @@ function parseCodexRollout(lines) {
7682
7985
  turnCount: turns
7683
7986
  });
7684
7987
  }
7685
- return out;
7988
+ return {
7989
+ sessions: replayedRollout ? [] : out,
7990
+ replaySessions: replayedRollout ? out : [],
7991
+ replayCandidateDates: replayedRollout ? dates : []
7992
+ };
7686
7993
  }
7687
7994
  function foldCodexSessions(acc, sessions) {
7688
7995
  for (const s of sessions) {
@@ -7730,7 +8037,7 @@ function resolveCodexHome(env = process.env) {
7730
8037
  return env.CODEX_HOME && env.CODEX_HOME.trim() ? env.CODEX_HOME.trim() : join9(homedir7(), ".codex");
7731
8038
  }
7732
8039
  function resolveCodexSessionsDirs(env = process.env) {
7733
- const home = resolveCodexHome(env);
8040
+ const home = resolve(resolveCodexHome(env));
7734
8041
  return [join9(home, "sessions"), join9(home, "archived_sessions")];
7735
8042
  }
7736
8043
  async function listJsonl3(dir) {
@@ -7759,9 +8066,10 @@ function* splitLines3(content) {
7759
8066
  if (start < content.length) yield content.slice(start);
7760
8067
  }
7761
8068
  var NATIVE_READ_BUDGET_MS2 = 45e3;
7762
- var CODEX_CACHE_VERSION = 2;
7763
- function toCachedSession(s) {
8069
+ var CODEX_CACHE_VERSION = 7;
8070
+ function toCachedSession(s, kind = "usage") {
7764
8071
  return [
8072
+ kind,
7765
8073
  s.date,
7766
8074
  s.model,
7767
8075
  s.inputTokens,
@@ -7773,25 +8081,53 @@ function toCachedSession(s) {
7773
8081
  }
7774
8082
  function fromCachedSession(t) {
7775
8083
  return {
7776
- date: t[0],
7777
- model: t[1],
7778
- inputTokens: t[2],
7779
- outputTokens: t[3],
7780
- cacheCreationTokens: t[4],
7781
- cacheReadTokens: t[5],
7782
- 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]
7783
8091
  };
7784
8092
  }
7785
8093
  async function collectCodexNative(env = process.env, opts = {}) {
7786
8094
  const dirs = resolveCodexSessionsDirs(env);
7787
- 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
+ }
7788
8108
  if (files.length === 0) return { entries: [], found: false, filesScanned: 0 };
7789
8109
  const now = opts.now ?? Date.now;
7790
8110
  const res = await readFilesWithCache({
7791
8111
  files,
7792
8112
  cachePath: opts.cachePath ?? nativeCachePath("codex", env),
7793
8113
  version: CODEX_CACHE_VERSION,
7794
- 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
+ },
7795
8131
  deadline: now() + (opts.budgetMs ?? NATIVE_READ_BUDGET_MS2),
7796
8132
  now
7797
8133
  });
@@ -7804,13 +8140,24 @@ async function collectCodexNative(env = process.env, opts = {}) {
7804
8140
  };
7805
8141
  }
7806
8142
  const acc = /* @__PURE__ */ new Map();
8143
+ const legacyAcc = /* @__PURE__ */ new Map();
8144
+ const replayCandidateDates = /* @__PURE__ */ new Set();
7807
8145
  for (const items of res.itemsByFile) {
7808
- 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);
7809
8154
  }
7810
8155
  return {
7811
8156
  entries: finalizeCodexEntries(acc),
7812
8157
  found: true,
7813
- filesScanned: res.filesRead
8158
+ filesScanned: res.filesRead,
8159
+ replayCandidateDates: [...replayCandidateDates].sort(),
8160
+ legacyEntries: finalizeCodexEntries(legacyAcc)
7814
8161
  };
7815
8162
  }
7816
8163
 
@@ -8016,16 +8363,20 @@ async function collectVscodeAgent(opts) {
8016
8363
  }
8017
8364
 
8018
8365
  // src/pricing-live.ts
8019
- import { mkdir, readFile as readFile3, rename, writeFile } from "node:fs/promises";
8020
- 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";
8021
8369
  var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
8022
8370
  var FETCH_TIMEOUT_MS = 5e3;
8371
+ var MAX_PRICING_BYTES = 32 * 1024 * 1024;
8023
8372
  function pricingCachePath(dir = defaultConfigDir()) {
8024
8373
  return join11(dir, "pricing-cache.json");
8025
8374
  }
8026
8375
  async function readCache(path) {
8027
8376
  try {
8028
- 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);
8029
8380
  if (typeof parsed?.fetchedAt === "number" && parsed.table && typeof parsed.table === "object") {
8030
8381
  return parsed;
8031
8382
  }
@@ -8036,10 +8387,11 @@ async function readCache(path) {
8036
8387
  async function fetchLiveTable(url) {
8037
8388
  try {
8038
8389
  const res = await fetch(url, {
8039
- signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
8390
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
8391
+ redirect: "error"
8040
8392
  });
8041
8393
  if (!res.ok) return null;
8042
- const table = litellmToTable(await res.json());
8394
+ const table = litellmToTable(await readJsonResponseCapped(res, MAX_PRICING_BYTES));
8043
8395
  return Object.keys(table).length > 0 ? table : null;
8044
8396
  } catch {
8045
8397
  return null;
@@ -8057,10 +8409,20 @@ async function loadLivePricing(env = process.env, now = Date.now, cachePath = pr
8057
8409
  if (live) {
8058
8410
  setLivePricing(live);
8059
8411
  try {
8060
- await mkdir(dirname4(path), { recursive: true });
8061
- const tmp = `${path}.${process.pid}.tmp`;
8062
- await writeFile(tmp, JSON.stringify({ fetchedAt: now(), table: live }));
8063
- 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
+ }
8064
8426
  } catch {
8065
8427
  }
8066
8428
  return "live";
@@ -8073,8 +8435,8 @@ async function loadLivePricing(env = process.env, now = Date.now, cachePath = pr
8073
8435
  }
8074
8436
 
8075
8437
  // src/provenance-store.ts
8076
- import { mkdirSync as mkdirSync4, readFileSync as readFileSync3, renameSync as renameSync4, writeFileSync as writeFileSync4 } from "node:fs";
8077
- 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;
8078
8440
  var PROVENANCE_STORE_VERSION = 1;
8079
8441
  var KEY_SEP = "|";
8080
8442
  var keyOf = (date, tool) => `${date}${KEY_SEP}${tool}`;
@@ -8088,7 +8450,9 @@ function provenanceStorePath(env = process.env) {
8088
8450
  }
8089
8451
  function loadProvenanceStore(path) {
8090
8452
  try {
8091
- 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);
8092
8456
  if (parsed && parsed.v === PROVENANCE_STORE_VERSION && parsed.req && typeof parsed.req === "object") {
8093
8457
  return {
8094
8458
  v: PROVENANCE_STORE_VERSION,
@@ -8102,10 +8466,7 @@ function loadProvenanceStore(path) {
8102
8466
  }
8103
8467
  function saveProvenanceStore(path, store) {
8104
8468
  try {
8105
- mkdirSync4(dirname5(path), { recursive: true });
8106
- const tmp = `${path}.tmp-${process.pid}`;
8107
- writeFileSync4(tmp, JSON.stringify(store));
8108
- renameSync4(tmp, path);
8469
+ writePrivateFileAtomic(path, JSON.stringify(store), MAX_PROVENANCE_STORE_BYTES);
8109
8470
  } catch {
8110
8471
  }
8111
8472
  }
@@ -8159,12 +8520,131 @@ function reconcileProvenance(entries, agent, complete, store) {
8159
8520
  };
8160
8521
  }
8161
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
+
8162
8623
  // src/collect.ts
8163
8624
  var execFileAsync = promisify(execFile);
8164
8625
  var NATIVE_COVERED_SOURCES = /* @__PURE__ */ new Set(["claude", "codex"]);
8165
8626
  var CCUSAGE_TIMEOUT_MS = 25e3;
8166
8627
  var CCUSAGE_FALLBACK_TIMEOUT_MS = NATIVE_READ_BUDGET_MS;
8167
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
+ }
8168
8648
  var SOURCES = [
8169
8649
  "claude",
8170
8650
  "codex",
@@ -8292,16 +8772,16 @@ function mapCcusageBlocks(json) {
8292
8772
  if (b.isGap === true) continue;
8293
8773
  if (typeof b.startTime !== "string") continue;
8294
8774
  const totalTokens = norm(b.totalTokens);
8295
- const costUSD = normCost(b.costUSD);
8296
- if (totalTokens === 0 && costUSD === 0) continue;
8297
- 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 });
8298
8778
  }
8299
8779
  return out;
8300
8780
  }
8301
8781
  function resolveCcusageBin() {
8302
- const require3 = createRequire3(import.meta.url);
8303
- const pkgPath = require3.resolve("ccusage/package.json");
8304
- 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");
8305
8785
  const rel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.ccusage ?? "ccusage";
8306
8786
  const binPath = join13(dirname6(pkgPath), rel);
8307
8787
  if (/\.(c|m)?js$/.test(binPath)) {
@@ -8309,6 +8789,68 @@ function resolveCcusageBin() {
8309
8789
  }
8310
8790
  return { cmd: binPath, prefixArgs: [] };
8311
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
+ }
8312
8854
  function dedupeDaily(entries) {
8313
8855
  const byKey = /* @__PURE__ */ new Map();
8314
8856
  for (const e of entries) {
@@ -8362,8 +8904,7 @@ function dedupeBlocks(blocks) {
8362
8904
  function selectSourceEntries(source, ccusageEntries, native) {
8363
8905
  if (source === "claude" && nativeReaderWon(native.claude))
8364
8906
  return native.claude.entries;
8365
- if (source === "codex" && nativeReaderWon(native.codex))
8366
- return native.codex.entries;
8907
+ if (source === "codex") return ccusageEntries;
8367
8908
  return ccusageEntries;
8368
8909
  }
8369
8910
  function nativeReaderWon(result) {
@@ -8371,7 +8912,7 @@ function nativeReaderWon(result) {
8371
8912
  }
8372
8913
  function ccusageFallbackSources(native) {
8373
8914
  return SOURCES.filter(
8374
- (s) => NATIVE_COVERED_SOURCES.has(s) && !nativeReaderWon(s === "claude" ? native.claude : native.codex)
8915
+ (s) => s === "codex" || s === "claude" && !nativeReaderWon(native.claude)
8375
8916
  );
8376
8917
  }
8377
8918
  function ccusageClaudeEnv(env = process.env) {
@@ -8387,7 +8928,7 @@ async function runCcusageOnce(cmd, args, env, timeoutMs = CCUSAGE_TIMEOUT_MS) {
8387
8928
  try {
8388
8929
  const { stdout } = await execFileAsync(cmd, args, {
8389
8930
  encoding: "utf8",
8390
- maxBuffer: 64 * 1024 * 1024,
8931
+ maxBuffer: CCUSAGE_MAX_BUFFER_BYTES,
8391
8932
  // A single source shouldn't be able to hang the whole run: a hung source
8392
8933
  // gets killed and (if transient) retried once below rather than stalling
8393
8934
  // everything for minutes. The claude/codex fallback passes a longer cap —
@@ -8428,22 +8969,26 @@ async function collectAll(onProgress, opts) {
8428
8969
  const nativeCodexTask = collectCodexNative().catch(
8429
8970
  () => ({ entries: [], found: false, filesScanned: 0, timedOut: true })
8430
8971
  );
8431
- const sourceTasks = SOURCES.map(async (source) => {
8432
- if (NATIVE_COVERED_SOURCES.has(source)) {
8433
- 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
+ ]);
8434
8988
  tick();
8435
- return { source, mapped: [] };
8989
+ return { source, mapped: json ? mapCcusageDaily(source, json) : [] };
8436
8990
  }
8437
- const json = await runCcusage(cmd, [
8438
- ...prefixArgs,
8439
- source,
8440
- "daily",
8441
- "--json",
8442
- "--offline"
8443
- ]);
8444
- tick();
8445
- return { source, mapped: json ? mapCcusageDaily(source, json) : [] };
8446
- });
8991
+ );
8447
8992
  const sessionTask = runCcusage(
8448
8993
  cmd,
8449
8994
  [...prefixArgs, "session", "--json", "--offline"],
@@ -8491,7 +9036,7 @@ async function collectAll(onProgress, opts) {
8491
9036
  vscodeResults,
8492
9037
  continueResult
8493
9038
  ] = await Promise.all([
8494
- Promise.all(sourceTasks),
9039
+ sourceTasks,
8495
9040
  sessionTask,
8496
9041
  blockTask,
8497
9042
  cursorTask,
@@ -8503,6 +9048,7 @@ async function collectAll(onProgress, opts) {
8503
9048
  ]);
8504
9049
  const native = { claude: nativeClaude, codex: nativeCodex };
8505
9050
  const fallbacks = /* @__PURE__ */ new Map();
9051
+ let codexReplayReadSucceeded = false;
8506
9052
  for (const source of ccusageFallbackSources(native)) {
8507
9053
  const json = await runCcusage(
8508
9054
  cmd,
@@ -8511,8 +9057,15 @@ async function collectAll(onProgress, opts) {
8511
9057
  source === "claude" ? ccusageClaudeEnv() : void 0,
8512
9058
  CCUSAGE_FALLBACK_TIMEOUT_MS
8513
9059
  );
9060
+ if (source === "codex") codexReplayReadSucceeded = json !== null;
8514
9061
  fallbacks.set(source, json ? mapCcusageDaily(source, json) : []);
8515
9062
  }
9063
+ const codexReplayEntries = fallbacks.get("codex") ?? [];
9064
+ const replayCorrection = codexReplayCorrectionMetadata(
9065
+ nativeCodex,
9066
+ codexReplayEntries,
9067
+ codexReplayReadSucceeded
9068
+ );
8516
9069
  const entries = [];
8517
9070
  const toolsFound = [];
8518
9071
  for (const { source, mapped } of sourceResults) {
@@ -8539,13 +9092,16 @@ async function collectAll(onProgress, opts) {
8539
9092
  }
8540
9093
  const { tools, skills, agent, sessionMessages, complete } = attribution;
8541
9094
  onProgress?.(COLLECT_STAGES, COLLECT_STAGES, "");
8542
- const dedupedSessions = dedupeSessions(sessions).map((s) => {
9095
+ const localSessions = dedupeSessions(sessions).map((s) => {
8543
9096
  const messageCount = sessionMessages.get(s.sessionId);
8544
9097
  return {
8545
9098
  ...s,
8546
9099
  ...messageCount ? { messageCount } : {}
8547
9100
  };
8548
9101
  });
9102
+ const dedupedSessions = sanitizeSessions(localSessions);
9103
+ const boundaryEntries = sanitizeDailyEntries(entries);
9104
+ const boundaryAgent = sanitizeAgentStat(agent);
8549
9105
  const scanComplete = isAuthoritativeScan(
8550
9106
  complete,
8551
9107
  nativeClaude,
@@ -8555,25 +9111,40 @@ async function collectAll(onProgress, opts) {
8555
9111
  );
8556
9112
  const storePath = provenanceStorePath();
8557
9113
  const reconciled = reconcileProvenance(
8558
- dedupeDaily(entries),
8559
- agent,
9114
+ dedupeDaily(boundaryEntries),
9115
+ boundaryAgent,
8560
9116
  scanComplete,
8561
9117
  loadProvenanceStore(storePath)
8562
9118
  );
9119
+ const reconciledEntries = sanitizeDailyEntries(reconciled.entries);
8563
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));
8564
9127
  return {
8565
9128
  // Cap each array to the server's accepted maximum (the shared SubmitPayload
8566
9129
  // schema: entries ≤ 20000, sessions/blocks ≤ 10000), keeping the
8567
9130
  // highest-token rows. Without this, a power user with >10000 distinct sessions
8568
9131
  // would have their ENTIRE submit rejected with a 400 instead of a capped one.
8569
9132
  // tools/skills are already bounded upstream (attribution caps).
8570
- entries: capByTokens(reconciled.entries, 2e4, entryTokens2),
9133
+ entries: cappedEntries,
9134
+ codexReplayTombstoneDates: payloadReplayCorrection.tombstoneDates.filter(
9135
+ (date) => safeReplayDates.has(date)
9136
+ ),
9137
+ codexReplayPriorScopes: safeReplayScopes,
8571
9138
  sessions: capByTokens(dedupedSessions, 1e4, entryTokens2),
8572
- blocks: capByTokens(dedupeBlocks(blocks), 1e4, (b) => b.totalTokens),
9139
+ blocks: capByTokens(
9140
+ dedupeBlocks(sanitizeBlocks(blocks)),
9141
+ 1e4,
9142
+ (b) => b.totalTokens
9143
+ ),
8573
9144
  toolsFound,
8574
- tools,
8575
- skills,
8576
- agent: reconciled.agent,
9145
+ tools: sanitizeToolStats(tools),
9146
+ skills: sanitizeSkillStats(skills),
9147
+ agent: sanitizeAgentStat(reconciled.agent),
8577
9148
  attributionComplete: complete
8578
9149
  };
8579
9150
  }
@@ -8598,7 +9169,7 @@ function antigravityNoticeLines() {
8598
9169
  }
8599
9170
 
8600
9171
  // src/verify-upload.ts
8601
- import { createHash } from "node:crypto";
9172
+ import { createHash as createHash3 } from "node:crypto";
8602
9173
  function prepareVerifyUpload(requests, scanTimedOut, cap = 5e4) {
8603
9174
  const sorted = requests.slice().sort((a, b) => b.ts - a.ts);
8604
9175
  const truncated = scanTimedOut || sorted.length > cap;
@@ -8612,7 +9183,7 @@ function prepareVerifyUpload(requests, scanTimedOut, cap = 5e4) {
8612
9183
  outputTokens: r.outputTokens,
8613
9184
  cacheCreationTokens: r.cacheCreationTokens,
8614
9185
  cacheReadTokens: r.cacheReadTokens,
8615
- reqHash: createHash("sha256").update(r.key).digest("hex").slice(0, 32)
9186
+ reqHash: createHash3("sha256").update(r.key).digest("hex").slice(0, 32)
8616
9187
  }));
8617
9188
  return { records, truncated };
8618
9189
  }
@@ -8679,6 +9250,9 @@ function agentStatusReport(now = Date.now()) {
8679
9250
  });
8680
9251
  }
8681
9252
 
9253
+ // src/local-dashboard.ts
9254
+ import { join as join15 } from "node:path";
9255
+
8682
9256
  // src/output.ts
8683
9257
  function formatTokens(n) {
8684
9258
  if (n >= 1e9) return `${(n / 1e9).toFixed(2)}B`;
@@ -8721,6 +9295,14 @@ function signedInNextStepLines(result) {
8721
9295
  }
8722
9296
 
8723
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
+ }
8724
9306
  function esc(s) {
8725
9307
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
8726
9308
  }
@@ -8951,8 +9533,8 @@ async function publishLocal(payload, deps) {
8951
9533
  }
8952
9534
 
8953
9535
  // src/index.ts
8954
- var require2 = createRequire4(import.meta.url);
8955
- var VERSION = require2("../package.json").version;
9536
+ var require3 = createRequire5(import.meta.url);
9537
+ var VERSION = require3("../package.json").version;
8956
9538
  var LOADING_VIBES = [
8957
9539
  "counting up your token usage, right here on your machine\u2026",
8958
9540
  "tallying tokens across every coding agent you use\u2026",
@@ -9024,10 +9606,8 @@ async function confirm(question) {
9024
9606
  }
9025
9607
  function showLocalDashboard(payload) {
9026
9608
  const dir = defaultConfigDir();
9027
- mkdirSync5(dir, { recursive: true });
9028
- const file = join15(dir, "dashboard.html");
9029
- writeFileSync5(
9030
- file,
9609
+ const file = writeLocalDashboard(
9610
+ dir,
9031
9611
  renderDashboardHtml(payload.entries, /* @__PURE__ */ new Date(), { webBaseUrl: webBase() })
9032
9612
  );
9033
9613
  const fileUrl = pathToFileURL(file).href;
@@ -9058,7 +9638,16 @@ async function run(flags) {
9058
9638
  } finally {
9059
9639
  progress.stop();
9060
9640
  }
9061
- 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;
9062
9651
  if (entries.length === 0) {
9063
9652
  console.log();
9064
9653
  console.log(" Nothing to burn yet \u2014 no local usage found from any coding agent.");
@@ -9070,8 +9659,14 @@ async function run(flags) {
9070
9659
  return;
9071
9660
  }
9072
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);
9073
9669
  payload.tzOffsetMinutes = -(/* @__PURE__ */ new Date()).getTimezoneOffset();
9074
- if (sessions.length > 0) payload.sessions = sessions;
9075
9670
  if (blocks.length > 0) payload.blocks = blocks;
9076
9671
  if (tools.length > 0) payload.tools = tools;
9077
9672
  if (skills.length > 0) payload.skills = skills;
@@ -9091,19 +9686,11 @@ async function run(flags) {
9091
9686
  confirm,
9092
9687
  signIn: ensureSignedIn,
9093
9688
  submit: async (token, p) => {
9094
- const result = await submit(token, p);
9689
+ const result = await submitFromBoundDevice(token, p);
9095
9690
  try {
9096
9691
  recordSync();
9097
9692
  } catch {
9098
9693
  }
9099
- try {
9100
- const cfgNow = loadConfig();
9101
- if (!cfgNow?.deviceBoundAt) {
9102
- const key = ensureAnonKey();
9103
- if (await bindDeviceKey(token, key)) recordDeviceBound();
9104
- }
9105
- } catch {
9106
- }
9107
9694
  return result;
9108
9695
  },
9109
9696
  openBrowser,
@@ -9122,7 +9709,7 @@ async function run(flags) {
9122
9709
  await submitSignedIn(cfg.cliToken, payload, flags, canSignIn);
9123
9710
  return;
9124
9711
  }
9125
- const healed = cfg?.anonKey ? await refreshCliToken(cfg.anonKey) : null;
9712
+ const healed = cfg?.refreshToken ? await refreshCliToken(cfg.refreshToken) : null;
9126
9713
  if (healed) {
9127
9714
  saveAuth(void 0, { cliToken: healed.token, handle: healed.handle });
9128
9715
  await submitSignedIn(healed.token, payload, flags, canSignIn);
@@ -9134,7 +9721,7 @@ async function run(flags) {
9134
9721
  console.log(pc2.yellow(" Sign in to put your usage on the leaderboard."));
9135
9722
  console.log(
9136
9723
  pc2.dim(
9137
- " 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."
9138
9725
  )
9139
9726
  );
9140
9727
  } else {
@@ -9146,8 +9733,8 @@ function sleep(ms) {
9146
9733
  }
9147
9734
  async function refreshCliTokenFromConfig() {
9148
9735
  const cfg = loadConfig();
9149
- if (!cfg?.anonKey) return null;
9150
- return refreshCliToken(cfg.anonKey);
9736
+ if (!cfg?.refreshToken) return null;
9737
+ return refreshCliToken(cfg.refreshToken);
9151
9738
  }
9152
9739
  async function ensureSignedIn() {
9153
9740
  const cfg = loadConfig();
@@ -9176,7 +9763,11 @@ async function ensureSignedIn() {
9176
9763
  continue;
9177
9764
  }
9178
9765
  if (res.status === "ok") {
9179
- 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
+ });
9180
9771
  console.log(pc2.green(` \u2713 Signed in as @${sanitizeServerText(res.handle)}.`));
9181
9772
  return { token: res.token, handle: res.handle };
9182
9773
  }
@@ -9188,24 +9779,21 @@ async function ensureSignedIn() {
9188
9779
  return null;
9189
9780
  }
9190
9781
  async function submitSignedIn(token, payload, flags, interactive) {
9191
- let activeToken = token;
9192
9782
  let result;
9193
9783
  try {
9194
- result = await submit(token, payload);
9784
+ result = await submitFromBoundDevice(token, payload);
9195
9785
  } catch (err) {
9196
9786
  if (err instanceof UnauthorizedError) {
9197
9787
  const healed = await refreshCliTokenFromConfig();
9198
9788
  if (healed) {
9199
9789
  saveAuth(void 0, { cliToken: healed.token, handle: healed.handle });
9200
- activeToken = healed.token;
9201
- result = await submit(healed.token, payload);
9790
+ result = await submitFromBoundDevice(healed.token, payload);
9202
9791
  } else {
9203
9792
  clearAuth();
9204
9793
  if (!interactive) return;
9205
9794
  const auth = await ensureSignedIn();
9206
9795
  if (!auth) return;
9207
- activeToken = auth.token;
9208
- result = await submit(auth.token, payload);
9796
+ result = await submitFromBoundDevice(auth.token, payload);
9209
9797
  }
9210
9798
  } else {
9211
9799
  throw err;
@@ -9215,14 +9803,6 @@ async function submitSignedIn(token, payload, flags, interactive) {
9215
9803
  recordSync();
9216
9804
  } catch {
9217
9805
  }
9218
- try {
9219
- const cfgNow = loadConfig();
9220
- if (!cfgNow?.deviceBoundAt) {
9221
- const key = ensureAnonKey();
9222
- if (await bindDeviceKey(activeToken, key)) recordDeviceBound();
9223
- }
9224
- } catch {
9225
- }
9226
9806
  const baseUrl = result.orgBoardUrl ?? result.boardUrl ?? result.profileUrl;
9227
9807
  if (!flags.quiet) {
9228
9808
  console.log(
@@ -9281,6 +9861,29 @@ async function submitSignedIn(token, payload, flags, interactive) {
9281
9861
  }
9282
9862
  afterSubmitChores(flags);
9283
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
+ }
9284
9887
  function afterSubmitChores(flags) {
9285
9888
  if (flags.quiet) {
9286
9889
  try {
@@ -9298,14 +9901,36 @@ function afterSubmitChores(flags) {
9298
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.")
9299
9902
  );
9300
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
+ }
9301
9922
  async function linkServerInstall(token) {
9302
9923
  if (!token) {
9303
- 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");
9304
9925
  }
9305
9926
  const anonKey = ensureAnonKey();
9306
9927
  const linked = await redeemServerInstall(token, anonKey);
9307
9928
  if (linked.cliToken) {
9308
- 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
+ });
9309
9934
  }
9310
9935
  const handle = sanitizeServerText(linked.handle);
9311
9936
  console.log(
@@ -9378,7 +10003,7 @@ async function runDaemon() {
9378
10003
  await run({ dryRun: false, noSubmit: false, local: false, quiet: true });
9379
10004
  if (!loadConfig()?.cliToken) {
9380
10005
  throw new Error(
9381
- "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)"
9382
10007
  );
9383
10008
  }
9384
10009
  }
@@ -9597,7 +10222,10 @@ async function main() {
9597
10222
  break;
9598
10223
  }
9599
10224
  case "link":
9600
- 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());
9601
10229
  break;
9602
10230
  case "daemon":
9603
10231
  await runDaemon();
@@ -9650,7 +10278,7 @@ function printHelp() {
9650
10278
  npx whoburnedmore --local build the dashboard on your machine and open it (offline)
9651
10279
  npx whoburnedmore --dry-run print exactly what would be sent, send nothing
9652
10280
  npx whoburnedmore --no-submit collect locally, send nothing (no dashboard)
9653
- 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)
9654
10282
  npx whoburnedmore daemon keep syncing in the foreground (VMs/containers with no cron)
9655
10283
  npx whoburnedmore private take yourself off the public leaderboard
9656
10284
  npx whoburnedmore public put yourself back on it