diff --git a/.gitignore b/.gitignore index 37052e6..8a0320a 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,14 @@ dist/ coverage/ .playwright-mcp/ FEATURE_COMPARISON.md +.grepai/ + +# Claude & AI config +.claude/ +.quint/ +.serena/ +.mcp.json + +# Experimental +openspec/ +*.png diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..b05ba31 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,198 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +**surf-cli** is a CLI tool for AI agents to control Chrome browser. It provides a Unix socket-based API that enables any AI agent to automate browser tasks via Chrome DevTools Protocol (CDP), with automatic fallback to chrome.scripting API. + +Architecture: `CLI → Unix Socket (/tmp/surf.sock) → Native Host → Chrome Extension → CDP/Scripting API` + +## Common Commands + +```bash +# Development +npm run dev # Watch mode with live rebuild +npm run build # Production build (Vite → native/*.cjs + dist/) +npm run check # TypeScript type checking +npm run lint # Biome linting +npm run lint:fix # Fix lint issues + +# Testing +npm run test # Run all tests +npm run test:watch # Watch mode +npm run test:coverage # With coverage report + +# Additional scripts +npm run format # Format code with Biome +npm run lint:test # Lint test files only +npm run test:ui # Run tests with Vitest UI + +# Extension loading +npm run install:native # Install native host (requires extension ID from chrome://extensions) +npm run uninstall:native # Uninstall native host + # Binary installs to: ~/.surf/bin/ (add to PATH) +``` + +## Project Structure + +``` +native/ # Compiled CLI and native host (TypeScript → CJS via Vite) + cli.cjs # CLI entry point, argument parsing, socket communication + host.cjs # Native host server, request handling, tool execution + mcp-server.cjs # Model Context Protocol server implementation + *-client.cjs # AI clients (chatgpt, gemini, grok, perplexity, claude, aistudio) + do-*.cjs # Workflow parsing and execution + network-store.cjs # Network request capture and storage + protocol.cjs # Protocol utilities + config.cjs # Config file handling (~/.surf/surf.json) + device-presets.cjs # Device emulation presets +dist/ # Chrome extension (loaded in chrome://extensions) + service-worker/index.js # Main service worker for CDP communication + content/ # Content scripts (accessibility-tree, visual-indicator) + options/ # Extension options page +skills/ # AI agent skill files for surf integration +``` + +## Key Architecture Notes + +### 1. CLI Flow (native/cli.cjs) +- Entry point registered in package.json `bin` +- Commands organized in `TOOLS` groups: ai, batch, bookmark, cookie, dialog, element, emulate, form, frame, history, input, js, locate, nav, network, page, perf, scroll, search, tab, wait, window, workflow, zoom +- `parseArgs()` handles argument parsing +- Sends JSON requests via Unix socket to host + +### 2. Host Flow (native/host.cjs) +- Listens on `/tmp/surf.sock` for incoming requests +- `handleToolRequest()` dispatches tool execution +- `executeBatch()` handles multi-step batch operations +- Automatic retry with exponential backoff for CDP failures +- AI request queue (`processAiQueue`) for sequential AI queries + +### 3. Protocol +- JSON over Unix socket: `{type, method, params, id, tabId, windowId}` +- Response: `{type, id, result, error}` +- Auto-capture screenshots after click, type, scroll operations + +### 4. AI Clients (native/*-client.cjs) +- **ChatGPT** (chatgpt-client.cjs): Uses browser session, supports file attachments +- **Gemini** (gemini-client.cjs): Image generation/editing, YouTube analysis +- **Grok** (grok-client.cjs): X.com integration, model validation +- **Perplexity** (perplexity-client.cjs): Research mode, file attachments +- **Claude** (claude-client.cjs): Claude API integration +- **AI Studio** (aistudio-client.cjs, aistudio-build.cjs): Google AI Studio, app building +- All use browser cookies—no API keys needed + +### 5. MCP Server (native/mcp-server.cjs) +- Implements @modelcontextprotocol/sdk +- Uses StdioServerTransport for stdio-based communication +- Tool schemas defined in TOOL_SCHEMAS constant + +### 6. Workflows +- `do` command parses pipe-separated commands: `'go "url" | click e5 | screenshot'` +- do-parser.cjs: Parses workflow syntax +- do-executor.cjs: Executes steps with auto-waits between operations + +### 7. Network Capture +- Automatic logging to `/tmp/surf/` (configurable via SURF_NETWORK_PATH) +- 24-hour TTL, 200MB max +- Filter by origin, method, status, type + +## Testing + +Tests use Vitest. Run a single test: + +```bash +npm run test -- native/tests/ +``` + +## CLI Aliases + +| Alias | Command | +|-------|---------| +| `snap` | `screenshot` | +| `read` | `page.read` | +| `find` | `search` | +| `go` | `navigate` | +| `net` | `network` | + +## Command Groups + +| Group | Commands | +|-------|----------| +| `workflow` | `do`, `workflow.list`, `workflow.info`, `workflow.validate` | +| `window.*` | `new`, `list`, `focus`, `close`, `resize` | +| `tab.*` | `list`, `new`, `switch`, `close`, `name`, `unname`, `named`, `group`, `ungroup`, `groups`, `reload` | +| `scroll.*` | `top`, `bottom`, `to`, `info` | +| `page.*` | `read`, `text`, `state` | +| `locate.*` | `role`, `text`, `label` | +| `element.*` | `styles` | +| `frame.*` | `list`, `switch`, `main`, `js` | +| `wait.*` | `element`, `network`, `url`, `dom`, `load` | +| `cookie.*` | `list`, `get`, `set`, `clear` | +| `emulate.*` | `network`, `cpu`, `geo`, `device`, `viewport`, `touch` | +| `perf.*` | `start`, `stop`, `metrics` | +| `network.*` | `get`, `body`, `curl`, `origins`, `clear`, `stats`, `export`, `path` | + +## AI Mode (aimode) + +- `surf aimode "query"` - Uses udm=50 (auto mode, has copy button) +- `surf aimode "query" --pro` - Uses nem=143 (pro mode) + +## Process Management + +- Find surf PID: `wmic process where "name='node.exe'" get processid,commandline` +- Kill only surf: `taskkill //F //PID ` (NOT all node processes) + +## Debugging AI Clients + +- Use `surf tab.new` for testing - don't navigate in user's active tab +- Find selectors with: `surf js "document.querySelector('...').outerHTML"` +- Use `surf page.read` to see accessibility tree with element refs + +## Surf Claude Fix History + +- Selectors: `textarea[placeholder*="How can I help you"]`, `button[aria-label="Send message"]`, `.font-claude-response-body` +- Cookie check: accepts `sessionKey`, `anthropic-device-id`, or `ARID` cookies + +## Extension Structure (dist/) + +``` +dist/ + service-worker/index.js # Main service worker, CDP communication + content/ + accessibility-tree.js # Page accessibility tree extraction + visual-indicator.js # Visual element labels overlay + options/ + options.html/js # Extension settings page + icons/ # Extension icons (16, 48, 128px) + manifest.json # Extension manifest +``` + +## Configuration + +- Config location: `~/.surf/surf.json` (user) or `./.surf/` (project) +- Multi-browser support: `chrome`, `chromium`, `brave`, `edge`, `arc`, `helium` + +## MCP Server + +The MCP server (`native/mcp-server.cjs`) provides Model Context Protocol integration: +- Uses `@modelcontextprotocol/sdk` +- Tool schemas defined in `TOOL_SCHEMAS` constant +- Communication via stdio (`StdioServerTransport`) + +## Important Implementation Details + +- Screenshot resize uses `sips` (macOS) or ImageMagick (Linux) +- CDP falls back to `chrome.scripting` API on restricted pages +- Screenshots fallback to `captureVisibleTab` when CDP capture fails +- Element refs (`e1`, `e2`...) are stable identifiers from accessibility tree +- First CDP operation on new tab takes ~100-500ms (debugger attachment) +- Cannot automate `chrome://` pages (Chrome restriction) + +## Troubleshooting + +- Socket not found: Ensure native host is running (`npm run install:native`) +- Extension not loading: Check chrome://extensions for errors +- CDP failures: Ensure debuggable tabs exist +- Permission denied on socket: Check that no other surf instance is running diff --git a/native/aimode-client.cjs b/native/aimode-client.cjs new file mode 100644 index 0000000..cd5060d --- /dev/null +++ b/native/aimode-client.cjs @@ -0,0 +1,370 @@ +/** + * AI Mode (Google) Client for surf-cli + * + * CDP-based client for Google search with AI mode (udm=50=auto, nem=143=pro). + * Uses browser automation to interact with Google's AI search. + */ + +const AIMODE_URL_AUTO = "https://www.google.com/search?udm=50&q="; +const AIMODE_URL_PRO = "https://www.google.com/search?nem=143&q="; + +const SELECTORS = { + searchInput: 'textarea[name="q"], input[name="q"], input[aria-label="Search"]', + resultContainer: '#main, [role="main"], .GybnWb, . Response-container', + answer: '.X7NTVe, .的气, [data-initq], .reply-content, .AdD1h', +}; + +function delay(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +function buildClickDispatcher() { + return `function dispatchClickSequence(target){ + if(!target || !(target instanceof EventTarget)) return false; + const types = ['pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click']; + for (const type of types) { + const common = { bubbles: true, cancelable: true, view: window }; + let event; + if (type.startsWith('pointer') && 'PointerEvent' in window) { + event = new PointerEvent(type, { ...common, pointerId: 1, pointerType: 'mouse' }); + } else { + event = new MouseEvent(type, common); + } + target.dispatchEvent(event); + } + return true; + }`; +} + +function hasRequiredCookies(cookies) { + if (!cookies || !Array.isArray(cookies)) return false; + // Check for Google session cookies + const hasSession = cookies.some(c => + c.name.includes('SID') || + c.name.includes('HSID') || + c.name === '__Secure-1PAPISID' || + c.name === '__Secure-1PSID' + ); + return hasSession || cookies.length > 0; +} + +async function evaluate(cdp, expression) { + const result = await cdp(expression); + if (result.exceptionDetails) { + const desc = result.exceptionDetails.exception?.description || + result.exceptionDetails.text || + "Evaluation failed"; + throw new Error(desc); + } + if (result.error) { + throw new Error(result.error); + } + return result.result?.value; +} + +async function waitForPageLoad(cdp, timeoutMs = 30000) { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + const ready = await evaluate(cdp, "document.readyState"); + if (ready === "complete" || ready === "interactive") { + await delay(500); + return; + } + await delay(100); + } + throw new Error("Page did not load in time"); +} + +async function waitForSearchBox(cdp, timeoutMs = 15000) { + const deadline = Date.now() + timeoutMs; + const selectors = JSON.stringify(SELECTORS.searchInput.split(", ")); + while (Date.now() < deadline) { + const found = await evaluate( + cdp, + `(() => { + const selectors = ${selectors}; + for (const selector of selectors) { + const node = document.querySelector(selector); + if (node && !node.hasAttribute('disabled')) { + return true; + } + } + return false; + })()` + ); + if (found) return true; + await delay(200); + } + return false; +} + +async function typeQuery(cdp, inputCdp, query) { + const selectors = SELECTORS.searchInput.split(", "); + + // Click on search box first + await evaluate( + cdp, + `(() => { + const selectors = ${JSON.stringify(selectors)}; + for (const selector of selectors) { + const node = document.querySelector(selector); + if (node) { + node.focus(); + return true; + } + } + return false; + })()` + ); + + await delay(200); + + // Type the query + await inputCdp("Input.insertText", { text: query }); + + await delay(100); +} + +async function pressEnter(cdp, inputCdp) { + await inputCdp("Input.dispatchKeyEvent", { + type: "keyDown", + key: "Enter", + text: "\r" + }); + await inputCdp("Input.dispatchKeyEvent", { + type: "keyUp", + key: "Enter", + text: "\r" + }); +} + +async function waitForResponse(cdp, timeoutMs, log = () => {}) { + const deadline = Date.now() + timeoutMs; + let lastContent = ""; + let stableCount = 0; + let checkCount = 0; + + while (Date.now() < deadline) { + await delay(1000); + checkCount++; + + const content = await getAnswerContent(cdp, log); + log(`Check ${checkCount}: content length = ${content?.length || 0}`); + + if (content && content !== lastContent) { + lastContent = content; + stableCount = 0; + log(` -> New content found, length: ${content.length}`); + } else if (content && content === lastContent) { + stableCount++; + log(` -> Stable count: ${stableCount}`); + if (stableCount >= 3) { + return { text: content }; + } + } else { + // Check if we're still loading + const isLoading = await evaluate( + cdp, + `(() => { + const spinner = document.querySelector('.pRzye, .l4cyt, [role="progressbar"]'); + return spinner !== null; + })()` + ); + log(` -> Loading: ${isLoading}, lastContent length: ${lastContent.length}`); + if (!isLoading && lastContent) { + return { text: lastContent }; + } + } + } + + // Timeout - return what we have + const content = await getAnswerContent(cdp); + log(`Final content length: ${content?.length || 0}`); + return { text: content || "Response timeout" }; +} + +async function getAnswerContent(cdp, log = () => {}) { + try { + const debugInfo = await evaluate( + cdp, + `(() => { + const info = { + readyState: document.readyState, + url: window.location.href, + aimfl: null, + aimc: null, + main: null, + bodyLength: document.body.textContent.length, + bodyPreview: document.body.textContent.substring(0, 200) + }; + try { info.aimfl = document.querySelector('[data-subtree="aimfl"]')?.textContent?.substring(0, 100); } catch(e) {} + try { info.aimc = document.querySelector('[data-subtree="aimc"]')?.textContent?.substring(0, 100); } catch(e) {} + try { info.main = document.querySelector('main')?.textContent?.substring(0, 100); } catch(e) {} + return info; + })()` + ); + log(`Debug: ${JSON.stringify(debugInfo)}`); + + const result = await evaluate( + cdp, + `(() => { + // Primary: Try data-subtree="aimc" - AI response with UI elements (FULL response) + const aimc = document.querySelector('[data-subtree="aimc"]'); + if (aimc) { + const text = aimc.textContent.trim(); + // Extract just the response before "AI responses may include mistakes" + const idx = text.indexOf('AI responses may include mistakes'); + if (idx > 0) { + return text.substring(0, idx).trim(); + } + return text; + } + + // Secondary: Try data-subtree="aimfl" - clean AI response text (shorter) + const aimfl = document.querySelector('[data-subtree="aimfl"]'); + if (aimfl) { + const text = aimfl.textContent.trim(); + if (text.length > 0) return text; + } + + // Fallback: legacy selectors + const legacySelectors = ['.X7NTVe', '.GybnWb', '.reply-content', '.AdD1h']; + for (const selector of legacySelectors) { + const el = document.querySelector(selector); + if (el) { + return el.textContent.trim(); + } + } + + // Check for any element containing significant text + const main = document.querySelector('main, #main, [role="main"]'); + if (main && main.textContent.length > 100) { + return main.textContent.trim().substring(0, 3000); + } + + // Last resort: body text cleaned + return document.body.textContent.trim().replace(/\\s+/g, ' ').substring(0, 3000); + })()` + ); + + return result || ""; + } catch (err) { + log(`Error in getAnswerContent: ${err.message}`); + // If evaluation fails, return empty to trigger fallback + return ""; + } +} + +async function query(options) { + const { + prompt: query, + pro = false, + timeout = 120000, + getCookies, + createTab, + closeTab, + cdpEvaluate, + cdpCommand, + log = () => {}, + } = options; + + const searchUrl = pro ? AIMODE_URL_PRO : AIMODE_URL_AUTO; + const startTime = Date.now(); + const debugLog = (msg) => { + console.error(`[AIMODE DEBUG] ${msg}`); + log(msg); + }; + + debugLog("Starting aimode query"); + + const { cookies } = await getCookies(); + const cookieNames = cookies?.map(c => c.name) || []; + if (!hasRequiredCookies(cookies)) { + debugLog(`Warning: No Google cookies found. Found: ${cookieNames.join(", ")}`); + } + debugLog(`Got ${cookies.length} cookies`); + + // For aimode, we'll navigate directly to the search URL instead of using CDP + // This avoids issues with tab creation timing + const fullSearchUrl = searchUrl + encodeURIComponent(query); + debugLog(`Will navigate to: ${fullSearchUrl}`); + + // Create a basic tab info - we'll use CDP navigate instead + const tabInfo = await createTab(); + const { tabId } = tabInfo; + if (!tabId) { + throw new Error("Failed to create tab"); + } + debugLog(`Created tab ${tabId}`); + + const cdp = (expr) => cdpEvaluate(tabId, expr); + const inputCdp = (method, params) => cdpCommand(tabId, method, params); + + try { + // Wait for tab to be ready + await delay(2000); + + // Navigate to Google search with the query + debugLog(`Navigating to: ${fullSearchUrl}`); + + // Navigate using CDP + await cdp(`window.location.href = "${fullSearchUrl}"`); + + await waitForPageLoad(cdp); + debugLog("Page loaded"); + + // Wait for AI to finish thinking/loading + debugLog("Waiting for AI thinking to complete..."); + const deadline = Date.now() + 60000; // max 60s wait + let thinking = true; + + while (thinking && Date.now() < deadline) { + const status = await evaluate( + cdp, + `(() => { + // Check if thinking/loading element exists + const loading = document.querySelector('.qewEec, [jsuid*="Creating layout"]'); + const hasAI = document.querySelector('[data-subtree="aimc"]') !== null || + document.querySelector('[data-subtree="aimfl"]') !== null; + return { loading: loading !== null, hasAI: hasAI }; + })()` + ); + debugLog(`Status: loading=${status.loading}, hasAI=${status.hasAI}`); + + if (status.hasAI) { + thinking = false; + debugLog("AI response detected!"); + } else if (!status.loading) { + // No loading, but no AI - might be no response available + debugLog("No loading, checking if AI will respond..."); + await delay(2000); + const retryStatus = await evaluate( + cdp, + `(() => document.querySelector('[data-subtree="aimc"]') !== null || + document.querySelector('[data-subtree="aimfl"]') !== null)` + ); + if (!retryStatus) { + debugLog("No AI response available"); + break; + } + thinking = false; + } else { + await delay(2000); + } + } + + // Get response content + const response = await waitForResponse(cdp, timeout, debugLog); + debugLog(`Response received (${response.text.length} chars)`); + + return { + response: response.text, + url: fullSearchUrl, + tookMs: Date.now() - startTime, + }; + } finally { + await closeTab(tabId).catch(() => {}); + } +} + +module.exports = { query, hasRequiredCookies }; diff --git a/native/chatgpt-client.cjs b/native/chatgpt-client.cjs index aec9aca..c5dbd02 100644 --- a/native/chatgpt-client.cjs +++ b/native/chatgpt-client.cjs @@ -39,7 +39,7 @@ function buildClickDispatcher() { function hasRequiredCookies(cookies) { if (!cookies || !Array.isArray(cookies)) return false; const sessionCookie = cookies.find( - (c) => c.name === "__Secure-next-auth.session-token" && c.value + (c) => c.name.startsWith("__Secure-next-auth.session-token") && c.value ); return Boolean(sessionCookie); } @@ -391,7 +391,7 @@ async function waitForResponse(cdp, timeoutMs = 2700000) { async function query(options) { const { - prompt, + prompt: originalPrompt, model, file, timeout = 2700000, @@ -400,13 +400,17 @@ async function query(options) { closeTab, cdpEvaluate, cdpCommand, + uploadFile, log = () => {}, } = options; + + let prompt = originalPrompt; const startTime = Date.now(); log("Starting ChatGPT query"); const { cookies } = await getCookies(); + const cookieNames = cookies?.map(c => c.name) || []; if (!hasRequiredCookies(cookies)) { - throw new Error("ChatGPT login required"); + throw new Error(`ChatGPT login required. Found ${cookies?.length || 0} cookies: ${cookieNames.join(", ")}`); } log(`Got ${cookies.length} cookies`); const tabInfo = await createTab(); @@ -415,10 +419,10 @@ async function query(options) { throw new Error("Failed to create ChatGPT tab"); } log(`Created tab ${tabId}`); - + const cdp = (expr) => cdpEvaluate(tabId, expr); const inputCdp = (method, params) => cdpCommand(tabId, method, params); - + try { await waitForPageLoad(cdp); log("Page loaded"); @@ -426,8 +430,9 @@ async function query(options) { throw new Error("Cloudflare challenge detected - complete in browser"); } const loginStatus = await checkLoginStatus(cdp); + log(`DEBUG loginStatus: ${JSON.stringify(loginStatus)}`); if (loginStatus.status !== 200 || loginStatus.hasLoginCta) { - throw new Error("ChatGPT login required"); + throw new Error(`ChatGPT login required. loginStatus: ${JSON.stringify(loginStatus)}`); } log("Login verified"); const promptReady = await waitForPromptReady(cdp); @@ -440,7 +445,27 @@ async function query(options) { log(`Selected model: ${selectedLabel}`); } if (file) { - throw new Error("File upload not yet implemented"); + const fs = require("fs"); + const path = require("path"); + + const absolutePath = path.resolve(process.cwd(), file); + if (!fs.existsSync(absolutePath)) { + throw new Error(`File not found: ${file}`); + } + + const fileName = path.basename(absolutePath); + const fileExt = path.extname(absolutePath).toLowerCase(); + + // Text-based extensions only + const textExtensions = [".js", ".ts", ".tsx", ".jsx", ".py", ".java", ".c", ".cpp", ".h", ".hpp", ".go", ".rs", ".rb", ".php", ".html", ".htm", ".css", ".scss", ".less", ".json", ".md", ".txt", ".sh", ".bash", ".zsh", ".yaml", ".yml", ".xml", ".sql", ".gitignore", ".env", ".toml", ".ini", ".cfg", ".conf", ".log", ".csv", ".tsv"]; + + if (!textExtensions.includes(fileExt)) { + throw new Error(`Unsupported file type: ${fileExt}. Only text files are supported.`); + } + + const fileContent = fs.readFileSync(absolutePath, "utf-8"); + prompt = `File: ${fileName}\n\n\`\`\`\n${fileContent}\n\`\`\`\n\n---\n\n${prompt}`; + log(`Attached file: ${fileName} (${fileContent.length} chars)`); } await typePrompt(cdp, inputCdp, prompt); log("Prompt typed"); diff --git a/native/claude-client.cjs b/native/claude-client.cjs new file mode 100644 index 0000000..ba7c833 --- /dev/null +++ b/native/claude-client.cjs @@ -0,0 +1,346 @@ +/** + * Claude Web Client for surf-cli + * + * CDP-based client for claude.ai using browser automation. + * Similar approach to the ChatGPT client. + */ + +const CLAUDE_URL = "https://claude.ai/"; + +const SELECTORS = { + promptTextarea: 'textarea[placeholder*="How can I help you"], textarea[placeholder*="message"], #composer-input, [data-testid="composer-input"], div[contenteditable="true"][role="textbox"]', + sendButton: 'button[aria-label="Send message"], button[data-testid="send-button"], button[type="submit"]', + assistantMessage: '[data-is-streaming="false"], .font-claude-response, [data-turn-author="assistant"]', + stopButton: '[data-testid="stop-button"], button[aria-label="Stop"]', + conversationTurn: '[data-is-streaming="false"], .font-claude-response, [data-turn-author="assistant"]', +}; + +function delay(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +function buildClickDispatcher() { + return `function dispatchClickSequence(target){ + if(!target || !(target instanceof EventTarget)) return false; + const types = ['pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click']; + for (const type of types) { + const common = { bubbles: true, cancelable: true, view: window }; + let event; + if (type.startsWith('pointer') && 'PointerEvent' in window) { + event = new PointerEvent(type, { ...common, pointerId: 1, pointerType: 'mouse' }); + } else { + event = new MouseEvent(type, common); + } + target.dispatchEvent(event); + } + return true; + }`; +} + +function hasRequiredCookies(cookies) { + if (!cookies || !Array.isArray(cookies)) return false; + // Check for session-related cookies (sessionKey, any session cookie) + // or device ID cookies that indicate authenticated session + const validCookie = cookies.find( + (c) => c.value && ( + c.name.includes("session") || + c.name === "anthropic-device-id" || + c.name === "ARID" + ) + ); + return Boolean(validCookie); +} + +async function evaluate(cdp, expression) { + const result = await cdp(expression); + if (result.exceptionDetails) { + const desc = result.exceptionDetails.exception?.description || + result.exceptionDetails.text || + "Evaluation failed"; + throw new Error(desc); + } + if (result.error) { + throw new Error(result.error); + } + return result.result?.value; +} + +async function waitForPageLoad(cdp, timeoutMs = 45000) { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + const ready = await evaluate(cdp, "document.readyState"); + if (ready === "complete" || ready === "interactive") { + return; + } + await delay(100); + } + throw new Error("Page did not load in time"); +} + +async function checkLoginStatus(cdp) { + const result = await evaluate(cdp, `(() => { + const buttons = Array.from(document.querySelectorAll('button, a')); + const hasSignIn = buttons.some(b => { + const text = (b.textContent || '').toLowerCase().trim(); + return text === 'sign in' || text === 'log in' || text.includes('sign in'); + }); + const hasAccount = buttons.some(b => { + const text = (b.textContent || '').toLowerCase(); + const label = (b.getAttribute('aria-label') || '').toLowerCase(); + return text.includes('account') || label.includes('account') || label.includes('profile'); + }); + return { + loggedIn: hasAccount || !hasSignIn, + hasSignIn + }; + })()`); + return result || { loggedIn: false }; +} + +async function waitForPromptReady(cdp, timeoutMs = 20000) { + const deadline = Date.now() + timeoutMs; + const selectors = JSON.stringify(SELECTORS.promptTextarea.split(", ")); + while (Date.now() < deadline) { + const found = await evaluate( + cdp, + `(() => { + const selectors = ${selectors}; + for (const selector of selectors) { + const node = document.querySelector(selector); + if (node && !node.hasAttribute('disabled')) { + return true; + } + } + return false; + })()` + ); + if (found) return true; + await delay(200); + } + return false; +} + +async function typePrompt(cdp, inputCdp, prompt) { + const selectors = SELECTORS.promptTextarea.split(", "); + const textarea = await evaluate( + cdp, + `(() => { + const selectors = ${JSON.stringify(selectors)}; + for (const selector of selectors) { + const node = document.querySelector(selector); + if (node) return selector; + } + return null; + })()` + ); + + if (!textarea) { + throw new Error("Prompt input not found"); + } + + // Use JavaScript to type (more reliable) + await evaluate( + cdp, + `(() => { + const selectors = ${JSON.stringify(selectors)}; + const node = document.querySelector(selectors[0]); + if (!node) return false; + + // Clear existing content + node.value = ''; + + // Focus the input + node.focus(); + + return true; + })()` + ); + + // Type the prompt + await inputCdp("Input.insertText", { text: prompt }); + + // Small delay after typing + await delay(100); +} + +async function clickSend(cdp, inputCdp) { + // Try multiple methods to click send + const clicked = await evaluate( + cdp, + `(() => { + ${buildClickDispatcher()} + const selectors = ${JSON.stringify(SELECTORS.sendButton.split(", "))}; + for (const selector of selectors) { + const btn = document.querySelector(selector); + if (btn) { + dispatchClickSequence(btn); + return true; + } + } + return false; + })()` + ); + + if (!clicked) { + // Fallback: try pressing Enter + await inputCdp("Input.dispatchKeyEvent", { + type: "keyDown", + key: "Enter", + text: "\r" + }); + await inputCdp("Input.dispatchKeyEvent", { + type: "keyUp", + key: "Enter", + text: "\r" + }); + } + + await delay(500); +} + +async function waitForResponse(cdp, timeoutMs) { + const deadline = Date.now() + timeoutMs; + let lastContent = ""; + let stableCount = 0; + + while (Date.now() < deadline) { + // Check if stop button is gone (generation complete) + const isGenerating = await evaluate( + cdp, + `(() => { + const selectors = ${JSON.stringify(SELECTORS.stopButton.split(", "))}; + for (const selector of selectors) { + if (document.querySelector(selector)) return true; + } + return false; + })()` + ); + + if (!isGenerating) { + // Wait a bit more for final content + await delay(1000); + + const content = await getAssistantContent(cdp); + if (content && content !== lastContent) { + lastContent = content; + stableCount = 0; + } else if (content && content === lastContent) { + stableCount++; + if (stableCount >= 3) { + return { text: content }; + } + } + } + + await delay(500); + } + + // Timeout - return what we have + const content = await getAssistantContent(cdp); + return { text: content || "Response timeout" }; +} + +async function getAssistantContent(cdp) { + const result = await evaluate( + cdp, + `(() => { + const selectors = ${JSON.stringify(SELECTORS.conversationTurn.split(", "))}; + let content = ""; + + for (const selector of selectors) { + const elements = document.querySelectorAll(selector); + if (elements.length > 0) { + const lastEl = elements[elements.length - 1]; + // Clone and remove feedback buttons and other UI elements + const clone = lastEl.cloneNode(true); + const removeSelectors = [ + 'button', '[role="button"]', '.feedback', + '[data-testid="feedback"]', '.thumbs-up', '.thumbs-down', + 'div[aria-label="Good response"]', 'div[aria-label="Bad response"]' + ]; + removeSelectors.forEach(sel => { + clone.querySelectorAll(sel).forEach(el => el.remove()); + }); + content = clone.textContent || ""; + break; + } + } + + return content.trim(); + })()` + ); + + return result || ""; +} + +async function query(options) { + const { + prompt: originalPrompt, + model, + timeout = 300000, + getCookies, + createTab, + closeTab, + cdpEvaluate, + cdpCommand, + log = () => {}, + } = options; + + const prompt = originalPrompt; + const startTime = Date.now(); + log("Starting Claude.ai query"); + + const { cookies } = await getCookies(); + const cookieNames = cookies?.map(c => c.name) || []; + if (!hasRequiredCookies(cookies)) { + throw new Error(`Claude.ai login required. Found ${cookies?.length || 0} cookies: ${cookieNames.join(", ")}`); + } + log(`Got ${cookies.length} cookies`); + + const tabInfo = await createTab(); + const { tabId } = tabInfo; + if (!tabId) { + throw new Error("Failed to create Claude.ai tab"); + } + log(`Created tab ${tabId}`); + + const cdp = (expr) => cdpEvaluate(tabId, expr); + const inputCdp = (method, params) => cdpCommand(tabId, method, params); + + try { + await waitForPageLoad(cdp); + log("Page loaded"); + + const loginStatus = await checkLoginStatus(cdp); + log(`DEBUG loginStatus: ${JSON.stringify(loginStatus)}`); + if (!loginStatus.loggedIn) { + throw new Error(`Claude.ai login required. Status: ${JSON.stringify(loginStatus)}`); + } + log("Login verified"); + + const promptReady = await waitForPromptReady(cdp); + if (!promptReady) { + throw new Error("Prompt textarea not ready"); + } + log("Prompt ready"); + + await typePrompt(cdp, inputCdp, prompt); + log("Prompt typed"); + + await clickSend(cdp, inputCdp); + log("Prompt sent, waiting for response..."); + + const response = await waitForResponse(cdp, timeout); + log(`Response received (${response.text.length} chars)`); + + return { + response: response.text, + model: model || "claude-3-5-sonnet", + tookMs: Date.now() - startTime, + }; + } finally { + await closeTab(tabId).catch(() => {}); + } +} + +module.exports = { query, hasRequiredCookies, CLAUDE_URL }; diff --git a/native/cli.cjs b/native/cli.cjs index a090de7..0bce624 100755 --- a/native/cli.cjs +++ b/native/cli.cjs @@ -342,10 +342,10 @@ const TOOLS = { ai: { desc: "AI assistants (ChatGPT, Gemini)", commands: { - "chatgpt": { - desc: "Send prompt to ChatGPT (uses browser cookies)", - args: ["query"], - opts: { + "chatgpt": { + desc: "Send prompt to ChatGPT (uses browser cookies)", + args: ["query"], + opts: { "with-page": "Include current page context", model: "Model: gpt-4o, o1, etc.", file: "Attach file", @@ -358,6 +358,34 @@ const TOOLS = { { cmd: 'chatgpt "analyze" --model gpt-4o', desc: "Specify model" }, ] }, + "claude": { + desc: "Send prompt to Claude AI (uses browser cookies)", + args: ["query"], + opts: { + "with-page": "Include current page context", + model: "Model: claude-3-5-sonnet (default), claude-3-opus, etc.", + timeout: "Timeout in seconds (default: 300)" + }, + examples: [ + { cmd: 'claude "explain quantum computing"', desc: "Basic query" }, + { cmd: 'claude "summarize" --with-page', desc: "With page context" }, + { cmd: 'claude "review this code"', desc: "Code review" }, + ] + }, + "aimode": { + desc: "Query Google AI Mode (udm=50=auto, nem=143=pro)", + args: ["query"], + opts: { + "with-page": "Include current page context", + timeout: "Timeout in seconds (default: 120)", + pro: "Use nem=143 (pro mode) instead of udm=50 (auto)" + }, + examples: [ + { cmd: 'aimode "what is quantum computing"', desc: "Basic query (auto mode)" }, + { cmd: 'aimode "summarize" --with-page', desc: "With page context" }, + { cmd: 'aimode "question" --pro', desc: "Use pro mode (nem=143)" }, + ] + }, "gemini": { desc: "Send prompt to Gemini (uses browser cookies)", args: ["query"], @@ -2414,7 +2442,7 @@ if (args[0] === "workflow.validate") { } } -const BOOLEAN_FLAGS = ["auto-capture", "json", "stream", "dry-run", "stop-on-error", "fail-fast", "clear", "submit", "all", "case-sensitive", "hard", "annotate", "fullpage", "reset", "no-screenshot", "full", "soft-fail", "has-body", "exclude-static", "v", "vv", "request", "by-tab", "har", "jsonl", "no-save", "no-auto-wait"]; +const BOOLEAN_FLAGS = ["auto-capture", "json", "stream", "dry-run", "stop-on-error", "fail-fast", "clear", "submit", "all", "case-sensitive", "hard", "annotate", "fullpage", "reset", "no-screenshot", "full", "soft-fail", "has-body", "exclude-static", "v", "vv", "request", "by-tab", "har", "jsonl", "no-save", "no-auto-wait", "pro"]; const AUTO_SCREENSHOT_TOOLS = ["click", "type", "key", "smart_type", "form.fill", "form_input", "drag", "hover", "scroll", "scroll.top", "scroll.bottom", "scroll.to", "dialog.accept", "dialog.dismiss", "js", "eval"]; @@ -2515,6 +2543,8 @@ const PRIMARY_ARG_MAP = { ai: "query", gemini: "query", chatgpt: "query", + claude: "query", + aimode: "query", perplexity: "query", grok: "query", aistudio: "query", @@ -2900,7 +2930,7 @@ const socket = net.createConnection(SOCKET_PATH, () => { socket.write(JSON.stringify(request) + "\n"); }); -const AI_TOOLS = ["smoke", "chatgpt", "gemini", "perplexity", "grok", "aistudio", "aistudio.build", "ai"]; +const AI_TOOLS = ["smoke", "chatgpt", "claude", "aimode", "gemini", "perplexity", "grok", "aistudio", "aistudio.build", "ai"]; let requestTimeout = AI_TOOLS.includes(tool) ? 300000 : 30000; if (tool === "aistudio.build") { const userTimeoutSec = parseInt(options.timeout || "600", 10); @@ -3195,6 +3225,12 @@ async function handleResponse(response) { meta.push(`${((data.tookMs || 0) / 1000).toFixed(1)}s`); console.error(`\n[${meta.join(' | ')}]`); if (data.url) console.error(`URL: ${data.url}`); + } else if (tool === "aimode" && data?.response) { + console.log(data.response); + const meta = []; + meta.push(`${((data.tookMs || 0) / 1000).toFixed(1)}s`); + console.error(`\n[${meta.join(' | ')}]`); + if (data.url) console.error(`URL: ${data.url}`); } else if (tool === "window.list" && data?.windows) { if (data.windows.length === 0) { console.log("No windows. Use 'surf window.new' to create one."); diff --git a/native/config.cjs b/native/config.cjs index ff50281..f3086aa 100644 --- a/native/config.cjs +++ b/native/config.cjs @@ -21,6 +21,21 @@ const STARTER_CONFIG = { } }; +const COUNCIL_CONFIG = { + defaultProviders: ['chatgpt', 'gemini', 'aimode'], + timeouts: { + chatgpt: 300000, // 5 min + gemini: 180000, // 3 min + aimode: 120000 // 2 min + }, + overallTimeout: 480000, // 8 min + zombieRecovery: { + enabled: true, + maxRetries: 2, + cleanupTimeout: 30000 + } +}; + // Grok models can be customized in surf.json if X.com UI changes: // { // "grok": { @@ -99,4 +114,5 @@ module.exports = { createStarterConfig, clearCache, STARTER_CONFIG, + COUNCIL_CONFIG, }; diff --git a/native/host-helpers.cjs b/native/host-helpers.cjs index c7f24c0..9058b70 100644 --- a/native/host-helpers.cjs +++ b/native/host-helpers.cjs @@ -1020,14 +1020,34 @@ function mapToolToMessage(tool, args, tabId) { return { type: "HISTORY_SEARCH", query: a.query, limit: a.limit !== undefined ? parseInt(a.limit, 10) : 20 }; case "chatgpt": if (!a.query) throw new Error("query required"); - return { - type: "CHATGPT_QUERY", - query: a.query, + return { + type: "CHATGPT_QUERY", + query: a.query, model: a.model, withPage: a["with-page"], file: a.file, timeout: a.timeout ? parseInt(a.timeout, 10) * 1000 : 2700000, - ...baseMsg + ...baseMsg + }; + case "claude": + if (!a.query) throw new Error("query required"); + return { + type: "CLAUDE_QUERY", + query: a.query, + model: a.model, + withPage: a["with-page"], + timeout: a.timeout ? parseInt(a.timeout, 10) * 1000 : 300000, + ...baseMsg + }; + case "aimode": + if (!a.query) throw new Error("query required"); + return { + type: "AIMODE_QUERY", + query: a.query, + withPage: a["with-page"], + pro: a.pro === true, + timeout: a.timeout ? parseInt(a.timeout, 10) * 1000 : 120000, + ...baseMsg }; case "gemini": if (!a.query && !a["generate-image"]) throw new Error("query required"); diff --git a/native/host.cjs b/native/host.cjs index c1c8c90..6e5c790 100755 --- a/native/host.cjs +++ b/native/host.cjs @@ -7,6 +7,8 @@ const https = require("https"); const { execSync } = require("child_process"); const { GoogleGenerativeAI } = require("@google/generative-ai"); const chatgptClient = require("./chatgpt-client.cjs"); +const claudeClient = require("./claude-client.cjs"); +const aimodeClient = require("./aimode-client.cjs"); const geminiClient = require("./gemini-client.cjs"); const perplexityClient = require("./perplexity-client.cjs"); const grokClient = require("./grok-client.cjs"); @@ -534,6 +536,16 @@ function handleToolRequest(msg, socket) { }); writeMessage({ type: "CHATGPT_CDP_COMMAND", tabId, method, params, id: cmdId }); }), + uploadFile: (tabId, filePaths) => new Promise((resolve) => { + const uploadId = ++requestCounter; + pendingToolRequests.set(uploadId, { + socket: null, + originalId: null, + tool: "upload_file", + onComplete: (r) => resolve(r) + }); + writeMessage({ type: "UPLOAD_FILE_TO_TAB", tabId, filePaths, id: uploadId }); + }), log: (msg) => log(`[chatgpt] ${msg}`) }); @@ -644,7 +656,207 @@ function handleToolRequest(msg, socket) { return; } - + + if (extensionMsg.type === "CLAUDE_QUERY") { + const { query, model, withPage, timeout } = extensionMsg; + + queueAiRequest(async () => { + let pageContext = null; + if (withPage) { + const pageResult = await new Promise((resolve) => { + const pageId = ++requestCounter; + pendingToolRequests.set(pageId, { + socket: null, + originalId: null, + tool: "read_page", + onComplete: resolve + }); + writeMessage({ type: "GET_PAGE_TEXT", tabId: extensionMsg.tabId, id: pageId }); + }); + if (pageResult && !pageResult.error) { + pageContext = { + url: pageResult.url, + text: pageResult.text || pageResult.pageContent || "" + }; + } + } + + let fullPrompt = query; + if (pageContext) { + fullPrompt = `Page: ${pageContext.url}\n\n${pageContext.text}\n\n---\n\n${query}`; + } + + const result = await claudeClient.query({ + prompt: fullPrompt, + model, + timeout: timeout || 300000, + getCookies: () => new Promise((resolve) => { + const cookieId = ++requestCounter; + pendingToolRequests.set(cookieId, { + socket: null, + originalId: null, + tool: "get_cookies", + onComplete: (r) => resolve(r) + }); + writeMessage({ type: "GET_CLAUDE_COOKIES", id: cookieId }); + }), + createTab: () => new Promise((resolve) => { + const tabCreateId = ++requestCounter; + pendingToolRequests.set(tabCreateId, { + socket: null, + originalId: null, + tool: "create_tab", + onComplete: (r) => resolve(r) + }); + writeMessage({ type: "CLAUDE_NEW_TAB", id: tabCreateId }); + }), + closeTab: (tabIdToClose) => new Promise((resolve) => { + const tabCloseId = ++requestCounter; + pendingToolRequests.set(tabCloseId, { + socket: null, + originalId: null, + tool: "close_tab", + onComplete: (r) => resolve(r) + }); + writeMessage({ type: "CLAUDE_CLOSE_TAB", tabId: tabIdToClose, id: tabCloseId }); + }), + cdpEvaluate: (tabId, expression) => new Promise((resolve) => { + const evalId = ++requestCounter; + pendingToolRequests.set(evalId, { + socket: null, + originalId: null, + tool: "cdp_evaluate", + onComplete: (r) => resolve(r) + }); + writeMessage({ type: "CLAUDE_EVALUATE", tabId, expression, id: evalId }); + }), + cdpCommand: (tabId, method, params) => new Promise((resolve) => { + const cmdId = ++requestCounter; + pendingToolRequests.set(cmdId, { + socket: null, + originalId: null, + tool: "cdp_command", + onComplete: (r) => resolve(r) + }); + writeMessage({ type: "CLAUDE_CDP_COMMAND", tabId, method, params, id: cmdId }); + }), + log: (msg) => log(`[claude] ${msg}`) + }); + + return result; + }).then((result) => { + sendToolResponse(socket, originalId, { + response: result.response, + model: result.model, + tookMs: result.tookMs + }, null); + }).catch((err) => { + sendToolResponse(socket, originalId, null, err.message); + }); + + return; + } + + if (extensionMsg.type === "AIMODE_QUERY") { + const { query, withPage, pro, timeout } = extensionMsg; + + queueAiRequest(async () => { + let pageContext = null; + if (withPage) { + const pageResult = await new Promise((resolve) => { + const pageId = ++requestCounter; + pendingToolRequests.set(pageId, { + socket: null, + originalId: null, + tool: "read_page", + onComplete: resolve + }); + writeMessage({ type: "GET_PAGE_TEXT", tabId: extensionMsg.tabId, id: pageId }); + }); + if (pageResult && !pageResult.error) { + pageContext = { + url: pageResult.url, + text: pageResult.text || pageResult.pageContent || "" + }; + } + } + + let fullPrompt = query; + if (pageContext) { + fullPrompt = `Page: ${pageContext.url}\n\n${pageContext.text}\n\n---\n\n${query}`; + } + + const result = await aimodeClient.query({ + prompt: fullPrompt, + pro: pro || false, + timeout: timeout || 120000, + getCookies: () => new Promise((resolve) => { + const cookieId = ++requestCounter; + pendingToolRequests.set(cookieId, { + socket: null, + originalId: null, + tool: "get_cookies", + onComplete: (r) => resolve(r) + }); + writeMessage({ type: "GET_GOOGLE_COOKIES", id: cookieId }); + }), + createTab: () => new Promise((resolve) => { + const tabCreateId = ++requestCounter; + pendingToolRequests.set(tabCreateId, { + socket: null, + originalId: null, + tool: "create_tab", + onComplete: (r) => resolve(r) + }); + writeMessage({ type: "AIMODE_NEW_TAB", pro: pro || false, id: tabCreateId }); + }), + closeTab: (tabIdToClose) => new Promise((resolve) => { + const tabCloseId = ++requestCounter; + pendingToolRequests.set(tabCloseId, { + socket: null, + originalId: null, + tool: "close_tab", + onComplete: (r) => resolve(r) + }); + writeMessage({ type: "AIMODE_CLOSE_TAB", tabId: tabIdToClose, id: tabCloseId }); + }), + cdpEvaluate: (tabId, expression) => new Promise((resolve) => { + const evalId = ++requestCounter; + pendingToolRequests.set(evalId, { + socket: null, + originalId: null, + tool: "cdp_evaluate", + onComplete: (r) => resolve(r) + }); + writeMessage({ type: "AIMODE_EVALUATE", tabId, expression, id: evalId }); + }), + cdpCommand: (tabId, method, params) => new Promise((resolve) => { + const cmdId = ++requestCounter; + pendingToolRequests.set(cmdId, { + socket: null, + originalId: null, + tool: "cdp_command", + onComplete: (r) => resolve(r) + }); + writeMessage({ type: "AIMODE_CDP_COMMAND", tabId, method, params, id: cmdId }); + }), + log: (msg) => log(`[aimode] ${msg}`) + }); + + return result; + }).then((result) => { + sendToolResponse(socket, originalId, { + response: result.response, + url: result.url, + tookMs: result.tookMs + }, null); + }).catch((err) => { + sendToolResponse(socket, originalId, null, err.message); + }); + + return; + } + if (extensionMsg.type === "GEMINI_QUERY") { const { query, model, withPage, file, generateImage, editImage, output, youtube, aspectRatio, timeout } = extensionMsg; diff --git a/package-lock.json b/package-lock.json index b2871c0..a860b8d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "surf-cli", - "version": "2.6.0", + "version": "2.7.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "surf-cli", - "version": "2.6.0", + "version": "2.7.1", "license": "MIT", "dependencies": { "@google/generative-ai": "^0.24.1", @@ -23,6 +23,7 @@ }, "devDependencies": { "@biomejs/biome": "^2.4.4", + "@crxjs/vite-plugin": "^2.3.0", "@types/chrome": "^0.1.37", "@vitest/coverage-v8": "^4.0.18", "@vitest/ui": "^4.0.18", @@ -254,6 +255,81 @@ "node": ">=14.21.3" } }, + "node_modules/@crxjs/vite-plugin": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@crxjs/vite-plugin/-/vite-plugin-2.3.0.tgz", + "integrity": "sha512-+0CNVGS4bB30OoaF1vUsHVwWU1Lm7MxI0XWY9Fd/Ob+ZVTZgEFNqJ1ZC69IVwQsoYhY0sMQLvpLWiFIuDz8htg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^4.1.2", + "@webcomponents/custom-elements": "^1.5.0", + "acorn-walk": "^8.2.0", + "cheerio": "^1.0.0-rc.10", + "convert-source-map": "^1.7.0", + "debug": "^4.3.3", + "es-module-lexer": "^0.10.0", + "fast-glob": "^3.2.11", + "fs-extra": "^10.0.1", + "jsesc": "^3.0.2", + "magic-string": "^0.30.12", + "pathe": "^2.0.1", + "picocolors": "^1.1.1", + "react-refresh": "^0.13.0", + "rollup": "2.79.2", + "rxjs": "7.5.7" + } + }, + "node_modules/@crxjs/vite-plugin/node_modules/@rollup/pluginutils": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", + "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "estree-walker": "^2.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/@crxjs/vite-plugin/node_modules/es-module-lexer": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.10.5.tgz", + "integrity": "sha512-+7IwY/kiGAacQfY+YBhKMvEmyAJnw5grTUgjG85Pe7vcUI/6b7pZjZG8nQ7+48YhzEAEqrEgD2dCz/JIK+AYvw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@crxjs/vite-plugin/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@crxjs/vite-plugin/node_modules/rollup": { + "version": "2.79.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", + "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", + "dev": true, + "license": "MIT", + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", @@ -758,6 +834,44 @@ } } }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@polka/url": { "version": "1.0.0-next.29", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", @@ -1336,6 +1450,13 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@webcomponents/custom-elements": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@webcomponents/custom-elements/-/custom-elements-1.6.0.tgz", + "integrity": "sha512-CqTpxOlUCPWRNUPZDxT5v2NnHXA4oox612iUGnmTUGQFhZ1Gkj8kirtl/2wcF6MqX7+PqqicZzOCBKKfIn0dww==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -1349,6 +1470,32 @@ "node": ">= 0.6" } }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/ajv": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", @@ -1509,6 +1656,26 @@ "url": "https://opencollective.com/express" } }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/brorand": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", @@ -1706,6 +1873,50 @@ "node": ">=18" } }, + "node_modules/cheerio": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", + "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.1.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.19.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=20.18.1" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/cipher-base": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.7.tgz", @@ -1753,6 +1964,13 @@ "node": ">= 0.6" } }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true, + "license": "MIT" + }, "node_modules/cookie": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", @@ -1879,6 +2097,36 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1966,6 +2214,21 @@ "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", "license": "MIT" }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, "node_modules/domain-browser": { "version": "4.22.0", "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-4.22.0.tgz", @@ -1978,6 +2241,50 @@ "url": "https://bevry.me/fund" } }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -2028,6 +2335,46 @@ "node": ">= 0.8" } }, + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/encoding-sniffer/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -2245,6 +2592,23 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "license": "MIT" }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, "node_modules/fast-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", @@ -2261,6 +2625,16 @@ ], "license": "BSD-3-Clause" }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -2285,6 +2659,19 @@ "dev": true, "license": "MIT" }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/finalhandler": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", @@ -2362,6 +2749,21 @@ "node": ">= 0.8" } }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2431,6 +2833,19 @@ "node": ">= 0.4" } }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -2443,6 +2858,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -2555,6 +2977,39 @@ "dev": true, "license": "MIT" }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -2684,6 +3139,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-generator-function": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", @@ -2703,6 +3168,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-nan": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", @@ -2719,6 +3197,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/is-promise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", @@ -2834,6 +3322,19 @@ "dev": true, "license": "MIT" }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -2846,6 +3347,19 @@ "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", "license": "BSD-2-Clause" }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -2939,6 +3453,43 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/miller-rabin": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", @@ -3114,6 +3665,19 @@ "node": ">= 6" } }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -3270,6 +3834,59 @@ "node": ">= 0.10" } }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -3496,6 +4113,27 @@ "node": ">=0.4.x" } }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -3539,6 +4177,16 @@ "node": ">= 0.10" } }, + "node_modules/react-refresh": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.13.0.tgz", + "integrity": "sha512-XP8A9BT0CpRBD+NYLLeIhld/RqG9+gktUjW1FkE+Vm7OCinbG1SshcK5tb9ls4kzvjZr9mOQc7HYgBngEyPAXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", @@ -3589,6 +4237,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, "node_modules/ripemd160": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.3.tgz", @@ -3675,6 +4334,40 @@ "node": ">= 18" } }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.7.tgz", + "integrity": "sha512-z9MzKh/UcOqB3i20H6rtrlaE/CgjLOvheWK/9ILrbhROGTweAi1BaFsTT9FbwZi5Trr1qNRs+MXkhmR06awzQA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -4137,6 +4830,19 @@ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", "license": "MIT" }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -4156,6 +4862,13 @@ "node": ">=6" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, "node_modules/tty-browserify": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz", @@ -4204,6 +4917,26 @@ "node": ">=14.17" } }, + "node_modules/undici": { + "version": "7.22.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.22.0.tgz", + "integrity": "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -4430,6 +5163,43 @@ "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", "license": "MIT" }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 3eac59f..7cbf273 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ }, "devDependencies": { "@biomejs/biome": "^2.4.4", + "@crxjs/vite-plugin": "^2.3.0", "@types/chrome": "^0.1.37", "@vitest/coverage-v8": "^4.0.18", "@vitest/ui": "^4.0.18", diff --git a/skills/surf-council/recovery/socket-health.cjs b/skills/surf-council/recovery/socket-health.cjs new file mode 100644 index 0000000..fd0c445 --- /dev/null +++ b/skills/surf-council/recovery/socket-health.cjs @@ -0,0 +1,97 @@ +/** + * Socket Health Check Module + * Checks if the surf socket is responsive by running 'surf window list' + * with a 10 second timeout. + */ + +const { spawn } = require("child_process"); + +const DEFAULT_TIMEOUT_MS = 10000; + +/** + * Check if the surf socket is healthy by spawning 'surf window list' + * @param {string} surfCommand - The surf command to run (default: 'surf') + * @returns {Promise<{healthy: boolean, windows?: Array, reason?: string}>} + */ +async function isSocketHealthy(surfCommand = "surf") { + return new Promise((resolve) => { + let resolved = false; + let stdout = ""; + let stderr = ""; + + const timeout = setTimeout(() => { + if (!resolved) { + resolved = true; + cleanup(); + resolve({ healthy: false, reason: "timeout" }); + } + }, DEFAULT_TIMEOUT_MS); + + const child = spawn(surfCommand, ["window", "list"], { + stdio: ["ignore", "pipe", "pipe"], + windowsHide: true, + }); + + child.stdout.on("data", (data) => { + stdout += data.toString(); + }); + + child.stderr.on("data", (data) => { + stderr += data.toString(); + }); + + child.on("error", (err) => { + if (!resolved) { + resolved = true; + clearTimeout(timeout); + cleanup(); + resolve({ healthy: false, reason: err.message }); + } + }); + + child.on("close", (code) => { + if (resolved) return; + resolved = true; + clearTimeout(timeout); + cleanup(); + + if (code !== 0) { + // Non-zero exit code indicates failure + const errorMsg = stderr.trim() || `exit code ${code}`; + resolve({ healthy: false, reason: errorMsg }); + return; + } + + // Try to parse the output as JSON + try { + // The window list output is JSON: { windows: [...] } + // But the CLI may also print non-JSON text, so find the JSON part + const jsonMatch = stdout.match(/\{[\s\S]*\}/); + if (jsonMatch) { + const parsed = JSON.parse(jsonMatch[0]); + if (parsed.windows !== undefined) { + resolve({ healthy: true, windows: parsed.windows }); + return; + } + } + // If no JSON found or no windows key, treat as healthy with empty result + resolve({ healthy: true, windows: [] }); + } catch { + // Parse error - socket might be responding but returning unexpected format + // This could indicate a stuck or corrupted state + const errorMsg = stderr.trim() || "invalid response format"; + resolve({ healthy: false, reason: errorMsg }); + } + }); + + function cleanup() { + try { + child.kill(); + } catch { + // Ignore kill errors + } + } + }); +} + +module.exports = { isSocketHealthy }; diff --git a/src/service-worker/index.ts b/src/service-worker/index.ts index 01b79a6..cdf0645 100644 --- a/src/service-worker/index.ts +++ b/src/service-worker/index.ts @@ -2621,13 +2621,17 @@ export async function handleMessage( } case "CHATGPT_NEW_TAB": { - const tab = await chrome.tabs.create({ + // Open in new window (background, no focus steal) + const window = await chrome.windows.create({ url: "https://chatgpt.com/", - active: true, + focused: false, + type: "normal", }); - if (!tab.id) throw new Error("Failed to create tab"); - const currentTab = await chrome.tabs.get(tab.id); - if (currentTab.status !== "complete") { + if (!window.id) throw new Error("Failed to create window"); + const tabs = await chrome.tabs.query({ windowId: window.id }); + const tab = tabs[0]; + if (!tab?.id) throw new Error("Failed to get tab from window"); + if (tab.status !== "complete") { await new Promise((resolve) => { const listener = (tabId: number, info: chrome.tabs.TabChangeInfo) => { if (tabId === tab.id && info.status === "complete") { @@ -2643,7 +2647,6 @@ export async function handleMessage( }); } await cdp.attach(tab.id); - // Wait for JS runtime to be ready after CDP attach await waitForRuntimeReady(tab.id, 10000); return { tabId: tab.id }; } @@ -2672,14 +2675,73 @@ export async function handleMessage( return result; } + case "CLAUDE_NEW_TAB": { + // Open in new window (background, no focus steal) + const window = await chrome.windows.create({ + url: "https://claude.ai/", + focused: false, + type: "normal", + }); + if (!window.id) throw new Error("Failed to create window"); + const tabs = await chrome.tabs.query({ windowId: window.id }); + const tab = tabs[0]; + if (!tab?.id) throw new Error("Failed to get tab from window"); + if (tab.status !== "complete") { + await new Promise((resolve) => { + const listener = (tabId: number, info: chrome.tabs.TabChangeInfo) => { + if (tabId === tab.id && info.status === "complete") { + chrome.tabs.onUpdated.removeListener(listener); + resolve(); + } + }; + chrome.tabs.onUpdated.addListener(listener); + setTimeout(() => { + chrome.tabs.onUpdated.removeListener(listener); + resolve(); + }, 30000); + }); + } + await cdp.attach(tab.id); + await waitForRuntimeReady(tab.id, 10000); + return { tabId: tab.id }; + } + + case "CLAUDE_CLOSE_TAB": { + const claudeTabId = message.tabId; + if (claudeTabId) { + try { + await cdp.detach(claudeTabId); + } catch {} + try { + await chrome.tabs.remove(claudeTabId); + } catch {} + } + return { success: true }; + } + + case "CLAUDE_CDP_COMMAND": { + const { method, params } = message; + const result = await cdp.sendCommand(message.tabId, method, params || {}); + return result; + } + + case "CLAUDE_EVALUATE": { + const result = await cdp.evaluateScript(message.tabId, message.expression); + return result; + } + case "PERPLEXITY_NEW_TAB": { - const tab = await chrome.tabs.create({ + // Open in new window (background, no focus steal) + const window = await chrome.windows.create({ url: "https://www.perplexity.ai/", - active: true, + focused: false, + type: "normal", }); - if (!tab.id) throw new Error("Failed to create tab"); - const currentTab = await chrome.tabs.get(tab.id); - if (currentTab.status !== "complete") { + if (!window.id) throw new Error("Failed to create window"); + const tabs = await chrome.tabs.query({ windowId: window.id }); + const tab = tabs[0]; + if (!tab?.id) throw new Error("Failed to get tab from window"); + if (tab.status !== "complete") { await new Promise((resolve) => { const listener = (tabId: number, info: chrome.tabs.TabChangeInfo) => { if (tabId === tab.id && info.status === "complete") { @@ -2695,7 +2757,6 @@ export async function handleMessage( }); } await cdp.attach(tab.id); - // Wait for JS runtime to be ready after CDP attach await waitForRuntimeReady(tab.id, 10000); return { tabId: tab.id }; } @@ -2758,13 +2819,17 @@ export async function handleMessage( } case "GROK_NEW_TAB": { - const tab = await chrome.tabs.create({ + // Open in new window (background, no focus steal) + const window = await chrome.windows.create({ url: "https://x.com/i/grok", - active: true, + focused: false, + type: "normal", }); - if (!tab.id) throw new Error("Failed to create tab"); - const currentTab = await chrome.tabs.get(tab.id); - if (currentTab.status !== "complete") { + if (!window.id) throw new Error("Failed to create window"); + const tabs = await chrome.tabs.query({ windowId: window.id }); + const tab = tabs[0]; + if (!tab?.id) throw new Error("Failed to get tab from window"); + if (tab.status !== "complete") { await new Promise((resolve) => { const listener = (tabId: number, info: chrome.tabs.TabChangeInfo) => { if (tabId === tab.id && info.status === "complete") { @@ -2780,7 +2845,6 @@ export async function handleMessage( }); } await cdp.attach(tab.id); - // Wait for JS runtime to be ready after CDP attach await waitForRuntimeReady(tab.id, 10000); return { tabId: tab.id }; } @@ -2810,11 +2874,16 @@ export async function handleMessage( } case "GEMINI_NEW_TAB": { - const tab = await chrome.tabs.create({ + // Open in new window (background, no focus steal) + const window = await chrome.windows.create({ url: "https://gemini.google.com/app", - active: true, + focused: false, + type: "normal", }); - if (!tab.id) throw new Error("Failed to create tab"); + if (!window.id) throw new Error("Failed to create window"); + const tabs = await chrome.tabs.query({ windowId: window.id }); + const tab = tabs[0]; + if (!tab?.id) throw new Error("Failed to get tab from window"); return { tabId: tab.id }; } @@ -2931,10 +3000,17 @@ export async function handleMessage( case "AISTUDIO_NEW_TAB": { const url = message.url || "https://aistudio.google.com/prompts/new_chat"; - const tab = await chrome.tabs.create({ url, active: true }); - if (!tab.id) throw new Error("Failed to create tab"); - const currentTab = await chrome.tabs.get(tab.id); - if (currentTab.status !== "complete") { + // Open in new window (background, no focus steal) + const window = await chrome.windows.create({ + url: url, + focused: false, + type: "normal", + }); + if (!window.id) throw new Error("Failed to create window"); + const tabs = await chrome.tabs.query({ windowId: window.id }); + const tab = tabs[0]; + if (!tab?.id) throw new Error("Failed to get tab from window"); + if (tab.status !== "complete") { await new Promise((resolve) => { const listener = (tabId: number, info: chrome.tabs.TabChangeInfo) => { if (tabId === tab.id && info.status === "complete") { @@ -2972,6 +3048,56 @@ export async function handleMessage( return await cdp.evaluateScript(message.tabId, message.expression); } + case "AIMODE_NEW_TAB": { + // Open in new window (background, no focus steal) + const window = await chrome.windows.create({ + url: message.pro + ? "https://www.google.com/search?nem=143&q=" + : "https://www.google.com/search?udm=50&q=", + focused: false, + type: "normal", + }); + if (!window.id) throw new Error("Failed to create window"); + const tabs = await chrome.tabs.query({ windowId: window.id }); + const tab = tabs[0]; + if (!tab?.id) throw new Error("Failed to get tab from window"); + if (tab.status !== "complete") { + await new Promise((resolve) => { + const listener = (tabId: number, info: chrome.tabs.TabChangeInfo) => { + if (tabId === tab.id && info.status === "complete") { + chrome.tabs.onUpdated.removeListener(listener); + resolve(); + } + }; + chrome.tabs.onUpdated.addListener(listener); + setTimeout(() => { + chrome.tabs.onUpdated.removeListener(listener); + resolve(); + }, 30000); + }); + } + await cdp.attach(tab.id); + await waitForRuntimeReady(tab.id, 10000); + return { tabId: tab.id }; + } + + case "AIMODE_CLOSE_TAB": { + if (message.tabId) { + await cdp.detach(message.tabId); + await chrome.tabs.remove(message.tabId); + } + return { success: true }; + } + + case "AIMODE_CDP_COMMAND": { + const { method, params } = message; + return await cdp.sendCommand(message.tabId, method, params || {}); + } + + case "AIMODE_EVALUATE": { + return await cdp.evaluateScript(message.tabId, message.expression); + } + case "DOWNLOADS_SEARCH": { const results = await chrome.downloads.search(message.searchParams || {}); return { @@ -3019,6 +3145,12 @@ export async function handleMessage( return { cookies: Array.from(seen.values()) }; } + case "GET_CLAUDE_COOKIES": { + const cookies = await chrome.cookies.getAll({ domain: ".claude.ai" }); + const anthropicCookies = await chrome.cookies.getAll({ domain: ".anthropic.com" }); + return { cookies: [...cookies, ...anthropicCookies] }; + } + case "WINDOW_NEW": { // Default to a usable blank page if no URL provided const url = message.url || 'data:text/html,Surf Agent

Agent Window

Ready for automation

'; @@ -3127,14 +3259,17 @@ const COMMANDS_WITHOUT_TAB = new Set([ "LIST_TABS", "NEW_TAB", "TABS_NEW", "CLOSE_TABS", "SWITCH_TAB", "TABS_SWITCH", "TABS_REGISTER", "TABS_UNREGISTER", "TABS_LIST_NAMED", "TABS_GET_BY_NAME", "CREATE_TAB_GROUP", "UNGROUP_TABS", "LIST_TAB_GROUPS", "GET_HISTORY", "SEARCH_HISTORY", - "GET_COOKIES", "SET_COOKIE", "DELETE_COOKIES", "GET_BOOKMARKS", "ADD_BOOKMARK", + "GET_COOKIES", "SET_COOKIE", "DELETE_COOKIES", "GET_BOOKMARKS", "ADD_BOOKMARK", "DELETE_BOOKMARK", "DIALOG_DISMISS", "DIALOG_ACCEPT", "DIALOG_INFO", "CHATGPT_NEW_TAB", "CHATGPT_CLOSE_TAB", "CHATGPT_EVALUATE", "CHATGPT_CDP_COMMAND", "GET_CHATGPT_COOKIES", "GET_GOOGLE_COOKIES", "GET_TWITTER_COOKIES", + "CLAUDE_NEW_TAB", "CLAUDE_CLOSE_TAB", "CLAUDE_EVALUATE", "CLAUDE_CDP_COMMAND", + "GET_CLAUDE_COOKIES", "PERPLEXITY_NEW_TAB", "PERPLEXITY_CLOSE_TAB", "PERPLEXITY_EVALUATE", "PERPLEXITY_CDP_COMMAND", "GROK_NEW_TAB", "GROK_CLOSE_TAB", "GROK_EVALUATE", "GROK_CDP_COMMAND", "GEMINI_NEW_TAB", "GEMINI_CLOSE_TAB", "GEMINI_FETCH_URL", "UPLOAD_FILE_TO_TAB", "AISTUDIO_NEW_TAB", "AISTUDIO_CLOSE_TAB", "AISTUDIO_EVALUATE", "AISTUDIO_CDP_COMMAND", + "AIMODE_NEW_TAB", "AIMODE_CLOSE_TAB", "AIMODE_EVALUATE", "AIMODE_CDP_COMMAND", "DOWNLOADS_SEARCH", "WINDOW_NEW", "WINDOW_LIST", "WINDOW_FOCUS", "WINDOW_CLOSE", "WINDOW_RESIZE", "EMULATE_DEVICE_LIST"