@hacksmith/doraval 0.2.20 → 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 +9 -5
  2. package/bin/doraval.js +1356 -168
  3. package/package.json +1 -1
package/bin/doraval.js CHANGED
@@ -2588,9 +2588,683 @@ var init_sync = __esm(() => {
2588
2588
  });
2589
2589
  });
2590
2590
 
2591
+ // src/cli/commands/claude/context.ts
2592
+ import { existsSync as existsSync8, readdirSync as readdirSync3 } from "fs";
2593
+ import { join as join7 } from "path";
2594
+ function detectContext(cwd = process.cwd()) {
2595
+ const hasClaudeDir = existsSync8(join7(cwd, ".claude"));
2596
+ const hasPluginManifest = existsSync8(join7(cwd, ".claude-plugin", "plugin.json"));
2597
+ let looseSkillFiles = [];
2598
+ try {
2599
+ const files = readdirSync3(cwd);
2600
+ looseSkillFiles = files.filter((f) => {
2601
+ if (!f.endsWith(".md") || f.startsWith("."))
2602
+ return false;
2603
+ const lower = f.toLowerCase();
2604
+ if (lower === "readme.md" || lower === "changelog.md" || lower === "license.md" || lower.includes("contributing"))
2605
+ return false;
2606
+ return lower.includes("skill") || lower === "skill.md";
2607
+ });
2608
+ } catch {}
2609
+ const isEmpty = !hasClaudeDir && !hasPluginManifest && looseSkillFiles.length === 0;
2610
+ return {
2611
+ cwd,
2612
+ hasClaudeDir,
2613
+ hasPluginManifest,
2614
+ looseSkillFiles,
2615
+ isEmpty
2616
+ };
2617
+ }
2618
+ var init_context = () => {};
2619
+
2620
+ // src/cli/commands/claude/new.ts
2621
+ var exports_new = {};
2622
+ __export(exports_new, {
2623
+ scaffold: () => scaffold,
2624
+ default: () => new_default,
2625
+ decidePath: () => decidePath
2626
+ });
2627
+ import { join as join8, basename as basename2 } from "path";
2628
+ import { mkdirSync as mkdirSync2, writeFileSync, existsSync as existsSync9 } from "fs";
2629
+ function decidePath(ctx, intent, providedName) {
2630
+ const rawName = providedName || "";
2631
+ let decisionPath = "standalone";
2632
+ let targetDir = ctx.cwd;
2633
+ let shouldCreateDir = false;
2634
+ let migrateExisting = false;
2635
+ const useCurrentDirAsRoot = rawName === "." || rawName === basename2(ctx.cwd) || !rawName;
2636
+ if (intent === "distribute" || intent === "self-later" && ctx.looseSkillFiles.length > 0 && !ctx.hasClaudeDir) {
2637
+ decisionPath = "plugin";
2638
+ if (useCurrentDirAsRoot) {
2639
+ targetDir = ctx.cwd;
2640
+ shouldCreateDir = false;
2641
+ } else {
2642
+ targetDir = join8(ctx.cwd, rawName);
2643
+ shouldCreateDir = true;
2644
+ }
2645
+ migrateExisting = ctx.looseSkillFiles.length > 0;
2646
+ } else if (intent === "self-later" && !ctx.hasClaudeDir) {
2647
+ decisionPath = "plugin";
2648
+ if (useCurrentDirAsRoot) {
2649
+ targetDir = ctx.cwd;
2650
+ shouldCreateDir = false;
2651
+ } else {
2652
+ targetDir = join8(ctx.cwd, rawName);
2653
+ shouldCreateDir = true;
2654
+ }
2655
+ } else if (decisionPath === "standalone") {
2656
+ if (useCurrentDirAsRoot) {
2657
+ targetDir = ctx.cwd;
2658
+ shouldCreateDir = false;
2659
+ } else {
2660
+ targetDir = join8(ctx.cwd, rawName);
2661
+ shouldCreateDir = true;
2662
+ }
2663
+ }
2664
+ return { path: decisionPath, targetDir, shouldCreateDir, migrateExisting };
2665
+ }
2666
+ function scaffold(decision, ctx, migrateContent) {
2667
+ const { targetDir, path, shouldCreateDir } = decision;
2668
+ if (existsSync9(targetDir) && shouldCreateDir) {
2669
+ ui.fail("Target already exists");
2670
+ process.exit(1);
2671
+ }
2672
+ if (shouldCreateDir) {
2673
+ mkdirSync2(targetDir, { recursive: true });
2674
+ }
2675
+ if (path === "plugin") {
2676
+ const pluginName = basename2(targetDir);
2677
+ const pluginJson = {
2678
+ name: pluginName,
2679
+ description: "Scaffolded by doraval claude new",
2680
+ version: "0.1.0"
2681
+ };
2682
+ mkdirSync2(join8(targetDir, ".claude-plugin"), { recursive: true });
2683
+ writeFileSync(join8(targetDir, ".claude-plugin", "plugin.json"), JSON.stringify(pluginJson, null, 2));
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.
2704
+ ---
2705
+
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 + `
2724
+
2725
+ Claude Code plugin scaffolded by doraval.`);
2726
+ }
2727
+ } else {
2728
+ mkdirSync2(join8(targetDir, ".claude", "skills", "my-skill"), { recursive: true });
2729
+ const skillBody = migrateContent || `# My Skill
2730
+
2731
+ Basic starter.`;
2732
+ writeFileSync(join8(targetDir, ".claude", "skills", "my-skill", "SKILL.md"), `---
2733
+ name: my-skill
2734
+ description: Starter
2735
+ ---
2736
+
2737
+ ${skillBody}`);
2738
+ }
2739
+ }
2740
+ var import_picocolors10, new_default;
2741
+ var init_new = __esm(() => {
2742
+ init_dist();
2743
+ init_out();
2744
+ init_context();
2745
+ init_prompt();
2746
+ import_picocolors10 = __toESM(require_picocolors(), 1);
2747
+ new_default = defineCommand({
2748
+ meta: {
2749
+ name: "new",
2750
+ description: "Create a new skill or plugin following Claude Code packaging rules"
2751
+ },
2752
+ args: {
2753
+ name: {
2754
+ type: "positional",
2755
+ description: "Optional name for the skill or plugin",
2756
+ required: false
2757
+ },
2758
+ yes: {
2759
+ type: "boolean",
2760
+ description: "Skip interactive prompts (use defaults and flags)",
2761
+ default: false
2762
+ },
2763
+ intent: {
2764
+ type: "string",
2765
+ description: 'Intent: "self" | "self-later" | "distribute"',
2766
+ required: false
2767
+ }
2768
+ },
2769
+ run({ args }) {
2770
+ ui.heading("doraval claude new \u2014 Context-aware scaffolding");
2771
+ const ctx = detectContext();
2772
+ let intent = args.intent || "self-later";
2773
+ if (!args.yes) {
2774
+ const ans = prompt(" Intent (self | self-later | distribute)", intent);
2775
+ intent = ans || intent;
2776
+ }
2777
+ const decision = decidePath(ctx, intent, args.name);
2778
+ ui.info(` Decision: path=${decision.path}, target=${decision.targetDir}`);
2779
+ let migrateContent;
2780
+ if (decision.migrateExisting && !args.yes) {
2781
+ migrateContent = "Content from your existing SKILL.md (user-confirmed).";
2782
+ }
2783
+ scaffold(decision, ctx, migrateContent);
2784
+ ui.write(`
2785
+ ${import_picocolors10.default.green("\u2713")} Created ${decision.path} at ${import_picocolors10.default.bold(decision.targetDir)}`);
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
+ }
2792
+ ui.info(` Test: claude --plugin-dir ${decision.targetDir} (or use normally for standalone)`);
2793
+ ui.info(` Validate: doraval validate ${decision.targetDir}`);
2794
+ if (decision.path === "plugin" && decision.migrateExisting) {
2795
+ ui.info(" (Existing content migrated where confirmed.)");
2796
+ }
2797
+ process.exit(0);
2798
+ }
2799
+ });
2800
+ });
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
+
2591
3265
  // src/validators/claude/skill.ts
2592
- import { existsSync as existsSync8 } from "fs";
2593
- import { resolve as resolve3 } from "path";
3266
+ import { existsSync as existsSync13 } from "fs";
3267
+ import { resolve as resolve4 } from "path";
2594
3268
  var OPTIONAL_DIRS2, claudeSkillValidator;
2595
3269
  var init_skill = __esm(() => {
2596
3270
  init_frontmatter();
@@ -2602,10 +3276,10 @@ var init_skill = __esm(() => {
2602
3276
  name: "Claude Skill",
2603
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.)",
2604
3278
  detect(dir) {
2605
- return existsSync8(resolve3(dir, "SKILL.md"));
3279
+ return existsSync13(resolve4(dir, "SKILL.md"));
2606
3280
  },
2607
3281
  async validate(dir, _opts) {
2608
- const skillMd = resolve3(dir, "SKILL.md");
3282
+ const skillMd = resolve4(dir, "SKILL.md");
2609
3283
  const raw = await Bun.file(skillMd).text();
2610
3284
  let parsed;
2611
3285
  try {
@@ -2617,32 +3291,108 @@ var init_skill = __esm(() => {
2617
3291
  passes: []
2618
3292
  };
2619
3293
  }
2620
- const existingDirs = OPTIONAL_DIRS2.filter((d) => existsSync8(resolve3(dir, d)));
3294
+ const existingDirs = OPTIONAL_DIRS2.filter((d) => existsSync13(resolve4(dir, d)));
2621
3295
  return validateSkillModel(parsed, { existingDirs: [...existingDirs] });
2622
3296
  }
2623
3297
  };
2624
3298
  });
2625
3299
 
2626
3300
  // src/validators/claude/plugin.ts
2627
- import { existsSync as existsSync9, readdirSync as readdirSync3 } from "fs";
2628
- import { resolve as resolve4, join as join7 } from "path";
2629
- 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;
2630
3354
  var init_plugin = __esm(() => {
2631
3355
  NAME_REGEX2 = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
2632
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"]);
2633
3382
  claudePluginValidator = {
2634
3383
  id: "claude:plugin",
2635
3384
  provider: "claude",
2636
3385
  name: "Claude Plugin",
2637
- 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",
2638
3387
  detect(dir) {
2639
- return existsSync9(resolve4(dir, ".claude-plugin", "plugin.json"));
3388
+ return existsSync14(resolve5(dir, ".claude-plugin", "plugin.json"));
2640
3389
  },
2641
3390
  async validate(dir, _opts) {
2642
3391
  const errors = [];
2643
3392
  const warnings = [];
2644
3393
  const passes = [];
2645
- const manifestPath = resolve4(dir, ".claude-plugin", "plugin.json");
3394
+ const manifestPath = resolve5(dir, ".claude-plugin", "plugin.json");
3395
+ const dotClaudePluginDir = resolve5(dir, ".claude-plugin");
2646
3396
  let manifest;
2647
3397
  try {
2648
3398
  const raw = await Bun.file(manifestPath).text();
@@ -2652,6 +3402,17 @@ var init_plugin = __esm(() => {
2652
3402
  errors.push(".claude-plugin/plugin.json is missing or invalid JSON");
2653
3403
  return { errors, warnings, passes };
2654
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 {}
2655
3416
  if (!manifest.name) {
2656
3417
  errors.push('Missing required field: "name"');
2657
3418
  } else {
@@ -2665,10 +3426,12 @@ var init_plugin = __esm(() => {
2665
3426
  if (manifest.version !== undefined) {
2666
3427
  const v = String(manifest.version);
2667
3428
  if (!/^\d+\.\d+\.\d+/.test(v)) {
2668
- 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`);
2669
3430
  } else {
2670
- passes.push(`version: "${v}"`);
3431
+ passes.push(`version: "${v}" (explicit \u2014 bump on every release to publish updates)`);
2671
3432
  }
3433
+ } else {
3434
+ passes.push("version omitted (git commit SHA used as version key \u2014 every commit becomes an available update)");
2672
3435
  }
2673
3436
  if (manifest.description !== undefined) {
2674
3437
  const desc = String(manifest.description);
@@ -2677,65 +3440,172 @@ var init_plugin = __esm(() => {
2677
3440
  } else {
2678
3441
  passes.push("description field present");
2679
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
+ }
2680
3456
  }
2681
- const checkPaths = (field, value) => {
2682
- const paths = Array.isArray(value) ? value : [value];
2683
- for (const p of paths) {
2684
- const s = String(p);
2685
- if (!RELATIVE_PATH_REGEX.test(s)) {
2686
- errors.push(`${field}: path "${s}" must start with "./" (relative)`);
2687
- } else if (s.includes("..")) {
2688
- errors.push(`${field}: path "${s}" must not use ".." (no parent traversal)`);
2689
- } else if (existsSync9(resolve4(dir, s))) {
2690
- passes.push(`${field}: path "${s}" exists`);
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
+ }
3466
+ }
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`);
2691
3501
  } else {
2692
- warnings.push(`${field}: path "${s}" does not exist on disk`);
3502
+ passes.push(`${field}: custom path or config (merge rules apply)`);
2693
3503
  }
3504
+ } else if (typeof val === "object") {
3505
+ passes.push(`${field}: inline ${field} config present`);
2694
3506
  }
2695
3507
  };
2696
- for (const field of ["commands", "agents", "hooks", "mcpServers"]) {
2697
- if (manifest[field] !== undefined) {
2698
- checkPaths(field, manifest[field]);
2699
- }
2700
- }
2701
- const skillsDir = resolve4(dir, "skills");
2702
- if (existsSync9(skillsDir)) {
2703
- const skillEntries = readdirSync3(skillsDir, { withFileTypes: true }).filter((e) => e.isDirectory());
2704
- for (const skill of skillEntries) {
2705
- const skillMd = join7(skillsDir, skill.name, "SKILL.md");
2706
- if (existsSync9(skillMd)) {
2707
- 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`);
2708
3547
  } else {
2709
- errors.push(`skills/${skill.name}/ missing SKILL.md`);
3548
+ errors.push(`skills/${e.name}/ is missing SKILL.md`);
2710
3549
  }
2711
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
+ }
2712
3554
  }
2713
- const commandsDir = resolve4(dir, "commands");
2714
- if (existsSync9(commandsDir)) {
2715
- const mdFiles = readdirSync3(commandsDir).filter((f) => f.endsWith(".md"));
2716
- if (mdFiles.length > 0) {
2717
- passes.push(`commands/ has ${mdFiles.length} .md file(s)`);
2718
- } else {
2719
- 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)');
2720
3563
  }
2721
3564
  }
2722
- const agentsDir = resolve4(dir, "agents");
2723
- if (existsSync9(agentsDir)) {
2724
- const mdFiles = readdirSync3(agentsDir).filter((f) => f.endsWith(".md"));
2725
- if (mdFiles.length > 0) {
2726
- passes.push(`agents/ has ${mdFiles.length} .md file(s)`);
2727
- } else {
2728
- 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)`);
3570
+ }
3571
+ if (manifest.agents !== undefined) {
3572
+ warnings.push('agents/ co-exists with manifest "agents" \u2014 manifest replaces default (dir ignored)');
2729
3573
  }
2730
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)');
3600
+ }
2731
3601
  return { errors, warnings, passes };
2732
3602
  }
2733
3603
  };
2734
3604
  });
2735
3605
 
2736
3606
  // src/validators/claude/marketplace.ts
2737
- import { existsSync as existsSync10, readdirSync as readdirSync4 } from "fs";
2738
- import { resolve as resolve5, join as join8 } from "path";
3607
+ import { existsSync as existsSync15, readdirSync as readdirSync7 } from "fs";
3608
+ import { resolve as resolve6, join as join13 } from "path";
2739
3609
  var claudeMarketplaceValidator;
2740
3610
  var init_marketplace = __esm(() => {
2741
3611
  claudeMarketplaceValidator = {
@@ -2744,16 +3614,16 @@ var init_marketplace = __esm(() => {
2744
3614
  name: "Claude Plugin Marketplace",
2745
3615
  description: "Validates marketplace structure: plugins/ directory with valid plugin subdirectories",
2746
3616
  detect(dir) {
2747
- const pluginsDir = resolve5(dir, "plugins");
2748
- if (!existsSync10(pluginsDir))
3617
+ const pluginsDir = resolve6(dir, "plugins");
3618
+ if (!existsSync15(pluginsDir))
2749
3619
  return false;
2750
3620
  try {
2751
- const entries = readdirSync4(pluginsDir, { withFileTypes: true });
3621
+ const entries = readdirSync7(pluginsDir, { withFileTypes: true });
2752
3622
  for (const entry of entries) {
2753
3623
  if (!entry.isDirectory())
2754
3624
  continue;
2755
- const hasSkills = existsSync10(join8(pluginsDir, entry.name, "skills"));
2756
- const hasManifest = existsSync10(join8(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"));
2757
3627
  if (hasSkills || hasManifest)
2758
3628
  return true;
2759
3629
  }
@@ -2764,33 +3634,33 @@ var init_marketplace = __esm(() => {
2764
3634
  const errors = [];
2765
3635
  const warnings = [];
2766
3636
  const passes = [];
2767
- const pluginsDir = resolve5(dir, "plugins");
2768
- if (!existsSync10(pluginsDir)) {
3637
+ const pluginsDir = resolve6(dir, "plugins");
3638
+ if (!existsSync15(pluginsDir)) {
2769
3639
  errors.push("Missing plugins/ directory");
2770
3640
  return { errors, warnings, passes };
2771
3641
  }
2772
3642
  passes.push("plugins/ directory exists");
2773
- const pluginEntries = readdirSync4(pluginsDir, { withFileTypes: true }).filter((e) => e.isDirectory());
3643
+ const pluginEntries = readdirSync7(pluginsDir, { withFileTypes: true }).filter((e) => e.isDirectory());
2774
3644
  if (pluginEntries.length === 0) {
2775
3645
  errors.push("plugins/ directory is empty \u2014 expected at least one plugin");
2776
3646
  return { errors, warnings, passes };
2777
3647
  }
2778
3648
  passes.push(`${pluginEntries.length} plugin(s) found`);
2779
- if (existsSync10(resolve5(dir, "README.md"))) {
3649
+ if (existsSync15(resolve6(dir, "README.md"))) {
2780
3650
  passes.push("README.md exists at marketplace root");
2781
3651
  } else {
2782
3652
  warnings.push("No README.md at marketplace root \u2014 recommended for discoverability");
2783
3653
  }
2784
- if (existsSync10(resolve5(dir, "LICENSE"))) {
3654
+ if (existsSync15(resolve6(dir, "LICENSE"))) {
2785
3655
  passes.push("LICENSE exists at marketplace root");
2786
3656
  } else {
2787
3657
  warnings.push("No LICENSE at marketplace root \u2014 recommended");
2788
3658
  }
2789
3659
  for (const plugin of pluginEntries) {
2790
- const pluginPath = join8(pluginsDir, plugin.name);
2791
- const hasSkills = existsSync10(join8(pluginPath, "skills"));
2792
- const hasManifest = existsSync10(join8(pluginPath, ".claude-plugin", "plugin.json"));
2793
- const hasReadme = existsSync10(join8(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"));
2794
3664
  if (hasManifest || hasSkills) {
2795
3665
  passes.push(`Plugin "${plugin.name}" has ${hasManifest ? "manifest" : "skills/"}`);
2796
3666
  } else {
@@ -2806,35 +3676,55 @@ var init_marketplace = __esm(() => {
2806
3676
  });
2807
3677
 
2808
3678
  // src/validators/claude/hooks.ts
2809
- import { existsSync as existsSync11 } from "fs";
2810
- import { resolve as resolve6 } from "path";
3679
+ import { existsSync as existsSync16 } from "fs";
3680
+ import { resolve as resolve7 } from "path";
2811
3681
  var KNOWN_EVENTS, claudeHooksValidator;
2812
3682
  var init_hooks = __esm(() => {
2813
3683
  KNOWN_EVENTS = [
3684
+ "SessionStart",
3685
+ "Setup",
3686
+ "UserPromptSubmit",
3687
+ "UserPromptExpansion",
2814
3688
  "PreToolUse",
3689
+ "PermissionRequest",
3690
+ "PermissionDenied",
2815
3691
  "PostToolUse",
2816
- "Stop",
3692
+ "PostToolUseFailure",
3693
+ "PostToolBatch",
3694
+ "Notification",
3695
+ "MessageDisplay",
3696
+ "SubagentStart",
2817
3697
  "SubagentStop",
2818
- "SessionStart",
2819
- "SessionEnd",
2820
- "UserPromptSubmit",
3698
+ "TaskCreated",
3699
+ "TaskCompleted",
3700
+ "Stop",
3701
+ "StopFailure",
3702
+ "TeammateIdle",
3703
+ "InstructionsLoaded",
3704
+ "ConfigChange",
3705
+ "CwdChanged",
3706
+ "FileChanged",
3707
+ "WorktreeCreate",
3708
+ "WorktreeRemove",
2821
3709
  "PreCompact",
2822
- "Notification",
2823
- "PermissionRequest"
3710
+ "PostCompact",
3711
+ "Elicitation",
3712
+ "ElicitationResult",
3713
+ "SessionEnd"
2824
3714
  ];
2825
3715
  claudeHooksValidator = {
2826
3716
  id: "claude:hooks",
2827
3717
  provider: "claude",
2828
3718
  name: "Claude Hooks",
2829
- 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)",
2830
3720
  detect(dir) {
2831
- return existsSync11(resolve6(dir, "hooks", "hooks.json")) || existsSync11(resolve6(dir, "hooks.json"));
3721
+ return existsSync16(resolve7(dir, "hooks", "hooks.json")) || existsSync16(resolve7(dir, "hooks.json"));
2832
3722
  },
2833
3723
  async validate(dir, _opts) {
2834
3724
  const errors = [];
2835
3725
  const warnings = [];
2836
3726
  const passes = [];
2837
- const hooksPath = existsSync11(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");
2838
3728
  let config;
2839
3729
  try {
2840
3730
  const raw = await Bun.file(hooksPath).text();
@@ -2849,32 +3739,74 @@ var init_hooks = __esm(() => {
2849
3739
  if (KNOWN_EVENTS.includes(name)) {
2850
3740
  passes.push(`Event "${name}" is a known lifecycle event`);
2851
3741
  } else {
2852
- 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, ...)`);
2853
3743
  }
2854
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
+ }
2855
3787
  return { errors, warnings, passes };
2856
3788
  }
2857
3789
  };
2858
3790
  });
2859
3791
 
2860
3792
  // src/validators/claude/mcp.ts
2861
- import { existsSync as existsSync12 } from "fs";
2862
- import { resolve as resolve7 } from "path";
3793
+ import { existsSync as existsSync17 } from "fs";
3794
+ import { resolve as resolve8 } from "path";
2863
3795
  var claudeMcpValidator;
2864
3796
  var init_mcp = __esm(() => {
2865
3797
  claudeMcpValidator = {
2866
3798
  id: "claude:mcp",
2867
3799
  provider: "claude",
2868
3800
  name: "Claude MCP Config",
2869
- 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",
2870
3802
  detect(dir) {
2871
- return existsSync12(resolve7(dir, ".mcp.json"));
3803
+ return existsSync17(resolve8(dir, ".mcp.json"));
2872
3804
  },
2873
3805
  async validate(dir, _opts) {
2874
3806
  const errors = [];
2875
3807
  const warnings = [];
2876
3808
  const passes = [];
2877
- const mcpPath = resolve7(dir, ".mcp.json");
3809
+ const mcpPath = resolve8(dir, ".mcp.json");
2878
3810
  let config;
2879
3811
  try {
2880
3812
  const raw = await Bun.file(mcpPath).text();
@@ -2894,14 +3826,42 @@ var init_mcp = __esm(() => {
2894
3826
  return { errors, warnings, passes };
2895
3827
  }
2896
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
+ }
2897
3857
  return { errors, warnings, passes };
2898
3858
  }
2899
3859
  };
2900
3860
  });
2901
3861
 
2902
3862
  // src/validators/claude/subagent.ts
2903
- import { existsSync as existsSync13, readdirSync as readdirSync5 } from "fs";
2904
- import { resolve as resolve8, join as join9 } from "path";
3863
+ import { existsSync as existsSync18, readdirSync as readdirSync8 } from "fs";
3864
+ import { resolve as resolve9, join as join14 } from "path";
2905
3865
  var claudeSubagentValidator;
2906
3866
  var init_subagent = __esm(() => {
2907
3867
  init_frontmatter();
@@ -2909,13 +3869,13 @@ var init_subagent = __esm(() => {
2909
3869
  id: "claude:subagent",
2910
3870
  provider: "claude",
2911
3871
  name: "Claude Subagents",
2912
- 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",
2913
3873
  detect(dir) {
2914
- const agentsDir = resolve8(dir, "agents");
2915
- if (!existsSync13(agentsDir))
3874
+ const agentsDir = resolve9(dir, "agents");
3875
+ if (!existsSync18(agentsDir))
2916
3876
  return false;
2917
3877
  try {
2918
- return readdirSync5(agentsDir).some((f) => f.endsWith(".md"));
3878
+ return readdirSync8(agentsDir).some((f) => f.endsWith(".md"));
2919
3879
  } catch {
2920
3880
  return false;
2921
3881
  }
@@ -2924,27 +3884,63 @@ var init_subagent = __esm(() => {
2924
3884
  const errors = [];
2925
3885
  const warnings = [];
2926
3886
  const passes = [];
2927
- const agentsDir = resolve8(dir, "agents");
2928
- const mdFiles = readdirSync5(agentsDir).filter((f) => f.endsWith(".md"));
3887
+ const agentsDir = resolve9(dir, "agents");
3888
+ const mdFiles = readdirSync8(agentsDir).filter((f) => f.endsWith(".md"));
2929
3889
  if (mdFiles.length === 0) {
2930
3890
  errors.push("agents/ directory has no .md files");
2931
3891
  return { errors, warnings, passes };
2932
3892
  }
2933
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"]);
2934
3908
  for (const file of mdFiles) {
2935
- const filePath = join9(agentsDir, file);
3909
+ const filePath = join14(agentsDir, file);
2936
3910
  const raw = await Bun.file(filePath).text();
2937
3911
  try {
2938
3912
  const parsed = parseFrontmatter(raw);
2939
- if (Object.keys(parsed.data).length === 0) {
2940
- warnings.push(`${file}: no YAML frontmatter`);
2941
- } else if (!parsed.data.description) {
2942
- 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)`);
2943
3916
  } else {
2944
- 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
+ }
2945
3939
  }
2946
3940
  if (!parsed.content.trim()) {
2947
3941
  errors.push(`${file}: body is empty`);
3942
+ } else {
3943
+ passes.push(`${file}: has agent system prompt body`);
2948
3944
  }
2949
3945
  } catch {
2950
3946
  errors.push(`${file}: failed to parse`);
@@ -2956,8 +3952,8 @@ var init_subagent = __esm(() => {
2956
3952
  });
2957
3953
 
2958
3954
  // src/validators/claude/command.ts
2959
- import { existsSync as existsSync14, readdirSync as readdirSync6 } from "fs";
2960
- import { resolve as resolve9, join as join10 } from "path";
3955
+ import { existsSync as existsSync19, readdirSync as readdirSync9 } from "fs";
3956
+ import { resolve as resolve10, join as join15 } from "path";
2961
3957
  var claudeCommandValidator;
2962
3958
  var init_command = __esm(() => {
2963
3959
  init_frontmatter();
@@ -2967,11 +3963,11 @@ var init_command = __esm(() => {
2967
3963
  name: "Claude Commands",
2968
3964
  description: "Validates commands/ (or legacy .claude/commands/) .md files: frontmatter (including rich skill fields), description, body",
2969
3965
  detect(dir) {
2970
- const commandsDir = resolve9(dir, "commands");
2971
- if (!existsSync14(commandsDir))
3966
+ const commandsDir = resolve10(dir, "commands");
3967
+ if (!existsSync19(commandsDir))
2972
3968
  return false;
2973
3969
  try {
2974
- return readdirSync6(commandsDir).some((f) => f.endsWith(".md"));
3970
+ return readdirSync9(commandsDir).some((f) => f.endsWith(".md"));
2975
3971
  } catch {
2976
3972
  return false;
2977
3973
  }
@@ -2980,15 +3976,15 @@ var init_command = __esm(() => {
2980
3976
  const errors = [];
2981
3977
  const warnings = [];
2982
3978
  const passes = [];
2983
- const commandsDir = resolve9(dir, "commands");
2984
- const mdFiles = readdirSync6(commandsDir).filter((f) => f.endsWith(".md"));
3979
+ const commandsDir = resolve10(dir, "commands");
3980
+ const mdFiles = readdirSync9(commandsDir).filter((f) => f.endsWith(".md"));
2985
3981
  if (mdFiles.length === 0) {
2986
3982
  errors.push("commands/ directory has no .md files");
2987
3983
  return { errors, warnings, passes };
2988
3984
  }
2989
3985
  passes.push(`${mdFiles.length} command definition(s) found`);
2990
3986
  for (const file of mdFiles) {
2991
- const filePath = join10(commandsDir, file);
3987
+ const filePath = join15(commandsDir, file);
2992
3988
  const raw = await Bun.file(filePath).text();
2993
3989
  try {
2994
3990
  const parsed = parseFrontmatter(raw);
@@ -3017,8 +4013,8 @@ var init_command = __esm(() => {
3017
4013
  });
3018
4014
 
3019
4015
  // src/validators/claude/memory.ts
3020
- import { existsSync as existsSync15 } from "fs";
3021
- import { resolve as resolve10 } from "path";
4016
+ import { existsSync as existsSync20 } from "fs";
4017
+ import { resolve as resolve11 } from "path";
3022
4018
  var claudeMemoryValidator;
3023
4019
  var init_memory = __esm(() => {
3024
4020
  claudeMemoryValidator = {
@@ -3027,13 +4023,13 @@ var init_memory = __esm(() => {
3027
4023
  name: "Claude CLAUDE.md",
3028
4024
  description: "Validates CLAUDE.md: non-empty, length recommendations, @path imports",
3029
4025
  detect(dir) {
3030
- return existsSync15(resolve10(dir, "CLAUDE.md"));
4026
+ return existsSync20(resolve11(dir, "CLAUDE.md"));
3031
4027
  },
3032
4028
  async validate(dir, _opts) {
3033
4029
  const errors = [];
3034
4030
  const warnings = [];
3035
4031
  const passes = [];
3036
- const filePath = resolve10(dir, "CLAUDE.md");
4032
+ const filePath = resolve11(dir, "CLAUDE.md");
3037
4033
  const raw = await Bun.file(filePath).text();
3038
4034
  if (!raw.trim()) {
3039
4035
  errors.push("CLAUDE.md is empty");
@@ -3051,8 +4047,8 @@ var init_memory = __esm(() => {
3051
4047
  let match;
3052
4048
  while ((match = importRegex.exec(raw)) !== null) {
3053
4049
  const importPath = match[1];
3054
- const resolvedImport = resolve10(dir, importPath);
3055
- if (existsSync15(resolvedImport)) {
4050
+ const resolvedImport = resolve11(dir, importPath);
4051
+ if (existsSync20(resolvedImport)) {
3056
4052
  passes.push(`@import "${importPath}" exists`);
3057
4053
  } else {
3058
4054
  warnings.push(`@import "${importPath}" \u2014 file not found at ${resolvedImport}`);
@@ -3063,6 +4059,165 @@ var init_memory = __esm(() => {
3063
4059
  };
3064
4060
  });
3065
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
+
3066
4221
  // src/validators/index.ts
3067
4222
  function resolveFor(forFlag, allValidators = validators) {
3068
4223
  if (!forFlag) {
@@ -3097,6 +4252,8 @@ var init_validators = __esm(() => {
3097
4252
  init_subagent();
3098
4253
  init_command();
3099
4254
  init_memory();
4255
+ init_lsp();
4256
+ init_monitors();
3100
4257
  validators = [
3101
4258
  claudeSkillValidator,
3102
4259
  claudePluginValidator,
@@ -3105,14 +4262,16 @@ var init_validators = __esm(() => {
3105
4262
  claudeMcpValidator,
3106
4263
  claudeSubagentValidator,
3107
4264
  claudeCommandValidator,
3108
- claudeMemoryValidator
4265
+ claudeMemoryValidator,
4266
+ claudeLspValidator,
4267
+ claudeMonitorsValidator
3109
4268
  ];
3110
4269
  });
3111
4270
 
3112
4271
  // src/core/remote.ts
3113
4272
  import { spawnSync as spawnSync4 } from "child_process";
3114
4273
  import { mkdtempSync, rmSync } from "fs";
3115
- import { join as join11 } from "path";
4274
+ import { join as join16 } from "path";
3116
4275
  import { tmpdir } from "os";
3117
4276
  function parseRemoteUrl(input) {
3118
4277
  if (input.startsWith(".") || input.startsWith("/") || input.startsWith("~")) {
@@ -3149,7 +4308,7 @@ function isGhAvailable() {
3149
4308
  return ghAvailable;
3150
4309
  }
3151
4310
  async function cloneToTemp(parsed) {
3152
- const tmpDir = mkdtempSync(join11(tmpdir(), "dora-"));
4311
+ const tmpDir = mkdtempSync(join16(tmpdir(), "dora-"));
3153
4312
  const cleanup = () => {
3154
4313
  try {
3155
4314
  rmSync(tmpDir, { recursive: true, force: true });
@@ -3197,15 +4356,15 @@ var exports_validate_top = {};
3197
4356
  __export(exports_validate_top, {
3198
4357
  default: () => validate_top_default
3199
4358
  });
3200
- import { existsSync as existsSync17 } from "fs";
3201
- import { resolve as resolve11 } from "path";
3202
- var import_picocolors10, validate_top_default;
4359
+ import { existsSync as existsSync24 } from "fs";
4360
+ import { resolve as resolve14 } from "path";
4361
+ var import_picocolors13, validate_top_default;
3203
4362
  var init_validate_top = __esm(() => {
3204
4363
  init_dist();
3205
4364
  init_out();
3206
4365
  init_validators();
3207
4366
  init_remote();
3208
- import_picocolors10 = __toESM(require_picocolors(), 1);
4367
+ import_picocolors13 = __toESM(require_picocolors(), 1);
3209
4368
  validate_top_default = defineCommand({
3210
4369
  meta: {
3211
4370
  name: "validate",
@@ -3245,24 +4404,24 @@ var init_validate_top = __esm(() => {
3245
4404
  let cleanup;
3246
4405
  if (remote) {
3247
4406
  ui.info(`
3248
- Cloning ${import_picocolors10.default.dim(args.path)}...`);
4407
+ Cloning ${import_picocolors13.default.dim(args.path)}...`);
3249
4408
  try {
3250
4409
  const result = await cloneToTemp(remote);
3251
- fullPath = remote.subpath ? resolve11(result.dir, remote.subpath) : result.dir;
4410
+ fullPath = remote.subpath ? resolve14(result.dir, remote.subpath) : result.dir;
3252
4411
  cleanup = result.cleanup;
3253
4412
  } catch (err) {
3254
4413
  const msg = err instanceof Error ? err.message : String(err);
3255
4414
  ui.fail(msg);
3256
4415
  process.exit(1);
3257
4416
  }
3258
- if (!existsSync17(fullPath)) {
4417
+ if (!existsSync24(fullPath)) {
3259
4418
  cleanup();
3260
4419
  ui.fail(`Subdirectory not found in repo: ${remote.subpath}`);
3261
4420
  process.exit(1);
3262
4421
  }
3263
4422
  } else {
3264
- fullPath = resolve11(args.path);
3265
- if (!existsSync17(fullPath)) {
4423
+ fullPath = resolve14(args.path);
4424
+ if (!existsSync24(fullPath)) {
3266
4425
  ui.fail(`Path not found: ${args.path}
3267
4426
 
3268
4427
  Check that the path is correct and the directory exists.`);
@@ -3293,13 +4452,13 @@ Check that the path is correct and the directory exists.`);
3293
4452
  ` + `Available providers:
3294
4453
  ` + providers.map((p) => {
3295
4454
  const pvs = validators.filter((v) => v.provider === p);
3296
- return ` ${import_picocolors10.default.bold(p)}
3297
- ` + pvs.map((v) => ` \u2022 ${import_picocolors10.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(`
3298
4457
  `);
3299
4458
  }).join(`
3300
4459
  `) + `
3301
4460
 
3302
- Use ${import_picocolors10.default.dim("--for <provider>")} or ${import_picocolors10.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.`);
3303
4462
  process.exit(1);
3304
4463
  }
3305
4464
  const allResults = [];
@@ -3320,7 +4479,7 @@ Use ${import_picocolors10.default.dim("--for <provider>")} or ${import_picocolor
3320
4479
  } else {
3321
4480
  for (const { id, name, result } of allResults) {
3322
4481
  ui.write(`
3323
- ${import_picocolors10.default.bold("dora validate")} \u2014 ${import_picocolors10.default.white(name)} ${import_picocolors10.default.dim(`(${id})`)}
4482
+ ${import_picocolors13.default.bold("dora validate")} \u2014 ${import_picocolors13.default.white(name)} ${import_picocolors13.default.dim(`(${id})`)}
3324
4483
  `);
3325
4484
  ui.info(` Path: ${args.path}
3326
4485
  `);
@@ -3335,7 +4494,7 @@ Use ${import_picocolors10.default.dim("--for <provider>")} or ${import_picocolor
3335
4494
  }
3336
4495
  if (result.errors.length === 0 && result.warnings.length === 0) {
3337
4496
  ui.write(`
3338
- ${import_picocolors10.default.green("\u2713")} ${import_picocolors10.default.white("All checks passed.")}
4497
+ ${import_picocolors13.default.green("\u2713")} ${import_picocolors13.default.white("All checks passed.")}
3339
4498
  `);
3340
4499
  } else {
3341
4500
  ui.info(`
@@ -3357,16 +4516,16 @@ var exports_init2 = {};
3357
4516
  __export(exports_init2, {
3358
4517
  default: () => init_default2
3359
4518
  });
3360
- import { basename as basename2, join as join12 } from "path";
4519
+ import { basename as basename4, join as join17 } from "path";
3361
4520
  var {spawnSync: spawnSync5 } = globalThis.Bun;
3362
- var import_picocolors11, init_default2;
4521
+ var import_picocolors14, init_default2;
3363
4522
  var init_init2 = __esm(() => {
3364
4523
  init_dist();
3365
4524
  init_out();
3366
4525
  init_journal_config();
3367
4526
  init_journal_remote();
3368
4527
  init_prompt();
3369
- import_picocolors11 = __toESM(require_picocolors(), 1);
4528
+ import_picocolors14 = __toESM(require_picocolors(), 1);
3370
4529
  init_default2 = defineCommand({
3371
4530
  meta: {
3372
4531
  name: "init",
@@ -3393,17 +4552,17 @@ var init_init2 = __esm(() => {
3393
4552
  ui.heading("dora init \u2014 Set up doraval, your journal, and the coding agent dora should use on the fly");
3394
4553
  const ghCheck = ensureGhCli();
3395
4554
  if (!ghCheck.ok) {
3396
- ui.write(` ${import_picocolors11.default.red("\u2717")} ${import_picocolors11.default.white("The GitHub CLI (")}${import_picocolors11.default.bold("gh")}${import_picocolors11.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.")}
3397
4556
  `);
3398
- ui.info(` doraval uses ${import_picocolors11.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.
3399
4558
  `);
3400
4559
  ui.info(` Install it:
3401
4560
  `);
3402
- ui.info(` macOS: ${import_picocolors11.default.dim("brew install gh")}`);
3403
- ui.info(` Linux: ${import_picocolors11.default.dim("https://github.com/cli/cli/blob/trunk/docs/install_linux.md")}`);
3404
- ui.info(` Windows: ${import_picocolors11.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")}
3405
4564
  `);
3406
- ui.info(` Then authenticate: ${import_picocolors11.default.dim("gh auth login")}
4565
+ ui.info(` Then authenticate: ${import_picocolors14.default.dim("gh auth login")}
3407
4566
  `);
3408
4567
  process.exit(1);
3409
4568
  }
@@ -3416,44 +4575,44 @@ var init_init2 = __esm(() => {
3416
4575
  if (gitOwner) {
3417
4576
  defaultRepo = `${gitOwner}/${gitOwner}.md`;
3418
4577
  if (ghLogin && ghLogin !== gitOwner) {
3419
- sourceNote = ` ${import_picocolors11.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 + ")")}
3420
4579
  `;
3421
4580
  } else {
3422
- sourceNote = ` ${import_picocolors11.default.dim("(from git remote)")}
4581
+ sourceNote = ` ${import_picocolors14.default.dim("(from git remote)")}
3423
4582
  `;
3424
4583
  }
3425
4584
  } else if (ghLogin) {
3426
4585
  defaultRepo = `${ghLogin}/${ghLogin}.md`;
3427
- sourceNote = ` ${import_picocolors11.default.dim("(from your active gh account)")}
4586
+ sourceNote = ` ${import_picocolors14.default.dim("(from your active gh account)")}
3428
4587
  `;
3429
4588
  } else {
3430
- ui.warn(`Not logged in to GitHub. Run ${import_picocolors11.default.dim("gh auth login")} first.
4589
+ ui.warn(`Not logged in to GitHub. Run ${import_picocolors14.default.dim("gh auth login")} first.
3431
4590
  `);
3432
4591
  process.exit(1);
3433
4592
  }
3434
4593
  const existingConfig = await readConfig();
3435
4594
  if (existingConfig?.journal.repo) {
3436
4595
  defaultRepo = existingConfig.journal.repo;
3437
- sourceNote = ` ${import_picocolors11.default.dim("(from your previous journal setup)")}
4596
+ sourceNote = ` ${import_picocolors14.default.dim("(from your previous journal setup)")}
3438
4597
  `;
3439
4598
  }
3440
- ui.info(` Journal repo ${import_picocolors11.default.dim("(owner/name)")}`);
4599
+ ui.info(` Journal repo ${import_picocolors14.default.dim("(owner/name)")}`);
3441
4600
  if (sourceNote)
3442
4601
  ui.write(sourceNote);
3443
4602
  repo = prompt(" >", defaultRepo);
3444
4603
  }
3445
4604
  let project = args.project || process.env.DORAVAL_PROJECT;
3446
4605
  if (!project) {
3447
- const defaultProject = basename2(process.cwd());
4606
+ const defaultProject = basename4(process.cwd());
3448
4607
  project = prompt(" Project name", defaultProject);
3449
4608
  }
3450
4609
  project = sanitizeProjectName(project);
3451
4610
  if (!repoExists(repo)) {
3452
- ui.write(` ${import_picocolors11.default.red("\u2717")} ${import_picocolors11.default.white("Repository")} ${import_picocolors11.default.bold(repo)} ${import_picocolors11.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.")}
3453
4612
  `);
3454
4613
  ui.info(` Create it first:
3455
4614
  `);
3456
- ui.info(` ${import_picocolors11.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"`)}
3457
4616
  `);
3458
4617
  process.exit(1);
3459
4618
  }
@@ -3461,16 +4620,16 @@ var init_init2 = __esm(() => {
3461
4620
  const alreadyRegistered = existing?.journal.projects[project];
3462
4621
  const isRefresh = alreadyRegistered && args.refresh;
3463
4622
  if (alreadyRegistered && !isRefresh) {
3464
- ui.write(` ${import_picocolors11.default.yellow("\u26A0")} ${import_picocolors11.default.white("Project")} ${import_picocolors11.default.bold(project)} ${import_picocolors11.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.")}
3465
4624
  `);
3466
4625
  ui.info(` Repo: ${existing.journal.repo}
3467
4626
  `);
3468
- ui.info(` To refresh journal files, use ${import_picocolors11.default.dim("dora journal update")} (or ${import_picocolors11.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")}).
3469
4628
  `);
3470
4629
  }
3471
4630
  const journalsDir = getJournalsDir();
3472
4631
  const remotePath = `projects/${project}.md`;
3473
- const localPath = join12(journalsDir, `${project}.md`);
4632
+ const localPath = join17(journalsDir, `${project}.md`);
3474
4633
  const effectiveRepo = isRefresh && !args.repo ? existing.journal.repo : repo;
3475
4634
  const config = existing ?? {
3476
4635
  journal: { repo: effectiveRepo, projects: {} }
@@ -3481,9 +4640,9 @@ var init_init2 = __esm(() => {
3481
4640
  local_path: localPath
3482
4641
  };
3483
4642
  ensureDoravalDirs();
3484
- ui.write(` ${import_picocolors11.default.dim(import_picocolors11.default.gray("Fetching journal files from"))} ${import_picocolors11.default.gray(effectiveRepo)}${import_picocolors11.default.dim(import_picocolors11.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("..."))}
3485
4644
  `);
3486
- const globalDest = join12(journalsDir, "global.md");
4645
+ const globalDest = join17(journalsDir, "global.md");
3487
4646
  const refreshGlobalRes = await refreshLocalJournalFile(effectiveRepo, "global.md", globalDest);
3488
4647
  let wroteGlobal;
3489
4648
  if (!refreshGlobalRes.ok) {
@@ -3500,7 +4659,7 @@ var init_init2 = __esm(() => {
3500
4659
  if (wroteGlobal) {
3501
4660
  ui.success("global.md");
3502
4661
  } else {
3503
- ui.write(` ${import_picocolors11.default.dim("\xB7")} global.md ${import_picocolors11.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)")}`);
3504
4663
  await Bun.write(globalDest, `# Global Journal
3505
4664
 
3506
4665
  Cross-project principles.
@@ -3522,7 +4681,7 @@ Cross-project principles.
3522
4681
  if (wroteProject) {
3523
4682
  ui.success(remotePath);
3524
4683
  } else {
3525
- ui.write(` ${import_picocolors11.default.dim("\xB7")} ${remotePath} ${import_picocolors11.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)")}`);
3526
4685
  await Bun.write(localPath, `# ${project} Journal
3527
4686
 
3528
4687
  Project-specific decisions.
@@ -3530,13 +4689,13 @@ Project-specific decisions.
3530
4689
  }
3531
4690
  await writeConfig(config);
3532
4691
  ui.write(`
3533
- ${import_picocolors11.default.green("\u2713")} ${import_picocolors11.default.white("Journal ready for project")} ${import_picocolors11.default.bold(import_picocolors11.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))}.
3534
4693
  `);
3535
4694
  const existingAgent = (await readConfig())?.agent;
3536
4695
  if (existingAgent?.command) {
3537
- ui.write(` ${import_picocolors11.default.bold(import_picocolors11.default.white("Coding agent (already configured)"))}
4696
+ ui.write(` ${import_picocolors14.default.bold(import_picocolors14.default.white("Coding agent (already configured)"))}
3538
4697
  `);
3539
- ui.write(` Current: ${import_picocolors11.default.dim(import_picocolors11.default.gray(existingAgent.command))} template: ${import_picocolors11.default.dim(import_picocolors11.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)"))}
3540
4699
  `);
3541
4700
  const change = prompt(" Reconfigure / change the coding agent for on-the-fly enrichment? (y/N)", "n");
3542
4701
  if (!/^y/i.test(String(change))) {
@@ -3546,16 +4705,16 @@ Project-specific decisions.
3546
4705
  if (existingAgent)
3547
4706
  cfg.agent = existingAgent;
3548
4707
  await writeConfig(cfg);
3549
- ui.write(` ${import_picocolors11.default.green("\u2713")} ${import_picocolors11.default.white("Try:")} ${import_picocolors11.default.dim(import_picocolors11.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"'))}
3550
4709
  `);
3551
4710
  process.exit(0);
3552
4711
  return;
3553
4712
  }
3554
4713
  ui.blank();
3555
4714
  } else {
3556
- ui.write(` ${import_picocolors11.default.bold(import_picocolors11.default.white("Coding agent for journal add"))}
4715
+ ui.write(` ${import_picocolors14.default.bold(import_picocolors14.default.white("Coding agent for journal add"))}
3557
4716
  `);
3558
- ui.info(` When configured, ${import_picocolors11.default.dim(import_picocolors11.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.
3559
4718
  `);
3560
4719
  }
3561
4720
  const common = [
@@ -3574,7 +4733,7 @@ Project-specific decisions.
3574
4733
  }
3575
4734
  }
3576
4735
  let agentCmd = detected || "claude";
3577
- ui.write(` Detected / default agent command: ${import_picocolors11.default.dim(import_picocolors11.default.gray(agentCmd))}`);
4736
+ ui.write(` Detected / default agent command: ${import_picocolors14.default.dim(import_picocolors14.default.gray(agentCmd))}`);
3578
4737
  agentCmd = prompt(" Agent command (the binary you run for prompts)", agentCmd);
3579
4738
  let template = detected ? common.find((c) => c.name === detected)?.template || '-p "{{prompt}}" --output-format json' : '-p "{{prompt}}" --output-format json';
3580
4739
  ui.info(` Prompt template (use {{prompt}} placeholder):`);
@@ -3586,11 +4745,11 @@ Project-specific decisions.
3586
4745
  };
3587
4746
  await writeConfig(finalConfig);
3588
4747
  ui.write(`
3589
- ${import_picocolors11.default.green("\u2713")} ${import_picocolors11.default.white("Agent configured.")}
4748
+ ${import_picocolors14.default.green("\u2713")} ${import_picocolors14.default.white("Agent configured.")}
3590
4749
  `);
3591
- ui.info(` Re-run ${import_picocolors11.default.dim(import_picocolors11.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.
3592
4751
  `);
3593
- ui.info(` Next: ${import_picocolors11.default.dim(import_picocolors11.default.gray('dora journal add ".."'))}, ${import_picocolors11.default.dim(import_picocolors11.default.gray("dora journal list"))}, or ${import_picocolors11.default.dim(import_picocolors11.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"))}.
3594
4753
  `);
3595
4754
  process.exit(0);
3596
4755
  }
@@ -3602,7 +4761,7 @@ init_dist();
3602
4761
  // package.json
3603
4762
  var package_default = {
3604
4763
  name: "@hacksmith/doraval",
3605
- version: "0.2.20",
4764
+ version: "0.2.23",
3606
4765
  author: "Saif",
3607
4766
  repository: {
3608
4767
  type: "git",
@@ -3663,7 +4822,7 @@ var package_default = {
3663
4822
  };
3664
4823
 
3665
4824
  // src/cli/index.ts
3666
- var import_picocolors12 = __toESM(require_picocolors(), 1);
4825
+ var import_picocolors15 = __toESM(require_picocolors(), 1);
3667
4826
  var skill = defineCommand({
3668
4827
  meta: {
3669
4828
  name: "skill",
@@ -3694,6 +4853,32 @@ var journal = defineCommand({
3694
4853
  showUsage(journal);
3695
4854
  }
3696
4855
  });
4856
+ var claude = defineCommand({
4857
+ meta: {
4858
+ name: "claude",
4859
+ description: "Claude Code-specific commands (packaging, scaffolding, distribution)"
4860
+ },
4861
+ subCommands: {
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)
4864
+ },
4865
+ run() {
4866
+ showUsage(claude);
4867
+ }
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
+ });
3697
4882
  var doraemonArt = `
3698
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
3699
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
@@ -3715,12 +4900,15 @@ var main = defineCommand({
3715
4900
  subCommands: {
3716
4901
  validate: () => Promise.resolve().then(() => (init_validate_top(), exports_validate_top)).then((m) => m.default),
3717
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),
3718
4904
  skill: () => Promise.resolve(skill),
3719
- journal: () => Promise.resolve(journal)
4905
+ journal: () => Promise.resolve(journal),
4906
+ claude: () => Promise.resolve(claude),
4907
+ codex: () => Promise.resolve(codex)
3720
4908
  },
3721
4909
  run() {
3722
4910
  console.log(`
3723
- ` + import_picocolors12.default.blue(doraemonArt) + `
4911
+ ` + import_picocolors15.default.blue(doraemonArt) + `
3724
4912
  `);
3725
4913
  showUsage(main);
3726
4914
  }