Skip to content

Latest commit

 

History

History
175 lines (135 loc) · 5.1 KB

File metadata and controls

175 lines (135 loc) · 5.1 KB

ghostchrome (Python SDK)

Python SDK for ghostchrome browser automation.

Typed client for the ghostchrome JSONL agent protocol. Drives a persistent ghostchrome agent subprocess over stdin/stdout — no HTTP, no RPC overhead.

Requirements

  • Python 3.10+
  • ghostchrome binary in $PATH (or injected via the command kwarg)
  • No third-party runtime dependencies (stdlib only)

Installation

pip install ghostchrome
# or from source:
pip install -e sdk/python/

Quick start

from ghostchrome import Ghostchrome

# Attach to an already-running Chrome on :9222 (preferred mode)
with Ghostchrome(extra_flags=["--connect=auto"]) as gc:
    # Navigate — returns (NavigateResult, Observation)
    nav, obs = gc.navigate("https://example.com")
    print(nav.status)   # 200
    print(nav.title)    # "Example Domain"
    print(nav.time_ms)  # e.g. 42

    # Accessibility tree with @refs
    tree, _ = gc.extract(level="skeleton")
    print(tree.stats.total_nodes)       # e.g. 15
    print(tree.stats.interactive_count) # e.g. 1
    print(list(tree.refs.keys()))       # ["@1", ...]

    # Interact — omitted-result ops return (None, observation)
    gc.click("@1")
    gc.type_("@2", "hello world")
    gc.press("Enter")

    # Fill multiple fields at once
    fill_result, _ = gc.fill({"@1": "Alice", "@2": "pass"})
    print(fill_result.filled)  # 2

    # Scroll
    scroll_result, _ = gc.scroll_by(300)
    print(scroll_result.y)  # new Y position

    # Evaluate JavaScript
    val, _ = gc.eval_("document.title")
    print(val.value)  # "Example Domain"

    # Screenshot — always base64+mime over the wire
    shot, _ = gc.screenshot(full_page=True)
    print(shot.mime)   # "image/png"
    print(shot.base64) # base64 data

    # Current URL
    url, _ = gc.url()
    print(url.url, url.title)

    # Console and network errors — returns list[ErrorEntry], not a wrapper
    errors = gc.errors()
    for e in errors:
        print(e.type, e.level, e.message)

Result shapes (ground truth)

All shapes are measured from the live binary — never guessed.

Method Result type Key fields
navigate(url, *, wait?) NavigateResult url, title, status, time_ms
back() BackForwardResult url?, title?
forward() BackForwardResult url?, title?
extract(*, level?, selector?) ExtractResult nodes, refs, stats (→ ExtractStats)
click(ref) None
hover(ref) None
type_(ref, text) None
press(key, *, ref?) None
select(ref, values) None
fill(fields) FillResult filled: int
scroll_by(dy) ScrollResult y: int
scroll_to(*, y?, bottom?) ScrollResult y: int
eval_(expr, *, ref?) EvalResult value: str
screenshot(*, full_page?, ref?, quality?) ScreenshotResult mime, base64
wait(*, selector?, ms?) None
errors() list[ErrorEntry] type, level, message, source, time_ms, status?, method?
url() UrlResult url, title
init() InitResult
close() None

ExtractStats fields: total_nodes, filtered_nodes, interactive_count.

Error handling

When ok=false, the client raises GhostchromeError (a RuntimeError subclass):

from ghostchrome import Ghostchrome, GhostchromeError

with Ghostchrome(extra_flags=["--connect=auto"]) as gc:
    gc.navigate("https://example.com")
    try:
        gc.click("@99")
    except GhostchromeError as e:
        print(e.op)      # "click"
        print(e.message) # "ref @99 not found"

Advanced usage

Custom binary path / flags

gc = Ghostchrome(
    command="/usr/local/bin/ghostchrome",
    extra_flags=["--stealth", "--observe"],
    timeout=60.0,
)
result, _ = gc.navigate("https://app.example.com")
gc.close()

Inject your own transport

from ghostchrome import Ghostchrome
from ghostchrome.transport import SubprocessTransport

transport = SubprocessTransport(
    command="ghostchrome",
    args=["agent"],
    extra_flags=["--connect=auto"],
)
gc = Ghostchrome(transport=transport)

Running tests

cd sdk/python
python3 -m unittest discover -s tests -q

Tests are hermetic — no real browser or ghostchrome binary needed.

Wire protocol

The SDK implements the JSONL agent protocol documented in docs/recipes/agent-jsonl.md.

Each op is a JSON line on stdin:

{"id":"r1","op":"navigate","args":{"url":"https://example.com"}}

Each response is a JSON line on stdout:

{"id":"r1","ok":true,"result":{"url":"https://example.com/","title":"Example Domain","status":200,"time_ms":42},"observation":{"url":"https://example.com/"}}

The errors op returns a JSON array directly (not a dict):

{"id":"r2","ok":true,"result":[{"type":"console","level":"error","message":"...","source":"app.js:1","time_ms":10}],"observation":{...}}

Ops that produce no output omit result entirely (init, close, click, hover, type, press, select, wait).