chore: fix formatting and clippy lints
This commit is contained in:
1 parent
8952bd5399
commit
9d9e959744
35 files changed
+1091
-582
No files matched your search
@@ -23,7 +23,7 @@ pub async fn read_mcp_message<R: tokio::io::AsyncRead + Unpin>(
|
|||||||
break;
|
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:")
|
&& trimmed_line.as_bytes()[..15].eq_ignore_ascii_case(b"content-length:")
|
||||||
{
|
{
|
||||||
length = trimmed_line[15..].trim().parse().unwrap_or(0);
|
length = trimmed_line[15..].trim().parse().unwrap_or(0);
|
||||||
|
|||||||
+39
-29
@@ -214,7 +214,7 @@ pub struct NvimRequest {
|
|||||||
pub reply: oneshot::Sender<Result<rmpv::Value, String>>,
|
pub reply: oneshot::Sender<Result<rmpv::Value, String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
use std::sync::atomic::{AtomicU32, AtomicU64, AtomicU8, Ordering};
|
use std::sync::atomic::{AtomicU8, AtomicU32, AtomicU64, Ordering};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum CircuitState {
|
pub enum CircuitState {
|
||||||
@@ -275,7 +275,9 @@ impl NvimCircuitBreaker {
|
|||||||
|
|
||||||
pub fn record_success(&self) {
|
pub fn record_success(&self) {
|
||||||
self.consecutive_failures.store(0, Ordering::SeqCst);
|
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 {
|
if prev != CircuitState::Closed as u8 {
|
||||||
tracing::info!("Neovim RPC circuit breaker transitioned to Closed (recovered)");
|
tracing::info!("Neovim RPC circuit breaker transitioned to Closed (recovered)");
|
||||||
}
|
}
|
||||||
@@ -299,7 +301,8 @@ impl NvimCircuitBreaker {
|
|||||||
|
|
||||||
pub fn reset(&self) {
|
pub fn reset(&self) {
|
||||||
self.consecutive_failures.store(0, Ordering::SeqCst);
|
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<mpsc::Sender<NvimRequest>, String> {
|
|||||||
// without spawning any external nvim.exe child process!
|
// without spawning any external nvim.exe child process!
|
||||||
{
|
{
|
||||||
let conn_lock = NVIM_CONN.lock().await;
|
let conn_lock = NVIM_CONN.lock().await;
|
||||||
if let Some(sender) = conn_lock.as_ref() {
|
if let Some(sender) = conn_lock.as_ref()
|
||||||
if !sender.is_closed() {
|
&& !sender.is_closed() {
|
||||||
let active_lock = ACTIVE_SOCKET.lock().await;
|
let active_lock = ACTIVE_SOCKET.lock().await;
|
||||||
if let Some(ref current_socket) = *active_lock {
|
if let Some(ref current_socket) = *active_lock
|
||||||
if let Ok(target) = read_active_socket_file().await {
|
&& let Ok(target) = read_active_socket_file().await
|
||||||
if !target.is_empty() && &target == current_socket {
|
&& !target.is_empty() && &target == current_socket {
|
||||||
return Ok(sender.clone());
|
return Ok(sender.clone());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let interactive_socket = get_socket_path().await.ok();
|
let interactive_socket = get_socket_path().await.ok();
|
||||||
@@ -643,7 +643,10 @@ async fn get_nvim_connection() -> Result<mpsc::Sender<NvimRequest>, String> {
|
|||||||
|
|
||||||
async fn call_nvim(req: rmpv::Value) -> Result<rmpv::Value, String> {
|
async fn call_nvim(req: rmpv::Value) -> Result<rmpv::Value, String> {
|
||||||
if !CIRCUIT_BREAKER.can_execute() {
|
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 {
|
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()
|
.to_string()
|
||||||
});
|
});
|
||||||
|
|
||||||
let options_arr = match args.get("options").and_then(|v| v.as_array()) {
|
let options_arr =
|
||||||
Some(arr) => {
|
match args.get("options").and_then(|v| v.as_array()) {
|
||||||
let rmp_arr: Vec<rmpv::Value> = arr
|
Some(arr) => {
|
||||||
.iter()
|
let rmp_arr: Vec<rmpv::Value> = arr
|
||||||
.filter_map(|item| item.as_str())
|
.iter()
|
||||||
.map(|s| rmpv::Value::String(s.into()))
|
.filter_map(|item| item.as_str())
|
||||||
.collect();
|
.map(|s| rmpv::Value::String(s.into()))
|
||||||
rmpv::Value::Array(rmp_arr)
|
.collect();
|
||||||
}
|
rmpv::Value::Array(rmp_arr)
|
||||||
None => rmpv::Value::Nil,
|
}
|
||||||
};
|
None => rmpv::Value::Nil,
|
||||||
|
};
|
||||||
|
|
||||||
let lua_code = r#"
|
let lua_code = r#"
|
||||||
local prompt_text, options, request_id = ...
|
local prompt_text, options, request_id = ...
|
||||||
@@ -1782,7 +1786,8 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
send_error(id, -32602, "Missing 'prompt' or 'prompt_text'").await;
|
send_error(id, -32602, "Missing 'prompt' or 'prompt_text'")
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"extmark" => {
|
"extmark" => {
|
||||||
@@ -1812,11 +1817,10 @@ pub async fn run_mcp_loop(app_name: &str, app_version: &str) {
|
|||||||
}
|
}
|
||||||
"highlight" | "clear_highlight" => {
|
"highlight" | "clear_highlight" => {
|
||||||
let mut modified_args = args.clone();
|
let mut modified_args = args.clone();
|
||||||
if action == "clear_highlight" {
|
if action == "clear_highlight"
|
||||||
if let Some(obj) = modified_args.as_object_mut() {
|
&& let Some(obj) = modified_args.as_object_mut() {
|
||||||
obj.insert("clear_only".to_string(), json!(true));
|
obj.insert("clear_only".to_string(), json!(true));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
let args_rmp = json_to_rmpv(&modified_args);
|
let args_rmp = json_to_rmpv(&modified_args);
|
||||||
let code = "
|
let code = "
|
||||||
local args = ...
|
local args = ...
|
||||||
@@ -2216,13 +2220,19 @@ mod tests {
|
|||||||
// 2nd failure - trips to Open
|
// 2nd failure - trips to Open
|
||||||
cb.record_failure();
|
cb.record_failure();
|
||||||
assert_eq!(cb.current_state(), CircuitState::Open);
|
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
|
// Wait for cooldown
|
||||||
std::thread::sleep(std::time::Duration::from_millis(60));
|
std::thread::sleep(std::time::Duration::from_millis(60));
|
||||||
|
|
||||||
// After cooldown, can_execute transitions to HalfOpen
|
// 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);
|
assert_eq!(cb.current_state(), CircuitState::HalfOpen);
|
||||||
|
|
||||||
// Success in HalfOpen recovers back to Closed
|
// Success in HalfOpen recovers back to Closed
|
||||||
|
|||||||
@@ -41,7 +41,10 @@ pub async fn wait_for_event_handler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(tokio::sync::broadcast::error::RecvError::Lagged(skipped)) => {
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
Err(tokio::sync::broadcast::error::RecvError::Closed) => {
|
Err(tokio::sync::broadcast::error::RecvError::Closed) => {
|
||||||
|
|||||||
+42
-24
@@ -44,8 +44,7 @@ pub fn find_projected_knowledge(
|
|||||||
|| normalized_file.ends_with(&format!("/{}", ndfp.trim_start_matches('/')))
|
|| normalized_file.ends_with(&format!("/{}", ndfp.trim_start_matches('/')))
|
||||||
|| ndfp.ends_with(&format!("/{}", normalized_file.trim_start_matches('/')))
|
|| ndfp.ends_with(&format!("/{}", normalized_file.trim_start_matches('/')))
|
||||||
|| (!file_name.is_empty()
|
|| (!file_name.is_empty()
|
||||||
&& (ndfp == file_name
|
&& (ndfp == file_name || ndfp.ends_with(&format!("/{}", file_name))))
|
||||||
|| ndfp.ends_with(&format!("/{}", file_name))))
|
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -73,8 +72,7 @@ pub fn find_projected_knowledge(
|
|||||||
all_adrs
|
all_adrs
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|a| {
|
.filter(|a| {
|
||||||
a.status.eq_ignore_ascii_case("accepted")
|
a.status.eq_ignore_ascii_case("accepted") || a.status.eq_ignore_ascii_case("active")
|
||||||
|| a.status.eq_ignore_ascii_case("active")
|
|
||||||
})
|
})
|
||||||
.filter(|a| {
|
.filter(|a| {
|
||||||
for comp in &a.affected_components {
|
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_") {
|
if payload.event.starts_with("agent_") || payload.event.starts_with("diff_") {
|
||||||
let payload_val = serde_json::json!(&payload);
|
let payload_val = serde_json::json!(&payload);
|
||||||
let _ = state.handler.state.event_bus_tx.send(crate::state::GenericEvent {
|
let _ = state
|
||||||
topic: format!("nvim:ui:{}", payload.event),
|
.handler
|
||||||
session_id: Some(payload.session_id.clone()),
|
.state
|
||||||
payload: payload_val.clone(),
|
.event_bus_tx
|
||||||
});
|
.send(crate::state::GenericEvent {
|
||||||
|
topic: format!("nvim:ui:{}", payload.event),
|
||||||
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()),
|
session_id: Some(payload.session_id.clone()),
|
||||||
payload: payload_val.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 {
|
if let Some(ref diff_id) = payload.diff_id {
|
||||||
let _ = state.handler.state.event_bus_tx.send(crate::state::GenericEvent {
|
let _ = state
|
||||||
topic: format!("nvim:ui:{}:{}", payload.event, diff_id),
|
.handler
|
||||||
session_id: Some(payload.session_id.clone()),
|
.state
|
||||||
payload: payload_val.clone(),
|
.event_bus_tx
|
||||||
});
|
.send(crate::state::GenericEvent {
|
||||||
let _ = state.handler.state.event_bus_tx.send(crate::state::GenericEvent {
|
topic: format!("nvim:ui:{}:{}", payload.event, diff_id),
|
||||||
topic: format!("nvim:ui:diff_decision:{}", diff_id),
|
session_id: Some(payload.session_id.clone()),
|
||||||
session_id: Some(payload.session_id.clone()),
|
payload: payload_val.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
|
tasks
|
||||||
.iter()
|
.iter()
|
||||||
.find(|t| t.is_active())
|
.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())
|
.map(|t| t.title.clone())
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+16
-8
@@ -66,9 +66,11 @@ pub async fn handle_socket(socket: WebSocket, state: Arc<AppState>, _client_type
|
|||||||
activities
|
activities
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|act_val| {
|
.filter_map(|act_val| {
|
||||||
if act_val["category"] == "TASK_EVENT" {
|
if act_val["category"] == "TASK_EVENT"
|
||||||
if let Some(details_str) = act_val["details"].as_str() {
|
&& let Some(details_str) = act_val["details"].as_str()
|
||||||
if let Ok(event_val) = serde_json::from_str::<serde_json::Value>(details_str) {
|
&& let Ok(event_val) =
|
||||||
|
serde_json::from_str::<serde_json::Value>(details_str)
|
||||||
|
{
|
||||||
return Some(
|
return Some(
|
||||||
serde_json::json!({
|
serde_json::json!({
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
@@ -78,8 +80,6 @@ pub async fn handle_socket(socket: WebSocket, state: Arc<AppState>, _client_type
|
|||||||
.to_string(),
|
.to_string(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
None
|
||||||
})
|
})
|
||||||
.take(5)
|
.take(5)
|
||||||
@@ -262,7 +262,10 @@ mod tests {
|
|||||||
mem_state.broadcast_task_event(task_event.clone());
|
mem_state.broadcast_task_event(task_event.clone());
|
||||||
|
|
||||||
// Verify recent activities recorded the event
|
// 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!(!recorded.is_empty());
|
||||||
assert_eq!(recorded[0]["category"], "TASK_EVENT");
|
assert_eq!(recorded[0]["category"], "TASK_EVENT");
|
||||||
|
|
||||||
@@ -293,7 +296,9 @@ mod tests {
|
|||||||
.filter_map(|act_val| {
|
.filter_map(|act_val| {
|
||||||
if act_val["category"] == "TASK_EVENT" {
|
if act_val["category"] == "TASK_EVENT" {
|
||||||
if let Some(details_str) = act_val["details"].as_str() {
|
if let Some(details_str) = act_val["details"].as_str() {
|
||||||
if let Ok(event_val) = serde_json::from_str::<serde_json::Value>(details_str) {
|
if let Ok(event_val) =
|
||||||
|
serde_json::from_str::<serde_json::Value>(details_str)
|
||||||
|
{
|
||||||
return Some(
|
return Some(
|
||||||
serde_json::json!({
|
serde_json::json!({
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
@@ -315,7 +320,10 @@ mod tests {
|
|||||||
let _ = tx.try_send(notif);
|
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();
|
let parsed: serde_json::Value = serde_json::from_str(&replayed_msg).unwrap();
|
||||||
assert_eq!(parsed["method"], "notifications/task/completed");
|
assert_eq!(parsed["method"], "notifications/task/completed");
|
||||||
assert_eq!(parsed["params"]["task_id"], "task-999");
|
assert_eq!(parsed["params"]["task_id"], "task-999");
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
|
use mcp_memory_server::router::MemoryHandler;
|
||||||
|
use mcp_memory_server::state::MemoryState;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use mcp_memory_server::router::MemoryHandler;
|
|
||||||
use mcp_memory_server::state::MemoryState;
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
@@ -13,7 +13,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let handler = MemoryHandler::new(state);
|
let handler = MemoryHandler::new(state);
|
||||||
|
|
||||||
let win_dir = PathBuf::from(r"C:\Users\reazul.ashraf\.gemini\antigravity-cli\mcp\mcp-memory");
|
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)?;
|
fs::create_dir_all(&win_dir)?;
|
||||||
if !wsl_dir.exists() {
|
if !wsl_dir.exists() {
|
||||||
@@ -41,14 +42,12 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
if let Ok(entries) = fs::read_dir(&win_dir) {
|
if let Ok(entries) = fs::read_dir(&win_dir) {
|
||||||
for entry in entries.flatten() {
|
for entry in entries.flatten() {
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
if path.extension().map_or(false, |ext| ext == "json") {
|
if path.extension().is_some_and(|ext| ext == "json")
|
||||||
if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
|
&& let Some(stem) = path.file_stem().and_then(|s| s.to_str())
|
||||||
if !active_tools.contains(stem) {
|
&& !active_tools.contains(stem) {
|
||||||
println!(" [STALE REMOVED Win] {}", path.display());
|
println!(" [STALE REMOVED Win] {}", path.display());
|
||||||
let _ = fs::remove_file(&path);
|
let _ = fs::remove_file(&path);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,23 +55,24 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
if let Ok(entries) = fs::read_dir(&wsl_dir) {
|
if let Ok(entries) = fs::read_dir(&wsl_dir) {
|
||||||
for entry in entries.flatten() {
|
for entry in entries.flatten() {
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
if path.extension().map_or(false, |ext| ext == "json") {
|
if path.extension().is_some_and(|ext| ext == "json")
|
||||||
if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
|
&& let Some(stem) = path.file_stem().and_then(|s| s.to_str())
|
||||||
if !active_tools.contains(stem) {
|
&& !active_tools.contains(stem) {
|
||||||
println!(" [STALE REMOVED WSL] {}", path.display());
|
println!(" [STALE REMOVED WSL] {}", path.display());
|
||||||
let _ = fs::remove_file(&path);
|
let _ = fs::remove_file(&path);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync instructions.md
|
// 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() {
|
if instructions_src.exists() {
|
||||||
let _ = fs::copy(&instructions_src, win_dir.join("instructions.md"));
|
let _ = fs::copy(&instructions_src, win_dir.join("instructions.md"));
|
||||||
let _ = fs::copy(&instructions_src, wsl_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);
|
let _ = fs::copy(&instructions_src, server_instructions);
|
||||||
println!(" [OK] Synchronized instructions.md to Win, WSL, and server/src/instructions.md");
|
println!(" [OK] Synchronized instructions.md to Win, WSL, and server/src/instructions.md");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
fn main() {
|
||||||
|
let mut img = image::DynamicImage::new_rgb8(10, 10);
|
||||||
|
img.invert();
|
||||||
|
let _img2 = img.adjust_contrast(20.0);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
-6
@@ -167,23 +167,21 @@ pub fn init_redb(base: &Path) -> Arc<Database> {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
let migrated_path = json_path.with_extension("json.migrated");
|
let migrated_path = json_path.with_extension("json.migrated");
|
||||||
if let Err(_) = std::fs::rename(&json_path, &migrated_path) {
|
if std::fs::rename(&json_path, &migrated_path).is_err()
|
||||||
if migrated_path.exists() {
|
&& migrated_path.exists() {
|
||||||
let _ = std::fs::remove_file(&migrated_path);
|
let _ = std::fs::remove_file(&migrated_path);
|
||||||
let _ = std::fs::rename(&json_path, &migrated_path);
|
let _ = std::fs::rename(&json_path, &migrated_path);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if opened_ok {
|
if opened_ok
|
||||||
if let Err(e) = write_txn.commit() {
|
&& let Err(e) = write_txn.commit() {
|
||||||
tracing::error!("Failed to commit database migration transaction: {}", e);
|
tracing::error!("Failed to commit database migration transaction: {}", e);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!(
|
tracing::error!(
|
||||||
|
|||||||
+13
-3
@@ -17,7 +17,11 @@ pub enum AppError {
|
|||||||
EntityNotFound { name: String, namespace: String },
|
EntityNotFound { name: String, namespace: String },
|
||||||
|
|
||||||
#[error("Invalid relation between '{from}' and '{to}': {reason}")]
|
#[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}")]
|
#[error("Database error for key '{key}': {message}")]
|
||||||
DatabaseError { key: String, message: String },
|
DatabaseError { key: String, message: String },
|
||||||
@@ -50,11 +54,17 @@ impl IntoResponse for AppError {
|
|||||||
AppError::NotFound(msg) => (StatusCode::NOT_FOUND, msg.clone()),
|
AppError::NotFound(msg) => (StatusCode::NOT_FOUND, msg.clone()),
|
||||||
AppError::EntityNotFound { name, namespace } => (
|
AppError::EntityNotFound { name, namespace } => (
|
||||||
StatusCode::NOT_FOUND,
|
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 } => (
|
AppError::InvalidRelation { from, to, reason } => (
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
format!("Invalid relation between '{}' and '{}': {}", from, to, reason),
|
format!(
|
||||||
|
"Invalid relation between '{}' and '{}': {}",
|
||||||
|
from, to, reason
|
||||||
|
),
|
||||||
),
|
),
|
||||||
AppError::DatabaseError { key, message } => (
|
AppError::DatabaseError { key, message } => (
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
|||||||
@@ -119,11 +119,10 @@ impl McpTool for ReadFileSkeletonHandler {
|
|||||||
header.push('\n');
|
header.push('\n');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if header.is_empty() {
|
if header.is_empty()
|
||||||
if let Some(first_line) = node_text.lines().next() {
|
&& let Some(first_line) = node_text.lines().next() {
|
||||||
header = first_line.to_string();
|
header = first_line.to_string();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
out.push_str(&indent);
|
out.push_str(&indent);
|
||||||
out.push_str(header.trim());
|
out.push_str(header.trim());
|
||||||
@@ -425,11 +424,10 @@ fn scan_workspace_for_symbol(
|
|||||||
if scanned_files > 500 {
|
if scanned_files > 500 {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if let Ok(meta) = std::fs::metadata(path) {
|
if let Ok(meta) = std::fs::metadata(path)
|
||||||
if meta.len() > 1024 * 1024 {
|
&& meta.len() > 1024 * 1024 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if let Ok(content) = std::fs::read_to_string(path) {
|
if let Ok(content) = std::fs::read_to_string(path) {
|
||||||
for (line_num, line) in content.lines().enumerate() {
|
for (line_num, line) in content.lines().enumerate() {
|
||||||
let is_match = if filter_fn_call {
|
let is_match = if filter_fn_call {
|
||||||
|
|||||||
+33
-17
@@ -22,16 +22,31 @@ impl McpTool for EnvironmentHandler {
|
|||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||||
let req: EnvironmentTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
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 {
|
match req.action {
|
||||||
EnvAction::UpdateFingerprint => {
|
EnvAction::UpdateFingerprint => {
|
||||||
let tool_versions = req.tool_versions.unwrap_or_default();
|
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_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("SHELL").ok())
|
||||||
.or_else(|| std::env::var("STARSHIP_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(|| {
|
||||||
.or_else(|| if std::env::var("COMSPEC").is_ok() { Some("cmd".to_string()) } else { None })
|
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());
|
.unwrap_or_else(|| "unknown".to_string());
|
||||||
|
|
||||||
state.env.env_fingerprints.modify(|fps| {
|
state.env.env_fingerprints.modify(|fps| {
|
||||||
@@ -102,10 +117,7 @@ impl McpTool for EnvironmentHandler {
|
|||||||
}
|
}
|
||||||
EnvAction::GetDetails => {
|
EnvAction::GetDetails => {
|
||||||
let data = state.env.environments.read_with(|envs| {
|
let data = state.env.environments.read_with(|envs| {
|
||||||
let filtered: Vec<_> = envs
|
let filtered: Vec<_> = envs.iter().filter(|e| e.namespace == ns).collect();
|
||||||
.iter()
|
|
||||||
.filter(|e| e.namespace == ns)
|
|
||||||
.collect();
|
|
||||||
Ok::<String, crate::error::AppError>(serde_json::to_string(&filtered)?)
|
Ok::<String, crate::error::AppError>(serde_json::to_string(&filtered)?)
|
||||||
})?;
|
})?;
|
||||||
Ok(data)
|
Ok(data)
|
||||||
@@ -114,7 +126,6 @@ impl McpTool for EnvironmentHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -143,7 +154,10 @@ mod tests {
|
|||||||
assert_eq!(res, "Env fingerprint updated");
|
assert_eq!(res, "Env fingerprint updated");
|
||||||
|
|
||||||
let res2 = handler
|
let res2 = handler
|
||||||
.execute(json!({"action": "read_fingerprint", "namespace": "global"}), state.clone())
|
.execute(
|
||||||
|
json!({"action": "read_fingerprint", "namespace": "global"}),
|
||||||
|
state.clone(),
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -171,7 +185,10 @@ mod tests {
|
|||||||
|
|
||||||
let handler = EnvironmentHandler;
|
let handler = EnvironmentHandler;
|
||||||
let res = handler
|
let res = handler
|
||||||
.execute(json!({"action": "get_details", "namespace": "global"}), state.clone())
|
.execute(
|
||||||
|
json!({"action": "get_details", "namespace": "global"}),
|
||||||
|
state.clone(),
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -215,7 +232,10 @@ mod tests {
|
|||||||
assert_eq!(res2, "Environment registered");
|
assert_eq!(res2, "Environment registered");
|
||||||
|
|
||||||
let res3 = handler
|
let res3 = handler
|
||||||
.execute(json!({"action": "get_details", "namespace": "global"}), state.clone())
|
.execute(
|
||||||
|
json!({"action": "get_details", "namespace": "global"}),
|
||||||
|
state.clone(),
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -237,12 +257,8 @@ mod tests {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let res = handler
|
let res = handler.execute(args, state.clone()).await;
|
||||||
.execute(args, state.clone())
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert!(res.is_ok());
|
assert!(res.is_ok());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -139,8 +139,8 @@ impl McpTool for QueryGitDiffsHandler {
|
|||||||
let git_matches = tokio::task::spawn_blocking(move || {
|
let git_matches = tokio::task::spawn_blocking(move || {
|
||||||
let mut results = Vec::new();
|
let mut results = Vec::new();
|
||||||
let cwd = env::current_dir().unwrap_or_default();
|
let cwd = env::current_dir().unwrap_or_default();
|
||||||
if let Ok(repo) = git2::Repository::discover(&cwd) {
|
if let Ok(repo) = git2::Repository::discover(&cwd)
|
||||||
if let Ok(mut revwalk) = repo.revwalk() {
|
&& let Ok(mut revwalk) = repo.revwalk() {
|
||||||
let _ = revwalk.push_head();
|
let _ = revwalk.push_head();
|
||||||
let mut count = 0;
|
let mut count = 0;
|
||||||
for oid in revwalk.flatten() {
|
for oid in revwalk.flatten() {
|
||||||
@@ -161,7 +161,6 @@ impl McpTool for QueryGitDiffsHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
results
|
results
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
@@ -215,4 +214,3 @@ mod tests {
|
|||||||
assert!(parsed.get("branch").is_some() || parsed.is_object());
|
assert!(parsed.get("branch").is_some() || parsed.is_object());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
+153
-69
@@ -62,8 +62,8 @@ impl<'a> GraphQueryBuilder<'a> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
nodes_at_current_depth -= 1;
|
nodes_at_current_depth -= 1;
|
||||||
if current_depth < self.max_depth {
|
if current_depth < self.max_depth
|
||||||
if let Some(neighbors) = adj.get(current) {
|
&& let Some(neighbors) = adj.get(current) {
|
||||||
for &(neighbor, rel_type, is_inverse) in neighbors {
|
for &(neighbor, rel_type, is_inverse) in neighbors {
|
||||||
if !visited.contains(neighbor) {
|
if !visited.contains(neighbor) {
|
||||||
visited.insert(neighbor);
|
visited.insert(neighbor);
|
||||||
@@ -73,7 +73,6 @@ impl<'a> GraphQueryBuilder<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if nodes_at_current_depth == 0 {
|
if nodes_at_current_depth == 0 {
|
||||||
current_depth += 1;
|
current_depth += 1;
|
||||||
nodes_at_current_depth = nodes_at_next_depth;
|
nodes_at_current_depth = nodes_at_next_depth;
|
||||||
@@ -138,7 +137,9 @@ impl McpTool for QueryGraphPathHandler {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
.await
|
.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<String> = inserted.iter().map(|e| format!("{} ({})", e.name, e.entity_type)).collect();
|
let names: Vec<String> = inserted
|
||||||
|
.iter()
|
||||||
|
.map(|e| format!("{} ({})", e.name, e.entity_type))
|
||||||
|
.collect();
|
||||||
if !inserted.is_empty() {
|
if !inserted.is_empty() {
|
||||||
let idx = state.get_search_index().await;
|
let idx = state.get_search_index().await;
|
||||||
let _ = idx.index_entities_batch(&inserted).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 {
|
crate::models::Entity {
|
||||||
name: node_name.clone(),
|
name: node_name.clone(),
|
||||||
entity_type: "Entity".to_string(),
|
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(),
|
namespace: crate::models::default_namespace(),
|
||||||
git_branch: None,
|
git_branch: None,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
@@ -247,16 +257,28 @@ impl McpTool for CreateRelationsHandler {
|
|||||||
}
|
}
|
||||||
for mut relation in req.relations {
|
for mut relation in req.relations {
|
||||||
if !relation.from.is_empty() && !relation.to.is_empty() {
|
if !relation.from.is_empty() && !relation.to.is_empty() {
|
||||||
relation.relation_type = crate::models::normalize_relation_type(&relation.relation_type);
|
relation.relation_type =
|
||||||
added_relations.push(format!("{} -[{}]-> {}", relation.from, relation.relation_type, relation.to));
|
crate::models::normalize_relation_type(&relation.relation_type);
|
||||||
|
added_relations.push(format!(
|
||||||
|
"{} -[{}]-> {}",
|
||||||
|
relation.from, relation.relation_type, relation.to
|
||||||
|
));
|
||||||
g.relations.push(relation);
|
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() {
|
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)
|
Ok(msg)
|
||||||
}
|
}
|
||||||
@@ -462,7 +484,7 @@ impl McpTool for ReadGraphHandler {
|
|||||||
let filtered_entities: Vec<(&String, &crate::models::Entity)> = full
|
let filtered_entities: Vec<(&String, &crate::models::Entity)> = full
|
||||||
.entities
|
.entities
|
||||||
.iter()
|
.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();
|
.collect();
|
||||||
|
|
||||||
let total_count = filtered_entities.len();
|
let total_count = filtered_entities.len();
|
||||||
@@ -481,7 +503,7 @@ impl McpTool for ReadGraphHandler {
|
|||||||
.relations
|
.relations
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|r| {
|
.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))
|
ns_match && (paged_entities.contains_key(&r.from) || paged_entities.contains_key(&r.to))
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
@@ -499,7 +521,7 @@ impl McpTool for ReadGraphHandler {
|
|||||||
let rel_vec: Vec<&crate::models::Relation> = full
|
let rel_vec: Vec<&crate::models::Relation> = full
|
||||||
.relations
|
.relations
|
||||||
.iter()
|
.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();
|
.collect();
|
||||||
(ent_map, rel_vec, None)
|
(ent_map, rel_vec, None)
|
||||||
};
|
};
|
||||||
@@ -549,21 +571,19 @@ impl McpTool for ReadGraphHandler {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(meta) = &meta_json {
|
if let Some(meta) = &meta_json
|
||||||
if let Some(obj) = truncated_val.as_object_mut() {
|
&& let Some(obj) = truncated_val.as_object_mut() {
|
||||||
obj.insert("total_count".to_string(), meta["total_count"].clone());
|
obj.insert("total_count".to_string(), meta["total_count"].clone());
|
||||||
obj.insert("offset".to_string(), meta["offset"].clone());
|
obj.insert("offset".to_string(), meta["offset"].clone());
|
||||||
obj.insert("limit".to_string(), meta["limit"].clone());
|
obj.insert("limit".to_string(), meta["limit"].clone());
|
||||||
obj.insert("has_more".to_string(), serde_json::Value::Bool(true));
|
obj.insert("has_more".to_string(), serde_json::Value::Bool(true));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(trunc_str) = serde_json::to_string(&truncated_val) {
|
if let Ok(trunc_str) = serde_json::to_string(&truncated_val)
|
||||||
if trunc_str.len() <= max_chars || keep_count == 1 {
|
&& (trunc_str.len() <= max_chars || keep_count == 1) {
|
||||||
result_json = trunc_str;
|
result_json = trunc_str;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
keep_count = keep_count.saturating_sub(keep_count / 3 + 1);
|
keep_count = keep_count.saturating_sub(keep_count / 3 + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -820,7 +840,10 @@ impl McpTool for CondenseEntityHandler {
|
|||||||
e.observations = unique_obs;
|
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| {
|
state.modify_graph(|master| {
|
||||||
@@ -953,10 +976,9 @@ impl McpTool for GetSubgraphHandler {
|
|||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||||
let req: GetSubgraphTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
let req: GetSubgraphTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||||
let root = req
|
let root = req.root_entity.or(req.root_node).ok_or_else(|| {
|
||||||
.root_entity
|
crate::error::AppError::Internal("root_entity or root_node is required".to_string())
|
||||||
.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 depth = req.depth.unwrap_or(2);
|
||||||
let format = req.format.unwrap_or(SubgraphFormat::Json);
|
let format = req.format.unwrap_or(SubgraphFormat::Json);
|
||||||
|
|
||||||
@@ -974,15 +996,14 @@ impl McpTool for GetSubgraphHandler {
|
|||||||
visited.insert(root.clone());
|
visited.insert(root.clone());
|
||||||
|
|
||||||
while let Some((curr, d)) = queue.pop_front() {
|
while let Some((curr, d)) = queue.pop_front() {
|
||||||
if d < depth {
|
if d < depth
|
||||||
if let Some(neighbors) = adj.get(curr) {
|
&& let Some(neighbors) = adj.get(curr) {
|
||||||
for &next in neighbors {
|
for &next in neighbors {
|
||||||
if visited.insert(next.to_string()) {
|
if visited.insert(next.to_string()) {
|
||||||
queue.push_back((next, d + 1));
|
queue.push_back((next, d + 1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let ent: std::collections::HashMap<String, crate::models::Entity> = visited
|
let ent: std::collections::HashMap<String, crate::models::Entity> = visited
|
||||||
@@ -1011,7 +1032,10 @@ impl McpTool for GetSubgraphHandler {
|
|||||||
|
|
||||||
markdown.push_str("\n## Relations\n");
|
markdown.push_str("\n## Relations\n");
|
||||||
for r in &matched_relations {
|
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);
|
return Ok(markdown);
|
||||||
}
|
}
|
||||||
@@ -1038,7 +1062,10 @@ impl McpTool for SweepGraphHealthHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn schema(&self) -> Value {
|
fn schema(&self) -> Value {
|
||||||
crate::mcp::tool_def::<SweepGraphHealthTool>("sweep_graph_health", "Audit and sweep graph health")
|
crate::mcp::tool_def::<SweepGraphHealthTool>(
|
||||||
|
"sweep_graph_health",
|
||||||
|
"Audit and sweep graph health",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||||
@@ -1132,26 +1159,31 @@ impl McpTool for ResolveStaleSymbolsHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn schema(&self) -> Value {
|
fn schema(&self) -> Value {
|
||||||
crate::mcp::tool_def::<ResolveStaleSymbolsTool>("resolve_stale_symbols", "Inspect Knowledge Graph entities and tech debt symbol/line references against files on disk, healing stale pointers.")
|
crate::mcp::tool_def::<ResolveStaleSymbolsTool>(
|
||||||
|
"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<MemoryState>) -> crate::error::Result<String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||||
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 auto_heal = req.auto_heal.unwrap_or(true);
|
||||||
|
|
||||||
let mut checked = 0;
|
let mut checked = 0;
|
||||||
let mut stale_entities = Vec::new();
|
let mut stale_entities = Vec::new();
|
||||||
|
|
||||||
state.modify_graph(|g| {
|
state.modify_graph(|g| {
|
||||||
for (name, _entity) in g.entities.iter_mut() {
|
for name in g.entities.keys() {
|
||||||
if let Some(target) = &req.target {
|
if let Some(target) = &req.target
|
||||||
if name != target && !name.contains(target) {
|
&& name != target && !name.contains(target) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
checked += 1;
|
checked += 1;
|
||||||
let path = std::path::Path::new(name);
|
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());
|
stale_entities.push(name.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1184,7 +1216,10 @@ impl McpTool for SummarizeSubgraphHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn schema(&self) -> Value {
|
fn schema(&self) -> Value {
|
||||||
crate::mcp::tool_def::<SummarizeSubgraphTool>("summarize_subgraph", "Generate compact, LLM-optimized Markdown topology of a graph component capped within a token budget.")
|
crate::mcp::tool_def::<SummarizeSubgraphTool>(
|
||||||
|
"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<MemoryState>) -> crate::error::Result<String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||||
@@ -1212,8 +1247,18 @@ impl McpTool for SummarizeSubgraphHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let ent: Vec<_> = g.entities.values().filter(|e| visited.contains(&e.name)).cloned().collect();
|
let ent: Vec<_> = g
|
||||||
let rel: Vec<_> = g.relations.iter().filter(|r| visited.contains(&r.from) && visited.contains(&r.to)).cloned().collect();
|
.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)
|
(ent, rel)
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1228,7 +1273,10 @@ impl McpTool for SummarizeSubgraphHandler {
|
|||||||
|
|
||||||
markdown.push_str("\n## Relations\n");
|
markdown.push_str("\n## Relations\n");
|
||||||
for r in &sub_relations {
|
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);
|
let max_tokens = req.max_tokens.unwrap_or(1000);
|
||||||
@@ -1249,7 +1297,6 @@ mod tests {
|
|||||||
use crate::handlers::meta::AgentSignalsHandler;
|
use crate::handlers::meta::AgentSignalsHandler;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_create_and_read_entities() {
|
async fn test_create_and_read_entities() {
|
||||||
let state = Arc::new(MemoryState::new_in_memory());
|
let state = Arc::new(MemoryState::new_in_memory());
|
||||||
@@ -1324,10 +1371,7 @@ mod tests {
|
|||||||
{"source": "A", "target": "B", "relationType": "knows"}
|
{"source": "A", "target": "B", "relationType": "knows"}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
let alias_res = handler
|
let alias_res = handler.execute(alias_args, state.clone()).await.unwrap();
|
||||||
.execute(alias_args, state.clone())
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert!(alias_res.contains("Successfully created 1 relation(s)"));
|
assert!(alias_res.contains("Successfully created 1 relation(s)"));
|
||||||
|
|
||||||
// Test semantic LLM schema feedback on missing fields
|
// Test semantic LLM schema feedback on missing fields
|
||||||
@@ -1499,7 +1543,10 @@ mod tests {
|
|||||||
{"name": "Beta", "entity_type": "Node", "observations": ["Obs3"], "namespace": "global"}
|
{"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 rel_handler = CreateRelationsHandler;
|
||||||
let args_rel = json!({
|
let args_rel = json!({
|
||||||
@@ -1510,48 +1557,86 @@ mod tests {
|
|||||||
rel_handler.execute(args_rel, state.clone()).await.unwrap();
|
rel_handler.execute(args_rel, state.clone()).await.unwrap();
|
||||||
|
|
||||||
let path_handler = QueryGraphPathHandler;
|
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"));
|
assert!(path_res.contains("Alpha"));
|
||||||
|
|
||||||
let sub_handler = GetSubgraphHandler;
|
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"));
|
assert!(sub_res.contains("Alpha"));
|
||||||
|
|
||||||
let sum_handler = SummarizeSubgraphHandler;
|
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());
|
assert!(!sum_res.is_empty());
|
||||||
|
|
||||||
|
|
||||||
let sweep_handler = SweepGraphHealthHandler;
|
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());
|
assert!(!sweep_res.is_empty());
|
||||||
|
|
||||||
let del_obs_handler = DeleteObservationsHandler;
|
let del_obs_handler = DeleteObservationsHandler;
|
||||||
let del_obs_res = del_obs_handler.execute(json!({
|
let del_obs_res = del_obs_handler
|
||||||
"deletions": [{"entity_name": "Alpha", "observations": ["Obs1"]}]
|
.execute(
|
||||||
}), state.clone()).await.unwrap();
|
json!({
|
||||||
|
"deletions": [{"entity_name": "Alpha", "observations": ["Obs1"]}]
|
||||||
|
}),
|
||||||
|
state.clone(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
assert_eq!(del_obs_res, "Observations deleted");
|
assert_eq!(del_obs_res, "Observations deleted");
|
||||||
|
|
||||||
let del_rel_handler = DeleteRelationsHandler;
|
let del_rel_handler = DeleteRelationsHandler;
|
||||||
let del_rel_res = del_rel_handler.execute(json!({
|
let del_rel_res = del_rel_handler
|
||||||
"relations": [{"from": "Alpha", "to": "Beta", "relation_type": "links_to"}]
|
.execute(
|
||||||
}), state.clone()).await.unwrap();
|
json!({
|
||||||
|
"relations": [{"from": "Alpha", "to": "Beta", "relation_type": "links_to"}]
|
||||||
|
}),
|
||||||
|
state.clone(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
assert_eq!(del_rel_res, "Relations deleted");
|
assert_eq!(del_rel_res, "Relations deleted");
|
||||||
|
|
||||||
let bcast_handler = AgentSignalsHandler;
|
let bcast_handler = AgentSignalsHandler;
|
||||||
let bcast_res = bcast_handler.execute(json!({
|
let bcast_res = bcast_handler
|
||||||
"action": "broadcast",
|
.execute(
|
||||||
"sender": "agent1",
|
json!({
|
||||||
"signal_type": "task_completed",
|
"action": "broadcast",
|
||||||
"payload": "fix_bug"
|
"sender": "agent1",
|
||||||
}), state.clone()).await.unwrap();
|
"signal_type": "task_completed",
|
||||||
|
"payload": "fix_bug"
|
||||||
|
}),
|
||||||
|
state.clone(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
assert!(bcast_res.contains("Broadcasted signal"));
|
assert!(bcast_res.contains("Broadcasted signal"));
|
||||||
|
|
||||||
let qsignal_handler = AgentSignalsHandler;
|
let qsignal_handler = AgentSignalsHandler;
|
||||||
let qsignal_res = qsignal_handler.execute(json!({
|
let qsignal_res = qsignal_handler
|
||||||
"action": "query",
|
.execute(
|
||||||
"sender": "agent1"
|
json!({
|
||||||
}), state.clone()).await.unwrap();
|
"action": "query",
|
||||||
|
"sender": "agent1"
|
||||||
|
}),
|
||||||
|
state.clone(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
assert!(qsignal_res.contains("task_completed"));
|
assert!(qsignal_res.contains("task_completed"));
|
||||||
let read_paged_handler = ReadGraphHandler;
|
let read_paged_handler = ReadGraphHandler;
|
||||||
let paged_res = read_paged_handler
|
let paged_res = read_paged_handler
|
||||||
@@ -1563,4 +1648,3 @@ mod tests {
|
|||||||
assert_eq!(parsed_paged["has_more"], true);
|
assert_eq!(parsed_paged["has_more"], true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
+24
-22
@@ -38,32 +38,34 @@ impl McpTool for ProcessLogsHandler {
|
|||||||
}
|
}
|
||||||
ProcessLogAction::Get => {
|
ProcessLogAction::Get => {
|
||||||
let max_lines = tool_args.max_lines.unwrap_or(100);
|
let max_lines = tool_args.max_lines.unwrap_or(100);
|
||||||
let result = tokio::task::spawn_blocking(move || -> crate::error::Result<String> {
|
let result =
|
||||||
let mut file = File::open(&safe_path).map_err(|e| {
|
tokio::task::spawn_blocking(move || -> crate::error::Result<String> {
|
||||||
crate::error::AppError::Internal(format!("Failed to open file: {}", e))
|
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 len = file.metadata().map_err(|e| e.to_string())?.len();
|
||||||
|
|
||||||
let read_size = std::cmp::min(16 * 1024, len);
|
let read_size = std::cmp::min(16 * 1024, len);
|
||||||
file.seek(SeekFrom::End(-(read_size as i64)))
|
file.seek(SeekFrom::End(-(read_size as i64)))
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
let mut vec_buf = Vec::new();
|
let mut vec_buf = Vec::new();
|
||||||
file.read_to_end(&mut vec_buf)
|
file.read_to_end(&mut vec_buf).map_err(|e| e.to_string())?;
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
|
|
||||||
let buffer = String::from_utf8_lossy(&vec_buf).to_string();
|
let buffer = String::from_utf8_lossy(&vec_buf).to_string();
|
||||||
let lines: Vec<&str> = buffer.lines().collect();
|
let lines: Vec<&str> = buffer.lines().collect();
|
||||||
let recent_lines = if lines.len() > max_lines {
|
let recent_lines = if lines.len() > max_lines {
|
||||||
lines[lines.len() - max_lines..].join("\n")
|
lines[lines.len() - max_lines..].join("\n")
|
||||||
} else {
|
} else {
|
||||||
buffer
|
buffer
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(recent_lines)
|
Ok(recent_lines)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|e| crate::error::AppError::Internal(format!("Task panic: {}", e)))??;
|
.map_err(|e| {
|
||||||
|
crate::error::AppError::Internal(format!("Task panic: {}", e))
|
||||||
|
})??;
|
||||||
|
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|||||||
+134
-103
@@ -110,11 +110,10 @@ impl McpTool for SearchErrorFixesHandler {
|
|||||||
} else if st_lower.contains(&sol_lower) || sol_lower.contains(&st_lower) {
|
} else if st_lower.contains(&sol_lower) || sol_lower.contains(&st_lower) {
|
||||||
score += 0.5;
|
score += 0.5;
|
||||||
}
|
}
|
||||||
if let Some(emb) = &f.embedding {
|
if let Some(emb) = &f.embedding
|
||||||
if !query_emb.is_empty() {
|
&& !query_emb.is_empty() {
|
||||||
score += crate::embedding::cosine_similarity(&query_emb, emb);
|
score += crate::embedding::cosine_similarity(&query_emb, emb);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
(f, score)
|
(f, score)
|
||||||
})
|
})
|
||||||
.filter(|(_, score)| *score > 0.1)
|
.filter(|(_, score)| *score > 0.1)
|
||||||
@@ -198,11 +197,10 @@ impl McpTool for LogCodeChangeHandler {
|
|||||||
if let Some(range) = &req.line_range {
|
if let Some(range) = &req.line_range {
|
||||||
description = format!("{} [Line Range: {}]", description, range);
|
description = format!("{} [Line Range: {}]", description, range);
|
||||||
}
|
}
|
||||||
if let Some(symbols) = &req.symbol_references {
|
if let Some(symbols) = &req.symbol_references
|
||||||
if !symbols.is_empty() {
|
&& !symbols.is_empty() {
|
||||||
description = format!("{} [Symbols: {}]", description, symbols.join(", "));
|
description = format!("{} [Symbols: {}]", description, symbols.join(", "));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if state.ollama.is_available().await {
|
if state.ollama.is_available().await {
|
||||||
let prompt = format!(
|
let prompt = format!(
|
||||||
@@ -323,13 +321,14 @@ impl McpTool for QueryRecentChangesHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||||
let req: QueryRecentChangesTool = serde_json::from_value(args).unwrap_or(QueryRecentChangesTool {
|
let req: QueryRecentChangesTool =
|
||||||
namespace: None,
|
serde_json::from_value(args).unwrap_or(QueryRecentChangesTool {
|
||||||
repo_name: None,
|
namespace: None,
|
||||||
vcs_type: None,
|
repo_name: None,
|
||||||
limit: None,
|
vcs_type: None,
|
||||||
offset: None,
|
limit: None,
|
||||||
});
|
offset: None,
|
||||||
|
});
|
||||||
let limit = req.limit.unwrap_or(50);
|
let limit = req.limit.unwrap_or(50);
|
||||||
let offset = req.offset.unwrap_or(0);
|
let offset = req.offset.unwrap_or(0);
|
||||||
let data = state.code.ledger.read_with(|l| {
|
let data = state.code.ledger.read_with(|l| {
|
||||||
@@ -337,25 +336,22 @@ impl McpTool for QueryRecentChangesHandler {
|
|||||||
.iter()
|
.iter()
|
||||||
.rev()
|
.rev()
|
||||||
.filter(|c| {
|
.filter(|c| {
|
||||||
if let Some(ns) = &req.namespace {
|
if let Some(ns) = &req.namespace
|
||||||
if !c.namespace.eq_ignore_ascii_case(ns) {
|
&& !c.namespace.eq_ignore_ascii_case(ns) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
if let Some(repo) = &req.repo_name
|
||||||
if let Some(repo) = &req.repo_name {
|
&& c.repo_name
|
||||||
if c.repo_name
|
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|rn| !rn.eq_ignore_ascii_case(repo))
|
.map(|rn| !rn.eq_ignore_ascii_case(repo))
|
||||||
.unwrap_or(true)
|
.unwrap_or(true)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
if let Some(vcs) = &req.vcs_type
|
||||||
if let Some(vcs) = &req.vcs_type {
|
&& !c.effective_vcs().eq_ignore_ascii_case(vcs) {
|
||||||
if !c.effective_vcs().eq_ignore_ascii_case(vcs) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
true
|
true
|
||||||
})
|
})
|
||||||
.skip(offset)
|
.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 req: DecisionsTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||||
let ns = req
|
let ns = req
|
||||||
.namespace
|
.namespace
|
||||||
.unwrap_or_else(|| crate::models::default_namespace());
|
.unwrap_or_else(crate::models::default_namespace);
|
||||||
match req.action {
|
match req.action {
|
||||||
DecisionAction::Log => {
|
DecisionAction::Log => {
|
||||||
let title = req.title.ok_or_else(|| {
|
let title = req.title.ok_or_else(|| {
|
||||||
@@ -464,14 +460,14 @@ impl McpTool for DecisionsHandler {
|
|||||||
state.code.adrs.modify(|adrs| {
|
state.code.adrs.modify(|adrs| {
|
||||||
let target_pos = adrs.iter().position(|a| a.id.eq_ignore_ascii_case(&id));
|
let target_pos = adrs.iter().position(|a| a.id.eq_ignore_ascii_case(&id));
|
||||||
if let Some(pos) = target_pos {
|
if let Some(pos) = target_pos {
|
||||||
if let Some(superseded_id) = &req.supersedes {
|
if let Some(superseded_id) = &req.supersedes
|
||||||
if let Some(s_pos) = adrs.iter().position(|a| a.id.eq_ignore_ascii_case(superseded_id)) {
|
&& let Some(s_pos) = adrs
|
||||||
if s_pos != pos {
|
.iter()
|
||||||
|
.position(|a| a.id.eq_ignore_ascii_case(superseded_id))
|
||||||
|
&& s_pos != pos {
|
||||||
adrs[s_pos].status = "superseded".to_string();
|
adrs[s_pos].status = "superseded".to_string();
|
||||||
adrs_to_index.push(adrs[s_pos].clone());
|
adrs_to_index.push(adrs[s_pos].clone());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let a = &mut adrs[pos];
|
let a = &mut adrs[pos];
|
||||||
if let Some(t) = req.title {
|
if let Some(t) = req.title {
|
||||||
@@ -488,7 +484,9 @@ impl McpTool for DecisionsHandler {
|
|||||||
}
|
}
|
||||||
if let Some(s) = req.status {
|
if let Some(s) = req.status {
|
||||||
let s_lower = s.to_ascii_lowercase();
|
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());
|
a.resolved_at = Some(crate::handlers::utils::now_secs());
|
||||||
} else if s_lower != "implemented" && s_lower != "resolved" {
|
} else if s_lower != "implemented" && s_lower != "resolved" {
|
||||||
a.resolved_at = None;
|
a.resolved_at = None;
|
||||||
@@ -535,11 +533,15 @@ impl McpTool for DecisionsHandler {
|
|||||||
&format!("Updated {}: {}", adr.id, adr.title),
|
&format!("Updated {}: {}", adr.id, adr.title),
|
||||||
Some(&adr.status),
|
Some(&adr.status),
|
||||||
);
|
);
|
||||||
Ok(format!("Updated decision {}: {} (status: {})", adr.id, adr.title, adr.status))
|
Ok(format!(
|
||||||
} else {
|
"Updated decision {}: {} (status: {})",
|
||||||
Err(crate::error::AppError::Internal(
|
adr.id, adr.title, adr.status
|
||||||
format!("Decision with id '{}' not found", id),
|
|
||||||
))
|
))
|
||||||
|
} else {
|
||||||
|
Err(crate::error::AppError::Internal(format!(
|
||||||
|
"Decision with id '{}' not found",
|
||||||
|
id
|
||||||
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
DecisionAction::Query => {
|
DecisionAction::Query => {
|
||||||
@@ -554,9 +556,17 @@ impl McpTool for DecisionsHandler {
|
|||||||
}
|
}
|
||||||
if let Some(q) = &req.query {
|
if let Some(q) = &req.query {
|
||||||
crate::handlers::utils::contains_ignore_ascii_case(&a.title, q)
|
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(
|
||||||
|| crate::handlers::utils::contains_ignore_ascii_case(&a.decision, q)
|
&a.context, q,
|
||||||
|| crate::handlers::utils::contains_ignore_ascii_case(&a.consequence, q)
|
)
|
||||||
|
|| crate::handlers::utils::contains_ignore_ascii_case(
|
||||||
|
&a.decision,
|
||||||
|
q,
|
||||||
|
)
|
||||||
|
|| crate::handlers::utils::contains_ignore_ascii_case(
|
||||||
|
&a.consequence,
|
||||||
|
q,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -632,17 +642,14 @@ impl McpTool for TechDebtHandler {
|
|||||||
let req: TechDebtTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
let req: TechDebtTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||||
let ns = req
|
let ns = req
|
||||||
.namespace
|
.namespace
|
||||||
.unwrap_or_else(|| crate::models::default_namespace());
|
.unwrap_or_else(crate::models::default_namespace);
|
||||||
match req.action {
|
match req.action {
|
||||||
TechDebtAction::Log => {
|
TechDebtAction::Log => {
|
||||||
let desc = req.description.or(req.title).ok_or_else(|| {
|
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())
|
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 ideal = req.ideal_solution.unwrap_or_default();
|
||||||
let text_to_embed = format!(
|
let text_to_embed = format!("Description: {}\nIdeal Solution: {}", desc, ideal);
|
||||||
"Description: {}\nIdeal Solution: {}",
|
|
||||||
desc, ideal
|
|
||||||
);
|
|
||||||
let embedding = crate::embedding::generate_embedding_async(text_to_embed)
|
let embedding = crate::embedding::generate_embedding_async(text_to_embed)
|
||||||
.await
|
.await
|
||||||
.ok();
|
.ok();
|
||||||
@@ -665,18 +672,21 @@ impl McpTool for TechDebtHandler {
|
|||||||
effort_estimate: req.effort_estimate,
|
effort_estimate: req.effort_estimate,
|
||||||
});
|
});
|
||||||
if debts.len() > 300 {
|
if debts.len() > 300 {
|
||||||
let severity_rank = |sev: Option<&str>| match sev.unwrap_or("").to_lowercase().as_str() {
|
let severity_rank =
|
||||||
"critical" => 4,
|
|sev: Option<&str>| match sev.unwrap_or("").to_lowercase().as_str() {
|
||||||
"high" => 3,
|
"critical" => 4,
|
||||||
"medium" => 2,
|
"high" => 3,
|
||||||
"low" => 1,
|
"medium" => 2,
|
||||||
_ => 1,
|
"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 };
|
if let Some((idx_to_remove, _)) =
|
||||||
let sev_score = severity_rank(d.severity.as_deref());
|
debts.iter().enumerate().min_by_key(|(_, d)| {
|
||||||
(status_score + sev_score, d.created_at)
|
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);
|
debts.remove(idx_to_remove);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -712,9 +722,7 @@ impl McpTool for TechDebtHandler {
|
|||||||
let data = state.code.tech_debts.read_with(|debts| {
|
let data = state.code.tech_debts.read_with(|debts| {
|
||||||
let filtered: Vec<_> = debts
|
let filtered: Vec<_> = debts
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|d| {
|
.filter(|d| d.namespace == ns && (inc || !d.is_resolved))
|
||||||
d.namespace == ns && (inc || !d.is_resolved)
|
|
||||||
})
|
|
||||||
.map(|d| match level {
|
.map(|d| match level {
|
||||||
"compact" => serde_json::json!({
|
"compact" => serde_json::json!({
|
||||||
"id": d.id,
|
"id": d.id,
|
||||||
@@ -811,7 +819,8 @@ impl McpTool for OmniSearchHandler {
|
|||||||
let mut ranked_items: Vec<_> = rrf_scores.into_values().collect();
|
let mut ranked_items: Vec<_> = rrf_scores.into_values().collect();
|
||||||
ranked_items.sort_by(|a, b| b.0.total_cmp(&a.0));
|
ranked_items.sort_by(|a, b| b.0.total_cmp(&a.0));
|
||||||
|
|
||||||
let matches: Vec<crate::search::SearchResult> = ranked_items.into_iter().map(|(_, item)| item).collect();
|
let matches: Vec<crate::search::SearchResult> =
|
||||||
|
ranked_items.into_iter().map(|(_, item)| item).collect();
|
||||||
|
|
||||||
let kg_json = state.read_graph(|full| {
|
let kg_json = state.read_graph(|full| {
|
||||||
let mut kg_results = serde_json::Map::new();
|
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_content) = std::fs::read_to_string(target_dir.join("graph.json"))
|
||||||
if let Ok(graph) = serde_json::from_str(&graph_content) {
|
&& let Ok(graph) = serde_json::from_str(&graph_content) {
|
||||||
state.graph.modify(|g| *g = graph);
|
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_content) = std::fs::read_to_string(target_dir.join("tasks.json"))
|
||||||
if let Ok(tasks) = serde_json::from_str(&tasks_content) {
|
&& let Ok(tasks) = serde_json::from_str(&tasks_content) {
|
||||||
state.project.tasks.modify(|t| *t = tasks);
|
state.project.tasks.modify(|t| *t = tasks);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(debts_content) =
|
if let Ok(debts_content) =
|
||||||
std::fs::read_to_string(target_dir.join("tech_debts.json"))
|
std::fs::read_to_string(target_dir.join("tech_debts.json"))
|
||||||
{
|
&& let Ok(debts) = serde_json::from_str(&debts_content) {
|
||||||
if let Ok(debts) = serde_json::from_str(&debts_content) {
|
|
||||||
state.code.tech_debts.modify(|d| *d = debts);
|
state.code.tech_debts.modify(|d| *d = debts);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Ok(format!("Checkpoint '{}' restored successfully.", name))
|
Ok(format!("Checkpoint '{}' restored successfully.", name))
|
||||||
}
|
}
|
||||||
@@ -1290,11 +1295,10 @@ impl McpTool for ManageCheckpointHandler {
|
|||||||
let checkpoints_dir = state.base_dir.join("checkpoints");
|
let checkpoints_dir = state.base_dir.join("checkpoints");
|
||||||
if let Ok(entries) = std::fs::read_dir(&checkpoints_dir) {
|
if let Ok(entries) = std::fs::read_dir(&checkpoints_dir) {
|
||||||
for entry in entries.flatten() {
|
for entry in entries.flatten() {
|
||||||
if entry.path().is_dir() {
|
if entry.path().is_dir()
|
||||||
if let Some(n) = entry.file_name().to_str() {
|
&& let Some(n) = entry.file_name().to_str() {
|
||||||
list.push(serde_json::json!({"type": "checkpoint", "name": n}));
|
list.push(serde_json::json!({"type": "checkpoint", "name": n}));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let snaps = state.project.snapshots.read_with(|snaps| snaps.clone());
|
let snaps = state.project.snapshots.read_with(|snaps| snaps.clone());
|
||||||
@@ -1461,13 +1465,11 @@ impl McpTool for GetNextActionableTasksHandler {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(branch) = &req.git_branch {
|
if let Some(branch) = &req.git_branch
|
||||||
if let Some(tb) = &task.git_branch {
|
&& let Some(tb) = &task.git_branch
|
||||||
if tb != branch {
|
&& tb != branch {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let unblocked = task.dependencies.is_empty()
|
let unblocked = task.dependencies.is_empty()
|
||||||
|| task.dependencies.iter().all(|d| completed_ids.contains(d));
|
|| task.dependencies.iter().all(|d| completed_ids.contains(d));
|
||||||
@@ -1509,7 +1511,9 @@ impl McpTool for HypothesesHandler {
|
|||||||
match req.action {
|
match req.action {
|
||||||
HypothesisAction::Log => {
|
HypothesisAction::Log => {
|
||||||
let hyp_text = req.hypothesis.ok_or_else(|| {
|
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!(
|
let hyp_id = format!(
|
||||||
"HYP-{}",
|
"HYP-{}",
|
||||||
@@ -1536,17 +1540,16 @@ impl McpTool for HypothesesHandler {
|
|||||||
let filtered: Vec<_> = hypotheses
|
let filtered: Vec<_> = hypotheses
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|h| {
|
.filter(|h| {
|
||||||
if let Some(tid) = &req.task_id {
|
if let Some(tid) = &req.task_id
|
||||||
if h.task_id.as_ref() != Some(tid) {
|
&& h.task_id.as_ref() != Some(tid) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if let Some(q) = &req.query {
|
if let Some(q) = &req.query {
|
||||||
let lq = q.to_lowercase();
|
let lq = q.to_lowercase();
|
||||||
return h.hypothesis.to_lowercase().contains(&lq)
|
return h.hypothesis.to_lowercase().contains(&lq)
|
||||||
|| h.evidence
|
|| h.evidence
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map_or(false, |e| e.to_lowercase().contains(&lq));
|
.is_some_and(|e| e.to_lowercase().contains(&lq));
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
})
|
})
|
||||||
@@ -1656,13 +1659,19 @@ impl McpTool for AgentSignalsHandler {
|
|||||||
match req.action {
|
match req.action {
|
||||||
AgentSignalAction::Broadcast => {
|
AgentSignalAction::Broadcast => {
|
||||||
let sender = req.sender.ok_or_else(|| {
|
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(|| {
|
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(|| {
|
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()
|
let timestamp = std::time::SystemTime::now()
|
||||||
@@ -1715,25 +1724,21 @@ impl McpTool for AgentSignalsHandler {
|
|||||||
signals
|
signals
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|s| {
|
.filter(|s| {
|
||||||
if let Some(ttl) = s.ttl_seconds {
|
if let Some(ttl) = s.ttl_seconds
|
||||||
if now > s.timestamp + ttl {
|
&& now > s.timestamp + ttl {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
if let Some(sender) = &req.sender
|
||||||
if let Some(sender) = &req.sender {
|
&& s.sender.to_lowercase() != sender.to_lowercase() {
|
||||||
if s.sender.to_lowercase() != sender.to_lowercase() {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
if let Some(st) = &req.signal_type
|
||||||
if let Some(st) = &req.signal_type {
|
&& s.signal_type.to_lowercase() != st.to_lowercase() {
|
||||||
if s.signal_type.to_lowercase() != st.to_lowercase() {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
true
|
true
|
||||||
})
|
})
|
||||||
.cloned()
|
.take(req.limit.unwrap_or(20)).cloned()
|
||||||
.take(req.limit.unwrap_or(20))
|
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2110,7 +2115,10 @@ mod tests {
|
|||||||
assert!(q_dec_res.contains("Fast async API routing"));
|
assert!(q_dec_res.contains("Fast async API routing"));
|
||||||
|
|
||||||
let q_by_consequence = handler_dec
|
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
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(q_by_consequence.contains("Use Axum"));
|
assert!(q_by_consequence.contains("Use Axum"));
|
||||||
@@ -2133,7 +2141,10 @@ mod tests {
|
|||||||
assert!(update_res.contains("implemented"));
|
assert!(update_res.contains("implemented"));
|
||||||
|
|
||||||
let q_after_update = handler_dec
|
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
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(q_after_update.contains("implemented"));
|
assert!(q_after_update.contains("implemented"));
|
||||||
@@ -2143,7 +2154,10 @@ mod tests {
|
|||||||
assert!(q_after_update.contains("resolved_at"));
|
assert!(q_after_update.contains("resolved_at"));
|
||||||
|
|
||||||
let del_dec_res = handler_dec
|
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;
|
.await;
|
||||||
assert!(del_dec_res.is_ok());
|
assert!(del_dec_res.is_ok());
|
||||||
|
|
||||||
@@ -2163,14 +2177,20 @@ mod tests {
|
|||||||
assert!(td_res.contains("Tech debt logged"));
|
assert!(td_res.contains("Tech debt logged"));
|
||||||
|
|
||||||
let list_td_res = handler_td
|
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
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(list_td_res.contains("Replace unwraps"));
|
assert!(list_td_res.contains("Replace unwraps"));
|
||||||
|
|
||||||
let debt_id = state.code.tech_debts.read_with(|debts| debts[0].id.clone());
|
let debt_id = state.code.tech_debts.read_with(|debts| debts[0].id.clone());
|
||||||
let res_td_res = handler_td
|
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;
|
.await;
|
||||||
assert!(res_td_res.is_ok());
|
assert!(res_td_res.is_ok());
|
||||||
|
|
||||||
@@ -2394,7 +2414,10 @@ mod tests {
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.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
|
let mg_list = mg_chk
|
||||||
.execute(serde_json::json!({"action": "list"}), state.clone())
|
.execute(serde_json::json!({"action": "list"}), state.clone())
|
||||||
@@ -2607,7 +2630,10 @@ mod tests {
|
|||||||
|
|
||||||
// DeleteDecision non-existent
|
// DeleteDecision non-existent
|
||||||
let del_dec_err = handler_dec
|
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;
|
.await;
|
||||||
assert!(del_dec_err.is_err());
|
assert!(del_dec_err.is_err());
|
||||||
|
|
||||||
@@ -2882,9 +2908,14 @@ mod tests {
|
|||||||
|
|
||||||
state.code.tech_debts.read_with(|debts| {
|
state.code.tech_debts.read_with(|debts| {
|
||||||
assert_eq!(debts.len(), 300);
|
assert_eq!(debts.len(), 300);
|
||||||
assert!(debts.iter().any(|d| d.id == "critical-debt"), "Critical unresolved debt must be retained");
|
assert!(
|
||||||
assert!(!debts.iter().any(|d| d.id == "resolved-debt"), "Resolved debt should have been evicted first");
|
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"
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -10,4 +10,3 @@ pub mod tasks;
|
|||||||
pub mod utils;
|
pub mod utils;
|
||||||
pub mod vision;
|
pub mod vision;
|
||||||
pub mod workspaces;
|
pub mod workspaces;
|
||||||
|
|
||||||
@@ -29,7 +29,9 @@ impl McpTool for HandoffMemosHandler {
|
|||||||
let content = req.content.ok_or_else(|| {
|
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())
|
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| {
|
state.telemetry.handoff_memos.modify(|memos| {
|
||||||
memos.push(crate::models::HandoffMemo {
|
memos.push(crate::models::HandoffMemo {
|
||||||
id: uuid::Uuid::new_v4().to_string(),
|
id: uuid::Uuid::new_v4().to_string(),
|
||||||
@@ -109,11 +111,13 @@ impl McpTool for AddSessionSummaryHandler {
|
|||||||
});
|
});
|
||||||
if summaries.len() > 200 {
|
if summaries.len() > 200 {
|
||||||
let now = crate::handlers::utils::now_secs();
|
let now = crate::handlers::utils::now_secs();
|
||||||
if let Some((idx_to_remove, _)) = summaries.iter().enumerate().min_by_key(|(_, s)| {
|
if let Some((idx_to_remove, _)) =
|
||||||
let is_expired = s.expires_at.map(|exp| exp < now).unwrap_or(false);
|
summaries.iter().enumerate().min_by_key(|(_, s)| {
|
||||||
let status_score = if is_expired { 0 } else { 1 };
|
let is_expired = s.expires_at.map(|exp| exp < now).unwrap_or(false);
|
||||||
(status_score, s.timestamp)
|
let status_score = if is_expired { 0 } else { 1 };
|
||||||
}) {
|
(status_score, s.timestamp)
|
||||||
|
})
|
||||||
|
{
|
||||||
summaries.remove(idx_to_remove);
|
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 cutoff = crate::handlers::utils::now_secs().saturating_sub(req.hours_lookback * 3600);
|
||||||
|
|
||||||
let filtered_tasks = state.project.tasks.read_with(|items| {
|
let filtered_tasks = state.project.tasks.read_with(|items| {
|
||||||
items.iter().filter(|t| t.updated_at >= cutoff).cloned().collect::<Vec<_>>()
|
items
|
||||||
|
.iter()
|
||||||
|
.filter(|t| t.updated_at >= cutoff)
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<_>>()
|
||||||
});
|
});
|
||||||
let filtered_changes = state.code.ledger.read_with(|changes| {
|
let filtered_changes = state.code.ledger.read_with(|changes| {
|
||||||
changes.iter().filter(|c| c.timestamp >= cutoff).cloned().collect::<Vec<_>>()
|
changes
|
||||||
|
.iter()
|
||||||
|
.filter(|c| c.timestamp >= cutoff)
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<_>>()
|
||||||
});
|
});
|
||||||
let filtered_summaries = state.telemetry.session_summaries.read_with(|summaries| {
|
let filtered_summaries = state.telemetry.session_summaries.read_with(|summaries| {
|
||||||
summaries.iter().filter(|s| s.namespace == req.namespace && s.timestamp >= cutoff).cloned().collect::<Vec<_>>()
|
summaries
|
||||||
|
.iter()
|
||||||
|
.filter(|s| s.namespace == req.namespace && s.timestamp >= cutoff)
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<_>>()
|
||||||
});
|
});
|
||||||
|
|
||||||
let report = serde_json::json!({
|
let report = serde_json::json!({
|
||||||
@@ -178,8 +194,7 @@ impl McpTool for PromoteToEntityHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||||
let req: PromoteToEntityTool =
|
let req: PromoteToEntityTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
|
||||||
|
|
||||||
let entity_name = req.entity_name.clone();
|
let entity_name = req.entity_name.clone();
|
||||||
let obs = req.content.clone();
|
let obs = req.content.clone();
|
||||||
@@ -236,7 +251,10 @@ mod tests {
|
|||||||
assert_eq!(res, "Handoff memo left.");
|
assert_eq!(res, "Handoff memo left.");
|
||||||
|
|
||||||
let res2 = handoff_handler
|
let res2 = handoff_handler
|
||||||
.execute(json!({"action": "read", "namespace": "global"}), state.clone())
|
.execute(
|
||||||
|
json!({"action": "read", "namespace": "global"}),
|
||||||
|
state.clone(),
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -266,5 +284,3 @@ mod tests {
|
|||||||
assert!(!res4.is_empty());
|
assert!(!res4.is_empty());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -39,7 +39,9 @@ pub fn extract_adrs(text: &str) -> Vec<String> {
|
|||||||
if i + 3 <= len && lower_chars[i..i + 3] == ['a', 'd', 'r'] {
|
if i + 3 <= len && lower_chars[i..i + 3] == ['a', 'd', 'r'] {
|
||||||
let mut cursor = i + 3;
|
let mut cursor = i + 3;
|
||||||
// Skip optional '-', ':', or whitespace
|
// Skip optional '-', ':', or whitespace
|
||||||
while cursor < len && (chars[cursor] == '-' || chars[cursor] == ':' || chars[cursor] == ' ') {
|
while cursor < len
|
||||||
|
&& (chars[cursor] == '-' || chars[cursor] == ':' || chars[cursor] == ' ')
|
||||||
|
{
|
||||||
cursor += 1;
|
cursor += 1;
|
||||||
}
|
}
|
||||||
// Collect consecutive digits
|
// Collect consecutive digits
|
||||||
@@ -80,11 +82,17 @@ pub fn extract_tech_debts(text: &str) -> Vec<String> {
|
|||||||
while i < len {
|
while i < len {
|
||||||
if i + 2 <= len && lower_chars[i..i + 2] == ['t', 'd'] {
|
if i + 2 <= len && lower_chars[i..i + 2] == ['t', 'd'] {
|
||||||
let mut cursor = i + 2;
|
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;
|
cursor += 1;
|
||||||
}
|
}
|
||||||
let start = cursor;
|
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;
|
cursor += 1;
|
||||||
}
|
}
|
||||||
if cursor > start {
|
if cursor > start {
|
||||||
@@ -117,11 +125,17 @@ pub fn extract_tasks(text: &str) -> Vec<String> {
|
|||||||
while i < len {
|
while i < len {
|
||||||
if i + 4 <= len && lower_chars[i..i + 4] == ['t', 'a', 's', 'k'] {
|
if i + 4 <= len && lower_chars[i..i + 4] == ['t', 'a', 's', 'k'] {
|
||||||
let mut cursor = i + 4;
|
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;
|
cursor += 1;
|
||||||
}
|
}
|
||||||
let start = cursor;
|
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;
|
cursor += 1;
|
||||||
}
|
}
|
||||||
if cursor > start {
|
if cursor > start {
|
||||||
@@ -162,8 +176,7 @@ pub fn cascade_unblock_tasks(state: &MemoryState) -> Vec<String> {
|
|||||||
} else {
|
} else {
|
||||||
let all_deps_completed = t.dependencies.iter().all(|dep_id| {
|
let all_deps_completed = t.dependencies.iter().all(|dep_id| {
|
||||||
tasks_snapshot.iter().any(|other| {
|
tasks_snapshot.iter().any(|other| {
|
||||||
(other.id == *dep_id || other.title == *dep_id)
|
(other.id == *dep_id || other.title == *dep_id) && !other.is_active()
|
||||||
&& !other.is_active()
|
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
if all_deps_completed {
|
if all_deps_completed {
|
||||||
@@ -226,19 +239,17 @@ pub fn recalculate_milestones(state: &MemoryState) -> Vec<String> {
|
|||||||
}
|
}
|
||||||
updated.push(m.id.clone());
|
updated.push(m.id.clone());
|
||||||
}
|
}
|
||||||
} else if completed > 0
|
} else if (completed > 0
|
||||||
|| tasks_snapshot.iter().any(|t| {
|
|| tasks_snapshot.iter().any(|t| {
|
||||||
m.task_ids.iter().any(|tid| tid == &t.id || tid == &t.title)
|
m.task_ids.iter().any(|tid| tid == &t.id || tid == &t.title)
|
||||||
&& (t.status == "in_progress" || t.status == "active")
|
&& (t.status == "in_progress" || t.status == "active")
|
||||||
})
|
}))
|
||||||
{
|
&& !m.status.eq_ignore_ascii_case("in_progress")
|
||||||
if !m.status.eq_ignore_ascii_case("in_progress")
|
|
||||||
&& !m.status.eq_ignore_ascii_case("completed")
|
&& !m.status.eq_ignore_ascii_case("completed")
|
||||||
{
|
{
|
||||||
m.status = "in_progress".to_string();
|
m.status = "in_progress".to_string();
|
||||||
updated.push(m.id.clone());
|
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) {
|
if adr.id.eq_ignore_ascii_case(cand) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if let (Some(n1), Some(n2)) = (adr_numeric_id(&adr.id), adr_numeric_id(cand)) {
|
if let (Some(n1), Some(n2)) = (adr_numeric_id(&adr.id), adr_numeric_id(cand))
|
||||||
if n1 == n2 {
|
&& n1 == n2 {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
false
|
false
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -324,7 +334,9 @@ pub async fn reconcile_commit_or_code_change(
|
|||||||
if d.is_resolved {
|
if d.is_resolved {
|
||||||
continue;
|
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
|
let matches_file = file_path
|
||||||
.map(|fp| d.file_path.as_deref() == Some(fp))
|
.map(|fp| d.file_path.as_deref() == Some(fp))
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
|
|||||||
@@ -366,14 +366,14 @@ impl McpTool for TasksHandler {
|
|||||||
let mut updated_task: Option<Task> = None;
|
let mut updated_task: Option<Task> = None;
|
||||||
state.project.tasks.modify(|tasks| {
|
state.project.tasks.modify(|tasks| {
|
||||||
if let Some(task) = tasks.iter_mut().find(|t| t.id == id || t.title == id) {
|
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 {
|
if ac.is_met {
|
||||||
already_met = true;
|
already_met = true;
|
||||||
} else {
|
} else {
|
||||||
ac.is_met = true;
|
ac.is_met = true;
|
||||||
success = true;
|
success = true;
|
||||||
task.updated_at = crate::handlers::utils::now_secs();
|
task.updated_at = crate::handlers::utils::now_secs();
|
||||||
updated_task = Some(task.clone());
|
updated_task = Some(task.clone());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
task.acceptance_criteria
|
task.acceptance_criteria
|
||||||
@@ -444,7 +444,7 @@ impl McpTool for MilestonesHandler {
|
|||||||
})?;
|
})?;
|
||||||
let ns = req
|
let ns = req
|
||||||
.namespace
|
.namespace
|
||||||
.unwrap_or_else(|| crate::models::default_namespace());
|
.unwrap_or_else(crate::models::default_namespace);
|
||||||
state.project.milestones.modify(|ms| {
|
state.project.milestones.modify(|ms| {
|
||||||
ms.push(crate::models::Milestone {
|
ms.push(crate::models::Milestone {
|
||||||
id: uuid::Uuid::new_v4().to_string(),
|
id: uuid::Uuid::new_v4().to_string(),
|
||||||
@@ -477,16 +477,16 @@ impl McpTool for MilestonesHandler {
|
|||||||
for m in ms.iter_mut() {
|
for m in ms.iter_mut() {
|
||||||
if m.id == id {
|
if m.id == id {
|
||||||
m.status = status.clone();
|
m.status = status.clone();
|
||||||
if status.eq_ignore_ascii_case("completed") || status.eq_ignore_ascii_case("done") {
|
if (status.eq_ignore_ascii_case("completed")
|
||||||
if m.completed_at.is_none() {
|
|| status.eq_ignore_ascii_case("done"))
|
||||||
|
&& m.completed_at.is_none() {
|
||||||
m.completed_at = Some(crate::handlers::utils::now_secs());
|
m.completed_at = Some(crate::handlers::utils::now_secs());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if let Some(task_ids) = &req.task_ids {
|
if let Some(task_ids) = &req.task_ids {
|
||||||
m.task_ids = task_ids.clone();
|
m.task_ids = task_ids.clone();
|
||||||
if let Some(deliverables) = &req.deliverables {
|
if let Some(deliverables) = &req.deliverables {
|
||||||
m.deliverables = deliverables.clone();
|
m.deliverables = deliverables.clone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
found = true;
|
found = true;
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -191,11 +191,10 @@ pub fn html_to_markdown(html: &str) -> String {
|
|||||||
out.push_str("* ");
|
out.push_str("* ");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"/li" => {
|
"/li"
|
||||||
if !in_cell {
|
if !in_cell => {
|
||||||
out.push('\n');
|
out.push('\n');
|
||||||
}
|
}
|
||||||
}
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
} else if c == '&' {
|
} else if c == '&' {
|
||||||
@@ -298,38 +297,46 @@ pub async fn ensure_ocr_engine() -> Option<&'static OcrEngine> {
|
|||||||
if let Some(engine) = OCR_ENGINE.get() {
|
if let Some(engine) = OCR_ENGINE.get() {
|
||||||
return Some(engine);
|
return Some(engine);
|
||||||
}
|
}
|
||||||
|
|
||||||
let cache_dir = dirs::home_dir()?.join(".gemini/mcp_memory/models");
|
let cache_dir = dirs::home_dir()?.join(".gemini/mcp_memory/models");
|
||||||
let _ = std::fs::create_dir_all(&cache_dir);
|
let _ = std::fs::create_dir_all(&cache_dir);
|
||||||
|
|
||||||
let det_path = cache_dir.join("text-detection.rten");
|
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 {
|
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) => {
|
Ok(resp) => {
|
||||||
if let Ok(bytes) = resp.bytes().await {
|
if let Ok(bytes) = resp.bytes().await {
|
||||||
if let Err(e) = std::fs::write(&det_path, &bytes) {
|
if let Err(e) = std::fs::write(&det_path, &bytes) {
|
||||||
tracing::error!("Failed to write text-detection.rten to disk: {}", e);
|
tracing::error!("Failed to write text-detection.rten to disk: {}", e);
|
||||||
}
|
}
|
||||||
} else {
|
} 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),
|
Err(e) => tracing::error!("Failed to download text-detection.rten: {}", e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let rec_path = cache_dir.join("text-recognition.rten");
|
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 {
|
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) => {
|
Ok(resp) => {
|
||||||
if let Ok(bytes) = resp.bytes().await {
|
if let Ok(bytes) = resp.bytes().await {
|
||||||
if let Err(e) = std::fs::write(&rec_path, &bytes) {
|
if let Err(e) = std::fs::write(&rec_path, &bytes) {
|
||||||
tracing::error!("Failed to write text-recognition.rten to disk: {}", e);
|
tracing::error!("Failed to write text-recognition.rten to disk: {}", e);
|
||||||
}
|
}
|
||||||
} else {
|
} 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),
|
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<String> {
|
pub fn perform_ocrs_ocr(engine: &OcrEngine, img: &image::DynamicImage) -> Option<String> {
|
||||||
let rgb_img = img.to_rgb8();
|
let rgb_img = img.to_rgb8();
|
||||||
let img_source = ocrs::ImageSource::from_bytes(
|
let img_source = ocrs::ImageSource::from_bytes(rgb_img.as_raw(), rgb_img.dimensions()).ok()?;
|
||||||
rgb_img.as_raw(),
|
|
||||||
rgb_img.dimensions(),
|
|
||||||
).ok()?;
|
|
||||||
|
|
||||||
let ocr_input = engine.prepare_input(img_source).ok()?;
|
let ocr_input = engine.prepare_input(img_source).ok()?;
|
||||||
let word_rects = engine.detect_words(&ocr_input).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_str(&text.to_string());
|
||||||
out.push('\n');
|
out.push('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
let trimmed = out.trim();
|
let trimmed = out.trim();
|
||||||
if trimmed.is_empty() {
|
if trimmed.is_empty() {
|
||||||
None
|
None
|
||||||
@@ -482,23 +486,22 @@ pub async fn process_clipboard_snapshot(state: Arc<MemoryState>) {
|
|||||||
|
|
||||||
if let Some(engine) = ensure_ocr_engine().await {
|
if let Some(engine) = ensure_ocr_engine().await {
|
||||||
let img_clone = img.clone();
|
let img_clone = img.clone();
|
||||||
ocr_text_opt = tokio::task::spawn_blocking(move || {
|
ocr_text_opt =
|
||||||
perform_ocrs_ocr(engine, &img_clone)
|
tokio::task::spawn_blocking(move || perform_ocrs_ocr(engine, &img_clone))
|
||||||
})
|
.await
|
||||||
.await
|
.ok()
|
||||||
.ok()
|
.flatten();
|
||||||
.flatten();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut cache = state.clipboard_cache.write().await;
|
let mut cache = state.clipboard_cache.write().await;
|
||||||
|
|
||||||
// Check if this is an identical image captured rapidly
|
// Check if this is an identical image captured rapidly
|
||||||
let is_duplicate = if let Some(ref prev) = cache.last_image {
|
let is_duplicate = if let Some(ref prev) = cache.last_image {
|
||||||
prev.width == w && prev.height == h && prev.size_bytes == size_bytes
|
prev.width == w && prev.height == h && prev.size_bytes == size_bytes
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
};
|
};
|
||||||
|
|
||||||
if !is_duplicate {
|
if !is_duplicate {
|
||||||
// Index OCR text into Tantivy for omni_search
|
// Index OCR text into Tantivy for omni_search
|
||||||
if let Some(ref text) = ocr_text_opt {
|
if let Some(ref text) = ocr_text_opt {
|
||||||
@@ -563,11 +566,10 @@ pub async fn process_clipboard_snapshot(state: Arc<MemoryState>) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let mut cache = state.clipboard_cache.write().await;
|
let mut cache = state.clipboard_cache.write().await;
|
||||||
if let Some(ref prev) = cache.last_text {
|
if let Some(ref prev) = cache.last_text
|
||||||
if prev.text == cached.text {
|
&& prev.text == cached.text {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
cache.last_text = Some(cached.clone());
|
cache.last_text = Some(cached.clone());
|
||||||
cache
|
cache
|
||||||
.history
|
.history
|
||||||
@@ -669,7 +671,7 @@ pub fn spawn_clipboard_listener(state: Arc<MemoryState>) {
|
|||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
while let Some(()) = rx.recv().await {
|
while let Some(()) = rx.recv().await {
|
||||||
tokio::time::sleep(tokio::time::Duration::from_millis(150)).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;
|
process_clipboard_snapshot(Arc::clone(&state)).await;
|
||||||
}
|
}
|
||||||
@@ -843,8 +845,8 @@ impl McpTool for ClipboardHandler {
|
|||||||
ClipboardAction::Read => {
|
ClipboardAction::Read => {
|
||||||
let engine = ensure_ocr_engine().await;
|
let engine = ensure_ocr_engine().await;
|
||||||
|
|
||||||
let out =
|
let out = tokio::task::spawn_blocking(
|
||||||
tokio::task::spawn_blocking(move || -> crate::error::Result<serde_json::Map<String, Value>> {
|
move || -> crate::error::Result<serde_json::Map<String, Value>> {
|
||||||
let mut out = serde_json::Map::new();
|
let mut out = serde_json::Map::new();
|
||||||
|
|
||||||
if let Some(text) = get_native_clipboard_text() {
|
if let Some(text) = get_native_clipboard_text() {
|
||||||
@@ -860,8 +862,19 @@ impl McpTool for ClipboardHandler {
|
|||||||
let rgb_img = img.into_rgb8();
|
let rgb_img = img.into_rgb8();
|
||||||
|
|
||||||
let mut jpeg_bytes = std::io::Cursor::new(Vec::new());
|
let mut jpeg_bytes = std::io::Cursor::new(Vec::new());
|
||||||
let mut encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut jpeg_bytes, 88);
|
let mut encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(
|
||||||
if encoder.encode(&rgb_img, rgb_img.width(), rgb_img.height(), image::ExtendedColorType::Rgb8).is_ok() {
|
&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 bytes = jpeg_bytes.into_inner();
|
||||||
|
|
||||||
let cache_dir = dirs::home_dir()
|
let cache_dir = dirs::home_dir()
|
||||||
@@ -876,17 +889,20 @@ impl McpTool for ClipboardHandler {
|
|||||||
out.insert("image_path_wsl".into(), json!(wsl_path));
|
out.insert("image_path_wsl".into(), json!(wsl_path));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(eng) = engine {
|
if let Some(eng) = engine
|
||||||
if let Some(ocr_text) = perform_ocrs_ocr(eng, &dynamic_img) {
|
&& let Some(ocr_text) = perform_ocrs_ocr(eng, &dynamic_img) {
|
||||||
out.insert("image_analysis".to_string(), json!(ocr_text.trim()));
|
out.insert(
|
||||||
|
"image_analysis".to_string(),
|
||||||
|
json!(ocr_text.trim()),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Ok(out)
|
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);
|
state.record_activity("clipboard", "Read contents from OS clipboard", None);
|
||||||
Ok::<String, crate::error::AppError>(serde_json::to_string_pretty(&Value::Object(
|
Ok::<String, crate::error::AppError>(serde_json::to_string_pretty(&Value::Object(
|
||||||
|
|||||||
@@ -6,14 +6,6 @@ use async_trait::async_trait;
|
|||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
pub struct SnippetsHandler;
|
pub struct SnippetsHandler;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -57,7 +49,7 @@ impl McpTool for SnippetsHandler {
|
|||||||
embedding,
|
embedding,
|
||||||
namespace: req
|
namespace: req
|
||||||
.namespace
|
.namespace
|
||||||
.unwrap_or_else(|| crate::models::default_namespace()),
|
.unwrap_or_else(crate::models::default_namespace),
|
||||||
repo_name: req.repo_name,
|
repo_name: req.repo_name,
|
||||||
origin_file: req.origin_file,
|
origin_file: req.origin_file,
|
||||||
line_range: req.line_range,
|
line_range: req.line_range,
|
||||||
@@ -89,8 +81,9 @@ impl McpTool for SnippetsHandler {
|
|||||||
let mut scored: Vec<_> = snippets
|
let mut scored: Vec<_> = snippets
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|s| {
|
.filter_map(|s| {
|
||||||
let text = format!("{} {} {} {}", s.name, s.description, s.language, s.code)
|
let text =
|
||||||
.to_lowercase();
|
format!("{} {} {} {}", s.name, s.description, s.language, s.code)
|
||||||
|
.to_lowercase();
|
||||||
let mut score = 0.0;
|
let mut score = 0.0;
|
||||||
for tok in &q_tokens {
|
for tok in &q_tokens {
|
||||||
if text.contains(tok) {
|
if text.contains(tok) {
|
||||||
@@ -143,9 +136,9 @@ impl McpTool for SnippetsHandler {
|
|||||||
|| s.tags.iter().any(|t| contains_ignore_ascii_case(t, &query));
|
|| s.tags.iter().any(|t| contains_ignore_ascii_case(t, &query));
|
||||||
|
|
||||||
let matches_tags = tags_filter.is_empty()
|
let matches_tags = tags_filter.is_empty()
|
||||||
|| tags_filter
|
|| tags_filter.iter().any(|tf| {
|
||||||
.iter()
|
s.tags.iter().any(|st| contains_ignore_ascii_case(st, tf))
|
||||||
.any(|tf| s.tags.iter().any(|st| contains_ignore_ascii_case(st, tf)));
|
});
|
||||||
|
|
||||||
matches_text && matches_tags
|
matches_text && matches_tags
|
||||||
})
|
})
|
||||||
@@ -352,13 +345,28 @@ impl McpTool for SemanticCodeSearchHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut out = format!("Semantic Search Results for '{}':\n", tool_args.query);
|
let mut out = format!("Semantic Search Results for '{}':\n", tool_args.query);
|
||||||
for res in results {
|
for res in &results {
|
||||||
out.push_str(&format!(
|
out.push_str(&format!(
|
||||||
"- [{:.2}] {}: {}\n",
|
"- [{:.2}] {}: {}\n",
|
||||||
res.score, res.title, res.body
|
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)
|
Ok(out)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+122
-38
@@ -4,6 +4,7 @@
|
|||||||
)]
|
)]
|
||||||
|
|
||||||
pub mod api;
|
pub mod api;
|
||||||
|
pub mod config;
|
||||||
pub mod db;
|
pub mod db;
|
||||||
pub mod embedding;
|
pub mod embedding;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
@@ -105,11 +106,10 @@ pub async fn ttl_sweeper_worker(state: Arc<MemoryState>) {
|
|||||||
|
|
||||||
state.project.tasks.read_with(|tasks| {
|
state.project.tasks.read_with(|tasks| {
|
||||||
for t in tasks.iter() {
|
for t in tasks.iter() {
|
||||||
if let Some(exp) = t.expires_at {
|
if let Some(exp) = t.expires_at
|
||||||
if t.is_active() {
|
&& t.is_active() {
|
||||||
next_expiry = Some(next_expiry.map_or(exp, |curr| curr.min(exp)));
|
next_expiry = Some(next_expiry.map_or(exp, |curr| curr.min(exp)));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -160,13 +160,12 @@ pub async fn ttl_sweeper_worker(state: Arc<MemoryState>) {
|
|||||||
let mut expired_tasks = Vec::new();
|
let mut expired_tasks = Vec::new();
|
||||||
state.project.tasks.modify(|tasks| {
|
state.project.tasks.modify(|tasks| {
|
||||||
for t in tasks.iter_mut() {
|
for t in tasks.iter_mut() {
|
||||||
if let Some(exp) = t.expires_at {
|
if let Some(exp) = t.expires_at
|
||||||
if exp <= now && t.is_active() {
|
&& exp <= now && t.is_active() {
|
||||||
t.status = "expired".to_string();
|
t.status = "expired".to_string();
|
||||||
t.updated_at = now;
|
t.updated_at = now;
|
||||||
expired_tasks.push(t.id.clone());
|
expired_tasks.push(t.id.clone());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -249,8 +248,8 @@ pub async fn condense_graph_worker(state: Arc<MemoryState>) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some((content, names)) = snippet_condensation {
|
if let Some((content, names)) = snippet_condensation
|
||||||
if !content.is_empty() {
|
&& !content.is_empty() {
|
||||||
let name = format!("Snippet History {}", now);
|
let name = format!("Snippet History {}", now);
|
||||||
state.modify_graph(|graph| {
|
state.modify_graph(|graph| {
|
||||||
graph.entities.insert(
|
graph.entities.insert(
|
||||||
@@ -271,6 +270,84 @@ pub async fn condense_graph_worker(state: Arc<MemoryState>) {
|
|||||||
});
|
});
|
||||||
tracing::info!("Condensed snippets into Historical Summary.");
|
tracing::info!("Condensed snippets into Historical Summary.");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn memory_consolidation_worker(state: Arc<MemoryState>) {
|
||||||
|
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::<Vec<String>>(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<MemoryState>) -> Result<(), Box<dyn std::erro
|
|||||||
tokio::spawn(index_committer_worker(Arc::clone(&state)));
|
tokio::spawn(index_committer_worker(Arc::clone(&state)));
|
||||||
tokio::spawn(ttl_sweeper_worker(Arc::clone(&state)));
|
tokio::spawn(ttl_sweeper_worker(Arc::clone(&state)));
|
||||||
tokio::spawn(condense_graph_worker(Arc::clone(&state)));
|
tokio::spawn(condense_graph_worker(Arc::clone(&state)));
|
||||||
|
tokio::spawn(memory_consolidation_worker(Arc::clone(&state)));
|
||||||
crate::watcher::spawn_watcher(Arc::clone(&state));
|
crate::watcher::spawn_watcher(Arc::clone(&state));
|
||||||
crate::handlers::vision::spawn_clipboard_listener(Arc::clone(&state));
|
crate::handlers::vision::spawn_clipboard_listener(Arc::clone(&state));
|
||||||
let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel();
|
let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel();
|
||||||
@@ -337,7 +415,7 @@ pub async fn run_server(state: Arc<MemoryState>) -> Result<(), Box<dyn std::erro
|
|||||||
if let Ok(socket) = tokio::net::UdpSocket::bind(format!("127.0.0.1:{}", port1)).await {
|
if let Ok(socket) = tokio::net::UdpSocket::bind(format!("127.0.0.1:{}", port1)).await {
|
||||||
let socket = Arc::new(socket);
|
let socket = Arc::new(socket);
|
||||||
let socket_rx = socket.clone();
|
let socket_rx = socket.clone();
|
||||||
|
|
||||||
let mut subscribers: HashMap<(String, String), std::net::SocketAddr> = HashMap::new();
|
let mut subscribers: HashMap<(String, String), std::net::SocketAddr> = HashMap::new();
|
||||||
let mut buf = vec![0u8; 65536];
|
let mut buf = vec![0u8; 65536];
|
||||||
let mut event_rx = udp_state.handler.state.event_bus_tx.subscribe();
|
let mut event_rx = udp_state.handler.state.event_bus_tx.subscribe();
|
||||||
@@ -371,32 +449,29 @@ pub async fn run_server(state: Arc<MemoryState>) -> Result<(), Box<dyn std::erro
|
|||||||
} else if let Ok(json_payload) = serde_json::from_slice::<serde_json::Value>(&buf[..len]) {
|
} else if let Ok(json_payload) = serde_json::from_slice::<serde_json::Value>(&buf[..len]) {
|
||||||
if json_payload.get("type").and_then(|t| t.as_str()) == Some("ping") {
|
if json_payload.get("type").and_then(|t| t.as_str()) == Some("ping") {
|
||||||
let _ = socket.send_to(b"pong", addr).await;
|
let _ = socket.send_to(b"pong", addr).await;
|
||||||
} else if json_payload.get("type").and_then(|t| t.as_str()) == Some("gate_wait") {
|
} else if json_payload.get("type").and_then(|t| t.as_str()) == Some("gate_wait")
|
||||||
if let (Some(action), Some(target)) = (
|
&& let (Some(action), Some(target)) = (
|
||||||
json_payload.get("action").and_then(|a| a.as_str()),
|
json_payload.get("action").and_then(|a| a.as_str()),
|
||||||
json_payload.get("target").and_then(|t| t.as_str())
|
json_payload.get("target").and_then(|t| t.as_str())
|
||||||
) {
|
) {
|
||||||
subscribers.insert((action.to_string(), target.to_string()), addr);
|
subscribers.insert((action.to_string(), target.to_string()), addr);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(event) = event_rx.recv() => {
|
Ok(event) = event_rx.recv() => {
|
||||||
if event.topic == "gate:event" {
|
if event.topic == "gate:event"
|
||||||
if let (Some(action), Some(target), Some(status)) = (
|
&& let (Some(action), Some(target), Some(status)) = (
|
||||||
event.payload.get("action").and_then(|a| a.as_str()),
|
event.payload.get("action").and_then(|a| a.as_str()),
|
||||||
event.payload.get("target").and_then(|t| t.as_str()),
|
event.payload.get("target").and_then(|t| t.as_str()),
|
||||||
event.payload.get("status").and_then(|s| s.as_str()),
|
event.payload.get("status").and_then(|s| s.as_str()),
|
||||||
) {
|
) {
|
||||||
if status == "authorized" || status == "blocked" {
|
if (status == "authorized" || status == "blocked")
|
||||||
if let Some(addr) = subscribers.remove(&(action.to_string(), target.to_string())) {
|
&& let Some(addr) = subscribers.remove(&(action.to_string(), target.to_string())) {
|
||||||
let response = if status == "authorized" { b"APPROVED" } else { b"REJECTED" };
|
let response = if status == "authorized" { b"APPROVED" } else { b"REJECTED" };
|
||||||
let _ = socket.send_to(response, addr).await;
|
let _ = socket.send_to(response, addr).await;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -476,34 +551,42 @@ pub async fn run_server(state: Arc<MemoryState>) -> Result<(), Box<dyn std::erro
|
|||||||
if payload.event.starts_with("agent_") || payload.event.starts_with("diff_") {
|
if payload.event.starts_with("agent_") || payload.event.starts_with("diff_") {
|
||||||
let payload_val = serde_json::json!(&payload);
|
let payload_val = serde_json::json!(&payload);
|
||||||
// 1. General event topic (e.g. nvim:ui:agent_prompt_response, nvim:ui:agent_diff_accepted)
|
// 1. General event topic (e.g. nvim:ui:agent_prompt_response, nvim:ui:agent_diff_accepted)
|
||||||
let _ = nvim_udp_state.handler.state.event_bus_tx.send(crate::state::GenericEvent {
|
let _ = nvim_udp_state.handler.state.event_bus_tx.send(
|
||||||
topic: format!("nvim:ui:{}", payload.event),
|
crate::state::GenericEvent {
|
||||||
session_id: Some(payload.session_id.clone()),
|
topic: format!("nvim:ui:{}", payload.event),
|
||||||
payload: payload_val.clone(),
|
session_id: Some(payload.session_id.clone()),
|
||||||
});
|
payload: payload_val.clone(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// 2. Correlated request_id topic (e.g. nvim:ui:agent_prompt_response:REQ_ID)
|
// 2. Correlated request_id topic (e.g. nvim:ui:agent_prompt_response:REQ_ID)
|
||||||
if let Some(ref req_id) = payload.request_id {
|
if let Some(ref req_id) = payload.request_id {
|
||||||
let _ = nvim_udp_state.handler.state.event_bus_tx.send(crate::state::GenericEvent {
|
let _ = nvim_udp_state.handler.state.event_bus_tx.send(
|
||||||
topic: format!("nvim:ui:{}:{}", payload.event, req_id),
|
crate::state::GenericEvent {
|
||||||
session_id: Some(payload.session_id.clone()),
|
topic: format!("nvim:ui:{}:{}", payload.event, req_id),
|
||||||
payload: payload_val.clone(),
|
session_id: Some(payload.session_id.clone()),
|
||||||
});
|
payload: payload_val.clone(),
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Correlated diff_id topics
|
// 3. Correlated diff_id topics
|
||||||
if let Some(ref diff_id) = payload.diff_id {
|
if let Some(ref diff_id) = payload.diff_id {
|
||||||
let _ = nvim_udp_state.handler.state.event_bus_tx.send(crate::state::GenericEvent {
|
let _ = nvim_udp_state.handler.state.event_bus_tx.send(
|
||||||
topic: format!("nvim:ui:{}:{}", payload.event, diff_id),
|
crate::state::GenericEvent {
|
||||||
session_id: Some(payload.session_id.clone()),
|
topic: format!("nvim:ui:{}:{}", payload.event, diff_id),
|
||||||
payload: payload_val.clone(),
|
session_id: Some(payload.session_id.clone()),
|
||||||
});
|
payload: payload_val.clone(),
|
||||||
|
},
|
||||||
|
);
|
||||||
// General diff decision topic
|
// General diff decision topic
|
||||||
let _ = nvim_udp_state.handler.state.event_bus_tx.send(crate::state::GenericEvent {
|
let _ = nvim_udp_state.handler.state.event_bus_tx.send(
|
||||||
topic: format!("nvim:ui:diff_decision:{}", diff_id),
|
crate::state::GenericEvent {
|
||||||
session_id: Some(payload.session_id.clone()),
|
topic: format!("nvim:ui:diff_decision:{}", diff_id),
|
||||||
payload: payload_val.clone(),
|
session_id: Some(payload.session_id.clone()),
|
||||||
});
|
payload: payload_val.clone(),
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -576,6 +659,7 @@ pub fn init_logging(app_name: &str) -> Option<tracing_appender::non_blocking::Wo
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn run_cli() -> Result<(), Box<dyn std::error::Error>> {
|
pub fn run_cli() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
crate::config::load_mcp_config_env();
|
||||||
let _guard = init_logging("mcp-memory-server");
|
let _guard = init_logging("mcp-memory-server");
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
|
||||||
|
|||||||
+9
-19
@@ -4,18 +4,15 @@ use std::collections::HashMap;
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
|
#[derive(Default)]
|
||||||
pub enum ChangeKind {
|
pub enum ChangeKind {
|
||||||
Added,
|
Added,
|
||||||
|
#[default]
|
||||||
Modified,
|
Modified,
|
||||||
Deleted,
|
Deleted,
|
||||||
Renamed,
|
Renamed,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ChangeKind {
|
|
||||||
fn default() -> Self {
|
|
||||||
ChangeKind::Modified
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
pub struct CodeChange {
|
pub struct CodeChange {
|
||||||
@@ -57,19 +54,17 @@ impl CodeChange {
|
|||||||
if let Some(vcs) = &self.vcs_type {
|
if let Some(vcs) = &self.vcs_type {
|
||||||
return vcs.as_str();
|
return vcs.as_str();
|
||||||
}
|
}
|
||||||
if let Some(rev) = self.revision.as_deref().or(self.git_commit.as_deref()) {
|
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()) {
|
&& rev.starts_with('r') && rev[1..].chars().all(|c| c.is_ascii_digit()) {
|
||||||
return "svn";
|
return "svn";
|
||||||
}
|
}
|
||||||
}
|
if let Some(br) = self.branch.as_deref().or(self.git_branch.as_deref())
|
||||||
if let Some(br) = self.branch.as_deref().or(self.git_branch.as_deref()) {
|
&& (br.eq_ignore_ascii_case("trunk")
|
||||||
if br.eq_ignore_ascii_case("trunk")
|
|
||||||
|| br.starts_with("branches/")
|
|| br.starts_with("branches/")
|
||||||
|| br.starts_with("tags/")
|
|| br.starts_with("tags/"))
|
||||||
{
|
{
|
||||||
return "svn";
|
return "svn";
|
||||||
}
|
}
|
||||||
}
|
|
||||||
"git"
|
"git"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,11 +261,10 @@ impl KnowledgeGraph {
|
|||||||
let mut adj: HashMap<&'a str, Vec<(&'a str, &'a str, bool)>> =
|
let mut adj: HashMap<&'a str, Vec<(&'a str, &'a str, bool)>> =
|
||||||
HashMap::with_capacity(self.relations.len() * 2);
|
HashMap::with_capacity(self.relations.len() * 2);
|
||||||
for rel in &self.relations {
|
for rel in &self.relations {
|
||||||
if let Some(rf) = relation_filter {
|
if let Some(rf) = relation_filter
|
||||||
if rel.relation_type != rf {
|
&& rel.relation_type != rf {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
adj.entry(rel.from.as_str()).or_default().push((
|
adj.entry(rel.from.as_str()).or_default().push((
|
||||||
rel.to.as_str(),
|
rel.to.as_str(),
|
||||||
rel.relation_type.as_str(),
|
rel.relation_type.as_str(),
|
||||||
@@ -398,7 +392,6 @@ fn default_adr_status() -> String {
|
|||||||
"accepted".to_string()
|
"accepted".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
pub struct ErrorFix {
|
pub struct ErrorFix {
|
||||||
pub signature: String,
|
pub signature: String,
|
||||||
@@ -420,7 +413,6 @@ pub struct ErrorFix {
|
|||||||
pub toolchain: Option<String>,
|
pub toolchain: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
pub struct SessionSummary {
|
pub struct SessionSummary {
|
||||||
pub timestamp: u64,
|
pub timestamp: u64,
|
||||||
@@ -527,7 +519,6 @@ pub struct EnvironmentDetail {
|
|||||||
pub ssh_host: Option<String>,
|
pub ssh_host: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
pub struct TechDebt {
|
pub struct TechDebt {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
@@ -554,7 +545,6 @@ pub struct TechDebt {
|
|||||||
pub effort_estimate: Option<String>,
|
pub effort_estimate: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
pub struct GateRecord {
|
pub struct GateRecord {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
|
|||||||
@@ -263,4 +263,3 @@ async fn test_ollama_client_invalid_api_key() {
|
|||||||
client.base_url = "http://invalid-api-key:11434".to_string();
|
client.base_url = "http://invalid-api-key:11434".to_string();
|
||||||
assert!(!client.is_available().await);
|
assert!(!client.is_available().await);
|
||||||
}
|
}
|
||||||
|
|
||||||
+97
-29
@@ -639,12 +639,19 @@ impl MemoryHandler {
|
|||||||
if let Some(resource) = self.resources.get(uri) {
|
if let Some(resource) = self.resources.get(uri) {
|
||||||
match resource.read(Arc::clone(&self.state)).await {
|
match resource.read(Arc::clone(&self.state)).await {
|
||||||
Ok(text) => {
|
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!({
|
let payload = serde_json::json!({
|
||||||
"contents": [{
|
"contents": [content_obj]
|
||||||
"uri": uri,
|
|
||||||
"mimeType": resource.mime_type().unwrap_or("application/json"),
|
|
||||||
"text": text
|
|
||||||
}]
|
|
||||||
});
|
});
|
||||||
Some(crate::mcp::success(id, payload))
|
Some(crate::mcp::success(id, payload))
|
||||||
}
|
}
|
||||||
@@ -756,8 +763,15 @@ impl MemoryHandler {
|
|||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(text) => {
|
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!({
|
let payload = serde_json::json!({
|
||||||
"content": [{"type": "text", "text": text}],
|
"content": [content_obj],
|
||||||
"isError": false
|
"isError": false
|
||||||
});
|
});
|
||||||
Some(crate::mcp::success(id_clone, payload))
|
Some(crate::mcp::success(id_clone, payload))
|
||||||
@@ -788,8 +802,13 @@ impl MemoryHandler {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
let unblocked = crate::handlers::reconciliation::cascade_unblock_tasks(&self.state);
|
let unblocked = crate::handlers::reconciliation::cascade_unblock_tasks(&self.state);
|
||||||
let milestones = crate::handlers::reconciliation::recalculate_milestones(&self.state);
|
let milestones =
|
||||||
self.state.record_activity("task", &format!("Completed task via WebSocket RPC: {}", task_id), Some("completed"));
|
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 {
|
let _ = self.state.event_bus_tx.send(crate::state::GenericEvent {
|
||||||
topic: "task:event".to_string(),
|
topic: "task:event".to_string(),
|
||||||
session_id: None,
|
session_id: None,
|
||||||
@@ -800,11 +819,14 @@ impl MemoryHandler {
|
|||||||
"updated_milestones": milestones,
|
"updated_milestones": milestones,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
Some(crate::mcp::success(id, serde_json::json!({
|
Some(crate::mcp::success(
|
||||||
"status": "success",
|
id,
|
||||||
"unblocked_tasks": unblocked,
|
serde_json::json!({
|
||||||
"updated_milestones": milestones
|
"status": "success",
|
||||||
})))
|
"unblocked_tasks": unblocked,
|
||||||
|
"updated_milestones": milestones
|
||||||
|
}),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
"tech_debts/resolve" => {
|
"tech_debts/resolve" => {
|
||||||
let params = req.get("params").unwrap_or(&serde_json::Value::Null);
|
let params = req.get("params").unwrap_or(&serde_json::Value::Null);
|
||||||
@@ -823,7 +845,11 @@ impl MemoryHandler {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
if found {
|
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 {
|
let _ = self.state.event_bus_tx.send(crate::state::GenericEvent {
|
||||||
topic: "tech_debt:event".to_string(),
|
topic: "tech_debt:event".to_string(),
|
||||||
session_id: None,
|
session_id: None,
|
||||||
@@ -832,7 +858,10 @@ impl MemoryHandler {
|
|||||||
"status": "resolved"
|
"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 {
|
} else {
|
||||||
Some(crate::mcp::error(id, -32602, "Tech debt not found"))
|
Some(crate::mcp::error(id, -32602, "Tech debt not found"))
|
||||||
}
|
}
|
||||||
@@ -853,13 +882,20 @@ impl MemoryHandler {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
if found {
|
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 {
|
let _ = self.state.event_bus_tx.send(crate::state::GenericEvent {
|
||||||
topic: "gate:event".to_string(),
|
topic: "gate:event".to_string(),
|
||||||
session_id: None,
|
session_id: None,
|
||||||
payload: serde_json::json!({ "id": gate_id, "status": "authorized", "active_gate": "authorized" }),
|
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 {
|
} else {
|
||||||
Some(crate::mcp::error(id, -32602, "Gate not found"))
|
Some(crate::mcp::error(id, -32602, "Gate not found"))
|
||||||
}
|
}
|
||||||
@@ -880,13 +916,20 @@ impl MemoryHandler {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
if found {
|
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 {
|
let _ = self.state.event_bus_tx.send(crate::state::GenericEvent {
|
||||||
topic: "gate:event".to_string(),
|
topic: "gate:event".to_string(),
|
||||||
session_id: None,
|
session_id: None,
|
||||||
payload: serde_json::json!({ "id": gate_id, "status": "blocked", "active_gate": "blocked" }),
|
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 {
|
} else {
|
||||||
Some(crate::mcp::error(id, -32602, "Gate not found"))
|
Some(crate::mcp::error(id, -32602, "Gate not found"))
|
||||||
}
|
}
|
||||||
@@ -902,13 +945,20 @@ impl MemoryHandler {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
if removed {
|
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 {
|
let _ = self.state.event_bus_tx.send(crate::state::GenericEvent {
|
||||||
topic: "gate:event".to_string(),
|
topic: "gate:event".to_string(),
|
||||||
session_id: None,
|
session_id: None,
|
||||||
payload: serde_json::json!({ "id": gate_id, "status": "deleted" }),
|
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 {
|
} else {
|
||||||
Some(crate::mcp::error(id, -32602, "Gate not found"))
|
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)
|
("Create Entities", names)
|
||||||
}
|
}
|
||||||
"hypotheses" => {
|
"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
|
let hyp = args
|
||||||
.get("hypothesis")
|
.get("hypothesis")
|
||||||
.or_else(|| args.get("query"))
|
.or_else(|| args.get("query"))
|
||||||
@@ -1011,24 +1064,39 @@ pub fn format_tool_activity_description(name: &str, args: &serde_json::Value) ->
|
|||||||
.unwrap_or("");
|
.unwrap_or("");
|
||||||
(
|
(
|
||||||
"Hypotheses",
|
"Hypotheses",
|
||||||
format!("{}: {}", act, hyp).trim_end_matches(": ").to_string(),
|
format!("{}: {}", act, hyp)
|
||||||
|
.trim_end_matches(": ")
|
||||||
|
.to_string(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
"agent_signals" => {
|
"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 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",
|
"Agent Signals",
|
||||||
format!("{}: {} [{}]", act, sender, st).trim_end_matches(": ").to_string(),
|
format!("{}: {} [{}]", act, sender, st)
|
||||||
|
.trim_end_matches(": ")
|
||||||
|
.to_string(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
"process_logs" => {
|
"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("");
|
let file = args.get("file_path").and_then(|v| v.as_str()).unwrap_or("");
|
||||||
(
|
(
|
||||||
"Process Logs",
|
"Process Logs",
|
||||||
format!("{}: {}", act, file).trim_end_matches(": ").to_string(),
|
format!("{}: {}", act, file)
|
||||||
|
.trim_end_matches(": ")
|
||||||
|
.to_string(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
_ => return name.to_string(),
|
_ => return name.to_string(),
|
||||||
@@ -1053,8 +1121,8 @@ async fn execute_tool_span(
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use tempfile::tempdir;
|
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_format_tool_activity_description() {
|
fn test_format_tool_activity_description() {
|
||||||
|
|||||||
+72
-28
@@ -16,6 +16,26 @@ pub struct SearchResult {
|
|||||||
|
|
||||||
pub type SearchResultTuple = (String, String, String, String, f32);
|
pub type SearchResultTuple = (String, String, String, String, f32);
|
||||||
|
|
||||||
|
fn calculate_time_decay(timestamp_sec: Option<u64>) -> 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)]
|
#[derive(Clone)]
|
||||||
pub struct MemoryIndex {
|
pub struct MemoryIndex {
|
||||||
pub index: Index,
|
pub index: Index,
|
||||||
@@ -55,7 +75,8 @@ impl MemoryIndex {
|
|||||||
.reload_policy(ReloadPolicy::OnCommitWithDelay)
|
.reload_policy(ReloadPolicy::OnCommitWithDelay)
|
||||||
.try_into()?;
|
.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 {
|
Ok(Self {
|
||||||
index,
|
index,
|
||||||
@@ -87,7 +108,8 @@ impl MemoryIndex {
|
|||||||
.reload_policy(ReloadPolicy::OnCommitWithDelay)
|
.reload_policy(ReloadPolicy::OnCommitWithDelay)
|
||||||
.try_into()?;
|
.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 {
|
Ok(Self {
|
||||||
index,
|
index,
|
||||||
@@ -117,7 +139,10 @@ impl MemoryIndex {
|
|||||||
self.index_entities_batch(std::slice::from_ref(e))
|
self.index_entities_batch(std::slice::from_ref(e))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn index_entities_batch(&self, entities: &[Entity]) -> tokio::task::JoinHandle<tantivy::Result<()>> {
|
pub fn index_entities_batch(
|
||||||
|
&self,
|
||||||
|
entities: &[Entity],
|
||||||
|
) -> tokio::task::JoinHandle<tantivy::Result<()>> {
|
||||||
let writer = Arc::clone(&self.writer);
|
let writer = Arc::clone(&self.writer);
|
||||||
let id_field = self.id_field;
|
let id_field = self.id_field;
|
||||||
let needs_commit = Arc::clone(&self.needs_commit);
|
let needs_commit = Arc::clone(&self.needs_commit);
|
||||||
@@ -154,13 +179,16 @@ impl MemoryIndex {
|
|||||||
let id_val = t.id.clone();
|
let id_val = t.id.clone();
|
||||||
let needs_commit = Arc::clone(&self.needs_commit);
|
let needs_commit = Arc::clone(&self.needs_commit);
|
||||||
|
|
||||||
let criteria_body = t.acceptance_criteria.iter().fold(String::new(), |mut acc, c| {
|
let criteria_body = t
|
||||||
if !acc.is_empty() {
|
.acceptance_criteria
|
||||||
acc.push('\n');
|
.iter()
|
||||||
}
|
.fold(String::new(), |mut acc, c| {
|
||||||
acc.push_str(&c.description);
|
if !acc.is_empty() {
|
||||||
acc
|
acc.push('\n');
|
||||||
});
|
}
|
||||||
|
acc.push_str(&c.description);
|
||||||
|
acc
|
||||||
|
});
|
||||||
|
|
||||||
let doc = doc!(
|
let doc = doc!(
|
||||||
self.id_field => t.id.as_str(),
|
self.id_field => t.id.as_str(),
|
||||||
@@ -231,7 +259,10 @@ impl MemoryIndex {
|
|||||||
if let Err(first_err) = writer.commit() {
|
if let Err(first_err) = writer.commit() {
|
||||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||||
writer.commit().map_err(|second_err| {
|
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();
|
let _ = reader.reload();
|
||||||
@@ -246,9 +277,11 @@ impl MemoryIndex {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub fn commit_if_needed(&self) -> tantivy::Result<()> {
|
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());
|
let mut writer = self.writer.lock().unwrap_or_else(|e| e.into_inner());
|
||||||
writer.commit()?;
|
writer.commit()?;
|
||||||
let _ = self.reader.reload();
|
let _ = self.reader.reload();
|
||||||
@@ -268,8 +301,8 @@ impl MemoryIndex {
|
|||||||
let sanitized: String = query
|
let sanitized: String = query
|
||||||
.chars()
|
.chars()
|
||||||
.map(|c| match c {
|
.map(|c| match c {
|
||||||
'+' | '-' | '&' | '|' | '!' | '(' | ')' | '{' | '}' | '[' | ']' | '^'
|
'+' | '-' | '&' | '|' | '!' | '(' | ')' | '{' | '}' | '[' | ']' | '^' | '"'
|
||||||
| '"' | '~' | '*' | '?' | ':' | '\\' | '/' => ' ',
|
| '~' | '*' | '?' | ':' | '\\' | '/' => ' ',
|
||||||
_ => c,
|
_ => c,
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
@@ -284,10 +317,11 @@ impl MemoryIndex {
|
|||||||
|
|
||||||
let target_limit = if is_wildcard_fallback { 10 } else { 50 };
|
let target_limit = if is_wildcard_fallback { 10 } else { 50 };
|
||||||
let final_query: Box<dyn tantivy::query::Query> = if let Some(ns) = namespace {
|
let final_query: Box<dyn tantivy::query::Query> = if let Some(ns) = namespace {
|
||||||
let ns_term_query: Box<dyn tantivy::query::Query> = Box::new(tantivy::query::TermQuery::new(
|
let ns_term_query: Box<dyn tantivy::query::Query> =
|
||||||
tantivy::Term::from_field_text(self.namespace_field, ns),
|
Box::new(tantivy::query::TermQuery::new(
|
||||||
tantivy::schema::IndexRecordOption::Basic,
|
tantivy::Term::from_field_text(self.namespace_field, ns),
|
||||||
));
|
tantivy::schema::IndexRecordOption::Basic,
|
||||||
|
));
|
||||||
let ns_filter: Box<dyn tantivy::query::Query> = if ns != "global" {
|
let ns_filter: Box<dyn tantivy::query::Query> = if ns != "global" {
|
||||||
let global_term_query = Box::new(tantivy::query::TermQuery::new(
|
let global_term_query = Box::new(tantivy::query::TermQuery::new(
|
||||||
tantivy::Term::from_field_text(self.namespace_field, "global"),
|
tantivy::Term::from_field_text(self.namespace_field, "global"),
|
||||||
@@ -491,7 +525,8 @@ impl SearchService {
|
|||||||
self.state.code.snippets.read_with(|snips| {
|
self.state.code.snippets.read_with(|snips| {
|
||||||
for snippet in snips.iter() {
|
for snippet in snips.iter() {
|
||||||
if let Some(ref emb) = snippet.embedding {
|
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 {
|
results.push(SearchResult {
|
||||||
id: snippet.name.clone(),
|
id: snippet.name.clone(),
|
||||||
doc_type: "snippet".to_string(),
|
doc_type: "snippet".to_string(),
|
||||||
@@ -508,6 +543,7 @@ impl SearchService {
|
|||||||
snippet.name.clone(),
|
snippet.name.clone(),
|
||||||
"snippet".to_string(),
|
"snippet".to_string(),
|
||||||
snippet.description.clone(),
|
snippet.description.clone(),
|
||||||
|
Some(snippet.updated_at),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -515,15 +551,15 @@ impl SearchService {
|
|||||||
|
|
||||||
self.state.read_graph(|graph| {
|
self.state.read_graph(|graph| {
|
||||||
for entity in graph.entities.values() {
|
for entity in graph.entities.values() {
|
||||||
if let Some(ns) = filter_namespace {
|
if let Some(ns) = filter_namespace
|
||||||
if entity.namespace != ns {
|
&& entity.namespace != ns {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
let obs = entity.observations.join("; ");
|
let obs = entity.observations.join("; ");
|
||||||
let desc = format!("{}: {}", entity.entity_type, obs);
|
let desc = format!("{}: {}", entity.entity_type, obs);
|
||||||
if let Some(ref emb) = entity.embedding {
|
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 {
|
results.push(SearchResult {
|
||||||
id: entity.name.clone(),
|
id: entity.name.clone(),
|
||||||
doc_type: "entity".to_string(),
|
doc_type: "entity".to_string(),
|
||||||
@@ -533,7 +569,12 @@ impl SearchService {
|
|||||||
});
|
});
|
||||||
} else if uncached_texts.len() < 50 {
|
} else if uncached_texts.len() < 50 {
|
||||||
uncached_texts.push(format!("{} {} {}", entity.name, entity.entity_type, obs));
|
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| {
|
self.state.code.error_fixes.read_with(|fixes| {
|
||||||
for fix in fixes.iter() {
|
for fix in fixes.iter() {
|
||||||
if let Some(ref emb) = fix.embedding {
|
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 {
|
results.push(SearchResult {
|
||||||
id: fix.signature.clone(),
|
id: fix.signature.clone(),
|
||||||
doc_type: "error_fix".to_string(),
|
doc_type: "error_fix".to_string(),
|
||||||
@@ -555,6 +597,7 @@ impl SearchService {
|
|||||||
fix.signature.clone(),
|
fix.signature.clone(),
|
||||||
"error_fix".to_string(),
|
"error_fix".to_string(),
|
||||||
fix.solution.clone(),
|
fix.solution.clone(),
|
||||||
|
Some(fix.timestamp),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -563,8 +606,9 @@ impl SearchService {
|
|||||||
if !uncached_texts.is_empty()
|
if !uncached_texts.is_empty()
|
||||||
&& let Ok(embeddings) = generate_embeddings_async(uncached_texts).await
|
&& let Ok(embeddings) = generate_embeddings_async(uncached_texts).await
|
||||||
{
|
{
|
||||||
for (emb, (title, doc_type, body)) in embeddings.into_iter().zip(uncached_meta) {
|
for (emb, (title, doc_type, body, ts)) in embeddings.into_iter().zip(uncached_meta) {
|
||||||
let sim = cosine_similarity(&query_emb, &emb);
|
let base_sim = cosine_similarity(&query_emb, &emb);
|
||||||
|
let sim = base_sim * calculate_time_decay(ts);
|
||||||
results.push(SearchResult {
|
results.push(SearchResult {
|
||||||
id: title.clone(),
|
id: title.clone(),
|
||||||
doc_type,
|
doc_type,
|
||||||
|
|||||||
+4
-6
@@ -86,11 +86,10 @@ impl ClipboardCacheState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn push_text(&mut self, text: CachedClipboardText) {
|
pub fn push_text(&mut self, text: CachedClipboardText) {
|
||||||
if let Some(ref prev) = self.last_text {
|
if let Some(ref prev) = self.last_text
|
||||||
if prev.text == text.text {
|
&& prev.text == text.text {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
self.last_text = Some(text.clone());
|
self.last_text = Some(text.clone());
|
||||||
self.history.push_front(ClipboardHistoryItem::Text(text));
|
self.history.push_front(ClipboardHistoryItem::Text(text));
|
||||||
if self.history.len() > 20 {
|
if self.history.len() > 20 {
|
||||||
@@ -142,11 +141,10 @@ impl MemoryState {
|
|||||||
pub fn new(base_dir_str: &str) -> Self {
|
pub fn new(base_dir_str: &str) -> Self {
|
||||||
let is_in_memory = base_dir_str == ":memory:";
|
let is_in_memory = base_dir_str == ":memory:";
|
||||||
let base = std::path::PathBuf::from(base_dir_str);
|
let base = std::path::PathBuf::from(base_dir_str);
|
||||||
if !is_in_memory {
|
if !is_in_memory
|
||||||
if let Err(e) = std::fs::create_dir_all(&base) {
|
&& let Err(e) = std::fs::create_dir_all(&base) {
|
||||||
tracing::error!("Failed to create store directory at {:?}: {}", base, e);
|
tracing::error!("Failed to create store directory at {:?}: {}", base, e);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let db = crate::db::init_redb(&base);
|
let db = crate::db::init_redb(&base);
|
||||||
|
|
||||||
|
|||||||
+13
-16
@@ -33,11 +33,10 @@ static QUEUE_REGISTRY: std::sync::Mutex<Option<(Arc<Database>, DbWriteQueue)>> =
|
|||||||
|
|
||||||
fn get_or_create_queue(db: Arc<Database>) -> DbWriteQueue {
|
fn get_or_create_queue(db: Arc<Database>) -> DbWriteQueue {
|
||||||
let mut reg = QUEUE_REGISTRY.lock().unwrap_or_else(|e| e.into_inner());
|
let mut reg = QUEUE_REGISTRY.lock().unwrap_or_else(|e| e.into_inner());
|
||||||
if let Some((ref existing_db, ref queue)) = *reg {
|
if let Some((ref existing_db, ref queue)) = *reg
|
||||||
if Arc::ptr_eq(existing_db, &db) && !queue.tx.is_closed() {
|
&& Arc::ptr_eq(existing_db, &db) && !queue.tx.is_closed() {
|
||||||
return queue.clone();
|
return queue.clone();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
let new_queue = DbWriteQueue::new(db.clone());
|
let new_queue = DbWriteQueue::new(db.clone());
|
||||||
*reg = Some((db, new_queue.clone()));
|
*reg = Some((db, new_queue.clone()));
|
||||||
new_queue
|
new_queue
|
||||||
@@ -334,19 +333,17 @@ impl<T: DeserializeOwned + Default + Serialize + Send + Sync + 'static> Store<T>
|
|||||||
let mut known = std::collections::HashSet::new();
|
let mut known = std::collections::HashSet::new();
|
||||||
|
|
||||||
if let Ok(range) = table.range(prefix.as_str()..) {
|
if let Ok(range) = table.range(prefix.as_str()..) {
|
||||||
for entry in range {
|
for (k, v) in range.flatten() {
|
||||||
if let Ok((k, v)) = entry {
|
let k_str = k.value();
|
||||||
let k_str = k.value();
|
if !k_str.starts_with(&prefix) {
|
||||||
if !k_str.starts_with(&prefix) {
|
break;
|
||||||
break;
|
}
|
||||||
}
|
found_granular = true;
|
||||||
found_granular = true;
|
known.insert(k_str.to_string());
|
||||||
known.insert(k_str.to_string());
|
if let Ok(val) = serde_json::from_slice::<serde_json::Value>(v.value()) {
|
||||||
if let Ok(val) = serde_json::from_slice::<serde_json::Value>(v.value()) {
|
let sub_key = &k_str[prefix.len()..];
|
||||||
let sub_key = &k_str[prefix.len()..];
|
items_array.push(val.clone());
|
||||||
items_array.push(val.clone());
|
items_map.insert(sub_key.to_string(), val);
|
||||||
items_map.insert(sub_key.to_string(), val);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-1
@@ -993,7 +993,13 @@ pub struct EnvironmentTool {
|
|||||||
#[derive(Debug, Clone, Copy, Deserialize, Serialize, JsonSchema, PartialEq)]
|
#[derive(Debug, Clone, Copy, Deserialize, Serialize, JsonSchema, PartialEq)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum ClipboardAction {
|
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,
|
Image,
|
||||||
#[serde(alias = "text", alias = "TEXT", alias = "Text")]
|
#[serde(alias = "text", alias = "TEXT", alias = "Text")]
|
||||||
Text,
|
Text,
|
||||||
|
|||||||
+46
-23
@@ -79,7 +79,11 @@ pub fn spawn_watcher(state: Arc<MemoryState>) {
|
|||||||
|
|
||||||
fn should_review(path: &Path) -> bool {
|
fn should_review(path: &Path) -> bool {
|
||||||
let path_str = path.to_string_lossy();
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,13 +93,19 @@ fn should_review(path: &Path) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn trigger_autonomous_review(path: &Path, state: Arc<MemoryState>) {
|
async fn trigger_autonomous_review(path: &Path, state: Arc<MemoryState>) {
|
||||||
info!("Triggering autonomous review & incremental AST index for {:?}", path);
|
info!(
|
||||||
state.broadcast_activity("AUTONOMOUS", &format!("Modified: {:?}", path.file_name().unwrap_or_default()));
|
"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
|
// ADR-0109: Incremental Background AST Indexing & Differential Graph Updates
|
||||||
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
|
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
|
||||||
if matches!(ext, "rs" | "ts" | "js" | "py" | "go" | "java" | "c" | "cpp") {
|
if matches!(ext, "rs" | "ts" | "js" | "py" | "go" | "java" | "c" | "cpp")
|
||||||
if let Ok(content) = std::fs::read_to_string(path) {
|
&& let Ok(content) = std::fs::read_to_string(path) {
|
||||||
let language = match ext {
|
let language = match ext {
|
||||||
"rs" => Some(tree_sitter_rust::LANGUAGE),
|
"rs" => Some(tree_sitter_rust::LANGUAGE),
|
||||||
"ts" | "js" => Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT),
|
"ts" | "js" => Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT),
|
||||||
@@ -109,12 +119,20 @@ async fn trigger_autonomous_review(path: &Path, state: Arc<MemoryState>) {
|
|||||||
|
|
||||||
if let Some(lang) = language {
|
if let Some(lang) = language {
|
||||||
let mut parser = tree_sitter::Parser::new();
|
let mut parser = tree_sitter::Parser::new();
|
||||||
if parser.set_language(&lang.into()).is_ok() {
|
if parser.set_language(&lang.into()).is_ok()
|
||||||
if let Some(tree) = parser.parse(&content, None) {
|
&& let Some(tree) = parser.parse(&content, None) {
|
||||||
let mut chunks = Vec::new();
|
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 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 chunks_count = chunks.len();
|
||||||
let file_str_clone = file_str.clone();
|
let file_str_clone = file_str.clone();
|
||||||
@@ -140,16 +158,21 @@ async fn trigger_autonomous_review(path: &Path, state: Arc<MemoryState>) {
|
|||||||
"McpTool".to_string()
|
"McpTool".to_string()
|
||||||
};
|
};
|
||||||
|
|
||||||
g.entities.insert(symbol_name.clone(), crate::models::Entity {
|
g.entities.insert(
|
||||||
name: symbol_name.clone(),
|
symbol_name.clone(),
|
||||||
entity_type: symbol_type,
|
crate::models::Entity {
|
||||||
observations: vec![format!("AST definition: {} chars", chunk_code.len())],
|
name: symbol_name.clone(),
|
||||||
namespace: "global".to_string(),
|
entity_type: symbol_type,
|
||||||
file_path: Some(file_str.clone()),
|
observations: vec![format!(
|
||||||
created_at: Some(now),
|
"AST definition: {} chars",
|
||||||
updated_at: Some(now),
|
chunk_code.len()
|
||||||
..Default::default()
|
)],
|
||||||
}
|
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 {
|
let rel = crate::models::Relation {
|
||||||
@@ -183,12 +206,13 @@ async fn trigger_autonomous_review(path: &Path, state: Arc<MemoryState>) {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
info!("Autonomous review & incremental AST index complete for {:?}", path);
|
info!(
|
||||||
|
"Autonomous review & incremental AST index complete for {:?}",
|
||||||
|
path
|
||||||
|
);
|
||||||
}
|
}
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
@@ -220,4 +244,3 @@ mod tests {
|
|||||||
spawn_watcher(state);
|
spawn_watcher(state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -93,10 +93,9 @@ fn test_dashboard_route_parity() {
|
|||||||
"server/src/api/setup.rs"
|
"server/src/api/setup.rs"
|
||||||
};
|
};
|
||||||
|
|
||||||
let dashboard_content = std::fs::read_to_string(dashboard_path)
|
let dashboard_content =
|
||||||
.expect("Failed to read dashboard.ts");
|
std::fs::read_to_string(dashboard_path).expect("Failed to read dashboard.ts");
|
||||||
let setup_content = std::fs::read_to_string(setup_path)
|
let setup_content = std::fs::read_to_string(setup_path).expect("Failed to read setup.rs");
|
||||||
.expect("Failed to read setup.rs");
|
|
||||||
|
|
||||||
// 1. Extract routes from setup.rs
|
// 1. Extract routes from setup.rs
|
||||||
let mut registered_routes = HashSet::new();
|
let mut registered_routes = HashSet::new();
|
||||||
@@ -142,7 +141,8 @@ fn test_dashboard_route_parity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if endpoint.is_empty() {
|
if endpoint.is_empty() {
|
||||||
let end_idx = rest.find(['"', '`', '\'', ' ', '\n', '?'])
|
let end_idx = rest
|
||||||
|
.find(['"', '`', '\'', ' ', '\n', '?'])
|
||||||
.unwrap_or(rest.len());
|
.unwrap_or(rest.len());
|
||||||
endpoint = rest[..end_idx].to_string();
|
endpoint = rest[..end_idx].to_string();
|
||||||
}
|
}
|
||||||
@@ -153,7 +153,10 @@ fn test_dashboard_route_parity() {
|
|||||||
cursor = start + 5;
|
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 {
|
for endpoint in &frontend_endpoints {
|
||||||
assert!(
|
assert!(
|
||||||
|
|||||||
+24
-8
@@ -48,28 +48,45 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_extract_log_prefix_response() {
|
fn test_extract_log_prefix_response() {
|
||||||
let json_str = r#"{"jsonrpc":"2.0","id":123,"result":{}}"#;
|
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"}}"#;
|
let err_json =
|
||||||
assert_eq!(extract_log_prefix(err_json, true), "Response id=456 [Error: true]");
|
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}}"#;
|
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]
|
#[test]
|
||||||
fn test_extract_log_prefix_tool_call() {
|
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");
|
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":{}}"#;
|
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]
|
#[test]
|
||||||
fn test_extract_log_prefix_request() {
|
fn test_extract_log_prefix_request() {
|
||||||
let json_str = r#"{"jsonrpc":"2.0","id":10,"method":"initialize"}"#;
|
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]
|
#[test]
|
||||||
@@ -84,4 +101,3 @@ mod tests {
|
|||||||
assert_eq!(extract_log_prefix(json_str, false), "Message");
|
assert_eq!(extract_log_prefix(json_str, false), "Message");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
+2
-2
@@ -170,11 +170,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if retry_count >= 20 {
|
if retry_count >= 20 {
|
||||||
return Err("Failed to connect to the MCP memory server after multiple attempts. Is it running?".into());
|
return Err("Failed to connect to the MCP memory server after multiple attempts. Is it running?".into());
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user