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                    });
80                }
81                return Ok(ToolResult {
82                    success: false,
83                    result: format!("Failed to read metadata for '{path}': {error}"),
84                    display_preference: Some("error".to_string()),
85                });
86            }
87        };
88
89        let modified_unix = metadata
90            .modified()
91            .ok()
92            .and_then(|time| time.duration_since(std::time::UNIX_EPOCH).ok())
93            .map(|duration| duration.as_secs());
94
95        Ok(ToolResult {
96            success: true,
97            result: json!({
98                "path": path,
99                "exists": true,
100                "is_file": metadata.is_file(),
101                "is_dir": metadata.is_dir(),
102                "size_bytes": metadata.len(),
103                "modified_unix": modified_unix
104            })
105            .to_string(),
106            display_preference: Some("json".to_string()),
107        })
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    #[tokio::test]
116    async fn get_file_info_returns_metadata_for_existing_file() {
117        let dir = tempfile::tempdir().unwrap();
118        let file_path = dir.path().join("demo.txt");
119        tokio::fs::write(&file_path, "hello").await.unwrap();
120
121        let tool = GetFileInfoTool::new();
122        let result = tool
123            .execute(json!({"path": file_path.to_string_lossy()}))
124            .await
125            .unwrap();
126
127        assert!(result.success);
128        assert!(result.result.contains("\"is_file\":true"));
129        assert!(result.result.contains("\"exists\":true"));
130    }
131
132    #[tokio::test]
133    async fn get_file_info_returns_exists_false_for_missing_path() {
134        let tool = GetFileInfoTool::new();
135        let result = tool
136            .execute(json!({"path": "/tmp/bamboo-file-info-missing-xyz-98765"}))
137            .await
138            .unwrap();
139
140        assert!(result.success);
141        let payload: serde_json::Value = serde_json::from_str(&result.result).unwrap();
142        assert_eq!(payload["exists"], false);
143    }
144}