1pub mod logs;
3pub mod models;
10
11use crate::agent::{Agent, ModelSize};
12
13pub fn projects_dir() -> Option<std::path::PathBuf> {
15 dirs::home_dir().map(|h| h.join(".claude/projects"))
16}
17use crate::output::AgentOutput;
18use crate::providers::common::CommonAgentState;
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] = &["default", "sonnet", "opus", "haiku"];
28
29pub type EventHandler = Box<dyn Fn(&crate::output::Event, bool) + Send + Sync>;
32
33pub struct Claude {
34 pub common: CommonAgentState,
35 pub session_id: Option<String>,
36 pub input_format: Option<String>,
37 pub verbose: bool,
38 pub json_schema: Option<String>,
39 pub event_handler: Option<EventHandler>,
40 pub replay_user_messages: bool,
41 pub include_partial_messages: bool,
42 pub mcp_config_path: Option<String>,
43}
44
45impl Claude {
46 pub fn new() -> Self {
47 Self {
48 common: CommonAgentState::new(DEFAULT_MODEL),
49 session_id: None,
50 input_format: None,
51 verbose: false,
52 json_schema: None,
53 event_handler: None,
54 replay_user_messages: false,
55 include_partial_messages: false,
56 mcp_config_path: None,
57 }
58 }
59
60 pub fn set_input_format(&mut self, format: Option<String>) {
61 self.input_format = format;
62 }
63
64 pub fn set_session_id(&mut self, session_id: String) {
65 self.session_id = Some(session_id);
66 }
67
68 pub fn set_verbose(&mut self, verbose: bool) {
69 self.verbose = verbose;
70 }
71
72 pub fn set_json_schema(&mut self, schema: Option<String>) {
73 self.json_schema = schema;
74 }
75
76 pub fn set_replay_user_messages(&mut self, replay: bool) {
77 self.replay_user_messages = replay;
78 }
79
80 pub fn set_include_partial_messages(&mut self, include: bool) {
81 self.include_partial_messages = include;
82 }
83
84 pub fn set_mcp_config(&mut self, config: Option<String>) {
86 self.mcp_config_path = config.map(|c| {
87 if c.trim_start().starts_with('{') {
88 let path =
89 std::env::temp_dir().join(format!("zag-mcp-{}.json", uuid::Uuid::new_v4()));
90 if let Err(e) = std::fs::write(&path, &c) {
91 log::warn!("Failed to write MCP config temp file: {e}");
92 return c;
93 }
94 path.to_string_lossy().into_owned()
95 } else {
96 c
97 }
98 });
99 }
100
101 pub fn set_event_handler(&mut self, handler: EventHandler) {
106 self.event_handler = Some(handler);
107 }
108
109 fn build_run_args(
111 &self,
112 interactive: bool,
113 prompt: Option<&str>,
114 effective_output_format: &Option<String>,
115 ) -> Vec<String> {
116 let mut args = Vec::new();
117 let in_sandbox = self.common.sandbox.is_some();
118
119 if !interactive {
120 args.push("--print".to_string());
121
122 match effective_output_format.as_deref() {
123 Some("json") | Some("json-pretty") => {
124 args.extend(["--verbose", "--output-format", "json"].map(String::from));
125 }
126 Some("stream-json") | None => {
127 args.extend(["--verbose", "--output-format", "stream-json"].map(String::from));
128 }
129 Some("native-json") => {
130 args.extend(["--verbose", "--output-format", "json"].map(String::from));
131 }
132 Some("text") => {}
133 _ => {}
134 }
135 }
136
137 if self.common.skip_permissions && !in_sandbox {
139 args.push("--dangerously-skip-permissions".to_string());
140 }
141
142 args.extend(["--model".to_string(), self.common.model.clone()]);
143
144 if interactive && let Some(session_id) = &self.session_id {
145 args.extend(["--session-id".to_string(), session_id.clone()]);
146 }
147
148 for dir in &self.common.add_dirs {
149 args.extend(["--add-dir".to_string(), dir.clone()]);
150 }
151
152 if !self.common.system_prompt.is_empty() {
153 args.extend([
154 "--append-system-prompt".to_string(),
155 self.common.system_prompt.clone(),
156 ]);
157 }
158
159 if !interactive && let Some(ref input_fmt) = self.input_format {
160 args.extend(["--input-format".to_string(), input_fmt.clone()]);
161 }
162
163 if !interactive && self.replay_user_messages {
164 args.push("--replay-user-messages".to_string());
165 }
166
167 if !interactive && self.include_partial_messages {
168 args.push("--include-partial-messages".to_string());
169 }
170
171 if let Some(ref schema) = self.json_schema {
172 args.extend(["--json-schema".to_string(), schema.clone()]);
173 }
174
175 if let Some(turns) = self.common.max_turns {
176 args.extend(["--max-turns".to_string(), turns.to_string()]);
177 }
178
179 if let Some(ref path) = self.mcp_config_path {
180 args.extend(["--mcp-config".to_string(), path.clone()]);
181 }
182
183 if let Some(p) = prompt {
184 args.push("--".to_string());
189 args.push(p.to_string());
190 }
191
192 args
193 }
194
195 fn build_resume_args(&self, session_id: Option<&str>) -> Vec<String> {
197 let mut args = Vec::new();
198 let in_sandbox = self.common.sandbox.is_some();
199
200 if let Some(id) = session_id {
201 args.extend(["--resume".to_string(), id.to_string()]);
202 } else {
203 args.push("--continue".to_string());
204 }
205
206 if self.common.skip_permissions && !in_sandbox {
207 args.push("--dangerously-skip-permissions".to_string());
208 }
209
210 args.extend(["--model".to_string(), self.common.model.clone()]);
211
212 for dir in &self.common.add_dirs {
213 args.extend(["--add-dir".to_string(), dir.clone()]);
214 }
215
216 args
217 }
218
219 fn make_command(&self, agent_args: Vec<String>) -> Command {
221 self.common.make_command("claude", agent_args)
222 }
223
224 pub fn execute_streaming(
239 &self,
240 prompt: Option<&str>,
241 ) -> Result<crate::streaming::StreamingSession> {
242 let mut args = Vec::new();
244 let in_sandbox = self.common.sandbox.is_some();
245
246 args.push("--print".to_string());
247 args.extend(["--verbose", "--output-format", "stream-json"].map(String::from));
248
249 if self.common.skip_permissions && !in_sandbox {
250 args.push("--dangerously-skip-permissions".to_string());
251 }
252
253 args.extend(["--model".to_string(), self.common.model.clone()]);
254
255 for dir in &self.common.add_dirs {
256 args.extend(["--add-dir".to_string(), dir.clone()]);
257 }
258
259 if !self.common.system_prompt.is_empty() {
260 args.extend([
261 "--append-system-prompt".to_string(),
262 self.common.system_prompt.clone(),
263 ]);
264 }
265
266 args.extend(["--input-format".to_string(), "stream-json".to_string()]);
267 args.push("--replay-user-messages".to_string());
268
269 if self.include_partial_messages {
270 args.push("--include-partial-messages".to_string());
271 }
272
273 if let Some(ref schema) = self.json_schema {
274 args.extend(["--json-schema".to_string(), schema.clone()]);
275 }
276
277 if let Some(p) = prompt {
278 args.push("--".to_string());
279 args.push(p.to_string());
280 }
281
282 log::debug!("Claude streaming command: claude {}", args.join(" "));
283
284 let mut cmd = self.make_command(args);
285 cmd.stdin(Stdio::piped())
286 .stdout(Stdio::piped())
287 .stderr(Stdio::piped());
288
289 let child = cmd
290 .spawn()
291 .context("Failed to execute 'claude' CLI. Is it installed and in PATH?")?;
292 crate::streaming::StreamingSession::new(child)
293 }
294
295 fn build_streaming_resume_args(&self, session_id: &str) -> Vec<String> {
297 let mut args = Vec::new();
298 let in_sandbox = self.common.sandbox.is_some();
299
300 args.push("--print".to_string());
301 args.extend(["--resume".to_string(), session_id.to_string()]);
302 args.extend(["--verbose", "--output-format", "stream-json"].map(String::from));
303
304 if self.common.skip_permissions && !in_sandbox {
305 args.push("--dangerously-skip-permissions".to_string());
306 }
307
308 args.extend(["--model".to_string(), self.common.model.clone()]);
309
310 for dir in &self.common.add_dirs {
311 args.extend(["--add-dir".to_string(), dir.clone()]);
312 }
313
314 args.extend(["--input-format".to_string(), "stream-json".to_string()]);
315 args.push("--replay-user-messages".to_string());
316
317 if self.include_partial_messages {
318 args.push("--include-partial-messages".to_string());
319 }
320
321 args
322 }
323
324 pub fn execute_streaming_resume(
333 &self,
334 session_id: &str,
335 ) -> Result<crate::streaming::StreamingSession> {
336 let args = self.build_streaming_resume_args(session_id);
337
338 log::debug!("Claude streaming resume command: claude {}", args.join(" "));
339
340 let mut cmd = self.make_command(args);
341 cmd.stdin(Stdio::piped())
342 .stdout(Stdio::piped())
343 .stderr(Stdio::piped());
344
345 let child = cmd
346 .spawn()
347 .context("Failed to execute 'claude' CLI. Is it installed and in PATH?")?;
348 crate::streaming::StreamingSession::new(child)
349 }
350
351 async fn execute(
352 &self,
353 interactive: bool,
354 prompt: Option<&str>,
355 ) -> Result<Option<AgentOutput>> {
356 let effective_output_format =
359 if self.common.capture_output && self.common.output_format.is_none() {
360 Some("json".to_string())
361 } else {
362 self.common.output_format.clone()
363 };
364
365 let capture_json = !interactive
368 && effective_output_format
369 .as_ref()
370 .is_none_or(|f| f == "json" || f == "json-pretty" || f == "stream-json");
371
372 let agent_args = self.build_run_args(interactive, prompt, &effective_output_format);
373 log::debug!("Claude command: claude {}", agent_args.join(" "));
374 if !self.common.system_prompt.is_empty() {
375 log::debug!("Claude system prompt: {}", self.common.system_prompt);
376 }
377 if let Some(p) = prompt {
378 log::debug!("Claude user prompt: {p}");
379 }
380 log::debug!(
381 "Claude mode: interactive={interactive}, capture_json={capture_json}, output_format={effective_output_format:?}"
382 );
383 let mut cmd = self.make_command(agent_args);
384
385 let is_native_json = effective_output_format.as_deref() == Some("native-json");
387
388 if interactive {
389 cmd.stdin(Stdio::inherit())
394 .stdout(Stdio::inherit())
395 .stderr(Stdio::inherit());
396
397 let mut child = cmd
398 .spawn()
399 .context("Failed to execute 'claude' CLI. Is it installed and in PATH?")?;
400 self.common.notify_spawn(&child);
401 let status = child
402 .wait()
403 .await
404 .context("Failed waiting on 'claude' CLI")?;
405 if !status.success() {
406 return Err(crate::process::ProcessError {
407 exit_code: status.code(),
408 stderr: String::new(),
409 agent_name: "Claude".to_string(),
410 }
411 .into());
412 }
413 Ok(None)
414 } else if is_native_json {
415 cmd.stdin(Stdio::inherit()).stdout(Stdio::inherit());
417
418 crate::process::run_with_captured_stderr(&mut cmd).await?;
419 Ok(None)
420 } else if capture_json {
421 let output_format = effective_output_format.as_deref();
422 let is_streaming = output_format == Some("stream-json") || output_format.is_none();
423
424 if is_streaming {
425 cmd.stdin(Stdio::inherit());
427 cmd.stdout(Stdio::piped());
428
429 let mut child = crate::process::spawn_with_captured_stderr(&mut cmd).await?;
430 let stdout = child
431 .stdout
432 .take()
433 .ok_or_else(|| anyhow::anyhow!("Failed to capture stdout"))?;
434
435 let reader = BufReader::new(stdout);
436 let mut lines = reader.lines();
437
438 let format_as_text = output_format.is_none(); let format_as_json = output_format == Some("stream-json"); let mut translator = ClaudeEventTranslator::new();
445
446 while let Some(line) = lines.next_line().await? {
448 if format_as_text || format_as_json {
449 match serde_json::from_str::<models::ClaudeEvent>(&line) {
450 Ok(claude_event) => {
451 for unified_event in translator.translate(&claude_event) {
452 if let Some(ref handler) = self.event_handler {
453 handler(&unified_event, self.verbose);
454 }
455 }
456 }
457 Err(e) => {
458 log::debug!(
459 "Failed to parse streaming Claude event: {}. Line: {}",
460 e,
461 crate::truncate_str(&line, 200)
462 );
463 }
464 }
465 }
466 }
467
468 if let Some(ref handler) = self.event_handler {
470 handler(
472 &crate::output::Event::Result {
473 success: true,
474 message: None,
475 duration_ms: None,
476 num_turns: None,
477 },
478 self.verbose,
479 );
480 }
481
482 crate::process::wait_with_stderr(child).await?;
483
484 Ok(None)
486 } else {
487 cmd.stdin(Stdio::inherit());
489 cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
490
491 let output = cmd.output().await?;
492
493 crate::process::handle_output(&output, "Claude")?;
494
495 let json_str = String::from_utf8(output.stdout)?;
497 log::debug!("Parsing Claude JSON output ({} bytes)", json_str.len());
498 let claude_output: models::ClaudeOutput =
499 serde_json::from_str(&json_str).map_err(|e| {
500 log::debug!(
501 "Failed to parse Claude JSON output: {}. First 500 chars: {}",
502 e,
503 crate::truncate_str(&json_str, 500)
504 );
505 anyhow::anyhow!("Failed to parse Claude JSON output: {e}")
506 })?;
507 log::debug!("Parsed {} Claude events successfully", claude_output.len());
508
509 if let Ok(raw_events) = serde_json::from_str::<Vec<serde_json::Value>>(&json_str) {
511 let known = ["system", "assistant", "user", "result"];
512 for raw in &raw_events {
513 if let Some(t) = raw.get("type").and_then(|v| v.as_str()) {
514 if !known.contains(&t) {
515 log::debug!(
516 "Unknown Claude event type: {:?} (first 300 chars: {})",
517 t,
518 crate::truncate_str(
519 &serde_json::to_string(raw).unwrap_or_default(),
520 300
521 )
522 );
523 }
524 }
525 }
526 }
527
528 let agent_output: AgentOutput =
530 models::claude_output_to_agent_output(claude_output);
531 Ok(Some(agent_output))
532 }
533 } else {
534 cmd.stdin(Stdio::inherit()).stdout(Stdio::inherit());
536
537 crate::process::run_with_captured_stderr(&mut cmd).await?;
538 Ok(None)
539 }
540 }
541}
542
543#[derive(Debug, Default)]
556pub(crate) struct ClaudeEventTranslator {
557 pending_stop_reason: Option<String>,
560 pending_usage: Option<crate::output::Usage>,
562 next_turn_index: u32,
565 last_assistant_text: Option<String>,
568 tool_name_by_id: std::collections::HashMap<String, String>,
572}
573
574impl ClaudeEventTranslator {
575 pub(crate) fn new() -> Self {
576 Self::default()
577 }
578
579 pub(crate) fn translate(&mut self, event: &models::ClaudeEvent) -> Vec<crate::output::Event> {
585 use crate::output::{Event as UnifiedEvent, Usage as UnifiedUsage};
586
587 if let models::ClaudeEvent::Assistant { message, .. } = event {
593 if let Some(reason) = &message.stop_reason {
594 self.pending_stop_reason = Some(reason.clone());
595 }
596 self.pending_usage = Some(UnifiedUsage {
597 input_tokens: message.usage.input_tokens,
598 output_tokens: message.usage.output_tokens,
599 cache_read_tokens: Some(message.usage.cache_read_input_tokens),
600 cache_creation_tokens: Some(message.usage.cache_creation_input_tokens),
601 web_search_requests: message
602 .usage
603 .server_tool_use
604 .as_ref()
605 .map(|s| s.web_search_requests),
606 web_fetch_requests: message
607 .usage
608 .server_tool_use
609 .as_ref()
610 .map(|s| s.web_fetch_requests),
611 });
612
613 let text_parts: Vec<&str> = message
615 .content
616 .iter()
617 .filter_map(|b| match b {
618 models::ContentBlock::Text { text } => Some(text.as_str()),
619 _ => None,
620 })
621 .collect();
622 if !text_parts.is_empty() {
623 self.last_assistant_text = Some(text_parts.join("\n"));
624 }
625
626 for block in &message.content {
629 if let models::ContentBlock::ToolUse { id, name, .. } = block {
630 self.tool_name_by_id.insert(id.clone(), name.clone());
631 }
632 }
633 }
634
635 let unified = convert_claude_event_to_unified(event);
636
637 match unified {
638 Some(UnifiedEvent::Result {
639 success,
640 message,
641 duration_ms,
642 num_turns,
643 }) if message.as_deref() == Some("") => {
644 let fallback = self.last_assistant_text.take();
646 if fallback.is_some() {
647 log::debug!(
648 "Streaming Result.message is empty; using last assistant text as fallback"
649 );
650 }
651 let result_event = UnifiedEvent::Result {
652 success,
653 message: fallback.or(message),
654 duration_ms,
655 num_turns,
656 };
657 let turn_complete = UnifiedEvent::TurnComplete {
658 stop_reason: self.pending_stop_reason.take(),
659 turn_index: self.next_turn_index,
660 usage: self.pending_usage.take(),
661 };
662 self.next_turn_index = self.next_turn_index.saturating_add(1);
663 vec![turn_complete, result_event]
664 }
665 Some(UnifiedEvent::Result { .. }) => {
666 let turn_complete = UnifiedEvent::TurnComplete {
667 stop_reason: self.pending_stop_reason.take(),
668 turn_index: self.next_turn_index,
669 usage: self.pending_usage.take(),
670 };
671 self.next_turn_index = self.next_turn_index.saturating_add(1);
672 vec![turn_complete, unified.unwrap()]
673 }
674 Some(UnifiedEvent::ToolExecution {
675 tool_name,
676 tool_id,
677 input,
678 result,
679 parent_tool_use_id,
680 }) => {
681 let resolved_name = self
684 .tool_name_by_id
685 .get(&tool_id)
686 .cloned()
687 .unwrap_or(tool_name);
688 vec![UnifiedEvent::ToolExecution {
689 tool_name: resolved_name,
690 tool_id,
691 input,
692 result,
693 parent_tool_use_id,
694 }]
695 }
696 Some(ev) => vec![ev],
697 None => Vec::new(),
698 }
699 }
700}
701
702pub(crate) fn convert_claude_event_to_unified(
709 event: &models::ClaudeEvent,
710) -> Option<crate::output::Event> {
711 use crate::output::{
712 ContentBlock as UnifiedContentBlock, Event as UnifiedEvent, ToolResult,
713 Usage as UnifiedUsage,
714 };
715 use models::ClaudeEvent;
716
717 match event {
718 ClaudeEvent::System {
719 model, tools, cwd, ..
720 } => {
721 let mut metadata = std::collections::HashMap::new();
722 if let Some(cwd_val) = cwd {
723 metadata.insert("cwd".to_string(), serde_json::json!(cwd_val));
724 }
725
726 Some(UnifiedEvent::Init {
727 model: model.clone(),
728 tools: tools.clone(),
729 working_directory: cwd.clone(),
730 metadata,
731 })
732 }
733
734 ClaudeEvent::Assistant {
735 message,
736 parent_tool_use_id,
737 ..
738 } => {
739 let content: Vec<UnifiedContentBlock> = message
741 .content
742 .iter()
743 .filter_map(|block| match block {
744 models::ContentBlock::Text { text } => {
745 Some(UnifiedContentBlock::Text { text: text.clone() })
746 }
747 models::ContentBlock::ToolUse { id, name, input } => {
748 Some(UnifiedContentBlock::ToolUse {
749 id: id.clone(),
750 name: name.clone(),
751 input: input.clone(),
752 })
753 }
754 models::ContentBlock::Thinking { .. } | models::ContentBlock::Other => None,
755 })
756 .collect();
757
758 let usage = Some(UnifiedUsage {
760 input_tokens: message.usage.input_tokens,
761 output_tokens: message.usage.output_tokens,
762 cache_read_tokens: Some(message.usage.cache_read_input_tokens),
763 cache_creation_tokens: Some(message.usage.cache_creation_input_tokens),
764 web_search_requests: message
765 .usage
766 .server_tool_use
767 .as_ref()
768 .map(|s| s.web_search_requests),
769 web_fetch_requests: message
770 .usage
771 .server_tool_use
772 .as_ref()
773 .map(|s| s.web_fetch_requests),
774 });
775
776 Some(UnifiedEvent::AssistantMessage {
777 content,
778 usage,
779 parent_tool_use_id: parent_tool_use_id.clone(),
780 })
781 }
782
783 ClaudeEvent::User {
784 message,
785 tool_use_result,
786 parent_tool_use_id,
787 ..
788 } => {
789 let first_tool_result = message.content.iter().find_map(|b| {
793 if let models::UserContentBlock::ToolResult {
794 tool_use_id,
795 content,
796 is_error,
797 } = b
798 {
799 Some((tool_use_id, content, is_error))
800 } else {
801 None
802 }
803 });
804
805 if let Some((tool_use_id, content, is_error)) = first_tool_result {
806 let tool_result = ToolResult {
807 success: !is_error,
808 output: if !is_error {
809 Some(content.clone())
810 } else {
811 None
812 },
813 error: if *is_error {
814 Some(content.clone())
815 } else {
816 None
817 },
818 data: tool_use_result.clone(),
819 };
820
821 Some(UnifiedEvent::ToolExecution {
822 tool_name: "unknown".to_string(),
823 tool_id: tool_use_id.clone(),
824 input: serde_json::Value::Null,
825 result: tool_result,
826 parent_tool_use_id: parent_tool_use_id.clone(),
827 })
828 } else {
829 let text_blocks: Vec<UnifiedContentBlock> = message
831 .content
832 .iter()
833 .filter_map(|b| {
834 if let models::UserContentBlock::Text { text } = b {
835 Some(UnifiedContentBlock::Text { text: text.clone() })
836 } else {
837 None
838 }
839 })
840 .collect();
841
842 if !text_blocks.is_empty() {
843 Some(UnifiedEvent::UserMessage {
844 content: text_blocks,
845 })
846 } else {
847 None
848 }
849 }
850 }
851
852 ClaudeEvent::Other => {
853 log::debug!("Skipping unknown Claude event type during streaming conversion");
854 None
855 }
856
857 ClaudeEvent::Result {
858 is_error,
859 result,
860 duration_ms,
861 num_turns,
862 structured_output,
863 ..
864 } => {
865 let effective_result = if result.is_empty() {
868 if let Some(so) = structured_output {
869 log::debug!("Streaming Result.result is empty; using structured_output");
870 serde_json::to_string(so).unwrap_or_default()
871 } else {
872 result.clone()
873 }
874 } else {
875 result.clone()
876 };
877 Some(UnifiedEvent::Result {
878 success: !is_error,
879 message: Some(effective_result),
880 duration_ms: Some(*duration_ms),
881 num_turns: Some(*num_turns),
882 })
883 }
884 }
885}
886
887#[cfg(test)]
888#[path = "claude_tests.rs"]
889mod tests;
890
891impl Default for Claude {
892 fn default() -> Self {
893 Self::new()
894 }
895}
896
897#[async_trait]
898impl Agent for Claude {
899 fn name(&self) -> &str {
900 "claude"
901 }
902
903 fn default_model() -> &'static str {
904 DEFAULT_MODEL
905 }
906
907 fn model_for_size(size: ModelSize) -> &'static str {
908 match size {
909 ModelSize::Small => "haiku",
910 ModelSize::Medium => "sonnet",
911 ModelSize::Large => "default",
912 }
913 }
914
915 fn available_models() -> &'static [&'static str] {
916 AVAILABLE_MODELS
917 }
918
919 crate::providers::common::impl_common_agent_setters!();
920
921 fn set_skip_permissions(&mut self, skip: bool) {
922 self.common.skip_permissions = skip;
923 }
924
925 crate::providers::common::impl_as_any!();
926
927 async fn run(&self, prompt: Option<&str>) -> Result<Option<AgentOutput>> {
928 self.execute(false, prompt).await
929 }
930
931 async fn run_interactive(&self, prompt: Option<&str>) -> Result<()> {
932 self.execute(true, prompt).await?;
933 Ok(())
934 }
935
936 async fn run_resume(&self, session_id: Option<&str>, _last: bool) -> Result<()> {
937 let agent_args = self.build_resume_args(session_id);
938 let mut cmd = self.make_command(agent_args);
939
940 cmd.stdin(Stdio::inherit())
941 .stdout(Stdio::inherit())
942 .stderr(Stdio::inherit());
943
944 let mut child = cmd
947 .spawn()
948 .context("Failed to execute 'claude' CLI. Is it installed and in PATH?")?;
949 self.common.notify_spawn(&child);
950 let status = child
951 .wait()
952 .await
953 .context("Failed waiting on 'claude' CLI")?;
954 if !status.success() {
955 return Err(crate::process::ProcessError {
956 exit_code: status.code(),
957 stderr: String::new(),
958 agent_name: "Claude".to_string(),
959 }
960 .into());
961 }
962 Ok(())
963 }
964
965 async fn run_resume_with_prompt(
966 &self,
967 session_id: &str,
968 prompt: &str,
969 ) -> Result<Option<AgentOutput>> {
970 log::debug!("Claude resume with prompt: session={session_id}, prompt={prompt}");
971 let in_sandbox = self.common.sandbox.is_some();
972 let mut args = vec!["--print".to_string()];
973 args.extend(["--resume".to_string(), session_id.to_string()]);
974 args.extend(["--verbose", "--output-format", "json"].map(String::from));
975
976 if self.common.skip_permissions && !in_sandbox {
977 args.push("--dangerously-skip-permissions".to_string());
978 }
979
980 args.extend(["--model".to_string(), self.common.model.clone()]);
981
982 for dir in &self.common.add_dirs {
983 args.extend(["--add-dir".to_string(), dir.clone()]);
984 }
985
986 if let Some(ref schema) = self.json_schema {
987 args.extend(["--json-schema".to_string(), schema.clone()]);
988 }
989
990 args.push("--".to_string());
991 args.push(prompt.to_string());
992
993 let mut cmd = self.make_command(args);
994
995 cmd.stdin(Stdio::inherit());
996 cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
997
998 let output = cmd.output().await?;
999
1000 crate::process::handle_output(&output, "Claude")?;
1001
1002 let json_str = String::from_utf8(output.stdout)?;
1004 log::debug!(
1005 "Parsing Claude resume JSON output ({} bytes)",
1006 json_str.len()
1007 );
1008 let claude_output: models::ClaudeOutput = serde_json::from_str(&json_str)
1009 .map_err(|e| anyhow::anyhow!("Failed to parse Claude resume JSON output: {e}"))?;
1010
1011 let agent_output: AgentOutput = models::claude_output_to_agent_output(claude_output);
1012 Ok(Some(agent_output))
1013 }
1014
1015 async fn cleanup(&self) -> Result<()> {
1016 Ok(())
1017 }
1018}