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.
- package/bin/dev.js +20 -1
- package/dist/client/assets/index-Csvqk3mE.js +70 -0
- package/dist/client/assets/index-DOk7moPK.css +1 -0
- package/dist/client/index.html +2 -2
- package/dist/server/claude/projects/repository.js +1 -1
- package/dist/server/claude/projects/service.js +4 -4
- package/dist/server/claude/sessions/service.js +9 -5
- package/dist/server/routes/sessionWindows.js +127 -0
- package/dist/server/routes/sessions.js +5 -4
- package/dist/server/routes/statistics.js +34 -310
- package/dist/server/statistics/aggregator.js +290 -0
- package/dist/server/statistics/service.js +226 -0
- package/dist/server/statistics/tokenStats.js +66 -0
- package/dist/server/statistics/tokenUsage.js +66 -0
- package/dist/server/statistics/utils.js +33 -0
- package/dist/server/utils/sessionWindows.js +159 -0
- package/package.json +2 -2
- package/dist/client/assets/index-DvK33tag.css +0 -1
- package/dist/client/assets/index-KEbXAXOS.js +0 -69
|
@@ -1,327 +1,51 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { stat } from 'fs/promises';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { CLAUDE_DIR } from '../constants.js';
|
|
4
|
-
import {
|
|
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
|
-
|
|
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
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
|
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
|
+
}
|