agent-first-http 0.7.3

Give your AI agent its own private browser — so it reads the real page, past logins and bot walls, without ever touching yours.
Documentation
//! CLI layer. Parses arguments, calls into the SDK, formats output.

pub mod args;
pub mod cmd;
pub mod output;

use std::process::ExitCode;

use crate::shared::error::Error;

/// Binary entry point. Always returns `ExitCode::SUCCESS` on a structured
/// response (success *or* error); the response itself carries the
/// success/failure shape on stdout.
pub fn run() -> ExitCode {
    let _stream_redirect =
        match agent_first_data::stream_redirect::install_from_raw_args(std::env::args()) {
            Ok(redirect) => redirect,
            Err(err) => {
                emit_bootstrap_error(&err.to_string());
                return ExitCode::from(2);
            }
        };

    // Install the process-wide rustls crypto provider before anything builds a
    // reqwest/TLS client. On the inline-fetch path the host-side CDP fetch runs
    // before the SDK client's own guard, so without this `afhttp fetch` panics
    // with "no rustls crypto provider is configured".
    crate::host::bootstrap::install_rustls_provider();

    // Version/help are rendered without spinning up the async runtime so
    // machine-readable requests do not fall through to clap's plain text exits.
    if let Some(code) = maybe_render_version() {
        return code;
    }
    // Help is rendered without spinning up the async runtime so
    // `--help --recursive --output markdown` can feed generated docs.
    if let Some(code) = maybe_render_help() {
        return code;
    }
    // The fetch/host pipeline polls a deeply nested future chain (inline host
    // launch → CDP handshake → …). Polling that depth builds a deep synchronous
    // call stack that overflows Windows' default 1 MiB main-thread stack
    // (Linux/macOS default to 8 MiB). Run the runtime on a thread with a generous
    // stack so behavior is uniform across platforms.
    match std::thread::Builder::new()
        .name("afhttp-main".to_string())
        .stack_size(16 * 1024 * 1024)
        .spawn(run_blocking)
    {
        Ok(handle) => match handle.join() {
            Ok(code) => code,
            Err(_) => {
                emit_bootstrap_error("afhttp worker thread panicked");
                ExitCode::from(2)
            }
        },
        Err(e) => {
            emit_bootstrap_error(&format!("spawn worker thread: {e}"));
            ExitCode::from(2)
        }
    }
}

/// Build the tokio runtime and drive the dispatched command to completion.
/// Runs on a dedicated large-stack thread spawned by `run`.
fn run_blocking() -> ExitCode {
    let rt = match tokio::runtime::Builder::new_multi_thread()
        .enable_all()
        .thread_stack_size(16 * 1024 * 1024)
        .build()
    {
        Ok(rt) => rt,
        Err(e) => {
            emit_bootstrap_error(&format!("tokio runtime: {e}"));
            return ExitCode::from(2);
        }
    };
    let exit = rt.block_on(async {
        match args::parse() {
            Ok(parsed) => dispatch(parsed).await,
            Err(err) => {
                emit_cli_error(&err);
                Err(err)
            }
        }
    });
    match exit {
        Ok(()) => ExitCode::SUCCESS,
        Err(_) => ExitCode::from(1),
    }
}

/// Render version and return an exit code, or `None` to continue normal parsing.
fn maybe_render_version() -> Option<ExitCode> {
    use std::io::Write;

    let raw: Vec<String> = std::env::args().collect();
    let mut handle = std::io::stdout();
    match agent_first_data::cli_handle_version_or_continue(
        &raw,
        "afhttp",
        env!("CARGO_PKG_VERSION"),
        &agent_first_data::VersionConfig::conventional_default().with_protocol_v1(),
    ) {
        Ok(Some(version)) => {
            let _ = write!(handle, "{version}");
            Some(ExitCode::SUCCESS)
        }
        Ok(None) => None,
        Err(err) => {
            let err = Error::new(
                crate::shared::error::ErrorCode::InvalidArgument,
                err.to_string(),
            );
            let _ = crate::shared::afdata::emit_error(&mut handle, &err);
            Some(ExitCode::from(2))
        }
    }
}

/// Render help and return an exit code, or `None` to continue normal parsing.
fn maybe_render_help() -> Option<ExitCode> {
    use clap::CommandFactory;
    use std::io::Write;

    let raw: Vec<String> = std::env::args().collect();
    let mut handle = std::io::stdout();
    match agent_first_data::cli_handle_help_or_continue(
        &raw,
        &args::Cli::command(),
        &agent_first_data::HelpConfig::human_cli_default().with_protocol_v1(),
    ) {
        Ok(Some(help)) => {
            let _ = write!(handle, "{help}");
            Some(ExitCode::SUCCESS)
        }
        Ok(None) => None,
        Err(err) => {
            let err = Error::new(
                crate::shared::error::ErrorCode::InvalidArgument,
                err.to_string(),
            );
            let _ = crate::shared::afdata::emit_error(&mut handle, &err);
            Some(ExitCode::from(2))
        }
    }
}

async fn dispatch(parsed: args::Parsed) -> Result<(), Error> {
    let command = match parsed.command {
        args::Command::Fetch(a) => {
            // `fetch` owns error emission so it can attach the fetch-local trace
            // without adding trace fields to the global Error type.
            return cmd::fetch::run(*a).await;
        }
        command => command,
    };
    let res = match command {
        args::Command::Host(a) => cmd::host::run(a).await,
        args::Command::Fetch(_) => unreachable!("fetch handled above"),
        args::Command::Upload(a) => cmd::upload::run(a).await,
        args::Command::Cdp(a) => cmd::cdp::run(a).await,
        args::Command::Panel(a) => cmd::panel::run(a).await,
        args::Command::Health(a) => cmd::health::run(a).await,
        args::Command::Capabilities(a) => cmd::capabilities::run(a).await,
        args::Command::Profile(a) => cmd::profile::run(a).await,
        args::Command::Tabs(a) => cmd::tabs::run(a).await,
        args::Command::Skill(a) => cmd::skill::run(a).await,
        args::Command::Container(a) => cmd::container::run(a).await,
    };
    if let Err(ref e) = res {
        emit_cli_error(e);
    }
    res
}

fn emit_cli_error(err: &Error) {
    let stdout = std::io::stdout();
    let mut handle = stdout.lock();
    let _ = crate::shared::afdata::emit_error(&mut handle, err);
}

fn emit_bootstrap_error(msg: &str) {
    // Fallback path used before the runtime exists. Stays on stdout to
    // match the AFDATA protocol channel rule (clippy bans stderr usage).
    let stdout = std::io::stdout();
    let mut handle = stdout.lock();
    let err = Error::new(crate::shared::error::ErrorCode::InternalError, msg);
    let _ = crate::shared::afdata::emit_error(&mut handle, &err);
}