lightpanda-io/agent-skill/pandascript

Claude Code and agent skill by lightpanda-io/agent-skill

Trust tier 3
  • community
Source check pending

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/pandascript

SKILL.md

Writing Lightpanda agent scripts

Run with:

console
./lightpanda run script.js

Mental model (get this right first)

The script runs in its own V8 context — neither the page nor Node.js:

  • Page is the only global. new Page() makes a page and await 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 with page.extract(...), run page-side JS only via page.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({...}), never await page.extract(...). The script body runs as an async function, so top-level await is allowed.
  • Re-navigating reuses the same page: await page.goto(url2) keeps page valid and points it at the new URL, discarding the old page — read it before navigating away. Independent URLs don't share a page: make a new 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({...}); or return results;. A bare trailing expression is NOT printed; neither is console.log(JSON.stringify(...)).

Primitives

Page is the only global; new Page() makes a page and everything else is a method on it.

CallNotes
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 as lp.<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, as pageTools lists it. arguments — Optional. The tool's input, matching its inputSchema. Defaults to {}.
  • classify: selector — Optional CSS selector to classify a specific element subtree instead of the full page presets — 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):

js
const { stories } = page.extract({
stories: [{
selector: "tr.athing", // one record per match
limit: 5,
fields: { // resolved relative to each match
title: ".titleline > a", // first match's text (null if missing)
url: { selector: ".titleline > a", attr: "href" },
text: "" // "" = the matched element's own text
}
}]
});
  • "sel" → first match's text; ["sel"] → all matches' text; { selector, attr } / [{ selector, attr }] → attribute(s); limit: N caps 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

  1. Navigate, settle, read. After await page.goto on a dynamic page (feeds, search results, comment threads), call page.waitForState("networkidle") or page.waitForSelector(...) before extracting. Most static pages are complete at load — don't wait blindly.
  2. 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

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
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/lightpanda-io/agent-skill/pandascript/badge)](https://verified-skill.com/skills/lightpanda-io/agent-skill/pandascript)

Something wrong with this skill or its page? Tell us.