"""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 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)