Skip to main content

claude_code/client/
mod.rs

1use std::{
2    collections::BTreeMap,
3    future::Future,
4    path::PathBuf,
5    pin::Pin,
6    sync::Arc,
7    task::{Context, Poll},
8    time::Duration,
9};
10
11use futures_core::Stream;
12use tokio::{
13    io::{AsyncBufReadExt, AsyncRead, AsyncReadExt, AsyncWriteExt, BufReader},
14    process::Command,
15    sync::{mpsc, oneshot},
16    time,
17};
18
19use crate::{
20    builder::ClaudeClientBuilder,
21    commands::command::ClaudeCommandRequest,
22    commands::doctor::ClaudeDoctorRequest,
23    commands::mcp::{
24        McpAddFromClaudeDesktopRequest, McpAddJsonRequest, McpAddRequest, McpGetRequest,
25        McpRemoveRequest, McpServeRequest,
26    },
27    commands::plugin::{
28        PluginDisableRequest, PluginEnableRequest, PluginInstallRequest, PluginListRequest,
29        PluginManifestMarketplaceRequest, PluginManifestRequest, PluginMarketplaceAddRequest,
30        PluginMarketplaceListRequest, PluginMarketplaceRemoveRequest, PluginMarketplaceRepoRequest,
31        PluginMarketplaceRequest, PluginMarketplaceUpdateRequest, PluginRequest,
32        PluginUninstallRequest, PluginUpdateRequest, PluginValidateRequest,
33    },
34    commands::print::{ClaudeOutputFormat, ClaudePrintRequest},
35    commands::update::ClaudeUpdateRequest,
36    home::{ClaudeHomeLayout, ClaudeHomeSeedRequest},
37    parse_stream_json_lines, process, ClaudeCodeError, ClaudePrintStreamJsonControlHandle,
38    ClaudePrintStreamJsonHandle, ClaudeStreamJsonEvent, ClaudeStreamJsonParseError,
39    ClaudeStreamJsonParser, ClaudeTerminationHandle, CommandOutput, DynClaudeStreamJsonCompletion,
40    DynClaudeStreamJsonEventStream, StreamJsonLineOutcome,
41};
42
43mod setup_token;
44
45pub use setup_token::ClaudeSetupTokenSession;
46
47#[derive(Debug, Clone)]
48pub struct ClaudeClient {
49    pub(crate) binary: Option<PathBuf>,
50    pub(crate) working_dir: Option<PathBuf>,
51    pub(crate) env: BTreeMap<String, String>,
52    pub(crate) claude_home: Option<ClaudeHomeLayout>,
53    pub(crate) create_home_dirs: bool,
54    pub(crate) home_seed: Option<ClaudeHomeSeedRequest>,
55    pub(crate) home_materialize_status: Arc<std::sync::OnceLock<Result<(), String>>>,
56    pub(crate) home_seed_status: Arc<std::sync::OnceLock<Result<(), String>>>,
57    pub(crate) timeout: Option<Duration>,
58    pub(crate) mirror_stdout: bool,
59    pub(crate) mirror_stderr: bool,
60}
61
62impl ClaudeClient {
63    pub fn builder() -> ClaudeClientBuilder {
64        ClaudeClientBuilder::default()
65    }
66
67    pub async fn help(&self) -> Result<CommandOutput, ClaudeCodeError> {
68        self.run_command(ClaudeCommandRequest::root().arg("--help"))
69            .await
70    }
71
72    pub async fn version(&self) -> Result<CommandOutput, ClaudeCodeError> {
73        self.run_command(ClaudeCommandRequest::root().arg("--version"))
74            .await
75    }
76
77    pub async fn run_command(
78        &self,
79        request: ClaudeCommandRequest,
80    ) -> Result<CommandOutput, ClaudeCodeError> {
81        self.ensure_home_prepared()?;
82        let binary = self.resolve_binary();
83        let mut cmd = Command::new(&binary);
84        cmd.args(request.argv());
85
86        if let Some(dir) = self.working_dir.as_ref() {
87            cmd.current_dir(dir);
88        }
89
90        process::apply_env(&mut cmd, &self.env);
91
92        let timeout = request.timeout.or(self.timeout);
93        process::run_command(
94            cmd,
95            &binary,
96            request.stdin.as_deref(),
97            timeout,
98            self.mirror_stdout,
99            self.mirror_stderr,
100        )
101        .await
102    }
103
104    pub async fn print(
105        &self,
106        request: ClaudePrintRequest,
107    ) -> Result<ClaudePrintResult, ClaudeCodeError> {
108        let allow_missing_prompt = request.stdin.is_some()
109            || request.continue_session
110            || request.resume
111            || request.resume_value.is_some()
112            || request.from_pr
113            || request.from_pr_value.is_some();
114        if request.prompt.is_none() && !allow_missing_prompt {
115            return Err(ClaudeCodeError::InvalidRequest(
116                "either prompt, stdin_bytes, or a continuation flag must be provided".to_string(),
117            ));
118        }
119
120        self.ensure_home_prepared()?;
121        let binary = self.resolve_binary();
122        let mut cmd = Command::new(&binary);
123        cmd.args(request.argv());
124
125        if let Some(dir) = self.working_dir.as_ref() {
126            cmd.current_dir(dir);
127        }
128
129        process::apply_env(&mut cmd, &self.env);
130
131        let timeout = request.timeout.or(self.timeout);
132        let output = process::run_command(
133            cmd,
134            &binary,
135            request.stdin.as_deref(),
136            timeout,
137            self.mirror_stdout,
138            self.mirror_stderr,
139        )
140        .await?;
141
142        let parsed = match request.output_format {
143            ClaudeOutputFormat::Json => {
144                let v = serde_json::from_slice(&output.stdout)?;
145                Some(ClaudeParsedOutput::Json(v))
146            }
147            ClaudeOutputFormat::StreamJson => {
148                let s = String::from_utf8_lossy(&output.stdout);
149                Some(ClaudeParsedOutput::StreamJson(parse_stream_json_lines(&s)))
150            }
151            ClaudeOutputFormat::Text => None,
152        };
153
154        Ok(ClaudePrintResult { output, parsed })
155    }
156
157    pub fn print_stream_json(
158        &self,
159        request: ClaudePrintRequest,
160    ) -> Pin<
161        Box<dyn Future<Output = Result<ClaudePrintStreamJsonHandle, ClaudeCodeError>> + Send + '_>,
162    > {
163        Box::pin(async move {
164            let (events, completion, _termination) = self.spawn_print_stream_json(request).await?;
165            Ok(ClaudePrintStreamJsonHandle { events, completion })
166        })
167    }
168
169    pub fn print_stream_json_control(
170        &self,
171        request: ClaudePrintRequest,
172    ) -> Pin<
173        Box<
174            dyn Future<Output = Result<ClaudePrintStreamJsonControlHandle, ClaudeCodeError>>
175                + Send
176                + '_,
177        >,
178    > {
179        Box::pin(async move {
180            let (events, completion, termination) = self.spawn_print_stream_json(request).await?;
181            Ok(ClaudePrintStreamJsonControlHandle {
182                events,
183                completion,
184                termination,
185            })
186        })
187    }
188
189    async fn spawn_print_stream_json(
190        &self,
191        request: ClaudePrintRequest,
192    ) -> Result<
193        (
194            DynClaudeStreamJsonEventStream,
195            DynClaudeStreamJsonCompletion,
196            ClaudeTerminationHandle,
197        ),
198        ClaudeCodeError,
199    > {
200        let allow_missing_prompt = request.stdin.is_some()
201            || request.continue_session
202            || request.resume
203            || request.resume_value.is_some()
204            || request.from_pr
205            || request.from_pr_value.is_some();
206        if request.prompt.is_none() && !allow_missing_prompt {
207            return Err(ClaudeCodeError::InvalidRequest(
208                "either prompt, stdin_bytes, or a continuation flag must be provided".to_string(),
209            ));
210        }
211
212        self.ensure_home_prepared()?;
213        let binary = self.resolve_binary();
214
215        let mut request = request;
216        request.output_format = ClaudeOutputFormat::StreamJson;
217        let stdin_bytes = request.stdin.take();
218        let mirror_stdout = self.mirror_stdout;
219        let mirror_stderr = self.mirror_stderr;
220        let timeout = request.timeout.or(self.timeout);
221
222        let mut cmd = Command::new(&binary);
223        cmd.args(request.argv());
224
225        if let Some(dir) = self.working_dir.as_ref() {
226            cmd.current_dir(dir);
227        }
228
229        process::apply_env(&mut cmd, &self.env);
230
231        cmd.kill_on_drop(true);
232        cmd.stdin(if stdin_bytes.is_some() {
233            std::process::Stdio::piped()
234        } else {
235            std::process::Stdio::null()
236        });
237        cmd.stdout(std::process::Stdio::piped());
238        cmd.stderr(if mirror_stderr {
239            std::process::Stdio::piped()
240        } else {
241            std::process::Stdio::null()
242        });
243
244        let mut child = process::spawn_with_retry(&mut cmd, &binary)?;
245
246        if let Some(bytes) = stdin_bytes {
247            if let Some(mut stdin) = child.stdin.take() {
248                stdin
249                    .write_all(&bytes)
250                    .await
251                    .map_err(ClaudeCodeError::StdinWrite)?;
252            }
253        }
254
255        let stdout = child.stdout.take().ok_or(ClaudeCodeError::MissingStdout)?;
256        let stderr = if mirror_stderr {
257            Some(child.stderr.take().ok_or(ClaudeCodeError::MissingStderr)?)
258        } else {
259            None
260        };
261
262        let termination = ClaudeTerminationHandle::new();
263        let termination_for_runner = termination.clone();
264
265        let (events_tx, events_rx) = mpsc::channel(32);
266        let (completion_tx, completion_rx) = oneshot::channel();
267
268        tokio::spawn(async move {
269            let res = run_print_stream_json_child(
270                child,
271                stdout,
272                stderr,
273                events_tx,
274                mirror_stdout,
275                timeout,
276                termination_for_runner,
277            )
278            .await;
279            let _ = completion_tx.send(res);
280        });
281
282        let events: DynClaudeStreamJsonEventStream =
283            Box::pin(ClaudeStreamJsonEventChannelStream::new(events_rx));
284
285        let completion: DynClaudeStreamJsonCompletion = Box::pin(async move {
286            completion_rx
287                .await
288                .map_err(|_| ClaudeCodeError::Join("stream-json task dropped".to_string()))?
289        });
290
291        Ok((events, completion, termination))
292    }
293
294    pub async fn mcp_list(&self) -> Result<CommandOutput, ClaudeCodeError> {
295        self.run_command(ClaudeCommandRequest::new(["mcp", "list"]))
296            .await
297    }
298
299    pub async fn mcp_reset_project_choices(&self) -> Result<CommandOutput, ClaudeCodeError> {
300        self.run_command(ClaudeCommandRequest::new(["mcp", "reset-project-choices"]))
301            .await
302    }
303
304    pub async fn mcp_get(&self, req: McpGetRequest) -> Result<CommandOutput, ClaudeCodeError> {
305        self.run_command(req.into_command()).await
306    }
307
308    pub async fn mcp_add(&self, req: McpAddRequest) -> Result<CommandOutput, ClaudeCodeError> {
309        self.run_command(req.into_command()).await
310    }
311
312    pub async fn mcp_remove(
313        &self,
314        req: McpRemoveRequest,
315    ) -> Result<CommandOutput, ClaudeCodeError> {
316        self.run_command(req.into_command()).await
317    }
318
319    pub async fn mcp_add_json(
320        &self,
321        req: McpAddJsonRequest,
322    ) -> Result<CommandOutput, ClaudeCodeError> {
323        self.run_command(req.into_command()).await
324    }
325
326    pub async fn mcp_serve(&self, req: McpServeRequest) -> Result<CommandOutput, ClaudeCodeError> {
327        self.run_command(req.into_command()).await
328    }
329
330    pub async fn mcp_add_from_claude_desktop(
331        &self,
332        req: McpAddFromClaudeDesktopRequest,
333    ) -> Result<CommandOutput, ClaudeCodeError> {
334        self.run_command(req.into_command()).await
335    }
336
337    pub async fn doctor(&self) -> Result<CommandOutput, ClaudeCodeError> {
338        self.doctor_with(ClaudeDoctorRequest::new()).await
339    }
340
341    pub async fn doctor_with(
342        &self,
343        req: ClaudeDoctorRequest,
344    ) -> Result<CommandOutput, ClaudeCodeError> {
345        self.run_command(req.into_command()).await
346    }
347
348    pub async fn plugin_list(
349        &self,
350        req: PluginListRequest,
351    ) -> Result<CommandOutput, ClaudeCodeError> {
352        self.run_command(req.into_command()).await
353    }
354
355    pub async fn plugin(&self, req: PluginRequest) -> Result<CommandOutput, ClaudeCodeError> {
356        self.run_command(req.into_command()).await
357    }
358
359    pub async fn plugin_enable(
360        &self,
361        req: PluginEnableRequest,
362    ) -> Result<CommandOutput, ClaudeCodeError> {
363        self.run_command(req.into_command()).await
364    }
365
366    pub async fn plugin_disable(
367        &self,
368        req: PluginDisableRequest,
369    ) -> Result<CommandOutput, ClaudeCodeError> {
370        self.run_command(req.into_command()).await
371    }
372
373    pub async fn plugin_install(
374        &self,
375        req: PluginInstallRequest,
376    ) -> Result<CommandOutput, ClaudeCodeError> {
377        self.run_command(req.into_command()).await
378    }
379
380    pub async fn plugin_uninstall(
381        &self,
382        req: PluginUninstallRequest,
383    ) -> Result<CommandOutput, ClaudeCodeError> {
384        self.run_command(req.into_command()).await
385    }
386
387    pub async fn plugin_update(
388        &self,
389        req: PluginUpdateRequest,
390    ) -> Result<CommandOutput, ClaudeCodeError> {
391        self.run_command(req.into_command()).await
392    }
393
394    pub async fn plugin_validate(
395        &self,
396        req: PluginValidateRequest,
397    ) -> Result<CommandOutput, ClaudeCodeError> {
398        self.run_command(req.into_command()).await
399    }
400
401    pub async fn plugin_manifest(
402        &self,
403        req: PluginManifestRequest,
404    ) -> Result<CommandOutput, ClaudeCodeError> {
405        self.run_command(req.into_command()).await
406    }
407
408    pub async fn plugin_manifest_marketplace(
409        &self,
410        req: PluginManifestMarketplaceRequest,
411    ) -> Result<CommandOutput, ClaudeCodeError> {
412        self.run_command(req.into_command()).await
413    }
414
415    pub async fn plugin_marketplace_repo(
416        &self,
417        req: PluginMarketplaceRepoRequest,
418    ) -> Result<CommandOutput, ClaudeCodeError> {
419        self.run_command(req.into_command()).await
420    }
421
422    pub async fn plugin_marketplace(
423        &self,
424        req: PluginMarketplaceRequest,
425    ) -> Result<CommandOutput, ClaudeCodeError> {
426        self.run_command(req.into_command()).await
427    }
428
429    pub async fn plugin_marketplace_add(
430        &self,
431        req: PluginMarketplaceAddRequest,
432    ) -> Result<CommandOutput, ClaudeCodeError> {
433        self.run_command(req.into_command()).await
434    }
435
436    pub async fn plugin_marketplace_list(
437        &self,
438        req: PluginMarketplaceListRequest,
439    ) -> Result<CommandOutput, ClaudeCodeError> {
440        self.run_command(req.into_command()).await
441    }
442
443    pub async fn plugin_marketplace_remove(
444        &self,
445        req: PluginMarketplaceRemoveRequest,
446    ) -> Result<CommandOutput, ClaudeCodeError> {
447        self.run_command(req.into_command()).await
448    }
449
450    pub async fn plugin_marketplace_update(
451        &self,
452        req: PluginMarketplaceUpdateRequest,
453    ) -> Result<CommandOutput, ClaudeCodeError> {
454        self.run_command(req.into_command()).await
455    }
456
457    pub async fn update(&self) -> Result<CommandOutput, ClaudeCodeError> {
458        self.update_with(ClaudeUpdateRequest::new()).await
459    }
460
461    pub async fn update_with(
462        &self,
463        req: ClaudeUpdateRequest,
464    ) -> Result<CommandOutput, ClaudeCodeError> {
465        self.run_command(req.into_command()).await
466    }
467
468    pub fn claude_home_layout(&self) -> Option<ClaudeHomeLayout> {
469        self.claude_home.clone()
470    }
471
472    fn resolve_binary(&self) -> PathBuf {
473        if let Some(b) = self.binary.as_ref() {
474            return b.clone();
475        }
476        if let Ok(v) = std::env::var("CLAUDE_BINARY") {
477            if !v.trim().is_empty() {
478                return PathBuf::from(v);
479            }
480        }
481        PathBuf::from("claude")
482    }
483
484    fn ensure_home_prepared(&self) -> Result<(), ClaudeCodeError> {
485        if self.claude_home.is_none() {
486            return Ok(());
487        }
488
489        let materialize = self.home_materialize_status.get_or_init(|| {
490            let Some(layout) = self.claude_home.as_ref() else {
491                return Ok(());
492            };
493            layout
494                .materialize(self.create_home_dirs)
495                .map_err(|e| e.to_string())
496        });
497        if let Err(msg) = materialize {
498            return Err(ClaudeCodeError::ClaudeHomePrepareFailed(msg.clone()));
499        }
500
501        let seeded = self.home_seed_status.get_or_init(|| {
502            let Some(layout) = self.claude_home.as_ref() else {
503                return Ok(());
504            };
505            let Some(seed_req) = self.home_seed.as_ref() else {
506                return Ok(());
507            };
508            // Seeding implies directories must exist even when the caller disabled auto-creation.
509            let _ = layout.materialize(true);
510            layout
511                .seed_from_user_home(&seed_req.seed_user_home, seed_req.level)
512                .map(|_| ())
513                .map_err(|e| e.to_string())
514        });
515        if let Err(msg) = seeded {
516            return Err(ClaudeCodeError::ClaudeHomeSeedFailed(msg.clone()));
517        }
518
519        Ok(())
520    }
521}
522
523struct ClaudeStreamJsonEventChannelStream {
524    rx: mpsc::Receiver<Result<ClaudeStreamJsonEvent, ClaudeStreamJsonParseError>>,
525}
526
527impl ClaudeStreamJsonEventChannelStream {
528    fn new(rx: mpsc::Receiver<Result<ClaudeStreamJsonEvent, ClaudeStreamJsonParseError>>) -> Self {
529        Self { rx }
530    }
531}
532
533impl Stream for ClaudeStreamJsonEventChannelStream {
534    type Item = Result<ClaudeStreamJsonEvent, ClaudeStreamJsonParseError>;
535
536    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
537        let this = self.get_mut();
538        this.rx.poll_recv(cx)
539    }
540}
541
542async fn mirror_child_stream_to_parent_stderr<R>(mut reader: R) -> Result<(), std::io::Error>
543where
544    R: AsyncRead + Unpin,
545{
546    let mut out = tokio::io::stderr();
547    let mut chunk = [0u8; 4096];
548    loop {
549        let n = reader.read(&mut chunk).await?;
550        if n == 0 {
551            break;
552        }
553        out.write_all(&chunk[..n]).await?;
554        out.flush().await?;
555    }
556    Ok(())
557}
558
559async fn run_print_stream_json_child(
560    mut child: tokio::process::Child,
561    stdout: tokio::process::ChildStdout,
562    stderr: Option<tokio::process::ChildStderr>,
563    events_tx: mpsc::Sender<Result<ClaudeStreamJsonEvent, ClaudeStreamJsonParseError>>,
564    mirror_stdout: bool,
565    timeout: Option<Duration>,
566    termination: ClaudeTerminationHandle,
567) -> Result<std::process::ExitStatus, ClaudeCodeError> {
568    let mut parser = ClaudeStreamJsonParser::new();
569    let mut lines = BufReader::new(stdout).lines();
570    let mut stdout_mirror = mirror_stdout.then(tokio::io::stdout);
571
572    let stderr_task =
573        stderr.map(|stderr| tokio::spawn(mirror_child_stream_to_parent_stderr(stderr)));
574
575    let started = time::Instant::now();
576    let deadline = timeout.map(|dur| started + dur);
577    let mut timeout_sleep: Option<Pin<Box<time::Sleep>>> =
578        deadline.map(|deadline| Box::pin(time::sleep_until(deadline)));
579
580    let mut timed_out = false;
581    let mut cancelled = false;
582    let mut io_error: Option<ClaudeCodeError> = None;
583
584    let closed_tx = events_tx.clone();
585
586    loop {
587        let next = tokio::select! {
588            _ = closed_tx.closed() => {
589                cancelled = true;
590                break;
591            }
592            _ = termination.requested() => {
593                cancelled = true;
594                break;
595            }
596            _ = async {
597                if let Some(sleep) = timeout_sleep.as_mut() {
598                    sleep.as_mut().await;
599                } else {
600                    std::future::pending::<()>().await;
601                }
602            } => {
603                timed_out = timeout.is_some();
604                break;
605            }
606            res = lines.next_line() => res,
607        };
608
609        let line = match next {
610            Ok(Some(line)) => line,
611            Ok(None) => break,
612            Err(err) => {
613                io_error = Some(ClaudeCodeError::StdoutRead(err));
614                break;
615            }
616        };
617
618        if line.trim().is_empty() {
619            continue;
620        }
621
622        if let Some(out) = stdout_mirror.as_mut() {
623            let res: Result<(), std::io::Error> = async {
624                use tokio::io::AsyncWriteExt as _;
625                out.write_all(line.as_bytes()).await?;
626                out.write_all(b"\n").await?;
627                out.flush().await
628            }
629            .await;
630
631            if let Err(err) = res {
632                io_error = Some(ClaudeCodeError::StdoutRead(err));
633                break;
634            }
635        }
636
637        let outcome = match parser.parse_line(&line) {
638            Ok(Some(event)) => Some(Ok(event)),
639            Ok(None) => None,
640            Err(err) => Some(Err(err)),
641        };
642        let Some(outcome) = outcome else {
643            continue;
644        };
645
646        let send_fut = events_tx.send(outcome);
647        tokio::select! {
648            _ = closed_tx.closed() => {
649                cancelled = true;
650                break;
651            }
652            _ = termination.requested() => {
653                cancelled = true;
654                break;
655            }
656            _ = async {
657                if let Some(sleep) = timeout_sleep.as_mut() {
658                    sleep.as_mut().await;
659                } else {
660                    std::future::pending::<()>().await;
661                }
662            } => {
663                timed_out = timeout.is_some();
664                break;
665            }
666            res = send_fut => {
667                if res.is_err() {
668                    cancelled = true;
669                    break;
670                }
671            }
672        }
673    }
674
675    // Close the event channel as soon as we stop producing events so downstream consumers can
676    // observe stream finality independent of process-exit cleanup.
677    drop(events_tx);
678    drop(closed_tx);
679    drop(lines);
680
681    // `start_kill` may leave a zombie until `wait()` reaps the process (Tokio docs). For Agent API
682    // DR-0012 / CA-C02 invariants, completion must not resolve before backend process exit.
683    if timed_out || cancelled || io_error.is_some() {
684        let _ = child.start_kill();
685    }
686
687    let status: Result<std::process::ExitStatus, ClaudeCodeError> = match io_error {
688        Some(err) => {
689            let _ = child.wait().await;
690            Err(err)
691        }
692        None if cancelled => child.wait().await.map_err(ClaudeCodeError::Wait),
693        None if timed_out => {
694            let timeout = timeout.expect("timed_out implies timeout");
695            match child.wait().await.map_err(ClaudeCodeError::Wait) {
696                Ok(_status) => Err(ClaudeCodeError::Timeout { timeout }),
697                Err(err) => Err(err),
698            }
699        }
700        None => {
701            if let Some(deadline) = deadline {
702                let remaining = deadline.saturating_duration_since(time::Instant::now());
703                if remaining.is_zero() {
704                    let timeout = timeout.expect("deadline implies timeout");
705                    let _ = child.start_kill();
706                    match child.wait().await.map_err(ClaudeCodeError::Wait) {
707                        Ok(_status) => Err(ClaudeCodeError::Timeout { timeout }),
708                        Err(err) => Err(err),
709                    }
710                } else {
711                    match time::timeout(remaining, child.wait()).await {
712                        Ok(res) => res.map_err(ClaudeCodeError::Wait),
713                        Err(_) => {
714                            let timeout = timeout.expect("deadline implies timeout");
715                            let _ = child.start_kill();
716                            match child.wait().await.map_err(ClaudeCodeError::Wait) {
717                                Ok(_status) => Err(ClaudeCodeError::Timeout { timeout }),
718                                Err(err) => Err(err),
719                            }
720                        }
721                    }
722                }
723            } else {
724                child.wait().await.map_err(ClaudeCodeError::Wait)
725            }
726        }
727    };
728
729    if let Some(task) = stderr_task {
730        match task.await {
731            Ok(Ok(())) => {}
732            Ok(Err(err)) => {
733                if status.is_ok() {
734                    return Err(ClaudeCodeError::StderrRead(err));
735                }
736            }
737            Err(err) => {
738                if status.is_ok() {
739                    return Err(ClaudeCodeError::Join(err.to_string()));
740                }
741            }
742        }
743    }
744
745    status
746}
747
748#[derive(Debug, Clone)]
749pub struct ClaudePrintResult {
750    pub output: CommandOutput,
751    pub parsed: Option<ClaudeParsedOutput>,
752}
753
754#[derive(Debug, Clone)]
755pub enum ClaudeParsedOutput {
756    Json(serde_json::Value),
757    StreamJson(Vec<StreamJsonLineOutcome>),
758}