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