ai-agent-sdk 0.5.0

Idiomatic agent sdk inspired by the claude code source leak
Documentation
use crate::types::*;
use glob::glob;

pub struct GlobTool;

impl GlobTool {
    pub fn new() -> Self {
        Self
    }

    pub fn name(&self) -> &str {
        "Glob"
    }

    pub fn description(&self) -> &str {
        "Find files by glob pattern"
    }

    pub fn input_schema(&self) -> ToolInputSchema {
        ToolInputSchema {
            schema_type: "object".to_string(),
            properties: serde_json::json!({
                "pattern": {
                    "type": "string",
                    "description": "The glob pattern to match"
                }
            }),
            required: Some(vec!["pattern".to_string()]),
        }
    }

    pub async fn execute(
        &self,
        input: serde_json::Value,
        context: &ToolContext,
    ) -> Result<ToolResult, crate::error::AgentError> {
        let pattern = input["pattern"]
            .as_str()
            .ok_or_else(|| crate::error::AgentError::Tool("pattern is required".to_string()))?;

        // Resolve relative patterns using cwd from context
        let base_path = std::path::Path::new(&context.cwd);
        let full_pattern = if std::path::Path::new(pattern).is_relative() {
            base_path.join(pattern)
        } else {
            std::path::PathBuf::from(pattern)
        };

        let matches: Vec<String> = glob(full_pattern.to_string_lossy().as_ref())
            .map_err(|e| crate::error::AgentError::Tool(e.to_string()))?
            .filter_map(std::result::Result::ok)
            .map(|path| path.to_string_lossy().to_string())
            .collect();

        let content = if matches.is_empty() {
            "No files found".to_string()
        } else {
            matches.join("\n")
        };

        Ok(ToolResult {
            result_type: "text".to_string(),
            tool_use_id: "".to_string(),
            content,
            is_error: None,
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_glob_tool_name() {
        let tool = GlobTool::new();
        assert_eq!(tool.name(), "Glob");
    }

    #[test]
    fn test_glob_tool_description_contains_glob() {
        let tool = GlobTool::new();
        assert!(tool.description().to_lowercase().contains("glob"));
    }

    #[test]
    fn test_glob_tool_has_pattern_in_schema() {
        let tool = GlobTool::new();
        let schema = tool.input_schema();
        assert!(schema.properties.get("pattern").is_some());
    }

    #[tokio::test]
    async fn test_glob_tool_finds_matching_files() {
        // Create temp files to match
        let temp_dir = std::env::temp_dir();
        let test_dir = temp_dir.join("test_glob_dir");
        std::fs::create_dir_all(&test_dir).ok();
        std::fs::write(test_dir.join("file1.txt"), "content1").ok();
        std::fs::write(test_dir.join("file2.txt"), "content2").ok();
        std::fs::write(test_dir.join("file3.md"), "content3").ok();

        let tool = GlobTool::new();
        let input = serde_json::json!({
            "pattern": format!("{}/**/*.txt", test_dir.to_str().unwrap())
        });
        let context = ToolContext::default();

        let result = tool.execute(input, &context).await;
        assert!(result.is_ok());
        let tool_result = result.unwrap();
        // Should find file1.txt and file2.txt
        assert!(tool_result.content.contains("file1.txt"));
        assert!(tool_result.content.contains("file2.txt"));
        // Should not contain file3.md
        assert!(!tool_result.content.contains("file3.md"));

        // Cleanup
        std::fs::remove_file(test_dir.join("file1.txt")).ok();
        std::fs::remove_file(test_dir.join("file2.txt")).ok();
        std::fs::remove_file(test_dir.join("file3.md")).ok();
        std::fs::remove_dir(test_dir).ok();
    }

    #[tokio::test]
    async fn test_glob_tool_returns_empty_for_no_matches() {
        let temp_dir = std::env::temp_dir();
        let test_dir = temp_dir.join("test_glob_empty");
        std::fs::create_dir_all(&test_dir).ok();

        let tool = GlobTool::new();
        let input = serde_json::json!({
            "pattern": format!("{}/**/*.nonexistent", test_dir.to_str().unwrap())
        });
        let context = ToolContext::default();

        let result = tool.execute(input, &context).await;
        assert!(result.is_ok());
        let tool_result = result.unwrap();
        // Should return empty or indicate no matches
        assert!(tool_result.content.is_empty() || tool_result.content.contains("No files found") || tool_result.content.contains("[]"));

        // Cleanup
        std::fs::remove_dir(test_dir).ok();
    }
}