UNPKG

chromancer

Version:

A powerful command-line interface for automating Chrome browser using Playwright. Perfect for web scraping, automation, testing, and browser workflows.

370 lines (365 loc) 18 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.WorkflowExecutor = void 0; const data_formatter_js_1 = require("./data-formatter.js"); const selector_normalizer_js_1 = require("./selector-normalizer.js"); const dom_inspector_js_1 = require("./dom-inspector.js"); class WorkflowExecutor { page; stepResults = []; originalPrompt; constructor(page, originalPrompt) { this.page = page; this.originalPrompt = originalPrompt; } async execute(workflow, options = {}) { const startTime = Date.now(); let successCount = 0; let failureCount = 0; // Clear previous results this.stepResults = []; for (const [index, step] of workflow.entries()) { const stepNumber = index + 1; const command = Object.keys(step)[0]; const args = this.replaceVariables(step[command], options.variables || {}); const stepStartTime = Date.now(); let success = false; let output; let error; // Notify step start if (options.onStepStart) { options.onStepStart(stepNumber, command, args); } try { output = await this.executeStep(command, args, options); success = true; successCount++; } catch (err) { error = err instanceof Error ? err.message : String(err); // Add selector-specific help if this looks like a selector error if (error.includes('selector') || error.includes('waitForSelector') || error.includes('No element matches')) { const selector = this.extractSelectorFromArgs(args); if (selector) { const suggestions = (0, selector_normalizer_js_1.suggestSelectorFix)(selector, error); if (suggestions.length > 0) { error += '\n' + suggestions.map(s => ` 💡 ${s}`).join('\n'); } } } failureCount++; if (options.strict) { // Still capture the result before throwing this.stepResults.push({ stepNumber, command, args, success: false, error, duration: Date.now() - stepStartTime }); throw new Error(`Workflow failed at step ${stepNumber}: ${error}`); } } this.stepResults.push({ stepNumber, command, args, success, output, error, duration: Date.now() - stepStartTime }); // Notify step complete if (options.onStepComplete) { options.onStepComplete(stepNumber, command, success); } // Add small delay between steps await this.page.waitForTimeout(100); } return { success: failureCount === 0, totalSteps: workflow.length, successfulSteps: successCount, failedSteps: failureCount, steps: this.stepResults, totalDuration: Date.now() - startTime }; } async executeStep(command, args, options) { const timeout = options.timeout || 5000; // Changed from 30s to 5s let output; switch (command) { case 'navigate': case 'goto': const url = typeof args === 'string' ? args : args.url; await this.page.goto(url, { waitUntil: args.waitUntil || 'load', timeout }); // Perform quick DOM scan after navigation try { const inspector = new dom_inspector_js_1.DOMInspector(this.page); const inspection = await inspector.inspectForDataExtraction(''); const pageContext = [`Navigated to ${url}`]; // Add current page info pageContext.push(`\nPAGE CONTEXT:`); pageContext.push(` URL: ${this.page.url()}`); pageContext.push(` Title: ${await this.page.title()}`); // Check for common issues const pageChecks = await this.page.evaluate(() => { return { hasModals: !!document.querySelector('[role="dialog"], .modal, .popup, [class*="modal"][style*="display: block"]'), hasOverlay: !!document.querySelector('.overlay:not([style*="display: none"]), .backdrop:not([style*="display: none"])'), errorMessages: Array.from(document.querySelectorAll('.error:not([style*="display: none"]), .alert-danger:not([style*="display: none"])')).slice(0, 2).map(el => el.textContent?.trim()).filter(Boolean) }; }); if (pageChecks.hasModals || pageChecks.hasOverlay) { pageContext.push(`\n⚠️ WARNINGS:`); if (pageChecks.hasModals) pageContext.push(` - Modal/dialog detected`); if (pageChecks.hasOverlay) pageContext.push(` - Overlay detected`); } if (pageChecks.errorMessages.length > 0) { pageContext.push(`\n❌ ERRORS:`); pageChecks.errorMessages.forEach(msg => pageContext.push(` - "${msg}"`)); } // Show available elements if (inspection.structure.buttons.length > 0) { pageContext.push(`\nAVAILABLE BUTTONS:`); inspection.structure.buttons.slice(0, 5).forEach(btn => { pageContext.push(` - ${btn.selector}: "${btn.text}"`); }); } const visibleInputs = inspection.structure.inputs.filter(i => i.visible !== false); if (visibleInputs.length > 0) { pageContext.push(`\nAVAILABLE INPUTS:`); visibleInputs.slice(0, 5).forEach(input => { const desc = input.placeholder || input.name || input.type; pageContext.push(` - ${input.selector}: ${desc}`); }); } output = pageContext.join('\n'); } catch (scanError) { // If scan fails, just use basic navigation message output = `Navigated to ${url}`; } break; case 'click': const rawClickSelector = typeof args === 'string' ? args : args.selector; const clickSelector = (0, selector_normalizer_js_1.normalizeSelector)(rawClickSelector); if (!(0, selector_normalizer_js_1.isValidSelector)(clickSelector)) { throw new Error(`Invalid selector: ${(0, selector_normalizer_js_1.formatSelectorForError)(clickSelector)}`); } await this.page.click(clickSelector, { button: args.button || 'left', clickCount: args.clickCount || 1, timeout, }); output = `Clicked ${clickSelector}`; break; case 'type': const rawTypeSelector = typeof args === 'string' ? args.split(' ')[0] : args.selector; const typeSelector = (0, selector_normalizer_js_1.normalizeSelector)(rawTypeSelector); if (!(0, selector_normalizer_js_1.isValidSelector)(typeSelector)) { throw new Error(`Invalid selector: ${(0, selector_normalizer_js_1.formatSelectorForError)(typeSelector)}`); } const text = typeof args === 'string' ? args.split(' ').slice(1).join(' ') : args.text; await this.page.type(typeSelector, text, { delay: args.delay || 0, }); if (args.submit || args.enter) { await this.page.press(typeSelector, 'Enter'); } output = `Typed "${text}" into ${typeSelector}`; break; case 'wait': if (typeof args === 'string') { const waitSelector = (0, selector_normalizer_js_1.normalizeSelector)(args); if (!(0, selector_normalizer_js_1.isValidSelector)(waitSelector)) { throw new Error(`Invalid selector: ${(0, selector_normalizer_js_1.formatSelectorForError)(waitSelector)}`); } await this.page.waitForSelector(waitSelector, { state: 'visible', timeout }); output = `Waited for selector: ${waitSelector}`; } else if (args.selector) { const waitSelector = (0, selector_normalizer_js_1.normalizeSelector)(args.selector); if (!(0, selector_normalizer_js_1.isValidSelector)(waitSelector)) { throw new Error(`Invalid selector: ${(0, selector_normalizer_js_1.formatSelectorForError)(waitSelector)}`); } await this.page.waitForSelector(waitSelector, { state: args.state || 'visible', timeout: args.timeout || timeout, }); output = `Waited for selector: ${waitSelector}`; } else if (args.time || args.ms) { const ms = args.time || args.ms; await this.page.waitForTimeout(ms); output = `Waited ${ms}ms`; } else if (args.url) { await this.page.waitForURL(args.url, { timeout }); output = `Waited for URL: ${args.url}`; } break; case 'screenshot': const path = typeof args === 'string' ? args : args.path; await this.page.screenshot({ path, fullPage: args.fullPage !== false, type: args.type || 'png', }); output = `Screenshot saved to ${path}`; break; case 'evaluate': case 'eval': const script = typeof args === 'string' ? args : args.script || args.code; const result = await this.page.evaluate(script); // Always capture evaluate results for data extraction if (result !== undefined && result !== null) { // Check if this looks like data extraction (arrays or objects with content) const isDataExtraction = Array.isArray(result) || (typeof result === 'object' && Object.keys(result).length > 0); if (isDataExtraction) { // Detect format from args or original prompt let format = args.format || 'json'; // Auto-detect format from original prompt if available if (!args.format && this.originalPrompt) { format = data_formatter_js_1.DataFormatter.detectFormat(this.originalPrompt); } const filename = args.filename; const { filepath, displayed } = await data_formatter_js_1.DataFormatter.saveAndDisplay(result, { format, filename, display: true }); // Show user-friendly path const home = process.env.HOME || process.env.USERPROFILE || ''; const displayPath = filepath.startsWith(home) ? filepath.replace(home, '~') : filepath; output = `Extracted data (${Array.isArray(result) ? result.length + ' items' : 'object'}) - saved to ${displayPath}`; } else { // Simple result, just log it const resultStr = typeof result === 'string' ? result : JSON.stringify(result); console.log(`\n📤 Result: ${resultStr}`); output = `Evaluated script - result: ${resultStr}`; } } else { output = `Evaluated script - no data returned`; } break; case 'scroll': if (typeof args === 'string') { await this.page.evaluate(`window.scrollTo(0, document.body.scrollHeight)`); output = `Scrolled to bottom`; } else if (args.to) { const percentage = parseInt(args.to); await this.page.evaluate(`window.scrollTo(0, document.body.scrollHeight * ${percentage} / 100)`); output = `Scrolled to ${percentage}%`; } else if (args.selector) { const scrollSelector = (0, selector_normalizer_js_1.normalizeSelector)(args.selector); if (!(0, selector_normalizer_js_1.isValidSelector)(scrollSelector)) { throw new Error(`Invalid selector: ${(0, selector_normalizer_js_1.formatSelectorForError)(scrollSelector)}`); } await this.page.evaluate(({ selector }) => { document.querySelector(selector)?.scrollIntoView({ behavior: 'smooth' }); }, { selector: scrollSelector }); output = `Scrolled to ${scrollSelector}`; } break; case 'select': const rawSelectSelector = typeof args === 'string' ? args.split(' ')[0] : args.selector; const selectSelector = (0, selector_normalizer_js_1.normalizeSelector)(rawSelectSelector); if (!(0, selector_normalizer_js_1.isValidSelector)(selectSelector)) { throw new Error(`Invalid selector: ${(0, selector_normalizer_js_1.formatSelectorForError)(selectSelector)}`); } const value = typeof args === 'string' ? args.split(' ').slice(1).join(' ') : args.value; await this.page.selectOption(selectSelector, value); output = `Selected "${value}" in ${selectSelector}`; break; case 'hover': const rawHoverSelector = typeof args === 'string' ? args : args.selector; const hoverSelector = (0, selector_normalizer_js_1.normalizeSelector)(rawHoverSelector); if (!(0, selector_normalizer_js_1.isValidSelector)(hoverSelector)) { throw new Error(`Invalid selector: ${(0, selector_normalizer_js_1.formatSelectorForError)(hoverSelector)}`); } await this.page.hover(hoverSelector, { timeout, position: args.position, }); output = `Hovered over ${hoverSelector}`; break; default: throw new Error(`Unknown command: ${command}`); } return output; } replaceVariables(args, variables) { if (typeof args === 'string') { return args.replace(/\$\{(\w+)\}/g, (_, key) => variables[key] || ''); } if (typeof args === 'object' && args !== null) { const result = Array.isArray(args) ? [] : {}; for (const [key, value] of Object.entries(args)) { result[key] = this.replaceVariables(value, variables); } return result; } return args; } extractSelectorFromArgs(args) { if (typeof args === 'string') { // For string args, the selector is usually the first part return args.split(' ')[0] || args; } if (typeof args === 'object' && args !== null) { // Look for common selector properties return args.selector || args.target || args.element || null; } return null; } // Helper to format results for Claude static formatResultsForAnalysis(result, originalPrompt, yamlContent) { const stepDetails = result.steps.map(step => { const status = step.success ? '✓' : '✗'; const details = step.output || step.error || 'No output'; return ` ${status} Step ${step.stepNumber}: ${step.command} - ${details}`; }).join('\n'); return ` User's original request: "${originalPrompt}" Workflow YAML: \`\`\`yaml ${yamlContent} \`\`\` Execution Results: - Total steps: ${result.totalSteps} - Successful: ${result.successfulSteps} - Failed: ${result.failedSteps} - Duration: ${result.totalDuration}ms Step Details: ${stepDetails} Based on these results, did the workflow successfully achieve the user's goal? If not, what went wrong and what changes would fix it? `; } } exports.WorkflowExecutor = WorkflowExecutor;