vibe-coder-mcp
Version:
Production-ready MCP server with complete agent integration, multi-transport support, and comprehensive development automation tools for AI-assisted workflows.
191 lines (190 loc) • 7.99 kB
JavaScript
import { promises as fs } from 'fs';
import path from 'path';
import logger from '../../../logger.js';
export class CodemapCacheManager {
static async findRecentCodemap(maxAgeMinutes, outputDir) {
const baseOutputDir = outputDir ||
process.env.VIBE_CODER_OUTPUT_DIR ||
path.join(process.cwd(), 'VibeCoderOutput');
const codemapDir = path.join(baseOutputDir, 'code-map-generator');
const maxAgeMs = maxAgeMinutes * 60 * 1000;
const now = Date.now();
try {
await fs.access(codemapDir);
const files = await fs.readdir(codemapDir);
const codemapFiles = files
.filter(f => f.endsWith('.md') && f.includes('code-map'))
.map(f => ({
name: f,
path: path.join(codemapDir, f),
timestamp: this.extractTimestampFromFilename(f)
}))
.filter(f => f.timestamp !== null)
.filter(f => (now - f.timestamp.getTime()) <= maxAgeMs)
.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
if (codemapFiles.length > 0) {
const latestCodemap = codemapFiles[0];
const ageMs = now - latestCodemap.timestamp.getTime();
logger.info({
path: latestCodemap.path,
ageMinutes: Math.round(ageMs / (60 * 1000)),
maxAgeMinutes,
fileCount: codemapFiles.length
}, 'Found recent codemap in cache');
const content = await this.readCodemapWithRetry(latestCodemap.path);
return {
content,
path: latestCodemap.path,
timestamp: latestCodemap.timestamp,
fromCache: true
};
}
else {
logger.debug({
codemapDir,
maxAgeMinutes,
totalFiles: files.length,
codemapFiles: files.filter(f => f.endsWith('.md') && f.includes('code-map')).length
}, 'No recent codemap found in cache');
}
}
catch (error) {
if (error.code === 'ENOENT') {
logger.debug({ codemapDir }, 'Codemap directory does not exist');
}
else {
logger.warn({
error: error instanceof Error ? error.message : 'Unknown error',
codemapDir
}, 'Failed to check for cached codemap');
}
}
return null;
}
static extractTimestampFromFilename(filename) {
try {
const match = filename.match(/^(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z)/);
if (match) {
const timestampStr = match[1];
const parts = timestampStr.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2})-(\d{2})-(\d{2})-(\d{3})Z$/);
if (!parts) {
logger.warn({ filename, timestampStr }, 'Invalid timestamp format in filename');
return null;
}
const [, year, month, day, hour, minute, second, millisecond] = parts;
const isoString = `${year}-${month}-${day}T${hour}:${minute}:${second}.${millisecond}Z`;
const date = new Date(isoString);
if (isNaN(date.getTime())) {
logger.warn({ filename, timestampStr, isoString }, 'Invalid timestamp in filename');
return null;
}
return date;
}
else {
logger.debug({ filename }, 'No timestamp pattern found in filename');
return null;
}
}
catch (error) {
logger.warn({
filename,
error: error instanceof Error ? error.message : 'Unknown error'
}, 'Failed to extract timestamp from filename');
return null;
}
}
static async readCodemapWithRetry(filePath, maxRetries = 3) {
const retryDelayMs = 100;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
await fs.access(filePath, fs.constants.R_OK);
const content = await fs.readFile(filePath, 'utf-8');
if (content.length > 0 && content.includes('# Code Map')) {
logger.debug({
filePath,
attempt,
contentLength: content.length
}, 'Successfully read cached codemap');
return content;
}
else {
throw new Error('File appears to be incomplete or corrupted');
}
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
logger.warn({
filePath,
attempt,
maxRetries,
error: errorMessage
}, 'Failed to read cached codemap file');
if (attempt === maxRetries) {
throw new Error(`Failed to read codemap after ${maxRetries} attempts: ${errorMessage}`);
}
await new Promise(resolve => setTimeout(resolve, retryDelayMs * attempt));
}
}
throw new Error('Unexpected end of retry loop');
}
static async getCacheStats(outputDir) {
const baseOutputDir = outputDir ||
process.env.VIBE_CODER_OUTPUT_DIR ||
path.join(process.cwd(), 'VibeCoderOutput');
const codemapDir = path.join(baseOutputDir, 'code-map-generator');
try {
const files = await fs.readdir(codemapDir);
const codemapFiles = files.filter(f => f.endsWith('.md') && f.includes('code-map'));
if (codemapFiles.length === 0) {
return {
totalCodemaps: 0,
oldestTimestamp: null,
newestTimestamp: null,
totalSizeBytes: 0,
averageAgeMinutes: 0
};
}
const now = Date.now();
let totalSizeBytes = 0;
let totalAgeMs = 0;
let oldestTimestamp = null;
let newestTimestamp = null;
for (const file of codemapFiles) {
const filePath = path.join(codemapDir, file);
const stats = await fs.stat(filePath);
const timestamp = this.extractTimestampFromFilename(file);
totalSizeBytes += stats.size;
if (timestamp) {
const ageMs = now - timestamp.getTime();
totalAgeMs += ageMs;
if (!oldestTimestamp || timestamp < oldestTimestamp) {
oldestTimestamp = timestamp;
}
if (!newestTimestamp || timestamp > newestTimestamp) {
newestTimestamp = timestamp;
}
}
}
return {
totalCodemaps: codemapFiles.length,
oldestTimestamp,
newestTimestamp,
totalSizeBytes,
averageAgeMinutes: Math.round(totalAgeMs / (codemapFiles.length * 60 * 1000))
};
}
catch (error) {
logger.warn({
error: error instanceof Error ? error.message : 'Unknown error',
codemapDir
}, 'Failed to get cache statistics');
return {
totalCodemaps: 0,
oldestTimestamp: null,
newestTimestamp: null,
totalSizeBytes: 0,
averageAgeMinutes: 0
};
}
}
}