feat(core): externalize plugin body into .plugin/plugin.json manifest (v0.13.0) - #155
Merged
Merged
Conversation
… (v0.13.0) A plugin entry can now set `path` to externalize its body into a `<path>/.plugin/plugin.json` manifest, keeping plugins.json a lightweight registry of description + path (+ default_in_roots). resolveArtifacts reads the manifest and merges its fields (artifact references + distribution metadata) into the entry before scope-qualification, with inline-wins precedence. This is AIR's vendor-neutral analog of the Open Plugins manifest: same file location, but component fields hold AIR artifact IDs rather than bundled component paths. Fully backward-compatible and opt-in. Adds schemas/plugin-manifest.schema.json, a `path` field on the plugins schema, and `path` on the core PluginEntry type. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This was referenced Jun 12, 2026
….json Warn when a plugins.json entry declares its body inline (no path) instead of referencing a .plugin/plugin.json manifest. The warning names the plugin and offending fields and links the removal-tracking issue (#157). Inline overrides layered on top of a `path` stay quiet — that's the sanctioned override path. Marks the affected fields `deprecated` in the plugins schema, migrates the bundled examples to manifest form, and documents the deprecation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Plugins were the only AIR artifact with no body — every field (artifact references and distribution metadata like
author,version,license,keywords) had to live inline on theplugins.jsonentry. A catalog with many plugins (or a plugin that joins dozens of roots viadefault_in_roots) bloated the index.This PR lets a plugin entry set a
paththat externalizes its body into a<path>/.plugin/plugin.jsonmanifest — AIR's vendor-neutral analog of the Open Plugins.plugin/plugin.jsonmanifest.plugins.jsoncan then stay a lightweight registry ofdescription+path(+default_in_roots).How it works
pathresolves exactly like thepathon skills/references/hooks: a relative path against the index file's directory, or a provider URI (github://owner/repo[@ref]/dir). The manifest is always read from the local filesystem (the provider's local clone), so remote plugins incur no extra fetch.resolveArtifactsreads the manifest and merges its fields into the entry before scope-qualification, so the manifest'sskills/mcp_servers/hooks/pluginsarrays canonicalize under the catalog's scope and expand identically to inline declarations.plugins.jsonentry overrides the manifest (the manifest only fills gaps). This composes a single plugin's split definition — it is not cross-catalog later-wins.description,path, anddefault_in_rootsalways live on the index entry (the registry needsdescriptionto list the plugin; root membership is a catalog-layer decision, not a property of the distributed plugin).nameis accepted for Open Plugins compatibility but ignored for identity.Backward compatibility
Fully opt-in. A plugin may declare everything inline (no
path), externalize everything, or mix the two. Errors are loud: a missing manifest, unparseable JSON, or a non-string-array reference field fails resolution with a diagnostic that names the plugin.Changes
schemas/plugin-manifest.schema.json— new AIR Plugin Manifest schema.schemas/plugins.schema.json— adds thepathfield;requiredunchanged (["description"]).packages/core/src/types.ts— addspath?toPluginEntry.packages/core/src/config.ts—hydratePluginManifests(), invoked inloadContributionsfor the plugins type after path resolution.packages/core/tests/plugin-manifest.test.ts— new, 9 tests (hydration, inline-wins, scope qualification, relative-path resolution, default_in_roots placement, plugin-of-plugin expansion, missing/invalid/malformed manifest errors, inline backward-compat).examples/plugins/—code-qualityconverted to the thin-index + manifest form; the other two examples stay inline to show both styles coexisting.docs/plugins.md— documents the manifest model and updates the Open Plugins / Claude Plugins deviation tables (AIR now adopts the same manifest location).Testing
npx vitest run— 1067 passed, 2 skipped, across all 57 test files.tsc --noEmitclean for core, sdk, cli.examples/air.json:code-qualityresolves withtitle/version/licensesourced from the manifest body and thelint-pre-commithook qualified to@local/lint-pre-commit.🤖 Generated with Claude Code