universal-plugin 0.11.1 → 0.11.3
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 +133 -23
- package/package.json +3 -3
- package/plugin.json +1 -1
- package/readme.md +1 -1
- package/references/slash-invocation.md +1 -1
- package/skills/doctor-universal-plugin/SKILL.md +6 -4
- package/skills/doctor-universal-plugin/scripts/doctor.mjs +17 -1
- package/skills/init-universal-plugin/references/vendors/claude-code.md +2 -1
- package/skills/init-universal-plugin/references/vendors/codex.md +27 -6
- package/skills/init-universal-plugin/references/vendors/cursor.md +6 -0
- package/skills/init-universal-plugin/scripts/init.mjs +0 -0
- package/skills/marketplace/scripts/add.mjs +0 -0
- package/skills/marketplace/scripts/validate.mjs +0 -0
- package/skills/version/README.md +1 -1
- package/skills/version/SKILL.md +7 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "universal-plugin",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.3",
|
|
4
4
|
"description": "Research and design toolkit for building universal AI coding agent plugins that work across Claude Code, Cursor, Codex, and GitHub Copilot CLI.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "unional"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "universal-plugin",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.3",
|
|
4
4
|
"description": "Research and design toolkit for building universal AI coding agent plugins that work across Claude Code, Cursor, Codex, and GitHub Copilot CLI.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "unional"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "universal-plugin",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.3",
|
|
4
4
|
"description": "Research and design toolkit for building universal AI coding agent plugins that work across Claude Code, Cursor, Codex, and GitHub Copilot CLI.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "unional"
|
package/dist/cli.mjs
CHANGED
|
@@ -6392,6 +6392,66 @@ const COPILOT_LSP_PATH = "lsp.json";
|
|
|
6392
6392
|
/** Authored under the namespace, never derived: Copilot's canvas extensions have no canonical root
|
|
6393
6393
|
* location to derive from, so `--clean` must leave this subtree alone. */
|
|
6394
6394
|
const COPILOT_AUTHORED_DIR = "extensions";
|
|
6395
|
+
/** The component paths each derived manifest accepts. A path on the shared extension reaches every
|
|
6396
|
+
* derived manifest unless the vendor is listed here without it — then the build leaves it out and
|
|
6397
|
+
* warns, as it does for `dependencies` (issue #132). A harness override is applied after this
|
|
6398
|
+
* filter, so `harnesses.<vendor>` can still set any key on purpose.
|
|
6399
|
+
* Each row is what the runtime reads: Claude Code's SchemaStore manifest schema, Cursor's published
|
|
6400
|
+
* `plugin.schema.json`, and Codex's manifest loader, which reads `commands` too (migrated into
|
|
6401
|
+
* skills on install). Codex's plugin-creator validator is narrower — it rejects `hooks` and
|
|
6402
|
+
* `commands` — and is not what the row follows. With a root Agent Plugins `$schema`, Codex reads only
|
|
6403
|
+
* `apps`, `hooks` and `interface` from its manifest and ignores the rest, so the Codex row is the
|
|
6404
|
+
* superset its legacy (no-`$schema`) mode reads (`.research/plugin-schema/evidence.md` E28).
|
|
6405
|
+
* Copilot CLI is absent: it reads the canonical manifest and derives no manifest of its own. */
|
|
6406
|
+
const VENDOR_COMPONENTS = {
|
|
6407
|
+
"claude-code": /* @__PURE__ */ new Set([
|
|
6408
|
+
"skills",
|
|
6409
|
+
"commands",
|
|
6410
|
+
"agents",
|
|
6411
|
+
"hooks",
|
|
6412
|
+
"mcpServers",
|
|
6413
|
+
"lspServers",
|
|
6414
|
+
"outputStyles",
|
|
6415
|
+
"themes",
|
|
6416
|
+
"channels",
|
|
6417
|
+
"monitors"
|
|
6418
|
+
]),
|
|
6419
|
+
cursor: /* @__PURE__ */ new Set([
|
|
6420
|
+
"skills",
|
|
6421
|
+
"commands",
|
|
6422
|
+
"agents",
|
|
6423
|
+
"rules",
|
|
6424
|
+
"hooks",
|
|
6425
|
+
"mcpServers"
|
|
6426
|
+
]),
|
|
6427
|
+
codex: /* @__PURE__ */ new Set([
|
|
6428
|
+
"skills",
|
|
6429
|
+
"commands",
|
|
6430
|
+
"apps",
|
|
6431
|
+
"hooks",
|
|
6432
|
+
"mcpServers"
|
|
6433
|
+
])
|
|
6434
|
+
};
|
|
6435
|
+
/** Every component key some vendor reads. Only these are filtered; any other key on the extension is
|
|
6436
|
+
* not a component and passes through as before. */
|
|
6437
|
+
const COMPONENT_KEYS = new Set(Object.values(VENDOR_COMPONENTS).flatMap((set) => [...set ?? []]));
|
|
6438
|
+
/** Splits the shared component config into what `vendor` reads and the component keys it has none
|
|
6439
|
+
* of. A vendor without a table entry keeps everything. */
|
|
6440
|
+
function vendorComponentConfig(componentConfig, vendor) {
|
|
6441
|
+
const supported = VENDOR_COMPONENTS[vendor];
|
|
6442
|
+
if (!supported) return {
|
|
6443
|
+
config: componentConfig,
|
|
6444
|
+
dropped: []
|
|
6445
|
+
};
|
|
6446
|
+
const config = {};
|
|
6447
|
+
const dropped = [];
|
|
6448
|
+
for (const [key, value] of Object.entries(componentConfig)) if (COMPONENT_KEYS.has(key) && !supported.has(key)) dropped.push(key);
|
|
6449
|
+
else config[key] = value;
|
|
6450
|
+
return {
|
|
6451
|
+
config,
|
|
6452
|
+
dropped
|
|
6453
|
+
};
|
|
6454
|
+
}
|
|
6395
6455
|
/** What each vendor's build output occupies in the published package, for `plugin init --npm`'s
|
|
6396
6456
|
* `package.json` `files` wiring. */
|
|
6397
6457
|
const VENDOR_SHIPPED_PATHS = {
|
|
@@ -6501,9 +6561,10 @@ function buildPlugin(root, opts = {}) {
|
|
|
6501
6561
|
const outputPath = path.join(root, relPath);
|
|
6502
6562
|
const outputDir = path.dirname(outputPath);
|
|
6503
6563
|
const vendorFields = harnesses[vendor] ?? {};
|
|
6564
|
+
const components = vendorComponentConfig(componentConfig, vendor);
|
|
6504
6565
|
const vendorManifest = {
|
|
6505
6566
|
...metadata,
|
|
6506
|
-
...
|
|
6567
|
+
...components.config,
|
|
6507
6568
|
...vendorFields
|
|
6508
6569
|
};
|
|
6509
6570
|
const hooks = canonicalHooks ? translateHooks(canonicalHooks, vendor) : null;
|
|
@@ -6515,7 +6576,7 @@ function buildPlugin(root, opts = {}) {
|
|
|
6515
6576
|
if (mcp?.changed) warnings.push(`${vendor} reads the canonical plugin.json directly — the pinned mcpServers is not delivered to it`);
|
|
6516
6577
|
const overrides = Object.keys(vendorFields);
|
|
6517
6578
|
if (overrides.length > 0) warnings.push(`harnesses.${vendor} sets ${overrides.join(", ")}, but ${vendor} reads the canonical plugin.json directly — these fields are not delivered`);
|
|
6518
|
-
writeSkillArtifacts(vendor, skills, opts, written
|
|
6579
|
+
writeSkillArtifacts(vendor, skills, opts, written);
|
|
6519
6580
|
const derived = deriveCopilotNamespace(root, componentConfig, hooks, indent, opts, written, warnings);
|
|
6520
6581
|
rows.push(derived ? {
|
|
6521
6582
|
vendor,
|
|
@@ -6528,6 +6589,7 @@ function buildPlugin(root, opts = {}) {
|
|
|
6528
6589
|
});
|
|
6529
6590
|
continue;
|
|
6530
6591
|
}
|
|
6592
|
+
for (const key of components.dropped.filter((k) => !(k in vendorFields))) warnings.push(`${vendor} has no "${key}" component — the path is left out of ${relPath}`);
|
|
6531
6593
|
for (const drop of dedupeDrops(hooks?.drops ?? [])) warnings.push(`${vendor} cannot run the "${drop.type}" hook handler on ${drop.event} — dropped from the derived hooks file`);
|
|
6532
6594
|
const derivedHooksPath = path.join(outputDir, "hooks.json");
|
|
6533
6595
|
if (hooks?.changed) {
|
|
@@ -6555,7 +6617,7 @@ function buildPlugin(root, opts = {}) {
|
|
|
6555
6617
|
else if (!opts.dryRun && fs.existsSync(derivedHooksPath)) fs.unlinkSync(derivedHooksPath);
|
|
6556
6618
|
}
|
|
6557
6619
|
if (mcp?.changed && declaredMcp && !declaredMcp.inline) writeArtifact(derivedMcpPath, `${JSON.stringify({ mcpServers: mcp.servers }, null, indent)}\n`, opts, written);
|
|
6558
|
-
writeSkillArtifacts(vendor, skills, opts, written
|
|
6620
|
+
writeSkillArtifacts(vendor, skills, opts, written);
|
|
6559
6621
|
rows.push({
|
|
6560
6622
|
vendor,
|
|
6561
6623
|
path: relPath,
|
|
@@ -6643,20 +6705,9 @@ function refreshCatalogs(root, manifest, vendors, opts, written, warnings) {
|
|
|
6643
6705
|
catalogRoot: repo.root
|
|
6644
6706
|
} : { catalogs: rows };
|
|
6645
6707
|
}
|
|
6646
|
-
function writeSkillArtifacts(vendor, skills, opts, written
|
|
6647
|
-
|
|
6648
|
-
|
|
6649
|
-
writeClaudeSkill(skill, opts, written);
|
|
6650
|
-
continue;
|
|
6651
|
-
}
|
|
6652
|
-
if (vendor === "cursor") continue;
|
|
6653
|
-
if (skill.invocationPolicy === "model") continue;
|
|
6654
|
-
if (vendor === "codex") try {
|
|
6655
|
-
writeArtifact(path.join(os.homedir(), ".codex", "prompts", `${skill.name}.md`), skill.body, opts, written);
|
|
6656
|
-
} catch (err) {
|
|
6657
|
-
warnings.push(`Failed to write Codex prompt for skill "${skill.name}" (best-effort): ${err instanceof Error ? err.message : String(err)}`);
|
|
6658
|
-
}
|
|
6659
|
-
}
|
|
6708
|
+
function writeSkillArtifacts(vendor, skills, opts, written) {
|
|
6709
|
+
if (vendor !== "claude-code") return;
|
|
6710
|
+
for (const skill of skills) writeClaudeSkill(skill, opts, written);
|
|
6660
6711
|
}
|
|
6661
6712
|
/** Resolves a `pathValue` declaration — a single "./" path, an array of them, or a { paths: [...] }
|
|
6662
6713
|
* object — into the list of declared paths. Returns null when the field is absent or malformed, so
|
|
@@ -7664,7 +7715,7 @@ function initCommand$1(deps = { fs: realInitFs }) {
|
|
|
7664
7715
|
return cmd;
|
|
7665
7716
|
}
|
|
7666
7717
|
//#endregion
|
|
7667
|
-
//#region ../../node_modules/.pnpm/@cyberuni+agent-harness@0.
|
|
7718
|
+
//#region ../../node_modules/.pnpm/@cyberuni+agent-harness@0.3.0/node_modules/@cyberuni/agent-harness/dist/index.js
|
|
7668
7719
|
const harnessIds = [
|
|
7669
7720
|
"claude-code",
|
|
7670
7721
|
"cursor",
|
|
@@ -7675,7 +7726,10 @@ const harnessIds = [
|
|
|
7675
7726
|
"gemini-cli",
|
|
7676
7727
|
"qwen-code",
|
|
7677
7728
|
"vscode-copilot",
|
|
7678
|
-
"cline"
|
|
7729
|
+
"cline",
|
|
7730
|
+
"crush",
|
|
7731
|
+
"openhands",
|
|
7732
|
+
"augment"
|
|
7679
7733
|
];
|
|
7680
7734
|
function resolveHarnessEnvironment(environment = {}) {
|
|
7681
7735
|
return {
|
|
@@ -7896,6 +7950,50 @@ const storages = {
|
|
|
7896
7950
|
research: "E-CLINE-P3"
|
|
7897
7951
|
}]
|
|
7898
7952
|
};
|
|
7953
|
+
},
|
|
7954
|
+
crush: ({ env, homedir }) => {
|
|
7955
|
+
return {
|
|
7956
|
+
harness: "crush",
|
|
7957
|
+
configDir: env.CRUSH_GLOBAL_CONFIG || join(env.XDG_CONFIG_HOME || join(homedir, ".config"), "crush"),
|
|
7958
|
+
locations: []
|
|
7959
|
+
};
|
|
7960
|
+
},
|
|
7961
|
+
openhands: ({ env, homedir }) => {
|
|
7962
|
+
const configDir = env.OH_PERSISTENCE_DIR || env.OPENHANDS_PERSISTENCE_DIR || join(homedir, ".openhands");
|
|
7963
|
+
const installed = join(configDir, "plugins", "installed");
|
|
7964
|
+
return {
|
|
7965
|
+
harness: "openhands",
|
|
7966
|
+
configDir,
|
|
7967
|
+
locations: [{
|
|
7968
|
+
kind: "installed-plugins",
|
|
7969
|
+
path: installed,
|
|
7970
|
+
description: "Installed plugins, one folder each",
|
|
7971
|
+
research: "E-OH-P3"
|
|
7972
|
+
}, {
|
|
7973
|
+
kind: "enabled-record",
|
|
7974
|
+
path: join(installed, ".installed.json"),
|
|
7975
|
+
description: "Install metadata keyed by plugin name, each with enabled: <bool>",
|
|
7976
|
+
research: "E-OH-P4"
|
|
7977
|
+
}]
|
|
7978
|
+
};
|
|
7979
|
+
},
|
|
7980
|
+
augment: ({ homedir }) => {
|
|
7981
|
+
const configDir = join(homedir, ".augment");
|
|
7982
|
+
return {
|
|
7983
|
+
harness: "augment",
|
|
7984
|
+
configDir,
|
|
7985
|
+
locations: [{
|
|
7986
|
+
kind: "marketplaces",
|
|
7987
|
+
path: join(configDir, "plugins", "marketplaces"),
|
|
7988
|
+
description: "Marketplace checkouts",
|
|
7989
|
+
research: "E-AUG-P2"
|
|
7990
|
+
}, {
|
|
7991
|
+
kind: "enabled-record",
|
|
7992
|
+
path: join(configDir, "settings.json"),
|
|
7993
|
+
description: "User settings; enabledPlugins maps plugin@marketplace to a boolean",
|
|
7994
|
+
research: "E-AUG-P4"
|
|
7995
|
+
}]
|
|
7996
|
+
};
|
|
7899
7997
|
}
|
|
7900
7998
|
};
|
|
7901
7999
|
//#endregion
|
|
@@ -9459,13 +9557,25 @@ function syncVersion(root, syncFs) {
|
|
|
9459
9557
|
//#region src/publish/cli.ts
|
|
9460
9558
|
function publishCommand() {
|
|
9461
9559
|
const cmd = new Command("publish").description("Prepare plugin for publishing").helpCommand(false);
|
|
9462
|
-
cmd.command("sync-version").description("Sync version from packagePath/package.json (default: the plugin root) into plugin.json").addOption(ROOT_OPTION).action((opts) => {
|
|
9560
|
+
cmd.command("sync-version").description("Sync version from packagePath/package.json (default: the plugin root) into plugin.json").option("--no-build", "Skip re-deriving the vendor manifests").addOption(ROOT_OPTION).action((opts) => {
|
|
9463
9561
|
try {
|
|
9464
|
-
const
|
|
9465
|
-
|
|
9562
|
+
const root = resolveRoot(opts.root);
|
|
9563
|
+
const result = syncVersion(root, realSyncVersionFs);
|
|
9564
|
+
const derived = [];
|
|
9565
|
+
if (opts.build !== false) {
|
|
9566
|
+
const build = buildPlugin(root, {});
|
|
9567
|
+
for (const warning of build.warnings) process.stderr.write(`warn: ${warning}\n`);
|
|
9568
|
+
for (const written of build.written) derived.push(path.relative(root, written).split(path.sep).join("/"));
|
|
9569
|
+
}
|
|
9570
|
+
output({
|
|
9571
|
+
...result,
|
|
9572
|
+
derived
|
|
9573
|
+
}, {
|
|
9466
9574
|
version: result.version,
|
|
9467
|
-
manifest: result.manifestPath
|
|
9575
|
+
manifest: result.manifestPath,
|
|
9576
|
+
derived
|
|
9468
9577
|
});
|
|
9578
|
+
if (opts.build === false) process.stderr.write("→ universal-plugin plugin build\n");
|
|
9469
9579
|
} catch (err) {
|
|
9470
9580
|
process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
|
|
9471
9581
|
process.exit(1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "universal-plugin",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.3",
|
|
4
4
|
"description": "Universal AI agent plugin build tool",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent-plugin",
|
|
@@ -39,8 +39,8 @@
|
|
|
39
39
|
"agents"
|
|
40
40
|
],
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@cyberuni/agent-harness": "^0.
|
|
43
|
-
"@repobuddy/upx": "^0.
|
|
42
|
+
"@cyberuni/agent-harness": "^0.3.0",
|
|
43
|
+
"@repobuddy/upx": "^0.2.0",
|
|
44
44
|
"@toon-format/toon": "^4.1.1",
|
|
45
45
|
"commander": "^14.0.3",
|
|
46
46
|
"semver": "^7.8.1"
|
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.11.
|
|
4
|
+
"version": "0.11.3",
|
|
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
|
@@ -69,7 +69,7 @@ npx universal-plugin sync apply <action-id>
|
|
|
69
69
|
### publish
|
|
70
70
|
|
|
71
71
|
```sh
|
|
72
|
-
npx universal-plugin publish sync-version # copy packagePath/package.json version into plugin.json
|
|
72
|
+
npx universal-plugin publish sync-version # copy packagePath/package.json version into plugin.json, then rebuild
|
|
73
73
|
```
|
|
74
74
|
|
|
75
75
|
This writes the canonical `plugin.json` only. Run `plugin build` afterwards, or the vendor manifests
|
|
@@ -23,7 +23,7 @@ Deploy $ARGUMENTS.
|
|
|
23
23
|
|
|
24
24
|
- Claude Code uses the same `SKILL.md` and adds its native invocation flag.
|
|
25
25
|
- Cursor receives a thin `.cursor/commands/<skill>.md` prompt insert for `user` and `both` skills.
|
|
26
|
-
- Codex
|
|
26
|
+
- Codex uses the skill itself. A user invokes it with `$skill` or `/skills`, so no command is derived. Codex custom prompts are deprecated and home-only, and `plugin build` never writes outside the plugin tree.
|
|
27
27
|
- Copilot CLI receives no derived command. Its `/skill-name` form is only a prompt hint, not deterministic invocation.
|
|
28
28
|
|
|
29
29
|
If a workflow requires deterministic user-triggered invocation, document Copilot
|
|
@@ -73,15 +73,16 @@ Each `code` below is what the script emits.
|
|
|
73
73
|
| `no-manifest` | no root `plugin.json` — this is not a plugin yet | `/universal-plugin:init-universal-plugin` |
|
|
74
74
|
| `legacy-manifest` | root `plugin.json` with neither `$schema` nor `extensions` — a single-vendor manifest on the canonical path | `/universal-plugin:init-universal-plugin`, adopt route |
|
|
75
75
|
| `vendor-only` | a vendor manifest with no canonical manifest above it | `/universal-plugin:init-universal-plugin`, adopt route |
|
|
76
|
-
| `unbuilt` | a declared vendor whose output path holds no file — that runtime sees no plugin |
|
|
77
|
-
| `stale` | a derived manifest older than `plugin.json` |
|
|
76
|
+
| `unbuilt` | a declared vendor whose output path holds no file — that runtime sees no plugin | `/universal-plugin:build-plugin` |
|
|
77
|
+
| `stale` | a derived manifest older than `plugin.json`; for a derived directory (`com.github.copilot/`), its newest file is what is compared | `/universal-plugin:build-plugin` |
|
|
78
78
|
| `hand-edited` | a derived manifest that `build` would rewrite — the edit is already lost, it just has not been overwritten yet | move the field to the canonical manifest or to `harnesses.<vendor>`, then rebuild |
|
|
79
79
|
| `unknown-vendor` | a `vendors` entry no build target matches; reported as `skipped` plus a warning | fix the id in `plugin.json` |
|
|
80
|
+
| `unsupported-component` | a component path on the shared extension names a component one targeted vendor has none of (`agents` for Codex, `rules` for Claude Code, …); the build leaves it out of that vendor's manifest | declare the path under `harnesses.<vendor>` for the vendors that read it, or leave it — nothing is lost |
|
|
80
81
|
| `undeliverable-override` | `harnesses["copilot-cli"]` sets fields that reach nothing | `/universal-plugin:init-universal-plugin`, update route — move them to a vendor that has a derived manifest, or drop them |
|
|
81
82
|
| `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 |
|
|
82
83
|
| `version-drift` | the `packagePath` `package.json` and the canonical manifest carry different versions | `/universal-plugin:version` |
|
|
83
84
|
| `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` |
|
|
84
|
-
| `copilot-root-components` | agents, commands, rules, hooks, or LSP servers sit at the plugin root with no copy under `com.github.copilot/` — Copilot CLI reads them only from there in spec mode, so it loads none of them, silently |
|
|
85
|
+
| `copilot-root-components` | agents, commands, rules, hooks, or LSP servers sit at the plugin root with no copy under `com.github.copilot/` — Copilot CLI reads them only from there in spec mode, so it loads none of them, silently | `/universal-plugin:build-plugin` |
|
|
85
86
|
| `stale-github-plugin` | a leftover `.github/plugin/plugin.json` from an older build — shadowed by root and no longer generated | `/universal-plugin:remove-plugin` |
|
|
86
87
|
| `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` |
|
|
87
88
|
| `no-vendors` | no vendor is declared, so the build writes nothing and no runtime reads the plugin. On a repository still on the pre-0.6 layout the build stops rather than reporting an empty result, and the detail says so — read it beside `legacy-manifest` and `shadowing-manifest`, which name the signals | `/universal-plugin:init-universal-plugin`, adopt route on the pre-0.6 layout, else update route |
|
|
@@ -125,7 +126,7 @@ the diff:
|
|
|
125
126
|
```bash
|
|
126
127
|
git status --short # must be clean first, or the diff proves nothing
|
|
127
128
|
npx universal-plugin plugin build
|
|
128
|
-
git diff -- .claude-plugin .cursor-plugin .codex-plugin
|
|
129
|
+
git diff -- .claude-plugin .cursor-plugin .codex-plugin com.github.copilot
|
|
129
130
|
```
|
|
130
131
|
|
|
131
132
|
An empty diff means the derived manifests match what the canonical manifest says. Any hunk is drift —
|
|
@@ -179,6 +180,7 @@ is meant to ship — content that is still being worked on is not a finding to a
|
|
|
179
180
|
| Task | Skill |
|
|
180
181
|
|------|-------|
|
|
181
182
|
| Create, adopt, or change what the plugin declares | `init-universal-plugin` |
|
|
183
|
+
| Rebuild the derived manifests (`unbuilt`, `stale`, `copilot-root-components`) | `build-plugin` |
|
|
182
184
|
| Move the plugin's version | `version` |
|
|
183
185
|
| Remove derived manifests, or the plugin itself | `remove-plugin` |
|
|
184
186
|
| Generate the repository's own marketplace catalogs | `marketplace` |
|
|
@@ -30,6 +30,15 @@ const readJson = (file) => {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
const mtime = (file) => (fs.existsSync(file) ? fs.statSync(file).mtimeMs : null)
|
|
33
|
+
// A derived directory (copilot-cli's com.github.copilot/) is as fresh as the newest file in it. The
|
|
34
|
+
// directory's own mtime moves only when an entry is added, removed or renamed, so a rebuild that
|
|
35
|
+
// rewrites its files in place leaves it as old as the first build (issue #144).
|
|
36
|
+
const contentMtime = (abs) => {
|
|
37
|
+
const stat = fs.statSync(abs)
|
|
38
|
+
if (!stat.isDirectory()) return stat.mtimeMs
|
|
39
|
+
const files = fs.readdirSync(abs).map((entry) => contentMtime(path.join(abs, entry)))
|
|
40
|
+
return files.length === 0 ? stat.mtimeMs : Math.max(...files)
|
|
41
|
+
}
|
|
33
42
|
|
|
34
43
|
const VENDOR_MANIFESTS = ['.claude-plugin/plugin.json', '.cursor-plugin/plugin.json', '.codex-plugin/plugin.json']
|
|
35
44
|
|
|
@@ -207,7 +216,7 @@ if (build === null) {
|
|
|
207
216
|
const abs = path.join(root, row.path)
|
|
208
217
|
const exists = fs.existsSync(abs)
|
|
209
218
|
// `canonical` means the vendor reads root plugin.json; no derived file is expected.
|
|
210
|
-
const stale = row.status === 'built' && exists && manifestMtime !== null &&
|
|
219
|
+
const stale = row.status === 'built' && exists && manifestMtime !== null && contentMtime(abs) < manifestMtime
|
|
211
220
|
vendors.push({ vendor: row.vendor, path: row.path, status: row.status, exists, stale })
|
|
212
221
|
|
|
213
222
|
if (row.status === 'built' && !exists) {
|
|
@@ -232,6 +241,13 @@ if (build === null) {
|
|
|
232
241
|
'no vendor is declared — the build writes nothing, so no runtime reads this plugin',
|
|
233
242
|
'/universal-plugin:init-universal-plugin, update route',
|
|
234
243
|
)
|
|
244
|
+
} else if (/has no "[^"]+" component/.test(warning)) {
|
|
245
|
+
add(
|
|
246
|
+
'unsupported-component',
|
|
247
|
+
'low',
|
|
248
|
+
warning,
|
|
249
|
+
'declare the path under harnesses.<vendor> for the vendors that read it, or leave it — the build already drops it',
|
|
250
|
+
)
|
|
235
251
|
} else if (/^Unknown vendor/.test(warning)) {
|
|
236
252
|
add('unknown-vendor', 'medium', warning, 'fix the vendor id in plugin.json')
|
|
237
253
|
} else {
|
|
@@ -8,7 +8,8 @@ npx universal-plugin plugin build --vendor claude-code
|
|
|
8
8
|
|
|
9
9
|
## What lands in the derived manifest
|
|
10
10
|
|
|
11
|
-
The shared metadata from the canonical top level, plus the component paths
|
|
11
|
+
The shared metadata from the canonical top level, plus the component paths Claude Code reads (every
|
|
12
|
+
one except `rules` and Codex's `apps`; the build drops those with a warning), plus whatever
|
|
12
13
|
`extensions["org.cyberuni.universal-plugin"].harnesses["claude-code"]` sets. `$schema`, `extensions`,
|
|
13
14
|
`vendors`, and `harnesses` are universal-plugin's own orchestration — they never
|
|
14
15
|
appear in a vendor manifest.
|
|
@@ -33,14 +33,35 @@ Codex's presentation metadata goes under its `harnesses` entry:
|
|
|
33
33
|
}
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
##
|
|
36
|
+
## Components
|
|
37
|
+
|
|
38
|
+
What Codex reads from `.codex-plugin/plugin.json` depends on the root `plugin.json`:
|
|
39
|
+
|
|
40
|
+
- **Root declares the canonical `$schema`** (`https://agent-plugins.org/schemas/1.0.0/plugin.schema.json`,
|
|
41
|
+
the normal universal-plugin layout). Codex reads root `plugin.json` as an Agent Plugins manifest,
|
|
42
|
+
takes skills from `./skills` and MCP servers from `./mcp.json`, and uses `.codex-plugin/plugin.json`
|
|
43
|
+
only as an overlay for `apps`, `hooks`, and `interface`. Any other key there is ignored, and
|
|
44
|
+
commands are not read at all.
|
|
45
|
+
- **Root has no `$schema`**. `.codex-plugin/plugin.json` is the manifest. Codex reads `skills`,
|
|
46
|
+
`commands`, `hooks`, `mcpServers`, and `apps` from it, and migrates each command into a skill on
|
|
47
|
+
install, falling back to `./commands/` when `commands` is absent.
|
|
37
48
|
|
|
38
|
-
|
|
39
|
-
|
|
49
|
+
Codex has no `agents`, `rules`, `lspServers`, `outputStyles`, `themes`, `channels`, or `monitors` in
|
|
50
|
+
either case. `plugin build` leaves any of those out of `.codex-plugin/plugin.json` and warns
|
|
51
|
+
(`codex has no "agents" component — the path is left out of .codex-plugin/plugin.json`); the other
|
|
52
|
+
vendors still get the path. Set the path under `harnesses.codex` only if you mean Codex to see it
|
|
53
|
+
anyway — a harness override is never filtered.
|
|
54
|
+
|
|
55
|
+
The Codex runtime ignores keys it does not read. The plugin-creator validator Codex ships
|
|
56
|
+
(`validate_plugin.py`) is stricter: it rejects any field outside its allowlist, including `commands`
|
|
57
|
+
and `hooks`. That matters only if you submit the plugin through OpenAI's ingestion flow.
|
|
58
|
+
|
|
59
|
+
## Skills
|
|
40
60
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
61
|
+
Codex reaches a plugin's skills natively. A user invokes one with `$name` or `/skills`, so the
|
|
62
|
+
build derives nothing per skill for Codex. Codex custom prompts (`~/.codex/prompts/`) are deprecated
|
|
63
|
+
and load only from the user's home directory, and the build never writes there. See
|
|
64
|
+
[`codex-skill-invocation`](https://github.com/cyberuni/universal-plugin/blob/main/.research/codex-skill-invocation/conclusion.md).
|
|
44
65
|
|
|
45
66
|
## Hooks
|
|
46
67
|
|
|
@@ -22,6 +22,12 @@ Cursor's catalog metadata goes under its `harnesses` entry, not at the canonical
|
|
|
22
22
|
}
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
+
## Components
|
|
26
|
+
|
|
27
|
+
Cursor reads `skills`, `commands`, `agents`, `rules`, `hooks`, and `mcpServers`. Its manifest schema
|
|
28
|
+
is closed, so the build leaves `lspServers`, `outputStyles`, `apps`, and Claude Code's other
|
|
29
|
+
components out of `.cursor-plugin/plugin.json` and warns.
|
|
30
|
+
|
|
25
31
|
## Skills
|
|
26
32
|
|
|
27
33
|
Cursor reads `SKILL.md` straight from the path the manifest's `skills` field names, and lets the user
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/skills/version/README.md
CHANGED
|
@@ -18,7 +18,7 @@ The first question the skill asks is whether the repository uses changesets.
|
|
|
18
18
|
|
|
19
19
|
- **With changesets** — the number is decided by the release, not by this skill. Add a changeset, let
|
|
20
20
|
the release run, and `publish sync-version` carries the released number into the canonical
|
|
21
|
-
manifest.
|
|
21
|
+
manifest and re-derives the vendor manifests. With `--no-build`, `plugin build` is the next step.
|
|
22
22
|
- **Without** — `plugin version <bump>` is the whole step. `scripts/version.mjs` runs it from the CLI
|
|
23
23
|
shipped beside the skill, so nothing is downloaded.
|
|
24
24
|
|
package/skills/version/SKILL.md
CHANGED
|
@@ -22,12 +22,17 @@ test -d .changeset && echo "changesets"
|
|
|
22
22
|
|
|
23
23
|
**If it does**, the version number is decided by changesets, not by you. Add a changeset and let the
|
|
24
24
|
release run — the repo's `version` script should already call `publish sync-version`, which carries
|
|
25
|
-
the released number from `package.json` into the canonical manifest
|
|
25
|
+
the released number from `package.json` into the canonical manifest and re-derives the vendor
|
|
26
|
+
manifests, the same way `plugin version` does:
|
|
26
27
|
|
|
27
28
|
```bash
|
|
28
29
|
npx universal-plugin publish sync-version
|
|
29
30
|
```
|
|
30
31
|
|
|
32
|
+
If the script passes `--no-build`, it must run `plugin build` next (`/universal-plugin:build-plugin`);
|
|
33
|
+
otherwise the vendor manifests and `com.github.copilot/` keep the old version, and a consumer's
|
|
34
|
+
plugin cache never re-extracts.
|
|
35
|
+
|
|
31
36
|
Do **not** run `plugin version` in a changesets repo — it would decide a number changesets is about
|
|
32
37
|
to decide again.
|
|
33
38
|
|
|
@@ -109,6 +114,7 @@ Every guard resolves before the first write, so a failed run leaves the tree unt
|
|
|
109
114
|
| Task | Skill |
|
|
110
115
|
|------|-------|
|
|
111
116
|
| Create, adopt, or change what the plugin declares | `init-universal-plugin` |
|
|
117
|
+
| Re-derive the vendor manifests by hand | `build-plugin` |
|
|
112
118
|
| Check whether the two authored versions agree | `doctor` |
|
|
113
119
|
| Add a changeset for the change being released | `add-changeset` |
|
|
114
120
|
| Refresh the repository's own marketplace catalogs after a bump | `marketplace` |
|