whoburnedmore 0.9.24 → 0.9.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +800 -728
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -422,6 +422,7 @@ function loadConfig(dir = defaultConfigDir()) {
422
422
  if (typeof parsed.deviceBoundAt === "number" && Number.isFinite(parsed.deviceBoundAt)) {
423
423
  config.deviceBoundAt = parsed.deviceBoundAt;
424
424
  }
425
+ if (parsed.autoSyncOptOut === true) config.autoSyncOptOut = true;
425
426
  return Object.keys(config).length > 0 ? config : null;
426
427
  } catch {
427
428
  return null;
@@ -448,6 +449,15 @@ function recordDeviceBound(dir = defaultConfigDir(), when = Date.now()) {
448
449
  const config = loadConfig(dir) ?? {};
449
450
  saveConfig(dir, { ...config, deviceBoundAt: when });
450
451
  }
452
+ function setAutoSyncOptOut(optOut, dir = defaultConfigDir()) {
453
+ const config = loadConfig(dir);
454
+ if (!optOut && !config?.autoSyncOptOut) return;
455
+ const { autoSyncOptOut: _o, ...rest } = config ?? {};
456
+ saveConfig(dir, optOut ? { ...rest, autoSyncOptOut: true } : rest);
457
+ }
458
+ function autoSyncOptedOut(dir = defaultConfigDir()) {
459
+ return loadConfig(dir)?.autoSyncOptOut === true;
460
+ }
451
461
  function saveAuth(dir = defaultConfigDir(), auth = { cliToken: "" }) {
452
462
  const config = loadConfig(dir) ?? {};
453
463
  saveConfig(dir, {
@@ -1153,8 +1163,7 @@ import { promisify } from "node:util";
1153
1163
 
1154
1164
  // src/attribution.ts
1155
1165
  import { readdirSync, statSync as statSync2 } from "node:fs";
1156
- import { homedir as homedir4 } from "node:os";
1157
- import { join as join6 } from "node:path";
1166
+ import { join as join7 } from "node:path";
1158
1167
 
1159
1168
  // src/native/claude.ts
1160
1169
  import { readdir } from "node:fs/promises";
@@ -6995,12 +7004,20 @@ function nativeCachePath(reader, env = process.env) {
6995
7004
  const dir = override || defaultConfigDir();
6996
7005
  return join4(dir, `native-cache-${reader}.json`);
6997
7006
  }
7007
+ function cacheTimeZone() {
7008
+ try {
7009
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || "unknown";
7010
+ } catch {
7011
+ return "unknown";
7012
+ }
7013
+ }
6998
7014
  async function loadCache(path, version, maxBytes) {
6999
7015
  try {
7000
7016
  const read = await readTextFileCapped(path, { maxBytes });
7001
7017
  if (!read.ok) return {};
7002
7018
  const parsed = JSON.parse(read.content);
7003
- if (parsed && parsed.v === version && parsed.files && typeof parsed.files === "object") {
7019
+ const sameZone = parsed?.tz === void 0 || parsed.tz === cacheTimeZone();
7020
+ if (parsed && parsed.v === version && sameZone && parsed.files && typeof parsed.files === "object") {
7004
7021
  return parsed.files;
7005
7022
  }
7006
7023
  } catch {
@@ -7014,7 +7031,7 @@ function saveCache(path, version, files, maxBytes) {
7014
7031
  mkdirSync3(dir, { recursive: true, mode: 448 });
7015
7032
  chmodSync2(dir, 448);
7016
7033
  tmp = `${path}.tmp-${process.pid}-${randomBytes3(8).toString("hex")}`;
7017
- const serialized = JSON.stringify({ v: version, files });
7034
+ const serialized = JSON.stringify({ v: version, tz: cacheTimeZone(), files });
7018
7035
  if (Buffer.byteLength(serialized, "utf8") > maxBytes) return;
7019
7036
  writeFileSync4(tmp, serialized, { encoding: "utf8", flag: "wx", mode: 384 });
7020
7037
  chmodSync2(tmp, 384);
@@ -7037,6 +7054,7 @@ async function readFilesWithCache(opts) {
7037
7054
  const cached = await loadCache(opts.cachePath, opts.version, maxCacheBytes);
7038
7055
  const fresh = {};
7039
7056
  const itemsByFile = [];
7057
+ const permissionDenied = [];
7040
7058
  let filesRead = 0;
7041
7059
  for (const f of opts.files) {
7042
7060
  let size;
@@ -7070,6 +7088,10 @@ async function readFilesWithCache(opts) {
7070
7088
  }
7071
7089
  if (opts.streamFile) {
7072
7090
  const streamed = await opts.streamFile(f).catch(() => ({ ok: false, reason: "unreadable" }));
7091
+ if (!streamed.ok && streamed.reason === "permission-denied") {
7092
+ permissionDenied.push(f);
7093
+ continue;
7094
+ }
7073
7095
  if (!streamed.ok) {
7074
7096
  const progress = { ...cached, ...fresh };
7075
7097
  delete progress[f];
@@ -7099,7 +7121,12 @@ async function readFilesWithCache(opts) {
7099
7121
  filesRead += 1;
7100
7122
  }
7101
7123
  saveCache(opts.cachePath, opts.version, fresh, maxCacheBytes);
7102
- return { itemsByFile, filesRead, timedOut: false };
7124
+ return {
7125
+ itemsByFile,
7126
+ filesRead,
7127
+ timedOut: false,
7128
+ ...permissionDenied.length > 0 ? { permissionDenied } : {}
7129
+ };
7103
7130
  }
7104
7131
 
7105
7132
  // src/native/jsonl-stream.ts
@@ -7146,7 +7173,9 @@ async function readJsonlStream(path, onLine, opts) {
7146
7173
  if (pendingBytes > 0) onLine(Buffer.concat(parts, pendingBytes).toString("utf8"));
7147
7174
  if (now() > opts.deadline) return { ok: false, reason: "timed-out" };
7148
7175
  return { ok: true };
7149
- } catch {
7176
+ } catch (err) {
7177
+ const code = err?.code;
7178
+ if (code === "EACCES" || code === "EPERM") return { ok: false, reason: "permission-denied" };
7150
7179
  return { ok: false, reason: "unreadable" };
7151
7180
  } finally {
7152
7181
  await handle?.close().catch(() => void 0);
@@ -7406,7 +7435,10 @@ async function collectClaudeNative(env = process.env, opts = {}) {
7406
7435
  return {
7407
7436
  entries: finalizeClaudeEntries(acc),
7408
7437
  found: true,
7409
- filesScanned: res.filesRead
7438
+ filesScanned: res.filesRead,
7439
+ // Skipped files mean this scan is partial: keep its rows, but flag it the
7440
+ // way a timeout is flagged so request fingerprints are never lowered.
7441
+ ...res.permissionDenied ? { timedOut: true, permissionDenied: res.permissionDenied } : {}
7410
7442
  };
7411
7443
  }
7412
7444
  async function collectClaudeRequests(env = process.env, opts = {}) {
@@ -7418,6 +7450,7 @@ async function collectClaudeRequests(env = process.env, opts = {}) {
7418
7450
  const deadline = now() + (opts.budgetMs ?? NATIVE_READ_BUDGET_MS);
7419
7451
  const acc = /* @__PURE__ */ new Map();
7420
7452
  const onLine = boundedLineAccumulator(acc, opts.maxRequestBytes);
7453
+ let skipped = false;
7421
7454
  for (const f of files) {
7422
7455
  if (now() > deadline) {
7423
7456
  return { requests: [], found: false, timedOut: true };
@@ -7427,187 +7460,175 @@ async function collectClaudeRequests(env = process.env, opts = {}) {
7427
7460
  deadline,
7428
7461
  now
7429
7462
  });
7463
+ if (!read.ok && read.reason === "permission-denied") {
7464
+ skipped = true;
7465
+ continue;
7466
+ }
7430
7467
  if (!read.ok) {
7431
7468
  return { requests: [], found: false, timedOut: true };
7432
7469
  }
7433
7470
  }
7434
- return { requests: [...acc.values()], found: true };
7471
+ return { requests: [...acc.values()], found: true, ...skipped ? { timedOut: true } : {} };
7435
7472
  }
7436
7473
 
7437
- // src/attribution.ts
7438
- var CLAUDE_PROJECTS = join6(homedir4(), ".claude", "projects");
7439
- var CODEX_SESSIONS = join6(homedir4(), ".codex", "sessions");
7440
- var MAX_FILES = 5e3;
7441
- var TIME_BUDGET_MS = NATIVE_READ_BUDGET_MS;
7442
- var ATTRIBUTION_CACHE_VERSION = 1;
7443
- var MAX_STATS = 300;
7444
- function createAccumulator() {
7445
- return {
7446
- tools: /* @__PURE__ */ new Map(),
7447
- skills: /* @__PURE__ */ new Map(),
7448
- agent: {
7449
- messageCount: 0,
7450
- subagentMessages: 0,
7451
- subagentTokens: 0,
7452
- totalTokens: 0,
7453
- userMessageCount: 0
7454
- },
7455
- sessionMessages: /* @__PURE__ */ new Map()
7456
- };
7457
- }
7458
- function createFileContext() {
7459
- return { toolNames: /* @__PURE__ */ new Map() };
7460
- }
7461
- function recordTokens(usage) {
7462
- if (!usage || typeof usage !== "object") return 0;
7463
- const u = usage;
7464
- const n = (v) => {
7465
- const x = Math.round(Number(v));
7466
- return Number.isFinite(x) && x > 0 ? x : 0;
7467
- };
7468
- return n(u.input_tokens) + n(u.output_tokens) + n(u.cache_creation_input_tokens) + n(u.cache_read_input_tokens);
7474
+ // src/native/codex.ts
7475
+ import { readdir as readdir2 } from "node:fs/promises";
7476
+ import { homedir as homedir4 } from "node:os";
7477
+ import { basename, join as join6, resolve as resolve2 } from "node:path";
7478
+ function num2(n) {
7479
+ const v = Math.round(Number(n));
7480
+ return Number.isFinite(v) && v > 0 ? v : 0;
7469
7481
  }
7470
- function hasHumanText(content) {
7471
- const isHuman = (t) => {
7472
- const s = t.trim();
7473
- return s.length > 0 && !s.startsWith("<system-reminder") && !s.startsWith("<command-") && !s.startsWith("Caveat:");
7482
+ function readTokenFields(payload) {
7483
+ const info = payload.info;
7484
+ const src = info?.total_token_usage ?? payload;
7485
+ const input = src.input_tokens;
7486
+ const output = src.output_tokens;
7487
+ if (input === void 0 && output === void 0) return null;
7488
+ return {
7489
+ input: num2(input),
7490
+ cached: num2(src.cached_input_tokens),
7491
+ output: num2(output)
7474
7492
  };
7475
- if (typeof content === "string") return isHuman(content);
7476
- if (!Array.isArray(content)) return false;
7477
- return content.some(
7478
- (b) => b !== null && typeof b === "object" && b.type === "text" && typeof b.text === "string" && isHuman(b.text)
7479
- );
7480
7493
  }
7481
- function processRecord(rec, acc, ctx) {
7482
- if (!rec || typeof rec !== "object") return;
7483
- const r = rec;
7484
- const recTokens = recordTokens(r.message?.usage);
7485
- if ((r.type === "user" || r.message?.role === "user") && r.isSidechain !== true && r.isMeta !== true && hasHumanText(r.message?.content)) {
7486
- acc.agent.userMessageCount += 1;
7487
- }
7488
- if (typeof r.attributionSkill === "string" && r.attributionSkill) {
7489
- const s = r.attributionSkill.slice(0, 128);
7490
- const sk = acc.skills.get(s) ?? { count: 0, tokens: 0 };
7491
- sk.count += 1;
7492
- sk.tokens += recTokens;
7493
- acc.skills.set(s, sk);
7494
- }
7495
- const content = r.message?.content;
7496
- if (!Array.isArray(content)) return;
7497
- const isAssistant = r.type === "assistant" || r.message?.role === "assistant";
7498
- const isUser = r.type === "user" || r.message?.role === "user";
7499
- if (isAssistant) {
7500
- const toolUses = [];
7501
- for (const block of content) {
7502
- if (block && typeof block === "object" && block.type === "tool_use" && typeof block.name === "string") {
7503
- const name = block.name.slice(0, 128);
7504
- if (!name) continue;
7505
- const id = block.id;
7506
- toolUses.push({ name, id: typeof id === "string" ? id : void 0 });
7507
- }
7508
- }
7509
- const perToolTokens = toolUses.length > 0 ? Math.floor(recTokens / toolUses.length) : 0;
7510
- for (const tu of toolUses) {
7511
- const t = acc.tools.get(tu.name) ?? { count: 0, errors: 0, tokens: 0 };
7512
- t.count += 1;
7513
- t.tokens += perToolTokens;
7514
- acc.tools.set(tu.name, t);
7515
- if (tu.id) ctx.toolNames.set(tu.id, tu.name);
7494
+ function inspectCodexRollout(lines) {
7495
+ let model = "unknown";
7496
+ let sawSessionMeta = false;
7497
+ let replayedRollout = false;
7498
+ const perDay = /* @__PURE__ */ new Map();
7499
+ let lastSeenDate = null;
7500
+ for (const raw of lines) {
7501
+ const trimmed = raw.trim();
7502
+ if (!trimmed) continue;
7503
+ let obj;
7504
+ try {
7505
+ obj = JSON.parse(trimmed);
7506
+ } catch {
7507
+ continue;
7516
7508
  }
7517
- const tokens = recTokens;
7518
- acc.agent.messageCount += 1;
7519
- acc.agent.totalTokens += tokens;
7520
- const sidechain = r.isSidechain === true;
7521
- if (sidechain) {
7522
- acc.agent.subagentMessages += 1;
7523
- acc.agent.subagentTokens += tokens;
7509
+ const payload = obj.payload;
7510
+ if (!payload || typeof payload !== "object") continue;
7511
+ const kind = obj.type;
7512
+ if (kind === "session_meta" && !sawSessionMeta) {
7513
+ sawSessionMeta = true;
7514
+ const source = payload.source;
7515
+ if (typeof payload.forked_from_id === "string" || typeof payload.parent_thread_id === "string" || source && typeof source === "object" && source.subagent !== void 0) {
7516
+ replayedRollout = true;
7517
+ }
7524
7518
  }
7525
- if (typeof r.sessionId === "string" && r.sessionId) {
7526
- acc.sessionMessages.set(
7527
- r.sessionId,
7528
- (acc.sessionMessages.get(r.sessionId) ?? 0) + 1
7529
- );
7519
+ if (kind === "session_meta" || kind === "turn_context") {
7520
+ if (typeof payload.model === "string" && payload.model) model = payload.model;
7530
7521
  }
7531
- } else if (isUser) {
7532
- for (const block of content) {
7533
- if (block && typeof block === "object" && block.type === "tool_result" && block.is_error === true) {
7534
- const id = block.tool_use_id;
7535
- const name = typeof id === "string" ? ctx.toolNames.get(id) : void 0;
7536
- if (name) {
7537
- const t = acc.tools.get(name) ?? { count: 0, errors: 0, tokens: 0 };
7538
- t.errors += 1;
7539
- acc.tools.set(name, t);
7540
- }
7522
+ if (payload.type === "token_count") {
7523
+ const fields = readTokenFields(payload);
7524
+ if (!fields) continue;
7525
+ const parsed = localUsageDate(Date.parse(String(obj.timestamp ?? "")));
7526
+ const day = parsed ?? lastSeenDate;
7527
+ if (!day) continue;
7528
+ lastSeenDate = day;
7529
+ const e = perDay.get(day);
7530
+ if (e) {
7531
+ e.cum = fields;
7532
+ e.turns += 1;
7533
+ } else {
7534
+ perDay.set(day, { cum: fields, turns: 1 });
7541
7535
  }
7542
7536
  }
7543
7537
  }
7544
- }
7545
- function toSkillStats(map) {
7546
- return [...map.entries()].map(([name, v]) => ({ name, count: v.count, tokens: v.tokens })).filter((s) => s.count > 0).sort((a, b) => b.tokens - a.tokens || b.count - a.count).slice(0, MAX_STATS).map((s) => s.tokens > 0 ? s : { name: s.name, count: s.count });
7547
- }
7548
- function toToolStats(map) {
7549
- return [...map.entries()].map(([name, v]) => ({ name, count: v.count, errors: v.errors, tokens: v.tokens })).filter((s) => s.count > 0).sort((a, b) => b.tokens - a.tokens || b.count - a.count).slice(0, MAX_STATS).map((s) => {
7550
- const base = { name: s.name, count: s.count };
7551
- if (s.errors > 0) base.errors = s.errors;
7552
- if (s.tokens > 0) base.tokens = s.tokens;
7553
- return base;
7554
- });
7555
- }
7556
- function accumulatorToResult(acc) {
7538
+ if (perDay.size === 0)
7539
+ return { sessions: [], replaySessions: [], replayCandidateDates: [] };
7540
+ const dates = [...perDay.keys()].sort();
7541
+ const out = [];
7542
+ let prev = { input: 0, cached: 0, output: 0 };
7543
+ for (const date of dates) {
7544
+ const { cum, turns } = perDay.get(date);
7545
+ const dInput = Math.max(0, cum.input - prev.input);
7546
+ const dCached = Math.max(0, cum.cached - prev.cached);
7547
+ const dOutput = Math.max(0, cum.output - prev.output);
7548
+ prev = cum;
7549
+ const cacheReadTokens = Math.min(dCached, dInput);
7550
+ const inputTokens = dInput - cacheReadTokens;
7551
+ const outputTokens = dOutput;
7552
+ if (inputTokens + outputTokens + cacheReadTokens === 0) continue;
7553
+ out.push({
7554
+ date,
7555
+ model,
7556
+ inputTokens,
7557
+ outputTokens,
7558
+ cacheCreationTokens: 0,
7559
+ cacheReadTokens,
7560
+ turnCount: turns
7561
+ });
7562
+ }
7557
7563
  return {
7558
- tools: toToolStats(acc.tools),
7559
- skills: toSkillStats(acc.skills),
7560
- agent: { ...acc.agent },
7561
- sessionMessages: acc.sessionMessages,
7562
- complete: true
7564
+ sessions: replayedRollout ? [] : out,
7565
+ replaySessions: replayedRollout ? out : [],
7566
+ replayCandidateDates: replayedRollout ? dates : []
7563
7567
  };
7564
7568
  }
7565
- function numTok(v) {
7566
- const x = Math.round(Number(v));
7567
- return Number.isFinite(x) && x > 0 ? x : 0;
7568
- }
7569
- function createCodexContext() {
7570
- return { pending: [] };
7569
+ function foldCodexSessions(acc, sessions) {
7570
+ for (const s of sessions) {
7571
+ const k = `${s.date}|${s.model}`;
7572
+ let b = acc.get(k);
7573
+ if (!b) {
7574
+ b = {
7575
+ date: s.date,
7576
+ model: s.model,
7577
+ inputTokens: 0,
7578
+ outputTokens: 0,
7579
+ cacheCreationTokens: 0,
7580
+ cacheReadTokens: 0,
7581
+ requestCount: 0
7582
+ };
7583
+ acc.set(k, b);
7584
+ }
7585
+ b.inputTokens += s.inputTokens;
7586
+ b.outputTokens += s.outputTokens;
7587
+ b.cacheCreationTokens += s.cacheCreationTokens;
7588
+ b.cacheReadTokens += s.cacheReadTokens;
7589
+ b.requestCount += s.turnCount;
7590
+ }
7571
7591
  }
7572
- function processCodexRecord(rec, acc, ctx) {
7573
- if (!rec || typeof rec !== "object") return;
7574
- const r = rec;
7575
- if (!r.payload || typeof r.payload !== "object") return;
7576
- const pl = r.payload;
7577
- const ptype = pl.type;
7578
- if (r.type === "session_meta" || r.type === "turn_context") return;
7579
- if (ptype === "function_call" || ptype === "custom_tool_call" || ptype === "local_shell_call") {
7580
- const raw = typeof pl.name === "string" ? pl.name : ptype === "local_shell_call" ? "local_shell" : "";
7581
- const name = raw.slice(0, 128);
7582
- if (!name) return;
7583
- const id = typeof pl.call_id === "string" ? pl.call_id : void 0;
7584
- ctx.pending.push({ name, id });
7585
- const t = acc.tools.get(name) ?? { count: 0, errors: 0, tokens: 0 };
7586
- t.count += 1;
7587
- acc.tools.set(name, t);
7588
- return;
7592
+ function finalizeCodexEntries(acc) {
7593
+ const entries = [];
7594
+ for (const b of acc.values()) {
7595
+ entries.push({
7596
+ date: b.date,
7597
+ tool: "codex",
7598
+ model: b.model,
7599
+ inputTokens: b.inputTokens,
7600
+ outputTokens: b.outputTokens,
7601
+ cacheCreationTokens: b.cacheCreationTokens,
7602
+ cacheReadTokens: b.cacheReadTokens,
7603
+ costUSD: estimateCostUSD(b.model, b),
7604
+ origin: "cli",
7605
+ verified: false,
7606
+ requestCount: b.requestCount
7607
+ });
7589
7608
  }
7590
- if (ptype === "token_count") {
7591
- const info = pl.info;
7592
- const last = info?.last_token_usage;
7593
- if (!last) return;
7594
- const inputTokens = numTok(last.input_tokens);
7595
- const cacheReadTokens = numTok(last.cached_input_tokens);
7596
- const outputTokens = numTok(last.output_tokens) + numTok(last.reasoning_output_tokens);
7597
- const tokens = numTok(last.total_tokens) || inputTokens + cacheReadTokens + outputTokens;
7598
- if (tokens <= 0) {
7599
- ctx.pending = [];
7600
- return;
7601
- }
7602
- acc.agent.messageCount += 1;
7603
- acc.agent.totalTokens += tokens;
7604
- const per = ctx.pending.length > 0 ? Math.floor(tokens / ctx.pending.length) : 0;
7605
- for (const tu of ctx.pending) {
7606
- const t = acc.tools.get(tu.name);
7607
- if (t) t.tokens += per;
7608
- }
7609
- ctx.pending = [];
7609
+ return entries;
7610
+ }
7611
+ function resolveCodexHome(env = process.env) {
7612
+ return env.CODEX_HOME && env.CODEX_HOME.trim() ? env.CODEX_HOME.trim() : join6(homedir4(), ".codex");
7613
+ }
7614
+ function resolveCodexSessionsDirs(env = process.env) {
7615
+ const home = resolve2(resolveCodexHome(env));
7616
+ return [join6(home, "sessions"), join6(home, "archived_sessions")];
7617
+ }
7618
+ async function listJsonl2(dir) {
7619
+ let dirents;
7620
+ try {
7621
+ dirents = await readdir2(dir, { withFileTypes: true });
7622
+ } catch {
7623
+ return [];
7610
7624
  }
7625
+ const out = [];
7626
+ for (const d of dirents) {
7627
+ const full = join6(dir, d.name);
7628
+ if (d.isDirectory()) out.push(...await listJsonl2(full));
7629
+ else if (d.isFile() && d.name.endsWith(".jsonl")) out.push(full);
7630
+ }
7631
+ return out;
7611
7632
  }
7612
7633
  function* splitLines2(content) {
7613
7634
  let start = 0;
@@ -7619,113 +7640,385 @@ function* splitLines2(content) {
7619
7640
  }
7620
7641
  if (start < content.length) yield content.slice(start);
7621
7642
  }
7622
- function serializeAcc(acc) {
7643
+ var NATIVE_READ_BUDGET_MS2 = 45e3;
7644
+ var CODEX_CACHE_VERSION = 7;
7645
+ function toCachedSession(s, kind = "usage") {
7646
+ return [
7647
+ kind,
7648
+ s.date,
7649
+ s.model,
7650
+ s.inputTokens,
7651
+ s.outputTokens,
7652
+ s.cacheCreationTokens,
7653
+ s.cacheReadTokens,
7654
+ s.turnCount
7655
+ ];
7656
+ }
7657
+ function fromCachedSession(t) {
7623
7658
  return {
7624
- tools: [...acc.tools].map(([n, v]) => [n, v.count, v.errors, v.tokens]),
7625
- skills: [...acc.skills].map(([n, v]) => [n, v.count, v.tokens]),
7626
- agent: [
7627
- acc.agent.messageCount,
7628
- acc.agent.subagentMessages,
7629
- acc.agent.subagentTokens,
7630
- acc.agent.totalTokens,
7631
- acc.agent.userMessageCount
7632
- ],
7633
- sessions: [...acc.sessionMessages].map(([s, c]) => [s, c])
7659
+ date: t[1],
7660
+ model: t[2],
7661
+ inputTokens: t[3],
7662
+ outputTokens: t[4],
7663
+ cacheCreationTokens: t[5],
7664
+ cacheReadTokens: t[6],
7665
+ turnCount: t[7]
7634
7666
  };
7635
7667
  }
7636
- function mergeAgg(acc, agg) {
7637
- for (const [n, count, errors, tokens] of agg.tools) {
7638
- const cur = acc.tools.get(n) ?? { count: 0, errors: 0, tokens: 0 };
7639
- cur.count += count;
7640
- cur.errors += errors;
7641
- cur.tokens += tokens;
7642
- acc.tools.set(n, cur);
7643
- }
7644
- for (const [n, count, tokens] of agg.skills) {
7645
- const cur = acc.skills.get(n) ?? { count: 0, tokens: 0 };
7646
- cur.count += count;
7647
- cur.tokens += tokens;
7648
- acc.skills.set(n, cur);
7668
+ async function collectCodexNative(env = process.env, opts = {}) {
7669
+ const dirs = resolveCodexSessionsDirs(env);
7670
+ const files = [];
7671
+ const primaryFiles = /* @__PURE__ */ new Set();
7672
+ const seenIdentities = /* @__PURE__ */ new Set();
7673
+ for (const dir of dirs) {
7674
+ for (const file of await listJsonl2(dir)) {
7675
+ const key = basename(file);
7676
+ files.push(file);
7677
+ if (!seenIdentities.has(key)) {
7678
+ seenIdentities.add(key);
7679
+ primaryFiles.add(file);
7680
+ }
7681
+ }
7649
7682
  }
7650
- acc.agent.messageCount += agg.agent[0];
7651
- acc.agent.subagentMessages += agg.agent[1];
7652
- acc.agent.subagentTokens += agg.agent[2];
7653
- acc.agent.totalTokens += agg.agent[3];
7654
- acc.agent.userMessageCount += agg.agent[4];
7655
- for (const [s, c] of agg.sessions) {
7656
- acc.sessionMessages.set(s, (acc.sessionMessages.get(s) ?? 0) + c);
7683
+ if (files.length === 0) return { entries: [], found: false, filesScanned: 0 };
7684
+ const now = opts.now ?? Date.now;
7685
+ const res = await readFilesWithCache({
7686
+ files,
7687
+ cachePath: opts.cachePath ?? nativeCachePath("codex", env),
7688
+ version: CODEX_CACHE_VERSION,
7689
+ parseFile: (content, path) => {
7690
+ const inspected = inspectCodexRollout(splitLines2(content));
7691
+ const primary = primaryFiles.has(path);
7692
+ return [
7693
+ // Do not pass toCachedSession directly to map: Array.map's numeric
7694
+ // index would be supplied as the optional `kind` argument.
7695
+ ...inspected.sessions.map(
7696
+ (session) => toCachedSession(session, primary ? "usage" : "duplicate")
7697
+ ),
7698
+ ...inspected.replaySessions.map(
7699
+ (session) => toCachedSession(
7700
+ session,
7701
+ primary ? "replay" : "replay-duplicate"
7702
+ )
7703
+ )
7704
+ ];
7705
+ },
7706
+ deadline: now() + (opts.budgetMs ?? NATIVE_READ_BUDGET_MS2),
7707
+ now
7708
+ });
7709
+ if (!res.itemsByFile) {
7710
+ return {
7711
+ entries: [],
7712
+ found: false,
7713
+ filesScanned: res.filesRead,
7714
+ timedOut: true
7715
+ };
7657
7716
  }
7658
- }
7659
- function parseClaudeAttrFile(content) {
7660
- const acc = createAccumulator();
7661
- const ctx = createFileContext();
7662
- for (const line of splitLines2(content)) {
7663
- if (!line) continue;
7664
- try {
7665
- processRecord(JSON.parse(line), acc, ctx);
7666
- } catch {
7667
- }
7717
+ const acc = /* @__PURE__ */ new Map();
7718
+ const legacyAcc = /* @__PURE__ */ new Map();
7719
+ const replayCandidateDates = /* @__PURE__ */ new Set();
7720
+ for (const items of res.itemsByFile) {
7721
+ const usage = items.filter((item) => item[0] === "usage").map(fromCachedSession);
7722
+ const legacy = items.map(fromCachedSession);
7723
+ const replay = items.filter(
7724
+ (item) => item[0] === "replay" || item[0] === "replay-duplicate"
7725
+ ).map(fromCachedSession);
7726
+ foldCodexSessions(acc, usage);
7727
+ foldCodexSessions(legacyAcc, legacy);
7728
+ for (const session of replay) replayCandidateDates.add(session.date);
7668
7729
  }
7669
- return [serializeAcc(acc)];
7730
+ return {
7731
+ entries: finalizeCodexEntries(acc),
7732
+ found: true,
7733
+ filesScanned: res.filesRead,
7734
+ replayCandidateDates: [...replayCandidateDates].sort(),
7735
+ legacyEntries: finalizeCodexEntries(legacyAcc)
7736
+ };
7670
7737
  }
7671
- function parseCodexAttrFile(content) {
7672
- const acc = createAccumulator();
7673
- const ctx = createCodexContext();
7674
- for (const line of splitLines2(content)) {
7675
- if (!line) continue;
7676
- try {
7677
- processCodexRecord(JSON.parse(line), acc, ctx);
7678
- } catch {
7679
- }
7680
- }
7681
- return [serializeAcc(acc)];
7738
+
7739
+ // src/attribution.ts
7740
+ var MAX_FILES = 5e3;
7741
+ var TIME_BUDGET_MS = NATIVE_READ_BUDGET_MS;
7742
+ var ATTRIBUTION_CACHE_VERSION = 1;
7743
+ var MAX_STATS = 300;
7744
+ function createAccumulator() {
7745
+ return {
7746
+ tools: /* @__PURE__ */ new Map(),
7747
+ skills: /* @__PURE__ */ new Map(),
7748
+ agent: {
7749
+ messageCount: 0,
7750
+ subagentMessages: 0,
7751
+ subagentTokens: 0,
7752
+ totalTokens: 0,
7753
+ userMessageCount: 0
7754
+ },
7755
+ sessionMessages: /* @__PURE__ */ new Map()
7756
+ };
7682
7757
  }
7683
- function claudeProjectDirs() {
7684
- const dirs = [CLAUDE_PROJECTS];
7685
- const cfg = process.env.CLAUDE_CONFIG_DIR;
7686
- if (cfg) dirs.push(join6(cfg, "projects"));
7687
- dirs.push(join6(homedir4(), ".config", "claude", "projects"));
7688
- return [...new Set(dirs)];
7758
+ function createFileContext() {
7759
+ return { toolNames: /* @__PURE__ */ new Map() };
7689
7760
  }
7690
- function listTranscripts(dir) {
7691
- const out = [];
7692
- const walk = (d) => {
7693
- if (out.length >= MAX_FILES) return;
7694
- let entries;
7695
- try {
7696
- entries = readdirSync(d, { withFileTypes: true });
7697
- } catch {
7698
- return;
7699
- }
7700
- for (const e of entries) {
7701
- if (out.length >= MAX_FILES) return;
7702
- const p = join6(d, e.name);
7703
- if (e.isDirectory()) walk(p);
7704
- else if (e.isFile() && e.name.endsWith(".jsonl")) {
7705
- try {
7706
- out.push({ path: p, mtime: statSync2(p).mtimeMs });
7707
- } catch {
7708
- }
7709
- }
7710
- }
7761
+ function recordTokens(usage) {
7762
+ if (!usage || typeof usage !== "object") return 0;
7763
+ const u = usage;
7764
+ const n = (v) => {
7765
+ const x = Math.round(Number(v));
7766
+ return Number.isFinite(x) && x > 0 ? x : 0;
7711
7767
  };
7712
- walk(dir);
7713
- return out.sort((a, b) => b.mtime - a.mtime).slice(0, MAX_FILES).map((f) => f.path);
7768
+ return n(u.input_tokens) + n(u.output_tokens) + n(u.cache_creation_input_tokens) + n(u.cache_read_input_tokens);
7714
7769
  }
7715
- async function collectAttribution(env = process.env) {
7716
- const acc = createAccumulator();
7717
- const deadline = Date.now() + TIME_BUDGET_MS;
7718
- let complete = true;
7719
- const mergeInto = (items) => {
7720
- if (!items) {
7721
- complete = false;
7722
- return;
7723
- }
7724
- for (const perFile of items) for (const agg of perFile) mergeAgg(acc, agg);
7770
+ function hasHumanText(content) {
7771
+ const isHuman = (t) => {
7772
+ const s = t.trim();
7773
+ return s.length > 0 && !s.startsWith("<system-reminder") && !s.startsWith("<command-") && !s.startsWith("Caveat:");
7725
7774
  };
7726
- try {
7727
- const claudeFiles = [];
7728
- for (const dir of claudeProjectDirs()) claudeFiles.push(...listTranscripts(dir));
7775
+ if (typeof content === "string") return isHuman(content);
7776
+ if (!Array.isArray(content)) return false;
7777
+ return content.some(
7778
+ (b) => b !== null && typeof b === "object" && b.type === "text" && typeof b.text === "string" && isHuman(b.text)
7779
+ );
7780
+ }
7781
+ function processRecord(rec, acc, ctx) {
7782
+ if (!rec || typeof rec !== "object") return;
7783
+ const r = rec;
7784
+ const recTokens = recordTokens(r.message?.usage);
7785
+ if ((r.type === "user" || r.message?.role === "user") && r.isSidechain !== true && r.isMeta !== true && hasHumanText(r.message?.content)) {
7786
+ acc.agent.userMessageCount += 1;
7787
+ }
7788
+ if (typeof r.attributionSkill === "string" && r.attributionSkill) {
7789
+ const s = r.attributionSkill.slice(0, 128);
7790
+ const sk = acc.skills.get(s) ?? { count: 0, tokens: 0 };
7791
+ sk.count += 1;
7792
+ sk.tokens += recTokens;
7793
+ acc.skills.set(s, sk);
7794
+ }
7795
+ const content = r.message?.content;
7796
+ if (!Array.isArray(content)) return;
7797
+ const isAssistant = r.type === "assistant" || r.message?.role === "assistant";
7798
+ const isUser = r.type === "user" || r.message?.role === "user";
7799
+ if (isAssistant) {
7800
+ const toolUses = [];
7801
+ for (const block of content) {
7802
+ if (block && typeof block === "object" && block.type === "tool_use" && typeof block.name === "string") {
7803
+ const name = block.name.slice(0, 128);
7804
+ if (!name) continue;
7805
+ const id = block.id;
7806
+ toolUses.push({ name, id: typeof id === "string" ? id : void 0 });
7807
+ }
7808
+ }
7809
+ const perToolTokens = toolUses.length > 0 ? Math.floor(recTokens / toolUses.length) : 0;
7810
+ for (const tu of toolUses) {
7811
+ const t = acc.tools.get(tu.name) ?? { count: 0, errors: 0, tokens: 0 };
7812
+ t.count += 1;
7813
+ t.tokens += perToolTokens;
7814
+ acc.tools.set(tu.name, t);
7815
+ if (tu.id) ctx.toolNames.set(tu.id, tu.name);
7816
+ }
7817
+ const tokens = recTokens;
7818
+ acc.agent.messageCount += 1;
7819
+ acc.agent.totalTokens += tokens;
7820
+ const sidechain = r.isSidechain === true;
7821
+ if (sidechain) {
7822
+ acc.agent.subagentMessages += 1;
7823
+ acc.agent.subagentTokens += tokens;
7824
+ }
7825
+ if (typeof r.sessionId === "string" && r.sessionId) {
7826
+ acc.sessionMessages.set(
7827
+ r.sessionId,
7828
+ (acc.sessionMessages.get(r.sessionId) ?? 0) + 1
7829
+ );
7830
+ }
7831
+ } else if (isUser) {
7832
+ for (const block of content) {
7833
+ if (block && typeof block === "object" && block.type === "tool_result" && block.is_error === true) {
7834
+ const id = block.tool_use_id;
7835
+ const name = typeof id === "string" ? ctx.toolNames.get(id) : void 0;
7836
+ if (name) {
7837
+ const t = acc.tools.get(name) ?? { count: 0, errors: 0, tokens: 0 };
7838
+ t.errors += 1;
7839
+ acc.tools.set(name, t);
7840
+ }
7841
+ }
7842
+ }
7843
+ }
7844
+ }
7845
+ function toSkillStats(map) {
7846
+ return [...map.entries()].map(([name, v]) => ({ name, count: v.count, tokens: v.tokens })).filter((s) => s.count > 0).sort((a, b) => b.tokens - a.tokens || b.count - a.count).slice(0, MAX_STATS).map((s) => s.tokens > 0 ? s : { name: s.name, count: s.count });
7847
+ }
7848
+ function toToolStats(map) {
7849
+ return [...map.entries()].map(([name, v]) => ({ name, count: v.count, errors: v.errors, tokens: v.tokens })).filter((s) => s.count > 0).sort((a, b) => b.tokens - a.tokens || b.count - a.count).slice(0, MAX_STATS).map((s) => {
7850
+ const base = { name: s.name, count: s.count };
7851
+ if (s.errors > 0) base.errors = s.errors;
7852
+ if (s.tokens > 0) base.tokens = s.tokens;
7853
+ return base;
7854
+ });
7855
+ }
7856
+ function accumulatorToResult(acc) {
7857
+ return {
7858
+ tools: toToolStats(acc.tools),
7859
+ skills: toSkillStats(acc.skills),
7860
+ agent: { ...acc.agent },
7861
+ sessionMessages: acc.sessionMessages,
7862
+ complete: true
7863
+ };
7864
+ }
7865
+ function numTok(v) {
7866
+ const x = Math.round(Number(v));
7867
+ return Number.isFinite(x) && x > 0 ? x : 0;
7868
+ }
7869
+ function createCodexContext() {
7870
+ return { pending: [] };
7871
+ }
7872
+ function processCodexRecord(rec, acc, ctx) {
7873
+ if (!rec || typeof rec !== "object") return;
7874
+ const r = rec;
7875
+ if (!r.payload || typeof r.payload !== "object") return;
7876
+ const pl = r.payload;
7877
+ const ptype = pl.type;
7878
+ if (r.type === "session_meta" || r.type === "turn_context") return;
7879
+ if (ptype === "function_call" || ptype === "custom_tool_call" || ptype === "local_shell_call") {
7880
+ const raw = typeof pl.name === "string" ? pl.name : ptype === "local_shell_call" ? "local_shell" : "";
7881
+ const name = raw.slice(0, 128);
7882
+ if (!name) return;
7883
+ const id = typeof pl.call_id === "string" ? pl.call_id : void 0;
7884
+ ctx.pending.push({ name, id });
7885
+ const t = acc.tools.get(name) ?? { count: 0, errors: 0, tokens: 0 };
7886
+ t.count += 1;
7887
+ acc.tools.set(name, t);
7888
+ return;
7889
+ }
7890
+ if (ptype === "token_count") {
7891
+ const info = pl.info;
7892
+ const last = info?.last_token_usage;
7893
+ if (!last) return;
7894
+ const inputTokens = numTok(last.input_tokens);
7895
+ const cacheReadTokens = numTok(last.cached_input_tokens);
7896
+ const outputTokens = numTok(last.output_tokens) + numTok(last.reasoning_output_tokens);
7897
+ const tokens = numTok(last.total_tokens) || inputTokens + cacheReadTokens + outputTokens;
7898
+ if (tokens <= 0) {
7899
+ ctx.pending = [];
7900
+ return;
7901
+ }
7902
+ acc.agent.messageCount += 1;
7903
+ acc.agent.totalTokens += tokens;
7904
+ const per = ctx.pending.length > 0 ? Math.floor(tokens / ctx.pending.length) : 0;
7905
+ for (const tu of ctx.pending) {
7906
+ const t = acc.tools.get(tu.name);
7907
+ if (t) t.tokens += per;
7908
+ }
7909
+ ctx.pending = [];
7910
+ }
7911
+ }
7912
+ function* splitLines3(content) {
7913
+ let start = 0;
7914
+ for (let i = 0; i < content.length; i++) {
7915
+ if (content[i] === "\n") {
7916
+ yield content.slice(start, i);
7917
+ start = i + 1;
7918
+ }
7919
+ }
7920
+ if (start < content.length) yield content.slice(start);
7921
+ }
7922
+ function serializeAcc(acc) {
7923
+ return {
7924
+ tools: [...acc.tools].map(([n, v]) => [n, v.count, v.errors, v.tokens]),
7925
+ skills: [...acc.skills].map(([n, v]) => [n, v.count, v.tokens]),
7926
+ agent: [
7927
+ acc.agent.messageCount,
7928
+ acc.agent.subagentMessages,
7929
+ acc.agent.subagentTokens,
7930
+ acc.agent.totalTokens,
7931
+ acc.agent.userMessageCount
7932
+ ],
7933
+ sessions: [...acc.sessionMessages].map(([s, c]) => [s, c])
7934
+ };
7935
+ }
7936
+ function mergeAgg(acc, agg) {
7937
+ for (const [n, count, errors, tokens] of agg.tools) {
7938
+ const cur = acc.tools.get(n) ?? { count: 0, errors: 0, tokens: 0 };
7939
+ cur.count += count;
7940
+ cur.errors += errors;
7941
+ cur.tokens += tokens;
7942
+ acc.tools.set(n, cur);
7943
+ }
7944
+ for (const [n, count, tokens] of agg.skills) {
7945
+ const cur = acc.skills.get(n) ?? { count: 0, tokens: 0 };
7946
+ cur.count += count;
7947
+ cur.tokens += tokens;
7948
+ acc.skills.set(n, cur);
7949
+ }
7950
+ acc.agent.messageCount += agg.agent[0];
7951
+ acc.agent.subagentMessages += agg.agent[1];
7952
+ acc.agent.subagentTokens += agg.agent[2];
7953
+ acc.agent.totalTokens += agg.agent[3];
7954
+ acc.agent.userMessageCount += agg.agent[4];
7955
+ for (const [s, c] of agg.sessions) {
7956
+ acc.sessionMessages.set(s, (acc.sessionMessages.get(s) ?? 0) + c);
7957
+ }
7958
+ }
7959
+ function parseClaudeAttrFile(content) {
7960
+ const acc = createAccumulator();
7961
+ const ctx = createFileContext();
7962
+ for (const line of splitLines3(content)) {
7963
+ if (!line) continue;
7964
+ try {
7965
+ processRecord(JSON.parse(line), acc, ctx);
7966
+ } catch {
7967
+ }
7968
+ }
7969
+ return [serializeAcc(acc)];
7970
+ }
7971
+ function parseCodexAttrFile(content) {
7972
+ const acc = createAccumulator();
7973
+ const ctx = createCodexContext();
7974
+ for (const line of splitLines3(content)) {
7975
+ if (!line) continue;
7976
+ try {
7977
+ processCodexRecord(JSON.parse(line), acc, ctx);
7978
+ } catch {
7979
+ }
7980
+ }
7981
+ return [serializeAcc(acc)];
7982
+ }
7983
+ function listTranscripts(dir) {
7984
+ const out = [];
7985
+ const walk = (d) => {
7986
+ if (out.length >= MAX_FILES) return;
7987
+ let entries;
7988
+ try {
7989
+ entries = readdirSync(d, { withFileTypes: true });
7990
+ } catch {
7991
+ return;
7992
+ }
7993
+ for (const e of entries) {
7994
+ if (out.length >= MAX_FILES) return;
7995
+ const p = join7(d, e.name);
7996
+ if (e.isDirectory()) walk(p);
7997
+ else if (e.isFile() && e.name.endsWith(".jsonl")) {
7998
+ try {
7999
+ out.push({ path: p, mtime: statSync2(p).mtimeMs });
8000
+ } catch {
8001
+ }
8002
+ }
8003
+ }
8004
+ };
8005
+ walk(dir);
8006
+ return out.sort((a, b) => b.mtime - a.mtime).slice(0, MAX_FILES).map((f) => f.path);
8007
+ }
8008
+ async function collectAttribution(env = process.env) {
8009
+ const acc = createAccumulator();
8010
+ const deadline = Date.now() + TIME_BUDGET_MS;
8011
+ let complete = true;
8012
+ const mergeInto = (items) => {
8013
+ if (!items) {
8014
+ complete = false;
8015
+ return;
8016
+ }
8017
+ for (const perFile of items) for (const agg of perFile) mergeAgg(acc, agg);
8018
+ };
8019
+ try {
8020
+ const claudeFiles = [];
8021
+ for (const dir of resolveClaudeProjectDirs(env)) claudeFiles.push(...listTranscripts(dir));
7729
8022
  if (claudeFiles.length > 0) {
7730
8023
  const res = await readFilesWithCache({
7731
8024
  files: claudeFiles,
@@ -7736,7 +8029,7 @@ async function collectAttribution(env = process.env) {
7736
8029
  });
7737
8030
  mergeInto(res.itemsByFile);
7738
8031
  }
7739
- const codexFiles = listTranscripts(CODEX_SESSIONS);
8032
+ const codexFiles = resolveCodexSessionsDirs(env).flatMap((dir) => listTranscripts(dir));
7740
8033
  if (codexFiles.length > 0) {
7741
8034
  const res = await readFilesWithCache({
7742
8035
  files: codexFiles,
@@ -7754,10 +8047,10 @@ async function collectAttribution(env = process.env) {
7754
8047
  }
7755
8048
 
7756
8049
  // src/continue.ts
7757
- import { readdir as readdir2 } from "node:fs/promises";
8050
+ import { readdir as readdir3 } from "node:fs/promises";
7758
8051
  import { homedir as homedir5 } from "node:os";
7759
- import { join as join7 } from "node:path";
7760
- function num2(n) {
8052
+ import { join as join8 } from "node:path";
8053
+ function num3(n) {
7761
8054
  const v = Math.round(Number(n));
7762
8055
  return Number.isFinite(v) && v > 0 ? v : 0;
7763
8056
  }
@@ -7785,8 +8078,8 @@ function mapContinueRecords(records) {
7785
8078
  if (ms === null) continue;
7786
8079
  const date = localUsageDate(ms);
7787
8080
  if (!date) continue;
7788
- const inputTokens = num2(rec.promptTokens ?? rec.prompt_tokens);
7789
- const outputTokens = num2(rec.generatedTokens ?? rec.generated_tokens);
8081
+ const inputTokens = num3(rec.promptTokens ?? rec.prompt_tokens);
8082
+ const outputTokens = num3(rec.generatedTokens ?? rec.generated_tokens);
7790
8083
  if (inputTokens + outputTokens === 0) continue;
7791
8084
  const key = `${date}|${model}`;
7792
8085
  let b = byKey.get(key);
@@ -7834,7 +8127,7 @@ function parseContinueJsonl(content) {
7834
8127
  }
7835
8128
  return mapContinueRecords(records);
7836
8129
  }
7837
- async function listJsonl2(root, opts) {
8130
+ async function listJsonl3(root, opts) {
7838
8131
  const queue = [{ dir: root, depth: 0 }];
7839
8132
  let queueIndex = 0;
7840
8133
  const files = [];
@@ -7845,7 +8138,7 @@ async function listJsonl2(root, opts) {
7845
8138
  const current = queue[queueIndex++];
7846
8139
  let dirents;
7847
8140
  try {
7848
- dirents = await readdir2(current.dir, { withFileTypes: true });
8141
+ dirents = await readdir3(current.dir, { withFileTypes: true });
7849
8142
  } catch {
7850
8143
  continue;
7851
8144
  }
@@ -7854,7 +8147,7 @@ async function listJsonl2(root, opts) {
7854
8147
  if (visitedEntries > maxEntries || opts.now() > opts.deadline) {
7855
8148
  return { files: [], aborted: true };
7856
8149
  }
7857
- const full = join7(current.dir, d.name);
8150
+ const full = join8(current.dir, d.name);
7858
8151
  if (d.isDirectory()) {
7859
8152
  if (current.depth >= 32) return { files: [], aborted: true };
7860
8153
  queue.push({ dir: full, depth: current.depth + 1 });
@@ -7869,10 +8162,10 @@ async function listJsonl2(root, opts) {
7869
8162
  var CONTINUE_CACHE_VERSION = 2;
7870
8163
  async function collectContinue(opts = {}) {
7871
8164
  const env = opts.env ?? process.env;
7872
- const home = opts.continueDir ?? join7(env.HOME || homedir5(), ".continue");
8165
+ const home = opts.continueDir ?? join8(env.HOME || homedir5(), ".continue");
7873
8166
  const now = opts.now ?? Date.now;
7874
8167
  const deadline = now() + (opts.budgetMs ?? NATIVE_READ_BUDGET_MS);
7875
- const discovery = await listJsonl2(join7(home, "dev_data"), {
8168
+ const discovery = await listJsonl3(join8(home, "dev_data"), {
7876
8169
  deadline,
7877
8170
  now,
7878
8171
  maxFiles: Math.max(1, opts.maxFiles ?? 1e4)
@@ -7936,14 +8229,14 @@ import { spawnSync as spawnSync4 } from "node:child_process";
7936
8229
  import { existsSync as existsSync4, realpathSync, statSync as statSync3 } from "node:fs";
7937
8230
  import { createRequire as createRequire3 } from "node:module";
7938
8231
  import { homedir as homedir6, platform as platform2 } from "node:os";
7939
- import { join as join9 } from "node:path";
8232
+ import { join as join10 } from "node:path";
7940
8233
 
7941
8234
  // src/tokscale.ts
7942
8235
  import { spawnSync as spawnSync3 } from "node:child_process";
7943
8236
  import { createRequire as createRequire2 } from "node:module";
7944
- import { dirname as dirname4, join as join8 } from "node:path";
8237
+ import { dirname as dirname4, join as join9 } from "node:path";
7945
8238
  var LOOKBACK_DAYS = 30;
7946
- function num3(n) {
8239
+ function num4(n) {
7947
8240
  const v = Math.round(Number(n));
7948
8241
  return Number.isFinite(v) && v > 0 ? v : 0;
7949
8242
  }
@@ -7956,10 +8249,10 @@ function mapTokscaleDay(date, json) {
7956
8249
  if (!Array.isArray(entries)) return [];
7957
8250
  const out = [];
7958
8251
  for (const e of entries) {
7959
- const inputTokens = num3(e.input);
7960
- const outputTokens = num3(e.output) + num3(e.reasoning);
7961
- const cacheCreationTokens = num3(e.cacheWrite);
7962
- const cacheReadTokens = num3(e.cacheRead);
8252
+ const inputTokens = num4(e.input);
8253
+ const outputTokens = num4(e.output) + num4(e.reasoning);
8254
+ const cacheCreationTokens = num4(e.cacheWrite);
8255
+ const cacheReadTokens = num4(e.cacheRead);
7963
8256
  const costUSD2 = numCost(e.cost);
7964
8257
  const total = inputTokens + outputTokens + cacheCreationTokens + cacheReadTokens;
7965
8258
  if (total === 0 && costUSD2 === 0) continue;
@@ -7985,7 +8278,7 @@ function resolveTokscaleBin() {
7985
8278
  const pkg = require4("tokscale/package.json");
7986
8279
  const rel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.tokscale ?? "";
7987
8280
  if (!rel) return null;
7988
- const binPath = join8(dirname4(pkgPath), rel);
8281
+ const binPath = join9(dirname4(pkgPath), rel);
7989
8282
  if (/\.(c|m)?js$/.test(binPath)) {
7990
8283
  return { cmd: process.execPath, prefixArgs: [binPath] };
7991
8284
  }
@@ -8046,7 +8339,7 @@ var MAX_CURSOR_PAGE_BYTES = 4 * 1024 * 1024;
8046
8339
  function cursorDbPath() {
8047
8340
  const home = homedir6();
8048
8341
  const os = platform2();
8049
- const p = os === "darwin" ? join9(home, "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb") : os === "win32" ? join9(process.env.APPDATA ?? join9(home, "AppData", "Roaming"), "Cursor", "User", "globalStorage", "state.vscdb") : join9(process.env.XDG_CONFIG_HOME ?? join9(home, ".config"), "Cursor", "User", "globalStorage", "state.vscdb");
8342
+ const p = os === "darwin" ? join10(home, "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb") : os === "win32" ? join10(process.env.APPDATA ?? join10(home, "AppData", "Roaming"), "Cursor", "User", "globalStorage", "state.vscdb") : join10(process.env.XDG_CONFIG_HOME ?? join10(home, ".config"), "Cursor", "User", "globalStorage", "state.vscdb");
8050
8343
  return existsSync4(p) ? p : null;
8051
8344
  }
8052
8345
  function trustedSqliteBinaries() {
@@ -8079,400 +8372,135 @@ function readCursorToken(db) {
8079
8372
  [db, "SELECT value FROM ItemTable WHERE key='cursorAuth/accessToken';"],
8080
8373
  { encoding: "utf8", windowsHide: true, timeout: 1e4 }
8081
8374
  );
8082
- const out = res.stdout?.trim();
8083
- if (res.status === 0 && out) return out;
8084
- } catch {
8085
- }
8086
- }
8087
- return null;
8088
- }
8089
- function cursorCookie(token) {
8090
- try {
8091
- const part = token.split(".")[1];
8092
- if (!part) return null;
8093
- const json = JSON.parse(
8094
- Buffer.from(part, "base64url").toString("utf8")
8095
- );
8096
- if (!json.sub) return null;
8097
- return `WorkosCursorSessionToken=${json.sub}%3A%3A${token}`;
8098
- } catch {
8099
- return null;
8100
- }
8101
- }
8102
- function num4(n) {
8103
- const v = Math.round(Number(n));
8104
- return Number.isFinite(v) && v > 0 ? v : 0;
8105
- }
8106
- function mapCursorEvents(events) {
8107
- const byDay = /* @__PURE__ */ new Map();
8108
- const byHour = /* @__PURE__ */ new Map();
8109
- for (const e of events) {
8110
- const tu = e.tokenUsage;
8111
- const ms = Number(e.timestamp);
8112
- if (!tu || !Number.isFinite(ms)) continue;
8113
- const d = new Date(ms);
8114
- const date = localUsageDate(ms);
8115
- if (!date) continue;
8116
- const model = e.model || "cursor";
8117
- const input = num4(tu.inputTokens);
8118
- const output = num4(tu.outputTokens);
8119
- const cacheWrite = num4(tu.cacheWriteTokens);
8120
- const cacheRead = num4(tu.cacheReadTokens);
8121
- const cost = Math.max(0, (Number(tu.totalCents) || 0) / 100);
8122
- const total = input + output + cacheWrite + cacheRead;
8123
- if (total === 0 && cost === 0) continue;
8124
- const key = `${date}|${model}`;
8125
- const day = byDay.get(key) ?? {
8126
- date,
8127
- tool: "cursor",
8128
- model,
8129
- inputTokens: 0,
8130
- outputTokens: 0,
8131
- cacheCreationTokens: 0,
8132
- cacheReadTokens: 0,
8133
- costUSD: 0,
8134
- origin: "cli",
8135
- verified: false
8136
- };
8137
- day.inputTokens += input;
8138
- day.outputTokens += output;
8139
- day.cacheCreationTokens += cacheWrite;
8140
- day.cacheReadTokens += cacheRead;
8141
- day.costUSD += cost;
8142
- byDay.set(key, day);
8143
- const hour = new Date(
8144
- Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), d.getUTCHours())
8145
- ).toISOString();
8146
- const blk = byHour.get(hour) ?? { startTime: hour, totalTokens: 0, costUSD: 0 };
8147
- blk.totalTokens += total;
8148
- blk.costUSD += cost;
8149
- byHour.set(hour, blk);
8150
- }
8151
- const entries = [...byDay.values()].map((e) => ({
8152
- ...e,
8153
- costUSD: Number(e.costUSD.toFixed(4))
8154
- }));
8155
- const blocks = [...byHour.values()].map((b) => ({
8156
- ...b,
8157
- costUSD: Number(b.costUSD.toFixed(4))
8158
- }));
8159
- return { entries, blocks };
8160
- }
8161
- async function fetchCursorEvents(cookie, maxPages = 30, pageSize = 500) {
8162
- const all = [];
8163
- for (let page = 1; page <= maxPages; page++) {
8164
- const res = await fetch(EVENTS_URL, {
8165
- method: "POST",
8166
- headers: {
8167
- "Content-Type": "application/json",
8168
- Origin: "https://cursor.com",
8169
- Cookie: cookie
8170
- },
8171
- body: JSON.stringify({ page, pageSize }),
8172
- signal: AbortSignal.timeout(2e4),
8173
- redirect: "error"
8174
- });
8175
- if (!res.ok) {
8176
- throw new Error(`cursor usage page ${page} failed (HTTP ${res.status})`);
8177
- }
8178
- const body = await readJsonResponseCapped(
8179
- res,
8180
- MAX_CURSOR_PAGE_BYTES
8181
- );
8182
- const batch = body.usageEventsDisplay ?? [];
8183
- all.push(...batch);
8184
- if (batch.length < pageSize) break;
8185
- }
8186
- return all;
8187
- }
8188
- async function collectCursor(opts) {
8189
- try {
8190
- const db = cursorDbPath();
8191
- const token = db ? readCursorToken(db) : null;
8192
- const cookie = token ? cursorCookie(token) : null;
8193
- if (cookie && !opts?.offline) {
8194
- const events = await fetchCursorEvents(cookie);
8195
- const { entries, blocks } = mapCursorEvents(events);
8196
- if (entries.length > 0) return { entries, blocks, found: true };
8197
- }
8198
- } catch {
8199
- }
8200
- try {
8201
- const entries = collectCursorViaTokscale();
8202
- if (entries.length > 0) return { entries, blocks: [], found: true };
8203
- } catch {
8204
- }
8205
- return { entries: [], blocks: [], found: false };
8206
- }
8207
-
8208
- // src/native/codex.ts
8209
- import { readdir as readdir3 } from "node:fs/promises";
8210
- import { homedir as homedir7 } from "node:os";
8211
- import { basename, join as join10, resolve as resolve2 } from "node:path";
8212
- function num5(n) {
8213
- const v = Math.round(Number(n));
8214
- return Number.isFinite(v) && v > 0 ? v : 0;
8215
- }
8216
- function readTokenFields(payload) {
8217
- const info = payload.info;
8218
- const src = info?.total_token_usage ?? payload;
8219
- const input = src.input_tokens;
8220
- const output = src.output_tokens;
8221
- if (input === void 0 && output === void 0) return null;
8222
- return {
8223
- input: num5(input),
8224
- cached: num5(src.cached_input_tokens),
8225
- output: num5(output)
8226
- };
8227
- }
8228
- function inspectCodexRollout(lines) {
8229
- let model = "unknown";
8230
- let sawSessionMeta = false;
8231
- let replayedRollout = false;
8232
- const perDay = /* @__PURE__ */ new Map();
8233
- let lastSeenDate = null;
8234
- for (const raw of lines) {
8235
- const trimmed = raw.trim();
8236
- if (!trimmed) continue;
8237
- let obj;
8238
- try {
8239
- obj = JSON.parse(trimmed);
8240
- } catch {
8241
- continue;
8242
- }
8243
- const payload = obj.payload;
8244
- if (!payload || typeof payload !== "object") continue;
8245
- const kind = obj.type;
8246
- if (kind === "session_meta" && !sawSessionMeta) {
8247
- sawSessionMeta = true;
8248
- const source = payload.source;
8249
- if (typeof payload.forked_from_id === "string" || typeof payload.parent_thread_id === "string" || source && typeof source === "object" && source.subagent !== void 0) {
8250
- replayedRollout = true;
8251
- }
8252
- }
8253
- if (kind === "session_meta" || kind === "turn_context") {
8254
- if (typeof payload.model === "string" && payload.model) model = payload.model;
8255
- }
8256
- if (payload.type === "token_count") {
8257
- const fields = readTokenFields(payload);
8258
- if (!fields) continue;
8259
- const parsed = localUsageDate(Date.parse(String(obj.timestamp ?? "")));
8260
- const day = parsed ?? lastSeenDate;
8261
- if (!day) continue;
8262
- lastSeenDate = day;
8263
- const e = perDay.get(day);
8264
- if (e) {
8265
- e.cum = fields;
8266
- e.turns += 1;
8267
- } else {
8268
- perDay.set(day, { cum: fields, turns: 1 });
8269
- }
8270
- }
8271
- }
8272
- if (perDay.size === 0)
8273
- return { sessions: [], replaySessions: [], replayCandidateDates: [] };
8274
- const dates = [...perDay.keys()].sort();
8275
- const out = [];
8276
- let prev = { input: 0, cached: 0, output: 0 };
8277
- for (const date of dates) {
8278
- const { cum, turns } = perDay.get(date);
8279
- const dInput = Math.max(0, cum.input - prev.input);
8280
- const dCached = Math.max(0, cum.cached - prev.cached);
8281
- const dOutput = Math.max(0, cum.output - prev.output);
8282
- prev = cum;
8283
- const cacheReadTokens = Math.min(dCached, dInput);
8284
- const inputTokens = dInput - cacheReadTokens;
8285
- const outputTokens = dOutput;
8286
- if (inputTokens + outputTokens + cacheReadTokens === 0) continue;
8287
- out.push({
8288
- date,
8289
- model,
8290
- inputTokens,
8291
- outputTokens,
8292
- cacheCreationTokens: 0,
8293
- cacheReadTokens,
8294
- turnCount: turns
8295
- });
8296
- }
8297
- return {
8298
- sessions: replayedRollout ? [] : out,
8299
- replaySessions: replayedRollout ? out : [],
8300
- replayCandidateDates: replayedRollout ? dates : []
8301
- };
8302
- }
8303
- function foldCodexSessions(acc, sessions) {
8304
- for (const s of sessions) {
8305
- const k = `${s.date}|${s.model}`;
8306
- let b = acc.get(k);
8307
- if (!b) {
8308
- b = {
8309
- date: s.date,
8310
- model: s.model,
8311
- inputTokens: 0,
8312
- outputTokens: 0,
8313
- cacheCreationTokens: 0,
8314
- cacheReadTokens: 0,
8315
- requestCount: 0
8316
- };
8317
- acc.set(k, b);
8318
- }
8319
- b.inputTokens += s.inputTokens;
8320
- b.outputTokens += s.outputTokens;
8321
- b.cacheCreationTokens += s.cacheCreationTokens;
8322
- b.cacheReadTokens += s.cacheReadTokens;
8323
- b.requestCount += s.turnCount;
8324
- }
8325
- }
8326
- function finalizeCodexEntries(acc) {
8327
- const entries = [];
8328
- for (const b of acc.values()) {
8329
- entries.push({
8330
- date: b.date,
8331
- tool: "codex",
8332
- model: b.model,
8333
- inputTokens: b.inputTokens,
8334
- outputTokens: b.outputTokens,
8335
- cacheCreationTokens: b.cacheCreationTokens,
8336
- cacheReadTokens: b.cacheReadTokens,
8337
- costUSD: estimateCostUSD(b.model, b),
8338
- origin: "cli",
8339
- verified: false,
8340
- requestCount: b.requestCount
8341
- });
8342
- }
8343
- return entries;
8344
- }
8345
- function resolveCodexHome(env = process.env) {
8346
- return env.CODEX_HOME && env.CODEX_HOME.trim() ? env.CODEX_HOME.trim() : join10(homedir7(), ".codex");
8347
- }
8348
- function resolveCodexSessionsDirs(env = process.env) {
8349
- const home = resolve2(resolveCodexHome(env));
8350
- return [join10(home, "sessions"), join10(home, "archived_sessions")];
8375
+ const out = res.stdout?.trim();
8376
+ if (res.status === 0 && out) return out;
8377
+ } catch {
8378
+ }
8379
+ }
8380
+ return null;
8351
8381
  }
8352
- async function listJsonl3(dir) {
8353
- let dirents;
8382
+ function cursorCookie(token) {
8354
8383
  try {
8355
- dirents = await readdir3(dir, { withFileTypes: true });
8384
+ const part = token.split(".")[1];
8385
+ if (!part) return null;
8386
+ const json = JSON.parse(
8387
+ Buffer.from(part, "base64url").toString("utf8")
8388
+ );
8389
+ if (!json.sub) return null;
8390
+ return `WorkosCursorSessionToken=${json.sub}%3A%3A${token}`;
8356
8391
  } catch {
8357
- return [];
8392
+ return null;
8358
8393
  }
8359
- const out = [];
8360
- for (const d of dirents) {
8361
- const full = join10(dir, d.name);
8362
- if (d.isDirectory()) out.push(...await listJsonl3(full));
8363
- else if (d.isFile() && d.name.endsWith(".jsonl")) out.push(full);
8394
+ }
8395
+ function num5(n) {
8396
+ const v = Math.round(Number(n));
8397
+ return Number.isFinite(v) && v > 0 ? v : 0;
8398
+ }
8399
+ function mapCursorEvents(events) {
8400
+ const byDay = /* @__PURE__ */ new Map();
8401
+ const byHour = /* @__PURE__ */ new Map();
8402
+ for (const e of events) {
8403
+ const tu = e.tokenUsage;
8404
+ const ms = Number(e.timestamp);
8405
+ if (!tu || !Number.isFinite(ms)) continue;
8406
+ const d = new Date(ms);
8407
+ const date = localUsageDate(ms);
8408
+ if (!date) continue;
8409
+ const model = e.model || "cursor";
8410
+ const input = num5(tu.inputTokens);
8411
+ const output = num5(tu.outputTokens);
8412
+ const cacheWrite = num5(tu.cacheWriteTokens);
8413
+ const cacheRead = num5(tu.cacheReadTokens);
8414
+ const cost = Math.max(0, (Number(tu.totalCents) || 0) / 100);
8415
+ const total = input + output + cacheWrite + cacheRead;
8416
+ if (total === 0 && cost === 0) continue;
8417
+ const key = `${date}|${model}`;
8418
+ const day = byDay.get(key) ?? {
8419
+ date,
8420
+ tool: "cursor",
8421
+ model,
8422
+ inputTokens: 0,
8423
+ outputTokens: 0,
8424
+ cacheCreationTokens: 0,
8425
+ cacheReadTokens: 0,
8426
+ costUSD: 0,
8427
+ origin: "cli",
8428
+ verified: false
8429
+ };
8430
+ day.inputTokens += input;
8431
+ day.outputTokens += output;
8432
+ day.cacheCreationTokens += cacheWrite;
8433
+ day.cacheReadTokens += cacheRead;
8434
+ day.costUSD += cost;
8435
+ byDay.set(key, day);
8436
+ const hour = new Date(
8437
+ Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), d.getUTCHours())
8438
+ ).toISOString();
8439
+ const blk = byHour.get(hour) ?? { startTime: hour, totalTokens: 0, costUSD: 0 };
8440
+ blk.totalTokens += total;
8441
+ blk.costUSD += cost;
8442
+ byHour.set(hour, blk);
8364
8443
  }
8365
- return out;
8444
+ const entries = [...byDay.values()].map((e) => ({
8445
+ ...e,
8446
+ costUSD: Number(e.costUSD.toFixed(4))
8447
+ }));
8448
+ const blocks = [...byHour.values()].map((b) => ({
8449
+ ...b,
8450
+ costUSD: Number(b.costUSD.toFixed(4))
8451
+ }));
8452
+ return { entries, blocks };
8366
8453
  }
8367
- function* splitLines3(content) {
8368
- let start = 0;
8369
- for (let i = 0; i < content.length; i++) {
8370
- if (content[i] === "\n") {
8371
- yield content.slice(start, i);
8372
- start = i + 1;
8454
+ async function fetchCursorEvents(cookie, maxPages = 30, pageSize = 500) {
8455
+ const all = [];
8456
+ for (let page = 1; page <= maxPages; page++) {
8457
+ const res = await fetch(EVENTS_URL, {
8458
+ method: "POST",
8459
+ headers: {
8460
+ "Content-Type": "application/json",
8461
+ Origin: "https://cursor.com",
8462
+ Cookie: cookie
8463
+ },
8464
+ body: JSON.stringify({ page, pageSize }),
8465
+ signal: AbortSignal.timeout(2e4),
8466
+ redirect: "error"
8467
+ });
8468
+ if (!res.ok) {
8469
+ throw new Error(`cursor usage page ${page} failed (HTTP ${res.status})`);
8373
8470
  }
8471
+ const body = await readJsonResponseCapped(
8472
+ res,
8473
+ MAX_CURSOR_PAGE_BYTES
8474
+ );
8475
+ const batch = body.usageEventsDisplay ?? [];
8476
+ all.push(...batch);
8477
+ if (batch.length < pageSize) break;
8374
8478
  }
8375
- if (start < content.length) yield content.slice(start);
8376
- }
8377
- var NATIVE_READ_BUDGET_MS2 = 45e3;
8378
- var CODEX_CACHE_VERSION = 7;
8379
- function toCachedSession(s, kind = "usage") {
8380
- return [
8381
- kind,
8382
- s.date,
8383
- s.model,
8384
- s.inputTokens,
8385
- s.outputTokens,
8386
- s.cacheCreationTokens,
8387
- s.cacheReadTokens,
8388
- s.turnCount
8389
- ];
8390
- }
8391
- function fromCachedSession(t) {
8392
- return {
8393
- date: t[1],
8394
- model: t[2],
8395
- inputTokens: t[3],
8396
- outputTokens: t[4],
8397
- cacheCreationTokens: t[5],
8398
- cacheReadTokens: t[6],
8399
- turnCount: t[7]
8400
- };
8479
+ return all;
8401
8480
  }
8402
- async function collectCodexNative(env = process.env, opts = {}) {
8403
- const dirs = resolveCodexSessionsDirs(env);
8404
- const files = [];
8405
- const primaryFiles = /* @__PURE__ */ new Set();
8406
- const seenIdentities = /* @__PURE__ */ new Set();
8407
- for (const dir of dirs) {
8408
- for (const file of await listJsonl3(dir)) {
8409
- const key = basename(file);
8410
- files.push(file);
8411
- if (!seenIdentities.has(key)) {
8412
- seenIdentities.add(key);
8413
- primaryFiles.add(file);
8414
- }
8481
+ async function collectCursor(opts) {
8482
+ try {
8483
+ const db = cursorDbPath();
8484
+ const token = db ? readCursorToken(db) : null;
8485
+ const cookie = token ? cursorCookie(token) : null;
8486
+ if (cookie && !opts?.offline) {
8487
+ const events = await fetchCursorEvents(cookie);
8488
+ const { entries, blocks } = mapCursorEvents(events);
8489
+ if (entries.length > 0) return { entries, blocks, found: true };
8415
8490
  }
8491
+ } catch {
8416
8492
  }
8417
- if (files.length === 0) return { entries: [], found: false, filesScanned: 0 };
8418
- const now = opts.now ?? Date.now;
8419
- const res = await readFilesWithCache({
8420
- files,
8421
- cachePath: opts.cachePath ?? nativeCachePath("codex", env),
8422
- version: CODEX_CACHE_VERSION,
8423
- parseFile: (content, path) => {
8424
- const inspected = inspectCodexRollout(splitLines3(content));
8425
- const primary = primaryFiles.has(path);
8426
- return [
8427
- // Do not pass toCachedSession directly to map: Array.map's numeric
8428
- // index would be supplied as the optional `kind` argument.
8429
- ...inspected.sessions.map(
8430
- (session) => toCachedSession(session, primary ? "usage" : "duplicate")
8431
- ),
8432
- ...inspected.replaySessions.map(
8433
- (session) => toCachedSession(
8434
- session,
8435
- primary ? "replay" : "replay-duplicate"
8436
- )
8437
- )
8438
- ];
8439
- },
8440
- deadline: now() + (opts.budgetMs ?? NATIVE_READ_BUDGET_MS2),
8441
- now
8442
- });
8443
- if (!res.itemsByFile) {
8444
- return {
8445
- entries: [],
8446
- found: false,
8447
- filesScanned: res.filesRead,
8448
- timedOut: true
8449
- };
8450
- }
8451
- const acc = /* @__PURE__ */ new Map();
8452
- const legacyAcc = /* @__PURE__ */ new Map();
8453
- const replayCandidateDates = /* @__PURE__ */ new Set();
8454
- for (const items of res.itemsByFile) {
8455
- const usage = items.filter((item) => item[0] === "usage").map(fromCachedSession);
8456
- const legacy = items.map(fromCachedSession);
8457
- const replay = items.filter(
8458
- (item) => item[0] === "replay" || item[0] === "replay-duplicate"
8459
- ).map(fromCachedSession);
8460
- foldCodexSessions(acc, usage);
8461
- foldCodexSessions(legacyAcc, legacy);
8462
- for (const session of replay) replayCandidateDates.add(session.date);
8493
+ try {
8494
+ const entries = collectCursorViaTokscale();
8495
+ if (entries.length > 0) return { entries, blocks: [], found: true };
8496
+ } catch {
8463
8497
  }
8464
- return {
8465
- entries: finalizeCodexEntries(acc),
8466
- found: true,
8467
- filesScanned: res.filesRead,
8468
- replayCandidateDates: [...replayCandidateDates].sort(),
8469
- legacyEntries: finalizeCodexEntries(legacyAcc)
8470
- };
8498
+ return { entries: [], blocks: [], found: false };
8471
8499
  }
8472
8500
 
8473
8501
  // src/native/vscode-agents.ts
8474
8502
  import { readdir as readdir4 } from "node:fs/promises";
8475
- import { homedir as homedir8, platform as platform3 } from "node:os";
8503
+ import { homedir as homedir7, platform as platform3 } from "node:os";
8476
8504
  import { join as join11 } from "node:path";
8477
8505
  var VSCODE_AGENTS = [
8478
8506
  { tool: "cline", extIds: ["saoudrizwan.claude-dev"] },
@@ -8586,7 +8614,7 @@ function parseVscodeAgentMessages(tool, raw) {
8586
8614
  }
8587
8615
  function vscodeGlobalStorageRoots(env = process.env) {
8588
8616
  const apps = ["Code", "Code - Insiders", "Cursor", "VSCodium", "Windsurf"];
8589
- const home = env.HOME || homedir8();
8617
+ const home = env.HOME || homedir7();
8590
8618
  const os = platform3();
8591
8619
  const base = (app) => os === "darwin" ? join11(home, "Library", "Application Support", app, "User", "globalStorage") : os === "win32" ? join11(env.APPDATA ?? join11(home, "AppData", "Roaming"), app, "User", "globalStorage") : join11(env.XDG_CONFIG_HOME ?? join11(home, ".config"), app, "User", "globalStorage");
8592
8620
  return apps.map(base);
@@ -8969,7 +8997,12 @@ var SOURCES = [
8969
8997
  "openclaw",
8970
8998
  "hermes",
8971
8999
  "pi",
8972
- "codebuff"
9000
+ "codebuff",
9001
+ // Read by ccusage >= 20.0.29 (Antigravity's conversations/*.db ledgers,
9002
+ // Grok Build CLI's sessions/**/updates.jsonl, ZCode's cli/db/db.sqlite).
9003
+ "antigravity",
9004
+ "grok",
9005
+ "zcode"
8973
9006
  ];
8974
9007
  function norm(n) {
8975
9008
  const v = Math.round(Number(n));
@@ -8979,6 +9012,7 @@ function normCost(n) {
8979
9012
  const v = Number(n);
8980
9013
  return Number.isFinite(v) && v > 0 ? v : 0;
8981
9014
  }
9015
+ var PRICE_TABLE_COST_SOURCES = /* @__PURE__ */ new Set(["claude", "codex"]);
8982
9016
  function mapCcusageDaily(tool, json, now = Date.now()) {
8983
9017
  const daily = json?.daily;
8984
9018
  if (!Array.isArray(daily)) return [];
@@ -9037,7 +9071,8 @@ function mapCcusageDaily(tool, json, now = Date.now()) {
9037
9071
  for (const c of candidates) {
9038
9072
  const tokens = c.inputTokens + c.outputTokens + c.cacheCreationTokens + c.cacheReadTokens;
9039
9073
  if (tokens === 0 && c.costUSD === 0) continue;
9040
- entries.push({ date, tool, ...c, origin: "cli", verified: false });
9074
+ const costUSD2 = c.costUSD === 0 && PRICE_TABLE_COST_SOURCES.has(tool) ? estimateCostUSD(c.model, c) : c.costUSD;
9075
+ entries.push({ date, tool, ...c, costUSD: costUSD2, origin: "cli", verified: false });
9041
9076
  }
9042
9077
  }
9043
9078
  return entries;
@@ -9325,13 +9360,19 @@ async function collectAll(onProgress, opts) {
9325
9360
  tick();
9326
9361
  return a;
9327
9362
  });
9363
+ const bailed = () => ({
9364
+ entries: [],
9365
+ found: false,
9366
+ filesScanned: 0,
9367
+ timedOut: true
9368
+ });
9328
9369
  const vscodeTasks = VSCODE_AGENTS.map(
9329
- (a) => collectVscodeAgent({ tool: a.tool, extIds: a.extIds }).then((r) => {
9370
+ (a) => collectVscodeAgent({ tool: a.tool, extIds: a.extIds }).catch(bailed).then((r) => {
9330
9371
  tick();
9331
9372
  return { tool: a.tool, result: r };
9332
9373
  })
9333
9374
  );
9334
- const continueTask = collectContinue().then((r) => {
9375
+ const continueTask = collectContinue().catch(bailed).then((r) => {
9335
9376
  tick();
9336
9377
  return r;
9337
9378
  });
@@ -9455,26 +9496,30 @@ async function collectAll(onProgress, opts) {
9455
9496
  tools: sanitizeToolStats(tools),
9456
9497
  skills: sanitizeSkillStats(skills),
9457
9498
  agent: sanitizeAgentStat(reconciled.agent),
9458
- attributionComplete: complete
9499
+ attributionComplete: complete,
9500
+ ...nativeClaude.permissionDenied ? { unreadableClaudeFiles: nativeClaude.permissionDenied } : {}
9459
9501
  };
9460
9502
  }
9461
9503
 
9462
9504
  // src/antigravity.ts
9463
9505
  import { existsSync as existsSync5 } from "node:fs";
9464
- import { homedir as homedir9 } from "node:os";
9506
+ import { homedir as homedir8 } from "node:os";
9465
9507
  import { join as join15 } from "node:path";
9466
- function antigravityDataDir(env = process.env) {
9467
- return join15(env.HOME || homedir9(), ".gemini", "antigravity");
9508
+ function antigravityDataDirs(env = process.env) {
9509
+ const gemini = join15(env.HOME || homedir8(), ".gemini");
9510
+ return ["antigravity", "antigravity-cli", "antigravity-ide"].map(
9511
+ (name) => join15(gemini, name)
9512
+ );
9468
9513
  }
9469
9514
  function detectAntigravity(env = process.env) {
9470
- return existsSync5(antigravityDataDir(env));
9515
+ return antigravityDataDirs(env).some((dir) => existsSync5(dir));
9471
9516
  }
9472
9517
  function antigravityNoticeLines() {
9473
9518
  return [
9474
- " \u2022 Google Antigravity detected \u2014 but its usage can't be counted.",
9475
- " Antigravity stores no readable local usage: its conversation logs are",
9476
- " encrypted and it bills flat-rate, so there are no per-request token",
9477
- " counts on disk to tally. It's the one major agent we can't put on the board."
9519
+ " \u2022 Google Antigravity detected. Its usage is read from the local",
9520
+ " conversation databases (~/.gemini/antigravity*/conversations/*.db).",
9521
+ " Older Antigravity versions kept only encrypted logs with no token",
9522
+ " counts; that history can't be counted. Update Antigravity if none shows."
9478
9523
  ];
9479
9524
  }
9480
9525
 
@@ -9512,7 +9557,7 @@ function buildStatusReport(s) {
9512
9557
  lines.push(" whoburnedmore \u2014 background sync status");
9513
9558
  lines.push("");
9514
9559
  lines.push(
9515
- s.installed ? ` \u2022 Background agent: installed${s.loaded ? " and loaded" : " but NOT loaded with the scheduler"}` : " \u2022 Background agent: NOT installed \u2014 run `npx whoburnedmore` to set it up"
9560
+ s.installed ? ` \u2022 Background agent: installed${s.loaded ? " and loaded" : " but NOT loaded with the scheduler"}` : s.optedOut ? " \u2022 Background agent: off (turned off with `uninstall-sync`) \u2014 run `npx whoburnedmore install-sync` to turn it back on" : " \u2022 Background agent: NOT installed \u2014 run `npx whoburnedmore` to set it up"
9516
9561
  );
9517
9562
  if (s.installed && s.drift !== "ok") {
9518
9563
  lines.push(
@@ -9556,7 +9601,8 @@ function agentStatusReport(now = Date.now()) {
9556
9601
  now,
9557
9602
  nodePath,
9558
9603
  nodePathStable: !nodePath.includes("/Cellar/"),
9559
- logPath: syncLogPath()
9604
+ logPath: syncLogPath(),
9605
+ optedOut: cfg?.autoSyncOptOut === true
9560
9606
  });
9561
9607
  }
9562
9608
 
@@ -9810,7 +9856,7 @@ ${connectCta}
9810
9856
 
9811
9857
  <div class="foot">
9812
9858
  Re-run <code>npx whoburnedmore --local</code> to refresh this page.<br>
9813
- ${connect ? "Use \u201CConnect your account\u201D above to save it to whoburnedmore.com and join the leaderboard." : "Run <code>npx whoburnedmore</code> (no flag) to get a shareable dashboard at whoburnedmore.com \u2014 no sign-in."}
9859
+ ${connect ? "Run the command above to sign in and put this burn on whoburnedmore.com." : "Run <code>npx whoburnedmore</code> to sign in and publish a shareable dashboard."}
9814
9860
  </div>
9815
9861
  </div>
9816
9862
  </body>
@@ -9830,6 +9876,10 @@ async function publishLocal(payload, deps) {
9830
9876
  return false;
9831
9877
  }
9832
9878
  const res = await deps.submit(auth.token, payload);
9879
+ if (!res) {
9880
+ deps.log(" Your sign-in expired and wasn't renewed \u2014 kept local. Run `npx whoburnedmore` to sign in again.");
9881
+ return false;
9882
+ }
9833
9883
  deps.log(` Published \u2014 you're on the board: ${sanitizeServerText(res.profileUrl)}`);
9834
9884
  if (isTrustedWebUrl(res.profileUrl)) {
9835
9885
  deps.openBrowser(res.profileUrl);
@@ -9907,12 +9957,13 @@ function openBrowser(url) {
9907
9957
  const [cmd, args] = os === "darwin" ? ["open", [url]] : os === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
9908
9958
  spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
9909
9959
  }
9910
- async function confirm(question) {
9960
+ async function confirm(question, defaultYes = true) {
9911
9961
  if (!process.stdin.isTTY) return false;
9912
9962
  const rl = createInterface({ input: process.stdin, output: process.stdout });
9913
- const answer = (await rl.question(`${question} ${pc2.dim("[Y/n]")} `)).trim();
9963
+ const hint = defaultYes ? "[Y/n]" : "[y/N]";
9964
+ const answer = (await rl.question(`${question} ${pc2.dim(hint)} `)).trim();
9914
9965
  rl.close();
9915
- return answer === "" || /^y(es)?$/i.test(answer);
9966
+ return answer === "" ? defaultYes : /^y(es)?$/i.test(answer);
9916
9967
  }
9917
9968
  function showLocalDashboard(payload) {
9918
9969
  const dir = defaultConfigDir();
@@ -9924,7 +9975,7 @@ function showLocalDashboard(payload) {
9924
9975
  console.log();
9925
9976
  console.log(` Local dashboard: ${pc2.cyan(fileUrl)}`);
9926
9977
  console.log(pc2.dim(" Re-run `npx whoburnedmore --local` to refresh it. Nothing left your machine."));
9927
- openBrowser(fileUrl);
9978
+ if (process.stdout.isTTY) openBrowser(fileUrl);
9928
9979
  }
9929
9980
  async function run(flags) {
9930
9981
  if (!flags.quiet) {
@@ -9948,6 +9999,18 @@ async function run(flags) {
9948
9999
  } finally {
9949
10000
  progress.stop();
9950
10001
  }
10002
+ const unreadable = collected.unreadableClaudeFiles ?? [];
10003
+ if (unreadable.length > 0 && !flags.quiet) {
10004
+ console.log(
10005
+ pc2.yellow(
10006
+ ` \u26A0 Skipped ${unreadable.length} Claude Code transcript${unreadable.length === 1 ? "" : "s"} this account can't read (permission denied), e.g.`
10007
+ )
10008
+ );
10009
+ console.log(pc2.dim(` ${sanitizeServerText(unreadable[0])}`));
10010
+ console.log(
10011
+ pc2.dim(" Their usage isn't counted until you fix the file's owner or permissions (often left by `sudo claude`).")
10012
+ );
10013
+ }
9951
10014
  const {
9952
10015
  entries,
9953
10016
  codexReplayTombstoneDates: codexReplayTombstoneDates2,
@@ -9994,9 +10057,12 @@ async function run(flags) {
9994
10057
  if (!flags.quiet && process.stdin.isTTY) {
9995
10058
  await publishLocal(payload, {
9996
10059
  confirm,
9997
- signIn: ensureSignedIn,
10060
+ // Stored token, then a silent refresh, then device sign-in — the same
10061
+ // order as every other signed-in command.
10062
+ signIn: () => requireSignedIn("--local"),
9998
10063
  submit: async (token, p) => {
9999
- const result = await submitFromBoundDevice(token, p);
10064
+ const result = await submitWithAuthRecovery(token, p, true);
10065
+ if (!result) return null;
10000
10066
  try {
10001
10067
  recordSync();
10002
10068
  } catch {
@@ -10088,27 +10154,26 @@ async function ensureSignedIn() {
10088
10154
  );
10089
10155
  return null;
10090
10156
  }
10091
- async function submitSignedIn(token, payload, flags, interactive) {
10092
- let result;
10157
+ async function submitWithAuthRecovery(token, payload, interactive) {
10093
10158
  try {
10094
- result = await submitFromBoundDevice(token, payload);
10159
+ return await submitFromBoundDevice(token, payload);
10095
10160
  } catch (err) {
10096
- if (err instanceof UnauthorizedError) {
10097
- const healed = await refreshCliTokenFromConfig();
10098
- if (healed) {
10099
- saveAuth(void 0, { cliToken: healed.token, handle: healed.handle });
10100
- result = await submitFromBoundDevice(healed.token, payload);
10101
- } else {
10102
- clearAuth();
10103
- if (!interactive) return;
10104
- const auth = await ensureSignedIn();
10105
- if (!auth) return;
10106
- result = await submitFromBoundDevice(auth.token, payload);
10107
- }
10108
- } else {
10109
- throw err;
10110
- }
10161
+ if (!(err instanceof UnauthorizedError)) throw err;
10162
+ const healed = await refreshCliTokenFromConfig();
10163
+ if (healed) {
10164
+ saveAuth(void 0, { cliToken: healed.token, handle: healed.handle });
10165
+ return await submitFromBoundDevice(healed.token, payload);
10166
+ }
10167
+ clearAuth();
10168
+ if (!interactive) return null;
10169
+ const auth = await ensureSignedIn();
10170
+ if (!auth) return null;
10171
+ return await submitFromBoundDevice(auth.token, payload);
10111
10172
  }
10173
+ }
10174
+ async function submitSignedIn(token, payload, flags, interactive) {
10175
+ const result = await submitWithAuthRecovery(token, payload, interactive);
10176
+ if (!result) return;
10112
10177
  try {
10113
10178
  recordSync();
10114
10179
  } catch {
@@ -10130,7 +10195,7 @@ async function submitSignedIn(token, payload, flags, interactive) {
10130
10195
  " Verify your usage to get back on \u2014 this sends a detailed per-request breakdown (timestamps + token counts, never your prompts or code)."
10131
10196
  )
10132
10197
  );
10133
- if (process.stdin.isTTY && await confirm(" Verify now?")) {
10198
+ if (process.stdin.isTTY && await confirm(" Verify now?", false)) {
10134
10199
  await runVerify();
10135
10200
  afterSubmitChores(flags);
10136
10201
  return;
@@ -10156,8 +10221,10 @@ async function submitSignedIn(token, payload, flags, interactive) {
10156
10221
  );
10157
10222
  }
10158
10223
  if (isTrustedWebUrl(baseUrl)) {
10159
- console.log(pc2.dim(" Opening your dashboard in your browser\u2026"));
10160
- openBrowser(baseUrl);
10224
+ if (process.stdout.isTTY) {
10225
+ console.log(pc2.dim(" Opening your dashboard in your browser\u2026"));
10226
+ openBrowser(baseUrl);
10227
+ }
10161
10228
  } else {
10162
10229
  console.log(
10163
10230
  pc2.dim(" The server returned an unexpected dashboard address, so it was NOT auto-opened. Open it yourself only if you trust it:")
@@ -10202,13 +10269,16 @@ function afterSubmitChores(flags) {
10202
10269
  }
10203
10270
  return;
10204
10271
  }
10205
- try {
10206
- reconcileAutoSync();
10207
- } catch {
10272
+ const optedOut = autoSyncOptedOut();
10273
+ if (!optedOut) {
10274
+ try {
10275
+ reconcileAutoSync();
10276
+ } catch {
10277
+ }
10208
10278
  }
10209
10279
  console.log();
10210
10280
  console.log(
10211
- 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.")
10281
+ optedOut ? pc2.dim(" Background sync is off. Re-run anytime to update your page, or `npx whoburnedmore install-sync` to turn it back on.") : 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.")
10212
10282
  );
10213
10283
  }
10214
10284
  async function readServerInstallToken() {
@@ -10256,6 +10326,7 @@ async function linkServerInstall(token) {
10256
10326
  quiet: true
10257
10327
  });
10258
10328
  try {
10329
+ setAutoSyncOptOut(false);
10259
10330
  const action = reconcileAutoSync();
10260
10331
  if (action === "installed") {
10261
10332
  console.log(pc2.dim(" Background sync installed; this machine will refresh every 15 min."));
@@ -10330,21 +10401,8 @@ async function runVerify() {
10330
10401
  " This sends a DETAILED per-request breakdown \u2014 timestamps and token counts, still never your prompts, code, or file names \u2014 so we can confirm your usage is real. It's deleted after review."
10331
10402
  )
10332
10403
  );
10333
- const cfg = loadConfig();
10334
- let token = cfg?.cliToken;
10335
- if (!token) {
10336
- if (!process.stdout.isTTY) {
10337
- console.log(
10338
- pc2.yellow(
10339
- " Sign in first \u2014 run `npx whoburnedmore verify` in an interactive terminal."
10340
- )
10341
- );
10342
- return;
10343
- }
10344
- const auth = await ensureSignedIn();
10345
- if (!auth) return;
10346
- token = auth.token;
10347
- }
10404
+ const auth = await requireSignedIn("verify");
10405
+ if (!auth) return;
10348
10406
  if (process.stdin.isTTY) {
10349
10407
  const ok = await confirm("\n Send this breakdown to verify your usage?");
10350
10408
  if (!ok) {
@@ -10374,13 +10432,25 @@ async function runVerify() {
10374
10432
  ` Sending ${records.length} request records for analysis${truncated ? " (most recent, sampled)" : ""}\u2026`
10375
10433
  )
10376
10434
  );
10435
+ const body = {
10436
+ cliVersion: VERSION,
10437
+ requests: records,
10438
+ ...truncated ? { truncated: true } : {}
10439
+ };
10440
+ const verifyWithRefresh = async () => {
10441
+ try {
10442
+ return await verifyUsage(auth.token, body);
10443
+ } catch (err) {
10444
+ if (!(err instanceof UnauthorizedError)) throw err;
10445
+ const healed = await refreshCliTokenFromConfig();
10446
+ if (!healed) throw err;
10447
+ saveAuth(void 0, { cliToken: healed.token, handle: healed.handle });
10448
+ return await verifyUsage(healed.token, body);
10449
+ }
10450
+ };
10377
10451
  let result;
10378
10452
  try {
10379
- result = await verifyUsage(token, {
10380
- cliVersion: VERSION,
10381
- requests: records,
10382
- ...truncated ? { truncated: true } : {}
10383
- });
10453
+ result = await verifyWithRefresh();
10384
10454
  } catch (err) {
10385
10455
  if (err instanceof UnauthorizedError) {
10386
10456
  clearAuth();
@@ -10561,9 +10631,11 @@ async function main() {
10561
10631
  break;
10562
10632
  case "install-sync":
10563
10633
  console.log(` ${installAutoSync()}`);
10634
+ setAutoSyncOptOut(false);
10564
10635
  break;
10565
10636
  case "uninstall-sync":
10566
10637
  console.log(` ${uninstallAutoSync()}`);
10638
+ setAutoSyncOptOut(true);
10567
10639
  break;
10568
10640
  case "help":
10569
10641
  printHelp();