UNPKG

@cgaspard/webappmcp

Version:

WebApp MCP - Model Context Protocol integration for web applications with server-side debugging tools

1,318 lines (1,258 loc) 54.8 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.WebAppMCP = {})); })(this, (function (exports) { 'use strict'; class MCPDevTools { constructor(config = {}) { this.container = null; this.isExpanded = false; this.logs = []; this.maxLogs = 500; this.connectionStatus = 'disconnected'; this.config = { position: 'bottom-right', theme: 'dark', ...config }; this.addStyles(); this.createDevToolsUI(); this.setupEventListeners(); } createDevToolsUI() { this.container = document.createElement('div'); this.container.id = 'mcp-devtools'; this.container.className = `mcp-theme-${this.config.theme} mcp-position-${this.config.position}`; this.container.innerHTML = ` <div class="mcp-devtools-indicator" id="mcp-indicator"> <div class="mcp-status-dot" id="mcp-status-dot"></div> <span class="mcp-label">MCP</span> </div> <div class="mcp-devtools-panel" id="mcp-panel" style="display: none;"> <div class="mcp-devtools-header"> <span>MCP DevTools</span> <div class="mcp-header-controls"> <button class="mcp-theme-toggle" id="mcp-theme-btn" title="Toggle theme">🌓</button> <button class="mcp-close-btn" id="mcp-close-btn">×</button> </div> </div> <div class="mcp-devtools-content"> <div class="mcp-status-section"> <div class="mcp-status-item"> <label>WebSocket:</label> <span id="mcp-ws-status">Disconnected</span> </div> <div class="mcp-status-item"> <label>MCP Server:</label> <span id="mcp-server-status">Disconnected</span> </div> </div> <div class="mcp-logs-section"> <div class="mcp-logs-header"> <span>Activity Log</span> <div class="mcp-logs-controls"> <label class="mcp-checkbox"> <input type="checkbox" id="mcp-autoscroll" checked> Auto-scroll </label> <button class="mcp-clear-btn" id="mcp-clear-btn">Clear</button> </div> </div> <div class="mcp-logs-container" id="mcp-logs"></div> </div> </div> </div> `; this.addStyles(); document.body.appendChild(this.container); } addStyles() { const style = document.createElement('style'); style.textContent = ` #mcp-devtools { position: fixed; z-index: 10000; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; } /* Positioning */ #mcp-devtools.mcp-position-bottom-right { bottom: 20px; right: 20px; } #mcp-devtools.mcp-position-bottom-left { bottom: 20px; left: 20px; } #mcp-devtools.mcp-position-top-right { top: 20px; right: 20px; } #mcp-devtools.mcp-position-top-left { top: 20px; left: 20px; } .mcp-devtools-indicator { cursor: pointer; display: flex; align-items: center; gap: 4px; padding: 4px 8px; background: rgba(255, 255, 255, 0.9); border-radius: 4px; border: 1px solid rgba(0, 0, 0, 0.1); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } .mcp-label { font-size: 10px; font-weight: 600; color: #495057; user-select: none; } .mcp-status-dot { width: 12px; height: 12px; border-radius: 50%; background-color: #dc3545; border: 2px solid rgba(255, 255, 255, 0.8); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); transition: background-color 0.3s ease; } .mcp-status-dot.connecting { background-color: #ffc107; animation: pulse 1.5s infinite; } .mcp-status-dot.connected { background-color: #28a745; } @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } } .mcp-devtools-panel { position: absolute; width: 500px; height: 400px; background: white; border: 1px solid #e0e0e0; border-radius: 8px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); display: flex; flex-direction: column; } /* Panel positioning based on indicator position */ .mcp-position-bottom-right .mcp-devtools-panel { bottom: 40px; right: 0; } .mcp-position-bottom-left .mcp-devtools-panel { bottom: 40px; left: 0; } .mcp-position-top-right .mcp-devtools-panel { top: 40px; right: 0; } .mcp-position-top-left .mcp-devtools-panel { top: 40px; left: 0; } .mcp-devtools-header { padding: 12px 16px; background: #f8f9fa; border-bottom: 1px solid #e0e0e0; border-radius: 8px 8px 0 0; display: flex; justify-content: space-between; align-items: center; font-weight: 600; font-size: 14px; } .mcp-close-btn { background: none; border: none; font-size: 18px; cursor: pointer; color: #6c757d; padding: 0; width: 20px; height: 20px; display: flex; align-items: center; justify-content: center; } .mcp-close-btn:hover { color: #495057; } .mcp-devtools-content { flex: 1; display: flex; flex-direction: column; overflow: hidden; } .mcp-status-section { padding: 12px 16px; border-bottom: 1px solid #e0e0e0; background: #f8f9fa; } .mcp-status-item { display: flex; justify-content: space-between; margin-bottom: 4px; font-size: 12px; } .mcp-status-item label { font-weight: 500; color: #495057; } .mcp-status-item span { color: #6c757d; } .mcp-logs-section { flex: 1; display: flex; flex-direction: column; overflow: hidden; } .mcp-logs-header { padding: 8px 16px; background: #f8f9fa; border-bottom: 1px solid #e0e0e0; display: flex; justify-content: space-between; align-items: center; font-size: 12px; font-weight: 500; } .mcp-clear-btn { background: none; border: 1px solid #dee2e6; border-radius: 4px; padding: 2px 8px; font-size: 11px; cursor: pointer; color: #6c757d; } .mcp-clear-btn:hover { background: #e9ecef; } .mcp-logs-container { flex: 1; overflow-y: auto; padding: 8px; font-size: 11px; font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace; } .mcp-log-entry { margin-bottom: 4px; padding: 4px 8px; border-radius: 4px; white-space: pre-wrap; word-break: break-word; } .mcp-log-entry.info { background: #e7f3ff; color: #0c5460; } .mcp-log-entry.warning { background: #fff3cd; color: #856404; } .mcp-log-entry.error { background: #f8d7da; color: #721c24; } .mcp-log-timestamp { color: #6c757d; font-size: 10px; } /* Header controls */ .mcp-header-controls { display: flex; gap: 8px; align-items: center; } .mcp-theme-toggle { background: none; border: none; font-size: 16px; cursor: pointer; padding: 0; opacity: 0.6; transition: opacity 0.2s; } .mcp-theme-toggle:hover { opacity: 1; } /* Log controls */ .mcp-logs-controls { display: flex; gap: 8px; align-items: center; } .mcp-checkbox { display: flex; align-items: center; gap: 4px; font-size: 11px; cursor: pointer; } .mcp-checkbox input { cursor: pointer; } /* Tool execution logs */ .mcp-log-entry.tool { background: #e7f3ff; color: #004085; border-left: 3px solid #004085; } .mcp-tool-details { font-size: 10px; margin-top: 4px; padding-left: 16px; opacity: 0.8; } /* Dark theme */ .mcp-theme-dark .mcp-devtools-indicator { background: rgba(30, 30, 30, 0.9); border-color: rgba(255, 255, 255, 0.1); } .mcp-theme-dark .mcp-label { color: #e0e0e0; } .mcp-theme-dark .mcp-devtools-panel { background: #1e1e1e; border-color: #333; } .mcp-theme-dark .mcp-devtools-header { background: #2d2d2d; border-color: #333; color: #e0e0e0; } .mcp-theme-dark .mcp-devtools-content { background: #1e1e1e; } .mcp-theme-dark .mcp-status-section { background: #2d2d2d; border-color: #333; } .mcp-theme-dark .mcp-status-item label { color: #b0b0b0; } .mcp-theme-dark .mcp-status-item span { color: #e0e0e0; } .mcp-theme-dark .mcp-logs-header { background: #2d2d2d; border-color: #333; color: #e0e0e0; } .mcp-theme-dark .mcp-logs-container { background: #252525; border-color: #333; } .mcp-theme-dark .mcp-log-entry { background: #2d2d2d; color: #e0e0e0; border-color: #333; } .mcp-theme-dark .mcp-log-entry.warning { background: #4a3800; color: #ffc107; } .mcp-theme-dark .mcp-log-entry.error { background: #4a0000; color: #ff6b6b; } .mcp-theme-dark .mcp-log-entry.tool { background: #003366; color: #66b3ff; border-left-color: #66b3ff; } .mcp-theme-dark .mcp-log-timestamp { color: #888; } .mcp-theme-dark .mcp-clear-btn, .mcp-theme-dark .mcp-close-btn { color: #e0e0e0; border-color: #555; } .mcp-theme-dark .mcp-clear-btn:hover, .mcp-theme-dark .mcp-close-btn:hover { background: #444; } } `; document.head.appendChild(style); } setupEventListeners() { const indicator = this.container?.querySelector('#mcp-indicator'); const closeBtn = this.container?.querySelector('#mcp-close-btn'); const clearBtn = this.container?.querySelector('#mcp-clear-btn'); const themeBtn = this.container?.querySelector('#mcp-theme-btn'); indicator?.addEventListener('click', () => this.togglePanel()); closeBtn?.addEventListener('click', () => this.togglePanel()); clearBtn?.addEventListener('click', () => this.clearLogs()); themeBtn?.addEventListener('click', () => this.toggleTheme()); } toggleTheme() { if (!this.container) return; this.config.theme = this.config.theme === 'light' ? 'dark' : 'light'; this.container.className = `mcp-theme-${this.config.theme} mcp-position-${this.config.position}`; } togglePanel() { this.isExpanded = !this.isExpanded; const panel = this.container?.querySelector('#mcp-panel'); if (panel) { panel.style.display = this.isExpanded ? 'flex' : 'none'; } } setConnectionStatus(status) { this.connectionStatus = status; const dot = this.container?.querySelector('#mcp-status-dot'); const wsStatus = this.container?.querySelector('#mcp-ws-status'); const serverStatus = this.container?.querySelector('#mcp-server-status'); if (dot) { dot.className = `mcp-status-dot ${status}`; } if (wsStatus && serverStatus) { const statusText = status.charAt(0).toUpperCase() + status.slice(1); wsStatus.textContent = statusText; serverStatus.textContent = statusText; } this.addLog('info', 'client', `Connection status: ${status}`); } addLog(level, source, message, data) { const log = { timestamp: new Date().toISOString(), level, source, message, data }; this.logs.unshift(log); if (this.logs.length > this.maxLogs) { this.logs = this.logs.slice(0, this.maxLogs); } this.renderLogs(); } renderLogs() { const logsContainer = this.container?.querySelector('#mcp-logs'); if (!logsContainer) return; logsContainer.innerHTML = this.logs.map(log => { const time = new Date(log.timestamp).toLocaleTimeString(); if (log.level === 'tool' && log.data) { // Special formatting for tool logs const details = log.data; let detailsHtml = ''; if (details.args) { detailsHtml += `<div class="mcp-tool-details">Args: ${this.safeStringify(details.args)}</div>`; } if (details.error) { detailsHtml += `<div class="mcp-tool-details">Error: ${details.error}</div>`; } if (details.result && details.status === 'completed') { const resultStr = this.safeStringify(details.result); if (resultStr.length > 100) { detailsHtml += `<div class="mcp-tool-details">Result: ${resultStr.substring(0, 100)}...</div>`; } else { detailsHtml += `<div class="mcp-tool-details">Result: ${resultStr}</div>`; } } return ` <div class="mcp-log-entry ${log.level}"> <span class="mcp-log-timestamp">[${time}]</span> [${log.source.toUpperCase()}] ${log.message} ${detailsHtml} </div> `; } else { // Regular log formatting const dataStr = log.data ? ` ${this.safeStringify(log.data)}` : ''; return ` <div class="mcp-log-entry ${log.level}"> <span class="mcp-log-timestamp">[${time}]</span> [${log.source.toUpperCase()}] ${log.message}${dataStr} </div> `; } }).join(''); // Auto-scroll to top if enabled (latest items are at the top) const autoScrollCheckbox = this.container?.querySelector('#mcp-autoscroll'); if (autoScrollCheckbox?.checked) { logsContainer.scrollTop = 0; } } safeStringify(obj) { try { // Handle simple types if (obj === null || obj === undefined) { return String(obj); } if (typeof obj === 'string' || typeof obj === 'number' || typeof obj === 'boolean') { return String(obj); } // Handle arrays and objects with circular reference protection const seen = new Set(); return JSON.stringify(obj, (key, value) => { if (typeof value === 'object' && value !== null) { if (seen.has(value)) { return '[Circular Reference]'; } seen.add(value); } return value; }); } catch (error) { return `[Error stringifying: ${error instanceof Error ? error.message : 'Unknown error'}]`; } } clearLogs() { this.logs = []; this.renderLogs(); } logWebSocketEvent(event, data) { this.addLog('info', 'websocket', event, data); } logMCPEvent(event, data) { this.addLog('info', 'mcp', event, data); } logError(source, message, error) { this.addLog('error', source, message, error); } logToolExecution(toolName, args, success, message, executionTime, result) { const status = success === null ? 'started' : (success ? 'completed' : 'failed'); const details = { tool: toolName, status, args, executionTime: executionTime ? `${executionTime}ms` : undefined, result: success && result ? result : undefined, error: !success && message !== 'Success' ? message : undefined }; const logMessage = `Tool ${toolName} ${status}${executionTime ? ` (${executionTime}ms)` : ''}`; this.addLog(success === false ? 'error' : 'tool', 'tool', logMessage, details); } } class WebAppMCPClient { get isConnected() { return this._isConnected && this.ws?.readyState === WebSocket.OPEN; } constructor(config) { this.ws = null; this.reconnectAttempts = 0; this.messageHandlers = new Map(); this.consoleLogs = []; this._isConnected = false; this.devTools = null; this.pluginHandlers = {}; this.config = { reconnectInterval: 5000, maxReconnectAttempts: 10, enableDevTools: true, debug: false, enableConnection: true, interceptConsole: true, enabledTools: [], // Empty array means all tools enabled devToolsPosition: 'bottom-right', devToolsTheme: 'dark', ...config, }; // Only intercept console if enabled if (this.config.interceptConsole) { this.setupConsoleInterception(); } // Auto-load html2canvas for screenshot functionality this.loadHtml2Canvas(); if (this.config.enableDevTools && this.config.enableConnection) { this.devTools = new MCPDevTools({ position: this.config.devToolsPosition, theme: this.config.devToolsTheme, }); this.devTools.setConnectionStatus('disconnected'); } } log(...args) { if (this.config.debug) { console.log('[webappmcp]', ...args); } } logError(...args) { // Always log errors regardless of debug setting console.error('[webappmcp]', ...args); } connect() { // Bypass connection if disabled (e.g., in production) if (!this.config.enableConnection) { this.log('Connection disabled by configuration'); return; } if (this.ws && this.ws.readyState === WebSocket.OPEN) { return; } this.devTools?.setConnectionStatus('connecting'); this.devTools?.logWebSocketEvent('Attempting to connect', { url: this.config.serverUrl }); try { const url = new URL(this.config.serverUrl); const headers = {}; if (this.config.authToken) { headers['Authorization'] = `Bearer ${this.config.authToken}`; } // WebSocket in browser doesn't support custom headers directly // So we'll pass the token in the URL if (this.config.authToken) { url.searchParams.set('token', this.config.authToken); } this.ws = new WebSocket(url.toString()); this.setupWebSocketHandlers(); } catch (error) { this.logError('Failed to connect to WebApp MCP server:', error); this.devTools?.logError('websocket', 'Failed to connect', error); this.scheduleReconnect(); } } disconnect() { this._isConnected = false; if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); this.reconnectTimer = null; } if (this.ws) { this.ws.close(); this.ws = null; } } setupWebSocketHandlers() { if (!this.ws) return; this.ws.onopen = () => { this.log('Connected to WebApp MCP server'); this._isConnected = true; this.reconnectAttempts = 0; this.devTools?.setConnectionStatus('connected'); this.devTools?.logWebSocketEvent('Connected to WebApp MCP server'); this.sendMessage({ type: 'init', url: window.location.href, }); }; this.ws.onmessage = (event) => { try { const message = JSON.parse(event.data); this.devTools?.logWebSocketEvent('Message received', message); this.handleMessage(message); } catch (error) { this.logError('Failed to parse WebSocket message:', error); this.devTools?.logError('websocket', 'Failed to parse message', error); } }; this.ws.onerror = (error) => { this.logError('WebSocket error:', error); this.devTools?.logError('websocket', 'WebSocket error', error); }; this.ws.onclose = () => { this.log('Disconnected from WebApp MCP server'); this._isConnected = false; this.devTools?.setConnectionStatus('disconnected'); this.devTools?.logWebSocketEvent('Disconnected from WebApp MCP server'); this.scheduleReconnect(); }; } scheduleReconnect() { if (this.reconnectAttempts >= (this.config.maxReconnectAttempts || 10) || this.reconnectTimer) { return; } this.reconnectAttempts++; this.log(`Scheduling reconnect attempt ${this.reconnectAttempts}/${this.config.maxReconnectAttempts}`); this.reconnectTimer = setTimeout(() => { this.reconnectTimer = null; this.connect(); }, this.config.reconnectInterval); } sendMessage(message) { if (this.ws && this.ws.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify(message)); this.devTools?.logWebSocketEvent('Message sent', message); } else { this.logError('WebSocket is not connected'); } } handleMessage(message) { const { type, requestId, tool, args } = message; this.log('[WebApp Client] Received message:', JSON.stringify(message)); if (type === 'connected') { this.log('WebApp MCP client registered:', message.clientId); this.devTools?.logMCPEvent('Client registered', { clientId: message.clientId }); return; } if (type === 'execute_tool') { this.log(`[WebApp Client] Executing tool: ${tool} with requestId: ${requestId}`); this.devTools?.logMCPEvent(`Executing tool: ${tool}`, { requestId, args }); this.executeToolHandler(requestId, tool, args); return; } if (type === 'plugin_extension') { this.log(`[WebApp Client] Loading plugin extension`); this.loadPluginExtension(message.extension); return; } const handler = this.messageHandlers.get(type); if (handler) { handler(message); } } async executeToolHandler(requestId, toolName, args) { // Check if tool is enabled if (this.config.enabledTools && this.config.enabledTools.length > 0) { if (!this.config.enabledTools.includes(toolName)) { const error = `Tool ${toolName} is not enabled`; this.logError(error); this.devTools?.logToolExecution(toolName, args, false, error); this.sendMessage({ type: 'tool_response', requestId, success: false, error, }); return; } } this.log(`[WebApp Client] Executing tool handler for ${toolName}`); this.log(`[WebApp Client] Tool args:`, JSON.stringify(args)); this.devTools?.logToolExecution(toolName, args, null, 'Started'); const startTime = Date.now(); try { let result; switch (toolName) { case 'dom_query': result = await this.domQuery(args); break; case 'dom_get_properties': result = await this.domGetProperties(args); break; case 'dom_get_text': result = await this.domGetText(args); break; case 'dom_get_html': result = await this.domGetHTML(args); break; case 'interaction_click': result = await this.interactionClick(args); break; case 'interaction_type': result = await this.interactionType(args); break; case 'interaction_scroll': result = await this.interactionScroll(args); break; case 'interaction_hover': result = await this.interactionHover(args); break; case 'capture_screenshot': result = await this.captureScreenshot(args); break; case 'capture_element_screenshot': result = await this.captureElementScreenshot(args); break; case 'state_get_variable': result = await this.stateGetVariable(args); break; case 'state_local_storage': result = await this.stateLocalStorage(args); break; case 'console_get_logs': result = await this.consoleGetLogs(args); break; case 'dom_manipulate': result = await this.domManipulate(args); break; case 'javascript_inject': result = await this.javascriptInject(args); break; case 'webapp_list_clients': result = await this.webappListClients(args); break; case 'execute_javascript': result = await this.executeJavascript(args); break; default: // Check if this is a plugin-provided tool if (this.pluginHandlers && this.pluginHandlers[toolName]) { result = await this.pluginHandlers[toolName](args); } else { throw new Error(`Unknown tool: ${toolName}`); } } const executionTime = Date.now() - startTime; this.log(`[WebApp Client] Tool execution successful, sending response`); this.devTools?.logToolExecution(toolName, args, true, 'Success', executionTime, result); this.sendMessage({ type: 'tool_response', requestId, result, success: true, }); } catch (error) { const executionTime = Date.now() - startTime; const errorMessage = error instanceof Error ? error.message : String(error); this.logError(`[WebApp Client] Tool execution failed:`, error); this.devTools?.logToolExecution(toolName, args, false, errorMessage, executionTime); this.sendMessage({ type: 'tool_response', requestId, success: false, error: errorMessage, }); } } loadHtml2Canvas() { // Check if html2canvas is already loaded if (typeof window.html2canvas !== 'undefined') { return; } // Create script element const script = document.createElement('script'); script.src = 'https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js'; script.async = true; script.onload = () => { if (this.config.debug) { console.log('[WebAppMCP] html2canvas loaded successfully'); } }; script.onerror = () => { console.warn('[WebAppMCP] Failed to load html2canvas - screenshots will use fallback mode'); }; document.head.appendChild(script); } setupConsoleInterception() { const originalConsole = { log: console.log, info: console.info, warn: console.warn, error: console.error, }; const interceptor = (level) => { return (...args) => { this.consoleLogs.push({ level, timestamp: new Date().toISOString(), args: args.map((arg) => { try { return typeof arg === 'object' ? JSON.stringify(arg) : String(arg); } catch { return String(arg); } }), }); if (this.consoleLogs.length > 1000) { this.consoleLogs.shift(); } originalConsole[level](...args); }; }; console.log = interceptor('log'); console.info = interceptor('info'); console.warn = interceptor('warn'); console.error = interceptor('error'); } async domQuery(args) { const { selector, limit = 10 } = args; const elements = Array.from(document.querySelectorAll(selector)).slice(0, limit); return { elements: elements.map((el) => ({ selector, tagName: el.tagName.toLowerCase(), id: el.id || undefined, className: el.className || undefined, text: el.textContent?.trim().substring(0, 100), attributes: (() => { const attrs = {}; for (let i = 0; i < el.attributes.length; i++) { const attr = el.attributes[i]; attrs[attr.name] = attr.value; } return attrs; })(), })), }; } async domGetProperties(args) { const { selector, properties = [] } = args; const element = document.querySelector(selector); if (!element) { throw new Error(`Element not found: ${selector}`); } const result = {}; for (const prop of properties) { try { result[prop] = element[prop]; } catch { result[prop] = undefined; } } return result; } async domGetText(args) { const { selector, includeHidden = false } = args; const elements = document.querySelectorAll(selector); const texts = []; elements.forEach((el) => { if (includeHidden || el.offsetParent !== null) { const text = el.textContent?.trim(); if (text) texts.push(text); } }); return { texts }; } async domGetHTML(args) { const { selector, outerHTML = false } = args; const element = document.querySelector(selector); if (!element) { throw new Error(`Element not found: ${selector}`); } return { html: outerHTML ? element.outerHTML : element.innerHTML, }; } async interactionClick(args) { const { selector, button = 'left' } = args; const element = document.querySelector(selector); if (!element) { throw new Error(`Element not found: ${selector}`); } const event = new MouseEvent('click', { view: window, bubbles: true, cancelable: true, button: button === 'right' ? 2 : button === 'middle' ? 1 : 0, }); element.dispatchEvent(event); return { success: true }; } async interactionType(args) { const { selector, text, clear = false } = args; const element = document.querySelector(selector); if (!element) { throw new Error(`Element not found: ${selector}`); } if (clear) { element.value = ''; } element.focus(); element.value += text; element.dispatchEvent(new Event('input', { bubbles: true })); element.dispatchEvent(new Event('change', { bubbles: true })); return { success: true }; } async interactionScroll(args) { const { selector, direction, amount = 100 } = args; const element = selector ? document.querySelector(selector) : window; if (!element && selector) { throw new Error(`Element not found: ${selector}`); } const scrollOptions = { behavior: 'smooth', }; if (direction === 'up' || direction === 'down') { scrollOptions.top = direction === 'down' ? amount : -amount; } else { scrollOptions.left = direction === 'right' ? amount : -amount; } if (element === window) { window.scrollBy(scrollOptions); } else { element.scrollBy(scrollOptions); } return { success: true }; } async interactionHover(args) { const { selector } = args; const element = document.querySelector(selector); if (!element) { throw new Error(`Element not found: ${selector}`); } element.dispatchEvent(new MouseEvent('mouseenter', { view: window, bubbles: true, cancelable: true, })); element.dispatchEvent(new MouseEvent('mouseover', { view: window, bubbles: true, cancelable: true, })); return { success: true }; } async captureScreenshot(args) { const { fullPage = true, format = 'png' } = args; try { // Use a more sophisticated approach to capture actual content const width = fullPage ? Math.max(document.documentElement.scrollWidth, document.body.scrollWidth, document.documentElement.offsetWidth, document.body.offsetWidth, document.documentElement.clientWidth) : window.innerWidth; const height = fullPage ? Math.max(document.documentElement.scrollHeight, document.body.scrollHeight, document.documentElement.offsetHeight, document.body.offsetHeight, document.documentElement.clientHeight) : window.innerHeight; // Create a canvas to draw the content const canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; const ctx = canvas.getContext('2d'); if (!ctx) { throw new Error('Failed to create canvas context'); } // Try to use html2canvas if available if (typeof window.html2canvas !== 'undefined') { const html2canvas = window.html2canvas; const capturedCanvas = await html2canvas(document.body, { width: width, height: height, windowWidth: width, windowHeight: height, x: 0, y: 0, scrollX: fullPage ? 0 : window.scrollX, scrollY: fullPage ? 0 : window.scrollY, useCORS: true, allowTaint: true }); const dataUrl = capturedCanvas.toDataURL(`image/${format}`); return { success: true, dataUrl, width, height, message: 'Screenshot captured successfully' }; } // Fallback: Create a more detailed representation // This is still a fallback but provides more information than a blank placeholder // Fill background const bgColor = window.getComputedStyle(document.body).backgroundColor || '#ffffff'; ctx.fillStyle = bgColor; ctx.fillRect(0, 0, width, height); // Add some context about the page ctx.fillStyle = '#666'; ctx.font = '14px system-ui, -apple-system, sans-serif'; ctx.textAlign = 'left'; const info = [ `Page Title: ${document.title}`, `URL: ${window.location.href}`, `Dimensions: ${width}x${height}`, '', 'Note: For full screenshot functionality, include html2canvas library:', '<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>' ]; let y = 30; info.forEach(line => { ctx.fillText(line, 20, y); y += 25; }); // Draw a border ctx.strokeStyle = '#ddd'; ctx.lineWidth = 2; ctx.strokeRect(1, 1, width - 2, height - 2); const dataUrl = canvas.toDataURL(`image/${format}`); return { success: true, dataUrl, width, height, message: 'Screenshot captured (basic mode - add html2canvas for full rendering)' }; } catch (error) { throw new Error(`Failed to capture screenshot: ${error}`); } } async captureElementScreenshot(args) { const { selector, format = 'png' } = args; if (!selector) { throw new Error('Selector is required for element screenshot'); } const element = document.querySelector(selector); if (!element) { throw new Error(`Element not found: ${selector}`); } try { const rect = element.getBoundingClientRect(); // Try to use html2canvas if available if (typeof window.html2canvas !== 'undefined') { const html2canvas = window.html2canvas; const capturedCanvas = await html2canvas(element, { width: rect.width, height: rect.height, x: rect.left + window.scrollX, y: rect.top + window.scrollY, scrollX: -rect.left, scrollY: -rect.top, useCORS: true, allowTaint: true }); const dataUrl = capturedCanvas.toDataURL(`image/${format}`); return { success: true, dataUrl, width: rect.width, height: rect.height, selector, message: 'Element screenshot captured successfully' }; } // Fallback approach const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); if (!ctx) { throw new Error('Failed to create canvas context'); } canvas.width = rect.width; canvas.height = rect.height; // Get element styles const styles = window.getComputedStyle(element); const bgColor = styles.backgroundColor || '#ffffff'; // Draw element representation ctx.fillStyle = bgColor; ctx.fillRect(0, 0, rect.width, rect.height); // Draw border ctx.strokeStyle = styles.borderColor || '#ddd'; ctx.lineWidth = parseInt(styles.borderWidth) || 1; ctx.strokeRect(0, 0, rect.width, rect.height); // Add element info ctx.fillStyle = '#666'; ctx.font = '12px system-ui, -apple-system, sans-serif'; ctx.textAlign = 'center'; const lines = [ `Element: ${selector}`, `Size: ${Math.round(rect.width)}x${Math.round(rect.height)}`, `Tag: ${element.tagName.toLowerCase()}`, element.className ? `Class: ${element.className}` : '', 'Add html2canvas for full rendering' ].filter(Boolean); let y = Math.max(20, rect.height / 2 - (lines.length * 15) / 2); lines.forEach(line => { ctx.fillText(line, rect.width / 2, y); y += 15; }); const dataUrl = canvas.toDataURL(`image/${format}`); return { success: true, dataUrl, width: rect.width, height: rect.height, selector, message: 'Element screenshot captured (basic mode - add html2canvas for full rendering)' }; } catch (error) { throw new Error(`Failed to capture element screenshot: ${error}`); } } async stateGetVariable(args) { const { path } = args; const parts = path.split('.'); let current = window; for (const part of parts) { if (current && typeof current === 'object' && part in current) { current = current[part]; } else { throw new Error(`Variable not found: ${path}`); } } return { value: current }; } async stateLocalStorage(args) { const { operation, key, value } = args; switch (operation) { case 'get': return { value: localStorage.getItem(key) }; case 'set': localStorage.setItem(key, value); return { success: true }; case 'remove': localStorage.removeItem(key); return { success: true }; case 'clear': localStorage.clear(); return { success: true }; case 'getAll': const items = {}; for (let i = 0; i < localStorage.length; i++) { const k = localStorage.key(i); if (k) items[k] = localStorage.getItem(k) || ''; } return { items }; default: throw new Error(`Unknown localStorage operation: ${operation}`); } } async consoleGetLogs(args) { const { level = 'all', limit = 100 } = args; let logs = this.consoleLogs; if (level !== 'all') { logs = logs.filter((log) => log.level === level); } return { logs: logs.slice(-limit) }; } async domManipulate(args) { const { action, selector, value, attribute, property } = args; if (!selector) { throw new Error('Selector is required for DOM manipulation'); } const element = document.querySelector(selector); if (!element) { throw new Error(`Element not found: ${selector}`); } switch (action) { case 'setAttribute': if (!attribute || value === undefined) { throw new Error('Attribute name and value are required for setAttribute'); } element.setAttribute(attribute, value); return { success: true, message: `Set attribute ${attribute}="${value}" on ${selector}` }; case 'removeAttribute': if (!attribute) { throw new Error('Attribute name is required for removeAttribute'); } element.removeAttribute(attribute); return { success: true, message: `Removed attribute ${attribute} from ${selector}` }; case 'setProperty': if (!property || value === undefined) { throw new Error('Property name and value are required for setProperty'); } element[property] = value; return { success: true, message: `Set property ${property}=${value} on ${selector}` }; case 'addClass': if (!value) { throw new Error('Class name is required for addClass'); } element.classList.add(value); return { success: true, message: `Added class "${value}" to ${selector}` }; case 'removeClass': if (!value) { throw new Error('Class name is required for removeClass'); } element.classList.remove(value); return { success: true, message: `Removed class "${value}" from ${selector}` }; case 'setInnerHTML': if (value === undefined) { throw new Error('HTML content is required for setInnerHTML'); } element.innerHTML = value; return { success: true, message: `Set innerHTML on ${selector}` }; case 'setTextContent': if (value === undefined) { throw new Error('Text content is required for setTextContent'); } element.textContent = value; return { s