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

372 lines 13.7 kB
/** * Bidirectional Sync Manager - Gerenciador de sincronização bidirecional * Coordena a sincronização entre .memory-bank local e MEMORY_BANK_ROOT */ import * as fs from 'fs'; import * as path from 'path'; import { createFileWatcher } from './file-watcher.js'; import { errorHandler } from './error-handler.js'; export class BidirectionalSyncManager { config; fileWatcher; isInitialized = false; isSyncing = false; syncQueue = []; conflictQueue = []; // Arquivos permitidos para sincronização allowedFiles = ['memory.md', 'task.md', 'planning.md']; constructor(config) { this.config = config; this.fileWatcher = createFileWatcher(config.projectName, config.projectPath, config.memoryBankRoot); // Configura listeners do FileWatcher this.setupFileWatcherListeners(); } /** * Inicializa o sistema de sincronização */ async initialize() { if (this.isInitialized) { return; } try { // console.log('[SyncManager] Inicializando sincronização bidirecional...'); // Realiza sincronização inicial await this.performInitialSync(); // Inicia monitoramento se auto-sync estiver habilitado if (this.config.enableAutoSync) { await this.fileWatcher.start(); } this.isInitialized = true; // console.log('[SyncManager] Sincronização inicializada com sucesso'); } catch (error) { const handledError = errorHandler.handleError(error, { operation: 'sync-initialize', projectName: this.config.projectName, timestamp: Date.now() }); throw handledError; } } /** * Para o sistema de sincronização */ async stop() { if (!this.isInitialized) { return; } await this.fileWatcher.stop(); this.isInitialized = false; // console.log('[SyncManager] Sincronização parada'); } /** * Verifica se um arquivo deve ser sincronizado */ shouldSyncFile(filePath) { const fileName = path.basename(filePath); const normalizedPath = filePath.replace(/\\/g, '/'); // Ignora completamente pasta reasoning e qualquer arquivo dentro dela if (normalizedPath.includes('/reasoning') || normalizedPath.includes('reasoning/') || normalizedPath.includes('reasoning\\') || normalizedPath.endsWith('reasoning') || fileName === 'reasoning') { return false; } // Sincroniza apenas os arquivos .md principais return this.allowedFiles.includes(fileName); } /** * Força sincronização manual */ async forceSync() { if (this.isSyncing) { // console.log('[SyncManager] Sincronização já em andamento'); return; } try { this.isSyncing = true; await this.performFullSync(); } finally { this.isSyncing = false; } } /** * Configura listeners do FileWatcher */ setupFileWatcherListeners() { this.fileWatcher.on('change', (event) => { this.handleFileChange(event); }); this.fileWatcher.on('error', (error) => { console.error('[SyncManager] Erro no FileWatcher:', error); }); } /** * Realiza sincronização inicial */ async performInitialSync() { // console.log('[SyncManager] Realizando sincronização inicial...'); const localPath = path.join(this.config.projectPath, '.memory-bank'); const remotePath = path.join(this.config.memoryBankRoot, this.config.projectName); // Verifica se as pastas existem const localExists = await this.pathExists(localPath); const remoteExists = await this.pathExists(remotePath); if (!localExists && !remoteExists) { // Nenhuma pasta existe - cria ambas await fs.promises.mkdir(localPath, { recursive: true }); await fs.promises.mkdir(remotePath, { recursive: true }); // console.log('[SyncManager] Pastas criadas: local e remota'); } else if (!localExists && remoteExists) { // Só remota existe - sincroniza para local (Regra 1) await this.syncFromRemoteToLocal(); // console.log('[SyncManager] Sincronizado: remoto → local'); } else if (localExists && !remoteExists) { // Só local existe - sincroniza para remoto await this.syncFromLocalToRemote(); // console.log('[SyncManager] Sincronizado: local → remoto'); } else { // Ambas existem - compara e sincroniza diferenças await this.performFullSync(); // console.log('[SyncManager] Sincronização completa realizada'); } } /** * Realiza sincronização completa comparando ambas as pastas */ async performFullSync() { const comparison = await this.fileWatcher.compareDirectories(); // Sincroniza arquivos que existem apenas localmente for (const file of comparison.localOnly) { await this.copyFileLocalToRemote(file); } // Sincroniza arquivos que existem apenas remotamente for (const file of comparison.remoteOnly) { await this.copyFileRemoteToLocal(file); } // Verifica arquivos comuns para conflitos de timestamp for (const file of comparison.common) { await this.syncCommonFile(file); } } /** * Processa mudanças de arquivo detectadas pelo watcher */ async handleFileChange(event) { // Verifica se o arquivo deve ser sincronizado if (!this.shouldSyncFile(event.filePath)) { return; // Ignora arquivos não permitidos } if (this.isSyncing) { // Adiciona à fila se já estiver sincronizando this.syncQueue.push(event); return; } try { this.isSyncing = true; await this.processSingleFileChange(event); // Processa fila se houver itens while (this.syncQueue.length > 0) { const queuedEvent = this.syncQueue.shift(); if (this.shouldSyncFile(queuedEvent.filePath)) { await this.processSingleFileChange(queuedEvent); } } } finally { this.isSyncing = false; } } /** * Processa mudança de um único arquivo */ async processSingleFileChange(event) { const relativePath = this.getRelativePath(event.filePath, event.source); // console.log(`[SyncManager] Processando: ${event.type} - ${relativePath} (${event.source})`); switch (event.type) { case 'created': case 'modified': if (event.source === 'local') { await this.copyFileLocalToRemote(relativePath); } else { await this.copyFileRemoteToLocal(relativePath); } break; case 'deleted': if (event.source === 'local') { await this.deleteRemoteFile(relativePath); } else { // Regra 2: Se arquivo remoto foi apagado, apaga do local também await this.deleteLocalFile(relativePath); } break; } } /** * Copia arquivo do local para remoto */ async copyFileLocalToRemote(relativePath) { const localPath = path.join(this.config.projectPath, '.memory-bank', relativePath); const remotePath = path.join(this.config.memoryBankRoot, this.config.projectName, relativePath); await this.copyFile(localPath, remotePath); } /** * Copia arquivo do remoto para local */ async copyFileRemoteToLocal(relativePath) { const remotePath = path.join(this.config.memoryBankRoot, this.config.projectName, relativePath); const localPath = path.join(this.config.projectPath, '.memory-bank', relativePath); await this.copyFile(remotePath, localPath); } /** * Copia arquivo entre locais */ async copyFile(sourcePath, destPath) { try { // Garante que o diretório de destino existe await fs.promises.mkdir(path.dirname(destPath), { recursive: true }); // Copia o arquivo await fs.promises.copyFile(sourcePath, destPath); // console.log(`[SyncManager] Copiado: ${path.basename(sourcePath)}`); } catch (error) { console.error(`[SyncManager] Erro ao copiar ${sourcePath} → ${destPath}:`, error); } } /** * Sincroniza do remoto para local (Regra 1) */ async syncFromRemoteToLocal() { const remotePath = path.join(this.config.memoryBankRoot, this.config.projectName); const localPath = path.join(this.config.projectPath, '.memory-bank'); await this.copyDirectory(remotePath, localPath); } /** * Sincroniza do local para remoto */ async syncFromLocalToRemote() { const localPath = path.join(this.config.projectPath, '.memory-bank'); const remotePath = path.join(this.config.memoryBankRoot, this.config.projectName); await this.copyDirectory(localPath, remotePath); } /** * Copia diretório recursivamente (apenas arquivos permitidos) */ async copyDirectory(sourcePath, destPath) { try { await fs.promises.mkdir(destPath, { recursive: true }); const entries = await fs.promises.readdir(sourcePath, { withFileTypes: true }); for (const entry of entries) { const srcPath = path.join(sourcePath, entry.name); const dstPath = path.join(destPath, entry.name); // Verifica se deve sincronizar este arquivo/pasta if (!this.shouldSyncFile(srcPath)) { continue; // Pula arquivos/pastas não permitidos } if (entry.isDirectory()) { await this.copyDirectory(srcPath, dstPath); } else { await fs.promises.copyFile(srcPath, dstPath); } } } catch (error) { console.error(`[SyncManager] Erro ao copiar diretório ${sourcePath}:`, error); } } /** * Sincroniza arquivo comum verificando timestamps */ async syncCommonFile(relativePath) { const localPath = path.join(this.config.projectPath, '.memory-bank', relativePath); const remotePath = path.join(this.config.memoryBankRoot, this.config.projectName, relativePath); try { const localStats = await fs.promises.stat(localPath); const remoteStats = await fs.promises.stat(remotePath); // Compara timestamps (Regra 3) if (localStats.mtime > remoteStats.mtime) { // Local é mais recente await this.copyFile(localPath, remotePath); } else if (remoteStats.mtime > localStats.mtime) { // Remoto é mais recente await this.copyFile(remotePath, localPath); } // Se iguais, não faz nada } catch (error) { console.error(`[SyncManager] Erro ao sincronizar arquivo comum ${relativePath}:`, error); } } /** * Apaga arquivo remoto */ async deleteRemoteFile(relativePath) { const remotePath = path.join(this.config.memoryBankRoot, this.config.projectName, relativePath); await this.deleteFile(remotePath); } /** * Apaga arquivo local (Regra 2) */ async deleteLocalFile(relativePath) { const localPath = path.join(this.config.projectPath, '.memory-bank', relativePath); await this.deleteFile(localPath); } /** * Apaga arquivo */ async deleteFile(filePath) { try { await fs.promises.unlink(filePath); // console.log(`[SyncManager] Apagado: ${path.basename(filePath)}`); } catch (error) { // Ignora erro se arquivo não existir if (error.code !== 'ENOENT') { console.error(`[SyncManager] Erro ao apagar ${filePath}:`, error); } } } /** * Obtém caminho relativo baseado na origem */ getRelativePath(fullPath, source) { const basePath = source === 'local' ? path.join(this.config.projectPath, '.memory-bank') : path.join(this.config.memoryBankRoot, this.config.projectName); return path.relative(basePath, fullPath); } /** * Verifica se caminho existe */ async pathExists(filePath) { try { await fs.promises.access(filePath); return true; } catch { return false; } } /** * Obtém estatísticas de sincronização */ getStats() { return { isInitialized: this.isInitialized, isSyncing: this.isSyncing, queueSize: this.syncQueue.length, conflictCount: this.conflictQueue.length }; } } // Factory function export function createBidirectionalSync(config) { return new BidirectionalSyncManager(config); } //# sourceMappingURL=bidirectional-sync.js.map