import os from 'node:os'; import path from 'node:path'; import { promises as fsPromises } from 'node:fs'; import chokidar, { type FSWatcher } from 'chokidar'; import { sessionSynchronizerService } from '@/modules/providers/services/session-synchronizer.service.js'; import type { LLMProvider } from '@/shared/types.js'; type WatcherEventType = 'add' | 'change'; const PROVIDER_WATCH_PATHS: Array<{ provider: LLMProvider; rootPath: string }> = [ { 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'), }, { provider: 'gemini', rootPath: path.join(os.homedir(), '.gemini', 'sessions'), }, { provider: 'gemini', rootPath: path.join(os.homedir(), '.gemini', 'tmp'), }, ]; const WATCHER_IGNORED_PATTERNS = [ '**/node_modules/**', '**/.git/**', '**/dist/**', '**/build/**', '**/*.tmp', '**/*.swp', '**/.DS_Store', ]; const watchers: FSWatcher[] = []; /** * Filters watcher events to provider-specific session artifact file types. */ function isWatcherTargetFile(provider: LLMProvider, filePath: string): boolean { if (provider === 'gemini') { return filePath.endsWith('.json'); } return filePath.endsWith('.jsonl'); } /** * Handles file watcher updates and triggers provider file-level synchronization. */ async function onUpdate( eventType: WatcherEventType, filePath: string, provider: LLMProvider ): Promise { if (!isWatcherTargetFile(provider, filePath)) { return; } try { const result = await sessionSynchronizerService.synchronizeProviderFile(provider, filePath); console.log(`Session watcher sync complete for provider "${provider}" after ${eventType}`, { filePath, indexed: result.indexed, }); } catch (error) { const message = error instanceof Error ? error.message : String(error); console.error(`Session watcher sync failed for provider "${provider}"`, { eventType, filePath, error: message, }); } } /** * Starts provider filesystem watchers and performs initial DB synchronization. */ export async function initializeSessionsWatcher(): Promise { console.log('Setting up session watchers'); const initialSync = await sessionSynchronizerService.synchronizeSessions(); console.log('Initial session synchronization complete', { processedByProvider: initialSync.processedByProvider, failures: initialSync.failures, }); for (const { provider, rootPath } of PROVIDER_WATCH_PATHS) { try { await fsPromises.mkdir(rootPath, { recursive: true }); const watcher = chokidar.watch(rootPath, { ignored: WATCHER_IGNORED_PATTERNS, persistent: true, ignoreInitial: true, followSymlinks: false, depth: 6, usePolling: true, interval: 2_000, binaryInterval: 6_000, }); watcher .on('add', (filePath: string) => { void onUpdate('add', filePath, provider); }) .on('change', (filePath: string) => { void onUpdate('change', filePath, provider); }) .on('error', (error: unknown) => { const message = error instanceof Error ? error.message : String(error); console.error(`Session watcher error for provider "${provider}"`, { error: message }); }); watchers.push(watcher); } catch (error) { const message = error instanceof Error ? error.message : String(error); console.error(`Failed to initialize session watcher for provider "${provider}"`, { rootPath, error: message, }); } } } /** * Stops all active provider session watchers. */ export async function closeSessionsWatcher(): Promise { await Promise.all( watchers.map(async (watcher) => { try { await watcher.close(); } catch (error) { const message = error instanceof Error ? error.message : String(error); console.error('Failed to close session watcher', { error: message }); } }) ); watchers.length = 0; }