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/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
- // Extract project name from file path like:
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 null;
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
- console.log(chalk.yellow(`🧹 Cleaning up conversation history: ${this.data.conversations.length} -> 150`));
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 ONLY for conversation states
689
+ // NEW: Ultra-fast endpoint for ALL conversation states
581
690
  this.app.get('/api/conversation-state', async (req, res) => {
582
691
  try {
583
- // Only detect processes and calculate states - no file reading
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
- // Quick state calculation for active conversations only
696
+ // Calculate states for ALL conversations, not just those with runningProcess
588
697
  for (const conversation of this.data.conversations) {
589
- if (conversation.runningProcess) {
590
- // Use existing state calculation but faster
591
- const state = this.stateCalculator.quickStateCalculation(conversation, runningProcesses);
592
- if (state) {
593
- activeStates.push({
594
- id: conversation.id,
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(`http://localhost:${this.port}`);
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(`http://localhost:${this.port}`));
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
- console.log(chalk.green('✅ WebSocket and notifications initialized'));
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
- console.log(chalk.blue('📊 Starting Claude Code Analytics Dashboard...'));
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
- console.log(chalk.green('✅ Analytics dashboard is running!'));
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
  };