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,754 @@
1
+ const chalk = require('chalk');
2
+ const fs = require('fs-extra');
3
+ const path = require('path');
4
+
5
+ /**
6
+ * ConversationAnalyzer - Handles conversation data loading, parsing, and analysis
7
+ * Extracted from monolithic analytics.js for better maintainability
8
+ */
9
+ class ConversationAnalyzer {
10
+ constructor(claudeDir, dataCache = null) {
11
+ this.claudeDir = claudeDir;
12
+ this.dataCache = dataCache;
13
+ this.data = {
14
+ conversations: [],
15
+ activeProjects: [],
16
+ summary: {},
17
+ orphanProcesses: [],
18
+ realtimeStats: {}
19
+ };
20
+ }
21
+
22
+ /**
23
+ * Main data loading orchestrator method
24
+ * @param {Object} stateCalculator - StateCalculator instance
25
+ * @param {Object} processDetector - ProcessDetector instance
26
+ * @returns {Promise<Object>} Complete analyzed data
27
+ */
28
+ async loadInitialData(stateCalculator, processDetector) {
29
+ console.log(chalk.yellow('📊 Analyzing Claude Code data...'));
30
+
31
+ try {
32
+ // Load conversation files
33
+ const conversations = await this.loadConversations(stateCalculator);
34
+ this.data.conversations = conversations;
35
+
36
+ // Load active projects
37
+ const projects = await this.loadActiveProjects();
38
+ this.data.activeProjects = projects;
39
+
40
+ // Detect active Claude processes and enrich data
41
+ const enrichmentResult = await processDetector.enrichWithRunningProcesses(
42
+ this.data.conversations,
43
+ this.claudeDir,
44
+ stateCalculator
45
+ );
46
+ this.data.conversations = enrichmentResult.conversations;
47
+ this.data.orphanProcesses = enrichmentResult.orphanProcesses;
48
+
49
+ // Calculate summary statistics with caching
50
+ this.data.summary = await this.calculateSummary(conversations, projects);
51
+
52
+ // Update realtime stats
53
+ this.updateRealtimeStats();
54
+
55
+ console.log(chalk.green('✅ Data analysis complete'));
56
+ console.log(chalk.gray(`Found ${conversations.length} conversations across ${projects.length} projects`));
57
+
58
+ return this.data;
59
+ } catch (error) {
60
+ console.error(chalk.red('Error loading Claude data:'), error.message);
61
+ throw error;
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Load and parse all conversation files recursively
67
+ * @param {Object} stateCalculator - StateCalculator instance for status determination
68
+ * @returns {Promise<Array>} Array of conversation objects
69
+ */
70
+ async loadConversations(stateCalculator) {
71
+ const conversations = [];
72
+
73
+ try {
74
+ // Search for .jsonl files recursively in all subdirectories
75
+ const findJsonlFiles = async (dir) => {
76
+ const files = [];
77
+ const items = await fs.readdir(dir);
78
+
79
+ for (const item of items) {
80
+ const itemPath = path.join(dir, item);
81
+ const stats = await fs.stat(itemPath);
82
+
83
+ if (stats.isDirectory()) {
84
+ // Recursively search subdirectories
85
+ const subFiles = await findJsonlFiles(itemPath);
86
+ files.push(...subFiles);
87
+ } else if (item.endsWith('.jsonl')) {
88
+ files.push(itemPath);
89
+ }
90
+ }
91
+
92
+ return files;
93
+ };
94
+
95
+ const jsonlFiles = await findJsonlFiles(this.claudeDir);
96
+ console.log(chalk.blue(`Found ${jsonlFiles.length} conversation files`));
97
+
98
+ for (const filePath of jsonlFiles) {
99
+ const stats = await this.getFileStats(filePath);
100
+ const filename = path.basename(filePath);
101
+
102
+ try {
103
+ // Extract project name from path
104
+ const projectFromPath = this.extractProjectFromPath(filePath);
105
+
106
+ // Use cached parsed conversation if available
107
+ const parsedMessages = await this.getParsedConversation(filePath);
108
+
109
+ // Calculate real token usage and extract model info with caching
110
+ const tokenUsage = await this.getCachedTokenUsage(filePath, parsedMessages);
111
+ const modelInfo = await this.getCachedModelInfo(filePath, parsedMessages);
112
+
113
+ const conversation = {
114
+ id: filename.replace('.jsonl', ''),
115
+ filename: filename,
116
+ filePath: filePath,
117
+ messageCount: parsedMessages.length,
118
+ fileSize: stats.size,
119
+ lastModified: stats.mtime,
120
+ created: stats.birthtime,
121
+ tokens: tokenUsage.total > 0 ? tokenUsage.total : this.estimateTokens(await this.getFileContent(filePath)),
122
+ tokenUsage: tokenUsage,
123
+ modelInfo: modelInfo,
124
+ project: projectFromPath || this.extractProjectFromConversation(parsedMessages),
125
+ status: stateCalculator.determineConversationStatus(parsedMessages, stats.mtime),
126
+ conversationState: stateCalculator.determineConversationState(parsedMessages, stats.mtime),
127
+ statusSquares: await this.getCachedStatusSquares(filePath, parsedMessages),
128
+ parsedMessages: parsedMessages, // Include parsed messages for session analysis
129
+ };
130
+
131
+ conversations.push(conversation);
132
+ } catch (error) {
133
+ console.warn(chalk.yellow(`Warning: Could not parse ${filename}:`, error.message));
134
+ }
135
+ }
136
+
137
+ return conversations.sort((a, b) => b.lastModified - a.lastModified);
138
+ } catch (error) {
139
+ console.error(chalk.red('Error loading conversations:'), error.message);
140
+ return [];
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Load active Claude projects from directory structure
146
+ * @returns {Promise<Array>} Array of project objects
147
+ */
148
+ async loadActiveProjects() {
149
+ const projects = [];
150
+
151
+ try {
152
+ const files = await fs.readdir(this.claudeDir);
153
+
154
+ for (const file of files) {
155
+ const filePath = path.join(this.claudeDir, file);
156
+ const stats = await fs.stat(filePath);
157
+
158
+ if (stats.isDirectory() && !file.startsWith('.')) {
159
+ const projectPath = filePath;
160
+ const todoFiles = await this.findTodoFiles(projectPath);
161
+
162
+ const project = {
163
+ name: file,
164
+ path: projectPath,
165
+ lastActivity: stats.mtime,
166
+ todoFiles: todoFiles.length,
167
+ status: this.determineProjectStatus(stats.mtime),
168
+ };
169
+
170
+ projects.push(project);
171
+ }
172
+ }
173
+
174
+ return projects.sort((a, b) => b.lastActivity - a.lastActivity);
175
+ } catch (error) {
176
+ console.error(chalk.red('Error loading projects:'), error.message);
177
+ return [];
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Get file content with caching support
183
+ * @param {string} filepath - Path to file
184
+ * @returns {Promise<string>} File content
185
+ */
186
+ async getFileContent(filepath) {
187
+ if (this.dataCache) {
188
+ return await this.dataCache.getFileContent(filepath);
189
+ }
190
+ return await fs.readFile(filepath, 'utf8');
191
+ }
192
+
193
+ /**
194
+ * Get file stats with caching support
195
+ * @param {string} filepath - Path to file
196
+ * @returns {Promise<Object>} File stats
197
+ */
198
+ async getFileStats(filepath) {
199
+ if (this.dataCache) {
200
+ return await this.dataCache.getFileStats(filepath);
201
+ }
202
+ return await fs.stat(filepath);
203
+ }
204
+
205
+ /**
206
+ * Get parsed conversation with caching support
207
+ * @param {string} filepath - Path to conversation file
208
+ * @returns {Promise<Array>} Parsed conversation messages
209
+ */
210
+ async getParsedConversation(filepath) {
211
+ if (this.dataCache) {
212
+ return await this.dataCache.getParsedConversation(filepath);
213
+ }
214
+
215
+ // Fallback to direct parsing
216
+ const content = await fs.readFile(filepath, 'utf8');
217
+ return content.trim().split('\n')
218
+ .filter(line => line.trim())
219
+ .map(line => {
220
+ try {
221
+ const item = JSON.parse(line);
222
+ if (item.message && item.message.role) {
223
+ return {
224
+ role: item.message.role,
225
+ timestamp: new Date(item.timestamp),
226
+ content: item.message.content,
227
+ model: item.message.model || null,
228
+ usage: item.message.usage || null,
229
+ };
230
+ }
231
+ } catch {}
232
+ return null;
233
+ })
234
+ .filter(Boolean);
235
+ }
236
+
237
+ /**
238
+ * Get cached token usage calculation
239
+ * @param {string} filepath - File path
240
+ * @param {Array} parsedMessages - Parsed messages array
241
+ * @returns {Promise<Object>} Token usage statistics
242
+ */
243
+ async getCachedTokenUsage(filepath, parsedMessages) {
244
+ if (this.dataCache) {
245
+ return await this.dataCache.getCachedTokenUsage(filepath, () => {
246
+ return this.calculateRealTokenUsage(parsedMessages);
247
+ });
248
+ }
249
+ return this.calculateRealTokenUsage(parsedMessages);
250
+ }
251
+
252
+ /**
253
+ * Get cached model info extraction
254
+ * @param {string} filepath - File path
255
+ * @param {Array} parsedMessages - Parsed messages array
256
+ * @returns {Promise<Object>} Model info data
257
+ */
258
+ async getCachedModelInfo(filepath, parsedMessages) {
259
+ if (this.dataCache) {
260
+ return await this.dataCache.getCachedModelInfo(filepath, () => {
261
+ return this.extractModelInfo(parsedMessages);
262
+ });
263
+ }
264
+ return this.extractModelInfo(parsedMessages);
265
+ }
266
+
267
+ /**
268
+ * Get cached status squares generation
269
+ * @param {string} filepath - File path
270
+ * @param {Array} parsedMessages - Parsed messages array
271
+ * @returns {Promise<Array>} Status squares data
272
+ */
273
+ async getCachedStatusSquares(filepath, parsedMessages) {
274
+ if (this.dataCache) {
275
+ return await this.dataCache.getCachedStatusSquares(filepath, () => {
276
+ return this.generateStatusSquares(parsedMessages);
277
+ });
278
+ }
279
+ return this.generateStatusSquares(parsedMessages);
280
+ }
281
+
282
+ /**
283
+ * Calculate real token usage from message usage data
284
+ * @param {Array} parsedMessages - Array of parsed message objects
285
+ * @returns {Object} Token usage statistics
286
+ */
287
+ calculateRealTokenUsage(parsedMessages) {
288
+ let totalInputTokens = 0;
289
+ let totalOutputTokens = 0;
290
+ let totalCacheCreationTokens = 0;
291
+ let totalCacheReadTokens = 0;
292
+ let messagesWithUsage = 0;
293
+
294
+ parsedMessages.forEach(message => {
295
+ if (message.usage) {
296
+ totalInputTokens += message.usage.input_tokens || 0;
297
+ totalOutputTokens += message.usage.output_tokens || 0;
298
+ totalCacheCreationTokens += message.usage.cache_creation_input_tokens || 0;
299
+ totalCacheReadTokens += message.usage.cache_read_input_tokens || 0;
300
+ messagesWithUsage++;
301
+ }
302
+ });
303
+
304
+ return {
305
+ total: totalInputTokens + totalOutputTokens,
306
+ inputTokens: totalInputTokens,
307
+ outputTokens: totalOutputTokens,
308
+ cacheCreationTokens: totalCacheCreationTokens,
309
+ cacheReadTokens: totalCacheReadTokens,
310
+ messagesWithUsage: messagesWithUsage,
311
+ totalMessages: parsedMessages.length,
312
+ };
313
+ }
314
+
315
+ /**
316
+ * Extract model and service tier information from messages
317
+ * @param {Array} parsedMessages - Array of parsed message objects
318
+ * @returns {Object} Model information
319
+ */
320
+ extractModelInfo(parsedMessages) {
321
+ const models = new Set();
322
+ const serviceTiers = new Set();
323
+ let lastModel = null;
324
+ let lastServiceTier = null;
325
+
326
+ parsedMessages.forEach(message => {
327
+ if (message.model) {
328
+ models.add(message.model);
329
+ lastModel = message.model;
330
+ }
331
+ if (message.usage && message.usage.service_tier) {
332
+ serviceTiers.add(message.usage.service_tier);
333
+ lastServiceTier = message.usage.service_tier;
334
+ }
335
+ });
336
+
337
+ return {
338
+ models: Array.from(models),
339
+ primaryModel: lastModel || models.values().next().value || 'Unknown',
340
+ serviceTiers: Array.from(serviceTiers),
341
+ currentServiceTier: lastServiceTier || serviceTiers.values().next().value || 'Unknown',
342
+ hasMultipleModels: models.size > 1,
343
+ };
344
+ }
345
+
346
+ /**
347
+ * Extract project name from Claude directory file path
348
+ * @param {string} filePath - Full path to conversation file
349
+ * @returns {string|null} Project name or null
350
+ */
351
+ extractProjectFromPath(filePath) {
352
+ // Extract project name from file path like:
353
+ // /Users/user/.claude/projects/-Users-user-Projects-MyProject/conversation.jsonl
354
+ const pathParts = filePath.split('/');
355
+ const projectIndex = pathParts.findIndex(part => part === 'projects');
356
+
357
+ if (projectIndex !== -1 && projectIndex + 1 < pathParts.length) {
358
+ const projectDir = pathParts[projectIndex + 1];
359
+ // Clean up the project directory name
360
+ const cleanName = projectDir
361
+ .replace(/^-/, '')
362
+ .replace(/-/g, '/')
363
+ .split('/')
364
+ .pop() || 'Unknown';
365
+
366
+ return cleanName;
367
+ }
368
+
369
+ return null;
370
+ }
371
+
372
+ /**
373
+ * Attempt to extract project information from conversation content
374
+ * @param {Array} messages - Array of message objects
375
+ * @returns {string} Project name or 'Unknown'
376
+ */
377
+ extractProjectFromConversation(messages) {
378
+ // Try to extract project information from conversation
379
+ for (const message of messages.slice(0, 5)) {
380
+ if (message.content && typeof message.content === 'string') {
381
+ const pathMatch = message.content.match(/\/([^\/\s]+)$/);
382
+ if (pathMatch) {
383
+ return pathMatch[1];
384
+ }
385
+ }
386
+ }
387
+ return 'Unknown';
388
+ }
389
+
390
+ /**
391
+ * Generate status indicators for conversation messages
392
+ * @param {Array} messages - Array of message objects
393
+ * @returns {Array} Array of status square objects
394
+ */
395
+ generateStatusSquares(messages) {
396
+ if (!messages || messages.length === 0) {
397
+ return [];
398
+ }
399
+
400
+ // Sort messages by timestamp and take last 10 for status squares
401
+ const sortedMessages = messages.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
402
+ const recentMessages = sortedMessages.slice(-10);
403
+
404
+ return recentMessages.map((message, index) => {
405
+ const messageNum = sortedMessages.length - recentMessages.length + index + 1;
406
+
407
+ // Determine status based on message content and role
408
+ if (message.role === 'user') {
409
+ return {
410
+ type: 'pending',
411
+ tooltip: `Message #${messageNum}: User input`,
412
+ };
413
+ } else if (message.role === 'assistant') {
414
+ // Check if the message contains tool usage or errors
415
+ const content = message.content || '';
416
+
417
+ if (typeof content === 'string') {
418
+ if (content.includes('[Tool:') || content.includes('tool_use')) {
419
+ return {
420
+ type: 'tool',
421
+ tooltip: `Message #${messageNum}: Tool execution`,
422
+ };
423
+ } else if (content.includes('error') || content.includes('Error') || content.includes('failed')) {
424
+ return {
425
+ type: 'error',
426
+ tooltip: `Message #${messageNum}: Error in response`,
427
+ };
428
+ } else {
429
+ return {
430
+ type: 'success',
431
+ tooltip: `Message #${messageNum}: Successful response`,
432
+ };
433
+ }
434
+ } else if (Array.isArray(content)) {
435
+ // Check for tool_use blocks in array content
436
+ const hasToolUse = content.some(block => block.type === 'tool_use');
437
+ const hasError = content.some(block =>
438
+ block.type === 'text' && (block.text?.includes('error') || block.text?.includes('Error'))
439
+ );
440
+
441
+ if (hasError) {
442
+ return {
443
+ type: 'error',
444
+ tooltip: `Message #${messageNum}: Error in response`,
445
+ };
446
+ } else if (hasToolUse) {
447
+ return {
448
+ type: 'tool',
449
+ tooltip: `Message #${messageNum}: Tool execution`,
450
+ };
451
+ } else {
452
+ return {
453
+ type: 'success',
454
+ tooltip: `Message #${messageNum}: Successful response`,
455
+ };
456
+ }
457
+ }
458
+ }
459
+
460
+ return {
461
+ type: 'pending',
462
+ tooltip: `Message #${messageNum}: Unknown status`,
463
+ };
464
+ });
465
+ }
466
+
467
+ /**
468
+ * Calculate summary statistics from conversations and projects data with caching
469
+ * @param {Array} conversations - Array of conversation objects
470
+ * @param {Array} projects - Array of project objects
471
+ * @returns {Promise<Object>} Summary statistics
472
+ */
473
+ async calculateSummary(conversations, projects) {
474
+ if (this.dataCache) {
475
+ const dependencies = conversations.map(conv => conv.filePath);
476
+ return await this.dataCache.getCachedComputation(
477
+ 'summary',
478
+ () => this.computeSummary(conversations, projects),
479
+ dependencies
480
+ );
481
+ }
482
+ return this.computeSummary(conversations, projects);
483
+ }
484
+
485
+ /**
486
+ * Compute summary statistics (internal method)
487
+ * @param {Array} conversations - Array of conversation objects
488
+ * @param {Array} projects - Array of project objects
489
+ * @returns {Promise<Object>} Summary statistics
490
+ */
491
+ async computeSummary(conversations, projects) {
492
+ const totalTokens = conversations.reduce((sum, conv) => sum + conv.tokens, 0);
493
+ const totalConversations = conversations.length;
494
+ const activeConversations = conversations.filter(c => c.status === 'active').length;
495
+ const activeProjects = projects.filter(p => p.status === 'active').length;
496
+
497
+ const avgTokensPerConversation = totalConversations > 0 ? Math.round(totalTokens / totalConversations) : 0;
498
+ const totalFileSize = conversations.reduce((sum, conv) => sum + conv.fileSize, 0);
499
+
500
+ // Calculate real Claude sessions (5-hour periods)
501
+ const claudeSessions = await this.calculateClaudeSessions(conversations);
502
+
503
+ return {
504
+ totalConversations,
505
+ totalTokens,
506
+ activeConversations,
507
+ activeProjects,
508
+ avgTokensPerConversation,
509
+ totalFileSize: this.formatBytes(totalFileSize),
510
+ lastActivity: conversations.length > 0 ? conversations[0].lastModified : null,
511
+ claudeSessions,
512
+ };
513
+ }
514
+
515
+ /**
516
+ * Calculate Claude usage sessions based on 5-hour periods with caching
517
+ * @param {Array} conversations - Array of conversation objects
518
+ * @returns {Promise<Object>} Session statistics
519
+ */
520
+ async calculateClaudeSessions(conversations) {
521
+ if (this.dataCache) {
522
+ const dependencies = conversations.map(conv => conv.filePath);
523
+ return await this.dataCache.getCachedComputation(
524
+ 'sessions',
525
+ () => this.computeClaudeSessions(conversations),
526
+ dependencies
527
+ );
528
+ }
529
+ return this.computeClaudeSessions(conversations);
530
+ }
531
+
532
+ /**
533
+ * Compute Claude usage sessions (internal method)
534
+ * @param {Array} conversations - Array of conversation objects
535
+ * @returns {Promise<Object>} Session statistics
536
+ */
537
+ async computeClaudeSessions(conversations) {
538
+ // Collect all message timestamps across all conversations
539
+ const allMessages = [];
540
+
541
+ for (const conv of conversations) {
542
+ // Use cached file content for better performance
543
+ try {
544
+ const content = await this.getFileContent(conv.filePath);
545
+ const lines = content.trim().split('\n').filter(line => line.trim());
546
+
547
+ lines.forEach(line => {
548
+ try {
549
+ const item = JSON.parse(line);
550
+ if (item.timestamp && item.message && item.message.role === 'user') {
551
+ // Only count user messages as session starters
552
+ allMessages.push({
553
+ timestamp: new Date(item.timestamp),
554
+ conversationId: conv.id,
555
+ });
556
+ }
557
+ } catch {}
558
+ });
559
+ } catch {}
560
+ };
561
+
562
+ if (allMessages.length === 0) return {
563
+ total: 0,
564
+ currentMonth: 0,
565
+ thisWeek: 0
566
+ };
567
+
568
+ // Sort messages by timestamp
569
+ allMessages.sort((a, b) => a.timestamp - b.timestamp);
570
+
571
+ // Calculate sessions (5-hour periods)
572
+ const sessions = [];
573
+ let currentSession = null;
574
+
575
+ allMessages.forEach(message => {
576
+ if (!currentSession) {
577
+ // Start first session
578
+ currentSession = {
579
+ start: message.timestamp,
580
+ end: new Date(message.timestamp.getTime() + 5 * 60 * 60 * 1000), // +5 hours
581
+ messageCount: 1,
582
+ conversations: new Set([message.conversationId]),
583
+ };
584
+ } else if (message.timestamp <= currentSession.end) {
585
+ // Message is within current session
586
+ currentSession.messageCount++;
587
+ currentSession.conversations.add(message.conversationId);
588
+ // Update session end if this message extends beyond current session
589
+ const potentialEnd = new Date(message.timestamp.getTime() + 5 * 60 * 60 * 1000);
590
+ if (potentialEnd > currentSession.end) {
591
+ currentSession.end = potentialEnd;
592
+ }
593
+ } else {
594
+ // Message is outside current session, start new session
595
+ sessions.push(currentSession);
596
+ currentSession = {
597
+ start: message.timestamp,
598
+ end: new Date(message.timestamp.getTime() + 5 * 60 * 60 * 1000),
599
+ messageCount: 1,
600
+ conversations: new Set([message.conversationId]),
601
+ };
602
+ }
603
+ });
604
+
605
+ // Add the last session
606
+ if (currentSession) {
607
+ sessions.push(currentSession);
608
+ }
609
+
610
+ // Calculate statistics
611
+ const now = new Date();
612
+ const currentMonth = new Date(now.getFullYear(), now.getMonth(), 1);
613
+ const thisWeek = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
614
+
615
+ const currentMonthSessions = sessions.filter(s => s.start >= currentMonth).length;
616
+ const thisWeekSessions = sessions.filter(s => s.start >= thisWeek).length;
617
+
618
+ return {
619
+ total: sessions.length,
620
+ currentMonth: currentMonthSessions,
621
+ thisWeek: thisWeekSessions,
622
+ sessions: sessions.map(s => ({
623
+ start: s.start,
624
+ end: s.end,
625
+ messageCount: s.messageCount,
626
+ conversationCount: s.conversations.size,
627
+ duration: Math.round((s.end - s.start) / (1000 * 60 * 60) * 10) / 10, // hours with 1 decimal
628
+ })),
629
+ };
630
+ }
631
+
632
+ /**
633
+ * Simple token estimation fallback
634
+ * @param {string} text - Text to estimate tokens for
635
+ * @returns {number} Estimated token count
636
+ */
637
+ estimateTokens(text) {
638
+ // Simple token estimation (roughly 4 characters per token)
639
+ return Math.ceil(text.length / 4);
640
+ }
641
+
642
+ /**
643
+ * Find TODO files in project directories
644
+ * @param {string} projectPath - Path to project directory
645
+ * @returns {Promise<Array>} Array of TODO file names
646
+ */
647
+ async findTodoFiles(projectPath) {
648
+ try {
649
+ const files = await fs.readdir(projectPath);
650
+ return files.filter(file => file.includes('todo') || file.includes('TODO'));
651
+ } catch {
652
+ return [];
653
+ }
654
+ }
655
+
656
+ /**
657
+ * Determine project activity status based on last modification time
658
+ * @param {Date} lastActivity - Last activity timestamp
659
+ * @returns {string} Status: 'active', 'recent', or 'inactive'
660
+ */
661
+ determineProjectStatus(lastActivity) {
662
+ const now = new Date();
663
+ const timeDiff = now - lastActivity;
664
+ const hoursAgo = timeDiff / (1000 * 60 * 60);
665
+
666
+ if (hoursAgo < 1) return 'active';
667
+ if (hoursAgo < 24) return 'recent';
668
+ return 'inactive';
669
+ }
670
+
671
+ /**
672
+ * Update real-time statistics cache
673
+ */
674
+ updateRealtimeStats() {
675
+ this.data.realtimeStats = {
676
+ totalConversations: this.data.conversations.length,
677
+ totalTokens: this.data.conversations.reduce((sum, conv) => sum + conv.tokens, 0),
678
+ activeProjects: this.data.activeProjects.filter(p => p.status === 'active').length,
679
+ lastActivity: this.data.summary.lastActivity,
680
+ };
681
+ }
682
+
683
+ /**
684
+ * Format byte sizes for display
685
+ * @param {number} bytes - Number of bytes
686
+ * @returns {string} Formatted byte string
687
+ */
688
+ formatBytes(bytes) {
689
+ if (bytes === 0) return '0 Bytes';
690
+ const k = 1024;
691
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
692
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
693
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
694
+ }
695
+
696
+ /**
697
+ * Get current conversation data
698
+ * @returns {Array} Current conversations
699
+ */
700
+ getConversations() {
701
+ return this.data.conversations;
702
+ }
703
+
704
+ /**
705
+ * Get current project data
706
+ * @returns {Array} Current projects
707
+ */
708
+ getActiveProjects() {
709
+ return this.data.activeProjects;
710
+ }
711
+
712
+ /**
713
+ * Get current summary data
714
+ * @returns {Object} Current summary
715
+ */
716
+ getSummary() {
717
+ return this.data.summary;
718
+ }
719
+
720
+ /**
721
+ * Get current orphan processes
722
+ * @returns {Array} Current orphan processes
723
+ */
724
+ getOrphanProcesses() {
725
+ return this.data.orphanProcesses;
726
+ }
727
+
728
+ /**
729
+ * Get current realtime stats
730
+ * @returns {Object} Current realtime stats
731
+ */
732
+ getRealtimeStats() {
733
+ return this.data.realtimeStats;
734
+ }
735
+
736
+ /**
737
+ * Update conversations data (used for external updates)
738
+ * @param {Array} conversations - Updated conversations array
739
+ */
740
+ setConversations(conversations) {
741
+ this.data.conversations = conversations;
742
+ this.updateRealtimeStats();
743
+ }
744
+
745
+ /**
746
+ * Update orphan processes data
747
+ * @param {Array} orphanProcesses - Updated orphan processes array
748
+ */
749
+ setOrphanProcesses(orphanProcesses) {
750
+ this.data.orphanProcesses = orphanProcesses;
751
+ }
752
+ }
753
+
754
+ module.exports = ConversationAnalyzer;