claude-skills-cli 0.0.19 → 0.0.21
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-B6iZtoPi.js +193 -0
- package/dist/add-hook.cmd-B6iZtoPi.js.map +1 -0
- package/dist/doctor.cmd-CkNw6ine.js +119 -0
- package/dist/doctor.cmd-CkNw6ine.js.map +1 -0
- package/dist/frontmatter-validator-DO686mla.js +226 -0
- package/dist/frontmatter-validator-DO686mla.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-BoeuCgQP.js +108 -0
- package/dist/init.cmd-BoeuCgQP.js.map +1 -0
- package/dist/install.cmd-CH7yZ92g.js +79 -0
- package/dist/install.cmd-CH7yZ92g.js.map +1 -0
- package/dist/output-Dz8fk6Gu.js +102 -0
- package/dist/output-Dz8fk6Gu.js.map +1 -0
- package/dist/package.cmd-CwGRHdEq.js +107 -0
- package/dist/package.cmd-CwGRHdEq.js.map +1 -0
- package/dist/stats.cmd-D1ujNiDO.js +121 -0
- package/dist/stats.cmd-D1ujNiDO.js.map +1 -0
- package/dist/{core/templates.js → templates-BQTgkXfH.js} +18 -15
- package/dist/templates-BQTgkXfH.js.map +1 -0
- package/dist/validate.cmd-CDUJDKGs.js +96 -0
- package/dist/validate.cmd-CDUJDKGs.js.map +1 -0
- package/dist/validator-DV5zeeel.js +721 -0
- package/dist/validator-DV5zeeel.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 -82
- 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 -136
- 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-BQTgkXfH.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-Dz8fk6Gu.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-B6iZtoPi.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"add-hook.cmd-B6iZtoPi.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,119 @@
|
|
|
1
|
+
import { n as error, o as success, r as info } from "./output-Dz8fk6Gu.js";
|
|
2
|
+
import { n as is_description_multiline, t as extract_frontmatter } from "./frontmatter-validator-DO686mla.js";
|
|
3
|
+
import { defineCommand } from "citty";
|
|
4
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { basename, join } from "node:path";
|
|
6
|
+
//#region src/commands/doctor.ts
|
|
7
|
+
function doctor_command(options) {
|
|
8
|
+
const { skill_path } = options;
|
|
9
|
+
const skill_name = basename(skill_path);
|
|
10
|
+
const skill_md_path = join(skill_path, "SKILL.md");
|
|
11
|
+
info(`Running doctor on: ${skill_name}`);
|
|
12
|
+
console.log("=".repeat(60));
|
|
13
|
+
let content;
|
|
14
|
+
try {
|
|
15
|
+
content = readFileSync(skill_md_path, "utf-8");
|
|
16
|
+
} catch (err) {
|
|
17
|
+
error(`Failed to read SKILL.md: ${String(err)}`);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
if (!extract_frontmatter(content).description_is_multiline) {
|
|
21
|
+
success("No issues found. Description is already on a single line.");
|
|
22
|
+
process.exit(0);
|
|
23
|
+
}
|
|
24
|
+
info("Found multi-line description. Fixing...");
|
|
25
|
+
const fixed_content = fix_multiline_description(content);
|
|
26
|
+
try {
|
|
27
|
+
writeFileSync(skill_md_path, fixed_content, "utf-8");
|
|
28
|
+
success("Fixed multi-line description!");
|
|
29
|
+
console.log("\nChanges made:");
|
|
30
|
+
console.log(" • Added # prettier-ignore comment before description");
|
|
31
|
+
console.log(" • Reflowed description to single line");
|
|
32
|
+
console.log("\n✓ Run validate command to confirm the fix");
|
|
33
|
+
process.exit(0);
|
|
34
|
+
} catch (err) {
|
|
35
|
+
error(`Failed to write SKILL.md: ${String(err)}`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Fix multi-line description by adding prettier-ignore and reflowing to single line
|
|
41
|
+
*/
|
|
42
|
+
function fix_multiline_description(content) {
|
|
43
|
+
const lines = content.split("\n");
|
|
44
|
+
const fixed_lines = [];
|
|
45
|
+
let in_frontmatter = false;
|
|
46
|
+
let frontmatter_count = 0;
|
|
47
|
+
let in_description = false;
|
|
48
|
+
let description_parts = [];
|
|
49
|
+
for (let i = 0; i < lines.length; i++) {
|
|
50
|
+
const line = lines[i];
|
|
51
|
+
if (line.trim() === "---") {
|
|
52
|
+
frontmatter_count++;
|
|
53
|
+
if (frontmatter_count === 2 && in_description) {
|
|
54
|
+
const full_description = description_parts.join(" ");
|
|
55
|
+
fixed_lines.push(`description: ${full_description}`);
|
|
56
|
+
description_parts = [];
|
|
57
|
+
in_description = false;
|
|
58
|
+
}
|
|
59
|
+
in_frontmatter = frontmatter_count === 1;
|
|
60
|
+
fixed_lines.push(line);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (!in_frontmatter) {
|
|
64
|
+
fixed_lines.push(line);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (line.match(/^description:/)) {
|
|
68
|
+
if (!is_description_multiline(lines.slice(i).join("\n"))) {
|
|
69
|
+
fixed_lines.push(line);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
in_description = true;
|
|
73
|
+
const match = line.match(/^description:\s*(.*)$/);
|
|
74
|
+
const value_on_line = match ? match[1].trim() : "";
|
|
75
|
+
if (value_on_line) description_parts.push(value_on_line);
|
|
76
|
+
fixed_lines.push("# prettier-ignore");
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (in_description) {
|
|
80
|
+
if (line.match(/^[a-z_-]+:/)) {
|
|
81
|
+
const full_description = description_parts.join(" ");
|
|
82
|
+
fixed_lines.push(`description: ${full_description}`);
|
|
83
|
+
description_parts = [];
|
|
84
|
+
in_description = false;
|
|
85
|
+
fixed_lines.push(line);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const trimmed = line.trim();
|
|
89
|
+
if (trimmed && !trimmed.startsWith("#")) description_parts.push(trimmed);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
fixed_lines.push(line);
|
|
93
|
+
}
|
|
94
|
+
if (in_description && description_parts.length > 0) {
|
|
95
|
+
const full_description = description_parts.join(" ");
|
|
96
|
+
fixed_lines.push(`description: ${full_description}`);
|
|
97
|
+
}
|
|
98
|
+
return fixed_lines.join("\n");
|
|
99
|
+
}
|
|
100
|
+
//#endregion
|
|
101
|
+
//#region src/commands/doctor.cmd.ts
|
|
102
|
+
var doctor_cmd_default = defineCommand({
|
|
103
|
+
meta: {
|
|
104
|
+
name: "doctor",
|
|
105
|
+
description: "Fix common skill issues automatically"
|
|
106
|
+
},
|
|
107
|
+
args: { skill_path: {
|
|
108
|
+
type: "positional",
|
|
109
|
+
description: "Path to skill directory",
|
|
110
|
+
required: true
|
|
111
|
+
} },
|
|
112
|
+
run({ args }) {
|
|
113
|
+
doctor_command({ skill_path: args.skill_path });
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
//#endregion
|
|
117
|
+
export { doctor_cmd_default as default };
|
|
118
|
+
|
|
119
|
+
//# sourceMappingURL=doctor.cmd-CkNw6ine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doctor.cmd-CkNw6ine.js","names":[],"sources":["../src/commands/doctor.ts","../src/commands/doctor.cmd.ts"],"sourcesContent":["import { readFileSync, writeFileSync } from 'node:fs';\nimport { basename, join } from 'node:path';\nimport type { DoctorOptions } from '../types.js';\nimport { error, info, success } from '../utils/output.js';\nimport {\n\textract_frontmatter,\n\tis_description_multiline,\n} from '../validators/frontmatter-validator.js';\n\nexport function doctor_command(options: DoctorOptions): void {\n\tconst { skill_path } = options;\n\tconst skill_name = basename(skill_path);\n\tconst skill_md_path = join(skill_path, 'SKILL.md');\n\n\tinfo(`Running doctor on: ${skill_name}`);\n\tconsole.log('='.repeat(60));\n\n\t// Read SKILL.md\n\tlet content: string;\n\ttry {\n\t\tcontent = readFileSync(skill_md_path, 'utf-8');\n\t} catch (err) {\n\t\terror(`Failed to read SKILL.md: ${String(err)}`);\n\t\tprocess.exit(1);\n\t}\n\n\t// Extract frontmatter\n\tconst frontmatter_data = extract_frontmatter(content);\n\n\tif (!frontmatter_data.description_is_multiline) {\n\t\tsuccess(\n\t\t\t'No issues found. Description is already on a single line.',\n\t\t);\n\t\tprocess.exit(0);\n\t}\n\n\tinfo('Found multi-line description. Fixing...');\n\n\t// Fix the multi-line description\n\tconst fixed_content = fix_multiline_description(content);\n\n\t// Write fixed content back\n\ttry {\n\t\twriteFileSync(skill_md_path, fixed_content, 'utf-8');\n\t\tsuccess('Fixed multi-line description!');\n\t\tconsole.log('\\nChanges made:');\n\t\tconsole.log(\n\t\t\t' • Added # prettier-ignore comment before description',\n\t\t);\n\t\tconsole.log(' • Reflowed description to single line');\n\t\tconsole.log('\\n✓ Run validate command to confirm the fix');\n\t\tprocess.exit(0);\n\t} catch (err) {\n\t\terror(`Failed to write SKILL.md: ${String(err)}`);\n\t\tprocess.exit(1);\n\t}\n}\n\n/**\n * Fix multi-line description by adding prettier-ignore and reflowing to single line\n */\nfunction fix_multiline_description(content: string): string {\n\tconst lines = content.split('\\n');\n\tconst fixed_lines: string[] = [];\n\tlet in_frontmatter = false;\n\tlet frontmatter_count = 0;\n\tlet in_description = false;\n\tlet description_parts: string[] = [];\n\n\tfor (let i = 0; i < lines.length; i++) {\n\t\tconst line = lines[i];\n\n\t\t// Track frontmatter boundaries\n\t\tif (line.trim() === '---') {\n\t\t\tfrontmatter_count++;\n\n\t\t\t// If we're closing frontmatter and still collecting description\n\t\t\tif (frontmatter_count === 2 && in_description) {\n\t\t\t\t// Write out the collected description\n\t\t\t\tconst full_description = description_parts.join(' ');\n\t\t\t\tfixed_lines.push(`description: ${full_description}`);\n\t\t\t\tdescription_parts = [];\n\t\t\t\tin_description = false;\n\t\t\t}\n\n\t\t\tin_frontmatter = frontmatter_count === 1;\n\t\t\tfixed_lines.push(line);\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Not in frontmatter, just pass through\n\t\tif (!in_frontmatter) {\n\t\t\tfixed_lines.push(line);\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Check if this is the description line\n\t\tif (line.match(/^description:/)) {\n\t\t\t// Check if it's already multi-line\n\t\t\tif (!is_description_multiline(lines.slice(i).join('\\n'))) {\n\t\t\t\t// Single line, just pass through\n\t\t\t\tfixed_lines.push(line);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tin_description = true;\n\n\t\t\t// Extract value on same line (if any)\n\t\t\tconst match = line.match(/^description:\\s*(.*)$/);\n\t\t\tconst value_on_line = match ? match[1].trim() : '';\n\t\t\tif (value_on_line) {\n\t\t\t\tdescription_parts.push(value_on_line);\n\t\t\t}\n\n\t\t\t// Add prettier-ignore comment\n\t\t\tfixed_lines.push('# prettier-ignore');\n\t\t\t// We'll add the description line later\n\t\t\tcontinue;\n\t\t}\n\n\t\t// If we're in description, collect continuation lines\n\t\tif (in_description) {\n\t\t\t// Stop if we hit another YAML field\n\t\t\tif (line.match(/^[a-z_-]+:/)) {\n\t\t\t\t// Done collecting description, write it out\n\t\t\t\tconst full_description = description_parts.join(' ');\n\t\t\t\tfixed_lines.push(`description: ${full_description}`);\n\t\t\t\tdescription_parts = [];\n\t\t\t\tin_description = false;\n\n\t\t\t\t// Add the current line (next field)\n\t\t\t\tfixed_lines.push(line);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// Continuation line - collect it\n\t\t\tconst trimmed = line.trim();\n\t\t\tif (trimmed && !trimmed.startsWith('#')) {\n\t\t\t\tdescription_parts.push(trimmed);\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Regular frontmatter line\n\t\tfixed_lines.push(line);\n\t}\n\n\t// If we ended while still in description (at end of frontmatter)\n\tif (in_description && description_parts.length > 0) {\n\t\tconst full_description = description_parts.join(' ');\n\t\tfixed_lines.push(`description: ${full_description}`);\n\t}\n\n\treturn fixed_lines.join('\\n');\n}\n","import { defineCommand } from 'citty';\nimport { doctor_command } from './doctor.js';\n\nexport default defineCommand({\n\tmeta: {\n\t\tname: 'doctor',\n\t\tdescription: 'Fix common skill issues automatically',\n\t},\n\targs: {\n\t\tskill_path: {\n\t\t\ttype: 'positional',\n\t\t\tdescription: 'Path to skill directory',\n\t\t\trequired: true,\n\t\t},\n\t},\n\trun({ args }) {\n\t\tdoctor_command({ skill_path: args.skill_path });\n\t},\n});\n"],"mappings":"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"}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
//#region src/validators/frontmatter-validator.ts
|
|
2
|
+
/**
|
|
3
|
+
* YAML frontmatter validation for SKILL.md
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Check if content has valid YAML frontmatter
|
|
7
|
+
*/
|
|
8
|
+
function has_yaml_frontmatter(content) {
|
|
9
|
+
return content.startsWith("---\n") || content.startsWith("---\r\n");
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Check if description field spans multiple lines in raw YAML
|
|
13
|
+
*/
|
|
14
|
+
function is_description_multiline(frontmatter) {
|
|
15
|
+
const desc_line_match = frontmatter.match(/^description:\s*(.*)$/m);
|
|
16
|
+
if (!desc_line_match) return false;
|
|
17
|
+
if (!desc_line_match[1].trim()) return true;
|
|
18
|
+
const lines = frontmatter.split("\n");
|
|
19
|
+
let found_desc = false;
|
|
20
|
+
for (let i = 0; i < lines.length; i++) {
|
|
21
|
+
const line = lines[i];
|
|
22
|
+
if (line.match(/^description:/)) {
|
|
23
|
+
found_desc = true;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (found_desc) {
|
|
27
|
+
if (line.match(/^\s+\S/) && !line.trim().startsWith("#") && !line.match(/^[a-z_-]+:/)) return true;
|
|
28
|
+
if (line.match(/^[a-z_-]+:/)) break;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Extract frontmatter and body from SKILL.md content
|
|
35
|
+
*/
|
|
36
|
+
function extract_frontmatter(content) {
|
|
37
|
+
if (!has_yaml_frontmatter(content)) return {
|
|
38
|
+
name: null,
|
|
39
|
+
description: null,
|
|
40
|
+
body: content,
|
|
41
|
+
description_is_multiline: false
|
|
42
|
+
};
|
|
43
|
+
const parts = content.split("---\n");
|
|
44
|
+
if (parts.length < 3) return {
|
|
45
|
+
name: null,
|
|
46
|
+
description: null,
|
|
47
|
+
body: content,
|
|
48
|
+
description_is_multiline: false
|
|
49
|
+
};
|
|
50
|
+
const frontmatter = parts[1];
|
|
51
|
+
const body = parts.slice(2).join("---\n");
|
|
52
|
+
const name_match = frontmatter.match(/name:\s*(.+)/);
|
|
53
|
+
const name = name_match ? name_match[1].trim() : null;
|
|
54
|
+
const desc_match = frontmatter.match(/description:\s*(.+?)(?=\n[a-z]+:|$)/s);
|
|
55
|
+
return {
|
|
56
|
+
name,
|
|
57
|
+
description: desc_match ? desc_match[1].trim() : null,
|
|
58
|
+
body,
|
|
59
|
+
description_is_multiline: is_description_multiline(frontmatter)
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Known frontmatter fields per Anthropic spec
|
|
64
|
+
* https://code.claude.com/docs/en/skills#frontmatter-reference
|
|
65
|
+
*/
|
|
66
|
+
const KNOWN_FRONTMATTER_FIELDS = new Set([
|
|
67
|
+
"name",
|
|
68
|
+
"description",
|
|
69
|
+
"argument-hint",
|
|
70
|
+
"disable-model-invocation",
|
|
71
|
+
"user-invocable",
|
|
72
|
+
"allowed-tools",
|
|
73
|
+
"model",
|
|
74
|
+
"effort",
|
|
75
|
+
"context",
|
|
76
|
+
"agent",
|
|
77
|
+
"hooks",
|
|
78
|
+
"paths",
|
|
79
|
+
"shell"
|
|
80
|
+
]);
|
|
81
|
+
/**
|
|
82
|
+
* Fields with constrained values
|
|
83
|
+
*/
|
|
84
|
+
const FIELD_VALUES = {
|
|
85
|
+
effort: [
|
|
86
|
+
"low",
|
|
87
|
+
"medium",
|
|
88
|
+
"high",
|
|
89
|
+
"max"
|
|
90
|
+
],
|
|
91
|
+
context: ["fork"],
|
|
92
|
+
shell: ["bash", "powershell"],
|
|
93
|
+
"disable-model-invocation": ["true", "false"],
|
|
94
|
+
"user-invocable": ["true", "false"]
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Extract top-level field names from raw YAML frontmatter
|
|
98
|
+
*/
|
|
99
|
+
function extract_field_names(frontmatter) {
|
|
100
|
+
const fields = /* @__PURE__ */ new Map();
|
|
101
|
+
for (const line of frontmatter.split("\n")) {
|
|
102
|
+
const match = line.match(/^([a-z][a-z0-9_-]*):\s*(.*)/);
|
|
103
|
+
if (match) fields.set(match[1], match[2].trim());
|
|
104
|
+
}
|
|
105
|
+
return fields;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Validate YAML frontmatter structure
|
|
109
|
+
*/
|
|
110
|
+
function validate_frontmatter_structure(content) {
|
|
111
|
+
const validation = {
|
|
112
|
+
valid: true,
|
|
113
|
+
has_frontmatter: false,
|
|
114
|
+
parse_error: null,
|
|
115
|
+
missing_fields: [],
|
|
116
|
+
unknown_fields: [],
|
|
117
|
+
field_value_warnings: []
|
|
118
|
+
};
|
|
119
|
+
if (!has_yaml_frontmatter(content)) {
|
|
120
|
+
validation.valid = false;
|
|
121
|
+
validation.parse_error = "Missing YAML frontmatter";
|
|
122
|
+
return validation;
|
|
123
|
+
}
|
|
124
|
+
validation.has_frontmatter = true;
|
|
125
|
+
const parts = content.split("---\n");
|
|
126
|
+
if (parts.length < 3) {
|
|
127
|
+
validation.valid = false;
|
|
128
|
+
validation.parse_error = "Malformed YAML frontmatter";
|
|
129
|
+
return validation;
|
|
130
|
+
}
|
|
131
|
+
const frontmatter = parts[1];
|
|
132
|
+
if (!frontmatter.includes("name:")) {
|
|
133
|
+
validation.missing_fields.push("name");
|
|
134
|
+
validation.valid = false;
|
|
135
|
+
}
|
|
136
|
+
if (!frontmatter.includes("description:")) {
|
|
137
|
+
validation.missing_fields.push("description");
|
|
138
|
+
validation.valid = false;
|
|
139
|
+
}
|
|
140
|
+
const fields = extract_field_names(frontmatter);
|
|
141
|
+
for (const [field, value] of fields) {
|
|
142
|
+
if (!KNOWN_FRONTMATTER_FIELDS.has(field)) validation.unknown_fields.push(field);
|
|
143
|
+
const allowed = FIELD_VALUES[field];
|
|
144
|
+
if (allowed && value && !allowed.includes(value)) validation.field_value_warnings.push(`'${field}' has value '${value}' (expected: ${allowed.join(", ")})`);
|
|
145
|
+
}
|
|
146
|
+
return validation;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Validate skill name format
|
|
150
|
+
*/
|
|
151
|
+
function validate_name_format(name, directory_name) {
|
|
152
|
+
const validation = {
|
|
153
|
+
name,
|
|
154
|
+
format_valid: true,
|
|
155
|
+
directory_name,
|
|
156
|
+
matches_directory: true,
|
|
157
|
+
errors: []
|
|
158
|
+
};
|
|
159
|
+
if (!/^[a-z0-9-]+$/.test(name)) {
|
|
160
|
+
validation.format_valid = false;
|
|
161
|
+
validation.errors.push(`Skill name must be lowercase kebab-case: '${name}'`);
|
|
162
|
+
}
|
|
163
|
+
if (name.startsWith("-") || name.endsWith("-")) {
|
|
164
|
+
validation.format_valid = false;
|
|
165
|
+
validation.errors.push(`Skill name must not start or end with a hyphen: '${name}'`);
|
|
166
|
+
}
|
|
167
|
+
if (name.includes("--")) {
|
|
168
|
+
validation.format_valid = false;
|
|
169
|
+
validation.errors.push(`Skill name must not contain consecutive hyphens: '${name}'`);
|
|
170
|
+
}
|
|
171
|
+
if (name.startsWith("claude") || name.startsWith("anthropic")) {
|
|
172
|
+
validation.format_valid = false;
|
|
173
|
+
validation.errors.push(`Skill name must not use reserved prefix 'claude' or 'anthropic': '${name}'`);
|
|
174
|
+
}
|
|
175
|
+
if (name.includes("<") || name.includes(">")) {
|
|
176
|
+
validation.format_valid = false;
|
|
177
|
+
validation.errors.push(`Skill name must not contain XML angle brackets: '${name}'`);
|
|
178
|
+
}
|
|
179
|
+
if (name !== directory_name) {
|
|
180
|
+
validation.matches_directory = false;
|
|
181
|
+
validation.errors.push(`Skill name '${name}' must match directory name '${directory_name}'`);
|
|
182
|
+
}
|
|
183
|
+
return validation;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Validate hard limits for name and description
|
|
187
|
+
*/
|
|
188
|
+
function validate_hard_limits(name, description) {
|
|
189
|
+
const limits = {
|
|
190
|
+
name: {
|
|
191
|
+
length: 0,
|
|
192
|
+
limit: 64,
|
|
193
|
+
valid: true,
|
|
194
|
+
error: null
|
|
195
|
+
},
|
|
196
|
+
description: {
|
|
197
|
+
length: 0,
|
|
198
|
+
limit: 250,
|
|
199
|
+
valid: true,
|
|
200
|
+
error: null
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
if (name) {
|
|
204
|
+
limits.name.length = name.length;
|
|
205
|
+
if (name.length > 64) {
|
|
206
|
+
limits.name.valid = false;
|
|
207
|
+
limits.name.error = `Skill name too long (max 64 chars): ${name.length}`;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (description) {
|
|
211
|
+
limits.description.length = description.length;
|
|
212
|
+
if (description.length > 250) {
|
|
213
|
+
limits.description.valid = false;
|
|
214
|
+
limits.description.error = `Description too long (max 250 chars — Claude truncates at this limit): ${description.length}`;
|
|
215
|
+
}
|
|
216
|
+
if (description.includes("<") || description.includes(">")) {
|
|
217
|
+
limits.description.valid = false;
|
|
218
|
+
limits.description.error = `Description must not contain XML angle brackets`;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return limits;
|
|
222
|
+
}
|
|
223
|
+
//#endregion
|
|
224
|
+
export { validate_name_format as a, validate_hard_limits as i, is_description_multiline as n, validate_frontmatter_structure as r, extract_frontmatter as t };
|
|
225
|
+
|
|
226
|
+
//# sourceMappingURL=frontmatter-validator-DO686mla.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frontmatter-validator-DO686mla.js","names":[],"sources":["../src/validators/frontmatter-validator.ts"],"sourcesContent":["/**\n * YAML frontmatter validation for SKILL.md\n */\n\nimport {\n\tDESCRIPTION_MAX_LENGTH,\n\tNAME_MAX_LENGTH,\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 * 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'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]);\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\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"],"mappings":"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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { chmodSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
//#region src/utils/fs.ts
|
|
4
|
+
function ensure_dir(path) {
|
|
5
|
+
mkdirSync(path, { recursive: true });
|
|
6
|
+
}
|
|
7
|
+
function write_file(path, content) {
|
|
8
|
+
ensure_dir(dirname(path));
|
|
9
|
+
writeFileSync(path, content, "utf-8");
|
|
10
|
+
}
|
|
11
|
+
function make_executable(path) {
|
|
12
|
+
chmodSync(path, 493);
|
|
13
|
+
}
|
|
14
|
+
function to_title_case(kebab_case) {
|
|
15
|
+
return kebab_case.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
16
|
+
}
|
|
17
|
+
function is_lowercase(str) {
|
|
18
|
+
return str === str.toLowerCase();
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
export { write_file as a, to_title_case as i, is_lowercase as n, make_executable as r, ensure_dir as t };
|
|
22
|
+
|
|
23
|
+
//# sourceMappingURL=fs-CuGv3Ob2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fs-CuGv3Ob2.js","names":[],"sources":["../src/utils/fs.ts"],"sourcesContent":["import { chmodSync, mkdirSync, writeFileSync } from 'node:fs';\nimport { dirname } from 'node:path';\n\nexport function ensure_dir(path: string): void {\n\tmkdirSync(path, { recursive: true });\n}\n\nexport function write_file(path: string, content: string): void {\n\tensure_dir(dirname(path));\n\twriteFileSync(path, content, 'utf-8');\n}\n\nexport function make_executable(path: string): void {\n\tchmodSync(path, 0o755);\n}\n\nexport function to_title_case(kebab_case: string): string {\n\treturn kebab_case\n\t\t.split('-')\n\t\t.map((word) => word.charAt(0).toUpperCase() + word.slice(1))\n\t\t.join(' ');\n}\n\nexport function is_kebab_case(str: string): boolean {\n\treturn /^[a-z0-9-]+$/.test(str);\n}\n\nexport function is_lowercase(str: string): boolean {\n\treturn str === str.toLowerCase();\n}\n"],"mappings":";;;AAGA,SAAgB,WAAW,MAAoB;AAC9C,WAAU,MAAM,EAAE,WAAW,MAAM,CAAC;;AAGrC,SAAgB,WAAW,MAAc,SAAuB;AAC/D,YAAW,QAAQ,KAAK,CAAC;AACzB,eAAc,MAAM,SAAS,QAAQ;;AAGtC,SAAgB,gBAAgB,MAAoB;AACnD,WAAU,MAAM,IAAM;;AAGvB,SAAgB,cAAc,YAA4B;AACzD,QAAO,WACL,MAAM,IAAI,CACV,KAAK,SAAS,KAAK,OAAO,EAAE,CAAC,aAAa,GAAG,KAAK,MAAM,EAAE,CAAC,CAC3D,KAAK,IAAI;;AAOZ,SAAgB,aAAa,KAAsB;AAClD,QAAO,QAAQ,IAAI,aAAa"}
|