diff --git a/README.md b/README.md index 31232f1..3057be6 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,7 @@ Command-line interface skills for using Kernel CLI commands. | **kernel-cli** | Complete guide to Kernel CLI - cloud browser platform with automation, deployment, and management | | **kernel-agent-browser** | Best practices for `agent-browser -p kernel` automation, bot detection handling, iframes, login persistence | | **kernel-auth** | Setup and manage Kernel authentication connections for any website with safety checks and reauthentication support | +| **cua-cli** | Drive a Kernel browser from shell via the `cua` binary: one-shot subcommands, named sessions, TUI, profile persistence, transcripts, live-view handoff | | **profile-website-bot-detection** | Profile a website for bot detection vendors using stealth vs non-stealth Kernel browsers; compare effectiveness and identify vendor products | ### kernel-sdks @@ -82,6 +83,7 @@ SDK skills for building browser automation with TypeScript and Python. |-------|-------------| | **typescript-sdk** | Build automation with Kernel's Typescript SDK | | **python-sdk** | Build automation with kernel's Python SDK | +| **cua-agent** | Build TypeScript apps that embed Kernel cua's loop with `CuaAgent` / `CuaAgentHarness`: provider switching, custom tools, session repos, event-stream debugging | ### generate-video diff --git a/plugins/kernel-cli/skills/cua-cli/SKILL.md b/plugins/kernel-cli/skills/cua-cli/SKILL.md new file mode 100644 index 0000000..29a0e6a --- /dev/null +++ b/plugins/kernel-cli/skills/cua-cli/SKILL.md @@ -0,0 +1,217 @@ +--- +name: cua-cli +description: Drive a Kernel cloud browser from the shell using the `cua` CLI. Use this skill when you need to open URLs, click elements, type into fields, take screenshots, or chain multi-step browser tasks across shell calls. Supports named sessions for stateful workflows, profile persistence for logins, transcript-based debugging, and live-view handoff when stealth fails. For building your own TS agent on top of cua, see `cua-agent`. +--- + +# cua-cli + +`cua` is a single-binary CLI that drives a real Chrome session running in Kernel. It's designed for agentic use: each subcommand returns a one-line result on stdout and a deterministic exit code, so you can chain calls together and parse the output. An LLM picks targets semantically from screenshots — there are no CSS selectors. + +## When to use this skill + +- **Use this skill** when you need shell-callable computer-use steps (`cua open`, `cua click`, `cua do …`), an interactive TUI, or want to chain browser actions in a shell pipeline. +- **Reach for the `cua-agent` skill** (in the `kernel-sdks` plugin) when you're writing a TypeScript app that needs to embed cua's prompt → screenshot → tool-call loop programmatically. +- **Reach for `kernel-agent-browser`** when you need deterministic browser scripting (semantic selectors, `find role`, `wait --text`, accessibility-tree snapshots). +- **Reach for `kernel-cli`** for raw Kernel browser management (`kernel browsers create`, `kernel browsers exec`, profile / proxy CRUD). + +## Prerequisites + +- A Kernel account and `KERNEL_API_KEY`. See `kernel-cli` for install + auth. +- At least one model-provider API key, matched to the model you pick (table below). +- Node 20+ for the npm install. + +## Install + +```bash +# Global install — puts `cua` on $PATH +npm i -g @onkernel/cua-cli + +# Or zero-install one-shot +npx -y -p @onkernel/cua-cli cua --help +``` + +## Environment variables + +| Env | Used for | +| --- | --- | +| `KERNEL_API_KEY` | Kernel API key (always required) | +| `OPENAI_API_KEY` | OpenAI models (`-m openai:…`) | +| `ANTHROPIC_API_KEY` | Anthropic models (`-m anthropic:…`) | +| `GOOGLE_API_KEY` / `GEMINI_API_KEY` | Google / Gemini models (`-m google:…`) | +| `YUTORI_API_KEY` | Yutori Navigator (`-m yutori:…`) | +| `TZAFON_API_KEY` | Tzafon (`-m tzafon:…`) | +| `KERNEL_BASE_URL` | Override Kernel base URL | +| `XDG_DATA_HOME` | Sessions / transcripts dir (defaults to `~/.local/share`) | +| `CUA_IMAGE_PROTOCOL` | Force inline image protocol (`kitty` / `iterm2` / `none` / `auto`) | + +## One-shot subcommands + +Each call provisions a fresh Kernel browser by default, runs the action, prints a one-line result, and tears the browser down. Chain via `-s ` (next section) to keep state. + +| Subcommand | What it does | Stdout | Exit code | +| --- | --- | --- | --- | +| `cua open ` | Navigate to a URL. | `ok` | 0 ok, 2 error | +| `cua click ""` | Find element matching natural-language description and click it. | `ok clicked (x, y)` or `not_found ` | 0 ok, 1 not_found, 2 error | +| `cua type "" ""` | Focus a field by description and type. | `ok typed` or `not_found ` | 0 ok, 1 not_found, 2 error | +| `cua press [...]` | Send a key combo (`cua press ctrl l`, `cua press Return`). | `ok pressed` | 0 ok, 2 error | +| `cua url` | Print the current URL. | the URL | 0 ok, 2 error | +| `cua observe [""]` | Describe the page; optionally answer a question. | the description | 0 ok, 2 error | +| `cua screenshot --out ` | Save a PNG. `--out -` writes bytes to stdout. | the path or `(stdout)` | 0 ok, 2 error | +| `cua do ""` | Open-ended; agent plans and acts. Bound by `--max-steps` (default 3). | the assistant's final text | 0 ok, 2 error | + +Useful flags: + +- `-m ` — pick the LLM (default `openai:gpt-5.5`). `cua models` to list. +- `--max-steps ` — bound the loop on `cua do`. +- `--profile ` — load a Kernel browser profile for persisted cookies / storage. Existing ids or names are reused; a non-id name is created if missing. Pass `--profile-no-save-changes` for read-only. +- `-v` — verbose progress on stderr (provisioning, tool calls, transcript path). + +`click` and `type` match **semantically**, not by selector — use natural-language descriptions of what's visible on screen. + +The cua CLI always provisions **stealth-on** browsers. If you need non-stealth or a custom viewport / proxy, pre-create the browser via `kernel browsers create` and attach the cua session to it. + +## Named sessions + +Without `-s`, each subcommand provisions a brand-new browser. To keep state across calls, allocate a named session first: + +```bash +cua --profile github session start login # provisions a Kernel browser, prints `name=login` +cua -s login open https://github.com/login +cua -s login type "email field" "$EMAIL" +cua -s login type "password field" "$PASSWORD" +cua -s login click "Sign in" +cua -s login url # prints post-login URL +cua session stop login # tears down the Kernel browser +``` + +Inspect: + +```bash +cua session list # NAME / KERNEL_ID / AGE / LIVE_URL +cua session show login # full JSON metadata +``` + +Pass `--profile` when starting the named session; later `cua -s …` calls attach to the same browser, so they don't need the profile flag again. + +**Liveness**: Kernel browsers time out from inactivity. If you see `error session "" is no longer alive on Kernel …`, run `cua session stop && cua --profile session start ` to re-provision with the same persisted profile. + +Named-session metadata lives in `$XDG_DATA_HOME/cua/named-sessions/.json`. + +## Free-form mode + +```bash +cua --print "open hn and tell me the top story" # one-shot, streams text +cua --print -o jsonl "..." # one-shot, streams JSONL events +cua "..." # interactive TUI (real terminal) +``` + +`--print` exits when the agent finishes; the TUI runs until Ctrl+C. Add `--jsonl-include-deltas` for token deltas, `--jsonl-include-images` for base64 screenshots in `tool_result` events. + +## Model selection + +Run `cua models` for the current catalog. Pick with `-m ` (default `openai:gpt-5.5`). Switch per call or per named session. + +| Model ref | Provider | +| --- | --- | +| `openai:gpt-5.5` | OpenAI (default) | +| `anthropic:claude-opus-4-7` | Anthropic (supports `--thinking off\|minimal\|low\|medium\|high\|xhigh`) | +| `google:gemini-3-flash-preview` | Google / Gemini | +| `yutori:n1.5-latest` | Yutori Navigator | + +Not every provider's native vocabulary includes navigation. If a model can click and type but can't navigate (`goto`, `back`, `forward`, `url`), pick a different model. + +## Live view URL and manual login fallback + +Stealth-on doesn't always beat bot detection. When automation gets stuck on a login, hand off to a human via the live view URL. + +```bash +cua --profile mysite session start login +cua session show login | jq -r .live_url # share this URL with the user +# user logs in manually in the live view +cua -s login url # confirm post-login URL +cua session stop login # profile state saves on teardown +``` + +If you only have a session id (e.g. from `cua session list`), the `kernel` CLI also surfaces it: + +```bash +kernel browsers view +``` + +## Cross-origin iframes / Playwright escape hatch + +cua drives by clicking pixels, so cross-origin iframes (payment forms, embedded vendor widgets) work in the screenshot flow without special handling — the model just clicks them. When you need a deterministic Playwright action against the underlying browser (e.g. fill a card form via a fixed selector), break out to Kernel's exec endpoint with the session id: + +```bash +# Find the session id +cua session show login | jq -r .kernel_session_id + +# Run a Playwright snippet against the same browser +kernel browsers exec --code " + const frame = page.frameLocator('#payment-iframe'); + await frame.locator('#card-number').fill('4111111111111111'); + await frame.locator('#submit').click(); +" +``` + +## Debugging + +- **Verbose stderr**: `cua -v --print "…"` writes provisioning info, tool calls, and the transcript path to stderr. +- **Live event stream**: `cua --print -o jsonl "…"` emits one event per line (`tool_call`, `tool_result`, `assistant_text_done`, etc.). Add `--jsonl-include-images` to inline screenshots in `tool_result`. +- **Persisted transcript**: every `--print`, TUI, and `-s ` invocation appends to `$XDG_DATA_HOME/cua/sessions//.jsonl`. Find the exact path: + ```bash + cua -v --print "..." # stderr includes: [cua] session= + cua session show login | jq -r .transcript_path + ``` + Roles: `user`, `assistant`, `toolResult`. There's also a custom `cua-browser` entry written once per session with `kernel_session_id` / `live_url` / `profile_id`. +- **Screenshots**: `cua screenshot --out shot.png` or inspect `image` blocks in `toolResult` transcript entries. +- **Page URL**: `cua url` to confirm post-action navigation. + +A few `jq` starters against a transcript path: + +```bash +# Every tool call the agent made, in order +jq -c 'select(.role == "assistant") | .content[]? + | select(.type == "tool_use") | {name, input}' "$TRANSCRIPT" + +# Final assistant text (the answer) +jq -r 'select(.role == "assistant") | .content[]? + | select(.type == "text") | .text' "$TRANSCRIPT" | tail -1 +``` + +## Gotchas + +- **Element descriptions are semantic, not selectors.** `cua click "Sign in button"` looks at the screenshot — describe what the user sees, not a CSS selector. +- **Viewport defaults to 1920x1080.** Pre-create the browser with `kernel browsers create` if you need something else. +- **Keyboard navigation > mouse-wheel scroll.** `cua press Page_Down` / `Home` / arrow keys is more reliable than scroll wheel via the LLM. +- **Multi-step state requires `-s `.** A second one-shot subcommand can't see what the first one did. +- **Profile saves on close, not continuously.** Tear down cleanly with `cua session stop` or you'll lose recent state. +- **`--max-steps` defaults to 3 on `cua do`.** Bump it for non-trivial tasks. + +## Quick reference + +```bash +# One-shot, fresh browser +cua --print "open hn and tell me the top story" + +# Named session for multi-step +cua --profile mysite session start work +cua -s work open https://example.com +cua -s work click "Log in" +cua -s work type "email field" "$EMAIL" +cua -s work click "Submit" +cua -s work url +cua session stop work + +# List models, switch model per call +cua models +cua --print -m anthropic:claude-opus-4-7 "..." + +# Get the live view URL +cua session show work | jq -r .live_url +kernel browsers view # alternative + +# Drop to Playwright for deterministic actions +cua session show work | jq -r .kernel_session_id +kernel browsers exec --code "..." +``` diff --git a/plugins/kernel-sdks/skills/cua-agent/SKILL.md b/plugins/kernel-sdks/skills/cua-agent/SKILL.md new file mode 100644 index 0000000..46696a3 --- /dev/null +++ b/plugins/kernel-sdks/skills/cua-agent/SKILL.md @@ -0,0 +1,293 @@ +--- +name: cua-agent +description: Build TypeScript apps that embed Kernel's computer-use loop with `@onkernel/cua-agent` — `CuaAgent` and `CuaAgentHarness` classes drive a Kernel cloud browser via prompt → screenshot → tool-call loops across OpenAI, Anthropic, Google, and Yutori provider tools. Use when writing TS code that needs computer-use against a Kernel browser, swapping providers mid-session, adding your own pi tools alongside computer use, or hooking into the agent event stream. For shell-callable cua, see `cua-cli`. +--- + +# cua-agent + +`@onkernel/cua-agent` ships two TS classes for running a computer-use loop against a Kernel cloud browser: + +- **`CuaAgentHarness`** — recommended entry point. Session-backed turns, `setModel` mid-conversation, steering / follow-up, `subscribe()` event stream. Extends pi-agent-core's `AgentHarness`. +- **`CuaAgent`** — lower-level. Direct `state.messages` access, custom streaming, explicit prompt/continue/queue. Extends pi-agent-core's `Agent`. + +Both translate per-provider computer-use tool calls (OpenAI's `computer`, Anthropic's `computer_20251124`, Gemini's normalized-coordinate functions, Yutori Navigator's browser actions) into Kernel SDK `browsers.computer.*` calls and feed a fresh screenshot back to the model on every turn. + +## When to use this skill + +- **Use this skill** when writing TS code that embeds cua inside a larger app, needs a custom session repo, runs its own pi tools alongside computer use, or reacts to per-event streams programmatically. +- **Reach for the `cua-cli` skill** (in the `kernel-cli` plugin) when shell-callable computer-use is enough (`cua open`, `cua click`, `cua do`). +- **Reach for `kernel-typescript-sdk`** for raw Playwright / CDP control over a Kernel browser without an LLM in the loop. + +## Prerequisites + +- A Kernel account and `KERNEL_API_KEY`. +- At least one model-provider API key, matched to the model you pick (table below). +- Node 20+, TypeScript app or `tsx` runner. + +## Install + +```bash +npm i @onkernel/cua-agent @onkernel/cua-ai @onkernel/sdk +``` + +The three packages divide responsibility: + +- `@onkernel/cua-agent` — `CuaAgent` / `CuaAgentHarness` execution loop. +- `@onkernel/cua-ai` — model catalog (`getCuaModel` / `listCuaModels`), canonical CUA tool schemas, per-provider adapters. +- `@onkernel/sdk` — Kernel SDK client used to provision the browser. + +Both classes re-export the full pi-agent-core surface from `@onkernel/cua-agent`, including `NodeExecutionEnv` (via the `/node` subpath under the hood) and `InMemorySessionRepo`. Import them from `@onkernel/cua-agent` directly. + +## Environment variables + +If you don't pass explicit auth callbacks, both classes resolve provider keys via `@onkernel/cua-ai`'s `getCuaEnvApiKey`: + +| Env | Used for | +| --- | --- | +| `KERNEL_API_KEY` | Kernel API key (always required) | +| `OPENAI_API_KEY` | `openai:…` models | +| `ANTHROPIC_API_KEY` or `ANTHROPIC_OAUTH_TOKEN` | `anthropic:…` models | +| `GOOGLE_API_KEY` or `GEMINI_API_KEY` | `google:…` models | +| `YUTORI_API_KEY` | `yutori:…` models | +| `TZAFON_API_KEY` | `tzafon:…` models | + +## Quick start — `CuaAgentHarness` + +```ts +import Kernel from "@onkernel/sdk"; +import { + CuaAgentHarness, + InMemorySessionRepo, + NodeExecutionEnv, +} from "@onkernel/cua-agent"; +import type { AssistantMessage } from "@onkernel/cua-ai"; + +const client = new Kernel({ apiKey: process.env.KERNEL_API_KEY! }); +const browser = await client.browsers.create({ stealth: true }); + +const repo = new InMemorySessionRepo(); +const session = await repo.create({ id: "research" }); + +const harness = new CuaAgentHarness({ + browser, + client, + env: new NodeExecutionEnv({ cwd: process.cwd() }), + model: "openai:gpt-5.5", + session, +}); + +const textOf = (m: AssistantMessage) => + m.content.flatMap((b) => (b.type === "text" ? [b.text] : [])).join("").trim(); + +const first = await harness.prompt("Open example.com and describe what you see."); +console.log(textOf(first)); + +// Swap providers mid-session — CUA tools and the default prompt refresh. +await harness.setModel("anthropic:claude-opus-4-7"); +const second = await harness.prompt("Open the most relevant link from what you found."); +console.log(textOf(second)); + +await client.browsers.deleteByID(browser.session_id); +``` + +While a turn is running: `steer()` injects course corrections, `followUp()` queues the next instruction, `subscribe()` streams underlying agent events, and `compact()` collapses long transcripts. See [`@earendil-works/pi-agent-core`](https://www.npmjs.com/package/@earendil-works/pi-agent-core) for the full harness lifecycle. + +## `CuaAgent` for raw pi `Agent` semantics + +Reach for `CuaAgent` when you want direct control — `state.messages` access, custom streaming, explicit prompt/continue/queue, no session repo. Same constructor shape except you assign `agent.state.model = …` instead of calling `setModel()`. + +```ts +import { CuaAgent } from "@onkernel/cua-agent"; + +const agent = new CuaAgent({ + browser, + client, + initialState: { + model: "openai:gpt-5.5", + systemPrompt: "You are a careful browser automation agent.", + }, +}); + +agent.subscribe((event) => { /* … */ }); +await agent.prompt("Open news.ycombinator.com and summarize the top story."); +``` + +### Harness vs Agent + +| You want to … | Use | +| --- | --- | +| Session-backed turns persisted to a repo | `CuaAgentHarness` | +| Steering, follow-up queue, compaction, branching | `CuaAgentHarness` | +| `await setModel()` mid-conversation | `CuaAgentHarness` | +| Direct `state.messages` access, no session machinery | `CuaAgent` | +| Custom streaming + explicit `prompt`/`continue`/`queue` control | `CuaAgent` | + +## Model selection and switching + +Run `listCuaModels()` from `@onkernel/cua-ai` for the current catalog. Pass either a CUA model ref (e.g. `"openai:gpt-5.5"`) or a concrete pi `Model` — both shape-widen the same options field. + +| Model ref | Provider | Notes | +| --- | --- | --- | +| `openai:gpt-5.5` | OpenAI | Built-in `computer` tool | +| `anthropic:claude-opus-4-7` | Anthropic | Built-in `computer_20251124` tool | +| `google:gemini-3-flash-preview` | Google | Predefined CU functions, 0–1000 normalized coords | +| `yutori:n1.5-latest` | Yutori | OpenAI-compatible chat with browser action tool calls | + +Switching: + +```ts +// Harness — async, updates via pi snapshot machinery +await harness.setModel("anthropic:claude-opus-4-7"); + +// Agent — direct assignment +agent.state.model = "anthropic:claude-opus-4-7"; +``` + +In both cases CUA-owned tools and the default system prompt refresh for the next provider request. + +Not every provider's native vocabulary includes navigation (`goto`, `back`, `forward`, `url`). Pass `computerUseExtra: true` to add the provider-neutral `computer_use_extra` tool when the model can click/type but can't navigate. + +## Browser provisioning + +You own the Kernel browser lifecycle — provision before constructing the agent, tear down after: + +```ts +const browser = await client.browsers.create({ + stealth: true, // bypass most fingerprinting; default off + headless: false, // headful => live view URL; headless => no live view, smaller image + timeout: 1800, // seconds before Kernel auto-times-out the browser + profile: { name: "github", save_changes: true }, + // proxy: { ... }, +}); + +try { + // ... use browser with harness/agent ... +} finally { + await client.browsers.deleteByID(browser.session_id); +} +``` + +The `browser.browser_live_view_url` field on the create response is the URL to share when you need a human to take over (manual login on a stealth-blocked site, captcha, etc.). + +## Adding your own tools + +Pass any pi `AgentTool` (see [`@earendil-works/pi-agent-core`](https://www.npmjs.com/package/@earendil-works/pi-agent-core) for the tool shape) via `extraTools`. The CUA defaults stay installed; your tools run alongside them. + +```ts +import type { AgentTool } from "@onkernel/cua-agent"; +import { CuaAgentHarness } from "@onkernel/cua-agent"; + +const lookupOrder: AgentTool = { + // shape per pi-agent-core docs: name, description, schema, run, ... +}; + +const harness = new CuaAgentHarness({ + browser, client, + model: "openai:gpt-5.5", + session, + env: new NodeExecutionEnv({ cwd: process.cwd() }), + extraTools: [lookupOrder], + computerUseExtra: true, +}); +``` + +If you want to compose the tool list yourself (e.g. wrap computer-use tools in a permission gate), reach for `createCuaComputerTools()`: + +```ts +import { resolveCuaRuntimeSpec } from "@onkernel/cua-ai"; +import { createCuaComputerTools } from "@onkernel/cua-agent"; + +const runtime = resolveCuaRuntimeSpec("openai:gpt-5.5"); +const tools = [ + ...createCuaComputerTools({ browser, client, toolExecutors: runtime.toolExecutors }), + lookupOrder, +]; +``` + +## Manual login handoff via live view URL + +Every Kernel browser response carries the live view URL on creation. When stealth doesn't beat bot detection, share that URL and wait for the human: + +```ts +const browser = await client.browsers.create({ + stealth: true, + headless: false, + profile: { name: "mysite", save_changes: true }, +}); +console.log("share with user:", browser.browser_live_view_url); + +// wait for user signal — e.g. a button, stdin, an HTTP callback — +// THEN start prompting the agent against the logged-in browser +await harness.prompt("Now click 'Settings' and read me the current value of X."); +``` + +Profile saves on browser teardown, so future runs with the same profile name skip the manual login. + +## Cross-origin iframes / Playwright escape hatch + +cua drives by clicking pixels, so cross-origin iframes work in the screenshot flow without special handling. When you need a deterministic Playwright action against the underlying browser (e.g. fill a card form via a fixed selector), drop to the Kernel SDK's exec endpoint with the session id you already have: + +```ts +await client.browsers.exec(browser.session_id, { + code: ` + const frame = page.frameLocator('#payment-iframe'); + await frame.locator('#card-number').fill('4111111111111111'); + await frame.locator('#submit').click(); + `, +}); +``` + +## Debugging + +- **`subscribe()`** — the harness and agent both stream pi-agent-core events. Use it to log tool calls, screenshot sizes, tokens: + ```ts + harness.subscribe((event) => { + if (event.type === "tool_call") console.log("tool:", event.toolName); + if (event.type === "assistant_text_done") console.log("text:", event.text); + }); + ``` +- **`agent.state.messages`** — full message history including image blocks (for `CuaAgent`). Inspect after a turn finishes. +- **Live view URL** — `browser.browser_live_view_url` lets you watch the agent work in real time, even headful. +- **Custom session repo** — implement pi-agent-core's `SessionRepo` interface to persist transcripts wherever you want (JSONL on disk, S3, a DB). + +## Gotchas + +- **You own the browser lifecycle.** Always tear down with `client.browsers.deleteByID(browser.session_id)` in a `finally` block — Kernel timeouts will reclaim eventually but profile state saves on close, not continuously. +- **`setModel` is async.** It propagates through pi's snapshot machinery — `await` it before the next `prompt()`. +- **Provider tool vocab gaps.** If a model can click and type but can't navigate, set `computerUseExtra: true` to add provider-neutral `goto` / `back` / `forward` / `url`. +- **`InMemorySessionRepo` is in-process only.** Reach for a persistent `SessionRepo` implementation if you need transcripts to survive restarts. +- **`extraTools` runs alongside CUA tools, not in place of them.** To replace the defaults, build the tool list with `createCuaComputerTools()` yourself. +- **Stealth, headless, viewport, proxy** are all `browsers.create` flags — set them when provisioning, not on the harness. + +## Quick reference + +```ts +import Kernel from "@onkernel/sdk"; +import { + CuaAgentHarness, + InMemorySessionRepo, + NodeExecutionEnv, +} from "@onkernel/cua-agent"; + +const client = new Kernel({ apiKey: process.env.KERNEL_API_KEY! }); +const browser = await client.browsers.create({ stealth: true }); + +const session = await new InMemorySessionRepo().create({ id: "main" }); + +const harness = new CuaAgentHarness({ + browser, client, session, + env: new NodeExecutionEnv({ cwd: process.cwd() }), + model: "openai:gpt-5.5", + computerUseExtra: true, +}); + +harness.subscribe((event) => { /* ... */ }); + +try { + const first = await harness.prompt("Open example.com and click the first link."); + await harness.setModel("anthropic:claude-opus-4-7"); + const second = await harness.prompt("Now extract the page title."); +} finally { + await client.browsers.deleteByID(browser.session_id); +} +```