Skip to main content

aster/tools/file/
read.rs

1//! Read Tool Implementation
2//!
3//! This module implements the `ReadTool` for reading files with:
4//! - Text file reading with line numbers
5//! - Image reading with base64 encoding
6//! - PDF reading (optional)
7//! - Jupyter notebook reading
8//! - File read history tracking
9//!
10//! Requirements: 4.1, 4.2, 4.3, 4.4, 4.5
11
12use std::fs;
13use std::path::{Path, PathBuf};
14
15use async_trait::async_trait;
16use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
17use serde::{Deserialize, Serialize};
18use tracing::debug;
19
20use super::{compute_content_hash, FileReadRecord, SharedFileReadHistory};
21use crate::tools::base::{PermissionCheckResult, Tool};
22use crate::tools::context::{ToolContext, ToolOptions, ToolResult};
23use crate::tools::error::ToolError;
24
25/// Maximum file size for text files (10MB)
26pub const MAX_TEXT_FILE_SIZE: u64 = 10 * 1024 * 1024;
27
28/// Maximum file size for images (50MB)
29pub const MAX_IMAGE_FILE_SIZE: u64 = 50 * 1024 * 1024;
30
31/// Supported image extensions
32const IMAGE_EXTENSIONS: &[&str] = &["png", "jpg", "jpeg", "gif", "webp", "bmp", "ico", "svg"];
33
34/// Supported text extensions (non-exhaustive, used for hints)
35const TEXT_EXTENSIONS: &[&str] = &[
36    "txt",
37    "md",
38    "rs",
39    "py",
40    "js",
41    "ts",
42    "jsx",
43    "tsx",
44    "json",
45    "yaml",
46    "yml",
47    "toml",
48    "xml",
49    "html",
50    "css",
51    "scss",
52    "less",
53    "sql",
54    "sh",
55    "bash",
56    "zsh",
57    "c",
58    "cpp",
59    "h",
60    "hpp",
61    "java",
62    "go",
63    "rb",
64    "php",
65    "swift",
66    "kt",
67    "scala",
68    "r",
69    "lua",
70    "pl",
71    "pm",
72    "ex",
73    "exs",
74    "erl",
75    "hrl",
76    "hs",
77    "ml",
78    "mli",
79    "fs",
80    "fsx",
81    "clj",
82    "cljs",
83    "lisp",
84    "el",
85    "vim",
86    "conf",
87    "ini",
88    "cfg",
89    "env",
90    "gitignore",
91    "dockerignore",
92    "makefile",
93    "cmake",
94    "gradle",
95];
96
97/// Line range for partial file reading
98#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
99pub struct LineRange {
100    /// Start line (1-indexed, inclusive)
101    pub start: usize,
102    /// End line (1-indexed, inclusive, None means to end of file)
103    pub end: Option<usize>,
104}
105
106impl LineRange {
107    /// Create a new line range
108    pub fn new(start: usize, end: Option<usize>) -> Self {
109        Self { start, end }
110    }
111
112    /// Create a range from start to end of file
113    pub fn from_start(start: usize) -> Self {
114        Self { start, end: None }
115    }
116
117    /// Create a range for a specific number of lines from start
118    pub fn lines(start: usize, count: usize) -> Self {
119        Self {
120            start,
121            end: Some(start + count - 1),
122        }
123    }
124}
125
126/// Read Tool for reading files
127///
128/// Supports reading:
129/// - Text files with line numbers
130/// - Images as base64
131/// - PDF files (optional)
132/// - Jupyter notebooks
133///
134/// Requirements: 4.1, 4.2, 4.3, 4.4, 4.5
135/// File analysis information for enhanced text reading
136#[derive(Debug)]
137struct TextFileInfo {
138    path: PathBuf,
139    extension: String,
140    language: Option<String>,
141    file_category: String,
142    size_bytes: u64,
143    total_lines: usize,
144}
145
146#[derive(Debug)]
147pub struct ReadTool {
148    /// Shared file read history
149    read_history: SharedFileReadHistory,
150    /// Whether PDF reading is enabled
151    pdf_enabled: bool,
152}
153
154impl ReadTool {
155    /// Create a new ReadTool with shared history
156    pub fn new(read_history: SharedFileReadHistory) -> Self {
157        Self {
158            read_history,
159            pdf_enabled: false,
160        }
161    }
162
163    /// Enable PDF reading
164    pub fn with_pdf_enabled(mut self, enabled: bool) -> Self {
165        self.pdf_enabled = enabled;
166        self
167    }
168
169    /// Get the shared read history
170    pub fn read_history(&self) -> &SharedFileReadHistory {
171        &self.read_history
172    }
173}
174
175// =============================================================================
176// Text File Reading (Requirements: 4.1)
177// =============================================================================
178
179impl ReadTool {
180    /// Read a text file with line numbers
181    ///
182    /// Returns the file content with line numbers prefixed.
183    /// Optionally reads only a specific line range.
184    ///
185    /// Requirements: 4.1
186    pub async fn read_text(
187        &self,
188        path: &Path,
189        range: Option<LineRange>,
190        context: &ToolContext,
191    ) -> Result<String, ToolError> {
192        let full_path = self.resolve_path(path, context);
193
194        // Check file exists
195        if !full_path.exists() {
196            return Err(ToolError::execution_failed(format!(
197                "File not found: {}",
198                full_path.display()
199            )));
200        }
201
202        // Check file size
203        let metadata = fs::metadata(&full_path)?;
204        if metadata.len() > MAX_TEXT_FILE_SIZE {
205            return Err(ToolError::execution_failed(format!(
206                "File too large: {} bytes (max: {} bytes)",
207                metadata.len(),
208                MAX_TEXT_FILE_SIZE
209            )));
210        }
211
212        // Read file content
213        let content = fs::read(&full_path)?;
214        let text = String::from_utf8_lossy(&content);
215
216        // Record the read
217        self.record_file_read(&full_path, &content, &metadata)?;
218
219        // Format with line numbers
220        let lines: Vec<&str> = text.lines().collect();
221        let total_lines = lines.len();
222
223        let (start, end) = match range {
224            Some(r) => {
225                let start = r.start.saturating_sub(1).min(total_lines);
226                let end = r.end.map(|e| e.min(total_lines)).unwrap_or(total_lines);
227                (start, end)
228            }
229            None => (0, total_lines),
230        };
231
232        // Calculate line number width for formatting
233        let line_width = (end.max(1)).to_string().len();
234
235        let formatted: Vec<String> = lines[start..end]
236            .iter()
237            .enumerate()
238            .map(|(i, line)| {
239                let line_num = start + i + 1;
240                format!("{:>width$} | {}", line_num, line, width = line_width)
241            })
242            .collect();
243
244        debug!(
245            "Read text file: {} ({} lines, showing {}-{})",
246            full_path.display(),
247            total_lines,
248            start + 1,
249            end
250        );
251
252        Ok(formatted.join("\n"))
253    }
254
255    /// Record a file read in the history
256    fn record_file_read(
257        &self,
258        path: &Path,
259        content: &[u8],
260        metadata: &fs::Metadata,
261    ) -> Result<(), ToolError> {
262        let hash = compute_content_hash(content);
263        let mtime = metadata.modified().ok();
264        let line_count = String::from_utf8_lossy(content).lines().count();
265
266        let mut record = FileReadRecord::new(path.to_path_buf(), hash, metadata.len())
267            .with_line_count(line_count);
268
269        if let Some(mt) = mtime {
270            record = record.with_mtime(mt);
271        }
272
273        self.read_history.write().unwrap().record_read(record);
274        Ok(())
275    }
276
277    /// Resolve a path relative to the working directory
278    fn resolve_path(&self, path: &Path, context: &ToolContext) -> PathBuf {
279        if path.is_absolute() {
280            path.to_path_buf()
281        } else {
282            context.working_directory.join(path)
283        }
284    }
285}
286
287// =============================================================================
288// Image Reading (Requirements: 4.2)
289// =============================================================================
290
291impl ReadTool {
292    /// Read an image file with enhanced analysis capabilities
293    ///
294    /// Enhanced version inspired by Claude Agent SDK:
295    /// - Provides detailed image metadata
296    /// - Estimates token consumption
297    /// - Supports intelligent image analysis
298    /// - Returns structured information for AI processing
299    ///
300    /// Requirements: 4.2
301    pub async fn read_image(
302        &self,
303        path: &Path,
304        context: &ToolContext,
305    ) -> Result<String, ToolError> {
306        let full_path = self.resolve_path(path, context);
307
308        // Check file exists
309        if !full_path.exists() {
310            return Err(ToolError::execution_failed(format!(
311                "Image not found: {}",
312                full_path.display()
313            )));
314        }
315
316        // Check file size
317        let metadata = fs::metadata(&full_path)?;
318        if metadata.len() > MAX_IMAGE_FILE_SIZE {
319            return Err(ToolError::execution_failed(format!(
320                "Image too large: {} bytes (max: {} bytes)",
321                metadata.len(),
322                MAX_IMAGE_FILE_SIZE
323            )));
324        }
325
326        // Use enhanced image processing from media module
327        let image_result = crate::media::read_image_file_enhanced(&full_path)
328            .map_err(|e| ToolError::execution_failed(format!("Failed to read image: {}", e)))?;
329
330        // Record the read
331        let content = fs::read(&full_path)?;
332        self.record_file_read(&full_path, &content, &metadata)?;
333
334        // Calculate enhanced metadata
335        let size_kb = (image_result.original_size as f64 / 1024.0).round() as u64;
336        let token_estimate = crate::media::estimate_image_tokens(&image_result.base64);
337
338        // Build enhanced output with analysis information
339        let mut output = Vec::new();
340        output.push(format!(
341            "[Enhanced Image Analysis: {}]",
342            full_path.display()
343        ));
344        output.push(format!("Format: {}", image_result.mime_type));
345        output.push(format!(
346            "Size: {} KB ({} bytes)",
347            size_kb, image_result.original_size
348        ));
349
350        if let Some(dims) = &image_result.dimensions {
351            if let (Some(w), Some(h)) = (dims.original_width, dims.original_height) {
352                output.push(format!("Original dimensions: {}x{}", w, h));
353                if let (Some(dw), Some(dh)) = (dims.display_width, dims.display_height) {
354                    if dw != w || dh != h {
355                        output.push(format!("Display dimensions: {}x{} (resized)", dw, dh));
356                    }
357                }
358            }
359        }
360
361        output.push(format!("Estimated tokens: {}", token_estimate));
362
363        // Add analysis hints for AI processing
364        output.push(String::new());
365        output.push("AI Analysis Capabilities:".to_string());
366        output.push("- Content recognition and description".to_string());
367        output.push("- Text extraction (OCR)".to_string());
368        output.push("- Object and scene detection".to_string());
369        output.push("- Color analysis and composition".to_string());
370        output.push("- Technical diagram interpretation".to_string());
371        output.push("- Screenshot analysis and UI element identification".to_string());
372
373        debug!(
374            "Enhanced image read: {} ({} KB, {} tokens, {})",
375            full_path.display(),
376            size_kb,
377            token_estimate,
378            image_result.mime_type
379        );
380
381        // Return formatted analysis with base64 data
382        Ok(format!(
383            "{}\n\nBase64 Data: data:{};base64,{}",
384            output.join("\n"),
385            image_result.mime_type,
386            image_result.base64
387        ))
388    }
389
390    /// Check if a file is an image based on extension (uses media module)
391    pub fn is_image_file(path: &Path) -> bool {
392        let ext = path
393            .extension()
394            .and_then(|e| e.to_str())
395            .unwrap_or("")
396            .to_lowercase();
397        crate::media::is_supported_image_format(&ext)
398    }
399}
400
401// =============================================================================
402// PDF Reading (Requirements: 4.3)
403// =============================================================================
404
405impl ReadTool {
406    /// Read a PDF file with enhanced processing capabilities
407    ///
408    /// Enhanced version inspired by Claude Agent SDK:
409    /// - Provides detailed PDF metadata
410    /// - Supports document block processing for AI analysis
411    /// - Returns structured information for multimodal AI processing
412    /// - Includes content extraction hints
413    ///
414    /// Note: PDF reading requires external dependencies and is disabled by default.
415    /// When enabled, extracts text content from PDF files and prepares them
416    /// for AI analysis with document blocks.
417    ///
418    /// Requirements: 4.3
419    pub async fn read_pdf(&self, path: &Path, context: &ToolContext) -> Result<String, ToolError> {
420        if !self.pdf_enabled {
421            return Err(ToolError::execution_failed(
422                "PDF reading is not enabled. Enable it with ReadTool::with_pdf_enabled(true)",
423            ));
424        }
425
426        let full_path = self.resolve_path(path, context);
427
428        // Check file exists
429        if !full_path.exists() {
430            return Err(ToolError::execution_failed(format!(
431                "PDF not found: {}",
432                full_path.display()
433            )));
434        }
435
436        // Read file content for history tracking and analysis
437        let content = fs::read(&full_path)?;
438        let metadata = fs::metadata(&full_path)?;
439        self.record_file_read(&full_path, &content, &metadata)?;
440
441        // Calculate enhanced metadata
442        let size_mb = (metadata.len() as f64 / 1_048_576.0 * 100.0).round() / 100.0;
443        let base64_content = BASE64.encode(&content);
444        let base64_length = base64_content.len();
445
446        // Build enhanced output with analysis information
447        let mut output = Vec::new();
448        output.push(format!("[Enhanced PDF Analysis: {}]", full_path.display()));
449        output.push(format!("Size: {} MB ({} bytes)", size_mb, metadata.len()));
450        output.push(format!("Base64 length: {} chars", base64_length));
451        output.push(String::new());
452
453        // Add analysis capabilities information
454        output.push("AI Analysis Capabilities:".to_string());
455        output.push("- Document structure analysis".to_string());
456        output.push("- Text extraction and content analysis".to_string());
457        output.push("- Table and form recognition".to_string());
458        output.push("- Image and diagram extraction".to_string());
459        output.push("- Layout and formatting analysis".to_string());
460        output.push("- Multi-page document processing".to_string());
461        output.push(String::new());
462
463        // Add processing hints
464        output.push("Processing Notes:".to_string());
465        output
466            .push("- PDF content will be processed as document blocks for AI analysis".to_string());
467        output.push("- Large PDFs may be processed in chunks for optimal performance".to_string());
468        output.push("- Text and visual elements will be analyzed together".to_string());
469
470        debug!(
471            "Enhanced PDF read: {} ({} MB, {} base64 chars)",
472            full_path.display(),
473            size_mb,
474            base64_length
475        );
476
477        // Return enhanced analysis information
478        // Note: In a full implementation, this would include the actual PDF processing
479        // and document block creation for AI analysis
480        Ok(format!("{}\n\nDocument ready for AI analysis.\nBase64 data available for multimodal processing.", 
481                   output.join("\n")))
482    }
483
484    /// Check if a file is a PDF (uses media module)
485    pub fn is_pdf_file(path: &Path) -> bool {
486        let ext = path
487            .extension()
488            .and_then(|e| e.to_str())
489            .unwrap_or("")
490            .to_lowercase();
491        crate::media::is_pdf_extension(&ext)
492    }
493}
494
495// =============================================================================
496// Jupyter Notebook Reading (Requirements: 4.4)
497// =============================================================================
498
499impl ReadTool {
500    /// Read an SVG file with enhanced rendering capabilities
501    ///
502    /// Enhanced version inspired by Claude Agent SDK:
503    /// - Supports SVG content analysis
504    /// - Provides rendering information
505    /// - Includes vector graphics analysis capabilities
506    /// - Returns structured information for AI processing
507    ///
508    /// Requirements: 4.2 (extended)
509    pub async fn read_svg(&self, path: &Path, context: &ToolContext) -> Result<String, ToolError> {
510        let full_path = self.resolve_path(path, context);
511
512        // Check file exists
513        if !full_path.exists() {
514            return Err(ToolError::execution_failed(format!(
515                "SVG not found: {}",
516                full_path.display()
517            )));
518        }
519
520        // Read file content
521        let content = fs::read(&full_path)?;
522        let metadata = fs::metadata(&full_path)?;
523        let svg_text = String::from_utf8_lossy(&content);
524
525        // Record the read
526        self.record_file_read(&full_path, &content, &metadata)?;
527
528        // Calculate metadata
529        let size_kb = (metadata.len() as f64 / 1024.0).round() as u64;
530
531        // Build enhanced output with analysis information
532        let mut output = Vec::new();
533        output.push(format!("[Enhanced SVG Analysis: {}]", full_path.display()));
534        output.push(format!("Size: {} KB ({} bytes)", size_kb, metadata.len()));
535        output.push("Content type: Scalable Vector Graphics".to_string());
536        output.push(String::new());
537
538        // Add analysis capabilities information
539        output.push("AI Analysis Capabilities:".to_string());
540        output.push("- Vector graphics structure analysis".to_string());
541        output.push("- Shape and path recognition".to_string());
542        output.push("- Text content extraction".to_string());
543        output.push("- Color scheme analysis".to_string());
544        output.push("- Diagram and flowchart interpretation".to_string());
545        output.push("- Icon and symbol recognition".to_string());
546        output.push(String::new());
547
548        // Add SVG content preview (first few lines)
549        output.push("SVG Content Preview:".to_string());
550        let lines: Vec<&str> = svg_text.lines().take(10).collect();
551        for (i, line) in lines.iter().enumerate() {
552            let trimmed = line.trim();
553            if !trimmed.is_empty() {
554                output.push(format!("  {}: {}", i + 1, trimmed));
555            }
556        }
557
558        if svg_text.lines().count() > 10 {
559            output.push("  ... (content truncated)".to_string());
560        }
561
562        debug!(
563            "Enhanced SVG read: {} ({} KB)",
564            full_path.display(),
565            size_kb
566        );
567
568        // Return enhanced analysis with full SVG content
569        Ok(format!(
570            "{}\n\nFull SVG Content:\n{}",
571            output.join("\n"),
572            svg_text
573        ))
574    }
575
576    /// Read a Jupyter notebook file with enhanced analysis
577    ///
578    /// Enhanced version inspired by Claude Agent SDK:
579    /// - Extracts and formats code cells and markdown cells
580    /// - Provides execution output analysis
581    /// - Includes data visualization detection
582    /// - Returns structured information for AI processing
583    ///
584    /// Requirements: 4.4
585    pub async fn read_notebook(
586        &self,
587        path: &Path,
588        context: &ToolContext,
589    ) -> Result<String, ToolError> {
590        let full_path = self.resolve_path(path, context);
591
592        // Check file exists and read content
593        let (content, metadata, notebook) = self.load_notebook_file(&full_path)?;
594
595        // Record the read
596        self.record_file_read(&full_path, &content, &metadata)?;
597
598        // Extract cells and build output
599        let cells = self.extract_notebook_cells(&notebook)?;
600        let output = self.build_notebook_output(&full_path, &metadata, cells);
601
602        debug!(
603            "Enhanced notebook read: {} ({} cells)",
604            full_path.display(),
605            cells.len()
606        );
607
608        Ok(output.join("\n"))
609    }
610
611    /// Add notebook header and statistics
612    fn add_notebook_header(
613        &self,
614        output: &mut Vec<String>,
615        full_path: &Path,
616        metadata: &fs::Metadata,
617        cells: &[serde_json::Value],
618    ) {
619        output.push(format!(
620            "[Enhanced Notebook Analysis: {}]",
621            full_path.display()
622        ));
623        output.push(format!(
624            "Size: {} KB",
625            (metadata.len() as f64 / 1024.0).round() as u64
626        ));
627        output.push(format!("Total cells: {}", cells.len()));
628
629        // Analyze cell types
630        let (code_cells, markdown_cells, other_cells) = self.analyze_cell_types(cells);
631        output.push(format!(
632            "Code cells: {}, Markdown cells: {}, Other: {}",
633            code_cells, markdown_cells, other_cells
634        ));
635        output.push(String::new());
636    }
637
638    /// Load and parse notebook file
639    fn load_notebook_file(
640        &self,
641        full_path: &Path,
642    ) -> Result<(Vec<u8>, fs::Metadata, serde_json::Value), ToolError> {
643        // Check file exists
644        if !full_path.exists() {
645            return Err(ToolError::execution_failed(format!(
646                "Notebook not found: {}",
647                full_path.display()
648            )));
649        }
650
651        // Read and parse JSON
652        let content = fs::read(full_path)?;
653        let metadata = fs::metadata(full_path)?;
654
655        let notebook: serde_json::Value = serde_json::from_slice(&content).map_err(|e| {
656            ToolError::execution_failed(format!("Failed to parse notebook JSON: {}", e))
657        })?;
658
659        Ok((content, metadata, notebook))
660    }
661
662    /// Extract cells from notebook JSON
663    fn extract_notebook_cells<'a>(
664        &self,
665        notebook: &'a serde_json::Value,
666    ) -> Result<&'a Vec<serde_json::Value>, ToolError> {
667        notebook
668            .get("cells")
669            .and_then(|c| c.as_array())
670            .ok_or_else(|| ToolError::execution_failed("Invalid notebook format: missing cells"))
671    }
672
673    /// Build complete notebook output
674    fn build_notebook_output(
675        &self,
676        full_path: &Path,
677        metadata: &fs::Metadata,
678        cells: &[serde_json::Value],
679    ) -> Vec<String> {
680        let mut output = Vec::new();
681
682        // Add header and statistics
683        self.add_notebook_header(&mut output, full_path, metadata, cells);
684
685        // Add analysis capabilities
686        self.add_analysis_capabilities(&mut output);
687
688        // Process each cell
689        self.process_notebook_cells(&mut output, cells);
690
691        output
692    }
693
694    /// Analyze cell types and return counts
695    fn analyze_cell_types(&self, cells: &[serde_json::Value]) -> (usize, usize, usize) {
696        let mut code_cells = 0;
697        let mut markdown_cells = 0;
698        let mut other_cells = 0;
699
700        for cell in cells {
701            match cell
702                .get("cell_type")
703                .and_then(|t| t.as_str())
704                .unwrap_or("unknown")
705            {
706                "code" => code_cells += 1,
707                "markdown" => markdown_cells += 1,
708                _ => other_cells += 1,
709            }
710        }
711
712        (code_cells, markdown_cells, other_cells)
713    }
714
715    /// Add analysis capabilities description
716    fn add_analysis_capabilities(&self, output: &mut Vec<String>) {
717        output.push("AI Analysis Capabilities:".to_string());
718        output.push("- Code execution flow analysis".to_string());
719        output.push("- Data visualization interpretation".to_string());
720        output.push("- Scientific computation analysis".to_string());
721        output.push("- Documentation and markdown processing".to_string());
722        output.push("- Output and result interpretation".to_string());
723        output.push("- Machine learning workflow analysis".to_string());
724        output.push(String::new());
725    }
726
727    /// Process all notebook cells
728    fn process_notebook_cells(&self, output: &mut Vec<String>, cells: &[serde_json::Value]) {
729        for (i, cell) in cells.iter().enumerate() {
730            self.process_single_cell(output, cell, i + 1);
731            output.push(String::new());
732        }
733    }
734
735    /// Process a single notebook cell
736    fn process_single_cell(
737        &self,
738        output: &mut Vec<String>,
739        cell: &serde_json::Value,
740        cell_num: usize,
741    ) {
742        let cell_type = cell
743            .get("cell_type")
744            .and_then(|t| t.as_str())
745            .unwrap_or("unknown");
746
747        let source = cell
748            .get("source")
749            .map(|s| self.extract_cell_source(s))
750            .unwrap_or_default();
751
752        match cell_type {
753            "code" => {
754                output.push(format!("## Cell {} [Code Cell] 🐍", cell_num));
755                output.push("```python".to_string());
756                output.push(source);
757                output.push("```".to_string());
758
759                // Include outputs if present
760                self.process_cell_outputs(output, cell);
761            }
762            "markdown" => {
763                output.push(format!("## Cell {} [Markdown Cell] 📝", cell_num));
764                output.push(source);
765            }
766            _ => {
767                output.push(format!("## Cell {} [{}] ❓", cell_num, cell_type));
768                output.push(source);
769            }
770        }
771    }
772
773    /// Process cell outputs
774    fn process_cell_outputs(&self, output: &mut Vec<String>, cell: &serde_json::Value) {
775        if let Some(outputs) = cell.get("outputs").and_then(|o| o.as_array()) {
776            if !outputs.is_empty() {
777                output.push("### Execution Output:".to_string());
778                for (out_idx, out) in outputs.iter().enumerate() {
779                    if let Some(text) = self.extract_output_text(out) {
780                        output.push(format!(
781                            "#### Output {} [{}]:",
782                            out_idx + 1,
783                            out.get("output_type")
784                                .and_then(|t| t.as_str())
785                                .unwrap_or("result")
786                        ));
787                        output.push("```".to_string());
788                        output.push(text);
789                        output.push("```".to_string());
790                    }
791                }
792            }
793        }
794    }
795
796    /// Extract source from a cell (handles both string and array formats)
797    fn extract_cell_source(&self, source: &serde_json::Value) -> String {
798        match source {
799            serde_json::Value::String(s) => s.clone(),
800            serde_json::Value::Array(arr) => arr
801                .iter()
802                .filter_map(|v| v.as_str())
803                .collect::<Vec<_>>()
804                .join(""),
805            _ => String::new(),
806        }
807    }
808
809    /// Extract text from cell output
810    fn extract_output_text(&self, output: &serde_json::Value) -> Option<String> {
811        // Try "text" field first (stream output)
812        if let Some(text) = output.get("text") {
813            return Some(self.extract_cell_source(text));
814        }
815
816        // Try "data" -> "text/plain" (execute_result)
817        if let Some(data) = output.get("data") {
818            if let Some(text) = data.get("text/plain") {
819                return Some(self.extract_cell_source(text));
820            }
821        }
822
823        None
824    }
825
826    /// Check if a file is a Jupyter notebook
827    pub fn is_notebook_file(path: &Path) -> bool {
828        path.extension()
829            .and_then(|e| e.to_str())
830            .map(|e| e.to_lowercase() == "ipynb")
831            .unwrap_or(false)
832    }
833}
834
835// =============================================================================
836// Tool Trait Implementation
837// =============================================================================
838
839#[async_trait]
840impl Tool for ReadTool {
841    fn name(&self) -> &str {
842        "read"
843    }
844
845    fn description(&self) -> &str {
846        "Enhanced multimodal file reader with intelligent analysis capabilities. \
847         Supports text files (with syntax highlighting and language detection), \
848         images (with metadata and AI analysis hints), PDF files (with document processing), \
849         SVG files (with vector graphics analysis), and Jupyter notebooks (with computational analysis). \
850         Automatically detects file type and provides structured information optimized for AI processing. \
851         Inspired by Claude Agent SDK for comprehensive file understanding and analysis."
852    }
853
854    fn input_schema(&self) -> serde_json::Value {
855        serde_json::json!({
856            "type": "object",
857            "properties": {
858                "path": {
859                    "type": "string",
860                    "description": "Path to the file to read (relative to working directory or absolute)"
861                },
862                "start_line": {
863                    "type": "integer",
864                    "description": "Start line number (1-indexed, for text files only)",
865                    "minimum": 1
866                },
867                "end_line": {
868                    "type": "integer",
869                    "description": "End line number (1-indexed, inclusive, for text files only)",
870                    "minimum": 1
871                }
872            },
873            "required": ["path"]
874        })
875    }
876
877    async fn execute(
878        &self,
879        params: serde_json::Value,
880        context: &ToolContext,
881    ) -> Result<ToolResult, ToolError> {
882        // Check for cancellation
883        if context.is_cancelled() {
884            return Err(ToolError::Cancelled);
885        }
886
887        // Extract path parameter
888        let path_str = params
889            .get("path")
890            .and_then(|v| v.as_str())
891            .ok_or_else(|| ToolError::invalid_params("Missing required parameter: path"))?;
892
893        let path = Path::new(path_str);
894
895        // Determine file type and read accordingly with enhanced analysis
896        if Self::is_image_file(path) {
897            let content = self.read_image(path, context).await?;
898            return Ok(ToolResult::success(content)
899                .with_metadata("file_type", serde_json::json!("image"))
900                .with_metadata("analysis_type", serde_json::json!("enhanced_multimodal")));
901        }
902
903        if Self::is_pdf_file(path) {
904            let content = self.read_pdf(path, context).await?;
905            return Ok(ToolResult::success(content)
906                .with_metadata("file_type", serde_json::json!("pdf"))
907                .with_metadata("analysis_type", serde_json::json!("enhanced_document")));
908        }
909
910        if Self::is_svg_file(path) {
911            let content = self.read_svg(path, context).await?;
912            return Ok(ToolResult::success(content)
913                .with_metadata("file_type", serde_json::json!("svg"))
914                .with_metadata("analysis_type", serde_json::json!("enhanced_vector")));
915        }
916
917        if Self::is_notebook_file(path) {
918            let content = self.read_notebook(path, context).await?;
919            return Ok(ToolResult::success(content)
920                .with_metadata("file_type", serde_json::json!("notebook"))
921                .with_metadata("analysis_type", serde_json::json!("enhanced_computational")));
922        }
923
924        // Enhanced text file reading with intelligent analysis
925        let range = self.extract_line_range(&params);
926        let content = self.read_text_enhanced(path, range, context).await?;
927
928        Ok(ToolResult::success(content)
929            .with_metadata("file_type", serde_json::json!("text"))
930            .with_metadata("analysis_type", serde_json::json!("enhanced_textual")))
931    }
932
933    async fn check_permissions(
934        &self,
935        params: &serde_json::Value,
936        context: &ToolContext,
937    ) -> PermissionCheckResult {
938        // Extract path for permission check
939        let path_str = match params.get("path").and_then(|v| v.as_str()) {
940            Some(p) => p,
941            None => return PermissionCheckResult::deny("Missing path parameter"),
942        };
943
944        let path = Path::new(path_str);
945        let full_path = self.resolve_path(path, context);
946
947        // Check if path is within allowed directories
948        // For now, allow all reads (permission manager handles restrictions)
949        debug!("Permission check for read: {}", full_path.display());
950
951        PermissionCheckResult::allow()
952    }
953
954    fn options(&self) -> ToolOptions {
955        ToolOptions::new()
956            .with_max_retries(1)
957            .with_base_timeout(std::time::Duration::from_secs(30))
958    }
959}
960
961impl ReadTool {
962    /// Extract line range from parameters
963    fn extract_line_range(&self, params: &serde_json::Value) -> Option<LineRange> {
964        let start = params
965            .get("start_line")
966            .and_then(|v| v.as_u64())
967            .map(|v| v as usize);
968        let end = params
969            .get("end_line")
970            .and_then(|v| v.as_u64())
971            .map(|v| v as usize);
972
973        match (start, end) {
974            (Some(s), e) => Some(LineRange::new(s, e)),
975            (None, Some(e)) => Some(LineRange::new(1, Some(e))),
976            (None, None) => None,
977        }
978    }
979
980    /// Read a text file with enhanced analysis capabilities
981    ///
982    /// Enhanced version inspired by Claude Agent SDK:
983    /// - Provides intelligent content analysis
984    /// - Detects programming languages and file types
985    /// - Includes syntax highlighting hints
986    /// - Returns structured information for AI processing
987    ///
988    /// Requirements: 4.1
989    pub async fn read_text_enhanced(
990        &self,
991        path: &Path,
992        range: Option<LineRange>,
993        context: &ToolContext,
994    ) -> Result<String, ToolError> {
995        let full_path = self.resolve_path(path, context);
996
997        // Load and validate file
998        let (content, metadata, text) = self.load_text_file(&full_path)?;
999
1000        // Record the read
1001        self.record_file_read(&full_path, &content, &metadata)?;
1002
1003        // Analyze and format content
1004        let file_info = self.analyze_text_file(&full_path, &text, &metadata);
1005        let formatted_content = self.format_text_with_lines(&text, range);
1006        let output = self.build_text_analysis_output(&file_info, &formatted_content, range);
1007
1008        debug!(
1009            "Enhanced text read: {} ({} lines, {}, {})",
1010            full_path.display(),
1011            file_info.total_lines,
1012            file_info.file_category,
1013            file_info.language.unwrap_or_else(|| "unknown".to_string())
1014        );
1015
1016        Ok(output.join("\n"))
1017    }
1018
1019    /// Analyze text file and extract metadata
1020    fn analyze_text_file(&self, path: &Path, text: &str, metadata: &fs::Metadata) -> TextFileInfo {
1021        let extension = path
1022            .extension()
1023            .and_then(|e| e.to_str())
1024            .unwrap_or("")
1025            .to_lowercase();
1026
1027        let language = self.detect_programming_language(&extension, text);
1028        let file_category = self.categorize_file_type(&extension);
1029        let total_lines = text.lines().count();
1030
1031        TextFileInfo {
1032            path: path.to_path_buf(),
1033            extension,
1034            language,
1035            file_category,
1036            size_bytes: metadata.len(),
1037            total_lines,
1038        }
1039    }
1040
1041    /// Load and validate text file
1042    fn load_text_file(
1043        &self,
1044        full_path: &Path,
1045    ) -> Result<(Vec<u8>, fs::Metadata, String), ToolError> {
1046        // Check file exists and size
1047        if !full_path.exists() {
1048            return Err(ToolError::execution_failed(format!(
1049                "File not found: {}",
1050                full_path.display()
1051            )));
1052        }
1053
1054        let metadata = fs::metadata(full_path)?;
1055        if metadata.len() > MAX_TEXT_FILE_SIZE {
1056            return Err(ToolError::execution_failed(format!(
1057                "File too large: {} bytes (max: {} bytes)",
1058                metadata.len(),
1059                MAX_TEXT_FILE_SIZE
1060            )));
1061        }
1062
1063        // Read and process file
1064        let content = fs::read(full_path)?;
1065        let text = String::from_utf8_lossy(&content).to_string();
1066
1067        Ok((content, metadata, text))
1068    }
1069
1070    /// Format text content with line numbers
1071    fn format_text_with_lines(&self, text: &str, range: Option<LineRange>) -> Vec<String> {
1072        let lines: Vec<&str> = text.lines().collect();
1073        let total_lines = lines.len();
1074
1075        let (start, end) = match range {
1076            Some(r) => {
1077                let start = r.start.saturating_sub(1).min(total_lines);
1078                let end = r.end.map(|e| e.min(total_lines)).unwrap_or(total_lines);
1079                (start, end)
1080            }
1081            None => (0, total_lines),
1082        };
1083
1084        let line_width = (end.max(1)).to_string().len();
1085
1086        lines[start..end]
1087            .iter()
1088            .enumerate()
1089            .map(|(i, line)| {
1090                let line_num = start + i + 1;
1091                format!("{:>width$} | {}", line_num, line, width = line_width)
1092            })
1093            .collect()
1094    }
1095
1096    /// Build enhanced text analysis output
1097    fn build_text_analysis_output(
1098        &self,
1099        file_info: &TextFileInfo,
1100        formatted_content: &[String],
1101        range: Option<LineRange>,
1102    ) -> Vec<String> {
1103        let mut output = Vec::new();
1104
1105        // Add header information
1106        output.push(format!(
1107            "[Enhanced Text Analysis: {}]",
1108            file_info.path.display()
1109        ));
1110        output.push(format!(
1111            "File type: {} ({})",
1112            file_info.file_category, file_info.extension
1113        ));
1114        if let Some(lang) = &file_info.language {
1115            output.push(format!("Programming language: {}", lang));
1116        }
1117        output.push(format!(
1118            "Size: {} KB ({} bytes)",
1119            (file_info.size_bytes as f64 / 1024.0).round() as u64,
1120            file_info.size_bytes
1121        ));
1122
1123        // Add line information
1124        let (start, end) = self.get_display_range(file_info.total_lines, range);
1125        output.push(format!(
1126            "Lines: {} total, showing {}-{}",
1127            file_info.total_lines, start, end
1128        ));
1129        output.push(String::new());
1130
1131        // Add analysis capabilities
1132        self.add_text_analysis_capabilities(&mut output, &file_info.file_category);
1133
1134        // Add formatted content
1135        output.push("File Content:".to_string());
1136        output.extend_from_slice(formatted_content);
1137
1138        output
1139    }
1140
1141    /// Get display range for line information
1142    fn get_display_range(&self, total_lines: usize, range: Option<LineRange>) -> (usize, usize) {
1143        match range {
1144            Some(r) => {
1145                let start = r.start.min(total_lines + 1);
1146                let end = r.end.map(|e| e.min(total_lines)).unwrap_or(total_lines);
1147                (start, end)
1148            }
1149            None => (1, total_lines),
1150        }
1151    }
1152
1153    /// Add analysis capabilities based on file type
1154    fn add_text_analysis_capabilities(&self, output: &mut Vec<String>, file_category: &str) {
1155        output.push("AI Analysis Capabilities:".to_string());
1156        match file_category {
1157            "Source Code" => {
1158                output.push("- Code structure and syntax analysis".to_string());
1159                output.push("- Function and class identification".to_string());
1160                output.push("- Code quality and best practices review".to_string());
1161                output.push("- Bug detection and security analysis".to_string());
1162                output.push("- Documentation and comment analysis".to_string());
1163            }
1164            "Configuration" => {
1165                output.push("- Configuration structure analysis".to_string());
1166                output.push("- Setting validation and optimization".to_string());
1167                output.push("- Dependency and version management".to_string());
1168                output.push("- Security configuration review".to_string());
1169            }
1170            "Documentation" => {
1171                output.push("- Content structure and organization".to_string());
1172                output.push("- Writing quality and clarity analysis".to_string());
1173                output.push("- Link and reference validation".to_string());
1174                output.push("- Documentation completeness review".to_string());
1175            }
1176            _ => {
1177                output.push("- Content analysis and understanding".to_string());
1178                output.push("- Structure and format recognition".to_string());
1179                output.push("- Data extraction and processing".to_string());
1180                output.push("- Pattern recognition and insights".to_string());
1181            }
1182        }
1183        output.push(String::new());
1184    }
1185
1186    /// Detect programming language from extension and content
1187    fn detect_programming_language(&self, extension: &str, content: &str) -> Option<String> {
1188        match extension {
1189            "rs" => Some("Rust".to_string()),
1190            "py" => Some("Python".to_string()),
1191            "js" => Some("JavaScript".to_string()),
1192            "ts" => Some("TypeScript".to_string()),
1193            "jsx" => Some("React JSX".to_string()),
1194            "tsx" => Some("React TSX".to_string()),
1195            "java" => Some("Java".to_string()),
1196            "c" => Some("C".to_string()),
1197            "cpp" | "cc" | "cxx" => Some("C++".to_string()),
1198            "h" | "hpp" => Some("C/C++ Header".to_string()),
1199            "go" => Some("Go".to_string()),
1200            "rb" => Some("Ruby".to_string()),
1201            "php" => Some("PHP".to_string()),
1202            "swift" => Some("Swift".to_string()),
1203            "kt" => Some("Kotlin".to_string()),
1204            "scala" => Some("Scala".to_string()),
1205            "sh" | "bash" | "zsh" => Some("Shell Script".to_string()),
1206            "sql" => Some("SQL".to_string()),
1207            "html" => Some("HTML".to_string()),
1208            "css" => Some("CSS".to_string()),
1209            "scss" | "sass" => Some("SCSS/Sass".to_string()),
1210            "xml" => Some("XML".to_string()),
1211            "json" => Some("JSON".to_string()),
1212            "yaml" | "yml" => Some("YAML".to_string()),
1213            "toml" => Some("TOML".to_string()),
1214            "md" => Some("Markdown".to_string()),
1215            _ => {
1216                // Try to detect from content
1217                if content.starts_with("#!/bin/bash") || content.starts_with("#!/bin/sh") {
1218                    Some("Shell Script".to_string())
1219                } else if content.starts_with("#!/usr/bin/env python") {
1220                    Some("Python".to_string())
1221                } else if content.starts_with("#!/usr/bin/env node") {
1222                    Some("JavaScript".to_string())
1223                } else {
1224                    None
1225                }
1226            }
1227        }
1228    }
1229
1230    /// Categorize file type for analysis
1231    fn categorize_file_type(&self, extension: &str) -> String {
1232        match extension {
1233            "rs" | "py" | "js" | "ts" | "jsx" | "tsx" | "java" | "c" | "cpp" | "cc" | "cxx"
1234            | "h" | "hpp" | "go" | "rb" | "php" | "swift" | "kt" | "scala" | "sh" | "bash"
1235            | "zsh" | "sql" => "Source Code".to_string(),
1236
1237            "json" | "yaml" | "yml" | "toml" | "xml" | "ini" | "cfg" | "conf" | "env" => {
1238                "Configuration".to_string()
1239            }
1240
1241            "md" | "txt" | "rst" | "adoc" => "Documentation".to_string(),
1242
1243            "html" | "css" | "scss" | "sass" | "less" => "Web Content".to_string(),
1244
1245            "csv" | "tsv" | "log" => "Data File".to_string(),
1246
1247            _ => "Text File".to_string(),
1248        }
1249    }
1250
1251    /// Check if a file is likely a text file based on extension (enhanced version)
1252    pub fn is_text_file(path: &Path) -> bool {
1253        match path.extension().and_then(|e| e.to_str()) {
1254            Some(ext) => {
1255                let ext_lower = ext.to_lowercase();
1256                // If it's a known text extension, return true
1257                // If it's a known non-text extension (image, pdf, notebook, svg), return false
1258                // Otherwise, default to true (assume text)
1259                if TEXT_EXTENSIONS.contains(&ext_lower.as_str()) {
1260                    true
1261                } else if IMAGE_EXTENSIONS.contains(&ext_lower.as_str())
1262                    || ext_lower == "pdf"
1263                    || ext_lower == "ipynb"
1264                    || ext_lower == "svg"
1265                {
1266                    false
1267                } else {
1268                    true // Unknown extensions default to text
1269                }
1270            }
1271            None => true, // No extension defaults to text
1272        }
1273    }
1274
1275    /// Check if a file is an SVG
1276    pub fn is_svg_file(path: &Path) -> bool {
1277        path.extension()
1278            .and_then(|e| e.to_str())
1279            .map(|e| e.to_lowercase() == "svg")
1280            .unwrap_or(false)
1281    }
1282}
1283
1284// =============================================================================
1285// Unit Tests
1286// =============================================================================
1287
1288#[cfg(test)]
1289mod tests {
1290    use super::*;
1291    use std::io::Write;
1292    use tempfile::TempDir;
1293
1294    fn create_test_context(dir: &Path) -> ToolContext {
1295        ToolContext::new(dir.to_path_buf())
1296            .with_session_id("test-session")
1297            .with_user("test-user")
1298    }
1299
1300    fn create_read_tool() -> ReadTool {
1301        ReadTool::new(super::super::create_shared_history())
1302    }
1303
1304    #[test]
1305    fn test_line_range_new() {
1306        let range = LineRange::new(5, Some(10));
1307        assert_eq!(range.start, 5);
1308        assert_eq!(range.end, Some(10));
1309    }
1310
1311    #[test]
1312    fn test_line_range_from_start() {
1313        let range = LineRange::from_start(5);
1314        assert_eq!(range.start, 5);
1315        assert_eq!(range.end, None);
1316    }
1317
1318    #[test]
1319    fn test_line_range_lines() {
1320        let range = LineRange::lines(5, 10);
1321        assert_eq!(range.start, 5);
1322        assert_eq!(range.end, Some(14));
1323    }
1324
1325    #[test]
1326    fn test_is_image_file() {
1327        assert!(ReadTool::is_image_file(Path::new("test.png")));
1328        assert!(ReadTool::is_image_file(Path::new("test.jpg")));
1329        assert!(ReadTool::is_image_file(Path::new("test.JPEG")));
1330        assert!(ReadTool::is_image_file(Path::new("test.gif")));
1331        assert!(!ReadTool::is_image_file(Path::new("test.txt")));
1332        assert!(!ReadTool::is_image_file(Path::new("test.rs")));
1333    }
1334
1335    #[test]
1336    fn test_is_pdf_file() {
1337        assert!(ReadTool::is_pdf_file(Path::new("test.pdf")));
1338        assert!(ReadTool::is_pdf_file(Path::new("test.PDF")));
1339        assert!(!ReadTool::is_pdf_file(Path::new("test.txt")));
1340    }
1341
1342    #[test]
1343    fn test_is_notebook_file() {
1344        assert!(ReadTool::is_notebook_file(Path::new("test.ipynb")));
1345        assert!(ReadTool::is_notebook_file(Path::new("test.IPYNB")));
1346        assert!(!ReadTool::is_notebook_file(Path::new("test.py")));
1347    }
1348
1349    #[test]
1350    fn test_is_text_file() {
1351        assert!(ReadTool::is_text_file(Path::new("test.txt")));
1352        assert!(ReadTool::is_text_file(Path::new("test.rs")));
1353        assert!(ReadTool::is_text_file(Path::new("test.py")));
1354        assert!(ReadTool::is_text_file(Path::new("test.json")));
1355        // Unknown extensions default to text
1356        assert!(ReadTool::is_text_file(Path::new("test.unknown")));
1357    }
1358
1359    #[tokio::test]
1360    async fn test_read_text_file() {
1361        let temp_dir = TempDir::new().unwrap();
1362        let file_path = temp_dir.path().join("test.txt");
1363
1364        // Create test file
1365        let mut file = fs::File::create(&file_path).unwrap();
1366        writeln!(file, "Line 1").unwrap();
1367        writeln!(file, "Line 2").unwrap();
1368        writeln!(file, "Line 3").unwrap();
1369
1370        let tool = create_read_tool();
1371        let context = create_test_context(temp_dir.path());
1372
1373        let result = tool.read_text(&file_path, None, &context).await.unwrap();
1374
1375        assert!(result.contains("1 | Line 1"));
1376        assert!(result.contains("2 | Line 2"));
1377        assert!(result.contains("3 | Line 3"));
1378
1379        // Check history was recorded
1380        assert!(tool.read_history.read().unwrap().has_read(&file_path));
1381    }
1382
1383    #[tokio::test]
1384    async fn test_read_text_file_with_range() {
1385        let temp_dir = TempDir::new().unwrap();
1386        let file_path = temp_dir.path().join("test.txt");
1387
1388        // Create test file with 10 lines
1389        let mut file = fs::File::create(&file_path).unwrap();
1390        for i in 1..=10 {
1391            writeln!(file, "Line {}", i).unwrap();
1392        }
1393
1394        let tool = create_read_tool();
1395        let context = create_test_context(temp_dir.path());
1396
1397        let range = LineRange::new(3, Some(5));
1398        let result = tool
1399            .read_text(&file_path, Some(range), &context)
1400            .await
1401            .unwrap();
1402
1403        assert!(result.contains("3 | Line 3"));
1404        assert!(result.contains("4 | Line 4"));
1405        assert!(result.contains("5 | Line 5"));
1406        assert!(!result.contains("Line 1"));
1407        assert!(!result.contains("Line 6"));
1408    }
1409
1410    #[tokio::test]
1411    async fn test_read_nonexistent_file() {
1412        let temp_dir = TempDir::new().unwrap();
1413        let file_path = temp_dir.path().join("nonexistent.txt");
1414
1415        let tool = create_read_tool();
1416        let context = create_test_context(temp_dir.path());
1417
1418        let result = tool.read_text(&file_path, None, &context).await;
1419        assert!(result.is_err());
1420    }
1421
1422    #[tokio::test]
1423    async fn test_read_image_file() {
1424        let temp_dir = TempDir::new().unwrap();
1425        let file_path = temp_dir.path().join("test.png");
1426
1427        // Create a minimal PNG file (1x1 transparent pixel)
1428        let png_data: Vec<u8> = vec![
1429            0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG signature
1430            0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, // IHDR chunk
1431            0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, 0x00, 0x1F,
1432            0x15, 0xC4, 0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, 0x41, // IDAT chunk
1433            0x54, 0x78, 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00, 0x01, 0x0D, 0x0A, 0x2D,
1434            0xB4, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, // IEND chunk
1435            0x42, 0x60, 0x82,
1436        ];
1437        fs::write(&file_path, &png_data).unwrap();
1438
1439        let tool = create_read_tool();
1440        let context = create_test_context(temp_dir.path());
1441
1442        let result = tool.read_image(&file_path, &context).await.unwrap();
1443
1444        // Updated assertion for enhanced image output format
1445        assert!(result.contains("[Enhanced Image Analysis:"));
1446        assert!(result.contains("Base64 Data: data:image/png;base64,"));
1447        assert!(tool.read_history.read().unwrap().has_read(&file_path));
1448    }
1449
1450    #[tokio::test]
1451    async fn test_read_notebook_file() {
1452        let temp_dir = TempDir::new().unwrap();
1453        let file_path = temp_dir.path().join("test.ipynb");
1454
1455        // Create a minimal notebook
1456        let notebook = serde_json::json!({
1457            "cells": [
1458                {
1459                    "cell_type": "code",
1460                    "source": ["print('Hello')"],
1461                    "outputs": []
1462                },
1463                {
1464                    "cell_type": "markdown",
1465                    "source": ["# Title"]
1466                }
1467            ],
1468            "metadata": {},
1469            "nbformat": 4,
1470            "nbformat_minor": 2
1471        });
1472        fs::write(&file_path, serde_json::to_string(&notebook).unwrap()).unwrap();
1473
1474        let tool = create_read_tool();
1475        let context = create_test_context(temp_dir.path());
1476
1477        let result = tool.read_notebook(&file_path, &context).await.unwrap();
1478
1479        // Updated assertions for enhanced notebook output format
1480        assert!(result.contains("[Enhanced Notebook Analysis:"));
1481        assert!(result.contains("Cell 1 [Code Cell]"));
1482        assert!(result.contains("print('Hello')"));
1483        assert!(result.contains("Cell 2 [Markdown Cell]"));
1484        assert!(result.contains("# Title"));
1485    }
1486
1487    #[tokio::test]
1488    async fn test_tool_execute_text() {
1489        let temp_dir = TempDir::new().unwrap();
1490        let file_path = temp_dir.path().join("test.txt");
1491        fs::write(&file_path, "Hello, World!").unwrap();
1492
1493        let tool = create_read_tool();
1494        let context = create_test_context(temp_dir.path());
1495        let params = serde_json::json!({
1496            "path": file_path.to_str().unwrap()
1497        });
1498
1499        let result = tool.execute(params, &context).await.unwrap();
1500
1501        assert!(result.is_success());
1502        assert!(result.output.unwrap().contains("Hello, World!"));
1503        assert_eq!(
1504            result.metadata.get("file_type"),
1505            Some(&serde_json::json!("text"))
1506        );
1507    }
1508
1509    #[tokio::test]
1510    async fn test_tool_execute_with_line_range() {
1511        let temp_dir = TempDir::new().unwrap();
1512        let file_path = temp_dir.path().join("test.txt");
1513
1514        let mut file = fs::File::create(&file_path).unwrap();
1515        for i in 1..=10 {
1516            writeln!(file, "Line {}", i).unwrap();
1517        }
1518
1519        let tool = create_read_tool();
1520        let context = create_test_context(temp_dir.path());
1521        let params = serde_json::json!({
1522            "path": file_path.to_str().unwrap(),
1523            "start_line": 2,
1524            "end_line": 4
1525        });
1526
1527        let result = tool.execute(params, &context).await.unwrap();
1528
1529        assert!(result.is_success());
1530        let output = result.output.unwrap();
1531        assert!(output.contains("Line 2"));
1532        assert!(output.contains("Line 3"));
1533        assert!(output.contains("Line 4"));
1534        assert!(!output.contains("Line 1"));
1535        assert!(!output.contains("Line 5"));
1536    }
1537
1538    #[tokio::test]
1539    async fn test_tool_execute_missing_path() {
1540        let temp_dir = TempDir::new().unwrap();
1541        let tool = create_read_tool();
1542        let context = create_test_context(temp_dir.path());
1543        let params = serde_json::json!({});
1544
1545        let result = tool.execute(params, &context).await;
1546        assert!(result.is_err());
1547        assert!(matches!(result.unwrap_err(), ToolError::InvalidParams(_)));
1548    }
1549
1550    #[test]
1551    fn test_tool_name() {
1552        let tool = create_read_tool();
1553        assert_eq!(tool.name(), "read");
1554    }
1555
1556    #[test]
1557    fn test_tool_description() {
1558        let tool = create_read_tool();
1559        assert!(!tool.description().is_empty());
1560        assert!(
1561            tool.description().contains("Enhanced") || tool.description().contains("multimodal")
1562        );
1563    }
1564
1565    #[test]
1566    fn test_tool_input_schema() {
1567        let tool = create_read_tool();
1568        let schema = tool.input_schema();
1569        assert_eq!(schema["type"], "object");
1570        assert!(schema["properties"]["path"].is_object());
1571        assert!(schema["properties"]["start_line"].is_object());
1572        assert!(schema["properties"]["end_line"].is_object());
1573    }
1574
1575    #[tokio::test]
1576    async fn test_check_permissions() {
1577        let temp_dir = TempDir::new().unwrap();
1578        let tool = create_read_tool();
1579        let context = create_test_context(temp_dir.path());
1580        let params = serde_json::json!({
1581            "path": "test.txt"
1582        });
1583
1584        let result = tool.check_permissions(&params, &context).await;
1585        assert!(result.is_allowed());
1586    }
1587
1588    #[tokio::test]
1589    async fn test_check_permissions_missing_path() {
1590        let temp_dir = TempDir::new().unwrap();
1591        let tool = create_read_tool();
1592        let context = create_test_context(temp_dir.path());
1593        let params = serde_json::json!({});
1594
1595        let result = tool.check_permissions(&params, &context).await;
1596        assert!(result.is_denied());
1597    }
1598}