objectiveai-cli 2.1.1

ObjectiveAI command-line interface and embeddable library
use futures::StreamExt;
use tokio::io::AsyncWriteExt;

#[tokio::main]
async fn main() {
    let _ = dotenv::dotenv();

    // Windows-only: clear `HANDLE_FLAG_INHERIT` on this process's
    // stdio handles before any child spawns. See
    // `objectiveai_cli::clear_stdio_inheritance` for the
    // grandchild-stdio-leak background. Still relevant: even
    // though there's no more `instance` subprocess, the
    // `stream=false` self-respawn for agents-spawn / functions-
    // execute spawns a detached cli child via `BinaryExecutor`,
    // and plugin RMCP servers spawned downstream from there
    // would inherit our stdio handles otherwise.
    #[cfg(windows)]
    objectiveai_cli::clear_stdio_inheritance();

    let args: Vec<String> = std::env::args().collect();
    let code = run_command(args).await;
    std::process::exit(code);
}

async fn run_command(args: Vec<String>) -> i32 {
    let mut stdout = tokio::io::stdout();
    match objectiveai_cli::run(args, None).await {
        Ok(mut stream) => {
            let mut last_tool_exit: Option<i32> = None;
            while let Some(item) = stream.next().await {
                match item {
                    Ok(response) => {
                        write_line(&mut stdout, &response).await;
                    }
                    Err(e) => {
                        // `ToolExit(code)` carries the exit code the
                        // upstream tool exited with — propagate it
                        // even though it's surfaced as an Err item.
                        if let objectiveai_cli::error::Error::ToolExit(code) = &e {
                            last_tool_exit = Some(*code);
                        }
                        write_error_line(&mut stdout, &e, None).await;
                    }
                }
            }
            last_tool_exit.unwrap_or(0)
        }
        Err(e) => {
            // `--help` / `--version` / no-subcommand → render the
            // clap output as a single informational line. Exit 0 so
            // scripts pipelining `--help` aren't penalised.
            if let objectiveai_cli::error::Error::ClapParse(ref clap_err) = e {
                if objectiveai_cli::is_informational(clap_err) {
                    write_help_line(&mut stdout, &clap_err.to_string()).await;
                    return 0;
                }
            }
            write_error_line(&mut stdout, &e, Some(true)).await;
            match e {
                objectiveai_cli::error::Error::ToolExit(code) => code,
                _ => 1,
            }
        }
    }
}

async fn write_line<T: serde::Serialize>(
    stdout: &mut tokio::io::Stdout,
    value: &T,
) {
    let line = match serde_json::to_string(value) {
        Ok(s) => s,
        Err(e) => format!(
            r#"{{"type":"error","fatal":false,"message":"serialize error: {e}"}}"#
        ),
    };
    let _ = stdout.write_all(line.as_bytes()).await;
    let _ = stdout.write_all(b"\n").await;
    let _ = stdout.flush().await;
}

async fn write_error_line(
    stdout: &mut tokio::io::Stdout,
    e: &objectiveai_cli::error::Error,
    fatal: Option<bool>,
) {
    let payload = objectiveai_sdk::cli::Error {
        r#type: objectiveai_sdk::cli::ErrorType::Error,
        level: Some(objectiveai_sdk::cli::Level::Error),
        fatal,
        message: e.output_message(),
    };
    write_line(stdout, &payload).await;
}

async fn write_help_line(stdout: &mut tokio::io::Stdout, help: &str) {
    let payload = serde_json::json!({
        "type": "help",
        "help": help,
    });
    write_line(stdout, &payload).await;
}