Skip to main content

claude_cli_sdk/
lib.rs

1#![warn(missing_docs)]
2//! # claude-cli-sdk
3//!
4//! Rust SDK for programmatic interaction with the [Claude Code CLI].
5//!
6//! This crate provides strongly-typed, async-first access to Claude Code
7//! sessions via the CLI's NDJSON stdio protocol.
8//!
9//! ## Platform support
10//!
11//! This SDK supports macOS, Linux, and Windows.
12//!
13//! ## Quick start
14//!
15//! ```rust,no_run
16//! use claude_cli_sdk::{query, ClientConfig};
17//!
18//! #[tokio::main]
19//! async fn main() -> claude_cli_sdk::Result<()> {
20//!     let config = ClientConfig::builder()
21//!         .prompt("List files in /tmp")
22//!         .build();
23//!     let messages = query(config).await?;
24//!     for msg in &messages {
25//!         if let Some(text) = msg.assistant_text() {
26//!             println!("{text}");
27//!         }
28//!     }
29//!     Ok(())
30//! }
31//! ```
32//!
33//! ## Timeouts
34//!
35//! All timeouts are configurable via [`ClientConfig`]:
36//!
37//! | Timeout | Default | Purpose |
38//! |---------|---------|---------|
39//! | `connect_timeout` | 30s | Deadline for process spawn + init message |
40//! | `close_timeout` | 10s | Deadline for graceful shutdown; kills on expiry |
41//! | `read_timeout` | None | Per-message recv deadline (detects hung processes) |
42//! | `default_hook_timeout` | 30s | Fallback when `HookMatcher::timeout` is None |
43//! | `version_check_timeout` | 5s | Deadline for `--version` check |
44//!
45//! Set any `Option<Duration>` to `None` to wait indefinitely.
46//!
47//! ## Feature flags
48//!
49//! | Feature | Description |
50//! |---------|-------------|
51//! | `testing` | Enables `testing` utilities (e.g., `MockTransport`) |
52//! | `efficiency` | Reserved for future throughput optimisations |
53//! | `integration` | Enables integration-test helpers (requires a live CLI) |
54//!
55//! [Claude Code CLI]: https://www.anthropic.com/claude-code
56
57pub mod callback;
58pub mod client;
59pub mod config;
60pub mod discovery;
61pub mod errors;
62pub mod hooks;
63pub mod mcp;
64pub mod permissions;
65pub mod transport;
66pub mod types;
67pub(crate) mod util;
68
69#[cfg(feature = "testing")]
70pub mod testing;
71
72// ── Top-level re-exports ────────────────────────────────────────────────────
73
74// Core
75pub use client::Client;
76pub use config::{ClientConfig, PermissionMode, SystemPrompt};
77pub use errors::{Error, Result};
78
79// Content types
80pub use types::content::{
81    ALLOWED_IMAGE_MIME_TYPES, Base64ImageSource, ContentBlock, ImageBlock, ImageSource,
82    MAX_IMAGE_BASE64_BYTES, TextBlock, ThinkingBlock, ToolResultBlock, ToolResultContent,
83    ToolUseBlock, UrlImageSource, UserContent,
84};
85
86// Message types
87pub use types::messages::{
88    AssistantMessage, AssistantMessageInner, McpServerStatus, Message, ResultMessage, SessionInfo,
89    StreamEvent, SystemMessage, Usage, UserMessage, UserMessageInner,
90};
91
92// Permissions
93pub use permissions::{CanUseToolCallback, PermissionContext, PermissionDecision};
94
95// Hooks
96pub use hooks::{
97    HookCallback, HookContext, HookDecision, HookEvent, HookInput, HookMatcher, HookOutput,
98};
99
100// MCP
101pub use mcp::{McpServerConfig, McpServers};
102
103// Discovery
104pub use discovery::{check_cli_version, find_cli};
105
106// Transport
107pub use transport::Transport;
108
109// Callback
110pub use callback::MessageCallback;
111
112use futures_core::Stream;
113
114// ── Shared stream helper ────────────────────────────────────────────────────
115
116/// Collect all messages from a stream into a vector.
117async fn collect_stream(stream: impl Stream<Item = Result<Message>>) -> Result<Vec<Message>> {
118    use tokio_stream::StreamExt;
119    tokio::pin!(stream);
120    let mut messages = Vec::new();
121    while let Some(msg) = stream.next().await {
122        messages.push(msg?);
123    }
124    Ok(messages)
125}
126
127// ── Top-level free functions ────────────────────────────────────────────────
128
129/// Run a one-shot query against Claude Code and collect all response messages.
130///
131/// This is the simplest way to use the SDK. It spawns a CLI process, sends the
132/// prompt from `config`, collects all response messages, and shuts down.
133///
134/// # Example
135///
136/// ```rust,no_run
137/// use claude_cli_sdk::{query, ClientConfig};
138///
139/// # async fn example() -> claude_cli_sdk::Result<()> {
140/// let config = ClientConfig::builder()
141///     .prompt("What is Rust?")
142///     .build();
143/// let messages = query(config).await?;
144/// # Ok(())
145/// # }
146/// ```
147///
148/// # Errors
149///
150/// Returns [`Error`] if the CLI cannot be found, spawning fails, or the
151/// session encounters an error.
152#[must_use = "the future must be awaited to run the query"]
153pub async fn query(config: ClientConfig) -> Result<Vec<Message>> {
154    let stream = query_stream(config).await?;
155    collect_stream(stream).await
156}
157
158/// Run a streaming query against Claude Code, yielding messages as they arrive.
159///
160/// Returns a `Stream` of [`Message`] values. The stream ends when the CLI
161/// emits a `Result` message.
162///
163/// # Example
164///
165/// ```rust,no_run
166/// use claude_cli_sdk::{query_stream, ClientConfig, Message};
167/// use tokio_stream::StreamExt;
168///
169/// # async fn example() -> claude_cli_sdk::Result<()> {
170/// let config = ClientConfig::builder()
171///     .prompt("Explain async/await in Rust")
172///     .build();
173/// let mut stream = query_stream(config).await?;
174/// tokio::pin!(stream);
175///
176/// while let Some(msg) = stream.next().await {
177///     let msg = msg?;
178///     if let Some(text) = msg.assistant_text() {
179///         print!("{text}");
180///     }
181/// }
182/// # Ok(())
183/// # }
184/// ```
185#[must_use = "the future must be awaited to run the query"]
186pub async fn query_stream(config: ClientConfig) -> Result<impl Stream<Item = Result<Message>>> {
187    let mut client = Client::new(config)?;
188    client.connect().await?;
189
190    let read_timeout = client.read_timeout();
191
192    // Take the message receiver and own it in the stream.
193    // The client stays alive because the transport/background task are Arc'd.
194    let rx = client.take_message_rx().ok_or(Error::NotConnected)?;
195
196    Ok(async_stream::stream! {
197        loop {
198            match crate::client::recv_with_timeout(&rx, read_timeout).await {
199                Ok(msg) => yield Ok(msg),
200                Err(ref e) if matches!(e, crate::errors::Error::Transport(_)) => break,
201                Err(e) => {
202                    yield Err(e);
203                    break;
204                }
205            }
206        }
207        // Explicitly close the client to clean up the CLI process.
208        let _ = client.close().await;
209    })
210}
211
212/// Run a one-shot multi-modal query and collect all response messages.
213///
214/// Like [`query()`] but accepts structured content blocks (text + images)
215/// instead of a plain string prompt. The `config.prompt` field is ignored
216/// when content blocks are provided.
217///
218/// # Errors
219///
220/// Returns [`Error::Config`] if `content` is empty.
221#[must_use = "the future must be awaited to run the query"]
222pub async fn query_with_content(
223    content: Vec<UserContent>,
224    config: ClientConfig,
225) -> Result<Vec<Message>> {
226    let stream = query_stream_with_content(content, config).await?;
227    collect_stream(stream).await
228}
229
230/// Run a streaming multi-modal query, yielding messages as they arrive.
231///
232/// Like [`query_stream()`] but accepts structured content blocks.
233/// The `config.prompt` field is ignored — content blocks are sent via stdin.
234///
235/// # Errors
236///
237/// Returns [`Error::Config`] if `content` is empty.
238#[must_use = "the future must be awaited to run the query"]
239pub async fn query_stream_with_content(
240    content: Vec<UserContent>,
241    config: ClientConfig,
242) -> Result<impl Stream<Item = Result<Message>>> {
243    if content.is_empty() {
244        return Err(Error::Config("content must not be empty".into()));
245    }
246
247    let mut client = Client::new(config)?;
248    client.connect().await?;
249
250    let read_timeout = client.read_timeout();
251
252    // Send the content blocks as a structured user message.
253    let user_message = serde_json::json!({
254        "type": "user",
255        "message": {
256            "role": "user",
257            "content": content
258        }
259    });
260    let json = serde_json::to_string(&user_message).map_err(Error::Json)?;
261    client.transport_write(&json).await?;
262
263    let rx = client.take_message_rx().ok_or(Error::NotConnected)?;
264
265    Ok(async_stream::stream! {
266        loop {
267            match crate::client::recv_with_timeout(&rx, read_timeout).await {
268                Ok(msg) => yield Ok(msg),
269                Err(ref e) if matches!(e, crate::errors::Error::Transport(_)) => break,
270                Err(e) => {
271                    yield Err(e);
272                    break;
273                }
274            }
275        }
276        let _ = client.close().await;
277    })
278}