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">
|
||||
|
||||
**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://github.com/aaronjmars/opendia/releases/latest)
|
||||
@ -31,8 +31,9 @@ OpenDia lets AI models control your browser automatically. **The key advantage?
|
||||
|
||||
## 🌐 Browser Support
|
||||
|
||||
Works with **any Chromium-based browser**:
|
||||
- ✅ **Google Chrome**
|
||||
Works with **Chrome, Firefox, and any Chromium-based browser**:
|
||||
- ✅ **Mozilla Firefox** (Manifest V2)
|
||||
- ✅ **Google Chrome** (Manifest V3)
|
||||
- ✅ **Arc**
|
||||
- ✅ **Microsoft Edge**
|
||||
- ✅ **Brave**
|
||||
@ -79,10 +80,22 @@ Perfect for **Cursor users** who want to automate their local testing and develo
|
||||
## ⚡ Quick Start
|
||||
|
||||
### 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)
|
||||
3. Enable "Developer mode"
|
||||
4. Click "Load unpacked" and select the extension folder
|
||||
|
||||
**For Chrome/Chromium browsers:**
|
||||
1. Download `opendia-chrome-1.0.6.zip` from [releases](https://github.com/aaronjmars/opendia/releases)
|
||||
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
|
||||
|
||||
@ -272,7 +285,8 @@ npm install
|
||||
npm start
|
||||
|
||||
# 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
|
||||
```
|
||||
|
||||
|
||||
37
build-dxt.sh
@ -34,7 +34,7 @@ cp opendia-mcp/server.js dist/opendia-dxt/
|
||||
cat > dist/opendia-dxt/package.json << 'EOF'
|
||||
{
|
||||
"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",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
@ -47,6 +47,7 @@ cat > dist/opendia-dxt/package.json << 'EOF'
|
||||
"ai",
|
||||
"claude",
|
||||
"chrome",
|
||||
"firefox",
|
||||
"extension",
|
||||
"twitter",
|
||||
"linkedin",
|
||||
@ -115,7 +116,7 @@ cat > dist/opendia-dxt/manifest.json << 'EOF'
|
||||
"dxt_version": "0.1",
|
||||
"name": "opendia",
|
||||
"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",
|
||||
"author": {
|
||||
"name": "Aaron Elijah Mars",
|
||||
@ -124,7 +125,7 @@ cat > dist/opendia-dxt/manifest.json << 'EOF'
|
||||
},
|
||||
"homepage": "https://github.com/aaronjmars/opendia",
|
||||
"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",
|
||||
"icons": {
|
||||
"128": "icon.png"
|
||||
@ -150,7 +151,7 @@ cat > dist/opendia-dxt/manifest.json << 'EOF'
|
||||
"ws_port": {
|
||||
"type": "number",
|
||||
"title": "WebSocket Port",
|
||||
"description": "Port for Chrome extension connection",
|
||||
"description": "Port for Chrome/Firefox extension connection",
|
||||
"default": 5555,
|
||||
"minimum": 1024,
|
||||
"maximum": 65535
|
||||
@ -267,10 +268,10 @@ cat > dist/opendia-dxt/manifest.json << 'EOF'
|
||||
},
|
||||
|
||||
"requirements": {
|
||||
"chrome_extension": {
|
||||
"browser_extension": {
|
||||
"name": "OpenDia Browser Extension",
|
||||
"description": "Required Chrome extension for browser automation by Aaron Elijah Mars",
|
||||
"version": "1.0.5",
|
||||
"description": "Required Chrome/Firefox extension for browser automation by Aaron Elijah Mars",
|
||||
"version": "1.0.6",
|
||||
"auto_install": false
|
||||
}
|
||||
}
|
||||
@ -292,10 +293,12 @@ cp LICENSE dist/opendia-dxt/ 2>/dev/null || echo "⚠️ LICENSE not found, ski
|
||||
|
||||
# Create extension installation guide
|
||||
cat > dist/opendia-dxt/EXTENSION_INSTALL.md << 'EOF'
|
||||
# OpenDia Chrome Extension Installation
|
||||
# OpenDia Browser Extension Installation
|
||||
|
||||
## Quick Setup
|
||||
|
||||
### For Chrome/Chromium Browsers
|
||||
|
||||
1. **Enable Developer Mode**
|
||||
- Go to `chrome://extensions/`
|
||||
- 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
|
||||
- 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
|
||||
- Should show "Connected to MCP server"
|
||||
- Green status indicator means ready to use
|
||||
|
||||
## 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
|
||||
|
||||
## Features
|
||||
@ -375,6 +390,6 @@ echo ""
|
||||
echo "🚀 Installation:"
|
||||
echo "1. Double-click the .dxt file"
|
||||
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 "🎯 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,
|
||||
"name": "OpenDia",
|
||||
"version": "1.0.5",
|
||||
"description": "Browser automation through Model Context Protocol",
|
||||
"version": "1.0.6",
|
||||
"description": "Connect your browser to AI models",
|
||||
"icons": {
|
||||
"16": "icon-16.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
|
||||
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
|
||||
|
||||
// Safety Mode configuration
|
||||
@ -13,10 +25,205 @@ const WRITE_EDIT_TOOLS = [
|
||||
];
|
||||
|
||||
// Load safety mode state on startup
|
||||
chrome.storage.local.get(['safetyMode'], (result) => {
|
||||
browser.storage.local.get(['safetyMode'], (result) => {
|
||||
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
|
||||
async function ensureContentScriptReady(tabId, retries = 3) {
|
||||
for (let attempt = 1; attempt <= retries; attempt++) {
|
||||
@ -27,10 +234,10 @@ async function ensureContentScriptReady(tabId, retries = 3) {
|
||||
reject(new Error('Content script ping timeout'));
|
||||
}, 2000);
|
||||
|
||||
chrome.tabs.sendMessage(tabId, { action: 'ping' }, (response) => {
|
||||
browser.tabs.sendMessage(tabId, { action: 'ping' }, (response) => {
|
||||
clearTimeout(timeout);
|
||||
if (chrome.runtime.lastError) {
|
||||
reject(new Error(chrome.runtime.lastError.message));
|
||||
if (browser.runtime.lastError) {
|
||||
reject(new Error(browser.runtime.lastError.message));
|
||||
} else {
|
||||
resolve(response);
|
||||
}
|
||||
@ -47,7 +254,7 @@ async function ensureContentScriptReady(tabId, retries = 3) {
|
||||
if (attempt === retries) {
|
||||
// Last attempt - try to inject content script
|
||||
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.)
|
||||
if (!isInjectableUrl(tab.url)) {
|
||||
@ -55,10 +262,33 @@ async function ensureContentScriptReady(tabId, retries = 3) {
|
||||
}
|
||||
|
||||
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 },
|
||||
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
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
@ -66,10 +296,10 @@ async function ensureContentScriptReady(tabId, retries = 3) {
|
||||
// Test again
|
||||
const testResponse = await new Promise((resolve, reject) => {
|
||||
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);
|
||||
if (chrome.runtime.lastError) {
|
||||
reject(new Error(chrome.runtime.lastError.message));
|
||||
if (browser.runtime.lastError) {
|
||||
reject(new Error(browser.runtime.lastError.message));
|
||||
} else {
|
||||
resolve(response);
|
||||
}
|
||||
@ -100,15 +330,15 @@ async function ensureContentScriptReady(tabId, retries = 3) {
|
||||
function isInjectableUrl(url) {
|
||||
if (!url) return false;
|
||||
|
||||
const restrictedProtocols = ['chrome:', 'chrome-extension:', 'chrome-devtools:', 'edge:', 'moz-extension:'];
|
||||
const restrictedDomains = ['chrome.google.com'];
|
||||
const restrictedProtocols = ['chrome:', 'chrome-extension:', 'chrome-devtools:', 'edge:', 'moz-extension:', 'about:'];
|
||||
const restrictedDomains = ['chrome.google.com', 'addons.mozilla.org'];
|
||||
|
||||
// Check protocol
|
||||
if (restrictedProtocols.some(protocol => url.startsWith(protocol))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check special Chrome pages
|
||||
// Check special browser pages
|
||||
if (url.startsWith('https://chrome.google.com/webstore') ||
|
||||
url.includes('chrome://') ||
|
||||
restrictedDomains.some(domain => url.includes(domain))) {
|
||||
@ -121,7 +351,7 @@ function isInjectableUrl(url) {
|
||||
// Get content script readiness status for a tab
|
||||
async function getTabContentScriptStatus(tabId) {
|
||||
try {
|
||||
const tab = await chrome.tabs.get(tabId);
|
||||
const tab = await browser.tabs.get(tabId);
|
||||
|
||||
if (!isInjectableUrl(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 timeout = setTimeout(() => resolve(null), 1000);
|
||||
chrome.tabs.sendMessage(tabId, { action: 'ping' }, (response) => {
|
||||
browser.tabs.sendMessage(tabId, { action: 'ping' }, (response) => {
|
||||
clearTimeout(timeout);
|
||||
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
|
||||
function getAvailableTools() {
|
||||
return [
|
||||
@ -839,6 +993,9 @@ async function handleMCPRequest(message) {
|
||||
const { id, method, params } = message;
|
||||
|
||||
try {
|
||||
// Ensure connection for Chrome service workers
|
||||
await connectionManager.ensureConnection();
|
||||
|
||||
// Safety Mode check: Block write/edit tools if safety mode is enabled
|
||||
if (safetyModeEnabled && WRITE_EDIT_TOOLS.includes(method)) {
|
||||
const targetInfo = params.tab_id ? `tab ${params.tab_id}` : 'the current page';
|
||||
@ -913,23 +1070,19 @@ async function handleMCPRequest(message) {
|
||||
}
|
||||
|
||||
// Send success response
|
||||
mcpSocket.send(
|
||||
JSON.stringify({
|
||||
connectionManager.send({
|
||||
id,
|
||||
result,
|
||||
})
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
// Send error response
|
||||
mcpSocket.send(
|
||||
JSON.stringify({
|
||||
connectionManager.send({
|
||||
id,
|
||||
error: {
|
||||
message: error.message,
|
||||
code: -32603,
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -940,13 +1093,13 @@ async function sendToContentScript(action, data, targetTabId = null) {
|
||||
if (targetTabId) {
|
||||
// Use specific tab
|
||||
try {
|
||||
targetTab = await chrome.tabs.get(targetTabId);
|
||||
targetTab = await browser.tabs.get(targetTabId);
|
||||
} catch (error) {
|
||||
throw new Error(`Tab ${targetTabId} not found or inaccessible`);
|
||||
}
|
||||
} else {
|
||||
// Fallback to active tab (maintains compatibility)
|
||||
const [activeTab] = await chrome.tabs.query({
|
||||
const [activeTab] = await browser.tabs.query({
|
||||
active: true,
|
||||
currentWindow: true,
|
||||
});
|
||||
@ -961,9 +1114,9 @@ async function sendToContentScript(action, data, targetTabId = null) {
|
||||
await ensureContentScriptReady(targetTab.id);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
chrome.tabs.sendMessage(targetTab.id, { action, data }, (response) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
reject(new Error(`Tab ${targetTab.id}: ${chrome.runtime.lastError.message}`));
|
||||
browser.tabs.sendMessage(targetTab.id, { action, data }, (response) => {
|
||||
if (browser.runtime.lastError) {
|
||||
reject(new Error(`Tab ${targetTab.id}: ${browser.runtime.lastError.message}`));
|
||||
} else if (response && response.success) {
|
||||
resolve(response.data);
|
||||
} else {
|
||||
@ -974,12 +1127,12 @@ async function sendToContentScript(action, data, targetTabId = null) {
|
||||
}
|
||||
|
||||
async function navigateToUrl(url, waitFor, timeout = 10000) {
|
||||
const [activeTab] = await chrome.tabs.query({
|
||||
const [activeTab] = await browser.tabs.query({
|
||||
active: 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) {
|
||||
@ -998,7 +1151,7 @@ async function waitForElement(tabId, selector, timeout = 5000) {
|
||||
|
||||
while (Date.now() - startTime < timeout) {
|
||||
try {
|
||||
const result = await chrome.tabs.sendMessage(tabId, {
|
||||
const result = await browser.tabs.sendMessage(tabId, {
|
||||
action: 'wait_for',
|
||||
data: {
|
||||
condition_type: 'element_visible',
|
||||
@ -1115,7 +1268,7 @@ async function createSingleTab(url, active, wait_for, timeout) {
|
||||
}
|
||||
|
||||
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 });
|
||||
|
||||
// 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
|
||||
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 });
|
||||
|
||||
// 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
|
||||
const shouldActivate = active && isLastTab;
|
||||
|
||||
const tab = await chrome.tabs.create({
|
||||
const tab = await browser.tabs.create({
|
||||
url: url,
|
||||
active: shouldActivate
|
||||
});
|
||||
|
||||
// Wait a moment and check actual URL
|
||||
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({
|
||||
tab_id: tab.id,
|
||||
@ -1354,7 +1507,7 @@ async function closeTabs(params) {
|
||||
tabsToClose = [tab_id];
|
||||
} else {
|
||||
// Close current tab
|
||||
const [activeTab] = await chrome.tabs.query({
|
||||
const [activeTab] = await browser.tabs.query({
|
||||
active: true,
|
||||
currentWindow: true,
|
||||
});
|
||||
@ -1368,7 +1521,7 @@ async function closeTabs(params) {
|
||||
}
|
||||
|
||||
// Close tabs
|
||||
await chrome.tabs.remove(tabsToClose);
|
||||
await browser.tabs.remove(tabsToClose);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
@ -1389,7 +1542,7 @@ async function listTabs(params) {
|
||||
queryOptions.currentWindow = true;
|
||||
}
|
||||
|
||||
const tabs = await chrome.tabs.query(queryOptions);
|
||||
const tabs = await browser.tabs.query(queryOptions);
|
||||
|
||||
// Check content script status if requested
|
||||
const contentScriptStatuses = new Map();
|
||||
@ -1469,17 +1622,17 @@ async function listTabs(params) {
|
||||
|
||||
async function switchToTab(tabId) {
|
||||
// First, get tab info to ensure it exists
|
||||
const tab = await chrome.tabs.get(tabId);
|
||||
const tab = await browser.tabs.get(tabId);
|
||||
|
||||
if (!tab) {
|
||||
throw new Error(`Tab with ID ${tabId} not found`);
|
||||
}
|
||||
|
||||
// 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
|
||||
await chrome.windows.update(tab.windowId, { focused: true });
|
||||
await browser.windows.update(tab.windowId, { focused: true });
|
||||
|
||||
return {
|
||||
success: true,
|
||||
@ -1496,9 +1649,9 @@ async function getBookmarks(params) {
|
||||
|
||||
let bookmarks;
|
||||
if (query) {
|
||||
bookmarks = await chrome.bookmarks.search(query);
|
||||
bookmarks = await browser.bookmarks.search(query);
|
||||
} else {
|
||||
bookmarks = await chrome.bookmarks.getTree();
|
||||
bookmarks = await browser.bookmarks.getTree();
|
||||
}
|
||||
|
||||
return {
|
||||
@ -1511,7 +1664,7 @@ async function getBookmarks(params) {
|
||||
async function addBookmark(params) {
|
||||
const { title, url, parentId } = params;
|
||||
|
||||
const bookmark = await chrome.bookmarks.create({
|
||||
const bookmark = await browser.bookmarks.create({
|
||||
title,
|
||||
url,
|
||||
parentId
|
||||
@ -1537,7 +1690,7 @@ async function getHistory(params) {
|
||||
} = params;
|
||||
|
||||
try {
|
||||
// Chrome History API search configuration
|
||||
// Browser History API search configuration
|
||||
const searchQuery = {
|
||||
text: keywords,
|
||||
maxResults: Math.min(max_results * 3, 1000), // Over-fetch for filtering
|
||||
@ -1552,7 +1705,7 @@ async function getHistory(params) {
|
||||
}
|
||||
|
||||
// Execute history search
|
||||
const historyItems = await chrome.history.search(searchQuery);
|
||||
const historyItems = await browser.history.search(searchQuery);
|
||||
|
||||
// Apply advanced filters
|
||||
let filteredItems = historyItems.filter(item => {
|
||||
@ -1679,7 +1832,7 @@ async function getSelectedText(params) {
|
||||
if (tab_id) {
|
||||
// Use specific tab
|
||||
try {
|
||||
targetTab = await chrome.tabs.get(tab_id);
|
||||
targetTab = await browser.tabs.get(tab_id);
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
@ -1692,7 +1845,7 @@ async function getSelectedText(params) {
|
||||
}
|
||||
} else {
|
||||
// Get the active tab
|
||||
const [activeTab] = await chrome.tabs.query({
|
||||
const [activeTab] = await browser.tabs.query({
|
||||
active: true,
|
||||
currentWindow: true,
|
||||
});
|
||||
@ -1710,69 +1863,22 @@ async function getSelectedText(params) {
|
||||
targetTab = activeTab;
|
||||
}
|
||||
|
||||
// Execute script to get selected text
|
||||
const results = await chrome.scripting.executeScript({
|
||||
// Execute script to get selected text - handle browser differences
|
||||
let results;
|
||||
if (browser.scripting) {
|
||||
// Chrome MV3
|
||||
results = await browser.scripting.executeScript({
|
||||
target: { tabId: targetTab.id },
|
||||
func: () => {
|
||||
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
|
||||
};
|
||||
}
|
||||
func: getSelectionFunction
|
||||
});
|
||||
} 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) {
|
||||
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)
|
||||
setTimeout(() => {
|
||||
connectToMCPServer();
|
||||
connectionManager.connect();
|
||||
}, 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
|
||||
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||
browser.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||
if (request.action === "getStatus") {
|
||||
sendResponse({
|
||||
connected: mcpSocket && mcpSocket.readyState === WebSocket.OPEN,
|
||||
});
|
||||
sendResponse(connectionManager.getStatus());
|
||||
} else if (request.action === "getToolCount") {
|
||||
const tools = getAvailableTools();
|
||||
sendResponse({
|
||||
@ -1872,7 +2028,7 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||
tools: tools.map(t => t.name)
|
||||
});
|
||||
} else if (request.action === "reconnect") {
|
||||
connectToMCPServer();
|
||||
connectionManager.connect();
|
||||
sendResponse({ success: true });
|
||||
} else if (request.action === "getPorts") {
|
||||
sendResponse({
|
||||
@ -1884,9 +2040,7 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||
console.log(`🛡️ Safety Mode ${safetyModeEnabled ? 'ENABLED' : 'DISABLED'}`);
|
||||
sendResponse({ success: true });
|
||||
} else if (request.action === "test") {
|
||||
if (mcpSocket && mcpSocket.readyState === WebSocket.OPEN) {
|
||||
mcpSocket.send(JSON.stringify({ type: "test", timestamp: Date.now() }));
|
||||
}
|
||||
connectionManager.send({ type: "test", timestamp: Date.now() });
|
||||
sendResponse({ success: true });
|
||||
}
|
||||
return true; // Keep the message channel open
|
||||
@ -1,4 +1,15 @@
|
||||
// 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");
|
||||
|
||||
// Enhanced Pattern Database with Twitter-First Priority
|
||||
@ -228,7 +239,7 @@ class BrowserAutomation {
|
||||
}
|
||||
|
||||
setupMessageListener() {
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
this.handleMessage(message)
|
||||
.then(sendResponse)
|
||||
.catch((error) => {
|
||||
@ -2882,3 +2893,5 @@ const THEME_PRESETS = {
|
||||
|
||||
// Initialize the automation system
|
||||
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="logo">
|
||||
<video autoplay loop muted playsinline>
|
||||
<source src="logo.webm" type="video/webm">
|
||||
<source src="logo.mp4" type="video/mp4">
|
||||
<source src="../../logo.webm" type="video/webm">
|
||||
<source src="../../logo.mp4" type="video/mp4">
|
||||
<span>OD</span>
|
||||
</video>
|
||||
</div>
|
||||
@ -1,4 +1,13 @@
|
||||
// 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 statusText = document.getElementById("statusText");
|
||||
let toolCount = document.getElementById("toolCount");
|
||||
@ -14,9 +23,9 @@ function updateToolCount() {
|
||||
"get_bookmarks", "add_bookmark", "get_history", "get_selected_text", "get_page_links"
|
||||
];
|
||||
|
||||
if (chrome.runtime?.id) {
|
||||
chrome.runtime.sendMessage({ action: "getToolCount" }, (response) => {
|
||||
if (!chrome.runtime.lastError && response?.toolCount) {
|
||||
if (runtimeAPI?.id) {
|
||||
runtimeAPI.sendMessage({ action: "getToolCount" }, (response) => {
|
||||
if (!runtimeAPI.lastError && 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>`;
|
||||
@ -32,9 +41,9 @@ function updateToolCount() {
|
||||
|
||||
// Check connection status and get page info
|
||||
function checkStatus() {
|
||||
if (chrome.runtime?.id) {
|
||||
chrome.runtime.sendMessage({ action: "getStatus" }, (response) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
if (runtimeAPI?.id) {
|
||||
runtimeAPI.sendMessage({ action: "getStatus" }, (response) => {
|
||||
if (runtimeAPI.lastError) {
|
||||
updateStatus(false);
|
||||
} else {
|
||||
updateStatus(response?.connected || false);
|
||||
@ -45,7 +54,7 @@ function checkStatus() {
|
||||
updateToolCount();
|
||||
|
||||
// Get current page info
|
||||
chrome.tabs.query({active: true, currentWindow: true}, (tabs) => {
|
||||
tabsAPI.query({active: true, currentWindow: true}, (tabs) => {
|
||||
if (tabs[0]) {
|
||||
const url = new URL(tabs[0].url);
|
||||
currentPage.textContent = url.hostname;
|
||||
@ -62,9 +71,9 @@ setInterval(checkStatus, 2000);
|
||||
|
||||
// Update server URL display
|
||||
function updateServerUrl() {
|
||||
if (chrome.runtime?.id) {
|
||||
chrome.runtime.sendMessage({ action: "getPorts" }, (response) => {
|
||||
if (!chrome.runtime.lastError && response?.websocketUrl) {
|
||||
if (runtimeAPI?.id) {
|
||||
runtimeAPI.sendMessage({ action: "getPorts" }, (response) => {
|
||||
if (!runtimeAPI.lastError && response?.websocketUrl) {
|
||||
serverUrl.textContent = response.websocketUrl;
|
||||
}
|
||||
});
|
||||
@ -90,9 +99,9 @@ function updateStatus(connected) {
|
||||
|
||||
// Reconnect button
|
||||
document.getElementById("reconnectBtn").addEventListener("click", () => {
|
||||
if (chrome.runtime?.id) {
|
||||
chrome.runtime.sendMessage({ action: "reconnect" }, (response) => {
|
||||
if (!chrome.runtime.lastError) {
|
||||
if (runtimeAPI?.id) {
|
||||
runtimeAPI.sendMessage({ action: "reconnect" }, (response) => {
|
||||
if (!runtimeAPI.lastError) {
|
||||
setTimeout(checkStatus, 1000);
|
||||
}
|
||||
});
|
||||
@ -104,7 +113,7 @@ document.getElementById("reconnectBtn").addEventListener("click", () => {
|
||||
const safetyModeToggle = document.getElementById("safetyMode");
|
||||
|
||||
// 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)
|
||||
safetyModeToggle.checked = safetyEnabled;
|
||||
});
|
||||
@ -114,17 +123,17 @@ safetyModeToggle.addEventListener('change', () => {
|
||||
const safetyEnabled = safetyModeToggle.checked;
|
||||
|
||||
// Save to storage
|
||||
chrome.storage.local.set({ safetyMode: safetyEnabled });
|
||||
storageAPI.local.set({ safetyMode: safetyEnabled });
|
||||
|
||||
// Notify background script
|
||||
chrome.runtime.sendMessage({
|
||||
runtimeAPI.sendMessage({
|
||||
action: "setSafetyMode",
|
||||
enabled: safetyEnabled
|
||||
});
|
||||
});
|
||||
|
||||
// Listen for updates from background script
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
runtimeAPI.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (message.type === "statusUpdate") {
|
||||
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",
|
||||
"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",
|
||||
"main": "server.js",
|
||||
"bin": {
|
||||
|
||||
@ -127,7 +127,7 @@ async function handlePortConflict(port, portName) {
|
||||
const altPort = await findAvailablePort(port + 1);
|
||||
console.error(`🔄 Port ${port} still busy, using port ${altPort}`);
|
||||
if (portName === 'WebSocket') {
|
||||
console.error(`💡 Update Chrome extension to: ws://localhost:${altPort}`);
|
||||
console.error(`💡 Update Chrome/Firefox extension to: ws://localhost:${altPort}`);
|
||||
}
|
||||
return altPort;
|
||||
} else {
|
||||
@ -135,7 +135,7 @@ async function handlePortConflict(port, portName) {
|
||||
const altPort = await findAvailablePort(port + 1);
|
||||
console.error(`🔄 ${portName} port ${port} busy (non-OpenDia), using port ${altPort}`);
|
||||
if (portName === 'WebSocket') {
|
||||
console.error(`💡 Update Chrome extension to: ws://localhost:${altPort}`);
|
||||
console.error(`💡 Update Chrome/Firefox extension to: ws://localhost:${altPort}`);
|
||||
}
|
||||
return altPort;
|
||||
}
|
||||
@ -146,7 +146,7 @@ const app = express();
|
||||
app.use(cors());
|
||||
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 chromeExtensionSocket = null;
|
||||
let availableTools = [];
|
||||
@ -236,7 +236,7 @@ async function handleMCPRequest(request) {
|
||||
content: [
|
||||
{
|
||||
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,
|
||||
@ -1515,14 +1515,14 @@ function getFallbackTools() {
|
||||
];
|
||||
}
|
||||
|
||||
// Call browser tool through Chrome Extension
|
||||
// Call browser tool through Chrome/Firefox Extension
|
||||
async function callBrowserTool(toolName, args) {
|
||||
if (
|
||||
!chromeExtensionSocket ||
|
||||
chromeExtensionSocket.readyState !== WebSocket.OPEN
|
||||
) {
|
||||
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) {
|
||||
const pending = pendingCalls.get(message.id);
|
||||
if (pending) {
|
||||
@ -1565,7 +1565,7 @@ function handleToolResponse(message) {
|
||||
// Setup WebSocket connection handlers
|
||||
function setupWebSocketHandlers() {
|
||||
wss.on("connection", (ws) => {
|
||||
console.error("Chrome Extension connected");
|
||||
console.error("Browser Extension connected");
|
||||
chromeExtensionSocket = ws;
|
||||
|
||||
// Set up ping/pong for keepalive
|
||||
@ -1602,7 +1602,7 @@ function setupWebSocketHandlers() {
|
||||
});
|
||||
|
||||
ws.on("close", () => {
|
||||
console.error("Chrome Extension disconnected");
|
||||
console.error("Browser Extension disconnected");
|
||||
chromeExtensionSocket = null;
|
||||
availableTools = []; // Clear tools when extension disconnects
|
||||
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) => {
|
||||
res.json({
|
||||
websocket: WS_PORT,
|
||||
@ -1782,7 +1782,7 @@ async function startServer() {
|
||||
// Start HTTP server
|
||||
const httpServer = app.listen(HTTP_PORT, () => {
|
||||
console.error(`🌐 HTTP/SSE server running on port ${HTTP_PORT}`);
|
||||
console.error(`🔌 Chrome Extension connected on ws://localhost:${WS_PORT}`);
|
||||
console.error(`🔌 Browser Extension connected on ws://localhost:${WS_PORT}`);
|
||||
console.error("🎯 Features: Anti-detection bypass + intelligent automation");
|
||||
});
|
||||
|
||||
|
||||