claude-skills-cli 0.0.20 → 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} +16 -13
- 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 -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,721 @@
|
|
|
1
|
+
import { l as LIMITS } from "./output-Dz8fk6Gu.js";
|
|
2
|
+
import { a as validate_name_format, i as validate_hard_limits, r as validate_frontmatter_structure, t as extract_frontmatter } from "./frontmatter-validator-DO686mla.js";
|
|
3
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
//#region src/validators/text-analysis.ts
|
|
6
|
+
/**
|
|
7
|
+
* Count words in text
|
|
8
|
+
*/
|
|
9
|
+
function count_words(text) {
|
|
10
|
+
return text.trim().split(/\s+/).filter((w) => w.length > 0).length;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Estimate tokens (rough approximation: 1 word ≈ 1.3 tokens for English)
|
|
14
|
+
*/
|
|
15
|
+
function estimate_tokens(word_count) {
|
|
16
|
+
return Math.round(word_count * 1.3);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Estimate tokens for a string by counting words and applying ratio
|
|
20
|
+
*/
|
|
21
|
+
function estimate_string_tokens(text) {
|
|
22
|
+
return estimate_tokens(count_words(text));
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Remove HTML comments from content (for line counting)
|
|
26
|
+
*/
|
|
27
|
+
function strip_html_comments(text) {
|
|
28
|
+
return text.replace(/<!--[\s\S]*?-->/g, "");
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Extract keywords from text (simplified extraction)
|
|
32
|
+
*/
|
|
33
|
+
function extract_keywords(text) {
|
|
34
|
+
const words = text.toLowerCase().replace(/[^\w\s-]/g, " ").split(/\s+/).filter((w) => w.length > 3);
|
|
35
|
+
return [...new Set(words)].filter((w) => ![
|
|
36
|
+
"this",
|
|
37
|
+
"that",
|
|
38
|
+
"with",
|
|
39
|
+
"from",
|
|
40
|
+
"have",
|
|
41
|
+
"will",
|
|
42
|
+
"when",
|
|
43
|
+
"what",
|
|
44
|
+
"where",
|
|
45
|
+
"which",
|
|
46
|
+
"their",
|
|
47
|
+
"them",
|
|
48
|
+
"then",
|
|
49
|
+
"than",
|
|
50
|
+
"these",
|
|
51
|
+
"those",
|
|
52
|
+
"there"
|
|
53
|
+
].includes(w));
|
|
54
|
+
}
|
|
55
|
+
//#endregion
|
|
56
|
+
//#region src/validators/alignment-validator.ts
|
|
57
|
+
/**
|
|
58
|
+
* Alignment validation - checks description and content alignment
|
|
59
|
+
*/
|
|
60
|
+
/**
|
|
61
|
+
* Analyze description and content alignment
|
|
62
|
+
*/
|
|
63
|
+
function analyze_alignment(description, body) {
|
|
64
|
+
const desc_keywords = extract_keywords(description);
|
|
65
|
+
const content_keywords = extract_keywords(body);
|
|
66
|
+
const overlap = desc_keywords.filter((k) => content_keywords.includes(k));
|
|
67
|
+
const desc_only = desc_keywords.filter((k) => !content_keywords.includes(k));
|
|
68
|
+
const content_only = content_keywords.filter((k) => !desc_keywords.includes(k)).slice(0, 20);
|
|
69
|
+
const overlap_ratio = desc_keywords.length > 0 ? overlap.length / desc_keywords.length : 0;
|
|
70
|
+
let severity = "good";
|
|
71
|
+
let explanation = "Description aligns well with content";
|
|
72
|
+
if (overlap_ratio < .2 && desc_keywords.length > 5) {
|
|
73
|
+
severity = "critical";
|
|
74
|
+
explanation = `Very low keyword overlap (${Math.round(overlap_ratio * 100)}%). Description may not match skill content.`;
|
|
75
|
+
} else if (overlap_ratio < .3 && desc_keywords.length > 5) {
|
|
76
|
+
severity = "moderate";
|
|
77
|
+
explanation = `Low keyword overlap (${Math.round(overlap_ratio * 100)}%). Description may not accurately reflect skill content.`;
|
|
78
|
+
}
|
|
79
|
+
const keywords = {
|
|
80
|
+
description_keywords: desc_keywords,
|
|
81
|
+
content_keywords: content_keywords.slice(0, 30),
|
|
82
|
+
overlap,
|
|
83
|
+
description_only: desc_only,
|
|
84
|
+
content_only
|
|
85
|
+
};
|
|
86
|
+
const alignment = {
|
|
87
|
+
severity,
|
|
88
|
+
description_focus: desc_keywords.slice(0, 10),
|
|
89
|
+
content_focus: content_keywords.slice(0, 10),
|
|
90
|
+
matches: overlap,
|
|
91
|
+
mismatches: desc_only,
|
|
92
|
+
explanation
|
|
93
|
+
};
|
|
94
|
+
const warnings = [];
|
|
95
|
+
if (overlap_ratio < .3 && desc_keywords.length > 5) warnings.push({
|
|
96
|
+
type: "low_overlap",
|
|
97
|
+
message: `Low keyword overlap between description and content (${Math.round(overlap_ratio * 100)}%)\n → Description may not accurately reflect skill content`
|
|
98
|
+
});
|
|
99
|
+
return {
|
|
100
|
+
keywords,
|
|
101
|
+
alignment,
|
|
102
|
+
warnings
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
//#endregion
|
|
106
|
+
//#region src/validators/content-validator.ts
|
|
107
|
+
/**
|
|
108
|
+
* Content validation (Level 2 progressive disclosure)
|
|
109
|
+
*/
|
|
110
|
+
/**
|
|
111
|
+
* Analyze content structure and patterns
|
|
112
|
+
*/
|
|
113
|
+
function analyze_content_structure(body) {
|
|
114
|
+
const code_block_matches = body.match(/```[\s\S]*?```/g);
|
|
115
|
+
const code_blocks = code_block_matches ? code_block_matches.length : 0;
|
|
116
|
+
const heading_matches = body.match(/^#{1,6}\s/gm);
|
|
117
|
+
return {
|
|
118
|
+
code_blocks,
|
|
119
|
+
sections: heading_matches ? heading_matches.length : 0,
|
|
120
|
+
long_paragraphs: body.split(/\n\n+/).filter((p) => {
|
|
121
|
+
return count_words(p) > 100;
|
|
122
|
+
}).length
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Validate progressive disclosure (word count, token budget, and line count)
|
|
127
|
+
*/
|
|
128
|
+
function validate_content(body, options = {}) {
|
|
129
|
+
const { mode = "strict" } = options;
|
|
130
|
+
const limits = LIMITS[mode];
|
|
131
|
+
const word_count = count_words(body);
|
|
132
|
+
const estimated_tokens = estimate_tokens(word_count);
|
|
133
|
+
const line_count = strip_html_comments(body).trim().split("\n").length;
|
|
134
|
+
const structure = analyze_content_structure(body);
|
|
135
|
+
const validation = {
|
|
136
|
+
stats: {
|
|
137
|
+
word_count,
|
|
138
|
+
estimated_tokens,
|
|
139
|
+
line_count,
|
|
140
|
+
...structure
|
|
141
|
+
},
|
|
142
|
+
warnings: [],
|
|
143
|
+
errors: []
|
|
144
|
+
};
|
|
145
|
+
if (word_count > limits.words.max) validation.errors.push({
|
|
146
|
+
type: "word_count",
|
|
147
|
+
message: `SKILL.md body has ${word_count} words (MAX: ${limits.words.max})\n → Move detailed content to references/ directory for Level 3 loading\n → This is a hard limit - skills must be concise`
|
|
148
|
+
});
|
|
149
|
+
else if (word_count > limits.words.good) validation.warnings.push({
|
|
150
|
+
type: "word_count",
|
|
151
|
+
message: `SKILL.md body has ${word_count} words (recommended: <${limits.words.good}, max: ${limits.words.max})\n → Consider moving examples/docs to references/ for better token efficiency`
|
|
152
|
+
});
|
|
153
|
+
if (line_count > limits.lines.max) validation.errors.push({
|
|
154
|
+
type: "line_count",
|
|
155
|
+
message: `SKILL.md body is ${line_count} lines (MAX: ${limits.lines.max})\n → Move detailed content to references/ directory\n → This is a hard limit - skills must be concise`
|
|
156
|
+
});
|
|
157
|
+
else if (line_count > limits.lines.good) validation.warnings.push({
|
|
158
|
+
type: "line_count",
|
|
159
|
+
message: `SKILL.md body is ${line_count} lines (recommended: <${limits.lines.good}, max: ${limits.lines.max})\n → Consider moving examples to references/ for Level 3 loading`
|
|
160
|
+
});
|
|
161
|
+
if (structure.code_blocks > 3) validation.warnings.push({
|
|
162
|
+
type: "code_blocks",
|
|
163
|
+
message: `SKILL.md contains ${structure.code_blocks} code examples (recommended: 1-2)\n → Move additional examples to references/examples.md for Level 3 loading`
|
|
164
|
+
});
|
|
165
|
+
if (structure.long_paragraphs > 3) validation.warnings.push({
|
|
166
|
+
type: "long_paragraphs",
|
|
167
|
+
message: `SKILL.md contains ${structure.long_paragraphs} lengthy paragraphs (>100 words)\n → Consider moving detailed explanations to references/`
|
|
168
|
+
});
|
|
169
|
+
if (structure.sections > 8) validation.warnings.push({
|
|
170
|
+
type: "sections",
|
|
171
|
+
message: `SKILL.md contains ${structure.sections} sections (recommended: 3-5)\n → Consider splitting into focused reference files`
|
|
172
|
+
});
|
|
173
|
+
if (!body.includes("## Quick Start") && !body.includes("## Quick start")) validation.warnings.push({
|
|
174
|
+
type: "missing_quick_start",
|
|
175
|
+
message: "Missing \"## Quick Start\" section\n → Add one minimal working example to help Claude get started quickly"
|
|
176
|
+
});
|
|
177
|
+
if (!body.includes("references/") && line_count > limits.lines.good) validation.warnings.push({
|
|
178
|
+
type: "no_references",
|
|
179
|
+
message: `No references/ links found but SKILL.md is ${line_count} lines\n → Consider splitting detailed content into reference files`
|
|
180
|
+
});
|
|
181
|
+
if (body.trim().length < 100) validation.warnings.push({
|
|
182
|
+
type: "short_body",
|
|
183
|
+
message: "SKILL.md body is very short"
|
|
184
|
+
});
|
|
185
|
+
if (body.includes("TODO") || body.includes("[Add your") || body.includes("[Provide")) validation.warnings.push({
|
|
186
|
+
type: "todo_placeholders",
|
|
187
|
+
message: "SKILL.md contains TODO placeholders"
|
|
188
|
+
});
|
|
189
|
+
return validation;
|
|
190
|
+
}
|
|
191
|
+
//#endregion
|
|
192
|
+
//#region src/validators/description-validator.ts
|
|
193
|
+
/**
|
|
194
|
+
* Description validation (Level 1 progressive disclosure)
|
|
195
|
+
*/
|
|
196
|
+
/**
|
|
197
|
+
* Validate description length and quality
|
|
198
|
+
*/
|
|
199
|
+
function validate_description_content(description) {
|
|
200
|
+
const desc_length = description.length;
|
|
201
|
+
const validation = {
|
|
202
|
+
stats: {
|
|
203
|
+
description_length: desc_length,
|
|
204
|
+
description_tokens: estimate_string_tokens(description)
|
|
205
|
+
},
|
|
206
|
+
warnings: [],
|
|
207
|
+
errors: []
|
|
208
|
+
};
|
|
209
|
+
if (desc_length > 250) validation.errors.push({
|
|
210
|
+
type: "length",
|
|
211
|
+
message: `Description is ${desc_length} characters (MAX: 250 — Claude truncates at this limit)\n → Keep descriptions concise - anything past 250 chars is never seen`
|
|
212
|
+
});
|
|
213
|
+
const lower_desc = description.toLowerCase();
|
|
214
|
+
if (!(lower_desc.includes("use when") || lower_desc.includes("use for") || lower_desc.includes("use to"))) validation.warnings.push({
|
|
215
|
+
type: "trigger",
|
|
216
|
+
message: "Description missing trigger keywords ('Use when...', 'Use for...', 'Use to...')\n → Help Claude know when to activate this skill"
|
|
217
|
+
});
|
|
218
|
+
const comma_count = (description.match(/,/g) || []).length;
|
|
219
|
+
if (desc_length > 150 && comma_count >= 5) validation.warnings.push({
|
|
220
|
+
type: "list_bloat",
|
|
221
|
+
message: `Description contains long lists (${comma_count} commas, ${desc_length} chars)\n → Move detailed lists to Level 2 (SKILL.md body) or Level 3 (references/)`
|
|
222
|
+
});
|
|
223
|
+
if (desc_length < 50) validation.warnings.push({
|
|
224
|
+
type: "short",
|
|
225
|
+
message: `Description is very short (${desc_length} chars, minimum recommended: 50)\n → Must answer both "what does it do" AND "when to use it"`
|
|
226
|
+
});
|
|
227
|
+
return validation;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Analyze trigger phrase in description
|
|
231
|
+
*/
|
|
232
|
+
function analyze_trigger_phrase(description) {
|
|
233
|
+
const lower = description.toLowerCase();
|
|
234
|
+
const has_trigger = lower.includes("use when") || lower.includes("use for") || lower.includes("use to");
|
|
235
|
+
let trigger_phrase = null;
|
|
236
|
+
let trigger_type = "missing";
|
|
237
|
+
if (has_trigger) {
|
|
238
|
+
const match = description.match(/(use when|use for|use to)[^.!?]*/i);
|
|
239
|
+
if (match) {
|
|
240
|
+
trigger_phrase = match[0].trim();
|
|
241
|
+
trigger_type = trigger_phrase.length > 50 ? "specific" : "generic";
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return {
|
|
245
|
+
has_explicit_trigger: has_trigger,
|
|
246
|
+
trigger_phrase,
|
|
247
|
+
trigger_type
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Analyze user phrasing style
|
|
252
|
+
*/
|
|
253
|
+
function analyze_user_phrasing(description) {
|
|
254
|
+
const issues = [];
|
|
255
|
+
const warnings = [];
|
|
256
|
+
const is_third_person = !/\b(I can|I will|I help|my|me)\b/i.test(description);
|
|
257
|
+
const first_person_patterns = /\b(I can|I will|I help|my|me)\b/i;
|
|
258
|
+
if (first_person_patterns.test(description)) {
|
|
259
|
+
const match = description.match(first_person_patterns);
|
|
260
|
+
if (match) warnings.push({
|
|
261
|
+
type: "first_person",
|
|
262
|
+
message: `Description uses first person: "${match[0]}"\n → Anthropic requires third-person voice (e.g., "Generates..." not "I can generate...")`
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
const second_person_patterns = /\b(You can|You should|You could|You'll|You will|You need|your)\b/i;
|
|
266
|
+
if (second_person_patterns.test(description)) {
|
|
267
|
+
const match = description.match(second_person_patterns);
|
|
268
|
+
if (match) warnings.push({
|
|
269
|
+
type: "second_person",
|
|
270
|
+
message: `Description uses second person: "${match[0]}"\n → Anthropic requires third-person voice (e.g., "Processes..." not "You can process...")`
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
const vague_patterns = /\b(helper|utility|tool|various|several|some)\b/i;
|
|
274
|
+
if (vague_patterns.test(description)) {
|
|
275
|
+
const match = description.match(vague_patterns);
|
|
276
|
+
if (match) warnings.push({
|
|
277
|
+
type: "vague",
|
|
278
|
+
message: `Description contains vague term: "${match[0]}"\n → Be specific about what the skill does`
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
const uses_gerund = /\b\w+ing\b/i.test(description);
|
|
282
|
+
const is_action_oriented = /^(create|build|design|analyze|test|validate|generate|process|manage|execute|handle|provide)/i.test(description.trim());
|
|
283
|
+
if (!uses_gerund && !is_action_oriented) warnings.push({
|
|
284
|
+
type: "passive",
|
|
285
|
+
message: "Description lacks action-oriented language\n → Start with a verb or gerund (e.g., \"Generates...\", \"Managing...\", \"Extract...\")"
|
|
286
|
+
});
|
|
287
|
+
return {
|
|
288
|
+
analysis: {
|
|
289
|
+
style_checks: {
|
|
290
|
+
is_third_person,
|
|
291
|
+
uses_gerund_form: uses_gerund,
|
|
292
|
+
is_action_oriented
|
|
293
|
+
},
|
|
294
|
+
issues
|
|
295
|
+
},
|
|
296
|
+
warnings
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
//#endregion
|
|
300
|
+
//#region src/validators/file-structure-validator.ts
|
|
301
|
+
/**
|
|
302
|
+
* File structure validation - paths, scripts, assets
|
|
303
|
+
*/
|
|
304
|
+
/**
|
|
305
|
+
* Validate that skill directory exists and is valid
|
|
306
|
+
*/
|
|
307
|
+
function validate_directory(skill_path) {
|
|
308
|
+
const errors = [];
|
|
309
|
+
if (!existsSync(skill_path)) {
|
|
310
|
+
errors.push({
|
|
311
|
+
type: "not_found",
|
|
312
|
+
message: `Skill directory does not exist: ${skill_path}`
|
|
313
|
+
});
|
|
314
|
+
return {
|
|
315
|
+
valid: false,
|
|
316
|
+
errors
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
if (!statSync(skill_path).isDirectory()) {
|
|
320
|
+
errors.push({
|
|
321
|
+
type: "not_directory",
|
|
322
|
+
message: `Path is not a directory: ${skill_path}`
|
|
323
|
+
});
|
|
324
|
+
return {
|
|
325
|
+
valid: false,
|
|
326
|
+
errors
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
return {
|
|
330
|
+
valid: true,
|
|
331
|
+
errors: []
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Validate path formats (no Windows backslashes)
|
|
336
|
+
*/
|
|
337
|
+
function validate_path_formats(content, file_name = "SKILL.md") {
|
|
338
|
+
const invalid_paths = [];
|
|
339
|
+
const errors = [];
|
|
340
|
+
content.split("\n").forEach((line, index) => {
|
|
341
|
+
if (line.trim().startsWith("```")) return;
|
|
342
|
+
const matches = line.match(/(?:scripts|references|assets|examples)\\[\w\\.-]+/g);
|
|
343
|
+
if (matches) matches.forEach((match) => {
|
|
344
|
+
const fixed = match.replace(/\\/g, "/");
|
|
345
|
+
invalid_paths.push({
|
|
346
|
+
line_number: index + 1,
|
|
347
|
+
path: match,
|
|
348
|
+
error: "Windows-style backslash detected",
|
|
349
|
+
suggested_fix: fixed
|
|
350
|
+
});
|
|
351
|
+
errors.push({
|
|
352
|
+
type: "windows_path",
|
|
353
|
+
message: `Windows-style path in ${file_name}:${index + 1}\n → Found: ${match}\n → Use: ${fixed}`
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
return {
|
|
358
|
+
validation: { invalid_paths },
|
|
359
|
+
errors
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Validate scripts directory
|
|
364
|
+
*/
|
|
365
|
+
function validate_scripts(skill_path) {
|
|
366
|
+
const scripts_dir = join(skill_path, "scripts");
|
|
367
|
+
const warnings = [];
|
|
368
|
+
if (existsSync(scripts_dir)) {
|
|
369
|
+
const script_files = readdirSync(scripts_dir).filter((f) => f.endsWith(".js") || f.endsWith(".ts") || f.endsWith(".mjs") || f.endsWith(".sh"));
|
|
370
|
+
if (script_files.length === 0) warnings.push({
|
|
371
|
+
type: "empty_directory",
|
|
372
|
+
message: "scripts/ directory exists but is empty"
|
|
373
|
+
});
|
|
374
|
+
for (const script_file of script_files) {
|
|
375
|
+
const script_path = join(scripts_dir, script_file);
|
|
376
|
+
if ((statSync(script_path).mode & 73) === 0) warnings.push({
|
|
377
|
+
type: "not_executable",
|
|
378
|
+
message: `Script is not executable: ${script_file}`
|
|
379
|
+
});
|
|
380
|
+
if (!readFileSync(script_path, "utf-8").split("\n")[0].startsWith("#!")) warnings.push({
|
|
381
|
+
type: "missing_shebang",
|
|
382
|
+
message: `Script missing shebang: ${script_file}`
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return { warnings };
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Validate assets directory
|
|
390
|
+
*/
|
|
391
|
+
function validate_assets(skill_path) {
|
|
392
|
+
const assets_dir = join(skill_path, "assets");
|
|
393
|
+
const warnings = [];
|
|
394
|
+
if (existsSync(assets_dir)) {
|
|
395
|
+
if (readdirSync(assets_dir).length === 0) warnings.push({
|
|
396
|
+
type: "empty_directory",
|
|
397
|
+
message: "assets/ directory exists but is empty"
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
return { warnings };
|
|
401
|
+
}
|
|
402
|
+
//#endregion
|
|
403
|
+
//#region src/validators/references-validator.ts
|
|
404
|
+
/**
|
|
405
|
+
* References validation (Level 3 progressive disclosure)
|
|
406
|
+
*/
|
|
407
|
+
/**
|
|
408
|
+
* Strip fenced code blocks from content to avoid parsing example links
|
|
409
|
+
*/
|
|
410
|
+
function strip_code_blocks(content) {
|
|
411
|
+
return content.replace(/```[\s\S]*?```|~~~[\s\S]*?~~~/g, "");
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Check nesting depth of reference files
|
|
415
|
+
*/
|
|
416
|
+
function check_reference_nesting(skill_path, file_path, visited = /* @__PURE__ */ new Set()) {
|
|
417
|
+
if (visited.has(file_path)) return {
|
|
418
|
+
depth: 0,
|
|
419
|
+
references: []
|
|
420
|
+
};
|
|
421
|
+
visited.add(file_path);
|
|
422
|
+
const full_path = join(skill_path, file_path);
|
|
423
|
+
if (!existsSync(full_path)) return {
|
|
424
|
+
depth: 0,
|
|
425
|
+
references: []
|
|
426
|
+
};
|
|
427
|
+
const references = [...strip_code_blocks(readFileSync(full_path, "utf-8")).matchAll(/\[([^\]]+)\]\(([^)]+\.md)\)/g)].map((m) => m[2]);
|
|
428
|
+
if (references.length === 0) return {
|
|
429
|
+
depth: 1,
|
|
430
|
+
references: []
|
|
431
|
+
};
|
|
432
|
+
let max_depth = 1;
|
|
433
|
+
for (const ref of references) {
|
|
434
|
+
const nested = check_reference_nesting(skill_path, ref, new Set(visited));
|
|
435
|
+
max_depth = Math.max(max_depth, 1 + nested.depth);
|
|
436
|
+
}
|
|
437
|
+
return {
|
|
438
|
+
depth: max_depth,
|
|
439
|
+
references
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Validate references directory and links
|
|
444
|
+
*/
|
|
445
|
+
function validate_references(skill_path) {
|
|
446
|
+
const references_dir = join(skill_path, "references");
|
|
447
|
+
const skill_md_path = join(skill_path, "SKILL.md");
|
|
448
|
+
const files_found = [];
|
|
449
|
+
const files_referenced = [];
|
|
450
|
+
const missing_files = [];
|
|
451
|
+
const nesting_data = [];
|
|
452
|
+
const warnings = [];
|
|
453
|
+
const errors = [];
|
|
454
|
+
if (existsSync(references_dir)) {
|
|
455
|
+
const md_files = readdirSync(references_dir).filter((f) => f.endsWith(".md"));
|
|
456
|
+
files_found.push(...md_files.map((f) => `references/${f}`));
|
|
457
|
+
if (md_files.length === 0) warnings.push({
|
|
458
|
+
type: "empty_directory",
|
|
459
|
+
message: "references/ directory exists but is empty"
|
|
460
|
+
});
|
|
461
|
+
if (existsSync(skill_md_path)) {
|
|
462
|
+
const skill_content = readFileSync(skill_md_path, "utf-8");
|
|
463
|
+
for (const md_file of md_files) if (!skill_content.includes(md_file)) warnings.push({
|
|
464
|
+
type: "orphaned_file",
|
|
465
|
+
message: `Reference file 'references/${md_file}' not mentioned in SKILL.md`
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
if (existsSync(skill_path)) {
|
|
470
|
+
const root_md_files = readdirSync(skill_path).filter((f) => f.endsWith(".md") && f !== "SKILL.md" && f !== "README.md");
|
|
471
|
+
files_found.push(...root_md_files);
|
|
472
|
+
if (existsSync(skill_md_path)) {
|
|
473
|
+
const skill_content = readFileSync(skill_md_path, "utf-8");
|
|
474
|
+
for (const md_file of root_md_files) if (!skill_content.includes(md_file)) warnings.push({
|
|
475
|
+
type: "orphaned_file",
|
|
476
|
+
message: `Root file '${md_file}' not mentioned in SKILL.md`
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
if (existsSync(skill_md_path)) {
|
|
481
|
+
const matches = strip_code_blocks(readFileSync(skill_md_path, "utf-8")).matchAll(/\[([^\]]+)\]\(([^)]+\.md)\)/g);
|
|
482
|
+
for (const match of matches) {
|
|
483
|
+
const link_text = match[1];
|
|
484
|
+
const file_path = match[2];
|
|
485
|
+
const full_path = join(skill_path, file_path);
|
|
486
|
+
files_referenced.push(file_path);
|
|
487
|
+
if (!existsSync(full_path)) {
|
|
488
|
+
missing_files.push(file_path);
|
|
489
|
+
errors.push({
|
|
490
|
+
type: "missing_file",
|
|
491
|
+
message: `Referenced file not found: ${file_path}\n → Linked from: [${link_text}]\n → Create the file or remove the broken link`
|
|
492
|
+
});
|
|
493
|
+
} else {
|
|
494
|
+
const nesting = check_reference_nesting(skill_path, file_path);
|
|
495
|
+
let warning = null;
|
|
496
|
+
if (nesting.depth > 1) {
|
|
497
|
+
warning = `File has depth ${nesting.depth} (recommended: 1). Keep references one level deep from SKILL.md.`;
|
|
498
|
+
warnings.push({
|
|
499
|
+
type: "nesting_depth",
|
|
500
|
+
message: `${file_path} has nesting depth ${nesting.depth} (recommended: 1)\n → Keep references one level deep from SKILL.md for clarity`
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
nesting_data.push({
|
|
504
|
+
file: file_path,
|
|
505
|
+
references: nesting.references,
|
|
506
|
+
depth: nesting.depth,
|
|
507
|
+
warning
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return {
|
|
513
|
+
validation: {
|
|
514
|
+
files_found,
|
|
515
|
+
files_referenced,
|
|
516
|
+
missing_files,
|
|
517
|
+
orphaned_files: files_found.filter((f) => !files_referenced.some((ref) => ref.includes(f))),
|
|
518
|
+
nesting: nesting_data,
|
|
519
|
+
max_nesting_depth: nesting_data.length > 0 ? Math.max(...nesting_data.map((n) => n.depth)) : 0
|
|
520
|
+
},
|
|
521
|
+
warnings,
|
|
522
|
+
errors
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
//#endregion
|
|
526
|
+
//#region src/core/validator.ts
|
|
527
|
+
var SkillValidator = class {
|
|
528
|
+
skill_path;
|
|
529
|
+
options;
|
|
530
|
+
errors = [];
|
|
531
|
+
warnings = [];
|
|
532
|
+
stats = {
|
|
533
|
+
word_count: 0,
|
|
534
|
+
estimated_tokens: 0,
|
|
535
|
+
line_count: 0,
|
|
536
|
+
description_length: 0,
|
|
537
|
+
description_tokens: 0,
|
|
538
|
+
code_blocks: 0,
|
|
539
|
+
sections: 0,
|
|
540
|
+
long_paragraphs: 0
|
|
541
|
+
};
|
|
542
|
+
structured_validation = {
|
|
543
|
+
hard_limits: {
|
|
544
|
+
name: {
|
|
545
|
+
length: 0,
|
|
546
|
+
limit: 64,
|
|
547
|
+
valid: true,
|
|
548
|
+
error: null
|
|
549
|
+
},
|
|
550
|
+
description: {
|
|
551
|
+
length: 0,
|
|
552
|
+
limit: 250,
|
|
553
|
+
valid: true,
|
|
554
|
+
error: null
|
|
555
|
+
}
|
|
556
|
+
},
|
|
557
|
+
name_format: {
|
|
558
|
+
name: "",
|
|
559
|
+
format_valid: true,
|
|
560
|
+
directory_name: "",
|
|
561
|
+
matches_directory: true,
|
|
562
|
+
errors: []
|
|
563
|
+
},
|
|
564
|
+
yaml_validation: {
|
|
565
|
+
valid: true,
|
|
566
|
+
has_frontmatter: false,
|
|
567
|
+
parse_error: null,
|
|
568
|
+
missing_fields: []
|
|
569
|
+
},
|
|
570
|
+
path_format: { invalid_paths: [] },
|
|
571
|
+
triggering: {
|
|
572
|
+
trigger_phrase: {
|
|
573
|
+
has_explicit_trigger: false,
|
|
574
|
+
trigger_phrase: null,
|
|
575
|
+
trigger_type: "missing"
|
|
576
|
+
},
|
|
577
|
+
user_phrasing: {
|
|
578
|
+
style_checks: {
|
|
579
|
+
is_third_person: true,
|
|
580
|
+
uses_gerund_form: true,
|
|
581
|
+
is_action_oriented: true
|
|
582
|
+
},
|
|
583
|
+
issues: []
|
|
584
|
+
},
|
|
585
|
+
keywords: {
|
|
586
|
+
description_keywords: [],
|
|
587
|
+
content_keywords: [],
|
|
588
|
+
overlap: [],
|
|
589
|
+
description_only: [],
|
|
590
|
+
content_only: []
|
|
591
|
+
},
|
|
592
|
+
alignment: {
|
|
593
|
+
severity: "good",
|
|
594
|
+
description_focus: [],
|
|
595
|
+
content_focus: [],
|
|
596
|
+
matches: [],
|
|
597
|
+
mismatches: [],
|
|
598
|
+
explanation: ""
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
constructor(skill_path, options = {}) {
|
|
603
|
+
this.skill_path = skill_path;
|
|
604
|
+
this.options = options;
|
|
605
|
+
}
|
|
606
|
+
error(msg) {
|
|
607
|
+
this.errors.push(`❌ ${msg}`);
|
|
608
|
+
}
|
|
609
|
+
warning(msg) {
|
|
610
|
+
this.warnings.push(`⚠️ ${msg}`);
|
|
611
|
+
}
|
|
612
|
+
validate_skill_md() {
|
|
613
|
+
const skill_md_path = join(this.skill_path, "SKILL.md");
|
|
614
|
+
if (!existsSync(skill_md_path)) {
|
|
615
|
+
this.error("SKILL.md file not found");
|
|
616
|
+
return false;
|
|
617
|
+
}
|
|
618
|
+
const content = readFileSync(skill_md_path, "utf-8");
|
|
619
|
+
const path_format_result = validate_path_formats(content);
|
|
620
|
+
this.structured_validation.path_format = path_format_result.validation;
|
|
621
|
+
path_format_result.errors.forEach((err) => this.error(err.message));
|
|
622
|
+
const frontmatter_validation = validate_frontmatter_structure(content);
|
|
623
|
+
this.structured_validation.yaml_validation = frontmatter_validation;
|
|
624
|
+
if (!frontmatter_validation.valid) {
|
|
625
|
+
if (frontmatter_validation.parse_error) this.error(frontmatter_validation.parse_error);
|
|
626
|
+
frontmatter_validation.missing_fields.forEach((field) => {
|
|
627
|
+
this.error(`SKILL.md frontmatter missing '${field}' field`);
|
|
628
|
+
});
|
|
629
|
+
return false;
|
|
630
|
+
}
|
|
631
|
+
if (frontmatter_validation.unknown_fields?.length) for (const field of frontmatter_validation.unknown_fields) this.warning(`Unknown frontmatter field '${field}'\n → See https://code.claude.com/docs/en/skills#frontmatter-reference`);
|
|
632
|
+
if (frontmatter_validation.field_value_warnings?.length) for (const warn of frontmatter_validation.field_value_warnings) this.warning(warn);
|
|
633
|
+
const { name, description, body, description_is_multiline } = extract_frontmatter(content);
|
|
634
|
+
if (!name || !description) {
|
|
635
|
+
this.error("Failed to extract name or description from frontmatter");
|
|
636
|
+
return false;
|
|
637
|
+
}
|
|
638
|
+
if (description_is_multiline) this.warning(`Multi-line description detected. Claude Code cannot recognize skills with multi-line descriptions.\n → Run 'claude-skills-cli doctor ${this.skill_path}' to fix automatically`);
|
|
639
|
+
const name_validation = validate_name_format(name, this.skill_path.replace(/\/+$/, "").split("/").pop() || "");
|
|
640
|
+
this.structured_validation.name_format = name_validation;
|
|
641
|
+
name_validation.errors.forEach((err) => this.error(err));
|
|
642
|
+
const hard_limits = validate_hard_limits(name, description);
|
|
643
|
+
this.structured_validation.hard_limits = hard_limits;
|
|
644
|
+
if (!hard_limits.name.valid && hard_limits.name.error) this.error(hard_limits.name.error);
|
|
645
|
+
if (!hard_limits.description.valid && hard_limits.description.error) this.error(hard_limits.description.error);
|
|
646
|
+
const desc_validation = validate_description_content(description);
|
|
647
|
+
this.stats.description_length = desc_validation.stats.description_length;
|
|
648
|
+
this.stats.description_tokens = desc_validation.stats.description_tokens;
|
|
649
|
+
desc_validation.errors.forEach((err) => this.error(err.message));
|
|
650
|
+
desc_validation.warnings.forEach((warn) => this.warning(warn.message));
|
|
651
|
+
const trigger_analysis = analyze_trigger_phrase(description);
|
|
652
|
+
this.structured_validation.triggering.trigger_phrase = trigger_analysis;
|
|
653
|
+
if (!trigger_analysis.has_explicit_trigger) this.warning("Description missing explicit trigger phrase ('Use when...', 'Use for...', 'Use to...')\n → Help Claude know when to activate this skill");
|
|
654
|
+
const { analysis: phrasing_analysis, warnings: phrasing_warnings } = analyze_user_phrasing(description);
|
|
655
|
+
this.structured_validation.triggering.user_phrasing = phrasing_analysis;
|
|
656
|
+
phrasing_warnings.forEach((warn) => this.warning(warn.message));
|
|
657
|
+
const alignment_result = analyze_alignment(description, body);
|
|
658
|
+
this.structured_validation.triggering.keywords = alignment_result.keywords;
|
|
659
|
+
this.structured_validation.triggering.alignment = alignment_result.alignment;
|
|
660
|
+
alignment_result.warnings.forEach((warn) => this.warning(warn.message));
|
|
661
|
+
const content_validation = validate_content(body, { mode: this.options.mode });
|
|
662
|
+
this.stats.word_count = content_validation.stats.word_count;
|
|
663
|
+
this.stats.estimated_tokens = content_validation.stats.estimated_tokens;
|
|
664
|
+
this.stats.line_count = content_validation.stats.line_count;
|
|
665
|
+
this.stats.code_blocks = content_validation.stats.code_blocks;
|
|
666
|
+
this.stats.sections = content_validation.stats.sections;
|
|
667
|
+
this.stats.long_paragraphs = content_validation.stats.long_paragraphs;
|
|
668
|
+
content_validation.errors.forEach((err) => this.error(err.message));
|
|
669
|
+
content_validation.warnings.forEach((warn) => this.warning(warn.message));
|
|
670
|
+
return true;
|
|
671
|
+
}
|
|
672
|
+
validate_all() {
|
|
673
|
+
const dir_result = validate_directory(this.skill_path);
|
|
674
|
+
if (!dir_result.valid) {
|
|
675
|
+
dir_result.errors.forEach((err) => this.error(err.message));
|
|
676
|
+
return {
|
|
677
|
+
errors: this.errors,
|
|
678
|
+
warnings: this.warnings,
|
|
679
|
+
is_valid: false,
|
|
680
|
+
stats: this.stats,
|
|
681
|
+
validation: this.structured_validation
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
this.validate_skill_md();
|
|
685
|
+
const refs_result = validate_references(this.skill_path);
|
|
686
|
+
refs_result.errors.forEach((err) => this.error(err.message));
|
|
687
|
+
refs_result.warnings.forEach((warn) => this.warning(warn.message));
|
|
688
|
+
validate_scripts(this.skill_path).warnings.forEach((warn) => this.warning(warn.message));
|
|
689
|
+
validate_assets(this.skill_path).warnings.forEach((warn) => this.warning(warn.message));
|
|
690
|
+
const mode_limits = LIMITS[this.options.mode || "strict"];
|
|
691
|
+
const line_limit = mode_limits.lines.max;
|
|
692
|
+
const word_limit = mode_limits.words.max;
|
|
693
|
+
this.structured_validation.progressive_disclosure = {
|
|
694
|
+
skill_md_size: {
|
|
695
|
+
lines: this.stats.line_count,
|
|
696
|
+
words: this.stats.word_count,
|
|
697
|
+
tokens: this.stats.estimated_tokens,
|
|
698
|
+
exceeds_line_limit: this.stats.line_count > line_limit,
|
|
699
|
+
exceeds_word_limit: this.stats.word_count > word_limit
|
|
700
|
+
},
|
|
701
|
+
references: refs_result.validation
|
|
702
|
+
};
|
|
703
|
+
return {
|
|
704
|
+
errors: this.errors,
|
|
705
|
+
warnings: this.warnings,
|
|
706
|
+
is_valid: this.errors.length === 0,
|
|
707
|
+
stats: this.stats,
|
|
708
|
+
validation: this.structured_validation
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
get_errors() {
|
|
712
|
+
return this.errors;
|
|
713
|
+
}
|
|
714
|
+
get_warnings() {
|
|
715
|
+
return this.warnings;
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
//#endregion
|
|
719
|
+
export { SkillValidator as t };
|
|
720
|
+
|
|
721
|
+
//# sourceMappingURL=validator-DV5zeeel.js.map
|