claude-session-viewer 0.3.4 → 0.3.6

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.
@@ -1,327 +1,51 @@
1
- import { readdir, stat } from 'fs/promises';
1
+ import { stat } from 'fs/promises';
2
2
  import { join } from 'path';
3
3
  import { CLAUDE_DIR } from '../constants.js';
4
- import { PRICING, DEFAULT_PRICING_MODEL, WEEKDAY_NAMES } from '../claude/config.js';
5
- import { parseJsonl } from '../utils/jsonl.js';
6
- import { extractTokenUsage, aggregateTokenUsage, calculateCost, createEmptyTokenUsage } from '../utils/tokenStats.js';
7
- import { getProjectDisplayName } from '../claude/projects/repository.js';
8
- import { collectAgentDescriptions } from '../claude/sessions/agents.js';
9
- import { shouldSkipSession, isAgentSession, isEmptyFile } from '../claude/sessions/filters.js';
4
+ import { getOverallStatistics, getProjectStatistics } from '../statistics/service.js';
10
5
  /**
11
6
  * Statistics routes
12
7
  */
13
8
  export async function registerStatisticsRoutes(server) {
14
- server.get('/api/statistics/tokens', async (request, reply) => {
9
+ /**
10
+ * GET /api/statistics/overall
11
+ * Get overall usage statistics across all projects
12
+ */
13
+ server.get('/api/statistics/overall', async (request, reply) => {
15
14
  try {
15
+ const days = request.query.days || '7';
16
+ const statistics = await getOverallStatistics(days);
17
+ return statistics;
18
+ }
19
+ catch (error) {
20
+ console.error('Error calculating overall statistics:', error);
21
+ return reply.code(500).send({ error: 'Internal server error' });
22
+ }
23
+ });
24
+ /**
25
+ * GET /api/statistics/projects/:projectId
26
+ * Get usage statistics for a specific project
27
+ */
28
+ server.get('/api/statistics/projects/:projectId', async (request, reply) => {
29
+ try {
30
+ const { projectId } = request.params;
16
31
  const projectsDir = join(CLAUDE_DIR, 'projects');
17
- const projects = await readdir(projectsDir);
18
- const daysParam = request.query.days || '30';
19
- const cutoffDate = new Date();
20
- if (daysParam !== 'all') {
21
- const days = parseInt(daysParam, 10);
22
- cutoffDate.setDate(cutoffDate.getDate() - days);
23
- cutoffDate.setHours(0, 0, 0, 0);
24
- }
25
- else {
26
- cutoffDate.setFullYear(2000, 0, 1);
27
- }
28
- const dailyMap = new Map();
29
- const projectMap = new Map();
30
- const modelMap = new Map();
31
- let totalMessages = 0;
32
- let totalSessions = 0;
33
- let minDate = null;
34
- let maxDate = null;
35
- let totalCacheCreation = 0;
36
- let totalCacheRead = 0;
37
- let ephemeral5mTokens = 0;
38
- let ephemeral1hTokens = 0;
39
- const allUsages = [];
40
- const allCosts = [];
41
- const toolUsageMap = new Map();
42
- let totalAgentSessions = 0;
43
- const hourlyMap = new Map();
44
- const weekdayMap = new Map();
45
- for (let hour = 0; hour < 24; hour++) {
46
- hourlyMap.set(hour, {
47
- usage: createEmptyTokenUsage(),
48
- sessionIds: new Set(),
49
- messageCount: 0
50
- });
51
- }
52
- for (let weekday = 0; weekday < 7; weekday++) {
53
- weekdayMap.set(weekday, {
54
- usage: createEmptyTokenUsage(),
55
- sessionIds: new Set(),
56
- messageCount: 0
57
- });
58
- }
59
- for (const project of projects) {
60
- const projectPath = join(projectsDir, project);
32
+ const projectPath = join(projectsDir, projectId);
33
+ // Check if project exists
34
+ try {
61
35
  const projectStat = await stat(projectPath);
62
- if (!projectStat.isDirectory())
63
- continue;
64
- const files = await readdir(projectPath);
65
- const displayName = getProjectDisplayName(project);
66
- if (!projectMap.has(project)) {
67
- projectMap.set(project, {
68
- usage: createEmptyTokenUsage(),
69
- sessionIds: new Set(),
70
- displayName
71
- });
72
- }
73
- for (const file of files) {
74
- if (!file.endsWith('.jsonl'))
75
- continue;
76
- const filePath = join(projectPath, file);
77
- const fileStat = await stat(filePath);
78
- if (isEmptyFile(fileStat.size))
79
- continue;
80
- const sessionId = file.replace('.jsonl', '');
81
- if (isAgentSession(sessionId))
82
- continue;
83
- try {
84
- const messages = await parseJsonl(filePath);
85
- if (shouldSkipSession(messages))
86
- continue;
87
- totalSessions++;
88
- const projectData = projectMap.get(project);
89
- projectData.sessionIds.add(sessionId);
90
- const agentDescriptions = collectAgentDescriptions(messages);
91
- if (agentDescriptions.size > 0) {
92
- totalAgentSessions++;
93
- }
94
- for (const message of messages) {
95
- const tokenData = extractTokenUsage(message);
96
- if (!tokenData)
97
- continue;
98
- const messageDate = new Date(message.timestamp);
99
- if (messageDate < cutoffDate)
100
- continue;
101
- totalMessages++;
102
- const { usage, model } = tokenData;
103
- const pricing = PRICING[model] || PRICING[DEFAULT_PRICING_MODEL];
104
- allUsages.push(usage);
105
- allCosts.push(calculateCost(usage, pricing));
106
- const dateKey = messageDate.toISOString().split('T')[0];
107
- if (!minDate || messageDate < minDate)
108
- minDate = messageDate;
109
- if (!maxDate || messageDate > maxDate)
110
- maxDate = messageDate;
111
- if (!dailyMap.has(dateKey)) {
112
- dailyMap.set(dateKey, {
113
- usage: createEmptyTokenUsage(),
114
- sessionIds: new Set()
115
- });
116
- }
117
- const dailyData = dailyMap.get(dateKey);
118
- dailyData.usage.inputTokens += usage.inputTokens;
119
- dailyData.usage.cacheCreationTokens += usage.cacheCreationTokens;
120
- dailyData.usage.cacheReadTokens += usage.cacheReadTokens;
121
- dailyData.usage.outputTokens += usage.outputTokens;
122
- dailyData.usage.totalTokens += usage.totalTokens;
123
- dailyData.sessionIds.add(sessionId);
124
- const hour = messageDate.getHours();
125
- const hourData = hourlyMap.get(hour);
126
- hourData.usage.inputTokens += usage.inputTokens;
127
- hourData.usage.cacheCreationTokens += usage.cacheCreationTokens;
128
- hourData.usage.cacheReadTokens += usage.cacheReadTokens;
129
- hourData.usage.outputTokens += usage.outputTokens;
130
- hourData.usage.totalTokens += usage.totalTokens;
131
- hourData.sessionIds.add(sessionId);
132
- hourData.messageCount++;
133
- const weekday = messageDate.getDay();
134
- const weekdayData = weekdayMap.get(weekday);
135
- weekdayData.usage.inputTokens += usage.inputTokens;
136
- weekdayData.usage.cacheCreationTokens += usage.cacheCreationTokens;
137
- weekdayData.usage.cacheReadTokens += usage.cacheReadTokens;
138
- weekdayData.usage.outputTokens += usage.outputTokens;
139
- weekdayData.usage.totalTokens += usage.totalTokens;
140
- weekdayData.sessionIds.add(sessionId);
141
- weekdayData.messageCount++;
142
- projectData.usage.inputTokens += usage.inputTokens;
143
- projectData.usage.cacheCreationTokens += usage.cacheCreationTokens;
144
- projectData.usage.cacheReadTokens += usage.cacheReadTokens;
145
- projectData.usage.outputTokens += usage.outputTokens;
146
- projectData.usage.totalTokens += usage.totalTokens;
147
- if (!modelMap.has(model)) {
148
- modelMap.set(model, {
149
- usage: createEmptyTokenUsage(),
150
- messageCount: 0
151
- });
152
- }
153
- const modelData = modelMap.get(model);
154
- modelData.usage.inputTokens += usage.inputTokens;
155
- modelData.usage.cacheCreationTokens += usage.cacheCreationTokens;
156
- modelData.usage.cacheReadTokens += usage.cacheReadTokens;
157
- modelData.usage.outputTokens += usage.outputTokens;
158
- modelData.usage.totalTokens += usage.totalTokens;
159
- modelData.messageCount++;
160
- totalCacheCreation += usage.cacheCreationTokens;
161
- totalCacheRead += usage.cacheReadTokens;
162
- const cacheCreation = message.message?.usage?.cache_creation;
163
- if (cacheCreation) {
164
- ephemeral5mTokens += cacheCreation.ephemeral_5m_input_tokens || 0;
165
- ephemeral1hTokens += cacheCreation.ephemeral_1h_input_tokens || 0;
166
- }
167
- }
168
- const toolUseIds = new Set();
169
- const successfulToolUseIds = new Set();
170
- for (const message of messages) {
171
- const messageDate = new Date(message.timestamp);
172
- if (messageDate < cutoffDate)
173
- continue;
174
- if (message.type === 'assistant' && message.message?.content && Array.isArray(message.message.content)) {
175
- for (const item of message.message.content) {
176
- if (item.type === 'tool_use' && item.name && item.id) {
177
- const toolName = item.name;
178
- if (!toolUsageMap.has(toolName)) {
179
- toolUsageMap.set(toolName, { total: 0, successful: 0 });
180
- }
181
- if (!toolUseIds.has(item.id)) {
182
- toolUseIds.add(item.id);
183
- toolUsageMap.get(toolName).total++;
184
- }
185
- }
186
- }
187
- }
188
- }
189
- for (const message of messages) {
190
- const messageDate = new Date(message.timestamp);
191
- if (messageDate < cutoffDate)
192
- continue;
193
- if (message.message?.content && Array.isArray(message.message.content)) {
194
- for (const item of message.message.content) {
195
- if (item.type === 'tool_result' && !item.is_error && item.tool_use_id) {
196
- if (!successfulToolUseIds.has(item.tool_use_id)) {
197
- successfulToolUseIds.add(item.tool_use_id);
198
- let found = false;
199
- for (const msg of messages) {
200
- if (found)
201
- break;
202
- if (msg.type === 'assistant' && msg.message?.content && Array.isArray(msg.message.content)) {
203
- for (const toolUseItem of msg.message.content) {
204
- if (toolUseItem.type === 'tool_use' && toolUseItem.id === item.tool_use_id && toolUseItem.name) {
205
- const toolStats = toolUsageMap.get(toolUseItem.name);
206
- if (toolStats) {
207
- toolStats.successful++;
208
- found = true;
209
- break;
210
- }
211
- }
212
- }
213
- }
214
- }
215
- }
216
- }
217
- }
218
- }
219
- }
220
- }
221
- catch (error) {
222
- console.error(`Error processing ${file}:`, error);
223
- }
36
+ if (!projectStat.isDirectory()) {
37
+ return reply.code(404).send({ error: 'Project not found' });
224
38
  }
225
39
  }
226
- const totalUsage = aggregateTokenUsage(allUsages);
227
- const totalCost = allCosts.reduce((acc, cost) => ({
228
- inputCost: acc.inputCost + cost.inputCost,
229
- outputCost: acc.outputCost + cost.outputCost,
230
- cacheCreationCost: acc.cacheCreationCost + cost.cacheCreationCost,
231
- cacheReadCost: acc.cacheReadCost + cost.cacheReadCost,
232
- totalCost: acc.totalCost + cost.totalCost
233
- }), { inputCost: 0, outputCost: 0, cacheCreationCost: 0, cacheReadCost: 0, totalCost: 0 });
234
- const totalPotentialInput = totalUsage.inputTokens + totalCacheCreation + totalCacheRead;
235
- const cacheHitRate = totalPotentialInput > 0 ? (totalCacheRead / totalPotentialInput) * 100 : 0;
236
- const defaultPricing = PRICING[DEFAULT_PRICING_MODEL];
237
- const savedCost = (totalCacheRead / 1000000) * (defaultPricing.input - defaultPricing.cacheRead);
238
- const daily = Array.from(dailyMap.entries())
239
- .map(([date, data]) => ({
240
- date,
241
- usage: data.usage,
242
- sessionCount: data.sessionIds.size
243
- }))
244
- .sort((a, b) => a.date.localeCompare(b.date));
245
- const byProject = Array.from(projectMap.entries())
246
- .map(([project, data]) => ({
247
- project,
248
- displayName: data.displayName,
249
- usage: data.usage,
250
- sessionCount: data.sessionIds.size
251
- }))
252
- .sort((a, b) => b.usage.totalTokens - a.usage.totalTokens);
253
- const byModel = Array.from(modelMap.entries())
254
- .map(([model, data]) => ({
255
- model,
256
- usage: data.usage,
257
- messageCount: data.messageCount
258
- }))
259
- .sort((a, b) => b.usage.totalTokens - a.usage.totalTokens);
260
- const toolUsage = Array.from(toolUsageMap.entries())
261
- .map(([toolName, stats]) => ({
262
- toolName,
263
- totalUses: stats.total,
264
- successfulUses: stats.successful,
265
- successRate: stats.total > 0 ? (stats.successful / stats.total) * 100 : 0
266
- }))
267
- .sort((a, b) => b.totalUses - a.totalUses);
268
- const totalToolCalls = Array.from(toolUsageMap.values()).reduce((sum, stats) => sum + stats.total, 0);
269
- const agentUsageRate = totalSessions > 0 ? (totalAgentSessions / totalSessions) * 100 : 0;
270
- const byHour = Array.from(hourlyMap.entries())
271
- .map(([hour, data]) => ({
272
- hour,
273
- sessionCount: data.sessionIds.size,
274
- messageCount: data.messageCount,
275
- usage: data.usage
276
- }))
277
- .sort((a, b) => a.hour - b.hour);
278
- const byWeekday = Array.from(weekdayMap.entries())
279
- .map(([weekday, data]) => ({
280
- weekday,
281
- weekdayName: WEEKDAY_NAMES[weekday],
282
- sessionCount: data.sessionIds.size,
283
- messageCount: data.messageCount,
284
- usage: data.usage
285
- }))
286
- .sort((a, b) => a.weekday - b.weekday);
287
- const statistics = {
288
- overview: {
289
- total: totalUsage,
290
- totalSessions,
291
- totalMessages,
292
- dateRange: {
293
- start: minDate?.toISOString() || new Date().toISOString(),
294
- end: maxDate?.toISOString() || new Date().toISOString()
295
- }
296
- },
297
- daily,
298
- byProject,
299
- byModel,
300
- cache: {
301
- totalCacheCreation,
302
- totalCacheRead,
303
- ephemeral5mTokens,
304
- ephemeral1hTokens,
305
- cacheHitRate,
306
- estimatedSavings: savedCost
307
- },
308
- cost: totalCost,
309
- productivity: {
310
- toolUsage,
311
- totalToolCalls,
312
- agentSessions: totalAgentSessions,
313
- totalSessions,
314
- agentUsageRate
315
- },
316
- trends: {
317
- byHour,
318
- byWeekday
319
- }
320
- };
40
+ catch (error) {
41
+ return reply.code(404).send({ error: 'Project not found' });
42
+ }
43
+ const days = request.query.days || '7';
44
+ const statistics = await getProjectStatistics(projectId, days);
321
45
  return statistics;
322
46
  }
323
47
  catch (error) {
324
- console.error('Error calculating token statistics:', error);
48
+ console.error('Error calculating project statistics:', error);
325
49
  return reply.code(500).send({ error: 'Internal server error' });
326
50
  }
327
51
  });
@@ -0,0 +1,290 @@
1
+ import { readdir, stat } from 'fs/promises';
2
+ import { join } from 'path';
3
+ import { CLAUDE_DIR } from '../constants.js';
4
+ import { PRICING, DEFAULT_PRICING_MODEL } from '../claude/config.js';
5
+ import { parseJsonl } from '../utils/jsonl.js';
6
+ import { extractTokenUsage, calculateCost, createEmptyTokenUsage } from './tokenUsage.js';
7
+ import { getProjectName } from '../claude/projects/repository.js';
8
+ import { collectAgentDescriptions } from '../claude/sessions/agents.js';
9
+ import { shouldSkipSession, isAgentSession, isEmptyFile } from '../claude/sessions/filters.js';
10
+ /**
11
+ * Format date to YYYY-MM-DD in local timezone
12
+ */
13
+ function formatDateLocal(date) {
14
+ const year = date.getFullYear();
15
+ const month = String(date.getMonth() + 1).padStart(2, '0');
16
+ const day = String(date.getDate()).padStart(2, '0');
17
+ return `${year}-${month}-${day}`;
18
+ }
19
+ /**
20
+ * Create empty aggregated data structure
21
+ */
22
+ function createEmptyAggregatedData() {
23
+ const hourlyMap = new Map();
24
+ const weekdayMap = new Map();
25
+ for (let hour = 0; hour < 24; hour++) {
26
+ hourlyMap.set(hour, {
27
+ tokenUsage: createEmptyTokenUsage(),
28
+ sessionIds: new Set(),
29
+ messageCount: 0
30
+ });
31
+ }
32
+ for (let weekday = 0; weekday < 7; weekday++) {
33
+ weekdayMap.set(weekday, {
34
+ tokenUsage: createEmptyTokenUsage(),
35
+ sessionIds: new Set(),
36
+ messageCount: 0
37
+ });
38
+ }
39
+ return {
40
+ dailyMap: new Map(),
41
+ projectMap: new Map(),
42
+ modelMap: new Map(),
43
+ hourlyMap,
44
+ weekdayMap,
45
+ toolUsageMap: new Map(),
46
+ totalMessages: 0,
47
+ totalSessions: 0,
48
+ minDate: null,
49
+ maxDate: null,
50
+ totalCacheCreation: 0,
51
+ totalCacheRead: 0,
52
+ ephemeral5mTokens: 0,
53
+ ephemeral1hTokens: 0,
54
+ allUsages: [],
55
+ allCosts: [],
56
+ totalAgentSessions: 0
57
+ };
58
+ }
59
+ /**
60
+ * Process a single message and update aggregated data
61
+ */
62
+ function processMessage(message, sessionId, projectId, data, toolUseIdMap // Maps tool_use_id to tool name
63
+ ) {
64
+ // Process tool_result first (before extractTokenUsage check) since user messages don't have usage
65
+ if (message.type === 'user' && message.message?.content) {
66
+ const content = Array.isArray(message.message.content)
67
+ ? message.message.content
68
+ : [{ type: 'text', text: message.message.content }];
69
+ for (const item of content) {
70
+ if (item.type === 'tool_result') {
71
+ const toolUseId = item.tool_use_id;
72
+ const toolName = toolUseIdMap.get(toolUseId);
73
+ // Count as successful if:
74
+ // 1. We can find the tool name from toolUseIdMap
75
+ // 2. Either is_error is false/undefined, OR there's no error field at all
76
+ if (toolName) {
77
+ const isError = item.is_error === true;
78
+ if (!isError) {
79
+ const stats = data.toolUsageMap.get(toolName);
80
+ if (stats) {
81
+ stats.successful++;
82
+ }
83
+ }
84
+ }
85
+ }
86
+ }
87
+ }
88
+ const result = extractTokenUsage(message);
89
+ if (!result)
90
+ return;
91
+ const { usage, model } = result;
92
+ const messageDate = new Date(message.timestamp);
93
+ // Update min/max dates
94
+ if (!data.minDate || messageDate < data.minDate) {
95
+ data.minDate = messageDate;
96
+ }
97
+ if (!data.maxDate || messageDate > data.maxDate) {
98
+ data.maxDate = messageDate;
99
+ }
100
+ data.totalMessages++;
101
+ const pricing = PRICING[model] || PRICING[DEFAULT_PRICING_MODEL];
102
+ data.allUsages.push(usage);
103
+ const costBreakdown = calculateCost(usage, pricing);
104
+ data.allCosts.push(costBreakdown.totalCost);
105
+ // Update daily map
106
+ const dateKey = formatDateLocal(messageDate);
107
+ if (!data.dailyMap.has(dateKey)) {
108
+ data.dailyMap.set(dateKey, {
109
+ tokenUsage: createEmptyTokenUsage(),
110
+ sessionIds: new Set()
111
+ });
112
+ }
113
+ const dailyData = data.dailyMap.get(dateKey);
114
+ dailyData.tokenUsage.inputTokens += usage.inputTokens;
115
+ dailyData.tokenUsage.cacheCreationTokens += usage.cacheCreationTokens;
116
+ dailyData.tokenUsage.cacheReadTokens += usage.cacheReadTokens;
117
+ dailyData.tokenUsage.outputTokens += usage.outputTokens;
118
+ dailyData.tokenUsage.totalTokens += usage.totalTokens;
119
+ dailyData.sessionIds.add(sessionId);
120
+ // Update hourly map
121
+ const hour = messageDate.getHours();
122
+ const hourData = data.hourlyMap.get(hour);
123
+ hourData.tokenUsage.inputTokens += usage.inputTokens;
124
+ hourData.tokenUsage.cacheCreationTokens += usage.cacheCreationTokens;
125
+ hourData.tokenUsage.cacheReadTokens += usage.cacheReadTokens;
126
+ hourData.tokenUsage.outputTokens += usage.outputTokens;
127
+ hourData.tokenUsage.totalTokens += usage.totalTokens;
128
+ hourData.sessionIds.add(sessionId);
129
+ hourData.messageCount++;
130
+ // Update weekday map
131
+ const weekday = messageDate.getDay();
132
+ const weekdayData = data.weekdayMap.get(weekday);
133
+ weekdayData.tokenUsage.inputTokens += usage.inputTokens;
134
+ weekdayData.tokenUsage.cacheCreationTokens += usage.cacheCreationTokens;
135
+ weekdayData.tokenUsage.cacheReadTokens += usage.cacheReadTokens;
136
+ weekdayData.tokenUsage.outputTokens += usage.outputTokens;
137
+ weekdayData.tokenUsage.totalTokens += usage.totalTokens;
138
+ weekdayData.sessionIds.add(sessionId);
139
+ weekdayData.messageCount++;
140
+ // Update project map
141
+ if (!data.projectMap.has(projectId)) {
142
+ data.projectMap.set(projectId, {
143
+ tokenUsage: createEmptyTokenUsage(),
144
+ sessionIds: new Set(),
145
+ name: getProjectName(projectId)
146
+ });
147
+ }
148
+ const projectData = data.projectMap.get(projectId);
149
+ projectData.tokenUsage.inputTokens += usage.inputTokens;
150
+ projectData.tokenUsage.cacheCreationTokens += usage.cacheCreationTokens;
151
+ projectData.tokenUsage.cacheReadTokens += usage.cacheReadTokens;
152
+ projectData.tokenUsage.outputTokens += usage.outputTokens;
153
+ projectData.tokenUsage.totalTokens += usage.totalTokens;
154
+ // Update model map
155
+ if (!data.modelMap.has(model)) {
156
+ data.modelMap.set(model, {
157
+ tokenUsage: createEmptyTokenUsage(),
158
+ messageCount: 0
159
+ });
160
+ }
161
+ const modelData = data.modelMap.get(model);
162
+ modelData.tokenUsage.inputTokens += usage.inputTokens;
163
+ modelData.tokenUsage.cacheCreationTokens += usage.cacheCreationTokens;
164
+ modelData.tokenUsage.cacheReadTokens += usage.cacheReadTokens;
165
+ modelData.tokenUsage.outputTokens += usage.outputTokens;
166
+ modelData.tokenUsage.totalTokens += usage.totalTokens;
167
+ modelData.messageCount++;
168
+ // Update cache stats
169
+ data.totalCacheCreation += usage.cacheCreationTokens;
170
+ data.totalCacheRead += usage.cacheReadTokens;
171
+ const cacheCreation = message.message?.usage?.cache_creation;
172
+ if (cacheCreation) {
173
+ data.ephemeral5mTokens += cacheCreation.ephemeral_5m_input_tokens || 0;
174
+ data.ephemeral1hTokens += cacheCreation.ephemeral_1h_input_tokens || 0;
175
+ }
176
+ // Process tool_use in assistant messages
177
+ if (message.message?.role === 'assistant' && message.message?.content) {
178
+ const content = Array.isArray(message.message.content)
179
+ ? message.message.content
180
+ : [{ type: 'text', text: message.message.content }];
181
+ for (const item of content) {
182
+ if (item.type === 'tool_use') {
183
+ const toolName = item.name;
184
+ const toolUseId = item.id;
185
+ // Track tool_use_id to tool name mapping
186
+ if (toolUseId && toolName) {
187
+ toolUseIdMap.set(toolUseId, toolName);
188
+ // Increment total uses
189
+ if (!data.toolUsageMap.has(toolName)) {
190
+ data.toolUsageMap.set(toolName, { total: 0, successful: 0 });
191
+ }
192
+ data.toolUsageMap.get(toolName).total++;
193
+ }
194
+ }
195
+ }
196
+ }
197
+ }
198
+ /**
199
+ * Aggregate statistics for all projects
200
+ */
201
+ export async function aggregateAllProjects(cutoffDate) {
202
+ const data = createEmptyAggregatedData();
203
+ const projectsDir = join(CLAUDE_DIR, 'projects');
204
+ const projects = await readdir(projectsDir);
205
+ for (const project of projects) {
206
+ const projectPath = join(projectsDir, project);
207
+ const projectStat = await stat(projectPath);
208
+ if (!projectStat.isDirectory())
209
+ continue;
210
+ const files = await readdir(projectPath);
211
+ const name = getProjectName(project);
212
+ if (!data.projectMap.has(project)) {
213
+ data.projectMap.set(project, {
214
+ tokenUsage: createEmptyTokenUsage(),
215
+ sessionIds: new Set(),
216
+ name
217
+ });
218
+ }
219
+ for (const file of files) {
220
+ if (!file.endsWith('.jsonl'))
221
+ continue;
222
+ const filePath = join(projectPath, file);
223
+ const sessionId = file.replace('.jsonl', '');
224
+ if (isAgentSession(sessionId))
225
+ continue;
226
+ const fileStat = await stat(filePath);
227
+ if (isEmptyFile(fileStat.size))
228
+ continue;
229
+ const messages = await parseJsonl(filePath);
230
+ if (shouldSkipSession(messages))
231
+ continue;
232
+ const agentDescriptions = collectAgentDescriptions(messages);
233
+ const isAgent = agentDescriptions.size > 0;
234
+ if (isAgent) {
235
+ data.totalAgentSessions++;
236
+ }
237
+ const projectData = data.projectMap.get(project);
238
+ projectData.sessionIds.add(sessionId);
239
+ data.totalSessions++;
240
+ // Track tool_use_id to tool name mapping for this session
241
+ const toolUseIdMap = new Map();
242
+ for (const message of messages) {
243
+ const messageDate = new Date(message.timestamp);
244
+ if (messageDate < cutoffDate)
245
+ continue;
246
+ processMessage(message, sessionId, project, data, toolUseIdMap);
247
+ }
248
+ }
249
+ }
250
+ return data;
251
+ }
252
+ /**
253
+ * Aggregate statistics for a single project
254
+ */
255
+ export async function aggregateProject(projectId, cutoffDate) {
256
+ const data = createEmptyAggregatedData();
257
+ const projectsDir = join(CLAUDE_DIR, 'projects');
258
+ const projectPath = join(projectsDir, projectId);
259
+ const files = await readdir(projectPath);
260
+ const name = getProjectName(projectId);
261
+ for (const file of files) {
262
+ if (!file.endsWith('.jsonl'))
263
+ continue;
264
+ const filePath = join(projectPath, file);
265
+ const sessionId = file.replace('.jsonl', '');
266
+ if (isAgentSession(sessionId))
267
+ continue;
268
+ const fileStat = await stat(filePath);
269
+ if (isEmptyFile(fileStat.size))
270
+ continue;
271
+ const messages = await parseJsonl(filePath);
272
+ if (shouldSkipSession(messages))
273
+ continue;
274
+ const agentDescriptions = collectAgentDescriptions(messages);
275
+ const isAgent = agentDescriptions.size > 0;
276
+ if (isAgent) {
277
+ data.totalAgentSessions++;
278
+ }
279
+ data.totalSessions++;
280
+ // Track tool_use_id to tool name mapping for this session
281
+ const toolUseIdMap = new Map();
282
+ for (const message of messages) {
283
+ const messageDate = new Date(message.timestamp);
284
+ if (messageDate < cutoffDate)
285
+ continue;
286
+ processMessage(message, sessionId, projectId, data, toolUseIdMap);
287
+ }
288
+ }
289
+ return data;
290
+ }