skill-tools 0.2.0

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/index.js ADDED
@@ -0,0 +1,709 @@
1
+ import { readdirSync, existsSync, statSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { resolveSkillFiles, parseSkill } from '@skill-tools/core';
4
+
5
+ // src/rules/consistent-headings.ts
6
+ var consistentHeadings = {
7
+ id: "consistent-headings",
8
+ description: "Headings should follow a consistent hierarchy without skipping levels",
9
+ defaultSeverity: "info",
10
+ check(skill) {
11
+ const diagnostics = [];
12
+ const sections = skill.sections;
13
+ for (let i = 1; i < sections.length; i++) {
14
+ const prev = sections[i - 1];
15
+ const curr = sections[i];
16
+ if (curr.depth > prev.depth + 1) {
17
+ diagnostics.push({
18
+ ruleId: "consistent-headings",
19
+ severity: "info",
20
+ message: `Heading "${curr.heading}" (H${curr.depth}) skips levels from "${prev.heading}" (H${prev.depth})`,
21
+ file: skill.filePath,
22
+ line: curr.line,
23
+ fix: `Change to H${prev.depth + 1} for consistent hierarchy`
24
+ });
25
+ }
26
+ }
27
+ return diagnostics;
28
+ }
29
+ };
30
+
31
+ // src/rules/description-specificity.ts
32
+ var GENERIC_VERBS = [
33
+ "manage",
34
+ "handle",
35
+ "process",
36
+ "deal with",
37
+ "work with",
38
+ "do stuff",
39
+ "help with",
40
+ "assist with",
41
+ "take care of"
42
+ ];
43
+ var descriptionSpecificity = {
44
+ id: "description-specificity",
45
+ description: 'Description should contain specific nouns or action verbs, not generic phrases like "manages" or "handles"',
46
+ defaultSeverity: "warning",
47
+ check(skill) {
48
+ const desc = skill.metadata.description;
49
+ if (!desc) return [];
50
+ const diagnostics = [];
51
+ const lowerDesc = desc.toLowerCase();
52
+ const foundGeneric = GENERIC_VERBS.filter((verb) => lowerDesc.includes(verb));
53
+ if (foundGeneric.length > 0) {
54
+ diagnostics.push({
55
+ ruleId: "description-specificity",
56
+ severity: "warning",
57
+ message: `Description uses generic verbs: ${foundGeneric.map((v) => `"${v}"`).join(", ")}. Use specific action verbs instead`,
58
+ file: skill.filePath,
59
+ line: 1,
60
+ fix: 'Replace generic verbs with specific ones. E.g., "Deploy apps to Vercel" instead of "Handle Vercel deployments"'
61
+ });
62
+ }
63
+ return diagnostics;
64
+ }
65
+ };
66
+
67
+ // src/rules/description-trigger-keywords.ts
68
+ var descriptionTriggerKeywords = {
69
+ id: "description-trigger-keywords",
70
+ description: "Description should contain words that a user would naturally type to trigger this skill",
71
+ defaultSeverity: "warning",
72
+ check(skill) {
73
+ const desc = skill.metadata.description;
74
+ if (!desc) return [];
75
+ const diagnostics = [];
76
+ const hasTriggerPhrase = /\buse when\b|\buse for\b|\buse this\b|\binvoke when\b|\btrigger when\b/i.test(desc);
77
+ const hasActionVerb = /\b(deploy|test|build|run|create|generate|analyze|review|format|lint|fix|check|commit|push|pull|fetch|install|configure|setup|search|find|list|delete|update|migrate|convert|export|import)\b/i.test(
78
+ desc
79
+ );
80
+ if (!hasTriggerPhrase && !hasActionVerb) {
81
+ diagnostics.push({
82
+ ruleId: "description-trigger-keywords",
83
+ severity: "warning",
84
+ message: 'Description lacks trigger keywords. Include "Use when..." or specific action verbs (deploy, test, build, etc.)',
85
+ file: skill.filePath,
86
+ line: 1,
87
+ fix: 'Add trigger context: "Deploy apps to Vercel. Use when the user wants to publish or ship a web app."'
88
+ });
89
+ }
90
+ return diagnostics;
91
+ }
92
+ };
93
+
94
+ // src/rules/instructions-has-error-handling.ts
95
+ var instructionsHasErrorHandling = {
96
+ id: "instructions-has-error-handling",
97
+ description: "Instructions should mention what to do when things fail",
98
+ defaultSeverity: "info",
99
+ check(skill) {
100
+ const diagnostics = [];
101
+ const body = skill.body.toLowerCase();
102
+ const hasErrorSection = /#{1,3}\s+(?:error|troubleshoot|fail|issue|problem|debug)/i.test(
103
+ skill.body
104
+ );
105
+ const hasErrorKeywords = /\b(?:error|fail|troubleshoot|if .+ fails|when .+ fails|common issues|known issues)\b/.test(
106
+ body
107
+ );
108
+ if (!hasErrorSection && !hasErrorKeywords) {
109
+ diagnostics.push({
110
+ ruleId: "instructions-has-error-handling",
111
+ severity: "info",
112
+ message: "Instructions have no error handling guidance. Add a section about common failures",
113
+ file: skill.filePath,
114
+ fix: 'Add an "## Error Handling" section describing common failures and how to resolve them'
115
+ });
116
+ }
117
+ return diagnostics;
118
+ }
119
+ };
120
+
121
+ // src/rules/instructions-has-examples.ts
122
+ var instructionsHasExamples = {
123
+ id: "instructions-has-examples",
124
+ description: "Skill instructions should include at least one concrete example",
125
+ defaultSeverity: "info",
126
+ check(skill) {
127
+ const diagnostics = [];
128
+ const body = skill.body;
129
+ const hasCodeBlock = /```[\s\S]*?```/.test(body);
130
+ const hasExampleSection = /^#{1,3}\s+(?:example|usage|demo)/im.test(body);
131
+ const hasInlineCode = /`[^`]+`/.test(body);
132
+ const hasNumberedSteps = /^\d+\.\s+/m.test(body);
133
+ if (!hasCodeBlock && !hasExampleSection && !hasInlineCode && !hasNumberedSteps) {
134
+ diagnostics.push({
135
+ ruleId: "instructions-has-examples",
136
+ severity: "info",
137
+ message: "Instructions have no examples. Add code blocks, numbered steps, or an Examples section",
138
+ file: skill.filePath,
139
+ fix: "Add a code block showing expected usage:\n```bash\nskill-tools check ./my-skill/\n```"
140
+ });
141
+ }
142
+ return diagnostics;
143
+ }
144
+ };
145
+
146
+ // src/rules/no-hardcoded-paths.ts
147
+ var HARDCODED_PATH_PATTERN = /(?:\/Users\/\w+|\/home\/\w+|[A-Z]:\\\\?Users\\\\?\w+|\/var\/|\/tmp\/\w+)/;
148
+ var noHardcodedPaths = {
149
+ id: "no-hardcoded-paths",
150
+ description: "Flag absolute filesystem paths that are machine-specific",
151
+ defaultSeverity: "error",
152
+ check(skill) {
153
+ const diagnostics = [];
154
+ const lines = skill.body.split("\n");
155
+ for (let i = 0; i < lines.length; i++) {
156
+ const line = lines[i];
157
+ if (HARDCODED_PATH_PATTERN.test(line)) {
158
+ const match = HARDCODED_PATH_PATTERN.exec(line);
159
+ diagnostics.push({
160
+ ruleId: "no-hardcoded-paths",
161
+ severity: "error",
162
+ message: `Hardcoded path found: "${match?.[0]}". Use environment variables or relative paths`,
163
+ file: skill.filePath,
164
+ line: i + 1,
165
+ fix: "Replace with $HOME, relative paths, or environment variables"
166
+ });
167
+ }
168
+ }
169
+ return diagnostics;
170
+ }
171
+ };
172
+
173
+ // src/rules/no-secrets.ts
174
+ var SECRET_PATTERNS = [
175
+ { pattern: /sk-[a-zA-Z0-9]{20,}/, label: "OpenAI API key" },
176
+ { pattern: /sk_live_[a-zA-Z0-9]{20,}/, label: "Stripe live key" },
177
+ { pattern: /sk_test_[a-zA-Z0-9]{20,}/, label: "Stripe test key" },
178
+ { pattern: /ghp_[a-zA-Z0-9]{36,}/, label: "GitHub personal access token" },
179
+ { pattern: /gho_[a-zA-Z0-9]{36,}/, label: "GitHub OAuth token" },
180
+ { pattern: /github_pat_[a-zA-Z0-9_]{20,}/, label: "GitHub fine-grained token" },
181
+ { pattern: /xoxb-[a-zA-Z0-9-]+/, label: "Slack bot token" },
182
+ { pattern: /xoxp-[a-zA-Z0-9-]+/, label: "Slack user token" },
183
+ { pattern: /AKIA[0-9A-Z]{16}/, label: "AWS access key ID" },
184
+ { pattern: /-----BEGIN (?:RSA |EC )?PRIVATE KEY-----/, label: "Private key" },
185
+ { pattern: /eyJ[a-zA-Z0-9_-]{20,500}\.[a-zA-Z0-9_-]{20,500}\./, label: "JWT token" }
186
+ ];
187
+ var noSecrets = {
188
+ id: "no-secrets",
189
+ description: "Flag patterns that look like API keys, tokens, or passwords",
190
+ defaultSeverity: "error",
191
+ check(skill) {
192
+ const diagnostics = [];
193
+ const lines = skill.rawContent.split("\n");
194
+ for (let i = 0; i < lines.length; i++) {
195
+ const line = lines[i];
196
+ for (const { pattern, label } of SECRET_PATTERNS) {
197
+ if (pattern.test(line)) {
198
+ diagnostics.push({
199
+ ruleId: "no-secrets",
200
+ severity: "error",
201
+ message: `Possible ${label} detected. Never embed secrets in SKILL.md files`,
202
+ file: skill.filePath,
203
+ line: i + 1,
204
+ fix: "Use environment variable references (e.g., $API_KEY) instead of actual secrets"
205
+ });
206
+ break;
207
+ }
208
+ }
209
+ }
210
+ return diagnostics;
211
+ }
212
+ };
213
+
214
+ // src/rules/progressive-disclosure.ts
215
+ var DEFAULT_MAX_LINES = 500;
216
+ var progressiveDisclosure = {
217
+ id: "progressive-disclosure",
218
+ description: "Large SKILL.md files should use references/ or scripts/ to keep the main file lean",
219
+ defaultSeverity: "warning",
220
+ check(skill) {
221
+ const diagnostics = [];
222
+ if (skill.lineCount > DEFAULT_MAX_LINES) {
223
+ const hasReferences = skill.fileReferences.some((r) => r.path.startsWith("references/"));
224
+ const hasScripts = skill.fileReferences.some((r) => r.path.startsWith("scripts/"));
225
+ if (!hasReferences && !hasScripts) {
226
+ diagnostics.push({
227
+ ruleId: "progressive-disclosure",
228
+ severity: "warning",
229
+ message: `SKILL.md is ${skill.lineCount} lines (recommended: <${DEFAULT_MAX_LINES}). Move detailed content to references/ or scripts/`,
230
+ file: skill.filePath,
231
+ fix: "Create a references/ directory and move detailed API docs, examples, or reference tables there. Link from SKILL.md: [See API reference](references/api.md)"
232
+ });
233
+ }
234
+ }
235
+ return diagnostics;
236
+ }
237
+ };
238
+
239
+ // src/rules/index.ts
240
+ var builtinRules = /* @__PURE__ */ new Map([
241
+ [descriptionSpecificity.id, descriptionSpecificity],
242
+ [descriptionTriggerKeywords.id, descriptionTriggerKeywords],
243
+ [progressiveDisclosure.id, progressiveDisclosure],
244
+ [noHardcodedPaths.id, noHardcodedPaths],
245
+ [noSecrets.id, noSecrets],
246
+ [instructionsHasExamples.id, instructionsHasExamples],
247
+ [instructionsHasErrorHandling.id, instructionsHasErrorHandling],
248
+ [consistentHeadings.id, consistentHeadings]
249
+ ]);
250
+ var recommendedConfig = Object.fromEntries(
251
+ Array.from(builtinRules.values()).map((rule) => [rule.id, rule.defaultSeverity])
252
+ );
253
+
254
+ // src/linter.ts
255
+ function lint(skill, rulesConfig) {
256
+ const diagnostics = [];
257
+ for (const [ruleId, rule] of builtinRules) {
258
+ const configuredSeverity = rulesConfig?.[ruleId];
259
+ if (configuredSeverity === "off") continue;
260
+ const ruleDiagnostics = rule.check(skill);
261
+ const severity = configuredSeverity ?? rule.defaultSeverity;
262
+ for (const diag of ruleDiagnostics) {
263
+ diagnostics.push({
264
+ ...diag,
265
+ severity
266
+ });
267
+ }
268
+ }
269
+ return {
270
+ filePath: skill.filePath,
271
+ name: skill.metadata.name ?? "unknown",
272
+ diagnostics,
273
+ errorCount: diagnostics.filter((d) => d.severity === "error").length,
274
+ warningCount: diagnostics.filter((d) => d.severity === "warning").length,
275
+ infoCount: diagnostics.filter((d) => d.severity === "info").length
276
+ };
277
+ }
278
+ function parseRulesConfig(raw) {
279
+ const config = {};
280
+ const validSeverities = /* @__PURE__ */ new Set(["error", "warning", "info", "off"]);
281
+ for (const [key, value] of Object.entries(raw)) {
282
+ if (typeof value === "string" && validSeverities.has(value)) {
283
+ config[key] = value;
284
+ }
285
+ }
286
+ return config;
287
+ }
288
+
289
+ // src/scorer/description-quality.ts
290
+ var MAX_POINTS = 30;
291
+ function scoreDescriptionQuality(skill) {
292
+ const desc = skill.metadata.description;
293
+ if (!desc) {
294
+ return {
295
+ score: 0,
296
+ max: MAX_POINTS,
297
+ label: "Description Quality",
298
+ details: "No description provided"
299
+ };
300
+ }
301
+ let points = 0;
302
+ const details = [];
303
+ const len = desc.length;
304
+ if (len >= 50 && len <= 300) {
305
+ points += 8;
306
+ } else if (len >= 30 && len <= 400) {
307
+ points += 5;
308
+ } else if (len >= 10) {
309
+ points += 2;
310
+ }
311
+ details.push(`Length: ${len} chars`);
312
+ const actionVerbs = /\b(deploy|test|build|run|create|generate|analyze|review|format|lint|fix|check|commit|push|pull|fetch|install|configure|search|find|list|delete|update|migrate|convert|export|import)\b/gi;
313
+ const verbCount = (desc.match(actionVerbs) ?? []).length;
314
+ if (verbCount >= 2) {
315
+ points += 8;
316
+ } else if (verbCount === 1) {
317
+ points += 5;
318
+ }
319
+ details.push(`Action verbs: ${verbCount}`);
320
+ const hasTriggerContext = /\buse when\b|\buse for\b|\buse this\b|\binvoke when\b|\bwhen the user\b/i.test(desc);
321
+ if (hasTriggerContext) {
322
+ points += 8;
323
+ details.push("Has trigger context");
324
+ }
325
+ const name = skill.metadata.name ?? "";
326
+ const nameWords = name.split("-").filter((w) => w.length > 2);
327
+ const descLower = desc.toLowerCase();
328
+ const nameRepetitions = nameWords.filter((w) => descLower.includes(w)).length;
329
+ const ratio = nameWords.length > 0 ? nameRepetitions / nameWords.length : 0;
330
+ if (ratio < 0.5) {
331
+ points += 6;
332
+ } else if (ratio < 0.8) {
333
+ points += 3;
334
+ }
335
+ return {
336
+ score: Math.min(points, MAX_POINTS),
337
+ max: MAX_POINTS,
338
+ label: "Description Quality",
339
+ details: details.join(", ")
340
+ };
341
+ }
342
+
343
+ // src/scorer/instruction-clarity.ts
344
+ var MAX_POINTS2 = 25;
345
+ function scoreInstructionClarity(skill) {
346
+ const body = skill.body;
347
+ if (!body || body.trim().length === 0) {
348
+ return { score: 0, max: MAX_POINTS2, label: "Instruction Clarity", details: "No instructions" };
349
+ }
350
+ let points = 0;
351
+ const details = [];
352
+ const codeBlockCount = (body.match(/```[\s\S]*?```/g) ?? []).length;
353
+ if (codeBlockCount >= 2) {
354
+ points += 7;
355
+ details.push(`${codeBlockCount} code blocks`);
356
+ } else if (codeBlockCount === 1) {
357
+ points += 4;
358
+ details.push("1 code block");
359
+ }
360
+ const numberedSteps = (body.match(/^\d+\.\s+/gm) ?? []).length;
361
+ if (numberedSteps >= 3) {
362
+ points += 6;
363
+ details.push(`${numberedSteps} numbered steps`);
364
+ } else if (numberedSteps >= 1) {
365
+ points += 3;
366
+ details.push(`${numberedSteps} numbered steps`);
367
+ }
368
+ const hasErrorSection = /#{1,3}\s+(?:error|troubleshoot|fail|issue|problem|debug)/i.test(body);
369
+ const hasErrorKeywords = /\b(?:error|fail|troubleshoot|if .+ fails|when .+ fails|common issues)\b/i.test(body);
370
+ if (hasErrorSection) {
371
+ points += 6;
372
+ details.push("Error handling section");
373
+ } else if (hasErrorKeywords) {
374
+ points += 3;
375
+ details.push("Error mentions");
376
+ }
377
+ const wordCount = body.split(/\s+/).length;
378
+ if (wordCount >= 100) {
379
+ points += 6;
380
+ } else if (wordCount >= 50) {
381
+ points += 4;
382
+ } else if (wordCount >= 20) {
383
+ points += 2;
384
+ }
385
+ details.push(`${wordCount} words`);
386
+ return {
387
+ score: Math.min(points, MAX_POINTS2),
388
+ max: MAX_POINTS2,
389
+ label: "Instruction Clarity",
390
+ details: details.join(", ")
391
+ };
392
+ }
393
+
394
+ // src/scorer/progressive-disclosure-score.ts
395
+ var MAX_POINTS3 = 15;
396
+ function scoreProgressiveDisclosure(skill) {
397
+ let points = 0;
398
+ const details = [];
399
+ const isSmall = skill.lineCount <= 100;
400
+ const isMedium = skill.lineCount <= 500;
401
+ if (isSmall) {
402
+ points += 15;
403
+ details.push("Skill is compact");
404
+ } else if (isMedium) {
405
+ points += 8;
406
+ const hasRefs = skill.fileReferences.some(
407
+ (r) => r.path.startsWith("references/") || r.path.startsWith("scripts/")
408
+ );
409
+ if (hasRefs) {
410
+ points += 7;
411
+ details.push("Uses supporting files");
412
+ } else {
413
+ details.push("Could benefit from references/");
414
+ }
415
+ } else {
416
+ const hasRefs = skill.fileReferences.some(
417
+ (r) => r.path.startsWith("references/") || r.path.startsWith("scripts/")
418
+ );
419
+ if (hasRefs) {
420
+ points += 10;
421
+ details.push("Uses supporting files (recommended: further reduce main file)");
422
+ } else {
423
+ points += 2;
424
+ details.push("Large file without progressive disclosure");
425
+ }
426
+ }
427
+ return {
428
+ score: Math.min(points, MAX_POINTS3),
429
+ max: MAX_POINTS3,
430
+ label: "Progressive Disclosure",
431
+ details: details.join(", ")
432
+ };
433
+ }
434
+
435
+ // src/scorer/security-score.ts
436
+ var MAX_POINTS4 = 10;
437
+ function scoreSecurityScore(skill) {
438
+ let points = MAX_POINTS4;
439
+ const details = [];
440
+ const secretDiags = noSecrets.check(skill);
441
+ if (secretDiags.length > 0) {
442
+ points -= 10;
443
+ details.push(`${secretDiags.length} possible secret(s) found`);
444
+ }
445
+ const pathDiags = noHardcodedPaths.check(skill);
446
+ if (pathDiags.length > 0) {
447
+ points -= 3;
448
+ details.push(`${pathDiags.length} hardcoded path(s)`);
449
+ }
450
+ const body = skill.body;
451
+ const suspiciousPatterns = [
452
+ /\brm\s+-rf\s+\/(?!\s)/,
453
+ // rm -rf / (not followed by space)
454
+ /\bcurl\s+.*\|\s*(?:bash|sh)\b/,
455
+ // curl | bash
456
+ /\beval\s+\$/
457
+ // eval $
458
+ ];
459
+ for (const pattern of suspiciousPatterns) {
460
+ if (pattern.test(body)) {
461
+ points -= 2;
462
+ details.push("Suspicious shell pattern");
463
+ break;
464
+ }
465
+ }
466
+ if (details.length === 0) {
467
+ details.push("No security issues");
468
+ }
469
+ return {
470
+ score: Math.max(0, points),
471
+ max: MAX_POINTS4,
472
+ label: "Security",
473
+ details: details.join(", ")
474
+ };
475
+ }
476
+
477
+ // src/scorer/spec-compliance.ts
478
+ var MAX_POINTS5 = 20;
479
+ function scoreSpecCompliance(skill) {
480
+ let points = 0;
481
+ const details = [];
482
+ if (skill.metadata.name) {
483
+ points += 5;
484
+ details.push("Has name");
485
+ }
486
+ if (skill.metadata.description) {
487
+ points += 5;
488
+ details.push("Has description");
489
+ }
490
+ if (skill.tokenCount <= 5e3) {
491
+ points += 5;
492
+ details.push(`${skill.tokenCount} tokens (within budget)`);
493
+ } else if (skill.tokenCount <= 7500) {
494
+ points += 2;
495
+ details.push(`${skill.tokenCount} tokens (over budget)`);
496
+ } else {
497
+ details.push(`${skill.tokenCount} tokens (far over budget)`);
498
+ }
499
+ if (skill.lineCount <= 500) {
500
+ points += 5;
501
+ details.push(`${skill.lineCount} lines`);
502
+ } else if (skill.lineCount <= 750) {
503
+ points += 2;
504
+ details.push(`${skill.lineCount} lines (over recommendation)`);
505
+ } else {
506
+ details.push(`${skill.lineCount} lines (far over recommendation)`);
507
+ }
508
+ return {
509
+ score: Math.min(points, MAX_POINTS5),
510
+ max: MAX_POINTS5,
511
+ label: "Spec Compliance",
512
+ details: details.join(", ")
513
+ };
514
+ }
515
+
516
+ // src/scorer/index.ts
517
+ function score(skill) {
518
+ const descriptionQuality = scoreDescriptionQuality(skill);
519
+ const instructionClarity = scoreInstructionClarity(skill);
520
+ const specCompliance = scoreSpecCompliance(skill);
521
+ const progressiveDisclosure2 = scoreProgressiveDisclosure(skill);
522
+ const security = scoreSecurityScore(skill);
523
+ const totalScore = descriptionQuality.score + instructionClarity.score + specCompliance.score + progressiveDisclosure2.score + security.score;
524
+ const dimensions = {
525
+ description_quality: descriptionQuality,
526
+ instruction_clarity: instructionClarity,
527
+ spec_compliance: specCompliance,
528
+ progressive_disclosure: progressiveDisclosure2,
529
+ security
530
+ };
531
+ const suggestions = generateSuggestions(skill, dimensions);
532
+ return {
533
+ score: totalScore,
534
+ dimensions,
535
+ suggestions
536
+ };
537
+ }
538
+ function generateSuggestions(skill, dimensions) {
539
+ const suggestions = [];
540
+ const descDim = dimensions.description_quality;
541
+ const instrDim = dimensions.instruction_clarity;
542
+ const progDim = dimensions.progressive_disclosure;
543
+ const secDim = dimensions.security;
544
+ if (!skill.metadata.description) {
545
+ suggestions.push({
546
+ message: "Add a description field to the frontmatter",
547
+ pointsGain: 15,
548
+ dimension: "description_quality"
549
+ });
550
+ } else {
551
+ if (descDim.score < descDim.max * 0.6) {
552
+ if (!/\buse when\b/i.test(skill.metadata.description)) {
553
+ suggestions.push({
554
+ message: 'Add "Use when..." to the description to clarify trigger conditions',
555
+ pointsGain: 8,
556
+ dimension: "description_quality"
557
+ });
558
+ }
559
+ }
560
+ }
561
+ if (instrDim.score < instrDim.max * 0.5) {
562
+ const hasCodeBlock = /```[\s\S]*?```/.test(skill.body);
563
+ if (!hasCodeBlock) {
564
+ suggestions.push({
565
+ message: "Add a concrete usage example with a code block",
566
+ pointsGain: 5,
567
+ dimension: "instruction_clarity"
568
+ });
569
+ }
570
+ const hasNumberedSteps = /^\d+\.\s+/m.test(skill.body);
571
+ if (!hasNumberedSteps) {
572
+ suggestions.push({
573
+ message: "Add numbered steps for the main workflow",
574
+ pointsGain: 4,
575
+ dimension: "instruction_clarity"
576
+ });
577
+ }
578
+ const hasErrorHandling = /#{1,3}\s+(?:error|troubleshoot)/i.test(skill.body);
579
+ if (!hasErrorHandling) {
580
+ suggestions.push({
581
+ message: 'Add an "## Error Handling" section',
582
+ pointsGain: 3,
583
+ dimension: "instruction_clarity"
584
+ });
585
+ }
586
+ }
587
+ if (!skill.metadata.name) {
588
+ suggestions.push({
589
+ message: "Add a name field to the frontmatter",
590
+ pointsGain: 5,
591
+ dimension: "spec_compliance"
592
+ });
593
+ }
594
+ if (skill.tokenCount > 5e3) {
595
+ suggestions.push({
596
+ message: `Reduce token count from ${skill.tokenCount} to under 5,000`,
597
+ pointsGain: 3,
598
+ dimension: "spec_compliance"
599
+ });
600
+ }
601
+ if (progDim.score < progDim.max * 0.5 && skill.lineCount > 200) {
602
+ suggestions.push({
603
+ message: "Move detailed reference content to a references/ directory",
604
+ pointsGain: 5,
605
+ dimension: "progressive_disclosure"
606
+ });
607
+ }
608
+ if (secDim.score < secDim.max) {
609
+ suggestions.push({
610
+ message: "Fix security issues (secrets, hardcoded paths, or suspicious patterns)",
611
+ pointsGain: secDim.max - secDim.score,
612
+ dimension: "security"
613
+ });
614
+ }
615
+ suggestions.sort((a, b) => b.pointsGain - a.pointsGain);
616
+ return suggestions;
617
+ }
618
+ async function validate(path) {
619
+ const locations = await resolveSkillFiles(path);
620
+ if (locations.length === 0) {
621
+ return [
622
+ {
623
+ filePath: path,
624
+ name: "unknown",
625
+ valid: false,
626
+ skill: null,
627
+ diagnostics: [
628
+ {
629
+ ruleId: "skill-not-found",
630
+ severity: "error",
631
+ message: `No SKILL.md file found at path: ${path}`,
632
+ file: path
633
+ }
634
+ ]
635
+ }
636
+ ];
637
+ }
638
+ const results = [];
639
+ for (const location of locations) {
640
+ const parseResult = await parseSkill(location.skillFile);
641
+ const extraDiagnostics = parseResult.ok ? validateStructure(parseResult.skill) : [];
642
+ const allDiagnostics = [...parseResult.diagnostics, ...extraDiagnostics];
643
+ const hasErrors = allDiagnostics.some((d) => d.severity === "error");
644
+ results.push({
645
+ filePath: location.skillFile,
646
+ name: parseResult.ok ? parseResult.skill.metadata.name ?? location.dirName : location.dirName,
647
+ valid: !hasErrors,
648
+ skill: parseResult.ok ? parseResult.skill : null,
649
+ diagnostics: allDiagnostics
650
+ });
651
+ }
652
+ return results;
653
+ }
654
+ function validateStructure(skill) {
655
+ const diagnostics = [];
656
+ const dirPath = skill.dirPath;
657
+ const expectedDirs = ["scripts", "references", "assets", "examples", "agents"];
658
+ const entries = safeReaddir(dirPath);
659
+ for (const entry of entries) {
660
+ if (entry === "SKILL.md" || entry.startsWith(".")) continue;
661
+ const entryPath = join(dirPath, entry);
662
+ const isDir = safeIsDirectory(entryPath);
663
+ if (isDir && !expectedDirs.includes(entry)) {
664
+ diagnostics.push({
665
+ ruleId: "unexpected-directory",
666
+ severity: "info",
667
+ message: `Unexpected directory "${entry}" in skill directory. Expected: ${expectedDirs.join(", ")}`,
668
+ file: skill.filePath
669
+ });
670
+ }
671
+ }
672
+ if (skill.rawContent.charCodeAt(0) === 65279) {
673
+ diagnostics.push({
674
+ ruleId: "no-bom",
675
+ severity: "warning",
676
+ message: "SKILL.md contains a UTF-8 BOM (byte order mark). Remove it for compatibility",
677
+ file: skill.filePath,
678
+ line: 1,
679
+ fix: "Save the file without BOM"
680
+ });
681
+ }
682
+ if (skill.rawContent.includes("\0")) {
683
+ diagnostics.push({
684
+ ruleId: "no-binary",
685
+ severity: "error",
686
+ message: "SKILL.md contains binary content (null bytes). It must be a text file",
687
+ file: skill.filePath
688
+ });
689
+ }
690
+ return diagnostics;
691
+ }
692
+ function safeReaddir(dirPath) {
693
+ try {
694
+ return readdirSync(dirPath).map(String);
695
+ } catch {
696
+ return [];
697
+ }
698
+ }
699
+ function safeIsDirectory(path) {
700
+ try {
701
+ return existsSync(path) && statSync(path).isDirectory();
702
+ } catch {
703
+ return false;
704
+ }
705
+ }
706
+
707
+ export { builtinRules, lint, parseRulesConfig, recommendedConfig, score, validate };
708
+ //# sourceMappingURL=index.js.map
709
+ //# sourceMappingURL=index.js.map