claude-session-viewer 0.3.5 → 0.3.7

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.
@@ -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
+ }
@@ -0,0 +1,226 @@
1
+ import { PRICING, DEFAULT_PRICING_MODEL, WEEKDAY_NAMES } from '../claude/config.js';
2
+ import { aggregateTokenUsage, calculateCost } from './tokenUsage.js';
3
+ import { fillMissingDates } from './utils.js';
4
+ import { aggregateAllProjects, aggregateProject } from './aggregator.js';
5
+ /**
6
+ * Calculate cutoff date based on days parameter
7
+ */
8
+ function calculateCutoffDate(days) {
9
+ const cutoffDate = new Date();
10
+ if (days === 'all') {
11
+ cutoffDate.setFullYear(2000, 0, 1);
12
+ }
13
+ else {
14
+ const daysNum = typeof days === 'string' ? parseInt(days, 10) : days;
15
+ cutoffDate.setDate(cutoffDate.getDate() - daysNum);
16
+ cutoffDate.setHours(0, 0, 0, 0);
17
+ }
18
+ return cutoffDate;
19
+ }
20
+ /**
21
+ * Get overall usage statistics across all projects
22
+ */
23
+ export async function getOverallStatistics(days = 7) {
24
+ const daysParam = days.toString();
25
+ const cutoffDate = calculateCutoffDate(days);
26
+ const data = await aggregateAllProjects(cutoffDate);
27
+ // Calculate totals
28
+ const totalUsage = aggregateTokenUsage(data.allUsages);
29
+ const defaultPricing = PRICING[DEFAULT_PRICING_MODEL];
30
+ const costBreakdown = calculateCost(totalUsage, defaultPricing);
31
+ const savedCost = (data.totalCacheRead / 1000000) * (defaultPricing.input - defaultPricing.cacheRead);
32
+ // Determine date range first (this drives all other date-based statistics)
33
+ const startDate = daysParam === 'all' && data.minDate
34
+ ? data.minDate
35
+ : cutoffDate;
36
+ const endDate = new Date();
37
+ // Fill daily stats based on the determined date range
38
+ const daily = fillMissingDates(data.dailyMap, startDate, endDate);
39
+ // Build project stats
40
+ const byProject = Array.from(data.projectMap.entries())
41
+ .map(([project, projectData]) => ({
42
+ id: project,
43
+ name: projectData.name,
44
+ tokenUsage: projectData.tokenUsage,
45
+ sessionCount: projectData.sessionIds.size
46
+ }))
47
+ .sort((a, b) => b.tokenUsage.totalTokens - a.tokenUsage.totalTokens);
48
+ // Build model stats
49
+ const byModel = Array.from(data.modelMap.entries())
50
+ .map(([model, modelData]) => ({
51
+ model,
52
+ tokenUsage: modelData.tokenUsage,
53
+ messageCount: modelData.messageCount
54
+ }))
55
+ .sort((a, b) => b.tokenUsage.totalTokens - a.tokenUsage.totalTokens);
56
+ // Build tool usage stats
57
+ const toolUsage = Array.from(data.toolUsageMap.entries())
58
+ .map(([toolName, stats]) => ({
59
+ toolName,
60
+ totalUses: stats.total,
61
+ successfulUses: stats.successful,
62
+ successRate: stats.total > 0 ? (stats.successful / stats.total) * 100 : 0
63
+ }))
64
+ .sort((a, b) => b.totalUses - a.totalUses);
65
+ const totalToolCalls = Array.from(data.toolUsageMap.values()).reduce((sum, stats) => sum + stats.total, 0);
66
+ const agentUsageRate = data.totalSessions > 0 ? (data.totalAgentSessions / data.totalSessions) * 100 : 0;
67
+ // Build hourly stats
68
+ const byHour = Array.from(data.hourlyMap.entries())
69
+ .map(([hour, hourData]) => ({
70
+ hour,
71
+ sessionCount: hourData.sessionIds.size,
72
+ messageCount: hourData.messageCount,
73
+ tokenUsage: hourData.tokenUsage
74
+ }))
75
+ .sort((a, b) => a.hour - b.hour);
76
+ // Build weekday stats
77
+ const byWeekday = Array.from(data.weekdayMap.entries())
78
+ .map(([weekday, weekdayData]) => ({
79
+ weekday,
80
+ weekdayName: WEEKDAY_NAMES[weekday],
81
+ sessionCount: weekdayData.sessionIds.size,
82
+ messageCount: weekdayData.messageCount,
83
+ tokenUsage: weekdayData.tokenUsage
84
+ }))
85
+ .sort((a, b) => a.weekday - b.weekday);
86
+ const statistics = {
87
+ overview: {
88
+ tokenUsage: totalUsage,
89
+ sessionCount: data.totalSessions,
90
+ messageCount: data.totalMessages,
91
+ dateRange: {
92
+ start: startDate?.toISOString() || endDate.toISOString(),
93
+ end: endDate.toISOString()
94
+ }
95
+ },
96
+ daily,
97
+ byProject,
98
+ byModel,
99
+ cache: {
100
+ totalCacheCreation: data.totalCacheCreation,
101
+ totalCacheRead: data.totalCacheRead,
102
+ ephemeral5mTokens: data.ephemeral5mTokens,
103
+ ephemeral1hTokens: data.ephemeral1hTokens,
104
+ cacheHitRate: (data.totalCacheCreation + data.totalCacheRead) > 0
105
+ ? (data.totalCacheRead / (data.totalCacheCreation + data.totalCacheRead)) * 100
106
+ : 0,
107
+ estimatedSavings: savedCost
108
+ },
109
+ cost: costBreakdown,
110
+ productivity: {
111
+ toolUsage,
112
+ totalToolCalls,
113
+ agentSessions: data.totalAgentSessions,
114
+ totalSessions: data.totalSessions,
115
+ agentUsageRate
116
+ },
117
+ trends: {
118
+ byHour,
119
+ byWeekday
120
+ }
121
+ };
122
+ return statistics;
123
+ }
124
+ /**
125
+ * Get usage statistics for a specific project
126
+ */
127
+ export async function getProjectStatistics(projectId, days = 7) {
128
+ const daysParam = days.toString();
129
+ const cutoffDate = calculateCutoffDate(days);
130
+ const data = await aggregateProject(projectId, cutoffDate);
131
+ // Calculate totals
132
+ const totalUsage = aggregateTokenUsage(data.allUsages);
133
+ const defaultPricing = PRICING[DEFAULT_PRICING_MODEL];
134
+ const costBreakdown = calculateCost(totalUsage, defaultPricing);
135
+ const savedCost = (data.totalCacheRead / 1000000) * (defaultPricing.input - defaultPricing.cacheRead);
136
+ // Determine date range first (this drives all other date-based statistics)
137
+ const startDate = daysParam === 'all' && data.minDate
138
+ ? data.minDate
139
+ : cutoffDate;
140
+ const endDate = new Date();
141
+ // Fill daily stats based on the determined date range
142
+ const daily = fillMissingDates(data.dailyMap, startDate, endDate);
143
+ // Build project stats (single project)
144
+ const projectData = data.projectMap.get(projectId);
145
+ const byProject = projectData ? [{
146
+ id: projectId,
147
+ name: projectData.name,
148
+ tokenUsage: totalUsage,
149
+ sessionCount: data.totalSessions
150
+ }] : [];
151
+ // Build model stats
152
+ const byModel = Array.from(data.modelMap.entries())
153
+ .map(([model, modelData]) => ({
154
+ model,
155
+ tokenUsage: modelData.tokenUsage,
156
+ messageCount: modelData.messageCount
157
+ }))
158
+ .sort((a, b) => b.tokenUsage.totalTokens - a.tokenUsage.totalTokens);
159
+ // Build tool usage stats
160
+ const toolUsage = Array.from(data.toolUsageMap.entries())
161
+ .map(([toolName, stats]) => ({
162
+ toolName,
163
+ totalUses: stats.total,
164
+ successfulUses: stats.successful,
165
+ successRate: stats.total > 0 ? (stats.successful / stats.total) * 100 : 0
166
+ }))
167
+ .sort((a, b) => b.totalUses - a.totalUses);
168
+ const totalToolCalls = Array.from(data.toolUsageMap.values()).reduce((sum, stats) => sum + stats.total, 0);
169
+ const agentUsageRate = data.totalSessions > 0 ? (data.totalAgentSessions / data.totalSessions) * 100 : 0;
170
+ // Build hourly stats
171
+ const byHour = Array.from(data.hourlyMap.entries())
172
+ .map(([hour, hourData]) => ({
173
+ hour,
174
+ sessionCount: hourData.sessionIds.size,
175
+ messageCount: hourData.messageCount,
176
+ tokenUsage: hourData.tokenUsage
177
+ }))
178
+ .sort((a, b) => a.hour - b.hour);
179
+ // Build weekday stats
180
+ const byWeekday = Array.from(data.weekdayMap.entries())
181
+ .map(([weekday, weekdayData]) => ({
182
+ weekday,
183
+ weekdayName: WEEKDAY_NAMES[weekday],
184
+ sessionCount: weekdayData.sessionIds.size,
185
+ messageCount: weekdayData.messageCount,
186
+ tokenUsage: weekdayData.tokenUsage
187
+ }))
188
+ .sort((a, b) => a.weekday - b.weekday);
189
+ const statistics = {
190
+ overview: {
191
+ tokenUsage: totalUsage,
192
+ sessionCount: data.totalSessions,
193
+ messageCount: data.totalMessages,
194
+ dateRange: {
195
+ start: startDate?.toISOString() || endDate.toISOString(),
196
+ end: endDate.toISOString()
197
+ }
198
+ },
199
+ daily,
200
+ byProject,
201
+ byModel,
202
+ cache: {
203
+ totalCacheCreation: data.totalCacheCreation,
204
+ totalCacheRead: data.totalCacheRead,
205
+ ephemeral5mTokens: data.ephemeral5mTokens,
206
+ ephemeral1hTokens: data.ephemeral1hTokens,
207
+ cacheHitRate: (data.totalCacheCreation + data.totalCacheRead) > 0
208
+ ? (data.totalCacheRead / (data.totalCacheCreation + data.totalCacheRead)) * 100
209
+ : 0,
210
+ estimatedSavings: savedCost
211
+ },
212
+ cost: costBreakdown,
213
+ productivity: {
214
+ toolUsage,
215
+ totalToolCalls,
216
+ agentSessions: data.totalAgentSessions,
217
+ totalSessions: data.totalSessions,
218
+ agentUsageRate
219
+ },
220
+ trends: {
221
+ byHour,
222
+ byWeekday
223
+ }
224
+ };
225
+ return statistics;
226
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Token statistics utilities
3
+ * Pure calculation functions for token usage analysis
4
+ */
5
+ /**
6
+ * Extract token usage from a message object
7
+ */
8
+ export function extractTokenUsage(message) {
9
+ if (message.type !== 'assistant' || !message.message?.usage) {
10
+ return null;
11
+ }
12
+ const usage = message.message.usage;
13
+ const model = message.message.model || 'unknown';
14
+ return {
15
+ usage: {
16
+ inputTokens: usage.input_tokens || 0,
17
+ cacheCreationTokens: usage.cache_creation_input_tokens || 0,
18
+ cacheReadTokens: usage.cache_read_input_tokens || 0,
19
+ outputTokens: usage.output_tokens || 0,
20
+ totalTokens: (usage.input_tokens || 0) +
21
+ (usage.cache_creation_input_tokens || 0) +
22
+ (usage.cache_read_input_tokens || 0) +
23
+ (usage.output_tokens || 0)
24
+ },
25
+ model
26
+ };
27
+ }
28
+ /**
29
+ * Aggregate multiple token usages into a single total
30
+ */
31
+ export function aggregateTokenUsage(usages) {
32
+ return usages.reduce((acc, usage) => ({
33
+ inputTokens: acc.inputTokens + usage.inputTokens,
34
+ cacheCreationTokens: acc.cacheCreationTokens + usage.cacheCreationTokens,
35
+ cacheReadTokens: acc.cacheReadTokens + usage.cacheReadTokens,
36
+ outputTokens: acc.outputTokens + usage.outputTokens,
37
+ totalTokens: acc.totalTokens + usage.totalTokens
38
+ }), createEmptyTokenUsage());
39
+ }
40
+ /**
41
+ * Calculate cost for token usage based on pricing
42
+ */
43
+ export function calculateCost(usage, pricing) {
44
+ return {
45
+ inputCost: (usage.inputTokens / 1000000) * pricing.input,
46
+ outputCost: (usage.outputTokens / 1000000) * pricing.output,
47
+ cacheCreationCost: (usage.cacheCreationTokens / 1000000) * pricing.cacheCreation,
48
+ cacheReadCost: (usage.cacheReadTokens / 1000000) * pricing.cacheRead,
49
+ totalCost: (usage.inputTokens / 1000000) * pricing.input +
50
+ (usage.outputTokens / 1000000) * pricing.output +
51
+ (usage.cacheCreationTokens / 1000000) * pricing.cacheCreation +
52
+ (usage.cacheReadTokens / 1000000) * pricing.cacheRead
53
+ };
54
+ }
55
+ /**
56
+ * Create an empty token usage object
57
+ */
58
+ export function createEmptyTokenUsage() {
59
+ return {
60
+ inputTokens: 0,
61
+ cacheCreationTokens: 0,
62
+ cacheReadTokens: 0,
63
+ outputTokens: 0,
64
+ totalTokens: 0
65
+ };
66
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Token statistics utilities
3
+ * Pure calculation functions for token usage analysis
4
+ */
5
+ /**
6
+ * Extract token usage from a message object
7
+ */
8
+ export function extractTokenUsage(message) {
9
+ if (message.type !== 'assistant' || !message.message?.usage) {
10
+ return null;
11
+ }
12
+ const usage = message.message.usage;
13
+ const model = message.message.model || 'unknown';
14
+ return {
15
+ usage: {
16
+ inputTokens: usage.input_tokens || 0,
17
+ cacheCreationTokens: usage.cache_creation_input_tokens || 0,
18
+ cacheReadTokens: usage.cache_read_input_tokens || 0,
19
+ outputTokens: usage.output_tokens || 0,
20
+ totalTokens: (usage.input_tokens || 0) +
21
+ (usage.cache_creation_input_tokens || 0) +
22
+ (usage.cache_read_input_tokens || 0) +
23
+ (usage.output_tokens || 0)
24
+ },
25
+ model
26
+ };
27
+ }
28
+ /**
29
+ * Aggregate multiple token usages into a single total
30
+ */
31
+ export function aggregateTokenUsage(usages) {
32
+ return usages.reduce((acc, usage) => ({
33
+ inputTokens: acc.inputTokens + usage.inputTokens,
34
+ cacheCreationTokens: acc.cacheCreationTokens + usage.cacheCreationTokens,
35
+ cacheReadTokens: acc.cacheReadTokens + usage.cacheReadTokens,
36
+ outputTokens: acc.outputTokens + usage.outputTokens,
37
+ totalTokens: acc.totalTokens + usage.totalTokens
38
+ }), createEmptyTokenUsage());
39
+ }
40
+ /**
41
+ * Calculate cost for token usage based on pricing
42
+ */
43
+ export function calculateCost(usage, pricing) {
44
+ return {
45
+ inputCost: (usage.inputTokens / 1000000) * pricing.input,
46
+ outputCost: (usage.outputTokens / 1000000) * pricing.output,
47
+ cacheCreationCost: (usage.cacheCreationTokens / 1000000) * pricing.cacheCreation,
48
+ cacheReadCost: (usage.cacheReadTokens / 1000000) * pricing.cacheRead,
49
+ totalCost: (usage.inputTokens / 1000000) * pricing.input +
50
+ (usage.outputTokens / 1000000) * pricing.output +
51
+ (usage.cacheCreationTokens / 1000000) * pricing.cacheCreation +
52
+ (usage.cacheReadTokens / 1000000) * pricing.cacheRead
53
+ };
54
+ }
55
+ /**
56
+ * Create an empty token usage object
57
+ */
58
+ export function createEmptyTokenUsage() {
59
+ return {
60
+ inputTokens: 0,
61
+ cacheCreationTokens: 0,
62
+ cacheReadTokens: 0,
63
+ outputTokens: 0,
64
+ totalTokens: 0
65
+ };
66
+ }
@@ -0,0 +1,33 @@
1
+ import { createEmptyTokenUsage } from './tokenUsage.js';
2
+ /**
3
+ * Format date to YYYY-MM-DD in local timezone
4
+ */
5
+ function formatDateLocal(date) {
6
+ const year = date.getFullYear();
7
+ const month = String(date.getMonth() + 1).padStart(2, '0');
8
+ const day = String(date.getDate()).padStart(2, '0');
9
+ return `${year}-${month}-${day}`;
10
+ }
11
+ /**
12
+ * Fill in missing dates in daily statistics with zero values
13
+ * @param dailyMap Map of date string to usage data
14
+ * @param startDate Start date for the range (inclusive)
15
+ * @param endDate End date for the range (inclusive)
16
+ * @returns Array of daily stats with all dates filled in
17
+ */
18
+ export function fillMissingDates(dailyMap, startDate, endDate) {
19
+ const daily = [];
20
+ const currentDate = new Date(startDate);
21
+ const end = new Date(endDate);
22
+ while (currentDate <= end) {
23
+ const dateKey = formatDateLocal(currentDate);
24
+ const data = dailyMap.get(dateKey);
25
+ daily.push({
26
+ date: dateKey,
27
+ tokenUsage: data?.tokenUsage || createEmptyTokenUsage(),
28
+ sessionCount: data?.sessionIds.size || 0
29
+ });
30
+ currentDate.setDate(currentDate.getDate() + 1);
31
+ }
32
+ return daily;
33
+ }