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}