vtcode_core/tools/registry/
legacy.rs

1use anyhow::{Context, Result, anyhow};
2use regex::Regex;
3use serde_json::{Value, json};
4use shell_words::split;
5
6use crate::config::constants::tools;
7use crate::config::loader::ConfigManager;
8use crate::tools::grep_search::GrepSearchResult;
9use crate::tools::types::EditInput;
10
11use super::ToolRegistry;
12use super::utils;
13
14impl ToolRegistry {
15    pub async fn read_file(&mut self, args: Value) -> Result<Value> {
16        self.execute_tool(tools::READ_FILE, args).await
17    }
18
19    pub async fn write_file(&mut self, args: Value) -> Result<Value> {
20        self.execute_tool(tools::WRITE_FILE, args).await
21    }
22
23    pub async fn edit_file(&mut self, args: Value) -> Result<Value> {
24        let input: EditInput = serde_json::from_value(args).context("invalid edit_file args")?;
25
26        let read_args = json!({
27            "path": input.path,
28            "max_lines": 1000000
29        });
30
31        let read_result = self.file_ops_tool.read_file(read_args).await?;
32        let current_content = read_result["content"]
33            .as_str()
34            .ok_or_else(|| anyhow!("Failed to read file content"))?;
35
36        let mut replacement_occurred = false;
37        let mut new_content = current_content.to_string();
38
39        if current_content.contains(&input.old_str) {
40            new_content = current_content.replace(&input.old_str, &input.new_str);
41            replacement_occurred = new_content != current_content;
42        }
43
44        if !replacement_occurred {
45            let normalized_content = utils::normalize_whitespace(current_content);
46            let normalized_old_str = utils::normalize_whitespace(&input.old_str);
47
48            if normalized_content.contains(&normalized_old_str) {
49                let old_lines: Vec<&str> = input.old_str.lines().collect();
50                let content_lines: Vec<&str> = current_content.lines().collect();
51
52                for i in 0..=(content_lines.len().saturating_sub(old_lines.len())) {
53                    let window = &content_lines[i..i + old_lines.len()];
54                    if utils::lines_match(window, &old_lines) {
55                        let before = content_lines[..i].join("\n");
56                        let after = content_lines[i + old_lines.len()..].join("\n");
57                        let replacement_lines: Vec<&str> = input.new_str.lines().collect();
58
59                        new_content =
60                            format!("{}\n{}\n{}", before, replacement_lines.join("\n"), after);
61                        replacement_occurred = true;
62                        break;
63                    }
64                }
65            }
66        }
67
68        if !replacement_occurred {
69            let content_preview = if current_content.len() > 500 {
70                format!(
71                    "{}...{}",
72                    &current_content[..250],
73                    &current_content[current_content.len().saturating_sub(250)..]
74                )
75            } else {
76                current_content.to_string()
77            };
78
79            return Err(anyhow!(
80                "Could not find text to replace in file.\n\nExpected to replace:\n{}\n\nFile content preview:\n{}",
81                input.old_str,
82                content_preview
83            ));
84        }
85
86        let write_args = json!({
87            "path": input.path,
88            "content": new_content,
89            "mode": "overwrite"
90        });
91
92        self.file_ops_tool.write_file(write_args).await
93    }
94
95    pub async fn delete_file(&mut self, _args: Value) -> Result<Value> {
96        Err(anyhow!("delete_file not yet implemented in modular system"))
97    }
98
99    pub async fn rp_search(&mut self, args: Value) -> Result<Value> {
100        self.execute_tool(tools::GREP_SEARCH, args).await
101    }
102
103    pub fn last_rp_search_result(&self) -> Option<GrepSearchResult> {
104        self.grep_search.last_result()
105    }
106
107    pub async fn list_files(&mut self, args: Value) -> Result<Value> {
108        self.execute_tool(tools::LIST_FILES, args).await
109    }
110
111    pub async fn run_terminal_cmd(&mut self, args: Value) -> Result<Value> {
112        let cfg = ConfigManager::load()
113            .or_else(|_| ConfigManager::load_from_workspace("."))
114            .or_else(|_| ConfigManager::load_from_file("vtcode.toml"))
115            .map(|cm| cm.config().clone())
116            .unwrap_or_default();
117
118        let mut args = args;
119        if let Some(cmd_str) = args.get("command").and_then(|v| v.as_str()) {
120            let parts = split(cmd_str).context("failed to parse command string")?;
121            if parts.is_empty() {
122                return Err(anyhow!("command cannot be empty"));
123            }
124            if let Some(map) = args.as_object_mut() {
125                map.insert("command".to_string(), json!(parts));
126            }
127        }
128
129        let cmd_text = if let Some(cmd_val) = args.get("command") {
130            if cmd_val.is_array() {
131                cmd_val
132                    .as_array()
133                    .unwrap()
134                    .iter()
135                    .filter_map(|v| v.as_str())
136                    .collect::<Vec<_>>()
137                    .join(" ")
138            } else {
139                cmd_val.as_str().unwrap_or("").to_string()
140            }
141        } else {
142            String::new()
143        };
144
145        let mut deny_regex = cfg.commands.deny_regex.clone();
146        if let Ok(extra) = std::env::var("VTCODE_COMMANDS_DENY_REGEX") {
147            deny_regex.extend(extra.split(',').map(|s| s.trim().to_string()));
148        }
149        for pat in &deny_regex {
150            if Regex::new(pat)
151                .ok()
152                .map(|re| re.is_match(&cmd_text))
153                .unwrap_or(false)
154            {
155                return Err(anyhow!("Command denied by regex policy: {}", pat));
156            }
157        }
158        let mut deny_glob = cfg.commands.deny_glob.clone();
159        if let Ok(extra) = std::env::var("VTCODE_COMMANDS_DENY_GLOB") {
160            deny_glob.extend(extra.split(',').map(|s| s.trim().to_string()));
161        }
162        for pat in &deny_glob {
163            let re = format!("^{}$", regex::escape(pat).replace(r"\\*", ".*"));
164            if Regex::new(&re)
165                .ok()
166                .map(|re| re.is_match(&cmd_text))
167                .unwrap_or(false)
168            {
169                return Err(anyhow!("Command denied by glob policy: {}", pat));
170            }
171        }
172        let mut deny_list = cfg.commands.deny_list.clone();
173        if let Ok(extra) = std::env::var("VTCODE_COMMANDS_DENY_LIST") {
174            deny_list.extend(extra.split(',').map(|s| s.trim().to_string()));
175        }
176        for d in &deny_list {
177            if cmd_text.starts_with(d) {
178                return Err(anyhow!("Command denied by policy: {}", d));
179            }
180        }
181
182        let mut allow_regex = cfg.commands.allow_regex.clone();
183        if let Ok(extra) = std::env::var("VTCODE_COMMANDS_ALLOW_REGEX") {
184            allow_regex.extend(extra.split(',').map(|s| s.trim().to_string()));
185        }
186        let mut allow_glob = cfg.commands.allow_glob.clone();
187        if let Ok(extra) = std::env::var("VTCODE_COMMANDS_ALLOW_GLOB") {
188            allow_glob.extend(extra.split(',').map(|s| s.trim().to_string()));
189        }
190        let mut allow_ok = allow_regex.is_empty() && allow_glob.is_empty();
191        if !allow_ok {
192            if allow_regex.iter().any(|pat| {
193                Regex::new(pat)
194                    .ok()
195                    .map(|re| re.is_match(&cmd_text))
196                    .unwrap_or(false)
197            }) {
198                allow_ok = true;
199            }
200            if !allow_ok
201                && allow_glob.iter().any(|pat| {
202                    let re = format!("^{}$", regex::escape(pat).replace(r"\\*", ".*"));
203                    Regex::new(&re)
204                        .ok()
205                        .map(|re| re.is_match(&cmd_text))
206                        .unwrap_or(false)
207                })
208            {
209                allow_ok = true;
210            }
211        }
212        if !allow_ok {
213            let mut allow_list = cfg.commands.allow_list.clone();
214            if let Ok(extra) = std::env::var("VTCODE_COMMANDS_ALLOW_LIST") {
215                allow_list.extend(extra.split(',').map(|s| s.trim().to_string()));
216            }
217            if !allow_list.is_empty() {
218                allow_ok = allow_list.iter().any(|p| cmd_text.starts_with(p));
219            }
220        }
221        if !allow_ok {
222            return Err(anyhow!("Command not allowed by policy"));
223        }
224
225        if args.get("cwd").is_none() {
226            if let Some(m) = args.as_object_mut() {
227                m.insert(
228                    "cwd".to_string(),
229                    json!(self.workspace_root.display().to_string()),
230                );
231            }
232        }
233
234        if args.get("mode").is_none() {
235            if let Some(m) = args.as_object_mut() {
236                m.insert("mode".to_string(), json!("pty"));
237            }
238        }
239
240        self.execute_tool(tools::RUN_TERMINAL_CMD, args).await
241    }
242}