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, 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(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                        Err(Error::Deserialization(error_message))
153                    }
154                }
155            }
156            Err(e) => {
157                debug!("[CLIENT] Error reading from stdout: {}", e);
158                Err(Error::Io(e))
159            }
160        }
161    }
162
163    /// Shutdown the client and wait for the process to exit
164    pub fn shutdown(&mut self) -> Result<()> {
165        debug!("[CLIENT] Shutting down client");
166        self.child.kill().map_err(Error::Io)?;
167        self.child.wait().map_err(Error::Io)?;
168        Ok(())
169    }
170
171    /// Get the session UUID if available
172    /// Returns an error if no response has been received yet
173    pub fn session_uuid(&self) -> Result<Uuid> {
174        self.session_uuid.ok_or(Error::SessionNotInitialized)
175    }
176}
177
178/// Iterator over responses from Claude
179pub struct ResponseIterator<'a> {
180    client: &'a mut SyncClient,
181    finished: bool,
182}
183
184impl Iterator for ResponseIterator<'_> {
185    type Item = Result<ClaudeOutput>;
186
187    fn next(&mut self) -> Option<Self::Item> {
188        if self.finished {
189            return None;
190        }
191
192        match self.client.read_next() {
193            Ok(Some(output)) => {
194                // Check if this is a result message
195                if matches!(output, ClaudeOutput::Result(_)) {
196                    self.finished = true;
197                }
198                Some(Ok(output))
199            }
200            Ok(None) => {
201                self.finished = true;
202                None
203            }
204            Err(e) => {
205                self.finished = true;
206                Some(Err(e))
207            }
208        }
209    }
210}
211
212impl Drop for SyncClient {
213    fn drop(&mut self) {
214        if let Err(e) = self.shutdown() {
215            debug!("[CLIENT] Error during shutdown: {}", e);
216        }
217    }
218}