vibestats 1.3.7 → 1.3.8

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 +376 -30
  2. package/package.json +8 -7
package/dist/index.js CHANGED
@@ -788,7 +788,7 @@ function getClaudeDir() {
788
788
  return process.env.CLAUDE_HOME || join(homedir(), ".claude");
789
789
  }
790
790
  function getCodexDir() {
791
- return join(homedir(), ".codex");
791
+ return process.env.CODEX_HOME || join(homedir(), ".codex");
792
792
  }
793
793
  async function pathExists(path) {
794
794
  try {
@@ -861,11 +861,12 @@ async function parseClaudeJsonl(projectFilter) {
861
861
  }
862
862
  const jsonlFiles = await findJsonlFiles(searchDir);
863
863
  for (const filePath of jsonlFiles) {
864
- const sessionId = basename(filePath, ".jsonl");
865
864
  try {
866
865
  const content = await fs.readFile(filePath, "utf-8");
867
866
  if (!content.trim()) continue;
868
867
  const lines = content.split("\n");
868
+ const fallbackSessionId = basename(filePath, ".jsonl");
869
+ const isSubagentFile = filePath.includes("/subagents/");
869
870
  for (const line of lines) {
870
871
  if (!line.trim()) continue;
871
872
  try {
@@ -880,6 +881,8 @@ async function parseClaudeJsonl(projectFilter) {
880
881
  const model = entry.message.model || "unknown";
881
882
  const timestamp = entry.timestamp;
882
883
  if (!timestamp) continue;
884
+ const sessionId = entry.sessionId || fallbackSessionId;
885
+ const sessionKind = entry.isSidechain === true || isSubagentFile ? "subagent" : "main";
883
886
  const date = toLocalDateString(timestamp);
884
887
  const pricing = getModelPricing(model);
885
888
  const inputTokens = usage.input_tokens || 0;
@@ -898,6 +901,8 @@ async function parseClaudeJsonl(projectFilter) {
898
901
  messageCount: 1,
899
902
  source: "claude",
900
903
  sessionId,
904
+ sessionKind,
905
+ subagentId: sessionKind === "subagent" ? filePath : void 0,
901
906
  timestamp
902
907
  });
903
908
  } catch {
@@ -924,10 +929,22 @@ async function parseCodexJsonl() {
924
929
  const content = await fs.readFile(filePath, "utf-8");
925
930
  const lines = content.split("\n");
926
931
  let currentModel = "gpt-5";
932
+ let fileSessionId = basename(filePath, ".jsonl");
933
+ let canonicalSessionId = fileSessionId;
934
+ let sessionKind = "main";
927
935
  for (const line of lines) {
928
936
  if (!line.trim()) continue;
929
937
  try {
930
938
  const entry = JSON.parse(line);
939
+ if (entry.type === "session_meta") {
940
+ const payload = entry.payload || {};
941
+ const payloadSessionId = typeof payload.id === "string" ? payload.id : fileSessionId;
942
+ const parentThreadId = typeof payload.source?.subagent?.thread_spawn?.parent_thread_id === "string" ? payload.source.subagent.thread_spawn.parent_thread_id : null;
943
+ fileSessionId = payloadSessionId;
944
+ canonicalSessionId = parentThreadId || payloadSessionId;
945
+ sessionKind = parentThreadId ? "subagent" : "main";
946
+ continue;
947
+ }
931
948
  if (entry.type === "turn_context" && entry.payload?.model) {
932
949
  currentModel = entry.payload.model;
933
950
  continue;
@@ -954,7 +971,9 @@ async function parseCodexJsonl() {
954
971
  cost,
955
972
  messageCount: 1,
956
973
  source: "codex",
957
- sessionId: basename(filePath, ".jsonl"),
974
+ sessionId: canonicalSessionId,
975
+ sessionKind,
976
+ subagentId: sessionKind === "subagent" ? fileSessionId : void 0,
958
977
  timestamp
959
978
  });
960
979
  }
@@ -966,6 +985,23 @@ async function parseCodexJsonl() {
966
985
  }
967
986
  return entries;
968
987
  }
988
+ function computeSessionCounts(entries) {
989
+ const mainSessions = /* @__PURE__ */ new Set();
990
+ const subagentSessions = /* @__PURE__ */ new Set();
991
+ for (const entry of entries) {
992
+ if (entry.sessionId) {
993
+ mainSessions.add(`${entry.source}:${entry.sessionId}`);
994
+ }
995
+ if (entry.sessionKind === "subagent" && entry.subagentId) {
996
+ subagentSessions.add(`${entry.source}:${entry.subagentId}`);
997
+ }
998
+ }
999
+ return {
1000
+ main: mainSessions.size,
1001
+ subagent: subagentSessions.size,
1002
+ total: mainSessions.size + subagentSessions.size
1003
+ };
1004
+ }
969
1005
  function filterByDateRange(entries, since, until) {
970
1006
  return entries.filter((e) => {
971
1007
  if (since && e.date < since) return false;
@@ -1082,7 +1118,7 @@ function aggregateByModel(entries) {
1082
1118
  function aggregateBySession(entries) {
1083
1119
  const sessionMap = /* @__PURE__ */ new Map();
1084
1120
  for (const e of entries) {
1085
- const sid = e.sessionId || "unknown";
1121
+ const sid = e.sessionId ? `${e.source}:${e.sessionId}` : "unknown";
1086
1122
  const existing = sessionMap.get(sid);
1087
1123
  if (existing) {
1088
1124
  existing.inputTokens += e.inputTokens;
@@ -1097,6 +1133,7 @@ function aggregateBySession(entries) {
1097
1133
  }
1098
1134
  } else {
1099
1135
  sessionMap.set(sid, {
1136
+ displaySessionId: e.sessionId || sid,
1100
1137
  modelsSet: /* @__PURE__ */ new Set([e.model]),
1101
1138
  inputTokens: e.inputTokens,
1102
1139
  outputTokens: e.outputTokens,
@@ -1110,7 +1147,7 @@ function aggregateBySession(entries) {
1110
1147
  }
1111
1148
  return Array.from(sessionMap.entries()).filter(([, data]) => data.totalTokens > 0).map(([sessionId, data]) => {
1112
1149
  const date = data.firstTimestamp ? toLocalDateString(data.firstTimestamp) : "unknown";
1113
- const shortId = sessionId.slice(0, 12);
1150
+ const shortId = data.displaySessionId.slice(0, 12);
1114
1151
  return {
1115
1152
  key: `${date} ${shortId}`,
1116
1153
  inputTokens: data.inputTokens,
@@ -1167,9 +1204,6 @@ function computeActivityStats(entries, source) {
1167
1204
  existing.cacheReadTokens += entry.cacheReadTokens;
1168
1205
  existing.totalTokens += entry.inputTokens + entry.outputTokens + entry.cacheWriteTokens + entry.cacheReadTokens;
1169
1206
  existing.messageCount += entry.messageCount;
1170
- if (entry.sessionId) {
1171
- existing.sessionCount += 0;
1172
- }
1173
1207
  } else {
1174
1208
  dayMap.set(entry.date, {
1175
1209
  date: entry.date,
@@ -1187,7 +1221,7 @@ function computeActivityStats(entries, source) {
1187
1221
  for (const entry of entries) {
1188
1222
  if (!entry.sessionId) continue;
1189
1223
  const sessions = sessionMap.get(entry.date) || /* @__PURE__ */ new Set();
1190
- sessions.add(entry.sessionId);
1224
+ sessions.add(`${entry.source}:${entry.sessionId}`);
1191
1225
  sessionMap.set(entry.date, sessions);
1192
1226
  }
1193
1227
  const days = Array.from(dayMap.values()).map((day) => ({
@@ -1223,7 +1257,8 @@ function computeActivityStats(entries, source) {
1223
1257
  },
1224
1258
  days,
1225
1259
  totals,
1226
- modelBreakdown: computeModelBreakdown(entries)
1260
+ modelBreakdown: computeModelBreakdown(entries),
1261
+ sessionCounts: computeSessionCounts(entries)
1227
1262
  };
1228
1263
  }
1229
1264
  async function loadUsageStats(options) {
@@ -1269,6 +1304,7 @@ async function loadUsageStats(options) {
1269
1304
  }
1270
1305
  const totals = aggregation === "total" ? computeTotals(aggregateByDay(entries)) : computeTotals(rows);
1271
1306
  const modelBreakdown = computeModelBreakdown(entries);
1307
+ const sessionCounts = computeSessionCounts(entries);
1272
1308
  let source = "claude";
1273
1309
  if (codexOnly) source = "codex";
1274
1310
  else if (combined) source = "combined";
@@ -1278,7 +1314,8 @@ async function loadUsageStats(options) {
1278
1314
  source,
1279
1315
  aggregation,
1280
1316
  dateRange,
1281
- modelBreakdown
1317
+ modelBreakdown,
1318
+ sessionCounts
1282
1319
  };
1283
1320
  }
1284
1321
  async function loadActivityStats(options) {
@@ -1340,6 +1377,9 @@ function padLeft(str, width) {
1340
1377
  function padRight(str, width) {
1341
1378
  return str.padEnd(width);
1342
1379
  }
1380
+ function formatCount(n) {
1381
+ return n.toLocaleString("en-US");
1382
+ }
1343
1383
  function displayUsageTable(stats, options = {}) {
1344
1384
  const c = getColors(options.showColors);
1345
1385
  const { compact, hideCost } = options;
@@ -1352,6 +1392,9 @@ function displayUsageTable(stats, options = {}) {
1352
1392
  const aggLabel = stats.aggregation === "daily" ? "Daily" : stats.aggregation === "monthly" ? "Monthly" : stats.aggregation === "model" ? "By Model" : stats.aggregation === "session" ? "By Session" : "Total";
1353
1393
  console.log(`${c.orange}${c.bold}${title} - ${aggLabel} Report${c.reset}`);
1354
1394
  console.log(`${c.gray}Date range: ${stats.dateRange.start} to ${stats.dateRange.end}${c.reset}`);
1395
+ if (stats.sessionCounts) {
1396
+ console.log(`${c.gray}Tracked mix: ${formatCount(stats.sessionCounts.main)} main, ${formatCount(stats.sessionCounts.subagent)} subagents${c.reset}`);
1397
+ }
1355
1398
  console.log(`${c.gray}${"\u2500".repeat(Math.min(termWidth - 2, 120))}${c.reset}`);
1356
1399
  console.log();
1357
1400
  const isSession = stats.aggregation === "session";
@@ -1431,6 +1474,9 @@ function displayTotalOnly(stats, options = {}) {
1431
1474
  else if (stats.source === "combined") title = "AI Coding Usage (Claude + Codex)";
1432
1475
  console.log(`${c.orange}${c.bold}${title} - Summary${c.reset}`);
1433
1476
  console.log(`${c.gray}Date range: ${stats.dateRange.start} to ${stats.dateRange.end}${c.reset}`);
1477
+ if (stats.sessionCounts) {
1478
+ console.log(`${c.gray}Tracked mix: ${formatCount(stats.sessionCounts.main)} main, ${formatCount(stats.sessionCounts.subagent)} subagents${c.reset}`);
1479
+ }
1434
1480
  console.log(`${c.gray}${"\u2500".repeat(50)}${c.reset}`);
1435
1481
  console.log();
1436
1482
  const t = stats.totals;
@@ -1463,7 +1509,7 @@ function displayTotalOnly(stats, options = {}) {
1463
1509
  // src/claude-jsonl-loader.ts
1464
1510
  import { promises as fs2 } from "fs";
1465
1511
  import { homedir as homedir2 } from "os";
1466
- import { join as join2 } from "path";
1512
+ import { basename as basename2, join as join2 } from "path";
1467
1513
  function getClaudeDir2() {
1468
1514
  return process.env.CLAUDE_HOME || join2(homedir2(), ".claude");
1469
1515
  }
@@ -1521,6 +1567,9 @@ async function loadClaudeStatsFromJsonl() {
1521
1567
  }
1522
1568
  const modelUsage = {};
1523
1569
  const dailyMap = /* @__PURE__ */ new Map();
1570
+ const sessionsByDate = /* @__PURE__ */ new Map();
1571
+ const mainSessions = /* @__PURE__ */ new Set();
1572
+ const subagentSessions = /* @__PURE__ */ new Set();
1524
1573
  const hourCounts = {};
1525
1574
  let firstTimestamp = null;
1526
1575
  let totalMessages = 0;
@@ -1529,6 +1578,8 @@ async function loadClaudeStatsFromJsonl() {
1529
1578
  try {
1530
1579
  const content = await fs2.readFile(filePath, "utf-8");
1531
1580
  const lines = content.split("\n");
1581
+ const fallbackSessionId = basename2(filePath, ".jsonl");
1582
+ const isSubagentFile = filePath.includes("/subagents/");
1532
1583
  for (const line of lines) {
1533
1584
  if (!line.trim()) continue;
1534
1585
  try {
@@ -1539,6 +1590,8 @@ async function loadClaudeStatsFromJsonl() {
1539
1590
  const model = entry.message.model || "unknown";
1540
1591
  const timestamp = entry.timestamp;
1541
1592
  const messageId = entry.message.id;
1593
+ const sessionId = entry.sessionId || fallbackSessionId;
1594
+ const sessionKind = entry.isSidechain === true || isSubagentFile ? "subagent" : "main";
1542
1595
  if (!modelUsage[model]) {
1543
1596
  modelUsage[model] = {
1544
1597
  inputTokens: 0,
@@ -1562,8 +1615,15 @@ async function loadClaudeStatsFromJsonl() {
1562
1615
  }
1563
1616
  const daily = dailyMap.get(date);
1564
1617
  daily.messageCount++;
1618
+ const dateSessions = sessionsByDate.get(date) || /* @__PURE__ */ new Set();
1619
+ dateSessions.add(sessionId);
1620
+ sessionsByDate.set(date, dateSessions);
1565
1621
  hourCounts[hour] = (hourCounts[hour] || 0) + 1;
1566
1622
  }
1623
+ mainSessions.add(sessionId);
1624
+ if (sessionKind === "subagent") {
1625
+ subagentSessions.add(filePath);
1626
+ }
1567
1627
  if (timestamp && (!firstTimestamp || timestamp < firstTimestamp)) {
1568
1628
  firstTimestamp = timestamp;
1569
1629
  }
@@ -1592,18 +1652,21 @@ async function loadClaudeStatsFromJsonl() {
1592
1652
  const dailyActivity = Array.from(dailyMap.entries()).map(([date, data]) => ({
1593
1653
  date,
1594
1654
  messageCount: data.messageCount,
1595
- sessionCount: 1,
1655
+ sessionCount: sessionsByDate.get(date)?.size || 0,
1596
1656
  toolCallCount: data.toolCallCount
1597
1657
  })).sort((a, b) => a.date.localeCompare(b.date));
1598
- const sessionsByDate = /* @__PURE__ */ new Map();
1599
- void sessionsByDate;
1658
+ const sessionCounts = {
1659
+ main: mainSessions.size,
1660
+ subagent: subagentSessions.size,
1661
+ total: mainSessions.size + subagentSessions.size
1662
+ };
1600
1663
  return {
1601
1664
  version: 1,
1602
1665
  lastComputedDate: toLocalDateString2((/* @__PURE__ */ new Date()).toISOString()),
1603
1666
  dailyActivity,
1604
1667
  dailyModelTokens: [],
1605
1668
  modelUsage: statsCacheModelUsage,
1606
- totalSessions: jsonlFiles.length,
1669
+ totalSessions: sessionCounts.main,
1607
1670
  totalMessages,
1608
1671
  longestSession: {
1609
1672
  sessionId: "",
@@ -1612,7 +1675,8 @@ async function loadClaudeStatsFromJsonl() {
1612
1675
  timestamp: ""
1613
1676
  },
1614
1677
  firstSessionDate: firstTimestamp || (/* @__PURE__ */ new Date()).toISOString(),
1615
- hourCounts
1678
+ hourCounts,
1679
+ sessionCounts
1616
1680
  };
1617
1681
  }
1618
1682
 
@@ -1677,12 +1741,16 @@ async function parseSessionFile(filePath) {
1677
1741
  let sessionMeta = null;
1678
1742
  let currentModel = "gpt-5";
1679
1743
  const perModelUsage = {};
1744
+ let sessionKind = "main";
1745
+ let canonicalId = "";
1680
1746
  for (const line of lines) {
1681
1747
  if (!line.trim()) continue;
1682
1748
  try {
1683
1749
  const entry = JSON.parse(line);
1684
1750
  if (entry.type === "session_meta") {
1685
1751
  sessionMeta = entry.payload;
1752
+ canonicalId = sessionMeta.source?.subagent?.thread_spawn?.parent_thread_id || sessionMeta.id;
1753
+ sessionKind = sessionMeta.source?.subagent?.thread_spawn?.parent_thread_id ? "subagent" : "main";
1686
1754
  } else if (entry.type === "turn_context") {
1687
1755
  const payload = entry.payload;
1688
1756
  if (payload.model) {
@@ -1734,11 +1802,13 @@ async function parseSessionFile(filePath) {
1734
1802
  }
1735
1803
  return {
1736
1804
  id: sessionMeta.id,
1805
+ canonicalId: canonicalId || sessionMeta.id,
1737
1806
  timestamp: sessionMeta.timestamp,
1738
1807
  cwd: sessionMeta.cwd,
1739
1808
  model: primaryModel,
1740
1809
  tokenUsage: summedUsage,
1741
- perModelUsage
1810
+ perModelUsage,
1811
+ sessionKind
1742
1812
  };
1743
1813
  } catch {
1744
1814
  return null;
@@ -1771,7 +1841,13 @@ async function loadCodexStats() {
1771
1841
  }
1772
1842
  sessions.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
1773
1843
  const modelUsage = {};
1844
+ const mainSessions = /* @__PURE__ */ new Set();
1845
+ const subagentSessions = /* @__PURE__ */ new Set();
1774
1846
  for (const session of sessions) {
1847
+ mainSessions.add(session.canonicalId);
1848
+ if (session.sessionKind === "subagent") {
1849
+ subagentSessions.add(session.id);
1850
+ }
1775
1851
  if (session.perModelUsage) {
1776
1852
  for (const [model, usage] of Object.entries(session.perModelUsage)) {
1777
1853
  if (!modelUsage[model]) {
@@ -1805,23 +1881,36 @@ async function loadCodexStats() {
1805
1881
  const dailyMap = /* @__PURE__ */ new Map();
1806
1882
  for (const session of sessions) {
1807
1883
  const date = toLocalDateString3(session.timestamp);
1808
- dailyMap.set(date, (dailyMap.get(date) || 0) + 1);
1884
+ dailyMap.set(date, 0);
1809
1885
  }
1810
- const dailyActivity = Array.from(dailyMap.entries()).map(([date, sessionCount]) => ({ date, sessionCount })).sort((a, b) => a.date.localeCompare(b.date));
1886
+ const sessionsByDate = /* @__PURE__ */ new Map();
1887
+ for (const session of sessions) {
1888
+ const date = toLocalDateString3(session.timestamp);
1889
+ const dateSessions = sessionsByDate.get(date) || /* @__PURE__ */ new Set();
1890
+ dateSessions.add(session.canonicalId);
1891
+ sessionsByDate.set(date, dateSessions);
1892
+ }
1893
+ const dailyActivity = Array.from(dailyMap.entries()).map(([date]) => ({ date, sessionCount: sessionsByDate.get(date)?.size || 0 })).sort((a, b) => a.date.localeCompare(b.date));
1811
1894
  const hourCounts = {};
1812
1895
  for (const session of sessions) {
1813
1896
  const hour = new Date(session.timestamp).getHours().toString();
1814
1897
  hourCounts[hour] = (hourCounts[hour] || 0) + 1;
1815
1898
  }
1816
1899
  const totalMessages = sessions.length;
1900
+ const sessionCounts = {
1901
+ main: mainSessions.size,
1902
+ subagent: subagentSessions.size,
1903
+ total: mainSessions.size + subagentSessions.size
1904
+ };
1817
1905
  return {
1818
1906
  sessions,
1819
- totalSessions: sessions.length,
1907
+ totalSessions: sessionCounts.main,
1820
1908
  totalMessages,
1821
1909
  modelUsage,
1822
1910
  dailyActivity,
1823
1911
  hourCounts,
1824
- firstSessionDate: toLocalDateString3(sessions[0].timestamp)
1912
+ firstSessionDate: toLocalDateString3(sessions[0].timestamp),
1913
+ sessionCounts
1825
1914
  };
1826
1915
  }
1827
1916
 
@@ -2009,6 +2098,23 @@ function calculateWordsGenerated(modelUsage) {
2009
2098
  }
2010
2099
  return Math.round(totalOutputTokens * 0.75);
2011
2100
  }
2101
+ function mergeSessionCounts(...counts) {
2102
+ let main2 = 0;
2103
+ let subagent = 0;
2104
+ let found = false;
2105
+ for (const count of counts) {
2106
+ if (!count) continue;
2107
+ found = true;
2108
+ main2 += count.main;
2109
+ subagent += count.subagent;
2110
+ }
2111
+ if (!found) return void 0;
2112
+ return {
2113
+ main: main2,
2114
+ subagent,
2115
+ total: main2 + subagent
2116
+ };
2117
+ }
2012
2118
  function computeWrappedStats(cache) {
2013
2119
  let totalCost = 0;
2014
2120
  for (const [modelName, usage] of Object.entries(cache.modelUsage)) {
@@ -2038,7 +2144,8 @@ function computeWrappedStats(cache) {
2038
2144
  modelBreakdown,
2039
2145
  wordsGenerated,
2040
2146
  firstSessionDate: cache.firstSessionDate,
2041
- source: "claude"
2147
+ source: "claude",
2148
+ sessionCounts: cache.sessionCounts
2042
2149
  };
2043
2150
  }
2044
2151
  function computeCodexWrappedStats(cache) {
@@ -2122,7 +2229,8 @@ function computeCodexWrappedStats(cache) {
2122
2229
  modelBreakdown,
2123
2230
  wordsGenerated,
2124
2231
  firstSessionDate: cache.firstSessionDate,
2125
- source: "codex"
2232
+ source: "codex",
2233
+ sessionCounts: cache.sessionCounts
2126
2234
  };
2127
2235
  }
2128
2236
  function combineWrappedStats(claude, codex) {
@@ -2154,8 +2262,9 @@ function combineWrappedStats(claude, codex) {
2154
2262
  const peakHour = claude.sessions > codex.sessions ? claude.peakHour : codex.peakHour;
2155
2263
  const peakDay = claude.sessions > codex.sessions ? claude.peakDay : codex.peakDay;
2156
2264
  const busiestMonth = claude.sessions > codex.sessions ? claude.busiestMonth : codex.busiestMonth;
2265
+ const sessionCounts = mergeSessionCounts(claude.sessionCounts, codex.sessionCounts);
2157
2266
  return {
2158
- sessions: claude.sessions + codex.sessions,
2267
+ sessions: sessionCounts?.main ?? claude.sessions + codex.sessions,
2159
2268
  messages: claude.messages + codex.messages,
2160
2269
  totalTokens: claude.totalTokens + codex.totalTokens,
2161
2270
  totalCost: claude.totalCost + codex.totalCost,
@@ -2170,7 +2279,8 @@ function combineWrappedStats(claude, codex) {
2170
2279
  modelBreakdown: combinedBreakdown,
2171
2280
  wordsGenerated: claude.wordsGenerated + codex.wordsGenerated,
2172
2281
  firstSessionDate,
2173
- source: "combined"
2282
+ source: "combined",
2283
+ sessionCounts
2174
2284
  };
2175
2285
  }
2176
2286
 
@@ -2196,6 +2306,9 @@ function getColors2(theme) {
2196
2306
  }
2197
2307
  return colors2;
2198
2308
  }
2309
+ function formatCount2(n) {
2310
+ return n.toLocaleString("en-US");
2311
+ }
2199
2312
  function displayWrappedStats(stats, url, options) {
2200
2313
  const c = getColors2(options?.theme);
2201
2314
  console.log();
@@ -2224,6 +2337,11 @@ function displayWrappedStats(stats, url, options) {
2224
2337
  `${c.amber}${c.bold}${stats.daysActive}${c.reset} days active`
2225
2338
  ];
2226
2339
  console.log(` ${statsLine.join(" \u2502 ")}`);
2340
+ if (stats.sessionCounts) {
2341
+ console.log(
2342
+ ` ${c.dim}Tracked mix: ${formatCount2(stats.sessionCounts.main)} main, ${formatCount2(stats.sessionCounts.subagent)} subagents${c.reset}`
2343
+ );
2344
+ }
2227
2345
  console.log();
2228
2346
  console.log(`${c.bold}\u{1F525} Consistency${c.reset}`);
2229
2347
  console.log(`${c.gray}${"\u2500".repeat(60)}${c.reset}`);
@@ -2369,11 +2487,164 @@ async function publishArtifact(artifact, baseUrl, legacyUrl) {
2369
2487
  }
2370
2488
  }
2371
2489
 
2372
- // src/config.ts
2373
- import { readFileSync, existsSync, writeFileSync } from "fs";
2490
+ // src/claude-inspector.ts
2491
+ import { promises as fs4 } from "fs";
2374
2492
  import { homedir as homedir4 } from "os";
2375
2493
  import { join as join4 } from "path";
2376
- var CONFIG_PATH = join4(homedir4(), ".vibestats.json");
2494
+ function getClaudeDir3() {
2495
+ return process.env.CLAUDE_HOME || join4(homedir4(), ".claude");
2496
+ }
2497
+ async function pathExists4(path) {
2498
+ try {
2499
+ await fs4.access(path);
2500
+ return true;
2501
+ } catch {
2502
+ return false;
2503
+ }
2504
+ }
2505
+ async function readJsonFile(path) {
2506
+ try {
2507
+ const content = await fs4.readFile(path, "utf-8");
2508
+ return JSON.parse(content);
2509
+ } catch {
2510
+ return null;
2511
+ }
2512
+ }
2513
+ async function walkFiles(dir, visitor) {
2514
+ if (!await pathExists4(dir)) return;
2515
+ let entries;
2516
+ try {
2517
+ entries = await fs4.readdir(dir);
2518
+ } catch {
2519
+ return;
2520
+ }
2521
+ for (const entry of entries) {
2522
+ const fullPath = join4(dir, entry);
2523
+ let stat;
2524
+ try {
2525
+ stat = await fs4.stat(fullPath);
2526
+ } catch {
2527
+ continue;
2528
+ }
2529
+ if (stat.isDirectory()) {
2530
+ await walkFiles(fullPath, visitor);
2531
+ } else {
2532
+ await visitor(fullPath, entry);
2533
+ }
2534
+ }
2535
+ }
2536
+ function daysSince(dateStr) {
2537
+ if (!dateStr) return null;
2538
+ const parsed = /* @__PURE__ */ new Date(`${dateStr}T12:00:00`);
2539
+ if (Number.isNaN(parsed.getTime())) return null;
2540
+ const diffMs = Date.now() - parsed.getTime();
2541
+ return Math.max(0, Math.floor(diffMs / (24 * 60 * 60 * 1e3)));
2542
+ }
2543
+ async function inspectClaudeSystem(home = homedir4()) {
2544
+ const path = join4(home, ".claude.json");
2545
+ const json = await readJsonFile(path);
2546
+ return {
2547
+ path,
2548
+ exists: Boolean(json),
2549
+ oauthAccount: json?.oauthAccount || null,
2550
+ hasAvailableSubscription: json?.hasAvailableSubscription,
2551
+ cachedExtraUsageDisabledReason: json?.cachedExtraUsageDisabledReason,
2552
+ subscriptionNoticeCount: json?.subscriptionNoticeCount,
2553
+ toolUsageCount: json?.toolUsage ? Object.keys(json.toolUsage).length : void 0,
2554
+ skillUsageCount: json?.skillUsage ? Object.keys(json.skillUsage).length : void 0,
2555
+ projectCount: json?.projects ? Object.keys(json.projects).length : void 0
2556
+ };
2557
+ }
2558
+ async function inspectClaudeUsage(claudeDir = getClaudeDir3()) {
2559
+ const statsCachePath = join4(claudeDir, "stats-cache.json");
2560
+ const statsCache = await readJsonFile(statsCachePath);
2561
+ const facetsDir = join4(claudeDir, "usage-data", "facets");
2562
+ const sessionMetaDir = join4(claudeDir, "usage-data", "session-meta");
2563
+ const projectsDir = join4(claudeDir, "projects");
2564
+ const facetFiles = await pathExists4(facetsDir) ? (await fs4.readdir(facetsDir)).filter((file) => file.endsWith(".json")) : [];
2565
+ const sessionMetaFiles = await pathExists4(sessionMetaDir) ? (await fs4.readdir(sessionMetaDir)).filter((file) => file.endsWith(".json")) : [];
2566
+ let rateLimitHit = 0;
2567
+ let apiTokenLimit = 0;
2568
+ let contextLimit = 0;
2569
+ for (const file of facetFiles) {
2570
+ const json = await readJsonFile(join4(facetsDir, file));
2571
+ const friction = json?.friction_counts || {};
2572
+ rateLimitHit += friction.rate_limit_hit || 0;
2573
+ apiTokenLimit += friction.api_token_limit || 0;
2574
+ contextLimit += friction.context_limit || 0;
2575
+ }
2576
+ let sessionsUsingTaskAgent = 0;
2577
+ let inputTokens = 0;
2578
+ let outputTokens = 0;
2579
+ for (const file of sessionMetaFiles) {
2580
+ const json = await readJsonFile(join4(sessionMetaDir, file));
2581
+ if (!json) continue;
2582
+ if (json.uses_task_agent) sessionsUsingTaskAgent += 1;
2583
+ inputTokens += json.input_tokens || 0;
2584
+ outputTokens += json.output_tokens || 0;
2585
+ }
2586
+ let mainJsonlFiles = 0;
2587
+ let subagentJsonlFiles = 0;
2588
+ let sessionsIndexFiles = 0;
2589
+ let indexedSessionEntries = 0;
2590
+ await walkFiles(projectsDir, async (fullPath, entryName) => {
2591
+ if (entryName === "sessions-index.json") {
2592
+ sessionsIndexFiles += 1;
2593
+ const indexJson = await readJsonFile(fullPath);
2594
+ if (Array.isArray(indexJson)) {
2595
+ indexedSessionEntries += indexJson.length;
2596
+ } else if (Array.isArray(indexJson?.entries)) {
2597
+ indexedSessionEntries += indexJson.entries.length;
2598
+ }
2599
+ return;
2600
+ }
2601
+ if (!entryName.endsWith(".jsonl")) return;
2602
+ if (fullPath.includes("/subagents/")) {
2603
+ subagentJsonlFiles += 1;
2604
+ } else {
2605
+ mainJsonlFiles += 1;
2606
+ }
2607
+ });
2608
+ return {
2609
+ claudeDir,
2610
+ statsCache: {
2611
+ path: statsCachePath,
2612
+ exists: Boolean(statsCache),
2613
+ lastComputedDate: statsCache?.lastComputedDate,
2614
+ totalSessions: statsCache?.totalSessions,
2615
+ totalMessages: statsCache?.totalMessages,
2616
+ ageDays: daysSince(statsCache?.lastComputedDate)
2617
+ },
2618
+ usageData: {
2619
+ sessionMetaFiles: sessionMetaFiles.length,
2620
+ facetFiles: facetFiles.length
2621
+ },
2622
+ facets: {
2623
+ rateLimitHit,
2624
+ apiTokenLimit,
2625
+ contextLimit
2626
+ },
2627
+ sessionMeta: {
2628
+ sessionsUsingTaskAgent,
2629
+ taskAgentShare: sessionMetaFiles.length > 0 ? Number((sessionsUsingTaskAgent / sessionMetaFiles.length * 100).toFixed(1)) : 0,
2630
+ inputTokens,
2631
+ outputTokens,
2632
+ totalTokens: inputTokens + outputTokens
2633
+ },
2634
+ projects: {
2635
+ mainJsonlFiles,
2636
+ subagentJsonlFiles,
2637
+ sessionsIndexFiles,
2638
+ indexedSessionEntries
2639
+ }
2640
+ };
2641
+ }
2642
+
2643
+ // src/config.ts
2644
+ import { readFileSync, existsSync, writeFileSync } from "fs";
2645
+ import { homedir as homedir5 } from "os";
2646
+ import { join as join5 } from "path";
2647
+ var CONFIG_PATH = join5(homedir5(), ".vibestats.json");
2377
2648
  var DEFAULT_CONFIG = {
2378
2649
  baseUrl: "https://vibestats.wolfai.dev",
2379
2650
  outputFormat: "normal",
@@ -2579,7 +2850,7 @@ async function publishArtifactWithFallback(artifact, baseUrl, fallbackUrl, prefe
2579
2850
  var main = defineCommand({
2580
2851
  meta: {
2581
2852
  name: "vibestats",
2582
- version: "1.3.7",
2853
+ version: "1.3.8",
2583
2854
  description: "AI coding stats - usage tracking and annual wrapped for Claude Code & Codex"
2584
2855
  },
2585
2856
  args: {
@@ -2708,6 +2979,16 @@ var main = defineCommand({
2708
2979
  type: "boolean",
2709
2980
  description: "Show current config location and values",
2710
2981
  default: false
2982
+ },
2983
+ "claude-system": {
2984
+ type: "boolean",
2985
+ description: "Inspect ~/.claude.json account and app state",
2986
+ default: false
2987
+ },
2988
+ "claude-limits": {
2989
+ type: "boolean",
2990
+ description: "Inspect Claude usage-data, cache freshness, and local limit signals",
2991
+ default: false
2711
2992
  }
2712
2993
  },
2713
2994
  async run({ args }) {
@@ -2721,6 +3002,14 @@ var main = defineCommand({
2721
3002
  console.log(JSON.stringify(config, null, 2));
2722
3003
  return;
2723
3004
  }
3005
+ if (args["claude-system"]) {
3006
+ await runClaudeSystem(args);
3007
+ return;
3008
+ }
3009
+ if (args["claude-limits"]) {
3010
+ await runClaudeLimits(args);
3011
+ return;
3012
+ }
2724
3013
  if (args.activity) {
2725
3014
  await runActivity(args, config);
2726
3015
  } else if (args.wrapped) {
@@ -2942,10 +3231,67 @@ async function runActivity(args, config) {
2942
3231
  imageUrl: shared?.imageUrl || null
2943
3232
  });
2944
3233
  }
3234
+ async function runClaudeSystem(args) {
3235
+ const summary = await inspectClaudeSystem();
3236
+ if (args.json) {
3237
+ console.log(JSON.stringify(summary, null, 2));
3238
+ return;
3239
+ }
3240
+ console.log("Claude System");
3241
+ console.log(`Path: ${summary.path}`);
3242
+ console.log(`Exists: ${summary.exists ? "yes" : "no"}`);
3243
+ if (!summary.exists) return;
3244
+ console.log(`Billing type: ${summary.oauthAccount?.billingType || "unknown"}`);
3245
+ console.log(`Extra usage enabled: ${summary.oauthAccount?.hasExtraUsageEnabled === true ? "yes" : "no"}`);
3246
+ console.log(`Available subscription: ${summary.hasAvailableSubscription === true ? "yes" : "no"}`);
3247
+ console.log(`Extra usage disabled reason: ${summary.cachedExtraUsageDisabledReason || "none"}`);
3248
+ console.log(`Subscription notices shown: ${formatPlainNumber(summary.subscriptionNoticeCount)}`);
3249
+ console.log(`Tracked tools in root file: ${formatPlainNumber(summary.toolUsageCount)}`);
3250
+ console.log(`Tracked skills in root file: ${formatPlainNumber(summary.skillUsageCount)}`);
3251
+ console.log(`Tracked projects in root file: ${formatPlainNumber(summary.projectCount)}`);
3252
+ }
3253
+ async function runClaudeLimits(args) {
3254
+ const diagnostics = await inspectClaudeUsage();
3255
+ if (args.json) {
3256
+ console.log(JSON.stringify(diagnostics, null, 2));
3257
+ return;
3258
+ }
3259
+ console.log("Claude Limits");
3260
+ console.log(`Claude dir: ${diagnostics.claudeDir}`);
3261
+ console.log(`Stats cache: ${diagnostics.statsCache.path}`);
3262
+ console.log(
3263
+ `Stats cache freshness: ${diagnostics.statsCache.exists ? `${diagnostics.statsCache.lastComputedDate || "unknown"} (${formatPlainNumber(diagnostics.statsCache.ageDays ?? void 0)} days old)` : "missing"}`
3264
+ );
3265
+ console.log(
3266
+ `Stats cache totals: ${formatPlainNumber(diagnostics.statsCache.totalSessions)} sessions, ${formatPlainNumber(diagnostics.statsCache.totalMessages)} messages`
3267
+ );
3268
+ console.log(
3269
+ `Usage-data files: ${formatPlainNumber(diagnostics.usageData.sessionMetaFiles)} session-meta, ${formatPlainNumber(diagnostics.usageData.facetFiles)} facets`
3270
+ );
3271
+ console.log(
3272
+ `Facet limits: ${formatPlainNumber(diagnostics.facets.rateLimitHit)} rate-limit hits, ${formatPlainNumber(diagnostics.facets.apiTokenLimit)} api-token limits, ${formatPlainNumber(diagnostics.facets.contextLimit)} context limits`
3273
+ );
3274
+ console.log(
3275
+ `Task-agent share: ${formatPlainNumber(diagnostics.sessionMeta.sessionsUsingTaskAgent)} / ${formatPlainNumber(diagnostics.usageData.sessionMetaFiles)} sessions (${diagnostics.sessionMeta.taskAgentShare}%)`
3276
+ );
3277
+ console.log(
3278
+ `Session-meta tokens: ${formatNumber2(diagnostics.sessionMeta.totalTokens)} total (${formatNumber2(diagnostics.sessionMeta.inputTokens)} in, ${formatNumber2(diagnostics.sessionMeta.outputTokens)} out)`
3279
+ );
3280
+ console.log(
3281
+ `Project logs: ${formatPlainNumber(diagnostics.projects.mainJsonlFiles)} main JSONL files, ${formatPlainNumber(diagnostics.projects.subagentJsonlFiles)} subagent JSONL files`
3282
+ );
3283
+ console.log(
3284
+ `Session indexes: ${formatPlainNumber(diagnostics.projects.sessionsIndexFiles)} files, ${formatPlainNumber(diagnostics.projects.indexedSessionEntries)} indexed entries`
3285
+ );
3286
+ }
2945
3287
  function formatNumber2(n) {
2946
3288
  if (n >= 1e9) return `${(n / 1e9).toFixed(1)}B`;
2947
3289
  if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
2948
3290
  if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
2949
3291
  return n.toString();
2950
3292
  }
3293
+ function formatPlainNumber(n) {
3294
+ if (typeof n !== "number") return "unknown";
3295
+ return n.toLocaleString("en-US");
3296
+ }
2951
3297
  runMain(main);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibestats",
3
- "version": "1.3.7",
3
+ "version": "1.3.8",
4
4
  "description": "AI coding stats - usage tracking and annual wrapped for Claude Code & Codex",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -18,6 +18,12 @@
18
18
  "files": [
19
19
  "dist"
20
20
  ],
21
+ "scripts": {
22
+ "dev": "tsx src/index.ts",
23
+ "build": "tsup src/index.ts --format esm --dts --clean --shims",
24
+ "test": "tsx --test src/**/*.test.ts",
25
+ "prepublishOnly": "pnpm build"
26
+ },
21
27
  "keywords": [
22
28
  "claude",
23
29
  "claude-code",
@@ -45,10 +51,5 @@
45
51
  },
46
52
  "engines": {
47
53
  "node": ">=18.0.0"
48
- },
49
- "scripts": {
50
- "dev": "tsx src/index.ts",
51
- "build": "tsup src/index.ts --format esm --dts --clean --shims",
52
- "test": "tsx --test src/**/*.test.ts"
53
54
  }
54
- }
55
+ }