Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,20 @@ Each host's plugin in `dist/<host>/` is rendered from shared skills plus that ho

```
skills/<skill>/SKILL.md shared skills; {{var}} and {{> slot}} mark what differs per host
shared/README.md every plugin's README; {{> setup}} and {{>? more}} take the host's own sections
shared/assets/ files every plugin host ships as they are (logo)
shared/text/<name>.md one fact in plain text, used by skills ({{< name}}) and code hosts ({{text:name}})
platforms/<host>/
vars.yaml values for {{var}}
skills/<skill>/<slot>.md the host's text for a {{> slot}} in that skill
readme/setup.md the host's Setup section of its README (readme/more.md: optional extra sections)
overrides.lock whole-file overrides and the shared version each was copied from (only once one exists)
everything else the host's manifests, README, …, copied into dist/<host>/ ({{version}} comes from release-please)
scripts/ the host's own tooling, never shipped; validate.mjs and package.sh are run by
npm run validate and npm run package -- <host>, the rest by the host itself
everything else the host's manifests, …, copied into dist/<host>/ ({{version}} comes from release-please)
dist/<host>/ GENERATED: the plugin the host installs, or a code host's files
.github/plugin/marketplace.json Copilot's marketplace catalog (see below)
scripts/ render, validate, override, new-host, mirror, test
scripts/ render, validate, override, new-host, package, mirror, test
```

### Marketplace catalogs
Expand All @@ -34,6 +38,8 @@ Requires Node 22. Run `npm ci` once.

- **A word or phrase:** use a variable, for example `{{host}}`, and set it in `platforms/<host>/vars.yaml`.
- **A paragraph or a few lines:** use a slot, for example `{{> connect}}`, and write the host's text in `platforms/<host>/skills/<skill>/connect.md`. Everything outside the slot stays shared, so later fixes still reach the host.
- **An optional paragraph:** `{{>? name}}` is a slot a host may leave out; without the file, its line goes. The README uses it for `readme/more.md`.
- **The README:** every plugin's README is `shared/README.md`. A host writes only `readme/setup.md` (how to install and sign in) and, if it needs more, `readme/more.md`; its own `README.md` fails the render. Code hosts keep their own README.
- **The whole file, as a last resort:** `npm run override -- <host> <skill>` copies the shared skill into `platforms/<host>/` and records which version it was copied from in `overrides.lock`. When the shared skill changes later, `npm run validate` fails until someone updates the copy, or confirms the change doesn't apply, and runs `npm run override:ack -- <host> <skill>`.
- **A skill only one host has:** put it in `platforms/<host>/skills/<name>/SKILL.md`, with no shared skill of that name. It ships only on that host and needs no `overrides.lock` entry.
- **A shared skill a host shouldn't ship:** list it under `skip_skills:` in that host's `vars.yaml` (Cursor uses a rules file instead of `using-zenrows`). A `{{skill:…}}` reference to a skipped skill fails the render.
Expand Down
5 changes: 3 additions & 2 deletions dist/copilot/README.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion dist/copilot/plugin.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion dist/copilot/skills/using-zenrows/SKILL.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"override": "node scripts/override.mjs create",
"override:ack": "node scripts/override.mjs ack",
"new-host": "node scripts/new-host.mjs",
"package": "node scripts/package.mjs",
"mirror": "node scripts/mirror.mjs"
},
"devDependencies": {
Expand Down
55 changes: 0 additions & 55 deletions platforms/copilot/README.md

This file was deleted.

2 changes: 1 addition & 1 deletion platforms/copilot/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
"name": "zenrows",
"version": "{{version}}",
"description": "Live web data for GitHub Copilot: fetch pages, extract structured data, run batch jobs and drive hosted browser sessions through the Zenrows MCP server.",
"description": "{{< zenrows-description}}.",
"author": {
"name": "Zenrows",
"email": "integrations@zenrows.com",
Expand Down
15 changes: 15 additions & 0 deletions platforms/copilot/readme/setup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
Install the plugin. In Copilot CLI, add this repository as a marketplace and install from it:

```
copilot plugin marketplace add ZenRows/zenrows-integrations
copilot plugin install zenrows@zenrows
```

In VS Code, run **Chat: Plugins** from the Command Palette and install from `ZenRows/zenrows-integrations`. For Copilot cloud agent, add the plugin to `enabledPlugins` in the repository's `.github/copilot/settings.json`.

Then sign in to Zenrows:

- **Copilot CLI:** run `/mcp auth zenrows` and sign in in the browser.
- **VS Code:** run **MCP: List Servers**, select `zenrows`, start it and allow it to authenticate.

Then ask Copilot something like: "Use Zenrows to get the product names and prices from https://www.scrapingcourse.com/ecommerce/".
44 changes: 36 additions & 8 deletions scripts/lib.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,13 @@ export const sharedTextDir = join(root, "shared", "text");
// Files copied into every host's plugin as they are.
const sharedFiles = { LICENSE: "LICENSE" };
const sharedDirs = { "shared/assets": "assets" };
// In platforms/<host>/, these are inputs to the render, not files of the plugin.
// In platforms/<host>/, 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/<host>/.
const reserved = ["vars.yaml", "overrides.lock", "skills/"];
const reserved = ["vars.yaml", "overrides.lock", "skills/", "scripts/", "readme/"];
// A plugin host's README.md is rendered from this template, with the host's
// slots from platforms/<host>/readme/.
const readmeTemplate = join(root, "shared", "README.md");
// What's left of one of this tool's tokens ({{name}}, {{skill:x}}, {{text:x}},
// {{< x}}, {{> x}}) after the render, for example in a file type it doesn't
// fill, or with a stray space, capital letter, _ or . the fill regexes don't
Expand Down Expand Up @@ -158,6 +162,23 @@ function fill(text, values, where) {
}

// Renders one host. Returns Map<path under dist/<host>, Buffer>.
// Puts a host's slot files into text: {{> slot}} needs <dir>/<slot>.md; {{>? slot}}
// is optional, and without the file its line (and one blank line after it) goes.
// Records each slot file used in usedSlots.
function fillSlots(text, dir, where, usedSlots) {
return text
.replace(/^[ \t]*\{\{>\?\s*([a-z0-9_-]+)\s*\}\}[ \t]*\n(\n)?/gm, (line, slot) => (existsSync(join(dir, `${slot}.md`)) ? line : ""))
.replace(/\{\{>(\?)?\s*([a-z0-9_-]+)\s*\}\}/g, (_, optional, slot) => {
const file = join(dir, `${slot}.md`);
if (!existsSync(file)) {
if (optional) return "";
throw new Error(`${where}: slot {{> ${slot}}} has no ${relative(root, file)}`);
}
usedSlots.add(relative(root, file));
return readFileSync(file, "utf8").replace(/\n$/, "");
});
}

export function renderHost(host) {
const values = vars(host);
const skills = hostSkills(host, values);
Expand All @@ -171,12 +192,7 @@ export function renderHost(host) {
const own = join(platform, "skills", name, "SKILL.md");
const source = existsSync(own) ? own : join(skillsDir, name, "SKILL.md");
const where = relative(root, source);
let text = readFileSync(source, "utf8").replace(/\{\{>\s*([a-z0-9_-]+)\s*\}\}/g, (_, slot) => {
const file = join(platform, "skills", name, `${slot}.md`);
if (!existsSync(file)) throw new Error(`${where}: slot {{> ${slot}}} has no ${relative(root, file)}`);
usedSlots.add(relative(root, file));
return readFileSync(file, "utf8").replace(/\n$/, "");
});
let text = fillSlots(readFileSync(source, "utf8"), join(platform, "skills", name), where, usedSlots);
text = fill(text, values, where);
out.set(`skills/${hostSkillName(values, name)}/SKILL.md`, Buffer.from(text));
}
Expand All @@ -187,6 +203,18 @@ export function renderHost(host) {
if (!usedSlots.has(path)) throw new Error(`${path}: no skill this host ships has this slot`);
}

if (!isCodeHost(values)) {
if (existsSync(join(platform, "README.md"))) {
throw new Error(`platforms/${host}/README.md: a plugin's README comes from shared/README.md; put the host's own text in platforms/${host}/readme/setup.md (and readme/more.md)`);
}
const where = relative(root, readmeTemplate);
out.set("README.md", Buffer.from(fill(fillSlots(readFileSync(readmeTemplate, "utf8"), join(platform, "readme"), where, usedSlots), values, where)));
for (const file of listFiles(join(platform, "readme"))) {
const path = relative(root, join(platform, "readme", file));
if (!usedSlots.has(path)) throw new Error(`${path}: shared/README.md has no slot for it`);
}
}

for (const [from, to] of isCodeHost(values) ? [] : Object.entries(sharedFiles)) out.set(to, readFileSync(join(root, from)));
for (const [from, to] of isCodeHost(values) ? [] : Object.entries(sharedDirs)) {
for (const file of listFiles(join(root, from))) out.set(`${to}/${file}`, readFileSync(join(root, from, file)));
Expand Down
36 changes: 7 additions & 29 deletions scripts/new-host.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// npm run new-host: asks for the host's id and full name, and creates
// platforms/<id>/ from templates (vars, a generic manifest and MCP config, the
// README and the getting-started sign-in slot) plus the host's release-please
// README setup slot and the getting-started sign-in slot) plus the host's release-please
// package. What only a person can decide is marked TODO(new-host), and
// npm run validate fails until it's done.
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
Expand Down Expand Up @@ -74,13 +74,13 @@ files.set(
);

// A generic manifest and MCP config. Each host reads its own layout, so the
// README's TODO asks for them to be moved and completed.
// setup slot's TODO asks for them to be moved and completed.
files.set(
"plugin.json",
json({
name: "zenrows",
version: "{{version}}",
description: `Live web data for ${name}: fetch pages, extract structured data, run batch jobs and drive hosted browser sessions through the Zenrows MCP server.`,
description: "{{< zenrows-description}}.",
author: { name: "Zenrows", email: "integrations@zenrows.com", url: "https://www.zenrows.com" },
homepage: "https://docs.zenrows.com",
repository: "https://github.com/ZenRows/zenrows-integrations",
Expand All @@ -98,36 +98,14 @@ files.set(
` - TODO(new-host): how to connect and sign in to the \`zenrows\` server in ${name}, one bullet per app (CLI, IDE, …).\n`,
);

// The README comes from shared/README.md; the host writes only its Setup section.
files.set(
"README.md",
`# Zenrows for ${name}

TODO(new-host): before this host ships, check its plugin files. platforms/${id}/plugin.json and mcp.json are generic templates: move them to the paths ${name} reads (for example .claude-plugin/plugin.json), add the fields it needs, update the manifest path in release-please-config.json, and add a marketplace catalog at the top of the repo if ${name} installs from a subfolder. Then delete this paragraph.

Live web data for ${name}. This plugin connects ${name} to the Zenrows MCP server, so it can fetch the current content of any public page, pull structured fields from it, run batch jobs over many URLs, and drive a hosted browser for pages that need clicks, forms or pagination. It also adds skills that tell ${name} which Zenrows tool fits each task and how to keep requests efficient.

## Requirements

A Zenrows account. Requests use credits on your Zenrows plan. You can [sign up here](https://app.zenrows.com/register).

## Setup
"readme/setup.md",
`TODO(new-host): before this host ships, check its plugin files. platforms/${id}/plugin.json and mcp.json are generic templates: move them to the paths ${name} reads (for example .claude-plugin/plugin.json), add the fields it needs, update the manifest path in release-please-config.json, and add a marketplace catalog at the top of the repo if ${name} installs from a subfolder. Then delete this paragraph.

TODO(new-host): how to install the plugin in ${name}, and how to sign in to Zenrows.

Then ask ${name} something like: "Use Zenrows to get the product names and prices from https://www.scrapingcourse.com/ecommerce/".

## Privacy

The plugin sends the URLs and options you ask ${name} 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)

## License

MIT
`,
);

Expand Down Expand Up @@ -163,7 +141,7 @@ writeFileSync(manifestPath, json(versions));
console.log(`
Created platforms/${id}/ from templates. Next:
1. Fill in every TODO(new-host): grep -rn "TODO(new-host)" platforms/${id}
(npm run validate fails until they're gone). The README's first TODO covers
(npm run validate fails until they're gone). readme/setup.md's first TODO covers
moving plugin.json and mcp.json to the layout ${name} reads.
2. npm run render, then npm run validate, and commit platforms/${id}, dist/${id}
and the release-please files together.`);
18 changes: 18 additions & 0 deletions scripts/package.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// npm run package -- <host>: builds what a host uploads by hand (for example
// OpenAI's ZIP) by running platforms/<host>/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 -- <host>, 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);
Loading
Loading