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={}, capture_json={}, output_format={:?}",
376 interactive,
377 capture_json,
378 effective_output_format
379 );
380 let mut cmd = self.make_command(agent_args);
381
382 let is_native_json = effective_output_format.as_deref() == Some("native-json");
384
385 if interactive {
386 cmd.stdin(Stdio::inherit())
388 .stdout(Stdio::inherit())
389 .stderr(Stdio::inherit());
390
391 let status = cmd
392 .status()
393 .await
394 .context("Failed to execute 'claude' CLI. Is it installed and in PATH?")?;
395 if !status.success() {
396 return Err(crate::process::ProcessError {
397 exit_code: status.code(),
398 stderr: String::new(),
399 agent_name: "Claude".to_string(),
400 }
401 .into());
402 }
403 Ok(None)
404 } else if is_native_json {
405 cmd.stdin(Stdio::inherit()).stdout(Stdio::inherit());
407
408 crate::process::run_with_captured_stderr(&mut cmd).await?;
409 Ok(None)
410 } else if capture_json {
411 let output_format = effective_output_format.as_deref();
412 let is_streaming = output_format == Some("stream-json") || output_format.is_none();
413
414 if is_streaming {
415 cmd.stdin(Stdio::inherit());
417 cmd.stdout(Stdio::piped());
418
419 let mut child = crate::process::spawn_with_captured_stderr(&mut cmd).await?;
420 let stdout = child
421 .stdout
422 .take()
423 .ok_or_else(|| anyhow::anyhow!("Failed to capture stdout"))?;
424
425 let reader = BufReader::new(stdout);
426 let mut lines = reader.lines();
427
428 let format_as_text = output_format.is_none(); let format_as_json = output_format == Some("stream-json"); let mut translator = ClaudeEventTranslator::new();
435
436 while let Some(line) = lines.next_line().await? {
438 if format_as_text || format_as_json {
439 match serde_json::from_str::<models::ClaudeEvent>(&line) {
440 Ok(claude_event) => {
441 for unified_event in translator.translate(&claude_event) {
442 if let Some(ref handler) = self.event_handler {
443 handler(&unified_event, self.verbose);
444 }
445 }
446 }
447 Err(e) => {
448 log::debug!(
449 "Failed to parse streaming Claude event: {}. Line: {}",
450 e,
451 crate::truncate_str(&line, 200)
452 );
453 }
454 }
455 }
456 }
457
458 if let Some(ref handler) = self.event_handler {
460 handler(
462 &crate::output::Event::Result {
463 success: true,
464 message: None,
465 duration_ms: None,
466 num_turns: None,
467 },
468 self.verbose,
469 );
470 }
471
472 crate::process::wait_with_stderr(child).await?;
473
474 Ok(None)
476 } else {
477 cmd.stdin(Stdio::inherit());
479 cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
480
481 let output = cmd.output().await?;
482
483 crate::process::handle_output(&output, "Claude")?;
484
485 let json_str = String::from_utf8(output.stdout)?;
487 log::debug!("Parsing Claude JSON output ({} bytes)", json_str.len());
488 let claude_output: models::ClaudeOutput =
489 serde_json::from_str(&json_str).map_err(|e| {
490 log::debug!(
491 "Failed to parse Claude JSON output: {}. First 500 chars: {}",
492 e,
493 crate::truncate_str(&json_str, 500)
494 );
495 anyhow::anyhow!("Failed to parse Claude JSON output: {}", e)
496 })?;
497 log::debug!("Parsed {} Claude events successfully", claude_output.len());
498
499 if let Ok(raw_events) = serde_json::from_str::<Vec<serde_json::Value>>(&json_str) {
501 let known = ["system", "assistant", "user", "result"];
502 for raw in &raw_events {
503 if let Some(t) = raw.get("type").and_then(|v| v.as_str()) {
504 if !known.contains(&t) {
505 log::debug!(
506 "Unknown Claude event type: {:?} (first 300 chars: {})",
507 t,
508 crate::truncate_str(
509 &serde_json::to_string(raw).unwrap_or_default(),
510 300
511 )
512 );
513 }
514 }
515 }
516 }
517
518 let agent_output: AgentOutput =
520 models::claude_output_to_agent_output(claude_output);
521 Ok(Some(agent_output))
522 }
523 } else {
524 cmd.stdin(Stdio::inherit()).stdout(Stdio::inherit());
526
527 crate::process::run_with_captured_stderr(&mut cmd).await?;
528 Ok(None)
529 }
530 }
531}
532
533#[derive(Debug, Default)]
546pub(crate) struct ClaudeEventTranslator {
547 pending_stop_reason: Option<String>,
550 pending_usage: Option<crate::output::Usage>,
552 next_turn_index: u32,
555 last_assistant_text: Option<String>,
558}
559
560impl ClaudeEventTranslator {
561 pub(crate) fn new() -> Self {
562 Self::default()
563 }
564
565 pub(crate) fn translate(&mut self, event: &models::ClaudeEvent) -> Vec<crate::output::Event> {
571 use crate::output::{Event as UnifiedEvent, Usage as UnifiedUsage};
572
573 if let models::ClaudeEvent::Assistant { message, .. } = event {
579 if let Some(reason) = &message.stop_reason {
580 self.pending_stop_reason = Some(reason.clone());
581 }
582 self.pending_usage = Some(UnifiedUsage {
583 input_tokens: message.usage.input_tokens,
584 output_tokens: message.usage.output_tokens,
585 cache_read_tokens: Some(message.usage.cache_read_input_tokens),
586 cache_creation_tokens: Some(message.usage.cache_creation_input_tokens),
587 web_search_requests: message
588 .usage
589 .server_tool_use
590 .as_ref()
591 .map(|s| s.web_search_requests),
592 web_fetch_requests: message
593 .usage
594 .server_tool_use
595 .as_ref()
596 .map(|s| s.web_fetch_requests),
597 });
598
599 let text_parts: Vec<&str> = message
601 .content
602 .iter()
603 .filter_map(|b| match b {
604 models::ContentBlock::Text { text } => Some(text.as_str()),
605 _ => None,
606 })
607 .collect();
608 if !text_parts.is_empty() {
609 self.last_assistant_text = Some(text_parts.join("\n"));
610 }
611 }
612
613 let unified = convert_claude_event_to_unified(event);
614
615 match unified {
616 Some(UnifiedEvent::Result {
617 success,
618 message,
619 duration_ms,
620 num_turns,
621 }) if message.as_deref() == Some("") => {
622 let fallback = self.last_assistant_text.take();
624 if fallback.is_some() {
625 log::debug!(
626 "Streaming Result.message is empty; using last assistant text as fallback"
627 );
628 }
629 let result_event = UnifiedEvent::Result {
630 success,
631 message: fallback.or(message),
632 duration_ms,
633 num_turns,
634 };
635 let turn_complete = UnifiedEvent::TurnComplete {
636 stop_reason: self.pending_stop_reason.take(),
637 turn_index: self.next_turn_index,
638 usage: self.pending_usage.take(),
639 };
640 self.next_turn_index = self.next_turn_index.saturating_add(1);
641 vec![turn_complete, result_event]
642 }
643 Some(UnifiedEvent::Result { .. }) => {
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, unified.unwrap()]
651 }
652 Some(ev) => vec![ev],
653 None => Vec::new(),
654 }
655 }
656}
657
658pub(crate) fn convert_claude_event_to_unified(
665 event: &models::ClaudeEvent,
666) -> Option<crate::output::Event> {
667 use crate::output::{
668 ContentBlock as UnifiedContentBlock, Event as UnifiedEvent, ToolResult,
669 Usage as UnifiedUsage,
670 };
671 use models::ClaudeEvent;
672
673 match event {
674 ClaudeEvent::System {
675 model, tools, cwd, ..
676 } => {
677 let mut metadata = std::collections::HashMap::new();
678 if let Some(cwd_val) = cwd {
679 metadata.insert("cwd".to_string(), serde_json::json!(cwd_val));
680 }
681
682 Some(UnifiedEvent::Init {
683 model: model.clone(),
684 tools: tools.clone(),
685 working_directory: cwd.clone(),
686 metadata,
687 })
688 }
689
690 ClaudeEvent::Assistant {
691 message,
692 parent_tool_use_id,
693 ..
694 } => {
695 let content: Vec<UnifiedContentBlock> = message
697 .content
698 .iter()
699 .filter_map(|block| match block {
700 models::ContentBlock::Text { text } => {
701 Some(UnifiedContentBlock::Text { text: text.clone() })
702 }
703 models::ContentBlock::ToolUse { id, name, input } => {
704 Some(UnifiedContentBlock::ToolUse {
705 id: id.clone(),
706 name: name.clone(),
707 input: input.clone(),
708 })
709 }
710 models::ContentBlock::Thinking { .. } => None,
711 })
712 .collect();
713
714 let usage = Some(UnifiedUsage {
716 input_tokens: message.usage.input_tokens,
717 output_tokens: message.usage.output_tokens,
718 cache_read_tokens: Some(message.usage.cache_read_input_tokens),
719 cache_creation_tokens: Some(message.usage.cache_creation_input_tokens),
720 web_search_requests: message
721 .usage
722 .server_tool_use
723 .as_ref()
724 .map(|s| s.web_search_requests),
725 web_fetch_requests: message
726 .usage
727 .server_tool_use
728 .as_ref()
729 .map(|s| s.web_fetch_requests),
730 });
731
732 Some(UnifiedEvent::AssistantMessage {
733 content,
734 usage,
735 parent_tool_use_id: parent_tool_use_id.clone(),
736 })
737 }
738
739 ClaudeEvent::User {
740 message,
741 tool_use_result,
742 parent_tool_use_id,
743 ..
744 } => {
745 let first_tool_result = message.content.iter().find_map(|b| {
749 if let models::UserContentBlock::ToolResult {
750 tool_use_id,
751 content,
752 is_error,
753 } = b
754 {
755 Some((tool_use_id, content, is_error))
756 } else {
757 None
758 }
759 });
760
761 if let Some((tool_use_id, content, is_error)) = first_tool_result {
762 let tool_result = ToolResult {
763 success: !is_error,
764 output: if !is_error {
765 Some(content.clone())
766 } else {
767 None
768 },
769 error: if *is_error {
770 Some(content.clone())
771 } else {
772 None
773 },
774 data: tool_use_result.clone(),
775 };
776
777 Some(UnifiedEvent::ToolExecution {
778 tool_name: "unknown".to_string(),
779 tool_id: tool_use_id.clone(),
780 input: serde_json::Value::Null,
781 result: tool_result,
782 parent_tool_use_id: parent_tool_use_id.clone(),
783 })
784 } else {
785 let text_blocks: Vec<UnifiedContentBlock> = message
787 .content
788 .iter()
789 .filter_map(|b| {
790 if let models::UserContentBlock::Text { text } = b {
791 Some(UnifiedContentBlock::Text { text: text.clone() })
792 } else {
793 None
794 }
795 })
796 .collect();
797
798 if !text_blocks.is_empty() {
799 Some(UnifiedEvent::UserMessage {
800 content: text_blocks,
801 })
802 } else {
803 None
804 }
805 }
806 }
807
808 ClaudeEvent::Other => {
809 log::debug!("Skipping unknown Claude event type during streaming conversion");
810 None
811 }
812
813 ClaudeEvent::Result {
814 is_error,
815 result,
816 duration_ms,
817 num_turns,
818 ..
819 } => Some(UnifiedEvent::Result {
820 success: !is_error,
821 message: Some(result.clone()),
822 duration_ms: Some(*duration_ms),
823 num_turns: Some(*num_turns),
824 }),
825 }
826}
827
828#[cfg(test)]
829#[path = "claude_tests.rs"]
830mod tests;
831
832impl Default for Claude {
833 fn default() -> Self {
834 Self::new()
835 }
836}
837
838#[async_trait]
839impl Agent for Claude {
840 fn name(&self) -> &str {
841 "claude"
842 }
843
844 fn default_model() -> &'static str {
845 DEFAULT_MODEL
846 }
847
848 fn model_for_size(size: ModelSize) -> &'static str {
849 match size {
850 ModelSize::Small => "haiku",
851 ModelSize::Medium => "sonnet",
852 ModelSize::Large => "default",
853 }
854 }
855
856 fn available_models() -> &'static [&'static str] {
857 AVAILABLE_MODELS
858 }
859
860 crate::providers::common::impl_common_agent_setters!();
861
862 fn set_skip_permissions(&mut self, skip: bool) {
863 self.common.skip_permissions = skip;
864 }
865
866 crate::providers::common::impl_as_any!();
867
868 async fn run(&self, prompt: Option<&str>) -> Result<Option<AgentOutput>> {
869 self.execute(false, prompt).await
870 }
871
872 async fn run_interactive(&self, prompt: Option<&str>) -> Result<()> {
873 self.execute(true, prompt).await?;
874 Ok(())
875 }
876
877 async fn run_resume(&self, session_id: Option<&str>, _last: bool) -> Result<()> {
878 let agent_args = self.build_resume_args(session_id);
879 let mut cmd = self.make_command(agent_args);
880
881 cmd.stdin(Stdio::inherit())
882 .stdout(Stdio::inherit())
883 .stderr(Stdio::inherit());
884
885 let status = cmd
886 .status()
887 .await
888 .context("Failed to execute 'claude' CLI. Is it installed and in PATH?")?;
889 if !status.success() {
890 return Err(crate::process::ProcessError {
891 exit_code: status.code(),
892 stderr: String::new(),
893 agent_name: "Claude".to_string(),
894 }
895 .into());
896 }
897 Ok(())
898 }
899
900 async fn run_resume_with_prompt(
901 &self,
902 session_id: &str,
903 prompt: &str,
904 ) -> Result<Option<AgentOutput>> {
905 log::debug!(
906 "Claude resume with prompt: session={}, prompt={}",
907 session_id,
908 prompt
909 );
910 let in_sandbox = self.common.sandbox.is_some();
911 let mut args = vec!["--print".to_string()];
912 args.extend(["--resume".to_string(), session_id.to_string()]);
913 args.extend(["--verbose", "--output-format", "json"].map(String::from));
914
915 if self.common.skip_permissions && !in_sandbox {
916 args.push("--dangerously-skip-permissions".to_string());
917 }
918
919 args.extend(["--model".to_string(), self.common.model.clone()]);
920
921 for dir in &self.common.add_dirs {
922 args.extend(["--add-dir".to_string(), dir.clone()]);
923 }
924
925 if let Some(ref schema) = self.json_schema {
926 args.extend(["--json-schema".to_string(), schema.clone()]);
927 }
928
929 args.push(prompt.to_string());
930
931 let mut cmd = self.make_command(args);
932
933 cmd.stdin(Stdio::inherit());
934 cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
935
936 let output = cmd.output().await?;
937
938 crate::process::handle_output(&output, "Claude")?;
939
940 let json_str = String::from_utf8(output.stdout)?;
942 log::debug!(
943 "Parsing Claude resume JSON output ({} bytes)",
944 json_str.len()
945 );
946 let claude_output: models::ClaudeOutput = serde_json::from_str(&json_str)
947 .map_err(|e| anyhow::anyhow!("Failed to parse Claude resume JSON output: {}", e))?;
948
949 let agent_output: AgentOutput = models::claude_output_to_agent_output(claude_output);
950 Ok(Some(agent_output))
951 }
952
953 async fn cleanup(&self) -> Result<()> {
954 Ok(())
955 }
956}