Skip to main content

skill_tools/
read.rs

1use crate::{ToolDefinition, ToolError, ToolResult};
2use serde::Deserialize;
3use tokio::fs;
4
5#[derive(Debug, Deserialize)]
6pub struct ReadParams {
7    pub path: String,
8    #[serde(default)]
9    pub offset: Option<usize>,
10    #[serde(default)]
11    pub limit: Option<usize>,
12}
13
14#[derive(Clone)]
15pub struct ReadTool {
16    base_dir: String,
17}
18
19impl ReadTool {
20    pub fn new(base_dir: String) -> Self {
21        Self { base_dir }
22    }
23
24    fn resolve_path(&self, path: &str) -> std::path::PathBuf {
25        let path = std::path::Path::new(path);
26        if path.is_absolute() {
27            path.to_path_buf()
28        } else {
29            std::path::Path::new(&self.base_dir).join(path)
30        }
31    }
32
33    pub fn definition(&self) -> ToolDefinition {
34        ToolDefinition {
35            name: "read".to_string(),
36            description: "Read contents of a file".to_string(),
37            parameters: serde_json::json!({
38                "type": "object",
39                "properties": {
40                    "path": {
41                        "type": "string",
42                        "description": "Path to the file to read"
43                    },
44                    "offset": {
45                        "type": "number",
46                        "description": "Line number to start reading from (1-indexed, optional)"
47                    },
48                    "limit": {
49                        "type": "number",
50                        "description": "Maximum number of lines to read (optional)"
51                    }
52                },
53                "required": ["path"]
54            }),
55        }
56    }
57
58    pub async fn execute(&self, params: serde_json::Value) -> Result<ToolResult, ToolError> {
59        let params: ReadParams = serde_json::from_value(params)
60            .map_err(|e| ToolError::InvalidParameters(e.to_string()))?;
61
62        let path = self.resolve_path(&params.path);
63
64        match fs::read_to_string(&path).await {
65            Ok(content) => {
66                let lines: Vec<&str> = content.lines().collect();
67                let total_lines = lines.len();
68
69                let start = params
70                    .offset
71                    .unwrap_or(1)
72                    .saturating_sub(1)
73                    .min(total_lines);
74                let end = params
75                    .limit
76                    .map(|l| (start + l).min(total_lines))
77                    .unwrap_or(total_lines);
78
79                let selected: Vec<&str> = lines[start..end].to_vec();
80                let output = selected.join("\n");
81
82                Ok(ToolResult {
83                    success: true,
84                    output: if params.offset.is_some() || params.limit.is_some() {
85                        format!(
86                            "{} lines {}-{} of {}:\n\n{}",
87                            path.display(),
88                            start + 1,
89                            end,
90                            total_lines,
91                            output
92                        )
93                    } else {
94                        output
95                    },
96                    error: None,
97                })
98            }
99            Err(e) => Ok(ToolResult {
100                success: false,
101                output: String::new(),
102                error: Some(format!("Failed to read {}: {}", path.display(), e)),
103            }),
104        }
105    }
106}