gemini-cli-sdk 0.1.0

Rust SDK wrapping Google's Gemini CLI as a subprocess via JSON-RPC 2.0
Documentation
//! Rust SDK wrapping Google's Gemini CLI as a subprocess via JSON-RPC 2.0.
//!
//! # Architecture
//!
//! The SDK communicates with the `gemini` binary using the `--experimental-acp`
//! JSON-RPC 2.0 mode. Each [`Client`] manages a single subprocess session.
//! Responses are translated from raw wire types to the public [`Message`] enum.
//!
//! # Quick Start — one-shot query
//!
//! ```rust,no_run
//! #[tokio::main]
//! async fn main() -> gemini_cli_sdk::Result<()> {
//!     let messages = gemini_cli_sdk::query("Explain quantum computing in one sentence").await?;
//!     for msg in messages {
//!         if let Some(text) = msg.assistant_text() {
//!             println!("{text}");
//!         }
//!     }
//!     Ok(())
//! }
//! ```
//!
//! # Quick Start — stateful multi-turn session
//!
//! ```rust,no_run
//! use gemini_cli_sdk::{Client, ClientConfig};
//!
//! #[tokio::main]
//! async fn main() -> gemini_cli_sdk::Result<()> {
//!     let config = ClientConfig::builder()
//!         .prompt("Explain quantum computing")
//!         .build();
//!     let mut client = Client::new(config)?;
//!     let _info = client.connect().await?;
//!     // send() returns a Stream; consume it then close.
//!     client.close().await?;
//!     Ok(())
//! }
//! ```

// ── Module declarations ───────────────────────────────────────────────────────
//
// Modules referenced in existing doctests via `gemini_cli_sdk::<module>::` must
// be `pub` so rustdoc can compile those examples.

pub mod callback;
mod client;
pub mod config;
pub mod discovery;
pub mod errors;
mod hooks;
mod jsonrpc;
pub mod mcp;
pub mod permissions;
mod translate;
pub mod transport;
pub mod types;
mod wire;

#[cfg(feature = "testing")]
pub mod testing;

// ── Core re-exports ───────────────────────────────────────────────────────────

pub use client::Client;
pub use errors::{Error, Result};

// ── Config re-exports ─────────────────────────────────────────────────────────

pub use config::{AuthMethod, ClientConfig, PermissionMode, SystemPrompt};

// ── Callback re-exports ───────────────────────────────────────────────────────

pub use callback::{sync_callback, tracing_callback, MessageCallback};

// ── Discovery re-exports ──────────────────────────────────────────────────────

pub use discovery::{check_cli_version, find_cli, version_satisfies, MIN_CLI_VERSION};

// ── Hooks re-exports ──────────────────────────────────────────────────────────

pub use hooks::{HookCallback, HookContext, HookDecision, HookEvent, HookInput, HookMatcher, HookOutput};

// ── MCP re-exports ────────────────────────────────────────────────────────────

pub use mcp::{McpServerConfig, McpServers};

// ── Permissions re-exports ────────────────────────────────────────────────────

pub use permissions::{
    CanUseToolCallback, PermissionContext, PermissionDecision, PermissionOptionInfo,
    ToolLocationInfo,
};

// ── Transport re-exports ──────────────────────────────────────────────────────

pub use transport::{GeminiTransport, Transport};

// ── Message type re-exports ───────────────────────────────────────────────────

pub use types::messages::{
    AssistantMessage, AssistantMessageInner, McpServerStatus, Message, PlanEntry, ResultMessage,
    SessionInfo, StreamEvent, SystemMessage, Usage, UserMessage, UserMessageInner,
};

// ── Content type re-exports ───────────────────────────────────────────────────

pub use types::content::{
    Base64ImageSource, ContentBlock, ImageBlock, ImageSource, TextBlock, ThinkingBlock,
    ToolResultBlock, ToolResultContent, ToolUseBlock, UrlImageSource, UserContent,
};

// ── Free-function helpers ─────────────────────────────────────────────────────

/// Run a one-shot query with a plain-text prompt, collecting all messages.
///
/// Creates a temporary [`Client`] with default configuration, connects it,
/// sends the prompt, collects the full response into a [`Vec`], and closes
/// the session. Equivalent to calling [`query_with_content`] with a single
/// [`UserContent::text`] block.
///
/// # Errors
///
/// Propagates all errors from [`Client::new`], [`Client::connect`],
/// [`Client::send`], and [`Client::close`].
///
/// # Example
///
/// ```rust,no_run
/// #[tokio::main]
/// async fn main() -> gemini_cli_sdk::Result<()> {
///     let msgs = gemini_cli_sdk::query("What is 2+2?").await?;
///     for m in msgs {
///         if let Some(t) = m.assistant_text() {
///             println!("{t}");
///         }
///     }
///     Ok(())
/// }
/// ```
pub async fn query(prompt: &str) -> Result<Vec<Message>> {
    query_with_content(prompt, vec![UserContent::text(prompt)]).await
}

/// Run a one-shot query with structured content, collecting all messages.
///
/// Identical to [`query`] but accepts a [`Vec<UserContent>`] instead of a
/// plain string, allowing images and mixed content to be sent.
///
/// The `prompt` parameter is used only to construct the [`ClientConfig`];
/// the actual content sent to the session is taken from `content`.
///
/// # Errors
///
/// Propagates all errors from [`Client::new`], [`Client::connect`],
/// [`Client::send_content`], and [`Client::close`].
///
/// # Example
///
/// ```rust,no_run
/// use gemini_cli_sdk::{UserContent, query_with_content};
///
/// #[tokio::main]
/// async fn main() -> gemini_cli_sdk::Result<()> {
///     let content = vec![
///         UserContent::text("Describe this image:"),
///         UserContent::image_url("https://example.com/img.png"),
///     ];
///     let msgs = query_with_content("Describe this image:", content).await?;
///     println!("{} messages", msgs.len());
///     Ok(())
/// }
/// ```
pub async fn query_with_content(
    prompt: &str,
    content: Vec<UserContent>,
) -> Result<Vec<Message>> {
    use tokio_stream::StreamExt as _;

    let config = ClientConfig::builder().prompt(prompt).build();
    let mut client = Client::new(config)?;
    client.connect().await?;

    // Collect inside a block so `stream` — which borrows `client` — is
    // dropped before the mutable `close()` call.
    let messages: Vec<Message> = {
        let stream = client.send_content(content).await?;
        tokio::pin!(stream);

        let mut acc = Vec::new();
        while let Some(item) = stream.next().await {
            acc.push(item?);
        }
        acc
    };

    client.close().await?;
    Ok(messages)
}

/// Run a one-shot query, yielding messages via a static boxed stream.
///
/// Collects the full response into a [`Vec`] and returns it wrapped in a
/// `tokio_stream::Stream`. This avoids the lifetime problem of streaming
/// directly from an owned [`Client`] without boxing the client.
///
/// For direct access to the underlying `Vec`, prefer [`query`].
///
/// # Errors
///
/// Any connection or streaming error is emitted as the first `Err` item in
/// the returned stream.
///
/// # Example
///
/// ```rust,no_run
/// use tokio_stream::StreamExt as _;
///
/// #[tokio::main]
/// async fn main() -> gemini_cli_sdk::Result<()> {
///     let stream = gemini_cli_sdk::query_stream("List five prime numbers").await;
///     tokio::pin!(stream);
///     while let Some(item) = stream.next().await {
///         let msg = item?;
///         if let Some(text) = msg.assistant_text() {
///             print!("{text}");
///         }
///     }
///     Ok(())
/// }
/// ```
pub async fn query_stream(
    prompt: &str,
) -> impl futures_core::Stream<Item = Result<Message>> {
    query_stream_with_content(prompt, vec![UserContent::text(prompt)]).await
}

/// Run a one-shot query with structured content, yielding messages as a stream
/// as they arrive.
///
/// Each message is forwarded to the caller immediately via an internal channel,
/// providing true streaming backpressure rather than buffering the full response.
///
/// Equivalent to [`query_stream`] but accepts a [`Vec<UserContent>`] for
/// mixed text/image inputs.
///
/// # Errors
///
/// Any connection or streaming error is emitted as the first `Err` item in
/// the returned stream.
///
/// # Example
///
/// ```rust,no_run
/// use gemini_cli_sdk::{UserContent, query_stream_with_content};
/// use tokio_stream::StreamExt as _;
///
/// #[tokio::main]
/// async fn main() -> gemini_cli_sdk::Result<()> {
///     let content = vec![UserContent::text("Hello!")];
///     let stream = query_stream_with_content("Hello!", content).await;
///     tokio::pin!(stream);
///     while let Some(item) = stream.next().await {
///         println!("{:?}", item?);
///     }
///     Ok(())
/// }
/// ```
pub async fn query_stream_with_content(
    prompt: &str,
    content: Vec<UserContent>,
) -> impl futures_core::Stream<Item = Result<Message>> {
    use tokio_stream::wrappers::ReceiverStream;

    let prompt = prompt.to_string();
    let (tx, rx) = tokio::sync::mpsc::channel::<Result<Message>>(64);

    tokio::spawn(async move {
        let config = ClientConfig::builder().prompt(&prompt).build();

        let mut client = match Client::new(config) {
            Ok(c) => c,
            Err(e) => {
                let _ = tx.send(Err(e)).await;
                return;
            }
        };
        if let Err(e) = client.connect().await {
            let _ = tx.send(Err(e)).await;
            return;
        }
        match client.send_content(content).await {
            Err(e) => {
                let _ = tx.send(Err(e)).await;
            }
            Ok(stream) => {
                use tokio_stream::StreamExt as _;
                tokio::pin!(stream);
                while let Some(item) = stream.next().await {
                    if tx.send(item).await.is_err() {
                        break;
                    }
                }
            }
        }
        let _ = client.close().await;
    });

    ReceiverStream::new(rx)
}

/// Extract the prompt string from a [`Client`] and return it.
///
/// Convenience wrapper for the free-function wrappers that need to inspect
/// the prompt on an already-constructed client.
///
/// # Example
///
/// ```rust
/// use gemini_cli_sdk::{Client, ClientConfig, client_prompt};
///
/// let config = ClientConfig::builder().prompt("hello").build();
/// // NOTE: Client::new requires a real CLI binary — this example shows
/// // the function signature only.
/// // let client = Client::new(config).unwrap();
/// // assert_eq!(client_prompt(&client), "hello");
/// ```
#[inline]
pub fn client_prompt(client: &Client) -> &str {
    client.prompt()
}