chore: pre-release cleanup

- Fix daemon per-command timeout: 30s → 120s (was shorter than CLI-layer timeouts)
- Remove debug command: grok/debug.ts
- Sync extension version to 1.0.0
- Rename PlaywrightMCP → BrowserBridge (keep backward-compat alias)
- Add accept/reply-dm to README command tables
- Clean up consoleMessages() JSDoc in page.ts
- Update runtime.ts comment
This commit is contained in:
jackwener committed 2026-03-19 22:49:54 +08:00
1 parent 13e2345089
commit 2e962b2e7c
9 files changed
+398 -363

No files matched your search

+1 -1
View File
@@ -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 |
+1 -1
View File
@@ -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` | 🔐 浏览器 |
+379 -291
View File
@@ -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
+1 -1
View File
@@ -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",
+7 -11
View File
@@ -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;
+6 -6
View File
@@ -183,12 +183,12 @@ export class Page implements IPage {
return sendCommand('exec', { code, ...this._tabOpt() });
}
async consoleMessages(level: string = 'info'): Promise<any> {
// 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<any> {
return [];
}
-49
View File
@@ -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<string, any>) => {
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) }];
},
});
+2 -2
View File
@@ -114,8 +114,8 @@ async function handleRequest(req: IncomingMessage, res: ServerResponse): Promise
const result = await new Promise<unknown>((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));
});
+1 -1
View File
@@ -32,7 +32,7 @@ export function withTimeoutMs<T>(promise: Promise<T>, 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<IPage>;
close(): Promise<void>;