use std::{collections::HashMap, path::Path};
use anyhow::Result;
use async_trait::async_trait;
use serde_json::Value;
use crate::{agent::types::UndoAction, tools, tools::base::Tool};
pub struct MoveCodeBlockTool;
#[async_trait]
impl Tool for MoveCodeBlockTool {
fn name(&self) -> &str {
"move_code_block"
}
async fn execute(
&self,
args: &HashMap<String, Value>,
undo: &mut Vec<UndoAction>,
_cwd: Option<&Path>,
) -> Result<String> {
let src = args
.get("source_path")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing 'source_path'"))?;
let dst = args
.get("destination_path")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing 'destination_path'"))?;
let pattern = args
.get("block_pattern")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing 'block_pattern'"))?;
let src_p = crate::tools::base::validate_path(src)?;
let dst_p = crate::tools::base::validate_path(dst)?;
let src_backup = tokio::fs::read(&src_p).await.ok();
let dst_backup = tokio::fs::read(&dst_p).await.ok();
undo.push(UndoAction {
r#type: "replace".to_string(),
path: src_p.to_string_lossy().to_string(),
backup: src_backup,
});
undo.push(UndoAction {
r#type: "replace".to_string(),
path: dst_p.to_string_lossy().to_string(),
backup: dst_backup,
});
tools::file_ops::move_code_block(src_p.to_str().unwrap(), dst_p.to_str().unwrap(), pattern)
.await
}
}
pub struct SplitFileTool;
#[async_trait]
impl Tool for SplitFileTool {
fn name(&self) -> &str {
"split_file"
}
async fn execute(
&self,
args: &HashMap<String, Value>,
_undo: &mut Vec<UndoAction>,
_cwd: Option<&Path>,
) -> Result<String> {
let path = args
.get("file_path")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing 'file_path'"))?;
let pattern = args
.get("split_pattern")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing 'split_pattern'"))?;
let prefix = args
.get("output_prefix")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing 'output_prefix'"))?;
tools::file_io::split_file(path, pattern, prefix).await
}
}
pub struct CleanupFileTool;
#[async_trait]
impl Tool for CleanupFileTool {
fn name(&self) -> &str {
"cleanup_file"
}
async fn execute(
&self,
args: &HashMap<String, Value>,
undo: &mut Vec<UndoAction>,
_cwd: Option<&Path>,
) -> Result<String> {
let path = args
.get("file_path")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing 'file_path'"))?;
let p = crate::tools::base::validate_path(path)?;
let backup = tokio::fs::read(&p).await.ok();
undo.push(UndoAction {
r#type: "replace".to_string(),
path: p.to_string_lossy().to_string(),
backup,
});
tools::file_io::cleanup_file(p.to_str().unwrap()).await
}
}
pub struct ProjectCheckpointTool;
#[async_trait]
impl Tool for ProjectCheckpointTool {
fn name(&self) -> &str {
"project_checkpoint"
}
async fn execute(
&self,
args: &HashMap<String, Value>,
_undo: &mut Vec<UndoAction>,
_cwd: Option<&Path>,
) -> Result<String> {
let name = args
.get("name")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing 'name'"))?;
if !name
.chars()
.all(|c| c.is_alphanumeric() || c == '_' || c == '-')
{
anyhow::bail!("Invalid checkpoint name: only alphanumeric, '_' and '-' are allowed");
}
let checkpoint_dir = crate::tools::base::validate_path(".deep/checkpoints")?;
if !checkpoint_dir.exists() {
std::fs::create_dir_all(&checkpoint_dir)?;
}
let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S");
let filename = format!("{}_{}.tar.gz", name, timestamp);
let path = checkpoint_dir.join(&filename);
let output = tokio::process::Command::new("tar")
.args([
"-czf",
path.to_str().unwrap(),
"src",
"Cargo.toml",
"README.md",
])
.output()
.await?;
if output.status.success() {
Ok(format!(
"Project checkpoint '{}' created successfully.",
filename
))
} else {
Err(anyhow::anyhow!(
"Failed to create checkpoint: {}",
String::from_utf8_lossy(&output.stderr)
))
}
}
}
pub struct RestoreCheckpointTool;
#[async_trait]
impl Tool for RestoreCheckpointTool {
fn name(&self) -> &str {
"restore_checkpoint"
}
async fn execute(
&self,
args: &HashMap<String, Value>,
_undo: &mut Vec<UndoAction>,
_cwd: Option<&Path>,
) -> Result<String> {
let name = args
.get("checkpoint_file")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing 'checkpoint_file'"))?;
let checkpoint_dir = crate::tools::base::validate_path(".deep/checkpoints")?;
let path = checkpoint_dir.join(name);
let validated_path = crate::tools::base::validate_path(path.to_str().unwrap())?;
if !validated_path.starts_with(&checkpoint_dir) {
anyhow::bail!("Access to checkpoint file denied: path traversal detected");
}
let output = tokio::process::Command::new("tar")
.args(["-xzf", validated_path.to_str().unwrap()])
.output()
.await?;
if output.status.success() {
Ok(format!("Project restored from checkpoint '{}'.", name))
} else {
Err(anyhow::anyhow!(
"Failed to restore checkpoint: {}",
String::from_utf8_lossy(&output.stderr)
))
}
}
}
pub struct ProjectWideReplaceTool;
#[async_trait]
impl Tool for ProjectWideReplaceTool {
fn name(&self) -> &str {
"project_wide_replace"
}
async fn execute(
&self,
args: &HashMap<String, Value>,
_undo: &mut Vec<UndoAction>,
_cwd: Option<&Path>,
) -> Result<String> {
let old_text = args
.get("old_text")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing 'old_text'"))?;
let new_text = args
.get("new_text")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow::anyhow!("Missing 'new_text'"))?;
let glob_pattern = args
.get("glob")
.and_then(|v| v.as_str())
.unwrap_or("**/*.rs");
let mut count = 0;
let mut file_count = 0;
if let Ok(paths) = glob::glob(glob_pattern) {
for entry in paths.filter_map(|e| e.ok()) {
if entry.is_file() {
let path_str = entry.to_string_lossy().to_string();
if let Ok(validated_path) = crate::tools::base::validate_path(&path_str) {
let path_str_val = validated_path.to_string_lossy();
if !path_str_val.contains("target") && !path_str_val.contains(".git") {
if let Ok(content) = std::fs::read_to_string(&validated_path) {
if content.contains(old_text) {
let new_content = content.replace(old_text, new_text);
std::fs::write(&validated_path, new_content)?;
file_count += 1;
count += content.matches(old_text).count();
}
}
}
}
}
}
}
Ok(format!(
"Replaced {} occurrences in {} files matching '{}'.",
count, file_count, glob_pattern
))
}
}