use anyhow::Result; use serde_json::{json, Value}; use walkdir::WalkDir; use crate::compile::MqlCompiler; use crate::models::Config; pub async fn handle_list_experts(config: &Config, args: &Value) -> Result { let filter = args.get("filter").and_then(|v| v.as_str()); let mut experts = Vec::new(); if let Some(experts_dir) = &config.experts_dir { for entry in WalkDir::new(experts_dir).max_depth(3) { if let Ok(entry) = entry { let path = entry.path(); if !path.is_file() { continue; } if let Some(name) = path.file_stem() { let name_str = name.to_string_lossy().to_string(); let is_compiled = path.extension() .map(|e| e == "ex5") .unwrap_or(false); if let Some(filter_str) = filter { if !name_str.to_lowercase().contains(&filter_str.to_lowercase()) { continue; } } experts.push(json!({ "name": name_str, "compiled": is_compiled, "path": path.to_string_lossy().to_string(), })); } } } } experts.sort_by(|a, b| a["name"].as_str().cmp(&b["name"].as_str())); Ok(json!({ "content": [{ "type": "text", "text": json!({ "success": true, "count": experts.len(), "experts": experts, }).to_string() }], "isError": false })) } pub async fn handle_search_experts(config: &Config, args: &Value) -> Result { let pattern = args.get("pattern") .and_then(|v| v.as_str()) .ok_or_else(|| anyhow::anyhow!("pattern is required"))?; let pattern_lower = pattern.to_lowercase(); let mut matches = Vec::new(); if let Some(experts_dir) = &config.experts_dir { for entry in WalkDir::new(experts_dir).max_depth(3) { if let Ok(entry) = entry { let path = entry.path(); if !path.is_file() { continue; } if let Some(name) = path.file_stem() { let name_str = name.to_string_lossy().to_string(); if name_str.to_lowercase().contains(&pattern_lower) { let is_compiled = path.extension() .map(|e| e == "ex5") .unwrap_or(false); matches.push(json!({ "name": name_str, "path": path.to_string_lossy().to_string(), "compiled": is_compiled, })); } } } } } matches.sort_by(|a, b| a["name"].as_str().cmp(&b["name"].as_str())); Ok(json!({ "content": [{ "type": "text", "text": json!({ "success": true, "pattern": pattern, "count": matches.len(), "matches": matches, }).to_string() }], "isError": false })) } pub async fn handle_list_indicators(config: &Config, args: &Value) -> Result { let filter = args.get("filter").and_then(|v| v.as_str()); let include_builtin = args.get("include_builtin").and_then(|v| v.as_bool()).unwrap_or(false); let mut indicators = Vec::new(); // List custom indicators if let Some(indicators_dir) = &config.indicators_dir { for entry in WalkDir::new(indicators_dir).max_depth(3) { if let Ok(entry) = entry { let path = entry.path(); if !path.is_file() { continue; } if let Some(name) = path.file_stem() { let name_str = name.to_string_lossy().to_string(); let is_compiled = path.extension() .map(|e| e == "ex5") .unwrap_or(false); if let Some(filter_str) = filter { if !name_str.to_lowercase().contains(&filter_str.to_lowercase()) { continue; } } indicators.push(json!({ "name": name_str, "compiled": is_compiled, "type": "custom", "path": path.to_string_lossy().to_string(), })); } } } } // Add built-in indicators if requested if include_builtin { let builtin = vec![ "Accelerator", "Accumulation", "ADX", "Alligator", "AO", "ATR", "Bands", "Bears", "Bulls", "CCI", "DeMarker", "Envelopes", "Force", "Fractals", "Gator", "Ichimoku", "MA", "MACD", "MFI", "Momentum", "OBV", "OsMA", "RSI", "RVI", "SAR", "StdDev", "Stochastic", "WPR", ]; for name in builtin { if filter.map(|f| name.to_lowercase().contains(&f.to_lowercase())).unwrap_or(true) { indicators.push(json!({ "name": name, "compiled": true, "type": "builtin", "path": null, })); } } } indicators.sort_by(|a, b| a["name"].as_str().cmp(&b["name"].as_str())); Ok(json!({ "content": [{ "type": "text", "text": json!({ "success": true, "count": indicators.len(), "indicators": indicators, "custom_dir": config.indicators_dir.clone(), }).to_string() }], "isError": false })) } pub async fn handle_list_scripts(config: &Config, args: &Value) -> Result { let filter = args.get("filter").and_then(|v| v.as_str()); let mut scripts = Vec::new(); if let Some(scripts_dir) = &config.scripts_dir { for entry in WalkDir::new(scripts_dir).max_depth(3) { if let Ok(entry) = entry { let path = entry.path(); if !path.is_file() { continue; } if let Some(name) = path.file_stem() { let name_str = name.to_string_lossy().to_string(); let is_compiled = path.extension() .map(|e| e == "ex5") .unwrap_or(false); if let Some(filter_str) = filter { if !name_str.to_lowercase().contains(&filter_str.to_lowercase()) { continue; } } scripts.push(json!({ "name": name_str, "compiled": is_compiled, "path": path.to_string_lossy().to_string(), })); } } } } scripts.sort_by(|a, b| a["name"].as_str().cmp(&b["name"].as_str())); Ok(json!({ "content": [{ "type": "text", "text": json!({ "success": true, "count": scripts.len(), "scripts": scripts, "scripts_dir": config.scripts_dir.clone(), }).to_string() }], "isError": false })) } pub async fn handle_compile_ea(config: &Config, args: &Value) -> Result { use std::path::PathBuf; let resolved_path: String = if let Some(p) = args.get("expert_path").and_then(|v| v.as_str()) { p.to_string() } else if let Some(name) = args.get("expert").and_then(|v| v.as_str()) { let mut candidates = vec![ PathBuf::from(name).with_extension("mq5"), ]; if let Some(experts_dir) = &config.experts_dir { candidates.push(PathBuf::from(experts_dir).join(name).join(format!("{}.mq5", name))); candidates.push(PathBuf::from(experts_dir).join(format!("{}.mq5", name))); } match candidates.into_iter().find(|p| p.exists()) { Some(p) => p.to_string_lossy().to_string(), None => return Ok(serde_json::json!({ "content": [{ "type": "text", "text": serde_json::json!({ "success": false, "error": format!("Cannot find {}.mq5 in MT5 Experts dir or current directory", name), }).to_string() }], "isError": true })), } } else { return Err(anyhow::anyhow!("Either 'expert' or 'expert_path' is required")); }; let compiler = MqlCompiler::new(config.clone()); let expert_path = resolved_path.as_str(); match compiler.compile(&expert_path) { Ok(result) => { Ok(json!({ "content": [{ "type": "text", "text": json!({ "success": result.success, "binary_path": result.ex5_path.map(|p| p.to_string_lossy().to_string()), "binary_size_bytes": result.binary_size, "files_synced": result.files_synced, "warnings": result.warnings.len(), "errors": result.errors.len(), "error_list": result.errors, "warning_list": result.warnings, }).to_string() }], "isError": !result.success })) } Err(e) => { Ok(json!({ "content": [{ "type": "text", "text": json!({ "success": false, "error": format!("Compilation failed: {}", e), }).to_string() }], "isError": true })) } } } pub async fn handle_search_indicators(config: &Config, args: &Value) -> Result { let pattern = args.get("pattern") .and_then(|v| v.as_str()) .ok_or_else(|| anyhow::anyhow!("pattern is required"))?; let include_builtin = args.get("include_builtin").and_then(|v| v.as_bool()).unwrap_or(false); let pattern_lower = pattern.to_lowercase(); let mut matches = Vec::new(); // Search custom indicators recursively if let Some(indicators_dir) = &config.indicators_dir { for entry in WalkDir::new(indicators_dir).max_depth(3) { if let Ok(entry) = entry { let path = entry.path(); if !path.is_file() { continue; } if let Some(name) = path.file_stem() { let name_str = name.to_string_lossy().to_string(); if name_str.to_lowercase().contains(&pattern_lower) { let is_compiled = path.extension() .map(|e| e == "ex5") .unwrap_or(false); matches.push(json!({ "name": name_str, "path": path.to_string_lossy().to_string(), "type": "custom", "compiled": is_compiled, })); } } } } } // Search built-in indicators if requested if include_builtin { let builtin = vec![ "Accelerator", "Accumulation", "ADX", "Alligator", "AO", "ATR", "Bands", "Bears", "Bulls", "CCI", "DeMarker", "Envelopes", "Force", "Fractals", "Gator", "Ichimoku", "MA", "MACD", "MFI", "Momentum", "OBV", "OsMA", "RSI", "RVI", "SAR", "StdDev", "Stochastic", "WPR", ]; for name in builtin { if name.to_lowercase().contains(&pattern_lower) { matches.push(json!({ "name": name, "path": null, "type": "builtin", "compiled": true, })); } } } matches.sort_by(|a, b| a["name"].as_str().cmp(&b["name"].as_str())); Ok(json!({ "content": [{ "type": "text", "text": json!({ "success": true, "pattern": pattern, "count": matches.len(), "matches": matches, }).to_string() }], "isError": false })) } pub async fn handle_search_scripts(config: &Config, args: &Value) -> Result { let pattern = args.get("pattern") .and_then(|v| v.as_str()) .ok_or_else(|| anyhow::anyhow!("pattern is required"))?; let pattern_lower = pattern.to_lowercase(); let mut matches = Vec::new(); if let Some(scripts_dir) = &config.scripts_dir { for entry in WalkDir::new(scripts_dir).max_depth(3) { if let Ok(entry) = entry { let path = entry.path(); if !path.is_file() { continue; } if let Some(name) = path.file_stem() { let name_str = name.to_string_lossy().to_string(); if name_str.to_lowercase().contains(&pattern_lower) { let is_compiled = path.extension() .map(|e| e == "ex5") .unwrap_or(false); matches.push(json!({ "name": name_str, "path": path.to_string_lossy().to_string(), "compiled": is_compiled, })); } } } } } matches.sort_by(|a, b| a["name"].as_str().cmp(&b["name"].as_str())); Ok(json!({ "content": [{ "type": "text", "text": json!({ "success": true, "pattern": pattern, "count": matches.len(), "matches": matches, }).to_string() }], "isError": false })) } pub async fn handle_copy_indicator_to_project(config: &Config, args: &Value) -> Result { use std::path::PathBuf; let source_path = args.get("source_path") .and_then(|v| v.as_str()) .ok_or_else(|| anyhow::anyhow!("source_path is required"))?; let target_name = args.get("target_name").and_then(|v| v.as_str()); // Determine project directory let project_dir = config.project_dir.as_ref() .map(PathBuf::from) .or_else(|| std::env::current_dir().ok()) .ok_or_else(|| anyhow::anyhow!("Cannot determine project directory"))?; let source = PathBuf::from(source_path); if !source.exists() { return Ok(json!({ "content": [{ "type": "text", "text": json!({ "success": false, "error": format!("Source file not found: {}", source_path), }).to_string() }], "isError": true })); } // Get extension let ext = source.extension() .and_then(|e| e.to_str()) .unwrap_or("mq5"); // Determine target name let target_filename = match target_name { Some(name) => format!("{}.{}", name, ext), None => source.file_name() .and_then(|n| n.to_str()) .map(|n| n.to_string()) .unwrap_or_else(|| format!("indicator.{}", ext)), }; let destination = project_dir.join(&target_filename); // Copy file match std::fs::copy(&source, &destination) { Ok(bytes) => { Ok(json!({ "content": [{ "type": "text", "text": json!({ "success": true, "source": source_path, "destination": destination.to_string_lossy().to_string(), "bytes_copied": bytes, }).to_string() }], "isError": false })) } Err(e) => { Ok(json!({ "content": [{ "type": "text", "text": json!({ "success": false, "error": format!("Failed to copy file: {}", e), }).to_string() }], "isError": true })) } } } pub async fn handle_copy_script_to_project(config: &Config, args: &Value) -> Result { use std::path::PathBuf; let source_path = args.get("source_path") .and_then(|v| v.as_str()) .ok_or_else(|| anyhow::anyhow!("source_path is required"))?; let target_name = args.get("target_name").and_then(|v| v.as_str()); // Determine project directory let project_dir = config.project_dir.as_ref() .map(PathBuf::from) .or_else(|| std::env::current_dir().ok()) .ok_or_else(|| anyhow::anyhow!("Cannot determine project directory"))?; let source = PathBuf::from(source_path); if !source.exists() { return Ok(json!({ "content": [{ "type": "text", "text": json!({ "success": false, "error": format!("Source file not found: {}", source_path), }).to_string() }], "isError": true })); } // Get extension let ext = source.extension() .and_then(|e| e.to_str()) .unwrap_or("mq5"); // Determine target name let target_filename = match target_name { Some(name) => format!("{}.{}", name, ext), None => source.file_name() .and_then(|n| n.to_str()) .map(|n| n.to_string()) .unwrap_or_else(|| format!("script.{}", ext)), }; let destination = project_dir.join(&target_filename); // Copy file match std::fs::copy(&source, &destination) { Ok(bytes) => { Ok(json!({ "content": [{ "type": "text", "text": json!({ "success": true, "source": source_path, "destination": destination.to_string_lossy().to_string(), "bytes_copied": bytes, }).to_string() }], "isError": false })) } Err(e) => { Ok(json!({ "content": [{ "type": "text", "text": json!({ "success": false, "error": format!("Failed to copy file: {}", e), }).to_string() }], "isError": true })) } } }