1use async_trait::async_trait;
4use serde::Deserialize;
5use serde_json::Value;
6
7use super::{resolve_path, Tool, ToolContext, ToolResult};
8use crate::error::Result;
9
10pub struct LsTool;
11
12#[derive(Debug, Deserialize)]
13struct LsArgs {
14 dir_path: Option<String>,
15 #[serde(default)]
16 show_hidden: bool,
17}
18
19impl LsTool {
20 pub fn new() -> Self {
21 Self
22 }
23}
24
25impl Default for LsTool {
26 fn default() -> Self {
27 Self::new()
28 }
29}
30
31#[async_trait]
32impl Tool for LsTool {
33 fn name(&self) -> &str {
34 "ls"
35 }
36
37 fn description(&self) -> &str {
38 "List directory contents. Lists current directory if no path is specified."
39 }
40
41 fn parameters_schema(&self) -> Value {
42 serde_json::json!({
43 "type": "object",
44 "properties": {
45 "dir_path": {
46 "type": "string",
47 "description": "Directory path (optional, defaults to current directory)"
48 },
49 "show_hidden": {
50 "type": "boolean",
51 "description": "Whether to show hidden files (files starting with .), default false"
52 }
53 },
54 "required": []
55 })
56 }
57
58 fn requires_confirmation(&self) -> bool {
59 false
60 }
61
62 async fn execute(&self, args: Value, ctx: &ToolContext) -> Result<ToolResult> {
63 let parsed: LsArgs = match serde_json::from_value(args) {
64 Ok(a) => a,
65 Err(e) => return Ok(ToolResult::error(format!("Argument parsing failed: {}", e))),
66 };
67
68 let dir_path = match parsed.dir_path {
70 Some(p) => resolve_path(&p, &ctx.working_dir),
71 None => ctx.working_dir.clone(),
72 };
73
74 if !dir_path.exists() {
76 return Ok(ToolResult::error(format!("Path does not exist: {}", dir_path.display())));
77 }
78 if !dir_path.is_dir() {
79 return Ok(ToolResult::error(format!("'{}' is not a directory", dir_path.display())));
80 }
81
82 let mut entries = match tokio::fs::read_dir(&dir_path).await {
84 Ok(e) => e,
85 Err(e) => {
86 return Ok(ToolResult::error(format!("Failed to read directory '{}': {}", dir_path.display(), e)));
87 }
88 };
89
90 let mut output = String::new();
91 let mut dirs = Vec::new();
92 let mut files = Vec::new();
93
94 loop {
95 let entry = match entries.next_entry().await {
96 Ok(Some(e)) => e,
97 Ok(None) => break,
98 Err(_) => continue,
99 };
100 let file_name = entry.file_name().to_string_lossy().to_string();
101
102 if !parsed.show_hidden && file_name.starts_with('.') {
104 continue;
105 }
106
107 let metadata = match entry.metadata().await {
108 Ok(m) => m,
109 Err(_) => continue,
110 };
111 if metadata.is_dir() {
112 dirs.push(file_name);
113 } else {
114 files.push(file_name);
115 }
116 }
117
118 dirs.sort();
120 files.sort();
121
122 output.push_str(&format!("Directory: {}\n", dir_path.display()));
123 output.push_str(&format!("{}", "ā".repeat(50)));
124
125 if !dirs.is_empty() {
126 output.push_str("\nš Directories:\n");
127 for dir in &dirs {
128 output.push_str(&format!(" {}/\n", dir));
129 }
130 }
131
132 if !files.is_empty() {
133 output.push_str("\nš Files:\n");
134 for file in &files {
135 output.push_str(&format!(" {}\n", file));
136 }
137 }
138
139 if dirs.is_empty() && files.is_empty() {
140 output.push_str("\n(Directory is empty)");
141 }
142
143 Ok(ToolResult::success(output))
144 }
145}