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 CHANGED
@@ -76,6 +76,10 @@ npx claude-code-templates
76
76
  ```bash
77
77
  # Launch real-time analytics dashboard
78
78
  npx claude-code-templates --analytics
79
+
80
+ # Launch chats/conversations dashboard (opens directly to conversations)
81
+ npx claude-code-templates --chats
82
+ npx claude-code-templates --agents
79
83
  ```
80
84
 
81
85
  ### Health Check
@@ -128,6 +132,8 @@ npx create-claude-config # Create-style command
128
132
  | `-y, --yes` | Skip prompts and use defaults | `--yes` |
129
133
  | `--dry-run` | Show what would be installed | `--dry-run` |
130
134
  | `--analytics` | Launch real-time analytics dashboard | `--analytics` |
135
+ | `--chats` | Launch chats/conversations dashboard | `--chats` |
136
+ | `--agents` | Launch agents dashboard (alias for chats) | `--agents` |
131
137
  | `--health-check` | Run comprehensive system validation | `--health-check` |
132
138
  | `--health` | Run system health check (alias) | `--health` |
133
139
  | `--check` | Run system validation (alias) | `--check` |
@@ -45,6 +45,7 @@ program
45
45
  .option('--hook-stats, --hooks-stats', 'analyze existing automation hooks and offer optimization')
46
46
  .option('--mcp-stats, --mcps-stats', 'analyze existing MCP server configurations and offer optimization')
47
47
  .option('--analytics', 'launch real-time Claude Code analytics dashboard')
48
+ .option('--chats, --agents', 'launch Claude Code chats/agents dashboard (opens directly to conversations)')
48
49
  .option('--health-check, --health, --check, --verify', 'run comprehensive health check to verify Claude Code setup')
49
50
  .action(async (options) => {
50
51
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-templates",
3
- "version": "1.10.1",
3
+ "version": "1.12.0",
4
4
  "description": "CLI tool to setup Claude Code configurations with framework-specific commands, automation hooks and MCP Servers for your projects",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -31,7 +31,6 @@
31
31
  "dev:link": "npm link",
32
32
  "dev:unlink": "npm unlink -g claude-code-templates",
33
33
  "pretest:commands": "npm run dev:link",
34
- "prepublishOnly": "echo 'Skipping tests for Ruby on Rails 8 release'",
35
34
  "analytics:start": "node src/analytics.js",
36
35
  "analytics:test": "npm run test:analytics"
37
36
  },
@@ -93,7 +93,7 @@ class ConversationAnalyzer {
93
93
  };
94
94
 
95
95
  const jsonlFiles = await findJsonlFiles(this.claudeDir);
96
- console.log(chalk.blue(`Found ${jsonlFiles.length} conversation files`));
96
+ // Loading conversation files quietly for better UX
97
97
 
98
98
  for (const filePath of jsonlFiles) {
99
99
  const stats = await this.getFileStats(filePath);
@@ -101,7 +101,7 @@ class ConversationAnalyzer {
101
101
 
102
102
  try {
103
103
  // Extract project name from path
104
- const projectFromPath = this.extractProjectFromPath(filePath);
104
+ const projectFromPath = await this.extractProjectFromPath(filePath);
105
105
 
106
106
  // Use cached parsed conversation if available
107
107
  const parsedMessages = await this.getParsedConversation(filePath);
@@ -113,6 +113,9 @@ class ConversationAnalyzer {
113
113
  // Calculate tool usage data with caching
114
114
  const toolUsage = await this.getCachedToolUsage(filePath, parsedMessages);
115
115
 
116
+ const projectFromConversation = await this.extractProjectFromConversation(filePath);
117
+ const finalProject = projectFromConversation || projectFromPath;
118
+
116
119
  const conversation = {
117
120
  id: filename.replace('.jsonl', ''),
118
121
  filename: filename,
@@ -125,7 +128,7 @@ class ConversationAnalyzer {
125
128
  tokenUsage: tokenUsage,
126
129
  modelInfo: modelInfo,
127
130
  toolUsage: toolUsage,
128
- project: projectFromPath || this.extractProjectFromConversation(parsedMessages),
131
+ project: finalProject,
129
132
  status: stateCalculator.determineConversationStatus(parsedMessages, stats.mtime),
130
133
  conversationState: stateCalculator.determineConversationState(parsedMessages, stats.mtime),
131
134
  statusSquares: await this.getCachedStatusSquares(filePath, parsedMessages),
@@ -216,26 +219,96 @@ class ConversationAnalyzer {
216
219
  return await this.dataCache.getParsedConversation(filepath);
217
220
  }
218
221
 
219
- // Fallback to direct parsing
222
+ // Fallback to direct parsing with tool correlation
220
223
  const content = await fs.readFile(filepath, 'utf8');
221
- return content.trim().split('\n')
222
- .filter(line => line.trim())
223
- .map(line => {
224
- try {
225
- const item = JSON.parse(line);
226
- if (item.message && item.message.role) {
227
- return {
228
- role: item.message.role,
229
- timestamp: new Date(item.timestamp),
230
- content: item.message.content,
231
- model: item.message.model || null,
232
- usage: item.message.usage || null,
233
- };
224
+ const lines = content.trim().split('\n').filter(line => line.trim());
225
+
226
+ return this.parseAndCorrelateToolMessages(lines);
227
+ }
228
+
229
+ /**
230
+ * Parse JSONL lines and correlate tool_use with tool_result
231
+ * @param {Array} lines - JSONL lines
232
+ * @returns {Array} Parsed and correlated messages
233
+ */
234
+ parseAndCorrelateToolMessages(lines) {
235
+ const entries = [];
236
+ const toolUseMap = new Map();
237
+
238
+ // First pass: parse all entries and map tool_use entries
239
+ for (const line of lines) {
240
+ try {
241
+ const item = JSON.parse(line);
242
+ if (item.message && (item.type === 'assistant' || item.type === 'user')) {
243
+ entries.push(item);
244
+
245
+ // Track tool_use entries by their ID
246
+ if (item.type === 'assistant' && item.message.content) {
247
+ const toolUseBlock = Array.isArray(item.message.content)
248
+ ? item.message.content.find(c => c.type === 'tool_use')
249
+ : (item.message.content.type === 'tool_use' ? item.message.content : null);
250
+
251
+ if (toolUseBlock && toolUseBlock.id) {
252
+ toolUseMap.set(toolUseBlock.id, item);
253
+ }
254
+ }
255
+ }
256
+ } catch (error) {
257
+ // Skip invalid JSONL lines
258
+ }
259
+ }
260
+
261
+ // Second pass: correlate tool_result with tool_use and filter out standalone tool_result entries
262
+ const processedMessages = [];
263
+
264
+ for (const item of entries) {
265
+ if (item.type === 'user' && item.message.content) {
266
+ // Check if this is a tool_result entry
267
+ const toolResultBlock = Array.isArray(item.message.content)
268
+ ? item.message.content.find(c => c.type === 'tool_result')
269
+ : (item.message.content.type === 'tool_result' ? item.message.content : null);
270
+
271
+ if (toolResultBlock && toolResultBlock.tool_use_id) {
272
+ // This is a tool_result - attach it to the corresponding tool_use
273
+ // console.log(`🔍 ConversationAnalyzer: Found tool_result for ${toolResultBlock.tool_use_id}, content: "${toolResultBlock.content}"`);
274
+ const toolUseEntry = toolUseMap.get(toolResultBlock.tool_use_id);
275
+ // console.log(`🔍 ConversationAnalyzer: toolUseEntry found: ${!!toolUseEntry}`);
276
+ if (toolUseEntry) {
277
+ // Attach tool result to the tool use entry
278
+ if (!toolUseEntry.toolResults) {
279
+ toolUseEntry.toolResults = [];
280
+ }
281
+ toolUseEntry.toolResults.push(toolResultBlock);
282
+ // console.log(`✅ ConversationAnalyzer: Attached tool result to ${toolResultBlock.tool_use_id}, content length: ${toolResultBlock.content?.length || 0}`);
283
+ // Don't add this tool_result as a separate message
284
+ continue;
285
+ } else {
286
+ // console.log(`❌ ConversationAnalyzer: No tool_use found for ${toolResultBlock.tool_use_id}`);
234
287
  }
235
- } catch {}
236
- return null;
237
- })
238
- .filter(Boolean);
288
+ }
289
+ }
290
+
291
+ // Convert to our standard format
292
+ if (item.toolResults) {
293
+ // console.log(`ConversationAnalyzer: Processing item with ${item.toolResults.length} tool results`);
294
+ }
295
+ const parsed = {
296
+ id: item.message.id || item.uuid || null,
297
+ role: item.message.role || (item.type === 'assistant' ? 'assistant' : 'user'),
298
+ timestamp: new Date(item.timestamp),
299
+ content: item.message.content,
300
+ model: item.message.model || null,
301
+ usage: item.message.usage || null,
302
+ toolResults: item.toolResults || null, // Include attached tool results
303
+ isCompactSummary: item.isCompactSummary || false, // Preserve compact summary flag
304
+ uuid: item.uuid || null, // Include UUID for message identification
305
+ type: item.type || null // Include type field
306
+ };
307
+
308
+ processedMessages.push(parsed);
309
+ }
310
+
311
+ return processedMessages;
239
312
  }
240
313
 
241
314
  /**
@@ -363,11 +436,11 @@ class ConversationAnalyzer {
363
436
  }
364
437
 
365
438
  /**
366
- * Extract project name from Claude directory file path
439
+ * Extract project name from Claude directory file path using settings.json
367
440
  * @param {string} filePath - Full path to conversation file
368
- * @returns {string|null} Project name or null
441
+ * @returns {Promise<string|null>} Project name or null
369
442
  */
370
- extractProjectFromPath(filePath) {
443
+ async extractProjectFromPath(filePath) {
371
444
  // Extract project name from file path like:
372
445
  // /Users/user/.claude/projects/-Users-user-Projects-MyProject/conversation.jsonl
373
446
  const pathParts = filePath.split('/');
@@ -375,34 +448,73 @@ class ConversationAnalyzer {
375
448
 
376
449
  if (projectIndex !== -1 && projectIndex + 1 < pathParts.length) {
377
450
  const projectDir = pathParts[projectIndex + 1];
378
- // Clean up the project directory name
379
- const cleanName = projectDir
380
- .replace(/^-/, '')
381
- .replace(/-/g, '/')
382
- .split('/')
383
- .pop() || 'Unknown';
384
-
385
- return cleanName;
451
+
452
+ // Try to read the settings.json file for this project
453
+ try {
454
+ const projectPath = path.join(path.dirname(filePath)); // Directory containing the conversation file
455
+ const settingsPath = path.join(projectPath, 'settings.json');
456
+
457
+ if (await fs.pathExists(settingsPath)) {
458
+ const settingsContent = await fs.readFile(settingsPath, 'utf8');
459
+ const settings = JSON.parse(settingsContent);
460
+
461
+ if (settings.projectName) {
462
+ return settings.projectName;
463
+ }
464
+
465
+ // If no projectName in settings, try to extract from projectPath
466
+ if (settings.projectPath) {
467
+ return path.basename(settings.projectPath);
468
+ }
469
+ }
470
+ } catch (error) {
471
+ // If we can't read settings.json, fall back to parsing the directory name
472
+ console.warn(chalk.yellow(`Warning: Could not read settings.json for project ${projectDir}:`, error.message));
473
+ }
474
+
475
+ // Fallback: we'll extract project name from conversation content instead
476
+ // For now, return null to trigger reading from conversation file
477
+ return null;
386
478
  }
387
479
 
388
480
  return null;
389
481
  }
390
482
 
483
+
391
484
  /**
392
485
  * Attempt to extract project information from conversation content
393
- * @param {Array} messages - Array of message objects
394
- * @returns {string} Project name or 'Unknown'
395
- */
396
- extractProjectFromConversation(messages) {
397
- // Try to extract project information from conversation
398
- for (const message of messages.slice(0, 5)) {
399
- if (message.content && typeof message.content === 'string') {
400
- const pathMatch = message.content.match(/\/([^\/\s]+)$/);
401
- if (pathMatch) {
402
- return pathMatch[1];
486
+ * @param {string} filePath - Path to the conversation file
487
+ * @returns {Promise<string>} Project name or 'Unknown'
488
+ */
489
+ async extractProjectFromConversation(filePath) {
490
+ try {
491
+ // Read the conversation file and look for cwd field
492
+ const content = await this.getFileContent(filePath);
493
+ const lines = content.trim().split('\n').filter(line => line.trim());
494
+
495
+ for (const line of lines.slice(0, 10)) { // Check first 10 lines
496
+ try {
497
+ const item = JSON.parse(line);
498
+
499
+ // Look for cwd field in the message
500
+ if (item.cwd) {
501
+ const projectName = path.basename(item.cwd);
502
+ return projectName;
503
+ }
504
+
505
+ // Also check if it's in nested objects
506
+ if (item.message && item.message.cwd) {
507
+ return path.basename(item.message.cwd);
508
+ }
509
+ } catch (parseError) {
510
+ // Skip invalid JSON lines
511
+ continue;
403
512
  }
404
513
  }
514
+ } catch (error) {
515
+ console.warn(chalk.yellow(`Warning: Could not extract project from conversation ${filePath}:`, error.message));
405
516
  }
517
+
406
518
  return 'Unknown';
407
519
  }
408
520
 
@@ -576,7 +688,8 @@ class ConversationAnalyzer {
576
688
  const totalFileSize = conversations.reduce((sum, conv) => sum + conv.fileSize, 0);
577
689
 
578
690
  // Calculate real Claude sessions (5-hour periods)
579
- const claudeSessions = await this.calculateClaudeSessions(conversations);
691
+ const claudeSessionsResult = await this.calculateClaudeSessions(conversations);
692
+ const claudeSessions = claudeSessionsResult?.total || 0;
580
693
 
581
694
  return {
582
695
  totalConversations,
@@ -585,8 +698,11 @@ class ConversationAnalyzer {
585
698
  activeProjects,
586
699
  avgTokensPerConversation,
587
700
  totalFileSize: this.formatBytes(totalFileSize),
701
+ dataSize: this.formatBytes(totalFileSize), // Alias for original dashboard compatibility
588
702
  lastActivity: conversations.length > 0 ? conversations[0].lastModified : null,
589
703
  claudeSessions,
704
+ claudeSessionsDetail: claudeSessions > 0 ? `${claudeSessions} session${claudeSessions > 1 ? 's' : ''}` : 'no sessions',
705
+ claudeSessionsFullData: claudeSessionsResult, // Keep full session data for detailed analysis
590
706
  };
591
707
  }
592
708
 
@@ -11,6 +11,8 @@ class FileWatcher {
11
11
  this.watchers = [];
12
12
  this.intervals = [];
13
13
  this.isActive = false;
14
+ this.fileActivity = new Map(); // Track file activity for typing detection
15
+ this.typingTimeout = new Map(); // Track typing timeouts
14
16
  }
15
17
 
16
18
  /**
@@ -20,13 +22,14 @@ class FileWatcher {
20
22
  * @param {Function} processRefreshCallback - Callback to refresh process data
21
23
  * @param {Object} dataCache - DataCache instance for invalidation
22
24
  */
23
- setupFileWatchers(claudeDir, dataRefreshCallback, processRefreshCallback, dataCache = null) {
25
+ setupFileWatchers(claudeDir, dataRefreshCallback, processRefreshCallback, dataCache = null, conversationChangeCallback = null) {
24
26
  console.log(chalk.blue('👀 Setting up file watchers for real-time updates...'));
25
27
 
26
28
  this.claudeDir = claudeDir;
27
29
  this.dataRefreshCallback = dataRefreshCallback;
28
30
  this.processRefreshCallback = processRefreshCallback;
29
31
  this.dataCache = dataCache;
32
+ this.conversationChangeCallback = conversationChangeCallback;
30
33
 
31
34
  this.setupConversationWatcher();
32
35
  this.setupProjectWatcher();
@@ -47,21 +50,28 @@ class FileWatcher {
47
50
  });
48
51
 
49
52
  conversationWatcher.on('change', async (filePath) => {
50
- console.log(chalk.yellow('🔄 Conversation file changed, updating data...'));
53
+
54
+ // Extract conversation ID from file path
55
+ const conversationId = this.extractConversationId(filePath);
56
+
57
+ // Enhanced file activity detection for typing
58
+ await this.handleFileActivity(conversationId, filePath);
51
59
 
52
60
  // Invalidate cache for the changed file
53
61
  if (this.dataCache && filePath) {
54
62
  this.dataCache.invalidateFile(filePath);
55
63
  }
56
64
 
65
+ // Notify specific conversation change if callback exists
66
+ if (this.conversationChangeCallback && conversationId) {
67
+ await this.conversationChangeCallback(conversationId, filePath);
68
+ }
69
+
57
70
  await this.triggerDataRefresh();
58
- console.log(chalk.green('✅ Data updated'));
59
71
  });
60
72
 
61
73
  conversationWatcher.on('add', async () => {
62
- console.log(chalk.yellow('📝 New conversation file detected...'));
63
74
  await this.triggerDataRefresh();
64
- console.log(chalk.green('✅ Data updated'));
65
75
  });
66
76
 
67
77
  this.watchers.push(conversationWatcher);
@@ -78,15 +88,11 @@ class FileWatcher {
78
88
  });
79
89
 
80
90
  projectWatcher.on('addDir', async () => {
81
- console.log(chalk.yellow('📁 New project directory detected...'));
82
91
  await this.triggerDataRefresh();
83
- console.log(chalk.green('✅ Data updated'));
84
92
  });
85
93
 
86
94
  projectWatcher.on('change', async () => {
87
- console.log(chalk.yellow('📁 Project directory changed...'));
88
95
  await this.triggerDataRefresh();
89
- console.log(chalk.green('✅ Data updated'));
90
96
  });
91
97
 
92
98
  this.watchers.push(projectWatcher);
@@ -98,7 +104,6 @@ class FileWatcher {
98
104
  setupPeriodicRefresh() {
99
105
  // Periodic refresh to catch any missed changes (reduced frequency)
100
106
  const dataRefreshInterval = setInterval(async () => {
101
- console.log(chalk.blue('⏱️ Periodic data refresh...'));
102
107
  await this.triggerDataRefresh();
103
108
  }, 120000); // Every 2 minutes (reduced from 30 seconds)
104
109
 
@@ -114,6 +119,137 @@ class FileWatcher {
114
119
  this.intervals.push(processRefreshInterval);
115
120
  }
116
121
 
122
+ /**
123
+ * Extract conversation ID from file path
124
+ * @param {string} filePath - Path to the conversation file
125
+ * @returns {string|null} Conversation ID or null if not found
126
+ */
127
+ extractConversationId(filePath) {
128
+ try {
129
+ // Handle different path formats:
130
+ // /Users/user/.claude/projects/PROJECT_NAME/conversation.jsonl -> PROJECT_NAME
131
+ // /Users/user/.claude/CONVERSATION_ID.jsonl -> CONVERSATION_ID
132
+
133
+ const pathParts = filePath.split(path.sep);
134
+ const fileName = pathParts[pathParts.length - 1];
135
+
136
+ if (fileName === 'conversation.jsonl') {
137
+ // Project-based conversation
138
+ const projectName = pathParts[pathParts.length - 2];
139
+ return projectName;
140
+ } else if (fileName.endsWith('.jsonl')) {
141
+ // Direct conversation file
142
+ return fileName.replace('.jsonl', '');
143
+ }
144
+
145
+ return null;
146
+ } catch (error) {
147
+ console.error(chalk.red('Error extracting conversation ID:'), error);
148
+ return null;
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Handle file activity for typing detection
154
+ * @param {string} conversationId - Conversation ID
155
+ * @param {string} filePath - File path that changed
156
+ */
157
+ async handleFileActivity(conversationId, filePath) {
158
+ if (!conversationId) return;
159
+
160
+ const fs = require('fs');
161
+ try {
162
+ // Get file stats
163
+ const stats = fs.statSync(filePath);
164
+ const now = Date.now();
165
+ const fileSize = stats.size;
166
+ const mtime = stats.mtime.getTime();
167
+
168
+ // Get previous activity
169
+ const previousActivity = this.fileActivity.get(conversationId) || {
170
+ lastSize: 0,
171
+ lastMtime: 0,
172
+ lastMessageCheck: 0
173
+ };
174
+
175
+ // Check if this is just a file touch/modification without significant content change
176
+ const sizeChanged = fileSize !== previousActivity.lastSize;
177
+ const timeChanged = mtime !== previousActivity.lastMtime;
178
+ const timeSinceLastCheck = now - previousActivity.lastMessageCheck;
179
+
180
+ // Update activity tracking
181
+ this.fileActivity.set(conversationId, {
182
+ lastSize: fileSize,
183
+ lastMtime: mtime,
184
+ lastMessageCheck: now
185
+ });
186
+
187
+ // If file changed but we haven't checked for complete messages recently
188
+ if ((sizeChanged || timeChanged) && timeSinceLastCheck > 1000) {
189
+ // Clear any existing typing timeout
190
+ const existingTimeout = this.typingTimeout.get(conversationId);
191
+ if (existingTimeout) {
192
+ clearTimeout(existingTimeout);
193
+ }
194
+
195
+ // Set a timeout to detect if this is typing activity
196
+ const typingTimeout = setTimeout(async () => {
197
+ // After delay, check if a complete message was added
198
+ await this.checkForTypingActivity(conversationId, filePath);
199
+ }, 2000); // Wait 2 seconds to see if a complete message appears
200
+
201
+ this.typingTimeout.set(conversationId, typingTimeout);
202
+ }
203
+ } catch (error) {
204
+ console.error(chalk.red(`Error handling file activity for ${conversationId}:`), error);
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Check if file activity indicates user typing
210
+ * @param {string} conversationId - Conversation ID
211
+ * @param {string} filePath - File path to check
212
+ */
213
+ async checkForTypingActivity(conversationId, filePath) {
214
+ try {
215
+ // Parse the conversation to see if new complete messages were added
216
+ const ConversationAnalyzer = require('./ConversationAnalyzer');
217
+ const analyzer = new ConversationAnalyzer();
218
+ const messages = await analyzer.getParsedConversation(filePath);
219
+
220
+ if (messages && messages.length > 0) {
221
+ const lastMessage = messages[messages.length - 1];
222
+ const lastMessageTime = new Date(lastMessage.timestamp).getTime();
223
+ const now = Date.now();
224
+ const messageAge = now - lastMessageTime;
225
+
226
+ // If the last message is very recent (< 5 seconds), it's probably a new complete message
227
+ // If it's older, the file activity might indicate typing
228
+ if (messageAge > 5000 && lastMessage.role === 'assistant') {
229
+ // File activity after assistant message suggests user is typing
230
+
231
+ // Send typing notification if we have access to notification manager
232
+ if (this.notificationManager) {
233
+ this.notificationManager.notifyConversationStateChange(conversationId, 'User typing...', {
234
+ detectionMethod: 'file_activity',
235
+ timestamp: new Date().toISOString()
236
+ });
237
+ }
238
+ }
239
+ }
240
+ } catch (error) {
241
+ console.error(chalk.red(`Error checking typing activity for ${conversationId}:`), error);
242
+ }
243
+ }
244
+
245
+ /**
246
+ * Set notification manager for state notifications
247
+ * @param {Object} notificationManager - NotificationManager instance
248
+ */
249
+ setNotificationManager(notificationManager) {
250
+ this.notificationManager = notificationManager;
251
+ }
252
+
117
253
  /**
118
254
  * Trigger data refresh with error handling
119
255
  */
@@ -274,7 +410,6 @@ class FileWatcher {
274
410
  * Force immediate refresh
275
411
  */
276
412
  async forceRefresh() {
277
- console.log(chalk.cyan('🔄 Force refreshing data...'));
278
413
  await this.triggerDataRefresh();
279
414
  if (this.processRefreshCallback) {
280
415
  await this.processRefreshCallback();