universal-plugin 0.2.1 → 0.3.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 +15 -0
- package/.codex-plugin/plugin.json +14 -0
- package/.cursor-plugin/plugin.json +14 -0
- package/agents/agentskills-specialist.md +132 -0
- package/bin/upx.mjs +6 -0
- package/dist/cli.mjs +1330 -255
- package/dist/run.mjs +271 -0
- package/governances/plugin-design.md +22 -17
- package/governances/slash-invocation.md +30 -0
- package/package.json +14 -5
- package/plugin.json +18 -0
- package/readme.md +37 -3
- package/skills/adopt-upx/README.md +38 -0
- package/skills/adopt-upx/SKILL.md +120 -0
- package/skills/adopt-upx/scripts/rewrite-upx.mjs +168 -0
- package/skills/migrate-plugin/SKILL.md +106 -0
- package/skills/migrate-plugin/evals/evals.json +11 -0
- package/skills/migrate-plugin/evals/trigger-queries.json +35 -0
- package/skills/plugin/README.md +37 -0
- package/skills/plugin/SKILL.md +105 -0
- package/skills/plugin/assets/templates/agent.md +7 -0
- package/skills/plugin/assets/templates/command.md +9 -0
- package/skills/plugin/assets/templates/hooks.json +9 -0
- package/skills/plugin/assets/templates/plugin.json +19 -0
- package/skills/plugin/assets/templates/setup-command.md +15 -0
- package/skills/plugin/assets/templates/skill.md +15 -0
- package/skills/plugin/references/adopt.md +114 -0
- package/skills/plugin/references/create.md +163 -0
- package/skills/plugin/references/delete.md +23 -0
- package/skills/plugin/references/inspect.md +21 -0
- package/skills/plugin/references/update.md +26 -0
- package/skills/plugin/references/version.md +97 -0
- package/skills/publish-plugin/SKILL.md +246 -0
- package/skills/publish-plugin/evals/evals.json +23 -0
- package/skills/publish-plugin/references/vendor-requirements.md +38 -0
- package/skills/upgrade-plugin/README.md +23 -0
- package/skills/upgrade-plugin/SKILL.md +86 -0
- package/LICENSE +0 -21
package/dist/run.mjs
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as fsNode from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import * as semver from "semver";
|
|
6
|
+
//#region src/run/fs.ts
|
|
7
|
+
function readInstall(dir) {
|
|
8
|
+
try {
|
|
9
|
+
const raw = fsNode.readFileSync(path.join(dir, "package.json"), "utf8");
|
|
10
|
+
const pkg = JSON.parse(raw);
|
|
11
|
+
if (typeof pkg.version !== "string") return void 0;
|
|
12
|
+
return {
|
|
13
|
+
dir,
|
|
14
|
+
version: pkg.version,
|
|
15
|
+
bin: pkg.bin
|
|
16
|
+
};
|
|
17
|
+
} catch {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function packageDirIn(nodeModulesDir, pkg) {
|
|
22
|
+
return path.join(nodeModulesDir, ...pkg.split("/"));
|
|
23
|
+
}
|
|
24
|
+
/** Every ancestor `node_modules` directory from `startDir` up to the filesystem root, nearest
|
|
25
|
+
* first. */
|
|
26
|
+
function ancestorNodeModulesDirs(startDir) {
|
|
27
|
+
const dirs = [];
|
|
28
|
+
let dir = startDir;
|
|
29
|
+
for (;;) {
|
|
30
|
+
dirs.push(path.join(dir, "node_modules"));
|
|
31
|
+
const parent = path.dirname(dir);
|
|
32
|
+
if (parent === dir) break;
|
|
33
|
+
dir = parent;
|
|
34
|
+
}
|
|
35
|
+
return dirs;
|
|
36
|
+
}
|
|
37
|
+
/** Walks `node_modules` from `cwd` up through its ancestors, nearest first, collecting every
|
|
38
|
+
* install of `pkg` found along the way (an install missing a readable `package.json` version is
|
|
39
|
+
* skipped, not a match). */
|
|
40
|
+
function findLocalInstalls(pkg, cwd) {
|
|
41
|
+
const installs = [];
|
|
42
|
+
for (const nodeModulesDir of ancestorNodeModulesDirs(cwd)) {
|
|
43
|
+
const install = readInstall(packageDirIn(nodeModulesDir, pkg));
|
|
44
|
+
if (install) installs.push(install);
|
|
45
|
+
}
|
|
46
|
+
return installs;
|
|
47
|
+
}
|
|
48
|
+
function globalRoot() {
|
|
49
|
+
try {
|
|
50
|
+
const out = spawnSync("npm", ["root", "-g"], { encoding: "utf8" }).stdout?.trim();
|
|
51
|
+
return out ? out : void 0;
|
|
52
|
+
} catch {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function findGlobalInstall(pkg) {
|
|
57
|
+
const root = globalRoot();
|
|
58
|
+
if (!root) return void 0;
|
|
59
|
+
return readInstall(packageDirIn(root, pkg));
|
|
60
|
+
}
|
|
61
|
+
function spawnBin(binPath, args) {
|
|
62
|
+
const result = spawnSync(binPath, args, { stdio: "inherit" });
|
|
63
|
+
if (result.error) throw result.error;
|
|
64
|
+
return result.status ?? 1;
|
|
65
|
+
}
|
|
66
|
+
function spawnNpx(args) {
|
|
67
|
+
const result = spawnSync("npx", args, { stdio: "inherit" });
|
|
68
|
+
if (result.error) throw result.error;
|
|
69
|
+
return result.status ?? 1;
|
|
70
|
+
}
|
|
71
|
+
function realRunFs() {
|
|
72
|
+
return {
|
|
73
|
+
findLocalInstalls: (pkg) => findLocalInstalls(pkg, process.cwd()),
|
|
74
|
+
findGlobalInstall,
|
|
75
|
+
spawnBin,
|
|
76
|
+
spawnNpx
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
//#endregion
|
|
80
|
+
//#region src/run/run.ts
|
|
81
|
+
const NPM_NAME_PATTERN = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
|
|
82
|
+
/** A loose approximation of npm's package-name rules: lowercase, alphanumeric/`.`/`_`/`-`, an
|
|
83
|
+
* optional `@scope/` prefix, non-empty. Good enough to reject the obviously malformed without
|
|
84
|
+
* chasing every edge npm itself enforces. */
|
|
85
|
+
function isValidPackageName(name) {
|
|
86
|
+
if (!name || name.length > 214) return false;
|
|
87
|
+
return NPM_NAME_PATTERN.test(name);
|
|
88
|
+
}
|
|
89
|
+
/** Splits `<pkg>@<range>` on the **last** `@` at index > 0, so a scoped name's leading `@` survives.
|
|
90
|
+
* No `@` after index 0 — or a trailing `@` with an empty range — is a bare package (range `*`). A
|
|
91
|
+
* package name that is empty or violates npm's naming rules is a fail-loud error. */
|
|
92
|
+
function parseSpec(spec) {
|
|
93
|
+
if (!spec) return {
|
|
94
|
+
ok: false,
|
|
95
|
+
error: "error: no package spec given"
|
|
96
|
+
};
|
|
97
|
+
const lastAt = spec.lastIndexOf("@");
|
|
98
|
+
let pkg;
|
|
99
|
+
let rangeRaw;
|
|
100
|
+
if (lastAt <= 0) {
|
|
101
|
+
pkg = spec;
|
|
102
|
+
rangeRaw = "";
|
|
103
|
+
} else {
|
|
104
|
+
pkg = spec.slice(0, lastAt);
|
|
105
|
+
rangeRaw = spec.slice(lastAt + 1);
|
|
106
|
+
}
|
|
107
|
+
if (!isValidPackageName(pkg)) return {
|
|
108
|
+
ok: false,
|
|
109
|
+
error: `error: unparseable package spec "${spec}"`
|
|
110
|
+
};
|
|
111
|
+
const bare = rangeRaw === "";
|
|
112
|
+
return {
|
|
113
|
+
ok: true,
|
|
114
|
+
spec: {
|
|
115
|
+
pkg,
|
|
116
|
+
range: bare ? "*" : rangeRaw,
|
|
117
|
+
bare
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/** A valid semver range drives local-first matching; a non-empty non-semver spec (`next`,
|
|
122
|
+
* `latest`) is a dist-tag that can't be matched against an installed `package.json` version. */
|
|
123
|
+
function isSemverRange(range) {
|
|
124
|
+
return semver.validRange(range) !== null;
|
|
125
|
+
}
|
|
126
|
+
/** Nearest-local → global, first install whose version satisfies `range`. `locals` must already be
|
|
127
|
+
* nearest-first. */
|
|
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;
|
|
131
|
+
}
|
|
132
|
+
/** Resolves the executable from a `package.json` `bin` field: a string bin, an object entry keyed
|
|
133
|
+
* by the package's unscoped name (even among several bins), or a single-entry object. A
|
|
134
|
+
* multi-entry object with no name match, or a missing `bin` field entirely, fails loud — `upx`
|
|
135
|
+
* never guesses which bin to run. */
|
|
136
|
+
function resolveBinPath(pkgName, bin) {
|
|
137
|
+
if (bin === void 0) return {
|
|
138
|
+
ok: false,
|
|
139
|
+
error: `error: package "${pkgName}" declares no bin field`
|
|
140
|
+
};
|
|
141
|
+
if (typeof bin === "string") return {
|
|
142
|
+
ok: true,
|
|
143
|
+
bin
|
|
144
|
+
};
|
|
145
|
+
const entries = Object.entries(bin);
|
|
146
|
+
if (entries.length === 0) return {
|
|
147
|
+
ok: false,
|
|
148
|
+
error: `error: package "${pkgName}" declares no bin field`
|
|
149
|
+
};
|
|
150
|
+
if (entries.length === 1) return {
|
|
151
|
+
ok: true,
|
|
152
|
+
bin: entries[0][1]
|
|
153
|
+
};
|
|
154
|
+
const match = bin[pkgName.includes("/") ? pkgName.slice(pkgName.lastIndexOf("/") + 1) : pkgName] ?? bin[pkgName];
|
|
155
|
+
if (match) return {
|
|
156
|
+
ok: true,
|
|
157
|
+
bin: match
|
|
158
|
+
};
|
|
159
|
+
return {
|
|
160
|
+
ok: false,
|
|
161
|
+
error: `error: package "${pkgName}" declares multiple bins and none matches its name — upx never guesses which to run`
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
const KNOWN_LEADING_FLAGS = new Set(["--help", "-h"]);
|
|
165
|
+
/** A flag is a token starting with `-`; `upx`'s own flags are recognized only before the first
|
|
166
|
+
* non-flag token (the package spec) — everything from the spec onward belongs to the child. An
|
|
167
|
+
* unknown flag before the spec fails loud. */
|
|
168
|
+
function parseArgv(argv) {
|
|
169
|
+
if (argv.length === 0) return {
|
|
170
|
+
ok: false,
|
|
171
|
+
error: "error: no package spec given"
|
|
172
|
+
};
|
|
173
|
+
const first = argv[0];
|
|
174
|
+
if (first.startsWith("-")) {
|
|
175
|
+
if (KNOWN_LEADING_FLAGS.has(first)) return {
|
|
176
|
+
ok: true,
|
|
177
|
+
args: {
|
|
178
|
+
help: true,
|
|
179
|
+
spec: void 0,
|
|
180
|
+
childArgs: []
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
return {
|
|
184
|
+
ok: false,
|
|
185
|
+
error: `error: unknown flag "${first}"`
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
ok: true,
|
|
190
|
+
args: {
|
|
191
|
+
help: false,
|
|
192
|
+
spec: first,
|
|
193
|
+
childArgs: argv.slice(1)
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
function fallbackNotice(pkg, range) {
|
|
198
|
+
return `upx: no installed ${pkg} satisfies "${range}", using npx`;
|
|
199
|
+
}
|
|
200
|
+
/** A dist-tag (`next`, `latest`) is not a version range, so the miss notice must not claim the
|
|
201
|
+
* installed versions failed to "satisfy" it — it never could. Keeps the fixed
|
|
202
|
+
* `upx: no installed <pkg>` prefix shared with {@link fallbackNotice}. */
|
|
203
|
+
function distTagNotice(pkg, tag) {
|
|
204
|
+
return `upx: no installed ${pkg}; "${tag}" is a dist-tag, using npx`;
|
|
205
|
+
}
|
|
206
|
+
const HELP_TEXT = `Usage: upx <pkg>@<range> [args…]
|
|
207
|
+
|
|
208
|
+
Runs a package's CLI from a local or global install matching <range>, falling
|
|
209
|
+
back to npx when nothing installed satisfies it.
|
|
210
|
+
|
|
211
|
+
Example:
|
|
212
|
+
$ upx cyberplace@^1.0.0 build
|
|
213
|
+
`;
|
|
214
|
+
/** Resolves `argv` (everything after `upx`) to a running child, per the Resolution algorithm in
|
|
215
|
+
* the spec: parse → classify the range → search local-then-global → resolve the bin → spawn, or
|
|
216
|
+
* fall back to npx with the spec exactly as given. */
|
|
217
|
+
function runUpx(argv, fs) {
|
|
218
|
+
const parsedArgv = parseArgv(argv);
|
|
219
|
+
if (!parsedArgv.ok) return {
|
|
220
|
+
kind: "error",
|
|
221
|
+
message: parsedArgv.error
|
|
222
|
+
};
|
|
223
|
+
if (parsedArgv.args.help) return {
|
|
224
|
+
kind: "help",
|
|
225
|
+
text: HELP_TEXT
|
|
226
|
+
};
|
|
227
|
+
const specResult = parseSpec(parsedArgv.args.spec);
|
|
228
|
+
if (!specResult.ok) return {
|
|
229
|
+
kind: "error",
|
|
230
|
+
message: specResult.error
|
|
231
|
+
};
|
|
232
|
+
const { pkg, range, bare } = specResult.spec;
|
|
233
|
+
const childArgs = parsedArgv.args.childArgs;
|
|
234
|
+
const semverRange = isSemverRange(range);
|
|
235
|
+
if (semverRange) {
|
|
236
|
+
const install = selectInstall(range, fs.findLocalInstalls(pkg), fs.findGlobalInstall(pkg));
|
|
237
|
+
if (install) {
|
|
238
|
+
const binResult = resolveBinPath(pkg, install.bin);
|
|
239
|
+
if (!binResult.ok) return {
|
|
240
|
+
kind: "error",
|
|
241
|
+
message: binResult.error
|
|
242
|
+
};
|
|
243
|
+
const binPath = path.join(install.dir, binResult.bin);
|
|
244
|
+
return {
|
|
245
|
+
kind: "exit",
|
|
246
|
+
code: fs.spawnBin(binPath, childArgs)
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const npxSpec = bare ? pkg : `${pkg}@${range}`;
|
|
251
|
+
return {
|
|
252
|
+
kind: "exit",
|
|
253
|
+
code: fs.spawnNpx([npxSpec, ...childArgs]),
|
|
254
|
+
notice: semverRange ? fallbackNotice(pkg, range) : distTagNotice(pkg, range)
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
//#endregion
|
|
258
|
+
//#region src/run/cli.ts
|
|
259
|
+
const outcome = runUpx(process.argv.slice(2), realRunFs());
|
|
260
|
+
if (outcome.kind === "help") {
|
|
261
|
+
process.stdout.write(outcome.text);
|
|
262
|
+
process.exit(0);
|
|
263
|
+
}
|
|
264
|
+
if (outcome.kind === "error") {
|
|
265
|
+
process.stderr.write(`${outcome.message}\n`);
|
|
266
|
+
process.exit(1);
|
|
267
|
+
}
|
|
268
|
+
if (outcome.notice) process.stderr.write(`${outcome.notice}\n`);
|
|
269
|
+
process.exit(outcome.code);
|
|
270
|
+
//#endregion
|
|
271
|
+
export {};
|
|
@@ -4,16 +4,18 @@ Authoritative rules for creating, validating, and transforming cross-vendor agen
|
|
|
4
4
|
|
|
5
5
|
A **plugin** is the distribution unit — it bundles skills, MCP servers, hooks, commands, agents, and other extensions into a single installable package. A **skill** is the capability unit inside a plugin. Install plugins; invoke skills.
|
|
6
6
|
|
|
7
|
-
## Source of Truth:
|
|
7
|
+
## Source of Truth: root `plugin.json`
|
|
8
8
|
|
|
9
|
-
Author
|
|
9
|
+
Author `plugin.json` at the plugin root as the canonical manifest — the Agent Plugins Specification v1.0.0 shape (closed field set, `additionalProperties: false`). All vendor manifests are derived from it via `build`. This file is never read directly by vendors at runtime; it is the single source that the build layer transforms into each vendor's manifest.
|
|
10
10
|
|
|
11
|
-
Schema declaration (first field):
|
|
11
|
+
Schema declaration (first field, fixed by the spec):
|
|
12
12
|
|
|
13
13
|
```json
|
|
14
|
-
{ "$schema": "https://
|
|
14
|
+
{ "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json" }
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
+
Only ten top-level fields exist: `$schema`, `name`, `version`, `description`, `author`, `homepage`, `repository`, `license`, `keywords`, `extensions`. No other top-level keys are permitted. All `universal-plugin`-specific config — component paths, the vendor build-target list, and per-harness overrides — nests under `extensions["org.cyberuni.universal-plugin"]`.
|
|
18
|
+
|
|
17
19
|
### Required fields
|
|
18
20
|
|
|
19
21
|
| Field | Type | Constraint |
|
|
@@ -34,12 +36,12 @@ Schema declaration (first field):
|
|
|
34
36
|
|
|
35
37
|
### Component path fields
|
|
36
38
|
|
|
37
|
-
Each accepts `string | string[] | { paths: string[] }`. Every path must start with `./`. No `../` segments — traversal rejected by conformant hosts.
|
|
39
|
+
All component paths and build config live under `extensions["org.cyberuni.universal-plugin"]` — the closed spec's field set has no top-level component keys. Each path field accepts `string | string[] | { paths: string[] }`. Every path must start with `./`. No `../` segments — traversal rejected by conformant hosts.
|
|
38
40
|
|
|
39
|
-
| Field | Component | Core? | Notes |
|
|
41
|
+
| Field (under `extensions["org.cyberuni.universal-plugin"]`) | Component | Core? | Notes |
|
|
40
42
|
| --- | --- | --- | --- |
|
|
41
43
|
| `skills` | Skill directories containing `SKILL.md` | Yes | Default: `./skills/` |
|
|
42
|
-
| `mcpServers` |
|
|
44
|
+
| `mcpServers` | `mcp.json` path or inline MCP config | Yes | Default: `./mcp.json` |
|
|
43
45
|
| `commands` | Slash command `.md` files | Extended | Default: `./commands/` |
|
|
44
46
|
| `agents` | Agent `.md` files | Extended | Default: `./agents/` |
|
|
45
47
|
| `rules` | Context rule `.mdc` files | Extended | Cursor-only; ignored by other hosts |
|
|
@@ -49,16 +51,16 @@ Each accepts `string | string[] | { paths: string[] }`. Every path must start wi
|
|
|
49
51
|
|
|
50
52
|
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.
|
|
51
53
|
|
|
52
|
-
### `
|
|
54
|
+
### `extensions["org.cyberuni.universal-plugin"]` — `vendors` and `harnesses`
|
|
53
55
|
|
|
54
|
-
|
|
56
|
+
`vendors` (array of vendor IDs) declares which vendor manifests `build` generates. `harnesses` (object keyed by vendor ID) provides vendor-specific fields for each declared vendor — the former `vendorExtensions.<vendor>` block, renamed and relocated. An empty `{}` entry in `harnesses` opts that vendor into build output with no vendor-specific fields, but the vendor ID must still be listed in `vendors` to be built.
|
|
55
57
|
|
|
56
58
|
| Vendor ID | Output path | Required beyond `name` |
|
|
57
59
|
| --- | --- | --- |
|
|
58
60
|
| `claude-code` | `.claude-plugin/plugin.json` | none |
|
|
59
61
|
| `cursor` | `.cursor-plugin/plugin.json` | none |
|
|
60
62
|
| `codex` | `.codex-plugin/plugin.json` | `version`, `description` |
|
|
61
|
-
| `copilot-cli` | `plugin.json` (repo root) | none |
|
|
63
|
+
| `copilot-cli` | `plugin.json` (repo root) — the canonical manifest itself; nothing is derived | none |
|
|
62
64
|
|
|
63
65
|
Vendor-specific extension fields:
|
|
64
66
|
|
|
@@ -82,8 +84,7 @@ Vendor-specific extension fields:
|
|
|
82
84
|
|
|
83
85
|
```
|
|
84
86
|
<plugin-name>/
|
|
85
|
-
├── .
|
|
86
|
-
│ └── plugin.json ← canonical source of truth
|
|
87
|
+
├── plugin.json ← canonical source of truth
|
|
87
88
|
│
|
|
88
89
|
├── skills/<skill-name>/SKILL.md ← shared: all vendors, identical format
|
|
89
90
|
│
|
|
@@ -112,12 +113,16 @@ Generated build artifacts (gitignore or commit — author's choice):
|
|
|
112
113
|
├── .codex-plugin/
|
|
113
114
|
│ ├── plugin.json ← generated
|
|
114
115
|
│ └── hooks/hooks.json ← generated (PascalCase, ${PLUGIN_ROOT} native)
|
|
115
|
-
└── plugin.json ← generated (copilot-cli root manifest)
|
|
116
116
|
```
|
|
117
117
|
|
|
118
|
+
`copilot-cli` has no entry here: it reads the canonical root `plugin.json` above, so nothing is
|
|
119
|
+
generated for it. Copilot CLI checks `.plugin/plugin.json` → `plugin.json` →
|
|
120
|
+
`.github/plugin/plugin.json` → `.claude-plugin/plugin.json` and takes the first match, so any file
|
|
121
|
+
derived to one of the lower paths would be shadowed by root and never read.
|
|
122
|
+
|
|
118
123
|
## Vendor Manifest Derivation
|
|
119
124
|
|
|
120
|
-
Build reads
|
|
125
|
+
Build reads root `plugin.json`, applies the rules below, writes each vendor's output.
|
|
121
126
|
|
|
122
127
|
### Metadata field mapping
|
|
123
128
|
|
|
@@ -210,7 +215,7 @@ If the repo needs explicit symlink tracking: `mcp.json symlink` in `.gitattribut
|
|
|
210
215
|
|
|
211
216
|
## Component Authoring Rules
|
|
212
217
|
|
|
213
|
-
**Skills:** Author `skills/<name>/SKILL.md` following the **skill-design** governance. Within a plugin, reference MCP tools by fully qualified name: `{plugin-name}:{server-name}__{tool-name}`.
|
|
218
|
+
**Skills:** Author `skills/<name>/SKILL.md` following the **skill-design** governance. Within a plugin, reference MCP tools by fully qualified name: `{plugin-name}:{server-name}__{tool-name}`. Use the **slash-invocation** governance when a skill needs a user-only, model-only, or explicit both-invocation policy.
|
|
214
219
|
|
|
215
220
|
**Commands:** One `.md` file per command in `commands/`. Filename (minus extension) is the command identifier. Optional frontmatter: `description`, `argument-hint`, `allowed-tools`, `disable-model-invocation`. `$ARGUMENTS` expands to user input.
|
|
216
221
|
|
|
@@ -241,7 +246,7 @@ If the repo needs explicit symlink tracking: `mcp.json symlink` in `.gitattribut
|
|
|
241
246
|
|
|
242
247
|
Default scope: **team**.
|
|
243
248
|
|
|
244
|
-
**npm distribution:**
|
|
249
|
+
**npm distribution:** The canonical `plugin.json`, the generated manifest directories (`.claude-plugin/`, `.cursor-plugin/`, `.codex-plugin/`), and component directories (`skills/`, `commands/`, `agents/`, `hooks/`) must be in `package.json#files`. `package.json` carries distribution metadata only — no plugin semantics.
|
|
245
250
|
|
|
246
251
|
## Anti-Patterns
|
|
247
252
|
|
|
@@ -276,4 +281,4 @@ npx universal-plugin@<version> governance show skill-repo-structure
|
|
|
276
281
|
npx universal-plugin@<version> governance show agent-tool-output
|
|
277
282
|
```
|
|
278
283
|
|
|
279
|
-
Spec: https://github.com/cyberuni/universal-plugin/blob/main/
|
|
284
|
+
Spec: https://github.com/cyberuni/universal-plugin/blob/main/packages/universal-plugin/.agents/spec/spec.md
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Slash Invocation
|
|
2
|
+
|
|
3
|
+
Use a skill's `invocation-policy` frontmatter field to say who may invoke it.
|
|
4
|
+
`skills/` remains the only canonical prompt-artifact tree; do not create a
|
|
5
|
+
parallel canonical `commands/` tree.
|
|
6
|
+
|
|
7
|
+
```md
|
|
8
|
+
---
|
|
9
|
+
description: Deploy the application after running release checks.
|
|
10
|
+
invocation-policy: user
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
Deploy $ARGUMENTS.
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
| Policy | Meaning | Default |
|
|
17
|
+
| --- | --- | --- |
|
|
18
|
+
| `user` | A person invokes the skill explicitly. Use for side-effecting workflows such as deploy or release. | No |
|
|
19
|
+
| `model` | The model may invoke the skill, but it is hidden from the slash menu. Use for background knowledge. | No |
|
|
20
|
+
| `both` | Either a person or the model may invoke the skill. | Yes |
|
|
21
|
+
|
|
22
|
+
`plugin build` derives vendor behavior from that policy:
|
|
23
|
+
|
|
24
|
+
- Claude Code uses the same `SKILL.md` and adds its native invocation flag.
|
|
25
|
+
- Cursor receives a thin `.cursor/commands/<skill>.md` prompt insert for `user` and `both` skills.
|
|
26
|
+
- Codex receives a best-effort, local-only `~/.codex/prompts/<skill>.md` for `user` and `both` skills. Codex has deprecated custom prompts, so the skill remains the primary integration.
|
|
27
|
+
- Copilot CLI receives no derived command. Its `/skill-name` form is only a prompt hint, not deterministic invocation.
|
|
28
|
+
|
|
29
|
+
If a workflow requires deterministic user-triggered invocation, document Copilot
|
|
30
|
+
CLI as unsupported for that workflow.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "universal-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Universal AI agent plugin build tool",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent-plugin",
|
|
@@ -11,14 +11,15 @@
|
|
|
11
11
|
],
|
|
12
12
|
"repository": {
|
|
13
13
|
"type": "git",
|
|
14
|
-
"url": "git+https://github.com/cyberuni/
|
|
14
|
+
"url": "git+https://github.com/cyberuni/universal-plugin.git",
|
|
15
15
|
"directory": "packages/universal-plugin"
|
|
16
16
|
},
|
|
17
17
|
"license": "MIT",
|
|
18
18
|
"author": "unional <homawong@gmail.com>",
|
|
19
19
|
"type": "module",
|
|
20
20
|
"bin": {
|
|
21
|
-
"universal-plugin": "bin/universal-plugin.mjs"
|
|
21
|
+
"universal-plugin": "bin/universal-plugin.mjs",
|
|
22
|
+
"upx": "bin/upx.mjs"
|
|
22
23
|
},
|
|
23
24
|
"exports": {
|
|
24
25
|
"./package.json": "./package.json"
|
|
@@ -26,13 +27,21 @@
|
|
|
26
27
|
"files": [
|
|
27
28
|
"bin",
|
|
28
29
|
"dist",
|
|
29
|
-
"governances"
|
|
30
|
+
"governances",
|
|
31
|
+
"plugin.json",
|
|
32
|
+
".claude-plugin",
|
|
33
|
+
".cursor-plugin",
|
|
34
|
+
".codex-plugin",
|
|
35
|
+
"skills",
|
|
36
|
+
"agents"
|
|
30
37
|
],
|
|
31
38
|
"dependencies": {
|
|
32
|
-
"commander": "^14.0.3"
|
|
39
|
+
"commander": "^14.0.3",
|
|
40
|
+
"semver": "^7.8.1"
|
|
33
41
|
},
|
|
34
42
|
"devDependencies": {
|
|
35
43
|
"@types/node": "^24.10.1",
|
|
44
|
+
"@types/semver": "^7.7.1",
|
|
36
45
|
"knip": "^6.14.1",
|
|
37
46
|
"tsdown": "^0.22.0",
|
|
38
47
|
"tsx": "^4.22.3",
|
package/plugin.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
|
+
"name": "universal-plugin",
|
|
4
|
+
"version": "0.3.0",
|
|
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
|
+
"author": {
|
|
7
|
+
"name": "unional"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/cyberuni/universal-plugin",
|
|
10
|
+
"repository": "https://github.com/cyberuni/universal-plugin",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"keywords": ["universal", "plugin", "agent", "cross-vendor"],
|
|
13
|
+
"extensions": {
|
|
14
|
+
"org.cyberuni.universal-plugin": {
|
|
15
|
+
"skills": "./skills/"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
package/readme.md
CHANGED
|
@@ -3,7 +3,13 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/universal-plugin)
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
|
|
6
|
-
Universal AI agent plugin build tool. Author one canonical plugin manifest (
|
|
6
|
+
Universal AI agent plugin build tool. Author one canonical plugin manifest (root `plugin.json`) and generate vendor-specific manifests for Claude Code, Cursor, Codex, and GitHub Copilot CLI.
|
|
7
|
+
|
|
8
|
+
## Specification
|
|
9
|
+
|
|
10
|
+
This package follows the [Agent Plugins Specification](https://github.com/agentplugins/agent-plugins-spec).
|
|
11
|
+
Consult that repository's versioned specification and releases before changing manifest
|
|
12
|
+
or component compatibility behavior; it is the canonical reference for the current standard.
|
|
7
13
|
|
|
8
14
|
## Usage
|
|
9
15
|
|
|
@@ -19,12 +25,28 @@ Or pin to an exact version for reproducible builds:
|
|
|
19
25
|
npx universal-plugin@0.2.0 <command>
|
|
20
26
|
```
|
|
21
27
|
|
|
28
|
+
## upx — the fast package runner
|
|
29
|
+
|
|
30
|
+
`npm i -g universal-plugin` also puts a second bin, `upx`, on PATH. `upx <pkg>@^<major>` finds an
|
|
31
|
+
already-installed version satisfying the range (local `node_modules` first, then global) and runs
|
|
32
|
+
it directly — about 10× faster than `npx`'s ~1s per-call resolve+spawn cost — falling back to
|
|
33
|
+
`npx` when nothing installed matches:
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
npm i -g universal-plugin
|
|
37
|
+
upx cyber-skills@^2 audit validate
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Use a caret range on the major, not an exact pin, so one global install serves every caller. `upx`
|
|
41
|
+
needs to be installed to be on PATH; `npx` always ships with npm, so `npx` remains the safe default
|
|
42
|
+
where `universal-plugin` isn't installed globally.
|
|
43
|
+
|
|
22
44
|
## Commands
|
|
23
45
|
|
|
24
46
|
### plugin — author the canonical manifest
|
|
25
47
|
|
|
26
48
|
```sh
|
|
27
|
-
# Generate vendor manifests from
|
|
49
|
+
# Generate vendor manifests from root plugin.json
|
|
28
50
|
npx universal-plugin plugin build
|
|
29
51
|
```
|
|
30
52
|
|
|
@@ -45,10 +67,22 @@ npx universal-plugin sync apply <action-id>
|
|
|
45
67
|
### publish
|
|
46
68
|
|
|
47
69
|
```sh
|
|
48
|
-
# Sync version from packagePath/package.json into
|
|
70
|
+
# Sync version from packagePath/package.json into root plugin.json
|
|
49
71
|
npx universal-plugin publish sync-version
|
|
50
72
|
```
|
|
51
73
|
|
|
74
|
+
### marketplace
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
# Generate a local Codex catalog from canonical plugin manifests.
|
|
78
|
+
npx universal-plugin marketplace init --codex --root .
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Codex caches a local plugin install by its marketplace entry version. After changing packaged plugin
|
|
82
|
+
files, update the canonical `plugin.json` version, regenerate the Codex catalog (use `--force` when
|
|
83
|
+
replacing an existing catalog), reinstall the plugin, and start a new Codex session. This ensures the
|
|
84
|
+
installed copy and its generated marketplace entry use the same version.
|
|
85
|
+
|
|
52
86
|
### governance
|
|
53
87
|
|
|
54
88
|
Version-pinned agent-tool contracts, read at runtime.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# adopt-upx skill
|
|
2
|
+
|
|
3
|
+
Rewrites `npx <pkg>@<version>` references in `SKILL.md` files to a caret range on `upx`
|
|
4
|
+
(`^<major>`, or `^0.<minor>` for a 0.x pin) — the fast local-first runner shipped by
|
|
5
|
+
`universal-plugin`.
|
|
6
|
+
|
|
7
|
+
## When to use
|
|
8
|
+
|
|
9
|
+
When you want a project's skills to call CLIs via `upx` instead of `npx`, for the ~10× speed win
|
|
10
|
+
on repeated invocations (local-first resolution vs. `npx`'s ~1s registry+spawn cost per call, even
|
|
11
|
+
cached).
|
|
12
|
+
|
|
13
|
+
## What it does
|
|
14
|
+
|
|
15
|
+
1. Confirms `upx` is installed (`npm i -g universal-plugin`) and on PATH.
|
|
16
|
+
2. Rewrites `npx <pkg>@<concrete-semver>` → `upx <pkg>@^<major>` (or `^0.<minor>` for a 0.x pin)
|
|
17
|
+
across a chosen scope:
|
|
18
|
+
- one specific skill (a path)
|
|
19
|
+
- a named set (a list of paths and/or globs)
|
|
20
|
+
- every skill in the project (`--all`)
|
|
21
|
+
3. Leaves non-semver placeholders (`@<version>`), dist-tags (`@next`), already-`upx` references,
|
|
22
|
+
and any `pin-exempt: true` skill untouched.
|
|
23
|
+
4. Reports a per-file rewrite count and a final tally. Safe to re-run (idempotent).
|
|
24
|
+
|
|
25
|
+
## Mechanism
|
|
26
|
+
|
|
27
|
+
`scripts/rewrite-upx.mjs` — a standalone Node script, no dependencies. See `SKILL.md` for usage.
|
|
28
|
+
|
|
29
|
+
## Tradeoff
|
|
30
|
+
|
|
31
|
+
A rewritten skill depends on `upx` being on PATH. `npx` ships with every npm install; `upx` only
|
|
32
|
+
exists after `npm i -g universal-plugin`. This is an opt-in migration, not a safe default.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npx skills add cyberuni/universal-plugin --skill adopt-upx
|
|
38
|
+
```
|