feat(read): target any tab for page info and extraction
- Add --tab option to all extract commands and page info - Thread tab_id through the SDK extract and page namespaces - Route page.info with a tab_id to tabs.status for cross-tab metadata - Accept tab_id in the MCP page_info, extract_text, extract_markdown tools - Forward tabId in the extension page.info and extract.html handlers - Keep the active tab as default when no tab is given - Cover tab-scoped reads in API, CLI, and MCP tests - Bump package and extension version to 0.16.7 - Refresh uv.lock with current dependency versions
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import json
|
||||
|
||||
import click
|
||||
from browser_cli.commands import client_from_ctx, handle_errors
|
||||
from browser_cli.commands import client_from_ctx, handle_errors, tab_option
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
||||
@@ -12,10 +12,11 @@ def extract_group():
|
||||
"""Extract content from the active tab."""
|
||||
|
||||
@extract_group.command("links")
|
||||
@tab_option
|
||||
@handle_errors
|
||||
def extract_links():
|
||||
"""Extract all links from the active tab."""
|
||||
links = client_from_ctx().extract.links()
|
||||
def extract_links(tab_id):
|
||||
"""Extract all links from the active tab or --tab."""
|
||||
links = client_from_ctx().extract.links(tab_id)
|
||||
if not links:
|
||||
console.print("[yellow]No links found[/yellow]")
|
||||
return
|
||||
@@ -27,10 +28,11 @@ def extract_links():
|
||||
console.print(table)
|
||||
|
||||
@extract_group.command("images")
|
||||
@tab_option
|
||||
@handle_errors
|
||||
def extract_images():
|
||||
"""Extract all images from the active tab."""
|
||||
images = client_from_ctx().extract.images()
|
||||
def extract_images(tab_id):
|
||||
"""Extract all images from the active tab or --tab."""
|
||||
images = client_from_ctx().extract.images(tab_id)
|
||||
if not images:
|
||||
console.print("[yellow]No images found[/yellow]")
|
||||
return
|
||||
@@ -42,29 +44,33 @@ def extract_images():
|
||||
console.print(table)
|
||||
|
||||
@extract_group.command("text")
|
||||
@tab_option
|
||||
@handle_errors
|
||||
def extract_text():
|
||||
"""Extract all visible text from the active tab."""
|
||||
console.print(client_from_ctx().extract.text())
|
||||
def extract_text(tab_id):
|
||||
"""Extract all visible text from the active tab or --tab."""
|
||||
console.print(client_from_ctx().extract.text(tab_id))
|
||||
|
||||
@extract_group.command("json")
|
||||
@click.argument("selector")
|
||||
@tab_option
|
||||
@handle_errors
|
||||
def extract_json(selector):
|
||||
"""Parse and pretty-print JSON content inside SELECTOR."""
|
||||
data = client_from_ctx().extract.json(selector)
|
||||
def extract_json(selector, tab_id):
|
||||
"""Parse and pretty-print JSON content inside SELECTOR in the active tab or --tab."""
|
||||
data = client_from_ctx().extract.json(selector, tab_id)
|
||||
console.print_json(json.dumps(data))
|
||||
|
||||
@extract_group.command("html")
|
||||
@tab_option
|
||||
@handle_errors
|
||||
def extract_html():
|
||||
"""Print the full HTML of the active tab to stdout."""
|
||||
click.echo(client_from_ctx().extract.html())
|
||||
def extract_html(tab_id):
|
||||
"""Print the full HTML of the active tab or --tab to stdout."""
|
||||
click.echo(client_from_ctx().extract.html(tab_id))
|
||||
|
||||
@extract_group.command("markdown")
|
||||
@click.option("--selector", help="Extract only the DOM subtree matching this CSS selector.")
|
||||
@tab_option
|
||||
@handle_errors
|
||||
def extract_markdown(selector):
|
||||
"""Extract the page's main content as Markdown."""
|
||||
markdown = client_from_ctx().extract.markdown(selector)
|
||||
def extract_markdown(selector, tab_id):
|
||||
"""Extract the page's main content as Markdown from the active tab or --tab."""
|
||||
markdown = client_from_ctx().extract.markdown(selector, tab_id)
|
||||
click.echo(markdown or "", nl=not (markdown or "").endswith("\n"))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import click
|
||||
from browser_cli.commands import client_from_ctx, handle_errors
|
||||
from browser_cli.commands import client_from_ctx, handle_errors, tab_option
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
||||
@@ -10,10 +10,11 @@ def page_group():
|
||||
"""Inspect current page metadata."""
|
||||
|
||||
@page_group.command("info")
|
||||
@tab_option
|
||||
@handle_errors
|
||||
def page_info():
|
||||
"""Show title, URL, readyState, language, and meta tags of the active tab."""
|
||||
info = client_from_ctx().page.info()
|
||||
def page_info(tab_id):
|
||||
"""Show title, URL, readyState, language, and meta tags of the active tab or --tab."""
|
||||
info = client_from_ctx().page.info(tab_id)
|
||||
table = Table(show_header=False)
|
||||
table.add_column("Field", style="bold cyan", no_wrap=True)
|
||||
table.add_column("Value")
|
||||
|
||||
@@ -120,31 +120,37 @@ def create_server(*, client_factory: ClientFactory = BrowserCLI, tool_prefix: st
|
||||
|
||||
@mcp.tool(name=tool_name("page_info", prefix))
|
||||
def page_info(
|
||||
tab_id: int | None = None,
|
||||
browser: str | None = None,
|
||||
remote: str | None = None,
|
||||
key: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Return title, URL, readiness, language, and metadata for the active page."""
|
||||
return structured(_client(client_factory, browser, remote, key).page.info())
|
||||
"""Return title, URL, readiness, language, and metadata for the active page or tab_id."""
|
||||
client = _client(client_factory, browser, remote, key)
|
||||
if tab_id is not None:
|
||||
return structured(client.tabs.status(tab_id))
|
||||
return structured(client.page.info())
|
||||
|
||||
@mcp.tool(name=tool_name("extract_text", prefix))
|
||||
def extract_text(
|
||||
tab_id: int | None = None,
|
||||
browser: str | None = None,
|
||||
remote: str | None = None,
|
||||
key: str | None = None,
|
||||
) -> str:
|
||||
"""Extract plain text from the active page."""
|
||||
return _client(client_factory, browser, remote, key).extract.text()
|
||||
"""Extract plain text from the active page or tab_id."""
|
||||
return _client(client_factory, browser, remote, key).extract.text(tab_id)
|
||||
|
||||
@mcp.tool(name=tool_name("extract_markdown", prefix))
|
||||
def extract_markdown(
|
||||
selector: str | None = None,
|
||||
tab_id: int | None = None,
|
||||
browser: str | None = None,
|
||||
remote: str | None = None,
|
||||
key: str | None = None,
|
||||
) -> str:
|
||||
"""Extract clean Markdown from the active page or an optional CSS selector."""
|
||||
return _client(client_factory, browser, remote, key).extract.markdown(selector)
|
||||
"""Extract clean Markdown from the active page or tab_id, optionally scoped by CSS selector."""
|
||||
return _client(client_factory, browser, remote, key).extract.markdown(selector, tab_id)
|
||||
|
||||
@mcp.tool(name=tool_name("dom_query", prefix))
|
||||
def dom_query(
|
||||
|
||||
+25
-23
@@ -131,39 +131,41 @@ class DomNS(Namespace):
|
||||
})
|
||||
|
||||
class ExtractNS(Namespace):
|
||||
"""Extract structured content from the active tab."""
|
||||
"""Extract structured content from the active (or specified) tab."""
|
||||
|
||||
@sdk_command("extract.links", default=[])
|
||||
def links(self) -> list[dict]:
|
||||
"""Return links from the active tab."""
|
||||
@sdk_command("extract.links", lambda self, tab_id=None: {"tabId": tab_id}, default=[])
|
||||
def links(self, tab_id: int | None = None) -> list[dict]:
|
||||
"""Return links from the active tab or *tab_id*."""
|
||||
|
||||
@sdk_command("extract.images", default=[])
|
||||
def images(self) -> list[dict]:
|
||||
"""Return images from the active tab."""
|
||||
@sdk_command("extract.images", lambda self, tab_id=None: {"tabId": tab_id}, default=[])
|
||||
def images(self, tab_id: int | None = None) -> list[dict]:
|
||||
"""Return images from the active tab or *tab_id*."""
|
||||
|
||||
@sdk_command("extract.text", default="")
|
||||
def text(self) -> str:
|
||||
"""Return plain text from the active tab."""
|
||||
@sdk_command("extract.text", lambda self, tab_id=None: {"tabId": tab_id}, default="")
|
||||
def text(self, tab_id: int | None = None) -> str:
|
||||
"""Return plain text from the active tab or *tab_id*."""
|
||||
|
||||
@sdk_command("extract.json", lambda self, selector: {"selector": selector})
|
||||
def json(self, selector: str):
|
||||
"""Extract JSON-like structured data from a selector."""
|
||||
@sdk_command("extract.json", lambda self, selector, tab_id=None: {"selector": selector, "tabId": tab_id})
|
||||
def json(self, selector: str, tab_id: int | None = None):
|
||||
"""Extract JSON-like structured data from a selector in the active tab or *tab_id*."""
|
||||
|
||||
@sdk_command("extract.html", default="")
|
||||
def html(self) -> str:
|
||||
"""Return the full HTML source of the active tab."""
|
||||
@sdk_command("extract.html", lambda self, tab_id=None: {"tabId": tab_id}, default="")
|
||||
def html(self, tab_id: int | None = None) -> str:
|
||||
"""Return the full HTML source of the active tab or *tab_id*."""
|
||||
|
||||
@sdk_command("extract.markdown", lambda self, selector=None: {"selector": selector}, mapper=_extract_markdown)
|
||||
def markdown(self, selector: str | None = None) -> str:
|
||||
"""Extract the page's main content as clean Markdown.
|
||||
@sdk_command("extract.markdown", lambda self, selector=None, tab_id=None: {"selector": selector, "tabId": tab_id}, mapper=_extract_markdown)
|
||||
def markdown(self, selector: str | None = None, tab_id: int | None = None) -> str:
|
||||
"""Extract the page's main content as clean Markdown from the active tab or *tab_id*.
|
||||
|
||||
The extractor may return either Markdown or raw HTML; both are
|
||||
normalized to Markdown here so SDK and CLI callers get identical output.
|
||||
"""
|
||||
|
||||
class PageNS(Namespace):
|
||||
"""Inspect the active page."""
|
||||
"""Inspect the active page or a specified tab."""
|
||||
|
||||
@sdk_command("page.info", default={})
|
||||
def info(self) -> dict:
|
||||
"""Return title, URL, readyState, lang, and meta tags of the active tab."""
|
||||
def info(self, tab_id: int | None = None) -> dict:
|
||||
"""Return metadata for the active page, or tab status for *tab_id*."""
|
||||
if tab_id is not None:
|
||||
return self.command("tabs.status", {"tabId": tab_id}) or {}
|
||||
return self.command("page.info", {}) or {}
|
||||
|
||||
Reference in New Issue
Block a user