vibestats 1.3.8 → 1.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +25 -1
  2. package/dist/index.js +519 -298
  3. package/package.json +7 -8
package/README.md CHANGED
@@ -21,6 +21,10 @@ vibestats --total # Show only totals
21
21
 
22
22
  # Wrapped summary
23
23
  vibestats --wrapped # Annual wrapped summary
24
+
25
+ # Claude local diagnostics
26
+ vibestats --claude-system
27
+ vibestats --claude-limits
24
28
  ```
25
29
 
26
30
  ## CLI Flags
@@ -41,6 +45,8 @@ vibestats --wrapped # Annual wrapped summary
41
45
  | `--quiet`, `-q` | Quiet output (totals line) |
42
46
  | `--share`, `-s` | Generate a shareable usage URL |
43
47
  | `--project`, `-p` | Current project only (Claude Code) |
48
+ | `--claude-system` | Inspect `~/.claude.json` account and app state |
49
+ | `--claude-limits` | Inspect `~/.claude/usage-data`, cache freshness, and local limit signals |
44
50
 
45
51
  ### Wrapped Mode
46
52
 
@@ -90,6 +96,20 @@ Creates `~/.vibestats.json`:
90
96
  | Claude Code | `~/.claude/projects/**/*.jsonl` |
91
97
  | OpenAI Codex | `~/.codex/sessions/*.jsonl` |
92
98
 
99
+ Additional Claude diagnostic files:
100
+ - `~/.claude.json`
101
+ - `~/.claude/stats-cache.json`
102
+ - `~/.claude/usage-data/session-meta/*.json`
103
+ - `~/.claude/usage-data/facets/*.json`
104
+
105
+ ## Session Semantics
106
+
107
+ - `sessions` means canonical top-level sessions
108
+ - subagents are counted separately and shown as a compact session mix notice
109
+ - token and cost totals still include subagent usage
110
+ - Claude subagents are inferred from sidechain/session metadata
111
+ - Codex subagents are inferred from spawned-thread metadata
112
+
93
113
  ## Requirements
94
114
 
95
115
  - Node.js 18+
@@ -97,7 +117,11 @@ Creates `~/.vibestats.json`:
97
117
 
98
118
  ## View Online
99
119
 
100
- Visit [vibestats.wolfai.dev](https://vibestats.wolfai.dev) to view your wrapped in the browser.
120
+ Visit [vibestats.wolfai.dev](https://vibestats.wolfai.dev) to view shares in the browser.
121
+
122
+ - [vibestats.wolfai.dev/wrapped](https://vibestats.wolfai.dev/wrapped)
123
+ - [vibestats.wolfai.dev/activity](https://vibestats.wolfai.dev/activity)
124
+ - [vibestats.wolfai.dev/changelog](https://vibestats.wolfai.dev/changelog)
101
125
 
102
126
  ## License
103
127
 
package/dist/index.js CHANGED
@@ -635,7 +635,7 @@ function buildActivityGraph(stats, metric, requestedDays = 365) {
635
635
  };
636
636
  }
637
637
  function buildActivityTitle(source, metric) {
638
- const sourceLabel = source === "codex" ? "Codex" : source === "combined" ? "AI Coding" : "Claude";
638
+ const sourceLabel = source === "codex" ? "Codex" : source === "combined" ? "AI Coding" : "Local AI";
639
639
  const metricLabel = metric === "tokens" ? "Activity" : metric === "sessions" ? "Session Activity" : "Message Activity";
640
640
  return `${sourceLabel} ${metricLabel}`;
641
641
  }
@@ -661,12 +661,38 @@ function buildActivityArtifact(stats, metric, requestedDays = 365) {
661
661
  }
662
662
 
663
663
  // src/usage/loader.ts
664
+ import { promises as fs2 } from "fs";
665
+ import { homedir as homedir2 } from "os";
666
+ import { join as join2, basename as basename2 } from "path";
667
+
668
+ // src/anthropic-sources.ts
669
+ import { execFile } from "child_process";
664
670
  import { promises as fs } from "fs";
665
671
  import { homedir } from "os";
666
- import { join, basename } from "path";
672
+ import { basename, join } from "path";
673
+ import { promisify } from "util";
667
674
 
668
675
  // src/pricing.ts
669
676
  var MODEL_PRICING = {
677
+ // Fireworks router pricing as of 2026-03-30:
678
+ // https://fireworks.ai/pricing
679
+ // Fireworks exposes a single "cached input" rate, so cache creation
680
+ // tokens are treated as regular input and cache reads use the discounted rate.
681
+ "accounts/fireworks/routers/kimi-k2p5-turbo": {
682
+ input: 0.99,
683
+ output: 4.94,
684
+ cacheWrite: 0.99,
685
+ cacheRead: 0.16
686
+ },
687
+ // Fireworks prices the MiniMax M2 family at one rate. We apply that
688
+ // official family pricing to MiniMax-M2.7 because Fireworks does not
689
+ // publish a distinct M2.7 per-token row.
690
+ "MiniMax-M2.7": {
691
+ input: 0.3,
692
+ output: 1.2,
693
+ cacheWrite: 0.3,
694
+ cacheRead: 0.03
695
+ },
670
696
  // Opus 4.6 (same pricing as Opus 4.5)
671
697
  "claude-opus-4-6-20260101": {
672
698
  input: 5,
@@ -730,10 +756,17 @@ var MODEL_PRICING = {
730
756
  cacheRead: 0.1
731
757
  }
732
758
  };
733
- function getModelPricing(modelName) {
759
+ function tryGetModelPricing(modelName) {
734
760
  if (MODEL_PRICING[modelName]) {
735
761
  return MODEL_PRICING[modelName];
736
762
  }
763
+ const normalized = modelName.toLowerCase();
764
+ if (normalized.includes("kimi-k2p5-turbo")) {
765
+ return MODEL_PRICING["accounts/fireworks/routers/kimi-k2p5-turbo"];
766
+ }
767
+ if (normalized.includes("minimax-m2.7")) {
768
+ return MODEL_PRICING["MiniMax-M2.7"];
769
+ }
737
770
  if (modelName.includes("opus-4-6") || modelName.includes("opus-4.6")) {
738
771
  return MODEL_PRICING["claude-opus-4-6-20260101"];
739
772
  }
@@ -758,9 +791,16 @@ function getModelPricing(modelName) {
758
791
  if (modelName.includes("haiku")) {
759
792
  return MODEL_PRICING["claude-3-5-haiku-20241022"];
760
793
  }
761
- return MODEL_PRICING["claude-sonnet-4-5-20250929"];
794
+ return null;
762
795
  }
763
796
  function getModelDisplayName(modelName) {
797
+ const normalized = modelName.toLowerCase();
798
+ if (normalized.includes("accounts/fireworks/routers/kimi-k2p5-turbo") || normalized.includes("custom:kimi-k2.5-turbo")) {
799
+ return "Kimi K2.5 Turbo";
800
+ }
801
+ if (normalized.includes("minimax-m2.7") || normalized.includes("custom:minimax-m2.7")) {
802
+ return "MiniMax-M2.7";
803
+ }
764
804
  if (modelName.includes("opus-4-6") || modelName.includes("opus-4.6")) return "Opus 4.6";
765
805
  if (modelName.includes("opus-4-5") || modelName.includes("opus-4.5")) return "Opus 4.5";
766
806
  if (modelName.includes("opus-4-1") || modelName.includes("opus-4.1")) return "Opus 4.1";
@@ -775,8 +815,10 @@ function getModelDisplayName(modelName) {
775
815
  return modelName;
776
816
  }
777
817
 
778
- // src/usage/loader.ts
779
- var MAX_RECURSION_DEPTH = 10;
818
+ // src/anthropic-sources.ts
819
+ var execFileAsync = promisify(execFile);
820
+ var SQLITE_SEPARATOR = "";
821
+ var SQLITE_MAX_BUFFER = 64 * 1024 * 1024;
780
822
  function toLocalDateString(isoTimestamp) {
781
823
  const date = new Date(isoTimestamp);
782
824
  const year = date.getFullYear();
@@ -784,12 +826,6 @@ function toLocalDateString(isoTimestamp) {
784
826
  const day = String(date.getDate()).padStart(2, "0");
785
827
  return `${year}-${month}-${day}`;
786
828
  }
787
- function getClaudeDir() {
788
- return process.env.CLAUDE_HOME || join(homedir(), ".claude");
789
- }
790
- function getCodexDir() {
791
- return process.env.CODEX_HOME || join(homedir(), ".codex");
792
- }
793
829
  async function pathExists(path) {
794
830
  try {
795
831
  await fs.access(path);
@@ -805,9 +841,8 @@ async function safeRealpath(path) {
805
841
  return path;
806
842
  }
807
843
  }
808
- async function findJsonlFiles(dir, visited = /* @__PURE__ */ new Set(), depth = 0, result = []) {
844
+ async function findFiles(dir, matcher, visited = /* @__PURE__ */ new Set(), result = []) {
809
845
  if (!await pathExists(dir)) return result;
810
- if (depth > MAX_RECURSION_DEPTH) return result;
811
846
  const realPath = await safeRealpath(dir);
812
847
  if (visited.has(realPath)) return result;
813
848
  visited.add(realPath);
@@ -826,13 +861,37 @@ async function findJsonlFiles(dir, visited = /* @__PURE__ */ new Set(), depth =
826
861
  continue;
827
862
  }
828
863
  if (stat.isDirectory()) {
829
- await findJsonlFiles(fullPath, visited, depth + 1, result);
830
- } else if (entry.endsWith(".jsonl")) {
864
+ await findFiles(fullPath, matcher, visited, result);
865
+ } else if (matcher(entry)) {
831
866
  result.push(fullPath);
832
867
  }
833
868
  }
834
869
  return result;
835
870
  }
871
+ function normalizeAnthropicModelName(modelName) {
872
+ const trimmed = modelName.trim();
873
+ const normalized = trimmed.toLowerCase();
874
+ if (/^custom:minimax-m2\.7(?:-\d+)?$/.test(normalized)) {
875
+ return "MiniMax-M2.7";
876
+ }
877
+ if (/^custom:kimi-k2\.5-turbo(?:-\d+)?$/.test(normalized)) {
878
+ return "accounts/fireworks/routers/kimi-k2p5-turbo";
879
+ }
880
+ return trimmed;
881
+ }
882
+ function calculateKnownCost(modelName, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens, explicitCost) {
883
+ if (typeof explicitCost === "number" && explicitCost > 0) {
884
+ return explicitCost;
885
+ }
886
+ const pricing = tryGetModelPricing(modelName);
887
+ if (!pricing) {
888
+ return 0;
889
+ }
890
+ return inputTokens / 1e6 * pricing.input + outputTokens / 1e6 * pricing.output + cacheWriteTokens / 1e6 * pricing.cacheWrite + cacheReadTokens / 1e6 * pricing.cacheRead;
891
+ }
892
+ function entryTotalTokens(entry) {
893
+ return entry.inputTokens + entry.outputTokens + entry.cacheWriteTokens + entry.cacheReadTokens;
894
+ }
836
895
  async function resolveProjectDir(projectsDir, cwd) {
837
896
  let current = cwd;
838
897
  while (current && current !== "/") {
@@ -845,79 +904,413 @@ async function resolveProjectDir(projectsDir, cwd) {
845
904
  }
846
905
  return null;
847
906
  }
848
- async function parseClaudeJsonl(projectFilter) {
907
+ function getClaudeDir() {
908
+ return process.env.CLAUDE_HOME || join(homedir(), ".claude");
909
+ }
910
+ function getOpenCodeDbPath() {
911
+ if (process.env.OPENCODE_DB_PATH) {
912
+ return process.env.OPENCODE_DB_PATH;
913
+ }
914
+ return join(homedir(), ".local", "share", "opencode", "opencode.db");
915
+ }
916
+ function getFactorySessionsDir() {
917
+ if (process.env.FACTORY_SESSIONS_DIR) {
918
+ return process.env.FACTORY_SESSIONS_DIR;
919
+ }
920
+ return join(homedir(), ".factory", "sessions");
921
+ }
922
+ async function claudeCompatibleDataExists() {
923
+ const [claudeProjects, opencodeDb, factorySessions] = await Promise.all([
924
+ pathExists(join(getClaudeDir(), "projects")),
925
+ pathExists(getOpenCodeDbPath()),
926
+ pathExists(getFactorySessionsDir())
927
+ ]);
928
+ return claudeProjects || opencodeDb || factorySessions;
929
+ }
930
+ async function parseClaudeProjectEntries(projectFilter) {
849
931
  const entries = [];
850
- const seenMessageIds = /* @__PURE__ */ new Set();
851
932
  const claudeDir = getClaudeDir();
852
933
  const projectsDir = join(claudeDir, "projects");
853
934
  if (!await pathExists(projectsDir)) return entries;
854
- let searchDir;
935
+ let searchDir = projectsDir;
855
936
  if (projectFilter) {
856
937
  const resolved = await resolveProjectDir(projectsDir, projectFilter);
857
938
  if (!resolved) return entries;
858
939
  searchDir = resolved;
859
- } else {
860
- searchDir = projectsDir;
861
940
  }
862
- const jsonlFiles = await findJsonlFiles(searchDir);
941
+ const jsonlFiles = await findFiles(searchDir, (entry) => entry.endsWith(".jsonl"));
863
942
  for (const filePath of jsonlFiles) {
864
943
  try {
865
944
  const content = await fs.readFile(filePath, "utf-8");
866
- if (!content.trim()) continue;
867
945
  const lines = content.split("\n");
868
946
  const fallbackSessionId = basename(filePath, ".jsonl");
869
947
  const isSubagentFile = filePath.includes("/subagents/");
870
- for (const line of lines) {
948
+ const bestByMessageKey = /* @__PURE__ */ new Map();
949
+ for (let index = 0; index < lines.length; index++) {
950
+ const line = lines[index];
871
951
  if (!line.trim()) continue;
872
952
  try {
873
953
  const entry = JSON.parse(line);
874
- if (entry.type !== "assistant" || !entry.message?.usage) continue;
875
- const messageId = entry.message?.id;
876
- if (messageId) {
877
- if (seenMessageIds.has(messageId)) continue;
878
- seenMessageIds.add(messageId);
879
- }
880
- const usage = entry.message.usage;
881
- const model = entry.message.model || "unknown";
882
- const timestamp = entry.timestamp;
883
- if (!timestamp) continue;
954
+ if (entry.type !== "assistant" || !entry.message?.usage || !entry.timestamp) continue;
955
+ const rawModel = normalizeAnthropicModelName(entry.message.model || "unknown");
884
956
  const sessionId = entry.sessionId || fallbackSessionId;
885
957
  const sessionKind = entry.isSidechain === true || isSubagentFile ? "subagent" : "main";
886
- const date = toLocalDateString(timestamp);
887
- const pricing = getModelPricing(model);
888
- const inputTokens = usage.input_tokens || 0;
889
- const outputTokens = usage.output_tokens || 0;
890
- const cacheWriteTokens = usage.cache_creation_input_tokens || 0;
891
- const cacheReadTokens = usage.cache_read_input_tokens || 0;
892
- const cost = inputTokens * pricing.input / 1e6 + outputTokens * pricing.output / 1e6 + cacheWriteTokens * pricing.cacheWrite / 1e6 + cacheReadTokens * pricing.cacheRead / 1e6;
893
- entries.push({
894
- date,
895
- model: getModelDisplayName(model),
896
- inputTokens,
897
- outputTokens,
898
- cacheWriteTokens,
899
- cacheReadTokens,
900
- cost,
958
+ const usage = entry.message.usage;
959
+ const messageKey = entry.message.id ? `${sessionId}:${entry.message.id}` : `${filePath}:${index}`;
960
+ const candidate = {
961
+ date: toLocalDateString(entry.timestamp),
962
+ timestamp: entry.timestamp,
963
+ rawModel,
964
+ model: getModelDisplayName(rawModel),
965
+ inputTokens: usage.input_tokens || 0,
966
+ outputTokens: usage.output_tokens || 0,
967
+ cacheWriteTokens: usage.cache_creation_input_tokens || 0,
968
+ cacheReadTokens: usage.cache_read_input_tokens || 0,
969
+ cost: calculateKnownCost(
970
+ rawModel,
971
+ usage.input_tokens || 0,
972
+ usage.output_tokens || 0,
973
+ usage.cache_creation_input_tokens || 0,
974
+ usage.cache_read_input_tokens || 0
975
+ ),
901
976
  messageCount: 1,
902
977
  source: "claude",
978
+ sourceKey: "claude",
903
979
  sessionId,
904
980
  sessionKind,
905
- subagentId: sessionKind === "subagent" ? filePath : void 0,
906
- timestamp
907
- });
981
+ subagentId: sessionKind === "subagent" ? filePath : void 0
982
+ };
983
+ const existing = bestByMessageKey.get(messageKey);
984
+ if (!existing || entryTotalTokens(candidate) > entryTotalTokens(existing)) {
985
+ bestByMessageKey.set(messageKey, candidate);
986
+ }
908
987
  } catch {
909
988
  }
910
989
  }
990
+ entries.push(...bestByMessageKey.values());
911
991
  } catch {
912
992
  }
913
993
  }
914
994
  return entries;
915
995
  }
996
+ async function parseOpenCodeEntries() {
997
+ const dbPath = getOpenCodeDbPath();
998
+ if (!await pathExists(dbPath)) return [];
999
+ const sql = `
1000
+ SELECT
1001
+ COALESCE(m.id, ''),
1002
+ COALESCE(m.session_id, ''),
1003
+ COALESCE(s.parent_id, ''),
1004
+ COALESCE(json_extract(m.data, '$.modelID'), ''),
1005
+ COALESCE(json_extract(m.data, '$.time.created'), 0),
1006
+ COALESCE(json_extract(m.data, '$.tokens.input'), 0),
1007
+ COALESCE(json_extract(m.data, '$.tokens.output'), 0),
1008
+ COALESCE(json_extract(m.data, '$.tokens.cache.write'), 0),
1009
+ COALESCE(json_extract(m.data, '$.tokens.cache.read'), 0),
1010
+ COALESCE(json_extract(m.data, '$.cost'), 0)
1011
+ FROM message m
1012
+ JOIN session s ON s.id = m.session_id
1013
+ WHERE json_extract(m.data, '$.role') = 'assistant'
1014
+ AND json_extract(m.data, '$.modelID') IS NOT NULL;
1015
+ `;
1016
+ let stdout = "";
1017
+ try {
1018
+ const result = await execFileAsync(
1019
+ "sqlite3",
1020
+ ["-separator", SQLITE_SEPARATOR, dbPath, sql],
1021
+ { maxBuffer: SQLITE_MAX_BUFFER }
1022
+ );
1023
+ stdout = result.stdout;
1024
+ } catch {
1025
+ return [];
1026
+ }
1027
+ const entries = [];
1028
+ for (const line of stdout.split("\n")) {
1029
+ if (!line.trim()) continue;
1030
+ const [
1031
+ messageId,
1032
+ sessionId,
1033
+ parentId,
1034
+ modelId,
1035
+ createdMsText,
1036
+ inputText,
1037
+ outputText,
1038
+ cacheWriteText,
1039
+ cacheReadText,
1040
+ costText
1041
+ ] = line.split(SQLITE_SEPARATOR);
1042
+ const createdMs = Number(createdMsText);
1043
+ if (!sessionId || !modelId || !Number.isFinite(createdMs) || createdMs <= 0) continue;
1044
+ const rawModel = normalizeAnthropicModelName(modelId);
1045
+ const timestamp = new Date(createdMs).toISOString();
1046
+ const inputTokens = Number(inputText) || 0;
1047
+ const outputTokens = Number(outputText) || 0;
1048
+ const cacheWriteTokens = Number(cacheWriteText) || 0;
1049
+ const cacheReadTokens = Number(cacheReadText) || 0;
1050
+ const explicitCost = Number(costText) || 0;
1051
+ const sessionKind = parentId ? "subagent" : "main";
1052
+ entries.push({
1053
+ date: toLocalDateString(timestamp),
1054
+ timestamp,
1055
+ rawModel,
1056
+ model: getModelDisplayName(rawModel),
1057
+ inputTokens,
1058
+ outputTokens,
1059
+ cacheWriteTokens,
1060
+ cacheReadTokens,
1061
+ cost: calculateKnownCost(rawModel, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens, explicitCost),
1062
+ messageCount: 1,
1063
+ source: "claude",
1064
+ sourceKey: "opencode",
1065
+ sessionId,
1066
+ sessionKind,
1067
+ subagentId: sessionKind === "subagent" ? messageId || sessionId : void 0
1068
+ });
1069
+ }
1070
+ return entries;
1071
+ }
1072
+ async function readFactorySessionMeta(sessionFilePath) {
1073
+ let messageCount = 0;
1074
+ let timestamp;
1075
+ let sessionKind = "main";
1076
+ let subagentId;
1077
+ try {
1078
+ const content = await fs.readFile(sessionFilePath, "utf-8");
1079
+ for (const line of content.split("\n")) {
1080
+ if (!line.trim()) continue;
1081
+ try {
1082
+ const entry = JSON.parse(line);
1083
+ if (!timestamp && typeof entry.timestamp === "string") {
1084
+ timestamp = entry.timestamp;
1085
+ }
1086
+ if (entry.type === "session_start") {
1087
+ const parent = typeof entry.callingSessionId === "string" ? entry.callingSessionId : void 0;
1088
+ if (parent) {
1089
+ sessionKind = "subagent";
1090
+ subagentId = String(entry.id || sessionFilePath);
1091
+ }
1092
+ }
1093
+ const message = entry.message;
1094
+ if (message?.role === "assistant") {
1095
+ messageCount++;
1096
+ }
1097
+ } catch {
1098
+ }
1099
+ }
1100
+ } catch {
1101
+ }
1102
+ return { messageCount: Math.max(messageCount, 1), timestamp, sessionKind, subagentId };
1103
+ }
1104
+ async function parseFactoryEntries() {
1105
+ const sessionsDir = getFactorySessionsDir();
1106
+ if (!await pathExists(sessionsDir)) return [];
1107
+ const settingsFiles = await findFiles(sessionsDir, (entry) => entry.endsWith(".settings.json"));
1108
+ const entries = [];
1109
+ for (const settingsPath of settingsFiles) {
1110
+ try {
1111
+ const settings = JSON.parse(await fs.readFile(settingsPath, "utf-8"));
1112
+ const usage = settings.tokenUsage;
1113
+ const configuredModel = settings.model;
1114
+ if (!usage || !configuredModel) continue;
1115
+ const rawModel = normalizeAnthropicModelName(configuredModel);
1116
+ const inputTokens = usage.inputTokens || 0;
1117
+ const cacheWriteTokens = usage.cacheCreationTokens || 0;
1118
+ const cacheReadTokens = usage.cacheReadTokens || 0;
1119
+ const outputTokens = (usage.outputTokens || 0) + (usage.thinkingTokens || 0);
1120
+ const totalTokens = inputTokens + outputTokens + cacheWriteTokens + cacheReadTokens;
1121
+ if (totalTokens === 0) continue;
1122
+ const sessionFilePath = settingsPath.replace(/\.settings\.json$/, ".jsonl");
1123
+ const sessionId = basename(settingsPath, ".settings.json");
1124
+ const meta = await readFactorySessionMeta(sessionFilePath);
1125
+ const timestamp = settings.providerLockTimestamp || meta.timestamp;
1126
+ if (!timestamp) continue;
1127
+ entries.push({
1128
+ date: toLocalDateString(timestamp),
1129
+ timestamp,
1130
+ rawModel,
1131
+ model: getModelDisplayName(rawModel),
1132
+ inputTokens,
1133
+ outputTokens,
1134
+ cacheWriteTokens,
1135
+ cacheReadTokens,
1136
+ cost: calculateKnownCost(rawModel, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens),
1137
+ messageCount: meta.messageCount,
1138
+ source: "claude",
1139
+ sourceKey: "factory",
1140
+ sessionId,
1141
+ sessionKind: meta.sessionKind,
1142
+ subagentId: meta.subagentId
1143
+ });
1144
+ } catch {
1145
+ }
1146
+ }
1147
+ return entries;
1148
+ }
1149
+ async function collectAnthropicUsageEntries(options = {}) {
1150
+ const [claudeEntries, opencodeEntries, factoryEntries] = await Promise.all([
1151
+ parseClaudeProjectEntries(options.projectFilter),
1152
+ parseOpenCodeEntries(),
1153
+ parseFactoryEntries()
1154
+ ]);
1155
+ return [...claudeEntries, ...opencodeEntries, ...factoryEntries];
1156
+ }
1157
+ async function loadClaudeStatsFromJsonl() {
1158
+ const entries = await collectAnthropicUsageEntries();
1159
+ if (entries.length === 0) {
1160
+ return null;
1161
+ }
1162
+ const modelUsage = /* @__PURE__ */ new Map();
1163
+ const dailyMap = /* @__PURE__ */ new Map();
1164
+ const sessionsByDate = /* @__PURE__ */ new Map();
1165
+ const mainSessions = /* @__PURE__ */ new Set();
1166
+ const subagentSessions = /* @__PURE__ */ new Set();
1167
+ const hourCounts = {};
1168
+ let firstTimestamp = null;
1169
+ let totalMessages = 0;
1170
+ for (const entry of entries) {
1171
+ const existing = modelUsage.get(entry.rawModel) ?? {
1172
+ inputTokens: 0,
1173
+ outputTokens: 0,
1174
+ cacheReadInputTokens: 0,
1175
+ cacheCreationInputTokens: 0,
1176
+ webSearchRequests: 0,
1177
+ costUSD: 0,
1178
+ contextWindow: 2e5
1179
+ };
1180
+ existing.inputTokens += entry.inputTokens;
1181
+ existing.outputTokens += entry.outputTokens;
1182
+ existing.cacheCreationInputTokens += entry.cacheWriteTokens;
1183
+ existing.cacheReadInputTokens += entry.cacheReadTokens;
1184
+ existing.costUSD += entry.cost;
1185
+ modelUsage.set(entry.rawModel, existing);
1186
+ const daily = dailyMap.get(entry.date) ?? { messageCount: 0, sessionCount: 0, toolCallCount: 0 };
1187
+ daily.messageCount += entry.messageCount;
1188
+ dailyMap.set(entry.date, daily);
1189
+ const dateSessions = sessionsByDate.get(entry.date) ?? /* @__PURE__ */ new Set();
1190
+ dateSessions.add(`${entry.sourceKey}:${entry.sessionId}`);
1191
+ sessionsByDate.set(entry.date, dateSessions);
1192
+ const hour = new Date(entry.timestamp).getHours().toString();
1193
+ hourCounts[hour] = (hourCounts[hour] || 0) + entry.messageCount;
1194
+ totalMessages += entry.messageCount;
1195
+ mainSessions.add(`${entry.sourceKey}:${entry.sessionId}`);
1196
+ if (entry.sessionKind === "subagent") {
1197
+ subagentSessions.add(`${entry.sourceKey}:${entry.subagentId || entry.sessionId}`);
1198
+ }
1199
+ if (!firstTimestamp || entry.timestamp < firstTimestamp) {
1200
+ firstTimestamp = entry.timestamp;
1201
+ }
1202
+ }
1203
+ const dailyActivity = Array.from(dailyMap.entries()).map(([date, data]) => ({
1204
+ date,
1205
+ messageCount: data.messageCount,
1206
+ sessionCount: sessionsByDate.get(date)?.size || 0,
1207
+ toolCallCount: data.toolCallCount
1208
+ })).sort((a, b) => a.date.localeCompare(b.date));
1209
+ const sessionCounts = {
1210
+ main: mainSessions.size,
1211
+ subagent: subagentSessions.size,
1212
+ total: mainSessions.size + subagentSessions.size
1213
+ };
1214
+ return {
1215
+ version: 1,
1216
+ lastComputedDate: toLocalDateString((/* @__PURE__ */ new Date()).toISOString()),
1217
+ dailyActivity,
1218
+ dailyModelTokens: [],
1219
+ modelUsage: Object.fromEntries(modelUsage.entries()),
1220
+ totalSessions: sessionCounts.main,
1221
+ totalMessages,
1222
+ longestSession: {
1223
+ sessionId: "",
1224
+ duration: 0,
1225
+ messageCount: 0,
1226
+ timestamp: ""
1227
+ },
1228
+ firstSessionDate: firstTimestamp || (/* @__PURE__ */ new Date()).toISOString(),
1229
+ hourCounts,
1230
+ sessionCounts
1231
+ };
1232
+ }
1233
+
1234
+ // src/usage/loader.ts
1235
+ var MAX_RECURSION_DEPTH = 10;
1236
+ function toLocalDateString2(isoTimestamp) {
1237
+ const date = new Date(isoTimestamp);
1238
+ const year = date.getFullYear();
1239
+ const month = String(date.getMonth() + 1).padStart(2, "0");
1240
+ const day = String(date.getDate()).padStart(2, "0");
1241
+ return `${year}-${month}-${day}`;
1242
+ }
1243
+ function getCodexDir() {
1244
+ return process.env.CODEX_HOME || join2(homedir2(), ".codex");
1245
+ }
1246
+ async function pathExists2(path) {
1247
+ try {
1248
+ await fs2.access(path);
1249
+ return true;
1250
+ } catch {
1251
+ return false;
1252
+ }
1253
+ }
1254
+ async function safeRealpath2(path) {
1255
+ try {
1256
+ return await fs2.realpath(path);
1257
+ } catch {
1258
+ return path;
1259
+ }
1260
+ }
1261
+ async function findJsonlFiles(dir, visited = /* @__PURE__ */ new Set(), depth = 0, result = []) {
1262
+ if (!await pathExists2(dir)) return result;
1263
+ if (depth > MAX_RECURSION_DEPTH) return result;
1264
+ const realPath = await safeRealpath2(dir);
1265
+ if (visited.has(realPath)) return result;
1266
+ visited.add(realPath);
1267
+ let entries;
1268
+ try {
1269
+ entries = await fs2.readdir(dir);
1270
+ } catch {
1271
+ return result;
1272
+ }
1273
+ for (const entry of entries) {
1274
+ const fullPath = join2(dir, entry);
1275
+ let stat;
1276
+ try {
1277
+ stat = await fs2.stat(fullPath);
1278
+ } catch {
1279
+ continue;
1280
+ }
1281
+ if (stat.isDirectory()) {
1282
+ await findJsonlFiles(fullPath, visited, depth + 1, result);
1283
+ } else if (entry.endsWith(".jsonl")) {
1284
+ result.push(fullPath);
1285
+ }
1286
+ }
1287
+ return result;
1288
+ }
1289
+ async function parseClaudeJsonl(projectFilter) {
1290
+ const entries = await collectAnthropicUsageEntries({ projectFilter });
1291
+ return entries.map((entry) => ({
1292
+ date: entry.date,
1293
+ model: entry.model,
1294
+ rawModel: entry.rawModel,
1295
+ inputTokens: entry.inputTokens,
1296
+ outputTokens: entry.outputTokens,
1297
+ cacheWriteTokens: entry.cacheWriteTokens,
1298
+ cacheReadTokens: entry.cacheReadTokens,
1299
+ cost: entry.cost,
1300
+ messageCount: entry.messageCount,
1301
+ source: entry.source,
1302
+ sourceKey: entry.sourceKey,
1303
+ sessionId: entry.sessionId,
1304
+ sessionKind: entry.sessionKind,
1305
+ subagentId: entry.subagentId,
1306
+ timestamp: entry.timestamp
1307
+ }));
1308
+ }
916
1309
  async function parseCodexJsonl() {
917
1310
  const entries = [];
918
1311
  const codexDir = getCodexDir();
919
- const sessionsDir = join(codexDir, "sessions");
920
- const archivedDir = join(codexDir, "archived_sessions");
1312
+ const sessionsDir = join2(codexDir, "sessions");
1313
+ const archivedDir = join2(codexDir, "archived_sessions");
921
1314
  const [sessionFiles, archivedFiles] = await Promise.all([
922
1315
  findJsonlFiles(sessionsDir),
923
1316
  findJsonlFiles(archivedDir)
@@ -926,10 +1319,10 @@ async function parseCodexJsonl() {
926
1319
  if (jsonlFiles.length === 0) return entries;
927
1320
  for (const filePath of jsonlFiles) {
928
1321
  try {
929
- const content = await fs.readFile(filePath, "utf-8");
1322
+ const content = await fs2.readFile(filePath, "utf-8");
930
1323
  const lines = content.split("\n");
931
1324
  let currentModel = "gpt-5";
932
- let fileSessionId = basename(filePath, ".jsonl");
1325
+ let fileSessionId = basename2(filePath, ".jsonl");
933
1326
  let canonicalSessionId = fileSessionId;
934
1327
  let sessionKind = "main";
935
1328
  for (const line of lines) {
@@ -955,7 +1348,7 @@ async function parseCodexJsonl() {
955
1348
  const usage = info.last_token_usage;
956
1349
  const timestamp = entry.timestamp;
957
1350
  if (!timestamp) continue;
958
- const date = toLocalDateString(timestamp);
1351
+ const date = toLocalDateString2(timestamp);
959
1352
  const pricing = getCodexModelPricing(currentModel);
960
1353
  const inputTokens = usage.input_tokens || 0;
961
1354
  const outputTokens = usage.output_tokens || 0;
@@ -989,11 +1382,12 @@ function computeSessionCounts(entries) {
989
1382
  const mainSessions = /* @__PURE__ */ new Set();
990
1383
  const subagentSessions = /* @__PURE__ */ new Set();
991
1384
  for (const entry of entries) {
1385
+ const sourceKey = entry.sourceKey || entry.source;
992
1386
  if (entry.sessionId) {
993
- mainSessions.add(`${entry.source}:${entry.sessionId}`);
1387
+ mainSessions.add(`${sourceKey}:${entry.sessionId}`);
994
1388
  }
995
1389
  if (entry.sessionKind === "subagent" && entry.subagentId) {
996
- subagentSessions.add(`${entry.source}:${entry.subagentId}`);
1390
+ subagentSessions.add(`${sourceKey}:${entry.subagentId}`);
997
1391
  }
998
1392
  }
999
1393
  return {
@@ -1032,13 +1426,14 @@ function sortModelsByTier(models) {
1032
1426
  function aggregateByDay(entries) {
1033
1427
  const dayMap = /* @__PURE__ */ new Map();
1034
1428
  for (const e of entries) {
1429
+ const entryTotal = e.inputTokens + e.outputTokens + e.cacheWriteTokens + e.cacheReadTokens;
1035
1430
  const existing = dayMap.get(e.date);
1036
1431
  if (existing) {
1037
1432
  existing.inputTokens += e.inputTokens;
1038
1433
  existing.outputTokens += e.outputTokens;
1039
1434
  existing.cacheWriteTokens += e.cacheWriteTokens;
1040
1435
  existing.cacheReadTokens += e.cacheReadTokens;
1041
- existing.totalTokens += e.inputTokens + e.outputTokens + e.cacheWriteTokens + e.cacheReadTokens;
1436
+ existing.totalTokens += entryTotal;
1042
1437
  existing.cost += e.cost;
1043
1438
  existing.modelsSet.add(e.model);
1044
1439
  } else {
@@ -1048,7 +1443,7 @@ function aggregateByDay(entries) {
1048
1443
  outputTokens: e.outputTokens,
1049
1444
  cacheWriteTokens: e.cacheWriteTokens,
1050
1445
  cacheReadTokens: e.cacheReadTokens,
1051
- totalTokens: e.inputTokens + e.outputTokens + e.cacheWriteTokens + e.cacheReadTokens,
1446
+ totalTokens: entryTotal,
1052
1447
  cost: e.cost,
1053
1448
  modelsSet: /* @__PURE__ */ new Set([e.model])
1054
1449
  });
@@ -1063,13 +1458,14 @@ function aggregateByMonth(entries) {
1063
1458
  const monthMap = /* @__PURE__ */ new Map();
1064
1459
  for (const e of entries) {
1065
1460
  const month = e.date.slice(0, 7);
1461
+ const entryTotal = e.inputTokens + e.outputTokens + e.cacheWriteTokens + e.cacheReadTokens;
1066
1462
  const existing = monthMap.get(month);
1067
1463
  if (existing) {
1068
1464
  existing.inputTokens += e.inputTokens;
1069
1465
  existing.outputTokens += e.outputTokens;
1070
1466
  existing.cacheWriteTokens += e.cacheWriteTokens;
1071
1467
  existing.cacheReadTokens += e.cacheReadTokens;
1072
- existing.totalTokens += e.inputTokens + e.outputTokens + e.cacheWriteTokens + e.cacheReadTokens;
1468
+ existing.totalTokens += entryTotal;
1073
1469
  existing.cost += e.cost;
1074
1470
  existing.modelsSet.add(e.model);
1075
1471
  } else {
@@ -1079,7 +1475,7 @@ function aggregateByMonth(entries) {
1079
1475
  outputTokens: e.outputTokens,
1080
1476
  cacheWriteTokens: e.cacheWriteTokens,
1081
1477
  cacheReadTokens: e.cacheReadTokens,
1082
- totalTokens: e.inputTokens + e.outputTokens + e.cacheWriteTokens + e.cacheReadTokens,
1478
+ totalTokens: entryTotal,
1083
1479
  cost: e.cost,
1084
1480
  modelsSet: /* @__PURE__ */ new Set([e.model])
1085
1481
  });
@@ -1093,13 +1489,14 @@ function aggregateByMonth(entries) {
1093
1489
  function aggregateByModel(entries) {
1094
1490
  const modelMap = /* @__PURE__ */ new Map();
1095
1491
  for (const e of entries) {
1492
+ const entryTotal = e.inputTokens + e.outputTokens + e.cacheWriteTokens + e.cacheReadTokens;
1096
1493
  const existing = modelMap.get(e.model);
1097
1494
  if (existing) {
1098
1495
  existing.inputTokens += e.inputTokens;
1099
1496
  existing.outputTokens += e.outputTokens;
1100
1497
  existing.cacheWriteTokens += e.cacheWriteTokens;
1101
1498
  existing.cacheReadTokens += e.cacheReadTokens;
1102
- existing.totalTokens += e.inputTokens + e.outputTokens + e.cacheWriteTokens + e.cacheReadTokens;
1499
+ existing.totalTokens += entryTotal;
1103
1500
  existing.cost += e.cost;
1104
1501
  } else {
1105
1502
  modelMap.set(e.model, {
@@ -1108,7 +1505,7 @@ function aggregateByModel(entries) {
1108
1505
  outputTokens: e.outputTokens,
1109
1506
  cacheWriteTokens: e.cacheWriteTokens,
1110
1507
  cacheReadTokens: e.cacheReadTokens,
1111
- totalTokens: e.inputTokens + e.outputTokens + e.cacheWriteTokens + e.cacheReadTokens,
1508
+ totalTokens: entryTotal,
1112
1509
  cost: e.cost
1113
1510
  });
1114
1511
  }
@@ -1118,14 +1515,16 @@ function aggregateByModel(entries) {
1118
1515
  function aggregateBySession(entries) {
1119
1516
  const sessionMap = /* @__PURE__ */ new Map();
1120
1517
  for (const e of entries) {
1121
- const sid = e.sessionId ? `${e.source}:${e.sessionId}` : "unknown";
1518
+ const sourceKey = e.sourceKey || e.source;
1519
+ const sid = e.sessionId ? `${sourceKey}:${e.sessionId}` : "unknown";
1520
+ const entryTotal = e.inputTokens + e.outputTokens + e.cacheWriteTokens + e.cacheReadTokens;
1122
1521
  const existing = sessionMap.get(sid);
1123
1522
  if (existing) {
1124
1523
  existing.inputTokens += e.inputTokens;
1125
1524
  existing.outputTokens += e.outputTokens;
1126
1525
  existing.cacheWriteTokens += e.cacheWriteTokens;
1127
1526
  existing.cacheReadTokens += e.cacheReadTokens;
1128
- existing.totalTokens += e.inputTokens + e.outputTokens + e.cacheWriteTokens + e.cacheReadTokens;
1527
+ existing.totalTokens += entryTotal;
1129
1528
  existing.cost += e.cost;
1130
1529
  existing.modelsSet.add(e.model);
1131
1530
  if (e.timestamp && e.timestamp < existing.firstTimestamp) {
@@ -1139,14 +1538,14 @@ function aggregateBySession(entries) {
1139
1538
  outputTokens: e.outputTokens,
1140
1539
  cacheWriteTokens: e.cacheWriteTokens,
1141
1540
  cacheReadTokens: e.cacheReadTokens,
1142
- totalTokens: e.inputTokens + e.outputTokens + e.cacheWriteTokens + e.cacheReadTokens,
1541
+ totalTokens: entryTotal,
1143
1542
  cost: e.cost,
1144
1543
  firstTimestamp: e.timestamp || ""
1145
1544
  });
1146
1545
  }
1147
1546
  }
1148
1547
  return Array.from(sessionMap.entries()).filter(([, data]) => data.totalTokens > 0).map(([sessionId, data]) => {
1149
- const date = data.firstTimestamp ? toLocalDateString(data.firstTimestamp) : "unknown";
1548
+ const date = data.firstTimestamp ? toLocalDateString2(data.firstTimestamp) : "unknown";
1150
1549
  const shortId = data.displaySessionId.slice(0, 12);
1151
1550
  return {
1152
1551
  key: `${date} ${shortId}`,
@@ -1506,180 +1905,6 @@ function displayTotalOnly(stats, options = {}) {
1506
1905
  console.log();
1507
1906
  }
1508
1907
 
1509
- // src/claude-jsonl-loader.ts
1510
- import { promises as fs2 } from "fs";
1511
- import { homedir as homedir2 } from "os";
1512
- import { basename as basename2, join as join2 } from "path";
1513
- function getClaudeDir2() {
1514
- return process.env.CLAUDE_HOME || join2(homedir2(), ".claude");
1515
- }
1516
- function toLocalDateString2(isoTimestamp) {
1517
- const date = new Date(isoTimestamp);
1518
- const year = date.getFullYear();
1519
- const month = String(date.getMonth() + 1).padStart(2, "0");
1520
- const day = String(date.getDate()).padStart(2, "0");
1521
- return `${year}-${month}-${day}`;
1522
- }
1523
- async function pathExists2(path) {
1524
- try {
1525
- await fs2.access(path);
1526
- return true;
1527
- } catch {
1528
- return false;
1529
- }
1530
- }
1531
- async function claudeJsonlDataExists() {
1532
- const projectsDir = join2(getClaudeDir2(), "projects");
1533
- return pathExists2(projectsDir);
1534
- }
1535
- async function findJsonlFiles2(dir) {
1536
- const files = [];
1537
- if (!await pathExists2(dir)) return files;
1538
- let entries;
1539
- try {
1540
- entries = await fs2.readdir(dir);
1541
- } catch {
1542
- return files;
1543
- }
1544
- for (const entry of entries) {
1545
- const fullPath = join2(dir, entry);
1546
- try {
1547
- const stat = await fs2.stat(fullPath);
1548
- if (stat.isDirectory()) {
1549
- files.push(...await findJsonlFiles2(fullPath));
1550
- } else if (entry.endsWith(".jsonl")) {
1551
- files.push(fullPath);
1552
- }
1553
- } catch {
1554
- }
1555
- }
1556
- return files;
1557
- }
1558
- async function loadClaudeStatsFromJsonl() {
1559
- const claudeDir = getClaudeDir2();
1560
- const projectsDir = join2(claudeDir, "projects");
1561
- if (!await claudeJsonlDataExists()) {
1562
- return null;
1563
- }
1564
- const jsonlFiles = await findJsonlFiles2(projectsDir);
1565
- if (jsonlFiles.length === 0) {
1566
- return null;
1567
- }
1568
- const modelUsage = {};
1569
- const dailyMap = /* @__PURE__ */ new Map();
1570
- const sessionsByDate = /* @__PURE__ */ new Map();
1571
- const mainSessions = /* @__PURE__ */ new Set();
1572
- const subagentSessions = /* @__PURE__ */ new Set();
1573
- const hourCounts = {};
1574
- let firstTimestamp = null;
1575
- let totalMessages = 0;
1576
- const messageIds = /* @__PURE__ */ new Set();
1577
- for (const filePath of jsonlFiles) {
1578
- try {
1579
- const content = await fs2.readFile(filePath, "utf-8");
1580
- const lines = content.split("\n");
1581
- const fallbackSessionId = basename2(filePath, ".jsonl");
1582
- const isSubagentFile = filePath.includes("/subagents/");
1583
- for (const line of lines) {
1584
- if (!line.trim()) continue;
1585
- try {
1586
- const entry = JSON.parse(line);
1587
- if (entry.type !== "assistant") continue;
1588
- if (!entry.message?.usage) continue;
1589
- const usage = entry.message.usage;
1590
- const model = entry.message.model || "unknown";
1591
- const timestamp = entry.timestamp;
1592
- const messageId = entry.message.id;
1593
- const sessionId = entry.sessionId || fallbackSessionId;
1594
- const sessionKind = entry.isSidechain === true || isSubagentFile ? "subagent" : "main";
1595
- if (!modelUsage[model]) {
1596
- modelUsage[model] = {
1597
- inputTokens: 0,
1598
- outputTokens: 0,
1599
- cacheReadInputTokens: 0,
1600
- cacheCreationInputTokens: 0
1601
- };
1602
- }
1603
- modelUsage[model].inputTokens += usage.input_tokens || 0;
1604
- modelUsage[model].outputTokens += usage.output_tokens || 0;
1605
- modelUsage[model].cacheReadInputTokens += usage.cache_read_input_tokens || 0;
1606
- modelUsage[model].cacheCreationInputTokens += usage.cache_creation_input_tokens || 0;
1607
- if (messageId) {
1608
- messageIds.add(messageId);
1609
- }
1610
- if (timestamp) {
1611
- const date = toLocalDateString2(timestamp);
1612
- const hour = new Date(timestamp).getHours().toString();
1613
- if (!dailyMap.has(date)) {
1614
- dailyMap.set(date, { messageCount: 0, sessionCount: 0, toolCallCount: 0 });
1615
- }
1616
- const daily = dailyMap.get(date);
1617
- daily.messageCount++;
1618
- const dateSessions = sessionsByDate.get(date) || /* @__PURE__ */ new Set();
1619
- dateSessions.add(sessionId);
1620
- sessionsByDate.set(date, dateSessions);
1621
- hourCounts[hour] = (hourCounts[hour] || 0) + 1;
1622
- }
1623
- mainSessions.add(sessionId);
1624
- if (sessionKind === "subagent") {
1625
- subagentSessions.add(filePath);
1626
- }
1627
- if (timestamp && (!firstTimestamp || timestamp < firstTimestamp)) {
1628
- firstTimestamp = timestamp;
1629
- }
1630
- } catch {
1631
- }
1632
- }
1633
- } catch {
1634
- }
1635
- }
1636
- totalMessages = messageIds.size;
1637
- if (totalMessages === 0) {
1638
- return null;
1639
- }
1640
- const statsCacheModelUsage = {};
1641
- for (const [model, usage] of Object.entries(modelUsage)) {
1642
- statsCacheModelUsage[model] = {
1643
- inputTokens: usage.inputTokens,
1644
- outputTokens: usage.outputTokens,
1645
- cacheReadInputTokens: usage.cacheReadInputTokens,
1646
- cacheCreationInputTokens: usage.cacheCreationInputTokens,
1647
- webSearchRequests: 0,
1648
- costUSD: 0,
1649
- contextWindow: 2e5
1650
- };
1651
- }
1652
- const dailyActivity = Array.from(dailyMap.entries()).map(([date, data]) => ({
1653
- date,
1654
- messageCount: data.messageCount,
1655
- sessionCount: sessionsByDate.get(date)?.size || 0,
1656
- toolCallCount: data.toolCallCount
1657
- })).sort((a, b) => a.date.localeCompare(b.date));
1658
- const sessionCounts = {
1659
- main: mainSessions.size,
1660
- subagent: subagentSessions.size,
1661
- total: mainSessions.size + subagentSessions.size
1662
- };
1663
- return {
1664
- version: 1,
1665
- lastComputedDate: toLocalDateString2((/* @__PURE__ */ new Date()).toISOString()),
1666
- dailyActivity,
1667
- dailyModelTokens: [],
1668
- modelUsage: statsCacheModelUsage,
1669
- totalSessions: sessionCounts.main,
1670
- totalMessages,
1671
- longestSession: {
1672
- sessionId: "",
1673
- duration: 0,
1674
- messageCount: 0,
1675
- timestamp: ""
1676
- },
1677
- firstSessionDate: firstTimestamp || (/* @__PURE__ */ new Date()).toISOString(),
1678
- hourCounts,
1679
- sessionCounts
1680
- };
1681
- }
1682
-
1683
1908
  // src/codex-loader.ts
1684
1909
  import { promises as fs3 } from "fs";
1685
1910
  import { homedir as homedir3 } from "os";
@@ -1709,7 +1934,7 @@ async function codexDataExists() {
1709
1934
  const archivedDir = join3(codexDir, "archived_sessions");
1710
1935
  return await pathExists3(sessionsDir) || await pathExists3(archivedDir);
1711
1936
  }
1712
- async function findJsonlFiles3(dir) {
1937
+ async function findJsonlFiles2(dir) {
1713
1938
  const files = [];
1714
1939
  if (!await pathExists3(dir)) return files;
1715
1940
  let entries;
@@ -1727,7 +1952,7 @@ async function findJsonlFiles3(dir) {
1727
1952
  continue;
1728
1953
  }
1729
1954
  if (stat.isDirectory()) {
1730
- files.push(...await findJsonlFiles3(fullPath));
1955
+ files.push(...await findJsonlFiles2(fullPath));
1731
1956
  } else if (entry.endsWith(".jsonl")) {
1732
1957
  files.push(fullPath);
1733
1958
  }
@@ -1822,8 +2047,8 @@ async function loadCodexStats() {
1822
2047
  const sessionsDir = join3(codexDir, "sessions");
1823
2048
  const archivedDir = join3(codexDir, "archived_sessions");
1824
2049
  const [sessionFiles, archivedFiles] = await Promise.all([
1825
- findJsonlFiles3(sessionsDir),
1826
- findJsonlFiles3(archivedDir)
2050
+ findJsonlFiles2(sessionsDir),
2051
+ findJsonlFiles2(archivedDir)
1827
2052
  ]);
1828
2053
  const jsonlFiles = [...sessionFiles, ...archivedFiles];
1829
2054
  if (jsonlFiles.length === 0) {
@@ -1920,7 +2145,7 @@ async function loadData(options) {
1920
2145
  let claude = null;
1921
2146
  let codex = null;
1922
2147
  if (!codexOnly) {
1923
- if (await claudeJsonlDataExists()) {
2148
+ if (await claudeCompatibleDataExists()) {
1924
2149
  claude = await loadClaudeStatsFromJsonl();
1925
2150
  }
1926
2151
  }
@@ -1941,10 +2166,10 @@ function validateData(data, options) {
1941
2166
  console.error("Make sure you have used the Codex CLI at least once.");
1942
2167
  } else if (options.combined) {
1943
2168
  console.error("Error: No usage data found");
1944
- console.error("Make sure you have used Claude Code or Codex CLI at least once.");
2169
+ console.error("Make sure you have used a supported local CLI source such as Claude Code, OpenCode, Droid, or Codex at least once.");
1945
2170
  } else {
1946
- console.error("Error: Claude Code data not found at ~/.claude");
1947
- console.error("Make sure you have used Claude Code at least once.");
2171
+ console.error("Error: No Claude-compatible local data found");
2172
+ console.error("Checked ~/.claude, ~/.local/share/opencode/opencode.db, and ~/.factory/sessions.");
1948
2173
  }
1949
2174
  process.exit(1);
1950
2175
  }
@@ -1958,7 +2183,10 @@ function toLocalDateString4(date) {
1958
2183
  return `${year}-${month}-${day}`;
1959
2184
  }
1960
2185
  function calculateModelCost(modelName, usage) {
1961
- const pricing = getModelPricing(modelName);
2186
+ const pricing = tryGetModelPricing(modelName);
2187
+ if (!pricing) {
2188
+ return 0;
2189
+ }
1962
2190
  const inputCost = usage.inputTokens / 1e6 * pricing.input;
1963
2191
  const outputCost = usage.outputTokens / 1e6 * pricing.output;
1964
2192
  const cacheWriteCost = usage.cacheCreationInputTokens / 1e6 * pricing.cacheWrite;
@@ -2062,34 +2290,39 @@ function shouldExcludeModel(modelName) {
2062
2290
  const lower = modelName.toLowerCase();
2063
2291
  return EXCLUDED_MODELS.some((excluded) => lower.includes(excluded));
2064
2292
  }
2065
- function getModelBreakdown(modelUsage) {
2066
- const totalTokens = calculateTotalTokens(modelUsage);
2067
- if (totalTokens === 0) return [];
2068
- const breakdown = [];
2293
+ function getUsageTokens(usage) {
2294
+ return usage.inputTokens + usage.outputTokens + usage.cacheCreationInputTokens + usage.cacheReadInputTokens;
2295
+ }
2296
+ function getDisplayModelTokens(modelUsage) {
2297
+ const aggregated = /* @__PURE__ */ new Map();
2069
2298
  for (const [modelName, usage] of Object.entries(modelUsage)) {
2070
2299
  if (shouldExcludeModel(modelName)) continue;
2071
- const modelTokens = usage.inputTokens + usage.outputTokens + usage.cacheCreationInputTokens + usage.cacheReadInputTokens;
2072
- breakdown.push({
2073
- model: getModelDisplayName(modelName),
2074
- percentage: Math.round(modelTokens / totalTokens * 100),
2075
- tokens: modelTokens
2076
- });
2300
+ const displayName = getModelDisplayName(modelName);
2301
+ const modelTokens = getUsageTokens(usage);
2302
+ aggregated.set(displayName, (aggregated.get(displayName) || 0) + modelTokens);
2077
2303
  }
2078
- breakdown.sort((a, b) => b.tokens - a.tokens);
2079
- return breakdown.map(({ model, percentage }) => ({ model, percentage }));
2304
+ return Array.from(aggregated.entries()).map(([model, tokens]) => ({ model, tokens })).sort((a, b) => b.tokens - a.tokens);
2305
+ }
2306
+ function getModelBreakdown(modelUsage) {
2307
+ const breakdown = getDisplayModelTokens(modelUsage);
2308
+ const totalTokens = breakdown.reduce((sum, entry) => sum + entry.tokens, 0);
2309
+ if (totalTokens === 0) return [];
2310
+ return breakdown.map(({ model, tokens }) => ({
2311
+ model,
2312
+ percentage: Math.round(tokens / totalTokens * 100)
2313
+ }));
2080
2314
  }
2081
2315
  function getFavoriteModel(modelUsage) {
2082
- let favoriteModel = "";
2083
- let maxTokens = 0;
2316
+ return getDisplayModelTokens(modelUsage)[0]?.model || "";
2317
+ }
2318
+ function getCodexDisplayModelTokens(modelUsage) {
2319
+ const aggregated = /* @__PURE__ */ new Map();
2084
2320
  for (const [modelName, usage] of Object.entries(modelUsage)) {
2085
- if (shouldExcludeModel(modelName)) continue;
2086
- const modelTokens = usage.inputTokens + usage.outputTokens + usage.cacheCreationInputTokens + usage.cacheReadInputTokens;
2087
- if (modelTokens > maxTokens) {
2088
- maxTokens = modelTokens;
2089
- favoriteModel = modelName;
2090
- }
2321
+ const displayName = getCodexModelDisplayName(modelName);
2322
+ const modelTokens = usage.inputTokens + usage.outputTokens;
2323
+ aggregated.set(displayName, (aggregated.get(displayName) || 0) + modelTokens);
2091
2324
  }
2092
- return getModelDisplayName(favoriteModel);
2325
+ return Array.from(aggregated.entries()).map(([model, tokens]) => ({ model, tokens })).sort((a, b) => b.tokens - a.tokens);
2093
2326
  }
2094
2327
  function calculateWordsGenerated(modelUsage) {
2095
2328
  let totalOutputTokens = 0;
@@ -2196,23 +2429,11 @@ function computeCodexWrappedStats(cache) {
2196
2429
  busiestMonth = month;
2197
2430
  }
2198
2431
  }
2199
- let favoriteModel = "";
2200
- let maxModelTokens = 0;
2201
- const breakdown = [];
2202
- for (const [modelName, usage] of Object.entries(cache.modelUsage)) {
2203
- const modelTokens = usage.inputTokens + usage.outputTokens;
2204
- breakdown.push({
2205
- model: getCodexModelDisplayName(modelName),
2206
- percentage: totalTokens > 0 ? Math.round(modelTokens / totalTokens * 100) : 0,
2207
- tokens: modelTokens
2208
- });
2209
- if (modelTokens > maxModelTokens) {
2210
- maxModelTokens = modelTokens;
2211
- favoriteModel = modelName;
2212
- }
2213
- }
2214
- breakdown.sort((a, b) => b.tokens - a.tokens);
2215
- const modelBreakdown = breakdown.map(({ model, percentage }) => ({ model, percentage }));
2432
+ const breakdown = getCodexDisplayModelTokens(cache.modelUsage);
2433
+ const modelBreakdown = breakdown.map(({ model, tokens }) => ({
2434
+ model,
2435
+ percentage: totalTokens > 0 ? Math.round(tokens / totalTokens * 100) : 0
2436
+ }));
2216
2437
  const wordsGenerated = Math.round(totalOutputTokens * 0.75);
2217
2438
  return {
2218
2439
  sessions: cache.totalSessions,
@@ -2225,7 +2446,7 @@ function computeCodexWrappedStats(cache) {
2225
2446
  peakHour,
2226
2447
  peakDay,
2227
2448
  busiestMonth,
2228
- favoriteModel: getCodexModelDisplayName(favoriteModel),
2449
+ favoriteModel: breakdown[0]?.model || "",
2229
2450
  modelBreakdown,
2230
2451
  wordsGenerated,
2231
2452
  firstSessionDate: cache.firstSessionDate,
@@ -2491,7 +2712,7 @@ async function publishArtifact(artifact, baseUrl, legacyUrl) {
2491
2712
  import { promises as fs4 } from "fs";
2492
2713
  import { homedir as homedir4 } from "os";
2493
2714
  import { join as join4 } from "path";
2494
- function getClaudeDir3() {
2715
+ function getClaudeDir2() {
2495
2716
  return process.env.CLAUDE_HOME || join4(homedir4(), ".claude");
2496
2717
  }
2497
2718
  async function pathExists4(path) {
@@ -2555,7 +2776,7 @@ async function inspectClaudeSystem(home = homedir4()) {
2555
2776
  projectCount: json?.projects ? Object.keys(json.projects).length : void 0
2556
2777
  };
2557
2778
  }
2558
- async function inspectClaudeUsage(claudeDir = getClaudeDir3()) {
2779
+ async function inspectClaudeUsage(claudeDir = getClaudeDir2()) {
2559
2780
  const statsCachePath = join4(claudeDir, "stats-cache.json");
2560
2781
  const statsCache = await readJsonFile(statsCachePath);
2561
2782
  const facetsDir = join4(claudeDir, "usage-data", "facets");
@@ -3060,9 +3281,9 @@ async function runUsage(args, config) {
3060
3281
  if (args.codex) {
3061
3282
  console.error("Checked: ~/.codex/sessions and ~/.codex/archived_sessions");
3062
3283
  } else if (args.combined) {
3063
- console.error("Checked: ~/.claude/projects and ~/.codex/sessions (plus archived sessions)");
3284
+ console.error("Checked: ~/.claude/projects, ~/.local/share/opencode/opencode.db, ~/.factory/sessions, and ~/.codex/sessions (plus archived sessions)");
3064
3285
  } else {
3065
- console.error("Checked: ~/.claude/projects");
3286
+ console.error("Checked: ~/.claude/projects, ~/.local/share/opencode/opencode.db, and ~/.factory/sessions");
3066
3287
  }
3067
3288
  console.error("Try widening the range or removing the date filter.");
3068
3289
  process.exit(1);
@@ -3072,10 +3293,10 @@ async function runUsage(args, config) {
3072
3293
  console.error("Make sure you have used the Codex CLI at least once.");
3073
3294
  } else if (args.combined) {
3074
3295
  console.error("Error: No usage data found");
3075
- console.error("Make sure you have used Claude Code or Codex CLI at least once.");
3296
+ console.error("Make sure you have used a supported local CLI source such as Claude Code, OpenCode, Droid, or Codex at least once.");
3076
3297
  } else {
3077
- console.error("Error: Claude Code data not found at ~/.claude");
3078
- console.error("Make sure you have used Claude Code at least once.");
3298
+ console.error("Error: No Claude-compatible local data found");
3299
+ console.error("Checked ~/.claude, ~/.local/share/opencode/opencode.db, and ~/.factory/sessions.");
3079
3300
  }
3080
3301
  process.exit(1);
3081
3302
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibestats",
3
- "version": "1.3.8",
3
+ "version": "1.3.9",
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,12 +18,6 @@
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
- },
27
21
  "keywords": [
28
22
  "claude",
29
23
  "claude-code",
@@ -51,5 +45,10 @@
51
45
  },
52
46
  "engines": {
53
47
  "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"
54
53
  }
55
- }
54
+ }