UNPKG

vibe-coder-mcp

Version:

Production-ready MCP server with complete agent integration, multi-transport support, and comprehensive development automation tools for AI-assisted workflows.

154 lines (153 loc) 5.42 kB
import crypto from 'crypto'; import logger from '../../../logger.js'; export class FileChangeDetector { fileContentManager; options; fileHashCache = new Map(); lastProcessedFiles = new Set(); static DEFAULT_OPTIONS = { useFileHashes: true, useFileMetadata: true, maxCachedHashes: 10000, maxHashAge: 24 * 60 * 60 * 1000 }; constructor(fileContentManager, options = {}) { this.fileContentManager = fileContentManager; this.options = { ...FileChangeDetector.DEFAULT_OPTIONS, ...options }; logger.debug(`FileChangeDetector created with options: ${JSON.stringify(this.options)}`); } async detectChange(filePath, baseDir) { try { const metadata = await this.fileContentManager.getMetadata(filePath); if (!metadata) { return { changed: true, reason: 'File metadata not found' }; } const cacheEntry = this.fileHashCache.get(filePath); if (this.options.useFileMetadata && cacheEntry) { const metadataChanged = this.hasMetadataChanged(metadata, cacheEntry.metadata); if (!metadataChanged) { return { changed: false, reason: 'File metadata unchanged', metadata, hash: cacheEntry.hash }; } if (!this.options.useFileHashes) { return { changed: true, reason: 'File metadata changed', metadata }; } } if (this.options.useFileHashes) { const content = await this.fileContentManager.getContent(filePath, baseDir); const hash = this.calculateHash(content); if (cacheEntry && hash === cacheEntry.hash) { this.fileHashCache.set(filePath, { hash, timestamp: Date.now(), metadata }); return { changed: false, reason: 'File content unchanged', metadata, hash }; } this.fileHashCache.set(filePath, { hash, timestamp: Date.now(), metadata }); this.pruneCache(); return { changed: true, reason: cacheEntry ? 'File content changed' : 'File not in cache', metadata, hash }; } return { changed: true, reason: 'New file', metadata }; } catch (error) { logger.error({ err: error, filePath }, 'Error detecting file change'); return { changed: true, reason: `Error: ${error instanceof Error ? error.message : String(error)}` }; } } hasMetadataChanged(current, previous) { if (current.size !== previous.size) { return true; } const currentMtime = typeof current.mtime === 'number' ? current.mtime : Number(current.mtime); const previousMtime = typeof previous.mtime === 'number' ? previous.mtime : Number(previous.mtime); if (currentMtime !== previousMtime) { return true; } return false; } calculateHash(content) { return crypto.createHash('md5').update(content).digest('hex'); } pruneCache() { if (this.fileHashCache.size <= this.options.maxCachedHashes) { return; } const entriesToRemove = this.fileHashCache.size - this.options.maxCachedHashes; const entries = Array.from(this.fileHashCache.entries()); entries.sort((a, b) => a[1].timestamp - b[1].timestamp); for (let i = 0; i < entriesToRemove; i++) { this.fileHashCache.delete(entries[i][0]); } logger.debug(`Pruned ${entriesToRemove} entries from file hash cache`); } cleanupCache() { const now = Date.now(); const maxAge = this.options.maxHashAge; let removedCount = 0; for (const [filePath, entry] of this.fileHashCache.entries()) { if (now - entry.timestamp > maxAge) { this.fileHashCache.delete(filePath); removedCount++; } } if (removedCount > 0) { logger.debug(`Removed ${removedCount} expired entries from file hash cache`); } } getCacheSize() { return this.fileHashCache.size; } clearCache() { this.fileHashCache.clear(); logger.debug('Cleared file hash cache'); } setProcessedFiles(filePaths) { this.lastProcessedFiles = new Set(filePaths); } getLastProcessedFiles() { return Array.from(this.lastProcessedFiles); } wasFileProcessed(filePath) { return this.lastProcessedFiles.has(filePath); } }