Skip to content
Merged
1 change: 1 addition & 0 deletions src/content/guides/_meta.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { MetaRecord } from 'nextra'

const meta: MetaRecord = {
'lightpanda-agent-tutorial': 'Use Lightpanda Agent',
'retrieve-an-html-webpage': 'Retrieve an HTML webpage',
'configure-a-proxy': 'Configure a proxy',
'markdown-axtree': 'Get Markdown and AX Tree',
Expand Down
10 changes: 5 additions & 5 deletions src/content/guides/configure-a-proxy.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ proxy. Ensure your proxy address starts with `http://` or `https://`.

Use a local proxy with the `fetch` command:
```sh copy
./lightpanda fetch --http-proxy http://127.0.0.1:3000 https://lightpanda.io
lightpanda fetch --http-proxy http://127.0.0.1:3000 https://lightpanda.io
```
Or configure the proxy with `serve` for the CDP server. All outgoing requests will use the proxy.
```sh copy
./lightpanda serve --http-proxy http://127.0.0.1:3000
lightpanda serve --http-proxy http://127.0.0.1:3000
```

### HTTP proxy with basic auth
Expand All @@ -30,7 +30,7 @@ for the proxy using the `username:password@` format in the proxy address. It
works for both `fetch` and `serve` commands.

```sh copy
./lightpanda fetch --http-proxy 'http://me:my-password@127.0.0.1:3000' https://lightpanda.io
lightpanda fetch --http-proxy 'http://me:my-password@127.0.0.1:3000' https://lightpanda.io
```

### HTTP proxy with bearer auth
Expand All @@ -43,7 +43,7 @@ to authenticate with the `--proxy-bearer-token`. It works for both `fetch` and
This option will add a `Proxy-Authorization` header all the outgoing requests.

```sh copy
./lightpanda fetch --http-proxy 'http://127.0.0.1:3000' --proxy-bearer-token 'MY-TOKEN' https://lightpanda.io
lightpanda fetch --http-proxy 'http://127.0.0.1:3000' --proxy-bearer-token 'MY-TOKEN' https://lightpanda.io
```

## Configure a proxy from your Puppeteer/Playwright script
Expand All @@ -57,7 +57,7 @@ With Puppeteer, you have to configure the proxy address when starting
Lightpanda.

```sh copy
./lightpanda fetch --http-proxy 'http://127.0.0.1:3000'
lightpanda fetch --http-proxy 'http://127.0.0.1:3000'
```

Then you can call `page.authenticate` function to inject your authentication
Expand Down
303 changes: 303 additions & 0 deletions src/content/guides/lightpanda-agent-tutorial.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,303 @@
---
title: Use Lightpanda Agent
description: Use Lightpanda Agent to create a reproducible JavaScript browser script.
---

# Use Lightpanda Agent

This tutorial will walk you through how to use Lightpanda Agent to create a
reproducible JavaScript browser script.

See [reference](/run-locally/commands/agent) for flags and commands.

## Prerequisites

- Lightpanda installed and on your `PATH`.
- A Hacker News account.
- An LLM API key for the natural-language sections (Anthropic, OpenAI,
Gemini, Hugging Face or a local Ollama). Recorded `.js` PandaScripts need no key.

Export your HN credentials as `LP_*` env vars. The `LP_` prefix
matters for security: Lightpanda only resolves these placeholders
inside its own subprocess (so your password never reaches the LLM),
and the `getEnv` tool refuses any variable that doesn't start with
`LP_` (so the LLM can't read your other secrets).

```sh copy
export LP_HN_USERNAME="your-hn-handle"
export LP_HN_PASSWORD="your-hn-password"
```

Check the variables are set. If one is missing, `/fill` will silently
type the literal `$LP_HN_USERNAME` into the form rather than your
username:

```sh copy
lightpanda agent --no-llm
```
```console
> /getEnv LP_HN_USERNAME
```

## 1. Start the REPL

```sh copy
lightpanda agent
```

The startup banner shows whether natural language is available;
`/model` prints the resolved model. REPL history lives in `.lp-history`
in the working directory.

```console
> /help # list every browser tool
> /help goto # JSON schema for one tool
> /quit
```

No API key? `lightpanda agent --no-llm` runs the slash-commands-only
REPL.

## 2. Run a single task

Before doing anything complicated, run a one-shot task to check
everything works:

```sh copy
lightpanda agent --task "what is the top story on news.ycombinator.com?"
```

`--task` runs one user turn, prints the answer on stdout, exits.
Tool calls and progress go to stderr, so redirecting gives you a
clean answer:

```sh copy
lightpanda agent --task "top story on news.ycombinator.com?" > out.txt
```

Use `-a <path>` (repeatable) to attach local files.

## 3. Log in to Hacker News

Paste these into the REPL in order:

```console
> /goto https://news.ycombinator.com/login
> /fill selector='form[action="login"] input[name="acct"]' value='$LP_HN_USERNAME'
> /fill selector='form[action="login"] input[name="pw"]' value='$LP_HN_PASSWORD'
> /click selector='form[action="login"] input[type="submit"][value="login"]'
> /waitForSelector '#logout'
```

A few things worth knowing:

- **Slash commands only.** `click '#foo'` is forwarded to the LLM;
only `/click '#foo'` runs as a command. TAB completes tool names.
- **`/waitForSelector '#logout'` is both a sync point and an
assertion.** It blocks until HN's logged-in DOM renders. If the
credentials are wrong (or the website throws a captcha, or the layout
changes), the command times out at the line where the failure
actually happened. Use this pattern after every state-changing
action.
- **Selectors are CSS only.** The click-family tools (`/click`,
`/fill`, `/hover`, `/selectOption`, `/setChecked`) accept CSS
only. Backend node IDs are invalidated by any DOM mutation and
can't be serialized into recordings.

Confirm the login worked:

```console
> /extract '{"karma": "#karma"}'
{"karma":"42"}
```

`/extract` takes a JSON schema and prints one JSON object to stdout.
Schema grammar:

- `"<sel>"`: text of the first match.
- `["<sel>"]`: text of every match.
- `{"selector": "<sel>", "attr": "<name>"}`: attribute of the first match.
- `[{"selector": "<sel>", "fields": {…}}]`: array of records, with
each `fields` entry resolved relative to the matched element.

Now go to the front page and pull the story list:

```console
> /goto https://news.ycombinator.com
> /extract '''
{
"topStories": [{
"selector": ".athing",
"fields": {
"rank": ".rank",
"title": ".titleline > a",
"url": {"selector": ".titleline > a", "attr": "href"}
}
}]
}
'''
```

Triple-quoted values let a schema span multiple lines.

### How we got those selectors

Skip this if you're happy treating them as given.

`/tree` prints the semantic tree. On the login page you'll see two
forms (login and signup) with unlabeled textboxes, which means
`/findElement role=textbox name=username` returns nothing.

`/detectForms` reads the HTML directly and surfaces each form's
`action` plus each input's `name`. The first form has `action: "login"`
and fields named `acct` and `pw`, which gives us the form-scoped
selector (`form[action="login"] input[name="acct"]`) that won't
collide with signup.

`/nodeDetails backendNodeId=<n>` is the alternative: it returns a
ready-to-use CSS selector for any node ID from `/tree`.

## 4. Save the session as a script

Retype the login plus front-page sequence in a single REPL session,
then:

```console
> /save hn_login.js
> /quit
```

In `--no-llm` mode, `/save` transcribes the session deterministically.
With an LLM, it synthesizes an idiomatic script. The result is
JavaScript:

```js copy
const page = new Page();
await page.goto("https://news.ycombinator.com/login");
page.fill({ selector: "form[action=\"login\"] input[name=\"acct\"]", value: "$LP_HN_USERNAME" });
page.fill({ selector: "form[action=\"login\"] input[name=\"pw\"]", value: "$LP_HN_PASSWORD" });
page.click({ selector: "form[action=\"login\"] input[type=\"submit\"][value=\"login\"]" });
page.waitForSelector("#logout");
await page.goto("https://news.ycombinator.com");
return page.extract({ topStories: [{ selector: ".athing", fields: { rank: ".rank", title: ".titleline > a", url: { selector: ".titleline > a", attr: "href" } } }] });
```

Only state-mutating commands are recorded; read-only ones (`/tree`,
`/markdown`) are dropped. `/extract` is recorded because it shapes
what the script returns.

## 5. Replay the script without an LLM

```sh copy
lightpanda agent hn_login.js
```

No `--provider`, no API key, no token spend. Whatever the script
`return`s is printed automatically as JSON. Because the saved script
ends with `return page.extract(...)`, you get clean JSON on stdout:

```sh copy
lightpanda agent hn_login.js > stories.json
```

From inside the REPL, `/load hn_login.js` runs the same script against
the current session.

To reshape the output, assign the result and end with `return <value>`
(what you return is what prints):

```js copy
const page = new Page();
await page.goto("https://news.ycombinator.com");

const topStories = page.extract({
topStories: [{
selector: ".athing",
fields: {
rank: ".rank",
title: ".titleline > a",
url: { selector: ".titleline > a", attr: "href" }
}
}]
});

return topStories;
```

## 6. Add your own JavaScript logic

Agent scripts run in a separate JavaScript context from the page. No
`window`, `document`, DOM API, `require`, or `process`. Browser
interaction happens through the `Page` object: `new Page()` makes a
page, `await page.goto(url)` navigates it, and every other primitive is
a synchronous method on it.

Use `page.extract(...)` to move page data into local logic, then
process it with normal JavaScript:

```js copy
const page = new Page();
await page.goto("https://news.ycombinator.com");

const topStories = page.extract({
topStories: [{
selector: ".athing",
limit: 5,
fields: {
rank: ".rank",
title: ".titleline > a",
url: { selector: ".titleline > a", attr: "href" }
}
}]
});

return topStories.map((s) => ({ rank: s.rank, title: s.title, url: s.url }));
```

Use `page.evaluate(...)` only when you intentionally want a string to
run in the page's JavaScript context. Page evaluate cannot see agent
variables or call agent primitives.

## 7. Use Lightpanda from another agent (MCP)

If you're driving Lightpanda from a different agent (Claude Code, a
custom MCP client, your own harness), use `lightpanda mcp` instead.
The calling agent supplies the LLM, so Lightpanda needs no API key.

```json copy
{
"mcpServers": {
"lightpanda": {
"command": "/path/to/lightpanda",
"args": ["mcp"]
}
}
}
```

Drive the browser with the usual tools (`goto`, `fill`, `click`,
`waitForSelector`), then hand back a script with the `save` tool:

```json
{
"tool": "save",
"args": {
"path": "hn_login.js",
"script": "const page = new Page();\nawait page.goto(\"...\");\n..."
}
}
```

The path must be relative and free of `..`. Literal `LP_*` values are
scrubbed back to placeholders before the file is written. The output
runs unmodified:

```sh copy
lightpanda agent hn_login.js
```

## Further reading

- [Agent](/usage/agent): full documentation reference,
- [PandaScript](/usage/pandascript): runtime, primitives,
return values.
Loading
Loading