From 9d9e959744a15d20acf6edc253b84a1fbdd4ec88 Mon Sep 17 00:00:00 2001 From: Riz Ashraf Date: Thu, 8 Oct 2026 23:01:38 +0100 Subject: [PATCH] chore: fix formatting and clippy lints --- mcp-stdio/src/lib.rs | 2 +- nvim-core/src/lib.rs | 68 ++++---- server/src/api/events.rs | 5 +- server/src/api/telemetry.rs | 66 ++++--- server/src/api/ws.rs | 24 ++- server/src/bin/export_schemas.rs | 30 ++-- server/src/bin/test_image.rs | 5 + server/src/config.rs | 42 +++++ server/src/db.rs | 10 +- server/src/error.rs | 16 +- server/src/handlers/ast.rs | 10 +- server/src/handlers/env.rs | 50 ++++-- server/src/handlers/git.rs | 6 +- server/src/handlers/graph.rs | 222 ++++++++++++++++-------- server/src/handlers/logs.rs | 46 ++--- server/src/handlers/meta.rs | 237 +++++++++++++++----------- server/src/handlers/mod.rs | 1 - server/src/handlers/notes.rs | 44 +++-- server/src/handlers/reconciliation.rs | 44 +++-- server/src/handlers/tasks.rs | 20 +-- server/src/handlers/vision.rs | 98 ++++++----- server/src/handlers/workspaces.rs | 38 +++-- server/src/lib.rs | 160 ++++++++++++----- server/src/models.rs | 28 +-- server/src/ollama.rs | 1 - server/src/router.rs | 126 ++++++++++---- server/src/search.rs | 100 ++++++++--- server/src/state.rs | 10 +- server/src/store.rs | 29 ++-- server/src/tools.rs | 8 +- server/src/watcher.rs | 69 +++++--- server/tests/image_test.rs | 7 + server/tests/parity_test.rs | 15 +- stub/src/logger.rs | 32 +++- stub/src/main.rs | 4 +- 35 files changed, 1091 insertions(+), 582 deletions(-) create mode 100644 server/src/bin/test_image.rs create mode 100644 server/src/config.rs create mode 100644 server/tests/image_test.rs diff --git a/mcp-stdio/src/lib.rs b/mcp-stdio/src/lib.rs index 9e99749..c298ad6 100644 --- a/mcp-stdio/src/lib.rs +++ b/mcp-stdio/src/lib.rs @@ -23,7 +23,7 @@ pub async fn read_mcp_message( break; } - if trimmed_line.as_bytes().len() >= 15 + if trimmed_line.len() >= 15 && trimmed_line.as_bytes()[..15].eq_ignore_ascii_case(b"content-length:") { length = trimmed_line[15..].trim().parse().unwrap_or(0); diff --git a/nvim-core/src/lib.rs b/nvim-core/src/lib.rs index 9adfc05..c7741d7 100644 --- a/nvim-core/src/lib.rs +++ b/nvim-core/src/lib.rs @@ -214,7 +214,7 @@ pub struct NvimRequest { pub reply: oneshot::Sender>, } -use std::sync::atomic::{AtomicU32, AtomicU64, AtomicU8, Ordering}; +use std::sync::atomic::{AtomicU8, AtomicU32, AtomicU64, Ordering}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum CircuitState { @@ -275,7 +275,9 @@ impl NvimCircuitBreaker { pub fn record_success(&self) { self.consecutive_failures.store(0, Ordering::SeqCst); - let prev = self.state.swap(CircuitState::Closed as u8, Ordering::SeqCst); + let prev = self + .state + .swap(CircuitState::Closed as u8, Ordering::SeqCst); if prev != CircuitState::Closed as u8 { tracing::info!("Neovim RPC circuit breaker transitioned to Closed (recovered)"); } @@ -299,7 +301,8 @@ impl NvimCircuitBreaker { pub fn reset(&self) { self.consecutive_failures.store(0, Ordering::SeqCst); - self.state.store(CircuitState::Closed as u8, Ordering::SeqCst); + self.state + .store(CircuitState::Closed as u8, Ordering::SeqCst); } } @@ -374,18 +377,15 @@ async fn get_nvim_connection() -> Result, String> { // without spawning any external nvim.exe child process! { let conn_lock = NVIM_CONN.lock().await; - if let Some(sender) = conn_lock.as_ref() { - if !sender.is_closed() { + if let Some(sender) = conn_lock.as_ref() + && !sender.is_closed() { let active_lock = ACTIVE_SOCKET.lock().await; - if let Some(ref current_socket) = *active_lock { - if let Ok(target) = read_active_socket_file().await { - if !target.is_empty() && &target == current_socket { + if let Some(ref current_socket) = *active_lock + && let Ok(target) = read_active_socket_file().await + && !target.is_empty() && &target == current_socket { return Ok(sender.clone()); } - } - } } - } } let interactive_socket = get_socket_path().await.ok(); @@ -643,7 +643,10 @@ async fn get_nvim_connection() -> Result, String> { async fn call_nvim(req: rmpv::Value) -> Result { if !CIRCUIT_BREAKER.can_execute() { - return Err("Neovim RPC circuit breaker is OPEN (consecutive failures detected). Failing fast.".to_string()); + return Err( + "Neovim RPC circuit breaker is OPEN (consecutive failures detected). Failing fast." + .to_string(), + ); } let msgid = if let rmpv::Value::Array(ref arr) = req { @@ -1749,17 +1752,18 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) { .to_string() }); - let options_arr = match args.get("options").and_then(|v| v.as_array()) { - Some(arr) => { - let rmp_arr: Vec = arr - .iter() - .filter_map(|item| item.as_str()) - .map(|s| rmpv::Value::String(s.into())) - .collect(); - rmpv::Value::Array(rmp_arr) - } - None => rmpv::Value::Nil, - }; + let options_arr = + match args.get("options").and_then(|v| v.as_array()) { + Some(arr) => { + let rmp_arr: Vec = arr + .iter() + .filter_map(|item| item.as_str()) + .map(|s| rmpv::Value::String(s.into())) + .collect(); + rmpv::Value::Array(rmp_arr) + } + None => rmpv::Value::Nil, + }; let lua_code = r#" local prompt_text, options, request_id = ... @@ -1782,7 +1786,8 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) { ) ); } else { - send_error(id, -32602, "Missing 'prompt' or 'prompt_text'").await; + send_error(id, -32602, "Missing 'prompt' or 'prompt_text'") + .await; } } "extmark" => { @@ -1812,11 +1817,10 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) { } "highlight" | "clear_highlight" => { let mut modified_args = args.clone(); - if action == "clear_highlight" { - if let Some(obj) = modified_args.as_object_mut() { + if action == "clear_highlight" + && let Some(obj) = modified_args.as_object_mut() { obj.insert("clear_only".to_string(), json!(true)); } - } let args_rmp = json_to_rmpv(&modified_args); let code = " local args = ... @@ -2216,13 +2220,19 @@ mod tests { // 2nd failure - trips to Open cb.record_failure(); assert_eq!(cb.current_state(), CircuitState::Open); - assert!(!cb.can_execute(), "Circuit breaker should fail fast when Open"); + assert!( + !cb.can_execute(), + "Circuit breaker should fail fast when Open" + ); // Wait for cooldown std::thread::sleep(std::time::Duration::from_millis(60)); // After cooldown, can_execute transitions to HalfOpen - assert!(cb.can_execute(), "After cooldown, should allow HalfOpen probe"); + assert!( + cb.can_execute(), + "After cooldown, should allow HalfOpen probe" + ); assert_eq!(cb.current_state(), CircuitState::HalfOpen); // Success in HalfOpen recovers back to Closed diff --git a/server/src/api/events.rs b/server/src/api/events.rs index 70a166e..01aa955 100644 --- a/server/src/api/events.rs +++ b/server/src/api/events.rs @@ -41,7 +41,10 @@ pub async fn wait_for_event_handler( } } Err(tokio::sync::broadcast::error::RecvError::Lagged(skipped)) => { - tracing::warn!("Event bus receiver lagged by {} messages; continuing wait.", skipped); + tracing::warn!( + "Event bus receiver lagged by {} messages; continuing wait.", + skipped + ); continue; } Err(tokio::sync::broadcast::error::RecvError::Closed) => { diff --git a/server/src/api/telemetry.rs b/server/src/api/telemetry.rs index d9eae17..9aab72a 100644 --- a/server/src/api/telemetry.rs +++ b/server/src/api/telemetry.rs @@ -44,8 +44,7 @@ pub fn find_projected_knowledge( || normalized_file.ends_with(&format!("/{}", ndfp.trim_start_matches('/'))) || ndfp.ends_with(&format!("/{}", normalized_file.trim_start_matches('/'))) || (!file_name.is_empty() - && (ndfp == file_name - || ndfp.ends_with(&format!("/{}", file_name)))) + && (ndfp == file_name || ndfp.ends_with(&format!("/{}", file_name)))) { return true; } @@ -73,8 +72,7 @@ pub fn find_projected_knowledge( all_adrs .iter() .filter(|a| { - a.status.eq_ignore_ascii_case("accepted") - || a.status.eq_ignore_ascii_case("active") + a.status.eq_ignore_ascii_case("accepted") || a.status.eq_ignore_ascii_case("active") }) .filter(|a| { for comp in &a.affected_components { @@ -189,31 +187,47 @@ pub async fn nvim_telemetry_handler( if payload.event.starts_with("agent_") || payload.event.starts_with("diff_") { let payload_val = serde_json::json!(&payload); - let _ = state.handler.state.event_bus_tx.send(crate::state::GenericEvent { - topic: format!("nvim:ui:{}", payload.event), - session_id: Some(payload.session_id.clone()), - payload: payload_val.clone(), - }); - - if let Some(ref req_id) = payload.request_id { - let _ = state.handler.state.event_bus_tx.send(crate::state::GenericEvent { - topic: format!("nvim:ui:{}:{}", payload.event, req_id), + let _ = state + .handler + .state + .event_bus_tx + .send(crate::state::GenericEvent { + topic: format!("nvim:ui:{}", payload.event), session_id: Some(payload.session_id.clone()), payload: payload_val.clone(), }); + + if let Some(ref req_id) = payload.request_id { + let _ = state + .handler + .state + .event_bus_tx + .send(crate::state::GenericEvent { + topic: format!("nvim:ui:{}:{}", payload.event, req_id), + session_id: Some(payload.session_id.clone()), + payload: payload_val.clone(), + }); } if let Some(ref diff_id) = payload.diff_id { - let _ = state.handler.state.event_bus_tx.send(crate::state::GenericEvent { - topic: format!("nvim:ui:{}:{}", payload.event, diff_id), - session_id: Some(payload.session_id.clone()), - payload: payload_val.clone(), - }); - let _ = state.handler.state.event_bus_tx.send(crate::state::GenericEvent { - topic: format!("nvim:ui:diff_decision:{}", diff_id), - session_id: Some(payload.session_id.clone()), - payload: payload_val.clone(), - }); + let _ = state + .handler + .state + .event_bus_tx + .send(crate::state::GenericEvent { + topic: format!("nvim:ui:{}:{}", payload.event, diff_id), + session_id: Some(payload.session_id.clone()), + payload: payload_val.clone(), + }); + let _ = state + .handler + .state + .event_bus_tx + .send(crate::state::GenericEvent { + topic: format!("nvim:ui:diff_decision:{}", diff_id), + session_id: Some(payload.session_id.clone()), + payload: payload_val.clone(), + }); } } @@ -230,7 +244,11 @@ pub async fn nvim_telemetry_handler( tasks .iter() .find(|t| t.is_active()) - .or_else(|| tasks.iter().find(|t| t.status == "pending" && t.parent_id.is_none())) + .or_else(|| { + tasks + .iter() + .find(|t| t.status == "pending" && t.parent_id.is_none()) + }) .map(|t| t.title.clone()) }); diff --git a/server/src/api/ws.rs b/server/src/api/ws.rs index ebcbfed..54aee27 100644 --- a/server/src/api/ws.rs +++ b/server/src/api/ws.rs @@ -66,9 +66,11 @@ pub async fn handle_socket(socket: WebSocket, state: Arc, _client_type activities .iter() .filter_map(|act_val| { - if act_val["category"] == "TASK_EVENT" { - if let Some(details_str) = act_val["details"].as_str() { - if let Ok(event_val) = serde_json::from_str::(details_str) { + if act_val["category"] == "TASK_EVENT" + && let Some(details_str) = act_val["details"].as_str() + && let Ok(event_val) = + serde_json::from_str::(details_str) + { return Some( serde_json::json!({ "jsonrpc": "2.0", @@ -78,8 +80,6 @@ pub async fn handle_socket(socket: WebSocket, state: Arc, _client_type .to_string(), ); } - } - } None }) .take(5) @@ -262,7 +262,10 @@ mod tests { mem_state.broadcast_task_event(task_event.clone()); // Verify recent activities recorded the event - let recorded = mem_state.telemetry.recent_activities.read_with(|act| act.clone()); + let recorded = mem_state + .telemetry + .recent_activities + .read_with(|act| act.clone()); assert!(!recorded.is_empty()); assert_eq!(recorded[0]["category"], "TASK_EVENT"); @@ -293,7 +296,9 @@ mod tests { .filter_map(|act_val| { if act_val["category"] == "TASK_EVENT" { if let Some(details_str) = act_val["details"].as_str() { - if let Ok(event_val) = serde_json::from_str::(details_str) { + if let Ok(event_val) = + serde_json::from_str::(details_str) + { return Some( serde_json::json!({ "jsonrpc": "2.0", @@ -315,7 +320,10 @@ mod tests { let _ = tx.try_send(notif); } - let replayed_msg = rx.recv().await.expect("Expected replayed task event notification"); + let replayed_msg = rx + .recv() + .await + .expect("Expected replayed task event notification"); let parsed: serde_json::Value = serde_json::from_str(&replayed_msg).unwrap(); assert_eq!(parsed["method"], "notifications/task/completed"); assert_eq!(parsed["params"]["task_id"], "task-999"); diff --git a/server/src/bin/export_schemas.rs b/server/src/bin/export_schemas.rs index cd7f551..1a00c46 100644 --- a/server/src/bin/export_schemas.rs +++ b/server/src/bin/export_schemas.rs @@ -1,8 +1,8 @@ +use mcp_memory_server::router::MemoryHandler; +use mcp_memory_server::state::MemoryState; use std::fs; use std::path::PathBuf; use std::sync::Arc; -use mcp_memory_server::router::MemoryHandler; -use mcp_memory_server::state::MemoryState; #[tokio::main] async fn main() -> Result<(), Box> { @@ -13,7 +13,8 @@ async fn main() -> Result<(), Box> { let handler = MemoryHandler::new(state); let win_dir = PathBuf::from(r"C:\Users\reazul.ashraf\.gemini\antigravity-cli\mcp\mcp-memory"); - let wsl_dir = PathBuf::from(r"\\wsl.localhost\Ubuntu\home\riz\.gemini\antigravity-cli\mcp\mcp-memory"); + let wsl_dir = + PathBuf::from(r"\\wsl.localhost\Ubuntu\home\riz\.gemini\antigravity-cli\mcp\mcp-memory"); fs::create_dir_all(&win_dir)?; if !wsl_dir.exists() { @@ -41,14 +42,12 @@ async fn main() -> Result<(), Box> { if let Ok(entries) = fs::read_dir(&win_dir) { for entry in entries.flatten() { let path = entry.path(); - if path.extension().map_or(false, |ext| ext == "json") { - if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) { - if !active_tools.contains(stem) { + if path.extension().is_some_and(|ext| ext == "json") + && let Some(stem) = path.file_stem().and_then(|s| s.to_str()) + && !active_tools.contains(stem) { println!(" [STALE REMOVED Win] {}", path.display()); let _ = fs::remove_file(&path); } - } - } } } @@ -56,23 +55,24 @@ async fn main() -> Result<(), Box> { if let Ok(entries) = fs::read_dir(&wsl_dir) { for entry in entries.flatten() { let path = entry.path(); - if path.extension().map_or(false, |ext| ext == "json") { - if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) { - if !active_tools.contains(stem) { + if path.extension().is_some_and(|ext| ext == "json") + && let Some(stem) = path.file_stem().and_then(|s| s.to_str()) + && !active_tools.contains(stem) { println!(" [STALE REMOVED WSL] {}", path.display()); let _ = fs::remove_file(&path); } - } - } } } // Sync instructions.md - let instructions_src = PathBuf::from(r"C:\Users\reazul.ashraf\workspace\rust\mcp-memory\instructions.md"); + let instructions_src = + PathBuf::from(r"C:\Users\reazul.ashraf\workspace\rust\mcp-memory\instructions.md"); if instructions_src.exists() { let _ = fs::copy(&instructions_src, win_dir.join("instructions.md")); let _ = fs::copy(&instructions_src, wsl_dir.join("instructions.md")); - let server_instructions = PathBuf::from(r"C:\Users\reazul.ashraf\workspace\rust\mcp-memory\server\src\instructions.md"); + let server_instructions = PathBuf::from( + r"C:\Users\reazul.ashraf\workspace\rust\mcp-memory\server\src\instructions.md", + ); let _ = fs::copy(&instructions_src, server_instructions); println!(" [OK] Synchronized instructions.md to Win, WSL, and server/src/instructions.md"); } diff --git a/server/src/bin/test_image.rs b/server/src/bin/test_image.rs new file mode 100644 index 0000000..1e05918 --- /dev/null +++ b/server/src/bin/test_image.rs @@ -0,0 +1,5 @@ +fn main() { + let mut img = image::DynamicImage::new_rgb8(10, 10); + img.invert(); + let _img2 = img.adjust_contrast(20.0); +} diff --git a/server/src/config.rs b/server/src/config.rs new file mode 100644 index 0000000..fc521df --- /dev/null +++ b/server/src/config.rs @@ -0,0 +1,42 @@ +use serde_json::Value; +use std::env; +use std::fs; +use std::path::PathBuf; +use tracing::info; + +pub fn load_mcp_config_env() { + let config_path = dirs::home_dir() + .unwrap_or_else(|| PathBuf::from(".")) + .join(".gemini/config/mcp_config.json"); + + if !config_path.exists() { + return; + } + + let config_str = match fs::read_to_string(&config_path) { + Ok(s) => s, + Err(_) => return, + }; + + let parsed: Value = match serde_json::from_str(&config_str) { + Ok(v) => v, + Err(_) => return, + }; + + if let Some(env_obj) = parsed + .get("mcpServers") + .and_then(|v| v.get("mcp-memory")) + .and_then(|v| v.get("env")) + .and_then(|v| v.as_object()) + { + for (key, val) in env_obj { + if let Some(val_str) = val.as_str() + && env::var(key).is_err() { + unsafe { + env::set_var(key, val_str); + } + info!("Loaded env var {} from mcp_config.json", key); + } + } + } +} diff --git a/server/src/db.rs b/server/src/db.rs index 1dee77e..32af155 100644 --- a/server/src/db.rs +++ b/server/src/db.rs @@ -167,23 +167,21 @@ pub fn init_redb(base: &Path) -> Arc { ); } else { let migrated_path = json_path.with_extension("json.migrated"); - if let Err(_) = std::fs::rename(&json_path, &migrated_path) { - if migrated_path.exists() { + if std::fs::rename(&json_path, &migrated_path).is_err() + && migrated_path.exists() { let _ = std::fs::remove_file(&migrated_path); let _ = std::fs::rename(&json_path, &migrated_path); } - } } } } } } } - if opened_ok { - if let Err(e) = write_txn.commit() { + if opened_ok + && let Err(e) = write_txn.commit() { tracing::error!("Failed to commit database migration transaction: {}", e); } - } } Err(e) => { tracing::error!( diff --git a/server/src/error.rs b/server/src/error.rs index a824239..3619f7c 100644 --- a/server/src/error.rs +++ b/server/src/error.rs @@ -17,7 +17,11 @@ pub enum AppError { EntityNotFound { name: String, namespace: String }, #[error("Invalid relation between '{from}' and '{to}': {reason}")] - InvalidRelation { from: String, to: String, reason: String }, + InvalidRelation { + from: String, + to: String, + reason: String, + }, #[error("Database error for key '{key}': {message}")] DatabaseError { key: String, message: String }, @@ -50,11 +54,17 @@ impl IntoResponse for AppError { AppError::NotFound(msg) => (StatusCode::NOT_FOUND, msg.clone()), AppError::EntityNotFound { name, namespace } => ( StatusCode::NOT_FOUND, - format!("Entity '{}' was not found in namespace '{}'", name, namespace), + format!( + "Entity '{}' was not found in namespace '{}'", + name, namespace + ), ), AppError::InvalidRelation { from, to, reason } => ( StatusCode::BAD_REQUEST, - format!("Invalid relation between '{}' and '{}': {}", from, to, reason), + format!( + "Invalid relation between '{}' and '{}': {}", + from, to, reason + ), ), AppError::DatabaseError { key, message } => ( StatusCode::INTERNAL_SERVER_ERROR, diff --git a/server/src/handlers/ast.rs b/server/src/handlers/ast.rs index ff0d7e2..3e58663 100644 --- a/server/src/handlers/ast.rs +++ b/server/src/handlers/ast.rs @@ -119,11 +119,10 @@ impl McpTool for ReadFileSkeletonHandler { header.push('\n'); } } - if header.is_empty() { - if let Some(first_line) = node_text.lines().next() { + if header.is_empty() + && let Some(first_line) = node_text.lines().next() { header = first_line.to_string(); } - } out.push_str(&indent); out.push_str(header.trim()); @@ -425,11 +424,10 @@ fn scan_workspace_for_symbol( if scanned_files > 500 { break; } - if let Ok(meta) = std::fs::metadata(path) { - if meta.len() > 1024 * 1024 { + if let Ok(meta) = std::fs::metadata(path) + && meta.len() > 1024 * 1024 { continue; } - } if let Ok(content) = std::fs::read_to_string(path) { for (line_num, line) in content.lines().enumerate() { let is_match = if filter_fn_call { diff --git a/server/src/handlers/env.rs b/server/src/handlers/env.rs index 3e4bcfd..6a55ec6 100644 --- a/server/src/handlers/env.rs +++ b/server/src/handlers/env.rs @@ -22,16 +22,31 @@ impl McpTool for EnvironmentHandler { async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: EnvironmentTool = serde_json::from_value(args).map_err(|e| e.to_string())?; - let ns = req.namespace.unwrap_or_else(|| crate::models::default_namespace()); + let ns = req + .namespace + .unwrap_or_else(crate::models::default_namespace); match req.action { EnvAction::UpdateFingerprint => { let tool_versions = req.tool_versions.unwrap_or_default(); let detected_os = req.os.unwrap_or_else(|| std::env::consts::OS.to_string()); - let detected_shell = req.shell + let detected_shell = req + .shell .or_else(|| std::env::var("SHELL").ok()) .or_else(|| std::env::var("STARSHIP_SHELL").ok()) - .or_else(|| if std::env::var("PSModulePath").is_ok() { Some("pwsh".to_string()) } else { None }) - .or_else(|| if std::env::var("COMSPEC").is_ok() { Some("cmd".to_string()) } else { None }) + .or_else(|| { + if std::env::var("PSModulePath").is_ok() { + Some("pwsh".to_string()) + } else { + None + } + }) + .or_else(|| { + if std::env::var("COMSPEC").is_ok() { + Some("cmd".to_string()) + } else { + None + } + }) .unwrap_or_else(|| "unknown".to_string()); state.env.env_fingerprints.modify(|fps| { @@ -102,10 +117,7 @@ impl McpTool for EnvironmentHandler { } EnvAction::GetDetails => { let data = state.env.environments.read_with(|envs| { - let filtered: Vec<_> = envs - .iter() - .filter(|e| e.namespace == ns) - .collect(); + let filtered: Vec<_> = envs.iter().filter(|e| e.namespace == ns).collect(); Ok::(serde_json::to_string(&filtered)?) })?; Ok(data) @@ -114,7 +126,6 @@ impl McpTool for EnvironmentHandler { } } - #[cfg(test)] mod tests { use super::*; @@ -143,7 +154,10 @@ mod tests { assert_eq!(res, "Env fingerprint updated"); let res2 = handler - .execute(json!({"action": "read_fingerprint", "namespace": "global"}), state.clone()) + .execute( + json!({"action": "read_fingerprint", "namespace": "global"}), + state.clone(), + ) .await .map_err(|e| crate::error::AppError::Internal(e.to_string())) .unwrap(); @@ -171,7 +185,10 @@ mod tests { let handler = EnvironmentHandler; let res = handler - .execute(json!({"action": "get_details", "namespace": "global"}), state.clone()) + .execute( + json!({"action": "get_details", "namespace": "global"}), + state.clone(), + ) .await .map_err(|e| crate::error::AppError::Internal(e.to_string())) .unwrap(); @@ -215,7 +232,10 @@ mod tests { assert_eq!(res2, "Environment registered"); let res3 = handler - .execute(json!({"action": "get_details", "namespace": "global"}), state.clone()) + .execute( + json!({"action": "get_details", "namespace": "global"}), + state.clone(), + ) .await .map_err(|e| crate::error::AppError::Internal(e.to_string())) .unwrap(); @@ -237,12 +257,8 @@ mod tests { } }); - let res = handler - .execute(args, state.clone()) - .await; + let res = handler.execute(args, state.clone()).await; assert!(res.is_ok()); } } - - diff --git a/server/src/handlers/git.rs b/server/src/handlers/git.rs index 8e14fbf..d0f5a81 100644 --- a/server/src/handlers/git.rs +++ b/server/src/handlers/git.rs @@ -139,8 +139,8 @@ impl McpTool for QueryGitDiffsHandler { let git_matches = tokio::task::spawn_blocking(move || { let mut results = Vec::new(); let cwd = env::current_dir().unwrap_or_default(); - if let Ok(repo) = git2::Repository::discover(&cwd) { - if let Ok(mut revwalk) = repo.revwalk() { + if let Ok(repo) = git2::Repository::discover(&cwd) + && let Ok(mut revwalk) = repo.revwalk() { let _ = revwalk.push_head(); let mut count = 0; for oid in revwalk.flatten() { @@ -161,7 +161,6 @@ impl McpTool for QueryGitDiffsHandler { } } } - } results }) .await @@ -215,4 +214,3 @@ mod tests { assert!(parsed.get("branch").is_some() || parsed.is_object()); } } - diff --git a/server/src/handlers/graph.rs b/server/src/handlers/graph.rs index 68e5077..b5232d4 100644 --- a/server/src/handlers/graph.rs +++ b/server/src/handlers/graph.rs @@ -62,8 +62,8 @@ impl<'a> GraphQueryBuilder<'a> { break; } nodes_at_current_depth -= 1; - if current_depth < self.max_depth { - if let Some(neighbors) = adj.get(current) { + if current_depth < self.max_depth + && let Some(neighbors) = adj.get(current) { for &(neighbor, rel_type, is_inverse) in neighbors { if !visited.contains(neighbor) { visited.insert(neighbor); @@ -73,7 +73,6 @@ impl<'a> GraphQueryBuilder<'a> { } } } - } if nodes_at_current_depth == 0 { current_depth += 1; nodes_at_current_depth = nodes_at_next_depth; @@ -138,7 +137,9 @@ impl McpTool for QueryGraphPathHandler { }) }) .await - .map_err(|e| crate::error::AppError::Internal(format!("Graph traversal task failed: {}", e)))? + .map_err(|e| { + crate::error::AppError::Internal(format!("Graph traversal task failed: {}", e)) + })? } } @@ -169,12 +170,19 @@ impl McpTool for CreateEntitiesHandler { } } }); - let names: Vec = inserted.iter().map(|e| format!("{} ({})", e.name, e.entity_type)).collect(); + let names: Vec = inserted + .iter() + .map(|e| format!("{} ({})", e.name, e.entity_type)) + .collect(); if !inserted.is_empty() { let idx = state.get_search_index().await; let _ = idx.index_entities_batch(&inserted).await; } - Ok(format!("Successfully created {} entity/entities: {}", names.len(), names.join(", "))) + Ok(format!( + "Successfully created {} entity/entities: {}", + names.len(), + names.join(", ") + )) } } @@ -236,7 +244,9 @@ impl McpTool for CreateRelationsHandler { crate::models::Entity { name: node_name.clone(), entity_type: "Entity".to_string(), - observations: vec!["Auto-created stub entity for relation endpoint".to_string()], + observations: vec![ + "Auto-created stub entity for relation endpoint".to_string(), + ], namespace: crate::models::default_namespace(), git_branch: None, ..Default::default() @@ -247,16 +257,28 @@ impl McpTool for CreateRelationsHandler { } for mut relation in req.relations { if !relation.from.is_empty() && !relation.to.is_empty() { - relation.relation_type = crate::models::normalize_relation_type(&relation.relation_type); - added_relations.push(format!("{} -[{}]-> {}", relation.from, relation.relation_type, relation.to)); + relation.relation_type = + crate::models::normalize_relation_type(&relation.relation_type); + added_relations.push(format!( + "{} -[{}]-> {}", + relation.from, relation.relation_type, relation.to + )); g.relations.push(relation); } } }); - let mut msg = format!("Successfully created {} relation(s):\n{}", added_relations.len(), added_relations.join("\n")); + let mut msg = format!( + "Successfully created {} relation(s):\n{}", + added_relations.len(), + added_relations.join("\n") + ); if !auto_created.is_empty() { - msg.push_str(&format!("\nNote: Auto-created {} missing stub entity/entities: {}", auto_created.len(), auto_created.join(", "))); + msg.push_str(&format!( + "\nNote: Auto-created {} missing stub entity/entities: {}", + auto_created.len(), + auto_created.join(", ") + )); } Ok(msg) } @@ -462,7 +484,7 @@ impl McpTool for ReadGraphHandler { let filtered_entities: Vec<(&String, &crate::models::Entity)> = full .entities .iter() - .filter(|(_, v)| req.namespace.as_ref().map_or(true, |ns| &v.namespace == ns)) + .filter(|(_, v)| req.namespace.as_ref().is_none_or(|ns| &v.namespace == ns)) .collect(); let total_count = filtered_entities.len(); @@ -481,7 +503,7 @@ impl McpTool for ReadGraphHandler { .relations .iter() .filter(|r| { - let ns_match = req.namespace.as_ref().map_or(true, |ns| &r.namespace == ns); + let ns_match = req.namespace.as_ref().is_none_or(|ns| &r.namespace == ns); ns_match && (paged_entities.contains_key(&r.from) || paged_entities.contains_key(&r.to)) }) .collect(); @@ -499,7 +521,7 @@ impl McpTool for ReadGraphHandler { let rel_vec: Vec<&crate::models::Relation> = full .relations .iter() - .filter(|r| req.namespace.as_ref().map_or(true, |ns| &r.namespace == ns)) + .filter(|r| req.namespace.as_ref().is_none_or(|ns| &r.namespace == ns)) .collect(); (ent_map, rel_vec, None) }; @@ -549,21 +571,19 @@ impl McpTool for ReadGraphHandler { } }); - if let Some(meta) = &meta_json { - if let Some(obj) = truncated_val.as_object_mut() { + if let Some(meta) = &meta_json + && let Some(obj) = truncated_val.as_object_mut() { obj.insert("total_count".to_string(), meta["total_count"].clone()); obj.insert("offset".to_string(), meta["offset"].clone()); obj.insert("limit".to_string(), meta["limit"].clone()); obj.insert("has_more".to_string(), serde_json::Value::Bool(true)); } - } - if let Ok(trunc_str) = serde_json::to_string(&truncated_val) { - if trunc_str.len() <= max_chars || keep_count == 1 { + if let Ok(trunc_str) = serde_json::to_string(&truncated_val) + && (trunc_str.len() <= max_chars || keep_count == 1) { result_json = trunc_str; break; } - } keep_count = keep_count.saturating_sub(keep_count / 3 + 1); } } @@ -820,7 +840,10 @@ impl McpTool for CondenseEntityHandler { e.observations = unique_obs; } }); - return Ok(format!("Entity '{}' observations condensed with deduplication/AI summary.", req.entity_name)); + return Ok(format!( + "Entity '{}' observations condensed with deduplication/AI summary.", + req.entity_name + )); } state.modify_graph(|master| { @@ -953,10 +976,9 @@ impl McpTool for GetSubgraphHandler { async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { let req: GetSubgraphTool = serde_json::from_value(args).map_err(|e| e.to_string())?; - let root = req - .root_entity - .or(req.root_node) - .ok_or_else(|| crate::error::AppError::Internal("root_entity or root_node is required".to_string()))?; + let root = req.root_entity.or(req.root_node).ok_or_else(|| { + crate::error::AppError::Internal("root_entity or root_node is required".to_string()) + })?; let depth = req.depth.unwrap_or(2); let format = req.format.unwrap_or(SubgraphFormat::Json); @@ -974,15 +996,14 @@ impl McpTool for GetSubgraphHandler { visited.insert(root.clone()); while let Some((curr, d)) = queue.pop_front() { - if d < depth { - if let Some(neighbors) = adj.get(curr) { + if d < depth + && let Some(neighbors) = adj.get(curr) { for &next in neighbors { if visited.insert(next.to_string()) { queue.push_back((next, d + 1)); } } } - } } let ent: std::collections::HashMap = visited @@ -1011,7 +1032,10 @@ impl McpTool for GetSubgraphHandler { markdown.push_str("\n## Relations\n"); for r in &matched_relations { - markdown.push_str(&format!("- `{}` --[{}]--> `{}`\n", r.from, r.relation_type, r.to)); + markdown.push_str(&format!( + "- `{}` --[{}]--> `{}`\n", + r.from, r.relation_type, r.to + )); } return Ok(markdown); } @@ -1038,7 +1062,10 @@ impl McpTool for SweepGraphHealthHandler { } fn schema(&self) -> Value { - crate::mcp::tool_def::("sweep_graph_health", "Audit and sweep graph health") + crate::mcp::tool_def::( + "sweep_graph_health", + "Audit and sweep graph health", + ) } async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { @@ -1132,26 +1159,31 @@ impl McpTool for ResolveStaleSymbolsHandler { } fn schema(&self) -> Value { - crate::mcp::tool_def::("resolve_stale_symbols", "Inspect Knowledge Graph entities and tech debt symbol/line references against files on disk, healing stale pointers.") + crate::mcp::tool_def::( + "resolve_stale_symbols", + "Inspect Knowledge Graph entities and tech debt symbol/line references against files on disk, healing stale pointers.", + ) } async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { - let req: ResolveStaleSymbolsTool = serde_json::from_value(args).map_err(|e| e.to_string())?; + let req: ResolveStaleSymbolsTool = + serde_json::from_value(args).map_err(|e| e.to_string())?; let auto_heal = req.auto_heal.unwrap_or(true); let mut checked = 0; let mut stale_entities = Vec::new(); state.modify_graph(|g| { - for (name, _entity) in g.entities.iter_mut() { - if let Some(target) = &req.target { - if name != target && !name.contains(target) { + for name in g.entities.keys() { + if let Some(target) = &req.target + && name != target && !name.contains(target) { continue; } - } checked += 1; let path = std::path::Path::new(name); - if (name.contains('/') || name.contains('\\') || name.contains('.')) && !path.exists() { + if (name.contains('/') || name.contains('\\') || name.contains('.')) + && !path.exists() + { stale_entities.push(name.clone()); } } @@ -1184,7 +1216,10 @@ impl McpTool for SummarizeSubgraphHandler { } fn schema(&self) -> Value { - crate::mcp::tool_def::("summarize_subgraph", "Generate compact, LLM-optimized Markdown topology of a graph component capped within a token budget.") + crate::mcp::tool_def::( + "summarize_subgraph", + "Generate compact, LLM-optimized Markdown topology of a graph component capped within a token budget.", + ) } async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { @@ -1212,8 +1247,18 @@ impl McpTool for SummarizeSubgraphHandler { } } - let ent: Vec<_> = g.entities.values().filter(|e| visited.contains(&e.name)).cloned().collect(); - let rel: Vec<_> = g.relations.iter().filter(|r| visited.contains(&r.from) && visited.contains(&r.to)).cloned().collect(); + let ent: Vec<_> = g + .entities + .values() + .filter(|e| visited.contains(&e.name)) + .cloned() + .collect(); + let rel: Vec<_> = g + .relations + .iter() + .filter(|r| visited.contains(&r.from) && visited.contains(&r.to)) + .cloned() + .collect(); (ent, rel) }); @@ -1228,7 +1273,10 @@ impl McpTool for SummarizeSubgraphHandler { markdown.push_str("\n## Relations\n"); for r in &sub_relations { - markdown.push_str(&format!("- `{}` --[{}]--> `{}`\n", r.from, r.relation_type, r.to)); + markdown.push_str(&format!( + "- `{}` --[{}]--> `{}`\n", + r.from, r.relation_type, r.to + )); } let max_tokens = req.max_tokens.unwrap_or(1000); @@ -1249,7 +1297,6 @@ mod tests { use crate::handlers::meta::AgentSignalsHandler; use serde_json::json; - #[tokio::test] async fn test_create_and_read_entities() { let state = Arc::new(MemoryState::new_in_memory()); @@ -1324,10 +1371,7 @@ mod tests { {"source": "A", "target": "B", "relationType": "knows"} ] }); - let alias_res = handler - .execute(alias_args, state.clone()) - .await - .unwrap(); + let alias_res = handler.execute(alias_args, state.clone()).await.unwrap(); assert!(alias_res.contains("Successfully created 1 relation(s)")); // Test semantic LLM schema feedback on missing fields @@ -1499,7 +1543,10 @@ mod tests { {"name": "Beta", "entity_type": "Node", "observations": ["Obs3"], "namespace": "global"} ] }); - create_handler.execute(args_ent, state.clone()).await.unwrap(); + create_handler + .execute(args_ent, state.clone()) + .await + .unwrap(); let rel_handler = CreateRelationsHandler; let args_rel = json!({ @@ -1510,48 +1557,86 @@ mod tests { rel_handler.execute(args_rel, state.clone()).await.unwrap(); let path_handler = QueryGraphPathHandler; - let path_res = path_handler.execute(json!({"start_node": "Alpha", "end_node": "Beta"}), state.clone()).await.unwrap(); + let path_res = path_handler + .execute( + json!({"start_node": "Alpha", "end_node": "Beta"}), + state.clone(), + ) + .await + .unwrap(); assert!(path_res.contains("Alpha")); let sub_handler = GetSubgraphHandler; - let sub_res = sub_handler.execute(json!({"root_entity": "Alpha"}), state.clone()).await.unwrap(); + let sub_res = sub_handler + .execute(json!({"root_entity": "Alpha"}), state.clone()) + .await + .unwrap(); assert!(sub_res.contains("Alpha")); let sum_handler = SummarizeSubgraphHandler; - let sum_res = sum_handler.execute(json!({"root_entity": "Alpha"}), state.clone()).await.unwrap(); + let sum_res = sum_handler + .execute(json!({"root_entity": "Alpha"}), state.clone()) + .await + .unwrap(); assert!(!sum_res.is_empty()); - let sweep_handler = SweepGraphHealthHandler; - let sweep_res = sweep_handler.execute(json!({}), state.clone()).await.unwrap(); + let sweep_res = sweep_handler + .execute(json!({}), state.clone()) + .await + .unwrap(); assert!(!sweep_res.is_empty()); let del_obs_handler = DeleteObservationsHandler; - let del_obs_res = del_obs_handler.execute(json!({ - "deletions": [{"entity_name": "Alpha", "observations": ["Obs1"]}] - }), state.clone()).await.unwrap(); + let del_obs_res = del_obs_handler + .execute( + json!({ + "deletions": [{"entity_name": "Alpha", "observations": ["Obs1"]}] + }), + state.clone(), + ) + .await + .unwrap(); assert_eq!(del_obs_res, "Observations deleted"); let del_rel_handler = DeleteRelationsHandler; - let del_rel_res = del_rel_handler.execute(json!({ - "relations": [{"from": "Alpha", "to": "Beta", "relation_type": "links_to"}] - }), state.clone()).await.unwrap(); + let del_rel_res = del_rel_handler + .execute( + json!({ + "relations": [{"from": "Alpha", "to": "Beta", "relation_type": "links_to"}] + }), + state.clone(), + ) + .await + .unwrap(); assert_eq!(del_rel_res, "Relations deleted"); let bcast_handler = AgentSignalsHandler; - let bcast_res = bcast_handler.execute(json!({ - "action": "broadcast", - "sender": "agent1", - "signal_type": "task_completed", - "payload": "fix_bug" - }), state.clone()).await.unwrap(); + let bcast_res = bcast_handler + .execute( + json!({ + "action": "broadcast", + "sender": "agent1", + "signal_type": "task_completed", + "payload": "fix_bug" + }), + state.clone(), + ) + .await + .unwrap(); assert!(bcast_res.contains("Broadcasted signal")); let qsignal_handler = AgentSignalsHandler; - let qsignal_res = qsignal_handler.execute(json!({ - "action": "query", - "sender": "agent1" - }), state.clone()).await.unwrap(); + let qsignal_res = qsignal_handler + .execute( + json!({ + "action": "query", + "sender": "agent1" + }), + state.clone(), + ) + .await + .unwrap(); assert!(qsignal_res.contains("task_completed")); let read_paged_handler = ReadGraphHandler; let paged_res = read_paged_handler @@ -1563,4 +1648,3 @@ mod tests { assert_eq!(parsed_paged["has_more"], true); } } - diff --git a/server/src/handlers/logs.rs b/server/src/handlers/logs.rs index 57a5a14..8bc8db1 100644 --- a/server/src/handlers/logs.rs +++ b/server/src/handlers/logs.rs @@ -38,32 +38,34 @@ impl McpTool for ProcessLogsHandler { } ProcessLogAction::Get => { let max_lines = tool_args.max_lines.unwrap_or(100); - let result = tokio::task::spawn_blocking(move || -> crate::error::Result { - let mut file = File::open(&safe_path).map_err(|e| { - crate::error::AppError::Internal(format!("Failed to open file: {}", e)) - })?; - let len = file.metadata().map_err(|e| e.to_string())?.len(); + let result = + tokio::task::spawn_blocking(move || -> crate::error::Result { + let mut file = File::open(&safe_path).map_err(|e| { + crate::error::AppError::Internal(format!("Failed to open file: {}", e)) + })?; + let len = file.metadata().map_err(|e| e.to_string())?.len(); - let read_size = std::cmp::min(16 * 1024, len); - file.seek(SeekFrom::End(-(read_size as i64))) - .map_err(|e| e.to_string())?; + let read_size = std::cmp::min(16 * 1024, len); + file.seek(SeekFrom::End(-(read_size as i64))) + .map_err(|e| e.to_string())?; - let mut vec_buf = Vec::new(); - file.read_to_end(&mut vec_buf) - .map_err(|e| e.to_string())?; + let mut vec_buf = Vec::new(); + file.read_to_end(&mut vec_buf).map_err(|e| e.to_string())?; - let buffer = String::from_utf8_lossy(&vec_buf).to_string(); - let lines: Vec<&str> = buffer.lines().collect(); - let recent_lines = if lines.len() > max_lines { - lines[lines.len() - max_lines..].join("\n") - } else { - buffer - }; + let buffer = String::from_utf8_lossy(&vec_buf).to_string(); + let lines: Vec<&str> = buffer.lines().collect(); + let recent_lines = if lines.len() > max_lines { + lines[lines.len() - max_lines..].join("\n") + } else { + buffer + }; - Ok(recent_lines) - }) - .await - .map_err(|e| crate::error::AppError::Internal(format!("Task panic: {}", e)))??; + Ok(recent_lines) + }) + .await + .map_err(|e| { + crate::error::AppError::Internal(format!("Task panic: {}", e)) + })??; Ok(result) } diff --git a/server/src/handlers/meta.rs b/server/src/handlers/meta.rs index 443e850..bc8523d 100644 --- a/server/src/handlers/meta.rs +++ b/server/src/handlers/meta.rs @@ -110,11 +110,10 @@ impl McpTool for SearchErrorFixesHandler { } else if st_lower.contains(&sol_lower) || sol_lower.contains(&st_lower) { score += 0.5; } - if let Some(emb) = &f.embedding { - if !query_emb.is_empty() { + if let Some(emb) = &f.embedding + && !query_emb.is_empty() { score += crate::embedding::cosine_similarity(&query_emb, emb); } - } (f, score) }) .filter(|(_, score)| *score > 0.1) @@ -198,11 +197,10 @@ impl McpTool for LogCodeChangeHandler { if let Some(range) = &req.line_range { description = format!("{} [Line Range: {}]", description, range); } - if let Some(symbols) = &req.symbol_references { - if !symbols.is_empty() { + if let Some(symbols) = &req.symbol_references + && !symbols.is_empty() { description = format!("{} [Symbols: {}]", description, symbols.join(", ")); } - } if state.ollama.is_available().await { let prompt = format!( @@ -323,13 +321,14 @@ impl McpTool for QueryRecentChangesHandler { } async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { - let req: QueryRecentChangesTool = serde_json::from_value(args).unwrap_or(QueryRecentChangesTool { - namespace: None, - repo_name: None, - vcs_type: None, - limit: None, - offset: None, - }); + let req: QueryRecentChangesTool = + serde_json::from_value(args).unwrap_or(QueryRecentChangesTool { + namespace: None, + repo_name: None, + vcs_type: None, + limit: None, + offset: None, + }); let limit = req.limit.unwrap_or(50); let offset = req.offset.unwrap_or(0); let data = state.code.ledger.read_with(|l| { @@ -337,25 +336,22 @@ impl McpTool for QueryRecentChangesHandler { .iter() .rev() .filter(|c| { - if let Some(ns) = &req.namespace { - if !c.namespace.eq_ignore_ascii_case(ns) { + if let Some(ns) = &req.namespace + && !c.namespace.eq_ignore_ascii_case(ns) { return false; } - } - if let Some(repo) = &req.repo_name { - if c.repo_name + if let Some(repo) = &req.repo_name + && c.repo_name .as_ref() .map(|rn| !rn.eq_ignore_ascii_case(repo)) .unwrap_or(true) { return false; } - } - if let Some(vcs) = &req.vcs_type { - if !c.effective_vcs().eq_ignore_ascii_case(vcs) { + if let Some(vcs) = &req.vcs_type + && !c.effective_vcs().eq_ignore_ascii_case(vcs) { return false; } - } true }) .skip(offset) @@ -386,7 +382,7 @@ impl McpTool for DecisionsHandler { let req: DecisionsTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let ns = req .namespace - .unwrap_or_else(|| crate::models::default_namespace()); + .unwrap_or_else(crate::models::default_namespace); match req.action { DecisionAction::Log => { let title = req.title.ok_or_else(|| { @@ -464,14 +460,14 @@ impl McpTool for DecisionsHandler { state.code.adrs.modify(|adrs| { let target_pos = adrs.iter().position(|a| a.id.eq_ignore_ascii_case(&id)); if let Some(pos) = target_pos { - if let Some(superseded_id) = &req.supersedes { - if let Some(s_pos) = adrs.iter().position(|a| a.id.eq_ignore_ascii_case(superseded_id)) { - if s_pos != pos { + if let Some(superseded_id) = &req.supersedes + && let Some(s_pos) = adrs + .iter() + .position(|a| a.id.eq_ignore_ascii_case(superseded_id)) + && s_pos != pos { adrs[s_pos].status = "superseded".to_string(); adrs_to_index.push(adrs[s_pos].clone()); } - } - } let a = &mut adrs[pos]; if let Some(t) = req.title { @@ -488,7 +484,9 @@ impl McpTool for DecisionsHandler { } if let Some(s) = req.status { let s_lower = s.to_ascii_lowercase(); - if (s_lower == "implemented" || s_lower == "resolved") && a.resolved_at.is_none() { + if (s_lower == "implemented" || s_lower == "resolved") + && a.resolved_at.is_none() + { a.resolved_at = Some(crate::handlers::utils::now_secs()); } else if s_lower != "implemented" && s_lower != "resolved" { a.resolved_at = None; @@ -535,11 +533,15 @@ impl McpTool for DecisionsHandler { &format!("Updated {}: {}", adr.id, adr.title), Some(&adr.status), ); - Ok(format!("Updated decision {}: {} (status: {})", adr.id, adr.title, adr.status)) - } else { - Err(crate::error::AppError::Internal( - format!("Decision with id '{}' not found", id), + Ok(format!( + "Updated decision {}: {} (status: {})", + adr.id, adr.title, adr.status )) + } else { + Err(crate::error::AppError::Internal(format!( + "Decision with id '{}' not found", + id + ))) } } DecisionAction::Query => { @@ -554,9 +556,17 @@ impl McpTool for DecisionsHandler { } if let Some(q) = &req.query { crate::handlers::utils::contains_ignore_ascii_case(&a.title, q) - || crate::handlers::utils::contains_ignore_ascii_case(&a.context, q) - || crate::handlers::utils::contains_ignore_ascii_case(&a.decision, q) - || crate::handlers::utils::contains_ignore_ascii_case(&a.consequence, q) + || crate::handlers::utils::contains_ignore_ascii_case( + &a.context, q, + ) + || crate::handlers::utils::contains_ignore_ascii_case( + &a.decision, + q, + ) + || crate::handlers::utils::contains_ignore_ascii_case( + &a.consequence, + q, + ) } else { true } @@ -632,17 +642,14 @@ impl McpTool for TechDebtHandler { let req: TechDebtTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let ns = req .namespace - .unwrap_or_else(|| crate::models::default_namespace()); + .unwrap_or_else(crate::models::default_namespace); match req.action { TechDebtAction::Log => { let desc = req.description.or(req.title).ok_or_else(|| { crate::error::AppError::Internal("Missing required parameter 'description' for action 'log'. Next step: Provide tech debt 'description' in request and retry.".to_string()) })?; let ideal = req.ideal_solution.unwrap_or_default(); - let text_to_embed = format!( - "Description: {}\nIdeal Solution: {}", - desc, ideal - ); + let text_to_embed = format!("Description: {}\nIdeal Solution: {}", desc, ideal); let embedding = crate::embedding::generate_embedding_async(text_to_embed) .await .ok(); @@ -665,18 +672,21 @@ impl McpTool for TechDebtHandler { effort_estimate: req.effort_estimate, }); if debts.len() > 300 { - let severity_rank = |sev: Option<&str>| match sev.unwrap_or("").to_lowercase().as_str() { - "critical" => 4, - "high" => 3, - "medium" => 2, - "low" => 1, - _ => 1, - }; - if let Some((idx_to_remove, _)) = debts.iter().enumerate().min_by_key(|(_, d)| { - let status_score = if d.is_resolved { 0 } else { 10 }; - let sev_score = severity_rank(d.severity.as_deref()); - (status_score + sev_score, d.created_at) - }) { + let severity_rank = + |sev: Option<&str>| match sev.unwrap_or("").to_lowercase().as_str() { + "critical" => 4, + "high" => 3, + "medium" => 2, + "low" => 1, + _ => 1, + }; + if let Some((idx_to_remove, _)) = + debts.iter().enumerate().min_by_key(|(_, d)| { + let status_score = if d.is_resolved { 0 } else { 10 }; + let sev_score = severity_rank(d.severity.as_deref()); + (status_score + sev_score, d.created_at) + }) + { debts.remove(idx_to_remove); } } @@ -712,9 +722,7 @@ impl McpTool for TechDebtHandler { let data = state.code.tech_debts.read_with(|debts| { let filtered: Vec<_> = debts .iter() - .filter(|d| { - d.namespace == ns && (inc || !d.is_resolved) - }) + .filter(|d| d.namespace == ns && (inc || !d.is_resolved)) .map(|d| match level { "compact" => serde_json::json!({ "id": d.id, @@ -811,7 +819,8 @@ impl McpTool for OmniSearchHandler { let mut ranked_items: Vec<_> = rrf_scores.into_values().collect(); ranked_items.sort_by(|a, b| b.0.total_cmp(&a.0)); - let matches: Vec = ranked_items.into_iter().map(|(_, item)| item).collect(); + let matches: Vec = + ranked_items.into_iter().map(|(_, item)| item).collect(); let kg_json = state.read_graph(|full| { let mut kg_results = serde_json::Map::new(); @@ -1263,25 +1272,21 @@ impl McpTool for ManageCheckpointHandler { ))); } - if let Ok(graph_content) = std::fs::read_to_string(target_dir.join("graph.json")) { - if let Ok(graph) = serde_json::from_str(&graph_content) { + if let Ok(graph_content) = std::fs::read_to_string(target_dir.join("graph.json")) + && let Ok(graph) = serde_json::from_str(&graph_content) { state.graph.modify(|g| *g = graph); } - } - if let Ok(tasks_content) = std::fs::read_to_string(target_dir.join("tasks.json")) { - if let Ok(tasks) = serde_json::from_str(&tasks_content) { + if let Ok(tasks_content) = std::fs::read_to_string(target_dir.join("tasks.json")) + && let Ok(tasks) = serde_json::from_str(&tasks_content) { state.project.tasks.modify(|t| *t = tasks); } - } if let Ok(debts_content) = std::fs::read_to_string(target_dir.join("tech_debts.json")) - { - if let Ok(debts) = serde_json::from_str(&debts_content) { + && let Ok(debts) = serde_json::from_str(&debts_content) { state.code.tech_debts.modify(|d| *d = debts); } - } Ok(format!("Checkpoint '{}' restored successfully.", name)) } @@ -1290,11 +1295,10 @@ impl McpTool for ManageCheckpointHandler { let checkpoints_dir = state.base_dir.join("checkpoints"); if let Ok(entries) = std::fs::read_dir(&checkpoints_dir) { for entry in entries.flatten() { - if entry.path().is_dir() { - if let Some(n) = entry.file_name().to_str() { + if entry.path().is_dir() + && let Some(n) = entry.file_name().to_str() { list.push(serde_json::json!({"type": "checkpoint", "name": n})); } - } } } let snaps = state.project.snapshots.read_with(|snaps| snaps.clone()); @@ -1461,13 +1465,11 @@ impl McpTool for GetNextActionableTasksHandler { continue; } - if let Some(branch) = &req.git_branch { - if let Some(tb) = &task.git_branch { - if tb != branch { + if let Some(branch) = &req.git_branch + && let Some(tb) = &task.git_branch + && tb != branch { continue; } - } - } let unblocked = task.dependencies.is_empty() || task.dependencies.iter().all(|d| completed_ids.contains(d)); @@ -1509,7 +1511,9 @@ impl McpTool for HypothesesHandler { match req.action { HypothesisAction::Log => { let hyp_text = req.hypothesis.ok_or_else(|| { - crate::error::AppError::Internal("Missing required 'hypothesis' for action 'log'".to_string()) + crate::error::AppError::Internal( + "Missing required 'hypothesis' for action 'log'".to_string(), + ) })?; let hyp_id = format!( "HYP-{}", @@ -1536,17 +1540,16 @@ impl McpTool for HypothesesHandler { let filtered: Vec<_> = hypotheses .into_iter() .filter(|h| { - if let Some(tid) = &req.task_id { - if h.task_id.as_ref() != Some(tid) { + if let Some(tid) = &req.task_id + && h.task_id.as_ref() != Some(tid) { return false; } - } if let Some(q) = &req.query { let lq = q.to_lowercase(); return h.hypothesis.to_lowercase().contains(&lq) || h.evidence .as_ref() - .map_or(false, |e| e.to_lowercase().contains(&lq)); + .is_some_and(|e| e.to_lowercase().contains(&lq)); } true }) @@ -1656,13 +1659,19 @@ impl McpTool for AgentSignalsHandler { match req.action { AgentSignalAction::Broadcast => { let sender = req.sender.ok_or_else(|| { - crate::error::AppError::Internal("Missing required 'sender' for action 'broadcast'".to_string()) + crate::error::AppError::Internal( + "Missing required 'sender' for action 'broadcast'".to_string(), + ) })?; let signal_type = req.signal_type.ok_or_else(|| { - crate::error::AppError::Internal("Missing required 'signal_type' for action 'broadcast'".to_string()) + crate::error::AppError::Internal( + "Missing required 'signal_type' for action 'broadcast'".to_string(), + ) })?; let payload = req.payload.ok_or_else(|| { - crate::error::AppError::Internal("Missing required 'payload' for action 'broadcast'".to_string()) + crate::error::AppError::Internal( + "Missing required 'payload' for action 'broadcast'".to_string(), + ) })?; let timestamp = std::time::SystemTime::now() @@ -1715,25 +1724,21 @@ impl McpTool for AgentSignalsHandler { signals .iter() .filter(|s| { - if let Some(ttl) = s.ttl_seconds { - if now > s.timestamp + ttl { + if let Some(ttl) = s.ttl_seconds + && now > s.timestamp + ttl { return false; } - } - if let Some(sender) = &req.sender { - if s.sender.to_lowercase() != sender.to_lowercase() { + if let Some(sender) = &req.sender + && s.sender.to_lowercase() != sender.to_lowercase() { return false; } - } - if let Some(st) = &req.signal_type { - if s.signal_type.to_lowercase() != st.to_lowercase() { + if let Some(st) = &req.signal_type + && s.signal_type.to_lowercase() != st.to_lowercase() { return false; } - } true }) - .cloned() - .take(req.limit.unwrap_or(20)) + .take(req.limit.unwrap_or(20)).cloned() .collect::>() }); @@ -2110,7 +2115,10 @@ mod tests { assert!(q_dec_res.contains("Fast async API routing")); let q_by_consequence = handler_dec - .execute(serde_json::json!({"action": "query", "query": "Fast async"}), state.clone()) + .execute( + serde_json::json!({"action": "query", "query": "Fast async"}), + state.clone(), + ) .await .unwrap(); assert!(q_by_consequence.contains("Use Axum")); @@ -2133,7 +2141,10 @@ mod tests { assert!(update_res.contains("implemented")); let q_after_update = handler_dec - .execute(serde_json::json!({"action": "query", "include_body": false}), state.clone()) + .execute( + serde_json::json!({"action": "query", "include_body": false}), + state.clone(), + ) .await .unwrap(); assert!(q_after_update.contains("implemented")); @@ -2143,7 +2154,10 @@ mod tests { assert!(q_after_update.contains("resolved_at")); let del_dec_res = handler_dec - .execute(serde_json::json!({"action": "delete", "id": "ADR-0001"}), state.clone()) + .execute( + serde_json::json!({"action": "delete", "id": "ADR-0001"}), + state.clone(), + ) .await; assert!(del_dec_res.is_ok()); @@ -2163,14 +2177,20 @@ mod tests { assert!(td_res.contains("Tech debt logged")); let list_td_res = handler_td - .execute(serde_json::json!({"action": "list", "include_resolved": true}), state.clone()) + .execute( + serde_json::json!({"action": "list", "include_resolved": true}), + state.clone(), + ) .await .unwrap(); assert!(list_td_res.contains("Replace unwraps")); let debt_id = state.code.tech_debts.read_with(|debts| debts[0].id.clone()); let res_td_res = handler_td - .execute(serde_json::json!({"action": "resolve", "id": debt_id}), state.clone()) + .execute( + serde_json::json!({"action": "resolve", "id": debt_id}), + state.clone(), + ) .await; assert!(res_td_res.is_ok()); @@ -2394,7 +2414,10 @@ mod tests { ) .await .unwrap(); - assert!(rest_state_res.contains("restored successfully") || rest_state_res.contains("restored memory state from snapshot")); + assert!( + rest_state_res.contains("restored successfully") + || rest_state_res.contains("restored memory state from snapshot") + ); let mg_list = mg_chk .execute(serde_json::json!({"action": "list"}), state.clone()) @@ -2607,7 +2630,10 @@ mod tests { // DeleteDecision non-existent let del_dec_err = handler_dec - .execute(serde_json::json!({"action": "delete", "id": "ADR-9999"}), state.clone()) + .execute( + serde_json::json!({"action": "delete", "id": "ADR-9999"}), + state.clone(), + ) .await; assert!(del_dec_err.is_err()); @@ -2882,9 +2908,14 @@ mod tests { state.code.tech_debts.read_with(|debts| { assert_eq!(debts.len(), 300); - assert!(debts.iter().any(|d| d.id == "critical-debt"), "Critical unresolved debt must be retained"); - assert!(!debts.iter().any(|d| d.id == "resolved-debt"), "Resolved debt should have been evicted first"); + assert!( + debts.iter().any(|d| d.id == "critical-debt"), + "Critical unresolved debt must be retained" + ); + assert!( + !debts.iter().any(|d| d.id == "resolved-debt"), + "Resolved debt should have been evicted first" + ); }); } } - diff --git a/server/src/handlers/mod.rs b/server/src/handlers/mod.rs index 5efcd5b..b8dac4a 100644 --- a/server/src/handlers/mod.rs +++ b/server/src/handlers/mod.rs @@ -10,4 +10,3 @@ pub mod tasks; pub mod utils; pub mod vision; pub mod workspaces; - diff --git a/server/src/handlers/notes.rs b/server/src/handlers/notes.rs index 1c89eef..fb5c7cd 100644 --- a/server/src/handlers/notes.rs +++ b/server/src/handlers/notes.rs @@ -29,7 +29,9 @@ impl McpTool for HandoffMemosHandler { let content = req.content.ok_or_else(|| { crate::error::AppError::Internal("Missing required parameter 'content' for action 'leave'. Next step: Provide non-empty 'content' string in request and retry.".to_string()) })?; - let ns = req.namespace.unwrap_or_else(|| crate::models::default_namespace()); + let ns = req + .namespace + .unwrap_or_else(crate::models::default_namespace); state.telemetry.handoff_memos.modify(|memos| { memos.push(crate::models::HandoffMemo { id: uuid::Uuid::new_v4().to_string(), @@ -109,11 +111,13 @@ impl McpTool for AddSessionSummaryHandler { }); if summaries.len() > 200 { let now = crate::handlers::utils::now_secs(); - if let Some((idx_to_remove, _)) = summaries.iter().enumerate().min_by_key(|(_, s)| { - let is_expired = s.expires_at.map(|exp| exp < now).unwrap_or(false); - let status_score = if is_expired { 0 } else { 1 }; - (status_score, s.timestamp) - }) { + if let Some((idx_to_remove, _)) = + summaries.iter().enumerate().min_by_key(|(_, s)| { + let is_expired = s.expires_at.map(|exp| exp < now).unwrap_or(false); + let status_score = if is_expired { 0 } else { 1 }; + (status_score, s.timestamp) + }) + { summaries.remove(idx_to_remove); } } @@ -143,13 +147,25 @@ impl McpTool for GenerateStandupReportHandler { let cutoff = crate::handlers::utils::now_secs().saturating_sub(req.hours_lookback * 3600); let filtered_tasks = state.project.tasks.read_with(|items| { - items.iter().filter(|t| t.updated_at >= cutoff).cloned().collect::>() + items + .iter() + .filter(|t| t.updated_at >= cutoff) + .cloned() + .collect::>() }); let filtered_changes = state.code.ledger.read_with(|changes| { - changes.iter().filter(|c| c.timestamp >= cutoff).cloned().collect::>() + changes + .iter() + .filter(|c| c.timestamp >= cutoff) + .cloned() + .collect::>() }); let filtered_summaries = state.telemetry.session_summaries.read_with(|summaries| { - summaries.iter().filter(|s| s.namespace == req.namespace && s.timestamp >= cutoff).cloned().collect::>() + summaries + .iter() + .filter(|s| s.namespace == req.namespace && s.timestamp >= cutoff) + .cloned() + .collect::>() }); let report = serde_json::json!({ @@ -178,8 +194,7 @@ impl McpTool for PromoteToEntityHandler { } async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { - let req: PromoteToEntityTool = - serde_json::from_value(args).map_err(|e| e.to_string())?; + let req: PromoteToEntityTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let entity_name = req.entity_name.clone(); let obs = req.content.clone(); @@ -236,7 +251,10 @@ mod tests { assert_eq!(res, "Handoff memo left."); let res2 = handoff_handler - .execute(json!({"action": "read", "namespace": "global"}), state.clone()) + .execute( + json!({"action": "read", "namespace": "global"}), + state.clone(), + ) .await .map_err(|e| crate::error::AppError::Internal(e.to_string())) .unwrap(); @@ -266,5 +284,3 @@ mod tests { assert!(!res4.is_empty()); } } - - diff --git a/server/src/handlers/reconciliation.rs b/server/src/handlers/reconciliation.rs index 9dcb117..625776d 100644 --- a/server/src/handlers/reconciliation.rs +++ b/server/src/handlers/reconciliation.rs @@ -39,7 +39,9 @@ pub fn extract_adrs(text: &str) -> Vec { if i + 3 <= len && lower_chars[i..i + 3] == ['a', 'd', 'r'] { let mut cursor = i + 3; // Skip optional '-', ':', or whitespace - while cursor < len && (chars[cursor] == '-' || chars[cursor] == ':' || chars[cursor] == ' ') { + while cursor < len + && (chars[cursor] == '-' || chars[cursor] == ':' || chars[cursor] == ' ') + { cursor += 1; } // Collect consecutive digits @@ -80,11 +82,17 @@ pub fn extract_tech_debts(text: &str) -> Vec { while i < len { if i + 2 <= len && lower_chars[i..i + 2] == ['t', 'd'] { let mut cursor = i + 2; - while cursor < len && (chars[cursor] == '-' || chars[cursor] == ':' || chars[cursor] == ' ') { + while cursor < len + && (chars[cursor] == '-' || chars[cursor] == ':' || chars[cursor] == ' ') + { cursor += 1; } let start = cursor; - while cursor < len && (chars[cursor].is_ascii_alphanumeric() || chars[cursor] == '_' || chars[cursor] == '-') { + while cursor < len + && (chars[cursor].is_ascii_alphanumeric() + || chars[cursor] == '_' + || chars[cursor] == '-') + { cursor += 1; } if cursor > start { @@ -117,11 +125,17 @@ pub fn extract_tasks(text: &str) -> Vec { while i < len { if i + 4 <= len && lower_chars[i..i + 4] == ['t', 'a', 's', 'k'] { let mut cursor = i + 4; - while cursor < len && (chars[cursor] == '-' || chars[cursor] == ':' || chars[cursor] == ' ') { + while cursor < len + && (chars[cursor] == '-' || chars[cursor] == ':' || chars[cursor] == ' ') + { cursor += 1; } let start = cursor; - while cursor < len && (chars[cursor].is_ascii_alphanumeric() || chars[cursor] == '_' || chars[cursor] == '-') { + while cursor < len + && (chars[cursor].is_ascii_alphanumeric() + || chars[cursor] == '_' + || chars[cursor] == '-') + { cursor += 1; } if cursor > start { @@ -162,8 +176,7 @@ pub fn cascade_unblock_tasks(state: &MemoryState) -> Vec { } else { let all_deps_completed = t.dependencies.iter().all(|dep_id| { tasks_snapshot.iter().any(|other| { - (other.id == *dep_id || other.title == *dep_id) - && !other.is_active() + (other.id == *dep_id || other.title == *dep_id) && !other.is_active() }) }); if all_deps_completed { @@ -226,19 +239,17 @@ pub fn recalculate_milestones(state: &MemoryState) -> Vec { } updated.push(m.id.clone()); } - } else if completed > 0 + } else if (completed > 0 || tasks_snapshot.iter().any(|t| { m.task_ids.iter().any(|tid| tid == &t.id || tid == &t.title) && (t.status == "in_progress" || t.status == "active") - }) - { - if !m.status.eq_ignore_ascii_case("in_progress") + })) + && !m.status.eq_ignore_ascii_case("in_progress") && !m.status.eq_ignore_ascii_case("completed") { m.status = "in_progress".to_string(); updated.push(m.id.clone()); } - } } }); @@ -275,11 +286,10 @@ pub async fn reconcile_commit_or_code_change( if adr.id.eq_ignore_ascii_case(cand) { return true; } - if let (Some(n1), Some(n2)) = (adr_numeric_id(&adr.id), adr_numeric_id(cand)) { - if n1 == n2 { + if let (Some(n1), Some(n2)) = (adr_numeric_id(&adr.id), adr_numeric_id(cand)) + && n1 == n2 { return true; } - } false }); @@ -324,7 +334,9 @@ pub async fn reconcile_commit_or_code_change( if d.is_resolved { continue; } - let matches_id = td_candidates.iter().any(|cand| d.id.eq_ignore_ascii_case(cand)); + let matches_id = td_candidates + .iter() + .any(|cand| d.id.eq_ignore_ascii_case(cand)); let matches_file = file_path .map(|fp| d.file_path.as_deref() == Some(fp)) .unwrap_or(false) diff --git a/server/src/handlers/tasks.rs b/server/src/handlers/tasks.rs index 49ab6cb..478173c 100644 --- a/server/src/handlers/tasks.rs +++ b/server/src/handlers/tasks.rs @@ -366,14 +366,14 @@ impl McpTool for TasksHandler { let mut updated_task: Option = None; state.project.tasks.modify(|tasks| { if let Some(task) = tasks.iter_mut().find(|t| t.id == id || t.title == id) { - if let Some(ac) = task.acceptance_criteria.iter_mut().next() { + if let Some(ac) = task.acceptance_criteria.first_mut() { if ac.is_met { already_met = true; } else { ac.is_met = true; success = true; - task.updated_at = crate::handlers::utils::now_secs(); - updated_task = Some(task.clone()); + task.updated_at = crate::handlers::utils::now_secs(); + updated_task = Some(task.clone()); } } else { task.acceptance_criteria @@ -444,7 +444,7 @@ impl McpTool for MilestonesHandler { })?; let ns = req .namespace - .unwrap_or_else(|| crate::models::default_namespace()); + .unwrap_or_else(crate::models::default_namespace); state.project.milestones.modify(|ms| { ms.push(crate::models::Milestone { id: uuid::Uuid::new_v4().to_string(), @@ -477,16 +477,16 @@ impl McpTool for MilestonesHandler { for m in ms.iter_mut() { if m.id == id { m.status = status.clone(); - if status.eq_ignore_ascii_case("completed") || status.eq_ignore_ascii_case("done") { - if m.completed_at.is_none() { + if (status.eq_ignore_ascii_case("completed") + || status.eq_ignore_ascii_case("done")) + && m.completed_at.is_none() { m.completed_at = Some(crate::handlers::utils::now_secs()); } - } if let Some(task_ids) = &req.task_ids { m.task_ids = task_ids.clone(); - if let Some(deliverables) = &req.deliverables { - m.deliverables = deliverables.clone(); - } + if let Some(deliverables) = &req.deliverables { + m.deliverables = deliverables.clone(); + } } found = true; break; diff --git a/server/src/handlers/vision.rs b/server/src/handlers/vision.rs index 91565d6..206b265 100644 --- a/server/src/handlers/vision.rs +++ b/server/src/handlers/vision.rs @@ -191,11 +191,10 @@ pub fn html_to_markdown(html: &str) -> String { out.push_str("* "); } } - "/li" => { - if !in_cell { + "/li" + if !in_cell => { out.push('\n'); } - } _ => {} } } else if c == '&' { @@ -298,38 +297,46 @@ pub async fn ensure_ocr_engine() -> Option<&'static OcrEngine> { if let Some(engine) = OCR_ENGINE.get() { return Some(engine); } - + let cache_dir = dirs::home_dir()?.join(".gemini/mcp_memory/models"); let _ = std::fs::create_dir_all(&cache_dir); - + let det_path = cache_dir.join("text-detection.rten"); if !det_path.exists() || std::fs::metadata(&det_path).map(|m| m.len()).unwrap_or(0) < 100_000 { - match reqwest::get("https://ocrs-models.s3-accelerate.amazonaws.com/text-detection.rten").await { + match reqwest::get("https://ocrs-models.s3-accelerate.amazonaws.com/text-detection.rten") + .await + { Ok(resp) => { if let Ok(bytes) = resp.bytes().await { if let Err(e) = std::fs::write(&det_path, &bytes) { tracing::error!("Failed to write text-detection.rten to disk: {}", e); } } else { - tracing::error!("Failed to read bytes from text-detection.rten download response"); + tracing::error!( + "Failed to read bytes from text-detection.rten download response" + ); } - }, + } Err(e) => tracing::error!("Failed to download text-detection.rten: {}", e), } } - + let rec_path = cache_dir.join("text-recognition.rten"); if !rec_path.exists() || std::fs::metadata(&rec_path).map(|m| m.len()).unwrap_or(0) < 100_000 { - match reqwest::get("https://ocrs-models.s3-accelerate.amazonaws.com/text-recognition.rten").await { + match reqwest::get("https://ocrs-models.s3-accelerate.amazonaws.com/text-recognition.rten") + .await + { Ok(resp) => { if let Ok(bytes) = resp.bytes().await { if let Err(e) = std::fs::write(&rec_path, &bytes) { tracing::error!("Failed to write text-recognition.rten to disk: {}", e); } } else { - tracing::error!("Failed to read bytes from text-recognition.rten download response"); + tracing::error!( + "Failed to read bytes from text-recognition.rten download response" + ); } - }, + } Err(e) => tracing::error!("Failed to download text-recognition.rten: {}", e), } } @@ -350,10 +357,7 @@ pub async fn ensure_ocr_engine() -> Option<&'static OcrEngine> { pub fn perform_ocrs_ocr(engine: &OcrEngine, img: &image::DynamicImage) -> Option { let rgb_img = img.to_rgb8(); - let img_source = ocrs::ImageSource::from_bytes( - rgb_img.as_raw(), - rgb_img.dimensions(), - ).ok()?; + let img_source = ocrs::ImageSource::from_bytes(rgb_img.as_raw(), rgb_img.dimensions()).ok()?; let ocr_input = engine.prepare_input(img_source).ok()?; let word_rects = engine.detect_words(&ocr_input).ok()?; @@ -365,7 +369,7 @@ pub fn perform_ocrs_ocr(engine: &OcrEngine, img: &image::DynamicImage) -> Option out.push_str(&text.to_string()); out.push('\n'); } - + let trimmed = out.trim(); if trimmed.is_empty() { None @@ -482,23 +486,22 @@ pub async fn process_clipboard_snapshot(state: Arc) { if let Some(engine) = ensure_ocr_engine().await { let img_clone = img.clone(); - ocr_text_opt = tokio::task::spawn_blocking(move || { - perform_ocrs_ocr(engine, &img_clone) - }) - .await - .ok() - .flatten(); + ocr_text_opt = + tokio::task::spawn_blocking(move || perform_ocrs_ocr(engine, &img_clone)) + .await + .ok() + .flatten(); } let mut cache = state.clipboard_cache.write().await; - + // Check if this is an identical image captured rapidly let is_duplicate = if let Some(ref prev) = cache.last_image { prev.width == w && prev.height == h && prev.size_bytes == size_bytes } else { false }; - + if !is_duplicate { // Index OCR text into Tantivy for omni_search if let Some(ref text) = ocr_text_opt { @@ -563,11 +566,10 @@ pub async fn process_clipboard_snapshot(state: Arc) { }; let mut cache = state.clipboard_cache.write().await; - if let Some(ref prev) = cache.last_text { - if prev.text == cached.text { + if let Some(ref prev) = cache.last_text + && prev.text == cached.text { return; } - } cache.last_text = Some(cached.clone()); cache .history @@ -669,7 +671,7 @@ pub fn spawn_clipboard_listener(state: Arc) { tokio::spawn(async move { while let Some(()) = rx.recv().await { tokio::time::sleep(tokio::time::Duration::from_millis(150)).await; - while let Ok(_) = rx.try_recv() {} + while rx.try_recv().is_ok() {} process_clipboard_snapshot(Arc::clone(&state)).await; } @@ -843,8 +845,8 @@ impl McpTool for ClipboardHandler { ClipboardAction::Read => { let engine = ensure_ocr_engine().await; - let out = - tokio::task::spawn_blocking(move || -> crate::error::Result> { + let out = tokio::task::spawn_blocking( + move || -> crate::error::Result> { let mut out = serde_json::Map::new(); if let Some(text) = get_native_clipboard_text() { @@ -860,8 +862,19 @@ impl McpTool for ClipboardHandler { let rgb_img = img.into_rgb8(); let mut jpeg_bytes = std::io::Cursor::new(Vec::new()); - let mut encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut jpeg_bytes, 88); - if encoder.encode(&rgb_img, rgb_img.width(), rgb_img.height(), image::ExtendedColorType::Rgb8).is_ok() { + let mut encoder = image::codecs::jpeg::JpegEncoder::new_with_quality( + &mut jpeg_bytes, + 88, + ); + if encoder + .encode( + &rgb_img, + rgb_img.width(), + rgb_img.height(), + image::ExtendedColorType::Rgb8, + ) + .is_ok() + { let bytes = jpeg_bytes.into_inner(); let cache_dir = dirs::home_dir() @@ -876,17 +889,20 @@ impl McpTool for ClipboardHandler { out.insert("image_path_wsl".into(), json!(wsl_path)); } } - - if let Some(eng) = engine { - if let Some(ocr_text) = perform_ocrs_ocr(eng, &dynamic_img) { - out.insert("image_analysis".to_string(), json!(ocr_text.trim())); + + if let Some(eng) = engine + && let Some(ocr_text) = perform_ocrs_ocr(eng, &dynamic_img) { + out.insert( + "image_analysis".to_string(), + json!(ocr_text.trim()), + ); } - } } Ok(out) - }) - .await - .map_err(|e| crate::error::AppError::Internal(format!("Task panic: {}", e)))??; + }, + ) + .await + .map_err(|e| crate::error::AppError::Internal(format!("Task panic: {}", e)))??; state.record_activity("clipboard", "Read contents from OS clipboard", None); Ok::(serde_json::to_string_pretty(&Value::Object( diff --git a/server/src/handlers/workspaces.rs b/server/src/handlers/workspaces.rs index 6872b5b..0ca803c 100644 --- a/server/src/handlers/workspaces.rs +++ b/server/src/handlers/workspaces.rs @@ -6,14 +6,6 @@ use async_trait::async_trait; use serde_json::Value; use std::sync::Arc; - - - - - - - - pub struct SnippetsHandler; #[async_trait] @@ -57,7 +49,7 @@ impl McpTool for SnippetsHandler { embedding, namespace: req .namespace - .unwrap_or_else(|| crate::models::default_namespace()), + .unwrap_or_else(crate::models::default_namespace), repo_name: req.repo_name, origin_file: req.origin_file, line_range: req.line_range, @@ -89,8 +81,9 @@ impl McpTool for SnippetsHandler { let mut scored: Vec<_> = snippets .into_iter() .filter_map(|s| { - let text = format!("{} {} {} {}", s.name, s.description, s.language, s.code) - .to_lowercase(); + let text = + format!("{} {} {} {}", s.name, s.description, s.language, s.code) + .to_lowercase(); let mut score = 0.0; for tok in &q_tokens { if text.contains(tok) { @@ -143,9 +136,9 @@ impl McpTool for SnippetsHandler { || s.tags.iter().any(|t| contains_ignore_ascii_case(t, &query)); let matches_tags = tags_filter.is_empty() - || tags_filter - .iter() - .any(|tf| s.tags.iter().any(|st| contains_ignore_ascii_case(st, tf))); + || tags_filter.iter().any(|tf| { + s.tags.iter().any(|st| contains_ignore_ascii_case(st, tf)) + }); matches_text && matches_tags }) @@ -352,13 +345,28 @@ impl McpTool for SemanticCodeSearchHandler { } let mut out = format!("Semantic Search Results for '{}':\n", tool_args.query); - for res in results { + for res in &results { out.push_str(&format!( "- [{:.2}] {}: {}\n", res.score, res.title, res.body )); } + if state.ollama.is_available().await { + let prompt = format!( + "You are an expert software architect. Synthesize a concise GraphRAG summary of the following graph nodes relevant to the query: '{}'.\n\nData:\n{}", + tool_args.query, out + ); + if let Ok(summary) = state + .ollama + .generate(&prompt, None, Some("Respond clearly and concisely.")) + .await + { + out.push_str("\n\n--- Local GraphRAG Summary ---\n"); + out.push_str(&summary); + } + } + Ok(out) } } diff --git a/server/src/lib.rs b/server/src/lib.rs index 88f4cad..b0b7188 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -4,6 +4,7 @@ )] pub mod api; +pub mod config; pub mod db; pub mod embedding; pub mod error; @@ -105,11 +106,10 @@ pub async fn ttl_sweeper_worker(state: Arc) { state.project.tasks.read_with(|tasks| { for t in tasks.iter() { - if let Some(exp) = t.expires_at { - if t.is_active() { + if let Some(exp) = t.expires_at + && t.is_active() { next_expiry = Some(next_expiry.map_or(exp, |curr| curr.min(exp))); } - } } }); @@ -160,13 +160,12 @@ pub async fn ttl_sweeper_worker(state: Arc) { let mut expired_tasks = Vec::new(); state.project.tasks.modify(|tasks| { for t in tasks.iter_mut() { - if let Some(exp) = t.expires_at { - if exp <= now && t.is_active() { + if let Some(exp) = t.expires_at + && exp <= now && t.is_active() { t.status = "expired".to_string(); t.updated_at = now; expired_tasks.push(t.id.clone()); } - } } }); @@ -249,8 +248,8 @@ pub async fn condense_graph_worker(state: Arc) { } }); - if let Some((content, names)) = snippet_condensation { - if !content.is_empty() { + if let Some((content, names)) = snippet_condensation + && !content.is_empty() { let name = format!("Snippet History {}", now); state.modify_graph(|graph| { graph.entities.insert( @@ -271,6 +270,84 @@ pub async fn condense_graph_worker(state: Arc) { }); tracing::info!("Condensed snippets into Historical Summary."); } + } +} + +pub async fn memory_consolidation_worker(state: Arc) { + let mut interval = tokio::time::interval(std::time::Duration::from_secs(300)); + loop { + tokio::select! { + _ = state.shutdown_notify.notified() => break, + _ = interval.tick() => {}, + } + + let entities: Vec<_> = state.graph.read_with(|g| { + g.entities + .values() + .map(|e| (e.name.clone(), e.entity_type.clone())) + .collect() + }); + + if entities.len() < 2 { + continue; + } + + let mut entity_summaries = String::new(); + for (name, e_type) in entities.iter().take(50) { + entity_summaries.push_str(&format!("- [{}] {}\n", e_type, name)); + } + + let prompt = format!( + "Analyze the following list of entities and identify exactly TWO that represent the exact same concept or item but have slightly different names (e.g. 'auth_service' and 'AuthService'). Return ONLY a valid JSON array containing exactly two strings: the two names to merge. If no obvious duplicates exist, return an empty array []. Do not output any markdown formatting or extra text.\n\nEntities:\n{}", + entity_summaries + ); + + if let Ok(response) = state + .ollama + .generate( + &prompt, + None, + Some("You are a helpful JSON-only data deduplication assistant. Output only JSON."), + ) + .await + { + let cleaned = response + .trim() + .trim_start_matches("```json") + .trim_start_matches("```") + .trim_end_matches("```") + .trim(); + if let Ok(duplicates) = serde_json::from_str::>(cleaned) + && duplicates.len() == 2 { + let e1_name = &duplicates[0]; + let e2_name = &duplicates[1]; + + if e1_name != e2_name { + tracing::info!( + "Memory Consolidation Daemon: Merging '{}' into '{}'", + e2_name, + e1_name + ); + state.modify_graph(|g| { + if let Some(mut e2) = g.entities.remove(e2_name) { + if let Some(e1) = g.entities.get_mut(e1_name) { + e1.observations.append(&mut e2.observations); + } else { + g.entities.insert(e2_name.clone(), e2); + } + } + + for rel in g.relations.iter_mut() { + if rel.from == *e2_name { + rel.from = e1_name.clone(); + } + if rel.to == *e2_name { + rel.to = e1_name.clone(); + } + } + }); + } + } } } } @@ -287,6 +364,7 @@ pub async fn run_server(state: Arc) -> Result<(), Box) -> Result<(), Box = HashMap::new(); let mut buf = vec![0u8; 65536]; let mut event_rx = udp_state.handler.state.event_bus_tx.subscribe(); @@ -371,32 +449,29 @@ pub async fn run_server(state: Arc) -> Result<(), Box(&buf[..len]) { if json_payload.get("type").and_then(|t| t.as_str()) == Some("ping") { let _ = socket.send_to(b"pong", addr).await; - } else if json_payload.get("type").and_then(|t| t.as_str()) == Some("gate_wait") { - if let (Some(action), Some(target)) = ( + } else if json_payload.get("type").and_then(|t| t.as_str()) == Some("gate_wait") + && let (Some(action), Some(target)) = ( json_payload.get("action").and_then(|a| a.as_str()), json_payload.get("target").and_then(|t| t.as_str()) ) { subscribers.insert((action.to_string(), target.to_string()), addr); } - } } } } Ok(event) = event_rx.recv() => { - if event.topic == "gate:event" { - if let (Some(action), Some(target), Some(status)) = ( + if event.topic == "gate:event" + && let (Some(action), Some(target), Some(status)) = ( event.payload.get("action").and_then(|a| a.as_str()), event.payload.get("target").and_then(|t| t.as_str()), event.payload.get("status").and_then(|s| s.as_str()), ) { - if status == "authorized" || status == "blocked" { - if let Some(addr) = subscribers.remove(&(action.to_string(), target.to_string())) { + if (status == "authorized" || status == "blocked") + && let Some(addr) = subscribers.remove(&(action.to_string(), target.to_string())) { let response = if status == "authorized" { b"APPROVED" } else { b"REJECTED" }; let _ = socket.send_to(response, addr).await; } - } } - } } } } @@ -476,34 +551,42 @@ pub async fn run_server(state: Arc) -> Result<(), Box Option Result<(), Box> { + crate::config::load_mcp_config_env(); let _guard = init_logging("mcp-memory-server"); let cli = Cli::parse(); diff --git a/server/src/models.rs b/server/src/models.rs index 8f0def5..9de55d6 100644 --- a/server/src/models.rs +++ b/server/src/models.rs @@ -4,18 +4,15 @@ use std::collections::HashMap; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "lowercase")] +#[derive(Default)] pub enum ChangeKind { Added, + #[default] Modified, Deleted, Renamed, } -impl Default for ChangeKind { - fn default() -> Self { - ChangeKind::Modified - } -} #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct CodeChange { @@ -57,19 +54,17 @@ impl CodeChange { if let Some(vcs) = &self.vcs_type { return vcs.as_str(); } - if let Some(rev) = self.revision.as_deref().or(self.git_commit.as_deref()) { - if rev.starts_with('r') && rev[1..].chars().all(|c| c.is_ascii_digit()) { + if let Some(rev) = self.revision.as_deref().or(self.git_commit.as_deref()) + && rev.starts_with('r') && rev[1..].chars().all(|c| c.is_ascii_digit()) { return "svn"; } - } - if let Some(br) = self.branch.as_deref().or(self.git_branch.as_deref()) { - if br.eq_ignore_ascii_case("trunk") + if let Some(br) = self.branch.as_deref().or(self.git_branch.as_deref()) + && (br.eq_ignore_ascii_case("trunk") || br.starts_with("branches/") - || br.starts_with("tags/") + || br.starts_with("tags/")) { return "svn"; } - } "git" } @@ -266,11 +261,10 @@ impl KnowledgeGraph { let mut adj: HashMap<&'a str, Vec<(&'a str, &'a str, bool)>> = HashMap::with_capacity(self.relations.len() * 2); for rel in &self.relations { - if let Some(rf) = relation_filter { - if rel.relation_type != rf { + if let Some(rf) = relation_filter + && rel.relation_type != rf { continue; } - } adj.entry(rel.from.as_str()).or_default().push(( rel.to.as_str(), rel.relation_type.as_str(), @@ -398,7 +392,6 @@ fn default_adr_status() -> String { "accepted".to_string() } - #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct ErrorFix { pub signature: String, @@ -420,7 +413,6 @@ pub struct ErrorFix { pub toolchain: Option, } - #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct SessionSummary { pub timestamp: u64, @@ -527,7 +519,6 @@ pub struct EnvironmentDetail { pub ssh_host: Option, } - #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct TechDebt { pub id: String, @@ -554,7 +545,6 @@ pub struct TechDebt { pub effort_estimate: Option, } - #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct GateRecord { pub id: String, diff --git a/server/src/ollama.rs b/server/src/ollama.rs index 641e943..359308b 100644 --- a/server/src/ollama.rs +++ b/server/src/ollama.rs @@ -263,4 +263,3 @@ async fn test_ollama_client_invalid_api_key() { client.base_url = "http://invalid-api-key:11434".to_string(); assert!(!client.is_available().await); } - diff --git a/server/src/router.rs b/server/src/router.rs index 4b00c62..9e1f8e7 100644 --- a/server/src/router.rs +++ b/server/src/router.rs @@ -639,12 +639,19 @@ impl MemoryHandler { if let Some(resource) = self.resources.get(uri) { match resource.read(Arc::clone(&self.state)).await { Ok(text) => { + let mut content_obj = serde_json::json!({ + "uri": uri, + "mimeType": resource.mime_type().unwrap_or("application/json"), + "text": &text + }); + + if text.len() > 4000 { + content_obj["cacheControl"] = + serde_json::json!({"type": "ephemeral"}); + } + let payload = serde_json::json!({ - "contents": [{ - "uri": uri, - "mimeType": resource.mime_type().unwrap_or("application/json"), - "text": text - }] + "contents": [content_obj] }); Some(crate::mcp::success(id, payload)) } @@ -756,8 +763,15 @@ impl MemoryHandler { match result { Ok(text) => { + let mut content_obj = serde_json::json!({"type": "text", "text": &text}); + + // Heuristic: If response is large (e.g. > 4000 chars), inject cache_control for Prompt Caching + if text.len() > 4000 { + content_obj["cacheControl"] = serde_json::json!({"type": "ephemeral"}); + } + let payload = serde_json::json!({ - "content": [{"type": "text", "text": text}], + "content": [content_obj], "isError": false }); Some(crate::mcp::success(id_clone, payload)) @@ -788,8 +802,13 @@ impl MemoryHandler { } }); let unblocked = crate::handlers::reconciliation::cascade_unblock_tasks(&self.state); - let milestones = crate::handlers::reconciliation::recalculate_milestones(&self.state); - self.state.record_activity("task", &format!("Completed task via WebSocket RPC: {}", task_id), Some("completed")); + let milestones = + crate::handlers::reconciliation::recalculate_milestones(&self.state); + self.state.record_activity( + "task", + &format!("Completed task via WebSocket RPC: {}", task_id), + Some("completed"), + ); let _ = self.state.event_bus_tx.send(crate::state::GenericEvent { topic: "task:event".to_string(), session_id: None, @@ -800,11 +819,14 @@ impl MemoryHandler { "updated_milestones": milestones, }), }); - Some(crate::mcp::success(id, serde_json::json!({ - "status": "success", - "unblocked_tasks": unblocked, - "updated_milestones": milestones - }))) + Some(crate::mcp::success( + id, + serde_json::json!({ + "status": "success", + "unblocked_tasks": unblocked, + "updated_milestones": milestones + }), + )) } "tech_debts/resolve" => { let params = req.get("params").unwrap_or(&serde_json::Value::Null); @@ -823,7 +845,11 @@ impl MemoryHandler { } }); if found { - self.state.record_activity("tech_debt", &format!("Resolved tech debt via WebSocket RPC: {}", debt_id), Some("resolved")); + self.state.record_activity( + "tech_debt", + &format!("Resolved tech debt via WebSocket RPC: {}", debt_id), + Some("resolved"), + ); let _ = self.state.event_bus_tx.send(crate::state::GenericEvent { topic: "tech_debt:event".to_string(), session_id: None, @@ -832,7 +858,10 @@ impl MemoryHandler { "status": "resolved" }), }); - Some(crate::mcp::success(id, serde_json::json!({ "status": "success", "message": format!("Tech debt {} resolved", debt_id) }))) + Some(crate::mcp::success( + id, + serde_json::json!({ "status": "success", "message": format!("Tech debt {} resolved", debt_id) }), + )) } else { Some(crate::mcp::error(id, -32602, "Tech debt not found")) } @@ -853,13 +882,20 @@ impl MemoryHandler { } }); if found { - self.state.record_activity("gate", &format!("Authorized push gate via WebSocket: {}", gate_id), Some("authorized")); + self.state.record_activity( + "gate", + &format!("Authorized push gate via WebSocket: {}", gate_id), + Some("authorized"), + ); let _ = self.state.event_bus_tx.send(crate::state::GenericEvent { topic: "gate:event".to_string(), session_id: None, payload: serde_json::json!({ "id": gate_id, "status": "authorized", "active_gate": "authorized" }), }); - Some(crate::mcp::success(id, serde_json::json!({ "status": "success" }))) + Some(crate::mcp::success( + id, + serde_json::json!({ "status": "success" }), + )) } else { Some(crate::mcp::error(id, -32602, "Gate not found")) } @@ -880,13 +916,20 @@ impl MemoryHandler { } }); if found { - self.state.record_activity("gate", &format!("Blocked push gate via WebSocket: {}", gate_id), Some("blocked")); + self.state.record_activity( + "gate", + &format!("Blocked push gate via WebSocket: {}", gate_id), + Some("blocked"), + ); let _ = self.state.event_bus_tx.send(crate::state::GenericEvent { topic: "gate:event".to_string(), session_id: None, payload: serde_json::json!({ "id": gate_id, "status": "blocked", "active_gate": "blocked" }), }); - Some(crate::mcp::success(id, serde_json::json!({ "status": "success" }))) + Some(crate::mcp::success( + id, + serde_json::json!({ "status": "success" }), + )) } else { Some(crate::mcp::error(id, -32602, "Gate not found")) } @@ -902,13 +945,20 @@ impl MemoryHandler { } }); if removed { - self.state.record_activity("gate", &format!("Deleted push gate via WebSocket: {}", gate_id), Some("deleted")); + self.state.record_activity( + "gate", + &format!("Deleted push gate via WebSocket: {}", gate_id), + Some("deleted"), + ); let _ = self.state.event_bus_tx.send(crate::state::GenericEvent { topic: "gate:event".to_string(), session_id: None, payload: serde_json::json!({ "id": gate_id, "status": "deleted" }), }); - Some(crate::mcp::success(id, serde_json::json!({ "status": "success" }))) + Some(crate::mcp::success( + id, + serde_json::json!({ "status": "success" }), + )) } else { Some(crate::mcp::error(id, -32602, "Gate not found")) } @@ -1003,7 +1053,10 @@ pub fn format_tool_activity_description(name: &str, args: &serde_json::Value) -> ("Create Entities", names) } "hypotheses" => { - let act = args.get("action").and_then(|v| v.as_str()).unwrap_or("manage"); + let act = args + .get("action") + .and_then(|v| v.as_str()) + .unwrap_or("manage"); let hyp = args .get("hypothesis") .or_else(|| args.get("query")) @@ -1011,24 +1064,39 @@ pub fn format_tool_activity_description(name: &str, args: &serde_json::Value) -> .unwrap_or(""); ( "Hypotheses", - format!("{}: {}", act, hyp).trim_end_matches(": ").to_string(), + format!("{}: {}", act, hyp) + .trim_end_matches(": ") + .to_string(), ) } "agent_signals" => { - let act = args.get("action").and_then(|v| v.as_str()).unwrap_or("signal"); + let act = args + .get("action") + .and_then(|v| v.as_str()) + .unwrap_or("signal"); let sender = args.get("sender").and_then(|v| v.as_str()).unwrap_or(""); - let st = args.get("signal_type").and_then(|v| v.as_str()).unwrap_or(""); + let st = args + .get("signal_type") + .and_then(|v| v.as_str()) + .unwrap_or(""); ( "Agent Signals", - format!("{}: {} [{}]", act, sender, st).trim_end_matches(": ").to_string(), + format!("{}: {} [{}]", act, sender, st) + .trim_end_matches(": ") + .to_string(), ) } "process_logs" => { - let act = args.get("action").and_then(|v| v.as_str()).unwrap_or("logs"); + let act = args + .get("action") + .and_then(|v| v.as_str()) + .unwrap_or("logs"); let file = args.get("file_path").and_then(|v| v.as_str()).unwrap_or(""); ( "Process Logs", - format!("{}: {}", act, file).trim_end_matches(": ").to_string(), + format!("{}: {}", act, file) + .trim_end_matches(": ") + .to_string(), ) } _ => return name.to_string(), @@ -1053,8 +1121,8 @@ async fn execute_tool_span( #[cfg(test)] mod tests { use super::*; - use tempfile::tempdir; use serde_json::json; + use tempfile::tempdir; #[test] fn test_format_tool_activity_description() { diff --git a/server/src/search.rs b/server/src/search.rs index 5b05287..4474fba 100644 --- a/server/src/search.rs +++ b/server/src/search.rs @@ -16,6 +16,26 @@ pub struct SearchResult { pub type SearchResultTuple = (String, String, String, String, f32); +fn calculate_time_decay(timestamp_sec: Option) -> f32 { + let Some(ts) = timestamp_sec else { return 1.0 }; + if ts == 0 { + return 1.0; + } // Avoid decaying default unset timestamps + + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + + if ts > now { + return 1.0; + } + + let age_days = (now - ts) as f32 / 86400.0; + // 1% decay per day + (-0.01 * age_days).exp() +} + #[derive(Clone)] pub struct MemoryIndex { pub index: Index, @@ -55,7 +75,8 @@ impl MemoryIndex { .reload_policy(ReloadPolicy::OnCommitWithDelay) .try_into()?; - let query_parser = tantivy::query::QueryParser::for_index(&index, vec![title_field, body_field]); + let query_parser = + tantivy::query::QueryParser::for_index(&index, vec![title_field, body_field]); Ok(Self { index, @@ -87,7 +108,8 @@ impl MemoryIndex { .reload_policy(ReloadPolicy::OnCommitWithDelay) .try_into()?; - let query_parser = tantivy::query::QueryParser::for_index(&index, vec![title_field, body_field]); + let query_parser = + tantivy::query::QueryParser::for_index(&index, vec![title_field, body_field]); Ok(Self { index, @@ -117,7 +139,10 @@ impl MemoryIndex { self.index_entities_batch(std::slice::from_ref(e)) } - pub fn index_entities_batch(&self, entities: &[Entity]) -> tokio::task::JoinHandle> { + pub fn index_entities_batch( + &self, + entities: &[Entity], + ) -> tokio::task::JoinHandle> { let writer = Arc::clone(&self.writer); let id_field = self.id_field; let needs_commit = Arc::clone(&self.needs_commit); @@ -154,13 +179,16 @@ impl MemoryIndex { let id_val = t.id.clone(); let needs_commit = Arc::clone(&self.needs_commit); - let criteria_body = t.acceptance_criteria.iter().fold(String::new(), |mut acc, c| { - if !acc.is_empty() { - acc.push('\n'); - } - acc.push_str(&c.description); - acc - }); + let criteria_body = t + .acceptance_criteria + .iter() + .fold(String::new(), |mut acc, c| { + if !acc.is_empty() { + acc.push('\n'); + } + acc.push_str(&c.description); + acc + }); let doc = doc!( self.id_field => t.id.as_str(), @@ -231,7 +259,10 @@ impl MemoryIndex { if let Err(first_err) = writer.commit() { std::thread::sleep(std::time::Duration::from_millis(100)); writer.commit().map_err(|second_err| { - tantivy::TantivyError::SystemError(format!("First commit error: {}; Second commit error: {}", first_err, second_err)) + tantivy::TantivyError::SystemError(format!( + "First commit error: {}; Second commit error: {}", + first_err, second_err + )) })?; } let _ = reader.reload(); @@ -246,9 +277,11 @@ impl MemoryIndex { }) } - pub fn commit_if_needed(&self) -> tantivy::Result<()> { - if self.needs_commit.swap(false, std::sync::atomic::Ordering::SeqCst) { + if self + .needs_commit + .swap(false, std::sync::atomic::Ordering::SeqCst) + { let mut writer = self.writer.lock().unwrap_or_else(|e| e.into_inner()); writer.commit()?; let _ = self.reader.reload(); @@ -268,8 +301,8 @@ impl MemoryIndex { let sanitized: String = query .chars() .map(|c| match c { - '+' | '-' | '&' | '|' | '!' | '(' | ')' | '{' | '}' | '[' | ']' | '^' - | '"' | '~' | '*' | '?' | ':' | '\\' | '/' => ' ', + '+' | '-' | '&' | '|' | '!' | '(' | ')' | '{' | '}' | '[' | ']' | '^' | '"' + | '~' | '*' | '?' | ':' | '\\' | '/' => ' ', _ => c, }) .collect(); @@ -284,10 +317,11 @@ impl MemoryIndex { let target_limit = if is_wildcard_fallback { 10 } else { 50 }; let final_query: Box = if let Some(ns) = namespace { - let ns_term_query: Box = Box::new(tantivy::query::TermQuery::new( - tantivy::Term::from_field_text(self.namespace_field, ns), - tantivy::schema::IndexRecordOption::Basic, - )); + let ns_term_query: Box = + Box::new(tantivy::query::TermQuery::new( + tantivy::Term::from_field_text(self.namespace_field, ns), + tantivy::schema::IndexRecordOption::Basic, + )); let ns_filter: Box = if ns != "global" { let global_term_query = Box::new(tantivy::query::TermQuery::new( tantivy::Term::from_field_text(self.namespace_field, "global"), @@ -491,7 +525,8 @@ impl SearchService { self.state.code.snippets.read_with(|snips| { for snippet in snips.iter() { if let Some(ref emb) = snippet.embedding { - let sim = cosine_similarity(&query_emb, emb); + let base_sim = cosine_similarity(&query_emb, emb); + let sim = base_sim * calculate_time_decay(Some(snippet.updated_at)); results.push(SearchResult { id: snippet.name.clone(), doc_type: "snippet".to_string(), @@ -508,6 +543,7 @@ impl SearchService { snippet.name.clone(), "snippet".to_string(), snippet.description.clone(), + Some(snippet.updated_at), )); } } @@ -515,15 +551,15 @@ impl SearchService { self.state.read_graph(|graph| { for entity in graph.entities.values() { - if let Some(ns) = filter_namespace { - if entity.namespace != ns { + if let Some(ns) = filter_namespace + && entity.namespace != ns { continue; } - } let obs = entity.observations.join("; "); let desc = format!("{}: {}", entity.entity_type, obs); if let Some(ref emb) = entity.embedding { - let sim = cosine_similarity(&query_emb, emb); + let base_sim = cosine_similarity(&query_emb, emb); + let sim = base_sim * calculate_time_decay(entity.updated_at); results.push(SearchResult { id: entity.name.clone(), doc_type: "entity".to_string(), @@ -533,7 +569,12 @@ impl SearchService { }); } else if uncached_texts.len() < 50 { uncached_texts.push(format!("{} {} {}", entity.name, entity.entity_type, obs)); - uncached_meta.push((entity.name.clone(), "entity".to_string(), desc)); + uncached_meta.push(( + entity.name.clone(), + "entity".to_string(), + desc, + entity.updated_at, + )); } } }); @@ -541,7 +582,8 @@ impl SearchService { self.state.code.error_fixes.read_with(|fixes| { for fix in fixes.iter() { if let Some(ref emb) = fix.embedding { - let sim = cosine_similarity(&query_emb, emb); + let base_sim = cosine_similarity(&query_emb, emb); + let sim = base_sim * calculate_time_decay(Some(fix.timestamp)); results.push(SearchResult { id: fix.signature.clone(), doc_type: "error_fix".to_string(), @@ -555,6 +597,7 @@ impl SearchService { fix.signature.clone(), "error_fix".to_string(), fix.solution.clone(), + Some(fix.timestamp), )); } } @@ -563,8 +606,9 @@ impl SearchService { if !uncached_texts.is_empty() && let Ok(embeddings) = generate_embeddings_async(uncached_texts).await { - for (emb, (title, doc_type, body)) in embeddings.into_iter().zip(uncached_meta) { - let sim = cosine_similarity(&query_emb, &emb); + for (emb, (title, doc_type, body, ts)) in embeddings.into_iter().zip(uncached_meta) { + let base_sim = cosine_similarity(&query_emb, &emb); + let sim = base_sim * calculate_time_decay(ts); results.push(SearchResult { id: title.clone(), doc_type, diff --git a/server/src/state.rs b/server/src/state.rs index a22e87c..512aa01 100644 --- a/server/src/state.rs +++ b/server/src/state.rs @@ -86,11 +86,10 @@ impl ClipboardCacheState { } pub fn push_text(&mut self, text: CachedClipboardText) { - if let Some(ref prev) = self.last_text { - if prev.text == text.text { + if let Some(ref prev) = self.last_text + && prev.text == text.text { return; } - } self.last_text = Some(text.clone()); self.history.push_front(ClipboardHistoryItem::Text(text)); if self.history.len() > 20 { @@ -142,11 +141,10 @@ impl MemoryState { pub fn new(base_dir_str: &str) -> Self { let is_in_memory = base_dir_str == ":memory:"; let base = std::path::PathBuf::from(base_dir_str); - if !is_in_memory { - if let Err(e) = std::fs::create_dir_all(&base) { + if !is_in_memory + && let Err(e) = std::fs::create_dir_all(&base) { tracing::error!("Failed to create store directory at {:?}: {}", base, e); } - } let db = crate::db::init_redb(&base); diff --git a/server/src/store.rs b/server/src/store.rs index 41913b8..5a74f60 100644 --- a/server/src/store.rs +++ b/server/src/store.rs @@ -33,11 +33,10 @@ static QUEUE_REGISTRY: std::sync::Mutex, DbWriteQueue)>> = fn get_or_create_queue(db: Arc) -> DbWriteQueue { let mut reg = QUEUE_REGISTRY.lock().unwrap_or_else(|e| e.into_inner()); - if let Some((ref existing_db, ref queue)) = *reg { - if Arc::ptr_eq(existing_db, &db) && !queue.tx.is_closed() { + if let Some((ref existing_db, ref queue)) = *reg + && Arc::ptr_eq(existing_db, &db) && !queue.tx.is_closed() { return queue.clone(); } - } let new_queue = DbWriteQueue::new(db.clone()); *reg = Some((db, new_queue.clone())); new_queue @@ -334,19 +333,17 @@ impl Store let mut known = std::collections::HashSet::new(); if let Ok(range) = table.range(prefix.as_str()..) { - for entry in range { - if let Ok((k, v)) = entry { - let k_str = k.value(); - if !k_str.starts_with(&prefix) { - break; - } - found_granular = true; - known.insert(k_str.to_string()); - if let Ok(val) = serde_json::from_slice::(v.value()) { - let sub_key = &k_str[prefix.len()..]; - items_array.push(val.clone()); - items_map.insert(sub_key.to_string(), val); - } + for (k, v) in range.flatten() { + let k_str = k.value(); + if !k_str.starts_with(&prefix) { + break; + } + found_granular = true; + known.insert(k_str.to_string()); + if let Ok(val) = serde_json::from_slice::(v.value()) { + let sub_key = &k_str[prefix.len()..]; + items_array.push(val.clone()); + items_map.insert(sub_key.to_string(), val); } } } diff --git a/server/src/tools.rs b/server/src/tools.rs index 6c7762d..11b3e9b 100644 --- a/server/src/tools.rs +++ b/server/src/tools.rs @@ -993,7 +993,13 @@ pub struct EnvironmentTool { #[derive(Debug, Clone, Copy, Deserialize, Serialize, JsonSchema, PartialEq)] #[serde(rename_all = "snake_case")] pub enum ClipboardAction { - #[serde(alias = "image", alias = "IMAGE", alias = "Image", alias = "screenshot", alias = "SCREENSHOT")] + #[serde( + alias = "image", + alias = "IMAGE", + alias = "Image", + alias = "screenshot", + alias = "SCREENSHOT" + )] Image, #[serde(alias = "text", alias = "TEXT", alias = "Text")] Text, diff --git a/server/src/watcher.rs b/server/src/watcher.rs index 45517d9..8c8b01e 100644 --- a/server/src/watcher.rs +++ b/server/src/watcher.rs @@ -79,7 +79,11 @@ pub fn spawn_watcher(state: Arc) { fn should_review(path: &Path) -> bool { let path_str = path.to_string_lossy(); - if path_str.contains(".git") || path_str.contains("target") || path_str.contains(".gemini") || path_str.contains("node_modules") { + if path_str.contains(".git") + || path_str.contains("target") + || path_str.contains(".gemini") + || path_str.contains("node_modules") + { return false; } @@ -89,13 +93,19 @@ fn should_review(path: &Path) -> bool { } async fn trigger_autonomous_review(path: &Path, state: Arc) { - info!("Triggering autonomous review & incremental AST index for {:?}", path); - state.broadcast_activity("AUTONOMOUS", &format!("Modified: {:?}", path.file_name().unwrap_or_default())); + info!( + "Triggering autonomous review & incremental AST index for {:?}", + path + ); + state.broadcast_activity( + "AUTONOMOUS", + &format!("Modified: {:?}", path.file_name().unwrap_or_default()), + ); // ADR-0109: Incremental Background AST Indexing & Differential Graph Updates let ext = path.extension().and_then(|e| e.to_str()).unwrap_or(""); - if matches!(ext, "rs" | "ts" | "js" | "py" | "go" | "java" | "c" | "cpp") { - if let Ok(content) = std::fs::read_to_string(path) { + if matches!(ext, "rs" | "ts" | "js" | "py" | "go" | "java" | "c" | "cpp") + && let Ok(content) = std::fs::read_to_string(path) { let language = match ext { "rs" => Some(tree_sitter_rust::LANGUAGE), "ts" | "js" => Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT), @@ -109,12 +119,20 @@ async fn trigger_autonomous_review(path: &Path, state: Arc) { if let Some(lang) = language { let mut parser = tree_sitter::Parser::new(); - if parser.set_language(&lang.into()).is_ok() { - if let Some(tree) = parser.parse(&content, None) { + if parser.set_language(&lang.into()).is_ok() + && let Some(tree) = parser.parse(&content, None) { let mut chunks = Vec::new(); - crate::indexer::extract_chunks(tree.root_node(), &content, &mut chunks, ext); + crate::indexer::extract_chunks( + tree.root_node(), + &content, + &mut chunks, + ext, + ); let file_str = path.to_string_lossy().to_string(); - let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs(); + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); let chunks_count = chunks.len(); let file_str_clone = file_str.clone(); @@ -140,16 +158,21 @@ async fn trigger_autonomous_review(path: &Path, state: Arc) { "McpTool".to_string() }; - g.entities.insert(symbol_name.clone(), crate::models::Entity { - name: symbol_name.clone(), - entity_type: symbol_type, - observations: vec![format!("AST definition: {} chars", chunk_code.len())], - namespace: "global".to_string(), - file_path: Some(file_str.clone()), - created_at: Some(now), - updated_at: Some(now), - ..Default::default() - } + g.entities.insert( + symbol_name.clone(), + crate::models::Entity { + name: symbol_name.clone(), + entity_type: symbol_type, + observations: vec![format!( + "AST definition: {} chars", + chunk_code.len() + )], + namespace: "global".to_string(), + file_path: Some(file_str.clone()), + created_at: Some(now), + updated_at: Some(now), + ..Default::default() + }, ); let rel = crate::models::Relation { @@ -183,12 +206,13 @@ async fn trigger_autonomous_review(path: &Path, state: Arc) { }), }); } - } } } - } - info!("Autonomous review & incremental AST index complete for {:?}", path); + info!( + "Autonomous review & incremental AST index complete for {:?}", + path + ); } #[cfg(test)] mod tests { @@ -220,4 +244,3 @@ mod tests { spawn_watcher(state); } } - diff --git a/server/tests/image_test.rs b/server/tests/image_test.rs new file mode 100644 index 0000000..42c0853 --- /dev/null +++ b/server/tests/image_test.rs @@ -0,0 +1,7 @@ +use image::DynamicImage; +#[test] +fn test_img() { + let mut img = image::DynamicImage::new_rgb8(10, 10); + img.invert(); + let img2 = img.adjust_contrast(20.0); +} diff --git a/server/tests/parity_test.rs b/server/tests/parity_test.rs index fd22720..950fc85 100644 --- a/server/tests/parity_test.rs +++ b/server/tests/parity_test.rs @@ -93,10 +93,9 @@ fn test_dashboard_route_parity() { "server/src/api/setup.rs" }; - let dashboard_content = std::fs::read_to_string(dashboard_path) - .expect("Failed to read dashboard.ts"); - let setup_content = std::fs::read_to_string(setup_path) - .expect("Failed to read setup.rs"); + let dashboard_content = + std::fs::read_to_string(dashboard_path).expect("Failed to read dashboard.ts"); + let setup_content = std::fs::read_to_string(setup_path).expect("Failed to read setup.rs"); // 1. Extract routes from setup.rs let mut registered_routes = HashSet::new(); @@ -142,7 +141,8 @@ fn test_dashboard_route_parity() { } } if endpoint.is_empty() { - let end_idx = rest.find(['"', '`', '\'', ' ', '\n', '?']) + let end_idx = rest + .find(['"', '`', '\'', ' ', '\n', '?']) .unwrap_or(rest.len()); endpoint = rest[..end_idx].to_string(); } @@ -153,7 +153,10 @@ fn test_dashboard_route_parity() { cursor = start + 5; } - assert!(!frontend_endpoints.is_empty(), "No frontend endpoints found in dashboard.ts"); + assert!( + !frontend_endpoints.is_empty(), + "No frontend endpoints found in dashboard.ts" + ); for endpoint in &frontend_endpoints { assert!( diff --git a/stub/src/logger.rs b/stub/src/logger.rs index 8181ac7..306d758 100644 --- a/stub/src/logger.rs +++ b/stub/src/logger.rs @@ -48,28 +48,45 @@ mod tests { #[test] fn test_extract_log_prefix_response() { let json_str = r#"{"jsonrpc":"2.0","id":123,"result":{}}"#; - assert_eq!(extract_log_prefix(json_str, true), "Response id=123 [Error: false]"); + assert_eq!( + extract_log_prefix(json_str, true), + "Response id=123 [Error: false]" + ); - let err_json = r#"{"jsonrpc":"2.0","id":456,"error":{"code":-32600,"message":"Invalid Request"}}"#; - assert_eq!(extract_log_prefix(err_json, true), "Response id=456 [Error: true]"); + let err_json = + r#"{"jsonrpc":"2.0","id":456,"error":{"code":-32600,"message":"Invalid Request"}}"#; + assert_eq!( + extract_log_prefix(err_json, true), + "Response id=456 [Error: true]" + ); let is_err_json = r#"{"jsonrpc":"2.0","id":789,"result":{"isError":true}}"#; - assert_eq!(extract_log_prefix(is_err_json, true), "Response id=789 [Error: true]"); + assert_eq!( + extract_log_prefix(is_err_json, true), + "Response id=789 [Error: true]" + ); } #[test] fn test_extract_log_prefix_tool_call() { - let json_str = r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"search"}}"#; + let json_str = + r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"search"}}"#; assert_eq!(extract_log_prefix(json_str, false), "ToolCall[search] id=1"); let unknown_tool = r#"{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{}}"#; - assert_eq!(extract_log_prefix(unknown_tool, false), "ToolCall[unknown] id=2"); + assert_eq!( + extract_log_prefix(unknown_tool, false), + "ToolCall[unknown] id=2" + ); } #[test] fn test_extract_log_prefix_request() { let json_str = r#"{"jsonrpc":"2.0","id":10,"method":"initialize"}"#; - assert_eq!(extract_log_prefix(json_str, false), "Request[initialize] id=10"); + assert_eq!( + extract_log_prefix(json_str, false), + "Request[initialize] id=10" + ); } #[test] @@ -84,4 +101,3 @@ mod tests { assert_eq!(extract_log_prefix(json_str, false), "Message"); } } - diff --git a/stub/src/main.rs b/stub/src/main.rs index bac6f71..49156bf 100644 --- a/stub/src/main.rs +++ b/stub/src/main.rs @@ -170,11 +170,11 @@ fn main() -> Result<(), Box> { } } } - + if retry_count >= 20 { return Err("Failed to connect to the MCP memory server after multiple attempts. Is it running?".into()); } - + Ok(()) }) }