cyberuni/universal-plugin
Research and design toolkit for building universal AI coding agent plugins that work across Claude Code, Cursor, Codex, and GitHub Copilot CLI.
universal-plugin
0.14.0
Minor Changes
- 2fa9ef4:
plugin buildadds the pnpm skill name beside each/<plugin>:<skill>reference in a skill's Markdown:`/my-plugin:doctor` (pnpm install: `pnpm-my-plugin-doctor`). pnpm links a skill aspnpm-<package>-<skill>(a scoped@scope/namebecomespnpm-@scope+name-<skill>), so the plugin-qualified name alone resolves to nothing there. Authors still write the reference once. The name comes from the plugin root'spackage.json; references to other plugins or unknown skills, fenced code, and already-rewritten references are left alone.
0.13.0
Minor Changes
-
d89e609:
plugin buildcompiles skill script entries into self-contained.mjsfiles. Declare them underextensions["org.cyberuni.universal-plugin"].scripts, mapping an output path to a source ("skills/doctor/scripts/doctor.mjs": "src/skills/doctor.ts", or{ "source", "command" }). Each entry is compiled with rolldown: Node built-ins stay imports and everything else is inlined, so the script works from a skill that pnpm links or copies on its own. Commit the output.- Each compiled file embeds
globalThis.__SKILL_SCRIPT__: the package and bin that ship the plugin, the version fromplugin.json, and a map from every declared command path to its entry. --dry-runreports each committed script asunchanged,missing,staleorversion.doctorreports a missing or stale one asstale-script. A script behind only on its embedded version isversion-drift, whichpublish check-versionnow checks, so one file is never reported twice.- A compiled script over 150 KB warns that the skill should call the installed CLI instead.
plugin versionstamps the new version into each compiled script's header, and nothing else in it. Pins inside compiled code are left to the build.
- Each compiled file embeds
-
517d001: Add
publish check-version, a read-only check that fails when thepackagePathpackage.json,plugin.json, the derived vendor manifests, the repository catalogs, or the skillnpx/upxpins carry different versions, naming each file left behind.doctornow reports oneversion-driftfinding per such file, and offers to add the check to the repository's verify script or CI. -
ec08877:
plugin init --npmcopies the packageversioninto theplugin.jsonit writes, and in a changesets repository wires the rootversionscript (changeset version && universal-plugin plugin version --from-package --root <plugin-root>) plus auniversal-plugindevDependency. A customversionscript is left alone and init prints the line to add. A script still runningpublish sync-versionis told to switch. -
b105a4d:
plugin versionis now the single version stamp. One call writes the version intoplugin.json, thepackagePathpackage.json, thenpx/upxpins in every skill file (scripts included),.plugin/pins.json, and each authoredmcpServersentry markedpinToPluginVersion, then re-derives the vendor manifests. It takes the number from a release type, an explicit version (semantic-release'sprepareCmd), or the new--from-package(changesets and release-please). WherepackagePathis declared, a release type is refused, because the release picks the number.plugin buildno longer decides any version: a markedmcpServersentry is copied as authored and only its marker is stripped.plugin bundleandpublish sync-versionare deprecated aliases that warn, and are removed in the next minor release.publish check-versionanddoctorreport a place holding0.0.0-developmentas unstamped rather than as drift, and compare the markedmcpServerspin too. -
d89e609:
plugin buildwarns on a relative Markdown link in a skill that climbs out of the skill folder (../../readme.md). pnpm links or copies each skill folder on its own, so such a link resolves to nothing there.doctorreports it asskill-link-escapes.
Patch Changes
- ab48ed1: Fix universal-plugin's own skill scripts when pnpm copies a skill instead of linking it. The six that run a CLI command (
plugin build,plugin init,plugin version,marketplace add,marketplace init,marketplace validate) call the installed CLI (pnpm exec universal-plugin), elsenpx universal-plugin@<version>pinned to the plugin's version.doctor,rewrite-upxandinstall-docsare self-contained.doctornow accepts a$schemaonly when it is hosted onagent-plugins.org, not a URL that merely mentions the name. - d5e6c0d:
plugin build,plugin version,publish sync-versionandplugin initnow write JSON in the format the file already has: its indentation, line ending, trailing newline, and the original text of every value they leave unchanged, so an array a formatter collapsed onto one line stays there. A new file, or one with no indentation to read, takes the indentation.editorconfigsets for it. - ca9abcb: Document in the
plugin-designreference thatrulesis for the HCF of all agents only, and that role- or task-scoped guidance ships as a reference loaded on demand.
0.12.2
Patch Changes
- d3650b6: Fix stale and dangling references in the shipped skills and the agentskills-specialist agent: point skills at
doctor-universal-plugin, link ADRs by URL, state current rules instead of migration-relative ones, and drop the project-specific commit and memory rules from the agent.
0.12.1
Patch Changes
- da18173: Depend on
@cyberuni/agent-harness^0.5.0. That release ships theagent-harnessCLI, sonpx @cyberuni/agent-harness reference show <name>reads this package's documents without a version pin. - 1904555: Point reference loading at the
cyber-agent-harnessplugin. Thereferenceskill moved there frombuddy-agent-harnessin buddy-agent-harness@0.20.0, so the shipped skills and references now namecyber-agent-harness, and the plugin declares it as a dependency.
0.12.0
Minor Changes
- c82b27b:
plugin buildnow checks each declared dependency against the marketplace catalogs it refreshes. A bare dependency the catalog does not list, or one qualified with a marketplace the catalog does not allow inallowCrossMarketplaceDependenciesOn, warns and names the fix. Pass--strict-dependenciesto fail the build on it instead. A dependency that declares asource, such as{ "name": "cyber-asana", "source": { "source": "npm", "package": "cyber-asana" } }, is listed in the catalog for you. - 602e1c8:
plugin buildno longer warns about a dependency qualified with the catalog's own name, such ascyber-asana@uip-pods-local, when the catalog does not list it. Only a plain dependency (a bare name, or an object with nomarketplace) has to be listed in the catalog. Naming a marketplace is an explicit choice by the author. - f87bfc8:
marketplace validatenow checks, in the Claude catalog, that each listed plugin's dependencies can resolve from it. A plain dependency (a bare name, or an object with nomarketplace) the catalog does not list is an issue, and so is a dependency on another marketplace that the catalog does not allow inallowCrossMarketplaceDependenciesOn. It reads what is on disk: an entry's owndependencies, and the manifest of a plugin at a./source.marketplace init --forcenow keeps a catalog'sallowCrossMarketplaceDependenciesOnwhen it regenerates the file. - b0558cd: The
publish-pluginskill now reads the plugin's dependencies. A plain dependency the target marketplace does not list is listed in the same PR, or the skill stops and asks for it to be published first. A dependency on another marketplace gets an offer to add it to the catalog'sallowCrossMarketplaceDependenciesOn.
0.11.3
Patch Changes
- 4a0e9ac:
doctorno longer reportscom.github.copilot/as stale after a rebuild. It compares the newest file in the directory againstplugin.json, because a directory's own mtime does not move when its files are rewritten in place. - 4a0e9ac:
publish sync-versionnow re-derives the vendor manifests after it moves the version, the same wayplugin versiondoes, so they no longer keep the old number. Pass--no-buildto skip that step when your release script runsplugin builditself. Theversionanddoctor-universal-pluginskills now namebuild-pluginas the follow-up step.
0.11.2
Patch Changes
- c8b81c8:
plugin buildno longer writes Codex custom prompts to~/.codex/prompts/. Codex invokes plugin skills natively with$nameor/skills, and its custom prompts are deprecated and no longer read, so the build now writes nothing outside the plugin tree. You can delete prompts that an earlier build left in~/.codex/prompts/. - 642f70e:
plugin buildnow leaves a component path out of a vendor's derived manifest when that vendor has no such component, and warns:codex has no "agents" component — the path is left out of .codex-plugin/plugin.json. Codex readsskills,commands,apps,hooks, andmcpServers; Cursor readsskills,commands,agents,rules,hooks, andmcpServers; Claude Code reads every component exceptrulesandapps. Aharnesses.<vendor>override is never filtered.doctorreports the case asunsupported-component.
0.11.1
Patch Changes
- d8bfd5b: Skills now load references with the
referenceskill in thebuddy-agent-harnessplugin, which replaces itsload-referenceskill. The line reads "Loadplugin-designwith thereferenceskill in thebuddy-agent-harnessplugin."
0.11.0
Minor Changes
- 55fdc24: Remove the retired
governancecommand.governance showandgovernance listare now unknown commands. Usebuddy-agent-harness reference show|list|search, or theload-referenceskill in thebuddy-agent-harnessplugin, to read the same documents.
Patch Changes
- dd275ba:
plugin init --scaffoldcreatesreferences/instead ofgovernances/, the folderbuddy-agent-harness referencereads. - 4ad2dc8:
preparenow reads installed plugins the way each harness records them. It reads Claude Code's version 2installed_plugins.jsonby plugin name and scope, and it reads Copilot CLI'sinstalled-pluginsfolders and the Codex plugin cache. Plugin paths come from@cyberuni/agent-harness, which honorsCLAUDE_CONFIG_DIR,CODEX_HOME, andCOPILOT_HOME. - 4ad2dc8: The vendor registry no longer carries
globalManifestorglobalPluginDir, and Cursor's local plugin folder now comes from@cyberuni/agent-harness.plugin installfor Cursor therefore honors the harness's config directory. AlocalPluginDirin~/.agents/universal-plugin-vendors.jsonstill wins.
0.10.0
Minor Changes
-
e0b672d: A new
build-pluginskill is the entry point forplugin build. It runs the project's own build script when one exists, and otherwise the CLI shipped beside it throughscripts/build.mjs, so nothing is downloaded. It reads the vendor and catalog rows back, reports each warning about something a vendor cannot represent, and hands abuilt 0result todoctor-universal-plugin. -
738df81:
marketplace addlists a plugin the repository does not hold, so a repository can curate a marketplace instead of only publishing its own plugins. One positional says where the plugin lives and is read by shape: a./path, anowner/reposlug, a git URL, an npm package (npm:pkgor@scope/pkg), or<plugin>@<marketplace>— with--path,--npm,--github,--url, and--from-marketplaceto force the reading whereowner/repoand a relative path collide. Nothing is fetched: metadata comes from a path's ownplugin.json, an installednode_modulescopy, the entry being copied, or the--description/--version/--homepage/--repository/--license/--keywordsflags. A<plugin>@<marketplace>entry is resolved from a marketplace the runtime has already added, or from--from <dir>. A source that is relative to that marketplace is rewritten rather than copied, against the origin the runtime recorded for it or its clone's own git remote:./plugins/acedincyberuni/cyberplacebecomes{ "source": "git-subdir", "url": "https://github.com/cyberuni/cyberplace.git", "path": "plugins/aced" }, and an entry at the marketplace root becomesgithuborurl. An entry only reaches a catalog whose runtime resolves that source — npm goes to Claude Code and Codex, and Copilot CLI and Cursor are reportedskippedwith the reason rather than written a source they refuse.A regeneration no longer discards the entries it did not derive.
marketplace init,plugin build, andplugin initkeep an entry whose source is not a local path, and keep a non-local source on a plugin they do discover while refreshing its derived metadata — so a plugin distributed through npm is not rewritten to a repository path holding gitignored build output, andaddandinitcompose on one repository. A local-path entry discovery no longer finds is still dropped.The
marketplaceskill is now a gateway over three routes (init,add,validate), each with its own reference, and ships ascripts/add.mjswrapper. -
2ba0e9c: Retire
governanceand the build's governance copies in favor ofbuddy-agent-harness referenceand itsload-referenceskill.governance showandgovernance listno longer read documents. For this release they print the replacement (buddy-agent-harness reference show <name>, or theload-referenceskill in thebuddy-agent-harnessplugin) and exit 1. The next release removes the command.plugin buildno longer copies governances intoskills/*/references/governances/, andplugin build --checkis gone. Remove--checkfrom CI; it now fails as an unknown option.- The documents this package owns (
plugin-design,slash-invocation, and auniversal-pluginpointer toplugin-design) ship underreferences/instead ofgovernances/, soreference showfinds them in any project that depends onuniversal-plugin. - The
init-universal-pluginskill loadsplugin-designthrough theload-referenceskill.
Patch Changes
- c430c4a:
publish sync-versionno longer requirespackagePath. With none set in.agents/universal-plugin.json, it reads thepackage.jsonat the plugin root, so a single-package repository wherepackage.jsonsits besideplugin.jsonneeds no config file. It fails only when that file is missing too, and the error names both places it looked. An explicitpackagePathstill wins.
0.9.0
Minor Changes
- 8fde5f7:
plugin buildcopies each governance into the skills that use it, so a skill reads its rule sets from disk instead of runningnpx <pkg> governance show <name>at run time — no registry lookup per read, and no network while the skill works. The.mdfiles already in<skill>/references/governances/declare which governances that skill uses; the build rewrites each from the package that owns it, copies the governances those reference, transitively, and rewrites everygovernance show <other>pointer inside a copy into an instruction to load the sibling copy. The build fails when a declared file names no governance, when a referenced one has no copy to point at, or whenSKILL.mddoes not list a copy under References. Commit the copies — a git-sourced install has no build step — and run the newplugin build --checkin CI, which writes nothing and exits non-zero naming each copy that differs from its source. - 7836787: Rename the
doctorskill todoctor-universal-plugin. Other plugins ship their owndoctorskill, and the bare name collided when more than one was installed —buddy-agent-harnessalready resolved this by namespacing its own asdoctor-buddy-agent-harness. Invoke it as/universal-plugin:doctor-universal-plugin; the CLI's own next-step hints now name that too.
0.8.0
Minor Changes
-
cbda7c2:
config get --key packagePathnow readspackagePathinstead of rejecting it.--format jsonprints the declared path as a JSON string, relative to the plugin root, ornullwhen no npm package is declared.plugin version,publish sync-version, andconfig getshare one reader, so they cannot disagree about a declaration.config add --key packagePathis still rejected: the key is a string, not a plugin-registered array. -
1eab810: Move the
upxrunner into its own package,@repobuddy/upx.A generic package runner is broader than this package's build/derivation charter — a placement note in the spec has said so since it landed. It also made the wrong trade for
upxitself: the runner's value is install once globally, use everywhere, and that install should be small, so requiringnpm i -g universal-pluginto get a runner word worked against it.Nothing about
upx's behavior changed, and the emitter side stays here:plugin bundle --runner upx, theadopt-upxskill, andupgrade-plugin's runner-word handling all still live in this package. Their coupling was always to the wordupx, never to its code.Install
@repobuddy/upxdirectly —npm i -g @repobuddy/upx. Theupxbin on this package now re-exports it so existing global installs keep working, and prints a deprecation notice on--help(never on a normal call —upxis a transparent exec wrapper). It will be removed in the next major. -
49f1036: Add
universal-plugin plugin validate, which checks the rootplugin.jsonwithout building anything. It reports every problem in one pass, in two groups:- Schema violations against the Agent Plugins 1.0.0 schema:
$schemaandnameare required,namemust match the schema's pattern, fields must have the right types, and top-level keys the standard does not define are rejected. - Vendor violations: for example, Codex requires
descriptionandversion.--vendor <id>limits these checks to one vendor.
An unknown vendor key in
harnessesis a warning, and--strictmakes it a violation. Output is TOON by default, with--format jsonand--fullavailable. Runninguniversal-plugin pluginwith no subcommand now validates the current project and lists its declared harnesses, instead of printing help.A manifest without
$schemanow failsplugin validate.plugin builddoes not check$schemaand still accepts it.plugin initalready writes$schema. - Schema violations against the Agent Plugins 1.0.0 schema:
Patch Changes
-
a40d5b0:
doctornow readspackagePathonly from.agents/universal-plugin.json, resolved from the plugin root — the same file and baseplugin versionandpublish sync-versionuse. It no longer falls back toextensions["org.cyberuni.universal-plugin"].packagePath, which the CLI never honored, so doctor could pass a plugin thatversionstill treated as not shipping to npm. ApackagePathdeclared in the manifest extension is now reported asmisplaced-package-path. -
f0be1fa:
doctornow accepts a repeatable--marketplace-root <path>flag to also validate a separately-cloned shared marketplace repository (e.g. a local clone ofcyberuni/marketplace) — previously only the plugin's own repository root was checked, so a bad entry that reached the shared catalog another way went unnoticed. A named--marketplace-rootthat does not exist is reported asmarketplace-root-missinginstead of being silently skipped. -
0211cae: State the CLI's dependency bundling explicitly in the build config.
dist/cli.mjsalready shipped with its runtime dependencies inlined, and that is what lets the shipped skill launchers run from an installed plugin directory at all — those directories are copies of a source checkout, so theirnode_modulesis absent or incomplete. The build now declares that intent through an explicitdeps.alwaysBundleblock rather than relying on it incidentally, so a dependency added later cannot quietly become external and break the launchers.@repobuddy/upxis deliberately excluded. It is reachable only from the separatebin/upx.mjsshim, which is not a build entry, so theupxbin still resolves it at runtime from an installed tree. -
becd7ef: Fix
plugin buildending with a next-step hint foruniversal-plugin plugin validate, a command that does not exist yet. A build that refreshed a marketplace catalog now points atuniversal-plugin marketplace validate(with--rootfor that repository), and any other successful build points at/universal-plugin:doctor. -
2469986: Fix
universal-plugin --version(and-V) always printing0.0.0instead of the installed package's actual version. -
c6304c5:
migrate-pluginnow bundles the package's CLI with tsdown as part of the migration, so the CLI runs from an installed plugin directory that has nonode_modules. It splitstsdown.config.tsinto a library config that keeps dependencies external and a CLI config that inlines them throughdeps.alwaysBundle. It then proves the result by running the CLI from an extracted tarball.The skill also covers plugins that live in a sibling workspace member such as
plugins/<name>/, merges a colliding readme instead of overwriting it, and notes thatpublish sync-versionreadspackagePathfrom.agents/universal-plugin.json, not from the manifest. It also repoints lint excludes, marketplace sources, and spec paths. -
1f82ccd:
schema/extension.schema.jsonand theinit-universal-pluginstandard reference no longer advertisepackagePathunderextensions["org.cyberuni.universal-plugin"]. The CLI never read it there, so a plugin that declared it where the schema said silently fell back to the author-picks release model.packagePathlives in.agents/universal-plugin.jsonbesideplugin.json, as a path relative to the plugin root; a namespacepackagePathnow fails schema validation. -
4161472: The
doctorandpublish-pluginskills now ask the CLI forpackagePath(config get --key packagePath) instead of reading.agents/universal-plugin.jsonthemselves, so they cannot drift fromplugin version. When the CLI is too old to answer, doctor reportspackage-path-unknownand skipsversion-driftandunreleased-contentrather than guessing.
0.7.0
Minor Changes
-
af44d02:
plugin buildnow separates "nothing was declared" from "this layout is one I cannot read".A repository left on the pre-0.6 layout — a top-level
vendorExtensionsblock, or a.plugin/plugin.jsonshadowing the canonical manifest — derives nothing, and until now that was reported as a definitive empty state:built 0, exit 0. Chained behind a release script it read as success, and a released version silently never reached the vendor manifests.Deriving nothing from a canonical manifest that genuinely declares no harnesses is still an empty result (exit 0), and its stderr now names
/universal-plugin:doctoras the next step rather thanplugin validate, which has nothing to say about it. Deriving nothing because the layout predates this CLI is a failed read: the build exits 1, names the signal it found, and points atdoctor. The check only fires where the target set was already empty, so a project carrying one of those signals whose harnesses still derive is unaffected.doctor's description now covers this case —built 0, "nothing to build", theNo vendors declared in harnesseswarning, a major upgrade, a leftover.plugin/plugin.jsonorvendorExtensions— so an agent seeing the symptom routes to the skill that diagnoses it.Closes #61
-
b87c872:
plugin buildnow derives thecom.github.copilot/component tree, so a plugin's agents reach GitHub Copilot CLI.plugin initalways writes the canonical$schema, which puts every plugin this CLI builds into Copilot CLI's spec mode. Spec mode reads the runtime's native components — agents, commands, rules,hooks/hooks.jsonandlsp.json— only undercom.github.copilot/, and no longer from the plugin root. The build derived nothing forcopilot-cli, so every plugin it shipped loaded none of them on Copilot CLI, with no warning from anywhere. Skills kept working, which is what made it easy to miss.Authoring does not move. You keep declaring
agents,commands,rules,hooksandlspServersat their canonical locations, and the build derives the namespace tree from them: the directory kinds are copied — with agents renamed to Copilot CLI's.agent.mdconvention, since the canonicalagents/is the Claude Code-shaped*.mdand a file under the other name is ignored — the hooks file is translated (so a handler Copilot CLI cannot run is now dropped from a real derived file rather than reported as ignored at runtime), and a declaredlspServerspath is copied tocom.github.copilot/lsp.json. An inlinelspServersmap is not delivered — the file's top-level shape is undocumented, so the build warns rather than guessing.skills/andmcp.jsondo not move and are not copied.com.github.copilot/extensions/runs the other way: it is authored there, passed through untouched, and left alone by--clean.copilot-clinow reportsbuiltatcom.github.copilot/when it derives any of this, and still reportscanonicalatplugin.jsonwhen the plugin declares none of the moved kinds. Its manifest is unchanged — rootplugin.jsonstill serves it, so aharnesses.copilot-clioverride and apinToPluginVersionMCP entry are still undelivered and still warn.plugin init --npmwirescom.github.copilot/intopackage.jsonfiles.doctorgainscopilot-root-components: components sitting at the plugin root with no namespace copy, which is the shape of the defect above.Verified against
@github/copilot-linux-x641.0.83; the move landed in Copilot CLI 1.0.80-0. The published CLI plugin reference still describes the old layout and is stale. Recorded as ADR-0015, with the evidence and its confidence under.research/copilot-spec-mode-namespace/.Closes #67
-
c152a2e: Stop vendoring the Agent Plugins Specification manifest schema; ship a schema for only the
org.cyberuni.universal-pluginextension namespace.The repository carried
schema/v1.json, a local fork of the upstream Agent Plugins Specification v1.0.0 manifest schema. Everyplugin.jsonalready declares the published schema (https://agent-plugins.org/schemas/1.0.0/plugin.schema.json), so the fork added nothing on the envelope and could only drift from a spec this project does not own.Upstream deliberately assigns no semantics inside
extensions[<namespace>], and that is where all of this tool's configuration lives. The file is therefore split along the ownership line: the envelope copy is deleted, and the namespace body is retained asschema/extension.schema.json, now shipped in the package so a consumer resolving these fields against an installed dependency can validate against a schema instead of reimplementing the rule.Validating that namespace for the first time surfaced two defects, both fixed:
plugin buildresolved only the string form of a component path. The array and{ "paths": [...] }forms — both valid — fell through to./skills/without complaint, so a plugin declaring its skills either way had them silently left underived. All three forms now resolve, every declared directory is searched, and a declared directory that does not exist warns instead of being replaced by the default.- The per-harness override blocks were a closed field set while the build copies each one verbatim into the derived manifest. A harness field newer than the schema now rides along rather than being rejected.
-
e8bdb8a:
plugin buildstamps the plugin's version onto an MCP invocation that asks for it.A plugin whose MCP server is its own npm package writes
{"command": "npx", "args": ["-y", "my-server", "mcp"]}, and nothing pins it: a consumer on 1.4.0 gets 1.4.0's skills and whatevernpxresolves as latest for the server, so skills and server drift apart silently. The version is known at build time and nowhere else —mcp.jsonexpands only${PLUGIN_ROOT}and${PLUGIN_DATA}, so a runtime placeholder would reach the client literally.An
mcpServersentry now opts in with"pinToPluginVersion": true, and the build rewrites that entry's package specifier to<pkg>@<manifest version>. Opt-in is required and a name match is never used — a plugin may publish its server under a package name that is not the plugin's, andnpx -y widget-climust never be stamped with this plugin's version. The marker is a build directive, so it is stripped from every derived manifest and derived MCP file; the authoredplugin.jsonandmcp.jsonare left as written. Delivery follows the hooks rule: with nothing marked, the declaration passes through and nothing is derived; with an entry marked, the pinned map is delivered inline when the canonical declared it inline, else as<vendor-dir>/mcp.jsonwith the vendor'smcpServersrepointed there.
Patch Changes
-
a286459:
init: stop the vendor table from reading as though Copilot CLI needs nothing derived.Copilot CLI has no derived manifest — that part was right. But declaring the canonical
$schemaputs a plugin in Open Plugin Spec mode, and Copilot CLI then reads its native components (agents, commands, rules, hooks) from acom.github.copilot/directory rather than the plugin root. The table's "Extra requirements: none" cell was the inaccurate one, and it now names the requirement.Superseded within this release: the build derives that directory now, so the table names the requirement as met rather than as tracked.
-
1524e07:
init: adoption no longer drops Copilot CLI's non-spec root fields in silence.A pre-0.6 project's root
plugin.jsonwas Copilot CLI's derived manifest, so it carries fields likecategoryandtagsthat the closed Agent Plugins Spec field set cannot hold. The adopt route now enumerates every non-spec field on the pre-adoption root manifest and reports it by name and value before overwriting the file, and the losslessness diff includes rootplugin.json— the one vendor manifest that can lose fields, and the one the proof used to skip.references/vendors/copilot-cli.mdnow records what is actually known aboutcategoryandtags: Copilot CLI documents both as manifest fields, nothing in its documentation consumes either, and the Agent Plugins Spec bars a conformant client from assigning unknown fields meaning — so dropping them costs nothing observable. What remains unestablished is written down as open questions rather than guessed at.The field-sorting table also routes a pre-0.6 top-level
vendorExtensionsblock explicitly: it is the old name forharnesses, not an undeliverable field, and dropping it would discard every per-harness override the project had.The Copilot CLI reference is now settled against the shipped runtime rather than the vendor's field table:
categoryandtagshave noplugin.jsonhandling at all in Copilot CLI 1.0.83 — the manifest validator treats them as unknown-and-ignored — and are real only on amarketplace.jsoncatalog entry. Adoption therefore folds them intokeywords, a spec field the validator does know, instead of dropping them.
0.6.0
Minor Changes
-
4c15a40: New
universal-plugin marketplace validate— check the catalogs a repository carries against the schema each runtime loads.A catalog is read at install time, in someone else's terminal, so a broken one is silent here and loud there.
validatemoves the refusal to the repository: per target it reportsvalid,invalid, ormissing(--requiredmakes missing a failure), exits 1 when any selected catalog is invalid, and names the key at fault plus the value to write instead —owner must be an object with a name, not string. The rules are the official Claude Code marketplace schema for Claude Code, Cursor, and Copilot CLI, and Codex's own document shape for Codex; a./source is also checked for existing on disk. Nothing is repaired or written.The same rules now run wherever a catalog is produced:
marketplace initvalidates every planned artifact and fails before any write, whileplugin initandplugin build— which fold one entry into a file they did not author — report the issues as notes and warnings.The
marketplaceskill gained the validation step and now refuses to hand-author a catalog.
Patch Changes
-
4c15a40: Catalog entries now carry manifest metadata in the shape the catalog schema states.
A
plugin.jsonwritten from apackage.jsoncarriesrepositoryas{ type, url }, and every generated entry copied that object through. Claude Code refuses such a catalog —plugins[0].repository: expected string— so a repository could generate a catalog nobody could install from. An npm-shapedrepositorynow becomes its URL, with anygit+prefix removed, and a manifest field that cannot be reduced to the type the schema states is omitted rather than written. -
4c15a40:
doctornow reports a marketplace catalog its runtime would refuse.The catalogs sit at the repository root, above the plugin in a monorepo, and each is read at install time — so a broken one is silent in diagnosis and loud in a user's terminal. The new
invalid-catalogfinding names the key at fault and hands the repair to/universal-plugin:marketplace. A missing catalog is still not a fault.
0.5.0
Minor Changes
-
f3e6979:
plugin buildnow keeps the repository's local marketplace catalogs true. Each build re-derives this plugin's entry in every catalog the repository already carries, for the vendors it is building, so a catalog entry's version follows the canonical manifest instead of drifting from it (ADR-0010 §3, ADR-0014). A version move reaches the catalogs in both release models with no extra command, sinceplugin versionandchangeset version → publish sync-versionboth end inplugin build.The refresh creates nothing: a catalog the repository does not carry is not written, and inside one it carries, only this plugin's entry changes — the catalog's own fields, its formatting, its indentation, and every other plugin's entry stay as they are.
--dry-runreports the refresh as planned. The build output and its JSON gain acatalogslist.Neither generator requires a version on a Codex entry any more.
marketplace init --codexused to fail andplugin init --vendor codexused to skip the catalog, both on the belief that Codex keys its install cache by the entry's version. It does not: the version comes from the plugin's own manifest, and an entry that declares none installs normally (verified against codex-cli 0.147.0,.research/local-marketplaces, E-CODEX-M15, E-CODEX-M16). The entry still carries the canonical manifest's version when there is one, because that is this project's policy. -
d73685a:
marketplace initnow writes Cursor the catalog it actually reads, and every catalog anownerobject.Cursor's plugins reference documents
.cursor-plugin/marketplace.jsonat the repository root, close to Claude Code's shape. This project's research had recorded the opposite, so--cursorproduced a.cursor-plugin/marketplace-submission.jsonand aCURSOR_MARKETPLACE_SUBMISSION.mdhandoff. Both are gone, replaced by the catalog. Cursor also joins the default target set, and theskipped-defaultstatus it carried no longer exists.ownerwas emitted as a string, which Claude Code rejects:claude plugin validatereportsowner: Invalid input: expected object, received string. It is now an object carryingname, plusemailandurlwhen the canonical manifest'sauthorsupplies them. The Claude catalog also carries a$schemakey for editor completion and validation. -
620f2d0:
plugin buildnow translates hooks per vendor instead of copying the canonical declaration through unchanged. Cursor gets a derivedhooks.jsonbeside its manifest with camelCase event names, the schema version it expects, and matcher groups flattened into its handler list; Claude Code, Codex, and Copilot CLI read the canonical PascalCase file as authored.A handler the target vendor cannot run —
httpon Cursor, anything butcommandon Codex,agenton Copilot CLI — is dropped from that vendor's file and warned about, one warning per event and handler type, and the build stays green (ADR-0011). A vendor left with no runnable hook at all gets no derived file and nohooksfield. -
d73685a:
plugin init --vendor <id>now registers the plugin in the repository's local marketplace, writing each selected vendor's catalog at the repository root —.claude-plugin/marketplace.json,.cursor-plugin/marketplace.json, and the rest — so the plugin can be installed and tested before it is published.--no-marketplaceopts out.The marketplace is named after the repository rather than the plugin,
<owner>-<repo>-local, since the catalog sits at the repository root and lists every plugin the repository develops. The entry'ssourceis the path from there to the plugin. Owner comes from the canonical manifest's author, the package that ships it, or the account the repository lives under; without one there is no catalog, because every runtime requires it.Re-running
initfolds the entry into the catalogs already on disk: the marketplace name, the owner, and every other plugin's entry stay as they are. The entry'sversionis derived from the canonical manifest, never authored, and a version left on an entry whose manifest declares none is removed (ADR-0010 §3). A Codex entry is written whether or not there is a version to derive: Codex caches a local install under the version the plugin's own manifest carries, never the entry's. -
37ae40f: Add a
marketplaceskill: generate the repository's own marketplace catalogs, then write the README install section.marketplace inithas shipped for a while with no skill in front of it, so nothing surfaced it to an agent. The skill picks targets with the user, runs the generation, verifies it, and offers the install documentation that goes with it.The install commands were verified by running the CLIs, because the documentation is incomplete and a third-party README was the alternative source:
- Claude Code, Codex, and Copilot CLI each install from a catalog the repository carries. Each reads its own path, and all three read
.claude-plugin/marketplace.json, so one file covers them when a repository wants fewer. - Codex's marketplace verbs exist but are undocumented.
codex plugin marketplace addandcodex plugin addship in codex-cli 0.147.0 and appear in no vendor page. Codex installs withplugin addwhere Copilot CLI usesplugin install. - Codex discovers a catalog by the filename
marketplace.jsoninside a supported directory..claude-plugin/and.agents/plugins/are read;.codex-plugin/,.plugin/,.github/plugin/, and the repository root are not. - A shared catalog must carry
owner. Claude Code rejects one without it; Codex does not require it and tolerates extra fields, so the Claude shape is the portable one. - Cursor has no repository-local marketplace. The
--cursoroutput is a submission handoff, andcursor-agenthas no plugin subcommand.
references/runtimes.mdis the only source of install commands, and every entry carries an evidence ID from.research/local-marketplaces/. The skill also documents the Codex local-development loop, where an install is cached by plugin version and a source edit needs a reinstall and a new session.scripts/install-docs.mjsderives the README section from the catalogs on disk, so the marketplace name, plugin names, and repository slug come from the repository. It emits JSON and writes nothing; the skill asks before editing the README. - Claude Code, Codex, and Copilot CLI each install from a catalog the repository carries. Each reads its own path, and all three read
-
aad26dd: Declare plugin dependencies once, and let the build deliver them per vendor. A plugin says what it needs under
extensions["org.cyberuni.universal-plugin"].dependencies— an array of plugin names, each optionally@marketplace-qualified or given as an object with a semver range or a commit sha. Claude Code is the only runtime that reads a dependency, so its manifest carries the declaration and the others are built without it, each drop named in a warning. The build stays green. A range written into the string form is accepted by the runtime and then discarded, so the build warns once and names the object form that is enforced (ADR-0013). -
f6bf784: Add
plugin installandplugin uninstall— put the plugin under development into a runtime, instead of hand-writing a symlink.Getting a working copy into a runtime meant a symlink per vendor, copy-pasted into every plugin repository's readme. Re-verifying that recipe against the shipped runtimes found both halves wrong:
~/.claude/plugins/local/does not exist in Claude Code — the path that works is its skills directory, which adopts a plugin and loads it as<name>@skills-dir— and Cursor's~/.cursor/plugins/local/resolves each symlink and refuses a target outside itself. An author following those two lines got silence from both runtimes and no way to tell why.plugin installinstalls into every vendor the canonical manifest already declares,--vendor <id>narrows it, and--listshows the resolved destinations without writing. Each vendor's local plugin directory now lives in the vendor registry alongside every other vendor path this tool knows, so a vendor moving its directory is one line here rather than a stale command in every downstream readme — and a machine with a runtime configured elsewhere can override it in~/.agents/universal-plugin-vendors.json.The mode resolves per vendor, because a single default cannot serve both runtimes: it links where the vendor follows an out-of-tree symlink and copies where it does not, and the result row names the mode each vendor got.
--copyforces a snapshot everywhere;--linkforces a link and fails a vendor that will not load one, rather than quietly copying when a live link was asked for. Codex and Copilot CLI scan no local directory at all and report asunsupported, with their marketplace route named.Re-running replaces this plugin's own earlier install rather than stacking; a destination another plugin owns is refused until
--force.plugin uninstallapplies the same ownership test, and reports a destination that was never installed rather than failing. Both refuse to run against a vendor whose derived manifest was never built, pointing atplugin build.Recorded as ADR-0012, with the verified per-runtime facts and their confidence in
.research/local-marketplaces/. -
50a9c82: Record the version policy as ADR-0010, and give
doctorthe check it obliges.The canonical
plugin.jsonowns a plugin's version; every other version-carrying artifact — the per-vendor manifests, the repository-local marketplace catalogs, thenpx/upxpins inskills/**— derives from it and is never authored by hand. Who picks the next value splits onpackagePath: without one the author picks, throughplugin version <bump>; with one the release picks, andpublish sync-versioncarries the number frompackage.jsoninto the manifest.A marketplace entry's version is copied from the canonical manifest of the plugin its
sourceresolves to. Where a runtime lets both the entry and the manifest carry one, the manifest wins — Claude Code documents that it overrides the entry silently — so a generated entry is never the number that decides anything, and never a number a human edits.doctorgainsunreleased-content. A runtime keys its plugin cache on the version, so content committed after the commit that set the current one never reaches a consumer who already installed the plugin, and neither side is told: the author sees a successful push, the consumer sees "already at the latest version". The check compares the shipped paths against that commit. It stays quiet on uncommitted work, on a plugin that declarespackagePath— there the release picks the number — and on a tree with no git history. Alongside it,doctornow readspackagePathfrom.agents/universal-plugin.json, where the CLI reads it, soversion-driftfires for the repositories that actually declare one.
Patch Changes
-
d998e13: Resolve
--rootto an absolute directory, and stop re-joining a workspace-relative root onto a cwd already inside itIn a pnpm monorepo a package is named by its workspace-relative path, so
--root packages/podsrun from insidepackages/podsresolved to<repo>/packages/pods/packages/pods— a directory that does not exist — andplugin buildreportedNo plugin.json foundagainst that doubled path (#43). Every command taking--rootnow resolves it against the cwd and, when the re-joined path is missing while the cwd already ends with the given path, uses the cwd — the package that was named.--rootalso resolves to an absolute path in every case now. A relative root previously flowed through unresolved, soNo plugin.json found at ../emptynamed a fragment rather than the directory searched, andplugin init --root .derived the plugin name frompath.basename('.')instead of the directory's own name.
0.4.0
Minor Changes
-
0d9136b: Replace the
plugingateway skill with four verb-shaped skills:init,doctor,version, andremove-plugin.The gateway routed six operations behind one name, and no single name covers that set.
initreads as first-time setup, which is wrong for deleting a manifest;pluginis a noun that says what the skill is about and never what it does. One description that has to trigger on "convert this to the open standard", "bump the version", and "delete the generated manifests" is a weaker match for each than three focused ones.Each skill is now scoped by the object it touches, which is also what keeps them from competing for the same asks:
init— the manifest's declaration: create, adopt, update. Runs five phases (survey, classify, confirm, apply, verify), where confirm is the gate the old gateway lacked: adoption turns files the user maintains into build output, and that needed approval it never asked for.doctor— read-only diagnosis. Reports what is declared, unbuilt, stale, hand-edited, drifting, or shadowing, and hands every repair to the skill that owns it.version— the released number, in both release models: changesets-decided and carried in bypublish sync-version, or moved directly byplugin version.remove-plugin— the artifacts: derived manifests, a stale.github/plugin/plugin.json, a shadowing.plugin/plugin.json, or the whole plugin behind a confirmation.
Also in this change:
scripts/init.mjsandscripts/version.mjsrun their CLI verb from the copy shipped beside the skill, so neither a scaffold nor a release number needs a network fetch.doctorshipsscripts/doctor.mjs: it composesplugin build --dry-run --format jsonwith the filesystem facts build cannot see — missing and stale derived manifests, a shadowing.plugin/plugin.json, version drift between the two authored numbers — and emits one JSON object. It stays a thin composition so it folds intoplugin validatewhen that command lands, rather than competing with it.- Each skill carries a README.
- One reference per vendor, read only when that vendor is enabled, replacing the vendor columns the create reference carried inline.
- A frontmatter reference documenting
invocation-policy, including the part that surprises people: the build rewrites the authoredSKILL.mdto carry the derived flags. - The create reference no longer claims
plugin buildis unavailable — it has shipped, and the reference now names its flags and the warnings worth reading. - The vendor references state plainly that the build does not translate hook event names across the PascalCase/camelCase divide, which it does not (tracked in #41).
ADR-0009 records the split and supersedes the single-gateway reach rule in
spec.mdandplugin/version/. -
4e0e5ee: Emit TOON as the default output format
Every command's
--formathelp namedtoonas its default, and the AXI output contract (ADR-0003) requires it, but the implementation printed aligned ASCII tables and padded field lists. Commands now encode their result with@toon-format/toon, soplugin build,plugin init,plugin version,plugin bundle,config add,config get,governance list,marketplace init, andpublish sync-versionemit parseable TOON on stdout.--format jsonis unchanged.governance showstill prints the document body, which is text rather than a record. Each default payload keeps its minimal row schema and its pre-computed aggregate summary, so the counts a script matched before are still there.
Patch Changes
-
a7fa800: Document how a skill runs a CLI its own plugin ships.
The npx-and-upx page already named importing in-process as the only complete fix for the runner's cost, and put it at the top of the "choosing a runner" table. It did not say how a skill reaches that code when the skill file and the package both sit in a plugin cache.
It now records the launcher pattern: a script in the skill's own
scripts/directory that resolves the package fromimport.meta.urland imports the bin, invoked asnode scripts/<name>.mjs. Four requirements come with it, each with its own failure mode: resolve from the script rather than the working directory, keepnodein front of a file that ships without an executable bit, publish to npm when the CLI has dependencies, and keep a pinnednpxfallback that is regenerated at release. -
dda4bca: Ship the MIT license file in the package
package.jsonandplugin.jsonboth declared"license": "MIT", but no license file existed, so the published tarball carried the declaration without the terms and the readme's license link pointed at a file that was never there.
0.3.1
Patch Changes
-
b0da34d: Stop deriving
.cursor/commands/*.mdmirrors of skillsThe build wrote a copy of every user-invocable skill body to
<root>/.cursor/commands/<name>.md. Cursor does not need it: a plugin'sskillspath is loaded directly, and a user invokes a skill by typing/and searching for it. Cursor also expresses explicit-only invocation natively withdisable-model-invocation, which the build already writes into SKILL.md — so the mirror was a redundant second copy of the same content, dropped into the plugin's own working tree. -
13879cc: Derive universal-plugin's own vendor manifests instead of hand-maintaining them
The package shipped
.claude-plugin/,.cursor-plugin/, and.codex-plugin/manifests that were written by hand and never regenerated, because the canonicalplugin.jsondeclared noharnesses. They had drifted toversion0.2.0and still carried the retiredvendorsand aassetspath pointing at a directory that does not exist — so every runtime reading a vendor manifest saw a stale version. Declaring the four harnesses letsuniversal-plugin plugin buildproduce them, and the regenerated files now track the canonical version. -
7c026fb: Regenerate the vendor manifests during
changeset versionThe release chain synced the canonical
plugin.jsonand stopped there —publish sync-versionwrites exactly one row, so.claude-plugin/,.cursor-plugin/, and.codex-plugin/kept whatever version they were last committed with. The repo's own manifests had drifted a full minor behind as a result. The rootversionscript now runsplugin:buildafter the sync, so every release derives the vendor manifests from the freshly synced version.
0.3.0
Minor Changes
-
e9a31ce: Add
universal-plugin marketplace initfor deterministic repository-local marketplace metadata. -
8fb8297:
plugin buildno longer derives a manifest forcopilot-cli. Copilot CLI searches.plugin/plugin.json→plugin.json→.github/plugin/plugin.json→.claude-plugin/plugin.jsonand takes the first match, so the canonical rootplugin.jsonalways shadowed the.github/plugin/plugin.jsonwe were emitting — that file was never read. Copilot CLI has consumed Open Plugin Spec v1 manifests since v1.0.74, so the canonical manifest serves it directly. The vendor is now reported with statuscanonical, and aharnesses["copilot-cli"]override warns that it has no delivery path (the canonical schema is closed to vendor-only fields).Delete any stale
.github/plugin/plugin.jsonfrom a previous build; it was inert. -
8dfffbd: Derive vendor slash-command artifacts from skill invocation policies.
-
b50b9c8: The
pluginskill now detects existing plugins on invocation. When a project has a vendor-specific manifest (.claude-plugin/,.cursor-plugin/, …) or already ships public skills but has no canonical rootplugin.json, the gateway offers to adopt the open Agent Plugins Specification. The lossless conversion procedure lives in the newreferences/adopt.md. Repo-private agent config (.claude/skills/,.agents/skills/) is excluded from detection. -
ab6ff25: New
universal-plugin plugin version <major|minor|patch|premajor|preminor|prepatch|prerelease|x.y.z>moves a plugin's version. A version lives in up to five places, but only two of them are authored: the canonical rootplugin.jsonand, when.agents/universal-plugin.jsondeclares apackagePath, thatpackage.json. The rest — the per-vendor manifests, the local marketplace catalogs, and thenpx/upxpins inskills/**— are derived by commands that already exist. So the verb writes the authored pair and then callsplugin build's own writer to re-derive, rather than becoming a second writer for filesbuildowns.--preid <id>picks the prerelease identifier,--forceallows a version that does not advance,--no-buildskips re-derivation, and--dry-runreports the plan without writing. Every guard — missing manifest, a relative bump with no current version, an unknown bump argument, a non-advancing target, a declaredpackagePathwhosepackage.jsonis absent — fails loud and leaves the tree untouched.publish sync-versionkeeps the opposite direction (a changesets-decided number flowingpackage.json→ manifest) with its behavior unchanged, but now shares the new applier so the two directions cannot drift apart.The shipped
plugingateway skill gains a matching route —references/version.md— so an agent asked to bump a plugin's version finds the verb instead of hand-editing aversionfield. Its route table previously covered create / adopt / inspect / update / delete, none of which is moving the version. The skill description now names the version triggers explicitly, and the reference routes changesets repos topublish sync-versionrather than to the new verb. -
36d05fc: Shorten the bundled skill names by dropping the redundant
universal-pluginhalf — the plugin namespace already supplies it.universal-plugin→plugin,publish-universal-plugin→publish-plugin,upgrade-universal-plugin→upgrade-plugin,migrate-universal-plugin→migrate-plugin. Invocation becomes/universal-plugin:plugininstead of/universal-plugin:universal-plugin.adopt-upxis unchanged.Breaking for name-pinned installs. Installing the whole plugin is unaffected — the marketplace entry resolves the package directory and discovers skills from
skills/, so nothing there refers to a skill by name. But a per-skill install pinned the old name, and that path is gone:# before npx skills add cyberuni/universal-plugin --skill upgrade-universal-plugin # after npx skills add cyberuni/universal-plugin --skill upgrade-pluginUpdate the
skillskey andsourcepath in yourskills-lock.json, or re-runskills addwith the new name. No aliases are shipped for the old names. -
e2a57e1: Add
upx, a local-first package runner, as a second lean bin.upx <pkg>@<range> [args…]resolves the requested semver range against installed packages — walkingnode_modulesfrom the cwd up through its ancestors (nearest wins), then the globalnpm root -gstore — and spawns the matching binary directly, roughly 10× faster thannpx(which pays ~1s of registry resolution per call even when cached). On a miss it falls back tonpxwith the spec exactly as given, printing a one-line stderr notice. It is a transparent exec wrapper: the child owns stdout/stderr and its exit code;upxinstalls nothing and writes nothing tonode_modulesor the global store. A dist-tag (pkg@next) goes straight tonpx.Also adds
plugin bundle --runner <npx|upx>: omitting it preserves each skill reference's existing runner word while re-pinning the version;--runner upxopts a release into emittingupxreferences.
Patch Changes
- d28501c: Synchronize local Codex marketplace entry versions with canonical plugin manifests.
- df326ae:
plugin init --npmnow always wires the open-standard base — the canonical rootplugin.jsonandskills/— intopackage.jsonfiles, whatever--vendortargets are named. Previously the base was tied to vendor selection, so a default--npmrun wired.claude-plugin/plugin.jsonandskills/but never the canonical manifest, and the published package shipped a Claude Code plugin rather than a standard one. Vendor-derived manifests are added on top of the base, never in place of it. - 454267e: Restructure the
pluginskill as a gateway. The Create, Inspect, Update, and Delete procedures moved out ofSKILL.mdintoreferences/create.md,references/inspect.md,references/update.md, andreferences/delete.md.SKILL.mdnow carries only the trigger, prerequisites, and a routing table, so an agent loads one operation's procedure instead of all four.
0.2.1
Patch Changes
- 7c92d8e: Mark the CLI bin shim as executable so it runs directly after install.
0.2.0
Minor Changes
-
c6bc08a: Add
publish sync-versioncommand to sync theversionfield in.plugin/plugin.jsonfrom the npm package declared by a newpackagePathfield. Rununiversal-plugin publish sync-versionafterchangeset versionto keep the plugin manifest version in sync with the npm package. Also fixes thebuildcommand to strippackagePathfrom generated vendor manifests. -
1100fa3: Add initial
universal-pluginCLI with cross-vendor plugin management commands.New commands:
prepare— diffs the current vendor's installed plugins against the last snapshot and writes pending cross-vendor sync actions to state.sync apply <actionId>— executes a pending install, update, or remove action using the target vendor's registered CLI command (or emits a manual instruction when none is configured).governance show <name>/governance list— resolves and displays governance files by scope (global → project).asset-store— manages the local store of downloaded plugin assets.self-update— rewritesuniversal-pluginversion pins in hook files when a newer version is detected.clean— removes the local asset store directory.
Supporting modules added: state file schema with tolerant reader and mutation helpers, vendor registry with bundled defaults and user-override support, source registry with store-path derivation for npm / GitHub / GitLab / URL sources.