#!/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()