@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
JavaScript
/**
* 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