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(¶ms.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}