diff --git a/.gitignore b/.gitignore index 63336b7..2803bc1 100644 --- a/.gitignore +++ b/.gitignore @@ -95,6 +95,9 @@ out .nuxt dist +# Packaged extension builds (published via GitHub Releases) +opendia-extension/releases/ + # Gatsby files .cache/ # Comment in the public line in if your project uses Gatsby and not Next.js diff --git a/build-dxt.sh b/build-dxt.sh index 61b5559..467bfaa 100755 --- a/build-dxt.sh +++ b/build-dxt.sh @@ -74,9 +74,17 @@ cd dist/opendia-dxt npm install --production --silent cd ../.. -# Copy browser extension +# Build and copy browser extension. The install instructions below point at +# extension/dist/{chrome,firefox}, so those have to exist in the bundle. +echo "๐ŸŒ Building browser extension..." +cd opendia-extension +npm install --silent +npm run build --silent +cd .. + echo "๐ŸŒ Copying browser extension..." cp -r opendia-extension dist/opendia-dxt/extension +rm -rf dist/opendia-dxt/extension/node_modules # Copy logo/icon files for DXT - try multiple sources echo "๐ŸŽจ Copying logo files..." @@ -290,7 +298,7 @@ Download the latest extension from: https://github.com/aeonfun/opendia/releases 2. **Install Extension** - Click "Load unpacked" - - Select the `extension/` folder from this DXT package + - Select the `extension/dist/chrome/` folder from this DXT package - Extension should appear in your extensions list with OpenDia icon #### For Firefox @@ -298,7 +306,7 @@ Download the latest extension from: https://github.com/aeonfun/opendia/releases 1. **Load Temporary Add-on** - Go to `about:debugging#/runtime/this-firefox` - Click "Load Temporary Add-on..." - - Select the `manifest-firefox.json` file from the `extension/` folder + - Select the `manifest.json` file from the `extension/dist/firefox/` folder > **Firefox Note**: Extensions are loaded as temporary add-ons and will be removed when Firefox restarts. For permanent installation, use the signed extension from GitHub releases. diff --git a/opendia-extension/build.js b/opendia-extension/build.js index 5198cd3..9db853e 100644 --- a/opendia-extension/build.js +++ b/opendia-extension/build.js @@ -31,15 +31,6 @@ async function buildForBrowser(browser) { path.join(buildDir, 'src/polyfill/browser-polyfill.min.js') ); - // Browser-specific post-processing - if (browser === 'chrome') { - console.log('๐Ÿ“ฆ Chrome MV3: Service worker mode enabled'); - // No additional processing needed for Chrome - } else if (browser === 'firefox') { - console.log('๐ŸฆŠ Firefox MV2: Background page mode enabled'); - // No additional processing needed for Firefox - } - console.log(`โœ… ${browser} extension built successfully in ${buildDir}`); } diff --git a/opendia-extension/manifest-chrome.json b/opendia-extension/manifest-chrome.json index a99dfef..8caee87 100644 --- a/opendia-extension/manifest-chrome.json +++ b/opendia-extension/manifest-chrome.json @@ -14,8 +14,6 @@ "activeTab", "storage", "scripting", - "webNavigation", - "notifications", "bookmarks", "history" ], diff --git a/opendia-extension/manifest-firefox.json b/opendia-extension/manifest-firefox.json index 4215458..fe3e891 100644 --- a/opendia-extension/manifest-firefox.json +++ b/opendia-extension/manifest-firefox.json @@ -19,12 +19,8 @@ "tabs", "activeTab", "storage", - "webNavigation", - "notifications", "bookmarks", "history", - "webRequest", - "webRequestBlocking", "" ], "background": { diff --git a/opendia-extension/manifest.json b/opendia-extension/manifest.json deleted file mode 100644 index f080b85..0000000 --- a/opendia-extension/manifest.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "manifest_version": 3, - "name": "OpenDia", - "version": "1.1.0", - "description": "Connect your browser to AI models", - "icons": { - "16": "icon-16.png", - "32": "icon-32.png", - "48": "icon-48.png", - "128": "icon-128.png" - }, - "permissions": [ - "tabs", - "activeTab", - "storage", - "scripting", - "webNavigation", - "notifications", - "bookmarks", - "history" - ], - "host_permissions": [ - "" - ], - "background": { - "service_worker": "background.js" - }, - "action": { - "default_popup": "popup.html", - "default_title": "OpenDia" - }, - "content_scripts": [ - { - "matches": [""], - "js": ["content.js"], - "run_at": "document_idle" - } - ], - "externally_connectable": { - "ids": ["*"], - "matches": ["http://localhost/*"] - } -} diff --git a/opendia-extension/releases/opendia-chrome-1.0.6.zip b/opendia-extension/releases/opendia-chrome-1.0.6.zip deleted file mode 100644 index 909bf4f..0000000 Binary files a/opendia-extension/releases/opendia-chrome-1.0.6.zip and /dev/null differ diff --git a/opendia-extension/releases/opendia-firefox-1.0.6.zip b/opendia-extension/releases/opendia-firefox-1.0.6.zip deleted file mode 100644 index 615f2f5..0000000 Binary files a/opendia-extension/releases/opendia-firefox-1.0.6.zip and /dev/null differ diff --git a/opendia-extension/src/background/background.js b/opendia-extension/src/background/background.js index 8191db5..7c287ba 100644 --- a/opendia-extension/src/background/background.js +++ b/opendia-extension/src/background/background.js @@ -33,7 +33,7 @@ browser.storage.local.get(['safetyMode'], (result) => { class ConnectionManager { constructor() { this.mcpSocket = null; - this.reconnectInterval = null; + this.reconnectTimer = null; this.reconnectAttempts = 0; this.heartbeatInterval = null; this.isServiceWorker = browserInfo.isServiceWorker; @@ -69,22 +69,11 @@ class ConnectionManager { }); } + // Reuse a live socket: reconnecting per operation replaced the socket the + // request arrived on, so the reply was written to a CONNECTING socket. async connect() { - if (this.isServiceWorker) { - // Reuse a live socket: reconnecting per operation replaced the socket the - // request arrived on, so the reply was written to a CONNECTING socket. - if (!this.mcpSocket || this.mcpSocket.readyState !== WebSocket.OPEN) { - console.log('๐Ÿ”ง Chrome MV3: Creating temporary connection'); - await this.createConnection(); - } - } else { - // Firefox MV2: Maintain persistent connection - if (!this.mcpSocket || this.mcpSocket.readyState !== WebSocket.OPEN) { - console.log('๐ŸฆŠ Firefox MV2: Creating persistent connection'); - await this.createConnection(); - } else { - console.log('๐ŸฆŠ Firefox MV2: Using existing connection'); - } + if (!this.mcpSocket || this.mcpSocket.readyState !== WebSocket.OPEN) { + await this.createConnection(); } } @@ -93,49 +82,55 @@ class ConnectionManager { // Try port discovery if using default URL or if connection failed if (MCP_SERVER_URL === 'ws://localhost:5555' || this.reconnectAttempts > 2) { await this.discoverServerPorts(); - // No reset here: discovery finding a port is not evidence the - // connection succeeded. Resetting made the counter oscillate 0->3->0, - // so backoff never grew and the give-up guard was unreachable. The - // real reset lives in onopen. + // Deliberately no reset: finding a port is not evidence the connection + // opened. The reset lives in onopen so backoff can actually grow. } console.log('๐Ÿ”— Connecting to MCP server at', MCP_SERVER_URL); - this.mcpSocket = new WebSocket(MCP_SERVER_URL); - - this.mcpSocket.onopen = () => { + // Handlers are bound to this specific socket. The server closes the old + // socket when the extension reconnects, so a stale close event can arrive + // after a healthy one is live; without the identity check it would clear + // the new connection's heartbeat and arm a reconnect that never clears. + // Mirrors the `chromeExtensionSocket === ws` guard on the server side. + const socket = new WebSocket(MCP_SERVER_URL); + this.mcpSocket = socket; + + socket.onopen = () => { + if (this.mcpSocket !== socket) return; console.log('โœ… Connected to MCP server'); - this.clearReconnectInterval(); + this.clearReconnectTimer(); this.reconnectAttempts = 0; // Reset attempts on successful connection - + const tools = getAvailableTools(); console.log(`๐Ÿ”ง Registering ${tools.length} tools:`, tools.map(t => t.name)); - - // Register available browser functions - this.mcpSocket.send(JSON.stringify({ + + socket.send(JSON.stringify({ type: 'register', tools: tools })); - + // Setup heartbeat for persistent connections if (!this.isServiceWorker) { this.setupHeartbeat(); } }; - - this.mcpSocket.onmessage = async (event) => { + + socket.onmessage = async (event) => { + if (this.mcpSocket !== socket) return; const message = JSON.parse(event.data); await handleMCPRequest(message); }; - - this.mcpSocket.onclose = (event) => { + + socket.onclose = (event) => { + if (this.mcpSocket !== socket) return; console.log(`โŒ Disconnected from MCP server (code: ${event.code}, reason: ${event.reason})`); this.clearHeartbeat(); // Clear heartbeat on disconnect this.reconnectAttempts++; - + // Check if this was a normal closure or abnormal if (event.code !== 1000 && event.code !== 1001) { console.log('๐Ÿ”„ Abnormal WebSocket closure, will attempt reconnection'); - + if (!this.isServiceWorker) { // Firefox: Attempt to reconnect this.scheduleReconnect(); @@ -145,13 +140,15 @@ class ConnectionManager { console.log('๐Ÿ”„ Normal WebSocket closure'); } }; - - this.mcpSocket.onerror = (error) => { + + // No attempt counting here: onclose always follows onerror for a failed + // socket, so incrementing in both halved the effective retry budget. + socket.onerror = (error) => { + if (this.mcpSocket !== socket) return; console.log('โš ๏ธ MCP WebSocket error:', error); - this.reconnectAttempts++; }; - await this.waitForSocketOpen(this.mcpSocket); + await this.waitForSocketOpen(socket); } catch (error) { console.error('Connection failed:', error); @@ -211,40 +208,36 @@ class ConnectionManager { } } + // One-shot, not an interval: a fixed period fires again while the previous + // attempt is still in port discovery (7 sequential un-timed fetches), which + // overwrites this.mcpSocket and orphans the pending socket. Failure re-arms + // via createConnection's catch and onclose, so nothing is lost. scheduleReconnect() { - this.clearReconnectInterval(); - + this.clearReconnectTimer(); + + if (this.reconnectAttempts >= 10) { + console.log('โŒ Maximum reconnection attempts reached'); + return; + } + // Exponential backoff for reconnection attempts const backoffTime = Math.min(5000 * Math.pow(2, this.reconnectAttempts), 30000); console.log(`๐Ÿ”„ Scheduling reconnection in ${backoffTime}ms (attempt ${this.reconnectAttempts})`); - - this.reconnectInterval = setInterval(() => { - if (this.reconnectAttempts < 10) { - this.connect().catch(() => {}); - } else { - console.log('โŒ Maximum reconnection attempts reached'); - this.clearReconnectInterval(); - } + + this.reconnectTimer = setTimeout(() => { + this.connect().catch(() => {}); }, backoffTime); } - clearReconnectInterval() { - if (this.reconnectInterval) { - clearInterval(this.reconnectInterval); - this.reconnectInterval = null; + clearReconnectTimer() { + if (this.reconnectTimer) { + clearTimeout(this.reconnectTimer); + this.reconnectTimer = null; } } async ensureConnection() { - if (this.isServiceWorker) { - // Chrome: Always create fresh connection - await this.connect(); - } else { - // Firefox: Use existing or create new - if (!this.mcpSocket || this.mcpSocket.readyState !== WebSocket.OPEN) { - await this.connect(); - } - } + await this.connect(); return this.mcpSocket; } @@ -270,25 +263,30 @@ class ConnectionManager { const connectionManager = new ConnectionManager(); // Content script management for background tabs +// Ping the content script in a tab. Rejects on timeout or extension error. +function pingContentScript(tabId, timeoutMs) { + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + reject(new Error(`Content script ping timeout after ${timeoutMs}ms`)); + }, timeoutMs); + + browser.tabs.sendMessage(tabId, { action: 'ping' }, (response) => { + clearTimeout(timer); + if (browser.runtime.lastError) { + reject(new Error(browser.runtime.lastError.message)); + } else { + resolve(response); + } + }); + }); +} + async function ensureContentScriptReady(tabId, retries = 3) { for (let attempt = 1; attempt <= retries; attempt++) { try { // Test if content script is responsive - const response = await new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - reject(new Error('Content script ping timeout')); - }, 2000); - - browser.tabs.sendMessage(tabId, { action: 'ping' }, (response) => { - clearTimeout(timeout); - if (browser.runtime.lastError) { - reject(new Error(browser.runtime.lastError.message)); - } else { - resolve(response); - } - }); - }); - + const response = await pingContentScript(tabId, 2000); + if (response && response.success) { console.log(`โœ… Content script ready in tab ${tabId}`); return true; @@ -339,18 +337,8 @@ async function ensureContentScriptReady(tabId, retries = 3) { await new Promise(resolve => setTimeout(resolve, 1000)); // Test again - const testResponse = await new Promise((resolve, reject) => { - const timeout = setTimeout(() => reject(new Error('Timeout after injection')), 3000); - browser.tabs.sendMessage(tabId, { action: 'ping' }, (response) => { - clearTimeout(timeout); - if (browser.runtime.lastError) { - reject(new Error(browser.runtime.lastError.message)); - } else { - resolve(response); - } - }); - }); - + const testResponse = await pingContentScript(tabId, 3000); + if (testResponse && testResponse.success) { console.log(`โœ… Content script successfully injected into tab ${tabId}`); return true; @@ -402,14 +390,9 @@ async function getTabContentScriptStatus(tabId) { return { ready: false, reason: 'restricted_url', url: tab.url }; } - const response = await new Promise((resolve, reject) => { - const timeout = setTimeout(() => resolve(null), 3000); // Increase to 3 seconds - browser.tabs.sendMessage(tabId, { action: 'ping' }, (response) => { - clearTimeout(timeout); - resolve(response); - }); - }); - + // A failed ping is the answer here, not an error: report the tab as not loaded. + const response = await pingContentScript(tabId, 3000).catch(() => null); + if (response && response.success) { return { ready: true, reason: 'active', url: tab.url }; } else { @@ -945,20 +928,23 @@ function getAvailableTools() { inputSchema: { type: "object", properties: { - link_type: { - type: "string", - enum: ["all", "internal", "external"], - default: "all", - description: "Filter by internal/external links" + include_internal: { + type: "boolean", + default: true, + description: "Include links pointing at the current domain" }, - domains: { - type: "array", - items: { type: "string" }, - description: "Filter by specific domains (optional)" + include_external: { + type: "boolean", + default: true, + description: "Include links pointing off the current domain" + }, + domain_filter: { + type: "string", + description: "Only return links whose domain contains this string (optional)" }, max_results: { type: "number", - default: 50, + default: 100, maximum: 200, description: "Maximum links to return" } @@ -1137,30 +1123,34 @@ async function handleMCPRequest(message) { } } -// Enhanced content script communication with background tab support -async function sendToContentScript(action, data, targetTabId = null) { - let targetTab; - - if (targetTabId) { - // Use specific tab +// Content script communication, targeting a specific tab or the active one +// Resolve the tab a tool should act on: an explicit tab id, else the active tab. +async function resolveTargetTab(tabId = null) { + if (tabId) { try { - targetTab = await browser.tabs.get(targetTabId); + return await browser.tabs.get(tabId); } catch (error) { - throw new Error(`Tab ${targetTabId} not found or inaccessible`); - } - } else { - // Fallback to active tab (maintains compatibility) - const [activeTab] = await browser.tabs.query({ - active: true, - currentWindow: true, - }); - - if (!activeTab) { - throw new Error('No active tab found'); + // tabs.get also rejects for reasons other than "not found" (bad id type, + // incognito access denied), so keep the underlying message. + throw new Error(`Tab ${tabId} not found or inaccessible: ${error.message}`); } - targetTab = activeTab; } - + + // tabs.query legitimately returns [] when there is no focused normal window. + const [activeTab] = await browser.tabs.query({ + active: true, + currentWindow: true, + }); + + if (!activeTab) { + throw new Error('No active tab found'); + } + return activeTab; +} + +async function sendToContentScript(action, data, targetTabId = null) { + const targetTab = await resolveTargetTab(targetTabId); + // Ensure content script is available in the target tab await ensureContentScriptReady(targetTab.id); @@ -1178,16 +1168,7 @@ async function sendToContentScript(action, data, targetTabId = null) { } async function navigateToUrl(url, waitFor, timeout = 10000) { - const [activeTab] = await browser.tabs.query({ - active: true, - currentWindow: true, - }); - - // tabs.query legitimately returns [] (no focused normal window), which used - // to crash here on activeTab.id. Other callers already guard this. - if (!activeTab) { - throw new Error("No active tab found"); - } + const activeTab = await resolveTargetTab(); await browser.tabs.update(activeTab.id, { url }); @@ -1231,7 +1212,7 @@ async function waitForElement(tabId, selector, timeout = 5000) { throw new Error(`Timeout waiting for element: ${selector}`); } -// Enhanced Tab Management Functions with Batch Support +// Tab management, single and batched async function createTab(params) { const { url, @@ -1267,7 +1248,7 @@ async function createTab(params) { const urlArray = Array(count).fill(url); return await createTabsBatch(urlArray, active, wait_for, timeout, batch_settings); } else { - // Single tab creation (legacy behavior) + // Single tab creation console.log(`๐Ÿ“ฑ Using single tab mode for: ${url || 'about:blank'}`); return await createSingleTab(url, active, wait_for, timeout); } @@ -1309,15 +1290,17 @@ function validateTabCreateParams(params) { } } - // Validate count - if (count < 1 || count > 50) { - return { valid: false, error: "Count must be between 1 and 50" }; + // Validate count. Both comparisons coerce, so a string or fractional count + // passed the range check and then broke Array(count): "5" yielded a single tab + // reported as a full success, 2.5 threw an opaque "Invalid array length". + if (!Number.isInteger(count) || count < 1 || count > 50) { + return { valid: false, error: "Count must be an integer between 1 and 50" }; } return { valid: true }; } -// Single tab creation (original behavior) +// Single tab creation async function createSingleTab(url, active, wait_for, timeout) { const createProperties = { active }; if (url) { @@ -1393,11 +1376,18 @@ async function createTabsBatch(urls, active, wait_for, timeout, batch_settings = console.log('๐Ÿ” createTabsBatch called with:', { urls: urls.length, batch_settings }); const { - chunk_size = 5, + chunk_size: requested_chunk_size = 5, delay_between_chunks = 1000, delay_between_tabs = 200 } = batch_settings || {}; - + + // A destructuring default only covers undefined, so an explicit 0 or a negative + // survived and the chunk loop never advanced - an unbounded spin in the worker + // while the server call timed out at 30s. + const chunk_size = Number.isInteger(requested_chunk_size) && requested_chunk_size > 0 + ? requested_chunk_size + : 5; + const startTime = Date.now(); const totalTabs = urls.length; const createdTabs = []; @@ -1497,6 +1487,7 @@ async function createTabsBatch(urls, active, wait_for, timeout, batch_settings = total_requested: totalTabs, successful: successCount, failed: errorCount, + chunks_processed: Math.ceil(totalTabs / chunk_size), execution_time_ms: executionTime }, // Only include full tab details for small batches @@ -1852,27 +1843,7 @@ async function getSelectedText(params) { } = params; try { - let targetTab; - - if (tab_id) { - // Use specific tab - try { - targetTab = await browser.tabs.get(tab_id); - } catch (error) { - throw new Error(`Tab ${tab_id} not found or inaccessible`); - } - } else { - // Get the active tab - const [activeTab] = await browser.tabs.query({ - active: true, - currentWindow: true, - }); - - if (!activeTab) { - throw new Error("No active tab found"); - } - targetTab = activeTab; - } + const targetTab = await resolveTargetTab(tab_id); // Execute script to get selected text - handle browser differences let results; @@ -1944,8 +1915,8 @@ async function getSelectedText(params) { return response; } catch (error) { - // These used to return has_selection: false, which formatSelectedTextResult - // renders as "No text selected" โ€” a failure was reported as an empty page. + // Throw rather than returning has_selection:false, which the formatter + // renders as "No text selected" - a failure indistinguishable from success. throw new Error(`Failed to get selected text: ${error.message}`); } } @@ -2027,8 +1998,8 @@ browser.runtime.onMessage.addListener((request, sender, sendResponse) => { tools: tools.map(t => t.name) }); } else if (request.action === "reconnect") { - // Report the real outcome: this used to answer success before the socket - // had opened, so the popup's reconnect button always looked like it worked. + // Answer after the socket opens, not before, so the popup reports the + // real outcome rather than always looking successful. connectionManager.connect() .then(() => sendResponse({ success: true })) .catch((error) => sendResponse({ success: false, error: error.message })); diff --git a/opendia-extension/src/content/content.js b/opendia-extension/src/content/content.js index 2743c03..d774d09 100644 --- a/opendia-extension/src/content/content.js +++ b/opendia-extension/src/content/content.js @@ -1,4 +1,5 @@ -// Enhanced Browser Automation Content Script with Anti-Detection +// Browser automation content script. Fills React-controlled editors via +// execCommand, which they accept where programmatic value writes are ignored. // Import WebExtension polyfill for cross-browser compatibility if (typeof browser === 'undefined' && typeof chrome !== 'undefined') { globalThis.browser = chrome; @@ -219,7 +220,7 @@ class BrowserAutomation { async executeDirectBypass(element, value, platformConfig, element_id) { try { - console.log(`๐Ÿฆ Executing ${platformConfig.bypassMethod} bypass`); + console.log(`๐Ÿ”“ Executing ${platformConfig.bypassMethod} bypass`); switch (platformConfig.bypassMethod) { case "twitter_direct": @@ -247,23 +248,20 @@ class BrowserAutomation { } 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 + // Draft.js ignores programmatic .value writes and synthesized input events. + // execCommand("insertText") produces a trusted beforeinput, which the editor's + // own handler accepts and mirrors into React state. Focus and click first: + // the handler is only attached once the editor considers itself active. element.focus(); element.click(); const execResult = document.execCommand("insertText", false, value); - // Wait for React state to update + // The editor commits on its next render; reading sooner sees the old value. await new Promise((r) => setTimeout(r, 500)); - // Verify success const currentText = element.textContent || element.value || ""; const success = currentText.includes(value); @@ -279,16 +277,13 @@ class BrowserAutomation { } 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 + // LinkedIn's editor appends rather than replaces, so clear before inserting. if (element.textContent) { document.execCommand("selectAll"); document.execCommand("delete"); @@ -296,7 +291,7 @@ class BrowserAutomation { const execResult = document.execCommand("insertText", false, value); - // LinkedIn needs more time for state updates + // Slower to commit than Twitter's; 500ms reads back a partial value. await new Promise((r) => setTimeout(r, 800)); const currentText = element.textContent || element.value || ""; @@ -314,16 +309,12 @@ class BrowserAutomation { } 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"); @@ -331,7 +322,8 @@ class BrowserAutomation { const execResult = document.execCommand("insertText", false, value); - // Trigger Facebook-specific events + // Facebook's composer also listens for these directly, unlike Twitter's, + // which picks the change up from beforeinput alone. element.dispatchEvent(new Event("input", { bubbles: true })); element.dispatchEvent(new Event("change", { bubbles: true })); @@ -351,13 +343,12 @@ class BrowserAutomation { }; } + // Reached only if a platform config names a bypassMethod without its own case. + // Kept as the extension point for adding a platform to ANTI_DETECTION_PLATFORMS. 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); @@ -390,7 +381,7 @@ class BrowserAutomation { throw new Error(`Element not found: ${element_id}`); } - // Enhanced focus sequence for modern web apps + // Modern editors attach their input handler on focus, not on load. if (force_focus) { await this.ensureProperFocus(element); } else { @@ -567,7 +558,6 @@ class BrowserAutomation { max_results = 10, }) { const startTime = performance.now(); - const pageType = this.detectPageType(); // Use default max results limit max_results = Math.min(max_results, 7); // Allow slightly more for detailed analysis @@ -620,7 +610,6 @@ class BrowserAutomation { } async extractContent({ content_type, max_items = 20, summarize = true }) { - console.log(`๐Ÿ” extractContent called with: content_type=${content_type}, max_items=${max_items}, summarize=${summarize}`); const startTime = performance.now(); const extractors = { article: () => this.extractArticleContent(summarize), @@ -650,8 +639,7 @@ class BrowserAutomation { extracted_at: new Date().toISOString(), }; } else { - // Legacy full content extraction - console.log(`๐ŸŽฏ Returning FULL content: ${rawContent?.content?.length || 0} characters`); + // Full content, returned when summarize is false return { content: rawContent, method: "semantic_extraction", @@ -663,7 +651,6 @@ class BrowserAutomation { } extractArticleContent(summarize = true) { - console.log(`๐Ÿ“„ extractArticleContent called with summarize=${summarize}`); const article = document.querySelector( 'article, [role="article"], .article-content, main' ); @@ -672,8 +659,6 @@ class BrowserAutomation { ?.textContent?.trim(); const content = article?.textContent?.trim() || this.extractMainContent(); - console.log(`๐Ÿ“ Extracted content length: ${content?.length || 0} characters`); - console.log(`๐Ÿ“ Content preview: ${content?.substring(0, 200)}...`); return { title, @@ -1000,7 +985,7 @@ class BrowserAutomation { } calculateQualityScore(results) { - // An extractor that matched nothing returns [], which used to make every + // An extractor that matched nothing returns [], which would make every // term NaN and ship "NaN%" to the model. if (results.length === 0) return 0; @@ -1295,6 +1280,7 @@ class BrowserAutomation { if (checkCondition()) { return { condition_met: true, + condition_type: condition_type, wait_time: Date.now() - startTime, }; } @@ -1626,7 +1612,6 @@ class BrowserAutomation { } async fullEnhancedAnalysis(intent_hint, max_results) { - // Enhanced version of semantic analysis with better filtering const relevantElements = document.querySelectorAll(` button, input, select, textarea, a[href], [role="button"], [role="textbox"], [role="searchbox"], @@ -2050,12 +2035,10 @@ class BrowserAutomation { }; const style = moodMap[mood.toLowerCase()] || moodMap['cozy coffee shop']; - return this.buildMoodCSS(style, intensity); + return this.buildMoodCSS(style); } - buildMoodCSS(style, intensity) { - const opacity = intensity === 'subtle' ? '0.3' : intensity === 'medium' ? '0.6' : '0.9'; - + buildMoodCSS(style) { let css = ` body { background: ${style.background} !important; diff --git a/opendia-extension/src/popup/popup.js b/opendia-extension/src/popup/popup.js index 4e9757a..3e4e7fc 100644 --- a/opendia-extension/src/popup/popup.js +++ b/opendia-extension/src/popup/popup.js @@ -15,8 +15,7 @@ let currentPage = document.getElementById("currentPage"); let serverUrl = document.getElementById("serverUrl"); // Shown when the background script can't be reached. Kept as one list so the -// name list and the count can't disagree โ€” they previously said 17 and omitted -// page_style, while the extension registered 18. +// names and the count cannot disagree. const KNOWN_TOOLS = [ "page_analyze", "page_extract_content", "element_click", "element_fill", "element_get_state", "page_navigate", "page_wait_for", "page_scroll", diff --git a/opendia-extension/test-extension.js b/opendia-extension/test-extension.js index f186dcc..2d2f736 100644 --- a/opendia-extension/test-extension.js +++ b/opendia-extension/test-extension.js @@ -1,182 +1,151 @@ -// Simple test script to verify browser extension compatibility +// Structural checks over the built extensions in dist/. +// Every check is an assertion: a failing probe fails the process, so CI gates on it. const fs = require('fs'); const path = require('path'); +// Prints "