#!/usr/bin/env -S uv run """ browser-cli — Control your running browser from the terminal. """ import click import sys import os import json import stat from pathlib import Path from rich.console import Console from browser_cli.commands.navigate import nav_group from browser_cli.commands.tabs import tabs_group from browser_cli.commands.groups import group_group from browser_cli.commands.windows import windows_group 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.client import send_command, BrowserNotConnected console = Console() NATIVE_HOST_NAME = "com.browsercli.host" NATIVE_HOST_DIRS = { "chrome": { "linux": [Path.home() / ".config/google-chrome/NativeMessagingHosts"], "darwin": [Path.home() / "Library/Application Support/Google/Chrome/NativeMessagingHosts"], }, "chromium": { "linux": [Path.home() / ".config/chromium/NativeMessagingHosts"], "darwin": [Path.home() / "Library/Application Support/Chromium/NativeMessagingHosts"], }, "brave": { "linux": [Path.home() / ".config/BraveSoftware/Brave-Browser/NativeMessagingHosts"], "darwin": [Path.home() / "Library/Application Support/BraveSoftware/Brave-Browser/NativeMessagingHosts"], }, } @click.group() def main(): """Control your running browser from the terminal via a Chrome extension.""" # ── Sub-command groups ───────────────────────────────────────────────────────── main.add_command(nav_group) main.add_command(tabs_group) main.add_command(group_group) main.add_command(windows_group) main.add_command(dom_group) main.add_command(extract_group) main.add_command(session_group) # ── clients ──────────────────────────────────────────────────────────────────── @main.command("clients") def cmd_clients(): """Show connected browser clients.""" try: clients = send_command("clients.list") except BrowserNotConnected as e: console.print(f"[red]Error:[/red] {e}") sys.exit(1) from rich.table import Table table = Table(show_header=True, header_style="bold cyan") table.add_column("Browser") table.add_column("Version") table.add_column("Platform") for c in (clients or []): table.add_row(c.get("name", ""), c.get("version", ""), c.get("platform", "")) console.print(table) # ── install ──────────────────────────────────────────────────────────────────── @main.command("install") @click.argument("browser", type=click.Choice(["chrome", "chromium", "brave"]), default="chrome") def cmd_install(browser): """Register the native messaging host and print extension load instructions.""" # Find the native_host.py path native_host_script = Path(__file__).parent / "native_host.py" if not native_host_script.exists(): console.print(f"[red]Cannot find native_host.py at {native_host_script}[/red]") sys.exit(1) # Build a wrapper shell script so it's executable by Chrome wrapper_path = Path(__file__).parent.parent / "browser-cli-native-host" python_exe = sys.executable wrapper_content = f"""#!/bin/sh exec "{python_exe}" "{native_host_script}" "$@" """ wrapper_path.write_text(wrapper_content) 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" 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)") console.print(f" 3. Click [bold]Load unpacked[/bold] → select: [cyan]{Path(__file__).parent.parent / 'extension'}[/cyan]") console.print(" 4. Copy the [bold]Extension ID[/bold] shown on the extension card\n") extension_id = click.prompt("Paste your extension ID here") extension_id = extension_id.strip() # Build native messaging manifest manifest = { "name": NATIVE_HOST_NAME, "description": "browser-cli native messaging host", "path": str(wrapper_path), "type": "stdio", "allowed_origins": [f"chrome-extension://{extension_id}/"], } # Write to OS native messaging dirs platform = "darwin" if sys.platform == "darwin" else "linux" dirs = NATIVE_HOST_DIRS[browser][platform] installed = [] for d in dirs: try: d.mkdir(parents=True, exist_ok=True) manifest_path = d / f"{NATIVE_HOST_NAME}.json" manifest_path.write_text(json.dumps(manifest, indent=2)) installed.append(manifest_path) except Exception as e: console.print(f"[yellow]Could not write to {d}: {e}[/yellow]") if not installed: console.print("[red]Failed to install native host manifest[/red]") sys.exit(1) for p in installed: console.print(f"[green]✓[/green] Wrote native host manifest: {p}") console.print("\n[bold]Step 2:[/bold] Restart Chrome 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]") if __name__ == "__main__": main()