Skip to main content

claude_rust_tools/infrastructure/
read_tool.rs

1use claude_rust_errors::{AppError, AppResult};
2use claude_rust_types::{PermissionLevel, Tool};
3use serde_json::{Value, json};
4
5pub struct ReadTool;
6
7#[async_trait::async_trait]
8impl Tool for ReadTool {
9    fn name(&self) -> &str {
10        "read"
11    }
12
13    fn description(&self) -> &str {
14        "Read the contents of a file. Returns numbered lines."
15    }
16
17    fn input_schema(&self) -> Value {
18        json!({
19            "type": "object",
20            "properties": {
21                "file_path": {
22                    "type": "string",
23                    "description": "Absolute path to the file to read"
24                },
25                "offset": {
26                    "type": "integer",
27                    "description": "Line number to start reading from (1-based)"
28                },
29                "limit": {
30                    "type": "integer",
31                    "description": "Maximum number of lines to read"
32                }
33            },
34            "required": ["file_path"]
35        })
36    }
37
38    fn permission_level(&self) -> PermissionLevel {
39        PermissionLevel::ReadOnly
40    }
41
42    async fn execute(&self, input: Value) -> AppResult<String> {
43        let path = input
44            .get("file_path")
45            .and_then(|v| v.as_str())
46            .ok_or_else(|| AppError::Tool("missing 'file_path' field".into()))?;
47
48        let offset = input
49            .get("offset")
50            .and_then(|v| v.as_u64())
51            .unwrap_or(1)
52            .max(1) as usize;
53
54        let limit = input
55            .get("limit")
56            .and_then(|v| v.as_u64())
57            .unwrap_or(2000) as usize;
58
59        tracing::info!(path, offset, limit, "reading file");
60
61        let content = tokio::fs::read_to_string(path)
62            .await
63            .map_err(|e| AppError::Tool(format!("cannot read '{path}': {e}")))?;
64
65        let lines: Vec<&str> = content.lines().collect();
66        let start = (offset - 1).min(lines.len());
67        let end = (start + limit).min(lines.len());
68
69        let mut result = String::new();
70        for (i, line) in lines[start..end].iter().enumerate() {
71            let line_num = start + i + 1;
72            result.push_str(&format!("{line_num:>6}\t{line}\n"));
73        }
74
75        if result.is_empty() {
76            result.push_str("(empty file)");
77        }
78
79        Ok(result)
80    }
81}