claude-skills-cli 0.0.20 → 0.0.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/add-hook.cmd-JWw5UqA3.js +193 -0
- package/dist/add-hook.cmd-JWw5UqA3.js.map +1 -0
- package/dist/dependency-validator-CQJ1hCoU.js +382 -0
- package/dist/dependency-validator-CQJ1hCoU.js.map +1 -0
- package/dist/doctor.cmd-DJpHLDCV.js +142 -0
- package/dist/doctor.cmd-DJpHLDCV.js.map +1 -0
- package/dist/fs-CuGv3Ob2.js +23 -0
- package/dist/fs-CuGv3Ob2.js.map +1 -0
- package/dist/index.js +24 -22
- package/dist/index.js.map +1 -1
- package/dist/init.cmd-BdqImX8b.js +108 -0
- package/dist/init.cmd-BdqImX8b.js.map +1 -0
- package/dist/install.cmd-BaP8k9d2.js +79 -0
- package/dist/install.cmd-BaP8k9d2.js.map +1 -0
- package/dist/output-DiffPD2u.js +104 -0
- package/dist/output-DiffPD2u.js.map +1 -0
- package/dist/package.cmd-BYhkheya.js +107 -0
- package/dist/package.cmd-BYhkheya.js.map +1 -0
- package/dist/stats.cmd-Dd46qjoV.js +154 -0
- package/dist/stats.cmd-Dd46qjoV.js.map +1 -0
- package/dist/{core/templates.js → templates-fyteNbD0.js} +20 -13
- package/dist/templates-fyteNbD0.js.map +1 -0
- package/dist/validate.cmd-BxF4HNsu.js +96 -0
- package/dist/validate.cmd-BxF4HNsu.js.map +1 -0
- package/dist/validator-Dp5x-OjP.js +729 -0
- package/dist/validator-Dp5x-OjP.js.map +1 -0
- package/package.json +34 -35
- package/dist/commands/add-hook.cmd.js +0 -35
- package/dist/commands/add-hook.cmd.js.map +0 -1
- package/dist/commands/add-hook.js +0 -216
- package/dist/commands/add-hook.js.map +0 -1
- package/dist/commands/doctor.cmd.js +0 -19
- package/dist/commands/doctor.cmd.js.map +0 -1
- package/dist/commands/doctor.js +0 -128
- package/dist/commands/doctor.js.map +0 -1
- package/dist/commands/init.cmd.js +0 -37
- package/dist/commands/init.cmd.js.map +0 -1
- package/dist/commands/init.js +0 -86
- package/dist/commands/init.js.map +0 -1
- package/dist/commands/install.cmd.js +0 -23
- package/dist/commands/install.cmd.js.map +0 -1
- package/dist/commands/install.js +0 -64
- package/dist/commands/install.js.map +0 -1
- package/dist/commands/package.cmd.js +0 -28
- package/dist/commands/package.cmd.js.map +0 -1
- package/dist/commands/package.js +0 -134
- package/dist/commands/package.js.map +0 -1
- package/dist/commands/stats.cmd.js +0 -19
- package/dist/commands/stats.cmd.js.map +0 -1
- package/dist/commands/stats.js +0 -154
- package/dist/commands/stats.js.map +0 -1
- package/dist/commands/validate.cmd.js +0 -39
- package/dist/commands/validate.cmd.js.map +0 -1
- package/dist/commands/validate.js +0 -77
- package/dist/commands/validate.js.map +0 -1
- package/dist/core/templates.js.map +0 -1
- package/dist/core/validator.js +0 -252
- package/dist/core/validator.js.map +0 -1
- package/dist/help.js +0 -305
- package/dist/help.js.map +0 -1
- package/dist/skills/.gitkeep +0 -0
- package/dist/types.js +0 -2
- package/dist/types.js.map +0 -1
- package/dist/utils/fs.js +0 -25
- package/dist/utils/fs.js.map +0 -1
- package/dist/utils/output.js +0 -102
- package/dist/utils/output.js.map +0 -1
- package/dist/validators/alignment-validator.js +0 -54
- package/dist/validators/alignment-validator.js.map +0 -1
- package/dist/validators/content-validator.js +0 -156
- package/dist/validators/content-validator.js.map +0 -1
- package/dist/validators/description-validator.js +0 -150
- package/dist/validators/description-validator.js.map +0 -1
- package/dist/validators/file-structure-validator.js +0 -125
- package/dist/validators/file-structure-validator.js.map +0 -1
- package/dist/validators/frontmatter-validator.js +0 -190
- package/dist/validators/frontmatter-validator.js.map +0 -1
- package/dist/validators/references-validator.js +0 -155
- package/dist/validators/references-validator.js.map +0 -1
- package/dist/validators/text-analysis.js +0 -71
- package/dist/validators/text-analysis.js.map +0 -1
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { a as SIMPLE_HOOK_TEMPLATE, n as LLM_EVAL_HOOK_TEMPLATE, t as FORCED_EVAL_HOOK_TEMPLATE } from "./templates-fyteNbD0.js";
|
|
2
|
+
import { r as make_executable, t as ensure_dir } from "./fs-CuGv3Ob2.js";
|
|
3
|
+
import { c as warning, n as error, o as success, r as info } from "./output-DiffPD2u.js";
|
|
4
|
+
import { defineCommand } from "citty";
|
|
5
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
//#region src/commands/add-hook.ts
|
|
9
|
+
const HOOK_TYPES = {
|
|
10
|
+
"simple-inline": {
|
|
11
|
+
name: "Simple Inline",
|
|
12
|
+
success_rate: "20%",
|
|
13
|
+
description: "Echo command in settings.json",
|
|
14
|
+
command: "echo 'INSTRUCTION: If the prompt matches any available skill keywords, use Skill(skill-name) to activate it.'",
|
|
15
|
+
script: null
|
|
16
|
+
},
|
|
17
|
+
"simple-script": {
|
|
18
|
+
name: "Simple Script",
|
|
19
|
+
success_rate: "20%",
|
|
20
|
+
description: "Script file with basic instruction",
|
|
21
|
+
command: null,
|
|
22
|
+
script: "skill-activation-simple.sh",
|
|
23
|
+
template: SIMPLE_HOOK_TEMPLATE
|
|
24
|
+
},
|
|
25
|
+
"forced-eval": {
|
|
26
|
+
name: "Forced Evaluation",
|
|
27
|
+
success_rate: "84%",
|
|
28
|
+
description: "Mandatory 3-step evaluation process",
|
|
29
|
+
command: null,
|
|
30
|
+
script: "skill-activation-forced-eval.sh",
|
|
31
|
+
template: FORCED_EVAL_HOOK_TEMPLATE
|
|
32
|
+
},
|
|
33
|
+
"llm-eval": {
|
|
34
|
+
name: "LLM Evaluation",
|
|
35
|
+
success_rate: "80%",
|
|
36
|
+
description: "Claude API pre-evaluation (requires ANTHROPIC_API_KEY)",
|
|
37
|
+
command: null,
|
|
38
|
+
script: "skill-activation-llm-eval.sh",
|
|
39
|
+
template: LLM_EVAL_HOOK_TEMPLATE
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
function add_hook_command(options = {}) {
|
|
43
|
+
const hook_type = options.type || "forced-eval";
|
|
44
|
+
if (!HOOK_TYPES[hook_type]) {
|
|
45
|
+
error(`Invalid hook type: ${hook_type}`);
|
|
46
|
+
info("Valid types: simple-inline, simple-script, forced-eval, llm-eval");
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
const hook_config = HOOK_TYPES[hook_type];
|
|
50
|
+
let settings_path;
|
|
51
|
+
let hooks_dir;
|
|
52
|
+
let scope;
|
|
53
|
+
if (options.local) {
|
|
54
|
+
settings_path = join(".claude", "settings.local.json");
|
|
55
|
+
hooks_dir = join(".claude", "hooks");
|
|
56
|
+
scope = "project-local";
|
|
57
|
+
} else if (options.project) {
|
|
58
|
+
settings_path = join(".claude", "settings.json");
|
|
59
|
+
hooks_dir = join(".claude", "hooks");
|
|
60
|
+
scope = "project";
|
|
61
|
+
} else {
|
|
62
|
+
settings_path = join(homedir(), ".claude", "settings.json");
|
|
63
|
+
hooks_dir = join(homedir(), ".claude", "hooks");
|
|
64
|
+
scope = "global";
|
|
65
|
+
}
|
|
66
|
+
let settings = {};
|
|
67
|
+
if (existsSync(settings_path)) try {
|
|
68
|
+
const content = readFileSync(settings_path, "utf-8");
|
|
69
|
+
settings = JSON.parse(content);
|
|
70
|
+
if (settings.disableAllHooks) warning("disableAllHooks is set to true in settings — hooks will not run");
|
|
71
|
+
if (settings.hooks?.UserPromptSubmit && Array.isArray(settings.hooks.UserPromptSubmit) && settings.hooks.UserPromptSubmit.length > 0) {
|
|
72
|
+
const userPromptSubmit = settings.hooks.UserPromptSubmit[0];
|
|
73
|
+
const existing_hook = userPromptSubmit.hooks?.find((h) => h.type === "command" && h.command && (h.command.includes("skill-activation") || h.command.includes("skill-forced-eval-hook") || h.command.includes("skill-llm-eval-hook") || h.command.includes("skill-simple-instruction-hook") || h.command.includes("If the prompt matches any available skill keywords")));
|
|
74
|
+
if (existing_hook) {
|
|
75
|
+
warning(`Skill activation hook already exists in ${scope} settings`);
|
|
76
|
+
info(`Current hook: ${existing_hook.command || existing_hook.prompt || "unknown"}`);
|
|
77
|
+
console.log("");
|
|
78
|
+
if (options.force) {
|
|
79
|
+
info("--force flag provided, replacing existing hook...");
|
|
80
|
+
userPromptSubmit.hooks = userPromptSubmit.hooks?.filter((h) => h !== existing_hook);
|
|
81
|
+
} else {
|
|
82
|
+
info("No changes made.");
|
|
83
|
+
info("To replace, run with --force flag or manually remove the existing hook.");
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
} catch (err) {
|
|
89
|
+
error(`Failed to parse ${settings_path}: ${String(err)}`);
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
let hook_handler;
|
|
93
|
+
if (hook_config.script) {
|
|
94
|
+
const script_path = join(hooks_dir, hook_config.script);
|
|
95
|
+
info(`Creating ${hook_config.name} hook script...`);
|
|
96
|
+
try {
|
|
97
|
+
ensure_dir(hooks_dir);
|
|
98
|
+
if (hook_config.template) writeFileSync(script_path, hook_config.template(), "utf-8");
|
|
99
|
+
make_executable(script_path);
|
|
100
|
+
success(`Script created: ${script_path}`);
|
|
101
|
+
} catch (err) {
|
|
102
|
+
error(`Failed to create hook script: ${String(err)}`);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
hook_handler = {
|
|
106
|
+
type: "command",
|
|
107
|
+
command: scope === "global" ? script_path : `.claude/hooks/${hook_config.script}`
|
|
108
|
+
};
|
|
109
|
+
} else hook_handler = {
|
|
110
|
+
type: "command",
|
|
111
|
+
command: hook_config.command
|
|
112
|
+
};
|
|
113
|
+
if (existsSync(settings_path)) {
|
|
114
|
+
const userPromptSubmit = settings.hooks?.UserPromptSubmit?.[0];
|
|
115
|
+
if (userPromptSubmit) {
|
|
116
|
+
if (!userPromptSubmit.hooks) userPromptSubmit.hooks = [];
|
|
117
|
+
userPromptSubmit.hooks.push(hook_handler);
|
|
118
|
+
info(`Adding ${hook_config.name} hook to existing ${scope} settings...`);
|
|
119
|
+
} else {
|
|
120
|
+
settings.hooks = settings.hooks || {};
|
|
121
|
+
settings.hooks.UserPromptSubmit = [{ hooks: [hook_handler] }];
|
|
122
|
+
info(`Adding ${hook_config.name} hook to ${scope} settings...`);
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
info(`Creating ${scope} settings with ${hook_config.name} hook...`);
|
|
126
|
+
settings = { hooks: { UserPromptSubmit: [{ hooks: [hook_handler] }] } };
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
ensure_dir(scope === "global" ? join(homedir(), ".claude") : ".claude");
|
|
130
|
+
writeFileSync(settings_path, JSON.stringify(settings, null, 2), "utf-8");
|
|
131
|
+
success(`${hook_config.name} hook added successfully! (${scope})`);
|
|
132
|
+
console.log("");
|
|
133
|
+
info(`Settings: ${settings_path}`);
|
|
134
|
+
if (hook_config.script) info(`Script: ${join(hooks_dir, hook_config.script)}`);
|
|
135
|
+
console.log("");
|
|
136
|
+
info(`Hook Type: ${hook_config.name}`);
|
|
137
|
+
info(`Success Rate: ${hook_config.success_rate}`);
|
|
138
|
+
info(`Description: ${hook_config.description}`);
|
|
139
|
+
console.log("");
|
|
140
|
+
if (hook_type === "llm-eval") {
|
|
141
|
+
warning("LLM eval hook requires ANTHROPIC_API_KEY environment variable");
|
|
142
|
+
info("Set with: export ANTHROPIC_API_KEY=your-key-here");
|
|
143
|
+
info("Falls back to simple instruction if API key not found");
|
|
144
|
+
console.log("");
|
|
145
|
+
}
|
|
146
|
+
warning("Restart Claude Code for hooks to take effect (hooks are captured at startup)");
|
|
147
|
+
console.log("");
|
|
148
|
+
info("Next steps:");
|
|
149
|
+
console.log(" 1. Create skills with: claude-skills-cli init --name <name>");
|
|
150
|
+
console.log(" 2. Validate with: claude-skills-cli validate <path>");
|
|
151
|
+
} catch (err) {
|
|
152
|
+
error(`Failed to write ${settings_path}: ${String(err)}`);
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
//#endregion
|
|
157
|
+
//#region src/commands/add-hook.cmd.ts
|
|
158
|
+
var add_hook_cmd_default = defineCommand({
|
|
159
|
+
meta: {
|
|
160
|
+
name: "add-hook",
|
|
161
|
+
description: "Add skill activation hook to .claude/settings.json"
|
|
162
|
+
},
|
|
163
|
+
args: {
|
|
164
|
+
local: {
|
|
165
|
+
type: "boolean",
|
|
166
|
+
description: "Install in project .claude/settings.local.json"
|
|
167
|
+
},
|
|
168
|
+
project: {
|
|
169
|
+
type: "boolean",
|
|
170
|
+
description: "Install in project .claude/settings.json"
|
|
171
|
+
},
|
|
172
|
+
type: {
|
|
173
|
+
type: "string",
|
|
174
|
+
description: "Hook type: simple-inline|simple-script|forced-eval|llm-eval"
|
|
175
|
+
},
|
|
176
|
+
force: {
|
|
177
|
+
type: "boolean",
|
|
178
|
+
description: "Replace existing hook without prompting"
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
run({ args }) {
|
|
182
|
+
add_hook_command({
|
|
183
|
+
local: args.local,
|
|
184
|
+
project: args.project,
|
|
185
|
+
type: args.type,
|
|
186
|
+
force: args.force
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
//#endregion
|
|
191
|
+
export { add_hook_cmd_default as default };
|
|
192
|
+
|
|
193
|
+
//# sourceMappingURL=add-hook.cmd-JWw5UqA3.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"add-hook.cmd-JWw5UqA3.js","names":[],"sources":["../src/commands/add-hook.ts","../src/commands/add-hook.cmd.ts"],"sourcesContent":["import { existsSync, readFileSync, writeFileSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\nimport {\n\tFORCED_EVAL_HOOK_TEMPLATE,\n\tLLM_EVAL_HOOK_TEMPLATE,\n\tSIMPLE_HOOK_TEMPLATE,\n} from '../core/templates.js';\nimport type { AddHookOptions } from '../types.js';\nimport { ensure_dir, make_executable } from '../utils/fs.js';\nimport { error, info, success, warning } from '../utils/output.js';\n\ninterface HookHandler {\n\ttype: string;\n\tcommand?: string;\n\tprompt?: string;\n\ttimeout?: number;\n}\n\ninterface SettingsJson {\n\thooks?: {\n\t\tUserPromptSubmit?: Array<{\n\t\t\thooks: Array<HookHandler>;\n\t\t}>;\n\t\t[key: string]: unknown;\n\t};\n\tdisableAllHooks?: boolean;\n\t[key: string]: unknown;\n}\n\nconst HOOK_TYPES = {\n\t'simple-inline': {\n\t\tname: 'Simple Inline',\n\t\tsuccess_rate: '20%',\n\t\tdescription: 'Echo command in settings.json',\n\t\tcommand:\n\t\t\t\"echo 'INSTRUCTION: If the prompt matches any available skill keywords, use Skill(skill-name) to activate it.'\",\n\t\tscript: null,\n\t},\n\t'simple-script': {\n\t\tname: 'Simple Script',\n\t\tsuccess_rate: '20%',\n\t\tdescription: 'Script file with basic instruction',\n\t\tcommand: null,\n\t\tscript: 'skill-activation-simple.sh',\n\t\ttemplate: SIMPLE_HOOK_TEMPLATE,\n\t},\n\t'forced-eval': {\n\t\tname: 'Forced Evaluation',\n\t\tsuccess_rate: '84%',\n\t\tdescription: 'Mandatory 3-step evaluation process',\n\t\tcommand: null,\n\t\tscript: 'skill-activation-forced-eval.sh',\n\t\ttemplate: FORCED_EVAL_HOOK_TEMPLATE,\n\t},\n\t'llm-eval': {\n\t\tname: 'LLM Evaluation',\n\t\tsuccess_rate: '80%',\n\t\tdescription:\n\t\t\t'Claude API pre-evaluation (requires ANTHROPIC_API_KEY)',\n\t\tcommand: null,\n\t\tscript: 'skill-activation-llm-eval.sh',\n\t\ttemplate: LLM_EVAL_HOOK_TEMPLATE,\n\t},\n} as const;\n\ntype HookType = keyof typeof HOOK_TYPES;\n\nexport function add_hook_command(options: AddHookOptions = {}): void {\n\t// Default to forced-eval for best performance\n\tconst hook_type: HookType = (options.type ||\n\t\t'forced-eval') as HookType;\n\n\tif (!HOOK_TYPES[hook_type]) {\n\t\terror(`Invalid hook type: ${hook_type}`);\n\t\tinfo(\n\t\t\t'Valid types: simple-inline, simple-script, forced-eval, llm-eval',\n\t\t);\n\t\tprocess.exit(1);\n\t}\n\n\tconst hook_config = HOOK_TYPES[hook_type];\n\n\t// Determine which settings file to use\n\tlet settings_path: string;\n\tlet hooks_dir: string;\n\tlet scope: string;\n\n\tif (options.local) {\n\t\t// Project-specific local (gitignored)\n\t\tsettings_path = join('.claude', 'settings.local.json');\n\t\thooks_dir = join('.claude', 'hooks');\n\t\tscope = 'project-local';\n\t} else if (options.project) {\n\t\t// Project-specific shared (committed)\n\t\tsettings_path = join('.claude', 'settings.json');\n\t\thooks_dir = join('.claude', 'hooks');\n\t\tscope = 'project';\n\t} else {\n\t\t// Global (default)\n\t\tsettings_path = join(homedir(), '.claude', 'settings.json');\n\t\thooks_dir = join(homedir(), '.claude', 'hooks');\n\t\tscope = 'global';\n\t}\n\n\tlet settings: SettingsJson = {};\n\n\t// Check if settings.json exists and load it\n\tif (existsSync(settings_path)) {\n\t\ttry {\n\t\t\tconst content = readFileSync(settings_path, 'utf-8');\n\t\t\tsettings = JSON.parse(content);\n\n\t\t\t// Warn if all hooks are disabled\n\t\t\tif (settings.disableAllHooks) {\n\t\t\t\twarning(\n\t\t\t\t\t'disableAllHooks is set to true in settings — hooks will not run',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// Check if UserPromptSubmit hook already exists\n\t\t\tif (\n\t\t\t\tsettings.hooks?.UserPromptSubmit &&\n\t\t\t\tArray.isArray(settings.hooks.UserPromptSubmit) &&\n\t\t\t\tsettings.hooks.UserPromptSubmit.length > 0\n\t\t\t) {\n\t\t\t\t// Get the first (and should be only) UserPromptSubmit object\n\t\t\t\tconst userPromptSubmit = settings.hooks.UserPromptSubmit[0];\n\n\t\t\t\t// Find existing skill activation hook (check for various patterns)\n\t\t\t\tconst existing_hook = userPromptSubmit.hooks?.find(\n\t\t\t\t\t(h) =>\n\t\t\t\t\t\th.type === 'command' &&\n\t\t\t\t\t\th.command &&\n\t\t\t\t\t\t(h.command.includes('skill-activation') ||\n\t\t\t\t\t\t\th.command.includes('skill-forced-eval-hook') ||\n\t\t\t\t\t\t\th.command.includes('skill-llm-eval-hook') ||\n\t\t\t\t\t\t\th.command.includes('skill-simple-instruction-hook') ||\n\t\t\t\t\t\t\th.command.includes(\n\t\t\t\t\t\t\t\t'If the prompt matches any available skill keywords',\n\t\t\t\t\t\t\t)),\n\t\t\t\t);\n\n\t\t\t\tif (existing_hook) {\n\t\t\t\t\twarning(\n\t\t\t\t\t\t`Skill activation hook already exists in ${scope} settings`,\n\t\t\t\t\t);\n\t\t\t\t\tinfo(\n\t\t\t\t\t\t`Current hook: ${existing_hook.command || existing_hook.prompt || 'unknown'}`,\n\t\t\t\t\t);\n\t\t\t\t\tconsole.log('');\n\n\t\t\t\t\tif (options.force) {\n\t\t\t\t\t\tinfo('--force flag provided, replacing existing hook...');\n\t\t\t\t\t\t// Remove the existing hook\n\t\t\t\t\t\tuserPromptSubmit.hooks = userPromptSubmit.hooks?.filter(\n\t\t\t\t\t\t\t(h) => h !== existing_hook,\n\t\t\t\t\t\t);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tinfo('No changes made.');\n\t\t\t\t\t\tinfo(\n\t\t\t\t\t\t\t'To replace, run with --force flag or manually remove the existing hook.',\n\t\t\t\t\t\t);\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (err) {\n\t\t\terror(`Failed to parse ${settings_path}: ${String(err)}`);\n\t\t\tprocess.exit(1);\n\t\t}\n\t}\n\n\t// Determine the hook handler to use\n\tlet hook_handler: HookHandler;\n\n\tif (hook_config.script) {\n\t\t// Script-based hook: create the script file\n\t\tconst script_path = join(hooks_dir, hook_config.script);\n\n\t\tinfo(`Creating ${hook_config.name} hook script...`);\n\n\t\ttry {\n\t\t\tensure_dir(hooks_dir);\n\n\t\t\t// Write the script file\n\t\t\tif (hook_config.template) {\n\t\t\t\twriteFileSync(script_path, hook_config.template(), 'utf-8');\n\t\t\t}\n\n\t\t\t// Make it executable\n\t\t\tmake_executable(script_path);\n\n\t\t\tsuccess(`Script created: ${script_path}`);\n\t\t} catch (err) {\n\t\t\terror(`Failed to create hook script: ${String(err)}`);\n\t\t\tprocess.exit(1);\n\t\t}\n\n\t\t// Use relative path for project hooks, absolute for global\n\t\tconst command =\n\t\t\tscope === 'global'\n\t\t\t\t? script_path\n\t\t\t\t: `.claude/hooks/${hook_config.script}`;\n\t\thook_handler = { type: 'command', command };\n\t} else {\n\t\t// Inline command\n\t\thook_handler = { type: 'command', command: hook_config.command! };\n\t}\n\n\t// Update or create settings.json\n\tif (existsSync(settings_path)) {\n\t\t// Add to existing settings\n\t\tconst userPromptSubmit = settings.hooks?.UserPromptSubmit?.[0];\n\n\t\tif (userPromptSubmit) {\n\t\t\t// Add to existing hooks array\n\t\t\tif (!userPromptSubmit.hooks) {\n\t\t\t\tuserPromptSubmit.hooks = [];\n\t\t\t}\n\t\t\tuserPromptSubmit.hooks.push(hook_handler);\n\n\t\t\tinfo(\n\t\t\t\t`Adding ${hook_config.name} hook to existing ${scope} settings...`,\n\t\t\t);\n\t\t} else {\n\t\t\t// Create UserPromptSubmit section\n\t\t\tsettings.hooks = settings.hooks || {};\n\t\t\tsettings.hooks.UserPromptSubmit = [\n\t\t\t\t{\n\t\t\t\t\thooks: [hook_handler],\n\t\t\t\t},\n\t\t\t];\n\n\t\t\tinfo(`Adding ${hook_config.name} hook to ${scope} settings...`);\n\t\t}\n\t} else {\n\t\t// Create new settings.json\n\t\tinfo(\n\t\t\t`Creating ${scope} settings with ${hook_config.name} hook...`,\n\t\t);\n\t\tsettings = {\n\t\t\thooks: {\n\t\t\t\tUserPromptSubmit: [\n\t\t\t\t\t{\n\t\t\t\t\t\thooks: [hook_handler],\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t},\n\t\t};\n\t}\n\n\t// Write settings.json\n\ttry {\n\t\tensure_dir(\n\t\t\tscope === 'global' ? join(homedir(), '.claude') : '.claude',\n\t\t);\n\t\twriteFileSync(\n\t\t\tsettings_path,\n\t\t\tJSON.stringify(settings, null, 2),\n\t\t\t'utf-8',\n\t\t);\n\t\tsuccess(\n\t\t\t`${hook_config.name} hook added successfully! (${scope})`,\n\t\t);\n\t\tconsole.log('');\n\t\tinfo(`Settings: ${settings_path}`);\n\t\tif (hook_config.script) {\n\t\t\tinfo(`Script: ${join(hooks_dir, hook_config.script)}`);\n\t\t}\n\t\tconsole.log('');\n\t\tinfo(`Hook Type: ${hook_config.name}`);\n\t\tinfo(`Success Rate: ${hook_config.success_rate}`);\n\t\tinfo(`Description: ${hook_config.description}`);\n\t\tconsole.log('');\n\n\t\tif (hook_type === 'llm-eval') {\n\t\t\twarning(\n\t\t\t\t'LLM eval hook requires ANTHROPIC_API_KEY environment variable',\n\t\t\t);\n\t\t\tinfo('Set with: export ANTHROPIC_API_KEY=your-key-here');\n\t\t\tinfo('Falls back to simple instruction if API key not found');\n\t\t\tconsole.log('');\n\t\t}\n\n\t\twarning(\n\t\t\t'Restart Claude Code for hooks to take effect (hooks are captured at startup)',\n\t\t);\n\t\tconsole.log('');\n\t\tinfo('Next steps:');\n\t\tconsole.log(\n\t\t\t' 1. Create skills with: claude-skills-cli init --name <name>',\n\t\t);\n\t\tconsole.log(\n\t\t\t' 2. Validate with: claude-skills-cli validate <path>',\n\t\t);\n\t} catch (err) {\n\t\terror(`Failed to write ${settings_path}: ${String(err)}`);\n\t\tprocess.exit(1);\n\t}\n}\n","import { defineCommand } from 'citty';\nimport { add_hook_command } from './add-hook.js';\n\nexport default defineCommand({\n\tmeta: {\n\t\tname: 'add-hook',\n\t\tdescription: 'Add skill activation hook to .claude/settings.json',\n\t},\n\targs: {\n\t\tlocal: {\n\t\t\ttype: 'boolean',\n\t\t\tdescription: 'Install in project .claude/settings.local.json',\n\t\t},\n\t\tproject: {\n\t\t\ttype: 'boolean',\n\t\t\tdescription: 'Install in project .claude/settings.json',\n\t\t},\n\t\ttype: {\n\t\t\ttype: 'string',\n\t\t\tdescription:\n\t\t\t\t'Hook type: simple-inline|simple-script|forced-eval|llm-eval',\n\t\t},\n\t\tforce: {\n\t\t\ttype: 'boolean',\n\t\t\tdescription: 'Replace existing hook without prompting',\n\t\t},\n\t},\n\trun({ args }) {\n\t\tadd_hook_command({\n\t\t\tlocal: args.local,\n\t\t\tproject: args.project,\n\t\t\ttype: args.type as\n\t\t\t\t| 'simple-inline'\n\t\t\t\t| 'simple-script'\n\t\t\t\t| 'forced-eval'\n\t\t\t\t| 'llm-eval'\n\t\t\t\t| undefined,\n\t\t\tforce: args.force,\n\t\t});\n\t},\n});\n"],"mappings":"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"}
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import { u as SEMVER_REGEX } from "./output-DiffPD2u.js";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { execSync } from "node:child_process";
|
|
6
|
+
//#region src/validators/frontmatter-validator.ts
|
|
7
|
+
/**
|
|
8
|
+
* YAML frontmatter validation for SKILL.md
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Check if content has valid YAML frontmatter
|
|
12
|
+
*/
|
|
13
|
+
function has_yaml_frontmatter(content) {
|
|
14
|
+
return content.startsWith("---\n") || content.startsWith("---\r\n");
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Check if description field spans multiple lines in raw YAML
|
|
18
|
+
*/
|
|
19
|
+
function is_description_multiline(frontmatter) {
|
|
20
|
+
const desc_line_match = frontmatter.match(/^description:\s*(.*)$/m);
|
|
21
|
+
if (!desc_line_match) return false;
|
|
22
|
+
if (!desc_line_match[1].trim()) return true;
|
|
23
|
+
const lines = frontmatter.split("\n");
|
|
24
|
+
let found_desc = false;
|
|
25
|
+
for (let i = 0; i < lines.length; i++) {
|
|
26
|
+
const line = lines[i];
|
|
27
|
+
if (line.match(/^description:/)) {
|
|
28
|
+
found_desc = true;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (found_desc) {
|
|
32
|
+
if (line.match(/^\s+\S/) && !line.trim().startsWith("#") && !line.match(/^[a-z_-]+:/)) return true;
|
|
33
|
+
if (line.match(/^[a-z_-]+:/)) break;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Extract frontmatter and body from SKILL.md content
|
|
40
|
+
*/
|
|
41
|
+
function extract_frontmatter(content) {
|
|
42
|
+
if (!has_yaml_frontmatter(content)) return {
|
|
43
|
+
name: null,
|
|
44
|
+
description: null,
|
|
45
|
+
body: content,
|
|
46
|
+
description_is_multiline: false
|
|
47
|
+
};
|
|
48
|
+
const parts = content.split("---\n");
|
|
49
|
+
if (parts.length < 3) return {
|
|
50
|
+
name: null,
|
|
51
|
+
description: null,
|
|
52
|
+
body: content,
|
|
53
|
+
description_is_multiline: false
|
|
54
|
+
};
|
|
55
|
+
const frontmatter = parts[1];
|
|
56
|
+
const body = parts.slice(2).join("---\n");
|
|
57
|
+
const name_match = frontmatter.match(/name:\s*(.+)/);
|
|
58
|
+
const name = name_match ? name_match[1].trim() : null;
|
|
59
|
+
const desc_match = frontmatter.match(/description:\s*(.+?)(?=\n[a-z]+:|$)/s);
|
|
60
|
+
return {
|
|
61
|
+
name,
|
|
62
|
+
description: desc_match ? desc_match[1].trim() : null,
|
|
63
|
+
body,
|
|
64
|
+
description_is_multiline: is_description_multiline(frontmatter)
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Extract array field values from raw YAML frontmatter
|
|
69
|
+
* Handles both inline [a, b] and YAML list (- item) formats
|
|
70
|
+
*/
|
|
71
|
+
function extract_array_field(frontmatter, field) {
|
|
72
|
+
const lines = frontmatter.split("\n");
|
|
73
|
+
let field_index = -1;
|
|
74
|
+
for (let i = 0; i < lines.length; i++) if (lines[i].match(new RegExp(`^${field}:`))) {
|
|
75
|
+
field_index = i;
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
if (field_index === -1) return null;
|
|
79
|
+
const value = lines[field_index].replace(new RegExp(`^${field}:\\s*`), "").trim();
|
|
80
|
+
if (value.startsWith("[")) {
|
|
81
|
+
const inner = value.slice(1, value.lastIndexOf("]"));
|
|
82
|
+
if (!inner.trim()) return [];
|
|
83
|
+
return inner.split(",").map((s) => s.trim().replace(/^["']|["']$/g, ""));
|
|
84
|
+
}
|
|
85
|
+
if (!value) {
|
|
86
|
+
const items = [];
|
|
87
|
+
for (let i = field_index + 1; i < lines.length; i++) {
|
|
88
|
+
const item_match = lines[i].match(/^\s+-\s+(.+)/);
|
|
89
|
+
if (item_match) items.push(item_match[1].trim().replace(/^["']|["']$/g, ""));
|
|
90
|
+
else if (lines[i].match(/^[a-z]/)) break;
|
|
91
|
+
}
|
|
92
|
+
return items;
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Known frontmatter fields per Anthropic spec
|
|
98
|
+
* https://code.claude.com/docs/en/skills#frontmatter-reference
|
|
99
|
+
*/
|
|
100
|
+
const KNOWN_FRONTMATTER_FIELDS = new Set([
|
|
101
|
+
"name",
|
|
102
|
+
"description",
|
|
103
|
+
"version",
|
|
104
|
+
"argument-hint",
|
|
105
|
+
"disable-model-invocation",
|
|
106
|
+
"user-invocable",
|
|
107
|
+
"allowed-tools",
|
|
108
|
+
"model",
|
|
109
|
+
"effort",
|
|
110
|
+
"context",
|
|
111
|
+
"agent",
|
|
112
|
+
"hooks",
|
|
113
|
+
"paths",
|
|
114
|
+
"shell",
|
|
115
|
+
"depends-on-skills",
|
|
116
|
+
"depends-on-mcp",
|
|
117
|
+
"depends-on-packages"
|
|
118
|
+
]);
|
|
119
|
+
/**
|
|
120
|
+
* Fields that expect array values
|
|
121
|
+
*/
|
|
122
|
+
const ARRAY_FIELDS = new Set([
|
|
123
|
+
"depends-on-skills",
|
|
124
|
+
"depends-on-mcp",
|
|
125
|
+
"depends-on-packages"
|
|
126
|
+
]);
|
|
127
|
+
/**
|
|
128
|
+
* Fields with constrained values
|
|
129
|
+
*/
|
|
130
|
+
const FIELD_VALUES = {
|
|
131
|
+
effort: [
|
|
132
|
+
"low",
|
|
133
|
+
"medium",
|
|
134
|
+
"high",
|
|
135
|
+
"max"
|
|
136
|
+
],
|
|
137
|
+
context: ["fork"],
|
|
138
|
+
shell: ["bash", "powershell"],
|
|
139
|
+
"disable-model-invocation": ["true", "false"],
|
|
140
|
+
"user-invocable": ["true", "false"]
|
|
141
|
+
};
|
|
142
|
+
/**
|
|
143
|
+
* Extract top-level field names from raw YAML frontmatter
|
|
144
|
+
*/
|
|
145
|
+
function extract_field_names(frontmatter) {
|
|
146
|
+
const fields = /* @__PURE__ */ new Map();
|
|
147
|
+
for (const line of frontmatter.split("\n")) {
|
|
148
|
+
const match = line.match(/^([a-z][a-z0-9_-]*):\s*(.*)/);
|
|
149
|
+
if (match) fields.set(match[1], match[2].trim());
|
|
150
|
+
}
|
|
151
|
+
return fields;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Validate YAML frontmatter structure
|
|
155
|
+
*/
|
|
156
|
+
function validate_frontmatter_structure(content) {
|
|
157
|
+
const validation = {
|
|
158
|
+
valid: true,
|
|
159
|
+
has_frontmatter: false,
|
|
160
|
+
parse_error: null,
|
|
161
|
+
missing_fields: [],
|
|
162
|
+
unknown_fields: [],
|
|
163
|
+
field_value_warnings: []
|
|
164
|
+
};
|
|
165
|
+
if (!has_yaml_frontmatter(content)) {
|
|
166
|
+
validation.valid = false;
|
|
167
|
+
validation.parse_error = "Missing YAML frontmatter";
|
|
168
|
+
return validation;
|
|
169
|
+
}
|
|
170
|
+
validation.has_frontmatter = true;
|
|
171
|
+
const parts = content.split("---\n");
|
|
172
|
+
if (parts.length < 3) {
|
|
173
|
+
validation.valid = false;
|
|
174
|
+
validation.parse_error = "Malformed YAML frontmatter";
|
|
175
|
+
return validation;
|
|
176
|
+
}
|
|
177
|
+
const frontmatter = parts[1];
|
|
178
|
+
if (!frontmatter.includes("name:")) {
|
|
179
|
+
validation.missing_fields.push("name");
|
|
180
|
+
validation.valid = false;
|
|
181
|
+
}
|
|
182
|
+
if (!frontmatter.includes("description:")) {
|
|
183
|
+
validation.missing_fields.push("description");
|
|
184
|
+
validation.valid = false;
|
|
185
|
+
}
|
|
186
|
+
const fields = extract_field_names(frontmatter);
|
|
187
|
+
for (const [field, value] of fields) {
|
|
188
|
+
if (!KNOWN_FRONTMATTER_FIELDS.has(field)) validation.unknown_fields.push(field);
|
|
189
|
+
const allowed = FIELD_VALUES[field];
|
|
190
|
+
if (allowed && value && !allowed.includes(value)) validation.field_value_warnings.push(`'${field}' has value '${value}' (expected: ${allowed.join(", ")})`);
|
|
191
|
+
if (field === "version" && value) {
|
|
192
|
+
if (value.startsWith("v")) validation.field_value_warnings.push(`'version' should not start with 'v' prefix — use '${value.slice(1)}' instead`);
|
|
193
|
+
else if (!SEMVER_REGEX.test(value)) validation.field_value_warnings.push(`'version' must be valid semver (e.g. 1.0.0) — got '${value}'`);
|
|
194
|
+
}
|
|
195
|
+
if (ARRAY_FIELDS.has(field) && value) {
|
|
196
|
+
if (!value.startsWith("[")) validation.field_value_warnings.push(`'${field}' should be a YAML list (e.g. [item1, item2] or - item) — got plain string '${value}'`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return validation;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Validate skill name format
|
|
203
|
+
*/
|
|
204
|
+
function validate_name_format(name, directory_name) {
|
|
205
|
+
const validation = {
|
|
206
|
+
name,
|
|
207
|
+
format_valid: true,
|
|
208
|
+
directory_name,
|
|
209
|
+
matches_directory: true,
|
|
210
|
+
errors: []
|
|
211
|
+
};
|
|
212
|
+
if (!/^[a-z0-9-]+$/.test(name)) {
|
|
213
|
+
validation.format_valid = false;
|
|
214
|
+
validation.errors.push(`Skill name must be lowercase kebab-case: '${name}'`);
|
|
215
|
+
}
|
|
216
|
+
if (name.startsWith("-") || name.endsWith("-")) {
|
|
217
|
+
validation.format_valid = false;
|
|
218
|
+
validation.errors.push(`Skill name must not start or end with a hyphen: '${name}'`);
|
|
219
|
+
}
|
|
220
|
+
if (name.includes("--")) {
|
|
221
|
+
validation.format_valid = false;
|
|
222
|
+
validation.errors.push(`Skill name must not contain consecutive hyphens: '${name}'`);
|
|
223
|
+
}
|
|
224
|
+
if (name.startsWith("claude") || name.startsWith("anthropic")) {
|
|
225
|
+
validation.format_valid = false;
|
|
226
|
+
validation.errors.push(`Skill name must not use reserved prefix 'claude' or 'anthropic': '${name}'`);
|
|
227
|
+
}
|
|
228
|
+
if (name.includes("<") || name.includes(">")) {
|
|
229
|
+
validation.format_valid = false;
|
|
230
|
+
validation.errors.push(`Skill name must not contain XML angle brackets: '${name}'`);
|
|
231
|
+
}
|
|
232
|
+
if (name !== directory_name) {
|
|
233
|
+
validation.matches_directory = false;
|
|
234
|
+
validation.errors.push(`Skill name '${name}' must match directory name '${directory_name}'`);
|
|
235
|
+
}
|
|
236
|
+
return validation;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Validate hard limits for name and description
|
|
240
|
+
*/
|
|
241
|
+
function validate_hard_limits(name, description) {
|
|
242
|
+
const limits = {
|
|
243
|
+
name: {
|
|
244
|
+
length: 0,
|
|
245
|
+
limit: 64,
|
|
246
|
+
valid: true,
|
|
247
|
+
error: null
|
|
248
|
+
},
|
|
249
|
+
description: {
|
|
250
|
+
length: 0,
|
|
251
|
+
limit: 250,
|
|
252
|
+
valid: true,
|
|
253
|
+
error: null
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
if (name) {
|
|
257
|
+
limits.name.length = name.length;
|
|
258
|
+
if (name.length > 64) {
|
|
259
|
+
limits.name.valid = false;
|
|
260
|
+
limits.name.error = `Skill name too long (max 64 chars): ${name.length}`;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (description) {
|
|
264
|
+
limits.description.length = description.length;
|
|
265
|
+
if (description.length > 250) {
|
|
266
|
+
limits.description.valid = false;
|
|
267
|
+
limits.description.error = `Description too long (max 250 chars — Claude truncates at this limit): ${description.length}`;
|
|
268
|
+
}
|
|
269
|
+
if (description.includes("<") || description.includes(">")) {
|
|
270
|
+
limits.description.valid = false;
|
|
271
|
+
limits.description.error = `Description must not contain XML angle brackets`;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return limits;
|
|
275
|
+
}
|
|
276
|
+
//#endregion
|
|
277
|
+
//#region src/validators/dependency-validator.ts
|
|
278
|
+
/**
|
|
279
|
+
* Dependency validation for skill frontmatter
|
|
280
|
+
* Checks depends-on-skills, depends-on-mcp, depends-on-packages
|
|
281
|
+
*/
|
|
282
|
+
/**
|
|
283
|
+
* Validate all dependency declarations in frontmatter
|
|
284
|
+
*/
|
|
285
|
+
function validate_dependencies(frontmatter_raw) {
|
|
286
|
+
const errors = [];
|
|
287
|
+
const warnings = [];
|
|
288
|
+
const validation = {
|
|
289
|
+
depends_on_skills: [],
|
|
290
|
+
depends_on_mcp: [],
|
|
291
|
+
depends_on_packages: []
|
|
292
|
+
};
|
|
293
|
+
const skills = extract_array_field(frontmatter_raw, "depends-on-skills");
|
|
294
|
+
if (skills !== null) {
|
|
295
|
+
validation.depends_on_skills = check_skill_dependencies(skills);
|
|
296
|
+
for (const dep of validation.depends_on_skills) if (!dep.found) warnings.push(`Skill dependency '${dep.name}' not found in ~/.claude/skills/ or .claude/skills/`);
|
|
297
|
+
}
|
|
298
|
+
const mcp = extract_array_field(frontmatter_raw, "depends-on-mcp");
|
|
299
|
+
if (mcp !== null) {
|
|
300
|
+
validation.depends_on_mcp = check_mcp_dependencies(mcp);
|
|
301
|
+
for (const dep of validation.depends_on_mcp) if (!dep.found) warnings.push(`MCP server dependency '${dep.name}' not found in Claude settings`);
|
|
302
|
+
}
|
|
303
|
+
const packages = extract_array_field(frontmatter_raw, "depends-on-packages");
|
|
304
|
+
if (packages !== null) {
|
|
305
|
+
validation.depends_on_packages = check_package_dependencies(packages);
|
|
306
|
+
for (const dep of validation.depends_on_packages) if (!dep.found) warnings.push(`Package dependency '${dep.name}' not found`);
|
|
307
|
+
}
|
|
308
|
+
return {
|
|
309
|
+
errors,
|
|
310
|
+
warnings,
|
|
311
|
+
validation
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Check if skill dependencies exist on disk
|
|
316
|
+
*/
|
|
317
|
+
function check_skill_dependencies(names) {
|
|
318
|
+
const search_paths = [join(homedir(), ".claude", "skills"), join(".claude", "skills")];
|
|
319
|
+
return names.map((name) => {
|
|
320
|
+
for (const base of search_paths) if (existsSync(join(base, name, "SKILL.md"))) return {
|
|
321
|
+
name,
|
|
322
|
+
found: true,
|
|
323
|
+
path: join(base, name)
|
|
324
|
+
};
|
|
325
|
+
return {
|
|
326
|
+
name,
|
|
327
|
+
found: false,
|
|
328
|
+
path: null
|
|
329
|
+
};
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Check if MCP server dependencies are configured in Claude settings
|
|
334
|
+
*/
|
|
335
|
+
function check_mcp_dependencies(names) {
|
|
336
|
+
const configured_servers = /* @__PURE__ */ new Set();
|
|
337
|
+
const settings_paths = [
|
|
338
|
+
join(homedir(), ".claude", "settings.json"),
|
|
339
|
+
join(".claude", "settings.json"),
|
|
340
|
+
join(".claude", "settings.local.json")
|
|
341
|
+
];
|
|
342
|
+
for (const settings_path of settings_paths) try {
|
|
343
|
+
if (!existsSync(settings_path)) continue;
|
|
344
|
+
const content = readFileSync(settings_path, "utf-8");
|
|
345
|
+
const settings = JSON.parse(content);
|
|
346
|
+
if (settings.mcpServers) for (const key of Object.keys(settings.mcpServers)) configured_servers.add(key);
|
|
347
|
+
} catch {}
|
|
348
|
+
return names.map((name) => ({
|
|
349
|
+
name,
|
|
350
|
+
found: configured_servers.has(name)
|
|
351
|
+
}));
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Check if package dependencies are available
|
|
355
|
+
* Tries node_modules and system PATH
|
|
356
|
+
*/
|
|
357
|
+
function check_package_dependencies(names) {
|
|
358
|
+
return names.map((name) => {
|
|
359
|
+
if (existsSync(join("node_modules", name))) return {
|
|
360
|
+
name,
|
|
361
|
+
found: true,
|
|
362
|
+
type: "node"
|
|
363
|
+
};
|
|
364
|
+
try {
|
|
365
|
+
execSync(`which ${name}`, { stdio: "pipe" });
|
|
366
|
+
return {
|
|
367
|
+
name,
|
|
368
|
+
found: true,
|
|
369
|
+
type: "system"
|
|
370
|
+
};
|
|
371
|
+
} catch {}
|
|
372
|
+
return {
|
|
373
|
+
name,
|
|
374
|
+
found: false,
|
|
375
|
+
type: "unknown"
|
|
376
|
+
};
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
//#endregion
|
|
380
|
+
export { has_yaml_frontmatter as a, validate_hard_limits as c, extract_frontmatter as i, validate_name_format as l, validate_dependencies as n, is_description_multiline as o, extract_array_field as r, validate_frontmatter_structure as s, check_package_dependencies as t };
|
|
381
|
+
|
|
382
|
+
//# sourceMappingURL=dependency-validator-CQJ1hCoU.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dependency-validator-CQJ1hCoU.js","names":[],"sources":["../src/validators/frontmatter-validator.ts","../src/validators/dependency-validator.ts"],"sourcesContent":["/**\n * YAML frontmatter validation for SKILL.md\n */\n\nimport {\n\tDESCRIPTION_MAX_LENGTH,\n\tNAME_MAX_LENGTH,\n\tSEMVER_REGEX,\n} from '../constants.js';\nimport type {\n\tHardLimitValidation,\n\tNameFormatValidation,\n\tYAMLValidation,\n} from '../types.js';\n\nexport interface FrontmatterData {\n\tname: string | null;\n\tdescription: string | null;\n\tbody: string;\n\tdescription_is_multiline: boolean;\n}\n\n/**\n * Check if content has valid YAML frontmatter\n */\nexport function has_yaml_frontmatter(content: string): boolean {\n\treturn content.startsWith('---\\n') || content.startsWith('---\\r\\n');\n}\n\n/**\n * Check if description field spans multiple lines in raw YAML\n */\nexport function is_description_multiline(\n\tfrontmatter: string,\n): boolean {\n\t// Find the description line\n\tconst desc_line_match = frontmatter.match(/^description:\\s*(.*)$/m);\n\tif (!desc_line_match) {\n\t\treturn false;\n\t}\n\n\tconst value_on_same_line = desc_line_match[1].trim();\n\n\t// If there's no value on the same line as \"description:\", it's multi-line\n\tif (!value_on_same_line) {\n\t\treturn true;\n\t}\n\n\t// Check if there are continuation lines (indented lines after description:)\n\t// that are not other YAML fields\n\tconst lines = frontmatter.split('\\n');\n\tlet found_desc = false;\n\tfor (let i = 0; i < lines.length; i++) {\n\t\tconst line = lines[i];\n\t\tif (line.match(/^description:/)) {\n\t\t\tfound_desc = true;\n\t\t\tcontinue;\n\t\t}\n\t\tif (found_desc) {\n\t\t\t// If next line starts with spaces/tabs and is not a comment and is not another field\n\t\t\tif (\n\t\t\t\tline.match(/^\\s+\\S/) &&\n\t\t\t\t!line.trim().startsWith('#') &&\n\t\t\t\t!line.match(/^[a-z_-]+:/)\n\t\t\t) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t\t// Stop checking after we hit another field or end\n\t\t\tif (line.match(/^[a-z_-]+:/)) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n\n\treturn false;\n}\n\n/**\n * Extract frontmatter and body from SKILL.md content\n */\nexport function extract_frontmatter(\n\tcontent: string,\n): FrontmatterData {\n\tif (!has_yaml_frontmatter(content)) {\n\t\treturn {\n\t\t\tname: null,\n\t\t\tdescription: null,\n\t\t\tbody: content,\n\t\t\tdescription_is_multiline: false,\n\t\t};\n\t}\n\n\tconst parts = content.split('---\\n');\n\tif (parts.length < 3) {\n\t\treturn {\n\t\t\tname: null,\n\t\t\tdescription: null,\n\t\t\tbody: content,\n\t\t\tdescription_is_multiline: false,\n\t\t};\n\t}\n\n\tconst frontmatter = parts[1];\n\tconst body = parts.slice(2).join('---\\n');\n\n\t// Extract name\n\tconst name_match = frontmatter.match(/name:\\s*(.+)/);\n\tconst name = name_match ? name_match[1].trim() : null;\n\n\t// Extract description\n\tconst desc_match = frontmatter.match(\n\t\t/description:\\s*(.+?)(?=\\n[a-z]+:|$)/s,\n\t);\n\tconst description = desc_match ? desc_match[1].trim() : null;\n\n\t// Check if description spans multiple lines in the raw YAML\n\tconst description_is_multiline =\n\t\tis_description_multiline(frontmatter);\n\n\treturn { name, description, body, description_is_multiline };\n}\n\n/**\n * Extract array field values from raw YAML frontmatter\n * Handles both inline [a, b] and YAML list (- item) formats\n */\nexport function extract_array_field(\n\tfrontmatter: string,\n\tfield: string,\n): string[] | null {\n\tconst lines = frontmatter.split('\\n');\n\tlet field_index = -1;\n\n\tfor (let i = 0; i < lines.length; i++) {\n\t\tif (lines[i].match(new RegExp(`^${field}:`))) {\n\t\t\tfield_index = i;\n\t\t\tbreak;\n\t\t}\n\t}\n\n\tif (field_index === -1) return null;\n\n\tconst value = lines[field_index]\n\t\t.replace(new RegExp(`^${field}:\\\\s*`), '')\n\t\t.trim();\n\n\t// Inline bracket syntax: [item1, item2]\n\tif (value.startsWith('[')) {\n\t\tconst inner = value.slice(1, value.lastIndexOf(']'));\n\t\tif (!inner.trim()) return [];\n\t\treturn inner\n\t\t\t.split(',')\n\t\t\t.map((s) => s.trim().replace(/^[\"']|[\"']$/g, ''));\n\t}\n\n\t// Empty value — check for YAML list items on following lines\n\tif (!value) {\n\t\tconst items: string[] = [];\n\t\tfor (let i = field_index + 1; i < lines.length; i++) {\n\t\t\tconst item_match = lines[i].match(/^\\s+-\\s+(.+)/);\n\t\t\tif (item_match) {\n\t\t\t\titems.push(item_match[1].trim().replace(/^[\"']|[\"']$/g, ''));\n\t\t\t} else if (lines[i].match(/^[a-z]/)) {\n\t\t\t\tbreak; // next field\n\t\t\t}\n\t\t}\n\t\treturn items;\n\t}\n\n\t// Plain string value (not an array) — return null to signal format error\n\treturn null;\n}\n\n/**\n * Known frontmatter fields per Anthropic spec\n * https://code.claude.com/docs/en/skills#frontmatter-reference\n */\nconst KNOWN_FRONTMATTER_FIELDS = new Set([\n\t'name',\n\t'description',\n\t'version',\n\t'argument-hint',\n\t'disable-model-invocation',\n\t'user-invocable',\n\t'allowed-tools',\n\t'model',\n\t'effort',\n\t'context',\n\t'agent',\n\t'hooks',\n\t'paths',\n\t'shell',\n\t'depends-on-skills',\n\t'depends-on-mcp',\n\t'depends-on-packages',\n]);\n\n/**\n * Fields that expect array values\n */\nconst ARRAY_FIELDS = new Set([\n\t'depends-on-skills',\n\t'depends-on-mcp',\n\t'depends-on-packages',\n]);\n\n/**\n * Fields with constrained values\n */\nconst FIELD_VALUES: Record<string, readonly string[]> = {\n\teffort: ['low', 'medium', 'high', 'max'],\n\tcontext: ['fork'],\n\tshell: ['bash', 'powershell'],\n\t'disable-model-invocation': ['true', 'false'],\n\t'user-invocable': ['true', 'false'],\n};\n\n/**\n * Extract top-level field names from raw YAML frontmatter\n */\nfunction extract_field_names(\n\tfrontmatter: string,\n): Map<string, string> {\n\tconst fields = new Map<string, string>();\n\tfor (const line of frontmatter.split('\\n')) {\n\t\tconst match = line.match(/^([a-z][a-z0-9_-]*):\\s*(.*)/);\n\t\tif (match) {\n\t\t\tfields.set(match[1], match[2].trim());\n\t\t}\n\t}\n\treturn fields;\n}\n\n/**\n * Validate YAML frontmatter structure\n */\nexport function validate_frontmatter_structure(\n\tcontent: string,\n): YAMLValidation {\n\tconst validation: YAMLValidation = {\n\t\tvalid: true,\n\t\thas_frontmatter: false,\n\t\tparse_error: null,\n\t\tmissing_fields: [],\n\t\tunknown_fields: [],\n\t\tfield_value_warnings: [],\n\t};\n\n\tif (!has_yaml_frontmatter(content)) {\n\t\tvalidation.valid = false;\n\t\tvalidation.parse_error = 'Missing YAML frontmatter';\n\t\treturn validation;\n\t}\n\n\tvalidation.has_frontmatter = true;\n\n\tconst parts = content.split('---\\n');\n\tif (parts.length < 3) {\n\t\tvalidation.valid = false;\n\t\tvalidation.parse_error = 'Malformed YAML frontmatter';\n\t\treturn validation;\n\t}\n\n\tconst frontmatter = parts[1];\n\n\t// Check required fields\n\tif (!frontmatter.includes('name:')) {\n\t\tvalidation.missing_fields.push('name');\n\t\tvalidation.valid = false;\n\t}\n\n\tif (!frontmatter.includes('description:')) {\n\t\tvalidation.missing_fields.push('description');\n\t\tvalidation.valid = false;\n\t}\n\n\t// Check for unknown fields\n\tconst fields = extract_field_names(frontmatter);\n\tfor (const [field, value] of fields) {\n\t\tif (!KNOWN_FRONTMATTER_FIELDS.has(field)) {\n\t\t\tvalidation.unknown_fields!.push(field);\n\t\t}\n\n\t\t// Validate constrained field values\n\t\tconst allowed = FIELD_VALUES[field];\n\t\tif (allowed && value && !allowed.includes(value)) {\n\t\t\tvalidation.field_value_warnings!.push(\n\t\t\t\t`'${field}' has value '${value}' (expected: ${allowed.join(', ')})`,\n\t\t\t);\n\t\t}\n\n\t\t// Validate version format\n\t\tif (field === 'version' && value) {\n\t\t\tif (value.startsWith('v')) {\n\t\t\t\tvalidation.field_value_warnings!.push(\n\t\t\t\t\t`'version' should not start with 'v' prefix — use '${value.slice(1)}' instead`,\n\t\t\t\t);\n\t\t\t} else if (!SEMVER_REGEX.test(value)) {\n\t\t\t\tvalidation.field_value_warnings!.push(\n\t\t\t\t\t`'version' must be valid semver (e.g. 1.0.0) — got '${value}'`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\t// Validate array fields have correct format\n\t\tif (ARRAY_FIELDS.has(field) && value) {\n\t\t\t// Inline bracket syntax [a, b] is valid\n\t\t\tif (!value.startsWith('[')) {\n\t\t\t\tvalidation.field_value_warnings!.push(\n\t\t\t\t\t`'${field}' should be a YAML list (e.g. [item1, item2] or - item) — got plain string '${value}'`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn validation;\n}\n\n/**\n * Validate skill name format\n */\nexport function validate_name_format(\n\tname: string,\n\tdirectory_name: string,\n): NameFormatValidation {\n\tconst validation: NameFormatValidation = {\n\t\tname,\n\t\tformat_valid: true,\n\t\tdirectory_name,\n\t\tmatches_directory: true,\n\t\terrors: [],\n\t};\n\n\t// Validate kebab-case format\n\tif (!/^[a-z0-9-]+$/.test(name)) {\n\t\tvalidation.format_valid = false;\n\t\tvalidation.errors.push(\n\t\t\t`Skill name must be lowercase kebab-case: '${name}'`,\n\t\t);\n\t}\n\n\t// Reject leading/trailing hyphens\n\tif (name.startsWith('-') || name.endsWith('-')) {\n\t\tvalidation.format_valid = false;\n\t\tvalidation.errors.push(\n\t\t\t`Skill name must not start or end with a hyphen: '${name}'`,\n\t\t);\n\t}\n\n\t// Reject consecutive hyphens\n\tif (name.includes('--')) {\n\t\tvalidation.format_valid = false;\n\t\tvalidation.errors.push(\n\t\t\t`Skill name must not contain consecutive hyphens: '${name}'`,\n\t\t);\n\t}\n\n\t// Reject reserved prefixes\n\tif (name.startsWith('claude') || name.startsWith('anthropic')) {\n\t\tvalidation.format_valid = false;\n\t\tvalidation.errors.push(\n\t\t\t`Skill name must not use reserved prefix 'claude' or 'anthropic': '${name}'`,\n\t\t);\n\t}\n\n\t// Reject XML angle brackets in name (security)\n\tif (name.includes('<') || name.includes('>')) {\n\t\tvalidation.format_valid = false;\n\t\tvalidation.errors.push(\n\t\t\t`Skill name must not contain XML angle brackets: '${name}'`,\n\t\t);\n\t}\n\n\t// Check name matches directory\n\tif (name !== directory_name) {\n\t\tvalidation.matches_directory = false;\n\t\tvalidation.errors.push(\n\t\t\t`Skill name '${name}' must match directory name '${directory_name}'`,\n\t\t);\n\t}\n\n\treturn validation;\n}\n\n/**\n * Validate hard limits for name and description\n */\nexport function validate_hard_limits(\n\tname: string | null,\n\tdescription: string | null,\n): HardLimitValidation {\n\tconst limits: HardLimitValidation = {\n\t\tname: { length: 0, limit: 64, valid: true, error: null },\n\t\tdescription: {\n\t\t\tlength: 0,\n\t\t\tlimit: DESCRIPTION_MAX_LENGTH,\n\t\t\tvalid: true,\n\t\t\terror: null,\n\t\t},\n\t};\n\n\t// Validate name length\n\tif (name) {\n\t\tlimits.name.length = name.length;\n\t\tif (name.length > NAME_MAX_LENGTH) {\n\t\t\tlimits.name.valid = false;\n\t\t\tlimits.name.error = `Skill name too long (max ${NAME_MAX_LENGTH} chars): ${name.length}`;\n\t\t}\n\t}\n\n\t// Validate description length (truncated at limit in skill listing)\n\tif (description) {\n\t\tlimits.description.length = description.length;\n\t\tif (description.length > DESCRIPTION_MAX_LENGTH) {\n\t\t\tlimits.description.valid = false;\n\t\t\tlimits.description.error = `Description too long (max ${DESCRIPTION_MAX_LENGTH} chars — Claude truncates at this limit): ${description.length}`;\n\t\t}\n\t\t// Reject XML angle brackets in description (security)\n\t\tif (description.includes('<') || description.includes('>')) {\n\t\t\tlimits.description.valid = false;\n\t\t\tlimits.description.error = `Description must not contain XML angle brackets`;\n\t\t}\n\t}\n\n\treturn limits;\n}\n","/**\n * Dependency validation for skill frontmatter\n * Checks depends-on-skills, depends-on-mcp, depends-on-packages\n */\n\nimport { execSync } from 'node:child_process';\nimport { existsSync, readFileSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\nimport type { DependencyValidation } from '../types.js';\nimport { extract_array_field } from './frontmatter-validator.js';\n\ninterface DependencyResult {\n\terrors: string[];\n\twarnings: string[];\n\tvalidation: DependencyValidation;\n}\n\n/**\n * Validate all dependency declarations in frontmatter\n */\nexport function validate_dependencies(\n\tfrontmatter_raw: string,\n): DependencyResult {\n\tconst errors: string[] = [];\n\tconst warnings: string[] = [];\n\tconst validation: DependencyValidation = {\n\t\tdepends_on_skills: [],\n\t\tdepends_on_mcp: [],\n\t\tdepends_on_packages: [],\n\t};\n\n\t// Parse depends-on-skills\n\tconst skills = extract_array_field(\n\t\tfrontmatter_raw,\n\t\t'depends-on-skills',\n\t);\n\tif (skills !== null) {\n\t\tvalidation.depends_on_skills = check_skill_dependencies(skills);\n\t\tfor (const dep of validation.depends_on_skills) {\n\t\t\tif (!dep.found) {\n\t\t\t\twarnings.push(\n\t\t\t\t\t`Skill dependency '${dep.name}' not found in ~/.claude/skills/ or .claude/skills/`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t}\n\n\t// Parse depends-on-mcp\n\tconst mcp = extract_array_field(frontmatter_raw, 'depends-on-mcp');\n\tif (mcp !== null) {\n\t\tvalidation.depends_on_mcp = check_mcp_dependencies(mcp);\n\t\tfor (const dep of validation.depends_on_mcp) {\n\t\t\tif (!dep.found) {\n\t\t\t\twarnings.push(\n\t\t\t\t\t`MCP server dependency '${dep.name}' not found in Claude settings`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t}\n\n\t// Parse depends-on-packages\n\tconst packages = extract_array_field(\n\t\tfrontmatter_raw,\n\t\t'depends-on-packages',\n\t);\n\tif (packages !== null) {\n\t\tvalidation.depends_on_packages =\n\t\t\tcheck_package_dependencies(packages);\n\t\tfor (const dep of validation.depends_on_packages) {\n\t\t\tif (!dep.found) {\n\t\t\t\twarnings.push(`Package dependency '${dep.name}' not found`);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn { errors, warnings, validation };\n}\n\n/**\n * Check if skill dependencies exist on disk\n */\nexport function check_skill_dependencies(\n\tnames: string[],\n): DependencyValidation['depends_on_skills'] {\n\tconst home = homedir();\n\tconst search_paths = [\n\t\tjoin(home, '.claude', 'skills'),\n\t\tjoin('.claude', 'skills'),\n\t];\n\n\treturn names.map((name) => {\n\t\tfor (const base of search_paths) {\n\t\t\tconst skill_md = join(base, name, 'SKILL.md');\n\t\t\tif (existsSync(skill_md)) {\n\t\t\t\treturn { name, found: true, path: join(base, name) };\n\t\t\t}\n\t\t}\n\t\treturn { name, found: false, path: null };\n\t});\n}\n\n/**\n * Check if MCP server dependencies are configured in Claude settings\n */\nexport function check_mcp_dependencies(\n\tnames: string[],\n): DependencyValidation['depends_on_mcp'] {\n\tconst configured_servers = new Set<string>();\n\tconst home = homedir();\n\n\tconst settings_paths = [\n\t\tjoin(home, '.claude', 'settings.json'),\n\t\tjoin('.claude', 'settings.json'),\n\t\tjoin('.claude', 'settings.local.json'),\n\t];\n\n\tfor (const settings_path of settings_paths) {\n\t\ttry {\n\t\t\tif (!existsSync(settings_path)) continue;\n\t\t\tconst content = readFileSync(settings_path, 'utf-8');\n\t\t\tconst settings = JSON.parse(content);\n\t\t\tif (settings.mcpServers) {\n\t\t\t\tfor (const key of Object.keys(settings.mcpServers)) {\n\t\t\t\t\tconfigured_servers.add(key);\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\t// Skip unparseable settings files\n\t\t}\n\t}\n\n\treturn names.map((name) => ({\n\t\tname,\n\t\tfound: configured_servers.has(name),\n\t}));\n}\n\n/**\n * Check if package dependencies are available\n * Tries node_modules and system PATH\n */\nexport function check_package_dependencies(\n\tnames: string[],\n): DependencyValidation['depends_on_packages'] {\n\treturn names.map((name) => {\n\t\t// Check node_modules\n\t\tif (existsSync(join('node_modules', name))) {\n\t\t\treturn { name, found: true, type: 'node' as const };\n\t\t}\n\n\t\t// Check system PATH\n\t\ttry {\n\t\t\texecSync(`which ${name}`, { stdio: 'pipe' });\n\t\t\treturn { name, found: true, type: 'system' as const };\n\t\t} catch {\n\t\t\t// Not in PATH\n\t\t}\n\n\t\treturn { name, found: false, type: 'unknown' as const };\n\t});\n}\n"],"mappings":"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"}
|