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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ htmlcov/

# Local scratch scripts (often contain live keys)
transcribe/
# But do track the packaged template
# But do track the packaged template and the transcribe orchestration subpackage
!aai_cli/init/templates/transcribe/
!aai_cli/app/transcribe/

# Wrong tool for this project (hatchling + uv); never commit a poetry lock
poetry.lock
Expand Down
103 changes: 35 additions & 68 deletions .importlinter
Original file line number Diff line number Diff line change
Expand Up @@ -3,94 +3,61 @@ root_package = aai_cli
include_external_packages = True

[importlinter:contract:1]
name = Core modules do not import command modules
name = Layered architecture
type = layers
; The flat root pile of library modules is gone: each layer is now a package,
; so intra-layer imports stay free and only the *direction* between layers is
; enforced. Higher may import lower, never the reverse. This single declarative
; contract replaces the old hand-maintained 47-module `forbidden` list (which
; the comments admitted had "silently drifted"). The CLI framework glue that
; assembles the command layer — main, command_registry, help_panels, options —
; stays at the package root, above `commands`, and is intentionally unlisted
; (it legitimately imports the command modules to discover/register them).
; Feature slices (agent, tts, streaming, code_gen, init, auth, onboard) are
; likewise unlisted vertical slices governed by contract 2.
layers =
commands
app
ui
core
containers =
aai_cli

[importlinter:contract:2]
name = Feature slices do not import commands
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).
; The vertical feature slices sit beside the layered stack (they internally mix
; protocol + rendering, so they aren't a single horizontal layer). They must
; never reach up into the command layer. Adding a feature slice is rare and
; deliberate, so this short list does not drift the way the old core list did.
source_modules =
aai_cli.agent
aai_cli.argscan
aai_cli.auth
aai_cli.choices
aai_cli.client
aai_cli.code_gen
aai_cli.coding_agent
aai_cli.config
aai_cli.config_builder
aai_cli.context
aai_cli.debuglog
aai_cli.doctor_checks
aai_cli.environments
aai_cli.errors
aai_cli.follow
aai_cli.help_panels
aai_cli.help_text
aai_cli.hotkey
aai_cli.init
aai_cli.init_exec
aai_cli.jsonshape
aai_cli.llm
aai_cli.mediafile
aai_cli.microphone
aai_cli.onboard
aai_cli.options
aai_cli.output
aai_cli.procs
aai_cli.remotefs
aai_cli.render
aai_cli.setup_exec
aai_cli.stdio
aai_cli.steps
aai_cli.streaming
aai_cli.sync_stt
aai_cli.telemetry
aai_cli.theme
aai_cli.timeparse
aai_cli.transcribe_batch
aai_cli.transcribe_exec
aai_cli.transcribe_render
aai_cli.transcribe_sources
aai_cli.transcribe_validate
aai_cli.tts
aai_cli.typer_patches
aai_cli.update_check
aai_cli.wer
aai_cli.ws
aai_cli.youtube
forbidden_modules =
aai_cli.commands

[importlinter:contract:2]
[importlinter:contract:3]
name = Command modules are independent
type = independence
; Wildcard so every module under aai_cli/commands/ is covered automatically —
; the previous enumerated list had silently drifted (onboard and speak were
; missing, so nothing forbade them from importing sibling commands).
; Wildcard so every module under aai_cli/commands/ is covered automatically.
modules =
aai_cli.commands.*

[importlinter:contract:3]
name = Library layers do not depend on Rich rendering
[importlinter:contract:4]
name = Core library and the testable command helpers stay Rich-free
type = forbidden
; The layered contract keeps `core` from importing the `ui` layer, but Rich is
; an external package, so "no Rich below the UI layer" still needs an explicit
; forbidden edge. The two command helpers are pure data/selection logic kept
; Rich-free so their tests never need a console.
source_modules =
aai_cli.argscan
aai_cli.client
aai_cli.core
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.hotkey
aai_cli.llm
aai_cli.remotefs
aai_cli.sync_stt
aai_cli.telemetry
aai_cli.wer
forbidden_modules =
rich
91 changes: 67 additions & 24 deletions aai_cli/AGENTS.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions aai_cli/agent/audio.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
from collections.abc import Callable, Iterator
from typing import Any

from aai_cli.errors import CLIError
from aai_cli.microphone import default_rate, import_sounddevice, resample_pcm16
from aai_cli.core.errors import CLIError
from aai_cli.core.microphone import default_rate, import_sounddevice, resample_pcm16

SAMPLE_RATE = 24000 # Voice Agent native PCM16 mono rate

Expand Down
2 changes: 1 addition & 1 deletion aai_cli/agent/render.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from rich.text import Text

from aai_cli.render import BaseRenderer
from aai_cli.ui.render import BaseRenderer


def _labeled(label: str, body: str, *, style: str = "aai.label") -> Text:
Expand Down
6 changes: 3 additions & 3 deletions aai_cli/agent/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
from dataclasses import dataclass
from typing import Any

from aai_cli import environments
from aai_cli import ws as wsutil
from aai_cli.errors import APIError, CLIError, NotAuthenticated
from aai_cli.core import environments
from aai_cli.core import ws as wsutil
from aai_cli.core.errors import APIError, CLIError, NotAuthenticated
from aai_cli.streaming import diagnostics


Expand Down
Empty file added aai_cli/app/__init__.py
Empty file.
File renamed without changes.
12 changes: 6 additions & 6 deletions aai_cli/context.py → aai_cli/app/context.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from __future__ import annotations

import os
import sys
from collections.abc import Callable
from dataclasses import dataclass
Expand All @@ -9,9 +8,10 @@
import keyring.errors
import typer

from aai_cli import config, debuglog, environments, output, telemetry, update_check
from aai_cli.environments import Environment
from aai_cli.errors import APIError, CLIError, NotAuthenticated
from aai_cli.core import config, debuglog, env, environments, telemetry
from aai_cli.core.environments import Environment
from aai_cli.core.errors import APIError, CLIError, NotAuthenticated
from aai_cli.ui import output, update_check


@dataclass
Expand Down Expand Up @@ -85,7 +85,7 @@ def env_override_warning(self) -> str | None:
"""
if self.env is not None:
source, selected = "--env", self.env
elif (from_env := os.environ.get("AAI_ENV")) is not None:
elif (from_env := env.get("AAI_ENV")) is not None:
source, selected = "AAI_ENV", from_env
else:
return None
Expand Down Expand Up @@ -151,7 +151,7 @@ def _should_auto_login(err: NotAuthenticated) -> bool:
# so retrying cannot fix that case. `rejected_key` is the structured marker set
# by auth_failure(); auth-owning commands (login/logout) opt out at their
# run_command call site with auto_login=False instead of being name-matched here.
return not (os.environ.get(config.ENV_API_KEY) and err.rejected_key)
return not (env.get(config.ENV_API_KEY) and err.rejected_key)


def _auto_login_and_exit(state: AppState, *, json_mode: bool) -> NoReturn:
Expand Down
6 changes: 4 additions & 2 deletions aai_cli/doctor_checks.py → aai_cli/app/doctor_checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@

from rich.markup import escape

from aai_cli import client, coding_agent, config, environments, output, theme
from aai_cli.errors import CLIError, NotAuthenticated
from aai_cli.app import coding_agent
from aai_cli.core import client, config, environments
from aai_cli.core.errors import CLIError, NotAuthenticated
from aai_cli.ui import output, theme


class Check(TypedDict):
Expand Down
8 changes: 5 additions & 3 deletions aai_cli/init_exec.py → aai_cli/app/init_exec.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@
import typer
from rich.markup import escape

from aai_cli import __version__, environments, output, stdio, steps
from aai_cli.context import AppState
from aai_cli.errors import CLIError, UsageError
from aai_cli import __version__
from aai_cli.app.context import AppState
from aai_cli.core import environments, stdio
from aai_cli.core.errors import CLIError, UsageError
from aai_cli.init import keys, runner, scaffold, templates
from aai_cli.ui import output, steps

DEFAULT_PORT = 3000

Expand Down
5 changes: 3 additions & 2 deletions aai_cli/mediafile.py → aai_cli/app/mediafile.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@

import assemblyai as aai

from aai_cli import client, output, youtube
from aai_cli.errors import APIError, CLIError, UsageError
from aai_cli.core import client, youtube
from aai_cli.core.errors import APIError, CLIError, UsageError
from aai_cli.ui import output


def validate_local_media(media: Path, command: str, *, kind: str = "audio/video") -> None:
Expand Down
4 changes: 2 additions & 2 deletions aai_cli/setup_exec.py → aai_cli/app/setup_exec.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
from pathlib import Path
from typing import TYPE_CHECKING

from aai_cli import coding_agent
from aai_cli.steps import Step, render_steps
from aai_cli.app import coding_agent
from aai_cli.ui.steps import Step, render_steps

if TYPE_CHECKING:
# Annotation only (PEP 563 string), so no runtime import. Import from
Expand Down
Empty file.
13 changes: 9 additions & 4 deletions aai_cli/transcribe_batch.py → aai_cli/app/transcribe/batch.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,11 @@
from rich.live import Live
from rich.markup import escape

from aai_cli import client, jsonshape, llm, output, remotefs, theme, transcribe_exec
from aai_cli.errors import CLIError, NotAuthenticated
from aai_cli.transcribe_sources import SIDECAR_SUFFIX, URL_PREFIXES
from aai_cli.app.transcribe import run as transcribe_exec
from aai_cli.app.transcribe.sources import SIDECAR_SUFFIX, URL_PREFIXES
from aai_cli.core import client, jsonshape, llm, remotefs
from aai_cli.core.errors import CLIError, NotAuthenticated
from aai_cli.ui import output, theme

if TYPE_CHECKING:
import assemblyai as aai
Expand Down Expand Up @@ -77,7 +79,10 @@ def resumable_record(sidecar: Path, *, digest: str | None) -> dict[str, object]


def _dump_sidecar(sidecar: Path, record: dict[str, object]) -> None:
sidecar.write_text(json.dumps(record, indent=2, default=str) + "\n")
# `sidecar` is derived from the user's own CLI source argument and written next to
# that source by design (it's the resume marker). A local CLI has no attacker-
# controlled path input, so the path-traversal taint warning is a false positive.
sidecar.write_text(json.dumps(record, indent=2, default=str) + "\n") # nosemgrep


def _write_sidecar(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
from rich.console import Console
from rich.text import Text

from aai_cli import jsonshape, theme
from aai_cli.core import jsonshape
from aai_cli.ui import theme


def _fmt_ms(ms: int) -> str:
Expand Down
27 changes: 9 additions & 18 deletions aai_cli/transcribe_exec.py → aai_cli/app/transcribe/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,15 @@
import assemblyai as aai
from rich.markup import escape

from aai_cli import (
choices,
client,
code_gen,
config_builder,
jsonshape,
llm,
output,
remotefs,
stdio,
transcribe_render,
transcribe_sources,
transcribe_validate,
youtube,
)
from aai_cli import code_gen
from aai_cli.app.context import AppState
from aai_cli.app.transcribe import render as transcribe_render
from aai_cli.app.transcribe import sources as transcribe_sources
from aai_cli.app.transcribe import validate as transcribe_validate
from aai_cli.code_gen.transcribe import render as render_transcribe_code
from aai_cli.context import AppState
from aai_cli.errors import UsageError
from aai_cli.core import choices, client, config_builder, jsonshape, llm, remotefs, stdio, youtube
from aai_cli.core.errors import UsageError
from aai_cli.ui import output


def render_transform_steps(d: dict[str, Any]) -> str:
Expand Down Expand Up @@ -311,7 +302,7 @@ def _print_show_code(opts: TranscribeOptions, merged: dict[str, object]) -> None
def run_transcribe(opts: TranscribeOptions, state: AppState, *, json_mode: bool) -> None:
"""Execute one `assembly transcribe` invocation from already-parsed flags."""
# Module-load order: transcribe_batch imports this module, so import it lazily.
from aai_cli import transcribe_batch
from aai_cli.app.transcribe import batch as transcribe_batch

transcribe_validate.validate_language_flags(
opts.language_code, language_detection=opts.language_detection
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@

from pathlib import Path

from aai_cli import remotefs, stdio
from aai_cli.errors import UsageError, mutually_exclusive
from aai_cli.core import remotefs, stdio
from aai_cli.core.errors import UsageError, mutually_exclusive

SIDECAR_SUFFIX = ".aai.json"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@

import assemblyai as aai

from aai_cli import choices, output, transcribe_sources
from aai_cli.errors import UsageError, mutually_exclusive
from aai_cli.app.transcribe import sources as transcribe_sources
from aai_cli.core import choices
from aai_cli.core.errors import UsageError, mutually_exclusive
from aai_cli.ui import output

# The PII policy strings the SDK accepts, validated client-side so a typo'd
# --redact-pii-policy fails before any upload — mirroring how an unknown --config
Expand Down
4 changes: 2 additions & 2 deletions aai_cli/auth/ams.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

import httpx2 as httpx

from aai_cli import jsonshape
from aai_cli.auth import endpoints
from aai_cli.errors import APIError, NotAuthenticated
from aai_cli.core import jsonshape
from aai_cli.core.errors import APIError, NotAuthenticated

_TIMEOUT = 30.0
_HTTP_ERROR_MIN_STATUS = 400
Expand Down
Loading
Loading