claude-code-templates 1.8.0 → 1.8.1

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.
@@ -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
100
+ const dataRefreshInterval = setInterval(async () => {
101
+ console.log(chalk.blue('⏱️ Periodic data refresh...'));
102
+ await this.triggerDataRefresh();
103
+ }, 30000); // Every 30 seconds
104
+
105
+ this.intervals.push(dataRefreshInterval);
106
+
107
+ // More frequent updates for active processes (every 10 seconds)
108
+ const processRefreshInterval = setInterval(async () => {
109
+ if (this.processRefreshCallback) {
110
+ await this.processRefreshCallback();
111
+ }
112
+ }, 10000);
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;