claude_codes/client/
sync.rs1use 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
12pub struct SyncClient {
14 child: Child,
15 stdin: ChildStdin,
16 stdout: BufReader<ChildStdout>,
17 session_uuid: Option<Uuid>,
18}
19
20impl SyncClient {
21 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 pub fn with_defaults() -> Result<Self> {
42 crate::version::check_claude_version()?;
48 let child = ClaudeCliBuilder::new().spawn_sync().map_err(Error::Io)?;
49 Self::new(child)
50 }
51
52 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 client.session_uuid = Some(session_uuid);
64 Ok(client)
65 }
66
67 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 client.session_uuid = Some(session_uuid);
82 Ok(client)
83 }
84
85 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 pub fn query_stream(&mut self, input: ClaudeInput) -> Result<ResponseIterator<'_>> {
96 Protocol::write_sync(&mut self.stdin, &input)?;
98
99 Ok(ResponseIterator {
100 client: self,
101 finished: false,
102 })
103 }
104
105 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 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 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 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 pub fn session_uuid(&self) -> Result<Uuid> {
178 self.session_uuid.ok_or(Error::SessionNotInitialized)
179 }
180
181 pub fn ping(&mut self) -> bool {
184 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 match self.query(ping_input) {
192 Ok(responses) => {
193 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
215pub 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 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}