Skip to main content

claude_dialog/
claude_executor.rs

1//! Claude command execution module
2//!
3//! This module handles building and executing Claude CLI commands with the appropriate
4//! arguments for prompts, models, and tool permissions.
5//!
6//! # Examples
7//!
8//! ```no_run
9//! use claude_dialog::claude_executor::{ClaudeCommand, execute_claude};
10//!
11//! # #[tokio::main]
12//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
13//! // Create a basic Claude command
14//! let command = ClaudeCommand {
15//!     prompt: "Hello, Claude!".to_string(),
16//!     system_prompt: None,
17//!     append_prompt: None,
18//!     model: Some("claude-3-opus".to_string()),
19//! };
20//!
21//! // Execute the command
22//! execute_claude(command).await?;
23//! # Ok(())
24//! # }
25//! ```
26
27use anyhow::{Result, Context};
28use tokio::process::Command;
29
30/// Represents a Claude command with all necessary parameters
31///
32/// This structure encapsulates all the information needed to construct
33/// and execute a Claude CLI command, including prompts, model selection,
34/// and system prompt configuration.
35///
36/// # Examples
37///
38/// ```
39/// use claude_dialog::claude_executor::ClaudeCommand;
40///
41/// // Basic command with just a prompt
42/// let cmd = ClaudeCommand {
43///     prompt: "What is Rust?".to_string(),
44///     system_prompt: None,
45///     append_prompt: None,
46///     model: None,
47/// };
48///
49/// // Command with custom system prompt and model
50/// let cmd = ClaudeCommand {
51///     prompt: "Explain memory safety".to_string(),
52///     system_prompt: Some("You are a Rust expert.".to_string()),
53///     append_prompt: None,
54///     model: Some("claude-3-opus".to_string()),
55/// };
56///
57/// // Command with append prompt
58/// let cmd = ClaudeCommand {
59///     prompt: "Write a function".to_string(),
60///     system_prompt: None,
61///     append_prompt: Some("Always use idiomatic Rust.".to_string()),
62///     model: None,
63/// };
64/// ```
65#[derive(Debug, Clone)]
66pub struct ClaudeCommand {
67    /// The main prompt to send to Claude
68    pub prompt: String,
69    
70    /// Optional system prompt that replaces the default
71    pub system_prompt: Option<String>,
72    
73    /// Optional prompt to append to the default system prompt
74    pub append_prompt: Option<String>,
75    
76    /// Optional model specification (e.g., "claude-3-opus")
77    pub model: Option<String>,
78}
79
80impl ClaudeCommand {
81    /// Build command-line arguments for the Claude CLI
82    ///
83    /// Constructs a vector of arguments based on the command configuration,
84    /// including the prompt, system prompts, model selection, and allowed tools.
85    ///
86    /// # Returns
87    ///
88    /// A vector of strings representing the command-line arguments
89    ///
90    /// # Examples
91    ///
92    /// ```
93    /// use claude_dialog::claude_executor::ClaudeCommand;
94    ///
95    /// let cmd = ClaudeCommand {
96    ///     prompt: "Hello".to_string(),
97    ///     system_prompt: Some("Be helpful".to_string()),
98    ///     append_prompt: None,
99    ///     model: Some("claude-3-opus".to_string()),
100    /// };
101    ///
102    /// let args = cmd.build_args();
103    /// assert!(args.contains(&"--continue".to_string()));
104    /// assert!(args.contains(&"-p".to_string()));
105    /// assert!(args.contains(&"Hello".to_string()));
106    /// assert!(args.contains(&"--system-prompt".to_string()));
107    /// assert!(args.contains(&"Be helpful".to_string()));
108    /// assert!(args.contains(&"--model".to_string()));
109    /// assert!(args.contains(&"claude-3-opus".to_string()));
110    /// ```
111    pub fn build_args(&self) -> Vec<String> {
112        let mut args = vec!["--continue".to_string(), "-p".to_string(), self.prompt.clone()];
113        
114        if let Some(system_prompt) = &self.system_prompt {
115            args.push("--system-prompt".to_string());
116            args.push(system_prompt.clone());
117        }
118        
119        if let Some(append_prompt) = &self.append_prompt {
120            args.push("--append-system-prompt".to_string());
121            args.push(append_prompt.clone());
122        }
123        
124        if let Some(model) = &self.model {
125            args.push("--model".to_string());
126            args.push(model.clone());
127        }
128        
129        // Add allowed tools
130        args.push("--allowedTools".to_string());
131        args.push("Write".to_string());
132        args.push("Edit".to_string());
133        
134        args
135    }
136}
137
138/// Execute a Claude command asynchronously
139///
140/// This function builds the command arguments and executes the Claude CLI
141/// with the specified configuration. It waits for the command to complete
142/// and returns an error if the command fails.
143///
144/// # Arguments
145///
146/// * `command` - The Claude command configuration to execute
147///
148/// # Returns
149///
150/// * `Result<()>` - Success or an error if the command fails
151///
152/// # Errors
153///
154/// Returns an error if:
155/// - The Claude CLI is not found or cannot be executed
156/// - The Claude command returns a non-zero exit status
157///
158/// # Examples
159///
160/// ```no_run
161/// use claude_dialog::claude_executor::{ClaudeCommand, execute_claude};
162///
163/// # #[tokio::main]
164/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
165/// let command = ClaudeCommand {
166///     prompt: "What is 2 + 2?".to_string(),
167///     system_prompt: None,
168///     append_prompt: None,
169///     model: None,
170/// };
171///
172/// execute_claude(command).await?;
173/// # Ok(())
174/// # }
175/// ```
176pub async fn execute_claude(command: ClaudeCommand) -> Result<()> {
177    let args = command.build_args();
178    
179    let mut cmd = Command::new("claude");
180    cmd.args(&args);
181    
182    let status = cmd.status()
183        .await
184        .context("Failed to execute claude command")?;
185    
186    if !status.success() {
187        anyhow::bail!("Claude command failed with status: {}", status);
188    }
189    
190    Ok(())
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196
197    #[test]
198    fn test_claude_command_creation() {
199        let cmd = ClaudeCommand {
200            prompt: "test".to_string(),
201            system_prompt: None,
202            append_prompt: None,
203            model: None,
204        };
205        assert_eq!(cmd.prompt, "test");
206    }
207
208    #[test]
209    fn test_build_args_basic() {
210        let cmd = ClaudeCommand {
211            prompt: "Hello, Claude!".to_string(),
212            system_prompt: None,
213            append_prompt: None,
214            model: None,
215        };
216        
217        let args = cmd.build_args();
218        assert_eq!(args[0], "--continue");
219        assert_eq!(args[1], "-p");
220        assert_eq!(args[2], "Hello, Claude!");
221        assert!(args.contains(&"--allowedTools".to_string()));
222        assert!(args.contains(&"Write".to_string()));
223        assert!(args.contains(&"Edit".to_string()));
224    }
225
226    #[test]
227    fn test_build_args_with_system_prompt() {
228        let cmd = ClaudeCommand {
229            prompt: "Test".to_string(),
230            system_prompt: Some("Custom system prompt".to_string()),
231            append_prompt: None,
232            model: None,
233        };
234        
235        let args = cmd.build_args();
236        assert!(args.contains(&"--system-prompt".to_string()));
237        assert!(args.contains(&"Custom system prompt".to_string()));
238    }
239
240    #[test]
241    fn test_build_args_with_append_prompt() {
242        let cmd = ClaudeCommand {
243            prompt: "Test".to_string(),
244            system_prompt: None,
245            append_prompt: Some("Additional instructions".to_string()),
246            model: None,
247        };
248        
249        let args = cmd.build_args();
250        assert!(args.contains(&"--append-system-prompt".to_string()));
251        assert!(args.contains(&"Additional instructions".to_string()));
252    }
253
254    #[test]
255    fn test_build_args_with_model() {
256        let cmd = ClaudeCommand {
257            prompt: "Test".to_string(),
258            system_prompt: None,
259            append_prompt: None,
260            model: Some("claude-3-opus".to_string()),
261        };
262        
263        let args = cmd.build_args();
264        assert!(args.contains(&"--model".to_string()));
265        assert!(args.contains(&"claude-3-opus".to_string()));
266    }
267
268    #[test]
269    fn test_build_args_full() {
270        let cmd = ClaudeCommand {
271            prompt: "Complex test".to_string(),
272            system_prompt: Some("System".to_string()),
273            append_prompt: Some("Append".to_string()),
274            model: Some("claude-3-sonnet".to_string()),
275        };
276        
277        let args = cmd.build_args();
278        assert!(args.contains(&"Complex test".to_string()));
279        assert!(args.contains(&"--system-prompt".to_string()));
280        assert!(args.contains(&"System".to_string()));
281        assert!(args.contains(&"--append-system-prompt".to_string()));
282        assert!(args.contains(&"Append".to_string()));
283        assert!(args.contains(&"--model".to_string()));
284        assert!(args.contains(&"claude-3-sonnet".to_string()));
285    }
286}