clau_runtime/
client.rs

1use clau_core::{Config, Message, Result, SessionId, StreamFormat, ClaudeCliResponse, ClaudeResponse};
2use crate::{MessageStream, process::execute_claude};
3use std::sync::Arc;
4use tokio::sync::mpsc;
5
6/// High-level client for interacting with Claude Code CLI
7/// 
8/// The `Client` provides a type-safe, async interface to Claude Code with support
9/// for different output formats, configuration options, and both simple and advanced
10/// response handling.
11/// 
12/// # Examples
13/// 
14/// Basic usage:
15/// ```rust,no_run
16/// # use clau_core::*;
17/// # use clau_runtime::Client;
18/// # #[tokio::main]
19/// # async fn main() -> clau_core::Result<()> {
20/// let client = Client::new(Config::default());
21/// let response = client.query("Hello").send().await?;
22/// println!("{}", response);
23/// # Ok(())
24/// # }
25/// ```
26/// 
27/// With configuration:
28/// ```rust,no_run
29/// # use clau_core::*;
30/// # use clau_runtime::Client;
31/// # #[tokio::main]
32/// # async fn main() -> clau_core::Result<()> {
33/// let client = Client::builder()
34///     .model("claude-3-opus-20240229")
35///     .stream_format(StreamFormat::Json)
36///     .timeout_secs(60)
37///     .build();
38/// # Ok(())
39/// # }
40/// ```
41#[derive(Clone)]
42pub struct Client {
43    config: Arc<Config>,
44}
45
46impl Client {
47    /// Create a new client with the given configuration
48    pub fn new(config: Config) -> Self {
49        Self {
50            config: Arc::new(config),
51        }
52    }
53    
54    /// Create a new client builder for fluent configuration
55    pub fn builder() -> ClientBuilder {
56        ClientBuilder::new()
57    }
58    
59    /// Create a query builder for the given query string
60    /// 
61    /// # Examples
62    /// 
63    /// ```rust,no_run
64    /// # use clau_core::*;
65    /// # use clau_runtime::Client;
66    /// # #[tokio::main]
67    /// # async fn main() -> clau_core::Result<()> {
68    /// let client = Client::new(Config::default());
69    /// let response = client
70    ///     .query("Explain Rust ownership")
71    ///     .send()
72    ///     .await?;
73    /// # Ok(())
74    /// # }
75    /// ```
76    pub fn query(&self, query: impl Into<String>) -> QueryBuilder {
77        QueryBuilder::new(self.clone(), query.into())
78    }
79    
80    /// Send a query and return just the text content (backwards compatible)
81    /// 
82    /// This is the simplest way to get a response from Claude. For access to 
83    /// metadata, costs, and raw JSON, use [`send_full`](Self::send_full).
84    /// 
85    /// # Examples
86    /// 
87    /// ```rust,no_run
88    /// # use clau_core::*;
89    /// # use clau_runtime::Client;
90    /// # #[tokio::main]
91    /// # async fn main() -> clau_core::Result<()> {
92    /// let client = Client::new(Config::default());
93    /// let answer = client.send("What is 2 + 2?").await?;
94    /// assert_eq!(answer.trim(), "4");
95    /// # Ok(())
96    /// # }
97    /// ```
98    pub async fn send(&self, query: &str) -> Result<String> {
99        let response = self.send_full(query).await?;
100        Ok(response.content)
101    }
102    
103    /// Send a query and return the full response with metadata and raw JSON
104    /// 
105    /// This method provides access to the complete response from Claude Code,
106    /// including metadata like costs, session IDs, and the raw JSON for 
107    /// advanced parsing or storage.
108    /// 
109    /// # Examples
110    /// 
111    /// ```rust,no_run
112    /// # use clau_core::*;
113    /// # use clau_runtime::Client;
114    /// # #[tokio::main]
115    /// # async fn main() -> clau_core::Result<()> {
116    /// let client = Client::builder()
117    ///     .stream_format(StreamFormat::Json)
118    ///     .build();
119    /// 
120    /// let response = client.send_full("Hello").await?;
121    /// println!("Content: {}", response.content);
122    /// 
123    /// if let Some(metadata) = &response.metadata {
124    ///     println!("Cost: ${:.6}", metadata.cost_usd.unwrap_or(0.0));
125    ///     println!("Session: {}", metadata.session_id);
126    /// }
127    /// 
128    /// // Access raw JSON for custom parsing
129    /// if let Some(raw) = &response.raw_json {
130    ///     // Custom field extraction
131    ///     let custom_field = raw.get("custom_field");
132    /// }
133    /// # Ok(())
134    /// # }
135    /// ```
136    pub async fn send_full(&self, query: &str) -> Result<ClaudeResponse> {
137        let output = execute_claude(&self.config, query).await?;
138        
139        // Parse response based on format
140        match self.config.stream_format {
141            StreamFormat::Text => {
142                Ok(ClaudeResponse::text(output.trim().to_string()))
143            }
144            StreamFormat::Json => {
145                // Parse the JSON response from claude CLI
146                let json_value: serde_json::Value = serde_json::from_str(&output)?;
147                let claude_response: ClaudeCliResponse = serde_json::from_value(json_value.clone())?;
148                Ok(ClaudeResponse::with_json(claude_response.result, json_value))
149            }
150            StreamFormat::StreamJson => {
151                // For stream-json, we need to parse multiple JSON lines
152                let mut result = String::new();
153                let mut all_json = Vec::new();
154                
155                for line in output.lines() {
156                    if line.trim().is_empty() {
157                        continue;
158                    }
159                    // Try to parse as a message
160                    if let Ok(msg) = serde_json::from_str::<serde_json::Value>(line) {
161                        all_json.push(msg.clone());
162                        
163                        // Check if it's an assistant message
164                        if msg.get("type").and_then(|v| v.as_str()) == Some("assistant") {
165                            // Extract text from the message content
166                            if let Some(message) = msg.get("message") {
167                                if let Some(content_array) = message.get("content").and_then(|v| v.as_array()) {
168                                    for content_item in content_array {
169                                        if content_item.get("type").and_then(|v| v.as_str()) == Some("text") {
170                                            if let Some(text) = content_item.get("text").and_then(|v| v.as_str()) {
171                                                result.push_str(text);
172                                            }
173                                        }
174                                    }
175                                }
176                            }
177                        }
178                    }
179                }
180                
181                // Return the response with all JSON messages as an array
182                let raw_json = serde_json::Value::Array(all_json);
183                Ok(ClaudeResponse::with_json(result, raw_json))
184            }
185        }
186    }
187}
188
189pub struct ClientBuilder {
190    config: Config,
191}
192
193impl ClientBuilder {
194    pub fn new() -> Self {
195        Self {
196            config: Config::default(),
197        }
198    }
199    
200    pub fn config(mut self, config: Config) -> Self {
201        self.config = config;
202        self
203    }
204    
205    pub fn system_prompt(mut self, prompt: impl Into<String>) -> Self {
206        self.config.system_prompt = Some(prompt.into());
207        self
208    }
209    
210    pub fn model(mut self, model: impl Into<String>) -> Self {
211        self.config.model = Some(model.into());
212        self
213    }
214    
215    
216    pub fn allowed_tools(mut self, tools: Vec<String>) -> Self {
217        self.config.allowed_tools = Some(tools);
218        self
219    }
220    
221    pub fn stream_format(mut self, format: StreamFormat) -> Self {
222        self.config.stream_format = format;
223        self
224    }
225    
226    pub fn verbose(mut self, verbose: bool) -> Self {
227        self.config.verbose = verbose;
228        self
229    }
230    
231    pub fn timeout_secs(mut self, timeout_secs: u64) -> Self {
232        self.config.timeout_secs = Some(timeout_secs);
233        self
234    }
235    
236    pub fn build(self) -> Client {
237        Client::new(self.config)
238    }
239}
240
241pub struct QueryBuilder {
242    client: Client,
243    query: String,
244    session_id: Option<SessionId>,
245    format: Option<StreamFormat>,
246}
247
248impl QueryBuilder {
249    fn new(client: Client, query: String) -> Self {
250        Self {
251            client,
252            query,
253            session_id: None,
254            format: None,
255        }
256    }
257    
258    pub fn session(mut self, session_id: SessionId) -> Self {
259        self.session_id = Some(session_id);
260        self
261    }
262    
263    pub fn format(mut self, format: StreamFormat) -> Self {
264        self.format = Some(format);
265        self
266    }
267    
268    /// Send the query and return just the text content
269    pub async fn send(self) -> Result<String> {
270        self.client.send(&self.query).await
271    }
272    
273    /// Send the query and return the full response with metadata and raw JSON
274    pub async fn send_full(self) -> Result<ClaudeResponse> {
275        self.client.send_full(&self.query).await
276    }
277    
278    pub async fn stream(self) -> Result<MessageStream> {
279        // For now, streaming is simulated by getting the full response
280        // and sending it as a single message
281        let (tx, rx) = mpsc::channel(100);
282        
283        let format = self.format.unwrap_or(self.client.config.stream_format);
284        let client = self.client;
285        let query = self.query;
286        
287        tokio::spawn(async move {
288            match client.send(&query).await {
289                Ok(response) => {
290                    // Send the response as a single assistant message
291                    let msg = Message::Assistant {
292                        content: response,
293                        meta: clau_core::MessageMeta {
294                            session_id: "stream-session".to_string(),
295                            timestamp: Some(std::time::SystemTime::now()),
296                            cost_usd: None,
297                            duration_ms: None,
298                            tokens_used: None,
299                        },
300                    };
301                    let _ = tx.send(Ok(msg)).await;
302                }
303                Err(e) => {
304                    let _ = tx.send(Err(e)).await;
305                }
306            }
307        });
308        
309        Ok(MessageStream::new(rx, format))
310    }
311    
312    pub async fn parse_output<T: serde::de::DeserializeOwned>(self) -> Result<T> {
313        let response = self.send().await?;
314        serde_json::from_str(&response).map_err(Into::into)
315    }
316}
317