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>
30
README.md
@ -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.
|
||||||
|
|
||||||
[](https://www.npmjs.com/package/opendia)
|
[](https://www.npmjs.com/package/opendia)
|
||||||
[](https://github.com/aaronjmars/opendia/releases/latest)
|
[](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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
37
build-dxt.sh
@ -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
@ -0,0 +1,2 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
312
opendia-extension/README.md
Normal 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
@ -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 };
|
||||||
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 6.0 KiB |
46
opendia-extension/manifest-chrome.json
Normal 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>"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
51
opendia-extension/manifest-firefox.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -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
28
opendia-extension/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
opendia-extension/releases/opendia-chrome-1.0.6.zip
Normal file
BIN
opendia-extension/releases/opendia-firefox-1.0.6.zip
Normal 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
|
||||||
@ -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
|
||||||
8
opendia-extension/src/polyfill/browser-polyfill.min.js
vendored
Normal 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>
|
||||||
@ -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);
|
||||||
}
|
}
|
||||||
182
opendia-extension/test-extension.js
Normal 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();
|
||||||
@ -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": {
|
||||||
|
|||||||
@ -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");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||