Skip to main content

astrid_tools/
list_directory.rs

1//! List directory tool — lists directory contents with type and size info.
2
3use std::fmt::Write;
4
5use crate::{BuiltinTool, ToolContext, ToolError, ToolResult};
6use serde_json::Value;
7use std::path::PathBuf;
8
9/// Built-in tool for listing directory contents.
10pub struct ListDirectoryTool;
11
12#[async_trait::async_trait]
13impl BuiltinTool for ListDirectoryTool {
14    fn name(&self) -> &'static str {
15        "list_directory"
16    }
17
18    fn description(&self) -> &'static str {
19        "Lists the contents of a directory. Shows directories first, then files, \
20         both sorted alphabetically. Includes type indicator and file sizes."
21    }
22
23    fn input_schema(&self) -> Value {
24        serde_json::json!({
25            "type": "object",
26            "properties": {
27                "path": {
28                    "type": "string",
29                    "description": "Absolute path to the directory to list"
30                }
31            },
32            "required": ["path"]
33        })
34    }
35
36    async fn execute(&self, args: Value, _ctx: &ToolContext) -> ToolResult {
37        let dir_path = args
38            .get("path")
39            .and_then(Value::as_str)
40            .ok_or_else(|| ToolError::InvalidArguments("path is required".into()))?;
41
42        let path = PathBuf::from(dir_path);
43        if !path.exists() {
44            return Err(ToolError::PathNotFound(dir_path.to_string()));
45        }
46        if !path.is_dir() {
47            return Err(ToolError::InvalidArguments(format!(
48                "{dir_path} is not a directory"
49            )));
50        }
51
52        let mut dirs: Vec<String> = Vec::new();
53        let mut files: Vec<String> = Vec::new();
54
55        let mut entries = tokio::fs::read_dir(&path).await?;
56        while let Some(entry) = entries.next_entry().await? {
57            let name = entry.file_name().to_string_lossy().to_string();
58            let metadata = entry.metadata().await?;
59
60            if metadata.is_dir() {
61                dirs.push(format!("  {name}/"));
62            } else {
63                let size = metadata.len();
64                let size_str = format_size(size);
65                files.push(format!("  {name}  ({size_str})"));
66            }
67        }
68
69        dirs.sort();
70        files.sort();
71
72        let mut output = String::new();
73        for d in &dirs {
74            output.push_str(d);
75            output.push('\n');
76        }
77        for f in &files {
78            output.push_str(f);
79            output.push('\n');
80        }
81
82        let total = dirs.len().saturating_add(files.len());
83
84        let _ = write!(
85            output,
86            "\n({} directories, {} files)",
87            dirs.len(),
88            files.len()
89        );
90
91        if total == 0 {
92            return Ok(format!("{dir_path} is empty"));
93        }
94
95        Ok(output)
96    }
97}
98
99/// Format a byte count into a human-readable size string.
100#[allow(clippy::cast_precision_loss)]
101fn format_size(bytes: u64) -> String {
102    const KB: u64 = 1024;
103    const MB: u64 = 1024 * KB;
104    const GB: u64 = 1024 * MB;
105
106    if bytes >= GB {
107        format!("{:.1} GB", bytes as f64 / GB as f64)
108    } else if bytes >= MB {
109        format!("{:.1} MB", bytes as f64 / MB as f64)
110    } else if bytes >= KB {
111        format!("{:.1} KB", bytes as f64 / KB as f64)
112    } else {
113        format!("{bytes} B")
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120    use tempfile::TempDir;
121
122    fn ctx() -> ToolContext {
123        ToolContext::new(std::env::temp_dir())
124    }
125
126    #[tokio::test]
127    async fn test_list_directory_basic() {
128        let dir = TempDir::new().unwrap();
129        std::fs::create_dir(dir.path().join("subdir")).unwrap();
130        std::fs::write(dir.path().join("file.txt"), "hello").unwrap();
131
132        let result = ListDirectoryTool
133            .execute(
134                serde_json::json!({"path": dir.path().to_str().unwrap()}),
135                &ctx(),
136            )
137            .await
138            .unwrap();
139
140        assert!(result.contains("subdir/"));
141        assert!(result.contains("file.txt"));
142        assert!(result.contains("1 directories, 1 files"));
143    }
144
145    #[tokio::test]
146    async fn test_list_directory_dirs_first() {
147        let dir = TempDir::new().unwrap();
148        std::fs::write(dir.path().join("aaa.txt"), "").unwrap();
149        std::fs::create_dir(dir.path().join("zzz")).unwrap();
150
151        let result = ListDirectoryTool
152            .execute(
153                serde_json::json!({"path": dir.path().to_str().unwrap()}),
154                &ctx(),
155            )
156            .await
157            .unwrap();
158
159        let dir_pos = result.find("zzz/").unwrap();
160        let file_pos = result.find("aaa.txt").unwrap();
161        assert!(dir_pos < file_pos);
162    }
163
164    #[tokio::test]
165    async fn test_list_directory_not_found() {
166        let result = ListDirectoryTool
167            .execute(
168                serde_json::json!({"path": "/tmp/astrid_nonexistent_dir_12345"}),
169                &ctx(),
170            )
171            .await;
172
173        assert!(result.is_err());
174    }
175
176    #[tokio::test]
177    async fn test_list_directory_not_a_dir() {
178        let dir = TempDir::new().unwrap();
179        let file_path = dir.path().join("file.txt");
180        std::fs::write(&file_path, "hello").unwrap();
181
182        let result = ListDirectoryTool
183            .execute(
184                serde_json::json!({"path": file_path.to_str().unwrap()}),
185                &ctx(),
186            )
187            .await;
188
189        assert!(result.is_err());
190    }
191
192    #[test]
193    fn test_format_size() {
194        assert_eq!(format_size(0), "0 B");
195        assert_eq!(format_size(512), "512 B");
196        assert_eq!(format_size(1024), "1.0 KB");
197        assert_eq!(format_size(1_048_576), "1.0 MB");
198        assert_eq!(format_size(1_073_741_824), "1.0 GB");
199    }
200}