Skip to main content

zag_agent/providers/claude/
mod.rs

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