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    /// Send a query and collect all responses
53    pub fn query(&mut self, input: ClaudeInput) -> Result<Vec<ClaudeOutput>> {
54        let mut responses = Vec::new();
55        for response in self.query_stream(input)? {
56            responses.push(response?);
57        }
58        Ok(responses)
59    }
60
61    /// Send a query and return an iterator over responses
62    pub fn query_stream(&mut self, input: ClaudeInput) -> Result<ResponseIterator<'_>> {
63        // Send the input
64        Protocol::write_sync(&mut self.stdin, &input)?;
65
66        Ok(ResponseIterator {
67            client: self,
68            finished: false,
69        })
70    }
71
72    /// Read the next response from Claude
73    fn read_next(&mut self) -> Result<Option<ClaudeOutput>> {
74        let mut line = String::new();
75        match self.stdout.read_line(&mut line) {
76            Ok(0) => {
77                debug!("[CLIENT] Stream closed");
78                Ok(None)
79            }
80            Ok(_) => {
81                let trimmed = line.trim();
82                if trimmed.is_empty() {
83                    debug!("[CLIENT] Skipping empty line");
84                    return self.read_next();
85                }
86
87                debug!("[CLIENT] Received: {}", trimmed);
88                match ClaudeOutput::parse_json(trimmed) {
89                    Ok(output) => {
90                        // Capture UUID from first response if not already set
91                        if self.session_uuid.is_none() {
92                            if let ClaudeOutput::Assistant(ref msg) = output {
93                                if let Some(ref uuid_str) = msg.uuid {
94                                    if let Ok(uuid) = Uuid::parse_str(uuid_str) {
95                                        debug!("[CLIENT] Captured session UUID: {}", uuid);
96                                        self.session_uuid = Some(uuid);
97                                    }
98                                }
99                            } else if let ClaudeOutput::Result(ref msg) = output {
100                                if let Some(ref uuid_str) = msg.uuid {
101                                    if let Ok(uuid) = Uuid::parse_str(uuid_str) {
102                                        debug!("[CLIENT] Captured session UUID: {}", uuid);
103                                        self.session_uuid = Some(uuid);
104                                    }
105                                }
106                            }
107                        }
108
109                        // Check if this is a result message
110                        if matches!(output, ClaudeOutput::Result(_)) {
111                            debug!("[CLIENT] Received result message, stream complete");
112                            Ok(Some(output))
113                        } else {
114                            Ok(Some(output))
115                        }
116                    }
117                    Err(ParseError { error_message, .. }) => {
118                        debug!("[CLIENT] Failed to deserialize: {}", error_message);
119                        Err(Error::Deserialization(error_message))
120                    }
121                }
122            }
123            Err(e) => {
124                debug!("[CLIENT] Error reading from stdout: {}", e);
125                Err(Error::Io(e))
126            }
127        }
128    }
129
130    /// Shutdown the client and wait for the process to exit
131    pub fn shutdown(&mut self) -> Result<()> {
132        debug!("[CLIENT] Shutting down client");
133        self.child.kill().map_err(Error::Io)?;
134        self.child.wait().map_err(Error::Io)?;
135        Ok(())
136    }
137
138    /// Get the session UUID if available
139    /// Returns an error if no response has been received yet
140    pub fn session_uuid(&self) -> Result<Uuid> {
141        self.session_uuid.ok_or(Error::SessionNotInitialized)
142    }
143}
144
145/// Iterator over responses from Claude
146pub struct ResponseIterator<'a> {
147    client: &'a mut SyncClient,
148    finished: bool,
149}
150
151impl Iterator for ResponseIterator<'_> {
152    type Item = Result<ClaudeOutput>;
153
154    fn next(&mut self) -> Option<Self::Item> {
155        if self.finished {
156            return None;
157        }
158
159        match self.client.read_next() {
160            Ok(Some(output)) => {
161                // Check if this is a result message
162                if matches!(output, ClaudeOutput::Result(_)) {
163                    self.finished = true;
164                }
165                Some(Ok(output))
166            }
167            Ok(None) => {
168                self.finished = true;
169                None
170            }
171            Err(e) => {
172                self.finished = true;
173                Some(Err(e))
174            }
175        }
176    }
177}
178
179impl Drop for SyncClient {
180    fn drop(&mut self) {
181        if let Err(e) = self.shutdown() {
182            debug!("[CLIENT] Error during shutdown: {}", e);
183        }
184    }
185}