Skip to content

CLI Documentation#

The CLI is the core interface of ai_term. It interacts with the users via text or voice, manages the chat history, and orchestrates calls to LLM, STT, and TTS services.

Overview#

The CLI is built using Textual, a TUI framework for Python.

Entry Point#

The application uses Typer for its CLI interface. The main entry point is src/ai_term/cli/main.py.

# Start the TUI application (default)
ai-term

# Start background services
ai-term start

# Check service status
ai-term status

Commands#

ai-term (default)#

Launches the full-screen terminal user interface for chatting with the AI.

ai-term start#

Starts the Docker-based STT and TTS services. - --build: Force rebuild of Docker images. - --detach / -d: Run in detached mode (default).

ai-term status#

Displays a formatted table showing the current state and port mappings of the backend services.

Structure#

  • src/ai_term/cli/ui: Contains all UI components (Screens, Widgets, Styles).
  • src/ai_term/cli/core: Contains business logic (Agent, Audio Client, MCP Manager).
  • src/ai_term/cli/db: Database models and engine using SQLAlchemy and aiosqlite.
  • src/ai_term/cli/config.py: Configuration management.

API Reference#

ai_term.cli.main #

CLI Chat Application Entry Point.

check_docker_compose() #

Check if docker compose is available.

Source code in src/ai_term/cli/main.py
def check_docker_compose():
    """Check if docker compose is available."""
    try:
        subprocess.run(
            ["docker", "compose", "version"],
            check=True,
            capture_output=True,
        )
        return True
    except (subprocess.CalledProcessError, FileNotFoundError):
        return False

force_exit() #

Force exit on interpreter shutdown.

Source code in src/ai_term/cli/main.py
def force_exit():
    """Force exit on interpreter shutdown."""
    os._exit(0)

get_compose_file_path() #

Get the path to the bundled docker-compose.yml file.

Source code in src/ai_term/cli/main.py
def get_compose_file_path() -> str:
    """Get the path to the bundled docker-compose.yml file."""
    try:
        from importlib.resources import files

        resource_path = files("ai_term.cli.resources").joinpath("docker-compose.yml")
        # For Python 3.9+, we can use as_file for a context manager
        # but for simplicity, we'll convert to string path
        return str(resource_path)
    except Exception:
        # Fallback: check current directory
        if os.path.exists("docker-compose.yml"):
            return os.path.abspath("docker-compose.yml")
        elif os.path.exists("docker-compose.yaml"):
            return os.path.abspath("docker-compose.yaml")
        return ""

main(ctx) #

AI-Term CLI Entry Point.

Runs the main TUI application by default. Use 'start' to run background services. Use 'status' to check service status.

Source code in src/ai_term/cli/main.py
@app.callback(invoke_without_command=True)
def main(ctx: typer.Context):
    """
    AI-Term CLI Entry Point.

    Runs the main TUI application by default.
    Use 'start' to run background services.
    Use 'status' to check service status.
    """
    if ctx.invoked_subcommand is None:
        # Default behavior: Run the TUI
        tui_app = ChatApp()
        try:
            tui_app.run()
        except KeyboardInterrupt:
            pass
        finally:
            force_exit()

start(build=False, detach=True) #

Start the backend services (STT/TTS) using Docker Compose.

Uses the bundled docker-compose.yml file automatically. Pass --build to force rebuild of images.

Source code in src/ai_term/cli/main.py
@app.command()
def start(
    build: Annotated[
        bool, typer.Option("--build", help="Force rebuild of Docker images.")
    ] = False,
    detach: Annotated[
        bool, typer.Option("--detach", "-d", help="Run in detached mode.")
    ] = True,
):
    """
    Start the backend services (STT/TTS) using Docker Compose.

    Uses the bundled docker-compose.yml file automatically.
    Pass --build to force rebuild of images.
    """
    if not check_docker_compose():
        typer.secho("Error: 'docker compose' is not available.", fg=typer.colors.RED)
        raise typer.Exit(code=1)

    compose_file = get_compose_file_path()
    if not compose_file:
        typer.secho(
            "Error: Could not locate docker-compose.yml file.", fg=typer.colors.RED
        )
        raise typer.Exit(code=1)

    cmd = ["docker", "compose", "-f", compose_file, "up"]
    if detach:
        cmd.append("-d")

    if build:
        cmd.append("--build")

    typer.echo("Starting services...")
    try:
        subprocess.run(cmd, check=True)
        typer.secho("Services started successfully.", fg=typer.colors.GREEN)
    except subprocess.CalledProcessError:
        typer.secho("Failed to start services.", fg=typer.colors.RED)
        raise typer.Exit(code=1)

status() #

Check the status of backend services.

Source code in src/ai_term/cli/main.py
@app.command()
def status():
    """
    Check the status of backend services.
    """
    if not check_docker_compose():
        typer.secho("Error: 'docker compose' is not available.", fg=typer.colors.RED)
        raise typer.Exit(code=1)

    compose_file = get_compose_file_path()
    if not compose_file:
        typer.secho(
            "Error: Could not locate docker-compose.yml file.", fg=typer.colors.RED
        )
        raise typer.Exit(code=1)

    try:
        result = subprocess.run(
            ["docker", "compose", "-f", compose_file, "ps", "--format", "json"],
            check=True,
            capture_output=True,
            text=True,
        )

        # Parse JSON output (one JSON object per line)
        services = []
        for line in result.stdout.strip().split("\n"):
            if line.strip():
                try:
                    services.append(json.loads(line))
                except json.JSONDecodeError:
                    continue

        if not services:
            typer.secho("No services are currently running.", fg=typer.colors.YELLOW)
            return

        console = Console()
        table = Table(title="AI-Term Services Status", box=box.ROUNDED)

        table.add_column("Service", style="cyan", no_wrap=True)
        table.add_column("State", style="green")
        table.add_column("Status", style="magenta")
        table.add_column("Ports", style="yellow")

        for service in services:
            # Extract relevant fields
            name = service.get("Service", "Unknown")
            state = service.get("State", "Unknown")
            status = service.get("Status", "Unknown")

            # Format ports
            ports = service.get("Publishers", [])
            port_str = ""
            if ports:
                port_list = []
                for p in ports:
                    if isinstance(p, dict):
                        # Handle list of dicts format from newer docker compose versions
                        url = p.get("URL", "0.0.0.0")
                        pub_port = p.get("PublishedPort", "")
                        target_port = p.get("TargetPort", "")
                        if pub_port:
                            port_list.append(f"{url}:{pub_port}->{target_port}")
                port_str = ", ".join(port_list)

            # Fallback for older formats or if Publishers structure is different
            if not port_str:
                port_str = service.get("Ports", "")

            # Colorize state
            state_style = "green" if state.lower() == "running" else "red"

            table.add_row(
                name, f"[{state_style}]{state}[/{state_style}]", status, port_str
            )

        console.print(table)

    except subprocess.CalledProcessError:
        typer.secho("Failed to check status. Is docker running?", fg=typer.colors.RED)
        raise typer.Exit(code=1)
    except Exception as e:
        typer.secho(f"An unexpected error occurred: {e}", fg=typer.colors.RED)
        raise typer.Exit(code=1)