Compare commits

...

3 Commits

Author SHA1 Message Date
Aaron Elijah Mars
f57010971a Bump version to 1.0.4
Updated version in package.json and manifest.json for release 1.0.4

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-12 21:18:09 +02:00
Aaron Elijah Mars
e40805801a fix port assignment + license 2025-07-12 21:04:26 +02:00
Aaron Elijah Mars
c3c77c1e04 hosted mode w/ Ngrok + SSE 2025-07-12 20:30:15 +02:00
9 changed files with 627 additions and 91 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**:
-**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!
@@ -93,6 +92,64 @@ Perfect for **Cursor users** who want to automate their local testing and develo
**For Cursor or other AI tools**, use the same configuration or follow their specific setup instructions.
## Usage Modes
### Local Mode (Default)
```bash
npx opendia
```
- Chrome extension: ws://localhost:5555 (auto-discovery enabled)
- Claude Desktop: stdio (existing config)
- 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
npx opendia --tunnel
```
- Automatically creates ngrok tunnel
- Copy URL for ChatGPT/online AI services
- Local functionality preserved
**Note**: For auto-tunneling to work, you need ngrok installed:
**macOS:**
```bash
brew install ngrok
```
**Windows:**
```bash
# Using Chocolatey
choco install ngrok
# Or download from https://ngrok.com/download
```
**Linux:**
```bash
# Ubuntu/Debian
curl -s https://ngrok-agent.s3.amazonaws.com/ngrok.asc | sudo tee /etc/apt/trusted.gpg.d/ngrok.asc >/dev/null
echo "deb https://ngrok-agent.s3.amazonaws.com buster main" | sudo tee /etc/apt/sources.list.d/ngrok.list
sudo apt update && sudo apt install ngrok
# Or download from https://ngrok.com/download
```
Then get your free authtoken from https://dashboard.ngrok.com/get-started/your-authtoken and run:
```bash
ngrok config add-authtoken YOUR_TOKEN_HERE
```
## 🛠️ Capabilities
OpenDia gives AI models **17 powerful browser tools**:
@@ -184,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

View File

@@ -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() }));

View File

@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "OpenDia",
"version": "1.0.0",
"version": "1.0.4",
"description": "Browser automation through Model Context Protocol",
"icons": {
"16": "icon-16.png",

View File

@@ -255,9 +255,9 @@
<span id="statusText" class="tooltip">
Checking connection...
<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.
Start server with: npx opendia
Auto-discovery will find the correct ports.
If issues persist, try: npx opendia --kill-existing
</span>
</span>
</div>
@@ -265,7 +265,7 @@
<div class="info">
<div class="info-row">
<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 class="info-row">
<span class="info-label">Available Tools</span>

View File

@@ -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
<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 {
statusIndicator.className = "status-indicator disconnected";
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

@@ -9,7 +9,8 @@
"version": "1.0.3",
"license": "MIT",
"dependencies": {
"express": "^4.19.2",
"cors": "^2.8.5",
"express": "^4.21.2",
"ws": "^8.18.0"
},
"bin": {
@@ -136,6 +137,19 @@
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
"license": "MIT"
},
"node_modules/cors": {
"version": "2.8.5",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
"license": "MIT",
"dependencies": {
"object-assign": "^4",
"vary": "^1"
},
"engines": {
"node": ">= 0.10"
}
},
"node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@@ -529,6 +543,15 @@
"node": ">= 0.6"
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/object-inspect": {
"version": "1.13.4",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "opendia",
"version": "1.0.3",
"version": "1.0.4",
"description": "🎯 OpenDia - The open alternative to Dia. Connect your browser to AI models with anti-detection bypass for Twitter/X, LinkedIn, Facebook",
"main": "server.js",
"bin": {
@@ -8,6 +8,9 @@
},
"scripts": {
"start": "node server.js",
"tunnel": "node server.js --tunnel",
"sse-only": "node server.js --sse-only",
"tunnel-sse": "node server.js --tunnel --sse-only",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
@@ -34,8 +37,9 @@
"url": "https://github.com/aaronjmars/opendia/issues"
},
"dependencies": {
"ws": "^8.18.0",
"express": "^4.19.2"
"cors": "^2.8.5",
"express": "^4.21.2",
"ws": "^8.18.0"
},
"engines": {
"node": ">=16.0.0"

View File

@@ -2,9 +2,141 @@
const WebSocket = require("ws");
const express = require("express");
const net = require('net');
const { exec } = require('child_process');
// WebSocket server for Chrome Extension
const wss = new WebSocket.Server({ port: 3000 });
// ADD: New imports for SSE transport
const cors = require('cors');
const { createServer } = require('http');
const { spawn } = require('child_process');
// 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 (will be initialized after port conflict resolution)
let wss = null;
let chromeExtensionSocket = null;
let availableTools = [];
@@ -1077,64 +1209,130 @@ 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();
}
}, 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) {
console.error("Error processing message:", error);
}
});
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')
.get((req, res) => {
// SSE stream for connection
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Cache-Control, Content-Type',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS'
});
res.write(`data: ${JSON.stringify({
type: 'connection',
status: 'connected',
server: 'OpenDia MCP Server',
version: '1.0.0'
})}\n\n`);
// Heartbeat to keep connection alive
const heartbeat = setInterval(() => {
res.write(`data: ${JSON.stringify({
type: 'heartbeat',
timestamp: Date.now()
})}\n\n`);
}, 30000);
req.on('close', () => {
clearInterval(heartbeat);
console.error('SSE client disconnected');
});
console.error('SSE client connected');
})
.post(async (req, res) => {
// MCP requests from online AI
console.error('MCP request received via SSE:', req.body);
try {
const result = await handleMCPRequest(req.body);
res.json({
jsonrpc: "2.0",
id: req.body.id,
result: result
});
} catch (error) {
console.error("Error processing message:", error);
res.status(500).json({
jsonrpc: "2.0",
id: req.body.id,
error: { code: -32603, message: error.message }
});
}
});
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: CORS preflight handler
app.options('*', (req, res) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type, Cache-Control');
res.sendStatus(200);
});
// Read from stdin
let inputBuffer = "";
process.stdin.on("data", async (chunk) => {
inputBuffer += chunk.toString();
if (!sseOnly) {
process.stdin.on("data", async (chunk) => {
inputBuffer += chunk.toString();
// Process complete lines
const lines = inputBuffer.split("\n");
@@ -1155,34 +1353,203 @@ process.stdin.on("data", async (chunk) => {
}
}
}
});
});
}
// Optional: HTTP endpoint for health checks
const app = express();
app.get("/health", (req, res) => {
// ADD: Health check endpoint (update existing one)
app.get('/health', (req, res) => {
res.json({
status: "ok",
status: 'ok',
chromeExtensionConnected: chromeExtensionSocket !== null,
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",
"Smart content extraction with summarization",
"Element state detection and interaction readiness",
"Performance analytics and token optimization",
],
'Anti-detection bypass for Twitter/X, LinkedIn, Facebook',
'Two-phase intelligent page analysis',
'Smart content extraction with summarization',
'Element state detection and interaction readiness',
'Performance analytics and token optimization',
'SSE transport for online AI services'
]
});
});
app.listen(3001, () => {
console.error("🎯 Enhanced Browser MCP Server with Anti-Detection Features");
console.error(
"Health check endpoint available at http://localhost:3001/health"
);
// 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`
});
});
console.error("🚀 Enhanced Browser MCP Server started");
console.error(
"🔌 Waiting for Chrome Extension connection on ws://localhost:3000"
);
console.error("🎯 Features: Anti-detection bypass + intelligent automation");
// 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(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");
});
// Auto-tunnel if requested
if (enableTunnel) {
try {
console.error('🔄 Starting automatic tunnel...');
// Use the system ngrok binary directly
const ngrokProcess = spawn('ngrok', ['http', HTTP_PORT, '--log', 'stdout'], {
stdio: ['ignore', 'pipe', 'pipe']
});
let tunnelUrl = null;
// Wait for tunnel URL
await new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
ngrokProcess.kill();
reject(new Error('Tunnel startup timeout'));
}, 10000);
ngrokProcess.stdout.on('data', (data) => {
const output = data.toString();
const match = output.match(/url=https:\/\/[^\s]+/);
if (match) {
tunnelUrl = match[0].replace('url=', '');
clearTimeout(timeout);
resolve();
}
});
ngrokProcess.stderr.on('data', (data) => {
const error = data.toString();
if (error.includes('error') || error.includes('failed')) {
clearTimeout(timeout);
ngrokProcess.kill();
reject(new Error(error.trim()));
}
});
ngrokProcess.on('error', (error) => {
clearTimeout(timeout);
reject(error);
});
});
if (tunnelUrl) {
console.error('');
console.error('🎉 OPENDIA READY!');
console.error('📋 Copy this URL for online AI services:');
console.error(`🔗 ${tunnelUrl}/sse`);
console.error('');
console.error('💡 ChatGPT: Settings → Connectors → Custom Connector');
console.error('💡 Claude Web: Add as external MCP server (if supported)');
console.error('');
console.error('🏠 Local access still available:');
console.error('🔗 http://localhost:3001/sse');
console.error('');
// Store ngrok process for cleanup
global.ngrokProcess = ngrokProcess;
} else {
throw new Error('Could not extract tunnel URL');
}
} catch (error) {
console.error('❌ Tunnel failed:', error.message);
console.error('');
console.error('💡 MANUAL NGROK OPTION:');
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:${HTTP_PORT}/sse`);
console.error('');
}
} else {
console.error('');
console.error('🏠 LOCAL MODE:');
console.error(`🔗 SSE endpoint: http://localhost:${HTTP_PORT}/sse`);
console.error('💡 For online AI access, restart with --tunnel flag');
console.error('');
}
// Display transport info
if (sseOnly) {
console.error('📡 Transport: SSE-only (stdio disabled)');
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
process.on('SIGINT', async () => {
console.error('🔄 Shutting down...');
if (enableTunnel && global.ngrokProcess) {
console.error('🔄 Closing tunnel...');
try {
global.ngrokProcess.kill('SIGTERM');
} catch (error) {
// Ignore cleanup errors
}
}
process.exit();
});
// Start the server
startServer();