Files
claudecodeui/server/src/modules/sessions/sessions.watcher.ts

163 lines
6.3 KiB
TypeScript

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,
);
}
}
}