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.
- package/bin/dev.js +20 -1
- package/dist/client/assets/index-BKZOwcdy.css +1 -0
- package/dist/client/assets/index-Bo1qXVSw.js +70 -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-BREABlIc.js +0 -69
- package/dist/client/assets/index-nqJgfpl7.css +0 -1
|
@@ -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.
|
|
3
|
+
"version": "0.3.7",
|
|
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",
|