vibestats 1.3.13 → 1.3.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +645 -155
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -902,9 +902,9 @@ function buildActivityHeatmapLines(graph, options) {
902
902
 
903
903
  // src/anthropic-sources.ts
904
904
  import { execFile } from "child_process";
905
- import { promises as fs } from "fs";
906
- import { homedir } from "os";
907
- import { basename, join } from "path";
905
+ import { promises as fs2 } from "fs";
906
+ import { homedir as homedir2 } from "os";
907
+ import { basename, join as join2 } from "path";
908
908
  import { promisify } from "util";
909
909
 
910
910
  // src/shared/async.ts
@@ -934,12 +934,168 @@ async function mapWithConcurrency(items, concurrency, worker) {
934
934
  return results;
935
935
  }
936
936
 
937
+ // src/usage/cache.ts
938
+ import { promises as fs } from "fs";
939
+ import { homedir } from "os";
940
+ import { dirname, join } from "path";
941
+ var USAGE_CACHE_SCHEMA_VERSION = 1;
942
+ var USAGE_CACHE_FILENAME = "usage-entries-v1.json";
943
+ var CACHE_LOCK_TIMEOUT_MS = 2e3;
944
+ var CACHE_LOCK_RETRY_MS = 25;
945
+ var CACHE_LOCK_STALE_MS = 3e4;
946
+ var cacheWriteQueue = Promise.resolve();
947
+ function getUsageCacheDir() {
948
+ return process.env.VIBESTATS_CACHE_DIR || join(homedir(), ".vibestats", "cache");
949
+ }
950
+ function getUsageCachePath() {
951
+ return join(getUsageCacheDir(), USAGE_CACHE_FILENAME);
952
+ }
953
+ function shouldUsePersistentUsageCache(options = {}) {
954
+ return options.useCache !== false;
955
+ }
956
+ function createMissingFingerprint(path) {
957
+ return {
958
+ path,
959
+ size: -1,
960
+ mtimeMs: -1
961
+ };
962
+ }
963
+ async function getSourceFingerprint(path) {
964
+ try {
965
+ const stats = await fs.stat(path);
966
+ if (!stats.isFile()) return null;
967
+ return {
968
+ path,
969
+ size: stats.size,
970
+ mtimeMs: Math.trunc(stats.mtimeMs)
971
+ };
972
+ } catch {
973
+ return null;
974
+ }
975
+ }
976
+ function fingerprintMatches(left, right) {
977
+ return Boolean(left && left.path === right.path && left.size === right.size && left.mtimeMs === right.mtimeMs);
978
+ }
979
+ function dependencyFingerprintsMatch(cached, current) {
980
+ if ((cached?.length || 0) !== current.length) return false;
981
+ for (let index = 0; index < current.length; index++) {
982
+ if (!fingerprintMatches(cached?.[index], current[index])) return false;
983
+ }
984
+ return true;
985
+ }
986
+ function getCachedUsageValue(cache, key, fingerprint, dependencyFingerprints = []) {
987
+ const record = cache.entries[key];
988
+ if (!record) return null;
989
+ if (!fingerprintMatches(record.fingerprint, fingerprint)) return null;
990
+ if (!dependencyFingerprintsMatch(record.dependencyFingerprints, dependencyFingerprints)) return null;
991
+ return record.value;
992
+ }
993
+ function setCachedUsageValue(cache, key, kind, fingerprint, value, dependencyFingerprints = []) {
994
+ cache.entries[key] = {
995
+ kind,
996
+ fingerprint,
997
+ dependencyFingerprints,
998
+ value,
999
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1000
+ };
1001
+ }
1002
+ function createEmptyUsageCache() {
1003
+ return {
1004
+ schemaVersion: USAGE_CACHE_SCHEMA_VERSION,
1005
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1006
+ entries: {}
1007
+ };
1008
+ }
1009
+ function isUsageCache(value) {
1010
+ const candidate = value;
1011
+ return candidate?.schemaVersion === USAGE_CACHE_SCHEMA_VERSION && candidate.entries !== null && typeof candidate.entries === "object";
1012
+ }
1013
+ async function readUsageCacheFile() {
1014
+ try {
1015
+ const content = await fs.readFile(getUsageCachePath(), "utf-8");
1016
+ const parsed = JSON.parse(content);
1017
+ if (!isUsageCache(parsed)) return createEmptyUsageCache();
1018
+ return parsed;
1019
+ } catch {
1020
+ return createEmptyUsageCache();
1021
+ }
1022
+ }
1023
+ async function readUsageCacheForLookup(options = {}) {
1024
+ if (!shouldUsePersistentUsageCache(options) || options.refreshCache) {
1025
+ return createEmptyUsageCache();
1026
+ }
1027
+ return readUsageCacheFile();
1028
+ }
1029
+ async function writeUsageCacheFile(cache) {
1030
+ const cachePath = getUsageCachePath();
1031
+ await fs.mkdir(dirname(cachePath), { recursive: true });
1032
+ const tmpPath = `${cachePath}.${process.pid}.${Date.now()}.tmp`;
1033
+ cache.generatedAt = (/* @__PURE__ */ new Date()).toISOString();
1034
+ await fs.writeFile(tmpPath, `${JSON.stringify(cache, null, 2)}
1035
+ `, "utf-8");
1036
+ await fs.rename(tmpPath, cachePath);
1037
+ }
1038
+ function sleep(ms) {
1039
+ return new Promise((resolve) => setTimeout(resolve, ms));
1040
+ }
1041
+ async function withUsageCacheLock(task) {
1042
+ const lockPath = `${getUsageCachePath()}.lock`;
1043
+ await fs.mkdir(dirname(lockPath), { recursive: true });
1044
+ const start = Date.now();
1045
+ let lockHandle = null;
1046
+ while (!lockHandle) {
1047
+ try {
1048
+ lockHandle = await fs.open(lockPath, "wx");
1049
+ } catch (error) {
1050
+ const code = error.code;
1051
+ if (code !== "EEXIST" || Date.now() - start >= CACHE_LOCK_TIMEOUT_MS) {
1052
+ return;
1053
+ }
1054
+ try {
1055
+ const lockStats = await fs.stat(lockPath);
1056
+ if (Date.now() - lockStats.mtimeMs > CACHE_LOCK_STALE_MS) {
1057
+ await fs.unlink(lockPath);
1058
+ continue;
1059
+ }
1060
+ } catch {
1061
+ continue;
1062
+ }
1063
+ await sleep(CACHE_LOCK_RETRY_MS);
1064
+ }
1065
+ }
1066
+ try {
1067
+ await task();
1068
+ } finally {
1069
+ try {
1070
+ await lockHandle.close();
1071
+ } catch {
1072
+ }
1073
+ try {
1074
+ await fs.unlink(lockPath);
1075
+ } catch {
1076
+ }
1077
+ }
1078
+ }
1079
+ async function updateUsageCache(options, mutator) {
1080
+ if (!shouldUsePersistentUsageCache(options)) return;
1081
+ cacheWriteQueue = cacheWriteQueue.then(() => withUsageCacheLock(async () => {
1082
+ const cache = await readUsageCacheFile();
1083
+ mutator(cache);
1084
+ await writeUsageCacheFile(cache);
1085
+ })).catch(() => {
1086
+ });
1087
+ await cacheWriteQueue;
1088
+ }
1089
+
937
1090
  // src/anthropic-sources.ts
938
1091
  var execFileAsync = promisify(execFile);
939
1092
  var SQLITE_SEPARATOR = "";
940
1093
  var SQLITE_MAX_BUFFER = 64 * 1024 * 1024;
941
1094
  var FILE_PARSE_CONCURRENCY = getRecommendedConcurrency();
942
1095
  var anthropicEntryCache = /* @__PURE__ */ new Map();
1096
+ var CLAUDE_JSONL_CACHE_KIND = "claude-jsonl";
1097
+ var FACTORY_SETTINGS_CACHE_KIND = "factory-settings";
1098
+ var OPENCODE_DB_CACHE_KIND = "opencode-db";
943
1099
  function toLocalDateString(isoTimestamp) {
944
1100
  const date = new Date(isoTimestamp);
945
1101
  const year = date.getFullYear();
@@ -949,7 +1105,7 @@ function toLocalDateString(isoTimestamp) {
949
1105
  }
950
1106
  async function pathExists(path) {
951
1107
  try {
952
- await fs.access(path);
1108
+ await fs2.access(path);
953
1109
  return true;
954
1110
  } catch {
955
1111
  return false;
@@ -957,7 +1113,7 @@ async function pathExists(path) {
957
1113
  }
958
1114
  async function safeRealpath(path) {
959
1115
  try {
960
- return await fs.realpath(path);
1116
+ return await fs2.realpath(path);
961
1117
  } catch {
962
1118
  return path;
963
1119
  }
@@ -967,17 +1123,17 @@ async function findFiles(dir, matcher, visited = /* @__PURE__ */ new Set(), resu
967
1123
  const realPath = await safeRealpath(dir);
968
1124
  if (visited.has(realPath)) return result;
969
1125
  visited.add(realPath);
970
- let entries2;
1126
+ let entries;
971
1127
  try {
972
- entries2 = await fs.readdir(dir);
1128
+ entries = await fs2.readdir(dir);
973
1129
  } catch {
974
1130
  return result;
975
1131
  }
976
- for (const entry of entries2) {
977
- const fullPath = join(dir, entry);
1132
+ for (const entry of entries) {
1133
+ const fullPath = join2(dir, entry);
978
1134
  let stat;
979
1135
  try {
980
- stat = await fs.stat(fullPath);
1136
+ stat = await fs2.stat(fullPath);
981
1137
  } catch {
982
1138
  continue;
983
1139
  }
@@ -1019,36 +1175,99 @@ function calculateKnownCost(modelName, inputTokens, outputTokens, cacheWriteToke
1019
1175
  function entryTotalTokens(entry) {
1020
1176
  return entry.inputTokens + entry.outputTokens + entry.cacheWriteTokens + entry.cacheReadTokens;
1021
1177
  }
1178
+ function getPersistentCacheKey(kind, filePath) {
1179
+ return `${kind}:${filePath}`;
1180
+ }
1181
+ function isPathWithin(path, root) {
1182
+ const normalizedRoot = root.endsWith("/") ? root : `${root}/`;
1183
+ return path === root || path.startsWith(normalizedRoot);
1184
+ }
1185
+ function toCachedAnthropicEntry(entry) {
1186
+ return {
1187
+ timestamp: entry.timestamp,
1188
+ rawModel: entry.rawModel,
1189
+ inputTokens: entry.inputTokens,
1190
+ outputTokens: entry.outputTokens,
1191
+ cacheWriteTokens: entry.cacheWriteTokens,
1192
+ cacheReadTokens: entry.cacheReadTokens,
1193
+ explicitCost: entry.explicitCost,
1194
+ messageCount: entry.messageCount,
1195
+ sourceKey: entry.sourceKey,
1196
+ sessionId: entry.sessionId,
1197
+ sessionKind: entry.sessionKind,
1198
+ subagentId: entry.subagentId
1199
+ };
1200
+ }
1201
+ function rehydrateAnthropicEntry(entry) {
1202
+ return {
1203
+ date: toLocalDateString(entry.timestamp),
1204
+ timestamp: entry.timestamp,
1205
+ rawModel: entry.rawModel,
1206
+ model: getModelDisplayName(entry.rawModel),
1207
+ inputTokens: entry.inputTokens,
1208
+ outputTokens: entry.outputTokens,
1209
+ cacheWriteTokens: entry.cacheWriteTokens,
1210
+ cacheReadTokens: entry.cacheReadTokens,
1211
+ cost: calculateKnownCost(
1212
+ entry.rawModel,
1213
+ entry.inputTokens,
1214
+ entry.outputTokens,
1215
+ entry.cacheWriteTokens,
1216
+ entry.cacheReadTokens,
1217
+ entry.explicitCost
1218
+ ),
1219
+ messageCount: entry.messageCount,
1220
+ source: "claude",
1221
+ sourceKey: entry.sourceKey,
1222
+ sessionId: entry.sessionId,
1223
+ sessionKind: entry.sessionKind,
1224
+ subagentId: entry.subagentId
1225
+ };
1226
+ }
1227
+ function rehydrateAnthropicEntries(entries) {
1228
+ return entries.map(rehydrateAnthropicEntry);
1229
+ }
1230
+ function cacheAnthropicEntries(entries) {
1231
+ return entries.map((entry) => toCachedAnthropicEntry(entry));
1232
+ }
1233
+ async function getOpenCodeDependencyFingerprints(dbPath) {
1234
+ const walPath = `${dbPath}-wal`;
1235
+ const shmPath = `${dbPath}-shm`;
1236
+ return [
1237
+ await getSourceFingerprint(walPath) || createMissingFingerprint(walPath),
1238
+ await getSourceFingerprint(shmPath) || createMissingFingerprint(shmPath)
1239
+ ];
1240
+ }
1022
1241
  async function resolveProjectDir(projectsDir, cwd) {
1023
1242
  let current = cwd;
1024
1243
  while (current && current !== "/") {
1025
1244
  const encoded = current.replace(/\//g, "-");
1026
- const candidate = join(projectsDir, encoded);
1245
+ const candidate = join2(projectsDir, encoded);
1027
1246
  if (await pathExists(candidate)) return candidate;
1028
- const parent = join(current, "..");
1247
+ const parent = join2(current, "..");
1029
1248
  if (parent === current) break;
1030
1249
  current = parent;
1031
1250
  }
1032
1251
  return null;
1033
1252
  }
1034
1253
  function getClaudeDir() {
1035
- return process.env.CLAUDE_HOME || join(homedir(), ".claude");
1254
+ return process.env.CLAUDE_HOME || join2(homedir2(), ".claude");
1036
1255
  }
1037
1256
  function getOpenCodeDbPath() {
1038
1257
  if (process.env.OPENCODE_DB_PATH) {
1039
1258
  return process.env.OPENCODE_DB_PATH;
1040
1259
  }
1041
- return join(homedir(), ".local", "share", "opencode", "opencode.db");
1260
+ return join2(homedir2(), ".local", "share", "opencode", "opencode.db");
1042
1261
  }
1043
1262
  function getFactorySessionsDir() {
1044
1263
  if (process.env.FACTORY_SESSIONS_DIR) {
1045
1264
  return process.env.FACTORY_SESSIONS_DIR;
1046
1265
  }
1047
- return join(homedir(), ".factory", "sessions");
1266
+ return join2(homedir2(), ".factory", "sessions");
1048
1267
  }
1049
1268
  async function claudeCompatibleDataExists() {
1050
1269
  const [claudeProjects, opencodeDb, factorySessions] = await Promise.all([
1051
- pathExists(join(getClaudeDir(), "projects")),
1270
+ pathExists(join2(getClaudeDir(), "projects")),
1052
1271
  pathExists(getOpenCodeDbPath()),
1053
1272
  pathExists(getFactorySessionsDir())
1054
1273
  ]);
@@ -1060,12 +1279,14 @@ function getAnthropicEntryCacheKey(options) {
1060
1279
  opencodeDbPath: getOpenCodeDbPath(),
1061
1280
  factorySessionsDir: getFactorySessionsDir(),
1062
1281
  projectFilter: options.projectFilter || "",
1063
- families: options.families || []
1282
+ families: options.families || [],
1283
+ useCache: options.useCache !== false,
1284
+ refreshCache: options.refreshCache === true
1064
1285
  });
1065
1286
  }
1066
1287
  async function parseClaudeProjectFile(filePath) {
1067
1288
  try {
1068
- const content = await fs.readFile(filePath, "utf-8");
1289
+ const content = await fs2.readFile(filePath, "utf-8");
1069
1290
  const lines = content.split("\n");
1070
1291
  const fallbackSessionId = basename(filePath, ".jsonl");
1071
1292
  const isSubagentFile = filePath.includes("/subagents/");
@@ -1116,27 +1337,70 @@ async function parseClaudeProjectFile(filePath) {
1116
1337
  return [];
1117
1338
  }
1118
1339
  }
1119
- async function parseClaudeProjectEntries(projectFilter) {
1340
+ async function parseClaudeProjectEntries(projectFilter, cacheOptions = {}) {
1120
1341
  const claudeDir = getClaudeDir();
1121
- const projectsDir = join(claudeDir, "projects");
1342
+ const projectsDir = join2(claudeDir, "projects");
1122
1343
  if (!await pathExists(projectsDir)) return [];
1123
1344
  let searchDir = projectsDir;
1124
1345
  if (projectFilter) {
1125
1346
  const resolved = await resolveProjectDir(projectsDir, projectFilter);
1126
- if (!resolved) return entries;
1347
+ if (!resolved) return [];
1127
1348
  searchDir = resolved;
1128
1349
  }
1129
1350
  const jsonlFiles = await findFiles(searchDir, (entry) => entry.endsWith(".jsonl"));
1351
+ const persistentCache = await readUsageCacheForLookup(cacheOptions);
1352
+ const retainedKeys = /* @__PURE__ */ new Set();
1353
+ const updates = [];
1130
1354
  const parsedFiles = await mapWithConcurrency(
1131
1355
  jsonlFiles,
1132
1356
  FILE_PARSE_CONCURRENCY,
1133
- (filePath) => parseClaudeProjectFile(filePath)
1357
+ async (filePath) => {
1358
+ const fingerprint = await getSourceFingerprint(filePath);
1359
+ if (!fingerprint) return [];
1360
+ const key = getPersistentCacheKey(CLAUDE_JSONL_CACHE_KIND, filePath);
1361
+ retainedKeys.add(key);
1362
+ const cached = getCachedUsageValue(persistentCache, key, fingerprint);
1363
+ if (cached) {
1364
+ return rehydrateAnthropicEntries(cached);
1365
+ }
1366
+ const entries = await parseClaudeProjectFile(filePath);
1367
+ updates.push({ key, fingerprint, value: cacheAnthropicEntries(entries) });
1368
+ return entries;
1369
+ }
1134
1370
  );
1371
+ await updateUsageCache(cacheOptions, (cache) => {
1372
+ for (const [key, record] of Object.entries(cache.entries)) {
1373
+ if (record.kind === CLAUDE_JSONL_CACHE_KIND && isPathWithin(record.fingerprint.path, searchDir) && !retainedKeys.has(key)) {
1374
+ delete cache.entries[key];
1375
+ }
1376
+ }
1377
+ for (const update of updates) {
1378
+ setCachedUsageValue(cache, update.key, CLAUDE_JSONL_CACHE_KIND, update.fingerprint, update.value);
1379
+ }
1380
+ });
1135
1381
  return parsedFiles.flat();
1136
1382
  }
1137
- async function parseOpenCodeEntries() {
1383
+ async function parseOpenCodeEntries(cacheOptions = {}) {
1138
1384
  const dbPath = getOpenCodeDbPath();
1139
- if (!await pathExists(dbPath)) return [];
1385
+ const key = getPersistentCacheKey(OPENCODE_DB_CACHE_KIND, dbPath);
1386
+ const fingerprint = await getSourceFingerprint(dbPath);
1387
+ if (!fingerprint) {
1388
+ await updateUsageCache(cacheOptions, (cache) => {
1389
+ delete cache.entries[key];
1390
+ });
1391
+ return [];
1392
+ }
1393
+ const dependencyFingerprints = await getOpenCodeDependencyFingerprints(dbPath);
1394
+ const persistentCache = await readUsageCacheForLookup(cacheOptions);
1395
+ const cached = getCachedUsageValue(
1396
+ persistentCache,
1397
+ key,
1398
+ fingerprint,
1399
+ dependencyFingerprints
1400
+ );
1401
+ if (cached) {
1402
+ return rehydrateAnthropicEntries(cached);
1403
+ }
1140
1404
  const sql = `
1141
1405
  SELECT
1142
1406
  COALESCE(m.id, ''),
@@ -1165,7 +1429,7 @@ async function parseOpenCodeEntries() {
1165
1429
  } catch {
1166
1430
  return [];
1167
1431
  }
1168
- const entries2 = [];
1432
+ const entries = [];
1169
1433
  for (const line of stdout.split("\n")) {
1170
1434
  if (!line.trim()) continue;
1171
1435
  const [
@@ -1190,7 +1454,7 @@ async function parseOpenCodeEntries() {
1190
1454
  const cacheReadTokens = Number(cacheReadText) || 0;
1191
1455
  const explicitCost = Number(costText) || 0;
1192
1456
  const sessionKind = parentId ? "subagent" : "main";
1193
- entries2.push({
1457
+ entries.push({
1194
1458
  date: toLocalDateString(timestamp),
1195
1459
  timestamp,
1196
1460
  rawModel,
@@ -1200,6 +1464,7 @@ async function parseOpenCodeEntries() {
1200
1464
  cacheWriteTokens,
1201
1465
  cacheReadTokens,
1202
1466
  cost: calculateKnownCost(rawModel, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens, explicitCost),
1467
+ explicitCost,
1203
1468
  messageCount: 1,
1204
1469
  source: "claude",
1205
1470
  sourceKey: "opencode",
@@ -1208,7 +1473,17 @@ async function parseOpenCodeEntries() {
1208
1473
  subagentId: sessionKind === "subagent" ? messageId || sessionId : void 0
1209
1474
  });
1210
1475
  }
1211
- return entries2;
1476
+ await updateUsageCache(cacheOptions, (cache) => {
1477
+ setCachedUsageValue(
1478
+ cache,
1479
+ key,
1480
+ OPENCODE_DB_CACHE_KIND,
1481
+ fingerprint,
1482
+ cacheAnthropicEntries(entries),
1483
+ dependencyFingerprints
1484
+ );
1485
+ });
1486
+ return entries;
1212
1487
  }
1213
1488
  async function readFactorySessionMeta(sessionFilePath) {
1214
1489
  let messageCount = 0;
@@ -1216,7 +1491,7 @@ async function readFactorySessionMeta(sessionFilePath) {
1216
1491
  let sessionKind = "main";
1217
1492
  let subagentId;
1218
1493
  try {
1219
- const content = await fs.readFile(sessionFilePath, "utf-8");
1494
+ const content = await fs2.readFile(sessionFilePath, "utf-8");
1220
1495
  for (const line of content.split("\n")) {
1221
1496
  if (!line.trim()) continue;
1222
1497
  try {
@@ -1242,13 +1517,33 @@ async function readFactorySessionMeta(sessionFilePath) {
1242
1517
  }
1243
1518
  return { messageCount: Math.max(messageCount, 1), timestamp, sessionKind, subagentId };
1244
1519
  }
1245
- async function parseFactoryEntries() {
1520
+ async function parseFactoryEntries(cacheOptions = {}) {
1246
1521
  const sessionsDir = getFactorySessionsDir();
1247
1522
  if (!await pathExists(sessionsDir)) return [];
1248
1523
  const settingsFiles = await findFiles(sessionsDir, (entry) => entry.endsWith(".settings.json"));
1524
+ const persistentCache = await readUsageCacheForLookup(cacheOptions);
1525
+ const retainedKeys = /* @__PURE__ */ new Set();
1526
+ const updates = [];
1249
1527
  const parsedFiles = await mapWithConcurrency(settingsFiles, FILE_PARSE_CONCURRENCY, async (settingsPath) => {
1528
+ const fingerprint = await getSourceFingerprint(settingsPath);
1529
+ if (!fingerprint) return null;
1530
+ const sessionFilePath = settingsPath.replace(/\.settings\.json$/, ".jsonl");
1531
+ const dependencyFingerprints = [
1532
+ await getSourceFingerprint(sessionFilePath) || createMissingFingerprint(sessionFilePath)
1533
+ ];
1534
+ const key = getPersistentCacheKey(FACTORY_SETTINGS_CACHE_KIND, settingsPath);
1535
+ retainedKeys.add(key);
1536
+ const cached = getCachedUsageValue(
1537
+ persistentCache,
1538
+ key,
1539
+ fingerprint,
1540
+ dependencyFingerprints
1541
+ );
1542
+ if (cached) {
1543
+ return rehydrateAnthropicEntries(cached);
1544
+ }
1250
1545
  try {
1251
- const settings = JSON.parse(await fs.readFile(settingsPath, "utf-8"));
1546
+ const settings = JSON.parse(await fs2.readFile(settingsPath, "utf-8"));
1252
1547
  const usage = settings.tokenUsage;
1253
1548
  const configuredModel = settings.model;
1254
1549
  if (!usage || !configuredModel) return null;
@@ -1259,12 +1554,11 @@ async function parseFactoryEntries() {
1259
1554
  const outputTokens = (usage.outputTokens || 0) + (usage.thinkingTokens || 0);
1260
1555
  const totalTokens = inputTokens + outputTokens + cacheWriteTokens + cacheReadTokens;
1261
1556
  if (totalTokens === 0) return null;
1262
- const sessionFilePath = settingsPath.replace(/\.settings\.json$/, ".jsonl");
1263
1557
  const sessionId = basename(settingsPath, ".settings.json");
1264
1558
  const meta = await readFactorySessionMeta(sessionFilePath);
1265
1559
  const timestamp = settings.providerLockTimestamp || meta.timestamp;
1266
1560
  if (!timestamp) return null;
1267
- return {
1561
+ const entry = {
1268
1562
  date: toLocalDateString(timestamp),
1269
1563
  timestamp,
1270
1564
  rawModel,
@@ -1281,11 +1575,35 @@ async function parseFactoryEntries() {
1281
1575
  sessionKind: meta.sessionKind,
1282
1576
  subagentId: meta.subagentId
1283
1577
  };
1578
+ updates.push({
1579
+ key,
1580
+ fingerprint,
1581
+ dependencyFingerprints,
1582
+ value: cacheAnthropicEntries([entry])
1583
+ });
1584
+ return entry;
1284
1585
  } catch {
1285
1586
  return null;
1286
1587
  }
1287
1588
  });
1288
- return parsedFiles.filter((entry) => entry !== null);
1589
+ await updateUsageCache(cacheOptions, (cache) => {
1590
+ for (const [key, record] of Object.entries(cache.entries)) {
1591
+ if (record.kind === FACTORY_SETTINGS_CACHE_KIND && isPathWithin(record.fingerprint.path, sessionsDir) && !retainedKeys.has(key)) {
1592
+ delete cache.entries[key];
1593
+ }
1594
+ }
1595
+ for (const update of updates) {
1596
+ setCachedUsageValue(
1597
+ cache,
1598
+ update.key,
1599
+ FACTORY_SETTINGS_CACHE_KIND,
1600
+ update.fingerprint,
1601
+ update.value,
1602
+ update.dependencyFingerprints
1603
+ );
1604
+ }
1605
+ });
1606
+ return parsedFiles.flat().filter((entry) => entry !== null);
1289
1607
  }
1290
1608
  async function collectAnthropicUsageEntries(options = {}) {
1291
1609
  const cacheKey = getAnthropicEntryCacheKey(options);
@@ -1295,22 +1613,22 @@ async function collectAnthropicUsageEntries(options = {}) {
1295
1613
  }
1296
1614
  const loadPromise = (async () => {
1297
1615
  const [claudeEntries, opencodeEntries, factoryEntries] = await Promise.all([
1298
- parseClaudeProjectEntries(options.projectFilter),
1299
- parseOpenCodeEntries(),
1300
- parseFactoryEntries()
1616
+ parseClaudeProjectEntries(options.projectFilter, options),
1617
+ parseOpenCodeEntries(options),
1618
+ parseFactoryEntries(options)
1301
1619
  ]);
1302
- const entries2 = [...claudeEntries, ...opencodeEntries, ...factoryEntries];
1620
+ const entries = [...claudeEntries, ...opencodeEntries, ...factoryEntries];
1303
1621
  if (!options.families || options.families.length === 0) {
1304
- return entries2;
1622
+ return entries;
1305
1623
  }
1306
- return entries2.filter((entry) => matchesModelFamilies(entry.rawModel, options.families));
1624
+ return entries.filter((entry) => matchesModelFamilies(entry.rawModel, options.families));
1307
1625
  })();
1308
1626
  anthropicEntryCache.set(cacheKey, loadPromise);
1309
1627
  return loadPromise;
1310
1628
  }
1311
1629
  async function loadClaudeStatsFromJsonl(options = {}) {
1312
- const entries2 = await collectAnthropicUsageEntries(options);
1313
- if (entries2.length === 0) {
1630
+ const entries = await collectAnthropicUsageEntries(options);
1631
+ if (entries.length === 0) {
1314
1632
  return null;
1315
1633
  }
1316
1634
  const modelUsage = /* @__PURE__ */ new Map();
@@ -1321,7 +1639,7 @@ async function loadClaudeStatsFromJsonl(options = {}) {
1321
1639
  const hourCounts = {};
1322
1640
  let firstTimestamp = null;
1323
1641
  let totalMessages = 0;
1324
- for (const entry of entries2) {
1642
+ for (const entry of entries) {
1325
1643
  const existing = modelUsage.get(entry.rawModel) ?? {
1326
1644
  inputTokens: 0,
1327
1645
  outputTokens: 0,
@@ -1386,13 +1704,14 @@ async function loadClaudeStatsFromJsonl(options = {}) {
1386
1704
  }
1387
1705
 
1388
1706
  // src/codex-loader.ts
1389
- import { promises as fs2 } from "fs";
1390
- import { homedir as homedir2 } from "os";
1391
- import { join as join2 } from "path";
1707
+ import { promises as fs3 } from "fs";
1708
+ import { homedir as homedir3 } from "os";
1709
+ import { join as join3 } from "path";
1392
1710
  var FILE_PARSE_CONCURRENCY2 = getRecommendedConcurrency();
1393
1711
  var codexParseCache = /* @__PURE__ */ new Map();
1712
+ var CODEX_JSONL_CACHE_KIND = "codex-jsonl";
1394
1713
  function getCodexDir() {
1395
- return process.env.CODEX_HOME || join2(homedir2(), ".codex");
1714
+ return process.env.CODEX_HOME || join3(homedir3(), ".codex");
1396
1715
  }
1397
1716
  function toLocalDateString2(isoTimestamp) {
1398
1717
  const date = new Date(isoTimestamp);
@@ -1403,7 +1722,7 @@ function toLocalDateString2(isoTimestamp) {
1403
1722
  }
1404
1723
  async function pathExists2(path) {
1405
1724
  try {
1406
- await fs2.access(path);
1725
+ await fs3.access(path);
1407
1726
  return true;
1408
1727
  } catch {
1409
1728
  return false;
@@ -1412,24 +1731,24 @@ async function pathExists2(path) {
1412
1731
  async function codexDataExists() {
1413
1732
  const codexDir = getCodexDir();
1414
1733
  if (!await pathExists2(codexDir)) return false;
1415
- const sessionsDir = join2(codexDir, "sessions");
1416
- const archivedDir = join2(codexDir, "archived_sessions");
1734
+ const sessionsDir = join3(codexDir, "sessions");
1735
+ const archivedDir = join3(codexDir, "archived_sessions");
1417
1736
  return await pathExists2(sessionsDir) || await pathExists2(archivedDir);
1418
1737
  }
1419
1738
  async function findJsonlFiles(dir) {
1420
1739
  const files = [];
1421
1740
  if (!await pathExists2(dir)) return files;
1422
- let entries2;
1741
+ let entries;
1423
1742
  try {
1424
- entries2 = await fs2.readdir(dir);
1743
+ entries = await fs3.readdir(dir);
1425
1744
  } catch {
1426
1745
  return files;
1427
1746
  }
1428
- for (const entry of entries2) {
1429
- const fullPath = join2(dir, entry);
1747
+ for (const entry of entries) {
1748
+ const fullPath = join3(dir, entry);
1430
1749
  let stat;
1431
1750
  try {
1432
- stat = await fs2.stat(fullPath);
1751
+ stat = await fs3.stat(fullPath);
1433
1752
  } catch {
1434
1753
  continue;
1435
1754
  }
@@ -1441,15 +1760,110 @@ async function findJsonlFiles(dir) {
1441
1760
  }
1442
1761
  return files;
1443
1762
  }
1444
- function getCodexParseCacheKey(families) {
1763
+ function getPersistentCacheKey2(kind, filePath) {
1764
+ return `${kind}:${filePath}`;
1765
+ }
1766
+ function isPathWithin2(path, root) {
1767
+ const normalizedRoot = root.endsWith("/") ? root : `${root}/`;
1768
+ return path === root || path.startsWith(normalizedRoot);
1769
+ }
1770
+ function toCachedCodexUsageEntry(entry) {
1771
+ return {
1772
+ timestamp: entry.timestamp,
1773
+ rawModel: entry.rawModel,
1774
+ inputTokens: entry.inputTokens,
1775
+ outputTokens: entry.outputTokens,
1776
+ cacheReadTokens: entry.cacheReadTokens,
1777
+ reasoningTokens: entry.reasoningTokens,
1778
+ messageCount: entry.messageCount,
1779
+ sessionId: entry.sessionId,
1780
+ sessionKind: entry.sessionKind,
1781
+ subagentId: entry.subagentId
1782
+ };
1783
+ }
1784
+ function rehydrateCodexUsageEntry(entry) {
1785
+ const pricing = getCodexModelPricing(entry.rawModel);
1786
+ return {
1787
+ date: toLocalDateString2(entry.timestamp),
1788
+ timestamp: entry.timestamp,
1789
+ model: getCodexModelDisplayName(entry.rawModel),
1790
+ rawModel: entry.rawModel,
1791
+ inputTokens: entry.inputTokens,
1792
+ outputTokens: entry.outputTokens,
1793
+ cacheReadTokens: entry.cacheReadTokens,
1794
+ reasoningTokens: entry.reasoningTokens,
1795
+ cost: entry.inputTokens * pricing.input / 1e6 + entry.outputTokens * pricing.output / 1e6 + entry.cacheReadTokens * pricing.cachedInput / 1e6,
1796
+ messageCount: entry.messageCount,
1797
+ sessionId: entry.sessionId,
1798
+ sessionKind: entry.sessionKind,
1799
+ subagentId: entry.subagentId,
1800
+ source: "codex",
1801
+ sourceKey: "codex"
1802
+ };
1803
+ }
1804
+ function toCachedParsedCodexFile(parsed) {
1805
+ return {
1806
+ session: parsed.session,
1807
+ rawEntries: parsed.rawEntries.map(toCachedCodexUsageEntry)
1808
+ };
1809
+ }
1810
+ function rehydrateParsedCodexFile(cached) {
1811
+ return {
1812
+ session: cached.session,
1813
+ rawEntries: cached.rawEntries.map(rehydrateCodexUsageEntry)
1814
+ };
1815
+ }
1816
+ function filterParsedCodexFileByFamilies(parsed, families) {
1817
+ if (!families || families.length === 0) {
1818
+ return parsed;
1819
+ }
1820
+ const filteredPerModelUsage = {};
1821
+ const summedUsage = {
1822
+ input_tokens: 0,
1823
+ cached_input_tokens: 0,
1824
+ output_tokens: 0,
1825
+ reasoning_output_tokens: 0,
1826
+ total_tokens: 0
1827
+ };
1828
+ let primaryModel = parsed.session.model;
1829
+ let maxTokens = 0;
1830
+ for (const [model, usage] of Object.entries(parsed.session.perModelUsage || {})) {
1831
+ if (!matchesModelFamilies(model, families)) continue;
1832
+ filteredPerModelUsage[model] = usage;
1833
+ summedUsage.input_tokens += usage.input_tokens;
1834
+ summedUsage.cached_input_tokens += usage.cached_input_tokens;
1835
+ summedUsage.output_tokens += usage.output_tokens;
1836
+ summedUsage.reasoning_output_tokens += usage.reasoning_output_tokens;
1837
+ summedUsage.total_tokens += usage.total_tokens;
1838
+ if (usage.total_tokens > maxTokens) {
1839
+ maxTokens = usage.total_tokens;
1840
+ primaryModel = model;
1841
+ }
1842
+ }
1843
+ if (Object.keys(filteredPerModelUsage).length === 0) {
1844
+ return null;
1845
+ }
1846
+ return {
1847
+ session: {
1848
+ ...parsed.session,
1849
+ model: primaryModel,
1850
+ tokenUsage: summedUsage,
1851
+ perModelUsage: filteredPerModelUsage
1852
+ },
1853
+ rawEntries: parsed.rawEntries.filter((entry) => matchesModelFamilies(entry.rawModel, families))
1854
+ };
1855
+ }
1856
+ function getCodexParseCacheKey(options) {
1445
1857
  return JSON.stringify({
1446
1858
  codexDir: getCodexDir(),
1447
- families: families || []
1859
+ families: options.families || [],
1860
+ useCache: options.useCache !== false,
1861
+ refreshCache: options.refreshCache === true
1448
1862
  });
1449
1863
  }
1450
1864
  async function parseSessionFile(filePath, families) {
1451
1865
  try {
1452
- const content = await fs2.readFile(filePath, "utf-8");
1866
+ const content = await fs3.readFile(filePath, "utf-8");
1453
1867
  const lines = content.trim().split("\n");
1454
1868
  let sessionMeta = null;
1455
1869
  let currentModel = "gpt-5";
@@ -1568,28 +1982,66 @@ async function parseSessionFile(filePath, families) {
1568
1982
  }
1569
1983
  }
1570
1984
  async function collectParsedCodexFiles(options = {}) {
1571
- const cacheKey = getCodexParseCacheKey(options.families);
1985
+ const cacheKey = getCodexParseCacheKey(options);
1572
1986
  const cached = codexParseCache.get(cacheKey);
1573
1987
  if (cached) {
1574
1988
  return cached;
1575
1989
  }
1576
1990
  const loadPromise = (async () => {
1577
1991
  const codexDir = getCodexDir();
1578
- const sessionsDir = join2(codexDir, "sessions");
1579
- const archivedDir = join2(codexDir, "archived_sessions");
1992
+ const sessionsDir = join3(codexDir, "sessions");
1993
+ const archivedDir = join3(codexDir, "archived_sessions");
1580
1994
  const [sessionFiles, archivedFiles] = await Promise.all([
1581
1995
  findJsonlFiles(sessionsDir),
1582
1996
  findJsonlFiles(archivedDir)
1583
1997
  ]);
1584
1998
  const jsonlFiles = [...sessionFiles, ...archivedFiles];
1585
1999
  if (jsonlFiles.length === 0) {
2000
+ await updateUsageCache(options, (cache) => {
2001
+ for (const [key, record] of Object.entries(cache.entries)) {
2002
+ if (record.kind === CODEX_JSONL_CACHE_KIND && (isPathWithin2(record.fingerprint.path, sessionsDir) || isPathWithin2(record.fingerprint.path, archivedDir))) {
2003
+ delete cache.entries[key];
2004
+ }
2005
+ }
2006
+ });
1586
2007
  return [];
1587
2008
  }
2009
+ const persistentCache = await readUsageCacheForLookup(options);
2010
+ const retainedKeys = /* @__PURE__ */ new Set();
2011
+ const updates = [];
1588
2012
  const parsedFiles = await mapWithConcurrency(
1589
2013
  jsonlFiles,
1590
2014
  FILE_PARSE_CONCURRENCY2,
1591
- (file) => parseSessionFile(file, options.families)
2015
+ async (file) => {
2016
+ const fingerprint = await getSourceFingerprint(file);
2017
+ if (!fingerprint) return null;
2018
+ const key = getPersistentCacheKey2(CODEX_JSONL_CACHE_KIND, file);
2019
+ retainedKeys.add(key);
2020
+ const cached2 = getCachedUsageValue(persistentCache, key, fingerprint);
2021
+ if (cached2) {
2022
+ const parsed2 = rehydrateParsedCodexFile(cached2);
2023
+ return filterParsedCodexFileByFamilies(parsed2, options.families);
2024
+ }
2025
+ const parsed = await parseSessionFile(file, options.families);
2026
+ if (parsed) {
2027
+ const fullParsed = options.families && options.families.length > 0 ? await parseSessionFile(file) : parsed;
2028
+ if (fullParsed) {
2029
+ updates.push({ key, fingerprint, value: toCachedParsedCodexFile(fullParsed) });
2030
+ }
2031
+ }
2032
+ return parsed;
2033
+ }
1592
2034
  );
2035
+ await updateUsageCache(options, (cache) => {
2036
+ for (const [key, record] of Object.entries(cache.entries)) {
2037
+ if (record.kind === CODEX_JSONL_CACHE_KIND && (isPathWithin2(record.fingerprint.path, sessionsDir) || isPathWithin2(record.fingerprint.path, archivedDir)) && !retainedKeys.has(key)) {
2038
+ delete cache.entries[key];
2039
+ }
2040
+ }
2041
+ for (const update of updates) {
2042
+ setCachedUsageValue(cache, update.key, CODEX_JSONL_CACHE_KIND, update.fingerprint, update.value);
2043
+ }
2044
+ });
1593
2045
  return parsedFiles.filter((entry) => entry !== null);
1594
2046
  })();
1595
2047
  codexParseCache.set(cacheKey, loadPromise);
@@ -1690,8 +2142,8 @@ async function loadCodexStats(options = {}) {
1690
2142
  function normalizePresentableModelName(modelName) {
1691
2143
  return getModelDisplayName(modelName).trim();
1692
2144
  }
1693
- function normalizeEntryModels(entries2) {
1694
- return entries2.map((entry) => ({
2145
+ function normalizeEntryModels(entries) {
2146
+ return entries.map((entry) => ({
1695
2147
  ...entry,
1696
2148
  model: normalizePresentableModelName(entry.model)
1697
2149
  }));
@@ -1703,9 +2155,9 @@ function toLocalDateString3(isoTimestamp) {
1703
2155
  const day = String(date.getDate()).padStart(2, "0");
1704
2156
  return `${year}-${month}-${day}`;
1705
2157
  }
1706
- async function parseClaudeJsonl(projectFilter, families) {
1707
- const entries2 = await collectAnthropicUsageEntries({ projectFilter, families });
1708
- return entries2.map((entry) => ({
2158
+ async function parseClaudeJsonl(projectFilter, families, cacheOptions = {}) {
2159
+ const entries = await collectAnthropicUsageEntries({ projectFilter, families, ...cacheOptions });
2160
+ return entries.map((entry) => ({
1709
2161
  date: entry.date,
1710
2162
  model: entry.model,
1711
2163
  rawModel: entry.rawModel,
@@ -1723,9 +2175,9 @@ async function parseClaudeJsonl(projectFilter, families) {
1723
2175
  timestamp: entry.timestamp
1724
2176
  }));
1725
2177
  }
1726
- async function parseCodexJsonl(families) {
1727
- const entries2 = await collectCodexUsageEntries({ families });
1728
- return entries2.map((entry) => ({
2178
+ async function parseCodexJsonl(families, cacheOptions = {}) {
2179
+ const entries = await collectCodexUsageEntries({ families, ...cacheOptions });
2180
+ return entries.map((entry) => ({
1729
2181
  date: entry.date,
1730
2182
  model: entry.model,
1731
2183
  rawModel: entry.rawModel,
@@ -1751,10 +2203,10 @@ function getSourceLabel(sourceKey, source) {
1751
2203
  if (key === "codex") return "Codex CLI";
1752
2204
  return key;
1753
2205
  }
1754
- function computeSessionCounts(entries2) {
2206
+ function computeSessionCounts(entries) {
1755
2207
  const mainSessions = /* @__PURE__ */ new Set();
1756
2208
  const subagentSessions = /* @__PURE__ */ new Set();
1757
- for (const entry of entries2) {
2209
+ for (const entry of entries) {
1758
2210
  const sourceKey = entry.sourceKey || entry.source;
1759
2211
  if (entry.sessionId) {
1760
2212
  mainSessions.add(`${sourceKey}:${entry.sessionId}`);
@@ -1769,8 +2221,8 @@ function computeSessionCounts(entries2) {
1769
2221
  total: mainSessions.size + subagentSessions.size
1770
2222
  };
1771
2223
  }
1772
- function filterByDateRange(entries2, since, until) {
1773
- return entries2.filter((e) => {
2224
+ function filterByDateRange(entries, since, until) {
2225
+ return entries.filter((e) => {
1774
2226
  if (since && e.date < since) return false;
1775
2227
  if (until && e.date > until) return false;
1776
2228
  return true;
@@ -1797,9 +2249,9 @@ function sortModelsByTier(models) {
1797
2249
  return priorityB - priorityA;
1798
2250
  });
1799
2251
  }
1800
- function aggregateByDay(entries2) {
2252
+ function aggregateByDay(entries) {
1801
2253
  const dayMap = /* @__PURE__ */ new Map();
1802
- for (const e of entries2) {
2254
+ for (const e of entries) {
1803
2255
  const entryTotal = e.inputTokens + e.outputTokens + e.cacheWriteTokens + e.cacheReadTokens;
1804
2256
  const existing = dayMap.get(e.date);
1805
2257
  if (existing) {
@@ -1828,9 +2280,9 @@ function aggregateByDay(entries2) {
1828
2280
  models: sortModelsByTier(Array.from(modelsSet))
1829
2281
  })).sort((a, b) => a.key.localeCompare(b.key));
1830
2282
  }
1831
- function aggregateByMonth(entries2) {
2283
+ function aggregateByMonth(entries) {
1832
2284
  const monthMap = /* @__PURE__ */ new Map();
1833
- for (const e of entries2) {
2285
+ for (const e of entries) {
1834
2286
  const month = e.date.slice(0, 7);
1835
2287
  const entryTotal = e.inputTokens + e.outputTokens + e.cacheWriteTokens + e.cacheReadTokens;
1836
2288
  const existing = monthMap.get(month);
@@ -1860,9 +2312,9 @@ function aggregateByMonth(entries2) {
1860
2312
  models: sortModelsByTier(Array.from(modelsSet))
1861
2313
  })).sort((a, b) => a.key.localeCompare(b.key));
1862
2314
  }
1863
- function aggregateByModel(entries2) {
2315
+ function aggregateByModel(entries) {
1864
2316
  const modelMap = /* @__PURE__ */ new Map();
1865
- for (const e of entries2) {
2317
+ for (const e of entries) {
1866
2318
  const entryTotal = e.inputTokens + e.outputTokens + e.cacheWriteTokens + e.cacheReadTokens;
1867
2319
  if (entryTotal <= 0) continue;
1868
2320
  const existing = modelMap.get(e.model);
@@ -1887,9 +2339,9 @@ function aggregateByModel(entries2) {
1887
2339
  }
1888
2340
  return Array.from(modelMap.values()).sort((a, b) => b.totalTokens - a.totalTokens);
1889
2341
  }
1890
- function aggregateBySession(entries2) {
2342
+ function aggregateBySession(entries) {
1891
2343
  const sessionMap = /* @__PURE__ */ new Map();
1892
- for (const e of entries2) {
2344
+ for (const e of entries) {
1893
2345
  const sourceKey = e.sourceKey || e.source;
1894
2346
  const sid = e.sessionId ? `${sourceKey}:${e.sessionId}` : "unknown";
1895
2347
  const entryTotal = e.inputTokens + e.outputTokens + e.cacheWriteTokens + e.cacheReadTokens;
@@ -1947,9 +2399,9 @@ function computeTotals(rows) {
1947
2399
  { inputTokens: 0, outputTokens: 0, cacheWriteTokens: 0, cacheReadTokens: 0, totalTokens: 0, cost: 0 }
1948
2400
  );
1949
2401
  }
1950
- function computeModelBreakdown(entries2) {
2402
+ function computeModelBreakdown(entries) {
1951
2403
  const modelMap = /* @__PURE__ */ new Map();
1952
- for (const e of entries2) {
2404
+ for (const e of entries) {
1953
2405
  const total = e.inputTokens + e.outputTokens + e.cacheWriteTokens + e.cacheReadTokens;
1954
2406
  if (total <= 0) continue;
1955
2407
  const existing = modelMap.get(e.model);
@@ -1968,10 +2420,10 @@ function computeModelBreakdown(entries2) {
1968
2420
  percentage: totalTokens > 0 ? Math.round(data.tokens / totalTokens * 100) : 0
1969
2421
  })).sort((a, b) => b.tokens - a.tokens);
1970
2422
  }
1971
- function computeSourceBreakdown(entries2) {
2423
+ function computeSourceBreakdown(entries) {
1972
2424
  const sourceMap = /* @__PURE__ */ new Map();
1973
2425
  const sessionsBySource = /* @__PURE__ */ new Map();
1974
- for (const entry of entries2) {
2426
+ for (const entry of entries) {
1975
2427
  const sourceKey = entry.sourceKey || entry.source;
1976
2428
  const label = getSourceLabel(entry.sourceKey, entry.source);
1977
2429
  const total = entry.inputTokens + entry.outputTokens + entry.cacheWriteTokens + entry.cacheReadTokens;
@@ -2001,9 +2453,9 @@ function computeSourceBreakdown(entries2) {
2001
2453
  percentage: totalTokens > 0 ? Math.round(source.tokens / totalTokens * 100) : 0
2002
2454
  })).sort((a, b) => b.tokens - a.tokens);
2003
2455
  }
2004
- function computeActivityStats(entries2, source) {
2456
+ function computeActivityStats(entries, source) {
2005
2457
  const dayMap = /* @__PURE__ */ new Map();
2006
- for (const entry of entries2) {
2458
+ for (const entry of entries) {
2007
2459
  const existing = dayMap.get(entry.date);
2008
2460
  if (existing) {
2009
2461
  existing.inputTokens += entry.inputTokens;
@@ -2026,7 +2478,7 @@ function computeActivityStats(entries2, source) {
2026
2478
  }
2027
2479
  }
2028
2480
  const sessionMap = /* @__PURE__ */ new Map();
2029
- for (const entry of entries2) {
2481
+ for (const entry of entries) {
2030
2482
  if (!entry.sessionId) continue;
2031
2483
  const sessions = sessionMap.get(entry.date) || /* @__PURE__ */ new Set();
2032
2484
  sessions.add(`${entry.source}:${entry.sessionId}`);
@@ -2065,33 +2517,34 @@ function computeActivityStats(entries2, source) {
2065
2517
  },
2066
2518
  days,
2067
2519
  totals,
2068
- modelBreakdown: computeModelBreakdown(entries2),
2069
- sessionCounts: computeSessionCounts(entries2),
2070
- sourceBreakdown: computeSourceBreakdown(entries2)
2520
+ modelBreakdown: computeModelBreakdown(entries),
2521
+ sessionCounts: computeSessionCounts(entries),
2522
+ sourceBreakdown: computeSourceBreakdown(entries)
2071
2523
  };
2072
2524
  }
2073
2525
  async function loadUsageStats(options) {
2074
2526
  const { aggregation, since, until, codexOnly, combined, projectFilter, families } = options;
2075
- let entries2 = [];
2527
+ const cacheOptions = { useCache: options.useCache, refreshCache: options.refreshCache };
2528
+ let entries = [];
2076
2529
  if (!codexOnly) {
2077
- entries2 = entries2.concat(await parseClaudeJsonl(projectFilter, families));
2530
+ entries = entries.concat(await parseClaudeJsonl(projectFilter, families, cacheOptions));
2078
2531
  }
2079
2532
  if (codexOnly || combined) {
2080
- entries2 = entries2.concat(await parseCodexJsonl(families));
2533
+ entries = entries.concat(await parseCodexJsonl(families, cacheOptions));
2081
2534
  }
2082
- if (entries2.length === 0) {
2535
+ if (entries.length === 0) {
2083
2536
  return null;
2084
2537
  }
2085
- entries2 = normalizeEntryModels(entries2);
2538
+ entries = normalizeEntryModels(entries);
2086
2539
  if (families && families.length > 0) {
2087
- entries2 = entries2.filter((entry) => matchesModelFamilies(entry.rawModel || entry.model, families));
2540
+ entries = entries.filter((entry) => matchesModelFamilies(entry.rawModel || entry.model, families));
2088
2541
  }
2089
- entries2 = filterByDateRange(entries2, since, until);
2090
- entries2 = entries2.filter((e) => !e.model.toLowerCase().includes("synthetic"));
2091
- if (entries2.length === 0) {
2542
+ entries = filterByDateRange(entries, since, until);
2543
+ entries = entries.filter((e) => !e.model.toLowerCase().includes("synthetic"));
2544
+ if (entries.length === 0) {
2092
2545
  return null;
2093
2546
  }
2094
- const dates = entries2.map((e) => e.date).sort();
2547
+ const dates = entries.map((e) => e.date).sort();
2095
2548
  const dateRange = {
2096
2549
  start: dates[0],
2097
2550
  end: dates[dates.length - 1]
@@ -2099,26 +2552,26 @@ async function loadUsageStats(options) {
2099
2552
  let rows;
2100
2553
  switch (aggregation) {
2101
2554
  case "monthly":
2102
- rows = aggregateByMonth(entries2);
2555
+ rows = aggregateByMonth(entries);
2103
2556
  break;
2104
2557
  case "model":
2105
- rows = aggregateByModel(entries2);
2558
+ rows = aggregateByModel(entries);
2106
2559
  break;
2107
2560
  case "session":
2108
- rows = aggregateBySession(entries2);
2561
+ rows = aggregateBySession(entries);
2109
2562
  break;
2110
2563
  case "total":
2111
2564
  rows = [];
2112
2565
  break;
2113
2566
  case "daily":
2114
2567
  default:
2115
- rows = aggregateByDay(entries2);
2568
+ rows = aggregateByDay(entries);
2116
2569
  break;
2117
2570
  }
2118
- const totals = aggregation === "total" ? computeTotals(aggregateByDay(entries2)) : computeTotals(rows);
2119
- const modelBreakdown = computeModelBreakdown(entries2);
2120
- const sessionCounts = computeSessionCounts(entries2);
2121
- const sourceBreakdown = computeSourceBreakdown(entries2);
2571
+ const totals = aggregation === "total" ? computeTotals(aggregateByDay(entries)) : computeTotals(rows);
2572
+ const modelBreakdown = computeModelBreakdown(entries);
2573
+ const sessionCounts = computeSessionCounts(entries);
2574
+ const sourceBreakdown = computeSourceBreakdown(entries);
2122
2575
  let source = "claude";
2123
2576
  if (codexOnly) source = "codex";
2124
2577
  else if (combined) source = "combined";
@@ -2136,29 +2589,30 @@ async function loadUsageStats(options) {
2136
2589
  }
2137
2590
  async function loadActivityStats(options) {
2138
2591
  const { since, until, codexOnly, combined, projectFilter, families } = options;
2139
- let entries2 = [];
2592
+ const cacheOptions = { useCache: options.useCache, refreshCache: options.refreshCache };
2593
+ let entries = [];
2140
2594
  if (!codexOnly) {
2141
- entries2 = entries2.concat(await parseClaudeJsonl(projectFilter, families));
2595
+ entries = entries.concat(await parseClaudeJsonl(projectFilter, families, cacheOptions));
2142
2596
  }
2143
2597
  if (codexOnly || combined) {
2144
- entries2 = entries2.concat(await parseCodexJsonl(families));
2598
+ entries = entries.concat(await parseCodexJsonl(families, cacheOptions));
2145
2599
  }
2146
- if (entries2.length === 0) {
2600
+ if (entries.length === 0) {
2147
2601
  return null;
2148
2602
  }
2149
- entries2 = normalizeEntryModels(entries2);
2603
+ entries = normalizeEntryModels(entries);
2150
2604
  if (families && families.length > 0) {
2151
- entries2 = entries2.filter((entry) => matchesModelFamilies(entry.rawModel || entry.model, families));
2605
+ entries = entries.filter((entry) => matchesModelFamilies(entry.rawModel || entry.model, families));
2152
2606
  }
2153
- entries2 = filterByDateRange(entries2, since, until);
2154
- entries2 = entries2.filter((entry) => !entry.model.toLowerCase().includes("synthetic"));
2155
- if (entries2.length === 0) {
2607
+ entries = filterByDateRange(entries, since, until);
2608
+ entries = entries.filter((entry) => !entry.model.toLowerCase().includes("synthetic"));
2609
+ if (entries.length === 0) {
2156
2610
  return null;
2157
2611
  }
2158
2612
  let source = "claude";
2159
2613
  if (codexOnly) source = "codex";
2160
2614
  else if (combined) source = "combined";
2161
- const stats = computeActivityStats(entries2, source);
2615
+ const stats = computeActivityStats(entries, source);
2162
2616
  stats.scopeLabel = formatModelFamilyLabel(families);
2163
2617
  return stats;
2164
2618
  }
@@ -2428,12 +2882,20 @@ async function loadData(options) {
2428
2882
  let codex = null;
2429
2883
  if (!codexOnly) {
2430
2884
  if (await claudeCompatibleDataExists()) {
2431
- claude = await loadClaudeStatsFromJsonl({ families });
2885
+ claude = await loadClaudeStatsFromJsonl({
2886
+ families,
2887
+ useCache: options.useCache,
2888
+ refreshCache: options.refreshCache
2889
+ });
2432
2890
  }
2433
2891
  }
2434
2892
  if (codexOnly || combined) {
2435
2893
  if (await codexDataExists()) {
2436
- codex = await loadCodexStats({ families });
2894
+ codex = await loadCodexStats({
2895
+ families,
2896
+ useCache: options.useCache,
2897
+ refreshCache: options.refreshCache
2898
+ });
2437
2899
  }
2438
2900
  }
2439
2901
  let source = "claude";
@@ -3052,15 +3514,15 @@ async function publishArtifact(artifact, baseUrl, legacyUrl) {
3052
3514
  }
3053
3515
 
3054
3516
  // src/claude-inspector.ts
3055
- import { promises as fs3 } from "fs";
3056
- import { homedir as homedir3 } from "os";
3057
- import { join as join3 } from "path";
3517
+ import { promises as fs4 } from "fs";
3518
+ import { homedir as homedir4 } from "os";
3519
+ import { join as join4 } from "path";
3058
3520
  function getClaudeDir2() {
3059
- return process.env.CLAUDE_HOME || join3(homedir3(), ".claude");
3521
+ return process.env.CLAUDE_HOME || join4(homedir4(), ".claude");
3060
3522
  }
3061
3523
  async function pathExists3(path) {
3062
3524
  try {
3063
- await fs3.access(path);
3525
+ await fs4.access(path);
3064
3526
  return true;
3065
3527
  } catch {
3066
3528
  return false;
@@ -3068,7 +3530,7 @@ async function pathExists3(path) {
3068
3530
  }
3069
3531
  async function readJsonFile(path) {
3070
3532
  try {
3071
- const content = await fs3.readFile(path, "utf-8");
3533
+ const content = await fs4.readFile(path, "utf-8");
3072
3534
  return JSON.parse(content);
3073
3535
  } catch {
3074
3536
  return null;
@@ -3076,17 +3538,17 @@ async function readJsonFile(path) {
3076
3538
  }
3077
3539
  async function walkFiles(dir, visitor) {
3078
3540
  if (!await pathExists3(dir)) return;
3079
- let entries2;
3541
+ let entries;
3080
3542
  try {
3081
- entries2 = await fs3.readdir(dir);
3543
+ entries = await fs4.readdir(dir);
3082
3544
  } catch {
3083
3545
  return;
3084
3546
  }
3085
- for (const entry of entries2) {
3086
- const fullPath = join3(dir, entry);
3547
+ for (const entry of entries) {
3548
+ const fullPath = join4(dir, entry);
3087
3549
  let stat;
3088
3550
  try {
3089
- stat = await fs3.stat(fullPath);
3551
+ stat = await fs4.stat(fullPath);
3090
3552
  } catch {
3091
3553
  continue;
3092
3554
  }
@@ -3104,8 +3566,8 @@ function daysSince(dateStr) {
3104
3566
  const diffMs = Date.now() - parsed.getTime();
3105
3567
  return Math.max(0, Math.floor(diffMs / (24 * 60 * 60 * 1e3)));
3106
3568
  }
3107
- async function inspectClaudeSystem(home = homedir3()) {
3108
- const path = join3(home, ".claude.json");
3569
+ async function inspectClaudeSystem(home = homedir4()) {
3570
+ const path = join4(home, ".claude.json");
3109
3571
  const json = await readJsonFile(path);
3110
3572
  return {
3111
3573
  path,
@@ -3120,18 +3582,18 @@ async function inspectClaudeSystem(home = homedir3()) {
3120
3582
  };
3121
3583
  }
3122
3584
  async function inspectClaudeUsage(claudeDir = getClaudeDir2()) {
3123
- const statsCachePath = join3(claudeDir, "stats-cache.json");
3585
+ const statsCachePath = join4(claudeDir, "stats-cache.json");
3124
3586
  const statsCache = await readJsonFile(statsCachePath);
3125
- const facetsDir = join3(claudeDir, "usage-data", "facets");
3126
- const sessionMetaDir = join3(claudeDir, "usage-data", "session-meta");
3127
- const projectsDir = join3(claudeDir, "projects");
3128
- const facetFiles = await pathExists3(facetsDir) ? (await fs3.readdir(facetsDir)).filter((file) => file.endsWith(".json")) : [];
3129
- const sessionMetaFiles = await pathExists3(sessionMetaDir) ? (await fs3.readdir(sessionMetaDir)).filter((file) => file.endsWith(".json")) : [];
3587
+ const facetsDir = join4(claudeDir, "usage-data", "facets");
3588
+ const sessionMetaDir = join4(claudeDir, "usage-data", "session-meta");
3589
+ const projectsDir = join4(claudeDir, "projects");
3590
+ const facetFiles = await pathExists3(facetsDir) ? (await fs4.readdir(facetsDir)).filter((file) => file.endsWith(".json")) : [];
3591
+ const sessionMetaFiles = await pathExists3(sessionMetaDir) ? (await fs4.readdir(sessionMetaDir)).filter((file) => file.endsWith(".json")) : [];
3130
3592
  let rateLimitHit = 0;
3131
3593
  let apiTokenLimit = 0;
3132
3594
  let contextLimit = 0;
3133
3595
  for (const file of facetFiles) {
3134
- const json = await readJsonFile(join3(facetsDir, file));
3596
+ const json = await readJsonFile(join4(facetsDir, file));
3135
3597
  const friction = json?.friction_counts || {};
3136
3598
  rateLimitHit += friction.rate_limit_hit || 0;
3137
3599
  apiTokenLimit += friction.api_token_limit || 0;
@@ -3141,7 +3603,7 @@ async function inspectClaudeUsage(claudeDir = getClaudeDir2()) {
3141
3603
  let inputTokens = 0;
3142
3604
  let outputTokens = 0;
3143
3605
  for (const file of sessionMetaFiles) {
3144
- const json = await readJsonFile(join3(sessionMetaDir, file));
3606
+ const json = await readJsonFile(join4(sessionMetaDir, file));
3145
3607
  if (!json) continue;
3146
3608
  if (json.uses_task_agent) sessionsUsingTaskAgent += 1;
3147
3609
  inputTokens += json.input_tokens || 0;
@@ -3216,7 +3678,9 @@ var USAGE_COMMAND_FLAGS = /* @__PURE__ */ new Set([
3216
3678
  "metric",
3217
3679
  "model",
3218
3680
  "monthly",
3681
+ "no-cache",
3219
3682
  "project",
3683
+ "refresh-cache",
3220
3684
  "sessions",
3221
3685
  "share",
3222
3686
  "since",
@@ -3241,9 +3705,11 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
3241
3705
  "minimax",
3242
3706
  "model",
3243
3707
  "monthly",
3708
+ "no-cache",
3244
3709
  "no-short",
3245
3710
  "project",
3246
3711
  "quiet",
3712
+ "refresh-cache",
3247
3713
  "sessions",
3248
3714
  "share",
3249
3715
  "total",
@@ -3390,7 +3856,7 @@ function applyCliIntent(args, intent) {
3390
3856
  // src/limits/claude.ts
3391
3857
  import { execFile as execFile2 } from "child_process";
3392
3858
  import { randomUUID } from "crypto";
3393
- import { dirname } from "path";
3859
+ import { dirname as dirname2 } from "path";
3394
3860
  import { fileURLToPath } from "url";
3395
3861
 
3396
3862
  // src/limits/pace.ts
@@ -3760,13 +4226,13 @@ function execFileCommand(command, args, options = {}) {
3760
4226
  );
3761
4227
  });
3762
4228
  }
3763
- function sleep(ms) {
4229
+ function sleep2(ms) {
3764
4230
  return new Promise((resolve) => setTimeout(resolve, ms));
3765
4231
  }
3766
4232
  function resolveDefaultClaudeUsageCwd() {
3767
4233
  const envCwd = process.env.VIBESTATS_CLAUDE_USAGE_CWD ?? process.env.VIBESTATS_USAGE_CWD;
3768
4234
  if (envCwd?.trim()) return envCwd;
3769
- return dirname(fileURLToPath(import.meta.url));
4235
+ return dirname2(fileURLToPath(import.meta.url));
3770
4236
  }
3771
4237
  async function captureUsageWithTmux(options) {
3772
4238
  const sessionName = `${TMUX_SESSION_PREFIX}-${randomUUID().replaceAll("-", "").slice(0, 16)}`;
@@ -3834,7 +4300,7 @@ async function fetchClaudeLimits(options = {}) {
3834
4300
  timeoutMs: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
3835
4301
  now,
3836
4302
  runCommand: options.runCommand ?? execFileCommand,
3837
- sleepFn: options.sleepFn ?? sleep
4303
+ sleepFn: options.sleepFn ?? sleep2
3838
4304
  });
3839
4305
  } catch (error) {
3840
4306
  return {
@@ -4291,9 +4757,9 @@ function displayUsageLimits(limits, options = {}) {
4291
4757
 
4292
4758
  // src/config.ts
4293
4759
  import { readFileSync, existsSync, writeFileSync } from "fs";
4294
- import { homedir as homedir4 } from "os";
4295
- import { join as join4 } from "path";
4296
- var CONFIG_PATH = join4(homedir4(), ".vibestats.json");
4760
+ import { homedir as homedir5 } from "os";
4761
+ import { join as join5 } from "path";
4762
+ var CONFIG_PATH = join5(homedir5(), ".vibestats.json");
4297
4763
  var DEFAULT_CONFIG = {
4298
4764
  baseUrl: "https://vibestats.wolfai.dev",
4299
4765
  outputFormat: "normal",
@@ -4381,6 +4847,24 @@ function getSelectedModelFamilies(args) {
4381
4847
  if (args.minimax === true) families.push("minimax");
4382
4848
  return families;
4383
4849
  }
4850
+ var cacheArgs = {
4851
+ "no-cache": {
4852
+ type: "boolean",
4853
+ description: "Bypass the persistent local usage cache for this run",
4854
+ default: false
4855
+ },
4856
+ "refresh-cache": {
4857
+ type: "boolean",
4858
+ description: "Rebuild the persistent local usage cache before showing stats",
4859
+ default: false
4860
+ }
4861
+ };
4862
+ function getUsageCacheOptions(args) {
4863
+ return {
4864
+ useCache: args["no-cache"] !== true,
4865
+ refreshCache: args["refresh-cache"] === true
4866
+ };
4867
+ }
4384
4868
 
4385
4869
  // src/index.ts
4386
4870
  function printCommandHelp(error) {
@@ -4539,7 +5023,7 @@ async function publishArtifactWithFallback(artifact, baseUrl, fallbackUrl, prefe
4539
5023
  var main = defineCommand({
4540
5024
  meta: {
4541
5025
  name: "vibestats",
4542
- version: "1.3.13",
5026
+ version: "1.3.14",
4543
5027
  description: "AI coding stats - usage tracking and annual wrapped for Claude Code & Codex"
4544
5028
  },
4545
5029
  args: {
@@ -4664,6 +5148,7 @@ var main = defineCommand({
4664
5148
  description: "Disable shortlink generation",
4665
5149
  default: false
4666
5150
  },
5151
+ ...cacheArgs,
4667
5152
  // Config management
4668
5153
  init: {
4669
5154
  type: "boolean",
@@ -4750,6 +5235,7 @@ async function runUsage(args, config) {
4750
5235
  else if (args.model) aggregation = "model";
4751
5236
  else if (args.total) aggregation = "total";
4752
5237
  const shouldShowSpinner = !args.json && !args.quiet;
5238
+ const cacheOptions = getUsageCacheOptions(args);
4753
5239
  const statsPromise = loadUsageStats({
4754
5240
  aggregation,
4755
5241
  since,
@@ -4758,7 +5244,8 @@ async function runUsage(args, config) {
4758
5244
  combined: args.combined,
4759
5245
  projectFilter: args.project ? process.cwd() : void 0,
4760
5246
  families,
4761
- scopeLabel
5247
+ scopeLabel,
5248
+ ...cacheOptions
4762
5249
  });
4763
5250
  const stats = shouldShowSpinner ? await createSpinner("Loading vibestats...").whilePromise(statsPromise) : await statsPromise;
4764
5251
  if (!stats) {
@@ -4865,14 +5352,15 @@ async function runWrapped(args, config) {
4865
5352
  const scopeLabel = formatModelFamilyLabel(families);
4866
5353
  const metric = parseActivityMetric(args.metric);
4867
5354
  const days = parseActivityDays(args.days);
5355
+ const cacheOptions = getUsageCacheOptions(args);
4868
5356
  const spinner = createSpinner("Preparing wrapped...");
4869
5357
  const [data, activityStats] = await spinner.whilePromise(
4870
5358
  Promise.all([
4871
- loadData({ codexOnly: args.codex, combined: args.combined, families, scopeLabel }),
4872
- loadActivityStats({ codexOnly: args.codex, combined: args.combined, families, scopeLabel })
5359
+ loadData({ codexOnly: args.codex, combined: args.combined, families, scopeLabel, ...cacheOptions }),
5360
+ loadActivityStats({ codexOnly: args.codex, combined: args.combined, families, scopeLabel, ...cacheOptions })
4873
5361
  ])
4874
5362
  );
4875
- validateData(data, { codexOnly: args.codex, combined: args.combined, families, scopeLabel });
5363
+ validateData(data, { codexOnly: args.codex, combined: args.combined, families, scopeLabel, ...cacheOptions });
4876
5364
  let claudeStats = null;
4877
5365
  let codexStats = null;
4878
5366
  if (data.claude) {
@@ -4933,6 +5421,7 @@ async function runActivity(args, config) {
4933
5421
  const metric = parseActivityMetric(args.metric);
4934
5422
  const days = parseActivityDays(args.days);
4935
5423
  const spinner = createSpinner("Preparing activity graph...");
5424
+ const cacheOptions = getUsageCacheOptions(args);
4936
5425
  const stats = await spinner.whilePromise(
4937
5426
  loadActivityStats({
4938
5427
  codexOnly: args.codex,
@@ -4941,7 +5430,8 @@ async function runActivity(args, config) {
4941
5430
  since: args.since,
4942
5431
  until: args.until,
4943
5432
  families,
4944
- scopeLabel
5433
+ scopeLabel,
5434
+ ...cacheOptions
4945
5435
  })
4946
5436
  );
4947
5437
  if (!stats) {