Based on claude-code-tools TmuxCLIController, this refactor: - Added DockerTmuxController class for robust tmux session management - Implements send_keys() with configurable delay_enter - Implements capture_pane() for output retrieval - Implements wait_for_prompt() for pattern-based completion detection - Implements wait_for_idle() for content-hash-based idle detection - Implements wait_for_shell_prompt() for shell prompt detection Also includes workflow improvements: - Pre-task git snapshot before agent execution - Post-task commit protocol in agent guidelines Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
345 lines
12 KiB
Python
345 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Tool Auto Loader - Dynamic tool discovery and documentation loading
|
|
|
|
Features:
|
|
1. Auto-discover available tools from config
|
|
2. Load tool documentation from docstrings/comments
|
|
3. Cache tool info for performance
|
|
4. Provide tool recommendations based on task
|
|
5. Track tool usage patterns
|
|
"""
|
|
|
|
import json
|
|
import re
|
|
from pathlib import Path
|
|
from typing import Dict, List, Set, Optional, Any
|
|
from datetime import datetime
|
|
import hashlib
|
|
|
|
class ToolAutoLoader:
|
|
"""Dynamically discovers and loads tool documentation"""
|
|
|
|
# Standard Claude Code tools
|
|
STANDARD_TOOLS = {
|
|
"Read": "Read file contents - use for examining code and documentation",
|
|
"Write": "Write or create files - use to create new files or overwrite",
|
|
"Edit": "Edit existing files with line-based replacements",
|
|
"Glob": "Find files matching patterns - use for file discovery",
|
|
"Grep": "Search file contents - use for code search",
|
|
"Bash": "Execute shell commands - use for system operations",
|
|
"Task": "Launch specialized agents - use for complex multi-step tasks",
|
|
"TodoWrite": "Manage task lists and progress tracking",
|
|
"AskUserQuestion": "Ask user for clarification or decisions",
|
|
"WebSearch": "Search the web for current information",
|
|
"WebFetch": "Fetch and analyze web content",
|
|
"Skill": "Execute pre-configured skills",
|
|
}
|
|
|
|
# MCP Server tools (shared ecosystem)
|
|
MCP_TOOLS = {
|
|
"zen": "Deep reasoning, code review, debugging via Gemini 3",
|
|
"dev-tools": "Browser debugging, screenshots, console access",
|
|
"task-queue": "Queue and track async tasks",
|
|
"sarlo-admin": "Server administration and configuration",
|
|
"shared-projects-memory": "Shared knowledge graph across projects",
|
|
"backup": "Create and manage backups",
|
|
}
|
|
|
|
def __init__(self, cache_dir: Optional[Path] = None):
|
|
"""Initialize tool loader
|
|
|
|
Args:
|
|
cache_dir: Optional directory for caching tool metadata
|
|
"""
|
|
self.cache_dir = cache_dir or Path("/tmp/.luzia-tool-cache")
|
|
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
|
self.tools_cache: Dict[str, Dict] = {}
|
|
self.usage_stats: Dict[str, int] = {}
|
|
self.load_cache()
|
|
|
|
def load_cache(self) -> None:
|
|
"""Load cached tool metadata from disk"""
|
|
cache_file = self.cache_dir / "tools_cache.json"
|
|
if cache_file.exists():
|
|
try:
|
|
data = json.loads(cache_file.read_text())
|
|
self.tools_cache = data.get("tools", {})
|
|
self.usage_stats = data.get("usage", {})
|
|
except Exception as e:
|
|
print(f"[Warning] Failed to load tool cache: {e}")
|
|
|
|
def save_cache(self) -> None:
|
|
"""Save tool metadata to cache"""
|
|
cache_file = self.cache_dir / "tools_cache.json"
|
|
cache_file.write_text(json.dumps({
|
|
"tools": self.tools_cache,
|
|
"usage": self.usage_stats,
|
|
"timestamp": datetime.now().isoformat()
|
|
}, indent=2))
|
|
|
|
def discover_tools(self, project_config: Dict[str, Any]) -> Dict[str, Dict]:
|
|
"""Discover available tools for a project
|
|
|
|
Args:
|
|
project_config: Project configuration dict
|
|
|
|
Returns:
|
|
Dict of available tools with metadata
|
|
"""
|
|
available = {}
|
|
tools_list = project_config.get("tools", [])
|
|
|
|
# Add standard tools
|
|
for tool in tools_list:
|
|
if tool in self.STANDARD_TOOLS:
|
|
available[tool] = {
|
|
"type": "standard",
|
|
"description": self.STANDARD_TOOLS[tool],
|
|
"category": self._categorize_tool(tool),
|
|
"usage_count": self.usage_stats.get(tool, 0)
|
|
}
|
|
|
|
# Add MCP tools if available
|
|
shared_tools = project_config.get("shared_tools", {})
|
|
for tool_name, description in shared_tools.items():
|
|
available[tool_name] = {
|
|
"type": "mcp",
|
|
"description": description,
|
|
"category": self._categorize_tool(tool_name),
|
|
"usage_count": self.usage_stats.get(tool_name, 0)
|
|
}
|
|
|
|
return available
|
|
|
|
def _categorize_tool(self, tool: str) -> str:
|
|
"""Categorize a tool by function"""
|
|
categories = {
|
|
# File operations
|
|
"Read": "file_read",
|
|
"Write": "file_write",
|
|
"Edit": "file_edit",
|
|
"Glob": "file_search",
|
|
"Grep": "file_search",
|
|
|
|
# System operations
|
|
"Bash": "system",
|
|
"Task": "delegation",
|
|
|
|
# Knowledge and reasoning
|
|
"zen": "reasoning",
|
|
"sarlo-admin": "administration",
|
|
"shared-projects-memory": "knowledge",
|
|
|
|
# User interaction
|
|
"AskUserQuestion": "interaction",
|
|
"WebSearch": "research",
|
|
"WebFetch": "research",
|
|
|
|
# Task management
|
|
"TodoWrite": "planning",
|
|
"task-queue": "async",
|
|
|
|
# Execution
|
|
"Skill": "special",
|
|
}
|
|
return categories.get(tool, "other")
|
|
|
|
def recommend_tools(self, task: str, available_tools: Dict[str, Dict]) -> List[str]:
|
|
"""Recommend tools for a task
|
|
|
|
Args:
|
|
task: Task description
|
|
available_tools: Dict of available tools
|
|
|
|
Returns:
|
|
Ordered list of recommended tool names
|
|
"""
|
|
recommendations = []
|
|
task_lower = task.lower()
|
|
|
|
# Keyword-based recommendations
|
|
patterns = {
|
|
# File-based tasks
|
|
r"(read|view|examine|check|look at|find|search|grep)": ["Read", "Glob", "Grep"],
|
|
r"(create|write|generate|output)": ["Write", "Edit"],
|
|
r"(modify|change|update|fix|edit)": ["Edit", "Read"],
|
|
r"(delete|remove|clean)": ["Edit", "Bash"],
|
|
|
|
# System tasks
|
|
r"(run|execute|start|stop|restart|service|systemd|docker)": ["Bash"],
|
|
r"(check|verify|status|health)": ["Bash", "Read"],
|
|
|
|
# Code/analysis tasks
|
|
r"(analyze|review|understand|debug|error)": ["Read", "Grep", "Bash"],
|
|
r"(test|build|compile|lint)": ["Bash"],
|
|
|
|
# Research/learning tasks
|
|
r"(research|search|find|learn|lookup|stack|reference)": ["WebSearch", "WebFetch"],
|
|
|
|
# Reasoning tasks
|
|
r"(think|reason|analyze|design|plan)": ["zen"],
|
|
|
|
# Project management
|
|
r"(track|todo|progress|schedule)": ["TodoWrite"],
|
|
|
|
# Knowledge/memory tasks
|
|
r"(remember|store|document|knowledge)": ["shared-projects-memory"],
|
|
}
|
|
|
|
scored_tools = {}
|
|
for pattern, tools in patterns.items():
|
|
if re.search(pattern, task_lower):
|
|
for tool in tools:
|
|
if tool in available_tools:
|
|
scored_tools[tool] = scored_tools.get(tool, 0) + 1
|
|
|
|
# Sort by score and frequency of use
|
|
recommendations = sorted(
|
|
scored_tools.keys(),
|
|
key=lambda t: (scored_tools[t], self.usage_stats.get(t, 0)),
|
|
reverse=True
|
|
)
|
|
|
|
# Add commonly used tools if not already included
|
|
common = ["Read", "Bash", "Edit"]
|
|
for tool in common:
|
|
if tool in available_tools and tool not in recommendations:
|
|
recommendations.append(tool)
|
|
|
|
return recommendations[:5] # Top 5 recommendations
|
|
|
|
def record_tool_usage(self, tool: str) -> None:
|
|
"""Record that a tool was used
|
|
|
|
Args:
|
|
tool: Tool name used
|
|
"""
|
|
self.usage_stats[tool] = self.usage_stats.get(tool, 0) + 1
|
|
|
|
def get_tool_documentation(self, tool: str) -> Dict[str, Any]:
|
|
"""Get detailed documentation for a tool
|
|
|
|
Args:
|
|
tool: Tool name
|
|
|
|
Returns:
|
|
Dict with tool documentation and usage info
|
|
"""
|
|
if tool in self.STANDARD_TOOLS:
|
|
return {
|
|
"name": tool,
|
|
"type": "standard",
|
|
"description": self.STANDARD_TOOLS[tool],
|
|
"usage_count": self.usage_stats.get(tool, 0),
|
|
"url": f"https://docs.anthropic.com/claude-code/{tool.lower()}"
|
|
}
|
|
|
|
if tool in self.MCP_TOOLS:
|
|
return {
|
|
"name": tool,
|
|
"type": "mcp",
|
|
"description": self.MCP_TOOLS[tool],
|
|
"usage_count": self.usage_stats.get(tool, 0),
|
|
}
|
|
|
|
return {
|
|
"name": tool,
|
|
"type": "unknown",
|
|
"description": "Unknown tool",
|
|
"usage_count": self.usage_stats.get(tool, 0)
|
|
}
|
|
|
|
def generate_tool_reference(self, available_tools: List[str]) -> str:
|
|
"""Generate a markdown reference for available tools
|
|
|
|
Args:
|
|
available_tools: List of available tool names
|
|
|
|
Returns:
|
|
Markdown reference text
|
|
"""
|
|
sections = ["# Available Tools Reference\n"]
|
|
|
|
# Group by category
|
|
by_category = {}
|
|
for tool in available_tools:
|
|
doc = self.get_tool_documentation(tool)
|
|
category = doc.get("type", "other")
|
|
if category not in by_category:
|
|
by_category[category] = []
|
|
by_category[category].append((tool, doc))
|
|
|
|
# Write sections
|
|
for category in ["standard", "mcp", "other"]:
|
|
if category in by_category:
|
|
sections.append(f"\n## {category.title()} Tools\n")
|
|
for tool, doc in by_category[category]:
|
|
sections.append(f"**{tool}**: {doc['description']}")
|
|
|
|
return "\n".join(sections)
|
|
|
|
def load_project_documentation(self, project_path: Path) -> Dict[str, str]:
|
|
"""Load documentation from project files
|
|
|
|
Args:
|
|
project_path: Path to project
|
|
|
|
Returns:
|
|
Dict of documentation by topic
|
|
"""
|
|
docs = {}
|
|
|
|
# Look for README, docs, etc
|
|
doc_files = [
|
|
project_path / "README.md",
|
|
project_path / "ARCHITECTURE.md",
|
|
project_path / ".claude.md",
|
|
project_path / "docs" / "index.md",
|
|
]
|
|
|
|
for doc_file in doc_files:
|
|
if doc_file.exists():
|
|
try:
|
|
content = doc_file.read_text()
|
|
# Extract first 500 chars as summary
|
|
docs[doc_file.stem] = content[:500]
|
|
except Exception as e:
|
|
pass
|
|
|
|
return docs
|
|
|
|
def get_tools_by_category(self, available_tools: Dict[str, Dict]) -> Dict[str, List[str]]:
|
|
"""Group tools by category
|
|
|
|
Args:
|
|
available_tools: Dict of available tools
|
|
|
|
Returns:
|
|
Dict with category -> list of tool names
|
|
"""
|
|
by_category = {}
|
|
for tool_name, tool_info in available_tools.items():
|
|
category = tool_info.get("category", "other")
|
|
if category not in by_category:
|
|
by_category[category] = []
|
|
by_category[category].append(tool_name)
|
|
|
|
return by_category
|
|
|
|
def get_top_tools(self, available_tools: Dict[str, Dict], limit: int = 5) -> List[str]:
|
|
"""Get most frequently used tools
|
|
|
|
Args:
|
|
available_tools: Dict of available tools
|
|
limit: Max tools to return
|
|
|
|
Returns:
|
|
List of most-used tool names
|
|
"""
|
|
return sorted(
|
|
available_tools.keys(),
|
|
key=lambda t: self.usage_stats.get(t, 0),
|
|
reverse=True
|
|
)[:limit]
|