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.
Files changed (38) hide show
  1. package/.claude-plugin/plugin.json +15 -0
  2. package/.codex-plugin/plugin.json +14 -0
  3. package/.cursor-plugin/plugin.json +14 -0
  4. package/agents/agentskills-specialist.md +132 -0
  5. package/bin/upx.mjs +6 -0
  6. package/dist/cli.mjs +1330 -255
  7. package/dist/run.mjs +271 -0
  8. package/governances/plugin-design.md +22 -17
  9. package/governances/slash-invocation.md +30 -0
  10. package/package.json +14 -5
  11. package/plugin.json +18 -0
  12. package/readme.md +37 -3
  13. package/skills/adopt-upx/README.md +38 -0
  14. package/skills/adopt-upx/SKILL.md +120 -0
  15. package/skills/adopt-upx/scripts/rewrite-upx.mjs +168 -0
  16. package/skills/migrate-plugin/SKILL.md +106 -0
  17. package/skills/migrate-plugin/evals/evals.json +11 -0
  18. package/skills/migrate-plugin/evals/trigger-queries.json +35 -0
  19. package/skills/plugin/README.md +37 -0
  20. package/skills/plugin/SKILL.md +105 -0
  21. package/skills/plugin/assets/templates/agent.md +7 -0
  22. package/skills/plugin/assets/templates/command.md +9 -0
  23. package/skills/plugin/assets/templates/hooks.json +9 -0
  24. package/skills/plugin/assets/templates/plugin.json +19 -0
  25. package/skills/plugin/assets/templates/setup-command.md +15 -0
  26. package/skills/plugin/assets/templates/skill.md +15 -0
  27. package/skills/plugin/references/adopt.md +114 -0
  28. package/skills/plugin/references/create.md +163 -0
  29. package/skills/plugin/references/delete.md +23 -0
  30. package/skills/plugin/references/inspect.md +21 -0
  31. package/skills/plugin/references/update.md +26 -0
  32. package/skills/plugin/references/version.md +97 -0
  33. package/skills/publish-plugin/SKILL.md +246 -0
  34. package/skills/publish-plugin/evals/evals.json +23 -0
  35. package/skills/publish-plugin/references/vendor-requirements.md +38 -0
  36. package/skills/upgrade-plugin/README.md +23 -0
  37. package/skills/upgrade-plugin/SKILL.md +86 -0
  38. 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: `.plugin/plugin.json`
7
+ ## Source of Truth: root `plugin.json`
8
8
 
9
- Author `.plugin/plugin.json` as the canonical manifest. 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.
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://schema.cyberuni.dev/universal-agent-plugin/v1.json" }
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` | `.mcp.json` path or inline MCP config | Yes | Default: `./.mcp.json` |
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
- ### `vendorExtensions` field
54
+ ### `extensions["org.cyberuni.universal-plugin"]` — `vendors` and `harnesses`
53
55
 
54
- Declares which vendor manifests `build` generates, and provides vendor-specific fields for each. Each key is a recognized vendor ID; its presence drives build output. An empty `{}` opts into that vendor's output with no vendor-specific fields.
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
- ├── .plugin/
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 `.plugin/plugin.json`, applies the rules below, writes each vendor's output.
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:** All manifest directories (`.plugin/`, `.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.
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/spec/universal-plugin-system.md
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.2.1",
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/cyberplace.git",
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
  [![npm version](https://img.shields.io/npm/v/universal-plugin.svg)](https://www.npmjs.com/package/universal-plugin)
4
4
  [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
5
5
 
6
- Universal AI agent plugin build tool. Author one canonical plugin manifest (`.plugin/plugin.json`) and generate vendor-specific manifests for Claude Code, Cursor, Codex, and GitHub Copilot CLI.
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 .plugin/plugin.json
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 .plugin/plugin.json
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
+ ```