chore: remove ServiceLink integration
- Drop the ServiceLink submodule, link-serve command, and serve --rpc mode. - Remove the now-unused httpx dependency and regenerate uv.lock. - Add a privacy policy for Chrome Web Store publication. - Refresh extension icons and bump package/extension version to 0.14.2. - Keep the native TCP serve path as the only remote-control endpoint.
@@ -1,3 +0,0 @@
|
|||||||
[submodule "servicelink"]
|
|
||||||
path = servicelink
|
|
||||||
url = git@git.yiprawr.dev:submodules/servicelink.git
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Privacy Policy for browser-cli
|
||||||
|
Last updated: 2026-06-14
|
||||||
|
|
||||||
|
browser-cli does not collect, transmit, sell, or share user data with the developer or any third party.
|
||||||
|
|
||||||
|
browser-cli is a local browser automation tool. The browser extension communicates with the locally installed browser-cli native messaging host so the user can control their own browser through the command line or Python SDK.
|
||||||
|
|
||||||
|
## Local data access
|
||||||
|
Depending on the command explicitly run by the user, browser-cli may locally access browser data such as:
|
||||||
|
- tab URLs, titles, status, and window or tab group information
|
||||||
|
- page content, links, images, HTML, text, screenshots, or DOM data
|
||||||
|
- cookies, local storage, session storage, and saved browser-cli session data
|
||||||
|
|
||||||
|
This access happens only to perform the command requested by the user. The data stays on the user's device unless the user explicitly configures browser-cli to connect to another machine they control.
|
||||||
|
|
||||||
|
## Remote control mode
|
||||||
|
browser-cli includes an optional remote control mode. If the user enables this mode, command data may be transmitted between the user's configured browser-cli client and server endpoints. This is user-configured infrastructure. The developer does not receive or operate these endpoints.
|
||||||
|
|
||||||
|
## No analytics or tracking
|
||||||
|
browser-cli does not use analytics, telemetry, advertising, behavioral tracking, or remote code. The extension does not send data to the developer.
|
||||||
|
|
||||||
|
## Contact
|
||||||
|
For privacy questions or security reports, please open an issue in the project repository or contact the project maintainer through the repository hosting platform.
|
||||||
@@ -23,7 +23,6 @@ from browser_cli.commands.storage import storage_group
|
|||||||
from browser_cli.commands.perf import perf_group
|
from browser_cli.commands.perf import perf_group
|
||||||
from browser_cli.commands.extension import extension_group
|
from browser_cli.commands.extension import extension_group
|
||||||
from browser_cli.commands.serve import cmd_serve
|
from browser_cli.commands.serve import cmd_serve
|
||||||
from browser_cli.commands.link_serve import cmd_link_serve
|
|
||||||
from browser_cli.commands.auth import auth_group
|
from browser_cli.commands.auth import auth_group
|
||||||
from browser_cli.commands.clients import clients_group
|
from browser_cli.commands.clients import clients_group
|
||||||
from browser_cli.commands.completion import cmd_completion
|
from browser_cli.commands.completion import cmd_completion
|
||||||
@@ -128,7 +127,6 @@ main.add_command(storage_group)
|
|||||||
main.add_command(perf_group)
|
main.add_command(perf_group)
|
||||||
main.add_command(extension_group)
|
main.add_command(extension_group)
|
||||||
main.add_command(cmd_serve)
|
main.add_command(cmd_serve)
|
||||||
main.add_command(cmd_link_serve)
|
|
||||||
main.add_command(clients_group)
|
main.add_command(clients_group)
|
||||||
main.add_command(cmd_completion)
|
main.add_command(cmd_completion)
|
||||||
main.add_command(cmd_install)
|
main.add_command(cmd_install)
|
||||||
|
|||||||
@@ -1,163 +0,0 @@
|
|||||||
"""Expose this browser over a ServiceLink HTTP /rpc endpoint.
|
|
||||||
|
|
||||||
This lets the other nodes (picoshare, website) drive the browser through the
|
|
||||||
shared servicelink envelope, reusing browser-cli's own wire commands verbatim:
|
|
||||||
a link method like `tabs.list` forwards straight to `send_command_async`.
|
|
||||||
|
|
||||||
It is separate from `serve` (the Ed25519/TCP remote-control daemon); use this
|
|
||||||
when you want a node in the mesh to call the browser with a bearer token.
|
|
||||||
|
|
||||||
servicelink (and its httpx dependency) is imported lazily inside the command so
|
|
||||||
that a missing optional dependency never breaks the rest of the CLI.
|
|
||||||
"""
|
|
||||||
import asyncio
|
|
||||||
import hmac
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import click
|
|
||||||
|
|
||||||
from browser_cli.client.core import send_command_async
|
|
||||||
from browser_cli.errors import BrowserNotConnected
|
|
||||||
|
|
||||||
# Curated set of browser commands exposed over the mesh. Method name == command.
|
|
||||||
EXPOSED_COMMANDS = [
|
|
||||||
"tabs.list", "tabs.open", "tabs.close", "tabs.active", "tabs.query",
|
|
||||||
"nav.open", "nav.reload", "nav.back", "nav.forward",
|
|
||||||
"dom.query", "dom.text", "dom.attr", "dom.exists", "dom.click",
|
|
||||||
"extract.links", "extract.images", "extract.text", "extract.markdown", "extract.html",
|
|
||||||
"page.info",
|
|
||||||
"session.save", "session.load", "session.list",
|
|
||||||
]
|
|
||||||
|
|
||||||
def _import_servicelink():
|
|
||||||
"""Import servicelink lazily; the flat submodule sits at the repo root."""
|
|
||||||
repo_root = Path(__file__).resolve().parents[2]
|
|
||||||
if str(repo_root) not in sys.path:
|
|
||||||
sys.path.insert(0, str(repo_root))
|
|
||||||
try:
|
|
||||||
import servicelink
|
|
||||||
return servicelink
|
|
||||||
except ImportError as exc:
|
|
||||||
raise click.ClickException(
|
|
||||||
"servicelink could not be imported. Initialise the submodule "
|
|
||||||
"(`git submodule update --init`) and ensure httpx is installed."
|
|
||||||
) from exc
|
|
||||||
|
|
||||||
def _build_router(sl, profile):
|
|
||||||
router = sl.Router("browser-cli")
|
|
||||||
|
|
||||||
def make_handler(command):
|
|
||||||
async def handler(params, ctx):
|
|
||||||
try:
|
|
||||||
return await send_command_async(command, params or None, profile=profile)
|
|
||||||
except BrowserNotConnected as exc:
|
|
||||||
raise sl.Unavailable(f"browser not connected: {exc}")
|
|
||||||
except (RuntimeError, ConnectionError) as exc:
|
|
||||||
raise sl.LinkError(str(exc))
|
|
||||||
return handler
|
|
||||||
|
|
||||||
for command in EXPOSED_COMMANDS:
|
|
||||||
router.register(command, make_handler(command))
|
|
||||||
return router
|
|
||||||
|
|
||||||
def _make_verifier(sl, token):
|
|
||||||
if not token:
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def verify(authorization, request):
|
|
||||||
presented = (authorization or "").split(" ", 1)[-1].strip()
|
|
||||||
# Constant-time compare so a wrong token can't be timed out character by character.
|
|
||||||
if not presented or not hmac.compare_digest(presented, token):
|
|
||||||
raise sl.Unauthorized("invalid or missing token")
|
|
||||||
return sl.Principal(subject="mesh", scopes=frozenset({"all", "mesh"}))
|
|
||||||
|
|
||||||
return verify
|
|
||||||
|
|
||||||
def _http_response(status, body, content_type):
|
|
||||||
head = (
|
|
||||||
f"HTTP/1.1 {status}\r\n"
|
|
||||||
f"Content-Type: {content_type}\r\n"
|
|
||||||
f"Content-Length: {len(body)}\r\n"
|
|
||||||
"Connection: close\r\n\r\n"
|
|
||||||
).encode("latin-1")
|
|
||||||
return head + body
|
|
||||||
|
|
||||||
async def _handle_connection(sl, router, verify, reader, writer):
|
|
||||||
try:
|
|
||||||
request_line = await reader.readline()
|
|
||||||
if not request_line:
|
|
||||||
return
|
|
||||||
headers = {}
|
|
||||||
while True:
|
|
||||||
line = await reader.readline()
|
|
||||||
if line in (b"\r\n", b"\n", b""):
|
|
||||||
break
|
|
||||||
key, _, value = line.decode("latin-1").partition(":")
|
|
||||||
headers[key.strip().lower()] = value.strip()
|
|
||||||
length = int(headers.get("content-length", "0") or "0")
|
|
||||||
body = await reader.readexactly(length) if length else b""
|
|
||||||
|
|
||||||
if not request_line.upper().startswith(b"POST"):
|
|
||||||
writer.write(_http_response(405, b'{"error":"only POST /rpc is supported"}', "application/json"))
|
|
||||||
else:
|
|
||||||
status, payload, content_type = await sl.handle_envelope(
|
|
||||||
router,
|
|
||||||
body,
|
|
||||||
authorization=headers.get("authorization"),
|
|
||||||
verify=verify,
|
|
||||||
content_type=headers.get("content-type", "application/json"),
|
|
||||||
)
|
|
||||||
writer.write(_http_response(status, payload, content_type))
|
|
||||||
await writer.drain()
|
|
||||||
except Exception: # noqa: BLE001 - never let one connection kill the server
|
|
||||||
pass
|
|
||||||
finally:
|
|
||||||
writer.close()
|
|
||||||
|
|
||||||
async def _serve(sl, host, port, profile, token):
|
|
||||||
router = _build_router(sl, profile)
|
|
||||||
verify = _make_verifier(sl, token)
|
|
||||||
server = await asyncio.start_server(
|
|
||||||
lambda r, w: _handle_connection(sl, router, verify, r, w), host, port
|
|
||||||
)
|
|
||||||
click.echo(f"servicelink browser node listening on http://{host}:{port}/rpc")
|
|
||||||
async with server:
|
|
||||||
await server.serve_forever()
|
|
||||||
|
|
||||||
_LOOPBACK_HOSTS = {"127.0.0.1", "::1", "localhost"}
|
|
||||||
|
|
||||||
@click.command("link-serve")
|
|
||||||
@click.option("--host", default="127.0.0.1", show_default=True, help="Address to bind.")
|
|
||||||
@click.option("--port", default=8770, show_default=True, type=int, help="HTTP port for /rpc.")
|
|
||||||
@click.option("--token", default=None, metavar="SECRET",
|
|
||||||
help="Shared bearer token required from callers (sent as 'Authorization: Bearer ...').")
|
|
||||||
@click.option("--insecure", is_flag=True, default=False,
|
|
||||||
help="Run with NO token. Grants full browser control to anyone who can reach the port.")
|
|
||||||
@click.pass_context
|
|
||||||
def cmd_link_serve(ctx, host, port, token, insecure):
|
|
||||||
"""Serve this browser to the ServiceLink mesh over HTTP /rpc.
|
|
||||||
|
|
||||||
Exposes the running browser (open/scrape pages, read storage), so
|
|
||||||
a token is required by default. Bind to loopback and keep the port off the
|
|
||||||
public network.
|
|
||||||
"""
|
|
||||||
if not token and not insecure:
|
|
||||||
raise click.ClickException(
|
|
||||||
"Refusing to start without --token (this endpoint can control your browser "
|
|
||||||
"and read page/storage data). Pass --insecure to override on a trusted host."
|
|
||||||
)
|
|
||||||
if host not in _LOOPBACK_HOSTS:
|
|
||||||
click.echo(
|
|
||||||
f"WARNING: binding to {host} (not loopback). Anyone who can reach this "
|
|
||||||
"address may control the browser; ensure it is firewalled.",
|
|
||||||
err=True,
|
|
||||||
)
|
|
||||||
if insecure and not token:
|
|
||||||
click.echo("WARNING: --insecure set; the endpoint is UNAUTHENTICATED.", err=True)
|
|
||||||
sl = _import_servicelink()
|
|
||||||
profile = ctx.obj.get("browser") if ctx.obj else None
|
|
||||||
try:
|
|
||||||
asyncio.run(_serve(sl, host, port, profile, token))
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
pass
|
|
||||||
@@ -45,32 +45,9 @@ __all__ = [
|
|||||||
default=False,
|
default=False,
|
||||||
help="Disable response compression / msgpack even for clients that support it.",
|
help="Disable response compression / msgpack even for clients that support it.",
|
||||||
)
|
)
|
||||||
@click.option(
|
|
||||||
"--rpc",
|
|
||||||
is_flag=True,
|
|
||||||
default=False,
|
|
||||||
help="Also expose a ServiceLink HTTP /rpc endpoint (for mesh nodes) in the same process.",
|
|
||||||
)
|
|
||||||
@click.option("--rpc-port", default=8770, show_default=True, type=int, help="Port for the /rpc endpoint (with --rpc).")
|
|
||||||
@click.option(
|
|
||||||
"--rpc-token",
|
|
||||||
default=None,
|
|
||||||
metavar="SECRET",
|
|
||||||
help="Bearer token required on /rpc (with --rpc).",
|
|
||||||
)
|
|
||||||
@click.option(
|
|
||||||
"--rpc-insecure",
|
|
||||||
is_flag=True,
|
|
||||||
default=False,
|
|
||||||
help="Allow --rpc with no token (DANGEROUS: full browser control to anyone who can reach the port).",
|
|
||||||
)
|
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def cmd_serve(ctx, host, port, no_auth, auth_keys_file, no_compress, rpc, rpc_port, rpc_token, rpc_insecure):
|
def cmd_serve(ctx, host, port, no_auth, auth_keys_file, no_compress):
|
||||||
"""Expose this browser over TCP so remote hosts can control it.
|
"""Expose this browser over TCP so remote hosts can control it."""
|
||||||
|
|
||||||
With --rpc, additionally serve the ServiceLink mesh over HTTP /rpc on
|
|
||||||
--rpc-port, so the native TCP protocol and the node mesh share one daemon.
|
|
||||||
"""
|
|
||||||
profile = ctx.obj.get("browser") if ctx.obj else None
|
profile = ctx.obj.get("browser") if ctx.obj else None
|
||||||
compress = not no_compress
|
compress = not no_compress
|
||||||
|
|
||||||
@@ -84,40 +61,16 @@ def cmd_serve(ctx, host, port, no_auth, auth_keys_file, no_compress, rpc, rpc_po
|
|||||||
if auth_keys_path is False:
|
if auth_keys_path is False:
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
if rpc and not rpc_token and not rpc_insecure:
|
|
||||||
console.print(
|
|
||||||
"[red]Error:[/red] --rpc requires --rpc-token (this endpoint can control your "
|
|
||||||
"browser and read page/storage data). Use --rpc-insecure to override on a trusted host."
|
|
||||||
)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
_print_startup(host, port, profile, auth_keys_path, compress)
|
_print_startup(host, port, profile, auth_keys_path, compress)
|
||||||
if rpc:
|
|
||||||
console.print(f" Mesh: [green]ServiceLink HTTP[/green] [cyan]{host}:{rpc_port}/rpc[/cyan]")
|
|
||||||
if not rpc_token:
|
|
||||||
console.print("[yellow] /rpc auth disabled (--rpc-insecure)[/yellow]")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if rpc:
|
asyncio.run(_serve_async(host, port, profile, auth_keys_path, compress))
|
||||||
asyncio.run(_serve_with_rpc(host, port, profile, auth_keys_path, compress, rpc_port, rpc_token))
|
|
||||||
else:
|
|
||||||
asyncio.run(_serve_async(host, port, profile, auth_keys_path, compress))
|
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
console.print(f"[red]Cannot bind to {host}:{port}:[/red] {e}")
|
console.print(f"[red]Cannot bind to {host}:{port}:[/red] {e}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
console.print("[yellow]Stopped.[/yellow]")
|
console.print("[yellow]Stopped.[/yellow]")
|
||||||
|
|
||||||
async def _serve_with_rpc(host, port, profile, auth_keys_path, compress, rpc_port, rpc_token):
|
|
||||||
"""Run the native TCP server and the ServiceLink HTTP /rpc server together."""
|
|
||||||
from browser_cli.commands import link_serve
|
|
||||||
|
|
||||||
sl = link_serve._import_servicelink()
|
|
||||||
await asyncio.gather(
|
|
||||||
_serve_async(host, port, profile, auth_keys_path, compress),
|
|
||||||
link_serve._serve(sl, host, rpc_port, profile, rpc_token),
|
|
||||||
)
|
|
||||||
|
|
||||||
def _resolve_auth_keys_path(auth_keys_file: str | None, no_auth: bool) -> Path | None | bool:
|
def _resolve_auth_keys_path(auth_keys_file: str | None, no_auth: bool) -> Path | None | bool:
|
||||||
if auth_keys_file:
|
if auth_keys_file:
|
||||||
from browser_cli.auth import load_authorized_keys
|
from browser_cli.auth import load_authorized_keys
|
||||||
|
|||||||
@@ -1,28 +1,31 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" role="img" aria-labelledby="title">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" role="img" aria-labelledby="title">
|
||||||
<title>browser-cli icon</title>
|
<title>browser-cli icon</title>
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="bg" x1="12" y1="10" x2="116" y2="118" gradientUnits="userSpaceOnUse">
|
<linearGradient id="bg" x1="16" y1="16" x2="112" y2="112" gradientUnits="userSpaceOnUse">
|
||||||
<stop offset="0" stop-color="#0f766e" />
|
<stop offset="0" stop-color="#0f766e" />
|
||||||
<stop offset="1" stop-color="#0f172a" />
|
<stop offset="1" stop-color="#0f172a" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<linearGradient id="panel" x1="28" y1="24" x2="100" y2="104" gradientUnits="userSpaceOnUse">
|
<linearGradient id="panel" x1="32" y1="29" x2="96" y2="99" gradientUnits="userSpaceOnUse">
|
||||||
<stop offset="0" stop-color="#f8fafc" />
|
<stop offset="0" stop-color="#f8fafc" />
|
||||||
<stop offset="1" stop-color="#cbd5e1" />
|
<stop offset="1" stop-color="#cbd5e1" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
|
|
||||||
<rect x="8" y="8" width="112" height="112" rx="28" fill="url(#bg)" />
|
<!-- Chrome Web Store compliant: 96x96 artwork centered in 128x128 canvas. -->
|
||||||
<rect x="25" y="25" width="78" height="66" rx="14" fill="url(#panel)" />
|
<rect x="16" y="16" width="96" height="96" rx="24" fill="url(#bg)" />
|
||||||
<rect x="25" y="25" width="78" height="15" rx="14" fill="#94a3b8" />
|
<rect x="17" y="17" width="94" height="94" rx="23" fill="none" stroke="#ccfbf1" stroke-opacity="0.55" stroke-width="2" />
|
||||||
<circle cx="36" cy="32.5" r="2.5" fill="#f8fafc" />
|
|
||||||
<circle cx="44" cy="32.5" r="2.5" fill="#f8fafc" opacity="0.85" />
|
|
||||||
<circle cx="52" cy="32.5" r="2.5" fill="#f8fafc" opacity="0.7" />
|
|
||||||
|
|
||||||
<path d="M46 56 35 64l11 8" fill="none" stroke="#0f172a" stroke-linecap="round" stroke-linejoin="round" stroke-width="8" />
|
<rect x="32" y="31" width="64" height="54" rx="11" fill="url(#panel)" />
|
||||||
<path d="M62 52h19" fill="none" stroke="#0f766e" stroke-linecap="round" stroke-width="8" />
|
<path d="M32 42c0-6.075 4.925-11 11-11h42c6.075 0 11 4.925 11 11v3H32z" fill="#94a3b8" />
|
||||||
<path d="M62 65h26" fill="none" stroke="#0f766e" stroke-linecap="round" stroke-width="8" />
|
<circle cx="42" cy="38.5" r="2.2" fill="#f8fafc" />
|
||||||
|
<circle cx="49" cy="38.5" r="2.2" fill="#f8fafc" opacity="0.85" />
|
||||||
|
<circle cx="56" cy="38.5" r="2.2" fill="#f8fafc" opacity="0.7" />
|
||||||
|
|
||||||
<rect x="69" y="77" width="26" height="17" rx="6" fill="#14b8a6" />
|
<path d="M49 57 40 64l9 7" fill="none" stroke="#0f172a" stroke-linecap="round" stroke-linejoin="round" stroke-width="7" />
|
||||||
<rect x="56" y="84" width="26" height="17" rx="6" fill="#2dd4bf" />
|
<path d="M62 55h17" fill="none" stroke="#0f766e" stroke-linecap="round" stroke-width="7" />
|
||||||
<rect x="43" y="91" width="26" height="17" rx="6" fill="#99f6e4" />
|
<path d="M62 67h23" fill="none" stroke="#0f766e" stroke-linecap="round" stroke-width="7" />
|
||||||
|
|
||||||
|
<rect x="70" y="78" width="22" height="15" rx="5" fill="#14b8a6" />
|
||||||
|
<rect x="59" y="84" width="22" height="15" rx="5" fill="#2dd4bf" />
|
||||||
|
<rect x="48" y="90" width="22" height="15" rx="5" fill="#99f6e4" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 576 B |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 1.9 KiB |
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "browser-cli",
|
"name": "browser-cli",
|
||||||
"version": "0.14.1",
|
"version": "0.14.2",
|
||||||
"description": "Control your browser from the terminal or Python SDK",
|
"description": "Control your browser from the terminal or Python SDK",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"tabs",
|
"tabs",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "browser-cli"
|
name = "browser-cli"
|
||||||
version = "0.14.1"
|
version = "0.14.2"
|
||||||
description = "Control your real running browser from the terminal or Python SDK"
|
description = "Control your real running browser from the terminal or Python SDK"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
@@ -8,7 +8,6 @@ dependencies = [
|
|||||||
"cryptography>=48",
|
"cryptography>=48",
|
||||||
"rich>=13",
|
"rich>=13",
|
||||||
"msgpack>=1",
|
"msgpack>=1",
|
||||||
"httpx>=0.28",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
|
|||||||
@@ -2,28 +2,13 @@ version = 1
|
|||||||
revision = 3
|
revision = 3
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "anyio"
|
|
||||||
version = "4.13.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
|
|
||||||
{ name = "idna" },
|
|
||||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "browser-cli"
|
name = "browser-cli"
|
||||||
version = "0.14.1"
|
version = "0.14.2"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "click" },
|
{ name = "click" },
|
||||||
{ name = "cryptography" },
|
{ name = "cryptography" },
|
||||||
{ name = "httpx" },
|
|
||||||
{ name = "msgpack" },
|
{ name = "msgpack" },
|
||||||
{ name = "rich" },
|
{ name = "rich" },
|
||||||
]
|
]
|
||||||
@@ -44,7 +29,6 @@ dev = [
|
|||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "click", specifier = ">=8" },
|
{ name = "click", specifier = ">=8" },
|
||||||
{ name = "cryptography", specifier = ">=48" },
|
{ name = "cryptography", specifier = ">=48" },
|
||||||
{ name = "httpx", specifier = ">=0.28" },
|
|
||||||
{ name = "msgpack", specifier = ">=1" },
|
{ name = "msgpack", specifier = ">=1" },
|
||||||
{ name = "rich", specifier = ">=13" },
|
{ name = "rich", specifier = ">=13" },
|
||||||
{ name = "zstandard", marker = "extra == 'fast'", specifier = ">=0.22" },
|
{ name = "zstandard", marker = "extra == 'fast'", specifier = ">=0.22" },
|
||||||
@@ -58,15 +42,6 @@ dev = [
|
|||||||
{ name = "zstandard", specifier = ">=0.22" },
|
{ name = "zstandard", specifier = ">=0.22" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "certifi"
|
|
||||||
version = "2026.5.20"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/f3/ce/ee2ecad540810a79593028e88299baeae54d346cc7a0d94b6199988b89b1/certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d", size = 135422, upload-time = "2026-05-20T11:46:50.073Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/59/8c/57e832b7af6d7c5abe66eb3fbe3a3a32f4d11ea23a1aa7131371035be991/certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897", size = 134134, upload-time = "2026-05-20T11:46:48.578Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cffi"
|
name = "cffi"
|
||||||
version = "2.0.0"
|
version = "2.0.0"
|
||||||
@@ -357,52 +332,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
|
{ url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "h11"
|
|
||||||
version = "0.16.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "httpcore"
|
|
||||||
version = "1.0.9"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "certifi" },
|
|
||||||
{ name = "h11" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "httpx"
|
|
||||||
version = "0.28.1"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "anyio" },
|
|
||||||
{ name = "certifi" },
|
|
||||||
{ name = "httpcore" },
|
|
||||||
{ name = "idna" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "idna"
|
|
||||||
version = "3.18"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iniconfig"
|
name = "iniconfig"
|
||||||
version = "2.3.0"
|
version = "2.3.0"
|
||||||
|
|||||||