claude-code-templates 1.8.0 → 1.8.2
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 +246 -0
- package/package.json +26 -12
- package/src/analytics/core/ConversationAnalyzer.js +754 -0
- package/src/analytics/core/FileWatcher.js +285 -0
- package/src/analytics/core/ProcessDetector.js +242 -0
- package/src/analytics/core/SessionAnalyzer.js +631 -0
- package/src/analytics/core/StateCalculator.js +190 -0
- package/src/analytics/data/DataCache.js +550 -0
- package/src/analytics/notifications/NotificationManager.js +448 -0
- package/src/analytics/notifications/WebSocketServer.js +526 -0
- package/src/analytics/utils/PerformanceMonitor.js +455 -0
- package/src/analytics-web/assets/js/main.js +312 -0
- package/src/analytics-web/components/Charts.js +114 -0
- package/src/analytics-web/components/ConversationTable.js +437 -0
- package/src/analytics-web/components/Dashboard.js +573 -0
- package/src/analytics-web/components/SessionTimer.js +596 -0
- package/src/analytics-web/index.html +882 -49
- package/src/analytics-web/index.html.original +1939 -0
- package/src/analytics-web/services/DataService.js +357 -0
- package/src/analytics-web/services/StateService.js +276 -0
- package/src/analytics-web/services/WebSocketService.js +523 -0
- package/src/analytics.js +641 -2311
- package/src/analytics.log +0 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const chokidar = require('chokidar');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* FileWatcher - Handles file system watching and automatic data refresh
|
|
7
|
+
* Extracted from monolithic analytics.js for better maintainability
|
|
8
|
+
*/
|
|
9
|
+
class FileWatcher {
|
|
10
|
+
constructor() {
|
|
11
|
+
this.watchers = [];
|
|
12
|
+
this.intervals = [];
|
|
13
|
+
this.isActive = false;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Setup file watchers for real-time updates
|
|
18
|
+
* @param {string} claudeDir - Path to Claude directory
|
|
19
|
+
* @param {Function} dataRefreshCallback - Callback to refresh data
|
|
20
|
+
* @param {Function} processRefreshCallback - Callback to refresh process data
|
|
21
|
+
* @param {Object} dataCache - DataCache instance for invalidation
|
|
22
|
+
*/
|
|
23
|
+
setupFileWatchers(claudeDir, dataRefreshCallback, processRefreshCallback, dataCache = null) {
|
|
24
|
+
console.log(chalk.blue('👀 Setting up file watchers for real-time updates...'));
|
|
25
|
+
|
|
26
|
+
this.claudeDir = claudeDir;
|
|
27
|
+
this.dataRefreshCallback = dataRefreshCallback;
|
|
28
|
+
this.processRefreshCallback = processRefreshCallback;
|
|
29
|
+
this.dataCache = dataCache;
|
|
30
|
+
|
|
31
|
+
this.setupConversationWatcher();
|
|
32
|
+
this.setupProjectWatcher();
|
|
33
|
+
this.setupPeriodicRefresh();
|
|
34
|
+
|
|
35
|
+
this.isActive = true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Setup watcher for conversation files (.jsonl)
|
|
40
|
+
*/
|
|
41
|
+
setupConversationWatcher() {
|
|
42
|
+
const conversationWatcher = chokidar.watch([
|
|
43
|
+
path.join(this.claudeDir, '**/*.jsonl')
|
|
44
|
+
], {
|
|
45
|
+
persistent: true,
|
|
46
|
+
ignoreInitial: true,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
conversationWatcher.on('change', async (filePath) => {
|
|
50
|
+
console.log(chalk.yellow('🔄 Conversation file changed, updating data...'));
|
|
51
|
+
|
|
52
|
+
// Invalidate cache for the changed file
|
|
53
|
+
if (this.dataCache && filePath) {
|
|
54
|
+
this.dataCache.invalidateFile(filePath);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
await this.triggerDataRefresh();
|
|
58
|
+
console.log(chalk.green('✅ Data updated'));
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
conversationWatcher.on('add', async () => {
|
|
62
|
+
console.log(chalk.yellow('📝 New conversation file detected...'));
|
|
63
|
+
await this.triggerDataRefresh();
|
|
64
|
+
console.log(chalk.green('✅ Data updated'));
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
this.watchers.push(conversationWatcher);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Setup watcher for project directories
|
|
72
|
+
*/
|
|
73
|
+
setupProjectWatcher() {
|
|
74
|
+
const projectWatcher = chokidar.watch(this.claudeDir, {
|
|
75
|
+
persistent: true,
|
|
76
|
+
ignoreInitial: true,
|
|
77
|
+
depth: 2, // Increased depth to catch subdirectories
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
projectWatcher.on('addDir', async () => {
|
|
81
|
+
console.log(chalk.yellow('📁 New project directory detected...'));
|
|
82
|
+
await this.triggerDataRefresh();
|
|
83
|
+
console.log(chalk.green('✅ Data updated'));
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
projectWatcher.on('change', async () => {
|
|
87
|
+
console.log(chalk.yellow('📁 Project directory changed...'));
|
|
88
|
+
await this.triggerDataRefresh();
|
|
89
|
+
console.log(chalk.green('✅ Data updated'));
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
this.watchers.push(projectWatcher);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Setup periodic refresh intervals
|
|
97
|
+
*/
|
|
98
|
+
setupPeriodicRefresh() {
|
|
99
|
+
// Periodic refresh to catch any missed changes (reduced frequency)
|
|
100
|
+
const dataRefreshInterval = setInterval(async () => {
|
|
101
|
+
console.log(chalk.blue('⏱️ Periodic data refresh...'));
|
|
102
|
+
await this.triggerDataRefresh();
|
|
103
|
+
}, 120000); // Every 2 minutes (reduced from 30 seconds)
|
|
104
|
+
|
|
105
|
+
this.intervals.push(dataRefreshInterval);
|
|
106
|
+
|
|
107
|
+
// Process updates for active processes (reduced frequency)
|
|
108
|
+
const processRefreshInterval = setInterval(async () => {
|
|
109
|
+
if (this.processRefreshCallback) {
|
|
110
|
+
await this.processRefreshCallback();
|
|
111
|
+
}
|
|
112
|
+
}, 30000); // Every 30 seconds (reduced from 10 seconds)
|
|
113
|
+
|
|
114
|
+
this.intervals.push(processRefreshInterval);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Trigger data refresh with error handling
|
|
119
|
+
*/
|
|
120
|
+
async triggerDataRefresh() {
|
|
121
|
+
try {
|
|
122
|
+
if (this.dataRefreshCallback) {
|
|
123
|
+
await this.dataRefreshCallback();
|
|
124
|
+
}
|
|
125
|
+
} catch (error) {
|
|
126
|
+
console.error(chalk.red('Error during data refresh:'), error.message);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Add a custom watcher
|
|
132
|
+
* @param {Object} watcher - Chokidar watcher instance
|
|
133
|
+
*/
|
|
134
|
+
addWatcher(watcher) {
|
|
135
|
+
this.watchers.push(watcher);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Add a custom interval
|
|
140
|
+
* @param {number} intervalId - Interval ID from setInterval
|
|
141
|
+
*/
|
|
142
|
+
addInterval(intervalId) {
|
|
143
|
+
this.intervals.push(intervalId);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Pause all watchers and intervals
|
|
148
|
+
*/
|
|
149
|
+
pause() {
|
|
150
|
+
console.log(chalk.yellow('⏸️ Pausing file watchers...'));
|
|
151
|
+
|
|
152
|
+
// Pause watchers (they will still exist but not trigger events)
|
|
153
|
+
this.watchers.forEach(watcher => {
|
|
154
|
+
if (watcher.unwatch) {
|
|
155
|
+
// Temporarily remove all watched paths
|
|
156
|
+
const watchedPaths = watcher.getWatched();
|
|
157
|
+
Object.keys(watchedPaths).forEach(dir => {
|
|
158
|
+
watchedPaths[dir].forEach(file => {
|
|
159
|
+
watcher.unwatch(path.join(dir, file));
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
this.isActive = false;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Resume all watchers
|
|
170
|
+
*/
|
|
171
|
+
resume() {
|
|
172
|
+
if (!this.isActive && this.claudeDir) {
|
|
173
|
+
console.log(chalk.green('▶️ Resuming file watchers...'));
|
|
174
|
+
|
|
175
|
+
// Clear existing watchers
|
|
176
|
+
this.stop();
|
|
177
|
+
|
|
178
|
+
// Restart watchers
|
|
179
|
+
this.setupFileWatchers(
|
|
180
|
+
this.claudeDir,
|
|
181
|
+
this.dataRefreshCallback,
|
|
182
|
+
this.processRefreshCallback
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Stop and cleanup all watchers and intervals
|
|
189
|
+
*/
|
|
190
|
+
stop() {
|
|
191
|
+
console.log(chalk.red('🛑 Stopping file watchers...'));
|
|
192
|
+
|
|
193
|
+
// Close all watchers
|
|
194
|
+
this.watchers.forEach(watcher => {
|
|
195
|
+
try {
|
|
196
|
+
watcher.close();
|
|
197
|
+
} catch (error) {
|
|
198
|
+
console.warn(chalk.yellow('Warning: Error closing watcher:'), error.message);
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// Clear all intervals
|
|
203
|
+
this.intervals.forEach(intervalId => {
|
|
204
|
+
clearInterval(intervalId);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// Reset arrays
|
|
208
|
+
this.watchers = [];
|
|
209
|
+
this.intervals = [];
|
|
210
|
+
this.isActive = false;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Get watcher status
|
|
215
|
+
* @returns {Object} Status information
|
|
216
|
+
*/
|
|
217
|
+
getStatus() {
|
|
218
|
+
return {
|
|
219
|
+
isActive: this.isActive,
|
|
220
|
+
watcherCount: this.watchers.length,
|
|
221
|
+
intervalCount: this.intervals.length,
|
|
222
|
+
watchedDir: this.claudeDir
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Check if watchers are active
|
|
228
|
+
* @returns {boolean} True if watchers are active
|
|
229
|
+
*/
|
|
230
|
+
isWatching() {
|
|
231
|
+
return this.isActive && this.watchers.length > 0;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Get list of watched paths (for debugging)
|
|
236
|
+
* @returns {Array} Array of watched paths
|
|
237
|
+
*/
|
|
238
|
+
getWatchedPaths() {
|
|
239
|
+
const watchedPaths = [];
|
|
240
|
+
|
|
241
|
+
this.watchers.forEach(watcher => {
|
|
242
|
+
if (watcher.getWatched) {
|
|
243
|
+
const watched = watcher.getWatched();
|
|
244
|
+
Object.keys(watched).forEach(dir => {
|
|
245
|
+
watched[dir].forEach(file => {
|
|
246
|
+
watchedPaths.push(path.join(dir, file));
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
return watchedPaths;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Set debounced refresh to avoid spam
|
|
257
|
+
* @param {number} debounceMs - Debounce time in milliseconds
|
|
258
|
+
*/
|
|
259
|
+
setDebounce(debounceMs = 200) {
|
|
260
|
+
let debounceTimeout;
|
|
261
|
+
const originalCallback = this.dataRefreshCallback;
|
|
262
|
+
|
|
263
|
+
this.dataRefreshCallback = async (...args) => {
|
|
264
|
+
clearTimeout(debounceTimeout);
|
|
265
|
+
debounceTimeout = setTimeout(async () => {
|
|
266
|
+
if (originalCallback) {
|
|
267
|
+
await originalCallback(...args);
|
|
268
|
+
}
|
|
269
|
+
}, debounceMs);
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Force immediate refresh
|
|
275
|
+
*/
|
|
276
|
+
async forceRefresh() {
|
|
277
|
+
console.log(chalk.cyan('🔄 Force refreshing data...'));
|
|
278
|
+
await this.triggerDataRefresh();
|
|
279
|
+
if (this.processRefreshCallback) {
|
|
280
|
+
await this.processRefreshCallback();
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
module.exports = FileWatcher;
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
const { exec } = require('child_process');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* ProcessDetector - Handles Claude CLI process detection and conversation matching
|
|
7
|
+
* Extracted from monolithic analytics.js for better maintainability
|
|
8
|
+
*/
|
|
9
|
+
class ProcessDetector {
|
|
10
|
+
constructor() {
|
|
11
|
+
// Cache for process detection to avoid repeated shell commands
|
|
12
|
+
this.processCache = {
|
|
13
|
+
data: null,
|
|
14
|
+
timestamp: 0,
|
|
15
|
+
ttl: 500 // 500ms cache
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Detect running Claude CLI processes
|
|
21
|
+
* @returns {Promise<Array>} Array of active Claude processes
|
|
22
|
+
*/
|
|
23
|
+
async detectRunningClaudeProcesses() {
|
|
24
|
+
// Check cache first
|
|
25
|
+
const now = Date.now();
|
|
26
|
+
if (this.processCache.data && (now - this.processCache.timestamp) < this.processCache.ttl) {
|
|
27
|
+
return this.processCache.data;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return new Promise((resolve) => {
|
|
31
|
+
// Search for processes containing 'claude' but exclude our own analytics process and system processes
|
|
32
|
+
exec('ps aux | grep -i claude | grep -v grep | grep -v analytics | grep -v "/Applications/Claude.app" | grep -v "npm start"', (error, stdout) => {
|
|
33
|
+
if (error) {
|
|
34
|
+
resolve([]);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const processes = stdout.split('\n')
|
|
39
|
+
.filter(line => line.trim())
|
|
40
|
+
.filter(line => {
|
|
41
|
+
// Only include actual Claude CLI processes, not system processes
|
|
42
|
+
const fullCommand = line.split(/\s+/).slice(10).join(' ');
|
|
43
|
+
return fullCommand.includes('claude') &&
|
|
44
|
+
!fullCommand.includes('chrome_crashpad_handler') &&
|
|
45
|
+
!fullCommand.includes('create-claude-config') &&
|
|
46
|
+
!fullCommand.includes('node bin/') &&
|
|
47
|
+
fullCommand.trim() === 'claude'; // Only the basic claude command
|
|
48
|
+
})
|
|
49
|
+
.map(line => {
|
|
50
|
+
const parts = line.split(/\s+/);
|
|
51
|
+
const fullCommand = parts.slice(10).join(' ');
|
|
52
|
+
|
|
53
|
+
// Extract useful information from command
|
|
54
|
+
const cwdMatch = fullCommand.match(/--cwd[=\s]+([^\s]+)/);
|
|
55
|
+
let workingDir = cwdMatch ? cwdMatch[1] : 'unknown';
|
|
56
|
+
|
|
57
|
+
// Skip pwdx for now since it doesn't exist on macOS
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
pid: parts[1],
|
|
61
|
+
command: fullCommand,
|
|
62
|
+
workingDir: workingDir,
|
|
63
|
+
startTime: new Date(), // For now we use current time
|
|
64
|
+
status: 'running',
|
|
65
|
+
user: parts[0]
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Cache the result
|
|
70
|
+
this.processCache = {
|
|
71
|
+
data: processes,
|
|
72
|
+
timestamp: now,
|
|
73
|
+
ttl: 500
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
resolve(processes);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Enrich conversation data with running process information
|
|
83
|
+
* @param {Array} conversations - Array of conversation objects
|
|
84
|
+
* @param {string} claudeDir - Path to Claude directory for file operations
|
|
85
|
+
* @param {Object} stateCalculator - StateCalculator instance for state calculations
|
|
86
|
+
* @returns {Promise<Object>} Object with enriched conversations and orphan processes
|
|
87
|
+
*/
|
|
88
|
+
async enrichWithRunningProcesses(conversations, claudeDir, stateCalculator) {
|
|
89
|
+
try {
|
|
90
|
+
const runningProcesses = await this.detectRunningClaudeProcesses();
|
|
91
|
+
|
|
92
|
+
// Add active process information to each conversation
|
|
93
|
+
for (const conversation of conversations) {
|
|
94
|
+
// Look for active process for this project
|
|
95
|
+
// If workingDir is unknown, match with the most recently modified conversation
|
|
96
|
+
let matchingProcess = runningProcesses.find(process =>
|
|
97
|
+
process.workingDir.includes(conversation.project) ||
|
|
98
|
+
process.command.includes(conversation.project)
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
// Fallback: if no direct match and workingDir is unknown,
|
|
102
|
+
// assume the most recently modified conversation belongs to the active process
|
|
103
|
+
if (!matchingProcess && runningProcesses.length > 0 && runningProcesses[0].workingDir === 'unknown') {
|
|
104
|
+
// Find the most recently modified conversation
|
|
105
|
+
const sortedConversations = [...conversations].sort((a, b) =>
|
|
106
|
+
new Date(b.lastModified) - new Date(a.lastModified)
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
if (conversation === sortedConversations[0]) {
|
|
110
|
+
matchingProcess = runningProcesses[0];
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (matchingProcess) {
|
|
115
|
+
// ENRICH without changing existing logic
|
|
116
|
+
conversation.runningProcess = {
|
|
117
|
+
pid: matchingProcess.pid,
|
|
118
|
+
startTime: matchingProcess.startTime,
|
|
119
|
+
workingDir: matchingProcess.workingDir,
|
|
120
|
+
hasActiveCommand: true
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// Only change status if not already marked as active by existing logic
|
|
124
|
+
if (conversation.status !== 'active') {
|
|
125
|
+
conversation.status = 'active';
|
|
126
|
+
conversation.statusReason = 'running_process';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Recalculate conversation state with process information
|
|
130
|
+
const conversationFile = path.join(claudeDir, conversation.fileName);
|
|
131
|
+
try {
|
|
132
|
+
const content = await fs.readFile(conversationFile, 'utf8');
|
|
133
|
+
const parsedMessages = content.split('\n')
|
|
134
|
+
.filter(line => line.trim())
|
|
135
|
+
.map(line => JSON.parse(line));
|
|
136
|
+
|
|
137
|
+
const stats = await fs.stat(conversationFile);
|
|
138
|
+
conversation.conversationState = stateCalculator.determineConversationState(
|
|
139
|
+
parsedMessages,
|
|
140
|
+
stats.mtime,
|
|
141
|
+
conversation.runningProcess
|
|
142
|
+
);
|
|
143
|
+
} catch (error) {
|
|
144
|
+
// If we can't read the file, keep the existing state
|
|
145
|
+
}
|
|
146
|
+
} else {
|
|
147
|
+
conversation.runningProcess = null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Disable orphan process detection to reduce noise
|
|
152
|
+
const orphanProcesses = [];
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
conversations,
|
|
156
|
+
orphanProcesses,
|
|
157
|
+
activeProcessCount: runningProcesses.length
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
} catch (error) {
|
|
161
|
+
// Silently handle process detection errors
|
|
162
|
+
return {
|
|
163
|
+
conversations,
|
|
164
|
+
orphanProcesses: [],
|
|
165
|
+
activeProcessCount: 0
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Get cached processes without triggering detection
|
|
172
|
+
* @returns {Array} Cached process data or empty array
|
|
173
|
+
*/
|
|
174
|
+
getCachedProcesses() {
|
|
175
|
+
const now = Date.now();
|
|
176
|
+
if (this.processCache.data && (now - this.processCache.timestamp) < this.processCache.ttl) {
|
|
177
|
+
return this.processCache.data;
|
|
178
|
+
}
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Clear process cache to force fresh detection
|
|
184
|
+
*/
|
|
185
|
+
clearCache() {
|
|
186
|
+
this.processCache = {
|
|
187
|
+
data: null,
|
|
188
|
+
timestamp: 0,
|
|
189
|
+
ttl: 500
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Check if there are any active Claude processes
|
|
195
|
+
* @returns {Promise<boolean>} True if there are active processes
|
|
196
|
+
*/
|
|
197
|
+
async hasActiveProcesses() {
|
|
198
|
+
const processes = await this.detectRunningClaudeProcesses();
|
|
199
|
+
return processes.length > 0;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Get process statistics
|
|
204
|
+
* @returns {Promise<Object>} Process statistics
|
|
205
|
+
*/
|
|
206
|
+
async getProcessStats() {
|
|
207
|
+
const processes = await this.detectRunningClaudeProcesses();
|
|
208
|
+
return {
|
|
209
|
+
total: processes.length,
|
|
210
|
+
withKnownWorkingDir: processes.filter(p => p.workingDir !== 'unknown').length,
|
|
211
|
+
withUnknownWorkingDir: processes.filter(p => p.workingDir === 'unknown').length,
|
|
212
|
+
processes: processes
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Match a specific process to conversations
|
|
218
|
+
* @param {Object} process - Process object
|
|
219
|
+
* @param {Array} conversations - Array of conversation objects
|
|
220
|
+
* @returns {Object|null} Matched conversation or null
|
|
221
|
+
*/
|
|
222
|
+
matchProcessToConversation(process, conversations) {
|
|
223
|
+
// Direct match by working directory or project name
|
|
224
|
+
let match = conversations.find(conv =>
|
|
225
|
+
process.workingDir.includes(conv.project) ||
|
|
226
|
+
process.command.includes(conv.project)
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
// Fallback for unknown working directories
|
|
230
|
+
if (!match && process.workingDir === 'unknown' && conversations.length > 0) {
|
|
231
|
+
// Match to most recently modified conversation
|
|
232
|
+
const sorted = [...conversations].sort((a, b) =>
|
|
233
|
+
new Date(b.lastModified) - new Date(a.lastModified)
|
|
234
|
+
);
|
|
235
|
+
match = sorted[0];
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return match;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
module.exports = ProcessDetector;
|