@grimoire-cc/cli 0.6.2 → 0.7.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.
Files changed (35) hide show
  1. package/dist/bin.js +4 -1
  2. package/dist/bin.js.map +1 -1
  3. package/dist/commands/logs.d.ts.map +1 -1
  4. package/dist/commands/logs.js +2 -2
  5. package/dist/commands/logs.js.map +1 -1
  6. package/dist/static/log-viewer.html +946 -690
  7. package/dist/static/static/log-viewer.html +946 -690
  8. package/package.json +1 -1
  9. package/packs/dev-pack/agents/gr.code-reviewer.md +286 -0
  10. package/packs/dev-pack/agents/gr.tdd-specialist.md +44 -0
  11. package/packs/dev-pack/grimoire.json +55 -0
  12. package/packs/dev-pack/skills/gr.tdd-specialist/SKILL.md +247 -0
  13. package/packs/dev-pack/skills/gr.tdd-specialist/reference/anti-patterns.md +166 -0
  14. package/packs/dev-pack/skills/gr.tdd-specialist/reference/language-frameworks.md +388 -0
  15. package/packs/dev-pack/skills/gr.tdd-specialist/reference/tdd-workflow-patterns.md +135 -0
  16. package/packs/docs-pack/grimoire.json +30 -0
  17. package/packs/docs-pack/skills/gr.business-logic-docs/SKILL.md +141 -0
  18. package/packs/docs-pack/skills/gr.business-logic-docs/references/tier2-template.md +74 -0
  19. package/packs/essentials-pack/agents/gr.fact-checker.md +202 -0
  20. package/packs/essentials-pack/grimoire.json +12 -0
  21. package/packs/meta-pack/grimoire.json +72 -0
  22. package/packs/meta-pack/skills/gr.context-file-guide/SKILL.md +201 -0
  23. package/packs/meta-pack/skills/gr.context-file-guide/scripts/validate-context-file.sh +29 -0
  24. package/packs/meta-pack/skills/gr.readme-guide/SKILL.md +362 -0
  25. package/packs/meta-pack/skills/gr.skill-developer/SKILL.md +321 -0
  26. package/packs/meta-pack/skills/gr.skill-developer/examples/brand-guidelines.md +94 -0
  27. package/packs/meta-pack/skills/gr.skill-developer/examples/financial-analysis.md +85 -0
  28. package/packs/meta-pack/skills/gr.skill-developer/reference/best-practices.md +410 -0
  29. package/packs/meta-pack/skills/gr.skill-developer/reference/file-organization.md +452 -0
  30. package/packs/meta-pack/skills/gr.skill-developer/reference/patterns.md +459 -0
  31. package/packs/meta-pack/skills/gr.skill-developer/reference/yaml-spec.md +214 -0
  32. package/packs/meta-pack/skills/gr.skill-developer/scripts/create-skill.sh +210 -0
  33. package/packs/meta-pack/skills/gr.skill-developer/scripts/validate-skill.py +520 -0
  34. package/packs/meta-pack/skills/gr.skill-developer/templates/basic-skill.md +94 -0
  35. 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