# Skill: Capture an Inkly demo from a URL

You are an external coding agent (Claude Code, Codex, Cursor, …) running inside the user's project. Your job is to turn a request like "capture a demo of <url>" into a finished Inkly demo ZIP of image/video steps. You plan the story and decide what to do; the Inkly CLI is the capture harness AND the page driver: it opens real Chrome, injects a recorder, hosts a navigation driver, and exports the ZIP. You drive the page through `inkly capture nav` — you do NOT attach your own browser-control library.

This skill is about CAPTURING the steps. Polishing the result — validating frames, trimming stray steps, relabeling, previewing — is a separate skill; focus here on getting clean captures. For self-contained HTML output instead of image/video, use the `agent-html-capture` skill.

## Contract

- Install the Inkly CLI globally, then capture the target.
- Produce a ZIP exported by `inkly capture stop`, plus the unpacked demo folder.
- Use only the agent-facing lifecycle: `capture start`, `capture nav` (to drive), `capture stop`, `capture cancel`. `capture status` and `capture undo` are available for inspection/recovery.
- Do not pass prompts into the CLI (`capture` rejects `--prompt`); you own planning and the sequence of nav actions.
- No Chrome extension is required. Do not open the Chrome Web Store or load an unpacked extension.
- Drive the page ONLY through `inkly capture nav <verb> --session <id>`. Do NOT attach Playwright/Puppeteer over CDP — the CLI hosts the driver for you.
- The capture browser is a fresh, isolated Chrome profile that does NOT touch the user's everyday Chrome. Never attach to, copy, or restart their real Chrome or profile.

## Install the Inkly CLI

Install the CLI globally, then confirm it runs:

```bash
npm install -g @inkly-org/cli
inkly version
```

Use the global `inkly` binary for capture commands. Do not run Inkly through `npx`; one-off execution can resolve Chrome differently and cause capture/browser launch failures.

### Connect the CLI if the user gives you an Inkly API key

Users sometimes paste an Inkly API key (token) into the request. If you are given one, authenticate the CLI with it FIRST — before you start capturing:

```bash
inkly login --token <inkly-api-key>   # or: export INKLY_API_TOKEN=<inkly-api-key>
inkly status                          # confirm it connected
```

Capture itself runs locally and does not require this, but connecting up front means the finished demo can be synced/published without re-authenticating, and it ties the run to the user's account. This is the Inkly PLATFORM key — it is unrelated to signing in to the site you are capturing (that is the persistent-profile flow below). If no key is provided, skip this; do not ask for one unless the user wants to publish.

## Choose what to capture

You can capture either a hosted URL or the user's own app running locally. If the user did not make it clear, ask which.

- A HOSTED URL (a SaaS site, marketing page, or web app the user names): use it directly as `<TARGET_URL>`.
- The USER'S LOCAL APP, when you are running inside its repo and they want a demo of their own product: this is usually the SIMPLEST path — no bot protection, and login is whatever their dev setup uses. Spin the dev server up for them:
  1. Find the dev/start script in `package.json` (commonly `npm run dev`, `pnpm dev`, `npm start`, or a framework command). If it is ambiguous, ask which command and port.
  2. Start it in the BACKGROUND and WAIT until the port actually responds (poll `http://localhost:<port>`), not just until the command launches.
  3. Use that `http://localhost:<port>` plus the right starting route as `<TARGET_URL>`.
  4. Leave the server running for the whole capture; stop it when you are done.

## Tell a good story

A demo is a narrative, not a tour. Before you click anything, decide the ONE sentence a viewer should believe by the end ("<product> lets me <core outcome> with <little effort>"), who that viewer is, and the single signature moment that proves it. Every click serves that sentence.

- Lead with value, not chrome. Your first real step should land on the core value or the signature interaction — not a login screen, a settings page, or an empty dashboard. Hook first, context later.
- Show the product WORKING, do not point at where it lives. A completed search with results, a populated dashboard, a command palette mid-use, a generated output, a real workflow screen — beats a static marketing page or an empty form every time. Capture the interaction in action, not just its location.
- One idea per step. Each step should advance the story by exactly one beat — a new capability, a differentiator, a payoff. If a step does not change what the viewer understands, do not capture it.
- Sequence value-first: hook (the aha) -> the core capability in action -> a differentiator that sets it apart -> a supporting feature -> a payoff. Put pricing and sign-up LATE, never mid-story.
- Prefer populated, real states. Navigate or seed your way to data-rich screens; avoid zero-states, empty inboxes, and lorem ipsum. A viewer believes a product they can see working with real content.
- Match the audience: a developer tool's signature moment is a real API call returning data; a design tool's is a real canvas edit; a CRM's is moving a real deal forward. Pick the moment that audience cares about.
- Keep it tight: 5-8 steps unless the user asks otherwise, and END on the payoff you want remembered. A short, coherent arc beats a long, flat tour.

## Browser and mode

`start` runs REAL Google Chrome (not Chrome for Testing — that build gets challenged; real Chrome clears most bot protection) and is HEADLESS by default (faster, no window; real Chrome passes passive Cloudflare even headless). Use `--headed` only when the USER must act in the window — to clear a challenge or sign in by hand. A headed automation window does not beat a wall on its own; it just lets the user reach it.

## Bot walls

- PASSIVE protection (most Cloudflare): real Chrome renders the page, headless or headed — nothing to do.
- ACTIVE challenge (a CAPTCHA/Turnstile widget, a "verify you are human" / "Just a moment..." interstitial, or "you have been blocked"): CDP-driven Chrome cannot pass it, headed or not — do NOT try to script around or evade it. If it only gates a marketing page, capture whatever rendered and report partial coverage. If it gates a login the demo needs, hand off to the user (see "Capturing a logged-in product").

## Sign in before you record (when a demo needs an account)

If it is clear — either because the USER told you so, or from your own judgment of the story — that a sign-in or sign-up is required for a successful demo (the value lives behind auth: a dashboard, a workspace, a generated result a logged-out visitor cannot reach), create the persistent profile FIRST, before the real capture:

```bash
inkly capture ask-user-to-log-in --url <login-or-signup-url>                 # returns profile.name; user signs in/up by hand
inkly capture start --profile <returned-profile-name> --url <app-url> --window-size 1440x900
```

Recording then begins from a signed-in app, and the user is never asked to authenticate mid-capture. Do NOT start the real capture until the profile is signed in. This is also the only way to handle OAuth / "Sign in with Google", which cannot be completed inside the CDP-driven capture. Full detail — credentials you can type yourself, and reusing a saved login across runs — is under "Capturing a logged-in product" below.

## Start capture

Use the target URL, a descriptive demo name, and a stable viewport (1440x900 unless asked otherwise). `--runtime cli` is the default and recommended.

```bash
inkly capture start \
  --runtime cli \
  --url <TARGET_URL> \
  --name "<descriptive demo name>" \
  --window-size 1440x900 \
  --no-video
```

Modes:

- Default image mode: omit mode flags; exports PNG image steps. Click zoom/pan is on by default — pass `--no-zoom` only if the user asks.
- Video motion: the `start` command above passes `--no-video` for deterministic image-only steps — the reliable default for a first capture. To capture motion instead, OMIT `--no-video`: a `nav click` that follows smooth scroll/typing then exports the preceding motion as a WebM step with a PNG poster. See the video caveat under "Drive the page with `inkly capture nav`".

Optional size levers (on `start`): `--compress-images` re-encodes captured PNG/JPEG screenshots to WebP (smaller, near-lossless); `--subset-fonts` subsets any captured font assets to the glyphs used (WOFF2). Both are screenshot-only — for self-contained HTML capture use `inkly capture-html` instead.

Parse the JSON output and SAVE `session.id` — every `nav`, `stop`, `cancel`, and `status` call needs `--session <id>`. (`browser.webSocketDebuggerUrl`, `browser.debuggingUrl`, and `tab.targetId` are still printed, but with the nav contract you do not need to attach to them yourself.)

## Record steps

After `start`, drive the page with `inkly capture nav`. Do not call manual capture commands and do not attach your own browser client.

1. `inkly capture nav snapshot --session <id>` ONCE to see the landing page (plain text tree with `@refs` — read it directly, no parsing). This should be your only standalone snapshot.
2. `inkly capture nav click "@<ref>" --session <id>` on a visible user-facing target to create the next step. The command echoes `url=… | steps=N` and the FRESH tree of the resulting page — read the WHOLE tree, do NOT pipe it through `head`/`sed` (that truncates the refs and forces a wasteful re-snapshot).
3. Pick your next ref from that returned tree and act again — aim for ~1 call per step. In the normal loop do NOT call standalone `nav snapshot`/`nav refs`/`nav get url`/`capture status`; the action already printed the tree, url, and step count.
4. Below-fold clicks become smooth scroll videos on their own — a `nav click` scrolls to an off-screen target first. Use a standalone `inkly capture nav scroll down --session <id>` then a click only for a scenic scroll over on-screen content — see the caveat.
5. To end on a payoff page, `nav click` once more on a NON-navigating element of it (an on-page heading or section anchor) — never the logo or a nav link.

## Drive the page with `inkly capture nav`

Do NOT attach your own browser-control library over CDP. The CLI hosts a navigation driver inside the capture listener; you drive the page entirely through `inkly capture nav <verb> --session <id>`, using the `session.id` you saved from `start`. Every nav verb that drives the page (click/fill/type/press/select/scroll/...) fires REAL browser input, which the capture recorder hears and records as a normal step. Driving the page IS what produces steps — there are no manual capture commands, and you never take a screenshot to CAPTURE a step. (The one exception, `nav screenshot`, is a DEBUG-ONLY escape hatch for inspecting a stuck page — it records nothing; see the verb reference.)

### Output is plain text — read it directly (no parsing, no truncating)

- `inkly capture nav snapshot --session <id>` prints a plain indented accessibility tree (roles + names + `@refs`) STRAIGHT to stdout. Read it as-is — do NOT pipe it through python/jq/grep, there is no JSON wrapper and no `urlMap`/`xpathMap` dump to parse. Pick the `@ref` of the element you want directly from the printed tree.
- Every page-driving verb (click/fill/type/select/scroll/back/...) ECHOES its own status line plus a FRESH tree: `ok | url=<current url> | steps=<N> | <last step label>` followed by the trimmed accessibility tree of the page AFTER the action. So after a click you already have the new page's refs and the current url + step count — you do NOT need a separate `nav snapshot`, and you do NOT need `nav get url` or `capture status` mid-run.
- A step that just NAVIGATED may still be committing when the command returns, so the header OMITS `steps=` and shows `step still committing` instead of a number. The click WAS recorded — do NOT re-click (that double-records); just continue with the returned tree. `capture status` has the settled count if you need it.
- Do NOT pipe nav output through `head`, `tail`, `sed`, or `| head -N`. Truncating the output cuts off the refs at the bottom of the post-action tree, so you lose the very refs you need and are forced into a wasteful standalone `nav snapshot` — which is exactly the redundant call to avoid. Read the WHOLE returned tree. `--filter`/`--max-depth` apply ONLY to a standalone `nav snapshot` — an ACTION's echoed tree is always the full trimmed tree and cannot be bounded by flags. So on a long page either just read the full echoed tree, or pass `--no-snapshot` on the action and then take a bounded standalone `nav snapshot --filter <text|/re/>` / `--max-depth <n>` to find your next ref.

### The loop: snapshot ONCE → ref → act → read the returned tree → act again

1. `inkly capture nav snapshot --session <id>` ONCE at the start to see the landing page and its refs. This is the ONLY standalone snapshot you should need.
2. Pick the ref of the element you want (a button, link, field, tab) from the printed tree.
3. Act on that ref, e.g. `inkly capture nav click "@0-12" --session <id>`. The recorder captures the step, and the command prints the new url + step count + the fresh tree of the resulting page.
4. Pick your next ref from THAT returned tree and act again. The returned tree IS your next snapshot — every action hands you one, so you should fall into a steady 1-call-per-step rhythm (one action = one step + the next tree).
5. In the normal loop do NOT call standalone `nav snapshot`, `nav refs`, `nav get url`, or `capture status` — the action you just ran already printed the tree, the url, and the step count. Reach for a standalone `nav snapshot` ONLY when you genuinely lost the tree (you passed `--no-snapshot`, or piped the output through `head`/`sed` and truncated it — don't do that), or need `--full`/`--filter`/`--max-depth`. `capture status` and `nav refs` are recovery/programmatic tools, not part of the capture loop.

The selector slot for any verb accepts a `ref` from the latest tree (`@0-12`, `[0-12]`, `ref=0-12`, or bare `0-12`), or an XPath, or a CSS selector. PREFER refs — they are resolved from the tree you just read. Run `nav snapshot` first so refs exist; the post-action tree refreshes them automatically.

Refs are valid ONLY from the MOST RECENT tree. Every snapshot (and every action's echoed tree) renumbers refs from scratch — the same on-screen element gets a different ref each time. So always pick from the tree the LAST command just printed; never reuse a ref from an earlier tree (and never hardcode one). A ref from a stale tree fails with `Unknown ref "<ref>" - run `inkly nav snapshot` first to populate refs` — if you see that, you used an old ref; re-read the latest returned tree and pick the ref from there (you rarely need a fresh standalone snapshot).

### Verb reference

All verbs take `--session <id>`. The `<selector>` slot is ref|xpath|css (prefer refs). Action verbs print the post-action tree by default; add `--no-snapshot` to suppress it or `--json` for the structured object.

- `nav snapshot [--full] [--filter <t|/re/>] [--max-depth <n>] [--json]` — plain accessibility tree with refs, trimmed to interactive/heading/landmark nodes by default. `--full` prints the untrimmed tree; `--filter` keeps only nodes matching a substring or `/regex/`; `--max-depth <n>` caps depth; `--json` emits `{tree,urlMap,xpathMap}` for programmatic use. You rarely need this standalone — actions already return a fresh tree.
- `nav refs` — print the structured ref maps (count + urlMap + xpathMap) as JSON; for programmatic callers only.
- `nav click <selector>` — click the element. The main step-producing verb. Returns the post-action tree.
- `nav fill <selector> <value> [--enter]` — set a field's value in one shot; `--enter` presses Enter after (submit a search/login).
- `nav type <text> [--delay <ms>]` — type text into the focused element character-by-character; `--delay <ms>` spaces keystrokes.
- `nav press <key>` (alias `nav key <key>`) — press a single key (e.g. `Enter`, `Tab`, `Escape`, `ArrowDown`).
- `nav select <selector> <value...>` — choose option(s) in a native `<select>`.
- `nav scroll <down|up> [pixels]` — SUSTAINED smooth scroll within one call (~1.5-2s of dense wheel motion), for a scenic scroll or to set up a VIDEO step over on-screen content (see below). You do NOT need it just to reveal a below-fold click target — a `nav click` scrolls to its target itself. Defaults to ~1600px; pass a positive pixel amount to scroll further.
- `nav upload <selector> <file...>` — set file input(s) (paths resolved against cwd).
- `nav get <url|title|text|html|value|visible|checked|markdown|box> [<selector>]` — read state without driving the page (no step). `box` returns the element centroid `{x,y}`. You do NOT need `get url` mid-run — actions already echo the url.
- `nav is <visible|checked> <selector>` — boolean check on an element (no step).
- `nav wait <load|selector|timeout> [<arg>] [--state <s>] [--timeout <ms>]` — wait for a load state (`load`|`domcontentloaded`|`networkidle`), for a selector to reach `--state`, or for N ms (`nav wait 1500`). Actions already wait for load before returning their tree, so you mainly need this for slow animations.
- `nav back [--wait <state>] [--timeout <ms>]` / `nav forward ...` / `nav reload ...` — history navigation.
- `nav mouse click <x> <y> [--button <...>]` / `nav mouse hover <x> <y>` / `nav mouse scroll <x> <y> <dx> <dy>` — raw coordinate input, for targets with no usable ref. Get coordinates from `nav get box <selector>`. For scrolling prefer `nav scroll`; for a video step use `nav scroll`, NOT separate `nav mouse scroll` calls.
- `nav screenshot [--full-page] [--out <path>]` — **DEBUG-ONLY escape hatch, NOT part of the capture.** Saves a PNG of the current viewport and prints its `path`; **READ that file to actually SEE the page.** Drives nothing and records no step. Reach for it ONLY when you are stuck — the snapshot tree is ambiguous, or an action recorded a step but the page did not change (e.g. an icon-only button whose accessible name is misleading like `Send message`, a mode toggle that reads as a plain button, or a disabled submit) — to confirm what is really on screen, then act on it (often `nav mouse click <x> <y>`; the visible viewport's pixels map 1:1 to the shot). `--full-page` captures the whole scroll height instead (its coordinates do NOT map to `mouse click`). Do NOT use it in the normal loop — the tree each action echoes is your view there.

### Getting a VIDEO step (important nav caveat)

A step becomes a VIDEO (WebM + poster) when smooth motion immediately precedes the committing click — and you usually get that FOR FREE. A `nav click` on an OFF-SCREEN target smooth-scrolls to it first (see "BELOW-FOLD CLICKS SCROLL THEMSELVES"), so a below-fold navigation click already commits as a connected scroll video; you do NOT need a separate scroll. Reach for an explicit `inkly capture nav scroll down --session <id>` (one ~1.5-2s smooth scroll) THEN a committing `nav click` only to feature motion over content ALREADY on screen, or a scenic scroll past a section you are not clicking. Typing motion (`nav type "query" --delay 45`) before a click also makes a video (e.g. a search box).

- Do NOT try to build motion out of SEPARATE `nav mouse scroll` calls — each pays CLI startup latency and lands too far apart to cross the motion threshold; they fall back to a still. Use the single `nav scroll` verb instead.
- Only make a video step when motion is part of the story (scrolling a pricing/feature section, typing a search). For a plain transition, a normal `nav click` still image is correct.

## How nav actions map to steps (read before driving)

- ONE step per driving action. A `nav click` (and any other page-driving verb) is captured as a step on the page it acted on. `nav snapshot`, `nav get`, `nav is`, `nav refs`, and `nav wait` do NOT create steps — use them freely to inspect and position. The action's echoed `steps=<N>` tells you your running count toward your 5-8 step budget.
- CLICK = CAPTURE. Every `nav click` the recorder sees becomes a step — including exploratory ones. Never click just to look around; read the tree the previous action already returned. `nav click` ONLY when you intend that exact click to be the next step.
- BELOW-FOLD CLICKS SCROLL THEMSELVES. A `nav click` on a target that is off-screen first SMOOTH-SCROLLS it into view, and that scroll is captured as motion — so the step reads as a connected scroll→click that starts from where you were (e.g. the hero), instead of teleporting to a mid-page still. You therefore do NOT need a separate `nav scroll` just to reveal something you're about to click; click it directly. Use an explicit `nav scroll` only to show a scenic scroll through content you are NOT then clicking. (A click on an element already on screen stays a normal still.)
- NAVIGATE BY CLICKING THE UI. This is a product demo for a human viewer: move between pages by `nav click`-ing the real on-screen nav link, menu item, card, or button (find its ref in the latest tree), not by jumping URLs. Each navigation click earns its step AND shows the viewer how to get there.
- After a click that navigates, the command already waited for load and returned the new page's tree + url — pick your next ref straight from that returned tree. Only re-`snapshot` if you used `--no-snapshot`, or `nav wait` more if a long animation is still running.
- Your first `nav click` captures the landing page, so make it a meaningful navigation — not a consent/cookie dismissal. Dismiss cookie/consent banners first (find the dismiss ref and `nav click` it), then begin your real steps. If the banner has no clearly-named dismiss ref in the tree, leave it — do not hunt; it will sit in the frames and you relabel/trim in polish. Note a banner-dismiss click still records as a step, so expect to drop or relabel that step later. Do NOT poll `nav get url` or `capture status` between actions — every action already echoes the url and step count.
- The page a click navigates TO is NOT captured on its own — it becomes a step only if you click on it too. So your payoff page is MISSING unless you `nav click` once more on it, and that closing click must be on a NON-navigating element (an on-page heading or section anchor) — NEVER the logo or a nav link.
- The step LABEL comes from the clicked element's accessible name. Prefer NAMED controls (links, buttons, tabs, headings with real text) so steps read well.

Driving rules:

- Before driving, restate your one-sentence story (see "Tell a good story") and sequence your 5-8 value-first steps. Include at least one step that shows the product's signature interaction in action (a search-with-result, a command palette, a populated dashboard/playground, a real workflow screen).
- Be intentional. Pick the exact ref of the element you mean from the latest snapshot; when a label repeats (a `Get started` in both header and hero), the refs differ — choose the one in the region you want.
- The action's returned tree is taken after the page settled; only add an explicit `nav wait` when a long animation or async load is still running before you pick the next ref.
- For a non-semantic clickable element with no usable ref (a `<tr>` row, a `<div>` card with no role), read its position with `nav get box <selector>` and click it with `nav mouse click <x> <y>`.
- Form fields: `inkly capture nav click` the field (a good step — it shows the viewer where to act), then `nav fill <ref> "<value>"` or `nav type` it, then `nav click` the submit button (a second step showing the filled form). For a search box, do not END on an empty focused box: fill/type, wait for results, then `nav click` a meaningful RESULT.
- Keep the viewport stable for the whole capture (set it once via `start --window-size <w>x<h>`); do not resize mid-run.
- Record a useful product flow — 5-8 meaningful steps unless the user asks otherwise. Place pricing LATE; never put a contact-sales/lead form mid-story. Avoid redundant steps where the screen does not meaningfully change.
- Login is allowed when the demo needs a signed-in view (see "Capturing a logged-in product"). Avoid checkout/payment, destructive actions, other people's private data, and CAPTCHAs.

## Capturing a logged-in product

Most demos need NO login — marketing sites, public product surfaces, and the user's own local app (with whatever dev login it already uses) capture directly. Only sign in when the demo genuinely needs a signed-in view, and check first whether the core interaction is reachable WITHOUT login (a public playground or sandbox).

### Settle login BEFORE you record

When it is clear — from the user's instruction OR your own judgment — that a sign-in or sign-up is required for the demo to land, create the persistent profile FIRST, before the real capture: run `inkly capture ask-user-to-log-in --url <login-or-signup-url>`, have the user authenticate by hand, then `capture start --profile <returned-profile-name>`. The command derives a profile name from the URL host when `--profile` is omitted and returns it as `profile.name`; pass an explicit `--profile <name>` only when you need a custom reusable name. Your recorded steps then begin from a signed-in app and the user is never asked to authenticate mid-capture. If instead a capture you already started lands on a login wall (a snapshot showing an email/password form or "Sign in with …"), do NOT push through it inside the capture — run the same `ask-user-to-log-in` hand-off (it closes the running capture for you), then start a fresh capture against the app.

### Ask how to authenticate

ASK the user how to sign in — never guess credentials or pick a method. Two paths:

- CREDENTIALS you can type (email + password they give you, or an email code / magic-link you read from a mail tool they connect): you may drive these yourself with `nav` inside the capture, or do them in the hand-off window. Avoid baking real secrets into recorded steps.
- OAUTH / SSO ("Sign in with Google/GitHub/SSO"), or any sign-in only the USER can complete (passkey, 2FA prompt, a CAPTCHA on the login page): use the sign-in hand-off below. The capture browser is CDP-driven, so an OAuth provider rejects an in-capture click — "this browser or app may not be secure" — even when a human clicks it. The hand-off opens a clean, NON-automated window where OAuth works normally.

### Sign-in hand-off: `ask-user-to-log-in`

When the user must sign in by hand, hand them the keyboard:

```bash
inkly capture ask-user-to-log-in --url <login-url>
```

- It derives a reusable profile name from the `--url` host unless you pass `--profile <name>` explicitly. Read `profile.name` from the JSON result and reuse it on `capture start --profile <name>`.
- It CLOSES any capture currently using that profile first (a profile can only be open in one Chrome at a time — you cannot record and hand-log-in simultaneously), then opens a SEPARATE real Chrome with NO automation attached, at `--url`. OAuth/SSO succeeds there because the provider sees an ordinary browser.
- It RETURNS IMMEDIATELY, leaving the sign-in window open. Tell the user in your own words: a sign-in window opened, finish signing in ("Sign in with Google/GitHub" is fine here) — they do NOT need to close the window. Then WAIT for the user to confirm they're signed in; don't guess when they're done.
- The sign-in PERSISTS into the returned `profile.name`. Once the user confirms, START THE CAPTURE FRESH against the app: `inkly capture start --profile <profile.name> --url <app-url> --window-size 1440x900`. It automatically closes the leftover sign-in window (flushing its cookies) before taking the profile, then opens already signed in — CDP attached is fine now because no interactive sign-in happens; the saved cookie is already valid.
- Point the capture `--url` at the APP host you land on after login (often an org subdomain like `<org>.example.com`); a marketing/login root can look logged-out even when the session is valid.

### Reuse forever (persistent profile)

A sign-in done once is saved and reused, so later captures of the same site need no sign-in at all:

- `--profile <name>` keeps a persistent Chrome profile (a bare name maps to `~/.inkly/capture-agent/profiles/<name>`) that survives across captures, including on error. `inkly capture profiles` lists which profiles are saved and whether each has cookies.
- REUSE: `start --profile <name>` opens already signed in — capture is hands-off; `start`'s JSON reports `browser.profile.reused: true`.
- VERIFY sign-in FROM THE PAGE (a visible account menu, the app URL, no login form) — never from `hasCookies` alone; `hasCookies: true` does not by itself mean signed in.

Never enter payment, take destructive actions (delete/send/publish), or change account settings.

## Stop and export

Stop after waiting ~2 seconds past the final `nav click` and confirming the page settled:

```bash
inkly capture stop --session <session-id> --out ./tmp/agent-capture-output
```

The JSON includes `output.zipPath` (the deliverable ZIP), `output.demoDir` (unpacked folder unless `--no-unpack`), and `output.stepCount`. Report these back. To abandon a run: `inkly capture cancel --session <session-id>`.

## When you're done — verify, then offer to polish

Capturing the steps is only step one; a raw capture is rarely shippable as-is. Before you call it finished:

- ASK THE USER TO VERIFY. Tell them where the demo landed (`output.demoDir` and `output.zipPath`) and the step count, and ask them to confirm it captured what they wanted — the right flow, the right pages, nothing missing or broken. If you can, preview it first with `inkly dev` so they review the real thing.
- OFFER TO POLISH (do not start silently). A raw capture is bare backgrounds + pointers. Offer to turn it into a guided demo with the **`agent-demo-polish`** skill — fetch https://www.app.inklyai.dev/__inkly/skills/agent-demo-polish.md. It validates every frame, trims/relabels/reorders steps, tightens the story, and adds annotations, chapters, cover screens, and optional TTS voiceover. If the user agrees, load that skill and follow it; otherwise hand back the export.
- A step that landed WRONG (blank or mismatched background, a broken top-left pointer, missing motion, the wrong page) is a re-capture fix, not a polish fix — re-drive those steps here with `capture nav` before handing off.

## Put it in a hub

A capture exports a STANDALONE demo folder — preview it directly with `inkly dev <demoDir>` (no hub needed). To keep it alongside other demos in a hub, and to sync/share it, IMPORT the folder — do NOT hand-copy it into `demos/` and hand-edit `inkly.json` (that leaves the demo out of the hub index):

```bash
inkly init my-hub          # once, if you don't already have a hub
cd my-hub
inkly add <name> --from <demoDir> --collection Main
```

`inkly add --from` copies the whole demo (demo.config.json, assets.json, `public/` bytes, and any `snapshots/`) AND registers it in the named collection so it appears in the hub index. If the hub has a single collection and you omit `--collection`, the demo joins that one automatically; a demo in NO collection is still valid but stays OFF the index (use that for a private/single-client demo). Captured asset bytes live in `<demo>/public/<file>` and `inkly dev` serves them at `/<slug>/<file>` (the startup banner prints this) — there is no separate cache, `inkly validate` is green with the bytes in place, and you do NOT need `inkly sync` just to preview (sync only uploads those bytes to the CDN for hosted sharing).

## Failure handling

- If `npm install -g @inkly-org/cli` fails, fix the npm global install path or permissions, then rerun the global install. Do not fall back to `npx` for Inkly capture commands.
- If `capture start` cannot launch Chrome, pass `--browser` with the user's real Google Chrome path (do not point it at a Chrome-for-Testing build).
- If FAR fewer steps were captured than the clicks you made (including zero), you acted before the recorder was armed — before the `start` JSON reported `capture.listenerReadyAt`. Wait for `listenerReadyAt` before your first `nav` action. (After a navigation the click already waits for load and the recorder re-arms itself, so you do not need to manage that; clicking browser chrome instead of page content also captures nothing.)
- If pages are clipped after navigation, the viewport changed — start over and enforce one stable content viewport before every click.
- Bot wall: distinguish PASSIVE (real Chrome clears it even headless — confirm `--browser` is not a Chrome-for-Testing build) from ACTIVE (`Just a moment...` with empty body, "Verify you are human", "Sorry, you have been blocked") which neither headless nor headed CDP can pass. For an active wall, capture whatever rendered and report partial coverage, or use a headed hand-off if it gates a login the demo needs.
- If the Chrome session is wedged, run `inkly capture cancel --session <session-id>`; do not leave orphaned sessions.
