1use serde_json::{json, Value};
2
3const REGEX_SIZE_LIMIT: usize = 10 * 1024 * 1024;
4
5use crate::bash_rewrite::footer::add_footer;
6use crate::bash_rewrite::parser::parse;
7use crate::bash_rewrite::RewriteRule;
8use crate::context::AppContext;
9use crate::protocol::{RawRequest, Response};
10
11pub struct GrepRule;
12pub struct RgRule;
13pub struct FindRule;
14pub struct CatRule;
15pub struct CatAppendRule;
16pub struct SedRule;
17pub struct LsRule;
18
19impl RewriteRule for GrepRule {
20 fn name(&self) -> &'static str {
21 "grep"
22 }
23
24 fn matches(&self, command: &str) -> bool {
25 grep_request(command, "grep").is_some()
26 }
27
28 fn rewrite(
29 &self,
30 command: &str,
31 session_id: Option<&str>,
32 ctx: &AppContext,
33 ) -> Result<Response, String> {
34 let params = grep_request(command, "grep").ok_or("not a grep rewrite")?;
35 try_call_and_footer(
36 crate::commands::grep::handle_grep(&request("grep", params, session_id), ctx),
37 "grep",
38 )
39 }
40}
41
42impl RewriteRule for RgRule {
43 fn name(&self) -> &'static str {
44 "rg"
45 }
46
47 fn matches(&self, command: &str) -> bool {
48 grep_request(command, "rg").is_some()
49 }
50
51 fn rewrite(
52 &self,
53 command: &str,
54 session_id: Option<&str>,
55 ctx: &AppContext,
56 ) -> Result<Response, String> {
57 let params = grep_request(command, "rg").ok_or("not an rg rewrite")?;
58 try_call_and_footer(
59 crate::commands::grep::handle_grep(&request("grep", params, session_id), ctx),
60 "grep",
61 )
62 }
63}
64
65impl RewriteRule for FindRule {
66 fn name(&self) -> &'static str {
67 "find"
68 }
69
70 fn matches(&self, command: &str) -> bool {
71 find_request(command).is_some()
72 }
73
74 fn rewrite(
75 &self,
76 command: &str,
77 session_id: Option<&str>,
78 ctx: &AppContext,
79 ) -> Result<Response, String> {
80 let params = find_request(command).ok_or("not a find rewrite")?;
81 try_call_and_footer(
82 crate::commands::glob::handle_glob(&request("glob", params, session_id), ctx),
83 "glob",
84 )
85 }
86}
87
88impl RewriteRule for CatRule {
89 fn name(&self) -> &'static str {
90 "cat"
91 }
92
93 fn matches(&self, command: &str) -> bool {
94 cat_read_request(command).is_some()
95 }
96
97 fn rewrite(
98 &self,
99 command: &str,
100 session_id: Option<&str>,
101 ctx: &AppContext,
102 ) -> Result<Response, String> {
103 let params = cat_read_request(command).ok_or("not a cat rewrite")?;
104 try_call_and_footer(
105 crate::commands::read::handle_read(&request("read", params, session_id), ctx),
106 "read",
107 )
108 }
109}
110
111impl RewriteRule for CatAppendRule {
112 fn name(&self) -> &'static str {
113 "cat_append"
114 }
115
116 fn matches(&self, command: &str) -> bool {
117 append_request(command).is_some()
118 }
119
120 fn rewrite(
121 &self,
122 command: &str,
123 session_id: Option<&str>,
124 ctx: &AppContext,
125 ) -> Result<Response, String> {
126 let params = append_request(command).ok_or("not an append rewrite")?;
127 try_call_and_footer(
128 crate::commands::edit_match::handle_edit_match(
129 &request("edit_match", params, session_id),
130 ctx,
131 ),
132 "edit",
133 )
134 }
135}
136
137impl RewriteRule for SedRule {
138 fn name(&self) -> &'static str {
139 "sed"
140 }
141
142 fn matches(&self, command: &str) -> bool {
143 sed_request(command).is_some()
144 }
145
146 fn rewrite(
147 &self,
148 command: &str,
149 session_id: Option<&str>,
150 ctx: &AppContext,
151 ) -> Result<Response, String> {
152 let params = sed_request(command).ok_or("not a sed rewrite")?;
153 try_call_and_footer(
154 crate::commands::read::handle_read(&request("read", params, session_id), ctx),
155 "read",
156 )
157 }
158}
159
160impl RewriteRule for LsRule {
161 fn name(&self) -> &'static str {
162 "ls"
163 }
164
165 fn matches(&self, command: &str) -> bool {
166 ls_request(command).is_some()
167 }
168
169 fn rewrite(
170 &self,
171 command: &str,
172 session_id: Option<&str>,
173 ctx: &AppContext,
174 ) -> Result<Response, String> {
175 let params = ls_request(command).ok_or("not an ls rewrite")?;
176 try_call_and_footer(
177 crate::commands::read::handle_read(&request("read", params, session_id), ctx),
178 "read",
179 )
180 }
181}
182
183fn request(command: &str, params: Value, session_id: Option<&str>) -> RawRequest {
184 RawRequest {
185 id: "bash_rewrite".to_string(),
186 command: command.to_string(),
187 lsp_hints: None,
188 session_id: session_id.map(str::to_string),
189 params,
190 }
191}
192
193fn try_call_and_footer(response: Response, replacement_tool: &str) -> Result<Response, String> {
199 if !response.success {
200 let message = response
201 .data
202 .get("message")
203 .and_then(Value::as_str)
204 .or_else(|| response.data.get("code").and_then(Value::as_str))
205 .unwrap_or("error");
206 return Err(format!("{} declined: {}", replacement_tool, message));
207 }
208 Ok(call_and_footer(response, replacement_tool))
209}
210
211fn call_and_footer(mut response: Response, replacement_tool: &str) -> Response {
212 let output = response_output(&response.data);
213 let output = add_footer(&output, replacement_tool);
214
215 if let Some(object) = response.data.as_object_mut() {
216 object.insert("output".to_string(), Value::String(output.clone()));
217
218 for key in ["text", "content", "message"] {
219 if object.get(key).is_some_and(Value::is_string) {
220 object.insert(key.to_string(), Value::String(output.clone()));
221 break;
222 }
223 }
224 } else {
225 response.data = json!({ "output": output });
226 }
227
228 response
229}
230
231fn response_output(data: &Value) -> String {
232 if let Some(output) = data.get("output").and_then(Value::as_str) {
233 return output.to_string();
234 }
235 if let Some(text) = data.get("text").and_then(Value::as_str) {
236 return text.to_string();
237 }
238 if let Some(content) = data.get("content").and_then(Value::as_str) {
239 return content.to_string();
240 }
241 if let Some(message) = data.get("message").and_then(Value::as_str) {
242 return message.to_string();
243 }
244 if let Some(entries) = data.get("entries").and_then(Value::as_array) {
245 return entries
246 .iter()
247 .filter_map(Value::as_str)
248 .collect::<Vec<_>>()
249 .join("\n");
250 }
251 serde_json::to_string_pretty(data).unwrap_or_else(|_| data.to_string())
252}
253
254fn grep_request(command: &str, binary: &str) -> Option<Value> {
255 let parsed = parse(command)?;
256 if parsed.appends_to.is_some() || parsed.heredoc.is_some() || parsed.args.first()? != binary {
257 return None;
258 }
259
260 let mut case_sensitive = true;
261 let mut word_match = false;
262 let mut index = 1;
263
264 while let Some(arg) = parsed.args.get(index) {
265 if !arg.starts_with('-') || arg == "-" {
266 break;
267 }
268 for flag in arg[1..].chars() {
269 match flag {
270 'n' | 'r' => {}
271 'i' => case_sensitive = false,
272 'w' => word_match = true,
273 _ => return None,
274 }
275 }
276 index += 1;
277 }
278
279 let pattern = parsed.args.get(index)?.clone();
280 let path = parsed.args.get(index + 1).cloned();
281 if parsed.args.len() > index + 2 {
282 return None;
283 }
284
285 let pattern = if word_match {
286 format!(r"\b(?:{})\b", pattern)
287 } else {
288 pattern
289 };
290
291 if regex::RegexBuilder::new(&pattern)
292 .size_limit(REGEX_SIZE_LIMIT)
293 .build()
294 .is_err()
295 {
296 return None;
297 }
298
299 let mut params = json!({
300 "pattern": pattern,
301 "case_sensitive": case_sensitive,
302 "max_results": 100,
303 });
304 if let Some(path) = path {
305 params["path"] = json!(path);
306 }
307 Some(params)
308}
309
310fn find_request(command: &str) -> Option<Value> {
311 let parsed = parse(command)?;
312 if parsed.appends_to.is_some() || parsed.heredoc.is_some() || parsed.args.first()? != "find" {
313 return None;
314 }
315 if parsed.args.len() != 4 && parsed.args.len() != 6 {
316 return None;
317 }
318
319 let path = parsed.args.get(1)?.clone();
320 let mut name = None;
321 let mut saw_type_file = false;
322 let mut index = 2;
323
324 while index < parsed.args.len() {
325 match parsed.args[index].as_str() {
326 "-name" if name.is_none() && index + 1 < parsed.args.len() => {
327 name = Some(parsed.args[index + 1].clone());
328 index += 2;
329 }
330 "-type" if !saw_type_file && index + 1 < parsed.args.len() => {
331 if parsed.args[index + 1] != "f" {
332 return None;
333 }
334 saw_type_file = true;
335 index += 2;
336 }
337 _ => return None,
338 }
339 }
340
341 let name = name?;
342 let pattern = if path == "." {
343 format!("**/{name}")
344 } else {
345 format!("{}/**/{name}", path.trim_end_matches('/'))
346 };
347
348 Some(json!({ "pattern": pattern }))
349}
350
351fn cat_read_request(command: &str) -> Option<Value> {
352 let parsed = parse(command)?;
353 if parsed.appends_to.is_some() || parsed.heredoc.is_some() {
354 return None;
355 }
356 if parsed.args.len() != 2 || parsed.args.first()? != "cat" {
357 return None;
358 }
359 Some(json!({ "file": parsed.args[1] }))
360}
361
362fn append_request(command: &str) -> Option<Value> {
363 let parsed = parse(command)?;
364 let file = parsed.appends_to.clone()?;
365
366 let append_content = if parsed.args == ["cat"] {
367 parsed.heredoc?
368 } else if parsed.heredoc.is_none()
369 && parsed.args.first().is_some_and(|arg| arg == "echo")
370 && parsed.args.len() >= 2
371 && !parsed.args[1].starts_with('-')
372 {
373 format!("{}\n", parsed.args[1..].join(" "))
374 } else {
375 return None;
376 };
377
378 Some(json!({
379 "op": "append",
380 "file": file,
381 "append_content": append_content,
382 "create_dirs": true,
383 }))
384}
385
386fn sed_request(command: &str) -> Option<Value> {
387 let parsed = parse(command)?;
388 if parsed.appends_to.is_some() || parsed.heredoc.is_some() {
389 return None;
390 }
391 if parsed.args.len() != 4 || parsed.args.first()? != "sed" || parsed.args[1] != "-n" {
392 return None;
393 }
394
395 let range = parsed.args[2].strip_suffix('p')?;
396 let (start, end) = range.split_once(',')?;
397 let start_line = start.parse::<u32>().ok()?;
398 let end_line = end.parse::<u32>().ok()?;
399 if start_line == 0 || end_line < start_line {
400 return None;
401 }
402
403 Some(json!({
404 "file": parsed.args[3],
405 "start_line": start_line,
406 "end_line": end_line,
407 }))
408}
409
410fn ls_request(command: &str) -> Option<Value> {
411 let parsed = parse(command)?;
412 if parsed.appends_to.is_some() || parsed.heredoc.is_some() || parsed.args.first()? != "ls" {
413 return None;
414 }
415
416 let mut path = None;
417 for arg in parsed.args.iter().skip(1) {
418 if let Some(flags) = arg.strip_prefix('-') {
419 if flags.is_empty() {
420 return None;
421 }
422 for flag in flags.chars() {
423 match flag {
424 'R' | 'a' => {}
431 _ => return None,
437 }
438 }
439 } else if path.is_none() {
440 path = Some(arg.clone());
441 } else {
442 return None;
443 }
444 }
445
446 let target = path.clone().unwrap_or_else(|| ".".to_string());
452 if let Ok(metadata) = std::fs::metadata(&target) {
453 if !metadata.is_dir() {
454 return None;
455 }
456 }
457 else if path.is_some() {
461 return None;
462 }
463
464 Some(json!({ "file": target }))
465}