- 1
use async_trait::async_trait; - 2
use serde_json::Value; - 3
- 4
use crate::{Tool, ToolContext, ToolOutput}; - 5
- 6
const DEFAULT_LIMIT: usize = 2000; - 7
const IMAGE_EXTS: [&str; 5] = ["jpg", "jpeg", "png", "gif", "webp"]; - 8
- 9
pub struct ReadTool; - 10
- 11
#[async_trait] - 12
impl Tool for ReadTool { - 13
fn name(&self) -> &str { - 14
"read" - 15
} - 16
- 17
fn serves(&self) -> &'static [&'static str] { - 18
&["filesystem"] - 19
} - 20
- 21
fn always_loaded(&self) -> bool { - 22
true - 23
} - 24
- 25
fn description(&self) -> &str { - 26
"Read a text file from disk. Returns numbered lines. Use offset/limit to page through large files. For Word, Excel, PowerPoint and Visio files use doc_read." - 27
} - 28
- 29
fn schema(&self) -> Value { - 30
serde_json::json!({ - 31
"type": "object", - 32
"properties": { - 33
"path": {"type": "string", "description": "File path (relative to cwd or absolute)"}, - 34
"offset": {"type": "integer", "minimum": 1, "description": "1-based start line"}, - 35
"limit": {"type": "integer", "minimum": 1, "description": "Max lines to return"} - 36
}, - 37
"required": ["path"] - 38
}) - 39
} - 40
- 41
fn refusal(&self, args: &Value) -> Option<String> { - 42
let path = args.get("path").and_then(Value::as_str)?; - 43
crate::office_apply::text_tool_refusal(std::path::Path::new(path), "read") - 44
} - 45
- 46
fn claims(&self, _args: &Value) -> crate::ResourceClaims { - 47
crate::ResourceClaims { - 48
exclusive: false, - 49
read_only: true, - 50
paths: Vec::new(), - 51
} - 52
} - 53
- 54
async fn execute(&self, args: &Value, ctx: &ToolContext) -> ToolOutput { - 55
let Some(path_str) = args.get("path").and_then(|p| p.as_str()) else { - 56
return ToolOutput::error("missing required parameter: path"); - 57
}; - 58
let path = ctx.resolve(std::path::Path::new(path_str)); - 59
if let Some(refusal) = crate::office_apply::text_tool_refusal(&path, "read") { - 60
return ToolOutput::error(refusal); - 61
} - 62
- 63
let ext = path - 64
.extension() - 65
.and_then(|e| e.to_str()) - 66
.map(|e| e.to_ascii_lowercase()) - 67
.unwrap_or_default(); - 68
if IMAGE_EXTS.contains(&ext.as_str()) { - 69
return ToolOutput::ok(format!( - 70
"[image file: {} — inline image rendering is not enabled in this build]", - 71
path.display() - 72
)); - 73
} - 74
- 75
let bytes = match tokio::fs::read(&path).await { - 76
Ok(b) => b, - 77
Err(e) => return ToolOutput::error(format!("cannot read {}: {e}", path.display())), - 78
}; - 79
if bytes.iter().take(8192).any(|&b| b == 0) { - 80
return ToolOutput::ok(format!("[binary file: {}]", path.display())); - 81
} - 82
- 83
let text = String::from_utf8_lossy(&bytes); - 84
let total_lines = text.lines().count(); - 85
let offset = args - 86
.get("offset") - 87
.and_then(|o| o.as_u64()) - 88
.unwrap_or(1) - 89
.max(1) as usize; - 90
let limit = args - 91
.get("limit") - 92
.and_then(|l| l.as_u64()) - 93
.map(|l| l as usize) - 94
.unwrap_or(DEFAULT_LIMIT); - 95
- 96
let start_idx = offset - 1; - 97
if start_idx >= total_lines { - 98
return ToolOutput::error(format!( - 99
"offset {offset} is beyond end of file ({total_lines} lines)" - 100
)); - 101
} - 102
let end_idx = (start_idx + limit).min(total_lines); - 103
- 104
let mut out = String::new(); - 105
for (i, line) in text - 106
.lines() - 107
.enumerate() - 108
.skip(start_idx) - 109
.take(end_idx - start_idx) - 110
{ - 111
out.push_str(&format!("{:>6}\t{line}\n", i + 1)); - 112
} - 113
if end_idx < total_lines { - 114
out.push_str(&format!( - 115
"\n[Showing lines {}–{} of {total_lines}. Use offset={} to continue.]\n", - 116
start_idx + 1, - 117
end_idx, - 118
end_idx + 1 - 119
)); - 120
} - 121
ToolOutput::ok(out) - 122
} - 123
} - 124
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.