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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ Lessons that cost time in agent sessions — read before exercising `uv run asse
- Ruff lint set: see `[tool.ruff.lint]` in `pyproject.toml`. `S603/S607` are ignored project-wide because the CLI intentionally shells out to `claude`/`npx` with controlled args. `B008` is ignored (Typer uses `typer.Option/Argument` calls as defaults).
- mypy is strict on `aai_cli` (`disallow_untyped_defs`); tests are type-checked but exempt from return annotations.
- Errors → stderr, data → stdout. Preserve this split; it's what makes the CLI pipeline-safe.
- **Help copy is terse and period-less (Codex-CLI style)**: one-line command summaries (the docstring's first line) and single-sentence option/argument `help=` strings are imperative, sentence-case, and carry **no trailing period** — `"Burn always-visible captions into a video"`, not `"…video."`. Only genuinely multi-sentence help (e.g. `"X. Default: Y."`) keeps normal punctuation. The strings render in `assembly --help`, so they're pinned by the syrupy `--help` goldens (`tests/__snapshots__/test_snapshots_help_*.ambr`) — regenerate with `--snapshot-update`, never hand-edit. Don't drop the period on internal helper docstrings (they aren't snapshot-covered, so the mutation gate would flag the changed line).
- **Deprecate flags with hidden traps, not removal**: keep the old flag parsing (`hidden=True`), emit a one-line "use X instead" warning, and drop it a release or two later — never hard-break a script mid-cycle. `login --api-key` (→ `--with-api-key`) is the pattern to copy.
- **Secrets never ride argv**: a key/token-valued option must read from stdin (`--with-api-key`) or the env, so it can't leak into shell history or `ps`. Run commands deliberately have no `--api-key` at all.
- **Every NDJSON stream line carries a `"type"` field** (see REFERENCE.md "JSON output"); new event types are additive, existing fields stay stable.
12 changes: 6 additions & 6 deletions aai_cli/commands/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ class _Usage(BaseModel):
usage_items: Annotated[list[_Window], _MappingList] = Field(default_factory=list[_Window])


app = typer.Typer(help="Account billing, usage, and limits.")
app = typer.Typer(help="Account billing, usage, and limits")

SPEC = command_registry.CommandModuleSpec(
panel=help_panels.ACCOUNT,
Expand All @@ -147,7 +147,7 @@ def balance(
ctx: typer.Context,
json_out: bool = options.json_option(),
) -> None:
"""Show your remaining account balance."""
"""Show your remaining account balance"""

def body(state: AppState, json_mode: bool) -> None:
_, jwt = state.resolve_session()
Expand Down Expand Up @@ -183,17 +183,17 @@ def usage(
),
end: str | None = typer.Option(None, "--end", help="End date (YYYY-MM-DD). Default: today."),
window: str | None = typer.Option(
None, "--window", help="Window size: 'day', 'week', or 'month'."
None, "--window", help="Window size: 'day', 'week', or 'month'"
),
include_zero: bool = typer.Option(
False,
"--include-zero",
"--all",
help="Include zero-usage windows (matches --include-logins on `assembly audit`).",
help="Include zero-usage windows (matches --include-logins on `assembly audit`)",
),
json_out: bool = options.json_option(),
) -> None:
"""Show usage over a date range (defaults to the last 30 days)."""
"""Show usage over a date range (default: last 30 days)"""

def body(state: AppState, json_mode: bool) -> None:
# Parse/validate the flags before any session resolution or network work,
Expand Down Expand Up @@ -270,7 +270,7 @@ def limits(
ctx: typer.Context,
json_out: bool = options.json_option(),
) -> None:
"""Show your account's rate limits per service."""
"""Show your account's rate limits per service"""

def body(state: AppState, json_mode: bool) -> None:
account_id, jwt = state.resolve_session()
Expand Down
20 changes: 10 additions & 10 deletions aai_cli/commands/agent/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def agent(
None, help="Audio file path or URL to speak to the agent. Omit to use the microphone."
),
sample: bool = typer.Option(
False, "--sample", help="Speak the hosted wildfires.mp3 sample to the agent."
False, "--sample", help="Speak the hosted wildfires.mp3 sample to the agent"
),
voice: str = typer.Option(
DEFAULT_VOICE,
Expand All @@ -62,32 +62,32 @@ def agent(
autocompletion=complete_voice,
),
system_prompt: str = typer.Option(
DEFAULT_PROMPT, "--system-prompt", help="System prompt (the agent's persona)."
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).",
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 = options.json_option("Emit newline-delimited JSON events."),
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 = options.json_option("Emit newline-delimited JSON events"),
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,
"--show-code",
help="Print the equivalent Python SDK code and exit (does not start a session).",
help="Print the equivalent Python SDK code and exit (does not start a session)",
),
) -> None:
"""Have a live two-way voice conversation with an AssemblyAI voice agent.
"""Hold a live two-way voice conversation with a voice agent

Use headphones: the mic stays open while the agent speaks, so on
speakers it would hear itself and loop. Pass an audio file/URL (or
Expand Down
12 changes: 6 additions & 6 deletions aai_cli/commands/audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from aai_cli.context import AppState, run_command
from aai_cli.help_text import examples_epilog

app = typer.Typer(help="View your account's audit log.")
app = typer.Typer(help="View your account's audit log")

SPEC = command_registry.CommandModuleSpec(
panel=help_panels.ACCOUNT,
Expand Down Expand Up @@ -93,15 +93,15 @@ def _audit_rows(payload: Mapping[str, object]) -> list[dict[str, object]]:
)
def audit(
ctx: typer.Context,
limit: int = typer.Option(20, "--limit", min=1, help="How many entries to show."),
action: str | None = typer.Option(None, "--action", help="Filter by raw action name."),
resource: str | None = typer.Option(None, "--resource", help="Filter by raw resource type."),
limit: int = typer.Option(20, "--limit", min=1, help="How many entries to show"),
action: str | None = typer.Option(None, "--action", help="Filter by raw action name"),
resource: str | None = typer.Option(None, "--resource", help="Filter by raw resource type"),
include_logins: bool = typer.Option(
False, "--include-logins", help="Show successful login events."
False, "--include-logins", help="Show successful login events"
),
json_out: bool = options.json_option(),
) -> None:
"""List recent audit-log entries for your account."""
"""List recent audit-log entries for your account"""

def body(state: AppState, json_mode: bool) -> None:
_, jwt = state.resolve_session()
Expand Down
14 changes: 7 additions & 7 deletions aai_cli/commands/caption/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,32 +44,32 @@ def caption(
media: str = typer.Argument(
...,
help="Video to caption: a local file, or a YouTube/media-page URL "
"(the full video is downloaded via yt-dlp).",
"(the full video is downloaded via yt-dlp)",
),
transcript_id: str | None = typer.Option(
None,
"--transcript-id",
"-t",
help="Reuse an existing transcript of this media instead of transcribing it again.",
help="Reuse an existing transcript of this media instead of transcribing it again",
),
chars_per_caption: int | None = typer.Option(
None,
"--chars-per-caption",
min=1,
help="Max characters per caption line.",
help="Max characters per caption line",
),
font_size: int | None = typer.Option(
None,
"--font-size",
min=1,
help="Font size of the burned-in captions (ffmpeg's default styling when omitted).",
help="Font size of the burned-in captions (ffmpeg's default styling when omitted)",
),
out: Path | None = typer.Option(
None, "--out", help="Output file (default: <name>.captioned<ext> next to the input)."
None, "--out", help="Output file (default: <name>.captioned<ext> next to the input)"
),
json_out: bool = options.json_option("Emit JSON describing the captioned file."),
json_out: bool = options.json_option("Emit JSON describing the captioned file"),
) -> None:
"""Burn always-visible captions into a video.
"""Burn always-visible captions into a video

The video is transcribed (or an existing transcript is reused with
--transcript-id), the transcript's SRT captions are fetched, and ffmpeg
Expand Down
24 changes: 12 additions & 12 deletions aai_cli/commands/clip/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,22 +61,22 @@ def clip(
media: str = typer.Argument(
...,
help="Audio/video to cut clips from: a local file, or a YouTube/media-page "
"URL (audio downloaded via yt-dlp).",
"URL (audio downloaded via yt-dlp)",
),
transcript_id: str | None = typer.Option(
None,
"--transcript-id",
"-t",
help="Reuse an existing transcript of this media instead of transcribing it again: "
"an id, or '-' to read an id or 'transcribe --json' output from stdin.",
"an id, or '-' to read an id or 'transcribe --json' output from stdin",
),
speaker: list[str] = typer.Option(
[],
"--speaker",
help="Keep segments spoken by this diarized speaker label (repeatable, e.g. --speaker A).",
help="Keep segments spoken by this diarized speaker label (repeatable, e.g. --speaker A)",
),
search: str | None = typer.Option(
None, "--search", help="Keep segments whose text contains this (case-insensitive)."
None, "--search", help="Keep segments whose text contains this (case-insensitive)"
),
llm_prompt: str | None = typer.Option(
None,
Expand All @@ -88,42 +88,42 @@ def clip(
model: str = typer.Option(
llm.DEFAULT_MODEL,
"--model",
help="LLM Gateway model for --llm.",
help="LLM Gateway model for --llm",
rich_help_panel=help_panels.OPT_LLM,
autocompletion=llm.complete_model,
),
max_tokens: int = typer.Option(
llm.DEFAULT_MAX_TOKENS,
"--max-tokens",
help="Max tokens for the --llm selection reply.",
help="Max tokens for the --llm selection reply",
rich_help_panel=help_panels.OPT_LLM,
),
ranges: list[str] = typer.Option(
[],
"--range",
help="Keep an explicit START-END window (seconds or [HH:]MM:SS; repeatable).",
help="Keep an explicit START-END window (seconds or [HH:]MM:SS; repeatable)",
),
padding: float = typer.Option(
0.0, "--padding", min=0.0, help="Seconds of padding to add around each clip."
0.0, "--padding", min=0.0, help="Seconds of padding to add around each clip"
),
snap: bool = typer.Option(
True,
"--snap/--no-snap",
help="Snap clip boundaries into nearby silence (detected with ffmpeg) so cuts "
"don't land mid-word; --no-snap cuts at the exact selected times.",
"don't land mid-word; --no-snap cuts at the exact selected times",
),
out_dir: Path | None = typer.Option(
None, "--out-dir", help="Directory for the clip files (default: next to the input)."
None, "--out-dir", help="Directory for the clip files (default: next to the input)"
),
video: bool = typer.Option(
False,
"--video",
help="Download the full video (not just the audio track) for a URL source, "
"so the clips are cut from the video. Local files keep their video already.",
),
json_out: bool = options.json_option("Emit JSON describing the clips written."),
json_out: bool = options.json_option("Emit JSON describing the clips written"),
) -> None:
"""Cut clips out of a media file by speaker, text match, LLM pick, or time range.
"""Cut clips from media by speaker, text match, LLM pick, or time range

--speaker and --search select from a diarized transcript (made on the fly,
or reused with --transcript-id); --llm has an LLM Gateway model pick the
Expand Down
16 changes: 8 additions & 8 deletions aai_cli/commands/config_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
)

app = typer.Typer(
help="Inspect and edit persisted CLI settings (profiles, env, telemetry).",
help="Inspect and edit persisted CLI settings (profiles, env, telemetry)",
no_args_is_help=True,
)

Expand Down Expand Up @@ -99,7 +99,7 @@ def path(
ctx: typer.Context,
json_out: bool = options.json_option(),
) -> None:
"""Print where config.toml lives."""
"""Print where config.toml lives"""

def body(_state: AppState, json_mode: bool) -> None:
file = config.config_file_path()
Expand All @@ -126,7 +126,7 @@ def list_settings(
ctx: typer.Context,
json_out: bool = options.json_option(),
) -> None:
"""Show every persisted setting and the stored profiles."""
"""Show every persisted setting and the stored profiles"""

def body(_state: AppState, json_mode: bool) -> None:
data: dict[str, object] = {
Expand Down Expand Up @@ -169,10 +169,10 @@ def render(d: dict[str, object]) -> object:
)
def get(
ctx: typer.Context,
key: ConfigKey = typer.Argument(..., help="Which setting to read."),
key: ConfigKey = typer.Argument(..., help="Which setting to read"),
json_out: bool = options.json_option(),
) -> None:
"""Print one setting's stored value (`env` reads the selected profile's)."""
"""Print one setting's stored value (`env` reads the selected profile's)"""

def body(state: AppState, json_mode: bool) -> None:
value = _current_value(key, state)
Expand All @@ -197,11 +197,11 @@ def body(state: AppState, json_mode: bool) -> None:
)
def set_setting(
ctx: typer.Context,
key: ConfigKey = typer.Argument(..., help="Which setting to change."),
value: str = typer.Argument(..., help="The new value."),
key: ConfigKey = typer.Argument(..., help="Which setting to change"),
value: str = typer.Argument(..., help="The new value"),
json_out: bool = options.json_option(),
) -> None:
"""Change one setting (`env` writes to the selected profile)."""
"""Change one setting (`env` writes to the selected profile)"""

def body(state: AppState, json_mode: bool) -> None:
stored = _store_value(key, value, state)
Expand Down
12 changes: 6 additions & 6 deletions aai_cli/commands/deploy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,14 @@
)
def deploy(
ctx: typer.Context,
prod: bool = typer.Option(False, "--prod", help="Deploy to production (Vercel only)."),
vercel: bool = typer.Option(False, "--vercel", help="Deploy to Vercel (the default)."),
railway: bool = typer.Option(False, "--railway", help="Deploy to Railway."),
fly: bool = typer.Option(False, "--fly", help="Deploy to Fly.io."),
assume_yes: bool = typer.Option(False, "--yes", "-y", help="Skip the confirmation prompt."),
prod: bool = typer.Option(False, "--prod", help="Deploy to production (Vercel only)"),
vercel: bool = typer.Option(False, "--vercel", help="Deploy to Vercel (the default)"),
railway: bool = typer.Option(False, "--railway", help="Deploy to Railway"),
fly: bool = typer.Option(False, "--fly", help="Deploy to Fly.io"),
assume_yes: bool = typer.Option(False, "--yes", "-y", help="Skip the confirmation prompt"),
json_out: bool = options.json_option(),
) -> None:
"""Deploy the current project to Vercel (default), Railway, or Fly.io.
"""Deploy the current project to Vercel, Railway, or Fly.io

Asks for confirmation first, then runs the target's CLI (`vercel deploy`,
`railway up`, or `fly launch`). Requires that target's CLI to be installed.
Expand Down
8 changes: 4 additions & 4 deletions aai_cli/commands/dev/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,19 @@
)
def dev(
ctx: typer.Context,
port: int = typer.Option(3000, "--port", help="Local server port."),
port: int = typer.Option(3000, "--port", help="Local server port"),
host: str = typer.Option(
devserver.LOCAL_HOST,
"--host",
help="Interface to bind. Loopback by default; pass 0.0.0.0 to expose on your network.",
),
no_open: bool = typer.Option(False, "--no-open", help="Launch, but don't open the browser."),
no_open: bool = typer.Option(False, "--no-open", help="Launch, but don't open the browser"),
no_install: bool = typer.Option(
False, "--no-install", help="Skip dependency install; launch directly."
False, "--no-install", help="Skip dependency install; launch directly"
),
json_out: bool = options.json_option(),
) -> None:
"""Launch the dev server for the app in the current directory.
"""Run the dev server for the app in the current directory

Run this from inside a project created by `assembly init`. It installs dependencies
if needed, then starts the FastAPI server with live reload and opens the browser.
Expand Down
16 changes: 8 additions & 8 deletions aai_cli/commands/dictate/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,28 +38,28 @@ def dictate(
None,
"--language",
help="ISO 639-1 language code, or a comma-separated list for "
"code-switching audio (default: en).",
"code-switching audio (default: en)",
),
prompt: str | None = typer.Option(
None,
"--prompt",
help="Custom transcription prompt (overrides --language).",
help="Custom transcription prompt (overrides --language)",
),
word_boost: list[str] | None = typer.Option(
None, "--word-boost", help="Bias recognition toward a term (repeatable)."
None, "--word-boost", help="Bias recognition toward a term (repeatable)"
),
device: int | None = typer.Option(None, "--device", help="Microphone device index."),
once: bool = typer.Option(False, "--once", help="Transcribe one utterance, then exit."),
device: int | None = typer.Option(None, "--device", help="Microphone device index"),
once: bool = typer.Option(False, "--once", help="Transcribe one utterance, then exit"),
max_seconds: float = typer.Option(
float(MAX_AUDIO_SECONDS),
"--max-seconds",
help="Auto-stop a recording after this many seconds.",
help="Auto-stop a recording after this many seconds",
min=1.0,
max=float(MAX_AUDIO_SECONDS),
),
json_out: bool = options.json_option("Emit one JSON object per utterance."),
json_out: bool = options.json_option("Emit one JSON object per utterance"),
) -> None:
"""Dictate with a hotkey: record the mic, get the transcript back instantly.
"""Push-to-talk dictation: record the mic, get the transcript back

Press Enter (or Space) to start recording and press it again to stop; the
utterance is sent to the AssemblyAI Sync API and the transcript prints
Expand Down
Loading
Loading