screencast
Produce a polished product-demo video from a browser flow that the agent drives with agent-browser.
The agent performs the demo (clicks, typing, navigation); this skill records it and post-processes the
raw capture into a cinematic MP4 — locally, with no paid tool and no upload.
What it renders
- Punch-in / pull-back zoom + pan — the camera zooms to an element only while something happens
there, then pulls straight back to 1x so the whole app, and what the action just changed, stays
visible between beats. Element-fit sizing (small controls get a closer zoom, wide ones barely any),
pan clamped so the page always fills the card. Back-to-back actions keep the zoom instead of
strobing once per click
- Animated cursor with a subtle trail and a click "pop" (synthesized — the raw capture has no cursor)
- Click ripples on every click
- Keystroke overlay (keycast) showing typed text and pressed keys
- Element highlight — spotlight (dim everything else) or a glowing ring around an element
- Annotations — labelled callouts with a connector pointing at an element or point
- Idle trimming — dead time freezes then hard-cuts, never fast-forwards
- Chapters — on-screen lower-thirds and real MP4 chapter markers
- Framing — gradient + accent glows, rounded corners, layered shadow, padding, intro/outro fades
Requirements
agent-browser on PATH with its browser binaries installed (agent-browser install, plus
--with-deps on Linux). The browser is driven and recorded through it.
ffmpeg + ffprobe on PATH
- Node 24+ — the renderer itself only needs 18+, but current
agent-browser declares
engines.node >= 24, so that is the real floor. It auto-installs @napi-rs/canvas (a prebuilt
binary, no compilation) into the skill directory on first run, at the exact version package.json
pins, so the renderer never moves onto an untested build behind your back. To upgrade it, change
that version and delete node_modules/.
Rendering is entirely local. The capture is whatever agent-browser is configured to drive, so
"no cloud" holds for a local browser; a remote/cloud provider would host the page itself elsewhere.
How to use it
Invoke the wrapper instead of agent-browser for the actions you want in the video. It is a thin
passthrough: anything it does not handle itself runs unchanged. Three caveats worth knowing —
start, stop, render, chapter, highlight and note are the wrapper's own commands (and
highlight shadows the upstream one of that name); only the actions listed under Commands below
produce timeline events; and --private is consumed before the call rather than forwarded.
Run it as node <SKILL_DIR>/bin/screencast.mjs … (or symlink that file onto your PATH as screencast).
# 1. Start: sets a fixed 1280x720 viewport, starts recording, drops a sync flash.
screencast start demo https://app.example.com/login
# 2. Explore normally (not logged as an action):
screencast snapshot -i
# 3. Drive the demo. Mark chapters as you go; run actions through the wrapper:
screencast chapter "Sign in"
screencast type @e3 "ada@example.com"
screencast type @e4 --private "$DEMO_PASSWORD" # kept out of the log and the keycast
screencast press Enter
screencast chapter "Create a project"
screencast click @e12
screencast type @e15 "My first project"
screencast click @e18
# 4. Stop → renders ./demo.mp4
screencast stop
Output: ./<name>.mp4 in the current directory (1920×1080, 30 fps, H.264, with chapters).
Commands
| Command |
Purpose |
screencast start <name> [url] |
Set viewport, start recording, inject the sync flash |
screencast <agent-browser cmd…> |
Run any agent-browser command; actions are logged |
screencast chapter "<title>" |
Mark a chapter at the current moment |
screencast highlight @ref [dur] |
Spotlight (or ring) an element and zoom to it |
screencast note "<text>" @ref [dur] |
Point a labelled callout at an element or point |
screencast stop |
Stop recording and render <name>.mp4 |
screencast render [name] |
Re-render from an existing take (fast iteration on styling) |
Logged actions: click, dblclick, type, fill, press, keyboard, hover, check, uncheck,
select. Everything else (snapshot, get, wait, screenshot, open, …) passes straight through
without adding an event.
Secrets
Typed text is written to the event log and drawn into the keystroke overlay, so anything you type
lands in the shipped MP4. Two things guard against that:
- Fields that declare themselves (
<input type="password">) are redacted automatically.
--private forces redaction on type, fill and keyboard, for fields that do not — API keys,
tokens, one-time codes, a password rendered as a plain text input.
Redacted actions still show a chip (••••••••) so the demo reads correctly; only the value is
dropped, and it is never written to .screencast/<name>/events.jsonl in the first place. The flag is
consumed by the wrapper and never reaches agent-browser. If the type of a field cannot be read, the
value is redacted anyway and a warning is logged — the wrapper does not guess when a leak is the cost
of guessing wrong.
What it cannot do: the recording is a video of the page, so --private only keeps a value out of
the event log and the keystroke overlay. It cannot un-draw what the browser itself renders. A
password field shows dots and is therefore safe; a token typed into a normal text input is on screen
in the video regardless of the flag. For those, either use a field the browser masks, or do not type
the real value on camera.
To type a literal --private, put it after --: screencast type @e1 -- --private.
Redaction covers the recording, not your machine: the expanded value is still an argument to the
wrapper and to agent-browser, so it is visible in the process list while the command runs. Passing
it as "$DEMO_PASSWORD" keeps the secret itself out of shell history (the history holds the variable
name), which is why the examples above do that — prefer an environment variable or a credential store
over pasting the literal.
Highlights & annotations
Both hold the element on screen for their duration automatically (they issue a matching wait), so the
recording actually contains the moment you are pointing at.
# Spotlight: dim everything except the element, glowing outline, zoom in (default 2.5s)
screencast highlight @e9
screencast highlight @e9 3 --mode ring # just a glowing outline, no dimming
screencast highlight @e9 --no-zoom # keep the current framing
# Note: a labelled callout with a connector pointing at the element (default 3s)
screencast note "Pick your plan here" @e12
screencast note "This updates live" --at 640,360 4 # anchor at a viewport point
screencast note "Read this first" @e5 --side top --zoom
--mode spotlight|ring, --no-zoom, --side auto|top|bottom|left|right, --zoom (needs an element
target — a point anchored with --at has no box to frame), --at x,y
(viewport CSS pixels). Highlights and notes are frame-local: anchor a fresh one after the page scrolls or
navigates rather than expecting one to track across a reflow.
Tips for good-looking demos
- Leave ~1 second between actions (e.g.
screencast wait 900) so the cursor travel and zoom have
time to animate. Back-to-back actions look rushed.
- Leave ~3 seconds after an action whose result matters — that is what buys the pull-back to 1x
where the viewer actually sees the row move, the count change, the panel appear. With the default
timings the next action has to be at least 2.85 s later (
zoomHold + zoomOutTime + zoomInTime
zoomRestMin) or the camera stays pushed in and the result happens off-frame.
- One
chapter per logical step. The title appears as a lower-third and as an MP4 chapter.
- Long pauses are fine — idle trimming removes them. You don't have to rush.
- Scroll the subject into view before recording if the page opens with a tall hero or upload
zone above it. The camera only frames what the viewport holds, so a demo of a table two viewport heights
below the fold otherwise spends its whole runtime showing an empty dropzone.
- Auth:
record start opens a fresh context and may or may not carry the existing session. Check
it (screencast get url, or grep the page for a logged-in marker) right after start; if it
landed logged out, sign in inside the recording with --private on the password field. Idle
trimming will not remove the login: typing is activity, so it is kept. Plan for it to be in the
video, or re-record with a session that is already signed in. Do not assume a login done before
start survives.
Troubleshooting
- Clicks report
✓ Done but nothing happens. agent-browser reports the dispatch, not the
effect, so a click on a stale ref still exits 0. Refs go stale whenever the page re-renders, which
includes the wait that highlight and note issue. Re-snapshot and click again with the fresh
ref. Verify state rather than trusting the exit code: screencast get text ..., or an eval that
reads the property the click was supposed to change. If several clicks in a row no-op, the take is
not recoverable — stop, discard it, and start over.
- Overlays drift out of sync with the video. The render logged
no flash found (wall-clock fallback): the sync flash was missed, usually because the recorder started on about:blank
(navigation raced start), so events are aligned on wall-clock instead. It renders, but timing is
approximate. Discard the take and restart with the page already open.
- Two renders of the same take at once. Not supported: they share the extracted-frame directory
and would delete frames from under each other. Render takes one at a time.
- Framing is wrong but the actions were fine. Do not re-record. Framing, zoom, trimming and all
cosmetics are render-time: edit
CONFIG in lib/util.mjs and re-run screencast render <name>.
captureScale and viewport are the exceptions — both shape the capture itself, so changing
either needs a fresh recording.
How it works (internals)
- Capture —
agent-browser record start streams the page to raw.webm (CDP screencast, ~10 fps, no
cursor). The viewport is set with a deviceScaleFactor (CONFIG.captureScale, default 2), so a
1280×720 layout is captured at 2560×1440; the compositor detects that ratio and supersamples, keeping
text crisp after framing and zoom instead of upscaling a 720p source. Each wrapped action is
timestamped; click/type targets are resolved to coordinates via agent-browser get box. A one-shot
full-viewport colour flash is injected at start to align the event log with the video timeline.
- Timeline (
lib/timeline.mjs) — events are converted to video-time (anchored on the detected flash
frame), then turned into zoom/pan keyframes, a cursor path, ripples, key chips, chapters, and an
idle-trim remap between source-time and output-time. A focus point normally gets a matching rest
keyframe back at 1x (zoomHold after the action, or the end of the effect, whichever is later),
which is what produces the punch-in/pull-back rhythm — a keyframe holds until the next one, so
focus points alone would park the camera at high zoom for the entire take. The rest is dropped
when the next action leaves no room for it (zoomRestMin) or when it would land past the end of
the recording, which is what keeps rapid sequences from strobing. Rests carry their own
keep-intervals so the trimmer cannot cut the beat that shows the result.
- Composite (
lib/render.mjs + lib/draw.mjs) — frames are extracted with ffmpeg; every output
frame is drawn in @napi-rs/canvas (background, zoomed/rounded/shadowed card, cursor, trail, ripple,
keystroke chip, chapter) and streamed as raw RGBA to ffmpeg, which encodes H.264 and muxes the
chapters. All text is drawn in canvas (the local ffmpeg has no drawtext).
Tuning
All cosmetics live in CONFIG at the top of lib/util.mjs: captureScale (retina capture factor — drop
to 1 only if a headless browser renders black at 2×), output size and fps, gradient colours, zoom,
transition timing, cursor size/trail, ripple, chip and chapter durations, and the idle-trim thresholds.
Edit and re-run screencast render <name> to preview without re-recording (styling, trim and framing are
all render-time; only captureScale and viewport need a fresh recording).
The zoom knobs are the ones worth knowing:
| Key |
Default |
Effect |
zoomTargetFrac |
0.34 |
how much of the frame the focused element fills — lower = wider, more context |
zoomMax |
1.7 |
ceiling. Keep it modest; past ~1.8 a demo shows a control, not a product |
zoomMin |
1.0 |
1.0 lets an already-large element get no zoom at all |
zoomHold |
0.85 |
seconds held in close after the action before pulling back to 1x (a highlight/note holds until its effect ends instead, if that is later) |
zoomRestMin |
0.55 |
minimum calm at 1x for a pull-back to be worth it; under this the camera stays in, which is what stops rapid clicks from strobing |
To make the camera calmer still, lower zoomTargetFrac and zoomMax together. To make it hold on
each action longer before pulling back, raise zoomHold.
Environment overrides: SCREENCAST_FPS=60 (silkier overlay motion, ~2× render time),
SCREENCAST_SESSION (dedicated agent-browser session), SCREENCAST_KEEP=1 (keep extracted source frames
for debugging), SCREENCAST_AB_BIN (path to the agent-browser binary).
1---2name: screencast3description: Turn an agent-browser session into a polished product-demo video — fully local, free, no cloud upload. Records the browser to WebM while logging every action, then composites an MP4 with automatic zoom-to-click, a smooth animated cursor with trail, click ripples, a keystroke overlay (keycast), idle trimming, chapters (burned-in lower-thirds plus embedded MP4 chapters), and a gradient background with a rounded, shadowed browser card. Use this whenever the user wants to record or generate a demo video, screencast, walkthrough, "how it works" clip, or animated screen recording of a web flow — especially one that looks like Screen Studio (auto-zoom, keystrokes, chapters) but produced by the agent itself. Trigger on "record a demo", "make a screencast", "generate a walkthrough video", "demo video with zooms and chapters", "record the signup/onboarding flow". Built on the agent-browser CLI; requires ffmpeg and Node. Not for live screen capture of the whole desktop — it records a browser flow the agent drives.4---56# screencast78Produce a polished product-demo video from a browser flow that the agent drives with `agent-browser`.9The agent performs the demo (clicks, typing, navigation); this skill records it and post-processes the10raw capture into a cinematic MP4 — locally, with no paid tool and no upload.1112## What it renders1314- **Punch-in / pull-back zoom + pan** — the camera zooms to an element only while something happens15 there, then pulls straight back to 1x so the whole app, and what the action just changed, stays16 visible between beats. Element-fit sizing (small controls get a closer zoom, wide ones barely any),17 pan clamped so the page always fills the card. Back-to-back actions keep the zoom instead of18 strobing once per click19- **Animated cursor** with a subtle trail and a click "pop" (synthesized — the raw capture has no cursor)20- **Click ripples** on every click21- **Keystroke overlay** (keycast) showing typed text and pressed keys22- **Element highlight** — spotlight (dim everything else) or a glowing ring around an element23- **Annotations** — labelled callouts with a connector pointing at an element or point24- **Idle trimming** — dead time freezes then hard-cuts, never fast-forwards25- **Chapters** — on-screen lower-thirds *and* real MP4 chapter markers26- **Framing** — gradient + accent glows, rounded corners, layered shadow, padding, intro/outro fades2728## Requirements2930- `agent-browser` on PATH with its browser binaries installed (`agent-browser install`, plus31 `--with-deps` on Linux). The browser is driven and recorded through it.32- `ffmpeg` + `ffprobe` on PATH33- Node 24+ — the renderer itself only needs 18+, but current `agent-browser` declares34 `engines.node >= 24`, so that is the real floor. It auto-installs `@napi-rs/canvas` (a prebuilt35 binary, no compilation) into the skill directory on first run, at the exact version `package.json`36 pins, so the renderer never moves onto an untested build behind your back. To upgrade it, change37 that version and delete `node_modules/`.3839Rendering is entirely local. The *capture* is whatever `agent-browser` is configured to drive, so40"no cloud" holds for a local browser; a remote/cloud provider would host the page itself elsewhere.4142## How to use it4344Invoke the wrapper instead of `agent-browser` for the actions you want in the video. It is a thin45passthrough: anything it does not handle itself runs unchanged. Three caveats worth knowing —46`start`, `stop`, `render`, `chapter`, `highlight` and `note` are the wrapper's own commands (and47`highlight` shadows the upstream one of that name); only the actions listed under *Commands* below48produce timeline events; and `--private` is consumed before the call rather than forwarded.4950Run it as `node <SKILL_DIR>/bin/screencast.mjs …` (or symlink that file onto your PATH as `screencast`).5152```bash53# 1. Start: sets a fixed 1280x720 viewport, starts recording, drops a sync flash.54screencast start demo https://app.example.com/login5556# 2. Explore normally (not logged as an action):57screencast snapshot -i5859# 3. Drive the demo. Mark chapters as you go; run actions through the wrapper:60screencast chapter "Sign in"61screencast type @e3 "ada@example.com"62screencast type @e4 --private "$DEMO_PASSWORD" # kept out of the log and the keycast63screencast press Enter6465screencast chapter "Create a project"66screencast click @e1267screencast type @e15 "My first project"68screencast click @e186970# 4. Stop → renders ./demo.mp471screencast stop72```7374Output: `./<name>.mp4` in the current directory (1920×1080, 30 fps, H.264, with chapters).7576### Commands7778| Command | Purpose |79|---|---|80| `screencast start <name> [url]` | Set viewport, start recording, inject the sync flash |81| `screencast <agent-browser cmd…>` | Run any agent-browser command; actions are logged |82| `screencast chapter "<title>"` | Mark a chapter at the current moment |83| `screencast highlight @ref [dur]` | Spotlight (or ring) an element and zoom to it |84| `screencast note "<text>" @ref [dur]` | Point a labelled callout at an element or point |85| `screencast stop` | Stop recording and render `<name>.mp4` |86| `screencast render [name]` | Re-render from an existing take (fast iteration on styling) |8788Logged actions: `click`, `dblclick`, `type`, `fill`, `press`, `keyboard`, `hover`, `check`, `uncheck`,89`select`. Everything else (`snapshot`, `get`, `wait`, `screenshot`, `open`, …) passes straight through90without adding an event.9192### Secrets9394Typed text is written to the event log *and* drawn into the keystroke overlay, so anything you type95lands in the shipped MP4. Two things guard against that:9697- Fields that declare themselves (`<input type="password">`) are redacted automatically.98- `--private` forces redaction on `type`, `fill` and `keyboard`, for fields that do not — API keys,99 tokens, one-time codes, a password rendered as a plain text input.100101Redacted actions still show a chip (`••••••••`) so the demo reads correctly; only the value is102dropped, and it is never written to `.screencast/<name>/events.jsonl` in the first place. The flag is103consumed by the wrapper and never reaches `agent-browser`. If the type of a field cannot be read, the104value is redacted anyway and a warning is logged — the wrapper does not guess when a leak is the cost105of guessing wrong.106107**What it cannot do:** the recording is a video of the page, so `--private` only keeps a value out of108the event log and the keystroke overlay. It cannot un-draw what the browser itself renders. A109password field shows dots and is therefore safe; a token typed into a normal text input is on screen110in the video regardless of the flag. For those, either use a field the browser masks, or do not type111the real value on camera.112113To type a literal `--private`, put it after `--`: `screencast type @e1 -- --private`.114115Redaction covers the recording, not your machine: the expanded value is still an argument to the116wrapper and to `agent-browser`, so it is visible in the process list while the command runs. Passing117it as `"$DEMO_PASSWORD"` keeps the secret itself out of shell history (the history holds the variable118name), which is why the examples above do that — prefer an environment variable or a credential store119over pasting the literal.120121### Highlights & annotations122123Both hold the element on screen for their duration automatically (they issue a matching `wait`), so the124recording actually contains the moment you are pointing at.125126```bash127# Spotlight: dim everything except the element, glowing outline, zoom in (default 2.5s)128screencast highlight @e9129screencast highlight @e9 3 --mode ring # just a glowing outline, no dimming130screencast highlight @e9 --no-zoom # keep the current framing131132# Note: a labelled callout with a connector pointing at the element (default 3s)133screencast note "Pick your plan here" @e12134screencast note "This updates live" --at 640,360 4 # anchor at a viewport point135screencast note "Read this first" @e5 --side top --zoom136```137138`--mode spotlight|ring`, `--no-zoom`, `--side auto|top|bottom|left|right`, `--zoom` (needs an element139target — a point anchored with `--at` has no box to frame), `--at x,y`140(viewport CSS pixels). Highlights and notes are frame-local: anchor a fresh one after the page scrolls or141navigates rather than expecting one to track across a reflow.142143## Tips for good-looking demos144145- **Leave ~1 second between actions** (e.g. `screencast wait 900`) so the cursor travel and zoom have146 time to animate. Back-to-back actions look rushed.147- **Leave ~3 seconds after an action whose result matters** — that is what buys the pull-back to 1x148 where the viewer actually sees the row move, the count change, the panel appear. With the default149 timings the next action has to be at least 2.85 s later (`zoomHold` + `zoomOutTime` + `zoomInTime`150 + `zoomRestMin`) or the camera stays pushed in and the result happens off-frame.151- **One `chapter` per logical step.** The title appears as a lower-third and as an MP4 chapter.152- Long pauses are fine — idle trimming removes them. You don't have to rush.153- **Scroll the subject into view before recording** if the page opens with a tall hero or upload154 zone above it. The camera only frames what the viewport holds, so a demo of a table two viewport heights155 below the fold otherwise spends its whole runtime showing an empty dropzone.156- Auth: `record start` opens a fresh context and *may or may not* carry the existing session. Check157 it (`screencast get url`, or grep the page for a logged-in marker) right after `start`; if it158 landed logged out, sign in inside the recording with `--private` on the password field. Idle159 trimming will *not* remove the login: typing is activity, so it is kept. Plan for it to be in the160 video, or re-record with a session that is already signed in. Do not assume a login done before161 `start` survives.162163## Troubleshooting164165- **Clicks report `✓ Done` but nothing happens.** `agent-browser` reports the dispatch, not the166 effect, so a click on a stale ref still exits 0. Refs go stale whenever the page re-renders, which167 includes the `wait` that `highlight` and `note` issue. Re-snapshot and click again with the fresh168 ref. Verify state rather than trusting the exit code: `screencast get text ...`, or an `eval` that169 reads the property the click was supposed to change. If several clicks in a row no-op, the take is170 not recoverable — stop, discard it, and start over.171- **Overlays drift out of sync with the video.** The render logged `no flash found (wall-clock172 fallback)`: the sync flash was missed, usually because the recorder started on `about:blank`173 (navigation raced `start`), so events are aligned on wall-clock instead. It renders, but timing is174 approximate. Discard the take and restart with the page already open.175- **Two renders of the same take at once.** Not supported: they share the extracted-frame directory176 and would delete frames from under each other. Render takes one at a time.177- **Framing is wrong but the actions were fine.** Do not re-record. Framing, zoom, trimming and all178 cosmetics are render-time: edit `CONFIG` in `lib/util.mjs` and re-run `screencast render <name>`.179 `captureScale` and `viewport` are the exceptions — both shape the capture itself, so changing180 either needs a fresh recording.181182## How it works (internals)1831841. **Capture** — `agent-browser record start` streams the page to `raw.webm` (CDP screencast, ~10 fps, no185 cursor). The viewport is set with a `deviceScaleFactor` (`CONFIG.captureScale`, default 2), so a186 1280×720 layout is captured at 2560×1440; the compositor detects that ratio and supersamples, keeping187 text crisp after framing and zoom instead of upscaling a 720p source. Each wrapped action is188 timestamped; click/type targets are resolved to coordinates via `agent-browser get box`. A one-shot189 full-viewport colour flash is injected at start to align the event log with the video timeline.1902. **Timeline** (`lib/timeline.mjs`) — events are converted to video-time (anchored on the detected flash191 frame), then turned into zoom/pan keyframes, a cursor path, ripples, key chips, chapters, and an192 idle-trim remap between source-time and output-time. A focus point normally gets a matching rest193 keyframe back at 1x (`zoomHold` after the action, or the end of the effect, whichever is later),194 which is what produces the punch-in/pull-back rhythm — a keyframe holds until the next one, so195 focus points alone would park the camera at high zoom for the entire take. The rest is dropped196 when the next action leaves no room for it (`zoomRestMin`) or when it would land past the end of197 the recording, which is what keeps rapid sequences from strobing. Rests carry their own198 keep-intervals so the trimmer cannot cut the beat that shows the result.1993. **Composite** (`lib/render.mjs` + `lib/draw.mjs`) — frames are extracted with ffmpeg; every output200 frame is drawn in `@napi-rs/canvas` (background, zoomed/rounded/shadowed card, cursor, trail, ripple,201 keystroke chip, chapter) and streamed as raw RGBA to ffmpeg, which encodes H.264 and muxes the202 chapters. All text is drawn in canvas (the local ffmpeg has no `drawtext`).203204## Tuning205206All cosmetics live in `CONFIG` at the top of `lib/util.mjs`: `captureScale` (retina capture factor — drop207to 1 only if a headless browser renders black at 2×), output size and fps, gradient colours, zoom,208transition timing, cursor size/trail, ripple, chip and chapter durations, and the idle-trim thresholds.209Edit and re-run `screencast render <name>` to preview without re-recording (styling, trim and framing are210all render-time; only `captureScale` and `viewport` need a fresh recording).211212The zoom knobs are the ones worth knowing:213214| Key | Default | Effect |215|---|---|---|216| `zoomTargetFrac` | `0.34` | how much of the frame the focused element fills — **lower = wider, more context** |217| `zoomMax` | `1.7` | ceiling. Keep it modest; past ~1.8 a demo shows a control, not a product |218| `zoomMin` | `1.0` | `1.0` lets an already-large element get no zoom at all |219| `zoomHold` | `0.85` | seconds held in close after the action before pulling back to 1x (a `highlight`/`note` holds until its effect ends instead, if that is later) |220| `zoomRestMin` | `0.55` | minimum calm at 1x for a pull-back to be worth it; under this the camera stays in, which is what stops rapid clicks from strobing |221222To make the camera calmer still, lower `zoomTargetFrac` and `zoomMax` together. To make it hold on223each action longer before pulling back, raise `zoomHold`.224225Environment overrides: `SCREENCAST_FPS=60` (silkier overlay motion, ~2× render time),226`SCREENCAST_SESSION` (dedicated agent-browser session), `SCREENCAST_KEEP=1` (keep extracted source frames227for debugging), `SCREENCAST_AB_BIN` (path to the agent-browser binary).