-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpyproject.toml
More file actions
526 lines (498 loc) · 27.8 KB
/
Copy pathpyproject.toml
File metadata and controls
526 lines (498 loc) · 27.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
[build-system]
requires = ["hatchling", "hatch-vcs"]
build-backend = "hatchling.build"
[project]
name = "aai-cli"
dynamic = ["version"]
description = "Command-line interface for AssemblyAI"
readme = "README.md"
license = "MIT"
license-files = ["LICENSE"]
authors = [{ name = "AssemblyAI", email = "support@assemblyai.com" }]
requires-python = ">=3.12"
keywords = ["assemblyai", "transcription", "speech-to-text", "cli", "audio", "streaming"]
classifiers = [
"Development Status :: 4 - Beta",
"Environment :: Console",
"Intended Audience :: Developers",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Topic :: Multimedia :: Sound/Audio :: Speech",
"Topic :: Software Development :: Libraries :: Python Modules",
]
dependencies = [
"typer>=0.26.7", # >=0.13 vendors its own click (typer._click); we no longer import click
"assemblyai>=0.64.4",
"rich>=15.0.0",
"keyring>=25.7.0",
# httpx2 is Pydantic's maintained fork of httpx (github.com/pydantic/httpx2,
# forked from httpx 0.28.1, API-identical bar the package rename). The exact
# version is pinned in uv.lock; safe-chain's minimum-package-age check governs
# which releases the lock may adopt, so no manual version cap is needed here.
# (Plain httpx still arrives transitively via the assemblyai SDK.)
"httpx2>=2.0.0",
"pydantic>=2.13.4",
"platformdirs>=4.10.0",
"tomli-w>=1.2.0",
# Floor kept at >=14 (not the dependabot-bumped >=16) so the resolver can pick a
# 15.x line: deepagents' langgraph-sdk caps websockets <16, and our usage
# (websockets.sync/asyncio.client) has been stable since 13.x. assemblyai needs >=11.
"websockets>=14",
"sounddevice>=0.5.5",
"openai>=2.41.0",
"yt-dlp>=2026.3.17",
"questionary>=2.0.1",
# Version compare for the update notifier. Conservative floor so safe-chain's
# minimum-package-age check doesn't reject resolution; packaging already arrives
# transitively, so the lock pins the same release.
"packaging>=24.0",
# audioop (used for PCM resampling) left the stdlib in 3.13; this backport provides it.
"audioop-lts>=0.2; python_version >= '3.13'",
# `assembly eval` WER scoring uses jiwer (the de-facto standard WER
# implementation); wer.py imports it lazily to keep CLI startup fast.
"jiwer>=4.0",
# Bucket-URL audio sources (s3://, gs://, …) for `assembly transcribe` (remotefs.py,
# imported lazily). fsspec core only — each protocol's backend (s3fs, gcsfs, adlfs,
# …) stays a user-installed extra surfaced via a clean install hint.
"fsspec>=2026.4.0",
# Podcast RSS/Atom feed parsing for `assembly transcribe <feed-url>` (feed.py,
# imported lazily). The de-facto standard feed parser; pure-Python, no compiled
# deps. We hand it already-fetched bytes (never a URL) so our bounded, safe
# httpx fetch stays the only network path.
"feedparser>=6.0.11",
# Web-page article extraction for `assembly speak --url` (webpage.py, imported
# lazily). Strips boilerplate down to the readable body; ships prebuilt wheels
# (lxml included), so it adds no source-compile step to Homebrew bottling.
"trafilatura>=2.1.0",
# PDF text extraction for `assembly speak --url` when the URL serves a PDF
# (webpage.py, imported lazily). Pure-Python, permissively licensed, ships a
# universal wheel, so it adds no source-compile step to Homebrew bottling.
"pypdf>=5.1.0",
# `assembly code` coding agent (deepagents on the LLM Gateway). Heavy trees,
# intentionally added on this WIP branch; see aai_cli/code_agent/.
"deepagents>=0.6.10",
"langchain-openai>=1.3.2",
"langgraph>=1.2.2",
"langchain-core>=1.4.7",
"langchain-mcp-adapters>=0.3.0",
"textual>=8.2.7",
"langgraph-checkpoint-sqlite>=3.1.0",
"pyperclip>=1.11.0",
"langchain-text-splitters>=1.0.0",
"langchain-firecrawl>=0.1.0",
]
[project.urls]
Homepage = "https://github.com/AssemblyAI/cli"
Repository = "https://github.com/AssemblyAI/cli"
Issues = "https://github.com/AssemblyAI/cli/issues"
[project.scripts]
assembly = "aai_cli.main:run"
# PEP 735 dependency group (not a [project] extra). uv installs this group by
# default for `uv run`/`uv sync` (see [tool.uv] default-groups below), so the
# type-checkers and pytest resolve their imports with no `--extra`/`--group` flag
# — and a plain `.venv` is correct for editors (see [tool.pyright] venv). pip
# installs it with `pip install --group dev` (requires pip >= 25.1).
[dependency-groups]
dev = [
"pytest>=9.0.3",
"pytest-cov>=7.1.0",
"pytest-mock>=3.14.0",
"pytest-randomly>=3.16.0",
"pytest-xdist>=3.6.0",
# Hermeticity guards beyond order-randomization (pytest-randomly):
# pytest-socket blocks real network in the unit suite so an unmocked SDK/HTTP
# call fails loudly instead of silently hitting the API (see addopts below);
# time-machine lets tests freeze the clock, complementing the TZ pin in
# tests/conftest.py so time rendering is deterministic across machines.
# Floors sit at the second-newest release so safe-chain's min-age gate can resolve.
"pytest-socket>=0.7.0",
# Hang guard for agent/CI runs: `pytest --timeout N` turns a stuck test into a
# failure instead of a wedged session (not in addopts — opt-in per run).
"pytest-timeout>=2.3.1",
"time-machine>=3.1.0",
# Visual-regression snapshots for the Textual TUIs (`assembly code` / `live`): the
# `snap_compare` fixture renders an app to SVG and diffs it against a committed golden,
# catching CSS/layout/docking regressions the behavioral pilot tests can't see. Stores
# SVGs under tests/__snapshots__/<module>/ (regenerate with --snapshot-update like the
# .ambr goldens). See tests/AGENTS.md "Textual visual snapshots".
"pytest-textual-snapshot>=1.0.0",
"hypothesis>=6.155.1",
"ruff>=0.15.15",
"mypy>=2.1.0",
"pyright>=1.1.409",
"pre-commit>=4.6.0",
"fastapi>=0.115.0",
"uvicorn>=0.30.0",
"python-dotenv>=1.0.0",
"python-multipart>=0.0.9",
"syrupy>=4.0",
"httpx>=0.28.1",
"xenon>=0.9.3",
"diff-cover>=9.0.0",
"vulture>=2.14",
"deptry>=0.23.0",
"import-linter>=2.3",
"zizmor>=1.10",
"coverage>=7.0",
"click>=8.1",
]
[tool.uv]
# Install the dev group for every `uv run`/`uv sync` (this is uv's default, pinned
# here so it's explicit and stable).
default-groups = ["dev"]
[tool.hatch.version]
source = "vcs"
[tool.hatch.build.hooks.vcs]
version-file = "aai_cli/_version.py"
[tool.hatch.build.targets.wheel]
packages = ["aai_cli"]
# Force-include the committed template files (incl. renamed dotfiles); the `**` glob
# would otherwise also sweep in __pycache__/*.pyc left by importing a template during
# tests, so exclude those from the wheel.
artifacts = [
"aai_cli/init/templates/**",
"aai_cli/skills/**",
"aai_cli/streaming/macos_system_audio.swift",
]
exclude = ["**/__pycache__", "**/*.pyc", "**/AGENTS.md", "**/CLAUDE.md"]
[tool.pytest.ini_options]
testpaths = ["tests"]
# Block real network in every pytest invocation (pytest-socket). The unit suite is
# fully mocked, so an accidental real SDK/HTTP call now raises SocketBlockedError
# instead of silently reaching the API — hard-enforcing the unit/e2e split we keep
# by convention. --allow-unix-socket keeps AF_UNIX (used by some libs) working. Tests
# that bind a loopback server (OAuth callback, dev server, tunnel) opt back in with
# `@pytest.mark.allow_hosts([...])`. The e2e/install suites drive the CLI as a
# subprocess, whose sockets this in-process patch never sees, so they're unaffected.
# -m: a bare `pytest` runs the same suite check.sh gates — the e2e/install
# marker suites are slow, need network/credentials, and fail confusingly in
# sandboxes otherwise. An explicit command-line -m (e.g. `pytest -m e2e`)
# overrides this one, so the opt-in runs keep working.
# -p no:langsmith_plugin: the langsmith pytest plugin (pulled in transitively by the
# deepagents/langchain stack) mutates global warning state at load, which surfaces
# unrelated pre-existing `.dict()` deprecations as errors under `filterwarnings=error`;
# we don't use it, so disable it.
addopts = "--disable-socket --allow-unix-socket -p no:langsmith_plugin -m 'not e2e and not install'"
# Treat warnings as errors so a deprecation or resource leak introduced in a change
# fails the build instead of scrolling past. Listed-later filters take precedence, so
# the targeted ignores below override the blanket `error`.
filterwarnings = [
"error",
# The deepagents/langchain dependency stack perturbs global warning state at import,
# surfacing the assemblyai SDK's own (legitimate) pydantic `.dict()` calls as
# PydanticDeprecatedSince20 errors under the `error` rule above. Ignore that specific
# third-party deprecation so it doesn't fail unrelated tests.
"ignore::pydantic.warnings.PydanticDeprecatedSince20",
# firecrawl-py's pydantic models define fields named "json" (shadowing BaseModel.json),
# which pydantic flags with a UserWarning at import. It's third-party and harmless to us
# (we never touch those models), so ignore that one message under the `error` rule.
'ignore:Field name .* shadows an attribute in parent:UserWarning',
]
markers = [
"e2e: real-API end-to-end tests that drive the CLI (need ASSEMBLYAI_API_KEY; skip otherwise)",
"install: install each init template's requirements.txt into a clean venv and import it (slow; needs network + uv; skip otherwise)",
]
[tool.mypy]
python_version = "3.12"
files = ["aai_cli", "tests"]
# Init templates ARE type-checked: they're importable packages
# (aai_cli.init.templates.<name>.api.*) whose api/ code must stay strict-clean against
# the real SDK types, not just shipped as scaffold text.
# Third-party deps (assemblyai, sounddevice) ship no type stubs.
ignore_missing_imports = true
disallow_untyped_defs = true
warn_unused_ignores = true
warn_return_any = true
no_implicit_optional = true
strict_equality = true
warn_redundant_casts = true
warn_unreachable = true
disallow_any_generics = true
no_implicit_reexport = true
extra_checks = true
# The remaining flags from mypy --strict, which aai_cli already satisfies. They close
# gaps the above leave open: a function with *some* annotations but an unannotated
# arg/return (disallow_incomplete_defs), the body of any still-untyped function going
# unchecked (check_untyped_defs), an untyped decorator silently erasing a function's
# type (disallow_untyped_decorators), subclassing an Any-typed base (disallow_subclassing_any),
# and a config option that no longer matches any file (warn_unused_configs). The one
# strict flag left off is disallow_untyped_calls: jiwer ships no stubs, so wer.py's
# RemovePunctuation() call is unavoidably untyped, and turning it on would force a
# net-new `# type: ignore` the escape-hatch gate rejects.
disallow_incomplete_defs = true
check_untyped_defs = true
disallow_untyped_decorators = true
disallow_subclassing_any = true
warn_unused_configs = true
[[tool.mypy.overrides]]
# Tests are type-checked too, but pytest functions don't need return annotations
# (disallow_untyped_defs/disallow_incomplete_defs), and the bodies of those untyped
# helpers — full of mock plumbing and ad-hoc fixtures — would drown the signal if
# type-checked (check_untyped_defs) or block subclassing untyped test doubles
# (disallow_subclassing_any) / wrapping them in untyped decorators
# (disallow_untyped_decorators). The strict flags stay on for the shipped package.
# With check_untyped_defs off, mypy emits an `annotation-unchecked` note per annotated
# untyped helper; silence those notes so the test output stays signal.
module = "tests.*"
disallow_untyped_defs = false
disallow_incomplete_defs = false
check_untyped_defs = false
disallow_subclassing_any = false
disallow_untyped_decorators = false
disable_error_code = ["annotation-unchecked"]
[[tool.mypy.overrides]]
# The deepagents/langchain/langgraph boundary is deeply generic: create_deep_agent
# returns an overloaded CompiledStateGraph and takes invariant middleware/interrupt
# types that our small structural `CompiledAgent` protocol and concrete middleware
# can't satisfy without dragging langgraph's full type params through our code. We
# keep our own signatures precise and silence only the boundary-assignment codes for
# these two wiring modules (the orchestration is covered by the real-graph tests).
module = [
"aai_cli.code_agent.agent",
"aai_cli.code_agent.skills",
"aai_cli.code_agent.memory",
"aai_cli.code_agent.store",
"aai_cli.code_agent.model",
"aai_cli.commands.code._exec",
"aai_cli.agent_cascade.brain",
]
disallow_any_generics = false
disable_error_code = ["return-value", "arg-type", "type-arg", "call-arg"]
[tool.pyright]
# Second type checker alongside mypy: pyright catches a different class of
# issues. Production code runs strict here; tests are checked in standard mode by
# pyrightconfig.tests.json from scripts/check.sh so pytest fixtures/mocks don't
# create thousands of low-value strict diagnostics.
include = ["aai_cli"]
# Re-list pyright's built-in excludes explicitly (defining any `exclude` drops the
# defaults, which pyright warns about). NOTE: init templates are deliberately NOT here —
# their api/ code is strict-clean and type-checked in-tree
# (aai_cli.init.templates.<name>.api.*), the same bar as the rest of the package; only
# generated/hidden dirs are skipped.
exclude = ["**/node_modules", "**/__pycache__", "**/.*"]
# The coding-agent slice wires the deeply-generic, only-partially-typed
# deepagents/langchain/langgraph boundary, where pyright-strict floods on
# Unknown*/invariance diagnostics our precise signatures can't satisfy. mypy still
# type-checks these modules (with the targeted overrides above) as the safety net, so
# we suppress pyright diagnostics here rather than littering per-line `# pyright: ignore`.
ignore = ["aai_cli/code_agent", "aai_cli/commands/code", "aai_cli/agent_cascade/brain.py"]
pythonVersion = "3.12"
typeCheckingMode = "strict"
# Third-party deps (assemblyai, sounddevice) ship no type stubs.
reportMissingTypeStubs = false
# Resolve imports against the uv-managed environment so the CLI and editors
# (Pylance) report identically. `uv run`/`uv sync` create `.venv` with the dev
# group, so pytest/fastapi/etc. resolve here. Run `uv sync` once on a fresh clone.
venvPath = "."
venv = ".venv"
# Editors (Pylance) read this config and also analyze whatever test file you have
# open. The `include` above scopes the *gate's* `pyright` run to aai_cli (strict), but
# the editor checks open tests too — and applies the strict mode above, surfacing
# thousands of low-value pytest fixture/mock diagnostics (untyped `monkeypatch`,
# unknown member/parameter types, …). `executionEnvironments` can't set
# `typeCheckingMode`, so we silence exactly the strict-only "unknown type" family for
# tests/ — matching the standard mode the gate uses for tests (pyrightconfig.tests.json
# in scripts/check.sh). Editor-facing only: the gate's `pyright` run never analyzes
# tests/ (not in `include`), so this changes nothing about what the gate checks.
[[tool.pyright.executionEnvironments]]
root = "tests"
reportUnknownParameterType = "none"
reportMissingParameterType = "none"
reportUnknownMemberType = "none"
reportUnknownVariableType = "none"
reportUnknownArgumentType = "none"
reportUnknownLambdaType = "none"
reportPrivateUsage = "none"
reportUnusedFunction = "none"
reportMissingTypeArgument = "none"
reportUntypedNamedTuple = "none"
[tool.ruff]
line-length = 100
target-version = "py312"
# hatch-vcs writes this at build time; it is not linted or formatted to project style.
extend-exclude = ["aai_cli/_version.py"]
[tool.ruff.lint]
# PGH/ERA/TRY/TD/FIX close the "make the error go away" escape hatches a coding agent
# reaches for: blanket `# noqa`/`# type: ignore` (PGH), commented-out code (ERA),
# exception anti-patterns (TRY), and untracked TODO/FIXME markers (TD/FIX).
# A/N/FBT/PL/T20/PT/PIE/PERF/TCH add maintainability pressure: naming/shadowing,
# boolean traps, pylint-style design issues, centralized raw output, pytest style,
# small simplifications, performance footguns, and type-only import hygiene.
# ASYNC/LOG/G/DTZ/FLY/ICN/SLOT/ISC/TID add correctness pressure the above miss and the
# codebase already satisfies (so they're forward-looking, zero-churn enforcement):
# ASYNC — blocking calls (time.sleep, open(), sync HTTP) inside the streaming/agent
# asyncio code, which would stall the event loop;
# LOG/G — logging anti-patterns (f-strings/`.format` in log calls, `exception()`
# outside handlers) in debuglog and friends;
# DTZ — naive datetime construction (timezone bugs);
# FLY — static `str.join` that should be an f-string (pairs with UP);
# ISC — implicitly concatenated string literals across lines (the classic
# missing-comma-in-a-list bug); ISC001 is owned by the formatter (ignored);
# ICN/SLOT — import-convention and __slots__ hygiene;
# TID — relative imports (banned outright below) so every import is absolute,
# reinforcing the import-linter architecture contracts;
# T10 — a forgotten breakpoint()/pdb/ipdb left in the shipped code (the debugger
# counterpart to the T20 print ban already selected).
select = ["E", "F", "I", "UP", "B", "BLE", "C4", "SIM", "RET", "PTH", "ARG", "S", "RUF",
"PGH", "ERA", "TRY", "TD", "FIX", "A", "N", "FBT", "PL", "C90", "T20", "PT",
"PIE", "PERF", "TCH", "ASYNC", "LOG", "G", "DTZ", "FLY", "ICN", "SLOT", "ISC",
"TID", "T10"]
# E501: line length is owned by the formatter.
# B008: Typer uses function calls (typer.Option/Argument) as parameter defaults.
# S603/S607: we intentionally shell out to `claude`/`npx` with controlled args.
# TRY003: the CLIError hierarchy is built around descriptive messages passed at the
# raise site, so long messages there are deliberate, not an anti-pattern.
# N818: NotAuthenticated is an established domain exception name.
# PLC0415: optional/heavy runtime deps are imported lazily to keep startup fast.
# TC001-TC003: the project intentionally keeps readable top-level type imports; TC006
# still enforces quoted runtime casts.
# ISC001: single-line implicit string concatenation is managed by the formatter, which
# would otherwise fight this lint (ruff's own recommendation when both are enabled).
ignore = ["E501", "B008", "S603", "S607", "TRY003", "N818", "PLC0415",
"TC001", "TC002", "TC003", "ISC001"]
# Function-size pressure, tuned to keep functions small enough to read and edit in
# one screen (the friction a coding agent hits most). These complement xenon's
# cyclomatic-complexity gate in check.sh: mccabe (C901) and max-branches bound
# branchiness; max-statements bounds raw length; max-args bounds signatures.
[tool.ruff.lint.flake8-tidy-imports]
# Every intra-package import is already absolute (`from aai_cli.x import y`); banning
# relative imports outright keeps it that way, which makes modules movable and the
# import-linter contracts unambiguous.
ban-relative-imports = "all"
# Disallowed-methods enforcement, modeled on the Deno toolchain's per-crate clippy.toml
# bans (only designated crates may call the fenced std methods). Process spawning and
# raw environment access stay confined to the modules that legitimately own them
# (allowlisted via per-file-ignores below); any *new* module reaching for them trips
# TID251, so adding one is a visible, reviewable edit rather than a silent spread.
# The matcher is AST-based, so the os.environ snippets inside the code_gen --show-code
# exemplars (string literals) don't trip it.
[tool.ruff.lint.flake8-tidy-imports.banned-api]
"subprocess".msg = "Spawn detached children via aai_cli.procs; if a module genuinely needs raw subprocess, add it to the TID251 allowlist in pyproject.toml."
"os.environ".msg = "Read/write the environment through aai_cli.core.env (env.get/child_env/force_color/...), the single allowlisted chokepoint; callers still own their variable names."
"os.getenv".msg = "Use aai_cli.core.env.get, the single project idiom and the only module allowlisted for raw os.environ (TID251)."
"os.putenv".msg = "os.putenv/os.unsetenv bypass os.environ and desync the mapping; mutate os.environ instead."
"os.unsetenv".msg = "os.putenv/os.unsetenv bypass os.environ and desync the mapping; mutate os.environ instead."
[tool.ruff.lint.mccabe]
max-complexity = 10 # matches xenon's grade-B ceiling (CC <= 10) so the two agree
[tool.ruff.lint.pylint]
max-args = 8
max-branches = 12
max-returns = 6
max-statements = 40
[tool.ruff.lint.per-file-ignores]
# Tests assert freely, use throwaway args/temp paths, and don't need pathlib/security lints.
# TRY300: test helpers commonly `return` inside a try while asserting on the except path.
# Tests also keep literal exit codes, local imports, composite assertions, and fake
# call signatures where those make the intent clearer than production-style indirection.
# TID251: tests drive the CLI as a subprocess and monkeypatch os.environ freely; the
# banned-api ban targets the shipped aai_cli package, not the test harness or dev gates.
# DTZ: tests build naive datetimes as deterministic fixtures (the suite pins TZ in
# conftest and uses time-machine), so timezone-aware construction isn't required here.
"tests/**" = ["S101", "S105", "S106", "S107", "S108", "ARG001", "ARG002", "ARG005",
"PTH123", "SIM117", "TRY300", "FBT", "PLR2004", "PLC0415", "PLR0913",
"PLW1510", "N806", "N818", "PLW0108", "PT018", "TCH", "TID251", "DTZ"]
"scripts/**" = ["TID251"]
# Typer command functions naturally have many boolean options and broad signatures
# (PLR0913/FBT). Their *bodies*, though, are held to the same length/branch limits as
# the rest of the package: PLR0912/PLR0915/C901 are deliberately NOT ignored here.
"aai_cli/commands/**" = ["FBT001", "FBT003", "PLR0913"]
# The root callback is also a Typer command signature.
"aai_cli/main.py" = ["FBT001", "FBT003"]
# Shared Typer option factories take the same boolean positional default the
# command signatures do.
"aai_cli/options.py" = ["FBT003"]
# Raw stdout/stderr writes are centralized here; command modules call output helpers.
"aai_cli/ui/output.py" = ["T201"]
# The active environment is process-global startup state by design.
"aai_cli/core/environments.py" = ["PLW0603"]
# Verbosity is process-global startup state by design (mirrors environments.py).
"aai_cli/core/debuglog.py" = ["PLW0603"]
# The "shutdown SIGINT guard installed" latch is process-global once-only state.
"aai_cli/core/microphone.py" = ["PLW0603"]
# BaseHTTPRequestHandler.log_message requires a parameter named `format`.
"aai_cli/auth/loopback.py" = ["A002"]
# Template constants include URL path names such as TOKEN_PATH, not credentials.
# TID251: the scaffolds are end-user example apps that read their own config straight
# from os.environ — that's correct, idiomatic code to ship, not a CLI-internal env read.
# BLE001: starter apps funnel any leg failure into one user-facing error event/response
# (a broad `except Exception` is the right shape to ship), so the blind-except lint
# doesn't apply to scaffolds.
# TID252: scaffolds ship as a self-contained top-level `api/` package, so their inner
# imports must be relative (`from . import settings`) — that's the one form that resolves
# both in the shipped app (`uvicorn api.index:app`) and when type-checked in-tree as
# aai_cli.init.templates.<name>.api. Absolute `from api import …` can't satisfy both.
"aai_cli/init/templates/**" = ["S105", "TID251", "BLE001", "TID252"]
# ENV_CLIENT_TOKEN holds an env-var *name*; the shipped token constant is empty in
# source (release builds inject the write-only client token).
"aai_cli/core/telemetry.py" = ["S105"]
# BLE001: connecting to the docs MCP server is best-effort — any failure (blocked host,
# offline, transport error) degrades to "no docs tools", so a broad except is the shape.
"aai_cli/code_agent/docs_mcp.py" = ["BLE001"]
# BLE001: launching each live-agent MCP server is best-effort — any failure (npx/uvx
# missing, offline host, transport error) skips just that server so one broken tool
# can't sink a live session, so a broad per-server except is the right shape.
"aai_cli/agent_cascade/mcp_tools.py" = ["BLE001"]
# BLE001: a turn must never crash the TUI/REPL — any agent/gateway failure is caught and
# surfaced as an ErrorText event so the user can simply retry.
"aai_cli/code_agent/session.py" = ["BLE001"]
# A002: the CompiledAgent protocol must mirror langgraph's `invoke(input, ...)` parameter
# name so the real compiled graph structurally satisfies it.
"aai_cli/code_agent/agent.py" = ["A002"]
# FBT001: a Textual push_screen result callback receives the bool decision positionally.
"aai_cli/code_agent/tui.py" = ["FBT001"]
# TID251 banned-api allowlist (see [tool.ruff.lint.flake8-tidy-imports.banned-api]).
# Two OS boundaries are fenced; each is owned by a chokepoint so the allowlist stays
# small and a new module reaching past the boundary trips the gate in review.
# Environment access: aai_cli.core.env is the SINGLE module permitted raw os.environ;
# every other module reads/writes the environment through it (callers still own their
# variable *names*). One entry, enforced structurally — not a per-file list to drift.
"aai_cli/core/env.py" = ["TID251"]
# Process spawning: unlike env reads, these are genuinely diverse (sync-capture,
# long-lived Popen with pipes, detached children) and each owns shelling out to its
# specific external tool — funnelling them through one module would just re-export all
# of `subprocess`, so they stay individually allowlisted (claude/npx/ffmpeg/yt-dlp/
# tunnels/vercel/the macOS Swift helper, etc.):
"aai_cli/core/procs.py" = ["TID251"]
# Runs the AssemblyAI CLI itself (python -m aai_cli) as a tool the coding agent calls.
"aai_cli/code_agent/cli_tool.py" = ["TID251"]
"aai_cli/app/coding_agent.py" = ["TID251"]
"aai_cli/app/mediafile.py" = ["TID251"]
"aai_cli/app/setup_exec.py" = ["TID251"]
"aai_cli/commands/deploy/_exec.py" = ["TID251"]
"aai_cli/commands/update.py" = ["TID251"]
"aai_cli/commands/webhooks/_listen.py" = ["TID251"]
"aai_cli/init/runner.py" = ["TID251"]
"aai_cli/init/tunnel.py" = ["TID251"]
"aai_cli/streaming/macos.py" = ["TID251"]
"aai_cli/streaming/sources.py" = ["TID251"]
[tool.vulture]
paths = ["aai_cli", "tests"]
exclude = ["aai_cli/_version.py"]
min_confidence = 90
ignore_decorators = ["@app.command", "@app.callback"]
ignore_names = ["app", "capture_output", "download", "healthy", "ist", "lpath", "memory_keyring",
"org", "preserve_logging_state", "refresh", "rpath"]
[tool.codespell]
# Spell-check code, comments, and docs (Kubernetes' verify-spelling, generalized). Run via
# `uvx codespell` in check.sh and as a pre-commit hook, so it needs no entry in uv.lock.
# Skip generated/binary/snapshot trees and the lockfile; recorded fixtures and snapshots
# are byte-pinned and must not be "corrected".
skip = "./.venv,./dist,./docs,./node_modules,./.git,uv.lock,*.ambr,./tests/fixtures,./aai_cli/_version.py"
# Domain words codespell misreads: "unparseable" (accepted variant), "ist" (an identifier),
# "expresso" (a deliberate mis-transcription used as an eval/WER example).
ignore-words-list = "unparseable,ist,expresso,notin,ans"
[tool.deptry]
exclude = ["docs", "dist", ".venv", "aai_cli/init/templates"]
[tool.deptry.package_module_name_map]
audioop-lts = "audioop"
[tool.deptry.per_rule_ignores]
# The CLI templates carry their own requirements and are dependency-checked by
# dedicated install tests, not by the root package metadata.
DEP002 = ["fastapi", "python-dotenv", "python-multipart", "uvicorn"]
# coverage is read by scripts/mutation_gate.py and click by
# scripts/generated_code_compile_gate.py (dev-only gates run from check.sh, never
# shipped in the wheel), so deptry sees dev deps imported from non-test code.
# syrupy is imported by tests/_snapshot_surface.py (typing the snapshot fixture).
DEP004 = ["fastapi", "httpx", "hypothesis", "pytest", "coverage", "click", "syrupy"]