# pup: the Puppeteer bridge

**pup is the AI's own browser: a real, full Chrome/Edge window on the user's desktop that the AI drives
with complete programmatic control, but a _sandbox_.** It runs in a fresh, isolated profile that is
never the user's real signed-in browser, and opens in the **background by default** so it never disrupts
them. That trade is exactly why pup exists alongside **nb / nbe** (the native browser and its extension):
nb/nbe drive the user's _real_ browser, their cookies, SSO, saved logins, their identity, for when a
task needs to BE the user. pup is the opposite trade: no borrowed identity, but **total control**, a
real, disposable browser the AI owns end to end (open/close windows + tabs, navigate, screenshot, eval
JS, record). Logged out by design, it's the safe, fully-drivable default for automation, testing,
scraping public pages, and showing wiki pages; reach for nb/nbe only when you genuinely need the user's
real logins. pup drives **Chrome for Testing**, the automation build (why: [docs/WHY-CFT.md](/blob/app/pup-bridge/docs/WHY-CFT.md));
pup installs and maintains it by itself, with the machine's installed Chrome/Edge as fallbacks.

**The core goal: an AI-driven browser that can do everything it needs to for you - the best assistant
in the world.** By default every pup window surfs on the shared durable **adom-you** profile, and every
login made there enriches a shared credential store. Over time that store lets pup automatically enter
usernames and passwords on every site it has ever seen, which is what makes a truly self-sufficient
AI browser possible. Isolated throwaway profiles exist for the cases that need them (logged-out
testing, acting as another user), but they build nothing: adom-you is the default on purpose.

**pup drives Chrome for Testing (CfT), the build Google ships for automation.** Branded Chrome
fights automation in ways that matter here: the "controlled by automated test software" banner
blips over the window during real driving and cannot be suppressed, and Chrome 137 removed every
silent way to load pup's toolbar extension. CfT has none of those fights, pins a deterministic
version, and lets the Adom Pup toolbar ride every launch as a flag. The full technical record,
including what we tried and verified on branded Chrome, is in
[docs/WHY-CFT.md](/blob/app/pup-bridge/docs/WHY-CFT.md).

> To drive the user's **real, signed-in** Chrome/Edge (their cookies, SSO, saved logins), that's the
> **Adom browser extension's `nbrowser_*` verbs**, a separate bridge, not pup.

Canonical page: **https://wiki.adom.inc/adom/pup-bridge**

## Install vs Download, which is which (and why the version numbers differ)

The page header shows **two** things, for two different places. Most people need only the first.

| On the page | What it is | Who runs it | You usually… |
|---|---|---|---|
| **Install**, `adom-wiki pkg install …` | The **AI skill pack**, the `pup` skills that teach a cloud/container AI how to drive the bridge. Docs only, no runtime; drops into `~/.claude/skills` + `~/.codex/skills`. | your **container / cloud AI** | **run this** (or it arrives via `sync_skills`) |
| **Download for your machine**, `adom-bridge-puppeteer-v<ver>.zip` | The **bridge runtime**, the actual Node program (browser detection + driving) that runs on the desktop. | **Bridge**, on the user's PC | **do nothing**, Bridge **auto-installs & auto-updates** it; the manual download is only for offline/manual installs |

**Why two different version numbers?** They're independent artifacts. The **Install (pkg)** version bumps
whenever the *skills/docs* change (often). The **Download (runtime)** version bumps only when the *bridge
code* changes (rarely). So it's normal to see e.g. Install `v1.8.27` and Download `v1.8.20`, nothing is
out of sync; they just version on their own clocks.

*(There's also a third copy, the runtime is **bundled inside the Bridge installer** as a first-run
seed, superseded by a newer cache copy via `updateManifestUrl`. You never touch it directly.)*

## Use it

```bash
# everyday: open + screenshot. Opens in the BACKGROUND automatically, no need to lower it,
# and it's fully drivable/screenshottable while hidden, so the user is never interrupted.
adom-bridge-cli pup_open_window '{"sessionId":"myapp-web","owner":"myapp","url":"http://localhost:3000"}'
adom-bridge-cli pup_screenshot  '{"sessionId":"myapp-web"}'

# readiness (use pup_readiness, NOT pup_status). On a box with Chrome or Edge this is
# instantly {ready:true} with no download. Only a box with no Chromium at all fetches Chrome for
# Testing in the background, poll until ready, then open.
adom-bridge-cli pup_readiness '{}'      # → {ready:true, browserKind:"chrome"|"edge"|...}
```

- **Task-prefix your `sessionId` + pass `owner`**, sessions are shared across every AI thread on the
  desktop; that lets pup protect your window from another thread navigating it away.
- **`foreground:true` is the only way to show a window**, sizing/positioning does not foreground it.

Full everyday usage (tabs, recording, window ownership, cold-start playbook) is in the `pup` skill
(`SKILL.md`).

## Window-type taskbar icons: read a pup window at a glance

![The pup window-type icon family](docs/icons/family-v1.png)

Every pup window's **taskbar icon states what kind of thing it is showing**. All five share the same
drawing (the teal Adom tile carrying a solid browser window) and differ only in the window's
*content*: a monochrome glyph on the 24x24 Adom house grid, per the brand icon law (single color,
no gradients, no emoji):

| Icon | Window content | Means |
|---|---|---|
| ![wiki](docs/icons/pup-cat-wiki.png) | open book | **Adom wiki, public view**: logged out, what the world sees |
| ![wiki signed in](docs/icons/pup-cat-wiki-in.png) | book + person | **Adom wiki, signed in**: the user's logged-in view (private source, drafts, owner cards) |
| ![adom app](docs/icons/pup-cat-app.png) | the Adom mark | **An Adom app**: localhost or a cloud-slug proxy URL |
| ![web](docs/icons/pup-cat-web.png) | globe | **The web**: any other site (ti.com, digikey, ...) |
| ![mixed](docs/icons/pup-cat-mixed.png) | two panes | **Mixed**: the window's tabs span more than one category |

- The category is derived from **all tabs** in the window; it updates **live** (add a digikey tab
  to a wiki window and the icon flips to *mixed* within about a second).
- The **overlay badge** riding the icon's corner is separate and sacred: it is the **active tab's
  own favicon**, so you always see which site you are looking at. In grouped-taskbar mode it shows
  window/tab counters instead.
- The **tab title** carries the same login signal in text: a solid dot (`●   Logged in`) or hollow dot
  (`○   Public`) leads the title of every Adom wiki tab.
- Under the hood, each window's identity (icon, name, Alt-Tab entry) rides a per-session Windows
  AppUserModelID that **carries the category**: the only way Win11 allows a taskbar tile to change
  live. Grouped mode instead keeps one stable, pinnable `Adom.Pup` identity.

### Using pup's icons in your own tool (HD browser picker, launchers, menus)

If you are building a surface that offers "open a pup window" (a browser picker, a launcher tile, a
menu item), **use pup's real icon, not a stand-in.** The canonical, self-describing asset set lives
in the wiki repo at [`assets/pup-icons/`](assets/pup-icons/), in three formats:

- **SVG** (`pup-cat-*.svg`), scalable, the right choice for a web/Tauri picker.
- **PNG** (`pup-cat-*-{32,48,128,256,512}.png`), raster fallback / retina.
- **ICO** (`pup-cat-*.ico`), Windows multi-size, for taskbar/shortcut contexts (256px frame is
  PNG-compressed, per the Vista+ rule, so it never renders as a generic document).

**For a launcher/picker item, use `pup-cat-default`**, the neutral teal browser-window tile. It is
what a pup window's taskbar button looks like before its content is known, so the picker previews the
real result. The five category icons above (`wiki`, `wiki-in`, `app`, `web`, `mixed`) are for
*showing an existing window's kind*, not for a generic "open pup" action.

Fetch any asset from the wiki without cloning:

```
https://wiki.adom.inc/api/v1/pages/pup-bridge/files/assets/pup-icons/pup-cat-default.svg
https://wiki.adom.inc/api/v1/pages/pup-bridge/files/assets/pup-icons/manifest.json
```

[`assets/pup-icons/manifest.json`](assets/pup-icons/manifest.json) is machine-readable: it maps each
icon id to its meaning, its glyph, and its file in every format, and names
`recommended_for_launcher` so a tool can pick the right one programmatically. Brand rule if you ever
redraw one: teal tile `#00b8b0` + dark-teal window body `#003d40` + a single-color teal glyph, no
gradients, shadows, or emoji.

## Real windows, one per type

Full windows as pup opens them (title bar, tabs, page). Note the tab title glyphs on the wiki pair
and the signed-in header on the second:

**Adom wiki, PUBLIC** (tab: `○ Public`, page header shows Login):

![A pup window on the Adom wiki, public view](docs/screenshots/window-wiki-public.png)

**Adom wiki, SIGNED IN** (tab: `● Logged in`, page header shows the user's name):

![A pup window on the Adom wiki, signed in](docs/screenshots/window-wiki-signedin.png)

**An Adom app** (here: the shotlog viewer on a cloud slug URL):

![A pup window on an Adom app](docs/screenshots/window-adom-app.png)

**The web** (ti.com):

![A pup window on the open web](docs/screenshots/window-web.png)

**Mixed** (a digikey tab and a wiki tab in one window):

![A pup window with mixed content](docs/screenshots/window-mixed.png)

## Overlay badges: the corner of the taskbar icon

![The overlay badges pup places on its taskbar icons](docs/icons/overlays-v1.png)

The base icon states the window TYPE; the small overlay riding its lower-right corner carries
LIVE detail, composited exactly like this:

| Overlay | When | What it tells you |
|---|---|---|
| the active tab's **favicon** on a dark rounded plate | split mode, page serves a favicon | which site the active tab is on, at a glance |
| a **count badge** (white digit, dark plate) | split mode, 2+ tabs and no favicon | how many tabs the window holds |
| the **dual counter** (dark = windows, teal = total tabs) | grouped mode | how much is stacked under the single Adom Pup button |

The overlay never carries branding or state that belongs to the base icon: it is live per-window
detail only, and the favicon always wins the slot when one exists.

## The Adom wiki: logged-in vs public view, and the taskbar toggle

pup can show any Adom wiki page from **two viewpoints**, and both the AI and the user can switch:

- **Public (default)**: a fresh logged-out profile: exactly what the world sees. Use it to verify
  what a publish actually exposed.
- **Signed in**: `pup_open_window {..., wikiView:"authed"}` routes to a reserved persistent
  profile whose cookie jar holds the user's ~30-day SSO session. One login serves **every** AI
  thread on the machine. First use returns `wikiLoginNeeded:true`; the user signs in once, in that
  window.

**The user can flip a wiki window themselves**: right-click its taskbar button: the jump list
carries **"Adom wiki: switch to logged-in view"** (or "...switch to public view"). Clicking it
relaunches that same window under the other cookie jar, brings it to the front, and shows an
on-screen caption while it works ("Switching to the logged-in view of the Adom wiki", then
"Adom wiki: now the LOGGED-IN view (Their Name)"). The AI-side equivalent is the
`pup_wiki_set_view` verb, which exists for that jump-list click: AI threads should pass
`wikiView` on open instead.

## One session, one OS window

Every pup session gets its **own OS window**, even when sessions share a browser profile (as all
signed-in wiki windows share the authed cookie jar). This is what makes per-window identity
possible: a window's title belongs to its active tab, so windows that share sessions as tabs
cannot be found, branded, flashed, or given jump lists individually. Sharing a profile shares the
cookies; it never merges windows.

## Recording

pup can record what it drives, a **single tab/window** or the **whole desktop**, straight from the
cloud, with no HUD and no need to bring the window forward.

![Example pup recording, driving the Adom wiki, captured in the background](https://wiki.adom.inc/api/v1/pages/pup-bridge/files/pup-recording-demo.gif)

*Above: a GIF preview of a real `pup_record` capture (≈8.6 s, the tab navigating the Adom wiki, recorded in the background on a VM). The native capture is the WebM linked below, this GIF is just an inline preview.*

### High-FPS windows: recording something the user is not looking at

pup windows are backgrounded by design, and Chrome throttles a window that is occluded, so
recording one normally yields a **1-2 fps** video. pup can disable Chrome's power-saving to keep a
window painting at full rate, but a window in that mode can never idle down (three of them once
drained a laptop overnight, wiki issue #14).

So it is **opt-in and leased**:

- **Default** is Chrome's own power behaviour. Normal windows throttle when occluded and cost
  nothing in the background. Right for driving, scraping and screenshots.
- **`pup_open_window {highFps:true}`** renders at full rate while occluded, for background
  recording. It holds a lease (default 20 min), and **an active recording holds that lease open
  indefinitely, so a take is never cut off**. On lapse the window is *downgraded* back to normal
  power by relaunching with its tabs intact, never closed.
- **`pup_highfps_extend {sessionId, minutes}`** negotiates more time up front.

**Full details, defaults, activity signals, and rules of thumb: [RECORDING.md](RECORDING.md).**

### Two recorders, pick by what you're capturing

| Verb pair | Captures | How it works under the hood |
|---|---|---|
| **`pup_record_start` / `pup_record_stop`** | ONE pup **tab/window**, the page content | **CDP `Page.startScreencast`** (JPEG frames, up to ~50 fps) piped to **ffmpeg**, encoded to **VP9** in real time → one `.webm`. Tab-scoped at the protocol level, so it captures the exact tab even in the **background**, no `getDisplayMedia`, no picker, no foreground. |
| **`desktop_record_start` / `desktop_record_stop`** | the WHOLE **desktop** (multiple apps, dialogs) | Screen capture via **MediaRecorder** → one `.webm`; ffmpeg remuxes it to stamp a proper Duration. |

```bash
adom-bridge-cli pup_record_start '{"sessionId":"demo-web"}'          # → { recordingId }
# ...drive the scene: navigate, click, type, scroll...
adom-bridge-cli pup_record_stop  '{"sessionId":"demo-web","recordingId":"<id>"}'   # → { path: "...webm" }
```

**ffmpeg** is required for recording. The bridge auto-detects it (a WinGet `Gyan.FFmpeg` install, PATH,
or common dirs) and re-checks **lazily on `record_start`**, so if it's missing you can
`winget install Gyan.FFmpeg` and just retry, no bridge restart. `pup_record_status` /
`pup_record_list` (and the `desktop_record_*` equivalents) report in-flight and finished captures.

### Formats, one native format, easy conversion

pup records to **one** format natively: **WebM (VP9)**, for both recorders. It doesn't offer a format
switch; instead, convert with the bundled ffmpeg when you need MP4 or GIF. Real numbers from the ≈8.6 s
demo above (1264×705):

| Format | Codec | Size | How you get it | Best for | Watch out for |
|---|---|---|---|---|---|
| **WebM** ([view](https://wiki.adom.inc/api/v1/pages/pup-bridge/files/pup-recording-demo.webm)) | VP9 | **143 KB** | pup's **native** output, no step | Chrome/Edge/Firefox, small + high quality, the source of truth | Older **Safari**, PowerPoint, and some chat apps don't accept VP9/WebM |
| **MP4** ([view](https://wiki.adom.inc/api/v1/pages/pup-bridge/files/pup-recording-demo.mp4)) | H.264 | **135 KB** | transcode (below) | **Universal**, Safari, iOS, PowerPoint, Slack/Teams, "send it to anyone" | H.264 needs **even width & height**, odd dims (like 705) fail at frame 0; the `scale` filter forces even |
| **GIF** ([view](https://wiki.adom.inc/api/v1/pages/pup-bridge/files/pup-recording-demo.gif)) |, | **192 KB** | transcode (below) | Auto-playing, looping **inline preview** in any README/chat/GitHub, no player, no click (like the one above) | No audio, capped colors, and it balloons fast at higher fps/resolution/length, keep it short + downscaled |

```bash
# → MP4 (H.264, universal). The scale filter forces EVEN dimensions (H.264 rejects odd w/h → frame 0).
ffmpeg -i rec.webm -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" -c:v libx264 -crf 23 -pix_fmt yuv420p -movflags +faststart rec.mp4

# → GIF (silent, looping inline preview). Palette pass keeps colors clean; drop fps/scale to shrink.
ffmpeg -i rec.webm -vf "fps=12,scale=640:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" rec.gif
```

**Rule of thumb:** keep **WebM** for archiving + web embeds; make an **MP4** the moment a human on Safari/
PowerPoint/phone needs it; make a short **GIF** only for an inline auto-play preview.

## Source & development

This bridge is cloud-owned here (extracted from `adom-bridge/plugins/puppeteer`, mirroring the
kicad/fusion bridges). Bridge ships a bundled seed and auto-pulls newer versions from this page.

- Clone the source: `adom-wiki repo clone adom/pup-bridge`
- Runtime lives in `src/` (`server.js`, `chrome.js`, `bridge.json`, …); the release zip is `src/` at zip-root.
- **Maintainer skills** (`pup-bridge-dev`, `pup-bridge-publish`) are **source-only**, in `dev-skills/` +
  `publish-skills/`, not shipped in the pkg (per the Bridge SDK a pkg ships only user skills). Skill map:

| Skill | Ships in pkg? | What it's for |
|---|---|---|
| `pup` (root SKILL.md) | ✅ user | Start here, mental model (CDP-launched fresh process of the installed browser, rendered-but-backgrounded), when-vs-extension, quick start |
| `pup-windows-sessions-tabs` | ✅ user | sessionIds, window ownership (don't steal another thread's window), background-vs-foreground, many tabs in one window |
| `pup-screenshots-recording` | ✅ user | Screenshots (full-page/full-res) + recording; driving/verifying tricky pages (shadow DOM, nested scroll) |
| `pup-browsers-and-chrome` | ✅ user | Browser detection/launch, `pup_use`, installing Chrome/CfT (incl. UAC-notify), readiness, cold-start errors |
| `pup-adom-wiki` | ✅ user | Driving + verifying `wiki.adom.inc` pages in pup (view/screenshot/verify; the login situation) |
| `pup-bridge-dev` / `pup-bridge-publish` | ❌ source-only | MAINTAINERS: bridge internals + the publish/release recipe (in `dev-skills/`/`publish-skills/`, not shipped) |
- Ownership boundary: `CLAUDE.md`. Publish recipe: `PUBLISHING.md`. History: `CHANGELOG.md`.

## Extend pup, add your own pup skill (third parties)

Built a skill that drives pup for a specific job (a site-specific scraper, a visual test flow, a
domain widget)? You don't need write access here, surface it from this page with a **breadcrumb**:

1. **Publish your skill** to your own wiki page (see the `adom-wiki` / `wiki-skillpack` skills). Make it a
   normal user skill so `pkg install` drops it into a container's `~/.claude/skills`.
2. **Drop a breadcrumb on this page** pointing at it:
   ```bash
   adom-wiki breadcrumb post adom/pup-bridge \
     --label "Your skill name, one-line what it does" \
     --target-url "https://wiki.adom.inc/<owner>/<your-slug>" \
     --category "skill"
   ```
   It's **pending until the page owner approves it**, then it shows in this page's **Breadcrumbs** tab.
3. **Discovery is automatic:** an agent working with pup can list related third-party skills with
   `adom-wiki breadcrumb list adom/pup-bridge`, approved breadcrumbs are how pup finds
   community skills that build on it.

Keep breadcrumbs on-topic (they genuinely extend pup / browser-driving); off-topic ones get rejected.

## Related

- [Bridge SDK guide](https://wiki.adom.inc/adom/adom-bridge-sdk), `bridge.json` schema, packaging, lifecycle
- [KiCad bridge](https://wiki.adom.inc/adom/kicad-bridge) · [Fusion 360 bridge](https://wiki.adom.inc/adom/fusion-bridge)
- [Bridge (parent app)](https://wiki.adom.inc/adom/adom-bridge)

MIT licensed.
