@hacksmith/doraval 0.2.21 → 0.2.23

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 (3) hide show
  1. package/README.md +7 -5
  2. package/bin/doraval.js +1187 -187
  3. package/package.json +1 -1
package/bin/doraval.js CHANGED
@@ -2624,17 +2624,17 @@ __export(exports_new, {
2624
2624
  default: () => new_default,
2625
2625
  decidePath: () => decidePath
2626
2626
  });
2627
- import { join as join8 } from "path";
2627
+ import { join as join8, basename as basename2 } from "path";
2628
2628
  import { mkdirSync as mkdirSync2, writeFileSync, existsSync as existsSync9 } from "fs";
2629
2629
  function decidePath(ctx, intent, providedName) {
2630
2630
  const rawName = providedName || "";
2631
- let path = "standalone";
2631
+ let decisionPath = "standalone";
2632
2632
  let targetDir = ctx.cwd;
2633
2633
  let shouldCreateDir = false;
2634
2634
  let migrateExisting = false;
2635
- const useCurrentDirAsRoot = rawName === "." || rawName === path.basename(ctx.cwd) || !rawName;
2635
+ const useCurrentDirAsRoot = rawName === "." || rawName === basename2(ctx.cwd) || !rawName;
2636
2636
  if (intent === "distribute" || intent === "self-later" && ctx.looseSkillFiles.length > 0 && !ctx.hasClaudeDir) {
2637
- path = "plugin";
2637
+ decisionPath = "plugin";
2638
2638
  if (useCurrentDirAsRoot) {
2639
2639
  targetDir = ctx.cwd;
2640
2640
  shouldCreateDir = false;
@@ -2644,7 +2644,7 @@ function decidePath(ctx, intent, providedName) {
2644
2644
  }
2645
2645
  migrateExisting = ctx.looseSkillFiles.length > 0;
2646
2646
  } else if (intent === "self-later" && !ctx.hasClaudeDir) {
2647
- path = "plugin";
2647
+ decisionPath = "plugin";
2648
2648
  if (useCurrentDirAsRoot) {
2649
2649
  targetDir = ctx.cwd;
2650
2650
  shouldCreateDir = false;
@@ -2652,7 +2652,7 @@ function decidePath(ctx, intent, providedName) {
2652
2652
  targetDir = join8(ctx.cwd, rawName);
2653
2653
  shouldCreateDir = true;
2654
2654
  }
2655
- } else if (path === "standalone") {
2655
+ } else if (decisionPath === "standalone") {
2656
2656
  if (useCurrentDirAsRoot) {
2657
2657
  targetDir = ctx.cwd;
2658
2658
  shouldCreateDir = false;
@@ -2661,7 +2661,7 @@ function decidePath(ctx, intent, providedName) {
2661
2661
  shouldCreateDir = true;
2662
2662
  }
2663
2663
  }
2664
- return { path, targetDir, shouldCreateDir, migrateExisting };
2664
+ return { path: decisionPath, targetDir, shouldCreateDir, migrateExisting };
2665
2665
  }
2666
2666
  function scaffold(decision, ctx, migrateContent) {
2667
2667
  const { targetDir, path, shouldCreateDir } = decision;
@@ -2673,26 +2673,57 @@ function scaffold(decision, ctx, migrateContent) {
2673
2673
  mkdirSync2(targetDir, { recursive: true });
2674
2674
  }
2675
2675
  if (path === "plugin") {
2676
+ const pluginName = basename2(targetDir);
2676
2677
  const pluginJson = {
2677
- name: decision.targetDir.split("/").pop(),
2678
+ name: pluginName,
2678
2679
  description: "Scaffolded by doraval claude new",
2679
2680
  version: "0.1.0"
2680
2681
  };
2681
2682
  mkdirSync2(join8(targetDir, ".claude-plugin"), { recursive: true });
2682
2683
  writeFileSync(join8(targetDir, ".claude-plugin", "plugin.json"), JSON.stringify(pluginJson, null, 2));
2683
- mkdirSync2(join8(targetDir, "skills", "my-skill"), { recursive: true });
2684
- const skillBody = migrateContent || `# My Skill
2685
-
2686
- Basic starter skill.`;
2687
- writeFileSync(join8(targetDir, "skills", "my-skill", "SKILL.md"), `---
2688
- name: my-skill
2689
- description: Starter skill
2684
+ const marketplaceJson = {
2685
+ name: pluginName,
2686
+ version: "0.1.0",
2687
+ description: "Scaffolded by doraval claude new",
2688
+ author: { name: "" },
2689
+ homepage: "",
2690
+ repository: "",
2691
+ license: "MIT",
2692
+ keywords: ["claude-code", "skills", "plugin"]
2693
+ };
2694
+ writeFileSync(join8(targetDir, "marketplace.json"), JSON.stringify(marketplaceJson, null, 2));
2695
+ const demoSkillName = "doraval";
2696
+ mkdirSync2(join8(targetDir, "skills", demoSkillName), { recursive: true });
2697
+ let skillContent;
2698
+ if (migrateContent) {
2699
+ skillContent = migrateContent;
2700
+ } else {
2701
+ skillContent = `---
2702
+ name: ${demoSkillName}
2703
+ description: Use doraval to validate, measure drift, and judge skills and plugins. Use when authoring or reviewing context engineering artifacts for AI coding agents.
2690
2704
  ---
2691
2705
 
2692
- ${skillBody}`);
2693
- writeFileSync(join8(targetDir, "README.md"), "# " + pluginJson.name + `
2706
+ # Use Doraval
2707
+
2708
+ Doraval is the context engineering toolkit.
2709
+
2710
+ When you need to check a skill or plugin:
2711
+
2712
+ - Validate the current directory: \`doraval validate .\`
2713
+ - Validate a specific plugin: \`doraval validate . --for claude:plugin\`
2714
+ - Validate one skill: \`doraval skill validate ./skills/${demoSkillName}/\`
2715
+ - Check for rubric drift: \`doraval skill drift ./skills/${demoSkillName}/\`
2716
+ - Get an AI quality judgment: \`doraval skill judge ./skills/${demoSkillName}/\`
2717
+
2718
+ Always run \`doraval validate\` before sharing or publishing a plugin. This skill demonstrates a complete, self-referential example of using doraval inside a generated plugin.`;
2719
+ }
2720
+ writeFileSync(join8(targetDir, "skills", demoSkillName, "SKILL.md"), skillContent);
2721
+ const readmePath = join8(targetDir, "README.md");
2722
+ if (!existsSync9(readmePath)) {
2723
+ writeFileSync(readmePath, "# " + pluginName + `
2694
2724
 
2695
2725
  Claude Code plugin scaffolded by doraval.`);
2726
+ }
2696
2727
  } else {
2697
2728
  mkdirSync2(join8(targetDir, ".claude", "skills", "my-skill"), { recursive: true });
2698
2729
  const skillBody = migrateContent || `# My Skill
@@ -2752,7 +2783,12 @@ var init_new = __esm(() => {
2752
2783
  scaffold(decision, ctx, migrateContent);
2753
2784
  ui.write(`
2754
2785
  ${import_picocolors10.default.green("\u2713")} Created ${decision.path} at ${import_picocolors10.default.bold(decision.targetDir)}`);
2755
- ui.info(` Command: ${decision.path === "plugin" ? `/${decision.targetDir.split("/").pop()}:my-skill` : "/my-skill"}`);
2786
+ const cmdName = decision.path === "plugin" ? `/${basename2(decision.targetDir)}:doraval` : "/my-skill";
2787
+ ui.info(` Command: ${cmdName}`);
2788
+ if (decision.path === "plugin") {
2789
+ ui.info(` Claude: .claude-plugin/plugin.json`);
2790
+ ui.info(` Marketplace: marketplace.json (unified / cross-provider listings)`);
2791
+ }
2756
2792
  ui.info(` Test: claude --plugin-dir ${decision.targetDir} (or use normally for standalone)`);
2757
2793
  ui.info(` Validate: doraval validate ${decision.targetDir}`);
2758
2794
  if (decision.path === "plugin" && decision.migrateExisting) {
@@ -2763,9 +2799,472 @@ var init_new = __esm(() => {
2763
2799
  });
2764
2800
  });
2765
2801
 
2802
+ // src/cli/commands/bump.ts
2803
+ var exports_bump = {};
2804
+ __export(exports_bump, {
2805
+ default: () => bump_default
2806
+ });
2807
+ import { resolve as resolve3, join as join9, dirname, relative } from "path";
2808
+ import { existsSync as existsSync10, readFileSync, writeFileSync as writeFileSync2, readdirSync as readdirSync4, statSync } from "fs";
2809
+ function bumpVersion(current, type) {
2810
+ if (/^\d+\.\d+\.\d+$/.test(type))
2811
+ return type;
2812
+ const curr = current || "0.0.0";
2813
+ const parts = curr.split(".").map((n) => parseInt(n, 10) || 0);
2814
+ const [major = 0, minor = 0, patch = 0] = parts;
2815
+ switch (type) {
2816
+ case "patch":
2817
+ return `${major}.${minor}.${patch + 1}`;
2818
+ case "minor":
2819
+ return `${major}.${minor + 1}.0`;
2820
+ case "major":
2821
+ return `${major + 1}.0.0`;
2822
+ default:
2823
+ throw new Error(`Invalid bump type "${type}". Use patch, minor, major, or an exact version like 1.2.3`);
2824
+ }
2825
+ }
2826
+ function readJson(p) {
2827
+ try {
2828
+ const content = readFileSync(p, "utf8");
2829
+ return JSON.parse(content);
2830
+ } catch {
2831
+ return null;
2832
+ }
2833
+ }
2834
+ function writeJson(p, data) {
2835
+ writeFileSync2(p, JSON.stringify(data, null, 2) + `
2836
+ `, "utf8");
2837
+ }
2838
+ function getVersion(obj) {
2839
+ if (!obj || typeof obj !== "object")
2840
+ return;
2841
+ if (typeof obj.version === "string")
2842
+ return obj.version;
2843
+ if (obj.metadata && typeof obj.metadata.version === "string")
2844
+ return obj.metadata.version;
2845
+ return;
2846
+ }
2847
+ function setVersion(obj, newVersion) {
2848
+ if (!obj || typeof obj !== "object")
2849
+ return false;
2850
+ if (typeof obj.version === "string") {
2851
+ obj.version = newVersion;
2852
+ return true;
2853
+ }
2854
+ if (obj.metadata && typeof obj.metadata.version === "string") {
2855
+ obj.metadata.version = newVersion;
2856
+ return true;
2857
+ }
2858
+ return false;
2859
+ }
2860
+ function walkForTargets(dir, maxDepth = 6, currentDepth = 0) {
2861
+ const results = [];
2862
+ if (currentDepth > maxDepth)
2863
+ return results;
2864
+ let entries;
2865
+ try {
2866
+ entries = readdirSync4(dir);
2867
+ } catch {
2868
+ return results;
2869
+ }
2870
+ for (const entry of entries) {
2871
+ const full = join9(dir, entry);
2872
+ let st;
2873
+ try {
2874
+ st = statSync(full);
2875
+ } catch {
2876
+ continue;
2877
+ }
2878
+ if (st.isDirectory()) {
2879
+ const sub = walkForTargets(full, maxDepth, currentDepth + 1);
2880
+ results.push(...sub);
2881
+ } else if (st.isFile()) {
2882
+ if (entry === "plugin.json") {
2883
+ const parentDir = dirname(full);
2884
+ const parentName = parentDir.split(/[/\\]/).pop();
2885
+ if (parentName === ".claude-plugin" || parentName === ".codex-plugin" || parentName === ".cursor-plugin") {
2886
+ results.push({
2887
+ file: full,
2888
+ kind: "plugin",
2889
+ label: `plugin manifest (${parentName.replace(".", "")})`
2890
+ });
2891
+ }
2892
+ } else if (entry === "marketplace.json") {
2893
+ const json = readJson(full);
2894
+ if (json && getVersion(json)) {
2895
+ results.push({
2896
+ file: full,
2897
+ kind: "marketplace",
2898
+ label: "marketplace.json"
2899
+ });
2900
+ }
2901
+ }
2902
+ }
2903
+ }
2904
+ return results;
2905
+ }
2906
+ var import_picocolors11, bump_default;
2907
+ var init_bump = __esm(() => {
2908
+ init_dist();
2909
+ init_out();
2910
+ import_picocolors11 = __toESM(require_picocolors(), 1);
2911
+ bump_default = defineCommand({
2912
+ meta: {
2913
+ name: "bump",
2914
+ description: "Bump semver versions in plugin.json (manifests) and marketplace.json files (supports Claude, Codex, Cursor)"
2915
+ },
2916
+ args: {
2917
+ type: {
2918
+ type: "positional",
2919
+ description: "patch | minor | major | x.y.z (exact version)",
2920
+ required: false
2921
+ },
2922
+ path: {
2923
+ type: "positional",
2924
+ description: "Directory to scan from (defaults to current dir). Supports single plugin or marketplace root with many plugins/",
2925
+ required: false
2926
+ },
2927
+ only: {
2928
+ type: "string",
2929
+ description: 'Scope to "all" (default), "plugin" (only plugin.json manifests), or "marketplace" (only marketplace.json files that carry a top-level version)',
2930
+ default: "all"
2931
+ }
2932
+ },
2933
+ run({ args }) {
2934
+ let rawType = args.type || "patch";
2935
+ let targetPath = args.path || ".";
2936
+ const scopeInput = (args.only || "all").toLowerCase();
2937
+ const scope = scopeInput === "plugin" || scopeInput === "marketplace" ? scopeInput : "all";
2938
+ if (!["all", "plugin", "marketplace"].includes(scopeInput)) {
2939
+ ui.fail(`Invalid --only "${args.only}". Allowed: all, plugin, marketplace.`);
2940
+ process.exit(1);
2941
+ }
2942
+ const isKnownType = ["patch", "minor", "major"].includes(rawType) || /^\d+\.\d+\.\d+$/.test(rawType);
2943
+ const maybePath = resolve3(rawType);
2944
+ const looksLikeDir = existsSync10(maybePath) || rawType === "." || rawType.startsWith("./") || rawType.startsWith("../");
2945
+ if (!isKnownType && looksLikeDir) {
2946
+ targetPath = rawType;
2947
+ rawType = "patch";
2948
+ } else if (!isKnownType) {
2949
+ ui.fail(`Unknown bump type "${rawType}". Use patch | minor | major | 1.2.3`);
2950
+ process.exit(1);
2951
+ }
2952
+ const root = resolve3(targetPath);
2953
+ if (!existsSync10(root)) {
2954
+ ui.fail(`Path does not exist: ${root}`);
2955
+ process.exit(1);
2956
+ }
2957
+ ui.heading("doraval bump");
2958
+ ui.info(` scanning: ${root}`);
2959
+ ui.info(` scope: ${scope} (use --only plugin or --only marketplace to narrow; Cursor metadata.version supported)`);
2960
+ const discovered = walkForTargets(root);
2961
+ let targets = discovered;
2962
+ if (scope === "plugin") {
2963
+ targets = discovered.filter((t) => t.kind === "plugin");
2964
+ } else if (scope === "marketplace") {
2965
+ targets = discovered.filter((t) => t.kind === "marketplace");
2966
+ }
2967
+ if (targets.length === 0) {
2968
+ ui.fail("No matching files found under the scope.");
2969
+ ui.info("");
2970
+ ui.info(" Looked for (recursively):");
2971
+ ui.info(" \u2022 **/.claude-plugin/plugin.json");
2972
+ ui.info(" \u2022 **/.codex-plugin/plugin.json");
2973
+ ui.info(" \u2022 **/.cursor-plugin/plugin.json (or marketplace.json)");
2974
+ ui.info(" \u2022 **/marketplace.json (top-level version or metadata.version for Cursor)");
2975
+ ui.info("");
2976
+ ui.info(" Tip: run from inside a plugin directory, or pass a path that contains plugins/.");
2977
+ ui.info(" Examples:");
2978
+ ui.info(" dora bump minor");
2979
+ ui.info(" dora bump minor ./my-claude-plugin");
2980
+ ui.info(" dora bump --only plugin . # only the manifests");
2981
+ ui.info(" dora bump --only marketplace ./marketplaces-root # includes Cursor metadata.version");
2982
+ process.exit(1);
2983
+ }
2984
+ ui.info(` matched ${targets.length} file(s)`);
2985
+ let bumpedCount = 0;
2986
+ for (const t of targets) {
2987
+ const json = readJson(t.file);
2988
+ if (!json || typeof json !== "object") {
2989
+ ui.warnItem(`skipped (invalid JSON): ${relative(root, t.file)}`);
2990
+ continue;
2991
+ }
2992
+ const current = getVersion(json);
2993
+ let next;
2994
+ try {
2995
+ next = bumpVersion(current, rawType);
2996
+ } catch (err) {
2997
+ ui.fail(err.message || String(err));
2998
+ process.exit(1);
2999
+ }
3000
+ const relPath = relative(root, t.file);
3001
+ if (current === next) {
3002
+ ui.dim(` \u2022 ${t.label} ${current || "(no version)"} (no change) [${relPath}]`);
3003
+ continue;
3004
+ }
3005
+ const didUpdate = setVersion(json, next);
3006
+ if (!didUpdate) {
3007
+ ui.warnItem(`skipped (could not locate version field to update): ${relPath}`);
3008
+ continue;
3009
+ }
3010
+ writeJson(t.file, json);
3011
+ ui.success(`${t.label}: ${import_picocolors11.default.dim(current || "(none)")} \u2192 ${import_picocolors11.default.green(next)}`);
3012
+ ui.info(` ${relPath}`);
3013
+ bumpedCount++;
3014
+ }
3015
+ ui.blank();
3016
+ if (bumpedCount === 0) {
3017
+ ui.info("All matched files were already at the target version.");
3018
+ } else {
3019
+ ui.info(`Done. Bumped ${bumpedCount} file(s).`);
3020
+ ui.dim(" Next: doraval validate " + (targetPath === "." ? "." : targetPath));
3021
+ }
3022
+ process.exit(0);
3023
+ }
3024
+ });
3025
+ });
3026
+
3027
+ // src/cli/commands/codex/context.ts
3028
+ import { existsSync as existsSync11, readdirSync as readdirSync5 } from "fs";
3029
+ import { join as join10 } from "path";
3030
+ function detectContext2(cwd = process.cwd()) {
3031
+ const hasCodexDir = existsSync11(join10(cwd, ".codex"));
3032
+ const hasPluginManifest = existsSync11(join10(cwd, ".codex-plugin", "plugin.json"));
3033
+ const hasMarketplace = existsSync11(join10(cwd, ".agents", "plugins", "marketplace.json")) || existsSync11(join10(cwd, ".codex-plugin", "marketplace.json"));
3034
+ let looseSkillFiles = [];
3035
+ try {
3036
+ const files = readdirSync5(cwd);
3037
+ looseSkillFiles = files.filter((f) => {
3038
+ if (!f.endsWith(".md") || f.startsWith("."))
3039
+ return false;
3040
+ const lower = f.toLowerCase();
3041
+ if (lower === "readme.md" || lower === "changelog.md" || lower === "license.md" || lower.includes("contributing"))
3042
+ return false;
3043
+ return lower.includes("skill") || lower === "skill.md";
3044
+ });
3045
+ } catch {}
3046
+ const isEmpty = !hasPluginManifest && looseSkillFiles.length === 0;
3047
+ return {
3048
+ cwd,
3049
+ hasCodexDir,
3050
+ hasPluginManifest,
3051
+ hasMarketplace,
3052
+ looseSkillFiles,
3053
+ isEmpty
3054
+ };
3055
+ }
3056
+ var init_context2 = () => {};
3057
+
3058
+ // src/cli/commands/codex/new.ts
3059
+ var exports_new2 = {};
3060
+ __export(exports_new2, {
3061
+ scaffold: () => scaffold2,
3062
+ default: () => new_default2,
3063
+ decidePath: () => decidePath2
3064
+ });
3065
+ import { join as join11, basename as basename3 } from "path";
3066
+ import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3, existsSync as existsSync12 } from "fs";
3067
+ function decidePath2(ctx, intent, providedName) {
3068
+ const rawName = providedName || "";
3069
+ let decisionPath = "standalone";
3070
+ let targetDir = ctx.cwd;
3071
+ let shouldCreateDir = false;
3072
+ let migrateExisting = false;
3073
+ const useCurrentDirAsRoot = rawName === "." || rawName === basename3(ctx.cwd) || !rawName;
3074
+ if (intent === "distribute" || intent === "self-later" && ctx.looseSkillFiles.length > 0 && !ctx.hasPluginManifest) {
3075
+ decisionPath = "plugin";
3076
+ if (useCurrentDirAsRoot) {
3077
+ targetDir = ctx.cwd;
3078
+ shouldCreateDir = false;
3079
+ } else {
3080
+ targetDir = join11(ctx.cwd, rawName);
3081
+ shouldCreateDir = true;
3082
+ }
3083
+ migrateExisting = ctx.looseSkillFiles.length > 0;
3084
+ } else if (intent === "self-later" && !ctx.hasPluginManifest) {
3085
+ decisionPath = "plugin";
3086
+ if (useCurrentDirAsRoot) {
3087
+ targetDir = ctx.cwd;
3088
+ shouldCreateDir = false;
3089
+ } else {
3090
+ targetDir = join11(ctx.cwd, rawName);
3091
+ shouldCreateDir = true;
3092
+ }
3093
+ } else if (decisionPath === "standalone") {
3094
+ if (useCurrentDirAsRoot) {
3095
+ targetDir = ctx.cwd;
3096
+ shouldCreateDir = false;
3097
+ } else {
3098
+ targetDir = join11(ctx.cwd, rawName);
3099
+ shouldCreateDir = true;
3100
+ }
3101
+ }
3102
+ return { path: decisionPath, targetDir, shouldCreateDir, migrateExisting };
3103
+ }
3104
+ function scaffold2(decision, ctx, migrateContent) {
3105
+ const { targetDir, path, shouldCreateDir } = decision;
3106
+ if (existsSync12(targetDir) && shouldCreateDir) {
3107
+ ui.fail("Target already exists");
3108
+ process.exit(1);
3109
+ }
3110
+ if (shouldCreateDir) {
3111
+ mkdirSync3(targetDir, { recursive: true });
3112
+ }
3113
+ if (path === "plugin") {
3114
+ const pluginName = basename3(targetDir);
3115
+ const pluginJson = {
3116
+ name: pluginName,
3117
+ version: "0.1.0",
3118
+ description: "Scaffolded by doraval codex new",
3119
+ skills: "./skills/",
3120
+ interface: {
3121
+ displayName: pluginName,
3122
+ shortDescription: "Scaffolded starter plugin",
3123
+ category: "Productivity"
3124
+ }
3125
+ };
3126
+ mkdirSync3(join11(targetDir, ".codex-plugin"), { recursive: true });
3127
+ writeFileSync3(join11(targetDir, ".codex-plugin", "plugin.json"), JSON.stringify(pluginJson, null, 2));
3128
+ mkdirSync3(join11(targetDir, ".agents", "plugins"), { recursive: true });
3129
+ const marketplaceJson = {
3130
+ name: "local",
3131
+ interface: {
3132
+ displayName: "Local (doraval scaffold)"
3133
+ },
3134
+ plugins: [
3135
+ {
3136
+ name: pluginName,
3137
+ source: {
3138
+ source: "local",
3139
+ path: "../.."
3140
+ },
3141
+ policy: {
3142
+ installation: "AVAILABLE",
3143
+ authentication: "ON_INSTALL"
3144
+ },
3145
+ category: "Productivity"
3146
+ }
3147
+ ]
3148
+ };
3149
+ writeFileSync3(join11(targetDir, ".agents", "plugins", "marketplace.json"), JSON.stringify(marketplaceJson, null, 2));
3150
+ const demoSkillName = "doraval";
3151
+ mkdirSync3(join11(targetDir, "skills", demoSkillName), { recursive: true });
3152
+ let skillContent;
3153
+ if (migrateContent) {
3154
+ skillContent = migrateContent;
3155
+ } else {
3156
+ skillContent = `---
3157
+ name: ${demoSkillName}
3158
+ 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 Codex too).
3159
+ ---
3160
+
3161
+ # Use Doraval (Codex edition)
3162
+
3163
+ Doraval is the context engineering toolkit.
3164
+
3165
+ When you need to check a skill or Codex plugin:
3166
+
3167
+ - Validate the current directory: \`doraval validate .\`
3168
+ - Validate one skill: \`doraval skill validate ./skills/${demoSkillName}/\`
3169
+ - Check for rubric drift: \`doraval skill drift ./skills/${demoSkillName}/\`
3170
+ - Get an AI quality judgment: \`doraval skill judge ./skills/${demoSkillName}/\`
3171
+
3172
+ Always run \`doraval validate\` before sharing or publishing a plugin.
3173
+
3174
+ This skill demonstrates a complete, self-referential example of using doraval inside a generated Codex plugin.
3175
+
3176
+ To test in Codex:
3177
+ 1. Make sure this plugin is listed in a marketplace (we created .agents/plugins/marketplace.json for you).
3178
+ 2. Restart Codex.
3179
+ 3. Open the plugin directory, select your local marketplace, and enable the plugin.
3180
+ 4. Invoke the demo with /${pluginName}:doraval`;
3181
+ }
3182
+ writeFileSync3(join11(targetDir, "skills", demoSkillName, "SKILL.md"), skillContent);
3183
+ const readmePath = join11(targetDir, "README.md");
3184
+ if (!existsSync12(readmePath)) {
3185
+ writeFileSync3(readmePath, "# " + pluginName + `
3186
+
3187
+ Codex plugin scaffolded by doraval.`);
3188
+ }
3189
+ } else {
3190
+ mkdirSync3(join11(targetDir, "skills", "doraval"), { recursive: true });
3191
+ const skillBody = migrateContent || `# My Skill
3192
+
3193
+ Basic starter for Codex.`;
3194
+ writeFileSync3(join11(targetDir, "skills", "doraval", "SKILL.md"), `---
3195
+ name: doraval
3196
+ description: Starter (local skill)
3197
+ ---
3198
+
3199
+ ${skillBody}`);
3200
+ }
3201
+ }
3202
+ var import_picocolors12, new_default2;
3203
+ var init_new2 = __esm(() => {
3204
+ init_dist();
3205
+ init_out();
3206
+ init_context2();
3207
+ init_prompt();
3208
+ import_picocolors12 = __toESM(require_picocolors(), 1);
3209
+ new_default2 = defineCommand({
3210
+ meta: {
3211
+ name: "new",
3212
+ description: "Create a new skill or plugin following Codex packaging rules"
3213
+ },
3214
+ args: {
3215
+ name: {
3216
+ type: "positional",
3217
+ description: "Optional name for the skill or plugin",
3218
+ required: false
3219
+ },
3220
+ yes: {
3221
+ type: "boolean",
3222
+ description: "Skip interactive prompts (use defaults and flags)",
3223
+ default: false
3224
+ },
3225
+ intent: {
3226
+ type: "string",
3227
+ description: 'Intent: "self" | "self-later" | "distribute"',
3228
+ required: false
3229
+ }
3230
+ },
3231
+ run({ args }) {
3232
+ ui.heading("doraval codex new \u2014 Context-aware scaffolding");
3233
+ const ctx = detectContext2();
3234
+ let intent = args.intent || "self-later";
3235
+ if (!args.yes) {
3236
+ const ans = prompt(" Intent (self | self-later | distribute)", intent);
3237
+ intent = ans || intent;
3238
+ }
3239
+ const decision = decidePath2(ctx, intent, args.name);
3240
+ ui.info(` Decision: path=${decision.path}, target=${decision.targetDir}`);
3241
+ let migrateContent;
3242
+ if (decision.migrateExisting && !args.yes) {
3243
+ migrateContent = "Content from your existing SKILL.md (user-confirmed).";
3244
+ }
3245
+ scaffold2(decision, ctx, migrateContent);
3246
+ ui.write(`
3247
+ ${import_picocolors12.default.green("\u2713")} Created ${decision.path} at ${import_picocolors12.default.bold(decision.targetDir)}`);
3248
+ const cmdName = decision.path === "plugin" ? `/${basename3(decision.targetDir)}:doraval` : "/doraval (local skill)";
3249
+ ui.info(` Command: ${cmdName}`);
3250
+ if (decision.path === "plugin") {
3251
+ ui.info(` Codex manifest: .codex-plugin/plugin.json`);
3252
+ ui.info(` Marketplace catalog: .agents/plugins/marketplace.json (starter for local testing)`);
3253
+ ui.info(` (Move/expand the marketplace.json to $REPO_ROOT/.agents/plugins/ or ~/.agents/plugins/ as needed)`);
3254
+ }
3255
+ ui.info(` Test (local): restart Codex, select your marketplace in the plugin directory`);
3256
+ ui.info(` Validate: doraval validate ${decision.targetDir}`);
3257
+ if (decision.path === "plugin" && decision.migrateExisting) {
3258
+ ui.info(" (Existing content migrated where confirmed.)");
3259
+ }
3260
+ process.exit(0);
3261
+ }
3262
+ });
3263
+ });
3264
+
2766
3265
  // src/validators/claude/skill.ts
2767
- import { existsSync as existsSync10 } from "fs";
2768
- import { resolve as resolve3 } from "path";
3266
+ import { existsSync as existsSync13 } from "fs";
3267
+ import { resolve as resolve4 } from "path";
2769
3268
  var OPTIONAL_DIRS2, claudeSkillValidator;
2770
3269
  var init_skill = __esm(() => {
2771
3270
  init_frontmatter();
@@ -2777,10 +3276,10 @@ var init_skill = __esm(() => {
2777
3276
  name: "Claude Skill",
2778
3277
  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.)",
2779
3278
  detect(dir) {
2780
- return existsSync10(resolve3(dir, "SKILL.md"));
3279
+ return existsSync13(resolve4(dir, "SKILL.md"));
2781
3280
  },
2782
3281
  async validate(dir, _opts) {
2783
- const skillMd = resolve3(dir, "SKILL.md");
3282
+ const skillMd = resolve4(dir, "SKILL.md");
2784
3283
  const raw = await Bun.file(skillMd).text();
2785
3284
  let parsed;
2786
3285
  try {
@@ -2792,32 +3291,108 @@ var init_skill = __esm(() => {
2792
3291
  passes: []
2793
3292
  };
2794
3293
  }
2795
- const existingDirs = OPTIONAL_DIRS2.filter((d) => existsSync10(resolve3(dir, d)));
3294
+ const existingDirs = OPTIONAL_DIRS2.filter((d) => existsSync13(resolve4(dir, d)));
2796
3295
  return validateSkillModel(parsed, { existingDirs: [...existingDirs] });
2797
3296
  }
2798
3297
  };
2799
3298
  });
2800
3299
 
2801
3300
  // src/validators/claude/plugin.ts
2802
- import { existsSync as existsSync11, readdirSync as readdirSync4 } from "fs";
2803
- import { resolve as resolve4, join as join9 } from "path";
2804
- var NAME_REGEX2, RELATIVE_PATH_REGEX, claudePluginValidator;
3301
+ import { existsSync as existsSync14, readdirSync as readdirSync6 } from "fs";
3302
+ import { resolve as resolve5, join as join12 } from "path";
3303
+ function levenshtein(a, b) {
3304
+ if (a === b)
3305
+ return 0;
3306
+ const m = a.length, n = b.length;
3307
+ if (m === 0)
3308
+ return n;
3309
+ if (n === 0)
3310
+ return m;
3311
+ const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
3312
+ for (let i = 0;i <= m; i++)
3313
+ dp[i][0] = i;
3314
+ for (let j = 0;j <= n; j++)
3315
+ dp[0][j] = j;
3316
+ for (let i = 1;i <= m; i++) {
3317
+ for (let j = 1;j <= n; j++) {
3318
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
3319
+ dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost);
3320
+ }
3321
+ }
3322
+ return dp[m][n];
3323
+ }
3324
+ function suggestField(unknown) {
3325
+ const lower = unknown.toLowerCase();
3326
+ for (const k of KNOWN_FIELDS2) {
3327
+ if (k.toLowerCase() === lower)
3328
+ return k;
3329
+ if (levenshtein(k.toLowerCase(), lower) <= 1)
3330
+ return k;
3331
+ if (k.toLowerCase().startsWith(lower.slice(0, 3)) && lower.length > 3)
3332
+ return k;
3333
+ }
3334
+ if (lower === "licence")
3335
+ return "license";
3336
+ if (lower === "dependancies" || lower === "deps")
3337
+ return "dependencies";
3338
+ if (lower === "mcp" || lower === "mcpservers")
3339
+ return "mcpServers";
3340
+ if (lower === "lsp")
3341
+ return "lspServers";
3342
+ if (lower === "outputstyles" || lower === "styles")
3343
+ return "outputStyles";
3344
+ if (lower === "userconfig")
3345
+ return "userConfig";
3346
+ return null;
3347
+ }
3348
+ function isRelativePathLike(v) {
3349
+ if (typeof v !== "string")
3350
+ return false;
3351
+ return RELATIVE_PATH_REGEX.test(v) && !v.includes("..");
3352
+ }
3353
+ var NAME_REGEX2, RELATIVE_PATH_REGEX, KNOWN_FIELDS2, REPLACES_DEFAULT, claudePluginValidator;
2805
3354
  var init_plugin = __esm(() => {
2806
3355
  NAME_REGEX2 = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
2807
3356
  RELATIVE_PATH_REGEX = /^\.\//;
3357
+ KNOWN_FIELDS2 = new Set([
3358
+ "$schema",
3359
+ "name",
3360
+ "displayName",
3361
+ "version",
3362
+ "description",
3363
+ "author",
3364
+ "homepage",
3365
+ "repository",
3366
+ "license",
3367
+ "keywords",
3368
+ "defaultEnabled",
3369
+ "skills",
3370
+ "commands",
3371
+ "agents",
3372
+ "hooks",
3373
+ "mcpServers",
3374
+ "outputStyles",
3375
+ "lspServers",
3376
+ "experimental",
3377
+ "userConfig",
3378
+ "channels",
3379
+ "dependencies"
3380
+ ]);
3381
+ REPLACES_DEFAULT = new Set(["commands", "agents", "outputStyles", "lspServers"]);
2808
3382
  claudePluginValidator = {
2809
3383
  id: "claude:plugin",
2810
3384
  provider: "claude",
2811
3385
  name: "Claude Plugin",
2812
- description: "Validates .claude-plugin/plugin.json manifest, component directories, and structure",
3386
+ 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",
2813
3387
  detect(dir) {
2814
- return existsSync11(resolve4(dir, ".claude-plugin", "plugin.json"));
3388
+ return existsSync14(resolve5(dir, ".claude-plugin", "plugin.json"));
2815
3389
  },
2816
3390
  async validate(dir, _opts) {
2817
3391
  const errors = [];
2818
3392
  const warnings = [];
2819
3393
  const passes = [];
2820
- const manifestPath = resolve4(dir, ".claude-plugin", "plugin.json");
3394
+ const manifestPath = resolve5(dir, ".claude-plugin", "plugin.json");
3395
+ const dotClaudePluginDir = resolve5(dir, ".claude-plugin");
2821
3396
  let manifest;
2822
3397
  try {
2823
3398
  const raw = await Bun.file(manifestPath).text();
@@ -2827,6 +3402,17 @@ var init_plugin = __esm(() => {
2827
3402
  errors.push(".claude-plugin/plugin.json is missing or invalid JSON");
2828
3403
  return { errors, warnings, passes };
2829
3404
  }
3405
+ try {
3406
+ const entries = readdirSync6(dotClaudePluginDir);
3407
+ const unexpected = entries.filter((e) => e !== "plugin.json");
3408
+ if (unexpected.length > 0) {
3409
+ for (const e of unexpected) {
3410
+ 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.`);
3411
+ }
3412
+ } else if (entries.length === 1) {
3413
+ passes.push(".claude-plugin/ contains only plugin.json (correct layout)");
3414
+ }
3415
+ } catch {}
2830
3416
  if (!manifest.name) {
2831
3417
  errors.push('Missing required field: "name"');
2832
3418
  } else {
@@ -2840,10 +3426,12 @@ var init_plugin = __esm(() => {
2840
3426
  if (manifest.version !== undefined) {
2841
3427
  const v = String(manifest.version);
2842
3428
  if (!/^\d+\.\d+\.\d+/.test(v)) {
2843
- errors.push(`Invalid version format: "${v}" \u2014 must be semver (MAJOR.MINOR.PATCH)`);
3429
+ errors.push(`Invalid version format: "${v}" \u2014 must look like semver (MAJOR.MINOR.PATCH) when using explicit versioning`);
2844
3430
  } else {
2845
- passes.push(`version: "${v}"`);
3431
+ passes.push(`version: "${v}" (explicit \u2014 bump on every release to publish updates)`);
2846
3432
  }
3433
+ } else {
3434
+ passes.push("version omitted (git commit SHA used as version key \u2014 every commit becomes an available update)");
2847
3435
  }
2848
3436
  if (manifest.description !== undefined) {
2849
3437
  const desc = String(manifest.description);
@@ -2852,56 +3440,163 @@ var init_plugin = __esm(() => {
2852
3440
  } else {
2853
3441
  passes.push("description field present");
2854
3442
  }
3443
+ } else {
3444
+ warnings.push('Missing "description" (recommended for UI, marketplace listings, and auto-discovery)');
3445
+ }
3446
+ if (manifest.displayName !== undefined) {
3447
+ passes.push(`displayName: "${manifest.displayName}" (human UI label; falls back to name)`);
3448
+ }
3449
+ if (manifest.author !== undefined) {
3450
+ const a = manifest.author;
3451
+ if (a && typeof a === "object" && a.name) {
3452
+ passes.push("author present");
3453
+ } else {
3454
+ warnings.push('author should be an object like {"name": "...", "email?": "..."}');
3455
+ }
3456
+ }
3457
+ if (manifest.license !== undefined) {
3458
+ passes.push(`license: "${manifest.license}"`);
3459
+ }
3460
+ if (manifest.keywords !== undefined) {
3461
+ if (Array.isArray(manifest.keywords)) {
3462
+ passes.push(`keywords: [${manifest.keywords.join(", ")}]`);
3463
+ } else {
3464
+ errors.push("keywords must be an array of strings");
3465
+ }
2855
3466
  }
2856
- const checkPaths = (field, value) => {
2857
- const paths = Array.isArray(value) ? value : [value];
2858
- for (const p of paths) {
2859
- const s = String(p);
2860
- if (!RELATIVE_PATH_REGEX.test(s)) {
2861
- errors.push(`${field}: path "${s}" must start with "./" (relative)`);
2862
- } else if (s.includes("..")) {
2863
- errors.push(`${field}: path "${s}" must not use ".." (no parent traversal)`);
2864
- } else if (existsSync11(resolve4(dir, s))) {
2865
- passes.push(`${field}: path "${s}" exists`);
3467
+ if (manifest.defaultEnabled !== undefined) {
3468
+ passes.push(`defaultEnabled: ${manifest.defaultEnabled}`);
3469
+ }
3470
+ if (manifest.homepage)
3471
+ passes.push("homepage present");
3472
+ if (manifest.repository)
3473
+ passes.push("repository present");
3474
+ const unknown = Object.keys(manifest).filter((k) => !KNOWN_FIELDS2.has(k));
3475
+ for (const k of unknown) {
3476
+ const sug = suggestField(k);
3477
+ const hint = sug ? ` (did you mean "${sug}"?)` : "";
3478
+ warnings.push(`Unrecognized top-level field "${k}"${hint} \u2014 will be ignored at runtime (allowed for cross-tool manifest compatibility).`);
3479
+ }
3480
+ const handleField = (field, val) => {
3481
+ if (val === undefined || val === null)
3482
+ return;
3483
+ if (isRelativePathLike(val) || Array.isArray(val) && val.every(isRelativePathLike)) {
3484
+ const arr = Array.isArray(val) ? val : [val];
3485
+ for (const p of arr) {
3486
+ const s = String(p);
3487
+ if (!RELATIVE_PATH_REGEX.test(s)) {
3488
+ errors.push(`${field}: path "${s}" must start with "./"`);
3489
+ } else if (s.includes("..")) {
3490
+ errors.push(`${field}: path "${s}" must not use ".." (paths are confined to the plugin tree after cache copy)`);
3491
+ } else if (existsSync14(resolve5(dir, s))) {
3492
+ passes.push(`${field}: path "${s}" exists`);
3493
+ } else {
3494
+ warnings.push(`${field}: path "${s}" does not exist on disk`);
3495
+ }
3496
+ }
3497
+ if (field === "skills") {
3498
+ passes.push(`${field}: augments the default skills/ (both are scanned)`);
3499
+ } else if (REPLACES_DEFAULT.has(field)) {
3500
+ passes.push(`${field}: custom path replaces default ${field}/ scan`);
2866
3501
  } else {
2867
- warnings.push(`${field}: path "${s}" does not exist on disk`);
3502
+ passes.push(`${field}: custom path or config (merge rules apply)`);
2868
3503
  }
3504
+ } else if (typeof val === "object") {
3505
+ passes.push(`${field}: inline ${field} config present`);
2869
3506
  }
2870
3507
  };
2871
- for (const field of ["commands", "agents", "hooks", "mcpServers"]) {
2872
- if (manifest[field] !== undefined) {
2873
- checkPaths(field, manifest[field]);
2874
- }
2875
- }
2876
- const skillsDir = resolve4(dir, "skills");
2877
- if (existsSync11(skillsDir)) {
2878
- const skillEntries = readdirSync4(skillsDir, { withFileTypes: true }).filter((e) => e.isDirectory());
2879
- for (const skill of skillEntries) {
2880
- const skillMd = join9(skillsDir, skill.name, "SKILL.md");
2881
- if (existsSync11(skillMd)) {
2882
- passes.push(`skills/${skill.name}/SKILL.md exists`);
3508
+ ["skills", "commands", "agents", "hooks", "mcpServers", "outputStyles", "lspServers"].forEach((f) => {
3509
+ if (manifest[f] !== undefined)
3510
+ handleField(f, manifest[f]);
3511
+ });
3512
+ if (manifest.experimental && typeof manifest.experimental === "object") {
3513
+ const exp = manifest.experimental;
3514
+ if (exp.themes !== undefined)
3515
+ handleField("experimental.themes", exp.themes);
3516
+ if (exp.monitors !== undefined)
3517
+ handleField("experimental.monitors", exp.monitors);
3518
+ passes.push("experimental section present (themes and monitors are experimental components)");
3519
+ }
3520
+ if (manifest.userConfig && typeof manifest.userConfig === "object") {
3521
+ const keys = Object.keys(manifest.userConfig);
3522
+ passes.push(`userConfig: ${keys.length} user-configurable value(s) declared`);
3523
+ for (const k of keys) {
3524
+ const opt = manifest.userConfig[k];
3525
+ if (!opt || !opt.type || !opt.title) {
3526
+ warnings.push(`userConfig.${k} is missing required "type" and/or "title"`);
3527
+ }
3528
+ }
3529
+ }
3530
+ if (Array.isArray(manifest.channels)) {
3531
+ passes.push(`channels: ${manifest.channels.length} channel(s) (each binds to an mcpServer)`);
3532
+ manifest.channels.forEach((ch, i) => {
3533
+ if (!ch?.server)
3534
+ warnings.push(`channels[${i}]: "server" is required and must match an mcpServers key`);
3535
+ });
3536
+ }
3537
+ if (Array.isArray(manifest.dependencies)) {
3538
+ passes.push(`dependencies: declares ${manifest.dependencies.length} plugin dependency/ies`);
3539
+ }
3540
+ const skillsDir = resolve5(dir, "skills");
3541
+ if (existsSync14(skillsDir)) {
3542
+ const entries = readdirSync6(skillsDir, { withFileTypes: true }).filter((e) => e.isDirectory());
3543
+ for (const e of entries) {
3544
+ const md = join12(skillsDir, e.name, "SKILL.md");
3545
+ if (existsSync14(md)) {
3546
+ passes.push(`skills/${e.name}/SKILL.md exists`);
2883
3547
  } else {
2884
- errors.push(`skills/${skill.name}/ missing SKILL.md`);
3548
+ errors.push(`skills/${e.name}/ is missing SKILL.md`);
2885
3549
  }
2886
3550
  }
3551
+ if (manifest.skills !== undefined) {
3552
+ warnings.push('Default skills/ dir co-exists with manifest "skills" \u2014 manifest path is authoritative; default folder ignored for loading');
3553
+ }
2887
3554
  }
2888
- const commandsDir = resolve4(dir, "commands");
2889
- if (existsSync11(commandsDir)) {
2890
- const mdFiles = readdirSync4(commandsDir).filter((f) => f.endsWith(".md"));
2891
- if (mdFiles.length > 0) {
2892
- passes.push(`commands/ has ${mdFiles.length} .md file(s)`);
2893
- } else {
2894
- warnings.push("commands/ directory exists but has no .md files");
3555
+ const commandsDir = resolve5(dir, "commands");
3556
+ if (existsSync14(commandsDir)) {
3557
+ const mds = readdirSync6(commandsDir).filter((f) => f.endsWith(".md"));
3558
+ if (mds.length) {
3559
+ passes.push(`commands/ has ${mds.length} .md file(s)`);
3560
+ }
3561
+ if (manifest.commands !== undefined) {
3562
+ warnings.push('commands/ co-exists with manifest "commands" \u2014 manifest replaces default (dir ignored)');
2895
3563
  }
2896
3564
  }
2897
- const agentsDir = resolve4(dir, "agents");
2898
- if (existsSync11(agentsDir)) {
2899
- const mdFiles = readdirSync4(agentsDir).filter((f) => f.endsWith(".md"));
2900
- if (mdFiles.length > 0) {
2901
- passes.push(`agents/ has ${mdFiles.length} .md file(s)`);
2902
- } else {
2903
- warnings.push("agents/ directory exists but has no .md files");
3565
+ const agentsDir = resolve5(dir, "agents");
3566
+ if (existsSync14(agentsDir)) {
3567
+ const mds = readdirSync6(agentsDir).filter((f) => f.endsWith(".md"));
3568
+ if (mds.length) {
3569
+ passes.push(`agents/ has ${mds.length} .md file(s)`);
2904
3570
  }
3571
+ if (manifest.agents !== undefined) {
3572
+ warnings.push('agents/ co-exists with manifest "agents" \u2014 manifest replaces default (dir ignored)');
3573
+ }
3574
+ }
3575
+ if (existsSync14(resolve5(dir, "output-styles"))) {
3576
+ passes.push("output-styles/ directory present");
3577
+ if (manifest.outputStyles)
3578
+ warnings.push("output-styles/ co-exists with manifest outputStyles \u2014 manifest wins");
3579
+ }
3580
+ if (existsSync14(resolve5(dir, "themes")))
3581
+ passes.push("themes/ present (experimental)");
3582
+ if (existsSync14(resolve5(dir, "monitors")) || manifest.experimental?.monitors) {
3583
+ passes.push("monitors config present (experimental)");
3584
+ }
3585
+ if (existsSync14(resolve5(dir, "bin")))
3586
+ passes.push("bin/ present (adds executables to Bash tool $PATH)");
3587
+ if (existsSync14(resolve5(dir, "settings.json")))
3588
+ passes.push("settings.json present (plugin defaults for agent/statusline)");
3589
+ if (existsSync14(resolve5(dir, "README.md")))
3590
+ passes.push("README.md present");
3591
+ if (existsSync14(resolve5(dir, ".mcp.json")))
3592
+ passes.push(".mcp.json present (validated by claude:mcp)");
3593
+ if (existsSync14(resolve5(dir, ".lsp.json")))
3594
+ passes.push(".lsp.json present (validated by claude:lsp when registered)");
3595
+ if (existsSync14(resolve5(dir, "hooks/hooks.json")) || existsSync14(resolve5(dir, "hooks.json"))) {
3596
+ passes.push("hooks config present (validated by claude:hooks)");
3597
+ }
3598
+ if (existsSync14(resolve5(dir, "SKILL.md")) && !existsSync14(skillsDir) && manifest.skills === undefined) {
3599
+ passes.push('Root SKILL.md detected \u2014 plugin will be treated as a single-skill plugin (prefer frontmatter "name" for stable /command)');
2905
3600
  }
2906
3601
  return { errors, warnings, passes };
2907
3602
  }
@@ -2909,8 +3604,8 @@ var init_plugin = __esm(() => {
2909
3604
  });
2910
3605
 
2911
3606
  // src/validators/claude/marketplace.ts
2912
- import { existsSync as existsSync12, readdirSync as readdirSync5 } from "fs";
2913
- import { resolve as resolve5, join as join10 } from "path";
3607
+ import { existsSync as existsSync15, readdirSync as readdirSync7 } from "fs";
3608
+ import { resolve as resolve6, join as join13 } from "path";
2914
3609
  var claudeMarketplaceValidator;
2915
3610
  var init_marketplace = __esm(() => {
2916
3611
  claudeMarketplaceValidator = {
@@ -2919,16 +3614,16 @@ var init_marketplace = __esm(() => {
2919
3614
  name: "Claude Plugin Marketplace",
2920
3615
  description: "Validates marketplace structure: plugins/ directory with valid plugin subdirectories",
2921
3616
  detect(dir) {
2922
- const pluginsDir = resolve5(dir, "plugins");
2923
- if (!existsSync12(pluginsDir))
3617
+ const pluginsDir = resolve6(dir, "plugins");
3618
+ if (!existsSync15(pluginsDir))
2924
3619
  return false;
2925
3620
  try {
2926
- const entries = readdirSync5(pluginsDir, { withFileTypes: true });
3621
+ const entries = readdirSync7(pluginsDir, { withFileTypes: true });
2927
3622
  for (const entry of entries) {
2928
3623
  if (!entry.isDirectory())
2929
3624
  continue;
2930
- const hasSkills = existsSync12(join10(pluginsDir, entry.name, "skills"));
2931
- const hasManifest = existsSync12(join10(pluginsDir, entry.name, ".claude-plugin", "plugin.json"));
3625
+ const hasSkills = existsSync15(join13(pluginsDir, entry.name, "skills"));
3626
+ const hasManifest = existsSync15(join13(pluginsDir, entry.name, ".claude-plugin", "plugin.json"));
2932
3627
  if (hasSkills || hasManifest)
2933
3628
  return true;
2934
3629
  }
@@ -2939,33 +3634,33 @@ var init_marketplace = __esm(() => {
2939
3634
  const errors = [];
2940
3635
  const warnings = [];
2941
3636
  const passes = [];
2942
- const pluginsDir = resolve5(dir, "plugins");
2943
- if (!existsSync12(pluginsDir)) {
3637
+ const pluginsDir = resolve6(dir, "plugins");
3638
+ if (!existsSync15(pluginsDir)) {
2944
3639
  errors.push("Missing plugins/ directory");
2945
3640
  return { errors, warnings, passes };
2946
3641
  }
2947
3642
  passes.push("plugins/ directory exists");
2948
- const pluginEntries = readdirSync5(pluginsDir, { withFileTypes: true }).filter((e) => e.isDirectory());
3643
+ const pluginEntries = readdirSync7(pluginsDir, { withFileTypes: true }).filter((e) => e.isDirectory());
2949
3644
  if (pluginEntries.length === 0) {
2950
3645
  errors.push("plugins/ directory is empty \u2014 expected at least one plugin");
2951
3646
  return { errors, warnings, passes };
2952
3647
  }
2953
3648
  passes.push(`${pluginEntries.length} plugin(s) found`);
2954
- if (existsSync12(resolve5(dir, "README.md"))) {
3649
+ if (existsSync15(resolve6(dir, "README.md"))) {
2955
3650
  passes.push("README.md exists at marketplace root");
2956
3651
  } else {
2957
3652
  warnings.push("No README.md at marketplace root \u2014 recommended for discoverability");
2958
3653
  }
2959
- if (existsSync12(resolve5(dir, "LICENSE"))) {
3654
+ if (existsSync15(resolve6(dir, "LICENSE"))) {
2960
3655
  passes.push("LICENSE exists at marketplace root");
2961
3656
  } else {
2962
3657
  warnings.push("No LICENSE at marketplace root \u2014 recommended");
2963
3658
  }
2964
3659
  for (const plugin of pluginEntries) {
2965
- const pluginPath = join10(pluginsDir, plugin.name);
2966
- const hasSkills = existsSync12(join10(pluginPath, "skills"));
2967
- const hasManifest = existsSync12(join10(pluginPath, ".claude-plugin", "plugin.json"));
2968
- const hasReadme = existsSync12(join10(pluginPath, "README.md"));
3660
+ const pluginPath = join13(pluginsDir, plugin.name);
3661
+ const hasSkills = existsSync15(join13(pluginPath, "skills"));
3662
+ const hasManifest = existsSync15(join13(pluginPath, ".claude-plugin", "plugin.json"));
3663
+ const hasReadme = existsSync15(join13(pluginPath, "README.md"));
2969
3664
  if (hasManifest || hasSkills) {
2970
3665
  passes.push(`Plugin "${plugin.name}" has ${hasManifest ? "manifest" : "skills/"}`);
2971
3666
  } else {
@@ -2981,35 +3676,55 @@ var init_marketplace = __esm(() => {
2981
3676
  });
2982
3677
 
2983
3678
  // src/validators/claude/hooks.ts
2984
- import { existsSync as existsSync13 } from "fs";
2985
- import { resolve as resolve6 } from "path";
3679
+ import { existsSync as existsSync16 } from "fs";
3680
+ import { resolve as resolve7 } from "path";
2986
3681
  var KNOWN_EVENTS, claudeHooksValidator;
2987
3682
  var init_hooks = __esm(() => {
2988
3683
  KNOWN_EVENTS = [
3684
+ "SessionStart",
3685
+ "Setup",
3686
+ "UserPromptSubmit",
3687
+ "UserPromptExpansion",
2989
3688
  "PreToolUse",
3689
+ "PermissionRequest",
3690
+ "PermissionDenied",
2990
3691
  "PostToolUse",
2991
- "Stop",
3692
+ "PostToolUseFailure",
3693
+ "PostToolBatch",
3694
+ "Notification",
3695
+ "MessageDisplay",
3696
+ "SubagentStart",
2992
3697
  "SubagentStop",
2993
- "SessionStart",
2994
- "SessionEnd",
2995
- "UserPromptSubmit",
3698
+ "TaskCreated",
3699
+ "TaskCompleted",
3700
+ "Stop",
3701
+ "StopFailure",
3702
+ "TeammateIdle",
3703
+ "InstructionsLoaded",
3704
+ "ConfigChange",
3705
+ "CwdChanged",
3706
+ "FileChanged",
3707
+ "WorktreeCreate",
3708
+ "WorktreeRemove",
2996
3709
  "PreCompact",
2997
- "Notification",
2998
- "PermissionRequest"
3710
+ "PostCompact",
3711
+ "Elicitation",
3712
+ "ElicitationResult",
3713
+ "SessionEnd"
2999
3714
  ];
3000
3715
  claudeHooksValidator = {
3001
3716
  id: "claude:hooks",
3002
3717
  provider: "claude",
3003
3718
  name: "Claude Hooks",
3004
- description: "Validates hooks/hooks.json: event names, matcher structure, hook types",
3719
+ 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)",
3005
3720
  detect(dir) {
3006
- return existsSync13(resolve6(dir, "hooks", "hooks.json")) || existsSync13(resolve6(dir, "hooks.json"));
3721
+ return existsSync16(resolve7(dir, "hooks", "hooks.json")) || existsSync16(resolve7(dir, "hooks.json"));
3007
3722
  },
3008
3723
  async validate(dir, _opts) {
3009
3724
  const errors = [];
3010
3725
  const warnings = [];
3011
3726
  const passes = [];
3012
- const hooksPath = existsSync13(resolve6(dir, "hooks", "hooks.json")) ? resolve6(dir, "hooks", "hooks.json") : resolve6(dir, "hooks.json");
3727
+ const hooksPath = existsSync16(resolve7(dir, "hooks", "hooks.json")) ? resolve7(dir, "hooks", "hooks.json") : resolve7(dir, "hooks.json");
3013
3728
  let config;
3014
3729
  try {
3015
3730
  const raw = await Bun.file(hooksPath).text();
@@ -3024,32 +3739,74 @@ var init_hooks = __esm(() => {
3024
3739
  if (KNOWN_EVENTS.includes(name)) {
3025
3740
  passes.push(`Event "${name}" is a known lifecycle event`);
3026
3741
  } else {
3027
- warnings.push(`Unknown event name: "${name}" \u2014 expected one of: ${KNOWN_EVENTS.join(", ")}`);
3742
+ warnings.push(`Unknown event name: "${name}" \u2014 see full list in Plugins reference (SessionStart, PreToolUse, PostToolUse, Stop, ...)`);
3028
3743
  }
3029
3744
  }
3745
+ for (const [event, groups] of Object.entries(config)) {
3746
+ if (!Array.isArray(groups)) {
3747
+ errors.push(`Event "${event}": value must be an array of hook groups`);
3748
+ continue;
3749
+ }
3750
+ groups.forEach((group, gi) => {
3751
+ if (!group || typeof group !== "object") {
3752
+ errors.push(`${event}[${gi}]: hook group must be an object`);
3753
+ return;
3754
+ }
3755
+ if (group.matcher !== undefined && typeof group.matcher !== "string") {
3756
+ warnings.push(`${event}[${gi}]: "matcher" should be a string (e.g. "Write|Edit" or glob)`);
3757
+ }
3758
+ const hooksArr = group.hooks;
3759
+ if (!Array.isArray(hooksArr)) {
3760
+ errors.push(`${event}[${gi}]: missing or invalid "hooks" array`);
3761
+ return;
3762
+ }
3763
+ hooksArr.forEach((h, hi) => {
3764
+ if (!h || typeof h !== "object" || !h.type) {
3765
+ errors.push(`${event}[${gi}].hooks[${hi}]: must have "type"`);
3766
+ return;
3767
+ }
3768
+ const t = String(h.type);
3769
+ if (!["command", "http", "mcp_tool", "prompt", "agent"].includes(t)) {
3770
+ warnings.push(`${event}[${gi}].hooks[${hi}]: unknown type "${t}" (valid: command, http, mcp_tool, prompt, agent)`);
3771
+ }
3772
+ if (t === "command" && !h.command) {
3773
+ errors.push(`${event}[${gi}].hooks[${hi}]: type=command requires "command"`);
3774
+ }
3775
+ if (t === "http" && !h.url) {
3776
+ errors.push(`${event}[${gi}].hooks[${hi}]: type=http requires "url"`);
3777
+ }
3778
+ if (h.command && typeof h.command === "string" && /\$\{CLAUDE_/.test(h.command)) {
3779
+ passes.push(`${event}[${gi}].hooks[${hi}]: uses plugin env substitution`);
3780
+ }
3781
+ });
3782
+ if (hooksArr.length > 0) {
3783
+ passes.push(`Event "${event}" has ${hooksArr.length} hook action(s)`);
3784
+ }
3785
+ });
3786
+ }
3030
3787
  return { errors, warnings, passes };
3031
3788
  }
3032
3789
  };
3033
3790
  });
3034
3791
 
3035
3792
  // src/validators/claude/mcp.ts
3036
- import { existsSync as existsSync14 } from "fs";
3037
- import { resolve as resolve7 } from "path";
3793
+ import { existsSync as existsSync17 } from "fs";
3794
+ import { resolve as resolve8 } from "path";
3038
3795
  var claudeMcpValidator;
3039
3796
  var init_mcp = __esm(() => {
3040
3797
  claudeMcpValidator = {
3041
3798
  id: "claude:mcp",
3042
3799
  provider: "claude",
3043
3800
  name: "Claude MCP Config",
3044
- description: "Validates .mcp.json: server definitions, required fields, path portability",
3801
+ 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",
3045
3802
  detect(dir) {
3046
- return existsSync14(resolve7(dir, ".mcp.json"));
3803
+ return existsSync17(resolve8(dir, ".mcp.json"));
3047
3804
  },
3048
3805
  async validate(dir, _opts) {
3049
3806
  const errors = [];
3050
3807
  const warnings = [];
3051
3808
  const passes = [];
3052
- const mcpPath = resolve7(dir, ".mcp.json");
3809
+ const mcpPath = resolve8(dir, ".mcp.json");
3053
3810
  let config;
3054
3811
  try {
3055
3812
  const raw = await Bun.file(mcpPath).text();
@@ -3069,14 +3826,42 @@ var init_mcp = __esm(() => {
3069
3826
  return { errors, warnings, passes };
3070
3827
  }
3071
3828
  passes.push(`${serverNames.length} server(s) defined`);
3829
+ for (const [name, entry] of Object.entries(config)) {
3830
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
3831
+ errors.push(`mcp server "${name}": definition must be an object`);
3832
+ continue;
3833
+ }
3834
+ const e = entry;
3835
+ const hasCommand = typeof e.command === "string";
3836
+ const hasUrl = typeof e.url === "string";
3837
+ if (!hasCommand && !hasUrl) {
3838
+ errors.push(`mcp server "${name}": must have either "command" (for stdio) or "url" (for SSE/HTTP)`);
3839
+ }
3840
+ if (hasCommand && !Array.isArray(e.args)) {
3841
+ warnings.push(`mcp server "${name}": "command" present but no "args" array (ok for some servers)`);
3842
+ }
3843
+ if (hasUrl && hasCommand) {
3844
+ warnings.push(`mcp server "${name}": both "command" and "url" present \u2014 usually one or the other`);
3845
+ }
3846
+ if (e.env && typeof e.env === "object") {
3847
+ passes.push(`mcp server "${name}": has env`);
3848
+ }
3849
+ if (typeof e.cwd === "string") {
3850
+ passes.push(`mcp server "${name}": has cwd`);
3851
+ }
3852
+ const hasSubs = JSON.stringify(e).match(/\$\{CLAUDE_PLUGIN_(ROOT|DATA)|CLAUDE_PROJECT_DIR|user_config\.|ENV_VAR\}/);
3853
+ if (hasSubs) {
3854
+ passes.push(`mcp server "${name}": uses \${CLAUDE_PLUGIN_*} / user_config / env substitution`);
3855
+ }
3856
+ }
3072
3857
  return { errors, warnings, passes };
3073
3858
  }
3074
3859
  };
3075
3860
  });
3076
3861
 
3077
3862
  // src/validators/claude/subagent.ts
3078
- import { existsSync as existsSync15, readdirSync as readdirSync6 } from "fs";
3079
- import { resolve as resolve8, join as join11 } from "path";
3863
+ import { existsSync as existsSync18, readdirSync as readdirSync8 } from "fs";
3864
+ import { resolve as resolve9, join as join14 } from "path";
3080
3865
  var claudeSubagentValidator;
3081
3866
  var init_subagent = __esm(() => {
3082
3867
  init_frontmatter();
@@ -3084,13 +3869,13 @@ var init_subagent = __esm(() => {
3084
3869
  id: "claude:subagent",
3085
3870
  provider: "claude",
3086
3871
  name: "Claude Subagents",
3087
- description: "Validates agents/ directory: .md files with frontmatter and description",
3872
+ 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",
3088
3873
  detect(dir) {
3089
- const agentsDir = resolve8(dir, "agents");
3090
- if (!existsSync15(agentsDir))
3874
+ const agentsDir = resolve9(dir, "agents");
3875
+ if (!existsSync18(agentsDir))
3091
3876
  return false;
3092
3877
  try {
3093
- return readdirSync6(agentsDir).some((f) => f.endsWith(".md"));
3878
+ return readdirSync8(agentsDir).some((f) => f.endsWith(".md"));
3094
3879
  } catch {
3095
3880
  return false;
3096
3881
  }
@@ -3099,27 +3884,63 @@ var init_subagent = __esm(() => {
3099
3884
  const errors = [];
3100
3885
  const warnings = [];
3101
3886
  const passes = [];
3102
- const agentsDir = resolve8(dir, "agents");
3103
- const mdFiles = readdirSync6(agentsDir).filter((f) => f.endsWith(".md"));
3887
+ const agentsDir = resolve9(dir, "agents");
3888
+ const mdFiles = readdirSync8(agentsDir).filter((f) => f.endsWith(".md"));
3104
3889
  if (mdFiles.length === 0) {
3105
3890
  errors.push("agents/ directory has no .md files");
3106
3891
  return { errors, warnings, passes };
3107
3892
  }
3108
3893
  passes.push(`${mdFiles.length} agent definition(s) found`);
3894
+ const SUPPORTED = new Set([
3895
+ "name",
3896
+ "description",
3897
+ "model",
3898
+ "effort",
3899
+ "maxTurns",
3900
+ "tools",
3901
+ "disallowedTools",
3902
+ "skills",
3903
+ "memory",
3904
+ "background",
3905
+ "isolation"
3906
+ ]);
3907
+ const DISALLOWED = new Set(["hooks", "mcpServers", "permissionMode"]);
3109
3908
  for (const file of mdFiles) {
3110
- const filePath = join11(agentsDir, file);
3909
+ const filePath = join14(agentsDir, file);
3111
3910
  const raw = await Bun.file(filePath).text();
3112
3911
  try {
3113
3912
  const parsed = parseFrontmatter(raw);
3114
- if (Object.keys(parsed.data).length === 0) {
3115
- warnings.push(`${file}: no YAML frontmatter`);
3116
- } else if (!parsed.data.description) {
3117
- warnings.push(`${file}: missing "description" in frontmatter`);
3913
+ const fm = parsed.data;
3914
+ if (Object.keys(fm).length === 0) {
3915
+ warnings.push(`${file}: no YAML frontmatter (description recommended so Claude knows when to invoke)`);
3118
3916
  } else {
3119
- passes.push(`${file}: has frontmatter with description`);
3917
+ if (fm.description) {
3918
+ passes.push(`${file}: has frontmatter with description`);
3919
+ } else {
3920
+ warnings.push(`${file}: missing "description" in frontmatter`);
3921
+ }
3922
+ const usedSupported = [];
3923
+ Object.keys(fm).forEach((k) => {
3924
+ if (SUPPORTED.has(k))
3925
+ usedSupported.push(k);
3926
+ if (DISALLOWED.has(k)) {
3927
+ errors.push(`${file}: frontmatter "${k}" is not supported for plugin-shipped agents (security restriction)`);
3928
+ }
3929
+ });
3930
+ if (usedSupported.length) {
3931
+ passes.push(`${file}: frontmatter fields: ${usedSupported.join(", ")}`);
3932
+ }
3933
+ if (fm.isolation !== undefined && fm.isolation !== "worktree") {
3934
+ errors.push(`${file}: "isolation" must be "worktree" if present (only supported value for plugin agents)`);
3935
+ }
3936
+ if (fm.name && typeof fm.name === "string") {
3937
+ passes.push(`${file}: name: "${fm.name}"`);
3938
+ }
3120
3939
  }
3121
3940
  if (!parsed.content.trim()) {
3122
3941
  errors.push(`${file}: body is empty`);
3942
+ } else {
3943
+ passes.push(`${file}: has agent system prompt body`);
3123
3944
  }
3124
3945
  } catch {
3125
3946
  errors.push(`${file}: failed to parse`);
@@ -3131,8 +3952,8 @@ var init_subagent = __esm(() => {
3131
3952
  });
3132
3953
 
3133
3954
  // src/validators/claude/command.ts
3134
- import { existsSync as existsSync16, readdirSync as readdirSync7 } from "fs";
3135
- import { resolve as resolve9, join as join12 } from "path";
3955
+ import { existsSync as existsSync19, readdirSync as readdirSync9 } from "fs";
3956
+ import { resolve as resolve10, join as join15 } from "path";
3136
3957
  var claudeCommandValidator;
3137
3958
  var init_command = __esm(() => {
3138
3959
  init_frontmatter();
@@ -3142,11 +3963,11 @@ var init_command = __esm(() => {
3142
3963
  name: "Claude Commands",
3143
3964
  description: "Validates commands/ (or legacy .claude/commands/) .md files: frontmatter (including rich skill fields), description, body",
3144
3965
  detect(dir) {
3145
- const commandsDir = resolve9(dir, "commands");
3146
- if (!existsSync16(commandsDir))
3966
+ const commandsDir = resolve10(dir, "commands");
3967
+ if (!existsSync19(commandsDir))
3147
3968
  return false;
3148
3969
  try {
3149
- return readdirSync7(commandsDir).some((f) => f.endsWith(".md"));
3970
+ return readdirSync9(commandsDir).some((f) => f.endsWith(".md"));
3150
3971
  } catch {
3151
3972
  return false;
3152
3973
  }
@@ -3155,15 +3976,15 @@ var init_command = __esm(() => {
3155
3976
  const errors = [];
3156
3977
  const warnings = [];
3157
3978
  const passes = [];
3158
- const commandsDir = resolve9(dir, "commands");
3159
- const mdFiles = readdirSync7(commandsDir).filter((f) => f.endsWith(".md"));
3979
+ const commandsDir = resolve10(dir, "commands");
3980
+ const mdFiles = readdirSync9(commandsDir).filter((f) => f.endsWith(".md"));
3160
3981
  if (mdFiles.length === 0) {
3161
3982
  errors.push("commands/ directory has no .md files");
3162
3983
  return { errors, warnings, passes };
3163
3984
  }
3164
3985
  passes.push(`${mdFiles.length} command definition(s) found`);
3165
3986
  for (const file of mdFiles) {
3166
- const filePath = join12(commandsDir, file);
3987
+ const filePath = join15(commandsDir, file);
3167
3988
  const raw = await Bun.file(filePath).text();
3168
3989
  try {
3169
3990
  const parsed = parseFrontmatter(raw);
@@ -3192,8 +4013,8 @@ var init_command = __esm(() => {
3192
4013
  });
3193
4014
 
3194
4015
  // src/validators/claude/memory.ts
3195
- import { existsSync as existsSync17 } from "fs";
3196
- import { resolve as resolve10 } from "path";
4016
+ import { existsSync as existsSync20 } from "fs";
4017
+ import { resolve as resolve11 } from "path";
3197
4018
  var claudeMemoryValidator;
3198
4019
  var init_memory = __esm(() => {
3199
4020
  claudeMemoryValidator = {
@@ -3202,13 +4023,13 @@ var init_memory = __esm(() => {
3202
4023
  name: "Claude CLAUDE.md",
3203
4024
  description: "Validates CLAUDE.md: non-empty, length recommendations, @path imports",
3204
4025
  detect(dir) {
3205
- return existsSync17(resolve10(dir, "CLAUDE.md"));
4026
+ return existsSync20(resolve11(dir, "CLAUDE.md"));
3206
4027
  },
3207
4028
  async validate(dir, _opts) {
3208
4029
  const errors = [];
3209
4030
  const warnings = [];
3210
4031
  const passes = [];
3211
- const filePath = resolve10(dir, "CLAUDE.md");
4032
+ const filePath = resolve11(dir, "CLAUDE.md");
3212
4033
  const raw = await Bun.file(filePath).text();
3213
4034
  if (!raw.trim()) {
3214
4035
  errors.push("CLAUDE.md is empty");
@@ -3226,8 +4047,8 @@ var init_memory = __esm(() => {
3226
4047
  let match;
3227
4048
  while ((match = importRegex.exec(raw)) !== null) {
3228
4049
  const importPath = match[1];
3229
- const resolvedImport = resolve10(dir, importPath);
3230
- if (existsSync17(resolvedImport)) {
4050
+ const resolvedImport = resolve11(dir, importPath);
4051
+ if (existsSync20(resolvedImport)) {
3231
4052
  passes.push(`@import "${importPath}" exists`);
3232
4053
  } else {
3233
4054
  warnings.push(`@import "${importPath}" \u2014 file not found at ${resolvedImport}`);
@@ -3238,6 +4059,165 @@ var init_memory = __esm(() => {
3238
4059
  };
3239
4060
  });
3240
4061
 
4062
+ // src/validators/claude/lsp.ts
4063
+ import { existsSync as existsSync21 } from "fs";
4064
+ import { resolve as resolve12 } from "path";
4065
+ var claudeLspValidator;
4066
+ var init_lsp = __esm(() => {
4067
+ claudeLspValidator = {
4068
+ id: "claude:lsp",
4069
+ provider: "claude",
4070
+ name: "Claude LSP Servers",
4071
+ description: "Validates .lsp.json (or plugin.json lspServers): language server configs with required command + extensionToLanguage; optional transport, env, settings, diagnostics etc. (binaries installed separately)",
4072
+ detect(dir) {
4073
+ return existsSync21(resolve12(dir, ".lsp.json")) || existsSync21(resolve12(dir, ".claude-plugin", "plugin.json"));
4074
+ },
4075
+ async validate(dir, _opts) {
4076
+ const errors = [];
4077
+ const warnings = [];
4078
+ const passes = [];
4079
+ let cfg = null;
4080
+ const lspPath = resolve12(dir, ".lsp.json");
4081
+ if (existsSync21(lspPath)) {
4082
+ try {
4083
+ cfg = JSON.parse(await Bun.file(lspPath).text());
4084
+ passes.push(".lsp.json is valid JSON");
4085
+ } catch {
4086
+ errors.push(".lsp.json is invalid JSON");
4087
+ return { errors, warnings, passes };
4088
+ }
4089
+ } else {
4090
+ const manifestPath = resolve12(dir, ".claude-plugin", "plugin.json");
4091
+ if (existsSync21(manifestPath)) {
4092
+ try {
4093
+ const m = JSON.parse(await Bun.file(manifestPath).text());
4094
+ if (m && m.lspServers && typeof m.lspServers === "object") {
4095
+ cfg = m.lspServers;
4096
+ passes.push("lspServers present inline in plugin.json");
4097
+ }
4098
+ } catch {}
4099
+ }
4100
+ }
4101
+ if (!cfg) {
4102
+ if (!existsSync21(lspPath)) {
4103
+ return { errors, warnings, passes };
4104
+ }
4105
+ }
4106
+ if (cfg && typeof cfg === "object") {
4107
+ const langs = Object.keys(cfg);
4108
+ passes.push(`${langs.length} language server(s) configured`);
4109
+ for (const lang of langs) {
4110
+ const entry = cfg[lang];
4111
+ if (!entry || !entry.command) {
4112
+ errors.push(`lsp "${lang}": "command" (the LSP binary) is required`);
4113
+ }
4114
+ if (!entry.extensionToLanguage || typeof entry.extensionToLanguage !== "object") {
4115
+ errors.push(`lsp "${lang}": "extensionToLanguage" map is required (e.g. { ".ts": "typescript" })`);
4116
+ } else {
4117
+ passes.push(`lsp "${lang}": has extensionToLanguage mapping`);
4118
+ }
4119
+ if (entry.diagnostics === false) {
4120
+ passes.push(`lsp "${lang}": diagnostics disabled (navigation only)`);
4121
+ }
4122
+ }
4123
+ }
4124
+ 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".');
4125
+ return { errors, warnings, passes };
4126
+ }
4127
+ };
4128
+ });
4129
+
4130
+ // src/validators/claude/monitors.ts
4131
+ import { existsSync as existsSync22 } from "fs";
4132
+ import { resolve as resolve13 } from "path";
4133
+ var claudeMonitorsValidator;
4134
+ var init_monitors = __esm(() => {
4135
+ claudeMonitorsValidator = {
4136
+ id: "claude:monitors",
4137
+ provider: "claude",
4138
+ name: "Claude Monitors (experimental)",
4139
+ 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.",
4140
+ detect(dir) {
4141
+ return existsSync22(resolve13(dir, "monitors", "monitors.json")) || existsSync22(resolve13(dir, "monitors.json")) || existsSync22(resolve13(dir, ".claude-plugin", "plugin.json"));
4142
+ },
4143
+ async validate(dir, _opts) {
4144
+ const errors = [];
4145
+ const warnings = [];
4146
+ const passes = [];
4147
+ let arr = null;
4148
+ const candidates = [
4149
+ resolve13(dir, "monitors", "monitors.json"),
4150
+ resolve13(dir, "monitors.json")
4151
+ ];
4152
+ for (const p of candidates) {
4153
+ if (existsSync22(p)) {
4154
+ try {
4155
+ const parsed = JSON.parse(await Bun.file(p).text());
4156
+ if (Array.isArray(parsed)) {
4157
+ arr = parsed;
4158
+ passes.push("monitors config is valid JSON array");
4159
+ }
4160
+ break;
4161
+ } catch {
4162
+ errors.push("monitors config is invalid JSON");
4163
+ return { errors, warnings, passes };
4164
+ }
4165
+ }
4166
+ }
4167
+ if (!arr) {
4168
+ const mp = resolve13(dir, ".claude-plugin", "plugin.json");
4169
+ if (existsSync22(mp)) {
4170
+ try {
4171
+ const m = JSON.parse(await Bun.file(mp).text());
4172
+ const exp = m?.experimental;
4173
+ const inline = typeof exp === "string" ? null : exp?.monitors;
4174
+ if (Array.isArray(inline))
4175
+ arr = inline;
4176
+ else if (typeof inline === "string") {
4177
+ passes.push("experimental.monitors declared as path in manifest (content not validated here)");
4178
+ }
4179
+ } catch {}
4180
+ }
4181
+ }
4182
+ if (!arr) {
4183
+ return { errors, warnings, passes };
4184
+ }
4185
+ if (!Array.isArray(arr)) {
4186
+ errors.push("monitors config must be a JSON array");
4187
+ return { errors, warnings, passes };
4188
+ }
4189
+ const seen = new Set;
4190
+ arr.forEach((mon, i) => {
4191
+ if (!mon || typeof mon !== "object") {
4192
+ errors.push(`monitors[${i}]: entry must be an object`);
4193
+ return;
4194
+ }
4195
+ if (!mon.name || typeof mon.name !== "string") {
4196
+ errors.push(`monitors[${i}]: "name" (unique id) is required`);
4197
+ } else {
4198
+ if (seen.has(mon.name))
4199
+ errors.push(`monitors: duplicate name "${mon.name}"`);
4200
+ seen.add(mon.name);
4201
+ }
4202
+ if (!mon.command || typeof mon.command !== "string") {
4203
+ errors.push(`monitors[${i}]: "command" (shell command) is required`);
4204
+ } else if (/\$\{CLAUDE_/.test(mon.command)) {
4205
+ passes.push(`monitors[${i}] "${mon.name || i}": uses CLAUDE_PLUGIN_* substitution`);
4206
+ }
4207
+ if (!mon.description) {
4208
+ warnings.push(`monitors[${i}]: "description" recommended (shown in task panel)`);
4209
+ }
4210
+ if (mon.when && !/^always$|^on-skill-invoke:/.test(String(mon.when))) {
4211
+ warnings.push(`monitors[${i}]: "when" should be "always" (default) or "on-skill-invoke:<skill>"`);
4212
+ }
4213
+ });
4214
+ passes.push(`${arr.length} monitor(s) declared`);
4215
+ 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.");
4216
+ return { errors, warnings, passes };
4217
+ }
4218
+ };
4219
+ });
4220
+
3241
4221
  // src/validators/index.ts
3242
4222
  function resolveFor(forFlag, allValidators = validators) {
3243
4223
  if (!forFlag) {
@@ -3272,6 +4252,8 @@ var init_validators = __esm(() => {
3272
4252
  init_subagent();
3273
4253
  init_command();
3274
4254
  init_memory();
4255
+ init_lsp();
4256
+ init_monitors();
3275
4257
  validators = [
3276
4258
  claudeSkillValidator,
3277
4259
  claudePluginValidator,
@@ -3280,14 +4262,16 @@ var init_validators = __esm(() => {
3280
4262
  claudeMcpValidator,
3281
4263
  claudeSubagentValidator,
3282
4264
  claudeCommandValidator,
3283
- claudeMemoryValidator
4265
+ claudeMemoryValidator,
4266
+ claudeLspValidator,
4267
+ claudeMonitorsValidator
3284
4268
  ];
3285
4269
  });
3286
4270
 
3287
4271
  // src/core/remote.ts
3288
4272
  import { spawnSync as spawnSync4 } from "child_process";
3289
4273
  import { mkdtempSync, rmSync } from "fs";
3290
- import { join as join13 } from "path";
4274
+ import { join as join16 } from "path";
3291
4275
  import { tmpdir } from "os";
3292
4276
  function parseRemoteUrl(input) {
3293
4277
  if (input.startsWith(".") || input.startsWith("/") || input.startsWith("~")) {
@@ -3324,7 +4308,7 @@ function isGhAvailable() {
3324
4308
  return ghAvailable;
3325
4309
  }
3326
4310
  async function cloneToTemp(parsed) {
3327
- const tmpDir = mkdtempSync(join13(tmpdir(), "dora-"));
4311
+ const tmpDir = mkdtempSync(join16(tmpdir(), "dora-"));
3328
4312
  const cleanup = () => {
3329
4313
  try {
3330
4314
  rmSync(tmpDir, { recursive: true, force: true });
@@ -3372,15 +4356,15 @@ var exports_validate_top = {};
3372
4356
  __export(exports_validate_top, {
3373
4357
  default: () => validate_top_default
3374
4358
  });
3375
- import { existsSync as existsSync19 } from "fs";
3376
- import { resolve as resolve11 } from "path";
3377
- var import_picocolors11, validate_top_default;
4359
+ import { existsSync as existsSync24 } from "fs";
4360
+ import { resolve as resolve14 } from "path";
4361
+ var import_picocolors13, validate_top_default;
3378
4362
  var init_validate_top = __esm(() => {
3379
4363
  init_dist();
3380
4364
  init_out();
3381
4365
  init_validators();
3382
4366
  init_remote();
3383
- import_picocolors11 = __toESM(require_picocolors(), 1);
4367
+ import_picocolors13 = __toESM(require_picocolors(), 1);
3384
4368
  validate_top_default = defineCommand({
3385
4369
  meta: {
3386
4370
  name: "validate",
@@ -3420,24 +4404,24 @@ var init_validate_top = __esm(() => {
3420
4404
  let cleanup;
3421
4405
  if (remote) {
3422
4406
  ui.info(`
3423
- Cloning ${import_picocolors11.default.dim(args.path)}...`);
4407
+ Cloning ${import_picocolors13.default.dim(args.path)}...`);
3424
4408
  try {
3425
4409
  const result = await cloneToTemp(remote);
3426
- fullPath = remote.subpath ? resolve11(result.dir, remote.subpath) : result.dir;
4410
+ fullPath = remote.subpath ? resolve14(result.dir, remote.subpath) : result.dir;
3427
4411
  cleanup = result.cleanup;
3428
4412
  } catch (err) {
3429
4413
  const msg = err instanceof Error ? err.message : String(err);
3430
4414
  ui.fail(msg);
3431
4415
  process.exit(1);
3432
4416
  }
3433
- if (!existsSync19(fullPath)) {
4417
+ if (!existsSync24(fullPath)) {
3434
4418
  cleanup();
3435
4419
  ui.fail(`Subdirectory not found in repo: ${remote.subpath}`);
3436
4420
  process.exit(1);
3437
4421
  }
3438
4422
  } else {
3439
- fullPath = resolve11(args.path);
3440
- if (!existsSync19(fullPath)) {
4423
+ fullPath = resolve14(args.path);
4424
+ if (!existsSync24(fullPath)) {
3441
4425
  ui.fail(`Path not found: ${args.path}
3442
4426
 
3443
4427
  Check that the path is correct and the directory exists.`);
@@ -3468,13 +4452,13 @@ Check that the path is correct and the directory exists.`);
3468
4452
  ` + `Available providers:
3469
4453
  ` + providers.map((p) => {
3470
4454
  const pvs = validators.filter((v) => v.provider === p);
3471
- return ` ${import_picocolors11.default.bold(p)}
3472
- ` + pvs.map((v) => ` \u2022 ${import_picocolors11.default.dim(v.id)} \u2014 ${v.description}`).join(`
4455
+ return ` ${import_picocolors13.default.bold(p)}
4456
+ ` + pvs.map((v) => ` \u2022 ${import_picocolors13.default.dim(v.id)} \u2014 ${v.description}`).join(`
3473
4457
  `);
3474
4458
  }).join(`
3475
4459
  `) + `
3476
4460
 
3477
- Use ${import_picocolors11.default.dim("--for <provider>")} or ${import_picocolors11.default.dim("--for <provider:type>")} to target explicitly.`);
4461
+ Use ${import_picocolors13.default.dim("--for <provider>")} or ${import_picocolors13.default.dim("--for <provider:type>")} to target explicitly.`);
3478
4462
  process.exit(1);
3479
4463
  }
3480
4464
  const allResults = [];
@@ -3495,7 +4479,7 @@ Use ${import_picocolors11.default.dim("--for <provider>")} or ${import_picocolor
3495
4479
  } else {
3496
4480
  for (const { id, name, result } of allResults) {
3497
4481
  ui.write(`
3498
- ${import_picocolors11.default.bold("dora validate")} \u2014 ${import_picocolors11.default.white(name)} ${import_picocolors11.default.dim(`(${id})`)}
4482
+ ${import_picocolors13.default.bold("dora validate")} \u2014 ${import_picocolors13.default.white(name)} ${import_picocolors13.default.dim(`(${id})`)}
3499
4483
  `);
3500
4484
  ui.info(` Path: ${args.path}
3501
4485
  `);
@@ -3510,7 +4494,7 @@ Use ${import_picocolors11.default.dim("--for <provider>")} or ${import_picocolor
3510
4494
  }
3511
4495
  if (result.errors.length === 0 && result.warnings.length === 0) {
3512
4496
  ui.write(`
3513
- ${import_picocolors11.default.green("\u2713")} ${import_picocolors11.default.white("All checks passed.")}
4497
+ ${import_picocolors13.default.green("\u2713")} ${import_picocolors13.default.white("All checks passed.")}
3514
4498
  `);
3515
4499
  } else {
3516
4500
  ui.info(`
@@ -3532,16 +4516,16 @@ var exports_init2 = {};
3532
4516
  __export(exports_init2, {
3533
4517
  default: () => init_default2
3534
4518
  });
3535
- import { basename as basename2, join as join14 } from "path";
4519
+ import { basename as basename4, join as join17 } from "path";
3536
4520
  var {spawnSync: spawnSync5 } = globalThis.Bun;
3537
- var import_picocolors12, init_default2;
4521
+ var import_picocolors14, init_default2;
3538
4522
  var init_init2 = __esm(() => {
3539
4523
  init_dist();
3540
4524
  init_out();
3541
4525
  init_journal_config();
3542
4526
  init_journal_remote();
3543
4527
  init_prompt();
3544
- import_picocolors12 = __toESM(require_picocolors(), 1);
4528
+ import_picocolors14 = __toESM(require_picocolors(), 1);
3545
4529
  init_default2 = defineCommand({
3546
4530
  meta: {
3547
4531
  name: "init",
@@ -3568,17 +4552,17 @@ var init_init2 = __esm(() => {
3568
4552
  ui.heading("dora init \u2014 Set up doraval, your journal, and the coding agent dora should use on the fly");
3569
4553
  const ghCheck = ensureGhCli();
3570
4554
  if (!ghCheck.ok) {
3571
- ui.write(` ${import_picocolors12.default.red("\u2717")} ${import_picocolors12.default.white("The GitHub CLI (")}${import_picocolors12.default.bold("gh")}${import_picocolors12.default.white(") is not installed.")}
4555
+ 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.")}
3572
4556
  `);
3573
- ui.info(` doraval uses ${import_picocolors12.default.bold("gh")} to fetch and sync journal files with GitHub.
4557
+ ui.info(` doraval uses ${import_picocolors14.default.bold("gh")} to fetch and sync journal files with GitHub.
3574
4558
  `);
3575
4559
  ui.info(` Install it:
3576
4560
  `);
3577
- ui.info(` macOS: ${import_picocolors12.default.dim("brew install gh")}`);
3578
- ui.info(` Linux: ${import_picocolors12.default.dim("https://github.com/cli/cli/blob/trunk/docs/install_linux.md")}`);
3579
- ui.info(` Windows: ${import_picocolors12.default.dim("winget install --id GitHub.cli")}
4561
+ ui.info(` macOS: ${import_picocolors14.default.dim("brew install gh")}`);
4562
+ ui.info(` Linux: ${import_picocolors14.default.dim("https://github.com/cli/cli/blob/trunk/docs/install_linux.md")}`);
4563
+ ui.info(` Windows: ${import_picocolors14.default.dim("winget install --id GitHub.cli")}
3580
4564
  `);
3581
- ui.info(` Then authenticate: ${import_picocolors12.default.dim("gh auth login")}
4565
+ ui.info(` Then authenticate: ${import_picocolors14.default.dim("gh auth login")}
3582
4566
  `);
3583
4567
  process.exit(1);
3584
4568
  }
@@ -3591,44 +4575,44 @@ var init_init2 = __esm(() => {
3591
4575
  if (gitOwner) {
3592
4576
  defaultRepo = `${gitOwner}/${gitOwner}.md`;
3593
4577
  if (ghLogin && ghLogin !== gitOwner) {
3594
- sourceNote = ` ${import_picocolors12.default.dim("(from git remote; your active gh account is " + ghLogin + ")")}
4578
+ sourceNote = ` ${import_picocolors14.default.dim("(from git remote; your active gh account is " + ghLogin + ")")}
3595
4579
  `;
3596
4580
  } else {
3597
- sourceNote = ` ${import_picocolors12.default.dim("(from git remote)")}
4581
+ sourceNote = ` ${import_picocolors14.default.dim("(from git remote)")}
3598
4582
  `;
3599
4583
  }
3600
4584
  } else if (ghLogin) {
3601
4585
  defaultRepo = `${ghLogin}/${ghLogin}.md`;
3602
- sourceNote = ` ${import_picocolors12.default.dim("(from your active gh account)")}
4586
+ sourceNote = ` ${import_picocolors14.default.dim("(from your active gh account)")}
3603
4587
  `;
3604
4588
  } else {
3605
- ui.warn(`Not logged in to GitHub. Run ${import_picocolors12.default.dim("gh auth login")} first.
4589
+ ui.warn(`Not logged in to GitHub. Run ${import_picocolors14.default.dim("gh auth login")} first.
3606
4590
  `);
3607
4591
  process.exit(1);
3608
4592
  }
3609
4593
  const existingConfig = await readConfig();
3610
4594
  if (existingConfig?.journal.repo) {
3611
4595
  defaultRepo = existingConfig.journal.repo;
3612
- sourceNote = ` ${import_picocolors12.default.dim("(from your previous journal setup)")}
4596
+ sourceNote = ` ${import_picocolors14.default.dim("(from your previous journal setup)")}
3613
4597
  `;
3614
4598
  }
3615
- ui.info(` Journal repo ${import_picocolors12.default.dim("(owner/name)")}`);
4599
+ ui.info(` Journal repo ${import_picocolors14.default.dim("(owner/name)")}`);
3616
4600
  if (sourceNote)
3617
4601
  ui.write(sourceNote);
3618
4602
  repo = prompt(" >", defaultRepo);
3619
4603
  }
3620
4604
  let project = args.project || process.env.DORAVAL_PROJECT;
3621
4605
  if (!project) {
3622
- const defaultProject = basename2(process.cwd());
4606
+ const defaultProject = basename4(process.cwd());
3623
4607
  project = prompt(" Project name", defaultProject);
3624
4608
  }
3625
4609
  project = sanitizeProjectName(project);
3626
4610
  if (!repoExists(repo)) {
3627
- ui.write(` ${import_picocolors12.default.red("\u2717")} ${import_picocolors12.default.white("Repository")} ${import_picocolors12.default.bold(repo)} ${import_picocolors12.default.white("not found on GitHub.")}
4611
+ 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.")}
3628
4612
  `);
3629
4613
  ui.info(` Create it first:
3630
4614
  `);
3631
- ui.info(` ${import_picocolors12.default.dim(`gh repo create ${repo} --private --description "Personal journal for agent decisions"`)}
4615
+ ui.info(` ${import_picocolors14.default.dim(`gh repo create ${repo} --private --description "Personal journal for agent decisions"`)}
3632
4616
  `);
3633
4617
  process.exit(1);
3634
4618
  }
@@ -3636,16 +4620,16 @@ var init_init2 = __esm(() => {
3636
4620
  const alreadyRegistered = existing?.journal.projects[project];
3637
4621
  const isRefresh = alreadyRegistered && args.refresh;
3638
4622
  if (alreadyRegistered && !isRefresh) {
3639
- ui.write(` ${import_picocolors12.default.yellow("\u26A0")} ${import_picocolors12.default.white("Project")} ${import_picocolors12.default.bold(project)} ${import_picocolors12.default.white("is already registered.")}
4623
+ ui.write(` ${import_picocolors14.default.yellow("\u26A0")} ${import_picocolors14.default.white("Project")} ${import_picocolors14.default.bold(project)} ${import_picocolors14.default.white("is already registered.")}
3640
4624
  `);
3641
4625
  ui.info(` Repo: ${existing.journal.repo}
3642
4626
  `);
3643
- ui.info(` To refresh journal files, use ${import_picocolors12.default.dim("dora journal update")} (or ${import_picocolors12.default.dim("dora init --refresh")}).
4627
+ ui.info(` To refresh journal files, use ${import_picocolors14.default.dim("dora journal update")} (or ${import_picocolors14.default.dim("dora init --refresh")}).
3644
4628
  `);
3645
4629
  }
3646
4630
  const journalsDir = getJournalsDir();
3647
4631
  const remotePath = `projects/${project}.md`;
3648
- const localPath = join14(journalsDir, `${project}.md`);
4632
+ const localPath = join17(journalsDir, `${project}.md`);
3649
4633
  const effectiveRepo = isRefresh && !args.repo ? existing.journal.repo : repo;
3650
4634
  const config = existing ?? {
3651
4635
  journal: { repo: effectiveRepo, projects: {} }
@@ -3656,9 +4640,9 @@ var init_init2 = __esm(() => {
3656
4640
  local_path: localPath
3657
4641
  };
3658
4642
  ensureDoravalDirs();
3659
- ui.write(` ${import_picocolors12.default.dim(import_picocolors12.default.gray("Fetching journal files from"))} ${import_picocolors12.default.gray(effectiveRepo)}${import_picocolors12.default.dim(import_picocolors12.default.gray("..."))}
4643
+ 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("..."))}
3660
4644
  `);
3661
- const globalDest = join14(journalsDir, "global.md");
4645
+ const globalDest = join17(journalsDir, "global.md");
3662
4646
  const refreshGlobalRes = await refreshLocalJournalFile(effectiveRepo, "global.md", globalDest);
3663
4647
  let wroteGlobal;
3664
4648
  if (!refreshGlobalRes.ok) {
@@ -3675,7 +4659,7 @@ var init_init2 = __esm(() => {
3675
4659
  if (wroteGlobal) {
3676
4660
  ui.success("global.md");
3677
4661
  } else {
3678
- ui.write(` ${import_picocolors12.default.dim("\xB7")} global.md ${import_picocolors12.default.dim("(not found \u2014 will be created on first sync)")}`);
4662
+ ui.write(` ${import_picocolors14.default.dim("\xB7")} global.md ${import_picocolors14.default.dim("(not found \u2014 will be created on first sync)")}`);
3679
4663
  await Bun.write(globalDest, `# Global Journal
3680
4664
 
3681
4665
  Cross-project principles.
@@ -3697,7 +4681,7 @@ Cross-project principles.
3697
4681
  if (wroteProject) {
3698
4682
  ui.success(remotePath);
3699
4683
  } else {
3700
- ui.write(` ${import_picocolors12.default.dim("\xB7")} ${remotePath} ${import_picocolors12.default.dim("(not found \u2014 will be created on first sync)")}`);
4684
+ ui.write(` ${import_picocolors14.default.dim("\xB7")} ${remotePath} ${import_picocolors14.default.dim("(not found \u2014 will be created on first sync)")}`);
3701
4685
  await Bun.write(localPath, `# ${project} Journal
3702
4686
 
3703
4687
  Project-specific decisions.
@@ -3705,13 +4689,13 @@ Project-specific decisions.
3705
4689
  }
3706
4690
  await writeConfig(config);
3707
4691
  ui.write(`
3708
- ${import_picocolors12.default.green("\u2713")} ${import_picocolors12.default.white("Journal ready for project")} ${import_picocolors12.default.bold(import_picocolors12.default.white(project))}.
4692
+ ${import_picocolors14.default.green("\u2713")} ${import_picocolors14.default.white("Journal ready for project")} ${import_picocolors14.default.bold(import_picocolors14.default.white(project))}.
3709
4693
  `);
3710
4694
  const existingAgent = (await readConfig())?.agent;
3711
4695
  if (existingAgent?.command) {
3712
- ui.write(` ${import_picocolors12.default.bold(import_picocolors12.default.white("Coding agent (already configured)"))}
4696
+ ui.write(` ${import_picocolors14.default.bold(import_picocolors14.default.white("Coding agent (already configured)"))}
3713
4697
  `);
3714
- ui.write(` Current: ${import_picocolors12.default.dim(import_picocolors12.default.gray(existingAgent.command))} template: ${import_picocolors12.default.dim(import_picocolors12.default.gray(existingAgent.prompt_template || "(default)"))}
4698
+ 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)"))}
3715
4699
  `);
3716
4700
  const change = prompt(" Reconfigure / change the coding agent for on-the-fly enrichment? (y/N)", "n");
3717
4701
  if (!/^y/i.test(String(change))) {
@@ -3721,16 +4705,16 @@ Project-specific decisions.
3721
4705
  if (existingAgent)
3722
4706
  cfg.agent = existingAgent;
3723
4707
  await writeConfig(cfg);
3724
- ui.write(` ${import_picocolors12.default.green("\u2713")} ${import_picocolors12.default.white("Try:")} ${import_picocolors12.default.dim(import_picocolors12.default.gray('dora journal add "short decision"'))}
4708
+ 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"'))}
3725
4709
  `);
3726
4710
  process.exit(0);
3727
4711
  return;
3728
4712
  }
3729
4713
  ui.blank();
3730
4714
  } else {
3731
- ui.write(` ${import_picocolors12.default.bold(import_picocolors12.default.white("Coding agent for journal add"))}
4715
+ ui.write(` ${import_picocolors14.default.bold(import_picocolors14.default.white("Coding agent for journal add"))}
3732
4716
  `);
3733
- ui.info(` When configured, ${import_picocolors12.default.dim(import_picocolors12.default.gray('dora journal add ".."'))} will use your agent to enrich entries with tags and rationale automatically.
4717
+ 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.
3734
4718
  `);
3735
4719
  }
3736
4720
  const common = [
@@ -3749,7 +4733,7 @@ Project-specific decisions.
3749
4733
  }
3750
4734
  }
3751
4735
  let agentCmd = detected || "claude";
3752
- ui.write(` Detected / default agent command: ${import_picocolors12.default.dim(import_picocolors12.default.gray(agentCmd))}`);
4736
+ ui.write(` Detected / default agent command: ${import_picocolors14.default.dim(import_picocolors14.default.gray(agentCmd))}`);
3753
4737
  agentCmd = prompt(" Agent command (the binary you run for prompts)", agentCmd);
3754
4738
  let template = detected ? common.find((c) => c.name === detected)?.template || '-p "{{prompt}}" --output-format json' : '-p "{{prompt}}" --output-format json';
3755
4739
  ui.info(` Prompt template (use {{prompt}} placeholder):`);
@@ -3761,11 +4745,11 @@ Project-specific decisions.
3761
4745
  };
3762
4746
  await writeConfig(finalConfig);
3763
4747
  ui.write(`
3764
- ${import_picocolors12.default.green("\u2713")} ${import_picocolors12.default.white("Agent configured.")}
4748
+ ${import_picocolors14.default.green("\u2713")} ${import_picocolors14.default.white("Agent configured.")}
3765
4749
  `);
3766
- ui.info(` Re-run ${import_picocolors12.default.dim(import_picocolors12.default.gray("dora init"))} anytime to change it.
4750
+ ui.info(` Re-run ${import_picocolors14.default.dim(import_picocolors14.default.gray("dora init"))} anytime to change it.
3767
4751
  `);
3768
- ui.info(` Next: ${import_picocolors12.default.dim(import_picocolors12.default.gray('dora journal add ".."'))}, ${import_picocolors12.default.dim(import_picocolors12.default.gray("dora journal list"))}, or ${import_picocolors12.default.dim(import_picocolors12.default.gray("dora journal update"))}.
4752
+ 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"))}.
3769
4753
  `);
3770
4754
  process.exit(0);
3771
4755
  }
@@ -3777,7 +4761,7 @@ init_dist();
3777
4761
  // package.json
3778
4762
  var package_default = {
3779
4763
  name: "@hacksmith/doraval",
3780
- version: "0.2.21",
4764
+ version: "0.2.23",
3781
4765
  author: "Saif",
3782
4766
  repository: {
3783
4767
  type: "git",
@@ -3838,7 +4822,7 @@ var package_default = {
3838
4822
  };
3839
4823
 
3840
4824
  // src/cli/index.ts
3841
- var import_picocolors13 = __toESM(require_picocolors(), 1);
4825
+ var import_picocolors15 = __toESM(require_picocolors(), 1);
3842
4826
  var skill = defineCommand({
3843
4827
  meta: {
3844
4828
  name: "skill",
@@ -3875,12 +4859,26 @@ var claude = defineCommand({
3875
4859
  description: "Claude Code-specific commands (packaging, scaffolding, distribution)"
3876
4860
  },
3877
4861
  subCommands: {
3878
- new: () => Promise.resolve().then(() => (init_new(), exports_new)).then((m) => m.default)
4862
+ new: () => Promise.resolve().then(() => (init_new(), exports_new)).then((m) => m.default),
4863
+ bump: () => Promise.resolve().then(() => (init_bump(), exports_bump)).then((m) => m.default)
3879
4864
  },
3880
4865
  run() {
3881
4866
  showUsage(claude);
3882
4867
  }
3883
4868
  });
4869
+ var codex = defineCommand({
4870
+ meta: {
4871
+ name: "codex",
4872
+ description: "Codex (OpenAI)-specific commands (packaging, scaffolding, distribution)"
4873
+ },
4874
+ subCommands: {
4875
+ new: () => Promise.resolve().then(() => (init_new2(), exports_new2)).then((m) => m.default),
4876
+ bump: () => Promise.resolve().then(() => (init_bump(), exports_bump)).then((m) => m.default)
4877
+ },
4878
+ run() {
4879
+ showUsage(codex);
4880
+ }
4881
+ });
3884
4882
  var doraemonArt = `
3885
4883
  \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
3886
4884
  \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
@@ -3902,13 +4900,15 @@ var main = defineCommand({
3902
4900
  subCommands: {
3903
4901
  validate: () => Promise.resolve().then(() => (init_validate_top(), exports_validate_top)).then((m) => m.default),
3904
4902
  init: () => Promise.resolve().then(() => (init_init2(), exports_init2)).then((m) => m.default),
4903
+ bump: () => Promise.resolve().then(() => (init_bump(), exports_bump)).then((m) => m.default),
3905
4904
  skill: () => Promise.resolve(skill),
3906
4905
  journal: () => Promise.resolve(journal),
3907
- claude: () => Promise.resolve(claude)
4906
+ claude: () => Promise.resolve(claude),
4907
+ codex: () => Promise.resolve(codex)
3908
4908
  },
3909
4909
  run() {
3910
4910
  console.log(`
3911
- ` + import_picocolors13.default.blue(doraemonArt) + `
4911
+ ` + import_picocolors15.default.blue(doraemonArt) + `
3912
4912
  `);
3913
4913
  showUsage(main);
3914
4914
  }