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
JavaScript
;
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;