from __future__ import annotations import argparse import importlib.metadata import json import logging import sys from pathlib import Path from config import Config, ConfigValidationError, save from constants import DEFAULT_CONFIG_PATH from diagnostics import ( format_diagnostic_line, run_doctor, run_self_check, ) LEGACY_MAINT_COMMANDS = {"sync-default-model"} def _local_project_version() -> str | None: pyproject_path = Path(__file__).resolve().parents[1] / "pyproject.toml" if not pyproject_path.exists(): return None for line in pyproject_path.read_text(encoding="utf-8").splitlines(): stripped = line.strip() if stripped.startswith('version = "'): return stripped.split('"')[1] return None def app_version() -> str: local_version = _local_project_version() if local_version: return local_version try: return importlib.metadata.version("aman") except importlib.metadata.PackageNotFoundError: return "0.0.0-dev" def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( description=( "Aman is an X11 dictation daemon for Linux desktops. " "Use `run` for foreground setup/support, `doctor` for fast preflight " "checks, and `self-check` for deeper installed-system readiness." ), epilog=( "Supported daily use is the systemd --user service. " "For recovery: doctor -> self-check -> journalctl -> " "aman run --verbose." ), ) subparsers = parser.add_subparsers(dest="command") run_parser = subparsers.add_parser( "run", help="run Aman in the foreground for setup, support, or debugging", description="Run Aman in the foreground for setup, support, or debugging.", ) run_parser.add_argument("--config", default="", help="path to config.json") run_parser.add_argument("--dry-run", action="store_true", help="log hotkey only") run_parser.add_argument( "-v", "--verbose", action="store_true", help="enable verbose logs", ) doctor_parser = subparsers.add_parser( "doctor", help="run fast preflight diagnostics for config and local environment", description="Run fast preflight diagnostics for config and the local environment.", ) doctor_parser.add_argument("--config", default="", help="path to config.json") doctor_parser.add_argument("--json", action="store_true", help="print JSON output") doctor_parser.add_argument( "-v", "--verbose", action="store_true", help="enable verbose logs", ) self_check_parser = subparsers.add_parser( "self-check", help="run deeper installed-system readiness diagnostics without modifying local state", description=( "Run deeper installed-system readiness diagnostics without modifying " "local state." ), ) self_check_parser.add_argument("--config", default="", help="path to config.json") self_check_parser.add_argument("--json", action="store_true", help="print JSON output") self_check_parser.add_argument( "-v", "--verbose", action="store_true", help="enable verbose logs", ) bench_parser = subparsers.add_parser( "bench", help="run the processing flow from input text without stt or injection", ) bench_parser.add_argument("--config", default="", help="path to config.json") bench_input = bench_parser.add_mutually_exclusive_group(required=True) bench_input.add_argument("--text", default="", help="input transcript text") bench_input.add_argument( "--text-file", default="", help="path to transcript text file", ) bench_parser.add_argument( "--repeat", type=int, default=1, help="number of measured runs", ) bench_parser.add_argument( "--warmup", type=int, default=1, help="number of warmup runs", ) bench_parser.add_argument("--json", action="store_true", help="print JSON output") bench_parser.add_argument( "--print-output", action="store_true", help="print final processed output text", ) bench_parser.add_argument( "-v", "--verbose", action="store_true", help="enable verbose logs", ) eval_parser = subparsers.add_parser( "eval-models", help="evaluate model/parameter matrices against expected outputs", ) eval_parser.add_argument( "--dataset", required=True, help="path to evaluation dataset (.jsonl)", ) eval_parser.add_argument( "--matrix", required=True, help="path to model matrix (.json)", ) eval_parser.add_argument( "--heuristic-dataset", default="", help="optional path to heuristic alignment dataset (.jsonl)", ) eval_parser.add_argument( "--heuristic-weight", type=float, default=0.25, help="weight for heuristic score contribution to combined ranking (0.0-1.0)", ) eval_parser.add_argument( "--report-version", type=int, default=2, help="report schema version to emit", ) eval_parser.add_argument( "--output", default="", help="optional path to write full JSON report", ) eval_parser.add_argument("--json", action="store_true", help="print JSON output") eval_parser.add_argument( "-v", "--verbose", action="store_true", help="enable verbose logs", ) heuristic_builder = subparsers.add_parser( "build-heuristic-dataset", help="build a canonical heuristic dataset from a raw JSONL source", ) heuristic_builder.add_argument( "--input", required=True, help="path to raw heuristic dataset (.jsonl)", ) heuristic_builder.add_argument( "--output", required=True, help="path to canonical heuristic dataset (.jsonl)", ) heuristic_builder.add_argument( "--json", action="store_true", help="print JSON summary output", ) heuristic_builder.add_argument( "-v", "--verbose", action="store_true", help="enable verbose logs", ) subparsers.add_parser("version", help="print aman version") init_parser = subparsers.add_parser("init", help="write a default config") init_parser.add_argument("--config", default="", help="path to config.json") init_parser.add_argument( "--force", action="store_true", help="overwrite existing config", ) return parser def parse_cli_args(argv: list[str]) -> argparse.Namespace: parser = build_parser() normalized_argv = list(argv) known_commands = { "run", "doctor", "self-check", "bench", "eval-models", "build-heuristic-dataset", "version", "init", } if normalized_argv and normalized_argv[0] in {"-h", "--help"}: return parser.parse_args(normalized_argv) if normalized_argv and normalized_argv[0] in LEGACY_MAINT_COMMANDS: parser.error( "`sync-default-model` moved to `aman-maint sync-default-model` " "(or use `make sync-default-model`)." ) if not normalized_argv or normalized_argv[0] not in known_commands: normalized_argv = ["run", *normalized_argv] return parser.parse_args(normalized_argv) def configure_logging(verbose: bool) -> None: logging.basicConfig( stream=sys.stderr, level=logging.DEBUG if verbose else logging.INFO, format="aman: %(asctime)s %(levelname)s %(message)s", ) def diagnostic_command(args, runner) -> int: report = runner(args.config) if args.json: print(report.to_json()) else: for check in report.checks: print(format_diagnostic_line(check)) print(f"overall: {report.status}") return 0 if report.ok else 2 def doctor_command(args) -> int: return diagnostic_command(args, run_doctor) def self_check_command(args) -> int: return diagnostic_command(args, run_self_check) def version_command(_args) -> int: print(app_version()) return 0 def init_command(args) -> int: config_path = Path(args.config) if args.config else DEFAULT_CONFIG_PATH if config_path.exists() and not args.force: logging.error( "init failed: config already exists at %s (use --force to overwrite)", config_path, ) return 1 cfg = Config() save(config_path, cfg) logging.info("wrote default config to %s", config_path) return 0 def main(argv: list[str] | None = None) -> int: args = parse_cli_args(list(argv) if argv is not None else sys.argv[1:]) if args.command == "run": configure_logging(args.verbose) from aman_run import run_command return run_command(args) if args.command == "doctor": configure_logging(args.verbose) return diagnostic_command(args, run_doctor) if args.command == "self-check": configure_logging(args.verbose) return diagnostic_command(args, run_self_check) if args.command == "bench": configure_logging(args.verbose) from aman_benchmarks import bench_command return bench_command(args) if args.command == "eval-models": configure_logging(args.verbose) from aman_benchmarks import eval_models_command return eval_models_command(args) if args.command == "build-heuristic-dataset": configure_logging(args.verbose) from aman_benchmarks import build_heuristic_dataset_command return build_heuristic_dataset_command(args) if args.command == "version": configure_logging(False) return version_command(args) if args.command == "init": configure_logging(False) return init_command(args) raise RuntimeError(f"unsupported command: {args.command}")