mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-05-01 18:28:38 +00:00
177 lines
4.9 KiB
TypeScript
177 lines
4.9 KiB
TypeScript
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';
|
|
import { getProjectsWithSessions } from '@/modules/projects/index.js';
|
|
import { connectedClients } from '@/index.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[] = [];
|
|
const WS_OPEN_STATE = 1;
|
|
|
|
/**
|
|
* 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<void> {
|
|
if (!isWatcherTargetFile(provider, filePath)) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const result = await sessionSynchronizerService.synchronizeProviderFile(provider, filePath);
|
|
|
|
// Get updated projects list
|
|
const updatedProjects = await getProjectsWithSessions();
|
|
|
|
// Notify all connected clients about the project changes
|
|
const updateMessage = JSON.stringify({
|
|
type: 'projects_updated',
|
|
projects: updatedProjects,
|
|
timestamp: new Date().toISOString(),
|
|
changeType: eventType,
|
|
updatedSessionId: result.sessionId ?? undefined,
|
|
watchProvider: provider
|
|
});
|
|
|
|
connectedClients.forEach(client => {
|
|
if (client.readyState === WS_OPEN_STATE) {
|
|
client.send(updateMessage);
|
|
}
|
|
});
|
|
|
|
|
|
console.log(`Session watcher sync complete for provider "${provider}" after ${eventType}`, {
|
|
filePath,
|
|
indexed: result.indexed,
|
|
sessionId: result.sessionId,
|
|
});
|
|
} 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<void> {
|
|
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<void> {
|
|
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;
|
|
}
|