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"); while let Some(line) = lines.next_line().await? {
442 if format_as_text || format_as_json {
443 match serde_json::from_str::<models::ClaudeEvent>(&line) {
444 Ok(claude_event) => {
445 if let Some(unified_event) =
446 convert_claude_event_to_unified(&claude_event)
447 {
448 if let Some(ref handler) = self.event_handler {
449 handler(&unified_event, self.verbose);
450 }
451 }
452 }
453 Err(e) => {
454 log::debug!(
455 "Failed to parse streaming Claude event: {}. Line: {}",
456 e,
457 crate::truncate_str(&line, 200)
458 );
459 }
460 }
461 }
462 }
463
464 if let Some(ref handler) = self.event_handler {
466 handler(
468 &crate::output::Event::Result {
469 success: true,
470 message: None,
471 duration_ms: None,
472 num_turns: None,
473 },
474 self.verbose,
475 );
476 }
477
478 crate::process::wait_with_stderr(child).await?;
479
480 Ok(None)
482 } else {
483 cmd.stdin(Stdio::inherit());
485 cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
486
487 let output = cmd.output().await?;
488
489 crate::process::handle_output(&output, "Claude")?;
490
491 let json_str = String::from_utf8(output.stdout)?;
493 log::debug!("Parsing Claude JSON output ({} bytes)", json_str.len());
494 let claude_output: models::ClaudeOutput =
495 serde_json::from_str(&json_str).map_err(|e| {
496 log::debug!(
497 "Failed to parse Claude JSON output: {}. First 500 chars: {}",
498 e,
499 crate::truncate_str(&json_str, 500)
500 );
501 anyhow::anyhow!("Failed to parse Claude JSON output: {}", e)
502 })?;
503 log::debug!("Parsed {} Claude events successfully", claude_output.len());
504
505 let agent_output: AgentOutput =
507 models::claude_output_to_agent_output(claude_output);
508 Ok(Some(agent_output))
509 }
510 } else {
511 cmd.stdin(Stdio::inherit()).stdout(Stdio::inherit());
513
514 crate::process::run_with_captured_stderr(&mut cmd).await?;
515 Ok(None)
516 }
517 }
518}
519
520pub(crate) fn convert_claude_event_to_unified(
523 event: &models::ClaudeEvent,
524) -> Option<crate::output::Event> {
525 use crate::output::{
526 ContentBlock as UnifiedContentBlock, Event as UnifiedEvent, ToolResult,
527 Usage as UnifiedUsage,
528 };
529 use models::ClaudeEvent;
530
531 match event {
532 ClaudeEvent::System {
533 model, tools, cwd, ..
534 } => {
535 let mut metadata = std::collections::HashMap::new();
536 if let Some(cwd_val) = cwd {
537 metadata.insert("cwd".to_string(), serde_json::json!(cwd_val));
538 }
539
540 Some(UnifiedEvent::Init {
541 model: model.clone(),
542 tools: tools.clone(),
543 working_directory: cwd.clone(),
544 metadata,
545 })
546 }
547
548 ClaudeEvent::Assistant {
549 message,
550 parent_tool_use_id,
551 ..
552 } => {
553 let content: Vec<UnifiedContentBlock> = message
555 .content
556 .iter()
557 .filter_map(|block| match block {
558 models::ContentBlock::Text { text } => {
559 Some(UnifiedContentBlock::Text { text: text.clone() })
560 }
561 models::ContentBlock::ToolUse { id, name, input } => {
562 Some(UnifiedContentBlock::ToolUse {
563 id: id.clone(),
564 name: name.clone(),
565 input: input.clone(),
566 })
567 }
568 models::ContentBlock::Thinking { .. } => None,
569 })
570 .collect();
571
572 let usage = Some(UnifiedUsage {
574 input_tokens: message.usage.input_tokens,
575 output_tokens: message.usage.output_tokens,
576 cache_read_tokens: Some(message.usage.cache_read_input_tokens),
577 cache_creation_tokens: Some(message.usage.cache_creation_input_tokens),
578 web_search_requests: message
579 .usage
580 .server_tool_use
581 .as_ref()
582 .map(|s| s.web_search_requests),
583 web_fetch_requests: message
584 .usage
585 .server_tool_use
586 .as_ref()
587 .map(|s| s.web_fetch_requests),
588 });
589
590 Some(UnifiedEvent::AssistantMessage {
591 content,
592 usage,
593 parent_tool_use_id: parent_tool_use_id.clone(),
594 })
595 }
596
597 ClaudeEvent::User {
598 message,
599 tool_use_result,
600 parent_tool_use_id,
601 ..
602 } => {
603 let first_tool_result = message.content.iter().find_map(|b| {
607 if let models::UserContentBlock::ToolResult {
608 tool_use_id,
609 content,
610 is_error,
611 } = b
612 {
613 Some((tool_use_id, content, is_error))
614 } else {
615 None
616 }
617 });
618
619 if let Some((tool_use_id, content, is_error)) = first_tool_result {
620 let tool_result = ToolResult {
621 success: !is_error,
622 output: if !is_error {
623 Some(content.clone())
624 } else {
625 None
626 },
627 error: if *is_error {
628 Some(content.clone())
629 } else {
630 None
631 },
632 data: tool_use_result.clone(),
633 };
634
635 Some(UnifiedEvent::ToolExecution {
636 tool_name: "unknown".to_string(),
637 tool_id: tool_use_id.clone(),
638 input: serde_json::Value::Null,
639 result: tool_result,
640 parent_tool_use_id: parent_tool_use_id.clone(),
641 })
642 } else {
643 let text_blocks: Vec<UnifiedContentBlock> = message
645 .content
646 .iter()
647 .filter_map(|b| {
648 if let models::UserContentBlock::Text { text } = b {
649 Some(UnifiedContentBlock::Text { text: text.clone() })
650 } else {
651 None
652 }
653 })
654 .collect();
655
656 if !text_blocks.is_empty() {
657 Some(UnifiedEvent::UserMessage {
658 content: text_blocks,
659 })
660 } else {
661 None
662 }
663 }
664 }
665
666 ClaudeEvent::Other => {
667 log::debug!("Skipping unknown Claude event type during streaming conversion");
668 None
669 }
670
671 ClaudeEvent::Result {
672 is_error,
673 result,
674 duration_ms,
675 num_turns,
676 ..
677 } => Some(UnifiedEvent::Result {
678 success: !is_error,
679 message: Some(result.clone()),
680 duration_ms: Some(*duration_ms),
681 num_turns: Some(*num_turns),
682 }),
683 }
684}
685
686#[cfg(test)]
687#[path = "claude_tests.rs"]
688mod tests;
689
690impl Default for Claude {
691 fn default() -> Self {
692 Self::new()
693 }
694}
695
696#[async_trait]
697impl Agent for Claude {
698 fn name(&self) -> &str {
699 "claude"
700 }
701
702 fn default_model() -> &'static str {
703 DEFAULT_MODEL
704 }
705
706 fn model_for_size(size: ModelSize) -> &'static str {
707 match size {
708 ModelSize::Small => "haiku",
709 ModelSize::Medium => "sonnet",
710 ModelSize::Large => "default",
711 }
712 }
713
714 fn available_models() -> &'static [&'static str] {
715 AVAILABLE_MODELS
716 }
717
718 crate::providers::common::impl_common_agent_setters!();
719
720 fn set_skip_permissions(&mut self, skip: bool) {
721 self.common.skip_permissions = skip;
722 }
723
724 crate::providers::common::impl_as_any!();
725
726 async fn run(&self, prompt: Option<&str>) -> Result<Option<AgentOutput>> {
727 self.execute(false, prompt).await
728 }
729
730 async fn run_interactive(&self, prompt: Option<&str>) -> Result<()> {
731 self.execute(true, prompt).await?;
732 Ok(())
733 }
734
735 async fn run_resume(&self, session_id: Option<&str>, _last: bool) -> Result<()> {
736 let agent_args = self.build_resume_args(session_id);
737 let mut cmd = self.make_command(agent_args);
738
739 cmd.stdin(Stdio::inherit())
740 .stdout(Stdio::inherit())
741 .stderr(Stdio::inherit());
742
743 let status = cmd
744 .status()
745 .await
746 .context("Failed to execute 'claude' CLI. Is it installed and in PATH?")?;
747 if !status.success() {
748 return Err(crate::process::ProcessError {
749 exit_code: status.code(),
750 stderr: String::new(),
751 agent_name: "Claude".to_string(),
752 }
753 .into());
754 }
755 Ok(())
756 }
757
758 async fn run_resume_with_prompt(
759 &self,
760 session_id: &str,
761 prompt: &str,
762 ) -> Result<Option<AgentOutput>> {
763 log::debug!(
764 "Claude resume with prompt: session={}, prompt={}",
765 session_id,
766 prompt
767 );
768 let in_sandbox = self.common.sandbox.is_some();
769 let mut args = vec!["--print".to_string()];
770 args.extend(["--resume".to_string(), session_id.to_string()]);
771 args.extend(["--verbose", "--output-format", "json"].map(String::from));
772
773 if self.common.skip_permissions && !in_sandbox {
774 args.push("--dangerously-skip-permissions".to_string());
775 }
776
777 args.extend(["--model".to_string(), self.common.model.clone()]);
778
779 for dir in &self.common.add_dirs {
780 args.extend(["--add-dir".to_string(), dir.clone()]);
781 }
782
783 if let Some(ref schema) = self.json_schema {
784 args.extend(["--json-schema".to_string(), schema.clone()]);
785 }
786
787 args.push(prompt.to_string());
788
789 let mut cmd = self.make_command(args);
790
791 cmd.stdin(Stdio::inherit());
792 cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
793
794 let output = cmd.output().await?;
795
796 crate::process::handle_output(&output, "Claude")?;
797
798 let json_str = String::from_utf8(output.stdout)?;
800 log::debug!(
801 "Parsing Claude resume JSON output ({} bytes)",
802 json_str.len()
803 );
804 let claude_output: models::ClaudeOutput = serde_json::from_str(&json_str)
805 .map_err(|e| anyhow::anyhow!("Failed to parse Claude resume JSON output: {}", e))?;
806
807 let agent_output: AgentOutput = models::claude_output_to_agent_output(claude_output);
808 Ok(Some(agent_output))
809 }
810
811 async fn cleanup(&self) -> Result<()> {
812 Ok(())
813 }
814}