bamboo_tools/tools/
get_file_info.rs1use async_trait::async_trait;
2use bamboo_agent_core::{Tool, ToolError, ToolResult};
3use serde_json::json;
4use tokio::fs;
5
6pub 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 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}