1pub mod run;
4
5use crate::error::Result;
6use std::path::Path;
7use tokio::process::Command;
8use tokio::time::{timeout, Duration};
9
10pub use run::{
11 execute_command, stream_command, validate_command_safety, CommandOptions, CommandResult,
12};
13
14pub const MAX_RESPONSE_LEN: usize = 16000;
16
17pub const TRUNCATED_MESSAGE: &str = "<response clipped><NOTE>To save on context only part of this file has been shown to you. You should retry this tool after you have searched inside the file with `grep -n` in order to find the line numbers of what you are looking for.</NOTE>";
19
20pub fn maybe_truncate(content: &str, truncate_after: Option<usize>) -> String {
22 let limit = truncate_after.unwrap_or(MAX_RESPONSE_LEN);
23 if content.len() <= limit {
24 content.to_string()
25 } else {
26 format!("{}{}", &content[..limit], TRUNCATED_MESSAGE)
27 }
28}
29
30pub async fn run_command(
32 cmd: &str,
33 timeout_secs: Option<u64>,
34 truncate_after: Option<usize>,
35) -> Result<(i32, String, String)> {
36 let timeout_duration = Duration::from_secs(timeout_secs.unwrap_or(120));
37
38 let result = timeout(timeout_duration, async {
39 let output = Command::new("sh").arg("-c").arg(cmd).output().await?;
40
41 let exit_code = output.status.code().unwrap_or(-1);
42 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
43 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
44
45 Ok::<(i32, String, String), std::io::Error>((exit_code, stdout, stderr))
46 })
47 .await;
48
49 match result {
50 Ok(Ok((exit_code, stdout, stderr))) => Ok((
51 exit_code,
52 maybe_truncate(&stdout, truncate_after),
53 maybe_truncate(&stderr, truncate_after),
54 )),
55 Ok(Err(e)) => Err(e.into()),
56 Err(_) => Err(format!(
57 "Command '{}' timed out after {} seconds",
58 cmd,
59 timeout_secs.unwrap_or(120)
60 )
61 .into()),
62 }
63}
64
65pub fn format_with_line_numbers(content: &str, start_line: usize) -> String {
67 content
68 .lines()
69 .enumerate()
70 .map(|(i, line)| format!("{:6}\t{}", i + start_line, line))
71 .collect::<Vec<_>>()
72 .join("\n")
73}
74
75pub fn validate_absolute_path(path: &Path) -> Result<()> {
77 if !path.is_absolute() {
78 let suggested_path = Path::new("/").join(path);
79 return Err(format!(
80 "The path {} is not an absolute path, it should start with `/`. Maybe you meant {}?",
81 path.display(),
82 suggested_path.display()
83 )
84 .into());
85 }
86 Ok(())
87}
88
89pub fn check_file_exists(path: &Path, operation: &str) -> Result<()> {
91 match operation {
92 "create" => {
93 if path.exists() {
94 return Err(format!(
95 "File already exists at: {}. Cannot overwrite files using command `create`.",
96 path.display()
97 )
98 .into());
99 }
100 }
101 _ => {
102 if !path.exists() {
103 return Err(format!(
104 "The path {} does not exist. Please provide a valid path.",
105 path.display()
106 )
107 .into());
108 }
109 }
110 }
111 Ok(())
112}
113
114pub fn validate_directory_operation(path: &Path, operation: &str) -> Result<()> {
116 if path.is_dir() && operation != "view" {
117 return Err(format!(
118 "The path {} is a directory and only the `view` command can be used on directories",
119 path.display()
120 )
121 .into());
122 }
123 Ok(())
124}
125
126pub fn expand_tabs(content: &str) -> String {
128 content.replace('\t', " ")
129}
130
131pub fn create_edit_snippet(content: &str, target_line: usize, snippet_lines: usize) -> String {
133 let lines: Vec<&str> = content.lines().collect();
134 let start_line = target_line.saturating_sub(snippet_lines);
135 let end_line = std::cmp::min(target_line + snippet_lines + 1, lines.len());
136
137 lines[start_line..end_line]
138 .iter()
139 .enumerate()
140 .map(|(i, line)| format!("{:6}\t{}", start_line + i + 1, line))
141 .collect::<Vec<_>>()
142 .join("\n")
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148
149 #[test]
150 fn test_maybe_truncate() {
151 let short_content = "Hello, world!";
152 assert_eq!(maybe_truncate(short_content, Some(20)), short_content);
153
154 let long_content = "a".repeat(100);
155 let truncated = maybe_truncate(&long_content, Some(50));
156 assert!(truncated.len() > 50);
157 assert!(truncated.contains(TRUNCATED_MESSAGE));
158 }
159
160 #[test]
161 fn test_format_with_line_numbers() {
162 let content = "line1\nline2\nline3";
163 let formatted = format_with_line_numbers(content, 10);
164 assert!(formatted.contains(" 10\tline1"));
165 assert!(formatted.contains(" 11\tline2"));
166 assert!(formatted.contains(" 12\tline3"));
167 }
168
169 #[test]
170 fn test_expand_tabs() {
171 let content = "hello\tworld\t!";
172 let expanded = expand_tabs(content);
173 assert_eq!(expanded, "hello world !");
174 }
175}