init
This commit is contained in:
263
.opencode/scripts/scan_skills.py
Executable file
263
.opencode/scripts/scan_skills.py
Executable file
@@ -0,0 +1,263 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Scan .opencode/skills directory and extract skill metadata.
|
||||
"""
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Dict, List
|
||||
try:
|
||||
import yaml
|
||||
except ModuleNotFoundError:
|
||||
raise SystemExit(
|
||||
"PyYAML is required. Install with: python3 -m pip install -r .opencode/scripts/requirements.txt"
|
||||
)
|
||||
|
||||
# Exact mappings for high-signal CK skills to avoid falling into "other".
|
||||
EXACT_CATEGORY_MAP = {
|
||||
# Utilities & Helpers
|
||||
"ask": "utilities",
|
||||
"bootstrap": "utilities",
|
||||
"brainstorm": "utilities",
|
||||
"ck-autoresearch": "utilities",
|
||||
"ck-debug": "utilities",
|
||||
"ck-loop": "utilities",
|
||||
"ck-predict": "utilities",
|
||||
"ck-scenario": "utilities",
|
||||
"code-review": "utilities",
|
||||
"coding-level": "utilities",
|
||||
"context-engineering": "utilities",
|
||||
"cook": "utilities",
|
||||
"copywriting": "utilities",
|
||||
"debug": "utilities",
|
||||
"docs": "utilities",
|
||||
"fix": "utilities",
|
||||
"journal": "utilities",
|
||||
"markdown-novel-viewer": "utilities",
|
||||
"mermaidjs-v11": "utilities",
|
||||
"plan": "utilities",
|
||||
"ck-plan": "utilities",
|
||||
"preview": "utilities",
|
||||
"problem-solving": "utilities",
|
||||
"project-management": "utilities",
|
||||
"project-organization": "utilities",
|
||||
"research": "utilities",
|
||||
"retro": "utilities",
|
||||
"sequential-thinking": "utilities",
|
||||
"test": "utilities",
|
||||
"watzup": "utilities",
|
||||
# Development Tools
|
||||
"find-skills": "dev-tools",
|
||||
"git": "dev-tools",
|
||||
"gkg": "dev-tools",
|
||||
"kanban": "dev-tools",
|
||||
"llms": "dev-tools",
|
||||
"mintlify": "dev-tools",
|
||||
"plans-kanban": "dev-tools",
|
||||
"scout": "dev-tools",
|
||||
"ship": "dev-tools",
|
||||
"team": "dev-tools",
|
||||
"use-mcp": "dev-tools",
|
||||
"worktree": "dev-tools",
|
||||
# Frontend & Design
|
||||
"react-best-practices": "frontend",
|
||||
"remotion": "frontend",
|
||||
"shader": "frontend",
|
||||
"stitch": "frontend",
|
||||
"web-design-guidelines": "frontend",
|
||||
# Frameworks & Platforms
|
||||
"tanstack": "frameworks",
|
||||
# Infrastructure & DevOps
|
||||
"deploy": "infrastructure",
|
||||
# Multimedia & Processing
|
||||
"agent-browser": "multimedia",
|
||||
"web-testing": "multimedia",
|
||||
# Security (mapped to utilities)
|
||||
"ck-security": "utilities",
|
||||
"security-scan": "utilities",
|
||||
}
|
||||
|
||||
def extract_frontmatter(content: str) -> Dict:
|
||||
"""Extract YAML frontmatter from markdown content."""
|
||||
match = re.match(r'^---\s*\n(.*?)\n---\s*\n', content, re.DOTALL)
|
||||
if match:
|
||||
try:
|
||||
return yaml.safe_load(match.group(1))
|
||||
except:
|
||||
return {}
|
||||
return {}
|
||||
|
||||
def extract_first_paragraph(content: str) -> str:
|
||||
"""Extract first meaningful paragraph after frontmatter."""
|
||||
# Remove frontmatter
|
||||
content = re.sub(r'^---\s*\n.*?\n---\s*\n', '', content, flags=re.DOTALL)
|
||||
|
||||
# Find first paragraph (after headings)
|
||||
lines = content.split('\n')
|
||||
paragraph = []
|
||||
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
# Skip headings and empty lines
|
||||
if line.startswith('#') or not line:
|
||||
if paragraph: # If we've started collecting, stop
|
||||
break
|
||||
continue
|
||||
|
||||
paragraph.append(line)
|
||||
|
||||
# Stop after first paragraph
|
||||
if line.endswith('.') and len(' '.join(paragraph)) > 50:
|
||||
break
|
||||
|
||||
return ' '.join(paragraph)[:200]
|
||||
|
||||
def scan_skills(base_path: Path) -> List[Dict]:
|
||||
"""Scan all skill files and extract metadata."""
|
||||
skills = []
|
||||
|
||||
for skill_file in sorted(base_path.rglob('SKILL.md')):
|
||||
# Get skill directory name
|
||||
skill_dir = skill_file.parent
|
||||
skill_name = skill_dir.name
|
||||
|
||||
# Skip template
|
||||
if skill_name == 'template-skill':
|
||||
continue
|
||||
|
||||
# Handle nested skills (like document-skills/*)
|
||||
if skill_dir.parent.name != 'skills':
|
||||
parent_name = skill_dir.parent.name
|
||||
skill_name = f"{parent_name}/{skill_name}"
|
||||
|
||||
try:
|
||||
content = skill_file.read_text()
|
||||
frontmatter = extract_frontmatter(content)
|
||||
|
||||
description = frontmatter.get('description', '')
|
||||
if not description:
|
||||
description = extract_first_paragraph(content)
|
||||
|
||||
# Categorize based on content/name
|
||||
category = categorize_skill(skill_name, description, content)
|
||||
|
||||
skill_entry = {
|
||||
'name': skill_name,
|
||||
'path': str(skill_file.relative_to(Path('.opencode/skills'))),
|
||||
'description': description,
|
||||
'category': category,
|
||||
'has_scripts': (skill_dir / 'scripts').exists(),
|
||||
'has_references': (skill_dir / 'references').exists()
|
||||
}
|
||||
|
||||
# Include argument-hint if present in frontmatter
|
||||
argument_hint = frontmatter.get('argument-hint', '')
|
||||
if argument_hint:
|
||||
skill_entry['argument_hint'] = str(argument_hint)
|
||||
|
||||
skills.append(skill_entry)
|
||||
except Exception as e:
|
||||
print(f"Error processing {skill_file}: {e}")
|
||||
|
||||
return skills
|
||||
|
||||
def categorize_skill(name: str, description: str, content: str) -> str:
|
||||
"""Categorize skill based on name and content."""
|
||||
lower_name = name.lower()
|
||||
if lower_name in EXACT_CATEGORY_MAP:
|
||||
return EXACT_CATEGORY_MAP[lower_name]
|
||||
|
||||
# AI/ML
|
||||
if any(x in lower_name for x in ['ai-', 'gemini', 'multimodal', 'adk']):
|
||||
return 'ai-ml'
|
||||
|
||||
# Frontend
|
||||
if any(x in lower_name for x in ['frontend', 'ui', 'design', 'aesthetic', 'threejs']):
|
||||
return 'frontend'
|
||||
|
||||
# Backend
|
||||
if any(x in lower_name for x in ['backend', 'auth', 'payment']):
|
||||
return 'backend'
|
||||
|
||||
# Infrastructure
|
||||
if any(x in lower_name for x in ['devops', 'docker', 'cloudflare', 'gcloud']):
|
||||
return 'infrastructure'
|
||||
|
||||
# Database
|
||||
if any(x in lower_name for x in ['database', 'mongodb', 'postgresql', 'sql']):
|
||||
return 'database'
|
||||
|
||||
# Development Tools
|
||||
if any(x in lower_name for x in ['mcp', 'skill-creator', 'repomix', 'docs-seeker']):
|
||||
return 'dev-tools'
|
||||
|
||||
# Multimedia
|
||||
if any(x in lower_name for x in ['media', 'chrome-devtools', 'document-skills']):
|
||||
return 'multimedia'
|
||||
|
||||
# Frameworks
|
||||
if any(x in lower_name for x in ['web-frameworks', 'mobile', 'shopify']):
|
||||
return 'frameworks'
|
||||
|
||||
# Utilities
|
||||
if any(x in lower_name for x in ['debug', 'problem', 'code-review', 'planning', 'research', 'sequential']):
|
||||
return 'utilities'
|
||||
|
||||
return 'other'
|
||||
|
||||
def group_by_category(skills: List[Dict]) -> Dict[str, List[Dict]]:
|
||||
"""Group skills by category."""
|
||||
categories = {}
|
||||
|
||||
for skill in skills:
|
||||
category = skill['category']
|
||||
if category not in categories:
|
||||
categories[category] = []
|
||||
categories[category].append(skill)
|
||||
|
||||
return categories
|
||||
|
||||
def main():
|
||||
"""Main execution."""
|
||||
base_path = Path('.opencode/skills')
|
||||
|
||||
if not base_path.exists():
|
||||
print(f"Error: {base_path} not found")
|
||||
return
|
||||
|
||||
print("Scanning skills...")
|
||||
skills = scan_skills(base_path)
|
||||
|
||||
print(f"\nFound {len(skills)} skills\n")
|
||||
|
||||
# Group by category
|
||||
categories = group_by_category(skills)
|
||||
|
||||
category_names = {
|
||||
'ai-ml': 'AI & Machine Learning',
|
||||
'frontend': 'Frontend & Design',
|
||||
'backend': 'Backend Development',
|
||||
'infrastructure': 'Infrastructure & DevOps',
|
||||
'database': 'Database & Storage',
|
||||
'dev-tools': 'Development Tools',
|
||||
'multimedia': 'Multimedia & Processing',
|
||||
'frameworks': 'Frameworks & Platforms',
|
||||
'utilities': 'Utilities & Helpers',
|
||||
'other': 'Other'
|
||||
}
|
||||
|
||||
for category, skills_list in sorted(categories.items()):
|
||||
print(f"\n{category_names.get(category, category.upper())}:")
|
||||
for skill in skills_list:
|
||||
scripts = '📦' if skill['has_scripts'] else ' '
|
||||
refs = '📚' if skill['has_references'] else ' '
|
||||
print(f" {scripts}{refs} {skill['name']:30} {skill['description'][:80]}")
|
||||
|
||||
# Output YAML to scripts directory
|
||||
output_path = Path('.opencode/scripts/skills_data.yaml')
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
output_path.write_text(yaml.dump(skills, allow_unicode=True, default_flow_style=False))
|
||||
print(f"\n✓ Saved metadata to {output_path}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user