fix port assignment + license

This commit is contained in:
Aaron Elijah Mars 2025-07-12 21:04:26 +02:00
parent c3c77c1e04
commit e40805801a
6 changed files with 347 additions and 76 deletions

21
LICENSE Normal file
View File

@ -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.

View File

@ -33,12 +33,11 @@ OpenDia lets AI models control your browser automatically. **The key advantage?
Works with **any Chromium-based browser**: Works with **any Chromium-based browser**:
- ✅ **Google Chrome** - ✅ **Google Chrome**
- ✅ **Arc Browser** - ✅ **Arc**
- ✅ **Microsoft Edge** - ✅ **Microsoft Edge**
- ✅ **Brave Browser** - ✅ **Brave**
- ✅ **Opera** - ✅ **Opera**
- ✅ **Vivaldi** - ✅ **Any Chromium based browser**
- ✅ **Any Chromium variant**
Perfect for **Cursor users** who want to automate their local testing and development workflows! 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 ```bash
npx opendia npx opendia
``` ```
- Chrome extension: ws://localhost:3000 - Chrome extension: ws://localhost:5555 (auto-discovery enabled)
- Claude Desktop: stdio (existing config) - 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 ### Auto-Tunnel Mode
```bash ```bash
@ -234,6 +243,7 @@ npm start
# Load extension in your browser # Load extension in your browser
# Go to chrome://extensions/ → Developer mode → Load unpacked: ./opendia-extension # Go to chrome://extensions/ → Developer mode → Load unpacked: ./opendia-extension
# Extension will auto-connect to server on localhost:5555
``` ```
### Ways to Contribute ### Ways to Contribute

View File

@ -1,13 +1,45 @@
// MCP Server connection configuration // 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 mcpSocket = null;
let reconnectInterval = null; let reconnectInterval = null;
let reconnectAttempts = 0; 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 // Initialize WebSocket connection to MCP server
function connectToMCPServer() { async function connectToMCPServer() {
if (mcpSocket && mcpSocket.readyState === WebSocket.OPEN) return; 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); console.log('🔗 Connecting to MCP server at', MCP_SERVER_URL);
mcpSocket = new WebSocket(MCP_SERVER_URL); mcpSocket = new WebSocket(MCP_SERVER_URL);
@ -32,12 +64,20 @@ function connectToMCPServer() {
mcpSocket.onclose = () => { mcpSocket.onclose = () => {
console.log('❌ Disconnected from MCP server, will reconnect...'); console.log('❌ Disconnected from MCP server, will reconnect...');
reconnectAttempts++;
// Clear any existing reconnect interval
if (reconnectInterval) {
clearInterval(reconnectInterval);
}
// Attempt to reconnect every 5 seconds // Attempt to reconnect every 5 seconds
reconnectInterval = setInterval(connectToMCPServer, 5000); reconnectInterval = setInterval(connectToMCPServer, 5000);
}; };
mcpSocket.onerror = (error) => { mcpSocket.onerror = (error) => {
console.log('⚠️ MCP WebSocket error:', error); console.log('⚠️ MCP WebSocket error:', error);
reconnectAttempts++;
}; };
} }
@ -1116,8 +1156,10 @@ async function getSelectedText(params) {
} }
} }
// Initialize connection when extension loads // Initialize connection when extension loads (with delay for server startup)
connectToMCPServer(); setTimeout(() => {
connectToMCPServer();
}, 1000);
// Heartbeat to keep connection alive // Heartbeat to keep connection alive
setInterval(() => { setInterval(() => {
@ -1141,6 +1183,11 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
} else if (request.action === "reconnect") { } else if (request.action === "reconnect") {
connectToMCPServer(); connectToMCPServer();
sendResponse({ success: true }); sendResponse({ success: true });
} else if (request.action === "getPorts") {
sendResponse({
current: lastKnownPorts,
websocketUrl: MCP_SERVER_URL
});
} else if (request.action === "test") { } else if (request.action === "test") {
if (mcpSocket && mcpSocket.readyState === WebSocket.OPEN) { if (mcpSocket && mcpSocket.readyState === WebSocket.OPEN) {
mcpSocket.send(JSON.stringify({ type: "test", timestamp: Date.now() })); mcpSocket.send(JSON.stringify({ type: "test", timestamp: Date.now() }));

View File

@ -255,9 +255,9 @@
<span id="statusText" class="tooltip"> <span id="statusText" class="tooltip">
Checking connection... Checking connection...
<span class="tooltip-content"> <span class="tooltip-content">
Make sure your MCP server is connected. Start server with: npx opendia
If it's the case, click on Reconnect. Auto-discovery will find the correct ports.
If it still don't work, kill your 3000 port & try again. If issues persist, try: npx opendia --kill-existing
</span> </span>
</span> </span>
</div> </div>
@ -265,7 +265,7 @@
<div class="info"> <div class="info">
<div class="info-row"> <div class="info-row">
<span class="info-label">Server</span> <span class="info-label">Server</span>
<span class="info-value" id="serverUrl">ws://localhost:3000</span> <span class="info-value" id="serverUrl">Auto-Discovery</span>
</div> </div>
<div class="info-row"> <div class="info-row">
<span class="info-label">Available Tools</span> <span class="info-label">Available Tools</span>

View File

@ -3,6 +3,7 @@ let statusIndicator = document.getElementById("statusIndicator");
let statusText = document.getElementById("statusText"); let statusText = document.getElementById("statusText");
let toolCount = document.getElementById("toolCount"); let toolCount = document.getElementById("toolCount");
let currentPage = document.getElementById("currentPage"); let currentPage = document.getElementById("currentPage");
let serverUrl = document.getElementById("serverUrl");
// Get dynamic tool count from background script // Get dynamic tool count from background script
function updateToolCount() { function updateToolCount() {
@ -59,16 +60,31 @@ function checkStatus() {
checkStatus(); checkStatus();
setInterval(checkStatus, 2000); 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 // Update UI based on connection status
function updateStatus(connected) { function updateStatus(connected) {
if (connected) { if (connected) {
statusIndicator.className = "status-indicator connected"; statusIndicator.className = "status-indicator connected";
statusText.innerHTML = `Connected to MCP server statusText.innerHTML = `Connected to MCP server
<span class="tooltip-content">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.</span>`; <span class="tooltip-content">Connected successfully! Server auto-discovery is working. Default ports: WebSocket=5555, HTTP=5556</span>`;
} else { } else {
statusIndicator.className = "status-indicator disconnected"; statusIndicator.className = "status-indicator disconnected";
statusText.innerHTML = `Disconnected from MCP server statusText.innerHTML = `Disconnected from MCP server
<span class="tooltip-content">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.</span>`; <span class="tooltip-content">Start server with: npx opendia. Auto-discovery will find the correct ports. If issues persist, try: npx opendia --kill-existing</span>`;
} }
} }

View File

@ -2,24 +2,141 @@
const WebSocket = require("ws"); const WebSocket = require("ws");
const express = require("express"); const express = require("express");
const net = require('net');
const { exec } = require('child_process');
// ADD: New imports for SSE transport // ADD: New imports for SSE transport
const cors = require('cors'); const cors = require('cors');
const { createServer } = require('http'); const { createServer } = require('http');
const { spawn } = require('child_process'); const { spawn } = require('child_process');
// ADD: Command line argument parsing // ADD: Enhanced command line argument parsing
const args = process.argv.slice(2); const args = process.argv.slice(2);
const enableTunnel = args.includes('--tunnel') || args.includes('--auto-tunnel'); const enableTunnel = args.includes('--tunnel') || args.includes('--auto-tunnel');
const sseOnly = args.includes('--sse-only'); 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 // ADD: Express app setup
const app = express(); const app = express();
app.use(cors()); app.use(cors());
app.use(express.json()); app.use(express.json());
// WebSocket server for Chrome Extension // WebSocket server for Chrome Extension (will be initialized after port conflict resolution)
const wss = new WebSocket.Server({ port: 3000 }); let wss = null;
let chromeExtensionSocket = null; let chromeExtensionSocket = null;
let availableTools = []; let availableTools = [];
@ -1092,59 +1209,61 @@ function handleToolResponse(message) {
} }
} }
// Handle Chrome Extension connections // Setup WebSocket connection handlers
wss.on("connection", (ws) => { function setupWebSocketHandlers() {
console.error("Chrome Extension connected"); wss.on("connection", (ws) => {
chromeExtensionSocket = ws; console.error("Chrome Extension connected");
chromeExtensionSocket = ws;
// Set up ping/pong for keepalive // Set up ping/pong for keepalive
const pingInterval = setInterval(() => { const pingInterval = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) { if (ws.readyState === WebSocket.OPEN) {
ws.ping(); 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);
} }
} catch (error) { }, 30000);
console.error("Error processing message:", error);
}
});
ws.on("close", () => { ws.on("message", (data) => {
console.error("Chrome Extension disconnected"); try {
chromeExtensionSocket = null; const message = JSON.parse(data);
availableTools = []; // Clear tools when extension disconnects
clearInterval(pingInterval);
});
ws.on("error", (error) => { if (message.type === "register") {
console.error("WebSocket error:", error); 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", () => { ws.on("close", () => {
// Extension is alive 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 // ADD: SSE/HTTP endpoints for online AI
app.route('/sse') app.route('/sse')
@ -1245,6 +1364,10 @@ app.get('/health', (req, res) => {
availableTools: availableTools.length, availableTools: availableTools.length,
transport: sseOnly ? 'sse-only' : 'hybrid', transport: sseOnly ? 'sse-only' : 'hybrid',
tunnelEnabled: enableTunnel, tunnelEnabled: enableTunnel,
ports: {
websocket: WS_PORT,
http: HTTP_PORT
},
features: [ features: [
'Anti-detection bypass for Twitter/X, LinkedIn, Facebook', 'Anti-detection bypass for Twitter/X, LinkedIn, Facebook',
'Two-phase intelligent page analysis', '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() { async function startServer() {
console.error("🚀 Enhanced Browser MCP Server with Anti-Detection Features"); 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 // Start HTTP server
const httpServer = app.listen(3001, () => { const httpServer = app.listen(HTTP_PORT, () => {
console.error("🌐 HTTP/SSE server running on port 3001"); console.error(`🌐 HTTP/SSE server running on port ${HTTP_PORT}`);
console.error("🔌 Waiting for Chrome Extension connection on ws://localhost:3000"); console.error(`🔌 Chrome Extension connected on ws://localhost:${WS_PORT}`);
console.error("🎯 Features: Anti-detection bypass + intelligent automation"); console.error("🎯 Features: Anti-detection bypass + intelligent automation");
}); });
@ -1273,7 +1441,7 @@ async function startServer() {
console.error('🔄 Starting automatic tunnel...'); console.error('🔄 Starting automatic tunnel...');
// Use the system ngrok binary directly // 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'] stdio: ['ignore', 'pipe', 'pipe']
}); });
@ -1334,17 +1502,17 @@ async function startServer() {
console.error('❌ Tunnel failed:', error.message); console.error('❌ Tunnel failed:', error.message);
console.error(''); console.error('');
console.error('💡 MANUAL NGROK OPTION:'); 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(' 2. Use the ngrok URL + /sse');
console.error(''); console.error('');
console.error('💡 Or use local URL:'); console.error('💡 Or use local URL:');
console.error(' 🔗 http://localhost:3001/sse'); console.error(` 🔗 http://localhost:${HTTP_PORT}/sse`);
console.error(''); console.error('');
} }
} else { } else {
console.error(''); console.error('');
console.error('🏠 LOCAL MODE:'); 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('💡 For online AI access, restart with --tunnel flag');
console.error(''); console.error('');
} }
@ -1352,12 +1520,21 @@ async function startServer() {
// Display transport info // Display transport info
if (sseOnly) { if (sseOnly) {
console.error('📡 Transport: SSE-only (stdio disabled)'); 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 { } else {
console.error('📡 Transport: Hybrid (stdio + SSE)'); console.error('📡 Transport: Hybrid (stdio + SSE)');
console.error('💡 Claude Desktop: Works with existing config'); console.error('💡 Claude Desktop: Works with existing config');
console.error('💡 Online AI: Use SSE endpoint above'); 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 // Cleanup on exit