Skip to content

Commit 23a01e8

Browse files
alexkromanclaude
andauthored
Refactor CLI help copy to terse, period-less Codex-CLI style (#154)
Rewrites every user-facing help string to the convention the Codex CLI uses: terse, imperative, sentence-case one-liners with no trailing period. Genuinely multi-sentence help (e.g. "X. Default: Y.") keeps normal punctuation. - Command summaries (root --help + each command's --help) tightened and de-periodised: e.g. "Transcribe a file, URL, or YouTube/podcast link — or a whole batch", "Hold a live two-way voice conversation with a voice agent", "Push-to-talk dictation: record the mic, get the transcript back". - ~180 option/argument help strings dropped their trailing period across every command, plus the shared option factories and root flags. - Only public command docstrings were touched (snapshot-covered); internal helper docstrings and CLIError messages are unchanged. - Regenerated the six --help snapshot goldens; updated two unit tests that pinned the old copy. - Added bare-invocation tests for sessions/transcripts/webhooks so the no_args_is_help=True flag (now sharing a changed line with the help string) stays covered by the mutation gate. - Recorded the help-copy convention in AGENTS.md. https://claude.ai/code/session_0172UnMSRedHhDjc1MdKpig6 Co-authored-by: Claude <noreply@anthropic.com>
1 parent f7dd4d5 commit 23a01e8

41 files changed

Lines changed: 634 additions & 621 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ Lessons that cost time in agent sessions — read before exercising `uv run asse
110110
- 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).
111111
- mypy is strict on `aai_cli` (`disallow_untyped_defs`); tests are type-checked but exempt from return annotations.
112112
- Errors → stderr, data → stdout. Preserve this split; it's what makes the CLI pipeline-safe.
113+
- **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).
113114
- **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.
114115
- **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.
115116
- **Every NDJSON stream line carries a `"type"` field** (see REFERENCE.md "JSON output"); new event types are additive, existing fields stay stable.

aai_cli/commands/account.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ class _Usage(BaseModel):
125125
usage_items: Annotated[list[_Window], _MappingList] = Field(default_factory=list[_Window])
126126

127127

128-
app = typer.Typer(help="Account billing, usage, and limits.")
128+
app = typer.Typer(help="Account billing, usage, and limits")
129129

130130
SPEC = command_registry.CommandModuleSpec(
131131
panel=help_panels.ACCOUNT,
@@ -147,7 +147,7 @@ def balance(
147147
ctx: typer.Context,
148148
json_out: bool = options.json_option(),
149149
) -> None:
150-
"""Show your remaining account balance."""
150+
"""Show your remaining account balance"""
151151

152152
def body(state: AppState, json_mode: bool) -> None:
153153
_, jwt = state.resolve_session()
@@ -183,17 +183,17 @@ def usage(
183183
),
184184
end: str | None = typer.Option(None, "--end", help="End date (YYYY-MM-DD). Default: today."),
185185
window: str | None = typer.Option(
186-
None, "--window", help="Window size: 'day', 'week', or 'month'."
186+
None, "--window", help="Window size: 'day', 'week', or 'month'"
187187
),
188188
include_zero: bool = typer.Option(
189189
False,
190190
"--include-zero",
191191
"--all",
192-
help="Include zero-usage windows (matches --include-logins on `assembly audit`).",
192+
help="Include zero-usage windows (matches --include-logins on `assembly audit`)",
193193
),
194194
json_out: bool = options.json_option(),
195195
) -> None:
196-
"""Show usage over a date range (defaults to the last 30 days)."""
196+
"""Show usage over a date range (default: last 30 days)"""
197197

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

275275
def body(state: AppState, json_mode: bool) -> None:
276276
account_id, jwt = state.resolve_session()

aai_cli/commands/agent/__init__.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ def agent(
5353
None, help="Audio file path or URL to speak to the agent. Omit to use the microphone."
5454
),
5555
sample: bool = typer.Option(
56-
False, "--sample", help="Speak the hosted wildfires.mp3 sample to the agent."
56+
False, "--sample", help="Speak the hosted wildfires.mp3 sample to the agent"
5757
),
5858
voice: str = typer.Option(
5959
DEFAULT_VOICE,
@@ -62,32 +62,32 @@ def agent(
6262
autocompletion=complete_voice,
6363
),
6464
system_prompt: str = typer.Option(
65-
DEFAULT_PROMPT, "--system-prompt", help="System prompt (the agent's persona)."
65+
DEFAULT_PROMPT, "--system-prompt", help="System prompt (the agent's persona)"
6666
),
6767
system_prompt_file: Path | None = typer.Option(
6868
None,
6969
"--system-prompt-file",
70-
help="Read the system prompt from a file (overrides --system-prompt).",
70+
help="Read the system prompt from a file (overrides --system-prompt)",
7171
exists=True,
7272
dir_okay=False,
7373
),
74-
greeting: str = typer.Option(DEFAULT_GREETING, "--greeting", help="Spoken greeting."),
75-
device: int | None = typer.Option(None, "--device", help="Microphone device index."),
76-
list_voices: bool = typer.Option(False, "--list-voices", help="Print known voices and exit."),
77-
json_out: bool = options.json_option("Emit newline-delimited JSON events."),
74+
greeting: str = typer.Option(DEFAULT_GREETING, "--greeting", help="Spoken greeting"),
75+
device: int | None = typer.Option(None, "--device", help="Microphone device index"),
76+
list_voices: bool = typer.Option(False, "--list-voices", help="Print known voices and exit"),
77+
json_out: bool = options.json_option("Emit newline-delimited JSON events"),
7878
output_field: choices.TextOrJson | None = typer.Option(
7979
None,
8080
"-o",
8181
"--output",
82-
help="Output mode: text (you:/agent: lines as plain stdout, pipe-friendly) or json.",
82+
help="Output mode: text (you:/agent: lines as plain stdout, pipe-friendly) or json",
8383
),
8484
show_code: bool = typer.Option(
8585
False,
8686
"--show-code",
87-
help="Print the equivalent Python SDK code and exit (does not start a session).",
87+
help="Print the equivalent Python SDK code and exit (does not start a session)",
8888
),
8989
) -> None:
90-
"""Have a live two-way voice conversation with an AssemblyAI voice agent.
90+
"""Hold a live two-way voice conversation with a voice agent
9191
9292
Use headphones: the mic stays open while the agent speaks, so on
9393
speakers it would hear itself and loop. Pass an audio file/URL (or

aai_cli/commands/audit.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from aai_cli.context import AppState, run_command
1111
from aai_cli.help_text import examples_epilog
1212

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

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

106106
def body(state: AppState, json_mode: bool) -> None:
107107
_, jwt = state.resolve_session()

aai_cli/commands/caption/__init__.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,32 +44,32 @@ def caption(
4444
media: str = typer.Argument(
4545
...,
4646
help="Video to caption: a local file, or a YouTube/media-page URL "
47-
"(the full video is downloaded via yt-dlp).",
47+
"(the full video is downloaded via yt-dlp)",
4848
),
4949
transcript_id: str | None = typer.Option(
5050
None,
5151
"--transcript-id",
5252
"-t",
53-
help="Reuse an existing transcript of this media instead of transcribing it again.",
53+
help="Reuse an existing transcript of this media instead of transcribing it again",
5454
),
5555
chars_per_caption: int | None = typer.Option(
5656
None,
5757
"--chars-per-caption",
5858
min=1,
59-
help="Max characters per caption line.",
59+
help="Max characters per caption line",
6060
),
6161
font_size: int | None = typer.Option(
6262
None,
6363
"--font-size",
6464
min=1,
65-
help="Font size of the burned-in captions (ffmpeg's default styling when omitted).",
65+
help="Font size of the burned-in captions (ffmpeg's default styling when omitted)",
6666
),
6767
out: Path | None = typer.Option(
68-
None, "--out", help="Output file (default: <name>.captioned<ext> next to the input)."
68+
None, "--out", help="Output file (default: <name>.captioned<ext> next to the input)"
6969
),
70-
json_out: bool = options.json_option("Emit JSON describing the captioned file."),
70+
json_out: bool = options.json_option("Emit JSON describing the captioned file"),
7171
) -> None:
72-
"""Burn always-visible captions into a video.
72+
"""Burn always-visible captions into a video
7373
7474
The video is transcribed (or an existing transcript is reused with
7575
--transcript-id), the transcript's SRT captions are fetched, and ffmpeg

aai_cli/commands/clip/__init__.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -61,22 +61,22 @@ def clip(
6161
media: str = typer.Argument(
6262
...,
6363
help="Audio/video to cut clips from: a local file, or a YouTube/media-page "
64-
"URL (audio downloaded via yt-dlp).",
64+
"URL (audio downloaded via yt-dlp)",
6565
),
6666
transcript_id: str | None = typer.Option(
6767
None,
6868
"--transcript-id",
6969
"-t",
7070
help="Reuse an existing transcript of this media instead of transcribing it again: "
71-
"an id, or '-' to read an id or 'transcribe --json' output from stdin.",
71+
"an id, or '-' to read an id or 'transcribe --json' output from stdin",
7272
),
7373
speaker: list[str] = typer.Option(
7474
[],
7575
"--speaker",
76-
help="Keep segments spoken by this diarized speaker label (repeatable, e.g. --speaker A).",
76+
help="Keep segments spoken by this diarized speaker label (repeatable, e.g. --speaker A)",
7777
),
7878
search: str | None = typer.Option(
79-
None, "--search", help="Keep segments whose text contains this (case-insensitive)."
79+
None, "--search", help="Keep segments whose text contains this (case-insensitive)"
8080
),
8181
llm_prompt: str | None = typer.Option(
8282
None,
@@ -88,42 +88,42 @@ def clip(
8888
model: str = typer.Option(
8989
llm.DEFAULT_MODEL,
9090
"--model",
91-
help="LLM Gateway model for --llm.",
91+
help="LLM Gateway model for --llm",
9292
rich_help_panel=help_panels.OPT_LLM,
9393
autocompletion=llm.complete_model,
9494
),
9595
max_tokens: int = typer.Option(
9696
llm.DEFAULT_MAX_TOKENS,
9797
"--max-tokens",
98-
help="Max tokens for the --llm selection reply.",
98+
help="Max tokens for the --llm selection reply",
9999
rich_help_panel=help_panels.OPT_LLM,
100100
),
101101
ranges: list[str] = typer.Option(
102102
[],
103103
"--range",
104-
help="Keep an explicit START-END window (seconds or [HH:]MM:SS; repeatable).",
104+
help="Keep an explicit START-END window (seconds or [HH:]MM:SS; repeatable)",
105105
),
106106
padding: float = typer.Option(
107-
0.0, "--padding", min=0.0, help="Seconds of padding to add around each clip."
107+
0.0, "--padding", min=0.0, help="Seconds of padding to add around each clip"
108108
),
109109
snap: bool = typer.Option(
110110
True,
111111
"--snap/--no-snap",
112112
help="Snap clip boundaries into nearby silence (detected with ffmpeg) so cuts "
113-
"don't land mid-word; --no-snap cuts at the exact selected times.",
113+
"don't land mid-word; --no-snap cuts at the exact selected times",
114114
),
115115
out_dir: Path | None = typer.Option(
116-
None, "--out-dir", help="Directory for the clip files (default: next to the input)."
116+
None, "--out-dir", help="Directory for the clip files (default: next to the input)"
117117
),
118118
video: bool = typer.Option(
119119
False,
120120
"--video",
121121
help="Download the full video (not just the audio track) for a URL source, "
122122
"so the clips are cut from the video. Local files keep their video already.",
123123
),
124-
json_out: bool = options.json_option("Emit JSON describing the clips written."),
124+
json_out: bool = options.json_option("Emit JSON describing the clips written"),
125125
) -> None:
126-
"""Cut clips out of a media file by speaker, text match, LLM pick, or time range.
126+
"""Cut clips from media by speaker, text match, LLM pick, or time range
127127
128128
--speaker and --search select from a diarized transcript (made on the fly,
129129
or reused with --transcript-id); --llm has an LLM Gateway model pick the

aai_cli/commands/config_cmd.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
)
2727

2828
app = typer.Typer(
29-
help="Inspect and edit persisted CLI settings (profiles, env, telemetry).",
29+
help="Inspect and edit persisted CLI settings (profiles, env, telemetry)",
3030
no_args_is_help=True,
3131
)
3232

@@ -99,7 +99,7 @@ def path(
9999
ctx: typer.Context,
100100
json_out: bool = options.json_option(),
101101
) -> None:
102-
"""Print where config.toml lives."""
102+
"""Print where config.toml lives"""
103103

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

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

177177
def body(state: AppState, json_mode: bool) -> None:
178178
value = _current_value(key, state)
@@ -197,11 +197,11 @@ def body(state: AppState, json_mode: bool) -> None:
197197
)
198198
def set_setting(
199199
ctx: typer.Context,
200-
key: ConfigKey = typer.Argument(..., help="Which setting to change."),
201-
value: str = typer.Argument(..., help="The new value."),
200+
key: ConfigKey = typer.Argument(..., help="Which setting to change"),
201+
value: str = typer.Argument(..., help="The new value"),
202202
json_out: bool = options.json_option(),
203203
) -> None:
204-
"""Change one setting (`env` writes to the selected profile)."""
204+
"""Change one setting (`env` writes to the selected profile)"""
205205

206206
def body(state: AppState, json_mode: bool) -> None:
207207
stored = _store_value(key, value, state)

aai_cli/commands/deploy/__init__.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,14 @@
3131
)
3232
def deploy(
3333
ctx: typer.Context,
34-
prod: bool = typer.Option(False, "--prod", help="Deploy to production (Vercel only)."),
35-
vercel: bool = typer.Option(False, "--vercel", help="Deploy to Vercel (the default)."),
36-
railway: bool = typer.Option(False, "--railway", help="Deploy to Railway."),
37-
fly: bool = typer.Option(False, "--fly", help="Deploy to Fly.io."),
38-
assume_yes: bool = typer.Option(False, "--yes", "-y", help="Skip the confirmation prompt."),
34+
prod: bool = typer.Option(False, "--prod", help="Deploy to production (Vercel only)"),
35+
vercel: bool = typer.Option(False, "--vercel", help="Deploy to Vercel (the default)"),
36+
railway: bool = typer.Option(False, "--railway", help="Deploy to Railway"),
37+
fly: bool = typer.Option(False, "--fly", help="Deploy to Fly.io"),
38+
assume_yes: bool = typer.Option(False, "--yes", "-y", help="Skip the confirmation prompt"),
3939
json_out: bool = options.json_option(),
4040
) -> None:
41-
"""Deploy the current project to Vercel (default), Railway, or Fly.io.
41+
"""Deploy the current project to Vercel, Railway, or Fly.io
4242
4343
Asks for confirmation first, then runs the target's CLI (`vercel deploy`,
4444
`railway up`, or `fly launch`). Requires that target's CLI to be installed.

aai_cli/commands/dev/__init__.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,19 +33,19 @@
3333
)
3434
def dev(
3535
ctx: typer.Context,
36-
port: int = typer.Option(3000, "--port", help="Local server port."),
36+
port: int = typer.Option(3000, "--port", help="Local server port"),
3737
host: str = typer.Option(
3838
devserver.LOCAL_HOST,
3939
"--host",
4040
help="Interface to bind. Loopback by default; pass 0.0.0.0 to expose on your network.",
4141
),
42-
no_open: bool = typer.Option(False, "--no-open", help="Launch, but don't open the browser."),
42+
no_open: bool = typer.Option(False, "--no-open", help="Launch, but don't open the browser"),
4343
no_install: bool = typer.Option(
44-
False, "--no-install", help="Skip dependency install; launch directly."
44+
False, "--no-install", help="Skip dependency install; launch directly"
4545
),
4646
json_out: bool = options.json_option(),
4747
) -> None:
48-
"""Launch the dev server for the app in the current directory.
48+
"""Run the dev server for the app in the current directory
4949
5050
Run this from inside a project created by `assembly init`. It installs dependencies
5151
if needed, then starts the FastAPI server with live reload and opens the browser.

aai_cli/commands/dictate/__init__.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,28 +38,28 @@ def dictate(
3838
None,
3939
"--language",
4040
help="ISO 639-1 language code, or a comma-separated list for "
41-
"code-switching audio (default: en).",
41+
"code-switching audio (default: en)",
4242
),
4343
prompt: str | None = typer.Option(
4444
None,
4545
"--prompt",
46-
help="Custom transcription prompt (overrides --language).",
46+
help="Custom transcription prompt (overrides --language)",
4747
),
4848
word_boost: list[str] | None = typer.Option(
49-
None, "--word-boost", help="Bias recognition toward a term (repeatable)."
49+
None, "--word-boost", help="Bias recognition toward a term (repeatable)"
5050
),
51-
device: int | None = typer.Option(None, "--device", help="Microphone device index."),
52-
once: bool = typer.Option(False, "--once", help="Transcribe one utterance, then exit."),
51+
device: int | None = typer.Option(None, "--device", help="Microphone device index"),
52+
once: bool = typer.Option(False, "--once", help="Transcribe one utterance, then exit"),
5353
max_seconds: float = typer.Option(
5454
float(MAX_AUDIO_SECONDS),
5555
"--max-seconds",
56-
help="Auto-stop a recording after this many seconds.",
56+
help="Auto-stop a recording after this many seconds",
5757
min=1.0,
5858
max=float(MAX_AUDIO_SECONDS),
5959
),
60-
json_out: bool = options.json_option("Emit one JSON object per utterance."),
60+
json_out: bool = options.json_option("Emit one JSON object per utterance"),
6161
) -> None:
62-
"""Dictate with a hotkey: record the mic, get the transcript back instantly.
62+
"""Push-to-talk dictation: record the mic, get the transcript back
6363
6464
Press Enter (or Space) to start recording and press it again to stop; the
6565
utterance is sent to the AssemblyAI Sync API and the transcript prints

0 commit comments

Comments
 (0)