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
4 changes: 2 additions & 2 deletions .claude/agents/security-reviewer.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
---
name: security-reviewer
description: Use to review changes to authentication, credential storage, environment selection, or subprocess shell-outs in the aai CLI. Focuses on secret handling and the auth/login flow. Invoke after editing aai_cli/auth/, config.py, environments.py, or any subprocess call.
description: Use to review changes to authentication, credential storage, environment selection, or subprocess shell-outs in the assembly CLI. Focuses on secret handling and the auth/login flow. Invoke after editing aai_cli/auth/, config.py, environments.py, or any subprocess call.
tools: Glob, Grep, LS, Read, NotebookRead, WebFetch, TodoWrite, WebSearch, KillShell, BashOutput
---

You are a security reviewer for the **AssemblyAI CLI** (`aai`). Your job is to find security regressions in the areas this CLI is most sensitive about, then report concrete findings — not generic advice.
You are a security reviewer for the **AssemblyAI CLI** (`assembly`). Your job is to find security regressions in the areas this CLI is most sensitive about, then report concrete findings — not generic advice.

## What this CLI guarantees (don't let a change break these)

Expand Down
4 changes: 2 additions & 2 deletions .claude/agents/template-contract-reviewer.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
---
name: template-contract-reviewer
description: Use after any change under aai_cli/init/templates/ (the `aai init` starter apps) to verify the scaffold still ships correctly and stays covered by the parametrized contract tests. Catches the renamed-dotfile and wheel-packaging gotchas.
description: Use after any change under aai_cli/init/templates/ (the `assembly init` starter apps) to verify the scaffold still ships correctly and stays covered by the parametrized contract tests. Catches the renamed-dotfile and wheel-packaging gotchas.
tools: Glob, Grep, LS, Read, NotebookRead, TodoWrite, KillShell, BashOutput
---

You review changes to the `aai init` starter templates in `aai_cli/init/templates/` (`transcribe/`, `stream/`, `agent/`). These ship inside the wheel and are scaffolded onto users' machines, so a broken template ships broken examples. Verify the following and report concrete gaps.
You review changes to the `assembly init` starter templates in `aai_cli/init/templates/` (`transcribe/`, `stream/`, `agent/`). These ship inside the wheel and are scaffolded onto users' machines, so a broken template ships broken examples. Verify the following and report concrete gaps.

## Packaging integrity

Expand Down
2 changes: 1 addition & 1 deletion .claude/commands/review-changes.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
description: Review the current changes with the code-review skill plus the aai CLI's specialized reviewers (security + template contract), scoped to what actually changed.
description: Review the current changes with the code-review skill plus the assembly CLI's specialized reviewers (security + template contract), scoped to what actually changed.
argument-hint: "[git ref to diff against, default: HEAD]"
allowed-tools: Bash(git diff *), Bash(git status *), Bash(git log *), Task, Skill
---
Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/check/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: check
description: Run the full local verification gate for the aai CLI (the same checks CI runs). Use before pushing or opening a PR.
description: Run the full local verification gate for the assembly CLI (the same checks CI runs). Use before pushing or opening a PR.
disable-model-invocation: true
---

Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/plan-feature/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: plan-feature
description: Plan a new aai CLI feature the way this repo does — drive the superpowers planning skill, then persist the spec + plan into docs/superpowers/ under this repo's naming and verification conventions. Use before building anything non-trivial.
description: Plan a new assembly CLI feature the way this repo does — drive the superpowers planning skill, then persist the spec + plan into docs/superpowers/ under this repo's naming and verification conventions. Use before building anything non-trivial.
disable-model-invocation: true
argument-hint: "<short feature description>"
---
Expand Down
6 changes: 3 additions & 3 deletions .claude/skills/release-prep/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
---
name: release-prep
description: Prepare an aai CLI release — bump the version, run the full gate, build and validate the wheel/sdist, and smoke-test the real install. Use when cutting a new release.
description: Prepare an assembly CLI release — bump the version, run the full gate, build and validate the wheel/sdist, and smoke-test the real install. Use when cutting a new release.
disable-model-invocation: true
---

# release-prep

Drive an `aai` release to a verified, publishable state. Stop and report at the first failure — never push or publish on a red check.
Drive an `assembly` release to a verified, publishable state. Stop and report at the first failure — never push or publish on a red check.

## 1. Version bump

Expand All @@ -27,7 +27,7 @@ Must end with `All checks passed.` (ruff, mypy, markdownlint, shellcheck, pytest
uv run pytest -q -m install_script
```

This builds the wheel and runs `install.sh` for real (pipx + pip --user), asserting `aai` runs. Needs network + uv/pipx.
This builds the wheel and runs `install.sh` for real (pipx + pip --user), asserting `assembly` runs. Needs network + uv/pipx.

## 4. Build + metadata validation

Expand Down
20 changes: 10 additions & 10 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ jobs:
# sha256, sidestepping the placeholder that stays until the v0.1.0 tag is cut).
# The stricter `brew audit --strict --online` still belongs in a release job.
- name: Lint the formula
run: brew style ./Formula/aai.rb
run: brew style ./Formula/assembly.rb

pre-commit:
name: pre-commit
Expand Down Expand Up @@ -211,7 +211,7 @@ jobs:
python-version: "3.12"
cache: pip

# `aai --version` imports the package, which pulls in sounddevice (needs
# `assembly --version` imports the package, which pulls in sounddevice (needs
# PortAudio) and ffmpeg-backed sources. Match the other jobs' system deps.
- name: System deps (Linux)
if: runner.os == 'Linux'
Expand Down Expand Up @@ -259,7 +259,7 @@ jobs:
# Mirror a GitHub archive: a single top-level dir brew strips on extract.
git archive --format=tar.gz --prefix="aai-${version}/" -o "$tarball" HEAD
sha="$(shasum -a 256 "$tarball" | awk '{print $1}')"
python3 - "Formula/aai.rb" "$tarball" "$sha" <<'PY'
python3 - "Formula/assembly.rb" "$tarball" "$sha" <<'PY'
import re, sys, pathlib
formula, tarball, sha = sys.argv[1], sys.argv[2], sys.argv[3]
p = pathlib.Path(formula)
Expand All @@ -268,18 +268,18 @@ jobs:
src = re.sub(r"sha256 .*", f'sha256 "{sha}"', src, count=1)
p.write_text(src)
PY
grep -nE '^ (url|sha256) ' Formula/aai.rb | head -2
grep -nE '^ (url|sha256) ' Formula/assembly.rb | head -2

# Newer Homebrew refuses formulae outside a tap, so install through a
# throwaway local tap holding the pinned formula (--no-git avoids needing a
# git identity on the runner). --build-from-source compiles the full resource
# list (rust + cryptography native builds); `brew test` then runs the
# formula's own `test do` block, asserting `aai --version`.
# formula's own `test do` block, asserting `assembly --version`.
- name: brew install + test
run: |
set -euo pipefail
brew tap-new --no-git aai/local
cp Formula/aai.rb "$(brew --repository aai/local)/Formula/aai.rb"
brew install --build-from-source --formula aai/local/aai
aai --version
brew test aai/local/aai
brew tap-new --no-git assembly/local
cp Formula/assembly.rb "$(brew --repository assembly/local)/Formula/assembly.rb"
brew install --build-from-source --formula assembly/local/assembly
assembly --version
brew test assembly/local/assembly
2 changes: 1 addition & 1 deletion .gitleaks.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# gitleaks config for the aai CLI secret-scan gate (scripts/check.sh + CI).
# gitleaks config for the assembly CLI secret-scan gate (scripts/check.sh + CI).
#
# Real credentials never live in this repo by design: the API key is stored only in
# the OS keyring (see aai_cli/config.py, KEYRING_SERVICE), never in a dotfile or source
Expand Down
26 changes: 13 additions & 13 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ This project uses [uv](https://docs.astral.sh/uv/). **Run every Python tool thro

```sh
uv sync # create/refresh the venv (the dev group installs by default)
uv run aai --help # run the CLI from the locked environment
uv run assembly --help # run the CLI from the locked environment
./scripts/check.sh # the full gate CI runs (scripts/check.sh is the source of truth)
```

Expand Down Expand Up @@ -90,14 +90,14 @@ Lessons that cost iterations getting the patch-coverage and mutation tail gates
(`next(o for o in objs if "env" in o)`). A monkeypatched fake must also mirror the real
signature — when a helper gains a kwarg (e.g. `output.status(…, quiet=…)`), doubles that
patch it must accept it or the call `TypeError`s.
- **`--json` / `-j` is a per-command flag, not a root flag**: `aai --json transcribe …` fails
with "No such option"; it's `aai transcribe … --json`. (The root callback still sniffs the
- **`--json` / `-j` is a per-command flag, not a root flag**: `assembly --json transcribe …` fails
with "No such option"; it's `assembly transcribe … --json`. (The root callback still sniffs the
whole token list via `_command_line_requests_json`, so a callback-level failure like a bad
`--env` keeps the JSON error shape — but the flag itself lives on the subcommand.)

### Manual QA / running the CLI in sandboxed sessions

Lessons that cost time in agent sessions — read before exercising `uv run aai` by hand:
Lessons that cost time in agent sessions — read before exercising `uv run assembly` by hand:

- **Probe network reachability first.** Remote/sandboxed environments often allowlist
PyPI but block `api.assemblyai.com` / `streaming.assemblyai.com` / `llm-gateway.assemblyai.com`
Expand All @@ -110,8 +110,8 @@ Lessons that cost time in agent sessions — read before exercising `uv run aai`
through that shared file — set `XDG_CONFIG_HOME=$(mktemp -d)` per run instead.
- **Write scratch output to `/tmp`, never the repo root.** Redirects like `cmd > out.txt`
in the repo show up as untracked files and trip commit hooks/gates.
- **Headless boxes have no mic/speakers/browser.** `aai stream`/`aai agent` mic paths and
`aai login`'s browser flow can't complete; wrap exploratory runs in `timeout 30 …` so a
- **Headless boxes have no mic/speakers/browser.** `assembly stream`/`assembly agent` mic paths and
`assembly login`'s browser flow can't complete; wrap exploratory runs in `timeout 30 …` so a
blocking path can't wedge the session. For pytest, `--timeout N` (pytest-timeout, in the
dev group) does the same per-test.

Expand All @@ -129,7 +129,7 @@ these live in the default suite. Three moving parts:
(it hits the network) and is *not* mypy/pyright-checked (only ruff covers `scripts/`).
Refresh after an API shape change: `ASSEMBLYAI_API_KEY=… uv run python scripts/record_fixtures.py`.
The key comes from the env; the AMS session JWT + `account_id` from the keyring/`config.toml`
of whoever ran `aai login` (profile `default`) — neither is ever written to a fixture.
of whoever ran `assembly login` (profile `default`) — neither is ever written to a fixture.
- **`tests/replay_fixtures.py`** — rebuilds the boundary objects from JSON. A transcript is a
real `aai.Transcript` via `Transcript.from_response`; an LLM response is rebuilt with
`ChatCompletion.model_construct` (**not** `model_validate`) because the gateway returns
Expand All @@ -143,13 +143,13 @@ the return value comes from a recorded payload instead of a hand-built mock.

## 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`).
- The **package/module** is `aai_cli`; the **distribution** name is `aai-cli`; the **console command** is `assembly` (`[project.scripts] assembly = "aai_cli.main:run"`).
- `assembly 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.12–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).
A Typer CLI. `aai_cli/main.py` builds the `app`, registers each command sub-app, and controls `assembly --help` ordering via `_COMMAND_ORDER` + a custom `_OrderedGroup`. `run()` is the entry point and swallows `BrokenPipeError` (closed downstream pipe → exit 0).

### Command layer

Expand All @@ -167,11 +167,11 @@ Each file in `aai_cli/commands/` is a Typer sub-app (`transcribe`, `stream`, `ag

- **`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`).
- **`tts/`** + `commands/speak.py` — `aai speak` synthesizes text to speech over the sandbox streaming-TTS WebSocket (`streaming-tts.sandbox000.…`). **Sandbox-only:** `session.is_available()` is false in production (empty `Environment.streaming_tts_host`), so the command exits 2 with a `--sandbox` hint. `session.synthesize` drives a Begin→Generate→Flush→Audio→Terminate protocol with an injectable `connect` for hermetic tests (mirrors `agent/session.py`); `audio.py` plays the PCM (default) or writes a WAV (`--out`).
- **`tts/`** + `commands/speak.py` — `assembly speak` synthesizes text to speech over the sandbox streaming-TTS WebSocket (`streaming-tts.sandbox000.…`). **Sandbox-only:** `session.is_available()` is false in production (empty `Environment.streaming_tts_host`), so the command exits 2 with a `--sandbox` hint. `session.synthesize` drives a Begin→Generate→Flush→Audio→Terminate protocol with an injectable `connect` for hermetic tests (mirrors `agent/session.py`); `audio.py` plays the PCM (default) or writes a WAV (`--out`).
- **`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.
- **`auth/`** — browser-assisted `assembly 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/setup.py`** — `aai setup install/status/remove` wires a coding agent up to AssemblyAI by installing three artifacts: the `assemblyai-docs` docs MCP (via `claude mcp add`), the AssemblyAI skill (via `npx skills add`), and the bundled `aai-cli` skill (copied out of the wheel, no network). Missing `claude`/`npx` is reported and skipped, not an error.
- **`commands/setup.py`** — `assembly setup install/status/remove` wires a coding agent up to AssemblyAI by installing three artifacts: the `assemblyai-docs` docs MCP (via `claude mcp add`), the AssemblyAI skill (via `npx skills add`), and the bundled `aai-cli` skill (copied out of the wheel, no network). Missing `claude`/`npx` is reported and skipped, not an error.

## Conventions

Expand Down
6 changes: 3 additions & 3 deletions Formula/aai.rb → Formula/assembly.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
class Aai < Formula
class Assembly < Formula
include Language::Python::Virtualenv

desc "Command-line interface for AssemblyAI"
Expand All @@ -10,7 +10,7 @@ class Aai < Formula

depends_on "pkgconf" => :build # cffi / cryptography native builds
depends_on "rust" => :build # pydantic-core, jiter, cryptography
depends_on "cloudflared" # public quick-tunnel for `aai share`
depends_on "cloudflared" # public quick-tunnel for `assembly share`
depends_on "ffmpeg" # decode non-WAV/URL audio (transcribe/stream)
depends_on "openssl@3" # cryptography linkage
depends_on "portaudio" # sounddevice (audio capture)
Expand Down Expand Up @@ -264,6 +264,6 @@ def install
end

test do
assert_match version.to_s, shell_output("#{bin}/aai --version")
assert_match version.to_s, shell_output("#{bin}/assembly --version")
end
end
Loading
Loading