335 lines
12 KiB
Markdown
335 lines
12 KiB
Markdown
# browser-cli
|
|
|
|
Control your real, running browser from the terminal or a Python script — no headless browser, no Playwright, no virtual display. Your actual open tabs, windows, and tab groups respond to your commands.
|
|
|
|
---
|
|
|
|
## What it does
|
|
|
|
You have 40 tabs open. You want to close all the duplicates, group the GitHub ones, save your session before a meeting, and open a few URLs into a specific group — all from a script. That is what browser-cli is for.
|
|
|
|
It works by pairing a small Chrome/Brave extension with a Python CLI tool. The extension has full access to your browser's tabs, windows, groups, and page DOM. The CLI talks to it in real time over a local socket.
|
|
|
|
---
|
|
|
|
## How it works
|
|
|
|
```
|
|
terminal / python script
|
|
│
|
|
│ Unix socket (/tmp/browser-cli.sock)
|
|
▼
|
|
Native Messaging Host (Python process, launched by the browser)
|
|
│
|
|
│ Native Messaging Protocol (stdin/stdout, 4-byte length prefix + JSON)
|
|
▼
|
|
Chrome Extension (background service worker)
|
|
│
|
|
│ chrome.* APIs
|
|
▼
|
|
Your running browser
|
|
```
|
|
|
|
1. The extension calls `chrome.runtime.connectNative('com.browsercli.host')` on startup.
|
|
2. The browser launches the native host Python process (registered in the OS).
|
|
3. The native host opens a Unix socket at `/tmp/browser-cli.sock`.
|
|
4. CLI commands connect to that socket, send a JSON command, and wait for the result.
|
|
5. The native host relays the command to the extension via stdout, receives the result via stdin, and sends it back to the CLI.
|
|
|
|
No server needs to be running beforehand. The browser manages the native host's lifecycle.
|
|
|
|
**Message format**
|
|
|
|
Every command is a JSON object:
|
|
```json
|
|
{ "id": "uuid", "command": "tabs.list", "args": {} }
|
|
```
|
|
Every response:
|
|
```json
|
|
{ "id": "uuid", "success": true, "data": [...] }
|
|
```
|
|
|
|
---
|
|
|
|
## Installation
|
|
|
|
**Requirements:** Python 3.10+, [uv](https://github.com/astral-sh/uv), Chrome or Brave
|
|
|
|
```sh
|
|
git clone <repo>
|
|
cd browser-cli
|
|
uv sync
|
|
uv run browser-cli install brave # or: chrome, chromium
|
|
```
|
|
|
|
The `install` command will:
|
|
1. Ask you to load the `extension/` folder as an unpacked extension in your browser (`brave://extensions` → Developer mode → Load unpacked)
|
|
2. Ask you to paste the extension ID shown on the extension card
|
|
3. Write the native messaging manifest to your OS so the browser can find the host
|
|
4. Create an executable wrapper script for the native host
|
|
|
|
After install, **fully restart your browser** (Quit and reopen — not just close the window). The extension will connect to the native host automatically on startup.
|
|
|
|
---
|
|
|
|
## Project structure
|
|
|
|
```
|
|
browser-cli/
|
|
├── browser_cli/
|
|
│ ├── __init__.py # Python API — BrowserCLI class
|
|
│ ├── native_host.py # Native messaging host (launched by browser)
|
|
│ ├── client.py # Unix socket client (used by CLI and Python API)
|
|
│ ├── cli.py # Click CLI entry point
|
|
│ └── commands/
|
|
│ ├── navigate.py # open, reload, back, forward, focus
|
|
│ ├── tabs.py # tab management
|
|
│ ├── groups.py # tab group management
|
|
│ ├── windows.py # window management
|
|
│ ├── dom.py # DOM querying and interaction
|
|
│ ├── extract.py # content extraction
|
|
│ └── session.py # session save/load
|
|
├── extension/
|
|
│ ├── manifest.json # MV3 extension manifest
|
|
│ ├── background.js # Service worker — receives commands, calls chrome.* APIs
|
|
│ └── content.js # Placeholder for future persistent content script
|
|
├── examples/
|
|
│ ├── demo.py # Python API walkthrough
|
|
│ └── demo.sh # Bash CLI walkthrough
|
|
└── pyproject.toml
|
|
```
|
|
|
|
---
|
|
|
|
## CLI reference
|
|
|
|
All commands are run with `uv run browser-cli <command>`.
|
|
|
|
### Navigation (`nav`)
|
|
|
|
```sh
|
|
# Open a URL
|
|
browser-cli nav open https://example.com
|
|
browser-cli nav open https://example.com --bg # background, no focus
|
|
browser-cli nav open https://example.com --window work # into a named window
|
|
browser-cli nav open https://example.com --group research # into a tab group (name or ID)
|
|
|
|
# Reload
|
|
browser-cli nav reload # reload active tab
|
|
browser-cli nav reload 1234 # reload tab by ID
|
|
browser-cli nav hard-reload # bypass cache
|
|
|
|
# Navigate history
|
|
browser-cli nav back
|
|
browser-cli nav forward 1234 # forward in specific tab
|
|
|
|
# Jump to a tab by URL pattern
|
|
browser-cli nav focus github # focuses first tab whose URL contains "github"
|
|
```
|
|
|
|
### Tabs
|
|
|
|
```sh
|
|
browser-cli tabs list # list all open tabs (all windows)
|
|
browser-cli tabs count # count all tabs
|
|
browser-cli tabs count youtube # count tabs matching URL pattern
|
|
browser-cli tabs filter youtube # list tabs matching URL pattern
|
|
browser-cli tabs query "pull request" # search tabs by URL or title
|
|
|
|
browser-cli tabs active 1234 # switch browser focus to tab
|
|
browser-cli tabs html # print full HTML of active tab
|
|
browser-cli tabs html 1234 # print HTML of specific tab
|
|
|
|
browser-cli tabs close 1234 # close specific tab
|
|
browser-cli tabs close --inactive # close all inactive tabs
|
|
browser-cli tabs close --duplicates # close duplicate URLs (keep first)
|
|
browser-cli tabs dedupe # same as close --duplicates
|
|
|
|
browser-cli tabs move 1234 --window 2 # move tab to another window
|
|
browser-cli tabs move 1234 --group 42 # move tab into a group
|
|
|
|
browser-cli tabs sort --by domain # sort tabs within each window
|
|
browser-cli tabs sort --by title
|
|
browser-cli tabs sort --by time
|
|
|
|
browser-cli tabs merge-windows # pull all tabs into the current window
|
|
```
|
|
|
|
### Tab groups
|
|
|
|
```sh
|
|
browser-cli group list # list all tab groups
|
|
browser-cli group count # count groups
|
|
browser-cli group query "work" # search groups by name
|
|
browser-cli group tabs 42 # list tabs inside group ID 42
|
|
|
|
browser-cli group create "research" # create a new group
|
|
browser-cli group add-tab research # open a blank tab in the group
|
|
browser-cli group add-tab research https://example.com # open URL in the group
|
|
browser-cli group add-tab 42 https://example.com # by group ID
|
|
|
|
browser-cli group close 42 # ungroup the group
|
|
```
|
|
|
|
### Windows
|
|
|
|
```sh
|
|
browser-cli windows list # list all windows
|
|
browser-cli windows open # open a new window
|
|
browser-cli windows rename 1 "work" # give a window a local alias
|
|
browser-cli windows close 1 # close a window
|
|
```
|
|
|
|
### DOM
|
|
|
|
These commands run on the **active tab**. The tab must be on a regular `http://` or `https://` page — not a browser internal page like `brave://newtab`.
|
|
|
|
```sh
|
|
browser-cli dom query "h1" # return elements matching CSS selector
|
|
browser-cli dom text "h1" # get text content of matching elements
|
|
browser-cli dom attr "a" href # get attribute value from elements
|
|
browser-cli dom exists ".cookie-banner" # exits 0 if found, 1 if not
|
|
browser-cli dom click ".accept-button" # click an element
|
|
browser-cli dom type "#search" "hello" # type text into an input
|
|
```
|
|
|
|
### Extract
|
|
|
|
```sh
|
|
browser-cli extract links # all <a href> links on the page
|
|
browser-cli extract images # all <img> tags (src + alt)
|
|
browser-cli extract text # all visible text (innerText)
|
|
browser-cli extract json "#data" # parse JSON inside a CSS selector
|
|
```
|
|
|
|
### Sessions
|
|
|
|
A session is a snapshot of all open tab URLs, stored inside the extension via `chrome.storage.local`. Sessions survive browser restarts but are lost if the extension is uninstalled or extension data is cleared.
|
|
|
|
```sh
|
|
browser-cli session save before-meeting # save current tabs as a named session
|
|
browser-cli session load before-meeting # reopen all saved tabs
|
|
browser-cli session list # list all saved sessions (name, tab count, date)
|
|
browser-cli session remove before-meeting # delete a saved session
|
|
browser-cli session diff session-a session-b # show which URLs were added / removed
|
|
browser-cli session auto-save on # auto-save after every tab change
|
|
browser-cli session auto-save off
|
|
```
|
|
|
|
### Misc
|
|
|
|
```sh
|
|
browser-cli clients # show connected browser info
|
|
browser-cli install brave # (re)register the native host
|
|
```
|
|
|
|
---
|
|
|
|
## Python API
|
|
|
|
```python
|
|
from browser_cli import BrowserCLI
|
|
|
|
b = BrowserCLI()
|
|
```
|
|
|
|
Every CLI command has a corresponding method. The call blocks until the browser responds and returns the data directly as a Python object.
|
|
|
|
```python
|
|
# Navigation
|
|
b.open("https://example.com")
|
|
b.open("https://example.com", background=True)
|
|
b.open("https://example.com", window="work")
|
|
b.reload()
|
|
b.hard_reload()
|
|
b.back()
|
|
b.forward(tab_id=1234)
|
|
b.focus_url("github")
|
|
|
|
# Tabs
|
|
tabs = b.tabs_list() # list of dicts: id, windowId, title, url, active, ...
|
|
b.tabs_active(1234)
|
|
b.tabs_close(1234)
|
|
b.tabs_close_inactive()
|
|
b.tabs_close_duplicates()
|
|
b.tabs_filter("youtube") # list of matching tabs
|
|
b.tabs_query("pull request")
|
|
b.tabs_count("github") # int
|
|
html = b.tabs_html() # full HTML string of active tab
|
|
b.tabs_sort(by="domain")
|
|
b.tabs_merge_windows()
|
|
b.tabs_dedupe()
|
|
|
|
# Tab groups
|
|
groups = b.group_list() # list of dicts: id, title, color, collapsed, tabCount
|
|
b.group_open("research") # creates group, returns { id, name }
|
|
b.group_close(42)
|
|
b.group_tabs(42) # tabs inside a group
|
|
|
|
# Windows
|
|
windows = b.windows_list()
|
|
b.windows_rename(1, "work")
|
|
b.windows_open()
|
|
b.windows_close(1)
|
|
|
|
# DOM (active tab must be http/https)
|
|
elements = b.dom_query("h2") # list of { tag, text, attrs }
|
|
texts = b.dom_text(".article p") # list of strings
|
|
attrs = b.dom_attr("a", "href") # list of strings
|
|
exists = b.dom_exists(".cookie-banner")# bool
|
|
b.dom_click(".accept-button")
|
|
b.dom_type("#search", "hello world")
|
|
|
|
# Extract
|
|
links = b.extract_links() # list of { text, href }
|
|
images = b.extract_images() # list of { alt, src }
|
|
text = b.extract_text() # string
|
|
data = b.extract_json("#app-data") # parsed Python object
|
|
|
|
# Sessions
|
|
b.session_save("before-meeting")
|
|
b.session_load("before-meeting")
|
|
sessions = b.session_list() # [{ name, tabs, savedAt }, ...]
|
|
b.session_remove("before-meeting")
|
|
diff = b.session_diff("session-a", "session-b")
|
|
# diff = { "added": [...urls], "removed": [...urls] }
|
|
b.session_auto_save(True)
|
|
|
|
# Misc
|
|
clients = b.clients()
|
|
```
|
|
|
|
**Error handling**
|
|
|
|
```python
|
|
from browser_cli import BrowserCLI, BrowserNotConnected
|
|
|
|
b = BrowserCLI()
|
|
try:
|
|
tabs = b.tabs_list()
|
|
except BrowserNotConnected:
|
|
print("Browser is not running or extension is not loaded")
|
|
except RuntimeError as e:
|
|
print(f"Browser returned an error: {e}")
|
|
```
|
|
|
|
---
|
|
|
|
## Example scripts
|
|
|
|
See `examples/demo.py` (Python) and `examples/demo.sh` (Bash) for full walkthroughs covering tabs, groups, DOM extraction, and session management.
|
|
|
|
```sh
|
|
uv run python examples/demo.py
|
|
bash examples/demo.sh
|
|
```
|
|
|
|
---
|
|
|
|
## Limitations
|
|
|
|
- **Chrome internal pages** (`chrome://`, `brave://`, `about:`) cannot be scripted. DOM and extract commands only work on regular `http://` and `https://` pages.
|
|
- **Profile switching** via `windows open --profile` opens a plain window; launching a different profile requires the browser to be started externally with `--profile-directory`.
|
|
- **One browser at a time** — the native host socket supports one connected extension. Running multiple browser profiles simultaneously is not supported.
|
|
- **Linux and macOS only** — Windows native messaging paths are not yet handled.
|