unified-agent-api-gemini-cli 0.3.5

Async wrapper around the Gemini CLI for headless stream-json flows
Documentation
use std::{
    env, fs,
    io::{self, Write},
    thread,
    time::Duration,
};

const INIT: &str = include_str!("../../tests/fixtures/stream_json/v1/init.jsonl");
const MESSAGE: &str = include_str!("../../tests/fixtures/stream_json/v1/message.jsonl");
const TOOL_USE: &str = include_str!("../../tests/fixtures/stream_json/v1/tool_use.jsonl");
const TOOL_RESULT: &str = include_str!("../../tests/fixtures/stream_json/v1/tool_result.jsonl");
const RESULT_SUCCESS: &str =
    include_str!("../../tests/fixtures/stream_json/v1/result_success.jsonl");
const RESULT_ERROR: &str = include_str!("../../tests/fixtures/stream_json/v1/result_error.jsonl");

fn first_nonempty_line(text: &str) -> &str {
    text.lines()
        .find(|line| !line.chars().all(|ch| ch.is_whitespace()))
        .expect("fixture contains a non-empty line")
}

fn write_line(out: &mut impl Write, line: &str) -> io::Result<()> {
    out.write_all(line.as_bytes())?;
    out.flush()?;
    Ok(())
}

fn capture_invocation() {
    let capture_path = match env::var("FAKE_GEMINI_CAPTURE") {
        Ok(path) => path,
        Err(_) => return,
    };

    let payload = serde_json::json!({
        "argv": env::args().skip(1).collect::<Vec<_>>(),
        "cwd": env::current_dir().ok().map(|path| path.display().to_string()),
    });
    fs::write(
        capture_path,
        serde_json::to_vec_pretty(&payload).expect("serialize capture"),
    )
    .expect("write capture");
}

fn main() -> io::Result<()> {
    capture_invocation();

    let scenario =
        env::var("FAKE_GEMINI_SCENARIO").unwrap_or_else(|_| "three_events_delayed".to_string());

    let init = first_nonempty_line(INIT);
    let message = first_nonempty_line(MESSAGE);
    let tool_use = first_nonempty_line(TOOL_USE);
    let tool_result = first_nonempty_line(TOOL_RESULT);
    let result_success = first_nonempty_line(RESULT_SUCCESS);
    let result_error = first_nonempty_line(RESULT_ERROR);
    let mut out = io::stdout().lock();

    match scenario.as_str() {
        "capture_args" => {
            write_line(&mut out, &format!("{init}\n"))?;
            write_line(&mut out, &format!("{message}\n"))?;
            write_line(&mut out, &format!("{result_success}\n"))?;
        }
        "crlf_blank_lines" => {
            write_line(&mut out, "\r\n")?;
            write_line(&mut out, "   \r\n")?;
            write_line(&mut out, &format!("{init}\r\n"))?;
            write_line(&mut out, "\r\n")?;
            write_line(&mut out, &format!("{message}\r\n"))?;
            write_line(&mut out, &format!("{result_success}\r\n"))?;
        }
        "parse_error_redaction" => {
            let secret = "VERY_SECRET_SHOULD_NOT_APPEAR";
            write_line(&mut out, &format!("not json {secret}\n"))?;
            write_line(&mut out, &format!("{init}\n"))?;
            write_line(&mut out, &format!("{message}\n"))?;
            write_line(&mut out, &format!("{result_success}\n"))?;
        }
        "tool_roundtrip" => {
            write_line(&mut out, &format!("{init}\n"))?;
            write_line(&mut out, &format!("{tool_use}\n"))?;
            write_line(&mut out, &format!("{tool_result}\n"))?;
            write_line(
                &mut out,
                "{\"type\":\"message\",\"timestamp\":\"2026-04-21T12:00:03Z\",\"role\":\"assistant\",\"content\":\"done\",\"delta\":true}\n",
            )?;
            write_line(&mut out, &format!("{result_success}\n"))?;
        }
        "slow_until_killed" => {
            write_line(&mut out, &format!("{init}\n"))?;
            thread::sleep(Duration::from_secs(30));
        }
        "turn_limit_exceeded" => {
            write_line(&mut out, &format!("{init}\n"))?;
            write_line(
                &mut out,
                "{\"type\":\"message\",\"timestamp\":\"2026-04-21T12:00:01Z\",\"role\":\"assistant\",\"content\":\"partial\",\"delta\":true}\n",
            )?;
            write_line(&mut out, &format!("{result_error}\n"))?;
            std::process::exit(53);
        }
        "invalid_input" => {
            write_line(
                &mut out,
                "{\"type\":\"result\",\"timestamp\":\"2026-04-21T12:00:02Z\",\"status\":\"error\",\"error\":{\"type\":\"input_error\",\"message\":\"Prompt rejected\"}}\n",
            )?;
            std::process::exit(42);
        }
        _ => {
            write_line(&mut out, &format!("{init}\n"))?;
            thread::sleep(Duration::from_millis(150));
            write_line(&mut out, &format!("{message}\n"))?;
            thread::sleep(Duration::from_millis(150));
            write_line(&mut out, &format!("{result_success}\n"))?;
        }
    }

    Ok(())
}