From d1000a3a602d238fdcd7cdfdaf2a1d8374596b0a Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 21 Oct 2026 03:33:34 +0000 Subject: [PATCH 1/9] Prepare the agent plugin for public marketplace listings - .agents/skills/.claude-plugin/plugin.json (written by plugin-dir.mjs, formerly cursor.mjs) so Claude's plugin directory can list the plugin from .agents/skills; no version, so every commit is an update - .agents/skills/README.md: the listing description the Claude directory and Cursor Marketplace require, with install lines and what the plugin runs - logo.svg without the inline - - - - - - - - - + + + + + + + + + + diff --git a/.claude-plugin/README.md b/.claude-plugin/README.md index ff6b1ad8dfea..c4448c0ab383 100644 --- a/.claude-plugin/README.md +++ b/.claude-plugin/README.md @@ -11,12 +11,27 @@ endpoint, rest, graphql, react and vue, implemented in `packages/core/agents/` a | File | What | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `marketplace.json` | The marketplace Claude Code reads (`claude plugin marketplace add reactive/data-client`). `data-client` is the plugin; `core`, `react`, `vue` and `migrations` are old names kept as copies so existing installs keep updating (`plans/next-breaking-release.md` removes them). | -| `cursor.mjs` | Writes Cursor's copy of the plugin (see below). | -| `*.test.mjs` | `node --test '.claude-plugin/*.test.mjs'`: `marketplace.json` and the Cursor copy against the repo. | +| `plugin-dir.mjs` | Writes the manifests that make `.agents/skills` a self-contained copy of the plugin, for Cursor and Claude's plugin directory (see below). | +| `openai-zip.mjs` | Packs the plugin as the ZIP OpenAI's plugin directory uploads (see below). | +| `*.test.mjs` | `node --test '.claude-plugin/*.test.mjs'`: `marketplace.json` and the plugin directory copies against the repo. | ## Codex and Cursor -Codex reads `marketplace.json` as is (`codex plugin add data-client@data-client`). Cursor plugins can't reach files outside their folder, so `cursor.mjs` writes a copy of the `data-client` entry rooted at `.agents/skills` (`.cursor-plugin/marketplace.json`, `.agents/skills/.cursor-plugin/plugin.json`), and `marketplace.test.mjs` fails when they drift. The marketplace source is a `git-subdir` pinned to `master`: Cursor's `/add-plugin` otherwise checks out `main`, which this repo does not have. Cursor's published `marketplace.schema.json` still types `source` as a string; the object form comes from its plugin docs, and the Cursor agent's install resolved it. +Codex reads `marketplace.json` as is (`codex plugin add data-client@data-client`). Cursor plugins can't reach files outside their folder, so `plugin-dir.mjs` writes a copy of the `data-client` entry rooted at `.agents/skills` (`.cursor-plugin/marketplace.json`, `.agents/skills/.cursor-plugin/plugin.json`), and `marketplace.test.mjs` fails when they drift. The marketplace source is a `git-subdir` pinned to `master`: Cursor's `/add-plugin` otherwise checks out `main`, which this repo does not have. Cursor's published `marketplace.schema.json` still types `source` as a string; the object form comes from its plugin docs, and the Cursor agent's install resolved it. + +## Public listings + +Each listing reads the plugin from `.agents/skills`, whose `README.md` is the listing's description. + +- **Claude's plugin directory** ([claude.ai/directory/manage](https://claude.ai/directory/manage), plugin path + `.agents/skills`, branch `master`) reads `.agents/skills/.claude-plugin/plugin.json`, which `plugin-dir.mjs` writes + without a `version` so every commit is an update. Check it with `claude plugin validate ./.agents/skills`. The + directory refuses repositories whose GitHub archive passes 50 MiB. +- **Cursor Marketplace** ([cursor.com/marketplace/publish](https://cursor.com/marketplace/publish)) reviews the + repository link and `.cursor-plugin/marketplace.json`. +- **OpenAI's plugin directory** (ChatGPT and Codex) takes an uploaded ZIP, not the repository: + `node .claude-plugin/openai-zip.mjs` writes `data-client-plugin.zip`. Upload a new one whenever the plugin's skills + change. ## What it reads from the rest of the repo diff --git a/.claude-plugin/marketplace.test.mjs b/.claude-plugin/marketplace.test.mjs index 6efb7a06efa3..f470df26beea 100644 --- a/.claude-plugin/marketplace.test.mjs +++ b/.claude-plugin/marketplace.test.mjs @@ -27,9 +27,9 @@ test('plugin skills exist and ship in a package', () => { } }); -test('the Cursor plugin matches marketplace.json', async () => { - // regenerate with node .claude-plugin/cursor.mjs - const { cursorFiles } = await import('./cursor.mjs'); - for (const [file, json] of Object.entries(cursorFiles())) +test('the plugin directory matches marketplace.json', async () => { + // regenerate with node .claude-plugin/plugin-dir.mjs + const { pluginDirFiles } = await import('./plugin-dir.mjs'); + for (const [file, json] of Object.entries(pluginDirFiles())) assert.deepEqual(readJSON(file), json, file); }); diff --git a/.claude-plugin/openai-zip.mjs b/.claude-plugin/openai-zip.mjs new file mode 100644 index 000000000000..be6c08abe97c --- /dev/null +++ b/.claude-plugin/openai-zip.mjs @@ -0,0 +1,34 @@ +// Packs the `data-client` plugin as the ZIP OpenAI's plugin directory +// (ChatGPT and Codex) uploads: node .claude-plugin/openai-zip.mjs [out.zip] +// The directory takes a Claude plugin archive with each skill at +// skills//SKILL.md, and doesn't follow the repo, so upload a new ZIP +// whenever the plugin's skills change. +import { execFileSync } from 'node:child_process'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; + +import { pluginDirFiles, root } from './plugin-dir.mjs'; + +const skills = path.join(root, '.agents/skills'); +const out = path.resolve(process.argv[2] ?? 'data-client-plugin.zip'); +const manifest = pluginDirFiles()['.agents/skills/.claude-plugin/plugin.json']; +const stage = fs.mkdtempSync(path.join(os.tmpdir(), 'data-client-plugin-')); + +for (const skill of manifest.skills) + fs.cpSync(path.join(skills, skill), path.join(stage, 'skills', skill), { + recursive: true, + }); +for (const file of ['README.md', 'assets/logo.svg']) + fs.cpSync(path.join(skills, file), path.join(stage, file)); +fs.cpSync(path.join(root, 'LICENSE'), path.join(stage, 'LICENSE')); +fs.mkdirSync(path.join(stage, '.claude-plugin')); +fs.writeFileSync( + path.join(stage, '.claude-plugin/plugin.json'), + `${JSON.stringify({ ...manifest, skills: './skills/' }, null, 2)}\n`, +); + +fs.rmSync(out, { force: true }); +execFileSync('zip', ['-qrX', out, '.'], { cwd: stage }); +fs.rmSync(stage, { recursive: true }); +console.log(`Wrote ${path.relative(process.cwd(), out)}`); diff --git a/.claude-plugin/cursor.mjs b/.claude-plugin/plugin-dir.mjs similarity index 56% rename from .claude-plugin/cursor.mjs rename to .claude-plugin/plugin-dir.mjs index 96eac7173f42..036c90b39cc9 100644 --- a/.claude-plugin/cursor.mjs +++ b/.claude-plugin/plugin-dir.mjs @@ -1,7 +1,9 @@ -// Writes the Cursor copy of the `data-client` plugin: node .claude-plugin/cursor.mjs -// Cursor plugins can't reach files outside their folder, so it's rooted at -// .agents/skills. marketplace.test.mjs fails when the written files drift -// from marketplace.json. +// Writes the manifests that make .agents/skills a self-contained copy of the +// `data-client` plugin: node .claude-plugin/plugin-dir.mjs +// Cursor plugins and plugins listed in Claude's directory can't reach files +// outside their folder, so they're rooted at .agents/skills. +// marketplace.test.mjs fails when the written files drift from +// marketplace.json. // // `/add-plugin` checks out ref "main" when the marketplace entry's source // doesn't pin one, and a branch on the GitHub URL is ignored. This repo's @@ -19,13 +21,26 @@ export const root = path.resolve( const skills = '.agents/skills'; const repository = 'https://github.com/reactive/data-client'; -/** Cursor's manifests (path => JSON): the `data-client` entry of - * marketplace.json plus Cursor's own listing fields */ -export function cursorFiles() { +/** The plugin directory's manifests (path => JSON): the `data-client` entry + * of marketplace.json plus each listing's own fields */ +export function pluginDirFiles() { const marketplace = JSON.parse( fs.readFileSync(path.join(root, '.claude-plugin/marketplace.json'), 'utf8'), ); const plugin = marketplace.plugins.find(p => p.name === marketplace.name); + const keywords = [ + 'data-client', + 'react', + 'vue', + 'rest', + 'graphql', + 'normalized-cache', + ]; + // Each entry is a skill directory (it contains SKILL.md). Listing the + // plugin root would also publish the maintainer skills beside them. + const pluginSkills = plugin.skills.map( + skill => `./${path.relative(skills, skill)}`, + ); return { '.cursor-plugin/marketplace.json': { name: marketplace.name, @@ -53,26 +68,30 @@ export function cursorFiles() { homepage: plugin.homepage, repository, license: plugin.license, - keywords: [ - 'data-client', - 'react', - 'vue', - 'rest', - 'graphql', - 'normalized-cache', - ], + keywords, category: 'developer-tools', tags: ['react', 'vue', 'data-fetching'], logo: 'assets/logo.svg', - // Each entry is a skill directory (it contains SKILL.md). Listing the - // plugin root would also publish the maintainer skills beside them. - skills: plugin.skills.map(skill => `./${path.relative(skills, skill)}`), + skills: pluginSkills, + }, + // Claude's plugin directory submits this folder. No `version`: Claude Code + // then treats every commit as an update, like the root marketplace does. + [`${skills}/.claude-plugin/plugin.json`]: { + name: plugin.name, + displayName: 'Reactive Data Client', + description: plugin.description, + author: marketplace.owner, + homepage: plugin.homepage, + repository, + license: plugin.license, + keywords, + skills: pluginSkills, }, }; } if (process.argv[1] === fileURLToPath(import.meta.url)) { - for (const [file, json] of Object.entries(cursorFiles())) { + for (const [file, json] of Object.entries(pluginDirFiles())) { fs.mkdirSync(path.dirname(path.join(root, file)), { recursive: true }); fs.writeFileSync( path.join(root, file), diff --git a/.claude-plugin/cursor-plugin.test.mjs b/.claude-plugin/plugin-dir.test.mjs similarity index 69% rename from .claude-plugin/cursor-plugin.test.mjs rename to .claude-plugin/plugin-dir.test.mjs index 79a40c1d6858..8371102629e5 100644 --- a/.claude-plugin/cursor-plugin.test.mjs +++ b/.claude-plugin/plugin-dir.test.mjs @@ -1,12 +1,13 @@ -// The Cursor manifests are written by cursor.mjs, and marketplace.test.mjs -// checks they match it. This checks what they point at still installs: +// The plugin directory's manifests are written by plugin-dir.mjs, and +// marketplace.test.mjs checks they match it. This checks what they point at +// still installs: // node --test '.claude-plugin/*.test.mjs' import assert from 'node:assert/strict'; import fs from 'node:fs'; import path from 'node:path'; import { test } from 'node:test'; -import { root } from './cursor.mjs'; +import { root } from './plugin-dir.mjs'; const pluginRoot = path.join(root, '.agents/skills'); const manifest = JSON.parse( @@ -35,3 +36,10 @@ test('logo stays inside the plugin directory', () => { assert.equal(manifest.logo.includes('..'), false); assert.ok(fs.existsSync(path.join(pluginRoot, manifest.logo))); }); + +test('README is long enough for the Claude directory listing', () => { + // the directory shows it as the description and blocks under 40 words, + // not counting code blocks + const prose = read('README.md').replace(/```[\s\S]*?```/g, ''); + assert.ok(prose.split(/\s+/).filter(Boolean).length >= 40); +}); diff --git a/.gitignore b/.gitignore index acff5beeb886..37c8f0b16306 100644 --- a/.gitignore +++ b/.gitignore @@ -96,3 +96,4 @@ packages/*/skills/ # data-client-skills code copied in at pack time (from packages/core/agents) packages/*/agents/ !packages/core/agents/ +data-client-plugin.zip From 92502775c7c05454e4803e83cb568badd42c81ad Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 21 Oct 2026 03:37:58 +0000 Subject: [PATCH 2/9] Share the plugin.json listing fields; leave repo tooling out of the OpenAI ZIP Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01MwNsZn4rWEGmrheHSn7ZP4 --- .agents/skills/.cursor-plugin/plugin.json | 6 +-- .claude-plugin/README.md | 6 +-- .claude-plugin/openai-zip.mjs | 9 +++- .claude-plugin/plugin-dir.mjs | 56 ++++++++++------------- 4 files changed, 36 insertions(+), 41 deletions(-) diff --git a/.agents/skills/.cursor-plugin/plugin.json b/.agents/skills/.cursor-plugin/plugin.json index 642a6d8b0b11..e46c20c16059 100644 --- a/.agents/skills/.cursor-plugin/plugin.json +++ b/.agents/skills/.cursor-plugin/plugin.json @@ -1,7 +1,6 @@ { "name": "data-client", "displayName": "Reactive Data Client", - "version": "1.0.0", "description": "Sets up Data Client, then points your agent at the skills shipped in your installed @data-client packages, so React or Vue apps get the guidance for their version", "author": { "name": "Nathaniel Tucker", @@ -18,8 +17,9 @@ "graphql", "normalized-cache" ], + "skills": ["./data-client", "./data-client-setup"], + "version": "1.0.0", "category": "developer-tools", "tags": ["react", "vue", "data-fetching"], - "logo": "assets/logo.svg", - "skills": ["./data-client", "./data-client-setup"] + "logo": "assets/logo.svg" } diff --git a/.claude-plugin/README.md b/.claude-plugin/README.md index c4448c0ab383..92eb5eb2f541 100644 --- a/.claude-plugin/README.md +++ b/.claude-plugin/README.md @@ -17,15 +17,15 @@ endpoint, rest, graphql, react and vue, implemented in `packages/core/agents/` a ## Codex and Cursor -Codex reads `marketplace.json` as is (`codex plugin add data-client@data-client`). Cursor plugins can't reach files outside their folder, so `plugin-dir.mjs` writes a copy of the `data-client` entry rooted at `.agents/skills` (`.cursor-plugin/marketplace.json`, `.agents/skills/.cursor-plugin/plugin.json`), and `marketplace.test.mjs` fails when they drift. The marketplace source is a `git-subdir` pinned to `master`: Cursor's `/add-plugin` otherwise checks out `main`, which this repo does not have. Cursor's published `marketplace.schema.json` still types `source` as a string; the object form comes from its plugin docs, and the Cursor agent's install resolved it. +Codex reads `marketplace.json` as is (`codex plugin add data-client@data-client`). Cursor plugins can't reach files outside their folder, so `plugin-dir.mjs` writes a copy of the `data-client` entry rooted at `.agents/skills` (`.cursor-plugin/marketplace.json`, `.agents/skills/.cursor-plugin/plugin.json`, and the `.agents/skills/.claude-plugin/plugin.json` Claude's directory reads), and `marketplace.test.mjs` fails when they drift. The marketplace source is a `git-subdir` pinned to `master`: Cursor's `/add-plugin` otherwise checks out `main`, which this repo does not have. Cursor's published `marketplace.schema.json` still types `source` as a string; the object form comes from its plugin docs, and the Cursor agent's install resolved it. ## Public listings Each listing reads the plugin from `.agents/skills`, whose `README.md` is the listing's description. - **Claude's plugin directory** ([claude.ai/directory/manage](https://claude.ai/directory/manage), plugin path - `.agents/skills`, branch `master`) reads `.agents/skills/.claude-plugin/plugin.json`, which `plugin-dir.mjs` writes - without a `version` so every commit is an update. Check it with `claude plugin validate ./.agents/skills`. The + `.agents/skills`, branch `master`) reads `.agents/skills/.claude-plugin/plugin.json`, written without a `version` so + every commit is an update. Check it with `claude plugin validate ./.agents/skills`. The directory refuses repositories whose GitHub archive passes 50 MiB. - **Cursor Marketplace** ([cursor.com/marketplace/publish](https://cursor.com/marketplace/publish)) reviews the repository link and `.cursor-plugin/marketplace.json`. diff --git a/.claude-plugin/openai-zip.mjs b/.claude-plugin/openai-zip.mjs index be6c08abe97c..ad22f10111c1 100644 --- a/.claude-plugin/openai-zip.mjs +++ b/.claude-plugin/openai-zip.mjs @@ -18,6 +18,8 @@ const stage = fs.mkdtempSync(path.join(os.tmpdir(), 'data-client-plugin-')); for (const skill of manifest.skills) fs.cpSync(path.join(skills, skill), path.join(stage, 'skills', skill), { recursive: true, + // repo tooling, left out like scripts/pack-skills.mjs does + filter: src => !['evals', 'references.json'].includes(path.basename(src)), }); for (const file of ['README.md', 'assets/logo.svg']) fs.cpSync(path.join(skills, file), path.join(stage, file)); @@ -29,6 +31,9 @@ fs.writeFileSync( ); fs.rmSync(out, { force: true }); -execFileSync('zip', ['-qrX', out, '.'], { cwd: stage }); -fs.rmSync(stage, { recursive: true }); +try { + execFileSync('zip', ['-qrX', out, '.'], { cwd: stage }); +} finally { + fs.rmSync(stage, { recursive: true }); +} console.log(`Wrote ${path.relative(process.cwd(), out)}`); diff --git a/.claude-plugin/plugin-dir.mjs b/.claude-plugin/plugin-dir.mjs index 036c90b39cc9..b71766ee59f2 100644 --- a/.claude-plugin/plugin-dir.mjs +++ b/.claude-plugin/plugin-dir.mjs @@ -28,19 +28,27 @@ export function pluginDirFiles() { fs.readFileSync(path.join(root, '.claude-plugin/marketplace.json'), 'utf8'), ); const plugin = marketplace.plugins.find(p => p.name === marketplace.name); - const keywords = [ - 'data-client', - 'react', - 'vue', - 'rest', - 'graphql', - 'normalized-cache', - ]; - // Each entry is a skill directory (it contains SKILL.md). Listing the - // plugin root would also publish the maintainer skills beside them. - const pluginSkills = plugin.skills.map( - skill => `./${path.relative(skills, skill)}`, - ); + // the fields both plugin.json listings share + const listing = { + name: plugin.name, + displayName: 'Reactive Data Client', + description: plugin.description, + author: marketplace.owner, + homepage: plugin.homepage, + repository, + license: plugin.license, + keywords: [ + 'data-client', + 'react', + 'vue', + 'rest', + 'graphql', + 'normalized-cache', + ], + // Each entry is a skill directory (it contains SKILL.md). Listing the + // plugin root would also publish the maintainer skills beside them. + skills: plugin.skills.map(skill => `./${path.relative(skills, skill)}`), + }; return { '.cursor-plugin/marketplace.json': { name: marketplace.name, @@ -60,33 +68,15 @@ export function pluginDirFiles() { ], }, [`${skills}/.cursor-plugin/plugin.json`]: { - name: plugin.name, - displayName: 'Reactive Data Client', + ...listing, version: '1.0.0', - description: plugin.description, - author: marketplace.owner, - homepage: plugin.homepage, - repository, - license: plugin.license, - keywords, category: 'developer-tools', tags: ['react', 'vue', 'data-fetching'], logo: 'assets/logo.svg', - skills: pluginSkills, }, // Claude's plugin directory submits this folder. No `version`: Claude Code // then treats every commit as an update, like the root marketplace does. - [`${skills}/.claude-plugin/plugin.json`]: { - name: plugin.name, - displayName: 'Reactive Data Client', - description: plugin.description, - author: marketplace.owner, - homepage: plugin.homepage, - repository, - license: plugin.license, - keywords, - skills: pluginSkills, - }, + [`${skills}/.claude-plugin/plugin.json`]: listing, }; } From de7a82f729dce56c1c1bf462f766592f53658e9b Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 21 Oct 2026 03:41:08 +0000 Subject: [PATCH 3/9] Clean up the OpenAI ZIP's staging folder on any failure; fix a stale test name Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01MwNsZn4rWEGmrheHSn7ZP4 --- .claude-plugin/README.md | 2 +- .claude-plugin/openai-zip.mjs | 33 +++++++++++++++++---------------- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/.claude-plugin/README.md b/.claude-plugin/README.md index 92eb5eb2f541..4b34e6120b30 100644 --- a/.claude-plugin/README.md +++ b/.claude-plugin/README.md @@ -43,4 +43,4 @@ Each listing reads the plugin from `.agents/skills`, whose `README.md` is the li The `agent-rules` workflow runs these tests and the `data-client-skills` tests (`packages/core/agents/*.test.mjs`), with no install. Its `paths` cover this directory and the inputs above, and `marketplace.test.mjs` fails when the old names -drift from `data-client` or a plugin skill goes missing or stops shipping in a package. `cursor-plugin.test.mjs` checks that the Cursor manifests still install. +drift from `data-client` or a plugin skill goes missing or stops shipping in a package. `plugin-dir.test.mjs` checks that the plugin directory still installs. diff --git a/.claude-plugin/openai-zip.mjs b/.claude-plugin/openai-zip.mjs index ad22f10111c1..a972382203ae 100644 --- a/.claude-plugin/openai-zip.mjs +++ b/.claude-plugin/openai-zip.mjs @@ -15,23 +15,24 @@ const out = path.resolve(process.argv[2] ?? 'data-client-plugin.zip'); const manifest = pluginDirFiles()['.agents/skills/.claude-plugin/plugin.json']; const stage = fs.mkdtempSync(path.join(os.tmpdir(), 'data-client-plugin-')); -for (const skill of manifest.skills) - fs.cpSync(path.join(skills, skill), path.join(stage, 'skills', skill), { - recursive: true, - // repo tooling, left out like scripts/pack-skills.mjs does - filter: src => !['evals', 'references.json'].includes(path.basename(src)), - }); -for (const file of ['README.md', 'assets/logo.svg']) - fs.cpSync(path.join(skills, file), path.join(stage, file)); -fs.cpSync(path.join(root, 'LICENSE'), path.join(stage, 'LICENSE')); -fs.mkdirSync(path.join(stage, '.claude-plugin')); -fs.writeFileSync( - path.join(stage, '.claude-plugin/plugin.json'), - `${JSON.stringify({ ...manifest, skills: './skills/' }, null, 2)}\n`, -); - -fs.rmSync(out, { force: true }); try { + for (const skill of manifest.skills) + fs.cpSync(path.join(skills, skill), path.join(stage, 'skills', skill), { + recursive: true, + // repo tooling, left out like scripts/pack-skills.mjs does + filter: src => !['evals', 'references.json'].includes(path.basename(src)), + }); + for (const file of ['README.md', 'assets/logo.svg']) + fs.cpSync(path.join(skills, file), path.join(stage, file)); + fs.cpSync(path.join(root, 'LICENSE'), path.join(stage, 'LICENSE')); + fs.mkdirSync(path.join(stage, '.claude-plugin')); + fs.writeFileSync( + path.join(stage, '.claude-plugin/plugin.json'), + `${JSON.stringify({ ...manifest, skills: './skills/' }, null, 2)}\n`, + ); + + fs.rmSync(out, { force: true }); + // needs the zip CLI execFileSync('zip', ['-qrX', out, '.'], { cwd: stage }); } finally { fs.rmSync(stage, { recursive: true }); From 437557511081a34c63c74c57ed30eca23eb046b6 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 21 Oct 2026 03:43:09 +0000 Subject: [PATCH 4/9] Say what plugin-dir.test.mjs checks Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01MwNsZn4rWEGmrheHSn7ZP4 --- .claude-plugin/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.claude-plugin/README.md b/.claude-plugin/README.md index 4b34e6120b30..5c9c15f89a67 100644 --- a/.claude-plugin/README.md +++ b/.claude-plugin/README.md @@ -43,4 +43,4 @@ Each listing reads the plugin from `.agents/skills`, whose `README.md` is the li The `agent-rules` workflow runs these tests and the `data-client-skills` tests (`packages/core/agents/*.test.mjs`), with no install. Its `paths` cover this directory and the inputs above, and `marketplace.test.mjs` fails when the old names -drift from `data-client` or a plugin skill goes missing or stops shipping in a package. `plugin-dir.test.mjs` checks that the plugin directory still installs. +drift from `data-client` or a plugin skill goes missing or stops shipping in a package. `plugin-dir.test.mjs` checks the skill names, logo and README the plugin directory listings read. From d12cbee97546e5f7f362f6a92aa2ebdfcb56ed21 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 21 Oct 2026 03:49:23 +0000 Subject: [PATCH 5/9] Give Claude's listing its icon and links; version the OpenAI ZIP Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01MwNsZn4rWEGmrheHSn7ZP4 --- .agents/skills/.claude-plugin/plugin.json | 5 ++++- .claude-plugin/README.md | 2 +- .claude-plugin/openai-zip.mjs | 5 ++++- .claude-plugin/plugin-dir.mjs | 8 +++++++- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/.agents/skills/.claude-plugin/plugin.json b/.agents/skills/.claude-plugin/plugin.json index 9a3b34bdbd00..9e5e266bc392 100644 --- a/.agents/skills/.claude-plugin/plugin.json +++ b/.agents/skills/.claude-plugin/plugin.json @@ -17,5 +17,8 @@ "graphql", "normalized-cache" ], - "skills": ["./data-client", "./data-client-setup"] + "skills": ["./data-client", "./data-client-setup"], + "icon": "./assets/logo.svg", + "documentationUrl": "https://dataclient.io/docs/getting-started/agent-skills", + "supportUrl": "https://github.com/reactive/data-client/issues" } diff --git a/.claude-plugin/README.md b/.claude-plugin/README.md index 5c9c15f89a67..af8f2c33a040 100644 --- a/.claude-plugin/README.md +++ b/.claude-plugin/README.md @@ -30,7 +30,7 @@ Each listing reads the plugin from `.agents/skills`, whose `README.md` is the li - **Cursor Marketplace** ([cursor.com/marketplace/publish](https://cursor.com/marketplace/publish)) reviews the repository link and `.cursor-plugin/marketplace.json`. - **OpenAI's plugin directory** (ChatGPT and Codex) takes an uploaded ZIP, not the repository: - `node .claude-plugin/openai-zip.mjs` writes `data-client-plugin.zip`. Upload a new one whenever the plugin's skills + `node .claude-plugin/openai-zip.mjs` writes `data-client-plugin.zip`, versioned `1..0`. Upload a new one whenever the plugin's skills change. ## What it reads from the rest of the repo diff --git a/.claude-plugin/openai-zip.mjs b/.claude-plugin/openai-zip.mjs index a972382203ae..fa8a3ce5e796 100644 --- a/.claude-plugin/openai-zip.mjs +++ b/.claude-plugin/openai-zip.mjs @@ -13,6 +13,9 @@ import { pluginDirFiles, root } from './plugin-dir.mjs'; const skills = path.join(root, '.agents/skills'); const out = path.resolve(process.argv[2] ?? 'data-client-plugin.zip'); const manifest = pluginDirFiles()['.agents/skills/.claude-plugin/plugin.json']; +// OpenAI requires a semver `version` that grows with each upload; the +// repo's manifest has none so Claude Code updates on every commit +const version = `1.${new Date().toISOString().slice(0, 10).replaceAll('-', '')}.0`; const stage = fs.mkdtempSync(path.join(os.tmpdir(), 'data-client-plugin-')); try { @@ -28,7 +31,7 @@ try { fs.mkdirSync(path.join(stage, '.claude-plugin')); fs.writeFileSync( path.join(stage, '.claude-plugin/plugin.json'), - `${JSON.stringify({ ...manifest, skills: './skills/' }, null, 2)}\n`, + `${JSON.stringify({ ...manifest, version, skills: './skills/' }, null, 2)}\n`, ); fs.rmSync(out, { force: true }); diff --git a/.claude-plugin/plugin-dir.mjs b/.claude-plugin/plugin-dir.mjs index b71766ee59f2..c4f2f3e3b57d 100644 --- a/.claude-plugin/plugin-dir.mjs +++ b/.claude-plugin/plugin-dir.mjs @@ -76,7 +76,13 @@ export function pluginDirFiles() { }, // Claude's plugin directory submits this folder. No `version`: Claude Code // then treats every commit as an update, like the root marketplace does. - [`${skills}/.claude-plugin/plugin.json`]: listing, + [`${skills}/.claude-plugin/plugin.json`]: { + ...listing, + // listing fields only Claude's directory reads + icon: './assets/logo.svg', + documentationUrl: plugin.homepage, + supportUrl: `${repository}/issues`, + }, }; } From 8866fb50d28573ac75fed09862c0e27372208b27 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 21 Oct 2026 03:51:32 +0000 Subject: [PATCH 6/9] Version the OpenAI ZIP by build time; one logo constant, tested for both listings Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01MwNsZn4rWEGmrheHSn7ZP4 --- .claude-plugin/README.md | 2 +- .claude-plugin/openai-zip.mjs | 10 ++++++---- .claude-plugin/plugin-dir.mjs | 5 +++-- .claude-plugin/plugin-dir.test.mjs | 7 +++++-- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/.claude-plugin/README.md b/.claude-plugin/README.md index af8f2c33a040..c6a41a16cd84 100644 --- a/.claude-plugin/README.md +++ b/.claude-plugin/README.md @@ -30,7 +30,7 @@ Each listing reads the plugin from `.agents/skills`, whose `README.md` is the li - **Cursor Marketplace** ([cursor.com/marketplace/publish](https://cursor.com/marketplace/publish)) reviews the repository link and `.cursor-plugin/marketplace.json`. - **OpenAI's plugin directory** (ChatGPT and Codex) takes an uploaded ZIP, not the repository: - `node .claude-plugin/openai-zip.mjs` writes `data-client-plugin.zip`, versioned `1..0`. Upload a new one whenever the plugin's skills + `node .claude-plugin/openai-zip.mjs` writes `data-client-plugin.zip`, versioned by its build time. Upload a new one whenever the plugin's skills change. ## What it reads from the rest of the repo diff --git a/.claude-plugin/openai-zip.mjs b/.claude-plugin/openai-zip.mjs index fa8a3ce5e796..accb87d39b57 100644 --- a/.claude-plugin/openai-zip.mjs +++ b/.claude-plugin/openai-zip.mjs @@ -13,9 +13,11 @@ import { pluginDirFiles, root } from './plugin-dir.mjs'; const skills = path.join(root, '.agents/skills'); const out = path.resolve(process.argv[2] ?? 'data-client-plugin.zip'); const manifest = pluginDirFiles()['.agents/skills/.claude-plugin/plugin.json']; -// OpenAI requires a semver `version` that grows with each upload; the -// repo's manifest has none so Claude Code updates on every commit -const version = `1.${new Date().toISOString().slice(0, 10).replaceAll('-', '')}.0`; +// OpenAI requires a semver `version` that grows with each upload: the UTC +// date and time of the build. The repo's manifest has none so Claude Code +// updates on every commit. +const [date, time] = new Date().toISOString().replace(/[-:]/g, '').split('T'); +const version = `1.${date}.${Number(time.slice(0, 6))}`; const stage = fs.mkdtempSync(path.join(os.tmpdir(), 'data-client-plugin-')); try { @@ -25,7 +27,7 @@ try { // repo tooling, left out like scripts/pack-skills.mjs does filter: src => !['evals', 'references.json'].includes(path.basename(src)), }); - for (const file of ['README.md', 'assets/logo.svg']) + for (const file of ['README.md', manifest.icon]) fs.cpSync(path.join(skills, file), path.join(stage, file)); fs.cpSync(path.join(root, 'LICENSE'), path.join(stage, 'LICENSE')); fs.mkdirSync(path.join(stage, '.claude-plugin')); diff --git a/.claude-plugin/plugin-dir.mjs b/.claude-plugin/plugin-dir.mjs index c4f2f3e3b57d..e467c1736d6c 100644 --- a/.claude-plugin/plugin-dir.mjs +++ b/.claude-plugin/plugin-dir.mjs @@ -20,6 +20,7 @@ export const root = path.resolve( ); const skills = '.agents/skills'; const repository = 'https://github.com/reactive/data-client'; +const logo = 'assets/logo.svg'; /** The plugin directory's manifests (path => JSON): the `data-client` entry * of marketplace.json plus each listing's own fields */ @@ -72,14 +73,14 @@ export function pluginDirFiles() { version: '1.0.0', category: 'developer-tools', tags: ['react', 'vue', 'data-fetching'], - logo: 'assets/logo.svg', + logo, }, // Claude's plugin directory submits this folder. No `version`: Claude Code // then treats every commit as an update, like the root marketplace does. [`${skills}/.claude-plugin/plugin.json`]: { ...listing, // listing fields only Claude's directory reads - icon: './assets/logo.svg', + icon: `./${logo}`, documentationUrl: plugin.homepage, supportUrl: `${repository}/issues`, }, diff --git a/.claude-plugin/plugin-dir.test.mjs b/.claude-plugin/plugin-dir.test.mjs index 8371102629e5..8af3858afba3 100644 --- a/.claude-plugin/plugin-dir.test.mjs +++ b/.claude-plugin/plugin-dir.test.mjs @@ -33,8 +33,11 @@ test('plugin skills are directories inside the plugin, named by SKILL.md', () => }); test('logo stays inside the plugin directory', () => { - assert.equal(manifest.logo.includes('..'), false); - assert.ok(fs.existsSync(path.join(pluginRoot, manifest.logo))); + const { icon } = JSON.parse(read('.claude-plugin/plugin.json')); + for (const file of [manifest.logo, icon]) { + assert.equal(file.includes('..'), false, file); + assert.ok(fs.existsSync(path.join(pluginRoot, file)), file); + } }); test('README is long enough for the Claude directory listing', () => { From 4d8a3f2da7992fa24f421914fc8ed9f40cb3ac32 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 21 Oct 2026 04:14:25 +0000 Subject: [PATCH 7/9] CI: validate the plugin directory with claude plugin validate Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01MwNsZn4rWEGmrheHSn7ZP4 --- .claude-plugin/README.md | 2 +- .github/workflows/agent-rules.yml | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.claude-plugin/README.md b/.claude-plugin/README.md index c6a41a16cd84..15621a246aa6 100644 --- a/.claude-plugin/README.md +++ b/.claude-plugin/README.md @@ -25,7 +25,7 @@ Each listing reads the plugin from `.agents/skills`, whose `README.md` is the li - **Claude's plugin directory** ([claude.ai/directory/manage](https://claude.ai/directory/manage), plugin path `.agents/skills`, branch `master`) reads `.agents/skills/.claude-plugin/plugin.json`, written without a `version` so - every commit is an update. Check it with `claude plugin validate ./.agents/skills`. The + every commit is an update. The `agent-rules` workflow checks it with `claude plugin validate ./.agents/skills`. The directory refuses repositories whose GitHub archive passes 50 MiB. - **Cursor Marketplace** ([cursor.com/marketplace/publish](https://cursor.com/marketplace/publish)) reviews the repository link and `.cursor-plugin/marketplace.json`. diff --git a/.github/workflows/agent-rules.yml b/.github/workflows/agent-rules.yml index a5009b1cb901..69d6bf21d117 100644 --- a/.github/workflows/agent-rules.yml +++ b/.github/workflows/agent-rules.yml @@ -60,3 +60,6 @@ jobs: run: node scripts/breaking-evals.mjs - name: Test agent hooks and the skill sync run: node --test '.cursor/hooks/*.test.js' '.claude-plugin/*.test.mjs' 'packages/core/agents/*.test.mjs' + # the folder Claude's plugin directory lists (.claude-plugin/README.md) + - name: Validate the plugin directory + run: npx -y @anthropic-ai/claude-code@2.1.306 plugin validate ./.agents/skills From 1ca359f8dbe3fd4df698be9575fca0a27a1f2ecd Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 21 Oct 2026 04:20:14 +0000 Subject: [PATCH 8/9] Docs: say the plugin validate step fetches claude-code and checks only the manifest Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01MwNsZn4rWEGmrheHSn7ZP4 --- .claude-plugin/README.md | 5 +++-- .github/workflows/agent-rules.yml | 4 +++- .github/workflows/regenerate.yml | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.claude-plugin/README.md b/.claude-plugin/README.md index 15621a246aa6..68d8cb0dc14e 100644 --- a/.claude-plugin/README.md +++ b/.claude-plugin/README.md @@ -25,7 +25,8 @@ Each listing reads the plugin from `.agents/skills`, whose `README.md` is the li - **Claude's plugin directory** ([claude.ai/directory/manage](https://claude.ai/directory/manage), plugin path `.agents/skills`, branch `master`) reads `.agents/skills/.claude-plugin/plugin.json`, written without a `version` so - every commit is an update. The `agent-rules` workflow checks it with `claude plugin validate ./.agents/skills`. The + every commit is an update. The `agent-rules` workflow checks its manifest and skills with `claude plugin validate ./.agents/skills`; the + directory's safety scan and policy review run only on submission. The directory refuses repositories whose GitHub archive passes 50 MiB. - **Cursor Marketplace** ([cursor.com/marketplace/publish](https://cursor.com/marketplace/publish)) reviews the repository link and `.cursor-plugin/marketplace.json`. @@ -42,5 +43,5 @@ Each listing reads the plugin from `.agents/skills`, whose `README.md` is the li ## CI The `agent-rules` workflow runs these tests and the `data-client-skills` tests (`packages/core/agents/*.test.mjs`), with -no install. Its `paths` cover this directory and the inputs above, and `marketplace.test.mjs` fails when the old names +no install, then fetches a pinned Claude Code CLI for `claude plugin validate`. Its `paths` cover this directory and the inputs above, and `marketplace.test.mjs` fails when the old names drift from `data-client` or a plugin skill goes missing or stops shipping in a package. `plugin-dir.test.mjs` checks the skill names, logo and README the plugin directory listings read. diff --git a/.github/workflows/agent-rules.yml b/.github/workflows/agent-rules.yml index 69d6bf21d117..6752120a1a15 100644 --- a/.github/workflows/agent-rules.yml +++ b/.github/workflows/agent-rules.yml @@ -60,6 +60,8 @@ jobs: run: node scripts/breaking-evals.mjs - name: Test agent hooks and the skill sync run: node --test '.cursor/hooks/*.test.js' '.claude-plugin/*.test.mjs' 'packages/core/agents/*.test.mjs' - # the folder Claude's plugin directory lists (.claude-plugin/README.md) + # the folder Claude's plugin directory lists (.claude-plugin/README.md). + # The one step that downloads (claude-code from npm); pinned so a CLI + # release can't change the check without a PR bumping it. - name: Validate the plugin directory run: npx -y @anthropic-ai/claude-code@2.1.306 plugin validate ./.agents/skills diff --git a/.github/workflows/regenerate.yml b/.github/workflows/regenerate.yml index b3ace622ec00..37f57955e655 100644 --- a/.github/workflows/regenerate.yml +++ b/.github/workflows/regenerate.yml @@ -4,7 +4,7 @@ name: regenerate # Fails when the committed output is stale: PR authors regenerate and commit it. # Also type-checks the docs' React and Vue examples, and runs the agents' # browser debugging snippets in Chromium. -# Claude Code rules are left to agent-rules.yml, which needs no install. +# Claude Code rules are left to agent-rules.yml, which installs no dependencies. on: push: branches: From df92bc6273ca487dedc560f0e8dbcd98aa8ec045 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 25 Oct 2026 00:10:23 +0000 Subject: [PATCH 9/9] Format .claude-plugin/README.md Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01MwNsZn4rWEGmrheHSn7ZP4 --- .claude-plugin/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.claude-plugin/README.md b/.claude-plugin/README.md index 5ba8409a9d58..efeab29475a6 100644 --- a/.claude-plugin/README.md +++ b/.claude-plugin/README.md @@ -13,7 +13,7 @@ endpoint, rest, graphql, react and vue, implemented in `packages/core/agents/` a | `marketplace.json` | The marketplace Claude Code reads (`claude plugin marketplace add reactive/data-client`). `data-client` is the plugin; `core`, `react`, `vue` and `migrations` are old names kept as copies so existing installs keep updating (`plans/next-breaking-release.md` removes them). | | `plugin-dir.mjs` | Writes the manifests that make `.agents/skills` a self-contained copy of the plugin, for Cursor and Claude's plugin directory (see below). | | `openai-zip.mjs` | Packs the plugin as the ZIP OpenAI's plugin directory uploads (see below). | -| `*.test.mjs` | `node --test '.claude-plugin/*.test.mjs'`: `marketplace.json` and the plugin directory copies against the repo; READMEs against the site. | +| `*.test.mjs` | `node --test '.claude-plugin/*.test.mjs'`: `marketplace.json` and the plugin directory copies against the repo; READMEs against the site. | ## Codex and Cursor