anyagent 0.0.2

One Rust interface to the coding agents installed on a machine.
Documentation
//! Per-agent facts, kept as data.

use crate::agent::AuthKind;

/// Everything discovery and the adapters need to know about one agent.
pub(crate) struct AgentProfile {
    pub id: &'static str,
    pub name: &'static str,
    /// Binary the user installs (`claude`, `codex`, `hermes`).
    pub cli: &'static str,
    /// Env var that overrides executable resolution (tests, custom installs).
    pub executable_env: &'static str,
    /// Env var that relocates the agent's config directory.
    pub config_home_env: Option<&'static str>,
    /// Which adapter drives it and how to put the CLI in protocol mode.
    pub connection: Connection,
    /// API-key env vars the agent documents; each is an `EnvVar` login
    /// method when it reports logged out.
    pub api_key_env: &'static [&'static str],
    /// `Some(kind)` when a successful session open proves the agent is logged
    /// in with this kind (probed: the agent refuses to open logged out).
    /// `None` when an open proves nothing (opencode serves free models
    /// logged out) or the adapter reads auth from its own wire.
    pub open_auth_kind: Option<AuthKind>,
    /// Fingerprints of the agent's own logged-out error, matched against a
    /// failed open's message and stderr (probed per agent; empty = the agent
    /// uses the ACP auth-required code, which needs no hint).
    pub auth_error_hints: &'static [&'static str],
    /// Args after the executable that start the agent's own login flow;
    /// empty when the agent only logs in interactively.
    pub login_args: &'static [&'static str],
    /// Shown in `MissingAgent` when the agent is not found.
    pub install_hint: &'static str,
    /// Extra install locations searched last. `~`-relative unless absolute.
    pub extra_paths: &'static [&'static str],
    /// A richer, separately installed runtime for the same agent, preferred
    /// when found. Without it the base CLI is opened and the installation
    /// names the upgrade as missing.
    pub upgrade: Option<Upgrade>,
}

/// A second executable that drives the same agent over ACP with more
/// capabilities than its CLI (Antigravity's ACP server).
pub(crate) struct Upgrade {
    pub name: &'static str,
    pub cli: &'static str,
    pub acp_args: &'static [&'static str],
    pub extra_paths: &'static [&'static str],
    pub install_hint: &'static str,
    /// ACP auth method to call when `session/new` says the server has no
    /// login selected; it adopts the base CLI's login without a browser.
    pub acp_auth_method: Option<&'static str>,
}

pub(crate) enum Connection {
    Acp { args: &'static [&'static str] },
    Native(NativeKind),
}

pub(crate) enum NativeKind {
    // Current native supported agents.
    Claude,
    Codex,
    /// Antigravity's `agy` CLI: its own stream-json event dialect
    /// (`--input-format=stream-json`, probed 1.1.24 on 2026-09-02).
    Antigravity,
    /// The pi RPC wire (`--mode rpc`).
    Pi,
    /// opencode's HTTP + SSE server (`opencode serve`), legacy engine.
    Opencode,
}

/// The supported agents. A profile ships only with launch flags verified
/// against a real install; guessed wire flags do not ship.
pub(crate) static PROFILES: &[AgentProfile] = &[
    AgentProfile {
        id: "claude",
        name: "Claude Code",
        cli: "claude",
        executable_env: "ANYAGENT_CLAUDE_BIN",
        config_home_env: Some("CLAUDE_CONFIG_DIR"),
        connection: Connection::Native(NativeKind::Claude),
        api_key_env: &["ANTHROPIC_API_KEY"],
        open_auth_kind: None,
        auth_error_hints: &[],
        login_args: &["auth", "login"],
        install_hint: "npm install -g @anthropic-ai/claude-code",
        extra_paths: &[".claude/local", ".local/bin"],
        upgrade: None,
    },
    AgentProfile {
        id: "codex",
        name: "Codex",
        cli: "codex",
        executable_env: "ANYAGENT_CODEX_BIN",
        config_home_env: Some("CODEX_HOME"),
        connection: Connection::Native(NativeKind::Codex),
        // app-server 0.147.0 ignores `OPENAI_API_KEY` (probed 2026-08-27);
        // auth comes only from `codex login`.
        api_key_env: &[],
        open_auth_kind: None,
        auth_error_hints: &[],
        login_args: &["login"],
        install_hint: "npm install -g @openai/codex",
        extra_paths: &[],
        upgrade: None,
    },
    // Gemini CLI is deprecated upstream (personal OAuth sunset, users moved
    // to Antigravity); its profile was replaced 2026-08-23.
    AgentProfile {
        id: "antigravity",
        name: "Antigravity",
        cli: "agy",
        executable_env: "ANYAGENT_ANTIGRAVITY_BIN",
        config_home_env: None,
        connection: Connection::Native(NativeKind::Antigravity),
        api_key_env: &[],
        // Logged out, `agy` exits before its `init` frame (probed 1.1.24).
        open_auth_kind: Some(AuthKind::Subscription),
        auth_error_hints: &["authentication required", "authentication failed"],
        // Login is the TUI itself: the bare executable is the command.
        login_args: &[],
        install_hint: "install Antigravity from https://antigravity.google, then run `agy install`",
        extra_paths: &[".local/bin"],
        // Google's ACP server (registry id `antigravity-acp`): permissions,
        // questions, in-process cancel, and live mode and model switches
        // that the CLI's headless wire lacks. No steer: a second prompt
        // mid-turn breaks its agent connection (probed 2026-09-07), so
        // those queue. Gemini models only.
        upgrade: Some(Upgrade {
            name: "Antigravity ACP server",
            cli: "agy_acp_server.par",
            acp_args: &[],
            extra_paths: &[".local/agy-acp-server", ".local/bin"],
            install_hint: "download the `antigravity-acp` archive for your platform from \
                 https://github.com/agentclientprotocol/registry/blob/main/antigravity-acp/agent.json \
                 and unpack it into ~/.local/agy-acp-server",
            // The server keeps its own auth choice (`auth.type` in
            // ~/.gemini/antigravity-acp/settings.json). Without one, a
            // single in-protocol `authenticate` adopts the `agy` login in
            // ~2 s with no browser (probed 2026-09-07).
            acp_auth_method: Some("oauth-personal"),
        }),
    },
    AgentProfile {
        id: "cursor",
        name: "Cursor CLI",
        cli: "cursor-agent",
        executable_env: "ANYAGENT_CURSOR_BIN",
        config_home_env: None,
        connection: Connection::Acp { args: &["acp"] },
        // Cursor's documented headless alternative to `cursor-agent login`.
        api_key_env: &["CURSOR_API_KEY"],
        open_auth_kind: Some(AuthKind::Subscription),
        // Every session call logged out fails with the auth code and this
        // text (read from the 2026.09.02 bundle).
        auth_error_hints: &["Authentication required"],
        login_args: &["login"],
        install_hint: "curl https://cursor.com/install -fsS | bash",
        extra_paths: &[".local/bin"],
        upgrade: None,
    },
    AgentProfile {
        id: "grok",
        name: "Grok",
        cli: "grok",
        executable_env: "ANYAGENT_GROK_BIN",
        config_home_env: None,
        // Verified against grok 1.0.4: top-level `--no-auto-update` skips a
        // multi-second update check; `--no-leader` starts a fresh agent
        // instead of attaching to a leader process that may be wedged.
        connection: Connection::Acp {
            args: &["--no-auto-update", "agent", "--no-leader", "stdio"],
        },
        // The API-key alternative to the OAuth login.
        api_key_env: &["XAI_API_KEY"],
        open_auth_kind: Some(AuthKind::Subscription),
        // Grok's -32000 carries no runnable method (probed 1.0.x: "no auth
        // method id provided"); login is its own TUI, so the fallback
        // command is the bare executable.
        auth_error_hints: &["Authentication required"],
        login_args: &[],
        install_hint: "npm install -g @xai-official/grok \
             (or `curl -fsSL https://x.ai/cli/install.sh | bash`)",
        extra_paths: &[".local/bin", ".grok/bin", ".npm-global/bin"],
        upgrade: None,
    },
    AgentProfile {
        id: "hermes",
        name: "Hermes Agent",
        cli: "hermes",
        executable_env: "ANYAGENT_HERMES_BIN",
        config_home_env: None,
        connection: Connection::Acp { args: &["acp"] },
        api_key_env: &[],
        open_auth_kind: Some(AuthKind::ApiKey),
        auth_error_hints: &["No LLM provider configured"],
        login_args: &["login"],
        install_hint: "see https://github.com/NousResearch/hermes-agent",
        extra_paths: &[".local/bin"],
        upgrade: None,
    },
    AgentProfile {
        id: "opencode",
        name: "opencode",
        cli: "opencode",
        executable_env: "ANYAGENT_OPENCODE_BIN",
        // Data home; `opencode auth login` writes auth.json here.
        // `XDG_DATA_HOME` relocates opencode's db/state but not its login
        // (auth stays in the shared keychain/auth.json), so it does not
        // isolate accounts — left unset, an isolation request fails typed.
        config_home_env: None,
        connection: Connection::Native(NativeKind::Opencode),
        api_key_env: &[],
        open_auth_kind: None,
        auth_error_hints: &[],
        login_args: &["auth", "login"],
        install_hint: "brew install sst/tap/opencode",
        extra_paths: &[],
        upgrade: None,
    },
    AgentProfile {
        id: "kiro",
        name: "Kiro CLI",
        cli: "kiro-cli",
        executable_env: "ANYAGENT_KIRO_BIN",
        config_home_env: None,
        connection: Connection::Acp { args: &["acp"] },
        api_key_env: &[],
        open_auth_kind: Some(AuthKind::Subscription),
        auth_error_hints: &["not logged in"],
        login_args: &["login"],
        install_hint: "install Kiro CLI from https://kiro.dev",
        extra_paths: &[".local/bin"],
        upgrade: None,
    },
    AgentProfile {
        id: "pi",
        name: "pi",
        cli: "pi",
        executable_env: "ANYAGENT_PI_BIN",
        // The agent dir itself, which is what `PI_CODING_AGENT_DIR` replaces
        // (documented in `pi --help`, probed 2026-08-30).
        config_home_env: Some("PI_CODING_AGENT_DIR"),
        connection: Connection::Native(NativeKind::Pi),
        // The adapter answers auth for real from `pi auth check`; these are
        // the API-key alternatives pi documents.
        api_key_env: &["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "OPENROUTER_API_KEY"],
        // A session opens fine with no provider configured, so an open proves
        // nothing (probed 2026-08-30).
        open_auth_kind: None,
        auth_error_hints: &[],
        // Login is the TUI's own `/login <provider>`; there is no CLI verb.
        login_args: &[],
        install_hint: "npm install -g @earendil-works/pi-coding-agent",
        extra_paths: &[".bun/bin", ".local/bin"],
        upgrade: None,
    },
    // oh-my-pi (`omp`) speaks the same pi RPC wire and would be one more
    // profile on `NativeKind::Pi`; deferred to its own ticket.
    AgentProfile {
        id: "qwen",
        name: "Qwen Code",
        cli: "qwen",
        executable_env: "ANYAGENT_QWEN_BIN",
        config_home_env: None,
        connection: Connection::Acp {
            args: &["--experimental-acp"],
        },
        // Advertised by qwen's own auth method (probed 0.22.0).
        api_key_env: &["OPENAI_API_KEY"],
        open_auth_kind: Some(AuthKind::Subscription),
        auth_error_hints: &[],
        login_args: &[],
        install_hint: "npm install -g @qwen-code/qwen-code",
        extra_paths: &[],
        upgrade: None,
    },
];

/// The environment variable that relocates an agent's config home, if it has
/// one. `None` for an agent outside the catalog (an ad-hoc ACP install) or one
/// with no such variable.
pub(crate) fn config_home_env(id: &str) -> Option<&'static str> {
    PROFILES
        .iter()
        .find(|p| p.id == id)
        .and_then(|p| p.config_home_env)
}