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/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.
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
.
|
|
382
|
-
.
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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 {
|
|
394
|
-
* @returns {string} Project name or 'Unknown'
|
|
395
|
-
*/
|
|
396
|
-
extractProjectFromConversation(
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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
|
|
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
|
-
|
|
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();
|