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(p.to_string());
185 }
186
187 args
188 }
189
190 fn build_resume_args(&self, session_id: Option<&str>) -> Vec<String> {
192 let mut args = Vec::new();
193 let in_sandbox = self.common.sandbox.is_some();
194
195 if let Some(id) = session_id {
196 args.extend(["--resume".to_string(), id.to_string()]);
197 } else {
198 args.push("--continue".to_string());
199 }
200
201 if self.common.skip_permissions && !in_sandbox {
202 args.push("--dangerously-skip-permissions".to_string());
203 }
204
205 args.extend(["--model".to_string(), self.common.model.clone()]);
206
207 for dir in &self.common.add_dirs {
208 args.extend(["--add-dir".to_string(), dir.clone()]);
209 }
210
211 args
212 }
213
214 fn make_command(&self, agent_args: Vec<String>) -> Command {
216 self.common.make_command("claude", agent_args)
217 }
218
219 pub fn execute_streaming(
234 &self,
235 prompt: Option<&str>,
236 ) -> Result<crate::streaming::StreamingSession> {
237 let mut args = Vec::new();
239 let in_sandbox = self.common.sandbox.is_some();
240
241 args.push("--print".to_string());
242 args.extend(["--verbose", "--output-format", "stream-json"].map(String::from));
243
244 if self.common.skip_permissions && !in_sandbox {
245 args.push("--dangerously-skip-permissions".to_string());
246 }
247
248 args.extend(["--model".to_string(), self.common.model.clone()]);
249
250 for dir in &self.common.add_dirs {
251 args.extend(["--add-dir".to_string(), dir.clone()]);
252 }
253
254 if !self.common.system_prompt.is_empty() {
255 args.extend([
256 "--append-system-prompt".to_string(),
257 self.common.system_prompt.clone(),
258 ]);
259 }
260
261 args.extend(["--input-format".to_string(), "stream-json".to_string()]);
262 args.push("--replay-user-messages".to_string());
263
264 if self.include_partial_messages {
265 args.push("--include-partial-messages".to_string());
266 }
267
268 if let Some(ref schema) = self.json_schema {
269 args.extend(["--json-schema".to_string(), schema.clone()]);
270 }
271
272 if let Some(p) = prompt {
273 args.push(p.to_string());
274 }
275
276 log::debug!("Claude streaming command: claude {}", args.join(" "));
277
278 let mut cmd = self.make_command(args);
279 cmd.stdin(Stdio::piped())
280 .stdout(Stdio::piped())
281 .stderr(Stdio::piped());
282
283 let child = cmd
284 .spawn()
285 .context("Failed to execute 'claude' CLI. Is it installed and in PATH?")?;
286 crate::streaming::StreamingSession::new(child)
287 }
288
289 fn build_streaming_resume_args(&self, session_id: &str) -> Vec<String> {
291 let mut args = Vec::new();
292 let in_sandbox = self.common.sandbox.is_some();
293
294 args.push("--print".to_string());
295 args.extend(["--resume".to_string(), session_id.to_string()]);
296 args.extend(["--verbose", "--output-format", "stream-json"].map(String::from));
297
298 if self.common.skip_permissions && !in_sandbox {
299 args.push("--dangerously-skip-permissions".to_string());
300 }
301
302 args.extend(["--model".to_string(), self.common.model.clone()]);
303
304 for dir in &self.common.add_dirs {
305 args.extend(["--add-dir".to_string(), dir.clone()]);
306 }
307
308 args.extend(["--input-format".to_string(), "stream-json".to_string()]);
309 args.push("--replay-user-messages".to_string());
310
311 if self.include_partial_messages {
312 args.push("--include-partial-messages".to_string());
313 }
314
315 args
316 }
317
318 pub fn execute_streaming_resume(
327 &self,
328 session_id: &str,
329 ) -> Result<crate::streaming::StreamingSession> {
330 let args = self.build_streaming_resume_args(session_id);
331
332 log::debug!("Claude streaming resume command: claude {}", args.join(" "));
333
334 let mut cmd = self.make_command(args);
335 cmd.stdin(Stdio::piped())
336 .stdout(Stdio::piped())
337 .stderr(Stdio::piped());
338
339 let child = cmd
340 .spawn()
341 .context("Failed to execute 'claude' CLI. Is it installed and in PATH?")?;
342 crate::streaming::StreamingSession::new(child)
343 }
344
345 async fn execute(
346 &self,
347 interactive: bool,
348 prompt: Option<&str>,
349 ) -> Result<Option<AgentOutput>> {
350 let effective_output_format =
353 if self.common.capture_output && self.common.output_format.is_none() {
354 Some("json".to_string())
355 } else {
356 self.common.output_format.clone()
357 };
358
359 let capture_json = !interactive
362 && effective_output_format
363 .as_ref()
364 .is_none_or(|f| f == "json" || f == "json-pretty" || f == "stream-json");
365
366 let agent_args = self.build_run_args(interactive, prompt, &effective_output_format);
367 log::debug!("Claude command: claude {}", agent_args.join(" "));
368 if !self.common.system_prompt.is_empty() {
369 log::debug!("Claude system prompt: {}", self.common.system_prompt);
370 }
371 if let Some(p) = prompt {
372 log::debug!("Claude user prompt: {p}");
373 }
374 log::debug!(
375 "Claude mode: interactive={interactive}, capture_json={capture_json}, output_format={effective_output_format:?}"
376 );
377 let mut cmd = self.make_command(agent_args);
378
379 let is_native_json = effective_output_format.as_deref() == Some("native-json");
381
382 if interactive {
383 cmd.stdin(Stdio::inherit())
385 .stdout(Stdio::inherit())
386 .stderr(Stdio::inherit());
387
388 let status = cmd
389 .status()
390 .await
391 .context("Failed to execute 'claude' CLI. Is it installed and in PATH?")?;
392 if !status.success() {
393 return Err(crate::process::ProcessError {
394 exit_code: status.code(),
395 stderr: String::new(),
396 agent_name: "Claude".to_string(),
397 }
398 .into());
399 }
400 Ok(None)
401 } else if is_native_json {
402 cmd.stdin(Stdio::inherit()).stdout(Stdio::inherit());
404
405 crate::process::run_with_captured_stderr(&mut cmd).await?;
406 Ok(None)
407 } else if capture_json {
408 let output_format = effective_output_format.as_deref();
409 let is_streaming = output_format == Some("stream-json") || output_format.is_none();
410
411 if is_streaming {
412 cmd.stdin(Stdio::inherit());
414 cmd.stdout(Stdio::piped());
415
416 let mut child = crate::process::spawn_with_captured_stderr(&mut cmd).await?;
417 let stdout = child
418 .stdout
419 .take()
420 .ok_or_else(|| anyhow::anyhow!("Failed to capture stdout"))?;
421
422 let reader = BufReader::new(stdout);
423 let mut lines = reader.lines();
424
425 let format_as_text = output_format.is_none(); let format_as_json = output_format == Some("stream-json"); let mut translator = ClaudeEventTranslator::new();
432
433 while let Some(line) = lines.next_line().await? {
435 if format_as_text || format_as_json {
436 match serde_json::from_str::<models::ClaudeEvent>(&line) {
437 Ok(claude_event) => {
438 for unified_event in translator.translate(&claude_event) {
439 if let Some(ref handler) = self.event_handler {
440 handler(&unified_event, self.verbose);
441 }
442 }
443 }
444 Err(e) => {
445 log::debug!(
446 "Failed to parse streaming Claude event: {}. Line: {}",
447 e,
448 crate::truncate_str(&line, 200)
449 );
450 }
451 }
452 }
453 }
454
455 if let Some(ref handler) = self.event_handler {
457 handler(
459 &crate::output::Event::Result {
460 success: true,
461 message: None,
462 duration_ms: None,
463 num_turns: None,
464 },
465 self.verbose,
466 );
467 }
468
469 crate::process::wait_with_stderr(child).await?;
470
471 Ok(None)
473 } else {
474 cmd.stdin(Stdio::inherit());
476 cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
477
478 let output = cmd.output().await?;
479
480 crate::process::handle_output(&output, "Claude")?;
481
482 let json_str = String::from_utf8(output.stdout)?;
484 log::debug!("Parsing Claude JSON output ({} bytes)", json_str.len());
485 let claude_output: models::ClaudeOutput =
486 serde_json::from_str(&json_str).map_err(|e| {
487 log::debug!(
488 "Failed to parse Claude JSON output: {}. First 500 chars: {}",
489 e,
490 crate::truncate_str(&json_str, 500)
491 );
492 anyhow::anyhow!("Failed to parse Claude JSON output: {e}")
493 })?;
494 log::debug!("Parsed {} Claude events successfully", claude_output.len());
495
496 if let Ok(raw_events) = serde_json::from_str::<Vec<serde_json::Value>>(&json_str) {
498 let known = ["system", "assistant", "user", "result"];
499 for raw in &raw_events {
500 if let Some(t) = raw.get("type").and_then(|v| v.as_str()) {
501 if !known.contains(&t) {
502 log::debug!(
503 "Unknown Claude event type: {:?} (first 300 chars: {})",
504 t,
505 crate::truncate_str(
506 &serde_json::to_string(raw).unwrap_or_default(),
507 300
508 )
509 );
510 }
511 }
512 }
513 }
514
515 let agent_output: AgentOutput =
517 models::claude_output_to_agent_output(claude_output);
518 Ok(Some(agent_output))
519 }
520 } else {
521 cmd.stdin(Stdio::inherit()).stdout(Stdio::inherit());
523
524 crate::process::run_with_captured_stderr(&mut cmd).await?;
525 Ok(None)
526 }
527 }
528}
529
530#[derive(Debug, Default)]
543pub(crate) struct ClaudeEventTranslator {
544 pending_stop_reason: Option<String>,
547 pending_usage: Option<crate::output::Usage>,
549 next_turn_index: u32,
552 last_assistant_text: Option<String>,
555 tool_name_by_id: std::collections::HashMap<String, String>,
559}
560
561impl ClaudeEventTranslator {
562 pub(crate) fn new() -> Self {
563 Self::default()
564 }
565
566 pub(crate) fn translate(&mut self, event: &models::ClaudeEvent) -> Vec<crate::output::Event> {
572 use crate::output::{Event as UnifiedEvent, Usage as UnifiedUsage};
573
574 if let models::ClaudeEvent::Assistant { message, .. } = event {
580 if let Some(reason) = &message.stop_reason {
581 self.pending_stop_reason = Some(reason.clone());
582 }
583 self.pending_usage = Some(UnifiedUsage {
584 input_tokens: message.usage.input_tokens,
585 output_tokens: message.usage.output_tokens,
586 cache_read_tokens: Some(message.usage.cache_read_input_tokens),
587 cache_creation_tokens: Some(message.usage.cache_creation_input_tokens),
588 web_search_requests: message
589 .usage
590 .server_tool_use
591 .as_ref()
592 .map(|s| s.web_search_requests),
593 web_fetch_requests: message
594 .usage
595 .server_tool_use
596 .as_ref()
597 .map(|s| s.web_fetch_requests),
598 });
599
600 let text_parts: Vec<&str> = message
602 .content
603 .iter()
604 .filter_map(|b| match b {
605 models::ContentBlock::Text { text } => Some(text.as_str()),
606 _ => None,
607 })
608 .collect();
609 if !text_parts.is_empty() {
610 self.last_assistant_text = Some(text_parts.join("\n"));
611 }
612
613 for block in &message.content {
616 if let models::ContentBlock::ToolUse { id, name, .. } = block {
617 self.tool_name_by_id.insert(id.clone(), name.clone());
618 }
619 }
620 }
621
622 let unified = convert_claude_event_to_unified(event);
623
624 match unified {
625 Some(UnifiedEvent::Result {
626 success,
627 message,
628 duration_ms,
629 num_turns,
630 }) if message.as_deref() == Some("") => {
631 let fallback = self.last_assistant_text.take();
633 if fallback.is_some() {
634 log::debug!(
635 "Streaming Result.message is empty; using last assistant text as fallback"
636 );
637 }
638 let result_event = UnifiedEvent::Result {
639 success,
640 message: fallback.or(message),
641 duration_ms,
642 num_turns,
643 };
644 let turn_complete = UnifiedEvent::TurnComplete {
645 stop_reason: self.pending_stop_reason.take(),
646 turn_index: self.next_turn_index,
647 usage: self.pending_usage.take(),
648 };
649 self.next_turn_index = self.next_turn_index.saturating_add(1);
650 vec![turn_complete, result_event]
651 }
652 Some(UnifiedEvent::Result { .. }) => {
653 let turn_complete = UnifiedEvent::TurnComplete {
654 stop_reason: self.pending_stop_reason.take(),
655 turn_index: self.next_turn_index,
656 usage: self.pending_usage.take(),
657 };
658 self.next_turn_index = self.next_turn_index.saturating_add(1);
659 vec![turn_complete, unified.unwrap()]
660 }
661 Some(UnifiedEvent::ToolExecution {
662 tool_name,
663 tool_id,
664 input,
665 result,
666 parent_tool_use_id,
667 }) => {
668 let resolved_name = self
671 .tool_name_by_id
672 .get(&tool_id)
673 .cloned()
674 .unwrap_or(tool_name);
675 vec![UnifiedEvent::ToolExecution {
676 tool_name: resolved_name,
677 tool_id,
678 input,
679 result,
680 parent_tool_use_id,
681 }]
682 }
683 Some(ev) => vec![ev],
684 None => Vec::new(),
685 }
686 }
687}
688
689pub(crate) fn convert_claude_event_to_unified(
696 event: &models::ClaudeEvent,
697) -> Option<crate::output::Event> {
698 use crate::output::{
699 ContentBlock as UnifiedContentBlock, Event as UnifiedEvent, ToolResult,
700 Usage as UnifiedUsage,
701 };
702 use models::ClaudeEvent;
703
704 match event {
705 ClaudeEvent::System {
706 model, tools, cwd, ..
707 } => {
708 let mut metadata = std::collections::HashMap::new();
709 if let Some(cwd_val) = cwd {
710 metadata.insert("cwd".to_string(), serde_json::json!(cwd_val));
711 }
712
713 Some(UnifiedEvent::Init {
714 model: model.clone(),
715 tools: tools.clone(),
716 working_directory: cwd.clone(),
717 metadata,
718 })
719 }
720
721 ClaudeEvent::Assistant {
722 message,
723 parent_tool_use_id,
724 ..
725 } => {
726 let content: Vec<UnifiedContentBlock> = message
728 .content
729 .iter()
730 .filter_map(|block| match block {
731 models::ContentBlock::Text { text } => {
732 Some(UnifiedContentBlock::Text { text: text.clone() })
733 }
734 models::ContentBlock::ToolUse { id, name, input } => {
735 Some(UnifiedContentBlock::ToolUse {
736 id: id.clone(),
737 name: name.clone(),
738 input: input.clone(),
739 })
740 }
741 models::ContentBlock::Thinking { .. } | models::ContentBlock::Other => None,
742 })
743 .collect();
744
745 let usage = Some(UnifiedUsage {
747 input_tokens: message.usage.input_tokens,
748 output_tokens: message.usage.output_tokens,
749 cache_read_tokens: Some(message.usage.cache_read_input_tokens),
750 cache_creation_tokens: Some(message.usage.cache_creation_input_tokens),
751 web_search_requests: message
752 .usage
753 .server_tool_use
754 .as_ref()
755 .map(|s| s.web_search_requests),
756 web_fetch_requests: message
757 .usage
758 .server_tool_use
759 .as_ref()
760 .map(|s| s.web_fetch_requests),
761 });
762
763 Some(UnifiedEvent::AssistantMessage {
764 content,
765 usage,
766 parent_tool_use_id: parent_tool_use_id.clone(),
767 })
768 }
769
770 ClaudeEvent::User {
771 message,
772 tool_use_result,
773 parent_tool_use_id,
774 ..
775 } => {
776 let first_tool_result = message.content.iter().find_map(|b| {
780 if let models::UserContentBlock::ToolResult {
781 tool_use_id,
782 content,
783 is_error,
784 } = b
785 {
786 Some((tool_use_id, content, is_error))
787 } else {
788 None
789 }
790 });
791
792 if let Some((tool_use_id, content, is_error)) = first_tool_result {
793 let tool_result = ToolResult {
794 success: !is_error,
795 output: if !is_error {
796 Some(content.clone())
797 } else {
798 None
799 },
800 error: if *is_error {
801 Some(content.clone())
802 } else {
803 None
804 },
805 data: tool_use_result.clone(),
806 };
807
808 Some(UnifiedEvent::ToolExecution {
809 tool_name: "unknown".to_string(),
810 tool_id: tool_use_id.clone(),
811 input: serde_json::Value::Null,
812 result: tool_result,
813 parent_tool_use_id: parent_tool_use_id.clone(),
814 })
815 } else {
816 let text_blocks: Vec<UnifiedContentBlock> = message
818 .content
819 .iter()
820 .filter_map(|b| {
821 if let models::UserContentBlock::Text { text } = b {
822 Some(UnifiedContentBlock::Text { text: text.clone() })
823 } else {
824 None
825 }
826 })
827 .collect();
828
829 if !text_blocks.is_empty() {
830 Some(UnifiedEvent::UserMessage {
831 content: text_blocks,
832 })
833 } else {
834 None
835 }
836 }
837 }
838
839 ClaudeEvent::Other => {
840 log::debug!("Skipping unknown Claude event type during streaming conversion");
841 None
842 }
843
844 ClaudeEvent::Result {
845 is_error,
846 result,
847 duration_ms,
848 num_turns,
849 structured_output,
850 ..
851 } => {
852 let effective_result = if result.is_empty() {
855 if let Some(so) = structured_output {
856 log::debug!("Streaming Result.result is empty; using structured_output");
857 serde_json::to_string(so).unwrap_or_default()
858 } else {
859 result.clone()
860 }
861 } else {
862 result.clone()
863 };
864 Some(UnifiedEvent::Result {
865 success: !is_error,
866 message: Some(effective_result),
867 duration_ms: Some(*duration_ms),
868 num_turns: Some(*num_turns),
869 })
870 }
871 }
872}
873
874#[cfg(test)]
875#[path = "claude_tests.rs"]
876mod tests;
877
878impl Default for Claude {
879 fn default() -> Self {
880 Self::new()
881 }
882}
883
884#[async_trait]
885impl Agent for Claude {
886 fn name(&self) -> &str {
887 "claude"
888 }
889
890 fn default_model() -> &'static str {
891 DEFAULT_MODEL
892 }
893
894 fn model_for_size(size: ModelSize) -> &'static str {
895 match size {
896 ModelSize::Small => "haiku",
897 ModelSize::Medium => "sonnet",
898 ModelSize::Large => "default",
899 }
900 }
901
902 fn available_models() -> &'static [&'static str] {
903 AVAILABLE_MODELS
904 }
905
906 crate::providers::common::impl_common_agent_setters!();
907
908 fn set_skip_permissions(&mut self, skip: bool) {
909 self.common.skip_permissions = skip;
910 }
911
912 crate::providers::common::impl_as_any!();
913
914 async fn run(&self, prompt: Option<&str>) -> Result<Option<AgentOutput>> {
915 self.execute(false, prompt).await
916 }
917
918 async fn run_interactive(&self, prompt: Option<&str>) -> Result<()> {
919 self.execute(true, prompt).await?;
920 Ok(())
921 }
922
923 async fn run_resume(&self, session_id: Option<&str>, _last: bool) -> Result<()> {
924 let agent_args = self.build_resume_args(session_id);
925 let mut cmd = self.make_command(agent_args);
926
927 cmd.stdin(Stdio::inherit())
928 .stdout(Stdio::inherit())
929 .stderr(Stdio::inherit());
930
931 let status = cmd
932 .status()
933 .await
934 .context("Failed to execute 'claude' CLI. Is it installed and in PATH?")?;
935 if !status.success() {
936 return Err(crate::process::ProcessError {
937 exit_code: status.code(),
938 stderr: String::new(),
939 agent_name: "Claude".to_string(),
940 }
941 .into());
942 }
943 Ok(())
944 }
945
946 async fn run_resume_with_prompt(
947 &self,
948 session_id: &str,
949 prompt: &str,
950 ) -> Result<Option<AgentOutput>> {
951 log::debug!("Claude resume with prompt: session={session_id}, prompt={prompt}");
952 let in_sandbox = self.common.sandbox.is_some();
953 let mut args = vec!["--print".to_string()];
954 args.extend(["--resume".to_string(), session_id.to_string()]);
955 args.extend(["--verbose", "--output-format", "json"].map(String::from));
956
957 if self.common.skip_permissions && !in_sandbox {
958 args.push("--dangerously-skip-permissions".to_string());
959 }
960
961 args.extend(["--model".to_string(), self.common.model.clone()]);
962
963 for dir in &self.common.add_dirs {
964 args.extend(["--add-dir".to_string(), dir.clone()]);
965 }
966
967 if let Some(ref schema) = self.json_schema {
968 args.extend(["--json-schema".to_string(), schema.clone()]);
969 }
970
971 args.push(prompt.to_string());
972
973 let mut cmd = self.make_command(args);
974
975 cmd.stdin(Stdio::inherit());
976 cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
977
978 let output = cmd.output().await?;
979
980 crate::process::handle_output(&output, "Claude")?;
981
982 let json_str = String::from_utf8(output.stdout)?;
984 log::debug!(
985 "Parsing Claude resume JSON output ({} bytes)",
986 json_str.len()
987 );
988 let claude_output: models::ClaudeOutput = serde_json::from_str(&json_str)
989 .map_err(|e| anyhow::anyhow!("Failed to parse Claude resume JSON output: {e}"))?;
990
991 let agent_output: AgentOutput = models::claude_output_to_agent_output(claude_output);
992 Ok(Some(agent_output))
993 }
994
995 async fn cleanup(&self) -> Result<()> {
996 Ok(())
997 }
998}