1use crate::types::*;
22use async_trait::async_trait;
23use std::time::Duration;
24use tokio::process::Command;
25
26pub struct ListFilesTool {
28 pub max_results: usize,
29 pub timeout: Duration,
30}
31
32impl Default for ListFilesTool {
33 fn default() -> Self {
34 Self {
35 max_results: 200,
36 timeout: Duration::from_secs(10),
37 }
38 }
39}
40
41impl ListFilesTool {
42 pub fn new() -> Self {
43 Self::default()
44 }
45}
46
47#[async_trait]
48impl AgentTool for ListFilesTool {
49 fn name(&self) -> &str {
50 "list_files"
51 }
52
53 fn label(&self) -> &str {
54 "List Files"
55 }
56
57 fn description(&self) -> &str {
58 "List files and directories. Optionally filter by glob pattern. Use to explore project structure before reading specific files."
59 }
60
61 fn parameters_schema(&self) -> serde_json::Value {
62 serde_json::json!({
63 "type": "object",
64 "properties": {
65 "path": {
66 "type": "string",
67 "description": "Directory to list (default: current directory)"
68 },
69 "pattern": {
70 "type": "string",
71 "description": "Glob pattern to filter files, e.g. '*.rs' (optional)"
72 },
73 "max_depth": {
74 "type": "integer",
75 "description": "Maximum directory depth (default: 3)"
76 }
77 }
78 })
79 }
80
81 async fn execute(
82 &self,
83 params: serde_json::Value, ctx: ToolContext, ) -> Result<ToolResult, ToolError> {
86 let cancel = ctx.cancel;
87 let path = params["path"].as_str().unwrap_or(".");
88 let pattern = params["pattern"].as_str();
89 let max_depth = params["max_depth"].as_u64().unwrap_or(3);
90
91 if cancel.is_cancelled() {
92 return Err(ToolError::Cancelled);
93 }
94
95 if !std::path::Path::new(path).exists() {
97 return Err(ToolError::Failed(format!(
98 "Directory not found: {}. Check the path and try again.",
99 path
100 )));
101 }
102
103 let mut cmd = Command::new("find");
104 cmd.arg(path);
105 cmd.args(["-maxdepth", &max_depth.to_string()]);
106
107 if let Some(pat) = pattern {
108 cmd.args(["-name", pat]);
109 }
110
111 cmd.args(["-not", "-path", "*/target/*"]);
113 cmd.args(["-not", "-path", "*/.git/*"]);
114 cmd.args(["-not", "-path", "*/node_modules/*"]);
115
116 cmd.arg("-type").arg("f");
117 cmd.stdout(std::process::Stdio::piped());
118 cmd.stderr(std::process::Stdio::piped());
119
120 let timeout = self.timeout;
121
122 let result = tokio::select! {
123 _ = cancel.cancelled() => return Err(ToolError::Cancelled),
124 _ = tokio::time::sleep(timeout) => return Err(ToolError::Failed("Listing timed out".into())),
125 result = cmd.output() => {
126 result.map_err(|e| ToolError::Failed(format!("Failed to list: {}", e)))?
127 }
128 };
129
130 let stdout = String::from_utf8_lossy(&result.stdout).to_string();
131 let mut lines: Vec<&str> = stdout.lines().collect();
132 lines.sort();
133
134 let total = lines.len();
135 let truncated = total > self.max_results;
136 if truncated {
137 lines.truncate(self.max_results);
138 }
139
140 let text = if lines.is_empty() {
141 format!("No files found in {}", path)
142 } else if truncated {
143 format!(
144 "{}\n\n... ({} files, showing first {})",
145 lines.join("\n"),
146 total,
147 self.max_results
148 )
149 } else {
150 format!("{}\n\n({} files)", lines.join("\n"), total)
151 };
152
153 Ok(ToolResult {
154 content: vec![Content::Text { text }],
155 details: serde_json::json!({ "total": total, "truncated": truncated }),
156 child_loop_id: None,
157 })
158 }
159}