use crate::core::{Tool, ToolArgs, ToolError, ToolResult};
use crate::tools::old::state::ToolState;
use anyhow::Result;
use std::fs;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
pub struct CreateFileTool {
name: String,
}
impl CreateFileTool {
pub fn new() -> Self {
Self {
name: "create_file".to_string(),
}
}
fn parse_params(&self, args: &ToolArgs) -> Result<serde_json::Value, ToolError> {
if let Some(json_str) = args.get_named_arg("json") {
return serde_json::from_str(json_str).map_err(|e| ToolError::Json(e));
}
if !args.named_args.is_empty() {
return Ok(serde_json::to_value(&args.named_args).map_err(|e| ToolError::Json(e))?);
}
if args.len() >= 2 {
let mut params = serde_json::Map::new();
params.insert(
"path".to_string(),
serde_json::Value::String(args.get_arg(0).unwrap().clone()),
);
params.insert(
"content".to_string(),
serde_json::Value::String(args.get_arg(1).unwrap().clone()),
);
return Ok(serde_json::Value::Object(params));
}
Err(ToolError::InvalidArgs {
message: "Insufficient parameters".to_string(),
})
}
}
impl Tool for CreateFileTool {
fn name(&self) -> &str {
&self.name
}
fn description(&self) -> &str {
"Create a new file with specified content"
}
fn signature(&self) -> &str {
"create_file(path: str, content: str)"
}
fn validate_args(&self, args: &ToolArgs) -> Result<(), ToolError> {
let params = self.parse_params(args)?;
let obj = params.as_object().ok_or_else(|| ToolError::InvalidArgs {
message: "Parameters must be an object".to_string(),
})?;
if !obj.contains_key("path") {
return Err(ToolError::InvalidArgs {
message: "Missing required parameter: path".to_string(),
});
}
if !obj.contains_key("content") {
return Err(ToolError::InvalidArgs {
message: "Missing required parameter: content".to_string(),
});
}
Ok(())
}
fn execute(&mut self, args: &ToolArgs, state: &Arc<Mutex<ToolState>>) -> Result<ToolResult> {
let params = self.parse_params(args)?;
let obj = params
.as_object()
.ok_or_else(|| anyhow::anyhow!("Invalid parameters"))?;
let path_str = obj
.get("path")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Invalid path parameter"))?;
let content = obj
.get("content")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Invalid content parameter"))?;
let path = PathBuf::from(path_str);
if path.exists() {
return Ok(ToolResult::error(format!(
"File already exists: {}",
path.display()
)));
}
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.map_err(|e| anyhow::anyhow!("Failed to create parent directories: {}", e))?;
}
fs::write(&path, content).map_err(|e| anyhow::anyhow!("Failed to create file: {}", e))?;
{
let mut state_guard = state
.lock()
.map_err(|e| anyhow::anyhow!("Failed to lock state: {}", e))?;
state_guard.push_history(format!("Created file: {}", path.display()));
}
Ok(ToolResult::success_with_data(
format!("Successfully created file: {}", path.display()),
serde_json::json!({
"path": path.to_string_lossy(),
"content_length": content.len(),
"lines_created": content.lines().count(),
"created": true
}),
))
}
fn get_parameters_schema(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Full path to the file to create"
},
"content": {
"type": "string",
"description": "Content to write to the file"
}
},
"required": ["path", "content"]
})
}
}
pub struct ReplaceTextTool {
name: String,
}
impl ReplaceTextTool {
pub fn new() -> Self {
Self {
name: "replace_text".to_string(),
}
}
fn parse_params(&self, args: &ToolArgs) -> Result<serde_json::Value, ToolError> {
if let Some(json_str) = args.get_named_arg("json") {
return serde_json::from_str(json_str).map_err(|e| ToolError::Json(e));
}
if !args.named_args.is_empty() {
return Ok(serde_json::to_value(&args.named_args).map_err(|e| ToolError::Json(e))?);
}
if args.len() >= 3 {
let mut params = serde_json::Map::new();
params.insert(
"path".to_string(),
serde_json::Value::String(args.get_arg(0).unwrap().clone()),
);
params.insert(
"old_text".to_string(),
serde_json::Value::String(args.get_arg(1).unwrap().clone()),
);
params.insert(
"new_text".to_string(),
serde_json::Value::String(args.get_arg(2).unwrap().clone()),
);
if args.len() >= 4 {
if let Ok(occurrence) = args.get_arg(3).unwrap().parse::<usize>() {
params.insert(
"occurrence".to_string(),
serde_json::Value::Number(occurrence.into()),
);
}
}
return Ok(serde_json::Value::Object(params));
}
Err(ToolError::InvalidArgs {
message: "Insufficient parameters".to_string(),
})
}
}
impl Tool for ReplaceTextTool {
fn name(&self) -> &str {
&self.name
}
fn description(&self) -> &str {
"Replace specific text in a file"
}
fn signature(&self) -> &str {
"replace_text(path: str, old_text: str, new_text: str, occurrence?: int)"
}
fn validate_args(&self, args: &ToolArgs) -> Result<(), ToolError> {
let params = self.parse_params(args)?;
let obj = params.as_object().ok_or_else(|| ToolError::InvalidArgs {
message: "Parameters must be an object".to_string(),
})?;
if !obj.contains_key("path") {
return Err(ToolError::InvalidArgs {
message: "Missing required parameter: path".to_string(),
});
}
if !obj.contains_key("old_text") {
return Err(ToolError::InvalidArgs {
message: "Missing required parameter: old_text".to_string(),
});
}
if !obj.contains_key("new_text") {
return Err(ToolError::InvalidArgs {
message: "Missing required parameter: new_text".to_string(),
});
}
Ok(())
}
fn execute(&mut self, args: &ToolArgs, state: &Arc<Mutex<ToolState>>) -> Result<ToolResult> {
let params = self.parse_params(args)?;
let obj = params
.as_object()
.ok_or_else(|| anyhow::anyhow!("Invalid parameters"))?;
let path_str = obj
.get("path")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Invalid path parameter"))?;
let old_text = obj
.get("old_text")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Invalid old_text parameter"))?;
let new_text = obj
.get("new_text")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Invalid new_text parameter"))?;
let occurrence = obj
.get("occurrence")
.and_then(|v| v.as_u64())
.map(|v| v as usize);
let path = PathBuf::from(path_str);
if !path.exists() {
return Ok(ToolResult::error(format!(
"File not found: {}",
path.display()
)));
}
let content =
fs::read_to_string(&path).map_err(|e| anyhow::anyhow!("Failed to read file: {}", e))?;
let mut matches = Vec::new();
let mut start = 0;
while let Some(pos) = content[start..].find(old_text) {
matches.push(start + pos);
start += pos + old_text.len();
}
if matches.is_empty() {
return Ok(ToolResult::error(format!("Text not found: '{}'", old_text)));
}
if matches.len() > 1 && occurrence.is_none() {
let mut previews: Vec<String> = Vec::new();
let mut line_start_offsets: Vec<usize> = Vec::new();
let mut acc = 0usize;
for line in content.lines() {
line_start_offsets.push(acc);
acc += line.len() + 1; }
for &pos in matches.iter() {
let mut line_no = 0usize;
for (ln_idx, &off) in line_start_offsets.iter().enumerate() {
if off <= pos {
line_no = ln_idx + 1; } else {
break;
}
}
let line_content = content.lines().nth(line_no.saturating_sub(1)).unwrap_or("");
let preview = format!("{}: {}", line_no, line_content.trim());
previews.push(preview);
if previews.len() >= 5 {
break;
} }
let preview_text = if previews.is_empty() {
"".to_string()
} else {
format!("\nMatches (line: snippet):\n{}", previews.join("\n"))
};
return Ok(ToolResult::error(format!(
"Found {} occurrences of '{}'. Use 'occurrence' parameter to specify which one to replace (1-{}).{}",
matches.len(), old_text, matches.len(), preview_text
)));
}
let selected_pos = if let Some(occ) = occurrence {
if occ == 0 || occ > matches.len() {
return Ok(ToolResult::error(format!(
"Invalid occurrence {}. Found {} matches",
occ,
matches.len()
)));
}
matches[occ - 1]
} else {
matches[0] };
let mut new_content = content;
let start_pos = selected_pos;
let end_pos = start_pos + old_text.len();
new_content.replace_range(start_pos..end_pos, new_text);
fs::write(&path, &new_content)
.map_err(|e| anyhow::anyhow!("Failed to write file: {}", e))?;
{
let mut state_guard = state
.lock()
.map_err(|e| anyhow::anyhow!("Failed to lock state: {}", e))?;
if state_guard.current_file.as_ref() == Some(&path) {
let lines: Vec<String> = new_content.lines().map(|s| s.to_string()).collect();
state_guard.open_file(path.clone(), lines, 100)?;
}
state_guard.push_history(format!("Replaced text in: {}", path.display()));
}
let occurrence_text = occurrence.unwrap_or(1);
let chars_changed = new_text.len() as i64 - old_text.len() as i64;
Ok(ToolResult::success_with_data(
format!(
"Successfully replaced occurrence {} in {}",
occurrence_text,
path.display()
),
serde_json::json!({
"path": path.to_string_lossy(),
"occurrence": occurrence_text,
"old_text": old_text,
"new_text": new_text,
"characters_changed": chars_changed,
"total_matches": matches.len()
}),
))
}
fn get_parameters_schema(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Full path to the file"
},
"old_text": {
"type": "string",
"description": "Exact text to replace"
},
"new_text": {
"type": "string",
"description": "Replacement text"
},
"occurrence": {
"type": "integer",
"description": "Which occurrence to replace (1-based, default: 1 if only one match)",
"minimum": 1
}
},
"required": ["path", "old_text", "new_text"]
})
}
}
pub struct InsertTextTool {
name: String,
}
impl InsertTextTool {
pub fn new() -> Self {
Self {
name: "insert_text".to_string(),
}
}
fn parse_params(&self, args: &ToolArgs) -> Result<serde_json::Value, ToolError> {
if let Some(json_str) = args.get_named_arg("json") {
return serde_json::from_str(json_str).map_err(|e| ToolError::Json(e));
}
if !args.named_args.is_empty() {
return Ok(serde_json::to_value(&args.named_args).map_err(|e| ToolError::Json(e))?);
}
if args.len() >= 3 {
let mut params = serde_json::Map::new();
params.insert(
"path".to_string(),
serde_json::Value::String(args.get_arg(0).unwrap().clone()),
);
if let Ok(line_number) = args.get_arg(1).unwrap().parse::<u64>() {
params.insert(
"line_number".to_string(),
serde_json::Value::Number(line_number.into()),
);
} else {
return Err(ToolError::InvalidArgs {
message: "Invalid line_number - must be a number".to_string(),
});
}
params.insert(
"text".to_string(),
serde_json::Value::String(args.get_arg(2).unwrap().clone()),
);
if args.len() >= 4 {
params.insert(
"position".to_string(),
serde_json::Value::String(args.get_arg(3).unwrap().clone()),
);
}
return Ok(serde_json::Value::Object(params));
}
Err(ToolError::InvalidArgs {
message: "Insufficient parameters".to_string(),
})
}
}
impl Tool for InsertTextTool {
fn name(&self) -> &str {
&self.name
}
fn description(&self) -> &str {
"Insert text at a specific line in a file"
}
fn signature(&self) -> &str {
"insert_text(path: str, line_number: int, text: str, position?: str)"
}
fn validate_args(&self, args: &ToolArgs) -> Result<(), ToolError> {
let params = self.parse_params(args)?;
let obj = params.as_object().ok_or_else(|| ToolError::InvalidArgs {
message: "Parameters must be an object".to_string(),
})?;
if !obj.contains_key("path") {
return Err(ToolError::InvalidArgs {
message: "Missing required parameter: path".to_string(),
});
}
if !obj.contains_key("line_number") {
return Err(ToolError::InvalidArgs {
message: "Missing required parameter: line_number".to_string(),
});
}
if !obj.contains_key("text") {
return Err(ToolError::InvalidArgs {
message: "Missing required parameter: text".to_string(),
});
}
Ok(())
}
fn execute(&mut self, args: &ToolArgs, state: &Arc<Mutex<ToolState>>) -> Result<ToolResult> {
let params = self.parse_params(args)?;
let obj = params
.as_object()
.ok_or_else(|| anyhow::anyhow!("Invalid parameters"))?;
let path_str = obj
.get("path")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Invalid path parameter"))?;
let line_number = {
let v = obj
.get("line_number")
.ok_or_else(|| anyhow::anyhow!("Invalid line_number parameter"))?;
if let Some(n) = v.as_u64() {
n as usize
} else if let Some(s) = v.as_str() {
s.parse::<u64>()
.map_err(|_| anyhow::anyhow!("Invalid line_number parameter"))?
as usize
} else {
return Err(anyhow::anyhow!("Invalid line_number parameter"));
}
};
let text = obj
.get("text")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Invalid text parameter"))?;
let position = obj
.get("position")
.and_then(|v| v.as_str())
.unwrap_or("after_line");
let path = PathBuf::from(path_str);
if !path.exists() {
return Ok(ToolResult::error(format!(
"File not found: {}",
path.display()
)));
}
let content =
fs::read_to_string(&path).map_err(|e| anyhow::anyhow!("Failed to read file: {}", e))?;
let mut lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
if line_number == 0 || line_number > lines.len() + 1 {
return Ok(ToolResult::error(format!(
"Invalid line number {}. File has {} lines",
line_number,
lines.len()
)));
}
let insert_index = match position {
"before_line" => line_number - 1,
"after_line" => std::cmp::min(line_number, lines.len()),
"at_end" => lines.len(),
_ => {
return Ok(ToolResult::error(format!(
"Invalid position '{}'. Use 'before_line', 'after_line', or 'at_end'",
position
)))
}
};
lines.insert(insert_index, text.to_string());
let new_content = lines.join("\n");
if !new_content.is_empty() && !new_content.ends_with('\n') {
fs::write(&path, format!("{}\n", new_content))
.map_err(|e| anyhow::anyhow!("Failed to write file: {}", e))?;
} else {
fs::write(&path, new_content)
.map_err(|e| anyhow::anyhow!("Failed to write file: {}", e))?;
}
{
let mut state_guard = state
.lock()
.map_err(|e| anyhow::anyhow!("Failed to lock state: {}", e))?;
if state_guard.current_file.as_ref() == Some(&path) {
state_guard.open_file(path.clone(), lines, 100)?;
}
state_guard.push_history(format!(
"Inserted text at line {} in: {}",
insert_index + 1,
path.display()
));
}
Ok(ToolResult::success_with_data(
format!(
"Successfully inserted text at line {} in {}",
insert_index + 1,
path.display()
),
serde_json::json!({
"path": path.to_string_lossy(),
"line_number": insert_index + 1,
"position": position,
"text": text,
"lines_added": 1
}),
))
}
fn get_parameters_schema(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Full path to the file"
},
"line_number": {
"type": "integer",
"description": "Line number where to insert text (1-based)",
"minimum": 1
},
"text": {
"type": "string",
"description": "Text to insert"
},
"position": {
"type": "string",
"description": "Where to insert relative to line number",
"enum": ["before_line", "after_line", "at_end"],
"default": "after_line"
}
},
"required": ["path", "line_number", "text"]
})
}
}
pub struct DeleteLineTool {
name: String,
}
impl DeleteLineTool {
pub fn new() -> Self {
Self {
name: "delete_line".to_string(),
}
}
fn parse_params(&self, args: &ToolArgs) -> Result<serde_json::Value, ToolError> {
if let Some(json_str) = args.get_named_arg("json") {
return serde_json::from_str(json_str).map_err(|e| ToolError::Json(e));
}
if !args.named_args.is_empty() {
let mut params = serde_json::Map::new();
for (key, value) in &args.named_args {
if key == "start_line" || key == "end_line" {
let numeric_value =
value.parse::<u64>().map_err(|_| ToolError::InvalidArgs {
message: format!("Invalid {} - must be a number", key),
})?;
params.insert(key.clone(), serde_json::Value::Number(numeric_value.into()));
} else {
params.insert(key.clone(), serde_json::Value::String(value.clone()));
}
}
return Ok(serde_json::Value::Object(params));
}
if args.len() >= 3 {
let mut params = serde_json::Map::new();
params.insert(
"path".to_string(),
serde_json::Value::String(args.get_arg(0).unwrap().clone()),
);
let start_line =
args.get_arg(1)
.unwrap()
.parse::<u64>()
.map_err(|_| ToolError::InvalidArgs {
message: "Invalid start_line - must be a number".to_string(),
})?;
let end_line =
args.get_arg(2)
.unwrap()
.parse::<u64>()
.map_err(|_| ToolError::InvalidArgs {
message: "Invalid end_line - must be a number".to_string(),
})?;
params.insert(
"start_line".to_string(),
serde_json::Value::Number(start_line.into()),
);
params.insert(
"end_line".to_string(),
serde_json::Value::Number(end_line.into()),
);
return Ok(serde_json::Value::Object(params));
}
Err(ToolError::InvalidArgs {
message: "Insufficient parameters".to_string(),
})
}
}
impl Tool for DeleteLineTool {
fn name(&self) -> &str {
&self.name
}
fn description(&self) -> &str {
"Delete a range of lines from a file (inclusive)"
}
fn signature(&self) -> &str {
"delete_line(path: str, start_line: int, end_line: int)"
}
fn validate_args(&self, args: &ToolArgs) -> Result<(), ToolError> {
let params = self.parse_params(args)?;
let obj = params.as_object().ok_or_else(|| ToolError::InvalidArgs {
message: "Parameters must be an object".to_string(),
})?;
if !obj.contains_key("path") {
return Err(ToolError::InvalidArgs {
message: "Missing required parameter: path".to_string(),
});
}
if !obj.contains_key("start_line") {
return Err(ToolError::InvalidArgs {
message: "Missing required parameter: start_line".to_string(),
});
}
if !obj.contains_key("end_line") {
return Err(ToolError::InvalidArgs {
message: "Missing required parameter: end_line".to_string(),
});
}
Ok(())
}
fn execute(&mut self, args: &ToolArgs, state: &Arc<Mutex<ToolState>>) -> Result<ToolResult> {
let params = self.parse_params(args)?;
let obj = params
.as_object()
.ok_or_else(|| anyhow::anyhow!("Invalid parameters"))?;
let path_str = obj
.get("path")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Invalid path parameter"))?;
let start_line_u64 = obj
.get("start_line")
.and_then(|v| v.as_u64())
.ok_or_else(|| anyhow::anyhow!("Invalid start_line parameter"))?;
let end_line_u64 = obj
.get("end_line")
.and_then(|v| v.as_u64())
.ok_or_else(|| anyhow::anyhow!("Invalid end_line parameter"))?;
let start_line = start_line_u64 as usize;
let mut end_line = end_line_u64 as usize;
if start_line == 0 {
return Ok(ToolResult::error(
"Invalid start_line 0. Line numbers are 1-based".to_string(),
));
}
if end_line < start_line {
return Ok(ToolResult::error(format!(
"Invalid line range: {} > {}",
start_line, end_line
)));
}
let path = PathBuf::from(path_str);
if !path.exists() {
return Ok(ToolResult::error(format!(
"File not found: {}",
path.display()
)));
}
let content =
fs::read_to_string(&path).map_err(|e| anyhow::anyhow!("Failed to read file: {}", e))?;
let mut lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
let total_lines = lines.len();
if start_line > total_lines {
return Ok(ToolResult::error(format!(
"Invalid start_line {}. File has {} lines",
start_line, total_lines
)));
}
if end_line > total_lines {
end_line = total_lines; }
let start_idx = start_line - 1;
let end_idx = end_line - 1;
let lines_to_delete = end_idx - start_idx + 1;
lines.drain(start_idx..=end_idx);
let new_content = lines.join("\n");
if !new_content.is_empty() && !new_content.ends_with('\n') {
fs::write(&path, format!("{}\n", new_content))
.map_err(|e| anyhow::anyhow!("Failed to write file: {}", e))?;
} else {
fs::write(&path, new_content)
.map_err(|e| anyhow::anyhow!("Failed to write file: {}", e))?;
}
{
let mut state_guard = state
.lock()
.map_err(|e| anyhow::anyhow!("Failed to lock state: {}", e))?;
if state_guard.current_file.as_ref() == Some(&path) {
state_guard.open_file(path.clone(), lines.clone(), 100)?;
}
state_guard.push_history(format!(
"Deleted lines {}-{} in: {}",
start_line,
end_line,
path.display()
));
}
Ok(ToolResult::success_with_data(
format!(
"Successfully deleted lines {}-{} in {}",
start_line,
end_line,
path.display()
),
serde_json::json!({
"path": path.to_string_lossy(),
"start_line": start_line,
"end_line": end_line,
"lines_deleted": lines_to_delete,
"total_lines": lines.len()
}),
))
}
fn get_parameters_schema(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {
"path": { "type": "string", "description": "Full path to the file" },
"start_line": { "type": "integer", "minimum": 1, "description": "Start line (1-based, inclusive)" },
"end_line": { "type": "integer", "minimum": 1, "description": "End line (1-based, inclusive)" }
},
"required": ["path", "start_line", "end_line"]
})
}
}
pub struct DeleteTextTool {
name: String,
}
impl DeleteTextTool {
pub fn new() -> Self {
Self {
name: "delete_text".to_string(),
}
}
fn parse_params(&self, args: &ToolArgs) -> Result<serde_json::Value, ToolError> {
if let Some(json_str) = args.get_named_arg("json") {
return serde_json::from_str(json_str).map_err(|e| ToolError::Json(e));
}
if !args.named_args.is_empty() {
return Ok(serde_json::to_value(&args.named_args).map_err(|e| ToolError::Json(e))?);
}
if args.len() >= 2 {
let mut params = serde_json::Map::new();
params.insert(
"path".to_string(),
serde_json::Value::String(args.get_arg(0).unwrap().clone()),
);
params.insert(
"text_to_delete".to_string(),
serde_json::Value::String(args.get_arg(1).unwrap().clone()),
);
if args.len() >= 3 {
if let Ok(occurrence) = args.get_arg(2).unwrap().parse::<usize>() {
params.insert(
"occurrence".to_string(),
serde_json::Value::Number(occurrence.into()),
);
}
}
return Ok(serde_json::Value::Object(params));
}
Err(ToolError::InvalidArgs {
message: "Insufficient parameters".to_string(),
})
}
}
impl Tool for DeleteTextTool {
fn name(&self) -> &str {
&self.name
}
fn description(&self) -> &str {
"Delete specific text from a file"
}
fn signature(&self) -> &str {
"delete_text(path: str, text_to_delete: str, occurrence?: int)"
}
fn validate_args(&self, args: &ToolArgs) -> Result<(), ToolError> {
let params = self.parse_params(args)?;
let obj = params.as_object().ok_or_else(|| ToolError::InvalidArgs {
message: "Parameters must be an object".to_string(),
})?;
if !obj.contains_key("path") {
return Err(ToolError::InvalidArgs {
message: "Missing required parameter: path".to_string(),
});
}
if !obj.contains_key("text_to_delete") {
return Err(ToolError::InvalidArgs {
message: "Missing required parameter: text_to_delete".to_string(),
});
}
Ok(())
}
fn execute(&mut self, args: &ToolArgs, state: &Arc<Mutex<ToolState>>) -> Result<ToolResult> {
let params = self.parse_params(args)?;
let obj = params
.as_object()
.ok_or_else(|| anyhow::anyhow!("Invalid parameters"))?;
let path_str = obj
.get("path")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Invalid path parameter"))?;
let text_to_delete = obj
.get("text_to_delete")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Invalid text_to_delete parameter"))?;
let occurrence = obj
.get("occurrence")
.and_then(|v| v.as_u64())
.map(|v| v as usize);
let path = PathBuf::from(path_str);
if !path.exists() {
return Ok(ToolResult::error(format!(
"File not found: {}",
path.display()
)));
}
let content =
fs::read_to_string(&path).map_err(|e| anyhow::anyhow!("Failed to read file: {}", e))?;
let mut matches = Vec::new();
let mut start = 0;
while let Some(pos) = content[start..].find(text_to_delete) {
matches.push(start + pos);
start += pos + text_to_delete.len();
}
if matches.is_empty() {
return Ok(ToolResult::error(format!(
"Text not found: '{}'",
text_to_delete
)));
}
if matches.len() > 1 && occurrence.is_none() {
return Ok(ToolResult::error(format!(
"Found {} occurrences of '{}'. Use 'occurrence' parameter to specify which one to delete (1-{})",
matches.len(), text_to_delete, matches.len()
)));
}
let selected_pos = if let Some(occ) = occurrence {
if occ == 0 || occ > matches.len() {
return Ok(ToolResult::error(format!(
"Invalid occurrence {}. Found {} matches",
occ,
matches.len()
)));
}
matches[occ - 1]
} else {
matches[0] };
let mut new_content = content;
let start_pos = selected_pos;
let end_pos = start_pos + text_to_delete.len();
new_content.replace_range(start_pos..end_pos, "");
fs::write(&path, &new_content)
.map_err(|e| anyhow::anyhow!("Failed to write file: {}", e))?;
{
let mut state_guard = state
.lock()
.map_err(|e| anyhow::anyhow!("Failed to lock state: {}", e))?;
if state_guard.current_file.as_ref() == Some(&path) {
let lines: Vec<String> = new_content.lines().map(|s| s.to_string()).collect();
state_guard.open_file(path.clone(), lines, 100)?;
}
state_guard.push_history(format!("Deleted text from: {}", path.display()));
}
let occurrence_text = occurrence.unwrap_or(1);
Ok(ToolResult::success_with_data(
format!(
"Successfully deleted occurrence {} from {}",
occurrence_text,
path.display()
),
serde_json::json!({
"path": path.to_string_lossy(),
"occurrence": occurrence_text,
"deleted_text": text_to_delete,
"characters_removed": text_to_delete.len(),
"total_matches": matches.len()
}),
))
}
fn get_parameters_schema(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Full path to the file"
},
"text_to_delete": {
"type": "string",
"description": "Exact text to delete"
},
"occurrence": {
"type": "integer",
"description": "Which occurrence to delete (1-based, default: 1 if only one match)",
"minimum": 1
}
},
"required": ["path", "text_to_delete"]
})
}
}
pub struct DeleteFunctionTool {
name: String,
}
impl DeleteFunctionTool {
pub fn new() -> Self {
Self {
name: "delete_function".to_string(),
}
}
fn parse_params(&self, args: &ToolArgs) -> Result<serde_json::Value, ToolError> {
if let Some(json_str) = args.get_named_arg("json") {
return serde_json::from_str(json_str).map_err(|e| ToolError::Json(e));
}
if !args.named_args.is_empty() {
return Ok(serde_json::to_value(&args.named_args).map_err(|e| ToolError::Json(e))?);
}
if args.len() >= 2 {
let mut params = serde_json::Map::new();
params.insert(
"file_name".to_string(),
serde_json::Value::String(args.get_arg(0).unwrap().clone()),
);
params.insert(
"function_name".to_string(),
serde_json::Value::String(args.get_arg(1).unwrap().clone()),
);
return Ok(serde_json::Value::Object(params));
}
Err(ToolError::InvalidArgs {
message: "Insufficient parameters".to_string(),
})
}
fn guess_language(&self, path: &std::path::Path) -> Option<&'static str> {
match path.extension().and_then(|s| s.to_str()) {
Some("rs") => Some("rust"),
_ => None,
}
}
fn delete_rust_function(
content: &str,
func_name: &str,
) -> Result<Option<(String, usize, usize)>> {
use regex::Regex;
let pattern = format!(
r"(?m)^[ \t]*(?:pub[ \t]+)?(?:async[ \t]+)?(?:const[ \t]+)?(?:unsafe[ \t]+)?fn[ \t]+{}\b",
regex::escape(func_name)
);
let re = Regex::new(&pattern).map_err(|e| anyhow::anyhow!("Invalid regex: {}", e))?;
if let Some(m) = re.find(content) {
let start_idx = m.start();
let mut i = m.end();
let bytes = content.as_bytes();
let len = bytes.len();
let mut paren_depth = 0i32; let mut angle_depth = 0i32; let mut in_string = false;
let mut in_char = false;
let mut in_line_comment = false;
let mut in_block_comment = false;
let mut prev = 0u8; let mut body_start = None;
while i < len {
let c = bytes[i];
let next = if i + 1 < len { bytes[i + 1] } else { 0 };
if in_line_comment {
if c == b'\n' {
in_line_comment = false;
}
i += 1;
continue;
}
if in_block_comment {
if c == b'*' && next == b'/' {
in_block_comment = false;
i += 2;
continue;
}
i += 1;
continue;
}
if in_string {
if c == b'\\' {
i += 2;
continue;
}
if c == b'"' {
in_string = false;
}
i += 1;
continue;
}
if in_char {
if c == b'\\' {
i += 2;
continue;
}
if c == b'\'' {
in_char = false;
}
i += 1;
continue;
}
if c == b'/' && next == b'/' {
in_line_comment = true;
i += 2;
continue;
}
if c == b'/' && next == b'*' {
in_block_comment = true;
i += 2;
continue;
}
if c == b'"' {
in_string = true;
i += 1;
continue;
}
if c == b'\'' {
in_char = true;
i += 1;
continue;
}
if c == b'(' {
paren_depth += 1;
i += 1;
continue;
}
if c == b')' {
paren_depth -= 1;
i += 1;
continue;
}
if c == b'<' {
angle_depth += 1;
i += 1;
continue;
}
if c == b'>' {
if prev == b'-' {
i += 1;
continue;
}
if angle_depth > 0 {
angle_depth -= 1;
}
i += 1;
continue;
}
if c == b';' && paren_depth == 0 && angle_depth == 0 {
return Ok(None);
}
if c == b'{' && paren_depth == 0 && angle_depth == 0 {
body_start = Some(i);
break;
}
i += 1;
prev = c;
}
let body_start = if let Some(pos) = body_start {
pos
} else {
return Ok(None);
};
let mut depth = 0i32;
let mut j = body_start;
let mut in_string = false;
let mut in_char = false;
let mut in_line_comment = false;
let mut in_block_comment = false;
while j < len {
let c = bytes[j];
let next = if j + 1 < len { bytes[j + 1] } else { 0 };
if in_line_comment {
if c == b'\n' {
in_line_comment = false;
}
j += 1;
continue;
}
if in_block_comment {
if c == b'*' && next == b'/' {
in_block_comment = false;
j += 2;
continue;
}
j += 1;
continue;
}
if in_string {
if c == b'\\' {
j += 2;
continue;
}
if c == b'"' {
in_string = false;
}
j += 1;
continue;
}
if in_char {
if c == b'\\' {
j += 2;
continue;
}
if c == b'\'' {
in_char = false;
}
j += 1;
continue;
}
if c == b'/' && next == b'/' {
in_line_comment = true;
j += 2;
continue;
}
if c == b'/' && next == b'*' {
in_block_comment = true;
j += 2;
continue;
}
if c == b'"' {
in_string = true;
j += 1;
continue;
}
if c == b'\'' {
in_char = true;
j += 1;
continue;
}
if c == b'{' {
depth += 1;
}
if c == b'}' {
depth -= 1;
if depth == 0 {
let end_idx = j + 1; let before = &content[..start_idx];
let func_block = &content[start_idx..end_idx];
let start_line = before.lines().count() + 1;
let end_line = start_line + func_block.lines().count() - 1;
let lines: Vec<&str> = content.lines().collect();
let mut adj_start_line = start_line;
while adj_start_line > 1 {
let prev_line = lines[adj_start_line - 2].trim_start();
if prev_line.starts_with("#[")
|| prev_line.starts_with("///")
|| prev_line.starts_with("//!")
{
adj_start_line -= 1;
continue;
}
break;
}
let mut new_lines: Vec<&str> = lines.clone();
new_lines.drain(adj_start_line - 1..end_line);
let new_content = new_lines.join("\n");
return Ok(Some((new_content, adj_start_line, end_line)));
}
}
j += 1;
}
Ok(None)
} else {
Ok(None)
}
}
}
impl Tool for DeleteFunctionTool {
fn name(&self) -> &str {
&self.name
}
fn description(&self) -> &str {
"Delete a function definition by name. Currently supports Rust (.rs). For unsupported languages, suggests using delete_line or delete_text."
}
fn signature(&self) -> &str {
"delete_function(file_name: str, function_name: str)"
}
fn validate_args(&self, args: &ToolArgs) -> Result<(), ToolError> {
let params = self.parse_params(args)?;
let obj = params.as_object().ok_or_else(|| ToolError::InvalidArgs {
message: "Parameters must be an object".to_string(),
})?;
if !obj.contains_key("file_name") && !obj.contains_key("path") {
return Err(ToolError::InvalidArgs {
message: "Missing required parameter: file_name".to_string(),
});
}
if !obj.contains_key("function_name") {
return Err(ToolError::InvalidArgs {
message: "Missing required parameter: function_name".to_string(),
});
}
Ok(())
}
fn execute(&mut self, args: &ToolArgs, state: &Arc<Mutex<ToolState>>) -> Result<ToolResult> {
let params = self.parse_params(args)?;
let obj = params
.as_object()
.ok_or_else(|| anyhow::anyhow!("Invalid parameters"))?;
let file_name = obj
.get("file_name")
.and_then(|v| v.as_str())
.or_else(|| obj.get("path").and_then(|v| v.as_str()))
.ok_or_else(|| anyhow::anyhow!("Invalid file_name parameter"))?;
let function_name = obj
.get("function_name")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Invalid function_name parameter"))?;
let path = PathBuf::from(file_name);
if !path.exists() {
return Ok(ToolResult::error(format!(
"File not found: {}",
path.display()
)));
}
let lang = self.guess_language(&path);
if lang != Some("rust") {
return Ok(ToolResult::error_with_data(
format!("delete_function is not implemented yet for this file type. Please use delete_line or delete_text."),
serde_json::json!({
"file_name": path.to_string_lossy(),
"function_name": function_name,
"language": path.extension().and_then(|s| s.to_str()).unwrap_or("unknown"),
"supported_languages": ["rs"],
"suggestions": [
"Use delete_line with the function's line range",
"Use delete_text to remove the function body manually",
"Consider adding support for this language in delete_function"
]
})
));
}
let content =
fs::read_to_string(&path).map_err(|e| anyhow::anyhow!("Failed to read file: {}", e))?;
match Self::delete_rust_function(&content, function_name)? {
Some((new_content, start_line, end_line)) => {
fs::write(
&path,
if !new_content.is_empty() && !new_content.ends_with('\n') {
format!("{}\n", new_content)
} else {
new_content.clone()
},
)
.map_err(|e| anyhow::anyhow!("Failed to write file: {}", e))?;
{
let mut state_guard = state
.lock()
.map_err(|e| anyhow::anyhow!("Failed to lock state: {}", e))?;
let lines: Vec<String> = new_content.lines().map(|s| s.to_string()).collect();
if state_guard.current_file.as_ref() == Some(&path) {
state_guard.open_file(path.clone(), lines.clone(), 100)?;
}
state_guard.push_history(format!(
"Deleted function '{}' (lines {}-{}) in: {}",
function_name,
start_line,
end_line,
path.display()
));
}
Ok(ToolResult::success_with_data(
format!(
"Successfully deleted function '{}' from {}",
function_name,
path.display()
),
serde_json::json!({
"path": path.to_string_lossy(),
"function_name": function_name,
"start_line": start_line,
"end_line": end_line,
"lines_deleted": (end_line - start_line + 1)
}),
))
}
None => Ok(ToolResult::error_with_data(
format!(
"Function '{}' not found in {}",
function_name,
path.display()
),
serde_json::json!({
"path": path.to_string_lossy(),
"function_name": function_name,
"suggestions": [
"Check the exact function name (case-sensitive)",
"Ensure it's a function with a body (trait methods without bodies are ignored)",
"Use search_dir/search_file to locate the function",
"As a fallback, use delete_line with the function's line range"
]
}),
)),
}
}
fn get_parameters_schema(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {
"file_name": {"type": "string", "description": "Path to the source file (currently only .rs supported)"},
"function_name": {"type": "string", "description": "Name of the function to delete"}
},
"required": ["file_name", "function_name"]
})
}
}
pub struct OverwriteFileTool {
name: String,
}
impl OverwriteFileTool {
pub fn new() -> Self {
Self {
name: "overwrite_file".to_string(),
}
}
fn parse_params(&self, args: &ToolArgs) -> Result<serde_json::Value, ToolError> {
if let Some(json_str) = args.get_named_arg("json") {
return serde_json::from_str(json_str).map_err(|e| ToolError::Json(e));
}
if !args.named_args.is_empty() {
return Ok(serde_json::to_value(&args.named_args).map_err(|e| ToolError::Json(e))?);
}
if args.len() >= 2 {
let mut params = serde_json::Map::new();
params.insert(
"path".to_string(),
serde_json::Value::String(args.get_arg(0).unwrap().clone()),
);
params.insert(
"content".to_string(),
serde_json::Value::String(args.get_arg(1).unwrap().clone()),
);
return Ok(serde_json::Value::Object(params));
}
Err(ToolError::InvalidArgs {
message: "Insufficient parameters".to_string(),
})
}
}
impl Tool for OverwriteFileTool {
fn name(&self) -> &str {
&self.name
}
fn description(&self) -> &str {
"Overwrite entire file with new content"
}
fn signature(&self) -> &str {
"overwrite_file(path: str, content: str)"
}
fn validate_args(&self, args: &ToolArgs) -> Result<(), ToolError> {
let params = self.parse_params(args)?;
let obj = params.as_object().ok_or_else(|| ToolError::InvalidArgs {
message: "Parameters must be an object".to_string(),
})?;
if !obj.contains_key("path") {
return Err(ToolError::InvalidArgs {
message: "Missing required parameter: path".to_string(),
});
}
if !obj.contains_key("content") {
return Err(ToolError::InvalidArgs {
message: "Missing required parameter: content".to_string(),
});
}
Ok(())
}
fn execute(&mut self, args: &ToolArgs, state: &Arc<Mutex<ToolState>>) -> Result<ToolResult> {
let params = self.parse_params(args)?;
let obj = params
.as_object()
.ok_or_else(|| anyhow::anyhow!("Invalid parameters"))?;
let path_str = obj
.get("path")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Invalid path parameter"))?;
let content = obj
.get("content")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Invalid content parameter"))?;
let path = PathBuf::from(path_str);
if !path.exists() {
return Ok(ToolResult::error(format!(
"File not found: {}",
path.display()
)));
}
let original_content =
fs::read_to_string(&path).map_err(|e| anyhow::anyhow!("Failed to read file: {}", e))?;
let original_lines = original_content.lines().count();
fs::write(&path, content).map_err(|e| anyhow::anyhow!("Failed to write file: {}", e))?;
{
let mut state_guard = state
.lock()
.map_err(|e| anyhow::anyhow!("Failed to lock state: {}", e))?;
if state_guard.current_file.as_ref() == Some(&path) {
let lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
state_guard.open_file(path.clone(), lines, 100)?;
}
state_guard.push_history(format!("Overwritten file: {}", path.display()));
}
let new_lines = content.lines().count();
Ok(ToolResult::success_with_data(
format!("Successfully overwritten file: {}", path.display()),
serde_json::json!({
"path": path.to_string_lossy(),
"content_length": content.len(),
"lines_original": original_lines,
"lines_new": new_lines,
"lines_changed": new_lines as i64 - original_lines as i64,
"overwritten": true
}),
))
}
fn get_parameters_schema(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Full path to the file to overwrite"
},
"content": {
"type": "string",
"description": "New content for the file"
}
},
"required": ["path", "content"]
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_create_file_tool() {
let temp_dir = TempDir::new().unwrap();
let mut tool = CreateFileTool::new();
let state = Arc::new(Mutex::new(ToolState::new()));
let test_file = temp_dir.path().join("test_file.txt");
let args = ToolArgs::with_named_args(
vec![],
vec![
("path".to_string(), test_file.to_string_lossy().to_string()),
("content".to_string(), "Hello, World!".to_string()),
]
.into_iter()
.collect(),
);
let result = tool.execute(&args, &state).unwrap();
assert!(result.success);
assert!(test_file.exists());
let content = fs::read_to_string(&test_file).unwrap();
assert_eq!(content, "Hello, World!");
}
#[test]
fn test_replace_text_tool() {
let temp_dir = TempDir::new().unwrap();
let test_file = temp_dir.path().join("test_replace.txt");
fs::write(&test_file, "Hello, World!\nThis is a test.").unwrap();
let mut tool = ReplaceTextTool::new();
let state = Arc::new(Mutex::new(ToolState::new()));
let args = ToolArgs::with_named_args(
vec![],
vec![
("path".to_string(), test_file.to_string_lossy().to_string()),
("old_text".to_string(), "World".to_string()),
("new_text".to_string(), "Rust".to_string()),
]
.into_iter()
.collect(),
);
let result = tool.execute(&args, &state).unwrap();
assert!(result.success);
let content = fs::read_to_string(&test_file).unwrap();
assert_eq!(content, "Hello, Rust!\nThis is a test.");
}
#[test]
fn test_insert_text_tool() {
let temp_dir = TempDir::new().unwrap();
let test_file = temp_dir.path().join("test_insert.txt");
fs::write(&test_file, "Line 1\nLine 3").unwrap();
let mut tool = InsertTextTool::new();
let state = Arc::new(Mutex::new(ToolState::new()));
let args = ToolArgs::with_named_args(
vec![],
vec![
("path".to_string(), test_file.to_string_lossy().to_string()),
("line_number".to_string(), "2".to_string()),
("text".to_string(), "Line 2".to_string()),
("position".to_string(), "before_line".to_string()),
]
.into_iter()
.collect(),
);
let result = tool.execute(&args, &state).unwrap();
assert!(result.success);
let content = fs::read_to_string(&test_file).unwrap();
assert_eq!(content, "Line 1\nLine 2\nLine 3\n");
}
#[test]
fn test_delete_text_tool() {
let temp_dir = TempDir::new().unwrap();
let test_file = temp_dir.path().join("test_delete.txt");
fs::write(&test_file, "Hello, World!\nThis is a test.").unwrap();
let mut tool = DeleteTextTool::new();
let state = Arc::new(Mutex::new(ToolState::new()));
let args = ToolArgs::with_named_args(
vec![],
vec![
("path".to_string(), test_file.to_string_lossy().to_string()),
("text_to_delete".to_string(), ", World".to_string()),
]
.into_iter()
.collect(),
);
let result = tool.execute(&args, &state).unwrap();
assert!(result.success);
let content = fs::read_to_string(&test_file).unwrap();
assert_eq!(content, "Hello!\nThis is a test.");
}
#[test]
fn test_overwrite_file_tool() {
let temp_dir = TempDir::new().unwrap();
let test_file = temp_dir.path().join("test_overwrite.txt");
fs::write(&test_file, "Original content").unwrap();
let mut tool = OverwriteFileTool::new();
let state = Arc::new(Mutex::new(ToolState::new()));
let args = ToolArgs::with_named_args(
vec![],
vec![
("path".to_string(), test_file.to_string_lossy().to_string()),
(
"content".to_string(),
"New content\nWith multiple lines".to_string(),
),
]
.into_iter()
.collect(),
);
let result = tool.execute(&args, &state).unwrap();
assert!(result.success);
let content = fs::read_to_string(&test_file).unwrap();
assert_eq!(content, "New content\nWith multiple lines");
}
#[test]
fn test_delete_line_tool_basic_range() {
let temp_dir = TempDir::new().unwrap();
let test_file = temp_dir.path().join("test_delete_line.txt");
fs::write(&test_file, "a\nb\nc\nd\ne\n").unwrap();
let mut tool = DeleteLineTool::new();
let state = Arc::new(Mutex::new(ToolState::new()));
let args = ToolArgs::with_named_args(
vec![],
vec![
("path".to_string(), test_file.to_string_lossy().to_string()),
("start_line".to_string(), "2".to_string()),
("end_line".to_string(), "4".to_string()),
]
.into_iter()
.collect(),
);
let result = tool.execute(&args, &state).unwrap();
assert!(result.success, "{}", result.message);
let content = fs::read_to_string(&test_file).unwrap();
assert_eq!(content, "a\ne\n");
}
#[test]
fn test_delete_function_tool_rust() {
let temp_dir = TempDir::new().unwrap();
let test_file = temp_dir.path().join("test_mod.rs");
let content = r#"// Simple Rust module
pub fn keep() {}
pub fn target(a: i32) -> i32 {
let x = a + 1;
x
}
pub fn keep2() {
println!("ok");
}
"#;
fs::write(&test_file, content).unwrap();
let mut tool = DeleteFunctionTool::new();
let state = Arc::new(Mutex::new(ToolState::new()));
let args = ToolArgs::with_named_args(
vec![],
vec![
(
"file_name".to_string(),
test_file.to_string_lossy().to_string(),
),
("function_name".to_string(), "target".to_string()),
]
.into_iter()
.collect(),
);
let result = tool.execute(&args, &state).unwrap();
assert!(result.success, "{}", result.message);
let updated = fs::read_to_string(&test_file).unwrap();
assert!(!updated.contains("fn target("));
assert!(updated.contains("fn keep()"));
assert!(updated.contains("fn keep2()"));
}
}