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.

174 lines (173 loc) 7.34 kB
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; }