universal-plugin 0.10.0 → 0.11.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.10.0",
3
+ "version": "0.11.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.10.0",
3
+ "version": "0.11.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.10.0",
3
+ "version": "0.11.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
@@ -4,8 +4,10 @@ import { fileURLToPath } from "node:url";
4
4
  import * as childProcess from "node:child_process";
5
5
  import { execFileSync } from "node:child_process";
6
6
  import * as path from "node:path";
7
+ import { join, win32 } from "node:path";
7
8
  import * as fs from "node:fs";
8
9
  import * as os from "node:os";
10
+ import { homedir } from "node:os";
9
11
  //#region \0rolldown/runtime.js
10
12
  var __create = Object.create;
11
13
  var __defProp = Object.defineProperty;
@@ -24,11 +26,11 @@ var __copyProps = (to, from, except, desc) => {
24
26
  }
25
27
  return to;
26
28
  };
27
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
29
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
28
30
  value: mod,
29
31
  enumerable: true
30
32
  }) : target, mod));
31
- var __require = /* @__PURE__ */ createRequire(import.meta.url);
33
+ var __require = /* #__PURE__ */ (() => createRequire(import.meta.url))();
32
34
  //#endregion
33
35
  //#region ../../node_modules/.pnpm/commander@14.0.3/node_modules/commander/lib/error.js
34
36
  var require_error = /* @__PURE__ */ __commonJSMin(((exports) => {
@@ -100,7 +102,6 @@ var require_argument = /* @__PURE__ */ __commonJSMin(((exports) => {
100
102
  default:
101
103
  this.required = true;
102
104
  this._name = name;
103
- break;
104
105
  }
105
106
  if (this._name.endsWith("...")) {
106
107
  this.variadic = true;
@@ -1595,16 +1596,20 @@ Expecting one of '${allowedValues.join("', '")}'`);
1595
1596
  const oldValue = this.getOptionValue(name);
1596
1597
  if (val !== null && option.parseArg) val = this._callParseArg(option, val, oldValue, invalidValueMessage);
1597
1598
  else if (val !== null && option.variadic) val = option._collectValue(val, oldValue);
1598
- if (val == null) if (option.negate) val = false;
1599
- else if (option.isBoolean() || option.optional) val = true;
1600
- else val = "";
1599
+ if (val == null) {
1600
+ if (option.negate) val = false;
1601
+ else if (option.isBoolean() || option.optional) val = true;
1602
+ else val = "";
1603
+ }
1601
1604
  this.setOptionValueWithSource(name, val, valueSource);
1602
1605
  };
1603
1606
  this.on("option:" + oname, (val) => {
1604
- handleOptionValue(val, `error: option '${option.flags}' argument '${val}' is invalid.`, "cli");
1607
+ const invalidValueMessage = `error: option '${option.flags}' argument '${val}' is invalid.`;
1608
+ handleOptionValue(val, invalidValueMessage, "cli");
1605
1609
  });
1606
1610
  if (option.envVar) this.on("optionEnv:" + oname, (val) => {
1607
- handleOptionValue(val, `error: option '${option.flags}' value '${val}' from env '${option.envVar}' is invalid.`, "env");
1611
+ const invalidValueMessage = `error: option '${option.flags}' value '${val}' from env '${option.envVar}' is invalid.`;
1612
+ handleOptionValue(val, invalidValueMessage, "env");
1608
1613
  });
1609
1614
  return this;
1610
1615
  }
@@ -1995,12 +2000,13 @@ Expecting one of '${allowedValues.join("', '")}'`);
1995
2000
  }
1996
2001
  launchWithNode = sourceExt.includes(path$1.extname(executableFile));
1997
2002
  let proc;
1998
- if (process$1.platform !== "win32") if (launchWithNode) {
1999
- args.unshift(executableFile);
2000
- args = incrementNodeInspectorPort(process$1.execArgv).concat(args);
2001
- proc = childProcess$1.spawn(process$1.argv[0], args, { stdio: "inherit" });
2002
- } else proc = childProcess$1.spawn(executableFile, args, { stdio: "inherit" });
2003
- else {
2003
+ if (process$1.platform !== "win32") {
2004
+ if (launchWithNode) {
2005
+ args.unshift(executableFile);
2006
+ args = incrementNodeInspectorPort(process$1.execArgv).concat(args);
2007
+ proc = childProcess$1.spawn(process$1.argv[0], args, { stdio: "inherit" });
2008
+ } else proc = childProcess$1.spawn(executableFile, args, { stdio: "inherit" });
2009
+ } else {
2004
2010
  this._checkForMissingExecutable(executableFile, executableDir, subcommand._name);
2005
2011
  args.unshift(executableFile);
2006
2012
  args = incrementNodeInspectorPort(process$1.execArgv).concat(args);
@@ -2444,8 +2450,10 @@ Expecting one of '${allowedValues.join("', '")}'`);
2444
2450
  "default",
2445
2451
  "config",
2446
2452
  "env"
2447
- ].includes(this.getOptionValueSource(optionKey))) if (option.required || option.optional) this.emit(`optionEnv:${option.name()}`, process$1.env[option.envVar]);
2448
- else this.emit(`optionEnv:${option.name()}`);
2453
+ ].includes(this.getOptionValueSource(optionKey))) {
2454
+ if (option.required || option.optional) this.emit(`optionEnv:${option.name()}`, process$1.env[option.envVar]);
2455
+ else this.emit(`optionEnv:${option.name()}`);
2456
+ }
2449
2457
  }
2450
2458
  });
2451
2459
  }
@@ -3069,7 +3077,7 @@ function resolveOwnVersion(packageJsonPath, deps = {}) {
3069
3077
  }
3070
3078
  //#endregion
3071
3079
  //#region src/state/state.ts
3072
- const KNOWN_ACTION_TYPES = new Set([
3080
+ const KNOWN_ACTION_TYPES = /* @__PURE__ */ new Set([
3073
3081
  "install",
3074
3082
  "upgrade",
3075
3083
  "remove"
@@ -3782,14 +3790,12 @@ function output(data, view) {
3782
3790
  //#endregion
3783
3791
  //#region ../../node_modules/.pnpm/semver@7.8.1/node_modules/semver/internal/constants.js
3784
3792
  var require_constants = /* @__PURE__ */ __commonJSMin(((exports, module) => {
3785
- const SEMVER_SPEC_VERSION = "2.0.0";
3786
- const MAX_LENGTH = 256;
3787
- const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER || 9007199254740991;
3788
3793
  module.exports = {
3789
- MAX_LENGTH,
3794
+ MAX_LENGTH: 256,
3790
3795
  MAX_SAFE_COMPONENT_LENGTH: 16,
3791
- MAX_SAFE_BUILD_LENGTH: MAX_LENGTH - 6,
3792
- MAX_SAFE_INTEGER,
3796
+ MAX_SAFE_BUILD_LENGTH: 250,
3797
+ MAX_SAFE_INTEGER: Number.MAX_SAFE_INTEGER ||
3798
+ /* istanbul ignore next */ 9007199254740991,
3793
3799
  RELEASE_TYPES: [
3794
3800
  "major",
3795
3801
  "premajor",
@@ -3799,7 +3805,7 @@ var require_constants = /* @__PURE__ */ __commonJSMin(((exports, module) => {
3799
3805
  "prepatch",
3800
3806
  "prerelease"
3801
3807
  ],
3802
- SEMVER_SPEC_VERSION,
3808
+ SEMVER_SPEC_VERSION: "2.0.0",
3803
3809
  FLAG_INCLUDE_PRERELEASE: 1,
3804
3810
  FLAG_LOOSE: 2
3805
3811
  };
@@ -3931,9 +3937,10 @@ var require_semver$1 = /* @__PURE__ */ __commonJSMin(((exports, module) => {
3931
3937
  module.exports = class SemVer {
3932
3938
  constructor(version, options) {
3933
3939
  options = parseOptions(options);
3934
- if (version instanceof SemVer) if (version.loose === !!options.loose && version.includePrerelease === !!options.includePrerelease) return version;
3935
- else version = version.version;
3936
- else if (typeof version !== "string") throw new TypeError(`Invalid version. Must be a string. Got type "${typeof version}".`);
3940
+ if (version instanceof SemVer) {
3941
+ if (version.loose === !!options.loose && version.includePrerelease === !!options.includePrerelease) return version;
3942
+ else version = version.version;
3943
+ } else if (typeof version !== "string") throw new TypeError(`Invalid version. Must be a string. Got type "${typeof version}".`);
3937
3944
  if (version.length > MAX_LENGTH) throw new TypeError(`version is longer than ${MAX_LENGTH} characters`);
3938
3945
  debug("SemVer", version, options);
3939
3946
  this.options = options;
@@ -4354,7 +4361,11 @@ var require_coerce = /* @__PURE__ */ __commonJSMin(((exports, module) => {
4354
4361
  }
4355
4362
  if (match === null) return null;
4356
4363
  const major = match[2];
4357
- return parse(`${major}.${match[3] || "0"}.${match[4] || "0"}${options.includePrerelease && match[5] ? `-${match[5]}` : ""}${options.includePrerelease && match[6] ? `+${match[6]}` : ""}`, options);
4364
+ const minor = match[3] || "0";
4365
+ const patch = match[4] || "0";
4366
+ const prerelease = options.includePrerelease && match[5] ? `-${match[5]}` : "";
4367
+ const build = options.includePrerelease && match[6] ? `+${match[6]}` : "";
4368
+ return parse(`${major}.${minor}.${patch}${prerelease}${build}`, options);
4358
4369
  };
4359
4370
  module.exports = coerce;
4360
4371
  }));
@@ -4370,7 +4381,8 @@ var require_truncate = /* @__PURE__ */ __commonJSMin(((exports, module) => {
4370
4381
  return clonedVersion && doTruncation(clonedVersion, truncation);
4371
4382
  };
4372
4383
  const cloneInputVersion = (version, options) => {
4373
- return parse(version instanceof SemVer ? version.version : version, options);
4384
+ const versionStringToParse = version instanceof SemVer ? version.version : version;
4385
+ return parse(versionStringToParse, options);
4374
4386
  };
4375
4387
  const doTruncation = (version, truncation) => {
4376
4388
  if (isPrerelease(truncation)) return version.version;
@@ -4380,9 +4392,7 @@ var require_truncate = /* @__PURE__ */ __commonJSMin(((exports, module) => {
4380
4392
  version.minor = 0;
4381
4393
  version.patch = 0;
4382
4394
  break;
4383
- case "minor":
4384
- version.patch = 0;
4385
- break;
4395
+ case "minor": version.patch = 0;
4386
4396
  }
4387
4397
  return version.format();
4388
4398
  };
@@ -4431,8 +4441,10 @@ var require_range = /* @__PURE__ */ __commonJSMin(((exports, module) => {
4431
4441
  module.exports = class Range {
4432
4442
  constructor(range, options) {
4433
4443
  options = parseOptions(options);
4434
- if (range instanceof Range) if (range.loose === !!options.loose && range.includePrerelease === !!options.includePrerelease) return range;
4435
- else return new Range(range.raw, options);
4444
+ if (range instanceof Range) {
4445
+ if (range.loose === !!options.loose && range.includePrerelease === !!options.includePrerelease) return range;
4446
+ else return new Range(range.raw, options);
4447
+ }
4436
4448
  if (range instanceof Comparator) {
4437
4449
  this.raw = range.value;
4438
4450
  this.set = [[range]];
@@ -4600,18 +4612,21 @@ var require_range = /* @__PURE__ */ __commonJSMin(((exports, module) => {
4600
4612
  let ret;
4601
4613
  if (isX(M)) ret = "";
4602
4614
  else if (isX(m)) ret = `>=${M}.0.0${z} <${+M + 1}.0.0-0`;
4603
- else if (isX(p)) if (M === "0") ret = `>=${M}.${m}.0${z} <${M}.${+m + 1}.0-0`;
4604
- else ret = `>=${M}.${m}.0${z} <${+M + 1}.0.0-0`;
4605
- else if (pr) {
4615
+ else if (isX(p)) {
4616
+ if (M === "0") ret = `>=${M}.${m}.0${z} <${M}.${+m + 1}.0-0`;
4617
+ else ret = `>=${M}.${m}.0${z} <${+M + 1}.0.0-0`;
4618
+ } else if (pr) {
4606
4619
  debug("replaceCaret pr", pr);
4607
- if (M === "0") if (m === "0") ret = `>=${M}.${m}.${p}-${pr} <${M}.${m}.${+p + 1}-0`;
4608
- else ret = `>=${M}.${m}.${p}-${pr} <${M}.${+m + 1}.0-0`;
4609
- else ret = `>=${M}.${m}.${p}-${pr} <${+M + 1}.0.0-0`;
4620
+ if (M === "0") {
4621
+ if (m === "0") ret = `>=${M}.${m}.${p}-${pr} <${M}.${m}.${+p + 1}-0`;
4622
+ else ret = `>=${M}.${m}.${p}-${pr} <${M}.${+m + 1}.0-0`;
4623
+ } else ret = `>=${M}.${m}.${p}-${pr} <${+M + 1}.0.0-0`;
4610
4624
  } else {
4611
4625
  debug("no pr");
4612
- if (M === "0") if (m === "0") ret = `>=${M}.${m}.${p}${z} <${M}.${m}.${+p + 1}-0`;
4613
- else ret = `>=${M}.${m}.${p}${z} <${M}.${+m + 1}.0-0`;
4614
- else ret = `>=${M}.${m}.${p} <${+M + 1}.0.0-0`;
4626
+ if (M === "0") {
4627
+ if (m === "0") ret = `>=${M}.${m}.${p}${z} <${M}.${m}.${+p + 1}-0`;
4628
+ else ret = `>=${M}.${m}.${p}${z} <${M}.${+m + 1}.0-0`;
4629
+ } else ret = `>=${M}.${m}.${p} <${+M + 1}.0.0-0`;
4615
4630
  }
4616
4631
  debug("caret return", ret);
4617
4632
  return ret;
@@ -4632,9 +4647,10 @@ var require_range = /* @__PURE__ */ __commonJSMin(((exports, module) => {
4632
4647
  const anyX = xp;
4633
4648
  if (gtlt === "=" && anyX) gtlt = "";
4634
4649
  pr = options.includePrerelease ? "-0" : "";
4635
- if (xM) if (gtlt === ">" || gtlt === "<") ret = "<0.0.0-0";
4636
- else ret = "*";
4637
- else if (gtlt && anyX) {
4650
+ if (xM) {
4651
+ if (gtlt === ">" || gtlt === "<") ret = "<0.0.0-0";
4652
+ else ret = "*";
4653
+ } else if (gtlt && anyX) {
4638
4654
  if (xm) m = 0;
4639
4655
  p = 0;
4640
4656
  if (gtlt === ">") {
@@ -4708,8 +4724,10 @@ var require_comparator = /* @__PURE__ */ __commonJSMin(((exports, module) => {
4708
4724
  }
4709
4725
  constructor(comp, options) {
4710
4726
  options = parseOptions(options);
4711
- if (comp instanceof Comparator) if (comp.loose === !!options.loose) return comp;
4712
- else comp = comp.value;
4727
+ if (comp instanceof Comparator) {
4728
+ if (comp.loose === !!options.loose) return comp;
4729
+ else comp = comp.value;
4730
+ }
4713
4731
  comp = comp.trim().split(/\s+/).join(" ");
4714
4732
  debug("comparator", comp, options);
4715
4733
  this.options = options;
@@ -5029,11 +5047,15 @@ var require_subset = /* @__PURE__ */ __commonJSMin(((exports, module) => {
5029
5047
  const minimumVersion = [new Comparator(">=0.0.0")];
5030
5048
  const simpleSubset = (sub, dom, options) => {
5031
5049
  if (sub === dom) return true;
5032
- if (sub.length === 1 && sub[0].semver === ANY) if (dom.length === 1 && dom[0].semver === ANY) return true;
5033
- else if (options.includePrerelease) sub = minimumVersionWithPreRelease;
5034
- else sub = minimumVersion;
5035
- if (dom.length === 1 && dom[0].semver === ANY) if (options.includePrerelease) return true;
5036
- else dom = minimumVersion;
5050
+ if (sub.length === 1 && sub[0].semver === ANY) {
5051
+ if (dom.length === 1 && dom[0].semver === ANY) return true;
5052
+ else if (options.includePrerelease) sub = minimumVersionWithPreRelease;
5053
+ else sub = minimumVersion;
5054
+ }
5055
+ if (dom.length === 1 && dom[0].semver === ANY) {
5056
+ if (options.includePrerelease) return true;
5057
+ else dom = minimumVersion;
5058
+ }
5037
5059
  const eqSet = /* @__PURE__ */ new Set();
5038
5060
  let gt, lt;
5039
5061
  for (const c of sub) if (c.operator === ">" || c.operator === ">=") gt = higherGT(gt, c, options);
@@ -5167,7 +5189,7 @@ const VENDOR_READS_DEPENDENCIES = {
5167
5189
  };
5168
5190
  /** Vendors the canonical root manifest serves as-is: nothing is derived for them, so a declaration
5169
5191
  * they cannot read is ignored at runtime rather than dropped from a file. */
5170
- const CANONICAL_SERVED$1 = new Set(["copilot-cli"]);
5192
+ const CANONICAL_SERVED$1 = /* @__PURE__ */ new Set(["copilot-cli"]);
5171
5193
  /** What Claude Code accepts as a dependency string: a plugin name, an optional `@marketplace`, and an
5172
5194
  * optional `@^…` range tail that the runtime strips before resolving. */
5173
5195
  const DEPENDENCY_STRING = /^[A-Za-z0-9][-A-Za-z0-9._]*(@[A-Za-z0-9][-A-Za-z0-9._]*)?(@\^[^@]*)?$/;
@@ -6230,12 +6252,12 @@ const STRING_FIELDS = [
6230
6252
  "repository",
6231
6253
  "license"
6232
6254
  ];
6233
- const AUTHOR_FIELDS = new Set([
6255
+ const AUTHOR_FIELDS = /* @__PURE__ */ new Set([
6234
6256
  "name",
6235
6257
  "email",
6236
6258
  "url"
6237
6259
  ]);
6238
- const TOP_LEVEL_FIELDS = new Set([
6260
+ const TOP_LEVEL_FIELDS = /* @__PURE__ */ new Set([
6239
6261
  "$schema",
6240
6262
  "name",
6241
6263
  ...STRING_FIELDS,
@@ -6256,7 +6278,7 @@ function validateCanonicalManifest(manifest, opts) {
6256
6278
  const uext = isObject(manifest.extensions) ? manifest.extensions[UP_NAMESPACE$2] : void 0;
6257
6279
  const extension = isObject(uext) ? uext : {};
6258
6280
  const harnesses = isObject(extension.harnesses) ? extension.harnesses : {};
6259
- const declared = [...new Set([...Array.isArray(extension.vendors) ? extension.vendors.filter((v) => typeof v === "string") : [], ...Object.keys(harnesses)])];
6281
+ const declared = [.../* @__PURE__ */ new Set([...Array.isArray(extension.vendors) ? extension.vendors.filter((v) => typeof v === "string") : [], ...Object.keys(harnesses)])];
6260
6282
  for (const vendor of declared.filter((v) => !opts.knownVendors.has(v))) {
6261
6283
  const message = `Unknown vendor "${vendor}" in harnesses`;
6262
6284
  warnings.push(message);
@@ -6313,18 +6335,22 @@ function schemaRuleViolations(manifest, warnings) {
6313
6335
  else for (const [key, value] of Object.entries(manifest.author)) if (!AUTHOR_FIELDS.has(key)) push(`author.${key}`, "additionalProperties", "unknown author field");
6314
6336
  else if (typeof value !== "string") push(`author.${key}`, "type", "must be a string");
6315
6337
  }
6316
- if (manifest.keywords !== void 0) if (!Array.isArray(manifest.keywords)) push("keywords", "type", "must be an array of strings");
6317
- else manifest.keywords.forEach((keyword, index) => {
6318
- if (typeof keyword !== "string") push(`keywords[${index}]`, "type", "must be a string");
6319
- });
6320
- if (manifest.extensions !== void 0) if (!isObject(manifest.extensions)) push("extensions", "type", "must be an object keyed by namespace");
6321
- else {
6322
- for (const [namespace, value] of Object.entries(manifest.extensions)) if (!isObject(value)) push(`extensions.${namespace}`, "type", "must be an object");
6323
- const uext = manifest.extensions[UP_NAMESPACE$2];
6324
- if (isObject(uext)) {
6325
- const dependencies = validateDependencies(uext.dependencies);
6326
- for (const error of dependencies.errors) push(`extensions.${UP_NAMESPACE$2}.dependencies`, "dependencies", error);
6327
- warnings.push(...dependencies.warnings);
6338
+ if (manifest.keywords !== void 0) {
6339
+ if (!Array.isArray(manifest.keywords)) push("keywords", "type", "must be an array of strings");
6340
+ else manifest.keywords.forEach((keyword, index) => {
6341
+ if (typeof keyword !== "string") push(`keywords[${index}]`, "type", "must be a string");
6342
+ });
6343
+ }
6344
+ if (manifest.extensions !== void 0) {
6345
+ if (!isObject(manifest.extensions)) push("extensions", "type", "must be an object keyed by namespace");
6346
+ else {
6347
+ for (const [namespace, value] of Object.entries(manifest.extensions)) if (!isObject(value)) push(`extensions.${namespace}`, "type", "must be an object");
6348
+ const uext = manifest.extensions[UP_NAMESPACE$2];
6349
+ if (isObject(uext)) {
6350
+ const dependencies = validateDependencies(uext.dependencies);
6351
+ for (const error of dependencies.errors) push(`extensions.${UP_NAMESPACE$2}.dependencies`, "dependencies", error);
6352
+ warnings.push(...dependencies.warnings);
6353
+ }
6328
6354
  }
6329
6355
  }
6330
6356
  for (const key of Object.keys(manifest)) if (!TOP_LEVEL_FIELDS.has(key)) push(key, "additionalProperties", "not an Agent Plugins 1.0.0 field; move tool-specific data under extensions");
@@ -6344,7 +6370,7 @@ const KNOWN_VENDORS$1 = new Set(Object.keys(VENDOR_OUTPUT));
6344
6370
  /** Vendors the canonical root manifest serves as-is. The build derives no *manifest* for these —
6345
6371
  * writing one would either be shadowed by root (a lower-precedence path) or clobber root itself.
6346
6372
  * Their components are a separate question: see COPILOT_NAMESPACE. */
6347
- const CANONICAL_SERVED = new Set(["copilot-cli"]);
6373
+ const CANONICAL_SERVED = /* @__PURE__ */ new Set(["copilot-cli"]);
6348
6374
  /** The reverse-domain directory Copilot CLI reads its native components from once a plugin declares
6349
6375
  * the canonical `$schema` (ADR-0015). It REPLACES the plugin root for these kinds — a spec-mode
6350
6376
  * runtime does not read `agents/` at all — so the build derives the tree rather than relying on the
@@ -6504,11 +6530,15 @@ function buildPlugin(root, opts = {}) {
6504
6530
  }
6505
6531
  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`);
6506
6532
  const derivedHooksPath = path.join(outputDir, "hooks.json");
6507
- if (hooks?.changed) if (hooks.hooks) vendorManifest["hooks"] = `./${path.dirname(relPath).split(path.sep).join("/")}/hooks.json`;
6508
- else delete vendorManifest["hooks"];
6533
+ if (hooks?.changed) {
6534
+ if (hooks.hooks) vendorManifest["hooks"] = `./${path.dirname(relPath).split(path.sep).join("/")}/hooks.json`;
6535
+ else delete vendorManifest["hooks"];
6536
+ }
6509
6537
  const derivedMcpPath = path.join(outputDir, "mcp.json");
6510
- if (mcp?.changed && declaredMcp) if (declaredMcp.inline) vendorManifest["mcpServers"] = mcp.servers;
6511
- else vendorManifest["mcpServers"] = `./${path.dirname(relPath).split(path.sep).join("/")}/mcp.json`;
6538
+ if (mcp?.changed && declaredMcp) {
6539
+ if (declaredMcp.inline) vendorManifest["mcpServers"] = mcp.servers;
6540
+ else vendorManifest["mcpServers"] = `./${path.dirname(relPath).split(path.sep).join("/")}/mcp.json`;
6541
+ }
6512
6542
  if (opts.verbose) {
6513
6543
  console.log(`[${vendor}] → ${outputPath}`);
6514
6544
  for (const key of Object.keys(vendorFields)) console.log(` + ${key} (from harnesses.${vendor})`);
@@ -6946,7 +6976,7 @@ function buildCommand() {
6946
6976
  }
6947
6977
  //#endregion
6948
6978
  //#region src/pin/fs.ts
6949
- const TEXT_EXTENSIONS = new Set([
6979
+ const TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
6950
6980
  ".md",
6951
6981
  ".mdx",
6952
6982
  ".markdown",
@@ -7363,35 +7393,6 @@ function configCommand(fs = realConfigFs) {
7363
7393
  return new Command("config").description("Read and write plugin-registered config in .agents/universal-plugin.json").addCommand(addCommand$1(fs)).addCommand(getCommand(fs));
7364
7394
  }
7365
7395
  //#endregion
7366
- //#region src/governance/cli.ts
7367
- const RETIRED = "universal-plugin governance is retired; buddy-agent-harness reference reads the same documents.\n";
7368
- const IN_A_SKILL = "In a skill, load a document with the load-reference skill in the buddy-agent-harness plugin.\n";
7369
- function retire(replacement) {
7370
- process.stderr.write(`${RETIRED}${IN_A_SKILL}→ ${replacement}\n`);
7371
- process.exit(1);
7372
- }
7373
- /** The flags `governance show` took that carried a value, so the value is not read as the name. */
7374
- const VALUE_FLAGS = new Set(["--root", "--format"]);
7375
- function documentName(args) {
7376
- for (let i = 0; i < args.length; i++) {
7377
- const arg = args[i];
7378
- if (VALUE_FLAGS.has(arg)) i++;
7379
- else if (!arg.startsWith("-")) return arg;
7380
- }
7381
- return "<name>";
7382
- }
7383
- /** Takes any argument or flag, so a caller still passing `--root` or `--format json` is told where
7384
- * to go instead of what it typed wrong. */
7385
- function retiredCommand(name) {
7386
- return new Command(name).allowUnknownOption().allowExcessArguments().helpOption(false);
7387
- }
7388
- function governanceCommand() {
7389
- const cmd = retiredCommand("governance").description("Retired: use buddy-agent-harness reference").helpOption("-h, --help", "Show help").helpCommand(false).addHelpText("after", "\nThis command is retired. buddy-agent-harness reference show|list|search reads the same documents.\nExample:\n $ buddy-agent-harness reference show plugin-design\n").action(() => retire("buddy-agent-harness reference list"));
7390
- cmd.addCommand(retiredCommand("show").action((_opts, command) => retire(`buddy-agent-harness reference show ${documentName(command.args)}`)));
7391
- cmd.addCommand(retiredCommand("list").action(() => retire("buddy-agent-harness reference list")));
7392
- return cmd;
7393
- }
7394
- //#endregion
7395
7396
  //#region src/init/fs.ts
7396
7397
  function git(root, args) {
7397
7398
  try {
@@ -7479,7 +7480,7 @@ const UP_NAMESPACE = "org.cyberuni.universal-plugin";
7479
7480
  const SCAFFOLD_DIRS = [
7480
7481
  "skills",
7481
7482
  "agents",
7482
- "governances",
7483
+ "references",
7483
7484
  "commands"
7484
7485
  ];
7485
7486
  /** The closed canonical manifest init writes: `$schema` + `name`, plus the extensions namespace
@@ -7630,7 +7631,7 @@ function collect$1(value, previous) {
7630
7631
  }
7631
7632
  function initCommand$1(deps = { fs: realInitFs }) {
7632
7633
  const cmd = new Command("init").description("Scaffold the canonical plugin.json (with --npm, wire an npm package to ship it)");
7633
- cmd.option("--name <name>", "Plugin name (default: the root directory name)").option("--vendor <id>", "Target vendor; repeatable", collect$1, []).option("--scaffold", "Create the standard skills/ agents/ governances/ commands/ directories").option("--force", "Overwrite an existing plugin.json").option("--yes", "Non-interactive (compatibility no-op; init never prompts)").option("--npm", "Wire package.json 'files' to ship the derived vendor manifests").option("--no-marketplace", "Skip the repository's local marketplace catalogs").option("--format <format>", "Output format: json or toon (default: toon)").addOption(new Option("--json").hideHelp()).addOption(ROOT_OPTION).addHelpText("after", "\nExample:\n $ universal-plugin plugin init --name my-plugin --scaffold\n").action((opts) => {
7634
+ cmd.option("--name <name>", "Plugin name (default: the root directory name)").option("--vendor <id>", "Target vendor; repeatable", collect$1, []).option("--scaffold", "Create the standard skills/ agents/ references/ commands/ directories").option("--force", "Overwrite an existing plugin.json").option("--yes", "Non-interactive (compatibility no-op; init never prompts)").option("--npm", "Wire package.json 'files' to ship the derived vendor manifests").option("--no-marketplace", "Skip the repository's local marketplace catalogs").option("--format <format>", "Output format: json or toon (default: toon)").addOption(new Option("--json").hideHelp()).addOption(ROOT_OPTION).addHelpText("after", "\nExample:\n $ universal-plugin plugin init --name my-plugin --scaffold\n").action((opts) => {
7634
7635
  try {
7635
7636
  const root = resolveRoot(opts.root);
7636
7637
  const plan = planInit(deps.fs.gather(root), {
@@ -7663,6 +7664,241 @@ function initCommand$1(deps = { fs: realInitFs }) {
7663
7664
  return cmd;
7664
7665
  }
7665
7666
  //#endregion
7667
+ //#region ../../node_modules/.pnpm/@cyberuni+agent-harness@0.1.0/node_modules/@cyberuni/agent-harness/dist/index.js
7668
+ const harnessIds = [
7669
+ "claude-code",
7670
+ "cursor",
7671
+ "codex",
7672
+ "copilot-cli",
7673
+ "opencode",
7674
+ "kilo",
7675
+ "gemini-cli",
7676
+ "qwen-code",
7677
+ "vscode-copilot",
7678
+ "cline"
7679
+ ];
7680
+ function resolveHarnessEnvironment(environment = {}) {
7681
+ return {
7682
+ env: environment.env ?? process.env,
7683
+ homedir: environment.homedir ?? homedir(),
7684
+ cwd: environment.cwd ?? process.cwd(),
7685
+ platform: environment.platform ?? process.platform
7686
+ };
7687
+ }
7688
+ /**
7689
+ * Where a harness keeps plugins at the user level. Paths are computed, not checked: a location may
7690
+ * not exist until the first plugin is installed.
7691
+ */
7692
+ function pluginStorage(harness, environment = {}) {
7693
+ return storages[harness](resolveHarnessEnvironment(environment));
7694
+ }
7695
+ /** `$XDG_CONFIG_HOME`, else `~/.config`, as the `xdg-basedir` package resolves it. */
7696
+ const xdgConfig = ({ env, homedir }) => env.XDG_CONFIG_HOME || join(homedir, ".config");
7697
+ /** The agent plugin folder of the stable VS Code build, per OS (E-VSC-P3). */
7698
+ function vscodeAgentPlugins({ env, homedir, platform }) {
7699
+ if (platform === "win32") {
7700
+ const configDir = win32.join(env.APPDATA || win32.join(homedir, "AppData", "Roaming"), "Code");
7701
+ return {
7702
+ configDir,
7703
+ path: win32.join(configDir, "agentPlugins")
7704
+ };
7705
+ }
7706
+ const configDir = platform === "darwin" ? join(homedir, "Library", "Application Support", "Code") : join(homedir, ".config", "Code");
7707
+ return {
7708
+ configDir,
7709
+ path: join(configDir, "agentPlugins")
7710
+ };
7711
+ }
7712
+ const storages = {
7713
+ "claude-code": ({ env, homedir }) => {
7714
+ const configDir = env.CLAUDE_CONFIG_DIR || join(homedir, ".claude");
7715
+ const plugins = join(configDir, "plugins");
7716
+ return {
7717
+ harness: "claude-code",
7718
+ configDir,
7719
+ locations: [
7720
+ {
7721
+ kind: "plugin-cache",
7722
+ path: join(plugins, "cache"),
7723
+ description: "Installed plugins as <marketplace>/<plugin>/<version>/",
7724
+ research: "E-CC-P1"
7725
+ },
7726
+ {
7727
+ kind: "marketplaces",
7728
+ path: join(plugins, "marketplaces"),
7729
+ description: "Marketplace checkouts",
7730
+ research: "E-CC-P1"
7731
+ },
7732
+ {
7733
+ kind: "install-record",
7734
+ path: join(plugins, "installed_plugins.json"),
7735
+ description: "Installs keyed by plugin@marketplace, each with scope, installPath, and version",
7736
+ research: "E-CC-P2"
7737
+ },
7738
+ {
7739
+ kind: "enabled-record",
7740
+ path: join(configDir, "settings.json"),
7741
+ description: "User settings; enabledPlugins maps plugin@marketplace to a boolean",
7742
+ research: "E-CC-P3"
7743
+ }
7744
+ ]
7745
+ };
7746
+ },
7747
+ cursor: ({ homedir }) => {
7748
+ const configDir = join(homedir, ".cursor");
7749
+ return {
7750
+ harness: "cursor",
7751
+ configDir,
7752
+ locations: [{
7753
+ kind: "local-plugins",
7754
+ path: join(configDir, "plugins", "local"),
7755
+ description: "Plugins under development, one folder each",
7756
+ research: "E-CUR-P2"
7757
+ }, {
7758
+ kind: "marketplaces",
7759
+ path: join(configDir, "plugins", "marketplaces"),
7760
+ description: "Marketplace checkouts",
7761
+ research: "E-CUR-P3"
7762
+ }]
7763
+ };
7764
+ },
7765
+ codex: ({ env, homedir }) => {
7766
+ const configDir = env.CODEX_HOME || join(homedir, ".codex");
7767
+ return {
7768
+ harness: "codex",
7769
+ configDir,
7770
+ locations: [{
7771
+ kind: "plugin-cache",
7772
+ path: join(configDir, "plugins", "cache"),
7773
+ description: "Installed plugins as <marketplace>/<plugin>/<version>/",
7774
+ research: "E-CODEX-P3"
7775
+ }, {
7776
+ kind: "enabled-record",
7777
+ path: join(configDir, "config.toml"),
7778
+ description: "User config; [plugins.\"<plugin>@<marketplace>\"] tables carry enabled = <bool>",
7779
+ research: "E-CODEX-P2"
7780
+ }]
7781
+ };
7782
+ },
7783
+ "copilot-cli": ({ env, homedir }) => {
7784
+ const configDir = env.COPILOT_HOME || join(homedir, ".copilot");
7785
+ return {
7786
+ harness: "copilot-cli",
7787
+ configDir,
7788
+ locations: [
7789
+ {
7790
+ kind: "installed-plugins",
7791
+ path: join(configDir, "installed-plugins"),
7792
+ description: "Installed plugins as <marketplace>/<plugin>/, or _direct/<source-id>/ for direct installs",
7793
+ research: "E-COPILOT-P9"
7794
+ },
7795
+ {
7796
+ kind: "install-record",
7797
+ path: join(configDir, "config.json"),
7798
+ description: "Application state, including installedPlugins",
7799
+ research: "E-COPILOT-P9"
7800
+ },
7801
+ {
7802
+ kind: "enabled-record",
7803
+ path: join(configDir, "settings.json"),
7804
+ description: "User settings (JSON with comments); enabledPlugins maps plugin specs to a boolean",
7805
+ research: "E-COPILOT-P4"
7806
+ }
7807
+ ]
7808
+ };
7809
+ },
7810
+ opencode: (environment) => {
7811
+ const configDir = join(xdgConfig(environment), "opencode");
7812
+ return {
7813
+ harness: "opencode",
7814
+ configDir,
7815
+ locations: [{
7816
+ kind: "local-plugins",
7817
+ path: join(configDir, "plugins"),
7818
+ description: "JS/TS plugin modules loaded at startup; npm plugins are named in the plugin config array",
7819
+ research: "E-OC-P2"
7820
+ }]
7821
+ };
7822
+ },
7823
+ kilo: (environment) => {
7824
+ const configDir = join(xdgConfig(environment), "kilo");
7825
+ return {
7826
+ harness: "kilo",
7827
+ configDir,
7828
+ locations: [{
7829
+ kind: "local-plugins",
7830
+ path: join(configDir, "plugin"),
7831
+ description: "JS/TS plugin modules loaded at startup; npm plugins are named in the plugin config array",
7832
+ research: "E-KILO-P2"
7833
+ }]
7834
+ };
7835
+ },
7836
+ "gemini-cli": ({ env, homedir }) => {
7837
+ const configDir = join(env.GEMINI_CLI_HOME || homedir, ".gemini");
7838
+ const extensions = join(configDir, "extensions");
7839
+ return {
7840
+ harness: "gemini-cli",
7841
+ configDir,
7842
+ locations: [{
7843
+ kind: "installed-plugins",
7844
+ path: extensions,
7845
+ description: "Installed extensions, one folder each with a gemini-extension.json manifest",
7846
+ research: "E-GEM-P1"
7847
+ }, {
7848
+ kind: "enabled-record",
7849
+ path: join(extensions, "extension-enablement.json"),
7850
+ description: "Per-extension path-glob overrides that enable or disable it by folder, not a boolean",
7851
+ research: "E-GEM-P3"
7852
+ }]
7853
+ };
7854
+ },
7855
+ "qwen-code": ({ env, homedir }) => {
7856
+ const configDir = env.QWEN_HOME || join(homedir, ".qwen");
7857
+ const extensions = join(configDir, "extensions");
7858
+ return {
7859
+ harness: "qwen-code",
7860
+ configDir,
7861
+ locations: [{
7862
+ kind: "installed-plugins",
7863
+ path: extensions,
7864
+ description: "Installed extensions, one folder each with a qwen-extension.json manifest",
7865
+ research: "E-QWEN-P1"
7866
+ }, {
7867
+ kind: "enabled-record",
7868
+ path: join(extensions, "extension-enablement.json"),
7869
+ description: "Extension enable/disable state, inherited from Gemini CLI's format",
7870
+ research: "E-QWEN-P2"
7871
+ }]
7872
+ };
7873
+ },
7874
+ "vscode-copilot": (environment) => {
7875
+ const { configDir, path } = vscodeAgentPlugins(environment);
7876
+ return {
7877
+ harness: "vscode-copilot",
7878
+ configDir,
7879
+ locations: [{
7880
+ kind: "installed-plugins",
7881
+ path,
7882
+ description: "Installed agent plugins as <host>/<org>/<repo>/",
7883
+ research: "E-VSC-P3"
7884
+ }]
7885
+ };
7886
+ },
7887
+ cline: ({ homedir }) => {
7888
+ const configDir = join(homedir, ".cline");
7889
+ return {
7890
+ harness: "cline",
7891
+ configDir,
7892
+ locations: [{
7893
+ kind: "installed-plugins",
7894
+ path: join(configDir, "plugins", "_installed"),
7895
+ description: "Installed plugins, grouped by source: npm, git, remote, or local",
7896
+ research: "E-CLINE-P3"
7897
+ }]
7898
+ };
7899
+ }
7900
+ };
7901
+ //#endregion
7666
7902
  //#region src/vendor-registry/vendor-registry.ts
7667
7903
  function lookupVendor(registry, vendorId) {
7668
7904
  return registry[vendorId] ?? null;
@@ -7675,6 +7911,15 @@ function mergeRegistries(base, override) {
7675
7911
  };
7676
7912
  return result;
7677
7913
  }
7914
+ /** Fills each vendor's `localPluginDir` from `locate`, which returns the harness's `local-plugins`
7915
+ * folder when `@cyberuni/agent-harness` knows one. A vendor it knows nothing about keeps its
7916
+ * registry value, or `null`. */
7917
+ function withLocalPluginDirs(registry, locate) {
7918
+ return Object.fromEntries(Object.entries(registry).map(([id, config]) => [id, {
7919
+ ...config,
7920
+ localPluginDir: locate(id) ?? config.localPluginDir ?? null
7921
+ }]));
7922
+ }
7678
7923
  //#endregion
7679
7924
  //#region src/vendor-registry/fs.ts
7680
7925
  function bundledRegistryPath() {
@@ -7684,8 +7929,15 @@ function bundledRegistryPath() {
7684
7929
  function userOverridePath() {
7685
7930
  return path.join(os.homedir(), ".agents", "universal-plugin-vendors.json");
7686
7931
  }
7932
+ /** The harness's `local-plugins` folder, as `@cyberuni/agent-harness` locates it. */
7933
+ function localPluginsDir(vendorId) {
7934
+ if (!harnessIds.includes(vendorId)) return void 0;
7935
+ return pluginStorage(vendorId).locations.find((l) => l.kind === "local-plugins")?.path;
7936
+ }
7937
+ /** The bundled registry, with the per-harness facts `@cyberuni/agent-harness` owns filled in, then
7938
+ * the user's override on top. */
7687
7939
  function loadRegistry() {
7688
- const bundled = JSON.parse(fs.readFileSync(bundledRegistryPath(), "utf8"));
7940
+ const bundled = withLocalPluginDirs(JSON.parse(fs.readFileSync(bundledRegistryPath(), "utf8")), localPluginsDir);
7689
7941
  try {
7690
7942
  return mergeRegistries(bundled, JSON.parse(fs.readFileSync(userOverridePath(), "utf8")));
7691
7943
  } catch (err) {
@@ -7740,7 +7992,7 @@ function readPluginName(manifestPath) {
7740
7992
  }
7741
7993
  /** Entries a copy never carries: the working copy's history and its installed dependencies. Neither
7742
7994
  * is part of the plugin, and both dwarf it. */
7743
- const COPY_EXCLUDED = new Set([".git", "node_modules"]);
7995
+ const COPY_EXCLUDED = /* @__PURE__ */ new Set([".git", "node_modules"]);
7744
7996
  /** Applies one planned write. A replace removes the destination first, so a copy never merges into
7745
7997
  * what was there before. A copy dereferences symlinks — a skill linked into the tree has to travel
7746
7998
  * as its content, since the destination is read on its own. */
@@ -8791,10 +9043,67 @@ function marketplaceCommand() {
8791
9043
  return new Command("marketplace").description("Generate repository-local marketplace metadata").addCommand(addCommand()).addCommand(initCommand()).addCommand(validateCommand$1());
8792
9044
  }
8793
9045
  //#endregion
8794
- //#region src/prepare/fs.ts
8795
- function expandHome(p) {
8796
- return p.startsWith("~") ? path.join(os.homedir(), p.slice(1)) : p;
9046
+ //#region src/prepare/installed-plugins.ts
9047
+ /**
9048
+ * Reads Claude Code's `installed_plugins.json` (version 2): `plugins` maps `<plugin>@<marketplace>`
9049
+ * to one entry per install, each with a `scope`, `installPath`, `version`, and, for project
9050
+ * installs, `projectPath`. Global scope takes the `user` installs; project scope takes the installs
9051
+ * whose `projectPath` is the project root.
9052
+ */
9053
+ function parseClaudeInstallRecord(raw, { scope, projectRoot }) {
9054
+ const plugins = isRecord(raw) && isRecord(raw["plugins"]) ? raw["plugins"] : {};
9055
+ const result = [];
9056
+ for (const [key, entries] of Object.entries(plugins)) {
9057
+ if (!Array.isArray(entries)) continue;
9058
+ const entry = entries.filter(isRecord).find((e) => scope === "global" ? e["scope"] === "user" : e["projectPath"] === projectRoot);
9059
+ if (!entry || typeof entry["installPath"] !== "string") continue;
9060
+ result.push({
9061
+ name: pluginName(key),
9062
+ version: typeof entry["version"] === "string" ? entry["version"] : "unknown",
9063
+ root: entry["installPath"]
9064
+ });
9065
+ }
9066
+ return result;
8797
9067
  }
9068
+ /** Strips the `@<marketplace>` suffix from a `<plugin>@<marketplace>` key. */
9069
+ function pluginName(key) {
9070
+ const at = key.lastIndexOf("@");
9071
+ return at > 0 ? key.slice(0, at) : key;
9072
+ }
9073
+ /**
9074
+ * Picks the newest of a plugin's cached version folders. Dotted numeric versions compare
9075
+ * numerically, part by part; anything else falls back to string order.
9076
+ */
9077
+ function latestVersion(versions) {
9078
+ return [...versions].sort(compareVersions).at(-1);
9079
+ }
9080
+ function compareVersions(a, b) {
9081
+ const pa = a.split(/[.+-]/);
9082
+ const pb = b.split(/[.+-]/);
9083
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
9084
+ const x = pa[i] ?? "";
9085
+ const y = pb[i] ?? "";
9086
+ if (x === y) continue;
9087
+ const nx = Number(x);
9088
+ const ny = Number(y);
9089
+ if (x !== "" && y !== "" && Number.isInteger(nx) && Number.isInteger(ny)) return nx - ny;
9090
+ return x < y ? -1 : 1;
9091
+ }
9092
+ return 0;
9093
+ }
9094
+ /** Maps installed plugins to the name → version record `prepare` snapshots. */
9095
+ function toVersions(plugins) {
9096
+ return Object.fromEntries(plugins.map((p) => [p.name, p.version]));
9097
+ }
9098
+ /** Maps installed plugins to name → root, with the home directory written as `~`. */
9099
+ function toRoots(plugins, home) {
9100
+ return Object.fromEntries(plugins.map((p) => [p.name, p.root.startsWith(home) ? "~" + p.root.slice(home.length) : p.root]));
9101
+ }
9102
+ function isRecord(value) {
9103
+ return typeof value === "object" && value !== null && !Array.isArray(value);
9104
+ }
9105
+ //#endregion
9106
+ //#region src/prepare/fs.ts
8798
9107
  function globalStatePath$2() {
8799
9108
  return path.join(os.homedir(), ".agents", "universal-plugin.json");
8800
9109
  }
@@ -8813,29 +9122,14 @@ function writeStateFile(filePath, state) {
8813
9122
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
8814
9123
  fs.writeFileSync(filePath, JSON.stringify(state, null, 2) + "\n", "utf8");
8815
9124
  }
8816
- function realPrepareFs(vendor, projectRoot) {
9125
+ /** `environment` locates the harness's plugin folders; it defaults to the current process. */
9126
+ function realPrepareFs(harness, installScope, environment = {}) {
9127
+ const projectRoot = installScope.projectRoot;
9128
+ let installed;
9129
+ const readInstalled = () => installed ??= readInstalledPlugins(harness, installScope, environment);
8817
9130
  return {
8818
- readManifest() {
8819
- if (!vendor.globalManifest) return {};
8820
- const manifestPath = expandHome(vendor.globalManifest);
8821
- try {
8822
- const raw = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
8823
- return Object.fromEntries(Object.entries(raw).map(([k, v]) => [k, typeof v === "object" && v !== null && "version" in v ? String(v.version) : String(v)]));
8824
- } catch (err) {
8825
- if (err.code === "ENOENT") return {};
8826
- throw err;
8827
- }
8828
- },
8829
- readPluginRoots() {
8830
- if (!vendor.globalPluginDir) return {};
8831
- const pluginDir = expandHome(vendor.globalPluginDir);
8832
- const manifest = this.readManifest();
8833
- const home = os.homedir();
8834
- return Object.fromEntries(Object.keys(manifest).map((name) => {
8835
- const absPath = path.join(pluginDir, name);
8836
- return [name, absPath.startsWith(home) ? "~" + absPath.slice(home.length) : absPath];
8837
- }));
8838
- },
9131
+ readManifest: () => toVersions(readInstalled()),
9132
+ readPluginRoots: () => toRoots(readInstalled(), environment.homedir ?? os.homedir()),
8839
9133
  readGlobalState: () => readStateFile(globalStatePath$2()) ?? emptyState(),
8840
9134
  readProjectState: () => projectRoot ? readStateFile(projectStatePath(projectRoot)) : null,
8841
9135
  writeGlobalState: (s) => writeStateFile(globalStatePath$2(), s),
@@ -8849,7 +9143,85 @@ function populateStoreFromVendorCache(pluginRoots, versions) {
8849
9143
  for (const [pluginName, pluginRoot] of Object.entries(pluginRoots)) {
8850
9144
  const entryPath = storeEntryPath(storePath, `npm/${pluginName}@${versions[pluginName] ?? "unknown"}`);
8851
9145
  if (entryExists(entryPath)) continue;
8852
- populateEntry(entryPath, pluginRoot);
9146
+ populateEntry(entryPath, expandHome(pluginRoot));
9147
+ }
9148
+ }
9149
+ function expandHome(p) {
9150
+ return p.startsWith("~") ? path.join(os.homedir(), p.slice(1)) : p;
9151
+ }
9152
+ /** Lists what a harness has installed, the way that harness records it: Claude Code in its install
9153
+ * record, Copilot CLI and Codex only in the layout of their plugin folders. Cursor keeps no record
9154
+ * of installed plugins. */
9155
+ function readInstalledPlugins(harness, installScope, environment) {
9156
+ const storage = pluginStorage(harness, environment);
9157
+ const location = (kind) => storage.locations.find((l) => l.kind === kind)?.path;
9158
+ switch (harness) {
9159
+ case "claude-code": {
9160
+ const record = location("install-record");
9161
+ const raw = record ? readJson(record) : null;
9162
+ return raw === null ? [] : parseClaudeInstallRecord(raw, installScope);
9163
+ }
9164
+ case "copilot-cli": {
9165
+ const dir = location("installed-plugins");
9166
+ return dir && installScope.scope === "global" ? readCopilotInstalls(dir) : [];
9167
+ }
9168
+ case "codex": {
9169
+ const dir = location("plugin-cache");
9170
+ return dir && installScope.scope === "global" ? readCodexCache(dir) : [];
9171
+ }
9172
+ default: return [];
9173
+ }
9174
+ }
9175
+ /** `<dir>/<marketplace>/<plugin>/` for marketplace installs, `<dir>/_direct/<source-id>/` for
9176
+ * direct ones; the plugin's own manifest carries its name and version. */
9177
+ function readCopilotInstalls(dir) {
9178
+ return subdirs(dir).flatMap((marketplace) => subdirs(path.join(dir, marketplace)).map((folder) => {
9179
+ const root = path.join(dir, marketplace, folder);
9180
+ const manifest = readPluginManifest(root);
9181
+ return {
9182
+ name: typeof manifest["name"] === "string" ? manifest["name"] : folder,
9183
+ version: typeof manifest["version"] === "string" ? manifest["version"] : "unknown",
9184
+ root
9185
+ };
9186
+ }));
9187
+ }
9188
+ /** `<dir>/<marketplace>/<plugin>/<version>/`; the newest cached version is the installed one. */
9189
+ function readCodexCache(dir) {
9190
+ return subdirs(dir).flatMap((marketplace) => subdirs(path.join(dir, marketplace)).flatMap((name) => {
9191
+ const version = latestVersion(subdirs(path.join(dir, marketplace, name)));
9192
+ return version ? [{
9193
+ name,
9194
+ version,
9195
+ root: path.join(dir, marketplace, name, version)
9196
+ }] : [];
9197
+ }));
9198
+ }
9199
+ const PLUGIN_MANIFESTS = [
9200
+ "plugin.json",
9201
+ ".github/plugin/plugin.json",
9202
+ ".claude-plugin/plugin.json"
9203
+ ];
9204
+ function readPluginManifest(root) {
9205
+ for (const file of PLUGIN_MANIFESTS) {
9206
+ const raw = readJson(path.join(root, file));
9207
+ if (typeof raw === "object" && raw !== null) return raw;
9208
+ }
9209
+ return {};
9210
+ }
9211
+ function readJson(filePath) {
9212
+ try {
9213
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
9214
+ } catch (err) {
9215
+ if (err.code === "ENOENT") return null;
9216
+ throw err;
9217
+ }
9218
+ }
9219
+ function subdirs(dir) {
9220
+ try {
9221
+ return fs.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
9222
+ } catch (err) {
9223
+ if (err.code === "ENOENT") return [];
9224
+ throw err;
8853
9225
  }
8854
9226
  }
8855
9227
  //#endregion
@@ -8958,16 +9330,17 @@ function runPrepare(opts) {
8958
9330
  version
8959
9331
  });
8960
9332
  }
8961
- if (!dryRun) if (scope === "global") prepareFs.writeGlobalState(updatedState);
8962
- else prepareFs.writeProjectState(updatedState);
9333
+ if (!dryRun) {
9334
+ if (scope === "global") prepareFs.writeGlobalState(updatedState);
9335
+ else prepareFs.writeProjectState(updatedState);
9336
+ }
8963
9337
  return { newActionCount: actions.length };
8964
9338
  }
8965
9339
  //#endregion
8966
9340
  //#region src/prepare/cli.ts
8967
9341
  function prepareCommand() {
8968
9342
  return new Command("prepare").description("Detect cross-vendor plugin sync actions").argument("<vendor-id>", "Vendor to read manifest from (e.g. claude-code)").addOption(new Option("--scope <scope>", "global or project").default("global").choices(["global", "project"])).option("--root <path>", "Project root for project-scope state file").option("--dry-run", "Print action count without writing state").action((vendorId, opts) => {
8969
- const vendor = lookupVendor(loadRegistry(), vendorId);
8970
- if (!vendor) {
9343
+ if (!lookupVendor(loadRegistry(), vendorId) || !harnessIds.includes(vendorId)) {
8971
9344
  process.stderr.write(`Unknown vendor: ${vendorId}\n`);
8972
9345
  process.exit(1);
8973
9346
  }
@@ -8976,12 +9349,16 @@ function prepareCommand() {
8976
9349
  process.exit(1);
8977
9350
  }
8978
9351
  const now = (/* @__PURE__ */ new Date()).toISOString();
8979
- const prepareFs = realPrepareFs(vendor, opts.root);
9352
+ const scope = opts.scope;
9353
+ const prepareFs = realPrepareFs(vendorId, {
9354
+ scope,
9355
+ projectRoot: opts.root ? path.resolve(opts.root) : void 0
9356
+ });
8980
9357
  const pluginRoots = prepareFs.readPluginRoots();
8981
9358
  const manifest = prepareFs.readManifest();
8982
9359
  const { newActionCount } = runPrepare({
8983
9360
  vendorId,
8984
- scope: opts.scope,
9361
+ scope,
8985
9362
  fs: prepareFs,
8986
9363
  now,
8987
9364
  dryRun: opts.dryRun
@@ -9497,7 +9874,6 @@ function pluginCommand() {
9497
9874
  program.addCommand(pluginCommand());
9498
9875
  program.addCommand(cleanCommand());
9499
9876
  program.addCommand(configCommand());
9500
- program.addCommand(governanceCommand());
9501
9877
  program.addCommand(marketplaceCommand());
9502
9878
  program.addCommand(prepareCommand());
9503
9879
  program.addCommand(publishCommand());
@@ -1,10 +1,8 @@
1
1
  {
2
2
  "claude-code": {
3
3
  "sessionStartEvent": "SessionStart",
4
- "globalManifest": "~/.claude/plugins/installed_plugins.json",
5
4
  "projectManifest": null,
6
5
  "hookGlob": "~/.claude/plugins/universal-plugin/hooks/hooks.json",
7
- "globalPluginDir": "~/.claude/plugins/",
8
6
  "pluginRootSuffix": ".claude-plugin/plugin.json",
9
7
  "localPluginDir": "~/.claude/skills/",
10
8
  "localPluginLink": true,
@@ -15,12 +13,9 @@
15
13
  },
16
14
  "cursor": {
17
15
  "sessionStartEvent": "sessionStart",
18
- "globalManifest": null,
19
16
  "projectManifest": null,
20
17
  "hookGlob": null,
21
- "globalPluginDir": null,
22
18
  "pluginRootSuffix": ".cursor-plugin/plugin.json",
23
- "localPluginDir": "~/.cursor/plugins/local/",
24
19
  "localPluginLink": false,
25
20
  "localReload": "run Developer: Reload Window in Cursor",
26
21
  "installCommand": null,
@@ -29,10 +24,8 @@
29
24
  },
30
25
  "codex": {
31
26
  "sessionStartEvent": "SessionStart",
32
- "globalManifest": null,
33
27
  "projectManifest": null,
34
28
  "hookGlob": null,
35
- "globalPluginDir": null,
36
29
  "pluginRootSuffix": ".codex-plugin/plugin.json",
37
30
  "localPluginDir": null,
38
31
  "localPluginLink": false,
@@ -43,10 +36,8 @@
43
36
  },
44
37
  "copilot-cli": {
45
38
  "sessionStartEvent": "sessionStart",
46
- "globalManifest": null,
47
39
  "projectManifest": null,
48
40
  "hookGlob": null,
49
- "globalPluginDir": null,
50
41
  "pluginRootSuffix": "plugin.json",
51
42
  "localPluginDir": null,
52
43
  "localPluginLink": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "universal-plugin",
3
- "version": "0.10.0",
3
+ "version": "0.11.1",
4
4
  "description": "Universal AI agent plugin build tool",
5
5
  "keywords": [
6
6
  "agent-plugin",
@@ -39,6 +39,7 @@
39
39
  "agents"
40
40
  ],
41
41
  "dependencies": {
42
+ "@cyberuni/agent-harness": "^0.1.0",
42
43
  "@repobuddy/upx": "^0.1.0",
43
44
  "@toon-format/toon": "^4.1.1",
44
45
  "commander": "^14.0.3",
@@ -49,7 +50,7 @@
49
50
  "@types/semver": "^7.7.1",
50
51
  "ajv": "^8.20.0",
51
52
  "knip": "^6.14.1",
52
- "tsdown": "^0.22.0",
53
+ "tsdown": "^0.23.0",
53
54
  "tsx": "^4.22.3",
54
55
  "typescript": "^6.0.0",
55
56
  "vitest": "^4.1.7"
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.10.0",
4
+ "version": "0.11.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"
package/readme.md CHANGED
@@ -106,14 +106,12 @@ npx universal-plugin config add --key sdd-plugins --entry '{"name":"aces","handl
106
106
  `add` appends the entry, or replaces the existing entry with the same `name`. Both commands print
107
107
  TOON by default; pass `--format json` for JSON.
108
108
 
109
- ### governance (retired)
110
-
111
- `governance list` and `governance show` are retired. They print the replacement and exit 1 for one
112
- release, then go away. Read the documents with `buddy-agent-harness reference show <name>`, or, from a
113
- skill, with the `load-reference` skill in the `buddy-agent-harness` plugin.
109
+ ### Reference documents
114
110
 
115
111
  This package ships its own documents (`plugin-design`, `slash-invocation`, `universal-plugin`) under
116
- `references/`, so `reference show` finds them in any project that depends on `universal-plugin`.
112
+ `references/`. Read one with `buddy-agent-harness reference show <name>` in any project that depends
113
+ on `universal-plugin`, or, from a skill, with the `reference` skill in the `buddy-agent-harness`
114
+ plugin.
117
115
 
118
116
  ### Housekeeping
119
117
 
@@ -330,6 +330,6 @@ Portability rules for skill bodies: keep each `SKILL.md` body under 6,000 chars;
330
330
 
331
331
  ## References
332
332
 
333
- Load `skill-design`, `skill-repo-structure`, and `agent-tool-output` with the `load-reference` skill in the `buddy-agent-harness` plugin.
333
+ Load `skill-design`, `skill-repo-structure`, and `agent-tool-output` with the `reference` skill in the `buddy-agent-harness` plugin.
334
334
 
335
335
  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 `load-reference` skill in the `buddy-agent-harness` plugin.
4
+ Load `plugin-design` with the `reference` skill in the `buddy-agent-harness` plugin.
@@ -117,7 +117,7 @@ Validation runs before the first write, so a failed build leaves the tree untouc
117
117
  - **Expect a build to pin skill `npx` references.** That is the release-time `plugin bundle`, not
118
118
  this step.
119
119
  - **Expect a build to copy governances into skills.** A skill loads a reference through the
120
- `load-reference` skill in the `buddy-agent-harness` plugin; the build no longer copies them, and
120
+ `reference` skill in the `buddy-agent-harness` plugin; the build no longer copies them, and
121
121
  `--check` is gone.
122
122
 
123
123
  ## Related skills
@@ -38,7 +38,7 @@ This skill owns the **authoring** side: the plugin a project ships. Setting a re
38
38
 
39
39
  ## Prerequisites
40
40
 
41
- Before any work that adds or removes components, load `plugin-design` with the `load-reference`
41
+ Before any work that adds or removes components, load `plugin-design` with the `reference`
42
42
  skill in the `buddy-agent-harness` plugin.
43
43
 
44
44
  It is the authoritative source for which component to reach for and which anti-patterns to avoid.
@@ -233,7 +233,7 @@ This skill is not a formatter. If the project has one, run it over the written f
233
233
 
234
234
  ## References
235
235
 
236
- - Reference: `plugin-design`, loaded with the `load-reference` skill in the `buddy-agent-harness` plugin
236
+ - Reference: `plugin-design`, loaded with the `reference` skill in the `buddy-agent-harness` plugin
237
237
  - Spec: https://github.com/cyberuni/universal-plugin/blob/main/packages/universal-plugin/.agents/spec/spec.md
238
238
  - Manifest schema (Agent Plugins Specification v1.0.0): https://agent-plugins.org/schemas/1.0.0/plugin.schema.json
239
239
  - Extension schema (the body of `extensions["org.cyberuni.universal-plugin"]`), shipped in the package at
@@ -62,7 +62,7 @@ what each build gets. A vendor listed in `vendors` with no `harnesses` entry sti
62
62
  └── README.md
63
63
  ```
64
64
 
65
- `plugin init --scaffold` creates the standard `skills/`, `agents/`, `governances/`, and `commands/`
65
+ `plugin init --scaffold` creates the standard `skills/`, `agents/`, `references/`, and `commands/`
66
66
  directories. Create the rest only when the plugin has content for them.
67
67
 
68
68
  ## Components