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