Implement hybrid agent distribution system
Complete implementation of the agent distribution framework including: CORE INFRASTRUCTURE: - AgentRegistry: Agent discovery, categorization, and dependency management - AgentInstaller: Agent installation, updates, and removal with safety measures - ProjectInitializer: Template-based project initialization with agent integration - CLI Tool: Comprehensive kaizen-agentic command-line interface DISTRIBUTION FEATURES: - Python package distribution with console script entry point - Agent categorization (project-management, development-process, code-quality, etc.) - Project templates (python-basic, python-web, python-cli, python-data, comprehensive) - Dependency resolution and validation - Idempotent operations with backup and rollback support CLI COMMANDS: - kaizen-agentic init: Initialize new projects with agents - kaizen-agentic install/update/remove: Manage agents in existing projects - kaizen-agentic list/status/validate: Discovery and maintenance - kaizen-agentic templates: Project template management INTEGRATION & DOCUMENTATION: - Makefile targets for agent management (list-agents, update-agents, etc.) - Automatic Claude Code configuration updates (CLAUDE.md) - Comprehensive documentation (GETTING_STARTED, AGENT_DISTRIBUTION, CLI_CHEAT_SHEET) - Multi-language build system integration examples - Complete test coverage for all components PACKAGE STRUCTURE: - Console script: kaizen-agentic command available globally - Package data: All agents included for distribution - Dependencies: click, pyyaml for CLI and parsing - Testing: Comprehensive test suite for registry and installer This enables sharing specialized AI agents across projects with easy installation, updates, and management through both CLI and integrated Makefile targets. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
499
src/kaizen_agentic/installer.py
Normal file
499
src/kaizen_agentic/installer.py
Normal file
@@ -0,0 +1,499 @@
|
||||
"""Agent installation and management utilities."""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Optional, Set
|
||||
from dataclasses import dataclass
|
||||
|
||||
from .registry import AgentRegistry, AgentDefinition
|
||||
|
||||
|
||||
@dataclass
|
||||
class InstallationConfig:
|
||||
"""Configuration for agent installation."""
|
||||
target_dir: Path
|
||||
claude_config_path: Optional[Path] = None
|
||||
makefile_path: Optional[Path] = None
|
||||
update_docs: bool = True
|
||||
create_backup: bool = True
|
||||
|
||||
|
||||
class AgentInstaller:
|
||||
"""Handles installation and management of agents in projects."""
|
||||
|
||||
def __init__(self, registry: AgentRegistry):
|
||||
self.registry = registry
|
||||
|
||||
def install_agents(
|
||||
self,
|
||||
agent_names: List[str],
|
||||
config: InstallationConfig
|
||||
) -> Dict[str, str]:
|
||||
"""Install agents into a project.
|
||||
|
||||
Returns:
|
||||
Dict mapping agent names to installation status
|
||||
"""
|
||||
results = {}
|
||||
|
||||
# Resolve dependencies
|
||||
resolved_agents = self.registry.resolve_dependencies(agent_names)
|
||||
|
||||
# Create target directory if it doesn't exist
|
||||
agents_dir = config.target_dir / "agents"
|
||||
agents_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Create backup if requested
|
||||
if config.create_backup and agents_dir.exists():
|
||||
self._create_backup(agents_dir)
|
||||
|
||||
# Install each agent
|
||||
for agent_name in resolved_agents:
|
||||
try:
|
||||
agent = self.registry.get_agent(agent_name)
|
||||
if not agent:
|
||||
results[agent_name] = f"ERROR: Agent not found"
|
||||
continue
|
||||
|
||||
target_path = agents_dir / f"agent-{agent_name}.md"
|
||||
shutil.copy2(agent.file_path, target_path)
|
||||
results[agent_name] = "INSTALLED"
|
||||
|
||||
except Exception as e:
|
||||
results[agent_name] = f"ERROR: {str(e)}"
|
||||
|
||||
# Update configuration files
|
||||
if config.claude_config_path:
|
||||
self._update_claude_config(resolved_agents, config.claude_config_path)
|
||||
|
||||
if config.makefile_path and config.makefile_path.exists():
|
||||
self._update_makefile(resolved_agents, config.makefile_path)
|
||||
|
||||
if config.update_docs:
|
||||
self._update_documentation(resolved_agents, config.target_dir)
|
||||
|
||||
return results
|
||||
|
||||
def list_installed_agents(self, project_dir: Path) -> List[str]:
|
||||
"""List agents currently installed in a project."""
|
||||
agents_dir = project_dir / "agents"
|
||||
if not agents_dir.exists():
|
||||
return []
|
||||
|
||||
installed = []
|
||||
for agent_file in agents_dir.glob("agent-*.md"):
|
||||
agent_name = agent_file.stem.replace("agent-", "")
|
||||
installed.append(agent_name)
|
||||
|
||||
return sorted(installed)
|
||||
|
||||
def update_agents(
|
||||
self,
|
||||
project_dir: Path,
|
||||
agent_names: Optional[List[str]] = None
|
||||
) -> Dict[str, str]:
|
||||
"""Update installed agents to latest versions."""
|
||||
if agent_names is None:
|
||||
agent_names = self.list_installed_agents(project_dir)
|
||||
|
||||
config = InstallationConfig(target_dir=project_dir)
|
||||
return self.install_agents(agent_names, config)
|
||||
|
||||
def remove_agents(
|
||||
self,
|
||||
agent_names: List[str],
|
||||
project_dir: Path
|
||||
) -> Dict[str, str]:
|
||||
"""Remove agents from a project."""
|
||||
results = {}
|
||||
agents_dir = project_dir / "agents"
|
||||
|
||||
for agent_name in agent_names:
|
||||
try:
|
||||
agent_file = agents_dir / f"agent-{agent_name}.md"
|
||||
if agent_file.exists():
|
||||
agent_file.unlink()
|
||||
results[agent_name] = "REMOVED"
|
||||
else:
|
||||
results[agent_name] = "NOT_FOUND"
|
||||
except Exception as e:
|
||||
results[agent_name] = f"ERROR: {str(e)}"
|
||||
|
||||
return results
|
||||
|
||||
def validate_installation(self, project_dir: Path) -> Dict[str, List[str]]:
|
||||
"""Validate agents installed in a project."""
|
||||
errors = {}
|
||||
agents_dir = project_dir / "agents"
|
||||
|
||||
if not agents_dir.exists():
|
||||
return {"project": ["No agents directory found"]}
|
||||
|
||||
# Check each installed agent
|
||||
for agent_file in agents_dir.glob("agent-*.md"):
|
||||
agent_name = agent_file.stem.replace("agent-", "")
|
||||
agent_errors = []
|
||||
|
||||
try:
|
||||
# Try to parse the agent file
|
||||
from .registry import AgentDefinition
|
||||
AgentDefinition.from_file(agent_file)
|
||||
except Exception as e:
|
||||
agent_errors.append(f"Invalid agent format: {str(e)}")
|
||||
|
||||
if agent_errors:
|
||||
errors[agent_name] = agent_errors
|
||||
|
||||
return errors
|
||||
|
||||
def _create_backup(self, agents_dir: Path):
|
||||
"""Create a backup of the existing agents directory."""
|
||||
backup_dir = agents_dir.parent / f"agents_backup_{self._get_timestamp()}"
|
||||
shutil.copytree(agents_dir, backup_dir)
|
||||
print(f"Created backup at: {backup_dir}")
|
||||
|
||||
def _get_timestamp(self) -> str:
|
||||
"""Get current timestamp for backup naming."""
|
||||
import datetime
|
||||
return datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
|
||||
def _update_claude_config(self, agent_names: List[str], config_path: Path):
|
||||
"""Update Claude Code configuration with agent references."""
|
||||
try:
|
||||
# Read existing config
|
||||
config = {}
|
||||
if config_path.exists():
|
||||
with open(config_path, 'r') as f:
|
||||
config = json.load(f)
|
||||
|
||||
# Ensure agents section exists
|
||||
if 'agents' not in config:
|
||||
config['agents'] = {}
|
||||
|
||||
# Add agent references
|
||||
for agent_name in agent_names:
|
||||
config['agents'][agent_name] = {
|
||||
"path": f"agents/agent-{agent_name}.md",
|
||||
"enabled": True
|
||||
}
|
||||
|
||||
# Write updated config
|
||||
with open(config_path, 'w') as f:
|
||||
json.dump(config, f, indent=2)
|
||||
|
||||
print(f"Updated Claude configuration: {config_path}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not update Claude config: {e}")
|
||||
|
||||
def _update_makefile(self, agent_names: List[str], makefile_path: Path):
|
||||
"""Update Makefile with agent-specific targets."""
|
||||
try:
|
||||
# Read existing Makefile
|
||||
with open(makefile_path, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Add agent management targets if not present
|
||||
agent_targets = """
|
||||
# Agent Management Targets
|
||||
list-agents:
|
||||
\t@echo "Installed agents:"
|
||||
\t@ls agents/ 2>/dev/null | grep agent- | sed 's/agent-//g' | sed 's/.md//g' || echo "No agents installed"
|
||||
|
||||
update-agents:
|
||||
\t@echo "Updating agents..."
|
||||
\t@kaizen-agentic update
|
||||
|
||||
validate-agents:
|
||||
\t@echo "Validating agents..."
|
||||
\t@kaizen-agentic validate agents/
|
||||
"""
|
||||
|
||||
if "list-agents:" not in content:
|
||||
content += agent_targets
|
||||
|
||||
# Write updated Makefile
|
||||
with open(makefile_path, 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
print(f"Updated Makefile: {makefile_path}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not update Makefile: {e}")
|
||||
|
||||
def _update_documentation(self, agent_names: List[str], project_dir: Path):
|
||||
"""Update project documentation with agent information."""
|
||||
try:
|
||||
claude_md = project_dir / "CLAUDE.md"
|
||||
|
||||
agent_section = "## Installed Agents\n\n"
|
||||
agent_section += "This project includes the following specialized agents:\n\n"
|
||||
|
||||
# Group agents by category
|
||||
categories = {}
|
||||
for agent_name in agent_names:
|
||||
agent = self.registry.get_agent(agent_name)
|
||||
if agent:
|
||||
category = agent.category.value
|
||||
if category not in categories:
|
||||
categories[category] = []
|
||||
categories[category].append(agent)
|
||||
|
||||
# Generate documentation
|
||||
for category, agents in categories.items():
|
||||
agent_section += f"### {category.replace('-', ' ').title()}\n\n"
|
||||
for agent in agents:
|
||||
agent_section += f"- **{agent.name}**: {agent.description}\n"
|
||||
agent_section += "\n"
|
||||
|
||||
agent_section += "Use these agents by referencing them in your Claude Code interactions.\n\n"
|
||||
|
||||
# Update or create CLAUDE.md
|
||||
if claude_md.exists():
|
||||
with open(claude_md, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Replace existing agent section or append
|
||||
if "## Installed Agents" in content:
|
||||
import re
|
||||
content = re.sub(
|
||||
r'## Installed Agents.*?(?=##|\Z)',
|
||||
agent_section,
|
||||
content,
|
||||
flags=re.DOTALL
|
||||
)
|
||||
else:
|
||||
content += "\n" + agent_section
|
||||
|
||||
with open(claude_md, 'w') as f:
|
||||
f.write(content)
|
||||
else:
|
||||
# Create new CLAUDE.md
|
||||
header = "# Claude Code Configuration\n\n"
|
||||
header += "This file contains Claude Code configuration and agent information.\n\n"
|
||||
|
||||
with open(claude_md, 'w') as f:
|
||||
f.write(header + agent_section)
|
||||
|
||||
print(f"Updated documentation: {claude_md}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Warning: Could not update documentation: {e}")
|
||||
|
||||
|
||||
class ProjectInitializer:
|
||||
"""Initializes new projects with agents and templates."""
|
||||
|
||||
def __init__(self, registry: AgentRegistry):
|
||||
self.registry = registry
|
||||
|
||||
def init_project(
|
||||
self,
|
||||
project_dir: Path,
|
||||
template: str = "python-basic",
|
||||
agent_names: Optional[List[str]] = None,
|
||||
project_name: Optional[str] = None
|
||||
) -> Dict[str, str]:
|
||||
"""Initialize a new project with agents and structure."""
|
||||
results = {}
|
||||
|
||||
# Create project directory
|
||||
project_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Get agents for template
|
||||
if agent_names is None:
|
||||
templates = self.registry.get_agent_templates()
|
||||
agent_names = templates.get(template, templates["python-basic"])
|
||||
|
||||
# Set up project name
|
||||
if project_name is None:
|
||||
project_name = project_dir.name
|
||||
|
||||
# Install agents
|
||||
config = InstallationConfig(
|
||||
target_dir=project_dir,
|
||||
claude_config_path=project_dir / "CLAUDE.md",
|
||||
makefile_path=project_dir / "Makefile"
|
||||
)
|
||||
|
||||
installer = AgentInstaller(self.registry)
|
||||
install_results = installer.install_agents(agent_names, config)
|
||||
results.update(install_results)
|
||||
|
||||
# Create basic project structure
|
||||
self._create_project_structure(project_dir, project_name, template)
|
||||
|
||||
return results
|
||||
|
||||
def _create_project_structure(self, project_dir: Path, project_name: str, template: str):
|
||||
"""Create basic project structure based on template."""
|
||||
# Create directories
|
||||
dirs_to_create = ["src", "tests", "docs"]
|
||||
if template.startswith("python"):
|
||||
dirs_to_create.extend([f"src/{project_name.replace('-', '_')}", ".github/workflows"])
|
||||
|
||||
for dir_name in dirs_to_create:
|
||||
(project_dir / dir_name).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Create basic files
|
||||
self._create_gitignore(project_dir)
|
||||
self._create_readme(project_dir, project_name)
|
||||
|
||||
if template.startswith("python"):
|
||||
self._create_pyproject_toml(project_dir, project_name)
|
||||
self._create_init_py(project_dir, project_name)
|
||||
|
||||
def _create_gitignore(self, project_dir: Path):
|
||||
"""Create .gitignore file."""
|
||||
gitignore_content = """# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# Virtual environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# IDEs
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Testing
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
.tox/
|
||||
"""
|
||||
(project_dir / ".gitignore").write_text(gitignore_content)
|
||||
|
||||
def _create_readme(self, project_dir: Path, project_name: str):
|
||||
"""Create README.md file."""
|
||||
readme_content = f"""# {project_name}
|
||||
|
||||
A Python project following best practices with Kaizen Agentic agents.
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone <repository-url>
|
||||
cd {project_name}
|
||||
|
||||
# Set up development environment
|
||||
make setup-complete
|
||||
|
||||
# Activate virtual environment
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# Run tests
|
||||
make test
|
||||
|
||||
# Check code quality
|
||||
make lint
|
||||
|
||||
# Format code
|
||||
make format
|
||||
```
|
||||
|
||||
## Agents
|
||||
|
||||
This project uses Kaizen Agentic agents for development workflow automation.
|
||||
See CLAUDE.md for agent details and usage.
|
||||
"""
|
||||
(project_dir / "README.md").write_text(readme_content)
|
||||
|
||||
def _create_pyproject_toml(self, project_dir: Path, project_name: str):
|
||||
"""Create pyproject.toml file."""
|
||||
package_name = project_name.replace('-', '_')
|
||||
pyproject_content = f"""[build-system]
|
||||
requires = ["setuptools>=61.0", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "{project_name}"
|
||||
version = "0.1.0"
|
||||
description = "A Python project with Kaizen Agentic agents"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.8"
|
||||
license = {{text = "MIT"}}
|
||||
authors = [
|
||||
{{name = "Author Name", email = "author@example.com"}}
|
||||
]
|
||||
dependencies = []
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"pytest>=7.0",
|
||||
"black>=22.0",
|
||||
"flake8>=5.0",
|
||||
"mypy>=1.0",
|
||||
"kaizen-agentic>=0.1.0",
|
||||
]
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["src"]
|
||||
|
||||
[tool.black]
|
||||
line-length = 88
|
||||
target-version = ['py38']
|
||||
|
||||
[tool.flake8]
|
||||
max-line-length = 100
|
||||
exclude = [".git", "__pycache__", "build", "dist"]
|
||||
|
||||
[tool.mypy]
|
||||
python_version = "3.8"
|
||||
warn_return_any = true
|
||||
warn_unused_configs = true
|
||||
disallow_untyped_defs = true
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
python_files = ["test_*.py"]
|
||||
python_classes = ["Test*"]
|
||||
python_functions = ["test_*"]
|
||||
"""
|
||||
(project_dir / "pyproject.toml").write_text(pyproject_content)
|
||||
|
||||
def _create_init_py(self, project_dir: Path, project_name: str):
|
||||
"""Create package __init__.py file."""
|
||||
package_name = project_name.replace('-', '_')
|
||||
init_content = f'''"""
|
||||
{project_name} - A Python project with Kaizen Agentic agents.
|
||||
"""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
'''
|
||||
(project_dir / f"src/{package_name}/__init__.py").write_text(init_content)
|
||||
Reference in New Issue
Block a user