Skip to main content

bamboo_tools/tools/
get_file_info.rs

1use async_trait::async_trait;
2use bamboo_agent_core::{Tool, ToolError, ToolResult};
3use serde_json::json;
4use tokio::fs;
5
6/// Return metadata for a file or directory.
7pub struct GetFileInfoTool;
8
9impl GetFileInfoTool {
10    pub fn new() -> Self {
11        Self
12    }
13}
14
15impl Default for GetFileInfoTool {
16    fn default() -> Self {
17        Self::new()
18    }
19}
20
21#[async_trait]
22impl Tool for GetFileInfoTool {
23    fn name(&self) -> &str {
24        "GetFileInfo"
25    }
26
27    fn description(&self) -> &str {
28        "Get file metadata such as type, size, modification time, and existence without loading file contents."
29    }
30
31    fn mutability(&self) -> crate::ToolMutability {
32        crate::ToolMutability::ReadOnly
33    }
34
35    fn concurrency_safe(&self) -> bool {
36        true
37    }
38
39    fn parameters_schema(&self) -> serde_json::Value {
40        json!({
41            "type": "object",
42            "properties": {
43                "path": {
44                    "type": "string",
45                    "description": "Absolute or relative path"
46                }
47            },
48            "required": ["path"],
49            "additionalProperties": false
50        })
51    }
52
53    async fn execute(&self, args: serde_json::Value) -> Result<ToolResult, ToolError> {
54        let path = args
55            .get("path")
56            .and_then(|v| v.as_str())
57            .ok_or_else(|| ToolError::InvalidArguments("Missing 'path' parameter".to_string()))?;
58
59        if path.trim().is_empty() {
60            return Err(ToolError::InvalidArguments(
61                "path must be a non-empty string".to_string(),
62            ));
63        }
64
65        let metadata = match fs::metadata(path).await {
66            Ok(metadata) => metadata,
67            Err(error) => {
68                // When the path simply does not exist, return a structured
69                // `exists: false` response so this tool subsumes FileExists.
70                if error.kind() == std::io::ErrorKind::NotFound {
71                    return Ok(ToolResult {
72                        success: true,
73                        result: json!({
74                            "path": path,
75                            "exists": false
76                        })
77                        .to_string(),
78                        display_preference: Some("json".to_string()),
79                        images: Vec::new(),
80                    });
81                }
82                return Ok(ToolResult {
83                    success: false,
84                    result: format!("Failed to read metadata for '{path}': {error}"),
85                    display_preference: Some("error".to_string()),
86                    images: Vec::new(),
87                });
88            }
89        };
90
91        let modified_unix = metadata
92            .modified()
93            .ok()
94            .and_then(|time| time.duration_since(std::time::UNIX_EPOCH).ok())
95            .map(|duration| duration.as_secs());
96
97        Ok(ToolResult {
98            success: true,
99            result: json!({
100                "path": path,
101                "exists": true,
102                "is_file": metadata.is_file(),
103                "is_dir": metadata.is_dir(),
104                "size_bytes": metadata.len(),
105                "modified_unix": modified_unix
106            })
107            .to_string(),
108            display_preference: Some("json".to_string()),
109            images: Vec::new(),
110        })
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    #[tokio::test]
119    async fn get_file_info_returns_metadata_for_existing_file() {
120        let dir = tempfile::tempdir().unwrap();
121        let file_path = dir.path().join("demo.txt");
122        tokio::fs::write(&file_path, "hello").await.unwrap();
123
124        let tool = GetFileInfoTool::new();
125        let result = tool
126            .execute(json!({"path": file_path.to_string_lossy()}))
127            .await
128            .unwrap();
129
130        assert!(result.success);
131        assert!(result.result.contains("\"is_file\":true"));
132        assert!(result.result.contains("\"exists\":true"));
133    }
134
135    #[tokio::test]
136    async fn get_file_info_returns_exists_false_for_missing_path() {
137        let tool = GetFileInfoTool::new();
138        let result = tool
139            .execute(json!({"path": "/tmp/bamboo-file-info-missing-xyz-98765"}))
140            .await
141            .unwrap();
142
143        assert!(result.success);
144        let payload: serde_json::Value = serde_json::from_str(&result.result).unwrap();
145        assert_eq!(payload["exists"], false);
146    }
147}