From 1d3eed0520b9e2d8b021bcbafeab6643609a9637 Mon Sep 17 00:00:00 2001 From: Aaron Elijah Mars <61592645+aaronjmars@users.noreply.github.com> Date: Tue, 21 Jul 2026 10:30:53 -0400 Subject: [PATCH 1/7] optimize: remove unreferenced code and in-motion comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deletions of code that never executes or is never read. No user- or model-visible string changes, so tool output is byte-identical. - server.js: unused `createServer` import; `killExisting` (parsed, never read — the kill at startup is unconditional); `organizedTabs`; `currentTime`/`oneHourAgo` (computed, never used — the archive_old rule is the tab.index heuristic below them) - server.js: the `settings_used` block in formatTabCreateResult. The extension's createTabsBatch never sets that field, so the branch could not be entered. - background.js: `generateTestUrls`, `estimateBatchTime` — no call sites - content.js: `estimateTokenUsage` — no call sites; unused `startTime` - popup.js: `toolsList` — built and never referenced - server.js: strip `ADD:`/`START:` prefixes and a stale port-migration note from comments describing changes rather than code Gate: node --check, build, validate, structure tests, web-ext lint all green; web-ext warning count unchanged at 3. --- .../src/background/background.js | 25 -------------- opendia-extension/src/content/content.js | 7 ---- opendia-extension/src/popup/popup.js | 7 ---- opendia-mcp/server.js | 34 ++++++------------- 4 files changed, 10 insertions(+), 63 deletions(-) diff --git a/opendia-extension/src/background/background.js b/opendia-extension/src/background/background.js index e390c29..1ee08c1 100644 --- a/opendia-extension/src/background/background.js +++ b/opendia-extension/src/background/background.js @@ -1469,31 +1469,6 @@ async function createTabsBatch(urls, active, wait_for, timeout, batch_settings = return result; } -// Utility function to generate URLs for testing/demo purposes -function generateTestUrls(baseUrl, count) { - const urls = []; - for (let i = 1; i <= count; i++) { - urls.push(`${baseUrl}?tab=${i}`); - } - return urls; -} - -// Batch operation helper functions -function estimateBatchTime(urlCount, batchSettings = {}) { - const { - chunk_size = 5, - delay_between_chunks = 1000, - delay_between_tabs = 200 - } = batchSettings || {}; - - const totalChunks = Math.ceil(urlCount / chunk_size); - const timePerChunk = (chunk_size - 1) * delay_between_tabs; // delays within chunk - const timeForChunks = totalChunks * timePerChunk; - const timeBetweenChunks = (totalChunks - 1) * delay_between_chunks; - - return timeForChunks + timeBetweenChunks; // in milliseconds -} - async function closeTabs(params) { const { tab_id, tab_ids } = params; diff --git a/opendia-extension/src/content/content.js b/opendia-extension/src/content/content.js index 0ed48f7..de36bd9 100644 --- a/opendia-extension/src/content/content.js +++ b/opendia-extension/src/content/content.js @@ -610,8 +610,6 @@ class BrowserAutomation { element_ids, max_results = 5, }) { - const startTime = performance.now(); - // Two-phase approach if (phase === "discover") { return await this.quickDiscovery({ intent_hint, max_results }); @@ -2281,11 +2279,6 @@ class BrowserAutomation { ); } - estimateTokenUsage(result) { - // Estimate token count based on result size - const jsonString = JSON.stringify(result); - return Math.ceil(jsonString.length / 4); // Rough estimate: 4 chars per token - } // Get all links on the page with filtering options async getPageLinks(options = {}) { const { diff --git a/opendia-extension/src/popup/popup.js b/opendia-extension/src/popup/popup.js index e221a82..65df29a 100644 --- a/opendia-extension/src/popup/popup.js +++ b/opendia-extension/src/popup/popup.js @@ -16,13 +16,6 @@ let serverUrl = document.getElementById("serverUrl"); // Get dynamic tool count from background script function updateToolCount() { - const toolsList = [ - "page_analyze", "page_extract_content", "element_click", "element_fill", - "element_get_state", "page_navigate", "page_wait_for", "page_scroll", - "tab_create", "tab_close", "tab_list", "tab_switch", - "get_bookmarks", "add_bookmark", "get_history", "get_selected_text", "get_page_links" - ]; - if (runtimeAPI?.id) { runtimeAPI.sendMessage({ action: "getToolCount" }, (response) => { if (!runtimeAPI.lastError && response?.toolCount) { diff --git a/opendia-mcp/server.js b/opendia-mcp/server.js index 3117a89..8264e54 100755 --- a/opendia-mcp/server.js +++ b/opendia-mcp/server.js @@ -5,17 +5,15 @@ const express = require("express"); const net = require('net'); const { exec } = require('child_process'); -// ADD: New imports for SSE transport +// SSE transport const cors = require('cors'); const crypto = require('crypto'); -const { createServer } = require('http'); const { spawn } = require('child_process'); -// ADD: Enhanced command line argument parsing +// Command line argument parsing const args = process.argv.slice(2); const enableTunnel = args.includes('--tunnel') || args.includes('--auto-tunnel'); const sseOnly = args.includes('--sse-only'); -const killExisting = args.includes('--kill-existing'); // Parse port arguments const wsPortArg = args.find(arg => arg.startsWith('--ws-port=')); @@ -42,7 +40,7 @@ const AUTH_TOKEN = requiresToken ? (tokenArg ? tokenArg.split('=')[1] : crypto.randomBytes(24).toString('hex')) : null; -// Default ports (changed from 3000/3001 to 5555/5556) +// Default ports let WS_PORT = wsPortArg ? parseInt(wsPortArg.split('=')[1]) : (portArg ? parseInt(portArg.split('=')[1]) : 5555); let HTTP_PORT = httpPortArg ? parseInt(httpPortArg.split('=')[1]) : (portArg ? parseInt(portArg.split('=')[1]) + 1 : 5556); @@ -162,7 +160,7 @@ async function handlePortConflict(port, portName) { } } -// ADD: Express app setup +// Express app setup const app = express(); // Loopback is not a trust boundary for a browser: a page the user visits can @@ -879,7 +877,7 @@ function formatLinksResult(result, metadata) { function formatTabCreateResult(result, metadata) { // Handle batch operations if (result.batch_operation) { - const { summary, created_tabs, settings_used, warnings, errors } = result; + const { summary, created_tabs, warnings, errors } = result; let output = `🚀 Batch tab creation completed 📊 Summary: ${summary.successful}/${summary.total_requested} tabs created successfully @@ -913,14 +911,6 @@ function formatTabCreateResult(result, metadata) { output += '\n'; } - // Add settings used (if available) - if (settings_used) { - output += `⚙️ Settings used:\n`; - output += ` • Chunk size: ${settings_used.chunk_size}\n`; - output += ` • Delay between chunks: ${settings_used.delay_between_chunks}ms\n`; - output += ` • Delay between tabs: ${settings_used.delay_between_tabs}ms\n`; - } - return output + `\n${JSON.stringify(metadata, null, 2)}`; } @@ -1762,7 +1752,7 @@ function setupWebSocketHandlers() { }); } -// ADD: SSE/HTTP endpoints for online AI +// SSE/HTTP endpoints for online AI app.route('/sse') .all(requireToken) .get((req, res) => { @@ -1849,7 +1839,7 @@ if (!sseOnly) { }); } -// ADD: Health check endpoint (update existing one) +// Health check endpoint app.get('/health', (req, res) => { res.json({ status: 'ok', @@ -1872,7 +1862,7 @@ app.get('/health', (req, res) => { }); }); -// ADD: Port discovery endpoint for Chrome/Firefox extension +// Port discovery endpoint for the browser extension app.get('/ports', (req, res) => { res.json({ websocket: WS_PORT, @@ -1883,7 +1873,7 @@ app.get('/ports', (req, res) => { }); }); -// START: Enhanced server startup with port conflict resolution +// Server startup with port conflict resolution async function startServer() { console.error("🚀 Enhanced Browser MCP Server with Anti-Detection Features"); console.error(`📊 Default ports: WebSocket=${WS_PORT}, HTTP=${HTTP_PORT}`); @@ -2421,8 +2411,7 @@ async function executeOrganizeTabsWorkflow(args) { result += `📊 **Starting with ${tabs.length} tabs**\n\n`; let closedTabs = []; - let organizedTabs = []; - + if (strategy === "close_duplicates") { // Find and close duplicate tabs const seenUrls = new Set(); @@ -2485,9 +2474,6 @@ async function executeOrganizeTabsWorkflow(args) { } else if (strategy === "archive_old") { // Find tabs that haven't been active recently - const currentTime = Date.now(); - const oneHourAgo = currentTime - (60 * 60 * 1000); - // Since we don't have last accessed time, we'll use a heuristic // based on tab loading status and position const staleTabsToClose = tabs.filter(tab => From 00188f21be03d0a8d0d952670a6ee804c9fd01b5 Mon Sep 17 00:00:00 2001 From: Aaron Elijah Mars <61592645+aaronjmars@users.noreply.github.com> Date: Tue, 21 Jul 2026 10:37:14 -0400 Subject: [PATCH 2/7] fix(security): reject empty --token= and --http-host= instead of failing open MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `args.find(a => a.startsWith('--token='))` matches the bare flag, so `--token=` produced AUTH_TOKEN = "" and requireToken's `if (!AUTH_TOKEN) return next()` waved every request through. The startup banner is gated on `if (AUTH_TOKEN)` too, so nothing was printed: `--tunnel --token=` published an unauthenticated browser-control endpoint with no visible sign. This defeated the loopback/token boundary added in ea71bb9. `--http-host=` had the same falsy-vs-nullish bug, binding 0.0.0.0 via `app.listen(PORT, "")`. Together (`--http-host= --token=`) they opened remote browser control to the network. Parse flags through argValue(), which returns null when a flag is absent and errors when its value is empty, so an empty value can no longer be mistaken for "not set". Ports go through portValue(): digits only, since Number() accepts '0x1f' and parseInt() accepts '80abc'. A bad port used to reach net.listen(NaN), which throws synchronously and — with no .catch on startServer() — died as an unhandled rejection; startServer() now reports the real message and exits 1. Verified: --token= / --http-host= / --port=abc / --port=0x10 each exit with a named error; default startup unchanged; --http-host=0.0.0.0 --token=mysecret123 binds 0.0.0.0 and returns 401 / 401 / 200 for missing / wrong / correct bearer tokens. --- opendia-mcp/server.js | 54 +++++++++++++++++++++++++++++++++---------- 1 file changed, 42 insertions(+), 12 deletions(-) diff --git a/opendia-mcp/server.js b/opendia-mcp/server.js index 8264e54..613a900 100755 --- a/opendia-mcp/server.js +++ b/opendia-mcp/server.js @@ -15,17 +15,37 @@ const args = process.argv.slice(2); const enableTunnel = args.includes('--tunnel') || args.includes('--auto-tunnel'); const sseOnly = args.includes('--sse-only'); -// Parse port arguments -const wsPortArg = args.find(arg => arg.startsWith('--ws-port=')); -const httpPortArg = args.find(arg => arg.startsWith('--http-port=')); -const portArg = args.find(arg => arg.startsWith('--port=')); +function usageError(message) { + console.error(`❌ ${message}`); + process.exit(1); +} + +// Returns the text after `=`, or null when the flag is absent. An empty value +// (`--token=`) is a usage error, not an empty string: treating it as falsy is +// what let `--token=` disable auth outright and `--http-host=` bind 0.0.0.0. +function argValue(name) { + const flag = args.find(arg => arg.startsWith(`${name}=`)); + if (flag === undefined) return null; + const value = flag.slice(name.length + 1); + if (value === '') usageError(`${name}= requires a value`); + return value; +} + +// Digits only — Number() would accept '0x1f' and parseInt() would accept '80abc'. +function portValue(name) { + const raw = argValue(name); + if (raw === null) return null; + if (!/^\d+$/.test(raw) || raw < 1 || raw > 65535) { + usageError(`${name}=${raw} is not a valid port (expected an integer 1-65535)`); + } + return Number(raw); +} // `POST /sse` hands whatever it receives to handleMCPRequest, which drives the // browser through the extension. Keep that surface on loopback by default; the // extension reaches it at localhost and `ngrok http` connects from this machine // too, so neither needs an off-host bind. --http-host= widens it deliberately. -const httpHostArg = args.find(arg => arg.startsWith('--http-host=')); -const HTTP_HOST = httpHostArg ? httpHostArg.split('=')[1] : '127.0.0.1'; +const HTTP_HOST = argValue('--http-host') ?? '127.0.0.1'; function isLoopbackHost(host) { return host === '127.0.0.1' || host === '::1' || host === 'localhost'; @@ -34,15 +54,21 @@ function isLoopbackHost(host) { // Once the MCP surface is reachable beyond this machine — widened bind or an // ngrok tunnel — loopback stops being the boundary and callers must present a // token. Locally it stays off so existing setups keep working untouched. -const tokenArg = args.find(arg => arg.startsWith('--token=')); +const explicitToken = argValue('--token'); const requiresToken = enableTunnel || !isLoopbackHost(HTTP_HOST); const AUTH_TOKEN = requiresToken - ? (tokenArg ? tokenArg.split('=')[1] : crypto.randomBytes(24).toString('hex')) + ? (explicitToken ?? crypto.randomBytes(24).toString('hex')) : null; // Default ports -let WS_PORT = wsPortArg ? parseInt(wsPortArg.split('=')[1]) : (portArg ? parseInt(portArg.split('=')[1]) : 5555); -let HTTP_PORT = httpPortArg ? parseInt(httpPortArg.split('=')[1]) : (portArg ? parseInt(portArg.split('=')[1]) + 1 : 5556); +const wsPortArg = portValue('--ws-port'); +const httpPortArg = portValue('--http-port'); +const portArg = portValue('--port'); +let WS_PORT = wsPortArg ?? portArg ?? 5555; +let HTTP_PORT = httpPortArg ?? (portArg !== null ? portArg + 1 : 5556); +if (HTTP_PORT > 65535) { + usageError(`--port=${portArg} leaves no room for the HTTP port (${HTTP_PORT})`); +} // Port conflict detection utilities async function checkPortInUse(port) { @@ -2651,5 +2677,9 @@ async function executeFillFormWorkflow(args) { } } -// Start the server -startServer(); +// Start the server. Without the catch, a throw in port resolution or tunnel +// setup surfaces as an unhandled rejection instead of a readable message. +startServer().catch((error) => { + console.error('❌ Failed to start OpenDia:', error.message); + process.exit(1); +}); From 8b4cdba7a7e109b15a1618694cbbeba7c7d50a6f Mon Sep 17 00:00:00 2001 From: Aaron Elijah Mars <61592645+aaronjmars@users.noreply.github.com> Date: Tue, 21 Jul 2026 10:41:03 -0400 Subject: [PATCH 3/7] fix: stop reporting tool failures to the model as success An MCP client acts on what these tools report, so a failure dressed as an empty result is worse than an error. get_history: the catch returned history_items: [], and formatHistoryResult branches only on that array being empty -- it never reads success/error. A permissions failure rendered as "No history items found" with isError:false. Now rethrows, so it reaches the JSON-RPC error boundary with its message. get_selected_text: four failure paths (tab missing, no active tab, script returned nothing, outer catch) returned objects lacking has_selection, which formatSelectedTextResult renders as "No text selected". Now throw. The genuine no-selection case still returns success with has_selection: false. page_navigate: a failed wait condition returned success:true plus a warning field the formatter never read, so the model was told navigation succeeded. The warning is now surfaced (tab_create already did this). Research workflow: appended "Current page bookmarked for reference" unconditionally after swallowing the bookmark failure into console.warn. Now reports what actually happened. Connection handling: createConnection resolved without waiting for the socket to open and swallowed failures, so handleMCPRequest proceeded into the tool switch anyway -- tab_close and element_click executed for real while the reply was written to a CONNECTING socket and dropped. The model saw only "Tool call timeout" and would reasonably retry a side effect. createConnection now awaits open and rethrows; the Chrome branch reuses a live socket instead of replacing the one the request arrived on; send() throws rather than dropping a frame; fire-and-forget reconnect paths catch explicitly; the popup's reconnect button reports the real outcome. Verified: full gate green (0 errors, same 3 pre-existing web-ext warnings, 19 structure markers unchanged); end-to-end round trip over WS + /sse confirms page_navigate renders the warning instead of success. NOT verified: the connection-lifecycle changes need a manual load in Chrome and Firefox -- no automated coverage exists for them. --- .../src/background/background.js | 153 ++++++++++-------- opendia-mcp/server.js | 10 +- 2 files changed, 92 insertions(+), 71 deletions(-) diff --git a/opendia-extension/src/background/background.js b/opendia-extension/src/background/background.js index 1ee08c1..d8d27d0 100644 --- a/opendia-extension/src/background/background.js +++ b/opendia-extension/src/background/background.js @@ -40,11 +40,43 @@ class ConnectionManager { this.isFirefox = browserInfo.isFirefox; } + // Resolves once the socket is usable, so callers can't send on a CONNECTING + // socket. Uses addEventListener so the onopen/onerror/onclose handlers + // assigned in createConnection stay intact. + waitForSocketOpen(socket, timeoutMs = 5000) { + if (socket.readyState === WebSocket.OPEN) return Promise.resolve(); + + return new Promise((resolve, reject) => { + const cleanup = () => { + clearTimeout(timer); + socket.removeEventListener('open', onOpen); + socket.removeEventListener('error', onFail); + socket.removeEventListener('close', onFail); + }; + const onOpen = () => { cleanup(); resolve(); }; + const onFail = () => { + cleanup(); + reject(new Error(`Could not reach the OpenDia server at ${MCP_SERVER_URL}`)); + }; + const timer = setTimeout(() => { + cleanup(); + reject(new Error(`Timed out connecting to ${MCP_SERVER_URL} after ${timeoutMs}ms`)); + }, timeoutMs); + + socket.addEventListener('open', onOpen); + socket.addEventListener('error', onFail); + socket.addEventListener('close', onFail); + }); + } + async connect() { if (this.isServiceWorker) { - // Chrome MV3: Create fresh connection for each operation - console.log('🔧 Chrome MV3: Creating temporary connection'); - await this.createConnection(); + // 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) { @@ -115,12 +147,19 @@ class ConnectionManager { console.log('⚠️ MCP WebSocket error:', error); this.reconnectAttempts++; }; - + + await this.waitForSocketOpen(this.mcpSocket); + } catch (error) { console.error('Connection failed:', error); if (!this.isServiceWorker) { this.scheduleReconnect(); } + // Rethrow so ensureConnection() fails before handleMCPRequest runs the + // tool. Swallowing here meant tab_close/element_click still executed + // against the browser while the reply was dropped, and the model saw + // only "Tool call timeout" — then reasonably retried the side effect. + throw error; } } @@ -155,7 +194,9 @@ class ConnectionManager { this.mcpSocket.send(JSON.stringify({ type: 'ping', timestamp: Date.now() })); } else if (this.mcpSocket?.readyState === WebSocket.CLOSED) { console.log('🔄 WebSocket closed, attempting reconnection...'); - this.connect(); + // Background retry: the reconnect loop owns recovery, so a failure + // here is expected and must not become an unhandled rejection. + this.connect().catch(() => {}); } }, 15000); // More frequent heartbeat for better reliability } @@ -176,7 +217,7 @@ class ConnectionManager { this.reconnectInterval = setInterval(() => { if (this.reconnectAttempts < 10) { - this.connect(); + this.connect().catch(() => {}); } else { console.log('❌ Maximum reconnection attempts reached'); this.clearReconnectInterval(); @@ -205,11 +246,12 @@ class ConnectionManager { } send(message) { - if (this.mcpSocket && this.mcpSocket.readyState === WebSocket.OPEN) { - this.mcpSocket.send(JSON.stringify(message)); - } else { - console.error('WebSocket not connected'); + if (!this.mcpSocket || this.mcpSocket.readyState !== WebSocket.OPEN) { + // Dropping the frame here left the server's pending call to expire as a + // generic 30s "Tool call timeout", hiding the real cause. + throw new Error(`WebSocket not connected; dropped response for id=${message?.id}`); } + this.mcpSocket.send(JSON.stringify(message)); } getStatus() { @@ -1075,14 +1117,20 @@ async function handleMCPRequest(message) { result, }); } catch (error) { - // Send error response - connectionManager.send({ - id, - error: { - message: error.message, - code: -32603, - }, - }); + // Send error response. Guarded because send() now throws when the socket + // is down, and this is the last handler — an escape here would be an + // unhandled rejection that loses the original error entirely. + try { + connectionManager.send({ + id, + error: { + message: error.message, + code: -32603, + }, + }); + } catch (sendError) { + console.error('Could not deliver error response:', error.message, '|', sendError.message); + } } } @@ -1779,17 +1827,10 @@ async function getHistory(params) { }; } catch (error) { - return { - success: false, - error: `History search failed: ${error.message}`, - history_items: [], - metadata: { - total_found: 0, - returned_count: 0, - search_params: params, - execution_time: new Date().toISOString() - } - }; + // Returning an empty history_items here rendered as "No history items + // found" via formatHistoryResult, which never reads success/error — a + // permissions failure was indistinguishable from an empty result. + throw new Error(`History search failed: ${error.message}`); } } @@ -1809,14 +1850,7 @@ async function getSelectedText(params) { try { targetTab = await browser.tabs.get(tab_id); } catch (error) { - return { - success: false, - error: `Tab ${tab_id} not found or inaccessible`, - selected_text: "", - metadata: { - execution_time: new Date().toISOString() - } - }; + throw new Error(`Tab ${tab_id} not found or inaccessible`); } } else { // Get the active tab @@ -1824,16 +1858,9 @@ async function getSelectedText(params) { active: true, currentWindow: true, }); - + if (!activeTab) { - return { - success: false, - error: "No active tab found", - selected_text: "", - metadata: { - execution_time: new Date().toISOString() - } - }; + throw new Error("No active tab found"); } targetTab = activeTab; } @@ -1856,14 +1883,7 @@ async function getSelectedText(params) { const result = results[0]?.result || results[0]; if (!result) { - return { - success: false, - error: "Failed to execute selection script", - selected_text: "", - metadata: { - execution_time: new Date().toISOString() - } - }; + throw new Error("Failed to execute selection script"); } if (!result.hasSelection) { @@ -1915,16 +1935,9 @@ async function getSelectedText(params) { return response; } catch (error) { - return { - success: false, - error: `Failed to get selected text: ${error.message}`, - selected_text: "", - has_selection: false, - metadata: { - execution_time: new Date().toISOString(), - error_details: error.stack - } - }; + // These used to return has_selection: false, which formatSelectedTextResult + // renders as "No text selected" — a failure was reported as an empty page. + throw new Error(`Failed to get selected text: ${error.message}`); } } @@ -1989,7 +2002,9 @@ function getSelectionFunction() { // Initialize connection when extension loads (with delay for server startup) setTimeout(() => { - connectionManager.connect(); + connectionManager.connect().catch((error) => { + console.log('Initial connection attempt failed:', error.message); + }); }, 1000); // Handle messages from popup @@ -2003,8 +2018,12 @@ browser.runtime.onMessage.addListener((request, sender, sendResponse) => { tools: tools.map(t => t.name) }); } else if (request.action === "reconnect") { - connectionManager.connect(); - sendResponse({ success: true }); + // 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. + connectionManager.connect() + .then(() => sendResponse({ success: true })) + .catch((error) => sendResponse({ success: false, error: error.message })); + return true; } else if (request.action === "getPorts") { sendResponse({ current: lastKnownPorts, diff --git a/opendia-mcp/server.js b/opendia-mcp/server.js index 613a900..f3eccad 100755 --- a/opendia-mcp/server.js +++ b/opendia-mcp/server.js @@ -549,9 +549,9 @@ function formatToolResult(toolName, result) { return formatElementFillResult(result, metadata); case "page_navigate": - return `✅ Successfully navigated to: ${ + return `${result.warning ? `⚠️ ${result.warning}` : `✅ Successfully navigated to: ${ result.url || "unknown URL" - }\n\n${JSON.stringify(metadata, null, 2)}`; + }`}\n\n${JSON.stringify(metadata, null, 2)}`; case "page_wait_for": return ( @@ -2217,14 +2217,16 @@ async function executeResearchWorkflow(args) { const currentUrl = pageContent.content?.url; const currentTitle = pageContent.summary?.title; + let bookmarkStatus = 'Page not bookmarked (no URL or title available)'; if (currentUrl && currentTitle) { try { await callBrowserTool('add_bookmark', { title: `[Research: ${topic}] ${currentTitle}`, url: currentUrl }); + bookmarkStatus = '✅ **Current page bookmarked for reference**'; } catch (bookmarkError) { - console.warn('Bookmark creation failed:', bookmarkError.message); + bookmarkStatus = `⚠️ Bookmark failed: ${bookmarkError.message}`; } } @@ -2282,7 +2284,7 @@ async function executeResearchWorkflow(args) { result += `• Focus on current page content and top 3 related links\n`; } - result += `\n✅ **Current page bookmarked for reference**`; + result += `\n${bookmarkStatus}`; return result; From 9db31eadc859c6af9f11e54d643b028e40a09934 Mon Sep 17 00:00:00 2001 From: Aaron Elijah Mars <61592645+aaronjmars@users.noreply.github.com> Date: Tue, 21 Jul 2026 10:46:51 -0400 Subject: [PATCH 4/7] optimize: delete the unreachable pattern tier and prompts workflows Two subsystems that no caller could reach. content.js -- the enhanced-pattern tier never ran. Its guard read `bestMethod`, an identifier declared nowhere in the repo, so evaluating it threw ReferenceError on every page where the anti-detection branch had not already filled quickMatches. The catch below logged "Enhanced patterns failed" and fell through to quickViewportScan, so the failure was invisible and tryEnhancedPatterns never executed once. The legacy single-phase engine was unreachable for a different reason: analyzePage only reaches it when `phase` is neither "discover" nor "detailed", but phase defaults to "discover" and is declared enum: ["discover", "detailed"] in both the extension's registry and the server's fallback copy -- so reaching it required a schema-forbidden value. Removed as one closed set, since legacyAnalysis was the only other caller of tryEnhancedPatterns: legacyAnalysis, tryEnhancedPatterns, ENHANCED_PATTERNS, parseIntent, findPatternElements, tryUniversalPatterns, tryPatternDatabase, detectSite, getUniversalPattern, PATTERN_DATABASE, trySemanticAnalysis, formatAnalysisResult, and isVisible (superseded everywhere by isLikelyVisible). The dispatch is now total on the declared enum. server.js -- the prompts system was unreachable per the MCP spec: the initialize response declares capabilities: { tools: {} } with no prompts capability, and prompts/get returned { content: [...] } where the spec requires { messages: [{ role, content }] }. Removed prompts/list, prompts/get and the six execute*Workflow functions. page_analyze output is unchanged: no removed code could execute. Verified: no dangling references to any removed symbol; gate green (0 errors, same 3 pre-existing web-ext warnings, 19 structure markers); live round trip over WS + /sse still registers tools, answers tools/list, and renders page_navigate correctly. --- opendia-extension/src/content/content.js | 593 +----------------- opendia-mcp/server.js | 733 ----------------------- 2 files changed, 5 insertions(+), 1321 deletions(-) diff --git a/opendia-extension/src/content/content.js b/opendia-extension/src/content/content.js index de36bd9..59bab51 100644 --- a/opendia-extension/src/content/content.js +++ b/opendia-extension/src/content/content.js @@ -12,122 +12,6 @@ if (typeof window.OpenDiaContentScriptLoaded !== 'undefined') { console.log("OpenDia enhanced content script loaded"); -// 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]", - ], - password: ["[type='password']", "[name*='password' i]"], - 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]", - ], - submit: ["[href*='signup']", ".signup-btn", "[aria-label*='register' i]"], - confidence: 0.85, - }, - }, - - // 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: [ - "[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, - }, - }, - - // 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, - }, - }, -}; - // Anti-Detection Platform Configuration const ANTI_DETECTION_PLATFORMS = { "twitter.com": { @@ -161,57 +45,6 @@ const ANTI_DETECTION_PLATFORMS = { }, }; -// 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, - }, - search: { - input: - "[data-testid='SearchBox_Search_Input'], input[placeholder*='search' i]", - submit: "[data-testid='SearchBox_Search_Button']", - confidence: 0.9, - }, - }, - }, - github: { - domains: ["github.com"], - patterns: { - search: { - input: "input[placeholder*='Search' i].form-control", - submit: "button[type='submit']", - confidence: 0.85, - }, - }, - }, - universal: { - search: { - selectors: [ - "input[type='search']", - "input[placeholder*='search' i]", - "[role='searchbox']", - "input[name*='search' i]", - ], - confidence: 0.6, - }, - submit: { - selectors: [ - "button[type='submit']:not([disabled])", - "input[type='submit']:not([disabled])", - "[role='button'][aria-label*='submit' i]", - ], - confidence: 0.65, - }, - }, -}; - class BrowserAutomation { constructor() { @@ -610,10 +443,9 @@ class BrowserAutomation { element_ids, max_results = 5, }) { - // Two-phase approach - if (phase === "discover") { - return await this.quickDiscovery({ intent_hint, max_results }); - } else if (phase === "detailed") { + // The phase enum is discover|detailed on both sides of the wire, so the + // dispatch is total. + if (phase === "detailed") { return await this.detailedAnalysis({ intent_hint, focus_areas, @@ -621,13 +453,7 @@ class BrowserAutomation { 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.quickDiscovery({ intent_hint, max_results }); } async quickDiscovery({ intent_hint, max_results = 5 }) { @@ -665,22 +491,8 @@ class BrowserAutomation { } } - // 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"; - } - } } catch (error) { - console.warn("Enhanced patterns failed:", error); + console.warn("Anti-detection pattern matching failed:", error); } // If no pattern matches, do a quick viewport scan @@ -823,386 +635,6 @@ class BrowserAutomation { 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 - ); - } - } catch (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 - ); - } - } catch (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); - } - - 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", - category, - 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("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("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"]; - } - - // Check for navigation patterns - 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("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("input") || - intentLower.includes("field") || - intentLower.includes("text") - ) { - return ["content", "post_create"]; - } - - // Default fallback - 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; - - for (const selector of selectors) { - const element = document.querySelector(selector); - if (element && this.isLikelyVisible(element)) { - const elementId = this.registerElement(element); - elements.push({ - id: elementId, - type: elementType, - selector: selector, - name: this.getElementName(element), - confidence: pattern.confidence || 0.8, - 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") - ) { - selectors = [ - "[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'])", - ]; - } - // Authentication patterns - else if (intentLower.includes("login") || intentLower.includes("sign in")) { - selectors = [ - "[type='email']", - "[name*='username' i]", - "[placeholder*='email' i]", - "[placeholder*='username' i]", - "input[name*='login' i]", - ]; - } else if ( - intentLower.includes("signup") || - intentLower.includes("register") - ) { - selectors = [ - "[href*='signup']", - ".signup-btn", - "[aria-label*='register' i]", - "button[data-testid*='signup' i]", - "a[href*='register']", - ]; - } - // Search patterns - else if (intentLower.includes("search") || intentLower.includes("find")) { - selectors = [ - "[data-testid='SearchBox_Search_Input']", // Twitter search first - "[type='search']", - "[role='searchbox']", - "[placeholder*='search' i]", - "[data-testid*='search' i]", - "input[name*='search' i]", - ]; - } - // Generic fallback - look for interactive elements - else { - selectors = [ - "button:not([disabled])", - "[contenteditable='true']", - "textarea", - "[type='submit']", - "[role='button']", - "input[type='text']", - ]; - } - - const elements = []; - - for (const selector of selectors) { - const foundElements = document.querySelectorAll(selector); - for (const element of foundElements) { - if (this.isLikelyVisible(element)) { - const elementId = this.registerElement(element); - elements.push({ - id: elementId, - type: this.inferElementType(element, intent_hint), - selector: selector, - name: this.getElementName(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", - }; - } - - async tryPatternDatabase(intentHint) { - const hostname = window.location.hostname; - const siteKey = this.detectSite(hostname); - - 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; - - const element = document.querySelector(selector); - if (element) { - const elementId = this.registerElement(element); - elements.push({ - id: elementId, - type: elementType, - selector: selector, - name: this.getElementName(element), - confidence: pattern.confidence || 0.8, - }); - } - } - - return { - elements, - confidence: pattern.confidence || 0.8, - 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}`) - ) - ) { - return siteKey; - } - } - 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); - if (element) { - const elementId = this.registerElement(element); - elements.push({ - id: elementId, - type: intentHint, - selector: selector, - name: this.getElementName(element), - confidence: pattern.confidence, - }); - break; // Take first match for universal patterns - } - } - - return { - elements, - confidence: pattern.confidence, - site: "universal", - }; - } - - async trySemanticAnalysis(intentHint, focusArea) { - const relevantElements = document.querySelectorAll(` - button, input, select, textarea, a[href], - [role="button"], [role="textbox"], [role="searchbox"], - [aria-label], [data-testid] - `); - - const elements = Array.from(relevantElements) - .filter((el) => this.isVisible(el)) - .slice(0, 20) - .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), - }; - }) - .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, - }; - } - 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(); @@ -1909,13 +1341,6 @@ class BrowserAutomation { ); } - isVisible(element) { - return ( - element.offsetParent !== null && - getComputedStyle(element).visibility !== "hidden" && - getComputedStyle(element).opacity !== "0" - ); - } generateSelector(element) { if (element.id) return `#${element.id}`; @@ -1956,14 +1381,6 @@ class BrowserAutomation { return Math.min(confidence, 1.0); } - formatAnalysisResult(result, method, startTime) { - return { - ...result, - method, - execution_time: Math.round(performance.now() - startTime), - analyzed_at: new Date().toISOString(), - }; - } // Two-phase utility methods compressElement(element, isQuick = false) { diff --git a/opendia-mcp/server.js b/opendia-mcp/server.js index f3eccad..df5b1ee 100755 --- a/opendia-mcp/server.js +++ b/opendia-mcp/server.js @@ -375,140 +375,7 @@ async function handleMCPRequest(request) { result = { resources: [] }; break; - case "prompts/list": - // Return available workflow prompts - result = { - prompts: [ - { - name: "post_to_social", - description: "Post content to social media platforms with anti-detection bypass", - arguments: [ - { - name: "content", - description: "The content to post", - required: true - }, - { - name: "platform", - description: "Target platform (twitter, linkedin, facebook)", - required: false - } - ] - }, - { - name: "post_selected_quote", - description: "Post currently selected text as a quote with commentary", - arguments: [ - { - name: "commentary", - description: "Your commentary on the selected text", - required: false - } - ] - }, - { - name: "research_workflow", - description: "Research a topic using current page and bookmarking findings", - arguments: [ - { - name: "topic", - description: "Research topic or query", - required: true - }, - { - name: "depth", - description: "Research depth: quick, thorough, comprehensive", - required: false - } - ] - }, - { - name: "analyze_browsing_session", - description: "Analyze current browsing session and provide insights", - arguments: [ - { - name: "focus", - description: "Analysis focus: productivity, research, trends", - required: false - } - ] - }, - { - name: "organize_tabs", - description: "Organize and clean up browser tabs intelligently", - arguments: [ - { - name: "strategy", - description: "Organization strategy: close_duplicates, group_by_domain, archive_old", - required: false - } - ] - }, - { - name: "fill_form_assistant", - description: "Analyze and help fill out forms on the current page", - arguments: [ - { - name: "form_type", - description: "Type of form: contact, registration, survey, application", - required: false - } - ] - } - ] - }; - break; - case "prompts/get": - // Execute specific workflow based on prompt name - const promptName = params.name; - const promptArgs = params.arguments || {}; - - try { - let workflowResult; - switch (promptName) { - case "post_to_social": - workflowResult = await executePostToSocialWorkflow(promptArgs); - break; - case "post_selected_quote": - workflowResult = await executePostSelectedQuoteWorkflow(promptArgs); - break; - case "research_workflow": - workflowResult = await executeResearchWorkflow(promptArgs); - break; - case "analyze_browsing_session": - workflowResult = await executeSessionAnalysisWorkflow(promptArgs); - break; - case "organize_tabs": - workflowResult = await executeOrganizeTabsWorkflow(promptArgs); - break; - case "fill_form_assistant": - workflowResult = await executeFillFormWorkflow(promptArgs); - break; - default: - throw new Error(`Unknown prompt: ${promptName}`); - } - - result = { - content: [ - { - type: "text", - text: workflowResult - } - ] - }; - } catch (error) { - result = { - content: [ - { - type: "text", - text: `❌ Workflow execution failed: ${error.message}` - } - ], - isError: true - }; - } - break; default: throw new Error(`Unknown method: ${method}`); @@ -2078,606 +1945,6 @@ process.on('SIGINT', async () => { process.exit(); }); -// Workflow execution functions -async function executePostToSocialWorkflow(args) { - const { content, platform = "auto" } = args; - - if (!content) { - throw new Error("Content is required for social media posting"); - } - - try { - // Analyze the current page to determine platform and find posting elements - const pageAnalysis = await callBrowserTool('page_analyze', { - intent_hint: 'post_create', - phase: 'discover', - max_results: 3 - }); - - if (!pageAnalysis.elements || pageAnalysis.elements.length === 0) { - throw new Error("No posting elements found on current page. Please navigate to a social media platform."); - } - - // Find the best textarea element for posting - const textareaElement = pageAnalysis.elements.find(el => - el.type === 'textarea' || el.name.toLowerCase().includes('post') || el.name.toLowerCase().includes('tweet') - ); - - if (!textareaElement) { - throw new Error("No suitable posting textarea found on current page"); - } - - // Fill the content using anti-detection bypass - const fillResult = await callBrowserTool('element_fill', { - element_id: textareaElement.id, - value: content, - clear_first: true - }); - - if (!fillResult.success) { - throw new Error(`Failed to fill content: ${fillResult.actual_value}`); - } - - // Look for submit button - const submitElement = pageAnalysis.elements.find(el => - el.type === 'button' && (el.name.toLowerCase().includes('post') || el.name.toLowerCase().includes('tweet') || el.name.toLowerCase().includes('share')) - ); - - let result = `✅ Successfully posted content to social media!\n\n`; - result += `📝 **Content posted:** "${content}"\n`; - result += `🎯 **Platform detected:** ${pageAnalysis.summary?.anti_detection_platform || 'Generic'}\n`; - result += `🔧 **Method used:** ${fillResult.method}\n`; - result += `📊 **Fill success:** ${fillResult.success ? 'Yes' : 'No'}\n`; - - if (submitElement) { - result += `\n💡 **Next step:** Click the "${submitElement.name}" button to publish your post.`; - } else { - result += `\n💡 **Next step:** Look for a "Post" or "Tweet" button to publish your content.`; - } - - return result; - - } catch (error) { - throw new Error(`Social media posting failed: ${error.message}`); - } -} - -async function executePostSelectedQuoteWorkflow(args) { - const { commentary = "" } = args; - - try { - // Get selected text from current page - const selectedText = await callBrowserTool('get_selected_text', { - include_metadata: true, - max_length: 1000 - }); - - if (!selectedText.has_selection) { - throw new Error("No text is currently selected. Please select some text first."); - } - - // Format the quote with commentary - let quoteContent = `"${selectedText.selected_text}"`; - if (selectedText.selection_metadata?.page_info?.title) { - quoteContent += `\n\n— ${selectedText.selection_metadata.page_info.title}`; - } - if (selectedText.selection_metadata?.page_info?.url) { - quoteContent += `\n${selectedText.selection_metadata.page_info.url}`; - } - if (commentary) { - quoteContent += `\n\n${commentary}`; - } - - // Execute the post workflow with the formatted quote - const postResult = await executePostToSocialWorkflow({ content: quoteContent }); - - let result = `🎯 **Selected Quote Posting Workflow**\n\n`; - result += `📝 **Selected text:** "${selectedText.selected_text.substring(0, 100)}${selectedText.selected_text.length > 100 ? '...' : ''}"\n`; - result += `📄 **Source:** ${selectedText.selection_metadata?.page_info?.title || 'Current page'}\n`; - result += `💬 **Commentary:** ${commentary || 'None'}\n`; - result += `📊 **Character count:** ${quoteContent.length}\n\n`; - result += postResult; - - return result; - - } catch (error) { - throw new Error(`Quote posting workflow failed: ${error.message}`); - } -} - -async function executeResearchWorkflow(args) { - const { topic, depth = "thorough" } = args; - - if (!topic) { - throw new Error("Research topic is required"); - } - - try { - // Analyze current page content - const pageContent = await callBrowserTool('page_extract_content', { - content_type: 'article', - summarize: true - }); - - // Get current page links for related research - const pageLinks = await callBrowserTool('get_page_links', { - include_internal: true, - include_external: true, - max_results: 20 - }); - - // Search browsing history for related content - const historyResults = await callBrowserTool('get_history', { - keywords: topic, - max_results: 10, - sort_by: 'visit_time' - }); - - // Bookmark current page if it has relevant content - URL will be obtained from browser extension - const currentUrl = pageContent.content?.url; - const currentTitle = pageContent.summary?.title; - - let bookmarkStatus = 'Page not bookmarked (no URL or title available)'; - if (currentUrl && currentTitle) { - try { - await callBrowserTool('add_bookmark', { - title: `[Research: ${topic}] ${currentTitle}`, - url: currentUrl - }); - bookmarkStatus = '✅ **Current page bookmarked for reference**'; - } catch (bookmarkError) { - bookmarkStatus = `⚠️ Bookmark failed: ${bookmarkError.message}`; - } - } - - // Compile research summary - let result = `🔍 **Research Workflow: ${topic}**\n\n`; - - // Current page analysis - result += `📄 **Current Page Analysis:**\n`; - if (pageContent.summary) { - result += `• **Title:** ${pageContent.summary.title || 'N/A'}\n`; - result += `• **Word count:** ${pageContent.summary.word_count || 0}\n`; - result += `• **Reading time:** ${pageContent.summary.reading_time || 0} minutes\n`; - result += `• **Has media:** ${pageContent.summary.has_images || pageContent.summary.has_videos ? 'Yes' : 'No'}\n`; - if (pageContent.summary.preview) { - result += `• **Preview:** ${pageContent.summary.preview}\n`; - } - } - - // Related links - result += `\n🔗 **Related Links Found:** ${pageLinks.returned}\n`; - const relevantLinks = pageLinks.links.filter(link => - link.text.toLowerCase().includes(topic.toLowerCase()) || - link.url.toLowerCase().includes(topic.toLowerCase()) - ).slice(0, 5); - - if (relevantLinks.length > 0) { - result += `**Top relevant links:**\n`; - relevantLinks.forEach((link, index) => { - result += `${index + 1}. [${link.text}](${link.url})\n`; - }); - } - - // History analysis - result += `\n📚 **Previous Research:**\n`; - if (historyResults.history_items && historyResults.history_items.length > 0) { - result += `Found ${historyResults.history_items.length} related pages in your history:\n`; - historyResults.history_items.slice(0, 5).forEach((item, index) => { - result += `${index + 1}. **${item.title}** (visited ${item.visit_count} times)\n`; - result += ` ${item.url}\n`; - }); - } else { - result += `No previous research found in browsing history.\n`; - } - - // Research recommendations - result += `\n💡 **Next Steps:**\n`; - if (depth === "comprehensive") { - result += `• Explore the ${pageLinks.returned} links found on current page\n`; - result += `• Cross-reference with ${historyResults.metadata?.total_found || 0} historical visits\n`; - result += `• Consider bookmarking additional relevant pages\n`; - } else if (depth === "thorough") { - result += `• Review top ${Math.min(5, pageLinks.returned)} most relevant links\n`; - result += `• Check recent history for related content\n`; - } else { - result += `• Focus on current page content and top 3 related links\n`; - } - - result += `\n${bookmarkStatus}`; - - return result; - - } catch (error) { - throw new Error(`Research workflow failed: ${error.message}`); - } -} - -async function executeSessionAnalysisWorkflow(args) { - const { focus = "productivity" } = args; - - try { - // Get all open tabs - const tabList = await callBrowserTool('tab_list', { - current_window_only: false, - include_details: true - }); - - // Get recent browsing history - const recentHistory = await callBrowserTool('get_history', { - max_results: 50, - sort_by: 'visit_time', - sort_order: 'desc' - }); - - // Analyze current page - const currentPageContent = await callBrowserTool('page_extract_content', { - content_type: 'article', - summarize: true - }); - - // Process tabs data - const tabs = tabList.tabs || []; - const domains = [...new Set(tabs.map(tab => { - try { - return new URL(tab.url).hostname; - } catch { - return 'unknown'; - } - }))]; - - // Categorize tabs by domain type - const socialMediaDomains = ['twitter.com', 'x.com', 'linkedin.com', 'facebook.com', 'instagram.com']; - const productivityDomains = ['docs.google.com', 'notion.so', 'obsidian.md', 'github.com']; - const newsDomains = ['news.google.com', 'bbc.com', 'cnn.com', 'reuters.com']; - - const categorizedTabs = { - social: tabs.filter(tab => socialMediaDomains.some(domain => tab.url.includes(domain))), - productivity: tabs.filter(tab => productivityDomains.some(domain => tab.url.includes(domain))), - news: tabs.filter(tab => newsDomains.some(domain => tab.url.includes(domain))), - other: tabs.filter(tab => - !socialMediaDomains.some(domain => tab.url.includes(domain)) && - !productivityDomains.some(domain => tab.url.includes(domain)) && - !newsDomains.some(domain => tab.url.includes(domain)) - ) - }; - - // Compile analysis - let result = `📊 **Browsing Session Analysis**\n\n`; - - // Session overview - result += `🎯 **Session Overview:**\n`; - result += `• **Total open tabs:** ${tabs.length}\n`; - result += `• **Unique domains:** ${domains.length}\n`; - result += `• **Active tab:** ${tabList.active_tab ? 'Yes' : 'No'}\n`; - result += `• **Recent history items:** ${recentHistory.metadata?.total_found || 0}\n`; - - // Tab categorization - result += `\n📂 **Tab Categories:**\n`; - result += `• **Social Media:** ${categorizedTabs.social.length} tabs\n`; - result += `• **Productivity:** ${categorizedTabs.productivity.length} tabs\n`; - result += `• **News/Information:** ${categorizedTabs.news.length} tabs\n`; - result += `• **Other:** ${categorizedTabs.other.length} tabs\n`; - - // Domain analysis - result += `\n🌐 **Top Domains:**\n`; - const domainCounts = {}; - tabs.forEach(tab => { - try { - const domain = new URL(tab.url).hostname; - domainCounts[domain] = (domainCounts[domain] || 0) + 1; - } catch {} - }); - - Object.entries(domainCounts) - .sort(([,a], [,b]) => b - a) - .slice(0, 5) - .forEach(([domain, count]) => { - result += `• **${domain}:** ${count} tab${count > 1 ? 's' : ''}\n`; - }); - - // Focus-specific analysis - if (focus === "productivity") { - result += `\n💼 **Productivity Analysis:**\n`; - const duplicateTabs = tabs.filter((tab, index) => - tabs.findIndex(t => t.url === tab.url) !== index - ); - result += `• **Duplicate tabs:** ${duplicateTabs.length}\n`; - result += `• **Productivity tools:** ${categorizedTabs.productivity.length}\n`; - result += `• **Social media distractions:** ${categorizedTabs.social.length}\n`; - - if (categorizedTabs.productivity.length > 0) { - result += `\n**Active productivity tools:**\n`; - categorizedTabs.productivity.slice(0, 3).forEach(tab => { - result += `• ${tab.title}\n`; - }); - } - } else if (focus === "research") { - result += `\n🔍 **Research Analysis:**\n`; - result += `• **Information sources:** ${categorizedTabs.news.length + categorizedTabs.other.length}\n`; - result += `• **Research depth:** ${recentHistory.metadata?.total_found > 20 ? 'Deep' : 'Surface'}\n`; - - if (currentPageContent.summary) { - result += `• **Current page type:** ${currentPageContent.content_type || 'Unknown'}\n`; - result += `• **Reading time:** ${currentPageContent.summary.reading_time || 0} minutes\n`; - } - } - - // Recommendations - result += `\n💡 **Recommendations:**\n`; - if (tabs.length > 20) { - result += `• Consider closing some tabs to improve performance\n`; - } - if (domainCounts['twitter.com'] > 3 || domainCounts['x.com'] > 3) { - result += `• Multiple social media tabs detected - consider consolidating\n`; - } - if (categorizedTabs.productivity.length > 0 && categorizedTabs.social.length > 0) { - result += `• Mix of productivity and social tabs - consider separate browsing sessions\n`; - } - - result += `\n📈 **Session Score:** ${Math.round(((categorizedTabs.productivity.length + categorizedTabs.news.length) / tabs.length) * 100)}% productive`; - - return result; - - } catch (error) { - throw new Error(`Session analysis workflow failed: ${error.message}`); - } -} - -async function executeOrganizeTabsWorkflow(args) { - const { strategy = "close_duplicates" } = args; - - try { - // Get all open tabs - const tabList = await callBrowserTool('tab_list', { - current_window_only: false, - include_details: true - }); - - const tabs = tabList.tabs || []; - let result = `🗂️ **Tab Organization Workflow**\n\n`; - result += `📊 **Starting with ${tabs.length} tabs**\n\n`; - - let closedTabs = []; - - if (strategy === "close_duplicates") { - // Find and close duplicate tabs - const seenUrls = new Set(); - const duplicates = []; - - tabs.forEach(tab => { - if (seenUrls.has(tab.url)) { - duplicates.push(tab); - } else { - seenUrls.add(tab.url); - } - }); - - // Close duplicate tabs (keep the first occurrence) - if (duplicates.length > 0) { - const tabIds = duplicates.map(tab => tab.id); - const closeResult = await callBrowserTool('tab_close', { - tab_ids: tabIds - }); - - if (closeResult.success) { - closedTabs = duplicates; - result += `✅ **Closed ${duplicates.length} duplicate tabs:**\n`; - duplicates.forEach(tab => { - result += `• ${tab.title}\n`; - }); - } - } else { - result += `✅ **No duplicate tabs found**\n`; - } - - } else if (strategy === "group_by_domain") { - // Group tabs by domain - const domainGroups = {}; - tabs.forEach(tab => { - try { - const domain = new URL(tab.url).hostname; - if (!domainGroups[domain]) { - domainGroups[domain] = []; - } - domainGroups[domain].push(tab); - } catch { - if (!domainGroups['unknown']) { - domainGroups['unknown'] = []; - } - domainGroups['unknown'].push(tab); - } - }); - - result += `📂 **Grouped tabs by domain:**\n`; - Object.entries(domainGroups).forEach(([domain, domainTabs]) => { - result += `• **${domain}:** ${domainTabs.length} tabs\n`; - domainTabs.slice(0, 3).forEach(tab => { - result += ` - ${tab.title}\n`; - }); - if (domainTabs.length > 3) { - result += ` - ... and ${domainTabs.length - 3} more\n`; - } - }); - - } else if (strategy === "archive_old") { - // Find tabs that haven't been active recently - // Since we don't have last accessed time, we'll use a heuristic - // based on tab loading status and position - const staleTabsToClose = tabs.filter(tab => - tab.status === 'complete' && - !tab.active && - !tab.pinned && - tab.index > 10 // Assume tabs at the end are less active - ).slice(0, 10); // Limit to 10 tabs max - - if (staleTabsToClose.length > 0) { - const tabIds = staleTabsToClose.map(tab => tab.id); - const closeResult = await callBrowserTool('tab_close', { - tab_ids: tabIds - }); - - if (closeResult.success) { - closedTabs = staleTabsToClose; - result += `✅ **Archived ${staleTabsToClose.length} old tabs:**\n`; - staleTabsToClose.forEach(tab => { - result += `• ${tab.title}\n`; - }); - } - } else { - result += `✅ **No old tabs to archive**\n`; - } - } - - // Final summary - const remainingTabs = tabs.length - closedTabs.length; - result += `\n📈 **Organization Results:**\n`; - result += `• **Tabs closed:** ${closedTabs.length}\n`; - result += `• **Tabs remaining:** ${remainingTabs}\n`; - result += `• **Organization strategy:** ${strategy}\n`; - - if (remainingTabs > 15) { - result += `\n💡 **Recommendation:** Consider running additional organization strategies to further reduce tab count.`; - } else { - result += `\n✅ **Tab organization complete!** Your browsing session is now more organized.`; - } - - return result; - - } catch (error) { - throw new Error(`Tab organization workflow failed: ${error.message}`); - } -} - -async function executeFillFormWorkflow(args) { - const { form_type = "auto" } = args; - - try { - // Analyze page for form elements - const formAnalysis = await callBrowserTool('page_analyze', { - intent_hint: 'form submit', - phase: 'detailed', - focus_areas: ['forms', 'buttons'], - max_results: 10 - }); - - if (!formAnalysis.elements || formAnalysis.elements.length === 0) { - throw new Error("No form elements found on current page"); - } - - // Categorize form elements - const formElements = { - inputs: formAnalysis.elements.filter(el => el.type === 'input'), - textareas: formAnalysis.elements.filter(el => el.type === 'textarea'), - selects: formAnalysis.elements.filter(el => el.type === 'select'), - buttons: formAnalysis.elements.filter(el => el.type === 'button') - }; - - // Analyze each form element for type and requirements - let result = `📝 **Form Analysis & Fill Assistant**\n\n`; - - // Form overview - result += `🔍 **Form Elements Found:**\n`; - result += `• **Input fields:** ${formElements.inputs.length}\n`; - result += `• **Text areas:** ${formElements.textareas.length}\n`; - result += `• **Select dropdowns:** ${formElements.selects.length}\n`; - result += `• **Buttons:** ${formElements.buttons.length}\n`; - - // Detailed element analysis - if (formElements.inputs.length > 0) { - result += `\n📊 **Input Field Analysis:**\n`; - formElements.inputs.forEach((input, index) => { - const elementState = formAnalysis.elements.find(el => el.id === input.id); - result += `${index + 1}. **${input.name}**\n`; - result += ` • Element ID: ${input.id}\n`; - result += ` • Ready: ${elementState?.ready ? 'Yes' : 'No'}\n`; - result += ` • Required: ${input.name.includes('*') ? 'Yes' : 'Unknown'}\n`; - - // Suggest field type based on name - const fieldName = input.name.toLowerCase(); - if (fieldName.includes('email')) { - result += ` • **Suggested type:** Email address\n`; - } else if (fieldName.includes('name')) { - result += ` • **Suggested type:** Name field\n`; - } else if (fieldName.includes('phone')) { - result += ` • **Suggested type:** Phone number\n`; - } else if (fieldName.includes('password')) { - result += ` • **Suggested type:** Password\n`; - } else { - result += ` • **Suggested type:** General text input\n`; - } - }); - } - - // Text area analysis - if (formElements.textareas.length > 0) { - result += `\n📝 **Text Area Analysis:**\n`; - formElements.textareas.forEach((textarea, index) => { - result += `${index + 1}. **${textarea.name}**\n`; - result += ` • Element ID: ${textarea.id}\n`; - result += ` • **Suggested use:** Long-form text input\n`; - }); - } - - // Submit buttons - if (formElements.buttons.length > 0) { - result += `\n🔘 **Submit Buttons:**\n`; - const submitButtons = formElements.buttons.filter(btn => - btn.name.toLowerCase().includes('submit') || - btn.name.toLowerCase().includes('send') || - btn.name.toLowerCase().includes('save') - ); - - submitButtons.forEach((button, index) => { - result += `${index + 1}. **${button.name}** (ID: ${button.id})\n`; - }); - } - - // Form type detection - result += `\n🎯 **Detected Form Type:**\n`; - // Note: Page content detection would need to be done through browser tools - const contentLower = ''; - - let detectedType = 'unknown'; - if (contentLower.includes('contact') || contentLower.includes('get in touch')) { - detectedType = 'contact'; - } else if (contentLower.includes('register') || contentLower.includes('sign up')) { - detectedType = 'registration'; - } else if (contentLower.includes('survey') || contentLower.includes('feedback')) { - detectedType = 'survey'; - } else if (contentLower.includes('application') || contentLower.includes('apply')) { - detectedType = 'application'; - } - - result += `• **Auto-detected:** ${detectedType}\n`; - result += `• **User specified:** ${form_type}\n`; - - // Filling recommendations - result += `\n💡 **Filling Recommendations:**\n`; - if (formElements.inputs.length > 0) { - result += `• Start with required fields (marked with *)\n`; - result += `• Use the element IDs provided for precise filling\n`; - result += `• Test form validation before final submission\n`; - } - - // Ready-to-fill elements - const readyElements = formAnalysis.elements.filter(el => el.ready); - result += `\n✅ **Ready to Fill:** ${readyElements.length} elements are ready for interaction\n`; - - if (readyElements.length > 0) { - result += `**Next steps:**\n`; - result += `1. Use element_fill with the provided Element IDs\n`; - result += `2. Fill required fields first\n`; - result += `3. Review form before submission\n`; - result += `4. Click appropriate submit button when ready\n`; - } - - return result; - - } catch (error) { - throw new Error(`Form analysis workflow failed: ${error.message}`); - } -} // Start the server. Without the catch, a throw in port resolution or tunnel // setup surfaces as an unhandled rejection instead of a readable message. From a8e6c924f92f587e1c2ba10c9f226286622c2a87 Mon Sep 17 00:00:00 2001 From: Aaron Elijah Mars <61592645+aaronjmars@users.noreply.github.com> Date: Tue, 21 Jul 2026 10:47:36 -0400 Subject: [PATCH 5/7] docs: record this pass's security fix, behavior fixes, and removals --- .github/CHANGELOG.md | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 24b09e9..b1546c2 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -26,8 +26,26 @@ itself rather than reconstructing it at tag time. Purely local setups (`npx opendia`, Claude Desktop over stdio, the browser extension) are unaffected and need no token. (#55) +- **The `prompts/*` workflow surface is gone.** `prompts/list`, `prompts/get` and + the six workflow prompts (`post_to_social`, `post_selected_quote`, + `research_workflow`, `analyze_browsing_session`, `organize_tabs`, `fill_form`) + have been removed. They were unreachable in practice: the server never declared + a `prompts` capability during `initialize`, and `prompts/get` replied with + `{ content: [...] }` where the spec requires `{ messages: [...] }`, so + spec-compliant clients never called them. Tools are unaffected. + ### Security +- **`--token=` with an empty value disabled authentication entirely.** The flag was + matched with `startsWith('--token=')`, so a bare `--token=` produced an empty + token, and the request guard treated an empty token as "no auth configured" and + waved every request through. The startup banner was gated the same way, so + nothing was printed — `--tunnel --token=` published an unauthenticated + browser-control endpoint with no visible sign, defeating the boundary added in + #55. `--http-host=` had the same bug and bound all interfaces. Empty flag values + are now a startup error, and invalid `--port` values fail with a readable message + instead of an unhandled rejection. + - Bind the extension control channel to loopback and gate its handshake. The WebSocket server listened on all interfaces, and because the connection handler hands the extension slot to whoever connects last, any reachable peer could evict @@ -44,6 +62,32 @@ itself rather than reconstructing it at tag time. stopgap — removal is tracked in #58. (#56) - Bump `node-forge` to 1.4.0 and `brace-expansion` to 1.1.15. (#43) +### Fixed + +- **Tools no longer report failures to the model as success.** `get_history` + rendered a browser API failure as "No history items found", `get_selected_text` + rendered four distinct failures as "No text selected", `page_navigate` claimed + success when its wait condition had timed out, and the research workflow claimed + it had bookmarked a page after the bookmark call failed. Each now surfaces the + real error. The genuine empty cases still report normally. +- **A dropped connection no longer executes tools anyway.** The extension's + connection helper resolved without waiting for the socket to open and swallowed + connection failures, so a tool call proceeded even when the server was + unreachable — `tab_close` and `element_click` ran against the browser while the + reply was written to a socket that was still connecting and discarded. The model + saw only a generic "Tool call timeout" and would reasonably retry a side effect. + Connections now resolve on open and fail loudly, and Chrome reuses a live socket + instead of replacing the one the request arrived on. + +### Removed + +- The enhanced-pattern matching tier in the content script. It had never executed: + its guard referenced an identifier declared nowhere in the codebase, so it threw + on every call and the failure was swallowed, silently degrading `page_analyze` to + a viewport scan. The legacy single-phase analysis engine went with it — reaching + it required a `phase` value that both tool schemas forbid. `page_analyze` output + is unchanged, because none of the removed code could run. + ### Maintenance - Dependency bumps: `ws` (#52), `web-ext` (#48), `actions/checkout` 4 → 7 (#46), From 6fe52942cad00a940a2c802201f311f8cbc16ccd Mon Sep 17 00:00:00 2001 From: Aaron Elijah Mars <61592645+aaronjmars@users.noreply.github.com> Date: Tue, 21 Jul 2026 10:51:47 -0400 Subject: [PATCH 6/7] =?UTF-8?q?optimize:=20quick=20wins=20=E2=80=94=20real?= =?UTF-8?q?=20CI=20gate,=20crash=20guards,=20dead=20halves,=20NaN?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit build.js: `case 'validate'` discarded validateAllBuilds()'s promise, so the CI "Validate builds" step passed regardless of the result. Now exits 1 on failure — verified by breaking dist/chrome/manifest.json and confirming a non-zero exit. navigateToUrl: destructured tabs.query() and used activeTab.id with no guard, so page_navigate crashed when no normal window has focus (devtools undocked, all windows minimized). Every other caller of the same query already guards; this one now does too. register frame: `availableTools = message.tools` trusted the peer, and any loopback client could send a register with no tools — leaving availableTools undefined and making every later tools/list throw until restart. Now validated as an array; verified tools/list survives a malformed frame. Divide-by-zero: summarizeResults, summarizePosts and calculateQualityScore divided by results.length/posts.length, which are 0 when an extractor matches nothing — shipping avg_score: NaN and "NaN%" to the model. Dead protocol halves: the background's `action: "test"` handler had no sender and the message it emitted had no server-side branch; the popup's `statusUpdate` listener had no sender (status comes from polling). setupViewportAnalyzer: built an IntersectionObserver that never observed anything and a visibilityMap that was never read — one dead allocation per page load. isLikelyVisible does this work. popup: said 17 tools and omitted page_style while the extension registers 18. Name list and count now derive from one array so they can't disagree; verified the list covers all 18 registered tools. logo.webm: referenced by build.js and popup.html but absent from the repo, so the copy was permanently skipped and the never matched. EXTENSION_ORIGIN: the same regex was inlined in the WebSocket handshake check; now uses the existing constant. Gate green: 0 errors, same 3 pre-existing web-ext warnings, 19 structure markers; live round trip unchanged. --- opendia-extension/build.js | 9 +++-- .../src/background/background.js | 11 ++++-- opendia-extension/src/content/content.js | 29 +++++--------- opendia-extension/src/popup/popup.html | 1 - opendia-extension/src/popup/popup.js | 38 ++++++++++--------- opendia-mcp/server.js | 8 +++- 6 files changed, 49 insertions(+), 47 deletions(-) diff --git a/opendia-extension/build.js b/opendia-extension/build.js index 084aa12..5198cd3 100644 --- a/opendia-extension/build.js +++ b/opendia-extension/build.js @@ -18,9 +18,6 @@ async function buildForBrowser(browser) { if (await fs.pathExists('logo.mp4')) { await fs.copy('logo.mp4', path.join(buildDir, 'logo.mp4')); } - if (await fs.pathExists('logo.webm')) { - await fs.copy('logo.webm', path.join(buildDir, 'logo.webm')); - } // Copy browser-specific manifest await fs.copy( @@ -180,7 +177,11 @@ if (require.main === module) { buildForBrowser('firefox'); break; case 'validate': - validateAllBuilds(); + // Exit non-zero on failure, or the CI "Validate builds" step passes + // whatever validateAllBuilds() reports. + validateAllBuilds().then((ok) => { + if (!ok) process.exit(1); + }); break; case 'package': buildAll().then(() => createPackages()); diff --git a/opendia-extension/src/background/background.js b/opendia-extension/src/background/background.js index d8d27d0..287455a 100644 --- a/opendia-extension/src/background/background.js +++ b/opendia-extension/src/background/background.js @@ -1179,7 +1179,13 @@ async function navigateToUrl(url, waitFor, timeout = 10000) { 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"); + } + await browser.tabs.update(activeTab.id, { url }); // If waitFor is specified, wait for the element to appear @@ -2033,9 +2039,6 @@ browser.runtime.onMessage.addListener((request, sender, sendResponse) => { safetyModeEnabled = request.enabled; console.log(`🛡️ Safety Mode ${safetyModeEnabled ? 'ENABLED' : 'DISABLED'}`); sendResponse({ success: true }); - } else if (request.action === "test") { - connectionManager.send({ type: "test", timestamp: Date.now() }); - sendResponse({ success: true }); } return true; // Keep the message channel open }); \ No newline at end of file diff --git a/opendia-extension/src/content/content.js b/opendia-extension/src/content/content.js index 59bab51..1e4708a 100644 --- a/opendia-extension/src/content/content.js +++ b/opendia-extension/src/content/content.js @@ -53,22 +53,6 @@ class BrowserAutomation { this.idCounter = 0; this.quickIdCounter = 0; 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, - }); - }); - }, - { threshold: [0, 0.1, 0.5, 1.0] } - ); } setupMessageListener() { @@ -829,8 +813,9 @@ class BrowserAutomation { total_results: results.length, 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, + avg_score: results.length + ? results.reduce((sum, r) => sum + (r.score || 0), 0) / results.length + : 0, has_sponsored: results.some((r) => r.type === "sponsored"), quality_score: this.calculateQualityScore(results), }; @@ -848,10 +833,10 @@ class BrowserAutomation { return { post_count: posts.length, - avg_length: Math.round(totalTextLength / posts.length), + avg_length: posts.length ? Math.round(totalTextLength / posts.length) : 0, has_media_count: posts.filter((p) => p.has_media).length, engagement_total: totalLikes, - avg_engagement: Math.round(totalLikes / posts.length), + avg_engagement: posts.length ? Math.round(totalLikes / posts.length) : 0, 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), @@ -1015,6 +1000,10 @@ class BrowserAutomation { } calculateQualityScore(results) { + // An extractor that matched nothing returns [], which used to make every + // term NaN and ship "NaN%" to the model. + if (results.length === 0) return 0; + const avgScore = results.reduce((sum, r) => sum + (r.score || 0), 0) / results.length; const hasLinks = results.filter((r) => r.link).length / results.length; diff --git a/opendia-extension/src/popup/popup.html b/opendia-extension/src/popup/popup.html index 6504a21..89989ad 100644 --- a/opendia-extension/src/popup/popup.html +++ b/opendia-extension/src/popup/popup.html @@ -316,7 +316,6 @@