feat: add browser automation commands (v0.6.0)
Navigation: open-wait (open + block until loaded) DOM: key, hover, check/uncheck, clear, focus, submit, poll, scroll, select, eval, wait-for Tabs: pin/unpin, screenshot, watch-url (block until URL matches regex) New command groups: page info, storage get/set, cookies list/get/set Extension: add cookies permission
This commit is contained in:
@@ -122,6 +122,45 @@ class BrowserCLI:
|
||||
"""Navigate a specific tab to *url*."""
|
||||
self._cmd("navigate.to", {"tabId": tab_id, "url": url})
|
||||
|
||||
def open_wait(
|
||||
self,
|
||||
url: str,
|
||||
*,
|
||||
timeout: float = 30.0,
|
||||
background: bool = False,
|
||||
window: str | None = None,
|
||||
group: str | None = None,
|
||||
) -> "Tab":
|
||||
"""Open URL in a new tab and block until fully loaded. Returns the Tab."""
|
||||
data = self._cmd("navigate.open_wait", {
|
||||
"url": url, "timeout": int(timeout * 1000),
|
||||
"background": background, "window": window, "group": group,
|
||||
})
|
||||
return self._make_tab(data) if isinstance(data, dict) and "id" in data else data
|
||||
|
||||
def wait_for_load(
|
||||
self,
|
||||
tab_id: int | None = None,
|
||||
*,
|
||||
timeout: float = 30.0,
|
||||
ready_state: str = "complete",
|
||||
) -> Tab:
|
||||
"""Block until the tab finishes loading. Returns the Tab when ready.
|
||||
|
||||
Args:
|
||||
tab_id: Tab to watch. Defaults to the active tab.
|
||||
timeout: Max seconds to wait before raising ``RuntimeError``.
|
||||
ready_state: ``"complete"`` (default) or ``"interactive"``.
|
||||
"""
|
||||
data = self._cmd("navigate.wait", {
|
||||
"tabId": tab_id,
|
||||
"timeout": int(timeout * 1000),
|
||||
"readyState": ready_state,
|
||||
})
|
||||
if not isinstance(data, dict) or "id" not in data:
|
||||
raise RuntimeError("navigate.wait returned unexpected data")
|
||||
return self._make_tab(data)
|
||||
|
||||
# ── Search ────────────────────────────────────────────────────────────
|
||||
|
||||
def search(
|
||||
@@ -190,6 +229,44 @@ class BrowserCLI:
|
||||
result = self._cmd("tabs.unmute", {"tabId": tab_id})
|
||||
return result.get("tabId", tab_id) if isinstance(result, dict) else int(tab_id or 0)
|
||||
|
||||
def tabs_pin(self, tab_id: int | None = None) -> int:
|
||||
"""Pin the active tab or a specific tab. Returns the target tab ID."""
|
||||
result = self._cmd("tabs.pin", {"tabId": tab_id})
|
||||
return result.get("tabId", tab_id) if isinstance(result, dict) else int(tab_id or 0)
|
||||
|
||||
def tabs_unpin(self, tab_id: int | None = None) -> int:
|
||||
"""Unpin the active tab or a specific tab. Returns the target tab ID."""
|
||||
result = self._cmd("tabs.unpin", {"tabId": tab_id})
|
||||
return result.get("tabId", tab_id) if isinstance(result, dict) else int(tab_id or 0)
|
||||
|
||||
def tabs_watch_url(
|
||||
self,
|
||||
pattern: str,
|
||||
*,
|
||||
tab_id: int | None = None,
|
||||
timeout: float = 30.0,
|
||||
) -> "Tab":
|
||||
"""Block until the tab URL matches regex pattern. Returns the Tab."""
|
||||
data = self._cmd("tabs.watch_url", {"pattern": pattern, "tabId": tab_id, "timeout": int(timeout * 1000)})
|
||||
return self._make_tab(data) if isinstance(data, dict) and "id" in data else data
|
||||
|
||||
def tabs_screenshot(
|
||||
self,
|
||||
tab_id: int | None = None,
|
||||
*,
|
||||
format: str = "png",
|
||||
quality: int | None = None,
|
||||
) -> str:
|
||||
"""Capture the visible area of a tab. Returns a base64 data URL.
|
||||
|
||||
Args:
|
||||
tab_id: Tab to capture. Defaults to the active tab.
|
||||
format: ``"png"`` (default) or ``"jpeg"``.
|
||||
quality: JPEG quality 0-100 (ignored for PNG).
|
||||
"""
|
||||
result = self._cmd("tabs.screenshot", {"tabId": tab_id, "format": format, "quality": quality})
|
||||
return result.get("dataUrl", "") if isinstance(result, dict) else str(result)
|
||||
|
||||
def window_active_tab(self, window_id: int) -> Tab:
|
||||
"""Return active tab for a specific browser window."""
|
||||
data = self._cmd("tabs.active_in_window", {"windowId": window_id})
|
||||
@@ -346,6 +423,161 @@ class BrowserCLI:
|
||||
def dom_exists(self, selector: str) -> bool:
|
||||
return self._cmd("dom.exists", {"selector": selector})
|
||||
|
||||
def dom_scroll(self, selector: str | None = None, *, x: int | None = None, y: int | None = None) -> None:
|
||||
"""Scroll to a CSS selector or to pixel coordinates."""
|
||||
self._cmd("dom.scroll", {"selector": selector, "x": x, "y": y})
|
||||
|
||||
def dom_select(self, selector: str, value: str) -> None:
|
||||
"""Set the value of a <select> element."""
|
||||
self._cmd("dom.select", {"selector": selector, "value": value})
|
||||
|
||||
def dom_eval(self, code: str, tab_id: int | None = None):
|
||||
"""Evaluate JavaScript in the page's main world and return the result."""
|
||||
return self._cmd("dom.eval", {"code": code, "tabId": tab_id})
|
||||
|
||||
def dom_key(self, key: str, selector: str | None = None) -> None:
|
||||
"""Dispatch a keyboard event. key examples: 'Enter', 'Tab', 'Escape', 'ArrowDown'."""
|
||||
self._cmd("dom.key", {"key": key, "selector": selector})
|
||||
|
||||
def dom_hover(self, selector: str) -> None:
|
||||
"""Dispatch mouseover/mouseenter on an element."""
|
||||
self._cmd("dom.hover", {"selector": selector})
|
||||
|
||||
def dom_check(self, selector: str) -> None:
|
||||
"""Check a checkbox."""
|
||||
self._cmd("dom.check", {"selector": selector})
|
||||
|
||||
def dom_uncheck(self, selector: str) -> None:
|
||||
"""Uncheck a checkbox."""
|
||||
self._cmd("dom.uncheck", {"selector": selector})
|
||||
|
||||
def dom_clear(self, selector: str) -> None:
|
||||
"""Clear the value of an input element."""
|
||||
self._cmd("dom.clear", {"selector": selector})
|
||||
|
||||
def dom_focus(self, selector: str) -> None:
|
||||
"""Focus an element."""
|
||||
self._cmd("dom.focus", {"selector": selector})
|
||||
|
||||
def dom_submit(self, selector: str) -> None:
|
||||
"""Submit the form containing the matched element."""
|
||||
self._cmd("dom.submit", {"selector": selector})
|
||||
|
||||
def dom_poll(
|
||||
self,
|
||||
selector: str,
|
||||
pattern: str,
|
||||
*,
|
||||
attr: str | None = None,
|
||||
timeout: float = 30.0,
|
||||
interval: float = 0.5,
|
||||
tab_id: int | None = None,
|
||||
) -> dict:
|
||||
"""Poll selector's text/value until it matches regex pattern.
|
||||
|
||||
Returns ``{"selector": ..., "value": ..., "pattern": ...}`` when matched.
|
||||
"""
|
||||
return self._cmd("dom.poll", {
|
||||
"selector": selector,
|
||||
"pattern": pattern,
|
||||
"attr": attr,
|
||||
"timeout": int(timeout * 1000),
|
||||
"interval": int(interval * 1000),
|
||||
"tabId": tab_id,
|
||||
})
|
||||
|
||||
def dom_wait_for(
|
||||
self,
|
||||
selector: str,
|
||||
*,
|
||||
timeout: float = 10.0,
|
||||
visible: bool = False,
|
||||
hidden: bool = False,
|
||||
tab_id: int | None = None,
|
||||
) -> dict:
|
||||
"""Wait until a CSS selector appears (or disappears) in the DOM.
|
||||
|
||||
Args:
|
||||
selector: CSS selector to watch.
|
||||
timeout: Max seconds to wait before raising ``RuntimeError``.
|
||||
visible: Wait until the element has non-zero dimensions.
|
||||
hidden: Wait until the element is absent or has ``offsetParent == null``.
|
||||
tab_id: Tab to watch. Defaults to the active tab.
|
||||
"""
|
||||
return self._cmd("dom.wait_for", {
|
||||
"selector": selector,
|
||||
"timeout": int(timeout * 1000),
|
||||
"visible": visible,
|
||||
"hidden": hidden,
|
||||
"tabId": tab_id,
|
||||
})
|
||||
|
||||
# ── Page ─────────────────────────────────────────────────────────────
|
||||
|
||||
def page_info(self) -> dict:
|
||||
"""Return title, URL, readyState, lang, and meta tags of the active tab."""
|
||||
return self._cmd("page.info", {}) or {}
|
||||
|
||||
# ── Storage ───────────────────────────────────────────────────────────
|
||||
|
||||
def storage_get(
|
||||
self,
|
||||
key: str | None = None,
|
||||
*,
|
||||
type: str = "local",
|
||||
tab_id: int | None = None,
|
||||
) -> str | dict | None:
|
||||
"""Get a localStorage/sessionStorage entry (or all entries if key omitted)."""
|
||||
return self._cmd("storage.get", {"key": key, "type": type, "tabId": tab_id})
|
||||
|
||||
def storage_set(
|
||||
self,
|
||||
key: str,
|
||||
value: str,
|
||||
*,
|
||||
type: str = "local",
|
||||
tab_id: int | None = None,
|
||||
) -> None:
|
||||
"""Set a localStorage/sessionStorage entry."""
|
||||
self._cmd("storage.set", {"key": key, "value": value, "type": type, "tabId": tab_id})
|
||||
|
||||
# ── Cookies ───────────────────────────────────────────────────────────
|
||||
|
||||
def cookies_list(
|
||||
self,
|
||||
*,
|
||||
url: str | None = None,
|
||||
domain: str | None = None,
|
||||
name: str | None = None,
|
||||
) -> list[dict]:
|
||||
"""List cookies, optionally filtered by url, domain, or name."""
|
||||
return self._cmd("cookies.list", {"url": url, "domain": domain, "name": name}) or []
|
||||
|
||||
def cookies_get(self, url: str, name: str) -> dict | None:
|
||||
"""Get a single cookie by url and name."""
|
||||
return self._cmd("cookies.get", {"url": url, "name": name})
|
||||
|
||||
def cookies_set(
|
||||
self,
|
||||
url: str,
|
||||
name: str,
|
||||
value: str,
|
||||
*,
|
||||
domain: str | None = None,
|
||||
path: str | None = None,
|
||||
secure: bool | None = None,
|
||||
http_only: bool | None = None,
|
||||
expiration_date: float | None = None,
|
||||
same_site: str | None = None,
|
||||
) -> dict:
|
||||
"""Set a cookie. Returns the created cookie dict."""
|
||||
return self._cmd("cookies.set", {
|
||||
"url": url, "name": name, "value": value,
|
||||
"domain": domain, "path": path,
|
||||
"secure": secure, "httpOnly": http_only,
|
||||
"expirationDate": expiration_date, "sameSite": same_site,
|
||||
})
|
||||
|
||||
# ── Extract ───────────────────────────────────────────────────────────
|
||||
|
||||
def extract_links(self) -> list[dict]:
|
||||
|
||||
@@ -21,6 +21,9 @@ from browser_cli.commands.dom import dom_group
|
||||
from browser_cli.commands.extract import extract_group
|
||||
from browser_cli.commands.session import session_group
|
||||
from browser_cli.commands.search import search_group
|
||||
from browser_cli.commands.page import page_group
|
||||
from browser_cli.commands.storage import storage_group
|
||||
from browser_cli.commands.cookies import cookies_group
|
||||
from browser_cli.client import (
|
||||
send_command,
|
||||
BrowserNotConnected,
|
||||
@@ -179,6 +182,9 @@ main.add_command(dom_group)
|
||||
main.add_command(extract_group)
|
||||
main.add_command(session_group)
|
||||
main.add_command(search_group)
|
||||
main.add_command(page_group)
|
||||
main.add_command(storage_group)
|
||||
main.add_command(cookies_group)
|
||||
|
||||
|
||||
# ── clients ────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import click
|
||||
from browser_cli.client import send_command, BrowserNotConnected
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
||||
console = Console()
|
||||
|
||||
|
||||
def _handle(command, args=None):
|
||||
try:
|
||||
return send_command(command, args or {})
|
||||
except BrowserNotConnected as e:
|
||||
console.print(f"[red]Error:[/red] {e}")
|
||||
raise SystemExit(1)
|
||||
except RuntimeError as e:
|
||||
console.print(f"[red]Browser error:[/red] {e}")
|
||||
raise SystemExit(1)
|
||||
|
||||
|
||||
@click.group("cookies")
|
||||
def cookies_group():
|
||||
"""Manage browser cookies."""
|
||||
|
||||
|
||||
@cookies_group.command("list")
|
||||
@click.option("--url", default=None, help="Filter by URL")
|
||||
@click.option("--domain", default=None, help="Filter by domain")
|
||||
@click.option("--name", default=None, help="Filter by cookie name")
|
||||
def cookies_list(url, domain, name):
|
||||
"""List cookies, optionally filtered by URL, domain, or name."""
|
||||
cookies = _handle("cookies.list", {"url": url, "domain": domain, "name": name}) or []
|
||||
if not cookies:
|
||||
console.print("[yellow]No cookies found[/yellow]")
|
||||
return
|
||||
table = Table(show_header=True, header_style="bold cyan")
|
||||
table.add_column("Name")
|
||||
table.add_column("Value")
|
||||
table.add_column("Domain")
|
||||
table.add_column("Path")
|
||||
table.add_column("Secure", width=7)
|
||||
table.add_column("HttpOnly", width=9)
|
||||
for c in cookies:
|
||||
table.add_row(
|
||||
c.get("name", ""),
|
||||
(c.get("value") or "")[:60],
|
||||
c.get("domain", ""),
|
||||
c.get("path", ""),
|
||||
"[green]✓[/green]" if c.get("secure") else "",
|
||||
"[green]✓[/green]" if c.get("httpOnly") else "",
|
||||
)
|
||||
console.print(table)
|
||||
|
||||
|
||||
@cookies_group.command("get")
|
||||
@click.argument("url")
|
||||
@click.argument("name")
|
||||
def cookies_get(url, name):
|
||||
"""Get the value of a single cookie by URL and NAME."""
|
||||
cookie = _handle("cookies.get", {"url": url, "name": name})
|
||||
if cookie is None:
|
||||
console.print(f"[yellow]Cookie '{name}' not found for {url}[/yellow]")
|
||||
raise SystemExit(1)
|
||||
console.print(cookie.get("value", ""))
|
||||
|
||||
|
||||
@cookies_group.command("set")
|
||||
@click.argument("url")
|
||||
@click.argument("name")
|
||||
@click.argument("value")
|
||||
@click.option("--domain", default=None)
|
||||
@click.option("--path", default=None)
|
||||
@click.option("--secure", is_flag=True)
|
||||
@click.option("--http-only", "http_only", is_flag=True)
|
||||
@click.option("--expires", "expiration_date", type=float, default=None, help="Unix timestamp")
|
||||
@click.option("--same-site", type=click.Choice(["no_restriction", "lax", "strict"]), default=None)
|
||||
def cookies_set(url, name, value, domain, path, secure, http_only, expiration_date, same_site):
|
||||
"""Set a cookie on URL."""
|
||||
_handle("cookies.set", {
|
||||
"url": url, "name": name, "value": value,
|
||||
"domain": domain, "path": path,
|
||||
"secure": secure or None,
|
||||
"httpOnly": http_only or None,
|
||||
"expirationDate": expiration_date,
|
||||
"sameSite": same_site,
|
||||
})
|
||||
console.print(f"[green]Set cookie:[/green] {name}={value!r} on {url}")
|
||||
@@ -87,3 +87,133 @@ def dom_exists(selector):
|
||||
else:
|
||||
console.print(f"[red]not found[/red]: {selector}")
|
||||
raise SystemExit(1)
|
||||
|
||||
|
||||
@dom_group.command("scroll")
|
||||
@click.argument("selector", required=False)
|
||||
@click.option("--x", type=int, default=None, help="Horizontal scroll position (px)")
|
||||
@click.option("--y", type=int, default=None, help="Vertical scroll position (px)")
|
||||
def dom_scroll(selector, x, y):
|
||||
"""Scroll to a CSS SELECTOR or to an X/Y coordinate."""
|
||||
_handle("dom.scroll", {"selector": selector, "x": x, "y": y})
|
||||
target = selector or f"({x or 0}, {y or 0})"
|
||||
console.print(f"[green]Scrolled to:[/green] {target}")
|
||||
|
||||
|
||||
@dom_group.command("select")
|
||||
@click.argument("selector")
|
||||
@click.argument("value")
|
||||
def dom_select(selector, value):
|
||||
"""Set the VALUE of a <select> dropdown matching CSS SELECTOR."""
|
||||
_handle("dom.select", {"selector": selector, "value": value})
|
||||
console.print(f"[green]Selected '{value}' in:[/green] {selector}")
|
||||
|
||||
|
||||
@dom_group.command("eval")
|
||||
@click.argument("code")
|
||||
@click.option("--tab", "tab_id", type=int, default=None, help="Tab ID (default: active tab)")
|
||||
def dom_eval(code, tab_id):
|
||||
"""Evaluate JavaScript CODE in the page and print the result."""
|
||||
result = _handle("dom.eval", {"code": code, "tabId": tab_id})
|
||||
if result is None:
|
||||
console.print("[dim]null[/dim]")
|
||||
else:
|
||||
console.print(json.dumps(result, indent=2) if isinstance(result, (dict, list)) else str(result))
|
||||
|
||||
|
||||
@dom_group.command("wait-for")
|
||||
@click.argument("selector")
|
||||
@click.option("--timeout", type=float, default=10.0, show_default=True, help="Max seconds to wait")
|
||||
@click.option("--visible", is_flag=True, help="Wait until element is visible (non-zero size)")
|
||||
@click.option("--hidden", is_flag=True, help="Wait until element is absent or hidden")
|
||||
@click.option("--tab", "tab_id", type=int, default=None, help="Tab ID (default: active tab)")
|
||||
def dom_wait_for(selector, timeout, visible, hidden, tab_id):
|
||||
"""Wait until CSS SELECTOR appears (or disappears) in the DOM."""
|
||||
_handle("dom.wait_for", {
|
||||
"selector": selector,
|
||||
"timeout": int(timeout * 1000),
|
||||
"visible": visible,
|
||||
"hidden": hidden,
|
||||
"tabId": tab_id,
|
||||
})
|
||||
state = "hidden" if hidden else ("visible" if visible else "present")
|
||||
console.print(f"[green]Ready ({state}):[/green] {selector}")
|
||||
|
||||
|
||||
@dom_group.command("key")
|
||||
@click.argument("key")
|
||||
@click.option("--selector", default=None, help="CSS selector to target (default: focused element)")
|
||||
def dom_key(key, selector):
|
||||
"""Dispatch a keyboard KEY event (e.g. Enter, Tab, Escape, ArrowDown)."""
|
||||
_handle("dom.key", {"key": key, "selector": selector})
|
||||
target = selector or "active element"
|
||||
console.print(f"[green]Key '{key}' sent to:[/green] {target}")
|
||||
|
||||
|
||||
@dom_group.command("hover")
|
||||
@click.argument("selector")
|
||||
def dom_hover(selector):
|
||||
"""Dispatch mouseover/mouseenter on the element matching CSS SELECTOR."""
|
||||
_handle("dom.hover", {"selector": selector})
|
||||
console.print(f"[green]Hovered:[/green] {selector}")
|
||||
|
||||
|
||||
@dom_group.command("check")
|
||||
@click.argument("selector")
|
||||
def dom_check(selector):
|
||||
"""Check a checkbox matching CSS SELECTOR."""
|
||||
_handle("dom.check", {"selector": selector})
|
||||
console.print(f"[green]Checked:[/green] {selector}")
|
||||
|
||||
|
||||
@dom_group.command("uncheck")
|
||||
@click.argument("selector")
|
||||
def dom_uncheck(selector):
|
||||
"""Uncheck a checkbox matching CSS SELECTOR."""
|
||||
_handle("dom.uncheck", {"selector": selector})
|
||||
console.print(f"[green]Unchecked:[/green] {selector}")
|
||||
|
||||
|
||||
@dom_group.command("clear")
|
||||
@click.argument("selector")
|
||||
def dom_clear(selector):
|
||||
"""Clear the value of an input matching CSS SELECTOR."""
|
||||
_handle("dom.clear", {"selector": selector})
|
||||
console.print(f"[green]Cleared:[/green] {selector}")
|
||||
|
||||
|
||||
@dom_group.command("focus")
|
||||
@click.argument("selector")
|
||||
def dom_focus(selector):
|
||||
"""Focus the element matching CSS SELECTOR."""
|
||||
_handle("dom.focus", {"selector": selector})
|
||||
console.print(f"[green]Focused:[/green] {selector}")
|
||||
|
||||
|
||||
@dom_group.command("submit")
|
||||
@click.argument("selector")
|
||||
def dom_submit(selector):
|
||||
"""Submit the form that contains the element matching CSS SELECTOR."""
|
||||
_handle("dom.submit", {"selector": selector})
|
||||
console.print(f"[green]Submitted form for:[/green] {selector}")
|
||||
|
||||
|
||||
@dom_group.command("poll")
|
||||
@click.argument("selector")
|
||||
@click.argument("pattern")
|
||||
@click.option("--attr", default=None, help="Attribute or property to read (default: textContent/value)")
|
||||
@click.option("--timeout", type=float, default=30.0, show_default=True, help="Max seconds to wait")
|
||||
@click.option("--interval", type=float, default=0.5, show_default=True, help="Poll interval in seconds")
|
||||
@click.option("--tab", "tab_id", type=int, default=None, help="Tab ID (default: active tab)")
|
||||
def dom_poll(selector, pattern, attr, timeout, interval, tab_id):
|
||||
"""Poll SELECTOR until its text/value matches regex PATTERN."""
|
||||
result = _handle("dom.poll", {
|
||||
"selector": selector,
|
||||
"pattern": pattern,
|
||||
"attr": attr,
|
||||
"timeout": int(timeout * 1000),
|
||||
"interval": int(interval * 1000),
|
||||
"tabId": tab_id,
|
||||
})
|
||||
value = result.get("value", "") if isinstance(result, dict) else ""
|
||||
console.print(f"[green]Matched:[/green] {selector!r} = {value!r}")
|
||||
|
||||
@@ -78,3 +78,29 @@ def cmd_focus(pattern):
|
||||
console.print(f"[green]Focused:[/green] {result.get('url', result)}")
|
||||
else:
|
||||
console.print(f"[yellow]No tab found matching:[/yellow] {pattern}")
|
||||
|
||||
|
||||
@nav_group.command("open-wait")
|
||||
@click.argument("url")
|
||||
@click.option("--timeout", type=float, default=30.0, show_default=True, help="Max seconds to wait for load")
|
||||
@click.option("--bg", is_flag=True, help="Open in background (no focus)")
|
||||
@click.option("--window", "window_name", default=None, help="Open in named window")
|
||||
@click.option("--group", "group_name", default=None, help="Open in tab group")
|
||||
def cmd_open_wait(url, timeout, bg, window_name, group_name):
|
||||
"""Open URL in a new tab and wait until fully loaded."""
|
||||
result = _handle("navigate.open_wait", {
|
||||
"url": url, "timeout": int(timeout * 1000),
|
||||
"background": bg, "window": window_name, "group": group_name,
|
||||
})
|
||||
title = result.get("title", "") if isinstance(result, dict) else ""
|
||||
console.print(f"[green]Loaded:[/green] {url}" + (f" — {title}" if title else ""))
|
||||
|
||||
|
||||
@nav_group.command("wait")
|
||||
@click.option("--tab", "tab_id", type=int, default=None, help="Tab ID (default: active tab)")
|
||||
@click.option("--timeout", type=float, default=30.0, show_default=True, help="Max seconds to wait")
|
||||
@click.option("--ready-state", type=click.Choice(["complete", "interactive"]), default="complete", show_default=True, help="Target ready state")
|
||||
def cmd_wait(tab_id, timeout, ready_state):
|
||||
"""Wait until tab finishes loading."""
|
||||
result = _handle("navigate.wait", {"tabId": tab_id, "timeout": int(timeout * 1000), "readyState": ready_state})
|
||||
console.print(f"[green]Ready:[/green] {result.get('url', '')} — {result.get('title', '')}")
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import click
|
||||
from browser_cli.client import send_command, BrowserNotConnected
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
||||
console = Console()
|
||||
|
||||
|
||||
def _handle(command, args=None):
|
||||
try:
|
||||
return send_command(command, args or {})
|
||||
except BrowserNotConnected as e:
|
||||
console.print(f"[red]Error:[/red] {e}")
|
||||
raise SystemExit(1)
|
||||
except RuntimeError as e:
|
||||
console.print(f"[red]Browser error:[/red] {e}")
|
||||
raise SystemExit(1)
|
||||
|
||||
|
||||
@click.group("page")
|
||||
def page_group():
|
||||
"""Inspect current page metadata."""
|
||||
|
||||
|
||||
@page_group.command("info")
|
||||
def page_info():
|
||||
"""Show title, URL, readyState, language, and meta tags of the active tab."""
|
||||
info = _handle("page.info") or {}
|
||||
table = Table(show_header=False)
|
||||
table.add_column("Field", style="bold cyan", no_wrap=True)
|
||||
table.add_column("Value")
|
||||
table.add_row("Title", info.get("title") or "")
|
||||
table.add_row("URL", info.get("url") or "")
|
||||
table.add_row("Ready", info.get("readyState") or "")
|
||||
table.add_row("Lang", info.get("lang") or "")
|
||||
for key, val in (info.get("meta") or {}).items():
|
||||
table.add_row(f"meta:{key}", val)
|
||||
console.print(table)
|
||||
@@ -0,0 +1,48 @@
|
||||
import json
|
||||
import click
|
||||
from browser_cli.client import send_command, BrowserNotConnected
|
||||
from rich.console import Console
|
||||
|
||||
console = Console()
|
||||
|
||||
|
||||
def _handle(command, args=None):
|
||||
try:
|
||||
return send_command(command, args or {})
|
||||
except BrowserNotConnected as e:
|
||||
console.print(f"[red]Error:[/red] {e}")
|
||||
raise SystemExit(1)
|
||||
except RuntimeError as e:
|
||||
console.print(f"[red]Browser error:[/red] {e}")
|
||||
raise SystemExit(1)
|
||||
|
||||
|
||||
@click.group("storage")
|
||||
def storage_group():
|
||||
"""Read and write the page's localStorage / sessionStorage."""
|
||||
|
||||
|
||||
@storage_group.command("get")
|
||||
@click.argument("key", required=False)
|
||||
@click.option("--type", "store_type", type=click.Choice(["local", "session"]), default="local", show_default=True)
|
||||
@click.option("--tab", "tab_id", type=int, default=None, help="Tab ID (default: active tab)")
|
||||
def storage_get(key, store_type, tab_id):
|
||||
"""Get a localStorage KEY (or dump all keys if omitted)."""
|
||||
result = _handle("storage.get", {"key": key, "type": store_type, "tabId": tab_id})
|
||||
if result is None:
|
||||
console.print("[dim]null[/dim]")
|
||||
elif isinstance(result, dict):
|
||||
console.print(json.dumps(result, indent=2))
|
||||
else:
|
||||
console.print(str(result))
|
||||
|
||||
|
||||
@storage_group.command("set")
|
||||
@click.argument("key")
|
||||
@click.argument("value")
|
||||
@click.option("--type", "store_type", type=click.Choice(["local", "session"]), default="local", show_default=True)
|
||||
@click.option("--tab", "tab_id", type=int, default=None, help="Tab ID (default: active tab)")
|
||||
def storage_set(key, value, store_type, tab_id):
|
||||
"""Set localStorage KEY to VALUE."""
|
||||
_handle("storage.set", {"key": key, "value": value, "type": store_type, "tabId": tab_id})
|
||||
console.print(f"[green]Set[/green] {store_type}[{key!r}] = {value!r}")
|
||||
@@ -1,3 +1,4 @@
|
||||
import base64
|
||||
import click
|
||||
from browser_cli.client import BrowserNotConnected, active_browser_targets, send_command
|
||||
from rich.console import Console
|
||||
@@ -236,3 +237,54 @@ def tabs_unmute(tab_id):
|
||||
result = _handle("tabs.unmute", {"tabId": tab_id})
|
||||
target = result.get("tabId", tab_id) if isinstance(result, dict) else tab_id
|
||||
console.print(f"[green]Unmuted tab {target}[/green]")
|
||||
|
||||
|
||||
@tabs_group.command("pin")
|
||||
@click.argument("tab_id", type=int, required=False)
|
||||
def tabs_pin(tab_id):
|
||||
"""Pin the active tab or a specific tab."""
|
||||
result = _handle("tabs.pin", {"tabId": tab_id})
|
||||
target = result.get("tabId", tab_id) if isinstance(result, dict) else tab_id
|
||||
console.print(f"[green]Pinned tab {target}[/green]")
|
||||
|
||||
|
||||
@tabs_group.command("unpin")
|
||||
@click.argument("tab_id", type=int, required=False)
|
||||
def tabs_unpin(tab_id):
|
||||
"""Unpin the active tab or a specific tab."""
|
||||
result = _handle("tabs.unpin", {"tabId": tab_id})
|
||||
target = result.get("tabId", tab_id) if isinstance(result, dict) else tab_id
|
||||
console.print(f"[green]Unpinned tab {target}[/green]")
|
||||
|
||||
|
||||
@tabs_group.command("watch-url")
|
||||
@click.argument("pattern")
|
||||
@click.option("--tab", "tab_id", type=int, default=None, help="Tab ID (default: active tab)")
|
||||
@click.option("--timeout", type=float, default=30.0, show_default=True, help="Max seconds to wait")
|
||||
def tabs_watch_url(pattern, tab_id, timeout):
|
||||
"""Wait until the active (or specified) tab URL matches regex PATTERN."""
|
||||
result = _handle("tabs.watch_url", {"pattern": pattern, "tabId": tab_id, "timeout": int(timeout * 1000)})
|
||||
url = result.get("url", "") if isinstance(result, dict) else ""
|
||||
console.print(f"[green]URL matched:[/green] {url}")
|
||||
|
||||
|
||||
@tabs_group.command("screenshot")
|
||||
@click.argument("output", required=False, metavar="FILE")
|
||||
@click.option("--tab", "tab_id", type=int, default=None, help="Tab ID (default: active tab)")
|
||||
@click.option("--format", "fmt", type=click.Choice(["png", "jpeg"]), default="png", show_default=True)
|
||||
@click.option("--quality", type=int, default=None, help="JPEG quality 0-100")
|
||||
def tabs_screenshot(output, tab_id, fmt, quality):
|
||||
"""Capture a screenshot of the active (or specified) tab.
|
||||
|
||||
Saves to FILE if given, otherwise prints the base64 data URL.
|
||||
"""
|
||||
result = _handle("tabs.screenshot", {"tabId": tab_id, "format": fmt, "quality": quality})
|
||||
data_url = result.get("dataUrl", "") if isinstance(result, dict) else ""
|
||||
if output:
|
||||
header = f"data:image/{fmt};base64,"
|
||||
raw = base64.b64decode(data_url[len(header):])
|
||||
with open(output, "wb") as f:
|
||||
f.write(raw)
|
||||
console.print(f"[green]Screenshot saved:[/green] {output}")
|
||||
else:
|
||||
console.print(data_url)
|
||||
|
||||
+300
-6
@@ -127,6 +127,8 @@ async function dispatch(command, args) {
|
||||
case "navigate.back": return navBack(args);
|
||||
case "navigate.forward": return navForward(args);
|
||||
case "navigate.focus": return navFocus(args);
|
||||
case "navigate.wait": return navWait(args);
|
||||
case "navigate.open_wait": return navOpenWait(args);
|
||||
|
||||
// ── Tabs ──────────────────────────────────────────────────────────────
|
||||
case "tabs.list": return tabsList();
|
||||
@@ -144,6 +146,10 @@ async function dispatch(command, args) {
|
||||
case "tabs.merge_windows": return tabsMergeWindows();
|
||||
case "tabs.mute": return tabsMute(args);
|
||||
case "tabs.unmute": return tabsUnmute(args);
|
||||
case "tabs.pin": return tabsPin(args);
|
||||
case "tabs.unpin": return tabsUnpin(args);
|
||||
case "tabs.screenshot": return tabsScreenshot(args);
|
||||
case "tabs.watch_url": return tabsWatchUrl(args);
|
||||
|
||||
// ── Groups ────────────────────────────────────────────────────────────
|
||||
case "group.list": return groupList();
|
||||
@@ -162,12 +168,36 @@ async function dispatch(command, args) {
|
||||
case "windows.open": return windowsOpen(args);
|
||||
|
||||
// ── DOM ───────────────────────────────────────────────────────────────
|
||||
case "dom.query": return domOp("domQuery", args);
|
||||
case "dom.click": return domOp("domClick", args);
|
||||
case "dom.type": return domOp("domType", args);
|
||||
case "dom.attr": return domOp("domAttr", args);
|
||||
case "dom.text": return domOp("domText", args);
|
||||
case "dom.exists": return domOp("domExists", args);
|
||||
case "dom.query": return domOp("domQuery", args);
|
||||
case "dom.click": return domOp("domClick", args);
|
||||
case "dom.type": return domOp("domType", args);
|
||||
case "dom.attr": return domOp("domAttr", args);
|
||||
case "dom.text": return domOp("domText", args);
|
||||
case "dom.exists": return domOp("domExists", args);
|
||||
case "dom.scroll": return domOp("domScroll", args);
|
||||
case "dom.select": return domOp("domSelect", args);
|
||||
case "dom.key": return domOp("domKey", args);
|
||||
case "dom.hover": return domOp("domHover", args);
|
||||
case "dom.check": return domOp("domCheck", { ...args, checked: true });
|
||||
case "dom.uncheck": return domOp("domCheck", { ...args, checked: false });
|
||||
case "dom.clear": return domOp("domClear", args);
|
||||
case "dom.focus": return domOp("domFocus", args);
|
||||
case "dom.submit": return domOp("domSubmit", args);
|
||||
case "dom.eval": return domEval(args);
|
||||
case "dom.wait_for": return domWaitFor(args);
|
||||
case "dom.poll": return domPoll(args);
|
||||
|
||||
// ── Page ──────────────────────────────────────────────────────────────
|
||||
case "page.info": return domOp("pageInfo", {});
|
||||
|
||||
// ── Storage ───────────────────────────────────────────────────────────
|
||||
case "storage.get": return storageGet(args);
|
||||
case "storage.set": return storageSet(args);
|
||||
|
||||
// ── Cookies ───────────────────────────────────────────────────────────
|
||||
case "cookies.list": return cookiesList(args);
|
||||
case "cookies.get": return cookiesGet(args);
|
||||
case "cookies.set": return cookiesSet(args);
|
||||
|
||||
// ── Extract ───────────────────────────────────────────────────────────
|
||||
case "extract.links": return domOp("extractLinks", args);
|
||||
@@ -267,6 +297,25 @@ async function navFocus({ pattern }) {
|
||||
return { id: match.id, url: match.url || match.pendingUrl, title: match.title };
|
||||
}
|
||||
|
||||
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`);
|
||||
}
|
||||
|
||||
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 ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
async function tabsList() {
|
||||
@@ -442,6 +491,47 @@ async function tabsMergeWindows() {
|
||||
return { moved };
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
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`);
|
||||
}
|
||||
|
||||
async function tabsMute({ tabId }) {
|
||||
const tab = await resolveTabForDirectAction(tabId, "mute");
|
||||
await chrome.tabs.update(tab.id, { muted: true });
|
||||
@@ -616,6 +706,131 @@ async function domOp(funcName, args) {
|
||||
return results[0]?.result;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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`);
|
||||
}
|
||||
|
||||
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`);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
async function cookiesGet({ url, name }) {
|
||||
return await chrome.cookies.get({ url, name });
|
||||
}
|
||||
|
||||
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
|
||||
function contentDispatch(funcName, args) {
|
||||
function domQuery({ selector }) {
|
||||
@@ -653,6 +868,83 @@ function contentDispatch(funcName, args) {
|
||||
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) => {
|
||||
@@ -1090,6 +1382,8 @@ function contentDispatch(funcName, args) {
|
||||
}
|
||||
|
||||
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}`);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "browser-cli",
|
||||
"version": "0.5.12",
|
||||
"version": "0.6.0",
|
||||
"description": "Control your browser from the terminal via browser-cli",
|
||||
"permissions": [
|
||||
"tabs",
|
||||
@@ -10,7 +10,8 @@
|
||||
"windows",
|
||||
"storage",
|
||||
"alarms",
|
||||
"nativeMessaging"
|
||||
"nativeMessaging",
|
||||
"cookies"
|
||||
],
|
||||
"host_permissions": ["<all_urls>"],
|
||||
"background": {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "browser-cli"
|
||||
version = "0.5.12"
|
||||
version = "0.6.0"
|
||||
description = "Control your real running browser from the terminal via a browser extension"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
|
||||
Reference in New Issue
Block a user