Files
browser-cli/browser_cli/__init__.py
T
daniel156161 5cec57e06d
Testing / remote-protocol-compat (0.9.3) (push) Successful in 40s
Testing / remote-protocol-compat (0.9.5) (push) Successful in 38s
Testing / test (push) Failing after 1m3s
Package Extension / package-extension (push) Successful in 29s
Build & Publish Package / publish (push) Successful in 33s
feat!: harden raw browser control and packaging
- Add safe-by-default policy gates for raw command surfaces: command, script, and serve-http /command.

- Require explicit opt-ins for page reads, browser control, and high-risk commands such as dom.eval, storage.*, and screenshots.

- Remove all cookies support from CLI, SDK, extension commands, permissions, constants, docs, and tests.

- Add diagnostic, events, watch, workspace, remote, raw command, script, HTTP gateway, tree-view, session import/export, and extension info/capability commands.

- Add Chrome Web Store packaging that strips manifest.key while keeping local packages with a stable native-messaging extension ID.

- Bump browser-cli and extension version to 0.14.1 and cover the new behavior with pytest and extension packaging tests.

BREAKING CHANGE: cookies commands and the b.cookies SDK namespace have been removed; generic raw command execution now blocks non-safe commands unless explicitly allowed.
2026-06-14 14:33:15 +02:00

165 lines
5.4 KiB
Python

"""
browser_cli — Python SDK for controlling your running browser.
Usage:
from browser_cli import BrowserCLI
b = BrowserCLI()
tabs = b.tabs.list() # list[Tab]
tabs[0].close()
tabs[0].move(forward=True)
groups = b.groups.list() # list[Group]
groups[0].tabs()
groups[0].add_tab("https://example.com")
b.nav.open("https://example.com")
b.dom.click("#submit")
b.session.save("work")
# When multiple browser instances are active, pass the alias:
b = BrowserCLI(browser="brave")
Commands are grouped into namespaces on the client:
b.nav navigation (open, reload, back, forward, focus, search)
b.tabs tabs (list, open, close, move, status, mute, sort, ...)
b.groups tab groups (list, create, add_tab, move, close)
b.windows browser windows (list, open, close, rename)
b.dom page elements (query, click, type, wait_for, eval, ...)
b.extract content extraction (links, images, text, json, markdown)
b.page page info
b.storage localStorage / sessionStorage
b.session sessions (save, load, list, diff, ...)
b.perf performance profile + background jobs
b.extension control the extension itself
b.decorators workflow decorators for scripts
"""
from collections.abc import Callable
from browser_cli.client import active_browser_targets, remote_browser_targets, send_command, send_command_async
from browser_cli.errors import BrowserNotConnected
from browser_cli.models import BrowserCounts, Group, Tab
from browser_cli.sdk import (
DecoratorsNS,
DomNS,
ExtensionNS,
ExtractNS,
GroupsNS,
NAMESPACE_SPECS,
NavigationNS,
PageNS,
PerfNS,
SessionNS,
StorageNS,
TabsNS,
WindowsNS,
)
from browser_cli.sdk.factories import FactoryMixin
from browser_cli.sdk.routing import RoutingMixin
from browser_cli.async_sdk import AsyncBrowserCLI
__all__ = ["BrowserCLI", "AsyncBrowserCLI", "BrowserCounts", "BrowserNotConnected", "Tab", "Group"]
class BrowserCLI(FactoryMixin, RoutingMixin):
"""Client for a running browser, with commands grouped into namespaces.
The client itself holds the connection target (browser/remote/key) and the
shared machinery; the actual commands live on namespace accessors such as
:attr:`tabs`, :attr:`dom`, and :attr:`session`. Object construction
(``Tab``/``Group``) comes from :class:`~browser_cli.sdk.factories.FactoryMixin`
and multi-browser fan-out from :class:`~browser_cli.sdk.routing.RoutingMixin`.
"""
_browser: str | None
_remote: str | None
_key: str | None
_command_sender: Callable
nav: NavigationNS
tabs: TabsNS
groups: GroupsNS
windows: WindowsNS
dom: DomNS
extract: ExtractNS
page: PageNS
storage: StorageNS
session: SessionNS
perf: PerfNS
extension: ExtensionNS
decorators: DecoratorsNS
def __init__(
self,
browser: str | None = None,
remote: str | None = None,
key: str | None = None,
*,
_command_sender=None,
):
"""
Args:
browser: Profile alias to target. Required when multiple browser
instances are active. Equivalent to ``--browser`` on the CLI.
remote: Connect to a remote browser exposed via ``browser-cli serve``.
Format: ``"host:port"`` (e.g. ``"browser-host.example:8765"``).
Can be combined with ``browser`` to route to a specific
remote profile.
key: Path to Ed25519 private key PEM for pubkey auth, or ``"agent"``
to use a key from the SSH agent (YubiKey, gpg-agent, etc.).
Defaults to ``~/.config/browser-cli/client.key.pem`` if that file exists.
"""
self._browser = browser
self._remote = remote
self._key = key if key else None
self._command_sender = _command_sender or send_command
for name, namespace_type in NAMESPACE_SPECS:
setattr(self, name, namespace_type(self))
self.decorators = DecoratorsNS(self)
@property
def browser(self) -> str | None:
"""Target browser/profile alias, equivalent to ``--browser``."""
return self._browser
@property
def remote(self) -> str | None:
"""Remote endpoint used by this client, if any."""
return self._remote
@property
def key(self) -> str | None:
"""Ed25519 key spec used for remote auth, if explicitly configured."""
return self._key
def dispatch(self, command: str, args: dict | None = None):
"""Dispatch one browser command using this client's target settings."""
return self._command_sender(command, args, profile=self._browser, remote=self._remote, key=self._key)
def require_tab(self, data, error: str):
"""Convert a tab-like command response into a bound Tab."""
return self.require_tab_response(data, error)
_FIELD_MISSING = object()
def field(self, result, key, default=None, *, fallback=_FIELD_MISSING):
"""Read a named field from command output."""
if fallback is self._FIELD_MISSING:
return self._field(result, key, default)
return self._field(result, key, default, fallback=fallback)
def _cmd(self, command: str, args: dict | None = None):
return self.dispatch(command, args)
def command(self, command: str, args: dict | None = None):
"""Send a raw browser-cli command and return its response.
This is the SDK escape hatch for commands that do not have a dedicated
namespace method yet.
"""
return self._cmd(command, args or {})
def clients(self) -> list[dict]:
"""Return the active browser clients known to this connection."""
return self._cmd("clients.list", {})