Update to version 1.0.6 with Firefox support and improved extension management

- Add Firefox extension support with Manifest V2
- Update all version numbers to 1.0.6
- Build Chrome and Firefox extension packages
- Clean up extension directory structure
- Update README with Firefox installation instructions
- Update server messages to mention Chrome/Firefox support
- Add .gitignore for extension directory
- Create release packages for both browsers

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Aaron Elijah Mars 2025-07-16 22:57:39 +02:00
parent 99d31b15c8
commit 0b09201160
24 changed files with 5617 additions and 271 deletions

View File

@ -1,7 +1,7 @@
# OpenDia <img src="opendia-extension/icon-128.png" alt="OpenDia" width="32" height="32"> # OpenDia <img src="opendia-extension/icon-128.png" alt="OpenDia" width="32" height="32">
**The open alternative to Dia** **The open alternative to Dia**
Connect your browser to AI models. No browser switching needed—works seamlessly with any Chromium browser including Chrome & Arc. Connect your browser to AI models. No browser switching needed—works seamlessly with Chrome, Firefox, and any Chromium browser.
[![npm version](https://img.shields.io/npm/v/opendia)](https://www.npmjs.com/package/opendia) [![npm version](https://img.shields.io/npm/v/opendia)](https://www.npmjs.com/package/opendia)
[![GitHub release](https://img.shields.io/github/release/aaronjmars/opendia.svg)](https://github.com/aaronjmars/opendia/releases/latest) [![GitHub release](https://img.shields.io/github/release/aaronjmars/opendia.svg)](https://github.com/aaronjmars/opendia/releases/latest)
@ -31,8 +31,9 @@ OpenDia lets AI models control your browser automatically. **The key advantage?
## 🌐 Browser Support ## 🌐 Browser Support
Works with **any Chromium-based browser**: Works with **Chrome, Firefox, and any Chromium-based browser**:
- ✅ **Google Chrome** - ✅ **Mozilla Firefox** (Manifest V2)
- ✅ **Google Chrome** (Manifest V3)
- ✅ **Arc** - ✅ **Arc**
- ✅ **Microsoft Edge** - ✅ **Microsoft Edge**
- ✅ **Brave** - ✅ **Brave**
@ -79,10 +80,22 @@ Perfect for **Cursor users** who want to automate their local testing and develo
## ⚡ Quick Start ## ⚡ Quick Start
### 1. Install the Browser Extension ### 1. Install the Browser Extension
1. Download from [releases](https://github.com/aaronjmars/opendia/releases)
2. Go to `chrome://extensions/` (or your browser's extension page) **For Chrome/Chromium browsers:**
3. Enable "Developer mode" 1. Download `opendia-chrome-1.0.6.zip` from [releases](https://github.com/aaronjmars/opendia/releases)
4. Click "Load unpacked" and select the extension folder 2. Extract the zip file to a folder
3. Go to `chrome://extensions/` (or your browser's extension page)
4. Enable "Developer mode"
5. Click "Load unpacked" and select the extracted folder
**For Firefox:**
1. Download `opendia-firefox-1.0.6.zip` from [releases](https://github.com/aaronjmars/opendia/releases)
2. Extract the zip file to a folder
3. Go to `about:debugging#/runtime/this-firefox`
4. Click "Load Temporary Add-on..."
5. Select the `manifest.json` file from the extracted folder
> **Note**: Firefox extensions are loaded as temporary add-ons and will be removed when Firefox restarts. This is a Firefox limitation for unsigned extensions.
### 2. Connect to Your AI ### 2. Connect to Your AI
@ -272,7 +285,8 @@ npm install
npm start npm start
# Load extension in your browser # Load extension in your browser
# Go to chrome://extensions/ → Developer mode → Load unpacked: ./opendia-extension # Chrome: Go to chrome://extensions/ → Developer mode → Load unpacked: ./opendia-extension/dist/chrome
# Firefox: Go to about:debugging#/runtime/this-firefox → Load Temporary Add-on → ./opendia-extension/dist/firefox/manifest.json
# Extension will auto-connect to server on localhost:5555 # Extension will auto-connect to server on localhost:5555
``` ```

View File

@ -34,7 +34,7 @@ cp opendia-mcp/server.js dist/opendia-dxt/
cat > dist/opendia-dxt/package.json << 'EOF' cat > dist/opendia-dxt/package.json << 'EOF'
{ {
"name": "opendia", "name": "opendia",
"version": "1.0.5", "version": "1.0.6",
"description": "🎯 OpenDia - The open alternative to Dia. Connect your browser to AI models with anti-detection bypass for Twitter/X, LinkedIn, Facebook", "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", "main": "server.js",
"scripts": { "scripts": {
@ -47,6 +47,7 @@ cat > dist/opendia-dxt/package.json << 'EOF'
"ai", "ai",
"claude", "claude",
"chrome", "chrome",
"firefox",
"extension", "extension",
"twitter", "twitter",
"linkedin", "linkedin",
@ -115,7 +116,7 @@ cat > dist/opendia-dxt/manifest.json << 'EOF'
"dxt_version": "0.1", "dxt_version": "0.1",
"name": "opendia", "name": "opendia",
"display_name": "OpenDia - Browser Automation", "display_name": "OpenDia - Browser Automation",
"version": "1.0.5", "version": "1.0.6",
"description": "🎯 OpenDia - The open alternative to Dia. Connect your browser to AI models with anti-detection bypass for Twitter/X, LinkedIn, Facebook + universal automation", "description": "🎯 OpenDia - The open alternative to Dia. Connect your browser to AI models with anti-detection bypass for Twitter/X, LinkedIn, Facebook + universal automation",
"author": { "author": {
"name": "Aaron Elijah Mars", "name": "Aaron Elijah Mars",
@ -124,7 +125,7 @@ cat > dist/opendia-dxt/manifest.json << 'EOF'
}, },
"homepage": "https://github.com/aaronjmars/opendia", "homepage": "https://github.com/aaronjmars/opendia",
"license": "MIT", "license": "MIT",
"keywords": ["browser", "automation", "mcp", "ai", "claude", "chrome", "extension", "twitter", "linkedin", "facebook", "anti-detection"], "keywords": ["browser", "automation", "mcp", "ai", "claude", "chrome", "firefox", "extension", "twitter", "linkedin", "facebook", "anti-detection"],
"icon": "icon.png", "icon": "icon.png",
"icons": { "icons": {
"128": "icon.png" "128": "icon.png"
@ -150,7 +151,7 @@ cat > dist/opendia-dxt/manifest.json << 'EOF'
"ws_port": { "ws_port": {
"type": "number", "type": "number",
"title": "WebSocket Port", "title": "WebSocket Port",
"description": "Port for Chrome extension connection", "description": "Port for Chrome/Firefox extension connection",
"default": 5555, "default": 5555,
"minimum": 1024, "minimum": 1024,
"maximum": 65535 "maximum": 65535
@ -267,10 +268,10 @@ cat > dist/opendia-dxt/manifest.json << 'EOF'
}, },
"requirements": { "requirements": {
"chrome_extension": { "browser_extension": {
"name": "OpenDia Browser Extension", "name": "OpenDia Browser Extension",
"description": "Required Chrome extension for browser automation by Aaron Elijah Mars", "description": "Required Chrome/Firefox extension for browser automation by Aaron Elijah Mars",
"version": "1.0.5", "version": "1.0.6",
"auto_install": false "auto_install": false
} }
} }
@ -292,10 +293,12 @@ cp LICENSE dist/opendia-dxt/ 2>/dev/null || echo "⚠️ LICENSE not found, ski
# Create extension installation guide # Create extension installation guide
cat > dist/opendia-dxt/EXTENSION_INSTALL.md << 'EOF' cat > dist/opendia-dxt/EXTENSION_INSTALL.md << 'EOF'
# OpenDia Chrome Extension Installation # OpenDia Browser Extension Installation
## Quick Setup ## Quick Setup
### For Chrome/Chromium Browsers
1. **Enable Developer Mode** 1. **Enable Developer Mode**
- Go to `chrome://extensions/` - Go to `chrome://extensions/`
- Toggle "Developer mode" in the top right - Toggle "Developer mode" in the top right
@ -305,13 +308,25 @@ cat > dist/opendia-dxt/EXTENSION_INSTALL.md << 'EOF'
- Select the `extension/` folder from this DXT package - Select the `extension/` folder from this DXT package
- Extension should appear in your extensions list - Extension should appear in your extensions list
3. **Verify Connection** ### For Firefox
1. **Load Temporary Add-on**
- Go to `about:debugging#/runtime/this-firefox`
- Click "Load Temporary Add-on..."
- Select the `manifest-firefox.json` file from the `extension/` folder
> **Note**: Firefox extensions are loaded as temporary add-ons and will be removed when Firefox restarts.
## Verify Connection
- Click the OpenDia extension icon - Click the OpenDia extension icon
- Should show "Connected to MCP server" - Should show "Connected to MCP server"
- Green status indicator means ready to use - Green status indicator means ready to use
## Supported Browsers ## Supported Browsers
- Google Chrome, Arc Browser, Microsoft Edge, Brave Browser, Opera - Mozilla Firefox (Manifest V2)
- Google Chrome (Manifest V3)
- Arc Browser, Microsoft Edge, Brave Browser, Opera
- Any Chromium-based browser - Any Chromium-based browser
## Features ## Features
@ -375,6 +390,6 @@ echo ""
echo "🚀 Installation:" echo "🚀 Installation:"
echo "1. Double-click the .dxt file" echo "1. Double-click the .dxt file"
echo "2. Or: Claude Desktop Settings → Extensions → Install Extension" echo "2. Or: Claude Desktop Settings → Extensions → Install Extension"
echo "3. Install Chrome extension from extension/ folder" echo "3. Install Chrome/Firefox extension from extension/ folder"
echo "" echo ""
echo "🎯 Features ready: Anti-detection bypass + universal automation" echo "🎯 Features ready: Anti-detection bypass + universal automation"

2
opendia-extension/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules/
dist/

312
opendia-extension/README.md Normal file
View File

@ -0,0 +1,312 @@
# OpenDia Cross-Browser Extension
A dual-manifest browser extension supporting both Chrome MV3 and Firefox MV2 for comprehensive browser automation through the Model Context Protocol (MCP).
## 🌐 Browser Support
| Browser | Manifest | Background | Connection | Store |
|---------|----------|------------|------------|-------|
| Chrome | V3 | Service Worker | Temporary | Chrome Web Store |
| Firefox | V2 | Background Page | Persistent | Firefox Add-ons |
| Edge | V3 | Service Worker | Temporary | Microsoft Store |
| Safari | - | - | - | Coming Soon |
## 🚀 Quick Start
### Development Setup
```bash
# Install dependencies
npm install
# Build for all browsers
npm run build
# Build for specific browser
npm run build:chrome
npm run build:firefox
# Create distribution packages
npm run package:chrome
npm run package:firefox
# Test builds
node test-extension.js
```
### Installation
#### Chrome/Edge/Brave
1. Build the extension: `npm run build:chrome`
2. Open `chrome://extensions/` (or equivalent)
3. Enable "Developer mode"
4. Click "Load unpacked" and select `dist/chrome`
#### Firefox
1. Build the extension: `npm run build:firefox`
2. Open `about:debugging#/runtime/this-firefox`
3. Click "Load Temporary Add-on"
4. Select any file in `dist/firefox` directory
## 🏗️ Architecture
### Dual-Manifest Strategy
OpenDia uses a dual-manifest approach to maximize browser compatibility:
- **Chrome MV3**: Required for Chrome Web Store, uses service workers
- **Firefox MV2**: Enhanced capabilities, persistent background pages
### Connection Management
```javascript
// Chrome MV3: Temporary connections
class ServiceWorkerManager {
async ensureConnection() {
// Create fresh connection for each operation
await this.createTemporaryConnection();
}
}
// Firefox MV2: Persistent connections
class BackgroundPageManager {
constructor() {
this.persistentSocket = null;
this.setupPersistentConnection();
}
}
```
### Cross-Browser Compatibility
The extension uses WebExtension polyfill for consistent API usage:
```javascript
// Polyfill setup
if (typeof browser === 'undefined' && typeof chrome !== 'undefined') {
globalThis.browser = chrome;
}
// Unified API usage
const tabs = await browser.tabs.query({active: true, currentWindow: true});
```
## 🔧 Build System
### Build Configuration
The build system creates browser-specific packages:
```javascript
// build.js
async function buildForBrowser(browser) {
// Copy common files
await fs.copy('src', path.join(buildDir, 'src'));
// Copy browser-specific manifest
await fs.copy(`manifest-${browser}.json`, path.join(buildDir, 'manifest.json'));
// Copy WebExtension polyfill
await fs.copy('node_modules/webextension-polyfill/dist/browser-polyfill.min.js',
path.join(buildDir, 'src/polyfill/browser-polyfill.min.js'));
}
```
### Build Validation
```bash
# Validate all builds
npm run build && node build.js validate
# Check specific browser
node build.js validate chrome
node build.js validate firefox
```
## 🧪 Testing
### Automated Testing
```bash
# Run comprehensive tests
node test-extension.js
# Test specific components
node test-extension.js --manifest
node test-extension.js --background
node test-extension.js --content
```
### Manual Testing
1. **Connection Test**: Extension popup should show "Connected to MCP server"
2. **Background Tab Test**: Use `tab_list` with `check_content_script: true`
3. **Cross-Browser Test**: Same functionality on both Chrome and Firefox
## 📁 Directory Structure
```
opendia-extension/
├── src/
│ ├── background/
│ │ └── background.js # Cross-browser background script
│ ├── content/
│ │ └── content.js # Content script with polyfill
│ ├── popup/
│ │ ├── popup.html # Extension popup
│ │ └── popup.js # Popup logic with browser APIs
│ └── polyfill/
│ └── browser-polyfill.min.js # WebExtension polyfill
├── icons/ # Extension icons
├── dist/ # Build output
│ ├── chrome/ # Chrome MV3 build
│ ├── firefox/ # Firefox MV2 build
│ ├── opendia-chrome.zip # Chrome package
│ └── opendia-firefox.zip # Firefox package
├── manifest-chrome.json # Chrome MV3 manifest
├── manifest-firefox.json # Firefox MV2 manifest
├── build.js # Build system
├── test-extension.js # Test suite
└── package.json # Dependencies and scripts
```
## 🔗 Integration
### MCP Server Connection
The extension automatically discovers and connects to the MCP server:
```javascript
// Port discovery
const commonPorts = [5556, 5557, 5558, 3001, 6001, 6002, 6003];
const response = await fetch(`http://localhost:${port}/ports`);
const portInfo = await response.json();
```
### Background Tab Support
All tools support background tab targeting:
```javascript
// Target specific tab
await browser.tabs.sendMessage(tabId, {
action: 'page_analyze',
data: { intent_hint: 'login', tab_id: 12345 }
});
```
## 🛠️ Development
### Adding New Features
1. **Cross-Browser First**: Use `browser` API throughout
2. **Connection Aware**: Handle both temporary and persistent connections
3. **Test Both Browsers**: Validate on Chrome and Firefox
4. **Update Both Manifests**: Ensure compatibility
### Browser-Specific Handling
```javascript
// Detect browser environment
const browserInfo = {
isFirefox: typeof browser !== 'undefined' && browser.runtime.getManifest().applications?.gecko,
isChrome: typeof chrome !== 'undefined' && !browser.runtime.getManifest().applications?.gecko,
isServiceWorker: typeof importScripts === 'function',
manifestVersion: browser.runtime.getManifest().manifest_version
};
// Handle differences
if (browserInfo.isServiceWorker) {
// Chrome MV3 service worker behavior
} else {
// Firefox MV2 background page behavior
}
```
### API Compatibility
| Feature | Chrome MV3 | Firefox MV2 | Implementation |
|---------|------------|-------------|----------------|
| Background | Service Worker | Background Page | Connection Manager |
| Script Injection | `browser.scripting` | `browser.tabs.executeScript` | Feature detection |
| Persistent State | ❌ | ✅ | Browser-specific storage |
| WebRequest Blocking | Limited | Full | Firefox advantage |
| Store Distribution | Required | Optional | Both supported |
## 🚀 Distribution
### Chrome Web Store
```bash
# Build and package
npm run package:chrome
# Upload dist/opendia-chrome.zip to Chrome Web Store
```
### Firefox Add-ons (AMO)
```bash
# Build and package
npm run package:firefox
# Upload dist/opendia-firefox.zip to addons.mozilla.org
```
### GitHub Releases
```bash
# Create both packages
npm run package:chrome
npm run package:firefox
# Upload both files to GitHub releases
```
## 🤝 Contributing
1. **Test Both Browsers**: Always test Chrome and Firefox
2. **Use Browser APIs**: Avoid `chrome.*` direct usage
3. **Update Both Manifests**: Keep manifests in sync
4. **Validate Builds**: Run test suite before committing
## 📚 Resources
- [WebExtension API Documentation](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions)
- [Chrome Extension MV3 Guide](https://developer.chrome.com/docs/extensions/mv3/)
- [Firefox Extension Development](https://extensionworkshop.com/)
- [WebExtension Polyfill](https://github.com/mozilla/webextension-polyfill)
## 🔧 Troubleshooting
### Common Issues
1. **Connection Fails**: Check MCP server is running (`npm start` in `opendia-mcp/`)
2. **Chrome Service Worker**: Extensions may need manual restart in `chrome://extensions`
3. **Firefox Temporary**: Extension reloads required after Firefox restart
4. **Build Errors**: Ensure all dependencies installed (`npm install`)
### Debug Commands
```bash
# Check server status
curl http://localhost:5556/ping
# Validate builds
node build.js validate
# Test extension compatibility
node test-extension.js
# Check extension logs
# Chrome: chrome://extensions -> OpenDia -> service worker
# Firefox: about:debugging -> OpenDia -> Inspect
```
## 🎯 Future Enhancements
- [ ] Safari extension support
- [ ] Edge-specific optimizations
- [ ] WebExtension Manifest V3 migration for Firefox
- [ ] Enhanced anti-detection features
- [ ] Performance optimizations for service workers

193
opendia-extension/build.js Normal file
View File

@ -0,0 +1,193 @@
const fs = require('fs-extra');
const path = require('path');
async function buildForBrowser(browser) {
const buildDir = `dist/${browser}`;
console.log(`🔧 Building ${browser} extension...`);
// Clean and create build directory
await fs.remove(buildDir);
await fs.ensureDir(buildDir);
// Copy common files
await fs.copy('src', path.join(buildDir, 'src'));
await fs.copy('icons', path.join(buildDir, 'icons'));
// Copy logo files for animated popup
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(
`manifest-${browser}.json`,
path.join(buildDir, 'manifest.json')
);
// Copy polyfill
await fs.copy(
'node_modules/webextension-polyfill/dist/browser-polyfill.min.js',
path.join(buildDir, 'src/polyfill/browser-polyfill.min.js')
);
// Browser-specific post-processing
if (browser === 'chrome') {
console.log('📦 Chrome MV3: Service worker mode enabled');
// No additional processing needed for Chrome
} else if (browser === 'firefox') {
console.log('🦊 Firefox MV2: Background page mode enabled');
// No additional processing needed for Firefox
}
console.log(`${browser} extension built successfully in ${buildDir}`);
}
async function buildAll() {
console.log('🚀 Building extensions for all browsers...');
try {
await buildForBrowser('chrome');
await buildForBrowser('firefox');
console.log('🎉 All extensions built successfully!');
console.log('');
console.log('📁 Build outputs:');
console.log(' Chrome MV3: dist/chrome/');
console.log(' Firefox MV2: dist/firefox/');
console.log('');
console.log('🧪 Testing instructions:');
console.log(' Chrome: Load dist/chrome in chrome://extensions (Developer mode)');
console.log(' Firefox: Load dist/firefox in about:debugging#/runtime/this-firefox');
} catch (error) {
console.error('❌ Build failed:', error);
process.exit(1);
}
}
async function createPackages() {
console.log('📦 Creating distribution packages...');
const { execSync } = require('child_process');
try {
// Create Chrome package
console.log('Creating Chrome package...');
execSync('cd dist/chrome && zip -r ../opendia-chrome.zip .', { stdio: 'inherit' });
// Create Firefox package (using web-ext if available)
console.log('Creating Firefox package...');
try {
execSync('cd dist/firefox && web-ext build --overwrite-dest', { stdio: 'inherit' });
console.log('✅ Firefox package created with web-ext');
} catch (e) {
// Fallback to zip if web-ext is not available
console.log('⚠️ web-ext not available, using zip fallback');
execSync('cd dist/firefox && zip -r ../opendia-firefox.zip .', { stdio: 'inherit' });
}
console.log('📦 Distribution packages created:');
console.log(' Chrome: dist/opendia-chrome.zip');
console.log(' Firefox: dist/opendia-firefox.zip (or .xpi)');
} catch (error) {
console.error('❌ Package creation failed:', error);
process.exit(1);
}
}
async function validateBuild(browser) {
const buildDir = `dist/${browser}`;
const manifestPath = path.join(buildDir, 'manifest.json');
console.log(`🔍 Validating ${browser} build...`);
try {
// Check manifest exists and is valid JSON
const manifest = await fs.readJson(manifestPath);
// Check required files exist
const requiredFiles = [
'src/background/background.js',
'src/content/content.js',
'src/popup/popup.html',
'src/popup/popup.js',
'src/polyfill/browser-polyfill.min.js'
];
for (const file of requiredFiles) {
const filePath = path.join(buildDir, file);
if (!await fs.pathExists(filePath)) {
throw new Error(`Missing required file: ${file}`);
}
}
// Browser-specific validation
if (browser === 'chrome') {
if (manifest.manifest_version !== 3) {
throw new Error('Chrome build must use manifest version 3');
}
if (!manifest.background?.service_worker) {
throw new Error('Chrome build must specify service_worker in background');
}
} else if (browser === 'firefox') {
if (manifest.manifest_version !== 2) {
throw new Error('Firefox build must use manifest version 2');
}
if (!manifest.background?.scripts) {
throw new Error('Firefox build must specify scripts in background');
}
}
console.log(`${browser} build validation passed`);
return true;
} catch (error) {
console.error(`${browser} build validation failed:`, error.message);
return false;
}
}
async function validateAllBuilds() {
console.log('🔍 Validating all builds...');
const chromeValid = await validateBuild('chrome');
const firefoxValid = await validateBuild('firefox');
if (chromeValid && firefoxValid) {
console.log('✅ All builds validated successfully');
return true;
} else {
console.error('❌ Build validation failed');
return false;
}
}
// CLI usage
if (require.main === module) {
const args = process.argv.slice(2);
const command = args[0];
switch (command) {
case 'chrome':
buildForBrowser('chrome');
break;
case 'firefox':
buildForBrowser('firefox');
break;
case 'validate':
validateAllBuilds();
break;
case 'package':
buildAll().then(() => createPackages());
break;
default:
buildAll();
}
}
module.exports = { buildForBrowser, buildAll, createPackages, validateBuild, validateAllBuilds };

View File

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

@ -0,0 +1,46 @@
{
"manifest_version": 3,
"name": "OpenDia",
"version": "1.0.6",
"description": "Connect your browser to AI models",
"icons": {
"16": "icons/icon-16.png",
"32": "icons/icon-32.png",
"48": "icons/icon-48.png",
"128": "icons/icon-128.png"
},
"permissions": [
"tabs",
"activeTab",
"storage",
"scripting",
"webNavigation",
"notifications",
"bookmarks",
"history"
],
"host_permissions": [
"<all_urls>"
],
"background": {
"service_worker": "src/background/background.js",
"type": "module"
},
"action": {
"default_popup": "src/popup/popup.html",
"default_title": "OpenDia"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["src/polyfill/browser-polyfill.min.js", "src/content/content.js"],
"run_at": "document_idle"
}
],
"web_accessible_resources": [
{
"resources": ["src/polyfill/browser-polyfill.min.js"],
"matches": ["<all_urls>"]
}
]
}

View File

@ -0,0 +1,51 @@
{
"manifest_version": 2,
"name": "OpenDia",
"version": "1.0.6",
"description": "Connect your browser to AI models",
"applications": {
"gecko": {
"id": "opendia@aaronjmars.com",
"strict_min_version": "109.0"
}
},
"icons": {
"16": "icons/icon-16.png",
"32": "icons/icon-32.png",
"48": "icons/icon-48.png",
"128": "icons/icon-128.png"
},
"permissions": [
"tabs",
"activeTab",
"storage",
"webNavigation",
"notifications",
"bookmarks",
"history",
"webRequest",
"webRequestBlocking",
"<all_urls>"
],
"background": {
"scripts": [
"src/polyfill/browser-polyfill.min.js",
"src/background/background.js"
],
"persistent": false
},
"browser_action": {
"default_popup": "src/popup/popup.html",
"default_title": "OpenDia"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["src/polyfill/browser-polyfill.min.js", "src/content/content.js"],
"run_at": "document_idle"
}
],
"web_accessible_resources": [
"src/polyfill/browser-polyfill.min.js"
]
}

View File

@ -1,8 +1,8 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "OpenDia", "name": "OpenDia",
"version": "1.0.5", "version": "1.0.6",
"description": "Browser automation through Model Context Protocol", "description": "Connect your browser to AI models",
"icons": { "icons": {
"16": "icon-16.png", "16": "icon-16.png",
"32": "icon-32.png", "32": "icon-32.png",

4319
opendia-extension/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,28 @@
{
"name": "opendia-extension",
"version": "1.0.6",
"description": "Connect your browser to AI models",
"scripts": {
"build": "node build.js",
"build:chrome": "node build.js chrome",
"build:firefox": "node build.js firefox",
"dev:chrome": "npm run build:chrome && echo 'Load dist/chrome in Chrome'",
"dev:firefox": "npm run build:firefox && web-ext run --source-dir=dist/firefox",
"package:chrome": "npm run build:chrome && cd dist/chrome && zip -r ../opendia-chrome.zip .",
"package:firefox": "npm run build:firefox && cd dist/firefox && web-ext build --overwrite-dest"
},
"keywords": [
"webextension",
"mcp",
"browser-automation"
],
"author": "Aaron J Mars",
"license": "MIT",
"dependencies": {
"webextension-polyfill": "^0.12.0"
},
"devDependencies": {
"fs-extra": "^11.3.0",
"web-ext": "^8.8.0"
}
}

Binary file not shown.

Binary file not shown.

View File

@ -1,8 +1,20 @@
// Import WebExtension polyfill at the top
if (typeof browser === 'undefined' && typeof chrome !== 'undefined') {
globalThis.browser = chrome;
}
// Browser detection
const browserInfo = {
isFirefox: typeof browser !== 'undefined' && browser.runtime.getManifest().applications?.gecko,
isChrome: typeof chrome !== 'undefined' && !browser.runtime.getManifest().applications?.gecko,
isServiceWorker: typeof importScripts === 'function',
manifestVersion: browser.runtime.getManifest().manifest_version
};
console.log('🌐 Browser detected:', browserInfo);
// MCP Server connection configuration // MCP Server connection configuration
let MCP_SERVER_URL = 'ws://localhost:5555'; // Default, will be auto-discovered 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 let lastKnownPorts = { websocket: 5555, http: 5556 }; // Cache for port discovery
// Safety Mode configuration // Safety Mode configuration
@ -13,10 +25,205 @@ const WRITE_EDIT_TOOLS = [
]; ];
// Load safety mode state on startup // Load safety mode state on startup
chrome.storage.local.get(['safetyMode'], (result) => { browser.storage.local.get(['safetyMode'], (result) => {
safetyModeEnabled = result.safetyMode || false; safetyModeEnabled = result.safetyMode || false;
}); });
// Cross-browser WebSocket connection manager
class ConnectionManager {
constructor() {
this.mcpSocket = null;
this.reconnectInterval = null;
this.reconnectAttempts = 0;
this.heartbeatInterval = null;
this.isServiceWorker = browserInfo.isServiceWorker;
this.isFirefox = browserInfo.isFirefox;
}
async connect() {
if (this.isServiceWorker) {
// Chrome MV3: Create fresh connection for each operation
console.log('🔧 Chrome MV3: Creating temporary connection');
await this.createConnection();
} else {
// Firefox MV2: Maintain persistent connection
if (!this.mcpSocket || this.mcpSocket.readyState !== WebSocket.OPEN) {
console.log('🦊 Firefox MV2: Creating persistent connection');
await this.createConnection();
} else {
console.log('🦊 Firefox MV2: Using existing connection');
}
}
}
async createConnection() {
try {
// Try port discovery if using default URL or if connection failed
if (MCP_SERVER_URL === 'ws://localhost:5555' || this.reconnectAttempts > 2) {
await this.discoverServerPorts();
this.reconnectAttempts = 0; // Reset attempts after discovery
}
console.log('🔗 Connecting to MCP server at', MCP_SERVER_URL);
this.mcpSocket = new WebSocket(MCP_SERVER_URL);
this.mcpSocket.onopen = () => {
console.log('✅ Connected to MCP server');
this.clearReconnectInterval();
this.reconnectAttempts = 0; // Reset attempts on successful connection
const tools = getAvailableTools();
console.log(`🔧 Registering ${tools.length} tools:`, tools.map(t => t.name));
// Register available browser functions
this.mcpSocket.send(JSON.stringify({
type: 'register',
tools: tools
}));
// Setup heartbeat for persistent connections
if (!this.isServiceWorker) {
this.setupHeartbeat();
}
};
this.mcpSocket.onmessage = async (event) => {
const message = JSON.parse(event.data);
await handleMCPRequest(message);
};
this.mcpSocket.onclose = (event) => {
console.log(`❌ Disconnected from MCP server (code: ${event.code}, reason: ${event.reason})`);
this.clearHeartbeat(); // Clear heartbeat on disconnect
this.reconnectAttempts++;
// Check if this was a normal closure or abnormal
if (event.code !== 1000 && event.code !== 1001) {
console.log('🔄 Abnormal WebSocket closure, will attempt reconnection');
if (!this.isServiceWorker) {
// Firefox: Attempt to reconnect
this.scheduleReconnect();
}
// Chrome: Will reconnect on next message
} else {
console.log('🔄 Normal WebSocket closure');
}
};
this.mcpSocket.onerror = (error) => {
console.log('⚠️ MCP WebSocket error:', error);
this.reconnectAttempts++;
};
} catch (error) {
console.error('Connection failed:', error);
if (!this.isServiceWorker) {
this.scheduleReconnect();
}
}
}
async 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
}
}
console.log('⚠️ Port discovery failed, using defaults');
return null;
}
setupHeartbeat() {
// Only maintain heartbeat in persistent background pages
this.clearHeartbeat();
this.heartbeatInterval = setInterval(() => {
if (this.mcpSocket?.readyState === WebSocket.OPEN) {
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();
}
}, 15000); // More frequent heartbeat for better reliability
}
clearHeartbeat() {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
}
}
scheduleReconnect() {
this.clearReconnectInterval();
// Exponential backoff for reconnection attempts
const backoffTime = Math.min(5000 * Math.pow(2, this.reconnectAttempts), 30000);
console.log(`🔄 Scheduling reconnection in ${backoffTime}ms (attempt ${this.reconnectAttempts})`);
this.reconnectInterval = setInterval(() => {
if (this.reconnectAttempts < 10) {
this.connect();
} else {
console.log('❌ Maximum reconnection attempts reached');
this.clearReconnectInterval();
}
}, backoffTime);
}
clearReconnectInterval() {
if (this.reconnectInterval) {
clearInterval(this.reconnectInterval);
this.reconnectInterval = null;
}
}
async ensureConnection() {
if (this.isServiceWorker) {
// Chrome: Always create fresh connection
await this.connect();
} else {
// Firefox: Use existing or create new
if (!this.mcpSocket || this.mcpSocket.readyState !== WebSocket.OPEN) {
await this.connect();
}
}
return this.mcpSocket;
}
send(message) {
if (this.mcpSocket && this.mcpSocket.readyState === WebSocket.OPEN) {
this.mcpSocket.send(JSON.stringify(message));
} else {
console.error('WebSocket not connected');
}
}
getStatus() {
return {
connected: this.mcpSocket && this.mcpSocket.readyState === WebSocket.OPEN,
browserInfo: browserInfo,
connectionType: this.isServiceWorker ? 'temporary' : 'persistent'
};
}
}
// Create global connection manager
const connectionManager = new ConnectionManager();
// Content script management for background tabs // Content script management for background tabs
async function ensureContentScriptReady(tabId, retries = 3) { async function ensureContentScriptReady(tabId, retries = 3) {
for (let attempt = 1; attempt <= retries; attempt++) { for (let attempt = 1; attempt <= retries; attempt++) {
@ -27,10 +234,10 @@ async function ensureContentScriptReady(tabId, retries = 3) {
reject(new Error('Content script ping timeout')); reject(new Error('Content script ping timeout'));
}, 2000); }, 2000);
chrome.tabs.sendMessage(tabId, { action: 'ping' }, (response) => { browser.tabs.sendMessage(tabId, { action: 'ping' }, (response) => {
clearTimeout(timeout); clearTimeout(timeout);
if (chrome.runtime.lastError) { if (browser.runtime.lastError) {
reject(new Error(chrome.runtime.lastError.message)); reject(new Error(browser.runtime.lastError.message));
} else { } else {
resolve(response); resolve(response);
} }
@ -47,7 +254,7 @@ async function ensureContentScriptReady(tabId, retries = 3) {
if (attempt === retries) { if (attempt === retries) {
// Last attempt - try to inject content script // Last attempt - try to inject content script
try { try {
const tab = await chrome.tabs.get(tabId); const tab = await browser.tabs.get(tabId);
// Check if tab URL is injectable (not chrome://, chrome-extension://, etc.) // Check if tab URL is injectable (not chrome://, chrome-extension://, etc.)
if (!isInjectableUrl(tab.url)) { if (!isInjectableUrl(tab.url)) {
@ -55,10 +262,33 @@ async function ensureContentScriptReady(tabId, retries = 3) {
} }
console.log(`🔄 Injecting content script into tab ${tabId}`); console.log(`🔄 Injecting content script into tab ${tabId}`);
await chrome.scripting.executeScript({
// Use appropriate API based on browser
if (browser.scripting) {
// Chrome MV3
await browser.scripting.executeScript({
target: { tabId: tabId }, target: { tabId: tabId },
files: ['content.js'] files: ['src/content/content.js']
}); });
} else {
// Firefox MV2 - check if already injected first
try {
const result = await browser.tabs.executeScript(tabId, {
code: 'typeof window.OpenDiaContentScriptLoaded !== "undefined"'
});
if (result && result[0]) {
console.log(`🔄 Content script already present in tab ${tabId}`);
return true;
}
} catch (e) {
// Continue with injection if check fails
}
await browser.tabs.executeScript(tabId, {
file: 'src/content/content.js'
});
}
// Wait a moment for script to initialize // Wait a moment for script to initialize
await new Promise(resolve => setTimeout(resolve, 1000)); await new Promise(resolve => setTimeout(resolve, 1000));
@ -66,10 +296,10 @@ async function ensureContentScriptReady(tabId, retries = 3) {
// Test again // Test again
const testResponse = await new Promise((resolve, reject) => { const testResponse = await new Promise((resolve, reject) => {
const timeout = setTimeout(() => reject(new Error('Timeout after injection')), 3000); const timeout = setTimeout(() => reject(new Error('Timeout after injection')), 3000);
chrome.tabs.sendMessage(tabId, { action: 'ping' }, (response) => { browser.tabs.sendMessage(tabId, { action: 'ping' }, (response) => {
clearTimeout(timeout); clearTimeout(timeout);
if (chrome.runtime.lastError) { if (browser.runtime.lastError) {
reject(new Error(chrome.runtime.lastError.message)); reject(new Error(browser.runtime.lastError.message));
} else { } else {
resolve(response); resolve(response);
} }
@ -100,15 +330,15 @@ async function ensureContentScriptReady(tabId, retries = 3) {
function isInjectableUrl(url) { function isInjectableUrl(url) {
if (!url) return false; if (!url) return false;
const restrictedProtocols = ['chrome:', 'chrome-extension:', 'chrome-devtools:', 'edge:', 'moz-extension:']; const restrictedProtocols = ['chrome:', 'chrome-extension:', 'chrome-devtools:', 'edge:', 'moz-extension:', 'about:'];
const restrictedDomains = ['chrome.google.com']; const restrictedDomains = ['chrome.google.com', 'addons.mozilla.org'];
// Check protocol // Check protocol
if (restrictedProtocols.some(protocol => url.startsWith(protocol))) { if (restrictedProtocols.some(protocol => url.startsWith(protocol))) {
return false; return false;
} }
// Check special Chrome pages // Check special browser pages
if (url.startsWith('https://chrome.google.com/webstore') || if (url.startsWith('https://chrome.google.com/webstore') ||
url.includes('chrome://') || url.includes('chrome://') ||
restrictedDomains.some(domain => url.includes(domain))) { restrictedDomains.some(domain => url.includes(domain))) {
@ -121,7 +351,7 @@ function isInjectableUrl(url) {
// Get content script readiness status for a tab // Get content script readiness status for a tab
async function getTabContentScriptStatus(tabId) { async function getTabContentScriptStatus(tabId) {
try { try {
const tab = await chrome.tabs.get(tabId); const tab = await browser.tabs.get(tabId);
if (!isInjectableUrl(tab.url)) { if (!isInjectableUrl(tab.url)) {
return { ready: false, reason: 'restricted_url', url: tab.url }; return { ready: false, reason: 'restricted_url', url: tab.url };
@ -129,7 +359,7 @@ async function getTabContentScriptStatus(tabId) {
const response = await new Promise((resolve, reject) => { const response = await new Promise((resolve, reject) => {
const timeout = setTimeout(() => resolve(null), 1000); const timeout = setTimeout(() => resolve(null), 1000);
chrome.tabs.sendMessage(tabId, { action: 'ping' }, (response) => { browser.tabs.sendMessage(tabId, { action: 'ping' }, (response) => {
clearTimeout(timeout); clearTimeout(timeout);
resolve(response); resolve(response);
}); });
@ -146,82 +376,6 @@ async function getTabContentScriptStatus(tabId) {
} }
} }
// 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
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);
mcpSocket.onopen = () => {
console.log('✅ Connected to MCP server');
clearInterval(reconnectInterval);
const tools = getAvailableTools();
console.log(`🔧 Registering ${tools.length} tools:`, tools.map(t => t.name));
// Register available browser functions
mcpSocket.send(JSON.stringify({
type: 'register',
tools: tools
}));
};
mcpSocket.onmessage = async (event) => {
const message = JSON.parse(event.data);
await handleMCPRequest(message);
};
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++;
};
}
// Define available browser automation tools for MCP // Define available browser automation tools for MCP
function getAvailableTools() { function getAvailableTools() {
return [ return [
@ -839,6 +993,9 @@ async function handleMCPRequest(message) {
const { id, method, params } = message; const { id, method, params } = message;
try { try {
// Ensure connection for Chrome service workers
await connectionManager.ensureConnection();
// Safety Mode check: Block write/edit tools if safety mode is enabled // Safety Mode check: Block write/edit tools if safety mode is enabled
if (safetyModeEnabled && WRITE_EDIT_TOOLS.includes(method)) { if (safetyModeEnabled && WRITE_EDIT_TOOLS.includes(method)) {
const targetInfo = params.tab_id ? `tab ${params.tab_id}` : 'the current page'; const targetInfo = params.tab_id ? `tab ${params.tab_id}` : 'the current page';
@ -913,23 +1070,19 @@ async function handleMCPRequest(message) {
} }
// Send success response // Send success response
mcpSocket.send( connectionManager.send({
JSON.stringify({
id, id,
result, result,
}) });
);
} catch (error) { } catch (error) {
// Send error response // Send error response
mcpSocket.send( connectionManager.send({
JSON.stringify({
id, id,
error: { error: {
message: error.message, message: error.message,
code: -32603, code: -32603,
}, },
}) });
);
} }
} }
@ -940,13 +1093,13 @@ async function sendToContentScript(action, data, targetTabId = null) {
if (targetTabId) { if (targetTabId) {
// Use specific tab // Use specific tab
try { try {
targetTab = await chrome.tabs.get(targetTabId); targetTab = await browser.tabs.get(targetTabId);
} catch (error) { } catch (error) {
throw new Error(`Tab ${targetTabId} not found or inaccessible`); throw new Error(`Tab ${targetTabId} not found or inaccessible`);
} }
} else { } else {
// Fallback to active tab (maintains compatibility) // Fallback to active tab (maintains compatibility)
const [activeTab] = await chrome.tabs.query({ const [activeTab] = await browser.tabs.query({
active: true, active: true,
currentWindow: true, currentWindow: true,
}); });
@ -961,9 +1114,9 @@ async function sendToContentScript(action, data, targetTabId = null) {
await ensureContentScriptReady(targetTab.id); await ensureContentScriptReady(targetTab.id);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
chrome.tabs.sendMessage(targetTab.id, { action, data }, (response) => { browser.tabs.sendMessage(targetTab.id, { action, data }, (response) => {
if (chrome.runtime.lastError) { if (browser.runtime.lastError) {
reject(new Error(`Tab ${targetTab.id}: ${chrome.runtime.lastError.message}`)); reject(new Error(`Tab ${targetTab.id}: ${browser.runtime.lastError.message}`));
} else if (response && response.success) { } else if (response && response.success) {
resolve(response.data); resolve(response.data);
} else { } else {
@ -974,12 +1127,12 @@ async function sendToContentScript(action, data, targetTabId = null) {
} }
async function navigateToUrl(url, waitFor, timeout = 10000) { async function navigateToUrl(url, waitFor, timeout = 10000) {
const [activeTab] = await chrome.tabs.query({ const [activeTab] = await browser.tabs.query({
active: true, active: true,
currentWindow: true, currentWindow: true,
}); });
await chrome.tabs.update(activeTab.id, { url }); await browser.tabs.update(activeTab.id, { url });
// If waitFor is specified, wait for the element to appear // If waitFor is specified, wait for the element to appear
if (waitFor) { if (waitFor) {
@ -998,7 +1151,7 @@ async function waitForElement(tabId, selector, timeout = 5000) {
while (Date.now() - startTime < timeout) { while (Date.now() - startTime < timeout) {
try { try {
const result = await chrome.tabs.sendMessage(tabId, { const result = await browser.tabs.sendMessage(tabId, {
action: 'wait_for', action: 'wait_for',
data: { data: {
condition_type: 'element_visible', condition_type: 'element_visible',
@ -1115,7 +1268,7 @@ async function createSingleTab(url, active, wait_for, timeout) {
} }
console.log(`🔍 Creating single tab with properties:`, createProperties); console.log(`🔍 Creating single tab with properties:`, createProperties);
const newTab = await chrome.tabs.create(createProperties); const newTab = await browser.tabs.create(createProperties);
console.log(`📝 Tab created:`, { id: newTab.id, url: newTab.url, pendingUrl: newTab.pendingUrl }); console.log(`📝 Tab created:`, { id: newTab.id, url: newTab.url, pendingUrl: newTab.pendingUrl });
// Wait a moment for the URL to load // Wait a moment for the URL to load
@ -1124,7 +1277,7 @@ async function createSingleTab(url, active, wait_for, timeout) {
// Check if tab loaded correctly // Check if tab loaded correctly
try { try {
const updatedTab = await chrome.tabs.get(newTab.id); const updatedTab = await browser.tabs.get(newTab.id);
console.log(`🔄 Tab after load check:`, { id: updatedTab.id, url: updatedTab.url, status: updatedTab.status }); console.log(`🔄 Tab after load check:`, { id: updatedTab.id, url: updatedTab.url, status: updatedTab.status });
// If URL was provided and wait_for is specified, wait for the element // If URL was provided and wait_for is specified, wait for the element
@ -1226,14 +1379,14 @@ async function createTabsBatch(urls, active, wait_for, timeout, batch_settings =
// Only activate the very last tab if active=true // Only activate the very last tab if active=true
const shouldActivate = active && isLastTab; const shouldActivate = active && isLastTab;
const tab = await chrome.tabs.create({ const tab = await browser.tabs.create({
url: url, url: url,
active: shouldActivate active: shouldActivate
}); });
// Wait a moment and check actual URL // Wait a moment and check actual URL
await new Promise(resolve => setTimeout(resolve, 300)); await new Promise(resolve => setTimeout(resolve, 300));
const updatedTab = await chrome.tabs.get(tab.id); const updatedTab = await browser.tabs.get(tab.id);
createdTabs.push({ createdTabs.push({
tab_id: tab.id, tab_id: tab.id,
@ -1354,7 +1507,7 @@ async function closeTabs(params) {
tabsToClose = [tab_id]; tabsToClose = [tab_id];
} else { } else {
// Close current tab // Close current tab
const [activeTab] = await chrome.tabs.query({ const [activeTab] = await browser.tabs.query({
active: true, active: true,
currentWindow: true, currentWindow: true,
}); });
@ -1368,7 +1521,7 @@ async function closeTabs(params) {
} }
// Close tabs // Close tabs
await chrome.tabs.remove(tabsToClose); await browser.tabs.remove(tabsToClose);
return { return {
success: true, success: true,
@ -1389,7 +1542,7 @@ async function listTabs(params) {
queryOptions.currentWindow = true; queryOptions.currentWindow = true;
} }
const tabs = await chrome.tabs.query(queryOptions); const tabs = await browser.tabs.query(queryOptions);
// Check content script status if requested // Check content script status if requested
const contentScriptStatuses = new Map(); const contentScriptStatuses = new Map();
@ -1469,17 +1622,17 @@ async function listTabs(params) {
async function switchToTab(tabId) { async function switchToTab(tabId) {
// First, get tab info to ensure it exists // First, get tab info to ensure it exists
const tab = await chrome.tabs.get(tabId); const tab = await browser.tabs.get(tabId);
if (!tab) { if (!tab) {
throw new Error(`Tab with ID ${tabId} not found`); throw new Error(`Tab with ID ${tabId} not found`);
} }
// Switch to the tab // Switch to the tab
await chrome.tabs.update(tabId, { active: true }); await browser.tabs.update(tabId, { active: true });
// Also focus the window containing the tab // Also focus the window containing the tab
await chrome.windows.update(tab.windowId, { focused: true }); await browser.windows.update(tab.windowId, { focused: true });
return { return {
success: true, success: true,
@ -1496,9 +1649,9 @@ async function getBookmarks(params) {
let bookmarks; let bookmarks;
if (query) { if (query) {
bookmarks = await chrome.bookmarks.search(query); bookmarks = await browser.bookmarks.search(query);
} else { } else {
bookmarks = await chrome.bookmarks.getTree(); bookmarks = await browser.bookmarks.getTree();
} }
return { return {
@ -1511,7 +1664,7 @@ async function getBookmarks(params) {
async function addBookmark(params) { async function addBookmark(params) {
const { title, url, parentId } = params; const { title, url, parentId } = params;
const bookmark = await chrome.bookmarks.create({ const bookmark = await browser.bookmarks.create({
title, title,
url, url,
parentId parentId
@ -1537,7 +1690,7 @@ async function getHistory(params) {
} = params; } = params;
try { try {
// Chrome History API search configuration // Browser History API search configuration
const searchQuery = { const searchQuery = {
text: keywords, text: keywords,
maxResults: Math.min(max_results * 3, 1000), // Over-fetch for filtering maxResults: Math.min(max_results * 3, 1000), // Over-fetch for filtering
@ -1552,7 +1705,7 @@ async function getHistory(params) {
} }
// Execute history search // Execute history search
const historyItems = await chrome.history.search(searchQuery); const historyItems = await browser.history.search(searchQuery);
// Apply advanced filters // Apply advanced filters
let filteredItems = historyItems.filter(item => { let filteredItems = historyItems.filter(item => {
@ -1679,7 +1832,7 @@ async function getSelectedText(params) {
if (tab_id) { if (tab_id) {
// Use specific tab // Use specific tab
try { try {
targetTab = await chrome.tabs.get(tab_id); targetTab = await browser.tabs.get(tab_id);
} catch (error) { } catch (error) {
return { return {
success: false, success: false,
@ -1692,7 +1845,7 @@ async function getSelectedText(params) {
} }
} else { } else {
// Get the active tab // Get the active tab
const [activeTab] = await chrome.tabs.query({ const [activeTab] = await browser.tabs.query({
active: true, active: true,
currentWindow: true, currentWindow: true,
}); });
@ -1710,69 +1863,22 @@ async function getSelectedText(params) {
targetTab = activeTab; targetTab = activeTab;
} }
// Execute script to get selected text // Execute script to get selected text - handle browser differences
const results = await chrome.scripting.executeScript({ let results;
if (browser.scripting) {
// Chrome MV3
results = await browser.scripting.executeScript({
target: { tabId: targetTab.id }, target: { tabId: targetTab.id },
func: () => { func: getSelectionFunction
const selection = window.getSelection();
const selectedText = selection.toString();
if (!selectedText) {
return {
text: "",
hasSelection: false,
metadata: null
};
}
// Get metadata about the selection
const range = selection.getRangeAt(0);
const rect = range.getBoundingClientRect();
// Get parent element info
const commonAncestor = range.commonAncestorContainer;
const parentElement = commonAncestor.nodeType === Node.TEXT_NODE
? commonAncestor.parentElement
: commonAncestor;
const metadata = {
length: selectedText.length,
word_count: selectedText.trim().split(/\s+/).filter(word => word.length > 0).length,
line_count: selectedText.split('\n').length,
position: {
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height
},
parent_element: {
tag_name: parentElement.tagName?.toLowerCase(),
class_name: parentElement.className,
id: parentElement.id,
text_content_length: parentElement.textContent?.length || 0
},
page_info: {
url: window.location.href,
title: document.title,
domain: window.location.hostname
},
selection_info: {
anchor_offset: selection.anchorOffset,
focus_offset: selection.focusOffset,
range_count: selection.rangeCount,
is_collapsed: selection.isCollapsed
}
};
return {
text: selectedText,
hasSelection: true,
metadata: metadata
};
}
}); });
} else {
// Firefox MV2
results = await browser.tabs.executeScript(targetTab.id, {
code: `(${getSelectionFunction.toString()})()`
});
}
const result = results[0]?.result; const result = results[0]?.result || results[0];
if (!result) { if (!result) {
return { return {
@ -1847,24 +1953,74 @@ async function getSelectedText(params) {
} }
} }
// Function to execute in page context
function getSelectionFunction() {
const selection = window.getSelection();
const selectedText = selection.toString();
if (!selectedText) {
return {
text: "",
hasSelection: false,
metadata: null
};
}
// Get metadata about the selection
const range = selection.getRangeAt(0);
const rect = range.getBoundingClientRect();
// Get parent element info
const commonAncestor = range.commonAncestorContainer;
const parentElement = commonAncestor.nodeType === Node.TEXT_NODE
? commonAncestor.parentElement
: commonAncestor;
const metadata = {
length: selectedText.length,
word_count: selectedText.trim().split(/\s+/).filter(word => word.length > 0).length,
line_count: selectedText.split('\n').length,
position: {
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height
},
parent_element: {
tag_name: parentElement.tagName?.toLowerCase(),
class_name: parentElement.className,
id: parentElement.id,
text_content_length: parentElement.textContent?.length || 0
},
page_info: {
url: window.location.href,
title: document.title,
domain: window.location.hostname
},
selection_info: {
anchor_offset: selection.anchorOffset,
focus_offset: selection.focusOffset,
range_count: selection.rangeCount,
is_collapsed: selection.isCollapsed
}
};
return {
text: selectedText,
hasSelection: true,
metadata: metadata
};
}
// Initialize connection when extension loads (with delay for server startup) // Initialize connection when extension loads (with delay for server startup)
setTimeout(() => { setTimeout(() => {
connectToMCPServer(); connectionManager.connect();
}, 1000); }, 1000);
// Heartbeat to keep connection alive
setInterval(() => {
if (mcpSocket && mcpSocket.readyState === WebSocket.OPEN) {
mcpSocket.send(JSON.stringify({ type: "ping", timestamp: Date.now() }));
}
}, 30000); // Every 30 seconds
// Handle messages from popup // Handle messages from popup
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { browser.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === "getStatus") { if (request.action === "getStatus") {
sendResponse({ sendResponse(connectionManager.getStatus());
connected: mcpSocket && mcpSocket.readyState === WebSocket.OPEN,
});
} else if (request.action === "getToolCount") { } else if (request.action === "getToolCount") {
const tools = getAvailableTools(); const tools = getAvailableTools();
sendResponse({ sendResponse({
@ -1872,7 +2028,7 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
tools: tools.map(t => t.name) tools: tools.map(t => t.name)
}); });
} else if (request.action === "reconnect") { } else if (request.action === "reconnect") {
connectToMCPServer(); connectionManager.connect();
sendResponse({ success: true }); sendResponse({ success: true });
} else if (request.action === "getPorts") { } else if (request.action === "getPorts") {
sendResponse({ sendResponse({
@ -1884,9 +2040,7 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
console.log(`🛡️ Safety Mode ${safetyModeEnabled ? 'ENABLED' : 'DISABLED'}`); console.log(`🛡️ Safety Mode ${safetyModeEnabled ? 'ENABLED' : 'DISABLED'}`);
sendResponse({ success: true }); sendResponse({ success: true });
} else if (request.action === "test") { } else if (request.action === "test") {
if (mcpSocket && mcpSocket.readyState === WebSocket.OPEN) { connectionManager.send({ type: "test", timestamp: Date.now() });
mcpSocket.send(JSON.stringify({ type: "test", timestamp: Date.now() }));
}
sendResponse({ success: true }); sendResponse({ success: true });
} }
return true; // Keep the message channel open return true; // Keep the message channel open

View File

@ -1,4 +1,15 @@
// Enhanced Browser Automation Content Script with Anti-Detection // Enhanced Browser Automation Content Script with Anti-Detection
// Import WebExtension polyfill for cross-browser compatibility
if (typeof browser === 'undefined' && typeof chrome !== 'undefined') {
globalThis.browser = chrome;
}
// Prevent multiple injections - especially important for Firefox
if (typeof window.OpenDiaContentScriptLoaded !== 'undefined') {
console.log("OpenDia content script already loaded, skipping re-injection");
} else {
window.OpenDiaContentScriptLoaded = true;
console.log("OpenDia enhanced content script loaded"); console.log("OpenDia enhanced content script loaded");
// Enhanced Pattern Database with Twitter-First Priority // Enhanced Pattern Database with Twitter-First Priority
@ -228,7 +239,7 @@ class BrowserAutomation {
} }
setupMessageListener() { setupMessageListener() {
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
this.handleMessage(message) this.handleMessage(message)
.then(sendResponse) .then(sendResponse)
.catch((error) => { .catch((error) => {
@ -2882,3 +2893,5 @@ const THEME_PRESETS = {
// Initialize the automation system // Initialize the automation system
const browserAutomation = new BrowserAutomation(); const browserAutomation = new BrowserAutomation();
} // End of injection guard

File diff suppressed because one or more lines are too long

View File

@ -316,8 +316,8 @@
<div class="header"> <div class="header">
<div class="logo"> <div class="logo">
<video autoplay loop muted playsinline> <video autoplay loop muted playsinline>
<source src="logo.webm" type="video/webm"> <source src="../../logo.webm" type="video/webm">
<source src="logo.mp4" type="video/mp4"> <source src="../../logo.mp4" type="video/mp4">
<span>OD</span> <span>OD</span>
</video> </video>
</div> </div>

View File

@ -1,4 +1,13 @@
// OpenDia Popup // OpenDia Popup
// Import WebExtension polyfill for cross-browser compatibility
if (typeof browser === 'undefined' && typeof chrome !== 'undefined') {
globalThis.browser = chrome;
}
// Cross-browser compatibility layer
const runtimeAPI = browser.runtime;
const tabsAPI = browser.tabs;
const storageAPI = browser.storage;
let statusIndicator = document.getElementById("statusIndicator"); 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");
@ -14,9 +23,9 @@ function updateToolCount() {
"get_bookmarks", "add_bookmark", "get_history", "get_selected_text", "get_page_links" "get_bookmarks", "add_bookmark", "get_history", "get_selected_text", "get_page_links"
]; ];
if (chrome.runtime?.id) { if (runtimeAPI?.id) {
chrome.runtime.sendMessage({ action: "getToolCount" }, (response) => { runtimeAPI.sendMessage({ action: "getToolCount" }, (response) => {
if (!chrome.runtime.lastError && response?.toolCount) { if (!runtimeAPI.lastError && response?.toolCount) {
toolCount.innerHTML = `<span class="tooltip">${response.toolCount} toolCount.innerHTML = `<span class="tooltip">${response.toolCount}
<span class="tooltip-content">Available MCP Tools:\npage_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</span> <span class="tooltip-content">Available MCP Tools:\npage_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</span>
</span>`; </span>`;
@ -32,9 +41,9 @@ function updateToolCount() {
// Check connection status and get page info // Check connection status and get page info
function checkStatus() { function checkStatus() {
if (chrome.runtime?.id) { if (runtimeAPI?.id) {
chrome.runtime.sendMessage({ action: "getStatus" }, (response) => { runtimeAPI.sendMessage({ action: "getStatus" }, (response) => {
if (chrome.runtime.lastError) { if (runtimeAPI.lastError) {
updateStatus(false); updateStatus(false);
} else { } else {
updateStatus(response?.connected || false); updateStatus(response?.connected || false);
@ -45,7 +54,7 @@ function checkStatus() {
updateToolCount(); updateToolCount();
// Get current page info // Get current page info
chrome.tabs.query({active: true, currentWindow: true}, (tabs) => { tabsAPI.query({active: true, currentWindow: true}, (tabs) => {
if (tabs[0]) { if (tabs[0]) {
const url = new URL(tabs[0].url); const url = new URL(tabs[0].url);
currentPage.textContent = url.hostname; currentPage.textContent = url.hostname;
@ -62,9 +71,9 @@ setInterval(checkStatus, 2000);
// Update server URL display // Update server URL display
function updateServerUrl() { function updateServerUrl() {
if (chrome.runtime?.id) { if (runtimeAPI?.id) {
chrome.runtime.sendMessage({ action: "getPorts" }, (response) => { runtimeAPI.sendMessage({ action: "getPorts" }, (response) => {
if (!chrome.runtime.lastError && response?.websocketUrl) { if (!runtimeAPI.lastError && response?.websocketUrl) {
serverUrl.textContent = response.websocketUrl; serverUrl.textContent = response.websocketUrl;
} }
}); });
@ -90,9 +99,9 @@ function updateStatus(connected) {
// Reconnect button // Reconnect button
document.getElementById("reconnectBtn").addEventListener("click", () => { document.getElementById("reconnectBtn").addEventListener("click", () => {
if (chrome.runtime?.id) { if (runtimeAPI?.id) {
chrome.runtime.sendMessage({ action: "reconnect" }, (response) => { runtimeAPI.sendMessage({ action: "reconnect" }, (response) => {
if (!chrome.runtime.lastError) { if (!runtimeAPI.lastError) {
setTimeout(checkStatus, 1000); setTimeout(checkStatus, 1000);
} }
}); });
@ -104,7 +113,7 @@ document.getElementById("reconnectBtn").addEventListener("click", () => {
const safetyModeToggle = document.getElementById("safetyMode"); const safetyModeToggle = document.getElementById("safetyMode");
// Load safety mode state from storage // Load safety mode state from storage
chrome.storage.local.get(['safetyMode'], (result) => { storageAPI.local.get(['safetyMode'], (result) => {
const safetyEnabled = result.safetyMode || false; // Default to false (safety off) const safetyEnabled = result.safetyMode || false; // Default to false (safety off)
safetyModeToggle.checked = safetyEnabled; safetyModeToggle.checked = safetyEnabled;
}); });
@ -114,17 +123,17 @@ safetyModeToggle.addEventListener('change', () => {
const safetyEnabled = safetyModeToggle.checked; const safetyEnabled = safetyModeToggle.checked;
// Save to storage // Save to storage
chrome.storage.local.set({ safetyMode: safetyEnabled }); storageAPI.local.set({ safetyMode: safetyEnabled });
// Notify background script // Notify background script
chrome.runtime.sendMessage({ runtimeAPI.sendMessage({
action: "setSafetyMode", action: "setSafetyMode",
enabled: safetyEnabled enabled: safetyEnabled
}); });
}); });
// Listen for updates from background script // Listen for updates from background script
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { runtimeAPI.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === "statusUpdate") { if (message.type === "statusUpdate") {
updateStatus(message.connected); updateStatus(message.connected);
} }

View File

@ -0,0 +1,182 @@
// Simple test script to verify browser extension compatibility
const fs = require('fs');
const path = require('path');
function testManifestStructure(browser) {
console.log(`\n🔍 Testing ${browser} extension structure...`);
const buildDir = `dist/${browser}`;
const manifestPath = path.join(buildDir, 'manifest.json');
try {
// Read and parse manifest
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
// Check manifest version
console.log(` Manifest version: ${manifest.manifest_version}`);
// Check background configuration
if (browser === 'chrome') {
console.log(` Background: Service Worker (${manifest.background.service_worker})`);
console.log(` Action: ${manifest.action ? 'Present' : 'Missing'}`);
console.log(` Host permissions: ${manifest.host_permissions ? manifest.host_permissions.length : 0}`);
} else {
console.log(` Background: Scripts (${manifest.background.scripts.length} files)`);
console.log(` Browser action: ${manifest.browser_action ? 'Present' : 'Missing'}`);
console.log(` Gecko ID: ${manifest.applications?.gecko?.id || 'Not set'}`);
}
// Check permissions
console.log(` Permissions: ${manifest.permissions.length} total`);
// Check content scripts
console.log(` Content scripts: ${manifest.content_scripts?.length || 0} configured`);
// Check polyfill inclusion
const polyfillPath = path.join(buildDir, 'src/polyfill/browser-polyfill.min.js');
const polyfillExists = fs.existsSync(polyfillPath);
console.log(` WebExtension polyfill: ${polyfillExists ? 'Present' : 'Missing'}`);
// Check if polyfill is included in content scripts
const hasPolyfillInContent = manifest.content_scripts?.[0]?.js?.includes('src/polyfill/browser-polyfill.min.js');
console.log(` Polyfill in content scripts: ${hasPolyfillInContent ? 'Yes' : 'No'}`);
// Check if polyfill is included in background (Firefox only)
if (browser === 'firefox') {
const hasPolyfillInBackground = manifest.background?.scripts?.includes('src/polyfill/browser-polyfill.min.js');
console.log(` Polyfill in background: ${hasPolyfillInBackground ? 'Yes' : 'No'}`);
}
console.log(`${browser} extension structure looks good!`);
return true;
} catch (error) {
console.error(`❌ Error testing ${browser} extension:`, error.message);
return false;
}
}
function testBackgroundScript(browser) {
console.log(`\n🔍 Testing ${browser} background script...`);
const scriptPath = `dist/${browser}/src/background/background.js`;
try {
const script = fs.readFileSync(scriptPath, 'utf8');
// Check for browser polyfill usage
const usesBrowserAPI = script.includes('browser.') || script.includes('globalThis.browser');
console.log(` Uses browser API: ${usesBrowserAPI ? 'Yes' : 'No'}`);
// Check for connection manager
const hasConnectionManager = script.includes('ConnectionManager');
console.log(` Has connection manager: ${hasConnectionManager ? 'Yes' : 'No'}`);
// Check for browser detection
const hasBrowserDetection = script.includes('browserInfo') || script.includes('isFirefox') || script.includes('isServiceWorker');
console.log(` Has browser detection: ${hasBrowserDetection ? 'Yes' : 'No'}`);
// Check for WebSocket management
const hasWebSocketManagement = script.includes('WebSocket') && script.includes('connect');
console.log(` Has WebSocket management: ${hasWebSocketManagement ? 'Yes' : 'No'}`);
console.log(`${browser} background script looks good!`);
return true;
} catch (error) {
console.error(`❌ Error testing ${browser} background script:`, error.message);
return false;
}
}
function testContentScript(browser) {
console.log(`\n🔍 Testing ${browser} content script...`);
const scriptPath = `dist/${browser}/src/content/content.js`;
try {
const script = fs.readFileSync(scriptPath, 'utf8');
// Check for browser polyfill usage
const usesBrowserAPI = script.includes('browser.') || script.includes('globalThis.browser');
console.log(` Uses browser API: ${usesBrowserAPI ? 'Yes' : 'No'}`);
// Check for message handling
const hasMessageHandling = script.includes('onMessage') && script.includes('sendResponse');
console.log(` Has message handling: ${hasMessageHandling ? 'Yes' : 'No'}`);
console.log(`${browser} content script looks good!`);
return true;
} catch (error) {
console.error(`❌ Error testing ${browser} content script:`, error.message);
return false;
}
}
function testPopupScript(browser) {
console.log(`\n🔍 Testing ${browser} popup script...`);
const scriptPath = `dist/${browser}/src/popup/popup.js`;
try {
const script = fs.readFileSync(scriptPath, 'utf8');
// Check for browser polyfill usage
const usesBrowserAPI = script.includes('browser.') || script.includes('globalThis.browser');
console.log(` Uses browser API: ${usesBrowserAPI ? 'Yes' : 'No'}`);
// Check for API abstraction
const hasAPIAbstraction = script.includes('runtimeAPI') || script.includes('tabsAPI') || script.includes('storageAPI');
console.log(` Has API abstraction: ${hasAPIAbstraction ? 'Yes' : 'No'}`);
console.log(`${browser} popup script looks good!`);
return true;
} catch (error) {
console.error(`❌ Error testing ${browser} popup script:`, error.message);
return false;
}
}
function runAllTests() {
console.log('🚀 Testing cross-browser extension compatibility...\n');
const browsers = ['chrome', 'firefox'];
let allPassed = true;
for (const browser of browsers) {
console.log(`\n🌐 Testing ${browser.toUpperCase()} extension:`);
console.log('='.repeat(40));
const tests = [
testManifestStructure,
testBackgroundScript,
testContentScript,
testPopupScript
];
for (const test of tests) {
if (!test(browser)) {
allPassed = false;
}
}
}
console.log('\n' + '='.repeat(50));
if (allPassed) {
console.log('🎉 All tests passed! Cross-browser extension is ready.');
console.log('\n📦 Distribution packages:');
console.log(' Chrome: dist/opendia-chrome.zip');
console.log(' Firefox: dist/opendia-firefox.zip');
console.log('\n🧪 Manual testing:');
console.log(' 1. Chrome: Load dist/chrome in chrome://extensions');
console.log(' 2. Firefox: Load dist/firefox in about:debugging');
console.log(' 3. Both should connect to MCP server on localhost:5555/5556');
} else {
console.log('❌ Some tests failed. Please check the output above.');
}
}
// Run tests
runAllTests();

View File

@ -1,6 +1,6 @@
{ {
"name": "opendia", "name": "opendia",
"version": "1.0.5", "version": "1.0.6",
"description": "🎯 OpenDia - The open alternative to Dia. Connect your browser to AI models with anti-detection bypass for Twitter/X, LinkedIn, Facebook", "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", "main": "server.js",
"bin": { "bin": {

View File

@ -127,7 +127,7 @@ async function handlePortConflict(port, portName) {
const altPort = await findAvailablePort(port + 1); const altPort = await findAvailablePort(port + 1);
console.error(`🔄 Port ${port} still busy, using port ${altPort}`); console.error(`🔄 Port ${port} still busy, using port ${altPort}`);
if (portName === 'WebSocket') { if (portName === 'WebSocket') {
console.error(`💡 Update Chrome extension to: ws://localhost:${altPort}`); console.error(`💡 Update Chrome/Firefox extension to: ws://localhost:${altPort}`);
} }
return altPort; return altPort;
} else { } else {
@ -135,7 +135,7 @@ async function handlePortConflict(port, portName) {
const altPort = await findAvailablePort(port + 1); const altPort = await findAvailablePort(port + 1);
console.error(`🔄 ${portName} port ${port} busy (non-OpenDia), using port ${altPort}`); console.error(`🔄 ${portName} port ${port} busy (non-OpenDia), using port ${altPort}`);
if (portName === 'WebSocket') { if (portName === 'WebSocket') {
console.error(`💡 Update Chrome extension to: ws://localhost:${altPort}`); console.error(`💡 Update Chrome/Firefox extension to: ws://localhost:${altPort}`);
} }
return altPort; return altPort;
} }
@ -146,7 +146,7 @@ const app = express();
app.use(cors()); app.use(cors());
app.use(express.json()); app.use(express.json());
// WebSocket server for Chrome Extension (will be initialized after port conflict resolution) // WebSocket server for Chrome/Firefox Extension (will be initialized after port conflict resolution)
let wss = null; let wss = null;
let chromeExtensionSocket = null; let chromeExtensionSocket = null;
let availableTools = []; let availableTools = [];
@ -236,7 +236,7 @@ async function handleMCPRequest(request) {
content: [ content: [
{ {
type: "text", type: "text",
text: "❌ Chrome Extension not connected. Please install and activate the browser extension, then try again.\n\nSetup instructions:\n1. Go to chrome://extensions/\n2. Enable Developer mode\n3. Click 'Load unpacked' and select the extension folder\n4. Ensure the extension is active\n\n🎯 Features: Anti-detection bypass for Twitter/X, LinkedIn, Facebook + universal automation", text: "❌ Browser Extension not connected. Please install and activate the browser extension, then try again.\n\nSetup instructions:\n\nFor Chrome: \n1. Go to chrome://extensions/\n2. Enable Developer mode\n3. Click 'Load unpacked' and select the Chrome extension folder\n\nFor Firefox:\n1. Go to about:debugging#/runtime/this-firefox\n2. Click 'Load Temporary Add-on...'\n3. Select the manifest-firefox.json file\n\n🎯 Features: Anti-detection bypass for Twitter/X, LinkedIn, Facebook + universal automation",
}, },
], ],
isError: true, isError: true,
@ -1515,14 +1515,14 @@ function getFallbackTools() {
]; ];
} }
// Call browser tool through Chrome Extension // Call browser tool through Chrome/Firefox Extension
async function callBrowserTool(toolName, args) { async function callBrowserTool(toolName, args) {
if ( if (
!chromeExtensionSocket || !chromeExtensionSocket ||
chromeExtensionSocket.readyState !== WebSocket.OPEN chromeExtensionSocket.readyState !== WebSocket.OPEN
) { ) {
throw new Error( throw new Error(
"Chrome Extension not connected. Make sure the extension is installed and active." "Browser Extension not connected. Make sure the extension is installed and active."
); );
} }
@ -1549,7 +1549,7 @@ async function callBrowserTool(toolName, args) {
}); });
} }
// Handle tool responses from Chrome Extension // Handle tool responses from Chrome/Firefox Extension
function handleToolResponse(message) { function handleToolResponse(message) {
const pending = pendingCalls.get(message.id); const pending = pendingCalls.get(message.id);
if (pending) { if (pending) {
@ -1565,7 +1565,7 @@ function handleToolResponse(message) {
// Setup WebSocket connection handlers // Setup WebSocket connection handlers
function setupWebSocketHandlers() { function setupWebSocketHandlers() {
wss.on("connection", (ws) => { wss.on("connection", (ws) => {
console.error("Chrome Extension connected"); console.error("Browser Extension connected");
chromeExtensionSocket = ws; chromeExtensionSocket = ws;
// Set up ping/pong for keepalive // Set up ping/pong for keepalive
@ -1602,7 +1602,7 @@ function setupWebSocketHandlers() {
}); });
ws.on("close", () => { ws.on("close", () => {
console.error("Chrome Extension disconnected"); console.error("Browser Extension disconnected");
chromeExtensionSocket = null; chromeExtensionSocket = null;
availableTools = []; // Clear tools when extension disconnects availableTools = []; // Clear tools when extension disconnects
clearInterval(pingInterval); clearInterval(pingInterval);
@ -1732,7 +1732,7 @@ app.get('/health', (req, res) => {
}); });
}); });
// ADD: Port discovery endpoint for Chrome extension // ADD: Port discovery endpoint for Chrome/Firefox extension
app.get('/ports', (req, res) => { app.get('/ports', (req, res) => {
res.json({ res.json({
websocket: WS_PORT, websocket: WS_PORT,
@ -1782,7 +1782,7 @@ async function startServer() {
// Start HTTP server // Start HTTP server
const httpServer = app.listen(HTTP_PORT, () => { const httpServer = app.listen(HTTP_PORT, () => {
console.error(`🌐 HTTP/SSE server running on port ${HTTP_PORT}`); console.error(`🌐 HTTP/SSE server running on port ${HTTP_PORT}`);
console.error(`🔌 Chrome Extension connected on ws://localhost:${WS_PORT}`); console.error(`🔌 Browser Extension connected on ws://localhost:${WS_PORT}`);
console.error("🎯 Features: Anti-detection bypass + intelligent automation"); console.error("🎯 Features: Anti-detection bypass + intelligent automation");
}); });