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
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ jobs:
- name: System deps (PortAudio + ffmpeg)
run: sudo apt-get update && sudo apt-get install -y libportaudio2 ffmpeg

# check.sh lints Markdown via the markdownlint CLI (a Node tool); pin to the
# version used locally. The runner ships Node, so a global npm install suffices.
- name: markdownlint CLI
run: npm install -g markdownlint-cli@0.45.0
# check.sh lints Markdown and template JS/CSS via Node CLIs; pin to the
# versions used locally. The runner ships Node, so a global npm install suffices.
- name: Node lint CLIs
run: npm install -g markdownlint-cli@0.45.0 prettier@3.8.3

# check.sh runs every tool through `uv run` / `uv build` for a locked,
# reproducible env, so only uv must be on PATH (installed from PyPI to match
Expand Down
2 changes: 1 addition & 1 deletion .importlinter
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,14 @@ modules =
aai_cli.commands.account
aai_cli.commands.agent
aai_cli.commands.audit
aai_cli.commands.claude
aai_cli.commands.doctor
aai_cli.commands.init
aai_cli.commands.keys
aai_cli.commands.llm
aai_cli.commands.login
aai_cli.commands.samples
aai_cli.commands.sessions
aai_cli.commands.setup
aai_cli.commands.stream
aai_cli.commands.transcribe
aai_cli.commands.transcripts
Expand Down
76 changes: 76 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# AGENTS.md

This file provides guidance to coding agents (Claude Code, Codex, Cursor, and
others) when working with code in this repository. `CLAUDE.md` is a symlink to
this file, so Claude Code reads the same instructions.

## Development commands

This project uses [uv](https://docs.astral.sh/uv/). **Run every Python tool through `uv run`** so it uses the locked environment (`pyproject.toml` + `uv.lock`), not whatever is on `PATH`:

```sh
uv sync --extra dev # create/refresh the venv with dev dependencies
uv run aai --help # run the CLI from the locked environment
./scripts/check.sh # the full gate CI runs: ruff + mypy + markdownlint + prettier + shellcheck + pytest(+coverage) + build/twine
```

Individual tools (all via `uv run`):

```sh
uv run ruff check . # lint
uv run ruff format . # format (line-length 100)
uv run mypy # files = ["aai_cli", "tests"] from pyproject; strict (disallow_untyped_defs on src)
prettier --check "aai_cli/init/templates/**/*.{js,css}" # JS/CSS template formatting
uv run pytest -q # default unit suite
uv run pytest tests/test_transcribe.py -q # a single file
uv run pytest tests/test_transcribe.py::test_name -q # a single test
```

### Test markers

The default suite **excludes** two slow/credentialed marker sets (see `scripts/check.sh` and `pyproject.toml`):

```sh
uv run pytest -m e2e # real-API end-to-end; needs ASSEMBLYAI_API_KEY, else skips
uv run pytest -m install_script # builds a wheel and runs install.sh for real; needs network + uv/pipx
```

`check.sh` runs `-m "not e2e and not install_script"` with a **90% branch-coverage gate** (`--cov-fail-under=90`). New code generally needs tests to clear that gate.

## Naming & packaging gotchas

- The **package/module** is `aai_cli`; the **distribution** name is `aai-cli`; the **console command** is `aai` (`[project.scripts] aai = "aai_cli.main:run"`).
- `aai init` templates live in `aai_cli/init/templates/` and are **committed**, including renamed dotfiles (`gitignore` → `.gitignore`, `env.example`). The wheel force-includes them via `[tool.hatch.build.targets.wheel] artifacts`, excluding `__pycache__/*.pyc`. Editing templates needs care — see the parametrized contract tests (`tests/test_init_template_*.py`).
- `audioop` left the stdlib in 3.13; `audioop-lts` backfills it (conditional dependency). Supported Pythons: 3.10–3.13.

## Architecture

A Typer CLI. `aai_cli/main.py` builds the `app`, registers each command sub-app, and controls `aai --help` ordering via `_COMMAND_ORDER` + a custom `_OrderedGroup`. `run()` is the entry point and swallows `BrokenPipeError` (closed downstream pipe → exit 0).

### Command layer

Each file in `aai_cli/commands/` is a Typer sub-app (`transcribe`, `stream`, `transcripts`, `agent`, `llm`, `login`, `doctor`, `samples`, `init`, `claude`). Command bodies run through `context.run_command(ctx, fn, json=...)`, which maps any `CLIError` to clean stderr output + the error's exit code. Commands never print tracebacks for expected failures.

### Cross-cutting state (resolution order matters)

- **`context.py`** — `AppState` (profile, env) is attached to the Typer context in the root `@app.callback()`. `run_command` is the standard command wrapper.
- **`config.py`** — profiles persisted in `config.toml` (via `platformdirs`); the **API key lives only in the OS keyring** (`KEYRING_SERVICE = "assemblyai-cli"`), never in a dotfile. Key resolution order: `--api-key` flag (validation paths only) → `ASSEMBLYAI_API_KEY` env → keyring. **Run commands deliberately expose no `--api-key` flag** so keys can't leak into `ps`/shell history.
- **`environments.py`** — a frozen `Environment` (api_base, streaming_host, llm_gateway_base, ams_base, stytch_*). `DEFAULT_ENV` is currently **`sandbox000`** (flip to `production` once the prod Stytch value is real). The active environment is a process-global set once at startup; precedence: `--env` → `AAI_ENV` → profile's stored env → default. A credential is only valid against the environment that minted it.
- **`client.py`** — thin wrappers over the `assemblyai` SDK (`transcribe`, `list_transcripts`, `stream_audio`, etc.). It normalizes SDK exceptions: auth failures become a single clean `auth_failure()` `CLIError`; everything else becomes `APIError`. New SDK calls should follow this try/except shape.
- **`errors.py`** — the `CLIError` hierarchy (each with `error_type` + `exit_code`). `output.py` emits errors to **stderr**; stdout stays clean for pipelines. `--json` (auto-enabled when piped/agent-run) switches to machine-readable output.

### Feature subsystems

- **`streaming/`** + `client.stream_audio` — v3 realtime API. Event callbacks run on the SDK reader thread and guard against `BrokenPipeError` (`stdio.silence_stdout()`) so a closed pipe never dumps a thread traceback.
- **`agent/`** — full-duplex voice agent (mic in, TTS out via `voices.py`).
- **`code_gen/`** — backs `--show-code` on `transcribe`/`stream`/`agent`: builds a ready-to-run Python SDK script from exactly the flags passed (no API key needed; generated code reads `ASSEMBLYAI_API_KEY`).
- **`auth/`** — browser-assisted `aai login` via AMS + **Stytch B2B OAuth discovery** (`discovery.py`, `flow.py`, `loopback.py`, `ams.py`). Not Stytch Connected Apps.
- **`init/`** — scaffolds a self-contained FastAPI + HTML starter (`audio-transcription`/`live-captions`/`voice-agent` templates), optionally installs deps and opens the browser; writes the key to a git-ignored `.env`.
- **`commands/claude.py`** — `aai claude install/status/remove` shells out to `claude mcp add` (the `assemblyai-docs` MCP) and `npx skills add` (the AssemblyAI skill). Missing `claude`/`npx` is reported and skipped, not an error.

## Conventions

- `from __future__ import annotations` at the top of every module; modern typing (`X | None`).
- Ruff lint set: `E,F,I,UP,B,BLE,C4,SIM,RET,PTH,ARG,S,RUF`. `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.
73 changes: 0 additions & 73 deletions CLAUDE.md

This file was deleted.

1 change: 1 addition & 0 deletions CLAUDE.md
23 changes: 13 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,20 @@ aai login # store your API key (browser-assisted)
aai transcribe --sample # transcribe the hosted wildfires.mp3 sample
```

## Scaffold A Starter App
## Build An App

Copy a small, self-contained FastAPI + HTML project you can run locally and deploy to Vercel as-is:
`aai init` is how you **build a new app** — it copies a small, self-contained FastAPI + HTML project you can run locally and deploy to Vercel as-is. This is the starting point whenever you want to *create* something, including a voice agent app:

```sh
aai init # pick a template, scaffold, install deps, open the browser
aai init audio-transcription myapp # non-interactive: template + directory
aai init voice-agent my-agent # build a voice agent app (full FastAPI + browser starter)
```

Your key is written to a git-ignored `.env` (never sent to the browser). Use `--no-install` to scaffold only.

> **Building a voice agent? Use `aai init voice-agent`, not `aai agent`.** `aai agent` only *runs* a live mic conversation in the terminal and writes no code; `aai init` creates the actual app.

## Commands

| Command | What it does |
Expand All @@ -65,9 +68,9 @@ Your key is written to a git-ignored `.env` (never sent to the browser). Use `--
| `aai transcribe <file\|url>` | Transcribe a file, URL, or YouTube URL (`--sample`, `--llm`, `--show-code`). |
| `aai transcripts list` / `get <id>` | Browse and fetch past transcripts. |
| `aai stream [file]` | Real-time transcription from a file or the microphone. |
| `aai agent` | Live two-way voice conversation with a voice agent. |
| `aai agent` | *Run* a live two-way voice conversation (to **build** a voice agent app, use `aai init voice-agent`). |
| `aai llm <prompt>` | Prompt the LLM Gateway (`--transcript-id`, or `--follow` for a live stream). |
| `aai claude install` | Wire Claude Code up to AssemblyAI's docs + skill. |
| `aai setup install` | Set up your coding agent for AssemblyAI (docs MCP + skills). |
| `aai samples create <name>` | Scaffold a runnable starter script. |
| `aai keys` / `balance` / `usage` / `limits` / `sessions` / `audit` | Account self-service (browser login). |

Expand Down Expand Up @@ -128,7 +131,7 @@ aai stream -o text | aai llm -f --system "You are a meeting scribe" "summarize a

## Voice Agent

Have a live, two-way voice conversation — full-duplex, so you can interrupt mid-sentence (barge-in). **Use headphones**, otherwise the agent hears itself:
Have a live, two-way voice conversation — full-duplex, so you can interrupt mid-sentence (barge-in). **Use headphones**, otherwise the agent hears itself. (To **build** a voice agent *app*, use `aai init voice-agent` instead — this command just runs a conversation in the terminal.)

```sh
aai agent # talk; the agent talks back. Ctrl-C to stop.
Expand Down Expand Up @@ -208,15 +211,15 @@ AMS sessions are short-lived — if a command reports it needs a browser login,

## AI Coding Agents

Wire Claude Code up to AssemblyAI's live docs (MCP server) and the AssemblyAI skill so your agent writes current, correct integration code:
Set your coding agent up for AssemblyAI — the live docs (MCP server), the AssemblyAI skill, and the bundled `aai-cli` skill so your agent writes current, correct integration code:

```sh
aai claude install # installs the docs MCP server + skill (user scope)
aai claude status # show what's wired up
aai claude remove # unwind both
aai setup install # docs MCP + assemblyai skill + bundled aai-cli skill (user scope)
aai setup status # show what's set up
aai setup remove # unwind all three
```

`install` shells out to `claude mcp add` and `npx skills add`. Pass `--scope project` to scope the MCP server to the current project. A missing `claude` or `npx` is reported and skipped, not treated as an error.
`install` shells out to `claude mcp add` for the MCP and `npx skills add` for the `assemblyai` skill; the `aai-cli` skill ships inside the package and is copied in directly (no network). Pass `--scope project` to scope the MCP server to the current project. A missing `claude` or `npx` is reported and skipped, not treated as an error.

## Reference

Expand Down
3 changes: 3 additions & 0 deletions aai_cli/commands/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ def agent(

Pass an audio file/URL (or --sample) to speak a recorded clip to the agent
instead of the microphone; the session then ends after the agent's reply.

This only runs a conversation in the terminal — it writes no code. To build
a voice agent app, run 'aai init voice-agent' instead.
"""

if list_voices:
Expand Down
9 changes: 6 additions & 3 deletions aai_cli/commands/doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,22 +177,25 @@ def _check_audio() -> Check:


def _check_coding_agent() -> Check:
affects = ["aai claude install"]
affects = ["aai setup install"]
missing = [tool for tool in ("claude", "npx") if shutil.which(tool) is None]
if not missing:
return {
"name": "coding-agent",
"status": "ok",
"affects": [],
"detail": "claude and npx found.",
"detail": "claude and npx found; run 'aai setup install' to wire up the docs MCP + skills.",
"fix": None,
}
return {
"name": "coding-agent",
"status": "warn",
"affects": affects,
"detail": f"not found: {', '.join(missing)}.",
"fix": "Install Claude Code (https://claude.com/claude-code) and Node.js to wire up docs.",
"fix": (
"Install Claude Code (https://claude.com/claude-code) and Node.js, "
"then run 'aai setup install'."
),
}


Expand Down
Loading
Loading