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
JavaScript
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);
}
}