@grimoire-cc/cli 0.6.3 → 0.7.1
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/commands/logs.d.ts.map +1 -1
- package/dist/commands/logs.js +2 -2
- package/dist/commands/logs.js.map +1 -1
- package/dist/static/log-viewer.html +946 -690
- package/dist/static/static/log-viewer.html +946 -690
- package/package.json +1 -1
- package/packs/dev-pack/agents/gr.code-reviewer.md +286 -0
- package/packs/dev-pack/agents/gr.tdd-specialist.md +44 -0
- package/packs/dev-pack/grimoire.json +55 -0
- package/packs/dev-pack/skills/gr.tdd-specialist/SKILL.md +247 -0
- package/packs/dev-pack/skills/gr.tdd-specialist/reference/anti-patterns.md +166 -0
- package/packs/dev-pack/skills/gr.tdd-specialist/reference/language-frameworks.md +388 -0
- package/packs/dev-pack/skills/gr.tdd-specialist/reference/tdd-workflow-patterns.md +135 -0
- package/packs/docs-pack/grimoire.json +30 -0
- package/packs/docs-pack/skills/gr.business-logic-docs/SKILL.md +278 -0
- package/packs/docs-pack/skills/gr.business-logic-docs/references/audit-checklist.md +48 -0
- package/packs/docs-pack/skills/gr.business-logic-docs/references/tier2-template.md +129 -0
- package/packs/essentials-pack/agents/gr.fact-checker.md +202 -0
- package/packs/essentials-pack/grimoire.json +12 -0
- package/packs/meta-pack/grimoire.json +72 -0
- package/packs/meta-pack/skills/gr.context-file-guide/SKILL.md +201 -0
- package/packs/meta-pack/skills/gr.context-file-guide/scripts/validate-context-file.sh +29 -0
- package/packs/meta-pack/skills/gr.readme-guide/SKILL.md +362 -0
- package/packs/meta-pack/skills/gr.skill-developer/SKILL.md +321 -0
- package/packs/meta-pack/skills/gr.skill-developer/examples/brand-guidelines.md +94 -0
- package/packs/meta-pack/skills/gr.skill-developer/examples/financial-analysis.md +85 -0
- package/packs/meta-pack/skills/gr.skill-developer/reference/best-practices.md +410 -0
- package/packs/meta-pack/skills/gr.skill-developer/reference/file-organization.md +452 -0
- package/packs/meta-pack/skills/gr.skill-developer/reference/patterns.md +459 -0
- package/packs/meta-pack/skills/gr.skill-developer/reference/yaml-spec.md +214 -0
- package/packs/meta-pack/skills/gr.skill-developer/scripts/create-skill.sh +210 -0
- package/packs/meta-pack/skills/gr.skill-developer/scripts/validate-skill.py +520 -0
- package/packs/meta-pack/skills/gr.skill-developer/templates/basic-skill.md +94 -0
- package/packs/meta-pack/skills/gr.skill-developer/templates/domain-skill.md +108 -0
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
validate-skill.py - Validate Claude Code skill YAML frontmatter and structure
|
|
4
|
+
|
|
5
|
+
Usage: ./validate-skill.py <path-to-SKILL.md>
|
|
6
|
+
|
|
7
|
+
This script validates skill files against official Anthropic requirements:
|
|
8
|
+
- YAML frontmatter format
|
|
9
|
+
- Name field (length, format, reserved words)
|
|
10
|
+
- Description field (length, content)
|
|
11
|
+
- Directory name matching
|
|
12
|
+
- SKILL.md body size (500 line limit)
|
|
13
|
+
- Total skill bundle size (8MB limit)
|
|
14
|
+
- Reference file table of contents (>100 lines)
|
|
15
|
+
- Reference file linking depth
|
|
16
|
+
|
|
17
|
+
Exit codes:
|
|
18
|
+
0 - Validation passed
|
|
19
|
+
1 - Validation failed
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import sys
|
|
23
|
+
import re
|
|
24
|
+
import os
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Tuple, List
|
|
27
|
+
|
|
28
|
+
# ANSI color codes
|
|
29
|
+
RED = '\033[0;31m'
|
|
30
|
+
GREEN = '\033[0;32m'
|
|
31
|
+
YELLOW = '\033[1;33m'
|
|
32
|
+
NC = '\033[0m' # No Color
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def error(message):
|
|
36
|
+
"""Print error message in red"""
|
|
37
|
+
print(f"{RED}✗ {message}{NC}", file=sys.stderr)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def success(message):
|
|
41
|
+
"""Print success message in green"""
|
|
42
|
+
print(f"{GREEN}✓ {message}{NC}")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def warning(message):
|
|
46
|
+
"""Print warning message in yellow"""
|
|
47
|
+
print(f"{YELLOW}⚠ {message}{NC}")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def validate_yaml_frontmatter(content):
|
|
51
|
+
"""Extract and validate YAML frontmatter"""
|
|
52
|
+
# Check for YAML frontmatter delimiters
|
|
53
|
+
if not content.startswith('---\n'):
|
|
54
|
+
error("SKILL.md must start with '---' on its own line")
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
# Find the closing delimiter
|
|
58
|
+
lines = content.split('\n')
|
|
59
|
+
closing_index = None
|
|
60
|
+
|
|
61
|
+
for i, line in enumerate(lines[1:], 1):
|
|
62
|
+
if line.strip() == '---':
|
|
63
|
+
closing_index = i
|
|
64
|
+
break
|
|
65
|
+
|
|
66
|
+
if closing_index is None:
|
|
67
|
+
error("YAML frontmatter must end with '---' on its own line")
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
# Extract YAML content
|
|
71
|
+
yaml_lines = lines[1:closing_index]
|
|
72
|
+
yaml_content = '\n'.join(yaml_lines)
|
|
73
|
+
|
|
74
|
+
# Parse YAML fields (simple parser for name and description)
|
|
75
|
+
frontmatter = {}
|
|
76
|
+
|
|
77
|
+
for line in yaml_lines:
|
|
78
|
+
if ':' in line:
|
|
79
|
+
key, value = line.split(':', 1)
|
|
80
|
+
key = key.strip()
|
|
81
|
+
value = value.strip()
|
|
82
|
+
|
|
83
|
+
# Handle quoted values
|
|
84
|
+
if value.startswith('"') and value.endswith('"'):
|
|
85
|
+
value = value[1:-1]
|
|
86
|
+
|
|
87
|
+
frontmatter[key] = value
|
|
88
|
+
|
|
89
|
+
return frontmatter
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def validate_name_field(name):
|
|
93
|
+
"""Validate the 'name' field"""
|
|
94
|
+
errors = []
|
|
95
|
+
warnings_list = []
|
|
96
|
+
|
|
97
|
+
# Check if name exists
|
|
98
|
+
if not name:
|
|
99
|
+
errors.append("'name' field is required and cannot be empty")
|
|
100
|
+
return errors, warnings_list
|
|
101
|
+
|
|
102
|
+
# Check length
|
|
103
|
+
if len(name) > 64:
|
|
104
|
+
errors.append(f"'name' must be ≤64 characters (got {len(name)})")
|
|
105
|
+
|
|
106
|
+
# Check for uppercase
|
|
107
|
+
if re.search(r'[A-Z]', name):
|
|
108
|
+
errors.append("'name' must be lowercase (found uppercase letters)")
|
|
109
|
+
|
|
110
|
+
# Check for invalid characters
|
|
111
|
+
if not re.match(r'^[a-z0-9-]+$', name):
|
|
112
|
+
errors.append("'name' can only contain lowercase letters, numbers, and hyphens")
|
|
113
|
+
|
|
114
|
+
# Check for reserved words
|
|
115
|
+
if 'anthropic' in name or 'claude' in name:
|
|
116
|
+
errors.append("'name' cannot contain reserved words 'anthropic' or 'claude'")
|
|
117
|
+
|
|
118
|
+
# Check if starts/ends with hyphen
|
|
119
|
+
if name.startswith('-') or name.endswith('-'):
|
|
120
|
+
errors.append("'name' cannot start or end with hyphen")
|
|
121
|
+
|
|
122
|
+
# Check for XML tags
|
|
123
|
+
if '<' in name or '>' in name:
|
|
124
|
+
errors.append("'name' cannot contain XML tags")
|
|
125
|
+
|
|
126
|
+
return errors, warnings_list
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def validate_description_field(description):
|
|
130
|
+
"""Validate the 'description' field"""
|
|
131
|
+
errors = []
|
|
132
|
+
warnings_list = []
|
|
133
|
+
|
|
134
|
+
# Check if description exists
|
|
135
|
+
if not description:
|
|
136
|
+
errors.append("'description' field is required and cannot be empty")
|
|
137
|
+
return errors, warnings_list
|
|
138
|
+
|
|
139
|
+
# Check length
|
|
140
|
+
if len(description) > 1024:
|
|
141
|
+
errors.append(f"'description' must be ≤1024 characters (got {len(description)})")
|
|
142
|
+
|
|
143
|
+
# Check for XML tags
|
|
144
|
+
if '<' in description or '>' in description:
|
|
145
|
+
errors.append("'description' cannot contain XML tags")
|
|
146
|
+
|
|
147
|
+
# Check for trigger keywords (heuristic)
|
|
148
|
+
# Description should be reasonably descriptive (> 20 chars)
|
|
149
|
+
if len(description) < 20:
|
|
150
|
+
warnings_list.append("'description' seems very short - include WHAT and WHEN (trigger keywords)")
|
|
151
|
+
|
|
152
|
+
# Check for common action verbs (good practice)
|
|
153
|
+
action_verbs = ['calculate', 'analyze', 'apply', 'create', 'generate', 'validate',
|
|
154
|
+
'format', 'process', 'convert', 'provide', 'use when', 'helps with']
|
|
155
|
+
has_action = any(verb in description.lower() for verb in action_verbs)
|
|
156
|
+
|
|
157
|
+
if not has_action:
|
|
158
|
+
warnings_list.append("'description' should include action verbs or 'use when' for better discoverability")
|
|
159
|
+
|
|
160
|
+
return errors, warnings_list
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def validate_directory_name(skill_file_path, frontmatter):
|
|
164
|
+
"""Validate that directory name matches the 'name' field"""
|
|
165
|
+
errors = []
|
|
166
|
+
warnings_list = []
|
|
167
|
+
|
|
168
|
+
skill_path = Path(skill_file_path)
|
|
169
|
+
directory_name = skill_path.parent.name
|
|
170
|
+
skill_name = frontmatter.get('name', '')
|
|
171
|
+
|
|
172
|
+
if directory_name != skill_name:
|
|
173
|
+
errors.append(
|
|
174
|
+
f"Directory name '{directory_name}' does not match skill name '{skill_name}'\n"
|
|
175
|
+
f" Expected directory: .claude/skills/{skill_name}/"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
return errors, warnings_list
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def count_body_lines(content: str) -> Tuple[int, int]:
|
|
182
|
+
"""
|
|
183
|
+
Count lines in SKILL.md body (excluding YAML frontmatter)
|
|
184
|
+
Returns: (total_lines, body_lines)
|
|
185
|
+
"""
|
|
186
|
+
lines = content.split('\n')
|
|
187
|
+
|
|
188
|
+
# Find the end of YAML frontmatter
|
|
189
|
+
closing_index = None
|
|
190
|
+
for i, line in enumerate(lines[1:], 1):
|
|
191
|
+
if line.strip() == '---':
|
|
192
|
+
closing_index = i
|
|
193
|
+
break
|
|
194
|
+
|
|
195
|
+
if closing_index is None:
|
|
196
|
+
# No valid frontmatter, count all lines
|
|
197
|
+
return len(lines), len(lines)
|
|
198
|
+
|
|
199
|
+
# Body starts after closing ---
|
|
200
|
+
body_lines = lines[closing_index + 1:]
|
|
201
|
+
return len(lines), len(body_lines)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def validate_skill_size(skill_file_path: str, content: str) -> Tuple[List[str], List[str]]:
|
|
205
|
+
"""Validate SKILL.md body size (500 line limit)"""
|
|
206
|
+
errors = []
|
|
207
|
+
warnings_list = []
|
|
208
|
+
|
|
209
|
+
total_lines, body_lines = count_body_lines(content)
|
|
210
|
+
|
|
211
|
+
if body_lines > 500:
|
|
212
|
+
errors.append(
|
|
213
|
+
f"SKILL.md body exceeds 500 line limit ({body_lines} lines)\n"
|
|
214
|
+
f" Lines to remove: {body_lines - 500}\n"
|
|
215
|
+
f" How to fix:\n"
|
|
216
|
+
f" - Extract detailed sections to reference/ files\n"
|
|
217
|
+
f" - Move full examples to examples/ directory\n"
|
|
218
|
+
f" - Keep only essential instructions in SKILL.md"
|
|
219
|
+
)
|
|
220
|
+
elif body_lines > 400:
|
|
221
|
+
warnings_list.append(
|
|
222
|
+
f"SKILL.md body is approaching 500 line limit ({body_lines}/500 lines)\n"
|
|
223
|
+
f" Consider splitting content if adding more material"
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
return errors, warnings_list
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def get_directory_size(directory: Path) -> int:
|
|
230
|
+
"""Calculate total size of directory in bytes"""
|
|
231
|
+
total_size = 0
|
|
232
|
+
for item in directory.rglob('*'):
|
|
233
|
+
if item.is_file():
|
|
234
|
+
total_size += item.stat().st_size
|
|
235
|
+
return total_size
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def format_size(size_bytes: int) -> str:
|
|
239
|
+
"""Format bytes as human-readable string"""
|
|
240
|
+
for unit in ['B', 'KB', 'MB', 'GB']:
|
|
241
|
+
if size_bytes < 1024.0:
|
|
242
|
+
return f"{size_bytes:.2f} {unit}"
|
|
243
|
+
size_bytes /= 1024.0
|
|
244
|
+
return f"{size_bytes:.2f} TB"
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def get_largest_files(directory: Path, top_n: int = 5) -> List[Tuple[Path, int]]:
|
|
248
|
+
"""Get the N largest files in directory"""
|
|
249
|
+
files = []
|
|
250
|
+
for item in directory.rglob('*'):
|
|
251
|
+
if item.is_file():
|
|
252
|
+
files.append((item, item.stat().st_size))
|
|
253
|
+
|
|
254
|
+
files.sort(key=lambda x: x[1], reverse=True)
|
|
255
|
+
return files[:top_n]
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def validate_bundle_size(skill_file_path: str) -> Tuple[List[str], List[str]]:
|
|
259
|
+
"""Validate total skill bundle size (8MB limit)"""
|
|
260
|
+
errors = []
|
|
261
|
+
warnings_list = []
|
|
262
|
+
|
|
263
|
+
skill_path = Path(skill_file_path)
|
|
264
|
+
skill_dir = skill_path.parent
|
|
265
|
+
|
|
266
|
+
total_size = get_directory_size(skill_dir)
|
|
267
|
+
max_size = 8 * 1024 * 1024 # 8MB in bytes
|
|
268
|
+
|
|
269
|
+
if total_size > max_size:
|
|
270
|
+
overage = total_size - max_size
|
|
271
|
+
largest_files = get_largest_files(skill_dir, top_n=5)
|
|
272
|
+
|
|
273
|
+
files_list = "\n".join([
|
|
274
|
+
f" - {f.relative_to(skill_dir)}: {format_size(size)}"
|
|
275
|
+
for f, size in largest_files
|
|
276
|
+
])
|
|
277
|
+
|
|
278
|
+
errors.append(
|
|
279
|
+
f"Total skill bundle exceeds 8MB limit ({format_size(total_size)})\n"
|
|
280
|
+
f" Overage: {format_size(overage)}\n"
|
|
281
|
+
f" Largest files:\n{files_list}\n"
|
|
282
|
+
f" How to fix:\n"
|
|
283
|
+
f" - Remove redundant content\n"
|
|
284
|
+
f" - Compress or remove large images\n"
|
|
285
|
+
f" - Split large files by topic\n"
|
|
286
|
+
f" - Use external resources for very large datasets"
|
|
287
|
+
)
|
|
288
|
+
elif total_size > max_size * 0.75: # Warn at 75% (6MB)
|
|
289
|
+
warnings_list.append(
|
|
290
|
+
f"Skill bundle is approaching 8MB limit ({format_size(total_size)}/8MB)\n"
|
|
291
|
+
f" Consider optimizing if adding more content"
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
return errors, warnings_list
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def has_table_of_contents(file_path: Path) -> bool:
|
|
298
|
+
"""Check if a file has a table of contents"""
|
|
299
|
+
try:
|
|
300
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
301
|
+
content = f.read().lower()
|
|
302
|
+
# Look for common TOC patterns
|
|
303
|
+
return ('## table of contents' in content or
|
|
304
|
+
'## contents' in content or
|
|
305
|
+
'## toc' in content)
|
|
306
|
+
except:
|
|
307
|
+
return False
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def count_file_lines(file_path: Path) -> int:
|
|
311
|
+
"""Count lines in a file"""
|
|
312
|
+
try:
|
|
313
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
314
|
+
return len(f.readlines())
|
|
315
|
+
except:
|
|
316
|
+
return 0
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def validate_reference_files(skill_file_path: str, content: str) -> Tuple[List[str], List[str]]:
|
|
320
|
+
"""Validate reference files (TOC requirement, linking depth)"""
|
|
321
|
+
errors = []
|
|
322
|
+
warnings_list = []
|
|
323
|
+
|
|
324
|
+
skill_path = Path(skill_file_path)
|
|
325
|
+
skill_dir = skill_path.parent
|
|
326
|
+
reference_dir = skill_dir / 'reference'
|
|
327
|
+
|
|
328
|
+
if not reference_dir.exists():
|
|
329
|
+
# No reference directory, nothing to validate
|
|
330
|
+
return errors, warnings_list
|
|
331
|
+
|
|
332
|
+
# Check for reference files >100 lines without TOC
|
|
333
|
+
for ref_file in reference_dir.glob('*.md'):
|
|
334
|
+
line_count = count_file_lines(ref_file)
|
|
335
|
+
if line_count > 100:
|
|
336
|
+
if not has_table_of_contents(ref_file):
|
|
337
|
+
warnings_list.append(
|
|
338
|
+
f"Reference file '{ref_file.name}' has {line_count} lines but no table of contents\n"
|
|
339
|
+
f" Recommendation: Add a TOC at the top for better navigation\n"
|
|
340
|
+
f" Example:\n"
|
|
341
|
+
f" ## Table of Contents\n"
|
|
342
|
+
f" - [Section 1](#section-1)\n"
|
|
343
|
+
f" - [Section 2](#section-2)"
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
# Check for broken reference links in SKILL.md
|
|
347
|
+
# Extract all markdown links
|
|
348
|
+
link_pattern = r'\[([^\]]+)\]\(([^\)]+)\)'
|
|
349
|
+
links = re.findall(link_pattern, content)
|
|
350
|
+
|
|
351
|
+
for link_text, link_path in links:
|
|
352
|
+
# Skip external URLs
|
|
353
|
+
if link_path.startswith('http://') or link_path.startswith('https://'):
|
|
354
|
+
continue
|
|
355
|
+
|
|
356
|
+
# Check if referenced file exists
|
|
357
|
+
if link_path.startswith('reference/') or link_path.startswith('examples/') or link_path.startswith('templates/'):
|
|
358
|
+
full_path = skill_dir / link_path
|
|
359
|
+
if not full_path.exists():
|
|
360
|
+
errors.append(
|
|
361
|
+
f"Broken link in SKILL.md: '{link_text}' -> {link_path}\n"
|
|
362
|
+
f" File does not exist: {full_path}"
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
return errors, warnings_list
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def validate_skill_file(file_path):
|
|
369
|
+
"""Main validation function"""
|
|
370
|
+
print(f"\nValidating: {file_path}\n")
|
|
371
|
+
|
|
372
|
+
# Check file exists
|
|
373
|
+
if not os.path.exists(file_path):
|
|
374
|
+
error(f"File not found: {file_path}")
|
|
375
|
+
return False
|
|
376
|
+
|
|
377
|
+
# Check file is named SKILL.md
|
|
378
|
+
if not file_path.endswith('SKILL.md'):
|
|
379
|
+
warning("File should be named 'SKILL.md'")
|
|
380
|
+
|
|
381
|
+
# Read file content
|
|
382
|
+
try:
|
|
383
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
384
|
+
content = f.read()
|
|
385
|
+
except Exception as e:
|
|
386
|
+
error(f"Failed to read file: {e}")
|
|
387
|
+
return False
|
|
388
|
+
|
|
389
|
+
# Validate YAML frontmatter
|
|
390
|
+
print("Checking YAML frontmatter...")
|
|
391
|
+
frontmatter = validate_yaml_frontmatter(content)
|
|
392
|
+
|
|
393
|
+
if frontmatter is None:
|
|
394
|
+
return False
|
|
395
|
+
|
|
396
|
+
success("YAML frontmatter format is valid")
|
|
397
|
+
|
|
398
|
+
# Validate required fields
|
|
399
|
+
all_errors = []
|
|
400
|
+
all_warnings = []
|
|
401
|
+
|
|
402
|
+
# Check 'name' field
|
|
403
|
+
print("\nValidating 'name' field...")
|
|
404
|
+
name = frontmatter.get('name', '')
|
|
405
|
+
name_errors, name_warnings = validate_name_field(name)
|
|
406
|
+
|
|
407
|
+
if name_errors:
|
|
408
|
+
all_errors.extend(name_errors)
|
|
409
|
+
else:
|
|
410
|
+
success(f"'name' field is valid: {name}")
|
|
411
|
+
|
|
412
|
+
all_warnings.extend(name_warnings)
|
|
413
|
+
|
|
414
|
+
# Check 'description' field
|
|
415
|
+
print("\nValidating 'description' field...")
|
|
416
|
+
description = frontmatter.get('description', '')
|
|
417
|
+
desc_errors, desc_warnings = validate_description_field(description)
|
|
418
|
+
|
|
419
|
+
if desc_errors:
|
|
420
|
+
all_errors.extend(desc_errors)
|
|
421
|
+
else:
|
|
422
|
+
success(f"'description' field is valid ({len(description)} characters)")
|
|
423
|
+
|
|
424
|
+
all_warnings.extend(desc_warnings)
|
|
425
|
+
|
|
426
|
+
# Validate directory name matches
|
|
427
|
+
print("\nValidating directory name...")
|
|
428
|
+
dir_errors, dir_warnings = validate_directory_name(file_path, frontmatter)
|
|
429
|
+
|
|
430
|
+
if dir_errors:
|
|
431
|
+
all_errors.extend(dir_errors)
|
|
432
|
+
else:
|
|
433
|
+
success("Directory name matches skill name")
|
|
434
|
+
|
|
435
|
+
all_warnings.extend(dir_warnings)
|
|
436
|
+
|
|
437
|
+
# Validate skill size (500 line limit)
|
|
438
|
+
print("\nValidating skill size...")
|
|
439
|
+
size_errors, size_warnings = validate_skill_size(file_path, content)
|
|
440
|
+
|
|
441
|
+
if size_errors:
|
|
442
|
+
all_errors.extend(size_errors)
|
|
443
|
+
else:
|
|
444
|
+
_, body_lines = count_body_lines(content)
|
|
445
|
+
success(f"SKILL.md body size is valid ({body_lines}/500 lines)")
|
|
446
|
+
|
|
447
|
+
all_warnings.extend(size_warnings)
|
|
448
|
+
|
|
449
|
+
# Validate total bundle size (8MB limit)
|
|
450
|
+
print("\nValidating bundle size...")
|
|
451
|
+
bundle_errors, bundle_warnings = validate_bundle_size(file_path)
|
|
452
|
+
|
|
453
|
+
if bundle_errors:
|
|
454
|
+
all_errors.extend(bundle_errors)
|
|
455
|
+
else:
|
|
456
|
+
skill_path = Path(file_path)
|
|
457
|
+
total_size = get_directory_size(skill_path.parent)
|
|
458
|
+
success(f"Total bundle size is valid ({format_size(total_size)}/8MB)")
|
|
459
|
+
|
|
460
|
+
all_warnings.extend(bundle_warnings)
|
|
461
|
+
|
|
462
|
+
# Validate reference files
|
|
463
|
+
print("\nValidating reference files...")
|
|
464
|
+
ref_errors, ref_warnings = validate_reference_files(file_path, content)
|
|
465
|
+
|
|
466
|
+
if ref_errors:
|
|
467
|
+
all_errors.extend(ref_errors)
|
|
468
|
+
elif ref_warnings:
|
|
469
|
+
# Only show success if there are no errors or warnings
|
|
470
|
+
success("Reference file linking is valid")
|
|
471
|
+
else:
|
|
472
|
+
success("Reference file validation passed")
|
|
473
|
+
|
|
474
|
+
all_warnings.extend(ref_warnings)
|
|
475
|
+
|
|
476
|
+
# Print all errors
|
|
477
|
+
if all_errors:
|
|
478
|
+
print(f"\n{RED}Validation Failed{NC}\n")
|
|
479
|
+
for err in all_errors:
|
|
480
|
+
error(err)
|
|
481
|
+
print()
|
|
482
|
+
|
|
483
|
+
# Print warnings
|
|
484
|
+
if all_warnings:
|
|
485
|
+
print(f"\n{YELLOW}Warnings:{NC}\n")
|
|
486
|
+
for warn in all_warnings:
|
|
487
|
+
warning(warn)
|
|
488
|
+
print()
|
|
489
|
+
|
|
490
|
+
# Summary
|
|
491
|
+
if not all_errors and not all_warnings:
|
|
492
|
+
print(f"\n{GREEN}✓ All validation checks passed!{NC}\n")
|
|
493
|
+
return True
|
|
494
|
+
elif not all_errors:
|
|
495
|
+
print(f"\n{GREEN}✓ Validation passed with warnings{NC}\n")
|
|
496
|
+
return True
|
|
497
|
+
else:
|
|
498
|
+
return False
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def main():
|
|
502
|
+
"""Main entry point"""
|
|
503
|
+
if len(sys.argv) != 2:
|
|
504
|
+
print("Usage: validate-skill.py <path-to-SKILL.md>", file=sys.stderr)
|
|
505
|
+
print("\nExample:", file=sys.stderr)
|
|
506
|
+
print(" ./validate-skill.py .claude/skills/my-skill/SKILL.md", file=sys.stderr)
|
|
507
|
+
sys.exit(1)
|
|
508
|
+
|
|
509
|
+
skill_file = sys.argv[1]
|
|
510
|
+
|
|
511
|
+
success_result = validate_skill_file(skill_file)
|
|
512
|
+
|
|
513
|
+
if success_result:
|
|
514
|
+
sys.exit(0)
|
|
515
|
+
else:
|
|
516
|
+
sys.exit(1)
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
if __name__ == '__main__':
|
|
520
|
+
main()
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Basic Skill Template
|
|
2
|
+
|
|
3
|
+
Use this template for focused, single-purpose skills.
|
|
4
|
+
|
|
5
|
+
<!--
|
|
6
|
+
IMPORTANT SIZE REQUIREMENTS:
|
|
7
|
+
- Keep SKILL.md body under 500 lines (excluding YAML frontmatter)
|
|
8
|
+
- Total skill bundle must be under 8MB (all files combined)
|
|
9
|
+
- Reference files >100 lines need table of contents at top
|
|
10
|
+
- Link all references one level deep from SKILL.md
|
|
11
|
+
- Validation script enforces these limits
|
|
12
|
+
|
|
13
|
+
For detailed guidance, see reference/file-organization.md
|
|
14
|
+
-->
|
|
15
|
+
|
|
16
|
+
```yaml
|
|
17
|
+
---
|
|
18
|
+
name: your-skill-name
|
|
19
|
+
description: "What this skill does and when to use it with trigger keywords"
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
# Skill Title
|
|
23
|
+
|
|
24
|
+
Brief introduction explaining the skill's purpose and value.
|
|
25
|
+
|
|
26
|
+
## Capabilities
|
|
27
|
+
|
|
28
|
+
What this skill provides:
|
|
29
|
+
- **Category 1**: Specific capabilities
|
|
30
|
+
- **Category 2**: Specific capabilities
|
|
31
|
+
- **Category 3**: Specific capabilities
|
|
32
|
+
|
|
33
|
+
## How to Use
|
|
34
|
+
|
|
35
|
+
1. **Step 1**: Description
|
|
36
|
+
2. **Step 2**: Description
|
|
37
|
+
3. **Step 3**: Description
|
|
38
|
+
|
|
39
|
+
## Input Format
|
|
40
|
+
|
|
41
|
+
What data or context is needed:
|
|
42
|
+
- Format 1 (CSV, JSON, etc.)
|
|
43
|
+
- Format 2
|
|
44
|
+
- Format 3
|
|
45
|
+
|
|
46
|
+
## Output Format
|
|
47
|
+
|
|
48
|
+
What the skill produces:
|
|
49
|
+
- Result component 1
|
|
50
|
+
- Result component 2
|
|
51
|
+
- Result component 3
|
|
52
|
+
|
|
53
|
+
## Example Usage
|
|
54
|
+
|
|
55
|
+
Concrete examples of user queries:
|
|
56
|
+
|
|
57
|
+
"Example query 1"
|
|
58
|
+
|
|
59
|
+
"Example query 2"
|
|
60
|
+
|
|
61
|
+
"Example query 3"
|
|
62
|
+
|
|
63
|
+
## Scripts
|
|
64
|
+
|
|
65
|
+
Optional supporting scripts:
|
|
66
|
+
- `script1.py`: Description
|
|
67
|
+
- `script2.py`: Description
|
|
68
|
+
|
|
69
|
+
## Best Practices
|
|
70
|
+
|
|
71
|
+
1. Best practice 1
|
|
72
|
+
2. Best practice 2
|
|
73
|
+
3. Best practice 3
|
|
74
|
+
|
|
75
|
+
## Limitations
|
|
76
|
+
|
|
77
|
+
- Limitation 1
|
|
78
|
+
- Limitation 2
|
|
79
|
+
- Limitation 3
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## When to Use This Template
|
|
83
|
+
|
|
84
|
+
- **Single-purpose skills**: One clear domain or task
|
|
85
|
+
- **Straightforward workflows**: Simple input → process → output
|
|
86
|
+
- **Minimal domain knowledge**: Doesn't require extensive background
|
|
87
|
+
- **Quick reference**: Users need fast, focused guidance
|
|
88
|
+
|
|
89
|
+
## Example Skills Using This Pattern
|
|
90
|
+
|
|
91
|
+
- File format conversion
|
|
92
|
+
- Code formatting
|
|
93
|
+
- Data validation
|
|
94
|
+
- Template application
|