Files
jackwener__opencli/extension/dist/background.js
jakevin 6cdcb9dd51 fix: add prepare script so source installs trigger build (#729)
* fix: add prepare script so source installs trigger build

npm install from git (e.g. npm install github:jackwener/opencli) skips
prepublishOnly, so dist/ is never generated. The prepare hook runs on
git-based installs; the [ -d src ] guard skips it for registry installs.

* fix: include extension/dist in git so clone works out of the box

.gitignore had conflicting rules: line 3 tried to un-ignore extension/dist/
but line 26 re-ignored it. Remove the later rule so the built extension JS
is tracked in git — users can load the extension directly after clone.
2026-04-03 16:23:05 +08:00

740 lines
26 KiB
JavaScript

const DAEMON_PORT = 19825;
const DAEMON_HOST = "localhost";
const DAEMON_WS_URL = `ws://${DAEMON_HOST}:${DAEMON_PORT}/ext`;
const DAEMON_PING_URL = `http://${DAEMON_HOST}:${DAEMON_PORT}/ping`;
const WS_RECONNECT_BASE_DELAY = 2e3;
const WS_RECONNECT_MAX_DELAY = 5e3;
const attached = /* @__PURE__ */ new Set();
function isDebuggableUrl$1(url) {
if (!url) return true;
return url.startsWith("http://") || url.startsWith("https://") || url === "about:blank" || url.startsWith("data:");
}
async function ensureAttached(tabId, aggressiveRetry = false) {
try {
const tab = await chrome.tabs.get(tabId);
if (!isDebuggableUrl$1(tab.url)) {
attached.delete(tabId);
throw new Error(`Cannot debug tab ${tabId}: URL is ${tab.url ?? "unknown"}`);
}
} catch (e) {
if (e instanceof Error && e.message.startsWith("Cannot debug tab")) throw e;
attached.delete(tabId);
throw new Error(`Tab ${tabId} no longer exists`);
}
if (attached.has(tabId)) {
try {
await chrome.debugger.sendCommand({ tabId }, "Runtime.evaluate", {
expression: "1",
returnByValue: true
});
return;
} catch {
attached.delete(tabId);
}
}
const MAX_ATTACH_RETRIES = aggressiveRetry ? 5 : 2;
const RETRY_DELAY_MS = aggressiveRetry ? 1500 : 500;
let lastError = "";
for (let attempt = 1; attempt <= MAX_ATTACH_RETRIES; attempt++) {
try {
try {
await chrome.debugger.detach({ tabId });
} catch {
}
await chrome.debugger.attach({ tabId }, "1.3");
lastError = "";
break;
} catch (e) {
lastError = e instanceof Error ? e.message : String(e);
if (attempt < MAX_ATTACH_RETRIES) {
console.warn(`[opencli] attach attempt ${attempt}/${MAX_ATTACH_RETRIES} failed: ${lastError}, retrying in ${RETRY_DELAY_MS}ms...`);
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
try {
const tab = await chrome.tabs.get(tabId);
if (!isDebuggableUrl$1(tab.url)) {
lastError = `Tab URL changed to ${tab.url} during retry`;
break;
}
} catch {
lastError = `Tab ${tabId} no longer exists`;
break;
}
}
}
}
if (lastError) {
let finalUrl = "unknown";
let finalWindowId = "unknown";
try {
const tab = await chrome.tabs.get(tabId);
finalUrl = tab.url ?? "undefined";
finalWindowId = String(tab.windowId);
} catch {
}
console.warn(`[opencli] attach failed for tab ${tabId}: url=${finalUrl}, windowId=${finalWindowId}, error=${lastError}`);
const hint = lastError.includes("chrome-extension://") ? ". Tip: another Chrome extension may be interfering — try disabling other extensions" : "";
throw new Error(`attach failed: ${lastError}${hint}`);
}
attached.add(tabId);
try {
await chrome.debugger.sendCommand({ tabId }, "Runtime.enable");
} catch {
}
}
async function evaluate(tabId, expression, aggressiveRetry = false) {
const MAX_EVAL_RETRIES = aggressiveRetry ? 3 : 2;
for (let attempt = 1; attempt <= MAX_EVAL_RETRIES; attempt++) {
try {
await ensureAttached(tabId, aggressiveRetry);
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;
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
const isNavigateError = msg.includes("Inspected target navigated") || msg.includes("Target closed");
const isAttachError = isNavigateError || msg.includes("attach failed") || msg.includes("Debugger is not attached") || msg.includes("chrome-extension://");
if (isAttachError && attempt < MAX_EVAL_RETRIES) {
attached.delete(tabId);
const retryMs = isNavigateError ? 200 : 500;
await new Promise((resolve) => setTimeout(resolve, retryMs));
continue;
}
throw e;
}
}
throw new Error("evaluate: max retries exhausted");
}
const evaluateAsync = evaluate;
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(() => {
});
}
}
}
async function setFileInputFiles(tabId, files, selector) {
await ensureAttached(tabId);
await chrome.debugger.sendCommand({ tabId }, "DOM.enable");
const doc = await chrome.debugger.sendCommand({ tabId }, "DOM.getDocument");
const query = selector || 'input[type="file"]';
const result = await chrome.debugger.sendCommand({ tabId }, "DOM.querySelector", {
nodeId: doc.root.nodeId,
selector: query
});
if (!result.nodeId) {
throw new Error(`No element found matching selector: ${query}`);
}
await chrome.debugger.sendCommand({ tabId }, "DOM.setFileInputFiles", {
files,
nodeId: result.nodeId
});
}
async function detach(tabId) {
if (!attached.has(tabId)) return;
attached.delete(tabId);
try {
await 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.onUpdated.addListener(async (tabId, info) => {
if (info.url && !isDebuggableUrl$1(info.url)) {
await detach(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);
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 {
}
}
console.log = (...args) => {
_origLog(...args);
forwardLog("info", args);
};
console.warn = (...args) => {
_origWarn(...args);
forwardLog("warn", args);
};
console.error = (...args) => {
_origError(...args);
forwardLog("error", args);
};
async function connect() {
if (ws?.readyState === WebSocket.OPEN || ws?.readyState === WebSocket.CONNECTING) return;
try {
const res = await fetch(DAEMON_PING_URL, { signal: AbortSignal.timeout(1e3) });
if (!res.ok) return;
} catch {
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?.send(JSON.stringify({ type: "hello", version: chrome.runtime.getManifest().version }));
};
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();
};
}
const MAX_EAGER_ATTEMPTS = 6;
function scheduleReconnect() {
if (reconnectTimer) return;
reconnectAttempts++;
if (reconnectAttempts > MAX_EAGER_ATTEMPTS) return;
const delay = Math.min(WS_RECONNECT_BASE_DELAY * Math.pow(2, reconnectAttempts - 1), WS_RECONNECT_MAX_DELAY);
reconnectTimer = setTimeout(() => {
reconnectTimer = null;
void connect();
}, delay);
}
const automationSessions = /* @__PURE__ */ new Map();
const WINDOW_IDLE_TIMEOUT = 3e4;
function getWorkspaceKey(workspace) {
return workspace?.trim() || "default";
}
function resetWindowIdleTimer(workspace) {
const session = automationSessions.get(workspace);
if (!session) return;
if (session.idleTimer) clearTimeout(session.idleTimer);
session.idleDeadlineAt = Date.now() + WINDOW_IDLE_TIMEOUT;
session.idleTimer = setTimeout(async () => {
const current = automationSessions.get(workspace);
if (!current) return;
try {
await chrome.windows.remove(current.windowId);
console.log(`[opencli] Automation window ${current.windowId} (${workspace}) closed (idle timeout)`);
} catch {
}
automationSessions.delete(workspace);
}, WINDOW_IDLE_TIMEOUT);
}
async function getAutomationWindow(workspace, initialUrl) {
const existing = automationSessions.get(workspace);
if (existing) {
try {
await chrome.windows.get(existing.windowId);
return existing.windowId;
} catch {
automationSessions.delete(workspace);
}
}
const startUrl = initialUrl && isSafeNavigationUrl(initialUrl) ? initialUrl : BLANK_PAGE;
const win = await chrome.windows.create({
url: startUrl,
focused: false,
width: 1280,
height: 900,
type: "normal"
});
const session = {
windowId: win.id,
idleTimer: null,
idleDeadlineAt: Date.now() + WINDOW_IDLE_TIMEOUT
};
automationSessions.set(workspace, session);
console.log(`[opencli] Created automation window ${session.windowId} (${workspace}, start=${startUrl})`);
resetWindowIdleTimer(workspace);
const tabs = await chrome.tabs.query({ windowId: win.id });
if (tabs[0]?.id) {
await new Promise((resolve) => {
const timeout = setTimeout(resolve, 500);
const listener = (tabId, info) => {
if (tabId === tabs[0].id && info.status === "complete") {
chrome.tabs.onUpdated.removeListener(listener);
clearTimeout(timeout);
resolve();
}
};
if (tabs[0].status === "complete") {
clearTimeout(timeout);
resolve();
} else {
chrome.tabs.onUpdated.addListener(listener);
}
});
}
return session.windowId;
}
chrome.windows.onRemoved.addListener((windowId) => {
for (const [workspace, session] of automationSessions.entries()) {
if (session.windowId === windowId) {
console.log(`[opencli] Automation window closed (${workspace})`);
if (session.idleTimer) clearTimeout(session.idleTimer);
automationSessions.delete(workspace);
}
}
});
let initialized = false;
function initialize() {
if (initialized) return;
initialized = true;
chrome.alarms.create("keepalive", { periodInMinutes: 0.4 });
registerListeners();
void connect();
console.log("[opencli] OpenCLI extension initialized");
}
chrome.runtime.onInstalled.addListener(() => {
initialize();
});
chrome.runtime.onStartup.addListener(() => {
initialize();
});
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === "keepalive") void connect();
});
chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
if (msg?.type === "getStatus") {
sendResponse({
connected: ws?.readyState === WebSocket.OPEN,
reconnecting: reconnectTimer !== null
});
}
return false;
});
async function handleCommand(cmd) {
const workspace = getWorkspaceKey(cmd.workspace);
resetWindowIdleTimer(workspace);
try {
switch (cmd.action) {
case "exec":
return await handleExec(cmd, workspace);
case "navigate":
return await handleNavigate(cmd, workspace);
case "tabs":
return await handleTabs(cmd, workspace);
case "cookies":
return await handleCookies(cmd);
case "screenshot":
return await handleScreenshot(cmd, workspace);
case "close-window":
return await handleCloseWindow(cmd, workspace);
case "cdp":
return await handleCdp(cmd, workspace);
case "sessions":
return await handleSessions(cmd);
case "set-file-input":
return await handleSetFileInput(cmd, workspace);
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)
};
}
}
const BLANK_PAGE = "about:blank";
function isDebuggableUrl(url) {
if (!url) return true;
return url.startsWith("http://") || url.startsWith("https://") || url === "about:blank" || url.startsWith("data:");
}
function isSafeNavigationUrl(url) {
return url.startsWith("http://") || url.startsWith("https://");
}
function normalizeUrlForComparison(url) {
if (!url) return "";
try {
const parsed = new URL(url);
if (parsed.protocol === "https:" && parsed.port === "443" || parsed.protocol === "http:" && parsed.port === "80") {
parsed.port = "";
}
const pathname = parsed.pathname === "/" ? "" : parsed.pathname;
return `${parsed.protocol}//${parsed.host}${pathname}${parsed.search}${parsed.hash}`;
} catch {
return url;
}
}
function isTargetUrl(currentUrl, targetUrl) {
return normalizeUrlForComparison(currentUrl) === normalizeUrlForComparison(targetUrl);
}
async function resolveTab(tabId, workspace, initialUrl) {
if (tabId !== void 0) {
try {
const tab = await chrome.tabs.get(tabId);
const session = automationSessions.get(workspace);
const matchesSession = session ? tab.windowId === session.windowId : false;
if (isDebuggableUrl(tab.url) && matchesSession) return { tabId, tab };
if (session && !matchesSession && isDebuggableUrl(tab.url)) {
console.warn(`[opencli] Tab ${tabId} drifted to window ${tab.windowId}, moving back to ${session.windowId}`);
try {
await chrome.tabs.move(tabId, { windowId: session.windowId, index: -1 });
const moved = await chrome.tabs.get(tabId);
if (moved.windowId === session.windowId && isDebuggableUrl(moved.url)) {
return { tabId, tab: moved };
}
} catch (moveErr) {
console.warn(`[opencli] Failed to move tab back: ${moveErr}`);
}
} else if (!isDebuggableUrl(tab.url)) {
console.warn(`[opencli] Tab ${tabId} URL is not debuggable (${tab.url}), re-resolving`);
}
} catch {
console.warn(`[opencli] Tab ${tabId} no longer exists, re-resolving`);
}
}
const windowId = await getAutomationWindow(workspace, initialUrl);
const tabs = await chrome.tabs.query({ windowId });
const debuggableTab = tabs.find((t) => t.id && isDebuggableUrl(t.url));
if (debuggableTab?.id) return { tabId: debuggableTab.id, tab: debuggableTab };
const reuseTab = tabs.find((t) => t.id);
if (reuseTab?.id) {
await chrome.tabs.update(reuseTab.id, { url: BLANK_PAGE });
await new Promise((resolve) => setTimeout(resolve, 300));
try {
const updated = await chrome.tabs.get(reuseTab.id);
if (isDebuggableUrl(updated.url)) return { tabId: reuseTab.id, tab: updated };
console.warn(`[opencli] data: URI was intercepted (${updated.url}), creating fresh tab`);
} catch {
}
}
const newTab = await chrome.tabs.create({ windowId, url: BLANK_PAGE, active: true });
if (!newTab.id) throw new Error("Failed to create tab in automation window");
return { tabId: newTab.id, tab: newTab };
}
async function resolveTabId(tabId, workspace, initialUrl) {
const resolved = await resolveTab(tabId, workspace, initialUrl);
return resolved.tabId;
}
async function listAutomationTabs(workspace) {
const session = automationSessions.get(workspace);
if (!session) return [];
try {
return await chrome.tabs.query({ windowId: session.windowId });
} catch {
automationSessions.delete(workspace);
return [];
}
}
async function listAutomationWebTabs(workspace) {
const tabs = await listAutomationTabs(workspace);
return tabs.filter((tab) => isDebuggableUrl(tab.url));
}
async function handleExec(cmd, workspace) {
if (!cmd.code) return { id: cmd.id, ok: false, error: "Missing code" };
const tabId = await resolveTabId(cmd.tabId, workspace);
try {
const aggressive = workspace.startsWith("operate:");
const data = await evaluateAsync(tabId, cmd.code, aggressive);
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, workspace) {
if (!cmd.url) return { id: cmd.id, ok: false, error: "Missing url" };
if (!isSafeNavigationUrl(cmd.url)) {
return { id: cmd.id, ok: false, error: "Blocked URL scheme -- only http:// and https:// are allowed" };
}
const resolved = await resolveTab(cmd.tabId, workspace, cmd.url);
const tabId = resolved.tabId;
const beforeTab = resolved.tab ?? await chrome.tabs.get(tabId);
const beforeNormalized = normalizeUrlForComparison(beforeTab.url);
const targetUrl = cmd.url;
if (beforeTab.status === "complete" && isTargetUrl(beforeTab.url, targetUrl)) {
return {
id: cmd.id,
ok: true,
data: { title: beforeTab.title, url: beforeTab.url, tabId, timedOut: false }
};
}
await detach(tabId);
await chrome.tabs.update(tabId, { url: targetUrl });
let timedOut = false;
await new Promise((resolve) => {
let settled = false;
let checkTimer = null;
let timeoutTimer = null;
const finish = () => {
if (settled) return;
settled = true;
chrome.tabs.onUpdated.removeListener(listener);
if (checkTimer) clearTimeout(checkTimer);
if (timeoutTimer) clearTimeout(timeoutTimer);
resolve();
};
const isNavigationDone = (url) => {
return isTargetUrl(url, targetUrl) || normalizeUrlForComparison(url) !== beforeNormalized;
};
const listener = (id, info, tab2) => {
if (id !== tabId) return;
if (info.status === "complete" && isNavigationDone(tab2.url ?? info.url)) {
finish();
}
};
chrome.tabs.onUpdated.addListener(listener);
checkTimer = setTimeout(async () => {
try {
const currentTab = await chrome.tabs.get(tabId);
if (currentTab.status === "complete" && isNavigationDone(currentTab.url)) {
finish();
}
} catch {
}
}, 100);
timeoutTimer = setTimeout(() => {
timedOut = true;
console.warn(`[opencli] Navigate to ${targetUrl} timed out after 15s`);
finish();
}, 15e3);
});
let tab = await chrome.tabs.get(tabId);
const session = automationSessions.get(workspace);
if (session && tab.windowId !== session.windowId) {
console.warn(`[opencli] Tab ${tabId} drifted to window ${tab.windowId} during navigation, moving back to ${session.windowId}`);
try {
await chrome.tabs.move(tabId, { windowId: session.windowId, index: -1 });
tab = await chrome.tabs.get(tabId);
} catch (moveErr) {
console.warn(`[opencli] Failed to recover drifted tab: ${moveErr}`);
}
}
return {
id: cmd.id,
ok: true,
data: { title: tab.title, url: tab.url, tabId, timedOut }
};
}
async function handleTabs(cmd, workspace) {
switch (cmd.op) {
case "list": {
const tabs = await listAutomationWebTabs(workspace);
const data = tabs.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": {
if (cmd.url && !isSafeNavigationUrl(cmd.url)) {
return { id: cmd.id, ok: false, error: "Blocked URL scheme -- only http:// and https:// are allowed" };
}
const windowId = await getAutomationWindow(workspace);
const tab = await chrome.tabs.create({ windowId, url: cmd.url ?? BLANK_PAGE, 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 listAutomationWebTabs(workspace);
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);
await detach(target.id);
return { id: cmd.id, ok: true, data: { closed: target.id } };
}
const tabId = await resolveTabId(cmd.tabId, workspace);
await chrome.tabs.remove(tabId);
await 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) {
const session = automationSessions.get(workspace);
let tab;
try {
tab = await chrome.tabs.get(cmd.tabId);
} catch {
return { id: cmd.id, ok: false, error: `Tab ${cmd.tabId} no longer exists` };
}
if (!session || tab.windowId !== session.windowId) {
return { id: cmd.id, ok: false, error: `Tab ${cmd.tabId} is not in the automation window` };
}
await chrome.tabs.update(cmd.tabId, { active: true });
return { id: cmd.id, ok: true, data: { selected: cmd.tabId } };
}
const tabs = await listAutomationWebTabs(workspace);
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}` };
}
}
async function handleCookies(cmd) {
if (!cmd.domain && !cmd.url) {
return { id: cmd.id, ok: false, error: "Cookie scope required: provide domain or url to avoid dumping all cookies" };
}
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 };
}
async function handleScreenshot(cmd, workspace) {
const tabId = await resolveTabId(cmd.tabId, workspace);
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 CDP_ALLOWLIST = /* @__PURE__ */ new Set([
// Agent DOM context
"Accessibility.getFullAXTree",
"DOM.getDocument",
"DOM.getBoxModel",
"DOM.getContentQuads",
"DOM.querySelectorAll",
"DOM.scrollIntoViewIfNeeded",
"DOMSnapshot.captureSnapshot",
// Native input events
"Input.dispatchMouseEvent",
"Input.dispatchKeyEvent",
"Input.insertText",
// Page metrics & screenshots
"Page.getLayoutMetrics",
"Page.captureScreenshot",
// Runtime.enable needed for CDP attach setup (Runtime.evaluate goes through 'exec' action)
"Runtime.enable",
// Emulation (used by screenshot full-page)
"Emulation.setDeviceMetricsOverride",
"Emulation.clearDeviceMetricsOverride"
]);
async function handleCdp(cmd, workspace) {
if (!cmd.cdpMethod) return { id: cmd.id, ok: false, error: "Missing cdpMethod" };
if (!CDP_ALLOWLIST.has(cmd.cdpMethod)) {
return { id: cmd.id, ok: false, error: `CDP method not permitted: ${cmd.cdpMethod}` };
}
const tabId = await resolveTabId(cmd.tabId, workspace);
try {
const aggressive = workspace.startsWith("operate:");
await ensureAttached(tabId, aggressive);
const data = await chrome.debugger.sendCommand(
{ tabId },
cmd.cdpMethod,
cmd.cdpParams ?? {}
);
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 handleCloseWindow(cmd, workspace) {
const session = automationSessions.get(workspace);
if (session) {
try {
await chrome.windows.remove(session.windowId);
} catch {
}
if (session.idleTimer) clearTimeout(session.idleTimer);
automationSessions.delete(workspace);
}
return { id: cmd.id, ok: true, data: { closed: true } };
}
async function handleSetFileInput(cmd, workspace) {
if (!cmd.files || !Array.isArray(cmd.files) || cmd.files.length === 0) {
return { id: cmd.id, ok: false, error: "Missing or empty files array" };
}
const tabId = await resolveTabId(cmd.tabId, workspace);
try {
await setFileInputFiles(tabId, cmd.files, cmd.selector);
return { id: cmd.id, ok: true, data: { count: cmd.files.length } };
} catch (err) {
return { id: cmd.id, ok: false, error: err instanceof Error ? err.message : String(err) };
}
}
async function handleSessions(cmd) {
const now = Date.now();
const data = await Promise.all([...automationSessions.entries()].map(async ([workspace, session]) => ({
workspace,
windowId: session.windowId,
tabCount: (await chrome.tabs.query({ windowId: session.windowId })).filter((tab) => isDebuggableUrl(tab.url)).length,
idleMsRemaining: Math.max(0, session.idleDeadlineAt - now)
})));
return { id: cmd.id, ok: true, data };
}