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::sandbox::SandboxConfig;
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 system_prompt: String,
43 model: String,
44 root: Option<String>,
45 session_id: Option<String>,
46 skip_permissions: bool,
47 output_format: Option<String>,
48 input_format: Option<String>,
49 add_dirs: Vec<String>,
50 capture_output: bool,
51 verbose: bool,
52 json_schema: Option<String>,
53 sandbox: Option<SandboxConfig>,
54 event_handler: Option<EventHandler>,
55 replay_user_messages: bool,
56 include_partial_messages: bool,
57 max_turns: Option<u32>,
58}
59
60impl Claude {
61 pub fn new() -> Self {
62 Self {
63 system_prompt: String::new(),
64 model: DEFAULT_MODEL.to_string(),
65 root: None,
66 session_id: None,
67 skip_permissions: false,
68 output_format: None,
69 input_format: None,
70 add_dirs: Vec::new(),
71 capture_output: false,
72 verbose: false,
73 json_schema: None,
74 sandbox: None,
75 event_handler: None,
76 replay_user_messages: false,
77 include_partial_messages: false,
78 max_turns: None,
79 }
80 }
81
82 pub fn set_input_format(&mut self, format: Option<String>) {
83 self.input_format = format;
84 }
85
86 pub fn set_session_id(&mut self, session_id: String) {
87 self.session_id = Some(session_id);
88 }
89
90 pub fn set_verbose(&mut self, verbose: bool) {
91 self.verbose = verbose;
92 }
93
94 pub fn set_json_schema(&mut self, schema: Option<String>) {
95 self.json_schema = schema;
96 }
97
98 pub fn set_replay_user_messages(&mut self, replay: bool) {
99 self.replay_user_messages = replay;
100 }
101
102 pub fn set_include_partial_messages(&mut self, include: bool) {
103 self.include_partial_messages = include;
104 }
105
106 pub fn set_event_handler(&mut self, handler: EventHandler) {
111 self.event_handler = Some(handler);
112 }
113
114 fn build_run_args(
116 &self,
117 interactive: bool,
118 prompt: Option<&str>,
119 effective_output_format: &Option<String>,
120 ) -> Vec<String> {
121 let mut args = Vec::new();
122 let in_sandbox = self.sandbox.is_some();
123
124 if !interactive {
125 args.push("--print".to_string());
126
127 match effective_output_format.as_deref() {
128 Some("json") | Some("json-pretty") => {
129 args.extend(["--verbose", "--output-format", "json"].map(String::from));
130 }
131 Some("stream-json") | None => {
132 args.extend(["--verbose", "--output-format", "stream-json"].map(String::from));
133 }
134 Some("native-json") => {
135 args.extend(["--verbose", "--output-format", "json"].map(String::from));
136 }
137 Some("text") => {}
138 _ => {}
139 }
140 }
141
142 if self.skip_permissions && !in_sandbox {
144 args.push("--dangerously-skip-permissions".to_string());
145 }
146
147 args.extend(["--model".to_string(), self.model.clone()]);
148
149 if interactive && let Some(session_id) = &self.session_id {
150 args.extend(["--session-id".to_string(), session_id.clone()]);
151 }
152
153 for dir in &self.add_dirs {
154 args.extend(["--add-dir".to_string(), dir.clone()]);
155 }
156
157 if !self.system_prompt.is_empty() {
158 args.extend([
159 "--append-system-prompt".to_string(),
160 self.system_prompt.clone(),
161 ]);
162 }
163
164 if !interactive && let Some(ref input_fmt) = self.input_format {
165 args.extend(["--input-format".to_string(), input_fmt.clone()]);
166 }
167
168 if !interactive && self.replay_user_messages {
169 args.push("--replay-user-messages".to_string());
170 }
171
172 if !interactive && self.include_partial_messages {
173 args.push("--include-partial-messages".to_string());
174 }
175
176 if let Some(ref schema) = self.json_schema {
177 args.extend(["--json-schema".to_string(), schema.clone()]);
178 }
179
180 if let Some(turns) = self.max_turns {
181 args.extend(["--max-turns".to_string(), turns.to_string()]);
182 }
183
184 if let Some(p) = prompt {
185 args.push(p.to_string());
186 }
187
188 args
189 }
190
191 fn build_resume_args(&self, session_id: Option<&str>) -> Vec<String> {
193 let mut args = Vec::new();
194 let in_sandbox = self.sandbox.is_some();
195
196 if let Some(id) = session_id {
197 args.extend(["--resume".to_string(), id.to_string()]);
198 } else {
199 args.push("--continue".to_string());
200 }
201
202 if self.skip_permissions && !in_sandbox {
203 args.push("--dangerously-skip-permissions".to_string());
204 }
205
206 args.extend(["--model".to_string(), self.model.clone()]);
207
208 for dir in &self.add_dirs {
209 args.extend(["--add-dir".to_string(), dir.clone()]);
210 }
211
212 args
213 }
214
215 fn make_command(&self, agent_args: Vec<String>) -> Command {
217 if let Some(ref sb) = self.sandbox {
218 let std_cmd = crate::sandbox::build_sandbox_command(sb, agent_args);
219 Command::from(std_cmd)
220 } else {
221 let mut cmd = Command::new("claude");
222 if let Some(ref root) = self.root {
223 cmd.current_dir(root);
224 }
225 cmd.args(&agent_args);
226 cmd
227 }
228 }
229
230 pub fn execute_streaming(
236 &self,
237 prompt: Option<&str>,
238 ) -> Result<crate::streaming::StreamingSession> {
239 let mut args = Vec::new();
241 let in_sandbox = self.sandbox.is_some();
242
243 args.push("--print".to_string());
244 args.extend(["--verbose", "--output-format", "stream-json"].map(String::from));
245
246 if self.skip_permissions && !in_sandbox {
247 args.push("--dangerously-skip-permissions".to_string());
248 }
249
250 args.extend(["--model".to_string(), self.model.clone()]);
251
252 for dir in &self.add_dirs {
253 args.extend(["--add-dir".to_string(), dir.clone()]);
254 }
255
256 if !self.system_prompt.is_empty() {
257 args.extend([
258 "--append-system-prompt".to_string(),
259 self.system_prompt.clone(),
260 ]);
261 }
262
263 args.extend(["--input-format".to_string(), "stream-json".to_string()]);
264 args.push("--replay-user-messages".to_string());
265
266 if self.include_partial_messages {
267 args.push("--include-partial-messages".to_string());
268 }
269
270 if let Some(ref schema) = self.json_schema {
271 args.extend(["--json-schema".to_string(), schema.clone()]);
272 }
273
274 if let Some(p) = prompt {
275 args.push(p.to_string());
276 }
277
278 log::debug!("Claude streaming command: claude {}", args.join(" "));
279
280 let mut cmd = self.make_command(args);
281 cmd.stdin(Stdio::piped())
282 .stdout(Stdio::piped())
283 .stderr(Stdio::piped());
284
285 let child = cmd
286 .spawn()
287 .context("Failed to execute 'claude' CLI. Is it installed and in PATH?")?;
288 crate::streaming::StreamingSession::new(child)
289 }
290
291 fn build_streaming_resume_args(&self, session_id: &str) -> Vec<String> {
293 let mut args = Vec::new();
294 let in_sandbox = self.sandbox.is_some();
295
296 args.push("--print".to_string());
297 args.extend(["--resume".to_string(), session_id.to_string()]);
298 args.extend(["--verbose", "--output-format", "stream-json"].map(String::from));
299
300 if self.skip_permissions && !in_sandbox {
301 args.push("--dangerously-skip-permissions".to_string());
302 }
303
304 args.extend(["--model".to_string(), self.model.clone()]);
305
306 for dir in &self.add_dirs {
307 args.extend(["--add-dir".to_string(), dir.clone()]);
308 }
309
310 args.extend(["--input-format".to_string(), "stream-json".to_string()]);
311 args.push("--replay-user-messages".to_string());
312
313 if self.include_partial_messages {
314 args.push("--include-partial-messages".to_string());
315 }
316
317 args
318 }
319
320 pub fn execute_streaming_resume(
326 &self,
327 session_id: &str,
328 ) -> Result<crate::streaming::StreamingSession> {
329 let args = self.build_streaming_resume_args(session_id);
330
331 log::debug!("Claude streaming resume command: claude {}", args.join(" "));
332
333 let mut cmd = self.make_command(args);
334 cmd.stdin(Stdio::piped())
335 .stdout(Stdio::piped())
336 .stderr(Stdio::piped());
337
338 let child = cmd
339 .spawn()
340 .context("Failed to execute 'claude' CLI. Is it installed and in PATH?")?;
341 crate::streaming::StreamingSession::new(child)
342 }
343
344 async fn execute(
345 &self,
346 interactive: bool,
347 prompt: Option<&str>,
348 ) -> Result<Option<AgentOutput>> {
349 let effective_output_format = if self.capture_output && self.output_format.is_none() {
352 Some("json".to_string())
353 } else {
354 self.output_format.clone()
355 };
356
357 let capture_json = !interactive
360 && effective_output_format
361 .as_ref()
362 .is_none_or(|f| f == "json" || f == "json-pretty" || f == "stream-json");
363
364 let agent_args = self.build_run_args(interactive, prompt, &effective_output_format);
365 log::debug!("Claude command: claude {}", agent_args.join(" "));
366 if !self.system_prompt.is_empty() {
367 log::debug!("Claude system prompt: {}", self.system_prompt);
368 }
369 if let Some(p) = prompt {
370 log::debug!("Claude user prompt: {}", p);
371 }
372 log::debug!(
373 "Claude mode: interactive={}, capture_json={}, output_format={:?}",
374 interactive,
375 capture_json,
376 effective_output_format
377 );
378 let mut cmd = self.make_command(agent_args);
379
380 let is_native_json = effective_output_format.as_deref() == Some("native-json");
382
383 if interactive {
384 cmd.stdin(Stdio::inherit())
386 .stdout(Stdio::inherit())
387 .stderr(Stdio::inherit());
388
389 let status = cmd
390 .status()
391 .await
392 .context("Failed to execute 'claude' CLI. Is it installed and in PATH?")?;
393 if !status.success() {
394 anyhow::bail!("Claude command failed with status: {}", status);
395 }
396 Ok(None)
397 } else if is_native_json {
398 cmd.stdin(Stdio::inherit()).stdout(Stdio::inherit());
400
401 crate::process::run_with_captured_stderr(&mut cmd).await?;
402 Ok(None)
403 } else if capture_json {
404 let output_format = effective_output_format.as_deref();
405 let is_streaming = output_format == Some("stream-json") || output_format.is_none();
406
407 if is_streaming {
408 cmd.stdin(Stdio::inherit());
410 cmd.stdout(Stdio::piped());
411
412 let mut child = crate::process::spawn_with_captured_stderr(&mut cmd).await?;
413 let stdout = child
414 .stdout
415 .take()
416 .ok_or_else(|| anyhow::anyhow!("Failed to capture stdout"))?;
417
418 let reader = BufReader::new(stdout);
419 let mut lines = reader.lines();
420
421 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? {
427 if format_as_text || format_as_json {
428 match serde_json::from_str::<models::ClaudeEvent>(&line) {
429 Ok(claude_event) => {
430 if let Some(unified_event) =
431 convert_claude_event_to_unified(&claude_event)
432 {
433 if let Some(ref handler) = self.event_handler {
434 handler(&unified_event, self.verbose);
435 }
436 }
437 }
438 Err(e) => {
439 log::debug!(
440 "Failed to parse streaming Claude event: {}. Line: {}",
441 e,
442 crate::truncate_str(&line, 200)
443 );
444 }
445 }
446 }
447 }
448
449 if let Some(ref handler) = self.event_handler {
451 handler(
453 &crate::output::Event::Result {
454 success: true,
455 message: None,
456 duration_ms: None,
457 num_turns: None,
458 },
459 self.verbose,
460 );
461 }
462
463 crate::process::wait_with_stderr(child).await?;
464
465 Ok(None)
467 } else {
468 cmd.stdin(Stdio::inherit());
470 cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
471
472 let output = cmd.output().await?;
473
474 crate::process::handle_output(&output, "Claude")?;
475
476 let json_str = String::from_utf8(output.stdout)?;
478 log::debug!("Parsing Claude JSON output ({} bytes)", json_str.len());
479 let claude_output: models::ClaudeOutput =
480 serde_json::from_str(&json_str).map_err(|e| {
481 log::debug!(
482 "Failed to parse Claude JSON output: {}. First 500 chars: {}",
483 e,
484 crate::truncate_str(&json_str, 500)
485 );
486 anyhow::anyhow!("Failed to parse Claude JSON output: {}", e)
487 })?;
488 log::debug!("Parsed {} Claude events successfully", claude_output.len());
489
490 let agent_output: AgentOutput =
492 models::claude_output_to_agent_output(claude_output);
493 Ok(Some(agent_output))
494 }
495 } else {
496 cmd.stdin(Stdio::inherit()).stdout(Stdio::inherit());
498
499 crate::process::run_with_captured_stderr(&mut cmd).await?;
500 Ok(None)
501 }
502 }
503}
504
505fn convert_claude_event_to_unified(event: &models::ClaudeEvent) -> Option<crate::output::Event> {
508 use crate::output::{
509 ContentBlock as UnifiedContentBlock, Event as UnifiedEvent, ToolResult,
510 Usage as UnifiedUsage,
511 };
512 use models::ClaudeEvent;
513
514 match event {
515 ClaudeEvent::System {
516 model, tools, cwd, ..
517 } => {
518 let mut metadata = std::collections::HashMap::new();
519 if let Some(cwd_val) = cwd {
520 metadata.insert("cwd".to_string(), serde_json::json!(cwd_val));
521 }
522
523 Some(UnifiedEvent::Init {
524 model: model.clone(),
525 tools: tools.clone(),
526 working_directory: cwd.clone(),
527 metadata,
528 })
529 }
530
531 ClaudeEvent::Assistant { message, .. } => {
532 let content: Vec<UnifiedContentBlock> = message
534 .content
535 .iter()
536 .filter_map(|block| match block {
537 models::ContentBlock::Text { text } => {
538 Some(UnifiedContentBlock::Text { text: text.clone() })
539 }
540 models::ContentBlock::ToolUse { id, name, input } => {
541 Some(UnifiedContentBlock::ToolUse {
542 id: id.clone(),
543 name: name.clone(),
544 input: input.clone(),
545 })
546 }
547 models::ContentBlock::Thinking { .. } => None,
548 })
549 .collect();
550
551 let usage = Some(UnifiedUsage {
553 input_tokens: message.usage.input_tokens,
554 output_tokens: message.usage.output_tokens,
555 cache_read_tokens: Some(message.usage.cache_read_input_tokens),
556 cache_creation_tokens: Some(message.usage.cache_creation_input_tokens),
557 web_search_requests: message
558 .usage
559 .server_tool_use
560 .as_ref()
561 .map(|s| s.web_search_requests),
562 web_fetch_requests: message
563 .usage
564 .server_tool_use
565 .as_ref()
566 .map(|s| s.web_fetch_requests),
567 });
568
569 Some(UnifiedEvent::AssistantMessage { content, usage })
570 }
571
572 ClaudeEvent::User {
573 message,
574 tool_use_result,
575 ..
576 } => {
577 let first_tool_result = message.content.iter().find_map(|b| {
581 if let models::UserContentBlock::ToolResult {
582 tool_use_id,
583 content,
584 is_error,
585 } = b
586 {
587 Some((tool_use_id, content, is_error))
588 } else {
589 None
590 }
591 });
592
593 if let Some((tool_use_id, content, is_error)) = first_tool_result {
594 let tool_result = ToolResult {
595 success: !is_error,
596 output: if !is_error {
597 Some(content.clone())
598 } else {
599 None
600 },
601 error: if *is_error {
602 Some(content.clone())
603 } else {
604 None
605 },
606 data: tool_use_result.clone(),
607 };
608
609 Some(UnifiedEvent::ToolExecution {
610 tool_name: "unknown".to_string(),
611 tool_id: tool_use_id.clone(),
612 input: serde_json::Value::Null,
613 result: tool_result,
614 })
615 } else {
616 let text_blocks: Vec<UnifiedContentBlock> = message
618 .content
619 .iter()
620 .filter_map(|b| {
621 if let models::UserContentBlock::Text { text } = b {
622 Some(UnifiedContentBlock::Text { text: text.clone() })
623 } else {
624 None
625 }
626 })
627 .collect();
628
629 if !text_blocks.is_empty() {
630 Some(UnifiedEvent::UserMessage {
631 content: text_blocks,
632 })
633 } else {
634 None
635 }
636 }
637 }
638
639 ClaudeEvent::Other => {
640 log::debug!("Skipping unknown Claude event type during streaming conversion");
641 None
642 }
643
644 ClaudeEvent::Result {
645 is_error,
646 result,
647 duration_ms,
648 num_turns,
649 ..
650 } => Some(UnifiedEvent::Result {
651 success: !is_error,
652 message: Some(result.clone()),
653 duration_ms: Some(*duration_ms),
654 num_turns: Some(*num_turns),
655 }),
656 }
657}
658
659#[cfg(test)]
660#[path = "claude_tests.rs"]
661mod tests;
662
663impl Default for Claude {
664 fn default() -> Self {
665 Self::new()
666 }
667}
668
669#[async_trait]
670impl Agent for Claude {
671 fn name(&self) -> &str {
672 "claude"
673 }
674
675 fn default_model() -> &'static str {
676 DEFAULT_MODEL
677 }
678
679 fn model_for_size(size: ModelSize) -> &'static str {
680 match size {
681 ModelSize::Small => "haiku",
682 ModelSize::Medium => "sonnet",
683 ModelSize::Large => "default",
684 }
685 }
686
687 fn available_models() -> &'static [&'static str] {
688 AVAILABLE_MODELS
689 }
690
691 fn system_prompt(&self) -> &str {
692 &self.system_prompt
693 }
694
695 fn set_system_prompt(&mut self, prompt: String) {
696 self.system_prompt = prompt;
697 }
698
699 fn get_model(&self) -> &str {
700 &self.model
701 }
702
703 fn set_model(&mut self, model: String) {
704 self.model = model;
705 }
706
707 fn set_root(&mut self, root: String) {
708 self.root = Some(root);
709 }
710
711 fn set_skip_permissions(&mut self, skip: bool) {
712 self.skip_permissions = skip;
713 }
714
715 fn set_output_format(&mut self, format: Option<String>) {
716 self.output_format = format;
717 }
718
719 fn set_capture_output(&mut self, capture: bool) {
720 self.capture_output = capture;
721 }
722
723 fn set_max_turns(&mut self, turns: u32) {
724 self.max_turns = Some(turns);
725 }
726
727 fn set_sandbox(&mut self, config: SandboxConfig) {
728 self.sandbox = Some(config);
729 }
730
731 fn set_add_dirs(&mut self, dirs: Vec<String>) {
732 self.add_dirs = dirs;
733 }
734
735 fn as_any_ref(&self) -> &dyn std::any::Any {
736 self
737 }
738
739 fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
740 self
741 }
742
743 async fn run(&self, prompt: Option<&str>) -> Result<Option<AgentOutput>> {
744 self.execute(false, prompt).await
745 }
746
747 async fn run_interactive(&self, prompt: Option<&str>) -> Result<()> {
748 self.execute(true, prompt).await?;
749 Ok(())
750 }
751
752 async fn run_resume(&self, session_id: Option<&str>, _last: bool) -> Result<()> {
753 let agent_args = self.build_resume_args(session_id);
754 let mut cmd = self.make_command(agent_args);
755
756 cmd.stdin(Stdio::inherit())
757 .stdout(Stdio::inherit())
758 .stderr(Stdio::inherit());
759
760 let status = cmd
761 .status()
762 .await
763 .context("Failed to execute 'claude' CLI. Is it installed and in PATH?")?;
764 if !status.success() {
765 anyhow::bail!("Claude resume failed with status: {}", status);
766 }
767 Ok(())
768 }
769
770 async fn run_resume_with_prompt(
771 &self,
772 session_id: &str,
773 prompt: &str,
774 ) -> Result<Option<AgentOutput>> {
775 log::debug!(
776 "Claude resume with prompt: session={}, prompt={}",
777 session_id,
778 prompt
779 );
780 let in_sandbox = self.sandbox.is_some();
781 let mut args = vec!["--print".to_string()];
782 args.extend(["--resume".to_string(), session_id.to_string()]);
783 args.extend(["--verbose", "--output-format", "json"].map(String::from));
784
785 if self.skip_permissions && !in_sandbox {
786 args.push("--dangerously-skip-permissions".to_string());
787 }
788
789 args.extend(["--model".to_string(), self.model.clone()]);
790
791 for dir in &self.add_dirs {
792 args.extend(["--add-dir".to_string(), dir.clone()]);
793 }
794
795 if let Some(ref schema) = self.json_schema {
796 args.extend(["--json-schema".to_string(), schema.clone()]);
797 }
798
799 args.push(prompt.to_string());
800
801 let mut cmd = self.make_command(args);
802
803 cmd.stdin(Stdio::inherit());
804 cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
805
806 let output = cmd.output().await?;
807
808 crate::process::handle_output(&output, "Claude")?;
809
810 let json_str = String::from_utf8(output.stdout)?;
812 log::debug!(
813 "Parsing Claude resume JSON output ({} bytes)",
814 json_str.len()
815 );
816 let claude_output: models::ClaudeOutput = serde_json::from_str(&json_str)
817 .map_err(|e| anyhow::anyhow!("Failed to parse Claude resume JSON output: {}", e))?;
818
819 let agent_output: AgentOutput = models::claude_output_to_agent_output(claude_output);
820 Ok(Some(agent_output))
821 }
822
823 async fn cleanup(&self) -> Result<()> {
824 Ok(())
825 }
826}