forked from cleveragents/cleveragents-core
7.3 KiB
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
- Dependency Injection: Commands get services from DI container
- Configuration: Load settings before command execution
- Logging: Initialize logging based on --verbose flag
- Error Handling: Centralized exception handler
- Output Formatting: JSON output with --json flag