From 8bc3c17793c719f8640d0a14914fcdd5cf15b988 Mon Sep 17 00:00:00 2001 From: Haileyesus Date: Wed, 11 Feb 2026 17:10:56 +0300 Subject: [PATCH] refactor(project-watcher): add codex and cursor file watchers --- server/index.js | 188 +++++++++++++++++++++++++++--------------------- 1 file changed, 108 insertions(+), 80 deletions(-) diff --git a/server/index.js b/server/index.js index 8a2604d..bc9667c 100755 --- a/server/index.js +++ b/server/index.js @@ -63,8 +63,24 @@ import { initializeDatabase } from './database/db.js'; import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js'; import { IS_PLATFORM } from './constants/config.js'; -// File system watcher for projects folder -let projectsWatcher = null; +// File system watchers for provider project/session folders +const PROVIDER_WATCH_PATHS = [ + { provider: 'claude', rootPath: path.join(os.homedir(), '.claude', 'projects') }, + { provider: 'cursor', rootPath: path.join(os.homedir(), '.cursor', 'chats') }, + { provider: 'codex', rootPath: path.join(os.homedir(), '.codex', 'sessions') } +]; +const WATCHER_IGNORED_PATTERNS = [ + '**/node_modules/**', + '**/.git/**', + '**/dist/**', + '**/build/**', + '**/*.tmp', + '**/*.swp', + '**/.DS_Store' +]; +const WATCHER_DEBOUNCE_MS = 300; +let projectsWatchers = []; +let projectsWatcherDebounceTimer = null; const connectedClients = new Set(); let isGetProjectsRunning = false; // Flag to prevent reentrant calls @@ -81,94 +97,106 @@ function broadcastProgress(progress) { }); } -// Setup file system watcher for Claude projects folder using chokidar +// Setup file system watchers for Claude, Cursor, and Codex project/session folders async function setupProjectsWatcher() { const chokidar = (await import('chokidar')).default; - const claudeProjectsPath = path.join(os.homedir(), '.claude', 'projects'); - if (projectsWatcher) { - projectsWatcher.close(); + if (projectsWatcherDebounceTimer) { + clearTimeout(projectsWatcherDebounceTimer); + projectsWatcherDebounceTimer = null; } - try { - // Initialize chokidar watcher with optimized settings - projectsWatcher = chokidar.watch(claudeProjectsPath, { - ignored: [ - '**/node_modules/**', - '**/.git/**', - '**/dist/**', - '**/build/**', - '**/*.tmp', - '**/*.swp', - '**/.DS_Store' - ], - persistent: true, - ignoreInitial: true, // Don't fire events for existing files on startup - followSymlinks: false, - depth: 10, // Reasonable depth limit - awaitWriteFinish: { - stabilityThreshold: 100, // Wait 100ms for file to stabilize - pollInterval: 50 + await Promise.all( + projectsWatchers.map(async (watcher) => { + try { + await watcher.close(); + } catch (error) { + console.error('[WARN] Failed to close watcher:', error); } - }); + }) + ); + projectsWatchers = []; - // Debounce function to prevent excessive notifications - let debounceTimer; - const debouncedUpdate = async (eventType, filePath) => { - clearTimeout(debounceTimer); - debounceTimer = setTimeout(async () => { - // Prevent reentrant calls - if (isGetProjectsRunning) { - return; + const debouncedUpdate = (eventType, filePath, provider, rootPath) => { + if (projectsWatcherDebounceTimer) { + clearTimeout(projectsWatcherDebounceTimer); + } + + projectsWatcherDebounceTimer = setTimeout(async () => { + // Prevent reentrant calls + if (isGetProjectsRunning) { + return; + } + + try { + isGetProjectsRunning = true; + + // Clear project directory cache when files change + clearProjectDirectoryCache(); + + // Get updated projects list + const updatedProjects = await getProjects(broadcastProgress); + + // Notify all connected clients about the project changes + const updateMessage = JSON.stringify({ + type: 'projects_updated', + projects: updatedProjects, + timestamp: new Date().toISOString(), + changeType: eventType, + changedFile: path.relative(rootPath, filePath), + watchProvider: provider + }); + + connectedClients.forEach(client => { + if (client.readyState === WebSocket.OPEN) { + client.send(updateMessage); + } + }); + + } catch (error) { + console.error('[ERROR] Error handling project changes:', error); + } finally { + isGetProjectsRunning = false; + } + }, WATCHER_DEBOUNCE_MS); + }; + + for (const { provider, rootPath } of PROVIDER_WATCH_PATHS) { + try { + // Initialize chokidar watcher with optimized settings + const watcher = chokidar.watch(rootPath, { + ignored: WATCHER_IGNORED_PATTERNS, + persistent: true, + ignoreInitial: true, // Don't fire events for existing files on startup + followSymlinks: false, + depth: 10, // Reasonable depth limit + awaitWriteFinish: { + stabilityThreshold: 100, // Wait 100ms for file to stabilize + pollInterval: 50 } - - try { - isGetProjectsRunning = true; - - // Clear project directory cache when files change - clearProjectDirectoryCache(); - - // Get updated projects list - const updatedProjects = await getProjects(broadcastProgress); - - // Notify all connected clients about the project changes - const updateMessage = JSON.stringify({ - type: 'projects_updated', - projects: updatedProjects, - timestamp: new Date().toISOString(), - changeType: eventType, - changedFile: path.relative(claudeProjectsPath, filePath) - }); - - connectedClients.forEach(client => { - if (client.readyState === WebSocket.OPEN) { - client.send(updateMessage); - } - }); - - } catch (error) { - console.error('[ERROR] Error handling project changes:', error); - } finally { - isGetProjectsRunning = false; - } - }, 300); // 300ms debounce (slightly faster than before) - }; - - // Set up event listeners - projectsWatcher - .on('add', (filePath) => debouncedUpdate('add', filePath)) - .on('change', (filePath) => debouncedUpdate('change', filePath)) - .on('unlink', (filePath) => debouncedUpdate('unlink', filePath)) - .on('addDir', (dirPath) => debouncedUpdate('addDir', dirPath)) - .on('unlinkDir', (dirPath) => debouncedUpdate('unlinkDir', dirPath)) - .on('error', (error) => { - console.error('[ERROR] Chokidar watcher error:', error); - }) - .on('ready', () => { }); - } catch (error) { - console.error('[ERROR] Failed to setup projects watcher:', error); + // Set up event listeners + watcher + .on('add', (filePath) => debouncedUpdate('add', filePath, provider, rootPath)) + .on('change', (filePath) => debouncedUpdate('change', filePath, provider, rootPath)) + .on('unlink', (filePath) => debouncedUpdate('unlink', filePath, provider, rootPath)) + .on('addDir', (dirPath) => debouncedUpdate('addDir', dirPath, provider, rootPath)) + .on('unlinkDir', (dirPath) => debouncedUpdate('unlinkDir', dirPath, provider, rootPath)) + .on('error', (error) => { + console.error(`[ERROR] ${provider} watcher error:`, error); + }) + .on('ready', () => { + }); + + projectsWatchers.push(watcher); + } catch (error) { + console.error(`[ERROR] Failed to setup ${provider} watcher for ${rootPath}:`, error); + } + } + + if (projectsWatchers.length === 0) { + console.error('[ERROR] Failed to setup any provider watchers'); } }