UNPKG

claude-flow

Version:

Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration

480 lines (440 loc) 18.3 kB
/** * @claude-flow/browser - Workflow Templates * Pre-built workflow templates for common browser automation tasks */ import type { BrowserTrajectoryStep } from '../domain/types.js'; // ============================================================================ // Workflow Types // ============================================================================ export interface WorkflowTemplate { id: string; name: string; description: string; category: WorkflowCategory; steps: WorkflowStep[]; variables: WorkflowVariable[]; tags: string[]; estimatedDuration: number; // ms successRate?: number; } export type WorkflowCategory = | 'authentication' | 'data-extraction' | 'form-submission' | 'navigation' | 'testing' | 'monitoring'; export interface WorkflowStep { id: string; action: BrowserAction; target?: string; // Selector or variable reference ${var} value?: string; // Value or variable reference waitAfter?: number; optional?: boolean; onError?: 'continue' | 'abort' | 'retry'; maxRetries?: number; condition?: string; // JavaScript condition } export type BrowserAction = | 'open' | 'click' | 'fill' | 'type' | 'press' | 'select' | 'check' | 'uncheck' | 'hover' | 'scroll' | 'wait' | 'screenshot' | 'snapshot' | 'get' | 'eval' | 'assert'; export interface WorkflowVariable { name: string; type: 'string' | 'number' | 'boolean' | 'selector'; required: boolean; default?: string | number | boolean; description: string; sensitive?: boolean; // Will be masked in logs } export interface WorkflowExecution { templateId: string; variables: Record<string, unknown>; status: 'pending' | 'running' | 'completed' | 'failed'; currentStep: number; results: WorkflowStepResult[]; startedAt: string; completedAt?: string; error?: string; } export interface WorkflowStepResult { stepId: string; success: boolean; duration: number; data?: unknown; error?: string; retries?: number; } // ============================================================================ // Built-in Workflow Templates // ============================================================================ export const WORKFLOW_TEMPLATES: WorkflowTemplate[] = [ // ============ Authentication ============ { id: 'login-basic', name: 'Basic Login', description: 'Standard username/password login flow', category: 'authentication', tags: ['login', 'auth', 'form'], estimatedDuration: 5000, variables: [ { name: 'url', type: 'string', required: true, description: 'Login page URL' }, { name: 'usernameSelector', type: 'selector', required: false, default: '#username, #email, [name="email"], [type="email"]', description: 'Username/email field selector' }, { name: 'passwordSelector', type: 'selector', required: false, default: '#password, [name="password"], [type="password"]', description: 'Password field selector' }, { name: 'submitSelector', type: 'selector', required: false, default: '[type="submit"], button[type="submit"], #login-btn', description: 'Submit button selector' }, { name: 'username', type: 'string', required: true, description: 'Username or email', sensitive: false }, { name: 'password', type: 'string', required: true, description: 'Password', sensitive: true }, { name: 'successIndicator', type: 'selector', required: false, default: '.dashboard, .home, #welcome', description: 'Element that indicates successful login' }, ], steps: [ { id: 'navigate', action: 'open', target: '\${url}', waitAfter: 1000 }, { id: 'snapshot-login', action: 'snapshot', onError: 'continue' }, { id: 'enter-username', action: 'fill', target: '\${usernameSelector}', value: '\${username}', onError: 'abort' }, { id: 'enter-password', action: 'fill', target: '\${passwordSelector}', value: '\${password}', onError: 'abort' }, { id: 'submit', action: 'click', target: '\${submitSelector}', waitAfter: 2000, onError: 'retry', maxRetries: 2 }, { id: 'verify-success', action: 'wait', target: '\${successIndicator}', onError: 'abort' }, { id: 'snapshot-dashboard', action: 'snapshot', optional: true }, ], }, { id: 'login-oauth', name: 'OAuth/SSO Login', description: 'Login via OAuth provider (Google, GitHub, etc.)', category: 'authentication', tags: ['login', 'oauth', 'sso', 'google', 'github'], estimatedDuration: 8000, variables: [ { name: 'url', type: 'string', required: true, description: 'App login page URL' }, { name: 'providerButton', type: 'selector', required: true, description: 'OAuth provider button selector (e.g., "Continue with Google")' }, { name: 'email', type: 'string', required: true, description: 'OAuth account email' }, { name: 'password', type: 'string', required: true, description: 'OAuth account password', sensitive: true }, { name: 'successUrl', type: 'string', required: false, description: 'URL pattern after successful login' }, ], steps: [ { id: 'navigate', action: 'open', target: '\${url}', waitAfter: 1000 }, { id: 'click-oauth', action: 'click', target: '\${providerButton}', waitAfter: 2000 }, { id: 'enter-email', action: 'fill', target: '[type="email"], #identifierId', value: '\${email}' }, { id: 'next-email', action: 'click', target: '#identifierNext, [type="submit"]', waitAfter: 1500 }, { id: 'enter-password', action: 'fill', target: '[type="password"], [name="password"]', value: '\${password}' }, { id: 'submit', action: 'click', target: '#passwordNext, [type="submit"]', waitAfter: 3000 }, { id: 'wait-redirect', action: 'wait', target: '\${successUrl}', onError: 'continue' }, ], }, { id: 'logout', name: 'Logout', description: 'Standard logout flow', category: 'authentication', tags: ['logout', 'auth', 'session'], estimatedDuration: 3000, variables: [ { name: 'menuSelector', type: 'selector', required: false, default: '.user-menu, #user-dropdown, .avatar', description: 'User menu selector (if needed)' }, { name: 'logoutSelector', type: 'selector', required: true, description: 'Logout button/link selector' }, { name: 'confirmSelector', type: 'selector', required: false, description: 'Confirmation button if needed' }, ], steps: [ { id: 'open-menu', action: 'click', target: '\${menuSelector}', optional: true, waitAfter: 500 }, { id: 'click-logout', action: 'click', target: '\${logoutSelector}', waitAfter: 1000 }, { id: 'confirm', action: 'click', target: '\${confirmSelector}', optional: true, waitAfter: 1000 }, { id: 'verify', action: 'wait', target: '/login, /signin, .login-form', onError: 'continue' }, ], }, // ============ Data Extraction ============ { id: 'scrape-table', name: 'Scrape Table Data', description: 'Extract data from HTML tables', category: 'data-extraction', tags: ['scrape', 'table', 'data', 'extract'], estimatedDuration: 3000, variables: [ { name: 'url', type: 'string', required: true, description: 'Page URL containing the table' }, { name: 'tableSelector', type: 'selector', required: false, default: 'table', description: 'Table selector' }, { name: 'includeHeaders', type: 'boolean', required: false, default: true, description: 'Include table headers' }, ], steps: [ { id: 'navigate', action: 'open', target: '\${url}', waitAfter: 1000 }, { id: 'wait-table', action: 'wait', target: '\${tableSelector}' }, { id: 'extract-data', action: 'eval', value: ` (() => { const table = document.querySelector('\${tableSelector}'); if (!table) return { error: 'Table not found' }; const rows = Array.from(table.querySelectorAll('tr')); const data = rows.map(row => { const cells = Array.from(row.querySelectorAll('td, th')); return cells.map(cell => cell.textContent.trim()); }); return { headers: data[0], rows: data.slice(1), totalRows: data.length - 1 }; })() `}, ], }, { id: 'scrape-list', name: 'Scrape List Items', description: 'Extract items from lists or repeated elements', category: 'data-extraction', tags: ['scrape', 'list', 'data', 'extract'], estimatedDuration: 3000, variables: [ { name: 'url', type: 'string', required: true, description: 'Page URL' }, { name: 'itemSelector', type: 'selector', required: true, description: 'Selector for each item' }, { name: 'fields', type: 'string', required: true, description: 'JSON object mapping field names to sub-selectors' }, ], steps: [ { id: 'navigate', action: 'open', target: '\${url}', waitAfter: 1000 }, { id: 'wait-items', action: 'wait', target: '\${itemSelector}' }, { id: 'extract-items', action: 'eval', value: ` (() => { const items = document.querySelectorAll('\${itemSelector}'); const fields = JSON.parse('\${fields}'); return Array.from(items).map(item => { const result = {}; for (const [name, selector] of Object.entries(fields)) { const el = item.querySelector(selector); result[name] = el ? el.textContent.trim() : null; } return result; }); })() `}, ], }, // ============ Form Submission ============ { id: 'contact-form', name: 'Contact Form Submission', description: 'Fill and submit a contact form', category: 'form-submission', tags: ['form', 'contact', 'submit'], estimatedDuration: 5000, variables: [ { name: 'url', type: 'string', required: true, description: 'Contact page URL' }, { name: 'name', type: 'string', required: true, description: 'Your name' }, { name: 'email', type: 'string', required: true, description: 'Your email' }, { name: 'message', type: 'string', required: true, description: 'Message content' }, { name: 'submitSelector', type: 'selector', required: false, default: '[type="submit"], button[type="submit"]', description: 'Submit button' }, ], steps: [ { id: 'navigate', action: 'open', target: '\${url}', waitAfter: 1000 }, { id: 'fill-name', action: 'fill', target: '#name, [name="name"], [placeholder*="name" i]', value: '\${name}' }, { id: 'fill-email', action: 'fill', target: '#email, [name="email"], [type="email"]', value: '\${email}' }, { id: 'fill-message', action: 'fill', target: '#message, [name="message"], textarea', value: '\${message}' }, { id: 'submit', action: 'click', target: '\${submitSelector}', waitAfter: 2000 }, { id: 'screenshot', action: 'screenshot', optional: true }, ], }, // ============ Testing ============ { id: 'visual-regression', name: 'Visual Regression Test', description: 'Take screenshots for visual comparison', category: 'testing', tags: ['test', 'visual', 'screenshot', 'regression'], estimatedDuration: 5000, variables: [ { name: 'urls', type: 'string', required: true, description: 'Comma-separated list of URLs to test' }, { name: 'viewport', type: 'string', required: false, default: '1280x720', description: 'Viewport size (WxH)' }, ], steps: [ { id: 'set-viewport', action: 'eval', value: ` (() => { const [w, h] = '\${viewport}'.split('x').map(Number); return { width: w, height: h }; })() `}, { id: 'test-urls', action: 'eval', value: `'\${urls}'.split(',').map(u => u.trim())` }, ], }, { id: 'smoke-test', name: 'Smoke Test', description: 'Basic smoke test to verify page loads correctly', category: 'testing', tags: ['test', 'smoke', 'health'], estimatedDuration: 3000, variables: [ { name: 'url', type: 'string', required: true, description: 'URL to test' }, { name: 'expectedTitle', type: 'string', required: false, description: 'Expected page title (partial match)' }, { name: 'requiredElements', type: 'string', required: false, description: 'Comma-separated selectors that must exist' }, ], steps: [ { id: 'navigate', action: 'open', target: '\${url}', waitAfter: 1000, onError: 'abort' }, { id: 'check-title', action: 'get', target: 'title', condition: '\${expectedTitle}' }, { id: 'check-elements', action: 'eval', value: ` (() => { const selectors = '\${requiredElements}'.split(',').map(s => s.trim()).filter(Boolean); const results = selectors.map(sel => ({ selector: sel, found: document.querySelector(sel) !== null })); const allFound = results.every(r => r.found); return { results, allFound }; })() `, optional: true }, { id: 'screenshot', action: 'screenshot' }, ], }, // ============ Monitoring ============ { id: 'uptime-check', name: 'Uptime Check', description: 'Check if a page is accessible and loads within timeout', category: 'monitoring', tags: ['monitor', 'uptime', 'health', 'availability'], estimatedDuration: 10000, variables: [ { name: 'url', type: 'string', required: true, description: 'URL to check' }, { name: 'timeout', type: 'number', required: false, default: 10000, description: 'Timeout in ms' }, { name: 'expectedStatus', type: 'number', required: false, default: 200, description: 'Expected HTTP status' }, ], steps: [ { id: 'navigate', action: 'open', target: '\${url}', onError: 'abort' }, { id: 'measure', action: 'eval', value: ` (() => { const timing = performance.timing; return { dns: timing.domainLookupEnd - timing.domainLookupStart, connection: timing.connectEnd - timing.connectStart, ttfb: timing.responseStart - timing.requestStart, domLoad: timing.domContentLoadedEventEnd - timing.navigationStart, fullLoad: timing.loadEventEnd - timing.navigationStart }; })() `}, { id: 'screenshot', action: 'screenshot', optional: true }, ], }, ]; // ============================================================================ // Workflow Manager // ============================================================================ export class WorkflowManager { private templates: Map<string, WorkflowTemplate> = new Map(); private executions: Map<string, WorkflowExecution> = new Map(); constructor() { // Load built-in templates for (const template of WORKFLOW_TEMPLATES) { this.templates.set(template.id, template); } } /** * Get all available templates */ listTemplates(category?: WorkflowCategory): WorkflowTemplate[] { const templates = Array.from(this.templates.values()); if (category) { return templates.filter((t) => t.category === category); } return templates; } /** * Get a specific template */ getTemplate(id: string): WorkflowTemplate | undefined { return this.templates.get(id); } /** * Register a custom template */ registerTemplate(template: WorkflowTemplate): void { this.templates.set(template.id, template); } /** * Search templates by tags */ searchTemplates(query: string): WorkflowTemplate[] { const terms = query.toLowerCase().split(/\s+/); return Array.from(this.templates.values()).filter((t) => { const searchText = `${t.name} ${t.description} ${t.tags.join(' ')}`.toLowerCase(); return terms.every((term) => searchText.includes(term)); }); } /** * Validate variables for a template */ validateVariables( templateId: string, variables: Record<string, unknown> ): { valid: boolean; errors: string[] } { const template = this.templates.get(templateId); if (!template) { return { valid: false, errors: [`Template '${templateId}' not found`] }; } const errors: string[] = []; for (const varDef of template.variables) { const value = variables[varDef.name]; if (varDef.required && (value === undefined || value === null || value === '')) { errors.push(`Required variable '${varDef.name}' is missing`); continue; } if (value !== undefined && value !== null) { switch (varDef.type) { case 'number': if (typeof value !== 'number' && isNaN(Number(value))) { errors.push(`Variable '${varDef.name}' must be a number`); } break; case 'boolean': if (typeof value !== 'boolean' && !['true', 'false'].includes(String(value))) { errors.push(`Variable '${varDef.name}' must be a boolean`); } break; } } } return { valid: errors.length === 0, errors }; } /** * Interpolate variables in step values */ interpolateStep(step: WorkflowStep, variables: Record<string, unknown>): WorkflowStep { const interpolate = (value: string | undefined): string | undefined => { if (!value) return value; return value.replace(/\$\{(\w+)\}/g, (_, name) => { const val = variables[name]; return val !== undefined ? String(val) : `\${${name}}`; }); }; return { ...step, target: interpolate(step.target), value: interpolate(step.value), }; } /** * Get execution status */ getExecution(executionId: string): WorkflowExecution | undefined { return this.executions.get(executionId); } } // ============================================================================ // Factory // ============================================================================ let defaultManager: WorkflowManager | null = null; export function getWorkflowManager(): WorkflowManager { if (!defaultManager) { defaultManager = new WorkflowManager(); } return defaultManager; } export function listWorkflows(category?: WorkflowCategory): WorkflowTemplate[] { return getWorkflowManager().listTemplates(category); } export function getWorkflow(id: string): WorkflowTemplate | undefined { return getWorkflowManager().getTemplate(id); }