1use async_trait::async_trait;
2use limit_agent::error::AgentError;
3use limit_agent::Tool;
4use regex::Regex;
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7use std::path::Path;
8use std::process::Command;
9
10const GREP_MAX_RESULTS: usize = 1000;
11const GREP_CONTEXT_LINES: usize = 3;
12
13#[derive(Debug, Clone, Deserialize, Serialize)]
14pub struct Position {
15 pub line: u32,
16 pub character: u32,
17}
18pub struct GrepTool;
19
20impl GrepTool {
21 pub fn new() -> Self {
22 GrepTool
23 }
24}
25
26impl Default for GrepTool {
27 fn default() -> Self {
28 Self::new()
29 }
30}
31
32#[async_trait]
33impl Tool for GrepTool {
34 fn name(&self) -> &str {
35 "grep"
36 }
37
38 async fn execute(&self, args: Value) -> Result<Value, AgentError> {
39 let pattern: String = serde_json::from_value(args["pattern"].clone())
40 .map_err(|e| AgentError::ToolError(format!("Invalid pattern argument: {}", e)))?;
41
42 if pattern.trim().is_empty() {
43 return Err(AgentError::ToolError(
44 "pattern argument cannot be empty".to_string(),
45 ));
46 }
47
48 Regex::new(&pattern)
50 .map_err(|e| AgentError::ToolError(format!("Invalid regex pattern: {}", e)))?;
51
52 let path = args.get("path").and_then(|v| v.as_str()).unwrap_or(".");
53
54 if !Path::new(path).exists() {
56 return Err(AgentError::ToolError(format!("Path not found: {}", path)));
57 }
58
59 let mut cmd = Command::new("grep");
61 cmd.arg("-r")
62 .arg("-n")
63 .arg("-I") .arg("--color=never")
65 .args(["-C", &GREP_CONTEXT_LINES.to_string()])
66 .arg(&pattern)
67 .arg(path);
68
69 let output = cmd
70 .output()
71 .map_err(|e| AgentError::ToolError(format!("Failed to execute grep: {}", e)))?;
72
73 if !output.status.success() {
74 let stderr = String::from_utf8_lossy(&output.stderr);
76 if !stderr.is_empty() && !stderr.contains("No such file") {
77 return Err(AgentError::ToolError(format!("grep failed: {}", stderr)));
78 }
79 }
80
81 let stdout = String::from_utf8_lossy(&output.stdout);
82 let lines: Vec<&str> = stdout.lines().collect();
83
84 let limited_lines = if lines.len() > GREP_MAX_RESULTS {
86 lines[..GREP_MAX_RESULTS].to_vec()
87 } else {
88 lines
89 };
90
91 let mut matches = Vec::new();
93 for line in limited_lines {
94 if let Some((rest, content)) = line.split_once(':') {
96 if let Some((file_path, line_number)) = rest.split_once(':') {
97 if let Ok(line_num) = line_number.parse::<usize>() {
98 matches.push(serde_json::json!({
99 "file": file_path,
100 "line": line_num,
101 "content": content
102 }));
103 }
104 }
105 }
106 }
107
108 Ok(serde_json::json!({
109 "matches": matches,
110 "count": matches.len(),
111 "pattern": pattern
112 }))
113 }
114}
115
116pub struct AstGrepTool;
117
118impl AstGrepTool {
119 pub fn new() -> Self {
120 AstGrepTool
121 }
122
123 fn get_language_support(lang: &str) -> Result<&'static str, AgentError> {
124 match lang.to_lowercase().as_str() {
125 "rust" | "rs" => Ok("rust"),
126 "typescript" | "ts" | "tsx" => Ok("typescript"),
127 "python" | "py" => Ok("python"),
128 _ => Err(AgentError::ToolError(format!(
129 "Unsupported language: {}. Supported: rust, typescript, python",
130 lang
131 ))),
132 }
133 }
134}
135
136impl Default for AstGrepTool {
137 fn default() -> Self {
138 Self::new()
139 }
140}
141
142#[async_trait]
143impl Tool for AstGrepTool {
144 fn name(&self) -> &str {
145 "ast_grep"
146 }
147
148 async fn execute(&self, args: Value) -> Result<Value, AgentError> {
149 let pattern: String = serde_json::from_value(args["pattern"].clone())
150 .map_err(|e| AgentError::ToolError(format!("Invalid pattern argument: {}", e)))?;
151
152 if pattern.trim().is_empty() {
153 return Err(AgentError::ToolError(
154 "pattern argument cannot be empty".to_string(),
155 ));
156 }
157
158 let language: String = serde_json::from_value(args["language"].clone())
159 .map_err(|e| AgentError::ToolError(format!("Invalid language argument: {}", e)))?;
160
161 let lang = Self::get_language_support(&language)?;
162
163 let path = args.get("path").and_then(|v| v.as_str()).unwrap_or(".");
164
165 if !Path::new(path).exists() {
167 return Err(AgentError::ToolError(format!("Path not found: {}", path)));
168 }
169
170 let check_result = Command::new("ast-grep").arg("--version").output();
172
173 match check_result {
174 Ok(output) if output.status.success() => {}
175 _ => {
176 return Err(AgentError::ToolError(
177 "ast-grep not found in PATH. Please install ast-grep CLI tool.".to_string(),
178 ));
179 }
180 }
181
182 let mut cmd = Command::new("ast-grep");
184 cmd.arg("run")
185 .arg("--json")
186 .args(["--lang", lang])
187 .arg(&pattern)
188 .arg(path);
189
190 let output = cmd
191 .output()
192 .map_err(|e| AgentError::ToolError(format!("Failed to execute ast-grep: {}", e)))?;
193
194 if !output.status.success() {
195 let stderr = String::from_utf8_lossy(&output.stderr);
196 return Err(AgentError::ToolError(format!(
197 "ast-grep failed: {}",
198 stderr
199 )));
200 }
201
202 let stdout = String::from_utf8_lossy(&output.stdout);
203
204 if stdout.trim().is_empty() {
206 return Ok(serde_json::json!({
207 "matches": [],
208 "count": 0,
209 "pattern": pattern,
210 "language": language
211 }));
212 }
213
214 let mut matches = Vec::new();
216 for line in stdout.lines() {
217 if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(line) {
218 matches.push(json_value);
219 }
220 }
221
222 Ok(serde_json::json!({
223 "matches": matches,
224 "count": matches.len(),
225 "pattern": pattern,
226 "language": language
227 }))
228 }
229}
230
231pub struct LspTool;
232
233impl LspTool {
234 pub fn new() -> Self {
235 LspTool
236 }
237
238 fn get_lsp_server(file_path: &Path) -> Result<String, AgentError> {
239 let extension = file_path
240 .extension()
241 .and_then(|ext| ext.to_str())
242 .unwrap_or("");
243
244 match extension {
245 "rs" => Ok("rust-analyzer".to_string()),
246 "ts" | "tsx" | "js" | "jsx" => Ok("typescript-language-server".to_string()),
247 "py" => Ok("pylsp".to_string()),
248 _ => Err(AgentError::ToolError(format!(
249 "Unsupported file extension: {}. Supported: rs, ts, tsx, js, jsx, py",
250 extension
251 ))),
252 }
253 }
254
255 fn check_lsp_server_available(server_name: &str) -> Result<(), AgentError> {
256 let result = Command::new(server_name).arg("--version").output();
257
258 match result {
259 Ok(output) if output.status.success() => Ok(()),
260 Ok(_) => Err(AgentError::ToolError(format!(
261 "LSP server {} failed to execute",
262 server_name
263 ))),
264 Err(_) => Err(AgentError::ToolError(format!(
265 "LSP server {} not found in PATH. Please install it to use LSP features.",
266 server_name
267 ))),
268 }
269 }
270}
271
272impl Default for LspTool {
273 fn default() -> Self {
274 Self::new()
275 }
276}
277
278#[async_trait]
279impl Tool for LspTool {
280 fn name(&self) -> &str {
281 "lsp"
282 }
283
284 async fn execute(&self, args: Value) -> Result<Value, AgentError> {
285 let command: String = serde_json::from_value(args["command"].clone())
286 .map_err(|e| AgentError::ToolError(format!("Invalid command argument: {}", e)))?;
287
288 match command.as_str() {
290 "goto_definition" | "find_references" => {}
291 _ => {
292 return Err(AgentError::ToolError(format!(
293 "Unsupported LSP command: {}. Supported: goto_definition, find_references",
294 command
295 )));
296 }
297 }
298
299 let file_path: String = serde_json::from_value(args["file_path"].clone())
300 .map_err(|e| AgentError::ToolError(format!("Invalid file_path argument: {}", e)))?;
301
302 if !Path::new(&file_path).exists() {
303 return Err(AgentError::ToolError(format!(
304 "File not found: {}",
305 file_path
306 )));
307 }
308
309 let position: Position = serde_json::from_value(args["position"].clone())
310 .map_err(|e| AgentError::ToolError(format!("Invalid position argument: {}", e)))?;
311 let lsp_server = Self::get_lsp_server(Path::new(&file_path))?;
312 Self::check_lsp_server_available(&lsp_server)?;
313
314 match command.as_str() {
321 "goto_definition" => Ok(serde_json::json!({
322 "command": command,
323 "file_path": file_path,
324 "position": position,
325 "result": "LSP goto_definition requires full LSP client implementation",
326 "note": "This is a placeholder. Implement full LSP client for production use."
327 })),
328 "find_references" => Ok(serde_json::json!({
329 "command": command,
330 "file_path": file_path,
331 "position": position,
332 "result": "LSP find_references requires full LSP client implementation",
333 "note": "This is a placeholder. Implement full LSP client for production use."
334 })),
335 _ => unreachable!(),
336 }
337 }
338}
339#[cfg(test)]
340mod tests {
341 use super::*;
342 use std::io::Write;
343 use tempfile::NamedTempFile;
344
345 #[tokio::test]
346 async fn test_grep_tool_name() {
347 let tool = GrepTool::new();
348 assert_eq!(tool.name(), "grep");
349 }
350
351 #[tokio::test]
352 async fn test_grep_tool_default() {
353 let tool = GrepTool;
354 assert_eq!(tool.name(), "grep");
355 }
356
357 #[tokio::test]
358 async fn test_grep_tool_empty_pattern() {
359 let tool = GrepTool::new();
360 let args = serde_json::json!({
361 "pattern": ""
362 });
363
364 let result = tool.execute(args).await;
365 assert!(result.is_err());
366 assert!(result.unwrap_err().to_string().contains("cannot be empty"));
367 }
368
369 #[tokio::test]
370 async fn test_grep_tool_invalid_regex() {
371 let tool = GrepTool::new();
372 let args = serde_json::json!({
373 "pattern": "[invalid(regex"
374 });
375
376 let result = tool.execute(args).await;
377 assert!(result.is_err());
378 assert!(result.unwrap_err().to_string().contains("Invalid regex"));
379 }
380
381 #[tokio::test]
382 async fn test_grep_tool_path_not_found() {
383 let tool = GrepTool::new();
384 let args = serde_json::json!({
385 "pattern": "test",
386 "path": "/nonexistent/path"
387 });
388
389 let result = tool.execute(args).await;
390 assert!(result.is_err());
391 assert!(result.unwrap_err().to_string().contains("not found"));
392 }
393
394 #[tokio::test]
395 async fn test_ast_grep_tool_name() {
396 let tool = AstGrepTool::new();
397 assert_eq!(tool.name(), "ast_grep");
398 }
399
400 #[tokio::test]
401 async fn test_ast_grep_tool_default() {
402 let tool = AstGrepTool;
403 assert_eq!(tool.name(), "ast_grep");
404 }
405
406 #[tokio::test]
407 async fn test_ast_grep_tool_empty_pattern() {
408 let tool = AstGrepTool::new();
409 let args = serde_json::json!({
410 "pattern": "",
411 "language": "rust"
412 });
413
414 let result = tool.execute(args).await;
415 assert!(result.is_err());
416 assert!(result.unwrap_err().to_string().contains("cannot be empty"));
417 }
418
419 #[tokio::test]
420 async fn test_ast_grep_tool_unsupported_lang() {
421 let tool = AstGrepTool::new();
422 let args = serde_json::json!({
423 "pattern": "test",
424 "language": "java"
425 });
426
427 let result = tool.execute(args).await;
428 assert!(result.is_err());
429 assert!(result
430 .unwrap_err()
431 .to_string()
432 .contains("Unsupported language"));
433 }
434
435 #[tokio::test]
436 async fn test_ast_grep_tool_path_not_found() {
437 let tool = AstGrepTool::new();
438 let args = serde_json::json!({
439 "pattern": "test",
440 "language": "rust",
441 "path": "/nonexistent/path"
442 });
443
444 let result = tool.execute(args).await;
445 assert!(result.is_err());
446 assert!(result.unwrap_err().to_string().contains("not found"));
447 }
448
449 #[tokio::test]
450 async fn test_ast_grep_tool_rust() {
451 let tool = AstGrepTool::new();
452
453 let mut temp_file = NamedTempFile::new().unwrap();
455 writeln!(temp_file, "fn hello() {{}}").unwrap();
456 writeln!(temp_file, "fn world() {{}}").unwrap();
457
458 let args = serde_json::json!({
459 "pattern": "fn $NAME() {}",
460 "language": "rust",
461 "path": temp_file.path().parent().unwrap().to_str().unwrap()
462 });
463
464 let result = tool.execute(args).await;
466
467 match result {
470 Ok(_) => {
471 }
473 Err(e) => {
474 let error_msg = e.to_string();
476 assert!(
477 error_msg.contains("ast-grep not found") || error_msg.contains("failed"),
478 "Unexpected error: {}",
479 error_msg
480 );
481 }
482 }
483 }
484
485 #[tokio::test]
486 async fn test_ast_grep_tool_typescript() {
487 let tool = AstGrepTool::new();
488
489 let args = serde_json::json!({
490 "pattern": "console.log($MSG)",
491 "language": "typescript"
492 });
493
494 let result = tool.execute(args).await;
496
497 match result {
499 Ok(_) => {}
500 Err(e) => {
501 let error_msg = e.to_string();
502 assert!(
503 error_msg.contains("ast-grep not found") || error_msg.contains("failed"),
504 "Unexpected error: {}",
505 error_msg
506 );
507 }
508 }
509 }
510
511 #[tokio::test]
512 async fn test_ast_grep_tool_python() {
513 let tool = AstGrepTool::new();
514
515 let args = serde_json::json!({
516 "pattern": "def $FUNC():",
517 "language": "python"
518 });
519
520 let result = tool.execute(args).await;
522
523 match result {
525 Ok(_) => {}
526 Err(e) => {
527 let error_msg = e.to_string();
528 assert!(
529 error_msg.contains("ast-grep not found") || error_msg.contains("failed"),
530 "Unexpected error: {}",
531 error_msg
532 );
533 }
534 }
535 }
536
537 #[tokio::test]
538 async fn test_lsp_tool_name() {
539 let tool = LspTool::new();
540 assert_eq!(tool.name(), "lsp");
541 }
542
543 #[tokio::test]
544 async fn test_lsp_tool_default() {
545 let tool = LspTool;
546 assert_eq!(tool.name(), "lsp");
547 }
548
549 #[tokio::test]
550 async fn test_lsp_tool_missing_command() {
551 let tool = LspTool::new();
552 let args = serde_json::json!({
553 "command": "invalid_command"
554 });
555
556 let result = tool.execute(args).await;
557 assert!(result.is_err());
558 assert!(result
559 .unwrap_err()
560 .to_string()
561 .contains("Unsupported LSP command"));
562 }
563
564 #[tokio::test]
565 async fn test_lsp_tool_file_not_found() {
566 let tool = LspTool::new();
567 let args = serde_json::json!({
568 "command": "goto_definition",
569 "file_path": "/nonexistent/file.rs",
570 "position": {"line": 1, "character": 0}
571 });
572
573 let result = tool.execute(args).await;
574 assert!(result.is_err());
575 assert!(result.unwrap_err().to_string().contains("not found"));
576 }
577
578 #[tokio::test]
579 async fn test_lsp_tool_unsupported_extension() {
580 let tool = LspTool::new();
581
582 let mut temp_file = NamedTempFile::new().unwrap();
583 writeln!(temp_file, "test").unwrap();
584
585 let args = serde_json::json!({
586 "command": "goto_definition",
587 "file_path": temp_file.path(),
588 "position": {"line": 1, "character": 0}
589 });
590
591 let result = tool.execute(args).await;
592 assert!(result.is_err());
593 assert!(result
594 .unwrap_err()
595 .to_string()
596 .contains("Unsupported file extension"));
597 }
598
599 #[tokio::test]
600 async fn test_lsp_tool_missing_server() {
601 let tool = LspTool::new();
602
603 let temp_dir = tempfile::tempdir().unwrap();
605 let rust_file = temp_dir.path().join("test.rs");
606 std::fs::write(&rust_file, "fn main() {}").unwrap();
607
608 let args = serde_json::json!({
609 "command": "goto_definition",
610 "file_path": rust_file,
611 "position": {"line": 0, "character": 0}
612 });
613 let result = tool.execute(args).await;
616
617 match result {
619 Ok(value) => {
620 assert!(value["command"] == "goto_definition");
622 }
623 Err(e) => {
624 let error_msg = e.to_string();
625 assert!(
626 error_msg.contains("not found in PATH")
627 || error_msg.contains("failed to execute"),
628 "Unexpected error: {}",
629 error_msg
630 );
631 }
632 }
633 }
634
635 #[tokio::test]
636 async fn test_all_tools_implement_default() {
637 let _grep = GrepTool;
638 let _ast_grep = AstGrepTool;
639 let _lsp = LspTool;
640 }
641
642 #[tokio::test]
643 async fn test_position_deserialize() {
644 let json = serde_json::json!({"line": 10, "character": 5});
645 let pos: Position = serde_json::from_value(json).unwrap();
646 assert_eq!(pos.line, 10);
647 assert_eq!(pos.character, 5);
648 }
649}