UNPKG

@boundless-oss/atlas

Version:

Atlas - MCP Server for comprehensive startup project management

340 lines (291 loc) 10.9 kB
import path from 'path'; import { ConfigManager } from '../../config/config-manager.js'; import { ProcessDefinition, ProcessExecution, ProcessTemplate } from './types.js'; import { FileSystemAdapter, NodeFileSystemAdapter } from './file-system-adapter.js'; export class ProcessStore { private dataPath: string = ''; private configManager: ConfigManager; private processCache: Map<string, ProcessDefinition> = new Map(); private executionCache: Map<string, ProcessExecution> = new Map(); private fs: FileSystemAdapter; constructor(configManager: ConfigManager, fs?: FileSystemAdapter) { this.configManager = configManager; this.fs = fs || new NodeFileSystemAdapter(); } async init(): Promise<void> { const storageManager = this.configManager.getStorageManager(); this.dataPath = await storageManager.getModuleDataPath('process-automation', ''); // Ensure subdirectories exist await this.fs.mkdir(path.join(this.dataPath, 'processes'), { recursive: true }); await this.fs.mkdir(path.join(this.dataPath, 'executions'), { recursive: true }); await this.fs.mkdir(path.join(this.dataPath, 'templates'), { recursive: true }); } // Process Definition Methods async saveProcess(process: ProcessDefinition): Promise<void> { const filePath = path.join(this.dataPath, 'processes', `${process.id}.json`); await this.fs.writeFile(filePath, JSON.stringify(process, null, 2)); this.processCache.set(process.id, process); } async getProcess(processId: string): Promise<ProcessDefinition | null> { // Check cache first if (this.processCache.has(processId)) { return this.processCache.get(processId)!; } try { const filePath = path.join(this.dataPath, 'processes', `${processId}.json`); await this.fs.access(filePath); const data = await this.fs.readFile(filePath, 'utf-8'); const process = JSON.parse(data); this.processCache.set(processId, process); return process; } catch (error) { return null; } } async getAllProcesses(filters?: { persona?: string; hasEnabledTriggers?: boolean; }): Promise<ProcessDefinition[]> { try { const files = await this.fs.readdir(path.join(this.dataPath, 'processes')); const processes: ProcessDefinition[] = []; for (const file of files) { if (file.endsWith('.json')) { const data = await this.fs.readFile( path.join(this.dataPath, 'processes', file), 'utf-8' ); const process = JSON.parse(data) as ProcessDefinition; // Apply filters if (filters?.persona && process.persona !== filters.persona) { continue; } if (filters?.hasEnabledTriggers) { const hasEnabled = process.triggers.some(t => t.enabled); if (!hasEnabled) { continue; } } processes.push(process); } } return processes; } catch (error) { return []; } } async deleteProcess(processId: string): Promise<void> { const filePath = path.join(this.dataPath, 'processes', `${processId}.json`); await this.fs.unlink(filePath).catch(() => {}); // Ignore if doesn't exist this.processCache.delete(processId); } // Execution Methods async saveExecution(execution: ProcessExecution): Promise<void> { // Ensure the process-specific execution directory exists const processDir = path.join(this.dataPath, 'executions', execution.processId); await this.fs.mkdir(processDir, { recursive: true }); const filePath = path.join(processDir, `${execution.id}.json`); await this.fs.writeFile(filePath, JSON.stringify(execution, null, 2)); } async getExecution(processId: string, executionId: string): Promise<ProcessExecution | null> { try { const filePath = path.join(this.dataPath, 'executions', processId, `${executionId}.json`); const data = await this.fs.readFile(filePath, 'utf-8'); return JSON.parse(data); } catch (error) { return null; } } async getExecutionsForProcess(processId: string): Promise<ProcessExecution[]> { try { const processDir = path.join(this.dataPath, 'executions', processId); const files = await this.fs.readdir(processDir); const executions: ProcessExecution[] = []; for (const file of files) { if (file.endsWith('.json')) { const data = await this.fs.readFile( path.join(processDir, file), 'utf-8' ); const execution = JSON.parse(data) as ProcessExecution; executions.push(execution); } } return executions.sort((a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime() ); } catch (error) { return []; } } async getRecentExecutions(limit: number = 10): Promise<ProcessExecution[]> { try { const executionsDirs = await this.fs.readdir(path.join(this.dataPath, 'executions')); const executions: ProcessExecution[] = []; for (const processDir of executionsDirs) { try { const files = await this.fs.readdir(path.join(this.dataPath, 'executions', processDir)); for (const file of files) { if (file.endsWith('.json')) { const data = await this.fs.readFile( path.join(this.dataPath, 'executions', processDir, file), 'utf-8' ); executions.push(JSON.parse(data)); } } } catch { // Skip if not a directory } } return executions .sort((a, b) => new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime() ) .slice(0, limit); } catch (error) { return []; } } // Template Methods async saveTemplate(template: ProcessTemplate): Promise<void> { const filePath = path.join(this.dataPath, 'templates', `${template.id}.json`); await this.fs.writeFile(filePath, JSON.stringify(template, null, 2)); } async getTemplate(templateId: string): Promise<ProcessTemplate | null> { try { const filePath = path.join(this.dataPath, 'templates', `${templateId}.json`); const data = await this.fs.readFile(filePath, 'utf-8'); return JSON.parse(data); } catch (error) { return null; } } async getTemplatesByPersona(persona: string): Promise<ProcessTemplate[]> { const allFiles = await this.fs.readdir(path.join(this.dataPath, 'templates')); const templates: ProcessTemplate[] = []; for (const file of allFiles) { if (file.endsWith('.json')) { const data = await this.fs.readFile( path.join(this.dataPath, 'templates', file), 'utf-8' ); const template = JSON.parse(data) as ProcessTemplate; if (template.persona === persona) { templates.push(template); } } } return templates; } // Metrics Methods async getProcessMetrics(processId: string): Promise<ProcessMetrics> { const executions = await this.getExecutionsForProcess(processId); const completed = executions.filter(e => e.status === 'completed'); const failed = executions.filter(e => e.status === 'failed'); const totalDuration = completed.reduce((sum, e) => sum + (e.duration || 0), 0); const averageDuration = completed.length > 0 ? totalDuration / completed.length : 0; return { executionCount: executions.length, successCount: completed.length, failureCount: failed.length, successRate: executions.length > 0 ? completed.length / executions.length : 0, averageDuration, lastExecutedAt: executions[0]?.startedAt }; } // Search and Filter Methods async getProcessExecutions(processId: string, options?: { status?: string; limit?: number; }): Promise<ProcessExecution[]> { let executions = await this.getExecutionsForProcess(processId); if (options?.status) { executions = executions.filter(e => e.status === options.status); } if (options?.limit) { executions = executions.slice(0, options.limit); } return executions; } async searchProcesses(query: string): Promise<ProcessDefinition[]> { const allProcesses = await this.getAllProcesses(); const lowerQuery = query.toLowerCase(); return allProcesses.filter(process => process.name.toLowerCase().includes(lowerQuery) || (process.description && process.description.toLowerCase().includes(lowerQuery)) ); } async getStats(): Promise<{ totalProcesses: number; byPersona: Record<string, number>; byTriggerType: Record<string, number>; enabledTriggers: number; totalTriggers: number; }> { const processes = await this.getAllProcesses(); const byPersona: Record<string, number> = {}; const byTriggerType: Record<string, number> = {}; let enabledTriggers = 0; let totalTriggers = 0; for (const process of processes) { // Count by persona const persona = process.persona || 'general'; byPersona[persona] = (byPersona[persona] || 0) + 1; // Count triggers for (const trigger of process.triggers) { totalTriggers++; if (trigger.enabled) { enabledTriggers++; } byTriggerType[trigger.type] = (byTriggerType[trigger.type] || 0) + 1; } } return { totalProcesses: processes.length, byPersona, byTriggerType, enabledTriggers, totalTriggers }; } // Cleanup Methods async cleanupOldExecutions(daysToKeep: number = 30): Promise<number> { const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - daysToKeep); try { const processDirs = await this.fs.readdir(path.join(this.dataPath, 'executions')); let deletedCount = 0; for (const processDir of processDirs) { try { const files = await this.fs.readdir(path.join(this.dataPath, 'executions', processDir)); for (const file of files) { if (file.endsWith('.json')) { const filePath = path.join(this.dataPath, 'executions', processDir, file); // For simplicity, delete all files in cleanup await this.fs.unlink(filePath); deletedCount++; } } } catch { // Skip if not a directory } } return deletedCount; } catch (error) { return 0; } } } interface ProcessMetrics { executionCount: number; successCount: number; failureCount: number; successRate: number; averageDuration: number; lastExecutedAt?: string; }