import chokidar from "chokidar"; import path from "path"; import os from "os"; import { promises as fsPromises } from "fs"; import { logger } from "@/shared/utils/logger.js"; import { getSessions } from "@/modules/sessions/sessions.service.js"; import { processClaudeSessionFile } from "@/modules/providers/claude/claude.session-parser.js"; import { processCodexSessionFile } from "@/modules/providers/codex/codex.session-parser.js"; import { processGeminiSessionFile } from "@/modules/providers/gemini/gemini.session-parser.js"; import { processCursorSessionFile } from "@/modules/providers/cursor/cursor.session-parser.js"; import { sessionsDb } from "@/shared/database/repositories/sessions.db.js"; import { LLMProvider } from "@/shared/types/app.js"; let projectsWatchers: any[] = []; // File system watchers for provider project/session folders const PROVIDER_WATCH_PATHS: { 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"), }, ]; const WATCHER_IGNORED_PATTERNS = [ "**/node_modules/**", "**/.git/**", "**/dist/**", "**/build/**", "**/*.tmp", "**/*.swp", "**/.DS_Store", ]; type EventType = "add" | "change"; const onUpdate = async ( eventType: EventType, filePath: string, provider: LLMProvider, ) => { try { console.log("[eventType] detected: ", eventType, " filePath: ", filePath, " provider: ", provider); switch (eventType) { case "add": case "change": { let sessionId: string | null = null; let workspacePath: string | null = null; let sessionName = `Untitled ${provider} Session`; switch (provider) { case "claude": { const result = await processClaudeSessionFile(filePath); if (result) { sessionId = result.sessionId; workspacePath = result.workspacePath; sessionName = result.sessionName || sessionName; } break; } case "codex": { const result = await processCodexSessionFile(filePath); if (result) { sessionId = result.sessionId; workspacePath = result.workspacePath; sessionName = result.sessionName || sessionName; } break; } case "gemini": { const result = await processGeminiSessionFile(filePath); if (result) { sessionId = result.sessionId; workspacePath = result.workspacePath; sessionName = result.sessionName || sessionName; } break; } case "cursor": { const result = await processCursorSessionFile(filePath); if (result) { sessionId = result.sessionId; workspacePath = result.workspacePath; sessionName = result.sessionName || sessionName; } break; } } if (sessionId && workspacePath) { sessionsDb.createSession(sessionId, provider, workspacePath, sessionName); } break; } } } catch (error: any) { logger.error( `[ERROR] Failed to handle ${provider} file change for ${filePath}:`, error, ); } }; // Setup file system watchers for Claude, Cursor, and Codex project/session folders export async function initializeWatcher() { logger.info("Setting up project watchers for providers..."); await getSessions(); for (const { provider, rootPath } of PROVIDER_WATCH_PATHS) { try { // chokidar v4 emits ENOENT via the "error" event for missing roots and will not auto-recover. // Ensure provider folders exist before creating the watcher so watching stays active. await fsPromises.mkdir(rootPath, { recursive: true }); logger.info(`Setting up watcher for ${provider} at: ${rootPath}`); 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: 6, // Reasonable depth limit usePolling: true, // Use polling to fix Windows fs.watch buffering/batching issues. It now stops relying on the OS's native file-system events and instead manually checks the files for changes at a set interval. interval: 2000, // Poll every 2000ms binaryInterval: 6000, // We set a high amount because checking large binary files for changes using polling is much more CPU-intensive than checking small text files. // Removed awaitWriteFinish to prevent delays when LLM streams to the file }); // Set up event listeners watcher .on("add", (filePath) => onUpdate("add", filePath, provider)) .on("change", (filePath) => onUpdate("change", filePath, provider), ) .on("error", (error: any) => { logger.error(`[ERROR] ${provider} watcher error: ${error.message}`); }) .on("ready", () => { }); projectsWatchers.push(watcher); } catch (error: any) { logger.error( `[ERROR] Failed to setup ${provider} watcher for ${rootPath}:`, error, ); } } }