claude_rust_tools/infrastructure/
read_tool.rs1use claude_rust_errors::{AppError, AppResult};
2use claude_rust_types::{PermissionLevel, SearchReadInfo, 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 fn is_read_only(&self, _input: &Value) -> bool { true }
43 fn is_concurrent_safe(&self, _input: &Value) -> bool { true }
44
45 fn is_search_or_read_command(&self, _input: &Value) -> SearchReadInfo {
46 SearchReadInfo { is_search: false, is_read: true, is_list: false }
47 }
48
49 fn get_path(&self, input: &Value) -> Option<String> {
50 input.get("file_path").and_then(|v| v.as_str()).map(|s| s.to_string())
51 }
52
53 async fn execute(&self, input: Value) -> AppResult<String> {
54 let path = input
55 .get("file_path")
56 .and_then(|v| v.as_str())
57 .ok_or_else(|| AppError::Tool("missing 'file_path' field".into()))?;
58
59 let offset = input
60 .get("offset")
61 .and_then(|v| v.as_u64())
62 .unwrap_or(1)
63 .max(1) as usize;
64
65 let limit = input
66 .get("limit")
67 .and_then(|v| v.as_u64())
68 .unwrap_or(2000) as usize;
69
70 tracing::info!(path, offset, limit, "reading file");
71
72 let content = tokio::fs::read_to_string(path)
73 .await
74 .map_err(|e| AppError::Tool(format!("cannot read '{path}': {e}")))?;
75
76 let lines: Vec<&str> = content.lines().collect();
77 let start = (offset - 1).min(lines.len());
78 let end = (start + limit).min(lines.len());
79
80 let mut result = String::new();
81 for (i, line) in lines[start..end].iter().enumerate() {
82 let line_num = start + i + 1;
83 result.push_str(&format!("{line_num:>6}\t{line}\n"));
84 }
85
86 if result.is_empty() {
87 result.push_str("(empty file)");
88 }
89
90 Ok(result)
91 }
92}