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.
@@ -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
+ }
@@ -0,0 +1,159 @@
1
+ import { SESSION_WINDOW_DURATION_MS, EXPIRING_SOON_THRESHOLD_MINUTES, PLAN_LIMITS } from '../claude/config.js';
2
+ import { extractTokenUsage } from './tokenStats.js';
3
+ /**
4
+ * Calculate 5-hour session windows from messages
5
+ * Claude Code uses a 5-hour rolling window starting from the first message
6
+ */
7
+ export function calculateSessionWindows(sessionsData, planConfig) {
8
+ const windows = [];
9
+ const windowMap = new Map();
10
+ // Collect all messages with timestamps
11
+ const allMessages = [];
12
+ for (const session of sessionsData) {
13
+ for (const message of session.messages) {
14
+ if (message.timestamp) {
15
+ allMessages.push({ message, sessionId: session.sessionId });
16
+ }
17
+ }
18
+ }
19
+ // Sort messages by timestamp
20
+ allMessages.sort((a, b) => new Date(a.message.timestamp).getTime() - new Date(b.message.timestamp).getTime());
21
+ if (allMessages.length === 0) {
22
+ return [];
23
+ }
24
+ // Group messages into 5-hour windows
25
+ for (const { message, sessionId } of allMessages) {
26
+ const messageTime = new Date(message.timestamp);
27
+ let assignedToWindow = false;
28
+ // Check existing windows to see if this message fits
29
+ for (const [windowId, windowData] of windowMap.entries()) {
30
+ const windowEnd = new Date(windowData.startTime.getTime() + SESSION_WINDOW_DURATION_MS);
31
+ if (messageTime >= windowData.startTime && messageTime < windowEnd) {
32
+ windowData.messages.push(message);
33
+ windowData.sessionIds.add(sessionId);
34
+ assignedToWindow = true;
35
+ break;
36
+ }
37
+ }
38
+ // Create new window if message doesn't fit in existing windows
39
+ if (!assignedToWindow) {
40
+ const windowId = `window-${messageTime.getTime()}`;
41
+ windowMap.set(windowId, {
42
+ messages: [message],
43
+ sessionIds: new Set([sessionId]),
44
+ startTime: messageTime
45
+ });
46
+ }
47
+ }
48
+ // Convert window map to SessionWindow objects
49
+ const now = new Date();
50
+ for (const [windowId, windowData] of windowMap.entries()) {
51
+ const startTime = windowData.startTime;
52
+ const endTime = new Date(startTime.getTime() + SESSION_WINDOW_DURATION_MS);
53
+ // Calculate token usage
54
+ let totalTokens = 0;
55
+ for (const message of windowData.messages) {
56
+ const tokenData = extractTokenUsage(message);
57
+ if (tokenData) {
58
+ totalTokens += tokenData.usage.totalTokens;
59
+ }
60
+ }
61
+ // Get first and last message times
62
+ const messageTimes = windowData.messages.map(m => new Date(m.timestamp).getTime());
63
+ const firstMessageTime = new Date(Math.min(...messageTimes));
64
+ const lastMessageTime = new Date(Math.max(...messageTimes));
65
+ // Calculate status
66
+ const remainingMs = endTime.getTime() - now.getTime();
67
+ const remainingMinutes = Math.floor(remainingMs / (1000 * 60));
68
+ const isCurrentWindow = now >= startTime && now < endTime;
69
+ let status = 'expired';
70
+ if (isCurrentWindow) {
71
+ if (remainingMinutes <= EXPIRING_SOON_THRESHOLD_MINUTES) {
72
+ status = 'expiring_soon';
73
+ }
74
+ else {
75
+ status = 'active';
76
+ }
77
+ }
78
+ const tokenLimit = planConfig.limits.tokensPerSession;
79
+ const percentage = (totalTokens / tokenLimit) * 100;
80
+ windows.push({
81
+ id: windowId,
82
+ startTime: startTime.toISOString(),
83
+ endTime: endTime.toISOString(),
84
+ firstMessageTime: firstMessageTime.toISOString(),
85
+ lastMessageTime: lastMessageTime.toISOString(),
86
+ tokenUsage: {
87
+ used: totalTokens,
88
+ limit: tokenLimit,
89
+ percentage: Math.min(percentage, 100)
90
+ },
91
+ messageCount: windowData.messages.length,
92
+ status,
93
+ sessionIds: Array.from(windowData.sessionIds),
94
+ remainingMinutes: isCurrentWindow ? Math.max(remainingMinutes, 0) : undefined,
95
+ isCurrentWindow
96
+ });
97
+ }
98
+ // Sort windows by start time (most recent first)
99
+ windows.sort((a, b) => new Date(b.startTime).getTime() - new Date(a.startTime).getTime());
100
+ return windows;
101
+ }
102
+ /**
103
+ * Get the currently active session window
104
+ */
105
+ export function getActiveWindow(windows) {
106
+ return windows.find(w => w.isCurrentWindow) || null;
107
+ }
108
+ /**
109
+ * Detect user's plan based on historical usage (P90 method)
110
+ * Analyzes the 90th percentile of token usage across sessions
111
+ */
112
+ export function detectUserPlan(windows) {
113
+ if (windows.length === 0) {
114
+ return {
115
+ name: 'Custom',
116
+ limits: PLAN_LIMITS.Custom,
117
+ autoDetected: false
118
+ };
119
+ }
120
+ // Get token usage from all windows
121
+ const tokenUsages = windows.map(w => w.tokenUsage.used).sort((a, b) => a - b);
122
+ // Calculate P90 (90th percentile)
123
+ const p90Index = Math.floor(tokenUsages.length * 0.9);
124
+ const p90Value = tokenUsages[p90Index] || 0;
125
+ // Match to known plans with tolerance
126
+ const tolerance = 0.15; // 15% tolerance
127
+ for (const [planName, limits] of Object.entries(PLAN_LIMITS)) {
128
+ const planLimit = limits.tokensPerSession;
129
+ const lowerBound = planLimit * (1 - tolerance);
130
+ const upperBound = planLimit * (1 + tolerance);
131
+ if (p90Value >= lowerBound && p90Value <= upperBound) {
132
+ const confidence = 100 - Math.abs((p90Value - planLimit) / planLimit) * 100;
133
+ return {
134
+ name: planName,
135
+ limits,
136
+ autoDetected: true,
137
+ confidence: Math.round(confidence)
138
+ };
139
+ }
140
+ }
141
+ // If no match, use Custom plan with P90 as limit
142
+ return {
143
+ name: 'Custom',
144
+ limits: {
145
+ tokensPerSession: Math.round(p90Value * 1.1), // Add 10% buffer
146
+ messagesPerSession: undefined
147
+ },
148
+ autoDetected: true,
149
+ confidence: 50
150
+ };
151
+ }
152
+ /**
153
+ * Load plan configuration from settings or auto-detect
154
+ */
155
+ export function loadPlanConfig(windows) {
156
+ // TODO: Load from user settings/config file if exists
157
+ // For now, use auto-detection
158
+ return detectUserPlan(windows);
159
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-session-viewer",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "claude-session-viewer": "./bin/cli.js"
@@ -14,7 +14,7 @@
14
14
  ],
15
15
  "scripts": {
16
16
  "dev": "node bin/dev.js",
17
- "dev:server": "tsx watch src/server/index.ts",
17
+ "dev:server": "tsx watch --clear-screen=false src/server/index.ts",
18
18
  "dev:client": "vite",
19
19
  "build:server": "tsc -p tsconfig.server.json",
20
20
  "build:client": "vite build --outDir dist/client",
@@ -1 +0,0 @@
1
- *,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.container{width:100%}@media (min-width: 640px){.container{max-width:640px}}@media (min-width: 768px){.container{max-width:768px}}@media (min-width: 1024px){.container{max-width:1024px}}@media (min-width: 1280px){.container{max-width:1280px}}@media (min-width: 1536px){.container{max-width:1536px}}.visible{visibility:visible}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.right-0{right:0}.top-0{top:0}.z-10{z-index:10}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.ml-2{margin-left:.5rem}.ml-4{margin-left:1rem}.ml-6{margin-left:1.5rem}.ml-auto{margin-left:auto}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.line-clamp-2{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.h-12{height:3rem}.h-2{height:.5rem}.h-3{height:.75rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-64{height:16rem}.h-8{height:2rem}.h-96{height:24rem}.h-full{height:100%}.h-screen{height:100vh}.w-1{width:.25rem}.w-1\/2{width:50%}.w-1\/3{width:33.333333%}.w-12{width:3rem}.w-16{width:4rem}.w-24{width:6rem}.w-3{width:.75rem}.w-3\/4{width:75%}.w-32{width:8rem}.w-4{width:1rem}.w-4\/5{width:80%}.w-5\/6{width:83.333333%}.w-64{width:16rem}.w-8{width:2rem}.w-full{width:100%}.min-w-0{min-width:0px}.max-w-4xl{max-width:56rem}.max-w-7xl{max-width:80rem}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.rotate-90{--tw-rotate: 90deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cursor-col-resize{cursor:col-resize}.resize{resize:both}.auto-rows-fr{grid-auto-rows:minmax(0,1fr)}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.items-baseline{align-items:baseline}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-l{border-left-width:1px}.border-l-2{border-left-width:2px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-blue-400{--tw-border-opacity: 1;border-color:rgb(96 165 250 / var(--tw-border-opacity, 1))}.border-blue-500{--tw-border-opacity: 1;border-color:rgb(59 130 246 / var(--tw-border-opacity, 1))}.border-blue-500\/20{border-color:#3b82f633}.border-cyan-500\/20{border-color:#06b6d433}.border-gray-700{--tw-border-opacity: 1;border-color:rgb(55 65 81 / var(--tw-border-opacity, 1))}.border-green-500\/20{border-color:#22c55e33}.border-orange-500\/20{border-color:#f9731633}.border-pink-500\/20{border-color:#ec489933}.border-purple-500\/20{border-color:#a855f733}.border-transparent{border-color:transparent}.bg-blue-500{--tw-bg-opacity: 1;background-color:rgb(59 130 246 / var(--tw-bg-opacity, 1))}.bg-blue-500\/10{background-color:#3b82f61a}.bg-blue-600{--tw-bg-opacity: 1;background-color:rgb(37 99 235 / var(--tw-bg-opacity, 1))}.bg-blue-900{--tw-bg-opacity: 1;background-color:rgb(30 58 138 / var(--tw-bg-opacity, 1))}.bg-blue-900\/50{background-color:#1e3a8a80}.bg-cyan-500{--tw-bg-opacity: 1;background-color:rgb(6 182 212 / var(--tw-bg-opacity, 1))}.bg-cyan-500\/10{background-color:#06b6d41a}.bg-gray-700{--tw-bg-opacity: 1;background-color:rgb(55 65 81 / var(--tw-bg-opacity, 1))}.bg-gray-800{--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity, 1))}.bg-gray-800\/30{background-color:#1f29374d}.bg-gray-800\/50{background-color:#1f293780}.bg-gray-900{--tw-bg-opacity: 1;background-color:rgb(17 24 39 / var(--tw-bg-opacity, 1))}.bg-gray-900\/50{background-color:#11182780}.bg-gray-900\/70{background-color:#111827b3}.bg-green-500{--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity, 1))}.bg-green-500\/10{background-color:#22c55e1a}.bg-green-900{--tw-bg-opacity: 1;background-color:rgb(20 83 45 / var(--tw-bg-opacity, 1))}.bg-green-900\/30{background-color:#14532d4d}.bg-green-900\/50{background-color:#14532d80}.bg-orange-500{--tw-bg-opacity: 1;background-color:rgb(249 115 22 / var(--tw-bg-opacity, 1))}.bg-orange-500\/10{background-color:#f973161a}.bg-pink-500{--tw-bg-opacity: 1;background-color:rgb(236 72 153 / var(--tw-bg-opacity, 1))}.bg-pink-500\/10{background-color:#ec48991a}.bg-purple-500{--tw-bg-opacity: 1;background-color:rgb(168 85 247 / var(--tw-bg-opacity, 1))}.bg-purple-500\/10{background-color:#a855f71a}.bg-purple-700{--tw-bg-opacity: 1;background-color:rgb(126 34 206 / var(--tw-bg-opacity, 1))}.bg-purple-900\/50{background-color:#581c8780}.bg-red-500{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity, 1))}.bg-transparent{background-color:transparent}.bg-yellow-500{--tw-bg-opacity: 1;background-color:rgb(234 179 8 / var(--tw-bg-opacity, 1))}.bg-yellow-900\/30{background-color:#713f124d}.bg-gradient-to-r{background-image:linear-gradient(to right,var(--tw-gradient-stops))}.from-blue-500{--tw-gradient-from: #3b82f6 var(--tw-gradient-from-position);--tw-gradient-to: rgb(59 130 246 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.to-green-500{--tw-gradient-to: #22c55e var(--tw-gradient-to-position)}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.pb-4{padding-bottom:1rem}.pl-14{padding-left:3.5rem}.pl-4{padding-left:1rem}.pl-8{padding-left:2rem}.pt-6{padding-top:1.5rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-\[10px\]{font-size:10px}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.leading-tight{line-height:1.25}.text-blue-200{--tw-text-opacity: 1;color:rgb(191 219 254 / var(--tw-text-opacity, 1))}.text-blue-300{--tw-text-opacity: 1;color:rgb(147 197 253 / var(--tw-text-opacity, 1))}.text-blue-400{--tw-text-opacity: 1;color:rgb(96 165 250 / var(--tw-text-opacity, 1))}.text-cyan-400{--tw-text-opacity: 1;color:rgb(34 211 238 / var(--tw-text-opacity, 1))}.text-gray-200{--tw-text-opacity: 1;color:rgb(229 231 235 / var(--tw-text-opacity, 1))}.text-gray-300{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity, 1))}.text-green-200{--tw-text-opacity: 1;color:rgb(187 247 208 / var(--tw-text-opacity, 1))}.text-green-300{--tw-text-opacity: 1;color:rgb(134 239 172 / var(--tw-text-opacity, 1))}.text-green-400{--tw-text-opacity: 1;color:rgb(74 222 128 / var(--tw-text-opacity, 1))}.text-purple-100{--tw-text-opacity: 1;color:rgb(243 232 255 / var(--tw-text-opacity, 1))}.text-purple-300{--tw-text-opacity: 1;color:rgb(216 180 254 / var(--tw-text-opacity, 1))}.text-purple-400{--tw-text-opacity: 1;color:rgb(192 132 252 / var(--tw-text-opacity, 1))}.text-red-400{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.text-yellow-400{--tw-text-opacity: 1;color:rgb(250 204 21 / var(--tw-text-opacity, 1))}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.\@container{container-type:inline-size}:root{font-family:Inter,system-ui,Avenir,Helvetica,Arial,sans-serif;line-height:1.5;font-weight:400;color-scheme:light dark;color:#ffffffde;background-color:#242424;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}body{margin:0;display:flex;place-items:center;min-width:320px;min-height:100vh}#root{width:100%;min-height:100vh}*{scrollbar-width:thin;scrollbar-color:#4B5563 #1F2937}*::-webkit-scrollbar{width:8px;height:8px}*::-webkit-scrollbar-track{background:#1f2937}*::-webkit-scrollbar-thumb{background-color:#4b5563;border-radius:4px}*::-webkit-scrollbar-thumb:hover{background-color:#6b7280}.last\:border-b-0:last-child{border-bottom-width:0px}.last\:pb-0:last-child{padding-bottom:0}.hover\:bg-gray-700:hover{--tw-bg-opacity: 1;background-color:rgb(55 65 81 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-700\/50:hover{background-color:#37415180}.hover\:bg-gray-800:hover{--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity, 1))}.hover\:bg-purple-600:hover{--tw-bg-opacity: 1;background-color:rgb(147 51 234 / var(--tw-bg-opacity, 1))}.hover\:text-blue-400:hover{--tw-text-opacity: 1;color:rgb(96 165 250 / var(--tw-text-opacity, 1))}.hover\:text-gray-200:hover{--tw-text-opacity: 1;color:rgb(229 231 235 / var(--tw-text-opacity, 1))}.hover\:text-gray-300:hover{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.group:hover .group-hover\:bg-blue-500{--tw-bg-opacity: 1;background-color:rgb(59 130 246 / var(--tw-bg-opacity, 1))}@container (min-width: 300px){.\@\[300px\]\:inline{display:inline}.\@\[300px\]\:hidden{display:none}}@media (min-width: 768px){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width: 1024px){.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}@media (min-width: 1280px){.xl\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.xl\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}