diff --git a/README.md b/README.md index 873f944..81f8564 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,7 @@ Run `opencli list` for the live registry. | Site | Commands | Mode | |------|----------|------| -| **twitter** | `trending` `bookmarks` `profile` `search` `timeline` `thread` `following` `followers` `notifications` `post` `reply` `delete` `like` `article` `follow` `unfollow` `bookmark` `unbookmark` `download` | ๐Ÿ” Browser | +| **twitter** | `trending` `bookmarks` `profile` `search` `timeline` `thread` `following` `followers` `notifications` `post` `reply` `delete` `like` `article` `follow` `unfollow` `bookmark` `unbookmark` `download` `accept` `reply-dm` | ๐Ÿ” Browser | | **reddit** | `hot` `frontpage` `popular` `search` `subreddit` `read` `user` `user-posts` `user-comments` `upvote` `save` `comment` `subscribe` `saved` `upvoted` | ๐Ÿ” Browser | | **cursor** | `status` `send` `read` `new` `dump` `composer` `model` `extract-code` `ask` `screenshot` `history` `export` | ๐Ÿ–ฅ๏ธ Desktop | | **bilibili** | `hot` `search` `me` `favorite` `history` `feed` `subtitle` `dynamic` `ranking` `following` `user-videos` `download` | ๐Ÿ” Browser | diff --git a/README.zh-CN.md b/README.zh-CN.md index 85b5cd7..dc33919 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -107,7 +107,7 @@ npm install -g @jackwener/opencli@latest | ็ซ™็‚น | ๅ‘ฝไปค | ๆจกๅผ | |------|------|------| -| **twitter** | `trending` `bookmarks` `profile` `search` `timeline` `thread` `following` `followers` `notifications` `post` `reply` `delete` `like` `article` `follow` `unfollow` `bookmark` `unbookmark` `download` | ๐Ÿ” ๆต่งˆๅ™จ | +| **twitter** | `trending` `bookmarks` `profile` `search` `timeline` `thread` `following` `followers` `notifications` `post` `reply` `delete` `like` `article` `follow` `unfollow` `bookmark` `unbookmark` `download` `accept` `reply-dm` | ๐Ÿ” ๆต่งˆๅ™จ | | **reddit** | `hot` `frontpage` `popular` `search` `subreddit` `read` `user` `user-posts` `user-comments` `upvote` `save` `comment` `subscribe` `saved` `upvoted` | ๐Ÿ” ๆต่งˆๅ™จ | | **cursor** | `status` `send` `read` `new` `dump` `composer` `model` `extract-code` `ask` `screenshot` `history` `export` | ๐Ÿ–ฅ๏ธ ๆกŒ้ข็ซฏ | | **bilibili** | `hot` `search` `me` `favorite` `history` `feed` `subtitle` `dynamic` `ranking` `following` `user-videos` `download` | ๐Ÿ” ๆต่งˆๅ™จ | diff --git a/extension/dist/background.js b/extension/dist/background.js index 79fee83..7144422 100644 --- a/extension/dist/background.js +++ b/extension/dist/background.js @@ -1,337 +1,425 @@ -const DAEMON_PORT = 19825; -const DAEMON_HOST = "localhost"; -const DAEMON_WS_URL = `ws://${DAEMON_HOST}:${DAEMON_PORT}/ext`; -const WS_RECONNECT_BASE_DELAY = 2e3; -const WS_RECONNECT_MAX_DELAY = 6e4; - -const attached = /* @__PURE__ */ new Set(); +//#region src/protocol.ts +/** Default daemon port */ +var DAEMON_PORT = 19825; +var DAEMON_HOST = "localhost"; +var DAEMON_WS_URL = `ws://${DAEMON_HOST}:${DAEMON_PORT}/ext`; +`${DAEMON_HOST}${DAEMON_PORT}`; +/** Base reconnect delay for extension WebSocket (ms) */ +var WS_RECONNECT_BASE_DELAY = 2e3; +/** Max reconnect delay (ms) */ +var WS_RECONNECT_MAX_DELAY = 6e4; +//#endregion +//#region src/cdp.ts +/** +* CDP execution via chrome.debugger API. +* +* chrome.debugger only needs the "debugger" permission โ€” no host_permissions. +* It can attach to any http/https tab. Avoid chrome:// and chrome-extension:// +* tabs (resolveTabId in background.ts filters them). +*/ +var attached = /* @__PURE__ */ new Set(); async function ensureAttached(tabId) { - if (attached.has(tabId)) return; - try { - await chrome.debugger.attach({ tabId }, "1.3"); - } catch (e) { - const msg = e instanceof Error ? e.message : String(e); - if (msg.includes("Another debugger is already attached")) { - try { - await chrome.debugger.detach({ tabId }); - } catch { - } - try { - await chrome.debugger.attach({ tabId }, "1.3"); - } catch { - throw new Error(`attach failed: ${msg}`); - } - } else { - throw new Error(`attach failed: ${msg}`); - } - } - attached.add(tabId); - try { - await chrome.debugger.sendCommand({ tabId }, "Runtime.enable"); - } catch { - } + if (attached.has(tabId)) return; + try { + await chrome.debugger.attach({ tabId }, "1.3"); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + if (msg.includes("Another debugger is already attached")) { + try { + await chrome.debugger.detach({ tabId }); + } catch {} + try { + await chrome.debugger.attach({ tabId }, "1.3"); + } catch { + throw new Error(`attach failed: ${msg}`); + } + } else throw new Error(`attach failed: ${msg}`); + } + attached.add(tabId); + try { + await chrome.debugger.sendCommand({ tabId }, "Runtime.enable"); + } catch {} } async function evaluate(tabId, expression) { - await ensureAttached(tabId); - const result = await chrome.debugger.sendCommand({ tabId }, "Runtime.evaluate", { - expression, - returnByValue: true, - awaitPromise: true - }); - if (result.exceptionDetails) { - const errMsg = result.exceptionDetails.exception?.description || result.exceptionDetails.text || "Eval error"; - throw new Error(errMsg); - } - return result.result?.value; + await ensureAttached(tabId); + const result = await chrome.debugger.sendCommand({ tabId }, "Runtime.evaluate", { + expression, + returnByValue: true, + awaitPromise: true + }); + if (result.exceptionDetails) { + const errMsg = result.exceptionDetails.exception?.description || result.exceptionDetails.text || "Eval error"; + throw new Error(errMsg); + } + return result.result?.value; } -const evaluateAsync = evaluate; +var evaluateAsync = evaluate; +/** +* Capture a screenshot via CDP Page.captureScreenshot. +* Returns base64-encoded image data. +*/ async function screenshot(tabId, options = {}) { - await ensureAttached(tabId); - const format = options.format ?? "png"; - if (options.fullPage) { - const metrics = await chrome.debugger.sendCommand({ tabId }, "Page.getLayoutMetrics"); - const size = metrics.cssContentSize || metrics.contentSize; - if (size) { - await chrome.debugger.sendCommand({ tabId }, "Emulation.setDeviceMetricsOverride", { - mobile: false, - width: Math.ceil(size.width), - height: Math.ceil(size.height), - deviceScaleFactor: 1 - }); - } - } - try { - const params = { format }; - if (format === "jpeg" && options.quality !== void 0) { - params.quality = Math.max(0, Math.min(100, options.quality)); - } - const result = await chrome.debugger.sendCommand({ tabId }, "Page.captureScreenshot", params); - return result.data; - } finally { - if (options.fullPage) { - await chrome.debugger.sendCommand({ tabId }, "Emulation.clearDeviceMetricsOverride").catch(() => { - }); - } - } + await ensureAttached(tabId); + const format = options.format ?? "png"; + if (options.fullPage) { + const metrics = await chrome.debugger.sendCommand({ tabId }, "Page.getLayoutMetrics"); + const size = metrics.cssContentSize || metrics.contentSize; + if (size) await chrome.debugger.sendCommand({ tabId }, "Emulation.setDeviceMetricsOverride", { + mobile: false, + width: Math.ceil(size.width), + height: Math.ceil(size.height), + deviceScaleFactor: 1 + }); + } + try { + const params = { format }; + if (format === "jpeg" && options.quality !== void 0) params.quality = Math.max(0, Math.min(100, options.quality)); + return (await chrome.debugger.sendCommand({ tabId }, "Page.captureScreenshot", params)).data; + } finally { + if (options.fullPage) await chrome.debugger.sendCommand({ tabId }, "Emulation.clearDeviceMetricsOverride").catch(() => {}); + } } function detach(tabId) { - if (!attached.has(tabId)) return; - attached.delete(tabId); - try { - chrome.debugger.detach({ tabId }); - } catch { - } + if (!attached.has(tabId)) return; + attached.delete(tabId); + try { + chrome.debugger.detach({ tabId }); + } catch {} } function registerListeners() { - chrome.tabs.onRemoved.addListener((tabId) => { - attached.delete(tabId); - }); - chrome.debugger.onDetach.addListener((source) => { - if (source.tabId) attached.delete(source.tabId); - }); + chrome.tabs.onRemoved.addListener((tabId) => { + attached.delete(tabId); + }); + chrome.debugger.onDetach.addListener((source) => { + if (source.tabId) attached.delete(source.tabId); + }); } - -let ws = null; -let reconnectTimer = null; -let reconnectAttempts = 0; -const _origLog = console.log.bind(console); -const _origWarn = console.warn.bind(console); -const _origError = console.error.bind(console); +//#endregion +//#region src/background.ts +var ws = null; +var reconnectTimer = null; +var reconnectAttempts = 0; +var _origLog = console.log.bind(console); +var _origWarn = console.warn.bind(console); +var _origError = console.error.bind(console); function forwardLog(level, args) { - if (!ws || ws.readyState !== WebSocket.OPEN) return; - try { - const msg = args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" "); - ws.send(JSON.stringify({ type: "log", level, msg, ts: Date.now() })); - } catch { - } + if (!ws || ws.readyState !== WebSocket.OPEN) return; + try { + const msg = args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" "); + ws.send(JSON.stringify({ + type: "log", + level, + msg, + ts: Date.now() + })); + } catch {} } console.log = (...args) => { - _origLog(...args); - forwardLog("info", args); + _origLog(...args); + forwardLog("info", args); }; console.warn = (...args) => { - _origWarn(...args); - forwardLog("warn", args); + _origWarn(...args); + forwardLog("warn", args); }; console.error = (...args) => { - _origError(...args); - forwardLog("error", args); + _origError(...args); + forwardLog("error", args); }; function connect() { - if (ws?.readyState === WebSocket.OPEN || ws?.readyState === WebSocket.CONNECTING) return; - try { - ws = new WebSocket(DAEMON_WS_URL); - } catch { - scheduleReconnect(); - return; - } - ws.onopen = () => { - console.log("[opencli] Connected to daemon"); - reconnectAttempts = 0; - if (reconnectTimer) { - clearTimeout(reconnectTimer); - reconnectTimer = null; - } - }; - ws.onmessage = async (event) => { - try { - const command = JSON.parse(event.data); - const result = await handleCommand(command); - ws?.send(JSON.stringify(result)); - } catch (err) { - console.error("[opencli] Message handling error:", err); - } - }; - ws.onclose = () => { - console.log("[opencli] Disconnected from daemon"); - ws = null; - scheduleReconnect(); - }; - ws.onerror = () => { - ws?.close(); - }; + if (ws?.readyState === WebSocket.OPEN || ws?.readyState === WebSocket.CONNECTING) return; + try { + ws = new WebSocket(DAEMON_WS_URL); + } catch { + scheduleReconnect(); + return; + } + ws.onopen = () => { + console.log("[opencli] Connected to daemon"); + reconnectAttempts = 0; + if (reconnectTimer) { + clearTimeout(reconnectTimer); + reconnectTimer = null; + } + }; + ws.onmessage = async (event) => { + try { + const result = await handleCommand(JSON.parse(event.data)); + ws?.send(JSON.stringify(result)); + } catch (err) { + console.error("[opencli] Message handling error:", err); + } + }; + ws.onclose = () => { + console.log("[opencli] Disconnected from daemon"); + ws = null; + scheduleReconnect(); + }; + ws.onerror = () => { + ws?.close(); + }; } function scheduleReconnect() { - if (reconnectTimer) return; - reconnectAttempts++; - const delay = Math.min(WS_RECONNECT_BASE_DELAY * Math.pow(2, reconnectAttempts - 1), WS_RECONNECT_MAX_DELAY); - reconnectTimer = setTimeout(() => { - reconnectTimer = null; - connect(); - }, delay); + if (reconnectTimer) return; + reconnectAttempts++; + const delay = Math.min(WS_RECONNECT_BASE_DELAY * Math.pow(2, reconnectAttempts - 1), WS_RECONNECT_MAX_DELAY); + reconnectTimer = setTimeout(() => { + reconnectTimer = null; + connect(); + }, delay); } -let initialized = false; +var initialized = false; function initialize() { - if (initialized) return; - initialized = true; - chrome.alarms.create("keepalive", { periodInMinutes: 0.4 }); - registerListeners(); - connect(); - console.log("[opencli] Browser Bridge extension initialized"); + if (initialized) return; + initialized = true; + chrome.alarms.create("keepalive", { periodInMinutes: .4 }); + registerListeners(); + connect(); + console.log("[opencli] Browser Bridge extension initialized"); } chrome.runtime.onInstalled.addListener(() => { - initialize(); + initialize(); }); chrome.runtime.onStartup.addListener(() => { - initialize(); + initialize(); }); chrome.alarms.onAlarm.addListener((alarm) => { - if (alarm.name === "keepalive") connect(); + if (alarm.name === "keepalive") connect(); }); async function handleCommand(cmd) { - try { - switch (cmd.action) { - case "exec": - return await handleExec(cmd); - case "navigate": - return await handleNavigate(cmd); - case "tabs": - return await handleTabs(cmd); - case "cookies": - return await handleCookies(cmd); - case "screenshot": - return await handleScreenshot(cmd); - default: - return { id: cmd.id, ok: false, error: `Unknown action: ${cmd.action}` }; - } - } catch (err) { - return { - id: cmd.id, - ok: false, - error: err instanceof Error ? err.message : String(err) - }; - } + try { + switch (cmd.action) { + case "exec": return await handleExec(cmd); + case "navigate": return await handleNavigate(cmd); + case "tabs": return await handleTabs(cmd); + case "cookies": return await handleCookies(cmd); + case "screenshot": return await handleScreenshot(cmd); + default: return { + id: cmd.id, + ok: false, + error: `Unknown action: ${cmd.action}` + }; + } + } catch (err) { + return { + id: cmd.id, + ok: false, + error: err instanceof Error ? err.message : String(err) + }; + } } +/** Check if a URL is a debuggable web page (not chrome:// or extension page) */ function isWebUrl(url) { - if (!url) return false; - return !url.startsWith("chrome://") && !url.startsWith("chrome-extension://"); + if (!url) return false; + return !url.startsWith("chrome://") && !url.startsWith("chrome-extension://"); } +/** Resolve target tab: use specified tabId or fall back to active web page tab */ async function resolveTabId(tabId) { - if (tabId !== void 0) return tabId; - const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true }); - if (activeTab?.id && isWebUrl(activeTab.url)) { - return activeTab.id; - } - const allTabs = await chrome.tabs.query({ currentWindow: true }); - const webTab = allTabs.find((t) => t.id && isWebUrl(t.url)); - if (webTab?.id) { - await chrome.tabs.update(webTab.id, { active: true }); - return webTab.id; - } - const newTab = await chrome.tabs.create({ url: "about:blank", active: true }); - if (!newTab.id) throw new Error("Failed to create new tab"); - return newTab.id; + if (tabId !== void 0) return tabId; + const [activeTab] = await chrome.tabs.query({ + active: true, + currentWindow: true + }); + if (activeTab?.id && isWebUrl(activeTab.url)) return activeTab.id; + const webTab = (await chrome.tabs.query({ currentWindow: true })).find((t) => t.id && isWebUrl(t.url)); + if (webTab?.id) { + await chrome.tabs.update(webTab.id, { active: true }); + return webTab.id; + } + const newTab = await chrome.tabs.create({ + url: "about:blank", + active: true + }); + if (!newTab.id) throw new Error("Failed to create new tab"); + return newTab.id; } async function handleExec(cmd) { - if (!cmd.code) return { id: cmd.id, ok: false, error: "Missing code" }; - const tabId = await resolveTabId(cmd.tabId); - try { - const data = await evaluateAsync(tabId, cmd.code); - return { id: cmd.id, ok: true, data }; - } catch (err) { - return { id: cmd.id, ok: false, error: err instanceof Error ? err.message : String(err) }; - } + if (!cmd.code) return { + id: cmd.id, + ok: false, + error: "Missing code" + }; + const tabId = await resolveTabId(cmd.tabId); + try { + const data = await evaluateAsync(tabId, cmd.code); + return { + id: cmd.id, + ok: true, + data + }; + } catch (err) { + return { + id: cmd.id, + ok: false, + error: err instanceof Error ? err.message : String(err) + }; + } } async function handleNavigate(cmd) { - if (!cmd.url) return { id: cmd.id, ok: false, error: "Missing url" }; - const tabId = await resolveTabId(cmd.tabId); - await chrome.tabs.update(tabId, { url: cmd.url }); - await new Promise((resolve) => { - chrome.tabs.get(tabId).then((tab2) => { - if (tab2.status === "complete") { - resolve(); - return; - } - const listener = (id, info) => { - if (id === tabId && info.status === "complete") { - chrome.tabs.onUpdated.removeListener(listener); - resolve(); - } - }; - chrome.tabs.onUpdated.addListener(listener); - setTimeout(() => { - chrome.tabs.onUpdated.removeListener(listener); - resolve(); - }, 15e3); - }); - }); - const tab = await chrome.tabs.get(tabId); - return { id: cmd.id, ok: true, data: { title: tab.title, url: tab.url, tabId } }; + if (!cmd.url) return { + id: cmd.id, + ok: false, + error: "Missing url" + }; + const tabId = await resolveTabId(cmd.tabId); + await chrome.tabs.update(tabId, { url: cmd.url }); + await new Promise((resolve) => { + chrome.tabs.get(tabId).then((tab) => { + if (tab.status === "complete") { + resolve(); + return; + } + const listener = (id, info) => { + if (id === tabId && info.status === "complete") { + chrome.tabs.onUpdated.removeListener(listener); + resolve(); + } + }; + chrome.tabs.onUpdated.addListener(listener); + setTimeout(() => { + chrome.tabs.onUpdated.removeListener(listener); + resolve(); + }, 15e3); + }); + }); + const tab = await chrome.tabs.get(tabId); + return { + id: cmd.id, + ok: true, + data: { + title: tab.title, + url: tab.url, + tabId + } + }; } async function handleTabs(cmd) { - switch (cmd.op) { - case "list": { - const tabs = await chrome.tabs.query({}); - const data = tabs.filter((t) => isWebUrl(t.url)).map((t, i) => ({ - index: i, - tabId: t.id, - url: t.url, - title: t.title, - active: t.active - })); - return { id: cmd.id, ok: true, data }; - } - case "new": { - const tab = await chrome.tabs.create({ url: cmd.url, active: true }); - return { id: cmd.id, ok: true, data: { tabId: tab.id, url: tab.url } }; - } - case "close": { - if (cmd.index !== void 0) { - const tabs = await chrome.tabs.query({}); - const target = tabs[cmd.index]; - if (!target?.id) return { id: cmd.id, ok: false, error: `Tab index ${cmd.index} not found` }; - await chrome.tabs.remove(target.id); - detach(target.id); - return { id: cmd.id, ok: true, data: { closed: target.id } }; - } - const tabId = await resolveTabId(cmd.tabId); - await chrome.tabs.remove(tabId); - detach(tabId); - return { id: cmd.id, ok: true, data: { closed: tabId } }; - } - case "select": { - if (cmd.index === void 0 && cmd.tabId === void 0) - return { id: cmd.id, ok: false, error: "Missing index or tabId" }; - if (cmd.tabId !== void 0) { - await chrome.tabs.update(cmd.tabId, { active: true }); - return { id: cmd.id, ok: true, data: { selected: cmd.tabId } }; - } - const tabs = await chrome.tabs.query({}); - const target = tabs[cmd.index]; - if (!target?.id) return { id: cmd.id, ok: false, error: `Tab index ${cmd.index} not found` }; - await chrome.tabs.update(target.id, { active: true }); - return { id: cmd.id, ok: true, data: { selected: target.id } }; - } - default: - return { id: cmd.id, ok: false, error: `Unknown tabs op: ${cmd.op}` }; - } + switch (cmd.op) { + case "list": { + const data = (await chrome.tabs.query({})).filter((t) => isWebUrl(t.url)).map((t, i) => ({ + index: i, + tabId: t.id, + url: t.url, + title: t.title, + active: t.active + })); + return { + id: cmd.id, + ok: true, + data + }; + } + case "new": { + const tab = await chrome.tabs.create({ + url: cmd.url, + active: true + }); + return { + id: cmd.id, + ok: true, + data: { + tabId: tab.id, + url: tab.url + } + }; + } + case "close": { + if (cmd.index !== void 0) { + const target = (await chrome.tabs.query({}))[cmd.index]; + if (!target?.id) return { + id: cmd.id, + ok: false, + error: `Tab index ${cmd.index} not found` + }; + await chrome.tabs.remove(target.id); + detach(target.id); + return { + id: cmd.id, + ok: true, + data: { closed: target.id } + }; + } + const tabId = await resolveTabId(cmd.tabId); + await chrome.tabs.remove(tabId); + detach(tabId); + return { + id: cmd.id, + ok: true, + data: { closed: tabId } + }; + } + case "select": { + if (cmd.index === void 0 && cmd.tabId === void 0) return { + id: cmd.id, + ok: false, + error: "Missing index or tabId" + }; + if (cmd.tabId !== void 0) { + await chrome.tabs.update(cmd.tabId, { active: true }); + return { + id: cmd.id, + ok: true, + data: { selected: cmd.tabId } + }; + } + const target = (await chrome.tabs.query({}))[cmd.index]; + if (!target?.id) return { + id: cmd.id, + ok: false, + error: `Tab index ${cmd.index} not found` + }; + await chrome.tabs.update(target.id, { active: true }); + return { + id: cmd.id, + ok: true, + data: { selected: target.id } + }; + } + default: return { + id: cmd.id, + ok: false, + error: `Unknown tabs op: ${cmd.op}` + }; + } } async function handleCookies(cmd) { - const details = {}; - if (cmd.domain) details.domain = cmd.domain; - if (cmd.url) details.url = cmd.url; - const cookies = await chrome.cookies.getAll(details); - const data = cookies.map((c) => ({ - name: c.name, - value: c.value, - domain: c.domain, - path: c.path, - secure: c.secure, - httpOnly: c.httpOnly, - expirationDate: c.expirationDate - })); - return { id: cmd.id, ok: true, data }; + const details = {}; + if (cmd.domain) details.domain = cmd.domain; + if (cmd.url) details.url = cmd.url; + const data = (await chrome.cookies.getAll(details)).map((c) => ({ + name: c.name, + value: c.value, + domain: c.domain, + path: c.path, + secure: c.secure, + httpOnly: c.httpOnly, + expirationDate: c.expirationDate + })); + return { + id: cmd.id, + ok: true, + data + }; } async function handleScreenshot(cmd) { - const tabId = await resolveTabId(cmd.tabId); - try { - const data = await screenshot(tabId, { - format: cmd.format, - quality: cmd.quality, - fullPage: cmd.fullPage - }); - return { id: cmd.id, ok: true, data }; - } catch (err) { - return { id: cmd.id, ok: false, error: err instanceof Error ? err.message : String(err) }; - } + const tabId = await resolveTabId(cmd.tabId); + try { + const data = await screenshot(tabId, { + format: cmd.format, + quality: cmd.quality, + fullPage: cmd.fullPage + }); + return { + id: cmd.id, + ok: true, + data + }; + } catch (err) { + return { + id: cmd.id, + ok: false, + error: err instanceof Error ? err.message : String(err) + }; + } } +//#endregion diff --git a/extension/manifest.json b/extension/manifest.json index c2a2d6c..3d69d79 100644 --- a/extension/manifest.json +++ b/extension/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "opencli Browser Bridge", - "version": "0.1.0", + "version": "1.0.0", "description": "Bridge between opencli CLI and your browser โ€” execute commands, read cookies, manage tabs.", "permissions": [ "debugger", diff --git a/src/browser/mcp.ts b/src/browser/mcp.ts index b97e2c1..e78093e 100644 --- a/src/browser/mcp.ts +++ b/src/browser/mcp.ts @@ -1,8 +1,5 @@ /** * Browser session manager โ€” auto-spawns daemon and provides IPage. - * - * Replaces the old PlaywrightMCP class. Still exports as PlaywrightMCP - * for backward compatibility with main.ts and other consumers. */ import { spawn, type ChildProcess } from 'node:child_process'; @@ -15,21 +12,17 @@ import { isDaemonRunning, isExtensionConnected } from './daemon-client.js'; const DAEMON_SPAWN_TIMEOUT = 10000; // 10s to wait for daemon + extension -export type PlaywrightMCPState = 'idle' | 'connecting' | 'connected' | 'closing' | 'closed'; - - +export type BrowserBridgeState = 'idle' | 'connecting' | 'connected' | 'closing' | 'closed'; /** * Browser factory: manages daemon lifecycle and provides IPage instances. - * - * Kept as `PlaywrightMCP` class name for backward compatibility. */ -export class PlaywrightMCP { - private _state: PlaywrightMCPState = 'idle'; +export class BrowserBridge { + private _state: BrowserBridgeState = 'idle'; private _page: Page | null = null; private _daemonProc: ChildProcess | null = null; - get state(): PlaywrightMCPState { + get state(): BrowserBridgeState { return this._state; } @@ -113,3 +106,6 @@ export class PlaywrightMCP { ); } } + +/** @deprecated Use BrowserBridge instead */ +export const PlaywrightMCP = BrowserBridge; diff --git a/src/browser/page.ts b/src/browser/page.ts index af9c7b7..fafc01d 100644 --- a/src/browser/page.ts +++ b/src/browser/page.ts @@ -183,12 +183,12 @@ export class Page implements IPage { return sendCommand('exec', { code, ...this._tabOpt() }); } - async consoleMessages(level: string = 'info'): Promise { - // Console messages can't be retrospectively read via CDP Runtime.evaluate. - // Would need Runtime.consoleAPICalled event listener, which is not yet implemented. - if (process.env.OPENCLI_VERBOSE) { - console.error('[page] consoleMessages() not supported in lightweight mode โ€” returning empty'); - } + /** + * Console messages are not available in lightweight daemon mode. + * Would require CDP Runtime.consoleAPICalled event listener. + * @returns Always returns empty array. + */ + async consoleMessages(_level: string = 'info'): Promise { return []; } diff --git a/src/clis/grok/debug.ts b/src/clis/grok/debug.ts deleted file mode 100644 index 1f44de1..0000000 --- a/src/clis/grok/debug.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { cli, Strategy } from '../../registry.js'; -import type { IPage } from '../../types.js'; - -export const debugCommand = cli({ - site: 'grok', - name: 'debug', - description: 'Debug grok page structure', - domain: 'grok.com', - strategy: Strategy.COOKIE, - browser: true, - columns: ['data'], - func: async (page: IPage, _kwargs: Record) => { - await page.goto('https://grok.com'); - await page.wait(3); - - // Get all button-like elements near textarea - const debug = await page.evaluate(`(() => { - const ta = document.querySelector('textarea'); - if (!ta) return { error: 'no textarea' }; - - // Get parent containers - let parent = ta.parentElement; - const parents = []; - for (let i = 0; i < 5 && parent; i++) { - parents.push({ - tag: parent.tagName, - class: parent.className?.substring(0, 80), - childCount: parent.children.length, - }); - parent = parent.parentElement; - } - - // Find buttons in the form/container near textarea - const form = ta.closest('form') || ta.closest('[class*="composer"]') || ta.closest('[class*="input"]') || ta.parentElement?.parentElement; - const buttons = form ? [...form.querySelectorAll('button')].map(b => ({ - testid: b.getAttribute('data-testid'), - type: b.type, - disabled: b.disabled, - text: (b.textContent || '').substring(0, 30), - html: b.outerHTML.substring(0, 200), - rect: b.getBoundingClientRect().toJSON(), - })) : []; - - return { parents, buttons, formTag: form?.tagName, formClass: form?.className?.substring(0, 80) }; - })()`); - - return [{ data: JSON.stringify(debug, null, 2) }]; - }, -}); diff --git a/src/daemon.ts b/src/daemon.ts index 033a3a7..36b4e72 100644 --- a/src/daemon.ts +++ b/src/daemon.ts @@ -114,8 +114,8 @@ async function handleRequest(req: IncomingMessage, res: ServerResponse): Promise const result = await new Promise((resolve, reject) => { const timer = setTimeout(() => { pending.delete(body.id); - reject(new Error('Command timeout (30s)')); - }, 30000); + reject(new Error('Command timeout (120s)')); + }, 120000); pending.set(body.id, { resolve, reject, timer }); extensionWs!.send(JSON.stringify(body)); }); diff --git a/src/runtime.ts b/src/runtime.ts index 657aa14..6bd6154 100644 --- a/src/runtime.ts +++ b/src/runtime.ts @@ -32,7 +32,7 @@ export function withTimeoutMs(promise: Promise, timeoutMs: number, message }); } -/** Interface for browser factory (PlaywrightMCP or test mocks) */ +/** Interface for browser factory (BrowserBridge or test mocks) */ export interface IBrowserFactory { connect(opts?: { timeout?: number }): Promise; close(): Promise;