#!/usr/bin/env python3 """Minimal client for Firefox Agent Bridge. Stdlib only.""" from __future__ import annotations import argparse import json import os import sys import urllib.error import urllib.request from pathlib import Path from typing import Any DEFAULT_BASE = os.environ.get("FAB_URL", "http://127.0.0.1:17634") TOKEN_PATH = Path(os.environ.get("LOCALAPPDATA", str(Path.home()))) / "firefox-agent-bridge" / "token" def token() -> str: env = os.environ.get("FAB_TOKEN") if env: return env.strip() if TOKEN_PATH.exists(): return TOKEN_PATH.read_text(encoding="utf-8").strip() raise SystemExit(f"no token: set FAB_TOKEN or create {TOKEN_PATH}") def call(method: str, args: list[Any] | None = None, base: str = DEFAULT_BASE) -> Any: payload = json.dumps({"method": method, "args": args or []}).encode("utf-8") req = urllib.request.Request( base.rstrip("/") + "/v1/call", data=payload, method="POST", headers={ "Authorization": f"Bearer {token()}", "Content-Type": "application/json", }, ) try: with urllib.request.urlopen(req, timeout=20) as resp: body = json.loads(resp.read().decode("utf-8")) except urllib.error.URLError as exc: raise SystemExit( f"bridge unreachable at {base} ({exc}). " "Firefox must be open with the extension loaded." ) from exc if not body.get("ok"): raise SystemExit(body.get("error") or body) return body.get("result") def main() -> int: parser = argparse.ArgumentParser(description="Call Firefox Agent Bridge") parser.add_argument("method", help="e.g. bookmarks.search or meta.methods") parser.add_argument("args_json", nargs="?", default="[]", help="JSON array of arguments") parser.add_argument("--url", default=DEFAULT_BASE) ns = parser.parse_args() args = json.loads(ns.args_json) if not isinstance(args, list): raise SystemExit("args_json must be a JSON array") print(json.dumps(call(ns.method, args, base=ns.url), indent=2)) return 0 if __name__ == "__main__": raise SystemExit(main())