universal-plugin 0.15.0 → 0.16.1

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.15.0",
3
+ "version": "0.16.1",
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"
@@ -16,6 +16,6 @@
16
16
  ],
17
17
  "skills": "./skills/",
18
18
  "dependencies": [
19
- "cyber-agent-harness"
19
+ "cyber-agentkit"
20
20
  ]
21
21
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "universal-plugin",
3
- "version": "0.15.0",
3
+ "version": "0.16.1",
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.15.0",
3
+ "version": "0.16.1",
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
@@ -5232,14 +5232,19 @@ function absoluteSource(origin, source) {
5232
5232
  path: subdir
5233
5233
  };
5234
5234
  }
5235
- /** The source forms each runtime installs from./** The source forms each runtime installs from.
5235
+ /** The source forms each runtime installs from.
5236
5236
  *
5237
5237
  * Every runtime takes a repository path. Beyond that they diverge, and the divergence is not
5238
5238
  * cosmetic: a catalog is read at install time in someone else's terminal, so a source a runtime
5239
5239
  * cannot resolve is a failure far from here. Claude Code's schema documents the full tagged set
5240
- * (<https://json.schemastore.org/claude-code-marketplace.json>). Codex documents npm alongside a
5241
- * local path. Copilot CLI and Cursor document local paths only, which is why an npm entry reaches
5242
- * two catalogs rather than four (`.research/local-marketplaces`, and issue #86). */
5240
+ * (<https://json.schemastore.org/claude-code-marketplace.json>). The rest was found by running the
5241
+ * CLIs (`.research/local-marketplaces`, "Remote entry sources"):
5242
+ *
5243
+ * - Codex installs `npm`, `url` and `git-subdir`, and silently drops a `github` entry.
5244
+ * - Copilot CLI installs `github` (also with `path`) and `url`, and refuses the *whole*
5245
+ * marketplace over a single `npm` or `git-subdir` entry.
5246
+ * - Cursor's shipped bundle resolves `github`, `url` and `git-subdir`, all by git URL, and has no
5247
+ * `npm` source. Read from the bundle, not run. */
5243
5248
  const TARGET_SOURCE_KINDS = {
5244
5249
  claude: [
5245
5250
  "path",
@@ -5248,10 +5253,164 @@ const TARGET_SOURCE_KINDS = {
5248
5253
  "url",
5249
5254
  "git-subdir"
5250
5255
  ],
5251
- codex: ["path", "npm"],
5252
- copilot: ["path"],
5253
- cursor: ["path"]
5256
+ codex: [
5257
+ "path",
5258
+ "npm",
5259
+ "url",
5260
+ "git-subdir"
5261
+ ],
5262
+ copilot: [
5263
+ "path",
5264
+ "github",
5265
+ "url"
5266
+ ],
5267
+ cursor: [
5268
+ "path",
5269
+ "github",
5270
+ "url",
5271
+ "git-subdir"
5272
+ ]
5254
5273
  };
5274
+ /** The kind name used to decide whether a runtime accepts a source: a repository path, or the tag. */
5275
+ function sourceKindOf(source) {
5276
+ if (isLocalCatalogSource(source)) return "path";
5277
+ return typeof source === "string" ? "path" : source.source;
5278
+ }
5279
+ /** The runtimes that read `.claude-plugin/marketplace.json` when they find no catalog of their own
5280
+ * ahead of it. Copilot CLI looks in `.github/plugin/` before `.claude-plugin/` (verified by running
5281
+ * it against a repository carrying both). Cursor's bundle lists `.cursor-plugin/marketplace.json`
5282
+ * first (read, not run). Codex prefers `.agents/plugins/marketplace.json` (E-CODEX-M22). So a
5283
+ * catalog of the runtime's own shields it from whatever the Claude catalog holds. */
5284
+ const CLAUDE_CATALOG_READERS = [
5285
+ "codex",
5286
+ "copilot",
5287
+ "cursor"
5288
+ ];
5289
+ function pickPins(source) {
5290
+ const pins = {};
5291
+ if (typeof source.ref === "string") pins.ref = source.ref;
5292
+ if (typeof source.sha === "string") pins.sha = source.sha;
5293
+ return pins;
5294
+ }
5295
+ /** A git URL ending in `.git`, which Cursor requires of `url` and `git-subdir`. Only a GitHub URL is
5296
+ * known to serve the same repository with the suffix, so any other host's URL is left as written. */
5297
+ function withGitSuffix(url) {
5298
+ if (/\.git$/.test(url)) return url;
5299
+ return GITHUB_URL.test(url) ? `${url.replace(/\/+$/, "")}.git` : void 0;
5300
+ }
5301
+ /** Turns a source into the closest one a runtime installs, or says why none exists.
5302
+ *
5303
+ * A mapping is made only where it is lossless: `github` becomes a `url` (or, with a `path`, a
5304
+ * `git-subdir`) on Codex, and a `git-subdir` on a GitHub URL becomes `github` + `path` on Copilot
5305
+ * CLI. `ref` and `sha` pins cross with it. Anything else the runtime cannot install is omitted,
5306
+ * because writing it would have the runtime drop the entry or refuse the whole catalog. */
5307
+ function adaptSource(target, source) {
5308
+ const kind = sourceKindOf(source);
5309
+ if (kind === "path" || typeof source === "string") return { source };
5310
+ const supported = TARGET_SOURCE_KINDS[target].includes(kind);
5311
+ if ((target === "codex" || target === "cursor" && typeof source.path === "string") && kind === "github" && typeof source.repo === "string") {
5312
+ const url = `https://github.com/${source.repo}.git`;
5313
+ return typeof source.path === "string" ? { source: {
5314
+ source: "git-subdir",
5315
+ url,
5316
+ path: source.path,
5317
+ ...pickPins(source)
5318
+ } } : { source: {
5319
+ source: "url",
5320
+ url,
5321
+ ...pickPins(source)
5322
+ } };
5323
+ }
5324
+ if (target === "copilot" && kind === "git-subdir" && typeof source.url === "string" && typeof source.path === "string") {
5325
+ const match = GITHUB_URL.exec(source.url.trim());
5326
+ if (match) return { source: {
5327
+ source: "github",
5328
+ repo: match[1],
5329
+ path: source.path,
5330
+ ...pickPins(source)
5331
+ } };
5332
+ return { omitted: "git-subdir is installable only from a GitHub URL, which is mapped to github with a path" };
5333
+ }
5334
+ if (!supported) return { omitted: `${kind} source is not supported by ${target}` };
5335
+ if (target === "cursor" && (kind === "url" || kind === "git-subdir") && typeof source.url === "string") {
5336
+ const url = withGitSuffix(source.url);
5337
+ if (url === void 0) return { omitted: `cursor requires a ".git" URL for a ${kind} source` };
5338
+ return { source: url === source.url ? source : {
5339
+ ...source,
5340
+ url
5341
+ } };
5342
+ }
5343
+ return { source };
5344
+ }
5345
+ /** Applies `adaptSource` to every entry of a serialized catalog: an entry is rewritten where a
5346
+ * mapping exists and removed where none does. The content comes back untouched when nothing
5347
+ * changed, so a catalog that needed no adapting is not reformatted. */
5348
+ function adaptCatalogContent(target, content) {
5349
+ let parsed;
5350
+ try {
5351
+ parsed = JSON.parse(content);
5352
+ } catch {
5353
+ return {
5354
+ content,
5355
+ omitted: []
5356
+ };
5357
+ }
5358
+ if (typeof parsed !== "object" || parsed === null || !Array.isArray(parsed.plugins)) return {
5359
+ content,
5360
+ omitted: []
5361
+ };
5362
+ const catalog = parsed;
5363
+ const omitted = [];
5364
+ let changed = false;
5365
+ const plugins = [];
5366
+ for (const entry of catalog.plugins) {
5367
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
5368
+ plugins.push(entry);
5369
+ continue;
5370
+ }
5371
+ const record = entry;
5372
+ const source = record.source;
5373
+ if (typeof source !== "string" && (typeof source !== "object" || source === null)) {
5374
+ plugins.push(entry);
5375
+ continue;
5376
+ }
5377
+ const adapted = adaptSource(target, source);
5378
+ if ("omitted" in adapted) {
5379
+ omitted.push({
5380
+ name: typeof record.name === "string" ? record.name : "(unnamed)",
5381
+ reason: adapted.omitted
5382
+ });
5383
+ changed = true;
5384
+ } else if (adapted.source !== source) {
5385
+ plugins.push({
5386
+ ...record,
5387
+ source: adapted.source
5388
+ });
5389
+ changed = true;
5390
+ } else plugins.push(entry);
5391
+ }
5392
+ if (!changed) return {
5393
+ content,
5394
+ omitted
5395
+ };
5396
+ return {
5397
+ content: json({
5398
+ ...catalog,
5399
+ plugins
5400
+ }),
5401
+ omitted
5402
+ };
5403
+ }
5404
+ /** The catalog a runtime that also reads the Claude catalog should carry of its own, derived from
5405
+ * the Claude one: the same identity and entries, each adapted to what that runtime installs. */
5406
+ function deriveSharedCatalog(target, claudeContent) {
5407
+ const claude = parseCatalog(claudeContent, TARGET_CATALOG_PATHS.claude);
5408
+ const metadata = existingMetadata(claude);
5409
+ const base = JSON.parse(serializeTarget(target, metadata, [])[0]?.content ?? "{}");
5410
+ const adapted = adaptCatalogContent(target, json({ plugins: Array.isArray(claude.plugins) ? claude.plugins : [] }));
5411
+ base.plugins = JSON.parse(adapted.content).plugins;
5412
+ return json(base);
5413
+ }
5255
5414
  /** Folds one plugin's entry into a catalog that may already exist, and returns the artifact to
5256
5415
  * write. An existing catalog keeps its own top-level fields — its name, its owner, a description
5257
5416
  * someone wrote — and every entry it lists for other plugins, in place. Only this plugin's entry is
@@ -5781,13 +5940,42 @@ function validateCodex(catalog) {
5781
5940
  });
5782
5941
  return issues;
5783
5942
  }
5943
+ /** What a runtime does with a source kind it cannot install: Copilot CLI refuses the whole
5944
+ * catalog, Codex drops the entry without a word, Cursor skips it. All three leave the plugin
5945
+ * uninstallable, so each is an issue rather than a note
5946
+ * (`.research/local-marketplaces`, "Remote entry sources"). */
5947
+ const REJECTION = {
5948
+ copilot: "Copilot CLI refuses the whole catalog",
5949
+ codex: "Codex silently drops the entry",
5950
+ cursor: "Cursor skips the entry"
5951
+ };
5952
+ /** Entries whose source `target` cannot install, one issue each. A source `adaptSource` can map is
5953
+ * still an issue here: this checks what a catalog says, not what the generator could have written. */
5954
+ function sourceKindIssues(target, catalog) {
5955
+ if (!isObject$1(catalog) || !Array.isArray(catalog.plugins) || target === "claude") return [];
5956
+ const issues = [];
5957
+ catalog.plugins.forEach((entry, index) => {
5958
+ if (!isObject$1(entry)) return;
5959
+ const source = entry.source;
5960
+ if (typeof source !== "string" && !isObject$1(source)) return;
5961
+ const adapted = adaptSource(target, source);
5962
+ if ("source" in adapted && adapted.source === source) return;
5963
+ const kind = sourceKindOf(source);
5964
+ const fix = "source" in adapted ? ` — write ${JSON.stringify(adapted.source)}` : " — omit the entry";
5965
+ issues.push({
5966
+ path: `plugins[${index}].source`,
5967
+ message: `${kind} source is not installable by ${target}; ${REJECTION[target]}${fix}`
5968
+ });
5969
+ });
5970
+ return issues;
5971
+ }
5784
5972
  /** Every issue a target's runtime would raise against this catalog, empty when it loads. */
5785
5973
  function validateCatalog(target, catalog) {
5786
5974
  if (!isObject$1(catalog)) return [{
5787
5975
  path: "",
5788
5976
  message: `catalog must be a JSON object, not ${typeName(catalog)}`
5789
5977
  }];
5790
- return target === "codex" ? validateCodex(catalog) : validateClaudeShaped(catalog);
5978
+ return [...target === "codex" ? validateCodex(catalog) : validateClaudeShaped(catalog), ...sourceKindIssues(target, catalog)];
5791
5979
  }
5792
5980
  /** The same check against a catalog still in its serialized form. Text that does not parse is one
5793
5981
  * issue rather than a thrown error, so a caller checking four files reports all four. */
@@ -9253,7 +9441,7 @@ function initCommand$1(deps = { fs: realInitFs }) {
9253
9441
  return cmd;
9254
9442
  }
9255
9443
  //#endregion
9256
- //#region ../../node_modules/.pnpm/@cyberuni+agent-harness@0.7.0/node_modules/@cyberuni/agent-harness/dist/reference-where-CIcUR8xX.js
9444
+ //#region ../../node_modules/.pnpm/cyber-agentkit@1.0.0/node_modules/cyber-agentkit/dist/reference-where-BhymfNgA.js
9257
9445
  var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
9258
9446
  var __require = /* #__PURE__ */ (() => createRequire(import.meta.url))();
9259
9447
  function resolveHarnessEnvironment(environment = {}) {
@@ -16033,7 +16221,7 @@ var require_public_api = /* @__PURE__ */ __commonJSMin(((exports) => {
16033
16221
  exports.visitAsync = visit.visitAsync;
16034
16222
  })))();
16035
16223
  //#endregion
16036
- //#region ../../node_modules/.pnpm/@cyberuni+agent-harness@0.7.0/node_modules/@cyberuni/agent-harness/dist/index.js
16224
+ //#region ../../node_modules/.pnpm/cyber-agentkit@1.0.0/node_modules/cyber-agentkit/dist/index.js
16037
16225
  const harnessIds = [
16038
16226
  "claude-code",
16039
16227
  "cursor",
@@ -16063,7 +16251,7 @@ function mergeRegistries(base, override) {
16063
16251
  return result;
16064
16252
  }
16065
16253
  /** Fills each vendor's `localPluginDir` from `locate`, which returns the harness's `local-plugins`
16066
- * folder when `@cyberuni/agent-harness` knows one. A vendor it knows nothing about keeps its
16254
+ * folder when `cyber-agentkit` knows one. A vendor it knows nothing about keeps its
16067
16255
  * registry value, or `null`. */
16068
16256
  function withLocalPluginDirs(registry, locate) {
16069
16257
  return Object.fromEntries(Object.entries(registry).map(([id, config]) => [id, {
@@ -16080,12 +16268,12 @@ function bundledRegistryPath() {
16080
16268
  function userOverridePath() {
16081
16269
  return path$1.join(os.homedir(), ".agents", "universal-plugin-vendors.json");
16082
16270
  }
16083
- /** The harness's `local-plugins` folder, as `@cyberuni/agent-harness` locates it. */
16271
+ /** The harness's `local-plugins` folder, as `cyber-agentkit` locates it. */
16084
16272
  function localPluginsDir(vendorId) {
16085
16273
  if (!harnessIds.includes(vendorId)) return void 0;
16086
16274
  return pluginStorage(vendorId).locations.find((l) => l.kind === "local-plugins")?.path;
16087
16275
  }
16088
- /** The bundled registry, with the per-harness facts `@cyberuni/agent-harness` owns filled in, then
16276
+ /** The bundled registry, with the per-harness facts `cyber-agentkit` owns filled in, then
16089
16277
  * the user's override on top. */
16090
16278
  function loadRegistry() {
16091
16279
  const bundled = withLocalPluginDirs(JSON.parse(fs$1.readFileSync(bundledRegistryPath(), "utf8")), localPluginsDir);
@@ -16572,19 +16760,64 @@ function normalizeJson(value) {
16572
16760
  const record = value;
16573
16761
  return Object.fromEntries(Object.keys(record).sort().map((key) => [key, normalizeJson(record[key])]));
16574
16762
  }
16763
+ /** The entries a runtime with no catalog of its own would have read from the Claude one, as the
16764
+ * starting point for its catalog. Codex states an entry its own way, so those are re-shaped. */
16765
+ function sharedStart(target, claudeContent) {
16766
+ if (claudeContent === void 0) return void 0;
16767
+ if (target !== "codex") return claudeContent;
16768
+ const plugins = JSON.parse(claudeContent).plugins ?? [];
16769
+ return JSON.stringify({ plugins: plugins.map((entry) => ({
16770
+ name: entry.name,
16771
+ ...typeof entry.version === "string" ? { version: entry.version } : {},
16772
+ source: entry.source,
16773
+ policy: {
16774
+ installation: "AVAILABLE",
16775
+ authentication: "ON_INSTALL"
16776
+ },
16777
+ category: "Productivity"
16778
+ })) });
16779
+ }
16575
16780
  function initializeMarketplace(rootInput, opts = {}, fs = realMarketplaceFs) {
16576
16781
  const root = path$1.resolve(rootInput);
16577
16782
  const metadata = deriveMetadata(root, fs, opts);
16578
16783
  const plugins = discoverPlugins(root, fs, opts.scanDirs);
16579
- const derived = selectedTargets$1(opts.targets).map((target) => ({
16580
- target,
16581
- artifacts: serializeTarget(target, metadata, plugins)
16582
- }));
16583
- for (const { artifacts } of derived) for (const artifact of artifacts) assertContained(root, path$1.join(root, artifact.path), fs, "selected artifact");
16584
- const planned = derived.map(({ target, artifacts }) => ({
16585
- target,
16586
- artifacts: artifacts.map((artifact) => mergeDiscoveredCatalog(artifact, readCatalog(fs, path$1.join(root, artifact.path))))
16587
- }));
16784
+ const requested = selectedTargets$1(opts.targets);
16785
+ const targets = requested.includes("claude") ? ["claude", ...requested.filter((t) => t !== "claude")] : requested;
16786
+ const derived = new Map(targets.map((target) => [target, serializeTarget(target, metadata, plugins)]));
16787
+ const readOwn = (artifact) => readCatalog(fs, path$1.join(root, artifact.path));
16788
+ const planTarget = (target, claudeContent) => {
16789
+ const omitted = [];
16790
+ return {
16791
+ target,
16792
+ artifacts: (derived.get(target) ?? []).map((artifact) => {
16793
+ assertContained(root, path$1.join(root, artifact.path), fs, "selected artifact");
16794
+ const shared = CLAUDE_CATALOG_READERS.includes(target) ? sharedStart(target, claudeContent) : void 0;
16795
+ const merged = mergeDiscoveredCatalog(artifact, readOwn(artifact) ?? shared);
16796
+ const adapted = adaptCatalogContent(target, merged.content);
16797
+ omitted.push(...adapted.omitted);
16798
+ return {
16799
+ path: merged.path,
16800
+ content: adapted.content
16801
+ };
16802
+ }),
16803
+ omitted
16804
+ };
16805
+ };
16806
+ const planned = [];
16807
+ let claudeContent;
16808
+ for (const target of targets) {
16809
+ const plan = planTarget(target, claudeContent);
16810
+ if (target === "claude") claudeContent = plan.artifacts[0]?.content;
16811
+ planned.push(plan);
16812
+ }
16813
+ const shielded = /* @__PURE__ */ new Set();
16814
+ if (claudeContent !== void 0) for (const target of ["copilot", "cursor"]) {
16815
+ if (targets.includes(target)) continue;
16816
+ if (adaptCatalogContent(target, claudeContent).omitted.length === 0) continue;
16817
+ derived.set(target, serializeTarget(target, metadata, plugins));
16818
+ planned.push(planTarget(target, claudeContent));
16819
+ shielded.add(target);
16820
+ }
16588
16821
  for (const { target, artifacts } of planned) for (const artifact of artifacts) {
16589
16822
  const issues = validateCatalogContent(target, artifact.content);
16590
16823
  if (issues.length > 0) throw new Error(formatCatalogIssues(artifact.path, issues));
@@ -16595,7 +16828,7 @@ function initializeMarketplace(rootInput, opts = {}, fs = realMarketplaceFs) {
16595
16828
  if (fs.exists(output) && !sameArtifact(fs, output, artifact.content) && !opts.force) conflicts.push(artifact.path);
16596
16829
  }
16597
16830
  if (conflicts.length > 0) throw new Error(`error: generated artifact differs: ${conflicts.join(", ")}; rerun with --force to replace selected artifacts`);
16598
- const results = planned.map(({ target, artifacts }) => {
16831
+ const results = planned.map(({ target, artifacts, omitted }) => {
16599
16832
  if (plugins.length === 0) return {
16600
16833
  target,
16601
16834
  status: "empty",
@@ -16608,7 +16841,9 @@ function initializeMarketplace(rootInput, opts = {}, fs = realMarketplaceFs) {
16608
16841
  target,
16609
16842
  status: opts.dryRun ? "planned" : unchanged ? "unchanged" : "generated",
16610
16843
  paths: artifacts.map((artifact) => artifact.path),
16611
- plugins: [...new Set(artifacts.flatMap((artifact) => catalogEntryNames(artifact.content)))]
16844
+ plugins: [...new Set(artifacts.flatMap((artifact) => catalogEntryNames(artifact.content)))],
16845
+ ...omitted.length > 0 ? { omitted } : {},
16846
+ ...shielded.has(target) ? { reason: `written so ${target} does not read ${TARGET_CATALOG_PATHS.claude}, which lists entries it cannot install` } : {}
16612
16847
  };
16613
16848
  });
16614
16849
  if (!opts.dryRun && plugins.length > 0) writeArtifacts(fs, planned.flatMap((entry) => entry.artifacts), root);
@@ -16810,11 +17045,6 @@ const ENTRY_METADATA_FIELDS = [
16810
17045
  "license",
16811
17046
  "keywords"
16812
17047
  ];
16813
- /** The kind name used to decide whether a runtime accepts a source, from the source itself. */
16814
- function sourceKindOf(source) {
16815
- if (isLocalCatalogSource(source)) return "path";
16816
- return typeof source === "string" ? "path" : source.source;
16817
- }
16818
17048
  /** A source rendered for the result table — one short cell, not a JSON blob. */
16819
17049
  function describeSource(source) {
16820
17050
  if (typeof source === "string") return source;
@@ -16936,27 +17166,46 @@ function addToMarketplace(rootInput, spec, opts = {}, fs = realMarketplaceFs) {
16936
17166
  source: resolved.source,
16937
17167
  metadata: entryMetadata(resolved.metadata, opts.metadata)
16938
17168
  };
16939
- const kind = sourceKindOf(resolved.source);
16940
17169
  const rendered = describeSource(resolved.source);
16941
- const planned = selectedTargets(opts.targets).map((target) => {
17170
+ const targets = selectedTargets(opts.targets);
17171
+ const readCatalogFile = (target) => {
17172
+ const file = path$1.join(root, TARGET_CATALOG_PATHS[target]);
17173
+ return fs.exists(file) ? fs.read(file) : void 0;
17174
+ };
17175
+ const claudeAfter = targets.includes("claude") ? mergeCatalogEntry("claude", metadata, plugin, () => readCatalogFile("claude"), { keepForeignSource: false }).content : readCatalogFile("claude");
17176
+ const planned = targets.map((target) => {
16942
17177
  const catalogPath = TARGET_CATALOG_PATHS[target];
16943
17178
  const file = path$1.join(root, catalogPath);
16944
- const existing = fs.exists(file) ? fs.read(file) : void 0;
16945
- if (!TARGET_SOURCE_KINDS[target].includes(kind)) return {
16946
- target,
16947
- catalogPath,
16948
- file,
16949
- existing,
16950
- skipped: `${kind} source is not supported by ${target}`,
16951
- artifact: void 0
16952
- };
17179
+ const existing = readCatalogFile(target);
17180
+ const adapted = adaptSource(target, resolved.source);
17181
+ if ("omitted" in adapted) {
17182
+ const reason = `${adapted.omitted}; "${name}" is left out of ${catalogPath}`;
17183
+ const shielded = existing === void 0 && CLAUDE_CATALOG_READERS.includes(target) && target !== "codex" && claudeAfter !== void 0 ? {
17184
+ path: catalogPath,
17185
+ content: deriveSharedCatalog(target, claudeAfter)
17186
+ } : void 0;
17187
+ return {
17188
+ target,
17189
+ catalogPath,
17190
+ file,
17191
+ existing,
17192
+ skipped: shielded ? `${reason}; wrote the catalog without it so ${target} does not read the Claude one` : reason,
17193
+ artifact: shielded
17194
+ };
17195
+ }
17196
+ const changed = adapted.source !== resolved.source;
17197
+ const base = existing ?? (CLAUDE_CATALOG_READERS.includes(target) && target !== "codex" && claudeAfter !== void 0 && adaptCatalogContent(target, claudeAfter).omitted.length > 0 ? deriveSharedCatalog(target, claudeAfter) : void 0);
16953
17198
  return {
16954
17199
  target,
16955
17200
  catalogPath,
16956
17201
  file,
16957
17202
  existing,
16958
17203
  skipped: void 0,
16959
- artifact: mergeCatalogEntry(target, metadata, plugin, () => existing, { keepForeignSource: false })
17204
+ artifact: mergeCatalogEntry(target, metadata, {
17205
+ ...plugin,
17206
+ source: adapted.source
17207
+ }, () => base, { keepForeignSource: false }),
17208
+ mapped: changed ? `${sourceKindOf(resolved.source)} source written as ${describeSource(adapted.source)}` : void 0
16960
17209
  };
16961
17210
  });
16962
17211
  for (const entry of planned) {
@@ -16977,23 +17226,30 @@ function addToMarketplace(rootInput, spec, opts = {}, fs = realMarketplaceFs) {
16977
17226
  source: rendered,
16978
17227
  path: entry.catalogPath
16979
17228
  };
16980
- if (entry.skipped || !entry.artifact) return {
17229
+ if (entry.skipped) return {
16981
17230
  ...row,
16982
17231
  status: "skipped",
16983
- source: rendered,
16984
17232
  reason: entry.skipped
16985
17233
  };
17234
+ if (!entry.artifact) return {
17235
+ ...row,
17236
+ status: "skipped"
17237
+ };
17238
+ const mapped = "mapped" in entry ? { reason: entry.mapped } : {};
16986
17239
  if (entry.existing !== void 0 && sameCatalogContent(entry.existing, entry.artifact.content)) return {
16987
17240
  ...row,
17241
+ ...mapped,
16988
17242
  status: "unchanged"
16989
17243
  };
16990
17244
  if (opts.dryRun) return {
16991
17245
  ...row,
17246
+ ...mapped,
16992
17247
  status: "planned"
16993
17248
  };
16994
17249
  const existed = entry.existing !== void 0 && catalogEntry(entry.existing, name) !== void 0;
16995
17250
  return {
16996
17251
  ...row,
17252
+ ...mapped,
16997
17253
  status: existed ? "updated" : "added"
16998
17254
  };
16999
17255
  });
@@ -17090,15 +17346,30 @@ function validateMarketplace(rootInput, opts = {}, fs = realMarketplaceFs) {
17090
17346
  ]).map((target) => {
17091
17347
  const relative = TARGET_CATALOG_PATHS[target];
17092
17348
  const file = path$1.join(root, relative);
17093
- if (!fs.exists(file)) return {
17094
- target,
17095
- path: relative,
17096
- status: opts.required ? "invalid" : "missing",
17097
- issues: opts.required ? [{
17098
- path: "",
17099
- message: "no catalog at this path"
17100
- }] : []
17101
- };
17349
+ if (!fs.exists(file)) {
17350
+ const claudeFile = path$1.join(root, TARGET_CATALOG_PATHS.claude);
17351
+ if (CLAUDE_CATALOG_READERS.includes(target) && fs.exists(claudeFile)) {
17352
+ const shared = parseAndCheck(fs.read(claudeFile), (parsed) => sourceKindIssues(target, parsed)).map((issue) => ({
17353
+ path: issue.path,
17354
+ message: `${issue.message} (read from ${TARGET_CATALOG_PATHS.claude}; write ${relative} to shield ${target} from it)`
17355
+ }));
17356
+ if (shared.length > 0) return {
17357
+ target,
17358
+ path: TARGET_CATALOG_PATHS.claude,
17359
+ status: "invalid",
17360
+ issues: shared
17361
+ };
17362
+ }
17363
+ return {
17364
+ target,
17365
+ path: relative,
17366
+ status: opts.required ? "invalid" : "missing",
17367
+ issues: opts.required ? [{
17368
+ path: "",
17369
+ message: "no catalog at this path"
17370
+ }] : []
17371
+ };
17372
+ }
17102
17373
  const content = fs.read(file);
17103
17374
  const issues = [...validateCatalogContent(target, content), ...parseAndCheck(content, (parsed) => [...checkSources(root, fs, parsed), ...target === "claude" ? checkDependencies(root, fs, parsed, content, relative) : []])];
17104
17375
  return {
@@ -17209,6 +17480,10 @@ function initCommand() {
17209
17480
  })),
17210
17481
  summary: `${results.length} targets`
17211
17482
  });
17483
+ for (const row of results) {
17484
+ for (const entry of row.omitted ?? []) process.stderr.write(`warning: omitted "${entry.name}" from ${row.target}: ${entry.reason}\n`);
17485
+ if (row.reason) process.stderr.write(`${row.target}: ${row.reason}\n`);
17486
+ }
17212
17487
  process.stderr.write("Generated repository metadata only; no marketplace publication, registration, installation, authentication, or provisioning occurred.\n");
17213
17488
  } catch (err) {
17214
17489
  process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "universal-plugin",
3
- "version": "0.15.0",
3
+ "version": "0.16.1",
4
4
  "description": "Universal AI agent plugin build tool",
5
5
  "keywords": [
6
6
  "agent-plugin",
@@ -39,10 +39,10 @@
39
39
  "agents"
40
40
  ],
41
41
  "dependencies": {
42
- "@cyberuni/agent-harness": "^0.7.0",
43
42
  "@repobuddy/upx": "^0.2.0",
44
43
  "@toon-format/toon": "^4.1.1",
45
44
  "commander": "^15.0.0",
45
+ "cyber-agentkit": "^1.0.0",
46
46
  "rolldown": "^1.2.11",
47
47
  "semver": "^7.8.1"
48
48
  },
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.15.0",
4
+ "version": "0.16.1",
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"
@@ -13,7 +13,7 @@
13
13
  "extensions": {
14
14
  "org.cyberuni.universal-plugin": {
15
15
  "skills": "./skills/",
16
- "dependencies": ["cyber-agent-harness"],
16
+ "dependencies": ["cyber-agentkit"],
17
17
  "harnesses": {
18
18
  "claude-code": {},
19
19
  "cursor": {},
package/readme.md CHANGED
@@ -110,9 +110,9 @@ TOON by default; pass `--format json` for JSON.
110
110
  ### Reference documents
111
111
 
112
112
  This package ships its own documents (`plugin-design`, `slash-invocation`, `universal-plugin`) under
113
- `references/`. Read one with `npx @cyberuni/agent-harness reference show <name>` in any
113
+ `references/`. Read one with `npx cyber-agentkit reference show <name>` in any
114
114
  project that depends on `universal-plugin`, or, from a skill, with the `reference` skill in the
115
- `cyber-agent-harness` plugin.
115
+ `cyber-agentkit` plugin.
116
116
 
117
117
  ### Housekeeping
118
118
 
@@ -285,7 +285,7 @@ A plugin whose MCP server is its own npm package writes an unpinned invocation
285
285
 
286
286
  - **Always-on content is the HCF (highest common factor) of all agents.** It holds only the constraints every agent in every role must follow, such as "never commit secrets" or "use Conventional Commits". That set is small.
287
287
  - **`rules/` is for the HCF only.** It stays for backward compatibility and for simple single-agent setups where no orchestration is involved. Its scope is narrow on purpose.
288
- - **Shared guidance is a reference, not a rule.** Content that cuts across components but applies only to some roles or tasks (a design standard, a review checklist, a workflow discipline) belongs in `references/<name>.md`. Whichever agent, skill, command, hook, or MCP server needs it loads it by name on demand, through the `reference` skill in the `cyber-agent-harness` plugin.
288
+ - **Shared guidance is a reference, not a rule.** Content that cuts across components but applies only to some roles or tasks (a design standard, a review checklist, a workflow discipline) belongs in `references/<name>.md`. Whichever agent, skill, command, hook, or MCP server needs it loads it by name on demand, through the `reference` skill in the `cyber-agentkit` plugin.
289
289
 
290
290
  **Decision tree for guidance:**
291
291
  - Every agent, every role (the HCF) → merge into **AGENTS.md**. For Cursor-only delivery, use `rules/` + `commands/setup.md`.
@@ -342,6 +342,6 @@ Portability rules for skill bodies: keep each `SKILL.md` body under 6,000 chars;
342
342
 
343
343
  ## References
344
344
 
345
- Load `skill-design`, `skill-repo-structure`, and `agent-tool-output` with the `reference` skill in the `cyber-agent-harness` plugin.
345
+ Load `skill-design`, `skill-repo-structure`, and `agent-tool-output` with the `reference` skill in the `cyber-agentkit` plugin.
346
346
 
347
347
  Spec: https://github.com/cyberuni/universal-plugin/blob/main/packages/universal-plugin/.agents/spec/spec.md
@@ -1,4 +1,4 @@
1
1
  # Universal Plugin Format
2
2
 
3
3
  The rules for the universal plugin format live in `plugin-design`, which this package also ships.
4
- Load `plugin-design` with the `reference` skill in the `cyber-agent-harness` plugin.
4
+ Load `plugin-design` with the `reference` skill in the `cyber-agentkit` plugin.
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  // Compiled by `universal-plugin plugin build` from src/skill-scripts/rewrite-upx.ts. Edit the source, not this file.
3
- globalThis.__SKILL_SCRIPT__ = {"package":"universal-plugin","bin":"universal-plugin","version":"0.15.0","entry":"skills/adopt-upx/scripts/rewrite-upx.mjs","commands":{"marketplace add":"skills/marketplace/scripts/add.mjs","marketplace init":"skills/marketplace/scripts/marketplace.mjs","marketplace validate":"skills/marketplace/scripts/validate.mjs","plugin build":"skills/build-plugin/scripts/build.mjs","plugin init":"skills/init-universal-plugin/scripts/init.mjs","plugin version":"skills/version/scripts/version.mjs"}};
3
+ globalThis.__SKILL_SCRIPT__ = {"package":"universal-plugin","bin":"universal-plugin","version":"0.16.1","entry":"skills/adopt-upx/scripts/rewrite-upx.mjs","commands":{"marketplace add":"skills/marketplace/scripts/add.mjs","marketplace init":"skills/marketplace/scripts/marketplace.mjs","marketplace validate":"skills/marketplace/scripts/validate.mjs","plugin build":"skills/build-plugin/scripts/build.mjs","plugin init":"skills/init-universal-plugin/scripts/init.mjs","plugin version":"skills/version/scripts/version.mjs"}};
4
4
  import { readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
5
5
  import { join, resolve } from "node:path";
6
6
  //#region src/skill-scripts/rewrite-upx.ts
@@ -47,7 +47,7 @@ node scripts/build.mjs --root <plugin-root>
47
47
 
48
48
  Resolve `scripts/build.mjs` against this skill's own directory. It runs the CLI the project installed
49
49
  (`pnpm exec universal-plugin`), else the CLI pinned to the version this skill shipped with
50
- (`npx universal-plugin@0.15.0`), so it works in a skill that was linked or copied on its own. `--root` defaults to the working directory and
50
+ (`npx universal-plugin@0.16.1`), so it works in a skill that was linked or copied on its own. `--root` defaults to the working directory and
51
51
  must name the directory that holds the canonical `plugin.json`.
52
52
 
53
53
  The command never prompts, so it is safe to run unattended.
@@ -132,7 +132,7 @@ Validation runs before the first write, so a failed build leaves the tree untouc
132
132
  - **Expect a build to pin skill `npx` references or decide a version.** Build derives and copies
133
133
  whatever `plugin.json` carries. Stamping is the release-time `plugin version`, not this step.
134
134
  - **Expect a build to copy governances into skills.** A skill loads a reference through the
135
- `reference` skill in the `cyber-agent-harness` plugin; the build does not copy them and has no
135
+ `reference` skill in the `cyber-agentkit` plugin; the build does not copy them and has no
136
136
  `--check` flag.
137
137
 
138
138
  ## Related skills
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  // Compiled by `universal-plugin plugin build` from src/skill-scripts/run-cli.ts. Edit the source, not this file.
3
- globalThis.__SKILL_SCRIPT__ = {"package":"universal-plugin","bin":"universal-plugin","version":"0.15.0","entry":"skills/build-plugin/scripts/build.mjs","commands":{"marketplace add":"skills/marketplace/scripts/add.mjs","marketplace init":"skills/marketplace/scripts/marketplace.mjs","marketplace validate":"skills/marketplace/scripts/validate.mjs","plugin build":"skills/build-plugin/scripts/build.mjs","plugin init":"skills/init-universal-plugin/scripts/init.mjs","plugin version":"skills/version/scripts/version.mjs"}};
3
+ globalThis.__SKILL_SCRIPT__ = {"package":"universal-plugin","bin":"universal-plugin","version":"0.16.1","entry":"skills/build-plugin/scripts/build.mjs","commands":{"marketplace add":"skills/marketplace/scripts/add.mjs","marketplace init":"skills/marketplace/scripts/marketplace.mjs","marketplace validate":"skills/marketplace/scripts/validate.mjs","plugin build":"skills/build-plugin/scripts/build.mjs","plugin init":"skills/init-universal-plugin/scripts/init.mjs","plugin version":"skills/version/scripts/version.mjs"}};
4
4
  import { spawnSync } from "node:child_process";
5
5
  //#region src/compile/scripts.ts
6
6
  /** The global the header assigns the script data to. Framework-neutral: a script's own code, or the
@@ -172,7 +172,7 @@ it does not, offer to add it — ask first, because both are the user's files:
172
172
  semantic-release bot opens.
173
173
 
174
174
  Use the locked `universal-plugin` from the repository's devDependencies when it has one; otherwise
175
- `npx universal-plugin@0.15.0`, pinned. This check matters most where the release tool picks the
175
+ `npx universal-plugin@0.16.1`, pinned. This check matters most where the release tool picks the
176
176
  number, because nothing else runs between that bump and publish.
177
177
 
178
178
  ## Unreleased content
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  // Compiled by `universal-plugin plugin build` from src/skill-scripts/doctor.ts. Edit the source, not this file.
3
- globalThis.__SKILL_SCRIPT__ = {"package":"universal-plugin","bin":"universal-plugin","version":"0.15.0","entry":"skills/doctor-universal-plugin/scripts/doctor.mjs","commands":{"marketplace add":"skills/marketplace/scripts/add.mjs","marketplace init":"skills/marketplace/scripts/marketplace.mjs","marketplace validate":"skills/marketplace/scripts/validate.mjs","plugin build":"skills/build-plugin/scripts/build.mjs","plugin init":"skills/init-universal-plugin/scripts/init.mjs","plugin version":"skills/version/scripts/version.mjs"}};
3
+ globalThis.__SKILL_SCRIPT__ = {"package":"universal-plugin","bin":"universal-plugin","version":"0.16.1","entry":"skills/doctor-universal-plugin/scripts/doctor.mjs","commands":{"marketplace add":"skills/marketplace/scripts/add.mjs","marketplace init":"skills/marketplace/scripts/marketplace.mjs","marketplace validate":"skills/marketplace/scripts/validate.mjs","plugin build":"skills/build-plugin/scripts/build.mjs","plugin init":"skills/init-universal-plugin/scripts/init.mjs","plugin version":"skills/version/scripts/version.mjs"}};
4
4
  import { spawnSync } from "node:child_process";
5
5
  import * as fs from "node:fs";
6
6
  import * as path from "node:path";
@@ -39,7 +39,7 @@ This skill owns the **authoring** side: the plugin a project ships. Setting a re
39
39
  ## Prerequisites
40
40
 
41
41
  Before any work that adds or removes components, load `plugin-design` with the `reference`
42
- skill in the `cyber-agent-harness` plugin.
42
+ skill in the `cyber-agentkit` plugin.
43
43
 
44
44
  It is the authoritative source for which component to reach for and which anti-patterns to avoid.
45
45
 
@@ -244,7 +244,7 @@ This skill is not a formatter. If the project has one, run it over the written f
244
244
 
245
245
  ## References
246
246
 
247
- - Reference: `plugin-design`, loaded with the `reference` skill in the `cyber-agent-harness` plugin
247
+ - Reference: `plugin-design`, loaded with the `reference` skill in the `cyber-agentkit` plugin
248
248
  - Spec: https://github.com/cyberuni/universal-plugin/blob/main/packages/universal-plugin/.agents/spec/spec.md
249
249
  - Manifest schema (Agent Plugins Specification v1.0.0): https://agent-plugins.org/schemas/1.0.0/plugin.schema.json
250
250
  - Extension schema (the body of `extensions["org.cyberuni.universal-plugin"]`), shipped in the package at
@@ -43,7 +43,7 @@ node scripts/init.mjs --name <plugin-name> --vendor claude-code --vendor cursor
43
43
  ```
44
44
 
45
45
  Resolve `scripts/init.mjs` against this skill's directory; it runs the project's installed CLI, else
46
- the pinned `npx universal-plugin@0.15.0`. Add `--npm` when an npm package ships the plugin,
46
+ the pinned `npx universal-plugin@0.16.1`. Add `--npm` when an npm package ships the plugin,
47
47
  which also wires that `package.json`'s `files` to carry the derived manifests, copies its `version`
48
48
  into the manifest, and, in a changesets repository, wires the root `version` script to run
49
49
  `plugin version --from-package` (SKILL.md Phase 3 asks before that rewrite).
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  // Compiled by `universal-plugin plugin build` from src/skill-scripts/run-cli.ts. Edit the source, not this file.
3
- globalThis.__SKILL_SCRIPT__ = {"package":"universal-plugin","bin":"universal-plugin","version":"0.15.0","entry":"skills/init-universal-plugin/scripts/init.mjs","commands":{"marketplace add":"skills/marketplace/scripts/add.mjs","marketplace init":"skills/marketplace/scripts/marketplace.mjs","marketplace validate":"skills/marketplace/scripts/validate.mjs","plugin build":"skills/build-plugin/scripts/build.mjs","plugin init":"skills/init-universal-plugin/scripts/init.mjs","plugin version":"skills/version/scripts/version.mjs"}};
3
+ globalThis.__SKILL_SCRIPT__ = {"package":"universal-plugin","bin":"universal-plugin","version":"0.16.1","entry":"skills/init-universal-plugin/scripts/init.mjs","commands":{"marketplace add":"skills/marketplace/scripts/add.mjs","marketplace init":"skills/marketplace/scripts/marketplace.mjs","marketplace validate":"skills/marketplace/scripts/validate.mjs","plugin build":"skills/build-plugin/scripts/build.mjs","plugin init":"skills/init-universal-plugin/scripts/init.mjs","plugin version":"skills/version/scripts/version.mjs"}};
4
4
  import { spawnSync } from "node:child_process";
5
5
  //#region src/compile/scripts.ts
6
6
  /** The global the header assigns the script data to. Framework-neutral: a script's own code, or the
@@ -56,7 +56,11 @@ disk is usually `add`.
56
56
  | Cursor | `.cursor-plugin/marketplace.json` | nothing; an admin imports the repository as a team marketplace |
57
57
 
58
58
  Three of the four read `.claude-plugin/marketplace.json`, so one file covers Claude Code, Codex, and
59
- Copilot CLI. The reverse does not hold: Claude Code rejects the Codex catalog for its missing
59
+ Copilot CLI. Not every plugin in it installs everywhere: a runtime installs only the source kinds it
60
+ supports (`references/add.md` has the table), so a catalog can list plugins a runtime cannot install.
61
+ Copilot CLI refuses the whole catalog over one `npm` or `git-subdir` entry. The generator therefore
62
+ writes each runtime's own catalog with only what that runtime installs, and Copilot CLI and Cursor read
63
+ their own catalog before the Claude one, which shields them. State per runtime which plugins install. The reverse does not hold: Claude Code rejects the Codex catalog for its missing
60
64
  `owner`.
61
65
 
62
66
  ## Rules for every route
@@ -47,20 +47,32 @@ must be the full 40-character hash, which the schema requires.
47
47
 
48
48
  ## 2. Choose targets, and know what will be skipped
49
49
 
50
- Not every runtime installs from every source. Only a repository path reaches all four:
50
+ Not every runtime installs from every source. Found by running the CLIs, not from the schema:
51
51
 
52
52
  | Source | Claude Code | Codex | Copilot CLI | Cursor |
53
53
  | --- | --- | --- | --- | --- |
54
54
  | path | yes | yes | yes | yes |
55
55
  | npm | yes | yes | — | — |
56
- | github, url, git-subdir | yes | — | — | — |
56
+ | github | yes | written as `url` | yes (also with `path`) | yes; with `path`, written as `git-subdir` |
57
+ | url | yes | yes | yes | yes, `.git` URL |
58
+ | git-subdir | yes | yes | on a GitHub URL, written as `github` + `path`; otherwise — | yes, `.git` URL |
57
59
 
58
- A target that cannot resolve the source is reported `skipped` with the reason, and no file is
59
- written for it. That is deliberate: a source a runtime refuses is an install failure in someone
60
- else's terminal. Say which targets were skipped and why — do not report "added" and leave the user
61
- to discover that two catalogs have no entry.
60
+ Cursor's row is read from its shipped bundle, not run. Where the table says "written as", the command
61
+ rewrites the source for that catalog and keeps `ref` and `sha`; nothing is lost.
62
62
 
63
- With no target flags all four are selected, so the skips are the normal case for an npm package.
63
+ A target that still cannot resolve the source is reported `skipped` with the reason, naming the entry
64
+ and the runtime, and the entry is not written to that catalog. That is deliberate: Copilot CLI refuses
65
+ the **whole** catalog over one `npm` or `git-subdir` entry, and Codex drops a `github` entry without a
66
+ word. Say which targets were skipped and why — do not report "added" and leave the user to discover
67
+ that two catalogs have no entry.
68
+
69
+ Copilot CLI and Cursor also read `.claude-plugin/marketplace.json` when they have no catalog of their
70
+ own. So a skip for either one writes its catalog anyway, derived from the Claude one without the entry:
71
+ a catalog of its own is read first and shields it. Tell the user those two files appeared and why.
72
+
73
+ With no target flags all four are selected, so skips are the normal case for an npm package.
74
+ Plugins that only npm can deliver reach Copilot CLI and Cursor only if they are vendored into this
75
+ repository as a `./` path, which this command does not do.
64
76
 
65
77
  ## 3. Supply the metadata
66
78
 
@@ -127,8 +139,8 @@ own git remote — which is also how `--from` resolves one. A marketplace with n
127
139
  there is no URL to rewrite against, and a bare path would name a directory this repository does not
128
140
  have.
129
141
 
130
- Both rewritten forms are Claude Code source types, so expect Codex, Copilot CLI, and Cursor to be
131
- skipped for a subdirectory plugin.
142
+ A subdirectory plugin reaches Codex and Cursor as `git-subdir`, and Copilot CLI as `github` + `path`
143
+ when the repository is on GitHub; on another host Copilot CLI is skipped.
132
144
 
133
145
  ## 6. Validate
134
146
 
@@ -26,6 +26,11 @@ error: catalog ".claude-plugin/marketplace.json" does not match the marketplace
26
26
  | `invalid` | the runtime would refuse it; the issues say why |
27
27
  | `missing` | no catalog at that path, which is only a failure under `--required` |
28
28
 
29
+ A source the runtime cannot install is an issue too: `npm` or `git-subdir` for Copilot CLI (it refuses
30
+ the whole catalog), `github` for Codex (silently dropped), `npm` for Cursor. A runtime with no catalog
31
+ of its own is checked against `.claude-plugin/marketplace.json`, because that is the file it reads;
32
+ the fix is to run `init` or `add` so the runtime gets a catalog of its own.
33
+
29
34
  ## Fixing what it reports
30
35
 
31
36
  Fix an issue in the source it comes from, then regenerate. An entry's fields are derived, so an
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  // Compiled by `universal-plugin plugin build` from src/skill-scripts/run-cli.ts. Edit the source, not this file.
3
- globalThis.__SKILL_SCRIPT__ = {"package":"universal-plugin","bin":"universal-plugin","version":"0.15.0","entry":"skills/marketplace/scripts/add.mjs","commands":{"marketplace add":"skills/marketplace/scripts/add.mjs","marketplace init":"skills/marketplace/scripts/marketplace.mjs","marketplace validate":"skills/marketplace/scripts/validate.mjs","plugin build":"skills/build-plugin/scripts/build.mjs","plugin init":"skills/init-universal-plugin/scripts/init.mjs","plugin version":"skills/version/scripts/version.mjs"}};
3
+ globalThis.__SKILL_SCRIPT__ = {"package":"universal-plugin","bin":"universal-plugin","version":"0.16.1","entry":"skills/marketplace/scripts/add.mjs","commands":{"marketplace add":"skills/marketplace/scripts/add.mjs","marketplace init":"skills/marketplace/scripts/marketplace.mjs","marketplace validate":"skills/marketplace/scripts/validate.mjs","plugin build":"skills/build-plugin/scripts/build.mjs","plugin init":"skills/init-universal-plugin/scripts/init.mjs","plugin version":"skills/version/scripts/version.mjs"}};
4
4
  import { spawnSync } from "node:child_process";
5
5
  //#region src/compile/scripts.ts
6
6
  /** The global the header assigns the script data to. Framework-neutral: a script's own code, or the
@@ -1,9 +1,121 @@
1
1
  #!/usr/bin/env node
2
2
  // Compiled by `universal-plugin plugin build` from src/skill-scripts/install-docs.ts. Edit the source, not this file.
3
- globalThis.__SKILL_SCRIPT__ = {"package":"universal-plugin","bin":"universal-plugin","version":"0.15.0","entry":"skills/marketplace/scripts/install-docs.mjs","commands":{"marketplace add":"skills/marketplace/scripts/add.mjs","marketplace init":"skills/marketplace/scripts/marketplace.mjs","marketplace validate":"skills/marketplace/scripts/validate.mjs","plugin build":"skills/build-plugin/scripts/build.mjs","plugin init":"skills/init-universal-plugin/scripts/init.mjs","plugin version":"skills/version/scripts/version.mjs"}};
3
+ globalThis.__SKILL_SCRIPT__ = {"package":"universal-plugin","bin":"universal-plugin","version":"0.16.1","entry":"skills/marketplace/scripts/install-docs.mjs","commands":{"marketplace add":"skills/marketplace/scripts/add.mjs","marketplace init":"skills/marketplace/scripts/marketplace.mjs","marketplace validate":"skills/marketplace/scripts/validate.mjs","plugin build":"skills/build-plugin/scripts/build.mjs","plugin init":"skills/init-universal-plugin/scripts/init.mjs","plugin version":"skills/version/scripts/version.mjs"}};
4
4
  import { execFileSync } from "node:child_process";
5
5
  import * as fs from "node:fs";
6
6
  import * as path from "node:path";
7
+ //#region src/marketplace/marketplace.ts
8
+ /** Whether a catalog entry's `source` names a place inside this repository — a `./`-prefixed path in
9
+ * the Claude-shaped catalogs, or Codex's `{ source: "local", path }`. Every other form names a
10
+ * plugin distributed from somewhere else: an npm package, a GitHub repository, a URL. Discovery
11
+ * walks directories, so it can produce the first kind and can say nothing about the second. */
12
+ function isLocalCatalogSource(source) {
13
+ if (typeof source === "string") return true;
14
+ if (typeof source !== "object" || source === null || Array.isArray(source)) return false;
15
+ return source.source === "local";
16
+ }
17
+ const GITHUB_URL = /^(?:https?:\/\/(?:www\.)?github\.com\/|git@github\.com:)([^/]+\/[^/]+?)(?:\.git)?\/?$/;
18
+ /** The source forms each runtime installs from.
19
+ *
20
+ * Every runtime takes a repository path. Beyond that they diverge, and the divergence is not
21
+ * cosmetic: a catalog is read at install time in someone else's terminal, so a source a runtime
22
+ * cannot resolve is a failure far from here. Claude Code's schema documents the full tagged set
23
+ * (<https://json.schemastore.org/claude-code-marketplace.json>). The rest was found by running the
24
+ * CLIs (`.research/local-marketplaces`, "Remote entry sources"):
25
+ *
26
+ * - Codex installs `npm`, `url` and `git-subdir`, and silently drops a `github` entry.
27
+ * - Copilot CLI installs `github` (also with `path`) and `url`, and refuses the *whole*
28
+ * marketplace over a single `npm` or `git-subdir` entry.
29
+ * - Cursor's shipped bundle resolves `github`, `url` and `git-subdir`, all by git URL, and has no
30
+ * `npm` source. Read from the bundle, not run. */
31
+ const TARGET_SOURCE_KINDS = {
32
+ claude: [
33
+ "path",
34
+ "npm",
35
+ "github",
36
+ "url",
37
+ "git-subdir"
38
+ ],
39
+ codex: [
40
+ "path",
41
+ "npm",
42
+ "url",
43
+ "git-subdir"
44
+ ],
45
+ copilot: [
46
+ "path",
47
+ "github",
48
+ "url"
49
+ ],
50
+ cursor: [
51
+ "path",
52
+ "github",
53
+ "url",
54
+ "git-subdir"
55
+ ]
56
+ };
57
+ /** The kind name used to decide whether a runtime accepts a source: a repository path, or the tag. */
58
+ function sourceKindOf(source) {
59
+ if (isLocalCatalogSource(source)) return "path";
60
+ return typeof source === "string" ? "path" : source.source;
61
+ }
62
+ function pickPins(source) {
63
+ const pins = {};
64
+ if (typeof source.ref === "string") pins.ref = source.ref;
65
+ if (typeof source.sha === "string") pins.sha = source.sha;
66
+ return pins;
67
+ }
68
+ /** A git URL ending in `.git`, which Cursor requires of `url` and `git-subdir`. Only a GitHub URL is
69
+ * known to serve the same repository with the suffix, so any other host's URL is left as written. */
70
+ function withGitSuffix(url) {
71
+ if (/\.git$/.test(url)) return url;
72
+ return GITHUB_URL.test(url) ? `${url.replace(/\/+$/, "")}.git` : void 0;
73
+ }
74
+ /** Turns a source into the closest one a runtime installs, or says why none exists.
75
+ *
76
+ * A mapping is made only where it is lossless: `github` becomes a `url` (or, with a `path`, a
77
+ * `git-subdir`) on Codex, and a `git-subdir` on a GitHub URL becomes `github` + `path` on Copilot
78
+ * CLI. `ref` and `sha` pins cross with it. Anything else the runtime cannot install is omitted,
79
+ * because writing it would have the runtime drop the entry or refuse the whole catalog. */
80
+ function adaptSource(target, source) {
81
+ const kind = sourceKindOf(source);
82
+ if (kind === "path" || typeof source === "string") return { source };
83
+ const supported = TARGET_SOURCE_KINDS[target].includes(kind);
84
+ if ((target === "codex" || target === "cursor" && typeof source.path === "string") && kind === "github" && typeof source.repo === "string") {
85
+ const url = `https://github.com/${source.repo}.git`;
86
+ return typeof source.path === "string" ? { source: {
87
+ source: "git-subdir",
88
+ url,
89
+ path: source.path,
90
+ ...pickPins(source)
91
+ } } : { source: {
92
+ source: "url",
93
+ url,
94
+ ...pickPins(source)
95
+ } };
96
+ }
97
+ if (target === "copilot" && kind === "git-subdir" && typeof source.url === "string" && typeof source.path === "string") {
98
+ const match = GITHUB_URL.exec(source.url.trim());
99
+ if (match) return { source: {
100
+ source: "github",
101
+ repo: match[1],
102
+ path: source.path,
103
+ ...pickPins(source)
104
+ } };
105
+ return { omitted: "git-subdir is installable only from a GitHub URL, which is mapped to github with a path" };
106
+ }
107
+ if (!supported) return { omitted: `${kind} source is not supported by ${target}` };
108
+ if (target === "cursor" && (kind === "url" || kind === "git-subdir") && typeof source.url === "string") {
109
+ const url = withGitSuffix(source.url);
110
+ if (url === void 0) return { omitted: `cursor requires a ".git" URL for a ${kind} source` };
111
+ return { source: url === source.url ? source : {
112
+ ...source,
113
+ url
114
+ } };
115
+ }
116
+ return { source };
117
+ }
118
+ //#endregion
7
119
  //#region src/skill-scripts/install-docs.ts
8
120
  const argv = process.argv.slice(2);
9
121
  const rootFlag = argv.indexOf("--root");
@@ -50,6 +162,11 @@ const cursor = readJson(path.join(root, ".cursor-plugin/marketplace.json"));
50
162
  const targets = [];
51
163
  const sections = [];
52
164
  const pluginNames = (catalog) => (catalog?.plugins ?? []).map((p) => p.name).filter(Boolean);
165
+ const installableNames = (target, catalog) => (catalog?.plugins ?? []).filter((p) => p?.name && p.source !== void 0 && !("omitted" in adaptSource(target, p.source))).map((p) => p.name);
166
+ const notInstallable = (installed, label) => {
167
+ const missing = pluginNames(claude).filter((name) => !installed.includes(name));
168
+ return missing.length === 0 ? "" : `\n\nNot installable on ${label}: ${missing.join(", ")}.`;
169
+ };
53
170
  if (claude) {
54
171
  const names = pluginNames(claude);
55
172
  const installs = (names.length > 0 ? names : ["<plugin>"]).map((n) => `/plugin install ${n}@${claude.name}`);
@@ -64,20 +181,20 @@ if (claude) {
64
181
  ].join("\n"));
65
182
  }
66
183
  if (copilot) {
67
- const names = pluginNames(copilot);
184
+ const names = installableNames("copilot", copilot);
68
185
  const installs = (names.length > 0 ? names : ["<plugin>"]).map((n) => `copilot plugin install ${n}@${copilot.name}`);
69
186
  targets.push("copilot-cli");
70
- sections.push([
187
+ sections.push([[
71
188
  "**GitHub Copilot CLI**",
72
189
  "",
73
190
  "```",
74
191
  `copilot plugin marketplace add ${repoRef}`,
75
192
  ...installs,
76
193
  "```"
77
- ].join("\n"));
194
+ ].join("\n"), notInstallable(names, "Copilot CLI")].join(""));
78
195
  }
79
196
  if (codex) {
80
- const names = pluginNames(codex);
197
+ const names = installableNames("codex", codex);
81
198
  const installs = (names.length > 0 ? names : ["<plugin>"]).map((n) => `codex plugin add ${n}@${codex.name}`);
82
199
  targets.push("codex");
83
200
  sections.push([
@@ -89,7 +206,7 @@ if (codex) {
89
206
  "```",
90
207
  "",
91
208
  "Start a new session before using the plugin."
92
- ].join("\n"));
209
+ ].join("\n").concat(notInstallable(names, "Codex")));
93
210
  }
94
211
  if (cursor) {
95
212
  targets.push("cursor");
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  // Compiled by `universal-plugin plugin build` from src/skill-scripts/run-cli.ts. Edit the source, not this file.
3
- globalThis.__SKILL_SCRIPT__ = {"package":"universal-plugin","bin":"universal-plugin","version":"0.15.0","entry":"skills/marketplace/scripts/marketplace.mjs","commands":{"marketplace add":"skills/marketplace/scripts/add.mjs","marketplace init":"skills/marketplace/scripts/marketplace.mjs","marketplace validate":"skills/marketplace/scripts/validate.mjs","plugin build":"skills/build-plugin/scripts/build.mjs","plugin init":"skills/init-universal-plugin/scripts/init.mjs","plugin version":"skills/version/scripts/version.mjs"}};
3
+ globalThis.__SKILL_SCRIPT__ = {"package":"universal-plugin","bin":"universal-plugin","version":"0.16.1","entry":"skills/marketplace/scripts/marketplace.mjs","commands":{"marketplace add":"skills/marketplace/scripts/add.mjs","marketplace init":"skills/marketplace/scripts/marketplace.mjs","marketplace validate":"skills/marketplace/scripts/validate.mjs","plugin build":"skills/build-plugin/scripts/build.mjs","plugin init":"skills/init-universal-plugin/scripts/init.mjs","plugin version":"skills/version/scripts/version.mjs"}};
4
4
  import { spawnSync } from "node:child_process";
5
5
  //#region src/compile/scripts.ts
6
6
  /** The global the header assigns the script data to. Framework-neutral: a script's own code, or the
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  // Compiled by `universal-plugin plugin build` from src/skill-scripts/run-cli.ts. Edit the source, not this file.
3
- globalThis.__SKILL_SCRIPT__ = {"package":"universal-plugin","bin":"universal-plugin","version":"0.15.0","entry":"skills/marketplace/scripts/validate.mjs","commands":{"marketplace add":"skills/marketplace/scripts/add.mjs","marketplace init":"skills/marketplace/scripts/marketplace.mjs","marketplace validate":"skills/marketplace/scripts/validate.mjs","plugin build":"skills/build-plugin/scripts/build.mjs","plugin init":"skills/init-universal-plugin/scripts/init.mjs","plugin version":"skills/version/scripts/version.mjs"}};
3
+ globalThis.__SKILL_SCRIPT__ = {"package":"universal-plugin","bin":"universal-plugin","version":"0.16.1","entry":"skills/marketplace/scripts/validate.mjs","commands":{"marketplace add":"skills/marketplace/scripts/add.mjs","marketplace init":"skills/marketplace/scripts/marketplace.mjs","marketplace validate":"skills/marketplace/scripts/validate.mjs","plugin build":"skills/build-plugin/scripts/build.mjs","plugin init":"skills/init-universal-plugin/scripts/init.mjs","plugin version":"skills/version/scripts/version.mjs"}};
4
4
  import { spawnSync } from "node:child_process";
5
5
  //#region src/compile/scripts.ts
6
6
  /** The global the header assigns the script data to. Framework-neutral: a script's own code, or the
@@ -39,8 +39,8 @@
39
39
  },
40
40
  {
41
41
  "id": 7,
42
- "prompt": "Publish cyber-asana (in ~/code/cyber-asana, a monorepo with the plugin at packages/cyber-asana, released as tag cyber-asana@1.2.0) to anthropics/claude-plugins-official. Its plugin.json declares the plain dependency \"cyber-agent-harness\".",
43
- "expected_output": "Before preparing a PR, runs `gh repo view anthropics/claude-plugins-official --json viewerPermission`, sees READ, then reads the target's .github/workflows/ for a workflow that closes outside PRs and its README's Contributing section. Concludes the catalog is curated (outside PRs are closed automatically; new plugins go through a submission form), so it does not clone, fork, branch, push, or open a PR. Runs pre-flight, resolves cyber-agent-harness against the target's live catalog and tells the user it will not resolve there, and drafts the entry in the shape the catalog's existing entries use (e.g. a git-subdir source pinned with the release tag as ref and that tag's commit as sha). Hands the user the draft and the submission form link the target names. Does not rely on remembered facts about that catalog.",
42
+ "prompt": "Publish cyber-asana (in ~/code/cyber-asana, a monorepo with the plugin at packages/cyber-asana, released as tag cyber-asana@1.2.0) to anthropics/claude-plugins-official. Its plugin.json declares the plain dependency \"cyber-agentkit\".",
43
+ "expected_output": "Before preparing a PR, runs `gh repo view anthropics/claude-plugins-official --json viewerPermission`, sees READ, then reads the target's .github/workflows/ for a workflow that closes outside PRs and its README's Contributing section. Concludes the catalog is curated (outside PRs are closed automatically; new plugins go through a submission form), so it does not clone, fork, branch, push, or open a PR. Runs pre-flight, resolves cyber-agentkit against the target's live catalog and tells the user it will not resolve there, and drafts the entry in the shape the catalog's existing entries use (e.g. a git-subdir source pinned with the release tag as ref and that tag's commit as sha). Hands the user the draft and the submission form link the target names. Does not rely on remembered facts about that catalog.",
44
44
  "files": []
45
45
  }
46
46
  ]
@@ -74,7 +74,7 @@ node scripts/version.mjs --from-package
74
74
  ```
75
75
 
76
76
  Resolve that path against this skill's own directory; it runs the project's installed CLI
77
- (`pnpm exec universal-plugin`), else `npx universal-plugin@0.15.0` pinned to this skill's version.
77
+ (`pnpm exec universal-plugin`), else `npx universal-plugin@0.16.1` pinned to this skill's version.
78
78
  `universal-plugin plugin version` on the `PATH` is the fallback. The command never prompts, so it is safe to run unattended once the
79
79
  source is decided.
80
80
 
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  // Compiled by `universal-plugin plugin build` from src/skill-scripts/run-cli.ts. Edit the source, not this file.
3
- globalThis.__SKILL_SCRIPT__ = {"package":"universal-plugin","bin":"universal-plugin","version":"0.15.0","entry":"skills/version/scripts/version.mjs","commands":{"marketplace add":"skills/marketplace/scripts/add.mjs","marketplace init":"skills/marketplace/scripts/marketplace.mjs","marketplace validate":"skills/marketplace/scripts/validate.mjs","plugin build":"skills/build-plugin/scripts/build.mjs","plugin init":"skills/init-universal-plugin/scripts/init.mjs","plugin version":"skills/version/scripts/version.mjs"}};
3
+ globalThis.__SKILL_SCRIPT__ = {"package":"universal-plugin","bin":"universal-plugin","version":"0.16.1","entry":"skills/version/scripts/version.mjs","commands":{"marketplace add":"skills/marketplace/scripts/add.mjs","marketplace init":"skills/marketplace/scripts/marketplace.mjs","marketplace validate":"skills/marketplace/scripts/validate.mjs","plugin build":"skills/build-plugin/scripts/build.mjs","plugin init":"skills/init-universal-plugin/scripts/init.mjs","plugin version":"skills/version/scripts/version.mjs"}};
4
4
  import { spawnSync } from "node:child_process";
5
5
  //#region src/compile/scripts.ts
6
6
  /** The global the header assigns the script data to. Framework-neutral: a script's own code, or the