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(
233 &self,
234 prompt: Option<&str>,
235 ) -> Result<crate::streaming::StreamingSession> {
236 let mut args = Vec::new();
238 let in_sandbox = self.common.sandbox.is_some();
239
240 args.push("--print".to_string());
241 args.extend(["--verbose", "--output-format", "stream-json"].map(String::from));
242
243 if self.common.skip_permissions && !in_sandbox {
244 args.push("--dangerously-skip-permissions".to_string());
245 }
246
247 args.extend(["--model".to_string(), self.common.model.clone()]);
248
249 for dir in &self.common.add_dirs {
250 args.extend(["--add-dir".to_string(), dir.clone()]);
251 }
252
253 if !self.common.system_prompt.is_empty() {
254 args.extend([
255 "--append-system-prompt".to_string(),
256 self.common.system_prompt.clone(),
257 ]);
258 }
259
260 args.extend(["--input-format".to_string(), "stream-json".to_string()]);
261 args.push("--replay-user-messages".to_string());
262
263 if self.include_partial_messages {
264 args.push("--include-partial-messages".to_string());
265 }
266
267 if let Some(ref schema) = self.json_schema {
268 args.extend(["--json-schema".to_string(), schema.clone()]);
269 }
270
271 if let Some(p) = prompt {
272 args.push(p.to_string());
273 }
274
275 log::debug!("Claude streaming command: claude {}", args.join(" "));
276
277 let mut cmd = self.make_command(args);
278 cmd.stdin(Stdio::piped())
279 .stdout(Stdio::piped())
280 .stderr(Stdio::piped());
281
282 let child = cmd
283 .spawn()
284 .context("Failed to execute 'claude' CLI. Is it installed and in PATH?")?;
285 crate::streaming::StreamingSession::new(child)
286 }
287
288 fn build_streaming_resume_args(&self, session_id: &str) -> Vec<String> {
290 let mut args = Vec::new();
291 let in_sandbox = self.common.sandbox.is_some();
292
293 args.push("--print".to_string());
294 args.extend(["--resume".to_string(), session_id.to_string()]);
295 args.extend(["--verbose", "--output-format", "stream-json"].map(String::from));
296
297 if self.common.skip_permissions && !in_sandbox {
298 args.push("--dangerously-skip-permissions".to_string());
299 }
300
301 args.extend(["--model".to_string(), self.common.model.clone()]);
302
303 for dir in &self.common.add_dirs {
304 args.extend(["--add-dir".to_string(), dir.clone()]);
305 }
306
307 args.extend(["--input-format".to_string(), "stream-json".to_string()]);
308 args.push("--replay-user-messages".to_string());
309
310 if self.include_partial_messages {
311 args.push("--include-partial-messages".to_string());
312 }
313
314 args
315 }
316
317 pub fn execute_streaming_resume(
323 &self,
324 session_id: &str,
325 ) -> Result<crate::streaming::StreamingSession> {
326 let args = self.build_streaming_resume_args(session_id);
327
328 log::debug!("Claude streaming resume command: claude {}", args.join(" "));
329
330 let mut cmd = self.make_command(args);
331 cmd.stdin(Stdio::piped())
332 .stdout(Stdio::piped())
333 .stderr(Stdio::piped());
334
335 let child = cmd
336 .spawn()
337 .context("Failed to execute 'claude' CLI. Is it installed and in PATH?")?;
338 crate::streaming::StreamingSession::new(child)
339 }
340
341 async fn execute(
342 &self,
343 interactive: bool,
344 prompt: Option<&str>,
345 ) -> Result<Option<AgentOutput>> {
346 let effective_output_format =
349 if self.common.capture_output && self.common.output_format.is_none() {
350 Some("json".to_string())
351 } else {
352 self.common.output_format.clone()
353 };
354
355 let capture_json = !interactive
358 && effective_output_format
359 .as_ref()
360 .is_none_or(|f| f == "json" || f == "json-pretty" || f == "stream-json");
361
362 let agent_args = self.build_run_args(interactive, prompt, &effective_output_format);
363 log::debug!("Claude command: claude {}", agent_args.join(" "));
364 if !self.common.system_prompt.is_empty() {
365 log::debug!("Claude system prompt: {}", self.common.system_prompt);
366 }
367 if let Some(p) = prompt {
368 log::debug!("Claude user prompt: {}", p);
369 }
370 log::debug!(
371 "Claude mode: interactive={}, capture_json={}, output_format={:?}",
372 interactive,
373 capture_json,
374 effective_output_format
375 );
376 let mut cmd = self.make_command(agent_args);
377
378 let is_native_json = effective_output_format.as_deref() == Some("native-json");
380
381 if interactive {
382 cmd.stdin(Stdio::inherit())
384 .stdout(Stdio::inherit())
385 .stderr(Stdio::inherit());
386
387 let status = cmd
388 .status()
389 .await
390 .context("Failed to execute 'claude' CLI. Is it installed and in PATH?")?;
391 if !status.success() {
392 return Err(crate::process::ProcessError {
393 exit_code: status.code(),
394 stderr: String::new(),
395 agent_name: "Claude".to_string(),
396 }
397 .into());
398 }
399 Ok(None)
400 } else if is_native_json {
401 cmd.stdin(Stdio::inherit()).stdout(Stdio::inherit());
403
404 crate::process::run_with_captured_stderr(&mut cmd).await?;
405 Ok(None)
406 } else if capture_json {
407 let output_format = effective_output_format.as_deref();
408 let is_streaming = output_format == Some("stream-json") || output_format.is_none();
409
410 if is_streaming {
411 cmd.stdin(Stdio::inherit());
413 cmd.stdout(Stdio::piped());
414
415 let mut child = crate::process::spawn_with_captured_stderr(&mut cmd).await?;
416 let stdout = child
417 .stdout
418 .take()
419 .ok_or_else(|| anyhow::anyhow!("Failed to capture stdout"))?;
420
421 let reader = BufReader::new(stdout);
422 let mut lines = reader.lines();
423
424 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? {
430 if format_as_text || format_as_json {
431 match serde_json::from_str::<models::ClaudeEvent>(&line) {
432 Ok(claude_event) => {
433 if let Some(unified_event) =
434 convert_claude_event_to_unified(&claude_event)
435 {
436 if let Some(ref handler) = self.event_handler {
437 handler(&unified_event, self.verbose);
438 }
439 }
440 }
441 Err(e) => {
442 log::debug!(
443 "Failed to parse streaming Claude event: {}. Line: {}",
444 e,
445 crate::truncate_str(&line, 200)
446 );
447 }
448 }
449 }
450 }
451
452 if let Some(ref handler) = self.event_handler {
454 handler(
456 &crate::output::Event::Result {
457 success: true,
458 message: None,
459 duration_ms: None,
460 num_turns: None,
461 },
462 self.verbose,
463 );
464 }
465
466 crate::process::wait_with_stderr(child).await?;
467
468 Ok(None)
470 } else {
471 cmd.stdin(Stdio::inherit());
473 cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
474
475 let output = cmd.output().await?;
476
477 crate::process::handle_output(&output, "Claude")?;
478
479 let json_str = String::from_utf8(output.stdout)?;
481 log::debug!("Parsing Claude JSON output ({} bytes)", json_str.len());
482 let claude_output: models::ClaudeOutput =
483 serde_json::from_str(&json_str).map_err(|e| {
484 log::debug!(
485 "Failed to parse Claude JSON output: {}. First 500 chars: {}",
486 e,
487 crate::truncate_str(&json_str, 500)
488 );
489 anyhow::anyhow!("Failed to parse Claude JSON output: {}", e)
490 })?;
491 log::debug!("Parsed {} Claude events successfully", claude_output.len());
492
493 let agent_output: AgentOutput =
495 models::claude_output_to_agent_output(claude_output);
496 Ok(Some(agent_output))
497 }
498 } else {
499 cmd.stdin(Stdio::inherit()).stdout(Stdio::inherit());
501
502 crate::process::run_with_captured_stderr(&mut cmd).await?;
503 Ok(None)
504 }
505 }
506}
507
508fn convert_claude_event_to_unified(event: &models::ClaudeEvent) -> Option<crate::output::Event> {
511 use crate::output::{
512 ContentBlock as UnifiedContentBlock, Event as UnifiedEvent, ToolResult,
513 Usage as UnifiedUsage,
514 };
515 use models::ClaudeEvent;
516
517 match event {
518 ClaudeEvent::System {
519 model, tools, cwd, ..
520 } => {
521 let mut metadata = std::collections::HashMap::new();
522 if let Some(cwd_val) = cwd {
523 metadata.insert("cwd".to_string(), serde_json::json!(cwd_val));
524 }
525
526 Some(UnifiedEvent::Init {
527 model: model.clone(),
528 tools: tools.clone(),
529 working_directory: cwd.clone(),
530 metadata,
531 })
532 }
533
534 ClaudeEvent::Assistant {
535 message,
536 parent_tool_use_id,
537 ..
538 } => {
539 let content: Vec<UnifiedContentBlock> = message
541 .content
542 .iter()
543 .filter_map(|block| match block {
544 models::ContentBlock::Text { text } => {
545 Some(UnifiedContentBlock::Text { text: text.clone() })
546 }
547 models::ContentBlock::ToolUse { id, name, input } => {
548 Some(UnifiedContentBlock::ToolUse {
549 id: id.clone(),
550 name: name.clone(),
551 input: input.clone(),
552 })
553 }
554 models::ContentBlock::Thinking { .. } => None,
555 })
556 .collect();
557
558 let usage = Some(UnifiedUsage {
560 input_tokens: message.usage.input_tokens,
561 output_tokens: message.usage.output_tokens,
562 cache_read_tokens: Some(message.usage.cache_read_input_tokens),
563 cache_creation_tokens: Some(message.usage.cache_creation_input_tokens),
564 web_search_requests: message
565 .usage
566 .server_tool_use
567 .as_ref()
568 .map(|s| s.web_search_requests),
569 web_fetch_requests: message
570 .usage
571 .server_tool_use
572 .as_ref()
573 .map(|s| s.web_fetch_requests),
574 });
575
576 Some(UnifiedEvent::AssistantMessage {
577 content,
578 usage,
579 parent_tool_use_id: parent_tool_use_id.clone(),
580 })
581 }
582
583 ClaudeEvent::User {
584 message,
585 tool_use_result,
586 parent_tool_use_id,
587 ..
588 } => {
589 let first_tool_result = message.content.iter().find_map(|b| {
593 if let models::UserContentBlock::ToolResult {
594 tool_use_id,
595 content,
596 is_error,
597 } = b
598 {
599 Some((tool_use_id, content, is_error))
600 } else {
601 None
602 }
603 });
604
605 if let Some((tool_use_id, content, is_error)) = first_tool_result {
606 let tool_result = ToolResult {
607 success: !is_error,
608 output: if !is_error {
609 Some(content.clone())
610 } else {
611 None
612 },
613 error: if *is_error {
614 Some(content.clone())
615 } else {
616 None
617 },
618 data: tool_use_result.clone(),
619 };
620
621 Some(UnifiedEvent::ToolExecution {
622 tool_name: "unknown".to_string(),
623 tool_id: tool_use_id.clone(),
624 input: serde_json::Value::Null,
625 result: tool_result,
626 parent_tool_use_id: parent_tool_use_id.clone(),
627 })
628 } else {
629 let text_blocks: Vec<UnifiedContentBlock> = message
631 .content
632 .iter()
633 .filter_map(|b| {
634 if let models::UserContentBlock::Text { text } = b {
635 Some(UnifiedContentBlock::Text { text: text.clone() })
636 } else {
637 None
638 }
639 })
640 .collect();
641
642 if !text_blocks.is_empty() {
643 Some(UnifiedEvent::UserMessage {
644 content: text_blocks,
645 })
646 } else {
647 None
648 }
649 }
650 }
651
652 ClaudeEvent::Other => {
653 log::debug!("Skipping unknown Claude event type during streaming conversion");
654 None
655 }
656
657 ClaudeEvent::Result {
658 is_error,
659 result,
660 duration_ms,
661 num_turns,
662 ..
663 } => Some(UnifiedEvent::Result {
664 success: !is_error,
665 message: Some(result.clone()),
666 duration_ms: Some(*duration_ms),
667 num_turns: Some(*num_turns),
668 }),
669 }
670}
671
672#[cfg(test)]
673#[path = "claude_tests.rs"]
674mod tests;
675
676impl Default for Claude {
677 fn default() -> Self {
678 Self::new()
679 }
680}
681
682#[async_trait]
683impl Agent for Claude {
684 fn name(&self) -> &str {
685 "claude"
686 }
687
688 fn default_model() -> &'static str {
689 DEFAULT_MODEL
690 }
691
692 fn model_for_size(size: ModelSize) -> &'static str {
693 match size {
694 ModelSize::Small => "haiku",
695 ModelSize::Medium => "sonnet",
696 ModelSize::Large => "default",
697 }
698 }
699
700 fn available_models() -> &'static [&'static str] {
701 AVAILABLE_MODELS
702 }
703
704 crate::providers::common::impl_common_agent_setters!();
705
706 fn set_skip_permissions(&mut self, skip: bool) {
707 self.common.skip_permissions = skip;
708 }
709
710 crate::providers::common::impl_as_any!();
711
712 async fn run(&self, prompt: Option<&str>) -> Result<Option<AgentOutput>> {
713 self.execute(false, prompt).await
714 }
715
716 async fn run_interactive(&self, prompt: Option<&str>) -> Result<()> {
717 self.execute(true, prompt).await?;
718 Ok(())
719 }
720
721 async fn run_resume(&self, session_id: Option<&str>, _last: bool) -> Result<()> {
722 let agent_args = self.build_resume_args(session_id);
723 let mut cmd = self.make_command(agent_args);
724
725 cmd.stdin(Stdio::inherit())
726 .stdout(Stdio::inherit())
727 .stderr(Stdio::inherit());
728
729 let status = cmd
730 .status()
731 .await
732 .context("Failed to execute 'claude' CLI. Is it installed and in PATH?")?;
733 if !status.success() {
734 return Err(crate::process::ProcessError {
735 exit_code: status.code(),
736 stderr: String::new(),
737 agent_name: "Claude".to_string(),
738 }
739 .into());
740 }
741 Ok(())
742 }
743
744 async fn run_resume_with_prompt(
745 &self,
746 session_id: &str,
747 prompt: &str,
748 ) -> Result<Option<AgentOutput>> {
749 log::debug!(
750 "Claude resume with prompt: session={}, prompt={}",
751 session_id,
752 prompt
753 );
754 let in_sandbox = self.common.sandbox.is_some();
755 let mut args = vec!["--print".to_string()];
756 args.extend(["--resume".to_string(), session_id.to_string()]);
757 args.extend(["--verbose", "--output-format", "json"].map(String::from));
758
759 if self.common.skip_permissions && !in_sandbox {
760 args.push("--dangerously-skip-permissions".to_string());
761 }
762
763 args.extend(["--model".to_string(), self.common.model.clone()]);
764
765 for dir in &self.common.add_dirs {
766 args.extend(["--add-dir".to_string(), dir.clone()]);
767 }
768
769 if let Some(ref schema) = self.json_schema {
770 args.extend(["--json-schema".to_string(), schema.clone()]);
771 }
772
773 args.push(prompt.to_string());
774
775 let mut cmd = self.make_command(args);
776
777 cmd.stdin(Stdio::inherit());
778 cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
779
780 let output = cmd.output().await?;
781
782 crate::process::handle_output(&output, "Claude")?;
783
784 let json_str = String::from_utf8(output.stdout)?;
786 log::debug!(
787 "Parsing Claude resume JSON output ({} bytes)",
788 json_str.len()
789 );
790 let claude_output: models::ClaudeOutput = serde_json::from_str(&json_str)
791 .map_err(|e| anyhow::anyhow!("Failed to parse Claude resume JSON output: {}", e))?;
792
793 let agent_output: AgentOutput = models::claude_output_to_agent_output(claude_output);
794 Ok(Some(agent_output))
795 }
796
797 async fn cleanup(&self) -> Result<()> {
798 Ok(())
799 }
800}