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