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}