pub mod run;
use crate::error::Result;
use std::path::Path;
use tokio::process::Command;
use tokio::time::{timeout, Duration};
pub use run::{execute_command, stream_command, validate_command_safety, CommandOptions, CommandResult};
pub const MAX_RESPONSE_LEN: usize = 16000;
pub 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>";
pub fn maybe_truncate(content: &str, truncate_after: Option<usize>) -> String {
let limit = truncate_after.unwrap_or(MAX_RESPONSE_LEN);
if content.len() <= limit {
content.to_string()
} else {
format!("{}{}", &content[..limit], TRUNCATED_MESSAGE)
}
}
pub async fn run_command(
cmd: &str,
timeout_secs: Option<u64>,
truncate_after: Option<usize>,
) -> Result<(i32, String, String)> {
let timeout_duration = Duration::from_secs(timeout_secs.unwrap_or(120));
let result = timeout(timeout_duration, async {
let output = Command::new("sh")
.arg("-c")
.arg(cmd)
.output()
.await?;
let exit_code = output.status.code().unwrap_or(-1);
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
Ok::<(i32, String, String), std::io::Error>((exit_code, stdout, stderr))
}).await;
match result {
Ok(Ok((exit_code, stdout, stderr))) => {
Ok((
exit_code,
maybe_truncate(&stdout, truncate_after),
maybe_truncate(&stderr, truncate_after),
))
}
Ok(Err(e)) => Err(e.into()),
Err(_) => Err(format!("Command '{}' timed out after {} seconds", cmd, timeout_secs.unwrap_or(120)).into()),
}
}
pub fn format_with_line_numbers(content: &str, start_line: usize) -> String {
content
.lines()
.enumerate()
.map(|(i, line)| format!("{:6}\t{}", i + start_line, line))
.collect::<Vec<_>>()
.join("\n")
}
pub fn validate_absolute_path(path: &Path) -> Result<()> {
if !path.is_absolute() {
let suggested_path = Path::new("/").join(path);
return Err(format!(
"The path {} is not an absolute path, it should start with `/`. Maybe you meant {}?",
path.display(),
suggested_path.display()
).into());
}
Ok(())
}
pub fn check_file_exists(path: &Path, operation: &str) -> Result<()> {
match operation {
"create" => {
if path.exists() {
return Err(format!(
"File already exists at: {}. Cannot overwrite files using command `create`.",
path.display()
).into());
}
}
_ => {
if !path.exists() {
return Err(format!(
"The path {} does not exist. Please provide a valid path.",
path.display()
).into());
}
}
}
Ok(())
}
pub fn validate_directory_operation(path: &Path, operation: &str) -> Result<()> {
if path.is_dir() && operation != "view" {
return Err(format!(
"The path {} is a directory and only the `view` command can be used on directories",
path.display()
).into());
}
Ok(())
}
pub fn expand_tabs(content: &str) -> String {
content.replace('\t', " ")
}
pub fn create_edit_snippet(
content: &str,
target_line: usize,
snippet_lines: usize,
) -> String {
let lines: Vec<&str> = content.lines().collect();
let start_line = target_line.saturating_sub(snippet_lines);
let end_line = std::cmp::min(target_line + snippet_lines + 1, lines.len());
lines[start_line..end_line]
.iter()
.enumerate()
.map(|(i, line)| format!("{:6}\t{}", start_line + i + 1, line))
.collect::<Vec<_>>()
.join("\n")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_maybe_truncate() {
let short_content = "Hello, world!";
assert_eq!(maybe_truncate(short_content, Some(20)), short_content);
let long_content = "a".repeat(100);
let truncated = maybe_truncate(&long_content, Some(50));
assert!(truncated.len() > 50);
assert!(truncated.contains(TRUNCATED_MESSAGE));
}
#[test]
fn test_format_with_line_numbers() {
let content = "line1\nline2\nline3";
let formatted = format_with_line_numbers(content, 10);
assert!(formatted.contains(" 10\tline1"));
assert!(formatted.contains(" 11\tline2"));
assert!(formatted.contains(" 12\tline3"));
}
#[test]
fn test_expand_tabs() {
let content = "hello\tworld\t!";
let expanded = expand_tabs(content);
assert_eq!(expanded, "hello world !");
}
}