agent_engine/tools/
find.rs1use super::{expand_path, Tool, ToolContext};
2use crate::{Result, RuntimeError};
3use serde_json::{json, Value};
4use std::time::Duration;
5use tokio::process::Command;
6
7pub struct FindTool;
8
9#[async_trait::async_trait]
10impl Tool for FindTool {
11 fn effect(&self) -> crate::tools::catalog::ToolEffect {
12 crate::tools::catalog::ToolEffect::ReadOnly
13 }
14
15 fn origin(&self) -> crate::tools::ToolOrigin {
16 crate::tools::ToolOrigin::Builtin
17 }
18
19 fn name(&self) -> &str {
20 "find"
21 }
22
23 fn description(&self) -> &str {
24 "Find files by name using glob patterns. Searches recursively from the given path. Excludes .git directories."
25 }
26
27 fn parameters(&self) -> Value {
28 json!({
29 "type": "object",
30 "properties": {
31 "pattern": {
32 "type": "string",
33 "description": "Glob pattern to match file names (e.g. \"*.rs\", \"Cargo.*\")"
34 },
35 "path": {
36 "type": "string",
37 "description": "Directory to search in (default: current directory)"
38 },
39 "type": {
40 "type": "string",
41 "description": "Filter by type: \"f\" for files, \"d\" for directories"
42 }
43 },
44 "required": ["pattern"]
45 })
46 }
47
48 async fn execute(&self, params: Value, _ctx: ToolContext) -> Result<String> {
49 let pattern = params["pattern"]
50 .as_str()
51 .ok_or_else(|| RuntimeError::Tool("Missing pattern parameter".to_string()))?;
52 let path = expand_path(params["path"].as_str().unwrap_or("."));
53 let file_type = params["type"].as_str();
54
55 let mut cmd = Command::new("find");
56 cmd.arg(&path);
57
58 cmd.args(["-not", "-path", "*/.git/*"]);
59 cmd.args(["-not", "-path", "*/node_modules/*"]);
60 cmd.args(["-not", "-path", "*/target/*"]);
61
62 if let Some(t) = file_type {
63 cmd.arg("-type").arg(t);
64 }
65
66 cmd.arg("-name").arg(pattern);
67
68 let output = tokio::time::timeout(Duration::from_secs(10), cmd.output())
69 .await
70 .map_err(|_| RuntimeError::Tool("Find timed out after 10s".to_string()))?
71 .map_err(|e| RuntimeError::Tool(format!("Failed to execute find: {}", e)))?;
72
73 let stdout = String::from_utf8_lossy(&output.stdout);
74
75 if stdout.is_empty() {
76 Ok("No files found.".to_string())
77 } else {
78 Ok(stdout.trim().to_string())
79 }
80 }
81}
82#[cfg(test)]
83mod tests {
84 use super::super::test_helpers::create_tool_context;
85 use super::*;
86 use crate::tools::Tool;
87 use serde_json::json;
88
89 #[test]
90 fn test_find_tool_schema() {
91 let tool = FindTool;
92 assert_eq!(tool.name(), "find");
93 assert!(!tool.description().is_empty());
94
95 let params = tool.parameters();
96 assert_eq!(params["type"], "object");
97 assert!(params["properties"].is_object());
98 assert!(params["required"].is_array());
99 }
100
101 #[tokio::test]
102 async fn test_find_tool_execution() {
103 let temp_dir = std::env::temp_dir().join("test_find_tool_execution");
104 std::fs::create_dir_all(&temp_dir).unwrap();
105
106 let test_file = temp_dir.join("test_find_me.txt");
107 std::fs::write(&test_file, "test content").unwrap();
108
109 let tool = FindTool;
110 let ctx = create_tool_context();
111
112 let params = json!({
113 "pattern": "test_find_me*",
114 "path": temp_dir.to_string_lossy()
115 });
116
117 let result = tool.execute(params, ctx).await.unwrap();
118
119 assert!(result.contains("test_find_me.txt"));
121
122 let _ = std::fs::remove_dir_all(&temp_dir);
124 }
125}