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