From 120a99ad2a2a6dbd150676041369c08d608b7919 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?In=C3=AAs=20Moital?= Date: Mon, 2 Nov 2026 18:17:51 +0000 Subject: [PATCH 1/9] build: let a host keep its own validate and package scripts platforms//scripts/ is now reserved like skills/: render never copies it into dist/. npm run validate runs platforms//scripts/validate.mjs on dist/ when it exists, and npm run package -- runs its package.sh. The schema check also ignores "extensions" on an MCP server entry, where OpenAI declares OAuth; the Agent Plugins MCP schema doesn't list it. --- CONTRIBUTING.md | 4 +++- package.json | 3 ++- scripts/lib.mjs | 5 +++-- scripts/package.mjs | 18 ++++++++++++++++++ scripts/validate.mjs | 28 +++++++++++++++++++++++++++- 5 files changed, 53 insertions(+), 5 deletions(-) create mode 100644 scripts/package.mjs diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ef5d3b9..a3334a2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,10 +11,12 @@ platforms// vars.yaml values for {{var}} skills//.md the host's text for a {{> slot}} in that skill overrides.lock whole-file overrides and the shared version each was copied from (only once one exists) + scripts/ the host's own tooling, never shipped: validate.mjs (run by npm run validate on + dist/) and package.sh (run by npm run package -- ), when the host needs them everything else the host's manifests, README, …, copied into dist// ({{version}} comes from release-please) dist// GENERATED: the plugin the host installs .github/plugin/marketplace.json Copilot's marketplace catalog (see below) -scripts/ render, validate, override, new-host +scripts/ render, validate, override, new-host, package ``` ### Marketplace catalogs diff --git a/package.json b/package.json index 5e8e0c9..0471988 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ "validate": "node scripts/validate.mjs", "override": "node scripts/override.mjs create", "override:ack": "node scripts/override.mjs ack", - "new-host": "node scripts/new-host.mjs" + "new-host": "node scripts/new-host.mjs", + "package": "node scripts/package.mjs" }, "devDependencies": { "ajv": "8.20.0", diff --git a/scripts/lib.mjs b/scripts/lib.mjs index 9b96f07..9de5705 100644 --- a/scripts/lib.mjs +++ b/scripts/lib.mjs @@ -14,9 +14,10 @@ export const distDir = join(root, "dist"); // Files copied into every host's plugin as they are. const sharedFiles = { LICENSE: "LICENSE" }; const sharedDirs = { "shared/assets": "assets" }; -// In platforms//, these are inputs to the render, not files of the plugin. +// In platforms//, these are inputs to the render or the host's own tooling +// (scripts/validate.mjs, scripts/package.sh), not files of the plugin. // Everything else there (manifests, README, …) is copied into dist//. -const reserved = ["vars.yaml", "overrides.lock", "skills/"]; +const reserved = ["vars.yaml", "overrides.lock", "skills/", "scripts/"]; // {{vars}} are filled in files with these extensions; others are copied as bytes. const textFile = /\.(json|md|mdc|ya?ml|txt|toml)$/; diff --git a/scripts/package.mjs b/scripts/package.mjs new file mode 100644 index 0000000..274068f --- /dev/null +++ b/scripts/package.mjs @@ -0,0 +1,18 @@ +// npm run package -- : builds what a host uploads by hand (for example +// OpenAI's ZIP) by running platforms//scripts/package.sh. +import { spawnSync } from "node:child_process"; +import { existsSync } from "node:fs"; +import { join } from "node:path"; +import { hosts, platformsDir } from "./lib.mjs"; + +const host = process.argv[2]; +if (!host || !hosts().includes(host)) { + console.error(`Usage: npm run package -- , one of: ${hosts().join(", ")}`); + process.exit(1); +} +const script = join(platformsDir, host, "scripts", "package.sh"); +if (!existsSync(script)) { + console.log(`${host} needs no packaging: it installs from dist/${host}/ (no platforms/${host}/scripts/package.sh).`); + process.exit(0); +} +process.exit(spawnSync("bash", [script], { stdio: "inherit" }).status ?? 1); diff --git a/scripts/validate.mjs b/scripts/validate.mjs index 66252bf..87a9c18 100644 --- a/scripts/validate.mjs +++ b/scripts/validate.mjs @@ -1,6 +1,7 @@ // Checks what each host ships: skill frontmatter, skill references, manifests // (against the JSON Schema they declare), the marketplace catalogs at the top of // the repo, and that no whole-file override was copied from an older shared file. +import { spawnSync } from "node:child_process"; import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { basename, dirname, join } from "node:path"; import Ajv2020 from "ajv/dist/2020.js"; @@ -105,6 +106,15 @@ function schemaErrors(url, json) { // Parses a JSON file and checks it against its "$schema". Returns the JSON, or // undefined after recording an error. +// OpenAI declares OAuth under mcpServers..extensions["com.openai"], as its +// docs show, but the Agent Plugins MCP schema doesn't list "extensions" on a +// server. Check the rest of the file against the schema. +function withoutServerExtensions(json) { + if (!json.mcpServers || typeof json.mcpServers !== "object") return json; + const servers = Object.fromEntries(Object.entries(json.mcpServers).map(([name, { extensions, ...server }]) => [name, server])); + return { ...json, mcpServers: servers }; +} + function readJson(host, file, buf) { let json; try { @@ -114,7 +124,7 @@ function readJson(host, file, buf) { return undefined; } if (json.$schema) { - for (const problem of schemaErrors(json.$schema, json)) errors.push(`${host}: ${file} ${problem}`); + for (const problem of schemaErrors(json.$schema, withoutServerExtensions(json))) errors.push(`${host}: ${file} ${problem}`); } return json; } @@ -206,6 +216,22 @@ for (const host of hosts()) { if (text.includes("TODO(new-host)")) errors.push(`${host}: platforms/${host}/${file} still has a TODO(new-host)`); } + // platforms//scripts/ is the host's tooling and never ships, so only the + // two files the repo runs belong there. + for (const file of listFiles(join(platformsDir, host, "scripts"))) { + if (!["validate.mjs", "package.sh"].includes(file)) { + errors.push(`${host}: platforms/${host}/scripts/${file} isn't shipped (scripts/ holds only validate.mjs and package.sh); move plugin files elsewhere`); + } + } + + // Checks only one host needs, such as OpenAI's submission limits, live in + // platforms//scripts/validate.mjs and run on the committed dist/. + const extra = join(platformsDir, host, "scripts", "validate.mjs"); + if (existsSync(extra)) { + const run = spawnSync(process.execPath, [extra, join(root, "dist", host)], { encoding: "utf8" }); + if (run.status !== 0) errors.push(`${host}: platforms/${host}/scripts/validate.mjs failed:\n${(run.stderr || run.stdout).trim()}`); + } + // How much skill text the host owns instead of sharing: its slot files and // whole-file overrides. Information only; a growing number is worth a look. const owned = listFiles(join(platformsDir, host, "skills")) From 6529c21bdc0774439e5a87c5859f4e62d698e7a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?In=C3=AAs=20Moital?= Date: Mon, 2 Nov 2026 18:17:51 +0000 Subject: [PATCH 2/9] feat(openai): plugin for ChatGPT and Codex in the shared skills layout Moves openai-zenrows (v0.1.8) into platforms/openai with its listing and review fields, OAuth mcp.json and the getting-started slot. Its submission checks and ZIP build move to platforms/openai/scripts. The getting-started description now names ChatGPT and Codex. Contact email is integrations@zenrows.com. --- .release-please-manifest.json | 3 +- README.md | 1 + dist/openai/LICENSE | 21 ++ dist/openai/README.md | 57 ++++++ dist/openai/assets/logo.png | Bin 0 -> 4959 bytes dist/openai/mcp.json | 19 ++ dist/openai/plugin.json | 95 +++++++++ dist/openai/skills/batch/SKILL.md | 45 +++++ .../openai/skills/browser-automation/SKILL.md | 60 ++++++ dist/openai/skills/capture-api-calls/SKILL.md | 35 ++++ dist/openai/skills/crawl/SKILL.md | 30 +++ .../skills/extract-structured-data/SKILL.md | 44 ++++ dist/openai/skills/getting-started/SKILL.md | 38 ++++ dist/openai/skills/map/SKILL.md | 22 ++ dist/openai/skills/scrape-webpage/SKILL.md | 17 ++ dist/openai/skills/using-zenrows/SKILL.md | 95 +++++++++ platforms/openai/README.md | 57 ++++++ platforms/openai/mcp.json | 19 ++ platforms/openai/plugin.json | 95 +++++++++ platforms/openai/scripts/package.sh | 15 ++ platforms/openai/scripts/validate.mjs | 191 ++++++++++++++++++ .../openai/skills/getting-started/connect.md | 1 + platforms/openai/vars.yaml | 18 ++ release-please-config.json | 11 + 24 files changed, 988 insertions(+), 1 deletion(-) create mode 100644 dist/openai/LICENSE create mode 100644 dist/openai/README.md create mode 100644 dist/openai/assets/logo.png create mode 100644 dist/openai/mcp.json create mode 100644 dist/openai/plugin.json create mode 100644 dist/openai/skills/batch/SKILL.md create mode 100644 dist/openai/skills/browser-automation/SKILL.md create mode 100644 dist/openai/skills/capture-api-calls/SKILL.md create mode 100644 dist/openai/skills/crawl/SKILL.md create mode 100644 dist/openai/skills/extract-structured-data/SKILL.md create mode 100644 dist/openai/skills/getting-started/SKILL.md create mode 100644 dist/openai/skills/map/SKILL.md create mode 100644 dist/openai/skills/scrape-webpage/SKILL.md create mode 100644 dist/openai/skills/using-zenrows/SKILL.md create mode 100644 platforms/openai/README.md create mode 100644 platforms/openai/mcp.json create mode 100644 platforms/openai/plugin.json create mode 100755 platforms/openai/scripts/package.sh create mode 100755 platforms/openai/scripts/validate.mjs create mode 100644 platforms/openai/skills/getting-started/connect.md create mode 100644 platforms/openai/vars.yaml diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 9a9ceb4..5fc8373 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,4 @@ { - "dist/copilot": "0.1.1" + "dist/copilot": "0.1.1", + "dist/openai": "0.1.8" } diff --git a/README.md b/README.md index 93c61ef..f71ab0e 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ Each host's guide covers installing the plugin and signing in to Zenrows. You si | Host | Setup guide | |---|---| +| ChatGPT and Codex | [dist/openai/README.md](dist/openai/README.md) | | GitHub Copilot | [dist/copilot/README.md](dist/copilot/README.md) | diff --git a/dist/openai/LICENSE b/dist/openai/LICENSE new file mode 100644 index 0000000..e7dd33a --- /dev/null +++ b/dist/openai/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Zenrows + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/dist/openai/README.md b/dist/openai/README.md new file mode 100644 index 0000000..2fd9f43 --- /dev/null +++ b/dist/openai/README.md @@ -0,0 +1,57 @@ +# Zenrows for ChatGPT and Codex + +Live web data for ChatGPT and Codex. This plugin connects them to the Zenrows MCP server, so the model can read the current content of any public page, pull structured fields from it, run batch jobs over many URLs, crawl from a start page, and drive a hosted browser for pages that need clicks, forms or pagination. It also adds skills that tell the model which Zenrows tool fits each task and how to keep requests efficient. + +## What you get + +- The Zenrows MCP server (`https://mcp.zenrows.com/mcp`). You sign in to your Zenrows account with OAuth; there is no API key to copy. +- Nine skills: + +| Skill | Use it for | +|---|---| +| `using-zenrows` | Overview: tools, defaults, errors and plan limits | +| `getting-started` | First-run checks and troubleshooting (the plugin's onboarding skill) | +| `scrape-webpage` | Reading one page | +| `extract-structured-data` | Named fields or JSON from one page | +| `batch` | A list of five or more URLs as one job | +| `crawl` | Many pages behind one start URL, with the Crawl tools | +| `map` | Listing a site's URLs from its sitemap without fetching them | +| `browser-automation` | Clicks, forms, pagination and multi-step flows | +| `capture-api-calls` | Finding the API calls a page makes | + +## Requirements + +A Zenrows account. Requests use credits on your Zenrows plan. You can [sign up here](https://app.zenrows.com/register). + +## Privacy + +The plugin sends the URLs and options you ask the model to fetch to Zenrows, which processes them under the [Zenrows privacy policy](https://www.zenrows.com/legal/privacy). + +## Support + +- Documentation: [docs.zenrows.com](https://docs.zenrows.com) +- Contact: [integrations@zenrows.com](mailto:integrations@zenrows.com) + +## For maintainers + +The package follows OpenAI's [plugin package format](https://developers.openai.com/plugins/build/plugins): a root `plugin.json` (Agent Plugins schema, with the listing, review and publication fields under `extensions.com.openai`), a root `mcp.json`, `skills/` and `assets/`. + +``` +npm run validate # includes the submission limits, from platforms/openai/scripts/validate.mjs +npm run package -- openai # validates, then builds .cache/packages/openai-zenrows-.zip +``` + +`platforms/openai/scripts/validate.mjs` in [zenrows-integrations](https://github.com/ZenRows/zenrows-integrations) encodes the limits from OpenAI's [manifest field reference](https://developers.openai.com/plugins/deploy/submission#automatically-provide-submission-and-review-information) and [submission error reference](https://developers.openai.com/plugins/deploy/submission-errors). CI runs it on every push. + +Things to know before submitting: + +- **Reviewer credentials never go in this repo.** OpenAI rejects `test_credentials` and `reviewer_instructions` in the package; they are entered in the portal's Review details form. +- **Run the review test cases with the reviewer test account before submitting.** OpenAI asks for it. Update `expected_behavior` to match what actually happens. +- **`demo_recording_url` must open without signing in.** It is required for MCP review. +- **`mcp.json` declares OAuth under `extensions.com.openai.auth`**, as OpenAI's [authentication reference](https://developers.openai.com/plugins/build/plugins#configure-mcp-authentication) says. The generic Agent Plugins MCP schema doesn't list `extensions` on a server; OpenAI's own examples combine both, so this follows OpenAI. +- **The tool scan reads the live server.** Every tool on `mcp.zenrows.com` needs `readOnlyHint`, `destructiveHint` and `openWorldHint` before submission ([ZenRows/zenrows-mcp#43](https://github.com/ZenRows/zenrows-mcp/pull/43)). +- **Bump `version` and add a `CHANGELOG.md` entry for every upload.** OpenAI requires a new version for each release. + +## License + +MIT diff --git a/dist/openai/assets/logo.png b/dist/openai/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..a32dc121e6d08ae56c36b82382e4671ae5cc7233 GIT binary patch literal 4959 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 R^YTU*3sdV0^(LNC{|k;`(4+tW literal 0 HcmV?d00001 diff --git a/dist/openai/mcp.json b/dist/openai/mcp.json new file mode 100644 index 0000000..2440854 --- /dev/null +++ b/dist/openai/mcp.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json", + "mcpServers": { + "zenrows": { + "type": "streamable-http", + "url": "https://mcp.zenrows.com/mcp", + "extensions": { + "com.openai": { + "auth": { + "type": "oauth", + "client": { + "mode": "dcr" + } + } + } + } + } + } +} diff --git a/dist/openai/plugin.json b/dist/openai/plugin.json new file mode 100644 index 0000000..4201c92 --- /dev/null +++ b/dist/openai/plugin.json @@ -0,0 +1,95 @@ +{ + "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", + "name": "zenrows", + "version": "0.1.8", + "description": "Live web data for ChatGPT and Codex: read pages, extract structured data, run batch jobs and site crawls, and drive hosted browser sessions through the Zenrows MCP server.", + "author": { + "name": "Zenrows", + "email": "integrations@zenrows.com", + "url": "https://www.zenrows.com" + }, + "homepage": "https://docs.zenrows.com", + "repository": "https://github.com/ZenRows/zenrows-integrations", + "license": "MIT", + "keywords": ["web-scraping", "data-extraction", "browser-automation", "crawl", "mcp", "zenrows"], + "extensions": { + "com.openai": { + "interface": { + "displayName": "Zenrows", + "shortDescription": "Live web data from any page", + "longDescription": "Zenrows lets ChatGPT and Codex read live web pages and turn them into usable data. Ask for a page's content and get clean markdown. Ask for products, prices, articles or tables and get structured fields. Hand over a list of URLs and get one result per page, or start from one page and collect the pages linked from it. For tasks that need clicking, scrolling or filling in a page, Zenrows runs a hosted browser session and reports what it finds.\n\nConnect your Zenrows account once with secure sign-in.", + "developerName": "Zenrows", + "category": "Developer Tools", + "capabilities": ["Read web pages", "Extract structured data", "Batch and crawl", "Browser automation"], + "websiteURL": "https://www.zenrows.com", + "supportURL": "https://www.zenrows.com/contact", + "privacyPolicyURL": "https://www.zenrows.com/legal/privacy", + "termsOfServiceURL": "https://www.zenrows.com/legal/terms", + "defaultPrompt": [ + "Use Zenrows to get the product names and prices shown on the first page of https://www.scrapingcourse.com/ecommerce/", + "Use Zenrows to find which products are out of stock on https://www.scrapingcourse.com/table-parsing", + "Use Zenrows to list the pages linked from https://www.scrapingcourse.com/ecommerce/" + ], + "composerIcon": "./assets/logo.png", + "logo": "./assets/logo.png" + }, + "onboardingSkill": "./skills/getting-started/SKILL.md", + "review": { + "demo_recording_url": "https://www.youtube.com/watch?v=4hDEUhIWlHo", + "test_cases": { + "positive": [ + { + "description": "Read one page and answer from its content", + "prompt": "Use Zenrows to read https://www.scrapingcourse.com/table-parsing and list the products that are out of stock.", + "tools_triggered": "scrape", + "expected_behavior": "Reads the product table and lists the four out-of-stock products: Headphones, Blender, Portable Speaker and Air Purifier." + }, + { + "description": "Structured fields from a listing page", + "prompt": "Use Zenrows to get the product names and prices shown on the first page of https://www.scrapingcourse.com/ecommerce/.", + "tools_triggered": "extract", + "expected_behavior": "Calls extract on that URL and returns the 16 products on the first page, each with its name and price, from Abominable Hoodie ($69.00) to Artemis Running Short ($45.00). It does not fetch the other catalog pages." + }, + { + "description": "Several URLs as one batch job", + "prompt": "Use Zenrows to fetch these five pages as one batch job and tell me which ones succeeded: https://www.scrapingcourse.com/ecommerce/, https://www.scrapingcourse.com/ecommerce/page/2/, https://www.scrapingcourse.com/pagination, https://www.scrapingcourse.com/table-parsing, https://www.scrapingcourse.com/javascript-rendering/", + "tools_triggered": "batch_create", + "expected_behavior": "Submits one Batch job with five tasks and reports one result per URL: five successful, zero failed. batch_create can wait for the job to finish and return its counts, so batch_status, batch_wait or batch_results may or may not follow." + }, + { + "description": "Multi-step browsing that needs a click", + "prompt": "Use Zenrows to open https://www.scrapingcourse.com/button-click in a browser session, click the Load more button once, and tell me the price of the Kenobi Trail Jacket.", + "tools_triggered": "browser_navigate, browser_click or browser_batch, browser_close", + "expected_behavior": "Opens a browser session, clicks Load more and answers $47. The Kenobi Trail Jacket is not on the page before the click, so the answer needs it. Then closes the browser session." + }, + { + "description": "Check account usage", + "prompt": "Use Zenrows to check how much of my Zenrows usage I have spent this period.", + "tools_triggered": "account_usage", + "expected_behavior": "Reports the connected account's credits used and remaining for the current period, without changing anything and without purchase or upgrade links." + } + ], + "negative": [ + { + "description": "Request unrelated to web data: the plugin should not be called", + "prompt": "Write a short poem about autumn." + }, + { + "description": "Purchase with card details: the plugin should decline to buy or to enter payment data", + "prompt": "Buy the first product on https://www.scrapingcourse.com/ecommerce/ and pay with my card 4111 1111 1111 1111." + }, + { + "description": "No target URL: the plugin should ask which page to read before calling any tool", + "prompt": "Scrape the website." + } + ] + }, + "commerce": false, + "commerce_description": "This plugin does not sell products or process payments. Requests use credits on the user's existing Zenrows plan." + }, + "publication": { + "release_notes": "First release. Read live web pages, extract structured data, run batch jobs and site crawls, and drive hosted browser sessions through the Zenrows MCP server." + } + } + } +} diff --git a/dist/openai/skills/batch/SKILL.md b/dist/openai/skills/batch/SKILL.md new file mode 100644 index 0000000..daa4126 --- /dev/null +++ b/dist/openai/skills/batch/SKILL.md @@ -0,0 +1,45 @@ +--- +name: batch +description: Fetch a list of URLs the user already has as one managed Zenrows job, with retries and per-task results. Use when the user supplies or points at roughly five or more URLs (a file, a pasted list, the output of map) and wants their content or extracted fields. Not for discovering URLs by following links (use crawl), not for listing a site's URLs (use map), and not for a single page (use scrape-webpage). +--- + +# Batch + +The user's instructions take precedence over this skill. If they conflict, follow the user. + +Zenrows Batch queues many URLs as one server-side job, retries transient failures, and stores the results. + +## When to use this instead of `scrape` + +Use batch at roughly five URLs or more. Below that, parallel `scrape` calls are faster and simpler, since a job costs a create, a wait and a results fetch. Above it, batch wins: one job instead of N round trips, server-side retries, and no plan-concurrency 429s (the Developer plan allows 5 concurrent requests). + +## Instructions + +1. Confirm the URL list and its size with the user before submitting. A batch bills per URL, so an unintended 5,000-URL job is expensive. +2. Submit the job: + - Same options for every URL: `batch_create(urls=[...], response_type='markdown')`. + - Different options per URL, or you need ids echoed back: `batch_create(tasks=[{url, external_id, metadata, zenrows_params}, ...])`. + - `urls` is ignored when `tasks` is provided. Do not pass both. +3. Wait for completion: + - Small jobs: pass `wait=true` to `batch_create` and get a terminal state in one call. + - Larger jobs: let `batch_create` return, then call `batch_wait(job_id)`. Default timeout is 600000 ms; raise `timeout_ms` (max 3600000) for big jobs. + - Report progress from `batch_status(job_id)` if the user asks mid-job. +4. Collect results with `batch_results(job_id)`. Use `status='successful'` or `status='failed'` to split them when reporting. +5. If you can write files (for example in Codex), save results to `./.zenrows/batch-.jsonl`, one JSON object per line, and summarize instead of reading the whole file back. Otherwise, summarize the results directly. +6. If the user stops the task, call `batch_cancel(job_id)`. The job keeps consuming credits until cancelled. + +## Options + +| Need | Where | Note | +|------|-------|------| +| Same settings for all URLs | job level: `js_render`, `premium_proxy`, `proxy_country`, `response_type` | Simplest form | +| Different settings per URL | `tasks[].zenrows_params` | Accepts `js_render`, `premium_proxy`, `extract`, `autoparse`, and other scrape params | +| Structured fields, not page content | `tasks[].zenrows_params` with `extract` or `autoparse` | Batch and Extract compose | +| Track which result is which | `tasks[].external_id` and `tasks[].metadata` | Echoed back on results | +| Geo-restricted pages | `proxy_country` | Works with the default settings | + +## Notes + +- `batch_create` has no required field, but a job with neither `tasks` nor `urls` does nothing. Always supply one. +- Always report the failed count alongside the successful one. Batch retries transient failures itself, so a URL in the failed set has genuinely not worked. +- `browser_batch` is unrelated: it batches actions inside one browser session. It is not this. diff --git a/dist/openai/skills/browser-automation/SKILL.md b/dist/openai/skills/browser-automation/SKILL.md new file mode 100644 index 0000000..34d815f --- /dev/null +++ b/dist/openai/skills/browser-automation/SKILL.md @@ -0,0 +1,60 @@ +--- +name: browser-automation +description: 'Drive a page through steps before its content is available: clicking, filling forms, logging in, paginating, or multi-step navigation that a single fetch cannot do. Use when interaction is required to reach the content. Not for static page reads (use scrape-webpage), and not for passively listing the network or API calls a page makes (use capture-api-calls).' +--- + +# Browser automation + +The user's instructions take precedence over this skill. If they conflict, follow the user. + +## Instructions + +1. Open a session with `browser_navigate(url)`. Save the returned `session_id` and pass it to every later call. +2. Find the selectors you need with `browser_get_accessibility_tree` or `browser_query_selector_all`. Both are cheaper and more precise than a screenshot. Take `browser_screenshot` only when the page state is unclear or the user asked to see it. +3. Run the interaction as one `browser_batch` call whenever the steps are known in advance. Every round trip to the hosted browser costs seconds; a login sent as six separate calls is six times slower than it needs to be. +4. Fall back to individual `browser_*` calls only when a later step depends on reading an earlier result. +5. Always call `browser_close(session_id)` when done, to free a concurrency slot. + +## browser_batch + +`browser_batch(session_id, actions, stop_on_error)` runs 1 to 50 ordered actions in one call, reads included. A login and its result: + +```json +{ + "session_id": "", + "actions": [ + { "type": "wait_for_selector", "selector": "#email" }, + { "type": "fill", "selector": "#email", "value": "user@example.com" }, + { "type": "fill", "selector": "#password", "value": "secret" }, + { "type": "click", "selector": "button[type=submit]" }, + { "type": "wait_for_navigation" }, + { "type": "get_text" } + ] +} +``` + +Action names inside `actions` drop the `browser_` prefix: `navigate`, `go_back`, `go_forward`, `reload`, `click`, `hover`, `type`, `fill`, `select`, `check`, `uncheck`, `focus`, `press_key`, `scroll`, `drag`, `wait`, `wait_for_selector`, `wait_for_navigation`, `evaluate`, `screenshot`, `get_text`, `get_html`, `get_url`, `get_title`, `get_attribute`, `get_accessibility_tree`, `query_selector_all`. + +Three traps: + +- The action is `select`, not `select_option`. +- `fill` takes `value`. `type` takes `text`. +- `stop_on_error` defaults to `true`, so one bad selector halts the rest of the list. Set it to `false` only when the later actions do not depend on the earlier ones. + +`browser_batch` batches actions inside one browser session. It is unrelated to the Batch product and its `batch_*` tools. + +## Session management + +- Pass `session_id` to every subsequent `browser_*` call. +- Do not open multiple sessions unless the task requires it. +- Browser sessions hold concurrency slots on the user's plan. Close them promptly. +- Geo-targeting is set when the session is created. Pass `proxy_country` (ISO 3166-1 alpha-2, for example `de`) or `proxy_region` (`eu`, `na`, `ap`, `sa`, `af`, `me`) to `browser_navigate`. Later `browser_*` calls do not accept either, so set it on the first call or start a new session. + +## When to prefer browser over scrape + +| Scenario | Tool | +|----------|------| +| Read a static docs page | `scrape` | +| Log in, then navigate | `browser_*` | +| Click "Load more" pagination | `browser_*` | +| Fill a search form and read results | `browser_*` | diff --git a/dist/openai/skills/capture-api-calls/SKILL.md b/dist/openai/skills/capture-api-calls/SKILL.md new file mode 100644 index 0000000..2b0aa56 --- /dev/null +++ b/dist/openai/skills/capture-api-calls/SKILL.md @@ -0,0 +1,35 @@ +--- +name: capture-api-calls +description: Discover the background API or XHR endpoints a page calls, or reverse-engineer how it loads its data. Use when the user wants the underlying request a site makes, not the rendered page. Not for interacting with the page UI (use browser-automation), and not for reading rendered content (use scrape-webpage). +--- + +# Capture API calls + +The user's instructions take precedence over this skill. If they conflict, follow the user. + +## Instructions + +1. Open a session with `browser_navigate(url)` and save the `session_id`. +2. Let the page finish loading: `browser_wait(session_id, ms=3000)`. Do not use `browser_wait_for_navigation` here; that must run before a navigation-triggering action, not after `browser_navigate`. +3. Collect network resource URLs with `browser_evaluate`: + + ```javascript + JSON.stringify( + performance.getEntriesByType('resource') + .filter(r => r.initiatorType === 'fetch' || r.initiatorType === 'xmlhttprequest' || r.name.includes('/api/')) + .map(r => ({ url: r.name, type: r.initiatorType })) + ) + ``` + +4. Identify the endpoint(s) most likely returning the target data (JSON paths, `/api/` URLs, GraphQL endpoints). +5. Present the endpoint URL, the method if inferable, and a sample response shape. +6. Optionally call the endpoint directly with `scrape(url=, response_type='html')` to get the raw response body. +7. Always call `browser_close(session_id)` when done. + +## Fallback + +If `performance.getEntriesByType` returns nothing (a single-page app with deferred loads), interact with the page (scroll, click load triggers) and re-run the evaluate script, or inspect inline `