vibe-coder-mcp
Version:
Production-ready MCP server with complete agent integration, multi-transport support, and comprehensive development automation tools for AI-assisted workflows.
174 lines (173 loc) • 7.34 kB
JavaScript
import fs from 'fs/promises';
import fsSync from 'fs';
import path from 'path';
import crypto from 'crypto';
import logger from '../../logger.js';
import { FileCache } from './cache/fileCache.js';
import { getCacheDirectory } from './directoryUtils.js';
export class IncrementalProcessor {
config;
fileMetadataCache = null;
previouslyProcessedFiles = new Set();
currentProcessedFiles = new Set();
allowedDir;
cacheDir;
constructor(config, allowedDir, cacheDir) {
this.config = config;
this.allowedDir = allowedDir;
this.cacheDir = cacheDir;
}
async initialize() {
this.fileMetadataCache = new FileCache({
name: 'file-metadata',
cacheDir: path.join(this.cacheDir, 'file-metadata'),
maxEntries: this.config.maxCachedHashes || 10000,
maxAge: this.config.maxHashAge || 24 * 60 * 60 * 1000,
});
await this.loadPreviouslyProcessedFiles();
logger.info('Incremental processor initialized');
}
async loadPreviouslyProcessedFiles() {
const previousFilesListPath = this.config.previousFilesListPath || path.join(this.cacheDir, 'processed-files.json');
try {
if (fsSync.existsSync(previousFilesListPath)) {
const fileContent = await fs.readFile(previousFilesListPath, 'utf-8');
const filesList = JSON.parse(fileContent);
this.previouslyProcessedFiles = new Set(filesList);
logger.info(`Loaded ${filesList.length} previously processed files from ${previousFilesListPath}`);
}
else {
logger.info(`No previously processed files list found at ${previousFilesListPath}`);
}
}
catch (error) {
logger.warn(`Error loading previously processed files: ${error instanceof Error ? error.message : String(error)}`);
}
}
async saveProcessedFilesList() {
if (!this.config.saveProcessedFilesList) {
logger.debug('Skipping saving processed files list (disabled in config)');
return;
}
const previousFilesListPath = this.config.previousFilesListPath || path.join(this.cacheDir, 'processed-files.json');
try {
const filesList = Array.from(this.currentProcessedFiles);
await fs.writeFile(previousFilesListPath, JSON.stringify(filesList), 'utf-8');
logger.info(`Saved ${filesList.length} processed files to ${previousFilesListPath}`);
}
catch (error) {
logger.warn(`Error saving processed files list: ${error instanceof Error ? error.message : String(error)}`);
}
}
async hasFileChanged(filePath) {
if (!this.fileMetadataCache) {
logger.warn('File metadata cache not initialized');
return true;
}
if (!this.previouslyProcessedFiles.has(filePath)) {
logger.debug(`File ${filePath} was not processed before`);
return true;
}
try {
const stats = await fs.stat(filePath);
const cachedMetadata = await this.fileMetadataCache.get(filePath);
if (!cachedMetadata) {
logger.debug(`No cached metadata for ${filePath}`);
return true;
}
if (stats.size !== cachedMetadata.size) {
logger.debug(`File size changed for ${filePath}: ${cachedMetadata.size} -> ${stats.size}`);
return true;
}
if (stats.mtimeMs > cachedMetadata.mtime) {
logger.debug(`File modification time changed for ${filePath}: ${new Date(cachedMetadata.mtime).toISOString()} -> ${new Date(stats.mtimeMs).toISOString()}`);
return true;
}
if (this.config.useFileHashes) {
const currentHash = await this.computeFileHash(filePath);
if (currentHash !== cachedMetadata.hash) {
logger.debug(`File hash changed for ${filePath}: ${cachedMetadata.hash} -> ${currentHash}`);
return true;
}
}
logger.debug(`File ${filePath} hasn't changed since last run`);
return false;
}
catch (error) {
logger.warn(`Error checking if file ${filePath} has changed: ${error instanceof Error ? error.message : String(error)}`);
return true;
}
}
async computeFileHash(filePath) {
try {
const fileContent = await fs.readFile(filePath);
return crypto.createHash('md5').update(fileContent).digest('hex');
}
catch (error) {
logger.warn(`Error computing hash for ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
return '';
}
}
async updateFileMetadata(filePath) {
if (!this.fileMetadataCache) {
logger.warn('File metadata cache not initialized');
return;
}
try {
const stats = await fs.stat(filePath);
const metadata = {
filePath,
size: stats.size,
mtime: stats.mtimeMs,
processedAt: Date.now(),
};
if (this.config.useFileHashes) {
metadata.hash = await this.computeFileHash(filePath);
}
await this.fileMetadataCache.set(filePath, metadata);
this.currentProcessedFiles.add(filePath);
}
catch (error) {
logger.warn(`Error updating metadata for ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
}
}
async filterChangedFiles(filePaths) {
if (!this.config.useFileHashes && !this.config.useFileMetadata) {
logger.info('Incremental processing is enabled but neither file hashes nor file metadata are used for change detection. Processing all files.');
return filePaths;
}
const changedFiles = [];
for (const filePath of filePaths) {
if (await this.hasFileChanged(filePath)) {
changedFiles.push(filePath);
}
}
logger.info(`Filtered ${filePaths.length} files to ${changedFiles.length} changed files`);
return changedFiles;
}
async close() {
await this.saveProcessedFilesList();
if (this.fileMetadataCache) {
await this.fileMetadataCache.close();
}
logger.info('Incremental processor closed');
}
}
export async function createIncrementalProcessor(config) {
if (!config.processing?.incremental) {
logger.info('Incremental processing is disabled');
return null;
}
if (!config.processing.incrementalConfig) {
logger.warn('Incremental processing is enabled but no configuration is provided');
return null;
}
const cacheDir = config.cache?.cacheDir || getCacheDirectory(config);
if (!cacheDir) {
logger.warn('Incremental processing is enabled but no cache directory is available');
return null;
}
const processor = new IncrementalProcessor(config.processing.incrementalConfig, config.allowedMappingDirectory, cacheDir);
await processor.initialize();
return processor;
}