Files
cleveragents-core/docs/architecture/decisions/ADR-009-cli-framework.md

7.3 KiB

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

# 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

# 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

# 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

# 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

# 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

# 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