universal-plugin 0.11.1 → 0.11.2

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "universal-plugin",
3
- "version": "0.11.1",
3
+ "version": "0.11.2",
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.1",
3
+ "version": "0.11.2",
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.1",
3
+ "version": "0.11.2",
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
- ...componentConfig,
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, warnings);
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, warnings);
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, warnings) {
6647
- for (const skill of skills) {
6648
- if (vendor === "claude-code") {
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.1.0/node_modules/@cyberuni/agent-harness/dist/index.js
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "universal-plugin",
3
- "version": "0.11.1",
3
+ "version": "0.11.2",
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.1.0",
43
- "@repobuddy/upx": "^0.1.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.1",
4
+ "version": "0.11.2",
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"
@@ -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 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.
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
@@ -77,6 +77,7 @@ Each `code` below is what the script emits.
77
77
  | `stale` | a derived manifest older than `plugin.json` | `universal-plugin plugin build` |
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` |
@@ -232,6 +232,13 @@ if (build === null) {
232
232
  'no vendor is declared — the build writes nothing, so no runtime reads this plugin',
233
233
  '/universal-plugin:init-universal-plugin, update route',
234
234
  )
235
+ } else if (/has no "[^"]+" component/.test(warning)) {
236
+ add(
237
+ 'unsupported-component',
238
+ 'low',
239
+ warning,
240
+ 'declare the path under harnesses.<vendor> for the vendors that read it, or leave it — the build already drops it',
241
+ )
235
242
  } else if (/^Unknown vendor/.test(warning)) {
236
243
  add('unknown-vendor', 'medium', warning, 'fix the vendor id in plugin.json')
237
244
  } 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, plus whatever
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
- ## Skills
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
- For every skill that is not `invocation-policy: model`, the build also writes
39
- `~/.codex/prompts/<name>.md` — the skill body, as a Codex prompt.
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
- Two things follow. It writes **outside the repository**, into the current machine's home directory,
42
- so it is not part of the plugin's tracked output and does not travel with a clone. And it is
43
- **best-effort**: a failure there becomes a build warning, not a failed build. Read the warnings.
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