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:
284
automation/agent-info.py
Executable file
284
automation/agent-info.py
Executable file
@@ -0,0 +1,284 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Show information about available agents.
|
||||||
|
Usage: python3 agent-info.py [--tree] [name]
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
|
CLAUDE_DIR = Path.home() / ".claude"
|
||||||
|
AGENTS_DIR = CLAUDE_DIR / "agents"
|
||||||
|
REGISTRY_PATH = CLAUDE_DIR / "state" / "component-registry.json"
|
||||||
|
|
||||||
|
|
||||||
|
# Agent hierarchy (from CLAUDE.md)
|
||||||
|
HIERARCHY = {
|
||||||
|
"personal-assistant": {
|
||||||
|
"supervisor": None,
|
||||||
|
"subordinates": ["master-orchestrator"]
|
||||||
|
},
|
||||||
|
"master-orchestrator": {
|
||||||
|
"supervisor": "personal-assistant",
|
||||||
|
"subordinates": ["linux-sysadmin", "k8s-orchestrator", "programmer-orchestrator"]
|
||||||
|
},
|
||||||
|
"linux-sysadmin": {
|
||||||
|
"supervisor": "master-orchestrator",
|
||||||
|
"subordinates": []
|
||||||
|
},
|
||||||
|
"k8s-orchestrator": {
|
||||||
|
"supervisor": "master-orchestrator",
|
||||||
|
"subordinates": ["k8s-diagnostician", "argocd-operator", "prometheus-analyst", "git-operator"]
|
||||||
|
},
|
||||||
|
"k8s-diagnostician": {
|
||||||
|
"supervisor": "k8s-orchestrator",
|
||||||
|
"subordinates": []
|
||||||
|
},
|
||||||
|
"argocd-operator": {
|
||||||
|
"supervisor": "k8s-orchestrator",
|
||||||
|
"subordinates": []
|
||||||
|
},
|
||||||
|
"prometheus-analyst": {
|
||||||
|
"supervisor": "k8s-orchestrator",
|
||||||
|
"subordinates": []
|
||||||
|
},
|
||||||
|
"git-operator": {
|
||||||
|
"supervisor": "k8s-orchestrator",
|
||||||
|
"subordinates": []
|
||||||
|
},
|
||||||
|
"programmer-orchestrator": {
|
||||||
|
"supervisor": "master-orchestrator",
|
||||||
|
"subordinates": ["code-planner", "code-implementer", "code-reviewer"]
|
||||||
|
},
|
||||||
|
"code-planner": {
|
||||||
|
"supervisor": "programmer-orchestrator",
|
||||||
|
"subordinates": []
|
||||||
|
},
|
||||||
|
"code-implementer": {
|
||||||
|
"supervisor": "programmer-orchestrator",
|
||||||
|
"subordinates": []
|
||||||
|
},
|
||||||
|
"code-reviewer": {
|
||||||
|
"supervisor": "programmer-orchestrator",
|
||||||
|
"subordinates": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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_agent_files() -> List[Path]:
|
||||||
|
"""Find all agent markdown files."""
|
||||||
|
if not AGENTS_DIR.exists():
|
||||||
|
return []
|
||||||
|
|
||||||
|
return [f for f in AGENTS_DIR.glob("*.md")
|
||||||
|
if f.name != "README.md"]
|
||||||
|
|
||||||
|
|
||||||
|
def parse_agent_md(path: Path) -> Dict:
|
||||||
|
"""Parse an agent markdown file for metadata."""
|
||||||
|
try:
|
||||||
|
content = path.read_text()
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"name": path.stem,
|
||||||
|
"path": str(path.relative_to(CLAUDE_DIR)),
|
||||||
|
"description": "",
|
||||||
|
"model": "unknown",
|
||||||
|
"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 == "name":
|
||||||
|
result["name"] = value
|
||||||
|
elif key == "description":
|
||||||
|
result["description"] = value
|
||||||
|
elif key == "model":
|
||||||
|
result["model"] = value
|
||||||
|
elif key == "tools":
|
||||||
|
result["tools"] = [t.strip() for t in value.split(",")]
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {"name": path.stem, "error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
def get_model_emoji(model: str) -> str:
|
||||||
|
"""Get emoji for model type."""
|
||||||
|
return {
|
||||||
|
"opus": "🔷",
|
||||||
|
"sonnet": "🔶",
|
||||||
|
"haiku": "🔸"
|
||||||
|
}.get(model.lower(), "○")
|
||||||
|
|
||||||
|
|
||||||
|
def list_agents():
|
||||||
|
"""List all available agents."""
|
||||||
|
registry = load_registry()
|
||||||
|
reg_agents = registry.get("agents", {})
|
||||||
|
|
||||||
|
print(f"\n🤖 Available Agents ({len(reg_agents)})\n")
|
||||||
|
|
||||||
|
# Group by model
|
||||||
|
by_model = {"opus": [], "sonnet": [], "haiku": [], "unknown": []}
|
||||||
|
|
||||||
|
for name, info in reg_agents.items():
|
||||||
|
model = info.get("model", "unknown")
|
||||||
|
by_model.get(model, by_model["unknown"]).append({
|
||||||
|
"name": name,
|
||||||
|
"description": info.get("description", "No description"),
|
||||||
|
"triggers": info.get("triggers", [])
|
||||||
|
})
|
||||||
|
|
||||||
|
for model in ["opus", "sonnet", "haiku"]:
|
||||||
|
agents = by_model[model]
|
||||||
|
if not agents:
|
||||||
|
continue
|
||||||
|
|
||||||
|
emoji = get_model_emoji(model)
|
||||||
|
print(f"=== {model.title()} {emoji} ===")
|
||||||
|
|
||||||
|
for agent in sorted(agents, key=lambda a: a["name"]):
|
||||||
|
print(f" {agent['name']}")
|
||||||
|
print(f" {agent['description']}")
|
||||||
|
if agent['triggers']:
|
||||||
|
print(f" Triggers: {', '.join(agent['triggers'][:3])}")
|
||||||
|
|
||||||
|
print("")
|
||||||
|
|
||||||
|
|
||||||
|
def show_tree():
|
||||||
|
"""Show agent hierarchy as a tree."""
|
||||||
|
print(f"\n🌳 Agent Hierarchy\n")
|
||||||
|
|
||||||
|
def print_tree(name: str, prefix: str = "", is_last: bool = True):
|
||||||
|
info = HIERARCHY.get(name, {})
|
||||||
|
registry = load_registry()
|
||||||
|
reg_info = registry.get("agents", {}).get(name, {})
|
||||||
|
model = reg_info.get("model", "?")
|
||||||
|
emoji = get_model_emoji(model)
|
||||||
|
|
||||||
|
connector = "└── " if is_last else "├── "
|
||||||
|
print(f"{prefix}{connector}{name} {emoji} ({model})")
|
||||||
|
|
||||||
|
new_prefix = prefix + (" " if is_last else "│ ")
|
||||||
|
subordinates = info.get("subordinates", [])
|
||||||
|
|
||||||
|
for i, sub in enumerate(subordinates):
|
||||||
|
print_tree(sub, new_prefix, i == len(subordinates) - 1)
|
||||||
|
|
||||||
|
# Start from root
|
||||||
|
print_tree("personal-assistant")
|
||||||
|
print("")
|
||||||
|
|
||||||
|
print("Legend: 🔷 opus 🔶 sonnet 🔸 haiku")
|
||||||
|
print("")
|
||||||
|
|
||||||
|
|
||||||
|
def show_agent(name: str):
|
||||||
|
"""Show details for a specific agent."""
|
||||||
|
registry = load_registry()
|
||||||
|
reg_agents = registry.get("agents", {})
|
||||||
|
|
||||||
|
# Find matching agent
|
||||||
|
matches = [n for n in reg_agents.keys() if name.lower() in n.lower()]
|
||||||
|
|
||||||
|
if not matches:
|
||||||
|
print(f"Agent '{name}' not found.")
|
||||||
|
print("\nAvailable agents:")
|
||||||
|
for n in sorted(reg_agents.keys()):
|
||||||
|
print(f" - {n}")
|
||||||
|
return
|
||||||
|
|
||||||
|
if len(matches) > 1 and name not in matches:
|
||||||
|
print(f"Multiple matches for '{name}':")
|
||||||
|
for m in matches:
|
||||||
|
print(f" - {m}")
|
||||||
|
return
|
||||||
|
|
||||||
|
agent_name = name if name in matches else matches[0]
|
||||||
|
reg_info = reg_agents[agent_name]
|
||||||
|
|
||||||
|
print(f"\n🤖 Agent: {agent_name}\n")
|
||||||
|
|
||||||
|
model = reg_info.get("model", "unknown")
|
||||||
|
print(f"Model: {model} {get_model_emoji(model)}")
|
||||||
|
print(f"Description: {reg_info.get('description', 'No description')}")
|
||||||
|
|
||||||
|
# Triggers
|
||||||
|
triggers = reg_info.get("triggers", [])
|
||||||
|
if triggers:
|
||||||
|
print(f"\nTriggers:")
|
||||||
|
for t in triggers:
|
||||||
|
print(f" - {t}")
|
||||||
|
|
||||||
|
# Hierarchy
|
||||||
|
hier = HIERARCHY.get(agent_name, {})
|
||||||
|
supervisor = hier.get("supervisor")
|
||||||
|
subordinates = hier.get("subordinates", [])
|
||||||
|
|
||||||
|
print(f"\nHierarchy:")
|
||||||
|
if supervisor:
|
||||||
|
print(f" Supervisor: {supervisor}")
|
||||||
|
else:
|
||||||
|
print(f" Supervisor: (top-level)")
|
||||||
|
|
||||||
|
if subordinates:
|
||||||
|
print(f" Subordinates:")
|
||||||
|
for sub in subordinates:
|
||||||
|
sub_info = reg_agents.get(sub, {})
|
||||||
|
sub_model = sub_info.get("model", "?")
|
||||||
|
print(f" - {sub} ({sub_model})")
|
||||||
|
|
||||||
|
# Check for agent file
|
||||||
|
agent_file = AGENTS_DIR / f"{agent_name}.md"
|
||||||
|
if agent_file.exists():
|
||||||
|
print(f"\nFile: agents/{agent_name}.md")
|
||||||
|
file_info = parse_agent_md(agent_file)
|
||||||
|
if file_info.get("tools"):
|
||||||
|
print(f"Tools: {', '.join(file_info['tools'])}")
|
||||||
|
|
||||||
|
print("")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="Show agent information")
|
||||||
|
parser.add_argument("name", nargs="?", help="Agent name to show details")
|
||||||
|
parser.add_argument("--tree", "-t", action="store_true",
|
||||||
|
help="Show agent hierarchy tree")
|
||||||
|
parser.add_argument("--list", "-l", action="store_true", help="List all agents")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.tree:
|
||||||
|
show_tree()
|
||||||
|
elif args.name and not args.list:
|
||||||
|
show_agent(args.name)
|
||||||
|
else:
|
||||||
|
list_agents()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -51,6 +51,24 @@ _claude_upgrade() {
|
|||||||
COMPREPLY=($(compgen -W "--check --backup --apply --help" -- "${cur}"))
|
COMPREPLY=($(compgen -W "--check --backup --apply --help" -- "${cur}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_claude_workflow() {
|
||||||
|
local cur="${COMP_WORDS[COMP_CWORD]}"
|
||||||
|
|
||||||
|
COMPREPLY=($(compgen -W "--category --list" -- "${cur}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
_claude_skill_info() {
|
||||||
|
local cur="${COMP_WORDS[COMP_CWORD]}"
|
||||||
|
|
||||||
|
COMPREPLY=($(compgen -W "--scripts --list" -- "${cur}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
_claude_agent_info() {
|
||||||
|
local cur="${COMP_WORDS[COMP_CWORD]}"
|
||||||
|
|
||||||
|
COMPREPLY=($(compgen -W "--tree --list" -- "${cur}"))
|
||||||
|
}
|
||||||
|
|
||||||
_claude_memory_add() {
|
_claude_memory_add() {
|
||||||
local cur="${COMP_WORDS[COMP_CWORD]}"
|
local cur="${COMP_WORDS[COMP_CWORD]}"
|
||||||
local prev="${COMP_WORDS[COMP_CWORD-1]}"
|
local prev="${COMP_WORDS[COMP_CWORD-1]}"
|
||||||
@@ -87,6 +105,9 @@ complete -F _claude_debug debug.sh
|
|||||||
complete -F _claude_export session-export.py
|
complete -F _claude_export session-export.py
|
||||||
complete -F _claude_mcp_status mcp-status.sh
|
complete -F _claude_mcp_status mcp-status.sh
|
||||||
complete -F _claude_upgrade upgrade.sh
|
complete -F _claude_upgrade upgrade.sh
|
||||||
|
complete -F _claude_workflow workflow-info.py
|
||||||
|
complete -F _claude_skill_info skill-info.py
|
||||||
|
complete -F _claude_agent_info agent-info.py
|
||||||
|
|
||||||
# Alias completions for convenience
|
# Alias completions for convenience
|
||||||
alias claude-validate='~/.claude/automation/validate-setup.sh'
|
alias claude-validate='~/.claude/automation/validate-setup.sh'
|
||||||
@@ -106,7 +127,11 @@ alias claude-debug='~/.claude/automation/debug.sh'
|
|||||||
alias claude-export='python3 ~/.claude/automation/session-export.py'
|
alias claude-export='python3 ~/.claude/automation/session-export.py'
|
||||||
alias claude-mcp='~/.claude/automation/mcp-status.sh'
|
alias claude-mcp='~/.claude/automation/mcp-status.sh'
|
||||||
alias claude-upgrade='~/.claude/automation/upgrade.sh'
|
alias claude-upgrade='~/.claude/automation/upgrade.sh'
|
||||||
|
alias claude-workflow='python3 ~/.claude/automation/workflow-info.py'
|
||||||
|
alias claude-skill='python3 ~/.claude/automation/skill-info.py'
|
||||||
|
alias claude-agent='python3 ~/.claude/automation/agent-info.py'
|
||||||
|
|
||||||
echo "Claude Code completions loaded. Available aliases:"
|
echo "Claude Code completions loaded. Available aliases:"
|
||||||
echo " claude-{validate,status,backup,restore,clean,memory-add,memory-list}"
|
echo " claude-{validate,status,backup,restore,clean,memory-add,memory-list,search}"
|
||||||
echo " claude-{search,history,install,test,maintenance,log,debug,export,mcp,upgrade}"
|
echo " claude-{history,install,test,maintenance,log,debug,export,mcp,upgrade}"
|
||||||
|
echo " claude-{workflow,skill,agent}"
|
||||||
|
|||||||
@@ -107,6 +107,30 @@ _claude_upgrade() {
|
|||||||
'--help[Show help]'
|
'--help[Show help]'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Workflow completion
|
||||||
|
_claude_workflow() {
|
||||||
|
_arguments \
|
||||||
|
'--category[Filter by category]:category:(health deploy incidents sysadmin)' \
|
||||||
|
'--list[List all workflows]' \
|
||||||
|
'*:name:'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Skill info completion
|
||||||
|
_claude_skill_info() {
|
||||||
|
_arguments \
|
||||||
|
'--scripts[Show scripts in listing]' \
|
||||||
|
'--list[List all skills]' \
|
||||||
|
'*:name:'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Agent info completion
|
||||||
|
_claude_agent_info() {
|
||||||
|
_arguments \
|
||||||
|
'--tree[Show hierarchy tree]' \
|
||||||
|
'--list[List all agents]' \
|
||||||
|
'*:name:'
|
||||||
|
}
|
||||||
|
|
||||||
# Register completions
|
# Register completions
|
||||||
compdef _memory_add memory-add.py
|
compdef _memory_add memory-add.py
|
||||||
compdef _memory_list memory-list.py
|
compdef _memory_list memory-list.py
|
||||||
@@ -118,6 +142,9 @@ compdef _claude_debug debug.sh
|
|||||||
compdef _claude_export session-export.py
|
compdef _claude_export session-export.py
|
||||||
compdef _claude_mcp_status mcp-status.sh
|
compdef _claude_mcp_status mcp-status.sh
|
||||||
compdef _claude_upgrade upgrade.sh
|
compdef _claude_upgrade upgrade.sh
|
||||||
|
compdef _claude_workflow workflow-info.py
|
||||||
|
compdef _claude_skill_info skill-info.py
|
||||||
|
compdef _claude_agent_info agent-info.py
|
||||||
|
|
||||||
# Aliases
|
# Aliases
|
||||||
alias claude-validate='~/.claude/automation/validate-setup.sh'
|
alias claude-validate='~/.claude/automation/validate-setup.sh'
|
||||||
@@ -137,7 +164,11 @@ alias claude-debug='~/.claude/automation/debug.sh'
|
|||||||
alias claude-export='python3 ~/.claude/automation/session-export.py'
|
alias claude-export='python3 ~/.claude/automation/session-export.py'
|
||||||
alias claude-mcp='~/.claude/automation/mcp-status.sh'
|
alias claude-mcp='~/.claude/automation/mcp-status.sh'
|
||||||
alias claude-upgrade='~/.claude/automation/upgrade.sh'
|
alias claude-upgrade='~/.claude/automation/upgrade.sh'
|
||||||
|
alias claude-workflow='python3 ~/.claude/automation/workflow-info.py'
|
||||||
|
alias claude-skill='python3 ~/.claude/automation/skill-info.py'
|
||||||
|
alias claude-agent='python3 ~/.claude/automation/agent-info.py'
|
||||||
|
|
||||||
echo "Claude Code completions loaded (zsh)"
|
echo "Claude Code completions loaded (zsh)"
|
||||||
echo " Aliases: claude-{validate,status,backup,restore,clean,memory-add,memory-list}"
|
echo " Aliases: claude-{validate,status,backup,restore,clean,memory-add,memory-list,search}"
|
||||||
echo " claude-{search,history,install,test,maintenance,log,debug,export,mcp,upgrade}"
|
echo " claude-{history,install,test,maintenance,log,debug,export,mcp,upgrade}"
|
||||||
|
echo " claude-{workflow,skill,agent}"
|
||||||
|
|||||||
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()
|
||||||
@@ -85,6 +85,27 @@ else
|
|||||||
fail "session-export.py syntax error"
|
fail "session-export.py syntax error"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Test 10: workflow-info.py
|
||||||
|
if python3 -m py_compile "${AUTOMATION_DIR}/workflow-info.py" 2>/dev/null; then
|
||||||
|
pass "workflow-info.py syntax valid"
|
||||||
|
else
|
||||||
|
fail "workflow-info.py syntax error"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 11: skill-info.py
|
||||||
|
if python3 -m py_compile "${AUTOMATION_DIR}/skill-info.py" 2>/dev/null; then
|
||||||
|
pass "skill-info.py syntax valid"
|
||||||
|
else
|
||||||
|
fail "skill-info.py syntax error"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 12: agent-info.py
|
||||||
|
if python3 -m py_compile "${AUTOMATION_DIR}/agent-info.py" 2>/dev/null; then
|
||||||
|
pass "agent-info.py syntax valid"
|
||||||
|
else
|
||||||
|
fail "agent-info.py syntax error"
|
||||||
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "=== Skill Scripts ==="
|
echo "=== Skill Scripts ==="
|
||||||
|
|
||||||
|
|||||||
182
automation/workflow-info.py
Executable file
182
automation/workflow-info.py
Executable file
@@ -0,0 +1,182 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
List and describe available workflows.
|
||||||
|
Usage: python3 workflow-info.py [--category CAT] [name]
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, List, Optional
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
CLAUDE_DIR = Path.home() / ".claude"
|
||||||
|
WORKFLOWS_DIR = CLAUDE_DIR / "workflows"
|
||||||
|
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_workflow_files() -> List[Path]:
|
||||||
|
"""Find all workflow YAML files."""
|
||||||
|
if not WORKFLOWS_DIR.exists():
|
||||||
|
return []
|
||||||
|
|
||||||
|
files = []
|
||||||
|
for pattern in ["*.yaml", "*.yml", "**/*.yaml", "**/*.yml"]:
|
||||||
|
files.extend(WORKFLOWS_DIR.glob(pattern))
|
||||||
|
|
||||||
|
# Filter out README and other non-workflow files
|
||||||
|
return [f for f in files if f.name not in ["README.md"]]
|
||||||
|
|
||||||
|
|
||||||
|
def parse_workflow(path: Path) -> Optional[Dict]:
|
||||||
|
"""Parse a workflow YAML file."""
|
||||||
|
try:
|
||||||
|
with open(path) as f:
|
||||||
|
content = f.read()
|
||||||
|
# Handle YAML front matter or full YAML
|
||||||
|
if content.startswith("---"):
|
||||||
|
parts = content.split("---", 2)
|
||||||
|
if len(parts) >= 2:
|
||||||
|
return yaml.safe_load(parts[1])
|
||||||
|
return yaml.safe_load(content)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_workflow_category(path: Path) -> str:
|
||||||
|
"""Get workflow category from path."""
|
||||||
|
rel_path = path.relative_to(WORKFLOWS_DIR)
|
||||||
|
if len(rel_path.parts) > 1:
|
||||||
|
return rel_path.parts[0]
|
||||||
|
return "general"
|
||||||
|
|
||||||
|
|
||||||
|
def list_workflows(category: Optional[str] = None):
|
||||||
|
"""List all available workflows."""
|
||||||
|
registry = load_registry()
|
||||||
|
workflows = registry.get("workflows", {})
|
||||||
|
|
||||||
|
# Group by category
|
||||||
|
categories: Dict[str, List] = {}
|
||||||
|
|
||||||
|
for name, info in workflows.items():
|
||||||
|
cat = name.split("/")[0] if "/" in name else "general"
|
||||||
|
if category and cat != category:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if cat not in categories:
|
||||||
|
categories[cat] = []
|
||||||
|
|
||||||
|
categories[cat].append({
|
||||||
|
"name": name,
|
||||||
|
"description": info.get("description", "No description"),
|
||||||
|
"triggers": info.get("triggers", [])
|
||||||
|
})
|
||||||
|
|
||||||
|
if not categories:
|
||||||
|
print("No workflows found.")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"\n📋 Available Workflows\n")
|
||||||
|
|
||||||
|
for cat in sorted(categories.keys()):
|
||||||
|
print(f"=== {cat.title()} ===")
|
||||||
|
for wf in categories[cat]:
|
||||||
|
print(f" {wf['name']}")
|
||||||
|
print(f" {wf['description']}")
|
||||||
|
if wf['triggers']:
|
||||||
|
print(f" Triggers: {', '.join(wf['triggers'][:3])}")
|
||||||
|
print("")
|
||||||
|
|
||||||
|
|
||||||
|
def show_workflow(name: str):
|
||||||
|
"""Show details for a specific workflow."""
|
||||||
|
registry = load_registry()
|
||||||
|
workflows = registry.get("workflows", {})
|
||||||
|
|
||||||
|
# Find matching workflow
|
||||||
|
matches = [n for n in workflows.keys() if name in n]
|
||||||
|
|
||||||
|
if not matches:
|
||||||
|
print(f"Workflow '{name}' not found.")
|
||||||
|
print("\nAvailable workflows:")
|
||||||
|
for n in sorted(workflows.keys()):
|
||||||
|
print(f" - {n}")
|
||||||
|
return
|
||||||
|
|
||||||
|
if len(matches) > 1 and name not in matches:
|
||||||
|
print(f"Multiple matches for '{name}':")
|
||||||
|
for m in matches:
|
||||||
|
print(f" - {m}")
|
||||||
|
return
|
||||||
|
|
||||||
|
wf_name = name if name in matches else matches[0]
|
||||||
|
wf_info = workflows[wf_name]
|
||||||
|
|
||||||
|
print(f"\n📋 Workflow: {wf_name}\n")
|
||||||
|
print(f"Description: {wf_info.get('description', 'No description')}")
|
||||||
|
|
||||||
|
triggers = wf_info.get("triggers", [])
|
||||||
|
if triggers:
|
||||||
|
print(f"\nTriggers:")
|
||||||
|
for t in triggers:
|
||||||
|
print(f" - {t}")
|
||||||
|
|
||||||
|
# Try to find and show the actual workflow file
|
||||||
|
possible_paths = [
|
||||||
|
WORKFLOWS_DIR / f"{wf_name}.yaml",
|
||||||
|
WORKFLOWS_DIR / f"{wf_name}.yml",
|
||||||
|
WORKFLOWS_DIR / wf_name / "workflow.yaml",
|
||||||
|
]
|
||||||
|
|
||||||
|
for path in possible_paths:
|
||||||
|
if path.exists():
|
||||||
|
wf_data = parse_workflow(path)
|
||||||
|
if wf_data:
|
||||||
|
print(f"\nFile: {path.relative_to(CLAUDE_DIR)}")
|
||||||
|
|
||||||
|
if "steps" in wf_data:
|
||||||
|
print(f"\nSteps:")
|
||||||
|
for i, step in enumerate(wf_data["steps"], 1):
|
||||||
|
step_name = step.get("name", f"Step {i}")
|
||||||
|
agent = step.get("agent", "unknown")
|
||||||
|
print(f" {i}. {step_name} (agent: {agent})")
|
||||||
|
|
||||||
|
if "trigger" in wf_data:
|
||||||
|
trigger = wf_data["trigger"]
|
||||||
|
if isinstance(trigger, dict):
|
||||||
|
if trigger.get("schedule"):
|
||||||
|
print(f"\nSchedule: {trigger['schedule']}")
|
||||||
|
if trigger.get("manual"):
|
||||||
|
print("Manual trigger: Yes")
|
||||||
|
break
|
||||||
|
|
||||||
|
print("")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="List and describe workflows")
|
||||||
|
parser.add_argument("name", nargs="?", help="Workflow name to show details")
|
||||||
|
parser.add_argument("--category", "-c", type=str, help="Filter by category")
|
||||||
|
parser.add_argument("--list", "-l", action="store_true", help="List all workflows")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.name and not args.list:
|
||||||
|
show_workflow(args.name)
|
||||||
|
else:
|
||||||
|
list_workflows(args.category)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -19,6 +19,9 @@ Slash commands for quick actions. User-invoked (type `/command` to trigger).
|
|||||||
| `/debug` | `/diag`, `/diagnose` | Debug and troubleshoot config |
|
| `/debug` | `/diag`, `/diagnose` | Debug and troubleshoot config |
|
||||||
| `/export` | `/session-export`, `/share` | Export session for sharing |
|
| `/export` | `/session-export`, `/share` | Export session for sharing |
|
||||||
| `/mcp-status` | `/mcp`, `/integrations` | Check MCP integrations |
|
| `/mcp-status` | `/mcp`, `/integrations` | Check MCP integrations |
|
||||||
|
| `/workflow` | `/workflows`, `/wf` | List and describe workflows |
|
||||||
|
| `/skill-info` | `/skill`, `/skills-info` | Show skill information |
|
||||||
|
| `/agent-info` | `/agent`, `/agents` | Show agent information |
|
||||||
| `/maintain` | `/maintenance`, `/admin` | Configuration maintenance |
|
| `/maintain` | `/maintenance`, `/admin` | Configuration maintenance |
|
||||||
| `/programmer` | | Code development tasks |
|
| `/programmer` | | Code development tasks |
|
||||||
| `/gcal` | `/calendar`, `/cal` | Google Calendar access |
|
| `/gcal` | `/calendar`, `/cal` | Google Calendar access |
|
||||||
|
|||||||
36
commands/agent-info.md
Normal file
36
commands/agent-info.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
name: agent-info
|
||||||
|
description: Show information about available agents
|
||||||
|
aliases: [agent, agents]
|
||||||
|
invokes: command:agent-info
|
||||||
|
---
|
||||||
|
|
||||||
|
# Agent Info Command
|
||||||
|
|
||||||
|
Show information about available agents and their hierarchy.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
/agent-info # List all agents
|
||||||
|
/agent-info <name> # Show agent details
|
||||||
|
/agent-info --tree # Show agent hierarchy
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
Run the agent info script:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 ~/.claude/automation/agent-info.py [options] [name]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Output Includes
|
||||||
|
|
||||||
|
| Field | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| Name | Agent identifier |
|
||||||
|
| Description | What the agent handles |
|
||||||
|
| Model | Assigned model (opus/sonnet/haiku) |
|
||||||
|
| Triggers | Keywords that route to this agent |
|
||||||
|
| Supervisor | Parent agent in hierarchy |
|
||||||
36
commands/skill-info.md
Normal file
36
commands/skill-info.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
name: skill-info
|
||||||
|
description: Show information about available skills
|
||||||
|
aliases: [skill, skills-info]
|
||||||
|
invokes: command:skill-info
|
||||||
|
---
|
||||||
|
|
||||||
|
# Skill Info Command
|
||||||
|
|
||||||
|
Show detailed information about available skills.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
/skill-info # List all skills
|
||||||
|
/skill-info <name> # Show skill details
|
||||||
|
/skill-info --scripts # List skills with scripts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
Run the skill info script:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 ~/.claude/automation/skill-info.py [options] [name]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Output Includes
|
||||||
|
|
||||||
|
| Field | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| Description | What the skill does |
|
||||||
|
| Scripts | Available executable scripts |
|
||||||
|
| Triggers | Keywords that invoke the skill |
|
||||||
|
| References | Documentation files |
|
||||||
|
| Allowed Tools | Tool restrictions (if any) |
|
||||||
40
commands/workflow.md
Normal file
40
commands/workflow.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
name: workflow
|
||||||
|
description: List and describe available workflows
|
||||||
|
aliases: [workflows, wf]
|
||||||
|
invokes: command:workflow
|
||||||
|
---
|
||||||
|
|
||||||
|
# Workflow Command
|
||||||
|
|
||||||
|
List and describe available workflows.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
/workflow # List all workflows
|
||||||
|
/workflow <name> # Show workflow details
|
||||||
|
/workflow --category <cat> # Filter by category
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
Run the workflow info script:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 ~/.claude/automation/workflow-info.py [options] [name]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Categories
|
||||||
|
|
||||||
|
| Category | Examples |
|
||||||
|
|----------|----------|
|
||||||
|
| `health` | cluster-health-check, cluster-daily-summary |
|
||||||
|
| `deploy` | deploy-app |
|
||||||
|
| `incidents` | pod-crashloop, node-issue-response |
|
||||||
|
| `sysadmin` | health-check, system-update |
|
||||||
|
|
||||||
|
## Note
|
||||||
|
|
||||||
|
Workflows are design documents - they guide Claude's actions but aren't
|
||||||
|
auto-executed. Use this command to understand available procedures.
|
||||||
@@ -184,6 +184,21 @@
|
|||||||
"description": "Check MCP integration status",
|
"description": "Check MCP integration status",
|
||||||
"aliases": ["/mcp", "/integrations"],
|
"aliases": ["/mcp", "/integrations"],
|
||||||
"invokes": "command:mcp-status"
|
"invokes": "command:mcp-status"
|
||||||
|
},
|
||||||
|
"/workflow": {
|
||||||
|
"description": "List and describe workflows",
|
||||||
|
"aliases": ["/workflows", "/wf"],
|
||||||
|
"invokes": "command:workflow"
|
||||||
|
},
|
||||||
|
"/skill-info": {
|
||||||
|
"description": "Show skill information",
|
||||||
|
"aliases": ["/skill", "/skills-info"],
|
||||||
|
"invokes": "command:skill-info"
|
||||||
|
},
|
||||||
|
"/agent-info": {
|
||||||
|
"description": "Show agent information",
|
||||||
|
"aliases": ["/agent", "/agents"],
|
||||||
|
"invokes": "command:agent-info"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"agents": {
|
"agents": {
|
||||||
@@ -317,7 +332,11 @@
|
|||||||
"debug": "~/.claude/automation/debug.sh",
|
"debug": "~/.claude/automation/debug.sh",
|
||||||
"daily-maintenance": "~/.claude/automation/daily-maintenance.sh",
|
"daily-maintenance": "~/.claude/automation/daily-maintenance.sh",
|
||||||
"session-export": "~/.claude/automation/session-export.py",
|
"session-export": "~/.claude/automation/session-export.py",
|
||||||
"mcp-status": "~/.claude/automation/mcp-status.sh"
|
"mcp-status": "~/.claude/automation/mcp-status.sh",
|
||||||
|
"upgrade": "~/.claude/automation/upgrade.sh",
|
||||||
|
"workflow-info": "~/.claude/automation/workflow-info.py",
|
||||||
|
"skill-info": "~/.claude/automation/skill-info.py",
|
||||||
|
"agent-info": "~/.claude/automation/agent-info.py"
|
||||||
},
|
},
|
||||||
"completions": {
|
"completions": {
|
||||||
"bash": "~/.claude/automation/completions.bash",
|
"bash": "~/.claude/automation/completions.bash",
|
||||||
|
|||||||
Reference in New Issue
Block a user