From e40805801afd18079bbc61185735ecbaf786f005 Mon Sep 17 00:00:00 2001 From: Aaron Elijah Mars Date: Sat, 12 Jul 2025 21:04:26 +0200 Subject: [PATCH] fix port assignment + license --- LICENSE | 21 +++ README.md | 24 ++- opendia-extension/background.js | 55 +++++- opendia-extension/popup.html | 8 +- opendia-extension/popup.js | 20 ++- opendia-mcp/server.js | 295 +++++++++++++++++++++++++------- 6 files changed, 347 insertions(+), 76 deletions(-) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3098b74 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 OpenDia Team + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index cf360f3..631c7dc 100644 --- a/README.md +++ b/README.md @@ -33,12 +33,11 @@ OpenDia lets AI models control your browser automatically. **The key advantage? Works with **any Chromium-based browser**: - ✅ **Google Chrome** -- ✅ **Arc Browser** +- ✅ **Arc** - ✅ **Microsoft Edge** -- ✅ **Brave Browser** +- ✅ **Brave** - ✅ **Opera** -- ✅ **Vivaldi** -- ✅ **Any Chromium variant** +- ✅ **Any Chromium based browser** Perfect for **Cursor users** who want to automate their local testing and development workflows! @@ -99,9 +98,19 @@ Perfect for **Cursor users** who want to automate their local testing and develo ```bash npx opendia ``` -- Chrome extension: ws://localhost:3000 +- Chrome extension: ws://localhost:5555 (auto-discovery enabled) - Claude Desktop: stdio (existing config) -- Local SSE: http://localhost:3001/sse +- Local SSE: http://localhost:5556/sse + +### Port Configuration +```bash +# Use custom ports +npx opendia --port=6000 # Uses 6000 (WebSocket) + 6001 (HTTP) +npx opendia --ws-port=5555 --http-port=5556 # Specify individually + +# Handle port conflicts +npx opendia --kill-existing # Safely terminate existing OpenDia processes +``` ### Auto-Tunnel Mode ```bash @@ -232,8 +241,9 @@ cd opendia-mcp npm install npm start -# Load extension in your browser +# Load extension in your browser # Go to chrome://extensions/ → Developer mode → Load unpacked: ./opendia-extension +# Extension will auto-connect to server on localhost:5555 ``` ### Ways to Contribute diff --git a/opendia-extension/background.js b/opendia-extension/background.js index a5f4574..ae4c556 100644 --- a/opendia-extension/background.js +++ b/opendia-extension/background.js @@ -1,13 +1,45 @@ // MCP Server connection configuration -const MCP_SERVER_URL = 'ws://localhost:3000'; +let MCP_SERVER_URL = 'ws://localhost:5555'; // Default, will be auto-discovered let mcpSocket = null; let reconnectInterval = null; let reconnectAttempts = 0; +let lastKnownPorts = { websocket: 5555, http: 5556 }; // Cache for port discovery + +// Port discovery function +async function discoverServerPorts() { + // Try common HTTP ports to find the server + const commonPorts = [5556, 5557, 5558, 3001, 6001, 6002, 6003]; + + for (const httpPort of commonPorts) { + try { + const response = await fetch(`http://localhost:${httpPort}/ports`); + if (response.ok) { + const portInfo = await response.json(); + console.log('🔍 Discovered server ports:', portInfo); + lastKnownPorts = { websocket: portInfo.websocket, http: portInfo.http }; + MCP_SERVER_URL = portInfo.websocketUrl; + return portInfo; + } + } catch (error) { + // Port not available or not OpenDia server, continue searching + } + } + + // Fallback to default if discovery fails + console.log('⚠️ Port discovery failed, using defaults'); + return null; +} // Initialize WebSocket connection to MCP server -function connectToMCPServer() { +async function connectToMCPServer() { if (mcpSocket && mcpSocket.readyState === WebSocket.OPEN) return; + // Try port discovery if using default URL or if connection failed + if (MCP_SERVER_URL === 'ws://localhost:5555' || reconnectAttempts > 2) { + await discoverServerPorts(); + reconnectAttempts = 0; // Reset attempts after discovery + } + console.log('🔗 Connecting to MCP server at', MCP_SERVER_URL); mcpSocket = new WebSocket(MCP_SERVER_URL); @@ -32,12 +64,20 @@ function connectToMCPServer() { mcpSocket.onclose = () => { console.log('❌ Disconnected from MCP server, will reconnect...'); + reconnectAttempts++; + + // Clear any existing reconnect interval + if (reconnectInterval) { + clearInterval(reconnectInterval); + } + // Attempt to reconnect every 5 seconds reconnectInterval = setInterval(connectToMCPServer, 5000); }; mcpSocket.onerror = (error) => { console.log('⚠️ MCP WebSocket error:', error); + reconnectAttempts++; }; } @@ -1116,8 +1156,10 @@ async function getSelectedText(params) { } } -// Initialize connection when extension loads -connectToMCPServer(); +// Initialize connection when extension loads (with delay for server startup) +setTimeout(() => { + connectToMCPServer(); +}, 1000); // Heartbeat to keep connection alive setInterval(() => { @@ -1141,6 +1183,11 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { } else if (request.action === "reconnect") { connectToMCPServer(); sendResponse({ success: true }); + } else if (request.action === "getPorts") { + sendResponse({ + current: lastKnownPorts, + websocketUrl: MCP_SERVER_URL + }); } else if (request.action === "test") { if (mcpSocket && mcpSocket.readyState === WebSocket.OPEN) { mcpSocket.send(JSON.stringify({ type: "test", timestamp: Date.now() })); diff --git a/opendia-extension/popup.html b/opendia-extension/popup.html index 4afc4d7..7683cdc 100644 --- a/opendia-extension/popup.html +++ b/opendia-extension/popup.html @@ -255,9 +255,9 @@ Checking connection... - Make sure your MCP server is connected. - If it's the case, click on Reconnect. - If it still don't work, kill your 3000 port & try again. + Start server with: npx opendia + Auto-discovery will find the correct ports. + If issues persist, try: npx opendia --kill-existing @@ -265,7 +265,7 @@
Server - ws://localhost:3000 + Auto-Discovery
Available Tools diff --git a/opendia-extension/popup.js b/opendia-extension/popup.js index 99f2599..329b3e0 100644 --- a/opendia-extension/popup.js +++ b/opendia-extension/popup.js @@ -3,6 +3,7 @@ let statusIndicator = document.getElementById("statusIndicator"); let statusText = document.getElementById("statusText"); let toolCount = document.getElementById("toolCount"); let currentPage = document.getElementById("currentPage"); +let serverUrl = document.getElementById("serverUrl"); // Get dynamic tool count from background script function updateToolCount() { @@ -59,16 +60,31 @@ function checkStatus() { checkStatus(); setInterval(checkStatus, 2000); +// Update server URL display +function updateServerUrl() { + if (chrome.runtime?.id) { + chrome.runtime.sendMessage({ action: "getPorts" }, (response) => { + if (!chrome.runtime.lastError && response?.websocketUrl) { + serverUrl.textContent = response.websocketUrl; + } + }); + } +} + +// Update server URL periodically +updateServerUrl(); +setInterval(updateServerUrl, 5000); + // Update UI based on connection status function updateStatus(connected) { if (connected) { statusIndicator.className = "status-indicator connected"; statusText.innerHTML = `Connected to MCP server - Make sure your MCP server is connected. If it's the case, click on Reconnect. If it still don't work, kill your 3000 port & try again.`; + Connected successfully! Server auto-discovery is working. Default ports: WebSocket=5555, HTTP=5556`; } else { statusIndicator.className = "status-indicator disconnected"; statusText.innerHTML = `Disconnected from MCP server - Make sure your MCP server is connected. If it's the case, click on Reconnect. If it still don't work, kill your 3000 port & try again.`; + Start server with: npx opendia. Auto-discovery will find the correct ports. If issues persist, try: npx opendia --kill-existing`; } } diff --git a/opendia-mcp/server.js b/opendia-mcp/server.js index ba13830..834981b 100755 --- a/opendia-mcp/server.js +++ b/opendia-mcp/server.js @@ -2,24 +2,141 @@ const WebSocket = require("ws"); const express = require("express"); +const net = require('net'); +const { exec } = require('child_process'); // ADD: New imports for SSE transport const cors = require('cors'); const { createServer } = require('http'); const { spawn } = require('child_process'); -// ADD: Command line argument parsing +// ADD: Enhanced 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=')); +const httpPortArg = args.find(arg => arg.startsWith('--http-port=')); +const portArg = args.find(arg => arg.startsWith('--port=')); + +// Default ports (changed from 3000/3001 to 5555/5556) +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); + +// Port conflict detection utilities +async function checkPortInUse(port) { + return new Promise((resolve) => { + const server = net.createServer(); + server.listen(port, () => { + server.once('close', () => resolve(false)); + server.close(); + }); + server.on('error', () => resolve(true)); + }); +} + +async function checkIfOpenDiaProcess(port) { + return new Promise((resolve) => { + exec(`lsof -ti:${port}`, (error, stdout) => { + if (error || !stdout.trim()) { + resolve(false); + return; + } + + const pid = stdout.trim().split('\n')[0]; + exec(`ps -p ${pid} -o command=`, (psError, psOutput) => { + resolve(!psError && ( + psOutput.includes('opendia') || + psOutput.includes('server.js') || + psOutput.includes('node') && psOutput.includes('opendia') + )); + }); + }); + }); +} + +async function findAvailablePort(startPort) { + let port = startPort; + while (await checkPortInUse(port)) { + port++; + if (port > startPort + 100) { // Safety limit + throw new Error(`Could not find available port after checking ${port - startPort} ports`); + } + } + return port; +} + +async function killExistingOpenDia(port) { + return new Promise((resolve) => { + exec(`lsof -ti:${port}`, async (error, stdout) => { + if (error || !stdout.trim()) { + resolve(false); + return; + } + + const pids = stdout.trim().split('\n'); + let killedAny = false; + + for (const pid of pids) { + const isOpenDia = await checkIfOpenDiaProcess(port); + if (isOpenDia) { + exec(`kill ${pid}`, (killError) => { + if (!killError) { + console.error(`🔧 Killed existing OpenDia process (PID: ${pid})`); + killedAny = true; + } + }); + } + } + + // Wait a moment for processes to fully exit + setTimeout(() => resolve(killedAny), 1000); + }); + }); +} + +async function handlePortConflict(port, portName) { + const isInUse = await checkPortInUse(port); + + if (!isInUse) { + return port; // Port is free, use it + } + + // Port is busy - give user options + console.error(`⚠️ ${portName} port ${port} is already in use`); + + // Check if it's likely another OpenDia instance + const isOpenDia = await checkIfOpenDiaProcess(port); + + if (isOpenDia) { + console.error(`🔍 Detected existing OpenDia instance on port ${port}`); + console.error(`💡 Options:`); + console.error(` 1. Kill existing: npx opendia --kill-existing`); + console.error(` 2. Use different port: npx opendia --${portName.toLowerCase()}-port=${port + 1}`); + console.error(` 3. Check running processes: lsof -i:${port}`); + console.error(``); + console.error(`⏹️ Exiting to avoid conflicts...`); + process.exit(1); + } else { + // Something else is using the port - auto-increment + const altPort = await findAvailablePort(port + 1); + console.error(`🔄 ${portName} port ${port} busy (non-OpenDia), using port ${altPort}`); + if (portName === 'WebSocket') { + console.error(`💡 Update Chrome extension to: ws://localhost:${altPort}`); + } + return altPort; + } +} // ADD: Express app setup const app = express(); app.use(cors()); app.use(express.json()); -// WebSocket server for Chrome Extension -const wss = new WebSocket.Server({ port: 3000 }); +// WebSocket server for Chrome Extension (will be initialized after port conflict resolution) +let wss = null; let chromeExtensionSocket = null; let availableTools = []; @@ -1092,59 +1209,61 @@ function handleToolResponse(message) { } } -// Handle Chrome Extension connections -wss.on("connection", (ws) => { - console.error("Chrome Extension connected"); - chromeExtensionSocket = ws; +// Setup WebSocket connection handlers +function setupWebSocketHandlers() { + wss.on("connection", (ws) => { + console.error("Chrome Extension connected"); + chromeExtensionSocket = ws; - // Set up ping/pong for keepalive - const pingInterval = setInterval(() => { - if (ws.readyState === WebSocket.OPEN) { - ws.ping(); - } - }, 30000); - - ws.on("message", (data) => { - try { - const message = JSON.parse(data); - - if (message.type === "register") { - availableTools = message.tools; - console.error( - `✅ Registered ${availableTools.length} browser tools from extension` - ); - console.error( - `🎯 Enhanced tools with anti-detection bypass: ${availableTools - .map((t) => t.name) - .join(", ")}` - ); - } else if (message.type === "ping") { - // Respond to ping with pong - ws.send(JSON.stringify({ type: "pong", timestamp: Date.now() })); - } else if (message.id) { - // Handle tool response - handleToolResponse(message); + // Set up ping/pong for keepalive + const pingInterval = setInterval(() => { + if (ws.readyState === WebSocket.OPEN) { + ws.ping(); } - } catch (error) { - console.error("Error processing message:", error); - } - }); + }, 30000); - ws.on("close", () => { - console.error("Chrome Extension disconnected"); - chromeExtensionSocket = null; - availableTools = []; // Clear tools when extension disconnects - clearInterval(pingInterval); - }); + ws.on("message", (data) => { + try { + const message = JSON.parse(data); - ws.on("error", (error) => { - console.error("WebSocket error:", error); - }); + if (message.type === "register") { + availableTools = message.tools; + console.error( + `✅ Registered ${availableTools.length} browser tools from extension` + ); + console.error( + `🎯 Enhanced tools with anti-detection bypass: ${availableTools + .map((t) => t.name) + .join(", ")}` + ); + } else if (message.type === "ping") { + // Respond to ping with pong + ws.send(JSON.stringify({ type: "pong", timestamp: Date.now() })); + } else if (message.id) { + // Handle tool response + handleToolResponse(message); + } + } catch (error) { + console.error("Error processing message:", error); + } + }); - ws.on("pong", () => { - // Extension is alive + ws.on("close", () => { + console.error("Chrome Extension disconnected"); + chromeExtensionSocket = null; + availableTools = []; // Clear tools when extension disconnects + clearInterval(pingInterval); + }); + + ws.on("error", (error) => { + console.error("WebSocket error:", error); + }); + + ws.on("pong", () => { + // Extension is alive + }); }); -}); +} // ADD: SSE/HTTP endpoints for online AI app.route('/sse') @@ -1245,6 +1364,10 @@ app.get('/health', (req, res) => { availableTools: availableTools.length, transport: sseOnly ? 'sse-only' : 'hybrid', tunnelEnabled: enableTunnel, + ports: { + websocket: WS_PORT, + http: HTTP_PORT + }, features: [ 'Anti-detection bypass for Twitter/X, LinkedIn, Facebook', 'Two-phase intelligent page analysis', @@ -1256,14 +1379,59 @@ app.get('/health', (req, res) => { }); }); -// START: Enhanced server startup with optional tunneling +// ADD: Port discovery endpoint for Chrome extension +app.get('/ports', (req, res) => { + res.json({ + websocket: WS_PORT, + http: HTTP_PORT, + websocketUrl: `ws://localhost:${WS_PORT}`, + httpUrl: `http://localhost:${HTTP_PORT}`, + sseUrl: `http://localhost:${HTTP_PORT}/sse` + }); +}); + +// START: Enhanced 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}`); + + // Handle --kill-existing flag + if (killExisting) { + console.error('🔧 Killing existing OpenDia processes...'); + const wsKilled = await killExistingOpenDia(WS_PORT); + const httpKilled = await killExistingOpenDia(HTTP_PORT); + + if (wsKilled || httpKilled) { + console.error('✅ Existing processes terminated'); + // Wait for ports to be fully released + await new Promise(resolve => setTimeout(resolve, 2000)); + } else { + console.error('ℹ️ No existing OpenDia processes found'); + } + } + + // Resolve port conflicts + WS_PORT = await handlePortConflict(WS_PORT, 'WebSocket'); + HTTP_PORT = await handlePortConflict(HTTP_PORT, 'HTTP'); + + // Ensure HTTP port doesn't conflict with resolved WebSocket port + if (HTTP_PORT === WS_PORT) { + HTTP_PORT = await findAvailablePort(WS_PORT + 1); + console.error(`🔄 HTTP port adjusted to ${HTTP_PORT} to avoid WebSocket conflict`); + } + + // Initialize WebSocket server after port resolution + wss = new WebSocket.Server({ port: WS_PORT }); + + // Set up WebSocket connection handling + setupWebSocketHandlers(); + + console.error(`✅ Ports resolved: WebSocket=${WS_PORT}, HTTP=${HTTP_PORT}`); // Start HTTP server - const httpServer = app.listen(3001, () => { - console.error("🌐 HTTP/SSE server running on port 3001"); - console.error("🔌 Waiting for Chrome Extension connection on ws://localhost:3000"); + const httpServer = app.listen(HTTP_PORT, () => { + console.error(`🌐 HTTP/SSE server running on port ${HTTP_PORT}`); + console.error(`🔌 Chrome Extension connected on ws://localhost:${WS_PORT}`); console.error("🎯 Features: Anti-detection bypass + intelligent automation"); }); @@ -1273,7 +1441,7 @@ async function startServer() { console.error('🔄 Starting automatic tunnel...'); // Use the system ngrok binary directly - const ngrokProcess = spawn('ngrok', ['http', '3001', '--log', 'stdout'], { + const ngrokProcess = spawn('ngrok', ['http', HTTP_PORT, '--log', 'stdout'], { stdio: ['ignore', 'pipe', 'pipe'] }); @@ -1334,17 +1502,17 @@ async function startServer() { console.error('❌ Tunnel failed:', error.message); console.error(''); console.error('💡 MANUAL NGROK OPTION:'); - console.error(' 1. Run: ngrok http 3001'); + console.error(` 1. Run: ngrok http ${HTTP_PORT}`); console.error(' 2. Use the ngrok URL + /sse'); console.error(''); console.error('💡 Or use local URL:'); - console.error(' 🔗 http://localhost:3001/sse'); + console.error(` 🔗 http://localhost:${HTTP_PORT}/sse`); console.error(''); } } else { console.error(''); console.error('🏠 LOCAL MODE:'); - console.error('🔗 SSE endpoint: http://localhost:3001/sse'); + console.error(`🔗 SSE endpoint: http://localhost:${HTTP_PORT}/sse`); console.error('💡 For online AI access, restart with --tunnel flag'); console.error(''); } @@ -1352,12 +1520,21 @@ async function startServer() { // Display transport info if (sseOnly) { console.error('📡 Transport: SSE-only (stdio disabled)'); - console.error('💡 Configure Claude Desktop with: http://localhost:3001/sse'); + console.error(`💡 Configure Claude Desktop with: http://localhost:${HTTP_PORT}/sse`); } else { console.error('📡 Transport: Hybrid (stdio + SSE)'); console.error('💡 Claude Desktop: Works with existing config'); console.error('💡 Online AI: Use SSE endpoint above'); } + + // Display port configuration help + console.error(''); + console.error('🔧 Port Configuration:'); + console.error(` Current: WebSocket=${WS_PORT}, HTTP=${HTTP_PORT}`); + console.error(' Custom: npx opendia --ws-port=6000 --http-port=6001'); + console.error(' Or: npx opendia --port=6000 (uses 6000 and 6001)'); + console.error(' Kill existing: npx opendia --kill-existing'); + console.error(''); } // Cleanup on exit