helios_engine/
tools.rs

1//! # Tools Module
2//!
3//! This module provides the framework for creating and managing tools that can be used by agents.
4//! It defines the `Tool` trait, which all tools must implement, and the `ToolRegistry`
5//! for managing a collection of tools.
6//! It also includes several built-in tools for common tasks.
7
8use crate::error::{HeliosError, Result};
9use async_trait::async_trait;
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12use std::collections::HashMap;
13use std::io::{BufReader, BufWriter, Read, Write};
14use std::path::Path;
15use std::time::{SystemTime, UNIX_EPOCH};
16use uuid::Uuid;
17
18/// A parameter for a tool.
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct ToolParameter {
21    /// The type of the parameter (e.g., "string", "number").
22    #[serde(rename = "type")]
23    pub param_type: String,
24    /// A description of the parameter.
25    pub description: String,
26    /// Whether the parameter is required.
27    #[serde(skip)]
28    pub required: Option<bool>,
29}
30
31/// The definition of a tool that can be sent to an LLM.
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct ToolDefinition {
34    /// The type of the tool (e.g., "function").
35    #[serde(rename = "type")]
36    pub tool_type: String,
37    /// The function definition for the tool.
38    pub function: FunctionDefinition,
39}
40
41/// The definition of a function that can be called by a tool.
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct FunctionDefinition {
44    /// The name of the function.
45    pub name: String,
46    /// A description of the function.
47    pub description: String,
48    /// The parameters for the function.
49    pub parameters: ParametersSchema,
50}
51
52/// The schema for the parameters of a function.
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct ParametersSchema {
55    /// The type of the schema (should be "object").
56    #[serde(rename = "type")]
57    pub schema_type: String,
58    /// The properties of the schema.
59    pub properties: HashMap<String, ToolParameter>,
60    /// The required properties.
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub required: Option<Vec<String>>,
63}
64
65/// The result of a tool execution.
66#[derive(Debug, Clone)]
67pub struct ToolResult {
68    /// Whether the execution was successful.
69    pub success: bool,
70    /// The output of the execution.
71    pub output: String,
72}
73
74impl ToolResult {
75    /// Creates a new successful `ToolResult`.
76    pub fn success(output: impl Into<String>) -> Self {
77        Self {
78            success: true,
79            output: output.into(),
80        }
81    }
82
83    /// Creates a new error `ToolResult`.
84    pub fn error(message: impl Into<String>) -> Self {
85        Self {
86            success: false,
87            output: message.into(),
88        }
89    }
90}
91
92/// A trait for tools that can be used by agents.
93#[async_trait]
94pub trait Tool: Send + Sync {
95    /// The name of the tool.
96    fn name(&self) -> &str;
97    /// A description of the tool.
98    fn description(&self) -> &str;
99    /// The parameters for the tool.
100    fn parameters(&self) -> HashMap<String, ToolParameter>;
101    /// Executes the tool with the given arguments.
102    async fn execute(&self, args: Value) -> Result<ToolResult>;
103
104    /// Converts the tool to a `ToolDefinition`.
105    fn to_definition(&self) -> ToolDefinition {
106        let required: Vec<String> = self
107            .parameters()
108            .iter()
109            .filter(|(_, param)| param.required.unwrap_or(false))
110            .map(|(name, _)| name.clone())
111            .collect();
112
113        ToolDefinition {
114            tool_type: "function".to_string(),
115            function: FunctionDefinition {
116                name: self.name().to_string(),
117                description: self.description().to_string(),
118                parameters: ParametersSchema {
119                    schema_type: "object".to_string(),
120                    properties: self.parameters(),
121                    required: if required.is_empty() {
122                        None
123                    } else {
124                        Some(required)
125                    },
126                },
127            },
128        }
129    }
130}
131
132/// A registry for managing a collection of tools.
133pub struct ToolRegistry {
134    tools: HashMap<String, Box<dyn Tool>>,
135}
136
137impl ToolRegistry {
138    /// Creates a new `ToolRegistry`.
139    pub fn new() -> Self {
140        Self {
141            tools: HashMap::new(),
142        }
143    }
144
145    /// Registers a tool with the registry.
146    pub fn register(&mut self, tool: Box<dyn Tool>) {
147        let name = tool.name().to_string();
148        self.tools.insert(name, tool);
149    }
150
151    /// Gets a tool from the registry by name.
152    pub fn get(&self, name: &str) -> Option<&dyn Tool> {
153        self.tools.get(name).map(|b| &**b)
154    }
155
156    /// Executes a tool in the registry by name.
157    pub async fn execute(&self, name: &str, args: Value) -> Result<ToolResult> {
158        let tool = self
159            .tools
160            .get(name)
161            .ok_or_else(|| HeliosError::ToolError(format!("Tool '{}' not found", name)))?;
162
163        tool.execute(args).await
164    }
165
166    /// Gets the definitions of all tools in the registry.
167    pub fn get_definitions(&self) -> Vec<ToolDefinition> {
168        self.tools
169            .values()
170            .map(|tool| tool.to_definition())
171            .collect()
172    }
173
174    /// Lists the names of all tools in the registry.
175    pub fn list_tools(&self) -> Vec<String> {
176        self.tools.keys().cloned().collect()
177    }
178}
179
180impl Default for ToolRegistry {
181    fn default() -> Self {
182        Self::new()
183    }
184}
185
186// Example built-in tools
187
188/// A tool for performing basic arithmetic operations.
189pub struct CalculatorTool;
190
191#[async_trait]
192impl Tool for CalculatorTool {
193    fn name(&self) -> &str {
194        "calculator"
195    }
196
197    fn description(&self) -> &str {
198        "Perform basic arithmetic operations. Supports +, -, *, / operations."
199    }
200
201    fn parameters(&self) -> HashMap<String, ToolParameter> {
202        let mut params = HashMap::new();
203        params.insert(
204            "expression".to_string(),
205            ToolParameter {
206                param_type: "string".to_string(),
207                description: "Mathematical expression to evaluate (e.g., '2 + 2')".to_string(),
208                required: Some(true),
209            },
210        );
211        params
212    }
213
214    async fn execute(&self, args: Value) -> Result<ToolResult> {
215        let expression = args
216            .get("expression")
217            .and_then(|v| v.as_str())
218            .ok_or_else(|| HeliosError::ToolError("Missing 'expression' parameter".to_string()))?;
219
220        // Simple expression evaluator
221        let result = evaluate_expression(expression)?;
222        Ok(ToolResult::success(result.to_string()))
223    }
224}
225
226/// Evaluates a simple mathematical expression.
227fn evaluate_expression(expr: &str) -> Result<f64> {
228    let expr = expr.replace(" ", "");
229
230    // Simple parsing for basic operations
231    for op in &['*', '/', '+', '-'] {
232        if let Some(pos) = expr.rfind(*op) {
233            if pos == 0 {
234                continue; // Skip if operator is at the beginning (negative number)
235            }
236            let left = &expr[..pos];
237            let right = &expr[pos + 1..];
238
239            let left_val = evaluate_expression(left)?;
240            let right_val = evaluate_expression(right)?;
241
242            return Ok(match op {
243                '+' => left_val + right_val,
244                '-' => left_val - right_val,
245                '*' => left_val * right_val,
246                '/' => {
247                    if right_val == 0.0 {
248                        return Err(HeliosError::ToolError("Division by zero".to_string()));
249                    }
250                    left_val / right_val
251                }
252                _ => unreachable!(),
253            });
254        }
255    }
256
257    expr.parse::<f64>()
258        .map_err(|_| HeliosError::ToolError(format!("Invalid expression: {}", expr)))
259}
260
261/// A tool that echoes back the provided message.
262pub struct EchoTool;
263
264#[async_trait]
265impl Tool for EchoTool {
266    fn name(&self) -> &str {
267        "echo"
268    }
269
270    fn description(&self) -> &str {
271        "Echo back the provided message."
272    }
273
274    fn parameters(&self) -> HashMap<String, ToolParameter> {
275        let mut params = HashMap::new();
276        params.insert(
277            "message".to_string(),
278            ToolParameter {
279                param_type: "string".to_string(),
280                description: "The message to echo back".to_string(),
281                required: Some(true),
282            },
283        );
284        params
285    }
286
287    async fn execute(&self, args: Value) -> Result<ToolResult> {
288        let message = args
289            .get("message")
290            .and_then(|v| v.as_str())
291            .ok_or_else(|| HeliosError::ToolError("Missing 'message' parameter".to_string()))?;
292
293        Ok(ToolResult::success(format!("Echo: {}", message)))
294    }
295}
296
297/// A tool for searching for files.
298pub struct FileSearchTool;
299
300#[async_trait]
301impl Tool for FileSearchTool {
302    fn name(&self) -> &str {
303        "file_search"
304    }
305
306    fn description(&self) -> &str {
307        "Search for files by name pattern or search for content within files. Can search recursively in directories."
308    }
309
310    fn parameters(&self) -> HashMap<String, ToolParameter> {
311        let mut params = HashMap::new();
312        params.insert(
313            "path".to_string(),
314            ToolParameter {
315                param_type: "string".to_string(),
316                description: "The directory path to search in (default: current directory)"
317                    .to_string(),
318                required: Some(false),
319            },
320        );
321        params.insert(
322            "pattern".to_string(),
323            ToolParameter {
324                param_type: "string".to_string(),
325                description: "File name pattern to search for (supports wildcards like *.rs)"
326                    .to_string(),
327                required: Some(false),
328            },
329        );
330        params.insert(
331            "content".to_string(),
332            ToolParameter {
333                param_type: "string".to_string(),
334                description: "Text content to search for within files".to_string(),
335                required: Some(false),
336            },
337        );
338        params.insert(
339            "max_results".to_string(),
340            ToolParameter {
341                param_type: "number".to_string(),
342                description: "Maximum number of results to return (default: 50)".to_string(),
343                required: Some(false),
344            },
345        );
346        params
347    }
348
349    async fn execute(&self, args: Value) -> Result<ToolResult> {
350        use walkdir::WalkDir;
351
352        let base_path = args.get("path").and_then(|v| v.as_str()).unwrap_or(".");
353
354        let pattern = args.get("pattern").and_then(|v| v.as_str());
355        let content_search = args.get("content").and_then(|v| v.as_str());
356        let max_results = args
357            .get("max_results")
358            .and_then(|v| v.as_u64())
359            .unwrap_or(50) as usize;
360
361        if pattern.is_none() && content_search.is_none() {
362            return Err(HeliosError::ToolError(
363                "Either 'pattern' or 'content' parameter is required".to_string(),
364            ));
365        }
366
367        let mut results = Vec::new();
368
369        // Precompile filename pattern to avoid compiling per file
370        let compiled_re = if let Some(pat) = pattern {
371            let re_pattern = pat.replace(".", r"\.").replace("*", ".*").replace("?", ".");
372            match regex::Regex::new(&format!("^{}$", re_pattern)) {
373                Ok(re) => Some(re),
374                Err(e) => {
375                    tracing::warn!(
376                        "Invalid glob pattern '{}' ({}). Falling back to substring matching.",
377                        pat,
378                        e
379                    );
380                    None
381                }
382            }
383        } else {
384            None
385        };
386
387        for entry in WalkDir::new(base_path)
388            .max_depth(10)
389            .follow_links(false)
390            .into_iter()
391            .filter_map(|e| e.ok())
392        {
393            if results.len() >= max_results {
394                break;
395            }
396
397            let path = entry.path();
398
399            // Skip hidden files and common ignore directories
400            if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
401                if file_name.starts_with('.')
402                    || file_name == "target"
403                    || file_name == "node_modules"
404                    || file_name == "__pycache__"
405                {
406                    continue;
407                }
408            }
409
410            // Pattern matching for file names
411            if let Some(pat) = pattern {
412                if path.is_file() {
413                    if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
414                        let is_match = if let Some(re) = &compiled_re {
415                            re.is_match(file_name)
416                        } else {
417                            file_name.contains(pat)
418                        };
419                        if is_match {
420                            results.push(format!("📄 {}", path.display()));
421                        }
422                    }
423                }
424            }
425
426            // Content search within files
427            if let Some(search_term) = content_search {
428                if path.is_file() {
429                    if let Ok(content) = std::fs::read_to_string(path) {
430                        if content.contains(search_term) {
431                            // Find line numbers where content appears
432                            let matching_lines: Vec<(usize, &str)> = content
433                                .lines()
434                                .enumerate()
435                                .filter(|(_, line)| line.contains(search_term))
436                                .take(3) // Show up to 3 matching lines per file
437                                .collect();
438
439                            if !matching_lines.is_empty() {
440                                results.push(format!(
441                                    "📄 {} (found in {} lines)",
442                                    path.display(),
443                                    matching_lines.len()
444                                ));
445                                for (line_num, line) in matching_lines {
446                                    results.push(format!(
447                                        "  Line {}: {}",
448                                        line_num + 1,
449                                        line.trim()
450                                    ));
451                                }
452                            }
453                        }
454                    }
455                }
456            }
457        }
458
459        if results.is_empty() {
460            Ok(ToolResult::success(
461                "No files found matching the criteria.".to_string(),
462            ))
463        } else {
464            let output = format!(
465                "Found {} result(s):\n\n{}",
466                results.len(),
467                results.join("\n")
468            );
469            Ok(ToolResult::success(output))
470        }
471    }
472}
473
474// (removed) glob_match helper – logic moved to precompiled regex in FileSearchTool::execute
475
476/// A tool for reading the contents of a file.
477pub struct FileReadTool;
478
479#[async_trait]
480impl Tool for FileReadTool {
481    fn name(&self) -> &str {
482        "file_read"
483    }
484
485    fn description(&self) -> &str {
486        "Read the contents of a file. Returns the full file content or specific lines."
487    }
488
489    fn parameters(&self) -> HashMap<String, ToolParameter> {
490        let mut params = HashMap::new();
491        params.insert(
492            "path".to_string(),
493            ToolParameter {
494                param_type: "string".to_string(),
495                description: "The file path to read".to_string(),
496                required: Some(true),
497            },
498        );
499        params.insert(
500            "start_line".to_string(),
501            ToolParameter {
502                param_type: "number".to_string(),
503                description: "Starting line number (1-indexed, optional)".to_string(),
504                required: Some(false),
505            },
506        );
507        params.insert(
508            "end_line".to_string(),
509            ToolParameter {
510                param_type: "number".to_string(),
511                description: "Ending line number (1-indexed, optional)".to_string(),
512                required: Some(false),
513            },
514        );
515        params
516    }
517
518    async fn execute(&self, args: Value) -> Result<ToolResult> {
519        let file_path = args
520            .get("path")
521            .and_then(|v| v.as_str())
522            .ok_or_else(|| HeliosError::ToolError("Missing 'path' parameter".to_string()))?;
523
524        let content = std::fs::read_to_string(file_path)
525            .map_err(|e| HeliosError::ToolError(format!("Failed to read file: {}", e)))?;
526
527        let start_line = args
528            .get("start_line")
529            .and_then(|v| v.as_u64())
530            .map(|n| n as usize);
531        let end_line = args
532            .get("end_line")
533            .and_then(|v| v.as_u64())
534            .map(|n| n as usize);
535
536        let output = if let (Some(start), Some(end)) = (start_line, end_line) {
537            let lines: Vec<&str> = content.lines().collect();
538            let start_idx = start.saturating_sub(1);
539            let end_idx = end.min(lines.len());
540
541            if start_idx >= lines.len() {
542                return Err(HeliosError::ToolError(format!(
543                    "Start line {} is beyond file length ({})",
544                    start,
545                    lines.len()
546                )));
547            }
548
549            let selected_lines = &lines[start_idx..end_idx];
550            format!(
551                "File: {} (lines {}-{}):\n\n{}",
552                file_path,
553                start,
554                end_idx,
555                selected_lines.join("\n")
556            )
557        } else {
558            format!("File: {}:\n\n{}", file_path, content)
559        };
560
561        Ok(ToolResult::success(output))
562    }
563}
564
565/// A tool for writing content to a file.
566pub struct FileWriteTool;
567
568#[async_trait]
569impl Tool for FileWriteTool {
570    fn name(&self) -> &str {
571        "file_write"
572    }
573
574    fn description(&self) -> &str {
575        "Write content to a file. Creates new file or overwrites existing file."
576    }
577
578    fn parameters(&self) -> HashMap<String, ToolParameter> {
579        let mut params = HashMap::new();
580        params.insert(
581            "path".to_string(),
582            ToolParameter {
583                param_type: "string".to_string(),
584                description: "The file path to write to".to_string(),
585                required: Some(true),
586            },
587        );
588        params.insert(
589            "content".to_string(),
590            ToolParameter {
591                param_type: "string".to_string(),
592                description: "The content to write to the file".to_string(),
593                required: Some(true),
594            },
595        );
596        params
597    }
598
599    async fn execute(&self, args: Value) -> Result<ToolResult> {
600        let file_path = args
601            .get("path")
602            .and_then(|v| v.as_str())
603            .ok_or_else(|| HeliosError::ToolError("Missing 'path' parameter".to_string()))?;
604
605        let content = args
606            .get("content")
607            .and_then(|v| v.as_str())
608            .ok_or_else(|| HeliosError::ToolError("Missing 'content' parameter".to_string()))?;
609
610        // Create parent directories if they don't exist
611        if let Some(parent) = std::path::Path::new(file_path).parent() {
612            std::fs::create_dir_all(parent).map_err(|e| {
613                HeliosError::ToolError(format!("Failed to create directories: {}", e))
614            })?;
615        }
616
617        std::fs::write(file_path, content)
618            .map_err(|e| HeliosError::ToolError(format!("Failed to write file: {}", e)))?;
619
620        Ok(ToolResult::success(format!(
621            "Successfully wrote {} bytes to {}",
622            content.len(),
623            file_path
624        )))
625    }
626}
627
628/// A tool for editing a file by replacing text.
629pub struct FileEditTool;
630
631#[async_trait]
632impl Tool for FileEditTool {
633    fn name(&self) -> &str {
634        "file_edit"
635    }
636
637    fn description(&self) -> &str {
638        "Edit a file by replacing specific text or lines. Use this to make targeted changes to existing files."
639    }
640
641    fn parameters(&self) -> HashMap<String, ToolParameter> {
642        let mut params = HashMap::new();
643        params.insert(
644            "path".to_string(),
645            ToolParameter {
646                param_type: "string".to_string(),
647                description: "The file path to edit".to_string(),
648                required: Some(true),
649            },
650        );
651        params.insert(
652            "find".to_string(),
653            ToolParameter {
654                param_type: "string".to_string(),
655                description: "The text to find and replace".to_string(),
656                required: Some(true),
657            },
658        );
659        params.insert(
660            "replace".to_string(),
661            ToolParameter {
662                param_type: "string".to_string(),
663                description: "The replacement text".to_string(),
664                required: Some(true),
665            },
666        );
667        params
668    }
669
670    async fn execute(&self, args: Value) -> Result<ToolResult> {
671        let file_path = args
672            .get("path")
673            .and_then(|v| v.as_str())
674            .ok_or_else(|| HeliosError::ToolError("Missing 'path' parameter".to_string()))?;
675
676        let find_text = args
677            .get("find")
678            .and_then(|v| v.as_str())
679            .ok_or_else(|| HeliosError::ToolError("Missing 'find' parameter".to_string()))?;
680
681        let replace_text = args
682            .get("replace")
683            .and_then(|v| v.as_str())
684            .ok_or_else(|| HeliosError::ToolError("Missing 'replace' parameter".to_string()))?;
685
686        if find_text.is_empty() {
687            return Err(HeliosError::ToolError(
688                "'find' parameter cannot be empty".to_string(),
689            ));
690        }
691
692        let path = Path::new(file_path);
693        let parent = path
694            .parent()
695            .ok_or_else(|| HeliosError::ToolError(format!("Invalid target path: {}", file_path)))?;
696        let file_name = path
697            .file_name()
698            .ok_or_else(|| HeliosError::ToolError(format!("Invalid target path: {}", file_path)))?;
699
700        // Build a temp file path in the same directory for atomic rename
701        let pid = std::process::id();
702        let nanos = SystemTime::now()
703            .duration_since(UNIX_EPOCH)
704            .map_err(|e| HeliosError::ToolError(format!("Clock error: {}", e)))?
705            .as_nanos();
706        let tmp_name = format!("{}.tmp.{}.{}", file_name.to_string_lossy(), pid, nanos);
707        let tmp_path = parent.join(tmp_name);
708
709        // Open files
710        let input_file = std::fs::File::open(path)
711            .map_err(|e| HeliosError::ToolError(format!("Failed to open file for read: {}", e)))?;
712        let mut reader = BufReader::new(input_file);
713
714        let tmp_file = std::fs::File::create(&tmp_path).map_err(|e| {
715            HeliosError::ToolError(format!(
716                "Failed to create temp file {}: {}",
717                tmp_path.display(),
718                e
719            ))
720        })?;
721        let mut writer = BufWriter::new(&tmp_file);
722
723        // Streamed find/replace to avoid loading entire file into memory
724        let replaced_count = replace_streaming(
725            &mut reader,
726            &mut writer,
727            find_text.as_bytes(),
728            replace_text.as_bytes(),
729        )
730        .map_err(|e| HeliosError::ToolError(format!("I/O error while replacing: {}", e)))?;
731
732        // Ensure all data is flushed and synced before rename
733        writer
734            .flush()
735            .map_err(|e| HeliosError::ToolError(format!("Failed to flush temp file: {}", e)))?;
736        tmp_file
737            .sync_all()
738            .map_err(|e| HeliosError::ToolError(format!("Failed to sync temp file: {}", e)))?;
739
740        // Preserve permissions
741        if let Ok(meta) = std::fs::metadata(path) {
742            if let Err(e) = std::fs::set_permissions(&tmp_path, meta.permissions()) {
743                let _ = std::fs::remove_file(&tmp_path);
744                return Err(HeliosError::ToolError(format!(
745                    "Failed to set permissions: {}",
746                    e
747                )));
748            }
749        }
750
751        // Atomic replace
752        std::fs::rename(&tmp_path, path).map_err(|e| {
753            let _ = std::fs::remove_file(&tmp_path);
754            HeliosError::ToolError(format!("Failed to replace original file: {}", e))
755        })?;
756
757        if replaced_count == 0 {
758            return Ok(ToolResult::error(format!(
759                "Text '{}' not found in file {}",
760                find_text, file_path
761            )));
762        }
763
764        Ok(ToolResult::success(format!(
765            "Successfully replaced {} occurrence(s) in {}",
766            replaced_count, file_path
767        )))
768    }
769}
770
771/// Performs a streaming replacement of a needle in a reader, writing to a writer.
772fn replace_streaming<R: Read, W: Write>(
773    reader: &mut R,
774    writer: &mut W,
775    needle: &[u8],
776    replacement: &[u8],
777) -> std::io::Result<usize> {
778    let mut replaced = 0usize;
779    let mut carry: Vec<u8> = Vec::new();
780    let mut buf = [0u8; 8192];
781
782    let tail = if needle.len() > 1 {
783        needle.len() - 1
784    } else {
785        0
786    };
787
788    loop {
789        let n = reader.read(&mut buf)?;
790        if n == 0 {
791            break;
792        }
793
794        let mut combined = Vec::with_capacity(carry.len() + n);
795        combined.extend_from_slice(&carry);
796        combined.extend_from_slice(&buf[..n]);
797
798        let process_len = combined.len().saturating_sub(tail);
799        let (to_process, new_carry) = combined.split_at(process_len);
800        replaced += write_with_replacements(writer, to_process, needle, replacement)?;
801        carry.clear();
802        carry.extend_from_slice(new_carry);
803    }
804
805    // Process remaining carry fully
806    replaced += write_with_replacements(writer, &carry, needle, replacement)?;
807    Ok(replaced)
808}
809
810/// Writes the haystack to the writer, replacing all occurrences of the needle with the replacement.
811fn write_with_replacements<W: Write>(
812    writer: &mut W,
813    haystack: &[u8],
814    needle: &[u8],
815    replacement: &[u8],
816) -> std::io::Result<usize> {
817    if needle.is_empty() {
818        writer.write_all(haystack)?;
819        return Ok(0);
820    }
821
822    let mut count = 0usize;
823    let mut i = 0usize;
824    while let Some(pos) = find_subslice(&haystack[i..], needle) {
825        let idx = i + pos;
826        writer.write_all(&haystack[i..idx])?;
827        writer.write_all(replacement)?;
828        count += 1;
829        i = idx + needle.len();
830    }
831    writer.write_all(&haystack[i..])?;
832    Ok(count)
833}
834
835/// Finds the first occurrence of a subslice in a slice.
836fn find_subslice(h: &[u8], n: &[u8]) -> Option<usize> {
837    if n.is_empty() {
838        return Some(0);
839    }
840    h.windows(n.len()).position(|w| w == n)
841}
842
843/// RAG (Retrieval-Augmented Generation) Tool with Qdrant Vector Database
844///
845/// Provides document embedding, storage, retrieval, and reranking capabilities.
846/// Supports operations: add_document, search, delete, list, clear
847#[derive(Clone)]
848pub struct QdrantRAGTool {
849    qdrant_url: String,
850    collection_name: String,
851    embedding_api_url: String,
852    embedding_api_key: String,
853    client: reqwest::Client,
854}
855
856/// A point in a Qdrant collection.
857#[derive(Debug, Serialize, Deserialize)]
858struct QdrantPoint {
859    id: String,
860    vector: Vec<f32>,
861    payload: HashMap<String, serde_json::Value>,
862}
863
864/// A search request to a Qdrant collection.
865#[derive(Debug, Serialize, Deserialize)]
866struct QdrantSearchRequest {
867    vector: Vec<f32>,
868    limit: usize,
869    with_payload: bool,
870    with_vector: bool,
871}
872
873/// A search response from a Qdrant collection.
874#[derive(Debug, Serialize, Deserialize)]
875struct QdrantSearchResponse {
876    result: Vec<QdrantSearchResult>,
877}
878
879/// A search result from a Qdrant collection.
880#[derive(Debug, Serialize, Deserialize)]
881struct QdrantSearchResult {
882    id: String,
883    score: f64,
884    payload: Option<HashMap<String, serde_json::Value>>,
885}
886
887/// A request to an embedding API.
888#[derive(Debug, Serialize, Deserialize)]
889struct EmbeddingRequest {
890    input: String,
891    model: String,
892}
893
894/// A response from an embedding API.
895#[derive(Debug, Serialize, Deserialize)]
896struct EmbeddingResponse {
897    data: Vec<EmbeddingData>,
898}
899
900/// The data for an embedding.
901#[derive(Debug, Serialize, Deserialize)]
902struct EmbeddingData {
903    embedding: Vec<f32>,
904}
905
906impl QdrantRAGTool {
907    /// Creates a new `QdrantRAGTool`.
908    pub fn new(
909        qdrant_url: impl Into<String>,
910        collection_name: impl Into<String>,
911        embedding_api_url: impl Into<String>,
912        embedding_api_key: impl Into<String>,
913    ) -> Self {
914        Self {
915            qdrant_url: qdrant_url.into(),
916            collection_name: collection_name.into(),
917            embedding_api_url: embedding_api_url.into(),
918            embedding_api_key: embedding_api_key.into(),
919            client: reqwest::Client::new(),
920        }
921    }
922
923    /// Generates an embedding for the given text.
924    async fn generate_embedding(&self, text: &str) -> Result<Vec<f32>> {
925        let request = EmbeddingRequest {
926            input: text.to_string(),
927            model: "text-embedding-ada-002".to_string(),
928        };
929
930        let response = self
931            .client
932            .post(&self.embedding_api_url)
933            .header(
934                "Authorization",
935                format!("Bearer {}", self.embedding_api_key),
936            )
937            .json(&request)
938            .send()
939            .await
940            .map_err(|e| HeliosError::ToolError(format!("Embedding API error: {}", e)))?;
941
942        if !response.status().is_success() {
943            let error_text = response
944                .text()
945                .await
946                .unwrap_or_else(|_| "Unknown error".to_string());
947            return Err(HeliosError::ToolError(format!(
948                "Embedding failed: {}",
949                error_text
950            )));
951        }
952
953        let embedding_response: EmbeddingResponse = response.json().await.map_err(|e| {
954            HeliosError::ToolError(format!("Failed to parse embedding response: {}", e))
955        })?;
956
957        embedding_response
958            .data
959            .into_iter()
960            .next()
961            .map(|d| d.embedding)
962            .ok_or_else(|| HeliosError::ToolError("No embedding returned".to_string()))
963    }
964
965    /// Ensures that the Qdrant collection exists.
966    async fn ensure_collection(&self) -> Result<()> {
967        let collection_url = format!("{}/collections/{}", self.qdrant_url, self.collection_name);
968
969        // Check if collection exists
970        let response = self.client.get(&collection_url).send().await;
971
972        if response.is_ok() && response.unwrap().status().is_success() {
973            return Ok(()); // Collection exists
974        }
975
976        // Create collection with 1536 dimensions (OpenAI embedding size)
977        let create_payload = serde_json::json!({
978            "vectors": {
979                "size": 1536,
980                "distance": "Cosine"
981            }
982        });
983
984        let response = self
985            .client
986            .put(&collection_url)
987            .json(&create_payload)
988            .send()
989            .await
990            .map_err(|e| HeliosError::ToolError(format!("Failed to create collection: {}", e)))?;
991
992        if !response.status().is_success() {
993            let error_text = response
994                .text()
995                .await
996                .unwrap_or_else(|_| "Unknown error".to_string());
997            return Err(HeliosError::ToolError(format!(
998                "Collection creation failed: {}",
999                error_text
1000            )));
1001        }
1002
1003        Ok(())
1004    }
1005
1006    /// Adds a document to the Qdrant collection.
1007    async fn add_document(
1008        &self,
1009        text: &str,
1010        metadata: HashMap<String, serde_json::Value>,
1011    ) -> Result<String> {
1012        self.ensure_collection().await?;
1013
1014        // Generate embedding
1015        let embedding = self.generate_embedding(text).await?;
1016
1017        // Create point with metadata
1018        let point_id = Uuid::new_v4().to_string();
1019        let mut payload = metadata;
1020        payload.insert("text".to_string(), serde_json::json!(text));
1021        payload.insert(
1022            "timestamp".to_string(),
1023            serde_json::json!(chrono::Utc::now().to_rfc3339()),
1024        );
1025
1026        let point = QdrantPoint {
1027            id: point_id.clone(),
1028            vector: embedding,
1029            payload,
1030        };
1031
1032        // Upload point to Qdrant
1033        let upsert_url = format!(
1034            "{}/collections/{}/points",
1035            self.qdrant_url, self.collection_name
1036        );
1037        let upsert_payload = serde_json::json!({
1038            "points": [point]
1039        });
1040
1041        let response = self
1042            .client
1043            .put(&upsert_url)
1044            .json(&upsert_payload)
1045            .send()
1046            .await
1047            .map_err(|e| HeliosError::ToolError(format!("Failed to upload document: {}", e)))?;
1048
1049        if !response.status().is_success() {
1050            let error_text = response
1051                .text()
1052                .await
1053                .unwrap_or_else(|_| "Unknown error".to_string());
1054            return Err(HeliosError::ToolError(format!(
1055                "Document upload failed: {}",
1056                error_text
1057            )));
1058        }
1059
1060        Ok(point_id)
1061    }
1062
1063    /// Searches for similar documents in the Qdrant collection.
1064    async fn search(&self, query: &str, limit: usize) -> Result<Vec<(String, f64, String)>> {
1065        // Generate query embedding
1066        let query_embedding = self.generate_embedding(query).await?;
1067
1068        // Search in Qdrant
1069        let search_url = format!(
1070            "{}/collections/{}/points/search",
1071            self.qdrant_url, self.collection_name
1072        );
1073        let search_request = QdrantSearchRequest {
1074            vector: query_embedding,
1075            limit,
1076            with_payload: true,
1077            with_vector: false,
1078        };
1079
1080        let response = self
1081            .client
1082            .post(&search_url)
1083            .json(&search_request)
1084            .send()
1085            .await
1086            .map_err(|e| HeliosError::ToolError(format!("Search failed: {}", e)))?;
1087
1088        if !response.status().is_success() {
1089            let error_text = response
1090                .text()
1091                .await
1092                .unwrap_or_else(|_| "Unknown error".to_string());
1093            return Err(HeliosError::ToolError(format!(
1094                "Search request failed: {}",
1095                error_text
1096            )));
1097        }
1098
1099        let search_response: QdrantSearchResponse = response.json().await.map_err(|e| {
1100            HeliosError::ToolError(format!("Failed to parse search response: {}", e))
1101        })?;
1102
1103        // Extract results
1104        let results: Vec<(String, f64, String)> = search_response
1105            .result
1106            .into_iter()
1107            .filter_map(|r| {
1108                r.payload.and_then(|p| {
1109                    p.get("text")
1110                        .and_then(|t| t.as_str())
1111                        .map(|text| (r.id, r.score, text.to_string()))
1112                })
1113            })
1114            .collect();
1115
1116        Ok(results)
1117    }
1118
1119    /// Deletes a document from the Qdrant collection by ID.
1120    async fn delete_document(&self, doc_id: &str) -> Result<()> {
1121        let delete_url = format!(
1122            "{}/collections/{}/points/delete",
1123            self.qdrant_url, self.collection_name
1124        );
1125        let delete_payload = serde_json::json!({
1126            "points": [doc_id]
1127        });
1128
1129        let response = self
1130            .client
1131            .post(&delete_url)
1132            .json(&delete_payload)
1133            .send()
1134            .await
1135            .map_err(|e| HeliosError::ToolError(format!("Delete failed: {}", e)))?;
1136
1137        if !response.status().is_success() {
1138            let error_text = response
1139                .text()
1140                .await
1141                .unwrap_or_else(|_| "Unknown error".to_string());
1142            return Err(HeliosError::ToolError(format!(
1143                "Delete request failed: {}",
1144                error_text
1145            )));
1146        }
1147
1148        Ok(())
1149    }
1150
1151    /// Clears all documents from the Qdrant collection.
1152    async fn clear_collection(&self) -> Result<()> {
1153        let delete_url = format!("{}/collections/{}", self.qdrant_url, self.collection_name);
1154
1155        let response = self
1156            .client
1157            .delete(&delete_url)
1158            .send()
1159            .await
1160            .map_err(|e| HeliosError::ToolError(format!("Clear failed: {}", e)))?;
1161
1162        if !response.status().is_success() {
1163            let error_text = response
1164                .text()
1165                .await
1166                .unwrap_or_else(|_| "Unknown error".to_string());
1167            return Err(HeliosError::ToolError(format!(
1168                "Clear collection failed: {}",
1169                error_text
1170            )));
1171        }
1172
1173        Ok(())
1174    }
1175}
1176
1177#[async_trait]
1178impl Tool for QdrantRAGTool {
1179    fn name(&self) -> &str {
1180        "rag_qdrant"
1181    }
1182
1183    fn description(&self) -> &str {
1184        "RAG (Retrieval-Augmented Generation) tool with vector database. Operations: add_document, search, delete, clear"
1185    }
1186
1187    fn parameters(&self) -> HashMap<String, ToolParameter> {
1188        let mut params = HashMap::new();
1189        params.insert(
1190            "operation".to_string(),
1191            ToolParameter {
1192                param_type: "string".to_string(),
1193                description: "Operation: 'add_document', 'search', 'delete', 'clear'".to_string(),
1194                required: Some(true),
1195            },
1196        );
1197        params.insert(
1198            "text".to_string(),
1199            ToolParameter {
1200                param_type: "string".to_string(),
1201                description: "Text content for add_document or search query".to_string(),
1202                required: Some(false),
1203            },
1204        );
1205        params.insert(
1206            "doc_id".to_string(),
1207            ToolParameter {
1208                param_type: "string".to_string(),
1209                description: "Document ID for delete operation".to_string(),
1210                required: Some(false),
1211            },
1212        );
1213        params.insert(
1214            "limit".to_string(),
1215            ToolParameter {
1216                param_type: "number".to_string(),
1217                description: "Number of results for search (default: 5)".to_string(),
1218                required: Some(false),
1219            },
1220        );
1221        params.insert(
1222            "metadata".to_string(),
1223            ToolParameter {
1224                param_type: "object".to_string(),
1225                description: "Additional metadata for the document (JSON object)".to_string(),
1226                required: Some(false),
1227            },
1228        );
1229        params
1230    }
1231
1232    async fn execute(&self, args: Value) -> Result<ToolResult> {
1233        let operation = args
1234            .get("operation")
1235            .and_then(|v| v.as_str())
1236            .ok_or_else(|| HeliosError::ToolError("Missing 'operation' parameter".to_string()))?;
1237
1238        match operation {
1239            "add_document" => {
1240                let text = args.get("text").and_then(|v| v.as_str()).ok_or_else(|| {
1241                    HeliosError::ToolError("Missing 'text' for add_document".to_string())
1242                })?;
1243
1244                let metadata: HashMap<String, serde_json::Value> = args
1245                    .get("metadata")
1246                    .and_then(|v| serde_json::from_value(v.clone()).ok())
1247                    .unwrap_or_default();
1248
1249                let doc_id = self.add_document(text, metadata).await?;
1250                Ok(ToolResult::success(format!(
1251                    "✓ Document added successfully\nID: {}\nText preview: {}",
1252                    doc_id,
1253                    &text[..text.len().min(100)]
1254                )))
1255            }
1256            "search" => {
1257                let query = args.get("text").and_then(|v| v.as_str()).ok_or_else(|| {
1258                    HeliosError::ToolError("Missing 'text' for search".to_string())
1259                })?;
1260
1261                let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(5) as usize;
1262
1263                let results = self.search(query, limit).await?;
1264
1265                if results.is_empty() {
1266                    Ok(ToolResult::success(
1267                        "No matching documents found".to_string(),
1268                    ))
1269                } else {
1270                    let formatted_results: Vec<String> = results
1271                        .iter()
1272                        .enumerate()
1273                        .map(|(i, (id, score, text))| {
1274                            format!(
1275                                "{}. [Score: {:.4}] {}\n   ID: {}\n",
1276                                i + 1,
1277                                score,
1278                                &text[..text.len().min(150)],
1279                                id
1280                            )
1281                        })
1282                        .collect();
1283
1284                    Ok(ToolResult::success(format!(
1285                        "Found {} result(s):\n\n{}",
1286                        results.len(),
1287                        formatted_results.join("\n")
1288                    )))
1289                }
1290            }
1291            "delete" => {
1292                let doc_id = args.get("doc_id").and_then(|v| v.as_str()).ok_or_else(|| {
1293                    HeliosError::ToolError("Missing 'doc_id' for delete".to_string())
1294                })?;
1295
1296                self.delete_document(doc_id).await?;
1297                Ok(ToolResult::success(format!(
1298                    "✓ Document '{}' deleted",
1299                    doc_id
1300                )))
1301            }
1302            "clear" => {
1303                self.clear_collection().await?;
1304                Ok(ToolResult::success(
1305                    "✓ All documents cleared from collection".to_string(),
1306                ))
1307            }
1308            _ => Err(HeliosError::ToolError(format!(
1309                "Unknown operation '{}'. Valid: add_document, search, delete, clear",
1310                operation
1311            ))),
1312        }
1313    }
1314}
1315
1316/// In-Memory Database Tool
1317///
1318/// Provides a simple key-value store for agents to cache data during conversations.
1319/// Supports set, get, delete, list keys, and clear operations.
1320pub struct MemoryDBTool {
1321    db: std::sync::Arc<std::sync::Mutex<HashMap<String, String>>>,
1322}
1323
1324impl MemoryDBTool {
1325    /// Creates a new `MemoryDBTool`.
1326    pub fn new() -> Self {
1327        Self {
1328            db: std::sync::Arc::new(std::sync::Mutex::new(HashMap::new())),
1329        }
1330    }
1331
1332    /// Creates a new `MemoryDBTool` with a shared database.
1333    pub fn with_shared_db(db: std::sync::Arc<std::sync::Mutex<HashMap<String, String>>>) -> Self {
1334        Self { db }
1335    }
1336}
1337
1338impl Default for MemoryDBTool {
1339    fn default() -> Self {
1340        Self::new()
1341    }
1342}
1343
1344#[async_trait]
1345impl Tool for MemoryDBTool {
1346    fn name(&self) -> &str {
1347        "memory_db"
1348    }
1349
1350    fn description(&self) -> &str {
1351        "In-memory key-value database for caching data. Operations: set, get, delete, list, clear, exists"
1352    }
1353
1354    fn parameters(&self) -> HashMap<String, ToolParameter> {
1355        let mut params = HashMap::new();
1356        params.insert(
1357            "operation".to_string(),
1358            ToolParameter {
1359                param_type: "string".to_string(),
1360                description:
1361                    "Operation to perform: 'set', 'get', 'delete', 'list', 'clear', 'exists'"
1362                        .to_string(),
1363                required: Some(true),
1364            },
1365        );
1366        params.insert(
1367            "key".to_string(),
1368            ToolParameter {
1369                param_type: "string".to_string(),
1370                description: "Key for set, get, delete, exists operations".to_string(),
1371                required: Some(false),
1372            },
1373        );
1374        params.insert(
1375            "value".to_string(),
1376            ToolParameter {
1377                param_type: "string".to_string(),
1378                description: "Value for set operation".to_string(),
1379                required: Some(false),
1380            },
1381        );
1382        params
1383    }
1384
1385    async fn execute(&self, args: Value) -> Result<ToolResult> {
1386        let operation = args
1387            .get("operation")
1388            .and_then(|v| v.as_str())
1389            .ok_or_else(|| HeliosError::ToolError("Missing 'operation' parameter".to_string()))?;
1390
1391        let mut db = self
1392            .db
1393            .lock()
1394            .map_err(|e| HeliosError::ToolError(format!("Failed to lock database: {}", e)))?;
1395
1396        match operation {
1397            "set" => {
1398                let key = args.get("key").and_then(|v| v.as_str()).ok_or_else(|| {
1399                    HeliosError::ToolError("Missing 'key' parameter for set operation".to_string())
1400                })?;
1401                let value = args.get("value").and_then(|v| v.as_str()).ok_or_else(|| {
1402                    HeliosError::ToolError(
1403                        "Missing 'value' parameter for set operation".to_string(),
1404                    )
1405                })?;
1406
1407                db.insert(key.to_string(), value.to_string());
1408                Ok(ToolResult::success(format!(
1409                    "✓ Set '{}' = '{}'",
1410                    key, value
1411                )))
1412            }
1413            "get" => {
1414                let key = args.get("key").and_then(|v| v.as_str()).ok_or_else(|| {
1415                    HeliosError::ToolError("Missing 'key' parameter for get operation".to_string())
1416                })?;
1417
1418                match db.get(key) {
1419                    Some(value) => Ok(ToolResult::success(format!(
1420                        "Value for '{}': {}",
1421                        key, value
1422                    ))),
1423                    None => Ok(ToolResult::error(format!("Key '{}' not found", key))),
1424                }
1425            }
1426            "delete" => {
1427                let key = args.get("key").and_then(|v| v.as_str()).ok_or_else(|| {
1428                    HeliosError::ToolError(
1429                        "Missing 'key' parameter for delete operation".to_string(),
1430                    )
1431                })?;
1432
1433                match db.remove(key) {
1434                    Some(value) => Ok(ToolResult::success(format!(
1435                        "✓ Deleted '{}' (was: '{}')",
1436                        key, value
1437                    ))),
1438                    None => Ok(ToolResult::error(format!("Key '{}' not found", key))),
1439                }
1440            }
1441            "list" => {
1442                if db.is_empty() {
1443                    Ok(ToolResult::success("Database is empty".to_string()))
1444                } else {
1445                    let mut items: Vec<String> = db
1446                        .iter()
1447                        .map(|(k, v)| format!("  • {} = {}", k, v))
1448                        .collect();
1449                    items.sort();
1450                    Ok(ToolResult::success(format!(
1451                        "Database contents ({} items):\n{}",
1452                        db.len(),
1453                        items.join("\n")
1454                    )))
1455                }
1456            }
1457            "clear" => {
1458                let count = db.len();
1459                db.clear();
1460                Ok(ToolResult::success(format!(
1461                    "✓ Cleared database ({} items removed)",
1462                    count
1463                )))
1464            }
1465            "exists" => {
1466                let key = args.get("key").and_then(|v| v.as_str()).ok_or_else(|| {
1467                    HeliosError::ToolError(
1468                        "Missing 'key' parameter for exists operation".to_string(),
1469                    )
1470                })?;
1471
1472                let exists = db.contains_key(key);
1473                Ok(ToolResult::success(format!(
1474                    "Key '{}' exists: {}",
1475                    key, exists
1476                )))
1477            }
1478            _ => Err(HeliosError::ToolError(format!(
1479                "Unknown operation '{}'. Valid operations: set, get, delete, list, clear, exists",
1480                operation
1481            ))),
1482        }
1483    }
1484}
1485
1486/// A tool for scraping web content from URLs.
1487pub struct WebScraperTool;
1488
1489#[async_trait]
1490impl Tool for WebScraperTool {
1491    fn name(&self) -> &str {
1492        "web_scraper"
1493    }
1494
1495    fn description(&self) -> &str {
1496        "Fetch and extract content from web URLs. Supports HTML text extraction and basic web scraping."
1497    }
1498
1499    fn parameters(&self) -> HashMap<String, ToolParameter> {
1500        let mut params = HashMap::new();
1501        params.insert(
1502            "url".to_string(),
1503            ToolParameter {
1504                param_type: "string".to_string(),
1505                description: "The URL to scrape content from".to_string(),
1506                required: Some(true),
1507            },
1508        );
1509        params.insert(
1510            "extract_text".to_string(),
1511            ToolParameter {
1512                param_type: "boolean".to_string(),
1513                description: "Whether to extract readable text from HTML (default: true)"
1514                    .to_string(),
1515                required: Some(false),
1516            },
1517        );
1518        params.insert(
1519            "timeout_seconds".to_string(),
1520            ToolParameter {
1521                param_type: "number".to_string(),
1522                description: "Request timeout in seconds (default: 30)".to_string(),
1523                required: Some(false),
1524            },
1525        );
1526        params
1527    }
1528
1529    async fn execute(&self, args: Value) -> Result<ToolResult> {
1530        let url = args
1531            .get("url")
1532            .and_then(|v| v.as_str())
1533            .ok_or_else(|| HeliosError::ToolError("Missing 'url' parameter".to_string()))?;
1534
1535        let extract_text = args
1536            .get("extract_text")
1537            .and_then(|v| v.as_bool())
1538            .unwrap_or(true);
1539
1540        let timeout_seconds = args
1541            .get("timeout_seconds")
1542            .and_then(|v| v.as_u64())
1543            .unwrap_or(30);
1544
1545        let client = reqwest::Client::builder()
1546            .timeout(std::time::Duration::from_secs(timeout_seconds))
1547            .user_agent("Helios-WebScraper/1.0")
1548            .build()
1549            .map_err(|e| HeliosError::ToolError(format!("Failed to create HTTP client: {}", e)))?;
1550
1551        let response = client
1552            .get(url)
1553            .send()
1554            .await
1555            .map_err(|e| HeliosError::ToolError(format!("HTTP request failed: {}", e)))?;
1556
1557        if !response.status().is_success() {
1558            return Err(HeliosError::ToolError(format!(
1559                "HTTP request failed with status: {}",
1560                response.status()
1561            )));
1562        }
1563
1564        let headers = response.headers().clone();
1565        let content_type = headers
1566            .get("content-type")
1567            .and_then(|ct| ct.to_str().ok())
1568            .unwrap_or("");
1569
1570        let body = response
1571            .text()
1572            .await
1573            .map_err(|e| HeliosError::ToolError(format!("Failed to read response body: {}", e)))?;
1574
1575        let result = if extract_text && content_type.contains("text/html") {
1576            // Simple HTML text extraction
1577            extract_text_from_html(&body)
1578        } else {
1579            body
1580        };
1581
1582        Ok(ToolResult::success(format!(
1583            "Content fetched from: {}\nContent-Type: {}\n\n{}",
1584            url, content_type, result
1585        )))
1586    }
1587}
1588
1589/// Simple HTML text extraction function.
1590fn extract_text_from_html(html: &str) -> String {
1591    // Very basic HTML text extraction - removes tags and keeps text content
1592    let mut result = String::new();
1593    let mut in_tag = false;
1594
1595    for ch in html.chars() {
1596        match ch {
1597            '<' => in_tag = true,
1598            '>' => in_tag = false,
1599            _ if !in_tag => result.push(ch),
1600            _ => {}
1601        }
1602    }
1603
1604    // Clean up whitespace
1605    result
1606        .lines()
1607        .map(|line| line.trim())
1608        .filter(|line| !line.is_empty())
1609        .collect::<Vec<_>>()
1610        .join("\n")
1611}
1612
1613/// A tool for parsing and manipulating JSON data.
1614pub struct JsonParserTool;
1615
1616#[async_trait]
1617impl Tool for JsonParserTool {
1618    fn name(&self) -> &str {
1619        "json_parser"
1620    }
1621
1622    fn description(&self) -> &str {
1623        "Parse, validate, format, and manipulate JSON data. Supports operations: parse, stringify, get_value, set_value, validate"
1624    }
1625
1626    fn parameters(&self) -> HashMap<String, ToolParameter> {
1627        let mut params = HashMap::new();
1628        params.insert(
1629            "operation".to_string(),
1630            ToolParameter {
1631                param_type: "string".to_string(),
1632                description: "Operation to perform: 'parse', 'stringify', 'get_value', 'set_value', 'validate'".to_string(),
1633                required: Some(true),
1634            },
1635        );
1636        params.insert(
1637            "json".to_string(),
1638            ToolParameter {
1639                param_type: "string".to_string(),
1640                description: "JSON string for parse/stringify/validate operations".to_string(),
1641                required: Some(false),
1642            },
1643        );
1644        params.insert(
1645            "path".to_string(),
1646            ToolParameter {
1647                param_type: "string".to_string(),
1648                description:
1649                    "JSON path for get_value/set_value operations (e.g., '$.key' or 'key.subkey')"
1650                        .to_string(),
1651                required: Some(false),
1652            },
1653        );
1654        params.insert(
1655            "value".to_string(),
1656            ToolParameter {
1657                param_type: "string".to_string(),
1658                description: "Value to set for set_value operation (JSON string)".to_string(),
1659                required: Some(false),
1660            },
1661        );
1662        params.insert(
1663            "indent".to_string(),
1664            ToolParameter {
1665                param_type: "number".to_string(),
1666                description: "Indentation spaces for stringify operation (default: 2)".to_string(),
1667                required: Some(false),
1668            },
1669        );
1670        params
1671    }
1672
1673    async fn execute(&self, args: Value) -> Result<ToolResult> {
1674        let operation = args
1675            .get("operation")
1676            .and_then(|v| v.as_str())
1677            .ok_or_else(|| HeliosError::ToolError("Missing 'operation' parameter".to_string()))?;
1678
1679        match operation {
1680            "parse" => {
1681                let json_str = args
1682                    .get("json")
1683                    .and_then(|v| v.as_str())
1684                    .ok_or_else(|| HeliosError::ToolError("Missing 'json' parameter for parse operation".to_string()))?;
1685
1686                let parsed: Value = serde_json::from_str(json_str)
1687                    .map_err(|e| HeliosError::ToolError(format!("JSON parse error: {}", e)))?;
1688
1689                Ok(ToolResult::success(format!(
1690                    "✓ JSON parsed successfully\nType: {}\nKeys: {}",
1691                    get_json_type(&parsed),
1692                    get_json_keys(&parsed)
1693                )))
1694            }
1695            "stringify" => {
1696                let json_str = args
1697                    .get("json")
1698                    .and_then(|v| v.as_str())
1699                    .ok_or_else(|| HeliosError::ToolError("Missing 'json' parameter for stringify operation".to_string()))?;
1700
1701                let parsed: Value = serde_json::from_str(json_str)
1702                    .map_err(|e| HeliosError::ToolError(format!("Invalid JSON for stringify: {}", e)))?;
1703
1704                let indent = args
1705                    .get("indent")
1706                    .and_then(|v| v.as_u64())
1707                    .unwrap_or(2) as usize;
1708
1709                let formatted = if indent == 0 {
1710                    serde_json::to_string(&parsed)
1711                } else {
1712                    serde_json::to_string_pretty(&parsed)
1713                }
1714                .map_err(|e| HeliosError::ToolError(format!("JSON stringify error: {}", e)))?;
1715
1716                Ok(ToolResult::success(formatted))
1717            }
1718            "get_value" => {
1719                let json_str = args
1720                    .get("json")
1721                    .and_then(|v| v.as_str())
1722                    .ok_or_else(|| HeliosError::ToolError("Missing 'json' parameter for get_value operation".to_string()))?;
1723
1724                let path = args
1725                    .get("path")
1726                    .and_then(|v| v.as_str())
1727                    .ok_or_else(|| HeliosError::ToolError("Missing 'path' parameter for get_value operation".to_string()))?;
1728
1729                let parsed: Value = serde_json::from_str(json_str)
1730                    .map_err(|e| HeliosError::ToolError(format!("Invalid JSON for get_value: {}", e)))?;
1731
1732                let value = get_value_by_path(&parsed, path)?;
1733                Ok(ToolResult::success(format!(
1734                    "Value at path '{}': {}",
1735                    path,
1736                    serde_json::to_string_pretty(&value).unwrap_or_else(|_| value.to_string())
1737                )))
1738            }
1739            "set_value" => {
1740                let json_str = args
1741                    .get("json")
1742                    .and_then(|v| v.as_str())
1743                    .ok_or_else(|| HeliosError::ToolError("Missing 'json' parameter for set_value operation".to_string()))?;
1744
1745                let path = args
1746                    .get("path")
1747                    .and_then(|v| v.as_str())
1748                    .ok_or_else(|| HeliosError::ToolError("Missing 'path' parameter for set_value operation".to_string()))?;
1749
1750                let value_str = args
1751                    .get("value")
1752                    .and_then(|v| v.as_str())
1753                    .ok_or_else(|| HeliosError::ToolError("Missing 'value' parameter for set_value operation".to_string()))?;
1754
1755                let new_value: Value = serde_json::from_str(value_str)
1756                    .map_err(|e| HeliosError::ToolError(format!("Invalid value JSON: {}", e)))?;
1757
1758                let mut parsed: Value = serde_json::from_str(json_str)
1759                    .map_err(|e| HeliosError::ToolError(format!("Invalid JSON for set_value: {}", e)))?;
1760
1761                set_value_by_path(&mut parsed, path, new_value)?;
1762                let result = serde_json::to_string_pretty(&parsed)
1763                    .map_err(|e| HeliosError::ToolError(format!("JSON stringify error: {}", e)))?;
1764
1765                Ok(ToolResult::success(format!(
1766                    "✓ Value set at path '{}'\n{}",
1767                    path, result
1768                )))
1769            }
1770            "validate" => {
1771                let json_str = args
1772                    .get("json")
1773                    .and_then(|v| v.as_str())
1774                    .ok_or_else(|| HeliosError::ToolError("Missing 'json' parameter for validate operation".to_string()))?;
1775
1776                match serde_json::from_str::<Value>(json_str) {
1777                    Ok(_) => Ok(ToolResult::success("✓ JSON is valid".to_string())),
1778                    Err(e) => Ok(ToolResult::error(format!("✗ JSON validation failed: {}", e))),
1779                }
1780            }
1781            _ => Err(HeliosError::ToolError(format!(
1782                "Unknown operation '{}'. Valid operations: parse, stringify, get_value, set_value, validate",
1783                operation
1784            ))),
1785        }
1786    }
1787}
1788
1789/// Get the type of a JSON value.
1790fn get_json_type(value: &Value) -> &'static str {
1791    match value {
1792        Value::Null => "null",
1793        Value::Bool(_) => "boolean",
1794        Value::Number(_) => "number",
1795        Value::String(_) => "string",
1796        Value::Array(_) => "array",
1797        Value::Object(_) => "object",
1798    }
1799}
1800
1801/// Get the keys of a JSON object or array length.
1802fn get_json_keys(value: &Value) -> String {
1803    match value {
1804        Value::Object(obj) => {
1805            let keys: Vec<&String> = obj.keys().collect();
1806            format!("{{{}}}", keys.len())
1807        }
1808        Value::Array(arr) => format!("[{}]", arr.len()),
1809        _ => "-".to_string(),
1810    }
1811}
1812
1813/// Get a value by JSON path (simple implementation).
1814fn get_value_by_path(value: &Value, path: &str) -> Result<Value> {
1815    let path = path.trim_start_matches("$.");
1816    let keys: Vec<&str> = path.split('.').collect();
1817
1818    let mut current = value;
1819    for key in keys {
1820        match current {
1821            Value::Object(obj) => {
1822                current = obj
1823                    .get(key)
1824                    .ok_or_else(|| HeliosError::ToolError(format!("Key '{}' not found", key)))?;
1825            }
1826            _ => {
1827                return Err(HeliosError::ToolError(format!(
1828                    "Cannot access '{}' on non-object",
1829                    key
1830                )))
1831            }
1832        }
1833    }
1834
1835    Ok(current.clone())
1836}
1837
1838/// Set a value by JSON path (simple implementation).
1839fn set_value_by_path(value: &mut Value, path: &str, new_value: Value) -> Result<()> {
1840    let path = path.trim_start_matches("$.");
1841    let keys: Vec<&str> = path.split('.').collect();
1842
1843    if keys.is_empty() {
1844        return Err(HeliosError::ToolError("Empty path".to_string()));
1845    }
1846
1847    let mut current = value;
1848    for (i, key) in keys.iter().enumerate() {
1849        if i == keys.len() - 1 {
1850            // Last key - set the value
1851            match current {
1852                Value::Object(obj) => {
1853                    obj.insert(key.to_string(), new_value);
1854                    return Ok(());
1855                }
1856                _ => {
1857                    return Err(HeliosError::ToolError(format!(
1858                        "Cannot set '{}' on non-object",
1859                        key
1860                    )))
1861                }
1862            }
1863        } else {
1864            // Intermediate key - navigate deeper
1865            match current {
1866                Value::Object(obj) => {
1867                    if !obj.contains_key(*key) {
1868                        obj.insert(key.to_string(), Value::Object(serde_json::Map::new()));
1869                    }
1870                    current = obj.get_mut(*key).unwrap();
1871                }
1872                _ => {
1873                    return Err(HeliosError::ToolError(format!(
1874                        "Cannot access '{}' on non-object",
1875                        key
1876                    )))
1877                }
1878            }
1879        }
1880    }
1881
1882    Ok(())
1883}
1884
1885/// A tool for date/time operations and timestamp manipulation.
1886pub struct TimestampTool;
1887
1888#[async_trait]
1889impl Tool for TimestampTool {
1890    fn name(&self) -> &str {
1891        "timestamp"
1892    }
1893
1894    fn description(&self) -> &str {
1895        "Work with timestamps and date/time operations. Supports current time, formatting, parsing, and time arithmetic."
1896    }
1897
1898    fn parameters(&self) -> HashMap<String, ToolParameter> {
1899        let mut params = HashMap::new();
1900        params.insert(
1901            "operation".to_string(),
1902            ToolParameter {
1903                param_type: "string".to_string(),
1904                description: "Operation: 'now', 'format', 'parse', 'add', 'subtract', 'diff'"
1905                    .to_string(),
1906                required: Some(true),
1907            },
1908        );
1909        params.insert(
1910            "timestamp".to_string(),
1911            ToolParameter {
1912                param_type: "string".to_string(),
1913                description: "Timestamp string for parse/format operations".to_string(),
1914                required: Some(false),
1915            },
1916        );
1917        params.insert(
1918            "format".to_string(),
1919            ToolParameter {
1920                param_type: "string".to_string(),
1921                description: "Date format string (default: RFC3339)".to_string(),
1922                required: Some(false),
1923            },
1924        );
1925        params.insert(
1926            "unit".to_string(),
1927            ToolParameter {
1928                param_type: "string".to_string(),
1929                description: "Time unit for arithmetic: 'seconds', 'minutes', 'hours', 'days'"
1930                    .to_string(),
1931                required: Some(false),
1932            },
1933        );
1934        params.insert(
1935            "amount".to_string(),
1936            ToolParameter {
1937                param_type: "number".to_string(),
1938                description: "Amount for add/subtract operations".to_string(),
1939                required: Some(false),
1940            },
1941        );
1942        params.insert(
1943            "timestamp1".to_string(),
1944            ToolParameter {
1945                param_type: "string".to_string(),
1946                description: "First timestamp for diff operation".to_string(),
1947                required: Some(false),
1948            },
1949        );
1950        params.insert(
1951            "timestamp2".to_string(),
1952            ToolParameter {
1953                param_type: "string".to_string(),
1954                description: "Second timestamp for diff operation".to_string(),
1955                required: Some(false),
1956            },
1957        );
1958        params
1959    }
1960
1961    async fn execute(&self, args: Value) -> Result<ToolResult> {
1962        let operation = args
1963            .get("operation")
1964            .and_then(|v| v.as_str())
1965            .ok_or_else(|| HeliosError::ToolError("Missing 'operation' parameter".to_string()))?;
1966
1967        match operation {
1968            "now" => {
1969                let now = chrono::Utc::now();
1970                let timestamp = now.timestamp();
1971                let rfc3339 = now.to_rfc3339();
1972
1973                Ok(ToolResult::success(format!(
1974                    "Current time:\nUnix timestamp: {}\nRFC3339: {}\nLocal: {}",
1975                    timestamp,
1976                    rfc3339,
1977                    now.with_timezone(&chrono::Local::now().timezone())
1978                )))
1979            }
1980            "format" => {
1981                let timestamp_str =
1982                    args.get("timestamp")
1983                        .and_then(|v| v.as_str())
1984                        .ok_or_else(|| {
1985                            HeliosError::ToolError(
1986                                "Missing 'timestamp' parameter for format operation".to_string(),
1987                            )
1988                        })?;
1989
1990                let format_str = args
1991                    .get("format")
1992                    .and_then(|v| v.as_str())
1993                    .unwrap_or("%Y-%m-%d %H:%M:%S");
1994
1995                // Try parsing as unix timestamp first, then as RFC3339
1996                let dt = if let Ok(ts) = timestamp_str.parse::<i64>() {
1997                    chrono::DateTime::from_timestamp(ts, 0).ok_or_else(|| {
1998                        HeliosError::ToolError("Invalid unix timestamp".to_string())
1999                    })?
2000                } else {
2001                    chrono::DateTime::parse_from_rfc3339(timestamp_str)
2002                        .map(|dt| dt.with_timezone(&chrono::Utc))
2003                        .or_else(|_| {
2004                            chrono::NaiveDateTime::parse_from_str(timestamp_str, format_str)
2005                                .map(|ndt| ndt.and_utc())
2006                                .map_err(|e| {
2007                                    HeliosError::ToolError(format!(
2008                                        "Failed to parse timestamp: {}",
2009                                        e
2010                                    ))
2011                                })
2012                        })
2013                        .map_err(|e| {
2014                            HeliosError::ToolError(format!("Failed to parse timestamp: {}", e))
2015                        })?
2016                };
2017
2018                let formatted = dt.format(format_str).to_string();
2019                Ok(ToolResult::success(format!(
2020                    "Formatted timestamp: {}",
2021                    formatted
2022                )))
2023            }
2024            "parse" => {
2025                let timestamp_str =
2026                    args.get("timestamp")
2027                        .and_then(|v| v.as_str())
2028                        .ok_or_else(|| {
2029                            HeliosError::ToolError(
2030                                "Missing 'timestamp' parameter for parse operation".to_string(),
2031                            )
2032                        })?;
2033
2034                let format_str = args
2035                    .get("format")
2036                    .and_then(|v| v.as_str())
2037                    .unwrap_or("%Y-%m-%d %H:%M:%S");
2038
2039                // Try multiple parsing strategies
2040                let dt = chrono::DateTime::parse_from_rfc3339(timestamp_str)
2041                    .map(|dt| dt.with_timezone(&chrono::Utc))
2042                    .or_else(|_| {
2043                        chrono::NaiveDateTime::parse_from_str(timestamp_str, format_str)
2044                            .map(|ndt| ndt.and_utc())
2045                    })
2046                    .map_err(|e| {
2047                        HeliosError::ToolError(format!("Failed to parse timestamp: {}", e))
2048                    })?;
2049
2050                let unix_ts = dt.timestamp();
2051                let rfc3339 = dt.to_rfc3339();
2052
2053                Ok(ToolResult::success(format!(
2054                    "Parsed timestamp:\nUnix: {}\nRFC3339: {}\nFormatted: {}",
2055                    unix_ts,
2056                    rfc3339,
2057                    dt.format("%Y-%m-%d %H:%M:%S UTC")
2058                )))
2059            }
2060            "add" | "subtract" => {
2061                let default_timestamp = chrono::Utc::now().to_rfc3339();
2062                let timestamp_str = args
2063                    .get("timestamp")
2064                    .and_then(|v| v.as_str())
2065                    .unwrap_or(&default_timestamp);
2066
2067                let unit = args.get("unit").and_then(|v| v.as_str()).ok_or_else(|| {
2068                    HeliosError::ToolError(
2069                        "Missing 'unit' parameter for arithmetic operation".to_string(),
2070                    )
2071                })?;
2072
2073                let amount = args.get("amount").and_then(|v| v.as_i64()).ok_or_else(|| {
2074                    HeliosError::ToolError(
2075                        "Missing 'amount' parameter for arithmetic operation".to_string(),
2076                    )
2077                })?;
2078
2079                let dt = chrono::DateTime::parse_from_rfc3339(timestamp_str)
2080                    .or_else(|_| {
2081                        if let Ok(ts) = timestamp_str.parse::<i64>() {
2082                            chrono::DateTime::from_timestamp(ts, 0)
2083                                .ok_or_else(|| {
2084                                    HeliosError::ToolError("Invalid unix timestamp".to_string())
2085                                })
2086                                .map(|dt| dt.into())
2087                        } else {
2088                            Err(HeliosError::ToolError(
2089                                "Invalid timestamp format".to_string(),
2090                            ))
2091                        }
2092                    })
2093                    .map_err(|e| {
2094                        HeliosError::ToolError(format!("Failed to parse timestamp: {}", e))
2095                    })?;
2096
2097                let duration = match unit {
2098                    "seconds" => chrono::Duration::seconds(amount),
2099                    "minutes" => chrono::Duration::minutes(amount),
2100                    "hours" => chrono::Duration::hours(amount),
2101                    "days" => chrono::Duration::days(amount),
2102                    _ => {
2103                        return Err(HeliosError::ToolError(format!(
2104                            "Unknown unit '{}'. Use: seconds, minutes, hours, days",
2105                            unit
2106                        )))
2107                    }
2108                };
2109
2110                let result_dt = if operation == "add" {
2111                    dt + duration
2112                } else {
2113                    dt - duration
2114                };
2115
2116                Ok(ToolResult::success(format!(
2117                    "{} {} {} to {}\nResult: {}\nUnix: {}",
2118                    if operation == "add" {
2119                        "Added"
2120                    } else {
2121                        "Subtracted"
2122                    },
2123                    amount.abs(),
2124                    unit,
2125                    timestamp_str,
2126                    result_dt.to_rfc3339(),
2127                    result_dt.timestamp()
2128                )))
2129            }
2130            "diff" => {
2131                let ts1_str = args
2132                    .get("timestamp1")
2133                    .and_then(|v| v.as_str())
2134                    .ok_or_else(|| {
2135                        HeliosError::ToolError(
2136                            "Missing 'timestamp1' parameter for diff operation".to_string(),
2137                        )
2138                    })?;
2139
2140                let ts2_str = args
2141                    .get("timestamp2")
2142                    .and_then(|v| v.as_str())
2143                    .ok_or_else(|| {
2144                        HeliosError::ToolError(
2145                            "Missing 'timestamp2' parameter for diff operation".to_string(),
2146                        )
2147                    })?;
2148
2149                let dt1 = parse_timestamp(ts1_str)?;
2150                let dt2 = parse_timestamp(ts2_str)?;
2151
2152                let duration = if dt1 > dt2 { dt1 - dt2 } else { dt2 - dt1 };
2153                let seconds = duration.num_seconds();
2154                let minutes = duration.num_minutes();
2155                let hours = duration.num_hours();
2156                let days = duration.num_days();
2157
2158                Ok(ToolResult::success(format!(
2159                    "Time difference between {} and {}:\n{} seconds\n{} minutes\n{} hours\n{} days",
2160                    ts1_str, ts2_str, seconds, minutes, hours, days
2161                )))
2162            }
2163            _ => Err(HeliosError::ToolError(format!(
2164                "Unknown operation '{}'. Valid operations: now, format, parse, add, subtract, diff",
2165                operation
2166            ))),
2167        }
2168    }
2169}
2170
2171/// Parse a timestamp string using multiple strategies.
2172fn parse_timestamp(ts_str: &str) -> Result<chrono::DateTime<chrono::Utc>> {
2173    if let Ok(ts) = ts_str.parse::<i64>() {
2174        chrono::DateTime::from_timestamp(ts, 0)
2175            .ok_or_else(|| HeliosError::ToolError("Invalid unix timestamp".to_string()))
2176    } else {
2177        chrono::DateTime::parse_from_rfc3339(ts_str)
2178            .map(|dt| dt.with_timezone(&chrono::Utc))
2179            .map_err(|_| HeliosError::ToolError("Invalid timestamp format".to_string()))
2180    }
2181}
2182
2183/// A tool for basic file I/O operations.
2184pub struct FileIOTool;
2185
2186#[async_trait]
2187impl Tool for FileIOTool {
2188    fn name(&self) -> &str {
2189        "file_io"
2190    }
2191
2192    fn description(&self) -> &str {
2193        "Basic file operations: read, write, append, delete, copy, move. Unified interface for common file I/O tasks. Delete operation is safe by default (only empty directories)."
2194    }
2195
2196    fn parameters(&self) -> HashMap<String, ToolParameter> {
2197        let mut params = HashMap::new();
2198        params.insert(
2199            "operation".to_string(),
2200            ToolParameter {
2201                param_type: "string".to_string(),
2202                description: "Operation: 'read', 'write', 'append', 'delete', 'copy', 'move', 'exists', 'size' (delete is safe by default)".to_string(),
2203                required: Some(true),
2204            },
2205        );
2206        params.insert(
2207            "path".to_string(),
2208            ToolParameter {
2209                param_type: "string".to_string(),
2210                description: "File path for operations".to_string(),
2211                required: Some(false),
2212            },
2213        );
2214        params.insert(
2215            "src_path".to_string(),
2216            ToolParameter {
2217                param_type: "string".to_string(),
2218                description: "Source path for copy/move operations".to_string(),
2219                required: Some(false),
2220            },
2221        );
2222        params.insert(
2223            "dst_path".to_string(),
2224            ToolParameter {
2225                param_type: "string".to_string(),
2226                description: "Destination path for copy/move operations".to_string(),
2227                required: Some(false),
2228            },
2229        );
2230        params.insert(
2231            "content".to_string(),
2232            ToolParameter {
2233                param_type: "string".to_string(),
2234                description: "Content for write/append operations".to_string(),
2235                required: Some(false),
2236            },
2237        );
2238        params.insert(
2239            "recursive".to_string(),
2240            ToolParameter {
2241                param_type: "boolean".to_string(),
2242                description: "Allow recursive directory deletion (default: false for safety)"
2243                    .to_string(),
2244                required: Some(false),
2245            },
2246        );
2247        params
2248    }
2249
2250    async fn execute(&self, args: Value) -> Result<ToolResult> {
2251        let operation = args
2252            .get("operation")
2253            .and_then(|v| v.as_str())
2254            .ok_or_else(|| HeliosError::ToolError("Missing 'operation' parameter".to_string()))?;
2255
2256        match operation {
2257            "read" => {
2258                let path = args
2259                    .get("path")
2260                    .and_then(|v| v.as_str())
2261                    .ok_or_else(|| HeliosError::ToolError("Missing 'path' parameter for read operation".to_string()))?;
2262
2263                let content = std::fs::read_to_string(path)
2264                    .map_err(|e| HeliosError::ToolError(format!("Failed to read file: {}", e)))?;
2265
2266                Ok(ToolResult::success(format!(
2267                    "File: {}\nSize: {} bytes\n\n{}",
2268                    path,
2269                    content.len(),
2270                    content
2271                )))
2272            }
2273            "write" => {
2274                let path = args
2275                    .get("path")
2276                    .and_then(|v| v.as_str())
2277                    .ok_or_else(|| HeliosError::ToolError("Missing 'path' parameter for write operation".to_string()))?;
2278
2279                let content = args
2280                    .get("content")
2281                    .and_then(|v| v.as_str())
2282                    .ok_or_else(|| HeliosError::ToolError("Missing 'content' parameter for write operation".to_string()))?;
2283
2284                // Create parent directories if they don't exist
2285                if let Some(parent) = std::path::Path::new(path).parent() {
2286                    std::fs::create_dir_all(parent).map_err(|e| {
2287                        HeliosError::ToolError(format!("Failed to create directories: {}", e))
2288                    })?;
2289                }
2290
2291                std::fs::write(path, content)
2292                    .map_err(|e| HeliosError::ToolError(format!("Failed to write file: {}", e)))?;
2293
2294                Ok(ToolResult::success(format!(
2295                    "✓ Wrote {} bytes to {}",
2296                    content.len(),
2297                    path
2298                )))
2299            }
2300            "append" => {
2301                let path = args
2302                    .get("path")
2303                    .and_then(|v| v.as_str())
2304                    .ok_or_else(|| HeliosError::ToolError("Missing 'path' parameter for append operation".to_string()))?;
2305
2306                let content = args
2307                    .get("content")
2308                    .and_then(|v| v.as_str())
2309                    .ok_or_else(|| HeliosError::ToolError("Missing 'content' parameter for append operation".to_string()))?;
2310
2311                std::fs::OpenOptions::new()
2312                    .create(true)
2313                    .append(true)
2314                    .open(path)
2315                    .and_then(|mut file| std::io::Write::write_all(&mut file, content.as_bytes()))
2316                    .map_err(|e| HeliosError::ToolError(format!("Failed to append to file: {}", e)))?;
2317
2318                Ok(ToolResult::success(format!(
2319                    "✓ Appended {} bytes to {}",
2320                    content.len(),
2321                    path
2322                )))
2323            }
2324            "delete" => {
2325                let path = args
2326                    .get("path")
2327                    .and_then(|v| v.as_str())
2328                    .ok_or_else(|| HeliosError::ToolError("Missing 'path' parameter for delete operation".to_string()))?;
2329
2330                let recursive = args.get("recursive").and_then(|v| v.as_bool()).unwrap_or(false);
2331
2332                let metadata = std::fs::metadata(path)
2333                    .map_err(|e| HeliosError::ToolError(format!("Cannot access file: {}", e)))?;
2334
2335                let file_type = if metadata.is_file() { "file" } else { "directory" };
2336
2337                if metadata.is_file() {
2338                    std::fs::remove_file(path)
2339                        .map_err(|e| HeliosError::ToolError(format!("Failed to delete file: {}", e)))?;
2340                } else if recursive {
2341                    // Recursive deletion allowed when explicitly requested
2342                    std::fs::remove_dir_all(path)
2343                        .map_err(|e| HeliosError::ToolError(format!("Failed to delete directory recursively: {}", e)))?;
2344                } else {
2345                    // Safe by default: only delete empty directories
2346                    std::fs::remove_dir(path)
2347                        .map_err(|e| HeliosError::ToolError(format!("Failed to delete directory (must be empty, or set recursive=true): {}", e)))?;
2348                }
2349
2350                let delete_type = if recursive && !metadata.is_file() { "recursively" } else { "" };
2351                let separator = if delete_type.is_empty() { "" } else { ": " };
2352                Ok(ToolResult::success(format!(
2353                    "✓ Deleted {} {}{}{}",
2354                    file_type, delete_type, separator, path
2355                )))
2356            }
2357            "copy" => {
2358                let src_path = args
2359                    .get("src_path")
2360                    .and_then(|v| v.as_str())
2361                    .ok_or_else(|| HeliosError::ToolError("Missing 'src_path' parameter for copy operation".to_string()))?;
2362
2363                let dst_path = args
2364                    .get("dst_path")
2365                    .and_then(|v| v.as_str())
2366                    .ok_or_else(|| HeliosError::ToolError("Missing 'dst_path' parameter for copy operation".to_string()))?;
2367
2368                std::fs::copy(src_path, dst_path)
2369                    .map_err(|e| HeliosError::ToolError(format!("Failed to copy file: {}", e)))?;
2370
2371                Ok(ToolResult::success(format!(
2372                    "✓ Copied {} to {}",
2373                    src_path, dst_path
2374                )))
2375            }
2376            "move" => {
2377                let src_path = args
2378                    .get("src_path")
2379                    .and_then(|v| v.as_str())
2380                    .ok_or_else(|| HeliosError::ToolError("Missing 'src_path' parameter for move operation".to_string()))?;
2381
2382                let dst_path = args
2383                    .get("dst_path")
2384                    .and_then(|v| v.as_str())
2385                    .ok_or_else(|| HeliosError::ToolError("Missing 'dst_path' parameter for move operation".to_string()))?;
2386
2387                std::fs::rename(src_path, dst_path)
2388                    .map_err(|e| HeliosError::ToolError(format!("Failed to move file: {}", e)))?;
2389
2390                Ok(ToolResult::success(format!(
2391                    "✓ Moved {} to {}",
2392                    src_path, dst_path
2393                )))
2394            }
2395            "exists" => {
2396                let path = args
2397                    .get("path")
2398                    .and_then(|v| v.as_str())
2399                    .ok_or_else(|| HeliosError::ToolError("Missing 'path' parameter for exists operation".to_string()))?;
2400
2401                let exists = std::path::Path::new(path).exists();
2402                let file_type = if exists {
2403                    if std::fs::metadata(path).map(|m| m.is_file()).unwrap_or(false) {
2404                        "file"
2405                    } else {
2406                        "directory"
2407                    }
2408                } else {
2409                    "nonexistent"
2410                };
2411
2412                Ok(ToolResult::success(format!(
2413                    "Path '{}' exists: {} ({})",
2414                    path, exists, file_type
2415                )))
2416            }
2417            "size" => {
2418                let path = args
2419                    .get("path")
2420                    .and_then(|v| v.as_str())
2421                    .ok_or_else(|| HeliosError::ToolError("Missing 'path' parameter for size operation".to_string()))?;
2422
2423                let metadata = std::fs::metadata(path)
2424                    .map_err(|e| HeliosError::ToolError(format!("Cannot access file: {}", e)))?;
2425
2426                let size = metadata.len();
2427                Ok(ToolResult::success(format!(
2428                    "Size of '{}': {} bytes",
2429                    path, size
2430                )))
2431            }
2432            _ => Err(HeliosError::ToolError(format!(
2433                "Unknown operation '{}'. Valid operations: read, write, append, delete, copy, move, exists, size",
2434                operation
2435            ))),
2436        }
2437    }
2438}
2439
2440/// A tool for executing shell commands safely.
2441pub struct ShellCommandTool;
2442
2443#[async_trait]
2444impl Tool for ShellCommandTool {
2445    fn name(&self) -> &str {
2446        "shell_command"
2447    }
2448
2449    fn description(&self) -> &str {
2450        "Execute shell commands with safety restrictions. Limited to basic commands, no destructive operations allowed."
2451    }
2452
2453    fn parameters(&self) -> HashMap<String, ToolParameter> {
2454        let mut params = HashMap::new();
2455        params.insert(
2456            "command".to_string(),
2457            ToolParameter {
2458                param_type: "string".to_string(),
2459                description: "Shell command to execute".to_string(),
2460                required: Some(true),
2461            },
2462        );
2463        params.insert(
2464            "timeout_seconds".to_string(),
2465            ToolParameter {
2466                param_type: "number".to_string(),
2467                description: "Command timeout in seconds (default: 30, max: 60)".to_string(),
2468                required: Some(false),
2469            },
2470        );
2471        params
2472    }
2473
2474    async fn execute(&self, args: Value) -> Result<ToolResult> {
2475        let command = args
2476            .get("command")
2477            .and_then(|v| v.as_str())
2478            .ok_or_else(|| HeliosError::ToolError("Missing 'command' parameter".to_string()))?;
2479
2480        let timeout_seconds = args
2481            .get("timeout_seconds")
2482            .and_then(|v| v.as_u64())
2483            .unwrap_or(30)
2484            .min(60); // Max 60 seconds
2485
2486        // Safety check - block dangerous commands
2487        let dangerous_patterns = [
2488            "rm ",
2489            "rmdir",
2490            "del ",
2491            "format",
2492            "fdisk",
2493            "mkfs",
2494            "dd ",
2495            "shred",
2496            "wipe",
2497            "sudo",
2498            "su ",
2499            "chmod 777",
2500            "chown root",
2501            "passwd",
2502            "usermod",
2503            "userdel",
2504            ">",
2505            ">>",
2506            "|",
2507            ";",
2508            "&&",
2509            "||",
2510            "`",
2511            "$(",
2512        ];
2513
2514        for pattern in &dangerous_patterns {
2515            if command.contains(pattern) {
2516                return Err(HeliosError::ToolError(format!(
2517                    "Command blocked for safety: contains '{}'",
2518                    pattern
2519                )));
2520            }
2521        }
2522
2523        // Execute command with timeout
2524        let output = tokio::time::timeout(
2525            std::time::Duration::from_secs(timeout_seconds),
2526            tokio::process::Command::new("sh")
2527                .arg("-c")
2528                .arg(command)
2529                .output(),
2530        )
2531        .await
2532        .map_err(|_| {
2533            HeliosError::ToolError(format!(
2534                "Command timed out after {} seconds",
2535                timeout_seconds
2536            ))
2537        })?
2538        .map_err(|e| HeliosError::ToolError(format!("Failed to execute command: {}", e)))?;
2539
2540        let stdout = String::from_utf8_lossy(&output.stdout);
2541        let stderr = String::from_utf8_lossy(&output.stderr);
2542
2543        let exit_code = output.status.code().unwrap_or(-1);
2544
2545        let mut result = format!("Command: {}\nExit code: {}\n", command, exit_code);
2546
2547        if !stdout.is_empty() {
2548            result.push_str(&format!("Stdout:\n{}\n", stdout));
2549        }
2550
2551        if !stderr.is_empty() {
2552            result.push_str(&format!("Stderr:\n{}\n", stderr));
2553        }
2554
2555        if exit_code == 0 {
2556            Ok(ToolResult::success(result))
2557        } else {
2558            Ok(ToolResult::error(result))
2559        }
2560    }
2561}
2562
2563/// A tool for making HTTP requests.
2564pub struct HttpRequestTool;
2565
2566#[async_trait]
2567impl Tool for HttpRequestTool {
2568    fn name(&self) -> &str {
2569        "http_request"
2570    }
2571
2572    fn description(&self) -> &str {
2573        "Make HTTP requests with various methods. Supports GET, POST, PUT, DELETE with custom headers and body."
2574    }
2575
2576    fn parameters(&self) -> HashMap<String, ToolParameter> {
2577        let mut params = HashMap::new();
2578        params.insert(
2579            "method".to_string(),
2580            ToolParameter {
2581                param_type: "string".to_string(),
2582                description: "HTTP method: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS"
2583                    .to_string(),
2584                required: Some(true),
2585            },
2586        );
2587        params.insert(
2588            "url".to_string(),
2589            ToolParameter {
2590                param_type: "string".to_string(),
2591                description: "Request URL".to_string(),
2592                required: Some(true),
2593            },
2594        );
2595        params.insert(
2596            "headers".to_string(),
2597            ToolParameter {
2598                param_type: "object".to_string(),
2599                description: "Request headers as JSON object (optional)".to_string(),
2600                required: Some(false),
2601            },
2602        );
2603        params.insert(
2604            "body".to_string(),
2605            ToolParameter {
2606                param_type: "string".to_string(),
2607                description: "Request body for POST/PUT/PATCH methods".to_string(),
2608                required: Some(false),
2609            },
2610        );
2611        params.insert(
2612            "timeout_seconds".to_string(),
2613            ToolParameter {
2614                param_type: "number".to_string(),
2615                description: "Request timeout in seconds (default: 30)".to_string(),
2616                required: Some(false),
2617            },
2618        );
2619        params
2620    }
2621
2622    async fn execute(&self, args: Value) -> Result<ToolResult> {
2623        let method = args
2624            .get("method")
2625            .and_then(|v| v.as_str())
2626            .ok_or_else(|| HeliosError::ToolError("Missing 'method' parameter".to_string()))?;
2627
2628        let url = args
2629            .get("url")
2630            .and_then(|v| v.as_str())
2631            .ok_or_else(|| HeliosError::ToolError("Missing 'url' parameter".to_string()))?;
2632
2633        let timeout_seconds = args
2634            .get("timeout_seconds")
2635            .and_then(|v| v.as_u64())
2636            .unwrap_or(30);
2637
2638        let client = reqwest::Client::builder()
2639            .timeout(std::time::Duration::from_secs(timeout_seconds))
2640            .build()
2641            .map_err(|e| HeliosError::ToolError(format!("Failed to create HTTP client: {}", e)))?;
2642
2643        let mut request = match method.to_uppercase().as_str() {
2644            "GET" => client.get(url),
2645            "POST" => client.post(url),
2646            "PUT" => client.put(url),
2647            "DELETE" => client.delete(url),
2648            "PATCH" => client.patch(url),
2649            "HEAD" => client.head(url),
2650            _ => {
2651                return Err(HeliosError::ToolError(format!(
2652                    "Unsupported HTTP method: {}",
2653                    method
2654                )))
2655            }
2656        };
2657
2658        // Add headers
2659        if let Some(headers) = args.get("headers") {
2660            if let Some(headers_obj) = headers.as_object() {
2661                for (key, value) in headers_obj {
2662                    if let Some(value_str) = value.as_str() {
2663                        request = request.header(key, value_str);
2664                    }
2665                }
2666            }
2667        }
2668
2669        // Add body for methods that support it
2670        if matches!(method.to_uppercase().as_str(), "POST" | "PUT" | "PATCH") {
2671            if let Some(body) = args.get("body").and_then(|v| v.as_str()) {
2672                request = request.body(body.to_string());
2673            }
2674        }
2675
2676        let response = request
2677            .send()
2678            .await
2679            .map_err(|e| HeliosError::ToolError(format!("HTTP request failed: {}", e)))?;
2680
2681        let status = response.status();
2682        let headers = response.headers().clone();
2683        let body = response
2684            .text()
2685            .await
2686            .unwrap_or_else(|_| "Binary content".to_string());
2687
2688        let mut result = format!(
2689            "HTTP {} {}\nStatus: {}\n\n",
2690            method.to_uppercase(),
2691            url,
2692            status
2693        );
2694
2695        // Add response headers
2696        result.push_str("Response Headers:\n");
2697        for (name, value) in headers.iter() {
2698            if let Ok(value_str) = value.to_str() {
2699                result.push_str(&format!("{}: {}\n", name, value_str));
2700            }
2701        }
2702        result.push_str("\nResponse Body:\n");
2703        result.push_str(&body);
2704
2705        if status.is_success() {
2706            Ok(ToolResult::success(result))
2707        } else {
2708            Ok(ToolResult::error(result))
2709        }
2710    }
2711}
2712
2713/// A tool for listing directory contents.
2714pub struct FileListTool;
2715
2716#[async_trait]
2717impl Tool for FileListTool {
2718    fn name(&self) -> &str {
2719        "file_list"
2720    }
2721
2722    fn description(&self) -> &str {
2723        "List directory contents with detailed information including file sizes, types, and modification times."
2724    }
2725
2726    fn parameters(&self) -> HashMap<String, ToolParameter> {
2727        let mut params = HashMap::new();
2728        params.insert(
2729            "path".to_string(),
2730            ToolParameter {
2731                param_type: "string".to_string(),
2732                description: "Directory path to list (default: current directory)".to_string(),
2733                required: Some(false),
2734            },
2735        );
2736        params.insert(
2737            "show_hidden".to_string(),
2738            ToolParameter {
2739                param_type: "boolean".to_string(),
2740                description: "Show hidden files/directories (default: false)".to_string(),
2741                required: Some(false),
2742            },
2743        );
2744        params.insert(
2745            "recursive".to_string(),
2746            ToolParameter {
2747                param_type: "boolean".to_string(),
2748                description: "List contents recursively (default: false)".to_string(),
2749                required: Some(false),
2750            },
2751        );
2752        params.insert(
2753            "max_depth".to_string(),
2754            ToolParameter {
2755                param_type: "number".to_string(),
2756                description: "Maximum recursion depth (default: 3)".to_string(),
2757                required: Some(false),
2758            },
2759        );
2760        params
2761    }
2762
2763    async fn execute(&self, args: Value) -> Result<ToolResult> {
2764        let base_path = args.get("path").and_then(|v| v.as_str()).unwrap_or(".");
2765        let show_hidden = args
2766            .get("show_hidden")
2767            .and_then(|v| v.as_bool())
2768            .unwrap_or(false);
2769        let recursive = args
2770            .get("recursive")
2771            .and_then(|v| v.as_bool())
2772            .unwrap_or(false);
2773        let max_depth = args.get("max_depth").and_then(|v| v.as_u64()).unwrap_or(3) as usize;
2774
2775        let mut results = Vec::new();
2776
2777        if recursive {
2778            for entry in walkdir::WalkDir::new(base_path)
2779                .max_depth(max_depth)
2780                .into_iter()
2781                .filter_map(|e| e.ok())
2782            {
2783                if let Some(entry_info) = format_walkdir_entry(&entry, show_hidden) {
2784                    results.push(entry_info);
2785                }
2786            }
2787        } else {
2788            let entries = std::fs::read_dir(base_path)
2789                .map_err(|e| HeliosError::ToolError(format!("Failed to read directory: {}", e)))?;
2790
2791            for entry in entries.filter_map(|e| e.ok()) {
2792                if let Some(entry_info) = format_entry(&entry, show_hidden) {
2793                    results.push(entry_info);
2794                }
2795            }
2796        }
2797
2798        results.sort();
2799
2800        let mut output = format!("Directory listing for: {}\n\n", base_path);
2801        output.push_str(&format!("Total items: {}\n\n", results.len()));
2802
2803        for entry in results {
2804            output.push_str(&entry);
2805            output.push('\n');
2806        }
2807
2808        Ok(ToolResult::success(output))
2809    }
2810}
2811
2812/// Format a walkdir directory entry with metadata.
2813fn format_walkdir_entry(entry: &walkdir::DirEntry, show_hidden: bool) -> Option<String> {
2814    let path = entry.path();
2815    let file_name = path.file_name()?.to_str()?;
2816
2817    // Skip hidden files if not requested
2818    if !show_hidden && file_name.starts_with('.') {
2819        return None;
2820    }
2821
2822    let metadata = entry.metadata().ok()?;
2823
2824    let file_type = if metadata.is_dir() { "DIR" } else { "FILE" };
2825    let size = metadata.len();
2826    let modified = metadata.modified().ok()?;
2827    let modified_dt = chrono::DateTime::<chrono::Local>::from(modified);
2828    let modified_str = modified_dt.format("%Y-%m-%d %H:%M:%S").to_string();
2829
2830    Some(format!(
2831        "{:4} {:>8} {} {}",
2832        file_type,
2833        size,
2834        modified_str,
2835        path.display()
2836    ))
2837}
2838
2839/// Format a directory entry with metadata.
2840fn format_entry(entry: &std::fs::DirEntry, show_hidden: bool) -> Option<String> {
2841    let path = entry.path();
2842    let file_name = path.file_name()?.to_str()?;
2843
2844    // Skip hidden files if not requested
2845    if !show_hidden && file_name.starts_with('.') {
2846        return None;
2847    }
2848
2849    let metadata = entry.metadata().ok()?;
2850
2851    let file_type = if metadata.is_dir() { "DIR" } else { "FILE" };
2852    let size = metadata.len();
2853    let modified = metadata.modified().ok()?;
2854    let modified_dt = chrono::DateTime::<chrono::Local>::from(modified);
2855    let modified_str = modified_dt.format("%Y-%m-%d %H:%M:%S").to_string();
2856
2857    Some(format!(
2858        "{:4} {:>8} {} {}",
2859        file_type,
2860        size,
2861        modified_str,
2862        path.display()
2863    ))
2864}
2865
2866/// A tool for retrieving system information.
2867pub struct SystemInfoTool;
2868
2869#[async_trait]
2870impl Tool for SystemInfoTool {
2871    fn name(&self) -> &str {
2872        "system_info"
2873    }
2874
2875    fn description(&self) -> &str {
2876        "Retrieve system information including OS, CPU, memory, disk usage, and network interfaces."
2877    }
2878
2879    fn parameters(&self) -> HashMap<String, ToolParameter> {
2880        let mut params = HashMap::new();
2881        params.insert(
2882            "category".to_string(),
2883            ToolParameter {
2884                param_type: "string".to_string(),
2885                description:
2886                    "Info category: 'all', 'os', 'cpu', 'memory', 'disk', 'network' (default: all)"
2887                        .to_string(),
2888                required: Some(false),
2889            },
2890        );
2891        params
2892    }
2893
2894    async fn execute(&self, args: Value) -> Result<ToolResult> {
2895        let category = args
2896            .get("category")
2897            .and_then(|v| v.as_str())
2898            .unwrap_or("all");
2899
2900        let mut system = sysinfo::System::new_all();
2901        system.refresh_all();
2902
2903        let disks = sysinfo::Disks::new_with_refreshed_list();
2904        let networks = sysinfo::Networks::new_with_refreshed_list();
2905
2906        let mut output = String::new();
2907
2908        match category {
2909            "all" => {
2910                output.push_str(&get_os_info(&system));
2911                output.push_str(&get_cpu_info(&system));
2912                output.push_str(&get_memory_info(&system));
2913                output.push_str(&get_disk_info(&disks));
2914                output.push_str(&get_network_info(&networks));
2915            }
2916            "os" => output.push_str(&get_os_info(&system)),
2917            "cpu" => output.push_str(&get_cpu_info(&system)),
2918            "memory" => output.push_str(&get_memory_info(&system)),
2919            "disk" => output.push_str(&get_disk_info(&disks)),
2920            "network" => output.push_str(&get_network_info(&networks)),
2921            _ => {
2922                return Err(HeliosError::ToolError(format!(
2923                    "Unknown category '{}'. Use: all, os, cpu, memory, disk, network",
2924                    category
2925                )))
2926            }
2927        }
2928
2929        Ok(ToolResult::success(output))
2930    }
2931}
2932
2933/// Get operating system information.
2934fn get_os_info(_system: &sysinfo::System) -> String {
2935    let mut info = String::from("=== Operating System ===\n");
2936
2937    info.push_str(&format!("OS: {}\n", std::env::consts::OS));
2938    info.push_str(&format!("Architecture: {}\n", std::env::consts::ARCH));
2939    info.push_str(&format!("Family: {}\n", std::env::consts::FAMILY));
2940
2941    if let Ok(hostname) = hostname::get() {
2942        if let Some(hostname_str) = hostname.to_str() {
2943            info.push_str(&format!("Hostname: {}\n", hostname_str));
2944        }
2945    }
2946
2947    info.push_str(&format!("Uptime: {} seconds\n", sysinfo::System::uptime()));
2948    info.push('\n');
2949
2950    info
2951}
2952
2953/// Get CPU information.
2954fn get_cpu_info(system: &sysinfo::System) -> String {
2955    let mut info = String::from("=== CPU Information ===\n");
2956
2957    info.push_str(&format!(
2958        "Physical cores: {}\n",
2959        sysinfo::System::physical_core_count().unwrap_or(0)
2960    ));
2961    info.push_str(&format!("Logical cores: {}\n", system.cpus().len()));
2962
2963    for (i, cpu) in system.cpus().iter().enumerate() {
2964        if i >= 4 {
2965            // Limit output to first 4 CPUs
2966            info.push_str("... and more CPUs\n");
2967            break;
2968        }
2969        info.push_str(&format!("CPU {}: {:.1}% usage\n", i, cpu.cpu_usage()));
2970    }
2971
2972    info.push('\n');
2973    info
2974}
2975
2976/// Get memory information.
2977fn get_memory_info(system: &sysinfo::System) -> String {
2978    let mut info = String::from("=== Memory Information ===\n");
2979
2980    let total_memory = system.total_memory();
2981    let used_memory = system.used_memory();
2982    let available_memory = system.available_memory();
2983
2984    info.push_str(&format!(
2985        "Total memory: {} MB\n",
2986        total_memory / 1024 / 1024
2987    ));
2988    info.push_str(&format!("Used memory: {} MB\n", used_memory / 1024 / 1024));
2989    info.push_str(&format!(
2990        "Available memory: {} MB\n",
2991        available_memory / 1024 / 1024
2992    ));
2993    info.push_str(&format!(
2994        "Memory usage: {:.1}%\n",
2995        (used_memory as f64 / total_memory as f64) * 100.0
2996    ));
2997
2998    info.push('\n');
2999    info
3000}
3001
3002/// Get disk information.
3003fn get_disk_info(disks: &sysinfo::Disks) -> String {
3004    let mut info = String::from("=== Disk Information ===\n");
3005
3006    for disk in disks.list() {
3007        let total_space = disk.total_space();
3008        let available_space = disk.available_space();
3009        let used_space = total_space - available_space;
3010
3011        info.push_str(&format!("Mount point: {}\n", disk.mount_point().display()));
3012        info.push_str(&format!(
3013            "File system: {}\n",
3014            disk.file_system().to_string_lossy()
3015        ));
3016        info.push_str(&format!(
3017            "Total space: {} GB\n",
3018            total_space / 1024 / 1024 / 1024
3019        ));
3020        info.push_str(&format!(
3021            "Used space: {} GB\n",
3022            used_space / 1024 / 1024 / 1024
3023        ));
3024        info.push_str(&format!(
3025            "Available space: {} GB\n",
3026            available_space / 1024 / 1024 / 1024
3027        ));
3028        info.push_str(&format!(
3029            "Usage: {:.1}%\n\n",
3030            (used_space as f64 / total_space as f64) * 100.0
3031        ));
3032    }
3033
3034    info
3035}
3036
3037/// Get network information.
3038fn get_network_info(networks: &sysinfo::Networks) -> String {
3039    let mut info = String::from("=== Network Information ===\n");
3040
3041    for (interface_name, data) in networks.list() {
3042        info.push_str(&format!("Interface: {}\n", interface_name));
3043
3044        // MAC address and IP information - simplified due to API changes
3045        info.push_str(&format!("Received: {} bytes\n", data.received()));
3046        info.push_str(&format!("Transmitted: {} bytes\n", data.transmitted()));
3047        info.push('\n');
3048    }
3049
3050    info
3051}
3052
3053/// A tool for text processing and manipulation operations.
3054pub struct TextProcessorTool;
3055
3056#[async_trait]
3057impl Tool for TextProcessorTool {
3058    fn name(&self) -> &str {
3059        "text_processor"
3060    }
3061
3062    fn description(&self) -> &str {
3063        "Process and manipulate text with operations like search, replace, split, join, count, and format."
3064    }
3065
3066    fn parameters(&self) -> HashMap<String, ToolParameter> {
3067        let mut params = HashMap::new();
3068        params.insert(
3069            "operation".to_string(),
3070            ToolParameter {
3071                param_type: "string".to_string(),
3072                description: "Operation: 'search', 'replace', 'split', 'join', 'count', 'uppercase', 'lowercase', 'trim', 'lines', 'words'".to_string(),
3073                required: Some(true),
3074            },
3075        );
3076        params.insert(
3077            "text".to_string(),
3078            ToolParameter {
3079                param_type: "string".to_string(),
3080                description: "Input text for processing".to_string(),
3081                required: Some(true),
3082            },
3083        );
3084        params.insert(
3085            "pattern".to_string(),
3086            ToolParameter {
3087                param_type: "string".to_string(),
3088                description: "Search pattern for search/replace/split operations".to_string(),
3089                required: Some(false),
3090            },
3091        );
3092        params.insert(
3093            "replacement".to_string(),
3094            ToolParameter {
3095                param_type: "string".to_string(),
3096                description: "Replacement text for replace operation".to_string(),
3097                required: Some(false),
3098            },
3099        );
3100        params.insert(
3101            "separator".to_string(),
3102            ToolParameter {
3103                param_type: "string".to_string(),
3104                description: "Separator for join/split operations (default: space)".to_string(),
3105                required: Some(false),
3106            },
3107        );
3108        params.insert(
3109            "case_sensitive".to_string(),
3110            ToolParameter {
3111                param_type: "boolean".to_string(),
3112                description: "Case sensitive search (default: true)".to_string(),
3113                required: Some(false),
3114            },
3115        );
3116        params
3117    }
3118
3119    async fn execute(&self, args: Value) -> Result<ToolResult> {
3120        let operation = args
3121            .get("operation")
3122            .and_then(|v| v.as_str())
3123            .ok_or_else(|| HeliosError::ToolError("Missing 'operation' parameter".to_string()))?;
3124
3125        let text = args
3126            .get("text")
3127            .and_then(|v| v.as_str())
3128            .ok_or_else(|| HeliosError::ToolError("Missing 'text' parameter".to_string()))?;
3129
3130        match operation {
3131            "search" => {
3132                let pattern = args
3133                    .get("pattern")
3134                    .and_then(|v| v.as_str())
3135                    .ok_or_else(|| HeliosError::ToolError("Missing 'pattern' parameter for search operation".to_string()))?;
3136
3137                let case_sensitive = args.get("case_sensitive").and_then(|v| v.as_bool()).unwrap_or(true);
3138
3139                let regex = if case_sensitive {
3140                    regex::Regex::new(pattern)
3141                } else {
3142                    regex::RegexBuilder::new(pattern).case_insensitive(true).build()
3143                }
3144                .map_err(|e| HeliosError::ToolError(format!("Invalid regex pattern: {}", e)))?;
3145
3146                let matches: Vec<(usize, &str)> = regex
3147                    .find_iter(text)
3148                    .map(|m| (m.start(), m.as_str()))
3149                    .collect();
3150
3151                let result = if matches.is_empty() {
3152                    "No matches found".to_string()
3153                } else {
3154                    let mut output = format!("Found {} match(es):\n", matches.len());
3155                    for (i, (pos, match_text)) in matches.iter().enumerate() {
3156                        output.push_str(&format!("{}. Position {}: '{}'\n", i + 1, pos, match_text));
3157                    }
3158                    output
3159                };
3160
3161                Ok(ToolResult::success(result))
3162            }
3163            "replace" => {
3164                let pattern = args
3165                    .get("pattern")
3166                    .and_then(|v| v.as_str())
3167                    .ok_or_else(|| HeliosError::ToolError("Missing 'pattern' parameter for replace operation".to_string()))?;
3168
3169                let replacement = args
3170                    .get("replacement")
3171                    .and_then(|v| v.as_str())
3172                    .unwrap_or("");
3173
3174                let case_sensitive = args.get("case_sensitive").and_then(|v| v.as_bool()).unwrap_or(true);
3175
3176                let regex = if case_sensitive {
3177                    regex::Regex::new(pattern)
3178                } else {
3179                    regex::RegexBuilder::new(pattern).case_insensitive(true).build()
3180                }
3181                .map_err(|e| HeliosError::ToolError(format!("Invalid regex pattern: {}", e)))?;
3182
3183                let result = regex.replace_all(text, replacement).to_string();
3184                let count = regex.find_iter(text).count();
3185
3186                Ok(ToolResult::success(format!(
3187                    "Replaced {} occurrence(s):\n\n{}",
3188                    count, result
3189                )))
3190            }
3191            "split" => {
3192                let separator = args
3193                    .get("separator")
3194                    .and_then(|v| v.as_str())
3195                    .unwrap_or(" ");
3196
3197                let parts: Vec<&str> = text.split(separator).collect();
3198                let result = format!(
3199                    "Split into {} parts:\n{}",
3200                    parts.len(),
3201                    parts.iter().enumerate()
3202                        .map(|(i, part)| format!("{}. '{}'", i + 1, part))
3203                        .collect::<Vec<_>>()
3204                        .join("\n")
3205                );
3206
3207                Ok(ToolResult::success(result))
3208            }
3209            "join" => {
3210                let separator = args
3211                    .get("separator")
3212                    .and_then(|v| v.as_str())
3213                    .unwrap_or(" ");
3214
3215                let lines: Vec<&str> = text.lines().collect();
3216                let result = lines.join(separator);
3217
3218                Ok(ToolResult::success(format!(
3219                    "Joined {} lines with '{}':\n{}",
3220                    lines.len(), separator, result
3221                )))
3222            }
3223            "count" => {
3224                let chars = text.chars().count();
3225                let bytes = text.len();
3226                let lines = text.lines().count();
3227                let words = text.split_whitespace().count();
3228
3229                Ok(ToolResult::success(format!(
3230                    "Text statistics:\nCharacters: {}\nBytes: {}\nLines: {}\nWords: {}",
3231                    chars, bytes, lines, words
3232                )))
3233            }
3234            "uppercase" => {
3235                Ok(ToolResult::success(text.to_uppercase()))
3236            }
3237            "lowercase" => {
3238                Ok(ToolResult::success(text.to_lowercase()))
3239            }
3240            "trim" => {
3241                Ok(ToolResult::success(text.trim().to_string()))
3242            }
3243            "lines" => {
3244                let lines: Vec<String> = text.lines()
3245                    .enumerate()
3246                    .map(|(i, line)| format!("{:4}: {}", i + 1, line))
3247                    .collect();
3248
3249                Ok(ToolResult::success(format!(
3250                    "Text with line numbers ({} lines):\n{}",
3251                    lines.len(),
3252                    lines.join("\n")
3253                )))
3254            }
3255            "words" => {
3256                let words: Vec<String> = text.split_whitespace()
3257                    .enumerate()
3258                    .map(|(i, word)| format!("{:4}: {}", i + 1, word))
3259                    .collect();
3260
3261                Ok(ToolResult::success(format!(
3262                    "Words ({} total):\n{}",
3263                    words.len(),
3264                    words.iter()
3265                        .collect::<Vec<_>>()
3266                        .chunks(10)
3267                        .enumerate()
3268                        .map(|(chunk_i, chunk)| {
3269                            format!("Line {}: {}", chunk_i + 1,
3270                                chunk.iter().map(|s| s.as_str()).collect::<Vec<_>>().join(" "))
3271                        })
3272                        .collect::<Vec<_>>()
3273                        .join("\n")
3274                )))
3275            }
3276            _ => Err(HeliosError::ToolError(format!(
3277                "Unknown operation '{}'. Valid operations: search, replace, split, join, count, uppercase, lowercase, trim, lines, words",
3278                operation
3279            ))),
3280        }
3281    }
3282}
3283
3284#[cfg(test)]
3285mod tests {
3286    use super::*;
3287    use serde_json::json;
3288
3289    /// Tests the creation of a successful `ToolResult`.
3290    #[test]
3291    fn test_tool_result_success() {
3292        let result = ToolResult::success("test output");
3293        assert!(result.success);
3294        assert_eq!(result.output, "test output");
3295    }
3296
3297    /// Tests the file search tool with a glob pattern.
3298    #[tokio::test]
3299    async fn test_file_search_tool_glob_pattern_precompiled_regex() {
3300        use std::time::{SystemTime, UNIX_EPOCH};
3301        let base_tmp = std::env::temp_dir();
3302        let pid = std::process::id();
3303        let nanos = SystemTime::now()
3304            .duration_since(UNIX_EPOCH)
3305            .unwrap()
3306            .as_nanos();
3307        let test_dir = base_tmp.join(format!("helios_fs_test_{}_{}", pid, nanos));
3308        std::fs::create_dir_all(&test_dir).unwrap();
3309
3310        // Create files
3311        let file_rs = test_dir.join("a.rs");
3312        let file_txt = test_dir.join("b.txt");
3313        let subdir = test_dir.join("subdir");
3314        std::fs::create_dir_all(&subdir).unwrap();
3315        let file_sub_rs = subdir.join("mod.rs");
3316        std::fs::write(&file_rs, "fn main() {}\n").unwrap();
3317        std::fs::write(&file_txt, "hello\n").unwrap();
3318        std::fs::write(&file_sub_rs, "pub fn x() {}\n").unwrap();
3319
3320        // Execute search with glob pattern
3321        let tool = FileSearchTool;
3322        let args = json!({
3323            "path": test_dir.to_string_lossy(),
3324            "pattern": "*.rs",
3325            "max_results": 50
3326        });
3327        let result = tool.execute(args).await.unwrap();
3328        assert!(result.success);
3329        let out = result.output;
3330        // Should find .rs files
3331        assert!(out.contains(&file_rs.to_string_lossy().to_string()));
3332        assert!(out.contains(&file_sub_rs.to_string_lossy().to_string()));
3333        // Should not include .txt
3334        assert!(!out.contains(&file_txt.to_string_lossy().to_string()));
3335
3336        // Cleanup
3337        let _ = std::fs::remove_dir_all(&test_dir);
3338    }
3339
3340    /// Tests the file search tool with an invalid pattern.
3341    #[tokio::test]
3342    async fn test_file_search_tool_invalid_pattern_fallback_contains() {
3343        use std::time::{SystemTime, UNIX_EPOCH};
3344        let base_tmp = std::env::temp_dir();
3345        let pid = std::process::id();
3346        let nanos = SystemTime::now()
3347            .duration_since(UNIX_EPOCH)
3348            .unwrap()
3349            .as_nanos();
3350        let test_dir = base_tmp.join(format!("helios_fs_test_invalid_{}_{}", pid, nanos));
3351        std::fs::create_dir_all(&test_dir).unwrap();
3352
3353        // Create file with '(' to be matched by substring fallback
3354        let special = test_dir.join("foo(bar).txt");
3355        std::fs::write(&special, "content\n").unwrap();
3356
3357        let tool = FileSearchTool;
3358        let args = json!({
3359            "path": test_dir.to_string_lossy(),
3360            "pattern": "(",
3361            "max_results": 50
3362        });
3363        let result = tool.execute(args).await.unwrap();
3364        assert!(result.success);
3365        let out = result.output;
3366        assert!(out.contains(&special.to_string_lossy().to_string()));
3367
3368        // Cleanup
3369        let _ = std::fs::remove_dir_all(&test_dir);
3370    }
3371
3372    /// Tests the creation of an error `ToolResult`.
3373    #[test]
3374    fn test_tool_result_error() {
3375        let result = ToolResult::error("test error");
3376        assert!(!result.success);
3377        assert_eq!(result.output, "test error");
3378    }
3379
3380    /// Tests the calculator tool.
3381    #[tokio::test]
3382    async fn test_calculator_tool() {
3383        let tool = CalculatorTool;
3384        assert_eq!(tool.name(), "calculator");
3385        assert_eq!(
3386            tool.description(),
3387            "Perform basic arithmetic operations. Supports +, -, *, / operations."
3388        );
3389
3390        let args = json!({"expression": "2 + 2"});
3391        let result = tool.execute(args).await.unwrap();
3392        assert!(result.success);
3393        assert_eq!(result.output, "4");
3394    }
3395
3396    /// Tests the calculator tool with multiplication.
3397    #[tokio::test]
3398    async fn test_calculator_tool_multiplication() {
3399        let tool = CalculatorTool;
3400        let args = json!({"expression": "3 * 4"});
3401        let result = tool.execute(args).await.unwrap();
3402        assert!(result.success);
3403        assert_eq!(result.output, "12");
3404    }
3405
3406    /// Tests the calculator tool with division.
3407    #[tokio::test]
3408    async fn test_calculator_tool_division() {
3409        let tool = CalculatorTool;
3410        let args = json!({"expression": "8 / 2"});
3411        let result = tool.execute(args).await.unwrap();
3412        assert!(result.success);
3413        assert_eq!(result.output, "4");
3414    }
3415
3416    /// Tests the calculator tool with division by zero.
3417    #[tokio::test]
3418    async fn test_calculator_tool_division_by_zero() {
3419        let tool = CalculatorTool;
3420        let args = json!({"expression": "8 / 0"});
3421        let result = tool.execute(args).await;
3422        assert!(result.is_err());
3423    }
3424
3425    /// Tests the calculator tool with an invalid expression.
3426    #[tokio::test]
3427    async fn test_calculator_tool_invalid_expression() {
3428        let tool = CalculatorTool;
3429        let args = json!({"expression": "invalid"});
3430        let result = tool.execute(args).await;
3431        assert!(result.is_err());
3432    }
3433
3434    /// Tests the echo tool.
3435    #[tokio::test]
3436    async fn test_echo_tool() {
3437        let tool = EchoTool;
3438        assert_eq!(tool.name(), "echo");
3439        assert_eq!(tool.description(), "Echo back the provided message.");
3440
3441        let args = json!({"message": "Hello, world!"});
3442        let result = tool.execute(args).await.unwrap();
3443        assert!(result.success);
3444        assert_eq!(result.output, "Echo: Hello, world!");
3445    }
3446
3447    /// Tests the echo tool with a missing parameter.
3448    #[tokio::test]
3449    async fn test_echo_tool_missing_parameter() {
3450        let tool = EchoTool;
3451        let args = json!({});
3452        let result = tool.execute(args).await;
3453        assert!(result.is_err());
3454    }
3455
3456    /// Tests the creation of a new `ToolRegistry`.
3457    #[test]
3458    fn test_tool_registry_new() {
3459        let registry = ToolRegistry::new();
3460        assert!(registry.tools.is_empty());
3461    }
3462
3463    /// Tests registering and getting a tool from the `ToolRegistry`.
3464    #[tokio::test]
3465    async fn test_tool_registry_register_and_get() {
3466        let mut registry = ToolRegistry::new();
3467        registry.register(Box::new(CalculatorTool));
3468
3469        let tool = registry.get("calculator");
3470        assert!(tool.is_some());
3471        assert_eq!(tool.unwrap().name(), "calculator");
3472    }
3473
3474    /// Tests executing a tool from the `ToolRegistry`.
3475    #[tokio::test]
3476    async fn test_tool_registry_execute() {
3477        let mut registry = ToolRegistry::new();
3478        registry.register(Box::new(CalculatorTool));
3479
3480        let args = json!({"expression": "5 * 6"});
3481        let result = registry.execute("calculator", args).await.unwrap();
3482        assert!(result.success);
3483        assert_eq!(result.output, "30");
3484    }
3485
3486    /// Tests executing a nonexistent tool from the `ToolRegistry`.
3487    #[tokio::test]
3488    async fn test_tool_registry_execute_nonexistent_tool() {
3489        let registry = ToolRegistry::new();
3490        let args = json!({"expression": "5 * 6"});
3491        let result = registry.execute("nonexistent", args).await;
3492        assert!(result.is_err());
3493    }
3494
3495    /// Tests getting the definitions of all tools in the `ToolRegistry`.
3496    #[test]
3497    fn test_tool_registry_get_definitions() {
3498        let mut registry = ToolRegistry::new();
3499        registry.register(Box::new(CalculatorTool));
3500        registry.register(Box::new(EchoTool));
3501
3502        let definitions = registry.get_definitions();
3503        assert_eq!(definitions.len(), 2);
3504
3505        // Check that we have both tools
3506        let names: Vec<String> = definitions
3507            .iter()
3508            .map(|d| d.function.name.clone())
3509            .collect();
3510        assert!(names.contains(&"calculator".to_string()));
3511        assert!(names.contains(&"echo".to_string()));
3512    }
3513
3514    /// Tests listing the names of all tools in the `ToolRegistry`.
3515    #[test]
3516    fn test_tool_registry_list_tools() {
3517        let mut registry = ToolRegistry::new();
3518        registry.register(Box::new(CalculatorTool));
3519        registry.register(Box::new(EchoTool));
3520
3521        let tools = registry.list_tools();
3522        assert_eq!(tools.len(), 2);
3523        assert!(tools.contains(&"calculator".to_string()));
3524        assert!(tools.contains(&"echo".to_string()));
3525    }
3526
3527    /// Tests setting and getting a value in the `MemoryDBTool`.
3528    #[tokio::test]
3529    async fn test_memory_db_set_and_get() {
3530        let tool = MemoryDBTool::new();
3531
3532        // Set a value
3533        let set_args = json!({
3534            "operation": "set",
3535            "key": "name",
3536            "value": "Alice"
3537        });
3538        let result = tool.execute(set_args).await.unwrap();
3539        assert!(result.success);
3540        assert!(result.output.contains("Set 'name' = 'Alice'"));
3541
3542        // Get the value
3543        let get_args = json!({
3544            "operation": "get",
3545            "key": "name"
3546        });
3547        let result = tool.execute(get_args).await.unwrap();
3548        assert!(result.success);
3549        assert!(result.output.contains("Alice"));
3550    }
3551
3552    /// Tests deleting a value from the `MemoryDBTool`.
3553    #[tokio::test]
3554    async fn test_memory_db_delete() {
3555        let tool = MemoryDBTool::new();
3556
3557        // Set a value
3558        let set_args = json!({
3559            "operation": "set",
3560            "key": "temp",
3561            "value": "data"
3562        });
3563        tool.execute(set_args).await.unwrap();
3564
3565        // Delete the value
3566        let delete_args = json!({
3567            "operation": "delete",
3568            "key": "temp"
3569        });
3570        let result = tool.execute(delete_args).await.unwrap();
3571        assert!(result.success);
3572        assert!(result.output.contains("Deleted 'temp'"));
3573
3574        // Try to get deleted value
3575        let get_args = json!({
3576            "operation": "get",
3577            "key": "temp"
3578        });
3579        let result = tool.execute(get_args).await.unwrap();
3580        assert!(!result.success);
3581        assert!(result.output.contains("not found"));
3582    }
3583
3584    /// Tests checking if a key exists in the `MemoryDBTool`.
3585    #[tokio::test]
3586    async fn test_memory_db_exists() {
3587        let tool = MemoryDBTool::new();
3588
3589        // Check non-existent key
3590        let exists_args = json!({
3591            "operation": "exists",
3592            "key": "test"
3593        });
3594        let result = tool.execute(exists_args).await.unwrap();
3595        assert!(result.success);
3596        assert!(result.output.contains("false"));
3597
3598        // Set a value
3599        let set_args = json!({
3600            "operation": "set",
3601            "key": "test",
3602            "value": "value"
3603        });
3604        tool.execute(set_args).await.unwrap();
3605
3606        // Check existing key
3607        let exists_args = json!({
3608            "operation": "exists",
3609            "key": "test"
3610        });
3611        let result = tool.execute(exists_args).await.unwrap();
3612        assert!(result.success);
3613        assert!(result.output.contains("true"));
3614    }
3615
3616    /// Tests listing the contents of the `MemoryDBTool`.
3617    #[tokio::test]
3618    async fn test_memory_db_list() {
3619        let tool = MemoryDBTool::new();
3620
3621        // List empty database
3622        let list_args = json!({
3623            "operation": "list"
3624        });
3625        let result = tool.execute(list_args).await.unwrap();
3626        assert!(result.success);
3627        assert!(result.output.contains("empty"));
3628
3629        // Add some items
3630        tool.execute(json!({
3631            "operation": "set",
3632            "key": "key1",
3633            "value": "value1"
3634        }))
3635        .await
3636        .unwrap();
3637
3638        tool.execute(json!({
3639            "operation": "set",
3640            "key": "key2",
3641            "value": "value2"
3642        }))
3643        .await
3644        .unwrap();
3645
3646        // List items
3647        let list_args = json!({
3648            "operation": "list"
3649        });
3650        let result = tool.execute(list_args).await.unwrap();
3651        assert!(result.success);
3652        assert!(result.output.contains("2 items"));
3653        assert!(result.output.contains("key1"));
3654        assert!(result.output.contains("key2"));
3655    }
3656
3657    /// Tests clearing the `MemoryDBTool`.
3658    #[tokio::test]
3659    async fn test_memory_db_clear() {
3660        let tool = MemoryDBTool::new();
3661
3662        // Add some items
3663        tool.execute(json!({
3664            "operation": "set",
3665            "key": "key1",
3666            "value": "value1"
3667        }))
3668        .await
3669        .unwrap();
3670
3671        tool.execute(json!({
3672            "operation": "set",
3673            "key": "key2",
3674            "value": "value2"
3675        }))
3676        .await
3677        .unwrap();
3678
3679        // Clear database
3680        let clear_args = json!({
3681            "operation": "clear"
3682        });
3683        let result = tool.execute(clear_args).await.unwrap();
3684        assert!(result.success);
3685        assert!(result.output.contains("2 items removed"));
3686
3687        // Verify database is empty
3688        let list_args = json!({
3689            "operation": "list"
3690        });
3691        let result = tool.execute(list_args).await.unwrap();
3692        assert!(result.output.contains("empty"));
3693    }
3694
3695    /// Tests an invalid operation in the `MemoryDBTool`.
3696    #[tokio::test]
3697    async fn test_memory_db_invalid_operation() {
3698        let tool = MemoryDBTool::new();
3699
3700        let args = json!({
3701            "operation": "invalid_op"
3702        });
3703        let result = tool.execute(args).await;
3704        assert!(result.is_err());
3705    }
3706
3707    /// Tests sharing the database between `MemoryDBTool` instances.
3708    #[tokio::test]
3709    async fn test_memory_db_shared_instance() {
3710        use std::sync::{Arc, Mutex};
3711
3712        // Create a shared database
3713        let shared_db = Arc::new(Mutex::new(HashMap::new()));
3714        let tool1 = MemoryDBTool::with_shared_db(shared_db.clone());
3715        let tool2 = MemoryDBTool::with_shared_db(shared_db.clone());
3716
3717        // Set value with tool1
3718        tool1
3719            .execute(json!({
3720                "operation": "set",
3721                "key": "shared",
3722                "value": "data"
3723            }))
3724            .await
3725            .unwrap();
3726
3727        // Get value with tool2
3728        let result = tool2
3729            .execute(json!({
3730                "operation": "get",
3731                "key": "shared"
3732            }))
3733            .await
3734            .unwrap();
3735        assert!(result.success);
3736        assert!(result.output.contains("data"));
3737    }
3738
3739    /// Tests the WebScraperTool.
3740    #[tokio::test]
3741    async fn test_web_scraper_tool() {
3742        let tool = WebScraperTool;
3743        assert_eq!(tool.name(), "web_scraper");
3744
3745        // Test with missing URL parameter
3746        let args = json!({});
3747        let result = tool.execute(args).await;
3748        assert!(result.is_err());
3749
3750        // Note: We can't easily test the actual HTTP functionality in unit tests
3751        // without mocking, but we can test parameter validation
3752    }
3753
3754    /// Tests the JsonParserTool parse operation.
3755    #[tokio::test]
3756    async fn test_json_parser_tool_parse() {
3757        let tool = JsonParserTool;
3758        assert_eq!(tool.name(), "json_parser");
3759
3760        let args = json!({
3761            "operation": "parse",
3762            "json": "{\"key\": \"value\", \"number\": 42}"
3763        });
3764        let result = tool.execute(args).await.unwrap();
3765        assert!(result.success);
3766        assert!(result.output.contains("✓ JSON parsed successfully"));
3767        assert!(result.output.contains("Type: object"));
3768    }
3769
3770    /// Tests the JsonParserTool stringify operation.
3771    #[tokio::test]
3772    async fn test_json_parser_tool_stringify() {
3773        let tool = JsonParserTool;
3774
3775        let args = json!({
3776            "operation": "stringify",
3777            "json": "  {\"key\": \"value\"}  "
3778        });
3779        let result = tool.execute(args).await.unwrap();
3780        assert!(result.success);
3781        assert!(result.output.contains("key"));
3782        assert!(result.output.contains("value"));
3783    }
3784
3785    /// Tests the JsonParserTool get_value operation.
3786    #[tokio::test]
3787    async fn test_json_parser_tool_get_value() {
3788        let tool = JsonParserTool;
3789
3790        let args = json!({
3791            "operation": "get_value",
3792            "json": "{\"user\": {\"name\": \"Alice\", \"age\": 30}}",
3793            "path": "user.name"
3794        });
3795        let result = tool.execute(args).await.unwrap();
3796        assert!(result.success);
3797        assert!(result.output.contains("Alice"));
3798    }
3799
3800    /// Tests the JsonParserTool validate operation.
3801    #[tokio::test]
3802    async fn test_json_parser_tool_validate() {
3803        let tool = JsonParserTool;
3804
3805        // Valid JSON
3806        let args = json!({
3807            "operation": "validate",
3808            "json": "{\"valid\": true}"
3809        });
3810        let result = tool.execute(args).await.unwrap();
3811        assert!(result.success);
3812        assert!(result.output.contains("✓ JSON is valid"));
3813
3814        // Invalid JSON
3815        let args = json!({
3816            "operation": "validate",
3817            "json": "{\"invalid\": }"
3818        });
3819        let result = tool.execute(args).await;
3820        assert!(result.is_ok()); // Tool returns success with error message for validation
3821        assert!(result.unwrap().output.contains("✗ JSON validation failed"));
3822    }
3823
3824    /// Tests the TimestampTool now operation.
3825    #[tokio::test]
3826    async fn test_timestamp_tool_now() {
3827        let tool = TimestampTool;
3828        assert_eq!(tool.name(), "timestamp");
3829
3830        let args = json!({
3831            "operation": "now"
3832        });
3833        let result = tool.execute(args).await.unwrap();
3834        assert!(result.success);
3835        assert!(result.output.contains("Current time"));
3836        assert!(result.output.contains("Unix timestamp"));
3837        assert!(result.output.contains("RFC3339"));
3838    }
3839
3840    /// Tests the TimestampTool format operation.
3841    #[tokio::test]
3842    async fn test_timestamp_tool_format() {
3843        let tool = TimestampTool;
3844
3845        let args = json!({
3846            "operation": "format",
3847            "timestamp": "1640995200", // 2022-01-01 00:00:00 UTC
3848            "format": "%Y-%m-%d"
3849        });
3850        let result = tool.execute(args).await.unwrap();
3851        assert!(result.success);
3852        assert!(result.output.contains("2022-01-01"));
3853    }
3854
3855    /// Tests the TimestampTool add operation.
3856    #[tokio::test]
3857    async fn test_timestamp_tool_add() {
3858        let tool = TimestampTool;
3859
3860        let args = json!({
3861            "operation": "add",
3862            "timestamp": "2022-01-01T00:00:00Z",
3863            "unit": "days",
3864            "amount": 5
3865        });
3866        let result = tool.execute(args).await.unwrap();
3867        assert!(result.success);
3868        assert!(result.output.contains("Added 5 days"));
3869    }
3870
3871    /// Tests the TimestampTool diff operation.
3872    #[tokio::test]
3873    async fn test_timestamp_tool_diff() {
3874        let tool = TimestampTool;
3875
3876        let args = json!({
3877            "operation": "diff",
3878            "timestamp1": "2022-01-01T00:00:00Z",
3879            "timestamp2": "2022-01-02T00:00:00Z"
3880        });
3881        let result = tool.execute(args).await.unwrap();
3882        assert!(result.success);
3883        assert!(result.output.contains("86400 seconds")); // 1 day in seconds
3884    }
3885
3886    /// Tests the FileIOTool read operation.
3887    #[tokio::test]
3888    async fn test_file_io_tool_read() {
3889        let tool = FileIOTool;
3890        assert_eq!(tool.name(), "file_io");
3891
3892        // Create a temporary file for testing
3893        let temp_file = tempfile::NamedTempFile::new().unwrap();
3894        let file_path = temp_file.path().to_string_lossy().to_string();
3895        std::fs::write(&file_path, "Hello, World!").unwrap();
3896
3897        let args = json!({
3898            "operation": "read",
3899            "path": file_path
3900        });
3901        let result = tool.execute(args).await.unwrap();
3902        assert!(result.success);
3903        assert!(result.output.contains("Hello, World!"));
3904    }
3905
3906    /// Tests the FileIOTool write operation.
3907    #[tokio::test]
3908    async fn test_file_io_tool_write() {
3909        let tool = FileIOTool;
3910
3911        let temp_file = tempfile::NamedTempFile::new().unwrap();
3912        let file_path = temp_file.path().to_string_lossy().to_string();
3913
3914        let args = json!({
3915            "operation": "write",
3916            "path": file_path,
3917            "content": "Test content"
3918        });
3919        let result = tool.execute(args).await.unwrap();
3920        assert!(result.success);
3921        assert!(result.output.contains("Wrote 12 bytes"));
3922
3923        // Verify the content was written
3924        let content = std::fs::read_to_string(&file_path).unwrap();
3925        assert_eq!(content, "Test content");
3926    }
3927
3928    /// Tests the FileIOTool exists operation.
3929    #[tokio::test]
3930    async fn test_file_io_tool_exists() {
3931        let tool = FileIOTool;
3932
3933        let temp_file = tempfile::NamedTempFile::new().unwrap();
3934        let file_path = temp_file.path().to_string_lossy().to_string();
3935
3936        let args = json!({
3937            "operation": "exists",
3938            "path": file_path
3939        });
3940        let result = tool.execute(args).await.unwrap();
3941        assert!(result.success);
3942        assert!(result.output.contains("exists: true"));
3943        assert!(result.output.contains("(file)"));
3944    }
3945
3946    /// Tests the FileIOTool safe delete operation (empty directories only by default).
3947    #[tokio::test]
3948    async fn test_file_io_tool_safe_delete() {
3949        let tool = FileIOTool;
3950
3951        // Create a temporary directory
3952        let temp_dir = tempfile::tempdir().unwrap();
3953        let dir_path = temp_dir.path().to_string_lossy().to_string();
3954
3955        // Test deleting an empty directory (should work)
3956        let args = json!({
3957            "operation": "delete",
3958            "path": dir_path
3959        });
3960        let result = tool.execute(args).await.unwrap();
3961        assert!(result.success);
3962        assert!(result.output.contains("✓ Deleted directory"));
3963
3964        // Create a new directory with a file inside
3965        let temp_dir2 = tempfile::tempdir().unwrap();
3966        let dir_path2 = temp_dir2.path().to_string_lossy().to_string();
3967        let file_path = temp_dir2.path().join("test.txt");
3968        std::fs::write(&file_path, "test content").unwrap();
3969
3970        // Test deleting a non-empty directory without recursive flag (should fail)
3971        let args2 = json!({
3972            "operation": "delete",
3973            "path": dir_path2
3974        });
3975        let result2 = tool.execute(args2).await;
3976        assert!(result2.is_err()); // Should fail because directory is not empty
3977        assert!(result2.unwrap_err().to_string().contains("must be empty"));
3978
3979        // Test deleting a non-empty directory with recursive flag (should work)
3980        let args3 = json!({
3981            "operation": "delete",
3982            "path": temp_dir2.path().to_string_lossy(),
3983            "recursive": true
3984        });
3985        let result3 = tool.execute(args3).await.unwrap();
3986        assert!(result3.success);
3987        assert!(result3.output.contains("✓ Deleted directory recursively:"));
3988    }
3989
3990    /// Tests the ShellCommandTool with a safe command.
3991    #[tokio::test]
3992    async fn test_shell_command_tool_safe() {
3993        let tool = ShellCommandTool;
3994        assert_eq!(tool.name(), "shell_command");
3995
3996        // Test with a safe command
3997        let args = json!({
3998            "command": "echo 'hello world'"
3999        });
4000        let result = tool.execute(args).await.unwrap();
4001        assert!(result.success);
4002        assert!(result.output.contains("hello world"));
4003    }
4004
4005    /// Tests the ShellCommandTool with a blocked dangerous command.
4006    #[tokio::test]
4007    async fn test_shell_command_tool_blocked() {
4008        let tool = ShellCommandTool;
4009
4010        let args = json!({
4011            "command": "rm -rf /"
4012        });
4013        let result = tool.execute(args).await;
4014        assert!(result.is_err());
4015        assert!(result.unwrap_err().to_string().contains("Command blocked"));
4016    }
4017
4018    /// Tests the HttpRequestTool with missing method.
4019    #[tokio::test]
4020    async fn test_http_request_tool_missing_method() {
4021        let tool = HttpRequestTool;
4022        assert_eq!(tool.name(), "http_request");
4023
4024        let args = json!({
4025            "url": "https://httpbin.org/get"
4026        });
4027        let result = tool.execute(args).await;
4028        assert!(result.is_err());
4029    }
4030
4031    /// Tests the FileListTool.
4032    #[tokio::test]
4033    async fn test_file_list_tool() {
4034        let tool = FileListTool;
4035        assert_eq!(tool.name(), "file_list");
4036
4037        let args = json!({
4038            "path": ".",
4039            "show_hidden": false
4040        });
4041        let result = tool.execute(args).await.unwrap();
4042        assert!(result.success);
4043        assert!(result.output.contains("Directory listing"));
4044        assert!(result.output.contains("Total items"));
4045    }
4046
4047    /// Tests the SystemInfoTool.
4048    #[tokio::test]
4049    async fn test_system_info_tool() {
4050        let tool = SystemInfoTool;
4051        assert_eq!(tool.name(), "system_info");
4052
4053        let args = json!({
4054            "category": "os"
4055        });
4056        let result = tool.execute(args).await.unwrap();
4057        assert!(result.success);
4058        assert!(result.output.contains("Operating System"));
4059        assert!(result.output.contains("OS:"));
4060    }
4061
4062    /// Tests the TextProcessorTool search operation.
4063    #[tokio::test]
4064    async fn test_text_processor_tool_search() {
4065        let tool = TextProcessorTool;
4066        assert_eq!(tool.name(), "text_processor");
4067
4068        let args = json!({
4069            "operation": "search",
4070            "text": "Hello world, hello universe",
4071            "pattern": "hello",
4072            "case_sensitive": false
4073        });
4074        let result = tool.execute(args).await.unwrap();
4075        assert!(result.success);
4076        assert!(result.output.contains("Found 2 match(es)"));
4077    }
4078
4079    /// Tests the TextProcessorTool replace operation.
4080    #[tokio::test]
4081    async fn test_text_processor_tool_replace() {
4082        let tool = TextProcessorTool;
4083
4084        let args = json!({
4085            "operation": "replace",
4086            "text": "Hello world",
4087            "pattern": "world",
4088            "replacement": "universe"
4089        });
4090        let result = tool.execute(args).await.unwrap();
4091        assert!(result.success);
4092        assert!(result.output.contains("Replaced 1 occurrence"));
4093        assert!(result.output.contains("Hello universe"));
4094    }
4095
4096    /// Tests the TextProcessorTool count operation.
4097    #[tokio::test]
4098    async fn test_text_processor_tool_count() {
4099        let tool = TextProcessorTool;
4100
4101        let args = json!({
4102            "operation": "count",
4103            "text": "Hello\nworld"
4104        });
4105        let result = tool.execute(args).await.unwrap();
4106        assert!(result.success);
4107        assert!(result.output.contains("Characters: 11"));
4108        assert!(result.output.contains("Lines: 2"));
4109        assert!(result.output.contains("Words: 2"));
4110    }
4111
4112    /// Tests the TextProcessorTool uppercase operation.
4113    #[tokio::test]
4114    async fn test_text_processor_tool_uppercase() {
4115        let tool = TextProcessorTool;
4116
4117        let args = json!({
4118            "operation": "uppercase",
4119            "text": "hello world"
4120        });
4121        let result = tool.execute(args).await.unwrap();
4122        assert!(result.success);
4123        assert_eq!(result.output, "HELLO WORLD");
4124    }
4125
4126    /// Tests the TextProcessorTool trim operation.
4127    #[tokio::test]
4128    async fn test_text_processor_tool_trim() {
4129        let tool = TextProcessorTool;
4130
4131        let args = json!({
4132            "operation": "trim",
4133            "text": "  hello world  "
4134        });
4135        let result = tool.execute(args).await.unwrap();
4136        assert!(result.success);
4137        assert_eq!(result.output, "hello world");
4138    }
4139}