change background.js into split typescript files for better managemand and build background.js from typescript

This commit is contained in:
2026-05-01 19:28:36 +02:00
parent fb78fd0471
commit 7ee664153b
17 changed files with 2261 additions and 1636 deletions
+62
View File
@@ -0,0 +1,62 @@
// @ts-nocheck
import { executeScript, getActiveTab, isScriptableUrl } from '../core';
export async function storageGet({ key, type = "local", tabId } = {}) {
const tab = tabId ? await chrome.tabs.get(tabId) : await getActiveTab();
if (!isScriptableUrl(tab.url)) {
throw new Error(`Cannot access storage on ${tab.url} — navigate to a regular web page first`);
}
const results = await executeScript({
target: { tabId: tab.id },
world: "MAIN",
func: (k, t) => {
const store = t === "session" ? sessionStorage : localStorage;
if (k) return store.getItem(k);
return Object.fromEntries(Object.keys(store).map(key => [key, store.getItem(key)]));
},
args: [key || null, type],
});
return results[0]?.result ?? null;
}
export async function storageSet({ key, value, type = "local", tabId } = {}) {
const tab = tabId ? await chrome.tabs.get(tabId) : await getActiveTab();
if (!isScriptableUrl(tab.url)) {
throw new Error(`Cannot access storage on ${tab.url} — navigate to a regular web page first`);
}
const results = await executeScript({
target: { tabId: tab.id },
world: "MAIN",
func: (k, v, t) => {
const store = t === "session" ? sessionStorage : localStorage;
store.setItem(k, typeof v === "string" ? v : JSON.stringify(v));
return true;
},
args: [key, value, type],
});
return results[0]?.result ?? false;
}
export async function cookiesList({ url, domain, name } = {}) {
const details = {};
if (url) details.url = url;
if (domain) details.domain = domain;
if (name) details.name = name;
return await chrome.cookies.getAll(details);
}
export async function cookiesGet({ url, name }) {
return await chrome.cookies.get({ url, name });
}
export async function cookiesSet({ url, name, value, domain, path, secure, httpOnly, expirationDate, sameSite } = {}) {
const details = { url, name, value };
if (domain != null) details.domain = domain;
if (path != null) details.path = path;
if (secure != null) details.secure = secure;
if (httpOnly != null) details.httpOnly = httpOnly;
if (expirationDate != null) details.expirationDate = expirationDate;
if (sameSite != null) details.sameSite = sameSite;
return await chrome.cookies.set(details);
}
// This function is serialized and injected into the page by chrome.scripting
+82
View File
@@ -0,0 +1,82 @@
// @ts-nocheck
import { executeScript, getActiveTab, isScriptableUrl } from '../core';
import { contentDispatch } from './injected';
export async function domOp(funcName, args) {
const tab = await getActiveTab();
if (!isScriptableUrl(tab.url)) {
throw new Error(`Cannot run DOM commands on ${tab.url} — navigate to a regular web page first`);
}
const results = await executeScript({
target: { tabId: tab.id },
func: contentDispatch,
args: [funcName, args],
});
return results[0]?.result;
}
export async function domEval({ code, tabId } = {}) {
const tab = tabId ? await chrome.tabs.get(tabId) : await getActiveTab();
if (!isScriptableUrl(tab.url)) {
throw new Error(`Cannot run DOM commands on ${tab.url} — navigate to a regular web page first`);
}
const results = await executeScript({
target: { tabId: tab.id },
world: "MAIN",
func: (c) => (0, eval)(c),
args: [code],
});
return results[0]?.result ?? null;
}
export async function domWaitFor({ selector, timeout = 10000, visible = false, hidden = false, tabId } = {}) {
const tab = tabId ? await chrome.tabs.get(tabId) : await getActiveTab();
if (!isScriptableUrl(tab.url)) {
throw new Error(`Cannot run DOM commands on ${tab.url} — navigate to a regular web page first`);
}
const deadline = Date.now() + timeout;
while (Date.now() < deadline) {
const results = await executeScript({
target: { tabId: tab.id },
func: (sel, vis, hid) => {
const el = document.querySelector(sel);
if (hid) return !el || el.offsetParent === null;
if (!el) return false;
if (vis) {
const r = el.getBoundingClientRect();
return r.width > 0 && r.height > 0;
}
return true;
},
args: [selector, visible, hidden],
});
if (results[0]?.result) return { selector, found: !hidden };
await new Promise(r => setTimeout(r, 200));
}
throw new Error(`Selector '${selector}' condition not met within ${timeout}ms`);
}
export async function domPoll({ selector, pattern, attr, timeout = 30000, interval = 500, tabId } = {}) {
const tab = tabId ? await chrome.tabs.get(tabId) : await getActiveTab();
if (!isScriptableUrl(tab.url)) {
throw new Error(`Cannot run DOM commands on ${tab.url} — navigate to a regular web page first`);
}
const deadline = Date.now() + timeout;
const regex = new RegExp(pattern);
while (Date.now() < deadline) {
const results = await executeScript({
target: { tabId: tab.id },
func: (sel, a) => {
const el = document.querySelector(sel);
if (!el) return null;
if (a) return el.getAttribute(a) ?? el[a] ?? null;
return el.value !== undefined ? el.value : el.textContent.trim();
},
args: [selector, attr || null],
});
const value = results[0]?.result;
if (value != null && regex.test(String(value))) return { selector, value, pattern };
await new Promise(r => setTimeout(r, interval));
}
throw new Error(`Selector '${selector}' did not match '${pattern}' within ${timeout}ms`);
}
+95
View File
@@ -0,0 +1,95 @@
// @ts-nocheck
import { buildTabBlocks, resolveGroupId, tabInfo } from '../core';
export async function groupList() {
const groups = await chrome.tabGroups.query({});
const all = await chrome.tabs.query({});
return groups.map(g => ({
id: g.id,
title: g.title,
color: g.color,
collapsed: g.collapsed,
windowId: g.windowId,
tabCount: all.filter(t => t.groupId === g.id).length,
}));
}
export async function groupTabs({ groupId }) {
const all = await chrome.tabs.query({});
return all.filter(t => t.groupId === groupId).map(tabInfo);
}
export async function groupCount() {
const groups = await chrome.tabGroups.query({});
return groups.length;
}
export async function groupQuery({ search }) {
const q = search.toLowerCase();
const groups = await chrome.tabGroups.query({});
return groups.filter(g => g.title && g.title.toLowerCase().includes(q));
}
export async function groupClose({ groupId }) {
const tabs = await chrome.tabs.query({});
const groupTabs = tabs.filter(t => t.groupId === groupId);
await chrome.tabs.ungroup(groupTabs.map(t => t.id));
return { groupId };
}
export async function groupOpen({ name }) {
const tab = await chrome.tabs.create({ active: true });
const groupId = await chrome.tabs.group({ tabIds: [tab.id] });
await chrome.tabGroups.update(groupId, { title: name });
return { id: groupId, name };
}
export async function groupAddTab({ group, url }) {
const groupId = await resolveGroupId(group);
const existingTabs = await chrome.tabs.query({ groupId });
const tab = await chrome.tabs.create({ url: url || "chrome://newtab/", active: true });
await chrome.tabs.group({ tabIds: [tab.id], groupId });
// If a URL was provided, close any blank placeholder tabs left from group creation
if (url) {
const placeholders = existingTabs.filter(t =>
t.url === "chrome://newtab/" || t.url === "about:blank" || t.pendingUrl === "chrome://newtab/"
);
if (placeholders.length) await chrome.tabs.remove(placeholders.map(t => t.id));
}
return { tabId: tab.id, groupId };
}
export async function groupMove({ group, forward, backward }) {
const groupId = await resolveGroupId(group);
const groupInfo = await chrome.tabGroups.get(groupId);
const allTabs = await chrome.tabs.query({ windowId: groupInfo.windowId });
allTabs.sort((a, b) => a.index - b.index);
const blocks = buildTabBlocks(allTabs);
const currentIdx = blocks.findIndex(block => block.groupId === groupId);
if (currentIdx === -1) throw new Error(`No tabs found in group '${group}'`);
const currentBlock = blocks[currentIdx];
const currentLength = currentBlock.tabIds.length;
if (forward) {
const nextBlock = blocks[currentIdx + 1];
if (!nextBlock) return { groupId, moved: false };
const targetIndex =
nextBlock.groupId === null
? currentBlock.startIndex + 1
: nextBlock.endIndex - currentLength + 1;
await chrome.tabGroups.move(groupId, { index: targetIndex });
} else if (backward) {
const previousBlock = blocks[currentIdx - 1];
if (!previousBlock) return { groupId, moved: false };
const targetIndex =
previousBlock.groupId === null
? currentBlock.startIndex - 1
: previousBlock.startIndex;
await chrome.tabGroups.move(groupId, { index: targetIndex });
}
return { groupId, moved: true };
}
// ── Windows ───────────────────────────────────────────────────────────────────
+560
View File
@@ -0,0 +1,560 @@
// @ts-nocheck
export function contentDispatch(funcName, args) {
function domQuery({ selector }) {
return Array.from(document.querySelectorAll(selector)).map(el => ({
tag: el.tagName.toLowerCase(),
text: el.textContent.trim().slice(0, 200),
attrs: Object.fromEntries(Array.from(el.attributes).map(a => [a.name, a.value])),
}));
}
function domClick({ selector }) {
const el = document.querySelector(selector);
if (!el) throw new Error(`No element: ${selector}`);
el.click();
return true;
}
function domType({ selector, text }) {
const el = document.querySelector(selector);
if (!el) throw new Error(`No element: ${selector}`);
el.focus();
el.value = text;
el.dispatchEvent(new Event("input", { bubbles: true }));
el.dispatchEvent(new Event("change", { bubbles: true }));
return true;
}
function domAttr({ selector, attr }) {
return Array.from(document.querySelectorAll(selector))
.map(el => el.getAttribute(attr))
.filter(v => v !== null);
}
function domText({ selector }) {
return Array.from(document.querySelectorAll(selector))
.map(el => el.textContent.trim())
.filter(Boolean);
}
function domExists({ selector }) {
return document.querySelector(selector) !== null;
}
function domKey({ selector, key }) {
const el = selector ? document.querySelector(selector) : document.activeElement;
if (selector && !el) throw new Error(`No element: ${selector}`);
const target = el || document.body;
["keydown", "keypress", "keyup"].forEach(type => {
target.dispatchEvent(new KeyboardEvent(type, { key, bubbles: true, cancelable: true }));
});
return true;
}
function domHover({ selector }) {
const el = document.querySelector(selector);
if (!el) throw new Error(`No element: ${selector}`);
el.dispatchEvent(new MouseEvent("mouseover", { bubbles: true }));
el.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }));
return true;
}
function domCheck({ selector, checked }) {
const el = document.querySelector(selector);
if (!el) throw new Error(`No element: ${selector}`);
el.checked = checked;
el.dispatchEvent(new Event("change", { bubbles: true }));
return true;
}
function domClear({ selector }) {
const el = document.querySelector(selector);
if (!el) throw new Error(`No element: ${selector}`);
el.value = "";
el.dispatchEvent(new Event("input", { bubbles: true }));
el.dispatchEvent(new Event("change", { bubbles: true }));
return true;
}
function domFocus({ selector }) {
const el = document.querySelector(selector);
if (!el) throw new Error(`No element: ${selector}`);
el.focus();
return true;
}
function domSubmit({ selector }) {
const el = document.querySelector(selector);
if (!el) throw new Error(`No element: ${selector}`);
const form = el.tagName === "FORM" ? el : el.closest("form");
if (!form) throw new Error(`No form found for: ${selector}`);
form.submit();
return true;
}
function pageInfo() {
const metas = {};
document.querySelectorAll("meta[name], meta[property]").forEach(m => {
const k = m.getAttribute("name") || m.getAttribute("property");
if (k) metas[k] = m.getAttribute("content") || "";
});
return {
title: document.title,
url: location.href,
readyState: document.readyState,
lang: document.documentElement.lang || null,
meta: metas,
};
}
function domScroll({ selector, x, y }) {
if (selector) {
const el = document.querySelector(selector);
if (!el) throw new Error(`No element: ${selector}`);
el.scrollIntoView({ behavior: "smooth", block: "center" });
return true;
}
window.scrollTo({ top: y || 0, left: x || 0, behavior: "smooth" });
return true;
}
function domSelect({ selector, value }) {
const el = document.querySelector(selector);
if (!el) throw new Error(`No element: ${selector}`);
el.value = value;
el.dispatchEvent(new Event("change", { bubbles: true }));
el.dispatchEvent(new Event("input", { bubbles: true }));
return true;
}
function extractLinks() {
const seen = new Set();
return Array.from(document.querySelectorAll("a[href]")).reduce((links, a) => {
const href = a.href;
if (!href || seen.has(href)) return links;
seen.add(href);
links.push({
text: a.textContent.trim().slice(0, 100),
href,
});
return links;
}, []);
}
function extractImages() {
const seen = new Set();
return Array.from(document.querySelectorAll("img")).reduce((images, img) => {
const src =
img.src ||
img.getAttribute("data-src") ||
img.getAttribute("data-lazy-src") ||
img.getAttribute("data-original") ||
(img.srcset ? img.srcset.split(",")[0].trim().split(" ")[0] : "") ||
"";
if (!src || seen.has(src)) return images;
seen.add(src);
const FAKE_ALT = new Set(["true", "false", "null", "undefined", "image", "img"]);
const alt = img.alt && !FAKE_ALT.has(img.alt.trim().toLowerCase()) ? img.alt.trim() : "";
images.push({ alt, src });
return images;
}, []);
}
function extractText() {
return document.body.innerText;
}
function extractJson({ selector }) {
const el = document.querySelector(selector);
if (!el) throw new Error(`No element: ${selector}`);
return JSON.parse(el.textContent);
}
function extractMarkdown({ selector }) {
const BLOCKS = new Set([
"article", "aside", "blockquote", "body", "div", "dl", "fieldset", "figcaption",
"figure", "footer", "form", "h1", "h2", "h3", "h4", "h5", "h6", "header", "hr",
"li", "main", "nav", "ol", "p", "pre", "section", "table", "tbody", "td", "tfoot",
"th", "thead", "tr", "ul"
]);
const NOISE_SELECTOR = [
"script",
"style",
"noscript",
"template",
"svg",
"canvas",
"iframe",
"dialog",
"button",
"input",
"textarea",
"select",
"option",
"form",
"[hidden]",
"[aria-hidden='true']",
".sr-only",
"[class*='sr-only']",
"[class*='file-tile']",
"form[data-type='unified-composer']",
".composer-btn",
"[data-composer-surface='true']",
"#thread-bottom-container",
"[data-testid*='action-button']",
].join(", ");
function normalizeText(value) {
return value.replace(/\s+/g, " ").trim();
}
function normalizeInline(value) {
return value
.replace(/[ \t]+\n/g, "\n")
.replace(/\n[ \t]+/g, "\n")
.replace(/\n{3,}/g, "\n\n")
.replace(/[ \t]{2,}/g, " ")
.trim();
}
function collapseBlankLines(value) {
return value
.replace(/[ \t]+\n/g, "\n")
.replace(/\n{3,}/g, "\n\n")
.trim();
}
function escapeMarkdown(text) {
return text.replace(/([\\`[\]])/g, "\\$1");
}
function escapeTableCell(text) {
return text.replace(/\|/g, "\\|").replace(/\n+/g, " ").trim();
}
function absoluteUrl(attr, fallback) {
return attr || fallback || "";
}
function isNoiseElement(node) {
if (!node || node.nodeType !== Node.ELEMENT_NODE) return false;
const tag = node.tagName.toLowerCase();
if (["script", "style", "noscript", "template", "svg", "canvas", "iframe", "dialog"].includes(tag)) return true;
if (["button", "input", "textarea", "select", "option", "form"].includes(tag)) return true;
if (node.hasAttribute("hidden")) return true;
if ((node.getAttribute("aria-hidden") || "").toLowerCase() === "true") return true;
if (node.matches(".sr-only, [class*='sr-only']")) return true;
if (node.matches("[class*='file-tile'], form[data-type='unified-composer'], .composer-btn, [data-composer-surface='true'], #thread-bottom-container")) return true;
if (node.matches("[data-testid*='action-button']")) return true;
return false;
}
function stripNoise(root) {
const clone = root.cloneNode(true);
clone.querySelectorAll(NOISE_SELECTOR).forEach(node => node.remove());
return clone;
}
function candidateScore(node) {
const text = normalizeText(node.innerText || "");
if (!text) return -Infinity;
const headings = node.querySelectorAll("h1, h2, h3, h4, h5, h6").length;
const paragraphs = node.querySelectorAll("p").length;
const listItems = node.querySelectorAll("li").length;
const tables = node.querySelectorAll("table").length;
const codeBlocks = node.querySelectorAll("pre, code").length;
const images = node.querySelectorAll("img, figure").length;
const mainLike = node.matches("main, article, [role='main']") ? 1 : 0;
const proseBlocks = node.matches(".markdown, .prose, [data-message-author-role='assistant']") ? 1 : 0;
const buttons = node.querySelectorAll("button, input, textarea, select").length;
const forms = node.querySelectorAll("form").length;
const svgs = node.querySelectorAll("svg, canvas").length;
return text.length
+ (mainLike * 4000)
+ (proseBlocks * 5000)
+ (headings * 250)
+ (paragraphs * 60)
+ (listItems * 35)
+ (tables * 80)
+ (codeBlocks * 60)
+ (images * 25)
- (buttons * 120)
- (forms * 200)
- (svgs * 40);
}
function pickRoot() {
if (selector) {
const matched = document.querySelector(selector);
if (!matched) throw new Error(`No element: ${selector}`);
return matched;
}
const candidates = Array.from(document.querySelectorAll(
"main, article, [role='main'], section, .markdown, .prose, [data-message-author-role]"
))
.filter(node => normalizeText(node.innerText || "").length > 0);
if (!candidates.length) return document.body;
candidates.sort((a, b) => candidateScore(b) - candidateScore(a));
return candidates[0];
}
function inlineText(node) {
if (node.nodeType === Node.TEXT_NODE) {
return escapeMarkdown(node.textContent || "");
}
if (node.nodeType !== Node.ELEMENT_NODE) return "";
if (isNoiseElement(node)) return "";
const tag = node.tagName.toLowerCase();
if (tag === "br") return "\n";
if (tag === "img") {
const src = absoluteUrl(node.getAttribute("src"), node.src);
if (!src) return "";
const alt = normalizeText(node.getAttribute("alt") || "");
return alt ? `![${escapeMarkdown(alt)}](${src})` : `![](${src})`;
}
if (tag === "a") {
const text = normalizeInline(Array.from(node.childNodes).map(inlineText).join(""));
const href = absoluteUrl(node.getAttribute("href"), node.href);
if (!href) return text;
return `[${text || href}](${href})`;
}
if (tag === "code") {
const text = normalizeInline(Array.from(node.childNodes).map(inlineText).join(""));
return text ? `\`${text.replace(/`/g, "\\`")}\`` : "";
}
if (tag === "strong" || tag === "b") {
const text = normalizeInline(Array.from(node.childNodes).map(inlineText).join(""));
return text ? `**${text}**` : "";
}
if (tag === "em" || tag === "i") {
const text = normalizeInline(Array.from(node.childNodes).map(inlineText).join(""));
return text ? `*${text}*` : "";
}
const chunks = [];
for (const child of node.childNodes) {
const rendered = inlineText(child);
if (!rendered) continue;
chunks.push(rendered);
if (child.nodeType === Node.ELEMENT_NODE && BLOCKS.has(child.tagName.toLowerCase())) {
chunks.push("\n");
}
}
return chunks.join("");
}
function textBlock(node) {
return collapseBlankLines(normalizeInline(Array.from(node.childNodes).map(inlineText).join("")));
}
function preserveNodeText(node) {
if (node.nodeType === Node.TEXT_NODE) {
return node.textContent || "";
}
if (node.nodeType !== Node.ELEMENT_NODE) return "";
const tag = node.tagName.toLowerCase();
if (tag === "br") return "\n";
const parts = [];
for (const child of node.childNodes) {
const rendered = preserveNodeText(child);
if (!rendered) continue;
parts.push(rendered);
}
if (["div", "p", "li"].includes(tag)) {
return `${parts.join("")}\n`;
}
return parts.join("");
}
function repairFlattenedDiagram(text) {
if (text.includes("\n")) return text;
const markerCount = (text.match(/[│▼├└]/g) || []).length;
if (markerCount < 2) return text;
let repaired = text;
repaired = repaired.replace(/\s{2,}([│▼])/g, "\n $1");
repaired = repaired.replace(/([│▼])\s{2,}/g, "$1\n");
repaired = repaired.replace(/([│▼])(?=[^\s\n│▼├└])/g, "$1\n");
repaired = repaired.replace(/(?<=[^\s\n])([├└])/g, "\n$1");
repaired = repaired.replace(/([^\s\n])(\()/g, "$1\n$2");
return repaired
.split("\n")
.map(line => line.replace(/\s+$/, ""))
.filter(line => line.trim())
.join("\n");
}
function convertDashListsToBranches(lines) {
const converted = [];
let index = 0;
while (index < lines.length) {
const match = lines[index].match(/^(\s*)-\s+(.*)$/);
if (!match) {
converted.push(lines[index]);
index += 1;
continue;
}
const indent = match[1];
const items = [];
while (index < lines.length) {
const nextMatch = lines[index].match(new RegExp(`^${indent.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}-\\s+(.*)$`));
if (!nextMatch) break;
items.push(nextMatch[1]);
index += 1;
}
items.forEach((item, itemIndex) => {
const branch = itemIndex === items.length - 1 ? "└" : "├";
converted.push(`${indent}${branch} ${item}`);
});
}
return converted;
}
function normalizeCodeBlock(text) {
let lines = text.replace(/\r\n?/g, "\n").split("\n").map(line => line.replace(/\s+$/, ""));
while (lines.length && !lines[0].trim()) lines.shift();
while (lines.length && !lines[lines.length - 1].trim()) lines.pop();
const flattened = repairFlattenedDiagram(lines.join("\n"));
lines = flattened ? flattened.split("\n") : [];
lines = lines.map(line => {
const trimmed = line.trim();
if ((trimmed === "│" || trimmed === "▼") && !/^\s+[│▼]\s*$/.test(line)) {
return ` ${trimmed}`;
}
return line;
});
lines = convertDashListsToBranches(lines);
return lines.join("\n");
}
function tableToMarkdown(table) {
const rows = Array.from(table.querySelectorAll("tr"))
.map(row => Array.from(row.children)
.filter(cell => cell.tagName === "TD" || cell.tagName === "TH")
.map(cell => escapeTableCell(textBlock(cell)))
)
.filter(cells => cells.length > 0);
if (!rows.length) return "";
const widths = rows.reduce((max, row) => Math.max(max, row.length), 0);
const normalizedRows = rows.map(row => {
const next = row.slice();
while (next.length < widths) next.push("");
return next;
});
let headers = normalizedRows[0];
let bodyRows = normalizedRows.slice(1);
const firstRowIsBlank = headers.every(cell => !cell.trim());
if (firstRowIsBlank && normalizedRows.length > 1) {
headers = normalizedRows[1];
bodyRows = normalizedRows.slice(2);
}
const firstRow = table.querySelector("tr");
const thead = table.querySelector("thead");
const firstRowHasTh = firstRow && Array.from(firstRow.children).some(cell => cell.tagName === "TH");
if (!(thead || firstRowHasTh || firstRowIsBlank)) {
headers = new Array(widths).fill("");
bodyRows = normalizedRows;
}
const separator = new Array(widths).fill("---");
const lines = [
`| ${headers.join(" | ")} |`,
`| ${separator.join(" | ")} |`,
];
for (const row of bodyRows) {
lines.push(`| ${row.join(" | ")} |`);
}
return lines.join("\n");
}
function listToMarkdown(list, depth = 0) {
const ordered = list.tagName.toLowerCase() === "ol";
const items = [];
const children = Array.from(list.children).filter(child => child.tagName === "LI");
children.forEach((item, index) => {
const marker = ordered ? `${index + 1}. ` : "- ";
const indent = " ".repeat(depth);
const nested = [];
const content = [];
for (const child of item.childNodes) {
if (child.nodeType === Node.ELEMENT_NODE && (child.tagName === "UL" || child.tagName === "OL")) {
nested.push(listToMarkdown(child, depth + 1));
} else {
content.push(inlineText(child));
}
}
const line = collapseBlankLines(normalizeInline(content.join("")));
if (line) {
const lineParts = line.split("\n");
items.push(`${indent}${marker}${lineParts[0]}`);
const continuationIndent = `${indent}${" ".repeat(marker.length)}`;
lineParts.slice(1).forEach(part => items.push(`${continuationIndent}${part}`));
}
nested.filter(Boolean).forEach(block => items.push(block));
});
return items.join("\n");
}
function blockToMarkdown(node) {
if (node.nodeType === Node.TEXT_NODE) {
return normalizeText(node.textContent || "");
}
if (node.nodeType !== Node.ELEMENT_NODE) return "";
if (isNoiseElement(node)) return "";
const tag = node.tagName.toLowerCase();
if (tag === "table") return tableToMarkdown(node);
if (tag === "ul" || tag === "ol") return listToMarkdown(node);
if (node.matches(".cm-editor[data-is-code-block-view='true']")) {
const lines = Array.from(node.querySelectorAll(".cm-line")).map(line => {
const text = preserveNodeText(line);
return text === "\n" ? "" : text.replace(/\n$/, "");
});
const code = normalizeCodeBlock(lines.join("\n"));
return code ? `\`\`\`\n${code}\n\`\`\`` : "";
}
if (tag === "pre") {
const code = normalizeCodeBlock(preserveNodeText(node));
return code ? `\`\`\`\n${code}\n\`\`\`` : "";
}
if (tag === "blockquote") {
const content = collapseBlankLines(Array.from(node.childNodes).map(blockToMarkdown).join("\n\n"));
return content
.split("\n")
.map(line => line ? `> ${line}` : ">")
.join("\n");
}
if (/^h[1-6]$/.test(tag)) {
const level = Number(tag.slice(1));
const text = textBlock(node);
return text ? `${"#".repeat(level)} ${text}` : "";
}
if (tag === "p" || tag === "figcaption") {
return textBlock(node);
}
if (tag === "hr") {
return "---";
}
if (tag === "img") {
return inlineText(node);
}
const childBlocks = Array.from(node.childNodes)
.map(child => blockToMarkdown(child))
.filter(Boolean);
if (childBlocks.length) return collapseBlankLines(childBlocks.join("\n\n"));
return textBlock(node);
}
const root = stripNoise(pickRoot());
const markdown = blockToMarkdown(root);
return collapseBlankLines(markdown);
}
const fns = { domQuery, domClick, domType, domAttr, domText, domExists,
domScroll, domSelect, domKey, domHover, domCheck, domClear, domFocus, domSubmit,
pageInfo,
extractLinks, extractImages, extractText, extractJson, extractMarkdown };
const fn = fns[funcName];
if (!fn) throw new Error(`Unknown content function: ${funcName}`);
return fn(args);
}
// ── Session ───────────────────────────────────────────────────────────────────
+93
View File
@@ -0,0 +1,93 @@
// @ts-nocheck
import { getActiveTab, getAliases, resolveGroupId, tabInfo } from '../core';
export async function navOpen({ url, background, window: windowName, windowId: explicitWindowId, group: groupNameOrId }) {
let windowId;
if (explicitWindowId != null) {
windowId = explicitWindowId;
} else if (windowName) {
const aliases = await getAliases();
const entry = Object.entries(aliases).find(([, v]) => v === windowName);
if (entry) windowId = parseInt(entry[0]);
}
const tab = await chrome.tabs.create({ url, active: !background, windowId });
if (groupNameOrId != null) {
let groupId;
try {
groupId = await resolveGroupId(groupNameOrId);
// Close any blank placeholder tabs that were created when the group was made
const groupTabs = await chrome.tabs.query({ groupId });
const placeholders = groupTabs.filter(t =>
t.id !== tab.id &&
(t.url === "chrome://newtab/" || t.url === "about:blank" || t.pendingUrl === "chrome://newtab/")
);
await chrome.tabs.group({ tabIds: [tab.id], groupId });
if (placeholders.length) await chrome.tabs.remove(placeholders.map(t => t.id));
} catch (e) {
if (!e.message.startsWith("No tab group found")) throw e;
// Group doesn't exist — create it with the tab already in it
groupId = await chrome.tabs.group({ tabIds: [tab.id] });
await chrome.tabGroups.update(groupId, { title: String(groupNameOrId) });
}
}
return { id: tab.id, url: tab.url };
}
export async function navTo({ tabId, url }) {
const tab = await chrome.tabs.update(tabId, { url });
return { id: tab.id, url: tab.url || url };
}
export async function navReload({ tabId }, bypassCache) {
const tab = tabId ? { id: tabId } : await getActiveTab();
await chrome.tabs.reload(tab.id, { bypassCache });
return { tabId: tab.id };
}
export async function navBack({ tabId }) {
const tab = tabId ? { id: tabId } : await getActiveTab();
await chrome.tabs.goBack(tab.id);
return { tabId: tab.id };
}
export async function navForward({ tabId }) {
const tab = tabId ? { id: tabId } : await getActiveTab();
await chrome.tabs.goForward(tab.id);
return { tabId: tab.id };
}
export async function navFocus({ pattern }) {
// If pattern is a plain integer, treat it as a tab ID
const asInt = parseInt(pattern);
let match;
if (!isNaN(asInt) && String(asInt) === String(pattern)) {
match = await chrome.tabs.get(asInt);
} else {
const all = await chrome.tabs.query({});
match = all.find(t => (t.url && t.url.includes(pattern)) || (t.pendingUrl && t.pendingUrl.includes(pattern)));
}
if (!match) return null;
await chrome.windows.update(match.windowId, { focused: true });
await chrome.tabs.update(match.id, { active: true });
return { id: match.id, url: match.url || match.pendingUrl, title: match.title };
}
export async function navWait({ tabId, timeout = 30000, readyState = "complete" } = {}) {
const tab = tabId ? { id: tabId } : await getActiveTab();
const deadline = Date.now() + timeout;
const interval = 200;
while (Date.now() < deadline) {
const t = await chrome.tabs.get(tab.id);
if (readyState === "complete" ? t.status === "complete" : t.status !== "loading") {
return tabInfo(t);
}
await new Promise(r => setTimeout(r, interval));
}
throw new Error(`Tab ${tab.id} did not reach status '${readyState}' within ${timeout}ms`);
}
export async function navOpenWait({ url, timeout = 30000, background, window: windowName, group } = {}) {
const opened = await navOpen({ url, background, window: windowName, group });
return await navWait({ tabId: opened.id, timeout });
}
// ── Tabs ──────────────────────────────────────────────────────────────────────
+127
View File
@@ -0,0 +1,127 @@
// @ts-nocheck
import { getProfileAlias, getSessionTabs, getSessions, normalizeGroupColor } from '../core';
export async function sessionSave({ name }) {
const tabs = await chrome.tabs.query({});
const groups = await chrome.tabGroups.query({});
const groupById = new Map(groups.map(group => [group.id, group]));
const sessionTabs = tabs
.filter(tab => Boolean(tab.url))
.sort((a, b) => (a.windowId - b.windowId) || (a.index - b.index))
.map(tab => {
const entry = { url: tab.url };
if (tab.groupId >= 0) {
const group = groupById.get(tab.groupId);
entry.group = {
key: `${tab.windowId}:${tab.groupId}`,
title: group?.title || "",
color: normalizeGroupColor(group?.color),
collapsed: Boolean(group?.collapsed),
};
}
return entry;
});
const sessions = await getSessions();
sessions[name] = {
tabs: sessionTabs,
urls: sessionTabs.map(tab => tab.url),
savedAt: Date.now(),
};
await chrome.storage.local.set({ sessions });
return { name, tabs: sessionTabs.length };
}
export async function sessionLoad({ name }) {
const sessions = await getSessions();
const session = sessions[name];
if (!session) throw new Error(`Session '${name}' not found`);
const sessionTabs = getSessionTabs(session);
const createdTabs = [];
for (const entry of sessionTabs) {
const tab = await chrome.tabs.create({ url: entry.url, active: false });
createdTabs.push({ tabId: tab.id, entry });
}
const groups = new Map();
for (const { tabId, entry } of createdTabs) {
if (!entry.group) continue;
const key = entry.group.key || `${entry.group.title || "group"}:${groups.size}`;
if (!groups.has(key)) {
groups.set(key, { meta: entry.group, tabIds: [] });
}
groups.get(key).tabIds.push(tabId);
}
for (const { meta, tabIds } of groups.values()) {
const restoredGroupId = await chrome.tabs.group({ tabIds });
await chrome.tabGroups.update(restoredGroupId, {
title: meta.title || "",
color: normalizeGroupColor(meta.color),
collapsed: Boolean(meta.collapsed),
});
}
return { name, tabs: sessionTabs.length };
}
export async function sessionList() {
const sessions = await getSessions();
return Object.entries(sessions).map(([name, s]) => ({
name,
tabs: getSessionTabs(s).length,
savedAt: s.savedAt || null,
}));
}
export async function sessionRemove({ name }) {
const sessions = await getSessions();
if (!(name in sessions)) throw new Error(`Session '${name}' not found`);
delete sessions[name];
await chrome.storage.local.set({ sessions });
return { name };
}
export async function sessionDiff({ nameA, nameB }) {
const sessions = await getSessions();
const a = new Set(getSessionTabs(sessions[nameA]).map(tab => tab.url));
const b = new Set(getSessionTabs(sessions[nameB]).map(tab => tab.url));
return {
added: [...b].filter(u => !a.has(u)),
removed: [...a].filter(u => !b.has(u)),
};
}
export async function sessionAutoSave({ enabled }) {
await chrome.storage.local.set({ autoSave: enabled });
if (enabled) {
chrome.tabs.onUpdated.addListener(autoSaveHandler);
chrome.tabs.onRemoved.addListener(autoSaveHandler);
}
return { enabled };
}
export async function autoSaveHandler() {
const { autoSave } = await chrome.storage.local.get("autoSave");
if (!autoSave) return;
await sessionSave({ name: "__auto__" });
}
// ── Misc ──────────────────────────────────────────────────────────────────────
export async function clientsList() {
const manifest = chrome.runtime.getManifest();
const alias = await getProfileAlias();
return [{
name: "Chrome",
version: navigator.userAgent.match(/Chrome\/([\d.]+)/)?.[1] || "unknown",
platform: navigator.platform,
extensionVersion: manifest.version,
profile: alias,
}];
}
export async function clientsRenameProfile({ alias }) {
await chrome.storage.local.set({ profileAlias: alias });
return { alias };
}
+214
View File
@@ -0,0 +1,214 @@
// @ts-nocheck
import { executeScript, getActiveTab, isScriptableUrl, resolveTabForDirectAction } from '../core';
export async function tabsList() {
const windows = await chrome.windows.getAll({ populate: true });
const aliases = await getAliases();
const tabs = [];
for (const w of windows) {
for (const t of w.tabs) {
tabs.push({
...tabInfo(t),
windowAlias: aliases[t.windowId] || null,
pinned: t.pinned,
favIconUrl: t.favIconUrl,
});
}
}
return tabs;
}
export async function tabsClose({ tabId, inactive, duplicates }) {
let toClose = [];
if (duplicates) {
const all = await chrome.tabs.query({});
const seen = new Set();
for (const t of all) {
if (seen.has(t.url)) toClose.push(t.id);
else seen.add(t.url);
}
} else if (inactive) {
const all = await chrome.tabs.query({});
toClose = all.filter(t => !t.active).map(t => t.id);
} else if (tabId) {
toClose = [tabId];
}
if (toClose.length) await chrome.tabs.remove(toClose);
return { closed: toClose.length };
}
export async function tabsMove({ tabId, groupId, windowId, index, forward, backward }) {
const moveProps = {};
if (windowId != null) moveProps.windowId = windowId;
if (forward || backward) {
const tab = await chrome.tabs.get(tabId);
if (forward) moveProps.index = tab.index + 2; // +2 because Chrome shifts after removal
else moveProps.index = Math.max(0, tab.index - 1);
} else if (index != null) {
moveProps.index = index;
} else {
moveProps.index = -1;
}
await chrome.tabs.move(tabId, moveProps);
if (groupId != null) {
await chrome.tabs.group({ tabIds: [tabId], groupId });
}
return { tabId };
}
export async function tabsActive({ tabId }) {
const tab = await chrome.tabs.get(tabId);
await chrome.windows.update(tab.windowId, { focused: true });
await chrome.tabs.update(tabId, { active: true });
return { tabId };
}
export async function tabsActiveInWindow({ windowId }) {
const activeTabs = await chrome.tabs.query({ windowId, active: true });
const tab = activeTabs[0];
if (!tab) {
throw new Error(`No active tab found for window ${windowId}`);
}
return tabInfo(tab);
}
export async function tabsStatus({ tabId }) {
const tab = tabId ? await chrome.tabs.get(tabId) : await getActiveTab();
return tabInfo(tab);
}
export async function tabsFilter({ pattern }) {
const all = await chrome.tabs.query({});
return all.filter(t => t.url && t.url.includes(pattern)).map(tabInfo);
}
export async function tabsCount({ pattern }) {
const all = await chrome.tabs.query({});
if (pattern) return all.filter(t => t.url && t.url.includes(pattern)).length;
return all.length;
}
export async function tabsQuery({ search }) {
const q = search.toLowerCase();
const all = await chrome.tabs.query({});
return all.filter(t =>
(t.url && t.url.toLowerCase().includes(q)) ||
(t.title && t.title.toLowerCase().includes(q))
).map(tabInfo);
}
export async function tabsHtml({ tabId }) {
for (let i = 0; i < 3; i++) {
const tab = tabId ? await chrome.tabs.get(tabId) : await getActiveTab();
if (!isScriptableUrl(tab.url || tab.pendingUrl || "")) {
throw new Error(`Cannot get HTML of ${tab.url || tab.pendingUrl} — navigate to a regular web page first`);
}
try {
const results = await executeScript({
target: { tabId: tab.id },
func: () => document.documentElement.outerHTML,
});
return results[0]?.result || "";
} catch (e) {
const transient = e.message && (e.message.includes("Frame with ID") || e.message.includes("No tab with id")) && !e.message.includes("error page");
if (i < 2 && transient) {
await new Promise(r => setTimeout(r, 300));
continue;
}
throw e;
}
}
}
export async function tabsDedupe() {
return tabsClose({ duplicates: true });
}
export async function tabsSort({ by }) {
const windows = await chrome.windows.getAll({ populate: true });
let moved = 0;
for (const w of windows) {
const sorted = [...w.tabs].sort((a, b) => {
if (by === "title") return (a.title || "").localeCompare(b.title || "");
if (by === "time") return a.id - b.id; // lower id = opened earlier
// domain (default)
const da = new URL(a.url || "about:blank").hostname;
const db = new URL(b.url || "about:blank").hostname;
return da.localeCompare(db);
});
for (let i = 0; i < sorted.length; i++) {
await chrome.tabs.move(sorted[i].id, { index: i });
moved++;
}
}
return { moved };
}
export async function tabsMergeWindows() {
const [focused] = await chrome.windows.getAll({ populate: false });
const current = await chrome.windows.getCurrent();
const all = await chrome.windows.getAll({ populate: true });
let moved = 0;
for (const w of all) {
if (w.id === current.id) continue;
const ids = w.tabs.map(t => t.id);
await chrome.tabs.move(ids, { windowId: current.id, index: -1 });
moved += ids.length;
}
return { moved };
}
export async function tabsPin({ tabId }) {
const tab = tabId ? await chrome.tabs.get(tabId) : await getActiveTab();
await chrome.tabs.update(tab.id, { pinned: true });
return { tabId: tab.id, pinned: true };
}
export async function tabsUnpin({ tabId }) {
const tab = tabId ? await chrome.tabs.get(tabId) : await getActiveTab();
await chrome.tabs.update(tab.id, { pinned: false });
return { tabId: tab.id, pinned: false };
}
export async function tabsScreenshot({ tabId, format = "png", quality } = {}) {
let windowId;
if (tabId) {
const tab = await chrome.tabs.get(tabId);
await chrome.tabs.update(tabId, { active: true });
windowId = tab.windowId;
} else {
const tab = await getActiveTab();
windowId = tab.windowId;
}
const opts = { format };
if (format === "jpeg" && quality != null) opts.quality = quality;
const dataUrl = await chrome.tabs.captureVisibleTab(windowId, opts);
return { dataUrl, format };
}
export async function tabsWatchUrl({ pattern, timeout = 30000, tabId } = {}) {
const tab = tabId ? await chrome.tabs.get(tabId) : await getActiveTab();
const deadline = Date.now() + timeout;
const regex = new RegExp(pattern);
while (Date.now() < deadline) {
const t = await chrome.tabs.get(tab.id);
const url = t.url || t.pendingUrl || "";
if (regex.test(url)) return tabInfo(t);
await new Promise(r => setTimeout(r, 200));
}
throw new Error(`Tab ${tab.id} URL did not match '${pattern}' within ${timeout}ms`);
}
export async function tabsMute({ tabId }) {
const tab = await resolveTabForDirectAction(tabId, "mute");
await chrome.tabs.update(tab.id, { muted: true });
return { tabId: tab.id, muted: true };
}
export async function tabsUnmute({ tabId }) {
const tab = await resolveTabForDirectAction(tabId, "unmute");
await chrome.tabs.update(tab.id, { muted: false });
return { tabId: tab.id, muted: false };
}
+34
View File
@@ -0,0 +1,34 @@
// @ts-nocheck
import { getAliases } from '../core';
export async function windowsList() {
const windows = await chrome.windows.getAll({ populate: true });
const aliases = await getAliases();
return windows.map(w => ({
id: w.id,
alias: aliases[w.id] || null,
focused: w.focused,
state: w.state,
tabCount: (w.tabs || []).length,
}));
}
export async function windowsRename({ windowId, name }) {
const aliases = await getAliases();
aliases[windowId] = name;
await chrome.storage.local.set({ windowAliases: aliases });
return { windowId, name };
}
export async function windowsClose({ windowId }) {
await chrome.windows.remove(windowId);
return { windowId };
}
export async function windowsOpen({ url }) {
const createData = { focused: true };
if (url) createData.url = url;
const w = await chrome.windows.create(createData);
return { id: w.id };
}
// ── DOM / Extract ─────────────────────────────────────────────────────────────
+131
View File
@@ -0,0 +1,131 @@
// @ts-nocheck
// Shared helpers for browser-cli extension command handlers.
export async function getProfileAlias() {
const { profileAlias } = await chrome.storage.local.get("profileAlias");
return profileAlias || "default";
}
export async function executeScript(options, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
return await chrome.scripting.executeScript(options);
} catch (e) {
if (i < retries - 1 && e.message && (e.message.includes("Frame with ID") || e.message.includes("No tab with id")) && !e.message.includes("error page")) {
await new Promise(r => setTimeout(r, 300));
continue;
}
throw e;
}
}
}
export function tabInfo(t) {
return {
id: t.id,
windowId: t.windowId,
active: t.active,
muted: Boolean(t.mutedInfo && t.mutedInfo.muted),
title: t.title,
url: t.url,
};
}
// ── Groups ────────────────────────────────────────────────────────────────────
export function isScriptableUrl(url) {
if (!url) return false;
return !url.startsWith("chrome://") &&
!url.startsWith("brave://") &&
!url.startsWith("about:") &&
!url.startsWith("edge://") &&
!url.startsWith("chrome-extension://");
}
export async function getActiveTab() {
const activeTabs = await chrome.tabs.query({ active: true });
if (!activeTabs.length) throw new Error("No active tab found");
const windows = await chrome.windows.getAll({ populate: false });
const focusedWindowIds = new Set(windows.filter(window => window.focused).map(window => window.id));
const chooseTab = (predicate) => activeTabs.find(predicate);
const byFocusAndScriptable = tab => focusedWindowIds.has(tab.windowId) && isScriptableUrl(tab.url || tab.pendingUrl || "");
const byScriptable = tab => isScriptableUrl(tab.url || tab.pendingUrl || "");
const byFocus = tab => focusedWindowIds.has(tab.windowId);
return chooseTab(byFocusAndScriptable)
|| chooseTab(byScriptable)
|| chooseTab(byFocus)
|| activeTabs[0];
}
export async function resolveTabForDirectAction(tabId, actionName) {
if (tabId != null) {
return chrome.tabs.get(tabId);
}
const allTabs = await chrome.tabs.query({});
if (allTabs.length !== 1) {
throw new Error(
`Refusing to ${actionName} without explicit tab ID when ${allTabs.length} tabs are open`
);
}
return allTabs[0];
}
export async function resolveGroupId(nameOrId) {
const asInt = parseInt(nameOrId);
if (!isNaN(asInt)) return asInt;
const groups = await chrome.tabGroups.query({});
const match = groups.find(g => g.title && g.title.toLowerCase() === String(nameOrId).toLowerCase());
if (!match) throw new Error(`No tab group found with name '${nameOrId}'`);
return match.id;
}
export function buildTabBlocks(tabs) {
const blocks = [];
for (const tab of tabs) {
const normalizedGroupId = tab.groupId >= 0 ? tab.groupId : null;
const lastBlock = blocks[blocks.length - 1];
if (lastBlock?.groupId === normalizedGroupId) {
lastBlock.tabIds.push(tab.id);
lastBlock.endIndex = tab.index;
continue;
}
blocks.push({
groupId: normalizedGroupId,
startIndex: tab.index,
endIndex: tab.index,
tabIds: [tab.id],
});
}
return blocks;
}
export function getSessionTabs(session) {
if (!session) return [];
if (Array.isArray(session.tabs)) {
return session.tabs
.map(entry => typeof entry === "string" ? { url: entry } : entry)
.filter(entry => entry?.url);
}
if (Array.isArray(session.urls)) {
return session.urls.filter(Boolean).map(url => ({ url }));
}
return [];
}
export function normalizeGroupColor(color) {
const allowed = new Set(["grey", "blue", "red", "yellow", "green", "pink", "purple", "cyan", "orange"]);
return allowed.has(color) ? color : "grey";
}
export async function getAliases() {
const { windowAliases } = await chrome.storage.local.get("windowAliases");
return windowAliases || {};
}
export async function getSessions() {
const { sessions } = await chrome.storage.local.get("sessions");
return sessions || {};
}
+246
View File
@@ -0,0 +1,246 @@
// @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 ────────────────────────────────────────────────────────────────