UNPKG

@andrebuzeli/advanced-memory-markdown-mcp

Version:

Advanced Memory Bank MCP v3.1.5 - Sistema avançado de gerenciamento de memória com isolamento de projetos por IDE, sincronização sob demanda, backup a cada 30min, apenas arquivos .md principais sincronizados, pasta reasoning temporária com limpeza automát

236 lines 8.31 kB
/** * File Watcher - Sistema de monitoramento de arquivos para sincronização * Detecta mudanças em .memory-bank local e MEMORY_BANK_ROOT */ import * as fs from 'fs'; import * as path from 'path'; import { EventEmitter } from 'events'; export class FileWatcher extends EventEmitter { localWatcher = null; remoteWatcher = null; debounceTimers = new Map(); config; isActive = false; constructor(config) { super(); this.config = config; } /** * Inicia o monitoramento de ambas as pastas */ async start() { if (this.isActive) { return; } try { // Garante que as pastas existem await this.ensureDirectories(); // Inicia watchers await this.startLocalWatcher(); await this.startRemoteWatcher(); this.isActive = true; // console.log('[FileWatcher] Monitoramento iniciado'); // console.log(`[FileWatcher] Local: ${this.config.localPath}`); // console.log(`[FileWatcher] Remote: ${this.config.remotePath}`); } catch (error) { console.error('[FileWatcher] Erro ao iniciar:', error); throw error; } } /** * Para o monitoramento */ async stop() { if (!this.isActive) { return; } // Para watchers if (this.localWatcher) { this.localWatcher.close(); this.localWatcher = null; } if (this.remoteWatcher) { this.remoteWatcher.close(); this.remoteWatcher = null; } // Limpa timers this.debounceTimers.forEach(timer => clearTimeout(timer)); this.debounceTimers.clear(); this.isActive = false; // console.log('[FileWatcher] Monitoramento parado'); } /** * Verifica se está ativo */ isWatching() { return this.isActive; } /** * Garante que os diretórios existem */ async ensureDirectories() { try { await fs.promises.mkdir(this.config.localPath, { recursive: true }); await fs.promises.mkdir(this.config.remotePath, { recursive: true }); } catch (error) { console.error('[FileWatcher] Erro ao criar diretórios:', error); throw error; } } /** * Inicia watcher local (.memory-bank) */ async startLocalWatcher() { this.localWatcher = fs.watch(this.config.localPath, { recursive: true }, (eventType, filename) => { if (filename) { this.handleFileChange('local', eventType, filename); } }); this.localWatcher.on('error', (error) => { console.error('[FileWatcher] Erro no watcher local:', error); this.emit('error', error); }); } /** * Inicia watcher remoto (MEMORY_BANK_ROOT) */ async startRemoteWatcher() { this.remoteWatcher = fs.watch(this.config.remotePath, { recursive: true }, (eventType, filename) => { if (filename) { this.handleFileChange('remote', eventType, filename); } }); this.remoteWatcher.on('error', (error) => { console.error('[FileWatcher] Erro no watcher remoto:', error); this.emit('error', error); }); } /** * Processa mudanças de arquivo com debounce */ handleFileChange(source, eventType, filename) { // Ignora arquivos temporários e padrões excluídos if (this.shouldIgnoreFile(filename)) { return; } const basePath = source === 'local' ? this.config.localPath : this.config.remotePath; const fullPath = path.join(basePath, filename); const debounceKey = `${source}:${fullPath}`; // Cancela timer anterior se existir const existingTimer = this.debounceTimers.get(debounceKey); if (existingTimer) { clearTimeout(existingTimer); } // Cria novo timer com debounce const timer = setTimeout(async () => { this.debounceTimers.delete(debounceKey); await this.processFileChange(source, eventType, fullPath, filename); }, this.config.debounceMs); this.debounceTimers.set(debounceKey, timer); } /** * Processa a mudança de arquivo após debounce */ async processFileChange(source, eventType, fullPath, filename) { try { let changeType; let isDirectory = false; // Determina o tipo de mudança try { const stats = await fs.promises.stat(fullPath); isDirectory = stats.isDirectory(); changeType = eventType === 'rename' ? 'created' : 'modified'; } catch (error) { // Arquivo não existe = foi deletado changeType = 'deleted'; } const event = { type: changeType, filePath: fullPath, isDirectory, timestamp: Date.now(), source }; // console.log(`[FileWatcher] ${source.toUpperCase()}: ${changeType} - ${filename}`); this.emit('change', event); } catch (error) { console.error('[FileWatcher] Erro ao processar mudança:', error); this.emit('error', error); } } /** * Verifica se deve ignorar o arquivo */ shouldIgnoreFile(filename) { // Ignora arquivos temporários if (filename.startsWith('.') && filename !== '.memory-bank') { return true; } // Ignora arquivos de sistema if (filename.includes('~') || filename.includes('.tmp')) { return true; } // Verifica padrões de exclusão return this.config.excludePatterns.some(pattern => { return filename.includes(pattern); }); } /** * Lista arquivos em um diretório recursivamente */ async listFiles(dirPath) { const files = []; try { const entries = await fs.promises.readdir(dirPath, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dirPath, entry.name); if (entry.isDirectory()) { const subFiles = await this.listFiles(fullPath); files.push(...subFiles); } else if (!this.shouldIgnoreFile(entry.name)) { files.push(fullPath); } } } catch (error) { console.error(`[FileWatcher] Erro ao listar arquivos em ${dirPath}:`, error); } return files; } /** * Compara dois diretórios e retorna diferenças */ async compareDirectories() { const localFiles = await this.listFiles(this.config.localPath); const remoteFiles = await this.listFiles(this.config.remotePath); // Normaliza caminhos para comparação const normalizeForComparison = (files, basePath) => { return files.map(file => path.relative(basePath, file).replace(/\\/g, '/')); }; const localRelative = normalizeForComparison(localFiles, this.config.localPath); const remoteRelative = normalizeForComparison(remoteFiles, this.config.remotePath); const localSet = new Set(localRelative); const remoteSet = new Set(remoteRelative); return { localOnly: localRelative.filter(file => !remoteSet.has(file)), remoteOnly: remoteRelative.filter(file => !localSet.has(file)), common: localRelative.filter(file => remoteSet.has(file)) }; } } // Factory function para criar watcher export function createFileWatcher(projectName, projectPath, memoryBankRoot) { const config = { localPath: path.join(projectPath, '.memory-bank'), remotePath: path.join(memoryBankRoot, projectName), debounceMs: 500, // 500ms debounce excludePatterns: ['.tmp', '~', '.lock', '.swp'] }; return new FileWatcher(config); } //# sourceMappingURL=file-watcher.js.map