- /workflow command to list and describe available workflows - Filter by category (health, deploy, incidents, sysadmin) - Show workflow steps and triggers - /skill-info command for skill introspection - List scripts, triggers, and allowed tools - Show references and documentation - /agent-info command with hierarchy visualization - Tree view of agent relationships - Model assignments (opus/sonnet/haiku) with visual indicators - Supervisor and subordinate information - Updated shell completions with 19 aliases total - Test suite now covers 27 tests 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
226 lines
6.5 KiB
Python
Executable File
226 lines
6.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Show information about available skills.
|
|
Usage: python3 skill-info.py [--scripts] [name]
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Dict, List, Optional
|
|
|
|
CLAUDE_DIR = Path.home() / ".claude"
|
|
SKILLS_DIR = CLAUDE_DIR / "skills"
|
|
REGISTRY_PATH = CLAUDE_DIR / "state" / "component-registry.json"
|
|
|
|
|
|
def load_registry() -> Dict:
|
|
"""Load component registry."""
|
|
try:
|
|
with open(REGISTRY_PATH) as f:
|
|
return json.load(f)
|
|
except (FileNotFoundError, json.JSONDecodeError):
|
|
return {}
|
|
|
|
|
|
def find_skills() -> List[Path]:
|
|
"""Find all skill directories with SKILL.md."""
|
|
if not SKILLS_DIR.exists():
|
|
return []
|
|
|
|
return [d for d in SKILLS_DIR.iterdir()
|
|
if d.is_dir() and (d / "SKILL.md").exists()]
|
|
|
|
|
|
def parse_skill_md(path: Path) -> Dict:
|
|
"""Parse a SKILL.md file for metadata."""
|
|
try:
|
|
content = path.read_text()
|
|
|
|
result = {
|
|
"name": path.parent.name,
|
|
"path": str(path.relative_to(CLAUDE_DIR)),
|
|
"description": "",
|
|
"allowed_tools": [],
|
|
}
|
|
|
|
# Parse YAML frontmatter
|
|
if content.startswith("---"):
|
|
parts = content.split("---", 2)
|
|
if len(parts) >= 2:
|
|
frontmatter = parts[1]
|
|
for line in frontmatter.strip().split("\n"):
|
|
if ":" in line:
|
|
key, value = line.split(":", 1)
|
|
key = key.strip()
|
|
value = value.strip()
|
|
if key == "description":
|
|
result["description"] = value
|
|
elif key == "allowed-tools":
|
|
result["allowed_tools"] = [t.strip() for t in value.split(",")]
|
|
|
|
# Get first paragraph as description if not in frontmatter
|
|
if not result["description"]:
|
|
body = content.split("---")[-1] if "---" in content else content
|
|
lines = body.strip().split("\n\n")
|
|
for para in lines:
|
|
if para.strip() and not para.startswith("#"):
|
|
result["description"] = para.strip()[:200]
|
|
break
|
|
|
|
return result
|
|
|
|
except Exception as e:
|
|
return {"name": path.parent.name, "error": str(e)}
|
|
|
|
|
|
def get_skill_scripts(skill_dir: Path) -> List[str]:
|
|
"""Get list of scripts in a skill's scripts/ directory."""
|
|
scripts_dir = skill_dir / "scripts"
|
|
if not scripts_dir.exists():
|
|
return []
|
|
|
|
scripts = []
|
|
for f in scripts_dir.iterdir():
|
|
if f.is_file() and f.suffix in [".py", ".sh"]:
|
|
scripts.append(f.name)
|
|
return sorted(scripts)
|
|
|
|
|
|
def get_skill_references(skill_dir: Path) -> List[str]:
|
|
"""Get list of reference files in a skill's references/ directory."""
|
|
refs_dir = skill_dir / "references"
|
|
if not refs_dir.exists():
|
|
return []
|
|
|
|
refs = []
|
|
for f in refs_dir.iterdir():
|
|
if f.is_file():
|
|
refs.append(f.name)
|
|
return sorted(refs)
|
|
|
|
|
|
def list_skills(show_scripts: bool = False):
|
|
"""List all available skills."""
|
|
registry = load_registry()
|
|
reg_skills = registry.get("skills", {})
|
|
|
|
skills = find_skills()
|
|
|
|
if not skills:
|
|
print("No skills found.")
|
|
return
|
|
|
|
print(f"\n🎯 Available Skills ({len(skills)})\n")
|
|
|
|
for skill_dir in sorted(skills):
|
|
name = skill_dir.name
|
|
skill_info = parse_skill_md(skill_dir / "SKILL.md")
|
|
reg_info = reg_skills.get(name, {})
|
|
|
|
desc = skill_info.get("description", reg_info.get("description", "No description"))
|
|
if len(desc) > 80:
|
|
desc = desc[:77] + "..."
|
|
|
|
print(f" {name}")
|
|
print(f" {desc}")
|
|
|
|
if show_scripts:
|
|
scripts = get_skill_scripts(skill_dir)
|
|
if scripts:
|
|
print(f" Scripts: {', '.join(scripts)}")
|
|
|
|
triggers = reg_info.get("triggers", [])
|
|
if triggers:
|
|
trigger_str = ", ".join(triggers[:4])
|
|
if len(triggers) > 4:
|
|
trigger_str += f" (+{len(triggers)-4} more)"
|
|
print(f" Triggers: {trigger_str}")
|
|
|
|
print("")
|
|
|
|
|
|
def show_skill(name: str):
|
|
"""Show details for a specific skill."""
|
|
# Find matching skill
|
|
skills = find_skills()
|
|
matches = [s for s in skills if name.lower() in s.name.lower()]
|
|
|
|
if not matches:
|
|
print(f"Skill '{name}' not found.")
|
|
print("\nAvailable skills:")
|
|
for s in sorted(skills):
|
|
print(f" - {s.name}")
|
|
return
|
|
|
|
if len(matches) > 1 and not any(s.name == name for s in matches):
|
|
print(f"Multiple matches for '{name}':")
|
|
for s in matches:
|
|
print(f" - {s.name}")
|
|
return
|
|
|
|
skill_dir = next((s for s in matches if s.name == name), matches[0])
|
|
skill_info = parse_skill_md(skill_dir / "SKILL.md")
|
|
|
|
registry = load_registry()
|
|
reg_info = registry.get("skills", {}).get(skill_dir.name, {})
|
|
|
|
print(f"\n🎯 Skill: {skill_dir.name}\n")
|
|
print(f"Path: {skill_dir.relative_to(CLAUDE_DIR)}/")
|
|
print(f"Description: {skill_info.get('description', 'No description')}")
|
|
|
|
# Allowed tools
|
|
allowed = skill_info.get("allowed_tools", [])
|
|
if allowed:
|
|
print(f"\nAllowed Tools: {', '.join(allowed)}")
|
|
|
|
# Triggers
|
|
triggers = reg_info.get("triggers", [])
|
|
if triggers:
|
|
print(f"\nTriggers:")
|
|
for t in triggers:
|
|
print(f" - {t}")
|
|
|
|
# Scripts
|
|
scripts = get_skill_scripts(skill_dir)
|
|
if scripts:
|
|
print(f"\nScripts:")
|
|
for s in scripts:
|
|
script_path = skill_dir / "scripts" / s
|
|
executable = "✓" if script_path.stat().st_mode & 0o111 else "○"
|
|
print(f" {executable} {s}")
|
|
|
|
# References
|
|
refs = get_skill_references(skill_dir)
|
|
if refs:
|
|
print(f"\nReferences:")
|
|
for r in refs:
|
|
print(f" - {r}")
|
|
|
|
# Registry script
|
|
if "script" in reg_info:
|
|
print(f"\nRegistry Script: {reg_info['script']}")
|
|
|
|
print("")
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Show skill information")
|
|
parser.add_argument("name", nargs="?", help="Skill name to show details")
|
|
parser.add_argument("--scripts", "-s", action="store_true",
|
|
help="Show scripts in listing")
|
|
parser.add_argument("--list", "-l", action="store_true", help="List all skills")
|
|
|
|
args = parser.parse_args()
|
|
|
|
if args.name and not args.list:
|
|
show_skill(args.name)
|
|
else:
|
|
list_skills(args.scripts)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|