Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions aai_cli/agent/voices.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,8 @@
def format_voice_list() -> str:
"""Human-readable, newline-separated voice IDs for --list-voices."""
return "\n".join(VOICES)


def complete_voice(incomplete: str) -> list[str]:
"""Shell-completion callback for ``--voice``: known voice ids matching the prefix."""
return [v for v in VOICES if v.startswith(incomplete)]
35 changes: 35 additions & 0 deletions aai_cli/choices.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from __future__ import annotations

import enum

# CLI-owned closed value sets for ``-o/--output``. ``StrEnum`` members *are* their
# string values: Typer renders them as choices in ``--help`` (e.g.
# [text|id|status|utterances|srt|json]), validates input with a clean listing error,
# and completes them on Tab — while existing ``field == "text"`` comparisons and
# ``select_transcript_field(t, field)`` calls keep working unchanged.


class TranscriptOutput(enum.StrEnum):
"""Single-field output modes for a finished transcript (`transcribe`, `transcripts get`)."""

text = "text"
id = "id"
status = "status"
utterances = "utterances"
srt = "srt"
json = "json"


class TextOrJson(enum.StrEnum):
"""Output mode for the streaming/LLM commands: plain finalized text or raw JSON."""

text = "text"
json = "json"


class Scope(enum.StrEnum):
"""Coding-agent config scope for `aai setup` (passed through to `claude mcp add`)."""

user = "user"
project = "project"
local = "local"
4 changes: 0 additions & 4 deletions aai_cli/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,10 +134,6 @@ def transcript_json_payload(transcript: Any) -> dict[str, object]:
return getattr(transcript, "json_response", None) or transcript_summary(transcript)


# Fields `transcribe` and `transcripts get` expose via `-o/--output` (raw, pipe-friendly).
TRANSCRIPT_OUTPUT_FIELDS = ("text", "id", "status", "utterances", "srt", "json")


def _transcript_text(transcript: Any) -> str:
return str(getattr(transcript, "text", "") or "")

Expand Down
17 changes: 12 additions & 5 deletions aai_cli/commands/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import typer

from aai_cli import client, code_gen, config, help_panels, output
from aai_cli import choices, client, code_gen, config, help_panels, output
from aai_cli.agent.audio import SAMPLE_RATE, DuplexAudio, NullPlayer
from aai_cli.agent.render import AgentRenderer
from aai_cli.agent.session import (
Expand All @@ -15,7 +15,7 @@
AgentRunConfig,
run_session,
)
from aai_cli.agent.voices import DEFAULT_VOICE, VOICES, format_voice_list
from aai_cli.agent.voices import DEFAULT_VOICE, VOICES, complete_voice, format_voice_list
from aai_cli.context import AppState, run_command
from aai_cli.errors import CLIError, UsageError
from aai_cli.help_text import examples_epilog
Expand Down Expand Up @@ -83,24 +83,31 @@ def agent(
sample: bool = typer.Option(
False, "--sample", help="Speak the hosted wildfires.mp3 sample to the agent."
),
voice: str = typer.Option(DEFAULT_VOICE, "--voice", help="Agent voice. See --list-voices."),
voice: str = typer.Option(
DEFAULT_VOICE,
"--voice",
help="Agent voice. See --list-voices.",
autocompletion=complete_voice,
),
system_prompt: str = typer.Option(
DEFAULT_PROMPT, "--system-prompt", help="System prompt (the agent's persona)."
),
system_prompt_file: Path | None = typer.Option(
None,
"--system-prompt-file",
help="Read the system prompt from a file (overrides --system-prompt).",
exists=True,
dir_okay=False,
),
greeting: str = typer.Option(DEFAULT_GREETING, "--greeting", help="Spoken greeting."),
device: int | None = typer.Option(None, "--device", help="Microphone device index."),
list_voices: bool = typer.Option(False, "--list-voices", help="Print known voices and exit."),
json_out: bool = typer.Option(False, "--json", help="Emit newline-delimited JSON events."),
output_field: str | None = typer.Option(
output_field: choices.TextOrJson | None = typer.Option(
None,
"-o",
"--output",
help="Output mode: 'text' (you:/agent: lines as plain stdout, pipe-friendly) or 'json'.",
help="Output mode: text (you:/agent: lines as plain stdout, pipe-friendly) or json.",
),
show_code: bool = typer.Option(
False,
Expand Down
12 changes: 8 additions & 4 deletions aai_cli/commands/llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import typer
from rich.markup import escape

from aai_cli import config, help_panels, output, stdio
from aai_cli import choices, config, help_panels, output, stdio
from aai_cli import llm as gateway
from aai_cli.context import AppState, run_command
from aai_cli.errors import UsageError
Expand Down Expand Up @@ -56,7 +56,12 @@ def llm(
ctx: typer.Context,
prompt: str | None = typer.Argument(None, help="The prompt to send to the model."),
# Note: text piped on stdin is injected into the prompt (e.g. `cat notes | aai llm "summarize"`).
model: str = typer.Option(gateway.DEFAULT_MODEL, "--model", help="LLM Gateway model."),
model: str = typer.Option(
gateway.DEFAULT_MODEL,
"--model",
help="LLM Gateway model.",
autocompletion=gateway.complete_model,
),
transcript_id: str | None = typer.Option(
None, "--transcript-id", help="Inject this transcript's text into the prompt."
),
Expand All @@ -69,7 +74,7 @@ def llm(
"the answer in place on every finalized turn (e.g. aai stream -o text | aai "
'llm -f "summarize action items as I talk"). Ctrl-C to stop.',
),
output_field: str | None = typer.Option(
output_field: choices.TextOrJson | None = typer.Option(
None,
"-o",
"--output",
Expand Down Expand Up @@ -121,7 +126,6 @@ def body(state: AppState, json_mode: bool) -> None:
suggestion="Or pass --list-models to see available models.",
)
prompt_text = prompt
output.validate_output_field(output_field, ("text", "json"))
api_key = config.resolve_api_key(profile=state.profile)
# Text piped on stdin becomes the content the prompt operates on, unless an
# explicit --transcript-id is given (that injects server-side and takes priority).
Expand Down
25 changes: 6 additions & 19 deletions aai_cli/commands/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,8 @@

import typer

from aai_cli import output
from aai_cli import choices, output
from aai_cli.context import AppState, run_command
from aai_cli.errors import UsageError
from aai_cli.help_text import examples_epilog
from aai_cli.steps import Step, render_steps

Expand All @@ -27,7 +26,6 @@
MCP_NAME = "assemblyai-docs"
MCP_URL = "https://mcp.assemblyai.com/docs"
SKILL_REPO = "AssemblyAI/assemblyai-skill"
_VALID_SCOPES = ("user", "project", "local")
_STEPS_HEADING = "AssemblyAI coding-agent setup:"


Expand Down Expand Up @@ -300,24 +298,17 @@ def _render(data: dict[str, list[Step]]) -> str:
)
def install(
ctx: typer.Context,
scope: str = typer.Option(
"user",
scope: choices.Scope = typer.Option(
choices.Scope.user,
"--scope",
help=(
"Config scope to register the MCP under: user, project, or local. "
"Presence is detected across all scopes."
),
help="Config scope to register the MCP under. Presence is detected across all scopes.",
),
force: bool = typer.Option(False, "--force", help="Reinstall even if already present."),
json_out: bool = typer.Option(False, "--json", help="Output raw JSON."),
) -> None:
"""Install the AssemblyAI docs MCP server and skills into your coding agent."""

def body(_state: AppState, json_mode: bool) -> None:
if scope not in _VALID_SCOPES:
raise UsageError(
f"Invalid --scope '{scope}'. Choose one of: {', '.join(_VALID_SCOPES)}."
)
steps = [_install_mcp(scope, force), _install_skill(force), _install_cli_skill(force)]
output.emit({"steps": steps}, _render, json_mode=json_mode)
if any(s["status"] == "failed" for s in steps):
Expand Down Expand Up @@ -355,11 +346,11 @@ def body(_state: AppState, json_mode: bool) -> None:
)
def remove(
ctx: typer.Context,
scope: str | None = typer.Option(
scope: choices.Scope | None = typer.Option(
None,
"--scope",
help=(
"Only remove the MCP from this scope (user, project, or local). "
"Only remove the MCP from this scope. "
"Default: remove from whichever scope it exists in."
),
),
Expand All @@ -368,10 +359,6 @@ def remove(
"""Remove the AssemblyAI MCP server and skills from your coding agent."""

def body(_state: AppState, json_mode: bool) -> None:
if scope is not None and scope not in _VALID_SCOPES:
raise UsageError(
f"Invalid --scope '{scope}'. Choose one of: {', '.join(_VALID_SCOPES)}."
)
steps = [_remove_mcp(scope), _remove_skill(), _remove_cli_skill()]
output.emit({"steps": steps}, _render, json_mode=json_mode)
if any(s["status"] == "failed" for s in steps):
Expand Down
Loading
Loading