shuvarie-core 0.3.2

Blazingly fast AI coding TUI for chivalrous people (core module)
use serde_json::{Value, json};
use shuvarie_llm::{Tool, ToolContext, ToolExecutionError, ToolOutput};

use crate::permissions::{Access, PathKind, resolve_read};

pub(crate) struct ListDir {
    access: Access,
}

impl ListDir {
    pub(crate) fn new(access: Access) -> Self {
        Self { access }
    }
}

impl Tool for ListDir {
    const NAME: &'static str = "list_dir";

    type Args = Value;
    type Output = ToolOutput;
    type Error = ToolExecutionError;

    fn description(&self) -> String {
        "List the entries of a directory, one per line (directories suffixed with '/'). Entries are sorted by name."
            .to_string()
    }

    fn parameters(&self) -> Value {
        json!({
            "type": "object",
            "properties": {
                "path": { "type": "string", "description": "Directory to list, relative to the workspace root (defaults to the root itself)" }
            },
            "required": []
        })
    }

    async fn call(
        &self,
        _ctx: &mut ToolContext,
        args: Value,
    ) -> Result<ToolOutput, ToolExecutionError> {
        let result: Result<ToolOutput, String> = async move {
            let path = args.get("path").and_then(Value::as_str).unwrap_or(".");
            let abs = resolve_read(path)?;
            self.access
                .authorize_path(PathKind::Read, &abs, path)
                .await?;
            let entries = std::fs::read_dir(&abs).map_err(|e| format!("read_dir {path}: {e}"))?;
            let mut names: Vec<String> = Vec::new();
            for entry in entries {
                let entry = entry.map_err(|e| format!("read_dir {path}: {e}"))?;
                let name = entry.file_name().to_string_lossy().into_owned();
                let suffix = if entry.file_type().map(|t| t.is_dir()).unwrap_or(false) {
                    "/"
                } else {
                    ""
                };
                names.push(format!("{name}{suffix}"));
            }
            names.sort();
            if names.is_empty() {
                Ok(ToolOutput::text(format!("{path} is empty")))
            } else {
                Ok(ToolOutput::text(names.join("\n")))
            }
        }
        .await;
        result.map_err(ToolExecutionError::other)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::test_util::{new_ctx, tempdir};

    #[tokio::test]
    async fn list_dir_sorted() {
        let (dir, _guard) = tempdir();
        std::fs::create_dir("zdir").unwrap();
        std::fs::write("afile", "").unwrap();
        let out = ListDir::new(crate::test_util::access())
            .call(&mut new_ctx(), json!({}))
            .await
            .unwrap();
        let lines: Vec<&str> = out.as_text().unwrap().lines().collect();
        assert_eq!(lines, vec!["afile", "zdir/"]);
        drop(dir);
    }
}