From 76854cc8d0246c7fd8f5890a53c8c56608270f88 Mon Sep 17 00:00:00 2001 From: Aaron Elijah Mars Date: Thu, 26 Jun 2025 15:58:29 +0200 Subject: [PATCH] works on X --- opendia-extension/content.js | 2080 ++++++++++++++++++++++------------ opendia-mcp/server.js | 448 ++++++-- 2 files changed, 1730 insertions(+), 798 deletions(-) diff --git a/opendia-extension/content.js b/opendia-extension/content.js index 5b128b4..b6277e6 100644 --- a/opendia-extension/content.js +++ b/opendia-extension/content.js @@ -1,165 +1,264 @@ -// Enhanced Browser Automation Content Script -console.log('OpenDia enhanced content script loaded'); +// Enhanced Browser Automation Content Script with Anti-Detection +console.log("OpenDia enhanced content script loaded"); -// Enhanced Pattern Database with Intent Categories +// Enhanced Pattern Database with Twitter-First Priority const ENHANCED_PATTERNS = { // Authentication patterns - "auth": { - "login": { - input: ["[type='email']", "[name*='username' i]", "[placeholder*='email' i]", "[name*='login' i]"], + auth: { + login: { + input: [ + "[type='email']", + "[name*='username' i]", + "[placeholder*='email' i]", + "[name*='login' i]", + ], password: ["[type='password']", "[name*='password' i]"], - submit: ["[type='submit']", "button[form]", ".login-btn", "[aria-label*='login' i]"], - confidence: 0.9 + submit: [ + "[type='submit']", + "button[form]", + ".login-btn", + "[aria-label*='login' i]", + ], + confidence: 0.9, }, - "signup": { - input: ["[name*='register' i]", "[placeholder*='signup' i]", "[name*='email' i]"], + signup: { + input: [ + "[name*='register' i]", + "[placeholder*='signup' i]", + "[name*='email' i]", + ], submit: ["[href*='signup']", ".signup-btn", "[aria-label*='register' i]"], - confidence: 0.85 - } - }, - - // Content creation patterns - "content": { - "post_create": { - textarea: ["[contenteditable='true']", "textarea[placeholder*='post' i]", "[data-text='true']"], - submit: ["[data-testid*='post']", ".post-btn", ".publish-btn", "[aria-label*='post' i]"], - confidence: 0.9 + confidence: 0.85, }, - "comment": { - textarea: ["textarea[placeholder*='comment' i]", "[role='textbox']", "[placeholder*='reply' i]"], - submit: [".comment-btn", "[aria-label*='comment' i]", "[aria-label*='reply' i]"], - confidence: 0.8 - } }, - + + // Content creation patterns - Twitter FIRST + content: { + post_create: { + textarea: [ + "[data-testid='tweetTextarea_0']", // Twitter FIRST (most specific) + "[aria-label='Post text']", // Twitter specific + "[contenteditable='true']", // Generic last + "textarea[placeholder*='post' i]", + "[data-text='true']", + ], + submit: [ + "[data-testid='tweetButtonInline']", // Twitter inline + "[data-testid='tweetButton']", // Twitter main + ".post-btn", + ".publish-btn", + "[aria-label*='post' i]", + ], + confidence: 0.95, + }, + comment: { + textarea: [ + "textarea[placeholder*='comment' i]", + "[role='textbox']", + "[placeholder*='reply' i]", + ], + submit: [ + ".comment-btn", + "[aria-label*='comment' i]", + "[aria-label*='reply' i]", + ], + confidence: 0.8, + }, + }, + // Search patterns - "search": { - "global": { - input: ["[type='search']", "[role='searchbox']", "[placeholder*='search' i]", "[name*='search' i]"], - submit: ["[aria-label*='search' i]", ".search-btn", "button[type='submit']"], - confidence: 0.85 - } - }, - - // Navigation patterns - "nav": { - "menu": { - toggle: ["[aria-label*='menu' i]", ".menu-btn", ".hamburger", "[data-toggle='menu']"], - items: ["nav a", ".nav-item", "[role='menuitem']"], - confidence: 0.8 - } - }, - - // Form patterns - "form": { - "submit": { - button: ["[type='submit']", "button[form]", ".submit-btn", "[aria-label*='submit' i]"], - confidence: 0.85 + search: { + global: { + input: [ + "[data-testid='SearchBox_Search_Input']", // Twitter search first + "[type='search']", + "[role='searchbox']", + "[placeholder*='search' i]", + "[name*='search' i]", + ], + submit: [ + "[aria-label*='search' i]", + ".search-btn", + "button[type='submit']", + ], + confidence: 0.85, }, - "reset": { + }, + + // Navigation patterns + nav: { + menu: { + toggle: [ + "[aria-label*='menu' i]", + ".menu-btn", + ".hamburger", + "[data-toggle='menu']", + ], + items: ["nav a", ".nav-item", "[role='menuitem']"], + confidence: 0.8, + }, + }, + + // Form patterns + form: { + submit: { + button: [ + "[type='submit']", + "button[form]", + ".submit-btn", + "[aria-label*='submit' i]", + ], + confidence: 0.85, + }, + reset: { button: ["[type='reset']", ".reset-btn", "[aria-label*='reset' i]"], - confidence: 0.8 - } - } + confidence: 0.8, + }, + }, +}; + +// Anti-Detection Platform Configuration +const ANTI_DETECTION_PLATFORMS = { + "twitter.com": { + selectors: { + textarea: "[data-testid='tweetTextarea_0']", + submit: "[data-testid='tweetButtonInline'], [data-testid='tweetButton']", + }, + bypassMethod: "twitter_direct", + }, + "x.com": { + selectors: { + textarea: "[data-testid='tweetTextarea_0']", + submit: "[data-testid='tweetButtonInline'], [data-testid='tweetButton']", + }, + bypassMethod: "twitter_direct", + }, + // Add other platforms that need special handling + "linkedin.com": { + selectors: { + textarea: "[contenteditable='true'][role='textbox']", + submit: "[data-control-name='share.post']", + }, + bypassMethod: "linkedin_direct", + }, + "facebook.com": { + selectors: { + textarea: "[contenteditable='true'][data-text='true']", + submit: "[data-testid='react-composer-post-button']", + }, + bypassMethod: "facebook_direct", + }, }; // Legacy pattern database for backward compatibility const PATTERN_DATABASE = { - "twitter": { - "domains": ["twitter.com", "x.com"], - "patterns": { - "post_tweet": { - textarea: "[data-testid='tweetTextarea_0'], [contenteditable='true'][data-text='true']", - submit: "[data-testid='tweetButtonInline'], [data-testid='tweetButton']", - confidence: 0.95 + twitter: { + domains: ["twitter.com", "x.com"], + patterns: { + post_tweet: { + textarea: + "[data-testid='tweetTextarea_0'], [contenteditable='true'][data-text='true']", + submit: + "[data-testid='tweetButtonInline'], [data-testid='tweetButton']", + confidence: 0.95, }, - "search": { - input: "[data-testid='SearchBox_Search_Input'], input[placeholder*='search' i]", + search: { + input: + "[data-testid='SearchBox_Search_Input'], input[placeholder*='search' i]", submit: "[data-testid='SearchBox_Search_Button']", - confidence: 0.90 - } - } + confidence: 0.9, + }, + }, }, - "github": { - "domains": ["github.com"], - "patterns": { - "search": { + github: { + domains: ["github.com"], + patterns: { + search: { input: "input[placeholder*='Search' i].form-control", submit: "button[type='submit']", - confidence: 0.85 - } - } + confidence: 0.85, + }, + }, }, - "universal": { - "search": { + universal: { + search: { selectors: [ "input[type='search']", "input[placeholder*='search' i]", "[role='searchbox']", - "input[name*='search' i]" + "input[name*='search' i]", ], - confidence: 0.60 + confidence: 0.6, }, - "submit": { + submit: { selectors: [ "button[type='submit']:not([disabled])", "input[type='submit']:not([disabled])", - "[role='button'][aria-label*='submit' i]" + "[role='button'][aria-label*='submit' i]", ], - confidence: 0.65 - } - } + confidence: 0.65, + }, + }, }; // Token usage tracking and performance optimization class TokenOptimizer { constructor() { - this.metrics = JSON.parse(localStorage.getItem('opendia_metrics') || '[]'); - this.successRates = JSON.parse(localStorage.getItem('opendia_success_rates') || '{}'); + this.metrics = JSON.parse(localStorage.getItem("opendia_metrics") || "[]"); + this.successRates = JSON.parse( + localStorage.getItem("opendia_success_rates") || "{}" + ); this.recommendedMaxResults = 5; this.lastCleanup = Date.now(); } - trackUsage(operation, tokenCount, success, pageType = 'unknown', method = 'unknown') { + trackUsage( + operation, + tokenCount, + success, + pageType = "unknown", + method = "unknown" + ) { const metric = { operation, tokens: tokenCount, success, pageType, method, - timestamp: Date.now() + timestamp: Date.now(), }; - + this.metrics.push(metric); - + // Keep only last 100 metrics to prevent storage bloat if (this.metrics.length > 100) { this.metrics = this.metrics.slice(-100); } - + // Auto-adjust limits based on success rates this.adjustLimits(); - + // Periodic cleanup (once per hour) if (Date.now() - this.lastCleanup > 3600000) { this.cleanup(); } - + // Persist to localStorage - localStorage.setItem('opendia_metrics', JSON.stringify(this.metrics)); + localStorage.setItem("opendia_metrics", JSON.stringify(this.metrics)); } adjustLimits() { const recent = this.metrics.slice(-20); if (recent.length < 10) return; - - const avgTokens = recent.reduce((sum, m) => sum + m.tokens, 0) / recent.length; - const successRate = recent.filter(m => m.success).length / recent.length; - + + const avgTokens = + recent.reduce((sum, m) => sum + m.tokens, 0) / recent.length; + const successRate = recent.filter((m) => m.success).length / recent.length; + // If using too many tokens but success rate is high, reduce max results if (avgTokens > 200 && successRate > 0.8) { this.recommendedMaxResults = Math.max(3, this.recommendedMaxResults - 1); - } + } // If success rate is low, increase max results to get better matches else if (successRate < 0.6) { this.recommendedMaxResults = Math.min(10, this.recommendedMaxResults + 1); @@ -168,20 +267,23 @@ class TokenOptimizer { cleanup() { // Remove metrics older than 7 days - const cutoff = Date.now() - (7 * 24 * 60 * 60 * 1000); - this.metrics = this.metrics.filter(m => m.timestamp > cutoff); - + const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1000; + this.metrics = this.metrics.filter((m) => m.timestamp > cutoff); + // Clean up success rates for rarely used combinations - const recentPageTypes = new Set(this.metrics.map(m => m.pageType)); - Object.keys(this.successRates).forEach(key => { - const [pageType] = key.split(':'); + const recentPageTypes = new Set(this.metrics.map((m) => m.pageType)); + Object.keys(this.successRates).forEach((key) => { + const [pageType] = key.split(":"); if (!recentPageTypes.has(pageType)) { delete this.successRates[key]; } }); - - localStorage.setItem('opendia_metrics', JSON.stringify(this.metrics)); - localStorage.setItem('opendia_success_rates', JSON.stringify(this.successRates)); + + localStorage.setItem("opendia_metrics", JSON.stringify(this.metrics)); + localStorage.setItem( + "opendia_success_rates", + JSON.stringify(this.successRates) + ); this.lastCleanup = Date.now(); } @@ -190,18 +292,27 @@ class TokenOptimizer { if (!this.successRates[key]) { this.successRates[key] = { attempts: 0, successes: 0 }; } - + this.successRates[key].attempts++; if (success) { this.successRates[key].successes++; } - - localStorage.setItem('opendia_success_rates', JSON.stringify(this.successRates)); + + localStorage.setItem( + "opendia_success_rates", + JSON.stringify(this.successRates) + ); } getBestMethod(pageType, intent) { - const methods = ['enhanced_pattern_match', 'pattern_database', 'viewport_scan', 'semantic_analysis']; - + const methods = [ + "anti_detection_bypass", + "enhanced_pattern_match", + "pattern_database", + "viewport_scan", + "semantic_analysis", + ]; + return methods.sort((a, b) => { const aKey = `${pageType}:${intent}:${a}`; const bKey = `${pageType}:${intent}:${b}`; @@ -223,18 +334,20 @@ class TokenOptimizer { getAnalytics() { const recent = this.metrics.slice(-50); - if (recent.length === 0) return { message: 'No metrics available' }; - - const avgTokens = recent.reduce((sum, m) => sum + m.tokens, 0) / recent.length; - const successRate = recent.filter(m => m.success).length / recent.length; + if (recent.length === 0) return { message: "No metrics available" }; + + const avgTokens = + recent.reduce((sum, m) => sum + m.tokens, 0) / recent.length; + const successRate = recent.filter((m) => m.success).length / recent.length; const operationBreakdown = {}; const methodBreakdown = {}; - - recent.forEach(m => { - operationBreakdown[m.operation] = (operationBreakdown[m.operation] || 0) + 1; + + recent.forEach((m) => { + operationBreakdown[m.operation] = + (operationBreakdown[m.operation] || 0) + 1; methodBreakdown[m.method] = (methodBreakdown[m.method] || 0) + 1; }); - + return { totalOperations: recent.length, avgTokensPerOperation: Math.round(avgTokens), @@ -242,7 +355,7 @@ class TokenOptimizer { recommendedMaxResults: this.recommendedMaxResults, operationBreakdown, methodBreakdown, - tokenSavings: this.calculateTokenSavings(recent) + tokenSavings: this.calculateTokenSavings(recent), }; } @@ -252,12 +365,12 @@ class TokenOptimizer { const estimatedNaiveTokens = metrics.length * 300; // Estimate for non-optimized approach const savings = estimatedNaiveTokens - actualTokens; const percentage = Math.round((savings / estimatedNaiveTokens) * 100); - + return { actualTokens, estimatedNaiveTokens, tokensSaved: savings, - percentageSaved: percentage + percentageSaved: percentage, }; } } @@ -272,28 +385,33 @@ class BrowserAutomation { this.setupMessageListener(); this.setupViewportAnalyzer(); } - + setupViewportAnalyzer() { this.visibilityMap = new Map(); - this.observer = new IntersectionObserver((entries) => { - entries.forEach(entry => { - this.visibilityMap.set(entry.target, { - visible: entry.isIntersecting, - ratio: entry.intersectionRatio + this.observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + this.visibilityMap.set(entry.target, { + visible: entry.isIntersecting, + ratio: entry.intersectionRatio, + }); }); - }); - }, { threshold: [0, 0.1, 0.5, 1.0] }); + }, + { threshold: [0, 0.1, 0.5, 1.0] } + ); } setupMessageListener() { chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { - this.handleMessage(message).then(sendResponse).catch(error => { - sendResponse({ - success: false, - error: error.message, - stack: error.stack + this.handleMessage(message) + .then(sendResponse) + .catch((error) => { + sendResponse({ + success: false, + error: error.message, + stack: error.stack, + }); }); - }); return true; // Keep message channel open for async response }); } @@ -301,42 +419,43 @@ class BrowserAutomation { async handleMessage(message) { const { action, data } = message; const startTime = performance.now(); - + try { let result; switch (action) { - case 'analyze': + case "analyze": result = await this.analyzePage(data); break; - case 'extract_content': + case "extract_content": result = await this.extractContent(data); break; - case 'element_click': + case "element_click": result = await this.clickElement(data); break; - case 'element_fill': - result = await this.fillElement(data); + case "element_fill": + // šŸŽÆ CRITICAL: Anti-Detection Bypass Implementation + result = await this.fillElementWithAntiDetection(data); break; - case 'wait_for': + case "wait_for": result = await this.waitForCondition(data); break; - case 'getPageInfo': + case "getPageInfo": result = { title: document.title, url: window.location.href, - content: document.body.innerText + content: document.body.innerText, }; break; - case 'get_analytics': + case "get_analytics": result = this.tokenOptimizer.getAnalytics(); break; - case 'clear_analytics': - localStorage.removeItem('opendia_metrics'); - localStorage.removeItem('opendia_success_rates'); + case "clear_analytics": + localStorage.removeItem("opendia_metrics"); + localStorage.removeItem("opendia_success_rates"); this.tokenOptimizer = new TokenOptimizer(); - result = { message: 'Analytics cleared successfully' }; + result = { message: "Analytics cleared successfully" }; break; - case 'get_element_state': + case "get_element_state": const element = this.getElementById(data.element_id); if (!element) { throw new Error(`Element not found: ${data.element_id}`); @@ -345,270 +464,717 @@ class BrowserAutomation { element_id: data.element_id, element_name: this.getElementName(element), state: this.getElementState(element), - current_value: this.getElementValue(element) + current_value: this.getElementValue(element), }; break; default: throw new Error(`Unknown action: ${action}`); } - + const executionTime = performance.now() - startTime; const dataSize = new Blob([JSON.stringify(result)]).size; - + return { success: true, data: result, execution_time: Math.round(executionTime), data_size: dataSize, - timestamp: Date.now() + timestamp: Date.now(), }; - } catch (error) { return { success: false, error: error.message, stack: error.stack, - execution_time: Math.round(performance.now() - startTime) + execution_time: Math.round(performance.now() - startTime), }; } } - async analyzePage({ intent_hint, phase = 'discover', focus_areas, element_ids, max_results = 5 }) { - const startTime = performance.now(); - - // Two-phase approach - if (phase === 'discover') { - return await this.quickDiscovery({ intent_hint, max_results }); - } else if (phase === 'detailed') { - return await this.detailedAnalysis({ intent_hint, focus_areas, element_ids, max_results }); + // šŸŽÆ ANTI-DETECTION BYPASS METHOD + async fillElementWithAntiDetection({ + element_id, + value, + clear_first = true, + force_focus = true, + }) { + const element = this.getElementById(element_id); + if (!element) { + throw new Error(`Element not found: ${element_id}`); } - + + const hostname = window.location.hostname; + const platformConfig = this.detectAntiDetectionPlatform(hostname); + + if (platformConfig && this.shouldUseBypass(element, platformConfig)) { + console.log( + `šŸŽÆ Using ${platformConfig.bypassMethod} bypass for ${hostname}` + ); + return await this.executeDirectBypass( + element, + value, + platformConfig, + element_id + ); + } else { + // Use normal fillElement for non-detection platforms + console.log("šŸ”§ Using standard fill method"); + return await this.fillElementStandard({ + element_id, + value, + clear_first, + force_focus, + }); + } + } + + detectAntiDetectionPlatform(hostname) { + // Check exact matches first + if (ANTI_DETECTION_PLATFORMS[hostname]) { + return ANTI_DETECTION_PLATFORMS[hostname]; + } + + // Check subdomain matches + for (const [domain, config] of Object.entries(ANTI_DETECTION_PLATFORMS)) { + if (hostname.includes(domain) || hostname.endsWith(`.${domain}`)) { + return config; + } + } + + return null; + } + + shouldUseBypass(element, platformConfig) { + // Check if element matches platform-specific selectors + try { + const isTextarea = + document.querySelector(platformConfig.selectors.textarea) === element; + if (isTextarea) return true; + + // Also check if element matches any textarea selector pattern + const textareaSelectors = platformConfig.selectors.textarea.split(", "); + return textareaSelectors.some((selector) => { + try { + return element.matches(selector); + } catch { + return false; + } + }); + } catch (error) { + console.warn("Bypass detection failed:", error); + return false; + } + } + + async executeDirectBypass(element, value, platformConfig, element_id) { + try { + console.log(`🐦 Executing ${platformConfig.bypassMethod} bypass`); + + switch (platformConfig.bypassMethod) { + case "twitter_direct": + return await this.twitterDirectBypass(element, value, element_id); + case "linkedin_direct": + return await this.linkedinDirectBypass(element, value, element_id); + case "facebook_direct": + return await this.facebookDirectBypass(element, value, element_id); + default: + // Generic direct bypass + return await this.genericDirectBypass(element, value, element_id); + } + } catch (error) { + console.error( + "Direct bypass failed, falling back to standard method:", + error + ); + return await this.fillElementStandard({ + element_id, + value, + clear_first: true, + force_focus: true, + }); + } + } + + async twitterDirectBypass(element, value, element_id) { + // THE WORKING FORMULA FOR TWITTER: + // 1. Focus 2. Click 3. execCommand + console.log("🐦 Twitter direct bypass - focus+click+execCommand"); + + // Ensure element is in view + element.scrollIntoView({ behavior: "smooth", block: "center" }); + await new Promise((r) => setTimeout(r, 200)); + + // The magic sequence that bypasses Twitter detection + element.focus(); + element.click(); + const execResult = document.execCommand("insertText", false, value); + + // Wait for React state to update + await new Promise((r) => setTimeout(r, 500)); + + // Verify success + const currentText = element.textContent || element.value || ""; + const success = currentText.includes(value); + + return { + success: success, + element_id: element_id, + value: value, + actual_value: currentText, + method: "twitter_direct_bypass", + execCommand_result: execResult, + element_name: this.getElementName(element), + }; + } + + async linkedinDirectBypass(element, value, element_id) { + console.log("šŸ’¼ LinkedIn direct bypass"); + + element.scrollIntoView({ behavior: "smooth", block: "center" }); + await new Promise((r) => setTimeout(r, 200)); + + // LinkedIn-specific sequence + element.focus(); + element.click(); + + // Clear existing content first for LinkedIn + if (element.textContent) { + document.execCommand("selectAll"); + document.execCommand("delete"); + } + + const execResult = document.execCommand("insertText", false, value); + + // LinkedIn needs more time for state updates + await new Promise((r) => setTimeout(r, 800)); + + const currentText = element.textContent || element.value || ""; + const success = currentText.includes(value); + + return { + success: success, + element_id: element_id, + value: value, + actual_value: currentText, + method: "linkedin_direct_bypass", + execCommand_result: execResult, + element_name: this.getElementName(element), + }; + } + + async facebookDirectBypass(element, value, element_id) { + console.log("šŸ“˜ Facebook direct bypass"); + + element.scrollIntoView({ behavior: "smooth", block: "center" }); + await new Promise((r) => setTimeout(r, 200)); + + // Facebook-specific sequence + element.focus(); + element.click(); + + // Facebook may need selection clearing + if (element.textContent) { + document.execCommand("selectAll"); + document.execCommand("delete"); + } + + const execResult = document.execCommand("insertText", false, value); + + // Trigger Facebook-specific events + element.dispatchEvent(new Event("input", { bubbles: true })); + element.dispatchEvent(new Event("change", { bubbles: true })); + + await new Promise((r) => setTimeout(r, 600)); + + const currentText = element.textContent || element.value || ""; + const success = currentText.includes(value); + + return { + success: success, + element_id: element_id, + value: value, + actual_value: currentText, + method: "facebook_direct_bypass", + execCommand_result: execResult, + element_name: this.getElementName(element), + }; + } + + async genericDirectBypass(element, value, element_id) { + console.log("šŸ”§ Generic direct bypass"); + + element.scrollIntoView({ behavior: "smooth", block: "center" }); + await new Promise((r) => setTimeout(r, 200)); + + // Generic direct sequence + element.focus(); + element.click(); + const execResult = document.execCommand("insertText", false, value); + + await new Promise((r) => setTimeout(r, 500)); + + const currentText = element.textContent || element.value || ""; + const success = currentText.includes(value); + + return { + success: success, + element_id: element_id, + value: value, + actual_value: currentText, + method: "generic_direct_bypass", + execCommand_result: execResult, + element_name: this.getElementName(element), + }; + } + + // Standard fill method (unchanged for compatibility) + async fillElementStandard({ + element_id, + value, + clear_first = true, + force_focus = true, + }) { + const element = this.getElementById(element_id); + if (!element) { + throw new Error(`Element not found: ${element_id}`); + } + + // Enhanced focus sequence for modern web apps + if (force_focus) { + await this.ensureProperFocus(element); + } else { + element.focus(); + } + + // Clear existing content if requested + if (clear_first) { + await this.clearElementContent(element); + } + + // Fill the value with proper event sequence + await this.fillWithEvents(element, value); + + // Validate the fill was successful + const actualValue = this.getElementValue(element); + const success = actualValue.includes(value); + + return { + success, + element_id, + value, + actual_value: actualValue, + element_name: this.getElementName(element), + method: "standard_fill", + focus_applied: force_focus, + }; + } + + async analyzePage({ + intent_hint, + phase = "discover", + focus_areas, + element_ids, + max_results = 5, + }) { + const startTime = performance.now(); + + // Two-phase approach + if (phase === "discover") { + return await this.quickDiscovery({ intent_hint, max_results }); + } else if (phase === "detailed") { + return await this.detailedAnalysis({ + intent_hint, + focus_areas, + element_ids, + max_results, + }); + } + // Legacy single-phase approach for backward compatibility - return await this.legacyAnalysis({ intent_hint, focus_area: focus_areas?.[0], max_results }); + return await this.legacyAnalysis({ + intent_hint, + focus_area: focus_areas?.[0], + max_results, + }); } async quickDiscovery({ intent_hint, max_results = 5 }) { const startTime = performance.now(); - + // Detect page type and get basic metrics const pageType = this.detectPageType(); const viewportElements = this.countViewportElements(); - + // Use token optimizer recommendations - const recommendedMaxResults = this.tokenOptimizer.getRecommendedMaxResults(); + const recommendedMaxResults = + this.tokenOptimizer.getRecommendedMaxResults(); max_results = Math.min(max_results, recommendedMaxResults); - + // Get best method based on historical performance const bestMethod = this.tokenOptimizer.getBestMethod(pageType, intent_hint); - + // Try to find obvious matches using enhanced patterns let quickMatches = []; - let usedMethod = 'quick_discovery'; - + let usedMethod = "quick_discovery"; + try { - if (bestMethod === 'enhanced_pattern_match' || bestMethod === 'pattern_database') { + // Check for anti-detection bypass first + const hostname = window.location.hostname; + const platformConfig = this.detectAntiDetectionPlatform(hostname); + + if ( + platformConfig && + (intent_hint.includes("post") || intent_hint.includes("tweet")) + ) { + const bypassResult = await this.tryAntiDetectionPatterns( + intent_hint, + platformConfig + ); + if (bypassResult.confidence > 0.9) { + quickMatches = bypassResult.elements + .slice(0, 3) + .map((el) => this.compressElement(el, true)); + usedMethod = "anti_detection_bypass"; + } + } + + // Fallback to enhanced patterns + if ( + quickMatches.length === 0 && + (bestMethod === "enhanced_pattern_match" || + bestMethod === "pattern_database") + ) { const patternResult = await this.tryEnhancedPatterns(intent_hint); if (patternResult.confidence > 0.7) { - quickMatches = patternResult.elements.slice(0, 3).map(el => this.compressElement(el, true)); - usedMethod = 'enhanced_patterns'; + quickMatches = patternResult.elements + .slice(0, 3) + .map((el) => this.compressElement(el, true)); + usedMethod = "enhanced_patterns"; } } } catch (error) { - console.warn('Enhanced patterns failed:', error); + console.warn("Enhanced patterns failed:", error); } - + // If no pattern matches, do a quick viewport scan if (quickMatches.length === 0) { quickMatches = await this.quickViewportScan(intent_hint, 3); - usedMethod = 'viewport_scan'; + usedMethod = "viewport_scan"; } - + const intentMatch = this.scoreIntentMatch(intent_hint, quickMatches); const suggestedAreas = this.suggestPhase2Areas(quickMatches, intent_hint); const executionTime = Math.round(performance.now() - startTime); - + const result = { summary: { page_type: pageType, intent_match: intentMatch, element_count: viewportElements, viewport_elements: quickMatches.length, - suggested_phase2: suggestedAreas + suggested_phase2: suggestedAreas, + anti_detection_platform: this.detectAntiDetectionPlatform( + window.location.hostname + ) + ? window.location.hostname + : null, }, quick_matches: quickMatches, token_estimate: this.estimatePhase2Tokens(quickMatches), method: usedMethod, execution_time: executionTime, intent_hint: intent_hint, // Add this for server.js compatibility - elements: quickMatches // Add this for backward compatibility + elements: quickMatches, // Add this for backward compatibility }; - + // Track token usage and success const tokenCount = this.estimateTokenUsage(result); - const success = quickMatches.length > 0 && intentMatch !== 'none'; - this.tokenOptimizer.trackUsage('page_analyze_discover', tokenCount, success, pageType, usedMethod); - this.tokenOptimizer.trackMethodSuccess(pageType, intent_hint, usedMethod, success); - + const success = quickMatches.length > 0 && intentMatch !== "none"; + this.tokenOptimizer.trackUsage( + "page_analyze_discover", + tokenCount, + success, + pageType, + usedMethod + ); + this.tokenOptimizer.trackMethodSuccess( + pageType, + intent_hint, + usedMethod, + success + ); + return result; } - async detailedAnalysis({ intent_hint, focus_areas, element_ids, max_results = 10 }) { + async tryAntiDetectionPatterns(intent_hint, platformConfig) { + const elements = []; + + // Try to find platform-specific elements + try { + const textareaElement = document.querySelector( + platformConfig.selectors.textarea + ); + if (textareaElement && this.isLikelyVisible(textareaElement)) { + const elementId = this.registerElement(textareaElement); + elements.push({ + id: elementId, + type: "textarea", + selector: platformConfig.selectors.textarea, + name: this.getElementName(textareaElement), + confidence: 0.95, // High confidence for anti-detection platforms + element: textareaElement, + }); + } + + const submitElement = document.querySelector( + platformConfig.selectors.submit + ); + if (submitElement && this.isLikelyVisible(submitElement)) { + const elementId = this.registerElement(submitElement); + elements.push({ + id: elementId, + type: "button", + selector: platformConfig.selectors.submit, + name: this.getElementName(submitElement), + confidence: 0.95, + element: submitElement, + }); + } + } catch (error) { + console.warn("Anti-detection pattern matching failed:", error); + } + + return { + elements, + confidence: elements.length > 0 ? 0.95 : 0, + method: "anti_detection_patterns", + platform: window.location.hostname, + }; + } + + async detailedAnalysis({ + intent_hint, + focus_areas, + element_ids, + max_results = 10, + }) { const startTime = performance.now(); const pageType = this.detectPageType(); - + // Use token optimizer recommendations - const recommendedMaxResults = this.tokenOptimizer.getRecommendedMaxResults(); + const recommendedMaxResults = + this.tokenOptimizer.getRecommendedMaxResults(); max_results = Math.min(max_results, recommendedMaxResults + 2); // Allow slightly more for detailed analysis - + let elements = []; - let method = 'detailed_analysis'; - + let method = "detailed_analysis"; + // Expand specific quick matches if provided if (element_ids?.length) { elements = await this.expandQuickMatches(element_ids); - method = 'expanded_matches'; + method = "expanded_matches"; } - + // Analyze specific focus areas if (focus_areas?.length) { - const areaElements = await this.analyzeFocusAreas(focus_areas, intent_hint); + const areaElements = await this.analyzeFocusAreas( + focus_areas, + intent_hint + ); elements = [...elements, ...areaElements]; - method = elements.length > 0 ? 'focus_area_analysis' : method; + method = elements.length > 0 ? "focus_area_analysis" : method; } - + // If no specific analysis requested, do full enhanced analysis if (elements.length === 0) { elements = await this.fullEnhancedAnalysis(intent_hint, max_results); - method = 'full_enhanced_analysis'; + method = "full_enhanced_analysis"; } - + // Deduplicate and enhance with metadata elements = this.deduplicateElements(elements); elements = await this.enhanceElementMetadata(elements); - + // Apply compact fingerprinting - elements = elements.slice(0, max_results).map(el => this.compressElement(el, false)); - + elements = elements + .slice(0, max_results) + .map((el) => this.compressElement(el, false)); + const executionTime = Math.round(performance.now() - startTime); const result = { elements, - interaction_ready: elements.every(el => el.conf > 50), + interaction_ready: elements.every((el) => el.conf > 50), method, execution_time: executionTime, - intent_hint: intent_hint // Add this for server.js compatibility + intent_hint: intent_hint, // Add this for server.js compatibility }; - + // Track token usage and success const tokenCount = this.estimateTokenUsage(result); - const success = elements.length > 0 && elements.some(el => el.conf > 70); - this.tokenOptimizer.trackUsage('page_analyze_detailed', tokenCount, success, pageType, method); - this.tokenOptimizer.trackMethodSuccess(pageType, intent_hint, method, success); - + const success = elements.length > 0 && elements.some((el) => el.conf > 70); + this.tokenOptimizer.trackUsage( + "page_analyze_detailed", + tokenCount, + success, + pageType, + method + ); + this.tokenOptimizer.trackMethodSuccess( + pageType, + intent_hint, + method, + success + ); + return result; } async legacyAnalysis({ intent_hint, focus_area, max_results = 5 }) { const startTime = performance.now(); let result; - + try { // Try enhanced patterns first result = await this.tryEnhancedPatterns(intent_hint); if (result.confidence > 0.8) { - return this.formatAnalysisResult(result, 'enhanced_patterns', startTime); + return this.formatAnalysisResult( + result, + "enhanced_patterns", + startTime + ); } } catch (error) { - console.warn('Enhanced patterns failed, trying legacy patterns:', error); + console.warn("Enhanced patterns failed, trying legacy patterns:", error); try { // Fallback to legacy pattern database result = await this.tryPatternDatabase(intent_hint); if (result.confidence > 0.8) { - return this.formatAnalysisResult(result, 'pattern_database', startTime); + return this.formatAnalysisResult( + result, + "pattern_database", + startTime + ); } } catch (legacyError) { - console.warn('Legacy pattern database failed:', legacyError); + console.warn("Legacy pattern database failed:", legacyError); } } - + // Final fallback to semantic analysis result = await this.trySemanticAnalysis(intent_hint, focus_area); - return this.formatAnalysisResult(result, 'semantic_analysis', startTime); + return this.formatAnalysisResult(result, "semantic_analysis", startTime); } async tryEnhancedPatterns(intent_hint) { const [category, action] = this.parseIntent(intent_hint); const pattern = ENHANCED_PATTERNS[category]?.[action]; - + if (!pattern) { return this.tryUniversalPatterns(intent_hint); } - + const elements = this.findPatternElements(pattern); return { elements: elements.slice(0, 3), confidence: pattern.confidence, - method: 'enhanced_pattern_match', + method: "enhanced_pattern_match", category, - action + action, }; } parseIntent(intent) { const intentLower = intent.toLowerCase(); - + // Check for authentication patterns - if (intentLower.includes('login') || intentLower.includes('sign in') || intentLower.includes('log in')) { - return ['auth', 'login']; + if ( + intentLower.includes("login") || + intentLower.includes("sign in") || + intentLower.includes("log in") + ) { + return ["auth", "login"]; } - if (intentLower.includes('signup') || intentLower.includes('sign up') || intentLower.includes('register') || intentLower.includes('create account')) { - return ['auth', 'signup']; + if ( + intentLower.includes("signup") || + intentLower.includes("sign up") || + intentLower.includes("register") || + intentLower.includes("create account") + ) { + return ["auth", "signup"]; } - + // Check for content creation patterns - if (intentLower.includes('tweet') || intentLower.includes('post') || intentLower.includes('compose') || - intentLower.includes('create') || intentLower.includes('write') || intentLower.includes('publish')) { - return ['content', 'post_create']; + if ( + intentLower.includes("tweet") || + intentLower.includes("post") || + intentLower.includes("compose") || + intentLower.includes("create") || + intentLower.includes("write") || + intentLower.includes("publish") + ) { + return ["content", "post_create"]; } - if (intentLower.includes('comment') || intentLower.includes('reply')) { - return ['content', 'comment']; + if (intentLower.includes("comment") || intentLower.includes("reply")) { + return ["content", "comment"]; } - + // Check for search patterns - if (intentLower.includes('search') || intentLower.includes('find') || intentLower.includes('look for')) { - return ['search', 'global']; + if ( + intentLower.includes("search") || + intentLower.includes("find") || + intentLower.includes("look for") + ) { + return ["search", "global"]; } - + // Check for navigation patterns - if (intentLower.includes('menu') || intentLower.includes('navigation') || intentLower.includes('nav')) { - return ['nav', 'menu']; + if ( + intentLower.includes("menu") || + intentLower.includes("navigation") || + intentLower.includes("nav") + ) { + return ["nav", "menu"]; } - + // Check for form patterns - if (intentLower.includes('submit') || intentLower.includes('send') || intentLower.includes('save')) { - return ['form', 'submit']; + if ( + intentLower.includes("submit") || + intentLower.includes("send") || + intentLower.includes("save") + ) { + return ["form", "submit"]; } - if (intentLower.includes('reset') || intentLower.includes('clear') || intentLower.includes('cancel')) { - return ['form', 'reset']; + if ( + intentLower.includes("reset") || + intentLower.includes("clear") || + intentLower.includes("cancel") + ) { + return ["form", "reset"]; } - + // Fallback - try to infer from context - if (intentLower.includes('button') || intentLower.includes('click')) { - return ['form', 'submit']; + if (intentLower.includes("button") || intentLower.includes("click")) { + return ["form", "submit"]; } - if (intentLower.includes('input') || intentLower.includes('field') || intentLower.includes('text')) { - return ['content', 'post_create']; + if ( + intentLower.includes("input") || + intentLower.includes("field") || + intentLower.includes("text") + ) { + return ["content", "post_create"]; } - + // Default fallback - return ['content', 'post_create']; // More useful default than search + return ["content", "post_create"]; // More useful default than search } findPatternElements(pattern) { const elements = []; - + for (const [elementType, selectors] of Object.entries(pattern)) { - if (elementType === 'confidence') continue; - + if (elementType === "confidence") continue; + for (const selector of selectors) { const element = document.querySelector(selector); if (element && this.isLikelyVisible(element)) { @@ -619,60 +1185,69 @@ class BrowserAutomation { selector: selector, name: this.getElementName(element), confidence: pattern.confidence || 0.8, - element: element + element: element, }); break; // Take first match per element type } } } - + return elements; } tryUniversalPatterns(intent_hint) { const intentLower = intent_hint.toLowerCase(); let selectors = []; - + // Content creation patterns - if (intentLower.includes('tweet') || intentLower.includes('post') || intentLower.includes('compose') || - intentLower.includes('create') || intentLower.includes('write')) { + if ( + intentLower.includes("tweet") || + intentLower.includes("post") || + intentLower.includes("compose") || + intentLower.includes("create") || + intentLower.includes("write") + ) { selectors = [ - "[contenteditable='true']", - "textarea[placeholder*='tweet' i]", + "[data-testid='tweetTextarea_0']", // Twitter first! + "[contenteditable='true']", + "textarea[placeholder*='tweet' i]", "textarea[placeholder*='post' i]", "textarea[placeholder*='what' i]", "[data-text='true']", "[role='textbox']", - "textarea:not([style*='display: none'])" + "textarea:not([style*='display: none'])", ]; } - // Authentication patterns - else if (intentLower.includes('login') || intentLower.includes('sign in')) { + // Authentication patterns + else if (intentLower.includes("login") || intentLower.includes("sign in")) { selectors = [ - "[type='email']", - "[name*='username' i]", + "[type='email']", + "[name*='username' i]", "[placeholder*='email' i]", "[placeholder*='username' i]", - "input[name*='login' i]" + "input[name*='login' i]", ]; - } - else if (intentLower.includes('signup') || intentLower.includes('register')) { + } else if ( + intentLower.includes("signup") || + intentLower.includes("register") + ) { selectors = [ - "[href*='signup']", - ".signup-btn", + "[href*='signup']", + ".signup-btn", "[aria-label*='register' i]", "button[data-testid*='signup' i]", - "a[href*='register']" + "a[href*='register']", ]; } // Search patterns - else if (intentLower.includes('search') || intentLower.includes('find')) { + else if (intentLower.includes("search") || intentLower.includes("find")) { selectors = [ - "[type='search']", - "[role='searchbox']", + "[data-testid='SearchBox_Search_Input']", // Twitter search first + "[type='search']", + "[role='searchbox']", "[placeholder*='search' i]", "[data-testid*='search' i]", - "input[name*='search' i]" + "input[name*='search' i]", ]; } // Generic fallback - look for interactive elements @@ -683,12 +1258,12 @@ class BrowserAutomation { "textarea", "[type='submit']", "[role='button']", - "input[type='text']" + "input[type='text']", ]; } - + const elements = []; - + for (const selector of selectors) { const foundElements = document.querySelectorAll(selector); for (const element of foundElements) { @@ -699,41 +1274,45 @@ class BrowserAutomation { type: this.inferElementType(element, intent_hint), selector: selector, name: this.getElementName(element), - confidence: 0.5 + (this.calculateConfidence(element, intent_hint) * 0.3), - element: element + confidence: + 0.5 + this.calculateConfidence(element, intent_hint) * 0.3, + element: element, }); if (elements.length >= 3) break; // Limit to 3 elements } } if (elements.length >= 3) break; } - + return { elements, - confidence: elements.length > 0 ? Math.max(...elements.map(e => e.confidence)) : 0, - method: 'universal_pattern' + confidence: + elements.length > 0 + ? Math.max(...elements.map((e) => e.confidence)) + : 0, + method: "universal_pattern", }; } async tryPatternDatabase(intentHint) { const hostname = window.location.hostname; const siteKey = this.detectSite(hostname); - - if (siteKey === 'universal') { + + if (siteKey === "universal") { return this.getUniversalPattern(intentHint); } - + const siteConfig = PATTERN_DATABASE[siteKey]; const pattern = siteConfig?.patterns?.[intentHint]; - + if (!pattern) { throw new Error(`No pattern found for ${intentHint} on ${siteKey}`); } - + const elements = []; for (const [elementType, selector] of Object.entries(pattern)) { - if (elementType === 'confidence') continue; - + if (elementType === "confidence") continue; + const element = document.querySelector(selector); if (element) { const elementId = this.registerElement(element); @@ -742,38 +1321,40 @@ class BrowserAutomation { type: elementType, selector: selector, name: this.getElementName(element), - confidence: pattern.confidence || 0.8 + confidence: pattern.confidence || 0.8, }); } } - + return { elements, confidence: pattern.confidence || 0.8, - site: siteKey + site: siteKey, }; } detectSite(hostname) { for (const [siteKey, config] of Object.entries(PATTERN_DATABASE)) { - if (siteKey === 'universal') continue; - if (config.domains?.some(domain => - hostname === domain || hostname.endsWith(`.${domain}`) - )) { + if (siteKey === "universal") continue; + if ( + config.domains?.some( + (domain) => hostname === domain || hostname.endsWith(`.${domain}`) + ) + ) { return siteKey; } } - return 'universal'; + return "universal"; } getUniversalPattern(intentHint) { const universalPatterns = PATTERN_DATABASE.universal; const pattern = universalPatterns[intentHint]; - + if (!pattern) { throw new Error(`No universal pattern for ${intentHint}`); } - + const elements = []; for (const selector of pattern.selectors) { const element = document.querySelector(selector); @@ -784,16 +1365,16 @@ class BrowserAutomation { type: intentHint, selector: selector, name: this.getElementName(element), - confidence: pattern.confidence + confidence: pattern.confidence, }); break; // Take first match for universal patterns } } - + return { elements, confidence: pattern.confidence, - site: 'universal' + site: "universal", }; } @@ -803,86 +1384,97 @@ class BrowserAutomation { [role="button"], [role="textbox"], [role="searchbox"], [aria-label], [data-testid] `); - + const elements = Array.from(relevantElements) - .filter(el => this.isVisible(el)) + .filter((el) => this.isVisible(el)) .slice(0, 20) - .map(element => { + .map((element) => { const elementId = this.registerElement(element); return { id: elementId, type: this.inferElementType(element, intentHint), selector: this.generateSelector(element), name: this.getElementName(element), - confidence: this.calculateConfidence(element, intentHint) + confidence: this.calculateConfidence(element, intentHint), }; }) - .filter(el => el.confidence > 0.3) + .filter((el) => el.confidence > 0.3) .sort((a, b) => b.confidence - a.confidence); - + return { elements, - confidence: elements.length > 0 ? Math.max(...elements.map(e => e.confidence)) : 0 + confidence: + elements.length > 0 + ? Math.max(...elements.map((e) => e.confidence)) + : 0, }; } async extractContent({ content_type, max_items = 20, summarize = true }) { const startTime = performance.now(); const extractors = { - 'article': () => this.extractArticleContent(), - 'search_results': () => this.extractSearchResults(max_items), - 'posts': () => this.extractPosts(max_items) + article: () => this.extractArticleContent(), + search_results: () => this.extractSearchResults(max_items), + posts: () => this.extractPosts(max_items), }; - + const extractor = extractors[content_type]; if (!extractor) { throw new Error(`Unknown content type: ${content_type}`); } - + const rawContent = extractor(); - + if (summarize) { // Return summary instead of full content to save tokens return { content_type, summary: this.summarizeContent(rawContent, content_type), items_found: Array.isArray(rawContent) ? rawContent.length : 1, - sample_items: Array.isArray(rawContent) ? rawContent.slice(0, 3) : [rawContent], + sample_items: Array.isArray(rawContent) + ? rawContent.slice(0, 3) + : [rawContent], extraction_method: this.getExtractionMethod(content_type), token_estimate: this.estimateContentTokens(rawContent), execution_time: Math.round(performance.now() - startTime), - extracted_at: new Date().toISOString() + extracted_at: new Date().toISOString(), }; } else { // Legacy full content extraction return { content: rawContent, - method: 'semantic_extraction', + method: "semantic_extraction", content_type: content_type, execution_time: Math.round(performance.now() - startTime), - extracted_at: new Date().toISOString() + extracted_at: new Date().toISOString(), }; } } extractArticleContent() { - const article = document.querySelector('article, [role="article"], .article-content, main'); - const title = document.querySelector('h1, .article-title, .post-title')?.textContent?.trim(); + const article = document.querySelector( + 'article, [role="article"], .article-content, main' + ); + const title = document + .querySelector("h1, .article-title, .post-title") + ?.textContent?.trim(); const content = article?.textContent?.trim() || this.extractMainContent(); - + return { title, content, - word_count: content?.split(/\s+/).length || 0 + word_count: content?.split(/\s+/).length || 0, }; } extractMainContent() { // Simple heuristic to find main content - const candidates = document.querySelectorAll('main, .content, .post-content, .article-body'); + const candidates = document.querySelectorAll( + "main, .content, .post-content, .article-body" + ); let bestCandidate = null; let maxTextLength = 0; - + for (const candidate of candidates) { const textLength = candidate.textContent.trim().length; if (textLength > maxTextLength) { @@ -890,36 +1482,40 @@ class BrowserAutomation { bestCandidate = candidate; } } - - return bestCandidate?.textContent?.trim() || document.body.textContent.trim(); + + return ( + bestCandidate?.textContent?.trim() || document.body.textContent.trim() + ); } extractSearchResults(max_items = 20) { // Common search result patterns const selectors = [ '.search-result, .result-item, [data-testid*="result"]', - '.g, .result, .search-item', // Google-style + ".g, .result, .search-item", // Google-style 'li[data-testid="search-result"], .SearchResult', // Twitter/X - '.Box-row, .issue-list-item', // GitHub - 'article, .post, .entry' // Generic content + ".Box-row, .issue-list-item", // GitHub + "article, .post, .entry", // Generic content ]; - + let results = []; for (const selector of selectors) { const elements = document.querySelectorAll(selector); if (elements.length > 0) { - results = Array.from(elements).slice(0, max_items).map((el, index) => ({ - index: index + 1, - title: this.extractResultTitle(el), - summary: this.extractResultSummary(el), - link: this.extractResultLink(el), - type: this.detectResultType(el), - score: this.scoreSearchResult(el) - })); + results = Array.from(elements) + .slice(0, max_items) + .map((el, index) => ({ + index: index + 1, + title: this.extractResultTitle(el), + summary: this.extractResultSummary(el), + link: this.extractResultLink(el), + type: this.detectResultType(el), + score: this.scoreSearchResult(el), + })); break; } } - + return results; } @@ -928,157 +1524,184 @@ class BrowserAutomation { const selectors = [ '[data-testid="tweet"], .tweet, .post', 'article[role="article"]', // Twitter/X posts - '.timeline-item, .feed-item', - '.status, .update, .entry' + ".timeline-item, .feed-item", + ".status, .update, .entry", ]; - + let posts = []; for (const selector of selectors) { const elements = document.querySelectorAll(selector); if (elements.length > 0) { - posts = Array.from(elements).slice(0, max_items).map((el, index) => ({ - index: index + 1, - text: this.extractPostText(el), - author: this.extractPostAuthor(el), - timestamp: this.extractPostTimestamp(el), - metrics: this.extractPostMetrics(el), - has_media: this.hasPostMedia(el), - post_type: this.detectPostType(el) - })); + posts = Array.from(elements) + .slice(0, max_items) + .map((el, index) => ({ + index: index + 1, + text: this.extractPostText(el), + author: this.extractPostAuthor(el), + timestamp: this.extractPostTimestamp(el), + metrics: this.extractPostMetrics(el), + has_media: this.hasPostMedia(el), + post_type: this.detectPostType(el), + })); break; } } - + return posts; } // Content summarization methods summarizeContent(content, content_type) { - switch(content_type) { - case 'article': + switch (content_type) { + case "article": return this.summarizeArticle(content); - case 'search_results': + case "search_results": return this.summarizeSearchResults(content); - case 'posts': + case "posts": return this.summarizePosts(content); default: - return { summary: 'Unknown content type' }; + return { summary: "Unknown content type" }; } } summarizeArticle(content) { return { - title: content.title || 'Untitled', + title: content.title || "Untitled", word_count: content.word_count || 0, reading_time: Math.ceil((content.word_count || 0) / 200), - has_images: document.querySelectorAll('img').length > 0, - has_videos: document.querySelectorAll('video, iframe[src*="youtube"], iframe[src*="vimeo"]').length > 0, - preview: content.content?.substring(0, 200) + (content.content?.length > 200 ? '...' : ''), - estimated_tokens: Math.ceil((content.content?.length || 0) / 4) + has_images: document.querySelectorAll("img").length > 0, + has_videos: + document.querySelectorAll( + 'video, iframe[src*="youtube"], iframe[src*="vimeo"]' + ).length > 0, + preview: + content.content?.substring(0, 200) + + (content.content?.length > 200 ? "..." : ""), + estimated_tokens: Math.ceil((content.content?.length || 0) / 4), }; } summarizeSearchResults(results) { - const domains = results.map(r => r.link).filter(Boolean) - .map(url => { - try { return new URL(url).hostname; } catch { return null; } - }).filter(Boolean); - + const domains = results + .map((r) => r.link) + .filter(Boolean) + .map((url) => { + try { + return new URL(url).hostname; + } catch { + return null; + } + }) + .filter(Boolean); + return { total_results: results.length, - result_types: [...new Set(results.map(r => r.type))], + result_types: [...new Set(results.map((r) => r.type))], top_domains: this.getTopDomains(domains), - avg_score: results.reduce((sum, r) => sum + (r.score || 0), 0) / results.length, - has_sponsored: results.some(r => r.type === 'sponsored'), - quality_score: this.calculateQualityScore(results) + avg_score: + results.reduce((sum, r) => sum + (r.score || 0), 0) / results.length, + has_sponsored: results.some((r) => r.type === "sponsored"), + quality_score: this.calculateQualityScore(results), }; } summarizePosts(posts) { - const totalTextLength = posts.reduce((sum, p) => sum + (p.text?.length || 0), 0); - const totalLikes = posts.reduce((sum, p) => sum + (p.metrics?.likes || 0), 0); - + const totalTextLength = posts.reduce( + (sum, p) => sum + (p.text?.length || 0), + 0 + ); + const totalLikes = posts.reduce( + (sum, p) => sum + (p.metrics?.likes || 0), + 0 + ); + return { post_count: posts.length, avg_length: Math.round(totalTextLength / posts.length), - has_media_count: posts.filter(p => p.has_media).length, + has_media_count: posts.filter((p) => p.has_media).length, engagement_total: totalLikes, avg_engagement: Math.round(totalLikes / posts.length), - post_types: [...new Set(posts.map(p => p.post_type))], - authors: [...new Set(posts.map(p => p.author).filter(Boolean))].length, - estimated_tokens: Math.ceil(totalTextLength / 4) + post_types: [...new Set(posts.map((p) => p.post_type))], + authors: [...new Set(posts.map((p) => p.author).filter(Boolean))].length, + estimated_tokens: Math.ceil(totalTextLength / 4), }; } // Helper methods for extraction extractResultTitle(element) { - const titleSelectors = ['h1, h2, h3, .title, .headline, [data-testid*="title"]']; + const titleSelectors = [ + 'h1, h2, h3, .title, .headline, [data-testid*="title"]', + ]; for (const selector of titleSelectors) { const title = element.querySelector(selector)?.textContent?.trim(); if (title) return title.substring(0, 100); } - return element.textContent?.trim()?.substring(0, 50) || 'No title'; + return element.textContent?.trim()?.substring(0, 50) || "No title"; } extractResultSummary(element) { - const summarySelectors = ['.summary, .description, .snippet, .excerpt']; + const summarySelectors = [".summary, .description, .snippet, .excerpt"]; for (const selector of summarySelectors) { const summary = element.querySelector(selector)?.textContent?.trim(); if (summary) return summary.substring(0, 200); } - return element.textContent?.trim()?.substring(0, 150) || ''; + return element.textContent?.trim()?.substring(0, 150) || ""; } extractResultLink(element) { - const link = element.querySelector('a[href]')?.href || - element.closest('a[href]')?.href || - element.getAttribute('href'); + const link = + element.querySelector("a[href]")?.href || + element.closest("a[href]")?.href || + element.getAttribute("href"); return link || null; } detectResultType(element) { - if (element.textContent?.toLowerCase().includes('sponsored') || - element.querySelector('.ad, .sponsored')) return 'sponsored'; - if (element.querySelector('img, video')) return 'media'; - if (element.querySelector('.price, .cost')) return 'product'; - return 'organic'; + if ( + element.textContent?.toLowerCase().includes("sponsored") || + element.querySelector(".ad, .sponsored") + ) + return "sponsored"; + if (element.querySelector("img, video")) return "media"; + if (element.querySelector(".price, .cost")) return "product"; + return "organic"; } scoreSearchResult(element) { let score = 0.5; - if (element.querySelector('h1, h2, h3')) score += 0.2; - if (element.querySelector('img')) score += 0.1; + if (element.querySelector("h1, h2, h3")) score += 0.2; + if (element.querySelector("img")) score += 0.1; if (element.textContent?.length > 100) score += 0.1; - if (element.querySelector('a[href]')) score += 0.1; + if (element.querySelector("a[href]")) score += 0.1; return Math.min(score, 1.0); } extractPostText(element) { const textSelectors = [ '[data-testid="tweetText"], .tweet-text', - '.post-content, .entry-content', - '.status-content, .message-content' + ".post-content, .entry-content", + ".status-content, .message-content", ]; - + for (const selector of textSelectors) { const text = element.querySelector(selector)?.textContent?.trim(); if (text) return text.substring(0, 280); } - - return element.textContent?.trim()?.substring(0, 280) || ''; + + return element.textContent?.trim()?.substring(0, 280) || ""; } extractPostAuthor(element) { const authorSelectors = [ '[data-testid="User-Name"], .username', - '.author, .user-name, .handle' + ".author, .user-name, .handle", ]; - + for (const selector of authorSelectors) { const author = element.querySelector(selector)?.textContent?.trim(); if (author) return author.substring(0, 50); } - return 'Unknown'; + return "Unknown"; } extractPostTimestamp(element) { @@ -1086,7 +1709,9 @@ class BrowserAutomation { for (const selector of timeSelectors) { const time = element.querySelector(selector); if (time) { - return time.getAttribute('datetime') || time.textContent?.trim() || null; + return ( + time.getAttribute("datetime") || time.textContent?.trim() || null + ); } } return null; @@ -1095,24 +1720,34 @@ class BrowserAutomation { extractPostMetrics(element) { const metrics = {}; const likeSelectors = ['[data-testid*="like"], .like-count, .heart-count']; - const replySelectors = ['[data-testid*="reply"], .reply-count, .comment-count']; - const shareSelectors = ['[data-testid*="retweet"], .share-count, .repost-count']; - + const replySelectors = [ + '[data-testid*="reply"], .reply-count, .comment-count', + ]; + const shareSelectors = [ + '[data-testid*="retweet"], .share-count, .repost-count', + ]; + for (const selector of likeSelectors) { - const likes = element.querySelector(selector)?.textContent?.match(/\d+/)?.[0]; + const likes = element + .querySelector(selector) + ?.textContent?.match(/\d+/)?.[0]; if (likes) metrics.likes = parseInt(likes); } - + for (const selector of replySelectors) { - const replies = element.querySelector(selector)?.textContent?.match(/\d+/)?.[0]; + const replies = element + .querySelector(selector) + ?.textContent?.match(/\d+/)?.[0]; if (replies) metrics.replies = parseInt(replies); } - + for (const selector of shareSelectors) { - const shares = element.querySelector(selector)?.textContent?.match(/\d+/)?.[0]; + const shares = element + .querySelector(selector) + ?.textContent?.match(/\d+/)?.[0]; if (shares) metrics.shares = parseInt(shares); } - + return metrics; } @@ -1121,37 +1756,42 @@ class BrowserAutomation { } detectPostType(element) { - if (element.querySelector('[data-testid*="retweet"]')) return 'repost'; - if (element.querySelector('[data-testid*="reply"]')) return 'reply'; - if (element.hasAttribute('data-promoted')) return 'promoted'; - return 'original'; + if (element.querySelector('[data-testid*="retweet"]')) return "repost"; + if (element.querySelector('[data-testid*="reply"]')) return "reply"; + if (element.hasAttribute("data-promoted")) return "promoted"; + return "original"; } getTopDomains(domains, limit = 5) { const domainCounts = {}; - domains.forEach(domain => { + domains.forEach((domain) => { domainCounts[domain] = (domainCounts[domain] || 0) + 1; }); - + return Object.entries(domainCounts) - .sort(([,a], [,b]) => b - a) + .sort(([, a], [, b]) => b - a) .slice(0, limit) .map(([domain, count]) => ({ domain, count })); } calculateQualityScore(results) { - const avgScore = results.reduce((sum, r) => sum + (r.score || 0), 0) / results.length; - const hasLinks = results.filter(r => r.link).length / results.length; - const hasContent = results.filter(r => r.summary?.length > 50).length / results.length; - - return Math.round((avgScore * 0.4 + hasLinks * 0.3 + hasContent * 0.3) * 100); + const avgScore = + results.reduce((sum, r) => sum + (r.score || 0), 0) / results.length; + const hasLinks = results.filter((r) => r.link).length / results.length; + const hasContent = + results.filter((r) => r.summary?.length > 50).length / results.length; + + return Math.round( + (avgScore * 0.4 + hasLinks * 0.3 + hasContent * 0.3) * 100 + ); } getExtractionMethod(content_type) { const hostname = window.location.hostname; - if (hostname.includes('twitter') || hostname.includes('x.com')) return 'twitter_patterns'; - if (hostname.includes('github')) return 'github_patterns'; - if (hostname.includes('google')) return 'google_patterns'; + if (hostname.includes("twitter") || hostname.includes("x.com")) + return "twitter_patterns"; + if (hostname.includes("github")) return "github_patterns"; + if (hostname.includes("google")) return "google_patterns"; return `semantic_${content_type}`; } @@ -1165,158 +1805,134 @@ class BrowserAutomation { } } - async clickElement({ element_id, click_type = 'left', wait_after = 500 }) { + async clickElement({ element_id, click_type = "left", wait_after = 500 }) { const element = this.getElementById(element_id); if (!element) { throw new Error(`Element not found: ${element_id}`); } - + // Scroll element into view - element.scrollIntoView({ behavior: 'smooth', block: 'center' }); - await new Promise(resolve => setTimeout(resolve, 200)); - + element.scrollIntoView({ behavior: "smooth", block: "center" }); + await new Promise((resolve) => setTimeout(resolve, 200)); + // Click the element - if (click_type === 'right') { - element.dispatchEvent(new MouseEvent('contextmenu', { bubbles: true })); + if (click_type === "right") { + element.dispatchEvent(new MouseEvent("contextmenu", { bubbles: true })); } else { element.click(); } - - await new Promise(resolve => setTimeout(resolve, wait_after)); - + + await new Promise((resolve) => setTimeout(resolve, wait_after)); + return { success: true, element_id, click_type, - element_name: this.getElementName(element) - }; - } - - async fillElement({ element_id, value, clear_first = true, force_focus = true }) { - const element = this.getElementById(element_id); - if (!element) { - throw new Error(`Element not found: ${element_id}`); - } - - // Enhanced focus sequence for modern web apps - if (force_focus) { - await this.ensureProperFocus(element); - } else { - element.focus(); - } - - // Clear existing content if requested - if (clear_first) { - await this.clearElementContent(element); - } - - // Fill the value with proper event sequence - await this.fillWithEvents(element, value); - - // Validate the fill was successful - const actualValue = this.getElementValue(element); - const success = actualValue.includes(value); - - return { - success, - element_id, - value, - actual_value: actualValue, element_name: this.getElementName(element), - focus_applied: force_focus }; } async ensureProperFocus(element) { // Scroll element into view first - element.scrollIntoView({ behavior: 'smooth', block: 'center' }); - await new Promise(resolve => setTimeout(resolve, 200)); - + element.scrollIntoView({ behavior: "smooth", block: "center" }); + await new Promise((resolve) => setTimeout(resolve, 200)); + // Simulate proper mouse interaction sequence const rect = element.getBoundingClientRect(); const centerX = rect.left + rect.width / 2; const centerY = rect.top + rect.height / 2; - + // Fire mouse events in sequence - element.dispatchEvent(new MouseEvent('mousedown', { - bubbles: true, - clientX: centerX, - clientY: centerY - })); - - element.dispatchEvent(new MouseEvent('mouseup', { - bubbles: true, - clientX: centerX, - clientY: centerY - })); - - element.dispatchEvent(new MouseEvent('click', { - bubbles: true, - clientX: centerX, - clientY: centerY - })); - + element.dispatchEvent( + new MouseEvent("mousedown", { + bubbles: true, + clientX: centerX, + clientY: centerY, + }) + ); + + element.dispatchEvent( + new MouseEvent("mouseup", { + bubbles: true, + clientX: centerX, + clientY: centerY, + }) + ); + + element.dispatchEvent( + new MouseEvent("click", { + bubbles: true, + clientX: centerX, + clientY: centerY, + }) + ); + // Focus and fire focus events element.focus(); - element.dispatchEvent(new FocusEvent('focusin', { bubbles: true })); - element.dispatchEvent(new FocusEvent('focus', { bubbles: true })); - + element.dispatchEvent(new FocusEvent("focusin", { bubbles: true })); + element.dispatchEvent(new FocusEvent("focus", { bubbles: true })); + // Wait for React/framework to update - await new Promise(resolve => setTimeout(resolve, 100)); + await new Promise((resolve) => setTimeout(resolve, 100)); } async clearElementContent(element) { - if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') { - element.value = ''; - element.dispatchEvent(new Event('input', { bubbles: true })); - } else if (element.contentEditable === 'true') { + if (element.tagName === "INPUT" || element.tagName === "TEXTAREA") { + element.value = ""; + element.dispatchEvent(new Event("input", { bubbles: true })); + } else if (element.contentEditable === "true") { // For contenteditable, simulate selecting all and deleting element.focus(); - document.execCommand('selectAll'); - document.execCommand('delete'); - element.dispatchEvent(new Event('input', { bubbles: true })); + document.execCommand("selectAll"); + document.execCommand("delete"); + element.dispatchEvent(new Event("input", { bubbles: true })); } - await new Promise(resolve => setTimeout(resolve, 50)); + await new Promise((resolve) => setTimeout(resolve, 50)); } async fillWithEvents(element, value) { - if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') { + if (element.tagName === "INPUT" || element.tagName === "TEXTAREA") { // Set value and fire comprehensive events element.value = value; - element.dispatchEvent(new Event('beforeinput', { bubbles: true })); - element.dispatchEvent(new Event('input', { bubbles: true })); - element.dispatchEvent(new Event('change', { bubbles: true })); - + element.dispatchEvent(new Event("beforeinput", { bubbles: true })); + element.dispatchEvent(new Event("input", { bubbles: true })); + element.dispatchEvent(new Event("change", { bubbles: true })); + // Fire keyboard events to simulate typing completion - element.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true, key: 'End' })); - element.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true, key: 'End' })); - - } else if (element.contentEditable === 'true') { + element.dispatchEvent( + new KeyboardEvent("keydown", { bubbles: true, key: "End" }) + ); + element.dispatchEvent( + new KeyboardEvent("keyup", { bubbles: true, key: "End" }) + ); + } else if (element.contentEditable === "true") { // For contenteditable elements (like Twitter) element.textContent = value; - element.dispatchEvent(new Event('beforeinput', { bubbles: true })); - element.dispatchEvent(new Event('input', { bubbles: true })); - + element.dispatchEvent(new Event("beforeinput", { bubbles: true })); + element.dispatchEvent(new Event("input", { bubbles: true })); + // Trigger composition events for better compatibility - element.dispatchEvent(new CompositionEvent('compositionend', { - bubbles: true, - data: value - })); - + element.dispatchEvent( + new CompositionEvent("compositionend", { + bubbles: true, + data: value, + }) + ); + // Fire selection change to notify frameworks - document.dispatchEvent(new Event('selectionchange')); + document.dispatchEvent(new Event("selectionchange")); } - - await new Promise(resolve => setTimeout(resolve, 100)); + + await new Promise((resolve) => setTimeout(resolve, 100)); } getElementValue(element) { - if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') { + if (element.tagName === "INPUT" || element.tagName === "TEXTAREA") { return element.value; - } else if (element.contentEditable === 'true') { - return element.textContent || element.innerText || ''; + } else if (element.contentEditable === "true") { + return element.textContent || element.innerText || ""; } - return ''; + return ""; } // Element state detection methods @@ -1327,94 +1943,104 @@ class BrowserAutomation { clickable: this.isElementClickable(element), focusable: this.isElementFocusable(element), hasText: this.hasText(element), - isEmpty: this.isEmpty(element) + isEmpty: this.isEmpty(element), }; - + // Overall interaction readiness - state.interaction_ready = state.visible && !state.disabled && (state.clickable || state.focusable); - + state.interaction_ready = + state.visible && !state.disabled && (state.clickable || state.focusable); + return state; } isElementDisabled(element) { // Check disabled attribute if (element.disabled === true) return true; - if (element.getAttribute('disabled') !== null) return true; - + if (element.getAttribute("disabled") !== null) return true; + // Check aria-disabled - if (element.getAttribute('aria-disabled') === 'true') return true; - + if (element.getAttribute("aria-disabled") === "true") return true; + // Check common disabled classes - const disabledClasses = ['disabled', 'btn-disabled', 'button-disabled', 'inactive']; + const disabledClasses = [ + "disabled", + "btn-disabled", + "button-disabled", + "inactive", + ]; const classList = Array.from(element.classList); - if (disabledClasses.some(cls => classList.includes(cls))) return true; - + if (disabledClasses.some((cls) => classList.includes(cls))) return true; + // Check if parent form/fieldset is disabled - const parentFieldset = element.closest('fieldset[disabled]'); + const parentFieldset = element.closest("fieldset[disabled]"); if (parentFieldset) return true; - + // Check computed styles for pointer-events: none const computedStyle = getComputedStyle(element); - if (computedStyle.pointerEvents === 'none') return true; - + if (computedStyle.pointerEvents === "none") return true; + return false; } isElementClickable(element) { - const clickableTags = ['BUTTON', 'A', 'INPUT']; - const clickableTypes = ['button', 'submit', 'reset']; - const clickableRoles = ['button', 'link', 'menuitem', 'tab']; - + const clickableTags = ["BUTTON", "A", "INPUT"]; + const clickableTypes = ["button", "submit", "reset"]; + const clickableRoles = ["button", "link", "menuitem", "tab"]; + // Check tag and type if (clickableTags.includes(element.tagName)) return true; if (element.type && clickableTypes.includes(element.type)) return true; - + // Check role - const role = element.getAttribute('role'); + const role = element.getAttribute("role"); if (role && clickableRoles.includes(role)) return true; - + // Check for click handlers - if (element.onclick || element.getAttribute('onclick')) return true; - + if (element.onclick || element.getAttribute("onclick")) return true; + // Check for common clickable classes - const clickableClasses = ['btn', 'button', 'clickable', 'link']; + const clickableClasses = ["btn", "button", "clickable", "link"]; const classList = Array.from(element.classList); - if (clickableClasses.some(cls => classList.includes(cls))) return true; - + if (clickableClasses.some((cls) => classList.includes(cls))) return true; + return false; } isElementFocusable(element) { - const focusableTags = ['INPUT', 'TEXTAREA', 'SELECT', 'BUTTON', 'A']; - + const focusableTags = ["INPUT", "TEXTAREA", "SELECT", "BUTTON", "A"]; + // Check if element is naturally focusable if (focusableTags.includes(element.tagName)) return true; - + // Check tabindex - const tabindex = element.getAttribute('tabindex'); - if (tabindex && tabindex !== '-1') return true; - + const tabindex = element.getAttribute("tabindex"); + if (tabindex && tabindex !== "-1") return true; + // Check contenteditable - if (element.contentEditable === 'true') return true; - + if (element.contentEditable === "true") return true; + // Check role - const focusableRoles = ['textbox', 'searchbox', 'button', 'link']; - const role = element.getAttribute('role'); + const focusableRoles = ["textbox", "searchbox", "button", "link"]; + const role = element.getAttribute("role"); if (role && focusableRoles.includes(role)) return true; - + return false; } hasText(element) { - const text = element.textContent || element.value || element.getAttribute('aria-label') || ''; + const text = + element.textContent || + element.value || + element.getAttribute("aria-label") || + ""; return text.trim().length > 0; } isEmpty(element) { - if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') { + if (element.tagName === "INPUT" || element.tagName === "TEXTAREA") { return !element.value || element.value.trim().length === 0; } - if (element.contentEditable === 'true') { + if (element.contentEditable === "true") { return !element.textContent || element.textContent.trim().length === 0; } return false; @@ -1422,30 +2048,30 @@ class BrowserAutomation { async waitForCondition({ condition_type, selector, text, timeout = 5000 }) { const startTime = Date.now(); - + const conditions = { - 'element_visible': () => { + element_visible: () => { const el = document.querySelector(selector); return el && el.offsetParent !== null; }, - 'text_present': () => document.body.textContent.includes(text) + text_present: () => document.body.textContent.includes(text), }; - + const checkCondition = conditions[condition_type]; if (!checkCondition) { throw new Error(`Unknown condition type: ${condition_type}`); } - + while (Date.now() - startTime < timeout) { if (checkCondition()) { return { condition_met: true, - wait_time: Date.now() - startTime + wait_time: Date.now() - startTime, }; } - await new Promise(resolve => setTimeout(resolve, 100)); + await new Promise((resolve) => setTimeout(resolve, 100)); } - + throw new Error(`Timeout waiting for condition: ${condition_type}`); } @@ -1458,7 +2084,7 @@ class BrowserAutomation { getElementById(id) { // Check quick registry first (for q1, q2, etc.) - if (id.startsWith('q')) { + if (id.startsWith("q")) { return this.quickRegistry.get(id); } // Then check main registry (for element_1, element_2, etc.) @@ -1466,54 +2092,59 @@ class BrowserAutomation { } getElementName(element) { - return element.getAttribute('aria-label') || - element.getAttribute('title') || - element.textContent?.trim()?.substring(0, 50) || - element.placeholder || - element.tagName.toLowerCase(); + return ( + element.getAttribute("aria-label") || + element.getAttribute("title") || + element.textContent?.trim()?.substring(0, 50) || + element.placeholder || + element.tagName.toLowerCase() + ); } isVisible(element) { - return element.offsetParent !== null && - getComputedStyle(element).visibility !== 'hidden' && - getComputedStyle(element).opacity !== '0'; + return ( + element.offsetParent !== null && + getComputedStyle(element).visibility !== "hidden" && + getComputedStyle(element).opacity !== "0" + ); } generateSelector(element) { if (element.id) return `#${element.id}`; - if (element.getAttribute('data-testid')) return `[data-testid="${element.getAttribute('data-testid')}"]`; - + if (element.getAttribute("data-testid")) + return `[data-testid="${element.getAttribute("data-testid")}"]`; + let selector = element.tagName.toLowerCase(); if (element.className) { - selector += `.${element.className.split(' ').join('.')}`; + selector += `.${element.className.split(" ").join(".")}`; } return selector; } inferElementType(element, intentHint) { const tagName = element.tagName.toLowerCase(); - const role = element.getAttribute('role'); - const type = element.getAttribute('type'); - - if (tagName === 'input' && type === 'search') return 'search_input'; - if (tagName === 'input') return 'input'; - if (tagName === 'textarea') return 'textarea'; - if (tagName === 'button' || role === 'button') return 'button'; - if (tagName === 'a') return 'link'; - - return 'element'; + const role = element.getAttribute("role"); + const type = element.getAttribute("type"); + + if (tagName === "input" && type === "search") return "search_input"; + if (tagName === "input") return "input"; + if (tagName === "textarea") return "textarea"; + if (tagName === "button" || role === "button") return "button"; + if (tagName === "a") return "link"; + + return "element"; } calculateConfidence(element, intentHint) { let confidence = 0.5; - + const text = this.getElementName(element).toLowerCase(); const hint = intentHint.toLowerCase(); - + if (text.includes(hint)) confidence += 0.3; - if (element.getAttribute('data-testid')) confidence += 0.2; - if (element.getAttribute('aria-label')) confidence += 0.1; - + if (element.getAttribute("data-testid")) confidence += 0.2; + if (element.getAttribute("aria-label")) confidence += 0.1; + return Math.min(confidence, 1.0); } @@ -1522,7 +2153,7 @@ class BrowserAutomation { ...result, method, execution_time: Math.round(performance.now() - startTime), - analyzed_at: new Date().toISOString() + analyzed_at: new Date().toISOString(), }; } @@ -1530,33 +2161,33 @@ class BrowserAutomation { compressElement(element, isQuick = false) { const actualElement = element.element || element; const state = this.getElementState(actualElement); - + if (isQuick) { // Quick phase - minimal data with state const quickId = `q${++this.quickIdCounter}`; this.quickRegistry.set(quickId, actualElement); - + return { id: quickId, - type: element.type || 'element', - name: element.name?.substring(0, 20) || 'unnamed', + type: element.type || "element", + name: element.name?.substring(0, 20) || "unnamed", conf: Math.round((element.confidence || 0.5) * 100), - selector: element.selector || 'unknown', - state: state.disabled ? 'disabled' : 'enabled', + selector: element.selector || "unknown", + state: state.disabled ? "disabled" : "enabled", clickable: state.clickable, - ready: state.interaction_ready + ready: state.interaction_ready, }; } else { // Detailed phase - compact fingerprint with full state return { id: element.id, fp: this.generateFingerprint(actualElement), - name: element.name?.substring(0, 30) || 'unnamed', + name: element.name?.substring(0, 30) || "unnamed", conf: Math.round((element.confidence || 0.5) * 100), meta: { ...this.getElementMeta(actualElement), - state: state - } + state: state, + }, }; } } @@ -1566,114 +2197,158 @@ class BrowserAutomation { const primaryClass = this.getPrimaryClass(element); const context = this.getContext(element); const position = this.getRelativePosition(element); - - return `${tag}${primaryClass ? '.' + primaryClass : ''}@${context}.${position}`; + + return `${tag}${ + primaryClass ? "." + primaryClass : "" + }@${context}.${position}`; } getPrimaryClass(element) { - const importantClasses = ['btn', 'button', 'link', 'input', 'search', 'submit', 'primary', 'secondary']; + const importantClasses = [ + "btn", + "button", + "link", + "input", + "search", + "submit", + "primary", + "secondary", + ]; const classList = Array.from(element.classList); - return classList.find(cls => importantClasses.includes(cls)) || classList[0]; + return ( + classList.find((cls) => importantClasses.includes(cls)) || classList[0] + ); } getContext(element) { - const parent = element.closest('nav, main, header, footer, form, section, article') || element.parentElement; - if (!parent) return 'body'; + const parent = + element.closest("nav, main, header, footer, form, section, article") || + element.parentElement; + if (!parent) return "body"; return parent.tagName.toLowerCase(); } getRelativePosition(element) { const siblings = Array.from(element.parentElement?.children || []); - const sameTypeElements = siblings.filter(el => el.tagName === element.tagName); + const sameTypeElements = siblings.filter( + (el) => el.tagName === element.tagName + ); return sameTypeElements.indexOf(element) + 1; } getElementMeta(element) { const rect = element.getBoundingClientRect(); return { - rect: [Math.round(rect.x), Math.round(rect.y), Math.round(rect.width), Math.round(rect.height)], + rect: [ + Math.round(rect.x), + Math.round(rect.y), + Math.round(rect.width), + Math.round(rect.height), + ], visible: this.isLikelyVisible(element), - form_context: element.closest('form') ? 'form' : null + form_context: element.closest("form") ? "form" : null, }; } detectPageType() { const hostname = window.location.hostname; const title = document.title.toLowerCase(); - const hasSearch = document.querySelector('[type="search"], [role="searchbox"]'); - const hasLogin = document.querySelector('[type="password"], [name*="login" i]'); - const hasPost = document.querySelector('[contenteditable="true"], textarea[placeholder*="post" i]'); - - if (hostname.includes('twitter') || hostname.includes('x.com')) return 'social_media'; - if (hostname.includes('github')) return 'code_repository'; - if (hostname.includes('google')) return 'search_engine'; - if (hasPost) return 'content_creation'; - if (hasLogin) return 'authentication'; - if (hasSearch) return 'search_interface'; - if (title.includes('shop') || title.includes('store')) return 'ecommerce'; - - return 'general_website'; + const hasSearch = document.querySelector( + '[type="search"], [role="searchbox"]' + ); + const hasLogin = document.querySelector( + '[type="password"], [name*="login" i]' + ); + const hasPost = document.querySelector( + '[contenteditable="true"], textarea[placeholder*="post" i]' + ); + + if (hostname.includes("twitter") || hostname.includes("x.com")) + return "social_media"; + if (hostname.includes("github")) return "code_repository"; + if (hostname.includes("google")) return "search_engine"; + if (hasPost) return "content_creation"; + if (hasLogin) return "authentication"; + if (hasSearch) return "search_interface"; + if (title.includes("shop") || title.includes("store")) return "ecommerce"; + + return "general_website"; } countViewportElements() { - const elements = document.querySelectorAll('button, input, select, textarea, a[href]'); - const viewportElements = Array.from(elements).filter(el => this.isLikelyVisible(el)); - + const elements = document.querySelectorAll( + "button, input, select, textarea, a[href]" + ); + const viewportElements = Array.from(elements).filter((el) => + this.isLikelyVisible(el) + ); + return { - buttons: viewportElements.filter(el => el.tagName === 'BUTTON' || el.getAttribute('role') === 'button').length, - inputs: viewportElements.filter(el => el.tagName === 'INPUT').length, - links: viewportElements.filter(el => el.tagName === 'A').length, - textareas: viewportElements.filter(el => el.tagName === 'TEXTAREA').length, - selects: viewportElements.filter(el => el.tagName === 'SELECT').length + buttons: viewportElements.filter( + (el) => el.tagName === "BUTTON" || el.getAttribute("role") === "button" + ).length, + inputs: viewportElements.filter((el) => el.tagName === "INPUT").length, + links: viewportElements.filter((el) => el.tagName === "A").length, + textareas: viewportElements.filter((el) => el.tagName === "TEXTAREA") + .length, + selects: viewportElements.filter((el) => el.tagName === "SELECT").length, }; } async quickViewportScan(intent_hint, maxResults = 3) { - const candidates = document.querySelectorAll('button, input, a[href], [role="button"], textarea'); + const candidates = document.querySelectorAll( + 'button, input, a[href], [role="button"], textarea' + ); const visibleElements = Array.from(candidates) - .filter(el => this.isLikelyVisible(el)) + .filter((el) => this.isLikelyVisible(el)) .slice(0, 10); // Limit scan to first 10 visible elements - - const scoredElements = visibleElements.map(element => { + + const scoredElements = visibleElements.map((element) => { const confidence = this.calculateConfidence(element, intent_hint); return { element, type: this.inferElementType(element, intent_hint), name: this.getElementName(element), - confidence + confidence, }; }); - + return scoredElements - .filter(el => el.confidence > 0.3) + .filter((el) => el.confidence > 0.3) .sort((a, b) => b.confidence - a.confidence) .slice(0, maxResults) - .map(el => this.compressElement(el, true)); + .map((el) => this.compressElement(el, true)); } scoreIntentMatch(intent_hint, quickMatches) { - if (quickMatches.length === 0) return 'none'; - const avgConfidence = quickMatches.reduce((sum, match) => sum + match.conf, 0) / quickMatches.length; - - if (avgConfidence >= 80) return 'high'; - if (avgConfidence >= 60) return 'medium'; - if (avgConfidence >= 40) return 'low'; - return 'none'; + if (quickMatches.length === 0) return "none"; + const avgConfidence = + quickMatches.reduce((sum, match) => sum + match.conf, 0) / + quickMatches.length; + + if (avgConfidence >= 80) return "high"; + if (avgConfidence >= 60) return "medium"; + if (avgConfidence >= 40) return "low"; + return "none"; } suggestPhase2Areas(quickMatches, intent_hint) { const suggestions = []; - const elementTypes = [...new Set(quickMatches.map(m => m.type))]; - - if (elementTypes.includes('button')) suggestions.push('buttons'); - if (elementTypes.includes('input') || elementTypes.includes('textarea')) suggestions.push('forms'); - if (elementTypes.includes('link')) suggestions.push('navigation'); - + const elementTypes = [...new Set(quickMatches.map((m) => m.type))]; + + if (elementTypes.includes("button")) suggestions.push("buttons"); + if (elementTypes.includes("input") || elementTypes.includes("textarea")) + suggestions.push("forms"); + if (elementTypes.includes("link")) suggestions.push("navigation"); + // Intent-based suggestions - if (intent_hint.toLowerCase().includes('search') && !suggestions.includes('forms')) { - suggestions.push('search_elements'); + if ( + intent_hint.toLowerCase().includes("search") && + !suggestions.includes("forms") + ) { + suggestions.push("search_elements"); } - + return suggestions.slice(0, 3); } @@ -1682,8 +2357,8 @@ class BrowserAutomation { const baseTokens = 50; // Base overhead const tokensPerElement = 15; // Detailed element info const contextTokens = 20; // Page context - - return baseTokens + (quickMatches.length * tokensPerElement) + contextTokens; + + return baseTokens + quickMatches.length * tokensPerElement + contextTokens; } async expandQuickMatches(element_ids) { @@ -1694,10 +2369,10 @@ class BrowserAutomation { const elementId = this.registerElement(element); elements.push({ id: elementId, - type: this.inferElementType(element, ''), + type: this.inferElementType(element, ""), name: this.getElementName(element), confidence: 0.8, // Default confidence for expanded elements - element: element + element: element, }); } } @@ -1707,12 +2382,13 @@ class BrowserAutomation { async analyzeFocusAreas(focus_areas, intent_hint) { const elements = []; const areaSelectors = { - 'buttons': 'button, [role="button"], input[type="submit"]', - 'forms': 'input, textarea, select, [contenteditable="true"]', - 'navigation': 'nav a, .nav-item, [role="navigation"] a', - 'search_elements': '[type="search"], [role="searchbox"], [placeholder*="search" i]' + buttons: 'button, [role="button"], input[type="submit"]', + forms: 'input, textarea, select, [contenteditable="true"]', + navigation: 'nav a, .nav-item, [role="navigation"] a', + search_elements: + '[type="search"], [role="searchbox"], [placeholder*="search" i]', }; - + for (const area of focus_areas) { const selector = areaSelectors[area]; if (selector) { @@ -1725,13 +2401,13 @@ class BrowserAutomation { type: this.inferElementType(element, intent_hint), name: this.getElementName(element), confidence: this.calculateConfidence(element, intent_hint), - element: element + element: element, }); } } } } - + return elements; } @@ -1742,11 +2418,11 @@ class BrowserAutomation { [role="button"], [role="textbox"], [role="searchbox"], [aria-label], [data-testid], [contenteditable="true"] `); - + const elements = Array.from(relevantElements) - .filter(el => this.isLikelyVisible(el)) + .filter((el) => this.isLikelyVisible(el)) .slice(0, 30) // Analyze more elements than before - .map(element => { + .map((element) => { const elementId = this.registerElement(element); return { id: elementId, @@ -1754,18 +2430,18 @@ class BrowserAutomation { selector: this.generateSelector(element), name: this.getElementName(element), confidence: this.calculateConfidence(element, intent_hint), - element: element + element: element, }; }) - .filter(el => el.confidence > 0.2) // Lower threshold for detailed analysis + .filter((el) => el.confidence > 0.2) // Lower threshold for detailed analysis .sort((a, b) => b.confidence - a.confidence); - + return elements.slice(0, max_results); } deduplicateElements(elements) { const seen = new Set(); - return elements.filter(element => { + return elements.filter((element) => { const key = element.name + element.type; if (seen.has(key)) return false; seen.add(key); @@ -1774,23 +2450,25 @@ class BrowserAutomation { } async enhanceElementMetadata(elements) { - return elements.map(element => ({ + return elements.map((element) => ({ ...element, - meta: this.getElementMeta(element.element) + meta: this.getElementMeta(element.element), })); } isLikelyVisible(element) { const rect = element.getBoundingClientRect(); const style = getComputedStyle(element); - - return rect.top < window.innerHeight && - rect.bottom > 0 && - rect.left < window.innerWidth && - rect.right > 0 && - style.visibility !== 'hidden' && - style.opacity !== '0' && - style.display !== 'none'; + + return ( + rect.top < window.innerHeight && + rect.bottom > 0 && + rect.left < window.innerWidth && + rect.right > 0 && + style.visibility !== "hidden" && + style.opacity !== "0" && + style.display !== "none" + ); } estimateTokenUsage(result) { @@ -1801,4 +2479,4 @@ class BrowserAutomation { } // Initialize the automation system -const browserAutomation = new BrowserAutomation(); \ No newline at end of file +const browserAutomation = new BrowserAutomation(); diff --git a/opendia-mcp/server.js b/opendia-mcp/server.js index df14fc3..6f0598a 100644 --- a/opendia-mcp/server.js +++ b/opendia-mcp/server.js @@ -32,7 +32,9 @@ async function handleMCPRequest(request) { switch (method) { case "initialize": // RESPOND IMMEDIATELY - don't wait for extension - console.error(`MCP client initializing: ${params?.clientInfo?.name || "unknown"}`); + console.error( + `MCP client initializing: ${params?.clientInfo?.name || "unknown"}` + ); result = { protocolVersion: "2024-11-05", capabilities: { @@ -40,19 +42,31 @@ async function handleMCPRequest(request) { }, serverInfo: { name: "browser-mcp-server", - version: "1.0.0", + version: "2.0.0", }, - instructions: "Browser automation tools via Chrome Extension bridge. Extension may take a moment to connect." + instructions: + "šŸŽÆ Enhanced browser automation with anti-detection bypass for Twitter/X, LinkedIn, Facebook. Extension may take a moment to connect.", }; break; case "tools/list": // Debug logging - console.error(`Tools/list called. Extension connected: ${chromeExtensionSocket && chromeExtensionSocket.readyState === WebSocket.OPEN}, Available tools: ${availableTools.length}`); - + console.error( + `Tools/list called. Extension connected: ${ + chromeExtensionSocket && + chromeExtensionSocket.readyState === WebSocket.OPEN + }, Available tools: ${availableTools.length}` + ); + // Return tools from extension if available, otherwise fallback tools - if (chromeExtensionSocket && chromeExtensionSocket.readyState === WebSocket.OPEN && availableTools.length > 0) { - console.error(`Returning ${availableTools.length} tools from extension`); + if ( + chromeExtensionSocket && + chromeExtensionSocket.readyState === WebSocket.OPEN && + availableTools.length > 0 + ) { + console.error( + `Returning ${availableTools.length} tools from extension` + ); result = { tools: availableTools.map((tool) => ({ name: tool.name, @@ -64,22 +78,25 @@ async function handleMCPRequest(request) { // Return basic fallback tools console.error("Extension not connected, returning fallback tools"); result = { - tools: getFallbackTools() + tools: getFallbackTools(), }; } break; case "tools/call": - if (!chromeExtensionSocket || chromeExtensionSocket.readyState !== WebSocket.OPEN) { + if ( + !chromeExtensionSocket || + chromeExtensionSocket.readyState !== WebSocket.OPEN + ) { // Extension not connected - return helpful error result = { content: [ { type: "text", - text: "āŒ Chrome Extension not connected. Please install and activate the browser extension, then try again.\n\nSetup instructions:\n1. Go to chrome://extensions/\n2. Enable Developer mode\n3. Click 'Load unpacked' and select the extension folder\n4. Ensure the extension is active", + text: "āŒ Chrome Extension not connected. Please install and activate the browser extension, then try again.\n\nSetup instructions:\n1. Go to chrome://extensions/\n2. Enable Developer mode\n3. Click 'Load unpacked' and select the extension folder\n4. Ensure the extension is active\n\nšŸŽÆ Features: Anti-detection bypass for Twitter/X, LinkedIn, Facebook + universal automation", }, ], - isError: true + isError: true, }; } else { // Extension connected - try the tool call @@ -88,10 +105,10 @@ async function handleMCPRequest(request) { params.name, params.arguments || {} ); - + // Format response based on tool type const formattedResult = formatToolResult(params.name, toolResult); - + result = { content: [ { @@ -99,7 +116,7 @@ async function handleMCPRequest(request) { text: formattedResult, }, ], - isError: false + isError: false, }; } catch (error) { result = { @@ -109,7 +126,7 @@ async function handleMCPRequest(request) { text: `āŒ Tool execution failed: ${error.message}`, }, ], - isError: true + isError: true, }; } } @@ -142,153 +159,373 @@ async function handleMCPRequest(request) { } } -// Remove static tools - they were causing duplicates -// All tools now come from the extension only - -// Format tool results for better MCP response +// Enhanced tool result formatting with anti-detection support function formatToolResult(toolName, result) { const metadata = { tool: toolName, execution_time: result.execution_time || 0, - timestamp: new Date().toISOString() + timestamp: new Date().toISOString(), }; - + switch (toolName) { - case 'page_analyze': - if (result.elements && result.elements.length > 0) { - const summary = `Found ${result.elements.length} relevant elements using ${result.method}:\n\n` + - result.elements.map(el => - `• ${el.name} (${el.type}) - Confidence: ${Math.round(el.confidence * 100)}%\n Selector: ${el.selector}\n Element ID: ${el.id}` - ).join('\n\n'); - return `${summary}\n\n${JSON.stringify(metadata, null, 2)}`; - } else { - return `No relevant elements found for intent: "${result.intent_hint || 'unknown'}"\n\n${JSON.stringify(metadata, null, 2)}`; - } - - case 'page_extract_content': - const contentSummary = `Extracted ${result.content_type} content using ${result.method}:\n\n`; - if (result.content) { - const preview = typeof result.content === 'string' - ? result.content.substring(0, 500) + (result.content.length > 500 ? '...' : '') - : JSON.stringify(result.content, null, 2).substring(0, 500); - return `${contentSummary}${preview}\n\n${JSON.stringify(metadata, null, 2)}`; - } else { - return `${contentSummary}No content found\n\n${JSON.stringify(metadata, null, 2)}`; - } - - case 'element_click': - return `āœ… Successfully clicked element: ${result.element_name || result.element_id}\n` + - `Click type: ${result.click_type || 'left'}\n\n${JSON.stringify(metadata, null, 2)}`; - - case 'element_fill': - return `āœ… Successfully filled element: ${result.element_name || result.element_id}\n` + - `Value: "${result.value}"\n\n${JSON.stringify(metadata, null, 2)}`; - - case 'page_navigate': - return `āœ… Successfully navigated to: ${result.url || 'unknown URL'}\n\n${JSON.stringify(metadata, null, 2)}`; - - case 'page_wait_for': - return `āœ… Condition met: ${result.condition_type || 'unknown'}\n` + - `Wait time: ${result.wait_time || 0}ms\n\n${JSON.stringify(metadata, null, 2)}`; - + case "page_analyze": + return formatPageAnalyzeResult(result, metadata); + + case "page_extract_content": + return formatContentExtractionResult(result, metadata); + + case "element_click": + return formatElementClickResult(result, metadata); + + case "element_fill": + return formatElementFillResult(result, metadata); + + case "page_navigate": + return `āœ… Successfully navigated to: ${ + result.url || "unknown URL" + }\n\n${JSON.stringify(metadata, null, 2)}`; + + case "page_wait_for": + return ( + `āœ… Condition met: ${result.condition_type || "unknown"}\n` + + `Wait time: ${result.wait_time || 0}ms\n\n${JSON.stringify( + metadata, + null, + 2 + )}` + ); + default: // Legacy tools or unknown tools return JSON.stringify(result, null, 2); } } -// Fallback tools when extension is not connected +function formatPageAnalyzeResult(result, metadata) { + if (result.elements && result.elements.length > 0) { + const platformInfo = result.summary?.anti_detection_platform + ? `\nšŸŽÆ Anti-detection platform detected: ${result.summary.anti_detection_platform}` + : ""; + + const summary = + `Found ${result.elements.length} relevant elements using ${result.method}:${platformInfo}\n\n` + + result.elements + .map((el) => { + const readyStatus = el.ready ? "āœ… Ready" : "āš ļø Not ready"; + const stateInfo = el.state === "disabled" ? " (disabled)" : ""; + return `• ${el.name} (${el.type}) - Confidence: ${el.conf}% ${readyStatus}${stateInfo}\n Element ID: ${el.id}`; + }) + .join("\n\n"); + return `${summary}\n\n${JSON.stringify(metadata, null, 2)}`; + } else { + const intentHint = result.intent_hint || "unknown"; + const platformInfo = result.summary?.anti_detection_platform + ? `\nPlatform: ${result.summary.anti_detection_platform}` + : ""; + return `No relevant elements found for intent: "${intentHint}"${platformInfo}\n\n${JSON.stringify( + metadata, + null, + 2 + )}`; + } +} + +function formatContentExtractionResult(result, metadata) { + const contentSummary = `Extracted ${result.content_type} content using ${result.method}:\n\n`; + if (result.content) { + const preview = + typeof result.content === "string" + ? result.content.substring(0, 500) + + (result.content.length > 500 ? "..." : "") + : JSON.stringify(result.content, null, 2).substring(0, 500); + return `${contentSummary}${preview}\n\n${JSON.stringify( + metadata, + null, + 2 + )}`; + } else if (result.summary) { + // Enhanced summarized content response + const summaryText = formatContentSummary( + result.summary, + result.content_type + ); + return `${contentSummary}${summaryText}\n\n${JSON.stringify( + metadata, + null, + 2 + )}`; + } else { + return `${contentSummary}No content found\n\n${JSON.stringify( + metadata, + null, + 2 + )}`; + } +} + +function formatContentSummary(summary, contentType) { + switch (contentType) { + case "article": + return ( + `šŸ“° Article: "${summary.title}"\n` + + `šŸ“ Word count: ${summary.word_count}\n` + + `ā±ļø Reading time: ${summary.reading_time} minutes\n` + + `šŸ–¼ļø Has media: ${summary.has_images || summary.has_videos}\n` + + `Preview: ${summary.preview}` + ); + + case "search_results": + return ( + `šŸ” Search Results Summary:\n` + + `šŸ“Š Total results: ${summary.total_results}\n` + + `šŸ† Quality score: ${summary.quality_score}/100\n` + + `šŸ“ˆ Average relevance: ${Math.round(summary.avg_score * 100)}%\n` + + `🌐 Top domains: ${summary.top_domains + ?.map((d) => d.domain) + .join(", ")}\n` + + `šŸ“ Result types: ${summary.result_types?.join(", ")}` + ); + + case "posts": + return ( + `šŸ“± Social Posts Summary:\n` + + `šŸ“Š Post count: ${summary.post_count}\n` + + `šŸ“ Average length: ${summary.avg_length} characters\n` + + `ā¤ļø Total engagement: ${summary.engagement_total}\n` + + `šŸ–¼ļø Posts with media: ${summary.has_media_count}\n` + + `šŸ‘„ Unique authors: ${summary.authors}\n` + + `šŸ“‹ Post types: ${summary.post_types?.join(", ")}` + ); + + default: + return JSON.stringify(summary, null, 2); + } +} + +function formatElementClickResult(result, metadata) { + return ( + `āœ… Successfully clicked element: ${ + result.element_name || result.element_id + }\n` + + `Click type: ${result.click_type || "left"}\n\n${JSON.stringify( + metadata, + null, + 2 + )}` + ); +} + +function formatElementFillResult(result, metadata) { + // Enhanced formatting for anti-detection bypass methods + const methodEmojis = { + twitter_direct_bypass: "🐦 Twitter Direct Bypass", + linkedin_direct_bypass: "šŸ’¼ LinkedIn Direct Bypass", + facebook_direct_bypass: "šŸ“˜ Facebook Direct Bypass", + generic_direct_bypass: "šŸŽÆ Generic Direct Bypass", + standard_fill: "šŸ”§ Standard Fill", + anti_detection_bypass: "šŸ›”ļø Anti-Detection Bypass", + }; + + const methodDisplay = methodEmojis[result.method] || result.method; + const successIcon = result.success ? "āœ…" : "āŒ"; + + let fillResult = `${successIcon} Element fill ${ + result.success ? "completed" : "failed" + } using ${methodDisplay}\n`; + fillResult += `šŸ“ Target: ${result.element_name || result.element_id}\n`; + fillResult += `šŸ’¬ Input: "${result.value}"\n`; + + if (result.actual_value) { + fillResult += `šŸ“„ Result: "${result.actual_value}"\n`; + } + + // Add bypass-specific information + if ( + result.method?.includes("bypass") && + result.execCommand_result !== undefined + ) { + fillResult += `šŸ”§ execCommand success: ${result.execCommand_result}\n`; + } + + if (!result.success && result.method?.includes("bypass")) { + fillResult += `\nāš ļø Direct bypass failed - page may have enhanced detection. Try refreshing the page.\n`; + } + + return `${fillResult}\n${JSON.stringify(metadata, null, 2)}`; +} + +// Enhanced fallback tools when extension is not connected function getFallbackTools() { return [ { name: "page_analyze", - description: "Analyze current page structure (Extension required)", + description: + "šŸŽÆ Analyze page structure with anti-detection bypass (Extension required)", inputSchema: { type: "object", properties: { - intent_hint: { type: "string", description: "What user wants to do" } + intent_hint: { + type: "string", + description: + "What user wants to do: post_tweet, search, login, etc.", + }, + phase: { + type: "string", + enum: ["discover", "detailed"], + default: "discover", + description: + "Analysis phase: 'discover' for quick scan, 'detailed' for full analysis", + }, }, - required: ["intent_hint"] - } + required: ["intent_hint"], + }, }, { name: "page_extract_content", - description: "Extract structured content (Extension required)", + description: + "šŸ“„ Extract structured content with smart summarization (Extension required)", inputSchema: { type: "object", properties: { - content_type: { type: "string", enum: ["article", "search_results", "posts"] } + content_type: { + type: "string", + enum: ["article", "search_results", "posts"], + description: "Type of content to extract", + }, + summarize: { + type: "boolean", + default: true, + description: + "Return summary instead of full content (saves tokens)", + }, }, - required: ["content_type"] - } + required: ["content_type"], + }, }, { name: "element_click", - description: "Click page elements (Extension required)", + description: + "šŸ–±ļø Click page elements with smart targeting (Extension required)", inputSchema: { type: "object", properties: { - element_id: { type: "string", description: "Element ID from page_analyze" } + element_id: { + type: "string", + description: "Element ID from page_analyze", + }, + click_type: { + type: "string", + enum: ["left", "right", "double"], + default: "left", + }, }, - required: ["element_id"] - } + required: ["element_id"], + }, }, { name: "element_fill", - description: "Fill input fields (Extension required)", + description: + "āœļø Fill input fields with anti-detection bypass for Twitter/X, LinkedIn, Facebook (Extension required)", inputSchema: { type: "object", properties: { - element_id: { type: "string", description: "Element ID" }, - value: { type: "string", description: "Text to input" } + element_id: { + type: "string", + description: "Element ID from page_analyze", + }, + value: { + type: "string", + description: "Text to input", + }, + clear_first: { + type: "boolean", + default: true, + description: "Clear existing content before filling", + }, }, - required: ["element_id", "value"] - } + required: ["element_id", "value"], + }, }, { name: "page_navigate", - description: "Navigate to URLs (Extension required)", + description: + "🧭 Navigate to URLs with wait conditions (Extension required)", inputSchema: { type: "object", properties: { - url: { type: "string", description: "URL to navigate to" } + url: { type: "string", description: "URL to navigate to" }, + wait_for: { + type: "string", + description: "CSS selector to wait for after navigation", + }, }, - required: ["url"] - } + required: ["url"], + }, }, { name: "page_wait_for", - description: "Wait for elements (Extension required)", + description: "ā³ Wait for elements or conditions (Extension required)", inputSchema: { type: "object", properties: { - condition_type: { type: "string", enum: ["element_visible", "text_present"] } + condition_type: { + type: "string", + enum: ["element_visible", "text_present"], + description: "Type of condition to wait for", + }, + selector: { + type: "string", + description: "CSS selector (for element_visible condition)", + }, + text: { + type: "string", + description: "Text to wait for (for text_present condition)", + }, }, - required: ["condition_type"] - } + required: ["condition_type"], + }, + }, + { + name: "get_analytics", + description: "šŸ“Š Get performance analytics and token usage metrics", + inputSchema: { + type: "object", + properties: {}, + additionalProperties: false, + }, + }, + { + name: "clear_analytics", + description: "šŸ—‘ļø Clear analytics data and reset performance tracking", + inputSchema: { + type: "object", + properties: {}, + additionalProperties: false, + }, }, { name: "browser_navigate", - description: "Navigate to URLs - legacy (Extension required)", + description: "🌐 Navigate to URLs - legacy (Extension required)", inputSchema: { type: "object", properties: { - url: { type: "string", description: "URL to navigate to" } + url: { type: "string", description: "URL to navigate to" }, }, - required: ["url"] - } + required: ["url"], + }, }, { name: "browser_execute_script", - description: "Execute JavaScript (Extension required - limited by CSP)", + description: + "⚔ Execute JavaScript (Extension required - limited by CSP)", inputSchema: { type: "object", properties: { - code: { type: "string", description: "JavaScript code" } + code: { type: "string", description: "JavaScript code" }, }, - required: ["code"] - } - } + required: ["code"], + }, + }, ]; } @@ -357,8 +594,14 @@ wss.on("connection", (ws) => { if (message.type === "register") { availableTools = message.tools; - console.error(`āœ… Registered ${availableTools.length} browser tools from extension`); - console.error(`Tools: ${availableTools.map(t => t.name).join(', ')}`); + console.error( + `āœ… Registered ${availableTools.length} browser tools from extension` + ); + console.error( + `šŸŽÆ Enhanced tools with anti-detection bypass: ${availableTools + .map((t) => t.name) + .join(", ")}` + ); } else if (message.type === "ping") { // Respond to ping with pong ws.send(JSON.stringify({ type: "pong", timestamp: Date.now() })); @@ -420,14 +663,25 @@ app.get("/health", (req, res) => { status: "ok", chromeExtensionConnected: chromeExtensionSocket !== null, availableTools: availableTools.length, + features: [ + "Anti-detection bypass for Twitter/X, LinkedIn, Facebook", + "Two-phase intelligent page analysis", + "Smart content extraction with summarization", + "Element state detection and interaction readiness", + "Performance analytics and token optimization", + ], }); }); app.listen(3001, () => { + console.error("šŸŽÆ Enhanced Browser MCP Server with Anti-Detection Features"); console.error( "Health check endpoint available at http://localhost:3001/health" ); }); -console.error("Browser MCP Server started"); -console.error("Waiting for Chrome Extension connection on ws://localhost:3000"); +console.error("šŸš€ Enhanced Browser MCP Server started"); +console.error( + "šŸ”Œ Waiting for Chrome Extension connection on ws://localhost:3000" +); +console.error("šŸŽÆ Features: Anti-detection bypass + intelligent automation");