claude-expert-workflow-mcp
Version:
Production-ready MCP server for AI-powered product development consultation through specialized expert roles. Enterprise-grade with memory management, monitoring, and Claude Code integration.
388 lines • 14.7 kB
JavaScript
import fs from 'fs/promises';
import path from 'path';
import crypto from 'crypto';
import { logger } from '../utils/logger';
export class FileBasedStorage {
constructor(dataDir = './data') {
this.dataDir = path.resolve(dataDir);
this.conversationsDir = path.join(this.dataDir, 'conversations');
this.workflowsDir = path.join(this.dataDir, 'workflows');
this.backupsDir = path.join(this.dataDir, 'backups');
this.lockFile = path.join(this.dataDir, '.lock');
}
/**
* Initialize storage directories and ensure data integrity
*/
async initialize() {
try {
await this.ensureDirectories();
await this.verifyDataIntegrity();
logger.info('File-based storage initialized successfully');
}
catch (error) {
logger.error('Failed to initialize file-based storage:', error);
throw error;
}
}
// Conversation management
async saveConversation(conversation) {
const filePath = path.join(this.conversationsDir, `${conversation.id}.json`);
await this.writeJsonFile(filePath, conversation);
logger.debug(`Saved conversation ${conversation.id}`);
}
async loadConversation(id) {
const filePath = path.join(this.conversationsDir, `${id}.json`);
try {
return await this.readJsonFile(filePath);
}
catch (error) {
if (error.code === 'ENOENT') {
return undefined;
}
throw error;
}
}
async deleteConversation(id) {
const filePath = path.join(this.conversationsDir, `${id}.json`);
try {
await fs.unlink(filePath);
logger.debug(`Deleted conversation ${id}`);
return true;
}
catch (error) {
if (error.code === 'ENOENT') {
return false;
}
logger.error(`Failed to delete conversation ${id}:`, error);
throw error;
}
}
async listConversations() {
try {
const files = await fs.readdir(this.conversationsDir);
return files
.filter(file => file.endsWith('.json'))
.map(file => file.replace('.json', ''));
}
catch (error) {
logger.error('Failed to list conversations:', error);
return [];
}
}
// Workflow management
async saveWorkflow(workflow) {
const filePath = path.join(this.workflowsDir, `${workflow.id}.json`);
await this.writeJsonFile(filePath, workflow);
logger.debug(`Saved workflow ${workflow.id}`);
}
async loadWorkflow(id) {
const filePath = path.join(this.workflowsDir, `${id}.json`);
try {
return await this.readJsonFile(filePath);
}
catch (error) {
if (error.code === 'ENOENT') {
return undefined;
}
throw error;
}
}
async deleteWorkflow(id) {
const filePath = path.join(this.workflowsDir, `${id}.json`);
try {
await fs.unlink(filePath);
logger.debug(`Deleted workflow ${id}`);
return true;
}
catch (error) {
if (error.code === 'ENOENT') {
return false;
}
logger.error(`Failed to delete workflow ${id}:`, error);
throw error;
}
}
async listWorkflows() {
try {
const files = await fs.readdir(this.workflowsDir);
return files
.filter(file => file.endsWith('.json'))
.map(file => file.replace('.json', ''));
}
catch (error) {
logger.error('Failed to list workflows:', error);
return [];
}
}
// Backup and recovery
async createBackup() {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const backupPath = path.join(this.backupsDir, `backup-${timestamp}`);
try {
await fs.mkdir(backupPath, { recursive: true });
// Copy conversations
const conversationBackupDir = path.join(backupPath, 'conversations');
await fs.mkdir(conversationBackupDir, { recursive: true });
await this.copyDirectory(this.conversationsDir, conversationBackupDir);
// Copy workflows
const workflowBackupDir = path.join(backupPath, 'workflows');
await fs.mkdir(workflowBackupDir, { recursive: true });
await this.copyDirectory(this.workflowsDir, workflowBackupDir);
// Create metadata
const metadata = {
timestamp: new Date(),
version: '1.0.0',
conversationCount: (await this.listConversations()).length,
workflowCount: (await this.listWorkflows()).length,
checksum: await this.calculateDirectoryChecksum(backupPath)
};
await fs.writeFile(path.join(backupPath, 'metadata.json'), JSON.stringify(metadata, null, 2), 'utf8');
logger.info(`Created backup at ${backupPath}`);
return backupPath;
}
catch (error) {
logger.error('Failed to create backup:', error);
throw error;
}
}
async restoreFromBackup(backupPath) {
try {
// Verify backup integrity
const metadataPath = path.join(backupPath, 'metadata.json');
const metadata = await this.readJsonFile(metadataPath);
const currentChecksum = await this.calculateDirectoryChecksum(backupPath);
if (currentChecksum !== metadata.checksum) {
throw new Error('Backup integrity check failed - checksums do not match');
}
// Create data directories backup before restore
const tempBackup = await this.createBackup();
try {
// Clear current data
await this.clearDirectory(this.conversationsDir);
await this.clearDirectory(this.workflowsDir);
// Restore data
await this.copyDirectory(path.join(backupPath, 'conversations'), this.conversationsDir);
await this.copyDirectory(path.join(backupPath, 'workflows'), this.workflowsDir);
logger.info(`Restored data from backup ${backupPath}`);
return true;
}
catch (error) {
// Rollback on failure
logger.error('Restore failed, rolling back:', error);
await this.restoreFromBackup(tempBackup);
throw error;
}
}
catch (error) {
logger.error('Failed to restore from backup:', error);
return false;
}
}
// Health and maintenance
async checkHealth() {
const errors = [];
let status = 'healthy';
try {
// Check directories exist
await Promise.all([
fs.access(this.conversationsDir),
fs.access(this.workflowsDir),
fs.access(this.backupsDir)
]);
}
catch (error) {
errors.push(`Storage directories not accessible: ${error}`);
status = 'failed';
}
// Get storage statistics
const conversationCount = (await this.listConversations()).length;
const workflowCount = (await this.listWorkflows()).length;
const storageUsed = await this.calculateStorageUsage();
// Find last backup
let lastBackup = null;
try {
const backups = await fs.readdir(this.backupsDir);
if (backups.length > 0) {
const latestBackup = backups
.filter(name => name.startsWith('backup-'))
.sort()
.pop();
if (latestBackup) {
const metadataPath = path.join(this.backupsDir, latestBackup, 'metadata.json');
const metadata = await this.readJsonFile(metadataPath);
lastBackup = new Date(metadata.timestamp);
}
}
}
catch (error) {
errors.push(`Failed to check backup status: ${error}`);
if (status === 'healthy')
status = 'degraded';
}
return {
status,
lastBackup,
totalConversations: conversationCount,
totalWorkflows: workflowCount,
storageUsed,
errors
};
}
async cleanup() {
try {
// Clean up old backups (keep only last 10)
const backups = await fs.readdir(this.backupsDir);
const backupDirs = backups
.filter(name => name.startsWith('backup-'))
.sort()
.reverse();
if (backupDirs.length > 10) {
const toDelete = backupDirs.slice(10);
for (const backup of toDelete) {
await this.removeDirectory(path.join(this.backupsDir, backup));
logger.debug(`Cleaned up old backup: ${backup}`);
}
}
logger.info('Storage cleanup completed');
}
catch (error) {
logger.error('Failed to cleanup storage:', error);
throw error;
}
}
// Private helper methods
async ensureDirectories() {
const dirs = [this.dataDir, this.conversationsDir, this.workflowsDir, this.backupsDir];
for (const dir of dirs) {
await fs.mkdir(dir, { recursive: true });
}
}
async verifyDataIntegrity() {
// Check for corrupted JSON files
const conversationFiles = await fs.readdir(this.conversationsDir);
const workflowFiles = await fs.readdir(this.workflowsDir);
for (const file of conversationFiles.filter(f => f.endsWith('.json'))) {
try {
await this.readJsonFile(path.join(this.conversationsDir, file));
}
catch (error) {
logger.warn(`Corrupted conversation file detected: ${file}`, error);
// Move corrupted file to quarantine
await this.quarantineFile(path.join(this.conversationsDir, file));
}
}
for (const file of workflowFiles.filter(f => f.endsWith('.json'))) {
try {
await this.readJsonFile(path.join(this.workflowsDir, file));
}
catch (error) {
logger.warn(`Corrupted workflow file detected: ${file}`, error);
// Move corrupted file to quarantine
await this.quarantineFile(path.join(this.workflowsDir, file));
}
}
}
async quarantineFile(filePath) {
const quarantineDir = path.join(this.dataDir, 'quarantine');
await fs.mkdir(quarantineDir, { recursive: true });
const fileName = path.basename(filePath);
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const quarantinePath = path.join(quarantineDir, `${timestamp}-${fileName}`);
await fs.rename(filePath, quarantinePath);
logger.info(`Quarantined corrupted file: ${fileName}`);
}
async writeJsonFile(filePath, data) {
const tempPath = `${filePath}.tmp`;
try {
await fs.writeFile(tempPath, JSON.stringify(data, null, 2), 'utf8');
await fs.rename(tempPath, filePath);
}
catch (error) {
// Cleanup temp file on error
try {
await fs.unlink(tempPath);
}
catch (cleanupError) {
// Ignore cleanup errors
}
throw error;
}
}
async readJsonFile(filePath) {
const content = await fs.readFile(filePath, 'utf8');
return JSON.parse(content);
}
async copyDirectory(src, dest) {
await fs.mkdir(dest, { recursive: true });
const files = await fs.readdir(src);
for (const file of files) {
const srcPath = path.join(src, file);
const destPath = path.join(dest, file);
const stat = await fs.stat(srcPath);
if (stat.isDirectory()) {
await this.copyDirectory(srcPath, destPath);
}
else {
await fs.copyFile(srcPath, destPath);
}
}
}
async clearDirectory(dir) {
try {
const files = await fs.readdir(dir);
await Promise.all(files.map(file => fs.unlink(path.join(dir, file))));
}
catch (error) {
if (error.code !== 'ENOENT') {
throw error;
}
}
}
async removeDirectory(dir) {
await fs.rm(dir, { recursive: true, force: true });
}
async calculateDirectoryChecksum(dir) {
const hash = crypto.createHash('sha256');
const processDir = async (currentDir) => {
const files = await fs.readdir(currentDir, { withFileTypes: true });
for (const file of files.sort((a, b) => a.name.localeCompare(b.name))) {
const fullPath = path.join(currentDir, file.name);
if (file.isDirectory()) {
hash.update(file.name);
await processDir(fullPath);
}
else {
hash.update(file.name);
const content = await fs.readFile(fullPath);
hash.update(content);
}
}
};
await processDir(dir);
return hash.digest('hex');
}
async calculateStorageUsage() {
let totalSize = 0;
const calculateDirSize = async (dir) => {
try {
const files = await fs.readdir(dir, { withFileTypes: true });
for (const file of files) {
const fullPath = path.join(dir, file.name);
if (file.isDirectory()) {
await calculateDirSize(fullPath);
}
else {
const stat = await fs.stat(fullPath);
totalSize += stat.size;
}
}
}
catch (error) {
// Ignore errors for individual directories
}
};
await calculateDirSize(this.dataDir);
return totalSize;
}
}
//# sourceMappingURL=fileStorage.js.map