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] = &[
28 "default",
29 "sonnet",
30 "sonnet-4.6",
31 "opus",
32 "opus-4.6",
33 "haiku",
34 "haiku-4.5",
35];
36
37pub type EventHandler = Box<dyn Fn(&crate::output::Event, bool) + Send + Sync>;
40
41pub struct Claude {
42 pub common: CommonAgentState,
43 pub session_id: Option<String>,
44 pub input_format: Option<String>,
45 pub verbose: bool,
46 pub json_schema: Option<String>,
47 pub event_handler: Option<EventHandler>,
48 pub replay_user_messages: bool,
49 pub include_partial_messages: bool,
50 pub mcp_config_path: Option<String>,
51}
52
53impl Claude {
54 pub fn new() -> Self {
55 Self {
56 common: CommonAgentState::new(DEFAULT_MODEL),
57 session_id: None,
58 input_format: None,
59 verbose: false,
60 json_schema: None,
61 event_handler: None,
62 replay_user_messages: false,
63 include_partial_messages: false,
64 mcp_config_path: None,
65 }
66 }
67
68 pub fn set_input_format(&mut self, format: Option<String>) {
69 self.input_format = format;
70 }
71
72 pub fn set_session_id(&mut self, session_id: String) {
73 self.session_id = Some(session_id);
74 }
75
76 pub fn set_verbose(&mut self, verbose: bool) {
77 self.verbose = verbose;
78 }
79
80 pub fn set_json_schema(&mut self, schema: Option<String>) {
81 self.json_schema = schema;
82 }
83
84 pub fn set_replay_user_messages(&mut self, replay: bool) {
85 self.replay_user_messages = replay;
86 }
87
88 pub fn set_include_partial_messages(&mut self, include: bool) {
89 self.include_partial_messages = include;
90 }
91
92 pub fn set_mcp_config(&mut self, config: Option<String>) {
94 self.mcp_config_path = config.map(|c| {
95 if c.trim_start().starts_with('{') {
96 let path =
97 std::env::temp_dir().join(format!("zag-mcp-{}.json", uuid::Uuid::new_v4()));
98 if let Err(e) = std::fs::write(&path, &c) {
99 log::warn!("Failed to write MCP config temp file: {}", e);
100 return c;
101 }
102 path.to_string_lossy().into_owned()
103 } else {
104 c
105 }
106 });
107 }
108
109 pub fn set_event_handler(&mut self, handler: EventHandler) {
114 self.event_handler = Some(handler);
115 }
116
117 fn build_run_args(
119 &self,
120 interactive: bool,
121 prompt: Option<&str>,
122 effective_output_format: &Option<String>,
123 ) -> Vec<String> {
124 let mut args = Vec::new();
125 let in_sandbox = self.common.sandbox.is_some();
126
127 if !interactive {
128 args.push("--print".to_string());
129
130 match effective_output_format.as_deref() {
131 Some("json") | Some("json-pretty") => {
132 args.extend(["--verbose", "--output-format", "json"].map(String::from));
133 }
134 Some("stream-json") | None => {
135 args.extend(["--verbose", "--output-format", "stream-json"].map(String::from));
136 }
137 Some("native-json") => {
138 args.extend(["--verbose", "--output-format", "json"].map(String::from));
139 }
140 Some("text") => {}
141 _ => {}
142 }
143 }
144
145 if self.common.skip_permissions && !in_sandbox {
147 args.push("--dangerously-skip-permissions".to_string());
148 }
149
150 args.extend(["--model".to_string(), self.common.model.clone()]);
151
152 if interactive && let Some(session_id) = &self.session_id {
153 args.extend(["--session-id".to_string(), session_id.clone()]);
154 }
155
156 for dir in &self.common.add_dirs {
157 args.extend(["--add-dir".to_string(), dir.clone()]);
158 }
159
160 if !self.common.system_prompt.is_empty() {
161 args.extend([
162 "--append-system-prompt".to_string(),
163 self.common.system_prompt.clone(),
164 ]);
165 }
166
167 if !interactive && let Some(ref input_fmt) = self.input_format {
168 args.extend(["--input-format".to_string(), input_fmt.clone()]);
169 }
170
171 if !interactive && self.replay_user_messages {
172 args.push("--replay-user-messages".to_string());
173 }
174
175 if !interactive && self.include_partial_messages {
176 args.push("--include-partial-messages".to_string());
177 }
178
179 if let Some(ref schema) = self.json_schema {
180 args.extend(["--json-schema".to_string(), schema.clone()]);
181 }
182
183 if let Some(turns) = self.common.max_turns {
184 args.extend(["--max-turns".to_string(), turns.to_string()]);
185 }
186
187 if let Some(ref path) = self.mcp_config_path {
188 args.extend(["--mcp-config".to_string(), path.clone()]);
189 }
190
191 if let Some(p) = prompt {
192 args.push(p.to_string());
193 }
194
195 args
196 }
197
198 fn build_resume_args(&self, session_id: Option<&str>) -> Vec<String> {
200 let mut args = Vec::new();
201 let in_sandbox = self.common.sandbox.is_some();
202
203 if let Some(id) = session_id {
204 args.extend(["--resume".to_string(), id.to_string()]);
205 } else {
206 args.push("--continue".to_string());
207 }
208
209 if self.common.skip_permissions && !in_sandbox {
210 args.push("--dangerously-skip-permissions".to_string());
211 }
212
213 args.extend(["--model".to_string(), self.common.model.clone()]);
214
215 for dir in &self.common.add_dirs {
216 args.extend(["--add-dir".to_string(), dir.clone()]);
217 }
218
219 args
220 }
221
222 fn make_command(&self, agent_args: Vec<String>) -> Command {
224 self.common.make_command("claude", agent_args)
225 }
226
227 pub fn execute_streaming(
242 &self,
243 prompt: Option<&str>,
244 ) -> Result<crate::streaming::StreamingSession> {
245 let mut args = Vec::new();
247 let in_sandbox = self.common.sandbox.is_some();
248
249 args.push("--print".to_string());
250 args.extend(["--verbose", "--output-format", "stream-json"].map(String::from));
251
252 if self.common.skip_permissions && !in_sandbox {
253 args.push("--dangerously-skip-permissions".to_string());
254 }
255
256 args.extend(["--model".to_string(), self.common.model.clone()]);
257
258 for dir in &self.common.add_dirs {
259 args.extend(["--add-dir".to_string(), dir.clone()]);
260 }
261
262 if !self.common.system_prompt.is_empty() {
263 args.extend([
264 "--append-system-prompt".to_string(),
265 self.common.system_prompt.clone(),
266 ]);
267 }
268
269 args.extend(["--input-format".to_string(), "stream-json".to_string()]);
270 args.push("--replay-user-messages".to_string());
271
272 if self.include_partial_messages {
273 args.push("--include-partial-messages".to_string());
274 }
275
276 if let Some(ref schema) = self.json_schema {
277 args.extend(["--json-schema".to_string(), schema.clone()]);
278 }
279
280 if let Some(p) = prompt {
281 args.push(p.to_string());
282 }
283
284 log::debug!("Claude streaming command: claude {}", args.join(" "));
285
286 let mut cmd = self.make_command(args);
287 cmd.stdin(Stdio::piped())
288 .stdout(Stdio::piped())
289 .stderr(Stdio::piped());
290
291 let child = cmd
292 .spawn()
293 .context("Failed to execute 'claude' CLI. Is it installed and in PATH?")?;
294 crate::streaming::StreamingSession::new(child)
295 }
296
297 fn build_streaming_resume_args(&self, session_id: &str) -> Vec<String> {
299 let mut args = Vec::new();
300 let in_sandbox = self.common.sandbox.is_some();
301
302 args.push("--print".to_string());
303 args.extend(["--resume".to_string(), session_id.to_string()]);
304 args.extend(["--verbose", "--output-format", "stream-json"].map(String::from));
305
306 if self.common.skip_permissions && !in_sandbox {
307 args.push("--dangerously-skip-permissions".to_string());
308 }
309
310 args.extend(["--model".to_string(), self.common.model.clone()]);
311
312 for dir in &self.common.add_dirs {
313 args.extend(["--add-dir".to_string(), dir.clone()]);
314 }
315
316 args.extend(["--input-format".to_string(), "stream-json".to_string()]);
317 args.push("--replay-user-messages".to_string());
318
319 if self.include_partial_messages {
320 args.push("--include-partial-messages".to_string());
321 }
322
323 args
324 }
325
326 pub fn execute_streaming_resume(
335 &self,
336 session_id: &str,
337 ) -> Result<crate::streaming::StreamingSession> {
338 let args = self.build_streaming_resume_args(session_id);
339
340 log::debug!("Claude streaming resume command: claude {}", args.join(" "));
341
342 let mut cmd = self.make_command(args);
343 cmd.stdin(Stdio::piped())
344 .stdout(Stdio::piped())
345 .stderr(Stdio::piped());
346
347 let child = cmd
348 .spawn()
349 .context("Failed to execute 'claude' CLI. Is it installed and in PATH?")?;
350 crate::streaming::StreamingSession::new(child)
351 }
352
353 async fn execute(
354 &self,
355 interactive: bool,
356 prompt: Option<&str>,
357 ) -> Result<Option<AgentOutput>> {
358 let effective_output_format =
361 if self.common.capture_output && self.common.output_format.is_none() {
362 Some("json".to_string())
363 } else {
364 self.common.output_format.clone()
365 };
366
367 let capture_json = !interactive
370 && effective_output_format
371 .as_ref()
372 .is_none_or(|f| f == "json" || f == "json-pretty" || f == "stream-json");
373
374 let agent_args = self.build_run_args(interactive, prompt, &effective_output_format);
375 log::debug!("Claude command: claude {}", agent_args.join(" "));
376 if !self.common.system_prompt.is_empty() {
377 log::debug!("Claude system prompt: {}", self.common.system_prompt);
378 }
379 if let Some(p) = prompt {
380 log::debug!("Claude user prompt: {}", p);
381 }
382 log::debug!(
383 "Claude mode: interactive={}, capture_json={}, output_format={:?}",
384 interactive,
385 capture_json,
386 effective_output_format
387 );
388 let mut cmd = self.make_command(agent_args);
389
390 let is_native_json = effective_output_format.as_deref() == Some("native-json");
392
393 if interactive {
394 cmd.stdin(Stdio::inherit())
396 .stdout(Stdio::inherit())
397 .stderr(Stdio::inherit());
398
399 let status = cmd
400 .status()
401 .await
402 .context("Failed to execute 'claude' CLI. Is it installed and in PATH?")?;
403 if !status.success() {
404 return Err(crate::process::ProcessError {
405 exit_code: status.code(),
406 stderr: String::new(),
407 agent_name: "Claude".to_string(),
408 }
409 .into());
410 }
411 Ok(None)
412 } else if is_native_json {
413 cmd.stdin(Stdio::inherit()).stdout(Stdio::inherit());
415
416 crate::process::run_with_captured_stderr(&mut cmd).await?;
417 Ok(None)
418 } else if capture_json {
419 let output_format = effective_output_format.as_deref();
420 let is_streaming = output_format == Some("stream-json") || output_format.is_none();
421
422 if is_streaming {
423 cmd.stdin(Stdio::inherit());
425 cmd.stdout(Stdio::piped());
426
427 let mut child = crate::process::spawn_with_captured_stderr(&mut cmd).await?;
428 let stdout = child
429 .stdout
430 .take()
431 .ok_or_else(|| anyhow::anyhow!("Failed to capture stdout"))?;
432
433 let reader = BufReader::new(stdout);
434 let mut lines = reader.lines();
435
436 let format_as_text = output_format.is_none(); let format_as_json = output_format == Some("stream-json"); let mut translator = ClaudeEventTranslator::new();
443
444 while let Some(line) = lines.next_line().await? {
446 if format_as_text || format_as_json {
447 match serde_json::from_str::<models::ClaudeEvent>(&line) {
448 Ok(claude_event) => {
449 for unified_event in translator.translate(&claude_event) {
450 if let Some(ref handler) = self.event_handler {
451 handler(&unified_event, self.verbose);
452 }
453 }
454 }
455 Err(e) => {
456 log::debug!(
457 "Failed to parse streaming Claude event: {}. Line: {}",
458 e,
459 crate::truncate_str(&line, 200)
460 );
461 }
462 }
463 }
464 }
465
466 if let Some(ref handler) = self.event_handler {
468 handler(
470 &crate::output::Event::Result {
471 success: true,
472 message: None,
473 duration_ms: None,
474 num_turns: None,
475 },
476 self.verbose,
477 );
478 }
479
480 crate::process::wait_with_stderr(child).await?;
481
482 Ok(None)
484 } else {
485 cmd.stdin(Stdio::inherit());
487 cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
488
489 let output = cmd.output().await?;
490
491 crate::process::handle_output(&output, "Claude")?;
492
493 let json_str = String::from_utf8(output.stdout)?;
495 log::debug!("Parsing Claude JSON output ({} bytes)", json_str.len());
496 let claude_output: models::ClaudeOutput =
497 serde_json::from_str(&json_str).map_err(|e| {
498 log::debug!(
499 "Failed to parse Claude JSON output: {}. First 500 chars: {}",
500 e,
501 crate::truncate_str(&json_str, 500)
502 );
503 anyhow::anyhow!("Failed to parse Claude JSON output: {}", e)
504 })?;
505 log::debug!("Parsed {} Claude events successfully", claude_output.len());
506
507 let agent_output: AgentOutput =
509 models::claude_output_to_agent_output(claude_output);
510 Ok(Some(agent_output))
511 }
512 } else {
513 cmd.stdin(Stdio::inherit()).stdout(Stdio::inherit());
515
516 crate::process::run_with_captured_stderr(&mut cmd).await?;
517 Ok(None)
518 }
519 }
520}
521
522#[derive(Debug, Default)]
535pub(crate) struct ClaudeEventTranslator {
536 pending_stop_reason: Option<String>,
539 pending_usage: Option<crate::output::Usage>,
541 next_turn_index: u32,
544}
545
546impl ClaudeEventTranslator {
547 pub(crate) fn new() -> Self {
548 Self::default()
549 }
550
551 pub(crate) fn translate(&mut self, event: &models::ClaudeEvent) -> Vec<crate::output::Event> {
557 use crate::output::{Event as UnifiedEvent, Usage as UnifiedUsage};
558
559 if let models::ClaudeEvent::Assistant { message, .. } = event {
565 if let Some(reason) = &message.stop_reason {
566 self.pending_stop_reason = Some(reason.clone());
567 }
568 self.pending_usage = Some(UnifiedUsage {
569 input_tokens: message.usage.input_tokens,
570 output_tokens: message.usage.output_tokens,
571 cache_read_tokens: Some(message.usage.cache_read_input_tokens),
572 cache_creation_tokens: Some(message.usage.cache_creation_input_tokens),
573 web_search_requests: message
574 .usage
575 .server_tool_use
576 .as_ref()
577 .map(|s| s.web_search_requests),
578 web_fetch_requests: message
579 .usage
580 .server_tool_use
581 .as_ref()
582 .map(|s| s.web_fetch_requests),
583 });
584 }
585
586 let unified = convert_claude_event_to_unified(event);
587
588 match unified {
589 Some(UnifiedEvent::Result { .. }) => {
590 let turn_complete = UnifiedEvent::TurnComplete {
591 stop_reason: self.pending_stop_reason.take(),
592 turn_index: self.next_turn_index,
593 usage: self.pending_usage.take(),
594 };
595 self.next_turn_index = self.next_turn_index.saturating_add(1);
596 vec![turn_complete, unified.unwrap()]
597 }
598 Some(ev) => vec![ev],
599 None => Vec::new(),
600 }
601 }
602}
603
604pub(crate) fn convert_claude_event_to_unified(
611 event: &models::ClaudeEvent,
612) -> Option<crate::output::Event> {
613 use crate::output::{
614 ContentBlock as UnifiedContentBlock, Event as UnifiedEvent, ToolResult,
615 Usage as UnifiedUsage,
616 };
617 use models::ClaudeEvent;
618
619 match event {
620 ClaudeEvent::System {
621 model, tools, cwd, ..
622 } => {
623 let mut metadata = std::collections::HashMap::new();
624 if let Some(cwd_val) = cwd {
625 metadata.insert("cwd".to_string(), serde_json::json!(cwd_val));
626 }
627
628 Some(UnifiedEvent::Init {
629 model: model.clone(),
630 tools: tools.clone(),
631 working_directory: cwd.clone(),
632 metadata,
633 })
634 }
635
636 ClaudeEvent::Assistant {
637 message,
638 parent_tool_use_id,
639 ..
640 } => {
641 let content: Vec<UnifiedContentBlock> = message
643 .content
644 .iter()
645 .filter_map(|block| match block {
646 models::ContentBlock::Text { text } => {
647 Some(UnifiedContentBlock::Text { text: text.clone() })
648 }
649 models::ContentBlock::ToolUse { id, name, input } => {
650 Some(UnifiedContentBlock::ToolUse {
651 id: id.clone(),
652 name: name.clone(),
653 input: input.clone(),
654 })
655 }
656 models::ContentBlock::Thinking { .. } => None,
657 })
658 .collect();
659
660 let usage = Some(UnifiedUsage {
662 input_tokens: message.usage.input_tokens,
663 output_tokens: message.usage.output_tokens,
664 cache_read_tokens: Some(message.usage.cache_read_input_tokens),
665 cache_creation_tokens: Some(message.usage.cache_creation_input_tokens),
666 web_search_requests: message
667 .usage
668 .server_tool_use
669 .as_ref()
670 .map(|s| s.web_search_requests),
671 web_fetch_requests: message
672 .usage
673 .server_tool_use
674 .as_ref()
675 .map(|s| s.web_fetch_requests),
676 });
677
678 Some(UnifiedEvent::AssistantMessage {
679 content,
680 usage,
681 parent_tool_use_id: parent_tool_use_id.clone(),
682 })
683 }
684
685 ClaudeEvent::User {
686 message,
687 tool_use_result,
688 parent_tool_use_id,
689 ..
690 } => {
691 let first_tool_result = message.content.iter().find_map(|b| {
695 if let models::UserContentBlock::ToolResult {
696 tool_use_id,
697 content,
698 is_error,
699 } = b
700 {
701 Some((tool_use_id, content, is_error))
702 } else {
703 None
704 }
705 });
706
707 if let Some((tool_use_id, content, is_error)) = first_tool_result {
708 let tool_result = ToolResult {
709 success: !is_error,
710 output: if !is_error {
711 Some(content.clone())
712 } else {
713 None
714 },
715 error: if *is_error {
716 Some(content.clone())
717 } else {
718 None
719 },
720 data: tool_use_result.clone(),
721 };
722
723 Some(UnifiedEvent::ToolExecution {
724 tool_name: "unknown".to_string(),
725 tool_id: tool_use_id.clone(),
726 input: serde_json::Value::Null,
727 result: tool_result,
728 parent_tool_use_id: parent_tool_use_id.clone(),
729 })
730 } else {
731 let text_blocks: Vec<UnifiedContentBlock> = message
733 .content
734 .iter()
735 .filter_map(|b| {
736 if let models::UserContentBlock::Text { text } = b {
737 Some(UnifiedContentBlock::Text { text: text.clone() })
738 } else {
739 None
740 }
741 })
742 .collect();
743
744 if !text_blocks.is_empty() {
745 Some(UnifiedEvent::UserMessage {
746 content: text_blocks,
747 })
748 } else {
749 None
750 }
751 }
752 }
753
754 ClaudeEvent::Other => {
755 log::debug!("Skipping unknown Claude event type during streaming conversion");
756 None
757 }
758
759 ClaudeEvent::Result {
760 is_error,
761 result,
762 duration_ms,
763 num_turns,
764 ..
765 } => Some(UnifiedEvent::Result {
766 success: !is_error,
767 message: Some(result.clone()),
768 duration_ms: Some(*duration_ms),
769 num_turns: Some(*num_turns),
770 }),
771 }
772}
773
774#[cfg(test)]
775#[path = "claude_tests.rs"]
776mod tests;
777
778impl Default for Claude {
779 fn default() -> Self {
780 Self::new()
781 }
782}
783
784#[async_trait]
785impl Agent for Claude {
786 fn name(&self) -> &str {
787 "claude"
788 }
789
790 fn default_model() -> &'static str {
791 DEFAULT_MODEL
792 }
793
794 fn model_for_size(size: ModelSize) -> &'static str {
795 match size {
796 ModelSize::Small => "haiku",
797 ModelSize::Medium => "sonnet",
798 ModelSize::Large => "default",
799 }
800 }
801
802 fn available_models() -> &'static [&'static str] {
803 AVAILABLE_MODELS
804 }
805
806 crate::providers::common::impl_common_agent_setters!();
807
808 fn set_skip_permissions(&mut self, skip: bool) {
809 self.common.skip_permissions = skip;
810 }
811
812 crate::providers::common::impl_as_any!();
813
814 async fn run(&self, prompt: Option<&str>) -> Result<Option<AgentOutput>> {
815 self.execute(false, prompt).await
816 }
817
818 async fn run_interactive(&self, prompt: Option<&str>) -> Result<()> {
819 self.execute(true, prompt).await?;
820 Ok(())
821 }
822
823 async fn run_resume(&self, session_id: Option<&str>, _last: bool) -> Result<()> {
824 let agent_args = self.build_resume_args(session_id);
825 let mut cmd = self.make_command(agent_args);
826
827 cmd.stdin(Stdio::inherit())
828 .stdout(Stdio::inherit())
829 .stderr(Stdio::inherit());
830
831 let status = cmd
832 .status()
833 .await
834 .context("Failed to execute 'claude' CLI. Is it installed and in PATH?")?;
835 if !status.success() {
836 return Err(crate::process::ProcessError {
837 exit_code: status.code(),
838 stderr: String::new(),
839 agent_name: "Claude".to_string(),
840 }
841 .into());
842 }
843 Ok(())
844 }
845
846 async fn run_resume_with_prompt(
847 &self,
848 session_id: &str,
849 prompt: &str,
850 ) -> Result<Option<AgentOutput>> {
851 log::debug!(
852 "Claude resume with prompt: session={}, prompt={}",
853 session_id,
854 prompt
855 );
856 let in_sandbox = self.common.sandbox.is_some();
857 let mut args = vec!["--print".to_string()];
858 args.extend(["--resume".to_string(), session_id.to_string()]);
859 args.extend(["--verbose", "--output-format", "json"].map(String::from));
860
861 if self.common.skip_permissions && !in_sandbox {
862 args.push("--dangerously-skip-permissions".to_string());
863 }
864
865 args.extend(["--model".to_string(), self.common.model.clone()]);
866
867 for dir in &self.common.add_dirs {
868 args.extend(["--add-dir".to_string(), dir.clone()]);
869 }
870
871 if let Some(ref schema) = self.json_schema {
872 args.extend(["--json-schema".to_string(), schema.clone()]);
873 }
874
875 args.push(prompt.to_string());
876
877 let mut cmd = self.make_command(args);
878
879 cmd.stdin(Stdio::inherit());
880 cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
881
882 let output = cmd.output().await?;
883
884 crate::process::handle_output(&output, "Claude")?;
885
886 let json_str = String::from_utf8(output.stdout)?;
888 log::debug!(
889 "Parsing Claude resume JSON output ({} bytes)",
890 json_str.len()
891 );
892 let claude_output: models::ClaudeOutput = serde_json::from_str(&json_str)
893 .map_err(|e| anyhow::anyhow!("Failed to parse Claude resume JSON output: {}", e))?;
894
895 let agent_output: AgentOutput = models::claude_output_to_agent_output(claude_output);
896 Ok(Some(agent_output))
897 }
898
899 async fn cleanup(&self) -> Result<()> {
900 Ok(())
901 }
902}