| 19 | |
| 20 | |
| 21 | class FileReadTool(Tool): |
| 22 | @property |
| 23 | def name(self) -> str: |
| 24 | return "read_file" |
| 25 | |
| 26 | @property |
| 27 | def description(self) -> str: |
| 28 | return "Read the contents of a file. Returns numbered lines for easy reference." |
| 29 | |
| 30 | @property |
| 31 | def input_schema(self) -> dict[str, Any]: |
| 32 | return { |
| 33 | "type": "object", |
| 34 | "properties": { |
| 35 | "path": {"type": "string", "description": "Absolute or relative path to the file."}, |
| 36 | "offset": {"type": "integer", "description": "1-based start line (optional)."}, |
| 37 | "limit": {"type": "integer", "description": "Number of lines to read (optional)."}, |
| 38 | }, |
| 39 | "required": ["path"], |
| 40 | } |
| 41 | |
| 42 | def execute(self, params: dict[str, Any]) -> ToolResult: |
| 43 | filepath = Path(params["path"]).expanduser() |
| 44 | if not filepath.exists(): |
| 45 | return ToolResult(output=f"Error: file not found: {filepath}", is_error=True) |
| 46 | if not filepath.is_file(): |
| 47 | return ToolResult(output=f"Error: not a file: {filepath}", is_error=True) |
| 48 | if filepath.stat().st_size > MAX_FILE_SIZE: |
| 49 | return ToolResult(output=f"Error: file too large (>{MAX_FILE_SIZE} bytes)", is_error=True) |
| 50 | try: |
| 51 | lines = filepath.read_text(errors="replace").splitlines(keepends=True) |
| 52 | except Exception as exc: |
| 53 | return ToolResult(output=f"Error reading file: {exc}", is_error=True) |
| 54 | |
| 55 | offset = max(1, params.get("offset", 1)) |
| 56 | limit = params.get("limit") |
| 57 | selected = lines[offset - 1:] |
| 58 | if limit is not None and limit > 0: |
| 59 | selected = selected[:limit] |
| 60 | |
| 61 | numbered = [] |
| 62 | for i, line in enumerate(selected, start=offset): |
| 63 | numbered.append(f"{i:>6}|{line.rstrip()}") |
| 64 | return ToolResult(output="\n".join(numbered) or "(empty file)") |