Add /workflow, /skill-info, and /agent-info commands
- /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>
This commit is contained in:
225
automation/skill-info.py
Executable file
225
automation/skill-info.py
Executable file
@@ -0,0 +1,225 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user