1use serde_json::{json, Value};
2use crate::{Result, RuntimeError};
3use super::{Tool, ToolContext, expand_path};
4
5pub struct ReadTool;
6
7#[async_trait::async_trait]
8impl Tool for ReadTool {
9 fn name(&self) -> &str { "read" }
10
11 fn description(&self) -> &str {
12 "Read the contents of a file. Returns lines with line numbers. Reads up to 500 lines by default. For large files, use offset and limit to read in sections."
13 }
14
15 fn parameters(&self) -> Value {
16 json!({
17 "type": "object",
18 "properties": {
19 "path": {
20 "type": "string",
21 "description": "Path to the file to read"
22 },
23 "offset": {
24 "type": "integer",
25 "description": "Line number to start reading from (0-indexed, default: 0)"
26 },
27 "limit": {
28 "type": "integer",
29 "description": "Maximum number of lines to read (default: all lines)"
30 }
31 },
32 "required": ["path"]
33 })
34 }
35
36 async fn execute(&self, params: Value, _ctx: ToolContext) -> Result<String> {
37 let raw_path = params["path"].as_str()
38 .ok_or_else(|| RuntimeError::Tool("Missing path parameter".to_string()))?;
39 let path = expand_path(raw_path);
40
41 let bytes = tokio::fs::read(&path).await
43 .map_err(|e| RuntimeError::Tool(format!("Failed to read file '{}': {}", path.display(), e)))?;
44
45 let content = match String::from_utf8(bytes) {
46 Ok(s) => s,
47 Err(_) => return Err(RuntimeError::Tool(format!(
48 "File '{}' appears to be binary (not valid UTF-8). Use `bash` with `xxd` or `file` to inspect binary files.",
49 path.display()
50 ))),
51 };
52
53 let lines: Vec<&str> = content.lines().collect();
54 let total_lines = lines.len();
55
56 let offset = params["offset"].as_u64().unwrap_or(0) as usize;
57 let limit = params["limit"].as_u64().map(|l| l as usize).unwrap_or(500.min(total_lines));
58
59 let start = offset.min(total_lines);
60 let end = (start + limit).min(total_lines);
61
62 let mut result = String::new();
63 for (i, line) in lines[start..end].iter().enumerate() {
64 result.push_str(&format!("{}\t{}\n", start + i + 1, line));
65 }
66
67 if total_lines > end {
68 result.push_str(&format!("\n... ({} more lines)", total_lines - end));
69 }
70
71 Ok(result)
72 }
73}
74#[cfg(test)]
75mod tests {
76 use super::*;
77 use super::super::test_helpers::create_tool_context;
78 use crate::tools::Tool;
79 use serde_json::json;
80
81 #[test]
82 fn test_read_tool_schema() {
83 let tool = ReadTool;
84 assert_eq!(tool.name(), "read");
85 assert!(!tool.description().is_empty());
86
87 let params = tool.parameters();
88 assert_eq!(params["type"], "object");
89 assert!(params["properties"].is_object());
90 assert!(params["required"].is_array());
91 }
92
93 #[tokio::test]
94 async fn test_read_tool_execution() {
95 let temp_dir = std::env::temp_dir();
96 let test_file = temp_dir.join("read_tool_test.txt");
97
98 let content = "line 1\nline 2\nline 3\nline 4\nline 5";
100 std::fs::write(&test_file, content).unwrap();
101
102 let tool = ReadTool;
103 let ctx = create_tool_context();
104
105 let params = json!({
107 "path": test_file.to_string_lossy()
108 });
109 let result = tool.execute(params, ctx).await.unwrap();
110
111 assert!(result.contains("1\tline 1"));
113 assert!(result.contains("2\tline 2"));
114 assert!(result.contains("5\tline 5"));
115
116 let ctx = create_tool_context();
118 let params = json!({
119 "path": test_file.to_string_lossy(),
120 "offset": 2,
121 "limit": 2
122 });
123 let result = tool.execute(params, ctx).await.unwrap();
124
125 assert!(result.contains("3\tline 3"));
126 assert!(result.contains("4\tline 4"));
127 assert!(!result.contains("1\tline 1"));
128 assert!(!result.contains("5\tline 5"));
129
130 let _ = std::fs::remove_file(&test_file);
132 }
133
134 #[tokio::test]
135 async fn test_read_tool_offset() {
136 let temp_dir = std::env::temp_dir();
137 let test_file = temp_dir.join("test_read_tool_offset.txt");
138
139 let lines = (1..=10).map(|i| format!("line {}", i)).collect::<Vec<_>>();
141 let content = lines.join("\n");
142 std::fs::write(&test_file, &content).unwrap();
143
144 let tool = ReadTool;
145 let ctx = create_tool_context();
146
147 let params = json!({
149 "path": test_file.to_string_lossy(),
150 "offset": 5
151 });
152
153 let result = tool.execute(params, ctx).await.unwrap();
154
155 assert!(result.contains("6\tline 6"));
157 assert!(!result.contains("1\tline 1"));
159 assert!(!result.contains("5\tline 5"));
160
161 let _ = std::fs::remove_file(&test_file);
163 }
164}