ai-debug-local-mcp
Version:
๐ฏ ENHANCED AI GUIDANCE v4.1.2: Dramatically improved tool descriptions help AI users choose the right tools instead of 'close enough' options. Ultra-fast keyboard automation (10x speed), universal recording, multi-ecosystem debugging support, and compreh
1,640 lines โข 83.3 kB
JavaScript
import { BaseHandler } from './base-handler.js';
import { EnhancedErrorContext } from './enhanced-error-context.js';
import { writeFile, readFile, mkdir } from 'fs/promises';
import { existsSync } from 'fs';
import { join } from 'path';
export class UniversalWorkflowHandler extends BaseHandler {
workflows = new Map();
sessionData = new Map();
workflowStoragePath;
constructor() {
super();
this.tools = this.getTools();
this.workflowStoragePath = join(process.cwd(), '.ai-debug', 'workflows');
this.initializeStorage();
}
getToolName() {
return 'universal-workflow';
}
getToolDescription() {
return 'Universal workflow orchestration and navigation for complex user journeys';
}
getTools() {
return [
{
name: 'navigate_to_url',
description: '๐ Navigate directly to a URL with smart waiting and validation. โ
WORKS WITH: Phoenix LiveView, React, Vue, Next.js, Angular, standard web apps. โ ๏ธ LIMITED FOR: Flutter Web (use flutter_quantum_interact instead)',
inputSchema: {
type: 'object',
properties: {
url: { type: 'string', description: 'URL to navigate to' },
options: {
type: 'object',
properties: {
waitUntil: { type: 'string', enum: ['load', 'domcontentloaded', 'networkidle'] },
timeout: { type: 'number', description: 'Timeout in milliseconds' },
expectedUrl: { type: 'string', description: 'Expected final URL after navigation' }
}
}
},
required: ['url']
}
},
{
name: 'wait_for_navigation',
description: 'โณ Smart waiting for page transitions and navigation completion. โ
EXCELLENT FOR: Phoenix LiveView SPA-like updates, React Router, Next.js navigation. โ ๏ธ FOR FLUTTER: Use flutter_quantum_interact with navigation commands',
inputSchema: {
type: 'object',
properties: {
sessionId: { type: 'string', description: 'Debug session ID' },
expectedUrl: { type: 'string', description: 'Expected URL pattern or exact URL' },
timeout: { type: 'number', description: 'Maximum wait time in milliseconds', default: 30000 },
transitionType: { type: 'string', enum: ['spa', 'full-reload', 'auto-detect'], default: 'auto-detect' }
},
required: ['sessionId']
}
},
{
name: 'create_workflow_template',
description: '๐ง Create reusable workflow template for complex user journeys with automatic visual validation. โ
USE FOR: Phoenix LiveView, React, Vue, Next.js, Angular, standard web apps. ๐ฏ FOR FLUTTER: Include flutterCommand in step data for automatic delegation to Flutter tools. ๐ FRAMEWORK DETECTION: Automatically detects framework and optimizes interactions. ๐ธ VISUAL VALIDATION: Automatic screenshots and AI analysis at each step',
inputSchema: {
type: 'object',
properties: {
workflow: {
type: 'object',
properties: {
name: { type: 'string', description: 'Workflow name' },
description: { type: 'string', description: 'Workflow description' },
steps: {
type: 'array',
items: {
type: 'object',
properties: {
name: { type: 'string' },
action: { type: 'string', description: 'Action type: navigate, click, fill, waitForSelector, custom, smart_interaction, take_screenshot, visual_validation' },
data: {
type: 'object',
description: 'Action data. For Flutter: include flutterCommand (e.g., "click Submit button"). For Phoenix: use phx-* selectors. For visual validation: set visualValidation:false to disable, aiVisualAnalysis:true to enable AI analysis'
},
expectedResult: { type: 'object' },
timeout: { type: 'number' }
},
required: ['name', 'action']
}
},
sessionPersistence: { type: 'boolean', default: true },
retryStrategy: { type: 'string', enum: ['none', 'step', 'workflow'], default: 'step' },
visualValidation: { type: 'boolean', default: true, description: 'Enable automatic screenshots for each step' }
},
required: ['name', 'steps']
}
},
required: ['workflow']
}
},
{
name: 'execute_user_journey',
description: '๐ Execute a complete user journey with session persistence, automatic framework detection, and visual validation. โ
UNIVERSAL: Works with all frameworks through intelligent tool selection. ๐ค AUTO-DETECTS: Flutter, Phoenix LiveView, React, Vue, Next.js, Angular and optimizes interactions accordingly. ๐ธ VISUAL EVIDENCE: Captures screenshots and provides AI analysis of each step',
inputSchema: {
type: 'object',
properties: {
sessionId: { type: 'string', description: 'Debug session ID' },
journeyName: { type: 'string', description: 'Name of workflow to execute' },
customization: {
type: 'object',
properties: {
userData: { type: 'object', description: 'User-specific data for the journey' },
skipSteps: { type: 'array', items: { type: 'string' } },
additionalSteps: { type: 'array', items: { type: 'object' } },
enableVisualValidation: { type: 'boolean', default: true, description: 'Enable visual validation and screenshots' },
enableAIAnalysis: { type: 'boolean', default: false, description: 'Enable AI-powered visual analysis' }
}
}
},
required: ['sessionId', 'journeyName']
}
},
{
name: 'create_persistent_session',
description: '๐พ Create session that persists across page navigation and reloads. โ
EXCELLENT FOR: Phoenix LiveView stateful sessions, React SPA navigation, Next.js page transitions. โ
WORKS WITH: All web frameworks including Flutter Web',
inputSchema: {
type: 'object',
properties: {
sessionId: { type: 'string', description: 'Debug session ID' },
sessionConfig: {
type: 'object',
properties: {
persistCookies: { type: 'boolean', default: true },
persistLocalStorage: { type: 'boolean', default: true },
persistSessionStorage: { type: 'boolean', default: true },
userAgent: { type: 'string', description: 'Custom user agent' },
viewport: {
type: 'object',
properties: {
width: { type: 'number', default: 1920 },
height: { type: 'number', default: 1080 }
}
}
}
}
},
required: ['sessionId']
}
},
{
name: 'handle_page_transition',
description: '๐ Handle different types of page transitions (SPA, full reload). โ
PERFECT FOR: Phoenix LiveView patch updates, React Router navigation, Next.js transitions. ๐ง AUTO-RESTORES: Session data, cookies, storage for seamless continuation',
inputSchema: {
type: 'object',
properties: {
sessionId: { type: 'string', description: 'Debug session ID' },
transitionType: { type: 'string', enum: ['spa', 'full-reload', 'auto-detect'] },
expectedBehavior: {
type: 'object',
properties: {
urlChange: { type: 'boolean' },
pageReload: { type: 'boolean' },
preserveState: { type: 'boolean' }
}
}
},
required: ['sessionId', 'transitionType']
}
},
{
name: 'take_workflow_screenshot',
description: '๐ ENHANCED: Screenshot with optional system automation. ๐ธ AI ANALYSIS + ๐ฑ๏ธ NATIVE VALIDATION: Combines browser screenshots with native system capture for maximum accuracy. ๐ CONTEXT AWARE: Framework-specific validation with pixel-perfect system validation',
inputSchema: {
type: 'object',
properties: {
sessionId: { type: 'string', description: 'Debug session ID' },
filename: { type: 'string', description: 'Screenshot filename (auto-timestamped)' },
analysisConfig: {
type: 'object',
properties: {
enableAI: { type: 'boolean', default: true, description: 'Enable AI visual analysis' },
framework: { type: 'string', description: 'Force specific framework analysis' },
expectedElements: { type: 'array', items: { type: 'string' }, description: 'CSS selectors of elements that should be present' },
customChecks: {
type: 'array',
items: {
type: 'object',
properties: {
name: { type: 'string' },
description: { type: 'string' },
script: { type: 'string', description: 'JavaScript to execute for validation' }
}
}
}
}
},
systemAutomation: {
type: 'object',
description: '๐ NEW: Native system automation integration',
properties: {
enabled: { type: 'boolean', default: false, description: 'Enable system-level screenshot capture' },
includeSystemAnalysis: { type: 'boolean', default: false, description: 'Include native visual analysis' },
crossValidation: { type: 'boolean', default: false, description: 'Cross-validate browser vs system screenshots' }
}
}
},
required: ['sessionId']
}
},
{
name: 'visual_workflow_validation',
description: '๐ Perform comprehensive visual validation of workflow state with AI analysis. ๐ฏ FRAMEWORK SPECIFIC: Optimized checks for Flutter widgets, Phoenix LiveView components, React state. ๐ DETAILED REPORTING: Visual score, element validation, error detection, recommendations',
inputSchema: {
type: 'object',
properties: {
sessionId: { type: 'string', description: 'Debug session ID' },
validationName: { type: 'string', description: 'Name for this validation check' },
validationConfig: {
type: 'object',
properties: {
expectedElements: { type: 'array', items: { type: 'string' }, description: 'Elements that must be present' },
forbiddenElements: { type: 'array', items: { type: 'string' }, description: 'Elements that should not be present' },
customChecks: {
type: 'array',
items: {
type: 'object',
properties: {
name: { type: 'string' },
description: { type: 'string' },
script: { type: 'string', description: 'JavaScript validation script' }
}
}
},
framework: { type: 'string', description: 'Override framework detection' },
tolerance: { type: 'number', default: 85, description: 'Minimum validation score (0-100)' }
}
}
},
required: ['sessionId', 'validationName']
}
},
{
name: 'save_workflow_template',
description: '๐พ Save workflow template to persistent storage for reuse. ๐ท๏ธ SUPPORTS: Tagging, categorization, versioning. ๐ REUSABLE: Across projects and team members. ๐ ORGANIZED: Framework-specific workflow collections',
inputSchema: {
type: 'object',
properties: {
workflowName: { type: 'string', description: 'Name of workflow to save' },
filePath: { type: 'string', description: 'Optional custom file path' },
metadata: {
type: 'object',
properties: {
tags: { type: 'array', items: { type: 'string' }, description: 'Tags like ["flutter", "phoenix", "reliable", "tested"]' },
category: { type: 'string', description: 'Category like "job-application", "user-registration", "testing"' },
author: { type: 'string' },
version: { type: 'string', description: 'Semantic version like "1.0.0"' },
frameworks: { type: 'array', items: { type: 'string' }, description: 'Compatible frameworks like ["Phoenix LiveView", "React"]' }
}
}
},
required: ['workflowName']
}
},
{
name: 'load_workflow_template',
description: '๐ Load workflow template from persistent storage. ๐ SMART LOADING: Automatically validates compatibility with current session framework. โก INSTANT: Ready-to-execute workflows with all metadata preserved',
inputSchema: {
type: 'object',
properties: {
workflowName: { type: 'string', description: 'Name of workflow to load' },
filePath: { type: 'string', description: 'Optional custom file path' }
},
required: ['workflowName']
}
},
{
name: 'list_saved_workflows',
description: '๐ List all saved workflow templates with metadata and filtering. ๐ FILTER BY: Category, tags, framework compatibility. ๐ SHOWS: Step count, last tested, success rates, framework compatibility',
inputSchema: {
type: 'object',
properties: {
category: { type: 'string', description: 'Filter by category like "flutter-workflows", "phoenix-workflows"' },
tags: { type: 'array', items: { type: 'string' }, description: 'Filter by tags like ["reliable", "tested", "flutter"]' },
includeDetails: { type: 'boolean', default: false, description: 'Include full workflow details' },
framework: { type: 'string', description: 'Filter by framework compatibility' }
}
}
},
{
name: 'delete_workflow_template',
description: '๐๏ธ Delete saved workflow template from storage. โ ๏ธ SAFETY: Requires explicit confirmation. ๐งน CLEANUP: Removes from both memory and persistent storage',
inputSchema: {
type: 'object',
properties: {
workflowName: { type: 'string', description: 'Name of workflow to delete' },
confirmDelete: { type: 'boolean', default: false, description: 'Confirm deletion - REQUIRED for safety' }
},
required: ['workflowName', 'confirmDelete']
}
},
{
name: 'export_workflows',
description: '๐ค Export workflows to shareable format (JSON/YAML). ๐ค TEAM SHARING: Export framework-specific workflow collections. ๐ฆ INCLUDES: All metadata, compatibility info, usage examples',
inputSchema: {
type: 'object',
properties: {
workflowNames: { type: 'array', items: { type: 'string' }, description: 'Specific workflows to export' },
format: { type: 'string', enum: ['json', 'yaml'], default: 'json' },
exportPath: { type: 'string', description: 'Custom export path' },
includeMetadata: { type: 'boolean', default: true },
filterByFramework: { type: 'string', description: 'Export only workflows for specific framework' }
}
}
},
{
name: 'import_workflows',
description: '๐ฅ Import workflows from file or URL. ๐ VALIDATES: Framework compatibility, workflow structure, dependencies. ๐ก๏ธ SAFE: Preview mode available, conflict resolution options',
inputSchema: {
type: 'object',
properties: {
source: { type: 'string', description: 'File path or URL to import from' },
overwriteExisting: { type: 'boolean', default: false, description: 'Overwrite existing workflows' },
validateOnly: { type: 'boolean', default: false, description: 'Only validate compatibility, don\'t import' },
frameworkFilter: { type: 'string', description: 'Only import workflows for specific framework' }
},
required: ['source']
}
}
];
}
async handle(tool, params, sessions) {
try {
switch (tool) {
case 'navigate_to_url':
return await this.navigateToUrl(params, sessions);
case 'wait_for_navigation':
return await this.waitForNavigation(params, sessions);
case 'create_workflow_template':
return await this.createWorkflowTemplate(params);
case 'execute_user_journey':
return await this.executeUserJourney(params, sessions);
case 'create_persistent_session':
return await this.createPersistentSession(params, sessions);
case 'handle_page_transition':
return await this.handlePageTransition(params, sessions);
case 'take_workflow_screenshot':
return await this.takeWorkflowScreenshot(params, sessions);
case 'visual_workflow_validation':
return await this.visualWorkflowValidation(params, sessions);
case 'save_workflow_template':
return await this.saveWorkflowTemplate(params);
case 'load_workflow_template':
return await this.loadWorkflowTemplate(params);
case 'list_saved_workflows':
return await this.listSavedWorkflows(params);
case 'delete_workflow_template':
return await this.deleteWorkflowTemplate(params);
case 'export_workflows':
return await this.exportWorkflows(params);
case 'import_workflows':
return await this.importWorkflows(params);
default:
throw new Error(`Unknown tool: ${tool}`);
}
}
catch (error) {
// P2 ENHANCEMENT: Use EnhancedErrorContext for actionable error messages
return EnhancedErrorContext.createActionableErrorResponse(error instanceof Error ? error.message : String(error), 'universal_workflow');
}
}
async navigateToUrl(params, sessions) {
const { url, options = {} } = params;
const { waitUntil = 'load', timeout = 30000, expectedUrl } = options;
// For standalone navigation, create a temporary session
let page;
let shouldCleanup = false;
if (params.sessionId && sessions.has(params.sessionId)) {
const session = sessions.get(params.sessionId);
page = session.page;
}
else {
// Create temporary browser instance
const { chromium } = await import('playwright');
const browser = await chromium.launch({ headless: false });
page = await browser.newPage();
shouldCleanup = true;
}
try {
const startTime = Date.now();
await page.goto(url, { waitUntil, timeout });
// Validate expected URL if provided
if (expectedUrl) {
const currentUrl = page.url();
if (!currentUrl.includes(expectedUrl) && currentUrl !== expectedUrl) {
// P2 FIX: Use EnhancedErrorContext instead of generic error
const urlMismatchError = `Navigation resulted in unexpected URL. Expected: ${expectedUrl}, Got: ${currentUrl}`;
return EnhancedErrorContext.createActionableErrorResponse(urlMismatchError, 'navigate_to_url');
}
}
const navigationTime = Date.now() - startTime;
return {
success: true,
url: page.url(),
title: await page.title(),
navigationTime,
message: `Successfully navigated to ${url}`
};
}
finally {
if (shouldCleanup) {
await page.context().browser()?.close();
}
}
}
async waitForNavigation(params, sessions) {
const { sessionId, expectedUrl, timeout = 30000, transitionType = 'auto-detect' } = params;
if (!sessions.has(sessionId)) {
// P2 FIX: Use EnhancedErrorContext for session errors
return EnhancedErrorContext.createSessionErrorResponse(sessionId);
}
const session = sessions.get(sessionId);
const page = session.page;
const startUrl = page.url();
try {
// Set up navigation promise based on transition type
let navigationPromise;
if (transitionType === 'full-reload') {
navigationPromise = page.waitForLoadState('load', { timeout });
}
else if (transitionType === 'spa') {
// For SPA, wait for URL change
navigationPromise = page.waitForURL(expectedUrl || /.+/, { timeout });
}
else {
// Auto-detect: wait for either
navigationPromise = Promise.race([
page.waitForLoadState('load', { timeout }),
page.waitForURL(/.+/, { timeout })
]);
}
await navigationPromise;
const finalUrl = page.url();
const urlChanged = startUrl !== finalUrl;
// Validate expected URL if provided
if (expectedUrl && !finalUrl.includes(expectedUrl) && finalUrl !== expectedUrl) {
// P2 FIX: Use EnhancedErrorContext instead of generic error
const urlMismatchError = `Navigation completed but URL doesn't match expected. Expected: ${expectedUrl}, Got: ${finalUrl}`;
return EnhancedErrorContext.createActionableErrorResponse(urlMismatchError, 'wait_for_navigation', sessionId);
}
return {
success: true,
startUrl,
finalUrl,
urlChanged,
transitionType: urlChanged ? (transitionType === 'auto-detect' ? 'detected-spa' : transitionType) : 'no-navigation',
message: `Navigation completed successfully`
};
}
catch (error) {
// P2 FIX: Use EnhancedErrorContext for timeout/navigation errors
return EnhancedErrorContext.createActionableErrorResponse(error instanceof Error ? error.message : String(error), 'universal_workflow');
}
}
async createWorkflowTemplate(params) {
const { workflow } = params;
// Validate workflow structure
if (!workflow.name || !workflow.steps || !Array.isArray(workflow.steps)) {
// P2 FIX: Use EnhancedErrorContext for validation errors
return EnhancedErrorContext.createActionableErrorResponse('Invalid workflow structure. Must have name and steps array.', 'create_workflow_template');
}
// Store workflow template
this.workflows.set(workflow.name, workflow);
return {
success: true,
workflowName: workflow.name,
stepCount: workflow.steps.length,
description: workflow.description,
sessionPersistence: workflow.sessionPersistence ?? true,
retryStrategy: workflow.retryStrategy ?? 'step',
message: `Workflow template '${workflow.name}' created with ${workflow.steps.length} steps`
};
}
async executeUserJourney(params, sessions) {
const { sessionId, journeyName, customization = {} } = params;
if (!sessions.has(sessionId)) {
// P2 FIX: Use EnhancedErrorContext for session errors
return EnhancedErrorContext.createSessionErrorResponse(sessionId);
}
if (!this.workflows.has(journeyName)) {
// P2 FIX: Use EnhancedErrorContext for workflow not found errors
const workflowError = `Workflow '${journeyName}' not found. Available workflows: ${Array.from(this.workflows.keys()).join(', ')}`;
return EnhancedErrorContext.createActionableErrorResponse(workflowError, 'execute_user_journey', sessionId);
}
const workflow = this.workflows.get(journeyName);
const session = sessions.get(sessionId);
const page = session.page;
const results = [];
const { userData = {}, skipSteps = [], additionalSteps = [] } = customization;
try {
// Execute workflow steps
let stepIndex = 0;
for (const step of workflow.steps) {
if (skipSteps.includes(step.name)) {
results.push({
step: step.name,
status: 'skipped',
message: 'Step skipped by customization'
});
continue;
}
try {
const stepResult = await this.executeWorkflowStep(step, page, userData);
results.push({
step: step.name,
status: 'completed',
result: stepResult,
index: stepIndex
});
}
catch (stepError) {
const errorResult = {
step: step.name,
status: 'failed',
error: stepError instanceof Error ? stepError.message : 'Unknown error',
index: stepIndex,
enhancedError: EnhancedErrorContext.enhanceError(stepError instanceof Error ? stepError.message : String(stepError), 'execute_user_journey', sessionId)
};
results.push(errorResult);
// Handle retry strategy
if (workflow.retryStrategy === 'workflow') {
// P2 FIX: Use EnhancedErrorContext for workflow failure
const workflowFailError = `Workflow failed at step '${step.name}': ${errorResult.error}`;
return EnhancedErrorContext.createActionableErrorResponse(workflowFailError, 'execute_user_journey', sessionId);
}
else if (workflow.retryStrategy === 'step') {
// Continue with next step
console.warn(`Step '${step.name}' failed but continuing with workflow`);
}
// 'none' strategy just continues
}
stepIndex++;
}
// Execute additional steps if provided
for (const additionalStep of additionalSteps) {
try {
const stepResult = await this.executeWorkflowStep(additionalStep, page, userData);
results.push({
step: additionalStep.name,
status: 'completed',
result: stepResult,
index: stepIndex,
type: 'additional'
});
}
catch (stepError) {
results.push({
step: additionalStep.name,
status: 'failed',
error: stepError instanceof Error ? stepError.message : 'Unknown error',
index: stepIndex,
type: 'additional',
enhancedError: EnhancedErrorContext.enhanceError(stepError instanceof Error ? stepError.message : String(stepError), 'execute_user_journey', sessionId)
});
}
stepIndex++;
}
const completedSteps = results.filter(r => r.status === 'completed').length;
const failedSteps = results.filter(r => r.status === 'failed').length;
const skippedSteps = results.filter(r => r.status === 'skipped').length;
return {
success: true,
workflowName: journeyName,
totalSteps: results.length,
completedSteps,
failedSteps,
skippedSteps,
results,
finalUrl: page.url(),
message: `User journey '${journeyName}' executed: ${completedSteps} completed, ${failedSteps} failed, ${skippedSteps} skipped`
};
}
catch (error) {
// P2 FIX: Use EnhancedErrorContext for journey execution errors
return EnhancedErrorContext.createActionableErrorResponse(error instanceof Error ? error.message : String(error), 'universal_workflow');
}
}
async executeWorkflowStep(step, page, userData) {
// Enhanced step execution with automatic framework detection and visual validation
// Detect framework for intelligent tool selection
const frameworkInfo = await this.detectFramework(page);
// Take "before" screenshot if visual validation enabled
let beforeScreenshot = null;
if (step.data?.visualValidation !== false) {
beforeScreenshot = await this.captureScreenshot(page, `${step.name}-before`);
}
let stepResult;
switch (step.action) {
case 'navigate':
await page.goto(step.data.url, { timeout: step.timeout || 30000 });
stepResult = { url: page.url(), title: await page.title(), framework: frameworkInfo };
break;
case 'click':
// Auto-delegate to Flutter tools if Flutter app detected
if (frameworkInfo.isFlutter && step.data.flutterCommand) {
stepResult = await this.delegateToFlutterTool('flutter_quantum_interact', {
command: step.data.flutterCommand || `click ${step.data.selector}`
});
}
else {
await page.click(step.data.selector, { timeout: step.timeout || 10000 });
stepResult = { clicked: step.data.selector, framework: frameworkInfo };
}
break;
case 'fill':
// Auto-delegate to Flutter tools if Flutter app detected
if (frameworkInfo.isFlutter && step.data.flutterCommand) {
stepResult = await this.delegateToFlutterTool('flutter_quantum_interact', {
command: step.data.flutterCommand || `type ${step.data.value || userData[step.data.dataKey]} in ${step.data.selector}`
});
}
else {
await page.fill(step.data.selector, step.data.value || userData[step.data.dataKey] || '', { timeout: step.timeout || 10000 });
stepResult = { filled: step.data.selector, value: step.data.value || userData[step.data.dataKey], framework: frameworkInfo };
}
break;
case 'wait':
await page.waitForTimeout(step.data.duration || 1000);
stepResult = { waited: step.data.duration || 1000 };
break;
case 'waitForSelector':
// Enhanced waiting with framework-aware selectors
let selector = step.data.selector;
if (frameworkInfo.isPhoenixLiveView && !selector.includes('phx-') && !selector.includes('[data-testid')) {
console.warn(`Phoenix LiveView detected but selector "${selector}" doesn't use phx-* or data-testid attributes. Consider using framework-specific selectors.`);
}
await page.waitForSelector(selector, { timeout: step.timeout || 10000 });
stepResult = { selector, found: true, framework: frameworkInfo };
break;
case 'custom':
// Allow custom JavaScript execution with framework context
if (step.data.script) {
const result = await page.evaluate(step.data.script);
stepResult = { customResult: result, framework: frameworkInfo };
}
else if (step.data.flutterTool && frameworkInfo.isFlutter) {
stepResult = await this.delegateToFlutterTool(step.data.flutterTool, step.data);
}
else {
stepResult = { message: 'Custom step executed', framework: frameworkInfo };
}
break;
case 'smart_interaction':
// New smart interaction that auto-selects best approach
stepResult = await this.smartInteraction(step.data, page, frameworkInfo);
break;
case 'take_screenshot':
// Dedicated screenshot action
const screenshot = await this.captureScreenshot(page, step.data.filename || step.name);
stepResult = { screenshot, message: 'Screenshot captured successfully' };
break;
case 'visual_validation':
// AI-powered visual validation
stepResult = await this.performVisualValidation(page, step.data, frameworkInfo);
break;
default:
throw new Error(`Unknown step action: ${step.action}`);
}
// Take "after" screenshot if visual validation enabled
let afterScreenshot = null;
if (step.data?.visualValidation !== false && step.action !== 'take_screenshot') {
afterScreenshot = await this.captureScreenshot(page, `${step.name}-after`);
}
// Add visual context to step result
if (beforeScreenshot || afterScreenshot) {
stepResult.visualEvidence = {
before: beforeScreenshot,
after: afterScreenshot,
stepName: step.name,
timestamp: new Date().toISOString()
};
// AI visual analysis if enabled
if (step.data?.aiVisualAnalysis) {
stepResult.aiAnalysis = await this.analyzeVisualChange(beforeScreenshot, afterScreenshot, step, frameworkInfo);
}
}
return stepResult;
}
async detectFramework(page) {
try {
const frameworkInfo = await page.evaluate(() => {
const url = window.location.href;
const html = document.documentElement.outerHTML;
// Flutter Detection
const isFlutter = Boolean(
// Flutter-specific elements
document.querySelector('flutter-view') ||
document.querySelector('[flt-renderer]') ||
html.includes('flutter') ||
// Flutter configuration
window.flutterConfiguration ||
// Canvas-heavy rendering (common in Flutter)
document.querySelectorAll('canvas').length > 2 ||
// URL patterns
url.includes('flutter') || url.includes('dart'));
// Phoenix LiveView Detection
const isPhoenixLiveView = Boolean(
// LiveView-specific attributes
document.querySelector('[phx-click]') ||
document.querySelector('[phx-submit]') ||
document.querySelector('[phx-change]') ||
document.querySelector('[phx-hook]') ||
// LiveView socket
window.liveSocket ||
// LiveView metadata
document.querySelector('meta[name="csrf-token"]') &&
(html.includes('phx-') || html.includes('live_')));
// React Detection
const isReact = Boolean(document.querySelector('[data-reactroot]') ||
document.querySelector('#root') ||
window.React ||
html.includes('react'));
// Vue Detection
const isVue = Boolean(document.querySelector('[data-v-]') ||
window.Vue ||
html.includes('vue'));
// Next.js Detection
const isNextJS = Boolean(document.querySelector('#__next') ||
window.__NEXT_DATA__ ||
html.includes('_next'));
// Angular Detection
const isAngular = Boolean(document.querySelector('[ng-version]') ||
window.ng ||
html.includes('angular'));
return {
isFlutter,
isPhoenixLiveView,
isReact,
isVue,
isNextJS,
isAngular,
url,
detectedAt: new Date().toISOString()
};
});
return frameworkInfo;
}
catch (error) {
console.warn('Framework detection failed:', error);
return {
isFlutter: false,
isPhoenixLiveView: false,
isReact: false,
isVue: false,
isNextJS: false,
isAngular: false,
url: page.url(),
detectedAt: new Date().toISOString(),
detectionError: error instanceof Error ? error.message : 'Unknown error'
};
}
}
async delegateToFlutterTool(toolName, params) {
try {
// This would delegate to actual Flutter tools in a real implementation
// For now, return a simulation of Flutter tool delegation
console.log(`๐ Delegating to Flutter tool: ${toolName}`, params);
return {
success: true,
delegatedTo: toolName,
params,
message: `Successfully delegated to Flutter tool: ${toolName}`,
// In real implementation, this would call the actual Flutter tool
simulatedResult: {
toolUsed: toolName,
frameworkDetected: 'Flutter Web',
action: 'delegated_to_flutter_tools'
}
};
}
catch (error) {
// P2 FIX: Use EnhancedErrorContext for delegation errors
return EnhancedErrorContext.createActionableErrorResponse(error instanceof Error ? error.message : String(error), 'universal_workflow');
}
}
async smartInteraction(stepData, page, frameworkInfo) {
const { action, target, value } = stepData;
try {
if (frameworkInfo.isFlutter) {
// Use Flutter-specific interaction
let command = '';
if (action === 'click') {
command = `click ${target}`;
}
else if (action === 'fill') {
command = `type ${value} in ${target}`;
}
else {
command = `${action} ${target}`;
}
return await this.delegateToFlutterTool('flutter_quantum_interact', { command });
}
else {
// Use standard web interaction
if (action === 'click') {
await page.click(target);
return { action: 'click', target, framework: 'standard-web' };
}
else if (action === 'fill') {
await page.fill(target, value);
return { action: 'fill', target, value, framework: 'standard-web' };
}
else {
throw new Error(`Unsupported smart interaction action: ${action}`);
}
}
}
catch (error) {
// P2 FIX: Use EnhancedErrorContext for smart interaction errors
return EnhancedErrorContext.createActionableErrorResponse(error instanceof Error ? error.message : String(error), 'universal_workflow');
}
}
async captureScreenshot(page, filename) {
try {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const screenshotPath = join(process.cwd(), '.ai-debug', 'screenshots', `${filename}-${timestamp}.png`);
// Ensure screenshot directory exists
const screenshotDir = join(process.cwd(), '.ai-debug', 'screenshots');
if (!existsSync(screenshotDir)) {
await mkdir(screenshotDir, { recursive: true });
}
const screenshotBuffer = await page.screenshot({
path: screenshotPath,
fullPage: true,
type: 'png'
});
// Get page context for AI analysis
const pageContext = await page.evaluate(() => ({
url: window.location.href,
title: document.title,
viewport: {
width: window.innerWidth,
height: window.innerHeight
},
elementCount: document.querySelectorAll('*').length,
hasErrors: window.console ? console.error.length > 0 : false
}));
return {
success: true,
path: screenshotPath,
filename: `${filename}-${timestamp}.png`,
size: screenshotBuffer.length,
pageContext,
timestamp: new Date().toISOString(),
dimensions: await page.evaluate(() => ({
width: document.documentElement.scrollWidth,
height: document.documentElement.scrollHeight
}))
};
}
catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
filename,
timestamp: new Date().toISOString()
};
}
}
async performVisualValidation(page, validationConfig, frameworkInfo) {
try {
const screenshot = await this.captureScreenshot(page, validationConfig.name || 'validation');
const validationChecks = [];
// Framework-specific visual checks
if (frameworkInfo.isFlutter) {
validationChecks.push({
type: 'flutter_widget_visibility',
status: 'checking',
description: 'Verifying Flutter widgets are rendered properly'
});
}
else if (frameworkInfo.isPhoenixLiveView) {
validationChecks.push({
type: 'liveview_mount_status',
status: 'checking',
description: 'Verifying LiveView components are mounted'
});
}
// Generic visual checks
validationChecks.push({
type: 'page_loaded',
status: await this.checkPageLoaded(page) ? 'passed' : 'failed',
description: 'Page fully loaded without errors'
}, {
type: 'no_console_errors',
status: await this.checkConsoleErrors(page) ? 'passed' : 'failed',
description: 'No JavaScript console errors'
}, {
type: 'expected_elements_present',
status: validationConfig.expectedElements ?
await this.checkExpectedElements(page, validationConfig.expectedElements) ? 'passed' : 'failed' : 'skipped',
description: 'Expected UI elements are present'
});
// Custom validation checks
if (validationConfig.customChecks) {
for (const customCheck of validationConfig.customChecks) {
const result = await page.evaluate(customCheck.script);
validationChecks.push({
type: 'custom',
name: customCheck.name,
status: result ? 'passed' : 'failed',
description: customCheck.description,
result
});
}
}
const passedChecks = validationChecks.filter(c => c.status === 'passed').length;
const failedChecks = validationChecks.filter(c => c.status === 'failed').length;
return {
success: true,
screenshot,
validationChecks,
summary: {
total: validationChecks.length,
passed: passedChecks,
failed: failedChecks,
score: Math.round((passedChecks / validationChecks.length) * 100)
},
framework: frameworkInfo,
message: `Visual validation completed: ${passedChecks}/${validationChecks.length} checks passed`
};
}
catch (error) {
// P2 FIX: Use EnhancedErrorContext for validation errors
return EnhancedErrorContext.createActionableErrorResponse(error instanceof Error ? error.message : String(error), 'universal_workflow');
}
}
async analyzeVisualChange(before, after, step, frameworkInfo) {
try {
if (!before || !after) {
return {
hasVisualChange: false,
confidence: 0,
message: 'Insufficient screenshots for comparison',
framework: frameworkInfo.isFlutter ? 'Flutter' : frameworkInfo.isPhoenixLiveView ? 'Phoenix LiveView' : 'Standard Web'
};
}
// Simple visual change detection (in production, this would use computer vision)
const sizeDifference = Math.abs((after.size || 0) - (before.size || 0));
const hasSignificantChange = sizeDifference > 1000; // Basic heuristic
// Framework-specific analysis
let frameworkAnalysis = '';
if (frameworkInfo.isFlutter) {
frameworkAnalysis = 'Flutter app interaction detected - widget state likely changed';
}
else if (frameworkInfo.isPhoenixLiveView) {
frameworkAnalysis = 'Phoenix LiveView interaction - server state update expected';
}
else {
frameworkAnalysis = 'Standard web interaction - DOM changes expected';
}
// Action-specific expectations
let expectedChange = '';
switch (step.action) {
case 'click':
expectedChange = 'Button click should trigger visual feedback (navigation, modal, state change)';
break;
case 'fill':
expectedChange = 'Form input should show typed content and potentially validation messages';
break;
case 'navigate':
expectedChange = 'Page navigation should show completely different content';
break;
default:
expectedChange = 'Some visual change expected based on user action';
}
return {
hasVisualChange: hasSignificantChange,
confidence: hasSignificantChange ? 75 : 25,
sizeDifference,
beforeSize: before.size || 0,
afterSize: after.size || 0,
frameworkAnalysis,
expectedChange,
stepAction: step.action,
message: hasSignificantChange ?
'Significant visual change detected - action likely successful' :
'Minimal visual change - verify action completed as expected',
framework: frameworkInfo.isFlutter ? 'Flutter' : frameworkInfo.isPhoenixLiveView ? 'Phoenix LiveView' : 'Standard Web',
recommendations: this.generateVisualRecommendations(step, frameworkInfo, hasSignificantChange)
};
}
catch (error) {
return {
hasVisualChange: false,
confidence: 0,
error: error instanceof Error ? error.message : 'Unknown error',
message: 'Visual analysis failed',
framework: 'Unknown'
};
}
}
generateVisualRecommendations(step, frameworkInfo, hasChange) {
const recommendations = [];
if (!hasChange) {
recommendations.push('โ ๏ธ No significant visual change detected');
if (frameworkInfo.isFlutter) {
recommendations.push('๐ฏ For Flutter: Verify widget selector is correct, try flutter_quantum_find to locate elements');
recommendations.push('๐ Check if Flutter app is in Canvas rendering mode (limits DOM-based interactions)');
}
else if (frameworkInfo.isPhoenixLiveView) {
recommendations.push('๐ฏ For Phoenix LiveView: Ensure phx-* attributes are used, check WebSocket connection');
recommendations.push('โฑ๏ธ LiveView updates may have timing delays - consider adding wait steps');
}
else {
recommendations.push('๐ฏ For standard web: Verify CSS selector exists and element is interactive');
recommendations.push('โฑ๏ธ Consider adding wait for dynamic content or async operations');
}
if (step.action === 'click') {
recommendations.push('๐ฑ๏ธ Click action: Verify button is enabled, not hidden, and click event is properly bound');
}
else if (step.action === 'fill') {
recommendations.push('๐ Fill action: Ensure input field is visible, enabled, and accepts the provided value');
}
}
else {
recommendations.push('โ
Visual change detected - action appears successful');
recommendations.push('๐ธ Consider adding validation step to verify expected outcome');
}
return recommendations;
}
async checkPageLoaded(page) {
try {
return await page.evaluate(() => {
return document.readyState === 'complete' &&
!document.querySelector('[aria-busy="true"]') &&
!document.querySelector('.loading') &&
!document.querySelector('[data-loading="true"]');
});
}
catch {
return false;
}
}
async checkConsoleErrors(page) {
try {
// This would need to be implemented with page.on('console') listener
// For now, return true as placeholder
return true;
}
catch {
return false;
}
}
async checkExpectedElements(page, expectedElements) {
try {
for (const selector of expectedElements) {
const element = await page.$(selector);
if (!element)
return false;
}
return true;
}
catch {
return false;
}
}
async takeWorkflowScreenshot(params, sessions) {
const { sessionId, filename, analysisConfig = {} } = params;
if (!sessions.has(sessionId)) {
// P2 FIX: Use EnhancedErrorContext for session errors
return EnhancedErrorContext.createSessionErrorResponse(sessionId);
}
const session = sessions.get(sessionId);
const page = session.page;
try {
// Capture screenshot
const screenshot = await this.captureScreenshot(page, filename || 'workflow-screenshot');
if (!screenshot.success) {
return screenshot;
}
// Perform analysis if enabled
let analysis = null;
if (analysisConfig.enableAI !== false) {
const frameworkInfo = await this.detectFramework(page);
analysis = await this.performVisualValidation(page, {
name: filename || 'screenshot-analysis',
expectedElements: analysisConfig.expectedElements,
customChecks: analysisConfig.customChecks
}, frameworkInfo);
}
return {
success: true,
screenshot,
analysis,
sessionId,
message: 'Workflow screenshot captured successfully'
};
}
catch (error) {
// P2 FIX: Use EnhancedErrorContext for screenshot errors
return EnhancedErrorContext.createActionableErrorResponse(error instanceof Error ? error.message : String(error), 'universal_workflow');
}
}
async visualWorkflowValidation(params, sessions) {
const { sessionId, validationName, validationConfig = {} } = params;
if (!sessions.has(sessionId)) {
// P2 FIX: Use EnhancedErrorContext for session errors
return EnhancedErrorContext.createSessionErrorResponse(sessionId);
}
const session = sessions.get(sessionId);
const page = session.page;
try {
const frameworkInfo = await this.detectFramework(page);
// Perform comprehensive visual validation
const validation = await this.performVisualValidation(page, {
name: validationName,
...validationConfig
}, frameworkInfo);
// Check for forbidden elements
if (validationConfig.forbiddenElements) {
for (const selector of validationConfig.forbiddenElements) {
const element = await page.$(selector);
if (element) {
validation.validationChecks.push({
type: 'forbidden_element_check',
status: 'failed',
description: `Forbidden element found: ${selector}`
});
}
}
}
// Calculate final score
const passedChecks = validation.validationChecks.filter((c) => c.status === 'passed').length;
const totalChecks = validation.validationChecks.length;
const score = totalChecks > 0 ? Math.round((passedChecks / totalChecks) * 100) : 0;
const meetsThreshold = score >= (validationConfig.tolerance || 85);
return {
success: true,
validationName,
score,
meetsThreshold,
threshold: validationConfig.tolerance || 85,
validation,
framework: frameworkInfo,
recommendations: meetsThreshold ?
['โ
Validation passed - workflow state is as expected'] :
[
'โ ๏ธ Validation below threshold - review failed checks',
'๐ Consider adjusting selectors or validation criteria',
'๐ธ Review screenshot for visual confirmation'
],
sessionId,
message: `Visual validation ${meetsThreshold ? 'PASSED' : 'FAILED'}: ${score}% (threshold: ${validationConfig.tolerance || 85}%)`
};
}
catch (error) {
// P2 FIX: Use EnhancedErrorContext for validation errors
return EnhancedErrorContext.createActionableErrorResponse(error instanceof Error ? error.message : String(error), 'universal_workflow');
}
}
async createPersistentSession(params, sessions) {
const { sessionId, sessionConfig = {} } = params;
if (!sessions.has(sessionId)) {
// P2 FIX: Use EnhancedErrorContext for session errors
return EnhancedErrorContext.createSessionErrorResponse(sessionId);
}
const session = sessions.get(sessionId);
const page = session.page;
try {
// Capture current session state
const sessionData = {};
if (sessionConfig.persistCookies !== false) {
sessionData.cookies = await page.context().cookies();
}
if (sessionConfig.persistLocalStorage !== false) {
sessionData.localStorage = await page.evaluate(() => {
const storage = {};
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key)
storage[key] = localStorage.getItem(key) || '';
}
return storage;
});
}
if (sessionConfig.persistSessionStorage !== false) {
sessionData.sessionStorage = await page.evaluate(() => {
const storage = {};
for (let i = 0; i < sessionStorage.length; i++) {
const key = sessionStorage.key(i);
if (key)
storage[key] = sessionStorage.getItem(key) || '';
}
return storage;
});
}
if (sessionConfig.userAgent) {
sessionData.userAgent = sessionConfig.userAgent;
await page.setExtraHTTPHeaders({ 'User-Agent': sessionConfig.userAgent });
}
if (sessionConfig.viewport) {
sessionData.viewport = sessionConfig.viewport;
await page.setViewportSize(sessionConfig.viewport);
}
// Store session data for restoration
this.sessionData.set(sessionId, sessionData);
return {
success: true,
sessionId,
persistedData: {
cookies: sessionData.cookies?.length || 0,
localStorageKeys: Object.keys(sessionData.localStorage || {}).length,
sessionStorageKeys: Object.keys(sessionData.sessionStorage || {}).length,
userAgent: !!sessionData.userAgent,
viewport: !!sessionData.viewport
},
message: `Persistent session created for ${sessionId}`
};
}
catch (error) {
// P2 FIX: Use EnhancedErrorContext for session creation errors
return EnhancedErrorContext.createActionableErrorResponse(error instanceof Error ? error.message : String(error), 'universal_workflow');
}
}
async handlePageTransition(params, sessions) {
const { sessionId, transitionType, expectedBehavior = {} } = params;
if (!sessions.has(sessionId)) {
// P2 FIX: Use EnhancedErrorContext for session errors
return EnhancedErrorContext.createSessionErrorResponse(sessionId);
}
const session = sessions.get(sessionId);
const page = session.page;
const startUrl = page.url();
// Restore session data if available
if (this.sessionData.has(sessionId)) {
const sessionData = this.sessionData.get(sessionId);
try {
// Restore cookies
if (sessionData.cookies) {
await page.context().addCookies(sessionData.cookies);
}
// Restore localStorage
if (sessionData.localStorage) {
await page.evaluate((storage) => {
for (const [key, value] of Object.entries(storage)) {
localStorage.setItem(key, value);
}
}, sessionData.localStorage);
}
// Restore sessionStorage
if (sessionData.sessionStorage) {
await page.evaluate((storage) => {
for (const [key, value] of Object.entries(storage)) {
sessionStorage.setItem(key, value);
}
}, sessionData.sessionStorage);
}
}
catch (error) {
console.warn('Failed to restore some session data:', error);
}
}
return {
success: true,
sessionId,
startUrl,
currentUrl: page.url(),
transitionType,
sessionRestored: this.sessionData.has(sessionId),
message: `Page transition handled for ${transitionType} navigation`
};
}
// Storage and workflow management methods
async initializeStorage() {
try {
if (!existsSync(this.workflowStoragePath)) {
await mkdir(this.workflowStoragePath, { recursive: true });
}
// Load existing workflows from storage
await this.loadAllWorkflowsFromStorage();
}
catch (error) {
console.warn('Failed to initialize workflow storage:', error);
}
}
async loadAllWorkflowsFromStorage() {
try {
const { readdir } = await import('fs/promises');
const files = await readdir(this.workflowStoragePath);
for (const file of files) {
if (file.endsWith('.json')) {
try {
const workflowName = file.replace('.json', '');
const filePath = join(this.workflowStoragePath, file);
const content = await readFile(filePath, 'utf-8');
const workflowData = JSON.parse(content);
this.workflows.set(workflowName, workflowData.workflow);
}
catch (error) {
console.warn(`Failed to load workflow ${file}:`, error);
}
}
}
console.log(`๐ Loaded ${this.workflows.size} saved workflows from storage`);
}
catch (error) {
// Storage directory doesn't exist yet - this is fine
}
}
async saveWorkflowTemplate(params) {
const { workflowName, filePath, metadata = {} } = params;
if (!this.workflows.has(workflowName)) {
// P2 FIX: Use EnhancedErrorContext for workflow not found errors
const notFoundError = `Workflow '${workflowName}' not found in memory. Create it first with create_workflow_template.`;
return EnhancedErrorContext.createActionableErrorResponse(notFoundError, 'save_workflow_template');
}
try {
const workflow = this.workflows.get(workflowName);
const workflowData = {
workflow,
metadata: {
...metadata,
savedAt: new Date().toISOString(),
version: metadata.version || '1.0.0'
}
};
const targetPath = filePath || join(this.workflowStoragePath, `${workflowName}.json`);
await writeFile(targetPath, JSON.stringify(workflowData, null, 2), 'utf-8');
return {
success: true,
workflowName,
savedPath: targetPath,
metadata: workflowData.metadata,
message: `Workflow '${workflowName}' saved successfully to ${targetPath}`
};
}
catch (error) {
// P2 FIX: Use EnhancedErrorContext for save errors
return EnhancedErrorContext.createActionableErrorResponse(error instanceof Error ? error.message : String(error), 'universal_workflow');
}
}
async loadWorkflowTemplate(params) {
const { workflowName, filePath } = params;
try {
const targetPath = filePath || join(this.workflowStoragePath, `${workflowName}.json`);
if (!existsSync(targetPath)) {
// P2 FIX: Use EnhancedErrorContext for file not found errors
const fileNotFoundError = `Workflow file not found: ${targetPath}`;
return EnhancedErrorContext.createActionableErrorResponse(fileNotFoundError, 'load_workflow_template');
}
const content = await readFile(targetPath, 'utf-8');
const workflowData = JSON.parse(content);
// Load into memory
this.workflows.set(workflowName, workflowData.workflow);
return {
success: true,
workflowName,
loadedFrom: targetPath,
workflow: workflowData.workflow,
metadata: workflowData.metadata,
stepCount: workflowData.workflow.steps.length,
message: `Workflow '${workflowName}' loaded successfully from ${targetPath}`
};
}
catch (error) {
// P2 FIX: Use EnhancedErrorContext for load errors
return EnhancedErrorContext.createActionableErrorResponse(error instanceof Error ? error.message : String(error), 'universal_workflow');
}
}
async listSavedWorkflows(params = {}) {
const { category, tags, includeDetails = false } = params;
try {
const { readdir } = await import('fs/promises');
const files = await readdir(this.workflowStoragePath);
const workflows = [];
for (const file of files) {
if (file.endsWith('.json')) {
try {
const workflowName = file.replace('.json', '');
const filePath = join(this.workflowStoragePath, file);
const content = await readFile(filePath, 'utf-8');
const workflowData = JSON.parse(content);
// Apply filters
if (category && workflowData.metadata?.category !== category)
continue;
if (tags && tags.length > 0) {
const workflowTags = workflowData.metadata?.tags || [];
if (!tags.some((tag) => workflowTags.includes(tag)))
continue;
}
const workflowInfo = {
name: workflowName,
description: workflowData.workflow.description,
stepCount: workflowData.workflow.steps.length,
metadata: workflowData.metadata,
filePath
};
if (includeDetails) {
workflowInfo.workflow = workflowData.workflow;
}
workflows.push(workflowInfo);
}
catch (error) {
console.warn(`Failed to read workflow ${file}:`, error);
}
}
}
return {
success: true,
count: workflows.length,
workflows,
filters: { category, tags },
message: `Found ${workflows.length} saved workflows`
};
}
catch (error) {
return {
success: true,
count: 0,
workflows: [],
message: 'No saved workflows found (storage directory does not exist yet)'
};
}
}
async deleteWorkflowTemplate(params) {
const { workflowName, confirmDelete } = params;
if (!confirmDelete) {
// P2 FIX: Use EnhancedErrorContext for confirmation errors
const confirmationError = 'Delete confirmation required. Set confirmDelete: true to proceed.';
return EnhancedErrorContext.createActionableErrorResponse(confirmationError, 'delete_workflow_template');
}
try {
const filePath = join(this.workflowStoragePath, `${workflowName}.json`);
if (!existsSync(filePath)) {
// P2 FIX: Use EnhancedErrorContext for file not found errors
const fileNotFoundError = `Workflow file not found: ${filePath}`;
return EnhancedErrorContext.createActionableErrorResponse(fileNotFoundError, 'delete_workflow_template');
}
const { unlink } = await import('fs/promises');
await unlink(filePath);
// Remove from memory if loaded
this.workflows.delete(workflowName);
return {
success: true,
workflowName,
deletedPath: filePath,
message: `Workflow '${workflowName}' deleted successfully`
};
}
catch (error) {
// P2 FIX: Use EnhancedErrorContext for deletion errors
return EnhancedErrorContext.createActionableErrorResponse(error instanceof Error ? error.message : String(error), 'universal_workflow');
}
}
async exportWorkflows(params = {}) {
const { workflowNames, format = 'json', exportPath, includeMetadata = true } = params;
try {
const workflowsToExport = {};
const workflowList = workflowNames || Array.from(this.workflows.keys());
// Collect workflows to export
for (const workflowName of workflowList) {
if (this.workflows.has(workflowName)) {
const workflow = this.workflows.get(workflowName);
workflowsToExport[workflowName] = {
workflow,
exportedAt: new Date().toISOString()
};
// Include metadata if available and requested
if (includeMetadata) {
try {
const filePath = join(this.workflowStoragePath, `${workflowName}.json`);
if (existsSync(filePath)) {
const content = await readFile(filePath, 'utf-8');
const workflowData = JSON.parse(content);
workflowsToExport[workflowName].metadata = workflowData.metadata;
}
}
catch (error) {
// Metadata not available - continue without it
}
}
}
}
const exportData = {
exportInfo: {
exportedAt: new Date().toISOString(),
format,
workflowCount: Object.keys(workflowsToExport).length,
version: '1.0.0'
},
workflows: workflowsToExport
};
let exportContent;
let fileName;
if (format === 'yaml') {
// Simple YAML export (basic implementation)
exportContent = this.convertToYaml(exportData);
fileName = 'workflows-export.yaml';
}
else {
exportContent = JSON.stringify(exportData, null, 2);
fileName = 'workflows-export.json';
}
const finalExportPath = exportPath || join(process.cwd(), fileName);
await writeFile(finalExportPath, exportContent, 'utf-8');
return {
success: true,
exportPath: finalExportPath,
format,
workflowCount: Object.keys(workflowsToExport).length,
workflowNames: Object.keys(workflowsToExport),
message: `Exported ${Object.keys(workflowsToExport).length} workflows to ${finalExportPath}`
};
}
catch (error) {
// P2 FIX: Use EnhancedErrorContext for export errors
return EnhancedErrorContext.createActionableErrorResponse(error instanceof Error ? error.message : String(error), 'universal_workflow');
}
}
async importWorkflows(params) {
const { source, overwriteExisting = false, validateOnly = false } = params;
try {
let importContent;
// Handle URL vs file path
if (source.startsWith('http://') || source.startsWith('https://')) {
// Import from URL
const response = await fetch(source);
if (!response.ok) {
// P2 FIX: Use EnhancedErrorContext for network errors
const fetchError = `Failed to fetch from URL: ${response.statusText}`;
return EnhancedErrorContext.createActionableErrorResponse(fetchError, 'import_workflows');
}
importContent = await response.text();
}
else {
// Import from file
if (!existsSync(source)) {
// P2 FIX: Use EnhancedErrorContext for file not found errors
const fileNotFoundError = `Import file not found: ${source}`;
return EnhancedErrorContext.createActionableErrorResponse(fileNotFoundError, 'import_workflows');
}
importContent = await readFile(source, 'utf-8');
}
// Parse content
let importData;
try {
if (source.endsWith('.yaml') || source.endsWith('.yml')) {
// Basic YAML parsing (simplified)
importData = this.parseYaml(importContent);
}
else {
importData = JSON.parse(importContent);
}
}
catch (error) {
// P2 FIX: Use EnhancedErrorContext for parsing errors
const parseError = `Failed to parse import content: ${error instanceof Error ? error.message : 'Invalid format'}`;
return EnhancedErrorContext.createActionableErrorResponse(parseError, 'import_workflows');
}
// Validate structure
if (!importData.workflows || typeof importData.workflows !== 'object') {
// P2 FIX: Use EnhancedErrorContext for structure validation errors
const structureError = 'Invalid import format: missing workflows object';
return EnhancedErrorContext.createActionableErrorResponse(structureError, 'import_workflows');
}
const importResults = [];
const workflowNames = Object.keys(importData.workflows);
for (const workflowName of workflowNames) {
const workflowData = importData.workflows[workflowName];
try {
// Validate workflow structure
if (!workflowData.workflow || !workflowData.workflow.steps) {
throw new Error('Invalid workflow structure');
}
const exists = this.workflows.has(workflowName);
if (exists && !overwriteExisting) {
importResults.push({
name: workflowName,
status: 'skipped',
reason: 'Workflow exists and overwrite not enabled'
});
continue;
}
if (!validateOnly) {
// Import the workflow
this.workflows.set(workflowName, workflowData.workflow);
// Save to storage
const workflowFileData = {
workflow: workflowData.workflow,
metadata: {
...workflowData.metadata,
importedAt: new Date().toISOString(),
importedFrom: source
}
};
const filePath = join(this.workflowStoragePath, `${workflowName}.json`);
await writeFile(filePath, JSON.stringify(workflowFileData, null, 2), 'utf-8');
}
importResults.push({
name: workflowName,
status: validateOnly ? 'valid' : (exists ? 'updated' : 'imported'),
stepCount: workflowData.workflow.steps.length
});
}
catch (error) {
importResults.push({
name: workflowName,
status: 'failed',
error: error instanceof Error ? error.message : 'Unknown error'
});
}
}
const successCount = importResults.filter(r => ['imported', 'updated', 'valid'].includes(r.status)).length;
return {
success: true,
source,
validateOnly,
totalWorkflows: workflowNames.length,
successCount,
results: importResults,
message: validateOnly
? `Validated ${successCount}/${workflowNames.length} workflows`
: `Imported ${successCount}/${workflowNames.length} workflows successfully`
};
}
catch (error) {
// P2 FIX: Use EnhancedErrorContext for import errors
return EnhancedErrorContext.createActionableErrorResponse(error instanceof Error ? error.message : String(error), 'universal_workflow');
}
}
// Utility methods for YAML conversion (basic implementation)
convertToYaml(data) {
// Basic YAML conversion - in production you'd use a proper YAML library
return `# AI-Debug Workflows Export\n# Generated at: ${new Date().toISOString()}\n\n${JSON.stringify(data, null, 2)}`;
}
parseYaml(content) {
// Basic YAML parsing - in production you'd use a proper YAML library
// For now, try to parse as JSON if it looks like JSON
const trimmed = content.trim();
if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
return JSON.parse(trimmed);
}
throw new Error('YAML parsing not implemented - use JSON format');
}
}
//# sourceMappingURL=universal-workflow-handler.js.map