Skip to main content

agent_engine/tools/
find.rs

1use super::{resolve_path_in, 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 = resolve_path_in(
53            params["path"].as_str().unwrap_or("."),
54            ctx.capabilities.cwd.as_deref(),
55        );
56        let file_type = params["type"].as_str();
57
58        let mut cmd = Command::new("find");
59        if let Some(env) = &ctx.capabilities.env {
60            cmd.env_clear().envs(env.iter().map(|(k, v)| (k, v)));
61        }
62        cmd.arg(&path);
63
64        cmd.args(["-not", "-path", "*/.git/*"]);
65        cmd.args(["-not", "-path", "*/node_modules/*"]);
66        cmd.args(["-not", "-path", "*/target/*"]);
67
68        if let Some(t) = file_type {
69            cmd.arg("-type").arg(t);
70        }
71
72        cmd.arg("-name").arg(pattern);
73
74        let output = tokio::time::timeout(Duration::from_secs(10), cmd.output())
75            .await
76            .map_err(|_| RuntimeError::Tool("Find timed out after 10s".to_string()))?
77            .map_err(|e| RuntimeError::Tool(format!("Failed to execute find: {}", e)))?;
78
79        let stdout = String::from_utf8_lossy(&output.stdout);
80
81        if stdout.is_empty() {
82            Ok("No files found.".to_string())
83        } else {
84            Ok(stdout.trim().to_string())
85        }
86    }
87}
88#[cfg(test)]
89mod tests {
90    use super::super::test_helpers::create_tool_context;
91    use super::*;
92    use crate::tools::Tool;
93    use serde_json::json;
94
95    #[test]
96    fn test_find_tool_schema() {
97        let tool = FindTool;
98        assert_eq!(tool.name(), "find");
99        assert!(!tool.description().is_empty());
100
101        let params = tool.parameters();
102        assert_eq!(params["type"], "object");
103        assert!(params["properties"].is_object());
104        assert!(params["required"].is_array());
105    }
106
107    #[tokio::test]
108    async fn test_find_tool_execution() {
109        let temp_dir = std::env::temp_dir().join("test_find_tool_execution");
110        std::fs::create_dir_all(&temp_dir).unwrap();
111
112        let test_file = temp_dir.join("test_find_me.txt");
113        std::fs::write(&test_file, "test content").unwrap();
114
115        let tool = FindTool;
116        let ctx = create_tool_context();
117
118        let params = json!({
119            "pattern": "test_find_me*",
120            "path": temp_dir.to_string_lossy()
121        });
122
123        let result = tool.execute(params, ctx).await.unwrap();
124
125        // Should contain the filename
126        assert!(result.contains("test_find_me.txt"));
127
128        // Cleanup
129        let _ = std::fs::remove_dir_all(&temp_dir);
130    }
131}