Skip to main content

zag_agent/providers/claude/
mod.rs

1// provider-updated: 2026-04-04
2pub mod logs;
3/// Claude agent implementation.
4///
5/// This module provides the Claude agent implementation, including:
6/// - Agent trait implementation for executing Claude commands
7/// - JSON output models for parsing Claude's verbose output
8/// - Conversion to unified AgentOutput format
9pub mod models;
10
11use crate::agent::{Agent, ModelSize};
12
13/// Return the Claude projects directory: `~/.claude/projects/`.
14pub fn projects_dir() -> Option<std::path::PathBuf> {
15    dirs::home_dir().map(|h| h.join(".claude/projects"))
16}
17use crate::output::AgentOutput;
18use crate::sandbox::SandboxConfig;
19use anyhow::{Context, Result};
20use async_trait::async_trait;
21use std::process::Stdio;
22use tokio::io::{AsyncBufReadExt, BufReader};
23use tokio::process::Command;
24
25pub const DEFAULT_MODEL: &str = "default";
26
27pub const AVAILABLE_MODELS: &[&str] = &[
28    "default",
29    "sonnet",
30    "sonnet-4.6",
31    "opus",
32    "opus-4.6",
33    "haiku",
34    "haiku-4.5",
35];
36
37/// Callback for streaming events. Set via `set_event_handler` to receive
38/// unified events as they arrive during non-interactive execution.
39pub type EventHandler = Box<dyn Fn(&crate::output::Event, bool) + Send + Sync>;
40
41pub struct Claude {
42    system_prompt: String,
43    model: String,
44    root: Option<String>,
45    session_id: Option<String>,
46    skip_permissions: bool,
47    output_format: Option<String>,
48    input_format: Option<String>,
49    add_dirs: Vec<String>,
50    capture_output: bool,
51    verbose: bool,
52    json_schema: Option<String>,
53    sandbox: Option<SandboxConfig>,
54    event_handler: Option<EventHandler>,
55    replay_user_messages: bool,
56    include_partial_messages: bool,
57    max_turns: Option<u32>,
58}
59
60impl Claude {
61    pub fn new() -> Self {
62        Self {
63            system_prompt: String::new(),
64            model: DEFAULT_MODEL.to_string(),
65            root: None,
66            session_id: None,
67            skip_permissions: false,
68            output_format: None,
69            input_format: None,
70            add_dirs: Vec::new(),
71            capture_output: false,
72            verbose: false,
73            json_schema: None,
74            sandbox: None,
75            event_handler: None,
76            replay_user_messages: false,
77            include_partial_messages: false,
78            max_turns: None,
79        }
80    }
81
82    pub fn set_input_format(&mut self, format: Option<String>) {
83        self.input_format = format;
84    }
85
86    pub fn set_session_id(&mut self, session_id: String) {
87        self.session_id = Some(session_id);
88    }
89
90    pub fn set_verbose(&mut self, verbose: bool) {
91        self.verbose = verbose;
92    }
93
94    pub fn set_json_schema(&mut self, schema: Option<String>) {
95        self.json_schema = schema;
96    }
97
98    pub fn set_replay_user_messages(&mut self, replay: bool) {
99        self.replay_user_messages = replay;
100    }
101
102    pub fn set_include_partial_messages(&mut self, include: bool) {
103        self.include_partial_messages = include;
104    }
105
106    /// Set a callback to receive streaming events during non-interactive execution.
107    ///
108    /// The callback receives `(event, verbose)` where `verbose` indicates whether
109    /// the user requested verbose output.
110    pub fn set_event_handler(&mut self, handler: EventHandler) {
111        self.event_handler = Some(handler);
112    }
113
114    /// Build the argument list for a run/exec invocation.
115    fn build_run_args(
116        &self,
117        interactive: bool,
118        prompt: Option<&str>,
119        effective_output_format: &Option<String>,
120    ) -> Vec<String> {
121        let mut args = Vec::new();
122        let in_sandbox = self.sandbox.is_some();
123
124        if !interactive {
125            args.push("--print".to_string());
126
127            match effective_output_format.as_deref() {
128                Some("json") | Some("json-pretty") => {
129                    args.extend(["--verbose", "--output-format", "json"].map(String::from));
130                }
131                Some("stream-json") | None => {
132                    args.extend(["--verbose", "--output-format", "stream-json"].map(String::from));
133                }
134                Some("native-json") => {
135                    args.extend(["--verbose", "--output-format", "json"].map(String::from));
136                }
137                Some("text") => {}
138                _ => {}
139            }
140        }
141
142        // Skip --dangerously-skip-permissions in sandbox (permissions are sandbox-default)
143        if self.skip_permissions && !in_sandbox {
144            args.push("--dangerously-skip-permissions".to_string());
145        }
146
147        args.extend(["--model".to_string(), self.model.clone()]);
148
149        if interactive && let Some(session_id) = &self.session_id {
150            args.extend(["--session-id".to_string(), session_id.clone()]);
151        }
152
153        for dir in &self.add_dirs {
154            args.extend(["--add-dir".to_string(), dir.clone()]);
155        }
156
157        if !self.system_prompt.is_empty() {
158            args.extend([
159                "--append-system-prompt".to_string(),
160                self.system_prompt.clone(),
161            ]);
162        }
163
164        if !interactive && let Some(ref input_fmt) = self.input_format {
165            args.extend(["--input-format".to_string(), input_fmt.clone()]);
166        }
167
168        if !interactive && self.replay_user_messages {
169            args.push("--replay-user-messages".to_string());
170        }
171
172        if !interactive && self.include_partial_messages {
173            args.push("--include-partial-messages".to_string());
174        }
175
176        if let Some(ref schema) = self.json_schema {
177            args.extend(["--json-schema".to_string(), schema.clone()]);
178        }
179
180        if let Some(turns) = self.max_turns {
181            args.extend(["--max-turns".to_string(), turns.to_string()]);
182        }
183
184        if let Some(p) = prompt {
185            args.push(p.to_string());
186        }
187
188        args
189    }
190
191    /// Build the argument list for a resume invocation.
192    fn build_resume_args(&self, session_id: Option<&str>) -> Vec<String> {
193        let mut args = Vec::new();
194        let in_sandbox = self.sandbox.is_some();
195
196        if let Some(id) = session_id {
197            args.extend(["--resume".to_string(), id.to_string()]);
198        } else {
199            args.push("--continue".to_string());
200        }
201
202        if self.skip_permissions && !in_sandbox {
203            args.push("--dangerously-skip-permissions".to_string());
204        }
205
206        args.extend(["--model".to_string(), self.model.clone()]);
207
208        for dir in &self.add_dirs {
209            args.extend(["--add-dir".to_string(), dir.clone()]);
210        }
211
212        args
213    }
214
215    /// Create a `Command` either directly or wrapped in sandbox.
216    fn make_command(&self, agent_args: Vec<String>) -> Command {
217        if let Some(ref sb) = self.sandbox {
218            let std_cmd = crate::sandbox::build_sandbox_command(sb, agent_args);
219            Command::from(std_cmd)
220        } else {
221            let mut cmd = Command::new("claude");
222            if let Some(ref root) = self.root {
223                cmd.current_dir(root);
224            }
225            cmd.args(&agent_args);
226            cmd
227        }
228    }
229
230    /// Spawn a streaming session with piped stdin/stdout.
231    ///
232    /// Automatically configures `--input-format stream-json`, `--output-format stream-json`,
233    /// and `--replay-user-messages`. Returns a `StreamingSession` for bidirectional
234    /// communication with the agent.
235    pub fn execute_streaming(
236        &self,
237        prompt: Option<&str>,
238    ) -> Result<crate::streaming::StreamingSession> {
239        // Build args for non-interactive streaming mode
240        let mut args = Vec::new();
241        let in_sandbox = self.sandbox.is_some();
242
243        args.push("--print".to_string());
244        args.extend(["--verbose", "--output-format", "stream-json"].map(String::from));
245
246        if self.skip_permissions && !in_sandbox {
247            args.push("--dangerously-skip-permissions".to_string());
248        }
249
250        args.extend(["--model".to_string(), self.model.clone()]);
251
252        for dir in &self.add_dirs {
253            args.extend(["--add-dir".to_string(), dir.clone()]);
254        }
255
256        if !self.system_prompt.is_empty() {
257            args.extend([
258                "--append-system-prompt".to_string(),
259                self.system_prompt.clone(),
260            ]);
261        }
262
263        args.extend(["--input-format".to_string(), "stream-json".to_string()]);
264        args.push("--replay-user-messages".to_string());
265
266        if self.include_partial_messages {
267            args.push("--include-partial-messages".to_string());
268        }
269
270        if let Some(ref schema) = self.json_schema {
271            args.extend(["--json-schema".to_string(), schema.clone()]);
272        }
273
274        if let Some(p) = prompt {
275            args.push(p.to_string());
276        }
277
278        log::debug!("Claude streaming command: claude {}", args.join(" "));
279
280        let mut cmd = self.make_command(args);
281        cmd.stdin(Stdio::piped())
282            .stdout(Stdio::piped())
283            .stderr(Stdio::piped());
284
285        let child = cmd
286            .spawn()
287            .context("Failed to execute 'claude' CLI. Is it installed and in PATH?")?;
288        crate::streaming::StreamingSession::new(child)
289    }
290
291    /// Build argument list for a streaming resume invocation.
292    fn build_streaming_resume_args(&self, session_id: &str) -> Vec<String> {
293        let mut args = Vec::new();
294        let in_sandbox = self.sandbox.is_some();
295
296        args.push("--print".to_string());
297        args.extend(["--resume".to_string(), session_id.to_string()]);
298        args.extend(["--verbose", "--output-format", "stream-json"].map(String::from));
299
300        if self.skip_permissions && !in_sandbox {
301            args.push("--dangerously-skip-permissions".to_string());
302        }
303
304        args.extend(["--model".to_string(), self.model.clone()]);
305
306        for dir in &self.add_dirs {
307            args.extend(["--add-dir".to_string(), dir.clone()]);
308        }
309
310        args.extend(["--input-format".to_string(), "stream-json".to_string()]);
311        args.push("--replay-user-messages".to_string());
312
313        if self.include_partial_messages {
314            args.push("--include-partial-messages".to_string());
315        }
316
317        args
318    }
319
320    /// Spawn a streaming session that resumes an existing session.
321    ///
322    /// Combines `--resume` with `--input-format stream-json`, `--output-format stream-json`,
323    /// and `--replay-user-messages`. Returns a `StreamingSession` for bidirectional
324    /// communication with the resumed session.
325    pub fn execute_streaming_resume(
326        &self,
327        session_id: &str,
328    ) -> Result<crate::streaming::StreamingSession> {
329        let args = self.build_streaming_resume_args(session_id);
330
331        log::debug!("Claude streaming resume command: claude {}", args.join(" "));
332
333        let mut cmd = self.make_command(args);
334        cmd.stdin(Stdio::piped())
335            .stdout(Stdio::piped())
336            .stderr(Stdio::piped());
337
338        let child = cmd
339            .spawn()
340            .context("Failed to execute 'claude' CLI. Is it installed and in PATH?")?;
341        crate::streaming::StreamingSession::new(child)
342    }
343
344    async fn execute(
345        &self,
346        interactive: bool,
347        prompt: Option<&str>,
348    ) -> Result<Option<AgentOutput>> {
349        // When capture_output is set (e.g. by auto-selector), use "json" format
350        // so stdout is piped and parsed into AgentOutput
351        let effective_output_format = if self.capture_output && self.output_format.is_none() {
352            Some("json".to_string())
353        } else {
354            self.output_format.clone()
355        };
356
357        // Determine if we should capture structured output
358        // Default to streaming unified output when no format is specified in print mode
359        let capture_json = !interactive
360            && effective_output_format
361                .as_ref()
362                .is_none_or(|f| f == "json" || f == "json-pretty" || f == "stream-json");
363
364        let agent_args = self.build_run_args(interactive, prompt, &effective_output_format);
365        log::debug!("Claude command: claude {}", agent_args.join(" "));
366        if !self.system_prompt.is_empty() {
367            log::debug!("Claude system prompt: {}", self.system_prompt);
368        }
369        if let Some(p) = prompt {
370            log::debug!("Claude user prompt: {}", p);
371        }
372        log::debug!(
373            "Claude mode: interactive={}, capture_json={}, output_format={:?}",
374            interactive,
375            capture_json,
376            effective_output_format
377        );
378        let mut cmd = self.make_command(agent_args);
379
380        // Check if we should pass through native JSON without conversion
381        let is_native_json = effective_output_format.as_deref() == Some("native-json");
382
383        if interactive {
384            // Interactive mode - inherit all stdio
385            cmd.stdin(Stdio::inherit())
386                .stdout(Stdio::inherit())
387                .stderr(Stdio::inherit());
388
389            let status = cmd
390                .status()
391                .await
392                .context("Failed to execute 'claude' CLI. Is it installed and in PATH?")?;
393            if !status.success() {
394                anyhow::bail!("Claude command failed with status: {}", status);
395            }
396            Ok(None)
397        } else if is_native_json {
398            // Native JSON mode - pass through Claude's raw JSON output, capture stderr
399            cmd.stdin(Stdio::inherit()).stdout(Stdio::inherit());
400
401            crate::process::run_with_captured_stderr(&mut cmd).await?;
402            Ok(None)
403        } else if capture_json {
404            let output_format = effective_output_format.as_deref();
405            let is_streaming = output_format == Some("stream-json") || output_format.is_none();
406
407            if is_streaming {
408                // For stream-json or default (None), stream output and convert to unified format
409                cmd.stdin(Stdio::inherit());
410                cmd.stdout(Stdio::piped());
411
412                let mut child = crate::process::spawn_with_captured_stderr(&mut cmd).await?;
413                let stdout = child
414                    .stdout
415                    .take()
416                    .ok_or_else(|| anyhow::anyhow!("Failed to capture stdout"))?;
417
418                let reader = BufReader::new(stdout);
419                let mut lines = reader.lines();
420
421                // Determine output mode
422                let format_as_text = output_format.is_none(); // Default: beautiful text
423                let format_as_json = output_format == Some("stream-json"); // Explicit: unified JSON
424
425                // Stream each line, dispatching via event_handler if set
426                while let Some(line) = lines.next_line().await? {
427                    if format_as_text || format_as_json {
428                        match serde_json::from_str::<models::ClaudeEvent>(&line) {
429                            Ok(claude_event) => {
430                                if let Some(unified_event) =
431                                    convert_claude_event_to_unified(&claude_event)
432                                {
433                                    if let Some(ref handler) = self.event_handler {
434                                        handler(&unified_event, self.verbose);
435                                    }
436                                }
437                            }
438                            Err(e) => {
439                                log::debug!(
440                                    "Failed to parse streaming Claude event: {}. Line: {}",
441                                    e,
442                                    crate::truncate_str(&line, 200)
443                                );
444                            }
445                        }
446                    }
447                }
448
449                // Signal end of streaming to handler
450                if let Some(ref handler) = self.event_handler {
451                    // Send a Result event to signal completion
452                    handler(
453                        &crate::output::Event::Result {
454                            success: true,
455                            message: None,
456                            duration_ms: None,
457                            num_turns: None,
458                        },
459                        self.verbose,
460                    );
461                }
462
463                crate::process::wait_with_stderr(child).await?;
464
465                // Return None to indicate output was streamed directly
466                Ok(None)
467            } else {
468                // For json/json-pretty, capture all output then parse
469                cmd.stdin(Stdio::inherit());
470                cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
471
472                let output = cmd.output().await?;
473
474                crate::process::handle_output(&output, "Claude")?;
475
476                // Parse JSON output
477                let json_str = String::from_utf8(output.stdout)?;
478                log::debug!("Parsing Claude JSON output ({} bytes)", json_str.len());
479                let claude_output: models::ClaudeOutput =
480                    serde_json::from_str(&json_str).map_err(|e| {
481                        log::debug!(
482                            "Failed to parse Claude JSON output: {}. First 500 chars: {}",
483                            e,
484                            crate::truncate_str(&json_str, 500)
485                        );
486                        anyhow::anyhow!("Failed to parse Claude JSON output: {}", e)
487                    })?;
488                log::debug!("Parsed {} Claude events successfully", claude_output.len());
489
490                // Convert to unified AgentOutput
491                let agent_output: AgentOutput =
492                    models::claude_output_to_agent_output(claude_output);
493                Ok(Some(agent_output))
494            }
495        } else {
496            // Explicit text mode - inherit stdout, capture stderr
497            cmd.stdin(Stdio::inherit()).stdout(Stdio::inherit());
498
499            crate::process::run_with_captured_stderr(&mut cmd).await?;
500            Ok(None)
501        }
502    }
503}
504
505/// Convert a single Claude event to a unified event format.
506/// Returns None if the event doesn't map to a user-visible unified event.
507fn convert_claude_event_to_unified(event: &models::ClaudeEvent) -> Option<crate::output::Event> {
508    use crate::output::{
509        ContentBlock as UnifiedContentBlock, Event as UnifiedEvent, ToolResult,
510        Usage as UnifiedUsage,
511    };
512    use models::ClaudeEvent;
513
514    match event {
515        ClaudeEvent::System {
516            model, tools, cwd, ..
517        } => {
518            let mut metadata = std::collections::HashMap::new();
519            if let Some(cwd_val) = cwd {
520                metadata.insert("cwd".to_string(), serde_json::json!(cwd_val));
521            }
522
523            Some(UnifiedEvent::Init {
524                model: model.clone(),
525                tools: tools.clone(),
526                working_directory: cwd.clone(),
527                metadata,
528            })
529        }
530
531        ClaudeEvent::Assistant { message, .. } => {
532            // Convert content blocks
533            let content: Vec<UnifiedContentBlock> = message
534                .content
535                .iter()
536                .filter_map(|block| match block {
537                    models::ContentBlock::Text { text } => {
538                        Some(UnifiedContentBlock::Text { text: text.clone() })
539                    }
540                    models::ContentBlock::ToolUse { id, name, input } => {
541                        Some(UnifiedContentBlock::ToolUse {
542                            id: id.clone(),
543                            name: name.clone(),
544                            input: input.clone(),
545                        })
546                    }
547                    models::ContentBlock::Thinking { .. } => None,
548                })
549                .collect();
550
551            // Convert usage
552            let usage = Some(UnifiedUsage {
553                input_tokens: message.usage.input_tokens,
554                output_tokens: message.usage.output_tokens,
555                cache_read_tokens: Some(message.usage.cache_read_input_tokens),
556                cache_creation_tokens: Some(message.usage.cache_creation_input_tokens),
557                web_search_requests: message
558                    .usage
559                    .server_tool_use
560                    .as_ref()
561                    .map(|s| s.web_search_requests),
562                web_fetch_requests: message
563                    .usage
564                    .server_tool_use
565                    .as_ref()
566                    .map(|s| s.web_fetch_requests),
567            });
568
569            Some(UnifiedEvent::AssistantMessage { content, usage })
570        }
571
572        ClaudeEvent::User {
573            message,
574            tool_use_result,
575            ..
576        } => {
577            // For streaming, we can't easily look up tool names from previous events
578            // So we'll use "unknown" for the tool name in streaming mode
579            // Find the first tool_result block (skip text and other blocks)
580            let first_tool_result = message.content.iter().find_map(|b| {
581                if let models::UserContentBlock::ToolResult {
582                    tool_use_id,
583                    content,
584                    is_error,
585                } = b
586                {
587                    Some((tool_use_id, content, is_error))
588                } else {
589                    None
590                }
591            });
592
593            if let Some((tool_use_id, content, is_error)) = first_tool_result {
594                let tool_result = ToolResult {
595                    success: !is_error,
596                    output: if !is_error {
597                        Some(content.clone())
598                    } else {
599                        None
600                    },
601                    error: if *is_error {
602                        Some(content.clone())
603                    } else {
604                        None
605                    },
606                    data: tool_use_result.clone(),
607                };
608
609                Some(UnifiedEvent::ToolExecution {
610                    tool_name: "unknown".to_string(),
611                    tool_id: tool_use_id.clone(),
612                    input: serde_json::Value::Null,
613                    result: tool_result,
614                })
615            } else {
616                // Check for text content (replayed user messages via --replay-user-messages)
617                let text_blocks: Vec<UnifiedContentBlock> = message
618                    .content
619                    .iter()
620                    .filter_map(|b| {
621                        if let models::UserContentBlock::Text { text } = b {
622                            Some(UnifiedContentBlock::Text { text: text.clone() })
623                        } else {
624                            None
625                        }
626                    })
627                    .collect();
628
629                if !text_blocks.is_empty() {
630                    Some(UnifiedEvent::UserMessage {
631                        content: text_blocks,
632                    })
633                } else {
634                    None
635                }
636            }
637        }
638
639        ClaudeEvent::Other => {
640            log::debug!("Skipping unknown Claude event type during streaming conversion");
641            None
642        }
643
644        ClaudeEvent::Result {
645            is_error,
646            result,
647            duration_ms,
648            num_turns,
649            ..
650        } => Some(UnifiedEvent::Result {
651            success: !is_error,
652            message: Some(result.clone()),
653            duration_ms: Some(*duration_ms),
654            num_turns: Some(*num_turns),
655        }),
656    }
657}
658
659#[cfg(test)]
660#[path = "claude_tests.rs"]
661mod tests;
662
663impl Default for Claude {
664    fn default() -> Self {
665        Self::new()
666    }
667}
668
669#[async_trait]
670impl Agent for Claude {
671    fn name(&self) -> &str {
672        "claude"
673    }
674
675    fn default_model() -> &'static str {
676        DEFAULT_MODEL
677    }
678
679    fn model_for_size(size: ModelSize) -> &'static str {
680        match size {
681            ModelSize::Small => "haiku",
682            ModelSize::Medium => "sonnet",
683            ModelSize::Large => "default",
684        }
685    }
686
687    fn available_models() -> &'static [&'static str] {
688        AVAILABLE_MODELS
689    }
690
691    fn system_prompt(&self) -> &str {
692        &self.system_prompt
693    }
694
695    fn set_system_prompt(&mut self, prompt: String) {
696        self.system_prompt = prompt;
697    }
698
699    fn get_model(&self) -> &str {
700        &self.model
701    }
702
703    fn set_model(&mut self, model: String) {
704        self.model = model;
705    }
706
707    fn set_root(&mut self, root: String) {
708        self.root = Some(root);
709    }
710
711    fn set_skip_permissions(&mut self, skip: bool) {
712        self.skip_permissions = skip;
713    }
714
715    fn set_output_format(&mut self, format: Option<String>) {
716        self.output_format = format;
717    }
718
719    fn set_capture_output(&mut self, capture: bool) {
720        self.capture_output = capture;
721    }
722
723    fn set_max_turns(&mut self, turns: u32) {
724        self.max_turns = Some(turns);
725    }
726
727    fn set_sandbox(&mut self, config: SandboxConfig) {
728        self.sandbox = Some(config);
729    }
730
731    fn set_add_dirs(&mut self, dirs: Vec<String>) {
732        self.add_dirs = dirs;
733    }
734
735    fn as_any_ref(&self) -> &dyn std::any::Any {
736        self
737    }
738
739    fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
740        self
741    }
742
743    async fn run(&self, prompt: Option<&str>) -> Result<Option<AgentOutput>> {
744        self.execute(false, prompt).await
745    }
746
747    async fn run_interactive(&self, prompt: Option<&str>) -> Result<()> {
748        self.execute(true, prompt).await?;
749        Ok(())
750    }
751
752    async fn run_resume(&self, session_id: Option<&str>, _last: bool) -> Result<()> {
753        let agent_args = self.build_resume_args(session_id);
754        let mut cmd = self.make_command(agent_args);
755
756        cmd.stdin(Stdio::inherit())
757            .stdout(Stdio::inherit())
758            .stderr(Stdio::inherit());
759
760        let status = cmd
761            .status()
762            .await
763            .context("Failed to execute 'claude' CLI. Is it installed and in PATH?")?;
764        if !status.success() {
765            anyhow::bail!("Claude resume failed with status: {}", status);
766        }
767        Ok(())
768    }
769
770    async fn run_resume_with_prompt(
771        &self,
772        session_id: &str,
773        prompt: &str,
774    ) -> Result<Option<AgentOutput>> {
775        log::debug!(
776            "Claude resume with prompt: session={}, prompt={}",
777            session_id,
778            prompt
779        );
780        let in_sandbox = self.sandbox.is_some();
781        let mut args = vec!["--print".to_string()];
782        args.extend(["--resume".to_string(), session_id.to_string()]);
783        args.extend(["--verbose", "--output-format", "json"].map(String::from));
784
785        if self.skip_permissions && !in_sandbox {
786            args.push("--dangerously-skip-permissions".to_string());
787        }
788
789        args.extend(["--model".to_string(), self.model.clone()]);
790
791        for dir in &self.add_dirs {
792            args.extend(["--add-dir".to_string(), dir.clone()]);
793        }
794
795        if let Some(ref schema) = self.json_schema {
796            args.extend(["--json-schema".to_string(), schema.clone()]);
797        }
798
799        args.push(prompt.to_string());
800
801        let mut cmd = self.make_command(args);
802
803        cmd.stdin(Stdio::inherit());
804        cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
805
806        let output = cmd.output().await?;
807
808        crate::process::handle_output(&output, "Claude")?;
809
810        // Parse JSON output
811        let json_str = String::from_utf8(output.stdout)?;
812        log::debug!(
813            "Parsing Claude resume JSON output ({} bytes)",
814            json_str.len()
815        );
816        let claude_output: models::ClaudeOutput = serde_json::from_str(&json_str)
817            .map_err(|e| anyhow::anyhow!("Failed to parse Claude resume JSON output: {}", e))?;
818
819        let agent_output: AgentOutput = models::claude_output_to_agent_output(claude_output);
820        Ok(Some(agent_output))
821    }
822
823    async fn cleanup(&self) -> Result<()> {
824        Ok(())
825    }
826}