diff --git a/packages/browseros-agent/CLAUDE.md b/packages/browseros-agent/CLAUDE.md index 039e4a4c2..0e14eeb4b 100644 --- a/packages/browseros-agent/CLAUDE.md +++ b/packages/browseros-agent/CLAUDE.md @@ -32,7 +32,7 @@ Use **kebab-case** for all file and folder names: | Multi-word files | kebab-case | `gemini-agent.ts`, `mcp-context.ts` | | Single-word files | lowercase | `types.ts`, `browser.ts`, `index.ts` | | Test files | `.test.ts` suffix | `mcp-context.test.ts` | -| Folders | kebab-case | `controller-server/`, `rate-limiter/` | +| Folders | kebab-case | `rate-limiter/`, `browser-tools/` | Classes remain PascalCase in code, but live in kebab-case files: ```typescript @@ -97,21 +97,16 @@ The main MCP server that exposes browser automation tools via HTTP/SSE. **Key components:** - `src/tools/` - MCP tool definitions, split into: - - `cdp-based/` - Tools using Chrome DevTools Protocol (network, console, emulation, input, etc.) - - `controller-based/` - Tools using the browser extension (navigation, clicks, screenshots, tabs, history, bookmarks) -- `src/controller-server/` - WebSocket server that bridges to the browser extension - - `ControllerBridge` handles WebSocket connections with extension clients - - `ControllerContext` wraps the bridge for tool handlers + - `cdp-based/` - Tools using Chrome DevTools Protocol (navigation, DOM interaction, network, console, emulation, input, etc.) - `src/common/` - Shared utilities (McpContext, PageCollector, browser connection, identity, db) - `src/agent/` - AI agent functionality (Gemini adapter, rate limiting, session management) - `src/http/` - Hono HTTP server with MCP, health, and provider routes **Tool types:** - CDP tools require a direct CDP connection (`--cdp-port`) -- Controller tools work via the browser extension over WebSocket ### Shared (`packages/shared`) -Shared constants, types, and configuration used by both server and extension. Avoids magic numbers. +Shared constants, types, and configuration used across packages. Avoids magic numbers. **Structure:** - `src/constants/` - Configuration values (ports, timeouts, limits, urls, paths) @@ -119,22 +114,12 @@ Shared constants, types, and configuration used by both server and extension. Av **Exports:** `@browseros/shared/constants/*`, `@browseros/shared/types/*` -### Controller Extension (`apps/controller-ext`) -Chrome extension that receives commands from the server via WebSocket. - -**Entry point:** `src/background/index.ts` → `BrowserOSController` - -**Structure:** -- `src/actions/` - Action handlers organized by domain (browser/, tab/, bookmark/, history/) -- `src/adapters/` - Chrome API adapters (TabAdapter, BookmarkAdapter, HistoryAdapter) -- `src/websocket/` - WebSocket client that connects to the server - ### Communication Flow ``` AI Agent/MCP Client → HTTP Server (Hono) → Tool Handler ↓ - CDP (direct) ←── or ──→ WebSocket → Extension → Chrome APIs + CDP → BrowserOS / Chrome APIs ``` ## Creating Packages diff --git a/packages/browseros-agent/README.md b/packages/browseros-agent/README.md index 6bae0716e..3ffadc798 100644 --- a/packages/browseros-agent/README.md +++ b/packages/browseros-agent/README.md @@ -10,7 +10,6 @@ apps/ agent/ # Agent UI (Chrome extension) cli/ # Go CLI for controlling BrowserOS from the terminal eval/ # Evaluation framework for benchmarking agents - controller-ext/ # BrowserOS Controller (Chrome extension for chrome.* APIs) packages/ agent-sdk/ # Node.js SDK (@browseros-ai/agent-sdk) @@ -24,7 +23,6 @@ packages/ | `apps/agent` | Agent UI — Chrome extension for the chat interface | | `apps/cli` | Go CLI — control BrowserOS from the terminal or AI coding agents | | `apps/eval` | Benchmark framework — WebVoyager, Mind2Web evaluation | -| `apps/controller-ext` | BrowserOS Controller — bridges `chrome.*` APIs to the server via WebSocket | | `packages/agent-sdk` | Node.js SDK for browser automation with natural language | | `packages/cdp-protocol` | Auto-generated CDP type bindings used by the server | | `packages/shared` | Shared constants used across packages | @@ -33,7 +31,6 @@ packages/ - `apps/server`: Bun server which contains the agent loop and tools. - `apps/agent`: Agent UI (Chrome extension). -- `apps/controller-ext`: BrowserOS Controller - a Chrome extension that bridges `chrome.*` APIs to the server. Controller tools within the server communicate with this extension via WebSocket. ``` ┌──────────────────────────────────────────────────────────────────────────┐ @@ -51,19 +48,19 @@ packages/ │ /health ─── Health check │ │ │ │ Tools: │ -│ ├── CDP Tools (console, network, input, screenshot, ...) │ -│ └── Controller Tools (tabs, navigation, clicks, bookmarks, history) │ +│ └── CDP-backed browser tools (tabs, navigation, input, screenshots, │ +│ bookmarks, history, console, DOM, tab groups, windows, ...) │ └──────────────────────────────────────────────────────────────────────────┘ - │ │ - │ CDP (client) │ WebSocket (server) - ▼ ▼ -┌─────────────────────┐ ┌─────────────────────────────────────┐ -│ Chromium CDP │ │ BrowserOS Controller Extension │ -│ (cdpPort: 9000) │ │ (extensionPort: 9300) │ -│ │ │ │ -│ Server connects │ │ Bridges chrome.tabs, chrome.history │ -│ TO this as client │ │ chrome.bookmarks to the server │ -└─────────────────────┘ └─────────────────────────────────────┘ + │ + │ CDP (client) + ▼ + ┌─────────────────────┐ + │ Chromium CDP │ + │ (cdpPort: 9000) │ + │ │ + │ Server connects │ + │ TO this as client │ + └─────────────────────┘ ``` ### Ports @@ -72,7 +69,7 @@ packages/ |------|--------------|---------| | 9100 | `BROWSEROS_SERVER_PORT` | HTTP server - MCP endpoints, agent chat, health | | 9000 | `BROWSEROS_CDP_PORT` | Chromium CDP server (BrowserOS Server connects as client) | -| 9300 | `BROWSEROS_EXTENSION_PORT` | WebSocket server for controller extension | +| 9300 | `BROWSEROS_EXTENSION_PORT` | Legacy BrowserOS launch arg kept for compatibility; not used by the server | ## Development @@ -96,9 +93,8 @@ process-compose up The `process-compose up` command runs the following in order: 1. `bun install` — installs dependencies -2. `bun --cwd apps/controller-ext build` — builds the controller extension -3. `bun --cwd apps/agent codegen` — generates agent code -4. `bun --cwd apps/server start` and `bun --cwd apps/agent dev` — starts server and agent in parallel +2. `bun --cwd apps/agent codegen` — generates agent code +3. `bun --cwd apps/server start` and `bun --cwd apps/agent dev` — starts server and agent in parallel ### Environment Variables @@ -114,7 +110,7 @@ Runtime uses `.env.development`, while production artifact builds use `.env.prod |----------|---------|-------------| | `BROWSEROS_SERVER_PORT` | 9100 | HTTP server port (MCP, chat, health) | | `BROWSEROS_CDP_PORT` | 9000 | Chromium CDP port (server connects as client) | -| `BROWSEROS_EXTENSION_PORT` | 9300 | WebSocket port for controller extension | +| `BROWSEROS_EXTENSION_PORT` | 9300 | Legacy BrowserOS launch arg kept for compatibility | | `BROWSEROS_CONFIG_URL` | - | Remote config endpoint for rate limits | | `BROWSEROS_INSTALL_ID` | - | Unique installation identifier (analytics) | | `BROWSEROS_CLIENT_ID` | - | Client identifier (analytics) | @@ -146,7 +142,7 @@ Copy from `apps/server/.env.production.example` before running `build:server`. |----------|---------|-------------| | `BROWSEROS_SERVER_PORT` | 9100 | Passed to BrowserOS via CLI args | | `BROWSEROS_CDP_PORT` | 9000 | Passed to BrowserOS via CLI args | -| `BROWSEROS_EXTENSION_PORT` | 9300 | Passed to BrowserOS via CLI args | +| `BROWSEROS_EXTENSION_PORT` | 9300 | Legacy BrowserOS CLI arg still passed for compatibility | | `VITE_BROWSEROS_SERVER_PORT` | 9100 | Agent UI connects to server (must match `BROWSEROS_SERVER_PORT`) | | `BROWSEROS_BINARY` | - | Path to BrowserOS binary | | `USE_BROWSEROS_BINARY` | true | Use BrowserOS instead of default Chrome | @@ -163,15 +159,13 @@ bun run start:server # Start the server bun run start:agent # Start agent extension (dev mode) # Build -bun run build # Build server, agent, and controller extension +bun run build # Build server and agent bun run build:server # Build production server resource artifacts and upload zips to R2 bun run build:agent # Build agent extension -bun run build:ext # Build controller extension # Test bun run test # Run standard tests bun run test:cdp # Run CDP-based tests -bun run test:controller # Run controller-based tests bun run test:integration # Run integration tests # Quality diff --git a/packages/browseros-agent/apps/agent/web-ext.config.ts b/packages/browseros-agent/apps/agent/web-ext.config.ts index 4fad4b345..088d379af 100644 --- a/packages/browseros-agent/apps/agent/web-ext.config.ts +++ b/packages/browseros-agent/apps/agent/web-ext.config.ts @@ -1,19 +1,13 @@ -import { dirname, join } from 'node:path' -import { fileURLToPath } from 'node:url' import { defineWebExtConfig } from 'wxt' // biome-ignore lint/style/noProcessEnv: config file needs env access const env = process.env -const MONOREPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), '../..') -const CONTROLLER_EXT_DIR = join(MONOREPO_ROOT, 'apps/controller-ext/dist') - const chromiumArgs = [ '--use-mock-keychain', '--show-component-extension-options', '--disable-browseros-server', '--disable-browseros-extensions', - `--load-extension=${CONTROLLER_EXT_DIR}`, ] if (env.BROWSEROS_CDP_PORT) { diff --git a/packages/browseros-agent/apps/cli/cmd/health.go b/packages/browseros-agent/apps/cli/cmd/health.go index 8550157c5..56dbc49fa 100644 --- a/packages/browseros-agent/apps/cli/cmd/health.go +++ b/packages/browseros-agent/apps/cli/cmd/health.go @@ -49,7 +49,7 @@ func init() { statusCmd := &cobra.Command{ Use: "status", Annotations: map[string]string{"group": "Setup:"}, - Short: "Check extension connection status", + Short: "Check BrowserOS runtime status", Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { c := newClient() @@ -64,12 +64,12 @@ func init() { green := color.New(color.FgGreen).SprintFunc() red := color.New(color.FgRed).SprintFunc() - ext := data["extensionConnected"] - extStr := red("disconnected") - if b, ok := ext.(bool); ok && b { - extStr = green("connected") + cdp := data["cdpConnected"] + cdpStr := red("disconnected") + if b, ok := cdp.(bool); ok && b { + cdpStr = green("connected") } - fmt.Printf("Extension: %s\n", extStr) + fmt.Printf("Browser: %s\n", cdpStr) }, } diff --git a/packages/browseros-agent/apps/controller-ext/.gitignore b/packages/browseros-agent/apps/controller-ext/.gitignore deleted file mode 100644 index 283e8b02e..000000000 --- a/packages/browseros-agent/apps/controller-ext/.gitignore +++ /dev/null @@ -1,32 +0,0 @@ -# Dependencies -node_modules/ - -# Build output -dist/ - -# Build unpublished docs -docs/ - -# TypeScript -*.tsbuildinfo - -# IDE -.vscode/ -.idea/ -*.swp -*.swo - -# OS -.DS_Store -Thumbs.db - -# Logs -*.log -npm-debug.log* - -# Environment -.env -.env.local - -# Claude -.claude diff --git a/packages/browseros-agent/apps/controller-ext/README.md b/packages/browseros-agent/apps/controller-ext/README.md deleted file mode 100644 index 1d0867e7a..000000000 --- a/packages/browseros-agent/apps/controller-ext/README.md +++ /dev/null @@ -1,430 +0,0 @@ -# BrowserOS Controller - -WebSocket-based Chrome Extension that exposes browser automation APIs for remote control. - -**⚠️ IMPORTANT:** This extension ONLY works in **BrowserOS Chrome**, not regular Chrome! - ---- - -## 🚀 Quick Start - -### 1. Build the Extension - -```bash -npm install -npm run build -``` - -### 2. Load Extension in BrowserOS Chrome - -1. Open BrowserOS Chrome -2. Go to `chrome://extensions/` -3. Enable **"Developer mode"** (top-right toggle) -4. Click **"Load unpacked"** -5. Select the `dist/` folder -6. Verify extension is loaded (you should see "BrowserOS Controller") - -### 3. Test the Extension - -```bash -npm test -``` - -This starts an interactive test client. You should see: - -``` -🚀 Starting BrowserOS Controller Test Client -────────────────────────────────────────────────────────── - -WebSocket Server Started -Listening on: ws://localhost:9224/controller -Waiting for extension to connect... - -✅ Extension connected! - -Running Diagnostic Test -============================================================ - -📤 Sending: checkBrowserOS - Request ID: test-1729012345678 - -📨 Response: test-1729012345678 - Status: ✅ SUCCESS - Data: { - "available": true, - "apis": [ - "captureScreenshot", - "clear", - "click", - ... - ] - } -``` - -**If you see "available": true**, you're all set! 🎉 - -**If you see "available": false**, you're not using BrowserOS Chrome. - ---- - -## ⚙️ Configuration - -The extension can be configured using environment variables. This is optional - sensible defaults are provided. - -### Environment Variables - -Create a `.env` file in the project root to customize configuration: - -```bash -# Copy the example file -cp .env.example .env - -# Edit .env with your values -``` - -### Available Configuration Options - -#### WebSocket Configuration - -```bash -WEBSOCKET_PROTOCOL=ws # ws or wss (default: ws) -WEBSOCKET_HOST=localhost # Server host (default: localhost) -WEBSOCKET_PORT=9224 # Server port (default: 9224) -WEBSOCKET_PATH=/controller # Server path (default: /controller) -``` - -#### Connection Settings - -```bash -WEBSOCKET_RECONNECT_DELAY=1000 # Initial reconnect delay in ms (default: 1000) -WEBSOCKET_MAX_RECONNECT_DELAY=30000 # Max reconnect delay in ms (default: 30000) -WEBSOCKET_RECONNECT_MULTIPLIER=1.5 # Exponential backoff multiplier (default: 1.5) -WEBSOCKET_MAX_RECONNECT_ATTEMPTS=0 # Max reconnect attempts, 0 = infinite (default: 0) -WEBSOCKET_HEARTBEAT_INTERVAL=30000 # Heartbeat interval in ms (default: 30000) -WEBSOCKET_HEARTBEAT_TIMEOUT=5000 # Heartbeat timeout in ms (default: 5000) -WEBSOCKET_CONNECTION_TIMEOUT=10000 # Connection timeout in ms (default: 10000) -WEBSOCKET_REQUEST_TIMEOUT=30000 # Request timeout in ms (default: 30000) -``` - -#### Concurrency Settings - -```bash -CONCURRENCY_MAX_CONCURRENT=100 # Max concurrent requests (default: 100) -CONCURRENCY_MAX_QUEUE_SIZE=1000 # Max queued requests (default: 1000) -``` - -#### Logging Settings - -```bash -LOGGING_ENABLED=true # Enable/disable logging (default: true) -LOGGING_LEVEL=info # Log level: debug, info, warn, error (default: info) -LOGGING_PREFIX=[BrowserOS Controller] # Log message prefix (default: [BrowserOS Controller]) -``` - -### Example: Custom Port Configuration - -If you want to use a different port (e.g., 8080): - -```bash -# .env -WEBSOCKET_PORT=8080 -``` - -Then rebuild the extension: - -```bash -npm run build -``` - -The extension will now connect to `ws://localhost:8080/controller` instead of the default port 9224. - ---- - -## 📖 Architecture - -See [ARCHITECTURE.md](./ARCHITECTURE.md) for complete system documentation including: - -- High-level architecture diagram -- Request flow (step-by-step) -- Component details -- All 14 registered actions -- WebSocket protocol specification -- Debugging guide - ---- - -## 🧪 Testing - -The test client (`npm test`) provides an interactive menu: - -``` -Available Commands: - - Tab Actions: - 1. getActiveTab - Get currently active tab - 2. getTabs - Get all tabs - - Browser Actions: - 3. getInteractiveSnapshot - Get page elements (requires tabId) - 4. click - Click element (requires tabId, nodeId) - 5. inputText - Type text (requires tabId, nodeId, text) - 6. captureScreenshot - Take screenshot (requires tabId) - - Diagnostic: - d. checkBrowserOS - Check if chrome.browserOS is available - - Other: - h. Show this menu - q. Quit -``` - -### Example Usage: - -1. Type `1` → Get active tab -2. Type `d` → Run diagnostic -3. Type `q` → Quit - ---- - -## 🔧 Development - -### Build Commands - -```bash -npm run build # Production build -npm run build:dev # Development build (with source maps) -npm run watch # Watch mode for development -``` - -### Debug Extension - -1. Go to `chrome://extensions/` -2. Click **"Inspect views service worker"** under "BrowserOS Controller" -3. Service worker console shows all logs - -**Check extension status:** - -```javascript -__browserosController.getStats(); -``` - -**Expected output:** - -```javascript -{ - connection: "connected", - requests: { inFlight: 0, avgDuration: 0, errorRate: 0, totalRequests: 0 }, - concurrency: { inFlight: 0, queued: 0, utilization: 0 }, - validator: { activeIds: 0 }, - responseQueue: { size: 0 } -} -``` - -**Check registered actions:** -Look for this log on extension load: - -``` -Registered 14 action(s): checkBrowserOS, getActiveTab, getTabs, ... -``` - ---- - -## 📋 Available Actions - -| Action | Input | Output | Description | -| ------------------------ | --------------------------------- | ------------------------------- | -------------------------------------- | -| `checkBrowserOS` | `{}` | `{available, apis}` | Check if chrome.browserOS is available | -| `getActiveTab` | `{}` | `{tabId, url, title, windowId}` | Get currently active tab | -| `getTabs` | `{}` | `{tabs[]}` | Get all open tabs | -| `getInteractiveSnapshot` | `{tabId, options?}` | `InteractiveSnapshot` | Get all interactive elements on page | -| `click` | `{tabId, nodeId}` | `{success}` | Click element by nodeId | -| `inputText` | `{tabId, nodeId, text}` | `{success}` | Type text into element | -| `clear` | `{tabId, nodeId}` | `{success}` | Clear text from element | -| `scrollToNode` | `{tabId, nodeId}` | `{scrolled}` | Scroll element into view | -| `captureScreenshot` | `{tabId, size?, showHighlights?}` | `{dataUrl}` | Take screenshot | -| `sendKeys` | `{tabId, keys}` | `{success}` | Send keyboard keys | -| `getPageLoadStatus` | `{tabId}` | `PageLoadStatus` | Get page load status | -| `getSnapshot` | `{tabId, type, options?}` | `Snapshot` | Get text/links snapshot | -| `clickCoordinates` | `{tabId, x, y}` | `{success}` | Click at coordinates | -| `typeAtCoordinates` | `{tabId, x, y, text}` | `{success}` | Type at coordinates | - ---- - -## 🔌 WebSocket Protocol - -**Endpoint:** `ws://localhost:9224/controller` - -**Request Format:** - -```json -{ - "id": "unique-request-id", - "action": "click", - "payload": { - "tabId": 12345, - "nodeId": 42 - } -} -``` - -**Response Format:** - -```json -{ - "id": "unique-request-id", - "ok": true, - "data": { - "success": true - } -} -``` - -**Error Response:** - -```json -{ - "id": "unique-request-id", - "ok": false, - "error": "Element not found: nodeId 42" -} -``` - ---- - -## ⚠️ Common Issues - -### Issue 1: "chrome.browserOS is undefined" - -**Symptoms:** - -- Diagnostic shows `"available": false` -- All browser actions fail - -**Cause:** Not using BrowserOS Chrome - -**Solution:** - -- Download and use BrowserOS Chrome (not regular Chrome) -- Verify at `chrome://version` - should show "BrowserOS" in the name - ---- - -### Issue 2: "Port 9224 is already in use" - -**Symptoms:** - -``` -❌ Fatal Error: Port 9224 is already in use! -``` - -**Solution:** - -```bash -lsof -ti:9224 | xargs kill -9 -npm test -``` - ---- - -### Issue 3: Extension Not Connecting - -**Symptoms:** - -- Test client shows "Waiting for extension to connect..." forever -- Service worker console shows "Connection timeout" - -**Checklist:** - -1. ✅ Test server running (`npm test`) -2. ✅ Extension loaded in BrowserOS Chrome -3. ✅ Extension enabled (chrome://extensions/) -4. ✅ Service worker active (not suspended) - -**Solution:** - -1. Reload extension: chrome://extensions/ → "Reload" button -2. Restart test server: Ctrl+C, then `npm test` - ---- - -### Issue 4: "Unknown action" - -**Symptoms:** - -``` -Error: Unknown action: "click". Available actions: getActiveTab, getTabs, ... -``` - -**Cause:** Action not registered (extension didn't reload properly) - -**Solution:** - -1. Toggle extension OFF and ON at chrome://extensions/ -2. Check service worker console for: `Registered 14 action(s): ...` - ---- - -## 📁 Project Structure - -``` -browseros-controller/ -├── README.md # This file -├── ARCHITECTURE.md # Complete architecture documentation -├── .env.example # Environment variable template -├── manifest.json # Extension manifest -├── package.json # Node dependencies -├── webpack.config.js # Build configuration -│ -├── src/ # Source code -│ ├── background/ # Service worker entry point -│ ├── actions/ # Action handlers -│ │ ├── bookmark/ # Bookmark management actions -│ │ ├── browser/ # Browser interaction actions -│ │ ├── diagnostics/ # Diagnostic actions -│ │ ├── history/ # History management actions -│ │ └── tab/ # Tab management actions -│ ├── adapters/ # Chrome API wrappers -│ ├── config/ # Configuration management -│ │ ├── constants.ts # Application constants -│ │ └── environment.ts # Environment variable handling -│ ├── websocket/ # WebSocket client -│ ├── utils/ # Utilities -│ ├── protocol/ # Protocol types -│ └── types/ # TypeScript definitions -│ -├── tests/ # Test files -│ ├── test-simple.js # Interactive test client -│ └── test-auto.js # Automated test client -│ -└── dist/ # Built extension (generated) - ├── background.js - └── manifest.json -``` - ---- - -## 🔗 Related Projects - -- **BrowserOS-agent**: AI agent that uses this controller for browser automation -- **BrowserOS Chrome**: Custom Chrome build with `chrome.browserOS` APIs - ---- - -## 📄 License - -MIT - ---- - -## 🆘 Support - -For issues or questions: - -1. Check [ARCHITECTURE.md](./ARCHITECTURE.md) for detailed documentation -2. Review the "Common Issues" section above -3. Check service worker console for detailed error logs -4. Verify you're using BrowserOS Chrome (run diagnostic test) - ---- - -**Happy automating! 🚀** diff --git a/packages/browseros-agent/apps/controller-ext/assets/icon128.png b/packages/browseros-agent/apps/controller-ext/assets/icon128.png deleted file mode 100644 index 78233c783..000000000 Binary files a/packages/browseros-agent/apps/controller-ext/assets/icon128.png and /dev/null differ diff --git a/packages/browseros-agent/apps/controller-ext/assets/icon16.png b/packages/browseros-agent/apps/controller-ext/assets/icon16.png deleted file mode 100644 index f257f5766..000000000 Binary files a/packages/browseros-agent/apps/controller-ext/assets/icon16.png and /dev/null differ diff --git a/packages/browseros-agent/apps/controller-ext/assets/icon48.png b/packages/browseros-agent/apps/controller-ext/assets/icon48.png deleted file mode 100644 index 0e14ea0c2..000000000 Binary files a/packages/browseros-agent/apps/controller-ext/assets/icon48.png and /dev/null differ diff --git a/packages/browseros-agent/apps/controller-ext/manifest.json b/packages/browseros-agent/apps/controller-ext/manifest.json deleted file mode 100644 index 57300aa06..000000000 --- a/packages/browseros-agent/apps/controller-ext/manifest.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "manifest_version": 3, - "name": "BrowserOS Controller", - "version": "1.0.0.8", - "description": "BrowserOS API bridge for BrowserOS Server", - "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAhlh9i/c2A3f0PL86hXhGPzguLIOQ+sPf3/Y8RD11gmdvoU6XqnUqv7GgBvm7SW7316uPnS58AYZY13jGtF4rFrscdda5H2CjZrtOyOycmKp2KzibJLwibXNm/JwKhZ3QEfgsW/orh1SMY2kNj62JemkWLcLyn3E1T+KTcTVyFOxiJS3hyQ+Y0/Jp1HOqGh5lYS58YYzwhId5rrJjfL7wFYtALgt2dEA2r7p4qpe+SW0QLA+ayjRAjS+yt+qitR0eWg+XgqcIk1f1KblN8/yDISssSD4LWiPofe5CmJPnqlHIuI0CpgvAFv9dvgR/w8OFkXxK5h06i6saum1xExj+IwIDAQAB", - "permissions": [ - "tabs", - "activeTab", - "bookmarks", - "history", - "scripting", - "storage", - "tabGroups", - "webNavigation", - "downloads", - "browserOS", - "alarms" - ], - "update_url": "https://cdn.browseros.com/extensions/update-manifest.xml", - "host_permissions": [""], - "background": { - "service_worker": "background.js", - "type": "module" - }, - "action": { - "default_icon": { - "16": "assets/icon16.png", - "48": "assets/icon48.png", - "128": "assets/icon128.png" - } - }, - "icons": { - "16": "assets/icon16.png", - "48": "assets/icon48.png", - "128": "assets/icon128.png" - } -} diff --git a/packages/browseros-agent/apps/controller-ext/package.json b/packages/browseros-agent/apps/controller-ext/package.json deleted file mode 100644 index 0060007a0..000000000 --- a/packages/browseros-agent/apps/controller-ext/package.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "name": "browseros-controller", - "version": "1.0.0", - "description": "Chrome Extension API bridge for BrowserOS Server", - "directories": { - "doc": "docs" - }, - "scripts": { - "build": "webpack --mode production", - "build:dev": "webpack --mode development", - "watch": "webpack --mode development --watch", - "test": "node tests/test-simple.js", - "test:auto": "node tests/test-auto.js", - "typecheck": "tsc --noEmit" - }, - "keywords": [ - "browser-automation", - "chrome-extension", - "browseros" - ], - "author": "BrowserOS Team", - "license": "MIT", - "type": "commonjs", - "dependencies": { - "@browseros/shared": "workspace:*", - "zod": "^4.1.12" - }, - "devDependencies": { - "@types/chrome": "^0.1.24", - "@types/node": "^24.7.1", - "copy-webpack-plugin": "^12.0.2", - "terser-webpack-plugin": "^5.3.11", - "ts-loader": "^9.5.4", - "typescript": "^5.9.3", - "webpack": "^5.102.1", - "webpack-cli": "^6.0.1", - "ws": "^8.18.3" - } -} diff --git a/packages/browseros-agent/apps/controller-ext/src/actions/ActionHandler.ts b/packages/browseros-agent/apps/controller-ext/src/actions/ActionHandler.ts deleted file mode 100644 index 885b3f04d..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/actions/ActionHandler.ts +++ /dev/null @@ -1,106 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -import { z } from 'zod' - -import type { ActionResponse } from '@/protocol/types' -import { ActionResponseSchema } from '@/protocol/types' -import { logger } from '@/utils/logger' - -// Re-export for convenience -export type { ActionResponse } -export { ActionResponseSchema } - -/** - * ActionHandler - Abstract base class for all actions - * - * Responsibilities: - * - Define contract for all actions (must implement inputSchema + execute) - * - Validate input using Zod schemas - * - Handle validation and execution errors - * - Return standardized ActionResponse - * - * Usage: - * class MyAction extends ActionHandler { - * inputSchema = z.object({ ... }); - * async execute(input: InputType): Promise { ... } - * } - */ -export abstract class ActionHandler { - /** - * Zod schema for input validation - * Must be implemented by concrete actions - */ - abstract readonly inputSchema: z.ZodSchema - - /** - * Execute the action logic - * Must be implemented by concrete actions - * - * @param input - Validated input (guaranteed to match inputSchema) - * @returns Action result - */ - abstract execute(input: TInput): Promise - - /** - * Handle request with validation and error handling - * Called by ActionRegistry - * - * Flow: - * 1. Validate input with Zod schema - * 2. Execute action logic - * 3. Return standardized response (ok/error) - * - * @param payload - Raw payload from request (unvalidated) - * @returns Standardized action response - */ - async handle(payload: unknown): Promise { - const actionName = this.constructor.name - - try { - // Step 1: Validate input - logger.debug(`[${actionName}] Validating input`) - const validatedInput = this.inputSchema.parse(payload) - - // Step 2: Execute action - logger.debug(`[${actionName}] Executing action`) - const result = await this.execute(validatedInput) - - // Step 3: Return success response - logger.debug(`[${actionName}] Action completed successfully`) - return { ok: true, data: result } - } catch (error) { - // Handle validation or execution errors - const errorMessage = this._formatError(error) - logger.error(`[${actionName}] Action failed: ${errorMessage}`) - return { ok: false, error: errorMessage } - } - } - - /** - * Format error for user-friendly response - * - * @param error - Error from validation or execution - * @returns Formatted error message - */ - protected _formatError(error: unknown): string { - // Zod validation error - if (error instanceof z.ZodError) { - const errors = error.issues.map((e: z.ZodIssue) => { - const path = e.path.length > 0 ? `${e.path.join('.')}: ` : '' - return `${path}${e.message}` - }) - return `Validation error: ${errors.join(', ')}` - } - - // Standard Error - if (error instanceof Error) { - return error.message - } - - // Unknown error - return String(error) - } -} diff --git a/packages/browseros-agent/apps/controller-ext/src/actions/ActionRegistry.ts b/packages/browseros-agent/apps/controller-ext/src/actions/ActionRegistry.ts deleted file mode 100644 index 5406a8652..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/actions/ActionRegistry.ts +++ /dev/null @@ -1,148 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import { logger } from '@/utils/logger' -import type { ActionHandler, ActionResponse } from './ActionHandler' - -/** - * ActionRegistry - Central dispatcher for all actions - * - * Responsibilities: - * - Register action handlers by name - * - Dispatch requests to correct handler - * - Return error for unknown actions - * - Provide introspection (list available actions) - * - * Usage: - * const registry = new ActionRegistry(); - * registry.register('getActiveTab', new GetActiveTabAction()); - * const response = await registry.dispatch('getActiveTab', {}); - */ -export class ActionRegistry { - private handlers = new Map() - - /** - * Register an action handler - * - * @param actionName - Unique action name (e.g., "getActiveTab") - * @param handler - Action handler instance - */ - register(actionName: string, handler: ActionHandler): void { - if (this.handlers.has(actionName)) { - logger.warn( - `[ActionRegistry] Action "${actionName}" already registered, overwriting`, - ) - } - - this.handlers.set(actionName, handler) - logger.info(`[ActionRegistry] Registered action: ${actionName}`) - } - - /** - * Dispatch request to appropriate action handler - * - * Flow: - * 1. Find handler for action name - * 2. If not found, return error - * 3. If found, delegate to handler.handle() - * 4. Handler validates input and executes - * 5. Return result - * - * @param actionName - Action to execute - * @param payload - Action payload (unvalidated) - * @returns Action response - */ - async dispatch( - actionName: string, - payload: unknown, - ): Promise { - logger.debug(`[ActionRegistry] Dispatching action: ${actionName}`) - - // Check if action exists - const handler = this.handlers.get(actionName) - - if (!handler) { - const availableActions = Array.from(this.handlers.keys()).join(', ') - const errorMessage = `Unknown action: "${actionName}". Available actions: ${availableActions || 'none'}` - logger.error(`[ActionRegistry] ${errorMessage}`) - return { - ok: false, - error: errorMessage, - } - } - - // Delegate to handler - try { - const response = await handler.handle(payload) - logger.debug( - `[ActionRegistry] Action "${actionName}" ${response.ok ? 'succeeded' : 'failed'}`, - ) - return response - } catch (error) { - // Catch any unexpected errors from handler - const errorMessage = - error instanceof Error ? error.message : String(error) - logger.error( - `[ActionRegistry] Unexpected error in "${actionName}": ${errorMessage}`, - ) - return { - ok: false, - error: `Action execution failed: ${errorMessage}`, - } - } - } - - /** - * Get list of registered action names - * - * @returns Array of action names - */ - getAvailableActions(): string[] { - return Array.from(this.handlers.keys()) - } - - /** - * Check if action is registered - * - * @param actionName - Action name to check - * @returns True if action exists - */ - hasAction(actionName: string): boolean { - return this.handlers.has(actionName) - } - - /** - * Get number of registered actions - * - * @returns Count of registered actions - */ - getActionCount(): number { - return this.handlers.size - } - - /** - * Unregister an action (useful for testing) - * - * @param actionName - Action to remove - * @returns True if action was removed - */ - unregister(actionName: string): boolean { - const removed = this.handlers.delete(actionName) - if (removed) { - logger.info(`[ActionRegistry] Unregistered action: ${actionName}`) - } - return removed - } - - /** - * Clear all registered actions (useful for testing) - */ - clear(): void { - const count = this.handlers.size - this.handlers.clear() - logger.info(`[ActionRegistry] Cleared ${count} registered actions`) - } -} diff --git a/packages/browseros-agent/apps/controller-ext/src/actions/bookmark/CreateBookmarkAction.ts b/packages/browseros-agent/apps/controller-ext/src/actions/bookmark/CreateBookmarkAction.ts deleted file mode 100644 index e921ff6e9..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/actions/bookmark/CreateBookmarkAction.ts +++ /dev/null @@ -1,81 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -import { z } from 'zod' -import { BookmarkAdapter } from '@/adapters/BookmarkAdapter' -import { ActionHandler } from '../ActionHandler' - -// Input schema -const CreateBookmarkInputSchema = z.object({ - title: z.string().describe('Bookmark title'), - url: z.string().url().describe('Bookmark URL'), - parentId: z - .string() - .optional() - .describe('Parent folder ID (optional, defaults to "Other Bookmarks")'), -}) - -// Output schema -const CreateBookmarkOutputSchema = z.object({ - id: z.string().describe('Created bookmark ID'), - title: z.string().describe('Bookmark title'), - url: z.string().describe('Bookmark URL'), - dateAdded: z - .number() - .optional() - .describe('Timestamp when bookmark was created'), -}) - -type CreateBookmarkInput = z.infer -type CreateBookmarkOutput = z.infer - -/** - * CreateBookmarkAction - Create a new bookmark - * - * Creates a bookmark with the specified title and URL. - * - * Input: - * - title: Display title for the bookmark - * - url: Full URL to bookmark - * - parentId (optional): Parent folder ID - * - * Output: - * - id: Created bookmark ID - * - title: Bookmark title - * - url: Bookmark URL - * - dateAdded: Creation timestamp - * - * Usage: - * Create a bookmark in the default location (Other Bookmarks). - * - * Example: - * { - * "title": "Google", - * "url": "https://www.google.com" - * } - * // Returns: { id: "123", title: "Google", url: "https://www.google.com", dateAdded: 1729012345678 } - */ -export class CreateBookmarkAction extends ActionHandler< - CreateBookmarkInput, - CreateBookmarkOutput -> { - readonly inputSchema = CreateBookmarkInputSchema - private bookmarkAdapter = new BookmarkAdapter() - - async execute(input: CreateBookmarkInput): Promise { - const created = await this.bookmarkAdapter.createBookmark({ - title: input.title, - url: input.url, - parentId: input.parentId, - }) - - return { - id: created.id, - title: created.title, - url: created.url || '', - dateAdded: created.dateAdded, - } - } -} diff --git a/packages/browseros-agent/apps/controller-ext/src/actions/bookmark/CreateBookmarkFolderAction.ts b/packages/browseros-agent/apps/controller-ext/src/actions/bookmark/CreateBookmarkFolderAction.ts deleted file mode 100644 index 973cbab37..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/actions/bookmark/CreateBookmarkFolderAction.ts +++ /dev/null @@ -1,52 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -import { z } from 'zod' -import { BookmarkAdapter } from '@/adapters/BookmarkAdapter' -import { ActionHandler } from '../ActionHandler' - -const CreateBookmarkFolderInputSchema = z.object({ - title: z.string().describe('Folder name'), - parentId: z - .string() - .optional() - .describe('Parent folder ID (defaults to "1" = Bookmarks Bar)'), -}) - -const CreateBookmarkFolderOutputSchema = z.object({ - id: z.string().describe('Created folder ID'), - title: z.string().describe('Folder name'), - parentId: z.string().optional().describe('Parent folder ID'), - dateAdded: z.number().optional().describe('Creation timestamp'), -}) - -type CreateBookmarkFolderInput = z.infer -type CreateBookmarkFolderOutput = z.infer< - typeof CreateBookmarkFolderOutputSchema -> - -export class CreateBookmarkFolderAction extends ActionHandler< - CreateBookmarkFolderInput, - CreateBookmarkFolderOutput -> { - readonly inputSchema = CreateBookmarkFolderInputSchema - private bookmarkAdapter = new BookmarkAdapter() - - async execute( - input: CreateBookmarkFolderInput, - ): Promise { - const created = await this.bookmarkAdapter.createBookmarkFolder({ - title: input.title, - parentId: input.parentId, - }) - - return { - id: created.id, - title: created.title, - parentId: created.parentId, - dateAdded: created.dateAdded, - } - } -} diff --git a/packages/browseros-agent/apps/controller-ext/src/actions/bookmark/GetBookmarkChildrenAction.ts b/packages/browseros-agent/apps/controller-ext/src/actions/bookmark/GetBookmarkChildrenAction.ts deleted file mode 100644 index dffcbf477..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/actions/bookmark/GetBookmarkChildrenAction.ts +++ /dev/null @@ -1,59 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -import { z } from 'zod' -import { BookmarkAdapter } from '@/adapters/BookmarkAdapter' -import { ActionHandler } from '../ActionHandler' - -const GetBookmarkChildrenInputSchema = z.object({ - folderId: z.string().describe('Folder ID to get children from'), -}) - -const GetBookmarkChildrenOutputSchema = z.object({ - children: z.array( - z.object({ - id: z.string(), - title: z.string(), - url: z.string().optional(), - parentId: z.string().optional(), - dateAdded: z.number().optional(), - isFolder: z.boolean(), - }), - ), - count: z.number(), -}) - -type GetBookmarkChildrenInput = z.infer -type GetBookmarkChildrenOutput = z.infer - -export class GetBookmarkChildrenAction extends ActionHandler< - GetBookmarkChildrenInput, - GetBookmarkChildrenOutput -> { - readonly inputSchema = GetBookmarkChildrenInputSchema - private bookmarkAdapter = new BookmarkAdapter() - - async execute( - input: GetBookmarkChildrenInput, - ): Promise { - const results = await this.bookmarkAdapter.getBookmarkChildren( - input.folderId, - ) - - const children = results.map((node) => ({ - id: node.id, - title: node.title, - url: node.url, - parentId: node.parentId, - dateAdded: node.dateAdded, - isFolder: !node.url, - })) - - return { - children, - count: children.length, - } - } -} diff --git a/packages/browseros-agent/apps/controller-ext/src/actions/bookmark/GetBookmarksAction.ts b/packages/browseros-agent/apps/controller-ext/src/actions/bookmark/GetBookmarksAction.ts deleted file mode 100644 index a3bfc72fd..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/actions/bookmark/GetBookmarksAction.ts +++ /dev/null @@ -1,111 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -import { z } from 'zod' -import { BookmarkAdapter } from '@/adapters/BookmarkAdapter' -import { ActionHandler } from '../ActionHandler' - -// Input schema -const GetBookmarksInputSchema = z.object({ - query: z - .string() - .optional() - .describe( - 'Search query to filter bookmarks (optional, returns all if not provided)', - ), - limit: z - .number() - .int() - .positive() - .optional() - .default(20) - .describe('Maximum number of results (default: 20)'), - recent: z - .boolean() - .optional() - .default(false) - .describe('Get recent bookmarks instead of searching'), -}) - -// Output schema -const GetBookmarksOutputSchema = z.object({ - bookmarks: z.array( - z.object({ - id: z.string(), - title: z.string(), - url: z.string().optional(), - dateAdded: z.number().optional(), - parentId: z.string().optional(), - }), - ), - count: z.number(), -}) - -type GetBookmarksInput = z.infer -type GetBookmarksOutput = z.infer - -/** - * GetBookmarksAction - Get or search bookmarks - * - * Retrieves bookmarks with optional filtering. - * - * Input: - * - query (optional): Search query to match title or URL - * - limit (optional): Maximum results (default: 20) - * - recent (optional): Get recent bookmarks instead (default: false) - * - * Output: - * - bookmarks: Array of bookmark objects - * - count: Number of bookmarks returned - * - * Usage: - * - Get recent: { "recent": true } - * - Search: { "query": "github" } - * - Get all (limited): { "limit": 50 } - * - * Example: - * { - * "query": "google", - * "limit": 10 - * } - * // Returns: { bookmarks: [{id: "1", title: "Google", url: "https://google.com"}], count: 1 } - */ -export class GetBookmarksAction extends ActionHandler< - GetBookmarksInput, - GetBookmarksOutput -> { - readonly inputSchema = GetBookmarksInputSchema - private bookmarkAdapter = new BookmarkAdapter() - - async execute(input: GetBookmarksInput): Promise { - let results: chrome.bookmarks.BookmarkTreeNode[] - - if (input.recent) { - // Get recent bookmarks - results = await this.bookmarkAdapter.getRecentBookmarks(input.limit) - } else if (input.query) { - // Search bookmarks - results = await this.bookmarkAdapter.searchBookmarks(input.query) - results = results.slice(0, input.limit) - } else { - // Get recent by default - results = await this.bookmarkAdapter.getRecentBookmarks(input.limit) - } - - // Map to output format - const bookmarks = results.map((b) => ({ - id: b.id, - title: b.title, - url: b.url, - dateAdded: b.dateAdded, - parentId: b.parentId, - })) - - return { - bookmarks, - count: bookmarks.length, - } - } -} diff --git a/packages/browseros-agent/apps/controller-ext/src/actions/bookmark/MoveBookmarkAction.ts b/packages/browseros-agent/apps/controller-ext/src/actions/bookmark/MoveBookmarkAction.ts deleted file mode 100644 index 6771dec44..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/actions/bookmark/MoveBookmarkAction.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -import { z } from 'zod' -import { BookmarkAdapter } from '@/adapters/BookmarkAdapter' -import { ActionHandler } from '../ActionHandler' - -const MoveBookmarkInputSchema = z.object({ - id: z.string().describe('Bookmark or folder ID to move'), - parentId: z.string().optional().describe('New parent folder ID'), - index: z.number().int().min(0).optional().describe('Position within parent'), -}) - -const MoveBookmarkOutputSchema = z.object({ - id: z.string().describe('Moved bookmark ID'), - title: z.string().describe('Bookmark title'), - url: z.string().optional().describe('Bookmark URL (undefined if folder)'), - parentId: z.string().optional().describe('New parent folder ID'), - index: z.number().optional().describe('New position within parent'), -}) - -type MoveBookmarkInput = z.infer -type MoveBookmarkOutput = z.infer - -export class MoveBookmarkAction extends ActionHandler< - MoveBookmarkInput, - MoveBookmarkOutput -> { - readonly inputSchema = MoveBookmarkInputSchema - private bookmarkAdapter = new BookmarkAdapter() - - async execute(input: MoveBookmarkInput): Promise { - const destination: { parentId?: string; index?: number } = {} - if (input.parentId !== undefined) destination.parentId = input.parentId - if (input.index !== undefined) destination.index = input.index - - const moved = await this.bookmarkAdapter.moveBookmark(input.id, destination) - - return { - id: moved.id, - title: moved.title, - url: moved.url, - parentId: moved.parentId, - index: moved.index, - } - } -} diff --git a/packages/browseros-agent/apps/controller-ext/src/actions/bookmark/RemoveBookmarkAction.ts b/packages/browseros-agent/apps/controller-ext/src/actions/bookmark/RemoveBookmarkAction.ts deleted file mode 100644 index aba03b41a..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/actions/bookmark/RemoveBookmarkAction.ts +++ /dev/null @@ -1,62 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -import { z } from 'zod' -import { BookmarkAdapter } from '@/adapters/BookmarkAdapter' -import { ActionHandler } from '../ActionHandler' - -// Input schema -const RemoveBookmarkInputSchema = z.object({ - id: z.string().describe('Bookmark ID to remove'), -}) - -// Output schema -const RemoveBookmarkOutputSchema = z.object({ - success: z - .boolean() - .describe('Whether the bookmark was successfully removed'), - message: z.string().describe('Confirmation message'), -}) - -type RemoveBookmarkInput = z.infer -type RemoveBookmarkOutput = z.infer - -/** - * RemoveBookmarkAction - Remove a bookmark - * - * Deletes a bookmark by its ID. - * - * Input: - * - id: Bookmark ID to remove - * - * Output: - * - success: true if removed - * - message: Confirmation message - * - * Usage: - * Get the bookmark ID from getBookmarks first, then remove it. - * - * Example: - * { - * "id": "123" - * } - * // Returns: { success: true, message: "Removed bookmark 123" } - */ -export class RemoveBookmarkAction extends ActionHandler< - RemoveBookmarkInput, - RemoveBookmarkOutput -> { - readonly inputSchema = RemoveBookmarkInputSchema - private bookmarkAdapter = new BookmarkAdapter() - - async execute(input: RemoveBookmarkInput): Promise { - await this.bookmarkAdapter.removeBookmark(input.id) - - return { - success: true, - message: `Removed bookmark ${input.id}`, - } - } -} diff --git a/packages/browseros-agent/apps/controller-ext/src/actions/bookmark/RemoveBookmarkTreeAction.ts b/packages/browseros-agent/apps/controller-ext/src/actions/bookmark/RemoveBookmarkTreeAction.ts deleted file mode 100644 index 8cbe612ea..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/actions/bookmark/RemoveBookmarkTreeAction.ts +++ /dev/null @@ -1,48 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -import { z } from 'zod' -import { BookmarkAdapter } from '@/adapters/BookmarkAdapter' -import { ActionHandler } from '../ActionHandler' - -const RemoveBookmarkTreeInputSchema = z.object({ - id: z.string().describe('Folder ID to remove'), - confirm: z.boolean().describe('Must be true to confirm recursive deletion'), -}) - -const RemoveBookmarkTreeOutputSchema = z.object({ - success: z.boolean().describe('Whether the folder was removed'), - message: z.string().describe('Result message'), -}) - -type RemoveBookmarkTreeInput = z.infer -type RemoveBookmarkTreeOutput = z.infer - -export class RemoveBookmarkTreeAction extends ActionHandler< - RemoveBookmarkTreeInput, - RemoveBookmarkTreeOutput -> { - readonly inputSchema = RemoveBookmarkTreeInputSchema - private bookmarkAdapter = new BookmarkAdapter() - - async execute( - input: RemoveBookmarkTreeInput, - ): Promise { - if (input.confirm !== true) { - return { - success: false, - message: - 'Recursive deletion requires confirm: true. This will permanently delete the folder and all its contents.', - } - } - - await this.bookmarkAdapter.removeBookmarkTree(input.id) - - return { - success: true, - message: `Removed folder ${input.id} and all its contents`, - } - } -} diff --git a/packages/browseros-agent/apps/controller-ext/src/actions/bookmark/UpdateBookmarkAction.ts b/packages/browseros-agent/apps/controller-ext/src/actions/bookmark/UpdateBookmarkAction.ts deleted file mode 100644 index 7739087c6..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/actions/bookmark/UpdateBookmarkAction.ts +++ /dev/null @@ -1,82 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -import { z } from 'zod' -import { BookmarkAdapter } from '@/adapters/BookmarkAdapter' -import { ActionHandler } from '../ActionHandler' - -// Input schema -const UpdateBookmarkInputSchema = z.object({ - id: z.string().describe('Bookmark ID to update'), - title: z.string().optional().describe('New bookmark title'), - url: z.string().url().optional().describe('New bookmark URL'), -}) - -// Output schema -const UpdateBookmarkOutputSchema = z.object({ - id: z.string().describe('Bookmark ID'), - title: z.string().describe('Updated bookmark title'), - url: z.string().optional().describe('Updated bookmark URL'), -}) - -type UpdateBookmarkInput = z.infer -type UpdateBookmarkOutput = z.infer - -/** - * UpdateBookmarkAction - Update a bookmark's title or URL - * - * Updates an existing bookmark with new title and/or URL. - * - * Input: - * - id: Bookmark ID to update - * - title (optional): New title for the bookmark - * - url (optional): New URL for the bookmark - * - * Output: - * - id: Bookmark ID - * - title: Updated title - * - url: Updated URL - * - * Usage: - * Update a bookmark's title or URL (at least one must be provided). - * - * Example: - * { - * "id": "123", - * "title": "New Title", - * "url": "https://www.example.com" - * } - * // Returns: { id: "123", title: "New Title", url: "https://www.example.com" } - */ -export class UpdateBookmarkAction extends ActionHandler< - UpdateBookmarkInput, - UpdateBookmarkOutput -> { - readonly inputSchema = UpdateBookmarkInputSchema - private bookmarkAdapter = new BookmarkAdapter() - - async execute(input: UpdateBookmarkInput): Promise { - const changes: { title?: string; url?: string } = {} - - if (input.title !== undefined) { - changes.title = input.title - } - if (input.url !== undefined) { - changes.url = input.url - } - - if (Object.keys(changes).length === 0) { - throw new Error('At least one of title or url must be provided') - } - - const updated = await this.bookmarkAdapter.updateBookmark(input.id, changes) - - return { - id: updated.id, - title: updated.title, - url: updated.url, - } - } -} diff --git a/packages/browseros-agent/apps/controller-ext/src/actions/browser/CaptureScreenshotAction.ts b/packages/browseros-agent/apps/controller-ext/src/actions/browser/CaptureScreenshotAction.ts deleted file mode 100644 index f14533bff..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/actions/browser/CaptureScreenshotAction.ts +++ /dev/null @@ -1,79 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -import { z } from 'zod' -import { - BrowserOSAdapter, - type ScreenshotSizeKey, -} from '@/adapters/BrowserOSAdapter' -import { ActionHandler } from '../ActionHandler' - -// Input schema -const CaptureScreenshotInputSchema = z.object({ - tabId: z.number().describe('The tab ID to capture'), - size: z - .enum(['small', 'medium', 'large']) - .optional() - .default('medium') - .describe('Screenshot size preset (default: medium)'), - showHighlights: z - .boolean() - .optional() - .default(true) - .describe('Show element highlights (default: true)'), - width: z.number().optional().describe('Exact width in pixels'), - height: z.number().optional().describe('Exact height in pixels'), -}) - -// Output schema -const CaptureScreenshotOutputSchema = z.object({ - dataUrl: z.string().describe('Base64-encoded PNG data URL'), -}) - -type CaptureScreenshotInput = z.infer -type CaptureScreenshotOutput = z.infer - -/** - * CaptureScreenshotAction - Capture a screenshot of the page - * - * Captures a screenshot with configurable size and options. - * - * Size Options: - * - small (512px): Low detail, minimal tokens - * - medium (768px): Balanced quality/tokens (default) - * - large (1028px): High detail, maximum tokens - * - * Or specify exact dimensions with width/height. - * - * Returns: - * - dataUrl: PNG image as base64 data URL (data:image/png;base64,...) - * - * Usage: - * 1. For AI vision models: use 'medium' or 'large' - * 2. For debugging: use 'small' - * 3. For exact size: specify width and height - * - * Used by: ScreenshotTool, VisualClick, VisualType - */ -export class CaptureScreenshotAction extends ActionHandler< - CaptureScreenshotInput, - CaptureScreenshotOutput -> { - readonly inputSchema = CaptureScreenshotInputSchema - private browserOSAdapter = BrowserOSAdapter.getInstance() - - async execute( - input: CaptureScreenshotInput, - ): Promise { - const dataUrl = await this.browserOSAdapter.captureScreenshot( - input.tabId, - input.size as ScreenshotSizeKey | undefined, - input.showHighlights, - input.width, - input.height, - ) - return { dataUrl } - } -} diff --git a/packages/browseros-agent/apps/controller-ext/src/actions/browser/CaptureScreenshotPointerAction.ts b/packages/browseros-agent/apps/controller-ext/src/actions/browser/CaptureScreenshotPointerAction.ts deleted file mode 100644 index 1d037d8ba..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/actions/browser/CaptureScreenshotPointerAction.ts +++ /dev/null @@ -1,124 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -import { z } from 'zod' -import { - BrowserOSAdapter, - type ScreenshotSizeKey, -} from '@/adapters/BrowserOSAdapter' -import { logger } from '@/utils/logger' -import { PointerOverlay } from '@/utils/PointerOverlay' -import { SnapshotCache } from '@/utils/SnapshotCache' -import { ActionHandler } from '../ActionHandler' - -// Input schema -const CaptureScreenshotPointerInputSchema = z.object({ - tabId: z.number().describe('The tab ID to capture'), - nodeId: z - .number() - .int() - .positive() - .describe('The nodeId to show pointer over'), - size: z - .enum(['small', 'medium', 'large']) - .optional() - .default('medium') - .describe('Screenshot size preset (default: medium)'), - pointerLabel: z - .string() - .optional() - .describe('Optional label to show with pointer (e.g., "Click", "Type")'), -}) - -// Output schema -const CaptureScreenshotPointerOutputSchema = z.object({ - dataUrl: z.string().describe('Base64-encoded PNG data URL'), - pointerPosition: z - .object({ - x: z.number(), - y: z.number(), - }) - .optional() - .describe('Coordinates where pointer was shown'), -}) - -type CaptureScreenshotPointerInput = z.infer< - typeof CaptureScreenshotPointerInputSchema -> -type CaptureScreenshotPointerOutput = z.infer< - typeof CaptureScreenshotPointerOutputSchema -> - -/** - * CaptureScreenshotPointerAction - Show pointer over element and capture screenshot - * - * Shows a visual pointer overlay at the center of the specified element, - * then captures a screenshot with the pointer visible. - * - * Prerequisites: - * - Must call getInteractiveSnapshot first to populate the cache - * - NodeId must exist in the cached snapshot - * - * Usage: - * 1. Get snapshot to find elements and populate cache - * 2. Call captureScreenshotPointer with tabId and nodeId - * 3. Returns screenshot with pointer overlay visible - * - * Used by: Visual debugging, automation demos, step-by-step captures - */ -export class CaptureScreenshotPointerAction extends ActionHandler< - CaptureScreenshotPointerInput, - CaptureScreenshotPointerOutput -> { - readonly inputSchema = CaptureScreenshotPointerInputSchema - private browserOSAdapter = BrowserOSAdapter.getInstance() - - async execute( - input: CaptureScreenshotPointerInput, - ): Promise { - const { tabId, nodeId, size, pointerLabel } = input - - // Get element rect from cache - const rect = SnapshotCache.getNodeRect(tabId, nodeId) - - let pointerPosition: { x: number; y: number } | undefined - - if (rect) { - // Calculate center coordinates - const { x, y } = PointerOverlay.getCenterCoordinates(rect) - pointerPosition = { x, y } - - // Show pointer - await PointerOverlay.showPointer(tabId, x, y, pointerLabel) - - logger.debug( - `[CaptureScreenshotPointerAction] Showed pointer at (${x}, ${y}) for node ${nodeId}`, - ) - } else { - logger.warn( - `[CaptureScreenshotPointerAction] No cached rect for node ${nodeId} in tab ${tabId}. Capturing without pointer.`, - ) - } - - // Small delay to ensure pointer is rendered - await this.delay(100) - - // Capture screenshot with pointer visible - const dataUrl = await this.browserOSAdapter.captureScreenshot( - tabId, - size as ScreenshotSizeKey | undefined, - false, // Don't show highlights, we have the pointer - ) - - return { - dataUrl, - pointerPosition, - } - } - - private delay(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)) - } -} diff --git a/packages/browseros-agent/apps/controller-ext/src/actions/browser/ClearAction.ts b/packages/browseros-agent/apps/controller-ext/src/actions/browser/ClearAction.ts deleted file mode 100644 index ed23034d7..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/actions/browser/ClearAction.ts +++ /dev/null @@ -1,38 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -import { z } from 'zod' -import { BrowserOSAdapter } from '@/adapters/BrowserOSAdapter' -import { ActionHandler } from '../ActionHandler' - -const ClearInputSchema = z.object({ - tabId: z.number().describe('The tab ID containing the element'), - nodeId: z - .number() - .int() - .positive() - .describe('The nodeId from interactive snapshot'), -}) - -type ClearInput = z.infer -interface ClearOutput { - success: boolean -} - -/** - * ClearAction - Clear text from an input element - * - * Clears all text from an input field or textarea. - * Used before inputText or to reset form fields. - */ -export class ClearAction extends ActionHandler { - readonly inputSchema = ClearInputSchema - private browserOSAdapter = BrowserOSAdapter.getInstance() - - async execute(input: ClearInput): Promise { - await this.browserOSAdapter.clear(input.tabId, input.nodeId) - return { success: true } - } -} diff --git a/packages/browseros-agent/apps/controller-ext/src/actions/browser/ClickAction.ts b/packages/browseros-agent/apps/controller-ext/src/actions/browser/ClickAction.ts deleted file mode 100644 index 7676a29fa..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/actions/browser/ClickAction.ts +++ /dev/null @@ -1,62 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -import { z } from 'zod' -import { BrowserOSAdapter } from '@/adapters/BrowserOSAdapter' -import { PointerOverlay } from '@/utils/PointerOverlay' -import { SnapshotCache } from '@/utils/SnapshotCache' -import { ActionHandler } from '../ActionHandler' - -// Input schema -const ClickInputSchema = z.object({ - tabId: z.number().describe('The tab ID containing the element'), - nodeId: z - .number() - .int() - .positive() - .describe('The nodeId from interactive snapshot'), -}) - -// Output schema -const ClickOutputSchema = z.object({ - success: z.boolean().describe('Whether the click succeeded'), -}) - -type ClickInput = z.infer -type ClickOutput = z.infer - -/** - * ClickAction - Click an element by its nodeId - * - * This action clicks an interactive element identified by its nodeId from getInteractiveSnapshot. - * - * Prerequisites: - * - Must call getInteractiveSnapshot first to get valid nodeIds - * - NodeIds are valid only for the current page state - * - NodeIds are invalidated on page navigation - * - * Usage: - * 1. Get snapshot to find clickable elements - * 2. Choose element by nodeId - * 3. Call click with tabId and nodeId - * - * Used by: ClickTool, all automation workflows - */ -export class ClickAction extends ActionHandler { - readonly inputSchema = ClickInputSchema - private browserOSAdapter = BrowserOSAdapter.getInstance() - - async execute(input: ClickInput): Promise { - // Show pointer overlay before click - const rect = SnapshotCache.getNodeRect(input.tabId, input.nodeId) - if (rect) { - const { x, y } = PointerOverlay.getCenterCoordinates(rect) - await PointerOverlay.showPointerAndWait(input.tabId, x, y, 'Click') - } - - await this.browserOSAdapter.click(input.tabId, input.nodeId) - return { success: true } - } -} diff --git a/packages/browseros-agent/apps/controller-ext/src/actions/browser/ClickCoordinatesAction.ts b/packages/browseros-agent/apps/controller-ext/src/actions/browser/ClickCoordinatesAction.ts deleted file mode 100644 index a71e5fc74..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/actions/browser/ClickCoordinatesAction.ts +++ /dev/null @@ -1,69 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -import { z } from 'zod' -import { getBrowserOSAdapter } from '@/adapters/BrowserOSAdapter' -import { PointerOverlay } from '@/utils/PointerOverlay' -import { ActionHandler } from '../ActionHandler' - -// Input schema for clickCoordinates action -const ClickCoordinatesInputSchema = z.object({ - tabId: z.number().int().positive().describe('Tab ID to click in'), - x: z.number().int().nonnegative().describe('X coordinate in viewport pixels'), - y: z.number().int().nonnegative().describe('Y coordinate in viewport pixels'), -}) - -type ClickCoordinatesInput = z.infer - -// Output confirms the click -export interface ClickCoordinatesOutput { - success: boolean - message: string - coordinates: { - x: number - y: number - } -} - -/** - * ClickCoordinatesAction - Click at specific viewport coordinates - * - * Performs a click at the specified (x, y) coordinates in the viewport. - * Coordinates are in pixels relative to the top-left of the visible viewport (0, 0). - * - * Useful when: - * - Elements don't have accessible node IDs - * - Working with canvas or interactive graphics - * - Vision-based automation (e.g., AI identifies coordinates from screenshots) - * - * Example payload: - * { - * "tabId": 123, - * "x": 500, - * "y": 300 - * } - */ -export class ClickCoordinatesAction extends ActionHandler< - ClickCoordinatesInput, - ClickCoordinatesOutput -> { - readonly inputSchema = ClickCoordinatesInputSchema - private browserOS = getBrowserOSAdapter() - - async execute(input: ClickCoordinatesInput): Promise { - const { tabId, x, y } = input - - // Show pointer overlay before click - await PointerOverlay.showPointerAndWait(tabId, x, y, 'Click') - - await this.browserOS.clickCoordinates(tabId, x, y) - - return { - success: true, - message: `Successfully clicked at coordinates (${x}, ${y}) in tab ${tabId}`, - coordinates: { x, y }, - } - } -} diff --git a/packages/browseros-agent/apps/controller-ext/src/actions/browser/CloseWindowAction.ts b/packages/browseros-agent/apps/controller-ext/src/actions/browser/CloseWindowAction.ts deleted file mode 100644 index e2cbc8271..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/actions/browser/CloseWindowAction.ts +++ /dev/null @@ -1,38 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -import { z } from 'zod' -import { CHROME_API_TIMEOUTS, withTimeout } from '@/utils/timeout' -import { ActionHandler } from '../ActionHandler' - -const CloseWindowInputSchema = z.object({ - windowId: z.number().int().positive().describe('ID of the window to close'), -}) - -const CloseWindowOutputSchema = z.object({ - success: z.boolean().describe('Whether the window was successfully closed'), -}) - -type CloseWindowInput = z.infer -type CloseWindowOutput = z.infer - -export class CloseWindowAction extends ActionHandler< - CloseWindowInput, - CloseWindowOutput -> { - readonly inputSchema = CloseWindowInputSchema - - async execute(input: CloseWindowInput): Promise { - await withTimeout( - chrome.windows.remove(input.windowId), - CHROME_API_TIMEOUTS.CHROME_API, - 'chrome.windows.remove', - ) - - return { - success: true, - } - } -} diff --git a/packages/browseros-agent/apps/controller-ext/src/actions/browser/CreateWindowAction.ts b/packages/browseros-agent/apps/controller-ext/src/actions/browser/CreateWindowAction.ts deleted file mode 100644 index ea92749f0..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/actions/browser/CreateWindowAction.ts +++ /dev/null @@ -1,73 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -import { z } from 'zod' -import { CHROME_API_TIMEOUTS, withTimeout } from '@/utils/timeout' -import { ActionHandler } from '../ActionHandler' - -const CreateWindowInputSchema = z.object({ - url: z - .string() - .optional() - .default('about:blank') - .describe('URL to open in the new window'), - incognito: z - .boolean() - .optional() - .default(false) - .describe('Create an incognito window'), - focused: z - .boolean() - .optional() - .default(true) - .describe('Whether to focus the new window'), -}) - -const CreateWindowOutputSchema = z.object({ - windowId: z.number().describe('ID of the newly created window'), - tabId: z.number().describe('ID of the first tab in the new window'), -}) - -type CreateWindowInput = z.infer -type CreateWindowOutput = z.infer - -export class CreateWindowAction extends ActionHandler< - CreateWindowInput, - CreateWindowOutput -> { - readonly inputSchema = CreateWindowInputSchema - - async execute(input: CreateWindowInput): Promise { - const createData: chrome.windows.CreateData = { - url: input.url, - focused: input.focused, - incognito: input.incognito, - } - - const createdWindow = await withTimeout( - chrome.windows.create(createData), - CHROME_API_TIMEOUTS.CHROME_API, - 'chrome.windows.create', - ) - - if (!createdWindow) { - throw new Error('Failed to create window') - } - - if (createdWindow.id === undefined) { - throw new Error('Created window has no ID') - } - - const tabId = createdWindow.tabs?.[0]?.id - if (tabId === undefined) { - throw new Error('Created window has no tab') - } - - return { - windowId: createdWindow.id, - tabId, - } - } -} diff --git a/packages/browseros-agent/apps/controller-ext/src/actions/browser/ExecuteJavaScriptAction.ts b/packages/browseros-agent/apps/controller-ext/src/actions/browser/ExecuteJavaScriptAction.ts deleted file mode 100644 index 690273364..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/actions/browser/ExecuteJavaScriptAction.ts +++ /dev/null @@ -1,64 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -import { z } from 'zod' -import { BrowserOSAdapter } from '@/adapters/BrowserOSAdapter' -import { ActionHandler } from '../ActionHandler' - -// Input schema -const ExecuteJavaScriptInputSchema = z.object({ - tabId: z.number().describe('The tab ID to execute code in'), - code: z.string().describe('JavaScript code to execute'), -}) - -// Output schema -const ExecuteJavaScriptOutputSchema = z.object({ - result: z.any().describe('The result of the code execution'), -}) - -type ExecuteJavaScriptInput = z.infer -type ExecuteJavaScriptOutput = z.infer - -/** - * ExecuteJavaScriptAction - Execute JavaScript code in page context - * - * Executes arbitrary JavaScript code in the page and returns the result. - * - * Input: - * - tabId: Tab ID to execute code in - * - code: JavaScript code as string - * - * Output: - * - result: The return value of the executed code - * - * Usage: - * - Extract data from page: "document.title" - * - Manipulate DOM: "document.body.style.background = 'red'" - * - Get element values: "document.querySelector('#email').value" - * - * Example: - * { - * "tabId": 123, - * "code": "document.title" - * } - * // Returns: { result: "Google" } - */ -export class ExecuteJavaScriptAction extends ActionHandler< - ExecuteJavaScriptInput, - ExecuteJavaScriptOutput -> { - readonly inputSchema = ExecuteJavaScriptInputSchema - private browserOSAdapter = BrowserOSAdapter.getInstance() - - async execute( - input: ExecuteJavaScriptInput, - ): Promise { - const result = await this.browserOSAdapter.executeJavaScript( - input.tabId, - input.code, - ) - return { result } - } -} diff --git a/packages/browseros-agent/apps/controller-ext/src/actions/browser/GetAccessibilityTreeAction.ts b/packages/browseros-agent/apps/controller-ext/src/actions/browser/GetAccessibilityTreeAction.ts deleted file mode 100644 index 62ddd6b36..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/actions/browser/GetAccessibilityTreeAction.ts +++ /dev/null @@ -1,53 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -import { z } from 'zod' -import { BrowserOSAdapter } from '@/adapters/BrowserOSAdapter' -import { ActionHandler } from '../ActionHandler' - -const GetAccessibilityTreeInputSchema = z.object({ - tabId: z - .number() - .int() - .positive() - .describe('Tab ID to get accessibility tree from'), -}) - -type GetAccessibilityTreeInput = z.infer -export type GetAccessibilityTreeOutput = chrome.browserOS.AccessibilityTree - -/** - * GetAccessibilityTreeAction - Get accessibility tree for a tab - * - * Returns the full accessibility tree structure containing: - * - rootId: The root node ID - * - nodes: Map of node IDs to accessibility nodes - * - * Each node contains: - * - nodeId: Unique node identifier - * - role: Accessibility role (e.g., 'staticText', 'heading', 'button') - * - name: Text content or label - * - childIds: Array of child node IDs - * - * Example payload: - * { - * "tabId": 123 - * } - */ -export class GetAccessibilityTreeAction extends ActionHandler< - GetAccessibilityTreeInput, - GetAccessibilityTreeOutput -> { - readonly inputSchema = GetAccessibilityTreeInputSchema - private browserOSAdapter = BrowserOSAdapter.getInstance() - - async execute( - input: GetAccessibilityTreeInput, - ): Promise { - const { tabId } = input - const tree = await this.browserOSAdapter.getAccessibilityTree(tabId) - return tree - } -} diff --git a/packages/browseros-agent/apps/controller-ext/src/actions/browser/GetInteractiveSnapshotAction.ts b/packages/browseros-agent/apps/controller-ext/src/actions/browser/GetInteractiveSnapshotAction.ts deleted file mode 100644 index 04730560d..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/actions/browser/GetInteractiveSnapshotAction.ts +++ /dev/null @@ -1,71 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -import { z } from 'zod' -import type { - InteractiveSnapshot, - InteractiveSnapshotOptions, -} from '@/adapters/BrowserOSAdapter' -import { BrowserOSAdapter } from '@/adapters/BrowserOSAdapter' -import { SnapshotCache } from '@/utils/SnapshotCache' -import { ActionHandler } from '../ActionHandler' - -// Input schema -const GetInteractiveSnapshotInputSchema = z.object({ - tabId: z.number().describe('The tab ID to get snapshot from'), - options: z - .object({ - includeHidden: z - .boolean() - .optional() - .default(false) - .describe('Include hidden elements (default: false)'), - }) - .optional() - .describe('Optional snapshot options'), -}) - -type GetInteractiveSnapshotInput = z.infer< - typeof GetInteractiveSnapshotInputSchema -> - -/** - * GetInteractiveSnapshotAction - Get interactive elements from the page - * - * This is THE MOST CRITICAL action - it returns all interactive elements - * with their nodeIds, which are needed by click, inputText, clear, and scrollToNode actions. - * - * Returns: - * - elements: Array of interactive nodes with nodeIds - * - hierarchicalStructure: String representation of page structure - * - * Each element contains: - * - nodeId: Sequential integer ID (1, 2, 3...) - * - type: 'clickable' | 'typeable' | 'selectable' - * - name: Element text/label - * - attributes: Element properties (html-tag, role, etc.) - * - rect: Bounding box coordinates - */ -export class GetInteractiveSnapshotAction extends ActionHandler< - GetInteractiveSnapshotInput, - InteractiveSnapshot -> { - readonly inputSchema = GetInteractiveSnapshotInputSchema - private browserOSAdapter = BrowserOSAdapter.getInstance() - - async execute( - input: GetInteractiveSnapshotInput, - ): Promise { - const snapshot = await this.browserOSAdapter.getInteractiveSnapshot( - input.tabId, - input.options as InteractiveSnapshotOptions | undefined, - ) - - // Cache snapshot for pointer overlay lookup - SnapshotCache.set(input.tabId, snapshot) - - return snapshot - } -} diff --git a/packages/browseros-agent/apps/controller-ext/src/actions/browser/GetPageLoadStatusAction.ts b/packages/browseros-agent/apps/controller-ext/src/actions/browser/GetPageLoadStatusAction.ts deleted file mode 100644 index eaed9d92b..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/actions/browser/GetPageLoadStatusAction.ts +++ /dev/null @@ -1,69 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -import { z } from 'zod' -import { - BrowserOSAdapter, - type PageLoadStatus, -} from '@/adapters/BrowserOSAdapter' -import { ActionHandler } from '../ActionHandler' - -// Input schema for getPageLoadStatus action -const GetPageLoadStatusInputSchema = z.object({ - tabId: z - .number() - .int() - .positive() - .describe('Tab ID to check page load status'), -}) - -type GetPageLoadStatusInput = z.infer - -// Output includes page load status details -export interface GetPageLoadStatusOutput { - tabId: number - isResourcesLoading: boolean - isDOMContentLoaded: boolean - isPageComplete: boolean -} - -/** - * GetPageLoadStatusAction - Get page loading status for a tab - * - * Returns the current page load status including: - * - isResourcesLoading: Whether resources (images, scripts, etc.) are still loading - * - isDOMContentLoaded: Whether the DOM is fully parsed and ready - * - isPageComplete: Whether the page has completely finished loading - * - * Useful for waiting for pages to load before taking actions. - * - * Example payload: - * { - * "tabId": 123 - * } - */ -export class GetPageLoadStatusAction extends ActionHandler< - GetPageLoadStatusInput, - GetPageLoadStatusOutput -> { - readonly inputSchema = GetPageLoadStatusInputSchema - private browserOSAdapter = BrowserOSAdapter.getInstance() - - async execute( - input: GetPageLoadStatusInput, - ): Promise { - const { tabId } = input - - const status: PageLoadStatus = - await this.browserOSAdapter.getPageLoadStatus(tabId) - - return { - tabId, - isResourcesLoading: status.isResourcesLoading, - isDOMContentLoaded: status.isDOMContentLoaded, - isPageComplete: status.isPageComplete, - } - } -} diff --git a/packages/browseros-agent/apps/controller-ext/src/actions/browser/GetSnapshotAction.ts b/packages/browseros-agent/apps/controller-ext/src/actions/browser/GetSnapshotAction.ts deleted file mode 100644 index 03f887520..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/actions/browser/GetSnapshotAction.ts +++ /dev/null @@ -1,74 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -import { z } from 'zod' -import { BrowserOSAdapter, type Snapshot } from '@/adapters/BrowserOSAdapter' -import { logger } from '@/utils/logger' -import { ActionHandler } from '../ActionHandler' - -// Input schema for getSnapshot action -const GetSnapshotInputSchema = z.object({ - tabId: z.number().int().positive().describe('Tab ID to get snapshot from'), - type: z - .enum(['text', 'links']) - .default('text') - .describe('Type of snapshot: text or links'), - options: z - .object({ - context: z.enum(['visible', 'full']).optional(), - includeSections: z - .array( - z.enum([ - 'main', - 'navigation', - 'footer', - 'header', - 'article', - 'aside', - ]), - ) - .optional(), - }) - .optional() - .describe('Optional snapshot configuration'), -}) - -type GetSnapshotInput = z.infer - -// Output is the full snapshot structure -export type GetSnapshotOutput = Snapshot - -/** - * GetSnapshotAction - Extract page content snapshot - * - * Extracts structured content from the page including: - * - Headings (with levels) - * - Text content - * - Links (with URLs) - * - * Returns items in document order with type information. - * - * Example payload: - * { - * "tabId": 123, - * "type": "text" - * } - */ -export class GetSnapshotAction extends ActionHandler< - GetSnapshotInput, - GetSnapshotOutput -> { - readonly inputSchema = GetSnapshotInputSchema - private browserOSAdapter = BrowserOSAdapter.getInstance() - - async execute(input: GetSnapshotInput): Promise { - const { tabId, type } = input - logger.info( - `[GetSnapshotAction] Getting snapshot for tab ${tabId} with type ${type}`, - ) - const snapshot = await this.browserOSAdapter.getSnapshot(tabId, type) - return snapshot - } -} diff --git a/packages/browseros-agent/apps/controller-ext/src/actions/browser/InputTextAction.ts b/packages/browseros-agent/apps/controller-ext/src/actions/browser/InputTextAction.ts deleted file mode 100644 index 630100fff..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/actions/browser/InputTextAction.ts +++ /dev/null @@ -1,75 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -import { z } from 'zod' -import { BrowserOSAdapter } from '@/adapters/BrowserOSAdapter' -import { PointerOverlay } from '@/utils/PointerOverlay' -import { SnapshotCache } from '@/utils/SnapshotCache' -import { ActionHandler } from '../ActionHandler' - -// Input schema -const InputTextInputSchema = z.object({ - tabId: z.number().describe('The tab ID containing the element'), - nodeId: z - .number() - .int() - .positive() - .describe('The nodeId from interactive snapshot'), - text: z.string().describe('Text to type into the element'), -}) - -// Output schema -const InputTextOutputSchema = z.object({ - success: z.boolean().describe('Whether the input succeeded'), -}) - -type InputTextInput = z.infer -type InputTextOutput = z.infer - -/** - * InputTextAction - Type text into an element by its nodeId - * - * This action types text into an input field or textarea identified by its nodeId. - * - * Prerequisites: - * - Must call getInteractiveSnapshot first to get valid nodeIds - * - Element must be typeable (type: 'typeable' in snapshot) - * - NodeIds are valid only for the current page state - * - * Behavior: - * - Automatically clears existing text before typing (handled by adapter) - * - Types the full text string - * - Triggers input/change events - * - * Usage: - * 1. Get snapshot to find typeable elements - * 2. Choose input field by nodeId - * 3. Call inputText with tabId, nodeId, and text - * - * Used by: TypeTool, form automation workflows - */ -export class InputTextAction extends ActionHandler< - InputTextInput, - InputTextOutput -> { - readonly inputSchema = InputTextInputSchema - private browserOSAdapter = BrowserOSAdapter.getInstance() - - async execute(input: InputTextInput): Promise { - // Show pointer overlay before typing - const rect = SnapshotCache.getNodeRect(input.tabId, input.nodeId) - if (rect) { - const { x, y } = PointerOverlay.getLeftCenterCoordinates(rect) - const textPreview = - input.text.length > 20 - ? `Type: ${input.text.substring(0, 20)}...` - : `Type: ${input.text}` - await PointerOverlay.showPointerAndWait(input.tabId, x, y, textPreview) - } - - await this.browserOSAdapter.inputText(input.tabId, input.nodeId, input.text) - return { success: true } - } -} diff --git a/packages/browseros-agent/apps/controller-ext/src/actions/browser/ScrollDownAction.ts b/packages/browseros-agent/apps/controller-ext/src/actions/browser/ScrollDownAction.ts deleted file mode 100644 index ad49b5824..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/actions/browser/ScrollDownAction.ts +++ /dev/null @@ -1,54 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -import { z } from 'zod' -import { BrowserOSAdapter } from '@/adapters/BrowserOSAdapter' -import { ActionHandler } from '../ActionHandler' - -// Input schema -const ScrollDownInputSchema = z.object({ - tabId: z.number().describe('The tab ID to scroll'), -}) - -// Output schema -const ScrollDownOutputSchema = z.object({ - success: z.boolean().describe('Whether the scroll succeeded'), -}) - -type ScrollDownInput = z.infer -type ScrollDownOutput = z.infer - -/** - * ScrollDownAction - Scroll page down - * - * Scrolls the page down by one viewport height using PageDown key. - * This approach is more reliable than the direct scrollDown API. - * - * Input: - * - tabId: Tab ID to scroll - * - * Output: - * - success: true if scroll succeeded - * - * Usage: - * Used for scrolling through long pages to view content below the fold. - */ -export class ScrollDownAction extends ActionHandler< - ScrollDownInput, - ScrollDownOutput -> { - readonly inputSchema = ScrollDownInputSchema - private browserOSAdapter = BrowserOSAdapter.getInstance() - - async execute(input: ScrollDownInput): Promise { - // Use sendKeys with PageDown instead of scrollDown API (more reliable) - await this.browserOSAdapter.sendKeys(input.tabId, 'PageDown') - - // Add small delay for scroll to complete - await new Promise((resolve) => setTimeout(resolve, 100)) - - return { success: true } - } -} diff --git a/packages/browseros-agent/apps/controller-ext/src/actions/browser/ScrollToNodeAction.ts b/packages/browseros-agent/apps/controller-ext/src/actions/browser/ScrollToNodeAction.ts deleted file mode 100644 index 155e04308..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/actions/browser/ScrollToNodeAction.ts +++ /dev/null @@ -1,42 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -import { z } from 'zod' -import { BrowserOSAdapter } from '@/adapters/BrowserOSAdapter' -import { ActionHandler } from '../ActionHandler' - -const ScrollToNodeInputSchema = z.object({ - tabId: z.number().describe('The tab ID containing the element'), - nodeId: z.number().int().positive().describe('The nodeId to scroll to'), -}) - -type ScrollToNodeInput = z.infer -interface ScrollToNodeOutput { - scrolled: boolean -} - -/** - * ScrollToNodeAction - Scroll an element into view - * - * Scrolls the page so that the specified element is visible in the viewport. - * Returns whether scrolling actually occurred. - * - * Used by: Click/Type tools to ensure element is visible before interaction - */ -export class ScrollToNodeAction extends ActionHandler< - ScrollToNodeInput, - ScrollToNodeOutput -> { - readonly inputSchema = ScrollToNodeInputSchema - private browserOSAdapter = BrowserOSAdapter.getInstance() - - async execute(input: ScrollToNodeInput): Promise { - const scrolled = await this.browserOSAdapter.scrollToNode( - input.tabId, - input.nodeId, - ) - return { scrolled } - } -} diff --git a/packages/browseros-agent/apps/controller-ext/src/actions/browser/ScrollUpAction.ts b/packages/browseros-agent/apps/controller-ext/src/actions/browser/ScrollUpAction.ts deleted file mode 100644 index 88fbd4bd3..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/actions/browser/ScrollUpAction.ts +++ /dev/null @@ -1,54 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -import { z } from 'zod' -import { BrowserOSAdapter } from '@/adapters/BrowserOSAdapter' -import { ActionHandler } from '../ActionHandler' - -// Input schema -const ScrollUpInputSchema = z.object({ - tabId: z.number().describe('The tab ID to scroll'), -}) - -// Output schema -const ScrollUpOutputSchema = z.object({ - success: z.boolean().describe('Whether the scroll succeeded'), -}) - -type ScrollUpInput = z.infer -type ScrollUpOutput = z.infer - -/** - * ScrollUpAction - Scroll page up - * - * Scrolls the page up by one viewport height using PageUp key. - * This approach is more reliable than the direct scrollUp API. - * - * Input: - * - tabId: Tab ID to scroll - * - * Output: - * - success: true if scroll succeeded - * - * Usage: - * Used for scrolling back up through long pages. - */ -export class ScrollUpAction extends ActionHandler< - ScrollUpInput, - ScrollUpOutput -> { - readonly inputSchema = ScrollUpInputSchema - private browserOSAdapter = BrowserOSAdapter.getInstance() - - async execute(input: ScrollUpInput): Promise { - // Use sendKeys with PageUp instead of scrollUp API (more reliable) - await this.browserOSAdapter.sendKeys(input.tabId, 'PageUp') - - // Add small delay for scroll to complete - await new Promise((resolve) => setTimeout(resolve, 100)) - - return { success: true } - } -} diff --git a/packages/browseros-agent/apps/controller-ext/src/actions/browser/SendKeysAction.ts b/packages/browseros-agent/apps/controller-ext/src/actions/browser/SendKeysAction.ts deleted file mode 100644 index fb090fbd5..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/actions/browser/SendKeysAction.ts +++ /dev/null @@ -1,69 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -import { z } from 'zod' -import { getBrowserOSAdapter } from '@/adapters/BrowserOSAdapter' -import { ActionHandler } from '../ActionHandler' - -// Input schema for sendKeys action -const SendKeysInputSchema = z.object({ - tabId: z.number().int().positive().describe('Tab ID to send keys to'), - key: z - .enum([ - 'Enter', - 'Delete', - 'Backspace', - 'Tab', - 'Escape', - 'ArrowUp', - 'ArrowDown', - 'ArrowLeft', - 'ArrowRight', - 'Home', - 'End', - 'PageUp', - 'PageDown', - ]) - .describe('Keyboard key to send'), -}) - -type SendKeysInput = z.infer - -// Output is just success (void result) -export interface SendKeysOutput { - success: boolean - message: string -} - -/** - * SendKeysAction - Send keyboard keys to a tab - * - * Sends special keyboard keys (Enter, Escape, arrows, etc.) to the specified tab. - * Useful for navigation, form submission, closing dialogs, etc. - * - * Example payload: - * { - * "tabId": 123, - * "key": "Enter" - * } - */ -export class SendKeysAction extends ActionHandler< - SendKeysInput, - SendKeysOutput -> { - readonly inputSchema = SendKeysInputSchema - private browserOS = getBrowserOSAdapter() - - async execute(input: SendKeysInput): Promise { - const { tabId, key } = input - - await this.browserOS.sendKeys(tabId, key as chrome.browserOS.Key) - - return { - success: true, - message: `Successfully sent "${key}" to tab ${tabId}`, - } - } -} diff --git a/packages/browseros-agent/apps/controller-ext/src/actions/browser/TypeAtCoordinatesAction.ts b/packages/browseros-agent/apps/controller-ext/src/actions/browser/TypeAtCoordinatesAction.ts deleted file mode 100644 index d0f99da09..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/actions/browser/TypeAtCoordinatesAction.ts +++ /dev/null @@ -1,81 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -import { z } from 'zod' -import { getBrowserOSAdapter } from '@/adapters/BrowserOSAdapter' -import { PointerOverlay } from '@/utils/PointerOverlay' -import { ActionHandler } from '../ActionHandler' - -// Input schema for typeAtCoordinates action -const TypeAtCoordinatesInputSchema = z.object({ - tabId: z.number().int().positive().describe('Tab ID to type in'), - x: z.number().int().nonnegative().describe('X coordinate in viewport pixels'), - y: z.number().int().nonnegative().describe('Y coordinate in viewport pixels'), - text: z.string().min(1).describe('Text to type at the location'), -}) - -type TypeAtCoordinatesInput = z.infer - -// Output confirms the typing -export interface TypeAtCoordinatesOutput { - success: boolean - message: string - coordinates: { - x: number - y: number - } - textLength: number -} - -/** - * TypeAtCoordinatesAction - Type text at specific viewport coordinates - * - * Clicks at the specified (x, y) coordinates and types the provided text. - * Coordinates are in pixels relative to the top-left of the visible viewport (0, 0). - * - * The action will: - * 1. Click at the coordinates to focus the element - * 2. Type the specified text - * - * Useful when: - * - Input fields don't have accessible node IDs - * - Working with complex forms or canvas-based inputs - * - Vision-based automation (e.g., AI identifies input coordinates from screenshots) - * - * Example payload: - * { - * "tabId": 123, - * "x": 500, - * "y": 300, - * "text": "Hello World" - * } - */ -export class TypeAtCoordinatesAction extends ActionHandler< - TypeAtCoordinatesInput, - TypeAtCoordinatesOutput -> { - readonly inputSchema = TypeAtCoordinatesInputSchema - private browserOS = getBrowserOSAdapter() - - async execute( - input: TypeAtCoordinatesInput, - ): Promise { - const { tabId, x, y, text } = input - - // Show pointer overlay before typing - const textPreview = - text.length > 20 ? `Type: ${text.substring(0, 20)}...` : `Type: ${text}` - await PointerOverlay.showPointerAndWait(tabId, x, y, textPreview) - - await this.browserOS.typeAtCoordinates(tabId, x, y, text) - - return { - success: true, - message: `Successfully typed "${text.substring(0, 50)}${text.length > 50 ? '...' : ''}" at coordinates (${x}, ${y}) in tab ${tabId}`, - coordinates: { x, y }, - textLength: text.length, - } - } -} diff --git a/packages/browseros-agent/apps/controller-ext/src/actions/diagnostics/CheckBrowserOSAction.ts b/packages/browseros-agent/apps/controller-ext/src/actions/diagnostics/CheckBrowserOSAction.ts deleted file mode 100644 index c9bb0a88e..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/actions/diagnostics/CheckBrowserOSAction.ts +++ /dev/null @@ -1,90 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -import { z } from 'zod' - -import { ActionHandler } from '../ActionHandler' - -// Input schema - no input needed -const CheckBrowserOSInputSchema = z.any() - -// Output schema -const CheckBrowserOSOutputSchema = z.object({ - available: z.boolean(), - apis: z.array(z.string()).optional(), - error: z.string().optional(), -}) - -type CheckBrowserOSInput = z.infer -type CheckBrowserOSOutput = z.infer - -/** - * CheckBrowserOSAction - Diagnostic action to check if chrome.browserOS is available - * - * This action checks: - * 1. Whether chrome.browserOS namespace exists - * 2. What APIs are available in the namespace - * 3. Returns detailed diagnostic information - */ -export class CheckBrowserOSAction extends ActionHandler< - CheckBrowserOSInput, - CheckBrowserOSOutput -> { - readonly inputSchema = CheckBrowserOSInputSchema - - async execute(_input: CheckBrowserOSInput): Promise { - try { - console.log('[CheckBrowserOSAction] Starting diagnostic...') - console.log('[CheckBrowserOSAction] typeof chrome:', typeof chrome) - console.log('[CheckBrowserOSAction] chrome exists:', chrome !== undefined) - - // Check if chrome.browserOS exists - const browserOSExists = typeof chrome.browserOS !== 'undefined' - console.log( - '[CheckBrowserOSAction] typeof chrome.browserOS:', - typeof chrome.browserOS, - ) - console.log('[CheckBrowserOSAction] browserOSExists:', browserOSExists) - - if (!browserOSExists) { - console.log('[CheckBrowserOSAction] chrome.browserOS is NOT available') - return { - available: false, - error: - 'chrome.browserOS is undefined - not running in BrowserOS Chrome', - } - } - - // Get available APIs - const apis: string[] = [] - const browserOS = chrome.browserOS as Record - - for (const key in browserOS) { - if (typeof browserOS[key] === 'function') { - apis.push(key) - } - } - - console.log('[CheckBrowserOSAction] Found APIs:', apis) - - return { - available: true, - apis: apis.sort(), - } - } catch (error) { - console.error('[CheckBrowserOSAction] Error during diagnostic:', error) - const errorMsg = - error instanceof Error - ? error.message - : error - ? String(error) - : 'Unknown error' - return { - available: false, - error: errorMsg, - } - } - } -} diff --git a/packages/browseros-agent/apps/controller-ext/src/actions/history/GetRecentHistoryAction.ts b/packages/browseros-agent/apps/controller-ext/src/actions/history/GetRecentHistoryAction.ts deleted file mode 100644 index 23bb33a61..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/actions/history/GetRecentHistoryAction.ts +++ /dev/null @@ -1,96 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -import { z } from 'zod' -import { HistoryAdapter } from '@/adapters/HistoryAdapter' -import { ActionHandler } from '../ActionHandler' - -// Input schema -const GetRecentHistoryInputSchema = z.object({ - maxResults: z - .number() - .int() - .positive() - .optional() - .default(20) - .describe('Maximum number of results (default: 20)'), - hoursBack: z - .number() - .int() - .positive() - .optional() - .default(24) - .describe('How many hours back to search (default: 24)'), -}) - -// Output schema -const GetRecentHistoryOutputSchema = z.object({ - items: z.array( - z.object({ - id: z.string(), - url: z.string().optional(), - title: z.string().optional(), - lastVisitTime: z.number().optional(), - visitCount: z.number().optional(), - }), - ), - count: z.number(), -}) - -type GetRecentHistoryInput = z.infer -type GetRecentHistoryOutput = z.infer - -/** - * GetRecentHistoryAction - Get recent browser history - * - * Retrieves the most recent browser history items. - * - * Input: - * - maxResults (optional): Max results (default: 20) - * - hoursBack (optional): Time range in hours (default: 24) - * - * Output: - * - items: Array of recent history items - * - count: Number of items returned - * - * Usage: - * - Last 24 hours: { } - * - Last hour: { "hoursBack": 1 } - * - Last week: { "hoursBack": 168, "maxResults": 50 } - * - * Example: - * { - * "maxResults": 10, - * "hoursBack": 1 - * } - * // Returns: { items: [{url: "https://google.com", title: "Google", lastVisitTime: 1729012345678}], count: 10 } - */ -export class GetRecentHistoryAction extends ActionHandler< - GetRecentHistoryInput, - GetRecentHistoryOutput -> { - readonly inputSchema = GetRecentHistoryInputSchema - private historyAdapter = new HistoryAdapter() - - async execute(input: GetRecentHistoryInput): Promise { - const results = await this.historyAdapter.getRecentHistory( - input.maxResults, - input.hoursBack, - ) - - const items = results.map((item) => ({ - id: item.id, - url: item.url, - title: item.title, - lastVisitTime: item.lastVisitTime, - visitCount: item.visitCount, - })) - - return { - items, - count: items.length, - } - } -} diff --git a/packages/browseros-agent/apps/controller-ext/src/actions/history/SearchHistoryAction.ts b/packages/browseros-agent/apps/controller-ext/src/actions/history/SearchHistoryAction.ts deleted file mode 100644 index e72ac9167..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/actions/history/SearchHistoryAction.ts +++ /dev/null @@ -1,104 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -import { z } from 'zod' -import { HistoryAdapter } from '@/adapters/HistoryAdapter' -import { ActionHandler } from '../ActionHandler' - -// Input schema -const SearchHistoryInputSchema = z.object({ - query: z.string().describe('Search query to match URL or title'), - maxResults: z - .number() - .int() - .positive() - .optional() - .default(20) - .describe('Maximum number of results (default: 20)'), - startTime: z - .number() - .optional() - .describe('Start time in milliseconds since epoch (optional)'), - endTime: z - .number() - .optional() - .describe('End time in milliseconds since epoch (optional)'), -}) - -// Output schema -const SearchHistoryOutputSchema = z.object({ - items: z.array( - z.object({ - id: z.string(), - url: z.string().optional(), - title: z.string().optional(), - lastVisitTime: z.number().optional(), - visitCount: z.number().optional(), - typedCount: z.number().optional(), - }), - ), - count: z.number(), -}) - -type SearchHistoryInput = z.infer -type SearchHistoryOutput = z.infer - -/** - * SearchHistoryAction - Search browser history - * - * Searches browser history for matching URLs and titles. - * - * Input: - * - query: Search text (matches URL and title) - * - maxResults (optional): Max results (default: 20) - * - startTime (optional): Start time filter - * - endTime (optional): End time filter - * - * Output: - * - items: Array of history items - * - count: Number of items returned - * - * Usage: - * - Simple search: { "query": "github" } - * - With limit: { "query": "google", "maxResults": 10 } - * - Time range: { "query": "", "startTime": 1729000000000, "endTime": 1729100000000 } - * - * Example: - * { - * "query": "github", - * "maxResults": 5 - * } - * // Returns: { items: [{url: "https://github.com", title: "GitHub", visitCount: 42}], count: 1 } - */ -export class SearchHistoryAction extends ActionHandler< - SearchHistoryInput, - SearchHistoryOutput -> { - readonly inputSchema = SearchHistoryInputSchema - private historyAdapter = new HistoryAdapter() - - async execute(input: SearchHistoryInput): Promise { - const results = await this.historyAdapter.searchHistory( - input.query, - input.maxResults, - input.startTime, - input.endTime, - ) - - const items = results.map((item) => ({ - id: item.id, - url: item.url, - title: item.title, - lastVisitTime: item.lastVisitTime, - visitCount: item.visitCount, - typedCount: item.typedCount, - })) - - return { - items, - count: items.length, - } - } -} diff --git a/packages/browseros-agent/apps/controller-ext/src/actions/tab/CloseTabAction.ts b/packages/browseros-agent/apps/controller-ext/src/actions/tab/CloseTabAction.ts deleted file mode 100644 index 20a71a52b..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/actions/tab/CloseTabAction.ts +++ /dev/null @@ -1,61 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -import { z } from 'zod' -import { TabAdapter } from '@/adapters/TabAdapter' -import { ActionHandler } from '../ActionHandler' - -// Input schema -const CloseTabInputSchema = z.object({ - tabId: z.number().int().positive().describe('Tab ID to close'), -}) - -// Output schema -const CloseTabOutputSchema = z.object({ - success: z.boolean().describe('Whether the tab was successfully closed'), - message: z.string().describe('Confirmation message'), -}) - -type CloseTabInput = z.infer -type CloseTabOutput = z.infer - -/** - * CloseTabAction - Close a specific tab by ID - * - * Closes the tab with the given ID. - * - * Input: - * - tabId: ID of the tab to close - * - * Output: - * - success: true if tab was closed - * - message: Confirmation message - * - * Usage: - * Use this to close tabs that are no longer needed. - * You can get tab IDs from the getTabs or openTab actions. - * - * Example: - * { - * "tabId": 123 - * } - * // Returns: { success: true, message: "Closed tab 123" } - */ -export class CloseTabAction extends ActionHandler< - CloseTabInput, - CloseTabOutput -> { - readonly inputSchema = CloseTabInputSchema - private tabAdapter = new TabAdapter() - - async execute(input: CloseTabInput): Promise { - await this.tabAdapter.closeTab(input.tabId) - - return { - success: true, - message: `Closed tab ${input.tabId}`, - } - } -} diff --git a/packages/browseros-agent/apps/controller-ext/src/actions/tab/GetActiveTabAction.ts b/packages/browseros-agent/apps/controller-ext/src/actions/tab/GetActiveTabAction.ts deleted file mode 100644 index 904218c86..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/actions/tab/GetActiveTabAction.ts +++ /dev/null @@ -1,103 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -import { z } from 'zod' -import { TabAdapter } from '@/adapters/TabAdapter' -import { ActionHandler } from '../ActionHandler' - -/** - * GetActiveTabAction - Returns information about the currently active tab - * - * Input: None (void) - * Output: { tabId, url, title, windowId } - * - * Use Case: - * - Agent needs to know which tab user is currently viewing - * - Required for most automation actions (need to know target tab) - * - * Example Request: - * { - * "id": "req-123", - * "action": "getActiveTab", - * "payload": {} - * } - * - * Example Response: - * { - * "id": "req-123", - * "ok": true, - * "data": { - * "tabId": 5, - * "url": "https://google.com", - * "title": "Google", - * "windowId": 1 - * } - * } - */ - -// Input schema - accepts optional windowId for multi-window support -const GetActiveTabInputSchema = z - .object({ - windowId: z - .number() - .int() - .optional() - .describe( - 'Window ID to get active tab from. If not provided, uses current window.', - ), - }) - .passthrough() - -// Output type -export interface GetActiveTabOutput { - tabId: number - url: string - title: string - windowId: number -} - -type GetActiveTabInput = z.infer - -export class GetActiveTabAction extends ActionHandler< - GetActiveTabInput, - GetActiveTabOutput -> { - readonly inputSchema = GetActiveTabInputSchema - private tabAdapter = new TabAdapter() - - /** - * Execute getActiveTab action - * - * Logic: - * 1. Get active tab via TabAdapter (using windowId if provided) - * 2. Extract relevant fields - * 3. Return typed result - * - * @param input - Optional windowId to specify which window - * @returns Active tab information - * @throws Error if no active tab found - */ - async execute(input: GetActiveTabInput): Promise { - // Get active tab from Chrome (use windowId if provided) - const tab = await this.tabAdapter.getActiveTab(input.windowId) - - // Validate required fields exist - if (tab.id === undefined) { - throw new Error('Active tab has no ID') - } - - if (tab.windowId === undefined) { - throw new Error('Active tab has no window ID') - } - - // Return typed result - return { - tabId: tab.id, - url: tab.url || '', - title: tab.title || '', - windowId: tab.windowId, - } - } -} diff --git a/packages/browseros-agent/apps/controller-ext/src/actions/tab/GetTabsAction.ts b/packages/browseros-agent/apps/controller-ext/src/actions/tab/GetTabsAction.ts deleted file mode 100644 index 35482f665..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/actions/tab/GetTabsAction.ts +++ /dev/null @@ -1,122 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -import { z } from 'zod' -import { TabAdapter } from '@/adapters/TabAdapter' -import { ActionHandler } from '../ActionHandler' - -// Input schema for getTabs action -const GetTabsInputSchema = z - .object({ - currentWindowOnly: z - .boolean() - .optional() - .default(false) - .describe('If true, return only tabs in current window'), - windowId: z - .number() - .int() - .optional() - .describe('If specified, return tabs in this window only'), - url: z - .string() - .optional() - .describe( - 'URL pattern to filter tabs (supports wildcards like "*://*.google.com/*")', - ), - title: z.string().optional().describe('Title pattern to filter tabs'), - }) - .describe('Optional filters for querying tabs') - -type GetTabsInput = z.infer - -// Tab info in output -interface TabInfo { - id: number - url: string - title: string - windowId: number - active: boolean - index: number -} - -// Output with array of tabs -export interface GetTabsOutput { - tabs: TabInfo[] - count: number -} - -/** - * GetTabsAction - List all available tabs - * - * Returns a list of all tabs (or filtered tabs) with their IDs, URLs, titles, and window info. - * Essential for discovering which tabs exist before taking actions on them. - * - * Filters (all optional): - * - currentWindowOnly: true to only get tabs in the current window - * - windowId: Get tabs in a specific window - * - url: URL pattern (supports wildcards like "*://*.google.com/*") - * - title: Title pattern (supports wildcards) - * - * Example payloads: - * - * Get all tabs across all windows: - * {} - * - * Get tabs in current window only: - * { "currentWindowOnly": true } - * - * Get tabs in specific window: - * { "windowId": 12345 } - * - * Get all Google tabs: - * { "url": "*://*.google.com/*" } - */ -export class GetTabsAction extends ActionHandler { - readonly inputSchema = GetTabsInputSchema - private tabAdapter = new TabAdapter() - - async execute(input: GetTabsInput): Promise { - let tabs: chrome.tabs.Tab[] - - // Apply filters based on input - if (input.windowId) { - // Get tabs in specific window (windowId takes precedence) - tabs = await this.tabAdapter.getTabsInWindow(input.windowId) - } else if (input.currentWindowOnly) { - // Get tabs in current window (windowId may be injected by agent for multi-window support) - tabs = await this.tabAdapter.getCurrentWindowTabs() - } else if (input.url || input.title) { - // Use query API for URL/title filtering - const query: chrome.tabs.QueryInfo = {} - if (input.url) query.url = input.url - if (input.title) query.title = input.title - tabs = await this.tabAdapter.queryTabs(query) - } else { - // Get all tabs - tabs = await this.tabAdapter.getAllTabs() - } - - // Convert to simplified TabInfo format - const tabInfos: TabInfo[] = tabs - .filter( - (tab): tab is chrome.tabs.Tab & { id: number; windowId: number } => - tab.id !== undefined && tab.windowId !== undefined, - ) - .map((tab) => ({ - id: tab.id, - url: tab.url || '', - title: tab.title || '', - windowId: tab.windowId, - active: tab.active || false, - index: tab.index, - })) - - return { - tabs: tabInfos, - count: tabInfos.length, - } - } -} diff --git a/packages/browseros-agent/apps/controller-ext/src/actions/tab/GroupTabsAction.ts b/packages/browseros-agent/apps/controller-ext/src/actions/tab/GroupTabsAction.ts deleted file mode 100644 index 2af4581e2..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/actions/tab/GroupTabsAction.ts +++ /dev/null @@ -1,126 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -import { z } from 'zod' -import { TabAdapter } from '@/adapters/TabAdapter' -import { ActionHandler } from '../ActionHandler' - -const TabGroupColorSchema = z.enum([ - 'grey', - 'blue', - 'red', - 'yellow', - 'green', - 'pink', - 'purple', - 'cyan', - 'orange', -]) - -const GroupTabsInputSchema = z - .object({ - tabIds: z - .array(z.number().int().positive()) - .min(1) - .describe('Array of tab IDs to group together'), - title: z - .string() - .optional() - .describe('Title for the group (e.g., "Shopping", "Work", "Research")'), - color: TabGroupColorSchema.optional().describe( - 'Color for the group: grey, blue, red, yellow, green, pink, purple, cyan, orange', - ), - groupId: z - .number() - .int() - .optional() - .describe( - 'Existing group ID to add tabs to. If not specified, creates a new group.', - ), - windowId: z - .number() - .int() - .optional() - .describe('Window ID for scoping the group lookup'), - }) - .describe('Group tabs together with optional title and color') - -type GroupTabsInput = z.infer - -export interface GroupTabsOutput { - groupId: number - title: string - color: string - tabCount: number -} - -/** - * GroupTabsAction - Group tabs together - * - * Groups the specified tabs together into a new or existing group. - * Optionally sets a title and color for the group. - * - * Example payloads: - * - * Create new group with tabs: - * { "tabIds": [123, 456, 789], "title": "Shopping", "color": "green" } - * - * Add tabs to existing group: - * { "tabIds": [123, 456], "groupId": 1 } - * - * Create unnamed group: - * { "tabIds": [123, 456] } - */ -export class GroupTabsAction extends ActionHandler< - GroupTabsInput, - GroupTabsOutput -> { - readonly inputSchema = GroupTabsInputSchema - private tabAdapter = new TabAdapter() - - async execute(input: GroupTabsInput): Promise { - // Group the tabs (pass windowId to prevent tabs moving to wrong window) - const groupId = await this.tabAdapter.groupTabs( - input.tabIds, - input.groupId, - input.windowId, - ) - - // Update group properties if title or color provided - if (input.title !== undefined || input.color !== undefined) { - const updateProps: chrome.tabGroups.UpdateProperties = {} - if (input.title !== undefined) updateProps.title = input.title - if (input.color !== undefined) updateProps.color = input.color - - const updatedGroup = await this.tabAdapter.updateTabGroup( - groupId, - updateProps, - ) - - return { - groupId, - title: updatedGroup.title || '', - color: updatedGroup.color, - tabCount: input.tabIds.length, - } - } - - // Get group info if no updates were made - // Determine which window to query - use windowId if provided, otherwise query all windows - const groups = await this.tabAdapter.getTabGroups(input.windowId) - const group = groups.find((g) => g.id === groupId) - - if (!group) { - throw new Error(`Tab group ${groupId} not found`) - } - - return { - groupId, - title: group.title || '', - color: group.color, - tabCount: input.tabIds.length, - } - } -} diff --git a/packages/browseros-agent/apps/controller-ext/src/actions/tab/ListTabGroupsAction.ts b/packages/browseros-agent/apps/controller-ext/src/actions/tab/ListTabGroupsAction.ts deleted file mode 100644 index ee020f0dc..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/actions/tab/ListTabGroupsAction.ts +++ /dev/null @@ -1,83 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -import { z } from 'zod' -import { TabAdapter } from '@/adapters/TabAdapter' -import { ActionHandler } from '../ActionHandler' - -const ListTabGroupsInputSchema = z - .object({ - windowId: z - .number() - .int() - .optional() - .describe( - 'Window ID to get groups from. If not specified, gets all groups.', - ), - }) - .describe('Optional filters for querying tab groups') - -type ListTabGroupsInput = z.infer - -interface TabGroupInfo { - id: number - windowId: number - title: string - color: string - collapsed: boolean - tabIds: number[] -} - -export interface ListTabGroupsOutput { - groups: TabGroupInfo[] - count: number -} - -/** - * ListTabGroupsAction - List all tab groups - * - * Returns a list of all tab groups with their IDs, titles, colors, and member tabs. - * - * Example payloads: - * - * Get all groups across all windows: - * {} - * - * Get groups in specific window: - * { "windowId": 12345 } - */ -export class ListTabGroupsAction extends ActionHandler< - ListTabGroupsInput, - ListTabGroupsOutput -> { - readonly inputSchema = ListTabGroupsInputSchema - private tabAdapter = new TabAdapter() - - async execute(input: ListTabGroupsInput): Promise { - const groups = await this.tabAdapter.getTabGroups(input.windowId) - - // Get all tabs to find which tabs belong to each group - const tabs = input.windowId - ? await this.tabAdapter.getTabsInWindow(input.windowId) - : await this.tabAdapter.getAllTabs() - - // Build group info with tab IDs - const groupInfos: TabGroupInfo[] = groups.map((group) => ({ - id: group.id, - windowId: group.windowId, - title: group.title || '', - color: group.color, - collapsed: group.collapsed, - tabIds: tabs - .filter((tab) => tab.groupId === group.id && tab.id !== undefined) - .map((tab) => tab.id as number), - })) - - return { - groups: groupInfos, - count: groupInfos.length, - } - } -} diff --git a/packages/browseros-agent/apps/controller-ext/src/actions/tab/NavigateAction.ts b/packages/browseros-agent/apps/controller-ext/src/actions/tab/NavigateAction.ts deleted file mode 100644 index 2140fd754..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/actions/tab/NavigateAction.ts +++ /dev/null @@ -1,93 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -import { z } from 'zod' -import { TabAdapter } from '@/adapters/TabAdapter' -import { ActionHandler } from '../ActionHandler' - -// Input schema -const NavigateInputSchema = z.object({ - url: z.string().url().describe('URL to navigate to (must include https://)'), - tabId: z - .number() - .int() - .positive() - .optional() - .describe('Tab ID to navigate (optional, defaults to active tab)'), - windowId: z - .number() - .int() - .optional() - .describe('Window ID for getting active tab when tabId not provided'), -}) - -// Output schema -const NavigateOutputSchema = z.object({ - tabId: z.number().describe('ID of the navigated tab'), - windowId: z.number().describe('ID of the window containing the tab'), - url: z.string().describe('URL that the tab is navigating to'), - message: z.string().describe('Confirmation message'), -}) - -type NavigateInput = z.infer -type NavigateOutput = z.infer - -/** - * NavigateAction - Navigate a tab to a URL - * - * Navigates the current tab or a specific tab to a URL. - * - * Input: - * - url: URL to navigate to (must be a valid URL with protocol) - * - tabId (optional): Specific tab to navigate (defaults to active tab) - * - * Output: - * - tabId: ID of the tab that was navigated - * - url: URL that the tab is navigating to - * - message: Confirmation message - * - * Usage: - * - Navigate active tab: { "url": "https://google.com" } - * - Navigate specific tab: { "url": "https://google.com", "tabId": 123 } - * - * Example: - * { - * "url": "https://www.wikipedia.org" - * } - * // Returns: { tabId: 123, url: "https://www.wikipedia.org", message: "Navigating to https://www.wikipedia.org" } - */ -export class NavigateAction extends ActionHandler< - NavigateInput, - NavigateOutput -> { - readonly inputSchema = NavigateInputSchema - private tabAdapter = new TabAdapter() - - async execute(input: NavigateInput): Promise { - // If no tabId provided, use the active tab (in specified window if provided) - let targetTabId = input.tabId - - if (!targetTabId) { - const activeTab = await this.tabAdapter.getActiveTab(input.windowId) - if (activeTab.id === undefined) { - throw new Error('Active tab has no ID') - } - targetTabId = activeTab.id - } - - // Navigate the tab - const tab = await this.tabAdapter.navigateTab(targetTabId, input.url) - - if (tab.id === undefined || tab.windowId === undefined) { - throw new Error('Navigated tab has no ID or windowId') - } - return { - tabId: tab.id, - windowId: tab.windowId, - url: input.url, - message: `Navigating to ${input.url}`, - } - } -} diff --git a/packages/browseros-agent/apps/controller-ext/src/actions/tab/OpenTabAction.ts b/packages/browseros-agent/apps/controller-ext/src/actions/tab/OpenTabAction.ts deleted file mode 100644 index d74c4b590..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/actions/tab/OpenTabAction.ts +++ /dev/null @@ -1,88 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -import { z } from 'zod' -import { TabAdapter } from '@/adapters/TabAdapter' -import { ActionHandler } from '../ActionHandler' - -// Input schema -const OpenTabInputSchema = z.object({ - url: z - .string() - .url() - .optional() - .describe('URL to open (optional, defaults to new tab page)'), - active: z - .boolean() - .optional() - .default(true) - .describe('Whether to make the new tab active'), - windowId: z - .number() - .int() - .optional() - .describe( - 'Window ID to open the tab in. If not provided, opens in current window.', - ), -}) - -// Output schema -const OpenTabOutputSchema = z.object({ - tabId: z.number().describe('ID of the newly created tab'), - url: z.string().describe('URL of the new tab'), - title: z.string().optional().describe('Title of the new tab'), -}) - -type OpenTabInput = z.infer -type OpenTabOutput = z.infer - -/** - * OpenTabAction - Open a new browser tab - * - * Opens a new tab with an optional URL. If no URL is provided, - * opens a new tab page. - * - * Input: - * - url (optional): URL to open in the new tab - * - active (optional): Whether to make the tab active (default: true) - * - * Output: - * - tabId: ID of the newly created tab - * - url: URL of the new tab - * - title: Title of the new tab (if available) - * - * Usage: - * - Open blank tab: { } - * - Open specific URL: { "url": "https://google.com" } - * - Open in background: { "url": "https://google.com", "active": false } - * - * Example: - * { - * "url": "https://www.google.com", - * "active": true - * } - * // Returns: { tabId: 456, url: "https://www.google.com", title: "Google" } - */ -export class OpenTabAction extends ActionHandler { - readonly inputSchema = OpenTabInputSchema - private tabAdapter = new TabAdapter() - - async execute(input: OpenTabInput): Promise { - const tab = await this.tabAdapter.openTab( - input.url, - input.active ?? true, - input.windowId, - ) - - if (tab.id === undefined) { - throw new Error('Opened tab has no ID') - } - return { - tabId: tab.id, - url: tab.url || tab.pendingUrl || input.url || 'chrome://newtab/', - title: tab.title, - } - } -} diff --git a/packages/browseros-agent/apps/controller-ext/src/actions/tab/SwitchTabAction.ts b/packages/browseros-agent/apps/controller-ext/src/actions/tab/SwitchTabAction.ts deleted file mode 100644 index 5686a6d8b..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/actions/tab/SwitchTabAction.ts +++ /dev/null @@ -1,66 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -import { z } from 'zod' -import { TabAdapter } from '@/adapters/TabAdapter' -import { ActionHandler } from '../ActionHandler' - -// Input schema -const SwitchTabInputSchema = z.object({ - tabId: z.number().int().positive().describe('Tab ID to switch to'), -}) - -// Output schema -const SwitchTabOutputSchema = z.object({ - tabId: z.number().describe('ID of the tab that is now active'), - url: z.string().describe('URL of the active tab'), - title: z.string().describe('Title of the active tab'), -}) - -type SwitchTabInput = z.infer -type SwitchTabOutput = z.infer - -/** - * SwitchTabAction - Switch to (focus) a specific tab - * - * Makes the specified tab the active tab in its window. - * - * Input: - * - tabId: ID of the tab to switch to - * - * Output: - * - tabId: ID of the now-active tab - * - url: URL of the active tab - * - title: Title of the active tab - * - * Usage: - * Use this to switch between tabs. Get tab IDs from the getTabs action. - * - * Example: - * { - * "tabId": 123 - * } - * // Returns: { tabId: 123, url: "https://google.com", title: "Google" } - */ -export class SwitchTabAction extends ActionHandler< - SwitchTabInput, - SwitchTabOutput -> { - readonly inputSchema = SwitchTabInputSchema - private tabAdapter = new TabAdapter() - - async execute(input: SwitchTabInput): Promise { - const tab = await this.tabAdapter.switchTab(input.tabId) - - if (tab.id === undefined) { - throw new Error('Switched tab has no ID') - } - return { - tabId: tab.id, - url: tab.url || '', - title: tab.title || '', - } - } -} diff --git a/packages/browseros-agent/apps/controller-ext/src/actions/tab/UngroupTabsAction.ts b/packages/browseros-agent/apps/controller-ext/src/actions/tab/UngroupTabsAction.ts deleted file mode 100644 index 4af7d82aa..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/actions/tab/UngroupTabsAction.ts +++ /dev/null @@ -1,48 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -import { z } from 'zod' -import { TabAdapter } from '@/adapters/TabAdapter' -import { ActionHandler } from '../ActionHandler' - -const UngroupTabsInputSchema = z - .object({ - tabIds: z - .array(z.number().int().positive()) - .min(1) - .describe('Array of tab IDs to remove from their groups'), - }) - .describe('Remove tabs from their groups') - -type UngroupTabsInput = z.infer - -export interface UngroupTabsOutput { - ungroupedCount: number -} - -/** - * UngroupTabsAction - Remove tabs from their groups - * - * Removes the specified tabs from any groups they belong to. - * The tabs remain open but are no longer part of any group. - * - * Example payload: - * { "tabIds": [123, 456, 789] } - */ -export class UngroupTabsAction extends ActionHandler< - UngroupTabsInput, - UngroupTabsOutput -> { - readonly inputSchema = UngroupTabsInputSchema - private tabAdapter = new TabAdapter() - - async execute(input: UngroupTabsInput): Promise { - await this.tabAdapter.ungroupTabs(input.tabIds) - - return { - ungroupedCount: input.tabIds.length, - } - } -} diff --git a/packages/browseros-agent/apps/controller-ext/src/actions/tab/UpdateTabGroupAction.ts b/packages/browseros-agent/apps/controller-ext/src/actions/tab/UpdateTabGroupAction.ts deleted file mode 100644 index 7ab8d8dbb..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/actions/tab/UpdateTabGroupAction.ts +++ /dev/null @@ -1,90 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -import { z } from 'zod' -import { TabAdapter } from '@/adapters/TabAdapter' -import { ActionHandler } from '../ActionHandler' - -const TabGroupColorSchema = z.enum([ - 'grey', - 'blue', - 'red', - 'yellow', - 'green', - 'pink', - 'purple', - 'cyan', - 'orange', -]) - -const UpdateTabGroupInputSchema = z - .object({ - groupId: z.number().int().describe('ID of the group to update'), - title: z.string().optional().describe('New title for the group'), - color: TabGroupColorSchema.optional().describe( - 'New color for the group: grey, blue, red, yellow, green, pink, purple, cyan, orange', - ), - collapsed: z - .boolean() - .optional() - .describe('Whether to collapse (hide) the group tabs'), - }) - .describe('Update tab group properties') - -type UpdateTabGroupInput = z.infer - -export interface UpdateTabGroupOutput { - groupId: number - title: string - color: string - collapsed: boolean -} - -/** - * UpdateTabGroupAction - Update a tab group's properties - * - * Updates the title, color, or collapsed state of an existing tab group. - * - * Example payloads: - * - * Rename a group: - * { "groupId": 1, "title": "Work Projects" } - * - * Change color: - * { "groupId": 1, "color": "blue" } - * - * Collapse a group: - * { "groupId": 1, "collapsed": true } - * - * Update multiple properties: - * { "groupId": 1, "title": "Research", "color": "purple", "collapsed": false } - */ -export class UpdateTabGroupAction extends ActionHandler< - UpdateTabGroupInput, - UpdateTabGroupOutput -> { - readonly inputSchema = UpdateTabGroupInputSchema - private tabAdapter = new TabAdapter() - - async execute(input: UpdateTabGroupInput): Promise { - const updateProps: chrome.tabGroups.UpdateProperties = {} - - if (input.title !== undefined) updateProps.title = input.title - if (input.color !== undefined) updateProps.color = input.color - if (input.collapsed !== undefined) updateProps.collapsed = input.collapsed - - const group = await this.tabAdapter.updateTabGroup( - input.groupId, - updateProps, - ) - - return { - groupId: group.id, - title: group.title || '', - color: group.color, - collapsed: group.collapsed, - } - } -} diff --git a/packages/browseros-agent/apps/controller-ext/src/adapters/BookmarkAdapter.ts b/packages/browseros-agent/apps/controller-ext/src/adapters/BookmarkAdapter.ts deleted file mode 100644 index 0d9315d45..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/adapters/BookmarkAdapter.ts +++ /dev/null @@ -1,377 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -import { logger } from '@/utils/logger' -import { CHROME_API_TIMEOUTS, withTimeout } from '@/utils/timeout' - -/** - * BookmarkAdapter - Wrapper for Chrome bookmarks API - * - * Responsibilities: - * - Provide clean Promise-based interface to Chrome bookmarks API - * - Handle Chrome API errors - * - Log operations for debugging - */ -export class BookmarkAdapter { - /** - * Get all bookmarks as a tree structure - * - * @returns Bookmark tree root nodes - */ - async getBookmarkTree(): Promise { - logger.debug('[BookmarkAdapter] Getting bookmark tree') - - try { - const tree = await withTimeout( - chrome.bookmarks.getTree(), - CHROME_API_TIMEOUTS.CHROME_API, - 'chrome.bookmarks.getTree', - ) - logger.debug( - `[BookmarkAdapter] Retrieved bookmark tree with ${tree.length} root nodes`, - ) - return tree - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error) - logger.error( - `[BookmarkAdapter] Failed to get bookmark tree: ${errorMessage}`, - ) - throw new Error(`Failed to get bookmark tree: ${errorMessage}`) - } - } - - /** - * Search bookmarks by query - * - * @param query - Search query (matches title and URL) - * @returns Array of matching bookmarks - */ - async searchBookmarks( - query: string, - ): Promise { - logger.debug(`[BookmarkAdapter] Searching bookmarks: "${query}"`) - - try { - const results = await withTimeout( - chrome.bookmarks.search(query), - CHROME_API_TIMEOUTS.CHROME_API, - 'chrome.bookmarks.search', - ) - logger.debug( - `[BookmarkAdapter] Found ${results.length} bookmarks matching "${query}"`, - ) - return results - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error) - logger.error( - `[BookmarkAdapter] Failed to search bookmarks: ${errorMessage}`, - ) - throw new Error(`Failed to search bookmarks: ${errorMessage}`) - } - } - - /** - * Get bookmark by ID - * - * @param id - Bookmark ID - * @returns Bookmark node - */ - async getBookmark(id: string): Promise { - logger.debug(`[BookmarkAdapter] Getting bookmark: ${id}`) - - try { - const results = await withTimeout( - chrome.bookmarks.get(id), - CHROME_API_TIMEOUTS.CHROME_API, - 'chrome.bookmarks.get', - ) - if (results.length === 0) { - throw new Error('Bookmark not found') - } - logger.debug(`[BookmarkAdapter] Retrieved bookmark: ${id}`) - return results[0] - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error) - logger.error(`[BookmarkAdapter] Failed to get bookmark: ${errorMessage}`) - throw new Error(`Failed to get bookmark: ${errorMessage}`) - } - } - - /** - * Create a new bookmark - * - * @param bookmark - Bookmark creation details - * @returns Created bookmark node - */ - async createBookmark(bookmark: { - title: string - url: string - parentId?: string - }): Promise { - logger.debug( - `[BookmarkAdapter] Creating bookmark: ${bookmark.title || 'Untitled'}`, - ) - - try { - const created = await withTimeout( - chrome.bookmarks.create(bookmark), - CHROME_API_TIMEOUTS.CHROME_API, - 'chrome.bookmarks.create', - ) - logger.debug( - `[BookmarkAdapter] Created bookmark: ${created.id} - ${created.title}`, - ) - return created - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error) - logger.error( - `[BookmarkAdapter] Failed to create bookmark: ${errorMessage}`, - ) - throw new Error(`Failed to create bookmark: ${errorMessage}`) - } - } - - /** - * Remove a bookmark by ID - * - * @param id - Bookmark ID to remove - */ - async removeBookmark(id: string): Promise { - logger.debug(`[BookmarkAdapter] Removing bookmark: ${id}`) - - try { - await withTimeout( - chrome.bookmarks.remove(id), - CHROME_API_TIMEOUTS.CHROME_API, - 'chrome.bookmarks.remove', - ) - logger.debug(`[BookmarkAdapter] Removed bookmark: ${id}`) - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error) - logger.error( - `[BookmarkAdapter] Failed to remove bookmark ${id}: ${errorMessage}`, - ) - throw new Error(`Failed to remove bookmark: ${errorMessage}`) - } - } - - /** - * Update a bookmark - * - * @param id - Bookmark ID to update - * @param changes - Changes to apply - * @returns Updated bookmark node - */ - async updateBookmark( - id: string, - changes: { title?: string; url?: string }, - ): Promise { - logger.debug(`[BookmarkAdapter] Updating bookmark: ${id}`) - - try { - const updated = await withTimeout( - chrome.bookmarks.update(id, changes), - CHROME_API_TIMEOUTS.CHROME_API, - 'chrome.bookmarks.update', - ) - logger.debug( - `[BookmarkAdapter] Updated bookmark: ${id} - ${updated.title}`, - ) - return updated - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error) - logger.error( - `[BookmarkAdapter] Failed to update bookmark ${id}: ${errorMessage}`, - ) - throw new Error(`Failed to update bookmark: ${errorMessage}`) - } - } - - /** - * Get recent bookmarks - * - * @param limit - Maximum number of bookmarks to return - * @returns Array of recent bookmarks - */ - async getRecentBookmarks( - limit = 20, - ): Promise { - logger.debug(`[BookmarkAdapter] Getting ${limit} recent bookmarks`) - - try { - const tree = await withTimeout( - chrome.bookmarks.getTree(), - CHROME_API_TIMEOUTS.CHROME_API, - 'chrome.bookmarks.getTree', - ) - const bookmarks = this._flattenBookmarkTree(tree) - - // Filter to only URL bookmarks (not folders) and sort by dateAdded - const urlBookmarks = bookmarks - .filter((b) => b.url && b.dateAdded) - .sort((a, b) => (b.dateAdded || 0) - (a.dateAdded || 0)) - .slice(0, limit) - - logger.debug( - `[BookmarkAdapter] Found ${urlBookmarks.length} recent bookmarks`, - ) - return urlBookmarks - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error) - logger.error( - `[BookmarkAdapter] Failed to get recent bookmarks: ${errorMessage}`, - ) - throw new Error(`Failed to get recent bookmarks: ${errorMessage}`) - } - } - - /** - * Create a bookmark folder - * - * @param title - Folder name - * @param parentId - Parent folder ID (defaults to "1" = Bookmarks Bar) - * @returns Created folder node - */ - async createBookmarkFolder(options: { - title: string - parentId?: string - }): Promise { - const { title, parentId = '1' } = options - logger.debug( - `[BookmarkAdapter] Creating bookmark folder: "${title}" in parent ${parentId}`, - ) - - try { - const created = await chrome.bookmarks.create({ - title, - parentId, - }) - logger.debug( - `[BookmarkAdapter] Created folder: ${created.id} - ${created.title}`, - ) - return created - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error) - logger.error( - `[BookmarkAdapter] Failed to create bookmark folder: ${errorMessage}`, - ) - throw new Error(`Failed to create bookmark folder: ${errorMessage}`) - } - } - - /** - * Get direct children of a folder - * - * @param folderId - Folder ID to get children from - * @returns Array of child nodes - */ - async getBookmarkChildren( - folderId: string, - ): Promise { - logger.debug(`[BookmarkAdapter] Getting children of folder: ${folderId}`) - - try { - const children = await chrome.bookmarks.getChildren(folderId) - logger.debug( - `[BookmarkAdapter] Found ${children.length} children in folder ${folderId}`, - ) - return children - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error) - logger.error( - `[BookmarkAdapter] Failed to get bookmark children: ${errorMessage}`, - ) - throw new Error(`Failed to get bookmark children: ${errorMessage}`) - } - } - - /** - * Move a bookmark or folder to a new location - * - * @param id - Bookmark or folder ID to move - * @param destination - New location - * @returns Updated bookmark node - */ - async moveBookmark( - id: string, - destination: { parentId?: string; index?: number }, - ): Promise { - logger.debug( - `[BookmarkAdapter] Moving bookmark ${id} to parent ${destination.parentId}, index ${destination.index}`, - ) - - try { - const moved = await chrome.bookmarks.move(id, destination) - logger.debug( - `[BookmarkAdapter] Moved bookmark ${id} to ${moved.parentId}`, - ) - return moved - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error) - logger.error( - `[BookmarkAdapter] Failed to move bookmark ${id}: ${errorMessage}`, - ) - throw new Error(`Failed to move bookmark: ${errorMessage}`) - } - } - - /** - * Remove a folder and all its contents recursively - * - * @param id - Folder ID to remove - * @throws if id is a root node ("0", "1", "2") - */ - async removeBookmarkTree(id: string): Promise { - const protectedIds = ['0', '1', '2'] - if (protectedIds.includes(id)) { - throw new Error( - `Cannot delete protected bookmark folder: ${id}. Root folders (Bookmarks Bar, Other Bookmarks, Mobile Bookmarks) cannot be deleted.`, - ) - } - - logger.debug(`[BookmarkAdapter] Removing bookmark tree: ${id}`) - - try { - await chrome.bookmarks.removeTree(id) - logger.debug(`[BookmarkAdapter] Removed bookmark tree: ${id}`) - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error) - logger.error( - `[BookmarkAdapter] Failed to remove bookmark tree ${id}: ${errorMessage}`, - ) - throw new Error(`Failed to remove bookmark tree: ${errorMessage}`) - } - } - - /** - * Flatten bookmark tree into array - * @private - */ - private _flattenBookmarkTree( - nodes: chrome.bookmarks.BookmarkTreeNode[], - ): chrome.bookmarks.BookmarkTreeNode[] { - const result: chrome.bookmarks.BookmarkTreeNode[] = [] - - for (const node of nodes) { - result.push(node) - if (node.children) { - result.push(...this._flattenBookmarkTree(node.children)) - } - } - - return result - } -} diff --git a/packages/browseros-agent/apps/controller-ext/src/adapters/BrowserOSAdapter.ts b/packages/browseros-agent/apps/controller-ext/src/adapters/BrowserOSAdapter.ts deleted file mode 100644 index 7856f3ead..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/adapters/BrowserOSAdapter.ts +++ /dev/null @@ -1,907 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -/// - -import { logger } from '@/utils/logger' -import { CHROME_API_TIMEOUTS, withTimeout } from '@/utils/timeout' - -// ============= Re-export types from chrome.browserOS namespace ============= - -export type InteractiveNode = chrome.browserOS.InteractiveNode -export type InteractiveSnapshot = chrome.browserOS.InteractiveSnapshot -export type InteractiveSnapshotOptions = - chrome.browserOS.InteractiveSnapshotOptions -export type PageLoadStatus = chrome.browserOS.PageLoadStatus -export type InteractiveNodeType = chrome.browserOS.InteractiveNodeType -export type Rect = chrome.browserOS.BoundingRect - -// New snapshot types -export type SnapshotType = chrome.browserOS.SnapshotType -export type SnapshotContext = chrome.browserOS.SnapshotContext -export type SectionType = chrome.browserOS.SectionType -export type TextSnapshotResult = chrome.browserOS.TextSnapshotResult -export type LinkInfo = chrome.browserOS.LinkInfo -export type LinksSnapshotResult = chrome.browserOS.LinksSnapshotResult -export type SnapshotSection = chrome.browserOS.SnapshotSection -export type Snapshot = chrome.browserOS.Snapshot -export type SnapshotOptions = chrome.browserOS.SnapshotOptions - -export type PrefObject = chrome.browserOS.PrefObject - -// ============= BrowserOS Adapter ============= - -// Screenshot size constants -export const SCREENSHOT_SIZES = { - small: 512, // Low token usage - medium: 768, // Balanced (default) - large: 1028, // High detail (note: 1028 not 1024) -} as const - -export type ScreenshotSizeKey = keyof typeof SCREENSHOT_SIZES - -/** - * Adapter for Chrome BrowserOS Extension APIs - * Provides a clean interface to browserOS functionality with extensibility - */ -export class BrowserOSAdapter { - private static instance: BrowserOSAdapter | null = null - - private constructor() {} - - /** - * Get singleton instance - */ - static getInstance(): BrowserOSAdapter { - if (!BrowserOSAdapter.instance) { - BrowserOSAdapter.instance = new BrowserOSAdapter() - } - return BrowserOSAdapter.instance - } - - /** - * Get interactive snapshot of the current page - */ - async getInteractiveSnapshot( - tabId: number, - options?: InteractiveSnapshotOptions, - ): Promise { - try { - logger.debug( - `[BrowserOSAdapter] Getting interactive snapshot for tab ${tabId} with options: ${JSON.stringify(options)}`, - ) - - const promise = new Promise((resolve, reject) => { - if (options) { - chrome.browserOS.getInteractiveSnapshot( - tabId, - options, - (snapshot: InteractiveSnapshot) => { - if (chrome.runtime.lastError) { - reject(new Error(chrome.runtime.lastError.message)) - } else { - logger.debug( - `[BrowserOSAdapter] Retrieved snapshot with ${snapshot.elements.length} elements`, - ) - resolve(snapshot) - } - }, - ) - } else { - chrome.browserOS.getInteractiveSnapshot( - tabId, - (snapshot: InteractiveSnapshot) => { - if (chrome.runtime.lastError) { - reject(new Error(chrome.runtime.lastError.message)) - } else { - logger.debug( - `[BrowserOSAdapter] Retrieved snapshot with ${snapshot.elements.length} elements`, - ) - resolve(snapshot) - } - }, - ) - } - }) - - return withTimeout( - promise, - CHROME_API_TIMEOUTS.BROWSEROS_HEAVY, - 'getInteractiveSnapshot', - ) - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error) - logger.error( - `[BrowserOSAdapter] Failed to get interactive snapshot: ${errorMessage}`, - ) - throw new Error(`Failed to get interactive snapshot: ${errorMessage}`) - } - } - - /** - * Click an element by node ID - */ - async click(tabId: number, nodeId: number): Promise { - try { - logger.debug(`[BrowserOSAdapter] Clicking node ${nodeId} in tab ${tabId}`) - - const promise = new Promise((resolve, reject) => { - chrome.browserOS.click(tabId, nodeId, () => { - if (chrome.runtime.lastError) { - reject(new Error(chrome.runtime.lastError.message)) - } else { - resolve() - } - }) - }) - - return withTimeout(promise, CHROME_API_TIMEOUTS.BROWSEROS_ACTION, 'click') - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error) - logger.error(`[BrowserOSAdapter] Failed to click node: ${errorMessage}`) - throw new Error(`Failed to click node ${nodeId}: ${errorMessage}`) - } - } - - /** - * Input text into an element - */ - async inputText(tabId: number, nodeId: number, text: string): Promise { - try { - logger.debug( - `[BrowserOSAdapter] Inputting text into node ${nodeId} in tab ${tabId}`, - ) - - const promise = new Promise((resolve, reject) => { - chrome.browserOS.inputText(tabId, nodeId, text, () => { - if (chrome.runtime.lastError) { - reject(new Error(chrome.runtime.lastError.message)) - } else { - resolve() - } - }) - }) - - return withTimeout( - promise, - CHROME_API_TIMEOUTS.BROWSEROS_ACTION, - 'inputText', - ) - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error) - logger.error(`[BrowserOSAdapter] Failed to input text: ${errorMessage}`) - throw new Error( - `Failed to input text into node ${nodeId}: ${errorMessage}`, - ) - } - } - - /** - * Clear text from an element - */ - async clear(tabId: number, nodeId: number): Promise { - try { - logger.debug(`[BrowserOSAdapter] Clearing node ${nodeId} in tab ${tabId}`) - - const promise = new Promise((resolve, reject) => { - chrome.browserOS.clear(tabId, nodeId, () => { - if (chrome.runtime.lastError) { - reject(new Error(chrome.runtime.lastError.message)) - } else { - resolve() - } - }) - }) - - return withTimeout(promise, CHROME_API_TIMEOUTS.BROWSEROS_ACTION, 'clear') - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error) - logger.error(`[BrowserOSAdapter] Failed to clear node: ${errorMessage}`) - throw new Error(`Failed to clear node ${nodeId}: ${errorMessage}`) - } - } - - /** - * Scroll to a specific node - */ - async scrollToNode(tabId: number, nodeId: number): Promise { - try { - logger.debug( - `[BrowserOSAdapter] Scrolling to node ${nodeId} in tab ${tabId}`, - ) - - const promise = new Promise((resolve, reject) => { - chrome.browserOS.scrollToNode(tabId, nodeId, (scrolled: boolean) => { - if (chrome.runtime.lastError) { - reject(new Error(chrome.runtime.lastError.message)) - } else { - resolve(scrolled) - } - }) - }) - - return withTimeout( - promise, - CHROME_API_TIMEOUTS.BROWSEROS_ACTION, - 'scrollToNode', - ) - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error) - logger.error( - `[BrowserOSAdapter] Failed to scroll to node: ${errorMessage}`, - ) - throw new Error(`Failed to scroll to node ${nodeId}: ${errorMessage}`) - } - } - - /** - * Send keyboard keys - */ - async sendKeys(tabId: number, keys: chrome.browserOS.Key): Promise { - try { - logger.debug(`[BrowserOSAdapter] Sending keys "${keys}" to tab ${tabId}`) - - const promise = new Promise((resolve, reject) => { - chrome.browserOS.sendKeys(tabId, keys, () => { - if (chrome.runtime.lastError) { - reject(new Error(chrome.runtime.lastError.message)) - } else { - resolve() - } - }) - }) - - return withTimeout( - promise, - CHROME_API_TIMEOUTS.BROWSEROS_ACTION, - 'sendKeys', - ) - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error) - logger.error(`[BrowserOSAdapter] Failed to send keys: ${errorMessage}`) - throw new Error(`Failed to send keys: ${errorMessage}`) - } - } - - /** - * Get page load status - */ - async getPageLoadStatus(tabId: number): Promise { - try { - logger.debug( - `[BrowserOSAdapter] Getting page load status for tab ${tabId}`, - ) - - const promise = new Promise((resolve, reject) => { - chrome.browserOS.getPageLoadStatus(tabId, (status: PageLoadStatus) => { - if (chrome.runtime.lastError) { - reject(new Error(chrome.runtime.lastError.message)) - } else { - resolve(status) - } - }) - }) - - return withTimeout( - promise, - CHROME_API_TIMEOUTS.BROWSEROS_HEAVY, - 'getPageLoadStatus', - ) - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error) - logger.error( - `[BrowserOSAdapter] Failed to get page load status: ${errorMessage}`, - ) - throw new Error(`Failed to get page load status: ${errorMessage}`) - } - } - - /** - * Get accessibility tree (if available) - */ - async getAccessibilityTree( - tabId: number, - ): Promise { - try { - logger.debug( - `[BrowserOSAdapter] Getting accessibility tree for tab ${tabId}`, - ) - - const promise = new Promise( - (resolve, reject) => { - chrome.browserOS.getAccessibilityTree( - tabId, - (tree: chrome.browserOS.AccessibilityTree) => { - if (chrome.runtime.lastError) { - reject(new Error(chrome.runtime.lastError.message)) - } else { - resolve(tree) - } - }, - ) - }, - ) - - return withTimeout( - promise, - CHROME_API_TIMEOUTS.BROWSEROS_HEAVY, - 'getAccessibilityTree', - ) - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error) - logger.error( - `[BrowserOSAdapter] Failed to get accessibility tree: ${errorMessage}`, - ) - throw new Error(`Failed to get accessibility tree: ${errorMessage}`) - } - } - - /** - * Capture a screenshot of the tab - * @param tabId - The tab ID to capture - * @param size - Optional screenshot size ('small', 'medium', or 'large') - * @param showHighlights - Optional flag to show element highlights - * @param width - Optional exact width for screenshot - * @param height - Optional exact height for screenshot - */ - async captureScreenshot( - tabId: number, - size?: ScreenshotSizeKey, - showHighlights?: boolean, - width?: number, - height?: number, - ): Promise { - try { - const sizeDesc = size ? ` (${size})` : '' - const highlightDesc = showHighlights ? ' with highlights' : '' - const dimensionsDesc = width && height ? ` (${width}x${height})` : '' - logger.debug( - `[BrowserOSAdapter] Capturing screenshot for tab ${tabId}${sizeDesc}${highlightDesc}${dimensionsDesc}`, - ) - - const promise = new Promise((resolve, reject) => { - // Use exact dimensions if provided - if (width !== undefined && height !== undefined) { - chrome.browserOS.captureScreenshot( - tabId, - 0, // thumbnailSize ignored when width/height specified - showHighlights || false, - width, - height, - (dataUrl: string) => { - if (chrome.runtime.lastError) { - reject(new Error(chrome.runtime.lastError.message)) - } else { - logger.debug( - `[BrowserOSAdapter] Screenshot captured for tab ${tabId} (${width}x${height})${highlightDesc}`, - ) - resolve(dataUrl) - } - }, - ) - } else if (size !== undefined || showHighlights !== undefined) { - const pixelSize = size ? SCREENSHOT_SIZES[size] : 0 - // Use the API with thumbnail size and highlights - if (showHighlights !== undefined) { - chrome.browserOS.captureScreenshot( - tabId, - pixelSize, - showHighlights, - (dataUrl: string) => { - if (chrome.runtime.lastError) { - reject(new Error(chrome.runtime.lastError.message)) - } else { - logger.debug( - `[BrowserOSAdapter] Screenshot captured for tab ${tabId}${sizeDesc}${highlightDesc}`, - ) - resolve(dataUrl) - } - }, - ) - } else { - chrome.browserOS.captureScreenshot( - tabId, - pixelSize, - (dataUrl: string) => { - if (chrome.runtime.lastError) { - reject(new Error(chrome.runtime.lastError.message)) - } else { - logger.debug( - `[BrowserOSAdapter] Screenshot captured for tab ${tabId} (${size}: ${pixelSize}px)`, - ) - resolve(dataUrl) - } - }, - ) - } - } else { - // Use the original API without size (backwards compatibility) - chrome.browserOS.captureScreenshot(tabId, (dataUrl: string) => { - if (chrome.runtime.lastError) { - reject(new Error(chrome.runtime.lastError.message)) - } else { - logger.debug( - `[BrowserOSAdapter] Screenshot captured for tab ${tabId}`, - ) - resolve(dataUrl) - } - }) - } - }) - - return withTimeout( - promise, - CHROME_API_TIMEOUTS.BROWSEROS_HEAVY, - 'captureScreenshot', - ) - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error) - logger.error( - `[BrowserOSAdapter] Failed to capture screenshot: ${errorMessage}`, - ) - throw new Error(`Failed to capture screenshot: ${errorMessage}`) - } - } - - /** - * Get a content snapshot from the page - */ - async getSnapshot(tabId: number, _type: SnapshotType): Promise { - try { - logger.debug(`[BrowserOSAdapter] Getting snapshot for tab ${tabId}`) - - const promise = new Promise((resolve, reject) => { - chrome.browserOS.getSnapshot(tabId, (snapshot: Snapshot) => { - if (chrome.runtime.lastError) { - reject(new Error(chrome.runtime.lastError.message)) - } else { - logger.debug( - `[BrowserOSAdapter] Retrieved snapshot: ${JSON.stringify(snapshot)}`, - ) - resolve(snapshot) - } - }) - }) - - return withTimeout( - promise, - CHROME_API_TIMEOUTS.BROWSEROS_HEAVY, - 'getSnapshot', - ) - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error) - logger.error(`[BrowserOSAdapter] Failed to get snapshot: ${errorMessage}`) - throw new Error(`Failed to get snapshot: ${errorMessage}`) - } - } - - /** - * Get text content snapshot from the page - * Convenience method (deprecated - use getSnapshot directly) - * Use getSnapshot(tabId, 'text') instead - */ - async getTextSnapshot(tabId: number): Promise { - return this.getSnapshot(tabId, 'text') - } - - /** - * Get links snapshot from the page - * Convenience method (deprecated - use getSnapshot directly) - * Use getSnapshot(tabId, 'links') instead - */ - async getLinksSnapshot(tabId: number): Promise { - return this.getSnapshot(tabId, 'links') - } - - /** - * Generic method to invoke any BrowserOS API - * Useful for future APIs or experimental features - */ - async invokeAPI(method: string, ...args: unknown[]): Promise { - try { - logger.debug(`[BrowserOSAdapter] Invoking BrowserOS API: ${method}`) - - if (!(method in chrome.browserOS)) { - throw new Error(`Unknown BrowserOS API method: ${method}`) - } - - // @ts-expect-error - Dynamic API invocation - const result = await chrome.browserOS[method](...args) - return result - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error) - logger.error( - `[BrowserOSAdapter] Failed to invoke API ${method}: ${errorMessage}`, - ) - throw new Error( - `Failed to invoke BrowserOS API ${method}: ${errorMessage}`, - ) - } - } - - /** - * Check if a specific API is available - */ - isAPIAvailable(method: string): boolean { - return method in chrome.browserOS - } - - /** - * Get list of available BrowserOS APIs - */ - getAvailableAPIs(): string[] { - return Object.keys(chrome.browserOS).filter((key) => { - // @ts-expect-error - Dynamic key access for API discovery - return typeof chrome.browserOS[key] === 'function' - }) - } - - /** - * Get BrowserOS version information - */ - async getVersion(): Promise { - try { - logger.debug('[BrowserOSAdapter] Getting BrowserOS version') - - return new Promise((resolve, reject) => { - // Check if getVersionNumber API is available - if ( - 'getVersionNumber' in chrome.browserOS && - typeof chrome.browserOS.getVersionNumber === 'function' - ) { - chrome.browserOS.getVersionNumber((version: string) => { - if (chrome.runtime.lastError) { - reject(new Error(chrome.runtime.lastError.message)) - } else { - logger.debug(`[BrowserOSAdapter] BrowserOS version: ${version}`) - resolve(version) - } - }) - } else { - // Fallback - return null if API not available - resolve(null) - } - }) - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error) - logger.error(`[BrowserOSAdapter] Failed to get version: ${errorMessage}`) - // Return null on error - return null - } - } - - /** - * Log a metric event with optional properties - */ - async logMetric( - eventName: string, - properties?: Record, - ): Promise { - try { - logger.debug( - `[BrowserOSAdapter] Logging metric: ${eventName} with properties: ${JSON.stringify(properties)}`, - ) - - return new Promise((resolve, reject) => { - // Check if logMetric API is available - if ( - 'logMetric' in chrome.browserOS && - typeof chrome.browserOS.logMetric === 'function' - ) { - if (properties) { - chrome.browserOS.logMetric(eventName, properties, () => { - if (chrome.runtime.lastError) { - reject(new Error(chrome.runtime.lastError.message)) - } else { - logger.debug(`[BrowserOSAdapter] Metric logged: ${eventName}`) - resolve() - } - }) - } else { - chrome.browserOS.logMetric(eventName, () => { - if (chrome.runtime.lastError) { - reject(new Error(chrome.runtime.lastError.message)) - } else { - logger.debug(`[BrowserOSAdapter] Metric logged: ${eventName}`) - resolve() - } - }) - } - } else { - // If API not available, log a warning but don't fail - logger.warn( - `[BrowserOSAdapter] logMetric API not available, skipping metric: ${eventName}`, - ) - resolve() - } - }) - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error) - logger.error(`[BrowserOSAdapter] Failed to log metric: ${errorMessage}`) - return - } - } - - /** - * Execute JavaScript code in the specified tab - * @param tabId - The tab ID to execute code in - * @param code - The JavaScript code to execute - * @returns The result of the execution - */ - async executeJavaScript(tabId: number, code: string): Promise { - try { - logger.debug(`[BrowserOSAdapter] Executing JavaScript in tab ${tabId}`) - - const promise = new Promise((resolve, reject) => { - // Check if executeJavaScript API is available - if ( - 'executeJavaScript' in chrome.browserOS && - typeof chrome.browserOS.executeJavaScript === 'function' - ) { - chrome.browserOS.executeJavaScript(tabId, code, (result: unknown) => { - if (chrome.runtime.lastError) { - reject(new Error(chrome.runtime.lastError.message)) - } else { - logger.debug( - `[BrowserOSAdapter] JavaScript executed successfully in tab ${tabId}`, - ) - resolve(result) - } - }) - } else { - reject(new Error('executeJavaScript API not available')) - } - }) - - return withTimeout( - promise, - CHROME_API_TIMEOUTS.BROWSEROS_HEAVY, - 'executeJavaScript', - ) - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error) - logger.error( - `[BrowserOSAdapter] Failed to execute JavaScript: ${errorMessage}`, - ) - throw new Error(`Failed to execute JavaScript: ${errorMessage}`) - } - } - - /** - * Click at specific viewport coordinates - * @param tabId - The tab ID to click in - * @param x - X coordinate in viewport pixels - * @param y - Y coordinate in viewport pixels - */ - async clickCoordinates(tabId: number, x: number, y: number): Promise { - try { - logger.debug( - `[BrowserOSAdapter] Clicking at coordinates (${x}, ${y}) in tab ${tabId}`, - ) - - const promise = new Promise((resolve, reject) => { - // Check if clickCoordinates API is available - if ( - 'clickCoordinates' in chrome.browserOS && - typeof chrome.browserOS.clickCoordinates === 'function' - ) { - chrome.browserOS.clickCoordinates(tabId, x, y, () => { - if (chrome.runtime.lastError) { - reject(new Error(chrome.runtime.lastError.message)) - } else { - logger.debug( - `[BrowserOSAdapter] Successfully clicked at (${x}, ${y}) in tab ${tabId}`, - ) - resolve() - } - }) - } else { - reject(new Error('clickCoordinates API not available')) - } - }) - - return withTimeout( - promise, - CHROME_API_TIMEOUTS.BROWSEROS_ACTION, - 'clickCoordinates', - ) - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error) - logger.error( - `[BrowserOSAdapter] Failed to click at coordinates: ${errorMessage}`, - ) - throw new Error( - `Failed to click at coordinates (${x}, ${y}): ${errorMessage}`, - ) - } - } - - /** - * Type text at specific viewport coordinates - * @param tabId - The tab ID to type in - * @param x - X coordinate in viewport pixels - * @param y - Y coordinate in viewport pixels - * @param text - Text to type at the location - */ - async typeAtCoordinates( - tabId: number, - x: number, - y: number, - text: string, - ): Promise { - try { - logger.debug( - `[BrowserOSAdapter] Typing at coordinates (${x}, ${y}) in tab ${tabId}`, - ) - - const promise = new Promise((resolve, reject) => { - // Check if typeAtCoordinates API is available - if ( - 'typeAtCoordinates' in chrome.browserOS && - typeof chrome.browserOS.typeAtCoordinates === 'function' - ) { - chrome.browserOS.typeAtCoordinates(tabId, x, y, text, () => { - if (chrome.runtime.lastError) { - reject(new Error(chrome.runtime.lastError.message)) - } else { - logger.debug( - `[BrowserOSAdapter] Successfully typed "${text}" at (${x}, ${y}) in tab ${tabId}`, - ) - resolve() - } - }) - } else { - reject(new Error('typeAtCoordinates API not available')) - } - }) - - return withTimeout( - promise, - CHROME_API_TIMEOUTS.BROWSEROS_ACTION, - 'typeAtCoordinates', - ) - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error) - logger.error( - `[BrowserOSAdapter] Failed to type at coordinates: ${errorMessage}`, - ) - throw new Error( - `Failed to type at coordinates (${x}, ${y}): ${errorMessage}`, - ) - } - } - - /** - * Get a specific preference value - * @param name - The preference name (e.g., "browseros.server.mcp_port") - * @returns Promise resolving to the preference object containing key, type, and value - */ - async getPref(name: string): Promise { - try { - console.log(`[BrowserOSAdapter] Getting preference: ${name}`) - - return new Promise((resolve, reject) => { - chrome.browserOS.getPref(name, (pref: PrefObject) => { - if (chrome.runtime.lastError) { - reject(new Error(chrome.runtime.lastError.message)) - } else { - console.log( - `[BrowserOSAdapter] Retrieved preference ${name}: ${JSON.stringify(pref)}`, - ) - resolve(pref) - } - }) - }) - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error) - console.error( - `[BrowserOSAdapter] Failed to get preference: ${errorMessage}`, - ) - throw new Error(`Failed to get preference ${name}: ${errorMessage}`) - } - } - - /** - * Set a specific preference value - * @param name - The preference name (e.g., "browseros.server.mcp_enabled") - * @param value - The value to set - * @param pageId - Optional page ID for settings tracking - * @returns Promise resolving to true if successful - */ - async setPref( - name: string, - value: unknown, - pageId?: string, - ): Promise { - try { - console.log( - `[BrowserOSAdapter] Setting preference ${name} to ${JSON.stringify(value)}`, - ) - - return new Promise((resolve, reject) => { - if (pageId !== undefined) { - chrome.browserOS.setPref(name, value, pageId, (success: boolean) => { - if (chrome.runtime.lastError) { - reject(new Error(chrome.runtime.lastError.message)) - } else { - console.log( - `[BrowserOSAdapter] Successfully set preference ${name}`, - ) - resolve(success) - } - }) - } else { - chrome.browserOS.setPref(name, value, (success: boolean) => { - if (chrome.runtime.lastError) { - reject(new Error(chrome.runtime.lastError.message)) - } else { - console.log( - `[BrowserOSAdapter] Successfully set preference ${name}`, - ) - resolve(success) - } - }) - } - }) - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error) - console.error( - `[BrowserOSAdapter] Failed to set preference: ${errorMessage}`, - ) - throw new Error(`Failed to set preference ${name}: ${errorMessage}`) - } - } - - /** - * Get all preferences (filtered to browseros.* prefs) - * @returns Promise resolving to array of preference objects - */ - async getAllPrefs(): Promise { - try { - console.log('[BrowserOSAdapter] Getting all preferences') - - return new Promise((resolve, reject) => { - chrome.browserOS.getAllPrefs((prefs: PrefObject[]) => { - if (chrome.runtime.lastError) { - reject(new Error(chrome.runtime.lastError.message)) - } else { - console.log( - `[BrowserOSAdapter] Retrieved ${prefs.length} preferences`, - ) - resolve(prefs) - } - }) - }) - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error) - console.error( - `[BrowserOSAdapter] Failed to get all preferences: ${errorMessage}`, - ) - throw new Error(`Failed to get all preferences: ${errorMessage}`) - } - } -} - -// Export singleton instance getter for convenience -export const getBrowserOSAdapter = () => BrowserOSAdapter.getInstance() diff --git a/packages/browseros-agent/apps/controller-ext/src/adapters/HistoryAdapter.ts b/packages/browseros-agent/apps/controller-ext/src/adapters/HistoryAdapter.ts deleted file mode 100644 index 9caadaed2..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/adapters/HistoryAdapter.ts +++ /dev/null @@ -1,261 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -import { logger } from '@/utils/logger' -import { CHROME_API_TIMEOUTS, withTimeout } from '@/utils/timeout' - -/** - * HistoryAdapter - Wrapper for Chrome history API - * - * Responsibilities: - * - Provide clean Promise-based interface to Chrome history API - * - Handle Chrome API errors - * - Log operations for debugging - */ -export class HistoryAdapter { - /** - * Search browser history - * - * @param query - Search query (matches URL and title) - * @param maxResults - Maximum number of results (default: 100) - * @param startTime - Start time in milliseconds since epoch (optional) - * @param endTime - End time in milliseconds since epoch (optional) - * @returns Array of history items - */ - async searchHistory( - query: string, - maxResults = 100, - startTime?: number, - endTime?: number, - ): Promise { - logger.debug( - `[HistoryAdapter] Searching history: "${query}" (max: ${maxResults})`, - ) - - try { - const results = await withTimeout( - chrome.history.search({ - text: query, - maxResults, - startTime, - endTime, - }), - CHROME_API_TIMEOUTS.CHROME_API, - 'chrome.history.search', - ) - - logger.debug(`[HistoryAdapter] Found ${results.length} history items`) - return results - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error) - logger.error(`[HistoryAdapter] Failed to search history: ${errorMessage}`) - throw new Error(`Failed to search history: ${errorMessage}`) - } - } - - /** - * Get recent browser history - * - * @param maxResults - Maximum number of results (default: 20) - * @param hoursBack - How many hours back to search (default: 24) - * @returns Array of recent history items - */ - async getRecentHistory( - maxResults = 20, - hoursBack = 24, - ): Promise { - logger.debug( - `[HistoryAdapter] Getting ${maxResults} recent history items (last ${hoursBack}h)`, - ) - - try { - const startTime = Date.now() - hoursBack * 60 * 60 * 1000 - - const results = await withTimeout( - chrome.history.search({ - text: '', - maxResults, - startTime, - }), - CHROME_API_TIMEOUTS.CHROME_API, - 'chrome.history.search', - ) - - logger.debug(`[HistoryAdapter] Retrieved ${results.length} recent items`) - return results - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error) - logger.error( - `[HistoryAdapter] Failed to get recent history: ${errorMessage}`, - ) - throw new Error(`Failed to get recent history: ${errorMessage}`) - } - } - - /** - * Get visit details for a specific URL - * - * @param url - URL to get visits for - * @returns Array of visit items - */ - async getVisits(url: string): Promise { - logger.debug(`[HistoryAdapter] Getting visits for: ${url}`) - - try { - const visits = await withTimeout( - chrome.history.getVisits({ url }), - CHROME_API_TIMEOUTS.CHROME_API, - 'chrome.history.getVisits', - ) - logger.debug(`[HistoryAdapter] Found ${visits.length} visits for ${url}`) - return visits - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error) - logger.error(`[HistoryAdapter] Failed to get visits: ${errorMessage}`) - throw new Error(`Failed to get visits: ${errorMessage}`) - } - } - - /** - * Add a URL to browser history - * - * @param url - URL to add - */ - async addUrl(url: string): Promise { - logger.debug(`[HistoryAdapter] Adding URL to history: ${url}`) - - try { - await withTimeout( - chrome.history.addUrl({ url }), - CHROME_API_TIMEOUTS.CHROME_API, - 'chrome.history.addUrl', - ) - logger.debug(`[HistoryAdapter] Added URL: ${url}`) - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error) - logger.error(`[HistoryAdapter] Failed to add URL: ${errorMessage}`) - throw new Error(`Failed to add URL to history: ${errorMessage}`) - } - } - - /** - * Remove a specific URL from history - * - * @param url - URL to remove - */ - async deleteUrl(url: string): Promise { - logger.debug(`[HistoryAdapter] Removing URL from history: ${url}`) - - try { - await withTimeout( - chrome.history.deleteUrl({ url }), - CHROME_API_TIMEOUTS.CHROME_API, - 'chrome.history.deleteUrl', - ) - logger.debug(`[HistoryAdapter] Removed URL: ${url}`) - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error) - logger.error(`[HistoryAdapter] Failed to delete URL: ${errorMessage}`) - throw new Error(`Failed to delete URL from history: ${errorMessage}`) - } - } - - /** - * Delete history within a time range - * - * @param startTime - Start time in milliseconds since epoch - * @param endTime - End time in milliseconds since epoch - */ - async deleteRange(startTime: number, endTime: number): Promise { - logger.debug( - `[HistoryAdapter] Deleting history range: ${new Date(startTime).toISOString()} to ${new Date(endTime).toISOString()}`, - ) - - try { - await withTimeout( - chrome.history.deleteRange({ startTime, endTime }), - CHROME_API_TIMEOUTS.CHROME_API, - 'chrome.history.deleteRange', - ) - logger.debug('[HistoryAdapter] Deleted history range') - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error) - logger.error( - `[HistoryAdapter] Failed to delete history range: ${errorMessage}`, - ) - throw new Error(`Failed to delete history range: ${errorMessage}`) - } - } - - /** - * Delete all browser history - * - * WARNING: This deletes ALL history permanently! - */ - async deleteAll(): Promise { - logger.warn('[HistoryAdapter] Deleting ALL browser history') - - try { - await withTimeout( - chrome.history.deleteAll(), - CHROME_API_TIMEOUTS.CHROME_API, - 'chrome.history.deleteAll', - ) - logger.warn('[HistoryAdapter] Deleted all history') - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error) - logger.error( - `[HistoryAdapter] Failed to delete all history: ${errorMessage}`, - ) - throw new Error(`Failed to delete all history: ${errorMessage}`) - } - } - - /** - * Get most visited URLs - * - * @param maxResults - Maximum number of results (default: 10) - * @returns Array of most visited history items - */ - async getMostVisited(maxResults = 10): Promise { - logger.debug(`[HistoryAdapter] Getting ${maxResults} most visited URLs`) - - try { - // Get all recent history - const allHistory = await withTimeout( - chrome.history.search({ - text: '', - maxResults: 1000, // Get a large sample - startTime: 0, - }), - CHROME_API_TIMEOUTS.CHROME_API, - 'chrome.history.search', - ) - - // Sort by visit count - const sorted = allHistory - .filter((item) => item.visitCount && item.visitCount > 1) - .sort((a, b) => (b.visitCount || 0) - (a.visitCount || 0)) - .slice(0, maxResults) - - logger.debug(`[HistoryAdapter] Found ${sorted.length} most visited URLs`) - return sorted - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error) - logger.error( - `[HistoryAdapter] Failed to get most visited: ${errorMessage}`, - ) - throw new Error(`Failed to get most visited URLs: ${errorMessage}`) - } - } -} diff --git a/packages/browseros-agent/apps/controller-ext/src/adapters/TabAdapter.ts b/packages/browseros-agent/apps/controller-ext/src/adapters/TabAdapter.ts deleted file mode 100644 index 9506282b2..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/adapters/TabAdapter.ts +++ /dev/null @@ -1,488 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -import { logger } from '@/utils/logger' -import { CHROME_API_TIMEOUTS, withTimeout } from '@/utils/timeout' - -/** - * TabAdapter - Wrapper for Chrome tabs API - * - * Responsibilities: - * - Provide clean Promise-based interface to Chrome tabs API - * - Handle Chrome API errors - * - Log operations for debugging - * - * Chrome tabs API is already Promise-based in Manifest V3, - * so we add error handling and logging. - */ -export class TabAdapter { - /** - * Get the currently active tab - * - * @param windowId - Optional window ID. If provided, gets active tab in that window. Otherwise uses current window. - * @returns Active tab in specified or current window - * @throws Error if no active tab found - */ - async getActiveTab(windowId?: number): Promise { - logger.debug( - `[TabAdapter] Getting active tab${windowId !== undefined ? ` in window ${windowId}` : ''}`, - ) - - try { - const query: chrome.tabs.QueryInfo = { active: true } - if (windowId !== undefined) { - query.windowId = windowId - } else { - query.currentWindow = true - } - const tabs = await withTimeout( - chrome.tabs.query(query), - CHROME_API_TIMEOUTS.CHROME_API, - 'chrome.tabs.query', - ) - - if (tabs.length === 0) { - throw new Error('No active tab found') - } - - logger.debug( - `[TabAdapter] Found active tab: ${tabs[0].id} (${tabs[0].url})`, - ) - return tabs[0] - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error) - logger.error(`[TabAdapter] Failed to get active tab: ${errorMessage}`) - throw new Error(`Failed to get active tab: ${errorMessage}`) - } - } - - /** - * Get a specific tab by ID - * - * @param tabId - Tab ID to retrieve - * @returns Tab object - * @throws Error if tab not found - */ - async getTab(tabId: number): Promise { - logger.debug(`[TabAdapter] Getting tab ${tabId}`) - - try { - const tab = await withTimeout( - chrome.tabs.get(tabId), - CHROME_API_TIMEOUTS.CHROME_API, - 'chrome.tabs.get', - ) - logger.debug(`[TabAdapter] Found tab: ${tab.id} (${tab.url})`) - return tab - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error) - logger.error(`[TabAdapter] Failed to get tab ${tabId}: ${errorMessage}`) - throw new Error(`Tab not found (id: ${tabId})`) - } - } - - /** - * Get all tabs across all windows - * - * @returns Array of all tabs - */ - async getAllTabs(): Promise { - logger.debug('[TabAdapter] Getting all tabs') - - try { - const tabs = await withTimeout( - chrome.tabs.query({}), - CHROME_API_TIMEOUTS.CHROME_API, - 'chrome.tabs.query', - ) - logger.debug(`[TabAdapter] Found ${tabs.length} tabs`) - return tabs - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error) - logger.error(`[TabAdapter] Failed to get all tabs: ${errorMessage}`) - throw new Error(`Failed to get tabs: ${errorMessage}`) - } - } - - /** - * Query tabs with specific criteria - * - * @param query - Chrome tabs query object - * @returns Array of matching tabs - */ - async queryTabs(query: chrome.tabs.QueryInfo): Promise { - logger.debug(`[TabAdapter] Querying tabs: ${JSON.stringify(query)}`) - - try { - const tabs = await withTimeout( - chrome.tabs.query(query), - CHROME_API_TIMEOUTS.CHROME_API, - 'chrome.tabs.query', - ) - logger.debug(`[TabAdapter] Query found ${tabs.length} tabs`) - return tabs - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error) - logger.error(`[TabAdapter] Failed to query tabs: ${errorMessage}`) - throw new Error(`Failed to query tabs: ${errorMessage}`) - } - } - - /** - * Get tabs in specific window - * - * @param windowId - Window ID - * @returns Array of tabs in window - */ - async getTabsInWindow(windowId: number): Promise { - logger.debug(`[TabAdapter] Getting tabs in window ${windowId}`) - - try { - const tabs = await withTimeout( - chrome.tabs.query({ windowId }), - CHROME_API_TIMEOUTS.CHROME_API, - 'chrome.tabs.query', - ) - logger.debug( - `[TabAdapter] Found ${tabs.length} tabs in window ${windowId}`, - ) - return tabs - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error) - logger.error( - `[TabAdapter] Failed to get tabs in window ${windowId}: ${errorMessage}`, - ) - throw new Error(`Failed to get tabs in window: ${errorMessage}`) - } - } - - /** - * Get current window's tabs - * - * @param windowId - Optional window ID. If provided, gets tabs in that window. Otherwise uses current window. - * @returns Array of tabs in specified or current window - */ - async getCurrentWindowTabs(windowId?: number): Promise { - logger.debug( - `[TabAdapter] Getting tabs in ${windowId !== undefined ? `window ${windowId}` : 'current window'}`, - ) - - try { - const query: chrome.tabs.QueryInfo = {} - if (windowId !== undefined) { - query.windowId = windowId - } else { - query.currentWindow = true - } - const tabs = await withTimeout( - chrome.tabs.query(query), - CHROME_API_TIMEOUTS.CHROME_API, - 'chrome.tabs.query', - ) - logger.debug(`[TabAdapter] Found ${tabs.length} tabs`) - return tabs - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error) - logger.error( - `[TabAdapter] Failed to get current window tabs: ${errorMessage}`, - ) - throw new Error(`Failed to get current window tabs: ${errorMessage}`) - } - } - - /** - * Open a new tab with optional URL - * - * @param url - URL to open (optional, defaults to new tab page) - * @param active - Whether to make the new tab active (default: true) - * @param windowId - Optional window ID to open tab in. If not provided, opens in current window. - * @returns Newly created tab - */ - async openTab( - url?: string, - active = true, - windowId?: number, - ): Promise { - const targetUrl = url || 'chrome://newtab/' - logger.debug( - `[TabAdapter] Opening new tab: ${targetUrl} (active: ${active}${windowId !== undefined ? `, window: ${windowId}` : ''})`, - ) - - try { - const createProps: chrome.tabs.CreateProperties = { - url: targetUrl, - active, - } - if (windowId !== undefined) { - createProps.windowId = windowId - } - const tab = await withTimeout( - chrome.tabs.create(createProps), - CHROME_API_TIMEOUTS.CHROME_API, - 'chrome.tabs.create', - ) - - if (!tab.id) { - throw new Error('Created tab has no ID') - } - - logger.debug(`[TabAdapter] Created tab ${tab.id}: ${targetUrl}`) - return tab - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error) - logger.error(`[TabAdapter] Failed to open tab: ${errorMessage}`) - throw new Error(`Failed to open tab: ${errorMessage}`) - } - } - - /** - * Close a specific tab by ID - * - * @param tabId - Tab ID to close - */ - async closeTab(tabId: number): Promise { - logger.debug(`[TabAdapter] Closing tab ${tabId}`) - - try { - // Get tab info before closing for logging - const tab = await withTimeout( - chrome.tabs.get(tabId), - CHROME_API_TIMEOUTS.CHROME_API, - 'chrome.tabs.get', - ) - const title = tab.title || 'Untitled' - - await withTimeout( - chrome.tabs.remove(tabId), - CHROME_API_TIMEOUTS.CHROME_API, - 'chrome.tabs.remove', - ) - logger.debug(`[TabAdapter] Closed tab ${tabId}: ${title}`) - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error) - logger.error(`[TabAdapter] Failed to close tab ${tabId}: ${errorMessage}`) - throw new Error(`Failed to close tab ${tabId}: ${errorMessage}`) - } - } - - /** - * Switch to (activate) a specific tab by ID - * - * @param tabId - Tab ID to switch to - * @returns Updated tab object - */ - async switchTab(tabId: number): Promise { - logger.debug(`[TabAdapter] Switching to tab ${tabId}`) - - try { - // Update tab to be active - const tab = await withTimeout( - chrome.tabs.update(tabId, { active: true }), - CHROME_API_TIMEOUTS.CHROME_API, - 'chrome.tabs.update', - ) - - if (!tab) { - throw new Error('Failed to update tab') - } - - logger.debug( - `[TabAdapter] Switched to tab ${tabId}: ${tab.title || 'Untitled'}`, - ) - return tab - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error) - logger.error( - `[TabAdapter] Failed to switch to tab ${tabId}: ${errorMessage}`, - ) - throw new Error(`Failed to switch to tab ${tabId}: ${errorMessage}`) - } - } - - /** - * Navigate a tab to a specific URL - * - * @param tabId - Tab ID to navigate - * @param url - URL to navigate to - * @returns Updated tab object - */ - async navigateTab(tabId: number, url: string): Promise { - logger.debug(`[TabAdapter] Navigating tab ${tabId} to ${url}`) - - try { - const tab = await withTimeout( - chrome.tabs.update(tabId, { url }), - CHROME_API_TIMEOUTS.CHROME_API, - 'chrome.tabs.update', - ) - - if (!tab) { - throw new Error('Failed to update tab') - } - - logger.debug(`[TabAdapter] Tab ${tabId} navigating to ${url}`) - return tab - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error) - logger.error( - `[TabAdapter] Failed to navigate tab ${tabId}: ${errorMessage}`, - ) - throw new Error( - `Failed to navigate tab ${tabId} to ${url}: ${errorMessage}`, - ) - } - } - - /** - * Group tabs together - * - * @param tabIds - Array of tab IDs to group - * @param groupId - Optional existing group ID to add tabs to - * @param windowId - Optional window ID to create the group in (prevents tabs moving to wrong window) - * @returns Group ID of the created or updated group - */ - async groupTabs( - tabIds: number[], - groupId?: number, - windowId?: number, - ): Promise { - if (tabIds.length === 0) { - throw new Error('At least one tab ID is required') - } - - logger.debug( - `Grouping tabs ${tabIds.join(', ')}${groupId ? ` into group ${groupId}` : ''}${windowId ? ` in window ${windowId}` : ''}`, - ) - - try { - // Chrome API expects [number, ...number[]] tuple type - const tabIdsTuple = tabIds as [number, ...number[]] - const options: chrome.tabs.GroupOptions = { tabIds: tabIdsTuple } - if (groupId !== undefined) { - options.groupId = groupId - } - // Specify windowId to prevent Chrome from moving tabs to the focused window - if (windowId !== undefined && groupId === undefined) { - options.createProperties = { windowId } - } - const resultGroupId = await withTimeout( - chrome.tabs.group(options), - CHROME_API_TIMEOUTS.CHROME_API, - 'chrome.tabs.group', - ) - logger.debug(`Grouped tabs into group ${resultGroupId}`) - return resultGroupId - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error) - logger.error(`Failed to group tabs: ${errorMessage}`) - throw new Error(`Failed to group tabs: ${errorMessage}`) - } - } - - /** - * Ungroup tabs (remove them from their groups) - * - * @param tabIds - Array of tab IDs to ungroup - */ - async ungroupTabs(tabIds: number[]): Promise { - if (tabIds.length === 0) { - throw new Error('At least one tab ID is required') - } - - logger.debug(`Ungrouping tabs ${tabIds.join(', ')}`) - - try { - // Chrome API expects [number, ...number[]] tuple type or single number - const tabIdsTuple = tabIds as [number, ...number[]] - await withTimeout( - chrome.tabs.ungroup(tabIdsTuple), - CHROME_API_TIMEOUTS.CHROME_API, - 'chrome.tabs.ungroup', - ) - logger.debug(`Ungrouped ${tabIds.length} tabs`) - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error) - logger.error(`Failed to ungroup tabs: ${errorMessage}`) - throw new Error(`Failed to ungroup tabs: ${errorMessage}`) - } - } - - /** - * Get all tab groups in a window - * - * @param windowId - Optional window ID. If not provided, gets groups from all windows. - * @returns Array of tab groups - */ - async getTabGroups(windowId?: number): Promise { - logger.debug( - `Getting tab groups${windowId !== undefined ? ` in window ${windowId}` : ''}`, - ) - - try { - const query: chrome.tabGroups.QueryInfo = {} - if (windowId !== undefined) { - query.windowId = windowId - } - const groups = await withTimeout( - chrome.tabGroups.query(query), - CHROME_API_TIMEOUTS.CHROME_API, - 'chrome.tabGroups.query', - ) - logger.debug(`Found ${groups.length} tab groups`) - return groups - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error) - logger.error(`Failed to get tab groups: ${errorMessage}`) - throw new Error(`Failed to get tab groups: ${errorMessage}`) - } - } - - /** - * Update a tab group's properties - * - * @param groupId - Group ID to update - * @param properties - Properties to update (title, color, collapsed) - * @returns Updated tab group - */ - async updateTabGroup( - groupId: number, - properties: chrome.tabGroups.UpdateProperties, - ): Promise { - logger.debug(`Updating tab group ${groupId}: ${JSON.stringify(properties)}`) - - try { - const group = await withTimeout( - chrome.tabGroups.update(groupId, properties), - CHROME_API_TIMEOUTS.CHROME_API, - 'chrome.tabGroups.update', - ) - if (!group) { - throw new Error(`Tab group ${groupId} not found`) - } - logger.debug( - `Updated tab group ${groupId}: title="${group.title}", color="${group.color}"`, - ) - return group - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error) - logger.error(`Failed to update tab group ${groupId}: ${errorMessage}`) - throw new Error(`Failed to update tab group ${groupId}: ${errorMessage}`) - } - } -} diff --git a/packages/browseros-agent/apps/controller-ext/src/background/BrowserOSController.ts b/packages/browseros-agent/apps/controller-ext/src/background/BrowserOSController.ts deleted file mode 100644 index f023588ab..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/background/BrowserOSController.ts +++ /dev/null @@ -1,390 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -import { ActionRegistry } from '@/actions/ActionRegistry' -import { CreateBookmarkAction } from '@/actions/bookmark/CreateBookmarkAction' -import { CreateBookmarkFolderAction } from '@/actions/bookmark/CreateBookmarkFolderAction' -import { GetBookmarkChildrenAction } from '@/actions/bookmark/GetBookmarkChildrenAction' -import { GetBookmarksAction } from '@/actions/bookmark/GetBookmarksAction' -import { MoveBookmarkAction } from '@/actions/bookmark/MoveBookmarkAction' -import { RemoveBookmarkAction } from '@/actions/bookmark/RemoveBookmarkAction' -import { RemoveBookmarkTreeAction } from '@/actions/bookmark/RemoveBookmarkTreeAction' -import { UpdateBookmarkAction } from '@/actions/bookmark/UpdateBookmarkAction' -import { CaptureScreenshotAction } from '@/actions/browser/CaptureScreenshotAction' -import { CaptureScreenshotPointerAction } from '@/actions/browser/CaptureScreenshotPointerAction' -import { ClearAction } from '@/actions/browser/ClearAction' -import { ClickAction } from '@/actions/browser/ClickAction' -import { ClickCoordinatesAction } from '@/actions/browser/ClickCoordinatesAction' -import { CloseWindowAction } from '@/actions/browser/CloseWindowAction' -import { CreateWindowAction } from '@/actions/browser/CreateWindowAction' -import { ExecuteJavaScriptAction } from '@/actions/browser/ExecuteJavaScriptAction' -import { GetAccessibilityTreeAction } from '@/actions/browser/GetAccessibilityTreeAction' -import { GetInteractiveSnapshotAction } from '@/actions/browser/GetInteractiveSnapshotAction' -import { GetPageLoadStatusAction } from '@/actions/browser/GetPageLoadStatusAction' -import { GetSnapshotAction } from '@/actions/browser/GetSnapshotAction' -import { InputTextAction } from '@/actions/browser/InputTextAction' -import { ScrollDownAction } from '@/actions/browser/ScrollDownAction' -import { ScrollToNodeAction } from '@/actions/browser/ScrollToNodeAction' -import { ScrollUpAction } from '@/actions/browser/ScrollUpAction' -import { SendKeysAction } from '@/actions/browser/SendKeysAction' -import { TypeAtCoordinatesAction } from '@/actions/browser/TypeAtCoordinatesAction' -import { CheckBrowserOSAction } from '@/actions/diagnostics/CheckBrowserOSAction' -import { GetRecentHistoryAction } from '@/actions/history/GetRecentHistoryAction' -import { SearchHistoryAction } from '@/actions/history/SearchHistoryAction' -import { CloseTabAction } from '@/actions/tab/CloseTabAction' -import { GetActiveTabAction } from '@/actions/tab/GetActiveTabAction' -import { GetTabsAction } from '@/actions/tab/GetTabsAction' -import { GroupTabsAction } from '@/actions/tab/GroupTabsAction' -import { ListTabGroupsAction } from '@/actions/tab/ListTabGroupsAction' -import { NavigateAction } from '@/actions/tab/NavigateAction' -import { OpenTabAction } from '@/actions/tab/OpenTabAction' -import { SwitchTabAction } from '@/actions/tab/SwitchTabAction' -import { UngroupTabsAction } from '@/actions/tab/UngroupTabsAction' -import { UpdateTabGroupAction } from '@/actions/tab/UpdateTabGroupAction' -import { CONCURRENCY_CONFIG } from '@/config/constants' -import type { ProtocolRequest, ProtocolResponse } from '@/protocol/types' -import { ConnectionStatus } from '@/protocol/types' -import { ConcurrencyLimiter } from '@/utils/ConcurrencyLimiter' -import { logger } from '@/utils/logger' -import { RequestTracker } from '@/utils/RequestTracker' -import { RequestValidator } from '@/utils/RequestValidator' -import { ResponseQueue } from '@/utils/ResponseQueue' -import type { PortProvider } from '@/websocket/WebSocketClient' -import { WebSocketClient } from '@/websocket/WebSocketClient' - -/** - * BrowserOS Controller - * - * Main controller class that orchestrates all components. - * Message flow: WebSocket → Validator → Tracker → Limiter → Action → Response/Queue → WebSocket - */ -export class BrowserOSController { - private wsClient: WebSocketClient - private requestTracker: RequestTracker - private concurrencyLimiter: ConcurrencyLimiter - private requestValidator: RequestValidator - private responseQueue: ResponseQueue - private actionRegistry: ActionRegistry - - constructor(getPort: PortProvider) { - logger.info('Initializing BrowserOS Controller...') - - this.requestTracker = new RequestTracker() - this.concurrencyLimiter = new ConcurrencyLimiter( - CONCURRENCY_CONFIG.maxConcurrent, - CONCURRENCY_CONFIG.maxQueueSize, - ) - this.requestValidator = new RequestValidator() - this.responseQueue = new ResponseQueue() - this.wsClient = new WebSocketClient(getPort) - this.actionRegistry = new ActionRegistry() - - this.registerActions() - this.setupWebSocketHandlers() - } - - async start(): Promise { - logger.info('Starting BrowserOS Controller...') - await this.wsClient.connect() - // Report owned windows after connection is established - await this.reportOwnedWindows() - } - - private async reportOwnedWindows(): Promise { - try { - const windows = await chrome.windows.getAll() - const windowIds = windows - .map((w) => w.id) - .filter((id): id is number => id !== undefined) - - if (windowIds.length > 0) { - this.wsClient.send({ type: 'register_windows', windowIds }) - logger.info('Reported owned windows to server', { - windowCount: windowIds.length, - windowIds, - }) - } - } catch (error) { - logger.warn('Failed to report owned windows', { - error: error instanceof Error ? error.message : String(error), - }) - } - } - - notifyWindowCreated(windowId: number): void { - try { - this.wsClient.send({ type: 'window_created', windowId }) - logger.info('Sent window_created event', { windowId }) - } catch (error) { - logger.warn('Failed to send window_created event', { - windowId, - error: error instanceof Error ? error.message : String(error), - }) - } - } - - notifyWindowRemoved(windowId: number): void { - try { - this.wsClient.send({ type: 'window_removed', windowId }) - logger.debug('Sent window_removed event', { windowId }) - } catch (error) { - logger.warn('Failed to send window_removed event', { - windowId, - error: error instanceof Error ? error.message : String(error), - }) - } - } - - stop(): void { - logger.info('Stopping BrowserOS Controller...') - this.wsClient.disconnect() - this.requestTracker.destroy() - this.requestValidator.destroy() - this.responseQueue.clear() - } - - logStats(): void { - const stats = this.getStats() - logger.info('=== Controller Stats ===') - logger.info(`Connection: ${stats.connection}`) - logger.info(`Requests: ${JSON.stringify(stats.requests)}`) - logger.info(`Concurrency: ${JSON.stringify(stats.concurrency)}`) - logger.info(`Validator: ${JSON.stringify(stats.validator)}`) - logger.info(`Response Queue: ${stats.responseQueue.size} queued`) - } - - getStats() { - return { - connection: this.wsClient.getStatus(), - requests: this.requestTracker.getStats(), - concurrency: this.concurrencyLimiter.getStats(), - validator: this.requestValidator.getStats(), - responseQueue: { - size: this.responseQueue.size(), - }, - } - } - - isConnected(): boolean { - return this.wsClient.isConnected() - } - - notifyWindowFocused(windowId?: number): void { - try { - this.wsClient.send({ type: 'focused', windowId }) - logger.debug('Sent focused event', { windowId }) - } catch (error) { - logger.warn('Failed to send focused event', { - windowId, - error: error instanceof Error ? error.message : String(error), - }) - } - } - - private registerActions(): void { - logger.info('Registering actions...') - - this.actionRegistry.register('checkBrowserOS', new CheckBrowserOSAction()) - - this.actionRegistry.register('getActiveTab', new GetActiveTabAction()) - this.actionRegistry.register('getTabs', new GetTabsAction()) - this.actionRegistry.register('openTab', new OpenTabAction()) - this.actionRegistry.register('closeTab', new CloseTabAction()) - this.actionRegistry.register('switchTab', new SwitchTabAction()) - this.actionRegistry.register('navigate', new NavigateAction()) - this.actionRegistry.register('listTabGroups', new ListTabGroupsAction()) - this.actionRegistry.register('groupTabs', new GroupTabsAction()) - this.actionRegistry.register('updateTabGroup', new UpdateTabGroupAction()) - this.actionRegistry.register('ungroupTabs', new UngroupTabsAction()) - - this.actionRegistry.register('createWindow', new CreateWindowAction()) - this.actionRegistry.register('closeWindow', new CloseWindowAction()) - - this.actionRegistry.register('getBookmarks', new GetBookmarksAction()) - this.actionRegistry.register('createBookmark', new CreateBookmarkAction()) - this.actionRegistry.register('removeBookmark', new RemoveBookmarkAction()) - this.actionRegistry.register('updateBookmark', new UpdateBookmarkAction()) - this.actionRegistry.register( - 'createBookmarkFolder', - new CreateBookmarkFolderAction(), - ) - this.actionRegistry.register( - 'getBookmarkChildren', - new GetBookmarkChildrenAction(), - ) - this.actionRegistry.register('moveBookmark', new MoveBookmarkAction()) - this.actionRegistry.register( - 'removeBookmarkTree', - new RemoveBookmarkTreeAction(), - ) - - this.actionRegistry.register('searchHistory', new SearchHistoryAction()) - this.actionRegistry.register( - 'getRecentHistory', - new GetRecentHistoryAction(), - ) - - this.actionRegistry.register( - 'getInteractiveSnapshot', - new GetInteractiveSnapshotAction(), - ) - this.actionRegistry.register('click', new ClickAction()) - this.actionRegistry.register('inputText', new InputTextAction()) - this.actionRegistry.register('clear', new ClearAction()) - this.actionRegistry.register('scrollToNode', new ScrollToNodeAction()) - - this.actionRegistry.register( - 'captureScreenshot', - new CaptureScreenshotAction(), - ) - this.actionRegistry.register( - 'captureScreenshotPointer', - new CaptureScreenshotPointerAction(), - ) - - this.actionRegistry.register('scrollDown', new ScrollDownAction()) - this.actionRegistry.register('scrollUp', new ScrollUpAction()) - - this.actionRegistry.register( - 'executeJavaScript', - new ExecuteJavaScriptAction(), - ) - this.actionRegistry.register('sendKeys', new SendKeysAction()) - this.actionRegistry.register( - 'getPageLoadStatus', - new GetPageLoadStatusAction(), - ) - this.actionRegistry.register('getSnapshot', new GetSnapshotAction()) - this.actionRegistry.register( - 'getAccessibilityTree', - new GetAccessibilityTreeAction(), - ) - this.actionRegistry.register( - 'clickCoordinates', - new ClickCoordinatesAction(), - ) - this.actionRegistry.register( - 'typeAtCoordinates', - new TypeAtCoordinatesAction(), - ) - - const actions = this.actionRegistry.getAvailableActions() - logger.info(`Registered ${actions.length} action(s): ${actions.join(', ')}`) - } - - private setupWebSocketHandlers(): void { - this.wsClient.onMessage((message: ProtocolResponse) => { - this.handleIncomingMessage(message) - }) - - this.wsClient.onStatusChange((status: ConnectionStatus) => { - this.handleStatusChange(status) - }) - } - - private handleIncomingMessage(message: ProtocolResponse): void { - const rawMessage = message as ProtocolResponse & Partial - - if (rawMessage.action) { - this.processRequest(rawMessage).catch((error) => { - logger.error( - `Unhandled error processing request ${rawMessage.id}: ${error}`, - ) - }) - } else if (rawMessage.ok !== undefined) { - logger.info( - `Received server message: ${rawMessage.id} - ${rawMessage.ok ? 'success' : 'error'}`, - ) - if (rawMessage.data) { - logger.debug(`Server data: ${JSON.stringify(rawMessage.data)}`) - } - } else { - logger.warn( - `Received unknown message format: ${JSON.stringify(rawMessage)}`, - ) - } - } - - private async processRequest(request: unknown): Promise { - let validatedRequest: ProtocolRequest - let requestId: string | undefined - - try { - validatedRequest = this.requestValidator.validate(request) - requestId = validatedRequest.id - - this.requestTracker.start(validatedRequest.id, validatedRequest.action) - - await this.concurrencyLimiter.execute(async () => { - this.requestTracker.markExecuting(validatedRequest.id) - await this.executeAction(validatedRequest) - }) - - this.requestTracker.complete(validatedRequest.id) - this.requestValidator.markComplete(validatedRequest.id) - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error) - logger.error(`Request processing failed: ${errorMessage}`) - - if (requestId) { - this.requestTracker.complete(requestId, errorMessage) - this.requestValidator.markComplete(requestId) - - this.sendResponse({ - id: requestId, - ok: false, - error: errorMessage, - }) - } - } - } - - private async executeAction(request: ProtocolRequest): Promise { - logger.info(`Executing action: ${request.action} [${request.id}]`) - - const actionResponse = await this.actionRegistry.dispatch( - request.action, - request.payload, - ) - - this.sendResponse({ - id: request.id, - ok: actionResponse.ok, - data: actionResponse.data, - error: actionResponse.error, - }) - - const status = actionResponse.ok ? 'succeeded' : 'failed' - logger.info(`Action ${status}: ${request.action} [${request.id}]`) - } - - private sendResponse(response: ProtocolResponse): void { - try { - if (this.wsClient.isConnected()) { - this.wsClient.send(response) - } else { - logger.warn(`Not connected. Queueing response: ${response.id}`) - this.responseQueue.enqueue(response) - } - } catch (error) { - logger.error(`Failed to send response ${response.id}: ${error}`) - this.responseQueue.enqueue(response) - } - } - - private handleStatusChange(status: ConnectionStatus): void { - logger.info(`Connection status changed: ${status}`) - - if (status === ConnectionStatus.CONNECTED) { - if (!this.responseQueue.isEmpty()) { - logger.info(`Flushing ${this.responseQueue.size()} queued responses...`) - this.responseQueue.flush((response) => { - this.wsClient.send(response) - }) - } - } - } -} diff --git a/packages/browseros-agent/apps/controller-ext/src/background/index.ts b/packages/browseros-agent/apps/controller-ext/src/background/index.ts deleted file mode 100644 index e0c648b65..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/background/index.ts +++ /dev/null @@ -1,213 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import { getWebSocketPort } from '@/utils/ConfigHelper' -import { startKeepAlive, stopKeepAlive } from '@/utils/KeepAlive' -import { logger } from '@/utils/logger' -import { BrowserOSController } from './BrowserOSController' - -const STATS_LOG_INTERVAL_MS = 30000 - -interface ControllerState { - controller: BrowserOSController | null - initPromise: Promise | null - statsTimer: ReturnType | null -} - -type BrowserOSGlobals = typeof globalThis & { - __browserosControllerState?: ControllerState - __browserosController?: BrowserOSController | null -} - -const globals = globalThis as BrowserOSGlobals -const controllerState: ControllerState = - globals.__browserosControllerState ?? - (() => { - const state: ControllerState = { - controller: globals.__browserosController ?? null, - initPromise: null, - statsTimer: null, - } - globals.__browserosControllerState = state - return state - })() - -function setDebugController(controller: BrowserOSController | null): void { - globals.__browserosController = controller -} - -function startStatsTimer(): void { - if (controllerState.statsTimer) { - return - } - - controllerState.statsTimer = setInterval(() => { - controllerState.controller?.logStats() - }, STATS_LOG_INTERVAL_MS) -} - -function stopStatsTimer(): void { - if (!controllerState.statsTimer) { - return - } - - clearInterval(controllerState.statsTimer) - controllerState.statsTimer = null -} - -async function getOrCreateController(): Promise { - if (controllerState.controller) { - return controllerState.controller - } - - if (!controllerState.initPromise) { - controllerState.initPromise = (async () => { - try { - await startKeepAlive() - const controller = new BrowserOSController(getWebSocketPort) - await controller.start() - - controllerState.controller = controller - setDebugController(controller) - startStatsTimer() - - return controller - } catch (error) { - controllerState.controller = null - setDebugController(null) - stopStatsTimer() - try { - await stopKeepAlive() - } catch { - // ignore - } - throw error - } finally { - controllerState.initPromise = null - } - })() - } - - const initPromise = controllerState.initPromise - if (!initPromise) { - throw new Error('Controller init promise missing') - } - return initPromise -} - -async function shutdownController(reason: string): Promise { - logger.info('Controller shutdown requested', { reason }) - - if (controllerState.initPromise) { - try { - await controllerState.initPromise - } catch { - // ignore start errors during shutdown - } - } - - const controller = controllerState.controller - if (!controller) { - try { - await stopKeepAlive() - } catch { - // ignore - } - stopStatsTimer() - setDebugController(null) - return - } - - controller.stop() - controllerState.controller = null - setDebugController(null) - stopStatsTimer() - - try { - await stopKeepAlive() - } catch { - // ignore - } -} - -function ensureControllerRunning(trigger: string): void { - getOrCreateController().catch((error) => { - const message = - error instanceof Error ? error.message : JSON.stringify(error) - logger.error('Controller failed to start', { trigger, error: message }) - }) -} - -logger.info('Extension loaded') - -chrome.runtime.onInstalled.addListener(() => { - logger.info('Extension installed') -}) - -chrome.runtime.onStartup.addListener(() => { - logger.info('Browser startup event') - ensureControllerRunning('runtime.onStartup') -}) - -// Immediately attempt to start the controller when the service worker initializes -ensureControllerRunning('service-worker-init') - -chrome.windows.onFocusChanged.addListener((windowId) => { - if (windowId === chrome.windows.WINDOW_ID_NONE) { - return - } - - notifyWindowFocused(windowId).catch((error) => { - const message = - error instanceof Error ? error.message : JSON.stringify(error) - logger.warn('Failed to notify focus change', { windowId, error: message }) - }) -}) - -chrome.windows.onCreated.addListener((window) => { - logger.info('Window created event received', { windowId: window.id }) - - if (window.id === undefined) { - return - } - - notifyWindowCreated(window.id).catch((error) => { - const message = - error instanceof Error ? error.message : JSON.stringify(error) - logger.warn('Failed to notify window created', { - windowId: window.id, - error: message, - }) - }) -}) - -chrome.windows.onRemoved.addListener((windowId) => { - notifyWindowRemoved(windowId).catch((error) => { - const message = - error instanceof Error ? error.message : JSON.stringify(error) - logger.warn('Failed to notify window removed', { windowId, error: message }) - }) -}) - -chrome.runtime.onSuspend?.addListener(() => { - logger.info('Extension suspending') - void shutdownController('runtime.onSuspend') -}) - -async function notifyWindowFocused(windowId: number): Promise { - const controller = await getOrCreateController() - controller.notifyWindowFocused(windowId) -} - -async function notifyWindowCreated(windowId: number): Promise { - const controller = await getOrCreateController() - controller.notifyWindowCreated(windowId) -} - -async function notifyWindowRemoved(windowId: number): Promise { - const controller = await getOrCreateController() - controller.notifyWindowRemoved(windowId) -} diff --git a/packages/browseros-agent/apps/controller-ext/src/config/constants.ts b/packages/browseros-agent/apps/controller-ext/src/config/constants.ts deleted file mode 100644 index 72d7ca3a1..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/config/constants.ts +++ /dev/null @@ -1,59 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import { CONTENT_LIMITS } from '@browseros/shared/constants/limits' -import { DEFAULT_PORTS } from '@browseros/shared/constants/ports' -import { TIMEOUTS } from '@browseros/shared/constants/timeouts' -export type WebSocketProtocol = 'ws' | 'wss' - -export interface WebSocketConfig { - readonly protocol: WebSocketProtocol - readonly host: string - readonly path: string - readonly defaultExtensionPort: number - readonly reconnectIntervalMs: number - readonly heartbeatInterval: number - readonly heartbeatTimeout: number - readonly connectionTimeout: number - readonly requestTimeout: number -} - -export interface ConcurrencyConfig { - readonly maxConcurrent: number - readonly maxQueueSize: number -} - -export interface LoggingConfig { - readonly enabled: boolean - readonly level: 'debug' | 'info' | 'warn' | 'error' - readonly prefix: string -} - -export const WEBSOCKET_CONFIG: WebSocketConfig = { - protocol: 'ws', - host: '127.0.0.1', - path: '/controller', - defaultExtensionPort: DEFAULT_PORTS.extension, - - reconnectIntervalMs: TIMEOUTS.WS_RECONNECT_INTERVAL, - - heartbeatInterval: TIMEOUTS.WS_HEARTBEAT_INTERVAL, - heartbeatTimeout: TIMEOUTS.WS_HEARTBEAT_TIMEOUT, - - connectionTimeout: TIMEOUTS.WS_CONNECTION_TIMEOUT, - requestTimeout: TIMEOUTS.WS_REQUEST_TIMEOUT, -} - -export const CONCURRENCY_CONFIG: ConcurrencyConfig = { - maxConcurrent: 1, - maxQueueSize: CONTENT_LIMITS.MAX_QUEUE_SIZE, -} - -export const LOGGING_CONFIG: LoggingConfig = { - enabled: true, - level: 'info', - prefix: '', -} diff --git a/packages/browseros-agent/apps/controller-ext/src/protocol/types.ts b/packages/browseros-agent/apps/controller-ext/src/protocol/types.ts deleted file mode 100644 index 3b41bb6f5..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/protocol/types.ts +++ /dev/null @@ -1,60 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -import { z } from 'zod' - -// Request schema -export const ProtocolRequestSchema = z.object({ - id: z.string().describe('Request UUID'), - action: z.string().min(1).describe('Action name'), - payload: z.any().optional().describe('Action-specific data'), -}) - -// Response schema -export const ProtocolResponseSchema = z.object({ - id: z.string().describe('Request ID (same as request)'), - ok: z.boolean().describe('Success flag'), - data: z.any().optional().describe('Result data'), - error: z.string().optional().describe('Error message'), -}) - -// Action response schema (used internally by action handlers) -export const ActionResponseSchema = z - .object({ - ok: z.boolean().describe('Success flag'), - data: z.any().optional().describe('Result data'), - error: z.string().optional().describe('Error message'), - }) - .refine( - (data) => { - // If ok is true, there should be no error - if (data.ok && data.error !== undefined) { - return false - } - // If ok is false, there should be an error - if (!data.ok && !data.error) { - return false - } - return true - }, - { - message: - 'When ok is true, error must be undefined. When ok is false, error must be provided.', - }, - ) - -// Type exports -export type ProtocolRequest = z.infer -export type ProtocolResponse = z.infer -export type ActionResponse = z.infer - -// Connection status enum -export enum ConnectionStatus { - DISCONNECTED = 'disconnected', - CONNECTING = 'connecting', - CONNECTED = 'connected', - RECONNECTING = 'reconnecting', - ERROR = 'error', -} diff --git a/packages/browseros-agent/apps/controller-ext/src/types/chrome-browser-os.d.ts b/packages/browseros-agent/apps/controller-ext/src/types/chrome-browser-os.d.ts deleted file mode 100644 index 229dbddfe..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/types/chrome-browser-os.d.ts +++ /dev/null @@ -1,419 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -// Type definitions for chrome.browserOS API - -declare namespace chrome.browserOS { - // Page load status information - interface PageLoadStatus { - isResourcesLoading: boolean - isDOMContentLoaded: boolean - isPageComplete: boolean - } - - // Rectangle bounds - interface Rect { - x: number - y: number - width: number - height: number - } - - // Alias for backward compatibility - type BoundingRect = Rect - - // Interactive element types - type InteractiveNodeType = 'clickable' | 'typeable' | 'selectable' | 'other' - - // Supported keyboard keys - type Key = - | 'Enter' - | 'Delete' - | 'Backspace' - | 'Tab' - | 'Escape' - | 'ArrowUp' - | 'ArrowDown' - | 'ArrowLeft' - | 'ArrowRight' - | 'Home' - | 'End' - | 'PageUp' - | 'PageDown' - - // Interactive node in the snapshot - interface InteractiveNode { - nodeId: number - type: InteractiveNodeType - name?: string - rect?: Rect - attributes?: { - in_viewport?: string // "true" if visible in viewport, "false" if not visible - [key: string]: string | undefined - } - } - - // Snapshot of interactive elements - interface InteractiveSnapshot { - snapshotId: number - timestamp: number - elements: InteractiveNode[] - hierarchicalStructure?: string // Hierarchical text representation with context - processingTimeMs: number // Performance metrics - } - - // Options for getInteractiveSnapshot - interface InteractiveSnapshotOptions { - viewportOnly?: boolean - } - - // Accessibility node - interface AccessibilityNode { - id: number - role: string - name?: string - value?: string - attributes?: Record - childIds?: number[] - } - - // Accessibility tree - interface AccessibilityTree { - rootId: number - nodes: Record - } - - // API functions - function getPageLoadStatus( - tabId: number, - callback: (status: PageLoadStatus) => void, - ): void - - function getPageLoadStatus(callback: (status: PageLoadStatus) => void): void - - function getAccessibilityTree( - tabId: number, - callback: (tree: AccessibilityTree) => void, - ): void - - function getAccessibilityTree( - callback: (tree: AccessibilityTree) => void, - ): void - - function getInteractiveSnapshot( - tabId: number, - options: InteractiveSnapshotOptions, - callback: (snapshot: InteractiveSnapshot) => void, - ): void - - function getInteractiveSnapshot( - tabId: number, - callback: (snapshot: InteractiveSnapshot) => void, - ): void - - function getInteractiveSnapshot( - options: InteractiveSnapshotOptions, - callback: (snapshot: InteractiveSnapshot) => void, - ): void - - function getInteractiveSnapshot( - callback: (snapshot: InteractiveSnapshot) => void, - ): void - - function click(tabId: number, nodeId: number, callback: () => void): void - - function click(nodeId: number, callback: () => void): void - - function inputText( - tabId: number, - nodeId: number, - text: string, - callback: () => void, - ): void - - function inputText(nodeId: number, text: string, callback: () => void): void - - function clear(tabId: number, nodeId: number, callback: () => void): void - - function clear(nodeId: number, callback: () => void): void - - function scrollUp(tabId: number, callback: () => void): void - - function scrollUp(callback: () => void): void - - function scrollDown(tabId: number, callback: () => void): void - - function scrollDown(callback: () => void): void - - function scrollToNode( - tabId: number, - nodeId: number, - callback: (scrolled: boolean) => void, - ): void - - function scrollToNode( - nodeId: number, - callback: (scrolled: boolean) => void, - ): void - - function sendKeys( - tabId: number, - key: - | 'Enter' - | 'Delete' - | 'Backspace' - | 'Tab' - | 'Escape' - | 'ArrowUp' - | 'ArrowDown' - | 'ArrowLeft' - | 'ArrowRight' - | 'Home' - | 'End' - | 'PageUp' - | 'PageDown', - callback: () => void, - ): void - - function sendKeys( - key: - | 'Enter' - | 'Delete' - | 'Backspace' - | 'Tab' - | 'Escape' - | 'ArrowUp' - | 'ArrowDown' - | 'ArrowLeft' - | 'ArrowRight' - | 'Home' - | 'End' - | 'PageUp' - | 'PageDown', - callback: () => void, - ): void - - // Capture screenshot with all optional parameters - function captureScreenshot( - tabId: number, - thumbnailSize: number, - showHighlights: boolean, - width: number, - height: number, - callback: (dataUrl: string) => void, - ): void - - // Capture screenshot with tab ID, thumbnail size, and highlights - function captureScreenshot( - tabId: number, - thumbnailSize: number, - showHighlights: boolean, - callback: (dataUrl: string) => void, - ): void - - // Capture screenshot with tab ID and thumbnail size - function captureScreenshot( - tabId: number, - thumbnailSize: number, - callback: (dataUrl: string) => void, - ): void - - // Capture screenshot with tab ID only (backwards compatibility) - function captureScreenshot( - tabId: number, - callback: (dataUrl: string) => void, - ): void - - // Capture screenshot of active tab with default size - function captureScreenshot(callback: (dataUrl: string) => void): void - - // Snapshot extraction types - type SnapshotType = 'text' | 'links' - - // Context for snapshot extraction - type SnapshotContext = 'visible' | 'full' - - // Section types based on ARIA landmarks - type SectionType = - | 'main' - | 'navigation' - | 'footer' - | 'header' - | 'article' - | 'aside' - | 'complementary' - | 'contentinfo' - | 'form' - | 'search' - | 'region' - | 'other' - - // Text snapshot result for a section - interface TextSnapshotResult { - text: string - characterCount: number - } - - // Link information - interface LinkInfo { - text: string - url: string - title?: string - attributes?: Record - isExternal: boolean - } - - // Links snapshot result for a section - interface LinksSnapshotResult { - links: LinkInfo[] - } - - // Section with all possible snapshot results - interface SnapshotSection { - type: string - textResult?: TextSnapshotResult - linksResult?: LinksSnapshotResult - } - - // Main snapshot result - interface Snapshot { - type: SnapshotType - context: SnapshotContext - timestamp: number - sections: SnapshotSection[] - processingTimeMs: number - } - - // Options for getSnapshot - interface SnapshotOptions { - context?: SnapshotContext - includeSections?: SectionType[] - } - - function getSnapshot( - tabId: number, - type: SnapshotType, - options: SnapshotOptions, - callback: (snapshot: Snapshot) => void, - ): void - - function getSnapshot( - tabId: number, - type: SnapshotType, - callback: (snapshot: Snapshot) => void, - ): void - - function getSnapshot( - tabId: number, - callback: (snapshot: Snapshot) => void, - ): void - - function getSnapshot( - type: SnapshotType, - options: SnapshotOptions, - callback: (snapshot: Snapshot) => void, - ): void - - function getSnapshot( - type: SnapshotType, - callback: (snapshot: Snapshot) => void, - ): void - - // Get BrowserOS version number - function getVersionNumber(callback: (version: string) => void): void - - // Logs a metric event with optional properties - function logMetric( - eventName: string, - properties: Record, - callback: () => void, - ): void - - function logMetric(eventName: string, callback: () => void): void - - function logMetric( - eventName: string, - properties?: Record, - ): void - - function logMetric(eventName: string): void - - // Execute JavaScript in a tab - function executeJavaScript( - tabId: number, - code: string, - callback: (result: unknown) => void, - ): void - - function executeJavaScript( - code: string, - callback: (result: unknown) => void, - ): void - - // Click at specific viewport coordinates - function clickCoordinates( - tabId: number, - x: number, - y: number, - callback: () => void, - ): void - - function clickCoordinates(x: number, y: number, callback: () => void): void - - // Type text at specific viewport coordinates - function typeAtCoordinates( - tabId: number, - x: number, - y: number, - text: string, - callback: () => void, - ): void - - function typeAtCoordinates( - x: number, - y: number, - text: string, - callback: () => void, - ): void - - // Preference object - interface PrefObject { - key: string - type: string - value: unknown - } - - // Get a specific preference value - function getPref(name: string, callback: (pref: PrefObject) => void): void - - // Set a specific preference value - function setPref( - name: string, - value: unknown, - pageId: string, - callback: (success: boolean) => void, - ): void - - function setPref( - name: string, - value: unknown, - callback: (success: boolean) => void, - ): void - - // Get all preferences (filtered to browseros.* prefs) - function getAllPrefs(callback: (prefs: PrefObject[]) => void): void -} -declare namespace chrome { - namespace BrowserOS { - function getPrefs( - keys: string[], - callback: (prefs: Record) => void, - ): void - function setPrefs( - prefs: Record, - callback?: (success: boolean) => void, - ): void - } -} diff --git a/packages/browseros-agent/apps/controller-ext/src/utils/ConcurrencyLimiter.ts b/packages/browseros-agent/apps/controller-ext/src/utils/ConcurrencyLimiter.ts deleted file mode 100644 index 624308510..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/utils/ConcurrencyLimiter.ts +++ /dev/null @@ -1,123 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -import { logger } from './logger' - -interface QueuedTask { - task: () => Promise - resolve: (value: T) => void - reject: (error: Error) => void -} - -export interface ConcurrencyStats { - inFlight: number - queued: number - utilization: number -} - -export class ConcurrencyLimiter { - private isProcessing = false - private queue: Array> = [] - - constructor( - maxConcurrent: number, - private maxQueueSize = 1000, - ) { - if (maxConcurrent !== 1) { - logger.warn( - `ConcurrencyLimiter: maxConcurrent=${maxConcurrent} but extension is single-threaded. ` + - `Using mutex mode (sequential execution) to prevent race conditions.`, - ) - } - logger.info( - `ConcurrencyLimiter initialized: sequential=true, queueSize=${maxQueueSize}`, - ) - } - - async execute(task: () => Promise): Promise { - // Queue limit check first - if (this.queue.length >= this.maxQueueSize) { - logger.error( - `Queue full (${this.maxQueueSize} requests). Rejecting request.`, - ) - throw new Error( - `Controller overloaded. Queue full (${this.maxQueueSize} requests). Server should slow down.`, - ) - } - - return new Promise((resolve, reject) => { - this.queue.push({ - task, - // @ts-expect-error - TS can't infer generic type here - resolve, - reject, - }) - - const status = this.isProcessing ? 'QUEUED (mutex held)' : 'IMMEDIATE' - logger.info( - `[MUTEX] Task arrival - Status: ${status}, Queue size now: ${this.queue.length}`, - ) - - if (!this.isProcessing) { - this.processQueue() - } - }) - } - - private processQueue(): void { - if (this.isProcessing || this.queue.length === 0) { - return - } - - // Log BEFORE we remove from queue to show true queue size - const queueSizeBeforeRemoval = this.queue.length - - this.isProcessing = true - const item = this.queue.shift() - if (!item) { - this.isProcessing = false - return - } - const { task, resolve, reject } = item - - logger.info( - `[MUTEX] Acquired. Started processing (${queueSizeBeforeRemoval} task(s) were queued, ${this.queue.length} still waiting).`, - ) - - const startTime = Date.now() - - task() - .then(resolve) - .catch(reject) - .finally(() => { - const duration = Date.now() - startTime - this.isProcessing = false - - logger.info( - `[MUTEX] Released after ${duration}ms. ${this.queue.length} task(s) remaining.`, - ) - - this.processQueue() - }) - } - - getStats(): ConcurrencyStats { - return { - inFlight: this.isProcessing ? 1 : 0, - queued: this.queue.length, - utilization: this.isProcessing ? 1.0 : 0.0, - } - } - - // For debugging - logStats(): void { - const stats = this.getStats() - logger.info( - `Concurrency: ${stats.inFlight} in-flight (mutex mode), ` + - `${stats.queued} queued, ` + - `${Math.round(stats.utilization * 100)}% utilization`, - ) - } -} diff --git a/packages/browseros-agent/apps/controller-ext/src/utils/ConfigHelper.ts b/packages/browseros-agent/apps/controller-ext/src/utils/ConfigHelper.ts deleted file mode 100644 index 3175b4bba..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/utils/ConfigHelper.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -/// - -import { getBrowserOSAdapter } from '@/adapters/BrowserOSAdapter' -import { WEBSOCKET_CONFIG } from '@/config/constants' -import { logger } from '@/utils/logger' - -/** - * Get the WebSocket port from BrowserOS preferences - * Returns browseros.server.extension_port preference value - * Falls back to port from constants if preference cannot be retrieved - */ -export async function getWebSocketPort(): Promise { - try { - const adapter = getBrowserOSAdapter() - const pref = await adapter.getPref('browseros.server.extension_port') - - if (pref && typeof pref.value === 'number') { - logger.info(`Using port from BrowserOS preferences: ${pref.value}`) - return pref.value - } - - logger.warn( - `Port preference not found, using default: ${WEBSOCKET_CONFIG.defaultExtensionPort}`, - ) - return WEBSOCKET_CONFIG.defaultExtensionPort - } catch (error) { - logger.error( - `Failed to get port from BrowserOS preferences: ${error}, using default: ${WEBSOCKET_CONFIG.defaultExtensionPort}`, - ) - return WEBSOCKET_CONFIG.defaultExtensionPort - } -} diff --git a/packages/browseros-agent/apps/controller-ext/src/utils/KeepAlive.ts b/packages/browseros-agent/apps/controller-ext/src/utils/KeepAlive.ts deleted file mode 100644 index 8ef7373a2..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/utils/KeepAlive.ts +++ /dev/null @@ -1,39 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -import { logger } from '@/utils/logger' - -const KEEPALIVE_ALARM_NAME = 'browseros-keepalive' -const KEEPALIVE_INTERVAL_MINUTES = 0.33 // ~20 seconds - -let isInitialized = false - -export async function startKeepAlive(): Promise { - if (isInitialized) { - logger.debug('KeepAlive already started') - return - } - - chrome.alarms.onAlarm.addListener((alarm) => { - if (alarm.name === KEEPALIVE_ALARM_NAME) { - logger.debug('KeepAlive: ping (service worker alive)') - } - }) - - await chrome.alarms.create(KEEPALIVE_ALARM_NAME, { - periodInMinutes: KEEPALIVE_INTERVAL_MINUTES, - }) - - isInitialized = true - logger.info( - `KeepAlive started: alarm every ${KEEPALIVE_INTERVAL_MINUTES * 60}s`, - ) -} - -export async function stopKeepAlive(): Promise { - await chrome.alarms.clear(KEEPALIVE_ALARM_NAME) - isInitialized = false - logger.info('KeepAlive stopped') -} diff --git a/packages/browseros-agent/apps/controller-ext/src/utils/PointerOverlay.ts b/packages/browseros-agent/apps/controller-ext/src/utils/PointerOverlay.ts deleted file mode 100644 index 5da1841d7..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/utils/PointerOverlay.ts +++ /dev/null @@ -1,142 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import { getBrowserOSAdapter } from '@/adapters/BrowserOSAdapter' -import { logger } from '@/utils/logger' - -const POINTER_DISPLAY_DURATION_MS = 3000 -const POINTER_DELAY_BEFORE_ACTION_MS = 500 - -/** - * PointerOverlay - Shows a visual mouse pointer overlay before actions - * - * Injects JavaScript to display a pointer arrow at the specified coordinates. - * The pointer auto-removes after POINTER_DISPLAY_DURATION_MS. - * - * biome-ignore lint/complexity/noStaticOnlyClass: class created before biome was setup - */ -export class PointerOverlay { - private static browserOS = getBrowserOSAdapter() - - /** - * Show a pointer at the specified coordinates - * @param tabId - Tab to show pointer in - * @param x - X coordinate in viewport pixels - * @param y - Y coordinate in viewport pixels - * @param text - Optional label text (e.g., "Click", "Type: hello...") - */ - static async showPointer( - tabId: number, - x: number, - y: number, - text?: string, - ): Promise { - const pointerId = `browseros-pointer-${Date.now()}` - - const textLabel = text - ? ` - var label = document.createElement('div'); - label.style.cssText = 'position: absolute; top: 20px; left: 12px; background: rgba(0,0,0,0.9); color: white; padding: 4px 8px; border-radius: 4px; font-size: 12px; font-family: monospace; white-space: nowrap; box-shadow: 0 2px 4px rgba(0,0,0,0.5);'; - label.textContent = '${text.replace(/[`$\\]/g, '\\$&').replace(/'/g, "\\'")}'; - shadow.appendChild(label); - ` - : '' - - const script = ` - (function() { - var existing = document.querySelector('browseros-pointer'); - if (existing) existing.remove(); - - if (!customElements.get('browseros-pointer')) { - customElements.define('browseros-pointer', class extends HTMLElement { - constructor() { - super(); - this.attachShadow({ mode: 'open' }); - } - }); - } - - var host = document.createElement('browseros-pointer'); - host.id = '${pointerId}'; - host.style.cssText = 'position: fixed; left: ${x}px; top: ${y}px; z-index: 2147483647; pointer-events: none;'; - - var shadow = host.shadowRoot; - - var arrow = document.createElement('div'); - arrow.style.cssText = 'width: 0; height: 0; border-style: solid; border-width: 0 12px 20px 12px; border-color: transparent transparent #FB6618 transparent; transform: translate(-3px, -3px) rotate(45deg); filter: drop-shadow(1px 1px 2px rgba(0,0,0,0.4));'; - shadow.appendChild(arrow); - - ${textLabel} - - document.body.appendChild(host); - - setTimeout(function() { - var el = document.getElementById('${pointerId}'); - if (el) el.remove(); - }, ${POINTER_DISPLAY_DURATION_MS}); - })(); - ` - - try { - await PointerOverlay.browserOS.executeJavaScript(tabId, script) - logger.debug( - `[PointerOverlay] Showed pointer at (${x}, ${y}) in tab ${tabId}${text ? ` with label "${text}"` : ''}`, - ) - } catch (error) { - logger.warn( - `[PointerOverlay] Failed to show pointer: ${error instanceof Error ? error.message : String(error)}`, - ) - } - } - - /** - * Show pointer and wait before action - * Returns after the delay so the action can proceed - */ - static async showPointerAndWait( - tabId: number, - x: number, - y: number, - text?: string, - ): Promise { - await PointerOverlay.showPointer(tabId, x, y, text) - await PointerOverlay.delay(POINTER_DELAY_BEFORE_ACTION_MS) - } - - /** - * Calculate center coordinates from a rect - */ - static getCenterCoordinates(rect: { - x: number - y: number - width: number - height: number - }): { x: number; y: number } { - return { - x: Math.round(rect.x + rect.width / 2), - y: Math.round(rect.y + rect.height / 2), - } - } - - /** - * Calculate left-center coordinates (for type actions) - */ - static getLeftCenterCoordinates(rect: { - x: number - y: number - width: number - height: number - }): { x: number; y: number } { - return { - x: Math.round(rect.x + 10), - y: Math.round(rect.y + rect.height / 2), - } - } - - private static delay(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)) - } -} diff --git a/packages/browseros-agent/apps/controller-ext/src/utils/RequestTracker.ts b/packages/browseros-agent/apps/controller-ext/src/utils/RequestTracker.ts deleted file mode 100644 index f44cfc6ba..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/utils/RequestTracker.ts +++ /dev/null @@ -1,129 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -import { logger } from './logger' - -export interface TrackedRequest { - id: string - action: string - startTime: number - status: 'pending' | 'executing' | 'completed' | 'failed' - duration?: number - error?: string -} - -export interface RequestStats { - inFlight: number - avgDuration: number - errorRate: number - totalRequests: number -} - -export class RequestTracker { - private requests = new Map() - private cleanupInterval: ReturnType | null = null - - constructor() { - // Start periodic cleanup of old completed requests - this.cleanupInterval = setInterval(() => this.cleanup(), 60000) // Every 1 minute - } - - start(id: string, action: string): void { - this.requests.set(id, { - id, - action, - startTime: Date.now(), - status: 'pending', - }) - logger.debug(`Request started: ${id} [${action}]`) - } - - markExecuting(id: string): void { - const req = this.requests.get(id) - if (req) { - req.status = 'executing' - logger.debug(`Request executing: ${id}`) - } - } - - complete(id: string, error?: string): void { - const req = this.requests.get(id) - if (req) { - req.status = error ? 'failed' : 'completed' - req.duration = Date.now() - req.startTime - req.error = error - - logger.info( - `Request ${error ? 'failed' : 'completed'}: ${id} [${req.action}] in ${req.duration}ms`, - ) - - // Schedule cleanup after 1 minute - setTimeout(() => this.requests.delete(id), 60000) - } - } - - getActiveRequests(): TrackedRequest[] { - return Array.from(this.requests.values()).filter( - (r) => r.status === 'pending' || r.status === 'executing', - ) - } - - getStats(): RequestStats { - const all = Array.from(this.requests.values()) - const inFlight = all.filter( - (r) => r.status === 'pending' || r.status === 'executing', - ).length - - const completed = all.filter( - (r): r is typeof r & { duration: number } => r.duration !== undefined, - ) - const avgDuration = - completed.length > 0 - ? completed.reduce((sum, r) => sum + r.duration, 0) / completed.length - : 0 - - const failed = all.filter((r) => r.status === 'failed').length - const errorRate = all.length > 0 ? failed / all.length : 0 - - return { - inFlight, - avgDuration: Math.round(avgDuration), - errorRate: Math.round(errorRate * 100) / 100, - totalRequests: all.length, - } - } - - getHungRequests(timeoutMs = 30000): TrackedRequest[] { - const now = Date.now() - return Array.from(this.requests.values()).filter( - (r) => - (r.status === 'pending' || r.status === 'executing') && - now - r.startTime > timeoutMs, - ) - } - - private cleanup(): void { - // Remove completed/failed requests older than 5 minutes - const now = Date.now() - const fiveMinutesAgo = now - 5 * 60 * 1000 - - for (const [id, req] of this.requests.entries()) { - if ( - (req.status === 'completed' || req.status === 'failed') && - req.startTime < fiveMinutesAgo - ) { - this.requests.delete(id) - } - } - } - - destroy(): void { - if (this.cleanupInterval) { - clearInterval(this.cleanupInterval) - this.cleanupInterval = null - } - this.requests.clear() - } -} diff --git a/packages/browseros-agent/apps/controller-ext/src/utils/RequestValidator.ts b/packages/browseros-agent/apps/controller-ext/src/utils/RequestValidator.ts deleted file mode 100644 index dadb65313..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/utils/RequestValidator.ts +++ /dev/null @@ -1,78 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import type { ProtocolRequest } from '@/protocol/types' -import { ProtocolRequestSchema } from '@/protocol/types' -import { logger } from './logger' - -export class RequestValidator { - private activeIds = new Set() - private idTimestamps = new Map() - private cleanupInterval: ReturnType | null = null - - constructor() { - // Periodically cleanup old IDs (prevent memory leak) - this.cleanupInterval = setInterval(() => this.cleanup(), 60000) // Every 1 minute - } - - validate(message: unknown): ProtocolRequest { - // Step 1: Parse and validate with Zod - const request = ProtocolRequestSchema.parse(message) - - // Step 2: Check for duplicate ID - if (this.activeIds.has(request.id)) { - logger.error(`Duplicate request ID detected: ${request.id}`) - throw new Error( - `Duplicate request ID: ${request.id}. Already processing this request.`, - ) - } - - // Step 3: Track this ID - this.activeIds.add(request.id) - this.idTimestamps.set(request.id, Date.now()) - - logger.debug(`Request validated: ${request.id} [${request.action}]`) - - return request - } - - markComplete(id: string): void { - this.activeIds.delete(id) - this.idTimestamps.delete(id) - logger.debug(`Request ID released: ${id}`) - } - - private cleanup(): void { - // Remove IDs older than 5 minutes (safety measure in case markComplete() not called) - const now = Date.now() - const fiveMinutesAgo = now - 5 * 60 * 1000 - - for (const [id, timestamp] of this.idTimestamps.entries()) { - if (timestamp < fiveMinutesAgo) { - logger.warn( - `Cleaning up stale request ID: ${id} (age: ${Math.round((now - timestamp) / 1000)}s)`, - ) - this.activeIds.delete(id) - this.idTimestamps.delete(id) - } - } - } - - getStats(): { activeIds: number } { - return { - activeIds: this.activeIds.size, - } - } - - destroy(): void { - if (this.cleanupInterval) { - clearInterval(this.cleanupInterval) - this.cleanupInterval = null - } - this.activeIds.clear() - this.idTimestamps.clear() - } -} diff --git a/packages/browseros-agent/apps/controller-ext/src/utils/ResponseQueue.ts b/packages/browseros-agent/apps/controller-ext/src/utils/ResponseQueue.ts deleted file mode 100644 index 86ef3f8aa..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/utils/ResponseQueue.ts +++ /dev/null @@ -1,73 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import type { ProtocolResponse } from '@/protocol/types' -import { logger } from './logger' - -export class ResponseQueue { - private queue: ProtocolResponse[] = [] - private maxSize: number - - constructor(maxSize = 1000) { - this.maxSize = maxSize - logger.info(`ResponseQueue initialized: maxSize=${maxSize}`) - } - - enqueue(response: ProtocolResponse): void { - if (this.queue.length >= this.maxSize) { - // Drop oldest response to prevent memory leak - const dropped = this.queue.shift() - logger.warn( - `Response queue full. Dropped oldest response: ${dropped?.id}`, - ) - } - - this.queue.push(response) - logger.debug( - `Response queued: ${response.id} (queue size: ${this.queue.length})`, - ) - } - - flush(send: (response: ProtocolResponse) => void): number { - let sent = 0 - - logger.info(`Flushing ${this.queue.length} queued responses...`) - - while (this.queue.length > 0) { - const response = this.queue.shift() - if (!response) break - - try { - send(response) - sent++ - } catch (error) { - // Re-queue if send fails - logger.error( - `Failed to send response ${response.id}: ${error}. Re-queueing.`, - ) - this.queue.unshift(response) - break - } - } - - logger.info(`Flushed ${sent} responses. ${this.queue.length} remaining.`) - return sent - } - - size(): number { - return this.queue.length - } - - clear(): void { - const count = this.queue.length - this.queue = [] - logger.warn(`Response queue cleared. Dropped ${count} responses.`) - } - - isEmpty(): boolean { - return this.queue.length === 0 - } -} diff --git a/packages/browseros-agent/apps/controller-ext/src/utils/SnapshotCache.ts b/packages/browseros-agent/apps/controller-ext/src/utils/SnapshotCache.ts deleted file mode 100644 index a4b924fec..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/utils/SnapshotCache.ts +++ /dev/null @@ -1,99 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import type { - InteractiveNode, - InteractiveSnapshot, - Rect, -} from '@/adapters/BrowserOSAdapter' -import { logger } from '@/utils/logger' - -/** - * SnapshotCache - Caches interactive snapshots per tabId for multi-agent support - * - * Used to lookup element coordinates from nodeId without re-fetching snapshot. - * This enables showing mouse pointer before click/type actions with minimal latency. - */ -class SnapshotCacheImpl { - private cache: Map = new Map() - private nodeRectCache: Map> = new Map() - - /** - * Cache a snapshot for a tab - */ - set(tabId: number, snapshot: InteractiveSnapshot): void { - this.cache.set(tabId, snapshot) - - const rectMap = new Map() - for (const element of snapshot.elements) { - if (element.rect) { - rectMap.set(element.nodeId, element.rect) - } - } - this.nodeRectCache.set(tabId, rectMap) - - logger.debug( - `[SnapshotCache] Cached snapshot for tab ${tabId} with ${snapshot.elements.length} elements`, - ) - } - - /** - * Get cached snapshot for a tab - */ - get(tabId: number): InteractiveSnapshot | undefined { - return this.cache.get(tabId) - } - - /** - * Get element rect by nodeId from cache - */ - getNodeRect(tabId: number, nodeId: number): Rect | undefined { - const rectMap = this.nodeRectCache.get(tabId) - if (!rectMap) { - logger.debug(`[SnapshotCache] No cached snapshot for tab ${tabId}`) - return undefined - } - return rectMap.get(nodeId) - } - - /** - * Get element by nodeId from cache - */ - getElement(tabId: number, nodeId: number): InteractiveNode | undefined { - const snapshot = this.cache.get(tabId) - if (!snapshot) { - return undefined - } - return snapshot.elements.find((el) => el.nodeId === nodeId) - } - - /** - * Clear cache for a specific tab - */ - clear(tabId: number): void { - this.cache.delete(tabId) - this.nodeRectCache.delete(tabId) - logger.debug(`[SnapshotCache] Cleared cache for tab ${tabId}`) - } - - /** - * Clear all caches - */ - clearAll(): void { - this.cache.clear() - this.nodeRectCache.clear() - logger.debug('[SnapshotCache] Cleared all caches') - } - - /** - * Check if snapshot is cached for a tab - */ - has(tabId: number): boolean { - return this.cache.has(tabId) - } -} - -export const SnapshotCache = new SnapshotCacheImpl() diff --git a/packages/browseros-agent/apps/controller-ext/src/utils/logger.ts b/packages/browseros-agent/apps/controller-ext/src/utils/logger.ts deleted file mode 100644 index cd37d31e2..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/utils/logger.ts +++ /dev/null @@ -1,58 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -import type { LoggerInterface, LogLevel } from '@browseros/shared/types/logger' -import { LOGGING_CONFIG } from '@/config/constants' - -const LEVEL_PRIORITY: Record = { - debug: 0, - info: 1, - warn: 2, - error: 3, -} - -export class Logger implements LoggerInterface { - private prefix: string - - constructor(prefix: string = LOGGING_CONFIG.prefix) { - this.prefix = prefix - } - - private shouldLog(level: LogLevel): boolean { - if (!LOGGING_CONFIG.enabled) return false - return LEVEL_PRIORITY[level] >= LEVEL_PRIORITY[LOGGING_CONFIG.level] - } - - private formatMessage(message: string): string { - const timestamp = new Date().toISOString() - return `${this.prefix} [${timestamp}] ${message}` - } - - private formatData(data?: Record): string { - return data ? `\n${JSON.stringify(data, null, 2)}` : '' - } - - debug(message: string, data?: Record): void { - if (!this.shouldLog('debug')) return - console.log(this.formatMessage(message) + this.formatData(data)) - } - - info(message: string, data?: Record): void { - if (!this.shouldLog('info')) return - console.info(this.formatMessage(message) + this.formatData(data)) - } - - warn(message: string, data?: Record): void { - if (!this.shouldLog('warn')) return - console.warn(this.formatMessage(message) + this.formatData(data)) - } - - error(message: string, data?: Record): void { - if (!this.shouldLog('error')) return - console.error(this.formatMessage(message) + this.formatData(data)) - } -} - -export const logger = new Logger() diff --git a/packages/browseros-agent/apps/controller-ext/src/utils/timeout.ts b/packages/browseros-agent/apps/controller-ext/src/utils/timeout.ts deleted file mode 100644 index 4fbe74458..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/utils/timeout.ts +++ /dev/null @@ -1,68 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -/** - * Timeout configuration for Chrome API and BrowserOS operations. - * These prevent hung APIs from blocking the mutex indefinitely. - */ -export const CHROME_API_TIMEOUTS = { - /** Quick Chrome API calls (tabs, bookmarks, history queries) */ - CHROME_API: 15_000, - - /** BrowserOS page actions (click, scroll, input, etc.) */ - BROWSEROS_ACTION: 10_000, - - /** Heavy BrowserOS operations (screenshot, snapshot, accessibility tree) */ - BROWSEROS_HEAVY: 60_000, -} as const - -/** - * Error thrown when a Chrome API call times out. - */ -export class ChromeAPITimeoutError extends Error { - constructor( - public readonly operation: string, - public readonly timeoutMs: number, - ) { - super(`Chrome API '${operation}' timed out after ${timeoutMs}ms`) - this.name = 'ChromeAPITimeoutError' - } -} - -/** - * Wraps a promise with a timeout. If the promise doesn't resolve within - * the specified time, it rejects with a ChromeAPITimeoutError. - * - * IMPORTANT: This doesn't cancel the underlying Chrome API call - it just - * stops waiting for it. The API call may still complete in the background. - * - * @param promise - The promise to wrap - * @param timeoutMs - Timeout in milliseconds - * @param operation - Name of the operation (for error messages) - * @returns The result of the promise if it resolves in time - * @throws ChromeAPITimeoutError if the timeout is exceeded - */ -export function withTimeout( - promise: Promise, - timeoutMs: number, - operation: string, -): Promise { - return new Promise((resolve, reject) => { - const timer = setTimeout(() => { - reject(new ChromeAPITimeoutError(operation, timeoutMs)) - }, timeoutMs) - - promise - .then((result) => { - clearTimeout(timer) - resolve(result) - }) - .catch((error) => { - clearTimeout(timer) - reject(error) - }) - }) -} diff --git a/packages/browseros-agent/apps/controller-ext/src/utils/versionUtils.ts b/packages/browseros-agent/apps/controller-ext/src/utils/versionUtils.ts deleted file mode 100644 index 077cf88bf..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/utils/versionUtils.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -// Parse "137.0.7207.69" → [137, 0, 7207, 69] -function parseVersion(version: string): number[] { - return version.split('.').map((n) => parseInt(n, 10) || 0) -} - -// Compare if versionA >= versionB -export function isVersionAtLeast(current: string, required: string): boolean { - const currentParts = parseVersion(current) - const requiredParts = parseVersion(required) - - for ( - let i = 0; - i < Math.max(currentParts.length, requiredParts.length); - i++ - ) { - const curr = currentParts[i] || 0 - const req = requiredParts[i] || 0 - - if (curr > req) return true - if (curr < req) return false - } - return true // Equal versions -} diff --git a/packages/browseros-agent/apps/controller-ext/src/websocket/WebSocketClient.ts b/packages/browseros-agent/apps/controller-ext/src/websocket/WebSocketClient.ts deleted file mode 100644 index cb56a86af..000000000 --- a/packages/browseros-agent/apps/controller-ext/src/websocket/WebSocketClient.ts +++ /dev/null @@ -1,297 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -import { WEBSOCKET_CONFIG } from '@/config/constants' -import type { ProtocolRequest, ProtocolResponse } from '@/protocol/types' -import { ConnectionStatus } from '@/protocol/types' -import { logger } from '@/utils/logger' - -export type PortProvider = () => Promise - -export class WebSocketClient { - private ws: WebSocket | null = null - private status: ConnectionStatus = ConnectionStatus.DISCONNECTED - private reconnectTimer: ReturnType | null = null - private heartbeatTimer: ReturnType | null = null - private heartbeatTimeoutTimer: ReturnType | null = null - private getPort: PortProvider - private lastPongReceived: number = Date.now() - private pendingPing = false - - // Event handlers - private messageHandlers = new Set<(msg: ProtocolResponse) => void>() - private statusHandlers = new Set<(status: ConnectionStatus) => void>() - - constructor(getPort: PortProvider) { - this.getPort = getPort - logger.info('WebSocketClient initialized') - } - - // Public API - - async connect(): Promise { - if (this.status === ConnectionStatus.CONNECTED) { - logger.debug('Already connected') - return - } - - this._setStatus(ConnectionStatus.CONNECTING) - - try { - const port = await this.getPort() - const url = this._buildUrl(port) - logger.info(`Connecting to ${url}`) - - this.ws = new WebSocket(url) - - this.ws.onopen = this._handleOpen.bind(this) - this.ws.onmessage = this._handleMessage.bind(this) - this.ws.onerror = this._handleError.bind(this) - this.ws.onclose = this._handleClose.bind(this) - - // Wait for connection with timeout - await this._waitForConnection() - } catch (error) { - logger.error(`Connection failed: ${error}`) - this._handleConnectionFailure() - } - } - - disconnect(): void { - logger.info('Disconnecting...') - this._clearTimers() - - if (this.ws) { - this.ws.close() - this.ws = null - } - - this._setStatus(ConnectionStatus.DISCONNECTED) - } - - send( - message: ProtocolRequest | ProtocolResponse | Record, - ): void { - this._sendSerialized(message) - } - - onMessage(handler: (msg: ProtocolResponse) => void): void { - this.messageHandlers.add(handler) - } - - onStatusChange(handler: (status: ConnectionStatus) => void): void { - this.statusHandlers.add(handler) - } - - isConnected(): boolean { - return this.status === ConnectionStatus.CONNECTED - } - - getStatus(): ConnectionStatus { - return this.status - } - - // Private methods - - private _buildUrl(port: number): string { - const { protocol, host, path } = WEBSOCKET_CONFIG - return `${protocol}://${host}:${port}${path}` - } - - private async _waitForConnection(): Promise { - return new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - reject(new Error('Connection timeout')) - }, WEBSOCKET_CONFIG.connectionTimeout) - - const checkConnection = () => { - if (this.status === ConnectionStatus.CONNECTED) { - clearTimeout(timeout) - resolve() - } else if (this.status === ConnectionStatus.ERROR) { - clearTimeout(timeout) - reject(new Error('Connection failed')) - } else { - setTimeout(checkConnection, 100) - } - } - - checkConnection() - }) - } - - private _handleOpen(): void { - logger.info('WebSocket connected') - this.lastPongReceived = Date.now() - this.pendingPing = false - this._setStatus(ConnectionStatus.CONNECTED) - this._startHeartbeat() - } - - private _handleMessage(event: MessageEvent): void { - try { - const message = JSON.parse(event.data) - - // Handle pong response for heartbeat - if (message.type === 'pong') { - this.lastPongReceived = Date.now() - this.pendingPing = false - logger.debug('Received pong from server') - return - } - - logger.debug(`Received: ${JSON.stringify(message).substring(0, 100)}...`) - - // Emit to all message handlers - for (const handler of this.messageHandlers) { - handler(message as ProtocolResponse) - } - } catch (error) { - logger.error(`Failed to parse message: ${error}`) - } - } - - private _handleError(event: Event): void { - logger.error(`WebSocket error: ${event}`) - this._setStatus(ConnectionStatus.ERROR) - } - - private _handleClose(event: CloseEvent): void { - logger.warn(`WebSocket closed: code=${event.code}, reason=${event.reason}`) - this._clearTimers() - this.ws = null - - // Only reconnect if we're not deliberately disconnecting - if (this.status !== ConnectionStatus.DISCONNECTED) { - this._reconnect() - } - } - - private _handleConnectionFailure(): void { - this._setStatus(ConnectionStatus.ERROR) - this._reconnect() - } - - private _reconnect(): void { - if (this.reconnectTimer) { - return // Already reconnecting - } - - this._setStatus(ConnectionStatus.RECONNECTING) - - const delay = WEBSOCKET_CONFIG.reconnectIntervalMs - logger.warn(`Reconnecting in ${Math.round(delay)}ms`) - - this.reconnectTimer = setTimeout(() => { - this.reconnectTimer = null - this.connect().catch((err) => { - logger.error(`Reconnection failed: ${err}`) - }) - }, delay) - } - - private _startHeartbeat(): void { - this._clearHeartbeat() - - this.heartbeatTimer = setInterval(() => { - if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { - return - } - - // Check if previous ping timed out - const timeSinceLastPong = Date.now() - this.lastPongReceived - if ( - timeSinceLastPong > - WEBSOCKET_CONFIG.heartbeatInterval + WEBSOCKET_CONFIG.heartbeatTimeout - ) { - logger.error( - `Heartbeat timeout: no pong received for ${timeSinceLastPong}ms`, - ) - this._handleHeartbeatTimeout() - return - } - - // Send ping - try { - this._sendSerialized({ type: 'ping' }) - this.pendingPing = true - logger.debug('Sent heartbeat ping') - - // Set timeout for this specific ping - this._clearHeartbeatTimeout() - this.heartbeatTimeoutTimer = setTimeout(() => { - if (this.pendingPing) { - logger.error( - `Ping timeout: no pong received within ${WEBSOCKET_CONFIG.heartbeatTimeout}ms`, - ) - this._handleHeartbeatTimeout() - } - }, WEBSOCKET_CONFIG.heartbeatTimeout) - } catch (error) { - logger.error(`Failed to send ping: ${error}`) - this._handleHeartbeatTimeout() - } - }, WEBSOCKET_CONFIG.heartbeatInterval) - } - - private _handleHeartbeatTimeout(): void { - logger.warn('Heartbeat failed, forcing reconnection') - if (this.ws) { - this.ws.close() - } - } - - private _clearHeartbeat(): void { - if (this.heartbeatTimer) { - clearInterval(this.heartbeatTimer) - this.heartbeatTimer = null - } - this._clearHeartbeatTimeout() - } - - private _clearHeartbeatTimeout(): void { - if (this.heartbeatTimeoutTimer) { - clearTimeout(this.heartbeatTimeoutTimer) - this.heartbeatTimeoutTimer = null - } - } - - private _clearTimers(): void { - this._clearHeartbeat() - - if (this.reconnectTimer) { - clearTimeout(this.reconnectTimer) - this.reconnectTimer = null - } - } - - private _setStatus(status: ConnectionStatus): void { - if (this.status === status) return - - this.status = status - logger.info(`Status changed: ${status}`) - - // Emit to all status handlers - for (const handler of this.statusHandlers) { - handler(status) - } - } - - private _sendSerialized( - message: ProtocolRequest | ProtocolResponse | Record, - ): void { - if (this.status !== ConnectionStatus.CONNECTED) { - throw new Error('WebSocket not connected') - } - - if (!this.ws) { - throw new Error('WebSocket instance is null') - } - - const messageStr = JSON.stringify(message) - logger.debug(`Sending: ${messageStr.substring(0, 100)}...`) - this.ws.send(messageStr) - } -} diff --git a/packages/browseros-agent/apps/controller-ext/tests/payloads.json b/packages/browseros-agent/apps/controller-ext/tests/payloads.json deleted file mode 100644 index f7b8573c2..000000000 --- a/packages/browseros-agent/apps/controller-ext/tests/payloads.json +++ /dev/null @@ -1,257 +0,0 @@ -{ - "tests": [ - { - "name": "Check BrowserOS Availability", - "action": "checkBrowserOS", - "payload": {}, - "description": "Verify chrome.browserOS is available and list all APIs" - }, - { - "name": "Get Active Tab", - "action": "getActiveTab", - "payload": {}, - "description": "Get the currently active tab information" - }, - { - "name": "Get All Tabs", - "action": "getTabs", - "payload": {}, - "description": "Get all open tabs in the browser" - }, - { - "name": "Get Page Load Status", - "action": "getPageLoadStatus", - "payload": { - "tabId": "{{activeTabId}}" - }, - "description": "Check if the active tab has finished loading" - }, - { - "name": "Capture Screenshot", - "action": "captureScreenshot", - "payload": { - "tabId": "{{activeTabId}}", - "size": "small" - }, - "description": "Capture a screenshot of the active tab" - }, - { - "name": "Get Interactive Snapshot", - "action": "getInteractiveSnapshot", - "payload": { - "tabId": "{{activeTabId}}" - }, - "description": "Get interactive elements snapshot from active tab" - }, - { - "name": "Execute JavaScript - Get Title", - "action": "executeJavaScript", - "payload": { - "tabId": "{{activeTabId}}", - "code": "document.title" - }, - "description": "Execute JavaScript to get page title" - }, - { - "name": "Execute JavaScript - Get URL", - "action": "executeJavaScript", - "payload": { - "tabId": "{{activeTabId}}", - "code": "window.location.href" - }, - "description": "Execute JavaScript to get current URL" - }, - { - "name": "Scroll Down", - "action": "scrollDown", - "payload": { - "tabId": "{{activeTabId}}" - }, - "description": "Scroll the active tab down by one viewport" - }, - { - "name": "Scroll Up", - "action": "scrollUp", - "payload": { - "tabId": "{{activeTabId}}" - }, - "description": "Scroll the active tab up by one viewport" - }, - { - "name": "Get Snapshot (Text)", - "action": "getSnapshot", - "payload": { - "tabId": "{{activeTabId}}", - "type": "text", - "options": { - "context": "visible" - } - }, - "description": "Get text snapshot of the active tab" - }, - { - "name": "Open New Tab", - "action": "openTab", - "payload": { - "url": "https://www.example.com", - "active": true - }, - "description": "Open a new tab with example.com" - }, - { - "name": "Navigate Current Tab", - "action": "navigate", - "payload": { - "url": "https://www.wikipedia.org" - }, - "description": "Navigate the active tab to Wikipedia" - }, - { - "name": "Switch Tab", - "action": "switchTab", - "payload": { - "tabId": "{{activeTabId}}" - }, - "description": "Switch to the active tab (should already be active)" - }, - { - "name": "Get Recent Bookmarks", - "action": "getBookmarks", - "payload": { - "recent": true, - "limit": 10 - }, - "description": "Get 10 most recent bookmarks" - }, - { - "name": "Search Bookmarks", - "action": "getBookmarks", - "payload": { - "query": "google", - "limit": 5 - }, - "description": "Search for bookmarks containing 'google'" - }, - { - "name": "Get Recent History", - "action": "getRecentHistory", - "payload": { - "maxResults": 10, - "hoursBack": 24 - }, - "description": "Get last 10 history items from past 24 hours" - }, - { - "name": "Search History", - "action": "searchHistory", - "payload": { - "query": "github", - "maxResults": 5 - }, - "description": "Search history for 'github'" - } - ], - "tabManagementTests": [ - { - "name": "Close Tab (Manual - requires tab ID)", - "action": "closeTab", - "payload": { - "tabId": "MANUAL_INPUT_REQUIRED" - }, - "description": "Close a specific tab by ID (get ID from getTabs first)" - } - ], - "bookmarkTests": [ - { - "name": "Create Bookmark (Manual)", - "action": "createBookmark", - "payload": { - "title": "Example Site", - "url": "https://www.example.com" - }, - "description": "Create a bookmark for example.com" - }, - { - "name": "Remove Bookmark (Manual - requires bookmark ID)", - "action": "removeBookmark", - "payload": { - "id": "MANUAL_INPUT_REQUIRED" - }, - "description": "Remove a bookmark by ID (get ID from getBookmarks first)" - } - ], - "manualTests": [ - { - "name": "Click Element (Requires nodeId)", - "action": "click", - "payload": { - "tabId": "{{activeTabId}}", - "nodeId": "MANUAL_INPUT_REQUIRED" - }, - "description": "Click an element by nodeId (get nodeId from getInteractiveSnapshot first)" - }, - { - "name": "Input Text (Requires nodeId)", - "action": "inputText", - "payload": { - "tabId": "{{activeTabId}}", - "nodeId": "MANUAL_INPUT_REQUIRED", - "text": "Test input" - }, - "description": "Type text into an input element" - }, - { - "name": "Clear Input (Requires nodeId)", - "action": "clear", - "payload": { - "tabId": "{{activeTabId}}", - "nodeId": "MANUAL_INPUT_REQUIRED" - }, - "description": "Clear an input element" - }, - { - "name": "Scroll to Node (Requires nodeId)", - "action": "scrollToNode", - "payload": { - "tabId": "{{activeTabId}}", - "nodeId": "MANUAL_INPUT_REQUIRED" - }, - "description": "Scroll to a specific element" - }, - { - "name": "Click at Coordinates", - "action": "clickCoordinates", - "payload": { - "tabId": "{{activeTabId}}", - "x": 100, - "y": 100 - }, - "description": "Click at specific screen coordinates" - }, - { - "name": "Type at Coordinates", - "action": "typeAtCoordinates", - "payload": { - "tabId": "{{activeTabId}}", - "x": 100, - "y": 100, - "text": "Test text" - }, - "description": "Type at specific screen coordinates" - }, - { - "name": "Send Keys", - "action": "sendKeys", - "payload": { - "tabId": "{{activeTabId}}", - "keys": "Hello" - }, - "description": "Send keyboard keys to the active tab" - } - ], - "_notes": { - "activeTabId": "This placeholder will be replaced with the actual active tab ID at runtime", - "tests": "These tests can run automatically without manual input", - "manualTests": "These tests require manual nodeId input or specific coordinates" - } -} diff --git a/packages/browseros-agent/apps/controller-ext/tsconfig.json b/packages/browseros-agent/apps/controller-ext/tsconfig.json deleted file mode 100644 index 069561b54..000000000 --- a/packages/browseros-agent/apps/controller-ext/tsconfig.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "module": "ES2020", - "moduleResolution": "bundler", - "lib": ["ES2020", "DOM"], - "outDir": "./dist", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "resolveJsonModule": true, - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "types": ["chrome", "node"], - "baseUrl": ".", - "paths": { - "@/*": ["src/*"] - } - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] -} diff --git a/packages/browseros-agent/apps/controller-ext/webpack.config.js b/packages/browseros-agent/apps/controller-ext/webpack.config.js deleted file mode 100644 index 56ba4d88d..000000000 --- a/packages/browseros-agent/apps/controller-ext/webpack.config.js +++ /dev/null @@ -1,83 +0,0 @@ -const path = require('node:path') -const webpack = require('webpack') -const TerserPlugin = require('terser-webpack-plugin') -const CopyPlugin = require('copy-webpack-plugin') - -module.exports = (_env, argv) => { - const isProduction = argv.mode === 'production' - - return { - mode: isProduction ? 'production' : 'development', - entry: { - background: './src/background/index.ts', - }, - output: { - path: path.resolve(__dirname, 'dist'), - filename: '[name].js', - clean: true, - }, - resolve: { - extensions: ['.ts', '.js'], - alias: { - '@': path.resolve(__dirname, 'src'), - }, - }, - module: { - rules: [ - { - test: /\.ts$/, - use: { - loader: 'ts-loader', - options: { - onlyCompileBundledFiles: true, - compilerOptions: { - declaration: false, - declarationMap: false, - }, - }, - }, - exclude: [/node_modules/, /\.(test|spec)\.(ts|tsx)$/], - }, - ], - }, - plugins: [ - new webpack.optimize.LimitChunkCountPlugin({ - maxChunks: 1, - }), - new CopyPlugin({ - patterns: [ - { from: 'manifest.json', to: '.' }, - { from: 'assets', to: 'assets' }, - ], - }), - ], - devtool: isProduction ? false : 'source-map', - optimization: { - splitChunks: false, - runtimeChunk: false, - minimize: isProduction, - minimizer: isProduction - ? [ - new TerserPlugin({ - extractComments: false, - terserOptions: { - format: { - comments: false, - }, - compress: { - // FIXME: nikhil - remove this later after few releases - drop_console: false, - drop_debugger: true, - }, - }, - }), - ] - : [], - }, - performance: { - hints: isProduction ? 'warning' : false, - maxEntrypointSize: 512000, - maxAssetSize: 512000, - }, - } -} diff --git a/packages/browseros-agent/apps/eval/README.md b/packages/browseros-agent/apps/eval/README.md index 1b3b86578..ee20bbe6c 100644 --- a/packages/browseros-agent/apps/eval/README.md +++ b/packages/browseros-agent/apps/eval/README.md @@ -179,7 +179,7 @@ The `apiKey` field supports two formats: } ``` -Each worker gets its own Chrome instance. Worker N uses `base_port + N` for CDP, server, and extension ports. +Each worker gets its own Chrome instance. Worker N uses `base_port + N` for CDP and server ports. `base_extension_port` is still reserved as a legacy BrowserOS launch argument for compatibility with Chromium builds that still pass it. ### Execution settings diff --git a/packages/browseros-agent/apps/eval/scripts/debug-long-run.ts b/packages/browseros-agent/apps/eval/scripts/debug-long-run.ts index 115c8e7bf..afecbbd75 100644 --- a/packages/browseros-agent/apps/eval/scripts/debug-long-run.ts +++ b/packages/browseros-agent/apps/eval/scripts/debug-long-run.ts @@ -13,14 +13,14 @@ const MCP_URL = `${SERVER_URL}/mcp` const NUM_TURNS = 60 const SCREENSHOT_EVERY_N_TURNS = 1 -async function checkExtension(): Promise { +async function checkBrowserReady(): Promise { try { - const res = await fetch(`${SERVER_URL}/extension-status`, { + const res = await fetch(`${SERVER_URL}/health`, { signal: AbortSignal.timeout(5000), }) if (!res.ok) return false - const data = (await res.json()) as { extensionConnected?: boolean } - return data.extensionConnected === true + const data = (await res.json()) as { cdpConnected?: boolean } + return data.cdpConnected === true } catch { return false } @@ -156,7 +156,7 @@ async function main() { let screenshotFail = 0 let toolSuccess = 0 let toolFail = 0 - let extensionDisconnects = 0 + let browserDisconnects = 0 const startTime = Date.now() @@ -201,18 +201,18 @@ async function main() { } } - // Check extension status - const extConnected = await checkExtension() - if (!extConnected) { - extensionDisconnects++ - console.log(` Turn ${turn}: ⚠️ Extension disconnected!`) + // Check browser status + const browserReady = await checkBrowserReady() + if (!browserReady) { + browserDisconnects++ + console.log(` Turn ${turn}: ⚠️ Browser became unavailable!`) } // Progress if (turn % 10 === 0) { const elapsed = ((Date.now() - startTime) / 1000).toFixed(1) console.log( - `Turn ${turn}/${NUM_TURNS} - Screenshots: ${screenshotSuccess}/${turn}, Tools: ${toolSuccess}/${turn}, Disconnects: ${extensionDisconnects}, Elapsed: ${elapsed}s`, + `Turn ${turn}/${NUM_TURNS} - Screenshots: ${screenshotSuccess}/${turn}, Tools: ${toolSuccess}/${turn}, Disconnects: ${browserDisconnects}, Elapsed: ${elapsed}s`, ) } @@ -237,9 +237,9 @@ async function main() { console.log( `Tool calls: ${toolSuccess}/${NUM_TURNS} (${((toolSuccess / NUM_TURNS) * 100).toFixed(1)}%)`, ) - console.log(`Extension disconnects: ${extensionDisconnects}`) + console.log(`Browser disconnects: ${browserDisconnects}`) - if (screenshotFail > 0 || toolFail > 0 || extensionDisconnects > 0) { + if (screenshotFail > 0 || toolFail > 0 || browserDisconnects > 0) { console.log('\n⚠️ Issues detected during long run!') } else { console.log('\n✅ All operations completed successfully!') diff --git a/packages/browseros-agent/apps/eval/scripts/debug-mcp.ts b/packages/browseros-agent/apps/eval/scripts/debug-mcp.ts index fbf12e492..b8fe59a5f 100644 --- a/packages/browseros-agent/apps/eval/scripts/debug-mcp.ts +++ b/packages/browseros-agent/apps/eval/scripts/debug-mcp.ts @@ -29,14 +29,14 @@ async function checkHealth(): Promise { } } -async function checkExtension(): Promise { +async function checkBrowserReady(): Promise { try { - const res = await fetch(`${SERVER_URL}/extension-status`, { + const res = await fetch(`${SERVER_URL}/health`, { signal: AbortSignal.timeout(5000), }) if (!res.ok) return false - const data = (await res.json()) as { extensionConnected?: boolean } - return data.extensionConnected === true + const data = (await res.json()) as { cdpConnected?: boolean } + return data.cdpConnected === true } catch { return false } @@ -124,9 +124,9 @@ async function main() { if (!healthy) throw new Error('Server not healthy') }) - await runTest('Extension status', async () => { - const connected = await checkExtension() - if (!connected) throw new Error('Extension not connected') + await runTest('Browser status', async () => { + const connected = await checkBrowserReady() + if (!connected) throw new Error('Browser not ready') }) // Phase 2: List tools @@ -208,10 +208,10 @@ async function main() { console.log(` Screenshot ${i}: ❌ ${res.error} (${res.duration}ms)`) } - // Check extension status between screenshots - const extConnected = await checkExtension() + // Check browser status between screenshots + const extConnected = await checkBrowserReady() if (!extConnected) { - console.log(` ⚠️ Extension disconnected after screenshot ${i}!`) + console.log(` ⚠️ Browser became unavailable after screenshot ${i}!`) } // Small delay between screenshots @@ -270,10 +270,10 @@ async function main() { }) } - // Final extension check - await runTest('Final extension status', async () => { - const connected = await checkExtension() - if (!connected) throw new Error('Extension not connected') + // Final browser readiness check + await runTest('Final browser status', async () => { + const connected = await checkBrowserReady() + if (!connected) throw new Error('Browser not ready') }) // Summary diff --git a/packages/browseros-agent/apps/eval/scripts/test-failure-scenarios.ts b/packages/browseros-agent/apps/eval/scripts/test-failure-scenarios.ts index b011bf886..7a5c0032a 100644 --- a/packages/browseros-agent/apps/eval/scripts/test-failure-scenarios.ts +++ b/packages/browseros-agent/apps/eval/scripts/test-failure-scenarios.ts @@ -14,7 +14,6 @@ import { type Subprocess, spawn, spawnSync } from 'bun' const EVAL_PORTS = { cdp: 9005, server: 9105, // http_mcp in config.dev.json - extension: 9305, } as const const MONOREPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), '../../..') @@ -61,7 +60,7 @@ async function killBrowserOSApp(): Promise { async function launchBrowserOSApp(): Promise { log( 'BROWSEROS', - `Launching BrowserOS (server disabled, CDP=${EVAL_PORTS.cdp}, Extension=${EVAL_PORTS.extension})...`, + `Launching BrowserOS (server disabled, CDP=${EVAL_PORTS.cdp})...`, ) spawnSync({ cmd: [ @@ -71,7 +70,6 @@ async function launchBrowserOSApp(): Promise { '--args', '--disable-browseros-server', `--browseros-cdp-port=${EVAL_PORTS.cdp}`, - `--browseros-extension-port=${EVAL_PORTS.extension}`, ], }) for (let i = 0; i < 30; i++) { @@ -123,19 +121,19 @@ async function waitForServerHealth( return false } -async function waitForExtension( +async function waitForBrowserReady( port: number, maxAttempts = 60, ): Promise { let connectedCount = 0 for (let i = 0; i < maxAttempts; i++) { try { - const res = await fetch(`http://127.0.0.1:${port}/extension-status`, { + const res = await fetch(`http://127.0.0.1:${port}/health`, { signal: AbortSignal.timeout(2000), }) if (res.ok) { - const data = (await res.json()) as { extensionConnected?: boolean } - if (data.extensionConnected) { + const data = (await res.json()) as { cdpConnected?: boolean } + if (data.cdpConnected) { connectedCount++ if (connectedCount >= 3) return true } else { @@ -150,14 +148,14 @@ async function waitForExtension( return false } -async function checkExtensionConnected(port: number): Promise { +async function checkBrowserReady(port: number): Promise { try { - const res = await fetch(`http://127.0.0.1:${port}/extension-status`, { + const res = await fetch(`http://127.0.0.1:${port}/health`, { signal: AbortSignal.timeout(3000), }) if (res.ok) { - const data = (await res.json()) as { extensionConnected?: boolean } - return data.extensionConnected === true + const data = (await res.json()) as { cdpConnected?: boolean } + return data.cdpConnected === true } } catch { /* failed */ @@ -174,9 +172,7 @@ let serverProc: Subprocess | null = null async function startServer(): Promise { log('SERVER', 'Cleaning up ports...') killPort(EVAL_PORTS.server) - killPort(EVAL_PORTS.extension) await waitForPortFree(EVAL_PORTS.server, 30) - await waitForPortFree(EVAL_PORTS.extension, 30) log('SERVER', 'Starting server process...') const proc = spawn({ @@ -185,8 +181,6 @@ async function startServer(): Promise { 'apps/server/src/index.ts', '--server-port', String(EVAL_PORTS.server), - '--extension-port', - String(EVAL_PORTS.extension), '--cdp-port', String(EVAL_PORTS.cdp), ], @@ -250,9 +244,9 @@ async function scenario1_AppNotRunningAtStart(): Promise { log('RESULT', 'SUCCESS - App is now running, can proceed with server start') } -async function scenario2_ExtensionNotConnecting(): Promise { +async function scenario2_BrowserNotReady(): Promise { console.log(`\n${'='.repeat(70)}`) - console.log('SCENARIO 2: Extension Does Not Connect Within 30 Seconds') + console.log('SCENARIO 2: Browser Does Not Become Ready Within 30 Seconds') console.log('='.repeat(70)) console.log( 'Expected: Wait 30s → Restart BrowserOS app → Retry → Success or fail after 3 attempts\n', @@ -280,11 +274,11 @@ async function scenario2_ExtensionNotConnecting(): Promise { } log('HEALTH', 'Server health OK') - log('WAIT', 'Waiting for extension to connect (30s timeout)...') - const extConnected = await waitForExtension(EVAL_PORTS.server, 60) // 60 * 500ms = 30s + log('WAIT', 'Waiting for browser readiness (30s timeout)...') + const browserReady = await waitForBrowserReady(EVAL_PORTS.server, 60) - if (!extConnected) { - log('TIMEOUT', 'Extension did not connect within 30 seconds') + if (!browserReady) { + log('TIMEOUT', 'Browser did not become ready within 30 seconds') await stopServer(proc) if (!browserOSRestartAttempted) { @@ -302,10 +296,10 @@ async function scenario2_ExtensionNotConnecting(): Promise { } } - throw new Error('Extension did not connect') + throw new Error('Browser did not become ready') } - log('CONNECTED', 'Extension connected!') + log('CONNECTED', 'Browser ready!') await stopServer(proc) log('RESULT', 'SUCCESS - Would proceed with task execution') return @@ -342,14 +336,14 @@ async function scenario3_ServerCrashesMidTask(): Promise { return } - const extConnected = await waitForExtension(EVAL_PORTS.server, 60) - if (!extConnected) { - log('SETUP', 'Extension failed to connect') + const browserReady = await waitForBrowserReady(EVAL_PORTS.server, 60) + if (!browserReady) { + log('SETUP', 'Browser did not become ready') await stopServer(proc) return } - log('READY', 'Server and extension ready') + log('READY', 'Server and browser ready') log('SIMULATE', 'Simulating server crash by killing the process...') // Kill the server to simulate crash @@ -361,9 +355,9 @@ async function scenario3_ServerCrashesMidTask(): Promise { const stillHealthy = await waitForServerHealth(EVAL_PORTS.server, 5) log('CHECK', `Server health: ${stillHealthy ? 'OK' : 'FAILED'}`) - log('CHECK', 'Checking extension status...') - const stillConnected = await checkExtensionConnected(EVAL_PORTS.server) - log('CHECK', `Extension connected: ${stillConnected}`) + log('CHECK', 'Checking browser readiness...') + const stillConnected = await checkBrowserReady(EVAL_PORTS.server) + log('CHECK', `Browser ready: ${stillConnected}`) if (!stillHealthy || !stillConnected) { log('DETECTED', '→ Infrastructure failure detected!') @@ -373,7 +367,6 @@ async function scenario3_ServerCrashesMidTask(): Promise { ) killPort(EVAL_PORTS.server) - killPort(EVAL_PORTS.extension) log('CLEANUP', 'Ports cleaned') log('RESULT', 'Task would FAIL, but next task gets clean environment') @@ -394,7 +387,6 @@ async function scenario4_ToolTimeout(): Promise { log('ERROR', `Received error: "${errorMessage}"`) const isInfraError = - errorMessage.includes('Extension') || errorMessage.includes('BrowserOS') || errorMessage.includes('server') || errorMessage.includes('not connected') || @@ -415,9 +407,9 @@ async function scenario4_ToolTimeout(): Promise { } } -async function scenario5_ExtensionDisconnectsMidTask(): Promise { +async function scenario5_BrowserUnavailableMidTask(): Promise { console.log(`\n${'='.repeat(70)}`) - console.log('SCENARIO 5: Extension Disconnects Mid-Task (App Crashes)') + console.log('SCENARIO 5: Browser Becomes Unavailable Mid-Task (App Crashes)') console.log('='.repeat(70)) console.log( 'Expected: Tool call fails → "not connected" error → Kill app → Restart for next task\n', @@ -432,20 +424,20 @@ async function scenario5_ExtensionDisconnectsMidTask(): Promise { log('WAIT', 'Waiting for server to be ready...') await waitForServerHealth(EVAL_PORTS.server, 30) - await waitForExtension(EVAL_PORTS.server, 60) - log('READY', 'Server and extension ready') + await waitForBrowserReady(EVAL_PORTS.server, 60) + log('READY', 'Server and browser ready') log('SIMULATE', 'Simulating BrowserOS crash by killing the app...') await killBrowserOSApp() await sleep(2000) - // Check extension status - log('CHECK', 'Checking extension status after app crash...') - const stillConnected = await checkExtensionConnected(EVAL_PORTS.server) - log('CHECK', `Extension connected: ${stillConnected}`) + // Check browser status + log('CHECK', 'Checking browser readiness after app crash...') + const stillConnected = await checkBrowserReady(EVAL_PORTS.server) + log('CHECK', `Browser ready: ${stillConnected}`) if (!stillConnected) { - log('DETECTED', '→ Extension disconnected!') + log('DETECTED', '→ Browser became unavailable!') const errorMessage = 'BrowserOS helper service not connected' log('ERROR', `Tool call would fail with: "${errorMessage}"`) @@ -457,7 +449,6 @@ async function scenario5_ExtensionDisconnectsMidTask(): Promise { log('RECOVERY', '→ Cleaning up for next task...') await stopServer(proc) killPort(EVAL_PORTS.server) - killPort(EVAL_PORTS.extension) log('RECOVERY', '→ Next task would check if BrowserOS is running...') const appRunning = isBrowserOSAppRunning() @@ -517,12 +508,12 @@ async function scenario7_ConsecutiveFailures(): Promise { // Simulate infrastructure check before task log('FLOW', '→ Start server') log('FLOW', '→ Wait for health') - log('FLOW', '→ Wait for extension') + log('FLOW', '→ Wait for browser readiness') // Simulate task failure const failureReason = taskId === 'task-1' - ? 'Extension did not connect' + ? 'Browser did not become ready' : taskId === 'task-2' ? 'Tool timed out after 65000ms' : 'BrowserOS helper service not connected' @@ -530,14 +521,12 @@ async function scenario7_ConsecutiveFailures(): Promise { log('ERROR', `Task failed: ${failureReason}`) const isInfraError = - failureReason.includes('Extension') || failureReason.includes('timeout') || failureReason.includes('not connected') if (isInfraError) { log('CLEANUP', '→ Detected infra error, cleaning ports') log('CLEANUP', '→ killPort(9110)') - log('CLEANUP', '→ killPort(9310)') } log('CLEANUP', '→ Stop server') @@ -558,7 +547,6 @@ async function main() { console.log('Failure Scenario Test Suite') console.log('='.repeat(70)) console.log(`Server Port: ${EVAL_PORTS.server}`) - console.log(`Extension Port: ${EVAL_PORTS.extension}`) console.log(`CDP Port: ${EVAL_PORTS.cdp}`) console.log() @@ -570,8 +558,8 @@ async function main() { }, { num: 2, - name: 'Extension Does Not Connect (30s timeout)', - fn: scenario2_ExtensionNotConnecting, + name: 'Browser Does Not Become Ready (30s timeout)', + fn: scenario2_BrowserNotReady, }, { num: 3, @@ -585,8 +573,8 @@ async function main() { }, { num: 5, - name: 'Extension Disconnects Mid-Task (App Crash)', - fn: scenario5_ExtensionDisconnectsMidTask, + name: 'Browser Becomes Unavailable Mid-Task (App Crash)', + fn: scenario5_BrowserUnavailableMidTask, }, { num: 6, @@ -627,7 +615,6 @@ async function main() { } catch {} } killPort(EVAL_PORTS.server) - killPort(EVAL_PORTS.extension) process.exit(0) } process.on('SIGINT', cleanup) diff --git a/packages/browseros-agent/apps/eval/scripts/test-lifecycle.ts b/packages/browseros-agent/apps/eval/scripts/test-lifecycle.ts index 15daff712..75c2644a4 100644 --- a/packages/browseros-agent/apps/eval/scripts/test-lifecycle.ts +++ b/packages/browseros-agent/apps/eval/scripts/test-lifecycle.ts @@ -5,7 +5,7 @@ * Tests: * 1. BrowserOS app detection * 2. Server start/stop - * 3. Extension connection with verification + * 3. Browser readiness with verification * 4. Window create/close * 5. Screenshot capture * 6. Multiple tasks in sequence with server restart @@ -21,7 +21,6 @@ import { type Subprocess, spawn, spawnSync } from 'bun' const EVAL_PORTS = { cdp: 9005, server: 9105, // http_mcp in config.dev.json - extension: 9305, } as const const MONOREPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), '../../..') const MCP_URL = `http://127.0.0.1:${EVAL_PORTS.server}/mcp` @@ -60,7 +59,7 @@ async function _killBrowserOSApp(): Promise { async function _launchBrowserOSApp(): Promise { console.log( - ` Launching BrowserOS (server disabled, CDP=${EVAL_PORTS.cdp}, Extension=${EVAL_PORTS.extension})...`, + ` Launching BrowserOS (server disabled, CDP=${EVAL_PORTS.cdp})...`, ) spawnSync({ cmd: [ @@ -72,7 +71,6 @@ async function _launchBrowserOSApp(): Promise { `--remote-debugging-port=${EVAL_PORTS.cdp}`, `--browseros-cdp-port=${EVAL_PORTS.cdp}`, `--browseros-mcp-port=${EVAL_PORTS.server}`, - `--browseros-extension-port=${EVAL_PORTS.extension}`, ], }) for (let i = 0; i < 30; i++) { @@ -119,22 +117,19 @@ async function waitForServerHealth( return false } -async function waitForExtension( +async function waitForBrowserReady( serverPort: number, maxAttempts = 90, ): Promise { let connectedCount = 0 for (let i = 0; i < maxAttempts; i++) { try { - const response = await fetch( - `http://127.0.0.1:${serverPort}/extension-status`, - { - signal: AbortSignal.timeout(2000), - }, - ) + const response = await fetch(`http://127.0.0.1:${serverPort}/health`, { + signal: AbortSignal.timeout(2000), + }) if (response.ok) { - const data = (await response.json()) as { extensionConnected?: boolean } - if (data.extensionConnected) { + const data = (await response.json()) as { cdpConnected?: boolean } + if (data.cdpConnected) { connectedCount++ if (connectedCount >= 3) return true } else { @@ -151,9 +146,7 @@ async function waitForExtension( async function startServer(): Promise { killPort(EVAL_PORTS.server) - killPort(EVAL_PORTS.extension) await waitForPortFree(EVAL_PORTS.server, 30) - await waitForPortFree(EVAL_PORTS.extension, 30) const serverProc = spawn({ cmd: [ @@ -161,8 +154,6 @@ async function startServer(): Promise { 'apps/server/src/index.ts', '--server-port', String(EVAL_PORTS.server), - '--extension-port', - String(EVAL_PORTS.extension), '--cdp-port', String(EVAL_PORTS.cdp), ], @@ -259,14 +250,14 @@ async function testServerStartStop(): Promise { } console.log(' ✅ Server healthy') - console.log(' Waiting for extension...') - const extConnected = await waitForExtension(EVAL_PORTS.server, 60) - if (!extConnected) { - console.log(' ❌ Extension did not connect') + console.log(' Waiting for browser readiness...') + const browserReady = await waitForBrowserReady(EVAL_PORTS.server, 60) + if (!browserReady) { + console.log(' ❌ Browser did not become ready') await stopServer(proc) return false } - console.log(' ✅ Extension connected') + console.log(' ✅ Browser ready') console.log(' Stopping server...') await stopServer(proc) @@ -288,9 +279,9 @@ async function testWindowLifecycle(): Promise { return false } - const extConnected = await waitForExtension(EVAL_PORTS.server, 60) - if (!extConnected) { - console.log(' ❌ Extension did not connect') + const browserReady = await waitForBrowserReady(EVAL_PORTS.server, 60) + if (!browserReady) { + console.log(' ❌ Browser did not become ready') await stopServer(proc) return false } @@ -371,9 +362,9 @@ async function testMultipleTasksWithRestart(): Promise { continue } - const extConnected = await waitForExtension(EVAL_PORTS.server, 60) - if (!extConnected) { - console.log(` ❌ Task ${task.id}: Extension not connected`) + const browserReady = await waitForBrowserReady(EVAL_PORTS.server, 60) + if (!browserReady) { + console.log(` ❌ Task ${task.id}: Browser not ready`) await stopServer(proc) continue } @@ -416,8 +407,8 @@ async function testMultipleTasksWithRestart(): Promise { return successCount === tasks.length } -async function testExtensionReconnect(): Promise { - console.log('\n=== Test 5: Extension Stability (30 seconds) ===') +async function testBrowserStability(): Promise { + console.log('\n=== Test 5: Browser Stability (30 seconds) ===') console.log(' Starting server...') const proc = await startServer() @@ -429,14 +420,14 @@ async function testExtensionReconnect(): Promise { return false } - const extConnected = await waitForExtension(EVAL_PORTS.server, 60) - if (!extConnected) { - console.log(' ❌ Extension did not connect') + const browserReady = await waitForBrowserReady(EVAL_PORTS.server, 60) + if (!browserReady) { + console.log(' ❌ Browser did not become ready') await stopServer(proc) return false } - console.log(' Monitoring extension connection for 30 seconds...') + console.log(' Monitoring browser readiness for 30 seconds...') let disconnects = 0 const checkInterval = 2000 const totalChecks = 30000 / checkInterval @@ -444,21 +435,21 @@ async function testExtensionReconnect(): Promise { for (let i = 0; i < totalChecks; i++) { try { const response = await fetch( - `http://127.0.0.1:${EVAL_PORTS.server}/extension-status`, + `http://127.0.0.1:${EVAL_PORTS.server}/health`, { signal: AbortSignal.timeout(2000), }, ) - const data = (await response.json()) as { extensionConnected?: boolean } - if (!data.extensionConnected) { + const data = (await response.json()) as { cdpConnected?: boolean } + if (!data.cdpConnected) { disconnects++ console.log( - ` ⚠️ Extension disconnected at check ${i + 1}/${totalChecks}`, + ` ⚠️ Browser became unavailable at check ${i + 1}/${totalChecks}`, ) } } catch { disconnects++ - console.log(` ⚠️ Failed to check extension at ${i + 1}/${totalChecks}`) + console.log(` ⚠️ Failed to check browser at ${i + 1}/${totalChecks}`) } await new Promise((r) => setTimeout(r, checkInterval)) } @@ -466,11 +457,11 @@ async function testExtensionReconnect(): Promise { await stopServer(proc) if (disconnects > 0) { - console.log(` ❌ Extension had ${disconnects} disconnections`) + console.log(` ❌ Browser had ${disconnects} readiness failures`) return false } - console.log(' ✅ Extension stayed connected for 30 seconds') + console.log(' ✅ Browser stayed ready for 30 seconds') return true } @@ -483,7 +474,6 @@ async function main() { console.log('Eval Lifecycle Test Suite') console.log('='.repeat(60)) console.log(`Server Port: ${EVAL_PORTS.server}`) - console.log(`Extension Port: ${EVAL_PORTS.extension}`) console.log(`CDP Port: ${EVAL_PORTS.cdp}`) const results: { name: string; passed: boolean }[] = [] @@ -516,10 +506,10 @@ async function main() { passed: await testMultipleTasksWithRestart(), }) - // Test 5: Extension Stability + // Test 5: Browser Stability results.push({ - name: 'Extension Stability', - passed: await testExtensionReconnect(), + name: 'Browser Stability', + passed: await testBrowserStability(), }) // Summary diff --git a/packages/browseros-agent/apps/eval/src/agents/orchestrator-executor/index.ts b/packages/browseros-agent/apps/eval/src/agents/orchestrator-executor/index.ts index 25fbc3114..6ebaefbda 100644 --- a/packages/browseros-agent/apps/eval/src/agents/orchestrator-executor/index.ts +++ b/packages/browseros-agent/apps/eval/src/agents/orchestrator-executor/index.ts @@ -30,21 +30,6 @@ import { Executor, type ExecutorCallbacks } from './executor' import { OrchestratorAgent } from './orchestrator-agent' import type { ExecutorFactory, ExecutorResult } from './types' -/** Stub controller for eval — CDP handles all browser interaction */ -interface ControllerStub { - start(): Promise - stop(): Promise - isConnected(): boolean - send(action: string, payload?: Record): Promise -} - -const CONTROLLER_STUB: ControllerStub = { - start: async () => {}, - stop: async () => {}, - isConnected: () => false, - send: async () => ({}), -} - function extractCdpPort(config: EvalConfig): number { const serverUrl = config.browseros.server_url const match = serverUrl.match(/:(\d+)$/) @@ -159,7 +144,7 @@ export class OrchestratorExecutorEvaluator implements AgentEvaluator { const cdpPort = extractCdpPort(config) const cdp = new CdpBackend({ port: cdpPort }) await cdp.connect() - const browser = new Browser(cdp, CONTROLLER_STUB) + const browser = new Browser(cdp) capture.screenshot.setBrowser(browser) const captchaWaiter = config.captcha diff --git a/packages/browseros-agent/apps/eval/src/agents/single-agent.ts b/packages/browseros-agent/apps/eval/src/agents/single-agent.ts index 0d50acce1..cdd5be415 100644 --- a/packages/browseros-agent/apps/eval/src/agents/single-agent.ts +++ b/packages/browseros-agent/apps/eval/src/agents/single-agent.ts @@ -14,13 +14,6 @@ import { resolveProviderConfig } from '../utils/resolve-provider-config' import { withEvalTimeout } from '../utils/with-eval-timeout' import type { AgentContext, AgentEvaluator, AgentResult } from './types' -const CONTROLLER_STUB = { - start: async () => {}, - stop: async () => {}, - isConnected: () => false, - send: async () => ({}), -} as any - function extractCdpPort(config: EvalConfig): number { const serverUrl = config.browseros.server_url const match = serverUrl.match(/:(\d+)$/) @@ -62,7 +55,7 @@ export class SingleAgentEvaluator implements AgentEvaluator { const cdp = new CdpBackend({ port: cdpPort }) await cdp.connect() - const browser = new Browser(cdp, CONTROLLER_STUB) + const browser = new Browser(cdp) capture.screenshot.setBrowser(browser) // Build browser context so the agent knows the correct starting page ID diff --git a/packages/browseros-agent/apps/eval/src/agents/types.ts b/packages/browseros-agent/apps/eval/src/agents/types.ts index af144a7ac..3632c8ac7 100644 --- a/packages/browseros-agent/apps/eval/src/agents/types.ts +++ b/packages/browseros-agent/apps/eval/src/agents/types.ts @@ -12,7 +12,7 @@ export interface AgentContext { // Page resolved once at task start (fresh browser has exactly one page) initialPageId: number - // Browser window info (only for controller-based agents, not used by CDP-based single-agent) + // Browser window info for agents that operate on explicit window/tab ids windowId?: number tabId?: number diff --git a/packages/browseros-agent/apps/eval/src/runner/browseros-app-manager.ts b/packages/browseros-agent/apps/eval/src/runner/browseros-app-manager.ts index e70b8f3f9..7566b5897 100644 --- a/packages/browseros-agent/apps/eval/src/runner/browseros-app-manager.ts +++ b/packages/browseros-agent/apps/eval/src/runner/browseros-app-manager.ts @@ -5,11 +5,10 @@ * Mirrors scripts/dev/start.ts --manual mode with per-worker isolation: * * 1. Kill ports - * 2. Build extensions (once, shared across workers) - * 3. Launch Chrome directly with per-worker user-data-dir and ports - * 4. Wait for CDP - * 5. Start server with port env vars - * 6. Wait for server health + * 2. Launch Chrome directly with per-worker user-data-dir and ports + * 3. Wait for CDP + * 4. Start server with port env vars + * 5. Wait for server health * * Each worker gets isolated ports: base + workerIndex offset. */ @@ -40,7 +39,6 @@ const BROWSEROS_BINARY = process.env.BROWSEROS_BINARY || '/Applications/BrowserOS.app/Contents/MacOS/BrowserOS' -const CONTROLLER_EXT_DIR = join(MONOREPO_ROOT, 'apps/controller-ext/dist') const CAPTCHA_EXT_DIR = join( dirname(fileURLToPath(import.meta.url)), '../../extensions/nopecha', @@ -80,24 +78,6 @@ export class BrowserOSAppManager { return this.ports } - /** - * Build extensions (call once before starting workers). - * Builds controller-ext — same as start.ts buildExtension('controller-ext', 'build:ext') - */ - static buildExtensions(): void { - console.log(`[BROWSEROS] Building controller extension...`) - const result = spawnSync({ - cmd: ['bun', 'run', 'build:ext'], - cwd: MONOREPO_ROOT, - stdout: 'inherit', - stderr: 'inherit', - }) - if (result.exitCode !== 0) { - throw new Error('Failed to build controller extension') - } - console.log(`[BROWSEROS] Controller extension built`) - } - /** * Restart: kill existing, then start fresh */ @@ -135,7 +115,7 @@ export class BrowserOSAppManager { * --disable-browseros-extensions (we load them explicitly if needed) * --remote-debugging-port, --browseros-mcp-port, --browseros-extension-port * --user-data-dir (unique per worker) - * --load-extension (optional, controller-ext) + * --load-extension (optional, unpacked helper extensions) */ private async startAll(): Promise { const { cdp, server, extension } = this.ports @@ -164,10 +144,7 @@ export class BrowserOSAppManager { ] const extensions: string[] = [] - if (this.loadExtensions && existsSync(CONTROLLER_EXT_DIR)) { - extensions.push(CONTROLLER_EXT_DIR) - } - if (existsSync(CAPTCHA_EXT_DIR)) { + if (this.loadExtensions && existsSync(CAPTCHA_EXT_DIR)) { extensions.push(CAPTCHA_EXT_DIR) } if (extensions.length > 0) { diff --git a/packages/browseros-agent/apps/eval/src/runner/parallel-executor.ts b/packages/browseros-agent/apps/eval/src/runner/parallel-executor.ts index 263eb966c..4766b1bc4 100644 --- a/packages/browseros-agent/apps/eval/src/runner/parallel-executor.ts +++ b/packages/browseros-agent/apps/eval/src/runner/parallel-executor.ts @@ -87,11 +87,7 @@ export class ParallelExecutor { const cleanup = this.setupSignalHandlers() - // Build extensions once if needed (shared across workers) const loadExtensions = this.config.config.browseros.load_extensions ?? false - if (loadExtensions) { - BrowserOSAppManager.buildExtensions() - } // Patch NopeCHA API key before launching any workers const captchaConfig = this.config.config.captcha diff --git a/packages/browseros-agent/apps/server/README.md b/packages/browseros-agent/apps/server/README.md index c9c369094..eb707b5f8 100644 --- a/packages/browseros-agent/apps/server/README.md +++ b/packages/browseros-agent/apps/server/README.md @@ -29,23 +29,22 @@ MCP server and AI agent loop powering BrowserOS browser automation. This is the │ │ └── MCP client for external tool servers │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ -│ ┌────────────────────┐ ┌────────────────────────────────────┐ │ -│ │ CDP Tools │ │ Controller Tools │ │ -│ │ (screenshots, │ │ (tabs, bookmarks, history, │ │ -│ │ DOM, network, │ │ navigation, tab groups) │ │ -│ │ console, input) │ │ │ │ -│ └────────────────────┘ └────────────────────────────────────┘ │ +│ ┌─────────────────────────────────────────────────────────────┐ │ +│ │ CDP-backed browser tools │ │ +│ │ (tabs, bookmarks, history, navigation, tab groups, │ │ +│ │ screenshots, DOM, network, console, input) │ │ +│ └─────────────────────────────────────────────────────────────┘ │ └──────────────────────────────────────────────────────────────────────┘ - │ │ - │ Chrome DevTools Protocol │ WebSocket - ▼ ▼ -┌─────────────────────┐ ┌─────────────────────────────────┐ -│ Chromium CDP │ │ Controller Extension │ -│ (port 9000) │ │ (port 9300) │ -│ │ │ │ -│ DOM, network, │ │ chrome.tabs, chrome.history, │ -│ input, screenshots │ │ chrome.bookmarks │ -└─────────────────────┘ └─────────────────────────────────┘ + │ + │ Chrome DevTools Protocol + ▼ + ┌─────────────────────┐ + │ Chromium CDP │ + │ (port 9000) │ + │ │ + │ DOM, network, │ + │ input, screenshots │ + └─────────────────────┘ ``` ## MCP Tools @@ -137,7 +136,7 @@ apps/server/ ### Prerequisites - [Bun](https://bun.sh) runtime -- A running BrowserOS instance (for CDP and controller connections) +- A running BrowserOS instance (for CDP connectivity) ### Setup @@ -178,4 +177,4 @@ bun scripts/build/server.ts --target=all --no-upload |------|-------------|---------| | 9100 | `BROWSEROS_SERVER_PORT` | HTTP server (MCP, chat, health) | | 9000 | `BROWSEROS_CDP_PORT` | Chromium CDP (server connects as client) | -| 9300 | `BROWSEROS_EXTENSION_PORT` | WebSocket for controller extension | +| 9300 | `BROWSEROS_EXTENSION_PORT` | Legacy BrowserOS launch arg kept for compatibility | diff --git a/packages/browseros-agent/apps/server/src/api/routes/health.ts b/packages/browseros-agent/apps/server/src/api/routes/health.ts index dee29ed2c..d36806632 100644 --- a/packages/browseros-agent/apps/server/src/api/routes/health.ts +++ b/packages/browseros-agent/apps/server/src/api/routes/health.ts @@ -13,7 +13,11 @@ interface HealthDeps { export function createHealthRoute(deps: HealthDeps = {}) { return new Hono().get('/', (c) => { - const cdpConnected = deps.browser?.isCdpConnected() ?? true - return c.json({ status: 'ok', cdpConnected }) + const cdpConnected = deps.browser?.isCdpConnected() + return c.json( + cdpConnected === undefined + ? { status: 'ok' } + : { status: 'ok', cdpConnected }, + ) }) } diff --git a/packages/browseros-agent/apps/server/src/api/routes/status.ts b/packages/browseros-agent/apps/server/src/api/routes/status.ts index 464abab5a..42b658fd5 100644 --- a/packages/browseros-agent/apps/server/src/api/routes/status.ts +++ b/packages/browseros-agent/apps/server/src/api/routes/status.ts @@ -5,19 +5,19 @@ */ import { Hono } from 'hono' -import type { ControllerBackend } from '../../browser/backends/controller' +import type { Browser } from '../../browser/browser' interface StatusDeps { - controller: ControllerBackend + browser?: Browser } -export function createStatusRoute(deps: StatusDeps) { - const { controller } = deps - - return new Hono().get('/', (c) => - c.json({ - status: 'ok', - extensionConnected: controller.isConnected(), - }), - ) +export function createStatusRoute(deps: StatusDeps = {}) { + return new Hono().get('/', (c) => { + const cdpConnected = deps.browser?.isCdpConnected() + return c.json( + cdpConnected === undefined + ? { status: 'ok' } + : { status: 'ok', cdpConnected }, + ) + }) } diff --git a/packages/browseros-agent/apps/server/src/api/server.ts b/packages/browseros-agent/apps/server/src/api/server.ts index fa5072f7d..b4213fbf6 100644 --- a/packages/browseros-agent/apps/server/src/api/server.ts +++ b/packages/browseros-agent/apps/server/src/api/server.ts @@ -74,7 +74,6 @@ export async function createHttpServer(config: HttpServerConfig) { resourcesDir, version, browser, - controller, registry, } = config @@ -120,7 +119,7 @@ export async function createHttpServer(config: HttpServerConfig) { }, }), ) - .route('/status', createStatusRoute({ controller })) + .route('/status', createStatusRoute({ browser })) .route('/soul', createSoulRoutes()) .route('/memory', createMemoryRoutes()) .route('/skills', createSkillsRoutes()) diff --git a/packages/browseros-agent/apps/server/src/api/types.ts b/packages/browseros-agent/apps/server/src/api/types.ts index 15bf047b2..42e1b84f5 100644 --- a/packages/browseros-agent/apps/server/src/api/types.ts +++ b/packages/browseros-agent/apps/server/src/api/types.ts @@ -14,7 +14,6 @@ import { } from '@browseros/shared/schemas/browser-context' import { LLMConfigSchema } from '@browseros/shared/schemas/llm' import { z } from 'zod' -import type { ControllerBackend } from '../browser/backends/controller' import type { Browser } from '../browser/browser' import type { ToolRegistry } from '../tools/tool-registry' @@ -93,7 +92,6 @@ export interface HttpServerConfig { version: string browser: Browser - controller: ControllerBackend registry: ToolRegistry browserosId?: string diff --git a/packages/browseros-agent/apps/server/src/browser/backends/controller.ts b/packages/browseros-agent/apps/server/src/browser/backends/controller.ts deleted file mode 100644 index 33cf6f771..000000000 --- a/packages/browseros-agent/apps/server/src/browser/backends/controller.ts +++ /dev/null @@ -1,250 +0,0 @@ -import { TIMEOUTS } from '@browseros/shared/constants/timeouts' -import type { WebSocket } from 'ws' -import { WebSocketServer } from 'ws' -import { logger } from '../../lib/logger' -import type { ControllerBackend as IControllerBackend } from './types' - -interface PendingRequest { - resolve: (value: unknown) => void - reject: (error: Error) => void - timeout: NodeJS.Timeout -} - -export class ControllerBackend implements IControllerBackend { - private wss: WebSocketServer | null = null - private port: number - private clients = new Map() - private primaryClientId: string | null = null - private requestCounter = 0 - private pendingRequests = new Map() - - constructor(config: { port: number }) { - this.port = config.port - } - - async start(): Promise { - return new Promise((resolve, reject) => { - this.wss = new WebSocketServer({ - port: this.port, - host: '127.0.0.1', - }) - - const onListening = () => { - this.wss?.off('error', onError) - logger.info( - `Controller WebSocket server listening on ws://127.0.0.1:${this.port}`, - ) - resolve() - } - - const onError = (error: Error) => { - this.wss?.off('listening', onListening) - reject(error) - } - - this.wss.once('listening', onListening) - this.wss.once('error', onError) - - this.wss.on('connection', (ws: WebSocket) => { - const clientId = this.registerClient(ws) - logger.info('Extension connected', { clientId }) - - ws.on('message', (data: Buffer) => { - try { - const message = data.toString() - const parsed = JSON.parse(message) - - if (parsed.type === 'ping') { - ws.send(JSON.stringify({ type: 'pong' })) - return - } - if (parsed.type === 'focused') { - this.handleFocusEvent(clientId) - return - } - if ( - parsed.type === 'register_windows' || - parsed.type === 'window_created' || - parsed.type === 'window_removed' - ) { - // Window ownership messages — ignored for now (multi-profile deferred) - return - } - - this.handleResponse(parsed) - } catch (error) { - logger.error(`Error parsing message from ${clientId}: ${error}`) - } - }) - - ws.on('close', () => { - logger.info('Extension disconnected', { clientId }) - this.handleClientDisconnect(clientId) - }) - - ws.on('error', (error: Error) => { - logger.error(`WebSocket error for ${clientId}: ${error.message}`) - }) - }) - - this.wss.on('error', (error: Error) => { - logger.error(`WebSocket server error: ${error.message}`) - }) - }) - } - - async stop(): Promise { - return new Promise((resolve) => { - for (const [id, pending] of this.pendingRequests.entries()) { - clearTimeout(pending.timeout) - pending.reject(new Error('ControllerBackend stopping')) - this.pendingRequests.delete(id) - } - - for (const ws of this.clients.values()) { - try { - ws.close() - } catch { - // ignore - } - } - this.clients.clear() - this.primaryClientId = null - - if (this.wss) { - this.wss.close(() => { - logger.info('Controller WebSocket server closed') - resolve() - }) - } else { - resolve() - } - }) - } - - isConnected(): boolean { - return this.primaryClientId !== null - } - - async send( - action: string, - payload?: Record, - ): Promise { - if (!this.isConnected()) { - throw new Error('BrowserOS helper service not connected') - } - - const client = this.primaryClientId - ? this.clients.get(this.primaryClientId) - : null - if (!client) { - throw new Error('BrowserOS helper service not connected') - } - - const id = `${Date.now()}-${++this.requestCounter}` - const timeoutMs = TIMEOUTS.CONTROLLER_BRIDGE - - return new Promise((resolve, reject) => { - const timeout = setTimeout(() => { - this.pendingRequests.delete(id) - reject(new Error(`Request ${action} timed out after ${timeoutMs}ms`)) - }, timeoutMs) - - this.pendingRequests.set(id, { resolve, reject, timeout }) - - try { - const message = JSON.stringify({ - id, - action, - payload: payload ?? {}, - }) - client.send(message) - } catch (error) { - clearTimeout(timeout) - this.pendingRequests.delete(id) - reject(error) - } - }) - } - - private handleResponse(response: { - id: string - ok: boolean - data?: unknown - error?: string - }): void { - const pending = this.pendingRequests.get(response.id) - - if (!pending) { - logger.warn(`Received response for unknown request ID: ${response.id}`) - return - } - - clearTimeout(pending.timeout) - this.pendingRequests.delete(response.id) - - if (response.ok) { - pending.resolve(response.data) - } else { - pending.reject(new Error(response.error || 'Unknown error')) - } - } - - private registerClient(ws: WebSocket): string { - const clientId = `client-${Date.now()}-${Math.floor(Math.random() * 1000000)}` - this.clients.set(clientId, ws) - - if (!this.primaryClientId) { - this.primaryClientId = clientId - logger.info('Primary controller assigned', { clientId }) - } else { - logger.info('Controller connected in standby mode', { - clientId, - primaryClientId: this.primaryClientId, - }) - } - - return clientId - } - - private handleClientDisconnect(clientId: string): void { - const wasPrimary = this.primaryClientId === clientId - this.clients.delete(clientId) - - if (wasPrimary) { - this.primaryClientId = null - - for (const [id, pending] of this.pendingRequests.entries()) { - clearTimeout(pending.timeout) - pending.reject(new Error('Primary connection closed')) - this.pendingRequests.delete(id) - } - - this.promoteNextPrimary() - } - } - - private promoteNextPrimary(): void { - const nextEntry = this.clients.keys().next() - if (nextEntry.done) { - logger.warn('No controller connections available to promote') - return - } - - this.primaryClientId = nextEntry.value - logger.info('Promoted controller to primary', { - clientId: this.primaryClientId, - }) - } - - private handleFocusEvent(clientId: string): void { - if (this.primaryClientId === clientId) return - - const previousPrimary = this.primaryClientId - this.primaryClientId = clientId - logger.info('Primary controller reassigned due to focus event', { - clientId, - previousPrimary, - }) - } -} diff --git a/packages/browseros-agent/apps/server/src/browser/backends/types.ts b/packages/browseros-agent/apps/server/src/browser/backends/types.ts index 669e03ad6..372f6ce6d 100644 --- a/packages/browseros-agent/apps/server/src/browser/backends/types.ts +++ b/packages/browseros-agent/apps/server/src/browser/backends/types.ts @@ -12,13 +12,6 @@ export interface CdpBackend extends ProtocolApi { ): () => void } -export interface ControllerBackend { - start(): Promise - stop(): Promise - isConnected(): boolean - send(action: string, payload?: Record): Promise -} - export interface CdpTarget { id: string type: string diff --git a/packages/browseros-agent/apps/server/src/browser/browser.ts b/packages/browseros-agent/apps/server/src/browser/browser.ts index 154475871..a4e34148f 100644 --- a/packages/browseros-agent/apps/server/src/browser/browser.ts +++ b/packages/browseros-agent/apps/server/src/browser/browser.ts @@ -1,6 +1,6 @@ import type { ProtocolApi } from '@browseros/cdp-protocol/protocol-api' import { logger } from '../lib/logger' -import type { CdpBackend, ControllerBackend } from './backends/types' +import type { CdpBackend } from './backends/types' import type { BookmarkNode } from './bookmarks' import * as bookmarks from './bookmarks' import { @@ -87,16 +87,13 @@ const EXCLUDED_URL_PREFIXES = [ export class Browser { private cdp: CdpBackend - // biome-ignore lint/correctness/noUnusedPrivateClassMembers: kept for later removal - private controller: ControllerBackend private consoleCollector: ConsoleCollector private pages = new Map() private sessions = new Map() private nextPageId = 1 - constructor(cdp: CdpBackend, controller: ControllerBackend) { + constructor(cdp: CdpBackend) { this.cdp = cdp - this.controller = controller this.consoleCollector = new ConsoleCollector(cdp) this.setupEventHandlers() } diff --git a/packages/browseros-agent/apps/server/src/config.ts b/packages/browseros-agent/apps/server/src/config.ts index d35aadac5..db7d1a57a 100644 --- a/packages/browseros-agent/apps/server/src/config.ts +++ b/packages/browseros-agent/apps/server/src/config.ts @@ -20,7 +20,7 @@ export const ServerConfigSchema = z.object({ cdpPort: portSchema.nullable(), serverPort: portSchema, agentPort: portSchema, - extensionPort: portSchema, + extensionPort: portSchema.nullable(), resourcesDir: z.string(), executionDir: z.string(), mcpAllowRemote: z.boolean(), @@ -120,7 +120,7 @@ function parseCliArgs(argv: string[]): ConfigResult { ) .option( '--extension-port ', - 'Extension WebSocket port', + '[DEPRECATED] No-op, kept for backwards compatibility', parsePortArg, ) .option('--resources-dir ', 'Resources directory path') @@ -151,12 +151,6 @@ function parseCliArgs(argv: string[]): ConfigResult { const opts = program.opts() - if (opts.disableMcpServer) { - console.warn( - 'Warning: --disable-mcp-server is deprecated and has no effect', - ) - } - if (opts.httpMcpPort !== undefined) { console.warn('Warning: --http-mcp-port is deprecated. Use --server-port.') } @@ -167,6 +161,10 @@ function parseCliArgs(argv: string[]): ConfigResult { ) } + if (opts.extensionPort !== undefined) { + console.warn('Warning: --extension-port is deprecated and has no effect.') + } + const cwd = process.cwd() return { @@ -302,6 +300,7 @@ function validateInlinedEnv(): ConfigResult { function getDefaults(cwd: string): PartialConfig { return { cdpPort: null, + extensionPort: null, resourcesDir: cwd, executionDir: cwd, mcpAllowRemote: false, diff --git a/packages/browseros-agent/apps/server/src/main.ts b/packages/browseros-agent/apps/server/src/main.ts index 3e63b8cb7..294ea5fc6 100644 --- a/packages/browseros-agent/apps/server/src/main.ts +++ b/packages/browseros-agent/apps/server/src/main.ts @@ -14,7 +14,6 @@ import path from 'node:path' import { EXIT_CODES } from '@browseros/shared/constants/exit-codes' import { createHttpServer } from './api/server' import { CdpBackend } from './browser/backends/cdp' -import { ControllerBackend } from './browser/backends/controller' import { Browser } from './browser/browser' import type { ServerConfig } from './config' import { INLINED_ENV } from './env' @@ -57,20 +56,6 @@ export class Application { await this.initCoreServices() - const controller = new ControllerBackend({ - port: this.config.extensionPort, - }) - let controllerServerStarted = false - try { - logger.debug( - `Starting WebSocket server on port ${this.config.extensionPort}`, - ) - await controller.start() - controllerServerStarted = true - } catch (error) { - this.handleControllerStartupError(this.config.extensionPort, error) - } - if (!this.config.cdpPort) { logger.error('CDP port is required (--cdp-port)') process.exit(EXIT_CODES.GENERAL_ERROR) @@ -85,7 +70,7 @@ export class Application { return this.handleStartupError('CDP', this.config.cdpPort, error) } - const browser = new Browser(cdp, controller) + const browser = new Browser(cdp) logger.info(`Loaded ${registry.names().length} unified tools`) @@ -95,7 +80,6 @@ export class Application { host: '0.0.0.0', version: VERSION, browser, - controller, registry, browserosId: identity.getBrowserOSId(), executionDir: this.config.executionDir, @@ -131,7 +115,7 @@ export class Application { `Health endpoint: http://127.0.0.1:${this.config.serverPort}/health`, ) - this.logStartupSummary(controllerServerStarted) + this.logStartupSummary() startSkillSync() metrics.log('http_server.started', { version: VERSION }) @@ -244,31 +228,9 @@ export class Application { process.exit(EXIT_CODES.GENERAL_ERROR) } - private handleControllerStartupError(port: number, error: unknown): void { - const errorMsg = error instanceof Error ? error.message : String(error) - logger.warn( - 'Controller WebSocket server unavailable, continuing without controller bridge', - { port, error: errorMsg }, - ) - if (isPortInUseError(error)) { - logger.warn( - 'Controller WebSocket port is already in use, continuing without controller bridge', - { port }, - ) - } - if (!isPortInUseError(error)) { - Sentry.captureException(error) - } - } - - private logStartupSummary(controllerServerStarted: boolean): void { + private logStartupSummary(): void { logger.info('') logger.info('Services running:') - logger.info( - controllerServerStarted - ? ` Controller Server: ws://127.0.0.1:${this.config.extensionPort}` - : ' Controller Server: unavailable', - ) logger.info(` HTTP Server: http://127.0.0.1:${this.config.serverPort}`) logger.info('') } diff --git a/packages/browseros-agent/apps/server/tests/__helpers__/server.ts b/packages/browseros-agent/apps/server/tests/__helpers__/server.ts index 094ff5d27..a36336ff4 100644 --- a/packages/browseros-agent/apps/server/tests/__helpers__/server.ts +++ b/packages/browseros-agent/apps/server/tests/__helpers__/server.ts @@ -76,8 +76,6 @@ export async function spawnServer(config: ServerConfig): Promise { config.cdpPort.toString(), '--server-port', config.serverPort.toString(), - '--extension-port', - config.extensionPort.toString(), ], { stdio: ['ignore', 'pipe', 'pipe'], diff --git a/packages/browseros-agent/apps/server/tests/__helpers__/setup.ts b/packages/browseros-agent/apps/server/tests/__helpers__/setup.ts index 9f43b8bab..14db19943 100644 --- a/packages/browseros-agent/apps/server/tests/__helpers__/setup.ts +++ b/packages/browseros-agent/apps/server/tests/__helpers__/setup.ts @@ -19,41 +19,10 @@ export interface TestEnvironmentConfig { cdpPort: number serverPort: number extensionPort: number - skipExtension?: boolean } let runtimePlan: TestRuntimePlan | null = null -async function isExtensionConnected(port: number): Promise { - try { - const response = await fetch(`http://127.0.0.1:${port}/status`, { - signal: AbortSignal.timeout(1000), - }) - if (response.ok) { - const data = (await response.json()) as { extensionConnected: boolean } - return data.extensionConnected - } - } catch { - // Not connected yet - } - return false -} - -async function waitForExtensionConnection( - port: number, - // Extension startup can be slow on a cold BrowserOS profile. - // Keep this aligned with typical per-test timeouts (30s). - maxAttempts = 60, -): Promise { - for (let i = 0; i < maxAttempts; i++) { - if (await isExtensionConnected(port)) { - return - } - await new Promise((resolve) => setTimeout(resolve, 500)) - } - throw new Error(`Extension failed to connect on port ${port} within timeout`) -} - function configsMatch( a: TestEnvironmentConfig, b: TestEnvironmentConfig, @@ -69,7 +38,6 @@ function configsMatch( * Ensures the full BrowserOS test environment is ready: * 1. Server running and healthy * 2. Browser running with CDP available - * 3. Extension connected to server * * Reuses existing processes if already running with same config. */ @@ -84,7 +52,6 @@ export async function ensureBrowserOS( cdpPort: options?.cdpPort ?? runtimePlan.ports.cdp, serverPort: options?.serverPort ?? runtimePlan.ports.server, extensionPort: options?.extensionPort ?? runtimePlan.ports.extension, - skipExtension: options?.skipExtension ?? false, } // Fast path: already running with same config @@ -96,26 +63,8 @@ export async function ensureBrowserOS( configsMatch(serverState.config, config) && configsMatch(browserState.config, config) ) { - if (config.skipExtension) { - console.log('Reusing existing test environment') - return config - } - - if (await isExtensionConnected(config.serverPort)) { - console.log('Reusing existing test environment') - return config - } - - // Same server+browser are already running; we just need the extension. - // Avoid restarting processes (which can flake by killing the test runner). - console.log('Reusing existing test environment (waiting for extension)...') - try { - await waitForExtensionConnection(config.serverPort) - console.log('Extension connected') - return config - } catch { - // Fall through to full setup below. - } + console.log('Reusing existing test environment') + return config } // Config changed or not running: full setup @@ -139,15 +88,6 @@ export async function ensureBrowserOS( // 3. Start server once CDP is available. await spawnServer(config) - // 4. Wait for extension to connect (unless skipped for CDP-only tests) - if (!config.skipExtension) { - console.log('Waiting for extension to connect...') - await waitForExtensionConnection(config.serverPort) - console.log('Extension connected') - } else { - console.log('Skipping extension connection (CDP-only mode)') - } - console.log('=== Test environment ready ===\n') return config } diff --git a/packages/browseros-agent/apps/server/tests/__helpers__/with-browser.ts b/packages/browseros-agent/apps/server/tests/__helpers__/with-browser.ts index 7afec4abc..a0b149fd0 100644 --- a/packages/browseros-agent/apps/server/tests/__helpers__/with-browser.ts +++ b/packages/browseros-agent/apps/server/tests/__helpers__/with-browser.ts @@ -1,7 +1,6 @@ import { existsSync } from 'node:fs' import { Mutex } from 'async-mutex' import { CdpBackend } from '../../src/browser/backends/cdp' -import type { ControllerBackend } from '../../src/browser/backends/types' import { Browser } from '../../src/browser/browser' import type { ToolDefinition } from '../../src/tools/framework' import { executeTool } from '../../src/tools/framework' @@ -15,15 +14,6 @@ let cachedCdp: CdpBackend | null = null let cachedBrowser: Browser | null = null let runtimePlan: TestRuntimePlan | null = null -const stubController: ControllerBackend = { - start: async () => {}, - stop: async () => {}, - isConnected: () => false, - send: async () => { - throw new Error('Controller not available in test mode') - }, -} - async function getOrCreateBrowser(): Promise { if (cachedBrowser && cachedCdp?.isConnected()) return cachedBrowser @@ -53,7 +43,7 @@ async function getOrCreateBrowser(): Promise { cachedCdp = new CdpBackend({ port: runtimePlan.ports.cdp }) await cachedCdp.connect() - cachedBrowser = new Browser(cachedCdp, stubController) + cachedBrowser = new Browser(cachedCdp) return cachedBrowser } diff --git a/packages/browseros-agent/apps/server/tests/api/routes/status.test.ts b/packages/browseros-agent/apps/server/tests/api/routes/status.test.ts new file mode 100644 index 000000000..2406a3206 --- /dev/null +++ b/packages/browseros-agent/apps/server/tests/api/routes/status.test.ts @@ -0,0 +1,43 @@ +/** + * @license + * Copyright 2025 BrowserOS + */ + +import { describe, it } from 'bun:test' +import assert from 'node:assert' + +import { createStatusRoute } from '../../../src/api/routes/status' + +describe('createStatusRoute', () => { + it('returns status ok when no browser is provided', async () => { + const route = createStatusRoute() + const response = await route.request('/') + + assert.strictEqual(response.status, 200) + const body = await response.json() + assert.deepStrictEqual(body, { status: 'ok' }) + }) + + it('reads CDP connectivity on each request', async () => { + let connected = false + const route = createStatusRoute({ + browser: { + isCdpConnected: () => connected, + } as never, + }) + + const firstResponse = await route.request('/') + assert.deepStrictEqual(await firstResponse.json(), { + status: 'ok', + cdpConnected: false, + }) + + connected = true + + const secondResponse = await route.request('/') + assert.deepStrictEqual(await secondResponse.json(), { + status: 'ok', + cdpConnected: true, + }) + }) +}) diff --git a/packages/browseros-agent/apps/server/tests/config.test.ts b/packages/browseros-agent/apps/server/tests/config.test.ts index e213d70b1..b90d52710 100644 --- a/packages/browseros-agent/apps/server/tests/config.test.ts +++ b/packages/browseros-agent/apps/server/tests/config.test.ts @@ -60,7 +60,6 @@ describe('loadServerConfig', () => { 'bun', 'src/index.ts', '--server-port=9223', - '--extension-port=9224', '--allow-remote-in-mcp', ]) @@ -74,12 +73,36 @@ describe('loadServerConfig', () => { 'bun', 'src/index.ts', '--server-port=9223', - '--extension-port=9224', ]) assert.strictEqual(result.ok, true) if (!result.ok) return assert.strictEqual(result.value.cdpPort, null) + assert.strictEqual(result.value.extensionPort, null) + }) + + it('warns when --extension-port is provided', () => { + const warnings: string[] = [] + const originalWarn = console.warn + console.warn = (message?: unknown) => { + warnings.push(String(message)) + } + + try { + const result = loadServerConfig([ + 'bun', + 'src/index.ts', + '--server-port=9223', + '--extension-port=9224', + ]) + + assert.strictEqual(result.ok, true) + assert.deepStrictEqual(warnings, [ + 'Warning: --extension-port is deprecated and has no effect.', + ]) + } finally { + console.warn = originalWarn + } }) }) @@ -268,7 +291,6 @@ describe('loadServerConfig', () => { assert.strictEqual(result.ok, false) if (result.ok) return assert.ok(result.error.includes('serverPort')) - assert.ok(result.error.includes('extensionPort')) }) it('returns error for missing config file', () => { @@ -355,7 +377,6 @@ describe('loadServerConfig', () => { 'bun', 'src/index.ts', '--server-port=3000', - '--extension-port=3002', ]) assert.strictEqual(result.ok, true) @@ -369,7 +390,6 @@ describe('loadServerConfig', () => { 'bun', 'src/index.ts', '--server-port=3000', - '--extension-port=3002', ]) assert.strictEqual(result.ok, true) @@ -382,7 +402,6 @@ describe('loadServerConfig', () => { 'bun', 'src/index.ts', '--server-port=3000', - '--extension-port=3002', ]) assert.strictEqual(result.ok, true) @@ -395,7 +414,6 @@ describe('loadServerConfig', () => { 'bun', 'src/index.ts', '--server-port=3000', - '--extension-port=3002', ]) assert.strictEqual(result.ok, true) @@ -403,12 +421,23 @@ describe('loadServerConfig', () => { assert.strictEqual(result.value.agentPort, result.value.serverPort) }) + it('defaults extensionPort to null', () => { + const result = loadServerConfig([ + 'bun', + 'src/index.ts', + '--server-port=3000', + ]) + + assert.strictEqual(result.ok, true) + if (!result.ok) return + assert.strictEqual(result.value.extensionPort, null) + }) + it('defaults aiSdkDevtoolsEnabled to false', () => { const result = loadServerConfig([ 'bun', 'src/index.ts', '--server-port=3000', - '--extension-port=3002', ]) assert.strictEqual(result.ok, true) @@ -425,7 +454,6 @@ describe('loadServerConfig', () => { 'bun', 'src/index.ts', '--server-port=3000', - '--extension-port=3002', ]) assert.strictEqual(result.ok, true) diff --git a/packages/browseros-agent/apps/server/tests/main.test.ts b/packages/browseros-agent/apps/server/tests/main.test.ts index 5fa60ffc6..fe06f6d5c 100644 --- a/packages/browseros-agent/apps/server/tests/main.test.ts +++ b/packages/browseros-agent/apps/server/tests/main.test.ts @@ -9,10 +9,11 @@ const config = { cdpPort: 9222, serverPort: 9100, agentPort: 9100, - extensionPort: 9300, + extensionPort: null, resourcesDir: '/tmp/browseros-resources', executionDir: '/tmp/browseros-execution', mcpAllowRemote: false, + aiSdkDevtoolsEnabled: false, } describe('Application.start', () => { @@ -20,140 +21,96 @@ describe('Application.start', () => { mock.restore() }) - it('continues when controller startup fails', async () => { - const controllerError = Object.assign(new Error('bind failed'), { - code: 'EADDRINUSE', - }) + it('starts with the CDP backend only', async () => { const createHttpServer = mock(async () => ({})) - const controllerStart = mock(async () => { - throw controllerError - }) const cdpConnect = mock(async () => {}) - const sentryCaptureException = mock(() => {}) + const browserCtor = mock(() => {}) const loggerInfo = mock(() => {}) const loggerWarn = mock(() => {}) const loggerDebug = mock(() => {}) const loggerError = mock(() => {}) - const processExit = mock((() => { - throw new Error('process.exit called') - }) as typeof process.exit) - const originalExit = process.exit - process.exit = processExit + mock.module('../src/api/server', () => ({ + createHttpServer, + })) + mock.module('../src/browser/backends/cdp', () => ({ + CdpBackend: class { + async connect(): Promise { + await cdpConnect() + } + }, + })) + mock.module('../src/browser/browser', () => ({ + Browser: class { + constructor(cdp: unknown) { + browserCtor(cdp) + } + }, + })) + mock.module('../src/lib/browseros-dir', () => ({ + cleanOldSessions: mock(async () => {}), + ensureBrowserosDir: mock(async () => {}), + removeServerConfigSync: mock(() => {}), + writeServerConfig: mock(async () => {}), + })) + mock.module('../src/lib/db', () => ({ + initializeDb: mock(() => ({})), + })) + mock.module('../src/lib/identity', () => ({ + identity: { + initialize: mock(() => {}), + getBrowserOSId: mock(() => 'browseros-id'), + }, + })) + mock.module('../src/lib/logger', () => ({ + logger: { + setLogFile: mock(() => {}), + info: loggerInfo, + warn: loggerWarn, + debug: loggerDebug, + error: loggerError, + }, + })) + mock.module('../src/lib/metrics', () => ({ + metrics: { + initialize: mock(() => {}), + isEnabled: mock(() => true), + log: mock(() => {}), + }, + })) + mock.module('../src/lib/sentry', () => ({ + Sentry: { + setContext: mock(() => {}), + setUser: mock(() => {}), + captureException: mock(() => {}), + }, + })) + mock.module('../src/lib/soul', () => ({ + seedSoulTemplate: mock(async () => {}), + })) + mock.module('../src/skills/migrate', () => ({ + migrateBuiltinSkills: mock(async () => {}), + })) + mock.module('../src/skills/remote-sync', () => ({ + startSkillSync: mock(() => {}), + stopSkillSync: mock(() => {}), + syncBuiltinSkills: mock(async () => {}), + })) + mock.module('../src/tools/registry', () => ({ + registry: { + names: () => ['test_tool'], + }, + })) - try { - mock.module('../src/api/server', () => ({ - createHttpServer, - })) - mock.module('../src/browser/backends/controller', () => ({ - ControllerBackend: class { - async start(): Promise { - await controllerStart() - } + const { Application } = await import('../src/main') + const app = new Application(config) - async stop(): Promise {} + await app.start() - isConnected(): boolean { - return false - } - - async send(): Promise { - throw new Error('BrowserOS helper service not connected') - } - }, - })) - mock.module('../src/browser/backends/cdp', () => ({ - CdpBackend: class { - async connect(): Promise { - await cdpConnect() - } - }, - })) - mock.module('../src/browser/browser', () => ({ - Browser: class {}, - })) - mock.module('../src/lib/browseros-dir', () => ({ - ensureBrowserosDir: mock(async () => {}), - })) - mock.module('../src/lib/db', () => ({ - initializeDb: mock(() => ({})), - })) - mock.module('../src/lib/identity', () => ({ - identity: { - initialize: mock(() => {}), - getBrowserOSId: mock(() => 'browseros-id'), - }, - })) - mock.module('../src/lib/logger', () => ({ - logger: { - setLogFile: mock(() => {}), - info: loggerInfo, - warn: loggerWarn, - debug: loggerDebug, - error: loggerError, - }, - })) - mock.module('../src/lib/metrics', () => ({ - metrics: { - initialize: mock(() => {}), - isEnabled: mock(() => true), - log: mock(() => {}), - }, - })) - mock.module('../src/lib/sentry', () => ({ - Sentry: { - setContext: mock(() => {}), - captureException: sentryCaptureException, - }, - })) - mock.module('../src/lib/soul', () => ({ - seedSoulTemplate: mock(async () => {}), - })) - mock.module('../src/tools/registry', () => ({ - registry: { - names: () => ['test_tool'], - }, - })) - - const { Application } = await import('../src/main') - const app = new Application(config) - - await app.start() - - expect(controllerStart).toHaveBeenCalledTimes(1) - expect(cdpConnect).toHaveBeenCalledTimes(1) - expect(createHttpServer).toHaveBeenCalledTimes(1) - expect(sentryCaptureException).toHaveBeenCalledTimes(1) - expect(sentryCaptureException).toHaveBeenCalledWith(controllerError) - expect(processExit).not.toHaveBeenCalled() - - const warningCall = loggerWarn.mock.calls.find( - ([message]) => - message === - 'Controller WebSocket server unavailable, continuing without controller bridge', - ) - expect(warningCall).toBeDefined() - expect(warningCall?.[1]).toEqual({ - port: config.extensionPort, - error: 'bind failed', - }) - - const portConflictCall = loggerWarn.mock.calls.find( - ([message]) => - message === - 'Controller WebSocket port is already in use, continuing without controller bridge', - ) - expect(portConflictCall).toBeDefined() - expect(portConflictCall?.[1]).toEqual({ - port: config.extensionPort, - }) - expect( - loggerInfo.mock.calls.some( - ([message]) => message === ' Controller Server: unavailable', - ), - ).toBe(true) - expect(loggerError).not.toHaveBeenCalled() - } finally { - process.exit = originalExit - } + expect(cdpConnect).toHaveBeenCalledTimes(1) + expect(browserCtor).toHaveBeenCalledTimes(1) + expect(createHttpServer).toHaveBeenCalledTimes(1) + expect(createHttpServer.mock.calls[0]?.[0]).not.toHaveProperty('controller') + expect(loggerWarn).not.toHaveBeenCalled() + expect(loggerError).not.toHaveBeenCalled() }) }) diff --git a/packages/browseros-agent/apps/server/tests/sdk/agent-sdk.test.ts b/packages/browseros-agent/apps/server/tests/sdk/agent-sdk.test.ts index ea3d38ac9..84d93736b 100644 --- a/packages/browseros-agent/apps/server/tests/sdk/agent-sdk.test.ts +++ b/packages/browseros-agent/apps/server/tests/sdk/agent-sdk.test.ts @@ -11,7 +11,6 @@ import assert from 'node:assert' import { Agent } from '@browseros-ai/agent-sdk' import { CdpBackend } from '../../src/browser/backends/cdp' -import type { ControllerBackend } from '../../src/browser/backends/types' import { Browser } from '../../src/browser/browser' import { ensureBrowserOS, @@ -22,15 +21,6 @@ let config: TestEnvironmentConfig let cdp: CdpBackend | null = null let runtimeWindowId: number -const stubController: ControllerBackend = { - start: async () => {}, - stop: async () => {}, - isConnected: () => false, - send: async () => { - throw new Error('Controller not available in SDK tests') - }, -} - async function getRuntimeWindow( testConfig: TestEnvironmentConfig, ): Promise { @@ -38,7 +28,7 @@ async function getRuntimeWindow( await runtimeCdp.connect() cdp = runtimeCdp - const browser = new Browser(runtimeCdp, stubController) + const browser = new Browser(runtimeCdp) const pages = await browser.listPages() const page = pages.find((entry) => !entry.isHidden && entry.windowId !== undefined) ?? diff --git a/packages/browseros-agent/apps/server/tests/server.integration.test.ts b/packages/browseros-agent/apps/server/tests/server.integration.test.ts index 20f4733cf..b6c82d906 100644 --- a/packages/browseros-agent/apps/server/tests/server.integration.test.ts +++ b/packages/browseros-agent/apps/server/tests/server.integration.test.ts @@ -69,16 +69,16 @@ describe('HTTP Server Integration Tests', () => { }) describe('Status endpoint', () => { - it('reports extension as connected', async () => { + it('reports CDP as connected', async () => { const response = await fetch(`${getBaseUrl()}/status`) assert.strictEqual(response.status, 200) const json = (await response.json()) as { status: string - extensionConnected: boolean + cdpConnected: boolean } assert.strictEqual(json.status, 'ok') - assert.strictEqual(json.extensionConnected, true) + assert.strictEqual(json.cdpConnected, true) }) }) diff --git a/packages/browseros-agent/bun.lock b/packages/browseros-agent/bun.lock index c3881f79d..351c2f28c 100644 --- a/packages/browseros-agent/bun.lock +++ b/packages/browseros-agent/bun.lock @@ -125,25 +125,6 @@ "wxt": "^0.20.18", }, }, - "apps/controller-ext": { - "name": "browseros-controller", - "version": "1.0.0", - "dependencies": { - "@browseros/shared": "workspace:*", - "zod": "^4.1.12", - }, - "devDependencies": { - "@types/chrome": "^0.1.24", - "@types/node": "^24.7.1", - "copy-webpack-plugin": "^12.0.2", - "terser-webpack-plugin": "^5.3.11", - "ts-loader": "^9.5.4", - "typescript": "^5.9.3", - "webpack": "^5.102.1", - "webpack-cli": "^6.0.1", - "ws": "^8.18.3", - }, - }, "apps/eval": { "name": "@browseros/eval", "version": "0.1.0", @@ -572,8 +553,6 @@ "@devicefarmer/adbkit-monkey": ["@devicefarmer/adbkit-monkey@1.2.1", "", {}, "sha512-ZzZY/b66W2Jd6NHbAhLyDWOEIBWC11VizGFk7Wx7M61JZRz7HR9Cq5P+65RKWUU7u6wgsE8Lmh9nE4Mz+U2eTg=="], - "@discoveryjs/json-ext": ["@discoveryjs/json-ext@0.6.3", "", {}, "sha512-4B4OijXeVNOPZlYA2oEwWOTkzyltLao+xbotHQeqN++Rv27Y6s818+n2Qkp8q+Fxhn0t/5lA5X1Mxktud8eayQ=="], - "@dnd-kit/accessibility": ["@dnd-kit/accessibility@3.1.1", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw=="], "@dnd-kit/core": ["@dnd-kit/core@6.3.1", "", { "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ=="], @@ -1628,7 +1607,7 @@ "@sindresorhus/is": ["@sindresorhus/is@7.2.0", "", {}, "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw=="], - "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@2.3.0", "", {}, "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg=="], + "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="], "@sinonjs/commons": ["@sinonjs/commons@3.0.1", "", { "dependencies": { "type-detect": "4.0.8" } }, "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ=="], @@ -1898,10 +1877,6 @@ "@types/dompurify": ["@types/dompurify@3.2.0", "", { "dependencies": { "dompurify": "*" } }, "sha512-Fgg31wv9QbLDA0SpTOXO3MaxySc4DKGLi8sna4/Utjo4r3ZRPdCt4UQee8BWr+Q5z21yifghREPJGYaEOEIACg=="], - "@types/eslint": ["@types/eslint@9.6.1", "", { "dependencies": { "@types/estree": "*", "@types/json-schema": "*" } }, "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag=="], - - "@types/eslint-scope": ["@types/eslint-scope@3.7.7", "", { "dependencies": { "@types/eslint": "*", "@types/estree": "*" } }, "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg=="], - "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="], @@ -2002,36 +1977,6 @@ "@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.2", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.53", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ=="], - "@webassemblyjs/ast": ["@webassemblyjs/ast@1.14.1", "", { "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ=="], - - "@webassemblyjs/floating-point-hex-parser": ["@webassemblyjs/floating-point-hex-parser@1.13.2", "", {}, "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA=="], - - "@webassemblyjs/helper-api-error": ["@webassemblyjs/helper-api-error@1.13.2", "", {}, "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ=="], - - "@webassemblyjs/helper-buffer": ["@webassemblyjs/helper-buffer@1.14.1", "", {}, "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA=="], - - "@webassemblyjs/helper-numbers": ["@webassemblyjs/helper-numbers@1.13.2", "", { "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.13.2", "@webassemblyjs/helper-api-error": "1.13.2", "@xtuc/long": "4.2.2" } }, "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA=="], - - "@webassemblyjs/helper-wasm-bytecode": ["@webassemblyjs/helper-wasm-bytecode@1.13.2", "", {}, "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA=="], - - "@webassemblyjs/helper-wasm-section": ["@webassemblyjs/helper-wasm-section@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/wasm-gen": "1.14.1" } }, "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw=="], - - "@webassemblyjs/ieee754": ["@webassemblyjs/ieee754@1.13.2", "", { "dependencies": { "@xtuc/ieee754": "^1.2.0" } }, "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw=="], - - "@webassemblyjs/leb128": ["@webassemblyjs/leb128@1.13.2", "", { "dependencies": { "@xtuc/long": "4.2.2" } }, "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw=="], - - "@webassemblyjs/utf8": ["@webassemblyjs/utf8@1.13.2", "", {}, "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ=="], - - "@webassemblyjs/wasm-edit": ["@webassemblyjs/wasm-edit@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/helper-wasm-section": "1.14.1", "@webassemblyjs/wasm-gen": "1.14.1", "@webassemblyjs/wasm-opt": "1.14.1", "@webassemblyjs/wasm-parser": "1.14.1", "@webassemblyjs/wast-printer": "1.14.1" } }, "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ=="], - - "@webassemblyjs/wasm-gen": ["@webassemblyjs/wasm-gen@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/ieee754": "1.13.2", "@webassemblyjs/leb128": "1.13.2", "@webassemblyjs/utf8": "1.13.2" } }, "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg=="], - - "@webassemblyjs/wasm-opt": ["@webassemblyjs/wasm-opt@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/wasm-gen": "1.14.1", "@webassemblyjs/wasm-parser": "1.14.1" } }, "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw=="], - - "@webassemblyjs/wasm-parser": ["@webassemblyjs/wasm-parser@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-api-error": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/ieee754": "1.13.2", "@webassemblyjs/leb128": "1.13.2", "@webassemblyjs/utf8": "1.13.2" } }, "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ=="], - - "@webassemblyjs/wast-printer": ["@webassemblyjs/wast-printer@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" } }, "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw=="], - "@webext-core/fake-browser": ["@webext-core/fake-browser@1.3.4", "", { "dependencies": { "lodash.merge": "^4.6.2" } }, "sha512-nZcVWr3JpwpS5E6hKpbAwAMBM/AXZShnfW0F76udW8oLd6Kv0nbW6vFS07md4Na/0ntQonk3hFnlQYGYBAlTrA=="], "@webext-core/isolated-element": ["@webext-core/isolated-element@1.1.3", "", { "dependencies": { "is-potential-custom-element-name": "^1.0.1" } }, "sha512-rbtnReIGdiVQb2UhK3MiECU6JqsiIo2K/luWvOdOw57Ot770Iw4KLCEPXUQMITIH5V5er2jfVK8hSWXaEOQGNQ=="], @@ -2040,12 +1985,6 @@ "@webext-core/messaging": ["@webext-core/messaging@2.3.0", "", { "dependencies": { "serialize-error": "^11.0.0", "uid": "^2.0.2", "webextension-polyfill": "^0.10.0" } }, "sha512-gChSVKdRs7JEq5hFH0jVROSvTq+sKq9afXTA/gBswep3RWNLhXyDsXFlvPMkYbmML1XZ8QKKC9ou2MlCKRZwSQ=="], - "@webpack-cli/configtest": ["@webpack-cli/configtest@3.0.1", "", { "peerDependencies": { "webpack": "^5.82.0", "webpack-cli": "6.x.x" } }, "sha512-u8d0pJ5YFgneF/GuvEiDA61Tf1VDomHHYMjv/wc9XzYj7nopltpG96nXN5dJRstxZhcNpV1g+nT6CydO7pHbjA=="], - - "@webpack-cli/info": ["@webpack-cli/info@3.0.1", "", { "peerDependencies": { "webpack": "^5.82.0", "webpack-cli": "6.x.x" } }, "sha512-coEmDzc2u/ffMvuW9aCjoRzNSPDl/XLuhPdlFRpT9tZHmJ/039az33CE7uH+8s0uL1j5ZNtfdv0HkfaKRBGJsQ=="], - - "@webpack-cli/serve": ["@webpack-cli/serve@3.0.1", "", { "peerDependencies": { "webpack": "^5.82.0", "webpack-cli": "6.x.x" } }, "sha512-sbgw03xQaCLiT6gcY/6u3qBDn01CWw/nbaXl3gTdTFuJJ75Gffv3E3DBpgvY2fkkrdS1fpjaXNOmJlnbtKauKg=="], - "@whatwg-node/disposablestack": ["@whatwg-node/disposablestack@0.0.6", "", { "dependencies": { "@whatwg-node/promise-helpers": "^1.0.0", "tslib": "^2.6.3" } }, "sha512-LOtTn+JgJvX8WfBVJtF08TGrdjuFzGJc4mkP8EdDI8ADbvO7kiexYep1o8dwnt0okb0jYclCDXF13xU7Ge4zSw=="], "@whatwg-node/fetch": ["@whatwg-node/fetch@0.10.13", "", { "dependencies": { "@whatwg-node/node-fetch": "^0.8.3", "urlpattern-polyfill": "^10.0.0" } }, "sha512-b4PhJ+zYj4357zwk4TTuF2nEe0vVtOrwdsrNo5hL+u1ojXNhh1FgJ6pg1jzDlwlT4oBdzfSwaBwMCtFCsIWg8Q=="], @@ -2062,10 +2001,6 @@ "@xterm/headless": ["@xterm/headless@5.5.0", "", {}, "sha512-5xXB7kdQlFBP82ViMJTwwEc3gKCLGKR/eoxQm4zge7GPBl86tCdI0IdPJjoKd8mUSFXz5V7i/25sfsEkP4j46g=="], - "@xtuc/ieee754": ["@xtuc/ieee754@1.2.0", "", {}, "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA=="], - - "@xtuc/long": ["@xtuc/long@4.2.2", "", {}, "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ=="], - "@xyflow/react": ["@xyflow/react@12.10.0", "", { "dependencies": { "@xyflow/system": "0.0.74", "classcat": "^5.0.3", "zustand": "^4.4.0" }, "peerDependencies": { "react": ">=17", "react-dom": ">=17" } }, "sha512-eOtz3whDMWrB4KWVatIBrKuxECHqip6PfA8fTpaS2RUGVpiEAe+nqDKsLqkViVWxDGreq0lWX71Xth/SPAzXiw=="], "@xyflow/system": ["@xyflow/system@0.0.74", "", { "dependencies": { "@types/d3-drag": "^3.0.7", "@types/d3-interpolate": "^3.0.4", "@types/d3-selection": "^3.0.10", "@types/d3-transition": "^3.0.8", "@types/d3-zoom": "^3.0.8", "d3-drag": "^3.0.0", "d3-interpolate": "^3.0.1", "d3-selection": "^3.0.0", "d3-zoom": "^3.0.0" } }, "sha512-7v7B/PkiVrkdZzSbL+inGAo6tkR/WQHHG0/jhSvLQToCsfa8YubOGmBYd1s08tpKpihdHDZFwzQZeR69QSBb4Q=="], @@ -2078,8 +2013,6 @@ "acorn-import-attributes": ["acorn-import-attributes@1.9.5", "", { "peerDependencies": { "acorn": "^8" } }, "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ=="], - "acorn-import-phases": ["acorn-import-phases@1.0.4", "", { "peerDependencies": { "acorn": "^8.14.0" } }, "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ=="], - "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], "adm-zip": ["adm-zip@0.5.16", "", {}, "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ=="], @@ -2096,8 +2029,6 @@ "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], - "ajv-keywords": ["ajv-keywords@5.1.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3" }, "peerDependencies": { "ajv": "^8.8.2" } }, "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw=="], - "anser": ["anser@2.3.5", "", {}, "sha512-vcZjxvvVoxTeR5XBNJB38oTu/7eDCZlwdz32N1eNgpyPF7j/Z7Idf+CUwQOkKKpJ7RJyjxgLHCM7vdIK0iCNMQ=="], "ansi-align": ["ansi-align@3.0.1", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="], @@ -2204,8 +2135,6 @@ "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], - "browseros-controller": ["browseros-controller@workspace:apps/controller-ext"], - "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], "bser": ["bser@2.1.1", "", { "dependencies": { "node-int64": "^0.4.0" } }, "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ=="], @@ -2284,8 +2213,6 @@ "chrome-launcher": ["chrome-launcher@1.2.0", "", { "dependencies": { "@types/node": "*", "escape-string-regexp": "^4.0.0", "is-wsl": "^2.2.0", "lighthouse-logger": "^2.0.1" }, "bin": { "print-chrome-path": "bin/print-chrome-path.cjs" } }, "sha512-JbuGuBNss258bvGil7FT4HKdC3SC2K7UAEUqiPy3ACS3Yxo3hAW6bvFpCu2HsIJLgTqxgEX6BkujvzZfLpUD0Q=="], - "chrome-trace-event": ["chrome-trace-event@1.0.4", "", {}, "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ=="], - "chromium-bidi": ["chromium-bidi@9.1.0", "", { "dependencies": { "mitt": "^3.0.1", "zod": "^3.24.1" }, "peerDependencies": { "devtools-protocol": "*" } }, "sha512-rlUzQ4WzIAWdIbY/viPShhZU2n21CxDUgazXVbw4Hu1MwaeUSEksSeM6DqPgpRjCLXRk702AVRxJxoOz0dw4OA=="], "ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="], @@ -2314,8 +2241,6 @@ "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], - "clone-deep": ["clone-deep@4.0.1", "", { "dependencies": { "is-plain-object": "^2.0.4", "kind-of": "^6.0.2", "shallow-clone": "^3.0.0" } }, "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ=="], - "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], "cm6-theme-basic-light": ["cm6-theme-basic-light@0.2.0", "", { "peerDependencies": { "@codemirror/language": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0", "@lezer/highlight": "^1.0.0" } }, "sha512-1prg2gv44sYfpHscP26uLT/ePrh0mlmVwMSoSd3zYKQ92Ab3jPRLzyCnpyOCQLJbK+YdNs4HvMRqMNYdy4pMhA=="], @@ -2372,8 +2297,6 @@ "copy-to-clipboard": ["copy-to-clipboard@3.3.3", "", { "dependencies": { "toggle-selection": "^1.0.6" } }, "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA=="], - "copy-webpack-plugin": ["copy-webpack-plugin@12.0.2", "", { "dependencies": { "fast-glob": "^3.3.2", "glob-parent": "^6.0.1", "globby": "^14.0.0", "normalize-path": "^3.0.0", "schema-utils": "^4.2.0", "serialize-javascript": "^6.0.2" }, "peerDependencies": { "webpack": "^5.1.0" } }, "sha512-SNwdBeHyII+rWvee/bTnAYyO8vfVdcSTud4EIb6jcZ8inLeWucJE0DnxXQBjlQ5zlteuuvooGQy3LIyGxhvlOA=="], - "core-js": ["core-js@3.45.1", "", {}, "sha512-L4NPsJlCfZsPeXukyzHFlg/i7IIVwHSItR0wg0FLNqYClJ4MQYTYLbC7EkjKYRLZF2iof2MUgN0EGy7MdQFChg=="], "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], @@ -2594,8 +2517,6 @@ "env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], - "envinfo": ["envinfo@7.21.0", "", { "bin": { "envinfo": "dist/cli.js" } }, "sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow=="], - "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], "error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="], @@ -2638,15 +2559,11 @@ "escodegen": ["escodegen@2.1.0", "", { "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", "esutils": "^2.0.2" }, "optionalDependencies": { "source-map": "~0.6.1" }, "bin": { "esgenerate": "bin/esgenerate.js", "escodegen": "bin/escodegen.js" } }, "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w=="], - "eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="], - "esniff": ["esniff@2.0.1", "", { "dependencies": { "d": "^1.0.1", "es5-ext": "^0.10.62", "event-emitter": "^0.3.5", "type": "^2.7.2" } }, "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg=="], "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], - "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], - - "estraverse": ["estraverse@4.3.0", "", {}, "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="], + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], "estree-util-attach-comments": ["estree-util-attach-comments@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw=="], @@ -2720,8 +2637,6 @@ "fast-xml-parser": ["fast-xml-parser@5.4.1", "", { "dependencies": { "fast-xml-builder": "^1.0.0", "strnum": "^2.1.2" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-BQ30U1mKkvXQXXkAGcuyUA/GA26oEB7NzOtsxCDtyu62sjGw5QraKFhx2Em3WQNjPw9PG6MQ9yuIIgkSDfGu5A=="], - "fastest-levenshtein": ["fastest-levenshtein@1.0.16", "", {}, "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg=="], - "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], "fault": ["fault@2.0.1", "", { "dependencies": { "format": "^0.2.0" } }, "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ=="], @@ -2762,8 +2677,6 @@ "firefox-profile": ["firefox-profile@4.7.0", "", { "dependencies": { "adm-zip": "~0.5.x", "fs-extra": "^11.2.0", "ini": "^4.1.3", "minimist": "^1.2.8", "xml2js": "^0.6.2" }, "bin": { "firefox-profile": "lib/cli.js" } }, "sha512-aGApEu5bfCNbA4PGUZiRJAIU6jKmghV2UVdklXAofnNtiDjqYw0czLS46W7IfFqVKgKhFB8Ao2YoNGHY4BoIMQ=="], - "flat": ["flat@5.0.2", "", { "bin": { "flat": "cli.js" } }, "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ=="], - "follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="], "for-in": ["for-in@1.0.2", "", {}, "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ=="], @@ -2832,7 +2745,7 @@ "glob": ["glob@13.0.0", "", { "dependencies": { "minimatch": "^10.1.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA=="], - "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "glob-to-regexp": ["glob-to-regexp@0.4.1", "", {}, "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="], @@ -2840,7 +2753,7 @@ "globals": ["globals@16.5.0", "", {}, "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ=="], - "globby": ["globby@14.1.0", "", { "dependencies": { "@sindresorhus/merge-streams": "^2.1.0", "fast-glob": "^3.3.3", "ignore": "^7.0.3", "path-type": "^6.0.0", "slash": "^5.1.0", "unicorn-magic": "^0.3.0" } }, "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA=="], + "globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="], "google-auth-library": ["google-auth-library@9.15.1", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^6.1.1", "gcp-metadata": "^6.1.0", "gtoken": "^7.0.0", "jws": "^4.0.0" } }, "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng=="], @@ -2980,8 +2893,6 @@ "import-in-the-middle": ["import-in-the-middle@2.0.5", "", { "dependencies": { "acorn": "^8.15.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" } }, "sha512-0InH9/4oDCBRzWXhpOqusspLBrVfK1vPvbn9Wxl8DAQ8yyx5fWJRETICSwkiAMaYntjJAMBP1R4B6cQnEUYVEA=="], - "import-local": ["import-local@3.2.0", "", { "dependencies": { "pkg-dir": "^4.2.0", "resolve-cwd": "^3.0.0" }, "bin": { "import-local-fixture": "fixtures/cli.js" } }, "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA=="], - "import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="], "index-to-position": ["index-to-position@1.2.0", "", {}, "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw=="], @@ -2994,8 +2905,6 @@ "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], - "interpret": ["interpret@3.1.1", "", {}, "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ=="], - "intersection-observer": ["intersection-observer@0.12.2", "", {}, "sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg=="], "invariant": ["invariant@2.2.4", "", { "dependencies": { "loose-envify": "^1.0.0" } }, "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA=="], @@ -3090,8 +2999,6 @@ "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], - "jest-worker": ["jest-worker@27.5.1", "", { "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg=="], - "jimp": ["jimp@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/diff": "1.6.0", "@jimp/js-bmp": "1.6.0", "@jimp/js-gif": "1.6.0", "@jimp/js-jpeg": "1.6.0", "@jimp/js-png": "1.6.0", "@jimp/js-tiff": "1.6.0", "@jimp/plugin-blit": "1.6.0", "@jimp/plugin-blur": "1.6.0", "@jimp/plugin-circle": "1.6.0", "@jimp/plugin-color": "1.6.0", "@jimp/plugin-contain": "1.6.0", "@jimp/plugin-cover": "1.6.0", "@jimp/plugin-crop": "1.6.0", "@jimp/plugin-displace": "1.6.0", "@jimp/plugin-dither": "1.6.0", "@jimp/plugin-fisheye": "1.6.0", "@jimp/plugin-flip": "1.6.0", "@jimp/plugin-hash": "1.6.0", "@jimp/plugin-mask": "1.6.0", "@jimp/plugin-print": "1.6.0", "@jimp/plugin-quantize": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/plugin-rotate": "1.6.0", "@jimp/plugin-threshold": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0" } }, "sha512-YcwCHw1kiqEeI5xRpDlPPBGL2EOpBKLwO4yIBJcXWHPj5PnA5urGq0jbyhM5KoNpypQ6VboSoxc9D8HyfvngSg=="], "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], @@ -3230,8 +3137,6 @@ "lit-html": ["lit-html@3.3.2", "", { "dependencies": { "@types/trusted-types": "^2.0.2" } }, "sha512-Qy9hU88zcmaxBXcc10ZpdK7cOLXvXpRoBxERdtqV9QOrfpMZZ6pSYP91LhpPtap3sFMUiL7Tw2RImbe0Al2/kw=="], - "loader-runner": ["loader-runner@4.3.1", "", {}, "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q=="], - "local-pkg": ["local-pkg@1.1.2", "", { "dependencies": { "mlly": "^1.7.4", "pkg-types": "^2.3.0", "quansync": "^0.2.11" } }, "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A=="], "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], @@ -3340,8 +3245,6 @@ "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], - "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], - "merge-value": ["merge-value@1.0.0", "", { "dependencies": { "get-value": "^2.0.6", "is-extendable": "^1.0.0", "mixin-deep": "^1.2.0", "set-value": "^2.0.0" } }, "sha512-fJMmvat4NeKz63Uv9iHWcPDjCWcCkoiRoajRTEO8hlhUC6rwaHg0QCF9hBOTjZmm4JuglPckPSTtcuJL5kp0TQ=="], "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], @@ -3438,9 +3341,9 @@ "mime": ["mime@4.0.7", "", { "bin": { "mime": "bin/cli.js" } }, "sha512-2OfDPL+e03E0LrXaGYOtTFIYhiuzep94NSsuhrNULq+stylcJedcHdzHtz0atMUuGwJfFYs0YL5xeC/Ca2x0eQ=="], - "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], - "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], @@ -3484,8 +3387,6 @@ "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], - "neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="], - "netmask": ["netmask@2.0.2", "", {}, "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg=="], "next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="], @@ -3582,8 +3483,6 @@ "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], - "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], - "pac-proxy-agent": ["pac-proxy-agent@7.2.0", "", { "dependencies": { "@tootallnate/quickjs-emscripten": "^0.23.0", "agent-base": "^7.1.2", "debug": "^4.3.4", "get-uri": "^6.0.1", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.6", "pac-resolver": "^7.0.1", "socks-proxy-agent": "^8.0.5" } }, "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA=="], "pac-resolver": ["pac-resolver@7.0.1", "", { "dependencies": { "degenerator": "^5.0.0", "netmask": "^2.0.2" } }, "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg=="], @@ -3642,7 +3541,7 @@ "path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], - "path-type": ["path-type@6.0.0", "", {}, "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ=="], + "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], @@ -3676,8 +3575,6 @@ "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="], - "pkg-dir": ["pkg-dir@4.2.0", "", { "dependencies": { "find-up": "^4.0.0" } }, "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ=="], - "pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="], "pngjs": ["pngjs@7.0.0", "", {}, "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow=="], @@ -3908,8 +3805,6 @@ "real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="], - "rechoir": ["rechoir@0.8.0", "", { "dependencies": { "resolve": "^1.20.0" } }, "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ=="], - "recma-build-jsx": ["recma-build-jsx@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-build-jsx": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew=="], "recma-jsx": ["recma-jsx@1.0.1", "", { "dependencies": { "acorn-jsx": "^5.0.0", "estree-util-to-js": "^2.0.0", "recma-parse": "^1.0.0", "recma-stringify": "^1.0.0", "unified": "^11.0.0" }, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w=="], @@ -3982,8 +3877,6 @@ "resolve-alpn": ["resolve-alpn@1.2.1", "", {}, "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="], - "resolve-cwd": ["resolve-cwd@3.0.0", "", { "dependencies": { "resolve-from": "^5.0.0" } }, "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg=="], - "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], "responselike": ["responselike@4.0.2", "", { "dependencies": { "lowercase-keys": "^3.0.0" } }, "sha512-cGk8IbWEAnaCpdAt1BHzJ3Ahz5ewDJa0KseTsE3qIRMJ3C698W8psM7byCeWVpd/Ha7FUYzuRVzXoKoM6nRUbA=="], @@ -4026,8 +3919,6 @@ "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], - "schema-utils": ["schema-utils@4.3.3", "", { "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", "ajv-formats": "^2.1.1", "ajv-keywords": "^5.1.0" } }, "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA=="], - "screenfull": ["screenfull@5.2.0", "", {}, "sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA=="], "scroll-into-view-if-needed": ["scroll-into-view-if-needed@3.1.0", "", { "dependencies": { "compute-scroll-into-view": "^3.0.2" } }, "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ=="], @@ -4050,8 +3941,6 @@ "serialize-error": ["serialize-error@11.0.3", "", { "dependencies": { "type-fest": "^2.12.2" } }, "sha512-2G2y++21dhj2R7iHAdd0FIzjGwuKZld+7Pl/bTU6YIkrC2ZMbVUjm+luj6A6V34Rv9XfKJDKpTWu9W4Gse1D9g=="], - "serialize-javascript": ["serialize-javascript@7.0.3", "", {}, "sha512-h+cZ/XXarqDgCjo+YSyQU/ulDEESGGf8AMK9pPNmhNSl/FzPl6L8pMp1leca5z6NuG6tvV/auC8/43tmovowww=="], - "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], @@ -4062,8 +3951,6 @@ "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], - "shallow-clone": ["shallow-clone@3.0.1", "", { "dependencies": { "kind-of": "^6.0.2" } }, "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA=="], - "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], @@ -4098,7 +3985,7 @@ "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], - "slash": ["slash@5.1.0", "", {}, "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg=="], + "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], "slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="], @@ -4234,8 +4121,6 @@ "terser": ["terser@5.46.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg=="], - "terser-webpack-plugin": ["terser-webpack-plugin@5.3.16", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", "schema-utils": "^4.3.0", "serialize-javascript": "^6.0.2", "terser": "^5.31.1" }, "peerDependencies": { "webpack": "^5.1.0" } }, "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q=="], - "text-decoder": ["text-decoder@1.2.3", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA=="], "thread-stream": ["thread-stream@3.1.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A=="], @@ -4284,8 +4169,6 @@ "ts-import-resolver": ["ts-import-resolver@0.1.23", "", { "peerDependencies": { "typescript": ">=4.5.0" }, "optionalPeers": ["typescript"] }, "sha512-282pgr6j6aOvP3P2I6XugDxdBobkpdMmdbWjRjGl5gjPI1p0+oTNGDh1t924t75kRlyIkF65DiwhSIUysmyHQA=="], - "ts-loader": ["ts-loader@9.5.4", "", { "dependencies": { "chalk": "^4.1.0", "enhanced-resolve": "^5.0.0", "micromatch": "^4.0.0", "semver": "^7.3.4", "source-map": "^0.7.4" }, "peerDependencies": { "typescript": "*", "webpack": "^5.0.0" } }, "sha512-nCz0rEwunlTZiy6rXFByQU1kVVpCIgUpc/psFiKVrUwrizdnIbRFu8w7bxhUF0X613DYwT4XzrZHpVyMe758hQ=="], - "ts-log": ["ts-log@2.2.7", "", {}, "sha512-320x5Ggei84AxzlXp91QkIGSw5wgaLT6GeAH0KsqDmRZdVWW2OiSeVvElVoatk3f7nicwXlElXsoFkARiGE2yg=="], "ts-md5": ["ts-md5@2.0.1", "", {}, "sha512-yF35FCoEOFBzOclSkMNEUbFQZuv89KEQ+5Xz03HrMSGUGB1+r+El+JiGOFwsP4p9RFNzwlrydYoTLvPOuICl9w=="], @@ -4326,7 +4209,7 @@ "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - "unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="], + "unicorn-magic": ["unicorn-magic@0.1.0", "", {}, "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ=="], "unidiff": ["unidiff@1.0.4", "", { "dependencies": { "diff": "^5.1.0" } }, "sha512-ynU0vsAXw0ir8roa+xPCUHmnJ5goc5BTM2Kuc3IJd8UwgaeRs7VSD5+eeaQL+xp1JtB92hu/Zy/Lgy7RZcr1pQ=="], @@ -4424,7 +4307,7 @@ "w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="], - "watchpack": ["watchpack@2.5.1", "", { "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" } }, "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg=="], + "watchpack": ["watchpack@2.4.4", "", { "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" } }, "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA=="], "web-ext-run": ["web-ext-run@0.2.4", "", { "dependencies": { "@babel/runtime": "7.28.2", "@devicefarmer/adbkit": "3.3.8", "chrome-launcher": "1.2.0", "debounce": "1.2.1", "es6-error": "4.1.1", "firefox-profile": "4.7.0", "fx-runner": "1.4.0", "multimatch": "6.0.0", "node-notifier": "10.0.1", "parse-json": "7.1.1", "pino": "9.7.0", "promise-toolbox": "0.21.0", "set-value": "4.1.0", "source-map-support": "0.5.21", "strip-bom": "5.0.0", "strip-json-comments": "5.0.2", "tmp": "0.2.5", "update-notifier": "7.3.1", "watchpack": "2.4.4", "zip-dir": "2.0.0" } }, "sha512-rQicL7OwuqWdQWI33JkSXKcp7cuv1mJG8u3jRQwx/8aDsmhbTHs9ZRmNYOL+LX0wX8edIEQX8jj4bB60GoXtKA=="], @@ -4442,12 +4325,6 @@ "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], - "webpack": ["webpack@5.104.1", "", { "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", "acorn": "^8.15.0", "acorn-import-phases": "^1.0.3", "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.17.4", "es-module-lexer": "^2.0.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", "loader-runner": "^4.3.1", "mime-types": "^2.1.27", "neo-async": "^2.6.2", "schema-utils": "^4.3.3", "tapable": "^2.3.0", "terser-webpack-plugin": "^5.3.16", "watchpack": "^2.4.4", "webpack-sources": "^3.3.3" }, "bin": { "webpack": "bin/webpack.js" } }, "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA=="], - - "webpack-cli": ["webpack-cli@6.0.1", "", { "dependencies": { "@discoveryjs/json-ext": "^0.6.1", "@webpack-cli/configtest": "^3.0.1", "@webpack-cli/info": "^3.0.1", "@webpack-cli/serve": "^3.0.1", "colorette": "^2.0.14", "commander": "^12.1.0", "cross-spawn": "^7.0.3", "envinfo": "^7.14.0", "fastest-levenshtein": "^1.0.12", "import-local": "^3.0.2", "interpret": "^3.1.1", "rechoir": "^0.8.0", "webpack-merge": "^6.0.1" }, "peerDependencies": { "webpack": "^5.82.0" }, "bin": { "webpack-cli": "./bin/cli.js" } }, "sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw=="], - - "webpack-merge": ["webpack-merge@6.0.1", "", { "dependencies": { "clone-deep": "^4.0.1", "flat": "^5.0.2", "wildcard": "^2.0.1" } }, "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg=="], - "webpack-sources": ["webpack-sources@3.3.3", "", {}, "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg=="], "webpack-virtual-modules": ["webpack-virtual-modules@0.5.0", "", {}, "sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw=="], @@ -4464,8 +4341,6 @@ "widest-line": ["widest-line@5.0.0", "", { "dependencies": { "string-width": "^7.0.0" } }, "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA=="], - "wildcard": ["wildcard@2.0.1", "", {}, "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ=="], - "winreg": ["winreg@0.0.12", "", {}, "sha512-typ/+JRmi7RqP1NanzFULK36vczznSNN8kWVA9vIqXyv8GhghUlwhGp1Xj3Nms1FsPcNnsQrJOR10N58/nQ9hQ=="], "wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], @@ -4562,8 +4437,6 @@ "@browseros/server/@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="], - "@codesandbox/sandpack-client/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], - "@codesandbox/sandpack-react/react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], "@devicefarmer/adbkit/commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="], @@ -4628,8 +4501,6 @@ "@graphql-tools/code-file-loader/@graphql-tools/utils": ["@graphql-tools/utils@11.0.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA=="], - "@graphql-tools/code-file-loader/globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="], - "@graphql-tools/delegate/@graphql-tools/utils": ["@graphql-tools/utils@11.0.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA=="], "@graphql-tools/executor/@graphql-tools/utils": ["@graphql-tools/utils@11.0.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA=="], @@ -4648,16 +4519,12 @@ "@graphql-tools/graphql-file-loader/@graphql-tools/utils": ["@graphql-tools/utils@11.0.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA=="], - "@graphql-tools/graphql-file-loader/globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="], - "@graphql-tools/graphql-tag-pluck/@graphql-tools/utils": ["@graphql-tools/utils@11.0.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA=="], "@graphql-tools/import/@graphql-tools/utils": ["@graphql-tools/utils@11.0.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA=="], "@graphql-tools/json-file-loader/@graphql-tools/utils": ["@graphql-tools/utils@11.0.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA=="], - "@graphql-tools/json-file-loader/globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="], - "@graphql-tools/load/@graphql-tools/utils": ["@graphql-tools/utils@11.0.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA=="], "@graphql-tools/merge/@graphql-tools/utils": ["@graphql-tools/utils@11.0.0", "", { "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", "@whatwg-node/promise-helpers": "^1.0.0", "cross-inspect": "1.0.1", "tslib": "^2.4.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-bM1HeZdXA2C3LSIeLOnH/bcqSgbQgKEDrjxODjqi3y58xai2TkNrtYcQSoWzGbt9VMN1dORGjR7Vem8SPnUFQA=="], @@ -4972,8 +4839,6 @@ "@types/request/form-data": ["form-data@2.5.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.35", "safe-buffer": "^5.2.1" } }, "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A=="], - "accepts/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], - "antd/@ant-design/cssinjs": ["@ant-design/cssinjs@1.24.0", "", { "dependencies": { "@babel/runtime": "^7.11.1", "@emotion/hash": "^0.8.0", "@emotion/unitless": "^0.7.5", "classnames": "^2.3.1", "csstype": "^3.1.3", "rc-util": "^5.35.0", "stylis": "^4.3.4" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-K4cYrJBsgvL+IoozUXYjbT6LHHNt+19a9zkvpBPxLjFHas1UpPM2A5MlhROb0BT8N8WoavM5VsP9MeSeNK/3mg=="], "antd/rc-collapse": ["rc-collapse@3.9.0", "", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", "rc-motion": "^2.3.4", "rc-util": "^5.27.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-swDdz4QZ4dFTo4RAUMLL50qP0EY62N2kvmk2We5xYdRwcRn8WcYtuetCJpwpaCbUfUt5+huLpVxhvmnK+PHrkA=="], @@ -4992,8 +4857,6 @@ "boxen/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], - "browseros-controller/zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="], - "c12/perfect-debounce": ["perfect-debounce@2.0.0", "", {}, "sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow=="], "chrome-launcher/escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], @@ -5022,38 +4885,28 @@ "d3-sankey/d3-shape": ["d3-shape@1.3.7", "", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="], - "dir-glob/path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], - "dotenv-expand/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], "duplexify/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], "ecdsa-sig-formatter/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - "escodegen/estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], - "escodegen/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], - "esrecurse/estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], - "eventid/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], - "execa/@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="], - "express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], - "express/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], - "extract-zip/get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="], - "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - "fetch-blob/web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], "figures/is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], "find-up/path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "fx-runner/commander": ["commander@2.9.0", "", { "dependencies": { "graceful-readlink": ">= 1.0.0" } }, "sha512-bmkUukX8wAOjHdN26xj5c4ctEV22TQ7dQYhSmuckKhToXrkUn0iIaolHdIxYYqD55nhpSPA9zPQ1yP57GdXP2A=="], "fx-runner/shell-quote": ["shell-quote@1.7.3", "", {}, "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw=="], @@ -5074,6 +4927,8 @@ "global-directory/ini": ["ini@4.1.1", "", {}, "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g=="], + "globby/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + "google-gax/@grpc/proto-loader": ["@grpc/proto-loader@0.7.15", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.2.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ=="], "got/form-data-encoder": ["form-data-encoder@4.1.0", "", {}, "sha512-G6NsmEW15s0Uw9XnCg+33H3ViYRyiM0hMrMhhqQOR8NFc5GhYrI+6I3u7OTw7b91J2g8rtvMBZJDbcGb2YUniw=="], @@ -5102,8 +4957,6 @@ "is-inside-container/is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], - "jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], - "jszip/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], "jwa/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], @@ -5142,6 +4995,8 @@ "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], + "npm-run-path/unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="], + "nypm/citty": ["citty@0.2.0", "", {}, "sha512-8csy5IBFI2ex2hTVpaHN2j+LNE199AgiI7y4dMintrr8i0lQiFn+0AWMZrWdHKIgMOer65f8IThysYhoReqjWA=="], "openai/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], @@ -5170,8 +5025,6 @@ "pixelmatch/pngjs": ["pngjs@6.0.0", "", {}, "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg=="], - "pkg-dir/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], - "postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], "prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], @@ -5202,12 +5055,6 @@ "read-pkg/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], - "read-pkg/unicorn-magic": ["unicorn-magic@0.1.0", "", {}, "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ=="], - - "schema-utils/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="], - - "send/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], - "sinon/diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="], "slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], @@ -5224,8 +5071,6 @@ "static-browser-server/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], - "static-browser-server/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], - "streamdown/lucide-react": ["lucide-react@0.542.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-w3hD8/SQB7+lzU2r4VdFyzzOzKnUjTZIF/MQJGSSvni7Llewni4vuViRppfRAa2guOsY5k4jZyxw/i9DQHv+dw=="], "string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], @@ -5246,8 +5091,6 @@ "tree-sitter-bash/node-addon-api": ["node-addon-api@8.5.0", "", {}, "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A=="], - "type-is/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], - "unidiff/diff": ["diff@5.2.2", "", {}, "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A=="], "unimport/unplugin": ["unplugin@2.3.11", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "acorn": "^8.15.0", "picomatch": "^4.0.3", "webpack-virtual-modules": "^0.6.2" } }, "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww=="], @@ -5272,10 +5115,6 @@ "web-ext-run/strip-json-comments": ["strip-json-comments@5.0.2", "", {}, "sha512-4X2FR3UwhNUE9G49aIsJW5hRRR3GXGTBTZRMfv568O60ojM8HcWjV/VxAxCDW3SUND33O6ZY66ZuRcdkj73q2g=="], - "web-ext-run/watchpack": ["watchpack@2.4.4", "", { "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" } }, "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA=="], - - "webpack-cli/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], - "widest-line/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], "wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], @@ -5428,18 +5267,6 @@ "@google/genai/google-auth-library/gtoken": ["gtoken@8.0.0", "", { "dependencies": { "gaxios": "^7.0.0", "jws": "^4.0.0" } }, "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw=="], - "@graphql-tools/code-file-loader/globby/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], - - "@graphql-tools/code-file-loader/globby/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], - - "@graphql-tools/graphql-file-loader/globby/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], - - "@graphql-tools/graphql-file-loader/globby/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], - - "@graphql-tools/json-file-loader/globby/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], - - "@graphql-tools/json-file-loader/globby/slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], - "@inquirer/core/wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], @@ -5612,12 +5439,10 @@ "@sentry/node/@opentelemetry/instrumentation/require-in-the-middle": ["require-in-the-middle@8.0.1", "", { "dependencies": { "debug": "^4.3.5", "module-details-from-path": "^1.0.3" } }, "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ=="], + "@types/request/form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "@types/request/form-data/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - "accepts/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], - - "babel-plugin-macros/cosmiconfig/path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], - "babel-plugin-macros/cosmiconfig/yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="], "boxen/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], @@ -5634,7 +5459,7 @@ "d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="], - "express/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], "fx-runner/which/is-absolute": ["is-absolute@0.1.7", "", { "dependencies": { "is-relative": "^0.1.0" } }, "sha512-Xi9/ZSn4NFapG8RP98iNPMOeaV3mXPisxKxzKtHVqr3g56j/fBn+yZmnxSVAA8lmZbl2J9b/a4kJvfU3hqQYgA=="], @@ -5654,8 +5479,6 @@ "graphql-config/@graphql-tools/url-loader/sync-fetch": ["sync-fetch@0.6.0-2", "", { "dependencies": { "node-fetch": "^3.3.2", "timeout-signal": "^2.0.0", "whatwg-mimetype": "^4.0.0" } }, "sha512-c7AfkZ9udatCuAy9RSfiGPpeOKKUAUK5e1cXadLOGUjasdxqYqAK0jTNkM/FSEyJ3a5Ra27j/tw/PS0qLmaF/A=="], - "graphql-config/cosmiconfig/path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], - "gray-matter/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], "jszip/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], @@ -5668,26 +5491,16 @@ "openai/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], - "pkg-dir/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], - - "pkg-dir/find-up/path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], - "publish-browser-extension/listr2/cli-truncate": ["cli-truncate@4.0.0", "", { "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^7.0.0" } }, "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA=="], - "send/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], - "split-string/extend-shallow/is-extendable": ["is-extendable@1.0.1", "", { "dependencies": { "is-plain-object": "^2.0.4" } }, "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA=="], "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "type-is/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], - "unimport/unplugin/webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="], - "unplugin/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - "unplugin/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], "web-ext-run/parse-json/json-parse-even-better-errors": ["json-parse-even-better-errors@3.0.2", "", {}, "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ=="], @@ -5798,6 +5611,8 @@ "@sentry/bundler-plugin-core/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + "@types/request/form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "fx-runner/which/is-absolute/is-relative": ["is-relative@0.1.3", "", {}, "sha512-wBOr+rNM4gkAZqoLRJI4myw5WzzIdQosFAAbnvfXP5z1LyzgAI3ivOKehC5KfqlQJZoihVhirgtCBj378Eg8GA=="], "glob/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], @@ -5812,8 +5627,6 @@ "graphql-config/@graphql-tools/url-loader/sync-fetch/node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], - "pkg-dir/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], - "publish-browser-extension/listr2/cli-truncate/slice-ansi": ["slice-ansi@5.0.0", "", { "dependencies": { "ansi-styles": "^6.0.0", "is-fullwidth-code-point": "^4.0.0" } }, "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ=="], "publish-browser-extension/listr2/cli-truncate/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], @@ -5844,8 +5657,6 @@ "graphql-config/@graphql-tools/url-loader/@graphql-tools/wrap/@graphql-tools/delegate/@graphql-tools/batch-execute": ["@graphql-tools/batch-execute@9.0.19", "", { "dependencies": { "@graphql-tools/utils": "^10.9.1", "@whatwg-node/promise-helpers": "^1.3.0", "dataloader": "^2.2.3", "tslib": "^2.8.1" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-VGamgY4PLzSx48IHPoblRw0oTaBa7S26RpZXt0Y4NN90ytoE0LutlpB2484RbkfcTjv9wa64QD474+YP1kEgGA=="], - "pkg-dir/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], - "publish-browser-extension/listr2/cli-truncate/slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], "publish-browser-extension/listr2/cli-truncate/slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="], diff --git a/packages/browseros-agent/package.json b/packages/browseros-agent/package.json index bdc86413f..acbe72302 100644 --- a/packages/browseros-agent/package.json +++ b/packages/browseros-agent/package.json @@ -16,8 +16,8 @@ "test:env": "./tools/dev/run.sh test", "test:cleanup": "./tools/dev/run.sh cleanup", "start:server": "bun run --filter @browseros/server --elide-lines=0 start", - "start:agent": "bun ./scripts/build/controller-ext.ts && bun run --filter @browseros/agent dev", - "build": "bun run build:server && bun run build:agent && bun run build:ext", + "start:agent": "bun run --filter @browseros/agent dev", + "build": "bun run build:server && bun run build:agent", "build:server": "FORCE_COLOR=1 bun scripts/build/server.ts --target=all", "build:server:ci": "FORCE_COLOR=1 bun scripts/build/server.ts --target=all --compile-only", "build:server:test": "FORCE_COLOR=1 bun scripts/build/server.ts --target=darwin-arm64 --no-upload", @@ -27,7 +27,6 @@ "build:agent": "bun run codegen:agent && bun run --filter @browseros/agent build", "build:agent-sdk": "bun run --filter @browseros-ai/agent-sdk build", "codegen:agent": "bun run --filter @browseros/agent codegen", - "build:ext": "FORCE_COLOR=1 bun run --filter browseros-controller --elide-lines=0 build", "test": "FORCE_COLOR=1 bun run --filter @browseros/server --elide-lines=0 test:tools", "test:tools": "bun run --filter @browseros/server test:tools", "test:integration": "bun run --filter @browseros/server test:integration", diff --git a/packages/browseros-agent/packages/shared/src/constants/timeouts.ts b/packages/browseros-agent/packages/shared/src/constants/timeouts.ts index 0d0480ec2..e36dc44fb 100644 --- a/packages/browseros-agent/packages/shared/src/constants/timeouts.ts +++ b/packages/browseros-agent/packages/shared/src/constants/timeouts.ts @@ -13,10 +13,6 @@ export const TIMEOUTS = { TEST_PROVIDER: 15_000, REFINE_PROMPT: 30_000, - // Controller communication - CONTROLLER_DEFAULT: 60_000, - CONTROLLER_BRIDGE: 30_000, - // MCP operations MCP_DEFAULT: 5_000, MCP_TRANSPORT_PROBE: 5_000, @@ -42,13 +38,6 @@ export const TIMEOUTS = { STABLE_DOM: 3_000, FILE_CHOOSER: 3_000, - // WebSocket (controller-ext) - WS_RECONNECT_INTERVAL: 5_000, - WS_HEARTBEAT_INTERVAL: 20_000, - WS_HEARTBEAT_TIMEOUT: 5_000, - WS_CONNECTION_TIMEOUT: 10_000, - WS_REQUEST_TIMEOUT: 30_000, - // OAuth OAUTH_FLOW_TTL: 300_000, OAUTH_TOKEN_EXPIRY_BUFFER: 300_000, diff --git a/packages/browseros-agent/process-compose.yaml b/packages/browseros-agent/process-compose.yaml index 110efc4d8..f9391c67b 100644 --- a/packages/browseros-agent/process-compose.yaml +++ b/packages/browseros-agent/process-compose.yaml @@ -6,20 +6,12 @@ processes: availability: restart: no - build-ext: - command: bun --cwd apps/controller-ext build - availability: - restart: no - depends_on: - install: - condition: process_completed_successfully - server: command: bun --cwd apps/server start availability: restart: always depends_on: - build-ext: + install: condition: process_completed_successfully agent-codegen: diff --git a/packages/browseros-agent/scripts/build/controller-ext.ts b/packages/browseros-agent/scripts/build/controller-ext.ts deleted file mode 100644 index aaede1278..000000000 --- a/packages/browseros-agent/scripts/build/controller-ext.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { execSync } from 'node:child_process' -import { dirname, join } from 'node:path' -import { fileURLToPath } from 'node:url' - -const MONOREPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), '../..') - -console.log('Building controller extension...') -execSync('bun run build:ext', { cwd: MONOREPO_ROOT, stdio: 'inherit' }) diff --git a/packages/browseros-agent/scripts/dev/start.ts b/packages/browseros-agent/scripts/dev/start.ts index 15c851af7..913bc60fb 100644 --- a/packages/browseros-agent/scripts/dev/start.ts +++ b/packages/browseros-agent/scripts/dev/start.ts @@ -12,7 +12,6 @@ type Mode = 'watch' | 'manual' const MONOREPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), '../..') const BROWSEROS_BINARY = '/Applications/BrowserOS.app/Contents/MacOS/BrowserOS' -const CONTROLLER_EXT_DIR = join(MONOREPO_ROOT, 'apps/controller-ext/dist') const AGENT_EXT_DIR = join(MONOREPO_ROOT, 'apps/agent/dist/chrome-mv3-dev') let USER_DATA_DIR = '/tmp/browseros-dev' @@ -201,7 +200,7 @@ function startManualBrowser(ports: Ports): ReturnType { `--browseros-mcp-port=${ports.server}`, `--browseros-extension-port=${ports.extension}`, `--user-data-dir=${USER_DATA_DIR}`, - `--load-extension=${CONTROLLER_EXT_DIR},${AGENT_EXT_DIR}`, + `--load-extension=${AGENT_EXT_DIR}`, 'chrome://newtab', ] @@ -263,8 +262,6 @@ async function main() { const procs: ReturnType[] = [] const streams: Promise[] = [] - buildExtension('controller-ext', 'build:ext') - if (args.mode === 'manual') { buildExtension('agent', 'build:agent:dev') diff --git a/packages/browseros-agent/tools/dev/browser/args.go b/packages/browseros-agent/tools/dev/browser/args.go index 08d71789a..a9fcc5a2f 100644 --- a/packages/browseros-agent/tools/dev/browser/args.go +++ b/packages/browseros-agent/tools/dev/browser/args.go @@ -48,9 +48,8 @@ func BuildArgs(cfg ArgsConfig) []string { ) if cfg.LoadDevExtensions { - controllerExtDir := filepath.Join(cfg.Root, "apps/controller-ext/dist") agentExtDir := filepath.Join(cfg.Root, "apps/agent/dist/chrome-mv3-dev") - args = append(args, fmt.Sprintf("--load-extension=%s,%s", controllerExtDir, agentExtDir)) + args = append(args, fmt.Sprintf("--load-extension=%s", agentExtDir)) args = append(args, "chrome://newtab") } diff --git a/packages/browseros-agent/tools/dev/cmd/test.go b/packages/browseros-agent/tools/dev/cmd/test.go index ebb695d55..fcac00dfd 100644 --- a/packages/browseros-agent/tools/dev/cmd/test.go +++ b/packages/browseros-agent/tools/dev/cmd/test.go @@ -25,13 +25,11 @@ var testCobraCmd = &cobra.Command{ } var ( - testCDPOnly bool testKeep bool testHeadless bool ) func init() { - testCobraCmd.Flags().BoolVar(&testCDPOnly, "cdp-only", false, "Skip waiting for extension connection") testCobraCmd.Flags().BoolVar(&testKeep, "keep", false, "Don't clean up after tests (for debugging)") testCobraCmd.Flags().BoolVar(&testHeadless, "headless", false, "Run BrowserOS headless") rootCmd.AddCommand(testCobraCmd) @@ -55,9 +53,6 @@ func runTest(cmd *cobra.Command, args []string) error { fmt.Println() proc.LogMsgf(proc.TagInfo, "Ports: CDP=%d Server=%d Extension=%d", p.CDP, p.Server, p.Extension) - if testCDPOnly { - proc.LogMsg(proc.TagInfo, "Mode: CDP-only (skipping extension)") - } if testHeadless { proc.LogMsg(proc.TagInfo, "Mode: headless") } @@ -119,7 +114,6 @@ func runTest(cmd *cobra.Command, args []string) error { "bun", filepath.Join(serverDir, "src/index.ts"), "--cdp-port", fmt.Sprintf("%d", p.CDP), "--server-port", fmt.Sprintf("%d", p.Server), - "--extension-port", fmt.Sprintf("%d", p.Extension), }, })) @@ -159,16 +153,6 @@ func runTest(cmd *cobra.Command, args []string) error { } proc.LogMsg(proc.TagBrowser, "CDP ready") - // Wait for extension (unless --cdp-only) - if !testCDPOnly { - proc.LogMsg(proc.TagInfo, "Waiting for extension connection...") - if !server.WaitForExtension(ctx, p.Server, 60) { - cleanup() - return fmt.Errorf("extension failed to connect within timeout") - } - proc.LogMsg(proc.TagInfo, "Extension connected") - } - fmt.Println() proc.LogMsg(proc.TagInfo, proc.BoldColor.Sprint("Test environment ready")) fmt.Println() diff --git a/packages/browseros-agent/tools/dev/cmd/watch.go b/packages/browseros-agent/tools/dev/cmd/watch.go index 44a16b656..624830bd8 100644 --- a/packages/browseros-agent/tools/dev/cmd/watch.go +++ b/packages/browseros-agent/tools/dev/cmd/watch.go @@ -18,7 +18,7 @@ import ( var watchCmd = &cobra.Command{ Use: "watch", Short: "Start the dev environment with process supervision", - Long: "Builds controller-ext, starts agent (WXT HMR or static), waits for CDP, then starts the server.", + Long: "Starts the agent (WXT HMR or static), waits for CDP, then starts the server.", RunE: runWatch, } @@ -100,13 +100,6 @@ func runWatch(cmd *cobra.Command, args []string) error { var wg sync.WaitGroup var procs []*proc.ManagedProc - // Pre-build controller-ext - proc.LogMsg(proc.TagBuild, "Building controller-ext...") - if err := proc.RunBlocking(ctx, root, proc.TagBuild, "bun", "--cwd", "apps/controller-ext", "build"); err != nil { - return fmt.Errorf("controller-ext build failed: %w", err) - } - proc.LogMsg(proc.TagBuild, "controller-ext built") - // Run agent codegen if generated files don't exist agentDir := filepath.Join(root, "apps/agent") if _, err := os.Stat(filepath.Join(agentDir, "generated/graphql")); os.IsNotExist(err) { diff --git a/packages/browseros-agent/tools/dev/server/health.go b/packages/browseros-agent/tools/dev/server/health.go index c7dcee751..4566c8ada 100644 --- a/packages/browseros-agent/tools/dev/server/health.go +++ b/packages/browseros-agent/tools/dev/server/health.go @@ -2,7 +2,6 @@ package server import ( "context" - "encoding/json" "fmt" "net/http" "time" @@ -31,31 +30,3 @@ func WaitForHealth(ctx context.Context, port int, maxAttempts int) bool { } return false } - -func WaitForExtension(ctx context.Context, port int, maxAttempts int) bool { - client := &http.Client{Timeout: time.Second} - url := fmt.Sprintf("http://127.0.0.1:%d/status", port) - - for range maxAttempts { - if ctx.Err() != nil { - return false - } - resp, err := client.Get(url) - if err == nil { - var status struct { - ExtensionConnected bool `json:"extensionConnected"` - } - if err := json.NewDecoder(resp.Body).Decode(&status); err == nil && status.ExtensionConnected { - resp.Body.Close() - return true - } - resp.Body.Close() - } - select { - case <-ctx.Done(): - return false - case <-time.After(500 * time.Millisecond): - } - } - return false -} diff --git a/packages/browseros-agent/tsconfig.json b/packages/browseros-agent/tsconfig.json index ac5fe0f02..f15038f26 100644 --- a/packages/browseros-agent/tsconfig.json +++ b/packages/browseros-agent/tsconfig.json @@ -25,10 +25,7 @@ "declaration": true, "declarationMap": true }, - "references": [ - { "path": "./apps/server" }, - { "path": "./apps/controller-ext" } - ], + "references": [{ "path": "./apps/server" }], "include": [], "exclude": ["node_modules", "dist", "build", "*.config.js"] }