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}