claude-code-templates 1.10.1 → 1.12.0
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 +6 -0
- package/bin/create-claude-config.js +1 -0
- package/package.json +1 -2
- package/src/analytics/core/ConversationAnalyzer.js +159 -43
- package/src/analytics/core/FileWatcher.js +146 -11
- package/src/analytics/data/DataCache.js +124 -19
- package/src/analytics/notifications/NotificationManager.js +37 -0
- package/src/analytics/notifications/WebSocketServer.js +1 -1
- package/src/analytics-web/FRONT_ARCHITECTURE.md +46 -0
- package/src/analytics-web/assets/js/{main.js → main.js.deprecated} +32 -3
- package/src/analytics-web/components/AgentsPage.js +4744 -0
- package/src/analytics-web/components/App.js +441 -0
- package/src/analytics-web/components/{Dashboard.js → Dashboard.js.deprecated} +23 -7
- package/src/analytics-web/components/DashboardPage.js +1531 -0
- package/src/analytics-web/components/Sidebar.js +197 -0
- package/src/analytics-web/components/ToolDisplay.js +554 -0
- package/src/analytics-web/index.html +5189 -1760
- package/src/analytics-web/services/DataService.js +89 -16
- package/src/analytics-web/services/StateService.js +9 -0
- package/src/analytics-web/services/WebSocketService.js +17 -5
- package/src/analytics.js +665 -38
- package/src/console-bridge.js +610 -0
- package/src/file-operations.js +143 -23
- package/src/index.js +24 -1
- package/src/templates.js +4 -0
- package/src/test-console-bridge.js +67 -0
package/src/analytics.js
CHANGED
|
@@ -14,6 +14,7 @@ const DataCache = require('./analytics/data/DataCache');
|
|
|
14
14
|
const WebSocketServer = require('./analytics/notifications/WebSocketServer');
|
|
15
15
|
const NotificationManager = require('./analytics/notifications/NotificationManager');
|
|
16
16
|
const PerformanceMonitor = require('./analytics/utils/PerformanceMonitor');
|
|
17
|
+
const ConsoleBridge = require('./console-bridge');
|
|
17
18
|
|
|
18
19
|
class ClaudeAnalytics {
|
|
19
20
|
constructor() {
|
|
@@ -32,6 +33,7 @@ class ClaudeAnalytics {
|
|
|
32
33
|
this.webSocketServer = null;
|
|
33
34
|
this.notificationManager = null;
|
|
34
35
|
this.httpServer = null;
|
|
36
|
+
this.consoleBridge = null;
|
|
35
37
|
this.data = {
|
|
36
38
|
conversations: [],
|
|
37
39
|
summary: {},
|
|
@@ -236,8 +238,35 @@ class ClaudeAnalytics {
|
|
|
236
238
|
};
|
|
237
239
|
}
|
|
238
240
|
|
|
239
|
-
extractProjectFromPath(filePath) {
|
|
240
|
-
//
|
|
241
|
+
async extractProjectFromPath(filePath) {
|
|
242
|
+
// First try to read cwd from the conversation file itself
|
|
243
|
+
try {
|
|
244
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
245
|
+
const lines = content.trim().split('\n').filter(line => line.trim());
|
|
246
|
+
|
|
247
|
+
for (const line of lines.slice(0, 10)) { // Check first 10 lines
|
|
248
|
+
try {
|
|
249
|
+
const item = JSON.parse(line);
|
|
250
|
+
|
|
251
|
+
// Look for cwd field in the message
|
|
252
|
+
if (item.cwd) {
|
|
253
|
+
return path.basename(item.cwd);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Also check if it's in nested objects
|
|
257
|
+
if (item.message && item.message.cwd) {
|
|
258
|
+
return path.basename(item.message.cwd);
|
|
259
|
+
}
|
|
260
|
+
} catch (parseError) {
|
|
261
|
+
// Skip invalid JSON lines
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
} catch (error) {
|
|
266
|
+
console.warn(chalk.yellow(`Warning: Could not extract project from conversation ${filePath}:`, error.message));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Fallback: Extract project name from file path like:
|
|
241
270
|
// /Users/user/.claude/projects/-Users-user-Projects-MyProject/conversation.jsonl
|
|
242
271
|
const pathParts = filePath.split('/');
|
|
243
272
|
const projectIndex = pathParts.findIndex(part => part === 'projects');
|
|
@@ -254,7 +283,7 @@ class ClaudeAnalytics {
|
|
|
254
283
|
return cleanName;
|
|
255
284
|
}
|
|
256
285
|
|
|
257
|
-
return
|
|
286
|
+
return 'Unknown';
|
|
258
287
|
}
|
|
259
288
|
|
|
260
289
|
|
|
@@ -494,6 +523,36 @@ class ClaudeAnalytics {
|
|
|
494
523
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
495
524
|
}
|
|
496
525
|
|
|
526
|
+
/**
|
|
527
|
+
* Handle conversation file changes and detect new messages
|
|
528
|
+
* @param {string} conversationId - Conversation ID that changed
|
|
529
|
+
* @param {string} filePath - Path to the conversation file
|
|
530
|
+
*/
|
|
531
|
+
async handleConversationChange(conversationId, filePath) {
|
|
532
|
+
try {
|
|
533
|
+
|
|
534
|
+
// Get the latest messages from the file
|
|
535
|
+
const messages = await this.conversationAnalyzer.getParsedConversation(filePath);
|
|
536
|
+
|
|
537
|
+
if (messages && messages.length > 0) {
|
|
538
|
+
// Get the most recent message
|
|
539
|
+
const latestMessage = messages[messages.length - 1];
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
// Send WebSocket notification for new message
|
|
543
|
+
if (this.notificationManager) {
|
|
544
|
+
this.notificationManager.notifyNewMessage(conversationId, latestMessage, {
|
|
545
|
+
totalMessages: messages.length,
|
|
546
|
+
timestamp: new Date().toISOString()
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
}
|
|
551
|
+
} catch (error) {
|
|
552
|
+
console.error(chalk.red(`Error handling conversation change for ${conversationId}:`), error);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
497
556
|
setupFileWatchers() {
|
|
498
557
|
// Setup file watchers using the FileWatcher module
|
|
499
558
|
this.fileWatcher.setupFileWatchers(
|
|
@@ -513,11 +572,30 @@ class ClaudeAnalytics {
|
|
|
513
572
|
this.data.orphanProcesses = enrichmentResult.orphanProcesses;
|
|
514
573
|
},
|
|
515
574
|
// DataCache for cache invalidation
|
|
516
|
-
this.dataCache
|
|
575
|
+
this.dataCache,
|
|
576
|
+
// Conversation change callback for real-time message updates
|
|
577
|
+
async (conversationId, filePath) => {
|
|
578
|
+
await this.handleConversationChange(conversationId, filePath);
|
|
579
|
+
}
|
|
517
580
|
);
|
|
518
581
|
}
|
|
519
582
|
|
|
520
583
|
setupWebServer() {
|
|
584
|
+
// Add CORS middleware
|
|
585
|
+
this.app.use((req, res, next) => {
|
|
586
|
+
res.header('Access-Control-Allow-Origin', '*');
|
|
587
|
+
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
588
|
+
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
|
|
589
|
+
|
|
590
|
+
// Handle preflight requests
|
|
591
|
+
if (req.method === 'OPTIONS') {
|
|
592
|
+
res.sendStatus(200);
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
next();
|
|
597
|
+
});
|
|
598
|
+
|
|
521
599
|
// Add performance monitoring middleware
|
|
522
600
|
this.app.use(this.performanceMonitor.createExpressMiddleware());
|
|
523
601
|
|
|
@@ -532,8 +610,7 @@ class ClaudeAnalytics {
|
|
|
532
610
|
|
|
533
611
|
// Memory cleanup: limit conversation history to prevent memory buildup
|
|
534
612
|
if (this.data.conversations && this.data.conversations.length > 150) {
|
|
535
|
-
|
|
536
|
-
this.data.conversations = this.data.conversations
|
|
613
|
+
this.data.conversations = this.data.conversations
|
|
537
614
|
.sort((a, b) => new Date(b.lastModified) - new Date(a.lastModified))
|
|
538
615
|
.slice(0, 150);
|
|
539
616
|
}
|
|
@@ -557,6 +634,39 @@ class ClaudeAnalytics {
|
|
|
557
634
|
}
|
|
558
635
|
});
|
|
559
636
|
|
|
637
|
+
// Paginated conversations endpoint
|
|
638
|
+
this.app.get('/api/conversations', async (req, res) => {
|
|
639
|
+
try {
|
|
640
|
+
const page = parseInt(req.query.page) || 0;
|
|
641
|
+
const limit = parseInt(req.query.limit) || 10;
|
|
642
|
+
const offset = page * limit;
|
|
643
|
+
|
|
644
|
+
// Sort conversations by lastModified (most recent first)
|
|
645
|
+
const sortedConversations = [...this.data.conversations]
|
|
646
|
+
.sort((a, b) => new Date(b.lastModified) - new Date(a.lastModified));
|
|
647
|
+
|
|
648
|
+
const paginatedConversations = sortedConversations.slice(offset, offset + limit);
|
|
649
|
+
const totalCount = this.data.conversations.length;
|
|
650
|
+
const hasMore = offset + limit < totalCount;
|
|
651
|
+
|
|
652
|
+
res.json({
|
|
653
|
+
conversations: paginatedConversations,
|
|
654
|
+
pagination: {
|
|
655
|
+
page,
|
|
656
|
+
limit,
|
|
657
|
+
offset,
|
|
658
|
+
totalCount,
|
|
659
|
+
hasMore,
|
|
660
|
+
currentCount: paginatedConversations.length
|
|
661
|
+
},
|
|
662
|
+
timestamp: new Date().toISOString()
|
|
663
|
+
});
|
|
664
|
+
} catch (error) {
|
|
665
|
+
console.error('Error getting paginated conversations:', error);
|
|
666
|
+
res.status(500).json({ error: 'Failed to get conversations' });
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
|
|
560
670
|
this.app.get('/api/realtime', async (req, res) => {
|
|
561
671
|
const realtimeWithTimestamp = {
|
|
562
672
|
...this.data.realtimeStats,
|
|
@@ -568,7 +678,6 @@ class ClaudeAnalytics {
|
|
|
568
678
|
|
|
569
679
|
// Force refresh endpoint
|
|
570
680
|
this.app.get('/api/refresh', async (req, res) => {
|
|
571
|
-
console.log(chalk.blue('🔄 Manual refresh requested...'));
|
|
572
681
|
await this.loadInitialData();
|
|
573
682
|
res.json({
|
|
574
683
|
success: true,
|
|
@@ -577,35 +686,115 @@ class ClaudeAnalytics {
|
|
|
577
686
|
});
|
|
578
687
|
});
|
|
579
688
|
|
|
580
|
-
// NEW: Ultra-fast endpoint
|
|
689
|
+
// NEW: Ultra-fast endpoint for ALL conversation states
|
|
581
690
|
this.app.get('/api/conversation-state', async (req, res) => {
|
|
582
691
|
try {
|
|
583
|
-
//
|
|
692
|
+
// Detect running processes for accurate state calculation
|
|
584
693
|
const runningProcesses = await this.processDetector.detectRunningClaudeProcesses();
|
|
585
|
-
const activeStates =
|
|
694
|
+
const activeStates = {};
|
|
586
695
|
|
|
587
|
-
//
|
|
696
|
+
// Calculate states for ALL conversations, not just those with runningProcess
|
|
588
697
|
for (const conversation of this.data.conversations) {
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
if
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
project: conversation.project,
|
|
596
|
-
state: state,
|
|
597
|
-
timestamp: Date.now()
|
|
598
|
-
});
|
|
698
|
+
try {
|
|
699
|
+
let state;
|
|
700
|
+
|
|
701
|
+
// First try quick calculation if there's a running process
|
|
702
|
+
if (conversation.runningProcess) {
|
|
703
|
+
state = this.stateCalculator.quickStateCalculation(conversation, runningProcesses);
|
|
599
704
|
}
|
|
705
|
+
|
|
706
|
+
// If no quick state found, use full state calculation
|
|
707
|
+
if (!state) {
|
|
708
|
+
// For conversations without running processes, use basic heuristics
|
|
709
|
+
const now = new Date();
|
|
710
|
+
const timeDiff = (now - new Date(conversation.lastModified)) / (1000 * 60); // minutes
|
|
711
|
+
|
|
712
|
+
if (timeDiff < 5) {
|
|
713
|
+
state = 'Recently active';
|
|
714
|
+
} else if (timeDiff < 60) {
|
|
715
|
+
state = 'Idle';
|
|
716
|
+
} else if (timeDiff < 1440) { // 24 hours
|
|
717
|
+
state = 'Inactive';
|
|
718
|
+
} else {
|
|
719
|
+
state = 'Old';
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// Store state with conversation ID as key
|
|
724
|
+
activeStates[conversation.id] = state;
|
|
725
|
+
|
|
726
|
+
} catch (error) {
|
|
727
|
+
activeStates[conversation.id] = 'unknown';
|
|
600
728
|
}
|
|
601
729
|
}
|
|
602
730
|
|
|
603
731
|
res.json({ activeStates, timestamp: Date.now() });
|
|
604
732
|
} catch (error) {
|
|
733
|
+
console.error('Error getting conversation states:', error);
|
|
605
734
|
res.status(500).json({ error: 'Failed to get conversation states' });
|
|
606
735
|
}
|
|
607
736
|
});
|
|
608
737
|
|
|
738
|
+
// Conversation messages endpoint with optional pagination
|
|
739
|
+
this.app.get('/api/conversations/:id/messages', async (req, res) => {
|
|
740
|
+
try {
|
|
741
|
+
const conversationId = req.params.id;
|
|
742
|
+
const page = parseInt(req.query.page);
|
|
743
|
+
const limit = parseInt(req.query.limit);
|
|
744
|
+
|
|
745
|
+
const conversation = this.data.conversations.find(conv => conv.id === conversationId);
|
|
746
|
+
|
|
747
|
+
if (!conversation) {
|
|
748
|
+
return res.status(404).json({ error: 'Conversation not found' });
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// Read all messages from the JSONL file
|
|
752
|
+
const allMessages = await this.conversationAnalyzer.getParsedConversation(conversation.filePath);
|
|
753
|
+
|
|
754
|
+
// If pagination parameters are provided, use pagination
|
|
755
|
+
if (!isNaN(page) && !isNaN(limit)) {
|
|
756
|
+
// Sort messages by timestamp (newest first for reverse pagination)
|
|
757
|
+
const sortedMessages = allMessages.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
|
|
758
|
+
|
|
759
|
+
// Calculate pagination
|
|
760
|
+
const totalCount = sortedMessages.length;
|
|
761
|
+
const offset = page * limit;
|
|
762
|
+
const hasMore = offset + limit < totalCount;
|
|
763
|
+
|
|
764
|
+
// Get page of messages (reverse order - newest first)
|
|
765
|
+
const paginatedMessages = sortedMessages.slice(offset, offset + limit);
|
|
766
|
+
|
|
767
|
+
// For display, we want messages in chronological order (oldest first)
|
|
768
|
+
const messagesInDisplayOrder = [...paginatedMessages].reverse();
|
|
769
|
+
|
|
770
|
+
res.json({
|
|
771
|
+
conversationId,
|
|
772
|
+
messages: messagesInDisplayOrder,
|
|
773
|
+
pagination: {
|
|
774
|
+
page,
|
|
775
|
+
limit,
|
|
776
|
+
offset,
|
|
777
|
+
totalCount,
|
|
778
|
+
hasMore,
|
|
779
|
+
currentCount: paginatedMessages.length
|
|
780
|
+
},
|
|
781
|
+
timestamp: new Date().toISOString()
|
|
782
|
+
});
|
|
783
|
+
} else {
|
|
784
|
+
// Non-paginated response (backward compatibility)
|
|
785
|
+
res.json({
|
|
786
|
+
conversationId,
|
|
787
|
+
messages: allMessages,
|
|
788
|
+
messageCount: allMessages.length,
|
|
789
|
+
timestamp: new Date().toISOString()
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
} catch (error) {
|
|
793
|
+
console.error('Error loading conversation messages:', error);
|
|
794
|
+
res.status(500).json({ error: 'Failed to load conversation messages' });
|
|
795
|
+
}
|
|
796
|
+
});
|
|
797
|
+
|
|
609
798
|
// Session data endpoint for Max plan usage tracking
|
|
610
799
|
this.app.get('/api/session/data', async (req, res) => {
|
|
611
800
|
try {
|
|
@@ -798,17 +987,10 @@ class ClaudeAnalytics {
|
|
|
798
987
|
}
|
|
799
988
|
});
|
|
800
989
|
|
|
801
|
-
if (hasChanges) {
|
|
802
|
-
console.log(chalk.gray(`⚡ State update: ${activeConvs.length} active conversations`));
|
|
803
|
-
activeConvs.forEach(conv => {
|
|
804
|
-
console.log(chalk.gray(` 📊 ${conv.project}: ${conv.conversationState}`));
|
|
805
|
-
});
|
|
806
|
-
}
|
|
807
990
|
}
|
|
808
991
|
|
|
809
992
|
// Memory cleanup: limit conversation history to prevent memory buildup
|
|
810
993
|
if (this.data.conversations.length > 100) {
|
|
811
|
-
console.log(chalk.yellow(`🧹 Cleaning up conversation history: ${this.data.conversations.length} -> 100`));
|
|
812
994
|
this.data.conversations = this.data.conversations
|
|
813
995
|
.sort((a, b) => new Date(b.lastModified) - new Date(a.lastModified))
|
|
814
996
|
.slice(0, 100);
|
|
@@ -915,6 +1097,44 @@ class ClaudeAnalytics {
|
|
|
915
1097
|
}
|
|
916
1098
|
});
|
|
917
1099
|
|
|
1100
|
+
// Cache management endpoint
|
|
1101
|
+
this.app.post('/api/cache/clear', (req, res) => {
|
|
1102
|
+
try {
|
|
1103
|
+
// Clear specific cache types or all
|
|
1104
|
+
const { type } = req.body;
|
|
1105
|
+
|
|
1106
|
+
if (!type || type === 'all') {
|
|
1107
|
+
// Clear all caches
|
|
1108
|
+
this.dataCache.invalidateComputations();
|
|
1109
|
+
this.dataCache.caches.parsedConversations.clear();
|
|
1110
|
+
this.dataCache.caches.fileContent.clear();
|
|
1111
|
+
this.dataCache.caches.fileStats.clear();
|
|
1112
|
+
res.json({ success: true, message: 'All caches cleared' });
|
|
1113
|
+
} else if (type === 'conversations') {
|
|
1114
|
+
// Clear only conversation-related caches
|
|
1115
|
+
this.dataCache.caches.parsedConversations.clear();
|
|
1116
|
+
this.dataCache.caches.fileContent.clear();
|
|
1117
|
+
res.json({ success: true, message: 'Conversation caches cleared' });
|
|
1118
|
+
} else {
|
|
1119
|
+
res.status(400).json({ error: 'Invalid cache type. Use "all" or "conversations"' });
|
|
1120
|
+
}
|
|
1121
|
+
} catch (error) {
|
|
1122
|
+
console.error('Error clearing cache:', error);
|
|
1123
|
+
res.status(500).json({ error: 'Failed to clear cache' });
|
|
1124
|
+
}
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1127
|
+
// Agents API endpoint
|
|
1128
|
+
this.app.get('/api/agents', async (req, res) => {
|
|
1129
|
+
try {
|
|
1130
|
+
const agents = await this.loadAgents();
|
|
1131
|
+
res.json({ agents });
|
|
1132
|
+
} catch (error) {
|
|
1133
|
+
console.error('Error loading agents:', error);
|
|
1134
|
+
res.status(500).json({ error: 'Failed to load agents data' });
|
|
1135
|
+
}
|
|
1136
|
+
});
|
|
1137
|
+
|
|
918
1138
|
// Main dashboard route
|
|
919
1139
|
this.app.get('/', (req, res) => {
|
|
920
1140
|
res.sendFile(path.join(__dirname, 'analytics-web', 'index.html'));
|
|
@@ -936,13 +1156,23 @@ class ClaudeAnalytics {
|
|
|
936
1156
|
});
|
|
937
1157
|
}
|
|
938
1158
|
|
|
939
|
-
async openBrowser() {
|
|
1159
|
+
async openBrowser(openTo = null) {
|
|
1160
|
+
const baseUrl = `http://localhost:${this.port}`;
|
|
1161
|
+
let fullUrl = baseUrl;
|
|
1162
|
+
|
|
1163
|
+
// Add fragment/hash for specific page
|
|
1164
|
+
if (openTo === 'agents') {
|
|
1165
|
+
fullUrl = `${baseUrl}/#agents`;
|
|
1166
|
+
console.log(chalk.blue('🌐 Opening browser to Claude Code Chats...'));
|
|
1167
|
+
} else {
|
|
1168
|
+
console.log(chalk.blue('🌐 Opening browser to Claude Code Analytics...'));
|
|
1169
|
+
}
|
|
1170
|
+
|
|
940
1171
|
try {
|
|
941
|
-
await open(
|
|
942
|
-
console.log(chalk.blue('🌐 Opening browser...'));
|
|
1172
|
+
await open(fullUrl);
|
|
943
1173
|
} catch (error) {
|
|
944
1174
|
console.log(chalk.yellow('Could not open browser automatically. Please visit:'));
|
|
945
|
-
console.log(chalk.cyan(
|
|
1175
|
+
console.log(chalk.cyan(fullUrl));
|
|
946
1176
|
}
|
|
947
1177
|
}
|
|
948
1178
|
|
|
@@ -962,14 +1192,84 @@ class ClaudeAnalytics {
|
|
|
962
1192
|
this.notificationManager = new NotificationManager(this.webSocketServer);
|
|
963
1193
|
await this.notificationManager.initialize();
|
|
964
1194
|
|
|
1195
|
+
// Connect notification manager to file watcher for typing detection
|
|
1196
|
+
this.fileWatcher.setNotificationManager(this.notificationManager);
|
|
1197
|
+
|
|
965
1198
|
// Setup notification subscriptions
|
|
966
1199
|
this.setupNotificationSubscriptions();
|
|
967
1200
|
|
|
968
|
-
|
|
1201
|
+
// Initialize Console Bridge for Claude Code interaction
|
|
1202
|
+
await this.initializeConsoleBridge();
|
|
1203
|
+
|
|
1204
|
+
console.log(chalk.green('✅ WebSocket, notifications, and console bridge initialized'));
|
|
969
1205
|
} catch (error) {
|
|
970
1206
|
console.error(chalk.red('❌ Failed to initialize WebSocket:'), error);
|
|
971
1207
|
}
|
|
972
1208
|
}
|
|
1209
|
+
|
|
1210
|
+
/**
|
|
1211
|
+
* Initialize Console Bridge for Claude Code interaction
|
|
1212
|
+
*/
|
|
1213
|
+
async initializeConsoleBridge() {
|
|
1214
|
+
try {
|
|
1215
|
+
console.log(chalk.blue('🌉 Initializing Console Bridge...'));
|
|
1216
|
+
|
|
1217
|
+
// Create console bridge on a different port (3334)
|
|
1218
|
+
this.consoleBridge = new ConsoleBridge({
|
|
1219
|
+
port: 3334,
|
|
1220
|
+
debug: false // Set to true for detailed debugging
|
|
1221
|
+
});
|
|
1222
|
+
|
|
1223
|
+
// Initialize the bridge
|
|
1224
|
+
const success = await this.consoleBridge.initialize();
|
|
1225
|
+
|
|
1226
|
+
if (success) {
|
|
1227
|
+
console.log(chalk.green('✅ Console Bridge initialized on port 3334'));
|
|
1228
|
+
console.log(chalk.cyan('🔌 Web interface can connect to ws://localhost:3334 for console interactions'));
|
|
1229
|
+
|
|
1230
|
+
// Bridge console interactions to main WebSocket
|
|
1231
|
+
this.setupConsoleBridgeIntegration();
|
|
1232
|
+
} else {
|
|
1233
|
+
console.warn(chalk.yellow('⚠️ Console Bridge failed to initialize - console interactions disabled'));
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
} catch (error) {
|
|
1237
|
+
console.warn(chalk.yellow('⚠️ Console Bridge initialization failed:'), error.message);
|
|
1238
|
+
console.log(chalk.gray('Console interactions will not be available, but analytics will continue normally'));
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
/**
|
|
1243
|
+
* Setup integration between Console Bridge and main WebSocket
|
|
1244
|
+
*/
|
|
1245
|
+
setupConsoleBridgeIntegration() {
|
|
1246
|
+
if (!this.consoleBridge || !this.webSocketServer) return;
|
|
1247
|
+
|
|
1248
|
+
// Forward console interactions from bridge to main WebSocket
|
|
1249
|
+
this.consoleBridge.on('console_interaction', (interactionData) => {
|
|
1250
|
+
console.log(chalk.blue('📡 Forwarding console interaction to web interface'));
|
|
1251
|
+
|
|
1252
|
+
// Broadcast to main WebSocket clients
|
|
1253
|
+
this.webSocketServer.broadcast({
|
|
1254
|
+
type: 'console_interaction',
|
|
1255
|
+
data: interactionData
|
|
1256
|
+
});
|
|
1257
|
+
});
|
|
1258
|
+
|
|
1259
|
+
// Listen for responses from main WebSocket and forward to bridge
|
|
1260
|
+
this.webSocketServer.on('console_response', (responseData) => {
|
|
1261
|
+
console.log(chalk.blue('📱 Forwarding console response to Claude Code'));
|
|
1262
|
+
|
|
1263
|
+
if (this.consoleBridge) {
|
|
1264
|
+
this.consoleBridge.handleWebMessage({
|
|
1265
|
+
type: 'console_response',
|
|
1266
|
+
data: responseData
|
|
1267
|
+
});
|
|
1268
|
+
}
|
|
1269
|
+
});
|
|
1270
|
+
|
|
1271
|
+
console.log(chalk.green('🔗 Console Bridge integration established'));
|
|
1272
|
+
}
|
|
973
1273
|
|
|
974
1274
|
/**
|
|
975
1275
|
* Setup notification subscriptions
|
|
@@ -977,7 +1277,6 @@ class ClaudeAnalytics {
|
|
|
977
1277
|
setupNotificationSubscriptions() {
|
|
978
1278
|
// Subscribe to refresh requests from WebSocket clients
|
|
979
1279
|
this.notificationManager.subscribe('refresh_requested', async (notification) => {
|
|
980
|
-
console.log(chalk.blue('🔄 Refresh requested via WebSocket'));
|
|
981
1280
|
await this.loadInitialData();
|
|
982
1281
|
|
|
983
1282
|
// Notify clients of the refreshed data
|
|
@@ -1021,6 +1320,307 @@ class ClaudeAnalytics {
|
|
|
1021
1320
|
});
|
|
1022
1321
|
}
|
|
1023
1322
|
|
|
1323
|
+
/**
|
|
1324
|
+
* Load available agents from .claude/agents directories (project and user level)
|
|
1325
|
+
* @returns {Promise<Array>} Array of agent objects
|
|
1326
|
+
*/
|
|
1327
|
+
async loadAgents() {
|
|
1328
|
+
const agents = [];
|
|
1329
|
+
const homeDir = os.homedir();
|
|
1330
|
+
|
|
1331
|
+
// Define agent paths (user level and project level)
|
|
1332
|
+
const userAgentsDir = path.join(homeDir, '.claude', 'agents');
|
|
1333
|
+
const projectAgentsDirs = [];
|
|
1334
|
+
|
|
1335
|
+
try {
|
|
1336
|
+
// 1. Check current working directory for .claude/agents
|
|
1337
|
+
const currentProjectAgentsDir = path.join(process.cwd(), '.claude', 'agents');
|
|
1338
|
+
if (await fs.pathExists(currentProjectAgentsDir)) {
|
|
1339
|
+
const currentProjectName = path.basename(process.cwd());
|
|
1340
|
+
projectAgentsDirs.push({
|
|
1341
|
+
path: currentProjectAgentsDir,
|
|
1342
|
+
projectName: currentProjectName
|
|
1343
|
+
});
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
// 2. Check parent directories for .claude/agents (for monorepo/nested projects)
|
|
1347
|
+
let currentDir = process.cwd();
|
|
1348
|
+
let parentDir = path.dirname(currentDir);
|
|
1349
|
+
|
|
1350
|
+
// Search up to 3 levels up for .claude/agents
|
|
1351
|
+
for (let i = 0; i < 3 && parentDir !== currentDir; i++) {
|
|
1352
|
+
const parentProjectAgentsDir = path.join(parentDir, '.claude', 'agents');
|
|
1353
|
+
|
|
1354
|
+
if (await fs.pathExists(parentProjectAgentsDir)) {
|
|
1355
|
+
const parentProjectName = path.basename(parentDir);
|
|
1356
|
+
|
|
1357
|
+
// Avoid duplicates
|
|
1358
|
+
const exists = projectAgentsDirs.some(p => p.path === parentProjectAgentsDir);
|
|
1359
|
+
if (!exists) {
|
|
1360
|
+
projectAgentsDirs.push({
|
|
1361
|
+
path: parentProjectAgentsDir,
|
|
1362
|
+
projectName: parentProjectName
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1365
|
+
break; // Found one, no need to go further up
|
|
1366
|
+
}
|
|
1367
|
+
currentDir = parentDir;
|
|
1368
|
+
parentDir = path.dirname(currentDir);
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
// 3. Find all project directories that might have agents (in ~/.claude/projects)
|
|
1372
|
+
const projectsDir = path.join(this.claudeDir, 'projects');
|
|
1373
|
+
if (await fs.pathExists(projectsDir)) {
|
|
1374
|
+
const projectDirs = await fs.readdir(projectsDir);
|
|
1375
|
+
for (const projectDir of projectDirs) {
|
|
1376
|
+
const projectAgentsDir = path.join(projectsDir, projectDir, '.claude', 'agents');
|
|
1377
|
+
if (await fs.pathExists(projectAgentsDir)) {
|
|
1378
|
+
projectAgentsDirs.push({
|
|
1379
|
+
path: projectAgentsDir,
|
|
1380
|
+
projectName: this.cleanProjectName(projectDir)
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
// Load user-level agents
|
|
1387
|
+
if (await fs.pathExists(userAgentsDir)) {
|
|
1388
|
+
const userAgents = await this.loadAgentsFromDirectory(userAgentsDir, 'user');
|
|
1389
|
+
agents.push(...userAgents);
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
// Load project-level agents
|
|
1393
|
+
for (const projectInfo of projectAgentsDirs) {
|
|
1394
|
+
const projectAgents = await this.loadAgentsFromDirectory(
|
|
1395
|
+
projectInfo.path,
|
|
1396
|
+
'project',
|
|
1397
|
+
projectInfo.projectName
|
|
1398
|
+
);
|
|
1399
|
+
agents.push(...projectAgents);
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
// Log agents summary
|
|
1403
|
+
console.log(chalk.blue('🤖 Agents loaded:'), agents.length);
|
|
1404
|
+
if (agents.length > 0) {
|
|
1405
|
+
const projectAgents = agents.filter(a => a.level === 'project').length;
|
|
1406
|
+
const userAgents = agents.filter(a => a.level === 'user').length;
|
|
1407
|
+
console.log(chalk.gray(` 📦 Project agents: ${projectAgents}, 👤 User agents: ${userAgents}`));
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
// Sort agents by name and prioritize project agents over user agents
|
|
1411
|
+
return agents.sort((a, b) => {
|
|
1412
|
+
if (a.level !== b.level) {
|
|
1413
|
+
return a.level === 'project' ? -1 : 1;
|
|
1414
|
+
}
|
|
1415
|
+
return a.name.localeCompare(b.name);
|
|
1416
|
+
});
|
|
1417
|
+
|
|
1418
|
+
} catch (error) {
|
|
1419
|
+
console.error(chalk.red('Error loading agents:'), error);
|
|
1420
|
+
return [];
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
/**
|
|
1425
|
+
* Load agents from a specific directory
|
|
1426
|
+
* @param {string} agentsDir - Directory containing agent files
|
|
1427
|
+
* @param {string} level - 'user' or 'project'
|
|
1428
|
+
* @param {string} projectName - Name of project (if project level)
|
|
1429
|
+
* @returns {Promise<Array>} Array of agent objects
|
|
1430
|
+
*/
|
|
1431
|
+
async loadAgentsFromDirectory(agentsDir, level, projectName = null) {
|
|
1432
|
+
const agents = [];
|
|
1433
|
+
|
|
1434
|
+
try {
|
|
1435
|
+
const files = await fs.readdir(agentsDir);
|
|
1436
|
+
|
|
1437
|
+
for (const file of files) {
|
|
1438
|
+
if (file.endsWith('.md')) {
|
|
1439
|
+
const filePath = path.join(agentsDir, file);
|
|
1440
|
+
const agentData = await this.parseAgentFile(filePath, level, projectName);
|
|
1441
|
+
if (agentData) {
|
|
1442
|
+
agents.push(agentData);
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
} catch (error) {
|
|
1447
|
+
console.warn(chalk.yellow(`Warning: Could not read agents directory ${agentsDir}:`, error.message));
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
return agents;
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
/**
|
|
1454
|
+
* Parse agent markdown file
|
|
1455
|
+
* @param {string} filePath - Path to agent file
|
|
1456
|
+
* @param {string} level - 'user' or 'project'
|
|
1457
|
+
* @param {string} projectName - Name of project (if project level)
|
|
1458
|
+
* @returns {Promise<Object|null>} Agent object or null if parsing failed
|
|
1459
|
+
*/
|
|
1460
|
+
async parseAgentFile(filePath, level, projectName = null) {
|
|
1461
|
+
try {
|
|
1462
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
1463
|
+
const stats = await fs.stat(filePath);
|
|
1464
|
+
|
|
1465
|
+
// Parse YAML frontmatter
|
|
1466
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
1467
|
+
if (!frontmatterMatch) {
|
|
1468
|
+
console.warn(chalk.yellow(`Agent file ${path.basename(filePath)} missing frontmatter`));
|
|
1469
|
+
return null;
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
const frontmatter = {};
|
|
1473
|
+
const yamlContent = frontmatterMatch[1];
|
|
1474
|
+
|
|
1475
|
+
// Simple YAML parser for the fields we need
|
|
1476
|
+
const yamlLines = yamlContent.split('\n');
|
|
1477
|
+
for (const line of yamlLines) {
|
|
1478
|
+
const match = line.match(/^(\w+):\s*(.*)$/);
|
|
1479
|
+
if (match) {
|
|
1480
|
+
const [, key, value] = match;
|
|
1481
|
+
frontmatter[key] = value.trim();
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
// Log parsed frontmatter for debugging
|
|
1486
|
+
console.log(chalk.blue(`📋 Parsed agent frontmatter for ${path.basename(filePath)}:`), frontmatter);
|
|
1487
|
+
|
|
1488
|
+
if (!frontmatter.name || !frontmatter.description) {
|
|
1489
|
+
console.warn(chalk.yellow(`Agent file ${path.basename(filePath)} missing required fields`));
|
|
1490
|
+
return null;
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
// Extract system prompt (content after frontmatter)
|
|
1494
|
+
const systemPrompt = content.substring(frontmatterMatch[0].length).trim();
|
|
1495
|
+
|
|
1496
|
+
// Parse tools if specified
|
|
1497
|
+
let tools = [];
|
|
1498
|
+
if (frontmatter.tools) {
|
|
1499
|
+
tools = frontmatter.tools.split(',').map(tool => tool.trim()).filter(Boolean);
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
// Use color from frontmatter if available, otherwise generate one
|
|
1503
|
+
const color = frontmatter.color ? this.convertColorToHex(frontmatter.color) : this.generateAgentColor(frontmatter.name);
|
|
1504
|
+
|
|
1505
|
+
return {
|
|
1506
|
+
name: frontmatter.name,
|
|
1507
|
+
description: frontmatter.description,
|
|
1508
|
+
systemPrompt,
|
|
1509
|
+
tools,
|
|
1510
|
+
level,
|
|
1511
|
+
projectName,
|
|
1512
|
+
filePath,
|
|
1513
|
+
lastModified: stats.mtime,
|
|
1514
|
+
color,
|
|
1515
|
+
isActive: true // All loaded agents are considered active
|
|
1516
|
+
};
|
|
1517
|
+
|
|
1518
|
+
} catch (error) {
|
|
1519
|
+
console.warn(chalk.yellow(`Warning: Could not parse agent file ${filePath}:`, error.message));
|
|
1520
|
+
return null;
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
/**
|
|
1525
|
+
* Generate consistent color for agent based on name
|
|
1526
|
+
* @param {string} agentName - Name of the agent
|
|
1527
|
+
* @returns {string} Hex color code
|
|
1528
|
+
*/
|
|
1529
|
+
generateAgentColor(agentName) {
|
|
1530
|
+
// Simple hash function to generate consistent colors
|
|
1531
|
+
let hash = 0;
|
|
1532
|
+
for (let i = 0; i < agentName.length; i++) {
|
|
1533
|
+
const char = agentName.charCodeAt(i);
|
|
1534
|
+
hash = ((hash << 5) - hash) + char;
|
|
1535
|
+
hash = hash & hash; // Convert to 32-bit integer
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
// Generate RGB values with good contrast and visibility
|
|
1539
|
+
const hue = Math.abs(hash) % 360;
|
|
1540
|
+
const saturation = 70 + (Math.abs(hash) % 30); // 70-100%
|
|
1541
|
+
const lightness = 45 + (Math.abs(hash) % 20); // 45-65%
|
|
1542
|
+
|
|
1543
|
+
// Convert HSL to RGB
|
|
1544
|
+
const hslToRgb = (h, s, l) => {
|
|
1545
|
+
h /= 360;
|
|
1546
|
+
s /= 100;
|
|
1547
|
+
l /= 100;
|
|
1548
|
+
|
|
1549
|
+
const hue2rgb = (p, q, t) => {
|
|
1550
|
+
if (t < 0) t += 1;
|
|
1551
|
+
if (t > 1) t -= 1;
|
|
1552
|
+
if (t < 1/6) return p + (q - p) * 6 * t;
|
|
1553
|
+
if (t < 1/2) return q;
|
|
1554
|
+
if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
|
|
1555
|
+
return p;
|
|
1556
|
+
};
|
|
1557
|
+
|
|
1558
|
+
let r, g, b;
|
|
1559
|
+
if (s === 0) {
|
|
1560
|
+
r = g = b = l;
|
|
1561
|
+
} else {
|
|
1562
|
+
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
1563
|
+
const p = 2 * l - q;
|
|
1564
|
+
r = hue2rgb(p, q, h + 1/3);
|
|
1565
|
+
g = hue2rgb(p, q, h);
|
|
1566
|
+
b = hue2rgb(p, q, h - 1/3);
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
|
|
1570
|
+
};
|
|
1571
|
+
|
|
1572
|
+
const [r, g, b] = hslToRgb(hue, saturation, lightness);
|
|
1573
|
+
return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
/**
|
|
1577
|
+
* Convert color names to hex values
|
|
1578
|
+
* @param {string} color - Color name or hex value
|
|
1579
|
+
* @returns {string} Hex color code
|
|
1580
|
+
*/
|
|
1581
|
+
convertColorToHex(color) {
|
|
1582
|
+
if (!color) return '#007acc';
|
|
1583
|
+
|
|
1584
|
+
// If already hex, return as-is
|
|
1585
|
+
if (color.startsWith('#')) return color;
|
|
1586
|
+
|
|
1587
|
+
// Convert common color names to hex
|
|
1588
|
+
const colorMap = {
|
|
1589
|
+
'red': '#ff4444',
|
|
1590
|
+
'blue': '#4444ff',
|
|
1591
|
+
'green': '#44ff44',
|
|
1592
|
+
'yellow': '#ffff44',
|
|
1593
|
+
'orange': '#ff8844',
|
|
1594
|
+
'purple': '#8844ff',
|
|
1595
|
+
'pink': '#ff44ff',
|
|
1596
|
+
'cyan': '#44ffff',
|
|
1597
|
+
'brown': '#8b4513',
|
|
1598
|
+
'gray': '#888888',
|
|
1599
|
+
'grey': '#888888',
|
|
1600
|
+
'black': '#333333',
|
|
1601
|
+
'white': '#ffffff',
|
|
1602
|
+
'teal': '#008080',
|
|
1603
|
+
'navy': '#000080',
|
|
1604
|
+
'lime': '#00ff00',
|
|
1605
|
+
'maroon': '#800000',
|
|
1606
|
+
'olive': '#808000',
|
|
1607
|
+
'silver': '#c0c0c0'
|
|
1608
|
+
};
|
|
1609
|
+
|
|
1610
|
+
return colorMap[color.toLowerCase()] || '#007acc';
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
/**
|
|
1614
|
+
* Clean project name for display
|
|
1615
|
+
* @param {string} projectDir - Raw project directory name
|
|
1616
|
+
* @returns {string} Cleaned project name
|
|
1617
|
+
*/
|
|
1618
|
+
cleanProjectName(projectDir) {
|
|
1619
|
+
// Convert encoded project paths like "-Users-user-Projects-MyProject" to "MyProject"
|
|
1620
|
+
const parts = projectDir.split('-').filter(Boolean);
|
|
1621
|
+
return parts[parts.length - 1] || projectDir;
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1024
1624
|
/**
|
|
1025
1625
|
* Get Claude session information from statsig files
|
|
1026
1626
|
*/
|
|
@@ -1108,6 +1708,7 @@ class ClaudeAnalytics {
|
|
|
1108
1708
|
}
|
|
1109
1709
|
}
|
|
1110
1710
|
|
|
1711
|
+
|
|
1111
1712
|
stop() {
|
|
1112
1713
|
// Stop file watchers
|
|
1113
1714
|
this.fileWatcher.stop();
|
|
@@ -1123,6 +1724,11 @@ class ClaudeAnalytics {
|
|
|
1123
1724
|
this.notificationManager.shutdown();
|
|
1124
1725
|
}
|
|
1125
1726
|
|
|
1727
|
+
// Shutdown console bridge
|
|
1728
|
+
if (this.consoleBridge) {
|
|
1729
|
+
this.consoleBridge.shutdown();
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1126
1732
|
if (this.httpServer) {
|
|
1127
1733
|
this.httpServer.close();
|
|
1128
1734
|
}
|
|
@@ -1140,7 +1746,14 @@ class ClaudeAnalytics {
|
|
|
1140
1746
|
}
|
|
1141
1747
|
|
|
1142
1748
|
async function runAnalytics(options = {}) {
|
|
1143
|
-
|
|
1749
|
+
// Determine if we're opening to a specific page
|
|
1750
|
+
const openTo = options.openTo;
|
|
1751
|
+
|
|
1752
|
+
if (openTo === 'agents') {
|
|
1753
|
+
console.log(chalk.blue('💬 Starting Claude Code Chats Dashboard...'));
|
|
1754
|
+
} else {
|
|
1755
|
+
console.log(chalk.blue('📊 Starting Claude Code Analytics Dashboard...'));
|
|
1756
|
+
}
|
|
1144
1757
|
|
|
1145
1758
|
const analytics = new ClaudeAnalytics();
|
|
1146
1759
|
|
|
@@ -1151,9 +1764,15 @@ async function runAnalytics(options = {}) {
|
|
|
1151
1764
|
// Web dashboard files are now static in analytics-web directory
|
|
1152
1765
|
|
|
1153
1766
|
await analytics.startServer();
|
|
1154
|
-
await analytics.openBrowser();
|
|
1155
|
-
|
|
1156
|
-
|
|
1767
|
+
await analytics.openBrowser(openTo);
|
|
1768
|
+
|
|
1769
|
+
if (openTo === 'agents') {
|
|
1770
|
+
console.log(chalk.green('✅ Claude Code Chats dashboard is running!'));
|
|
1771
|
+
console.log(chalk.cyan(`📱 Access at: http://localhost:${analytics.port}/#agents`));
|
|
1772
|
+
} else {
|
|
1773
|
+
console.log(chalk.green('✅ Analytics dashboard is running!'));
|
|
1774
|
+
console.log(chalk.cyan(`📱 Access at: http://localhost:${analytics.port}`));
|
|
1775
|
+
}
|
|
1157
1776
|
console.log(chalk.gray('Press Ctrl+C to stop the server'));
|
|
1158
1777
|
|
|
1159
1778
|
// Handle graceful shutdown
|
|
@@ -1173,6 +1792,14 @@ async function runAnalytics(options = {}) {
|
|
|
1173
1792
|
}
|
|
1174
1793
|
|
|
1175
1794
|
|
|
1795
|
+
// If this file is executed directly, run analytics
|
|
1796
|
+
if (require.main === module) {
|
|
1797
|
+
runAnalytics().catch(error => {
|
|
1798
|
+
console.error(chalk.red('❌ Analytics startup failed:'), error);
|
|
1799
|
+
process.exit(1);
|
|
1800
|
+
});
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1176
1803
|
module.exports = {
|
|
1177
1804
|
runAnalytics
|
|
1178
1805
|
};
|