# Reference
Everything about installing, starting, and driving `abt`: every op, every
endpoint, the CLI, MCP, and the mechanics behind the diff and the text track.
For what the toolkit is and why it's built this way, see the
[README](../README.md). If you're an agent about to drive it, read
[`guidelines/toolkit-workflow.md`](../guidelines/toolkit-workflow.md) first —
this document is the detail behind that one, not a replacement for it.
## Install
Needs **Python 3.11+** and **Google Chrome** or **Microsoft Edge** installed.
Drivers are resolved automatically — nothing to download by hand.
```bash
pip install ai-browser-toolkit
abt doctor # what browsers are installed, and where
```
Also on **winget** (`winget install skssmd.AIBrowserToolkit`), **Scoop**,
**Homebrew** (`brew install skssmd/tap/aibrowsertoolkit`) and the **AUR**.
Point an MCP client at it:
```json
{"mcpServers": {"browser": {"command": "abt", "args": ["mcp"]}}}
```
From a source checkout instead
```bash
git clone https://github.com/skssmd/Ai-Browser-Toolkit
cd Ai-Browser-Toolkit
python -m venv .venv
.venv/Scripts/python -m pip install -e ".[dev]" # Windows
# .venv/bin/python -m pip install -e ".[dev]" # macOS / Linux
.venv/Scripts/python -m pytest -q # 485 tests, needs Chrome
```
The `abt` command lands in the venv. Either activate it
(`.venv\Scripts\activate` / `source .venv/bin/activate`) so `abt` is on your
PATH, or call it explicitly as `.venv/Scripts/python -m abt.cli …` everywhere
below.
## Run
> **Start the server as a separate process.** `abt serve` is the command loop —
> it listens and **never returns on its own**. Run it in the foreground and it
> holds that terminal until you shut it down, so an agent or script that
> launches it inline will hang there forever. Background it, or give it its own
> terminal.
**`abt up` is the shortest safe way**, and the one to reach for from an agent:
```bash
abt up # start a server if none is running, then return
```
It returns in seconds and never blocks. On Windows it asks WMI (falling back to
Task Scheduler) to do the spawning, so the server's parent is a Windows service
rather than your shell — which means it sits **outside your caller's job
object**. That distinction is the whole point: a harness that waits on its job
will hang on a plain background launch no matter how you redirect stdio, and
`CREATE_BREAKAWAY_FROM_JOB` fails silently when the job forbids breakaway. On
POSIX it double-forks with `setsid`.
**The server starts with no browser.** It listens in about a second instead of
waiting up to two minutes for Chrome on the persistent profile. Ask for a
browser when you want one:
```bash
curl -X POST http://127.0.0.1:8765/browser/start
curl http://127.0.0.1:8765/browser # running? on what config?
curl http://127.0.0.1:8765/health # is the *server* up (never touches the driver)
```
Pass `--start-browser` to `abt serve` for the old eager behaviour.
**When a connection to the browser drops, the session mends it.** The command
that hit it returns once, saying so; the next command re-attaches to the same
browser -- relaunching the profile's Chrome if it is gone -- keeping the server,
the session log, your logins and your tabs. `browser_start` is safe to send at
any time: with a browser up it simply attaches.
**The safe way — use the start script.** It does the whole dance for you and is
the only thing an agent or CI job should call:
```bash
./start-server.sh # Linux / macOS / Git Bash / WSL
start-server.bat # Windows cmd
```
It no-ops if a server is already running, creates `.venv` and installs
dependencies only when they are missing, launches the server detached with its
output redirected to `server.log` / `server.err`, then polls `/health` and exits
0 once the server answers (about a second now that no browser is launched at
startup). Exit 1 means it never came up — it prints the tail
of `server.err`; exit 2 means dependencies failed.
```bash
./start-server.sh --status # only report up/down, start nothing (exit 0 = up)
./start-server.sh --no-wait # launch and return immediately, skip polling
./start-server.sh --browser edge --port 9000 --headless # extra flags go to `abt serve`
```
A server on a non-default port writes `server-.log` instead, so starting
one never clobbers the logs of the server on 8765.
**By hand,** if you prefer. Detaching is not enough on its own — redirect the
output too, or the launching process keeps waiting on the inherited pipe:
```bash
# Linux / macOS — background it and keep the log
nohup abt serve --browser chrome > server.log 2>&1 &
# Windows PowerShell
Start-Process .venv\Scripts\abt.exe `
-ArgumentList "serve","--browser","chrome" `
-RedirectStandardOutput server.log -RedirectStandardError server.err
# or just give it its own terminal and leave it there
abt serve --browser chrome
```
It is ready when `/status` answers — poll rather than sleeping a fixed amount,
since first launch has to start Chrome:
```bash
until curl -s -m 3 localhost:8765/status > /dev/null; do sleep 1; done
```
Options:
```bash
abt serve --headless # no window
abt serve --port 9000 # another port
abt serve --profile ~/work-profile # another profile
abt serve --no-log --no-diff # leanest responses
abt serve --settle-timeout 10 # give a slow SPA longer to render
abt serve --no-frames # stop reading inside iframes
```
Stop it with `abt shutdown` (or `POST {"op":"shutdown"}`), which closes Chrome
and exits the process. `Ctrl+C` works too if it has its own terminal.
Because the server owns exactly one browser, run **one server per browser** —
use `--port` and `--profile` together to run more than one side by side.
The profile directory persists cookies and logins across restarts, and is isolated
from your everyday Chrome. Log in by hand once in the visible window and the session
sticks around — including across a restart to pick up new code.
## Start at login
The toolkit is most useful already up: an agent that has to start a server
first pays for the start, and one that starts it wrongly wedges itself.
```bash
abt autostart install --browser chrome --dry-run # read it first
abt autostart install --browser chrome
abt autostart status
abt autostart uninstall
```
| Platform | What it writes |
|---|---|
| Windows | a Task Scheduler logon task |
| macOS | `~/Library/LaunchAgents/com.aibrowsertoolkit.server.plist` |
| Linux | `~/.config/systemd/user/abt-server.service` |
User-level, never elevated, and **opt in** — nothing is installed unless you
ask. It runs in the account that owns the browser profile, because a system
service would run as somebody else and find none of your logins.
**No browser is launched at login.** The entry runs `abt serve` without
`--start-browser`, so logging in costs about a second and the browser arrives
when you ask for it with `browser_start`. This is the whole reason the feature
is sane: against the old eager behaviour it would have opened Chrome on the
persistent profile at every logon.
`--browser` is required rather than prompted, and every path is resolved to an
absolute one at install time. A logon entry has neither a terminal to answer a
prompt nor a working directory to resolve against — a relative `./profile`
would land in `C:\Windows\System32` and quietly build a second, empty profile
there.
On Linux a user unit stops at logout. For a server that outlives the session:
```bash
loginctl enable-linger $USER
```
## Send commands
One command:
```bash
curl -s localhost:8765/command-list -H 'content-type: application/json' \
-d '{"op":"goto","url":"https://example.com"}'
```
A list, run in order:
```bash
curl -s localhost:8765/command-list -H 'content-type: application/json' -d '[
{"op":"goto","url":"https://example.com"},
{"op":"find","css":"a"},
{"op":"click","level":"AEDBa"},
{"op":"get_text","css":"h1"}
]'
```
Every response is `{"ok": true, "result": ...}` or
`{"ok": false, "error": {"type", "message", "op_index"}}`.
Batches stop at the first failure by default. Send
`{"commands": [...], "continue_on_error": true}` to run them all regardless.
## Console and network
The DOM cannot tell you why a request failed. These can:
```bash
curl -s localhost:8765/command-list -d '{"op":"read_network","failures_only":true}'
curl -s localhost:8765/command-list -d '{"op":"read_console","levels":["error"]}'
```
`read_console` captures from **document start**, so a reload hands back what the
page logged while loading — uncaught errors and unhandled rejections included.
`read_network` returns each request with its status, duration, and size;
`failures_only` keeps the 4xx/5xx and anything the browser would not disclose.
Both take a `pattern` regex.
## Levels: every string says where it sits
Text comes back with its position in the page in front of it — one letter per
level down from the body, so `AEDB` sits inside `AED`, which sits inside `AE`:
```
AEDBAAAB
a 000000192
b Sep 3, 2022
c $109.00
AEDBAAAC
a 000000174
b May 2, 2022
```
Two strings sharing a prefix sit in the same container — the same table row, the
same form group. That is what a flat list of strings cannot say, and what
counting rows and matching a value to its label depend on.
Past the 52nd sibling a level is a number (`AB53`); a dot separates two numbers
(`AB100.200`) and appears nowhere else.
**A `#` marks something you can operate, and the same address acts on it.**
```
AEDBa#btn Save changes
AEDBb#lnk Issues → /dashboard/issues?assignee_username=byteblaze
AEDBc#inp-q laptop
AEDBd#sel-country United Kingdom
AEDBe#lnk → /notifications
```
| mark | what |
|---|---|
| `#btn` | button, or anything behaving as one — `role="button"`, a focusable div |
| `#lnk` | link; its target follows the text after `→` |
| `#inp` | text input or textarea |
| `#sel` | select, or a custom `role="combobox"` widget |
| `#chk` `#rad` `#opt` `#file` | checkbox, radio, option, file input |
For `#inp` and `#sel` the **name rides in the mark** (`#inp-q`) and the line's
text is the **current value** — which is what you want to read back, and what
makes a field changing value show up in a diff at all.
The last line is a link with no text: an icon. Its target is all it has to say,
and before this it appeared nowhere.
Act on it with the same address:
```json
{"op": "click", "level": "AEDBa"}
{"op": "input", "level": "AEDBc", "value": "laptop pro"}
```
**`input` and `select` are two names for one intent** — make this control hold
that value — and either works on any of them. On a `#sel` the value is the
option's visible text (its underlying `value` is accepted too, and an option
matching neither is refused with the list of options that do exist). On a
`#chk` or `#rad` it is `"true"` or `"false"`, and the click is spent only if the
state actually changes, so setting a ticked box to `true` leaves it ticked
rather than toggling it off. A radio cannot be switched off directly; set the
one you want instead. A date field takes its value, and a text field is typed
into.
Neither spelling is a dead end. `select` at a checkbox used to be refused as
"not a `