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 });
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}