#!/usr/bin/env python3 """ Extensions Analyzer Analyzes Claude Code commands, skills, and agents. """ import json import re import sys from pathlib import Path try: import yaml HAS_YAML = True except ImportError: HAS_YAML = False MAX_COMMAND_LINES = 100 MAX_SKILL_LINES = 500 class ExtensionsAnalyzer: def __init__(self): self.findings = { "critical": [], "warnings": [], "passing": [], "recommendations": [] } self.commands = {} self.skills = {} self.agents = {} def find_extension_dirs(self) -> dict: """Find extension directories.""" base_paths = [ Path.home() / ".claude", Path.cwd() / ".claude", ] dirs = {"commands": [], "skills": [], "agents": []} for base in base_paths: for ext_type in dirs.keys(): path = base / ext_type if path.exists() and path.is_dir(): dirs[ext_type].append(path) return dirs def parse_frontmatter(self, content: str) -> dict | None: """Parse YAML frontmatter.""" if not content.startswith('---'): return None try: end = content.find('---', 3) if end == -1: return None yaml_content = content[3:end].strip() if HAS_YAML: return yaml.safe_load(yaml_content) else: # Basic parsing without yaml result = {} for line in yaml_content.split('\n'): if ':' in line: key, value = line.split(':', 1) result[key.strip()] = value.strip() return result except Exception: return None def analyze_command(self, path: Path) -> dict: """Analyze a command file.""" try: content = path.read_text() except IOError: return {"name": path.stem, "error": "Could not read"} lines = len(content.split('\n')) frontmatter = self.parse_frontmatter(content) analysis = { "name": path.stem, "lines": lines, "has_frontmatter": frontmatter is not None, "has_description": frontmatter and "description" in frontmatter, "issues": [] } if not analysis["has_frontmatter"]: analysis["issues"].append("Missing YAML frontmatter") elif not analysis["has_description"]: analysis["issues"].append("Missing description") if lines > MAX_COMMAND_LINES: analysis["issues"].append(f"Too long: {lines} lines (max {MAX_COMMAND_LINES})") if not re.match(r'^[a-z][a-z0-9-]*$', analysis["name"]): analysis["issues"].append("Name should be kebab-case") return analysis def analyze_skill(self, path: Path) -> dict: """Analyze a skill directory.""" skill_md = path / "SKILL.md" if not skill_md.exists(): return { "name": path.name, "error": "Missing SKILL.md", "issues": ["Missing SKILL.md"] } try: content = skill_md.read_text() except IOError: return {"name": path.name, "error": "Could not read SKILL.md", "issues": []} lines = len(content.split('\n')) frontmatter = self.parse_frontmatter(content) analysis = { "name": path.name, "lines": lines, "has_frontmatter": frontmatter is not None, "has_description": frontmatter and "description" in frontmatter, "issues": [] } if not analysis["has_frontmatter"]: analysis["issues"].append("Missing frontmatter in SKILL.md") if lines > MAX_SKILL_LINES: analysis["issues"].append(f"Too long: {lines} lines (max {MAX_SKILL_LINES})") return analysis def analyze_agent(self, path: Path) -> dict: """Analyze an agent file.""" try: content = path.read_text() except IOError: return {"name": path.stem, "error": "Could not read", "issues": []} frontmatter = self.parse_frontmatter(content) analysis = { "name": path.stem, "has_frontmatter": frontmatter is not None, "tools_restricted": False, "issues": [] } if frontmatter: tools = frontmatter.get("tools", "*") analysis["tools_restricted"] = tools != "*" and tools if not analysis["has_frontmatter"]: analysis["issues"].append("Missing frontmatter") if not analysis["tools_restricted"]: analysis["issues"].append("Tools not restricted (consider limiting)") return analysis def analyze(self) -> dict: """Run full analysis.""" dirs = self.find_extension_dirs() # Analyze commands for cmd_dir in dirs["commands"]: for cmd_file in cmd_dir.glob("*.md"): analysis = self.analyze_command(cmd_file) self.commands[analysis["name"]] = analysis if analysis.get("issues"): for issue in analysis["issues"]: self.findings["warnings"].append(f"Command '{analysis['name']}': {issue}") else: self.findings["passing"].append(f"Command '{analysis['name']}': OK") # Analyze skills for skill_dir in dirs["skills"]: for skill_path in skill_dir.iterdir(): if skill_path.is_dir(): analysis = self.analyze_skill(skill_path) self.skills[analysis["name"]] = analysis if analysis.get("issues"): for issue in analysis["issues"]: if "Missing SKILL.md" in issue: self.findings["critical"].append(f"Skill '{analysis['name']}': {issue}") else: self.findings["warnings"].append(f"Skill '{analysis['name']}': {issue}") else: self.findings["passing"].append(f"Skill '{analysis['name']}': OK") # Analyze agents for agent_dir in dirs["agents"]: for agent_file in agent_dir.glob("*.md"): analysis = self.analyze_agent(agent_file) self.agents[analysis["name"]] = analysis if analysis.get("issues"): for issue in analysis["issues"]: self.findings["warnings"].append(f"Agent '{analysis['name']}': {issue}") else: self.findings["passing"].append(f"Agent '{analysis['name']}': OK") return { "commands_count": len(self.commands), "skills_count": len(self.skills), "agents_count": len(self.agents), "commands": self.commands, "skills": self.skills, "agents": self.agents, "findings": self.findings } def main(): analyzer = ExtensionsAnalyzer() report = analyzer.analyze() print(json.dumps(report, indent=2, default=str)) return 0 if __name__ == "__main__": sys.exit(main())