One source of truth for agent plugins across AI app ecosystems.
pluginpack compiles portable skills, commands, agents, rules, hooks, assets, and metadata into the native plugin layouts expected by each AI app.
pluginpack is intentionally boring: it copies files, writes the manifests each target expects, and validates the result. It is a build tool, not a package manager or publisher.
Agent apps increasingly support similar ideas: skills, commands, agents, rules, hooks, MCP configuration, and plugin marketplaces. The packaging formats are different enough that maintaining one repo per app quickly drifts.
pluginpack helps when you want to:
- keep skills and related plugin files in one source repo
- emit installable plugin directories for more than one app
- allow target-specific overrides where portability breaks down
- detect when generated plugin repos are stale
It does not try to make every app behave the same. Target adapters own target-specific layout, manifests, and validation.
The preferred path is one public plugin repository with a top-level skills/ directory as the portable install surface, plus generated native plugin outputs in the same repo.
skills/
release-notes/
SKILL.md
pluginpack.config.ts
.cursor-plugin/
marketplace.json
plugins/
cursor/
acme/
.cursor-plugin/plugin.json
skills/
claude/
acme/
.claude-plugin/plugin.json
skills/
gemini/
.pluginpack/
gemini.json
acme/
gemini-extension.json
skills/
.claude-plugin/
marketplace.json
.pluginpack/
cursor.json
claude.jsonUsers should install portable skills from the skills/ subpath, for example npx skills add owner/repo/skills --skill '*'. The repo root is intentionally also home to generated native plugin outputs, so the subpath keeps skills CLI discovery focused on the canonical portable skills. Claude, Cursor, and other native plugin users install from the generated marketplace/plugin layout their app expects.
pluginpack writes a .pluginpack/<target>.json managed-file manifest for each built target. That manifest lets builds and cleanup commands remove stale generated files without touching source files or unmanaged repo content.
npm install -D @gleanwork/pluginpackimport { defineConfig } from "@gleanwork/pluginpack";
export default defineConfig({
name: "acme-plugins",
version: "0.1.0",
source: {
skills: "skills",
rootPlugin: {
id: "core",
description: "Acme portable skills.",
},
},
metadata: {
description: "Acme agent plugins.",
author: { name: "Acme" },
license: "MIT",
},
targets: {
cursor: {
outDir: ".",
plugins: {
acme: {
from: ["core"],
path: "plugins/cursor/acme",
components: ["skills"],
},
},
},
claude: {
outDir: ".",
pluginRoot: "plugins/claude",
plugins: {
acme: { from: ["core"] },
},
},
gemini: {
outDir: "plugins/gemini",
plugins: {
acme: { from: ["core"] },
},
},
copilot: {
outDir: "plugins/copilot",
plugins: {
acme: { from: ["core"] },
},
},
},
});source.skills points at the repo-level skills directory. source.rootPlugin.id creates the source plugin name used by each target's from array.
For more complex source content, keep source plugins under plugins/ and emit them into one or more target outputs:
plugins/
core/
plugin.pluginpack.json
.mcp.json
skills/
release-notes/
SKILL.md
agents/
commands/
rules/
hooks/
assets/A target can emit a source plugin directly, rename it, or merge multiple source plugins into one emitted plugin.
A source plugin declares MCP servers with a standard .mcp.json file at its root ({ "mcpServers": { "name": { ... } } }), or with an mcpServers key in plugin.pluginpack.json. The file wins if both are present, and merging plugins with the same server name is an error.
Each target wires MCP into its native shape: claude ships .mcp.json at the plugin root (auto-discovered), cursor references it from plugin.json ("mcpServers": "./.mcp.json"), copilot references it from the marketplace entry, and gemini inlines the server map into gemini-extension.json.
There are two reasonable alternatives when the single-repo shape is not enough:
- Single source repo, multiple output repos: best when each target ecosystem expects its own repo root shape.
- Single source repo, release artifacts: best when users install zipped plugin payloads or release assets instead of browsing generated files in Git.
Skill files are not always perfectly portable. When one app needs different frontmatter or content, add a target override next to the base file:
skills/release-notes/SKILL.md
skills/release-notes/targets/cursor/SKILL.md
skills/release-notes/targets/claude/SKILL.mdResolution order is target override first, then the base file.
The first adapters are:
cursorclaudegeminicopilot
gemini emits Gemini CLI extensions with a gemini-extension.json manifest. copilot emits the GitHub Copilot plugins format (per github/copilot-plugins): a .claude-plugin/marketplace.json mirrored to .github/plugin/marketplace.json, each plugin under plugins/<name>/ with a skills array per marketplace entry. Because Copilot reuses the Claude marketplace layout, the claude and copilot targets both write .claude-plugin/marketplace.json and therefore need separate output roots (distinct outDirs or separate repos).
More targets should be added from official docs or real plugin examples, not guessed abstractions.
For one target, copying files by hand may be enough. pluginpack starts to earn its keep when you need deterministic manifests, target-specific overrides, validation, and CI checks across multiple target repos.
pluginpack diff is designed for automation. It builds into a temporary directory, compares generated managed files against an existing plugin repo, and exits non-zero when the plugin repo is stale:
pluginpack diff --target cursor --against ../cursor-plugins
pluginpack diff --target claude --against ../claude-pluginsUse that in CI to fail clearly or to trigger an action that opens a PR against the generated plugin repo.
When a generated target repo intentionally owns a path, add ignoredDiffPaths to that target config. Entries are target-output-relative paths; a directory entry ignores everything below it.
Create a starter pluginpack.config.ts and source plugin layout.
pluginpack init [options]Examples:
pluginpack init
Exit codes:
- 0 when files are created
- 1 when files already exist or cannot be written
Compile configured source plugins into target-native plugin payloads.
pluginpack build [--target cursor|claude|gemini|copilot] [--out-dir <path>] [--dry-run]Options:
--target <target>: Build only one configured target.--out-dir <path>: Override the configured output directory for the selected target.--dry-run: Resolve and print planned managed output paths without writing files.
Examples:
pluginpack buildpluginpack build --target cursorpluginpack build --target claude --dry-run
Exit codes:
- 0 when all selected targets build
- 1 when config, source resolution, or file output fails
Validate an existing target output directory for native manifest, path, and frontmatter requirements.
pluginpack validate --target cursor|claude|gemini|copilot [--dir <path>]Options:
--target <target>: Required target validator.--dir <path>: Directory to validate. Defaults to the configured target outDir.
Examples:
pluginpack validate --target cursor --dir ../cursor-plugins
Exit codes:
- 0 when validation passes
- 1 when validation finds errors
Build into a temporary directory and compare generated managed files with an existing target repo.
pluginpack diff --target cursor|claude|gemini|copilot --against <path>Options:
--target <target>: Required target to build and compare.--against <path>: Existing target repo or output directory to compare against.
Examples:
pluginpack diff --target cursor --against ../cursor-plugins
Exit codes:
- 0 when managed files match
- 1 when managed files differ or the command fails
Remove stale managed files that are no longer emitted by the current config.
pluginpack prune [--target cursor|claude|gemini|copilot] [--dry-run]Options:
--target <target>: Prune only one configured target.--dry-run: Print stale managed files without deleting them.--force: Delete even paths that resolve inside the source tree or config.
Examples:
pluginpack prunepluginpack prune --target claude --dry-run
Exit codes:
- 0 when stale managed files are removed or listed
- 1 when config, source resolution, or cleanup fails
Remove all managed files for configured target outputs.
pluginpack clean [--target cursor|claude|gemini|copilot] [--dry-run]Options:
--target <target>: Clean only one configured target.--dry-run: Print managed files without deleting them.--force: Delete even paths that resolve inside the source tree or config.
Examples:
pluginpack cleanpluginpack clean --target cursor --dry-run
Exit codes:
- 0 when managed files are removed or listed
- 1 when config, manifest loading, or cleanup fails
Generate the README CLI reference section from command metadata.
pluginpack docs [options]Options:
--check: Fail if README.md is not up to date.
Examples:
pluginpack docspluginpack docs --check
Exit codes:
- 0 when docs are current or updated
- 1 when --check finds stale docs