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.

417 lines (416 loc) 15.7 kB
import fs from 'fs/promises'; import fsSync from 'fs'; import path from 'path'; import crypto from 'crypto'; import logger from '../../../logger.js'; export class FileCache { name; cacheDir; metadataPath; metadata; options; stats; initialized = false; pruneTimer = null; static DEFAULT_OPTIONS = { maxEntries: 10000, maxAge: 24 * 60 * 60 * 1000, validateOnGet: true, pruneOnStartup: true, pruneInterval: 60 * 60 * 1000, serialize: JSON.stringify, deserialize: JSON.parse, }; constructor(options) { this.name = options.name; this.cacheDir = path.resolve(options.cacheDir); this.metadataPath = path.join(this.cacheDir, `${this.name}-metadata.json`); this.options = { ...FileCache.DEFAULT_OPTIONS, name: options.name, cacheDir: this.cacheDir, maxEntries: options.maxEntries ?? FileCache.DEFAULT_OPTIONS.maxEntries, maxAge: options.maxAge ?? FileCache.DEFAULT_OPTIONS.maxAge, validateOnGet: options.validateOnGet ?? FileCache.DEFAULT_OPTIONS.validateOnGet, pruneOnStartup: options.pruneOnStartup ?? FileCache.DEFAULT_OPTIONS.pruneOnStartup, pruneInterval: options.pruneInterval ?? FileCache.DEFAULT_OPTIONS.pruneInterval, serialize: options.serialize ?? FileCache.DEFAULT_OPTIONS.serialize, deserialize: options.deserialize ?? FileCache.DEFAULT_OPTIONS.deserialize, }; this.metadata = { name: this.name, size: 0, createdAt: Date.now(), lastUpdated: Date.now(), keys: [], maxEntries: this.options.maxEntries, maxAge: this.options.maxAge, sizeInBytes: 0 }; this.stats = { name: this.name, size: 0, hits: 0, misses: 0, hitRatio: 0, createdAt: Date.now(), lastUpdated: Date.now(), sizeInBytes: 0, }; } async init() { if (this.initialized) { return; } try { await this.createCacheDirectory(); this.metadata = { name: this.name, size: 0, createdAt: Date.now(), lastUpdated: Date.now(), keys: [], maxEntries: this.options.maxEntries || 10000, maxAge: this.options.maxAge || 24 * 60 * 60 * 1000, sizeInBytes: 0, }; try { const metadataContent = await fs.readFile(this.metadataPath, 'utf-8'); this.metadata = JSON.parse(metadataContent); logger.debug(`Loaded cache metadata for ${this.name} with ${this.metadata.size} entries`); } catch (error) { if (error instanceof Error && 'code' in error && error.code === 'ENOENT') { await this.saveMetadata(); logger.debug(`Created new cache metadata for ${this.name}`); } else { logger.warn({ err: error, metadataPath: this.metadataPath }, `Error loading cache metadata for ${this.name}, using default metadata`); await this.saveMetadata(); } } this.initialized = true; if (this.options.pruneOnStartup) { try { await this.prune(); } catch (pruneError) { logger.warn({ err: pruneError }, `Error pruning cache ${this.name} during initialization`); } } if (this.options.pruneInterval > 0) { this.pruneTimer = setInterval(() => { this.prune().catch(error => { logger.error({ err: error }, `Error pruning cache ${this.name}`); }); }, this.options.pruneInterval); } logger.info(`Cache ${this.name} initialized successfully at ${this.cacheDir}`); } catch (error) { logger.error({ err: error, cacheDir: this.cacheDir }, `Error initializing cache ${this.name}`); throw error; } } async createCacheDirectory() { const maxRetries = 3; let retryCount = 0; let lastError = null; while (retryCount < maxRetries) { try { await fs.mkdir(this.cacheDir, { recursive: true }); const testFilePath = path.join(this.cacheDir, `.write-test-${Date.now()}.tmp`); await fs.writeFile(testFilePath, 'test'); await fs.unlink(testFilePath); logger.debug(`Cache directory ${this.cacheDir} created and verified as writable`); return; } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); logger.warn({ err: error, cacheDir: this.cacheDir, retry: retryCount + 1, maxRetries }, `Error creating cache directory, retrying (${retryCount + 1}/${maxRetries})...`); await new Promise(resolve => setTimeout(resolve, 1000)); retryCount++; } } throw new Error(`Failed to create cache directory after ${maxRetries} attempts: ${lastError?.message}`); } ensureInitialized() { if (!this.initialized) { logger.warn(`Cache ${this.name} is not initialized. Call init() first.`); return false; } return true; } async saveMetadata() { try { await fs.writeFile(this.metadataPath, JSON.stringify(this.metadata, null, 2), 'utf-8'); } catch (error) { logger.error({ err: error, metadataPath: this.metadataPath }, `Error saving cache metadata for ${this.name}`); throw error; } } hashKey(key) { return crypto.createHash('md5').update(key).digest('hex'); } getEntryPath(key) { const hashedKey = this.hashKey(key); return path.join(this.cacheDir, `${hashedKey}.json`); } async get(key) { if (!this.ensureInitialized()) { return undefined; } const entryPath = this.getEntryPath(key); try { try { await fs.access(entryPath, fsSync.constants.R_OK); } catch { this.stats.misses++; return undefined; } const entryContent = await fs.readFile(entryPath, 'utf-8'); const entry = this.options.deserialize(entryContent); if (this.options.validateOnGet && entry.expiry < Date.now()) { try { await this.delete(key); } catch (error) { logger.warn({ err: error, key }, `Error deleting expired cache entry for ${key}`); } this.stats.misses++; return undefined; } this.stats.hits++; this.stats.hitRatio = this.stats.hits / (this.stats.hits + this.stats.misses); return entry.value; } catch (error) { logger.error({ err: error, key, entryPath }, `Error getting cache entry for ${key}`); this.stats.misses++; return undefined; } } async set(key, value, ttl) { if (!this.ensureInitialized()) { try { await this.init(); } catch (error) { logger.error({ err: error }, `Failed to initialize cache ${this.name} during set operation`); throw new Error(`Cannot set cache entry - initialization failed: ${error instanceof Error ? error.message : String(error)}`); } } const entryPath = this.getEntryPath(key); const now = Date.now(); const expiry = now + (ttl ?? this.options.maxAge); const entry = { key, value, timestamp: now, expiry, }; try { try { await fs.mkdir(path.dirname(entryPath), { recursive: true }); } catch { } await fs.writeFile(entryPath, this.options.serialize(entry), 'utf-8'); if (!this.metadata.keys.includes(key)) { this.metadata.keys.push(key); this.metadata.size++; } this.metadata.lastUpdated = now; try { await this.saveMetadata(); } catch (error) { logger.warn({ err: error }, `Error saving metadata after setting cache entry for ${key}`); } this.stats.size = this.metadata.size; this.stats.lastUpdated = now; if (this.metadata.size > this.options.maxEntries) { try { await this.prune(); } catch (error) { logger.warn({ err: error }, `Error pruning cache after setting entry for ${key}`); } } } catch (error) { logger.error({ err: error, key, entryPath }, `Error setting cache entry for ${key}`); throw error; } } async has(key) { if (!this.ensureInitialized()) { return false; } const entryPath = this.getEntryPath(key); try { await fs.access(entryPath, fsSync.constants.R_OK); if (this.options.validateOnGet) { try { const entryContent = await fs.readFile(entryPath, 'utf-8'); const entry = this.options.deserialize(entryContent); if (entry.expiry < Date.now()) { try { await this.delete(key); } catch (error) { logger.warn({ err: error, key }, `Error deleting expired cache entry for ${key}`); } return false; } } catch (error) { logger.warn({ err: error, key }, `Error validating cache entry for ${key}`); return false; } } return true; } catch { return false; } } async delete(key) { if (!this.ensureInitialized()) { return false; } const entryPath = this.getEntryPath(key); try { await fs.unlink(entryPath); const keyIndex = this.metadata.keys.indexOf(key); if (keyIndex !== -1) { this.metadata.keys.splice(keyIndex, 1); this.metadata.size--; this.metadata.lastUpdated = Date.now(); try { await this.saveMetadata(); } catch (error) { logger.warn({ err: error }, `Error saving metadata after deleting cache entry for ${key}`); } this.stats.size = this.metadata.size; this.stats.lastUpdated = Date.now(); } return true; } catch (error) { if (error instanceof Error && 'code' in error && error.code === 'ENOENT') { return false; } logger.error({ err: error, key, entryPath }, `Error deleting cache entry for ${key}`); throw error; } } async clear() { if (!this.ensureInitialized()) { try { await this.init(); } catch (error) { logger.error({ err: error }, `Failed to initialize cache ${this.name} during clear operation`); throw new Error(`Cannot clear cache - initialization failed: ${error instanceof Error ? error.message : String(error)}`); } } try { for (const key of this.metadata.keys) { const entryPath = this.getEntryPath(key); try { await fs.unlink(entryPath); } catch (error) { if (error instanceof Error && 'code' in error && error.code !== 'ENOENT') { logger.warn({ err: error, key, entryPath }, `Error deleting cache entry for ${key}`); } } } this.metadata.keys = []; this.metadata.size = 0; this.metadata.lastUpdated = Date.now(); try { await this.saveMetadata(); } catch (error) { logger.warn({ err: error }, `Error saving metadata after clearing cache ${this.name}`); } this.stats.size = 0; this.stats.lastUpdated = Date.now(); logger.info(`Cleared cache ${this.name}`); } catch (error) { logger.error({ err: error }, `Error clearing cache ${this.name}`); throw error; } } async prune() { if (!this.initialized) { logger.warn(`Cannot prune cache ${this.name} - not initialized`); return 0; } const now = Date.now(); const prunedKeys = []; try { for (const key of this.metadata.keys) { const entryPath = this.getEntryPath(key); try { const entryContent = await fs.readFile(entryPath, 'utf-8'); const entry = this.options.deserialize(entryContent); if (entry.expiry < now) { await fs.unlink(entryPath); prunedKeys.push(key); } } catch (error) { if (error instanceof Error && 'code' in error && error.code === 'ENOENT') { prunedKeys.push(key); } else { logger.warn({ err: error, key, entryPath }, `Error checking cache entry for ${key}`); } } } for (const key of prunedKeys) { const keyIndex = this.metadata.keys.indexOf(key); if (keyIndex !== -1) { this.metadata.keys.splice(keyIndex, 1); } } this.metadata.size = this.metadata.keys.length; this.metadata.lastUpdated = now; try { await this.saveMetadata(); } catch (error) { logger.warn({ err: error }, `Error saving metadata after pruning cache ${this.name}`); } this.stats.size = this.metadata.size; this.stats.lastUpdated = now; logger.debug(`Pruned ${prunedKeys.length} entries from cache ${this.name}`); return prunedKeys.length; } catch (error) { logger.error({ err: error }, `Error pruning cache ${this.name}`); throw error; } } getStats() { return { ...this.stats }; } close() { if (this.pruneTimer) { clearInterval(this.pruneTimer); this.pruneTimer = null; } } }