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,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.5",
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",