//! Command line interface definition (clap v4, derive API). use clap::{ArgAction, Args, Parser, Subcommand}; use clap_complete::Shell; use std::path::PathBuf; /// Manage local AI coding agents: install, start, stop, update. #[derive(Parser, Debug, Clone)] #[command( name = "am", bin_name = "am", version, // The help subcommand is provided by the custom Nushell-style help // (cli::Command::Help) instead of clap's generated one. disable_help_subcommand = true, about = "agent-manager (am) — manage local AI coding agents", long_about = "agent-manager (am) — a CLI to manage local AI coding agents. Install, uninstall, start, stop, restart and update AI agents from a YAML catalog. Handles dependencies (Node, Python, Go, Rust, Bun), binaries from GitHub Releases, install scripts, and git repositories. All state lives under your user directories; nothing pollutes the system." )] pub struct Cli { /// Use an alternate configuration file #[arg(short = 'c', long, global = true, value_name = "FILE")] pub config: Option, /// Verbose output: show every command executed and its details #[arg(short = 'v', long, global = true, action = ArgAction::SetTrue)] pub verbose: bool, /// Quiet mode: only errors are printed #[arg(short = 'q', long, global = true, conflicts_with = "verbose", action = ArgAction::SetTrue)] pub quiet: bool, /// Answer yes to every confirmation prompt #[arg(short = 'y', long, global = true, action = ArgAction::SetTrue)] pub yes: bool, /// Simulate the action without changing anything #[arg(long, global = true, action = ArgAction::SetTrue)] pub dry_run: bool, /// Emit machine-readable JSON on stdout (for scripts) #[arg(long, global = true, action = ArgAction::SetTrue)] pub json: bool, /// Disable ANSI colors #[arg(long, global = true, action = ArgAction::SetTrue)] pub no_color: bool, /// Color theme for the output (see 'am help theme' for the list) #[arg(long, global = true, value_name = "THEME")] pub theme: Option, /// Output language: fr (default) or en (issue #74) #[arg(long, global = true, value_enum, value_name = "LANG")] pub lang: Option, #[command(subcommand)] pub command: Option, } #[derive(Subcommand, Debug, Clone)] pub enum Command { /// List installed agents (default); use --all for the whole catalog List { /// Show every agent known to the catalog, not only the installed ones #[arg(long)] all: bool, /// Only show agents of this category #[arg(long, value_name = "CATEGORY")] category: Option, /// Only show running agents #[arg(long)] running: bool, /// Sort by name, version or status (default: name) #[arg(long, value_name = "KEY")] sort: Option, }, /// Tail the log file of an agent (--follow for a live view) Logs { /// Agent name or alias agent: String, /// Number of lines to show (default: 20) #[arg(long, value_name = "N", default_value = "20")] lines: usize, /// Follow the log as it grows #[arg(long)] follow: bool, }, /// Open an agent's installation directory in the file manager Open { /// Agent name or alias agent: String, }, /// Supervise an agent and optionally restart it on crash Watch { /// Agent name or alias agent: String, /// Restart the agent automatically when it dies (with backoff) #[arg(long)] restart: bool, /// Desktop notification on every restart #[arg(long)] notify: bool, /// Check interval in seconds (default: 5) #[arg(long, value_name = "SECS", default_value = "5")] interval: u64, }, /// Live TUI dashboard: overview, activity, stats, sessions, projects Dashboard, /// Mark an agent as a favorite (star in list and status) Favorite { /// Agent name or alias agent: String, }, /// Remove the favorite star from an agent Unfavorite { /// Agent name or alias agent: String, }, /// Attach a free-form note to an agent (shown in 'am info'); /// 'am note ' without text prints the current note Note { /// Agent name or alias agent: String, /// Note text (several words are joined); empty prints the note #[arg(value_name = "TEXT...", num_args = 0..)] text: Vec, }, /// Add a personal tag to an agent Tag { /// Agent name or alias agent: String, /// One or more tags to add #[arg(value_name = "TAG", num_args = 1.., required = true)] words: Vec, }, /// Remove a personal tag from an agent Untag { /// Agent name or alias agent: String, /// Tag to remove tag: String, }, /// List personal tags (with counts), optionally for one agent Tags { /// Only show the tags of this agent agent: Option, }, /// Manage environment profiles (dev, prod, ...) #[command(subcommand)] Profile(ProfileCmd), /// Generate a man page for am (or for one command) Man { /// Command name (omit for the whole CLI) command: Option, /// Write the page(s) to this directory instead of stdout #[arg(long, value_name = "DIR")] output: Option, }, /// Set or show the active color theme (persisted for the next run) Theme { /// Theme name (omit to list available themes) name: Option, }, /// Internal: list agent names for shell completions (hidden) #[command(name = "_agents", hide = true)] Agents { /// Only installed agents (managed + external), aliases and groups #[arg(long)] installed: bool, }, /// Manage command aliases (cc -> claude-code) #[command(subcommand)] Alias(AliasCmd), /// Manage secrets in the OS keyring (never in plaintext config) #[command(subcommand)] Secret(SecretCmd), /// Audit the configuration: who changed what, when (issue #63) Audit, /// Register an agent as a system service (issue #55) #[command(subcommand)] Service(ServiceCmd), /// Schedule am commands (issue #56) #[command(subcommand)] Schedule(ScheduleCmd), /// List local models (ollama, llama.cpp, LM Studio) and prune unused ones Models(ModelsArgs), /// Manage remote catalogs: update the official one, add external ones #[command(subcommand)] Catalog(CatalogCmd), /// Manage the LLM provider registry: base URLs, models, default provider Providers(ProviderArgs), /// Suggest agents matching a query, boosted by real usage (issue #61) Suggest { /// Free-form request ("un agent pour du Python") #[arg(value_name = "QUERY", num_args = 1.., required = true)] words: Vec, }, /// Ask a natural-language request and get the matching am command(s) /// (issue #78): local rules first, optional LLM refinement, confirmation /// before execution. Ask { /// The request in natural language #[arg(value_name = "REQUEST", num_args = 1.., required = true)] query: Vec, /// Skip the confirmation prompt #[arg(long)] yes: bool, }, /// Start an agent (foreground by default, or detached with --background) Start(StartArgs), /// Stop a background agent (SIGTERM, then SIGKILL after the timeout) Stop { /// Agent name, alias, or group: agent: String, /// Kill immediately instead of terminating gracefully #[arg(long)] force: bool, /// Grace period in seconds before killing (default: from config, 5s) #[arg(long, value_name = "SECS")] timeout: Option, }, /// Restart an agent: stop, then start with the same options Restart { #[command(flatten)] start: StartArgs, /// Kill immediately instead of terminating gracefully #[arg(long)] force: bool, /// Grace period in seconds before killing (default: from config, 5s) #[arg(long, value_name = "SECS")] timeout: Option, }, /// Show the state of one agent, or of every installed agent Status { /// Agent name or alias (omit to show all installed agents) agent: Option, }, /// List the sessions of every agent (and of the interactive shell) Sessions { /// Only sessions of this agent agent: Option, /// Only sessions of this project #[arg(long, value_name = "PROJECT")] project: Option, /// Only sessions with this status (running, stopped, failed, interrupted) #[arg(long, value_name = "STATUS")] status: Option, /// Show one session in detail (summary + log excerpt) #[arg(long, value_name = "ID")] show: Option, /// Resume a finished session: relaunch the agent with its recorded arguments #[arg(long, value_name = "ID")] resume: Option, /// Export one session (metadata, commands, log excerpt) to a JSON file #[arg(long, value_name = "ID")] export: Option, /// Destination of the export (default: ./am-session-.json) #[arg(long, value_name = "FILE")] output: Option, /// Purge sessions finished more than N days ago (settings default 90) #[arg(long, value_name = "DAYS")] retention: Option, }, /// Show usage statistics computed from the event journal Stats { /// Agent name or alias (omit for the global view) agent: Option, /// Only events of the last period (7d, 30d, 90d, all) #[arg(long, value_name = "PERIOD")] period: Option, /// Show token usage and estimated spend per agent (issue #49) #[arg(long, action = ArgAction::SetTrue)] costs: bool, }, /// Real-time monitor of the managed processes (issue #50) Monitor { /// Refresh interval in seconds (default 2) #[arg(long, value_name = "SECONDS")] interval: Option, /// Print one JSON document per tick instead of the TUI #[arg(long, action = ArgAction::SetTrue)] json: bool, }, /// Local read-only web dashboard with charts (issue #54) Web(WebArgs), /// Push the state into the configured git repository (issue #66) Sync { /// Commit message (default: "am sync — state update") #[arg(long, value_name = "MESSAGE")] message: Option, }, /// Transfer the installation and state to another machine (issue #68) Migrate { /// Export the bundle (default) or import one #[arg(long, action = ArgAction::SetTrue)] export: bool, /// Bundle path for export (default agent-manager-migrate.amx) #[arg(long, value_name = "FILE")] output: Option, /// Bundle path to import #[arg(value_name = "BUNDLE")] bundle: Option, }, /// Show the most used agents (top 10) Top { /// Only events of the last period (7d, 30d, 90d, all) #[arg(long, value_name = "PERIOD")] period: Option, }, /// Write a markdown activity report Report { /// Cover the last 7 days #[arg(long, conflicts_with = "last_month")] last_week: bool, /// Cover the last 30 days #[arg(long)] last_month: bool, /// Output file (default: report-YYYYMMDD.md in the state directory) #[arg(long, value_name = "FILE")] output: Option, }, /// Show which agent works on which project Projects { /// Project name (omit to list every project) name: Option, }, /// One chronological view of every activity (journal + REPL history) Timeline { /// Only activity of this agent agent: Option, /// Only activity of this project #[arg(long, value_name = "PROJECT")] project: Option, /// Only activity at or after this date (YYYY-MM-DD or RFC 3339) #[arg(long, value_name = "DATE")] since: Option, /// Maximum number of entries (default: 50) #[arg(long, value_name = "N", default_value = "50")] limit: usize, }, /// Read the event journal: everything am did Log { /// Only events of this agent agent: Option, /// Only these event kinds (comma separated: start,stop,run,install,...) #[arg(long, value_name = "KINDS")] kind: Option, /// Only events at or after this date (YYYY-MM-DD or RFC 3339) #[arg(long, value_name = "DATE")] since: Option, /// Maximum number of events (default: 50) #[arg(long, value_name = "N", default_value = "50")] limit: usize, /// Follow the journal as new events arrive #[arg(long)] follow: bool, }, /// Install an agent and its dependencies Install { /// Agent name or alias agent: String, /// Select the install method (index, or type: npm, pip, uv, cargo, go, bun, curl, binary, git) #[arg(long, value_name = "METHOD")] method: Option, /// Reinstall even if already installed #[arg(long)] force: bool, /// Provider to configure (default: the agent's provider, else settings.default_provider) (issue #90) #[arg(long, value_name = "PROVIDER")] provider: Option, /// Model to configure (default: the provider's default model) (issue #90) #[arg(long, value_name = "MODEL")] model: Option, /// Skip the post-install provider configuration (issue #91) #[arg(long)] no_config: bool, }, /// Uninstall an agent (managed or external) and clean its files Uninstall { /// Agent name or alias agent: String, /// Also remove logs and the agent entry from the user config file #[arg(long)] purge: bool, }, /// Update an installed agent to the latest available version Update { /// Agent name or alias (required unless --all) agent: Option, /// Update every installed managed agent #[arg(long)] all: bool, /// Roll back to a pre-update backup instead of updating /// ("latest" or a backup id; "--rollback list" shows them) #[arg(long, num_args = 0..=1, default_missing_value = "latest")] rollback: Option, }, /// Search the catalog by keyword (name, description, category, tags) Search { /// Keyword to search for (case-insensitive substring) #[arg(value_name = "KEYWORD", required_unless_present = "tag")] keyword: Option, /// Restrict to this category #[arg(long, value_name = "CATEGORY")] category: Option, /// Only agents carrying this personal tag #[arg(long, value_name = "TAG")] tag: Option, }, /// Search the command history (am and shell commands) History { /// Only commands of this kind (am or shell) #[arg(long, value_name = "KIND")] kind: Option, /// Only commands run in this directory #[arg(long, value_name = "DIR")] cwd: Option, /// Full-text search in the command line #[arg(long, value_name = "TEXT")] search: Option, /// Only failed commands #[arg(long)] failed: bool, /// Only commands of this session #[arg(long, value_name = "ID")] session: Option, /// Maximum number of entries (default: 100) #[arg(long, value_name = "N", default_value = "100")] limit: usize, /// Re-run the Nth most recent entry (with confirmation) #[arg(long, value_name = "N")] rerun: Option, /// Range to export as a playbook: "12..25" (1-based, newest first) #[arg(value_name = "RANGE")] range: Option, /// Export the range as a playbook YAML file (issue #51) #[arg(long, value_name = "FILE")] save: Option, }, /// Replay a saved playbook step by step with confirmation (issue #51) Playbook { /// Playbook file (state_dir/playbooks/ when a bare name is given) #[arg(value_name = "FILE")] path: PathBuf, /// Variable values, repeatable: --var name=value #[arg(long, value_name = "NAME=VALUE")] var: Vec, }, /// Benchmark: run the same task on several agents and compare (issue #62) Lab(LabArgs), /// List the event plugins and test one (issue #75) Plugins(PluginsArgs), /// Show detailed information about one agent Info { /// Agent name or alias agent: String, }, /// Generate a local agent-manager.yaml for the current directory Init { /// Overwrite an existing agent-manager.yaml #[arg(long)] force: bool, /// Use a template (web, python, rust, cli) instead of stack detection; /// "list" shows the available templates #[arg(long, value_name = "TEMPLATE")] template: Option, }, /// Display help: the overview, a command, the command list, or a search Help { /// Command name (also accepts an agent name or alias); "commands" lists every command command: Option, /// Search through all help commands table #[arg(long, value_name = "TEXT")] find: Option, }, /// Display version and build information Version, /// Cheat sheet: the most useful commands, their key options, your /// most-used commands and a rotating tip of the day Tip { /// Show a single random tip instead of the whole page #[arg(long)] random: bool, }, /// Manage the configuration file #[command(subcommand)] Config(ConfigCmd), /// Check the environment: tools, config validity, paths, permissions Doctor { /// Attempt to repair problems (create directories, fix the state file) #[arg(long)] fix: bool, /// Re-run the checks every N seconds, alerting on failure (issue #59) #[arg(long, value_name = "SECONDS")] watch: Option, }, /// Run the agent command directly with the given arguments (no process management) Run { /// Agent name or alias agent: String, /// Local model to use for this run (validated against the local runtimes) #[arg(long, value_name = "MODEL")] model: Option, /// Provider to use for this run (issue #92): registry lookup, base_url + keyring token + model #[arg(long, value_name = "PROVIDER")] provider: Option, /// Run the agent inside a container (issue #58) #[arg(long)] container: bool, /// Arguments passed through to the agent command #[arg(trailing_var_arg = true, allow_hyphen_values = true, value_name = "ARGS...")] args: Vec, }, /// Generate a shell completion script Completion { /// Shell to generate completions for #[arg(value_enum, value_name = "SHELL")] shell: Shell, /// Add dynamic completion of the installed agents, aliases and /// groups (queried from 'am _agents' at Tab time) #[arg(long)] installed: bool, }, /// Update agent-manager itself from the latest release SelfUpdate { /// Only check whether a newer version exists #[arg(long)] check: bool, /// Download the new binary to this path instead of replacing the current one #[arg(long, value_name = "FILE")] to: Option, }, /// Remove agent-manager and everything it created from this machine /// /// Stops every managed background agent, then deletes the data, state and /// configuration directories, and finally the executable itself. Combine /// with --yes for a fully automated one-liner. SelfUninstall, /// Export the configuration and installation state (backup) Export { /// Output file (default: am-export.json) #[arg(long, value_name = "FILE", default_value = "am-export.json")] output: PathBuf, }, /// Import a previously exported configuration and state Import { /// File exported by the export command file: PathBuf, }, } /// Arguments shared by start and restart. #[derive(Args, Debug, Clone, Default)] pub struct StartArgs { /// Agent name, alias, or group: (defaults to the project's default agent) pub agent: Option, /// Run detached in the background; output goes to the agent log file #[arg(short = 'b', long, conflicts_with = "foreground", action = ArgAction::SetTrue)] pub background: bool, /// Run in the foreground, attached to this terminal (the default) #[arg(short = 'f', long, action = ArgAction::SetTrue)] pub foreground: bool, /// Extra arguments passed to the agent (repeatable; quoted strings are split on spaces) #[arg(long, value_name = "ARGS", num_args = 0.., action = ArgAction::Append)] pub args: Vec, /// Set an environment variable for the agent (repeatable): --env KEY=VALUE #[arg(long, value_name = "KEY=VALUE", action = ArgAction::Append)] pub env: Vec, /// Send a desktop notification once the agent has started #[arg(long, action = ArgAction::SetTrue)] pub notify: bool, /// Apply an environment profile (env + args, defined in the config) #[arg(long, value_name = "NAME")] pub profile: Option, /// Local model to use for this run (validated against the local runtimes) #[arg(long, value_name = "MODEL")] pub model: Option, /// Provider to use for this run (issue #92): registry lookup, base_url + keyring token + model #[arg(long, value_name = "PROVIDER")] pub provider: Option, /// Start every member of a group simultaneously (issue #57) #[arg(long, action = ArgAction::SetTrue)] pub parallel: bool, /// Run the agent inside a container (issue #58) #[arg(long, action = ArgAction::SetTrue)] pub container: bool, } /// Arguments of the models command: inventory by default, prune with --prune. #[derive(Args, Debug, Clone, Default)] pub struct ModelsArgs { /// Purge unused models instead of listing them #[arg(long)] pub prune: bool, /// Only list what would be pruned, delete nothing #[arg(long, conflicts_with = "yes")] pub dry_run: bool, /// Answer yes to the deletion prompt #[arg(long)] pub yes: bool, /// Consider a model unused after N days (default: 30) #[arg(long, value_name = "DAYS")] pub days: Option, } /// Arguments of the lab command (issue #62). #[derive(Args, Debug, Clone, Default)] pub struct LabArgs { /// Comma-separated agent names (or aliases) to benchmark #[arg(long, value_name = "AGENTS", required_unless_present = "list")] pub agents: Option, /// Task file (a bare name resolves under state_dir/lab/, .yaml appended) #[arg(long, value_name = "FILE", required_unless_present = "list")] pub task: Option, /// Run every agent concurrently instead of one after another #[arg(long, action = ArgAction::SetTrue)] pub parallel: bool, /// Override the task timeout in seconds #[arg(long, value_name = "SECS")] pub timeout: Option, /// List the task files of the lab directory #[arg(long, action = ArgAction::SetTrue)] pub list: bool, } /// Arguments of the plugins command (issue #75). #[derive(Args, Debug, Clone, Default)] pub struct PluginsArgs { /// Run one plugin against a synthetic event and print its response #[arg(long, value_name = "NAME")] pub test: Option, } /// Arguments of the web command (issue #54). #[derive(Args, Debug, Clone, Default)] pub struct WebArgs { /// Port to listen on (default 7878) #[arg(long, value_name = "PORT")] pub port: Option, /// Do not open the browser automatically #[arg(long, action = ArgAction::SetTrue)] pub no_open: bool, } /// Remote catalog subcommands (issues #60 #67). #[derive(Subcommand, Debug, Clone)] pub enum CatalogCmd { /// Fetch the official catalog, show the diff and register it as an include Update, /// Register an external catalog (same YAML format) by URL Add { /// Catalog URL (http, https or file) #[arg(value_name = "URL")] url: String, }, /// List the registered remote catalogs List, } /// LLM provider registry subcommands (issue #88). The subcommand is /// optional: `am providers` alone lists the registry. #[derive(Args, Debug, Clone, Default)] pub struct ProviderArgs { #[command(subcommand)] pub sub: Option, } #[derive(Subcommand, Debug, Clone)] pub enum ProvidersCmd { /// List the registered providers (the default one is starred) List, /// Show one provider in detail (base URL, models, default model) Show { /// Provider name name: String, }, /// Register a provider, or update an existing one Add { /// Provider name (lowercase slug, e.g. openai) name: String, /// Base URL of the provider API #[arg(long, value_name = "URL", required = true)] base_url: String, /// Model used by default for this provider #[arg(long, value_name = "MODEL")] model: Option, /// Comma-separated model list #[arg(long, value_name = "MODELS")] models: Option, }, /// Remove a provider from the registry Remove { /// Provider name name: String, }, /// Store the provider API token in the OS keyring (shared by every /// agent of this provider through the @secret cascade, issue #89) SetToken { /// Provider name name: String, /// Token value (never logged) #[arg(long, value_name = "VALUE")] value: String, }, /// Check whether a provider token exists (the value is never shown) Token { /// Provider name name: String, }, /// Set the default provider Default { /// Provider name name: String, }, } /// Environment profile subcommands (issue #40). #[derive(Subcommand, Debug, Clone)] pub enum ProfileCmd { /// List every defined profile List, /// Show one profile in detail (agent, env, args) Show { /// Profile name name: String, }, } /// 'am service' subcommands (issue #55). #[derive(Subcommand, Debug, Clone)] pub enum ServiceCmd { /// Install the service unit (systemd / launchd / Task Scheduler) Install { /// Agent name agent: String, /// Enable autostart (boot/logon) #[arg(long)] autostart: bool, }, /// Remove the service unit Uninstall { /// Agent name agent: String, }, /// Show whether the service is installed Status { /// Agent name agent: String, }, } /// 'am schedule' subcommands (issue #56). #[derive(Subcommand, Debug, Clone)] pub enum ScheduleCmd { /// Add a daily schedule (ex: 'am schedule add update --all') Add { /// Command to run, e.g. ["update", "--all"] #[arg(trailing_var_arg = true, allow_hyphen_values = true, value_name = "CMD...")] command: Vec, /// Time of day HH:MM (default 08:00) #[arg(long, value_name = "HH:MM")] at: Option, }, /// List the schedules List, /// Remove a schedule by id Remove { /// Schedule id ('am schedule list' shows them) id: String, }, /// Run a schedule now (invoked by the OS task, issue #56) Run { /// Schedule id id: String, }, } #[derive(Subcommand, Debug, Clone)] pub enum SecretCmd { /// Store a secret for an agent (or a provider, issue #89) Set { /// Secret name (the environment variable name) name: String, /// Agent it belongs to (required unless --provider is given) #[arg(long, value_name = "AGENT")] agent: Option, /// Provider it belongs to (shared by every agent of this provider) #[arg(long, value_name = "PROVIDER")] provider: Option, /// Secret value (prefer --value over shell history; never logged) #[arg(long, value_name = "VALUE")] value: String, }, /// Remove a secret Unset { /// Secret name name: String, /// Agent it belongs to (required unless --provider is given) #[arg(long, value_name = "AGENT")] agent: Option, /// Provider it belongs to #[arg(long, value_name = "PROVIDER")] provider: Option, }, /// List stored secret names (values are never shown) List, } /// Alias management subcommands. #[derive(Subcommand, Debug, Clone)] pub enum AliasCmd { /// Add an alias (am alias add cc claude-code) Add { /// Short name of the alias name: String, /// Agent name (or another alias) it points to target: String, }, /// Remove an alias Remove { /// Alias to remove name: String, }, /// List every alias List, } /// Configuration management subcommands. #[derive(Subcommand, Debug, Clone)] pub enum ConfigCmd { /// Print the effective (merged) configuration as YAML Show, /// Print the location of the configuration file(s) Path, /// Open the user configuration file in your editor Edit, /// Validate the configuration file(s) Validate, /// Add another agent definition file to the user configuration Add { /// YAML (or JSON) file defining agents to include file: PathBuf, }, /// Set one configuration key without an editor (config set settings.default_shell pwsh) Set { /// Dotted key path, e.g. settings.default_shell key: String, /// New value (booleans and integers keep their type) value: String, }, }