// @ts-nocheck /** * browser-cli Extension — Background Service Worker * * Connects to the native host (com.browsercli.host) via Native Messaging. */ import { getProfileAlias } from './core'; import * as nav from './commands/navigation'; import * as tabs from './commands/tabs'; import * as groups from './commands/groups'; import * as windowsCmd from './commands/windows'; import * as dom from './commands/dom'; import * as browserData from './commands/browser-data'; import * as session from './commands/session'; const NATIVE_HOST = "com.browsercli.host"; let port = null; let keepaliveEnabled = true; // ── Connection management ───────────────────────────────────────────────────── function sendControlMessage(targetPort, message) { if (!targetPort) return; try { targetPort.postMessage(message); } catch (e) { console.warn("[browser-cli] Failed to send control message:", e); } } function disconnectPort({ sendBye = false } = {}) { const currentPort = port; if (!currentPort) return; if (sendBye) sendControlMessage(currentPort, { type: "bye" }); if (port === currentPort) port = null; try { currentPort.disconnect(); } catch (e) { console.warn("[browser-cli] Failed to disconnect native port:", e); } } async function connect() { if (port || !keepaliveEnabled) return; try { const nativePort = chrome.runtime.connectNative(NATIVE_HOST); port = nativePort; nativePort.onMessage.addListener(onMessage); nativePort.onDisconnect.addListener(() => { if (port === nativePort) port = null; const err = chrome.runtime.lastError; if (err) console.warn("[browser-cli] Native host disconnected:", err.message); }); // Send hello so native host knows which profile/alias this is const alias = await getProfileAlias(); nativePort.postMessage({ type: "hello", alias }); console.log("[browser-cli] Connected to native host as profile:", alias); } catch (e) { port = null; console.error("[browser-cli] Failed to connect:", e); } } chrome.runtime.onInstalled.addListener(connect); chrome.runtime.onStartup.addListener(connect); chrome.runtime.onSuspend.addListener(() => { disconnectPort({ sendBye: true }); }); chrome.windows.onCreated.addListener(() => { keepaliveEnabled = true; if (!port) connect(); }); chrome.windows.onRemoved.addListener(async () => { const windows = await chrome.windows.getAll({}); if (windows.length > 0) return; keepaliveEnabled = false; disconnectPort({ sendBye: true }); }); // Keepalive alarm — prevents service worker suspension and reconnects if needed chrome.alarms.create("keepalive", { periodInMinutes: 0.4 }); chrome.alarms.onAlarm.addListener((alarm) => { if (alarm.name === "keepalive") { if (!port && keepaliveEnabled) connect(); } }); // ── Message dispatcher ──────────────────────────────────────────────────────── async function onMessage(msg) { const { id, command, args } = msg; if (!id || !command) return; console.log("[browser-cli] ←", command, args); let data, error; try { const { __page, ...commandArgs } = args || {}; data = await dispatch(command, commandArgs); if (__page && Array.isArray(data)) { data = makePagedData(data, __page); } } catch (e) { error = e.message || String(e); } if (error !== undefined) { console.log("[browser-cli] → ERROR", command, error); port.postMessage({ id, success: false, error }); } else { console.log("[browser-cli] →", command, data); port.postMessage({ id, success: true, data }); } if (command === "clients.rename_profile" && error === undefined) { disconnectPort({ sendBye: true }); keepaliveEnabled = true; await connect(); } } function makePagedData(items, page) { const total = items.length; const offset = Math.max(0, Number(page.offset) || 0); const requestedLimit = Math.max(1, Number(page.limit) || 100); const limit = Math.min(requestedLimit, 1000); const end = Math.min(offset + limit, total); return { __browserCliPage: true, items: items.slice(offset, end), offset, limit, total, nextOffset: end < total ? end : null, }; } async function dispatch(command, args) { switch (command) { // ── Navigation ──────────────────────────────────────────────────────── case "navigate.open": return nav.navOpen(args); case "navigate.to": return nav.navTo(args); case "navigate.reload": return nav.navReload(args, false); case "navigate.hard_reload": return nav.navReload(args, true); case "navigate.back": return nav.navBack(args); case "navigate.forward": return nav.navForward(args); case "navigate.focus": return nav.navFocus(args); case "navigate.wait": return nav.navWait(args); case "navigate.open_wait": return nav.navOpenWait(args); // ── Tabs ────────────────────────────────────────────────────────────── case "tabs.list": return tabs.tabsList(); case "tabs.close": return tabs.tabsClose(args); case "tabs.move": return tabs.tabsMove(args); case "tabs.active": return tabs.tabsActive(args); case "tabs.active_in_window": return tabs.tabsActiveInWindow(args); case "tabs.status": return tabs.tabsStatus(args); case "tabs.filter": return tabs.tabsFilter(args); case "tabs.count": return tabs.tabsCount(args); case "tabs.query": return tabs.tabsQuery(args); case "tabs.html": return tabs.tabsHtml(args); case "tabs.dedupe": return tabs.tabsDedupe(); case "tabs.sort": return tabs.tabsSort(args); case "tabs.merge_windows": return tabs.tabsMergeWindows(); case "tabs.mute": return tabs.tabsMute(args); case "tabs.unmute": return tabs.tabsUnmute(args); case "tabs.pin": return tabs.tabsPin(args); case "tabs.unpin": return tabs.tabsUnpin(args); case "tabs.screenshot": return tabs.tabsScreenshot(args); case "tabs.watch_url": return tabs.tabsWatchUrl(args); // ── Groups ──────────────────────────────────────────────────────────── case "group.list": return groups.groupList(); case "group.tabs": return groups.groupTabs(args); case "group.count": return groups.groupCount(); case "group.query": return groups.groupQuery(args); case "group.close": return groups.groupClose(args); case "group.open": return groups.groupOpen(args); case "group.add_tab": return groups.groupAddTab(args); case "group.move": return groups.groupMove(args); // ── Windows ─────────────────────────────────────────────────────────── case "windows.list": return windowsCmd.windowsList(); case "windows.rename": return windowsCmd.windowsRename(args); case "windows.close": return windowsCmd.windowsClose(args); case "windows.open": return windowsCmd.windowsOpen(args); // ── DOM ─────────────────────────────────────────────────────────────── case "dom.query": return dom.domOp("domQuery", args); case "dom.click": return dom.domOp("domClick", args); case "dom.type": return dom.domOp("domType", args); case "dom.attr": return dom.domOp("domAttr", args); case "dom.text": return dom.domOp("domText", args); case "dom.exists": return dom.domOp("domExists", args); case "dom.scroll": return dom.domOp("domScroll", args); case "dom.select": return dom.domOp("domSelect", args); case "dom.key": return dom.domOp("domKey", args); case "dom.hover": return dom.domOp("domHover", args); case "dom.check": return dom.domOp("domCheck", { ...args, checked: true }); case "dom.uncheck": return dom.domOp("domCheck", { ...args, checked: false }); case "dom.clear": return dom.domOp("domClear", args); case "dom.focus": return dom.domOp("domFocus", args); case "dom.submit": return dom.domOp("domSubmit", args); case "dom.eval": return dom.domEval(args); case "dom.wait_for": return dom.domWaitFor(args); case "dom.poll": return dom.domPoll(args); // ── Page ────────────────────────────────────────────────────────────── case "page.info": return dom.domOp("pageInfo", {}); // ── Storage ─────────────────────────────────────────────────────────── case "storage.get": return browserData.storageGet(args); case "storage.set": return browserData.storageSet(args); // ── Cookies ─────────────────────────────────────────────────────────── case "cookies.list": return browserData.cookiesList(args); case "cookies.get": return browserData.cookiesGet(args); case "cookies.set": return browserData.cookiesSet(args); // ── Extract ─────────────────────────────────────────────────────────── case "extract.links": return dom.domOp("extractLinks", args); case "extract.images": return dom.domOp("extractImages", args); case "extract.text": return dom.domOp("extractText", args); case "extract.json": return dom.domOp("extractJson", args); case "extract.markdown": return dom.domOp("extractMarkdown", args); case "extract.html": return tabs.tabsHtml({}); // ── Session ─────────────────────────────────────────────────────────── case "session.save": return session.sessionSave(args); case "session.load": return session.sessionLoad(args); case "session.list": return session.sessionList(); case "session.remove": return session.sessionRemove(args); case "session.diff": return session.sessionDiff(args); case "session.auto_save": return session.sessionAutoSave(args); // ── Misc ────────────────────────────────────────────────────────────── case "clients.list": return session.clientsList(); case "clients.rename_profile": return session.clientsRenameProfile(args); default: throw new Error(`Unknown command: ${command}`); } } // ── Navigation ────────────────────────────────────────────────────────────────