Skip to main content

synaps_cli/tools/
find.rs

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