init commit

This commit is contained in:
2026-04-08 21:17:59 +02:00
commit 178b7bf7a2
24 changed files with 2466 additions and 0 deletions
View File
+89
View File
@@ -0,0 +1,89 @@
import click
from browser_cli.client import send_command, BrowserNotConnected
from rich.console import Console
from rich.table import Table
import json
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("dom")
def dom_group():
"""Query and interact with page DOM elements."""
@dom_group.command("query")
@click.argument("selector")
def dom_query(selector):
"""Return elements matching CSS SELECTOR (like mini DevTools)."""
elements = _handle("dom.query", {"selector": selector})
if not elements:
console.print("[yellow]No elements found[/yellow]")
return
table = Table(show_header=True, header_style="bold cyan")
table.add_column("Tag", width=12)
table.add_column("Text", width=40)
table.add_column("Attributes")
for el in elements:
attrs = ", ".join(f"{k}={v!r}" for k, v in (el.get("attrs") or {}).items())
table.add_row(el.get("tag", ""), (el.get("text") or "")[:60], attrs[:80])
console.print(table)
@dom_group.command("click")
@click.argument("selector")
def dom_click(selector):
"""Click the first element matching CSS SELECTOR."""
_handle("dom.click", {"selector": selector})
console.print(f"[green]Clicked:[/green] {selector}")
@dom_group.command("type")
@click.argument("selector")
@click.argument("text")
def dom_type(selector, text):
"""Type TEXT into the element matching CSS SELECTOR."""
_handle("dom.type", {"selector": selector, "text": text})
console.print(f"[green]Typed into:[/green] {selector}")
@dom_group.command("attr")
@click.argument("selector")
@click.argument("attr_name")
def dom_attr(selector, attr_name):
"""Get attribute ATTR_NAME from elements matching CSS SELECTOR."""
values = _handle("dom.attr", {"selector": selector, "attr": attr_name})
for v in (values or []):
console.print(v)
@dom_group.command("text")
@click.argument("selector")
def dom_text(selector):
"""Get text content of elements matching CSS SELECTOR."""
values = _handle("dom.text", {"selector": selector})
for v in (values or []):
console.print(v)
@dom_group.command("exists")
@click.argument("selector")
def dom_exists(selector):
"""Check if an element matching CSS SELECTOR exists on the page."""
exists = _handle("dom.exists", {"selector": selector})
if exists:
console.print(f"[green]exists[/green]: {selector}")
else:
console.print(f"[red]not found[/red]: {selector}")
raise SystemExit(1)
+68
View File
@@ -0,0 +1,68 @@
import click
import json
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("extract")
def extract_group():
"""Extract content from the active tab."""
@extract_group.command("links")
def extract_links():
"""Extract all links from the active tab."""
links = _handle("extract.links")
if not links:
console.print("[yellow]No links found[/yellow]")
return
table = Table(show_header=True, header_style="bold cyan")
table.add_column("Text", width=40)
table.add_column("URL")
for lnk in links:
table.add_row((lnk.get("text") or "")[:60], lnk.get("href") or "")
console.print(table)
@extract_group.command("images")
def extract_images():
"""Extract all images from the active tab."""
images = _handle("extract.images")
if not images:
console.print("[yellow]No images found[/yellow]")
return
table = Table(show_header=True, header_style="bold cyan")
table.add_column("Alt", width=30)
table.add_column("Src")
for img in images:
table.add_row((img.get("alt") or "")[:40], img.get("src") or "")
console.print(table)
@extract_group.command("text")
def extract_text():
"""Extract all visible text from the active tab."""
text = _handle("extract.text")
console.print(text or "")
@extract_group.command("json")
@click.argument("selector")
def extract_json(selector):
"""Parse and pretty-print JSON content inside SELECTOR."""
data = _handle("extract.json", {"selector": selector})
console.print_json(json.dumps(data))
+102
View File
@@ -0,0 +1,102 @@
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)
def _print_groups(groups: list[dict]) -> None:
if not groups:
console.print("[yellow]No groups found[/yellow]")
return
table = Table(show_header=True, header_style="bold cyan")
table.add_column("ID", style="dim", no_wrap=True)
table.add_column("Name")
table.add_column("Color", width=10)
table.add_column("Collapsed", width=10)
table.add_column("Tabs", width=6)
for g in groups:
table.add_row(
str(g.get("id", "")),
g.get("title") or "(unnamed)",
g.get("color") or "",
"yes" if g.get("collapsed") else "no",
str(g.get("tabCount", "")),
)
console.print(table)
@click.group("group")
def group_group():
"""Manage tab groups."""
@group_group.command("list")
def group_list():
"""List all tab groups."""
groups = _handle("group.list")
_print_groups(groups or [])
@group_group.command("tabs")
@click.argument("group_id", type=int)
def group_tabs(group_id):
"""List tabs inside a group."""
from browser_cli.commands.tabs import _print_tabs
tabs = _handle("group.tabs", {"groupId": group_id})
_print_tabs(tabs or [])
@group_group.command("count")
def group_count():
"""Count all tab groups."""
count = _handle("group.count")
console.print(f"[bold]{count}[/bold] group(s)")
@group_group.command("query")
@click.argument("search")
def group_query(search):
"""Search groups by name."""
groups = _handle("group.query", {"search": search})
_print_groups(groups or [])
@group_group.command("close")
@click.argument("group_id", type=int)
def group_close(group_id):
"""Close (ungroup and optionally close) a tab group."""
_handle("group.close", {"groupId": group_id})
console.print(f"[green]Group {group_id} closed[/green]")
@group_group.command("create")
@click.argument("name")
def group_create(name):
"""Create a new tab group with NAME."""
result = _handle("group.open", {"name": name})
gid = result.get("id") if isinstance(result, dict) else result
console.print(f"[green]Created group '{name}'[/green] (id: {gid})")
@group_group.command("add-tab")
@click.argument("group")
@click.argument("url", required=False)
def group_add_tab(group, url):
"""Open a new tab (optionally at URL) inside GROUP (name or ID)."""
result = _handle("group.add_tab", {"group": group, "url": url})
tab_id = result.get("tabId") if isinstance(result, dict) else result
label = url or "new tab"
console.print(f"[green]Opened {label}[/green] in group '{group}' (tab id: {tab_id})")
+75
View File
@@ -0,0 +1,75 @@
import click
from browser_cli.client import send_command, BrowserNotConnected
from rich.console import Console
console = Console()
def _handle(command, args):
try:
return send_command(command, args)
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.command("open")
@click.argument("url")
@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 directly into a tab group (name or ID)")
def cmd_open(url, bg, window_name, group_name):
"""Open URL in a new tab."""
result = _handle("navigate.open", {"url": url, "background": bg, "window": window_name, "group": group_name})
suffix = ""
if group_name:
suffix = f" in group '{group_name}'"
elif window_name:
suffix = f" in window '{window_name}'"
console.print(f"[green]Opened:[/green] {url}{suffix}")
@click.command("reload")
@click.argument("tab_id", type=int, required=False)
def cmd_reload(tab_id):
"""Reload the active (or specified) tab."""
_handle("navigate.reload", {"tabId": tab_id})
console.print("[green]Reloaded[/green]")
@click.command("hard-reload")
@click.argument("tab_id", type=int, required=False)
def cmd_hard_reload(tab_id):
"""Hard reload (bypass cache) the active (or specified) tab."""
_handle("navigate.hard_reload", {"tabId": tab_id})
console.print("[green]Hard reloaded[/green]")
@click.command("back")
@click.argument("tab_id", type=int, required=False)
def cmd_back(tab_id):
"""Navigate back in the active (or specified) tab."""
_handle("navigate.back", {"tabId": tab_id})
console.print("[green]Navigated back[/green]")
@click.command("forward")
@click.argument("tab_id", type=int, required=False)
def cmd_forward(tab_id):
"""Navigate forward in the active (or specified) tab."""
_handle("navigate.forward", {"tabId": tab_id})
console.print("[green]Navigated forward[/green]")
@click.command("focus")
@click.argument("pattern")
def cmd_focus(pattern):
"""Jump to the first tab whose URL matches PATTERN."""
result = _handle("navigate.focus", {"pattern": pattern})
if result:
console.print(f"[green]Focused:[/green] {result.get('url', result)}")
else:
console.print(f"[yellow]No tab found matching:[/yellow] {pattern}")
+103
View File
@@ -0,0 +1,103 @@
import click
import json
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("session")
def session_group():
"""Save and restore browser sessions."""
@session_group.command("save")
@click.argument("name")
def session_save(name):
"""Save all current tabs as session NAME."""
result = _handle("session.save", {"name": name})
count = result.get("tabs", 0) if isinstance(result, dict) else 0
console.print(f"[green]Session '{name}' saved[/green] ({count} tabs)")
@session_group.command("load")
@click.argument("name")
def session_load(name):
"""Restore session NAME (opens all saved tabs)."""
result = _handle("session.load", {"name": name})
count = result.get("tabs", 0) if isinstance(result, dict) else 0
console.print(f"[green]Session '{name}' loaded[/green] ({count} tabs opened)")
@session_group.command("diff")
@click.argument("name_a")
@click.argument("name_b")
def session_diff(name_a, name_b):
"""Show tabs added/removed between two saved sessions."""
diff = _handle("session.diff", {"nameA": name_a, "nameB": name_b})
if not diff:
console.print("[yellow]No diff data returned[/yellow]")
return
added = diff.get("added") or []
removed = diff.get("removed") or []
if added:
console.print(f"[green]Added in '{name_b}':[/green]")
for url in added:
console.print(f" + {url}")
if removed:
console.print(f"[red]Removed in '{name_b}':[/red]")
for url in removed:
console.print(f" - {url}")
if not added and not removed:
console.print("[green]Sessions are identical[/green]")
@session_group.command("list")
def session_list():
"""List all saved sessions."""
from rich.table import Table
sessions = _handle("session.list")
if not sessions:
console.print("[yellow]No saved sessions[/yellow]")
return
table = Table(show_header=True, header_style="bold cyan")
table.add_column("Name")
table.add_column("Tabs", width=6)
table.add_column("Saved at")
for s in sessions:
from datetime import datetime
saved = datetime.fromtimestamp(s["savedAt"] / 1000).strftime("%Y-%m-%d %H:%M") if s.get("savedAt") else ""
table.add_row(s["name"], str(s["tabs"]), saved)
console.print(table)
@session_group.command("remove")
@click.argument("name")
def session_remove(name):
"""Delete a saved session."""
_handle("session.remove", {"name": name})
console.print(f"[green]Session '{name}' removed[/green]")
@session_group.command("auto-save")
@click.argument("state", type=click.Choice(["on", "off"]))
def session_auto_save(state):
"""Enable or disable automatic session saving."""
enabled = state == "on"
_handle("session.auto_save", {"enabled": enabled})
console.print(f"[green]Auto-save {state}[/green]")
+138
View File
@@ -0,0 +1,138 @@
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)
def _print_tabs(tabs: list[dict]) -> None:
if not tabs:
console.print("[yellow]No tabs found[/yellow]")
return
table = Table(show_header=True, header_style="bold cyan")
table.add_column("ID", style="dim", no_wrap=True)
table.add_column("Window", no_wrap=True)
table.add_column("Active", width=7)
table.add_column("Title")
table.add_column("URL")
for t in tabs:
active = "[green]✓[/green]" if t.get("active") else ""
table.add_row(
str(t.get("id", "")),
str(t.get("windowId", "")),
active,
(t.get("title") or "")[:60],
(t.get("url") or "")[:80],
)
console.print(table)
@click.group("tabs")
def tabs_group():
"""Manage browser tabs."""
@tabs_group.command("list")
def tabs_list():
"""List all open tabs across all windows."""
tabs = _handle("tabs.list")
_print_tabs(tabs or [])
@tabs_group.command("close")
@click.argument("tab_id", type=int, required=False)
@click.option("--inactive", is_flag=True, help="Close all inactive tabs")
@click.option("--duplicates", is_flag=True, help="Close duplicate tabs (keep first)")
def tabs_close(tab_id, inactive, duplicates):
"""Close a tab, all inactive tabs, or all duplicate tabs."""
result = _handle("tabs.close", {"tabId": tab_id, "inactive": inactive, "duplicates": duplicates})
count = result.get("closed", 0) if isinstance(result, dict) else 1
console.print(f"[green]Closed {count} tab(s)[/green]")
@tabs_group.command("move")
@click.argument("tab_id", type=int)
@click.option("--group", "group_id", type=int, default=None, help="Move to tab group ID")
@click.option("--window", "window_id", type=int, default=None, help="Move to window ID")
@click.option("--index", type=int, default=None, help="Position index in target")
def tabs_move(tab_id, group_id, window_id, index):
"""Move a tab to a different window or group."""
_handle("tabs.move", {"tabId": tab_id, "groupId": group_id, "windowId": window_id, "index": index})
console.print("[green]Tab moved[/green]")
@tabs_group.command("active")
@click.argument("tab_id", type=int)
def tabs_active(tab_id):
"""Switch browser focus to a tab."""
_handle("tabs.active", {"tabId": tab_id})
console.print(f"[green]Switched to tab {tab_id}[/green]")
@tabs_group.command("filter")
@click.argument("pattern")
def tabs_filter(pattern):
"""List tabs whose URL contains PATTERN."""
tabs = _handle("tabs.filter", {"pattern": pattern})
_print_tabs(tabs or [])
@tabs_group.command("count")
@click.argument("pattern", required=False)
def tabs_count(pattern):
"""Count open tabs, optionally filtered by URL PATTERN."""
count = _handle("tabs.count", {"pattern": pattern})
label = f" matching '{pattern}'" if pattern else ""
console.print(f"[bold]{count}[/bold] tab(s){label}")
@tabs_group.command("query")
@click.argument("search")
def tabs_query(search):
"""Search tabs by URL or title."""
tabs = _handle("tabs.query", {"search": search})
_print_tabs(tabs or [])
@tabs_group.command("html")
@click.argument("tab_id", type=int, required=False)
def tabs_html(tab_id):
"""Print the full HTML of a tab."""
html = _handle("tabs.html", {"tabId": tab_id})
console.print(html or "")
@tabs_group.command("dedupe")
def tabs_dedupe():
"""Close duplicate tabs (keep the first occurrence of each URL)."""
result = _handle("tabs.dedupe")
count = result.get("closed", 0) if isinstance(result, dict) else 0
console.print(f"[green]Closed {count} duplicate tab(s)[/green]")
@tabs_group.command("sort")
@click.option("--by", type=click.Choice(["domain", "title", "time"]), default="domain", show_default=True)
def tabs_sort(by):
"""Sort tabs within each window."""
_handle("tabs.sort", {"by": by})
console.print(f"[green]Tabs sorted by {by}[/green]")
@tabs_group.command("merge-windows")
def tabs_merge_windows():
"""Move all tabs into the focused window."""
result = _handle("tabs.merge_windows")
count = result.get("moved", 0) if isinstance(result, dict) else 0
console.print(f"[green]Merged — moved {count} tab(s) into current window[/green]")
+77
View File
@@ -0,0 +1,77 @@
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)
def _print_windows(windows: list[dict]) -> None:
if not windows:
console.print("[yellow]No windows found[/yellow]")
return
table = Table(show_header=True, header_style="bold cyan")
table.add_column("ID", style="dim", no_wrap=True)
table.add_column("Alias", width=20)
table.add_column("Focused", width=8)
table.add_column("Tabs", width=6)
table.add_column("State", width=12)
for w in windows:
focused = "[green]✓[/green]" if w.get("focused") else ""
table.add_row(
str(w.get("id", "")),
w.get("alias") or "",
focused,
str(w.get("tabCount", "")),
w.get("state") or "",
)
console.print(table)
@click.group("windows")
def windows_group():
"""Manage browser windows."""
@windows_group.command("list")
def windows_list():
"""List all browser windows."""
windows = _handle("windows.list")
_print_windows(windows or [])
@windows_group.command("rename")
@click.argument("window_id", type=int)
@click.argument("name")
def windows_rename(window_id, name):
"""Give a window a local alias NAME (stored in native host)."""
_handle("windows.rename", {"windowId": window_id, "name": name})
console.print(f"[green]Window {window_id} aliased as '{name}'[/green]")
@windows_group.command("close")
@click.argument("window_id", type=int)
def windows_close(window_id):
"""Close a browser window."""
_handle("windows.close", {"windowId": window_id})
console.print(f"[green]Window {window_id} closed[/green]")
@windows_group.command("open")
@click.option("--profile", default=None, help="Open with a specific Chrome profile name")
def windows_open(profile):
"""Open a new browser window."""
result = _handle("windows.open", {"profile": profile})
wid = result.get("id") if isinstance(result, dict) else result
console.print(f"[green]Opened new window[/green] (id: {wid})" + (f" with profile '{profile}'" if profile else ""))