# ADR-009: CLI Framework Selection ## Status Accepted ## Context The discovery phase identified 67 CLI commands that need implementation. We need a framework that: - Supports complex nested command structures - Provides automatic help generation - Handles argument parsing and validation - Supports both interactive and non-interactive modes - Integrates well with asyncio - Provides good testing support Python CLI framework options: - **Click**: Mature, decorator-based, extensive ecosystem - **Typer**: Modern, built on Click, uses type hints - **argparse**: Built-in, no dependencies, verbose - **Fire**: Auto-generates CLI from functions, less control ## Decision We will use **Typer** as our CLI framework, leveraging its type hint integration and modern Python features. The CLI surface is actor-first: provider/model flags were removed in favor of `--actor` plus a default actor stored in the actor registry (see ADR-008 for registry boundaries). CLI help/man snippets and README/docs must avoid `--provider/--model` references and show actor-only usage with default resolution. ### Architecture ```python # cleveragents.cli.app import typer from typing import Optional from pathlib import Path app = typer.Typer( name="agents", help="CleverAgents - AI-powered development assistant (actor-first CLI)", no_args_is_help=True, rich_markup_mode="rich", pretty_exceptions_enable=True, context_settings={"help_option_names": ["-h", "--help"]} ) # Sub-command groups plan_app = typer.Typer(help="Plan operations (actor required)") context_app = typer.Typer(help="Context management commands") actor_app = typer.Typer(help="Actor management and defaults") app.add_typer(plan_app, name="plan") app.add_typer(context_app, name="context") app.add_typer(actor_app, name="actor") ``` ### Command Implementation ```python # cleveragents.cli.commands.plan from typer import Argument, Option from rich.console import Console from rich.progress import Progress console = Console() @plan_app.command("create") def create_plan( name: str = Argument(..., help="Name of the plan"), description: str = Option(None, "--desc", "-d", help="Plan description"), actor: str = Option(None, "--actor", "-a", help="Actor to use (required; defaults to the configured actor if set)"), context: Optional[List[Path]] = Option(None, "--context", "-c", help="Context files"), interactive: bool = Option(True, "--interactive/--no-interactive", help="Interactive mode") ): """Create a new development plan.""" # Validate arguments using Pydantic args = PlanCreateArgs( name=name, description=description, actor=actor, context=context or [] ) # Show progress for long operations with Progress() as progress: task = progress.add_task("Creating plan...", total=100) # Delegate to application service result = asyncio.run(create_plan_async(args)) progress.update(task, completed=100) # Rich output console.print(f"[green]✓[/green] Plan '{result.name}' created successfully") console.print(f"ID: {result.id}") ``` ### Async Command Support ```python # cleveragents.cli.async_support import asyncio from functools import wraps def async_command(f): """Decorator to run async functions in CLI commands""" @wraps(f) def wrapper(*args, **kwargs): return asyncio.run(f(*args, **kwargs)) return wrapper @plan_app.command("build") @async_command async def build_plan( plan_id: str = Argument(..., help="Plan ID or name"), stream: bool = Option(True, "--stream/--no-stream", help="Stream output") ): """Build a plan asynchronously.""" async with get_plan_service() as service: if stream: async for chunk in service.build_stream(plan_id): console.print(chunk, end="") else: result = await service.build(plan_id) console.print(result) ``` ### Interactive Mode ```python # cleveragents.cli.interactive from prompt_toolkit import prompt from prompt_toolkit.completion import WordCompleter @app.command("repl") def start_repl(): """Start interactive REPL mode.""" commands = WordCompleter(['plan', 'context', 'build', 'apply', 'exit']) console.print("[bold]CleverAgents Interactive Mode[/bold]") console.print("Type 'help' for commands, 'exit' to quit\n") while True: try: command = prompt("agents> ", completer=commands) if command == "exit": break # Parse and execute command app(command.split(), standalone_mode=False) except Exception as e: console.print(f"[red]Error: {e}[/red]") ``` ### Testing Support ```python # tests/test_cli.py from typer.testing import CliRunner from cleveragents.cli.app import app runner = CliRunner() def test_plan_create(): result = runner.invoke(app, ["plan", "create", "test-plan"]) assert result.exit_code == 0 assert "Plan 'test-plan' created successfully" in result.stdout def test_help(): result = runner.invoke(app, ["--help"]) assert result.exit_code == 0 assert "CleverAgents" in result.stdout ``` ### Error Handling ```python # cleveragents.cli.errors from typer import Exit def handle_cli_exception(exc: Exception): """Convert exceptions to CLI-friendly output""" if isinstance(exc, ValidationError): console.print(f"[red]Validation Error:[/red] {exc.message}") raise Exit(1) elif isinstance(exc, AuthenticationError): console.print("[red]Authentication failed.[/red] Run 'agents auth login'") raise Exit(2) elif isinstance(exc, NetworkError): console.print(f"[red]Network Error:[/red] {exc.message}") console.print("[yellow]Check your connection and try again[/yellow]") raise Exit(3) else: console.print(f"[red]Error:[/red] {exc}") raise Exit(99) ``` ## Consequences ### Positive - Actor-only surface keeps provider/model details inside actor configs and registry - Type hints provide automatic validation - Rich terminal output with colors and formatting - Built on Click's solid foundation - Great IDE support with autocomplete - Easy testing with CliRunner - Automatic help generation ### Negative - Additional dependency (typer + rich) - Less flexibility than raw Click - Learning curve for decorators ### Neutral - Opinionated structure - Need to wrap async commands - Documentation generated from docstrings ## Command Mapping Based on the 67 commands discovered: | Category | Commands | Implementation | |----------|----------|----------------| | Plan | create, list, load, delete, archive | `plan_app` | | Context | add, remove, list, update | `context_app` | | Execution | tell, continue, build, apply | Main `app` | | Model | list, set, add-custom | `model_app` | | Auth | login, logout, whoami | `auth_app` | ## Integration Points 1. **Dependency Injection**: Commands get services from DI container 2. **Configuration**: Load settings before command execution 3. **Logging**: Initialize logging based on --verbose flag 4. **Error Handling**: Centralized exception handler 5. **Output Formatting**: JSON output with --json flag ## References - [Typer Documentation](https://typer.tiangolo.com/) - [Click Documentation](https://click.palletsprojects.com/) - [Rich Documentation](https://rich.readthedocs.io/)