bamboo_tools/tools/
get_file_info.rs1use async_trait::async_trait;
2use bamboo_agent_core::{Tool, ToolClass, ToolCtx, ToolError, ToolOutcome, 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 classify(&self, _args: &serde_json::Value) -> ToolClass {
32 ToolClass::READONLY_PARALLEL
33 }
34
35 fn parameters_schema(&self) -> serde_json::Value {
36 json!({
37 "type": "object",
38 "properties": {
39 "path": {
40 "type": "string",
41 "description": "Absolute or relative path"
42 }
43 },
44 "required": ["path"],
45 "additionalProperties": false
46 })
47 }
48
49 async fn invoke(
50 &self,
51 args: serde_json::Value,
52 _ctx: ToolCtx,
53 ) -> Result<ToolOutcome, 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(ToolOutcome::Completed(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(ToolOutcome::Completed(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(ToolOutcome::Completed(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 out = tool
126 .invoke(
127 json!({"path": file_path.to_string_lossy()}),
128 ToolCtx::none("t"),
129 )
130 .await
131 .unwrap();
132 let ToolOutcome::Completed(result) = out else {
133 panic!("expected Completed")
134 };
135
136 assert!(result.success);
137 assert!(result.result.contains("\"is_file\":true"));
138 assert!(result.result.contains("\"exists\":true"));
139 }
140
141 #[tokio::test]
142 async fn get_file_info_returns_exists_false_for_missing_path() {
143 let tool = GetFileInfoTool::new();
144 let out = tool
145 .invoke(
146 json!({"path": "/tmp/bamboo-file-info-missing-xyz-98765"}),
147 ToolCtx::none("t"),
148 )
149 .await
150 .unwrap();
151 let ToolOutcome::Completed(result) = out else {
152 panic!("expected Completed")
153 };
154
155 assert!(result.success);
156 let payload: serde_json::Value = serde_json::from_str(&result.result).unwrap();
157 assert_eq!(payload["exists"], false);
158 }
159}