claude_codes/client/
sync.rs

1//! Synchronous client for Claude communication
2
3use crate::cli::ClaudeCliBuilder;
4use crate::error::{Error, Result};
5use crate::io::{ClaudeInput, ClaudeOutput, ContentBlock, ParseError};
6use crate::protocol::Protocol;
7use std::io::{BufRead, BufReader};
8use std::process::{Child, ChildStdin, ChildStdout};
9use tracing::debug;
10use uuid::Uuid;
11
12/// Synchronous client for communicating with Claude
13pub struct SyncClient {
14    child: Child,
15    stdin: ChildStdin,
16    stdout: BufReader<ChildStdout>,
17    session_uuid: Option<Uuid>,
18}
19
20impl SyncClient {
21    /// Create a new synchronous client from an existing child process
22    pub fn new(mut child: Child) -> Result<Self> {
23        let stdin = child
24            .stdin
25            .take()
26            .ok_or_else(|| Error::Protocol("Failed to get stdin".to_string()))?;
27        let stdout = child
28            .stdout
29            .take()
30            .ok_or_else(|| Error::Protocol("Failed to get stdout".to_string()))?;
31
32        Ok(Self {
33            child,
34            stdin,
35            stdout: BufReader::new(stdout),
36            session_uuid: None,
37        })
38    }
39
40    /// Create a new synchronous client with default settings
41    pub fn with_defaults() -> Result<Self> {
42        // Check Claude version (only warns once per session)
43        // NOTE: The claude-codes API is in high flux. If you wish to work around
44        // this version check, you can use SyncClient::new() directly with:
45        //   let child = ClaudeCliBuilder::new().spawn_sync()?;
46        //   SyncClient::new(child)
47        crate::version::check_claude_version()?;
48        let child = ClaudeCliBuilder::new().spawn_sync().map_err(Error::Io)?;
49        Self::new(child)
50    }
51
52    /// Resume a previous session by UUID
53    /// This creates a new client that resumes an existing session
54    pub fn resume_session(session_uuid: Uuid) -> Result<Self> {
55        let child = ClaudeCliBuilder::new()
56            .resume(Some(session_uuid.to_string()))
57            .spawn_sync()
58            .map_err(Error::Io)?;
59
60        debug!("Resuming Claude session with UUID: {}", session_uuid);
61        let mut client = Self::new(child)?;
62        // Pre-populate the session UUID since we're resuming
63        client.session_uuid = Some(session_uuid);
64        Ok(client)
65    }
66
67    /// Resume a previous session with a specific model
68    pub fn resume_session_with_model(session_uuid: Uuid, model: &str) -> Result<Self> {
69        let child = ClaudeCliBuilder::new()
70            .model(model)
71            .resume(Some(session_uuid.to_string()))
72            .spawn_sync()
73            .map_err(Error::Io)?;
74
75        debug!(
76            "Resuming Claude session with UUID: {} and model: {}",
77            session_uuid, model
78        );
79        let mut client = Self::new(child)?;
80        // Pre-populate the session UUID since we're resuming
81        client.session_uuid = Some(session_uuid);
82        Ok(client)
83    }
84
85    /// Send a query and collect all responses
86    pub fn query(&mut self, input: ClaudeInput) -> Result<Vec<ClaudeOutput>> {
87        let mut responses = Vec::new();
88        for response in self.query_stream(input)? {
89            responses.push(response?);
90        }
91        Ok(responses)
92    }
93
94    /// Send a query and return an iterator over responses
95    pub fn query_stream(&mut self, input: ClaudeInput) -> Result<ResponseIterator<'_>> {
96        // Send the input
97        Protocol::write_sync(&mut self.stdin, &input)?;
98
99        Ok(ResponseIterator {
100            client: self,
101            finished: false,
102        })
103    }
104
105    /// Read the next response from Claude
106    fn read_next(&mut self) -> Result<Option<ClaudeOutput>> {
107        let mut line = String::new();
108        match self.stdout.read_line(&mut line) {
109            Ok(0) => {
110                debug!("[CLIENT] Stream closed");
111                Ok(None)
112            }
113            Ok(_) => {
114                let trimmed = line.trim();
115                if trimmed.is_empty() {
116                    debug!("[CLIENT] Skipping empty line");
117                    return self.read_next();
118                }
119
120                debug!("[CLIENT] Received: {}", trimmed);
121                match ClaudeOutput::parse_json_tolerant(trimmed) {
122                    Ok(output) => {
123                        // Capture UUID from first response if not already set
124                        if self.session_uuid.is_none() {
125                            if let ClaudeOutput::Assistant(ref msg) = output {
126                                if let Some(ref uuid_str) = msg.uuid {
127                                    if let Ok(uuid) = Uuid::parse_str(uuid_str) {
128                                        debug!("[CLIENT] Captured session UUID: {}", uuid);
129                                        self.session_uuid = Some(uuid);
130                                    }
131                                }
132                            } else if let ClaudeOutput::Result(ref msg) = output {
133                                if let Some(ref uuid_str) = msg.uuid {
134                                    if let Ok(uuid) = Uuid::parse_str(uuid_str) {
135                                        debug!("[CLIENT] Captured session UUID: {}", uuid);
136                                        self.session_uuid = Some(uuid);
137                                    }
138                                }
139                            }
140                        }
141
142                        // Check if this is a result message
143                        if matches!(output, ClaudeOutput::Result(_)) {
144                            debug!("[CLIENT] Received result message, stream complete");
145                            Ok(Some(output))
146                        } else {
147                            Ok(Some(output))
148                        }
149                    }
150                    Err(ParseError { error_message, .. }) => {
151                        debug!("[CLIENT] Failed to deserialize: {}", error_message);
152                        debug!("[CLIENT] Raw JSON that failed: {}", trimmed);
153                        Err(Error::Deserialization(format!(
154                            "{} (raw: {})",
155                            error_message, trimmed
156                        )))
157                    }
158                }
159            }
160            Err(e) => {
161                debug!("[CLIENT] Error reading from stdout: {}", e);
162                Err(Error::Io(e))
163            }
164        }
165    }
166
167    /// Shutdown the client and wait for the process to exit
168    pub fn shutdown(&mut self) -> Result<()> {
169        debug!("[CLIENT] Shutting down client");
170        self.child.kill().map_err(Error::Io)?;
171        self.child.wait().map_err(Error::Io)?;
172        Ok(())
173    }
174
175    /// Get the session UUID if available
176    /// Returns an error if no response has been received yet
177    pub fn session_uuid(&self) -> Result<Uuid> {
178        self.session_uuid.ok_or(Error::SessionNotInitialized)
179    }
180
181    /// Test if the Claude connection is working by sending a ping message
182    /// Returns true if Claude responds with "pong", false otherwise
183    pub fn ping(&mut self) -> bool {
184        // Send a simple ping request
185        let ping_input = ClaudeInput::user_message(
186            "ping - respond with just the word 'pong' and nothing else",
187            self.session_uuid.unwrap_or_else(Uuid::new_v4),
188        );
189
190        // Try to send the ping and get responses
191        match self.query(ping_input) {
192            Ok(responses) => {
193                // Check all responses for "pong"
194                for output in responses {
195                    if let ClaudeOutput::Assistant(msg) = &output {
196                        for content in &msg.message.content {
197                            if let ContentBlock::Text(text) = content {
198                                if text.text.to_lowercase().contains("pong") {
199                                    return true;
200                                }
201                            }
202                        }
203                    }
204                }
205                false
206            }
207            Err(e) => {
208                debug!("Ping failed: {}", e);
209                false
210            }
211        }
212    }
213}
214
215/// Iterator over responses from Claude
216pub struct ResponseIterator<'a> {
217    client: &'a mut SyncClient,
218    finished: bool,
219}
220
221impl Iterator for ResponseIterator<'_> {
222    type Item = Result<ClaudeOutput>;
223
224    fn next(&mut self) -> Option<Self::Item> {
225        if self.finished {
226            return None;
227        }
228
229        match self.client.read_next() {
230            Ok(Some(output)) => {
231                // Check if this is a result message
232                if matches!(output, ClaudeOutput::Result(_)) {
233                    self.finished = true;
234                }
235                Some(Ok(output))
236            }
237            Ok(None) => {
238                self.finished = true;
239                None
240            }
241            Err(e) => {
242                self.finished = true;
243                Some(Err(e))
244            }
245        }
246    }
247}
248
249impl Drop for SyncClient {
250    fn drop(&mut self) {
251        if let Err(e) = self.shutdown() {
252            debug!("[CLIENT] Error during shutdown: {}", e);
253        }
254    }
255}