Skip to main content
Glama
fileChangeDetector.ts•8.94 kB
/** * File Change Detector for the Code-Map Generator tool. * This file contains the FileChangeDetector class for detecting changes in files. */ import crypto from 'crypto'; import logger from '../../../logger.js'; import { FileContentManager, FileMetadata } from './fileContentManager.js'; /** * Interface for file change detection options. */ export interface FileChangeDetectionOptions { /** * Whether to use file hashes for change detection. * Default is true. */ useFileHashes?: boolean; /** * Whether to use file metadata (size, modification time) for change detection. * Default is true. */ useFileMetadata?: boolean; /** * Maximum number of file hashes to cache. * Default is 10000. */ maxCachedHashes?: number; /** * Maximum age of cached hashes in milliseconds. * Default is 24 hours. */ maxHashAge?: number; } /** * Interface for file change detection result. */ export interface FileChangeDetectionResult { /** * Whether the file has changed. */ changed: boolean; /** * The reason for the change detection result. */ reason: string; /** * The file metadata. */ metadata?: FileMetadata; /** * The file hash. */ hash?: string; } /** * Interface for file hash cache entry. */ interface FileHashCacheEntry { /** * The file hash. */ hash: string; /** * The timestamp when the hash was calculated. */ timestamp: number; /** * The file metadata when the hash was calculated. */ metadata: FileMetadata; } /** * Detects changes in files using file metadata and content hashes. */ export class FileChangeDetector { private fileContentManager: FileContentManager; private options: Required<FileChangeDetectionOptions>; private fileHashCache: Map<string, FileHashCacheEntry> = new Map(); private lastProcessedFiles: Set<string> = new Set(); /** * Default options for file change detection. */ private static readonly DEFAULT_OPTIONS: Required<FileChangeDetectionOptions> = { useFileHashes: true, useFileMetadata: true, maxCachedHashes: 10000, maxHashAge: 24 * 60 * 60 * 1000 // 24 hours }; /** * Creates a new FileChangeDetector instance. * @param fileContentManager The file content manager to use * @param options The change detection options */ constructor(fileContentManager: FileContentManager, options: FileChangeDetectionOptions = {}) { this.fileContentManager = fileContentManager; this.options = { ...FileChangeDetector.DEFAULT_OPTIONS, ...options }; logger.debug(`FileChangeDetector created with options: ${JSON.stringify(this.options)}`); } /** * Detects if a file has changed. * @param filePath The file path * @param baseDir The base directory for resolving relative paths * @returns A promise that resolves to a change detection result */ public async detectChange(filePath: string, baseDir: string): Promise<FileChangeDetectionResult> { try { // Get file metadata const metadata = await this.fileContentManager.getMetadata(filePath); if (!metadata) { return { changed: true, reason: 'File metadata not found' }; } // Check if the file is in the cache const cacheEntry = this.fileHashCache.get(filePath); // If metadata-based detection is enabled, check if the file has changed based on metadata 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 metadata has changed and we're not using file hashes, return true if (!this.options.useFileHashes) { return { changed: true, reason: 'File metadata changed', metadata }; } } // If we're using file hashes, calculate the hash and compare if (this.options.useFileHashes) { // Get file content const content = await this.fileContentManager.getContent(filePath, baseDir); // Calculate hash const hash = this.calculateHash(content); // Check if the hash has changed if (cacheEntry && hash === cacheEntry.hash) { // Update the cache entry timestamp this.fileHashCache.set(filePath, { hash, timestamp: Date.now(), metadata }); return { changed: false, reason: 'File content unchanged', metadata, hash }; } // Update the cache this.fileHashCache.set(filePath, { hash, timestamp: Date.now(), metadata }); // Prune the cache if it's too large this.pruneCache(); return { changed: true, reason: cacheEntry ? 'File content changed' : 'File not in cache', metadata, hash }; } // If we're not using file hashes, return true for new files return { changed: true, reason: 'New file', metadata }; } catch (error) { logger.error({ err: error, filePath }, 'Error detecting file change'); // If there's an error, assume the file has changed return { changed: true, reason: `Error: ${error instanceof Error ? error.message : String(error)}` }; } } /** * Checks if file metadata has changed. * @param current The current file metadata * @param previous The previous file metadata * @returns True if the metadata has changed, false otherwise */ private hasMetadataChanged(current: FileMetadata, previous: FileMetadata): boolean { // Check if the size has changed if (current.size !== previous.size) { return true; } // Check if the modification time has changed // Handle both Date objects and timestamps (numbers) 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; } /** * Calculates a hash for file content. * @param content The file content * @returns The hash */ private calculateHash(content: string): string { return crypto.createHash('md5').update(content).digest('hex'); } /** * Prunes the file hash cache if it's too large. */ private pruneCache(): void { if (this.fileHashCache.size <= this.options.maxCachedHashes) { return; } // Remove the oldest entries const entriesToRemove = this.fileHashCache.size - this.options.maxCachedHashes; const entries = Array.from(this.fileHashCache.entries()); // Sort by timestamp (oldest first) entries.sort((a, b) => a[1].timestamp - b[1].timestamp); // Remove the oldest entries for (let i = 0; i < entriesToRemove; i++) { this.fileHashCache.delete(entries[i][0]); } logger.debug(`Pruned ${entriesToRemove} entries from file hash cache`); } /** * Cleans up old entries from the file hash cache. */ public cleanupCache(): void { 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`); } } /** * Gets the number of entries in the file hash cache. * @returns The number of entries */ public getCacheSize(): number { return this.fileHashCache.size; } /** * Clears the file hash cache. */ public clearCache(): void { this.fileHashCache.clear(); logger.debug('Cleared file hash cache'); } /** * Sets the list of processed files for the current run. * @param filePaths The file paths */ public setProcessedFiles(filePaths: string[]): void { this.lastProcessedFiles = new Set(filePaths); } /** * Gets the list of processed files from the last run. * @returns The file paths */ public getLastProcessedFiles(): string[] { return Array.from(this.lastProcessedFiles); } /** * Checks if a file was processed in the last run. * @param filePath The file path * @returns True if the file was processed, false otherwise */ public wasFileProcessed(filePath: string): boolean { return this.lastProcessedFiles.has(filePath); } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/freshtechbro/vibe-coder-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server