universal-plugin@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, since
plugin 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 anowner
object.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 the
skipped-defaultstatus it carried no longer exists.ownerwas emitted as a string, which Claude Code rejects:claude plugin validatereports
owner: Invalid input: expected object, received string. It is now an object carryingname, plus
emailandurlwhen 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,agent
on 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's
sourceis 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 underextensions["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/pods
run 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.