claude-code-templates 1.10.0 → 1.11.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.
Files changed (35) hide show
  1. package/README.md +6 -0
  2. package/bin/create-claude-config.js +1 -0
  3. package/package.json +2 -2
  4. package/src/analytics/core/ConversationAnalyzer.js +94 -20
  5. package/src/analytics/core/FileWatcher.js +146 -11
  6. package/src/analytics/data/DataCache.js +124 -19
  7. package/src/analytics/notifications/NotificationManager.js +37 -0
  8. package/src/analytics/notifications/WebSocketServer.js +1 -1
  9. package/src/analytics-web/FRONT_ARCHITECTURE.md +46 -0
  10. package/src/analytics-web/assets/js/{main.js → main.js.deprecated} +32 -3
  11. package/src/analytics-web/components/AgentsPage.js +2535 -0
  12. package/src/analytics-web/components/App.js +430 -0
  13. package/src/analytics-web/components/{Dashboard.js → Dashboard.js.deprecated} +23 -7
  14. package/src/analytics-web/components/DashboardPage.js +1527 -0
  15. package/src/analytics-web/components/Sidebar.js +197 -0
  16. package/src/analytics-web/components/ToolDisplay.js +539 -0
  17. package/src/analytics-web/index.html +3275 -1792
  18. package/src/analytics-web/services/DataService.js +89 -16
  19. package/src/analytics-web/services/StateService.js +9 -0
  20. package/src/analytics-web/services/WebSocketService.js +17 -5
  21. package/src/analytics.js +323 -35
  22. package/src/console-bridge.js +610 -0
  23. package/src/file-operations.js +143 -23
  24. package/src/hook-scanner.js +21 -1
  25. package/src/index.js +24 -1
  26. package/src/templates.js +28 -0
  27. package/src/test-console-bridge.js +67 -0
  28. package/src/utils.js +46 -0
  29. package/templates/ruby/.claude/commands/model.md +360 -0
  30. package/templates/ruby/.claude/commands/test.md +480 -0
  31. package/templates/ruby/.claude/settings.json +146 -0
  32. package/templates/ruby/.mcp.json +83 -0
  33. package/templates/ruby/CLAUDE.md +284 -0
  34. package/templates/ruby/examples/rails-app/.claude/commands/authentication.md +490 -0
  35. package/templates/ruby/examples/rails-app/CLAUDE.md +376 -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: {},
@@ -494,6 +496,36 @@ class ClaudeAnalytics {
494
496
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
495
497
  }
496
498
 
499
+ /**
500
+ * Handle conversation file changes and detect new messages
501
+ * @param {string} conversationId - Conversation ID that changed
502
+ * @param {string} filePath - Path to the conversation file
503
+ */
504
+ async handleConversationChange(conversationId, filePath) {
505
+ try {
506
+
507
+ // Get the latest messages from the file
508
+ const messages = await this.conversationAnalyzer.getParsedConversation(filePath);
509
+
510
+ if (messages && messages.length > 0) {
511
+ // Get the most recent message
512
+ const latestMessage = messages[messages.length - 1];
513
+
514
+
515
+ // Send WebSocket notification for new message
516
+ if (this.notificationManager) {
517
+ this.notificationManager.notifyNewMessage(conversationId, latestMessage, {
518
+ totalMessages: messages.length,
519
+ timestamp: new Date().toISOString()
520
+ });
521
+ }
522
+
523
+ }
524
+ } catch (error) {
525
+ console.error(chalk.red(`Error handling conversation change for ${conversationId}:`), error);
526
+ }
527
+ }
528
+
497
529
  setupFileWatchers() {
498
530
  // Setup file watchers using the FileWatcher module
499
531
  this.fileWatcher.setupFileWatchers(
@@ -513,11 +545,30 @@ class ClaudeAnalytics {
513
545
  this.data.orphanProcesses = enrichmentResult.orphanProcesses;
514
546
  },
515
547
  // DataCache for cache invalidation
516
- this.dataCache
548
+ this.dataCache,
549
+ // Conversation change callback for real-time message updates
550
+ async (conversationId, filePath) => {
551
+ await this.handleConversationChange(conversationId, filePath);
552
+ }
517
553
  );
518
554
  }
519
555
 
520
556
  setupWebServer() {
557
+ // Add CORS middleware
558
+ this.app.use((req, res, next) => {
559
+ res.header('Access-Control-Allow-Origin', '*');
560
+ res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
561
+ res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
562
+
563
+ // Handle preflight requests
564
+ if (req.method === 'OPTIONS') {
565
+ res.sendStatus(200);
566
+ return;
567
+ }
568
+
569
+ next();
570
+ });
571
+
521
572
  // Add performance monitoring middleware
522
573
  this.app.use(this.performanceMonitor.createExpressMiddleware());
523
574
 
@@ -532,8 +583,7 @@ class ClaudeAnalytics {
532
583
 
533
584
  // Memory cleanup: limit conversation history to prevent memory buildup
534
585
  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
586
+ this.data.conversations = this.data.conversations
537
587
  .sort((a, b) => new Date(b.lastModified) - new Date(a.lastModified))
538
588
  .slice(0, 150);
539
589
  }
@@ -557,6 +607,39 @@ class ClaudeAnalytics {
557
607
  }
558
608
  });
559
609
 
610
+ // Paginated conversations endpoint
611
+ this.app.get('/api/conversations', async (req, res) => {
612
+ try {
613
+ const page = parseInt(req.query.page) || 0;
614
+ const limit = parseInt(req.query.limit) || 10;
615
+ const offset = page * limit;
616
+
617
+ // Sort conversations by lastModified (most recent first)
618
+ const sortedConversations = [...this.data.conversations]
619
+ .sort((a, b) => new Date(b.lastModified) - new Date(a.lastModified));
620
+
621
+ const paginatedConversations = sortedConversations.slice(offset, offset + limit);
622
+ const totalCount = this.data.conversations.length;
623
+ const hasMore = offset + limit < totalCount;
624
+
625
+ res.json({
626
+ conversations: paginatedConversations,
627
+ pagination: {
628
+ page,
629
+ limit,
630
+ offset,
631
+ totalCount,
632
+ hasMore,
633
+ currentCount: paginatedConversations.length
634
+ },
635
+ timestamp: new Date().toISOString()
636
+ });
637
+ } catch (error) {
638
+ console.error('Error getting paginated conversations:', error);
639
+ res.status(500).json({ error: 'Failed to get conversations' });
640
+ }
641
+ });
642
+
560
643
  this.app.get('/api/realtime', async (req, res) => {
561
644
  const realtimeWithTimestamp = {
562
645
  ...this.data.realtimeStats,
@@ -568,7 +651,6 @@ class ClaudeAnalytics {
568
651
 
569
652
  // Force refresh endpoint
570
653
  this.app.get('/api/refresh', async (req, res) => {
571
- console.log(chalk.blue('🔄 Manual refresh requested...'));
572
654
  await this.loadInitialData();
573
655
  res.json({
574
656
  success: true,
@@ -577,35 +659,115 @@ class ClaudeAnalytics {
577
659
  });
578
660
  });
579
661
 
580
- // NEW: Ultra-fast endpoint ONLY for conversation states
662
+ // NEW: Ultra-fast endpoint for ALL conversation states
581
663
  this.app.get('/api/conversation-state', async (req, res) => {
582
664
  try {
583
- // Only detect processes and calculate states - no file reading
665
+ // Detect running processes for accurate state calculation
584
666
  const runningProcesses = await this.processDetector.detectRunningClaudeProcesses();
585
- const activeStates = [];
667
+ const activeStates = {};
586
668
 
587
- // Quick state calculation for active conversations only
669
+ // Calculate states for ALL conversations, not just those with runningProcess
588
670
  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
- });
671
+ try {
672
+ let state;
673
+
674
+ // First try quick calculation if there's a running process
675
+ if (conversation.runningProcess) {
676
+ state = this.stateCalculator.quickStateCalculation(conversation, runningProcesses);
599
677
  }
678
+
679
+ // If no quick state found, use full state calculation
680
+ if (!state) {
681
+ // For conversations without running processes, use basic heuristics
682
+ const now = new Date();
683
+ const timeDiff = (now - new Date(conversation.lastModified)) / (1000 * 60); // minutes
684
+
685
+ if (timeDiff < 5) {
686
+ state = 'Recently active';
687
+ } else if (timeDiff < 60) {
688
+ state = 'Idle';
689
+ } else if (timeDiff < 1440) { // 24 hours
690
+ state = 'Inactive';
691
+ } else {
692
+ state = 'Old';
693
+ }
694
+ }
695
+
696
+ // Store state with conversation ID as key
697
+ activeStates[conversation.id] = state;
698
+
699
+ } catch (error) {
700
+ activeStates[conversation.id] = 'unknown';
600
701
  }
601
702
  }
602
703
 
603
704
  res.json({ activeStates, timestamp: Date.now() });
604
705
  } catch (error) {
706
+ console.error('Error getting conversation states:', error);
605
707
  res.status(500).json({ error: 'Failed to get conversation states' });
606
708
  }
607
709
  });
608
710
 
711
+ // Conversation messages endpoint with optional pagination
712
+ this.app.get('/api/conversations/:id/messages', async (req, res) => {
713
+ try {
714
+ const conversationId = req.params.id;
715
+ const page = parseInt(req.query.page);
716
+ const limit = parseInt(req.query.limit);
717
+
718
+ const conversation = this.data.conversations.find(conv => conv.id === conversationId);
719
+
720
+ if (!conversation) {
721
+ return res.status(404).json({ error: 'Conversation not found' });
722
+ }
723
+
724
+ // Read all messages from the JSONL file
725
+ const allMessages = await this.conversationAnalyzer.getParsedConversation(conversation.filePath);
726
+
727
+ // If pagination parameters are provided, use pagination
728
+ if (!isNaN(page) && !isNaN(limit)) {
729
+ // Sort messages by timestamp (newest first for reverse pagination)
730
+ const sortedMessages = allMessages.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
731
+
732
+ // Calculate pagination
733
+ const totalCount = sortedMessages.length;
734
+ const offset = page * limit;
735
+ const hasMore = offset + limit < totalCount;
736
+
737
+ // Get page of messages (reverse order - newest first)
738
+ const paginatedMessages = sortedMessages.slice(offset, offset + limit);
739
+
740
+ // For display, we want messages in chronological order (oldest first)
741
+ const messagesInDisplayOrder = [...paginatedMessages].reverse();
742
+
743
+ res.json({
744
+ conversationId,
745
+ messages: messagesInDisplayOrder,
746
+ pagination: {
747
+ page,
748
+ limit,
749
+ offset,
750
+ totalCount,
751
+ hasMore,
752
+ currentCount: paginatedMessages.length
753
+ },
754
+ timestamp: new Date().toISOString()
755
+ });
756
+ } else {
757
+ // Non-paginated response (backward compatibility)
758
+ res.json({
759
+ conversationId,
760
+ messages: allMessages,
761
+ messageCount: allMessages.length,
762
+ timestamp: new Date().toISOString()
763
+ });
764
+ }
765
+ } catch (error) {
766
+ console.error('Error loading conversation messages:', error);
767
+ res.status(500).json({ error: 'Failed to load conversation messages' });
768
+ }
769
+ });
770
+
609
771
  // Session data endpoint for Max plan usage tracking
610
772
  this.app.get('/api/session/data', async (req, res) => {
611
773
  try {
@@ -798,17 +960,10 @@ class ClaudeAnalytics {
798
960
  }
799
961
  });
800
962
 
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
963
  }
808
964
 
809
965
  // Memory cleanup: limit conversation history to prevent memory buildup
810
966
  if (this.data.conversations.length > 100) {
811
- console.log(chalk.yellow(`🧹 Cleaning up conversation history: ${this.data.conversations.length} -> 100`));
812
967
  this.data.conversations = this.data.conversations
813
968
  .sort((a, b) => new Date(b.lastModified) - new Date(a.lastModified))
814
969
  .slice(0, 100);
@@ -915,6 +1070,33 @@ class ClaudeAnalytics {
915
1070
  }
916
1071
  });
917
1072
 
1073
+ // Cache management endpoint
1074
+ this.app.post('/api/cache/clear', (req, res) => {
1075
+ try {
1076
+ // Clear specific cache types or all
1077
+ const { type } = req.body;
1078
+
1079
+ if (!type || type === 'all') {
1080
+ // Clear all caches
1081
+ this.dataCache.invalidateComputations();
1082
+ this.dataCache.caches.parsedConversations.clear();
1083
+ this.dataCache.caches.fileContent.clear();
1084
+ this.dataCache.caches.fileStats.clear();
1085
+ res.json({ success: true, message: 'All caches cleared' });
1086
+ } else if (type === 'conversations') {
1087
+ // Clear only conversation-related caches
1088
+ this.dataCache.caches.parsedConversations.clear();
1089
+ this.dataCache.caches.fileContent.clear();
1090
+ res.json({ success: true, message: 'Conversation caches cleared' });
1091
+ } else {
1092
+ res.status(400).json({ error: 'Invalid cache type. Use "all" or "conversations"' });
1093
+ }
1094
+ } catch (error) {
1095
+ console.error('Error clearing cache:', error);
1096
+ res.status(500).json({ error: 'Failed to clear cache' });
1097
+ }
1098
+ });
1099
+
918
1100
  // Main dashboard route
919
1101
  this.app.get('/', (req, res) => {
920
1102
  res.sendFile(path.join(__dirname, 'analytics-web', 'index.html'));
@@ -936,13 +1118,23 @@ class ClaudeAnalytics {
936
1118
  });
937
1119
  }
938
1120
 
939
- async openBrowser() {
1121
+ async openBrowser(openTo = null) {
1122
+ const baseUrl = `http://localhost:${this.port}`;
1123
+ let fullUrl = baseUrl;
1124
+
1125
+ // Add fragment/hash for specific page
1126
+ if (openTo === 'agents') {
1127
+ fullUrl = `${baseUrl}/#agents`;
1128
+ console.log(chalk.blue('🌐 Opening browser to Claude Code Chats...'));
1129
+ } else {
1130
+ console.log(chalk.blue('🌐 Opening browser to Claude Code Analytics...'));
1131
+ }
1132
+
940
1133
  try {
941
- await open(`http://localhost:${this.port}`);
942
- console.log(chalk.blue('🌐 Opening browser...'));
1134
+ await open(fullUrl);
943
1135
  } catch (error) {
944
1136
  console.log(chalk.yellow('Could not open browser automatically. Please visit:'));
945
- console.log(chalk.cyan(`http://localhost:${this.port}`));
1137
+ console.log(chalk.cyan(fullUrl));
946
1138
  }
947
1139
  }
948
1140
 
@@ -962,14 +1154,84 @@ class ClaudeAnalytics {
962
1154
  this.notificationManager = new NotificationManager(this.webSocketServer);
963
1155
  await this.notificationManager.initialize();
964
1156
 
1157
+ // Connect notification manager to file watcher for typing detection
1158
+ this.fileWatcher.setNotificationManager(this.notificationManager);
1159
+
965
1160
  // Setup notification subscriptions
966
1161
  this.setupNotificationSubscriptions();
967
1162
 
968
- console.log(chalk.green('✅ WebSocket and notifications initialized'));
1163
+ // Initialize Console Bridge for Claude Code interaction
1164
+ await this.initializeConsoleBridge();
1165
+
1166
+ console.log(chalk.green('✅ WebSocket, notifications, and console bridge initialized'));
969
1167
  } catch (error) {
970
1168
  console.error(chalk.red('❌ Failed to initialize WebSocket:'), error);
971
1169
  }
972
1170
  }
1171
+
1172
+ /**
1173
+ * Initialize Console Bridge for Claude Code interaction
1174
+ */
1175
+ async initializeConsoleBridge() {
1176
+ try {
1177
+ console.log(chalk.blue('🌉 Initializing Console Bridge...'));
1178
+
1179
+ // Create console bridge on a different port (3334)
1180
+ this.consoleBridge = new ConsoleBridge({
1181
+ port: 3334,
1182
+ debug: false // Set to true for detailed debugging
1183
+ });
1184
+
1185
+ // Initialize the bridge
1186
+ const success = await this.consoleBridge.initialize();
1187
+
1188
+ if (success) {
1189
+ console.log(chalk.green('✅ Console Bridge initialized on port 3334'));
1190
+ console.log(chalk.cyan('🔌 Web interface can connect to ws://localhost:3334 for console interactions'));
1191
+
1192
+ // Bridge console interactions to main WebSocket
1193
+ this.setupConsoleBridgeIntegration();
1194
+ } else {
1195
+ console.warn(chalk.yellow('⚠️ Console Bridge failed to initialize - console interactions disabled'));
1196
+ }
1197
+
1198
+ } catch (error) {
1199
+ console.warn(chalk.yellow('⚠️ Console Bridge initialization failed:'), error.message);
1200
+ console.log(chalk.gray('Console interactions will not be available, but analytics will continue normally'));
1201
+ }
1202
+ }
1203
+
1204
+ /**
1205
+ * Setup integration between Console Bridge and main WebSocket
1206
+ */
1207
+ setupConsoleBridgeIntegration() {
1208
+ if (!this.consoleBridge || !this.webSocketServer) return;
1209
+
1210
+ // Forward console interactions from bridge to main WebSocket
1211
+ this.consoleBridge.on('console_interaction', (interactionData) => {
1212
+ console.log(chalk.blue('📡 Forwarding console interaction to web interface'));
1213
+
1214
+ // Broadcast to main WebSocket clients
1215
+ this.webSocketServer.broadcast({
1216
+ type: 'console_interaction',
1217
+ data: interactionData
1218
+ });
1219
+ });
1220
+
1221
+ // Listen for responses from main WebSocket and forward to bridge
1222
+ this.webSocketServer.on('console_response', (responseData) => {
1223
+ console.log(chalk.blue('📱 Forwarding console response to Claude Code'));
1224
+
1225
+ if (this.consoleBridge) {
1226
+ this.consoleBridge.handleWebMessage({
1227
+ type: 'console_response',
1228
+ data: responseData
1229
+ });
1230
+ }
1231
+ });
1232
+
1233
+ console.log(chalk.green('🔗 Console Bridge integration established'));
1234
+ }
973
1235
 
974
1236
  /**
975
1237
  * Setup notification subscriptions
@@ -977,7 +1239,6 @@ class ClaudeAnalytics {
977
1239
  setupNotificationSubscriptions() {
978
1240
  // Subscribe to refresh requests from WebSocket clients
979
1241
  this.notificationManager.subscribe('refresh_requested', async (notification) => {
980
- console.log(chalk.blue('🔄 Refresh requested via WebSocket'));
981
1242
  await this.loadInitialData();
982
1243
 
983
1244
  // Notify clients of the refreshed data
@@ -1108,6 +1369,7 @@ class ClaudeAnalytics {
1108
1369
  }
1109
1370
  }
1110
1371
 
1372
+
1111
1373
  stop() {
1112
1374
  // Stop file watchers
1113
1375
  this.fileWatcher.stop();
@@ -1123,6 +1385,11 @@ class ClaudeAnalytics {
1123
1385
  this.notificationManager.shutdown();
1124
1386
  }
1125
1387
 
1388
+ // Shutdown console bridge
1389
+ if (this.consoleBridge) {
1390
+ this.consoleBridge.shutdown();
1391
+ }
1392
+
1126
1393
  if (this.httpServer) {
1127
1394
  this.httpServer.close();
1128
1395
  }
@@ -1140,7 +1407,14 @@ class ClaudeAnalytics {
1140
1407
  }
1141
1408
 
1142
1409
  async function runAnalytics(options = {}) {
1143
- console.log(chalk.blue('📊 Starting Claude Code Analytics Dashboard...'));
1410
+ // Determine if we're opening to a specific page
1411
+ const openTo = options.openTo;
1412
+
1413
+ if (openTo === 'agents') {
1414
+ console.log(chalk.blue('💬 Starting Claude Code Chats Dashboard...'));
1415
+ } else {
1416
+ console.log(chalk.blue('📊 Starting Claude Code Analytics Dashboard...'));
1417
+ }
1144
1418
 
1145
1419
  const analytics = new ClaudeAnalytics();
1146
1420
 
@@ -1151,9 +1425,15 @@ async function runAnalytics(options = {}) {
1151
1425
  // Web dashboard files are now static in analytics-web directory
1152
1426
 
1153
1427
  await analytics.startServer();
1154
- await analytics.openBrowser();
1155
-
1156
- console.log(chalk.green('✅ Analytics dashboard is running!'));
1428
+ await analytics.openBrowser(openTo);
1429
+
1430
+ if (openTo === 'agents') {
1431
+ console.log(chalk.green('✅ Claude Code Chats dashboard is running!'));
1432
+ console.log(chalk.cyan(`📱 Access at: http://localhost:${analytics.port}/#agents`));
1433
+ } else {
1434
+ console.log(chalk.green('✅ Analytics dashboard is running!'));
1435
+ console.log(chalk.cyan(`📱 Access at: http://localhost:${analytics.port}`));
1436
+ }
1157
1437
  console.log(chalk.gray('Press Ctrl+C to stop the server'));
1158
1438
 
1159
1439
  // Handle graceful shutdown
@@ -1173,6 +1453,14 @@ async function runAnalytics(options = {}) {
1173
1453
  }
1174
1454
 
1175
1455
 
1456
+ // If this file is executed directly, run analytics
1457
+ if (require.main === module) {
1458
+ runAnalytics().catch(error => {
1459
+ console.error(chalk.red('❌ Analytics startup failed:'), error);
1460
+ process.exit(1);
1461
+ });
1462
+ }
1463
+
1176
1464
  module.exports = {
1177
1465
  runAnalytics
1178
1466
  };