Drive a browser from a program
One browser on a pipe with navigera serve: one JSON command per line in, one reply out.
Programs can keep one browser on a pipe instead: navigera serve reads
one JSON command per stdin line, for example
{"id":1,"op":"goto","url":"https://example.com"}. The session socket speaks
the same protocol.
Serve protocol (programs)
ax without format returns the flat JSON list ([{ref, role, name, value?, url?}], url on links; main frame only) that serve clients have always received; send
"format":"text" for the indented tree the CLI prints.
{"id":1,"op":"goto","url":"https://example.com"}
{"id":2,"op":"ax","format":"text"}
{"id":3,"op":"click","ref":12}
{"id":4,"op":"fill","selector":"#q","value":"widget","timeout_ms":3000}
{"id":5,"op":"select","text":"Country","value":["Canada"]}
{"id":6,"op":"wait","text":"Saved"}
{"id":7,"op":"eval","expression":"() => document.title"}
{"id":8,"op":"quit"}
Each op’s fields are the long flag names in snake case: selector, ref,
text, value, timeout_ms, wait, format, limit, by, to,
files, ms, js, accept, prompt_text and so on. Malformed lines and
unknown ops return ok:false and leave the session running. EOF or quit
closes the browser. The session socket speaks exactly this protocol, one
connection at a time.
A line holding a JSON array of commands is a batch: each runs in order
and gets its own response line (a failure doesn’t stop the rest; quit
does). Use it where the shell can’t send newlines: PowerShell’s echo
passes \n through literally, so echo '{…}\n{…}' | navigera serve is one
bad line, while echo '[{…},{…}]' | navigera serve works. A leading UTF-8
BOM (PowerShell 5 adds one) is ignored.
Driving serve from a program. Read one response line per command line
(a batch: one per element), match responses by id, and give every read a
timeout. Read or discard stderr (stderr=DEVNULL in Python): a full stderr
pipe stalls the server. Decode stdout as UTF-8 (encoding="utf-8" in
Python; Windows’ default code page can’t decode page text). A minimal Python
client:
import json, subprocess
nv = subprocess.Popen(["navigera", "serve"], stdin=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL, text=True, encoding="utf-8")
def call(op, _id=[0], **fields):
_id[0] += 1
nv.stdin.write(json.dumps({"id": _id[0], "op": op, **fields}) + "\n"); nv.stdin.flush()
reply = json.loads(nv.stdout.readline())
assert reply["id"] == _id[0], reply
return reply
call("goto", url="https://example.com")
print(call("ax", format="text")["result"])
call("quit"); nv.wait()
An agent working from a shell needs none of this: use a session
(navigera -s work start, then one command per call). Per call, a direct
session command costs about 3 ms more than a line on a serve pipe (Linux,
warm session: title 3.3 vs 0.5 ms, ax 14.7 vs 11.1 ms), far less than one
model turn. A wrapper pays off only for long runs with no decision between
steps, written by a program that already exists; a script that launches its
own serve pays a browser cold start and loses cookies on every run.
Made from docs/guides/programs.md, docs/navigera.md.