From 6794f2462cfc5e24114231711a94b38b379907f5 Mon Sep 17 00:00:00 2001 From: Sidney Swift <158200036+sidneyswift@users.noreply.github.com> Date: Sun, 10 May 2026 17:30:59 -0400 Subject: [PATCH] feat(marketplace): add unified marketplace machinery (with recoup branding) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sets up the single-source-of-truth marketplace pattern. After this lands, adding a new plugin is one entry in marketplace.source.json + a regen. No end-user behavior change yet — this PR ships the infrastructure only. What this PR adds - marketplace.source.json — single source of truth for the marketplace + the recoup-skills virtual plugin (the existing 12 broad music skills). - scripts/generate-marketplaces.py — emits the three platform manifests: .claude-plugin/marketplace.json, .agents/plugins/marketplace.json, .cursor-plugin/marketplace.json. --check mode detects drift. - scripts/validate-manifests.py — checks generator parity, plugin source paths, .claude/.codex/.cursor plugin manifests on each plugins/ folder, and SKILL.md frontmatter (name + description) on every skill. - .github/workflows/validate.yml — runs both scripts on every PR. - AGENTS.md, README.md, contributing.md, CHANGELOG.md — describe the source-of-truth + generated-files pattern (with recoup branding). - Removes the now-superseded root .claude-plugin/plugin.json and .codex-plugin/plugin.json (the recoup-skills virtual plugin entry in the marketplace replaces them). Branding - Marketplace name: recoup - Plugin name: recoup-skills - Display names: Recoup Skills, developerName: Recoup - Owner/author: Recoup - Install path: @recoup - recoupable.com URLs, agent@recoupable.com, recoupable/skills repo path, @recoupable/cli npm package, and LICENSE entity stay unchanged. After this lands /plugin marketplace add recoupable/skills /plugin install recoup-skills@recoup Why this is small enough to review - 13 files, +961 / -137. All-new infrastructure, zero behavior change for end users (no skill paths under skills/ moved). - Generated files are mechanically derivable from marketplace.source.json. Spot-check the source file; trust the generator. What's intentionally NOT in this PR - No catalog plugin (separate PR — adds plugins/music-catalog-diligence/ directory and registers it in marketplace.source.json). - No getting-started orchestrator rewrite (separate PR). Test plan - python3 scripts/generate-marketplaces.py --check → exit 0 - python3 scripts/validate-manifests.py → exit 0 Replaces #23 (split into 3 PRs; rename baked into each). Co-authored-by: Cursor --- .agents/plugins/marketplace.json | 51 ++++++++ .claude-plugin/marketplace.json | 47 ++++++++ .claude-plugin/plugin.json | 23 ---- .codex-plugin/plugin.json | 34 ------ .cursor-plugin/marketplace.json | 37 ++++++ .github/workflows/validate.yml | 22 ++++ AGENTS.md | 97 ++++++++------- README.md | 88 +++++++------- contributing.md | 48 +++----- marketplace.source.json | 62 ++++++++++ scripts/generate-marketplaces.py | 195 +++++++++++++++++++++++++++++++ scripts/validate-manifests.py | 169 +++++++++++++++++++++++++++ 12 files changed, 705 insertions(+), 168 deletions(-) create mode 100644 .agents/plugins/marketplace.json create mode 100644 .claude-plugin/marketplace.json delete mode 100644 .claude-plugin/plugin.json delete mode 100644 .codex-plugin/plugin.json create mode 100644 .cursor-plugin/marketplace.json create mode 100644 .github/workflows/validate.yml create mode 100644 marketplace.source.json create mode 100755 scripts/generate-marketplaces.py create mode 100755 scripts/validate-manifests.py diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json new file mode 100644 index 0000000..e45748c --- /dev/null +++ b/.agents/plugins/marketplace.json @@ -0,0 +1,51 @@ +{ + "name": "recoup", + "description": "Recoup's marketplace of AI agent skills and plugins for the music industry.", + "owner": { + "name": "Recoup", + "email": "agent@recoupable.com", + "url": "https://recoupable.com" + }, + "interface": { + "displayName": "Recoup" + }, + "plugins": [ + { + "name": "recoup-skills", + "description": "Music-industry skills for AI agents — artist management, songwriting, analytics, release campaigns, research, content creation.", + "source": { + "source": "local", + "path": "./" + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL" + }, + "category": "Music", + "keywords": [ + "music", + "artist-management", + "songwriting", + "music-marketing", + "analytics", + "release-management", + "chartmetric", + "content-creation", + "agent-skills" + ], + "interface": { + "displayName": "Recoup Skills", + "shortDescription": "Music industry skills for AI agents", + "longDescription": "Teach your coding agent how to manage artists, write songs, analyze music analytics, plan releases, and create content. General skills work for anyone; platform skills integrate with the Recoup platform.", + "developerName": "Recoup", + "category": "Music", + "capabilities": [ + "Read", + "Write" + ], + "websiteURL": "https://recoupable.com", + "brandColor": "#345A5D" + } + } + ] +} diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 0000000..a27343c --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,47 @@ +{ + "name": "recoup", + "owner": { + "name": "Recoup", + "email": "agent@recoupable.com", + "url": "https://recoupable.com" + }, + "metadata": { + "description": "Recoup's marketplace of AI agent skills and plugins for the music industry.", + "version": "1.0.0" + }, + "plugins": [ + { + "name": "recoup-skills", + "description": "Music-industry skills for AI agents — artist management, songwriting, analytics, release campaigns, research, content creation.", + "version": "1.0.0", + "source": "./", + "category": "Music", + "keywords": [ + "music", + "artist-management", + "songwriting", + "music-marketing", + "analytics", + "release-management", + "chartmetric", + "content-creation", + "agent-skills" + ], + "strict": false, + "skills": [ + "./skills/artist-workspace", + "./skills/chart-metric", + "./skills/content-creation", + "./skills/create-artist", + "./skills/getting-started", + "./skills/music-industry-research", + "./skills/recoup-api", + "./skills/release-management", + "./skills/setup-sandbox", + "./skills/song-writing", + "./skills/streaming-growth", + "./skills/trend-to-song" + ] + } + ] +} diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json deleted file mode 100644 index ac1ca62..0000000 --- a/.claude-plugin/plugin.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "recoupable-skills", - "version": "1.0.0", - "description": "AI agent skills for the music industry — artist management, songwriting, analytics, release campaigns, and more.", - "author": { - "name": "Recoupable", - "email": "agent@recoupable.com", - "url": "https://recoupable.com" - }, - "homepage": "https://recoupable.com", - "repository": "https://github.com/recoupable/skills", - "license": "Apache-2.0", - "keywords": [ - "music", - "artist-management", - "songwriting", - "music-marketing", - "analytics", - "release-management", - "chartmetric", - "agent-skills" - ] -} diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json deleted file mode 100644 index 0c949a6..0000000 --- a/.codex-plugin/plugin.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "recoupable-skills", - "version": "1.0.0", - "description": "AI agent skills for the music industry — artist management, songwriting, analytics, release campaigns, and more.", - "author": { - "name": "Recoupable", - "email": "agent@recoupable.com", - "url": "https://recoupable.com" - }, - "homepage": "https://recoupable.com", - "repository": "https://github.com/recoupable/skills", - "license": "Apache-2.0", - "keywords": [ - "music", - "artist-management", - "songwriting", - "music-marketing", - "analytics", - "release-management", - "chartmetric", - "agent-skills" - ], - "skills": "./skills/", - "interface": { - "displayName": "Recoupable Skills", - "shortDescription": "Music industry skills for AI agents", - "longDescription": "Teach your coding agent how to manage artists, write songs, analyze music analytics, plan releases, and more. General skills work for anyone — platform skills integrate with the Recoupable platform.", - "developerName": "Recoupable", - "category": "Productivity", - "capabilities": ["Read", "Write"], - "websiteURL": "https://recoupable.com", - "brandColor": "#345A5D" - } -} diff --git a/.cursor-plugin/marketplace.json b/.cursor-plugin/marketplace.json new file mode 100644 index 0000000..f2359c6 --- /dev/null +++ b/.cursor-plugin/marketplace.json @@ -0,0 +1,37 @@ +{ + "name": "recoup", + "owner": { + "name": "Recoup", + "email": "agent@recoupable.com" + }, + "metadata": { + "description": "Recoup's marketplace of AI agent skills and plugins for the music industry." + }, + "plugins": [ + { + "name": "recoup-skills", + "source": ".", + "description": "Music-industry skills for AI agents — artist management, songwriting, analytics, release campaigns, research, content creation.", + "version": "1.0.0", + "author": { + "name": "Recoup", + "email": "agent@recoupable.com" + }, + "homepage": "https://recoupable.com", + "repository": "https://github.com/recoupable/skills", + "license": "Apache-2.0", + "category": "Music", + "keywords": [ + "music", + "artist-management", + "songwriting", + "music-marketing", + "analytics", + "release-management", + "chartmetric", + "content-creation", + "agent-skills" + ] + } + ] +} diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 0000000..e2d7e6d --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,22 @@ +name: validate + +on: + push: + branches: [main] + pull_request: + +jobs: + validate-manifests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Check marketplace files are in sync with source + run: python3 scripts/generate-marketplaces.py --check + + - name: Validate all manifests + skills + run: python3 scripts/validate-manifests.py diff --git a/AGENTS.md b/AGENTS.md index 692a776..caedd1e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,70 +1,85 @@ -# Agent Instructions — Recoupable Skills +# Agent Instructions — Recoup Skills Marketplace -This file provides context for any AI agent operating within this repository. +Context for any AI agent operating within this repository. ## Repository Purpose -Public skills for AI agents working in the music industry. Skills teach agents how to complete specific tasks — from songwriting to analytics to release campaigns. +Recoup's unified marketplace of agent skills and plugins for the music industry. It is simultaneously a **skill collection** (broad skills under `skills/`), a **plugin marketplace** (manifests at the repo root), and a **plugin host** (self-contained plugins under `plugins/`). + +Supported agents: **Claude Code**, **OpenAI Codex**, **Cursor**. ## Structure ```text recoupable/skills/ -├── .claude-plugin/ ← plugin manifest for Claude Code -├── .codex-plugin/ ← plugin manifest for OpenAI Codex -├── skills/ ← all skills live here -│ ├── chart-metric/ -│ ├── content-creation/ -│ ├── music-industry-research/ -│ ├── song-writing/ -│ └── ... -├── README.md -├── contributing.md -└── AGENTS.md ← this file +├── .claude-plugin/marketplace.json ← Claude Code marketplace (generated) +├── .agents/plugins/marketplace.json ← Codex marketplace (generated) +├── .cursor-plugin/marketplace.json ← Cursor marketplace (generated) +│ +├── marketplace.source.json ← SINGLE SOURCE OF TRUTH — edit this +│ +├── skills/ ← Broad music skills +│ └── {skill-name}/ +│ ├── SKILL.md ← required — instructions + YAML frontmatter +│ ├── references/ ← optional — docs loaded on-demand +│ ├── scripts/ ← optional — executable code +│ └── assets/ ← optional — templates, fonts, icons +│ +├── plugins/ ← Self-contained vertical plugins +│ └── {plugin-name}/ +│ ├── .claude-plugin/plugin.json +│ ├── .codex-plugin/plugin.json +│ ├── .cursor-plugin/plugin.json +│ ├── skills/ +│ ├── agents/ +│ ├── commands/ +│ ├── scripts/ +│ ├── templates/ +│ └── references/ +│ +└── scripts/ + ├── generate-marketplaces.py ← Regenerate platform marketplace JSONs + └── validate-manifests.py ← Validate every manifest + skill ``` -## How Skills Load - -Skills use progressive disclosure: - -1. **Frontmatter** (`name` + `description`) — always in context. This is how you decide whether to load a skill. -2. **SKILL.md body** — loaded when you determine the skill is relevant. -3. **Linked files** (`references/`, `scripts/`, `assets/`) — loaded on-demand as needed. - -The `description` field is the trigger. If it's vague, the skill won't activate. +A new vertical plugin earns its own folder under `plugins/` when it has **at least 3 related skills** AND needs commands, agents, scripts, templates, or MCP integrations. ## Rules 1. **Read before you act.** Always read a skill's `SKILL.md` before executing or referencing it. -2. **Respect boundaries.** Each skill is self-contained. No cross-dependencies between skills. +2. **Respect boundaries.** Skills are self-contained. No cross-dependencies between skills. 3. **Design for composability.** Multiple skills may be loaded at once. Never assume yours is the only one active. 4. **Keep it simple.** Prefer the simplest working solution. 5. **One skill, one job.** Each skill does one thing well. -6. **No secrets in skills.** Reference environment variables — never hardcode credentials. +6. **Description is the trigger.** Vague descriptions don't activate the skill — include trigger phrases users would say. +7. **No secrets in skills.** Reference environment variables — never hardcode credentials. +8. **`marketplace.source.json` is the source of truth.** Never hand-edit the three generated marketplace files. Edit the source and run `python3 scripts/generate-marketplaces.py`. ## Skill Format -Every skill directory must contain a `SKILL.md`: - -```text -my-skill/ -├── SKILL.md ← required — instructions + YAML frontmatter -├── references/ ← optional — docs loaded on-demand -├── scripts/ ← optional — executable code -└── assets/ ← optional — templates, fonts, icons -``` - -### Frontmatter +Every skill directory must contain a `SKILL.md` with YAML frontmatter: ```yaml --- name: skill-name -description: What it does and when to use it +description: What it does and when to use it. Include trigger phrases. --- ``` -### Writing the description +## Validation + +Before committing changes: + +```bash +python3 scripts/validate-manifests.py +``` + +Checks performed: + +1. Generated marketplace files match `marketplace.source.json`. +2. Every plugin's `source` path exists and is a directory. +3. Every skill listed in a virtual plugin is a directory with valid `SKILL.md` frontmatter. +4. Every plugin folder under `plugins/` has `.claude-plugin/plugin.json`, `.codex-plugin/plugin.json`, and `.cursor-plugin/plugin.json`. +5. Every `SKILL.md` has `name` and `description` frontmatter. -- Start with **what** the skill does -- Include **when** to use it — mention trigger phrases users would say -- Be specific — vague descriptions won't trigger +Validation runs on every PR via CI. diff --git a/README.md b/README.md index fcc2a42..7c08d47 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,16 @@ -# Recoupable Skills +# Recoup Skills -AI agent skills for the music industry. Teach your coding agent how to manage artists, write songs, analyze analytics, plan releases, and more. +The official marketplace of AI agent **skills** and **plugins** for the music industry. + +One repo. Multiple installable plugins. Works with Claude Code, Codex, and Cursor. + +## What's in the marketplace + +| Plugin | What it is | What you get | +| ------ | ---------- | ------------ | +| [`recoup-skills`](./skills/) | Broad music-industry skills | Artist management, songwriting, analytics, release campaigns, research, content creation, brand | + +> Self-contained vertical plugins (under `plugins/`) ship in follow-up PRs. ## Install @@ -8,56 +18,56 @@ AI agent skills for the music industry. Teach your coding agent how to manage ar ```bash /plugin marketplace add recoupable/skills +/plugin install recoup-skills@recoup ``` -### Manual - -Clone and point your agent at the `skills/` directory: +### Codex ```bash -git clone https://github.com/recoupable/skills.git +codex plugin marketplace add recoupable/skills +codex plugin install recoup-skills@recoup ``` -## Skills +### Cursor + +Add `recoupable/skills` as a plugin marketplace source. Cursor discovers plugins via `.cursor-plugin/marketplace.json` at the repo root. + +## Skills (broad, shared pool) + +The `recoup-skills` plugin exposes these skills from `./skills/`: | Skill | What it does | -|-------|-------------| -| [getting-started](skills/getting-started) | Install CLI, get API key, connect via MCP/REST — start here | -| [artist-workspace](skills/artist-workspace) | Manage artist directories — context, songs, brand, audience | -| [streaming-growth](skills/streaming-growth) | Grow a new artist past streaming milestones that unlock platform tools | -| [brand-guidelines](skills/brand-guidelines) | Apply Recoupable's brand identity to agent outputs | -| [chartmetric](skills/chartmetric) | Query and analyze music data from the Chartmetric API | -| [content-creation](skills/content-creation) | Create social videos, TikToks, Reels, and visual content using AI primitives | -| [music-industry-research](skills/music-industry-research) | Music industry research — artist analytics, people search, competitive analysis, web intelligence | -| [recoup-api](skills/recoup-api) | Call the Recoupable API from the sandbox — artist data, socials, orgs, reports | -| [release-management](skills/release-management) | Plan and execute release campaigns | -| [setup-sandbox](skills/setup-sandbox) | Scaffold the workspace for an account's orgs and artists | -| [songwriting](skills/songwriting) | Structured songwriting using the 7 C's method | -| [trend-to-song](skills/trend-to-song) | Turn cultural moments into songs and campaign strategies | - -## Creating a Skill - -Every skill needs: - -1. A `SKILL.md` file with YAML frontmatter (`name` + `description`) -2. A clear description that tells the agent **when** to use it -3. Instructions the agent follows to complete the task - -```text -skills/ -└── my-skill/ - ├── SKILL.md ← required - ├── references/ ← optional — docs loaded on-demand - ├── scripts/ ← optional — executable code - └── assets/ ← optional — templates, fonts, icons +| ----- | ------------ | +| [artist-workspace](./skills/artist-workspace) | Manage artist directories — context, songs, brand, audience | +| [chart-metric](./skills/chart-metric) | Query and analyze music data from the Chartmetric API | +| [content-creation](./skills/content-creation) | Create social videos, TikToks, Reels, visual content using AI primitives | +| [create-artist](./skills/create-artist) | 8-step API playbook for onboarding a new artist | +| [getting-started](./skills/getting-started) | Install CLI, get API key, connect via MCP/REST — start here | +| [music-industry-research](./skills/music-industry-research) | Artist analytics, people search, competitive analysis, web intelligence | +| [recoup-api](./skills/recoup-api) | Call the Recoup API from the sandbox — artist data, socials, orgs, reports | +| [release-management](./skills/release-management) | Plan and execute release campaigns | +| [setup-sandbox](./skills/setup-sandbox) | Scaffold the workspace for an account's orgs and artists | +| [song-writing](./skills/song-writing) | Structured songwriting using the 7 C's method | +| [streaming-growth](./skills/streaming-growth) | Grow a new artist past streaming milestones | +| [trend-to-song](./skills/trend-to-song) | Turn cultural moments into songs and campaign strategies | + +## Maintenance + +After editing `marketplace.source.json`: + +```bash +python3 scripts/generate-marketplaces.py # regenerate platform marketplaces +python3 scripts/validate-manifests.py # full repo validation ``` -See [contributing.md](contributing.md) for guidelines. +Both scripts are zero-dependency Python 3.9+ stdlib. CI runs `scripts/validate-manifests.py` on every PR. + +For repo layout, skill format, and contribution rules, see [`AGENTS.md`](./AGENTS.md) and [`contributing.md`](./contributing.md). ## About -[Recoupable](https://recoupable.com) is an AI-powered music management platform. These skills power the agents that help artists and labels manage their careers. +[Recoup](https://recoupable.com) is an AI-powered music management platform. -- **Website**: [recoupable.com](https://recoupable.com) - **App**: [chat.recoupable.com](https://chat.recoupable.com) - **Support**: agent@recoupable.com +- **License**: [Apache-2.0](./LICENSE) diff --git a/contributing.md b/contributing.md index bd5a623..d223ede 100644 --- a/contributing.md +++ b/contributing.md @@ -1,43 +1,29 @@ # Contributing -We welcome contributions — especially new skills for the music industry. +We welcome new music skills and new vertical plugins. -## Adding a Skill +## Where things go -1. Create a new directory under `skills/` with a `SKILL.md`: +| Type | Location | +|------|----------| +| Generic music skill | `skills/{name}/` — listed in the `recoup-skills` plugin | +| Vertical workflow (3+ skills + scripts/agents/commands) | `plugins/{name}/` — self-contained plugin | +| New skill inside an existing plugin | `plugins/{plugin}/skills/{name}/` | -``` -skills/my-new-skill/ -└── SKILL.md -``` +When in doubt, start as a skill under `skills/`. Promote to a plugin once it grows past three related skills AND needs scripts, agents, or commands. -2. Fill in the frontmatter: - -```yaml ---- -name: my-new-skill -description: What it does and when to use it. Include trigger phrases. ---- -``` - -3. Write clear instructions in the body. Keep it under 5,000 words — move heavy reference material to a `references/` subdirectory. +## How to add anything +1. Read [`AGENTS.md`](./AGENTS.md) — it documents the skill format, plugin layout, and validation rules. +2. Edit `marketplace.source.json` to register what you added. +3. Run: + ```bash + python3 scripts/generate-marketplaces.py + python3 scripts/validate-manifests.py + ``` 4. Open a pull request. -## Skill Guidelines - -- **One skill, one job.** Each skill does one thing well. -- **Self-contained.** No cross-dependencies between skills. -- **Description is the trigger.** The `description` field determines when an agent loads the skill. Be specific — include the phrases a user would actually say. -- **Real instructions, not theory.** Tell the agent exactly what to do, step by step. -- **No secrets.** Don't include API keys, tokens, or credentials. Reference environment variables instead. - -## Skill Size - -- `SKILL.md` body: under 5,000 words -- Put large reference material in `references/` -- Put executable code in `scripts/` -- Critical instructions go at the top +CI runs the validator on every PR. ## Code of Conduct diff --git a/marketplace.source.json b/marketplace.source.json new file mode 100644 index 0000000..91e67b3 --- /dev/null +++ b/marketplace.source.json @@ -0,0 +1,62 @@ +{ + "$comment": "Single source of truth for Recoup's marketplace. Run `python3 scripts/generate-marketplaces.py` to regenerate the three platform marketplace files (.claude-plugin/, .agents/plugins/, .cursor-plugin/). Hand-edits to those generated files will be overwritten.", + "marketplace": { + "name": "recoup", + "version": "1.0.0", + "description": "Recoup's marketplace of AI agent skills and plugins for the music industry.", + "owner": { + "name": "Recoup", + "email": "agent@recoupable.com", + "url": "https://recoupable.com" + }, + "homepage": "https://recoupable.com", + "repository": "https://github.com/recoupable/skills", + "license": "Apache-2.0", + "brandColor": "#345A5D" + }, + "plugins": [ + { + "name": "recoup-skills", + "version": "1.0.0", + "description": "Music-industry skills for AI agents — artist management, songwriting, analytics, release campaigns, research, content creation.", + "category": "Music", + "type": "virtual", + "source": "./", + "skills": [ + "./skills/artist-workspace", + "./skills/chart-metric", + "./skills/content-creation", + "./skills/create-artist", + "./skills/getting-started", + "./skills/music-industry-research", + "./skills/recoup-api", + "./skills/release-management", + "./skills/setup-sandbox", + "./skills/song-writing", + "./skills/streaming-growth", + "./skills/trend-to-song" + ], + "keywords": [ + "music", + "artist-management", + "songwriting", + "music-marketing", + "analytics", + "release-management", + "chartmetric", + "content-creation", + "agent-skills" + ], + "interface": { + "displayName": "Recoup Skills", + "shortDescription": "Music industry skills for AI agents", + "longDescription": "Teach your coding agent how to manage artists, write songs, analyze music analytics, plan releases, and create content. General skills work for anyone; platform skills integrate with the Recoup platform.", + "developerName": "Recoup", + "category": "Music", + "capabilities": ["Read", "Write"], + "websiteURL": "https://recoupable.com", + "brandColor": "#345A5D" + } + } + ] +} diff --git a/scripts/generate-marketplaces.py b/scripts/generate-marketplaces.py new file mode 100755 index 0000000..51cd37b --- /dev/null +++ b/scripts/generate-marketplaces.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python3 +""" +Generate Recoup marketplace files for Claude Code, Codex, and Cursor. + +Reads marketplace.source.json (the single source of truth) and writes three +platform-specific marketplace files: + + - .claude-plugin/marketplace.json (Claude Code) + - .agents/plugins/marketplace.json (Codex) + - .cursor-plugin/marketplace.json (Cursor) + +Usage: + python3 scripts/generate-marketplaces.py [--check] + + --check exit non-zero if generated files differ from source-of-truth + (suitable for CI). + +Zero external dependencies. Python 3.9+ stdlib only. +""" +from __future__ import annotations + +import argparse +import json +import sys +from pathlib import Path +from typing import Any + +REPO_ROOT = Path(__file__).resolve().parent.parent +SOURCE_FILE = REPO_ROOT / "marketplace.source.json" + +CLAUDE_OUT = REPO_ROOT / ".claude-plugin" / "marketplace.json" +CODEX_OUT = REPO_ROOT / ".agents" / "plugins" / "marketplace.json" +CURSOR_OUT = REPO_ROOT / ".cursor-plugin" / "marketplace.json" + + +def load_source() -> dict[str, Any]: + if not SOURCE_FILE.exists(): + sys.exit(f"error: {SOURCE_FILE} not found") + with SOURCE_FILE.open() as fh: + data = json.load(fh) + return data + + +def build_claude(source: dict[str, Any]) -> dict[str, Any]: + """Claude Code marketplace.json (anthropics/skills + anthropics/claude-plugins-official shape).""" + mp = source["marketplace"] + out: dict[str, Any] = { + "name": mp["name"], + "owner": mp["owner"], + "metadata": { + "description": mp["description"], + "version": mp["version"], + }, + "plugins": [], + } + for plugin in source["plugins"]: + entry: dict[str, Any] = { + "name": plugin["name"], + "description": plugin["description"], + "version": plugin["version"], + "source": plugin["source"], + "category": plugin["category"], + "keywords": plugin.get("keywords", []), + } + if plugin["type"] == "virtual": + entry["strict"] = False + entry["skills"] = plugin["skills"] + if "tags" in plugin: + entry["tags"] = plugin["tags"] + out["plugins"].append(entry) + return out + + +def build_codex(source: dict[str, Any]) -> dict[str, Any]: + """Codex marketplace.json with richer policy/auth metadata per Codex spec.""" + mp = source["marketplace"] + out: dict[str, Any] = { + "name": mp["name"], + "description": mp["description"], + "owner": mp["owner"], + "interface": { + "displayName": "Recoup", + }, + "plugins": [], + } + for plugin in source["plugins"]: + entry: dict[str, Any] = { + "name": plugin["name"], + "description": plugin["description"], + "source": { + "source": "local", + "path": plugin["source"], + }, + "policy": { + "installation": "AVAILABLE", + "authentication": "ON_INSTALL", + }, + "category": plugin["category"], + "keywords": plugin.get("keywords", []), + } + if "interface" in plugin: + entry["interface"] = plugin["interface"] + out["plugins"].append(entry) + return out + + +def build_cursor(source: dict[str, Any]) -> dict[str, Any]: + """Cursor marketplace.json (matches current Recoup Cursor pattern).""" + mp = source["marketplace"] + out: dict[str, Any] = { + "name": mp["name"], + "owner": { + "name": mp["owner"]["name"], + "email": mp["owner"]["email"], + }, + "metadata": { + "description": mp["description"], + }, + "plugins": [], + } + for plugin in source["plugins"]: + entry: dict[str, Any] = { + "name": plugin["name"], + "source": plugin["source"].lstrip("./") or ".", + "description": plugin["description"], + "version": plugin["version"], + "author": { + "name": mp["owner"]["name"], + "email": mp["owner"]["email"], + }, + "homepage": mp["homepage"], + "repository": mp["repository"], + "license": mp["license"], + "category": plugin["category"], + "keywords": plugin.get("keywords", []), + } + if "tags" in plugin: + entry["tags"] = plugin["tags"] + out["plugins"].append(entry) + return out + + +def serialize(data: dict[str, Any]) -> str: + return json.dumps(data, indent=2, ensure_ascii=False) + "\n" + + +def write_if_changed(path: Path, content: str, *, check: bool) -> bool: + """Return True when path matches content; False when it differs.""" + path.parent.mkdir(parents=True, exist_ok=True) + existing = path.read_text() if path.exists() else None + if existing == content: + return True + if check: + return False + path.write_text(content) + print(f"wrote {path.relative_to(REPO_ROOT)}") + return True + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--check", + action="store_true", + help="exit non-zero if generated files would change (for CI)", + ) + args = parser.parse_args() + + source = load_source() + + targets = [ + (CLAUDE_OUT, build_claude(source)), + (CODEX_OUT, build_codex(source)), + (CURSOR_OUT, build_cursor(source)), + ] + + all_match = True + for path, data in targets: + if not write_if_changed(path, serialize(data), check=args.check): + all_match = False + print(f"would update {path.relative_to(REPO_ROOT)}", file=sys.stderr) + + if args.check and not all_match: + print( + "error: marketplace files are stale. Run " + "`python3 scripts/generate-marketplaces.py` and commit the result.", + file=sys.stderr, + ) + return 1 + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/validate-manifests.py b/scripts/validate-manifests.py new file mode 100755 index 0000000..9bfe53e --- /dev/null +++ b/scripts/validate-manifests.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python3 +""" +Validate every marketplace, plugin manifest, and skill in this repo. + +Checks performed: + 1. Generated marketplace files match marketplace.source.json + (delegates to `generate-marketplaces.py --check`). + 2. Each plugin's `source` path resolves to an existing directory. + 3. Each `skills` entry in a virtual marketplace plugin resolves to a real + skill folder with a valid SKILL.md (frontmatter: name + description). + 4. Each self-contained plugin folder under `plugins/` exposes the expected + manifests (`.claude-plugin/plugin.json`, `.codex-plugin/plugin.json`, + and `.cursor-plugin/plugin.json`). + 5. Every SKILL.md under skills/ has `name` and `description` frontmatter. + +Zero external dependencies. Python 3.9+ stdlib only. + +Exit code 0 = all OK. Non-zero = at least one validation error printed to stderr. +""" +from __future__ import annotations + +import json +import re +import subprocess +import sys +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent +SOURCE_FILE = REPO_ROOT / "marketplace.source.json" + +CLAUDE_MP = REPO_ROOT / ".claude-plugin" / "marketplace.json" +CODEX_MP = REPO_ROOT / ".agents" / "plugins" / "marketplace.json" +CURSOR_MP = REPO_ROOT / ".cursor-plugin" / "marketplace.json" + +ROOT_SKILLS_DIR = REPO_ROOT / "skills" +PLUGINS_DIR = REPO_ROOT / "plugins" + +FRONTMATTER_RE = re.compile(r"^---\s*\n(.*?)\n---", re.DOTALL) +FRONTMATTER_FIELD_RE = re.compile(r"^([A-Za-z_][\w-]*)\s*:\s*(.+?)\s*$", re.MULTILINE) + + +class ValidationError(Exception): + """Single named validation failure.""" + + +def report(errors: list[str], msg: str) -> None: + errors.append(msg) + print(f" ✗ {msg}", file=sys.stderr) + + +def check_generator(errors: list[str]) -> None: + print("→ checking marketplace generator parity") + result = subprocess.run( + [sys.executable, str(REPO_ROOT / "scripts" / "generate-marketplaces.py"), "--check"], + capture_output=True, + text=True, + ) + if result.returncode != 0: + report(errors, f"generated marketplace files are out of sync. {result.stderr.strip()}") + + +def parse_frontmatter(skill_md: Path) -> dict[str, str]: + text = skill_md.read_text(encoding="utf-8") + match = FRONTMATTER_RE.match(text) + if not match: + raise ValidationError(f"{skill_md.relative_to(REPO_ROOT)}: missing YAML frontmatter") + fields: dict[str, str] = {} + for m in FRONTMATTER_FIELD_RE.finditer(match.group(1)): + fields[m.group(1)] = m.group(2) + return fields + + +def check_skill(skill_dir: Path, errors: list[str]) -> None: + skill_md = skill_dir / "SKILL.md" + rel = skill_dir.relative_to(REPO_ROOT) + if not skill_md.exists(): + report(errors, f"{rel}: SKILL.md missing") + return + try: + fields = parse_frontmatter(skill_md) + except ValidationError as exc: + report(errors, str(exc)) + return + if not fields.get("name"): + report(errors, f"{rel}/SKILL.md: frontmatter missing `name`") + if not fields.get("description"): + report(errors, f"{rel}/SKILL.md: frontmatter missing `description`") + + +def check_root_skills(errors: list[str]) -> None: + print(f"→ checking skills under {ROOT_SKILLS_DIR.relative_to(REPO_ROOT)}/") + if not ROOT_SKILLS_DIR.exists(): + return + for entry in sorted(ROOT_SKILLS_DIR.iterdir()): + if entry.is_dir() and not entry.name.startswith("."): + check_skill(entry, errors) + + +def check_plugin_dir(plugin_dir: Path, errors: list[str]) -> None: + rel = plugin_dir.relative_to(REPO_ROOT) + claude = plugin_dir / ".claude-plugin" / "plugin.json" + codex = plugin_dir / ".codex-plugin" / "plugin.json" + cursor = plugin_dir / ".cursor-plugin" / "plugin.json" + if not claude.exists(): + report(errors, f"{rel}: missing .claude-plugin/plugin.json") + if not codex.exists(): + report(errors, f"{rel}: missing .codex-plugin/plugin.json") + if not cursor.exists(): + report(errors, f"{rel}: missing .cursor-plugin/plugin.json") + plugin_skills = plugin_dir / "skills" + if plugin_skills.exists(): + for entry in sorted(plugin_skills.iterdir()): + if entry.is_dir() and not entry.name.startswith("."): + check_skill(entry, errors) + + +def check_self_contained_plugins(errors: list[str]) -> None: + if not PLUGINS_DIR.exists(): + return + print(f"→ checking plugins under {PLUGINS_DIR.relative_to(REPO_ROOT)}/") + for entry in sorted(PLUGINS_DIR.iterdir()): + if entry.is_dir() and not entry.name.startswith("."): + check_plugin_dir(entry, errors) + + +def check_source_paths(errors: list[str]) -> None: + print("→ checking marketplace.source.json plugin paths") + with SOURCE_FILE.open() as fh: + data = json.load(fh) + for plugin in data.get("plugins", []): + name = plugin["name"] + source = plugin["source"] + target = (REPO_ROOT / source).resolve() + if not target.exists(): + report(errors, f"plugin `{name}`: source path `{source}` does not exist") + continue + if not target.is_dir(): + report(errors, f"plugin `{name}`: source path `{source}` is not a directory") + continue + if plugin.get("type") == "virtual": + for skill_path in plugin.get("skills", []): + resolved = (REPO_ROOT / skill_path).resolve() + if not resolved.exists(): + report(errors, f"plugin `{name}`: skill path `{skill_path}` does not exist") + continue + if not resolved.is_dir(): + report(errors, f"plugin `{name}`: skill path `{skill_path}` is not a directory") + continue + check_skill(resolved, errors) + + +def main() -> int: + errors: list[str] = [] + + check_generator(errors) + check_source_paths(errors) + check_root_skills(errors) + check_self_contained_plugins(errors) + + print() + if errors: + print(f"✗ {len(errors)} validation error(s)", file=sys.stderr) + return 1 + print("✓ all manifests valid") + return 0 + + +if __name__ == "__main__": + sys.exit(main())