universal-plugin 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/dist/cli.mjs +409 -63
- package/dist/data/vendors.json +0 -9
- package/package.json +2 -1
- package/plugin.json +1 -1
- package/readme.md +4 -6
- package/skills/init-universal-plugin/references/standard.md +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "universal-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
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.
|
|
3
|
+
"version": "0.11.0",
|
|
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.
|
|
3
|
+
"version": "0.11.0",
|
|
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;
|
|
@@ -7363,35 +7365,6 @@ function configCommand(fs = realConfigFs) {
|
|
|
7363
7365
|
return new Command("config").description("Read and write plugin-registered config in .agents/universal-plugin.json").addCommand(addCommand$1(fs)).addCommand(getCommand(fs));
|
|
7364
7366
|
}
|
|
7365
7367
|
//#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
7368
|
//#region src/init/fs.ts
|
|
7396
7369
|
function git(root, args) {
|
|
7397
7370
|
try {
|
|
@@ -7479,7 +7452,7 @@ const UP_NAMESPACE = "org.cyberuni.universal-plugin";
|
|
|
7479
7452
|
const SCAFFOLD_DIRS = [
|
|
7480
7453
|
"skills",
|
|
7481
7454
|
"agents",
|
|
7482
|
-
"
|
|
7455
|
+
"references",
|
|
7483
7456
|
"commands"
|
|
7484
7457
|
];
|
|
7485
7458
|
/** The closed canonical manifest init writes: `$schema` + `name`, plus the extensions namespace
|
|
@@ -7630,7 +7603,7 @@ function collect$1(value, previous) {
|
|
|
7630
7603
|
}
|
|
7631
7604
|
function initCommand$1(deps = { fs: realInitFs }) {
|
|
7632
7605
|
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/
|
|
7606
|
+
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
7607
|
try {
|
|
7635
7608
|
const root = resolveRoot(opts.root);
|
|
7636
7609
|
const plan = planInit(deps.fs.gather(root), {
|
|
@@ -7663,6 +7636,241 @@ function initCommand$1(deps = { fs: realInitFs }) {
|
|
|
7663
7636
|
return cmd;
|
|
7664
7637
|
}
|
|
7665
7638
|
//#endregion
|
|
7639
|
+
//#region ../../node_modules/.pnpm/@cyberuni+agent-harness@0.1.0/node_modules/@cyberuni/agent-harness/dist/index.js
|
|
7640
|
+
const harnessIds = [
|
|
7641
|
+
"claude-code",
|
|
7642
|
+
"cursor",
|
|
7643
|
+
"codex",
|
|
7644
|
+
"copilot-cli",
|
|
7645
|
+
"opencode",
|
|
7646
|
+
"kilo",
|
|
7647
|
+
"gemini-cli",
|
|
7648
|
+
"qwen-code",
|
|
7649
|
+
"vscode-copilot",
|
|
7650
|
+
"cline"
|
|
7651
|
+
];
|
|
7652
|
+
function resolveHarnessEnvironment(environment = {}) {
|
|
7653
|
+
return {
|
|
7654
|
+
env: environment.env ?? process.env,
|
|
7655
|
+
homedir: environment.homedir ?? homedir(),
|
|
7656
|
+
cwd: environment.cwd ?? process.cwd(),
|
|
7657
|
+
platform: environment.platform ?? process.platform
|
|
7658
|
+
};
|
|
7659
|
+
}
|
|
7660
|
+
/**
|
|
7661
|
+
* Where a harness keeps plugins at the user level. Paths are computed, not checked: a location may
|
|
7662
|
+
* not exist until the first plugin is installed.
|
|
7663
|
+
*/
|
|
7664
|
+
function pluginStorage(harness, environment = {}) {
|
|
7665
|
+
return storages[harness](resolveHarnessEnvironment(environment));
|
|
7666
|
+
}
|
|
7667
|
+
/** `$XDG_CONFIG_HOME`, else `~/.config`, as the `xdg-basedir` package resolves it. */
|
|
7668
|
+
const xdgConfig = ({ env, homedir }) => env.XDG_CONFIG_HOME || join(homedir, ".config");
|
|
7669
|
+
/** The agent plugin folder of the stable VS Code build, per OS (E-VSC-P3). */
|
|
7670
|
+
function vscodeAgentPlugins({ env, homedir, platform }) {
|
|
7671
|
+
if (platform === "win32") {
|
|
7672
|
+
const configDir = win32.join(env.APPDATA || win32.join(homedir, "AppData", "Roaming"), "Code");
|
|
7673
|
+
return {
|
|
7674
|
+
configDir,
|
|
7675
|
+
path: win32.join(configDir, "agentPlugins")
|
|
7676
|
+
};
|
|
7677
|
+
}
|
|
7678
|
+
const configDir = platform === "darwin" ? join(homedir, "Library", "Application Support", "Code") : join(homedir, ".config", "Code");
|
|
7679
|
+
return {
|
|
7680
|
+
configDir,
|
|
7681
|
+
path: join(configDir, "agentPlugins")
|
|
7682
|
+
};
|
|
7683
|
+
}
|
|
7684
|
+
const storages = {
|
|
7685
|
+
"claude-code": ({ env, homedir }) => {
|
|
7686
|
+
const configDir = env.CLAUDE_CONFIG_DIR || join(homedir, ".claude");
|
|
7687
|
+
const plugins = join(configDir, "plugins");
|
|
7688
|
+
return {
|
|
7689
|
+
harness: "claude-code",
|
|
7690
|
+
configDir,
|
|
7691
|
+
locations: [
|
|
7692
|
+
{
|
|
7693
|
+
kind: "plugin-cache",
|
|
7694
|
+
path: join(plugins, "cache"),
|
|
7695
|
+
description: "Installed plugins as <marketplace>/<plugin>/<version>/",
|
|
7696
|
+
research: "E-CC-P1"
|
|
7697
|
+
},
|
|
7698
|
+
{
|
|
7699
|
+
kind: "marketplaces",
|
|
7700
|
+
path: join(plugins, "marketplaces"),
|
|
7701
|
+
description: "Marketplace checkouts",
|
|
7702
|
+
research: "E-CC-P1"
|
|
7703
|
+
},
|
|
7704
|
+
{
|
|
7705
|
+
kind: "install-record",
|
|
7706
|
+
path: join(plugins, "installed_plugins.json"),
|
|
7707
|
+
description: "Installs keyed by plugin@marketplace, each with scope, installPath, and version",
|
|
7708
|
+
research: "E-CC-P2"
|
|
7709
|
+
},
|
|
7710
|
+
{
|
|
7711
|
+
kind: "enabled-record",
|
|
7712
|
+
path: join(configDir, "settings.json"),
|
|
7713
|
+
description: "User settings; enabledPlugins maps plugin@marketplace to a boolean",
|
|
7714
|
+
research: "E-CC-P3"
|
|
7715
|
+
}
|
|
7716
|
+
]
|
|
7717
|
+
};
|
|
7718
|
+
},
|
|
7719
|
+
cursor: ({ homedir }) => {
|
|
7720
|
+
const configDir = join(homedir, ".cursor");
|
|
7721
|
+
return {
|
|
7722
|
+
harness: "cursor",
|
|
7723
|
+
configDir,
|
|
7724
|
+
locations: [{
|
|
7725
|
+
kind: "local-plugins",
|
|
7726
|
+
path: join(configDir, "plugins", "local"),
|
|
7727
|
+
description: "Plugins under development, one folder each",
|
|
7728
|
+
research: "E-CUR-P2"
|
|
7729
|
+
}, {
|
|
7730
|
+
kind: "marketplaces",
|
|
7731
|
+
path: join(configDir, "plugins", "marketplaces"),
|
|
7732
|
+
description: "Marketplace checkouts",
|
|
7733
|
+
research: "E-CUR-P3"
|
|
7734
|
+
}]
|
|
7735
|
+
};
|
|
7736
|
+
},
|
|
7737
|
+
codex: ({ env, homedir }) => {
|
|
7738
|
+
const configDir = env.CODEX_HOME || join(homedir, ".codex");
|
|
7739
|
+
return {
|
|
7740
|
+
harness: "codex",
|
|
7741
|
+
configDir,
|
|
7742
|
+
locations: [{
|
|
7743
|
+
kind: "plugin-cache",
|
|
7744
|
+
path: join(configDir, "plugins", "cache"),
|
|
7745
|
+
description: "Installed plugins as <marketplace>/<plugin>/<version>/",
|
|
7746
|
+
research: "E-CODEX-P3"
|
|
7747
|
+
}, {
|
|
7748
|
+
kind: "enabled-record",
|
|
7749
|
+
path: join(configDir, "config.toml"),
|
|
7750
|
+
description: "User config; [plugins.\"<plugin>@<marketplace>\"] tables carry enabled = <bool>",
|
|
7751
|
+
research: "E-CODEX-P2"
|
|
7752
|
+
}]
|
|
7753
|
+
};
|
|
7754
|
+
},
|
|
7755
|
+
"copilot-cli": ({ env, homedir }) => {
|
|
7756
|
+
const configDir = env.COPILOT_HOME || join(homedir, ".copilot");
|
|
7757
|
+
return {
|
|
7758
|
+
harness: "copilot-cli",
|
|
7759
|
+
configDir,
|
|
7760
|
+
locations: [
|
|
7761
|
+
{
|
|
7762
|
+
kind: "installed-plugins",
|
|
7763
|
+
path: join(configDir, "installed-plugins"),
|
|
7764
|
+
description: "Installed plugins as <marketplace>/<plugin>/, or _direct/<source-id>/ for direct installs",
|
|
7765
|
+
research: "E-COPILOT-P9"
|
|
7766
|
+
},
|
|
7767
|
+
{
|
|
7768
|
+
kind: "install-record",
|
|
7769
|
+
path: join(configDir, "config.json"),
|
|
7770
|
+
description: "Application state, including installedPlugins",
|
|
7771
|
+
research: "E-COPILOT-P9"
|
|
7772
|
+
},
|
|
7773
|
+
{
|
|
7774
|
+
kind: "enabled-record",
|
|
7775
|
+
path: join(configDir, "settings.json"),
|
|
7776
|
+
description: "User settings (JSON with comments); enabledPlugins maps plugin specs to a boolean",
|
|
7777
|
+
research: "E-COPILOT-P4"
|
|
7778
|
+
}
|
|
7779
|
+
]
|
|
7780
|
+
};
|
|
7781
|
+
},
|
|
7782
|
+
opencode: (environment) => {
|
|
7783
|
+
const configDir = join(xdgConfig(environment), "opencode");
|
|
7784
|
+
return {
|
|
7785
|
+
harness: "opencode",
|
|
7786
|
+
configDir,
|
|
7787
|
+
locations: [{
|
|
7788
|
+
kind: "local-plugins",
|
|
7789
|
+
path: join(configDir, "plugins"),
|
|
7790
|
+
description: "JS/TS plugin modules loaded at startup; npm plugins are named in the plugin config array",
|
|
7791
|
+
research: "E-OC-P2"
|
|
7792
|
+
}]
|
|
7793
|
+
};
|
|
7794
|
+
},
|
|
7795
|
+
kilo: (environment) => {
|
|
7796
|
+
const configDir = join(xdgConfig(environment), "kilo");
|
|
7797
|
+
return {
|
|
7798
|
+
harness: "kilo",
|
|
7799
|
+
configDir,
|
|
7800
|
+
locations: [{
|
|
7801
|
+
kind: "local-plugins",
|
|
7802
|
+
path: join(configDir, "plugin"),
|
|
7803
|
+
description: "JS/TS plugin modules loaded at startup; npm plugins are named in the plugin config array",
|
|
7804
|
+
research: "E-KILO-P2"
|
|
7805
|
+
}]
|
|
7806
|
+
};
|
|
7807
|
+
},
|
|
7808
|
+
"gemini-cli": ({ env, homedir }) => {
|
|
7809
|
+
const configDir = join(env.GEMINI_CLI_HOME || homedir, ".gemini");
|
|
7810
|
+
const extensions = join(configDir, "extensions");
|
|
7811
|
+
return {
|
|
7812
|
+
harness: "gemini-cli",
|
|
7813
|
+
configDir,
|
|
7814
|
+
locations: [{
|
|
7815
|
+
kind: "installed-plugins",
|
|
7816
|
+
path: extensions,
|
|
7817
|
+
description: "Installed extensions, one folder each with a gemini-extension.json manifest",
|
|
7818
|
+
research: "E-GEM-P1"
|
|
7819
|
+
}, {
|
|
7820
|
+
kind: "enabled-record",
|
|
7821
|
+
path: join(extensions, "extension-enablement.json"),
|
|
7822
|
+
description: "Per-extension path-glob overrides that enable or disable it by folder, not a boolean",
|
|
7823
|
+
research: "E-GEM-P3"
|
|
7824
|
+
}]
|
|
7825
|
+
};
|
|
7826
|
+
},
|
|
7827
|
+
"qwen-code": ({ env, homedir }) => {
|
|
7828
|
+
const configDir = env.QWEN_HOME || join(homedir, ".qwen");
|
|
7829
|
+
const extensions = join(configDir, "extensions");
|
|
7830
|
+
return {
|
|
7831
|
+
harness: "qwen-code",
|
|
7832
|
+
configDir,
|
|
7833
|
+
locations: [{
|
|
7834
|
+
kind: "installed-plugins",
|
|
7835
|
+
path: extensions,
|
|
7836
|
+
description: "Installed extensions, one folder each with a qwen-extension.json manifest",
|
|
7837
|
+
research: "E-QWEN-P1"
|
|
7838
|
+
}, {
|
|
7839
|
+
kind: "enabled-record",
|
|
7840
|
+
path: join(extensions, "extension-enablement.json"),
|
|
7841
|
+
description: "Extension enable/disable state, inherited from Gemini CLI's format",
|
|
7842
|
+
research: "E-QWEN-P2"
|
|
7843
|
+
}]
|
|
7844
|
+
};
|
|
7845
|
+
},
|
|
7846
|
+
"vscode-copilot": (environment) => {
|
|
7847
|
+
const { configDir, path } = vscodeAgentPlugins(environment);
|
|
7848
|
+
return {
|
|
7849
|
+
harness: "vscode-copilot",
|
|
7850
|
+
configDir,
|
|
7851
|
+
locations: [{
|
|
7852
|
+
kind: "installed-plugins",
|
|
7853
|
+
path,
|
|
7854
|
+
description: "Installed agent plugins as <host>/<org>/<repo>/",
|
|
7855
|
+
research: "E-VSC-P3"
|
|
7856
|
+
}]
|
|
7857
|
+
};
|
|
7858
|
+
},
|
|
7859
|
+
cline: ({ homedir }) => {
|
|
7860
|
+
const configDir = join(homedir, ".cline");
|
|
7861
|
+
return {
|
|
7862
|
+
harness: "cline",
|
|
7863
|
+
configDir,
|
|
7864
|
+
locations: [{
|
|
7865
|
+
kind: "installed-plugins",
|
|
7866
|
+
path: join(configDir, "plugins", "_installed"),
|
|
7867
|
+
description: "Installed plugins, grouped by source: npm, git, remote, or local",
|
|
7868
|
+
research: "E-CLINE-P3"
|
|
7869
|
+
}]
|
|
7870
|
+
};
|
|
7871
|
+
}
|
|
7872
|
+
};
|
|
7873
|
+
//#endregion
|
|
7666
7874
|
//#region src/vendor-registry/vendor-registry.ts
|
|
7667
7875
|
function lookupVendor(registry, vendorId) {
|
|
7668
7876
|
return registry[vendorId] ?? null;
|
|
@@ -7675,6 +7883,15 @@ function mergeRegistries(base, override) {
|
|
|
7675
7883
|
};
|
|
7676
7884
|
return result;
|
|
7677
7885
|
}
|
|
7886
|
+
/** Fills each vendor's `localPluginDir` from `locate`, which returns the harness's `local-plugins`
|
|
7887
|
+
* folder when `@cyberuni/agent-harness` knows one. A vendor it knows nothing about keeps its
|
|
7888
|
+
* registry value, or `null`. */
|
|
7889
|
+
function withLocalPluginDirs(registry, locate) {
|
|
7890
|
+
return Object.fromEntries(Object.entries(registry).map(([id, config]) => [id, {
|
|
7891
|
+
...config,
|
|
7892
|
+
localPluginDir: locate(id) ?? config.localPluginDir ?? null
|
|
7893
|
+
}]));
|
|
7894
|
+
}
|
|
7678
7895
|
//#endregion
|
|
7679
7896
|
//#region src/vendor-registry/fs.ts
|
|
7680
7897
|
function bundledRegistryPath() {
|
|
@@ -7684,8 +7901,15 @@ function bundledRegistryPath() {
|
|
|
7684
7901
|
function userOverridePath() {
|
|
7685
7902
|
return path.join(os.homedir(), ".agents", "universal-plugin-vendors.json");
|
|
7686
7903
|
}
|
|
7904
|
+
/** The harness's `local-plugins` folder, as `@cyberuni/agent-harness` locates it. */
|
|
7905
|
+
function localPluginsDir(vendorId) {
|
|
7906
|
+
if (!harnessIds.includes(vendorId)) return void 0;
|
|
7907
|
+
return pluginStorage(vendorId).locations.find((l) => l.kind === "local-plugins")?.path;
|
|
7908
|
+
}
|
|
7909
|
+
/** The bundled registry, with the per-harness facts `@cyberuni/agent-harness` owns filled in, then
|
|
7910
|
+
* the user's override on top. */
|
|
7687
7911
|
function loadRegistry() {
|
|
7688
|
-
const bundled = JSON.parse(fs.readFileSync(bundledRegistryPath(), "utf8"));
|
|
7912
|
+
const bundled = withLocalPluginDirs(JSON.parse(fs.readFileSync(bundledRegistryPath(), "utf8")), localPluginsDir);
|
|
7689
7913
|
try {
|
|
7690
7914
|
return mergeRegistries(bundled, JSON.parse(fs.readFileSync(userOverridePath(), "utf8")));
|
|
7691
7915
|
} catch (err) {
|
|
@@ -8791,10 +9015,67 @@ function marketplaceCommand() {
|
|
|
8791
9015
|
return new Command("marketplace").description("Generate repository-local marketplace metadata").addCommand(addCommand()).addCommand(initCommand()).addCommand(validateCommand$1());
|
|
8792
9016
|
}
|
|
8793
9017
|
//#endregion
|
|
8794
|
-
//#region src/prepare/
|
|
8795
|
-
|
|
8796
|
-
|
|
9018
|
+
//#region src/prepare/installed-plugins.ts
|
|
9019
|
+
/**
|
|
9020
|
+
* Reads Claude Code's `installed_plugins.json` (version 2): `plugins` maps `<plugin>@<marketplace>`
|
|
9021
|
+
* to one entry per install, each with a `scope`, `installPath`, `version`, and, for project
|
|
9022
|
+
* installs, `projectPath`. Global scope takes the `user` installs; project scope takes the installs
|
|
9023
|
+
* whose `projectPath` is the project root.
|
|
9024
|
+
*/
|
|
9025
|
+
function parseClaudeInstallRecord(raw, { scope, projectRoot }) {
|
|
9026
|
+
const plugins = isRecord(raw) && isRecord(raw["plugins"]) ? raw["plugins"] : {};
|
|
9027
|
+
const result = [];
|
|
9028
|
+
for (const [key, entries] of Object.entries(plugins)) {
|
|
9029
|
+
if (!Array.isArray(entries)) continue;
|
|
9030
|
+
const entry = entries.filter(isRecord).find((e) => scope === "global" ? e["scope"] === "user" : e["projectPath"] === projectRoot);
|
|
9031
|
+
if (!entry || typeof entry["installPath"] !== "string") continue;
|
|
9032
|
+
result.push({
|
|
9033
|
+
name: pluginName(key),
|
|
9034
|
+
version: typeof entry["version"] === "string" ? entry["version"] : "unknown",
|
|
9035
|
+
root: entry["installPath"]
|
|
9036
|
+
});
|
|
9037
|
+
}
|
|
9038
|
+
return result;
|
|
8797
9039
|
}
|
|
9040
|
+
/** Strips the `@<marketplace>` suffix from a `<plugin>@<marketplace>` key. */
|
|
9041
|
+
function pluginName(key) {
|
|
9042
|
+
const at = key.lastIndexOf("@");
|
|
9043
|
+
return at > 0 ? key.slice(0, at) : key;
|
|
9044
|
+
}
|
|
9045
|
+
/**
|
|
9046
|
+
* Picks the newest of a plugin's cached version folders. Dotted numeric versions compare
|
|
9047
|
+
* numerically, part by part; anything else falls back to string order.
|
|
9048
|
+
*/
|
|
9049
|
+
function latestVersion(versions) {
|
|
9050
|
+
return [...versions].sort(compareVersions).at(-1);
|
|
9051
|
+
}
|
|
9052
|
+
function compareVersions(a, b) {
|
|
9053
|
+
const pa = a.split(/[.+-]/);
|
|
9054
|
+
const pb = b.split(/[.+-]/);
|
|
9055
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
9056
|
+
const x = pa[i] ?? "";
|
|
9057
|
+
const y = pb[i] ?? "";
|
|
9058
|
+
if (x === y) continue;
|
|
9059
|
+
const nx = Number(x);
|
|
9060
|
+
const ny = Number(y);
|
|
9061
|
+
if (x !== "" && y !== "" && Number.isInteger(nx) && Number.isInteger(ny)) return nx - ny;
|
|
9062
|
+
return x < y ? -1 : 1;
|
|
9063
|
+
}
|
|
9064
|
+
return 0;
|
|
9065
|
+
}
|
|
9066
|
+
/** Maps installed plugins to the name → version record `prepare` snapshots. */
|
|
9067
|
+
function toVersions(plugins) {
|
|
9068
|
+
return Object.fromEntries(plugins.map((p) => [p.name, p.version]));
|
|
9069
|
+
}
|
|
9070
|
+
/** Maps installed plugins to name → root, with the home directory written as `~`. */
|
|
9071
|
+
function toRoots(plugins, home) {
|
|
9072
|
+
return Object.fromEntries(plugins.map((p) => [p.name, p.root.startsWith(home) ? "~" + p.root.slice(home.length) : p.root]));
|
|
9073
|
+
}
|
|
9074
|
+
function isRecord(value) {
|
|
9075
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9076
|
+
}
|
|
9077
|
+
//#endregion
|
|
9078
|
+
//#region src/prepare/fs.ts
|
|
8798
9079
|
function globalStatePath$2() {
|
|
8799
9080
|
return path.join(os.homedir(), ".agents", "universal-plugin.json");
|
|
8800
9081
|
}
|
|
@@ -8813,29 +9094,14 @@ function writeStateFile(filePath, state) {
|
|
|
8813
9094
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
8814
9095
|
fs.writeFileSync(filePath, JSON.stringify(state, null, 2) + "\n", "utf8");
|
|
8815
9096
|
}
|
|
8816
|
-
|
|
9097
|
+
/** `environment` locates the harness's plugin folders; it defaults to the current process. */
|
|
9098
|
+
function realPrepareFs(harness, installScope, environment = {}) {
|
|
9099
|
+
const projectRoot = installScope.projectRoot;
|
|
9100
|
+
let installed;
|
|
9101
|
+
const readInstalled = () => installed ??= readInstalledPlugins(harness, installScope, environment);
|
|
8817
9102
|
return {
|
|
8818
|
-
readManifest()
|
|
8819
|
-
|
|
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
|
-
},
|
|
9103
|
+
readManifest: () => toVersions(readInstalled()),
|
|
9104
|
+
readPluginRoots: () => toRoots(readInstalled(), environment.homedir ?? os.homedir()),
|
|
8839
9105
|
readGlobalState: () => readStateFile(globalStatePath$2()) ?? emptyState(),
|
|
8840
9106
|
readProjectState: () => projectRoot ? readStateFile(projectStatePath(projectRoot)) : null,
|
|
8841
9107
|
writeGlobalState: (s) => writeStateFile(globalStatePath$2(), s),
|
|
@@ -8849,7 +9115,85 @@ function populateStoreFromVendorCache(pluginRoots, versions) {
|
|
|
8849
9115
|
for (const [pluginName, pluginRoot] of Object.entries(pluginRoots)) {
|
|
8850
9116
|
const entryPath = storeEntryPath(storePath, `npm/${pluginName}@${versions[pluginName] ?? "unknown"}`);
|
|
8851
9117
|
if (entryExists(entryPath)) continue;
|
|
8852
|
-
populateEntry(entryPath, pluginRoot);
|
|
9118
|
+
populateEntry(entryPath, expandHome(pluginRoot));
|
|
9119
|
+
}
|
|
9120
|
+
}
|
|
9121
|
+
function expandHome(p) {
|
|
9122
|
+
return p.startsWith("~") ? path.join(os.homedir(), p.slice(1)) : p;
|
|
9123
|
+
}
|
|
9124
|
+
/** Lists what a harness has installed, the way that harness records it: Claude Code in its install
|
|
9125
|
+
* record, Copilot CLI and Codex only in the layout of their plugin folders. Cursor keeps no record
|
|
9126
|
+
* of installed plugins. */
|
|
9127
|
+
function readInstalledPlugins(harness, installScope, environment) {
|
|
9128
|
+
const storage = pluginStorage(harness, environment);
|
|
9129
|
+
const location = (kind) => storage.locations.find((l) => l.kind === kind)?.path;
|
|
9130
|
+
switch (harness) {
|
|
9131
|
+
case "claude-code": {
|
|
9132
|
+
const record = location("install-record");
|
|
9133
|
+
const raw = record ? readJson(record) : null;
|
|
9134
|
+
return raw === null ? [] : parseClaudeInstallRecord(raw, installScope);
|
|
9135
|
+
}
|
|
9136
|
+
case "copilot-cli": {
|
|
9137
|
+
const dir = location("installed-plugins");
|
|
9138
|
+
return dir && installScope.scope === "global" ? readCopilotInstalls(dir) : [];
|
|
9139
|
+
}
|
|
9140
|
+
case "codex": {
|
|
9141
|
+
const dir = location("plugin-cache");
|
|
9142
|
+
return dir && installScope.scope === "global" ? readCodexCache(dir) : [];
|
|
9143
|
+
}
|
|
9144
|
+
default: return [];
|
|
9145
|
+
}
|
|
9146
|
+
}
|
|
9147
|
+
/** `<dir>/<marketplace>/<plugin>/` for marketplace installs, `<dir>/_direct/<source-id>/` for
|
|
9148
|
+
* direct ones; the plugin's own manifest carries its name and version. */
|
|
9149
|
+
function readCopilotInstalls(dir) {
|
|
9150
|
+
return subdirs(dir).flatMap((marketplace) => subdirs(path.join(dir, marketplace)).map((folder) => {
|
|
9151
|
+
const root = path.join(dir, marketplace, folder);
|
|
9152
|
+
const manifest = readPluginManifest(root);
|
|
9153
|
+
return {
|
|
9154
|
+
name: typeof manifest["name"] === "string" ? manifest["name"] : folder,
|
|
9155
|
+
version: typeof manifest["version"] === "string" ? manifest["version"] : "unknown",
|
|
9156
|
+
root
|
|
9157
|
+
};
|
|
9158
|
+
}));
|
|
9159
|
+
}
|
|
9160
|
+
/** `<dir>/<marketplace>/<plugin>/<version>/`; the newest cached version is the installed one. */
|
|
9161
|
+
function readCodexCache(dir) {
|
|
9162
|
+
return subdirs(dir).flatMap((marketplace) => subdirs(path.join(dir, marketplace)).flatMap((name) => {
|
|
9163
|
+
const version = latestVersion(subdirs(path.join(dir, marketplace, name)));
|
|
9164
|
+
return version ? [{
|
|
9165
|
+
name,
|
|
9166
|
+
version,
|
|
9167
|
+
root: path.join(dir, marketplace, name, version)
|
|
9168
|
+
}] : [];
|
|
9169
|
+
}));
|
|
9170
|
+
}
|
|
9171
|
+
const PLUGIN_MANIFESTS = [
|
|
9172
|
+
"plugin.json",
|
|
9173
|
+
".github/plugin/plugin.json",
|
|
9174
|
+
".claude-plugin/plugin.json"
|
|
9175
|
+
];
|
|
9176
|
+
function readPluginManifest(root) {
|
|
9177
|
+
for (const file of PLUGIN_MANIFESTS) {
|
|
9178
|
+
const raw = readJson(path.join(root, file));
|
|
9179
|
+
if (typeof raw === "object" && raw !== null) return raw;
|
|
9180
|
+
}
|
|
9181
|
+
return {};
|
|
9182
|
+
}
|
|
9183
|
+
function readJson(filePath) {
|
|
9184
|
+
try {
|
|
9185
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
9186
|
+
} catch (err) {
|
|
9187
|
+
if (err.code === "ENOENT") return null;
|
|
9188
|
+
throw err;
|
|
9189
|
+
}
|
|
9190
|
+
}
|
|
9191
|
+
function subdirs(dir) {
|
|
9192
|
+
try {
|
|
9193
|
+
return fs.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
|
|
9194
|
+
} catch (err) {
|
|
9195
|
+
if (err.code === "ENOENT") return [];
|
|
9196
|
+
throw err;
|
|
8853
9197
|
}
|
|
8854
9198
|
}
|
|
8855
9199
|
//#endregion
|
|
@@ -8966,8 +9310,7 @@ function runPrepare(opts) {
|
|
|
8966
9310
|
//#region src/prepare/cli.ts
|
|
8967
9311
|
function prepareCommand() {
|
|
8968
9312
|
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
|
-
|
|
8970
|
-
if (!vendor) {
|
|
9313
|
+
if (!lookupVendor(loadRegistry(), vendorId) || !harnessIds.includes(vendorId)) {
|
|
8971
9314
|
process.stderr.write(`Unknown vendor: ${vendorId}\n`);
|
|
8972
9315
|
process.exit(1);
|
|
8973
9316
|
}
|
|
@@ -8976,12 +9319,16 @@ function prepareCommand() {
|
|
|
8976
9319
|
process.exit(1);
|
|
8977
9320
|
}
|
|
8978
9321
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8979
|
-
const
|
|
9322
|
+
const scope = opts.scope;
|
|
9323
|
+
const prepareFs = realPrepareFs(vendorId, {
|
|
9324
|
+
scope,
|
|
9325
|
+
projectRoot: opts.root ? path.resolve(opts.root) : void 0
|
|
9326
|
+
});
|
|
8980
9327
|
const pluginRoots = prepareFs.readPluginRoots();
|
|
8981
9328
|
const manifest = prepareFs.readManifest();
|
|
8982
9329
|
const { newActionCount } = runPrepare({
|
|
8983
9330
|
vendorId,
|
|
8984
|
-
scope
|
|
9331
|
+
scope,
|
|
8985
9332
|
fs: prepareFs,
|
|
8986
9333
|
now,
|
|
8987
9334
|
dryRun: opts.dryRun
|
|
@@ -9497,7 +9844,6 @@ function pluginCommand() {
|
|
|
9497
9844
|
program.addCommand(pluginCommand());
|
|
9498
9845
|
program.addCommand(cleanCommand());
|
|
9499
9846
|
program.addCommand(configCommand());
|
|
9500
|
-
program.addCommand(governanceCommand());
|
|
9501
9847
|
program.addCommand(marketplaceCommand());
|
|
9502
9848
|
program.addCommand(prepareCommand());
|
|
9503
9849
|
program.addCommand(publishCommand());
|
package/dist/data/vendors.json
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "0.11.0",
|
|
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",
|
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.
|
|
4
|
+
"version": "0.11.0",
|
|
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
|
-
###
|
|
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
|
|
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 `load-reference` skill in the `buddy-agent-harness`
|
|
114
|
+
plugin.
|
|
117
115
|
|
|
118
116
|
### Housekeeping
|
|
119
117
|
|
|
@@ -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/`, `
|
|
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
|