pup-bridge-macos
Public Made by Adomby adom
The pup for macOS SKILL PACK: the AI skills that teach a cloud/container AI how to drive the macOS Puppeteer (pup) bridge (pup_* verbs): the everyday `pup` skill plus focused sub-skills for windows/sessions/tabs, screenshots+recording, credentials, lanes, settings, browsers+Chrome install, vendor logins and the Adom wiki. Installs into ~/.claude/skills + ~/.codex/skills. DOCS ONLY: this is NOT the bridge runtime. The bridge itself is one signed macOS binary that Adom Bridge for macOS installs an
name: pup-screenshots-recording user-invocable: true description: "Capturing pup (Puppeteer bridge) windows: SCREENSHOTS (pup_screenshot — lossless PNG, full-page vs viewport, full-resolution via pup_screenshot_full_res, and how to actually READ the image) and RECORDING (pup_record_start/stop records ONE pup window through ab's native recorder, MP4, extension lane included; desktop_record_start/stop records the WHOLE desktop across apps). Covers framing a window at an exact size for a clean shot WITHOUT foregrounding it, and why pup shots are true-to-life (real rendered window, not headless). Read when screenshotting a page, capturing full-page vs above-the-fold, recording a demo/walkthrough of an app, or choosing the window-recorder vs the desktop-recorder. Trigger words: pup_screenshot, pup_screenshot_full_res, full page screenshot, screenshot the page, read the screenshot, pup_record_start, pup_record_stop, desktop_record_start, desktop_record_stop, record my app, record a demo, screen recording, walkthrough video, pup screenshot, pup recording."
Parent skill: pup-bridge · See also: pup (start here), pup-windows-sessions-tabs, pup-browsers-and-chrome.
pup — screenshots & recording
pup windows are real, GPU-rendered windows (not headless), so screenshots and recordings are
true-to-life — fonts, canvas, WebGL, video, and CSS all render exactly as a user would see them. And it
all works while the window is in the background (see pup-windows-sessions-tabs), so you capture
without disturbing the user.
Screenshots
adom-bridge-cli pup_screenshot '{"sessionId":"mytask-web"}' # viewport, lossless PNG
adom-bridge-cli pup_screenshot '{"sessionId":"mytask-web","fullPage":true}' # entire scrollable page
adom-bridge-cli pup_screenshot '{"sessionId":"mytask-web","tabId":"tab-2"}' # a specific tab
- Returns a PNG saved on the container (the machine running the CLI) — the response gives you the path.
Readthat path to actually see it; a returned path is not the same as having looked. Downscaled to stay under the image-read limit by default. fullPage:truestitches the whole scrollable page; omit for just the viewport (above the fold).pup_screenshot_full_rescaptures at full native resolution (no downscale) when you need to read fine detail — larger file; use when the default shrink loses text you need.- Framing for a clean shot without showing the user: size/position the window on open —
pup_open_window {width,height,x,y}— which places it without foregrounding (background stays background). Great for a deterministic, repeatable capture frame. To capture the OS window plus any owned popups/dialogs on top, use AD'sdesktop_screenshot_windowinstead (it returns the window with its child popups). - Sanity-check, don't assume: a page that 404'd or hit a login wall still screenshots — pair with
pup_evalondocument.titlewhen in doubt. - Rendering your OWN HTML to screenshot it? Use
pup_render_html {sessionId, html}— it renders the string viapage.setContent, which is UTF-8-correct. (Adata:text/html,…URL mojibakes accented characters —Café→Café— unless you hand-add;charset=utf-8;render_htmljust does it right.) Then screenshot/drive it like any page. Great for previewing generated markup without writing a temp file.
Recording: one recorder, ab's, and pup's verbs ride it
Desktop recording belongs to ab, not pup (John, 2026-09-07), and since 2.0.582 so does window
recording. pup_record_start {window} and pup_record_stop {window} delegate to ab's native
recorder (desktop_record_window_start / _stop keyed on the pup window's handle) when this Mac's
Adom Bridge offers it; otherwise pup records the page itself, which needs ffmpeg (brew install ffmpeg,
re-detected without a restart). It works on the extension lane. Frames are emitted only when the window's content
changes, so a static page yields few frames and a small file; that is by design, not a dropped take.
The response carries filePath, bytes, frames and recordedMs; pull the file with
adom-bridge pull_file {filePaths:[path]}.
| Ask | Verb | Captures |
|---|---|---|
| "record my app", a UI walkthrough | pup_record_start / pup_record_stop |
ONE pup window, whole window frame, MP4, no audio |
| "record my desktop / the demo with sound" | ab desktop_record_start / desktop_record_stop (ab 2.1.36+) |
a whole monitor plus the system audio mix, MP4 |
The old Chrome screen-share recorder (pup_screen_record_* through the pup_recorder_open HUD) and the
DevTools screencast are retired to fallbacks pup uses only where ab's recorder is absent; do not offer
them. pup_record_status and pup_record_list report through ab too.
Say it the way the user will understand: "ab records your desktop natively (GPU accelerated); pup can do it Meet-style if that is not available." Never present desktop recording as pup's own feature.
# Recording a pup window the user will watch → DO bring it forward first
adom-bridge-cli pup_raise_os_window '{"sessionId":"demo-web"}'
adom-bridge-cli pup_record_start '{"sessionId":"demo-web"}'
# ...drive the scene: navigate, click, type, scroll...
adom-bridge-cli pup_record_stop '{"sessionId":"demo-web"}' # → { path: "...webm" }
adom-bridge-cli pup_lower_os_window '{"sessionId":"demo-web"}' # tuck it away again when done
pup_record_stopreturns the saved video path. Status and list verbs:pup_record_status/pup_record_list(the tab/window recorder),pup_screen_record_status/pup_screen_record_list(pup's Meet-style desktop fallback),desktop_record_status/desktop_record_list(ab's native recorder; see theadom-bridge-recordingskill). On an ab older than 2.1.36 thedesktop_record_*names were routed to pup's fallback; pup now answers those withwrong_bridgeand the fallback names.
Driving & verifying tricky pages (shadow DOM, nested scroll) — use the first-class verbs
Component-heavy pages (an app, a wiki widget) break the naive path: document.querySelector misses shadow
content, window.scrollTo no-ops when the page scrolls inside a nested <div>, fullPage returns only the
viewport, and a sized window can even come up ~170px wide. As of v1.8.21 pup has first-class verbs for
all of it — reach for these instead of hand-rolling evals:
pup_screenshot_element { sessionId, selector? | text?, closest?, padding?, deep? }— screenshot ONE element. Resolve by CSS selector (most reliable) or visible text (pierces shadow DOM), scrolled into view first. This is usually the whole answer to "capture just this card." Text matches the tightest text node — to grab the enclosing card addclosest:".card"; and if the phrase is split across spans or appears in prose too, use a selector (e.g..download-card).pup_scroll { sessionId, dy?/dx? | toSelector?/toText?, block?, behavior? }— scrolls the ACTUAL scrolling element (nearest scrollable ancestor, notwindow). Scroll by delta or to an element/text (shadow-piercing). Returns{ scrollElement, scrollTop, scrollHeight, clientHeight, atBottom }so you can confirm it landed — no more overshoot from hand-computed offsets.pup_set_viewport { sessionId, width, height, deviceScaleFactor? }— pins the viewport deterministically (fixes a too-small innerWidth).pup_open_window { width, height }now applies it on open, so a sized window renders at the size you asked for.$deep(sel)/$$deep(sel)/deepText(txt)are auto-injected into every page, sopup_evalcan pierce shadow roots directly — no hand-rolled walk:
adom-bridge-cli pup_eval '{"sessionId":"...","expr":"$$deep(\"a[href]\").length"}' # counts across shadow roots
Still verify by DOM, then by eye. To count cards/links/rows, count in pup_eval by document-Y
(rect.top + window.scrollY) — immune to an overshot screenshot — then pup_screenshot_element (or
pup_scroll + pup_screenshot) to confirm. And first check whether a page is even shadow DOM
before assuming: a shadow survey ($$deep finding more than querySelectorAll) tells you; often an "empty"
result just means a wrong selector on plain DOM.
fullPage caveat: pup_screenshot { fullPage:true } only covers document-level scroll. On a
nested-scroll SPA it returns just the viewport and a nestedScroll:true hint — use pup_screenshot_element
for one region, or pup_scroll through the container.
Reminder: since v2.0.274 pup_eval accepts statement lists (semicolons fine — a syntax error in
the expression form retries with devtools-console semantics, and bad code fails honestly with
eval_failed); a comma-operator/IIFE still gives the cleanest single-expression result. If a
screenshot lands on the wrong content, don't trust it — it almost always means an inner scroll container;
re-scroll the element, don't nudge the window.
---
name: pup-screenshots-recording
user-invocable: true
description: "Capturing pup (Puppeteer bridge) windows: SCREENSHOTS (pup_screenshot — lossless PNG, full-page vs viewport, full-resolution via pup_screenshot_full_res, and how to actually READ the image) and RECORDING (pup_record_start/stop records ONE pup window through ab's native recorder, MP4, extension lane included; desktop_record_start/stop records the WHOLE desktop across apps). Covers framing a window at an exact size for a clean shot WITHOUT foregrounding it, and why pup shots are true-to-life (real rendered window, not headless). Read when screenshotting a page, capturing full-page vs above-the-fold, recording a demo/walkthrough of an app, or choosing the window-recorder vs the desktop-recorder. Trigger words: pup_screenshot, pup_screenshot_full_res, full page screenshot, screenshot the page, read the screenshot, pup_record_start, pup_record_stop, desktop_record_start, desktop_record_stop, record my app, record a demo, screen recording, walkthrough video, pup screenshot, pup recording."
---
**Parent skill:** pup-bridge · See also: `pup` (start here), `pup-windows-sessions-tabs`, `pup-browsers-and-chrome`.
# pup — screenshots & recording
pup windows are **real, GPU-rendered windows** (not headless), so screenshots and recordings are
true-to-life — fonts, canvas, WebGL, video, and CSS all render exactly as a user would see them. And it
all works while the window is in the **background** (see `pup-windows-sessions-tabs`), so you capture
without disturbing the user.
## Screenshots
```bash
adom-bridge-cli pup_screenshot '{"sessionId":"mytask-web"}' # viewport, lossless PNG
adom-bridge-cli pup_screenshot '{"sessionId":"mytask-web","fullPage":true}' # entire scrollable page
adom-bridge-cli pup_screenshot '{"sessionId":"mytask-web","tabId":"tab-2"}' # a specific tab
```
- Returns a **PNG saved on the container** (the machine running the CLI) — the response gives you the path.
**`Read` that path** to actually see it; a returned path is not the same as having looked. Downscaled to
stay under the image-read limit by default.
- **`fullPage:true`** stitches the whole scrollable page; omit for just the viewport (above the fold).
- **`pup_screenshot_full_res`** captures at full native resolution (no downscale) when you need to read
fine detail — larger file; use when the default shrink loses text you need.
- **Framing for a clean shot without showing the user:** size/position the window on open —
`pup_open_window {width,height,x,y}` — which places it **without foregrounding** (background stays
background). Great for a deterministic, repeatable capture frame. To capture the OS window *plus* any
owned popups/dialogs on top, use AD's `desktop_screenshot_window` instead (it returns the window with its
child popups).
- **Sanity-check, don't assume:** a page that 404'd or hit a login wall still screenshots — pair with
`pup_eval` on `document.title` when in doubt.
- **Rendering your OWN HTML to screenshot it?** Use **`pup_render_html {sessionId, html}`** — it renders
the string via `page.setContent`, which is **UTF-8-correct**. (A `data:text/html,…` URL mojibakes accented
characters — `Café` → `Café` — unless you hand-add `;charset=utf-8`; `render_html` just does it right.)
Then screenshot/drive it like any page. Great for previewing generated markup without writing a temp file.
## Recording: one recorder, ab's, and pup's verbs ride it
**Desktop recording belongs to ab, not pup (John, 2026-09-07), and since 2.0.582 so does window
recording.** `pup_record_start {window}` and `pup_record_stop {window}` delegate to ab's native
recorder (`desktop_record_window_start` / `_stop` keyed on the pup window's handle) when this Mac's
Adom Bridge offers it; otherwise pup records the page itself, which needs ffmpeg (`brew install ffmpeg`,
re-detected without a restart). It works on the extension lane. Frames are emitted only when the window's content
changes, so a static page yields few frames and a small file; that is by design, not a dropped take.
The response carries `filePath`, `bytes`, `frames` and `recordedMs`; pull the file with
`adom-bridge pull_file {filePaths:[path]}`.
| Ask | Verb | Captures |
|---|---|---|
| "record my app", a UI walkthrough | `pup_record_start` / `pup_record_stop` | ONE pup window, whole window frame, MP4, no audio |
| "record my desktop / the demo with sound" | ab `desktop_record_start` / `desktop_record_stop` (ab 2.1.36+) | a whole monitor plus the system audio mix, MP4 |
The old Chrome screen-share recorder (`pup_screen_record_*` through the `pup_recorder_open` HUD) and the
DevTools screencast are retired to fallbacks pup uses only where ab's recorder is absent; do not offer
them. `pup_record_status` and `pup_record_list` report through ab too.
Say it the way the user will understand: "ab records your desktop natively (GPU accelerated); pup can
do it Meet-style if that is not available." Never present desktop recording as pup's own feature.
```bash
# Recording a pup window the user will watch → DO bring it forward first
adom-bridge-cli pup_raise_os_window '{"sessionId":"demo-web"}'
adom-bridge-cli pup_record_start '{"sessionId":"demo-web"}'
# ...drive the scene: navigate, click, type, scroll...
adom-bridge-cli pup_record_stop '{"sessionId":"demo-web"}' # → { path: "...webm" }
adom-bridge-cli pup_lower_os_window '{"sessionId":"demo-web"}' # tuck it away again when done
```
- `pup_record_stop` returns the saved video **path**. Status and list verbs: `pup_record_status` /
`pup_record_list` (the tab/window recorder), `pup_screen_record_status` / `pup_screen_record_list`
(pup's Meet-style desktop fallback), `desktop_record_status` / `desktop_record_list` (ab's native
recorder; see the `adom-bridge-recording` skill). On an ab older than 2.1.36 the `desktop_record_*`
names were routed to pup's fallback; pup now answers those with `wrong_bridge` and the fallback names.
## Driving & verifying tricky pages (shadow DOM, nested scroll) — use the first-class verbs
Component-heavy pages (an app, a wiki widget) break the naive path: `document.querySelector` misses shadow
content, `window.scrollTo` no-ops when the page scrolls inside a nested `<div>`, `fullPage` returns only the
viewport, and a sized window can even come up ~170px wide. As of **v1.8.21 pup has first-class verbs for
all of it** — reach for these instead of hand-rolling evals:
- **`pup_screenshot_element { sessionId, selector? | text?, closest?, padding?, deep? }`** — screenshot
ONE element. Resolve by CSS **selector** (most reliable) or visible **text** (pierces shadow DOM), scrolled
into view first. **This is usually the whole answer to "capture just this card."** Text matches the
*tightest* text node — to grab the enclosing card add `closest:".card"`; and if the phrase is split across
spans or appears in prose too, use a **selector** (e.g. `.download-card`).
- **`pup_scroll { sessionId, dy?/dx? | toSelector?/toText?, block?, behavior? }`** — scrolls the ACTUAL
scrolling element (nearest scrollable ancestor, not `window`). Scroll by delta or to an element/text
(shadow-piercing). Returns `{ scrollElement, scrollTop, scrollHeight, clientHeight, atBottom }` so you can
confirm it landed — no more overshoot from hand-computed offsets.
- **`pup_set_viewport { sessionId, width, height, deviceScaleFactor? }`** — pins the viewport
deterministically (fixes a too-small innerWidth). `pup_open_window { width, height }` now applies it on
open, so a sized window renders at the size you asked for.
- **`$deep(sel)` / `$$deep(sel)` / `deepText(txt)`** are auto-injected into every page, so `pup_eval` can
pierce shadow roots directly — no hand-rolled walk:
```bash
adom-bridge-cli pup_eval '{"sessionId":"...","expr":"$$deep(\"a[href]\").length"}' # counts across shadow roots
```
**Still verify by DOM, then by eye.** To count cards/links/rows, count in `pup_eval` by document-Y
(`rect.top + window.scrollY`) — immune to an overshot screenshot — then `pup_screenshot_element` (or
`pup_scroll` + `pup_screenshot`) to confirm. And **first check whether a page is even shadow DOM**
before assuming: a shadow survey (`$$deep` finding more than `querySelectorAll`) tells you; often an "empty"
result just means a wrong selector on plain DOM.
**fullPage caveat:** `pup_screenshot { fullPage:true }` only covers *document*-level scroll. On a
nested-scroll SPA it returns just the viewport and a `nestedScroll:true` hint — use `pup_screenshot_element`
for one region, or `pup_scroll` through the container.
Reminder: since v2.0.274 `pup_eval` accepts statement lists (semicolons fine — a syntax error in
the expression form retries with devtools-console semantics, and bad code fails honestly with
`eval_failed`); a comma-operator/IIFE still gives the cleanest single-expression result. If a
screenshot lands on the wrong content, don't trust it — it almost always means an inner scroll container;
re-scroll the element, don't nudge the window.