UNPKG

@webmcp/puppeteer

Version:

Puppeteer integration for webMCP - Headless Chrome automation with AI

324 lines (323 loc) 12.4 kB
"use strict"; /** * @webmcp/puppeteer - Puppeteer integration for webMCP * Headless Chrome automation with AI-driven element detection */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.WebMCPPuppeteer = void 0; exports.createWebMCPPuppeteer = createWebMCPPuppeteer; const puppeteer_1 = __importDefault(require("puppeteer")); const core_1 = require("@webmcp/core"); const ai_sdk_1 = require("@webmcp/ai-sdk"); class WebMCPPuppeteer { constructor(config = {}) { this.config = { optimizationLevel: 'basic', screenshotOnError: true, timeout: 30000, puppeteerOptions: { headless: true, args: ['--no-sandbox', '--disable-setuid-sandbox'] }, ...config }; this.processor = new core_1.WebMCPProcessor({ cacheEnabled: true }); if (config.aiConfig) { this.aiClient = new ai_sdk_1.WebMCPAIClient({ apiKeys: config.aiConfig.apiKeys, defaultModel: config.aiConfig.defaultModel || 'gpt-4o', enableOptimization: true }); } } /** * Launch browser and create page */ async launch() { this.browser = await puppeteer_1.default.launch(this.config.puppeteerOptions); this.page = await this.browser.newPage(); // Set viewport and user agent await this.page.setViewport({ width: 1920, height: 1080 }); await this.page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'); return this.page; } /** * Navigate to URL */ async goto(url) { if (!this.page) { throw new Error('Browser not launched. Call launch() first.'); } await this.page.goto(url, { waitUntil: 'networkidle0' }); } /** * Scan current page and generate webMCP data */ async scanPage() { if (!this.page) { throw new Error('Browser not launched. Call launch() first.'); } const html = await this.page.content(); this.webmcpData = this.processor.parseWebMCP(html); // Add page metadata const url = this.page.url(); const title = await this.page.title(); if (this.webmcpData.metadata) { this.webmcpData.metadata.page_url = url; this.webmcpData.metadata.page_title = title; } return this.webmcpData; } /** * Generate AI-driven test actions based on goal */ async generateActions(goal) { if (!this.aiClient) { throw new Error('AI client not configured. Provide aiConfig to use AI features.'); } if (!this.webmcpData) { throw new Error('Page not scanned. Call scanPage() first.'); } const response = await this.aiClient.processWebMCP(this.webmcpData, goal, { compressionLevel: this.config.optimizationLevel, targetModel: this.config.aiConfig?.defaultModel }); if (response.success && response.response) { return this.parseAIResponse(response.response); } throw new Error('Failed to generate actions: ' + response.error); } /** * Execute webMCP-based actions */ async executeActions(actions) { if (!this.page || !this.webmcpData) { throw new Error('Page not ready. Call launch() and scanPage() first.'); } for (let i = 0; i < actions.length; i++) { const action = actions[i]; console.log(`Executing action ${i + 1}/${actions.length}: ${action}`); try { await this.executeAction(action); await new Promise(resolve => setTimeout(resolve, 500)); // Small delay between actions } catch (error) { if (this.config.screenshotOnError) { await this.page.screenshot({ path: `puppeteer-error-${i + 1}.png` }); } throw new Error(`Action ${i + 1} failed: ${error instanceof Error ? error.message : String(error)}`); } } } /** * Smart element interaction using webMCP names */ async interactWith(elementName, action, value) { if (!this.page || !this.webmcpData) { throw new Error('Page not ready. Call launch() and scanPage() first.'); } const element = this.webmcpData.elements.find((el) => el.name === elementName || el.name.includes(elementName.toUpperCase())); if (!element) { throw new Error(`Element with name '${elementName}' not found in webMCP data`); } // Wait for element to be available await this.page.waitForSelector(element.selector, { timeout: this.config.timeout }); switch (action.toLowerCase()) { case 'click': await this.page.click(element.selector); break; case 'type': await this.page.type(element.selector, value || ''); break; case 'clear': await this.page.evaluate((selector) => { const el = document.querySelector(selector); if (el) el.value = ''; }, element.selector); break; case 'select': await this.page.select(element.selector, value || ''); break; case 'hover': await this.page.hover(element.selector); break; case 'focus': await this.page.focus(element.selector); break; case 'screenshot': const elementHandle = await this.page.$(element.selector); if (elementHandle) { await elementHandle.screenshot({ path: `element-${elementName}.png` }); } break; default: throw new Error(`Unknown action: ${action}`); } } /** * Get element by webMCP name */ async getElement(elementName) { if (!this.page || !this.webmcpData) { throw new Error('Page not ready. Call launch() and scanPage() first.'); } const element = this.webmcpData.elements.find((el) => el.name === elementName || el.name.includes(elementName.toUpperCase())); if (!element) { throw new Error(`Element with name '${elementName}' not found in webMCP data`); } return await this.page.$(element.selector); } /** * Wait for element to appear */ async waitForElement(elementName, timeout) { if (!this.page || !this.webmcpData) { throw new Error('Page not ready. Call launch() and scanPage() first.'); } const element = this.webmcpData.elements.find((el) => el.name === elementName || el.name.includes(elementName.toUpperCase())); if (!element) { throw new Error(`Element with name '${elementName}' not found in webMCP data`); } await this.page.waitForSelector(element.selector, { timeout: timeout || this.config.timeout }); } /** * Take screenshot of the page */ async screenshot(path) { if (!this.page) { throw new Error('Browser not launched. Call launch() first.'); } await this.page.screenshot({ path: path, fullPage: true }); } /** * Generate PDF of the page */ async generatePDF(path) { if (!this.page) { throw new Error('Browser not launched. Call launch() first.'); } await this.page.pdf({ path, format: 'A4', printBackground: true }); } /** * Evaluate JavaScript in page context */ async evaluate(fn, ...args) { if (!this.page) { throw new Error('Browser not launched. Call launch() first.'); } return await this.page.evaluate(fn, ...args); } /** * Get page performance metrics */ async getPerformanceMetrics() { if (!this.page) { throw new Error('Browser not launched. Call launch() first.'); } const metrics = await this.page.metrics(); const performance = await this.page.evaluate(() => { const navigation = performance.getEntriesByType('navigation')[0]; return { domContentLoaded: navigation.domContentLoadedEventEnd, loadComplete: navigation.loadEventEnd, firstPaint: performance.getEntriesByType('paint').find((p) => p.name === 'first-paint')?.startTime, firstContentfulPaint: performance.getEntriesByType('paint').find((p) => p.name === 'first-contentful-paint')?.startTime }; }); return { puppeteerMetrics: metrics, performanceTimings: performance, webmcpMetrics: this.webmcpData ? { totalElements: this.webmcpData.elements.length, interactiveElements: this.webmcpData.elements.filter((el) => el.role.includes('input') || el.role.includes('action')).length } : null }; } /** * Close browser */ async close() { if (this.browser) { await this.browser.close(); this.browser = undefined; this.page = undefined; } } parseAIResponse(response) { const lines = response.split('\\n').filter(line => line.trim()); const actions = []; lines.forEach(line => { if (/^\\d+\\./.test(line) || /^(click|type|fill|select|submit|navigate)/i.test(line)) { actions.push(line.replace(/^\\d+\\.\\s*/, '').trim()); } }); return actions; } async executeAction(action) { const actionLower = action.toLowerCase(); if (actionLower.includes('click')) { const elementName = this.extractElementName(action); await this.interactWith(elementName, 'click'); } else if (actionLower.includes('type') || actionLower.includes('fill')) { const elementName = this.extractElementName(action); const value = this.extractValue(action); await this.interactWith(elementName, 'type', value); } else if (actionLower.includes('select')) { const elementName = this.extractElementName(action); const value = this.extractValue(action); await this.interactWith(elementName, 'select', value); } else if (actionLower.includes('navigate')) { const url = this.extractValue(action); await this.goto(url); } else if (actionLower.includes('wait')) { const duration = parseInt(this.extractValue(action)) || 1000; await new Promise(resolve => setTimeout(resolve, duration)); } else if (actionLower.includes('screenshot')) { await this.screenshot(`action-screenshot-${Date.now()}.png`); } } extractElementName(action) { // For type/fill actions, extract element name before "with" if (action.toLowerCase().includes('type') || action.toLowerCase().includes('fill')) { const beforeWith = action.split(/\s+with\s+/i)[0]; const matches = beforeWith.match(/\b([A-Z_]+)\b/); return matches ? matches[1] : ''; } // For other actions, extract the element name const matches = action.match(/\b([A-Z_]+)\b/); return matches ? matches[1] : ''; } extractValue(action) { const matches = action.match(/with\s+["']([^"']+)["']/) || action.match(/:\s*["']([^"']+)["']/) || action.match(/to\s+["']([^"']+)["']/); return matches ? matches[1] : ''; } } exports.WebMCPPuppeteer = WebMCPPuppeteer; // Export factory function async function createWebMCPPuppeteer(config) { const instance = new WebMCPPuppeteer(config); await instance.launch(); return instance; }