claude-code-templates 1.8.0 → 1.8.2
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/README.md +246 -0
- package/package.json +26 -12
- package/src/analytics/core/ConversationAnalyzer.js +754 -0
- package/src/analytics/core/FileWatcher.js +285 -0
- package/src/analytics/core/ProcessDetector.js +242 -0
- package/src/analytics/core/SessionAnalyzer.js +631 -0
- package/src/analytics/core/StateCalculator.js +190 -0
- package/src/analytics/data/DataCache.js +550 -0
- package/src/analytics/notifications/NotificationManager.js +448 -0
- package/src/analytics/notifications/WebSocketServer.js +526 -0
- package/src/analytics/utils/PerformanceMonitor.js +455 -0
- package/src/analytics-web/assets/js/main.js +312 -0
- package/src/analytics-web/components/Charts.js +114 -0
- package/src/analytics-web/components/ConversationTable.js +437 -0
- package/src/analytics-web/components/Dashboard.js +573 -0
- package/src/analytics-web/components/SessionTimer.js +596 -0
- package/src/analytics-web/index.html +882 -49
- package/src/analytics-web/index.html.original +1939 -0
- package/src/analytics-web/services/DataService.js +357 -0
- package/src/analytics-web/services/StateService.js +276 -0
- package/src/analytics-web/services/WebSocketService.js +523 -0
- package/src/analytics.js +641 -2311
- package/src/analytics.log +0 -0
|
@@ -0,0 +1,631 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SessionAnalyzer - Extracts session timing, token usage, and plan information
|
|
3
|
+
* Tracks Claude Max plan session limits and usage patterns
|
|
4
|
+
*/
|
|
5
|
+
const chalk = require('chalk');
|
|
6
|
+
|
|
7
|
+
class SessionAnalyzer {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.SESSION_DURATION = 5 * 60 * 60 * 1000; // 5 hours in milliseconds
|
|
10
|
+
this.MONTHLY_SESSION_LIMIT = 50;
|
|
11
|
+
|
|
12
|
+
// Plan-specific message limits (conservative estimates)
|
|
13
|
+
this.PLAN_LIMITS = {
|
|
14
|
+
'free': {
|
|
15
|
+
name: 'Free Plan',
|
|
16
|
+
messagesPerSession: null,
|
|
17
|
+
monthlyPrice: 0,
|
|
18
|
+
hasSessionLimits: false
|
|
19
|
+
},
|
|
20
|
+
'standard': {
|
|
21
|
+
name: 'Pro Plan',
|
|
22
|
+
messagesPerSession: 45,
|
|
23
|
+
monthlyPrice: 20,
|
|
24
|
+
hasSessionLimits: true
|
|
25
|
+
},
|
|
26
|
+
'max': {
|
|
27
|
+
name: 'Max Plan (5x)',
|
|
28
|
+
messagesPerSession: 225,
|
|
29
|
+
monthlyPrice: 100,
|
|
30
|
+
hasSessionLimits: true
|
|
31
|
+
},
|
|
32
|
+
'premium': {
|
|
33
|
+
name: 'Max Plan (5x)',
|
|
34
|
+
messagesPerSession: 900,
|
|
35
|
+
monthlyPrice: 200,
|
|
36
|
+
hasSessionLimits: true
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Analyze all conversations to extract session information
|
|
43
|
+
* @param {Array} conversations - Array of conversation objects with parsed messages
|
|
44
|
+
* @param {Object} claudeSessionInfo - Real Claude session information from statsig files
|
|
45
|
+
* @returns {Object} Session analysis data
|
|
46
|
+
*/
|
|
47
|
+
analyzeSessionData(conversations, claudeSessionInfo = null) {
|
|
48
|
+
let sessions, currentSession;
|
|
49
|
+
|
|
50
|
+
if (claudeSessionInfo && claudeSessionInfo.hasSession) {
|
|
51
|
+
// Use real Claude session information
|
|
52
|
+
sessions = this.extractSessionsFromClaudeInfo(conversations, claudeSessionInfo);
|
|
53
|
+
currentSession = this.getCurrentActiveSessionFromClaudeInfo(sessions, claudeSessionInfo);
|
|
54
|
+
} else {
|
|
55
|
+
// Fallback to old logic
|
|
56
|
+
sessions = this.extractSessions(conversations);
|
|
57
|
+
currentSession = this.getCurrentActiveSession(sessions);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const monthlyUsage = this.calculateMonthlyUsage(sessions);
|
|
61
|
+
const userPlan = this.detectUserPlan(conversations);
|
|
62
|
+
|
|
63
|
+
const limits = this.PLAN_LIMITS[userPlan.planType] || this.PLAN_LIMITS['standard'];
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
sessions,
|
|
67
|
+
currentSession,
|
|
68
|
+
monthlyUsage,
|
|
69
|
+
userPlan,
|
|
70
|
+
limits: limits,
|
|
71
|
+
warnings: this.generateWarnings(currentSession, monthlyUsage, userPlan),
|
|
72
|
+
claudeSessionInfo
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Calculate message complexity weight based on token usage
|
|
78
|
+
* Pro plan limits are based on message complexity, not just count
|
|
79
|
+
* @param {Object} message - Message object
|
|
80
|
+
* @returns {number} Message weight (1.0 = average message)
|
|
81
|
+
*/
|
|
82
|
+
calculateMessageWeight(message) {
|
|
83
|
+
// If we have token usage data, use it for more accurate weighting
|
|
84
|
+
if (message.usage && message.usage.input_tokens) {
|
|
85
|
+
// Average user message is ~200 English sentences = ~3000-4000 tokens
|
|
86
|
+
// But Claude Code messages tend to be shorter, so we use ~500 tokens as average
|
|
87
|
+
const AVERAGE_MESSAGE_TOKENS = 500;
|
|
88
|
+
const inputTokens = message.usage.input_tokens || 0;
|
|
89
|
+
const cacheTokens = message.usage.cache_creation_input_tokens || 0;
|
|
90
|
+
const totalTokens = inputTokens + cacheTokens;
|
|
91
|
+
|
|
92
|
+
// Calculate weight based on token count relative to average
|
|
93
|
+
const weight = Math.max(0.1, totalTokens / AVERAGE_MESSAGE_TOKENS);
|
|
94
|
+
|
|
95
|
+
// Cap maximum weight to prevent single very long messages from dominating
|
|
96
|
+
return Math.min(weight, 5.0);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Fallback: assume average message if no usage data
|
|
100
|
+
return 1.0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Calculate session usage based on weighted messages
|
|
105
|
+
* @param {Array} userMessages - Array of user messages
|
|
106
|
+
* @returns {Object} Usage statistics
|
|
107
|
+
*/
|
|
108
|
+
calculateSessionUsage(userMessages) {
|
|
109
|
+
let totalWeight = 0;
|
|
110
|
+
let shortMessages = 0;
|
|
111
|
+
let longMessages = 0;
|
|
112
|
+
|
|
113
|
+
userMessages.forEach(msg => {
|
|
114
|
+
const weight = this.calculateMessageWeight(msg);
|
|
115
|
+
totalWeight += weight;
|
|
116
|
+
|
|
117
|
+
if (weight < 0.5) shortMessages++;
|
|
118
|
+
else if (weight > 2.0) longMessages++;
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
messageCount: userMessages.length,
|
|
123
|
+
totalWeight: totalWeight,
|
|
124
|
+
shortMessages,
|
|
125
|
+
longMessages,
|
|
126
|
+
averageWeight: userMessages.length > 0 ? totalWeight / userMessages.length : 0
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Generate estimated messages for session analysis when parsedMessages is not available
|
|
132
|
+
* @param {Object} conversation - Conversation object
|
|
133
|
+
* @returns {Array} Array of estimated message objects
|
|
134
|
+
*/
|
|
135
|
+
generateEstimatedMessages(conversation) {
|
|
136
|
+
const messages = [];
|
|
137
|
+
const messageCount = conversation.messageCount || 0;
|
|
138
|
+
const created = new Date(conversation.created);
|
|
139
|
+
const lastModified = new Date(conversation.lastModified);
|
|
140
|
+
|
|
141
|
+
if (messageCount === 0) return messages;
|
|
142
|
+
|
|
143
|
+
// Estimate message distribution over time
|
|
144
|
+
const timeDiff = lastModified - created;
|
|
145
|
+
const timePerMessage = timeDiff / messageCount;
|
|
146
|
+
|
|
147
|
+
// Generate alternating user/assistant messages
|
|
148
|
+
for (let i = 0; i < messageCount; i++) {
|
|
149
|
+
const timestamp = new Date(created.getTime() + (i * timePerMessage));
|
|
150
|
+
const role = i % 2 === 0 ? 'user' : 'assistant';
|
|
151
|
+
|
|
152
|
+
messages.push({
|
|
153
|
+
timestamp: timestamp,
|
|
154
|
+
role: role,
|
|
155
|
+
usage: conversation.tokenUsage || null
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return messages;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Extract 5-hour sliding window sessions from conversations
|
|
164
|
+
* @param {Array} conversations - Array of conversation objects
|
|
165
|
+
* @returns {Array} Array of 5-hour session windows
|
|
166
|
+
*/
|
|
167
|
+
extractSessions(conversations) {
|
|
168
|
+
// Collect all messages from all conversations with timestamps
|
|
169
|
+
const allMessages = [];
|
|
170
|
+
|
|
171
|
+
conversations.forEach(conversation => {
|
|
172
|
+
// Skip conversations without message count or with zero messages
|
|
173
|
+
if (!conversation.messageCount || conversation.messageCount === 0) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Generate estimated messages based on token usage and timestamps
|
|
178
|
+
// This is a fallback when parsedMessages is not available
|
|
179
|
+
const estimatedMessages = this.generateEstimatedMessages(conversation);
|
|
180
|
+
|
|
181
|
+
estimatedMessages.forEach(message => {
|
|
182
|
+
allMessages.push({
|
|
183
|
+
timestamp: message.timestamp,
|
|
184
|
+
role: message.role,
|
|
185
|
+
conversationId: conversation.id,
|
|
186
|
+
usage: message.usage
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// Sort all messages by timestamp
|
|
192
|
+
allMessages.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
|
|
193
|
+
|
|
194
|
+
// Group messages into 5-hour sliding windows
|
|
195
|
+
const sessions = [];
|
|
196
|
+
const FIVE_HOURS_MS = 5 * 60 * 60 * 1000;
|
|
197
|
+
|
|
198
|
+
// Find first user message to start session tracking
|
|
199
|
+
const firstUserMessage = allMessages.find(msg => msg.role === 'user');
|
|
200
|
+
if (!firstUserMessage) return [];
|
|
201
|
+
|
|
202
|
+
let currentWindowStart = new Date(firstUserMessage.timestamp);
|
|
203
|
+
let sessionCounter = 1;
|
|
204
|
+
|
|
205
|
+
// Create sessions based on 5-hour windows
|
|
206
|
+
while (currentWindowStart <= new Date()) {
|
|
207
|
+
const windowEnd = new Date(currentWindowStart.getTime() + FIVE_HOURS_MS);
|
|
208
|
+
|
|
209
|
+
// Find messages within this 5-hour window
|
|
210
|
+
const windowMessages = allMessages.filter(msg => {
|
|
211
|
+
const msgTime = new Date(msg.timestamp);
|
|
212
|
+
return msgTime >= currentWindowStart && msgTime < windowEnd;
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
if (windowMessages.length > 0) {
|
|
216
|
+
const session = {
|
|
217
|
+
id: `session_${sessionCounter}`,
|
|
218
|
+
startTime: currentWindowStart,
|
|
219
|
+
endTime: windowEnd,
|
|
220
|
+
messages: windowMessages,
|
|
221
|
+
tokenUsage: {
|
|
222
|
+
input: 0,
|
|
223
|
+
output: 0,
|
|
224
|
+
cacheCreation: 0,
|
|
225
|
+
cacheRead: 0,
|
|
226
|
+
total: 0
|
|
227
|
+
},
|
|
228
|
+
conversations: [...new Set(windowMessages.map(msg => msg.conversationId))],
|
|
229
|
+
serviceTier: null,
|
|
230
|
+
isActive: false
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
// Calculate token usage for this window
|
|
234
|
+
windowMessages.forEach(message => {
|
|
235
|
+
if (message.usage) {
|
|
236
|
+
session.tokenUsage.input += message.usage.input_tokens || 0;
|
|
237
|
+
session.tokenUsage.output += message.usage.output_tokens || 0;
|
|
238
|
+
session.tokenUsage.cacheCreation += message.usage.cache_creation_input_tokens || 0;
|
|
239
|
+
session.tokenUsage.cacheRead += message.usage.cache_read_input_tokens || 0;
|
|
240
|
+
session.serviceTier = message.usage.service_tier || session.serviceTier;
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
session.tokenUsage.total = session.tokenUsage.input + session.tokenUsage.output +
|
|
245
|
+
session.tokenUsage.cacheCreation + session.tokenUsage.cacheRead;
|
|
246
|
+
|
|
247
|
+
// Calculate additional properties
|
|
248
|
+
const now = new Date();
|
|
249
|
+
session.duration = windowEnd - currentWindowStart;
|
|
250
|
+
// Only count USER messages for session limits (Claude Code only counts prompts, not responses)
|
|
251
|
+
const userMessages = windowMessages.filter(msg => msg.role === 'user');
|
|
252
|
+
|
|
253
|
+
// Calculate session usage with message complexity weighting
|
|
254
|
+
const sessionUsage = this.calculateSessionUsage(userMessages);
|
|
255
|
+
session.messageCount = sessionUsage.messageCount;
|
|
256
|
+
session.messageWeight = sessionUsage.totalWeight;
|
|
257
|
+
session.usageDetails = sessionUsage;
|
|
258
|
+
session.conversationCount = session.conversations.length;
|
|
259
|
+
|
|
260
|
+
// Session is active if current time is within this window
|
|
261
|
+
session.isActive = now >= currentWindowStart && now < windowEnd;
|
|
262
|
+
|
|
263
|
+
// Calculate time remaining in this window
|
|
264
|
+
if (session.isActive) {
|
|
265
|
+
session.timeRemaining = Math.max(0, windowEnd - now);
|
|
266
|
+
} else {
|
|
267
|
+
session.timeRemaining = 0;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
session.actualDuration = session.duration;
|
|
271
|
+
|
|
272
|
+
sessions.push(session);
|
|
273
|
+
sessionCounter++;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Move to next potential session start (look for next user message after current window)
|
|
277
|
+
const nextUserMessage = allMessages.find(msg =>
|
|
278
|
+
msg.role === 'user' && new Date(msg.timestamp) >= windowEnd
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
if (nextUserMessage) {
|
|
282
|
+
currentWindowStart = new Date(nextUserMessage.timestamp);
|
|
283
|
+
} else {
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Sort by start time (most recent first)
|
|
289
|
+
return sessions.sort((a, b) => b.startTime - a.startTime);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Extract sessions based on real Claude session information
|
|
295
|
+
* @param {Array} conversations - Array of conversation objects
|
|
296
|
+
* @param {Object} claudeSessionInfo - Real Claude session information
|
|
297
|
+
* @returns {Array} Array of session objects
|
|
298
|
+
*/
|
|
299
|
+
extractSessionsFromClaudeInfo(conversations, claudeSessionInfo) {
|
|
300
|
+
// Get all messages from all conversations
|
|
301
|
+
const allMessages = [];
|
|
302
|
+
|
|
303
|
+
conversations.forEach(conversation => {
|
|
304
|
+
// Skip conversations without message count or with zero messages
|
|
305
|
+
if (!conversation.messageCount || conversation.messageCount === 0) {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Generate estimated messages based on token usage and timestamps
|
|
310
|
+
// This is a fallback when parsedMessages is not available
|
|
311
|
+
const estimatedMessages = this.generateEstimatedMessages(conversation);
|
|
312
|
+
|
|
313
|
+
estimatedMessages.forEach(message => {
|
|
314
|
+
allMessages.push({
|
|
315
|
+
timestamp: message.timestamp,
|
|
316
|
+
role: message.role,
|
|
317
|
+
conversationId: conversation.id,
|
|
318
|
+
usage: message.usage
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// Sort all messages by timestamp
|
|
324
|
+
allMessages.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
|
|
325
|
+
|
|
326
|
+
// Create current session based on Claude's actual session window
|
|
327
|
+
const sessionStartTime = new Date(claudeSessionInfo.startTime);
|
|
328
|
+
const sessionEndTime = new Date(claudeSessionInfo.startTime + claudeSessionInfo.sessionLimit.ms);
|
|
329
|
+
const now = new Date();
|
|
330
|
+
|
|
331
|
+
// Find the first user message that occurred AT OR AFTER the Claude session started
|
|
332
|
+
// This handles cases where a conversation was ongoing when Claude session reset
|
|
333
|
+
const firstMessageAfterSessionStart = allMessages.find(msg => {
|
|
334
|
+
const msgTime = new Date(msg.timestamp);
|
|
335
|
+
return msg.role === 'user' && msgTime >= sessionStartTime;
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
let effectiveSessionStart = sessionStartTime;
|
|
339
|
+
if (firstMessageAfterSessionStart) {
|
|
340
|
+
effectiveSessionStart = new Date(firstMessageAfterSessionStart.timestamp);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Filter messages that are within the current Claude session window AND after the effective session start
|
|
344
|
+
const currentSessionMessages = allMessages.filter(msg => {
|
|
345
|
+
const msgTime = new Date(msg.timestamp);
|
|
346
|
+
return msgTime >= effectiveSessionStart && msgTime < sessionEndTime;
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
if (currentSessionMessages.length === 0) {
|
|
350
|
+
return [];
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Create the current session object
|
|
354
|
+
const session = {
|
|
355
|
+
id: `claude_session_${claudeSessionInfo.sessionId.substring(0, 8)}`,
|
|
356
|
+
startTime: effectiveSessionStart,
|
|
357
|
+
endTime: sessionEndTime,
|
|
358
|
+
messages: currentSessionMessages,
|
|
359
|
+
tokenUsage: {
|
|
360
|
+
input: 0,
|
|
361
|
+
output: 0,
|
|
362
|
+
cacheCreation: 0,
|
|
363
|
+
cacheRead: 0,
|
|
364
|
+
total: 0
|
|
365
|
+
},
|
|
366
|
+
conversations: [...new Set(currentSessionMessages.map(msg => msg.conversationId))],
|
|
367
|
+
serviceTier: null,
|
|
368
|
+
isActive: now >= sessionStartTime && now < sessionEndTime && !claudeSessionInfo.estimatedTimeRemaining.isExpired
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
// Calculate token usage for this session
|
|
372
|
+
currentSessionMessages.forEach(message => {
|
|
373
|
+
if (message.usage) {
|
|
374
|
+
session.tokenUsage.input += message.usage.input_tokens || 0;
|
|
375
|
+
session.tokenUsage.output += message.usage.output_tokens || 0;
|
|
376
|
+
session.tokenUsage.cacheCreation += message.usage.cache_creation_input_tokens || 0;
|
|
377
|
+
session.tokenUsage.cacheRead += message.usage.cache_read_input_tokens || 0;
|
|
378
|
+
session.serviceTier = message.usage.service_tier || session.serviceTier;
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
session.tokenUsage.total = session.tokenUsage.input + session.tokenUsage.output +
|
|
383
|
+
session.tokenUsage.cacheCreation + session.tokenUsage.cacheRead;
|
|
384
|
+
|
|
385
|
+
// Only count USER messages for session limits
|
|
386
|
+
const userMessages = currentSessionMessages.filter(msg => msg.role === 'user');
|
|
387
|
+
|
|
388
|
+
// Calculate session usage with message complexity weighting
|
|
389
|
+
const sessionUsage = this.calculateSessionUsage(userMessages);
|
|
390
|
+
session.messageCount = sessionUsage.messageCount;
|
|
391
|
+
session.messageWeight = sessionUsage.totalWeight;
|
|
392
|
+
session.usageDetails = sessionUsage;
|
|
393
|
+
session.conversationCount = session.conversations.length;
|
|
394
|
+
|
|
395
|
+
// Use Claude's actual time remaining
|
|
396
|
+
session.timeRemaining = Math.max(0, claudeSessionInfo.estimatedTimeRemaining.ms);
|
|
397
|
+
session.actualDuration = claudeSessionInfo.sessionDuration.ms;
|
|
398
|
+
session.duration = claudeSessionInfo.sessionLimit.ms;
|
|
399
|
+
|
|
400
|
+
return [session];
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Get current active session based on Claude session info
|
|
405
|
+
* @param {Array} sessions - Array of session objects
|
|
406
|
+
* @param {Object} claudeSessionInfo - Real Claude session information
|
|
407
|
+
* @returns {Object|null} Current active session or null
|
|
408
|
+
*/
|
|
409
|
+
getCurrentActiveSessionFromClaudeInfo(sessions, claudeSessionInfo) {
|
|
410
|
+
if (sessions.length === 0) return null;
|
|
411
|
+
|
|
412
|
+
// If Claude session is expired, return null
|
|
413
|
+
if (claudeSessionInfo.estimatedTimeRemaining.isExpired) {
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return sessions[0]; // Since we only create one session based on Claude's current session
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Get current active session
|
|
422
|
+
* @param {Array} sessions - Array of session objects
|
|
423
|
+
* @returns {Object|null} Current active session or null
|
|
424
|
+
*/
|
|
425
|
+
getCurrentActiveSession(sessions) {
|
|
426
|
+
return sessions.find(session => session.isActive) || null;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Calculate monthly usage statistics
|
|
431
|
+
* @param {Array} sessions - Array of session objects
|
|
432
|
+
* @returns {Object} Monthly usage data
|
|
433
|
+
*/
|
|
434
|
+
calculateMonthlyUsage(sessions) {
|
|
435
|
+
const now = new Date();
|
|
436
|
+
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
437
|
+
|
|
438
|
+
const monthlySessions = sessions.filter(session =>
|
|
439
|
+
session.startTime >= monthStart
|
|
440
|
+
);
|
|
441
|
+
|
|
442
|
+
const totalTokens = monthlySessions.reduce((sum, session) =>
|
|
443
|
+
sum + session.tokenUsage.total, 0
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
const totalMessages = monthlySessions.reduce((sum, session) =>
|
|
447
|
+
sum + session.messageCount, 0
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
return {
|
|
451
|
+
sessionCount: monthlySessions.length,
|
|
452
|
+
totalTokens,
|
|
453
|
+
totalMessages,
|
|
454
|
+
remainingSessions: Math.max(0, this.MONTHLY_SESSION_LIMIT - monthlySessions.length),
|
|
455
|
+
averageTokensPerSession: monthlySessions.length > 0 ?
|
|
456
|
+
Math.round(totalTokens / monthlySessions.length) : 0,
|
|
457
|
+
averageMessagesPerSession: monthlySessions.length > 0 ?
|
|
458
|
+
Math.round(totalMessages / monthlySessions.length) : 0
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Detect user plan based on service tier information
|
|
464
|
+
* @param {Array} conversations - Array of conversation objects
|
|
465
|
+
* @returns {Object} User plan information
|
|
466
|
+
*/
|
|
467
|
+
detectUserPlan(conversations) {
|
|
468
|
+
const serviceTiers = new Set();
|
|
469
|
+
let latestTier = null;
|
|
470
|
+
let latestTimestamp = null;
|
|
471
|
+
|
|
472
|
+
conversations.forEach(conversation => {
|
|
473
|
+
if (!conversation.parsedMessages) return;
|
|
474
|
+
|
|
475
|
+
conversation.parsedMessages.forEach(message => {
|
|
476
|
+
if (message.usage && message.usage.service_tier) {
|
|
477
|
+
serviceTiers.add(message.usage.service_tier);
|
|
478
|
+
|
|
479
|
+
if (!latestTimestamp || message.timestamp > latestTimestamp) {
|
|
480
|
+
latestTimestamp = message.timestamp;
|
|
481
|
+
latestTier = message.usage.service_tier;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
// Map service tier to plan type - Pro plan users typically have 'standard' service tier
|
|
488
|
+
// Default to Pro plan since most users have Pro plan
|
|
489
|
+
const planMapping = {
|
|
490
|
+
'free': 'free', // Free Plan - daily limits
|
|
491
|
+
'standard': 'standard', // Pro Plan - 45 messages per 5-hour session
|
|
492
|
+
'premium': 'premium', // Max Plan 20x - 900 messages per 5-hour session
|
|
493
|
+
'max': 'max' // Max Plan 5x - 225 messages per 5-hour session
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
const detectedTier = latestTier || 'standard';
|
|
497
|
+
const planType = planMapping[detectedTier] || 'standard';
|
|
498
|
+
|
|
499
|
+
return {
|
|
500
|
+
tier: detectedTier,
|
|
501
|
+
planType: planType,
|
|
502
|
+
allTiers: Array.from(serviceTiers),
|
|
503
|
+
confidence: latestTier ? 'high' : 'low',
|
|
504
|
+
lastDetected: latestTimestamp
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Generate warnings based on current usage
|
|
510
|
+
* @param {Object|null} currentSession - Current active session
|
|
511
|
+
* @param {Object} monthlyUsage - Monthly usage data
|
|
512
|
+
* @param {Object} userPlan - User plan information
|
|
513
|
+
* @returns {Array} Array of warning objects
|
|
514
|
+
*/
|
|
515
|
+
generateWarnings(currentSession, monthlyUsage, userPlan) {
|
|
516
|
+
const warnings = [];
|
|
517
|
+
const planLimits = this.PLAN_LIMITS[userPlan.planType] || this.PLAN_LIMITS['standard'];
|
|
518
|
+
|
|
519
|
+
// Session-level warnings
|
|
520
|
+
if (currentSession) {
|
|
521
|
+
const sessionProgress = currentSession.messageCount / planLimits.messagesPerSession;
|
|
522
|
+
|
|
523
|
+
if (sessionProgress >= 0.9) {
|
|
524
|
+
warnings.push({
|
|
525
|
+
type: 'session_limit_critical',
|
|
526
|
+
level: 'error',
|
|
527
|
+
message: `You're near your session message limit (${currentSession.messageCount}/${planLimits.messagesPerSession})`,
|
|
528
|
+
timeRemaining: currentSession.timeRemaining
|
|
529
|
+
});
|
|
530
|
+
} else if (sessionProgress >= 0.75) {
|
|
531
|
+
warnings.push({
|
|
532
|
+
type: 'session_limit_warning',
|
|
533
|
+
level: 'warning',
|
|
534
|
+
message: `75% of session messages used (${currentSession.messageCount}/${planLimits.messagesPerSession})`,
|
|
535
|
+
timeRemaining: currentSession.timeRemaining
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Time remaining warning
|
|
540
|
+
if (currentSession.timeRemaining < 30 * 60 * 1000) { // 30 minutes
|
|
541
|
+
warnings.push({
|
|
542
|
+
type: 'session_time_warning',
|
|
543
|
+
level: 'info',
|
|
544
|
+
message: `Session expires in ${Math.round(currentSession.timeRemaining / 60000)} minutes`,
|
|
545
|
+
timeRemaining: currentSession.timeRemaining
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Monthly warnings
|
|
551
|
+
const monthlyProgress = monthlyUsage.sessionCount / this.MONTHLY_SESSION_LIMIT;
|
|
552
|
+
|
|
553
|
+
if (monthlyProgress >= 0.9) {
|
|
554
|
+
warnings.push({
|
|
555
|
+
type: 'monthly_limit_critical',
|
|
556
|
+
level: 'error',
|
|
557
|
+
message: `You're near your monthly session limit (${monthlyUsage.sessionCount}/${this.MONTHLY_SESSION_LIMIT})`,
|
|
558
|
+
remainingSessions: monthlyUsage.remainingSessions
|
|
559
|
+
});
|
|
560
|
+
} else if (monthlyProgress >= 0.75) {
|
|
561
|
+
warnings.push({
|
|
562
|
+
type: 'monthly_limit_warning',
|
|
563
|
+
level: 'warning',
|
|
564
|
+
message: `75% of monthly sessions used (${monthlyUsage.sessionCount}/${this.MONTHLY_SESSION_LIMIT})`,
|
|
565
|
+
remainingSessions: monthlyUsage.remainingSessions
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
return warnings;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Format time remaining for display
|
|
574
|
+
* @param {number} milliseconds - Time in milliseconds
|
|
575
|
+
* @returns {string} Formatted time string
|
|
576
|
+
*/
|
|
577
|
+
formatTimeRemaining(milliseconds) {
|
|
578
|
+
if (milliseconds <= 0) return '0m';
|
|
579
|
+
|
|
580
|
+
const hours = Math.floor(milliseconds / (60 * 60 * 1000));
|
|
581
|
+
const minutes = Math.floor((milliseconds % (60 * 60 * 1000)) / (60 * 1000));
|
|
582
|
+
|
|
583
|
+
if (hours > 0) {
|
|
584
|
+
return `${hours}h ${minutes}m`;
|
|
585
|
+
}
|
|
586
|
+
return `${minutes}m`;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Get session timer data for dashboard display
|
|
591
|
+
* @param {Object} sessionData - Session analysis data
|
|
592
|
+
* @returns {Object} Timer display data
|
|
593
|
+
*/
|
|
594
|
+
getSessionTimerData(sessionData) {
|
|
595
|
+
const { currentSession, monthlyUsage, limits, warnings } = sessionData;
|
|
596
|
+
|
|
597
|
+
if (!currentSession) {
|
|
598
|
+
return {
|
|
599
|
+
hasActiveSession: false,
|
|
600
|
+
message: 'No active session',
|
|
601
|
+
nextSessionAvailable: true
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Ensure limits exist, fallback to standard plan
|
|
606
|
+
const planLimits = limits || this.PLAN_LIMITS['standard'];
|
|
607
|
+
|
|
608
|
+
// Use weighted message calculation for more accurate progress
|
|
609
|
+
const weightedProgress = (currentSession.messageWeight / planLimits.messagesPerSession) * 100;
|
|
610
|
+
|
|
611
|
+
return {
|
|
612
|
+
hasActiveSession: true,
|
|
613
|
+
timeRemaining: currentSession.timeRemaining,
|
|
614
|
+
timeRemainingFormatted: this.formatTimeRemaining(currentSession.timeRemaining),
|
|
615
|
+
messagesUsed: currentSession.messageCount,
|
|
616
|
+
messagesLimit: planLimits.messagesPerSession,
|
|
617
|
+
messageWeight: currentSession.messageWeight,
|
|
618
|
+
usageDetails: currentSession.usageDetails,
|
|
619
|
+
tokensUsed: currentSession.tokenUsage.total,
|
|
620
|
+
sessionProgress: weightedProgress,
|
|
621
|
+
sessionProgressSimple: (currentSession.messageCount / planLimits.messagesPerSession) * 100,
|
|
622
|
+
planName: planLimits.name,
|
|
623
|
+
monthlySessionsUsed: monthlyUsage.sessionCount,
|
|
624
|
+
monthlySessionsLimit: this.MONTHLY_SESSION_LIMIT,
|
|
625
|
+
warnings: warnings.filter(w => w.type.includes('session')),
|
|
626
|
+
willResetAt: currentSession.endTime
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
module.exports = SessionAnalyzer;
|