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