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