deepseek-rust-cli 1.20.7

A lightweight, high-speed autonomous CLI system agent port of DeepSeek CLI.
Documentation
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 ReadFileTool;
#[async_trait]
impl Tool for ReadFileTool {
    fn name(&self) -> &str {
        "read_local_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 start = args
            .get("start_line")
            .and_then(|v| v.as_u64())
            .map(|v| v as usize);
        let end = args
            .get("end_line")
            .and_then(|v| v.as_u64())
            .map(|v| v as usize);
        tools::file_io::read_local_file(path, start, end).await
    }
}

pub struct WriteFileTool;
#[async_trait]
impl Tool for WriteFileTool {
    fn name(&self) -> &str {
        "write_local_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 content = args
            .get("content")
            .and_then(|v| v.as_str())
            .ok_or_else(|| anyhow::anyhow!("Missing 'content'"))?;
        let p = crate::tools::base::validate_path(path)?;
        let backup = tokio::fs::read(&p).await.ok();
        undo.push(UndoAction {
            r#type: "write".to_string(),
            path: p.to_string_lossy().to_string(),
            backup,
        });
        tools::file_io::write_local_file(p.to_str().unwrap(), content)
            .await
            .map(|_| "File written.".to_string())
    }
}

pub struct ReplaceTextTool;
#[async_trait]
impl Tool for ReplaceTextTool {
    fn name(&self) -> &str {
        "replace_text_in_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 old = args
            .get("old_text")
            .and_then(|v| v.as_str())
            .ok_or_else(|| anyhow::anyhow!("Missing 'old_text'"))?;
        let new = args
            .get("new_text")
            .and_then(|v| v.as_str())
            .ok_or_else(|| anyhow::anyhow!("Missing 'new_text'"))?;
        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::fuzzy_replace_in_file(p.to_str().unwrap(), old, new).await
    }
}

pub struct RegexReplaceTool;
#[async_trait]
impl Tool for RegexReplaceTool {
    fn name(&self) -> &str {
        "regex_replace_in_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 regex_str = args
            .get("regex")
            .and_then(|v| v.as_str())
            .ok_or_else(|| anyhow::anyhow!("Missing 'regex'"))?;
        let replacement = args
            .get("replacement")
            .and_then(|v| v.as_str())
            .ok_or_else(|| anyhow::anyhow!("Missing 'replacement'"))?;

        let p = crate::tools::base::validate_path(path)?;
        let re = regex::Regex::new(regex_str)?;
        let content = tokio::fs::read_to_string(&p).await?;

        let backup = Some(content.as_bytes().to_vec());
        undo.push(UndoAction {
            r#type: "replace".to_string(),
            path: p.to_string_lossy().to_string(),
            backup,
        });

        let new_content = re.replace_all(&content, replacement).to_string();
        tokio::fs::write(&p, new_content).await?;
        Ok("Regex replacement complete.".to_string())
    }
}

pub struct JsonUpdateValueTool;
#[async_trait]
impl Tool for JsonUpdateValueTool {
    fn name(&self) -> &str {
        "json_update_value"
    }
    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 key_path = args
            .get("key_path")
            .and_then(|v| v.as_str())
            .ok_or_else(|| anyhow::anyhow!("Missing 'key_path'"))?;
        let new_value_str = args
            .get("new_value")
            .and_then(|v| v.as_str())
            .ok_or_else(|| anyhow::anyhow!("Missing 'new_value'"))?;

        let p = crate::tools::base::validate_path(path)?;
        let new_val: serde_json::Value = serde_json::from_str(new_value_str)
            .unwrap_or_else(|_| serde_json::Value::String(new_value_str.to_string()));

        let raw_content = tokio::fs::read(&p).await?;
        let mut json_data: serde_json::Value = serde_json::from_slice(&raw_content)?;

        undo.push(UndoAction {
            r#type: "replace".to_string(),
            path: p.to_string_lossy().to_string(),
            backup: Some(raw_content),
        });

        let mut parts = Vec::new();
        let mut current_part = String::new();
        let mut chars = key_path.chars().peekable();
        while let Some(c) = chars.next() {
            if c == '\\' && chars.peek() == Some(&'.') {
                current_part.push('.');
                chars.next();
            } else if c == '.' {
                parts.push(current_part);
                current_part = String::new();
            } else {
                current_part.push(c);
            }
        }
        parts.push(current_part);

        if parts.is_empty() || (parts.len() == 1 && parts[0].is_empty()) {
            return Err(anyhow::anyhow!("Empty key_path"));
        }

        let mut current = &mut json_data;
        for (i, part) in parts.iter().enumerate() {
            if i == parts.len() - 1 {
                if let Some(obj) = current.as_object_mut() {
                    obj.insert(part.clone(), new_val.clone());
                } else {
                    return Err(anyhow::anyhow!("Value at path is not a JSON object"));
                }
            } else {
                if !current.is_object() {
                    *current = serde_json::Value::Object(serde_json::Map::new());
                }
                let obj = current.as_object_mut().unwrap();
                if !obj.contains_key(part) {
                    obj.insert(
                        part.clone(),
                        serde_json::Value::Object(serde_json::Map::new()),
                    );
                }
                current = obj.get_mut(part).unwrap();
            }
        }

        let updated_raw = serde_json::to_vec_pretty(&json_data)?;
        tokio::fs::write(&p, updated_raw).await?;
        Ok(format!("Successfully updated JSON path '{}'.", key_path))
    }
}

pub struct EditFileByLinesTool;
#[async_trait]
impl Tool for EditFileByLinesTool {
    fn name(&self) -> &str {
        "edit_file_by_lines"
    }
    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 edits_val = args
            .get("edits")
            .ok_or_else(|| anyhow::anyhow!("Missing 'edits'"))?;

        let edits: Vec<tools::file_io::LineEdit> = serde_json::from_value(edits_val.clone())?;

        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::edit_file_by_lines(p.to_str().unwrap(), edits).await
    }
}

pub struct ApplyDiffPatchTool;
#[async_trait]
impl Tool for ApplyDiffPatchTool {
    fn name(&self) -> &str {
        "apply_diff_patch"
    }
    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 patch_content = args
            .get("patch_content")
            .and_then(|v| v.as_str())
            .ok_or_else(|| anyhow::anyhow!("Missing 'patch_content'"))?;

        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::apply_diff_patch(p.to_str().unwrap(), patch_content).await
    }
}