Skip to main content

robit_agent/tool/
ls.rs

1//! `ls` tool — lists directory contents.
2
3use 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        // Resolve directory path
69        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        // Check if path exists and is a directory
75        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        // Read directory contents
83        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            // Skip hidden files unless show_hidden is true
103            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        // Sort alphabetically
119        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}