adding the edge and vivaldi browser for installing the extension to, generate uuids when the browser not have a alias set
This commit is contained in:
@@ -53,23 +53,25 @@ Every response:
|
||||
|
||||
## Installation
|
||||
|
||||
**Requirements:** Python 3.10+, [uv](https://github.com/astral-sh/uv), Chrome or Brave
|
||||
**Requirements:** Python 3.10+, [uv](https://github.com/astral-sh/uv), Chrome, Chromium, Brave, Edge, or Vivaldi
|
||||
|
||||
```sh
|
||||
git clone <repo>
|
||||
cd browser-cli
|
||||
uv sync
|
||||
uv run browser-cli install brave # or: chrome, chromium
|
||||
uv run browser-cli install brave # or: chrome, chromium, edge, vivaldi
|
||||
```
|
||||
|
||||
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
|
||||
4. Copy the native host into an internal `libexec` directory and create a small wrapper outside your `PATH`
|
||||
|
||||
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.
|
||||
|
||||
Only the `browser-cli` command needs to be on your `PATH`. The browser launches the native host wrapper directly from its absolute path in the native messaging manifest, and that wrapper points to the internally installed `native_host.py` copy.
|
||||
|
||||
---
|
||||
|
||||
## Project structure
|
||||
@@ -103,7 +105,11 @@ browser-cli/
|
||||
|
||||
## CLI reference
|
||||
|
||||
All commands are run with `uv run browser-cli <command>`.
|
||||
All commands are run with `uv run browser-cli [--browser ALIAS] <command>`.
|
||||
|
||||
Use `--browser ALIAS` when multiple browser instances are connected. You can inspect the active instances with `browser-cli clients` and assign a persistent profile alias from inside the target browser with `browser-cli rename-profile --browser <current-alias> <new-alias>`.
|
||||
|
||||
Important: profile aliases are browser-instance aliases, not window aliases. Window aliases created with `windows rename` are only for targeting windows in commands like `nav open --window work`. If a browser instance has no explicit profile alias set, the native host gives it a generated UUID alias so multiple unaliased browsers stay distinct.
|
||||
|
||||
### Navigation (`nav`)
|
||||
|
||||
@@ -127,6 +133,30 @@ browser-cli nav forward 1234 # forward in specific tab
|
||||
browser-cli nav focus github # focuses first tab whose URL contains "github"
|
||||
```
|
||||
|
||||
### Search
|
||||
|
||||
Each search command opens the search results in your browser using the same flags as `nav open`.
|
||||
|
||||
```sh
|
||||
browser-cli search google openai api
|
||||
browser-cli search brave rust iterators --bg
|
||||
browser-cli search ddg tab groups --window work
|
||||
browser-cli search youtube browser automation
|
||||
browser-cli search yt lo fi
|
||||
browser-cli search spotify aphex twin
|
||||
browser-cli search amazon mechanical keyboard
|
||||
browser-cli search ecosia native messaging
|
||||
browser-cli search furaffinity dragons
|
||||
browser-cli search fa dragons
|
||||
browser-cli search bing browser cli
|
||||
browser-cli search github browser-cli
|
||||
browser-cli search wikipedia native messaging
|
||||
browser-cli search wiki native messaging
|
||||
browser-cli search reddit chrome extensions
|
||||
browser-cli search stackoverflow click choices
|
||||
browser-cli search so click choices
|
||||
```
|
||||
|
||||
### Tabs
|
||||
|
||||
```sh
|
||||
@@ -169,6 +199,8 @@ 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
|
||||
browser-cli group move research --forward # move group right
|
||||
browser-cli group move 42 --backward # move group left
|
||||
```
|
||||
|
||||
### Windows
|
||||
@@ -176,6 +208,7 @@ browser-cli group close 42 # ungroup the group
|
||||
```sh
|
||||
browser-cli windows list # list all windows
|
||||
browser-cli windows open # open a new window
|
||||
browser-cli windows open --profile Default # request a specific Chrome profile name
|
||||
browser-cli windows rename 1 "work" # give a window a local alias
|
||||
browser-cli windows close 1 # close a window
|
||||
```
|
||||
@@ -200,6 +233,7 @@ 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
|
||||
browser-cli extract html # full HTML of the active tab
|
||||
```
|
||||
|
||||
### Sessions
|
||||
@@ -220,7 +254,11 @@ browser-cli session auto-save off
|
||||
|
||||
```sh
|
||||
browser-cli clients # show connected browser info
|
||||
browser-cli rename-profile --browser abcd1234 work # rename one connected browser instance
|
||||
browser-cli --browser abcd1234 rename-profile work # equivalent global form
|
||||
browser-cli install brave # (re)register the native host
|
||||
browser-cli completion zsh # print setup instructions
|
||||
browser-cli completion zsh --script # output raw completion script
|
||||
```
|
||||
|
||||
---
|
||||
@@ -329,6 +367,7 @@ 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.
|
||||
- **Profile switching** via `windows open --profile` depends on browser support and does not replace launching a separate browser profile externally with `--profile-directory`.
|
||||
- **Multiple browser instances can be auto-distinguished, but generated aliases are temporary**. Unaliased browsers get UUID aliases from the native host, which avoids collisions but is less ergonomic than setting a stable alias with `browser-cli rename-profile --browser <current-alias> <new-alias>` and restarting that browser.
|
||||
- **Supported install targets are explicit, not “all Chromium browsers”**. The installer currently supports Chrome, Chromium, Brave, Edge, and Vivaldi. Other Chromium-based browsers may use different or shared native messaging manifest locations, so they need browser-specific verification before being added safely.
|
||||
- **Linux and macOS only** — Windows native messaging paths are not yet handled.
|
||||
|
||||
+25
-6
@@ -40,6 +40,14 @@ NATIVE_HOST_DIRS = {
|
||||
"linux": [Path.home() / ".config/BraveSoftware/Brave-Browser/NativeMessagingHosts"],
|
||||
"darwin": [Path.home() / "Library/Application Support/BraveSoftware/Brave-Browser/NativeMessagingHosts"],
|
||||
},
|
||||
"edge": {
|
||||
"linux": [Path.home() / ".config/microsoft-edge/NativeMessagingHosts"],
|
||||
"darwin": [Path.home() / "Library/Application Support/Microsoft Edge/NativeMessagingHosts"],
|
||||
},
|
||||
"vivaldi": {
|
||||
"linux": [Path.home() / ".config/vivaldi/NativeMessagingHosts"],
|
||||
"darwin": [Path.home() / "Library/Application Support/Vivaldi/NativeMessagingHosts"],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -155,11 +163,15 @@ def cmd_clients():
|
||||
|
||||
|
||||
@main.command("rename-profile")
|
||||
@click.option(
|
||||
"--browser", "target_browser", default=None, metavar="ALIAS",
|
||||
help="Browser profile alias to rename. Overrides the global --browser option for this command.",
|
||||
)
|
||||
@click.argument("alias")
|
||||
def cmd_rename_profile(alias):
|
||||
def cmd_rename_profile(target_browser, alias):
|
||||
"""Set the profile alias used to identify this browser instance."""
|
||||
try:
|
||||
send_command("clients.rename_profile", {"alias": alias})
|
||||
send_command("clients.rename_profile", {"alias": alias}, profile=target_browser)
|
||||
except BrowserNotConnected as e:
|
||||
console.print(f"[red]Error:[/red] {e}")
|
||||
sys.exit(1)
|
||||
@@ -170,7 +182,7 @@ def cmd_rename_profile(alias):
|
||||
# ── install ────────────────────────────────────────────────────────────────────
|
||||
|
||||
@main.command("install")
|
||||
@click.argument("browser", type=click.Choice(["chrome", "chromium", "brave"]), default="chrome")
|
||||
@click.argument("browser", type=click.Choice(["chrome", "chromium", "brave", "edge", "vivaldi"]), default="chrome")
|
||||
def cmd_install(browser):
|
||||
"""Register the native messaging host and print extension load instructions."""
|
||||
|
||||
@@ -188,7 +200,14 @@ def cmd_install(browser):
|
||||
wrapper_path.chmod(wrapper_path.stat().st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
|
||||
|
||||
# Ask for extension ID
|
||||
ext_url = "brave://extensions" if browser == "brave" else "chrome://extensions"
|
||||
ext_urls = {
|
||||
"chrome": "chrome://extensions",
|
||||
"chromium": "chrome://extensions",
|
||||
"brave": "brave://extensions",
|
||||
"edge": "edge://extensions",
|
||||
"vivaldi": "vivaldi://extensions",
|
||||
}
|
||||
ext_url = ext_urls[browser]
|
||||
console.print("\n[bold]Step 1:[/bold] Load the extension in your browser")
|
||||
console.print(f" 1. Open [cyan]{ext_url}[/cyan]")
|
||||
console.print(" 2. Enable [bold]Developer mode[/bold] (top-right toggle)")
|
||||
@@ -230,9 +249,9 @@ def cmd_install(browser):
|
||||
console.print(f"[green]✓[/green] Installed native host script: {native_host_script_path}")
|
||||
console.print(f"[green]✓[/green] Installed native host wrapper: {wrapper_path}")
|
||||
|
||||
console.print("\n[bold]Step 2:[/bold] Restart Chrome completely (Cmd/Ctrl+Q, then reopen)")
|
||||
console.print(f"\n[bold]Step 2:[/bold] Restart {browser.capitalize()} completely (Cmd/Ctrl+Q, then reopen)")
|
||||
console.print("\n[green bold]✓ Installation complete![/green bold]")
|
||||
console.print(" After restarting Chrome, try: [cyan]browser-cli tabs list[/cyan]")
|
||||
console.print(" After restarting the browser, try: [cyan]browser-cli tabs list[/cyan]")
|
||||
|
||||
|
||||
# ── native-host (hidden, called by Chrome via native messaging) ────────────────
|
||||
|
||||
@@ -75,6 +75,15 @@ def _socket_path_for(alias: str) -> str:
|
||||
return str(SOCKET_DIR / f"{safe}.sock")
|
||||
|
||||
|
||||
def _resolve_profile_alias(first_msg: dict | None) -> str:
|
||||
"""Return a unique alias when the extension did not provide one."""
|
||||
if first_msg and first_msg.get("type") == "hello":
|
||||
alias = first_msg.get("alias")
|
||||
if alias and alias != DEFAULT_ALIAS:
|
||||
return alias
|
||||
return str(uuid.uuid4())
|
||||
|
||||
|
||||
# --- Thread A: read messages from extension (stdin) ---
|
||||
|
||||
def stdin_reader(alias: str):
|
||||
@@ -191,10 +200,10 @@ def main():
|
||||
# Wait for the hello handshake to learn the profile alias
|
||||
first_msg = read_native_message(stdin)
|
||||
if first_msg and first_msg.get("type") == "hello":
|
||||
alias = first_msg.get("alias") or DEFAULT_ALIAS
|
||||
alias = _resolve_profile_alias(first_msg)
|
||||
else:
|
||||
# No hello — fall back to default, re-queue message if it was a command
|
||||
alias = DEFAULT_ALIAS
|
||||
# No hello — use a generated alias and re-queue the first command if needed.
|
||||
alias = str(uuid.uuid4())
|
||||
if first_msg:
|
||||
msg_id = first_msg.get("id")
|
||||
if msg_id:
|
||||
|
||||
+20
-4
@@ -5,7 +5,6 @@ from unittest.mock import patch
|
||||
|
||||
from browser_cli.cli import main, _project_version
|
||||
|
||||
|
||||
def _expected_version() -> str:
|
||||
pyproject = Path(__file__).resolve().parent.parent / "pyproject.toml"
|
||||
for line in pyproject.read_text(encoding="utf-8").splitlines():
|
||||
@@ -13,21 +12,38 @@ def _expected_version() -> str:
|
||||
return line.split('"')[1]
|
||||
raise AssertionError("version not found in pyproject.toml")
|
||||
|
||||
|
||||
def test_short_version_option():
|
||||
result = CliRunner().invoke(main, ["-V"])
|
||||
assert result.exit_code == 0
|
||||
assert result.output.strip() == _expected_version()
|
||||
|
||||
|
||||
def test_long_version_option():
|
||||
result = CliRunner().invoke(main, ["--version"])
|
||||
assert result.exit_code == 0
|
||||
assert result.output.strip() == _expected_version()
|
||||
|
||||
|
||||
def test_project_version_falls_back_to_installed_package_metadata():
|
||||
with patch("browser_cli.cli.Path.read_text", side_effect=OSError), patch(
|
||||
"browser_cli.cli.package_version", return_value="9.9.9"
|
||||
):
|
||||
assert _project_version() == "9.9.9"
|
||||
|
||||
def test_rename_profile_uses_command_level_browser_target():
|
||||
with patch("browser_cli.cli.send_command") as send_command:
|
||||
result = CliRunner().invoke(main, ["rename-profile", "--browser", "old-id", "work"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
send_command.assert_called_once_with("clients.rename_profile", {"alias": "work"}, profile="old-id")
|
||||
|
||||
def test_rename_profile_uses_global_browser_target_when_set():
|
||||
with patch("browser_cli.cli.send_command") as send_command:
|
||||
result = CliRunner().invoke(main, ["--browser", "old-id", "rename-profile", "work"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
send_command.assert_called_once_with("clients.rename_profile", {"alias": "work"}, profile=None)
|
||||
|
||||
def test_install_help_lists_supported_browsers():
|
||||
result = CliRunner().invoke(main, ["install", "--help"])
|
||||
|
||||
assert result.exit_code == 0
|
||||
assert "[chrome|chromium|brave|edge|vivaldi]" in result.output
|
||||
|
||||
Reference in New Issue
Block a user