@boundless-oss/atlas
Version:
Atlas - MCP Server for comprehensive startup project management
340 lines (291 loc) • 10.9 kB
text/typescript
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;
}