UNPKG

@boundless-oss/atlas

Version:

Atlas - MCP Server for comprehensive startup project management

627 lines 24.5 kB
import { TriggerSuggestionEngine } from './trigger-suggestion-engine.js'; export class ProcessBuilder { sessions = new Map(); store; triggerEngine; constructor(store) { this.store = store; this.triggerEngine = new TriggerSuggestionEngine(store); } // Simple API for tests async createProcess(options) { if (!options.name || options.name.trim().length === 0) { throw new Error('Process name is required'); } const process = { id: `process-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`, name: options.name, description: options.description || '', version: '1.0.0', persona: options.persona, triggers: options.triggers || [], activities: options.activities || [], variables: options.variables || {}, metadata: { createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), executionCount: 0 } }; await this.store.saveProcess(process); return process; } async addActivity(processId, activity, position) { const process = await this.store.getProcess(processId); if (!process) { throw new Error(`Process ${processId} not found`); } if (position !== undefined && position >= 0 && position <= process.activities.length) { process.activities.splice(position, 0, activity); } else { process.activities.push(activity); } process.metadata.updatedAt = new Date().toISOString(); await this.store.saveProcess(process); return process; } async updateActivity(processId, activityId, updates) { const process = await this.store.getProcess(processId); if (!process) { throw new Error(`Process ${processId} not found`); } const activityIndex = process.activities.findIndex(a => a.id === activityId); if (activityIndex === -1) { throw new Error(`Activity ${activityId} not found`); } process.activities[activityIndex] = { ...process.activities[activityIndex], ...updates }; process.metadata.updatedAt = new Date().toISOString(); await this.store.saveProcess(process); return process; } async removeActivity(processId, activityId) { const process = await this.store.getProcess(processId); if (!process) { throw new Error(`Process ${processId} not found`); } const originalLength = process.activities.length; process.activities = process.activities.filter(a => a.id !== activityId); if (process.activities.length === originalLength) { throw new Error('Activity not found'); } process.metadata.updatedAt = new Date().toISOString(); await this.store.saveProcess(process); return process; } async addTrigger(processId, trigger) { const process = await this.store.getProcess(processId); if (!process) { throw new Error(`Process ${processId} not found`); } // Check for duplicate trigger ID if (process.triggers.some(t => t.id === trigger.id)) { throw new Error(`Trigger with ID ${trigger.id} already exists`); } process.triggers.push(trigger); process.metadata.updatedAt = new Date().toISOString(); await this.store.saveProcess(process); return process; } async updateTrigger(processId, triggerId, updates) { const process = await this.store.getProcess(processId); if (!process) { throw new Error(`Process ${processId} not found`); } const triggerIndex = process.triggers.findIndex(t => t.id === triggerId); if (triggerIndex === -1) { throw new Error(`Trigger ${triggerId} not found`); } process.triggers[triggerIndex] = { ...process.triggers[triggerIndex], ...updates }; process.metadata.updatedAt = new Date().toISOString(); await this.store.saveProcess(process); return process; } async removeTrigger(processId, triggerId) { const process = await this.store.getProcess(processId); if (!process) { throw new Error(`Process ${processId} not found`); } process.triggers = process.triggers.filter(t => t.id !== triggerId); process.metadata.updatedAt = new Date().toISOString(); await this.store.saveProcess(process); return process; } async cloneProcess(processId, newName, options) { const process = await this.store.getProcess(processId); if (!process) { throw new Error(`Process ${processId} not found`); } const cloned = { ...process, id: `process-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`, name: newName, persona: (options?.persona || process.persona), // Deep clone activities with new IDs activities: process.activities.map(activity => ({ ...activity, id: `${activity.id}-clone-${Date.now()}` })), // Deep clone triggers with new IDs triggers: process.triggers.map(trigger => ({ ...trigger, id: `${trigger.id}-clone-${Date.now()}` })), // Deep clone variables variables: JSON.parse(JSON.stringify(process.variables)), metadata: { ...process.metadata, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), executionCount: 0, } }; await this.store.saveProcess(cloned); return cloned; } steps = [ { id: 'name', type: 'question', prompt: '📝 What would you like to name this process?', helpText: 'Choose a descriptive name that explains what the process does', validation: (value) => { if (!value || value.trim().length < 3) { return 'Process name must be at least 3 characters'; } return true; } }, { id: 'description', type: 'question', prompt: '📄 Describe what this process does', helpText: 'This helps others understand the purpose of the process' }, { id: 'persona', type: 'question', prompt: '👤 Who is this process for?', helpText: 'Choose a persona or type "custom" for general use', options: [ 'software-engineer', 'architect', 'cto', 'ceo', 'cfo', 'marketing', 'sales', 'product-manager', 'designer', 'custom' ] }, { id: 'activities', type: 'multiSelect', prompt: '📊 What activities should this process include?', helpText: 'Select all that apply (we\'ll configure details later)', options: [ 'Run automated tools', 'Wait for human approval', 'Delegate to AI agents', 'Make conditional decisions', 'Loop through items', 'Call external APIs', 'Generate reports', 'Send notifications' ] }, { id: 'trigger-review', type: 'trigger-suggestion', prompt: '🎯 Based on your process, here are my trigger recommendations', helpText: 'You can accept, modify, or choose alternatives' }, { id: 'confirm', type: 'confirm', prompt: '✅ Ready to create your process?', helpText: 'You can always modify the process later' } ]; async startProcessBuilder() { const session = { id: this.generateSessionId(), currentStep: 0, processData: {}, responses: {} }; this.sessions.set(session.id, session); return session; } async continueBuilder(sessionId, response) { const session = this.sessions.get(sessionId); if (!session) { return { valid: false, message: 'Session not found' }; } const currentStep = this.steps[session.currentStep]; // Validate response if (currentStep.validation) { const validationResult = currentStep.validation(response); if (validationResult !== true) { return { valid: false, message: typeof validationResult === 'string' ? validationResult : 'Invalid response' }; } } // Store response session.responses[currentStep.id] = response; // Process based on step type if (currentStep.type === 'trigger-suggestion') { // User selected a trigger configuration if (response.selectedTrigger) { session.processData.triggers = [response.selectedTrigger]; } } // Move to next step session.currentStep++; // Check if we need to prepare trigger suggestions if (session.currentStep < this.steps.length) { const nextStep = this.steps[session.currentStep]; if (nextStep.type === 'trigger-suggestion') { // Generate trigger suggestions based on collected data const tempProcess = this.buildProcessFromSession(session); const suggestions = await this.triggerEngine.suggestTriggers(tempProcess); session.suggestedTriggers = suggestions.map(s => s.trigger); } } // Check if complete if (session.currentStep >= this.steps.length) { const process = this.buildProcessFromSession(session); this.sessions.delete(sessionId); return { valid: true, completed: true, process }; } return { valid: true }; } formatCurrentStep(sessionId) { const session = this.sessions.get(sessionId); if (!session) { return 'Session not found'; } const step = this.steps[session.currentStep]; const progress = `Step ${session.currentStep + 1} of ${this.steps.length}`; let output = `${progress}\n\n${step.prompt}\n`; if (step.helpText) { output += `💡 ${step.helpText}\n`; } if (step.type === 'trigger-suggestion' && session.suggestedTriggers) { output += '\n'; session.suggestedTriggers.forEach((trigger, index) => { output += `\n${index + 1}. **${trigger.name}**\n`; output += ` Type: ${trigger.type}\n`; if (trigger.type === 'schedule' && trigger.config.cron) { output += ` Schedule: ${trigger.config.cron}\n`; } if (trigger.reasoning) { output += ` 📝 ${trigger.reasoning}\n`; } }); output += '\nWhich trigger would you like to use? (number or "custom" for manual setup)'; } else if (step.options) { output += '\nOptions:\n'; step.options.forEach((option, index) => { output += `${index + 1}. ${option}\n`; }); } return output; } cancelSession(sessionId) { this.sessions.delete(sessionId); } buildProcessFromSession(session) { const responses = session.responses; // Build activities based on selections const activities = []; const activitySelections = responses.activities || []; // Map selections to basic activity templates if (activitySelections.includes('Run automated tools')) { activities.push({ id: 'tool-1', type: 'tool', name: 'Execute Tool', config: { toolName: 'check_project_status', toolArgs: {} } }); } if (activitySelections.includes('Wait for human approval')) { activities.push({ id: 'human-1', type: 'human', name: 'Human Approval', config: { prompt: 'Please review and approve', approvalType: 'any' } }); } if (activitySelections.includes('Generate reports')) { activities.push({ id: 'tool-2', type: 'tool', name: 'Generate Report', config: { toolName: 'generate_report', toolArgs: {} } }); } const process = { id: this.generateId('process'), name: responses.name || 'Unnamed Process', description: responses.description, version: '1.0.0', persona: responses.persona, triggers: session.processData.triggers || [], activities, variables: {}, metadata: { createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), executionCount: 0 } }; return process; } generateSessionId() { return `session-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`; } generateId(prefix) { return `${prefix}-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`; } // Additional methods for test compatibility async setVariables(processId, variables, merge = false) { const process = await this.store.getProcess(processId); if (!process) { throw new Error(`Process ${processId} not found`); } if (merge) { // Merge variables with existing ones process.variables = { ...process.variables, ...variables }; } else { // Replace all variables process.variables = variables; } process.metadata.updatedAt = new Date().toISOString(); await this.store.saveProcess(process); return process; } async validateProcess(processId) { const process = await this.store.getProcess(processId); if (!process) { return { isValid: false, errors: [{ type: 'error', message: `Process ${processId} not found` }], warnings: [] }; } const errors = []; const warnings = []; // Validate required fields if (!process.name || process.name.trim().length === 0) { errors.push({ type: 'error', field: 'name', message: 'Process name is required' }); } if (!process.id) { errors.push({ type: 'error', field: 'id', message: 'Process ID is required' }); } // Validate activities if (!process.activities || process.activities.length === 0) { warnings.push({ type: 'warning', message: 'Process has no activities defined' }); } else { // Validate each activity process.activities.forEach((activity, index) => { if (!activity.id) { errors.push({ type: 'error', field: `activities[${index}].id`, message: `Activity at index ${index} is missing ID` }); } if (!activity.type) { errors.push({ type: 'error', field: `activities[${index}].type`, message: `Activity ${activity.id || index} is missing type` }); } if (!activity.name) { errors.push({ type: 'error', field: `activities[${index}].name`, message: `Activity ${activity.id || index} is missing name` }); } // Validate activity-specific config if (activity.type === 'tool' && (!activity.config || !activity.config.toolName)) { errors.push({ type: 'error', field: `activities[${index}].config`, message: `Tool activity ${activity.id || index} is missing toolName in config` }); } // Validate condition syntax if (activity.condition && !this.isValidCondition(activity.condition)) { errors.push({ type: 'error', field: `activities[${index}].condition`, message: `Invalid condition syntax for activity ${activity.id || index}` }); } }); } // Validate triggers if (!process.triggers || process.triggers.length === 0) { warnings.push({ type: 'warning', message: 'Process has no triggers defined' }); } else { process.triggers.forEach((trigger, index) => { if (!trigger.id) { errors.push({ type: 'error', field: `triggers[${index}].id`, message: `Trigger at index ${index} is missing ID` }); } if (!trigger.type) { errors.push({ type: 'error', field: `triggers[${index}].type`, message: `Trigger ${trigger.id || index} is missing type` }); } if (trigger.type === 'schedule') { if (!trigger.config?.cron) { errors.push({ type: 'error', field: `triggers[${index}].config.cron`, message: `Schedule trigger ${trigger.id || index} is missing cron expression` }); } else if (!this.isValidCronExpression(trigger.config.cron)) { errors.push({ type: 'error', field: `triggers[${index}].config.cron`, message: `Invalid cron expression for trigger ${trigger.id || index}` }); } } }); } return { isValid: errors.length === 0, errors, warnings }; } isValidCondition(condition) { // Simple validation for condition syntax // Should contain comparison operators and valid syntax const validOperators = ['===', '!==', '==', '!=', '>', '<', '>=', '<=', '&&', '||', '!']; const hasValidOperator = validOperators.some(op => condition.includes(op)); // Check for invalid characters const invalidChars = ['@', '#', '$']; const hasInvalidChars = invalidChars.some(char => condition.includes(char)); // Check for balanced parentheses let parenCount = 0; for (const char of condition) { if (char === '(') parenCount++; if (char === ')') parenCount--; if (parenCount < 0) return false; } return hasValidOperator && !hasInvalidChars && parenCount === 0; } isValidCronExpression(cron) { // Basic cron expression validation // Format: minute hour day month weekday const parts = cron.trim().split(/\s+/); // Must have exactly 5 parts for standard cron if (parts.length !== 5) { return false; } // Validate each part const patterns = [ /^(\*|[0-5]?\d)(,(\*|[0-5]?\d))*$/, // minute (0-59) /^(\*|[01]?\d|2[0-3])(,(\*|[01]?\d|2[0-3]))*$/, // hour (0-23) /^(\*|[1-9]|[12]\d|3[01])(,(\*|[1-9]|[12]\d|3[01]))*$/, // day (1-31) /^(\*|[1-9]|1[0-2])(,(\*|[1-9]|1[0-2]))*$/, // month (1-12) /^(\*|[0-6])(,(\*|[0-6]))*$/ // weekday (0-6) ]; for (let i = 0; i < parts.length; i++) { const part = parts[i]; // Handle ranges (e.g., 1-5) if (part.includes('-')) { const rangeParts = part.split('-'); if (rangeParts.length !== 2) return false; // For simplicity, just check it's not completely invalid if (!/^\d+$/.test(rangeParts[0]) || !/^\d+$/.test(rangeParts[1])) { return false; } continue; } // Handle step values (e.g., */5) if (part.includes('/')) { const stepParts = part.split('/'); if (stepParts.length !== 2) return false; if (stepParts[0] !== '*' && !/^\d+$/.test(stepParts[0])) return false; if (!/^\d+$/.test(stepParts[1])) return false; continue; } // Validate against pattern if (!patterns[i].test(part)) { return false; } } return true; } async exportProcess(processId) { const process = await this.store.getProcess(processId); if (!process) { throw new Error(`Process ${processId} not found`); } // Create export format without runtime data const exportData = { name: process.name, description: process.description, version: process.version, persona: process.persona, triggers: process.triggers, activities: process.activities, variables: process.variables, exportVersion: '1.0', exportedAt: new Date().toISOString() }; return exportData; } async importProcess(data, options) { let importData; // Handle both JSON string and object input if (typeof data === 'string') { try { importData = JSON.parse(data); } catch (error) { throw new Error('Invalid JSON format'); } } else { importData = data; } // Build process from import data const process = { id: this.generateId('process'), name: (options?.namePrefix || '') + (importData.name || 'Imported Process'), description: importData.description, version: importData.version || '1.0.0', persona: importData.persona || 'custom', triggers: importData.triggers || [], activities: importData.activities || [], variables: importData.variables || {}, metadata: { createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), executionCount: 0, } }; // Save the imported process first await this.store.saveProcess(process); // Then validate it const validation = await this.validateProcess(process.id); if (!validation.isValid) { // Remove invalid process await this.store.deleteProcess(process.id); const errorMessages = validation.errors.map(e => e.message).join(', '); throw new Error(`Invalid process: ${errorMessages}`); } return process; } } //# sourceMappingURL=process-builder.js.map