claude-code-templates 1.8.0 → 1.8.1

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,597 @@
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
+ * Extract 5-hour sliding window sessions from conversations
132
+ * @param {Array} conversations - Array of conversation objects
133
+ * @returns {Array} Array of 5-hour session windows
134
+ */
135
+ extractSessions(conversations) {
136
+ // Collect all messages from all conversations with timestamps
137
+ const allMessages = [];
138
+
139
+ conversations.forEach(conversation => {
140
+ if (!conversation.parsedMessages || conversation.parsedMessages.length === 0) {
141
+ return;
142
+ }
143
+
144
+ const sortedMessages = conversation.parsedMessages.sort((a, b) =>
145
+ new Date(a.timestamp) - new Date(b.timestamp)
146
+ );
147
+
148
+ sortedMessages.forEach(message => {
149
+ allMessages.push({
150
+ timestamp: message.timestamp,
151
+ role: message.role,
152
+ conversationId: conversation.id,
153
+ usage: message.usage
154
+ });
155
+ });
156
+ });
157
+
158
+ // Sort all messages by timestamp
159
+ allMessages.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
160
+
161
+ // Group messages into 5-hour sliding windows
162
+ const sessions = [];
163
+ const FIVE_HOURS_MS = 5 * 60 * 60 * 1000;
164
+
165
+ // Find first user message to start session tracking
166
+ const firstUserMessage = allMessages.find(msg => msg.role === 'user');
167
+ if (!firstUserMessage) return [];
168
+
169
+ let currentWindowStart = new Date(firstUserMessage.timestamp);
170
+ let sessionCounter = 1;
171
+
172
+ // Create sessions based on 5-hour windows
173
+ while (currentWindowStart <= new Date()) {
174
+ const windowEnd = new Date(currentWindowStart.getTime() + FIVE_HOURS_MS);
175
+
176
+ // Find messages within this 5-hour window
177
+ const windowMessages = allMessages.filter(msg => {
178
+ const msgTime = new Date(msg.timestamp);
179
+ return msgTime >= currentWindowStart && msgTime < windowEnd;
180
+ });
181
+
182
+ if (windowMessages.length > 0) {
183
+ const session = {
184
+ id: `session_${sessionCounter}`,
185
+ startTime: currentWindowStart,
186
+ endTime: windowEnd,
187
+ messages: windowMessages,
188
+ tokenUsage: {
189
+ input: 0,
190
+ output: 0,
191
+ cacheCreation: 0,
192
+ cacheRead: 0,
193
+ total: 0
194
+ },
195
+ conversations: [...new Set(windowMessages.map(msg => msg.conversationId))],
196
+ serviceTier: null,
197
+ isActive: false
198
+ };
199
+
200
+ // Calculate token usage for this window
201
+ windowMessages.forEach(message => {
202
+ if (message.usage) {
203
+ session.tokenUsage.input += message.usage.input_tokens || 0;
204
+ session.tokenUsage.output += message.usage.output_tokens || 0;
205
+ session.tokenUsage.cacheCreation += message.usage.cache_creation_input_tokens || 0;
206
+ session.tokenUsage.cacheRead += message.usage.cache_read_input_tokens || 0;
207
+ session.serviceTier = message.usage.service_tier || session.serviceTier;
208
+ }
209
+ });
210
+
211
+ session.tokenUsage.total = session.tokenUsage.input + session.tokenUsage.output +
212
+ session.tokenUsage.cacheCreation + session.tokenUsage.cacheRead;
213
+
214
+ // Calculate additional properties
215
+ const now = new Date();
216
+ session.duration = windowEnd - currentWindowStart;
217
+ // Only count USER messages for session limits (Claude Code only counts prompts, not responses)
218
+ const userMessages = windowMessages.filter(msg => msg.role === 'user');
219
+
220
+ // Calculate session usage with message complexity weighting
221
+ const sessionUsage = this.calculateSessionUsage(userMessages);
222
+ session.messageCount = sessionUsage.messageCount;
223
+ session.messageWeight = sessionUsage.totalWeight;
224
+ session.usageDetails = sessionUsage;
225
+ session.conversationCount = session.conversations.length;
226
+
227
+ // Session is active if current time is within this window
228
+ session.isActive = now >= currentWindowStart && now < windowEnd;
229
+
230
+ // Calculate time remaining in this window
231
+ if (session.isActive) {
232
+ session.timeRemaining = Math.max(0, windowEnd - now);
233
+ } else {
234
+ session.timeRemaining = 0;
235
+ }
236
+
237
+ session.actualDuration = session.duration;
238
+
239
+ sessions.push(session);
240
+ sessionCounter++;
241
+ }
242
+
243
+ // Move to next potential session start (look for next user message after current window)
244
+ const nextUserMessage = allMessages.find(msg =>
245
+ msg.role === 'user' && new Date(msg.timestamp) >= windowEnd
246
+ );
247
+
248
+ if (nextUserMessage) {
249
+ currentWindowStart = new Date(nextUserMessage.timestamp);
250
+ } else {
251
+ break;
252
+ }
253
+ }
254
+
255
+ // Sort by start time (most recent first)
256
+ return sessions.sort((a, b) => b.startTime - a.startTime);
257
+ }
258
+
259
+
260
+ /**
261
+ * Extract sessions based on real Claude session information
262
+ * @param {Array} conversations - Array of conversation objects
263
+ * @param {Object} claudeSessionInfo - Real Claude session information
264
+ * @returns {Array} Array of session objects
265
+ */
266
+ extractSessionsFromClaudeInfo(conversations, claudeSessionInfo) {
267
+ // Get all messages from all conversations
268
+ const allMessages = [];
269
+
270
+ conversations.forEach(conversation => {
271
+ if (!conversation.parsedMessages || conversation.parsedMessages.length === 0) {
272
+ return;
273
+ }
274
+
275
+ const sortedMessages = conversation.parsedMessages.sort((a, b) =>
276
+ new Date(a.timestamp) - new Date(b.timestamp)
277
+ );
278
+
279
+ sortedMessages.forEach(message => {
280
+ allMessages.push({
281
+ timestamp: message.timestamp,
282
+ role: message.role,
283
+ conversationId: conversation.id,
284
+ usage: message.usage
285
+ });
286
+ });
287
+ });
288
+
289
+ // Sort all messages by timestamp
290
+ allMessages.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
291
+
292
+ // Create current session based on Claude's actual session window
293
+ const sessionStartTime = new Date(claudeSessionInfo.startTime);
294
+ const sessionEndTime = new Date(claudeSessionInfo.startTime + claudeSessionInfo.sessionLimit.ms);
295
+ const now = new Date();
296
+
297
+ // Find the first user message that occurred AT OR AFTER the Claude session started
298
+ // This handles cases where a conversation was ongoing when Claude session reset
299
+ const firstMessageAfterSessionStart = allMessages.find(msg => {
300
+ const msgTime = new Date(msg.timestamp);
301
+ return msg.role === 'user' && msgTime >= sessionStartTime;
302
+ });
303
+
304
+ let effectiveSessionStart = sessionStartTime;
305
+ if (firstMessageAfterSessionStart) {
306
+ effectiveSessionStart = new Date(firstMessageAfterSessionStart.timestamp);
307
+ }
308
+
309
+ // Filter messages that are within the current Claude session window AND after the effective session start
310
+ const currentSessionMessages = allMessages.filter(msg => {
311
+ const msgTime = new Date(msg.timestamp);
312
+ return msgTime >= effectiveSessionStart && msgTime < sessionEndTime;
313
+ });
314
+
315
+ if (currentSessionMessages.length === 0) {
316
+ return [];
317
+ }
318
+
319
+ // Create the current session object
320
+ const session = {
321
+ id: `claude_session_${claudeSessionInfo.sessionId.substring(0, 8)}`,
322
+ startTime: effectiveSessionStart,
323
+ endTime: sessionEndTime,
324
+ messages: currentSessionMessages,
325
+ tokenUsage: {
326
+ input: 0,
327
+ output: 0,
328
+ cacheCreation: 0,
329
+ cacheRead: 0,
330
+ total: 0
331
+ },
332
+ conversations: [...new Set(currentSessionMessages.map(msg => msg.conversationId))],
333
+ serviceTier: null,
334
+ isActive: now >= sessionStartTime && now < sessionEndTime && !claudeSessionInfo.estimatedTimeRemaining.isExpired
335
+ };
336
+
337
+ // Calculate token usage for this session
338
+ currentSessionMessages.forEach(message => {
339
+ if (message.usage) {
340
+ session.tokenUsage.input += message.usage.input_tokens || 0;
341
+ session.tokenUsage.output += message.usage.output_tokens || 0;
342
+ session.tokenUsage.cacheCreation += message.usage.cache_creation_input_tokens || 0;
343
+ session.tokenUsage.cacheRead += message.usage.cache_read_input_tokens || 0;
344
+ session.serviceTier = message.usage.service_tier || session.serviceTier;
345
+ }
346
+ });
347
+
348
+ session.tokenUsage.total = session.tokenUsage.input + session.tokenUsage.output +
349
+ session.tokenUsage.cacheCreation + session.tokenUsage.cacheRead;
350
+
351
+ // Only count USER messages for session limits
352
+ const userMessages = currentSessionMessages.filter(msg => msg.role === 'user');
353
+
354
+ // Calculate session usage with message complexity weighting
355
+ const sessionUsage = this.calculateSessionUsage(userMessages);
356
+ session.messageCount = sessionUsage.messageCount;
357
+ session.messageWeight = sessionUsage.totalWeight;
358
+ session.usageDetails = sessionUsage;
359
+ session.conversationCount = session.conversations.length;
360
+
361
+ // Use Claude's actual time remaining
362
+ session.timeRemaining = Math.max(0, claudeSessionInfo.estimatedTimeRemaining.ms);
363
+ session.actualDuration = claudeSessionInfo.sessionDuration.ms;
364
+ session.duration = claudeSessionInfo.sessionLimit.ms;
365
+
366
+ return [session];
367
+ }
368
+
369
+ /**
370
+ * Get current active session based on Claude session info
371
+ * @param {Array} sessions - Array of session objects
372
+ * @param {Object} claudeSessionInfo - Real Claude session information
373
+ * @returns {Object|null} Current active session or null
374
+ */
375
+ getCurrentActiveSessionFromClaudeInfo(sessions, claudeSessionInfo) {
376
+ if (sessions.length === 0) return null;
377
+
378
+ // If Claude session is expired, return null
379
+ if (claudeSessionInfo.estimatedTimeRemaining.isExpired) {
380
+ return null;
381
+ }
382
+
383
+ return sessions[0]; // Since we only create one session based on Claude's current session
384
+ }
385
+
386
+ /**
387
+ * Get current active session
388
+ * @param {Array} sessions - Array of session objects
389
+ * @returns {Object|null} Current active session or null
390
+ */
391
+ getCurrentActiveSession(sessions) {
392
+ return sessions.find(session => session.isActive) || null;
393
+ }
394
+
395
+ /**
396
+ * Calculate monthly usage statistics
397
+ * @param {Array} sessions - Array of session objects
398
+ * @returns {Object} Monthly usage data
399
+ */
400
+ calculateMonthlyUsage(sessions) {
401
+ const now = new Date();
402
+ const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
403
+
404
+ const monthlySessions = sessions.filter(session =>
405
+ session.startTime >= monthStart
406
+ );
407
+
408
+ const totalTokens = monthlySessions.reduce((sum, session) =>
409
+ sum + session.tokenUsage.total, 0
410
+ );
411
+
412
+ const totalMessages = monthlySessions.reduce((sum, session) =>
413
+ sum + session.messageCount, 0
414
+ );
415
+
416
+ return {
417
+ sessionCount: monthlySessions.length,
418
+ totalTokens,
419
+ totalMessages,
420
+ remainingSessions: Math.max(0, this.MONTHLY_SESSION_LIMIT - monthlySessions.length),
421
+ averageTokensPerSession: monthlySessions.length > 0 ?
422
+ Math.round(totalTokens / monthlySessions.length) : 0,
423
+ averageMessagesPerSession: monthlySessions.length > 0 ?
424
+ Math.round(totalMessages / monthlySessions.length) : 0
425
+ };
426
+ }
427
+
428
+ /**
429
+ * Detect user plan based on service tier information
430
+ * @param {Array} conversations - Array of conversation objects
431
+ * @returns {Object} User plan information
432
+ */
433
+ detectUserPlan(conversations) {
434
+ const serviceTiers = new Set();
435
+ let latestTier = null;
436
+ let latestTimestamp = null;
437
+
438
+ conversations.forEach(conversation => {
439
+ if (!conversation.parsedMessages) return;
440
+
441
+ conversation.parsedMessages.forEach(message => {
442
+ if (message.usage && message.usage.service_tier) {
443
+ serviceTiers.add(message.usage.service_tier);
444
+
445
+ if (!latestTimestamp || message.timestamp > latestTimestamp) {
446
+ latestTimestamp = message.timestamp;
447
+ latestTier = message.usage.service_tier;
448
+ }
449
+ }
450
+ });
451
+ });
452
+
453
+ // Map service tier to plan type - Pro plan users typically have 'standard' service tier
454
+ // Default to Pro plan since most users have Pro plan
455
+ const planMapping = {
456
+ 'free': 'free', // Free Plan - daily limits
457
+ 'standard': 'standard', // Pro Plan - 45 messages per 5-hour session
458
+ 'premium': 'premium', // Max Plan 20x - 900 messages per 5-hour session
459
+ 'max': 'max' // Max Plan 5x - 225 messages per 5-hour session
460
+ };
461
+
462
+ const detectedTier = latestTier || 'standard';
463
+ const planType = planMapping[detectedTier] || 'standard';
464
+
465
+ return {
466
+ tier: detectedTier,
467
+ planType: planType,
468
+ allTiers: Array.from(serviceTiers),
469
+ confidence: latestTier ? 'high' : 'low',
470
+ lastDetected: latestTimestamp
471
+ };
472
+ }
473
+
474
+ /**
475
+ * Generate warnings based on current usage
476
+ * @param {Object|null} currentSession - Current active session
477
+ * @param {Object} monthlyUsage - Monthly usage data
478
+ * @param {Object} userPlan - User plan information
479
+ * @returns {Array} Array of warning objects
480
+ */
481
+ generateWarnings(currentSession, monthlyUsage, userPlan) {
482
+ const warnings = [];
483
+ const planLimits = this.PLAN_LIMITS[userPlan.planType] || this.PLAN_LIMITS['standard'];
484
+
485
+ // Session-level warnings
486
+ if (currentSession) {
487
+ const sessionProgress = currentSession.messageCount / planLimits.messagesPerSession;
488
+
489
+ if (sessionProgress >= 0.9) {
490
+ warnings.push({
491
+ type: 'session_limit_critical',
492
+ level: 'error',
493
+ message: `You're near your session message limit (${currentSession.messageCount}/${planLimits.messagesPerSession})`,
494
+ timeRemaining: currentSession.timeRemaining
495
+ });
496
+ } else if (sessionProgress >= 0.75) {
497
+ warnings.push({
498
+ type: 'session_limit_warning',
499
+ level: 'warning',
500
+ message: `75% of session messages used (${currentSession.messageCount}/${planLimits.messagesPerSession})`,
501
+ timeRemaining: currentSession.timeRemaining
502
+ });
503
+ }
504
+
505
+ // Time remaining warning
506
+ if (currentSession.timeRemaining < 30 * 60 * 1000) { // 30 minutes
507
+ warnings.push({
508
+ type: 'session_time_warning',
509
+ level: 'info',
510
+ message: `Session expires in ${Math.round(currentSession.timeRemaining / 60000)} minutes`,
511
+ timeRemaining: currentSession.timeRemaining
512
+ });
513
+ }
514
+ }
515
+
516
+ // Monthly warnings
517
+ const monthlyProgress = monthlyUsage.sessionCount / this.MONTHLY_SESSION_LIMIT;
518
+
519
+ if (monthlyProgress >= 0.9) {
520
+ warnings.push({
521
+ type: 'monthly_limit_critical',
522
+ level: 'error',
523
+ message: `You're near your monthly session limit (${monthlyUsage.sessionCount}/${this.MONTHLY_SESSION_LIMIT})`,
524
+ remainingSessions: monthlyUsage.remainingSessions
525
+ });
526
+ } else if (monthlyProgress >= 0.75) {
527
+ warnings.push({
528
+ type: 'monthly_limit_warning',
529
+ level: 'warning',
530
+ message: `75% of monthly sessions used (${monthlyUsage.sessionCount}/${this.MONTHLY_SESSION_LIMIT})`,
531
+ remainingSessions: monthlyUsage.remainingSessions
532
+ });
533
+ }
534
+
535
+ return warnings;
536
+ }
537
+
538
+ /**
539
+ * Format time remaining for display
540
+ * @param {number} milliseconds - Time in milliseconds
541
+ * @returns {string} Formatted time string
542
+ */
543
+ formatTimeRemaining(milliseconds) {
544
+ if (milliseconds <= 0) return '0m';
545
+
546
+ const hours = Math.floor(milliseconds / (60 * 60 * 1000));
547
+ const minutes = Math.floor((milliseconds % (60 * 60 * 1000)) / (60 * 1000));
548
+
549
+ if (hours > 0) {
550
+ return `${hours}h ${minutes}m`;
551
+ }
552
+ return `${minutes}m`;
553
+ }
554
+
555
+ /**
556
+ * Get session timer data for dashboard display
557
+ * @param {Object} sessionData - Session analysis data
558
+ * @returns {Object} Timer display data
559
+ */
560
+ getSessionTimerData(sessionData) {
561
+ const { currentSession, monthlyUsage, limits, warnings } = sessionData;
562
+
563
+ if (!currentSession) {
564
+ return {
565
+ hasActiveSession: false,
566
+ message: 'No active session',
567
+ nextSessionAvailable: true
568
+ };
569
+ }
570
+
571
+ // Ensure limits exist, fallback to standard plan
572
+ const planLimits = limits || this.PLAN_LIMITS['standard'];
573
+
574
+ // Use weighted message calculation for more accurate progress
575
+ const weightedProgress = (currentSession.messageWeight / planLimits.messagesPerSession) * 100;
576
+
577
+ return {
578
+ hasActiveSession: true,
579
+ timeRemaining: currentSession.timeRemaining,
580
+ timeRemainingFormatted: this.formatTimeRemaining(currentSession.timeRemaining),
581
+ messagesUsed: currentSession.messageCount,
582
+ messagesLimit: planLimits.messagesPerSession,
583
+ messageWeight: currentSession.messageWeight,
584
+ usageDetails: currentSession.usageDetails,
585
+ tokensUsed: currentSession.tokenUsage.total,
586
+ sessionProgress: weightedProgress,
587
+ sessionProgressSimple: (currentSession.messageCount / planLimits.messagesPerSession) * 100,
588
+ planName: planLimits.name,
589
+ monthlySessionsUsed: monthlyUsage.sessionCount,
590
+ monthlySessionsLimit: this.MONTHLY_SESSION_LIMIT,
591
+ warnings: warnings.filter(w => w.type.includes('session')),
592
+ willResetAt: currentSession.endTime
593
+ };
594
+ }
595
+ }
596
+
597
+ module.exports = SessionAnalyzer;