claude-stats 0.2.2 → 0.2.3

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 +127 -125
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -507,6 +507,9 @@ async function forceRefreshUsageLimits() {
507
507
  var CLAUDE_DIR = path2.join(os3.homedir(), ".claude");
508
508
  var STATS_FILE = path2.join(CLAUDE_DIR, "stats-cache.json");
509
509
  var PROJECTS_DIR = path2.join(CLAUDE_DIR, "projects");
510
+ var allTimeStatsCache = null;
511
+ var allTimeStatsCacheTime = 0;
512
+ var ALL_TIME_CACHE_DURATION = 6e4;
510
513
  var cachedAccountId = null;
511
514
  function getAccountId() {
512
515
  if (cachedAccountId) return cachedAccountId;
@@ -803,25 +806,28 @@ function getLocalDateString(date) {
803
806
  const day = String(date.getDate()).padStart(2, "0");
804
807
  return `${year}-${month}-${day}`;
805
808
  }
806
- function getTodayUsageFromSessions() {
807
- const now = /* @__PURE__ */ new Date();
808
- const today = getLocalDateString(now);
809
- const todayStart = new Date(now);
810
- todayStart.setHours(0, 0, 0, 0);
811
- const todayStartTs = todayStart.getTime();
812
- const modelTokens = {};
813
- let messages = 0;
814
- let sessions = 0;
809
+ function calculateAllTimeStats() {
810
+ if (allTimeStatsCache && Date.now() - allTimeStatsCacheTime < ALL_TIME_CACHE_DURATION) {
811
+ return allTimeStatsCache;
812
+ }
815
813
  const seen = /* @__PURE__ */ new Set();
814
+ const modelTokens = {};
815
+ const dailyTokens = {};
816
+ const dailyMessages = {};
817
+ const dailySessions = {};
818
+ let totalMessages = 0;
819
+ let firstSessionDate = null;
820
+ const allSessionIds = /* @__PURE__ */ new Set();
816
821
  if (!fs2.existsSync(PROJECTS_DIR)) {
817
822
  return {
818
- date: today,
819
- tokens: 0,
820
- cost: 0,
821
- messages: 0,
822
- sessions: 0,
823
- models: [],
824
- model_breakdown: {}
823
+ totalCost: 0,
824
+ totalTokens: 0,
825
+ totalMessages: 0,
826
+ totalSessions: 0,
827
+ modelUsage: {},
828
+ modelCosts: {},
829
+ dailyUsage: [],
830
+ firstSessionDate: null
825
831
  };
826
832
  }
827
833
  const projectDirs = fs2.readdirSync(PROJECTS_DIR);
@@ -831,36 +837,51 @@ function getTodayUsageFromSessions() {
831
837
  const sessionFiles = fs2.readdirSync(projectPath).filter((f) => f.endsWith(".jsonl"));
832
838
  for (const sessionFile of sessionFiles) {
833
839
  const sessionPath = path2.join(projectPath, sessionFile);
834
- const stat = fs2.statSync(sessionPath);
835
- if (stat.mtimeMs < todayStartTs) continue;
836
- sessions++;
840
+ const sessionId = path2.basename(sessionFile, ".jsonl");
841
+ allSessionIds.add(sessionId);
837
842
  try {
838
843
  const content = fs2.readFileSync(sessionPath, "utf-8");
839
844
  for (const line of content.trim().split("\n")) {
840
845
  try {
841
846
  const msg = JSON.parse(line);
847
+ if (msg.type !== "assistant") continue;
848
+ const hash = getDedupeHash(msg);
849
+ if (hash) {
850
+ if (seen.has(hash)) continue;
851
+ seen.add(hash);
852
+ }
842
853
  const ts = msg.timestamp || "";
843
854
  if (!ts) continue;
844
855
  const msgDate = new Date(ts);
845
- const msgLocalDate = getLocalDateString(msgDate);
846
- if (msgLocalDate !== today) continue;
847
- if (msg.type === "assistant") {
848
- const hash = getDedupeHash(msg);
849
- if (hash) {
850
- if (seen.has(hash)) continue;
851
- seen.add(hash);
852
- }
853
- messages++;
854
- const usage = msg.message?.usage || {};
855
- const model = msg.message?.model || "unknown";
856
- if (!modelTokens[model]) {
857
- modelTokens[model] = { input: 0, output: 0, cache_read: 0, cache_write: 0 };
858
- }
859
- modelTokens[model].input += usage.input_tokens || 0;
860
- modelTokens[model].output += usage.output_tokens || 0;
861
- modelTokens[model].cache_read += usage.cache_read_input_tokens || 0;
862
- modelTokens[model].cache_write += usage.cache_creation_input_tokens || 0;
856
+ const dateStr = getLocalDateString(msgDate);
857
+ if (!firstSessionDate || dateStr < firstSessionDate) {
858
+ firstSessionDate = dateStr;
859
+ }
860
+ totalMessages++;
861
+ const usage = msg.message?.usage || {};
862
+ const model = msg.message?.model || "unknown";
863
+ if (!modelTokens[model]) {
864
+ modelTokens[model] = { input: 0, output: 0, cache_read: 0, cache_write: 0 };
865
+ }
866
+ modelTokens[model].input += usage.input_tokens || 0;
867
+ modelTokens[model].output += usage.output_tokens || 0;
868
+ modelTokens[model].cache_read += usage.cache_read_input_tokens || 0;
869
+ modelTokens[model].cache_write += usage.cache_creation_input_tokens || 0;
870
+ if (!dailyTokens[dateStr]) {
871
+ dailyTokens[dateStr] = {};
872
+ }
873
+ if (!dailyTokens[dateStr][model]) {
874
+ dailyTokens[dateStr][model] = { input: 0, output: 0, cache_read: 0, cache_write: 0 };
863
875
  }
876
+ dailyTokens[dateStr][model].input += usage.input_tokens || 0;
877
+ dailyTokens[dateStr][model].output += usage.output_tokens || 0;
878
+ dailyTokens[dateStr][model].cache_read += usage.cache_read_input_tokens || 0;
879
+ dailyTokens[dateStr][model].cache_write += usage.cache_creation_input_tokens || 0;
880
+ dailyMessages[dateStr] = (dailyMessages[dateStr] || 0) + 1;
881
+ if (!dailySessions[dateStr]) {
882
+ dailySessions[dateStr] = /* @__PURE__ */ new Set();
883
+ }
884
+ dailySessions[dateStr].add(sessionId);
864
885
  } catch {
865
886
  continue;
866
887
  }
@@ -870,59 +891,57 @@ function getTodayUsageFromSessions() {
870
891
  }
871
892
  }
872
893
  }
873
- let allTokens = 0;
894
+ const modelUsage = {};
895
+ const modelCosts = {};
874
896
  let totalCost = 0;
875
- const modelBreakdown = {};
897
+ let totalTokens = 0;
876
898
  for (const [model, tokens] of Object.entries(modelTokens)) {
877
- const modelTotal = tokens.input + tokens.output + tokens.cache_read + tokens.cache_write;
878
- allTokens += modelTotal;
879
- const pricing = import_shared.MODEL_PRICING[model] || import_shared.DEFAULT_PRICING;
880
- const cost = tokens.input / 1e6 * pricing.input + tokens.output / 1e6 * pricing.output + tokens.cache_read / 1e6 * pricing.cache_read + tokens.cache_write / 1e6 * pricing.cache_write;
881
- totalCost += cost;
882
- modelBreakdown[model] = {
899
+ modelUsage[model] = {
883
900
  inputTokens: tokens.input,
884
901
  outputTokens: tokens.output,
885
902
  cacheReadInputTokens: tokens.cache_read,
886
- cacheCreationInputTokens: tokens.cache_write,
887
- cost: Math.round(cost * 1e4) / 1e4
903
+ cacheCreationInputTokens: tokens.cache_write
888
904
  };
905
+ const cost = (0, import_shared.calculateCost)(model, modelUsage[model]);
906
+ modelCosts[model] = cost;
907
+ totalCost += cost;
908
+ totalTokens += tokens.input + tokens.output + tokens.cache_read + tokens.cache_write;
889
909
  }
890
- return {
891
- date: today,
892
- tokens: allTokens,
893
- cost: Math.round(totalCost * 1e4) / 1e4,
894
- messages,
895
- sessions,
896
- models: Object.keys(modelTokens),
897
- model_breakdown: modelBreakdown
898
- };
899
- }
900
- function getDailyUsage(stats) {
901
- const dailyTokens = stats.dailyModelTokens || [];
902
- const dailyActivity = {};
903
- for (const d of stats.dailyActivity || []) {
904
- dailyActivity[d.date] = d;
905
- }
906
- const result = [];
907
- for (const day of dailyTokens.slice(-14)) {
908
- const date = day.date;
909
- const tokensByModel = day.tokensByModel || {};
910
- const totalTokens = Object.values(tokensByModel).reduce((sum, t) => sum + t, 0);
910
+ const dailyUsage = [];
911
+ const dates = Object.keys(dailyTokens).sort();
912
+ for (const date of dates) {
913
+ const dayModels = dailyTokens[date];
914
+ let dayTokens = 0;
911
915
  let dayCost = 0;
912
- for (const [model, tokens] of Object.entries(tokensByModel)) {
916
+ const models = [];
917
+ for (const [model, tokens] of Object.entries(dayModels)) {
918
+ models.push(model);
919
+ const modelTotal = tokens.input + tokens.output + tokens.cache_read + tokens.cache_write;
920
+ dayTokens += modelTotal;
913
921
  const pricing = import_shared.MODEL_PRICING[model] || import_shared.DEFAULT_PRICING;
914
- dayCost += tokens / 1e6 * pricing.input;
922
+ dayCost += tokens.input / 1e6 * pricing.input + tokens.output / 1e6 * pricing.output + tokens.cache_read / 1e6 * pricing.cache_read + tokens.cache_write / 1e6 * pricing.cache_write;
915
923
  }
916
- const activity = dailyActivity[date] || { messageCount: 0, sessionCount: 0 };
917
- result.push({
924
+ dailyUsage.push({
918
925
  date,
919
- tokens: totalTokens,
926
+ tokens: dayTokens,
920
927
  cost: Math.round(dayCost * 1e4) / 1e4,
921
- messages: activity.messageCount,
922
- sessions: activity.sessionCount,
923
- models: Object.keys(tokensByModel)
928
+ messages: dailyMessages[date] || 0,
929
+ sessions: dailySessions[date]?.size || 0,
930
+ models
924
931
  });
925
932
  }
933
+ const result = {
934
+ totalCost: Math.round(totalCost * 100) / 100,
935
+ totalTokens,
936
+ totalMessages,
937
+ totalSessions: allSessionIds.size,
938
+ modelUsage,
939
+ modelCosts,
940
+ dailyUsage,
941
+ firstSessionDate
942
+ };
943
+ allTimeStatsCache = result;
944
+ allTimeStatsCacheTime = Date.now();
926
945
  return result;
927
946
  }
928
947
  function getSessions() {
@@ -991,57 +1010,41 @@ function collectData() {
991
1010
  const stats = getStats();
992
1011
  const processes = getCliProcesses();
993
1012
  const { active: activeSessions, recent: recentSessions } = getSessions();
994
- const todayUsage = getTodayUsageFromSessions();
995
- const modelUsage = { ...stats.modelUsage };
996
- const todayBreakdown = todayUsage.model_breakdown;
997
- for (const [model, todayData] of Object.entries(todayBreakdown)) {
998
- if (modelUsage[model]) {
999
- modelUsage[model] = {
1000
- inputTokens: (modelUsage[model].inputTokens || 0) + (todayData.inputTokens || 0),
1001
- outputTokens: (modelUsage[model].outputTokens || 0) + (todayData.outputTokens || 0),
1002
- cacheReadInputTokens: (modelUsage[model].cacheReadInputTokens || 0) + (todayData.cacheReadInputTokens || 0),
1003
- cacheCreationInputTokens: (modelUsage[model].cacheCreationInputTokens || 0) + (todayData.cacheCreationInputTokens || 0)
1004
- };
1005
- } else {
1006
- modelUsage[model] = {
1007
- inputTokens: todayData.inputTokens,
1008
- outputTokens: todayData.outputTokens,
1009
- cacheReadInputTokens: todayData.cacheReadInputTokens,
1010
- cacheCreationInputTokens: todayData.cacheCreationInputTokens
1011
- };
1012
- }
1013
- }
1014
- const modelCosts = {};
1015
- let totalCost = 0;
1016
- let totalTokens = 0;
1017
- for (const [model, usage] of Object.entries(modelUsage)) {
1018
- const cost = (0, import_shared.calculateCost)(model, usage);
1019
- modelCosts[model] = cost;
1020
- totalCost += cost;
1021
- totalTokens += (usage.inputTokens || 0) + (usage.outputTokens || 0) + (usage.cacheReadInputTokens || 0) + (usage.cacheCreationInputTokens || 0);
1022
- }
1023
- const dailyUsage = getDailyUsage(stats);
1024
- if (todayUsage.tokens > 0) {
1025
- dailyUsage.push({
1026
- date: todayUsage.date,
1027
- tokens: todayUsage.tokens,
1028
- cost: todayUsage.cost,
1029
- messages: todayUsage.messages,
1030
- sessions: todayUsage.sessions,
1031
- models: todayUsage.models
1032
- });
1033
- }
1013
+ const allTimeStats = calculateAllTimeStats();
1014
+ const today = getLocalDateString(/* @__PURE__ */ new Date());
1015
+ const todayFromAllTime = allTimeStats.dailyUsage.find((d) => d.date === today);
1016
+ const todayUsage = todayFromAllTime ? {
1017
+ date: today,
1018
+ tokens: todayFromAllTime.tokens,
1019
+ cost: todayFromAllTime.cost,
1020
+ messages: todayFromAllTime.messages,
1021
+ sessions: todayFromAllTime.sessions,
1022
+ models: todayFromAllTime.models,
1023
+ model_breakdown: {}
1024
+ // Not used in summary display
1025
+ } : {
1026
+ date: today,
1027
+ tokens: 0,
1028
+ cost: 0,
1029
+ messages: 0,
1030
+ sessions: 0,
1031
+ models: [],
1032
+ model_breakdown: {}
1033
+ };
1034
1034
  const activeSessionsCost = activeSessions.reduce((sum, s) => sum + (s.cost || 0), 0);
1035
1035
  const activeSessionsTokens = activeSessions.reduce((sum, s) => {
1036
1036
  if (!s.tokens) return sum;
1037
1037
  return sum + s.tokens.input_tokens + s.tokens.output_tokens + s.tokens.cache_read + s.tokens.cache_write;
1038
1038
  }, 0);
1039
- const totalSessionsCount = (stats.totalSessions || 0) + todayUsage.sessions;
1040
- const totalMessagesCount = (stats.totalMessages || 0) + todayUsage.messages;
1039
+ const totalCost = allTimeStats.totalCost;
1040
+ const totalTokens = allTimeStats.totalTokens;
1041
+ const totalMessagesCount = allTimeStats.totalMessages;
1042
+ const totalSessionsCount = allTimeStats.totalSessions;
1041
1043
  const avgCostPerMessage = totalMessagesCount > 0 ? totalCost / totalMessagesCount : 0;
1042
1044
  const avgCostPerSession = totalSessionsCount > 0 ? totalCost / totalSessionsCount : 0;
1045
+ const dailyUsage = allTimeStats.dailyUsage;
1043
1046
  let daysActive = dailyUsage.length || 1;
1044
- const firstSessionDate = stats.firstSessionDate;
1047
+ const firstSessionDate = allTimeStats.firstSessionDate;
1045
1048
  if (firstSessionDate) {
1046
1049
  try {
1047
1050
  const firstDt = new Date(firstSessionDate);
@@ -1051,11 +1054,10 @@ function collectData() {
1051
1054
  }
1052
1055
  const dailyAvgCost = daysActive > 0 ? totalCost / daysActive : 0;
1053
1056
  const last7Days = dailyUsage.slice(-7);
1054
- let weeklyCost = last7Days.reduce((sum, d) => sum + (d.cost || 0), 0);
1055
- weeklyCost += todayUsage.cost;
1057
+ const weeklyCost = last7Days.reduce((sum, d) => sum + (d.cost || 0), 0);
1056
1058
  const currentHour = (/* @__PURE__ */ new Date()).getHours();
1057
1059
  const hoursElapsed = Math.max(currentHour, 1);
1058
- const todayHourlyRate = hoursElapsed > 0 ? todayUsage.cost / hoursElapsed : 0;
1060
+ const todayHourlyRate = todayUsage.cost > 0 ? todayUsage.cost / hoursElapsed : 0;
1059
1061
  const hourCounts = stats.hourCounts || {};
1060
1062
  const summary = {
1061
1063
  is_running: activeSessions.length > 0,
@@ -1079,11 +1081,11 @@ function collectData() {
1079
1081
  };
1080
1082
  return {
1081
1083
  stats,
1082
- model_usage: modelUsage,
1084
+ model_usage: allTimeStats.modelUsage,
1083
1085
  processes,
1084
1086
  active_sessions: activeSessions,
1085
1087
  recent_sessions: recentSessions,
1086
- model_costs: modelCosts,
1088
+ model_costs: allTimeStats.modelCosts,
1087
1089
  daily_usage: dailyUsage,
1088
1090
  today_usage: todayUsage,
1089
1091
  hour_counts: hourCounts,
@@ -1242,7 +1244,7 @@ function setupDaemonCleanup() {
1242
1244
  // src/index.ts
1243
1245
  var DEFAULT_SERVER = "https://cm.cban.top";
1244
1246
  var program = new import_commander.Command();
1245
- program.name("claude-stats").description("Monitor and stream Claude Code usage statistics").version("0.2.2");
1247
+ program.name("claude-stats").description("Monitor and stream Claude Code usage statistics").version("0.2.3");
1246
1248
  function promptForInput(question, hidden = false) {
1247
1249
  return new Promise((resolve) => {
1248
1250
  const rl = readline.createInterface({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-stats",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Monitor and stream Claude Code usage statistics to a remote dashboard",
5
5
  "keywords": [
6
6
  "claude",