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
26 changes: 8 additions & 18 deletions .importlinter
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,27 @@ include_external_packages = True
[importlinter:contract:1]
name = Core modules do not import command modules
type = forbidden
; A command's private run-logic now lives inside its own package
; (aai_cli/commands/<cmd>/_exec.py, …) and is governed by contract 2's
; independence rule, so those modules are intentionally absent here. The
; *_exec modules that remain are the ones still at the package root because
; they're shared beyond their command (transcribe_exec/render/batch and
; init_exec are reused by the onboarding wizard; setup_exec/doctor_checks too).
source_modules =
aai_cli.agent
aai_cli.agent_exec
aai_cli.argscan
aai_cli.auth
aai_cli.caption_exec
aai_cli.choices
aai_cli.client
aai_cli.clip_exec
aai_cli.clip_select
aai_cli.code_gen
aai_cli.coding_agent
aai_cli.config
aai_cli.config_builder
aai_cli.context
aai_cli.debuglog
aai_cli.deploy_exec
aai_cli.dev_exec
aai_cli.dictate_exec
aai_cli.doctor_checks
aai_cli.dub_exec
aai_cli.environments
aai_cli.errors
aai_cli.eval_data
aai_cli.eval_hf_api
aai_cli.evaluate_exec
aai_cli.follow
aai_cli.help_panels
aai_cli.help_text
Expand All @@ -39,7 +34,6 @@ source_modules =
aai_cli.init_exec
aai_cli.jsonshape
aai_cli.llm
aai_cli.llm_exec
aai_cli.mediafile
aai_cli.microphone
aai_cli.onboard
Expand All @@ -49,11 +43,8 @@ source_modules =
aai_cli.remotefs
aai_cli.render
aai_cli.setup_exec
aai_cli.share_exec
aai_cli.speak_exec
aai_cli.stdio
aai_cli.steps
aai_cli.stream_exec
aai_cli.streaming
aai_cli.sync_stt
aai_cli.telemetry
Expand All @@ -65,7 +56,6 @@ source_modules =
aai_cli.tts
aai_cli.typer_patches
aai_cli.update_check
aai_cli.webhook_listen
aai_cli.wer
aai_cli.ws
aai_cli.youtube
Expand All @@ -87,13 +77,13 @@ type = forbidden
source_modules =
aai_cli.argscan
aai_cli.client
aai_cli.clip_select
aai_cli.commands.clip._select
aai_cli.commands.evaluate._data
aai_cli.config
aai_cli.config_builder
aai_cli.debuglog
aai_cli.environments
aai_cli.errors
aai_cli.eval_data
aai_cli.hotkey
aai_cli.llm
aai_cli.remotefs
Expand Down
27 changes: 23 additions & 4 deletions aai_cli/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,30 @@ pipe → exit 0).

### Command layer & the registration convention

Each file in `aai_cli/commands/` is a Typer sub-app (`transcribe`, `stream`,
Each entry under `aai_cli/commands/` is a Typer sub-app (`transcribe`, `stream`,
`dictate`, `agent`, `speak`, `llm`, `clip`, `dub`, `caption`, `eval`,
`transcripts`, `login` (login/logout/whoami), `doctor`, `init`, `dev`, `share`,
`deploy`, `setup`, `onboard`, `account` (balance/usage/limits), `keys`,
`sessions`, `audit`, `telemetry` (status/enable/disable), `webhooks` (listen)).

**A command is either a single module *or* a package** — `command_registry`
discovers both (it iterates `pkgutil.iter_modules`, which enumerates packages
too). A simple command stays a flat `commands/<cmd>.py`. A command with private
run-logic becomes a package `commands/<cmd>/`: `__init__.py` holds the Typer
`app` + `SPEC` (and is what gets imported as `aai_cli.commands.<cmd>`), and its
support modules sit beside it **underscore-prefixed** — `_exec.py` for the
`run_<cmd>` body, plus any private helpers (`clip/_select.py`,
`evaluate/_data.py`, `evaluate/_hf_api.py`). The underscore both marks them
private and avoids colliding with the package's own command functions (the
`webhooks` package binds a `listen` command, so its module is `_listen.py`, not
`listen.py`). This is the Prefect/spaCy convention: flat file by default,
promote to a folder only when the command has earned multiple modules. Run-logic
that's **shared beyond one command stays at the package root**, not inside a
command package — `transcribe_exec`/`transcribe_render`/`transcribe_batch` and
`init_exec` are reused by the onboarding wizard (`onboard/sections.py`), so they
live at the root alongside `doctor_checks`/`setup_exec` rather than under
`commands/transcribe/` or `commands/init/`.

**Adding a command is purely additive — no shared file edits.** Every command
module declares a module-level
`SPEC = command_registry.CommandModuleSpec(panel=…, order=…, commands=…)`:
Expand Down Expand Up @@ -61,9 +79,10 @@ command module from another.
function only parses argv into a frozen `<Cmd>Options` dataclass and hands it
to a module-level `run_<cmd>(opts, state, *, json_mode)` through a thin lambda
adapter in `run_command(ctx, ..., json=...)`. The run commands follow it —
`aai_cli/stream_exec.py` (the reference implementation), `transcribe_exec.py`,
`agent_exec.py`, `speak_exec.py`, `llm_exec.py`, `clip_exec.py`,
`dictate_exec.py`. Because the run path is a plain function of data, tests
`commands/stream/_exec.py` (the reference implementation), `transcribe_exec.py`
(at the root — shared with onboarding), `commands/agent/_exec.py`,
`commands/speak/_exec.py`, `commands/llm/_exec.py`, `commands/clip/_exec.py`,
`commands/dictate/_exec.py`. Because the run path is a plain function of data, tests
construct options directly (`dataclasses.replace` off a defaults instance, see
`tests/test_stream_exec.py` and `tests/test_command_options_seam.py`) instead
of round-tripping argv through `CliRunner` — which is also the cheap way to
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@

import typer

from aai_cli import agent_exec, choices, command_registry, help_panels, options, output
from aai_cli import choices, command_registry, help_panels, options, output
from aai_cli.agent.session import DEFAULT_GREETING, DEFAULT_PROMPT
from aai_cli.agent.voices import (
DEFAULT_VOICE,
VOICES,
complete_voice,
format_voice_list,
)
from aai_cli.commands.agent import _exec as agent_exec
from aai_cli.context import AppState, run_command
from aai_cli.help_text import examples_epilog

Expand Down
2 changes: 1 addition & 1 deletion aai_cli/agent_exec.py → aai_cli/commands/agent/_exec.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Run logic for `assembly agent`: the options/run split (see AGENTS.md).

The command module (aai_cli/commands/agent.py) only parses argv — it builds an
The command module (aai_cli/commands/agent/__init__.py) only parses argv — it builds an
``AgentOptions`` and hands it to ``run_agent`` via ``context.run_command``, so tests
can drive validation, --show-code, and session wiring by constructing options
directly, with no CliRunner argv round-trip.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@

import typer

from aai_cli import caption_exec, command_registry, help_panels, options
from aai_cli import command_registry, help_panels, options
from aai_cli.commands.caption import _exec as caption_exec
from aai_cli.context import run_command
from aai_cli.help_text import examples_epilog

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Run logic for `assembly caption`: transcribe → SRT export → ffmpeg burn-in.

The command module (aai_cli/commands/caption.py) only parses argv — it builds a
The command module (aai_cli/commands/caption/__init__.py) only parses argv — it builds a
``CaptionOptions`` and hands it to ``run_caption`` via ``context.run_command``
(the options/run split, see AGENTS.md), so tests drive the whole pipeline by
constructing options directly.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@

import typer

from aai_cli import clip_exec, command_registry, help_panels, llm, options
from aai_cli import command_registry, help_panels, llm, options
from aai_cli.commands.clip import _exec as clip_exec
from aai_cli.context import run_command
from aai_cli.help_text import examples_epilog

Expand Down
7 changes: 4 additions & 3 deletions aai_cli/clip_exec.py → aai_cli/commands/clip/_exec.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Run logic for `assembly clip`: cut a media file by transcript content.

The command module (aai_cli/commands/clip.py) only parses argv — it builds a
The command module (aai_cli/commands/clip/__init__.py) only parses argv — it builds a
``ClipOptions`` and hands it to ``run_clip`` via ``context.run_command`` (the
options/run split, see AGENTS.md), so tests drive transcript resolution and the
ffmpeg orchestration by constructing options directly. The pure selection logic
Expand All @@ -27,8 +27,9 @@

from rich.markup import escape

from aai_cli import clip_select, jsonshape, llm, mediafile, output, stdio, youtube
from aai_cli.clip_select import Segment
from aai_cli import jsonshape, llm, mediafile, output, stdio, youtube
from aai_cli.commands.clip import _select as clip_select
from aai_cli.commands.clip._select import Segment
from aai_cli.context import AppState
from aai_cli.errors import CLIError, UsageError

Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# aai_cli/commands/deploy.py
# aai_cli/commands/deploy/__init__.py
from __future__ import annotations

import typer

from aai_cli import command_registry, deploy_exec, help_panels, options
from aai_cli import command_registry, help_panels, options
from aai_cli.commands.deploy import _exec as deploy_exec
from aai_cli.context import run_command
from aai_cli.help_text import examples_epilog

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Run logic for `assembly deploy`: ship the current project to a PaaS target.

The command module (aai_cli/commands/deploy.py) only parses argv — it builds a
The command module (aai_cli/commands/deploy/__init__.py) only parses argv — it builds a
``DeployOptions`` and hands it to ``run_deploy`` via ``context.run_command`` (the
options/run split, see AGENTS.md), so tests drive target resolution and the
subprocess orchestration by constructing options directly instead of
Expand Down
5 changes: 3 additions & 2 deletions aai_cli/commands/dev.py → aai_cli/commands/dev/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# aai_cli/commands/dev.py
# aai_cli/commands/dev/__init__.py
from __future__ import annotations

import typer

from aai_cli import command_registry, dev_exec, help_panels, options
from aai_cli import command_registry, help_panels, options
from aai_cli.commands.dev import _exec as dev_exec
from aai_cli.context import run_command
from aai_cli.help_text import examples_epilog
from aai_cli.init import devserver
Expand Down
2 changes: 1 addition & 1 deletion aai_cli/dev_exec.py → aai_cli/commands/dev/_exec.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Run logic for `assembly dev`: boot the scaffolded project's dev server.

The command module (aai_cli/commands/dev.py) only parses argv — it builds a
The command module (aai_cli/commands/dev/__init__.py) only parses argv — it builds a
``DevOptions`` and hands it to ``run_dev`` via ``context.run_command`` (the
options/run split, see AGENTS.md), so tests drive the server orchestration by
constructing options directly instead of round-tripping argv.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

import typer

from aai_cli import command_registry, dictate_exec, help_panels, options
from aai_cli import command_registry, help_panels, options
from aai_cli.commands.dictate import _exec as dictate_exec
from aai_cli.context import run_command
from aai_cli.help_text import examples_epilog
from aai_cli.sync_stt import MAX_AUDIO_SECONDS
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Push-to-talk dictation over the Sync STT API: wait for a hotkey, record the
microphone until the hotkey is pressed again (or the duration cap), POST the
utterance to the Sync API, print the transcript, repeat. The command module
(aai_cli/commands/dictate.py) only parses argv into a ``DictateOptions``; tests
(aai_cli/commands/dictate/__init__.py) only parses argv into a ``DictateOptions``; tests
drive the session by constructing options directly and injecting the key/mic/
HTTP boundaries, with no CliRunner argv round-trip and no real terminal.
"""
Expand Down
3 changes: 2 additions & 1 deletion aai_cli/commands/dub.py → aai_cli/commands/dub/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@

import typer

from aai_cli import command_registry, dub_exec, help_panels, llm, options
from aai_cli import command_registry, help_panels, llm, options
from aai_cli.commands.dub import _exec as dub_exec
from aai_cli.context import run_command
from aai_cli.help_text import examples_epilog

Expand Down
2 changes: 1 addition & 1 deletion aai_cli/dub_exec.py → aai_cli/commands/dub/_exec.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Run logic for `assembly dub`: transcribe → translate → synthesize → ffmpeg track-swap.

The command module (aai_cli/commands/dub.py) only parses argv — it builds a
The command module (aai_cli/commands/dub/__init__.py) only parses argv — it builds a
``DubOptions`` and hands it to ``run_dub`` via ``context.run_command`` (the
options/run split, see AGENTS.md), so tests drive the whole pipeline by
constructing options directly.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@

The module is named ``evaluate`` because importing a module named ``eval`` would
shadow the builtin; the command itself registers as ``eval``. The scoring/render
logic lives in ``aai_cli.evaluate_exec`` (the options/run split, see AGENTS.md).
logic lives in ``aai_cli.commands.evaluate._exec`` (the options/run split, see AGENTS.md).
"""

from __future__ import annotations

import typer

from aai_cli import command_registry, evaluate_exec, help_panels, options
from aai_cli import command_registry, help_panels, options
from aai_cli.commands.evaluate import _exec as evaluate_exec
from aai_cli.commands.evaluate._exec import EvalSpeechModel
from aai_cli.context import run_command
from aai_cli.evaluate_exec import EvalSpeechModel
from aai_cli.help_text import examples_epilog

app = typer.Typer()
Expand Down
3 changes: 2 additions & 1 deletion aai_cli/eval_data.py → aai_cli/commands/evaluate/_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
from dataclasses import dataclass
from pathlib import Path

from aai_cli import eval_hf_api, jsonshape, wer
from aai_cli import jsonshape, wer
from aai_cli.commands.evaluate import _hf_api as eval_hf_api
from aai_cli.errors import APIError, CLIError, UsageError

_MANIFEST_SUFFIXES = (".csv", ".jsonl")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Run logic for `assembly eval`: transcribe a dataset and score WER against references.

The command module (aai_cli/commands/evaluate.py) only parses argv — it builds an
The command module (aai_cli/commands/evaluate/__init__.py) only parses argv — it builds an
``EvalOptions`` and hands it to ``run_evaluate`` via ``context.run_command`` (the
options/run split, see AGENTS.md), so tests drive the scoring and rendering by
constructing options directly instead of round-tripping argv.
Expand All @@ -19,7 +19,8 @@
import assemblyai as aai
from rich.console import RenderableType

from aai_cli import client, eval_data, jsonshape, output, wer
from aai_cli import client, jsonshape, output, wer
from aai_cli.commands.evaluate import _data as eval_data
from aai_cli.context import AppState
from aai_cli.errors import CLIError, NotAuthenticated

Expand Down
File renamed without changes.
3 changes: 2 additions & 1 deletion aai_cli/commands/llm.py → aai_cli/commands/llm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

import typer

from aai_cli import choices, command_registry, help_panels, llm_exec, options, output
from aai_cli import choices, command_registry, help_panels, options, output
from aai_cli import llm as gateway
from aai_cli.commands.llm import _exec as llm_exec
from aai_cli.context import run_command
from aai_cli.errors import UsageError
from aai_cli.help_text import examples_epilog
Expand Down
2 changes: 1 addition & 1 deletion aai_cli/llm_exec.py → aai_cli/commands/llm/_exec.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Run logic for `assembly llm`: the options/run split (see AGENTS.md).

The command module (aai_cli/commands/llm.py) only parses argv — it builds an
The command module (aai_cli/commands/llm/__init__.py) only parses argv — it builds an
``LlmOptions`` and hands it to ``run_llm`` via ``context.run_command``, so tests can
drive one-shot and --follow behavior by constructing options directly, with no
CliRunner argv round-trip. (``aai_cli/llm.py`` is the gateway client itself and is
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# aai_cli/commands/share.py
# aai_cli/commands/share/__init__.py
from __future__ import annotations

import typer

from aai_cli import command_registry, help_panels, options, share_exec
from aai_cli import command_registry, help_panels, options
from aai_cli.commands.share import _exec as share_exec
from aai_cli.context import run_command
from aai_cli.help_text import examples_epilog

Expand Down
2 changes: 1 addition & 1 deletion aai_cli/share_exec.py → aai_cli/commands/share/_exec.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Run logic for `assembly share`: expose the local dev server on a public URL.

The command module (aai_cli/commands/share.py) only parses argv — it builds a
The command module (aai_cli/commands/share/__init__.py) only parses argv — it builds a
``ShareOptions`` and hands it to ``run_share`` via ``context.run_command`` (the
options/run split, see AGENTS.md), so tests drive the tunnel orchestration by
constructing options directly instead of round-tripping argv.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@

import typer

from aai_cli import command_registry, help_panels, options, speak_exec
from aai_cli import command_registry, help_panels, options
from aai_cli.commands.speak import _exec as speak_exec
from aai_cli.commands.speak._exec import DEFAULT_LANGUAGE
from aai_cli.context import run_command
from aai_cli.help_text import examples_epilog
from aai_cli.speak_exec import DEFAULT_LANGUAGE

app = typer.Typer()

Expand Down
2 changes: 1 addition & 1 deletion aai_cli/speak_exec.py → aai_cli/commands/speak/_exec.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Run logic for `assembly speak`: the options/run split (see AGENTS.md).

The command module (aai_cli/commands/speak.py) only parses argv — it builds a
The command module (aai_cli/commands/speak/__init__.py) only parses argv — it builds a
``SpeakOptions`` and hands it to ``run_speak`` via ``context.run_command``, so tests
can drive text resolution, voice assignment, and synthesis wiring by constructing
options directly, with no CliRunner argv round-trip.
Expand Down
Loading
Loading