lightpanda-io/agent-skill/pandascript
Claude Code and agent skill by lightpanda-io/agent-skill
- community
Checking that the SKILL.md file is still in the repository.
Write PandaScript agent scripts (.js) — Lightpanda's replayable browser-automation format, run token-free with `lightpanda run script.js`.
Install
npx vskill@latest install lightpanda-io/agent-skill/pandascriptSKILL.md
Writing Lightpanda agent scripts
Run with:
Mental model (get this right first)
The script runs in its own V8 context — neither the page nor Node.js:
Pageis the only global.new Page()makes a page andawait page.goto(url)navigates it; every other primitive is a method on that page:const page = new Page(); await page.goto(url); page.extract({...}); page.click(sel);.- No
window,document, DOM,localStorage— read pages withpage.extract(...), run page-side JS only viapage.evaluate("..."). - No
require,process,fs, npm. Standard ECMAScript built-ins only (JSON,Map, template literals, …). page.goto(...)is async — always `await` it. Page methods are synchronous:const data = page.extract({...}), neverawait page.extract(...). The script body runs as an async function, so top-levelawaitis allowed.- Re-navigating reuses the same page:
await page.goto(url2)keepspagevalid and points it at the new URL, discarding the old page — read it before navigating away. Independent URLs don't share a page: make anew Page()for each and load them in parallel (fan-out, best practice 2). - Page
evaluate("...")cannot see script variables — interpolate values into the string. Script code cannot see page variables. - Variables persist across navigations within one run, so cross-page aggregation is plain JS.
- `return <value>` is the script's output, printed automatically (objects/arrays as JSON). End with
return page.extract({...});orreturn results;. A bare trailing expression is NOT printed; neither isconsole.log(JSON.stringify(...)).
Primitives
Page is the only global; new Page() makes a page and everything else is a method on it.
| Call | Notes | |
|---|---|---|
new Page() | Makes a page object. No navigation yet — call page.goto(url) before any other method. Make several to navigate in parallel (fan-out, best practice 2). | |
page.close() | Marks the page done; later method calls on it error. The page is otherwise reclaimed at script end. | |
await page.goto(url[, { timeout, waitUntil }]) | Async — must be `await`ed. Navigates the page (re-navigating reuses the same object). Waits for load. Rejects on navigation failure; a timeout does NOT reject (the page may still be usable). Default timeout 10000 ms. waitUntil: one of "load", "domcontentloaded", "networkalmostidle", "networkidle", "done". | |
page.screenshot([{ path, selector, fullPage, strip, url, timeout }]) | path is required: writes a PNG of the text layout. | |
page.evaluate(script[, { url, timeout, save }]) | Page-side JS escape hatch; returns text (JSON for objects/arrays). | |
page.extract(schema) | The only primitive returning a real JS value (object/array). The schema is its only argument. See schema below. | |
page.click(selector) | ||
page.fill(selector, value) | ||
page.scroll([{ selector, x, y }]) | ||
page.waitForSelector(selector[, { timeout }]) | waitFor* default timeout 5000 ms. | |
page.waitForScript(script[, { timeout }]) | Re-evaluates page JS until truthy. | |
page.waitForState(state[, { timeout }]) | state: one of "load", "domcontentloaded", "networkalmostidle", "networkidle", "done". | |
page.hover(selector) | ||
page.press(selector, key) | Selector first! page.press("Enter") binds "Enter" to selector and fails — use page.press(null, "Enter") or page.press({ key: "Enter" }). | |
page.selectOption(selector, value) | ||
page.setChecked(selector[, checked]) | checked defaults to true. | |
page.download([{ url, path }]) | Sends the session's cookies, so authenticated downloads work. Nothing is written unless the status is 200-203, and a failed download leaves path as it was. | |
page.callPageTool([{ name, arguments }]) | Runs a tool the page registered through WebMCP; returns its text result. | |
page.classify(questions[, { selector, presets }]) | Classifies page state or candidate elements via TypeSafe Jev: an array of categories, or an object of presets and questions. Replay calls Jev live and needs TYPESAFE_API_KEY. | |
page.acceptCookies([{ prefer, selectors }]) | Clicks the consent control Jev picks, then re-reads the page to check the banner went. Pass the selectors from an earlier result to click them with no model call; otherwise needs TYPESAFE_API_KEY. prefer: one of "accept", "reject". | |
page.login([{ password, username, usernameSelector, passwordSelector, submitSelector, nextSelector, decideOnly }]) | Fills the sign-in fields Jev picks and submits. Credentials go in as $LP_* placeholders. Pass the *Selector fields from an earlier result to replay with no model call; otherwise needs TYPESAFE_API_KEY. |
Options (the trailing { … } object; every option may be omitted):
goto:timeout— Optional timeout in milliseconds. Defaults to 10000.waitUntil— Event that completes the navigation. Defaults to 'load'. Prefer 'domcontentloaded' followed by waitForSelector on pages whose late scripts (ads) hold 'load' back. Avoid 'done' (full quiescence): on pages with constant background activity it is the slowest choice and can run to the timeout.screenshot:path— Optional relative path (no '..' segments) to write the PNG to. Created or overwritten. Without it the image is returned inline, which needs a client that can display images.selector— Optional CSS selector. When set, render only that element.fullPage— Render the whole content height instead of one viewport. Defaults to false.strip— Optional. Omit element groups from the render; same groups as the html tool's strip (js,css,ui,invisible,shell,clutter).url— Optional URL to navigate to before rendering. An empty string is treated as omitted.timeout— Optional timeout in milliseconds. Defaults to 10000.evaluate:url— Optional URL to navigate to before evaluating. An empty string is treated as omitted.timeout— Optional timeout in milliseconds. Defaults to 10000.save— Optional bridge-store key. The evaluate's return value is stored under this name and re-exposed aslp.<name>to subsequent evaluates. Objects, arrays, and strings are serialized automatically — no JSON.stringify needed.scroll:selector— Optional: CSS selector of the element to scroll. Preferred over backendNodeId. If the element is not itself a scroll container, its nearest scrollable ancestor is scrolled instead.x— Optional: The horizontal scroll offset.y— Optional: The vertical scroll offset.waitForSelector:timeout— Optional timeout in milliseconds. Defaults to 5000, or 15000 when the page has not reached 'load' yet.waitForScript:timeout— Optional timeout in milliseconds. Defaults to 5000, or 15000 when the page has not reached 'load' yet.waitForState:timeout— Optional timeout in milliseconds. Defaults to 5000.download:url— Absolute http(s) URL to fetch.path— Relative path (no '..' segments) to save the response body to. Created or overwritten. The response reports the absolute location.callPageTool:name— Tool name, aspageToolslists it.arguments— Optional. The tool's input, matching itsinputSchema. Defaults to {}.classify:selector— Optional CSS selector to classify a specific element subtree instead of the full pagepresets— Also ask every preset in the same call, each answered under its own name beside the questions. Needs the object form.acceptCookies:prefer— Whether to accept the cookies or refuse all non-essential ones. Defaults to 'accept'.selectors— Optional CSS selectors of the controls to click in order, from an earlier call's result. Clicked without asking the model; one that is not on the page stops the clicking there.login:password— The password, as a $LP_ placeholder. `username` — The username, email or other identifier, as a $LP_ placeholder. Omit on a page that asks only for the password.usernameSelector— Optional CSS selector of the identifier field, from an earlier call's result. Given with passwordSelector, the fields are filled with no model call.passwordSelector— Optional CSS selector of the password field, from an earlier call's result. Must name an input[type=password].submitSelector— Optional CSS selector of the control that submits the form. Without it, Enter is pressed in the filled field.nextSelector— Optional CSS selector of the control that submits the username on a page that asks for the password on a later step. Without it, Enter is pressed in the username field.decideOnly— Report which fields would be used and type nothing. For checking the picks on a page without sending credentials.
Calling convention: leading positionals + optional trailing options object, or one object with everything (page.waitForSelector("#row", { timeout: 2000 }) ≡ page.waitForSelector({ selector: "#row", timeout: 2000 })). A bare option positional (page.waitForSelector("#row", 2000)) and a field passed both ways are invalid arguments. null skips a positional. Arguments must be JSON-serializable.
CSS selectors only — backendNodeIds don't exist here. Standard CSS only: no jQuery :contains() or Playwright :has-text().
extract schema
Keys = output field names; values pick what to lift (not a JSON Schema):
"sel"→ first match's text;["sel"]→ all matches' text;{ selector, attr }/[{ selector, attr }]→ attribute(s);limit: Ncaps any array form.- Every value is a string or null — parse numbers in script logic.
- Empty arrays are valid results; if every field misses, extract throws ("no schema selector matched any element") → your selectors are wrong, not the page empty.
- An object schema always returns an object (destructure it); a bare array schema returns the array directly.
- No
save:option in scripts — keep results in variables.
Best practices
- Navigate, settle, read. After
await page.gotoon a dynamic page (feeds, search results, comment threads), callpage.waitForState("networkidle")orpage.waitForSelector(...)before extracting. Most static pages are complete atload— don't wait blindly. - List-to-detail — fan out independent pages. Extract the list, then open one page per item and start every navigation together so the detail pages load in parallel instead of one-after-another:
```js const list = new Page(); await list.goto(listUrl); const { items } = list.extract({ items: [{ selector: "a.row", fields: { url: { attr: "href" } } }] });
This is the start of the file. Read the full SKILL.md on GitHub.
Security
- Security scanPassed, 95 of 100Pattern checks on the skill's files.
- Model reviewPassed
Quality evaluation
- Task evaluationNot runNo evaluation yet. Run one in Skill Studio.
Source
- SKILL.md in the repositoryCheckingpandascript/SKILL.md
Popularity
- Repository stars
- 97
- Repository forks
- 10
- 7-day trend score
- 23
Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.
Works with
Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.
- Claude Code
- Cursor
- GitHub Copilot
- Windsurf
- Codex
- Gemini CLI
- Cline
- Amp
- Roo Code
- Goose
- Continue
- Kilo Code
- Junie
- Trae
- OpenHands
- Qwen Code
- AI Agent
- Tabnine Chat
- Sourcegraph Cody
- Amazon Q
- Firebase Genkit
- Sweep
- Devin
- Phind
- Mentat
- Claude Desktop
- Aider
- GPT Pilot
- Plandex
- bolt.new
- v0
- Replit Agent
- Anse
- TypingMind
- BetterChatGPT
- LibreChat
- Warp AI
- Pieces
- Fig AI
Embed badge
[](https://verified-skill.com/skills/lightpanda-io/agent-skill/pandascript)