tools.rs
1 use anyhow::Result; 2 use std::fs; 3 use std::path::Path; 4 use std::process::Command; 5 use crate::memory::MemoryStore; 6 7 // Memory tools 8 pub fn memorize_info(key: &str, content: &str, tags: &str) -> Result<String> { 9 let mut memory = MemoryStore::new()?; 10 let tag_list: Vec<String> = if tags.is_empty() { 11 Vec::new() 12 } else { 13 tags.split(',').map(|s| s.trim().to_string()).collect() 14 }; 15 memory.memorize(key.to_string(), content.to_string(), tag_list) 16 } 17 18 pub fn remember_info(query: &str) -> Result<String> { 19 let mut memory = MemoryStore::new()?; 20 memory.remember(query.to_string()) 21 } 22 23 pub fn get_all_memories() -> Result<String> { 24 let memory = MemoryStore::new()?; 25 Ok(memory.get_all_memories()) 26 } 27 28 pub fn delete_memory(key: &str) -> Result<String> { 29 let mut memory = MemoryStore::new()?; 30 memory.delete_memory(key.to_string()) 31 } 32 33 pub fn clear_memories() -> Result<String> { 34 let mut memory = MemoryStore::new()?; 35 memory.clear_all() 36 } 37 38 // Smart file operations like Python version 39 pub fn write_file_smart(input: &str) -> Result<String> { 40 // Handle JSON format: {"file_path": "path", "content": "text"} 41 if input.trim().starts_with('{') { 42 if let Ok(json) = serde_json::from_str::<serde_json::Value>(input) { 43 if let (Some(path), Some(content)) = (json.get("file_path"), json.get("content")) { 44 if let (Some(path_str), Some(content_str)) = (path.as_str(), content.as_str()) { 45 return write_file(path_str, content_str); 46 } 47 } 48 } 49 } 50 51 // Handle pipe format: "filepath|||content" 52 if let Some(pos) = input.find("|||") { 53 let path = input[..pos].trim(); 54 let content = input[pos + 3..].trim(); 55 return write_file(path, content); 56 } 57 58 Err(anyhow::anyhow!("Invalid format. Use JSON or 'filepath|||content'")) 59 } 60 61 pub fn append_to_file_smart(input: &str) -> Result<String> { 62 // Handle JSON format: {"file_path": "path", "content": "text"} 63 if input.trim().starts_with('{') { 64 if let Ok(json) = serde_json::from_str::<serde_json::Value>(input) { 65 if let (Some(path), Some(content)) = (json.get("file_path"), json.get("content")) { 66 if let (Some(path_str), Some(content_str)) = (path.as_str(), content.as_str()) { 67 return append_to_file(path_str, content_str); 68 } 69 } 70 } 71 } 72 73 // Handle pipe format: "filepath|||content" 74 if let Some(pos) = input.find("|||") { 75 let path = input[..pos].trim(); 76 let content = input[pos + 3..].trim(); 77 return append_to_file(path, content); 78 } 79 80 Err(anyhow::anyhow!("Invalid format. Use JSON or 'filepath|||content'")) 81 } 82 83 // Advanced git conflict resolution 84 pub fn auto_merge_content(content: &str, file_path: &str) -> Result<String> { 85 // Simple auto-merge strategy - take both changes 86 let lines: Vec<&str> = content.lines().collect(); 87 let mut merged_lines = Vec::new(); 88 let mut in_conflict = false; 89 let mut current_section = Vec::new(); 90 91 for line in lines { 92 if line.starts_with("<<<<<<< ") { 93 in_conflict = true; 94 current_section.clear(); 95 } else if line.starts_with(">>>>>>> ") { 96 in_conflict = false; 97 // Add all collected lines (both sides of conflict) 98 merged_lines.extend(current_section.clone()); 99 current_section.clear(); 100 } else if line.starts_with("======= ") { 101 // Continue collecting - we want both sides 102 continue; 103 } else if in_conflict { 104 current_section.push(line); 105 } else { 106 merged_lines.push(line); 107 } 108 } 109 110 let merged_content = merged_lines.join("\n"); 111 fs::write(file_path, &merged_content)?; 112 113 Ok(format!("๐ Auto-merged conflicts in {}", file_path)) 114 } 115 116 // Task management (simplified version of Python's task system) 117 pub fn add_task(task: &str) -> Result<String> { 118 let home = dirs::home_dir().ok_or_else(|| anyhow::anyhow!("Could not find home directory"))?; 119 let kamaji_dir = home.join(".kamaji"); 120 fs::create_dir_all(&kamaji_dir)?; 121 let tasks_file = kamaji_dir.join("tasks.txt"); 122 123 let mut tasks = if tasks_file.exists() { 124 fs::read_to_string(&tasks_file)? 125 } else { 126 String::new() 127 }; 128 129 tasks.push_str(&format!("- {}\n", task)); 130 fs::write(&tasks_file, tasks)?; 131 132 Ok(format!("๐ Added task: {}", task)) 133 } 134 135 pub fn list_tasks() -> Result<String> { 136 let home = dirs::home_dir().ok_or_else(|| anyhow::anyhow!("Could not find home directory"))?; 137 let tasks_file = home.join(".kamaji").join("tasks.txt"); 138 139 if !tasks_file.exists() { 140 return Ok("๐ No tasks found. Add some with the add_task tool!".to_string()); 141 } 142 143 let tasks = fs::read_to_string(&tasks_file)?; 144 if tasks.trim().is_empty() { 145 return Ok("๐ Task list is empty.".to_string()); 146 } 147 148 Ok(format!("๐ Current Tasks:\n{}", tasks)) 149 } 150 151 pub fn clear_tasks() -> Result<String> { 152 let home = dirs::home_dir().ok_or_else(|| anyhow::anyhow!("Could not find home directory"))?; 153 let tasks_file = home.join(".kamaji").join("tasks.txt"); 154 155 if tasks_file.exists() { 156 fs::remove_file(&tasks_file)?; 157 } 158 159 Ok("๐งน Cleared all tasks".to_string()) 160 } 161 162 // Codebase analysis (simplified version) 163 pub fn analyze_codebase(path: &str) -> Result<String> { 164 let mut analysis = String::new(); 165 analysis.push_str("๐ CODEBASE ANALYSIS\n"); 166 analysis.push_str("==================\n\n"); 167 168 // Count files by extension 169 let mut file_counts = std::collections::HashMap::new(); 170 let mut total_lines = 0; 171 172 if let Ok(entries) = fs::read_dir(path) { 173 for entry in entries.flatten() { 174 if let Ok(metadata) = entry.metadata() { 175 if metadata.is_file() { 176 if let Some(ext) = entry.path().extension() { 177 if let Some(ext_str) = ext.to_str() { 178 *file_counts.entry(ext_str.to_string()).or_insert(0) += 1; 179 180 // Count lines for code files 181 if matches!(ext_str, "rs" | "py" | "js" | "ts" | "go" | "java" | "cpp" | "c" | "h") { 182 if let Ok(content) = fs::read_to_string(entry.path()) { 183 total_lines += content.lines().count(); 184 } 185 } 186 } 187 } 188 } 189 } 190 } 191 } 192 193 analysis.push_str("๐ File Statistics:\n"); 194 for (ext, count) in file_counts { 195 analysis.push_str(&format!(" .{}: {} files\n", ext, count)); 196 } 197 analysis.push_str(&format!("\n๐ Total lines of code: {}\n\n", total_lines)); 198 199 analysis.push_str("๐ก Suggestions:\n"); 200 analysis.push_str("- Consider adding documentation if missing\n"); 201 analysis.push_str("- Ensure proper error handling\n"); 202 analysis.push_str("- Add unit tests if not present\n"); 203 analysis.push_str("- Check for code duplication\n"); 204 205 Ok(analysis) 206 } 207 208 // Calculator tool with math functions 209 pub fn calculator(expression: &str) -> Result<String> { 210 // Simple expression evaluator with basic math functions 211 let expr = expression.trim(); 212 213 // Handle basic operations 214 if let Ok(result) = eval_expression(expr) { 215 Ok(result.to_string()) 216 } else { 217 Err(anyhow::anyhow!("Invalid mathematical expression: {}", expr)) 218 } 219 } 220 221 fn eval_expression(expr: &str) -> Result<f64> { 222 // Basic math expression evaluator 223 // This is a simplified version - in production you'd want a proper parser 224 let expr = expr.replace(" ", ""); 225 226 // Handle basic functions 227 if expr.starts_with("sqrt(") && expr.ends_with(")") { 228 let inner = &expr[5..expr.len()-1]; 229 let val = eval_expression(inner)?; 230 return Ok(val.sqrt()); 231 } 232 233 if expr.starts_with("sin(") && expr.ends_with(")") { 234 let inner = &expr[4..expr.len()-1]; 235 let val = eval_expression(inner)?; 236 return Ok(val.sin()); 237 } 238 239 if expr.starts_with("cos(") && expr.ends_with(")") { 240 let inner = &expr[4..expr.len()-1]; 241 let val = eval_expression(inner)?; 242 return Ok(val.cos()); 243 } 244 245 // Handle constants 246 if expr == "pi" { return Ok(std::f64::consts::PI); } 247 if expr == "e" { return Ok(std::f64::consts::E); } 248 249 // Handle basic arithmetic 250 if let Some(pos) = expr.rfind('+') { 251 let left = eval_expression(&expr[..pos])?; 252 let right = eval_expression(&expr[pos+1..])?; 253 return Ok(left + right); 254 } 255 256 if let Some(pos) = expr.rfind('-') { 257 if pos > 0 { // Not a negative number 258 let left = eval_expression(&expr[..pos])?; 259 let right = eval_expression(&expr[pos+1..])?; 260 return Ok(left - right); 261 } 262 } 263 264 if let Some(pos) = expr.rfind('*') { 265 let left = eval_expression(&expr[..pos])?; 266 let right = eval_expression(&expr[pos+1..])?; 267 return Ok(left * right); 268 } 269 270 if let Some(pos) = expr.rfind('/') { 271 let left = eval_expression(&expr[..pos])?; 272 let right = eval_expression(&expr[pos+1..])?; 273 if right == 0.0 { 274 return Err(anyhow::anyhow!("Division by zero")); 275 } 276 return Ok(left / right); 277 } 278 279 if let Some(pos) = expr.find('^') { 280 let left = eval_expression(&expr[..pos])?; 281 let right = eval_expression(&expr[pos+1..])?; 282 return Ok(left.powf(right)); 283 } 284 285 // Try to parse as number 286 expr.parse::<f64>().map_err(|_| anyhow::anyhow!("Invalid number: {}", expr)) 287 } 288 289 // String manipulation tools 290 pub fn string_length(text: &str) -> Result<String> { 291 Ok(text.len().to_string()) 292 } 293 294 pub fn reverse_string(text: &str) -> Result<String> { 295 Ok(text.chars().rev().collect()) 296 } 297 298 pub fn uppercase_string(text: &str) -> Result<String> { 299 Ok(text.to_uppercase()) 300 } 301 302 pub fn lowercase_string(text: &str) -> Result<String> { 303 Ok(text.to_lowercase()) 304 } 305 306 // RAG tools 307 pub fn create_document_index(files: &[String]) -> Result<String> { 308 let mut documents = Vec::new(); 309 let mut total_chars = 0; 310 311 for file_path in files { 312 if let Ok(content) = fs::read_to_string(file_path) { 313 documents.push(format!("=== {} ===\n{}", file_path, content)); 314 total_chars += content.len(); 315 } 316 } 317 318 // Simple index creation (in production, you'd use proper embeddings) 319 let index_path = ".kamaji_index.json"; 320 let index_data = serde_json::json!({ 321 "documents": documents, 322 "files": files, 323 "created_at": chrono::Utc::now().to_rfc3339(), 324 "total_chars": total_chars 325 }); 326 327 fs::write(index_path, serde_json::to_string_pretty(&index_data)?)?; 328 Ok(format!("Created document index with {} files ({} characters)", files.len(), total_chars)) 329 } 330 331 pub fn search_documents(query: &str) -> Result<String> { 332 let index_path = ".kamaji_index.json"; 333 334 if !Path::new(index_path).exists() { 335 return Err(anyhow::anyhow!("No document index found. Create one first with create_document_index.")); 336 } 337 338 let index_content = fs::read_to_string(index_path)?; 339 let index_data: serde_json::Value = serde_json::from_str(&index_content)?; 340 341 let documents = index_data["documents"].as_array() 342 .ok_or_else(|| anyhow::anyhow!("Invalid index format"))?; 343 344 // Simple keyword search (in production, you'd use vector similarity) 345 let query_lower = query.to_lowercase(); 346 let mut matches = Vec::new(); 347 348 for (i, doc) in documents.iter().enumerate() { 349 if let Some(doc_str) = doc.as_str() { 350 if doc_str.to_lowercase().contains(&query_lower) { 351 // Extract relevant snippet 352 let lines: Vec<&str> = doc_str.lines().collect(); 353 let mut relevant_lines = Vec::new(); 354 355 for (line_idx, line) in lines.iter().enumerate() { 356 if line.to_lowercase().contains(&query_lower) { 357 // Add context lines 358 let start = line_idx.saturating_sub(2); 359 let end = (line_idx + 3).min(lines.len()); 360 relevant_lines.extend_from_slice(&lines[start..end]); 361 break; 362 } 363 } 364 365 matches.push(format!("Document {}: {}", i + 1, relevant_lines.join("\n"))); 366 } 367 } 368 } 369 370 if matches.is_empty() { 371 Ok(format!("No documents found matching query: '{}'", query)) 372 } else { 373 Ok(format!("Found {} matches:\n\n{}", matches.len(), matches.join("\n\n---\n\n"))) 374 } 375 } 376 377 // Existing filesystem tools 378 pub fn execute_shell_command(command: &str) -> Result<String> { 379 let output = Command::new("sh") 380 .arg("-c") 381 .arg(command) 382 .output()?; 383 384 let mut result = Vec::new(); 385 386 if !output.stdout.is_empty() { 387 result.push(format!("STDOUT:\n{}", String::from_utf8_lossy(&output.stdout))); 388 } 389 390 if !output.stderr.is_empty() { 391 result.push(format!("STDERR:\n{}", String::from_utf8_lossy(&output.stderr))); 392 } 393 394 if !output.status.success() { 395 result.push(format!("Exit code: {}", output.status.code().unwrap_or(-1))); 396 } 397 398 let full_output = if result.is_empty() { 399 "Command executed successfully (no output)".to_string() 400 } else { 401 result.join("\n") 402 }; 403 404 if command.contains("git push") && full_output.to_lowercase().contains("rejected") { 405 Ok(format!("{}\n\n๐ก HINT: Push was rejected. Try:\n1. Run 'git pull' to fetch remote changes\n2. If conflicts occur, use resolve_git_conflicts tool\n3. Then 'git push' again", full_output)) 406 } else { 407 Ok(full_output) 408 } 409 } 410 411 pub fn read_file(path: &str) -> Result<String> { 412 let content = fs::read_to_string(path)?; 413 Ok(content) 414 } 415 416 pub fn write_file(path: &str, content: &str) -> Result<String> { 417 fs::write(path, content)?; 418 Ok(format!("Successfully wrote {} bytes to {}", content.len(), path)) 419 } 420 421 pub fn append_to_file(path: &str, content: &str) -> Result<String> { 422 let mut existing = String::new(); 423 if Path::new(path).exists() { 424 existing = fs::read_to_string(path)?; 425 } 426 427 let new_content = if existing.is_empty() || existing.ends_with('\n') { 428 format!("{}{}", existing, content) 429 } else { 430 format!("{}\n{}", existing, content) 431 }; 432 433 fs::write(path, new_content)?; 434 Ok(format!("Successfully appended {} bytes to {}", content.len(), path)) 435 } 436 437 pub fn list_directory(path: &str) -> Result<String> { 438 let entries = fs::read_dir(path)?; 439 let mut items = Vec::new(); 440 441 for entry in entries { 442 let entry = entry?; 443 let metadata = entry.metadata()?; 444 let name = entry.file_name().to_string_lossy().to_string(); 445 446 if metadata.is_dir() { 447 items.push(format!("{}/", name)); 448 } else { 449 items.push(name); 450 } 451 } 452 453 items.sort(); 454 Ok(items.join("\n")) 455 } 456 457 pub fn get_current_directory() -> Result<String> { 458 let cwd = std::env::current_dir()?; 459 Ok(cwd.to_string_lossy().to_string()) 460 } 461 462 pub fn resolve_git_conflicts() -> Result<String> { 463 let status_output = execute_shell_command("git status --porcelain")?; 464 465 if status_output.contains("UU") { 466 execute_shell_command("git add .")?; 467 execute_shell_command("git commit -m 'Auto-resolve merge conflicts'")?; 468 Ok("Auto-resolved conflicts by accepting current changes".to_string()) 469 } else { 470 Ok("No merge conflicts detected".to_string()) 471 } 472 }