universal-plugin 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/dist/cli.mjs +1534 -247
- package/dist/data/vendors.json +12 -0
- package/dist/run.mjs +4 -4
- package/governances/plugin-design.md +42 -1
- package/package.json +2 -1
- package/plugin.json +1 -1
- package/readme.md +11 -0
- package/skills/doctor/README.md +4 -2
- package/skills/doctor/SKILL.md +33 -1
- package/skills/doctor/scripts/doctor.mjs +109 -4
- package/skills/init/SKILL.md +23 -4
- package/skills/init/references/create.md +13 -2
- package/skills/init/references/standard.md +2 -1
- package/skills/init/references/vendors/claude-code.md +40 -8
- package/skills/init/references/vendors/codex.md +10 -2
- package/skills/init/references/vendors/copilot-cli.md +10 -2
- package/skills/init/references/vendors/cursor.md +9 -2
- package/skills/marketplace/README.md +50 -0
- package/skills/marketplace/SKILL.md +209 -0
- package/skills/marketplace/references/runtimes.md +104 -0
- package/skills/marketplace/scripts/install-docs.mjs +115 -0
- package/skills/marketplace/scripts/marketplace.mjs +11 -0
- package/skills/marketplace/scripts/validate.mjs +11 -0
- package/skills/publish-plugin/SKILL.md +10 -8
- package/skills/publish-plugin/references/vendor-requirements.md +13 -10
- package/skills/version/SKILL.md +2 -1
package/dist/data/vendors.json
CHANGED
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
"hookGlob": "~/.claude/plugins/universal-plugin/hooks/hooks.json",
|
|
7
7
|
"globalPluginDir": "~/.claude/plugins/",
|
|
8
8
|
"pluginRootSuffix": ".claude-plugin/plugin.json",
|
|
9
|
+
"localPluginDir": "~/.claude/skills/",
|
|
10
|
+
"localPluginLink": true,
|
|
11
|
+
"localReload": "restart Claude Code — it loads as <name>@skills-dir",
|
|
9
12
|
"installCommand": "claude plugin install {name}",
|
|
10
13
|
"removeCommand": "claude plugin remove {name}",
|
|
11
14
|
"updateCommand": "claude plugin update {name}@{version}"
|
|
@@ -17,6 +20,9 @@
|
|
|
17
20
|
"hookGlob": null,
|
|
18
21
|
"globalPluginDir": null,
|
|
19
22
|
"pluginRootSuffix": ".cursor-plugin/plugin.json",
|
|
23
|
+
"localPluginDir": "~/.cursor/plugins/local/",
|
|
24
|
+
"localPluginLink": false,
|
|
25
|
+
"localReload": "run Developer: Reload Window in Cursor",
|
|
20
26
|
"installCommand": null,
|
|
21
27
|
"removeCommand": null,
|
|
22
28
|
"updateCommand": null
|
|
@@ -28,6 +34,9 @@
|
|
|
28
34
|
"hookGlob": null,
|
|
29
35
|
"globalPluginDir": null,
|
|
30
36
|
"pluginRootSuffix": ".codex-plugin/plugin.json",
|
|
37
|
+
"localPluginDir": null,
|
|
38
|
+
"localPluginLink": false,
|
|
39
|
+
"localReload": null,
|
|
31
40
|
"installCommand": null,
|
|
32
41
|
"removeCommand": null,
|
|
33
42
|
"updateCommand": null
|
|
@@ -39,6 +48,9 @@
|
|
|
39
48
|
"hookGlob": null,
|
|
40
49
|
"globalPluginDir": null,
|
|
41
50
|
"pluginRootSuffix": "plugin.json",
|
|
51
|
+
"localPluginDir": null,
|
|
52
|
+
"localPluginLink": false,
|
|
53
|
+
"localReload": null,
|
|
42
54
|
"installCommand": null,
|
|
43
55
|
"removeCommand": null,
|
|
44
56
|
"updateCommand": null
|
package/dist/run.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import * as fsNode from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
+
import * as semver$1 from "semver";
|
|
4
5
|
import { spawnSync } from "node:child_process";
|
|
5
|
-
import * as semver from "semver";
|
|
6
6
|
//#region src/run/fs.ts
|
|
7
7
|
function readInstall(dir) {
|
|
8
8
|
try {
|
|
@@ -121,13 +121,13 @@ function parseSpec(spec) {
|
|
|
121
121
|
/** A valid semver range drives local-first matching; a non-empty non-semver spec (`next`,
|
|
122
122
|
* `latest`) is a dist-tag that can't be matched against an installed `package.json` version. */
|
|
123
123
|
function isSemverRange(range) {
|
|
124
|
-
return semver.validRange(range) !== null;
|
|
124
|
+
return semver$1.validRange(range) !== null;
|
|
125
125
|
}
|
|
126
126
|
/** Nearest-local → global, first install whose version satisfies `range`. `locals` must already be
|
|
127
127
|
* nearest-first. */
|
|
128
128
|
function selectInstall(range, locals, globalInstall) {
|
|
129
|
-
for (const install of locals) if (semver.satisfies(install.version, range)) return install;
|
|
130
|
-
if (globalInstall && semver.satisfies(globalInstall.version, range)) return globalInstall;
|
|
129
|
+
for (const install of locals) if (semver$1.satisfies(install.version, range)) return install;
|
|
130
|
+
if (globalInstall && semver$1.satisfies(globalInstall.version, range)) return globalInstall;
|
|
131
131
|
}
|
|
132
132
|
/** Resolves the executable from a `package.json` `bin` field: a string bin, an object entry keyed
|
|
133
133
|
* by the package's unscoped name (even among several bins), or a single-entry object. A
|
|
@@ -49,6 +49,9 @@ All component paths and build config live under `extensions["org.cyberuni.univer
|
|
|
49
49
|
| `lspServers` | `.lsp.json` path | Extended | Claude Code only |
|
|
50
50
|
| `outputStyles` | Output style resources directory | Extended | Claude Code only |
|
|
51
51
|
|
|
52
|
+
One field under this namespace is not a component path: `dependencies`. See
|
|
53
|
+
[Plugin Dependencies](#plugin-dependencies).
|
|
54
|
+
|
|
52
55
|
A conformant host must support at least one core component (`skills` or `mcpServers`). Extended types are silently ignored on non-supporting hosts — do not rely on them for core plugin functionality.
|
|
53
56
|
|
|
54
57
|
### `extensions["org.cyberuni.universal-plugin"]` — `vendors` and `harnesses`
|
|
@@ -70,7 +73,7 @@ Vendor-specific extension fields:
|
|
|
70
73
|
| `defaultEnabled` | ✓ (bool) | — | — | — |
|
|
71
74
|
| `userConfig` | ✓ (prompted at enable) | — | — | — |
|
|
72
75
|
| `channels` | ✓ | — | — | — |
|
|
73
|
-
| `dependencies` | ✓ (inter-plugin) | — | — | — |
|
|
76
|
+
| `dependencies` | ✓ (inter-plugin — declared canonically, see [Plugin Dependencies](#plugin-dependencies)) | — | — | — |
|
|
74
77
|
| `themes` | ✓ | — | — | — |
|
|
75
78
|
| `monitors` | ✓ | — | — | — |
|
|
76
79
|
| `logo` | — | ✓ | — | — |
|
|
@@ -145,6 +148,44 @@ Build reads root `plugin.json`, applies the rules below, writes each vendor's ou
|
|
|
145
148
|
| `hooks` | adapt → PascalCase, `${CLAUDE_PLUGIN_ROOT}` | adapt → camelCase, pass-through env | ✓ → PascalCase, `${PLUGIN_ROOT}` native | adapt → camelCase, pass-through env |
|
|
146
149
|
| `lspServers` | ✓ | **omit** | **omit** | **omit** |
|
|
147
150
|
| `outputStyles` | ✓ | **omit** | **omit** | **omit** |
|
|
151
|
+
| `dependencies` | ✓ | **drop + warn** | **drop + warn** | **drop + warn** |
|
|
152
|
+
|
|
153
|
+
## Plugin Dependencies
|
|
154
|
+
|
|
155
|
+
A plugin declares the plugins it needs under
|
|
156
|
+
`extensions["org.cyberuni.universal-plugin"].dependencies`, once, whatever it targets. Claude Code is
|
|
157
|
+
the only runtime that reads a dependency; build drops the declaration for the rest and warns
|
|
158
|
+
(ADR-0013). The build stays green.
|
|
159
|
+
|
|
160
|
+
```json
|
|
161
|
+
{
|
|
162
|
+
"extensions": {
|
|
163
|
+
"org.cyberuni.universal-plugin": {
|
|
164
|
+
"dependencies": [
|
|
165
|
+
"cyber-asana",
|
|
166
|
+
{ "name": "cyber-notion", "marketplace": "cyberuni", "version": "^0.9.0" }
|
|
167
|
+
]
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
An entry is a plugin name, optionally `@marketplace`-qualified, or an object carrying that name plus a
|
|
174
|
+
constraint:
|
|
175
|
+
|
|
176
|
+
| Key | Type | Notes |
|
|
177
|
+
| --- | --- | --- |
|
|
178
|
+
| `name` | string | Required. |
|
|
179
|
+
| `marketplace` | string | Which marketplace to resolve `name` in. A bare name resolves against the declaring plugin's own marketplace. |
|
|
180
|
+
| `version` | string | Semver range, checked against the installed plugin's version. |
|
|
181
|
+
| `sha` | string | Commit sha to pin a git-sourced dependency to. |
|
|
182
|
+
|
|
183
|
+
Write a range in the object form. `"cyber-asana@^0.9.0"` validates and the runtime then discards the
|
|
184
|
+
range, so build warns and names the object to write instead. `"cyber-asana@>=1.0.0"` is not a legal
|
|
185
|
+
name at all and fails the build, as does an npm-style object map.
|
|
186
|
+
|
|
187
|
+
Build validates shape only. Whether a declared plugin exists is a question for a resolver, and
|
|
188
|
+
resolving, fetching, and installing dependencies is out of scope (ADR-0013).
|
|
148
189
|
|
|
149
190
|
## Hook Event Name Mapping
|
|
150
191
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "universal-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Universal AI agent plugin build tool",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent-plugin",
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@types/node": "^24.10.1",
|
|
46
46
|
"@types/semver": "^7.7.1",
|
|
47
|
+
"ajv": "^8.20.0",
|
|
47
48
|
"knip": "^6.14.1",
|
|
48
49
|
"tsdown": "^0.22.0",
|
|
49
50
|
"tsx": "^4.22.3",
|
package/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
3
|
"name": "universal-plugin",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.6.0",
|
|
5
5
|
"description": "Research and design toolkit for building universal AI coding agent plugins that work across Claude Code, Cursor, Codex, and GitHub Copilot CLI.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "unional"
|
package/readme.md
CHANGED
|
@@ -37,6 +37,8 @@ Author the canonical manifest and derive everything from it.
|
|
|
37
37
|
npx universal-plugin plugin init # scaffold plugin.json
|
|
38
38
|
npx universal-plugin plugin init --npm # also wire an npm package to ship it
|
|
39
39
|
npx universal-plugin plugin build # generate vendor manifests
|
|
40
|
+
npx universal-plugin plugin install # install the working copy into the runtimes it targets
|
|
41
|
+
npx universal-plugin plugin uninstall # take it back out
|
|
40
42
|
npx universal-plugin plugin version <bump> # move the version across every file carrying one
|
|
41
43
|
npx universal-plugin plugin bundle # pin skill npx references to workspace versions
|
|
42
44
|
```
|
|
@@ -45,6 +47,11 @@ npx universal-plugin plugin bundle # pin skill npx references to w
|
|
|
45
47
|
`.codex-plugin/plugin.json`. Copilot CLI reads the canonical root `plugin.json` directly, so no
|
|
46
48
|
fourth file is derived.
|
|
47
49
|
|
|
50
|
+
`install` puts the plugin you are editing into each runtime's local plugin directory, so you can use
|
|
51
|
+
it before publishing anything. It links where the runtime follows a symlink out of the tree, copies
|
|
52
|
+
where it does not, refuses a destination another plugin owns, and prints the reload each runtime now
|
|
53
|
+
needs. Installing a *published* plugin by name stays the runtime's own job.
|
|
54
|
+
|
|
48
55
|
Each command writes JSON with `JSON.stringify`. Your repository decides how JSON looks, so run your
|
|
49
56
|
formatter after any command that writes a manifest.
|
|
50
57
|
|
|
@@ -72,8 +79,12 @@ keep their previous version.
|
|
|
72
79
|
|
|
73
80
|
```sh
|
|
74
81
|
npx universal-plugin marketplace init --codex --root .
|
|
82
|
+
npx universal-plugin marketplace validate --root .
|
|
75
83
|
```
|
|
76
84
|
|
|
85
|
+
`validate` checks each catalog against the schema its runtime loads and names the key at fault, so a
|
|
86
|
+
catalog that would be refused at install time is caught in the repository.
|
|
87
|
+
|
|
77
88
|
Codex caches a local plugin install by its marketplace entry version. After you change packaged
|
|
78
89
|
plugin files: update the canonical `plugin.json` version, regenerate the catalog (add `--force` to
|
|
79
90
|
replace an existing one), reinstall the plugin, then start a new Codex session. The installed copy
|
package/skills/doctor/README.md
CHANGED
|
@@ -7,8 +7,10 @@ disk still matches it for Claude Code, Cursor, Codex, and GitHub Copilot CLI.
|
|
|
7
7
|
|
|
8
8
|
`scripts/doctor.mjs` composes the shipped CLI's `plugin build --dry-run --format json` with the
|
|
9
9
|
filesystem facts that build cannot see — whether each derived manifest exists, whether it predates
|
|
10
|
-
the canonical manifest, whether a stale or shadowing manifest is lying around,
|
|
11
|
-
authored version numbers still agree
|
|
10
|
+
the canonical manifest, whether a stale or shadowing manifest is lying around, whether the two
|
|
11
|
+
authored version numbers still agree, whether shipped content has moved since the version did, and
|
|
12
|
+
whether every marketplace catalog at the repository root is a shape its runtime loads. It emits one
|
|
13
|
+
JSON object: `vendors`, `findings`, `ok`.
|
|
12
14
|
|
|
13
15
|
The skill supplies the judgment around it: which finding matters, and which skill owns its repair.
|
|
14
16
|
|
package/skills/doctor/SKILL.md
CHANGED
|
@@ -70,11 +70,23 @@ Each `code` below is what the script emits.
|
|
|
70
70
|
| `undeliverable-override` | `harnesses["copilot-cli"]` sets fields that reach nothing | `/universal-plugin:init`, update route — move them to a vendor that has a derived manifest, or drop them |
|
|
71
71
|
| `codex-fields-missing` | Codex is targeted without `version` or `description`; the build fails and writes **nothing at all**, including for the other vendors | add both to the canonical top level |
|
|
72
72
|
| `version-drift` | the `packagePath` `package.json` and the canonical manifest carry different versions | `/universal-plugin:version` |
|
|
73
|
+
| `unreleased-content` | shipped content was committed after the commit that set the current version — a consumer keyed on that version never re-extracts it | `/universal-plugin:version` |
|
|
73
74
|
| `stale-github-plugin` | a leftover `.github/plugin/plugin.json` from an older build — shadowed by root and no longer generated | `/universal-plugin:remove-plugin` |
|
|
74
75
|
| `shadowing-manifest` | a `.plugin/plugin.json` exists — it outranks root in Copilot CLI's search order and silently shadows the canonical manifest | `/universal-plugin:remove-plugin` |
|
|
75
76
|
| `no-vendors` | no vendor is declared, so the build writes nothing and no runtime reads the plugin | `/universal-plugin:init`, update route |
|
|
76
77
|
| `package-path-missing` | `packagePath` names a directory with no readable `package.json` | fix `packagePath`, or create the package |
|
|
77
78
|
| `unparsable-manifest` | root `plugin.json` is not valid JSON | fix the syntax error |
|
|
79
|
+
| `invalid-catalog` | a marketplace catalog at the repository root is not a shape its runtime loads — it is found, read, and refused at install time, in the user's terminal | `/universal-plugin:marketplace` |
|
|
80
|
+
|
|
81
|
+
## Catalogs are checked at the repository root
|
|
82
|
+
|
|
83
|
+
The marketplace catalogs sit above the plugin in a monorepo, so the catalog check runs against the
|
|
84
|
+
repository root rather than `--root`. It reports only a catalog that would be **refused**: a missing
|
|
85
|
+
one is not a fault, and nothing here has an opinion on which catalogs a repository ought to carry.
|
|
86
|
+
|
|
87
|
+
The detail names the key at fault, so hand it to `/universal-plugin:marketplace` as it stands. An
|
|
88
|
+
entry's fields are derived from the plugin's `plugin.json`, and the catalog's own `name` and `owner`
|
|
89
|
+
are authored in the catalog — which half is at fault decides where the repair goes.
|
|
78
90
|
|
|
79
91
|
## Checking staleness properly
|
|
80
92
|
|
|
@@ -104,6 +116,25 @@ They diverge when someone ran `npm version`, or when changesets released a numbe
|
|
|
104
116
|
back. Both are `/universal-plugin:version`'s to fix — never patch one file by hand to match the
|
|
105
117
|
other.
|
|
106
118
|
|
|
119
|
+
## Unreleased content
|
|
120
|
+
|
|
121
|
+
A runtime keys its plugin cache on the version, not on content: Claude Code resolves the version,
|
|
122
|
+
finds it unchanged, and reports *"already at the latest version"* without re-extracting. So content
|
|
123
|
+
pushed without a bump reaches nobody who already installed the plugin, and neither side is told
|
|
124
|
+
([ADR-0010](../../.agents/spec/design/decisions/0010-version-policy.md) §6).
|
|
125
|
+
|
|
126
|
+
The script compares the shipped paths — the canonical manifest, the skills directory, `agents/`,
|
|
127
|
+
`governances/`, `mcp.json` — against the commit that set the version the manifest carries now, and
|
|
128
|
+
emits `unreleased-content` for anything committed since. Uncommitted work is not reported; it has not
|
|
129
|
+
shipped.
|
|
130
|
+
|
|
131
|
+
Two cases are deliberately silent. A plugin that declares `packagePath` is skipped, because there the
|
|
132
|
+
release picks the number (ADR-0010 §2) and content waiting ahead of the last released version is the
|
|
133
|
+
normal state of a branch. A tree with no git history is skipped rather than guessed at.
|
|
134
|
+
|
|
135
|
+
The repair is the bump, and it belongs to `/universal-plugin:version`. Judge first whether the change
|
|
136
|
+
is meant to ship — content that is still being worked on is not a finding to act on.
|
|
137
|
+
|
|
107
138
|
## Rules
|
|
108
139
|
|
|
109
140
|
- **Never repair.** Report the finding and name the skill that owns it.
|
|
@@ -120,4 +151,5 @@ other.
|
|
|
120
151
|
| Create, adopt, or change what the plugin declares | `init` |
|
|
121
152
|
| Move the plugin's version | `version` |
|
|
122
153
|
| Remove derived manifests, or the plugin itself | `remove-plugin` |
|
|
123
|
-
|
|
|
154
|
+
| Generate the repository's own marketplace catalogs | `marketplace` |
|
|
155
|
+
| Publish it to the shared marketplace repository | `publish-plugin` |
|
|
@@ -51,6 +51,12 @@ if (manifest === null) {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
const ext = manifest.extensions?.[UP_NAMESPACE] ?? null
|
|
54
|
+
|
|
55
|
+
// `packagePath` is the CLI's own config, and the CLI reads it from `.agents/universal-plugin.json`
|
|
56
|
+
// (src/version/fs.ts). It is read here from the same file, so a plugin the CLI treats as npm-shipping
|
|
57
|
+
// is one this script treats the same way. The manifest extension is accepted as a fallback for a
|
|
58
|
+
// repository that put it there.
|
|
59
|
+
const packagePath = readPackagePath()
|
|
54
60
|
if (!manifest.$schema?.includes('agent-plugins.org') || ext === null) {
|
|
55
61
|
add(
|
|
56
62
|
'legacy-manifest',
|
|
@@ -148,28 +154,120 @@ if (build === null) {
|
|
|
148
154
|
}
|
|
149
155
|
|
|
150
156
|
// Version drift between the two authored numbers.
|
|
151
|
-
if (
|
|
152
|
-
const pkgPath = path.join(root,
|
|
157
|
+
if (packagePath !== null) {
|
|
158
|
+
const pkgPath = path.join(root, packagePath, 'package.json')
|
|
153
159
|
const pkg = readJson(pkgPath)
|
|
154
160
|
if (pkg === null) {
|
|
155
161
|
add(
|
|
156
162
|
'package-path-missing',
|
|
157
163
|
'medium',
|
|
158
|
-
`packagePath names ${
|
|
164
|
+
`packagePath names ${packagePath}, which holds no readable package.json`,
|
|
159
165
|
'fix packagePath, or create the package',
|
|
160
166
|
)
|
|
161
167
|
} else if (manifest.version !== undefined && pkg.version !== manifest.version) {
|
|
162
168
|
add(
|
|
163
169
|
'version-drift',
|
|
164
170
|
'high',
|
|
165
|
-
`plugin.json is ${manifest.version}, ${
|
|
171
|
+
`plugin.json is ${manifest.version}, ${packagePath}/package.json is ${pkg.version}`,
|
|
166
172
|
'/universal-plugin:version',
|
|
167
173
|
)
|
|
168
174
|
}
|
|
169
175
|
}
|
|
170
176
|
|
|
177
|
+
// Content shipped since the version last moved (ADR-0010 §6). A runtime keys its plugin cache on the
|
|
178
|
+
// version, so anything committed after the commit that set the current one is invisible to a consumer
|
|
179
|
+
// who already installed the plugin. Read-only: the comparison is git's, and it is skipped wherever
|
|
180
|
+
// git cannot answer.
|
|
181
|
+
//
|
|
182
|
+
// Not run where the release picks the number. ADR-0010 §2 makes `packagePath` the switch: a plugin
|
|
183
|
+
// that ships to npm gets its version from the release, so content sitting ahead of the last released
|
|
184
|
+
// one is the normal state there, not a defect. Only the author-picks model can forget the bump.
|
|
185
|
+
if (manifest.version !== undefined && packagePath === null) {
|
|
186
|
+
const introduced = commitThatSetVersion(manifest.version)
|
|
187
|
+
if (introduced !== null) {
|
|
188
|
+
const changed = git('diff', '--name-only', `${introduced}..HEAD`, '--', ...shippedPaths())
|
|
189
|
+
const files = (changed ?? '').split('\n').filter(Boolean)
|
|
190
|
+
if (files.length > 0) {
|
|
191
|
+
const sample = files.slice(0, 3).join(', ')
|
|
192
|
+
add(
|
|
193
|
+
'unreleased-content',
|
|
194
|
+
'medium',
|
|
195
|
+
`${files.length} shipped file(s) changed since ${manifest.version} was set (${sample}${files.length > 3 ? ', …' : ''}) — a consumer keyed on that version never re-extracts them`,
|
|
196
|
+
'/universal-plugin:version',
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// The marketplace catalogs a user installs from. They sit at the *repository* root, above a plugin in
|
|
203
|
+
// a monorepo, and each is read by its runtime at install time — a catalog whose shape that runtime
|
|
204
|
+
// refuses fails in the user's terminal, not here. The shipped CLI owns the rules; this only asks.
|
|
205
|
+
for (const row of invalidCatalogs()) {
|
|
206
|
+
add(
|
|
207
|
+
'invalid-catalog',
|
|
208
|
+
'high',
|
|
209
|
+
`${row.path} is not a shape its runtime loads: ${row.issues.map((issue) => `${issue.path} ${issue.message}`).join('; ')}`,
|
|
210
|
+
'/universal-plugin:marketplace',
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
|
|
171
214
|
report({ vendors })
|
|
172
215
|
|
|
216
|
+
/** Every catalog the repository carries that its runtime would refuse. Empty when there is nothing to
|
|
217
|
+
* read, when the CLI is too old to answer, or when every catalog is fine — a missing catalog is not a
|
|
218
|
+
* fault, and this reports no opinion on which ones a repository ought to carry. */
|
|
219
|
+
function invalidCatalogs() {
|
|
220
|
+
const catalogRoot = git('rev-parse', '--show-toplevel') ?? root
|
|
221
|
+
const result = fs.existsSync(bin)
|
|
222
|
+
? spawnSync(process.execPath, [bin, 'marketplace', 'validate', '--format', 'json', '--root', catalogRoot], {
|
|
223
|
+
encoding: 'utf8',
|
|
224
|
+
})
|
|
225
|
+
: spawnSync('npx', ['universal-plugin', 'marketplace', 'validate', '--format', 'json', '--root', catalogRoot], {
|
|
226
|
+
encoding: 'utf8',
|
|
227
|
+
})
|
|
228
|
+
const rows = readJson_stdout(result.stdout)
|
|
229
|
+
return Array.isArray(rows) ? rows.filter((row) => row.status === 'invalid') : []
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Runs git inside `root`, returning its stdout or `null` — a non-zero status, a missing git, and a
|
|
233
|
+
* directory outside any repository are all the same answer here: no history to read. */
|
|
234
|
+
function git(...args) {
|
|
235
|
+
const result = spawnSync('git', ['-C', root, ...args], { encoding: 'utf8' })
|
|
236
|
+
return result.status === 0 ? result.stdout.trim() : null
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** The commit that introduced the version the manifest carries now, walking `plugin.json`'s history
|
|
240
|
+
* newest-first until the version changes. `null` when there is no history, or when the newest
|
|
241
|
+
* committed manifest already disagrees — that version is uncommitted, so nothing shipped under it. */
|
|
242
|
+
function commitThatSetVersion(current) {
|
|
243
|
+
const log = git('log', '--format=%H', '-100', '--', 'plugin.json')
|
|
244
|
+
if (log === null || log === '') return null
|
|
245
|
+
|
|
246
|
+
let introduced = null
|
|
247
|
+
for (const sha of log.split('\n').filter(Boolean)) {
|
|
248
|
+
const blob = git('show', `${sha}:./plugin.json`)
|
|
249
|
+
if (blob === null) break
|
|
250
|
+
let version
|
|
251
|
+
try {
|
|
252
|
+
version = JSON.parse(blob).version
|
|
253
|
+
} catch {
|
|
254
|
+
break
|
|
255
|
+
}
|
|
256
|
+
if (version !== current) break
|
|
257
|
+
introduced = sha
|
|
258
|
+
}
|
|
259
|
+
return introduced
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** What a consumer installs, as pathspecs. The derived vendor manifests are deliberately absent —
|
|
263
|
+
* they only ever change because the canonical manifest did, and counting both would report one
|
|
264
|
+
* change twice. */
|
|
265
|
+
function shippedPaths() {
|
|
266
|
+
const skills = typeof ext?.skills === 'string' ? ext.skills : './skills/'
|
|
267
|
+
const paths = ['plugin.json', skills, 'agents', 'governances', 'mcp.json']
|
|
268
|
+
return paths.filter((rel) => fs.existsSync(path.join(root, rel)))
|
|
269
|
+
}
|
|
270
|
+
|
|
173
271
|
function readJson_stdout(stdout) {
|
|
174
272
|
if (!stdout) return null
|
|
175
273
|
try {
|
|
@@ -194,3 +292,10 @@ function report({ vendors }) {
|
|
|
194
292
|
}
|
|
195
293
|
process.exit(0)
|
|
196
294
|
}
|
|
295
|
+
|
|
296
|
+
/** Where the npm package that ships this plugin lives, or `null` when the plugin ships to no
|
|
297
|
+
* package. `null` is the author-picks release model of ADR-0010 §2. */
|
|
298
|
+
function readPackagePath() {
|
|
299
|
+
const declared = readJson(path.join(root, '.agents', 'universal-plugin.json'))?.packagePath ?? ext?.packagePath
|
|
300
|
+
return typeof declared === 'string' && declared.length > 0 ? declared : null
|
|
301
|
+
}
|
package/skills/init/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: init
|
|
3
3
|
description: Use this skill to create or change a universal agent plugin — scaffold a new one, adopt an existing vendor-specific plugin or already-shipped skills onto the open Agent Plugins Specification, or add and remove vendors and components on the canonical plugin.json that drives Claude Code, Cursor, Codex, and GitHub Copilot CLI. Trigger on "init a plugin here", "make my Claude Code plugin work in Cursor", "convert this to the open plugin standard", "turn these skills into a plugin", "add Codex support", or "add a hooks component".
|
|
4
|
-
argument-hint: '[--name <name>] [--vendor <id>] [--scaffold] [--npm] [--force]'
|
|
4
|
+
argument-hint: '[--name <name>] [--vendor <id>] [--scaffold] [--npm] [--no-marketplace] [--force]'
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Plugin Init
|
|
@@ -133,6 +133,23 @@ Then derive the vendor manifests:
|
|
|
133
133
|
npx universal-plugin plugin build
|
|
134
134
|
```
|
|
135
135
|
|
|
136
|
+
To try the result before publishing, `npx universal-plugin plugin install` puts the working copy
|
|
137
|
+
into every runtime the manifest declares and names the reload each one needs. Never hand-write a
|
|
138
|
+
symlink for this — `references/create.md` records why the recipe that circulated for it does not
|
|
139
|
+
work.
|
|
140
|
+
|
|
141
|
+
With `--vendor`, `plugin init` also registers the plugin in the repository's local marketplace: it
|
|
142
|
+
writes each selected vendor's catalog at the **repository** root and folds an entry for this plugin
|
|
143
|
+
into it, so users can add the repository as a marketplace and install from it. Say what it wrote,
|
|
144
|
+
and read back any line it printed on stderr — a repository with no author, no package author, and no
|
|
145
|
+
remote gets no catalog, because every runtime requires an owner. `--no-marketplace` skips the step.
|
|
146
|
+
|
|
147
|
+
The catalog is named after the repository, `<owner>-<repo>-local`, not after the plugin: it lists
|
|
148
|
+
every plugin the repository develops. Re-running `init` folds the entry back in and leaves the
|
|
149
|
+
marketplace name, the owner, and every other entry alone, so it is safe over a catalog someone
|
|
150
|
+
edited. Generating catalogs for a repository that already holds several plugins, and writing the
|
|
151
|
+
README install section, is the `marketplace` skill's job.
|
|
152
|
+
|
|
136
153
|
`plugin init` writes a **minimal** manifest — `$schema`, `name`, and the vendor list. It never reads
|
|
137
154
|
an existing vendor manifest, so shared metadata and per-vendor overrides are carried in by hand
|
|
138
155
|
afterwards, per the reference you routed to.
|
|
@@ -172,9 +189,10 @@ This skill is not a formatter. If the project has one, run it over the written f
|
|
|
172
189
|
the Phase 5 diff is the check that proves it.
|
|
173
190
|
- **Do not package repo-private agent configuration.** A `.claude/skills/` directory is the project's
|
|
174
191
|
own tooling; offering to publish it is wrong.
|
|
175
|
-
- **Do not convert vendor settings without a documented mapping.**
|
|
176
|
-
|
|
177
|
-
`references/vendors/claude-code.md`. Unmapped settings stay where they are,
|
|
192
|
+
- **Do not convert vendor settings without a documented mapping.** Hooks have one: author them in
|
|
193
|
+
canonical PascalCase and `plugin build` derives each vendor's form, dropping handler types a vendor
|
|
194
|
+
cannot run — see `references/vendors/claude-code.md`. Unmapped settings stay where they are,
|
|
195
|
+
reported.
|
|
178
196
|
- **Offer adoption once.** If the user declines, or asked for something unrelated, drop it and do
|
|
179
197
|
what they asked.
|
|
180
198
|
- Plugin authoring only. Do not change CI workflows, repository settings, or unrelated project files.
|
|
@@ -184,6 +202,7 @@ This skill is not a formatter. If the project has one, run it over the written f
|
|
|
184
202
|
| Task | Skill |
|
|
185
203
|
|------|-------|
|
|
186
204
|
| Diagnose a plugin — what is declared, built, stale, or drifting | `doctor` |
|
|
205
|
+
| Make the repository installable, and document how | `marketplace` |
|
|
187
206
|
| Move the plugin's version, or reconcile one that drifted | `version` |
|
|
188
207
|
| Delete derived manifests, or the whole plugin | `remove-plugin` |
|
|
189
208
|
| Move a repo-root plugin into its npm package | `migrate-plugin` |
|
|
@@ -101,10 +101,21 @@ write all surface there rather than as errors.
|
|
|
101
101
|
## Step 8 — Install locally to test
|
|
102
102
|
|
|
103
103
|
```bash
|
|
104
|
-
|
|
105
|
-
ln -sf "$(pwd)" ~/.cursor/plugins/local/<plugin-name> # Cursor → Developer: Reload Window
|
|
104
|
+
npx universal-plugin plugin install
|
|
106
105
|
```
|
|
107
106
|
|
|
107
|
+
It installs into every runtime the manifest declares, linking where the runtime follows a symlink
|
|
108
|
+
out of the tree and copying where it does not, and it prints the reload each one now needs — a
|
|
109
|
+
restart for Claude Code, **Developer: Reload Window** for Cursor. `--list` shows where it would go
|
|
110
|
+
without writing; `--vendor <id>` narrows it; `plugin uninstall` removes it again.
|
|
111
|
+
|
|
112
|
+
Codex and Copilot CLI scan no local plugin directory, so they report as `unsupported`. Reach those
|
|
113
|
+
through a repository-local marketplace — `publish-plugin`.
|
|
114
|
+
|
|
115
|
+
Do not hand-write a symlink for this. The recipe that circulated for it named
|
|
116
|
+
`~/.claude/plugins/local/`, which does not exist, and a symlink into Cursor's local directory is
|
|
117
|
+
rejected by Cursor's own scan.
|
|
118
|
+
|
|
108
119
|
## Next
|
|
109
120
|
|
|
110
121
|
Shipping it on npm → `migrate-plugin`. Listing it in a marketplace → `publish-plugin`. Releasing a
|
|
@@ -38,6 +38,7 @@ lives under one namespaced key, `extensions["org.cyberuni.universal-plugin"]`:
|
|
|
38
38
|
| `harnesses` | per-vendor overrides, keyed by vendor id. `{}` opts in with no overrides |
|
|
39
39
|
| `packagePath` | the npm package whose `package.json` carries the same version |
|
|
40
40
|
| component paths (`skills`, `commands`, `agents`, `hooks`, …) | where each component lives |
|
|
41
|
+
| `dependencies` | the plugins this plugin needs. Only Claude Code reads them; see [`vendors/claude-code.md`](./vendors/claude-code.md) |
|
|
41
42
|
|
|
42
43
|
`vendors` and `harnesses` are separate on purpose: `vendors` says what to build, `harnesses` says
|
|
43
44
|
what each build gets. A vendor listed in `vendors` with no `harnesses` entry still builds; a
|
|
@@ -68,7 +69,7 @@ directories. Create the rest only when the plugin has content for them.
|
|
|
68
69
|
| MCP servers | `mcpServers` | `.mcp.json` | every vendor |
|
|
69
70
|
| Commands | `commands` | `commands/<name>.md` | Claude Code, Cursor, Copilot CLI |
|
|
70
71
|
| Agents | `agents` | `agents/<name>.md` | Claude Code, Cursor, Copilot CLI |
|
|
71
|
-
| Hooks | `hooks` | `hooks/hooks.json` |
|
|
72
|
+
| Hooks | `hooks` | `hooks/hooks.json` | every vendor — authored PascalCase, translated per vendor by the build; handler types vary |
|
|
72
73
|
| LSP servers | `lspServers` | `.lsp.json` | Claude Code, Cursor |
|
|
73
74
|
| Rules | `rules` | `rules/<name>.mdc` | Cursor only |
|
|
74
75
|
| Output styles | `outputStyles` | `output-styles/` | Claude Code only |
|
|
@@ -27,16 +27,48 @@ true` for `user`, `user-invocable: false` for `model`. See [`../frontmatter.md`]
|
|
|
27
27
|
|
|
28
28
|
## Hooks
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
`UserPromptSubmit`)
|
|
30
|
+
Author hooks once, in canonical form: **PascalCase** event names (`SessionStart`, `PreToolUse`,
|
|
31
|
+
`PostToolUse`, `Stop`, `UserPromptSubmit`) over Claude Code's matcher-group shape. That is what the
|
|
32
|
+
canonical schema admits, and `plugin build` derives the rest (ADR-0011).
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
Claude Code and Codex read that form as authored. Copilot CLI accepts it too — PascalCase selects its
|
|
35
|
+
Claude-compatible payload format. Cursor is the one vendor translated: it gets
|
|
36
|
+
`.cursor-plugin/hooks.json` with camelCase events, `"version": 1`, and each matcher group flattened
|
|
37
|
+
into one entry per handler, and its derived manifest points there.
|
|
37
38
|
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
**A handler type the vendor cannot run is dropped, and the build warns.** Claude Code runs `command`,
|
|
40
|
+
`http`, `prompt`, and `agent`; Codex runs `command` only; Cursor runs `command` and `prompt`; Copilot
|
|
41
|
+
CLI runs `command`, `http`, and `prompt`. Read the warnings — a plugin whose only `SessionStart`
|
|
42
|
+
handler is `http` reaches Claude Code and Copilot CLI and nothing else. Copilot CLI reads the
|
|
43
|
+
canonical file directly, so its unsupported handlers are reported as ignored at runtime rather than
|
|
44
|
+
dropped from a derived file.
|
|
45
|
+
|
|
46
|
+
Source: `.research/hook-event-survey/conclusion.md` (re-verified August 2026) — re-verify against
|
|
47
|
+
vendor docs before relying on it.
|
|
48
|
+
|
|
49
|
+
## Dependencies
|
|
50
|
+
|
|
51
|
+
Claude Code is the only runtime that reads a plugin dependency, and it acts on one: it installs a
|
|
52
|
+
missing dependency, enables it alongside the plugin that needs it, prunes it once nothing needs it,
|
|
53
|
+
and refuses to load a plugin whose declared range the installed version does not satisfy. Declare it
|
|
54
|
+
once, canonically, under `extensions["org.cyberuni.universal-plugin"].dependencies` — not under
|
|
55
|
+
`harnesses["claude-code"]` (ADR-0013):
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
"dependencies": ["cyber-asana", { "name": "cyber-notion", "marketplace": "cyberuni", "version": "^0.9.0" }]
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
A bare name resolves against the declaring plugin's own marketplace; `marketplace` picks another one,
|
|
62
|
+
which the root marketplace must have allowed. Put a range in the object form — a range written as
|
|
63
|
+
`"cyber-asana@^0.9.0"` is accepted and then discarded by the runtime, and the build warns and names
|
|
64
|
+
the object to write instead.
|
|
65
|
+
|
|
66
|
+
Cursor, Codex, and Copilot CLI read no such field. The build leaves it out of their manifests and
|
|
67
|
+
warns; the build stays green. A plugin that loads there without its dependency is worth a line in
|
|
68
|
+
your README.
|
|
69
|
+
|
|
70
|
+
Source: `.research/plugin-schema/` (re-verified August 2026 against Claude Code 2.1.235) — re-verify
|
|
71
|
+
against vendor docs before relying on it.
|
|
40
72
|
|
|
41
73
|
## Leave alone
|
|
42
74
|
|
|
@@ -44,5 +44,13 @@ so it is not part of the plugin's tracked output and does not travel with a clon
|
|
|
44
44
|
|
|
45
45
|
## Hooks
|
|
46
46
|
|
|
47
|
-
Codex hook events are **PascalCase**, like Claude Code's
|
|
48
|
-
|
|
47
|
+
Codex hook events are **PascalCase**, like Claude Code's, so the canonical file reaches Codex as
|
|
48
|
+
authored. Codex runs `command` handlers only — an `http`, `prompt`, or `agent` handler is dropped
|
|
49
|
+
from `.codex-plugin/hooks.json` with a warning. See [`claude-code.md`](./claude-code.md).
|
|
50
|
+
|
|
51
|
+
## Dependencies
|
|
52
|
+
|
|
53
|
+
Codex reads no plugin dependency. A declaration is left out of `.codex-plugin/plugin.json` with a
|
|
54
|
+
build warning — deliberately, because the validator Codex ships for its plugin ingestion contract
|
|
55
|
+
rejects any field outside its allowlist, and one unaccepted key fails the whole manifest. See
|
|
56
|
+
[`claude-code.md`](./claude-code.md).
|
|
@@ -41,5 +41,13 @@ derived manifest, or it does not ship. Do not invent a path for it.
|
|
|
41
41
|
|
|
42
42
|
## Hooks
|
|
43
43
|
|
|
44
|
-
Copilot CLI
|
|
45
|
-
|
|
44
|
+
Copilot CLI accepts **either casing**, and the casing selects the payload format: PascalCase gets the
|
|
45
|
+
Claude-compatible format, so the canonical file reaches Copilot CLI unchanged. Because Copilot CLI
|
|
46
|
+
reads that file directly, the build derives nothing for it — an `agent` handler is reported as ignored
|
|
47
|
+
at runtime rather than dropped. See [`claude-code.md`](./claude-code.md).
|
|
48
|
+
|
|
49
|
+
## Dependencies
|
|
50
|
+
|
|
51
|
+
Copilot CLI reads no plugin dependency. Because it reads the canonical manifest directly, there is no
|
|
52
|
+
derived file to leave the declaration out of — it sits under `extensions`, which Copilot CLI ignores,
|
|
53
|
+
and the build reports it as ignored at runtime. See [`claude-code.md`](./claude-code.md).
|
|
@@ -41,5 +41,12 @@ never generate rules from a skill or a skill from a rule.
|
|
|
41
41
|
|
|
42
42
|
## Hooks
|
|
43
43
|
|
|
44
|
-
Cursor hook events are **camelCase** (`sessionStart`)
|
|
45
|
-
|
|
44
|
+
Cursor hook events are **camelCase** (`sessionStart`), and Cursor's hooks file differs in shape as
|
|
45
|
+
well as casing. The build derives `.cursor-plugin/hooks.json` from the canonical file — never author
|
|
46
|
+
it by hand. Cursor runs `command` and `prompt` handlers; an `http` or `agent` handler is dropped with
|
|
47
|
+
a warning. See [`claude-code.md`](./claude-code.md).
|
|
48
|
+
|
|
49
|
+
## Dependencies
|
|
50
|
+
|
|
51
|
+
Cursor reads no plugin dependency. A declaration is left out of `.cursor-plugin/plugin.json` with a
|
|
52
|
+
build warning. See [`claude-code.md`](./claude-code.md).
|