Skip to main content

gemini_cli_sdk/
lib.rs

1//! Rust SDK wrapping Google's Gemini CLI as a subprocess via JSON-RPC 2.0.
2//!
3//! # Architecture
4//!
5//! The SDK communicates with the `gemini` binary using the `--experimental-acp`
6//! JSON-RPC 2.0 mode. Each [`Client`] manages a single subprocess session.
7//! Responses are translated from raw wire types to the public [`Message`] enum.
8//!
9//! # Quick Start — one-shot query
10//!
11//! ```rust,no_run
12//! #[tokio::main]
13//! async fn main() -> gemini_cli_sdk::Result<()> {
14//!     let messages = gemini_cli_sdk::query("Explain quantum computing in one sentence").await?;
15//!     for msg in messages {
16//!         if let Some(text) = msg.assistant_text() {
17//!             println!("{text}");
18//!         }
19//!     }
20//!     Ok(())
21//! }
22//! ```
23//!
24//! # Quick Start — stateful multi-turn session
25//!
26//! ```rust,no_run
27//! use gemini_cli_sdk::{Client, ClientConfig};
28//!
29//! #[tokio::main]
30//! async fn main() -> gemini_cli_sdk::Result<()> {
31//!     let config = ClientConfig::builder()
32//!         .prompt("Explain quantum computing")
33//!         .build();
34//!     let mut client = Client::new(config)?;
35//!     let _info = client.connect().await?;
36//!     // send() returns a Stream; consume it then close.
37//!     client.close().await?;
38//!     Ok(())
39//! }
40//! ```
41
42// ── Module declarations ───────────────────────────────────────────────────────
43//
44// Modules referenced in existing doctests via `gemini_cli_sdk::<module>::` must
45// be `pub` so rustdoc can compile those examples.
46
47pub mod callback;
48mod client;
49pub mod config;
50pub mod discovery;
51pub mod errors;
52mod hooks;
53mod jsonrpc;
54pub mod mcp;
55pub mod permissions;
56mod translate;
57pub mod transport;
58pub mod types;
59mod wire;
60
61#[cfg(feature = "testing")]
62pub mod testing;
63
64// ── Core re-exports ───────────────────────────────────────────────────────────
65
66pub use client::Client;
67pub use errors::{Error, Result};
68
69// ── Config re-exports ─────────────────────────────────────────────────────────
70
71pub use config::{AuthMethod, ClientConfig, PermissionMode, SystemPrompt};
72
73// ── Callback re-exports ───────────────────────────────────────────────────────
74
75pub use callback::{sync_callback, tracing_callback, MessageCallback};
76
77// ── Discovery re-exports ──────────────────────────────────────────────────────
78
79pub use discovery::{check_cli_version, find_cli, version_satisfies, MIN_CLI_VERSION};
80
81// ── Hooks re-exports ──────────────────────────────────────────────────────────
82
83pub use hooks::{HookCallback, HookContext, HookDecision, HookEvent, HookInput, HookMatcher, HookOutput};
84
85// ── MCP re-exports ────────────────────────────────────────────────────────────
86
87pub use mcp::{McpServerConfig, McpServers};
88
89// ── Permissions re-exports ────────────────────────────────────────────────────
90
91pub use permissions::{
92    CanUseToolCallback, PermissionContext, PermissionDecision, PermissionOptionInfo,
93    ToolLocationInfo,
94};
95
96// ── Transport re-exports ──────────────────────────────────────────────────────
97
98pub use transport::{GeminiTransport, Transport};
99
100// ── Message type re-exports ───────────────────────────────────────────────────
101
102pub use types::messages::{
103    AssistantMessage, AssistantMessageInner, McpServerStatus, Message, PlanEntry, ResultMessage,
104    SessionInfo, StreamEvent, SystemMessage, Usage, UserMessage, UserMessageInner,
105};
106
107// ── Content type re-exports ───────────────────────────────────────────────────
108
109pub use types::content::{
110    Base64ImageSource, ContentBlock, ImageBlock, ImageSource, TextBlock, ThinkingBlock,
111    ToolResultBlock, ToolResultContent, ToolUseBlock, UrlImageSource, UserContent,
112};
113
114// ── Free-function helpers ─────────────────────────────────────────────────────
115
116/// Run a one-shot query with a plain-text prompt, collecting all messages.
117///
118/// Creates a temporary [`Client`] with default configuration, connects it,
119/// sends the prompt, collects the full response into a [`Vec`], and closes
120/// the session. Equivalent to calling [`query_with_content`] with a single
121/// [`UserContent::text`] block.
122///
123/// # Errors
124///
125/// Propagates all errors from [`Client::new`], [`Client::connect`],
126/// [`Client::send`], and [`Client::close`].
127///
128/// # Example
129///
130/// ```rust,no_run
131/// #[tokio::main]
132/// async fn main() -> gemini_cli_sdk::Result<()> {
133///     let msgs = gemini_cli_sdk::query("What is 2+2?").await?;
134///     for m in msgs {
135///         if let Some(t) = m.assistant_text() {
136///             println!("{t}");
137///         }
138///     }
139///     Ok(())
140/// }
141/// ```
142pub async fn query(prompt: &str) -> Result<Vec<Message>> {
143    query_with_content(prompt, vec![UserContent::text(prompt)]).await
144}
145
146/// Run a one-shot query with structured content, collecting all messages.
147///
148/// Identical to [`query`] but accepts a [`Vec<UserContent>`] instead of a
149/// plain string, allowing images and mixed content to be sent.
150///
151/// The `prompt` parameter is used only to construct the [`ClientConfig`];
152/// the actual content sent to the session is taken from `content`.
153///
154/// # Errors
155///
156/// Propagates all errors from [`Client::new`], [`Client::connect`],
157/// [`Client::send_content`], and [`Client::close`].
158///
159/// # Example
160///
161/// ```rust,no_run
162/// use gemini_cli_sdk::{UserContent, query_with_content};
163///
164/// #[tokio::main]
165/// async fn main() -> gemini_cli_sdk::Result<()> {
166///     let content = vec![
167///         UserContent::text("Describe this image:"),
168///         UserContent::image_url("https://example.com/img.png"),
169///     ];
170///     let msgs = query_with_content("Describe this image:", content).await?;
171///     println!("{} messages", msgs.len());
172///     Ok(())
173/// }
174/// ```
175pub async fn query_with_content(
176    prompt: &str,
177    content: Vec<UserContent>,
178) -> Result<Vec<Message>> {
179    use tokio_stream::StreamExt as _;
180
181    let config = ClientConfig::builder().prompt(prompt).build();
182    let mut client = Client::new(config)?;
183    client.connect().await?;
184
185    // Collect inside a block so `stream` — which borrows `client` — is
186    // dropped before the mutable `close()` call.
187    let messages: Vec<Message> = {
188        let stream = client.send_content(content).await?;
189        tokio::pin!(stream);
190
191        let mut acc = Vec::new();
192        while let Some(item) = stream.next().await {
193            acc.push(item?);
194        }
195        acc
196    };
197
198    client.close().await?;
199    Ok(messages)
200}
201
202/// Run a one-shot query, yielding messages via a static boxed stream.
203///
204/// Collects the full response into a [`Vec`] and returns it wrapped in a
205/// `tokio_stream::Stream`. This avoids the lifetime problem of streaming
206/// directly from an owned [`Client`] without boxing the client.
207///
208/// For direct access to the underlying `Vec`, prefer [`query`].
209///
210/// # Errors
211///
212/// Any connection or streaming error is emitted as the first `Err` item in
213/// the returned stream.
214///
215/// # Example
216///
217/// ```rust,no_run
218/// use tokio_stream::StreamExt as _;
219///
220/// #[tokio::main]
221/// async fn main() -> gemini_cli_sdk::Result<()> {
222///     let stream = gemini_cli_sdk::query_stream("List five prime numbers").await;
223///     tokio::pin!(stream);
224///     while let Some(item) = stream.next().await {
225///         let msg = item?;
226///         if let Some(text) = msg.assistant_text() {
227///             print!("{text}");
228///         }
229///     }
230///     Ok(())
231/// }
232/// ```
233pub async fn query_stream(
234    prompt: &str,
235) -> impl futures_core::Stream<Item = Result<Message>> {
236    query_stream_with_content(prompt, vec![UserContent::text(prompt)]).await
237}
238
239/// Run a one-shot query with structured content, yielding messages as a stream
240/// as they arrive.
241///
242/// Each message is forwarded to the caller immediately via an internal channel,
243/// providing true streaming backpressure rather than buffering the full response.
244///
245/// Equivalent to [`query_stream`] but accepts a [`Vec<UserContent>`] for
246/// mixed text/image inputs.
247///
248/// # Errors
249///
250/// Any connection or streaming error is emitted as the first `Err` item in
251/// the returned stream.
252///
253/// # Example
254///
255/// ```rust,no_run
256/// use gemini_cli_sdk::{UserContent, query_stream_with_content};
257/// use tokio_stream::StreamExt as _;
258///
259/// #[tokio::main]
260/// async fn main() -> gemini_cli_sdk::Result<()> {
261///     let content = vec![UserContent::text("Hello!")];
262///     let stream = query_stream_with_content("Hello!", content).await;
263///     tokio::pin!(stream);
264///     while let Some(item) = stream.next().await {
265///         println!("{:?}", item?);
266///     }
267///     Ok(())
268/// }
269/// ```
270pub async fn query_stream_with_content(
271    prompt: &str,
272    content: Vec<UserContent>,
273) -> impl futures_core::Stream<Item = Result<Message>> {
274    use tokio_stream::wrappers::ReceiverStream;
275
276    let prompt = prompt.to_string();
277    let (tx, rx) = tokio::sync::mpsc::channel::<Result<Message>>(64);
278
279    tokio::spawn(async move {
280        let config = ClientConfig::builder().prompt(&prompt).build();
281
282        let mut client = match Client::new(config) {
283            Ok(c) => c,
284            Err(e) => {
285                let _ = tx.send(Err(e)).await;
286                return;
287            }
288        };
289        if let Err(e) = client.connect().await {
290            let _ = tx.send(Err(e)).await;
291            return;
292        }
293        match client.send_content(content).await {
294            Err(e) => {
295                let _ = tx.send(Err(e)).await;
296            }
297            Ok(stream) => {
298                use tokio_stream::StreamExt as _;
299                tokio::pin!(stream);
300                while let Some(item) = stream.next().await {
301                    if tx.send(item).await.is_err() {
302                        break;
303                    }
304                }
305            }
306        }
307        let _ = client.close().await;
308    });
309
310    ReceiverStream::new(rx)
311}
312
313/// Extract the prompt string from a [`Client`] and return it.
314///
315/// Convenience wrapper for the free-function wrappers that need to inspect
316/// the prompt on an already-constructed client.
317///
318/// # Example
319///
320/// ```rust
321/// use gemini_cli_sdk::{Client, ClientConfig, client_prompt};
322///
323/// let config = ClientConfig::builder().prompt("hello").build();
324/// // NOTE: Client::new requires a real CLI binary — this example shows
325/// // the function signature only.
326/// // let client = Client::new(config).unwrap();
327/// // assert_eq!(client_prompt(&client), "hello");
328/// ```
329#[inline]
330pub fn client_prompt(client: &Client) -> &str {
331    client.prompt()
332}