@hacksmith/doraval 0.2.21 → 0.2.25

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 +8 -5
  2. package/bin/doraval.js +1487 -250
  3. package/package.json +1 -1
package/bin/doraval.js CHANGED
@@ -595,6 +595,71 @@ var init_dist = __esm(() => {
595
595
  negativePrefixRe = /^no[-A-Z]/;
596
596
  });
597
597
 
598
+ // package.json
599
+ var require_package = __commonJS((exports, module) => {
600
+ module.exports = {
601
+ name: "@hacksmith/doraval",
602
+ version: "0.2.25",
603
+ author: "Saif",
604
+ repository: {
605
+ type: "git",
606
+ url: "git+https://github.com/saif-shines/doraval.git"
607
+ },
608
+ devDependencies: {
609
+ "@types/bun": "latest"
610
+ },
611
+ bin: {
612
+ doraval: "bin/doraval-wrapper.js",
613
+ dora: "bin/doraval-wrapper.js"
614
+ },
615
+ description: "The context engineering toolkit for coding agents",
616
+ engines: {
617
+ bun: ">=1.2.0",
618
+ node: ">=14.18.0"
619
+ },
620
+ files: [
621
+ "bin/",
622
+ "dist/",
623
+ "README.md"
624
+ ],
625
+ keywords: [
626
+ "cli",
627
+ "skills",
628
+ "plugins",
629
+ "agent",
630
+ "validation",
631
+ "lint",
632
+ "claude-code",
633
+ "grok",
634
+ "cursor",
635
+ "windsurf",
636
+ "mcp"
637
+ ],
638
+ license: "MIT",
639
+ workspaces: [
640
+ "apps/*"
641
+ ],
642
+ scripts: {
643
+ build: "bun build ./src/cli/index.ts --outfile ./bin/doraval.js --target bun",
644
+ dev: "bun run ./src/cli/index.ts",
645
+ test: "bun test",
646
+ typecheck: "bunx tsc --noEmit --skipLibCheck",
647
+ prepublishOnly: `bun run build && node -e "const p=require('./package.json'),j=require('./jsr.json');if(p.version!==j.version){console.error('Version mismatch: package.json='+p.version+' jsr.json='+j.version);process.exit(1)}"`,
648
+ bump: "bun run scripts/bump.ts",
649
+ release: "bun run scripts/release.ts",
650
+ "jsr:publish": "bunx jsr publish",
651
+ "site:dev": "cd apps/website && bun run dev",
652
+ "site:build": "cd apps/website && bun run build",
653
+ "site:preview": "cd apps/website && bun run preview"
654
+ },
655
+ type: "module",
656
+ dependencies: {
657
+ citty: "^0.2.2",
658
+ picocolors: "^1.1.1"
659
+ }
660
+ };
661
+ });
662
+
598
663
  // node_modules/.bun/picocolors@1.1.1/node_modules/picocolors/picocolors.js
599
664
  var require_picocolors = __commonJS((exports, module) => {
600
665
  var p = process || {};
@@ -2624,17 +2689,17 @@ __export(exports_new, {
2624
2689
  default: () => new_default,
2625
2690
  decidePath: () => decidePath
2626
2691
  });
2627
- import { join as join8 } from "path";
2692
+ import { join as join8, basename as basename2 } from "path";
2628
2693
  import { mkdirSync as mkdirSync2, writeFileSync, existsSync as existsSync9 } from "fs";
2629
2694
  function decidePath(ctx, intent, providedName) {
2630
2695
  const rawName = providedName || "";
2631
- let path = "standalone";
2696
+ let decisionPath = "standalone";
2632
2697
  let targetDir = ctx.cwd;
2633
2698
  let shouldCreateDir = false;
2634
2699
  let migrateExisting = false;
2635
- const useCurrentDirAsRoot = rawName === "." || rawName === path.basename(ctx.cwd) || !rawName;
2700
+ const useCurrentDirAsRoot = rawName === "." || rawName === basename2(ctx.cwd) || !rawName;
2636
2701
  if (intent === "distribute" || intent === "self-later" && ctx.looseSkillFiles.length > 0 && !ctx.hasClaudeDir) {
2637
- path = "plugin";
2702
+ decisionPath = "plugin";
2638
2703
  if (useCurrentDirAsRoot) {
2639
2704
  targetDir = ctx.cwd;
2640
2705
  shouldCreateDir = false;
@@ -2644,7 +2709,7 @@ function decidePath(ctx, intent, providedName) {
2644
2709
  }
2645
2710
  migrateExisting = ctx.looseSkillFiles.length > 0;
2646
2711
  } else if (intent === "self-later" && !ctx.hasClaudeDir) {
2647
- path = "plugin";
2712
+ decisionPath = "plugin";
2648
2713
  if (useCurrentDirAsRoot) {
2649
2714
  targetDir = ctx.cwd;
2650
2715
  shouldCreateDir = false;
@@ -2652,7 +2717,7 @@ function decidePath(ctx, intent, providedName) {
2652
2717
  targetDir = join8(ctx.cwd, rawName);
2653
2718
  shouldCreateDir = true;
2654
2719
  }
2655
- } else if (path === "standalone") {
2720
+ } else if (decisionPath === "standalone") {
2656
2721
  if (useCurrentDirAsRoot) {
2657
2722
  targetDir = ctx.cwd;
2658
2723
  shouldCreateDir = false;
@@ -2661,7 +2726,7 @@ function decidePath(ctx, intent, providedName) {
2661
2726
  shouldCreateDir = true;
2662
2727
  }
2663
2728
  }
2664
- return { path, targetDir, shouldCreateDir, migrateExisting };
2729
+ return { path: decisionPath, targetDir, shouldCreateDir, migrateExisting };
2665
2730
  }
2666
2731
  function scaffold(decision, ctx, migrateContent) {
2667
2732
  const { targetDir, path, shouldCreateDir } = decision;
@@ -2673,26 +2738,57 @@ function scaffold(decision, ctx, migrateContent) {
2673
2738
  mkdirSync2(targetDir, { recursive: true });
2674
2739
  }
2675
2740
  if (path === "plugin") {
2741
+ const pluginName = basename2(targetDir);
2676
2742
  const pluginJson = {
2677
- name: decision.targetDir.split("/").pop(),
2743
+ name: pluginName,
2678
2744
  description: "Scaffolded by doraval claude new",
2679
2745
  version: "0.1.0"
2680
2746
  };
2681
2747
  mkdirSync2(join8(targetDir, ".claude-plugin"), { recursive: true });
2682
2748
  writeFileSync(join8(targetDir, ".claude-plugin", "plugin.json"), JSON.stringify(pluginJson, null, 2));
2683
- mkdirSync2(join8(targetDir, "skills", "my-skill"), { recursive: true });
2684
- const skillBody = migrateContent || `# My Skill
2685
-
2686
- Basic starter skill.`;
2687
- writeFileSync(join8(targetDir, "skills", "my-skill", "SKILL.md"), `---
2688
- name: my-skill
2689
- description: Starter skill
2749
+ const marketplaceJson = {
2750
+ name: pluginName,
2751
+ version: "0.1.0",
2752
+ description: "Scaffolded by doraval claude new",
2753
+ author: { name: "" },
2754
+ homepage: "",
2755
+ repository: "",
2756
+ license: "MIT",
2757
+ keywords: ["claude-code", "skills", "plugin"]
2758
+ };
2759
+ writeFileSync(join8(targetDir, "marketplace.json"), JSON.stringify(marketplaceJson, null, 2));
2760
+ const demoSkillName = "doraval";
2761
+ mkdirSync2(join8(targetDir, "skills", demoSkillName), { recursive: true });
2762
+ let skillContent;
2763
+ if (migrateContent) {
2764
+ skillContent = migrateContent;
2765
+ } else {
2766
+ skillContent = `---
2767
+ name: ${demoSkillName}
2768
+ description: Use doraval to validate, measure drift, and judge skills and plugins. Use when authoring or reviewing context engineering artifacts for AI coding agents.
2690
2769
  ---
2691
2770
 
2692
- ${skillBody}`);
2693
- writeFileSync(join8(targetDir, "README.md"), "# " + pluginJson.name + `
2771
+ # Use Doraval
2772
+
2773
+ Doraval is the context engineering toolkit.
2774
+
2775
+ When you need to check a skill or plugin:
2776
+
2777
+ - Validate the current directory: \`doraval validate .\`
2778
+ - Validate a specific plugin: \`doraval validate . --for claude:plugin\`
2779
+ - Validate one skill: \`doraval skill validate ./skills/${demoSkillName}/\`
2780
+ - Check for rubric drift: \`doraval skill drift ./skills/${demoSkillName}/\`
2781
+ - Get an AI quality judgment: \`doraval skill judge ./skills/${demoSkillName}/\`
2782
+
2783
+ 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.`;
2784
+ }
2785
+ writeFileSync(join8(targetDir, "skills", demoSkillName, "SKILL.md"), skillContent);
2786
+ const readmePath = join8(targetDir, "README.md");
2787
+ if (!existsSync9(readmePath)) {
2788
+ writeFileSync(readmePath, "# " + pluginName + `
2694
2789
 
2695
2790
  Claude Code plugin scaffolded by doraval.`);
2791
+ }
2696
2792
  } else {
2697
2793
  mkdirSync2(join8(targetDir, ".claude", "skills", "my-skill"), { recursive: true });
2698
2794
  const skillBody = migrateContent || `# My Skill
@@ -2752,7 +2848,12 @@ var init_new = __esm(() => {
2752
2848
  scaffold(decision, ctx, migrateContent);
2753
2849
  ui.write(`
2754
2850
  ${import_picocolors10.default.green("\u2713")} Created ${decision.path} at ${import_picocolors10.default.bold(decision.targetDir)}`);
2755
- ui.info(` Command: ${decision.path === "plugin" ? `/${decision.targetDir.split("/").pop()}:my-skill` : "/my-skill"}`);
2851
+ const cmdName = decision.path === "plugin" ? `/${basename2(decision.targetDir)}:doraval` : "/my-skill";
2852
+ ui.info(` Command: ${cmdName}`);
2853
+ if (decision.path === "plugin") {
2854
+ ui.info(` Claude: .claude-plugin/plugin.json`);
2855
+ ui.info(` Marketplace: marketplace.json (unified / cross-provider listings)`);
2856
+ }
2756
2857
  ui.info(` Test: claude --plugin-dir ${decision.targetDir} (or use normally for standalone)`);
2757
2858
  ui.info(` Validate: doraval validate ${decision.targetDir}`);
2758
2859
  if (decision.path === "plugin" && decision.migrateExisting) {
@@ -2763,9 +2864,472 @@ var init_new = __esm(() => {
2763
2864
  });
2764
2865
  });
2765
2866
 
2867
+ // src/cli/commands/bump.ts
2868
+ var exports_bump = {};
2869
+ __export(exports_bump, {
2870
+ default: () => bump_default
2871
+ });
2872
+ import { resolve as resolve3, join as join9, dirname, relative } from "path";
2873
+ import { existsSync as existsSync10, readFileSync, writeFileSync as writeFileSync2, readdirSync as readdirSync4, statSync } from "fs";
2874
+ function bumpVersion(current, type) {
2875
+ if (/^\d+\.\d+\.\d+$/.test(type))
2876
+ return type;
2877
+ const curr = current || "0.0.0";
2878
+ const parts = curr.split(".").map((n) => parseInt(n, 10) || 0);
2879
+ const [major = 0, minor = 0, patch = 0] = parts;
2880
+ switch (type) {
2881
+ case "patch":
2882
+ return `${major}.${minor}.${patch + 1}`;
2883
+ case "minor":
2884
+ return `${major}.${minor + 1}.0`;
2885
+ case "major":
2886
+ return `${major + 1}.0.0`;
2887
+ default:
2888
+ throw new Error(`Invalid bump type "${type}". Use patch, minor, major, or an exact version like 1.2.3`);
2889
+ }
2890
+ }
2891
+ function readJson(p) {
2892
+ try {
2893
+ const content = readFileSync(p, "utf8");
2894
+ return JSON.parse(content);
2895
+ } catch {
2896
+ return null;
2897
+ }
2898
+ }
2899
+ function writeJson(p, data) {
2900
+ writeFileSync2(p, JSON.stringify(data, null, 2) + `
2901
+ `, "utf8");
2902
+ }
2903
+ function getVersion(obj) {
2904
+ if (!obj || typeof obj !== "object")
2905
+ return;
2906
+ if (typeof obj.version === "string")
2907
+ return obj.version;
2908
+ if (obj.metadata && typeof obj.metadata.version === "string")
2909
+ return obj.metadata.version;
2910
+ return;
2911
+ }
2912
+ function setVersion(obj, newVersion) {
2913
+ if (!obj || typeof obj !== "object")
2914
+ return false;
2915
+ if (typeof obj.version === "string") {
2916
+ obj.version = newVersion;
2917
+ return true;
2918
+ }
2919
+ if (obj.metadata && typeof obj.metadata.version === "string") {
2920
+ obj.metadata.version = newVersion;
2921
+ return true;
2922
+ }
2923
+ return false;
2924
+ }
2925
+ function walkForTargets(dir, maxDepth = 6, currentDepth = 0) {
2926
+ const results = [];
2927
+ if (currentDepth > maxDepth)
2928
+ return results;
2929
+ let entries;
2930
+ try {
2931
+ entries = readdirSync4(dir);
2932
+ } catch {
2933
+ return results;
2934
+ }
2935
+ for (const entry of entries) {
2936
+ const full = join9(dir, entry);
2937
+ let st;
2938
+ try {
2939
+ st = statSync(full);
2940
+ } catch {
2941
+ continue;
2942
+ }
2943
+ if (st.isDirectory()) {
2944
+ const sub = walkForTargets(full, maxDepth, currentDepth + 1);
2945
+ results.push(...sub);
2946
+ } else if (st.isFile()) {
2947
+ if (entry === "plugin.json") {
2948
+ const parentDir = dirname(full);
2949
+ const parentName = parentDir.split(/[/\\]/).pop();
2950
+ if (parentName === ".claude-plugin" || parentName === ".codex-plugin" || parentName === ".cursor-plugin") {
2951
+ results.push({
2952
+ file: full,
2953
+ kind: "plugin",
2954
+ label: `plugin manifest (${parentName.replace(".", "")})`
2955
+ });
2956
+ }
2957
+ } else if (entry === "marketplace.json") {
2958
+ const json = readJson(full);
2959
+ if (json && getVersion(json)) {
2960
+ results.push({
2961
+ file: full,
2962
+ kind: "marketplace",
2963
+ label: "marketplace.json"
2964
+ });
2965
+ }
2966
+ }
2967
+ }
2968
+ }
2969
+ return results;
2970
+ }
2971
+ var import_picocolors11, bump_default;
2972
+ var init_bump = __esm(() => {
2973
+ init_dist();
2974
+ init_out();
2975
+ import_picocolors11 = __toESM(require_picocolors(), 1);
2976
+ bump_default = defineCommand({
2977
+ meta: {
2978
+ name: "bump",
2979
+ description: "Bump semver versions in plugin.json (manifests) and marketplace.json files (supports Claude, Codex, Cursor)"
2980
+ },
2981
+ args: {
2982
+ type: {
2983
+ type: "positional",
2984
+ description: "patch | minor | major | x.y.z (exact version)",
2985
+ required: false
2986
+ },
2987
+ path: {
2988
+ type: "positional",
2989
+ description: "Directory to scan from (defaults to current dir). Supports single plugin or marketplace root with many plugins/",
2990
+ required: false
2991
+ },
2992
+ only: {
2993
+ type: "string",
2994
+ description: 'Scope to "all" (default), "plugin" (only plugin.json manifests), or "marketplace" (only marketplace.json files that carry a top-level version)',
2995
+ default: "all"
2996
+ }
2997
+ },
2998
+ run({ args }) {
2999
+ let rawType = args.type || "patch";
3000
+ let targetPath = args.path || ".";
3001
+ const scopeInput = (args.only || "all").toLowerCase();
3002
+ const scope = scopeInput === "plugin" || scopeInput === "marketplace" ? scopeInput : "all";
3003
+ if (!["all", "plugin", "marketplace"].includes(scopeInput)) {
3004
+ ui.fail(`Invalid --only "${args.only}". Allowed: all, plugin, marketplace.`);
3005
+ process.exit(1);
3006
+ }
3007
+ const isKnownType = ["patch", "minor", "major"].includes(rawType) || /^\d+\.\d+\.\d+$/.test(rawType);
3008
+ const maybePath = resolve3(rawType);
3009
+ const looksLikeDir = existsSync10(maybePath) || rawType === "." || rawType.startsWith("./") || rawType.startsWith("../");
3010
+ if (!isKnownType && looksLikeDir) {
3011
+ targetPath = rawType;
3012
+ rawType = "patch";
3013
+ } else if (!isKnownType) {
3014
+ ui.fail(`Unknown bump type "${rawType}". Use patch | minor | major | 1.2.3`);
3015
+ process.exit(1);
3016
+ }
3017
+ const root = resolve3(targetPath);
3018
+ if (!existsSync10(root)) {
3019
+ ui.fail(`Path does not exist: ${root}`);
3020
+ process.exit(1);
3021
+ }
3022
+ ui.heading("doraval bump");
3023
+ ui.info(` scanning: ${root}`);
3024
+ ui.info(` scope: ${scope} (use --only plugin or --only marketplace to narrow; Cursor metadata.version supported)`);
3025
+ const discovered = walkForTargets(root);
3026
+ let targets = discovered;
3027
+ if (scope === "plugin") {
3028
+ targets = discovered.filter((t) => t.kind === "plugin");
3029
+ } else if (scope === "marketplace") {
3030
+ targets = discovered.filter((t) => t.kind === "marketplace");
3031
+ }
3032
+ if (targets.length === 0) {
3033
+ ui.fail("No matching files found under the scope.");
3034
+ ui.info("");
3035
+ ui.info(" Looked for (recursively):");
3036
+ ui.info(" \u2022 **/.claude-plugin/plugin.json");
3037
+ ui.info(" \u2022 **/.codex-plugin/plugin.json");
3038
+ ui.info(" \u2022 **/.cursor-plugin/plugin.json (or marketplace.json)");
3039
+ ui.info(" \u2022 **/marketplace.json (top-level version or metadata.version for Cursor)");
3040
+ ui.info("");
3041
+ ui.info(" Tip: run from inside a plugin directory, or pass a path that contains plugins/.");
3042
+ ui.info(" Examples:");
3043
+ ui.info(" dora bump minor");
3044
+ ui.info(" dora bump minor ./my-claude-plugin");
3045
+ ui.info(" dora bump --only plugin . # only the manifests");
3046
+ ui.info(" dora bump --only marketplace ./marketplaces-root # includes Cursor metadata.version");
3047
+ process.exit(1);
3048
+ }
3049
+ ui.info(` matched ${targets.length} file(s)`);
3050
+ let bumpedCount = 0;
3051
+ for (const t of targets) {
3052
+ const json = readJson(t.file);
3053
+ if (!json || typeof json !== "object") {
3054
+ ui.warnItem(`skipped (invalid JSON): ${relative(root, t.file)}`);
3055
+ continue;
3056
+ }
3057
+ const current = getVersion(json);
3058
+ let next;
3059
+ try {
3060
+ next = bumpVersion(current, rawType);
3061
+ } catch (err) {
3062
+ ui.fail(err.message || String(err));
3063
+ process.exit(1);
3064
+ }
3065
+ const relPath = relative(root, t.file);
3066
+ if (current === next) {
3067
+ ui.dim(` \u2022 ${t.label} ${current || "(no version)"} (no change) [${relPath}]`);
3068
+ continue;
3069
+ }
3070
+ const didUpdate = setVersion(json, next);
3071
+ if (!didUpdate) {
3072
+ ui.warnItem(`skipped (could not locate version field to update): ${relPath}`);
3073
+ continue;
3074
+ }
3075
+ writeJson(t.file, json);
3076
+ ui.success(`${t.label}: ${import_picocolors11.default.dim(current || "(none)")} \u2192 ${import_picocolors11.default.green(next)}`);
3077
+ ui.info(` ${relPath}`);
3078
+ bumpedCount++;
3079
+ }
3080
+ ui.blank();
3081
+ if (bumpedCount === 0) {
3082
+ ui.info("All matched files were already at the target version.");
3083
+ } else {
3084
+ ui.info(`Done. Bumped ${bumpedCount} file(s).`);
3085
+ ui.dim(" Next: doraval validate " + (targetPath === "." ? "." : targetPath));
3086
+ }
3087
+ process.exit(0);
3088
+ }
3089
+ });
3090
+ });
3091
+
3092
+ // src/cli/commands/codex/context.ts
3093
+ import { existsSync as existsSync11, readdirSync as readdirSync5 } from "fs";
3094
+ import { join as join10 } from "path";
3095
+ function detectContext2(cwd = process.cwd()) {
3096
+ const hasCodexDir = existsSync11(join10(cwd, ".codex"));
3097
+ const hasPluginManifest = existsSync11(join10(cwd, ".codex-plugin", "plugin.json"));
3098
+ const hasMarketplace = existsSync11(join10(cwd, ".agents", "plugins", "marketplace.json")) || existsSync11(join10(cwd, ".codex-plugin", "marketplace.json"));
3099
+ let looseSkillFiles = [];
3100
+ try {
3101
+ const files = readdirSync5(cwd);
3102
+ looseSkillFiles = files.filter((f) => {
3103
+ if (!f.endsWith(".md") || f.startsWith("."))
3104
+ return false;
3105
+ const lower = f.toLowerCase();
3106
+ if (lower === "readme.md" || lower === "changelog.md" || lower === "license.md" || lower.includes("contributing"))
3107
+ return false;
3108
+ return lower.includes("skill") || lower === "skill.md";
3109
+ });
3110
+ } catch {}
3111
+ const isEmpty = !hasPluginManifest && looseSkillFiles.length === 0;
3112
+ return {
3113
+ cwd,
3114
+ hasCodexDir,
3115
+ hasPluginManifest,
3116
+ hasMarketplace,
3117
+ looseSkillFiles,
3118
+ isEmpty
3119
+ };
3120
+ }
3121
+ var init_context2 = () => {};
3122
+
3123
+ // src/cli/commands/codex/new.ts
3124
+ var exports_new2 = {};
3125
+ __export(exports_new2, {
3126
+ scaffold: () => scaffold2,
3127
+ default: () => new_default2,
3128
+ decidePath: () => decidePath2
3129
+ });
3130
+ import { join as join11, basename as basename3 } from "path";
3131
+ import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3, existsSync as existsSync12 } from "fs";
3132
+ function decidePath2(ctx, intent, providedName) {
3133
+ const rawName = providedName || "";
3134
+ let decisionPath = "standalone";
3135
+ let targetDir = ctx.cwd;
3136
+ let shouldCreateDir = false;
3137
+ let migrateExisting = false;
3138
+ const useCurrentDirAsRoot = rawName === "." || rawName === basename3(ctx.cwd) || !rawName;
3139
+ if (intent === "distribute" || intent === "self-later" && ctx.looseSkillFiles.length > 0 && !ctx.hasPluginManifest) {
3140
+ decisionPath = "plugin";
3141
+ if (useCurrentDirAsRoot) {
3142
+ targetDir = ctx.cwd;
3143
+ shouldCreateDir = false;
3144
+ } else {
3145
+ targetDir = join11(ctx.cwd, rawName);
3146
+ shouldCreateDir = true;
3147
+ }
3148
+ migrateExisting = ctx.looseSkillFiles.length > 0;
3149
+ } else if (intent === "self-later" && !ctx.hasPluginManifest) {
3150
+ decisionPath = "plugin";
3151
+ if (useCurrentDirAsRoot) {
3152
+ targetDir = ctx.cwd;
3153
+ shouldCreateDir = false;
3154
+ } else {
3155
+ targetDir = join11(ctx.cwd, rawName);
3156
+ shouldCreateDir = true;
3157
+ }
3158
+ } else if (decisionPath === "standalone") {
3159
+ if (useCurrentDirAsRoot) {
3160
+ targetDir = ctx.cwd;
3161
+ shouldCreateDir = false;
3162
+ } else {
3163
+ targetDir = join11(ctx.cwd, rawName);
3164
+ shouldCreateDir = true;
3165
+ }
3166
+ }
3167
+ return { path: decisionPath, targetDir, shouldCreateDir, migrateExisting };
3168
+ }
3169
+ function scaffold2(decision, ctx, migrateContent) {
3170
+ const { targetDir, path, shouldCreateDir } = decision;
3171
+ if (existsSync12(targetDir) && shouldCreateDir) {
3172
+ ui.fail("Target already exists");
3173
+ process.exit(1);
3174
+ }
3175
+ if (shouldCreateDir) {
3176
+ mkdirSync3(targetDir, { recursive: true });
3177
+ }
3178
+ if (path === "plugin") {
3179
+ const pluginName = basename3(targetDir);
3180
+ const pluginJson = {
3181
+ name: pluginName,
3182
+ version: "0.1.0",
3183
+ description: "Scaffolded by doraval codex new",
3184
+ skills: "./skills/",
3185
+ interface: {
3186
+ displayName: pluginName,
3187
+ shortDescription: "Scaffolded starter plugin",
3188
+ category: "Productivity"
3189
+ }
3190
+ };
3191
+ mkdirSync3(join11(targetDir, ".codex-plugin"), { recursive: true });
3192
+ writeFileSync3(join11(targetDir, ".codex-plugin", "plugin.json"), JSON.stringify(pluginJson, null, 2));
3193
+ mkdirSync3(join11(targetDir, ".agents", "plugins"), { recursive: true });
3194
+ const marketplaceJson = {
3195
+ name: "local",
3196
+ interface: {
3197
+ displayName: "Local (doraval scaffold)"
3198
+ },
3199
+ plugins: [
3200
+ {
3201
+ name: pluginName,
3202
+ source: {
3203
+ source: "local",
3204
+ path: "../.."
3205
+ },
3206
+ policy: {
3207
+ installation: "AVAILABLE",
3208
+ authentication: "ON_INSTALL"
3209
+ },
3210
+ category: "Productivity"
3211
+ }
3212
+ ]
3213
+ };
3214
+ writeFileSync3(join11(targetDir, ".agents", "plugins", "marketplace.json"), JSON.stringify(marketplaceJson, null, 2));
3215
+ const demoSkillName = "doraval";
3216
+ mkdirSync3(join11(targetDir, "skills", demoSkillName), { recursive: true });
3217
+ let skillContent;
3218
+ if (migrateContent) {
3219
+ skillContent = migrateContent;
3220
+ } else {
3221
+ skillContent = `---
3222
+ name: ${demoSkillName}
3223
+ 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).
3224
+ ---
3225
+
3226
+ # Use Doraval (Codex edition)
3227
+
3228
+ Doraval is the context engineering toolkit.
3229
+
3230
+ When you need to check a skill or Codex plugin:
3231
+
3232
+ - Validate the current directory: \`doraval validate .\`
3233
+ - Validate one skill: \`doraval skill validate ./skills/${demoSkillName}/\`
3234
+ - Check for rubric drift: \`doraval skill drift ./skills/${demoSkillName}/\`
3235
+ - Get an AI quality judgment: \`doraval skill judge ./skills/${demoSkillName}/\`
3236
+
3237
+ Always run \`doraval validate\` before sharing or publishing a plugin.
3238
+
3239
+ This skill demonstrates a complete, self-referential example of using doraval inside a generated Codex plugin.
3240
+
3241
+ To test in Codex:
3242
+ 1. Make sure this plugin is listed in a marketplace (we created .agents/plugins/marketplace.json for you).
3243
+ 2. Restart Codex.
3244
+ 3. Open the plugin directory, select your local marketplace, and enable the plugin.
3245
+ 4. Invoke the demo with /${pluginName}:doraval`;
3246
+ }
3247
+ writeFileSync3(join11(targetDir, "skills", demoSkillName, "SKILL.md"), skillContent);
3248
+ const readmePath = join11(targetDir, "README.md");
3249
+ if (!existsSync12(readmePath)) {
3250
+ writeFileSync3(readmePath, "# " + pluginName + `
3251
+
3252
+ Codex plugin scaffolded by doraval.`);
3253
+ }
3254
+ } else {
3255
+ mkdirSync3(join11(targetDir, "skills", "doraval"), { recursive: true });
3256
+ const skillBody = migrateContent || `# My Skill
3257
+
3258
+ Basic starter for Codex.`;
3259
+ writeFileSync3(join11(targetDir, "skills", "doraval", "SKILL.md"), `---
3260
+ name: doraval
3261
+ description: Starter (local skill)
3262
+ ---
3263
+
3264
+ ${skillBody}`);
3265
+ }
3266
+ }
3267
+ var import_picocolors12, new_default2;
3268
+ var init_new2 = __esm(() => {
3269
+ init_dist();
3270
+ init_out();
3271
+ init_context2();
3272
+ init_prompt();
3273
+ import_picocolors12 = __toESM(require_picocolors(), 1);
3274
+ new_default2 = defineCommand({
3275
+ meta: {
3276
+ name: "new",
3277
+ description: "Create a new skill or plugin following Codex packaging rules"
3278
+ },
3279
+ args: {
3280
+ name: {
3281
+ type: "positional",
3282
+ description: "Optional name for the skill or plugin",
3283
+ required: false
3284
+ },
3285
+ yes: {
3286
+ type: "boolean",
3287
+ description: "Skip interactive prompts (use defaults and flags)",
3288
+ default: false
3289
+ },
3290
+ intent: {
3291
+ type: "string",
3292
+ description: 'Intent: "self" | "self-later" | "distribute"',
3293
+ required: false
3294
+ }
3295
+ },
3296
+ run({ args }) {
3297
+ ui.heading("doraval codex new \u2014 Context-aware scaffolding");
3298
+ const ctx = detectContext2();
3299
+ let intent = args.intent || "self-later";
3300
+ if (!args.yes) {
3301
+ const ans = prompt(" Intent (self | self-later | distribute)", intent);
3302
+ intent = ans || intent;
3303
+ }
3304
+ const decision = decidePath2(ctx, intent, args.name);
3305
+ ui.info(` Decision: path=${decision.path}, target=${decision.targetDir}`);
3306
+ let migrateContent;
3307
+ if (decision.migrateExisting && !args.yes) {
3308
+ migrateContent = "Content from your existing SKILL.md (user-confirmed).";
3309
+ }
3310
+ scaffold2(decision, ctx, migrateContent);
3311
+ ui.write(`
3312
+ ${import_picocolors12.default.green("\u2713")} Created ${decision.path} at ${import_picocolors12.default.bold(decision.targetDir)}`);
3313
+ const cmdName = decision.path === "plugin" ? `/${basename3(decision.targetDir)}:doraval` : "/doraval (local skill)";
3314
+ ui.info(` Command: ${cmdName}`);
3315
+ if (decision.path === "plugin") {
3316
+ ui.info(` Codex manifest: .codex-plugin/plugin.json`);
3317
+ ui.info(` Marketplace catalog: .agents/plugins/marketplace.json (starter for local testing)`);
3318
+ ui.info(` (Move/expand the marketplace.json to $REPO_ROOT/.agents/plugins/ or ~/.agents/plugins/ as needed)`);
3319
+ }
3320
+ ui.info(` Test (local): restart Codex, select your marketplace in the plugin directory`);
3321
+ ui.info(` Validate: doraval validate ${decision.targetDir}`);
3322
+ if (decision.path === "plugin" && decision.migrateExisting) {
3323
+ ui.info(" (Existing content migrated where confirmed.)");
3324
+ }
3325
+ process.exit(0);
3326
+ }
3327
+ });
3328
+ });
3329
+
2766
3330
  // src/validators/claude/skill.ts
2767
- import { existsSync as existsSync10 } from "fs";
2768
- import { resolve as resolve3 } from "path";
3331
+ import { existsSync as existsSync13 } from "fs";
3332
+ import { resolve as resolve4 } from "path";
2769
3333
  var OPTIONAL_DIRS2, claudeSkillValidator;
2770
3334
  var init_skill = __esm(() => {
2771
3335
  init_frontmatter();
@@ -2777,10 +3341,10 @@ var init_skill = __esm(() => {
2777
3341
  name: "Claude Skill",
2778
3342
  description: "Validates SKILL.md per current Claude Code spec: frontmatter (name/description relaxed to recommended; directory name usually provides the /command), body, supporting files, dynamic injection (!`cmd`), substitutions ($ARGUMENTS, ${CLAUDE_*}), and advanced fields (allowed-tools, context, disable-model-invocation, when_to_use, etc.)",
2779
3343
  detect(dir) {
2780
- return existsSync10(resolve3(dir, "SKILL.md"));
3344
+ return existsSync13(resolve4(dir, "SKILL.md"));
2781
3345
  },
2782
3346
  async validate(dir, _opts) {
2783
- const skillMd = resolve3(dir, "SKILL.md");
3347
+ const skillMd = resolve4(dir, "SKILL.md");
2784
3348
  const raw = await Bun.file(skillMd).text();
2785
3349
  let parsed;
2786
3350
  try {
@@ -2792,32 +3356,108 @@ var init_skill = __esm(() => {
2792
3356
  passes: []
2793
3357
  };
2794
3358
  }
2795
- const existingDirs = OPTIONAL_DIRS2.filter((d) => existsSync10(resolve3(dir, d)));
3359
+ const existingDirs = OPTIONAL_DIRS2.filter((d) => existsSync13(resolve4(dir, d)));
2796
3360
  return validateSkillModel(parsed, { existingDirs: [...existingDirs] });
2797
3361
  }
2798
3362
  };
2799
3363
  });
2800
3364
 
2801
3365
  // src/validators/claude/plugin.ts
2802
- import { existsSync as existsSync11, readdirSync as readdirSync4 } from "fs";
2803
- import { resolve as resolve4, join as join9 } from "path";
2804
- var NAME_REGEX2, RELATIVE_PATH_REGEX, claudePluginValidator;
3366
+ import { existsSync as existsSync14, readdirSync as readdirSync6 } from "fs";
3367
+ import { resolve as resolve5, join as join12 } from "path";
3368
+ function levenshtein(a, b) {
3369
+ if (a === b)
3370
+ return 0;
3371
+ const m = a.length, n = b.length;
3372
+ if (m === 0)
3373
+ return n;
3374
+ if (n === 0)
3375
+ return m;
3376
+ const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
3377
+ for (let i = 0;i <= m; i++)
3378
+ dp[i][0] = i;
3379
+ for (let j = 0;j <= n; j++)
3380
+ dp[0][j] = j;
3381
+ for (let i = 1;i <= m; i++) {
3382
+ for (let j = 1;j <= n; j++) {
3383
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
3384
+ dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost);
3385
+ }
3386
+ }
3387
+ return dp[m][n];
3388
+ }
3389
+ function suggestField(unknown) {
3390
+ const lower = unknown.toLowerCase();
3391
+ for (const k of KNOWN_FIELDS2) {
3392
+ if (k.toLowerCase() === lower)
3393
+ return k;
3394
+ if (levenshtein(k.toLowerCase(), lower) <= 1)
3395
+ return k;
3396
+ if (k.toLowerCase().startsWith(lower.slice(0, 3)) && lower.length > 3)
3397
+ return k;
3398
+ }
3399
+ if (lower === "licence")
3400
+ return "license";
3401
+ if (lower === "dependancies" || lower === "deps")
3402
+ return "dependencies";
3403
+ if (lower === "mcp" || lower === "mcpservers")
3404
+ return "mcpServers";
3405
+ if (lower === "lsp")
3406
+ return "lspServers";
3407
+ if (lower === "outputstyles" || lower === "styles")
3408
+ return "outputStyles";
3409
+ if (lower === "userconfig")
3410
+ return "userConfig";
3411
+ return null;
3412
+ }
3413
+ function isRelativePathLike(v) {
3414
+ if (typeof v !== "string")
3415
+ return false;
3416
+ return RELATIVE_PATH_REGEX.test(v) && !v.includes("..");
3417
+ }
3418
+ var NAME_REGEX2, RELATIVE_PATH_REGEX, KNOWN_FIELDS2, REPLACES_DEFAULT, claudePluginValidator;
2805
3419
  var init_plugin = __esm(() => {
2806
3420
  NAME_REGEX2 = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
2807
3421
  RELATIVE_PATH_REGEX = /^\.\//;
3422
+ KNOWN_FIELDS2 = new Set([
3423
+ "$schema",
3424
+ "name",
3425
+ "displayName",
3426
+ "version",
3427
+ "description",
3428
+ "author",
3429
+ "homepage",
3430
+ "repository",
3431
+ "license",
3432
+ "keywords",
3433
+ "defaultEnabled",
3434
+ "skills",
3435
+ "commands",
3436
+ "agents",
3437
+ "hooks",
3438
+ "mcpServers",
3439
+ "outputStyles",
3440
+ "lspServers",
3441
+ "experimental",
3442
+ "userConfig",
3443
+ "channels",
3444
+ "dependencies"
3445
+ ]);
3446
+ REPLACES_DEFAULT = new Set(["commands", "agents", "outputStyles", "lspServers"]);
2808
3447
  claudePluginValidator = {
2809
3448
  id: "claude:plugin",
2810
3449
  provider: "claude",
2811
3450
  name: "Claude Plugin",
2812
- description: "Validates .claude-plugin/plugin.json manifest, component directories, and structure",
3451
+ description: "Validates .claude-plugin/plugin.json manifest (complete schema per Plugins reference), component path rules (replace vs augment), .claude-plugin/ purity, default dirs, single-root-skill layout, unrecognized fields + suggestions, and structure",
2813
3452
  detect(dir) {
2814
- return existsSync11(resolve4(dir, ".claude-plugin", "plugin.json"));
3453
+ return existsSync14(resolve5(dir, ".claude-plugin", "plugin.json"));
2815
3454
  },
2816
3455
  async validate(dir, _opts) {
2817
3456
  const errors = [];
2818
3457
  const warnings = [];
2819
3458
  const passes = [];
2820
- const manifestPath = resolve4(dir, ".claude-plugin", "plugin.json");
3459
+ const manifestPath = resolve5(dir, ".claude-plugin", "plugin.json");
3460
+ const dotClaudePluginDir = resolve5(dir, ".claude-plugin");
2821
3461
  let manifest;
2822
3462
  try {
2823
3463
  const raw = await Bun.file(manifestPath).text();
@@ -2827,6 +3467,17 @@ var init_plugin = __esm(() => {
2827
3467
  errors.push(".claude-plugin/plugin.json is missing or invalid JSON");
2828
3468
  return { errors, warnings, passes };
2829
3469
  }
3470
+ try {
3471
+ const entries = readdirSync6(dotClaudePluginDir);
3472
+ const unexpected = entries.filter((e) => e !== "plugin.json");
3473
+ if (unexpected.length > 0) {
3474
+ for (const e of unexpected) {
3475
+ 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.`);
3476
+ }
3477
+ } else if (entries.length === 1) {
3478
+ passes.push(".claude-plugin/ contains only plugin.json (correct layout)");
3479
+ }
3480
+ } catch {}
2830
3481
  if (!manifest.name) {
2831
3482
  errors.push('Missing required field: "name"');
2832
3483
  } else {
@@ -2840,10 +3491,12 @@ var init_plugin = __esm(() => {
2840
3491
  if (manifest.version !== undefined) {
2841
3492
  const v = String(manifest.version);
2842
3493
  if (!/^\d+\.\d+\.\d+/.test(v)) {
2843
- errors.push(`Invalid version format: "${v}" \u2014 must be semver (MAJOR.MINOR.PATCH)`);
3494
+ errors.push(`Invalid version format: "${v}" \u2014 must look like semver (MAJOR.MINOR.PATCH) when using explicit versioning`);
2844
3495
  } else {
2845
- passes.push(`version: "${v}"`);
3496
+ passes.push(`version: "${v}" (explicit \u2014 bump on every release to publish updates)`);
2846
3497
  }
3498
+ } else {
3499
+ passes.push("version omitted (git commit SHA used as version key \u2014 every commit becomes an available update)");
2847
3500
  }
2848
3501
  if (manifest.description !== undefined) {
2849
3502
  const desc = String(manifest.description);
@@ -2852,65 +3505,172 @@ var init_plugin = __esm(() => {
2852
3505
  } else {
2853
3506
  passes.push("description field present");
2854
3507
  }
3508
+ } else {
3509
+ warnings.push('Missing "description" (recommended for UI, marketplace listings, and auto-discovery)');
2855
3510
  }
2856
- const checkPaths = (field, value) => {
2857
- const paths = Array.isArray(value) ? value : [value];
2858
- for (const p of paths) {
2859
- const s = String(p);
2860
- if (!RELATIVE_PATH_REGEX.test(s)) {
2861
- errors.push(`${field}: path "${s}" must start with "./" (relative)`);
2862
- } else if (s.includes("..")) {
2863
- errors.push(`${field}: path "${s}" must not use ".." (no parent traversal)`);
2864
- } else if (existsSync11(resolve4(dir, s))) {
2865
- passes.push(`${field}: path "${s}" exists`);
3511
+ if (manifest.displayName !== undefined) {
3512
+ passes.push(`displayName: "${manifest.displayName}" (human UI label; falls back to name)`);
3513
+ }
3514
+ if (manifest.author !== undefined) {
3515
+ const a = manifest.author;
3516
+ if (a && typeof a === "object" && a.name) {
3517
+ passes.push("author present");
3518
+ } else {
3519
+ warnings.push('author should be an object like {"name": "...", "email?": "..."}');
3520
+ }
3521
+ }
3522
+ if (manifest.license !== undefined) {
3523
+ passes.push(`license: "${manifest.license}"`);
3524
+ }
3525
+ if (manifest.keywords !== undefined) {
3526
+ if (Array.isArray(manifest.keywords)) {
3527
+ passes.push(`keywords: [${manifest.keywords.join(", ")}]`);
3528
+ } else {
3529
+ errors.push("keywords must be an array of strings");
3530
+ }
3531
+ }
3532
+ if (manifest.defaultEnabled !== undefined) {
3533
+ passes.push(`defaultEnabled: ${manifest.defaultEnabled}`);
3534
+ }
3535
+ if (manifest.homepage)
3536
+ passes.push("homepage present");
3537
+ if (manifest.repository)
3538
+ passes.push("repository present");
3539
+ const unknown = Object.keys(manifest).filter((k) => !KNOWN_FIELDS2.has(k));
3540
+ for (const k of unknown) {
3541
+ const sug = suggestField(k);
3542
+ const hint = sug ? ` (did you mean "${sug}"?)` : "";
3543
+ warnings.push(`Unrecognized top-level field "${k}"${hint} \u2014 will be ignored at runtime (allowed for cross-tool manifest compatibility).`);
3544
+ }
3545
+ const handleField = (field, val) => {
3546
+ if (val === undefined || val === null)
3547
+ return;
3548
+ if (isRelativePathLike(val) || Array.isArray(val) && val.every(isRelativePathLike)) {
3549
+ const arr = Array.isArray(val) ? val : [val];
3550
+ for (const p of arr) {
3551
+ const s = String(p);
3552
+ if (!RELATIVE_PATH_REGEX.test(s)) {
3553
+ errors.push(`${field}: path "${s}" must start with "./"`);
3554
+ } else if (s.includes("..")) {
3555
+ errors.push(`${field}: path "${s}" must not use ".." (paths are confined to the plugin tree after cache copy)`);
3556
+ } else if (existsSync14(resolve5(dir, s))) {
3557
+ passes.push(`${field}: path "${s}" exists`);
3558
+ } else {
3559
+ warnings.push(`${field}: path "${s}" does not exist on disk`);
3560
+ }
3561
+ }
3562
+ if (field === "skills") {
3563
+ passes.push(`${field}: augments the default skills/ (both are scanned)`);
3564
+ } else if (REPLACES_DEFAULT.has(field)) {
3565
+ passes.push(`${field}: custom path replaces default ${field}/ scan`);
2866
3566
  } else {
2867
- warnings.push(`${field}: path "${s}" does not exist on disk`);
3567
+ passes.push(`${field}: custom path or config (merge rules apply)`);
2868
3568
  }
3569
+ } else if (typeof val === "object") {
3570
+ passes.push(`${field}: inline ${field} config present`);
2869
3571
  }
2870
3572
  };
2871
- for (const field of ["commands", "agents", "hooks", "mcpServers"]) {
2872
- if (manifest[field] !== undefined) {
2873
- checkPaths(field, manifest[field]);
2874
- }
2875
- }
2876
- const skillsDir = resolve4(dir, "skills");
2877
- if (existsSync11(skillsDir)) {
2878
- const skillEntries = readdirSync4(skillsDir, { withFileTypes: true }).filter((e) => e.isDirectory());
2879
- for (const skill of skillEntries) {
2880
- const skillMd = join9(skillsDir, skill.name, "SKILL.md");
2881
- if (existsSync11(skillMd)) {
2882
- passes.push(`skills/${skill.name}/SKILL.md exists`);
3573
+ ["skills", "commands", "agents", "hooks", "mcpServers", "outputStyles", "lspServers"].forEach((f) => {
3574
+ if (manifest[f] !== undefined)
3575
+ handleField(f, manifest[f]);
3576
+ });
3577
+ if (manifest.experimental && typeof manifest.experimental === "object") {
3578
+ const exp = manifest.experimental;
3579
+ if (exp.themes !== undefined)
3580
+ handleField("experimental.themes", exp.themes);
3581
+ if (exp.monitors !== undefined)
3582
+ handleField("experimental.monitors", exp.monitors);
3583
+ passes.push("experimental section present (themes and monitors are experimental components)");
3584
+ }
3585
+ if (manifest.userConfig && typeof manifest.userConfig === "object") {
3586
+ const keys = Object.keys(manifest.userConfig);
3587
+ passes.push(`userConfig: ${keys.length} user-configurable value(s) declared`);
3588
+ for (const k of keys) {
3589
+ const opt = manifest.userConfig[k];
3590
+ if (!opt || !opt.type || !opt.title) {
3591
+ warnings.push(`userConfig.${k} is missing required "type" and/or "title"`);
3592
+ }
3593
+ }
3594
+ }
3595
+ if (Array.isArray(manifest.channels)) {
3596
+ passes.push(`channels: ${manifest.channels.length} channel(s) (each binds to an mcpServer)`);
3597
+ manifest.channels.forEach((ch, i) => {
3598
+ if (!ch?.server)
3599
+ warnings.push(`channels[${i}]: "server" is required and must match an mcpServers key`);
3600
+ });
3601
+ }
3602
+ if (Array.isArray(manifest.dependencies)) {
3603
+ passes.push(`dependencies: declares ${manifest.dependencies.length} plugin dependency/ies`);
3604
+ }
3605
+ const skillsDir = resolve5(dir, "skills");
3606
+ if (existsSync14(skillsDir)) {
3607
+ const entries = readdirSync6(skillsDir, { withFileTypes: true }).filter((e) => e.isDirectory());
3608
+ for (const e of entries) {
3609
+ const md = join12(skillsDir, e.name, "SKILL.md");
3610
+ if (existsSync14(md)) {
3611
+ passes.push(`skills/${e.name}/SKILL.md exists`);
2883
3612
  } else {
2884
- errors.push(`skills/${skill.name}/ missing SKILL.md`);
3613
+ errors.push(`skills/${e.name}/ is missing SKILL.md`);
2885
3614
  }
2886
3615
  }
3616
+ if (manifest.skills !== undefined) {
3617
+ warnings.push('Default skills/ dir co-exists with manifest "skills" \u2014 manifest path is authoritative; default folder ignored for loading');
3618
+ }
2887
3619
  }
2888
- const commandsDir = resolve4(dir, "commands");
2889
- if (existsSync11(commandsDir)) {
2890
- const mdFiles = readdirSync4(commandsDir).filter((f) => f.endsWith(".md"));
2891
- if (mdFiles.length > 0) {
2892
- passes.push(`commands/ has ${mdFiles.length} .md file(s)`);
2893
- } else {
2894
- warnings.push("commands/ directory exists but has no .md files");
3620
+ const commandsDir = resolve5(dir, "commands");
3621
+ if (existsSync14(commandsDir)) {
3622
+ const mds = readdirSync6(commandsDir).filter((f) => f.endsWith(".md"));
3623
+ if (mds.length) {
3624
+ passes.push(`commands/ has ${mds.length} .md file(s)`);
3625
+ }
3626
+ if (manifest.commands !== undefined) {
3627
+ warnings.push('commands/ co-exists with manifest "commands" \u2014 manifest replaces default (dir ignored)');
2895
3628
  }
2896
3629
  }
2897
- const agentsDir = resolve4(dir, "agents");
2898
- if (existsSync11(agentsDir)) {
2899
- const mdFiles = readdirSync4(agentsDir).filter((f) => f.endsWith(".md"));
2900
- if (mdFiles.length > 0) {
2901
- passes.push(`agents/ has ${mdFiles.length} .md file(s)`);
2902
- } else {
2903
- warnings.push("agents/ directory exists but has no .md files");
3630
+ const agentsDir = resolve5(dir, "agents");
3631
+ if (existsSync14(agentsDir)) {
3632
+ const mds = readdirSync6(agentsDir).filter((f) => f.endsWith(".md"));
3633
+ if (mds.length) {
3634
+ passes.push(`agents/ has ${mds.length} .md file(s)`);
3635
+ }
3636
+ if (manifest.agents !== undefined) {
3637
+ warnings.push('agents/ co-exists with manifest "agents" \u2014 manifest replaces default (dir ignored)');
2904
3638
  }
2905
3639
  }
3640
+ if (existsSync14(resolve5(dir, "output-styles"))) {
3641
+ passes.push("output-styles/ directory present");
3642
+ if (manifest.outputStyles)
3643
+ warnings.push("output-styles/ co-exists with manifest outputStyles \u2014 manifest wins");
3644
+ }
3645
+ if (existsSync14(resolve5(dir, "themes")))
3646
+ passes.push("themes/ present (experimental)");
3647
+ if (existsSync14(resolve5(dir, "monitors")) || manifest.experimental?.monitors) {
3648
+ passes.push("monitors config present (experimental)");
3649
+ }
3650
+ if (existsSync14(resolve5(dir, "bin")))
3651
+ passes.push("bin/ present (adds executables to Bash tool $PATH)");
3652
+ if (existsSync14(resolve5(dir, "settings.json")))
3653
+ passes.push("settings.json present (plugin defaults for agent/statusline)");
3654
+ if (existsSync14(resolve5(dir, "README.md")))
3655
+ passes.push("README.md present");
3656
+ if (existsSync14(resolve5(dir, ".mcp.json")))
3657
+ passes.push(".mcp.json present (validated by claude:mcp)");
3658
+ if (existsSync14(resolve5(dir, ".lsp.json")))
3659
+ passes.push(".lsp.json present (validated by claude:lsp when registered)");
3660
+ if (existsSync14(resolve5(dir, "hooks/hooks.json")) || existsSync14(resolve5(dir, "hooks.json"))) {
3661
+ passes.push("hooks config present (validated by claude:hooks)");
3662
+ }
3663
+ if (existsSync14(resolve5(dir, "SKILL.md")) && !existsSync14(skillsDir) && manifest.skills === undefined) {
3664
+ passes.push('Root SKILL.md detected \u2014 plugin will be treated as a single-skill plugin (prefer frontmatter "name" for stable /command)');
3665
+ }
2906
3666
  return { errors, warnings, passes };
2907
3667
  }
2908
3668
  };
2909
3669
  });
2910
3670
 
2911
3671
  // src/validators/claude/marketplace.ts
2912
- import { existsSync as existsSync12, readdirSync as readdirSync5 } from "fs";
2913
- import { resolve as resolve5, join as join10 } from "path";
3672
+ import { existsSync as existsSync15, readdirSync as readdirSync7 } from "fs";
3673
+ import { resolve as resolve6, join as join13 } from "path";
2914
3674
  var claudeMarketplaceValidator;
2915
3675
  var init_marketplace = __esm(() => {
2916
3676
  claudeMarketplaceValidator = {
@@ -2919,16 +3679,16 @@ var init_marketplace = __esm(() => {
2919
3679
  name: "Claude Plugin Marketplace",
2920
3680
  description: "Validates marketplace structure: plugins/ directory with valid plugin subdirectories",
2921
3681
  detect(dir) {
2922
- const pluginsDir = resolve5(dir, "plugins");
2923
- if (!existsSync12(pluginsDir))
3682
+ const pluginsDir = resolve6(dir, "plugins");
3683
+ if (!existsSync15(pluginsDir))
2924
3684
  return false;
2925
3685
  try {
2926
- const entries = readdirSync5(pluginsDir, { withFileTypes: true });
3686
+ const entries = readdirSync7(pluginsDir, { withFileTypes: true });
2927
3687
  for (const entry of entries) {
2928
3688
  if (!entry.isDirectory())
2929
3689
  continue;
2930
- const hasSkills = existsSync12(join10(pluginsDir, entry.name, "skills"));
2931
- const hasManifest = existsSync12(join10(pluginsDir, entry.name, ".claude-plugin", "plugin.json"));
3690
+ const hasSkills = existsSync15(join13(pluginsDir, entry.name, "skills"));
3691
+ const hasManifest = existsSync15(join13(pluginsDir, entry.name, ".claude-plugin", "plugin.json"));
2932
3692
  if (hasSkills || hasManifest)
2933
3693
  return true;
2934
3694
  }
@@ -2939,33 +3699,33 @@ var init_marketplace = __esm(() => {
2939
3699
  const errors = [];
2940
3700
  const warnings = [];
2941
3701
  const passes = [];
2942
- const pluginsDir = resolve5(dir, "plugins");
2943
- if (!existsSync12(pluginsDir)) {
3702
+ const pluginsDir = resolve6(dir, "plugins");
3703
+ if (!existsSync15(pluginsDir)) {
2944
3704
  errors.push("Missing plugins/ directory");
2945
3705
  return { errors, warnings, passes };
2946
3706
  }
2947
3707
  passes.push("plugins/ directory exists");
2948
- const pluginEntries = readdirSync5(pluginsDir, { withFileTypes: true }).filter((e) => e.isDirectory());
3708
+ const pluginEntries = readdirSync7(pluginsDir, { withFileTypes: true }).filter((e) => e.isDirectory());
2949
3709
  if (pluginEntries.length === 0) {
2950
3710
  errors.push("plugins/ directory is empty \u2014 expected at least one plugin");
2951
3711
  return { errors, warnings, passes };
2952
3712
  }
2953
3713
  passes.push(`${pluginEntries.length} plugin(s) found`);
2954
- if (existsSync12(resolve5(dir, "README.md"))) {
3714
+ if (existsSync15(resolve6(dir, "README.md"))) {
2955
3715
  passes.push("README.md exists at marketplace root");
2956
3716
  } else {
2957
3717
  warnings.push("No README.md at marketplace root \u2014 recommended for discoverability");
2958
3718
  }
2959
- if (existsSync12(resolve5(dir, "LICENSE"))) {
3719
+ if (existsSync15(resolve6(dir, "LICENSE"))) {
2960
3720
  passes.push("LICENSE exists at marketplace root");
2961
3721
  } else {
2962
3722
  warnings.push("No LICENSE at marketplace root \u2014 recommended");
2963
3723
  }
2964
3724
  for (const plugin of pluginEntries) {
2965
- const pluginPath = join10(pluginsDir, plugin.name);
2966
- const hasSkills = existsSync12(join10(pluginPath, "skills"));
2967
- const hasManifest = existsSync12(join10(pluginPath, ".claude-plugin", "plugin.json"));
2968
- const hasReadme = existsSync12(join10(pluginPath, "README.md"));
3725
+ const pluginPath = join13(pluginsDir, plugin.name);
3726
+ const hasSkills = existsSync15(join13(pluginPath, "skills"));
3727
+ const hasManifest = existsSync15(join13(pluginPath, ".claude-plugin", "plugin.json"));
3728
+ const hasReadme = existsSync15(join13(pluginPath, "README.md"));
2969
3729
  if (hasManifest || hasSkills) {
2970
3730
  passes.push(`Plugin "${plugin.name}" has ${hasManifest ? "manifest" : "skills/"}`);
2971
3731
  } else {
@@ -2981,35 +3741,55 @@ var init_marketplace = __esm(() => {
2981
3741
  });
2982
3742
 
2983
3743
  // src/validators/claude/hooks.ts
2984
- import { existsSync as existsSync13 } from "fs";
2985
- import { resolve as resolve6 } from "path";
3744
+ import { existsSync as existsSync16 } from "fs";
3745
+ import { resolve as resolve7 } from "path";
2986
3746
  var KNOWN_EVENTS, claudeHooksValidator;
2987
3747
  var init_hooks = __esm(() => {
2988
3748
  KNOWN_EVENTS = [
3749
+ "SessionStart",
3750
+ "Setup",
3751
+ "UserPromptSubmit",
3752
+ "UserPromptExpansion",
2989
3753
  "PreToolUse",
3754
+ "PermissionRequest",
3755
+ "PermissionDenied",
2990
3756
  "PostToolUse",
2991
- "Stop",
3757
+ "PostToolUseFailure",
3758
+ "PostToolBatch",
3759
+ "Notification",
3760
+ "MessageDisplay",
3761
+ "SubagentStart",
2992
3762
  "SubagentStop",
2993
- "SessionStart",
2994
- "SessionEnd",
2995
- "UserPromptSubmit",
3763
+ "TaskCreated",
3764
+ "TaskCompleted",
3765
+ "Stop",
3766
+ "StopFailure",
3767
+ "TeammateIdle",
3768
+ "InstructionsLoaded",
3769
+ "ConfigChange",
3770
+ "CwdChanged",
3771
+ "FileChanged",
3772
+ "WorktreeCreate",
3773
+ "WorktreeRemove",
2996
3774
  "PreCompact",
2997
- "Notification",
2998
- "PermissionRequest"
3775
+ "PostCompact",
3776
+ "Elicitation",
3777
+ "ElicitationResult",
3778
+ "SessionEnd"
2999
3779
  ];
3000
3780
  claudeHooksValidator = {
3001
3781
  id: "claude:hooks",
3002
3782
  provider: "claude",
3003
3783
  name: "Claude Hooks",
3004
- description: "Validates hooks/hooks.json: event names, matcher structure, hook types",
3784
+ description: "Validates hooks/hooks.json (or root hooks.json): all lifecycle events per Plugins reference, hook group structure (matcher + hooks[]), supported hook types (command, http, mcp_tool, prompt, agent)",
3005
3785
  detect(dir) {
3006
- return existsSync13(resolve6(dir, "hooks", "hooks.json")) || existsSync13(resolve6(dir, "hooks.json"));
3786
+ return existsSync16(resolve7(dir, "hooks", "hooks.json")) || existsSync16(resolve7(dir, "hooks.json"));
3007
3787
  },
3008
3788
  async validate(dir, _opts) {
3009
3789
  const errors = [];
3010
3790
  const warnings = [];
3011
3791
  const passes = [];
3012
- const hooksPath = existsSync13(resolve6(dir, "hooks", "hooks.json")) ? resolve6(dir, "hooks", "hooks.json") : resolve6(dir, "hooks.json");
3792
+ const hooksPath = existsSync16(resolve7(dir, "hooks", "hooks.json")) ? resolve7(dir, "hooks", "hooks.json") : resolve7(dir, "hooks.json");
3013
3793
  let config;
3014
3794
  try {
3015
3795
  const raw = await Bun.file(hooksPath).text();
@@ -3024,32 +3804,74 @@ var init_hooks = __esm(() => {
3024
3804
  if (KNOWN_EVENTS.includes(name)) {
3025
3805
  passes.push(`Event "${name}" is a known lifecycle event`);
3026
3806
  } else {
3027
- warnings.push(`Unknown event name: "${name}" \u2014 expected one of: ${KNOWN_EVENTS.join(", ")}`);
3807
+ warnings.push(`Unknown event name: "${name}" \u2014 see full list in Plugins reference (SessionStart, PreToolUse, PostToolUse, Stop, ...)`);
3028
3808
  }
3029
3809
  }
3810
+ for (const [event, groups] of Object.entries(config)) {
3811
+ if (!Array.isArray(groups)) {
3812
+ errors.push(`Event "${event}": value must be an array of hook groups`);
3813
+ continue;
3814
+ }
3815
+ groups.forEach((group, gi) => {
3816
+ if (!group || typeof group !== "object") {
3817
+ errors.push(`${event}[${gi}]: hook group must be an object`);
3818
+ return;
3819
+ }
3820
+ if (group.matcher !== undefined && typeof group.matcher !== "string") {
3821
+ warnings.push(`${event}[${gi}]: "matcher" should be a string (e.g. "Write|Edit" or glob)`);
3822
+ }
3823
+ const hooksArr = group.hooks;
3824
+ if (!Array.isArray(hooksArr)) {
3825
+ errors.push(`${event}[${gi}]: missing or invalid "hooks" array`);
3826
+ return;
3827
+ }
3828
+ hooksArr.forEach((h, hi) => {
3829
+ if (!h || typeof h !== "object" || !h.type) {
3830
+ errors.push(`${event}[${gi}].hooks[${hi}]: must have "type"`);
3831
+ return;
3832
+ }
3833
+ const t = String(h.type);
3834
+ if (!["command", "http", "mcp_tool", "prompt", "agent"].includes(t)) {
3835
+ warnings.push(`${event}[${gi}].hooks[${hi}]: unknown type "${t}" (valid: command, http, mcp_tool, prompt, agent)`);
3836
+ }
3837
+ if (t === "command" && !h.command) {
3838
+ errors.push(`${event}[${gi}].hooks[${hi}]: type=command requires "command"`);
3839
+ }
3840
+ if (t === "http" && !h.url) {
3841
+ errors.push(`${event}[${gi}].hooks[${hi}]: type=http requires "url"`);
3842
+ }
3843
+ if (h.command && typeof h.command === "string" && /\$\{CLAUDE_/.test(h.command)) {
3844
+ passes.push(`${event}[${gi}].hooks[${hi}]: uses plugin env substitution`);
3845
+ }
3846
+ });
3847
+ if (hooksArr.length > 0) {
3848
+ passes.push(`Event "${event}" has ${hooksArr.length} hook action(s)`);
3849
+ }
3850
+ });
3851
+ }
3030
3852
  return { errors, warnings, passes };
3031
3853
  }
3032
3854
  };
3033
3855
  });
3034
3856
 
3035
3857
  // src/validators/claude/mcp.ts
3036
- import { existsSync as existsSync14 } from "fs";
3037
- import { resolve as resolve7 } from "path";
3858
+ import { existsSync as existsSync17 } from "fs";
3859
+ import { resolve as resolve8 } from "path";
3038
3860
  var claudeMcpValidator;
3039
3861
  var init_mcp = __esm(() => {
3040
3862
  claudeMcpValidator = {
3041
3863
  id: "claude:mcp",
3042
3864
  provider: "claude",
3043
3865
  name: "Claude MCP Config",
3044
- description: "Validates .mcp.json: server definitions, required fields, path portability",
3866
+ description: "Validates .mcp.json (or inline via plugin.json mcpServers): server entries (stdio: command+args, or url), env, cwd, ${CLAUDE_PLUGIN_ROOT} etc. substitutions per Plugins reference",
3045
3867
  detect(dir) {
3046
- return existsSync14(resolve7(dir, ".mcp.json"));
3868
+ return existsSync17(resolve8(dir, ".mcp.json"));
3047
3869
  },
3048
3870
  async validate(dir, _opts) {
3049
3871
  const errors = [];
3050
3872
  const warnings = [];
3051
3873
  const passes = [];
3052
- const mcpPath = resolve7(dir, ".mcp.json");
3874
+ const mcpPath = resolve8(dir, ".mcp.json");
3053
3875
  let config;
3054
3876
  try {
3055
3877
  const raw = await Bun.file(mcpPath).text();
@@ -3069,14 +3891,42 @@ var init_mcp = __esm(() => {
3069
3891
  return { errors, warnings, passes };
3070
3892
  }
3071
3893
  passes.push(`${serverNames.length} server(s) defined`);
3894
+ for (const [name, entry] of Object.entries(config)) {
3895
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
3896
+ errors.push(`mcp server "${name}": definition must be an object`);
3897
+ continue;
3898
+ }
3899
+ const e = entry;
3900
+ const hasCommand = typeof e.command === "string";
3901
+ const hasUrl = typeof e.url === "string";
3902
+ if (!hasCommand && !hasUrl) {
3903
+ errors.push(`mcp server "${name}": must have either "command" (for stdio) or "url" (for SSE/HTTP)`);
3904
+ }
3905
+ if (hasCommand && !Array.isArray(e.args)) {
3906
+ warnings.push(`mcp server "${name}": "command" present but no "args" array (ok for some servers)`);
3907
+ }
3908
+ if (hasUrl && hasCommand) {
3909
+ warnings.push(`mcp server "${name}": both "command" and "url" present \u2014 usually one or the other`);
3910
+ }
3911
+ if (e.env && typeof e.env === "object") {
3912
+ passes.push(`mcp server "${name}": has env`);
3913
+ }
3914
+ if (typeof e.cwd === "string") {
3915
+ passes.push(`mcp server "${name}": has cwd`);
3916
+ }
3917
+ const hasSubs = JSON.stringify(e).match(/\$\{CLAUDE_PLUGIN_(ROOT|DATA)|CLAUDE_PROJECT_DIR|user_config\.|ENV_VAR\}/);
3918
+ if (hasSubs) {
3919
+ passes.push(`mcp server "${name}": uses \${CLAUDE_PLUGIN_*} / user_config / env substitution`);
3920
+ }
3921
+ }
3072
3922
  return { errors, warnings, passes };
3073
3923
  }
3074
3924
  };
3075
3925
  });
3076
3926
 
3077
3927
  // src/validators/claude/subagent.ts
3078
- import { existsSync as existsSync15, readdirSync as readdirSync6 } from "fs";
3079
- import { resolve as resolve8, join as join11 } from "path";
3928
+ import { existsSync as existsSync18, readdirSync as readdirSync8 } from "fs";
3929
+ import { resolve as resolve9, join as join14 } from "path";
3080
3930
  var claudeSubagentValidator;
3081
3931
  var init_subagent = __esm(() => {
3082
3932
  init_frontmatter();
@@ -3084,13 +3934,13 @@ var init_subagent = __esm(() => {
3084
3934
  id: "claude:subagent",
3085
3935
  provider: "claude",
3086
3936
  name: "Claude Subagents",
3087
- description: "Validates agents/ directory: .md files with frontmatter and description",
3937
+ description: "Validates agents/*.md (plugin subagents): frontmatter per spec (name, description, model, effort, maxTurns, tools, disallowedTools, skills, memory, background, isolation=worktree), body; warns on disallowed fields (hooks, mcpServers, permissionMode) for security",
3088
3938
  detect(dir) {
3089
- const agentsDir = resolve8(dir, "agents");
3090
- if (!existsSync15(agentsDir))
3939
+ const agentsDir = resolve9(dir, "agents");
3940
+ if (!existsSync18(agentsDir))
3091
3941
  return false;
3092
3942
  try {
3093
- return readdirSync6(agentsDir).some((f) => f.endsWith(".md"));
3943
+ return readdirSync8(agentsDir).some((f) => f.endsWith(".md"));
3094
3944
  } catch {
3095
3945
  return false;
3096
3946
  }
@@ -3099,27 +3949,63 @@ var init_subagent = __esm(() => {
3099
3949
  const errors = [];
3100
3950
  const warnings = [];
3101
3951
  const passes = [];
3102
- const agentsDir = resolve8(dir, "agents");
3103
- const mdFiles = readdirSync6(agentsDir).filter((f) => f.endsWith(".md"));
3952
+ const agentsDir = resolve9(dir, "agents");
3953
+ const mdFiles = readdirSync8(agentsDir).filter((f) => f.endsWith(".md"));
3104
3954
  if (mdFiles.length === 0) {
3105
3955
  errors.push("agents/ directory has no .md files");
3106
3956
  return { errors, warnings, passes };
3107
3957
  }
3108
3958
  passes.push(`${mdFiles.length} agent definition(s) found`);
3959
+ const SUPPORTED = new Set([
3960
+ "name",
3961
+ "description",
3962
+ "model",
3963
+ "effort",
3964
+ "maxTurns",
3965
+ "tools",
3966
+ "disallowedTools",
3967
+ "skills",
3968
+ "memory",
3969
+ "background",
3970
+ "isolation"
3971
+ ]);
3972
+ const DISALLOWED = new Set(["hooks", "mcpServers", "permissionMode"]);
3109
3973
  for (const file of mdFiles) {
3110
- const filePath = join11(agentsDir, file);
3974
+ const filePath = join14(agentsDir, file);
3111
3975
  const raw = await Bun.file(filePath).text();
3112
3976
  try {
3113
3977
  const parsed = parseFrontmatter(raw);
3114
- if (Object.keys(parsed.data).length === 0) {
3115
- warnings.push(`${file}: no YAML frontmatter`);
3116
- } else if (!parsed.data.description) {
3117
- warnings.push(`${file}: missing "description" in frontmatter`);
3978
+ const fm = parsed.data;
3979
+ if (Object.keys(fm).length === 0) {
3980
+ warnings.push(`${file}: no YAML frontmatter (description recommended so Claude knows when to invoke)`);
3118
3981
  } else {
3119
- passes.push(`${file}: has frontmatter with description`);
3982
+ if (fm.description) {
3983
+ passes.push(`${file}: has frontmatter with description`);
3984
+ } else {
3985
+ warnings.push(`${file}: missing "description" in frontmatter`);
3986
+ }
3987
+ const usedSupported = [];
3988
+ Object.keys(fm).forEach((k) => {
3989
+ if (SUPPORTED.has(k))
3990
+ usedSupported.push(k);
3991
+ if (DISALLOWED.has(k)) {
3992
+ errors.push(`${file}: frontmatter "${k}" is not supported for plugin-shipped agents (security restriction)`);
3993
+ }
3994
+ });
3995
+ if (usedSupported.length) {
3996
+ passes.push(`${file}: frontmatter fields: ${usedSupported.join(", ")}`);
3997
+ }
3998
+ if (fm.isolation !== undefined && fm.isolation !== "worktree") {
3999
+ errors.push(`${file}: "isolation" must be "worktree" if present (only supported value for plugin agents)`);
4000
+ }
4001
+ if (fm.name && typeof fm.name === "string") {
4002
+ passes.push(`${file}: name: "${fm.name}"`);
4003
+ }
3120
4004
  }
3121
4005
  if (!parsed.content.trim()) {
3122
4006
  errors.push(`${file}: body is empty`);
4007
+ } else {
4008
+ passes.push(`${file}: has agent system prompt body`);
3123
4009
  }
3124
4010
  } catch {
3125
4011
  errors.push(`${file}: failed to parse`);
@@ -3131,8 +4017,8 @@ var init_subagent = __esm(() => {
3131
4017
  });
3132
4018
 
3133
4019
  // src/validators/claude/command.ts
3134
- import { existsSync as existsSync16, readdirSync as readdirSync7 } from "fs";
3135
- import { resolve as resolve9, join as join12 } from "path";
4020
+ import { existsSync as existsSync19, readdirSync as readdirSync9 } from "fs";
4021
+ import { resolve as resolve10, join as join15 } from "path";
3136
4022
  var claudeCommandValidator;
3137
4023
  var init_command = __esm(() => {
3138
4024
  init_frontmatter();
@@ -3142,11 +4028,11 @@ var init_command = __esm(() => {
3142
4028
  name: "Claude Commands",
3143
4029
  description: "Validates commands/ (or legacy .claude/commands/) .md files: frontmatter (including rich skill fields), description, body",
3144
4030
  detect(dir) {
3145
- const commandsDir = resolve9(dir, "commands");
3146
- if (!existsSync16(commandsDir))
4031
+ const commandsDir = resolve10(dir, "commands");
4032
+ if (!existsSync19(commandsDir))
3147
4033
  return false;
3148
4034
  try {
3149
- return readdirSync7(commandsDir).some((f) => f.endsWith(".md"));
4035
+ return readdirSync9(commandsDir).some((f) => f.endsWith(".md"));
3150
4036
  } catch {
3151
4037
  return false;
3152
4038
  }
@@ -3155,15 +4041,15 @@ var init_command = __esm(() => {
3155
4041
  const errors = [];
3156
4042
  const warnings = [];
3157
4043
  const passes = [];
3158
- const commandsDir = resolve9(dir, "commands");
3159
- const mdFiles = readdirSync7(commandsDir).filter((f) => f.endsWith(".md"));
4044
+ const commandsDir = resolve10(dir, "commands");
4045
+ const mdFiles = readdirSync9(commandsDir).filter((f) => f.endsWith(".md"));
3160
4046
  if (mdFiles.length === 0) {
3161
4047
  errors.push("commands/ directory has no .md files");
3162
4048
  return { errors, warnings, passes };
3163
4049
  }
3164
4050
  passes.push(`${mdFiles.length} command definition(s) found`);
3165
4051
  for (const file of mdFiles) {
3166
- const filePath = join12(commandsDir, file);
4052
+ const filePath = join15(commandsDir, file);
3167
4053
  const raw = await Bun.file(filePath).text();
3168
4054
  try {
3169
4055
  const parsed = parseFrontmatter(raw);
@@ -3192,8 +4078,8 @@ var init_command = __esm(() => {
3192
4078
  });
3193
4079
 
3194
4080
  // src/validators/claude/memory.ts
3195
- import { existsSync as existsSync17 } from "fs";
3196
- import { resolve as resolve10 } from "path";
4081
+ import { existsSync as existsSync20 } from "fs";
4082
+ import { resolve as resolve11 } from "path";
3197
4083
  var claudeMemoryValidator;
3198
4084
  var init_memory = __esm(() => {
3199
4085
  claudeMemoryValidator = {
@@ -3202,13 +4088,13 @@ var init_memory = __esm(() => {
3202
4088
  name: "Claude CLAUDE.md",
3203
4089
  description: "Validates CLAUDE.md: non-empty, length recommendations, @path imports",
3204
4090
  detect(dir) {
3205
- return existsSync17(resolve10(dir, "CLAUDE.md"));
4091
+ return existsSync20(resolve11(dir, "CLAUDE.md"));
3206
4092
  },
3207
4093
  async validate(dir, _opts) {
3208
4094
  const errors = [];
3209
4095
  const warnings = [];
3210
4096
  const passes = [];
3211
- const filePath = resolve10(dir, "CLAUDE.md");
4097
+ const filePath = resolve11(dir, "CLAUDE.md");
3212
4098
  const raw = await Bun.file(filePath).text();
3213
4099
  if (!raw.trim()) {
3214
4100
  errors.push("CLAUDE.md is empty");
@@ -3226,8 +4112,8 @@ var init_memory = __esm(() => {
3226
4112
  let match;
3227
4113
  while ((match = importRegex.exec(raw)) !== null) {
3228
4114
  const importPath = match[1];
3229
- const resolvedImport = resolve10(dir, importPath);
3230
- if (existsSync17(resolvedImport)) {
4115
+ const resolvedImport = resolve11(dir, importPath);
4116
+ if (existsSync20(resolvedImport)) {
3231
4117
  passes.push(`@import "${importPath}" exists`);
3232
4118
  } else {
3233
4119
  warnings.push(`@import "${importPath}" \u2014 file not found at ${resolvedImport}`);
@@ -3238,6 +4124,165 @@ var init_memory = __esm(() => {
3238
4124
  };
3239
4125
  });
3240
4126
 
4127
+ // src/validators/claude/lsp.ts
4128
+ import { existsSync as existsSync21 } from "fs";
4129
+ import { resolve as resolve12 } from "path";
4130
+ var claudeLspValidator;
4131
+ var init_lsp = __esm(() => {
4132
+ claudeLspValidator = {
4133
+ id: "claude:lsp",
4134
+ provider: "claude",
4135
+ name: "Claude LSP Servers",
4136
+ description: "Validates .lsp.json (or plugin.json lspServers): language server configs with required command + extensionToLanguage; optional transport, env, settings, diagnostics etc. (binaries installed separately)",
4137
+ detect(dir) {
4138
+ return existsSync21(resolve12(dir, ".lsp.json")) || existsSync21(resolve12(dir, ".claude-plugin", "plugin.json"));
4139
+ },
4140
+ async validate(dir, _opts) {
4141
+ const errors = [];
4142
+ const warnings = [];
4143
+ const passes = [];
4144
+ let cfg = null;
4145
+ const lspPath = resolve12(dir, ".lsp.json");
4146
+ if (existsSync21(lspPath)) {
4147
+ try {
4148
+ cfg = JSON.parse(await Bun.file(lspPath).text());
4149
+ passes.push(".lsp.json is valid JSON");
4150
+ } catch {
4151
+ errors.push(".lsp.json is invalid JSON");
4152
+ return { errors, warnings, passes };
4153
+ }
4154
+ } else {
4155
+ const manifestPath = resolve12(dir, ".claude-plugin", "plugin.json");
4156
+ if (existsSync21(manifestPath)) {
4157
+ try {
4158
+ const m = JSON.parse(await Bun.file(manifestPath).text());
4159
+ if (m && m.lspServers && typeof m.lspServers === "object") {
4160
+ cfg = m.lspServers;
4161
+ passes.push("lspServers present inline in plugin.json");
4162
+ }
4163
+ } catch {}
4164
+ }
4165
+ }
4166
+ if (!cfg) {
4167
+ if (!existsSync21(lspPath)) {
4168
+ return { errors, warnings, passes };
4169
+ }
4170
+ }
4171
+ if (cfg && typeof cfg === "object") {
4172
+ const langs = Object.keys(cfg);
4173
+ passes.push(`${langs.length} language server(s) configured`);
4174
+ for (const lang of langs) {
4175
+ const entry = cfg[lang];
4176
+ if (!entry || !entry.command) {
4177
+ errors.push(`lsp "${lang}": "command" (the LSP binary) is required`);
4178
+ }
4179
+ if (!entry.extensionToLanguage || typeof entry.extensionToLanguage !== "object") {
4180
+ errors.push(`lsp "${lang}": "extensionToLanguage" map is required (e.g. { ".ts": "typescript" })`);
4181
+ } else {
4182
+ passes.push(`lsp "${lang}": has extensionToLanguage mapping`);
4183
+ }
4184
+ if (entry.diagnostics === false) {
4185
+ passes.push(`lsp "${lang}": diagnostics disabled (navigation only)`);
4186
+ }
4187
+ }
4188
+ }
4189
+ 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".');
4190
+ return { errors, warnings, passes };
4191
+ }
4192
+ };
4193
+ });
4194
+
4195
+ // src/validators/claude/monitors.ts
4196
+ import { existsSync as existsSync22 } from "fs";
4197
+ import { resolve as resolve13 } from "path";
4198
+ var claudeMonitorsValidator;
4199
+ var init_monitors = __esm(() => {
4200
+ claudeMonitorsValidator = {
4201
+ id: "claude:monitors",
4202
+ provider: "claude",
4203
+ name: "Claude Monitors (experimental)",
4204
+ 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.",
4205
+ detect(dir) {
4206
+ return existsSync22(resolve13(dir, "monitors", "monitors.json")) || existsSync22(resolve13(dir, "monitors.json")) || existsSync22(resolve13(dir, ".claude-plugin", "plugin.json"));
4207
+ },
4208
+ async validate(dir, _opts) {
4209
+ const errors = [];
4210
+ const warnings = [];
4211
+ const passes = [];
4212
+ let arr = null;
4213
+ const candidates = [
4214
+ resolve13(dir, "monitors", "monitors.json"),
4215
+ resolve13(dir, "monitors.json")
4216
+ ];
4217
+ for (const p of candidates) {
4218
+ if (existsSync22(p)) {
4219
+ try {
4220
+ const parsed = JSON.parse(await Bun.file(p).text());
4221
+ if (Array.isArray(parsed)) {
4222
+ arr = parsed;
4223
+ passes.push("monitors config is valid JSON array");
4224
+ }
4225
+ break;
4226
+ } catch {
4227
+ errors.push("monitors config is invalid JSON");
4228
+ return { errors, warnings, passes };
4229
+ }
4230
+ }
4231
+ }
4232
+ if (!arr) {
4233
+ const mp = resolve13(dir, ".claude-plugin", "plugin.json");
4234
+ if (existsSync22(mp)) {
4235
+ try {
4236
+ const m = JSON.parse(await Bun.file(mp).text());
4237
+ const exp = m?.experimental;
4238
+ const inline = typeof exp === "string" ? null : exp?.monitors;
4239
+ if (Array.isArray(inline))
4240
+ arr = inline;
4241
+ else if (typeof inline === "string") {
4242
+ passes.push("experimental.monitors declared as path in manifest (content not validated here)");
4243
+ }
4244
+ } catch {}
4245
+ }
4246
+ }
4247
+ if (!arr) {
4248
+ return { errors, warnings, passes };
4249
+ }
4250
+ if (!Array.isArray(arr)) {
4251
+ errors.push("monitors config must be a JSON array");
4252
+ return { errors, warnings, passes };
4253
+ }
4254
+ const seen = new Set;
4255
+ arr.forEach((mon, i) => {
4256
+ if (!mon || typeof mon !== "object") {
4257
+ errors.push(`monitors[${i}]: entry must be an object`);
4258
+ return;
4259
+ }
4260
+ if (!mon.name || typeof mon.name !== "string") {
4261
+ errors.push(`monitors[${i}]: "name" (unique id) is required`);
4262
+ } else {
4263
+ if (seen.has(mon.name))
4264
+ errors.push(`monitors: duplicate name "${mon.name}"`);
4265
+ seen.add(mon.name);
4266
+ }
4267
+ if (!mon.command || typeof mon.command !== "string") {
4268
+ errors.push(`monitors[${i}]: "command" (shell command) is required`);
4269
+ } else if (/\$\{CLAUDE_/.test(mon.command)) {
4270
+ passes.push(`monitors[${i}] "${mon.name || i}": uses CLAUDE_PLUGIN_* substitution`);
4271
+ }
4272
+ if (!mon.description) {
4273
+ warnings.push(`monitors[${i}]: "description" recommended (shown in task panel)`);
4274
+ }
4275
+ if (mon.when && !/^always$|^on-skill-invoke:/.test(String(mon.when))) {
4276
+ warnings.push(`monitors[${i}]: "when" should be "always" (default) or "on-skill-invoke:<skill>"`);
4277
+ }
4278
+ });
4279
+ passes.push(`${arr.length} monitor(s) declared`);
4280
+ 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.");
4281
+ return { errors, warnings, passes };
4282
+ }
4283
+ };
4284
+ });
4285
+
3241
4286
  // src/validators/index.ts
3242
4287
  function resolveFor(forFlag, allValidators = validators) {
3243
4288
  if (!forFlag) {
@@ -3272,6 +4317,8 @@ var init_validators = __esm(() => {
3272
4317
  init_subagent();
3273
4318
  init_command();
3274
4319
  init_memory();
4320
+ init_lsp();
4321
+ init_monitors();
3275
4322
  validators = [
3276
4323
  claudeSkillValidator,
3277
4324
  claudePluginValidator,
@@ -3280,14 +4327,16 @@ var init_validators = __esm(() => {
3280
4327
  claudeMcpValidator,
3281
4328
  claudeSubagentValidator,
3282
4329
  claudeCommandValidator,
3283
- claudeMemoryValidator
4330
+ claudeMemoryValidator,
4331
+ claudeLspValidator,
4332
+ claudeMonitorsValidator
3284
4333
  ];
3285
4334
  });
3286
4335
 
3287
4336
  // src/core/remote.ts
3288
4337
  import { spawnSync as spawnSync4 } from "child_process";
3289
4338
  import { mkdtempSync, rmSync } from "fs";
3290
- import { join as join13 } from "path";
4339
+ import { join as join16 } from "path";
3291
4340
  import { tmpdir } from "os";
3292
4341
  function parseRemoteUrl(input) {
3293
4342
  if (input.startsWith(".") || input.startsWith("/") || input.startsWith("~")) {
@@ -3324,7 +4373,7 @@ function isGhAvailable() {
3324
4373
  return ghAvailable;
3325
4374
  }
3326
4375
  async function cloneToTemp(parsed) {
3327
- const tmpDir = mkdtempSync(join13(tmpdir(), "dora-"));
4376
+ const tmpDir = mkdtempSync(join16(tmpdir(), "dora-"));
3328
4377
  const cleanup = () => {
3329
4378
  try {
3330
4379
  rmSync(tmpDir, { recursive: true, force: true });
@@ -3372,15 +4421,15 @@ var exports_validate_top = {};
3372
4421
  __export(exports_validate_top, {
3373
4422
  default: () => validate_top_default
3374
4423
  });
3375
- import { existsSync as existsSync19 } from "fs";
3376
- import { resolve as resolve11 } from "path";
3377
- var import_picocolors11, validate_top_default;
4424
+ import { existsSync as existsSync24 } from "fs";
4425
+ import { resolve as resolve14 } from "path";
4426
+ var import_picocolors13, validate_top_default;
3378
4427
  var init_validate_top = __esm(() => {
3379
4428
  init_dist();
3380
4429
  init_out();
3381
4430
  init_validators();
3382
4431
  init_remote();
3383
- import_picocolors11 = __toESM(require_picocolors(), 1);
4432
+ import_picocolors13 = __toESM(require_picocolors(), 1);
3384
4433
  validate_top_default = defineCommand({
3385
4434
  meta: {
3386
4435
  name: "validate",
@@ -3420,24 +4469,24 @@ var init_validate_top = __esm(() => {
3420
4469
  let cleanup;
3421
4470
  if (remote) {
3422
4471
  ui.info(`
3423
- Cloning ${import_picocolors11.default.dim(args.path)}...`);
4472
+ Cloning ${import_picocolors13.default.dim(args.path)}...`);
3424
4473
  try {
3425
4474
  const result = await cloneToTemp(remote);
3426
- fullPath = remote.subpath ? resolve11(result.dir, remote.subpath) : result.dir;
4475
+ fullPath = remote.subpath ? resolve14(result.dir, remote.subpath) : result.dir;
3427
4476
  cleanup = result.cleanup;
3428
4477
  } catch (err) {
3429
4478
  const msg = err instanceof Error ? err.message : String(err);
3430
4479
  ui.fail(msg);
3431
4480
  process.exit(1);
3432
4481
  }
3433
- if (!existsSync19(fullPath)) {
4482
+ if (!existsSync24(fullPath)) {
3434
4483
  cleanup();
3435
4484
  ui.fail(`Subdirectory not found in repo: ${remote.subpath}`);
3436
4485
  process.exit(1);
3437
4486
  }
3438
4487
  } else {
3439
- fullPath = resolve11(args.path);
3440
- if (!existsSync19(fullPath)) {
4488
+ fullPath = resolve14(args.path);
4489
+ if (!existsSync24(fullPath)) {
3441
4490
  ui.fail(`Path not found: ${args.path}
3442
4491
 
3443
4492
  Check that the path is correct and the directory exists.`);
@@ -3468,13 +4517,13 @@ Check that the path is correct and the directory exists.`);
3468
4517
  ` + `Available providers:
3469
4518
  ` + providers.map((p) => {
3470
4519
  const pvs = validators.filter((v) => v.provider === p);
3471
- return ` ${import_picocolors11.default.bold(p)}
3472
- ` + pvs.map((v) => ` \u2022 ${import_picocolors11.default.dim(v.id)} \u2014 ${v.description}`).join(`
4520
+ return ` ${import_picocolors13.default.bold(p)}
4521
+ ` + pvs.map((v) => ` \u2022 ${import_picocolors13.default.dim(v.id)} \u2014 ${v.description}`).join(`
3473
4522
  `);
3474
4523
  }).join(`
3475
4524
  `) + `
3476
4525
 
3477
- Use ${import_picocolors11.default.dim("--for <provider>")} or ${import_picocolors11.default.dim("--for <provider:type>")} to target explicitly.`);
4526
+ Use ${import_picocolors13.default.dim("--for <provider>")} or ${import_picocolors13.default.dim("--for <provider:type>")} to target explicitly.`);
3478
4527
  process.exit(1);
3479
4528
  }
3480
4529
  const allResults = [];
@@ -3495,7 +4544,7 @@ Use ${import_picocolors11.default.dim("--for <provider>")} or ${import_picocolor
3495
4544
  } else {
3496
4545
  for (const { id, name, result } of allResults) {
3497
4546
  ui.write(`
3498
- ${import_picocolors11.default.bold("dora validate")} \u2014 ${import_picocolors11.default.white(name)} ${import_picocolors11.default.dim(`(${id})`)}
4547
+ ${import_picocolors13.default.bold("dora validate")} \u2014 ${import_picocolors13.default.white(name)} ${import_picocolors13.default.dim(`(${id})`)}
3499
4548
  `);
3500
4549
  ui.info(` Path: ${args.path}
3501
4550
  `);
@@ -3510,7 +4559,7 @@ Use ${import_picocolors11.default.dim("--for <provider>")} or ${import_picocolor
3510
4559
  }
3511
4560
  if (result.errors.length === 0 && result.warnings.length === 0) {
3512
4561
  ui.write(`
3513
- ${import_picocolors11.default.green("\u2713")} ${import_picocolors11.default.white("All checks passed.")}
4562
+ ${import_picocolors13.default.green("\u2713")} ${import_picocolors13.default.white("All checks passed.")}
3514
4563
  `);
3515
4564
  } else {
3516
4565
  ui.info(`
@@ -3532,16 +4581,16 @@ var exports_init2 = {};
3532
4581
  __export(exports_init2, {
3533
4582
  default: () => init_default2
3534
4583
  });
3535
- import { basename as basename2, join as join14 } from "path";
4584
+ import { basename as basename4, join as join17 } from "path";
3536
4585
  var {spawnSync: spawnSync5 } = globalThis.Bun;
3537
- var import_picocolors12, init_default2;
4586
+ var import_picocolors14, init_default2;
3538
4587
  var init_init2 = __esm(() => {
3539
4588
  init_dist();
3540
4589
  init_out();
3541
4590
  init_journal_config();
3542
4591
  init_journal_remote();
3543
4592
  init_prompt();
3544
- import_picocolors12 = __toESM(require_picocolors(), 1);
4593
+ import_picocolors14 = __toESM(require_picocolors(), 1);
3545
4594
  init_default2 = defineCommand({
3546
4595
  meta: {
3547
4596
  name: "init",
@@ -3568,17 +4617,17 @@ var init_init2 = __esm(() => {
3568
4617
  ui.heading("dora init \u2014 Set up doraval, your journal, and the coding agent dora should use on the fly");
3569
4618
  const ghCheck = ensureGhCli();
3570
4619
  if (!ghCheck.ok) {
3571
- ui.write(` ${import_picocolors12.default.red("\u2717")} ${import_picocolors12.default.white("The GitHub CLI (")}${import_picocolors12.default.bold("gh")}${import_picocolors12.default.white(") is not installed.")}
4620
+ ui.write(` ${import_picocolors14.default.red("\u2717")} ${import_picocolors14.default.white("The GitHub CLI (")}${import_picocolors14.default.bold("gh")}${import_picocolors14.default.white(") is not installed.")}
3572
4621
  `);
3573
- ui.info(` doraval uses ${import_picocolors12.default.bold("gh")} to fetch and sync journal files with GitHub.
4622
+ ui.info(` doraval uses ${import_picocolors14.default.bold("gh")} to fetch and sync journal files with GitHub.
3574
4623
  `);
3575
4624
  ui.info(` Install it:
3576
4625
  `);
3577
- ui.info(` macOS: ${import_picocolors12.default.dim("brew install gh")}`);
3578
- ui.info(` Linux: ${import_picocolors12.default.dim("https://github.com/cli/cli/blob/trunk/docs/install_linux.md")}`);
3579
- ui.info(` Windows: ${import_picocolors12.default.dim("winget install --id GitHub.cli")}
4626
+ ui.info(` macOS: ${import_picocolors14.default.dim("brew install gh")}`);
4627
+ ui.info(` Linux: ${import_picocolors14.default.dim("https://github.com/cli/cli/blob/trunk/docs/install_linux.md")}`);
4628
+ ui.info(` Windows: ${import_picocolors14.default.dim("winget install --id GitHub.cli")}
3580
4629
  `);
3581
- ui.info(` Then authenticate: ${import_picocolors12.default.dim("gh auth login")}
4630
+ ui.info(` Then authenticate: ${import_picocolors14.default.dim("gh auth login")}
3582
4631
  `);
3583
4632
  process.exit(1);
3584
4633
  }
@@ -3591,44 +4640,44 @@ var init_init2 = __esm(() => {
3591
4640
  if (gitOwner) {
3592
4641
  defaultRepo = `${gitOwner}/${gitOwner}.md`;
3593
4642
  if (ghLogin && ghLogin !== gitOwner) {
3594
- sourceNote = ` ${import_picocolors12.default.dim("(from git remote; your active gh account is " + ghLogin + ")")}
4643
+ sourceNote = ` ${import_picocolors14.default.dim("(from git remote; your active gh account is " + ghLogin + ")")}
3595
4644
  `;
3596
4645
  } else {
3597
- sourceNote = ` ${import_picocolors12.default.dim("(from git remote)")}
4646
+ sourceNote = ` ${import_picocolors14.default.dim("(from git remote)")}
3598
4647
  `;
3599
4648
  }
3600
4649
  } else if (ghLogin) {
3601
4650
  defaultRepo = `${ghLogin}/${ghLogin}.md`;
3602
- sourceNote = ` ${import_picocolors12.default.dim("(from your active gh account)")}
4651
+ sourceNote = ` ${import_picocolors14.default.dim("(from your active gh account)")}
3603
4652
  `;
3604
4653
  } else {
3605
- ui.warn(`Not logged in to GitHub. Run ${import_picocolors12.default.dim("gh auth login")} first.
4654
+ ui.warn(`Not logged in to GitHub. Run ${import_picocolors14.default.dim("gh auth login")} first.
3606
4655
  `);
3607
4656
  process.exit(1);
3608
4657
  }
3609
4658
  const existingConfig = await readConfig();
3610
4659
  if (existingConfig?.journal.repo) {
3611
4660
  defaultRepo = existingConfig.journal.repo;
3612
- sourceNote = ` ${import_picocolors12.default.dim("(from your previous journal setup)")}
4661
+ sourceNote = ` ${import_picocolors14.default.dim("(from your previous journal setup)")}
3613
4662
  `;
3614
4663
  }
3615
- ui.info(` Journal repo ${import_picocolors12.default.dim("(owner/name)")}`);
4664
+ ui.info(` Journal repo ${import_picocolors14.default.dim("(owner/name)")}`);
3616
4665
  if (sourceNote)
3617
4666
  ui.write(sourceNote);
3618
4667
  repo = prompt(" >", defaultRepo);
3619
4668
  }
3620
4669
  let project = args.project || process.env.DORAVAL_PROJECT;
3621
4670
  if (!project) {
3622
- const defaultProject = basename2(process.cwd());
4671
+ const defaultProject = basename4(process.cwd());
3623
4672
  project = prompt(" Project name", defaultProject);
3624
4673
  }
3625
4674
  project = sanitizeProjectName(project);
3626
4675
  if (!repoExists(repo)) {
3627
- ui.write(` ${import_picocolors12.default.red("\u2717")} ${import_picocolors12.default.white("Repository")} ${import_picocolors12.default.bold(repo)} ${import_picocolors12.default.white("not found on GitHub.")}
4676
+ ui.write(` ${import_picocolors14.default.red("\u2717")} ${import_picocolors14.default.white("Repository")} ${import_picocolors14.default.bold(repo)} ${import_picocolors14.default.white("not found on GitHub.")}
3628
4677
  `);
3629
4678
  ui.info(` Create it first:
3630
4679
  `);
3631
- ui.info(` ${import_picocolors12.default.dim(`gh repo create ${repo} --private --description "Personal journal for agent decisions"`)}
4680
+ ui.info(` ${import_picocolors14.default.dim(`gh repo create ${repo} --private --description "Personal journal for agent decisions"`)}
3632
4681
  `);
3633
4682
  process.exit(1);
3634
4683
  }
@@ -3636,16 +4685,16 @@ var init_init2 = __esm(() => {
3636
4685
  const alreadyRegistered = existing?.journal.projects[project];
3637
4686
  const isRefresh = alreadyRegistered && args.refresh;
3638
4687
  if (alreadyRegistered && !isRefresh) {
3639
- ui.write(` ${import_picocolors12.default.yellow("\u26A0")} ${import_picocolors12.default.white("Project")} ${import_picocolors12.default.bold(project)} ${import_picocolors12.default.white("is already registered.")}
4688
+ ui.write(` ${import_picocolors14.default.yellow("\u26A0")} ${import_picocolors14.default.white("Project")} ${import_picocolors14.default.bold(project)} ${import_picocolors14.default.white("is already registered.")}
3640
4689
  `);
3641
4690
  ui.info(` Repo: ${existing.journal.repo}
3642
4691
  `);
3643
- ui.info(` To refresh journal files, use ${import_picocolors12.default.dim("dora journal update")} (or ${import_picocolors12.default.dim("dora init --refresh")}).
4692
+ ui.info(` To refresh journal files, use ${import_picocolors14.default.dim("dora journal update")} (or ${import_picocolors14.default.dim("dora init --refresh")}).
3644
4693
  `);
3645
4694
  }
3646
4695
  const journalsDir = getJournalsDir();
3647
4696
  const remotePath = `projects/${project}.md`;
3648
- const localPath = join14(journalsDir, `${project}.md`);
4697
+ const localPath = join17(journalsDir, `${project}.md`);
3649
4698
  const effectiveRepo = isRefresh && !args.repo ? existing.journal.repo : repo;
3650
4699
  const config = existing ?? {
3651
4700
  journal: { repo: effectiveRepo, projects: {} }
@@ -3656,9 +4705,9 @@ var init_init2 = __esm(() => {
3656
4705
  local_path: localPath
3657
4706
  };
3658
4707
  ensureDoravalDirs();
3659
- ui.write(` ${import_picocolors12.default.dim(import_picocolors12.default.gray("Fetching journal files from"))} ${import_picocolors12.default.gray(effectiveRepo)}${import_picocolors12.default.dim(import_picocolors12.default.gray("..."))}
4708
+ ui.write(` ${import_picocolors14.default.dim(import_picocolors14.default.gray("Fetching journal files from"))} ${import_picocolors14.default.gray(effectiveRepo)}${import_picocolors14.default.dim(import_picocolors14.default.gray("..."))}
3660
4709
  `);
3661
- const globalDest = join14(journalsDir, "global.md");
4710
+ const globalDest = join17(journalsDir, "global.md");
3662
4711
  const refreshGlobalRes = await refreshLocalJournalFile(effectiveRepo, "global.md", globalDest);
3663
4712
  let wroteGlobal;
3664
4713
  if (!refreshGlobalRes.ok) {
@@ -3675,7 +4724,7 @@ var init_init2 = __esm(() => {
3675
4724
  if (wroteGlobal) {
3676
4725
  ui.success("global.md");
3677
4726
  } else {
3678
- ui.write(` ${import_picocolors12.default.dim("\xB7")} global.md ${import_picocolors12.default.dim("(not found \u2014 will be created on first sync)")}`);
4727
+ ui.write(` ${import_picocolors14.default.dim("\xB7")} global.md ${import_picocolors14.default.dim("(not found \u2014 will be created on first sync)")}`);
3679
4728
  await Bun.write(globalDest, `# Global Journal
3680
4729
 
3681
4730
  Cross-project principles.
@@ -3697,7 +4746,7 @@ Cross-project principles.
3697
4746
  if (wroteProject) {
3698
4747
  ui.success(remotePath);
3699
4748
  } else {
3700
- ui.write(` ${import_picocolors12.default.dim("\xB7")} ${remotePath} ${import_picocolors12.default.dim("(not found \u2014 will be created on first sync)")}`);
4749
+ ui.write(` ${import_picocolors14.default.dim("\xB7")} ${remotePath} ${import_picocolors14.default.dim("(not found \u2014 will be created on first sync)")}`);
3701
4750
  await Bun.write(localPath, `# ${project} Journal
3702
4751
 
3703
4752
  Project-specific decisions.
@@ -3705,13 +4754,13 @@ Project-specific decisions.
3705
4754
  }
3706
4755
  await writeConfig(config);
3707
4756
  ui.write(`
3708
- ${import_picocolors12.default.green("\u2713")} ${import_picocolors12.default.white("Journal ready for project")} ${import_picocolors12.default.bold(import_picocolors12.default.white(project))}.
4757
+ ${import_picocolors14.default.green("\u2713")} ${import_picocolors14.default.white("Journal ready for project")} ${import_picocolors14.default.bold(import_picocolors14.default.white(project))}.
3709
4758
  `);
3710
4759
  const existingAgent = (await readConfig())?.agent;
3711
4760
  if (existingAgent?.command) {
3712
- ui.write(` ${import_picocolors12.default.bold(import_picocolors12.default.white("Coding agent (already configured)"))}
4761
+ ui.write(` ${import_picocolors14.default.bold(import_picocolors14.default.white("Coding agent (already configured)"))}
3713
4762
  `);
3714
- ui.write(` Current: ${import_picocolors12.default.dim(import_picocolors12.default.gray(existingAgent.command))} template: ${import_picocolors12.default.dim(import_picocolors12.default.gray(existingAgent.prompt_template || "(default)"))}
4763
+ ui.write(` Current: ${import_picocolors14.default.dim(import_picocolors14.default.gray(existingAgent.command))} template: ${import_picocolors14.default.dim(import_picocolors14.default.gray(existingAgent.prompt_template || "(default)"))}
3715
4764
  `);
3716
4765
  const change = prompt(" Reconfigure / change the coding agent for on-the-fly enrichment? (y/N)", "n");
3717
4766
  if (!/^y/i.test(String(change))) {
@@ -3721,16 +4770,16 @@ Project-specific decisions.
3721
4770
  if (existingAgent)
3722
4771
  cfg.agent = existingAgent;
3723
4772
  await writeConfig(cfg);
3724
- ui.write(` ${import_picocolors12.default.green("\u2713")} ${import_picocolors12.default.white("Try:")} ${import_picocolors12.default.dim(import_picocolors12.default.gray('dora journal add "short decision"'))}
4773
+ ui.write(` ${import_picocolors14.default.green("\u2713")} ${import_picocolors14.default.white("Try:")} ${import_picocolors14.default.dim(import_picocolors14.default.gray('dora journal add "short decision"'))}
3725
4774
  `);
3726
4775
  process.exit(0);
3727
4776
  return;
3728
4777
  }
3729
4778
  ui.blank();
3730
4779
  } else {
3731
- ui.write(` ${import_picocolors12.default.bold(import_picocolors12.default.white("Coding agent for journal add"))}
4780
+ ui.write(` ${import_picocolors14.default.bold(import_picocolors14.default.white("Coding agent for journal add"))}
3732
4781
  `);
3733
- ui.info(` When configured, ${import_picocolors12.default.dim(import_picocolors12.default.gray('dora journal add ".."'))} will use your agent to enrich entries with tags and rationale automatically.
4782
+ ui.info(` When configured, ${import_picocolors14.default.dim(import_picocolors14.default.gray('dora journal add ".."'))} will use your agent to enrich entries with tags and rationale automatically.
3734
4783
  `);
3735
4784
  }
3736
4785
  const common = [
@@ -3749,7 +4798,7 @@ Project-specific decisions.
3749
4798
  }
3750
4799
  }
3751
4800
  let agentCmd = detected || "claude";
3752
- ui.write(` Detected / default agent command: ${import_picocolors12.default.dim(import_picocolors12.default.gray(agentCmd))}`);
4801
+ ui.write(` Detected / default agent command: ${import_picocolors14.default.dim(import_picocolors14.default.gray(agentCmd))}`);
3753
4802
  agentCmd = prompt(" Agent command (the binary you run for prompts)", agentCmd);
3754
4803
  let template = detected ? common.find((c) => c.name === detected)?.template || '-p "{{prompt}}" --output-format json' : '-p "{{prompt}}" --output-format json';
3755
4804
  ui.info(` Prompt template (use {{prompt}} placeholder):`);
@@ -3761,84 +4810,255 @@ Project-specific decisions.
3761
4810
  };
3762
4811
  await writeConfig(finalConfig);
3763
4812
  ui.write(`
3764
- ${import_picocolors12.default.green("\u2713")} ${import_picocolors12.default.white("Agent configured.")}
4813
+ ${import_picocolors14.default.green("\u2713")} ${import_picocolors14.default.white("Agent configured.")}
3765
4814
  `);
3766
- ui.info(` Re-run ${import_picocolors12.default.dim(import_picocolors12.default.gray("dora init"))} anytime to change it.
4815
+ ui.info(` Re-run ${import_picocolors14.default.dim(import_picocolors14.default.gray("dora init"))} anytime to change it.
3767
4816
  `);
3768
- ui.info(` Next: ${import_picocolors12.default.dim(import_picocolors12.default.gray('dora journal add ".."'))}, ${import_picocolors12.default.dim(import_picocolors12.default.gray("dora journal list"))}, or ${import_picocolors12.default.dim(import_picocolors12.default.gray("dora journal update"))}.
4817
+ ui.info(` Next: ${import_picocolors14.default.dim(import_picocolors14.default.gray('dora journal add ".."'))}, ${import_picocolors14.default.dim(import_picocolors14.default.gray("dora journal list"))}, or ${import_picocolors14.default.dim(import_picocolors14.default.gray("dora journal update"))}.
3769
4818
  `);
3770
4819
  process.exit(0);
3771
4820
  }
3772
4821
  });
3773
4822
  });
3774
4823
 
3775
- // src/cli/index.ts
3776
- init_dist();
3777
- // package.json
3778
- var package_default = {
3779
- name: "@hacksmith/doraval",
3780
- version: "0.2.21",
3781
- author: "Saif",
3782
- repository: {
3783
- type: "git",
3784
- url: "git+https://github.com/saif-shines/doraval.git"
3785
- },
3786
- devDependencies: {
3787
- "@types/bun": "latest"
3788
- },
3789
- bin: {
3790
- doraval: "bin/doraval-wrapper.js",
3791
- dora: "bin/doraval-wrapper.js"
3792
- },
3793
- description: "The context engineering toolkit for coding agents",
3794
- engines: {
3795
- bun: ">=1.2.0",
3796
- node: ">=14.18.0"
3797
- },
3798
- files: [
3799
- "bin/",
3800
- "dist/",
3801
- "README.md"
3802
- ],
3803
- keywords: [
3804
- "cli",
3805
- "skills",
3806
- "plugins",
3807
- "agent",
3808
- "validation",
3809
- "lint",
3810
- "claude-code",
3811
- "grok",
3812
- "cursor",
3813
- "windsurf",
3814
- "mcp"
3815
- ],
3816
- license: "MIT",
3817
- workspaces: [
3818
- "apps/*"
3819
- ],
3820
- scripts: {
3821
- build: "bun build ./src/cli/index.ts --outfile ./bin/doraval.js --target bun",
3822
- dev: "bun run ./src/cli/index.ts",
3823
- test: "bun test",
3824
- typecheck: "bunx tsc --noEmit --skipLibCheck",
3825
- prepublishOnly: `bun run build && node -e "const p=require('./package.json'),j=require('./jsr.json');if(p.version!==j.version){console.error('Version mismatch: package.json='+p.version+' jsr.json='+j.version);process.exit(1)}"`,
3826
- bump: "bun run scripts/bump.ts",
3827
- release: "bun run scripts/release.ts",
3828
- "jsr:publish": "bunx jsr publish",
3829
- "site:dev": "cd apps/website && bun run dev",
3830
- "site:build": "cd apps/website && bun run build",
3831
- "site:preview": "cd apps/website && bun run preview"
3832
- },
3833
- type: "module",
3834
- dependencies: {
3835
- citty: "^0.2.2",
3836
- picocolors: "^1.1.1"
4824
+ // src/core/update.ts
4825
+ import { execSync } from "child_process";
4826
+ import { existsSync as existsSync25 } from "fs";
4827
+ import { resolve as resolve15 } from "path";
4828
+ import { homedir as homedir2 } from "os";
4829
+ function isInPath(cmd) {
4830
+ try {
4831
+ execSync(`which ${cmd}`, { stdio: "ignore" });
4832
+ return true;
4833
+ } catch {
4834
+ return false;
3837
4835
  }
3838
- };
4836
+ }
4837
+ async function autoDetect() {
4838
+ const execPath = process.execPath;
4839
+ const argv0 = process.argv[0] || "";
4840
+ if (execPath.includes("/Cellar/") || execPath.includes("/homebrew/") || execPath.includes("/opt/homebrew/")) {
4841
+ if (isInPath("brew"))
4842
+ return { type: "homebrew" };
4843
+ }
4844
+ if (execPath.includes("/.npm/") || argv0.includes("npm")) {
4845
+ return { type: "npm" };
4846
+ }
4847
+ if (execPath.includes("/.bun/") || argv0.includes("bun")) {
4848
+ return { type: "bun" };
4849
+ }
4850
+ const home = homedir2();
4851
+ const possibleGlobals = [
4852
+ resolve15(home, ".npm-global/bin/doraval"),
4853
+ resolve15(home, ".bun/bin/doraval")
4854
+ ];
4855
+ for (const p of possibleGlobals) {
4856
+ if (existsSync25(p)) {
4857
+ if (p.includes(".npm"))
4858
+ return { type: "npm" };
4859
+ if (p.includes(".bun"))
4860
+ return { type: "bun" };
4861
+ }
4862
+ }
4863
+ return null;
4864
+ }
4865
+ async function detectInstallMethod(options) {
4866
+ if (options?.force) {
4867
+ if (["homebrew", "npm", "bun"].includes(options.force)) {
4868
+ return { type: options.force };
4869
+ }
4870
+ if (options.force === "npx" || options.force === "bunx") {
4871
+ return { type: "transient", via: options.force };
4872
+ }
4873
+ }
4874
+ const auto = await autoDetect();
4875
+ if (auto)
4876
+ return auto;
4877
+ const marker = await readInstallMarker();
4878
+ if (marker)
4879
+ return marker;
4880
+ return { type: "transient", via: "npx" };
4881
+ }
4882
+ async function fetchLatestVersionInfo() {
4883
+ const npmRes = await fetch("https://registry.npmjs.org/@hacksmith/doraval/latest");
4884
+ if (!npmRes.ok)
4885
+ throw new Error("Failed to fetch from npm");
4886
+ const npmData = await npmRes.json();
4887
+ const version = npmData.version;
4888
+ let summary = "New release available.";
4889
+ try {
4890
+ const ghRes = await fetch("https://api.github.com/repos/saif-shines/doraval/releases/latest", {
4891
+ headers: { "User-Agent": "doraval-update" }
4892
+ });
4893
+ if (ghRes.ok) {
4894
+ const ghData = await ghRes.json();
4895
+ const body = (ghData.body || "").trim();
4896
+ const lines = body.split(`
4897
+ `).filter((l) => l.trim().startsWith("-") || l.trim().startsWith("*")).slice(0, 2);
4898
+ if (lines.length)
4899
+ summary = lines.join(" ").slice(0, 200);
4900
+ else if (body)
4901
+ summary = body.split(`
4902
+ `)[0].slice(0, 150);
4903
+ }
4904
+ } catch {}
4905
+ return { version, summary };
4906
+ }
4907
+ function buildUpgradeCommand(method) {
4908
+ switch (method.type) {
4909
+ case "homebrew":
4910
+ return ["brew", "upgrade", "doraval"];
4911
+ case "npm":
4912
+ return ["npm", "install", "-g", "@hacksmith/doraval@latest"];
4913
+ case "bun":
4914
+ return ["bun", "add", "-g", "@hacksmith/doraval@latest"];
4915
+ default:
4916
+ throw new Error("Cannot build upgrade command for transient installs");
4917
+ }
4918
+ }
4919
+ function shouldUpdate(current, latest) {
4920
+ if (current === latest)
4921
+ return false;
4922
+ const c = current.split(".").map(Number);
4923
+ const l = latest.split(".").map(Number);
4924
+ for (let i = 0;i < 3; i++) {
4925
+ if ((l[i] || 0) > (c[i] || 0))
4926
+ return true;
4927
+ if ((l[i] || 0) < (c[i] || 0))
4928
+ return false;
4929
+ }
4930
+ return false;
4931
+ }
4932
+ async function readInstallMarker() {
4933
+ try {
4934
+ const { readFile } = await import("fs/promises");
4935
+ const data = await readFile(MARKER_PATH, "utf8");
4936
+ const parsed = JSON.parse(data);
4937
+ if (parsed && parsed.type)
4938
+ return parsed;
4939
+ } catch {}
4940
+ return null;
4941
+ }
4942
+ async function writeInstallMarker(method) {
4943
+ try {
4944
+ const { mkdir, writeFile } = await import("fs/promises");
4945
+ const { dirname: dirname2 } = await import("path");
4946
+ await mkdir(dirname2(MARKER_PATH), { recursive: true });
4947
+ await writeFile(MARKER_PATH, JSON.stringify(method, null, 2));
4948
+ } catch {}
4949
+ }
4950
+ var MARKER_PATH;
4951
+ var init_update2 = __esm(() => {
4952
+ MARKER_PATH = resolve15(homedir2(), ".doraval", "install.json");
4953
+ });
4954
+
4955
+ // src/cli/commands/update.ts
4956
+ var exports_update2 = {};
4957
+ __export(exports_update2, {
4958
+ default: () => update_default2
4959
+ });
4960
+ import { spawnSync as spawnSync6 } from "child_process";
4961
+ async function confirmUpdate() {
4962
+ const { createInterface } = await import("readline");
4963
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
4964
+ return new Promise((resolve16) => {
4965
+ rl.question("Update now? (y/N) ", (answer) => {
4966
+ rl.close();
4967
+ resolve16(answer.toLowerCase().startsWith("y"));
4968
+ });
4969
+ });
4970
+ }
4971
+ var update_default2;
4972
+ var init_update3 = __esm(() => {
4973
+ init_dist();
4974
+ init_out();
4975
+ init_update2();
4976
+ update_default2 = defineCommand({
4977
+ meta: {
4978
+ name: "update",
4979
+ description: "Update doraval to the latest version"
4980
+ },
4981
+ args: {
4982
+ check: {
4983
+ type: "boolean",
4984
+ description: "Only check for updates, do not install",
4985
+ default: false
4986
+ },
4987
+ yes: {
4988
+ type: "boolean",
4989
+ description: "Skip confirmation prompt",
4990
+ default: false
4991
+ }
4992
+ },
4993
+ async run({ args }) {
4994
+ const currentVersion = require_package().version;
4995
+ const argv1 = process.argv[1] || "";
4996
+ const isNpx = process.env.npm_execpath?.includes("npx") || argv1.includes("/.npm/") || process.env.npm_lifecycle_script?.includes("npx");
4997
+ const isBunx = process.env.BUN_INSTALL || argv1.includes(".bun/bin/bunx") || argv1.includes("bunx");
4998
+ if (isNpx || isBunx) {
4999
+ ui.info("It looks like you're using doraval via npx or bunx.");
5000
+ ui.info("These always fetch the latest version on the next run.");
5001
+ ui.info("");
5002
+ ui.info("For easier updates, install globally:");
5003
+ ui.info(" brew install saif-shines/tap/doraval");
5004
+ ui.info(" npm install -g @hacksmith/doraval");
5005
+ ui.info(" bun add -g @hacksmith/doraval");
5006
+ process.exit(0);
5007
+ }
5008
+ const method = await detectInstallMethod();
5009
+ if (method.type === "transient") {
5010
+ ui.info("Transient usage detected. Install globally for update support.");
5011
+ process.exit(0);
5012
+ }
5013
+ const latestInfo = await fetchLatestVersionInfo();
5014
+ if (!shouldUpdate(currentVersion, latestInfo.version)) {
5015
+ ui.success(`doraval is up to date (${currentVersion}).`);
5016
+ process.exit(0);
5017
+ }
5018
+ if (args.check) {
5019
+ ui.info(`Update available: ${currentVersion} \u2192 ${latestInfo.version}`);
5020
+ process.exit(1);
5021
+ }
5022
+ ui.heading("doraval update");
5023
+ ui.info(` Current: ${currentVersion}`);
5024
+ ui.info(` Latest: ${latestInfo.version}
5025
+ `);
5026
+ ui.info(` ${latestInfo.summary}
5027
+ `);
5028
+ if (!args.yes) {
5029
+ const confirmed = await confirmUpdate();
5030
+ if (!confirmed) {
5031
+ ui.info("Update cancelled.");
5032
+ process.exit(0);
5033
+ }
5034
+ }
5035
+ const cmd = buildUpgradeCommand(method);
5036
+ ui.info(`Running: ${cmd.join(" ")}
5037
+ `);
5038
+ const result = spawnSync6(cmd[0], cmd.slice(1), { stdio: "inherit" });
5039
+ if (result.status === 0) {
5040
+ ui.success(`Successfully updated to ${latestInfo.version}.`);
5041
+ ui.info("You may need to restart your shell to pick up the new version.");
5042
+ await writeInstallMarker(method);
5043
+ } else {
5044
+ ui.fail("Update failed.");
5045
+ ui.info("Common fixes:");
5046
+ if (cmd[0] === "brew")
5047
+ ui.info(" \u2022 Try: sudo brew upgrade doraval or ensure you are in the admin group");
5048
+ if (cmd[0] === "npm" || cmd[0] === "bun")
5049
+ ui.info(" \u2022 Try running with appropriate permissions or check network.");
5050
+ ui.info(`
5051
+ Raw output above.`);
5052
+ process.exit(result.status ?? 1);
5053
+ }
5054
+ }
5055
+ });
5056
+ });
3839
5057
 
3840
5058
  // src/cli/index.ts
3841
- var import_picocolors13 = __toESM(require_picocolors(), 1);
5059
+ init_dist();
5060
+ var import__package = __toESM(require_package(), 1);
5061
+ var import_picocolors15 = __toESM(require_picocolors(), 1);
3842
5062
  var skill = defineCommand({
3843
5063
  meta: {
3844
5064
  name: "skill",
@@ -3875,12 +5095,26 @@ var claude = defineCommand({
3875
5095
  description: "Claude Code-specific commands (packaging, scaffolding, distribution)"
3876
5096
  },
3877
5097
  subCommands: {
3878
- new: () => Promise.resolve().then(() => (init_new(), exports_new)).then((m) => m.default)
5098
+ new: () => Promise.resolve().then(() => (init_new(), exports_new)).then((m) => m.default),
5099
+ bump: () => Promise.resolve().then(() => (init_bump(), exports_bump)).then((m) => m.default)
3879
5100
  },
3880
5101
  run() {
3881
5102
  showUsage(claude);
3882
5103
  }
3883
5104
  });
5105
+ var codex = defineCommand({
5106
+ meta: {
5107
+ name: "codex",
5108
+ description: "Codex (OpenAI)-specific commands (packaging, scaffolding, distribution)"
5109
+ },
5110
+ subCommands: {
5111
+ new: () => Promise.resolve().then(() => (init_new2(), exports_new2)).then((m) => m.default),
5112
+ bump: () => Promise.resolve().then(() => (init_bump(), exports_bump)).then((m) => m.default)
5113
+ },
5114
+ run() {
5115
+ showUsage(codex);
5116
+ }
5117
+ });
3884
5118
  var doraemonArt = `
3885
5119
  \u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2800\u2880\u28E0\u28E4\u28F4\u28F6\u28F6\u28F6\u28F6\u28F6\u2836\u28F6\u28E4\u28E4\u28C0\u2800\u2800\u2800\u2800\u2800\u2800
3886
5120
  \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
@@ -3896,19 +5130,22 @@ var doraemonArt = `
3896
5130
  var main = defineCommand({
3897
5131
  meta: {
3898
5132
  name: "doraval",
3899
- version: package_default.version,
5133
+ version: import__package.default.version,
3900
5134
  description: "The context engineering toolkit for coding agents"
3901
5135
  },
3902
5136
  subCommands: {
3903
5137
  validate: () => Promise.resolve().then(() => (init_validate_top(), exports_validate_top)).then((m) => m.default),
3904
5138
  init: () => Promise.resolve().then(() => (init_init2(), exports_init2)).then((m) => m.default),
5139
+ bump: () => Promise.resolve().then(() => (init_bump(), exports_bump)).then((m) => m.default),
5140
+ update: () => Promise.resolve().then(() => (init_update3(), exports_update2)).then((m) => m.default),
3905
5141
  skill: () => Promise.resolve(skill),
3906
5142
  journal: () => Promise.resolve(journal),
3907
- claude: () => Promise.resolve(claude)
5143
+ claude: () => Promise.resolve(claude),
5144
+ codex: () => Promise.resolve(codex)
3908
5145
  },
3909
5146
  run() {
3910
5147
  console.log(`
3911
- ` + import_picocolors13.default.blue(doraemonArt) + `
5148
+ ` + import_picocolors15.default.blue(doraemonArt) + `
3912
5149
  `);
3913
5150
  showUsage(main);
3914
5151
  }