Skip to main content

yapi_core/tools/
ls.rs

1//! `ls`: directory entries, sorted, with `/` after directories.
2
3use futures_util::future::BoxFuture;
4use serde_json::{Map, Value, json};
5use tokio_util::sync::CancellationToken;
6use yapi_agent::{Tool, UpdateSink};
7use yapi_types::event::ToolResult;
8use yapi_types::message::ToolDeclaration;
9
10use super::path::resolve_to_cwd;
11use super::truncate::DEFAULT_MAX_BYTES;
12use super::{
13    ToolEnv, capped_output, check_abort, js_number, node_error, plain_declaration, text_result,
14};
15use yapi_types::collate::locale_compare;
16
17const DEFAULT_LIMIT: f64 = 500.0;
18
19/// The `ls` tool.
20pub struct Ls {
21    env: ToolEnv,
22    declaration: ToolDeclaration,
23}
24
25impl Ls {
26    /// An `ls` tool for `env`.
27    pub fn new(env: ToolEnv) -> Ls {
28        Ls {
29            env,
30            declaration: plain_declaration(
31                "ls",
32                format!(
33                    "List directory contents. Returns entries sorted alphabetically, with '/' suffix for directories. Includes dotfiles. Output is truncated to {} entries or {}KB (whichever is hit first).",
34                    DEFAULT_LIMIT,
35                    DEFAULT_MAX_BYTES / 1024
36                ),
37                json!({"type":"object","properties":{
38                    "path":{"type":"string","description":"Directory to list (default: current directory)"},
39                    "limit":{"type":"number","description":"Maximum number of entries to return (default: 500)"}}}),
40            ),
41        }
42    }
43}
44
45impl Tool for Ls {
46    fn declaration(&self) -> &ToolDeclaration {
47        &self.declaration
48    }
49
50    fn execute(
51        &self,
52        _call_id: String,
53        args: Value,
54        cancel: CancellationToken,
55        _updates: UpdateSink,
56    ) -> BoxFuture<'_, Result<ToolResult, String>> {
57        Box::pin(async move {
58            check_abort(&cancel)?;
59            self.list(args)
60        })
61    }
62}
63
64impl Ls {
65    fn list(&self, args: Value) -> Result<ToolResult, String> {
66        let dir = args["path"]
67            .as_str()
68            .filter(|p| !p.is_empty())
69            .unwrap_or(".");
70        let path = resolve_to_cwd(dir, &self.env.cwd);
71        let limit = args["limit"].as_f64().unwrap_or(DEFAULT_LIMIT);
72        if !path.exists() {
73            return Err(format!("Path not found: {}", path.display()));
74        }
75        let metadata = std::fs::metadata(&path).map_err(|err| node_error(&err, "stat", &path))?;
76        if !metadata.is_dir() {
77            return Err(format!("Not a directory: {}", path.display()));
78        }
79        let mut entries: Vec<String> = std::fs::read_dir(&path)
80            .map_err(|err| {
81                format!(
82                    "Cannot read directory: {}",
83                    node_error(&err, "scandir", &path)
84                )
85            })?
86            .flatten()
87            .map(|entry| entry.file_name().to_string_lossy().into_owned())
88            .collect();
89        entries.sort_by(|a, b| locale_compare(&a.to_lowercase(), &b.to_lowercase()));
90        let mut results = Vec::new();
91        let mut limit_reached = false;
92        for entry in entries {
93            if results.len() as f64 >= limit {
94                limit_reached = true;
95                break;
96            }
97            // Entries that cannot be stat'ed, such as broken links, are skipped.
98            let Ok(metadata) = std::fs::metadata(path.join(&entry)) else {
99                continue;
100            };
101            results.push(if metadata.is_dir() {
102                format!("{entry}/")
103            } else {
104                entry
105            });
106        }
107        if results.is_empty() {
108            return Ok(text_result("(empty directory)", None));
109        }
110        let mut details = Map::new();
111        let mut notices = Vec::new();
112        if limit_reached {
113            notices.push(format!(
114                "{} entries limit reached. Use limit={} for more",
115                js_number(limit),
116                js_number(limit * 2.0)
117            ));
118            details.insert("entryLimitReached".into(), json!(limit));
119        }
120        Ok(capped_output(
121            &results.join("\n"),
122            notices,
123            Vec::new(),
124            details,
125        ))
126    }
127}