diff --git a/AGENTS.md b/AGENTS.md index 5224bffe..69b529b6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. diff --git a/aai_cli/commands/account.py b/aai_cli/commands/account.py index 42028b82..92c9a3d4 100644 --- a/aai_cli/commands/account.py +++ b/aai_cli/commands/account.py @@ -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, @@ -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() @@ -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, @@ -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() diff --git a/aai_cli/commands/agent/__init__.py b/aai_cli/commands/agent/__init__.py index 6a92cffb..3112f57c 100644 --- a/aai_cli/commands/agent/__init__.py +++ b/aai_cli/commands/agent/__init__.py @@ -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, @@ -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 diff --git a/aai_cli/commands/audit.py b/aai_cli/commands/audit.py index 2981506d..454a3d57 100644 --- a/aai_cli/commands/audit.py +++ b/aai_cli/commands/audit.py @@ -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, @@ -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() diff --git a/aai_cli/commands/caption/__init__.py b/aai_cli/commands/caption/__init__.py index 581148fb..8c918ad2 100644 --- a/aai_cli/commands/caption/__init__.py +++ b/aai_cli/commands/caption/__init__.py @@ -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: .captioned next to the input)." + None, "--out", help="Output file (default: .captioned 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 diff --git a/aai_cli/commands/clip/__init__.py b/aai_cli/commands/clip/__init__.py index 934c8613..064b4d9d 100644 --- a/aai_cli/commands/clip/__init__.py +++ b/aai_cli/commands/clip/__init__.py @@ -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, @@ -88,32 +88,32 @@ 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, @@ -121,9 +121,9 @@ def clip( 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 diff --git a/aai_cli/commands/config_cmd.py b/aai_cli/commands/config_cmd.py index bb2765f7..5e220f93 100644 --- a/aai_cli/commands/config_cmd.py +++ b/aai_cli/commands/config_cmd.py @@ -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, ) @@ -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() @@ -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] = { @@ -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) @@ -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) diff --git a/aai_cli/commands/deploy/__init__.py b/aai_cli/commands/deploy/__init__.py index e8078d12..02a1ea4e 100644 --- a/aai_cli/commands/deploy/__init__.py +++ b/aai_cli/commands/deploy/__init__.py @@ -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. diff --git a/aai_cli/commands/dev/__init__.py b/aai_cli/commands/dev/__init__.py index 6446d542..30ddae6c 100644 --- a/aai_cli/commands/dev/__init__.py +++ b/aai_cli/commands/dev/__init__.py @@ -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. diff --git a/aai_cli/commands/dictate/__init__.py b/aai_cli/commands/dictate/__init__.py index 5e55dd46..ed49b6dc 100644 --- a/aai_cli/commands/dictate/__init__.py +++ b/aai_cli/commands/dictate/__init__.py @@ -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 diff --git a/aai_cli/commands/doctor.py b/aai_cli/commands/doctor.py index ebd334d8..b2a1aab3 100644 --- a/aai_cli/commands/doctor.py +++ b/aai_cli/commands/doctor.py @@ -28,7 +28,7 @@ def doctor( ctx: typer.Context, json_out: bool = options.json_option(), ) -> None: - """Check that your environment is ready to use AssemblyAI.""" + """Check that your environment is ready for AssemblyAI""" def body(state: AppState, json_mode: bool) -> None: profile = state.resolve_profile() diff --git a/aai_cli/commands/dub/__init__.py b/aai_cli/commands/dub/__init__.py index 8a298ba7..713d18da 100644 --- a/aai_cli/commands/dub/__init__.py +++ b/aai_cli/commands/dub/__init__.py @@ -58,13 +58,13 @@ def dub( media: str = typer.Argument( ..., help="Audio/video to dub: a local file (the video stream is copied untouched), " - "or a YouTube/media-page URL (downloaded via yt-dlp).", + "or a YouTube/media-page URL (downloaded via yt-dlp)", ), lang: str = typer.Option( ..., "--lang", "-l", - help="Target language: an ISO code (de, fr, es, …) or a language name (German).", + help="Target language: an ISO code (de, fr, es, …) or a language name (German)", ), source_lang: str | None = typer.Option( None, @@ -75,8 +75,7 @@ def dub( None, "--transcript-id", "-t", - help="Reuse an existing diarized transcript of this media instead of " - "transcribing it again.", + help="Reuse an existing diarized transcript of this media instead of transcribing it again", ), voice: list[str] = typer.Option( [], @@ -88,18 +87,18 @@ def dub( model: str = typer.Option( llm.DEFAULT_MODEL, "--model", - help="LLM Gateway model that translates the utterances.", + help="LLM Gateway model that translates the utterances", 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 per utterance translation.", + help="Max tokens per utterance translation", rich_help_panel=help_panels.OPT_LLM, ), out: Path | None = typer.Option( - None, "--out", help="Output file (default: .dub. next to the input)." + None, "--out", help="Output file (default: .dub. next to the input)" ), video: bool = typer.Option( False, @@ -112,11 +111,11 @@ def dub( "--download-sections", help="For a URL source, download (and dub) only part of it (yt-dlp " '"--download-sections" syntax, e.g. "*0:00-15:00" for the first fifteen ' - "minutes; repeatable).", + "minutes; repeatable)", ), - json_out: bool = options.json_option("Emit JSON describing the dubbed file."), + json_out: bool = options.json_option("Emit JSON describing the dubbed file"), ) -> None: - r"""\[sandbox] Dub a video or audio file into another language. + r"""\[sandbox] Dub a video or audio file into another language The whole platform in one command: the media is transcribed with diarized utterance timestamps, each utterance is translated by an LLM Gateway model, diff --git a/aai_cli/commands/evaluate/__init__.py b/aai_cli/commands/evaluate/__init__.py index 94ee3562..00ce36d1 100644 --- a/aai_cli/commands/evaluate/__init__.py +++ b/aai_cli/commands/evaluate/__init__.py @@ -52,36 +52,36 @@ def evaluate( ctx: typer.Context, dataset: str = typer.Argument( ..., - help="Hugging Face dataset id, or a local .csv/.jsonl manifest with audio + text columns.", + help="Hugging Face dataset id, or a local .csv/.jsonl manifest with audio + text columns", ), split: str | None = typer.Option( - None, "--split", help="Hugging Face split to score (default: test)." + None, "--split", help="Hugging Face split to score (default: test)" ), subset: str | None = typer.Option( - None, "--subset", help="Hugging Face config/subset name (e.g. a language)." + None, "--subset", help="Hugging Face config/subset name (e.g. a language)" ), - limit: int = typer.Option(10, "--limit", min=1, max=100, help="Rows to evaluate (1-100)."), + limit: int = typer.Option(10, "--limit", min=1, max=100, help="Rows to evaluate (1-100)"), audio_column: str | None = typer.Option( - None, "--audio-column", help="Audio column name (default: auto-detect)." + None, "--audio-column", help="Audio column name (default: auto-detect)" ), text_column: str | None = typer.Option( - None, "--text-column", help="Reference text column name (default: auto-detect)." + None, "--text-column", help="Reference text column name (default: auto-detect)" ), speech_model: EvalSpeechModel | None = typer.Option( - None, "--speech-model", help="Speech model to evaluate." + None, "--speech-model", help="Speech model to evaluate" ), language_code: str | None = typer.Option( - None, "--language-code", help="Force a language (e.g. en_us)." + None, "--language-code", help="Force a language (e.g. en_us)" ), concurrency: int = typer.Option( 1, "--concurrency", min=1, - help="How many items to transcribe at once (sequential by default).", + help="How many items to transcribe at once (sequential by default)", ), - json_out: bool = options.json_option("Output the rows and summary as one JSON object."), + json_out: bool = options.json_option("Output the rows and summary as one JSON object"), ) -> None: - """Transcribe an evaluation dataset and score WER against its reference texts. + """Transcribe a dataset and score WER against its reference texts Each row's audio is transcribed, then scored against the row's reference text; both are normalized first (lowercased, punctuation stripped) so style diff --git a/aai_cli/commands/init.py b/aai_cli/commands/init.py index 2cbb3795..f7fc972c 100644 --- a/aai_cli/commands/init.py +++ b/aai_cli/commands/init.py @@ -46,29 +46,29 @@ def init( # that actually ship. help=( f"Template to scaffold: {', '.join(templates.TEMPLATE_ORDER)} " - "(omit to pick interactively)." + "(omit to pick interactively)" ), ), - directory: str | None = typer.Argument(None, help="Target directory (default: