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// Cancellation
110pub use tokio_util::sync::CancellationToken;
111
112// Callback
113pub use callback::MessageCallback;
114
115use futures_core::Stream;
116
117// ── Shared stream helper ────────────────────────────────────────────────────
118
119/// Collect all messages from a stream into a vector.
120async fn collect_stream(stream: impl Stream<Item = Result<Message>>) -> Result<Vec<Message>> {
121    use tokio_stream::StreamExt;
122    tokio::pin!(stream);
123    let mut messages = Vec::new();
124    while let Some(msg) = stream.next().await {
125        messages.push(msg?);
126    }
127    Ok(messages)
128}
129
130// ── Top-level free functions ────────────────────────────────────────────────
131
132/// Run a one-shot query against Claude Code and collect all response messages.
133///
134/// This is the simplest way to use the SDK. It spawns a CLI process, sends the
135/// prompt from `config`, collects all response messages, and shuts down.
136///
137/// # Example
138///
139/// ```rust,no_run
140/// use claude_cli_sdk::{query, ClientConfig};
141///
142/// # async fn example() -> claude_cli_sdk::Result<()> {
143/// let config = ClientConfig::builder()
144///     .prompt("What is Rust?")
145///     .build();
146/// let messages = query(config).await?;
147/// # Ok(())
148/// # }
149/// ```
150///
151/// # Errors
152///
153/// Returns [`Error`] if the CLI cannot be found, spawning fails, or the
154/// session encounters an error.
155#[must_use = "the future must be awaited to run the query"]
156pub async fn query(config: ClientConfig) -> Result<Vec<Message>> {
157    let stream = query_stream(config).await?;
158    collect_stream(stream).await
159}
160
161/// Run a streaming query against Claude Code, yielding messages as they arrive.
162///
163/// Returns a `Stream` of [`Message`] values. The stream ends when the CLI
164/// emits a `Result` message.
165///
166/// # Example
167///
168/// ```rust,no_run
169/// use claude_cli_sdk::{query_stream, ClientConfig, Message};
170/// use tokio_stream::StreamExt;
171///
172/// # async fn example() -> claude_cli_sdk::Result<()> {
173/// let config = ClientConfig::builder()
174///     .prompt("Explain async/await in Rust")
175///     .build();
176/// let mut stream = query_stream(config).await?;
177/// tokio::pin!(stream);
178///
179/// while let Some(msg) = stream.next().await {
180///     let msg = msg?;
181///     if let Some(text) = msg.assistant_text() {
182///         print!("{text}");
183///     }
184/// }
185/// # Ok(())
186/// # }
187/// ```
188#[must_use = "the future must be awaited to run the query"]
189pub async fn query_stream(config: ClientConfig) -> Result<impl Stream<Item = Result<Message>>> {
190    let cancel = config.cancellation_token.clone();
191    let mut client = Client::new(config)?;
192    client.connect().await?;
193
194    let read_timeout = client.read_timeout();
195
196    // Take the message receiver and own it in the stream.
197    // The client stays alive because the transport/background task are Arc'd.
198    let rx = client.take_message_rx().ok_or(Error::NotConnected)?;
199
200    Ok(async_stream::stream! {
201        loop {
202            match crate::client::recv_with_timeout(&rx, read_timeout, cancel.as_ref()).await {
203                Ok(msg) => yield Ok(msg),
204                Err(ref e) if matches!(e, crate::errors::Error::Transport(_)) => break,
205                Err(e) => {
206                    yield Err(e);
207                    break;
208                }
209            }
210        }
211        // Explicitly close the client to clean up the CLI process.
212        let _ = client.close().await;
213    })
214}
215
216/// Run a one-shot multi-modal query and collect all response messages.
217///
218/// Like [`query()`] but accepts structured content blocks (text + images)
219/// instead of a plain string prompt. The `config.prompt` field is ignored
220/// when content blocks are provided.
221///
222/// # Errors
223///
224/// Returns [`Error::Config`] if `content` is empty.
225#[must_use = "the future must be awaited to run the query"]
226pub async fn query_with_content(
227    content: Vec<UserContent>,
228    config: ClientConfig,
229) -> Result<Vec<Message>> {
230    let stream = query_stream_with_content(content, config).await?;
231    collect_stream(stream).await
232}
233
234/// Run a streaming multi-modal query, yielding messages as they arrive.
235///
236/// Like [`query_stream()`] but accepts structured content blocks.
237/// The `config.prompt` field is ignored — content blocks are sent via stdin.
238///
239/// # Errors
240///
241/// Returns [`Error::Config`] if `content` is empty.
242#[must_use = "the future must be awaited to run the query"]
243pub async fn query_stream_with_content(
244    content: Vec<UserContent>,
245    config: ClientConfig,
246) -> Result<impl Stream<Item = Result<Message>>> {
247    if content.is_empty() {
248        return Err(Error::Config("content must not be empty".into()));
249    }
250
251    let cancel = config.cancellation_token.clone();
252    let mut client = Client::new(config)?;
253    client.connect().await?;
254
255    let read_timeout = client.read_timeout();
256
257    // Send the content blocks as a structured user message.
258    let user_message = serde_json::json!({
259        "type": "user",
260        "message": {
261            "role": "user",
262            "content": content
263        }
264    });
265    let json = serde_json::to_string(&user_message).map_err(Error::Json)?;
266    client.transport_write(&json).await?;
267
268    let rx = client.take_message_rx().ok_or(Error::NotConnected)?;
269
270    Ok(async_stream::stream! {
271        loop {
272            match crate::client::recv_with_timeout(&rx, read_timeout, cancel.as_ref()).await {
273                Ok(msg) => yield Ok(msg),
274                Err(ref e) if matches!(e, crate::errors::Error::Transport(_)) => break,
275                Err(e) => {
276                    yield Err(e);
277                    break;
278                }
279            }
280        }
281        let _ = client.close().await;
282    })
283}