Skip to main content

agent_engine/tools/
ls.rs

1use super::{resolve_path_in, Tool, ToolContext};
2use crate::{Result, RuntimeError};
3use serde_json::{json, Value};
4use tokio::process::Command;
5
6pub struct LsTool;
7
8#[async_trait::async_trait]
9impl Tool for LsTool {
10    fn effect(&self) -> crate::tools::catalog::ToolEffect {
11        crate::tools::catalog::ToolEffect::ReadOnly
12    }
13
14    fn origin(&self) -> crate::tools::ToolOrigin {
15        crate::tools::ToolOrigin::Builtin
16    }
17
18    fn name(&self) -> &str {
19        "ls"
20    }
21
22    fn description(&self) -> &str {
23        "List directory contents with details (permissions, size, modification date). Defaults to current directory."
24    }
25
26    fn parameters(&self) -> Value {
27        json!({
28            "type": "object",
29            "properties": {
30                "path": {
31                    "type": "string",
32                    "description": "Directory path to list (default: current directory)"
33                }
34            },
35            "required": []
36        })
37    }
38
39    async fn execute(&self, params: Value, ctx: ToolContext) -> Result<String> {
40        let path = resolve_path_in(
41            params["path"].as_str().unwrap_or("."),
42            ctx.capabilities.cwd.as_deref(),
43        );
44
45        let mut cmd = Command::new("ls");
46        if let Some(env) = &ctx.capabilities.env {
47            cmd.env_clear().envs(env.iter().map(|(k, v)| (k, v)));
48        }
49        let output = cmd
50            .arg("-lah")
51            .arg(&path)
52            .output()
53            .await
54            .map_err(|e| RuntimeError::Tool(format!("Failed to execute ls: {}", e)))?;
55
56        let stdout = String::from_utf8_lossy(&output.stdout);
57        let stderr = String::from_utf8_lossy(&output.stderr);
58
59        if !output.status.success() {
60            Err(RuntimeError::Tool(format!("ls failed: {}", stderr)))
61        } else if stdout.is_empty() {
62            Ok("Directory is empty.".to_string())
63        } else {
64            Ok(stdout.to_string())
65        }
66    }
67}
68#[cfg(test)]
69mod tests {
70    use super::super::test_helpers::create_tool_context;
71    use super::*;
72    use crate::tools::Tool;
73    use serde_json::json;
74
75    #[test]
76    fn test_ls_tool_schema() {
77        let tool = LsTool;
78        assert_eq!(tool.name(), "ls");
79        assert!(!tool.description().is_empty());
80
81        let params = tool.parameters();
82        assert_eq!(params["type"], "object");
83        assert!(params["properties"].is_object());
84        assert!(params["required"].is_array());
85    }
86
87    #[tokio::test]
88    async fn test_ls_tool_execution() {
89        let tool = LsTool;
90        let ctx = create_tool_context();
91
92        // Use a dedicated temp dir to avoid races with other tests
93        let dir = std::env::temp_dir().join("synaps_test_ls");
94        std::fs::create_dir_all(&dir).unwrap();
95        std::fs::write(dir.join("hello.txt"), "hi").unwrap();
96
97        let params = json!({
98            "path": dir.to_str().unwrap()
99        });
100
101        let result = tool.execute(params, ctx).await.unwrap();
102        assert!(result.contains("hello.txt"));
103
104        // Cleanup
105        std::fs::remove_dir_all(&dir).ok();
106    }
107}