@hacksmith/doraval 0.2.29 → 0.2.35

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.
Files changed (2) hide show
  1. package/bin/doraval.js +2689 -849
  2. package/package.json +1 -1
package/bin/doraval.js CHANGED
@@ -599,7 +599,7 @@ var init_dist = __esm(() => {
599
599
  var require_package = __commonJS((exports, module) => {
600
600
  module.exports = {
601
601
  name: "@hacksmith/doraval",
602
- version: "0.2.29",
602
+ version: "0.2.35",
603
603
  author: "Saif",
604
604
  repository: {
605
605
  type: "git",
@@ -2662,12 +2662,64 @@ var init_sync = __esm(() => {
2662
2662
  });
2663
2663
  });
2664
2664
 
2665
+ // src/providers/spec.ts
2666
+ function getProviderSpec(id) {
2667
+ return PROVIDER_SPECS[id];
2668
+ }
2669
+ var PROVIDER_SPECS, supportedProviders;
2670
+ var init_spec = __esm(() => {
2671
+ PROVIDER_SPECS = {
2672
+ claude: {
2673
+ id: "claude",
2674
+ name: "Claude Code",
2675
+ manifestPath: ".claude-plugin/plugin.json",
2676
+ marketplacePath: ".claude-plugin/marketplace.json",
2677
+ mcpFilename: ".mcp.json",
2678
+ skillsField: "array-or-dir-string",
2679
+ sourceShape: "string",
2680
+ requiresInterface: false
2681
+ },
2682
+ codex: {
2683
+ id: "codex",
2684
+ name: "Codex",
2685
+ manifestPath: ".codex-plugin/plugin.json",
2686
+ marketplacePath: ".agents/plugins/marketplace.json",
2687
+ mcpFilename: ".mcp.json",
2688
+ skillsField: "directory-string",
2689
+ sourceShape: "object",
2690
+ requiresInterface: true
2691
+ },
2692
+ cursor: {
2693
+ id: "cursor",
2694
+ name: "Cursor",
2695
+ manifestPath: ".cursor-plugin/plugin.json",
2696
+ marketplacePath: ".cursor-plugin/marketplace.json",
2697
+ mcpFilename: "mcp.json",
2698
+ skillsField: "directory-string",
2699
+ sourceShape: "string",
2700
+ requiresInterface: false
2701
+ },
2702
+ copilot: {
2703
+ id: "copilot",
2704
+ name: "Copilot CLI",
2705
+ manifestPath: ".github/plugin/plugin.json",
2706
+ marketplacePath: ".github/plugin/marketplace.json",
2707
+ mcpFilename: ".mcp.json",
2708
+ skillsField: "array-of-paths",
2709
+ sourceShape: "string",
2710
+ requiresInterface: false
2711
+ }
2712
+ };
2713
+ supportedProviders = Object.keys(PROVIDER_SPECS);
2714
+ });
2715
+
2665
2716
  // src/cli/commands/claude/context.ts
2666
2717
  import { existsSync as existsSync8, readdirSync as readdirSync3 } from "fs";
2667
2718
  import { join as join7 } from "path";
2668
2719
  function detectContext(cwd = process.cwd()) {
2720
+ const claudeSpec = getProviderSpec("claude");
2669
2721
  const hasClaudeDir = existsSync8(join7(cwd, ".claude"));
2670
- const hasPluginManifest = existsSync8(join7(cwd, ".claude-plugin", "plugin.json"));
2722
+ const hasPluginManifest = existsSync8(join7(cwd, claudeSpec.manifestPath));
2671
2723
  let looseSkillFiles = [];
2672
2724
  try {
2673
2725
  const files = readdirSync3(cwd);
@@ -2689,7 +2741,9 @@ function detectContext(cwd = process.cwd()) {
2689
2741
  isEmpty
2690
2742
  };
2691
2743
  }
2692
- var init_context = () => {};
2744
+ var init_context = __esm(() => {
2745
+ init_spec();
2746
+ });
2693
2747
 
2694
2748
  // src/cli/commands/claude/new.ts
2695
2749
  var exports_new = {};
@@ -2698,7 +2752,7 @@ __export(exports_new, {
2698
2752
  default: () => new_default,
2699
2753
  decidePath: () => decidePath
2700
2754
  });
2701
- import { join as join8, basename as basename2 } from "path";
2755
+ import { join as join8, basename as basename2, dirname } from "path";
2702
2756
  import { mkdirSync as mkdirSync2, writeFileSync, existsSync as existsSync9 } from "fs";
2703
2757
  function decidePath(ctx, intent, providedName) {
2704
2758
  const rawName = providedName || "";
@@ -2748,13 +2802,15 @@ function scaffold(decision, ctx, migrateContent) {
2748
2802
  }
2749
2803
  if (path === "plugin") {
2750
2804
  const pluginName = basename2(targetDir);
2805
+ const claudeSpec = getProviderSpec("claude");
2806
+ const claudeManifestDir = dirname(claudeSpec.manifestPath);
2751
2807
  const pluginJson = {
2752
2808
  name: pluginName,
2753
2809
  description: "Scaffolded by doraval claude new",
2754
2810
  version: "0.1.0"
2755
2811
  };
2756
- mkdirSync2(join8(targetDir, ".claude-plugin"), { recursive: true });
2757
- writeFileSync(join8(targetDir, ".claude-plugin", "plugin.json"), JSON.stringify(pluginJson, null, 2));
2812
+ mkdirSync2(join8(targetDir, claudeManifestDir), { recursive: true });
2813
+ writeFileSync(join8(targetDir, claudeSpec.manifestPath), JSON.stringify(pluginJson, null, 2));
2758
2814
  const marketplaceJson = {
2759
2815
  name: pluginName,
2760
2816
  version: "0.1.0",
@@ -2817,6 +2873,7 @@ var init_new = __esm(() => {
2817
2873
  init_out();
2818
2874
  init_context();
2819
2875
  init_prompt();
2876
+ init_spec();
2820
2877
  import_picocolors10 = __toESM(require_picocolors(), 1);
2821
2878
  new_default = defineCommand({
2822
2879
  meta: {
@@ -2860,7 +2917,8 @@ var init_new = __esm(() => {
2860
2917
  const cmdName = decision.path === "plugin" ? `/${basename2(decision.targetDir)}:doraval` : "/my-skill";
2861
2918
  ui.info(` Command: ${cmdName}`);
2862
2919
  if (decision.path === "plugin") {
2863
- ui.info(` Claude: .claude-plugin/plugin.json`);
2920
+ const claudeSpec = getProviderSpec("claude");
2921
+ ui.info(` Claude: ${claudeSpec.manifestPath}`);
2864
2922
  ui.info(` Marketplace: marketplace.json (unified / cross-provider listings)`);
2865
2923
  }
2866
2924
  ui.info(` Test: claude --plugin-dir ${decision.targetDir} (or use normally for standalone)`);
@@ -2878,7 +2936,7 @@ var exports_bump = {};
2878
2936
  __export(exports_bump, {
2879
2937
  default: () => bump_default
2880
2938
  });
2881
- import { resolve as resolve4, join as join9, dirname, relative } from "path";
2939
+ import { resolve as resolve4, join as join9, dirname as dirname2, relative } from "path";
2882
2940
  import { existsSync as existsSync10, readFileSync, writeFileSync as writeFileSync2, readdirSync as readdirSync4, statSync } from "fs";
2883
2941
  function bumpVersion(current, type) {
2884
2942
  if (/^\d+\.\d+\.\d+$/.test(type))
@@ -2931,6 +2989,26 @@ function setVersion(obj, newVersion) {
2931
2989
  }
2932
2990
  return false;
2933
2991
  }
2992
+ function bumpPluginEntriesVersions(plugins, bumpType) {
2993
+ if (!Array.isArray(plugins))
2994
+ return 0;
2995
+ let changed = 0;
2996
+ for (const p of plugins) {
2997
+ if (p && typeof p === "object") {
2998
+ const currentVer = typeof p.version === "string" ? p.version : undefined;
2999
+ if (currentVer) {
3000
+ try {
3001
+ const nextVer = bumpVersion(currentVer, bumpType);
3002
+ if (currentVer !== nextVer) {
3003
+ p.version = nextVer;
3004
+ changed++;
3005
+ }
3006
+ } catch {}
3007
+ }
3008
+ }
3009
+ }
3010
+ return changed;
3011
+ }
2934
3012
  function walkForTargets(dir, maxDepth = 6, currentDepth = 0) {
2935
3013
  const results = [];
2936
3014
  if (currentDepth > maxDepth)
@@ -2954,9 +3032,9 @@ function walkForTargets(dir, maxDepth = 6, currentDepth = 0) {
2954
3032
  results.push(...sub);
2955
3033
  } else if (st.isFile()) {
2956
3034
  if (entry === "plugin.json") {
2957
- const parentDir = dirname(full);
3035
+ const parentDir = dirname2(full);
2958
3036
  const parentName = parentDir.split(/[/\\]/).pop();
2959
- if (parentName === ".claude-plugin" || parentName === ".codex-plugin" || parentName === ".cursor-plugin") {
3037
+ if (parentName === ".claude-plugin" || parentName === ".codex-plugin" || parentName === ".cursor-plugin" || parentName === ".github") {
2960
3038
  results.push({
2961
3039
  file: full,
2962
3040
  kind: "plugin",
@@ -2985,7 +3063,7 @@ var init_bump = __esm(() => {
2985
3063
  bump_default = defineCommand({
2986
3064
  meta: {
2987
3065
  name: "bump",
2988
- description: "Bump semver versions in plugin.json (manifests) and marketplace.json files (supports Claude, Codex, Cursor)"
3066
+ description: "Bump semver versions in plugin.json (manifests) and marketplace.json files (supports Claude, Codex, Cursor, Copilot)"
2989
3067
  },
2990
3068
  args: {
2991
3069
  type: {
@@ -3030,7 +3108,7 @@ var init_bump = __esm(() => {
3030
3108
  }
3031
3109
  ui.heading("doraval bump");
3032
3110
  ui.info(` scanning: ${root}`);
3033
- ui.info(` scope: ${scope} (use --only plugin or --only marketplace to narrow; Cursor metadata.version supported)`);
3111
+ ui.info(` scope: ${scope} (use --only plugin or --only marketplace to narrow; Cursor/Copilot metadata.version supported)`);
3034
3112
  const discovered = walkForTargets(root);
3035
3113
  let targets = discovered;
3036
3114
  if (scope === "plugin") {
@@ -3045,14 +3123,15 @@ var init_bump = __esm(() => {
3045
3123
  ui.info(" \u2022 **/.claude-plugin/plugin.json");
3046
3124
  ui.info(" \u2022 **/.codex-plugin/plugin.json");
3047
3125
  ui.info(" \u2022 **/.cursor-plugin/plugin.json (or marketplace.json)");
3048
- ui.info(" \u2022 **/marketplace.json (top-level version or metadata.version for Cursor)");
3126
+ ui.info(" \u2022 **/.github/plugin/plugin.json (or marketplace.json)");
3127
+ ui.info(" \u2022 **/marketplace.json (top-level/metadata.version + versions inside plugins[] for Cursor/Copilot)");
3049
3128
  ui.info("");
3050
3129
  ui.info(" Tip: run from inside a plugin directory, or pass a path that contains plugins/.");
3051
3130
  ui.info(" Examples:");
3052
3131
  ui.info(" dora bump minor");
3053
3132
  ui.info(" dora bump minor ./my-claude-plugin");
3054
3133
  ui.info(" dora bump --only plugin . # only the manifests");
3055
- ui.info(" dora bump --only marketplace ./marketplaces-root # includes Cursor metadata.version");
3134
+ ui.info(" dora bump --only marketplace ./marketplaces-root # bumps metadata.version + plugins[].version (Copilot/Cursor)");
3056
3135
  process.exit(1);
3057
3136
  }
3058
3137
  ui.info(` matched ${targets.length} file(s)`);
@@ -3072,18 +3151,33 @@ var init_bump = __esm(() => {
3072
3151
  process.exit(1);
3073
3152
  }
3074
3153
  const relPath = relative(root, t.file);
3075
- if (current === next) {
3154
+ const rootUnchanged = current === next;
3155
+ let innerChanged = 0;
3156
+ if (t.kind === "marketplace" && Array.isArray(json.plugins)) {
3157
+ innerChanged = bumpPluginEntriesVersions(json.plugins, rawType);
3158
+ }
3159
+ if (rootUnchanged && innerChanged === 0) {
3076
3160
  ui.dim(` \u2022 ${t.label} ${current || "(no version)"} (no change) [${relPath}]`);
3077
3161
  continue;
3078
3162
  }
3079
- const didUpdate = setVersion(json, next);
3080
- if (!didUpdate) {
3163
+ const didRootUpdate = setVersion(json, next);
3164
+ const didAnyUpdate = didRootUpdate || innerChanged > 0;
3165
+ if (!didAnyUpdate) {
3081
3166
  ui.warnItem(`skipped (could not locate version field to update): ${relPath}`);
3082
3167
  continue;
3083
3168
  }
3084
3169
  writeJson(t.file, json);
3085
- ui.success(`${t.label}: ${import_picocolors11.default.dim(current || "(none)")} \u2192 ${import_picocolors11.default.green(next)}`);
3170
+ if (didRootUpdate && current) {
3171
+ ui.success(`${t.label}: ${import_picocolors11.default.dim(current)} \u2192 ${import_picocolors11.default.green(next)}`);
3172
+ } else if (didRootUpdate) {
3173
+ ui.success(`${t.label}: ${import_picocolors11.default.green(next)}`);
3174
+ } else {
3175
+ ui.success(`${t.label} (no root version)`);
3176
+ }
3086
3177
  ui.info(` ${relPath}`);
3178
+ if (innerChanged > 0) {
3179
+ ui.info(` + bumped ${innerChanged} entry version(s) inside plugins[]`);
3180
+ }
3087
3181
  bumpedCount++;
3088
3182
  }
3089
3183
  ui.blank();
@@ -3102,9 +3196,10 @@ var init_bump = __esm(() => {
3102
3196
  import { existsSync as existsSync11, readdirSync as readdirSync5 } from "fs";
3103
3197
  import { join as join10 } from "path";
3104
3198
  function detectContext2(cwd = process.cwd()) {
3199
+ const codexSpec = getProviderSpec("codex");
3105
3200
  const hasCodexDir = existsSync11(join10(cwd, ".codex"));
3106
- const hasPluginManifest = existsSync11(join10(cwd, ".codex-plugin", "plugin.json"));
3107
- const hasMarketplace = existsSync11(join10(cwd, ".agents", "plugins", "marketplace.json")) || existsSync11(join10(cwd, ".codex-plugin", "marketplace.json"));
3201
+ const hasPluginManifest = existsSync11(join10(cwd, codexSpec.manifestPath));
3202
+ const hasMarketplace = existsSync11(join10(cwd, ".agents", "plugins", "marketplace.json")) || existsSync11(join10(cwd, codexSpec.manifestPath));
3108
3203
  let looseSkillFiles = [];
3109
3204
  try {
3110
3205
  const files = readdirSync5(cwd);
@@ -3127,7 +3222,9 @@ function detectContext2(cwd = process.cwd()) {
3127
3222
  isEmpty
3128
3223
  };
3129
3224
  }
3130
- var init_context2 = () => {};
3225
+ var init_context2 = __esm(() => {
3226
+ init_spec();
3227
+ });
3131
3228
 
3132
3229
  // src/cli/commands/codex/new.ts
3133
3230
  var exports_new2 = {};
@@ -3136,7 +3233,7 @@ __export(exports_new2, {
3136
3233
  default: () => new_default2,
3137
3234
  decidePath: () => decidePath2
3138
3235
  });
3139
- import { join as join11, basename as basename3 } from "path";
3236
+ import { join as join11, basename as basename3, dirname as dirname3 } from "path";
3140
3237
  import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3, existsSync as existsSync12 } from "fs";
3141
3238
  function decidePath2(ctx, intent, providedName) {
3142
3239
  const rawName = providedName || "";
@@ -3186,6 +3283,8 @@ function scaffold2(decision, ctx, migrateContent) {
3186
3283
  }
3187
3284
  if (path === "plugin") {
3188
3285
  const pluginName = basename3(targetDir);
3286
+ const codexSpec = getProviderSpec("codex");
3287
+ const codexManifestDir = dirname3(codexSpec.manifestPath);
3189
3288
  const pluginJson = {
3190
3289
  name: pluginName,
3191
3290
  version: "0.1.0",
@@ -3197,9 +3296,10 @@ function scaffold2(decision, ctx, migrateContent) {
3197
3296
  category: "Productivity"
3198
3297
  }
3199
3298
  };
3200
- mkdirSync3(join11(targetDir, ".codex-plugin"), { recursive: true });
3201
- writeFileSync3(join11(targetDir, ".codex-plugin", "plugin.json"), JSON.stringify(pluginJson, null, 2));
3202
- mkdirSync3(join11(targetDir, ".agents", "plugins"), { recursive: true });
3299
+ mkdirSync3(join11(targetDir, codexManifestDir), { recursive: true });
3300
+ writeFileSync3(join11(targetDir, codexSpec.manifestPath), JSON.stringify(pluginJson, null, 2));
3301
+ const marketplaceDir = dirname3(codexSpec.marketplacePath);
3302
+ mkdirSync3(join11(targetDir, marketplaceDir), { recursive: true });
3203
3303
  const marketplaceJson = {
3204
3304
  name: "local",
3205
3305
  interface: {
@@ -3220,7 +3320,7 @@ function scaffold2(decision, ctx, migrateContent) {
3220
3320
  }
3221
3321
  ]
3222
3322
  };
3223
- writeFileSync3(join11(targetDir, ".agents", "plugins", "marketplace.json"), JSON.stringify(marketplaceJson, null, 2));
3323
+ writeFileSync3(join11(targetDir, codexSpec.marketplacePath), JSON.stringify(marketplaceJson, null, 2));
3224
3324
  const demoSkillName = "doraval";
3225
3325
  mkdirSync3(join11(targetDir, "skills", demoSkillName), { recursive: true });
3226
3326
  let skillContent;
@@ -3279,6 +3379,7 @@ var init_new2 = __esm(() => {
3279
3379
  init_out();
3280
3380
  init_context2();
3281
3381
  init_prompt();
3382
+ init_spec();
3282
3383
  import_picocolors12 = __toESM(require_picocolors(), 1);
3283
3384
  new_default2 = defineCommand({
3284
3385
  meta: {
@@ -3336,176 +3437,1560 @@ var init_new2 = __esm(() => {
3336
3437
  });
3337
3438
  });
3338
3439
 
3339
- // src/validators/claude/skill.ts
3340
- import { existsSync as existsSync13 } from "fs";
3341
- import { resolve as resolve5 } from "path";
3342
- var claudeSkillValidator;
3343
- var init_skill = __esm(() => {
3344
- init_skill_validate();
3345
- claudeSkillValidator = {
3346
- id: "claude:skill",
3347
- provider: "claude",
3348
- name: "Claude Skill",
3349
- description: "Validates SKILL.md per current Claude Code spec: frontmatter (name/description relaxed to recommended; directory name usually provides the /command), body, supporting files, dynamic injection (!`cmd`), substitutions ($ARGUMENTS, ${CLAUDE_*}), and advanced fields (allowed-tools, context, disable-model-invocation, when_to_use, etc.)",
3350
- detect(dir) {
3351
- return existsSync13(resolve5(dir, "SKILL.md"));
3352
- },
3353
- async validate(dir, _opts) {
3354
- const loaded = await loadSkill(dir);
3355
- if (!loaded.ok) {
3356
- return {
3357
- errors: [loaded.error],
3358
- warnings: [],
3359
- passes: []
3360
- };
3361
- }
3362
- const { model, existingDirs } = loaded;
3363
- return validateSkillModel(model, { existingDirs: [...existingDirs] });
3364
- }
3440
+ // src/cli/commands/cursor/context.ts
3441
+ import { existsSync as existsSync13, readdirSync as readdirSync6 } from "fs";
3442
+ import { join as join12 } from "path";
3443
+ function detectContext3(cwd = process.cwd()) {
3444
+ const hasCursorDir = existsSync13(join12(cwd, ".cursor"));
3445
+ const hasPluginManifest = existsSync13(join12(cwd, ".cursor-plugin", "plugin.json"));
3446
+ let looseSkillFiles = [];
3447
+ try {
3448
+ const files = readdirSync6(cwd);
3449
+ looseSkillFiles = files.filter((f) => {
3450
+ if (!f.endsWith(".md") || f.startsWith("."))
3451
+ return false;
3452
+ const lower = f.toLowerCase();
3453
+ if (lower === "readme.md" || lower === "changelog.md" || lower === "license.md" || lower.includes("contributing"))
3454
+ return false;
3455
+ return lower.includes("skill") || lower === "skill.md";
3456
+ });
3457
+ } catch {}
3458
+ const isEmpty = !hasCursorDir && !hasPluginManifest && looseSkillFiles.length === 0;
3459
+ return {
3460
+ cwd,
3461
+ hasCursorDir,
3462
+ hasPluginManifest,
3463
+ looseSkillFiles,
3464
+ isEmpty
3365
3465
  };
3366
- });
3466
+ }
3467
+ var init_context3 = () => {};
3367
3468
 
3368
- // src/validators/claude/plugin.ts
3369
- import { existsSync as existsSync14, readdirSync as readdirSync6 } from "fs";
3370
- import { resolve as resolve6, join as join12 } from "path";
3371
- function levenshtein(a, b) {
3372
- if (a === b)
3373
- return 0;
3374
- const m = a.length, n = b.length;
3375
- if (m === 0)
3376
- return n;
3377
- if (n === 0)
3378
- return m;
3379
- const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
3380
- for (let i = 0;i <= m; i++) {
3381
- const row = dp[i];
3382
- row[0] = i;
3383
- }
3384
- for (let j = 0;j <= n; j++) {
3385
- const row = dp[0];
3386
- row[j] = j;
3387
- }
3388
- for (let i = 1;i <= m; i++) {
3389
- const row = dp[i];
3390
- const prev = dp[i - 1];
3391
- for (let j = 1;j <= n; j++) {
3392
- const cost = a[i - 1] === b[j - 1] ? 0 : 1;
3393
- row[j] = Math.min((prev[j] ?? 0) + 1, (row[j - 1] ?? 0) + 1, (prev[j - 1] ?? 0) + cost);
3469
+ // src/cli/commands/cursor/new.ts
3470
+ var exports_new3 = {};
3471
+ __export(exports_new3, {
3472
+ scaffold: () => scaffold3,
3473
+ default: () => new_default3,
3474
+ decidePath: () => decidePath3
3475
+ });
3476
+ import { join as join13, basename as basename4, dirname as dirname4 } from "path";
3477
+ import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4, existsSync as existsSync14 } from "fs";
3478
+ function decidePath3(ctx, intent, providedName) {
3479
+ const rawName = providedName || "";
3480
+ let decisionPath = "standalone";
3481
+ let targetDir = ctx.cwd;
3482
+ let shouldCreateDir = false;
3483
+ let migrateExisting = false;
3484
+ const useCurrentDirAsRoot = rawName === "." || rawName === basename4(ctx.cwd) || !rawName;
3485
+ if (intent === "distribute" || intent === "self-later" && ctx.looseSkillFiles.length > 0 && !ctx.hasPluginManifest) {
3486
+ decisionPath = "plugin";
3487
+ if (useCurrentDirAsRoot) {
3488
+ targetDir = ctx.cwd;
3489
+ shouldCreateDir = false;
3490
+ } else {
3491
+ targetDir = join13(ctx.cwd, rawName);
3492
+ shouldCreateDir = true;
3493
+ }
3494
+ migrateExisting = ctx.looseSkillFiles.length > 0;
3495
+ } else if (intent === "self-later" && !ctx.hasPluginManifest) {
3496
+ decisionPath = "plugin";
3497
+ if (useCurrentDirAsRoot) {
3498
+ targetDir = ctx.cwd;
3499
+ shouldCreateDir = false;
3500
+ } else {
3501
+ targetDir = join13(ctx.cwd, rawName);
3502
+ shouldCreateDir = true;
3503
+ }
3504
+ } else if (decisionPath === "standalone") {
3505
+ if (useCurrentDirAsRoot) {
3506
+ targetDir = ctx.cwd;
3507
+ shouldCreateDir = false;
3508
+ } else {
3509
+ targetDir = join13(ctx.cwd, rawName);
3510
+ shouldCreateDir = true;
3394
3511
  }
3395
3512
  }
3396
- return dp[m][n];
3513
+ return { path: decisionPath, targetDir, shouldCreateDir, migrateExisting };
3397
3514
  }
3398
- function suggestField(unknown) {
3399
- const lower = unknown.toLowerCase();
3400
- for (const k of KNOWN_FIELDS2) {
3401
- if (k.toLowerCase() === lower)
3402
- return k;
3403
- if (levenshtein(k.toLowerCase(), lower) <= 1)
3404
- return k;
3405
- if (k.toLowerCase().startsWith(lower.slice(0, 3)) && lower.length > 3)
3406
- return k;
3515
+ function scaffold3(decision, ctx, migrateContent) {
3516
+ const { targetDir, path, shouldCreateDir } = decision;
3517
+ if (existsSync14(targetDir) && shouldCreateDir) {
3518
+ ui.fail("Target already exists");
3519
+ process.exit(1);
3520
+ }
3521
+ if (shouldCreateDir) {
3522
+ mkdirSync4(targetDir, { recursive: true });
3523
+ }
3524
+ if (path === "plugin") {
3525
+ const pluginName = basename4(targetDir);
3526
+ const cursorSpec = getProviderSpec("cursor");
3527
+ const cursorManifestDir = dirname4(cursorSpec.manifestPath);
3528
+ const pluginJson = {
3529
+ name: pluginName,
3530
+ version: "0.1.0",
3531
+ description: "Scaffolded by doraval cursor new",
3532
+ skills: "./skills/",
3533
+ displayName: pluginName
3534
+ };
3535
+ mkdirSync4(join13(targetDir, cursorManifestDir), { recursive: true });
3536
+ writeFileSync4(join13(targetDir, cursorSpec.manifestPath), JSON.stringify(pluginJson, null, 2));
3537
+ const marketplaceDir = dirname4(cursorSpec.marketplacePath);
3538
+ mkdirSync4(join13(targetDir, marketplaceDir), { recursive: true });
3539
+ const marketplaceJson = {
3540
+ name: pluginName,
3541
+ version: "0.1.0",
3542
+ description: "Scaffolded by doraval cursor new",
3543
+ author: { name: "" },
3544
+ homepage: "",
3545
+ repository: "",
3546
+ license: "MIT",
3547
+ keywords: ["cursor", "skills", "plugin"]
3548
+ };
3549
+ writeFileSync4(join13(targetDir, cursorSpec.marketplacePath), JSON.stringify(marketplaceJson, null, 2));
3550
+ const demoSkillName = "doraval";
3551
+ mkdirSync4(join13(targetDir, "skills", demoSkillName), { recursive: true });
3552
+ let skillContent;
3553
+ if (migrateContent) {
3554
+ skillContent = migrateContent;
3555
+ } else {
3556
+ skillContent = `---
3557
+ name: ${demoSkillName}
3558
+ description: Use doraval to validate, measure drift, and judge skills and plugins. Use when authoring or reviewing context engineering artifacts for AI coding agents (works for Cursor too).
3559
+ ---
3560
+
3561
+ # Use Doraval (Cursor edition)
3562
+
3563
+ Doraval is the context engineering toolkit.
3564
+
3565
+ When you need to check a skill or Cursor plugin:
3566
+
3567
+ - Validate the current directory: \`doraval validate .\`
3568
+ - Validate one skill: \`doraval skill validate ./skills/${demoSkillName}/\`
3569
+ - Check for rubric drift: \`doraval skill drift ./skills/${demoSkillName}/\`
3570
+ - Get an AI quality judgment: \`doraval skill judge ./skills/${demoSkillName}/\`
3571
+
3572
+ Always run \`doraval validate\` before sharing or publishing a plugin.
3573
+
3574
+ This skill demonstrates a complete, self-referential example of using doraval inside a generated Cursor plugin.
3575
+
3576
+ To test in Cursor:
3577
+ 1. Open the plugin directory or add via marketplace.
3578
+ 2. The demo skill will be available.`;
3579
+ }
3580
+ writeFileSync4(join13(targetDir, "skills", demoSkillName, "SKILL.md"), skillContent);
3581
+ const readmePath = join13(targetDir, "README.md");
3582
+ if (!existsSync14(readmePath)) {
3583
+ writeFileSync4(readmePath, "# " + pluginName + `
3584
+
3585
+ Cursor plugin scaffolded by doraval.`);
3586
+ }
3587
+ } else {
3588
+ mkdirSync4(join13(targetDir, "skills", "doraval"), { recursive: true });
3589
+ const skillBody = migrateContent || `# My Skill
3590
+
3591
+ Basic starter for Cursor.`;
3592
+ writeFileSync4(join13(targetDir, "skills", "doraval", "SKILL.md"), `---
3593
+ name: doraval
3594
+ description: Starter (local skill)
3595
+ ---
3596
+
3597
+ ${skillBody}`);
3407
3598
  }
3408
- if (lower === "licence")
3409
- return "license";
3410
- if (lower === "dependancies" || lower === "deps")
3411
- return "dependencies";
3412
- if (lower === "mcp" || lower === "mcpservers")
3413
- return "mcpServers";
3414
- if (lower === "lsp")
3415
- return "lspServers";
3416
- if (lower === "outputstyles" || lower === "styles")
3417
- return "outputStyles";
3418
- if (lower === "userconfig")
3419
- return "userConfig";
3420
- return null;
3421
- }
3422
- function isRelativePathLike(v) {
3423
- if (typeof v !== "string")
3424
- return false;
3425
- return RELATIVE_PATH_REGEX.test(v) && !v.includes("..");
3426
3599
  }
3427
- var NAME_REGEX2, RELATIVE_PATH_REGEX, KNOWN_FIELDS2, REPLACES_DEFAULT, claudePluginValidator;
3428
- var init_plugin = __esm(() => {
3429
- NAME_REGEX2 = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
3430
- RELATIVE_PATH_REGEX = /^\.\//;
3431
- KNOWN_FIELDS2 = new Set([
3432
- "$schema",
3433
- "name",
3434
- "displayName",
3435
- "version",
3436
- "description",
3437
- "author",
3438
- "homepage",
3439
- "repository",
3440
- "license",
3441
- "keywords",
3442
- "defaultEnabled",
3443
- "skills",
3444
- "commands",
3445
- "agents",
3446
- "hooks",
3447
- "mcpServers",
3448
- "outputStyles",
3449
- "lspServers",
3450
- "experimental",
3451
- "userConfig",
3452
- "channels",
3453
- "dependencies"
3454
- ]);
3455
- REPLACES_DEFAULT = new Set(["commands", "agents", "outputStyles", "lspServers"]);
3456
- claudePluginValidator = {
3457
- id: "claude:plugin",
3458
- provider: "claude",
3459
- name: "Claude Plugin",
3460
- description: "Validates .claude-plugin/plugin.json manifest (complete schema per Plugins reference), component path rules (replace vs augment), .claude-plugin/ purity, default dirs, single-root-skill layout, unrecognized fields + suggestions, and structure",
3461
- detect(dir) {
3462
- return existsSync14(resolve6(dir, ".claude-plugin", "plugin.json"));
3600
+ var import_picocolors13, new_default3;
3601
+ var init_new3 = __esm(() => {
3602
+ init_dist();
3603
+ init_out();
3604
+ init_context3();
3605
+ init_prompt();
3606
+ init_spec();
3607
+ import_picocolors13 = __toESM(require_picocolors(), 1);
3608
+ new_default3 = defineCommand({
3609
+ meta: {
3610
+ name: "new",
3611
+ description: "Create a new skill or plugin following Cursor packaging rules"
3463
3612
  },
3464
- async validate(dir, _opts) {
3465
- const errors = [];
3466
- const warnings = [];
3467
- const passes = [];
3468
- const manifestPath = resolve6(dir, ".claude-plugin", "plugin.json");
3469
- const dotClaudePluginDir = resolve6(dir, ".claude-plugin");
3470
- let manifest;
3471
- try {
3472
- const raw = await Bun.file(manifestPath).text();
3473
- manifest = JSON.parse(raw);
3474
- passes.push(".claude-plugin/plugin.json is valid JSON");
3475
- } catch {
3476
- errors.push(".claude-plugin/plugin.json is missing or invalid JSON");
3477
- return { errors, warnings, passes };
3478
- }
3479
- try {
3480
- const entries = readdirSync6(dotClaudePluginDir);
3481
- const unexpected = entries.filter((e) => e !== "plugin.json");
3613
+ args: {
3614
+ name: {
3615
+ type: "positional",
3616
+ description: "Optional name for the skill or plugin",
3617
+ required: false
3618
+ },
3619
+ yes: {
3620
+ type: "boolean",
3621
+ description: "Skip interactive prompts (use defaults and flags)",
3622
+ default: false
3623
+ },
3624
+ intent: {
3625
+ type: "string",
3626
+ description: 'Intent: "self" | "self-later" | "distribute"',
3627
+ required: false
3628
+ }
3629
+ },
3630
+ run({ args }) {
3631
+ ui.heading("doraval cursor new \u2014 Context-aware scaffolding");
3632
+ const ctx = detectContext3();
3633
+ let intent = args.intent || "self-later";
3634
+ if (!args.yes) {
3635
+ const ans = prompt(" Intent (self | self-later | distribute)", intent);
3636
+ intent = ans || intent;
3637
+ }
3638
+ const decision = decidePath3(ctx, intent, args.name);
3639
+ ui.info(` Decision: path=${decision.path}, target=${decision.targetDir}`);
3640
+ let migrateContent;
3641
+ if (decision.migrateExisting && !args.yes) {
3642
+ migrateContent = "Content from your existing SKILL.md (user-confirmed).";
3643
+ }
3644
+ scaffold3(decision, ctx, migrateContent);
3645
+ ui.write(`
3646
+ ${import_picocolors13.default.green("\u2713")} Created ${decision.path} at ${import_picocolors13.default.bold(decision.targetDir)}`);
3647
+ const cmdName = decision.path === "plugin" ? `/${basename4(decision.targetDir)}:doraval` : "/doraval (local skill)";
3648
+ ui.info(` Command: ${cmdName}`);
3649
+ if (decision.path === "plugin") {
3650
+ ui.info(` Cursor manifest: .cursor-plugin/plugin.json`);
3651
+ ui.info(` Marketplace catalog: .cursor-plugin/marketplace.json`);
3652
+ }
3653
+ ui.info(` Test (local): add the plugin dir in Cursor settings or use local skills`);
3654
+ ui.info(` Validate: doraval validate ${decision.targetDir}`);
3655
+ if (decision.path === "plugin" && decision.migrateExisting) {
3656
+ ui.info(" (Existing content migrated where confirmed.)");
3657
+ }
3658
+ process.exit(0);
3659
+ }
3660
+ });
3661
+ });
3662
+
3663
+ // src/cli/commands/copilot/context.ts
3664
+ import { existsSync as existsSync15, readdirSync as readdirSync7 } from "fs";
3665
+ import { join as join14 } from "path";
3666
+ function detectContext4(cwd = process.cwd()) {
3667
+ const hasGithubDir = existsSync15(join14(cwd, ".github"));
3668
+ const hasPluginManifest = existsSync15(join14(cwd, ".github", "plugin", "plugin.json"));
3669
+ let looseSkillFiles = [];
3670
+ try {
3671
+ const files = readdirSync7(cwd);
3672
+ looseSkillFiles = files.filter((f) => {
3673
+ if (!f.endsWith(".md") || f.startsWith("."))
3674
+ return false;
3675
+ const lower = f.toLowerCase();
3676
+ if (lower === "readme.md" || lower === "changelog.md" || lower === "license.md" || lower.includes("contributing"))
3677
+ return false;
3678
+ return lower.includes("skill") || lower === "skill.md";
3679
+ });
3680
+ } catch {}
3681
+ const isEmpty = !hasGithubDir && !hasPluginManifest && looseSkillFiles.length === 0;
3682
+ return {
3683
+ cwd,
3684
+ hasGithubDir,
3685
+ hasPluginManifest,
3686
+ looseSkillFiles,
3687
+ isEmpty
3688
+ };
3689
+ }
3690
+ var init_context4 = () => {};
3691
+
3692
+ // src/cli/commands/copilot/new.ts
3693
+ var exports_new4 = {};
3694
+ __export(exports_new4, {
3695
+ scaffold: () => scaffold4,
3696
+ default: () => new_default4,
3697
+ decidePath: () => decidePath4
3698
+ });
3699
+ import { join as join15, basename as basename5, dirname as dirname5 } from "path";
3700
+ import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync5, existsSync as existsSync16 } from "fs";
3701
+ function decidePath4(ctx, intent, providedName) {
3702
+ const rawName = providedName || "";
3703
+ let decisionPath = "standalone";
3704
+ let targetDir = ctx.cwd;
3705
+ let shouldCreateDir = false;
3706
+ let migrateExisting = false;
3707
+ const useCurrentDirAsRoot = rawName === "." || rawName === basename5(ctx.cwd) || !rawName;
3708
+ if (intent === "distribute" || intent === "self-later" && ctx.looseSkillFiles.length > 0 && !ctx.hasPluginManifest) {
3709
+ decisionPath = "plugin";
3710
+ if (useCurrentDirAsRoot) {
3711
+ targetDir = ctx.cwd;
3712
+ shouldCreateDir = false;
3713
+ } else {
3714
+ targetDir = join15(ctx.cwd, rawName);
3715
+ shouldCreateDir = true;
3716
+ }
3717
+ migrateExisting = ctx.looseSkillFiles.length > 0;
3718
+ } else if (intent === "self-later" && !ctx.hasPluginManifest) {
3719
+ decisionPath = "plugin";
3720
+ if (useCurrentDirAsRoot) {
3721
+ targetDir = ctx.cwd;
3722
+ shouldCreateDir = false;
3723
+ } else {
3724
+ targetDir = join15(ctx.cwd, rawName);
3725
+ shouldCreateDir = true;
3726
+ }
3727
+ } else if (decisionPath === "standalone") {
3728
+ if (useCurrentDirAsRoot) {
3729
+ targetDir = ctx.cwd;
3730
+ shouldCreateDir = false;
3731
+ } else {
3732
+ targetDir = join15(ctx.cwd, rawName);
3733
+ shouldCreateDir = true;
3734
+ }
3735
+ }
3736
+ return { path: decisionPath, targetDir, shouldCreateDir, migrateExisting };
3737
+ }
3738
+ function scaffold4(decision, ctx, migrateContent) {
3739
+ const { targetDir, path, shouldCreateDir } = decision;
3740
+ if (existsSync16(targetDir) && shouldCreateDir) {
3741
+ ui.fail("Target already exists");
3742
+ process.exit(1);
3743
+ }
3744
+ if (shouldCreateDir) {
3745
+ mkdirSync5(targetDir, { recursive: true });
3746
+ }
3747
+ if (path === "plugin") {
3748
+ const pluginName = basename5(targetDir);
3749
+ const copilotSpec = getProviderSpec("copilot");
3750
+ const copilotManifestDir = dirname5(copilotSpec.manifestPath);
3751
+ const pluginJson = {
3752
+ name: pluginName,
3753
+ version: "0.1.0",
3754
+ description: "Scaffolded by doraval copilot new",
3755
+ skills: ["./skills/doraval"],
3756
+ displayName: pluginName
3757
+ };
3758
+ mkdirSync5(join15(targetDir, copilotManifestDir), { recursive: true });
3759
+ writeFileSync5(join15(targetDir, copilotSpec.manifestPath), JSON.stringify(pluginJson, null, 2));
3760
+ const marketplaceDir = dirname5(copilotSpec.marketplacePath);
3761
+ mkdirSync5(join15(targetDir, marketplaceDir), { recursive: true });
3762
+ const marketplaceJson = {
3763
+ name: "local",
3764
+ plugins: [
3765
+ {
3766
+ name: pluginName,
3767
+ source: {
3768
+ source: "local",
3769
+ path: "."
3770
+ }
3771
+ }
3772
+ ]
3773
+ };
3774
+ writeFileSync5(join15(targetDir, copilotSpec.marketplacePath), JSON.stringify(marketplaceJson, null, 2));
3775
+ const demoSkillName = "doraval";
3776
+ mkdirSync5(join15(targetDir, "skills", demoSkillName), { recursive: true });
3777
+ let skillContent;
3778
+ if (migrateContent) {
3779
+ skillContent = migrateContent;
3780
+ } else {
3781
+ skillContent = `---
3782
+ name: ${demoSkillName}
3783
+ description: Use doraval to validate, measure drift, and judge skills and plugins. Use when authoring or reviewing context engineering artifacts for AI coding agents (works for Copilot too).
3784
+ ---
3785
+
3786
+ # Use Doraval (Copilot edition)
3787
+
3788
+ Doraval is the context engineering toolkit.
3789
+
3790
+ When you need to check a skill or Copilot plugin:
3791
+
3792
+ - Validate the current directory: \`doraval validate .\`
3793
+ - Validate one skill: \`doraval skill validate ./skills/${demoSkillName}/\`
3794
+ - Check for rubric drift: \`doraval skill drift ./skills/${demoSkillName}/\`
3795
+ - Get an AI quality judgment: \`doraval skill judge ./skills/${demoSkillName}/\`
3796
+
3797
+ Always run \`doraval validate\` before sharing or publishing a plugin.
3798
+
3799
+ This skill demonstrates a complete, self-referential example of using doraval inside a generated Copilot plugin.
3800
+
3801
+ To test in Copilot:
3802
+ 1. Configure the .github/plugin as local source.
3803
+ 2. Restart/reload and invoke the skill.`;
3804
+ }
3805
+ writeFileSync5(join15(targetDir, "skills", demoSkillName, "SKILL.md"), skillContent);
3806
+ const readmePath = join15(targetDir, "README.md");
3807
+ if (!existsSync16(readmePath)) {
3808
+ writeFileSync5(readmePath, "# " + pluginName + `
3809
+
3810
+ Copilot plugin scaffolded by doraval.`);
3811
+ }
3812
+ } else {
3813
+ mkdirSync5(join15(targetDir, "skills", "doraval"), { recursive: true });
3814
+ const skillBody = migrateContent || `# My Skill
3815
+
3816
+ Basic starter for Copilot.`;
3817
+ writeFileSync5(join15(targetDir, "skills", "doraval", "SKILL.md"), `---
3818
+ name: doraval
3819
+ description: Starter (local skill)
3820
+ ---
3821
+
3822
+ ${skillBody}`);
3823
+ }
3824
+ }
3825
+ var import_picocolors14, new_default4;
3826
+ var init_new4 = __esm(() => {
3827
+ init_dist();
3828
+ init_out();
3829
+ init_context4();
3830
+ init_prompt();
3831
+ init_spec();
3832
+ import_picocolors14 = __toESM(require_picocolors(), 1);
3833
+ new_default4 = defineCommand({
3834
+ meta: {
3835
+ name: "new",
3836
+ description: "Create a new skill or plugin following Copilot packaging rules"
3837
+ },
3838
+ args: {
3839
+ name: {
3840
+ type: "positional",
3841
+ description: "Optional name for the skill or plugin",
3842
+ required: false
3843
+ },
3844
+ yes: {
3845
+ type: "boolean",
3846
+ description: "Skip interactive prompts (use defaults and flags)",
3847
+ default: false
3848
+ },
3849
+ intent: {
3850
+ type: "string",
3851
+ description: 'Intent: "self" | "self-later" | "distribute"',
3852
+ required: false
3853
+ }
3854
+ },
3855
+ run({ args }) {
3856
+ ui.heading("doraval copilot new \u2014 Context-aware scaffolding");
3857
+ const ctx = detectContext4();
3858
+ let intent = args.intent || "self-later";
3859
+ if (!args.yes) {
3860
+ const ans = prompt(" Intent (self | self-later | distribute)", intent);
3861
+ intent = ans || intent;
3862
+ }
3863
+ const decision = decidePath4(ctx, intent, args.name);
3864
+ ui.info(` Decision: path=${decision.path}, target=${decision.targetDir}`);
3865
+ let migrateContent;
3866
+ if (decision.migrateExisting && !args.yes) {
3867
+ migrateContent = "Content from your existing SKILL.md (user-confirmed).";
3868
+ }
3869
+ scaffold4(decision, ctx, migrateContent);
3870
+ ui.write(`
3871
+ ${import_picocolors14.default.green("\u2713")} Created ${decision.path} at ${import_picocolors14.default.bold(decision.targetDir)}`);
3872
+ const cmdName = decision.path === "plugin" ? `/${basename5(decision.targetDir)}:doraval` : "/doraval (local skill)";
3873
+ ui.info(` Command: ${cmdName}`);
3874
+ if (decision.path === "plugin") {
3875
+ ui.info(` Copilot manifest: .github/plugin/plugin.json`);
3876
+ ui.info(` Marketplace catalog: .github/plugin/marketplace.json`);
3877
+ }
3878
+ ui.info(` Test (local): configure local plugin source in Copilot and reload`);
3879
+ ui.info(` Validate: doraval validate ${decision.targetDir}`);
3880
+ if (decision.path === "plugin" && decision.migrateExisting) {
3881
+ ui.info(" (Existing content migrated where confirmed.)");
3882
+ }
3883
+ process.exit(0);
3884
+ }
3885
+ });
3886
+ });
3887
+
3888
+ // src/validators/claude/skill.ts
3889
+ import { existsSync as existsSync17 } from "fs";
3890
+ import { resolve as resolve5 } from "path";
3891
+ var claudeSkillValidator;
3892
+ var init_skill = __esm(() => {
3893
+ init_skill_validate();
3894
+ claudeSkillValidator = {
3895
+ id: "claude:skill",
3896
+ provider: "claude",
3897
+ name: "Claude Skill",
3898
+ description: "Validates SKILL.md per current Claude Code spec: frontmatter (name/description relaxed to recommended; directory name usually provides the /command), body, supporting files, dynamic injection (!`cmd`), substitutions ($ARGUMENTS, ${CLAUDE_*}), and advanced fields (allowed-tools, context, disable-model-invocation, when_to_use, etc.)",
3899
+ detect(dir) {
3900
+ return existsSync17(resolve5(dir, "SKILL.md"));
3901
+ },
3902
+ async validate(dir, _opts) {
3903
+ const loaded = await loadSkill(dir);
3904
+ if (!loaded.ok) {
3905
+ return {
3906
+ errors: [loaded.error],
3907
+ warnings: [],
3908
+ passes: []
3909
+ };
3910
+ }
3911
+ const { model, existingDirs } = loaded;
3912
+ return validateSkillModel(model, { existingDirs: [...existingDirs] });
3913
+ }
3914
+ };
3915
+ });
3916
+
3917
+ // src/validators/claude/plugin.ts
3918
+ import { existsSync as existsSync18, readdirSync as readdirSync8 } from "fs";
3919
+ import { resolve as resolve6, join as join16 } from "path";
3920
+ function levenshtein(a, b) {
3921
+ if (a === b)
3922
+ return 0;
3923
+ const m = a.length, n = b.length;
3924
+ if (m === 0)
3925
+ return n;
3926
+ if (n === 0)
3927
+ return m;
3928
+ const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
3929
+ for (let i = 0;i <= m; i++) {
3930
+ const row = dp[i];
3931
+ row[0] = i;
3932
+ }
3933
+ for (let j = 0;j <= n; j++) {
3934
+ const row = dp[0];
3935
+ row[j] = j;
3936
+ }
3937
+ for (let i = 1;i <= m; i++) {
3938
+ const row = dp[i];
3939
+ const prev = dp[i - 1];
3940
+ for (let j = 1;j <= n; j++) {
3941
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
3942
+ row[j] = Math.min((prev[j] ?? 0) + 1, (row[j - 1] ?? 0) + 1, (prev[j - 1] ?? 0) + cost);
3943
+ }
3944
+ }
3945
+ return dp[m][n];
3946
+ }
3947
+ function suggestField(unknown) {
3948
+ const lower = unknown.toLowerCase();
3949
+ for (const k of KNOWN_FIELDS2) {
3950
+ if (k.toLowerCase() === lower)
3951
+ return k;
3952
+ if (levenshtein(k.toLowerCase(), lower) <= 1)
3953
+ return k;
3954
+ if (k.toLowerCase().startsWith(lower.slice(0, 3)) && lower.length > 3)
3955
+ return k;
3956
+ }
3957
+ if (lower === "licence")
3958
+ return "license";
3959
+ if (lower === "dependancies" || lower === "deps")
3960
+ return "dependencies";
3961
+ if (lower === "mcp" || lower === "mcpservers")
3962
+ return "mcpServers";
3963
+ if (lower === "lsp")
3964
+ return "lspServers";
3965
+ if (lower === "outputstyles" || lower === "styles")
3966
+ return "outputStyles";
3967
+ if (lower === "userconfig")
3968
+ return "userConfig";
3969
+ return null;
3970
+ }
3971
+ function isRelativePathLike(v) {
3972
+ if (typeof v !== "string")
3973
+ return false;
3974
+ return RELATIVE_PATH_REGEX.test(v) && !v.includes("..");
3975
+ }
3976
+ var NAME_REGEX2, RELATIVE_PATH_REGEX, KNOWN_FIELDS2, REPLACES_DEFAULT, claudePluginValidator;
3977
+ var init_plugin = __esm(() => {
3978
+ NAME_REGEX2 = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
3979
+ RELATIVE_PATH_REGEX = /^\.\//;
3980
+ KNOWN_FIELDS2 = new Set([
3981
+ "$schema",
3982
+ "name",
3983
+ "displayName",
3984
+ "version",
3985
+ "description",
3986
+ "author",
3987
+ "homepage",
3988
+ "repository",
3989
+ "license",
3990
+ "keywords",
3991
+ "defaultEnabled",
3992
+ "skills",
3993
+ "commands",
3994
+ "agents",
3995
+ "hooks",
3996
+ "mcpServers",
3997
+ "outputStyles",
3998
+ "lspServers",
3999
+ "experimental",
4000
+ "userConfig",
4001
+ "channels",
4002
+ "dependencies"
4003
+ ]);
4004
+ REPLACES_DEFAULT = new Set(["commands", "agents", "outputStyles", "lspServers"]);
4005
+ claudePluginValidator = {
4006
+ id: "claude:plugin",
4007
+ provider: "claude",
4008
+ name: "Claude Plugin",
4009
+ description: "Validates .claude-plugin/plugin.json manifest (complete schema per Plugins reference), component path rules (replace vs augment), .claude-plugin/ purity, default dirs, single-root-skill layout, unrecognized fields + suggestions, and structure",
4010
+ detect(dir) {
4011
+ return existsSync18(resolve6(dir, ".claude-plugin", "plugin.json"));
4012
+ },
4013
+ async validate(dir, _opts) {
4014
+ const errors = [];
4015
+ const warnings = [];
4016
+ const passes = [];
4017
+ const manifestPath = resolve6(dir, ".claude-plugin", "plugin.json");
4018
+ const dotClaudePluginDir = resolve6(dir, ".claude-plugin");
4019
+ let manifest;
4020
+ try {
4021
+ const raw = await Bun.file(manifestPath).text();
4022
+ manifest = JSON.parse(raw);
4023
+ passes.push(".claude-plugin/plugin.json is valid JSON");
4024
+ } catch {
4025
+ errors.push(".claude-plugin/plugin.json is missing or invalid JSON");
4026
+ return { errors, warnings, passes };
4027
+ }
4028
+ try {
4029
+ const entries = readdirSync8(dotClaudePluginDir);
4030
+ const unexpected = entries.filter((e) => e !== "plugin.json");
3482
4031
  if (unexpected.length > 0) {
3483
4032
  for (const e of unexpected) {
3484
4033
  warnings.push(`Unexpected item "${e}" inside .claude-plugin/ \u2014 only plugin.json belongs here. Move component directories and files (skills/, commands/, agents/, hooks/, .mcp.json etc.) to the plugin root.`);
3485
4034
  }
3486
- } else if (entries.length === 1) {
3487
- passes.push(".claude-plugin/ contains only plugin.json (correct layout)");
4035
+ } else if (entries.length === 1) {
4036
+ passes.push(".claude-plugin/ contains only plugin.json (correct layout)");
4037
+ }
4038
+ } catch {}
4039
+ if (!manifest.name) {
4040
+ errors.push('Missing required field: "name"');
4041
+ } else {
4042
+ const name = String(manifest.name);
4043
+ if (!NAME_REGEX2.test(name)) {
4044
+ errors.push(`Invalid name format: "${name}" \u2014 must be kebab-case (a-z, 0-9, hyphens)`);
4045
+ } else {
4046
+ passes.push(`name: "${name}"`);
4047
+ }
4048
+ }
4049
+ if (manifest.version !== undefined) {
4050
+ const v = String(manifest.version);
4051
+ if (!/^\d+\.\d+\.\d+/.test(v)) {
4052
+ errors.push(`Invalid version format: "${v}" \u2014 must look like semver (MAJOR.MINOR.PATCH) when using explicit versioning`);
4053
+ } else {
4054
+ passes.push(`version: "${v}" (explicit \u2014 bump on every release to publish updates)`);
4055
+ }
4056
+ } else {
4057
+ passes.push("version omitted (git commit SHA used as version key \u2014 every commit becomes an available update)");
4058
+ }
4059
+ if (manifest.description !== undefined) {
4060
+ const desc = String(manifest.description);
4061
+ if (desc.length < 10) {
4062
+ warnings.push(`Description is very short (${desc.length} chars) \u2014 50-200 chars recommended`);
4063
+ } else {
4064
+ passes.push("description field present");
4065
+ }
4066
+ } else {
4067
+ warnings.push('Missing "description" (recommended for UI, marketplace listings, and auto-discovery)');
4068
+ }
4069
+ if (manifest.displayName !== undefined) {
4070
+ passes.push(`displayName: "${manifest.displayName}" (human UI label; falls back to name)`);
4071
+ }
4072
+ if (manifest.author !== undefined) {
4073
+ const a = manifest.author;
4074
+ if (a && typeof a === "object" && a.name) {
4075
+ passes.push("author present");
4076
+ } else {
4077
+ warnings.push('author should be an object like {"name": "...", "email?": "..."}');
4078
+ }
4079
+ }
4080
+ if (manifest.license !== undefined) {
4081
+ passes.push(`license: "${manifest.license}"`);
4082
+ }
4083
+ if (manifest.keywords !== undefined) {
4084
+ if (Array.isArray(manifest.keywords)) {
4085
+ passes.push(`keywords: [${manifest.keywords.join(", ")}]`);
4086
+ } else {
4087
+ errors.push("keywords must be an array of strings");
4088
+ }
4089
+ }
4090
+ if (manifest.defaultEnabled !== undefined) {
4091
+ passes.push(`defaultEnabled: ${manifest.defaultEnabled}`);
4092
+ }
4093
+ if (manifest.homepage)
4094
+ passes.push("homepage present");
4095
+ if (manifest.repository)
4096
+ passes.push("repository present");
4097
+ const unknown = Object.keys(manifest).filter((k) => !KNOWN_FIELDS2.has(k));
4098
+ for (const k of unknown) {
4099
+ const sug = suggestField(k);
4100
+ const hint = sug ? ` (did you mean "${sug}"?)` : "";
4101
+ warnings.push(`Unrecognized top-level field "${k}"${hint} \u2014 will be ignored at runtime (allowed for cross-tool manifest compatibility).`);
4102
+ }
4103
+ const handleField = (field, val) => {
4104
+ if (val === undefined || val === null)
4105
+ return;
4106
+ if (isRelativePathLike(val) || Array.isArray(val) && val.every(isRelativePathLike)) {
4107
+ const arr = Array.isArray(val) ? val : [val];
4108
+ for (const p of arr) {
4109
+ const s = String(p);
4110
+ if (!RELATIVE_PATH_REGEX.test(s)) {
4111
+ errors.push(`${field}: path "${s}" must start with "./"`);
4112
+ } else if (s.includes("..")) {
4113
+ errors.push(`${field}: path "${s}" must not use ".." (paths are confined to the plugin tree after cache copy)`);
4114
+ } else if (existsSync18(resolve6(dir, s))) {
4115
+ passes.push(`${field}: path "${s}" exists`);
4116
+ } else {
4117
+ warnings.push(`${field}: path "${s}" does not exist on disk`);
4118
+ }
4119
+ }
4120
+ if (field === "skills") {
4121
+ passes.push(`${field}: augments the default skills/ (both are scanned)`);
4122
+ } else if (REPLACES_DEFAULT.has(field)) {
4123
+ passes.push(`${field}: custom path replaces default ${field}/ scan`);
4124
+ } else {
4125
+ passes.push(`${field}: custom path or config (merge rules apply)`);
4126
+ }
4127
+ } else if (typeof val === "object") {
4128
+ passes.push(`${field}: inline ${field} config present`);
4129
+ }
4130
+ };
4131
+ ["skills", "commands", "agents", "hooks", "mcpServers", "outputStyles", "lspServers"].forEach((f) => {
4132
+ if (manifest[f] !== undefined)
4133
+ handleField(f, manifest[f]);
4134
+ });
4135
+ if (manifest.experimental && typeof manifest.experimental === "object") {
4136
+ const exp = manifest.experimental;
4137
+ if (exp.themes !== undefined)
4138
+ handleField("experimental.themes", exp.themes);
4139
+ if (exp.monitors !== undefined)
4140
+ handleField("experimental.monitors", exp.monitors);
4141
+ passes.push("experimental section present (themes and monitors are experimental components)");
4142
+ }
4143
+ if (manifest.userConfig && typeof manifest.userConfig === "object") {
4144
+ const keys = Object.keys(manifest.userConfig);
4145
+ passes.push(`userConfig: ${keys.length} user-configurable value(s) declared`);
4146
+ for (const k of keys) {
4147
+ const opt = manifest.userConfig[k];
4148
+ if (!opt || !opt.type || !opt.title) {
4149
+ warnings.push(`userConfig.${k} is missing required "type" and/or "title"`);
4150
+ }
4151
+ }
4152
+ }
4153
+ if (Array.isArray(manifest.channels)) {
4154
+ passes.push(`channels: ${manifest.channels.length} channel(s) (each binds to an mcpServer)`);
4155
+ manifest.channels.forEach((ch, i) => {
4156
+ if (!ch?.server)
4157
+ warnings.push(`channels[${i}]: "server" is required and must match an mcpServers key`);
4158
+ });
4159
+ }
4160
+ if (Array.isArray(manifest.dependencies)) {
4161
+ passes.push(`dependencies: declares ${manifest.dependencies.length} plugin dependency/ies`);
4162
+ }
4163
+ const skillsDir = resolve6(dir, "skills");
4164
+ if (existsSync18(skillsDir)) {
4165
+ const entries = readdirSync8(skillsDir, { withFileTypes: true }).filter((e) => e.isDirectory());
4166
+ for (const e of entries) {
4167
+ const md = join16(skillsDir, e.name, "SKILL.md");
4168
+ if (existsSync18(md)) {
4169
+ passes.push(`skills/${e.name}/SKILL.md exists`);
4170
+ } else {
4171
+ errors.push(`skills/${e.name}/ is missing SKILL.md`);
4172
+ }
4173
+ }
4174
+ if (manifest.skills !== undefined) {
4175
+ warnings.push('Default skills/ dir co-exists with manifest "skills" \u2014 manifest path is authoritative; default folder ignored for loading');
4176
+ }
4177
+ }
4178
+ const commandsDir = resolve6(dir, "commands");
4179
+ if (existsSync18(commandsDir)) {
4180
+ const mds = readdirSync8(commandsDir).filter((f) => f.endsWith(".md"));
4181
+ if (mds.length) {
4182
+ passes.push(`commands/ has ${mds.length} .md file(s)`);
4183
+ }
4184
+ if (manifest.commands !== undefined) {
4185
+ warnings.push('commands/ co-exists with manifest "commands" \u2014 manifest replaces default (dir ignored)');
4186
+ }
4187
+ }
4188
+ const agentsDir = resolve6(dir, "agents");
4189
+ if (existsSync18(agentsDir)) {
4190
+ const mds = readdirSync8(agentsDir).filter((f) => f.endsWith(".md"));
4191
+ if (mds.length) {
4192
+ passes.push(`agents/ has ${mds.length} .md file(s)`);
4193
+ }
4194
+ if (manifest.agents !== undefined) {
4195
+ warnings.push('agents/ co-exists with manifest "agents" \u2014 manifest replaces default (dir ignored)');
4196
+ }
4197
+ }
4198
+ if (existsSync18(resolve6(dir, "output-styles"))) {
4199
+ passes.push("output-styles/ directory present");
4200
+ if (manifest.outputStyles)
4201
+ warnings.push("output-styles/ co-exists with manifest outputStyles \u2014 manifest wins");
4202
+ }
4203
+ if (existsSync18(resolve6(dir, "themes")))
4204
+ passes.push("themes/ present (experimental)");
4205
+ if (existsSync18(resolve6(dir, "monitors")) || manifest.experimental?.monitors) {
4206
+ passes.push("monitors config present (experimental)");
4207
+ }
4208
+ if (existsSync18(resolve6(dir, "bin")))
4209
+ passes.push("bin/ present (adds executables to Bash tool $PATH)");
4210
+ if (existsSync18(resolve6(dir, "settings.json")))
4211
+ passes.push("settings.json present (plugin defaults for agent/statusline)");
4212
+ if (existsSync18(resolve6(dir, "README.md")))
4213
+ passes.push("README.md present");
4214
+ if (existsSync18(resolve6(dir, ".mcp.json")))
4215
+ passes.push(".mcp.json present (validated by claude:mcp)");
4216
+ if (existsSync18(resolve6(dir, ".lsp.json")))
4217
+ passes.push(".lsp.json present (validated by claude:lsp when registered)");
4218
+ if (existsSync18(resolve6(dir, "hooks/hooks.json")) || existsSync18(resolve6(dir, "hooks.json"))) {
4219
+ passes.push("hooks config present (validated by claude:hooks)");
4220
+ }
4221
+ if (existsSync18(resolve6(dir, "SKILL.md")) && !existsSync18(skillsDir) && manifest.skills === undefined) {
4222
+ passes.push('Root SKILL.md detected \u2014 plugin will be treated as a single-skill plugin (prefer frontmatter "name" for stable /command)');
4223
+ }
4224
+ return { errors, warnings, passes };
4225
+ }
4226
+ };
4227
+ });
4228
+
4229
+ // src/validators/claude/marketplace.ts
4230
+ import { existsSync as existsSync19, readdirSync as readdirSync9 } from "fs";
4231
+ import { resolve as resolve7, join as join17 } from "path";
4232
+ var claudeMarketplaceValidator;
4233
+ var init_marketplace = __esm(() => {
4234
+ claudeMarketplaceValidator = {
4235
+ id: "claude:marketplace",
4236
+ provider: "claude",
4237
+ name: "Claude Plugin Marketplace",
4238
+ description: "Validates .claude-plugin/marketplace.json or plugins/ marketplace layouts (plugins array with sources)",
4239
+ detect(dir) {
4240
+ if (existsSync19(resolve7(dir, ".claude-plugin", "marketplace.json")))
4241
+ return true;
4242
+ const pluginsDir = resolve7(dir, "plugins");
4243
+ if (!existsSync19(pluginsDir))
4244
+ return false;
4245
+ try {
4246
+ const entries = readdirSync9(pluginsDir, { withFileTypes: true });
4247
+ for (const entry of entries) {
4248
+ if (!entry.isDirectory())
4249
+ continue;
4250
+ const hasSkills = existsSync19(join17(pluginsDir, entry.name, "skills"));
4251
+ const hasManifest = existsSync19(join17(pluginsDir, entry.name, ".claude-plugin", "plugin.json"));
4252
+ if (hasSkills || hasManifest)
4253
+ return true;
4254
+ }
4255
+ } catch {}
4256
+ return false;
4257
+ },
4258
+ async validate(dir, _opts) {
4259
+ const errors = [];
4260
+ const warnings = [];
4261
+ const passes = [];
4262
+ const claudeMktPath = resolve7(dir, ".claude-plugin", "marketplace.json");
4263
+ const hasClaudeMkt = existsSync19(claudeMktPath);
4264
+ const pluginsDir = resolve7(dir, "plugins");
4265
+ const hasPluginsDirLayout = existsSync19(pluginsDir);
4266
+ if (!hasClaudeMkt && !hasPluginsDirLayout) {
4267
+ errors.push("Missing .claude-plugin/marketplace.json or plugins/ directory");
4268
+ return { errors, warnings, passes };
4269
+ }
4270
+ if (hasClaudeMkt) {
4271
+ let mkt;
4272
+ try {
4273
+ const raw = await Bun.file(claudeMktPath).text();
4274
+ mkt = JSON.parse(raw);
4275
+ passes.push(".claude-plugin/marketplace.json is valid JSON");
4276
+ } catch {
4277
+ errors.push(".claude-plugin/marketplace.json is missing or invalid JSON");
4278
+ return { errors, warnings, passes };
4279
+ }
4280
+ if (mkt.name) {
4281
+ passes.push(`name: "${mkt.name}"`);
4282
+ } else {
4283
+ warnings.push('Missing "name" at marketplace root');
4284
+ }
4285
+ if (mkt.description) {
4286
+ passes.push("description present");
4287
+ }
4288
+ if (mkt.owner) {
4289
+ passes.push("owner present");
4290
+ }
4291
+ if (!Array.isArray(mkt.plugins) || mkt.plugins.length === 0) {
4292
+ errors.push('"plugins" must be a non-empty array');
4293
+ return { errors, warnings, passes };
4294
+ }
4295
+ passes.push(`${mkt.plugins.length} plugin(s) declared`);
4296
+ for (const [i, p] of mkt.plugins.entries()) {
4297
+ if (!p || typeof p !== "object") {
4298
+ errors.push(`plugins[${i}]: must be an object`);
4299
+ continue;
4300
+ }
4301
+ if (p.name) {
4302
+ passes.push(`plugins[${i}].name: "${p.name}"`);
4303
+ } else {
4304
+ errors.push(`plugins[${i}]: missing "name"`);
4305
+ }
4306
+ if (p.source) {
4307
+ const src = String(p.source);
4308
+ passes.push(`plugins[${i}].source: "${src}"`);
4309
+ const srcDir = resolve7(dir, src);
4310
+ if (existsSync19(srcDir)) {
4311
+ const hasManifest = existsSync19(resolve7(srcDir, ".claude-plugin", "plugin.json"));
4312
+ const hasSkills = existsSync19(resolve7(srcDir, "skills"));
4313
+ if (hasManifest || hasSkills) {
4314
+ passes.push(`plugins[${i}]: source exists (${hasManifest ? "manifest" : "skills/"})`);
4315
+ } else {
4316
+ warnings.push(`plugins[${i}].source "${src}" exists but lacks plugin markers`);
4317
+ }
4318
+ } else {
4319
+ warnings.push(`plugins[${i}].source path "${src}" does not exist`);
4320
+ }
4321
+ } else {
4322
+ errors.push(`plugins[${i}]: missing "source"`);
4323
+ }
4324
+ if (p.category) {
4325
+ passes.push(`plugins[${i}].category: "${p.category}"`);
4326
+ }
4327
+ }
4328
+ if (existsSync19(resolve7(dir, "README.md"))) {
4329
+ passes.push("README.md exists at marketplace root");
4330
+ } else {
4331
+ warnings.push("No README.md at marketplace root \u2014 recommended for discoverability");
4332
+ }
4333
+ if (existsSync19(resolve7(dir, "LICENSE"))) {
4334
+ passes.push("LICENSE exists at marketplace root");
4335
+ } else {
4336
+ warnings.push("No LICENSE at marketplace root \u2014 recommended");
4337
+ }
4338
+ return { errors, warnings, passes };
4339
+ }
4340
+ if (hasPluginsDirLayout) {
4341
+ passes.push("plugins/ directory exists");
4342
+ const pluginEntries = readdirSync9(pluginsDir, { withFileTypes: true }).filter((e) => e.isDirectory());
4343
+ if (pluginEntries.length === 0) {
4344
+ errors.push("plugins/ directory is empty \u2014 expected at least one plugin");
4345
+ return { errors, warnings, passes };
4346
+ }
4347
+ passes.push(`${pluginEntries.length} plugin(s) found`);
4348
+ if (existsSync19(resolve7(dir, "README.md"))) {
4349
+ passes.push("README.md exists at marketplace root");
4350
+ } else {
4351
+ warnings.push("No README.md at marketplace root \u2014 recommended for discoverability");
4352
+ }
4353
+ if (existsSync19(resolve7(dir, "LICENSE"))) {
4354
+ passes.push("LICENSE exists at marketplace root");
4355
+ } else {
4356
+ warnings.push("No LICENSE at marketplace root \u2014 recommended");
4357
+ }
4358
+ for (const plugin of pluginEntries) {
4359
+ const pluginPath = join17(pluginsDir, plugin.name);
4360
+ const hasSkills = existsSync19(join17(pluginPath, "skills"));
4361
+ const hasManifest = existsSync19(join17(pluginPath, ".claude-plugin", "plugin.json"));
4362
+ const hasReadme = existsSync19(join17(pluginPath, "README.md"));
4363
+ if (hasManifest || hasSkills) {
4364
+ passes.push(`Plugin "${plugin.name}" has ${hasManifest ? "manifest" : "skills/"}`);
4365
+ } else {
4366
+ warnings.push(`Plugin "${plugin.name}" has neither .claude-plugin/plugin.json nor skills/`);
4367
+ }
4368
+ if (!hasReadme) {
4369
+ warnings.push(`Plugin "${plugin.name}" has no README.md`);
4370
+ }
4371
+ }
4372
+ return { errors, warnings, passes };
4373
+ }
4374
+ return { errors, warnings, passes };
4375
+ }
4376
+ };
4377
+ });
4378
+
4379
+ // src/validators/claude/hooks.ts
4380
+ import { existsSync as existsSync20 } from "fs";
4381
+ import { resolve as resolve8 } from "path";
4382
+ var KNOWN_EVENTS, claudeHooksValidator;
4383
+ var init_hooks = __esm(() => {
4384
+ KNOWN_EVENTS = [
4385
+ "SessionStart",
4386
+ "Setup",
4387
+ "UserPromptSubmit",
4388
+ "UserPromptExpansion",
4389
+ "PreToolUse",
4390
+ "PermissionRequest",
4391
+ "PermissionDenied",
4392
+ "PostToolUse",
4393
+ "PostToolUseFailure",
4394
+ "PostToolBatch",
4395
+ "Notification",
4396
+ "MessageDisplay",
4397
+ "SubagentStart",
4398
+ "SubagentStop",
4399
+ "TaskCreated",
4400
+ "TaskCompleted",
4401
+ "Stop",
4402
+ "StopFailure",
4403
+ "TeammateIdle",
4404
+ "InstructionsLoaded",
4405
+ "ConfigChange",
4406
+ "CwdChanged",
4407
+ "FileChanged",
4408
+ "WorktreeCreate",
4409
+ "WorktreeRemove",
4410
+ "PreCompact",
4411
+ "PostCompact",
4412
+ "Elicitation",
4413
+ "ElicitationResult",
4414
+ "SessionEnd"
4415
+ ];
4416
+ claudeHooksValidator = {
4417
+ id: "claude:hooks",
4418
+ provider: "claude",
4419
+ name: "Claude Hooks",
4420
+ description: "Validates hooks/hooks.json (or root hooks.json): all lifecycle events per Plugins reference, hook group structure (matcher + hooks[]), supported hook types (command, http, mcp_tool, prompt, agent)",
4421
+ detect(dir) {
4422
+ return existsSync20(resolve8(dir, "hooks", "hooks.json")) || existsSync20(resolve8(dir, "hooks.json"));
4423
+ },
4424
+ async validate(dir, _opts) {
4425
+ const errors = [];
4426
+ const warnings = [];
4427
+ const passes = [];
4428
+ const hooksPath = existsSync20(resolve8(dir, "hooks", "hooks.json")) ? resolve8(dir, "hooks", "hooks.json") : resolve8(dir, "hooks.json");
4429
+ let config;
4430
+ try {
4431
+ const raw = await Bun.file(hooksPath).text();
4432
+ config = JSON.parse(raw);
4433
+ passes.push("hooks.json is valid JSON");
4434
+ } catch {
4435
+ errors.push("hooks.json is missing or invalid JSON");
4436
+ return { errors, warnings, passes };
4437
+ }
4438
+ const eventNames = Object.keys(config);
4439
+ for (const name of eventNames) {
4440
+ if (KNOWN_EVENTS.includes(name)) {
4441
+ passes.push(`Event "${name}" is a known lifecycle event`);
4442
+ } else {
4443
+ warnings.push(`Unknown event name: "${name}" \u2014 see full list in Plugins reference (SessionStart, PreToolUse, PostToolUse, Stop, ...)`);
4444
+ }
4445
+ }
4446
+ for (const [event, groups] of Object.entries(config)) {
4447
+ if (!Array.isArray(groups)) {
4448
+ errors.push(`Event "${event}": value must be an array of hook groups`);
4449
+ continue;
4450
+ }
4451
+ groups.forEach((group, gi) => {
4452
+ if (!group || typeof group !== "object") {
4453
+ errors.push(`${event}[${gi}]: hook group must be an object`);
4454
+ return;
4455
+ }
4456
+ if (group.matcher !== undefined && typeof group.matcher !== "string") {
4457
+ warnings.push(`${event}[${gi}]: "matcher" should be a string (e.g. "Write|Edit" or glob)`);
4458
+ }
4459
+ const hooksArr = group.hooks;
4460
+ if (!Array.isArray(hooksArr)) {
4461
+ errors.push(`${event}[${gi}]: missing or invalid "hooks" array`);
4462
+ return;
4463
+ }
4464
+ hooksArr.forEach((h, hi) => {
4465
+ if (!h || typeof h !== "object" || !h.type) {
4466
+ errors.push(`${event}[${gi}].hooks[${hi}]: must have "type"`);
4467
+ return;
4468
+ }
4469
+ const t = String(h.type);
4470
+ if (!["command", "http", "mcp_tool", "prompt", "agent"].includes(t)) {
4471
+ warnings.push(`${event}[${gi}].hooks[${hi}]: unknown type "${t}" (valid: command, http, mcp_tool, prompt, agent)`);
4472
+ }
4473
+ if (t === "command" && !h.command) {
4474
+ errors.push(`${event}[${gi}].hooks[${hi}]: type=command requires "command"`);
4475
+ }
4476
+ if (t === "http" && !h.url) {
4477
+ errors.push(`${event}[${gi}].hooks[${hi}]: type=http requires "url"`);
4478
+ }
4479
+ if (h.command && typeof h.command === "string" && /\$\{CLAUDE_/.test(h.command)) {
4480
+ passes.push(`${event}[${gi}].hooks[${hi}]: uses plugin env substitution`);
4481
+ }
4482
+ });
4483
+ if (hooksArr.length > 0) {
4484
+ passes.push(`Event "${event}" has ${hooksArr.length} hook action(s)`);
4485
+ }
4486
+ });
4487
+ }
4488
+ return { errors, warnings, passes };
4489
+ }
4490
+ };
4491
+ });
4492
+
4493
+ // src/validators/claude/mcp.ts
4494
+ import { existsSync as existsSync21 } from "fs";
4495
+ import { resolve as resolve9 } from "path";
4496
+ var claudeMcpValidator;
4497
+ var init_mcp = __esm(() => {
4498
+ claudeMcpValidator = {
4499
+ id: "claude:mcp",
4500
+ provider: "claude",
4501
+ name: "Claude MCP Config",
4502
+ description: "Validates .mcp.json (or inline via plugin.json mcpServers): server entries (stdio: command+args, or url), env, cwd, ${CLAUDE_PLUGIN_ROOT} etc. substitutions per Plugins reference",
4503
+ detect(dir) {
4504
+ return existsSync21(resolve9(dir, ".mcp.json"));
4505
+ },
4506
+ async validate(dir, _opts) {
4507
+ const errors = [];
4508
+ const warnings = [];
4509
+ const passes = [];
4510
+ const mcpPath = resolve9(dir, ".mcp.json");
4511
+ let config;
4512
+ try {
4513
+ const raw = await Bun.file(mcpPath).text();
4514
+ config = JSON.parse(raw);
4515
+ passes.push(".mcp.json is valid JSON");
4516
+ } catch {
4517
+ errors.push(".mcp.json is missing or invalid JSON");
4518
+ return { errors, warnings, passes };
4519
+ }
4520
+ if (typeof config !== "object" || Array.isArray(config)) {
4521
+ errors.push(".mcp.json must be a JSON object with server name keys");
4522
+ return { errors, warnings, passes };
4523
+ }
4524
+ const serverNames = Object.keys(config);
4525
+ if (serverNames.length === 0) {
4526
+ warnings.push(".mcp.json is empty \u2014 no servers defined");
4527
+ return { errors, warnings, passes };
4528
+ }
4529
+ passes.push(`${serverNames.length} server(s) defined`);
4530
+ for (const [name, entry] of Object.entries(config)) {
4531
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
4532
+ errors.push(`mcp server "${name}": definition must be an object`);
4533
+ continue;
4534
+ }
4535
+ const e = entry;
4536
+ const hasCommand = typeof e.command === "string";
4537
+ const hasUrl = typeof e.url === "string";
4538
+ if (!hasCommand && !hasUrl) {
4539
+ errors.push(`mcp server "${name}": must have either "command" (for stdio) or "url" (for SSE/HTTP)`);
4540
+ }
4541
+ if (hasCommand && !Array.isArray(e.args)) {
4542
+ warnings.push(`mcp server "${name}": "command" present but no "args" array (ok for some servers)`);
4543
+ }
4544
+ if (hasUrl && hasCommand) {
4545
+ warnings.push(`mcp server "${name}": both "command" and "url" present \u2014 usually one or the other`);
4546
+ }
4547
+ if (e.env && typeof e.env === "object") {
4548
+ passes.push(`mcp server "${name}": has env`);
4549
+ }
4550
+ if (typeof e.cwd === "string") {
4551
+ passes.push(`mcp server "${name}": has cwd`);
4552
+ }
4553
+ const hasSubs = JSON.stringify(e).match(/\$\{CLAUDE_PLUGIN_(ROOT|DATA)|CLAUDE_PROJECT_DIR|user_config\.|ENV_VAR\}/);
4554
+ if (hasSubs) {
4555
+ passes.push(`mcp server "${name}": uses \${CLAUDE_PLUGIN_*} / user_config / env substitution`);
4556
+ }
4557
+ }
4558
+ return { errors, warnings, passes };
4559
+ }
4560
+ };
4561
+ });
4562
+
4563
+ // src/validators/claude/subagent.ts
4564
+ import { existsSync as existsSync22, readdirSync as readdirSync10 } from "fs";
4565
+ import { resolve as resolve10, join as join18 } from "path";
4566
+ var claudeSubagentValidator;
4567
+ var init_subagent = __esm(() => {
4568
+ init_frontmatter();
4569
+ claudeSubagentValidator = {
4570
+ id: "claude:subagent",
4571
+ provider: "claude",
4572
+ name: "Claude Subagents",
4573
+ description: "Validates agents/*.md (plugin subagents): frontmatter per spec (name, description, model, effort, maxTurns, tools, disallowedTools, skills, memory, background, isolation=worktree), body; warns on disallowed fields (hooks, mcpServers, permissionMode) for security",
4574
+ detect(dir) {
4575
+ const agentsDir = resolve10(dir, "agents");
4576
+ if (!existsSync22(agentsDir))
4577
+ return false;
4578
+ try {
4579
+ return readdirSync10(agentsDir).some((f) => f.endsWith(".md"));
4580
+ } catch {
4581
+ return false;
4582
+ }
4583
+ },
4584
+ async validate(dir, _opts) {
4585
+ const errors = [];
4586
+ const warnings = [];
4587
+ const passes = [];
4588
+ const agentsDir = resolve10(dir, "agents");
4589
+ const mdFiles = readdirSync10(agentsDir).filter((f) => f.endsWith(".md"));
4590
+ if (mdFiles.length === 0) {
4591
+ errors.push("agents/ directory has no .md files");
4592
+ return { errors, warnings, passes };
4593
+ }
4594
+ passes.push(`${mdFiles.length} agent definition(s) found`);
4595
+ const SUPPORTED = new Set([
4596
+ "name",
4597
+ "description",
4598
+ "model",
4599
+ "effort",
4600
+ "maxTurns",
4601
+ "tools",
4602
+ "disallowedTools",
4603
+ "skills",
4604
+ "memory",
4605
+ "background",
4606
+ "isolation"
4607
+ ]);
4608
+ const DISALLOWED = new Set(["hooks", "mcpServers", "permissionMode"]);
4609
+ for (const file of mdFiles) {
4610
+ const filePath = join18(agentsDir, file);
4611
+ const raw = await Bun.file(filePath).text();
4612
+ try {
4613
+ const parsed = parseFrontmatter(raw);
4614
+ const fm = parsed.data;
4615
+ if (Object.keys(fm).length === 0) {
4616
+ warnings.push(`${file}: no YAML frontmatter (description recommended so Claude knows when to invoke)`);
4617
+ } else {
4618
+ if (fm.description) {
4619
+ passes.push(`${file}: has frontmatter with description`);
4620
+ } else {
4621
+ warnings.push(`${file}: missing "description" in frontmatter`);
4622
+ }
4623
+ const usedSupported = [];
4624
+ Object.keys(fm).forEach((k) => {
4625
+ if (SUPPORTED.has(k))
4626
+ usedSupported.push(k);
4627
+ if (DISALLOWED.has(k)) {
4628
+ errors.push(`${file}: frontmatter "${k}" is not supported for plugin-shipped agents (security restriction)`);
4629
+ }
4630
+ });
4631
+ if (usedSupported.length) {
4632
+ passes.push(`${file}: frontmatter fields: ${usedSupported.join(", ")}`);
4633
+ }
4634
+ if (fm.isolation !== undefined && fm.isolation !== "worktree") {
4635
+ errors.push(`${file}: "isolation" must be "worktree" if present (only supported value for plugin agents)`);
4636
+ }
4637
+ if (fm.name && typeof fm.name === "string") {
4638
+ passes.push(`${file}: name: "${fm.name}"`);
4639
+ }
4640
+ }
4641
+ if (!parsed.content.trim()) {
4642
+ errors.push(`${file}: body is empty`);
4643
+ } else {
4644
+ passes.push(`${file}: has agent system prompt body`);
4645
+ }
4646
+ } catch {
4647
+ errors.push(`${file}: failed to parse`);
4648
+ }
4649
+ }
4650
+ return { errors, warnings, passes };
4651
+ }
4652
+ };
4653
+ });
4654
+
4655
+ // src/validators/claude/command.ts
4656
+ import { existsSync as existsSync23, readdirSync as readdirSync11 } from "fs";
4657
+ import { resolve as resolve11, join as join19 } from "path";
4658
+ var claudeCommandValidator;
4659
+ var init_command = __esm(() => {
4660
+ init_frontmatter();
4661
+ claudeCommandValidator = {
4662
+ id: "claude:command",
4663
+ provider: "claude",
4664
+ name: "Claude Commands",
4665
+ description: "Validates commands/ (or legacy .claude/commands/) .md files: frontmatter (including rich skill fields), description, body",
4666
+ detect(dir) {
4667
+ const commandsDir = resolve11(dir, "commands");
4668
+ if (!existsSync23(commandsDir))
4669
+ return false;
4670
+ try {
4671
+ return readdirSync11(commandsDir).some((f) => f.endsWith(".md"));
4672
+ } catch {
4673
+ return false;
4674
+ }
4675
+ },
4676
+ async validate(dir, _opts) {
4677
+ const errors = [];
4678
+ const warnings = [];
4679
+ const passes = [];
4680
+ const commandsDir = resolve11(dir, "commands");
4681
+ const mdFiles = readdirSync11(commandsDir).filter((f) => f.endsWith(".md"));
4682
+ if (mdFiles.length === 0) {
4683
+ errors.push("commands/ directory has no .md files");
4684
+ return { errors, warnings, passes };
4685
+ }
4686
+ passes.push(`${mdFiles.length} command definition(s) found`);
4687
+ for (const file of mdFiles) {
4688
+ const filePath = join19(commandsDir, file);
4689
+ const raw = await Bun.file(filePath).text();
4690
+ try {
4691
+ const parsed = parseFrontmatter(raw);
4692
+ if (Object.keys(parsed.data).length === 0) {
4693
+ warnings.push(`${file}: no YAML frontmatter`);
4694
+ } else if (!parsed.data.description) {
4695
+ warnings.push(`${file}: missing "description" in frontmatter`);
4696
+ } else {
4697
+ passes.push(`${file}: has frontmatter with description`);
4698
+ }
4699
+ if (!parsed.content.trim()) {
4700
+ errors.push(`${file}: body is empty`);
4701
+ }
4702
+ const advancedKeys = ["allowed-tools", "disallowed-tools", "context", "when_to_use", "disable-model-invocation", "user-invocable", "arguments", "argument-hint", "shell", "paths", "hooks"];
4703
+ const foundAdvanced = advancedKeys.filter((k) => parsed.data[k] !== undefined);
4704
+ if (foundAdvanced.length > 0) {
4705
+ passes.push(`${file}: advanced frontmatter: ${foundAdvanced.join(", ")}`);
4706
+ }
4707
+ } catch {
4708
+ errors.push(`${file}: failed to parse`);
4709
+ }
4710
+ }
4711
+ return { errors, warnings, passes };
4712
+ }
4713
+ };
4714
+ });
4715
+
4716
+ // src/validators/claude/memory.ts
4717
+ import { existsSync as existsSync24 } from "fs";
4718
+ import { resolve as resolve12 } from "path";
4719
+ var claudeMemoryValidator;
4720
+ var init_memory = __esm(() => {
4721
+ claudeMemoryValidator = {
4722
+ id: "claude:memory",
4723
+ provider: "claude",
4724
+ name: "Claude CLAUDE.md",
4725
+ description: "Validates CLAUDE.md: non-empty, length recommendations, @path imports",
4726
+ detect(dir) {
4727
+ return existsSync24(resolve12(dir, "CLAUDE.md"));
4728
+ },
4729
+ async validate(dir, _opts) {
4730
+ const errors = [];
4731
+ const warnings = [];
4732
+ const passes = [];
4733
+ const filePath = resolve12(dir, "CLAUDE.md");
4734
+ const raw = await Bun.file(filePath).text();
4735
+ if (!raw.trim()) {
4736
+ errors.push("CLAUDE.md is empty");
4737
+ return { errors, warnings, passes };
4738
+ }
4739
+ passes.push("CLAUDE.md is non-empty");
4740
+ const lines = raw.split(`
4741
+ `);
4742
+ if (lines.length > 200) {
4743
+ warnings.push(`CLAUDE.md is ${lines.length} lines \u2014 official recommendation is under 200. Move reference content to skills.`);
4744
+ } else {
4745
+ passes.push(`CLAUDE.md is ${lines.length} lines (under 200 recommended limit)`);
4746
+ }
4747
+ const importRegex = /^@([^\s]+)\s*$/gm;
4748
+ let match;
4749
+ while ((match = importRegex.exec(raw)) !== null) {
4750
+ const importPath = match[1];
4751
+ const resolvedImport = resolve12(dir, importPath);
4752
+ if (existsSync24(resolvedImport)) {
4753
+ passes.push(`@import "${importPath}" exists`);
4754
+ } else {
4755
+ warnings.push(`@import "${importPath}" \u2014 file not found at ${resolvedImport}`);
4756
+ }
4757
+ }
4758
+ return { errors, warnings, passes };
4759
+ }
4760
+ };
4761
+ });
4762
+
4763
+ // src/validators/claude/lsp.ts
4764
+ import { existsSync as existsSync25 } from "fs";
4765
+ import { resolve as resolve13 } from "path";
4766
+ var claudeLspValidator;
4767
+ var init_lsp = __esm(() => {
4768
+ claudeLspValidator = {
4769
+ id: "claude:lsp",
4770
+ provider: "claude",
4771
+ name: "Claude LSP Servers",
4772
+ description: "Validates .lsp.json (or plugin.json lspServers): language server configs with required command + extensionToLanguage; optional transport, env, settings, diagnostics etc. (binaries installed separately)",
4773
+ detect(dir) {
4774
+ return existsSync25(resolve13(dir, ".lsp.json")) || existsSync25(resolve13(dir, ".claude-plugin", "plugin.json"));
4775
+ },
4776
+ async validate(dir, _opts) {
4777
+ const errors = [];
4778
+ const warnings = [];
4779
+ const passes = [];
4780
+ let cfg = null;
4781
+ const lspPath = resolve13(dir, ".lsp.json");
4782
+ if (existsSync25(lspPath)) {
4783
+ try {
4784
+ cfg = JSON.parse(await Bun.file(lspPath).text());
4785
+ passes.push(".lsp.json is valid JSON");
4786
+ } catch {
4787
+ errors.push(".lsp.json is invalid JSON");
4788
+ return { errors, warnings, passes };
4789
+ }
4790
+ } else {
4791
+ const manifestPath = resolve13(dir, ".claude-plugin", "plugin.json");
4792
+ if (existsSync25(manifestPath)) {
4793
+ try {
4794
+ const m = JSON.parse(await Bun.file(manifestPath).text());
4795
+ if (m && m.lspServers && typeof m.lspServers === "object") {
4796
+ cfg = m.lspServers;
4797
+ passes.push("lspServers present inline in plugin.json");
4798
+ }
4799
+ } catch {}
4800
+ }
4801
+ }
4802
+ if (!cfg) {
4803
+ if (!existsSync25(lspPath)) {
4804
+ return { errors, warnings, passes };
4805
+ }
4806
+ }
4807
+ if (cfg && typeof cfg === "object") {
4808
+ const langs = Object.keys(cfg);
4809
+ passes.push(`${langs.length} language server(s) configured`);
4810
+ for (const lang of langs) {
4811
+ const entry = cfg[lang];
4812
+ if (!entry || !entry.command) {
4813
+ errors.push(`lsp "${lang}": "command" (the LSP binary) is required`);
4814
+ }
4815
+ if (!entry.extensionToLanguage || typeof entry.extensionToLanguage !== "object") {
4816
+ errors.push(`lsp "${lang}": "extensionToLanguage" map is required (e.g. { ".ts": "typescript" })`);
4817
+ } else {
4818
+ passes.push(`lsp "${lang}": has extensionToLanguage mapping`);
4819
+ }
4820
+ if (entry.diagnostics === false) {
4821
+ passes.push(`lsp "${lang}": diagnostics disabled (navigation only)`);
4822
+ }
4823
+ }
4824
+ }
4825
+ warnings.push('Reminder: the actual language server binary (gopls, pyright, etc.) must be installed separately on PATH. See /plugin errors tab if "Executable not found".');
4826
+ return { errors, warnings, passes };
4827
+ }
4828
+ };
4829
+ });
4830
+
4831
+ // src/validators/claude/monitors.ts
4832
+ import { existsSync as existsSync26 } from "fs";
4833
+ import { resolve as resolve14 } from "path";
4834
+ var claudeMonitorsValidator;
4835
+ var init_monitors = __esm(() => {
4836
+ claudeMonitorsValidator = {
4837
+ id: "claude:monitors",
4838
+ provider: "claude",
4839
+ name: "Claude Monitors (experimental)",
4840
+ description: "Validates monitors/monitors.json (or experimental.monitors): array of {name, command, description, when?}; commands support ${CLAUDE_PLUGIN_*} subs. Monitors run only in interactive CLI sessions.",
4841
+ detect(dir) {
4842
+ return existsSync26(resolve14(dir, "monitors", "monitors.json")) || existsSync26(resolve14(dir, "monitors.json")) || existsSync26(resolve14(dir, ".claude-plugin", "plugin.json"));
4843
+ },
4844
+ async validate(dir, _opts) {
4845
+ const errors = [];
4846
+ const warnings = [];
4847
+ const passes = [];
4848
+ let arr = null;
4849
+ const candidates = [
4850
+ resolve14(dir, "monitors", "monitors.json"),
4851
+ resolve14(dir, "monitors.json")
4852
+ ];
4853
+ for (const p of candidates) {
4854
+ if (existsSync26(p)) {
4855
+ try {
4856
+ const parsed = JSON.parse(await Bun.file(p).text());
4857
+ if (Array.isArray(parsed)) {
4858
+ arr = parsed;
4859
+ passes.push("monitors config is valid JSON array");
4860
+ }
4861
+ break;
4862
+ } catch {
4863
+ errors.push("monitors config is invalid JSON");
4864
+ return { errors, warnings, passes };
4865
+ }
4866
+ }
4867
+ }
4868
+ if (!arr) {
4869
+ const mp = resolve14(dir, ".claude-plugin", "plugin.json");
4870
+ if (existsSync26(mp)) {
4871
+ try {
4872
+ const m = JSON.parse(await Bun.file(mp).text());
4873
+ const exp = m?.experimental;
4874
+ const inline = typeof exp === "string" ? null : exp?.monitors;
4875
+ if (Array.isArray(inline))
4876
+ arr = inline;
4877
+ else if (typeof inline === "string") {
4878
+ passes.push("experimental.monitors declared as path in manifest (content not validated here)");
4879
+ }
4880
+ } catch {}
3488
4881
  }
3489
- } catch {}
4882
+ }
4883
+ if (!arr) {
4884
+ return { errors, warnings, passes };
4885
+ }
4886
+ if (!Array.isArray(arr)) {
4887
+ errors.push("monitors config must be a JSON array");
4888
+ return { errors, warnings, passes };
4889
+ }
4890
+ const seen = new Set;
4891
+ arr.forEach((mon, i) => {
4892
+ if (!mon || typeof mon !== "object") {
4893
+ errors.push(`monitors[${i}]: entry must be an object`);
4894
+ return;
4895
+ }
4896
+ if (!mon.name || typeof mon.name !== "string") {
4897
+ errors.push(`monitors[${i}]: "name" (unique id) is required`);
4898
+ } else {
4899
+ if (seen.has(mon.name))
4900
+ errors.push(`monitors: duplicate name "${mon.name}"`);
4901
+ seen.add(mon.name);
4902
+ }
4903
+ if (!mon.command || typeof mon.command !== "string") {
4904
+ errors.push(`monitors[${i}]: "command" (shell command) is required`);
4905
+ } else if (/\$\{CLAUDE_/.test(mon.command)) {
4906
+ passes.push(`monitors[${i}] "${mon.name || i}": uses CLAUDE_PLUGIN_* substitution`);
4907
+ }
4908
+ if (!mon.description) {
4909
+ warnings.push(`monitors[${i}]: "description" recommended (shown in task panel)`);
4910
+ }
4911
+ if (mon.when && !/^always$|^on-skill-invoke:/.test(String(mon.when))) {
4912
+ warnings.push(`monitors[${i}]: "when" should be "always" (default) or "on-skill-invoke:<skill>"`);
4913
+ }
4914
+ });
4915
+ passes.push(`${arr.length} monitor(s) declared`);
4916
+ warnings.push("Note: monitors are experimental, run only for interactive CLI sessions, and are skipped on some hosts. They do not stop automatically if the plugin is disabled mid-session.");
4917
+ return { errors, warnings, passes };
4918
+ }
4919
+ };
4920
+ });
4921
+
4922
+ // src/validators/codex/plugin.ts
4923
+ import { existsSync as existsSync27, readdirSync as readdirSync12 } from "fs";
4924
+ import { resolve as resolve15, join as join20 } from "path";
4925
+ var NAME_REGEX3, codexPluginValidator;
4926
+ var init_plugin2 = __esm(() => {
4927
+ NAME_REGEX3 = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
4928
+ codexPluginValidator = {
4929
+ id: "codex:plugin",
4930
+ provider: "codex",
4931
+ name: "Codex Plugin",
4932
+ description: "Validates .codex-plugin/plugin.json manifest (requires interface block and skills as directory string per Codex packaging)",
4933
+ detect(dir) {
4934
+ return existsSync27(resolve15(dir, ".codex-plugin", "plugin.json"));
4935
+ },
4936
+ async validate(dir, _opts) {
4937
+ const errors = [];
4938
+ const warnings = [];
4939
+ const passes = [];
4940
+ const manifestPath = resolve15(dir, ".codex-plugin", "plugin.json");
4941
+ let manifest;
4942
+ try {
4943
+ const raw = await Bun.file(manifestPath).text();
4944
+ manifest = JSON.parse(raw);
4945
+ passes.push(".codex-plugin/plugin.json is valid JSON");
4946
+ } catch {
4947
+ errors.push(".codex-plugin/plugin.json is missing or invalid JSON");
4948
+ return { errors, warnings, passes };
4949
+ }
3490
4950
  if (!manifest.name) {
3491
4951
  errors.push('Missing required field: "name"');
3492
4952
  } else {
3493
4953
  const name = String(manifest.name);
3494
- if (!NAME_REGEX2.test(name)) {
4954
+ if (!NAME_REGEX3.test(name)) {
3495
4955
  errors.push(`Invalid name format: "${name}" \u2014 must be kebab-case (a-z, 0-9, hyphens)`);
3496
4956
  } else {
3497
4957
  passes.push(`name: "${name}"`);
3498
4958
  }
3499
4959
  }
4960
+ if (manifest.skills === undefined) {
4961
+ errors.push('Missing required field: "skills" (must be a directory string like "./skills/")');
4962
+ } else if (typeof manifest.skills !== "string") {
4963
+ errors.push('"skills" must be a string directory path');
4964
+ } else {
4965
+ const s = manifest.skills;
4966
+ if (!s.startsWith("./")) {
4967
+ warnings.push('"skills" should start with "./"');
4968
+ }
4969
+ passes.push(`skills: "${s}" (directory string)`);
4970
+ }
4971
+ if (!manifest.interface || typeof manifest.interface !== "object") {
4972
+ errors.push('Missing required "interface" object (Codex uses it for displayName, shortDescription, category, etc.)');
4973
+ } else {
4974
+ const iface = manifest.interface;
4975
+ if (iface.displayName) {
4976
+ passes.push(`interface.displayName: "${iface.displayName}"`);
4977
+ } else {
4978
+ warnings.push("interface.displayName recommended");
4979
+ }
4980
+ if (iface.category) {
4981
+ passes.push(`interface.category: "${iface.category}"`);
4982
+ }
4983
+ passes.push("interface block present");
4984
+ }
3500
4985
  if (manifest.version !== undefined) {
3501
4986
  const v = String(manifest.version);
3502
4987
  if (!/^\d+\.\d+\.\d+/.test(v)) {
3503
- errors.push(`Invalid version format: "${v}" \u2014 must look like semver (MAJOR.MINOR.PATCH) when using explicit versioning`);
4988
+ warnings.push(`version "${v}" should look like semver for explicit versioning`);
3504
4989
  } else {
3505
- passes.push(`version: "${v}" (explicit \u2014 bump on every release to publish updates)`);
4990
+ passes.push(`version: "${v}"`);
3506
4991
  }
3507
4992
  } else {
3508
- passes.push("version omitted (git commit SHA used as version key \u2014 every commit becomes an available update)");
4993
+ passes.push("version omitted (git commit SHA used as version key)");
3509
4994
  }
3510
4995
  if (manifest.description !== undefined) {
3511
4996
  const desc = String(manifest.description);
@@ -3515,189 +5000,401 @@ var init_plugin = __esm(() => {
3515
5000
  passes.push("description field present");
3516
5001
  }
3517
5002
  } else {
3518
- warnings.push('Missing "description" (recommended for UI, marketplace listings, and auto-discovery)');
5003
+ warnings.push('Missing "description" (recommended)');
3519
5004
  }
3520
- if (manifest.displayName !== undefined) {
3521
- passes.push(`displayName: "${manifest.displayName}" (human UI label; falls back to name)`);
5005
+ const skillsDir = resolve15(dir, "skills");
5006
+ if (existsSync27(skillsDir)) {
5007
+ const entries = readdirSync12(skillsDir, { withFileTypes: true }).filter((e) => e.isDirectory());
5008
+ for (const e of entries) {
5009
+ const md = join20(skillsDir, e.name, "SKILL.md");
5010
+ if (existsSync27(md)) {
5011
+ passes.push(`skills/${e.name}/SKILL.md exists`);
5012
+ } else {
5013
+ errors.push(`skills/${e.name}/ is missing SKILL.md`);
5014
+ }
5015
+ }
3522
5016
  }
3523
- if (manifest.author !== undefined) {
3524
- const a = manifest.author;
3525
- if (a && typeof a === "object" && a.name) {
3526
- passes.push("author present");
5017
+ const known = new Set(["name", "version", "description", "skills", "interface", "author", "homepage", "repository", "license", "keywords"]);
5018
+ const unknown = Object.keys(manifest).filter((k) => !known.has(k));
5019
+ for (const k of unknown) {
5020
+ warnings.push(`Unrecognized field "${k}" \u2014 will be ignored (for compatibility)`);
5021
+ }
5022
+ return { errors, warnings, passes };
5023
+ }
5024
+ };
5025
+ });
5026
+
5027
+ // src/validators/codex/marketplace.ts
5028
+ import { existsSync as existsSync28 } from "fs";
5029
+ import { resolve as resolve16 } from "path";
5030
+ var codexMarketplaceValidator;
5031
+ var init_marketplace2 = __esm(() => {
5032
+ codexMarketplaceValidator = {
5033
+ id: "codex:marketplace",
5034
+ provider: "codex",
5035
+ name: "Codex Plugin Marketplace",
5036
+ description: "Validates .agents/plugins/marketplace.json (Codex convention: object source + policy blocks)",
5037
+ detect(dir) {
5038
+ if (existsSync28(resolve16(dir, ".agents", "plugins", "marketplace.json")))
5039
+ return true;
5040
+ if (existsSync28(resolve16(dir, ".agents", "plugins", "marketplace.json")))
5041
+ return true;
5042
+ return false;
5043
+ },
5044
+ async validate(dir, _opts) {
5045
+ const errors = [];
5046
+ const warnings = [];
5047
+ const passes = [];
5048
+ const marketplacePath = resolve16(dir, ".agents", "plugins", "marketplace.json");
5049
+ if (!existsSync28(marketplacePath)) {
5050
+ errors.push("Missing .agents/plugins/marketplace.json");
5051
+ return { errors, warnings, passes };
5052
+ }
5053
+ let marketplace;
5054
+ try {
5055
+ const raw = await Bun.file(marketplacePath).text();
5056
+ marketplace = JSON.parse(raw);
5057
+ passes.push(".agents/plugins/marketplace.json is valid JSON");
5058
+ } catch {
5059
+ errors.push(".agents/plugins/marketplace.json is missing or invalid JSON");
5060
+ return { errors, warnings, passes };
5061
+ }
5062
+ if (marketplace.name) {
5063
+ passes.push(`name: "${marketplace.name}"`);
5064
+ } else {
5065
+ warnings.push('Missing "name" at marketplace root');
5066
+ }
5067
+ if (marketplace.interface && typeof marketplace.interface === "object") {
5068
+ const iface = marketplace.interface;
5069
+ if (iface.displayName) {
5070
+ passes.push(`interface.displayName: "${iface.displayName}"`);
5071
+ }
5072
+ passes.push("interface block present");
5073
+ } else {
5074
+ warnings.push('Recommended: "interface" with displayName at marketplace root');
5075
+ }
5076
+ if (!Array.isArray(marketplace.plugins) || marketplace.plugins.length === 0) {
5077
+ errors.push('"plugins" must be a non-empty array');
5078
+ return { errors, warnings, passes };
5079
+ }
5080
+ passes.push(`${marketplace.plugins.length} plugin(s) declared`);
5081
+ for (const [i, p] of marketplace.plugins.entries()) {
5082
+ if (!p || typeof p !== "object") {
5083
+ errors.push(`plugins[${i}]: must be an object`);
5084
+ continue;
5085
+ }
5086
+ if (!p.name) {
5087
+ errors.push(`plugins[${i}]: missing "name"`);
3527
5088
  } else {
3528
- warnings.push('author should be an object like {"name": "...", "email?": "..."}');
5089
+ passes.push(`plugins[${i}].name: "${p.name}"`);
5090
+ }
5091
+ if (!p.source || typeof p.source !== "object") {
5092
+ errors.push(`plugins[${i}].source: must be an object like { "source": "local", "path": "..." }`);
5093
+ } else {
5094
+ if (p.source.source) {
5095
+ passes.push(`plugins[${i}].source.source: "${p.source.source}"`);
5096
+ } else {
5097
+ warnings.push(`plugins[${i}].source: missing "source"`);
5098
+ }
5099
+ if (p.source.path) {
5100
+ const pathStr = String(p.source.path);
5101
+ if (!pathStr.startsWith("./") && !pathStr.startsWith("../")) {
5102
+ warnings.push(`plugins[${i}].source.path: "${pathStr}" should be relative (./ or ../)`);
5103
+ }
5104
+ passes.push(`plugins[${i}].source.path: "${pathStr}"`);
5105
+ } else {
5106
+ errors.push(`plugins[${i}].source: missing "path"`);
5107
+ }
5108
+ }
5109
+ if (p.policy && typeof p.policy === "object") {
5110
+ passes.push(`plugins[${i}].policy present`);
5111
+ if (p.policy.installation) {
5112
+ passes.push(`plugins[${i}].policy.installation: "${p.policy.installation}"`);
5113
+ }
5114
+ if (p.policy.authentication) {
5115
+ passes.push(`plugins[${i}].policy.authentication: "${p.policy.authentication}"`);
5116
+ }
5117
+ } else {
5118
+ warnings.push(`plugins[${i}]: "policy" recommended (installation/authentication)`);
5119
+ }
5120
+ if (p.category) {
5121
+ passes.push(`plugins[${i}].category: "${p.category}"`);
3529
5122
  }
3530
5123
  }
3531
- if (manifest.license !== undefined) {
3532
- passes.push(`license: "${manifest.license}"`);
5124
+ if (existsSync28(resolve16(dir, "README.md"))) {
5125
+ passes.push("README.md exists at marketplace root");
5126
+ } else {
5127
+ warnings.push("No README.md at marketplace root \u2014 recommended");
3533
5128
  }
3534
- if (manifest.keywords !== undefined) {
3535
- if (Array.isArray(manifest.keywords)) {
3536
- passes.push(`keywords: [${manifest.keywords.join(", ")}]`);
5129
+ if (existsSync28(resolve16(dir, "LICENSE"))) {
5130
+ passes.push("LICENSE exists at marketplace root");
5131
+ } else {
5132
+ warnings.push("No LICENSE at marketplace root \u2014 recommended");
5133
+ }
5134
+ return { errors, warnings, passes };
5135
+ }
5136
+ };
5137
+ });
5138
+
5139
+ // src/validators/codex/mcp.ts
5140
+ import { existsSync as existsSync29 } from "fs";
5141
+ import { resolve as resolve17 } from "path";
5142
+ var codexMcpValidator;
5143
+ var init_mcp2 = __esm(() => {
5144
+ codexMcpValidator = {
5145
+ id: "codex:mcp",
5146
+ provider: "codex",
5147
+ name: "Codex MCP Config",
5148
+ description: "Validates .mcp.json (or inline via plugin.json mcpServers): server entries (stdio: command+args, or url), env, cwd, substitutions per Codex MCP support",
5149
+ detect(dir) {
5150
+ return existsSync29(resolve17(dir, ".mcp.json"));
5151
+ },
5152
+ async validate(dir, _opts) {
5153
+ const errors = [];
5154
+ const warnings = [];
5155
+ const passes = [];
5156
+ const mcpPath = resolve17(dir, ".mcp.json");
5157
+ let config;
5158
+ try {
5159
+ const raw = await Bun.file(mcpPath).text();
5160
+ config = JSON.parse(raw);
5161
+ passes.push(".mcp.json is valid JSON");
5162
+ } catch {
5163
+ errors.push(".mcp.json is missing or invalid JSON");
5164
+ return { errors, warnings, passes };
5165
+ }
5166
+ if (typeof config !== "object" || Array.isArray(config)) {
5167
+ errors.push(".mcp.json must be a JSON object with server name keys");
5168
+ return { errors, warnings, passes };
5169
+ }
5170
+ const serverNames = Object.keys(config);
5171
+ if (serverNames.length === 0) {
5172
+ warnings.push(".mcp.json is empty \u2014 no servers defined");
5173
+ return { errors, warnings, passes };
5174
+ }
5175
+ passes.push(`${serverNames.length} server(s) defined`);
5176
+ for (const [name, entry] of Object.entries(config)) {
5177
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
5178
+ errors.push(`mcp server "${name}": definition must be an object`);
5179
+ continue;
5180
+ }
5181
+ const e = entry;
5182
+ const hasCommand = typeof e.command === "string";
5183
+ const hasUrl = typeof e.url === "string";
5184
+ if (!hasCommand && !hasUrl) {
5185
+ errors.push(`mcp server "${name}": must have either "command" (for stdio) or "url" (for SSE/HTTP)`);
5186
+ }
5187
+ if (hasCommand && !Array.isArray(e.args)) {
5188
+ warnings.push(`mcp server "${name}": "command" present but no "args" array (ok for some servers)`);
5189
+ }
5190
+ if (hasUrl && hasCommand) {
5191
+ warnings.push(`mcp server "${name}": both "command" and "url" present \u2014 usually one or the other`);
5192
+ }
5193
+ if (e.env && typeof e.env === "object") {
5194
+ passes.push(`mcp server "${name}": has env`);
5195
+ }
5196
+ if (typeof e.cwd === "string") {
5197
+ passes.push(`mcp server "${name}": has cwd`);
5198
+ }
5199
+ const hasSubs = JSON.stringify(e).match(/\$\{CODEX_|CLAUDE_PLUGIN_|user_config\.|ENV_VAR\}/);
5200
+ if (hasSubs) {
5201
+ passes.push(`mcp server "${name}": uses substitutions (e.g. \${CODEX_*} or env)`);
5202
+ }
5203
+ }
5204
+ return { errors, warnings, passes };
5205
+ }
5206
+ };
5207
+ });
5208
+
5209
+ // src/validators/codex/skill.ts
5210
+ import { existsSync as existsSync30 } from "fs";
5211
+ import { resolve as resolve18 } from "path";
5212
+ var codexSkillValidator;
5213
+ var init_skill2 = __esm(() => {
5214
+ init_skill_validate();
5215
+ codexSkillValidator = {
5216
+ id: "codex:skill",
5217
+ provider: "codex",
5218
+ name: "Codex Skill",
5219
+ description: "Validates SKILL.md (shared format): frontmatter (name/description), body, supporting files, substitutions. Codex uses the same SKILL.md spec as other providers.",
5220
+ detect(dir) {
5221
+ return existsSync30(resolve18(dir, "SKILL.md"));
5222
+ },
5223
+ async validate(dir, _opts) {
5224
+ const loaded = await loadSkill(dir);
5225
+ if (!loaded.ok) {
5226
+ return {
5227
+ errors: [loaded.error],
5228
+ warnings: [],
5229
+ passes: []
5230
+ };
5231
+ }
5232
+ const { model, existingDirs } = loaded;
5233
+ return validateSkillModel(model, { existingDirs: [...existingDirs] });
5234
+ }
5235
+ };
5236
+ });
5237
+
5238
+ // src/validators/cursor/plugin.ts
5239
+ import { existsSync as existsSync31, readdirSync as readdirSync14 } from "fs";
5240
+ import { resolve as resolve19, join as join22 } from "path";
5241
+ var NAME_REGEX4, cursorPluginValidator;
5242
+ var init_plugin3 = __esm(() => {
5243
+ NAME_REGEX4 = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
5244
+ cursorPluginValidator = {
5245
+ id: "cursor:plugin",
5246
+ provider: "cursor",
5247
+ name: "Cursor Plugin",
5248
+ description: "Validates .cursor-plugin/plugin.json manifest (skills as directory string; mcpServers support)",
5249
+ detect(dir) {
5250
+ return existsSync31(resolve19(dir, ".cursor-plugin", "plugin.json"));
5251
+ },
5252
+ async validate(dir, _opts) {
5253
+ const errors = [];
5254
+ const warnings = [];
5255
+ const passes = [];
5256
+ const manifestPath = resolve19(dir, ".cursor-plugin", "plugin.json");
5257
+ let manifest;
5258
+ try {
5259
+ const raw = await Bun.file(manifestPath).text();
5260
+ manifest = JSON.parse(raw);
5261
+ passes.push(".cursor-plugin/plugin.json is valid JSON");
5262
+ } catch {
5263
+ errors.push(".cursor-plugin/plugin.json is missing or invalid JSON");
5264
+ return { errors, warnings, passes };
5265
+ }
5266
+ if (!manifest.name) {
5267
+ errors.push('Missing required field: "name"');
5268
+ } else {
5269
+ const name = String(manifest.name);
5270
+ if (!NAME_REGEX4.test(name)) {
5271
+ errors.push(`Invalid name format: "${name}" \u2014 must be kebab-case (a-z, 0-9, hyphens)`);
5272
+ } else {
5273
+ passes.push(`name: "${name}"`);
5274
+ }
5275
+ }
5276
+ if (manifest.skills === undefined) {
5277
+ errors.push('Missing required field: "skills" (must be a directory string like "./skills")');
5278
+ } else if (typeof manifest.skills !== "string") {
5279
+ errors.push('"skills" must be a string directory path');
5280
+ } else {
5281
+ const s = manifest.skills;
5282
+ if (!s.startsWith("./")) {
5283
+ warnings.push('"skills" should start with "./"');
5284
+ }
5285
+ passes.push(`skills: "${s}" (directory string)`);
5286
+ }
5287
+ if (manifest.mcpServers !== undefined) {
5288
+ if (typeof manifest.mcpServers === "string") {
5289
+ passes.push(`mcpServers: "${manifest.mcpServers}"`);
5290
+ } else {
5291
+ warnings.push('"mcpServers" should be a string path when present');
5292
+ }
5293
+ }
5294
+ if (manifest.displayName) {
5295
+ passes.push(`displayName: "${manifest.displayName}"`);
5296
+ } else {
5297
+ warnings.push("displayName recommended for Cursor UI");
5298
+ }
5299
+ if (manifest.version !== undefined) {
5300
+ const v = String(manifest.version);
5301
+ if (!/^\d+\.\d+\.\d+/.test(v)) {
5302
+ warnings.push(`version "${v}" should look like semver`);
3537
5303
  } else {
3538
- errors.push("keywords must be an array of strings");
5304
+ passes.push(`version: "${v}"`);
3539
5305
  }
5306
+ } else {
5307
+ passes.push("version omitted (git commit SHA used as version key)");
3540
5308
  }
3541
- if (manifest.defaultEnabled !== undefined) {
3542
- passes.push(`defaultEnabled: ${manifest.defaultEnabled}`);
5309
+ if (manifest.description !== undefined) {
5310
+ const desc = String(manifest.description);
5311
+ if (desc.length < 10) {
5312
+ warnings.push(`Description is very short (${desc.length} chars) \u2014 50-200 chars recommended`);
5313
+ } else {
5314
+ passes.push("description field present");
5315
+ }
5316
+ } else {
5317
+ warnings.push('Missing "description" (recommended)');
3543
5318
  }
5319
+ if (manifest.author)
5320
+ passes.push("author present");
5321
+ if (manifest.license)
5322
+ passes.push(`license: "${manifest.license}"`);
3544
5323
  if (manifest.homepage)
3545
5324
  passes.push("homepage present");
3546
5325
  if (manifest.repository)
3547
5326
  passes.push("repository present");
3548
- const unknown = Object.keys(manifest).filter((k) => !KNOWN_FIELDS2.has(k));
3549
- for (const k of unknown) {
3550
- const sug = suggestField(k);
3551
- const hint = sug ? ` (did you mean "${sug}"?)` : "";
3552
- warnings.push(`Unrecognized top-level field "${k}"${hint} \u2014 will be ignored at runtime (allowed for cross-tool manifest compatibility).`);
3553
- }
3554
- const handleField = (field, val) => {
3555
- if (val === undefined || val === null)
3556
- return;
3557
- if (isRelativePathLike(val) || Array.isArray(val) && val.every(isRelativePathLike)) {
3558
- const arr = Array.isArray(val) ? val : [val];
3559
- for (const p of arr) {
3560
- const s = String(p);
3561
- if (!RELATIVE_PATH_REGEX.test(s)) {
3562
- errors.push(`${field}: path "${s}" must start with "./"`);
3563
- } else if (s.includes("..")) {
3564
- errors.push(`${field}: path "${s}" must not use ".." (paths are confined to the plugin tree after cache copy)`);
3565
- } else if (existsSync14(resolve6(dir, s))) {
3566
- passes.push(`${field}: path "${s}" exists`);
3567
- } else {
3568
- warnings.push(`${field}: path "${s}" does not exist on disk`);
3569
- }
3570
- }
3571
- if (field === "skills") {
3572
- passes.push(`${field}: augments the default skills/ (both are scanned)`);
3573
- } else if (REPLACES_DEFAULT.has(field)) {
3574
- passes.push(`${field}: custom path replaces default ${field}/ scan`);
3575
- } else {
3576
- passes.push(`${field}: custom path or config (merge rules apply)`);
3577
- }
3578
- } else if (typeof val === "object") {
3579
- passes.push(`${field}: inline ${field} config present`);
3580
- }
3581
- };
3582
- ["skills", "commands", "agents", "hooks", "mcpServers", "outputStyles", "lspServers"].forEach((f) => {
3583
- if (manifest[f] !== undefined)
3584
- handleField(f, manifest[f]);
3585
- });
3586
- if (manifest.experimental && typeof manifest.experimental === "object") {
3587
- const exp = manifest.experimental;
3588
- if (exp.themes !== undefined)
3589
- handleField("experimental.themes", exp.themes);
3590
- if (exp.monitors !== undefined)
3591
- handleField("experimental.monitors", exp.monitors);
3592
- passes.push("experimental section present (themes and monitors are experimental components)");
3593
- }
3594
- if (manifest.userConfig && typeof manifest.userConfig === "object") {
3595
- const keys = Object.keys(manifest.userConfig);
3596
- passes.push(`userConfig: ${keys.length} user-configurable value(s) declared`);
3597
- for (const k of keys) {
3598
- const opt = manifest.userConfig[k];
3599
- if (!opt || !opt.type || !opt.title) {
3600
- warnings.push(`userConfig.${k} is missing required "type" and/or "title"`);
3601
- }
3602
- }
3603
- }
3604
- if (Array.isArray(manifest.channels)) {
3605
- passes.push(`channels: ${manifest.channels.length} channel(s) (each binds to an mcpServer)`);
3606
- manifest.channels.forEach((ch, i) => {
3607
- if (!ch?.server)
3608
- warnings.push(`channels[${i}]: "server" is required and must match an mcpServers key`);
3609
- });
5327
+ if (Array.isArray(manifest.keywords)) {
5328
+ passes.push(`keywords: [${manifest.keywords.join(", ")}]`);
3610
5329
  }
3611
- if (Array.isArray(manifest.dependencies)) {
3612
- passes.push(`dependencies: declares ${manifest.dependencies.length} plugin dependency/ies`);
3613
- }
3614
- const skillsDir = resolve6(dir, "skills");
3615
- if (existsSync14(skillsDir)) {
3616
- const entries = readdirSync6(skillsDir, { withFileTypes: true }).filter((e) => e.isDirectory());
5330
+ const skillsDir = resolve19(dir, "skills");
5331
+ if (existsSync31(skillsDir)) {
5332
+ const entries = readdirSync14(skillsDir, { withFileTypes: true }).filter((e) => e.isDirectory());
3617
5333
  for (const e of entries) {
3618
- const md = join12(skillsDir, e.name, "SKILL.md");
3619
- if (existsSync14(md)) {
5334
+ const md = join22(skillsDir, e.name, "SKILL.md");
5335
+ if (existsSync31(md)) {
3620
5336
  passes.push(`skills/${e.name}/SKILL.md exists`);
3621
5337
  } else {
3622
5338
  errors.push(`skills/${e.name}/ is missing SKILL.md`);
3623
5339
  }
3624
5340
  }
3625
- if (manifest.skills !== undefined) {
3626
- warnings.push('Default skills/ dir co-exists with manifest "skills" \u2014 manifest path is authoritative; default folder ignored for loading');
3627
- }
3628
- }
3629
- const commandsDir = resolve6(dir, "commands");
3630
- if (existsSync14(commandsDir)) {
3631
- const mds = readdirSync6(commandsDir).filter((f) => f.endsWith(".md"));
3632
- if (mds.length) {
3633
- passes.push(`commands/ has ${mds.length} .md file(s)`);
3634
- }
3635
- if (manifest.commands !== undefined) {
3636
- warnings.push('commands/ co-exists with manifest "commands" \u2014 manifest replaces default (dir ignored)');
3637
- }
3638
5341
  }
3639
- const agentsDir = resolve6(dir, "agents");
3640
- if (existsSync14(agentsDir)) {
3641
- const mds = readdirSync6(agentsDir).filter((f) => f.endsWith(".md"));
3642
- if (mds.length) {
3643
- passes.push(`agents/ has ${mds.length} .md file(s)`);
3644
- }
3645
- if (manifest.agents !== undefined) {
3646
- warnings.push('agents/ co-exists with manifest "agents" \u2014 manifest replaces default (dir ignored)');
5342
+ if (typeof manifest.mcpServers === "string") {
5343
+ const mcpRef = manifest.mcpServers;
5344
+ if (mcpRef.startsWith("./") || mcpRef.startsWith("../")) {
5345
+ const mcpPath = resolve19(dir, mcpRef);
5346
+ if (existsSync31(mcpPath)) {
5347
+ passes.push(`mcpServers file exists at ${mcpRef}`);
5348
+ } else {
5349
+ warnings.push(`mcpServers path "${mcpRef}" does not exist on disk`);
5350
+ }
3647
5351
  }
3648
5352
  }
3649
- if (existsSync14(resolve6(dir, "output-styles"))) {
3650
- passes.push("output-styles/ directory present");
3651
- if (manifest.outputStyles)
3652
- warnings.push("output-styles/ co-exists with manifest outputStyles \u2014 manifest wins");
3653
- }
3654
- if (existsSync14(resolve6(dir, "themes")))
3655
- passes.push("themes/ present (experimental)");
3656
- if (existsSync14(resolve6(dir, "monitors")) || manifest.experimental?.monitors) {
3657
- passes.push("monitors config present (experimental)");
3658
- }
3659
- if (existsSync14(resolve6(dir, "bin")))
3660
- passes.push("bin/ present (adds executables to Bash tool $PATH)");
3661
- if (existsSync14(resolve6(dir, "settings.json")))
3662
- passes.push("settings.json present (plugin defaults for agent/statusline)");
3663
- if (existsSync14(resolve6(dir, "README.md")))
3664
- passes.push("README.md present");
3665
- if (existsSync14(resolve6(dir, ".mcp.json")))
3666
- passes.push(".mcp.json present (validated by claude:mcp)");
3667
- if (existsSync14(resolve6(dir, ".lsp.json")))
3668
- passes.push(".lsp.json present (validated by claude:lsp when registered)");
3669
- if (existsSync14(resolve6(dir, "hooks/hooks.json")) || existsSync14(resolve6(dir, "hooks.json"))) {
3670
- passes.push("hooks config present (validated by claude:hooks)");
3671
- }
3672
- if (existsSync14(resolve6(dir, "SKILL.md")) && !existsSync14(skillsDir) && manifest.skills === undefined) {
3673
- passes.push('Root SKILL.md detected \u2014 plugin will be treated as a single-skill plugin (prefer frontmatter "name" for stable /command)');
5353
+ const known = new Set([
5354
+ "name",
5355
+ "displayName",
5356
+ "version",
5357
+ "description",
5358
+ "author",
5359
+ "homepage",
5360
+ "repository",
5361
+ "license",
5362
+ "keywords",
5363
+ "skills",
5364
+ "mcpServers"
5365
+ ]);
5366
+ const unknown = Object.keys(manifest).filter((k) => !known.has(k));
5367
+ for (const k of unknown) {
5368
+ warnings.push(`Unrecognized field "${k}" \u2014 will be ignored (for compatibility)`);
3674
5369
  }
3675
5370
  return { errors, warnings, passes };
3676
5371
  }
3677
5372
  };
3678
5373
  });
3679
5374
 
3680
- // src/validators/claude/marketplace.ts
3681
- import { existsSync as existsSync15, readdirSync as readdirSync7 } from "fs";
3682
- import { resolve as resolve7, join as join13 } from "path";
3683
- var claudeMarketplaceValidator;
3684
- var init_marketplace = __esm(() => {
3685
- claudeMarketplaceValidator = {
3686
- id: "claude:marketplace",
3687
- provider: "claude",
3688
- name: "Claude Plugin Marketplace",
3689
- description: "Validates marketplace structure: plugins/ directory with valid plugin subdirectories",
5375
+ // src/validators/cursor/marketplace.ts
5376
+ import { existsSync as existsSync32, readdirSync as readdirSync15 } from "fs";
5377
+ import { resolve as resolve20, join as join23 } from "path";
5378
+ var cursorMarketplaceValidator;
5379
+ var init_marketplace3 = __esm(() => {
5380
+ cursorMarketplaceValidator = {
5381
+ id: "cursor:marketplace",
5382
+ provider: "cursor",
5383
+ name: "Cursor Plugin Marketplace",
5384
+ description: "Validates .cursor-plugin/marketplace.json (string sources + metadata.pluginRoot)",
3690
5385
  detect(dir) {
3691
- const pluginsDir = resolve7(dir, "plugins");
3692
- if (!existsSync15(pluginsDir))
5386
+ if (existsSync32(resolve20(dir, ".cursor-plugin", "marketplace.json")))
5387
+ return true;
5388
+ const pluginsDir = resolve20(dir, "plugins");
5389
+ if (!existsSync32(pluginsDir))
3693
5390
  return false;
3694
5391
  try {
3695
- const entries = readdirSync7(pluginsDir, { withFileTypes: true });
5392
+ const entries = readdirSync15(pluginsDir, { withFileTypes: true });
3696
5393
  for (const entry of entries) {
3697
5394
  if (!entry.isDirectory())
3698
5395
  continue;
3699
- const hasSkills = existsSync15(join13(pluginsDir, entry.name, "skills"));
3700
- const hasManifest = existsSync15(join13(pluginsDir, entry.name, ".claude-plugin", "plugin.json"));
5396
+ const hasSkills = existsSync32(join23(pluginsDir, entry.name, "skills"));
5397
+ const hasManifest = existsSync32(join23(pluginsDir, entry.name, ".cursor-plugin", "plugin.json"));
3701
5398
  if (hasSkills || hasManifest)
3702
5399
  return true;
3703
5400
  }
@@ -3708,195 +5405,171 @@ var init_marketplace = __esm(() => {
3708
5405
  const errors = [];
3709
5406
  const warnings = [];
3710
5407
  const passes = [];
3711
- const pluginsDir = resolve7(dir, "plugins");
3712
- if (!existsSync15(pluginsDir)) {
3713
- errors.push("Missing plugins/ directory");
3714
- return { errors, warnings, passes };
3715
- }
3716
- passes.push("plugins/ directory exists");
3717
- const pluginEntries = readdirSync7(pluginsDir, { withFileTypes: true }).filter((e) => e.isDirectory());
3718
- if (pluginEntries.length === 0) {
3719
- errors.push("plugins/ directory is empty \u2014 expected at least one plugin");
5408
+ const cursorMktPath = resolve20(dir, ".cursor-plugin", "marketplace.json");
5409
+ const hasCursorMkt = existsSync32(cursorMktPath);
5410
+ const pluginsDir = resolve20(dir, "plugins");
5411
+ const hasPluginsDirLayout = existsSync32(pluginsDir);
5412
+ if (!hasCursorMkt && !hasPluginsDirLayout) {
5413
+ errors.push("Missing .cursor-plugin/marketplace.json or plugins/ directory");
3720
5414
  return { errors, warnings, passes };
3721
5415
  }
3722
- passes.push(`${pluginEntries.length} plugin(s) found`);
3723
- if (existsSync15(resolve7(dir, "README.md"))) {
3724
- passes.push("README.md exists at marketplace root");
3725
- } else {
3726
- warnings.push("No README.md at marketplace root \u2014 recommended for discoverability");
3727
- }
3728
- if (existsSync15(resolve7(dir, "LICENSE"))) {
3729
- passes.push("LICENSE exists at marketplace root");
3730
- } else {
3731
- warnings.push("No LICENSE at marketplace root \u2014 recommended");
3732
- }
3733
- for (const plugin of pluginEntries) {
3734
- const pluginPath = join13(pluginsDir, plugin.name);
3735
- const hasSkills = existsSync15(join13(pluginPath, "skills"));
3736
- const hasManifest = existsSync15(join13(pluginPath, ".claude-plugin", "plugin.json"));
3737
- const hasReadme = existsSync15(join13(pluginPath, "README.md"));
3738
- if (hasManifest || hasSkills) {
3739
- passes.push(`Plugin "${plugin.name}" has ${hasManifest ? "manifest" : "skills/"}`);
3740
- } else {
3741
- warnings.push(`Plugin "${plugin.name}" has neither .claude-plugin/plugin.json nor skills/`);
5416
+ if (hasCursorMkt) {
5417
+ let mkt;
5418
+ try {
5419
+ const raw = await Bun.file(cursorMktPath).text();
5420
+ mkt = JSON.parse(raw);
5421
+ passes.push(".cursor-plugin/marketplace.json is valid JSON");
5422
+ } catch {
5423
+ errors.push(".cursor-plugin/marketplace.json is missing or invalid JSON");
5424
+ return { errors, warnings, passes };
3742
5425
  }
3743
- if (!hasReadme) {
3744
- warnings.push(`Plugin "${plugin.name}" has no README.md`);
5426
+ if (mkt.name) {
5427
+ passes.push(`name: "${mkt.name}"`);
5428
+ } else {
5429
+ warnings.push('Missing "name" at marketplace root');
3745
5430
  }
3746
- }
3747
- return { errors, warnings, passes };
3748
- }
3749
- };
3750
- });
3751
-
3752
- // src/validators/claude/hooks.ts
3753
- import { existsSync as existsSync16 } from "fs";
3754
- import { resolve as resolve8 } from "path";
3755
- var KNOWN_EVENTS, claudeHooksValidator;
3756
- var init_hooks = __esm(() => {
3757
- KNOWN_EVENTS = [
3758
- "SessionStart",
3759
- "Setup",
3760
- "UserPromptSubmit",
3761
- "UserPromptExpansion",
3762
- "PreToolUse",
3763
- "PermissionRequest",
3764
- "PermissionDenied",
3765
- "PostToolUse",
3766
- "PostToolUseFailure",
3767
- "PostToolBatch",
3768
- "Notification",
3769
- "MessageDisplay",
3770
- "SubagentStart",
3771
- "SubagentStop",
3772
- "TaskCreated",
3773
- "TaskCompleted",
3774
- "Stop",
3775
- "StopFailure",
3776
- "TeammateIdle",
3777
- "InstructionsLoaded",
3778
- "ConfigChange",
3779
- "CwdChanged",
3780
- "FileChanged",
3781
- "WorktreeCreate",
3782
- "WorktreeRemove",
3783
- "PreCompact",
3784
- "PostCompact",
3785
- "Elicitation",
3786
- "ElicitationResult",
3787
- "SessionEnd"
3788
- ];
3789
- claudeHooksValidator = {
3790
- id: "claude:hooks",
3791
- provider: "claude",
3792
- name: "Claude Hooks",
3793
- description: "Validates hooks/hooks.json (or root hooks.json): all lifecycle events per Plugins reference, hook group structure (matcher + hooks[]), supported hook types (command, http, mcp_tool, prompt, agent)",
3794
- detect(dir) {
3795
- return existsSync16(resolve8(dir, "hooks", "hooks.json")) || existsSync16(resolve8(dir, "hooks.json"));
3796
- },
3797
- async validate(dir, _opts) {
3798
- const errors = [];
3799
- const warnings = [];
3800
- const passes = [];
3801
- const hooksPath = existsSync16(resolve8(dir, "hooks", "hooks.json")) ? resolve8(dir, "hooks", "hooks.json") : resolve8(dir, "hooks.json");
3802
- let config;
3803
- try {
3804
- const raw = await Bun.file(hooksPath).text();
3805
- config = JSON.parse(raw);
3806
- passes.push("hooks.json is valid JSON");
3807
- } catch {
3808
- errors.push("hooks.json is missing or invalid JSON");
3809
- return { errors, warnings, passes };
3810
- }
3811
- const eventNames = Object.keys(config);
3812
- for (const name of eventNames) {
3813
- if (KNOWN_EVENTS.includes(name)) {
3814
- passes.push(`Event "${name}" is a known lifecycle event`);
5431
+ if (mkt.metadata && typeof mkt.metadata === "object") {
5432
+ passes.push("metadata present");
5433
+ if (mkt.metadata.pluginRoot) {
5434
+ passes.push(`metadata.pluginRoot: "${mkt.metadata.pluginRoot}"`);
5435
+ }
5436
+ if (mkt.metadata.description) {
5437
+ passes.push("metadata.description present");
5438
+ }
3815
5439
  } else {
3816
- warnings.push(`Unknown event name: "${name}" \u2014 see full list in Plugins reference (SessionStart, PreToolUse, PostToolUse, Stop, ...)`);
5440
+ warnings.push('Recommended: "metadata" with pluginRoot and description');
3817
5441
  }
3818
- }
3819
- for (const [event, groups] of Object.entries(config)) {
3820
- if (!Array.isArray(groups)) {
3821
- errors.push(`Event "${event}": value must be an array of hook groups`);
3822
- continue;
5442
+ if (mkt.owner) {
5443
+ passes.push("owner present");
3823
5444
  }
3824
- groups.forEach((group, gi) => {
3825
- if (!group || typeof group !== "object") {
3826
- errors.push(`${event}[${gi}]: hook group must be an object`);
3827
- return;
3828
- }
3829
- if (group.matcher !== undefined && typeof group.matcher !== "string") {
3830
- warnings.push(`${event}[${gi}]: "matcher" should be a string (e.g. "Write|Edit" or glob)`);
5445
+ if (!Array.isArray(mkt.plugins) || mkt.plugins.length === 0) {
5446
+ errors.push('"plugins" must be a non-empty array');
5447
+ return { errors, warnings, passes };
5448
+ }
5449
+ passes.push(`${mkt.plugins.length} plugin(s) declared`);
5450
+ const pluginRoot = mkt.metadata && mkt.metadata.pluginRoot ? String(mkt.metadata.pluginRoot) : ".";
5451
+ for (const [i, p] of mkt.plugins.entries()) {
5452
+ if (!p || typeof p !== "object") {
5453
+ errors.push(`plugins[${i}]: must be an object`);
5454
+ continue;
3831
5455
  }
3832
- const hooksArr = group.hooks;
3833
- if (!Array.isArray(hooksArr)) {
3834
- errors.push(`${event}[${gi}]: missing or invalid "hooks" array`);
3835
- return;
5456
+ if (p.name) {
5457
+ passes.push(`plugins[${i}].name: "${p.name}"`);
5458
+ } else {
5459
+ errors.push(`plugins[${i}]: missing "name"`);
3836
5460
  }
3837
- hooksArr.forEach((h, hi) => {
3838
- if (!h || typeof h !== "object" || !h.type) {
3839
- errors.push(`${event}[${gi}].hooks[${hi}]: must have "type"`);
3840
- return;
3841
- }
3842
- const t = String(h.type);
3843
- if (!["command", "http", "mcp_tool", "prompt", "agent"].includes(t)) {
3844
- warnings.push(`${event}[${gi}].hooks[${hi}]: unknown type "${t}" (valid: command, http, mcp_tool, prompt, agent)`);
3845
- }
3846
- if (t === "command" && !h.command) {
3847
- errors.push(`${event}[${gi}].hooks[${hi}]: type=command requires "command"`);
3848
- }
3849
- if (t === "http" && !h.url) {
3850
- errors.push(`${event}[${gi}].hooks[${hi}]: type=http requires "url"`);
3851
- }
3852
- if (h.command && typeof h.command === "string" && /\$\{CLAUDE_/.test(h.command)) {
3853
- passes.push(`${event}[${gi}].hooks[${hi}]: uses plugin env substitution`);
5461
+ if (p.source !== undefined) {
5462
+ const src = String(p.source);
5463
+ passes.push(`plugins[${i}].source: "${src}"`);
5464
+ const srcDir = resolve20(dir, pluginRoot, src);
5465
+ if (existsSync32(srcDir)) {
5466
+ const hasManifest = existsSync32(resolve20(srcDir, ".cursor-plugin", "plugin.json"));
5467
+ const hasSkills = existsSync32(resolve20(srcDir, "skills"));
5468
+ if (hasManifest || hasSkills) {
5469
+ passes.push(`plugins[${i}]: source exists (${hasManifest ? "manifest" : "skills/"})`);
5470
+ } else {
5471
+ warnings.push(`plugins[${i}].source "${src}" exists but lacks plugin markers`);
5472
+ }
5473
+ } else {
5474
+ warnings.push(`plugins[${i}].source path "${src}" (under ${pluginRoot}) does not exist`);
5475
+ }
5476
+ } else {
5477
+ const implicitSrc = resolve20(dir, pluginRoot, p.name || "");
5478
+ if (p.name && existsSync32(implicitSrc)) {
5479
+ passes.push(`plugins[${i}]: implicit source via name under ${pluginRoot}`);
5480
+ } else {
5481
+ warnings.push(`plugins[${i}]: missing "source" (and no implicit dir)`);
3854
5482
  }
3855
- });
3856
- if (hooksArr.length > 0) {
3857
- passes.push(`Event "${event}" has ${hooksArr.length} hook action(s)`);
3858
5483
  }
3859
- });
5484
+ if (p.description)
5485
+ passes.push(`plugins[${i}].description present`);
5486
+ if (p.category) {
5487
+ passes.push(`plugins[${i}].category: "${p.category}"`);
5488
+ }
5489
+ if (p.homepage) {
5490
+ passes.push(`plugins[${i}].homepage present`);
5491
+ }
5492
+ }
5493
+ if (existsSync32(resolve20(dir, "README.md"))) {
5494
+ passes.push("README.md exists at marketplace root");
5495
+ } else {
5496
+ warnings.push("No README.md at marketplace root \u2014 recommended for discoverability");
5497
+ }
5498
+ if (existsSync32(resolve20(dir, "LICENSE"))) {
5499
+ passes.push("LICENSE exists at marketplace root");
5500
+ } else {
5501
+ warnings.push("No LICENSE at marketplace root \u2014 recommended");
5502
+ }
5503
+ return { errors, warnings, passes };
5504
+ }
5505
+ if (hasPluginsDirLayout) {
5506
+ passes.push("plugins/ directory exists");
5507
+ const pluginEntries = readdirSync15(pluginsDir, { withFileTypes: true }).filter((e) => e.isDirectory());
5508
+ if (pluginEntries.length === 0) {
5509
+ errors.push("plugins/ directory is empty \u2014 expected at least one plugin");
5510
+ return { errors, warnings, passes };
5511
+ }
5512
+ passes.push(`${pluginEntries.length} plugin(s) found`);
5513
+ if (existsSync32(resolve20(dir, "README.md"))) {
5514
+ passes.push("README.md exists at marketplace root");
5515
+ } else {
5516
+ warnings.push("No README.md at marketplace root \u2014 recommended");
5517
+ }
5518
+ for (const plugin of pluginEntries) {
5519
+ const pluginPath = join23(pluginsDir, plugin.name);
5520
+ const hasSkills = existsSync32(join23(pluginPath, "skills"));
5521
+ const hasManifest = existsSync32(join23(pluginPath, ".cursor-plugin", "plugin.json"));
5522
+ if (hasManifest || hasSkills) {
5523
+ passes.push(`Plugin "${plugin.name}" has ${hasManifest ? "manifest" : "skills/"}`);
5524
+ }
5525
+ }
5526
+ return { errors, warnings, passes };
3860
5527
  }
3861
5528
  return { errors, warnings, passes };
3862
5529
  }
3863
5530
  };
3864
5531
  });
3865
5532
 
3866
- // src/validators/claude/mcp.ts
3867
- import { existsSync as existsSync17 } from "fs";
3868
- import { resolve as resolve9 } from "path";
3869
- var claudeMcpValidator;
3870
- var init_mcp = __esm(() => {
3871
- claudeMcpValidator = {
3872
- id: "claude:mcp",
3873
- provider: "claude",
3874
- name: "Claude MCP Config",
3875
- description: "Validates .mcp.json (or inline via plugin.json mcpServers): server entries (stdio: command+args, or url), env, cwd, ${CLAUDE_PLUGIN_ROOT} etc. substitutions per Plugins reference",
5533
+ // src/validators/cursor/mcp.ts
5534
+ import { existsSync as existsSync33 } from "fs";
5535
+ import { resolve as resolve21 } from "path";
5536
+ var cursorMcpValidator;
5537
+ var init_mcp3 = __esm(() => {
5538
+ cursorMcpValidator = {
5539
+ id: "cursor:mcp",
5540
+ provider: "cursor",
5541
+ name: "Cursor MCP Config",
5542
+ description: "Validates mcp.json (Cursor uses no leading dot; supports mcpServers wrapper or direct server map)",
3876
5543
  detect(dir) {
3877
- return existsSync17(resolve9(dir, ".mcp.json"));
5544
+ return existsSync33(resolve21(dir, "mcp.json"));
3878
5545
  },
3879
5546
  async validate(dir, _opts) {
3880
5547
  const errors = [];
3881
5548
  const warnings = [];
3882
5549
  const passes = [];
3883
- const mcpPath = resolve9(dir, ".mcp.json");
3884
- let config;
5550
+ const mcpPath = resolve21(dir, "mcp.json");
5551
+ let rawConfig;
3885
5552
  try {
3886
5553
  const raw = await Bun.file(mcpPath).text();
3887
- config = JSON.parse(raw);
3888
- passes.push(".mcp.json is valid JSON");
5554
+ rawConfig = JSON.parse(raw);
5555
+ passes.push("mcp.json is valid JSON");
3889
5556
  } catch {
3890
- errors.push(".mcp.json is missing or invalid JSON");
5557
+ errors.push("mcp.json is missing or invalid JSON");
3891
5558
  return { errors, warnings, passes };
3892
5559
  }
3893
- if (typeof config !== "object" || Array.isArray(config)) {
3894
- errors.push(".mcp.json must be a JSON object with server name keys");
5560
+ let config;
5561
+ if (rawConfig && typeof rawConfig === "object" && !Array.isArray(rawConfig) && rawConfig.mcpServers && typeof rawConfig.mcpServers === "object") {
5562
+ config = rawConfig.mcpServers;
5563
+ passes.push("mcp.json uses mcpServers wrapper (normalized)");
5564
+ } else if (typeof rawConfig === "object" && !Array.isArray(rawConfig)) {
5565
+ config = rawConfig;
5566
+ } else {
5567
+ errors.push("mcp.json must be an object (or contain mcpServers object)");
3895
5568
  return { errors, warnings, passes };
3896
5569
  }
3897
5570
  const serverNames = Object.keys(config);
3898
5571
  if (serverNames.length === 0) {
3899
- warnings.push(".mcp.json is empty \u2014 no servers defined");
5572
+ warnings.push("mcp.json is empty \u2014 no servers defined");
3900
5573
  return { errors, warnings, passes };
3901
5574
  }
3902
5575
  passes.push(`${serverNames.length} server(s) defined`);
@@ -3923,9 +5596,9 @@ var init_mcp = __esm(() => {
3923
5596
  if (typeof e.cwd === "string") {
3924
5597
  passes.push(`mcp server "${name}": has cwd`);
3925
5598
  }
3926
- const hasSubs = JSON.stringify(e).match(/\$\{CLAUDE_PLUGIN_(ROOT|DATA)|CLAUDE_PROJECT_DIR|user_config\.|ENV_VAR\}/);
5599
+ const hasSubs = JSON.stringify(e).match(/\$\{CODEX_|CLAUDE_PLUGIN_|CURSOR_|user_config\.|ENV_VAR\}/);
3927
5600
  if (hasSubs) {
3928
- passes.push(`mcp server "${name}": uses \${CLAUDE_PLUGIN_*} / user_config / env substitution`);
5601
+ passes.push(`mcp server "${name}": uses substitutions`);
3929
5602
  }
3930
5603
  }
3931
5604
  return { errors, warnings, passes };
@@ -3933,365 +5606,468 @@ var init_mcp = __esm(() => {
3933
5606
  };
3934
5607
  });
3935
5608
 
3936
- // src/validators/claude/subagent.ts
3937
- import { existsSync as existsSync18, readdirSync as readdirSync8 } from "fs";
3938
- import { resolve as resolve10, join as join14 } from "path";
3939
- var claudeSubagentValidator;
3940
- var init_subagent = __esm(() => {
3941
- init_frontmatter();
3942
- claudeSubagentValidator = {
3943
- id: "claude:subagent",
3944
- provider: "claude",
3945
- name: "Claude Subagents",
3946
- description: "Validates agents/*.md (plugin subagents): frontmatter per spec (name, description, model, effort, maxTurns, tools, disallowedTools, skills, memory, background, isolation=worktree), body; warns on disallowed fields (hooks, mcpServers, permissionMode) for security",
5609
+ // src/validators/cursor/skill.ts
5610
+ import { existsSync as existsSync34 } from "fs";
5611
+ import { resolve as resolve22 } from "path";
5612
+ var cursorSkillValidator;
5613
+ var init_skill3 = __esm(() => {
5614
+ init_skill_validate();
5615
+ cursorSkillValidator = {
5616
+ id: "cursor:skill",
5617
+ provider: "cursor",
5618
+ name: "Cursor Skill",
5619
+ description: "Validates SKILL.md (shared format): frontmatter (name/description), body, supporting files, substitutions. Cursor uses the same SKILL.md spec as other providers.",
3947
5620
  detect(dir) {
3948
- const agentsDir = resolve10(dir, "agents");
3949
- if (!existsSync18(agentsDir))
3950
- return false;
3951
- try {
3952
- return readdirSync8(agentsDir).some((f) => f.endsWith(".md"));
3953
- } catch {
3954
- return false;
3955
- }
5621
+ return existsSync34(resolve22(dir, "SKILL.md"));
3956
5622
  },
3957
5623
  async validate(dir, _opts) {
3958
- const errors = [];
3959
- const warnings = [];
3960
- const passes = [];
3961
- const agentsDir = resolve10(dir, "agents");
3962
- const mdFiles = readdirSync8(agentsDir).filter((f) => f.endsWith(".md"));
3963
- if (mdFiles.length === 0) {
3964
- errors.push("agents/ directory has no .md files");
3965
- return { errors, warnings, passes };
3966
- }
3967
- passes.push(`${mdFiles.length} agent definition(s) found`);
3968
- const SUPPORTED = new Set([
3969
- "name",
3970
- "description",
3971
- "model",
3972
- "effort",
3973
- "maxTurns",
3974
- "tools",
3975
- "disallowedTools",
3976
- "skills",
3977
- "memory",
3978
- "background",
3979
- "isolation"
3980
- ]);
3981
- const DISALLOWED = new Set(["hooks", "mcpServers", "permissionMode"]);
3982
- for (const file of mdFiles) {
3983
- const filePath = join14(agentsDir, file);
3984
- const raw = await Bun.file(filePath).text();
3985
- try {
3986
- const parsed = parseFrontmatter(raw);
3987
- const fm = parsed.data;
3988
- if (Object.keys(fm).length === 0) {
3989
- warnings.push(`${file}: no YAML frontmatter (description recommended so Claude knows when to invoke)`);
3990
- } else {
3991
- if (fm.description) {
3992
- passes.push(`${file}: has frontmatter with description`);
3993
- } else {
3994
- warnings.push(`${file}: missing "description" in frontmatter`);
3995
- }
3996
- const usedSupported = [];
3997
- Object.keys(fm).forEach((k) => {
3998
- if (SUPPORTED.has(k))
3999
- usedSupported.push(k);
4000
- if (DISALLOWED.has(k)) {
4001
- errors.push(`${file}: frontmatter "${k}" is not supported for plugin-shipped agents (security restriction)`);
4002
- }
4003
- });
4004
- if (usedSupported.length) {
4005
- passes.push(`${file}: frontmatter fields: ${usedSupported.join(", ")}`);
4006
- }
4007
- if (fm.isolation !== undefined && fm.isolation !== "worktree") {
4008
- errors.push(`${file}: "isolation" must be "worktree" if present (only supported value for plugin agents)`);
4009
- }
4010
- if (fm.name && typeof fm.name === "string") {
4011
- passes.push(`${file}: name: "${fm.name}"`);
4012
- }
4013
- }
4014
- if (!parsed.content.trim()) {
4015
- errors.push(`${file}: body is empty`);
4016
- } else {
4017
- passes.push(`${file}: has agent system prompt body`);
4018
- }
4019
- } catch {
4020
- errors.push(`${file}: failed to parse`);
4021
- }
5624
+ const loaded = await loadSkill(dir);
5625
+ if (!loaded.ok) {
5626
+ return {
5627
+ errors: [loaded.error],
5628
+ warnings: [],
5629
+ passes: []
5630
+ };
4022
5631
  }
4023
- return { errors, warnings, passes };
5632
+ const { model, existingDirs } = loaded;
5633
+ return validateSkillModel(model, { existingDirs: [...existingDirs] });
4024
5634
  }
4025
5635
  };
4026
5636
  });
4027
5637
 
4028
- // src/validators/claude/command.ts
4029
- import { existsSync as existsSync19, readdirSync as readdirSync9 } from "fs";
4030
- import { resolve as resolve11, join as join15 } from "path";
4031
- var claudeCommandValidator;
4032
- var init_command = __esm(() => {
4033
- init_frontmatter();
4034
- claudeCommandValidator = {
4035
- id: "claude:command",
4036
- provider: "claude",
4037
- name: "Claude Commands",
4038
- description: "Validates commands/ (or legacy .claude/commands/) .md files: frontmatter (including rich skill fields), description, body",
5638
+ // src/validators/copilot/plugin.ts
5639
+ import { existsSync as existsSync35 } from "fs";
5640
+ import { resolve as resolve23 } from "path";
5641
+ var NAME_REGEX5, copilotPluginValidator;
5642
+ var init_plugin4 = __esm(() => {
5643
+ NAME_REGEX5 = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
5644
+ copilotPluginValidator = {
5645
+ id: "copilot:plugin",
5646
+ provider: "copilot",
5647
+ name: "Copilot Plugin",
5648
+ description: "Validates .github/plugin/plugin.json (skills as array of paths, mcpServers support)",
4039
5649
  detect(dir) {
4040
- const commandsDir = resolve11(dir, "commands");
4041
- if (!existsSync19(commandsDir))
4042
- return false;
4043
- try {
4044
- return readdirSync9(commandsDir).some((f) => f.endsWith(".md"));
4045
- } catch {
4046
- return false;
4047
- }
5650
+ return existsSync35(resolve23(dir, ".github", "plugin", "plugin.json"));
4048
5651
  },
4049
5652
  async validate(dir, _opts) {
4050
5653
  const errors = [];
4051
5654
  const warnings = [];
4052
5655
  const passes = [];
4053
- const commandsDir = resolve11(dir, "commands");
4054
- const mdFiles = readdirSync9(commandsDir).filter((f) => f.endsWith(".md"));
4055
- if (mdFiles.length === 0) {
4056
- errors.push("commands/ directory has no .md files");
5656
+ const manifestPath = resolve23(dir, ".github", "plugin", "plugin.json");
5657
+ let manifest;
5658
+ try {
5659
+ const raw = await Bun.file(manifestPath).text();
5660
+ manifest = JSON.parse(raw);
5661
+ passes.push(".github/plugin/plugin.json is valid JSON");
5662
+ } catch {
5663
+ errors.push(".github/plugin/plugin.json is missing or invalid JSON");
4057
5664
  return { errors, warnings, passes };
4058
5665
  }
4059
- passes.push(`${mdFiles.length} command definition(s) found`);
4060
- for (const file of mdFiles) {
4061
- const filePath = join15(commandsDir, file);
4062
- const raw = await Bun.file(filePath).text();
4063
- try {
4064
- const parsed = parseFrontmatter(raw);
4065
- if (Object.keys(parsed.data).length === 0) {
4066
- warnings.push(`${file}: no YAML frontmatter`);
4067
- } else if (!parsed.data.description) {
4068
- warnings.push(`${file}: missing "description" in frontmatter`);
4069
- } else {
4070
- passes.push(`${file}: has frontmatter with description`);
5666
+ if (!manifest.name) {
5667
+ errors.push('Missing required field: "name"');
5668
+ } else {
5669
+ const name = String(manifest.name);
5670
+ if (!NAME_REGEX5.test(name)) {
5671
+ errors.push(`Invalid name format: "${name}" \u2014 must be kebab-case (a-z, 0-9, hyphens)`);
5672
+ } else {
5673
+ passes.push(`name: "${name}"`);
5674
+ }
5675
+ }
5676
+ if (manifest.skills === undefined) {
5677
+ errors.push('Missing required field: "skills" (must be an array of paths like ["./skills/foo"])');
5678
+ } else if (!Array.isArray(manifest.skills)) {
5679
+ errors.push('"skills" must be an array of relative paths');
5680
+ } else {
5681
+ const skillsArr = manifest.skills;
5682
+ passes.push(`skills: array with ${skillsArr.length} path(s)`);
5683
+ for (const [i, p] of skillsArr.entries()) {
5684
+ if (typeof p !== "string") {
5685
+ errors.push(`skills[${i}]: must be a string path`);
5686
+ continue;
4071
5687
  }
4072
- if (!parsed.content.trim()) {
4073
- errors.push(`${file}: body is empty`);
5688
+ if (!p.startsWith("./") && !p.startsWith("../")) {
5689
+ warnings.push(`skills[${i}]: "${p}" should be relative (./ or ../)`);
4074
5690
  }
4075
- const advancedKeys = ["allowed-tools", "disallowed-tools", "context", "when_to_use", "disable-model-invocation", "user-invocable", "arguments", "argument-hint", "shell", "paths", "hooks"];
4076
- const foundAdvanced = advancedKeys.filter((k) => parsed.data[k] !== undefined);
4077
- if (foundAdvanced.length > 0) {
4078
- passes.push(`${file}: advanced frontmatter: ${foundAdvanced.join(", ")}`);
5691
+ const skillDir = resolve23(dir, p);
5692
+ const skillMd = resolve23(skillDir, "SKILL.md");
5693
+ if (existsSync35(skillMd)) {
5694
+ passes.push(`skills[${i}]: ${p}/SKILL.md exists`);
5695
+ } else if (existsSync35(skillDir)) {
5696
+ warnings.push(`skills[${i}]: directory exists but no SKILL.md inside`);
5697
+ } else {
5698
+ warnings.push(`skills[${i}]: path "${p}" does not exist`);
4079
5699
  }
4080
- } catch {
4081
- errors.push(`${file}: failed to parse`);
4082
5700
  }
4083
5701
  }
4084
- return { errors, warnings, passes };
4085
- }
4086
- };
4087
- });
4088
-
4089
- // src/validators/claude/memory.ts
4090
- import { existsSync as existsSync20 } from "fs";
4091
- import { resolve as resolve12 } from "path";
4092
- var claudeMemoryValidator;
4093
- var init_memory = __esm(() => {
4094
- claudeMemoryValidator = {
4095
- id: "claude:memory",
4096
- provider: "claude",
4097
- name: "Claude CLAUDE.md",
4098
- description: "Validates CLAUDE.md: non-empty, length recommendations, @path imports",
4099
- detect(dir) {
4100
- return existsSync20(resolve12(dir, "CLAUDE.md"));
4101
- },
4102
- async validate(dir, _opts) {
4103
- const errors = [];
4104
- const warnings = [];
4105
- const passes = [];
4106
- const filePath = resolve12(dir, "CLAUDE.md");
4107
- const raw = await Bun.file(filePath).text();
4108
- if (!raw.trim()) {
4109
- errors.push("CLAUDE.md is empty");
4110
- return { errors, warnings, passes };
4111
- }
4112
- passes.push("CLAUDE.md is non-empty");
4113
- const lines = raw.split(`
4114
- `);
4115
- if (lines.length > 200) {
4116
- warnings.push(`CLAUDE.md is ${lines.length} lines \u2014 official recommendation is under 200. Move reference content to skills.`);
4117
- } else {
4118
- passes.push(`CLAUDE.md is ${lines.length} lines (under 200 recommended limit)`);
5702
+ if (manifest.mcpServers !== undefined) {
5703
+ if (typeof manifest.mcpServers === "string") {
5704
+ passes.push(`mcpServers: "${manifest.mcpServers}"`);
5705
+ const mcpRef = String(manifest.mcpServers);
5706
+ const mcpPath = resolve23(dir, mcpRef);
5707
+ if (existsSync35(mcpPath)) {
5708
+ passes.push(`mcpServers file exists at ${mcpRef}`);
5709
+ } else {
5710
+ warnings.push(`mcpServers path "${mcpRef}" does not exist on disk`);
5711
+ }
5712
+ } else {
5713
+ warnings.push('"mcpServers" should be a string path when present');
5714
+ }
4119
5715
  }
4120
- const importRegex = /^@([^\s]+)\s*$/gm;
4121
- let match;
4122
- while ((match = importRegex.exec(raw)) !== null) {
4123
- const importPath = match[1];
4124
- const resolvedImport = resolve12(dir, importPath);
4125
- if (existsSync20(resolvedImport)) {
4126
- passes.push(`@import "${importPath}" exists`);
5716
+ if (manifest.description) {
5717
+ const desc = String(manifest.description);
5718
+ if (desc.length < 10) {
5719
+ warnings.push(`Description is very short (${desc.length} chars)`);
4127
5720
  } else {
4128
- warnings.push(`@import "${importPath}" \u2014 file not found at ${resolvedImport}`);
5721
+ passes.push("description field present");
4129
5722
  }
5723
+ } else {
5724
+ warnings.push('Missing "description" (recommended)');
5725
+ }
5726
+ if (manifest.version)
5727
+ passes.push(`version: "${manifest.version}"`);
5728
+ if (manifest.author)
5729
+ passes.push("author present");
5730
+ if (manifest.license)
5731
+ passes.push(`license: "${manifest.license}"`);
5732
+ if (manifest.homepage)
5733
+ passes.push("homepage present");
5734
+ if (manifest.repository)
5735
+ passes.push("repository present");
5736
+ if (Array.isArray(manifest.keywords)) {
5737
+ passes.push(`keywords: [${manifest.keywords.join(", ")}]`);
5738
+ }
5739
+ const known = new Set([
5740
+ "name",
5741
+ "version",
5742
+ "description",
5743
+ "author",
5744
+ "homepage",
5745
+ "repository",
5746
+ "license",
5747
+ "keywords",
5748
+ "skills",
5749
+ "mcpServers"
5750
+ ]);
5751
+ const unknown = Object.keys(manifest).filter((k) => !known.has(k));
5752
+ for (const k of unknown) {
5753
+ warnings.push(`Unrecognized field "${k}" \u2014 will be ignored (for compatibility)`);
4130
5754
  }
4131
5755
  return { errors, warnings, passes };
4132
5756
  }
4133
5757
  };
4134
5758
  });
4135
5759
 
4136
- // src/validators/claude/lsp.ts
4137
- import { existsSync as existsSync21 } from "fs";
4138
- import { resolve as resolve13 } from "path";
4139
- var claudeLspValidator;
4140
- var init_lsp = __esm(() => {
4141
- claudeLspValidator = {
4142
- id: "claude:lsp",
4143
- provider: "claude",
4144
- name: "Claude LSP Servers",
4145
- description: "Validates .lsp.json (or plugin.json lspServers): language server configs with required command + extensionToLanguage; optional transport, env, settings, diagnostics etc. (binaries installed separately)",
5760
+ // src/validators/copilot/marketplace.ts
5761
+ import { existsSync as existsSync36 } from "fs";
5762
+ import { resolve as resolve24 } from "path";
5763
+ var copilotMarketplaceValidator;
5764
+ var init_marketplace4 = __esm(() => {
5765
+ copilotMarketplaceValidator = {
5766
+ id: "copilot:marketplace",
5767
+ provider: "copilot",
5768
+ name: "Copilot Plugin Marketplace",
5769
+ description: "Validates .github/plugin/marketplace.json (string sources)",
4146
5770
  detect(dir) {
4147
- return existsSync21(resolve13(dir, ".lsp.json")) || existsSync21(resolve13(dir, ".claude-plugin", "plugin.json"));
5771
+ return existsSync36(resolve24(dir, ".github", "plugin", "marketplace.json"));
4148
5772
  },
4149
5773
  async validate(dir, _opts) {
4150
5774
  const errors = [];
4151
5775
  const warnings = [];
4152
5776
  const passes = [];
4153
- let cfg = null;
4154
- const lspPath = resolve13(dir, ".lsp.json");
4155
- if (existsSync21(lspPath)) {
4156
- try {
4157
- cfg = JSON.parse(await Bun.file(lspPath).text());
4158
- passes.push(".lsp.json is valid JSON");
4159
- } catch {
4160
- errors.push(".lsp.json is invalid JSON");
4161
- return { errors, warnings, passes };
4162
- }
5777
+ const mktPath = resolve24(dir, ".github", "plugin", "marketplace.json");
5778
+ let mkt;
5779
+ try {
5780
+ const raw = await Bun.file(mktPath).text();
5781
+ mkt = JSON.parse(raw);
5782
+ passes.push(".github/plugin/marketplace.json is valid JSON");
5783
+ } catch {
5784
+ errors.push(".github/plugin/marketplace.json is missing or invalid JSON");
5785
+ return { errors, warnings, passes };
5786
+ }
5787
+ if (mkt.name) {
5788
+ passes.push(`name: "${mkt.name}"`);
4163
5789
  } else {
4164
- const manifestPath = resolve13(dir, ".claude-plugin", "plugin.json");
4165
- if (existsSync21(manifestPath)) {
4166
- try {
4167
- const m = JSON.parse(await Bun.file(manifestPath).text());
4168
- if (m && m.lspServers && typeof m.lspServers === "object") {
4169
- cfg = m.lspServers;
4170
- passes.push("lspServers present inline in plugin.json");
4171
- }
4172
- } catch {}
4173
- }
5790
+ warnings.push('Missing "name" at marketplace root');
4174
5791
  }
4175
- if (!cfg) {
4176
- if (!existsSync21(lspPath)) {
4177
- return { errors, warnings, passes };
4178
- }
5792
+ if (mkt.metadata && typeof mkt.metadata === "object") {
5793
+ passes.push("metadata present");
5794
+ if (mkt.metadata.description)
5795
+ passes.push("metadata.description present");
5796
+ if (mkt.metadata.version)
5797
+ passes.push(`metadata.version: "${mkt.metadata.version}"`);
4179
5798
  }
4180
- if (cfg && typeof cfg === "object") {
4181
- const langs = Object.keys(cfg);
4182
- passes.push(`${langs.length} language server(s) configured`);
4183
- for (const lang of langs) {
4184
- const entry = cfg[lang];
4185
- if (!entry || !entry.command) {
4186
- errors.push(`lsp "${lang}": "command" (the LSP binary) is required`);
4187
- }
4188
- if (!entry.extensionToLanguage || typeof entry.extensionToLanguage !== "object") {
4189
- errors.push(`lsp "${lang}": "extensionToLanguage" map is required (e.g. { ".ts": "typescript" })`);
5799
+ if (mkt.owner) {
5800
+ passes.push("owner present");
5801
+ }
5802
+ if (!Array.isArray(mkt.plugins) || mkt.plugins.length === 0) {
5803
+ errors.push('"plugins" must be a non-empty array');
5804
+ return { errors, warnings, passes };
5805
+ }
5806
+ passes.push(`${mkt.plugins.length} plugin(s) declared`);
5807
+ for (const [i, p] of mkt.plugins.entries()) {
5808
+ if (!p || typeof p !== "object") {
5809
+ errors.push(`plugins[${i}]: must be an object`);
5810
+ continue;
5811
+ }
5812
+ if (p.name) {
5813
+ passes.push(`plugins[${i}].name: "${p.name}"`);
5814
+ } else {
5815
+ errors.push(`plugins[${i}]: missing "name"`);
5816
+ }
5817
+ if (p.source) {
5818
+ const src = String(p.source);
5819
+ passes.push(`plugins[${i}].source: "${src}"`);
5820
+ const srcDir = resolve24(dir, src);
5821
+ if (existsSync36(srcDir)) {
5822
+ const hasManifest = existsSync36(resolve24(srcDir, ".github", "plugin", "plugin.json"));
5823
+ const hasSkills = existsSync36(resolve24(srcDir, "skills"));
5824
+ if (hasManifest || hasSkills) {
5825
+ passes.push(`plugins[${i}]: source exists (${hasManifest ? "manifest" : "skills/"})`);
5826
+ } else {
5827
+ warnings.push(`plugins[${i}].source "${src}" exists but lacks plugin markers`);
5828
+ }
4190
5829
  } else {
4191
- passes.push(`lsp "${lang}": has extensionToLanguage mapping`);
4192
- }
4193
- if (entry.diagnostics === false) {
4194
- passes.push(`lsp "${lang}": diagnostics disabled (navigation only)`);
5830
+ warnings.push(`plugins[${i}].source path "${src}" does not exist`);
4195
5831
  }
5832
+ } else {
5833
+ warnings.push(`plugins[${i}]: missing "source"`);
4196
5834
  }
5835
+ if (p.description)
5836
+ passes.push(`plugins[${i}].description present`);
5837
+ if (p.version)
5838
+ passes.push(`plugins[${i}].version: "${p.version}"`);
5839
+ }
5840
+ if (existsSync36(resolve24(dir, "README.md"))) {
5841
+ passes.push("README.md exists at marketplace root");
5842
+ } else {
5843
+ warnings.push("No README.md at marketplace root \u2014 recommended");
5844
+ }
5845
+ if (existsSync36(resolve24(dir, "LICENSE"))) {
5846
+ passes.push("LICENSE exists at marketplace root");
5847
+ } else {
5848
+ warnings.push("No LICENSE at marketplace root \u2014 recommended");
4197
5849
  }
4198
- warnings.push('Reminder: the actual language server binary (gopls, pyright, etc.) must be installed separately on PATH. See /plugin errors tab if "Executable not found".');
4199
5850
  return { errors, warnings, passes };
4200
5851
  }
4201
5852
  };
4202
5853
  });
4203
5854
 
4204
- // src/validators/claude/monitors.ts
4205
- import { existsSync as existsSync22 } from "fs";
4206
- import { resolve as resolve14 } from "path";
4207
- var claudeMonitorsValidator;
4208
- var init_monitors = __esm(() => {
4209
- claudeMonitorsValidator = {
4210
- id: "claude:monitors",
4211
- provider: "claude",
4212
- name: "Claude Monitors (experimental)",
4213
- description: "Validates monitors/monitors.json (or experimental.monitors): array of {name, command, description, when?}; commands support ${CLAUDE_PLUGIN_*} subs. Monitors run only in interactive CLI sessions.",
5855
+ // src/validators/copilot/mcp.ts
5856
+ import { existsSync as existsSync37 } from "fs";
5857
+ import { resolve as resolve25 } from "path";
5858
+ var copilotMcpValidator;
5859
+ var init_mcp4 = __esm(() => {
5860
+ copilotMcpValidator = {
5861
+ id: "copilot:mcp",
5862
+ provider: "copilot",
5863
+ name: "Copilot MCP Config",
5864
+ description: "Validates .mcp.json (referenced via mcpServers in manifest). Supports stdio and http servers.",
4214
5865
  detect(dir) {
4215
- return existsSync22(resolve14(dir, "monitors", "monitors.json")) || existsSync22(resolve14(dir, "monitors.json")) || existsSync22(resolve14(dir, ".claude-plugin", "plugin.json"));
5866
+ return existsSync37(resolve25(dir, ".mcp.json"));
4216
5867
  },
4217
5868
  async validate(dir, _opts) {
4218
5869
  const errors = [];
4219
5870
  const warnings = [];
4220
5871
  const passes = [];
4221
- let arr = null;
4222
- const candidates = [
4223
- resolve14(dir, "monitors", "monitors.json"),
4224
- resolve14(dir, "monitors.json")
4225
- ];
4226
- for (const p of candidates) {
4227
- if (existsSync22(p)) {
4228
- try {
4229
- const parsed = JSON.parse(await Bun.file(p).text());
4230
- if (Array.isArray(parsed)) {
4231
- arr = parsed;
4232
- passes.push("monitors config is valid JSON array");
4233
- }
4234
- break;
4235
- } catch {
4236
- errors.push("monitors config is invalid JSON");
4237
- return { errors, warnings, passes };
4238
- }
4239
- }
4240
- }
4241
- if (!arr) {
4242
- const mp = resolve14(dir, ".claude-plugin", "plugin.json");
4243
- if (existsSync22(mp)) {
4244
- try {
4245
- const m = JSON.parse(await Bun.file(mp).text());
4246
- const exp = m?.experimental;
4247
- const inline = typeof exp === "string" ? null : exp?.monitors;
4248
- if (Array.isArray(inline))
4249
- arr = inline;
4250
- else if (typeof inline === "string") {
4251
- passes.push("experimental.monitors declared as path in manifest (content not validated here)");
4252
- }
4253
- } catch {}
4254
- }
5872
+ const mcpPath = resolve25(dir, ".mcp.json");
5873
+ let rawConfig;
5874
+ try {
5875
+ const raw = await Bun.file(mcpPath).text();
5876
+ rawConfig = JSON.parse(raw);
5877
+ passes.push(".mcp.json is valid JSON");
5878
+ } catch {
5879
+ errors.push(".mcp.json is missing or invalid JSON");
5880
+ return { errors, warnings, passes };
4255
5881
  }
4256
- if (!arr) {
5882
+ let config;
5883
+ if (rawConfig && typeof rawConfig === "object" && !Array.isArray(rawConfig) && rawConfig.mcpServers && typeof rawConfig.mcpServers === "object") {
5884
+ config = rawConfig.mcpServers;
5885
+ passes.push("mcp.json uses mcpServers wrapper (normalized)");
5886
+ } else if (typeof rawConfig === "object" && !Array.isArray(rawConfig)) {
5887
+ config = rawConfig;
5888
+ } else {
5889
+ errors.push(".mcp.json must be an object (or contain mcpServers object)");
4257
5890
  return { errors, warnings, passes };
4258
5891
  }
4259
- if (!Array.isArray(arr)) {
4260
- errors.push("monitors config must be a JSON array");
5892
+ const serverNames = Object.keys(config);
5893
+ if (serverNames.length === 0) {
5894
+ warnings.push(".mcp.json is empty \u2014 no servers defined");
4261
5895
  return { errors, warnings, passes };
4262
5896
  }
4263
- const seen = new Set;
4264
- arr.forEach((mon, i) => {
4265
- if (!mon || typeof mon !== "object") {
4266
- errors.push(`monitors[${i}]: entry must be an object`);
4267
- return;
5897
+ passes.push(`${serverNames.length} server(s) defined`);
5898
+ for (const [name, entry] of Object.entries(config)) {
5899
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
5900
+ errors.push(`mcp server "${name}": definition must be an object`);
5901
+ continue;
4268
5902
  }
4269
- if (!mon.name || typeof mon.name !== "string") {
4270
- errors.push(`monitors[${i}]: "name" (unique id) is required`);
4271
- } else {
4272
- if (seen.has(mon.name))
4273
- errors.push(`monitors: duplicate name "${mon.name}"`);
4274
- seen.add(mon.name);
5903
+ const e = entry;
5904
+ const hasCommand = typeof e.command === "string";
5905
+ const hasUrl = typeof e.url === "string";
5906
+ if (!hasCommand && !hasUrl) {
5907
+ errors.push(`mcp server "${name}": must have either "command" (for stdio) or "url" (for SSE/HTTP)`);
4275
5908
  }
4276
- if (!mon.command || typeof mon.command !== "string") {
4277
- errors.push(`monitors[${i}]: "command" (shell command) is required`);
4278
- } else if (/\$\{CLAUDE_/.test(mon.command)) {
4279
- passes.push(`monitors[${i}] "${mon.name || i}": uses CLAUDE_PLUGIN_* substitution`);
5909
+ if (hasCommand && !Array.isArray(e.args)) {
5910
+ warnings.push(`mcp server "${name}": "command" present but no "args" array (ok for some servers)`);
4280
5911
  }
4281
- if (!mon.description) {
4282
- warnings.push(`monitors[${i}]: "description" recommended (shown in task panel)`);
5912
+ if (hasUrl && hasCommand) {
5913
+ warnings.push(`mcp server "${name}": both "command" and "url" present \u2014 usually one or the other`);
4283
5914
  }
4284
- if (mon.when && !/^always$|^on-skill-invoke:/.test(String(mon.when))) {
4285
- warnings.push(`monitors[${i}]: "when" should be "always" (default) or "on-skill-invoke:<skill>"`);
5915
+ if (e.env && typeof e.env === "object") {
5916
+ passes.push(`mcp server "${name}": has env`);
4286
5917
  }
4287
- });
4288
- passes.push(`${arr.length} monitor(s) declared`);
4289
- warnings.push("Note: monitors are experimental, run only for interactive CLI sessions, and are skipped on some hosts. They do not stop automatically if the plugin is disabled mid-session.");
5918
+ if (typeof e.cwd === "string") {
5919
+ passes.push(`mcp server "${name}": has cwd`);
5920
+ }
5921
+ const hasSubs = JSON.stringify(e).match(/\$\{CODEX_|CLAUDE_PLUGIN_|COPILOT_|PLUGIN_ROOT|user_config\.|ENV_VAR\}/);
5922
+ if (hasSubs) {
5923
+ passes.push(`mcp server "${name}": uses substitutions (e.g. \${PLUGIN_ROOT} or env)`);
5924
+ }
5925
+ }
4290
5926
  return { errors, warnings, passes };
4291
5927
  }
4292
5928
  };
4293
5929
  });
4294
5930
 
5931
+ // src/validators/copilot/skill.ts
5932
+ import { existsSync as existsSync38 } from "fs";
5933
+ import { resolve as resolve26 } from "path";
5934
+ var copilotSkillValidator;
5935
+ var init_skill4 = __esm(() => {
5936
+ init_skill_validate();
5937
+ copilotSkillValidator = {
5938
+ id: "copilot:skill",
5939
+ provider: "copilot",
5940
+ name: "Copilot Skill",
5941
+ description: "Validates SKILL.md (shared format). Copilot supports skills referenced via array paths in the manifest.",
5942
+ detect(dir) {
5943
+ return existsSync38(resolve26(dir, "SKILL.md"));
5944
+ },
5945
+ async validate(dir, _opts) {
5946
+ const loaded = await loadSkill(dir);
5947
+ if (!loaded.ok) {
5948
+ return {
5949
+ errors: [loaded.error],
5950
+ warnings: [],
5951
+ passes: []
5952
+ };
5953
+ }
5954
+ const { model, existingDirs } = loaded;
5955
+ return validateSkillModel(model, { existingDirs: [...existingDirs] });
5956
+ }
5957
+ };
5958
+ });
5959
+
5960
+ // src/providers/index.ts
5961
+ var claudeAdapter, codexAdapter, cursorAdapter, copilotAdapter, adapters;
5962
+ var init_providers = __esm(() => {
5963
+ init_skill();
5964
+ init_plugin();
5965
+ init_marketplace();
5966
+ init_hooks();
5967
+ init_mcp();
5968
+ init_subagent();
5969
+ init_command();
5970
+ init_memory();
5971
+ init_lsp();
5972
+ init_monitors();
5973
+ init_plugin2();
5974
+ init_marketplace2();
5975
+ init_mcp2();
5976
+ init_skill2();
5977
+ init_plugin3();
5978
+ init_marketplace3();
5979
+ init_mcp3();
5980
+ init_skill3();
5981
+ init_plugin4();
5982
+ init_marketplace4();
5983
+ init_mcp4();
5984
+ init_skill4();
5985
+ init_spec();
5986
+ claudeAdapter = {
5987
+ id: "claude",
5988
+ name: "Claude Code",
5989
+ manifestPath: ".claude-plugin/plugin.json",
5990
+ marketplacePath: ".claude-plugin/marketplace.json",
5991
+ mcpFilename: ".mcp.json",
5992
+ validators: [
5993
+ claudeSkillValidator,
5994
+ claudePluginValidator,
5995
+ claudeMarketplaceValidator,
5996
+ claudeHooksValidator,
5997
+ claudeMcpValidator,
5998
+ claudeSubagentValidator,
5999
+ claudeCommandValidator,
6000
+ claudeMemoryValidator,
6001
+ claudeLspValidator,
6002
+ claudeMonitorsValidator
6003
+ ],
6004
+ detectContext(dir) {
6005
+ return { cwd: dir };
6006
+ },
6007
+ async scaffold(decision, ctx) {
6008
+ throw new Error("Scaffold via adapter not yet implemented");
6009
+ }
6010
+ };
6011
+ codexAdapter = {
6012
+ id: "codex",
6013
+ name: "Codex",
6014
+ manifestPath: ".codex-plugin/plugin.json",
6015
+ marketplacePath: ".agents/plugins/marketplace.json",
6016
+ mcpFilename: ".mcp.json",
6017
+ validators: [
6018
+ codexPluginValidator,
6019
+ codexMarketplaceValidator,
6020
+ codexMcpValidator,
6021
+ codexSkillValidator
6022
+ ],
6023
+ detectContext(dir) {
6024
+ return { cwd: dir };
6025
+ },
6026
+ async scaffold(decision, ctx) {
6027
+ throw new Error("Scaffold via adapter not yet implemented");
6028
+ }
6029
+ };
6030
+ cursorAdapter = {
6031
+ id: "cursor",
6032
+ name: "Cursor",
6033
+ manifestPath: ".cursor-plugin/plugin.json",
6034
+ marketplacePath: ".cursor-plugin/marketplace.json",
6035
+ mcpFilename: "mcp.json",
6036
+ validators: [
6037
+ cursorPluginValidator,
6038
+ cursorMarketplaceValidator,
6039
+ cursorMcpValidator,
6040
+ cursorSkillValidator
6041
+ ],
6042
+ detectContext(dir) {
6043
+ return { cwd: dir };
6044
+ },
6045
+ async scaffold(decision, ctx) {
6046
+ throw new Error("Scaffold via adapter not yet implemented");
6047
+ }
6048
+ };
6049
+ copilotAdapter = {
6050
+ id: "copilot",
6051
+ name: "Copilot CLI",
6052
+ manifestPath: ".github/plugin/plugin.json",
6053
+ marketplacePath: ".github/plugin/marketplace.json",
6054
+ mcpFilename: ".mcp.json",
6055
+ validators: [
6056
+ copilotPluginValidator,
6057
+ copilotMarketplaceValidator,
6058
+ copilotMcpValidator,
6059
+ copilotSkillValidator
6060
+ ],
6061
+ detectContext(dir) {
6062
+ return { cwd: dir };
6063
+ },
6064
+ async scaffold(decision, ctx) {
6065
+ throw new Error("Scaffold via adapter not yet implemented");
6066
+ }
6067
+ };
6068
+ adapters = [claudeAdapter, codexAdapter, cursorAdapter, copilotAdapter];
6069
+ });
6070
+
4295
6071
  // src/validators/index.ts
4296
6072
  function resolveFor(forFlag, allValidators = validators) {
4297
6073
  if (!forFlag) {
@@ -4309,43 +6085,30 @@ Available: ${available}` };
4309
6085
  }
4310
6086
  const byProvider = allValidators.filter((v) => v.provider === forFlag);
4311
6087
  if (byProvider.length === 0) {
4312
- const providers = [...new Set(allValidators.map((v) => v.provider))];
4313
- return { matched: [], error: `Unknown provider: "${forFlag}"
6088
+ const knownProviders = [...new Set([
6089
+ ...allValidators.map((v) => v.provider),
6090
+ ...supportedProviders
6091
+ ])];
6092
+ if (!knownProviders.includes(forFlag)) {
6093
+ return { matched: [], error: `Unknown provider: "${forFlag}"
4314
6094
 
4315
- Available providers: ${providers.join(", ")}` };
6095
+ Available providers: ${knownProviders.join(", ")}` };
6096
+ }
6097
+ return { matched: [] };
4316
6098
  }
4317
6099
  return { matched: byProvider };
4318
6100
  }
4319
6101
  var validators;
4320
6102
  var init_validators = __esm(() => {
4321
- init_skill();
4322
- init_plugin();
4323
- init_marketplace();
4324
- init_hooks();
4325
- init_mcp();
4326
- init_subagent();
4327
- init_command();
4328
- init_memory();
4329
- init_lsp();
4330
- init_monitors();
4331
- validators = [
4332
- claudeSkillValidator,
4333
- claudePluginValidator,
4334
- claudeMarketplaceValidator,
4335
- claudeHooksValidator,
4336
- claudeMcpValidator,
4337
- claudeSubagentValidator,
4338
- claudeCommandValidator,
4339
- claudeMemoryValidator,
4340
- claudeLspValidator,
4341
- claudeMonitorsValidator
4342
- ];
6103
+ init_providers();
6104
+ init_spec();
6105
+ validators = adapters.flatMap((a) => a.validators);
4343
6106
  });
4344
6107
 
4345
6108
  // src/core/remote.ts
4346
6109
  import { spawnSync as spawnSync4 } from "child_process";
4347
6110
  import { mkdtempSync, rmSync } from "fs";
4348
- import { join as join16 } from "path";
6111
+ import { join as join25 } from "path";
4349
6112
  import { tmpdir } from "os";
4350
6113
  function parseRemoteUrl(input) {
4351
6114
  if (input.startsWith(".") || input.startsWith("/") || input.startsWith("~")) {
@@ -4382,7 +6145,7 @@ function isGhAvailable() {
4382
6145
  return ghAvailable;
4383
6146
  }
4384
6147
  async function cloneToTemp(parsed) {
4385
- const tmpDir = mkdtempSync(join16(tmpdir(), "dora-"));
6148
+ const tmpDir = mkdtempSync(join25(tmpdir(), "dora-"));
4386
6149
  const cleanup = () => {
4387
6150
  try {
4388
6151
  rmSync(tmpDir, { recursive: true, force: true });
@@ -4430,15 +6193,15 @@ var exports_validate_top = {};
4430
6193
  __export(exports_validate_top, {
4431
6194
  default: () => validate_top_default
4432
6195
  });
4433
- import { existsSync as existsSync24 } from "fs";
4434
- import { resolve as resolve15 } from "path";
4435
- var import_picocolors13, validate_top_default;
6196
+ import { existsSync as existsSync40 } from "fs";
6197
+ import { resolve as resolve27 } from "path";
6198
+ var import_picocolors15, validate_top_default;
4436
6199
  var init_validate_top = __esm(() => {
4437
6200
  init_dist();
4438
6201
  init_out();
4439
6202
  init_validators();
4440
6203
  init_remote();
4441
- import_picocolors13 = __toESM(require_picocolors(), 1);
6204
+ import_picocolors15 = __toESM(require_picocolors(), 1);
4442
6205
  validate_top_default = defineCommand({
4443
6206
  meta: {
4444
6207
  name: "validate",
@@ -4478,24 +6241,24 @@ var init_validate_top = __esm(() => {
4478
6241
  let cleanup;
4479
6242
  if (remote) {
4480
6243
  ui.info(`
4481
- Cloning ${import_picocolors13.default.dim(args.path)}...`);
6244
+ Cloning ${import_picocolors15.default.dim(args.path)}...`);
4482
6245
  try {
4483
6246
  const result = await cloneToTemp(remote);
4484
- fullPath = remote.subpath ? resolve15(result.dir, remote.subpath) : result.dir;
6247
+ fullPath = remote.subpath ? resolve27(result.dir, remote.subpath) : result.dir;
4485
6248
  cleanup = result.cleanup;
4486
6249
  } catch (err) {
4487
6250
  const msg = err instanceof Error ? err.message : String(err);
4488
6251
  ui.fail(msg);
4489
6252
  process.exit(1);
4490
6253
  }
4491
- if (!existsSync24(fullPath)) {
6254
+ if (!existsSync40(fullPath)) {
4492
6255
  cleanup();
4493
6256
  ui.fail(`Subdirectory not found in repo: ${remote.subpath}`);
4494
6257
  process.exit(1);
4495
6258
  }
4496
6259
  } else {
4497
- fullPath = resolve15(args.path);
4498
- if (!existsSync24(fullPath)) {
6260
+ fullPath = resolve27(args.path);
6261
+ if (!existsSync40(fullPath)) {
4499
6262
  ui.fail(`Path not found: ${args.path}
4500
6263
 
4501
6264
  Check that the path is correct and the directory exists.`);
@@ -4526,13 +6289,13 @@ Check that the path is correct and the directory exists.`);
4526
6289
  ` + `Available providers:
4527
6290
  ` + providers.map((p) => {
4528
6291
  const pvs = validators.filter((v) => v.provider === p);
4529
- return ` ${import_picocolors13.default.bold(p)}
4530
- ` + pvs.map((v) => ` \u2022 ${import_picocolors13.default.dim(v.id)} \u2014 ${v.description}`).join(`
6292
+ return ` ${import_picocolors15.default.bold(p)}
6293
+ ` + pvs.map((v) => ` \u2022 ${import_picocolors15.default.dim(v.id)} \u2014 ${v.description}`).join(`
4531
6294
  `);
4532
6295
  }).join(`
4533
6296
  `) + `
4534
6297
 
4535
- Use ${import_picocolors13.default.dim("--for <provider>")} or ${import_picocolors13.default.dim("--for <provider:type>")} to target explicitly.`);
6298
+ Use ${import_picocolors15.default.dim("--for <provider>")} or ${import_picocolors15.default.dim("--for <provider:type>")} to target explicitly.`);
4536
6299
  process.exit(1);
4537
6300
  }
4538
6301
  const allResults = [];
@@ -4553,7 +6316,7 @@ Use ${import_picocolors13.default.dim("--for <provider>")} or ${import_picocolor
4553
6316
  } else {
4554
6317
  for (const { id, name, result } of allResults) {
4555
6318
  ui.write(`
4556
- ${import_picocolors13.default.bold("dora validate")} \u2014 ${import_picocolors13.default.white(name)} ${import_picocolors13.default.dim(`(${id})`)}
6319
+ ${import_picocolors15.default.bold("dora validate")} \u2014 ${import_picocolors15.default.white(name)} ${import_picocolors15.default.dim(`(${id})`)}
4557
6320
  `);
4558
6321
  ui.info(` Path: ${args.path}
4559
6322
  `);
@@ -4568,7 +6331,7 @@ Use ${import_picocolors13.default.dim("--for <provider>")} or ${import_picocolor
4568
6331
  }
4569
6332
  if (result.errors.length === 0 && result.warnings.length === 0) {
4570
6333
  ui.write(`
4571
- ${import_picocolors13.default.green("\u2713")} ${import_picocolors13.default.white("All checks passed.")}
6334
+ ${import_picocolors15.default.green("\u2713")} ${import_picocolors15.default.white("All checks passed.")}
4572
6335
  `);
4573
6336
  } else {
4574
6337
  ui.info(`
@@ -4590,16 +6353,16 @@ var exports_init2 = {};
4590
6353
  __export(exports_init2, {
4591
6354
  default: () => init_default2
4592
6355
  });
4593
- import { basename as basename4, join as join17 } from "path";
6356
+ import { basename as basename6, join as join26 } from "path";
4594
6357
  var {spawnSync: spawnSync5 } = globalThis.Bun;
4595
- var import_picocolors14, init_default2;
6358
+ var import_picocolors16, init_default2;
4596
6359
  var init_init2 = __esm(() => {
4597
6360
  init_dist();
4598
6361
  init_out();
4599
6362
  init_journal_config();
4600
6363
  init_journal_remote();
4601
6364
  init_prompt();
4602
- import_picocolors14 = __toESM(require_picocolors(), 1);
6365
+ import_picocolors16 = __toESM(require_picocolors(), 1);
4603
6366
  init_default2 = defineCommand({
4604
6367
  meta: {
4605
6368
  name: "init",
@@ -4626,17 +6389,17 @@ var init_init2 = __esm(() => {
4626
6389
  ui.heading("dora init \u2014 Set up doraval, your journal, and the coding agent dora should use on the fly");
4627
6390
  const ghCheck = ensureGhCli();
4628
6391
  if (!ghCheck.ok) {
4629
- ui.write(` ${import_picocolors14.default.red("\u2717")} ${import_picocolors14.default.white("The GitHub CLI (")}${import_picocolors14.default.bold("gh")}${import_picocolors14.default.white(") is not installed.")}
6392
+ ui.write(` ${import_picocolors16.default.red("\u2717")} ${import_picocolors16.default.white("The GitHub CLI (")}${import_picocolors16.default.bold("gh")}${import_picocolors16.default.white(") is not installed.")}
4630
6393
  `);
4631
- ui.info(` doraval uses ${import_picocolors14.default.bold("gh")} to fetch and sync journal files with GitHub.
6394
+ ui.info(` doraval uses ${import_picocolors16.default.bold("gh")} to fetch and sync journal files with GitHub.
4632
6395
  `);
4633
6396
  ui.info(` Install it:
4634
6397
  `);
4635
- ui.info(` macOS: ${import_picocolors14.default.dim("brew install gh")}`);
4636
- ui.info(` Linux: ${import_picocolors14.default.dim("https://github.com/cli/cli/blob/trunk/docs/install_linux.md")}`);
4637
- ui.info(` Windows: ${import_picocolors14.default.dim("winget install --id GitHub.cli")}
6398
+ ui.info(` macOS: ${import_picocolors16.default.dim("brew install gh")}`);
6399
+ ui.info(` Linux: ${import_picocolors16.default.dim("https://github.com/cli/cli/blob/trunk/docs/install_linux.md")}`);
6400
+ ui.info(` Windows: ${import_picocolors16.default.dim("winget install --id GitHub.cli")}
4638
6401
  `);
4639
- ui.info(` Then authenticate: ${import_picocolors14.default.dim("gh auth login")}
6402
+ ui.info(` Then authenticate: ${import_picocolors16.default.dim("gh auth login")}
4640
6403
  `);
4641
6404
  process.exit(1);
4642
6405
  }
@@ -4649,44 +6412,44 @@ var init_init2 = __esm(() => {
4649
6412
  if (gitOwner) {
4650
6413
  defaultRepo = `${gitOwner}/${gitOwner}.md`;
4651
6414
  if (ghLogin && ghLogin !== gitOwner) {
4652
- sourceNote = ` ${import_picocolors14.default.dim("(from git remote; your active gh account is " + ghLogin + ")")}
6415
+ sourceNote = ` ${import_picocolors16.default.dim("(from git remote; your active gh account is " + ghLogin + ")")}
4653
6416
  `;
4654
6417
  } else {
4655
- sourceNote = ` ${import_picocolors14.default.dim("(from git remote)")}
6418
+ sourceNote = ` ${import_picocolors16.default.dim("(from git remote)")}
4656
6419
  `;
4657
6420
  }
4658
6421
  } else if (ghLogin) {
4659
6422
  defaultRepo = `${ghLogin}/${ghLogin}.md`;
4660
- sourceNote = ` ${import_picocolors14.default.dim("(from your active gh account)")}
6423
+ sourceNote = ` ${import_picocolors16.default.dim("(from your active gh account)")}
4661
6424
  `;
4662
6425
  } else {
4663
- ui.warn(`Not logged in to GitHub. Run ${import_picocolors14.default.dim("gh auth login")} first.
6426
+ ui.warn(`Not logged in to GitHub. Run ${import_picocolors16.default.dim("gh auth login")} first.
4664
6427
  `);
4665
6428
  process.exit(1);
4666
6429
  }
4667
6430
  const existingConfig = await readConfig();
4668
6431
  if (existingConfig?.journal.repo) {
4669
6432
  defaultRepo = existingConfig.journal.repo;
4670
- sourceNote = ` ${import_picocolors14.default.dim("(from your previous journal setup)")}
6433
+ sourceNote = ` ${import_picocolors16.default.dim("(from your previous journal setup)")}
4671
6434
  `;
4672
6435
  }
4673
- ui.info(` Journal repo ${import_picocolors14.default.dim("(owner/name)")}`);
6436
+ ui.info(` Journal repo ${import_picocolors16.default.dim("(owner/name)")}`);
4674
6437
  if (sourceNote)
4675
6438
  ui.write(sourceNote);
4676
6439
  repo = prompt(" >", defaultRepo);
4677
6440
  }
4678
6441
  let project = args.project || process.env.DORAVAL_PROJECT;
4679
6442
  if (!project) {
4680
- const defaultProject = basename4(process.cwd());
6443
+ const defaultProject = basename6(process.cwd());
4681
6444
  project = prompt(" Project name", defaultProject);
4682
6445
  }
4683
6446
  project = sanitizeProjectName(project);
4684
6447
  if (!repoExists(repo)) {
4685
- ui.write(` ${import_picocolors14.default.red("\u2717")} ${import_picocolors14.default.white("Repository")} ${import_picocolors14.default.bold(repo)} ${import_picocolors14.default.white("not found on GitHub.")}
6448
+ ui.write(` ${import_picocolors16.default.red("\u2717")} ${import_picocolors16.default.white("Repository")} ${import_picocolors16.default.bold(repo)} ${import_picocolors16.default.white("not found on GitHub.")}
4686
6449
  `);
4687
6450
  ui.info(` Create it first:
4688
6451
  `);
4689
- ui.info(` ${import_picocolors14.default.dim(`gh repo create ${repo} --private --description "Personal journal for agent decisions"`)}
6452
+ ui.info(` ${import_picocolors16.default.dim(`gh repo create ${repo} --private --description "Personal journal for agent decisions"`)}
4690
6453
  `);
4691
6454
  process.exit(1);
4692
6455
  }
@@ -4694,16 +6457,16 @@ var init_init2 = __esm(() => {
4694
6457
  const alreadyRegistered = existing?.journal.projects[project];
4695
6458
  const isRefresh = alreadyRegistered && args.refresh;
4696
6459
  if (alreadyRegistered && !isRefresh) {
4697
- ui.write(` ${import_picocolors14.default.yellow("\u26A0")} ${import_picocolors14.default.white("Project")} ${import_picocolors14.default.bold(project)} ${import_picocolors14.default.white("is already registered.")}
6460
+ ui.write(` ${import_picocolors16.default.yellow("\u26A0")} ${import_picocolors16.default.white("Project")} ${import_picocolors16.default.bold(project)} ${import_picocolors16.default.white("is already registered.")}
4698
6461
  `);
4699
6462
  ui.info(` Repo: ${existing.journal.repo}
4700
6463
  `);
4701
- ui.info(` To refresh journal files, use ${import_picocolors14.default.dim("dora journal update")} (or ${import_picocolors14.default.dim("dora init --refresh")}).
6464
+ ui.info(` To refresh journal files, use ${import_picocolors16.default.dim("dora journal update")} (or ${import_picocolors16.default.dim("dora init --refresh")}).
4702
6465
  `);
4703
6466
  }
4704
6467
  const journalsDir = getJournalsDir();
4705
6468
  const remotePath = `projects/${project}.md`;
4706
- const localPath = join17(journalsDir, `${project}.md`);
6469
+ const localPath = join26(journalsDir, `${project}.md`);
4707
6470
  const effectiveRepo = isRefresh && !args.repo ? existing.journal.repo : repo;
4708
6471
  const config = existing ?? {
4709
6472
  journal: { repo: effectiveRepo, projects: {} }
@@ -4714,9 +6477,9 @@ var init_init2 = __esm(() => {
4714
6477
  local_path: localPath
4715
6478
  };
4716
6479
  ensureDoravalDirs();
4717
- ui.write(` ${import_picocolors14.default.dim(import_picocolors14.default.gray("Fetching journal files from"))} ${import_picocolors14.default.gray(effectiveRepo)}${import_picocolors14.default.dim(import_picocolors14.default.gray("..."))}
6480
+ ui.write(` ${import_picocolors16.default.dim(import_picocolors16.default.gray("Fetching journal files from"))} ${import_picocolors16.default.gray(effectiveRepo)}${import_picocolors16.default.dim(import_picocolors16.default.gray("..."))}
4718
6481
  `);
4719
- const globalDest = join17(journalsDir, "global.md");
6482
+ const globalDest = join26(journalsDir, "global.md");
4720
6483
  const refreshGlobalRes = await refreshLocalJournalFile(effectiveRepo, "global.md", globalDest);
4721
6484
  let wroteGlobal;
4722
6485
  if (!refreshGlobalRes.ok) {
@@ -4733,7 +6496,7 @@ var init_init2 = __esm(() => {
4733
6496
  if (wroteGlobal) {
4734
6497
  ui.success("global.md");
4735
6498
  } else {
4736
- ui.write(` ${import_picocolors14.default.dim("\xB7")} global.md ${import_picocolors14.default.dim("(not found \u2014 will be created on first sync)")}`);
6499
+ ui.write(` ${import_picocolors16.default.dim("\xB7")} global.md ${import_picocolors16.default.dim("(not found \u2014 will be created on first sync)")}`);
4737
6500
  await Bun.write(globalDest, `# Global Journal
4738
6501
 
4739
6502
  Cross-project principles.
@@ -4755,7 +6518,7 @@ Cross-project principles.
4755
6518
  if (wroteProject) {
4756
6519
  ui.success(remotePath);
4757
6520
  } else {
4758
- ui.write(` ${import_picocolors14.default.dim("\xB7")} ${remotePath} ${import_picocolors14.default.dim("(not found \u2014 will be created on first sync)")}`);
6521
+ ui.write(` ${import_picocolors16.default.dim("\xB7")} ${remotePath} ${import_picocolors16.default.dim("(not found \u2014 will be created on first sync)")}`);
4759
6522
  await Bun.write(localPath, `# ${project} Journal
4760
6523
 
4761
6524
  Project-specific decisions.
@@ -4763,13 +6526,13 @@ Project-specific decisions.
4763
6526
  }
4764
6527
  await writeConfig(config);
4765
6528
  ui.write(`
4766
- ${import_picocolors14.default.green("\u2713")} ${import_picocolors14.default.white("Journal ready for project")} ${import_picocolors14.default.bold(import_picocolors14.default.white(project))}.
6529
+ ${import_picocolors16.default.green("\u2713")} ${import_picocolors16.default.white("Journal ready for project")} ${import_picocolors16.default.bold(import_picocolors16.default.white(project))}.
4767
6530
  `);
4768
6531
  const existingAgent = (await readConfig())?.agent;
4769
6532
  if (existingAgent?.command) {
4770
- ui.write(` ${import_picocolors14.default.bold(import_picocolors14.default.white("Coding agent (already configured)"))}
6533
+ ui.write(` ${import_picocolors16.default.bold(import_picocolors16.default.white("Coding agent (already configured)"))}
4771
6534
  `);
4772
- ui.write(` Current: ${import_picocolors14.default.dim(import_picocolors14.default.gray(existingAgent.command))} template: ${import_picocolors14.default.dim(import_picocolors14.default.gray(existingAgent.prompt_template || "(default)"))}
6535
+ ui.write(` Current: ${import_picocolors16.default.dim(import_picocolors16.default.gray(existingAgent.command))} template: ${import_picocolors16.default.dim(import_picocolors16.default.gray(existingAgent.prompt_template || "(default)"))}
4773
6536
  `);
4774
6537
  const change = prompt(" Reconfigure / change the coding agent for on-the-fly enrichment? (y/N)", "n");
4775
6538
  if (!/^y/i.test(String(change))) {
@@ -4779,16 +6542,16 @@ Project-specific decisions.
4779
6542
  if (existingAgent)
4780
6543
  cfg.agent = existingAgent;
4781
6544
  await writeConfig(cfg);
4782
- ui.write(` ${import_picocolors14.default.green("\u2713")} ${import_picocolors14.default.white("Try:")} ${import_picocolors14.default.dim(import_picocolors14.default.gray('dora journal add "short decision"'))}
6545
+ ui.write(` ${import_picocolors16.default.green("\u2713")} ${import_picocolors16.default.white("Try:")} ${import_picocolors16.default.dim(import_picocolors16.default.gray('dora journal add "short decision"'))}
4783
6546
  `);
4784
6547
  process.exit(0);
4785
6548
  return;
4786
6549
  }
4787
6550
  ui.blank();
4788
6551
  } else {
4789
- ui.write(` ${import_picocolors14.default.bold(import_picocolors14.default.white("Coding agent for journal add"))}
6552
+ ui.write(` ${import_picocolors16.default.bold(import_picocolors16.default.white("Coding agent for journal add"))}
4790
6553
  `);
4791
- ui.info(` When configured, ${import_picocolors14.default.dim(import_picocolors14.default.gray('dora journal add ".."'))} will use your agent to enrich entries with tags and rationale automatically.
6554
+ ui.info(` When configured, ${import_picocolors16.default.dim(import_picocolors16.default.gray('dora journal add ".."'))} will use your agent to enrich entries with tags and rationale automatically.
4792
6555
  `);
4793
6556
  }
4794
6557
  const common = [
@@ -4807,7 +6570,7 @@ Project-specific decisions.
4807
6570
  }
4808
6571
  }
4809
6572
  let agentCmd = detected || "claude";
4810
- ui.write(` Detected / default agent command: ${import_picocolors14.default.dim(import_picocolors14.default.gray(agentCmd))}`);
6573
+ ui.write(` Detected / default agent command: ${import_picocolors16.default.dim(import_picocolors16.default.gray(agentCmd))}`);
4811
6574
  agentCmd = prompt(" Agent command (the binary you run for prompts)", agentCmd);
4812
6575
  let template = detected ? common.find((c) => c.name === detected)?.template || '-p "{{prompt}}" --output-format json' : '-p "{{prompt}}" --output-format json';
4813
6576
  ui.info(` Prompt template (use {{prompt}} placeholder):`);
@@ -4819,11 +6582,11 @@ Project-specific decisions.
4819
6582
  };
4820
6583
  await writeConfig(finalConfig);
4821
6584
  ui.write(`
4822
- ${import_picocolors14.default.green("\u2713")} ${import_picocolors14.default.white("Agent configured.")}
6585
+ ${import_picocolors16.default.green("\u2713")} ${import_picocolors16.default.white("Agent configured.")}
4823
6586
  `);
4824
- ui.info(` Re-run ${import_picocolors14.default.dim(import_picocolors14.default.gray("dora init"))} anytime to change it.
6587
+ ui.info(` Re-run ${import_picocolors16.default.dim(import_picocolors16.default.gray("dora init"))} anytime to change it.
4825
6588
  `);
4826
- ui.info(` Next: ${import_picocolors14.default.dim(import_picocolors14.default.gray('dora journal add ".."'))}, ${import_picocolors14.default.dim(import_picocolors14.default.gray("dora journal list"))}, or ${import_picocolors14.default.dim(import_picocolors14.default.gray("dora journal update"))}.
6589
+ ui.info(` Next: ${import_picocolors16.default.dim(import_picocolors16.default.gray('dora journal add ".."'))}, ${import_picocolors16.default.dim(import_picocolors16.default.gray("dora journal list"))}, or ${import_picocolors16.default.dim(import_picocolors16.default.gray("dora journal update"))}.
4827
6590
  `);
4828
6591
  process.exit(0);
4829
6592
  }
@@ -4831,7 +6594,7 @@ Project-specific decisions.
4831
6594
  });
4832
6595
 
4833
6596
  // src/core/update.ts
4834
- import { resolve as resolve16 } from "path";
6597
+ import { resolve as resolve28 } from "path";
4835
6598
  import { homedir as homedir2 } from "os";
4836
6599
  function normalizePath(p) {
4837
6600
  return p.replace(/\\/g, "/").replace(/\/+$/, "");
@@ -5010,14 +6773,14 @@ async function readMarker() {
5010
6773
  async function writeMarker(marker) {
5011
6774
  try {
5012
6775
  const { mkdir, writeFile } = await import("fs/promises");
5013
- const { dirname: dirname2 } = await import("path");
5014
- await mkdir(dirname2(MARKER_PATH), { recursive: true });
6776
+ const { dirname: dirname7 } = await import("path");
6777
+ await mkdir(dirname7(MARKER_PATH), { recursive: true });
5015
6778
  await writeFile(MARKER_PATH, JSON.stringify(marker, null, 2));
5016
6779
  } catch {}
5017
6780
  }
5018
6781
  var MARKER_PATH;
5019
6782
  var init_update2 = __esm(() => {
5020
- MARKER_PATH = resolve16(homedir2(), ".doraval", "install.json");
6783
+ MARKER_PATH = resolve28(homedir2(), ".doraval", "install.json");
5021
6784
  });
5022
6785
 
5023
6786
  // src/cli/commands/update.ts
@@ -5032,10 +6795,10 @@ import { realpath, access } from "fs/promises";
5032
6795
  async function confirmUpdate() {
5033
6796
  const { createInterface } = await import("readline");
5034
6797
  const rl = createInterface({ input: process.stdin, output: process.stdout });
5035
- return new Promise((resolve17) => {
6798
+ return new Promise((resolve29) => {
5036
6799
  rl.question("Update now? (y/N) ", (answer) => {
5037
6800
  rl.close();
5038
- resolve17(answer.toLowerCase().startsWith("y"));
6801
+ resolve29(answer.toLowerCase().startsWith("y"));
5039
6802
  });
5040
6803
  });
5041
6804
  }
@@ -5158,10 +6921,58 @@ Raw output above.`);
5158
6921
  });
5159
6922
  });
5160
6923
 
6924
+ // src/cli/commands/providers.ts
6925
+ var exports_providers = {};
6926
+ __export(exports_providers, {
6927
+ default: () => providers_default
6928
+ });
6929
+ var import_picocolors17, providers_default;
6930
+ var init_providers2 = __esm(() => {
6931
+ init_dist();
6932
+ init_out();
6933
+ init_spec();
6934
+ import_picocolors17 = __toESM(require_picocolors(), 1);
6935
+ providers_default = defineCommand({
6936
+ meta: {
6937
+ name: "providers",
6938
+ description: "List supported providers and their packaging details"
6939
+ },
6940
+ args: {
6941
+ json: {
6942
+ type: "boolean",
6943
+ description: "Output as JSON",
6944
+ default: false
6945
+ }
6946
+ },
6947
+ run({ args }) {
6948
+ if (args.json) {
6949
+ console.log(JSON.stringify(supportedProviders.map((id) => {
6950
+ const spec = getProviderSpec(id);
6951
+ return { ...spec, id };
6952
+ }), null, 2));
6953
+ process.exit(0);
6954
+ }
6955
+ ui.heading("doraval providers \u2014 Supported platforms");
6956
+ for (const id of supportedProviders) {
6957
+ const spec = getProviderSpec(id);
6958
+ ui.write(`
6959
+ ${import_picocolors17.default.bold(id)} \u2014 ${spec.name}`);
6960
+ ui.info(` Manifest: ${spec.manifestPath}`);
6961
+ ui.info(` Marketplace: ${spec.marketplacePath}`);
6962
+ ui.info(` MCP: ${spec.mcpFilename}`);
6963
+ ui.info(` Example: doraval validate . --for ${id}`);
6964
+ }
6965
+ ui.write(`
6966
+ Use --json for machine-readable output.`);
6967
+ process.exit(0);
6968
+ }
6969
+ });
6970
+ });
6971
+
5161
6972
  // src/cli/index.ts
5162
6973
  init_dist();
5163
6974
  var import__package = __toESM(require_package(), 1);
5164
- var import_picocolors15 = __toESM(require_picocolors(), 1);
6975
+ var import_picocolors18 = __toESM(require_picocolors(), 1);
5165
6976
  var skill = defineCommand({
5166
6977
  meta: {
5167
6978
  name: "skill",
@@ -5218,6 +7029,32 @@ var codex = defineCommand({
5218
7029
  showUsage(codex);
5219
7030
  }
5220
7031
  });
7032
+ var cursor = defineCommand({
7033
+ meta: {
7034
+ name: "cursor",
7035
+ description: "Cursor-specific commands (packaging, scaffolding, distribution)"
7036
+ },
7037
+ subCommands: {
7038
+ new: () => Promise.resolve().then(() => (init_new3(), exports_new3)).then((m) => m.default),
7039
+ bump: () => Promise.resolve().then(() => (init_bump(), exports_bump)).then((m) => m.default)
7040
+ },
7041
+ run() {
7042
+ showUsage(cursor);
7043
+ }
7044
+ });
7045
+ var copilot = defineCommand({
7046
+ meta: {
7047
+ name: "copilot",
7048
+ description: "Copilot CLI-specific commands (packaging, scaffolding, distribution)"
7049
+ },
7050
+ subCommands: {
7051
+ new: () => Promise.resolve().then(() => (init_new4(), exports_new4)).then((m) => m.default),
7052
+ bump: () => Promise.resolve().then(() => (init_bump(), exports_bump)).then((m) => m.default)
7053
+ },
7054
+ run() {
7055
+ showUsage(copilot);
7056
+ }
7057
+ });
5221
7058
  var doraemonArt = `
5222
7059
  \u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2880\u28E0\u28E4\u28F4\u28F6\u28F6\u28F6\u28F6\u28F6\u2836\u28F6\u28E4\u28E4\u28C0\u2800\u2800\u2800\u2800\u2800\u2800
5223
7060
  \u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2880\u28E4\u28FE\u28FF\u28FF\u28FF\u2801\u2800\u2880\u2808\u28BF\u2880\u28C0\u2800\u2839\u28FF\u28FF\u28FF\u28E6\u28C4\u2800\u2800\u2800
@@ -5241,14 +7078,17 @@ var main = defineCommand({
5241
7078
  init: () => Promise.resolve().then(() => (init_init2(), exports_init2)).then((m) => m.default),
5242
7079
  bump: () => Promise.resolve().then(() => (init_bump(), exports_bump)).then((m) => m.default),
5243
7080
  update: () => Promise.resolve().then(() => (init_update3(), exports_update2)).then((m) => m.default),
7081
+ providers: () => Promise.resolve().then(() => (init_providers2(), exports_providers)).then((m) => m.default),
5244
7082
  skill: () => Promise.resolve(skill),
5245
7083
  journal: () => Promise.resolve(journal),
5246
7084
  claude: () => Promise.resolve(claude),
5247
- codex: () => Promise.resolve(codex)
7085
+ codex: () => Promise.resolve(codex),
7086
+ cursor: () => Promise.resolve(cursor),
7087
+ copilot: () => Promise.resolve(copilot)
5248
7088
  },
5249
7089
  run() {
5250
7090
  console.log(`
5251
- ` + import_picocolors15.default.blue(doraemonArt) + `
7091
+ ` + import_picocolors18.default.blue(doraemonArt) + `
5252
7092
  `);
5253
7093
  showUsage(main);
5254
7094
  }