chore: fix formatting and clippy lints

This commit is contained in:
Riz Ashraf committed 2026-10-08 23:01:38 +01:00
1 parent 8952bd5399
commit 9d9e959744
35 files changed
+1091 -582

No files matched your search

+4 -6
View File
@@ -119,11 +119,10 @@ impl McpTool for ReadFileSkeletonHandler {
header.push('\n');
}
}
if header.is_empty() {
if let Some(first_line) = node_text.lines().next() {
if header.is_empty()
&& let Some(first_line) = node_text.lines().next() {
header = first_line.to_string();
}
}
out.push_str(&indent);
out.push_str(header.trim());
@@ -425,11 +424,10 @@ fn scan_workspace_for_symbol(
if scanned_files > 500 {
break;
}
if let Ok(meta) = std::fs::metadata(path) {
if meta.len() > 1024 * 1024 {
if let Ok(meta) = std::fs::metadata(path)
&& meta.len() > 1024 * 1024 {
continue;
}
}
if let Ok(content) = std::fs::read_to_string(path) {
for (line_num, line) in content.lines().enumerate() {
let is_match = if filter_fn_call {
+33 -17
View File
@@ -22,16 +22,31 @@ impl McpTool for EnvironmentHandler {
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 ns = req.namespace.unwrap_or_else(|| crate::models::default_namespace());
let ns = req
.namespace
.unwrap_or_else(crate::models::default_namespace);
match req.action {
EnvAction::UpdateFingerprint => {
let tool_versions = req.tool_versions.unwrap_or_default();
let detected_os = req.os.unwrap_or_else(|| std::env::consts::OS.to_string());
let detected_shell = req.shell
let detected_shell = req
.shell
.or_else(|| std::env::var("SHELL").ok())
.or_else(|| std::env::var("STARSHIP_SHELL").ok())
.or_else(|| if std::env::var("PSModulePath").is_ok() { Some("pwsh".to_string()) } else { None })
.or_else(|| if std::env::var("COMSPEC").is_ok() { Some("cmd".to_string()) } else { None })
.or_else(|| {
if std::env::var("PSModulePath").is_ok() {
Some("pwsh".to_string())
} else {
None
}
})
.or_else(|| {
if std::env::var("COMSPEC").is_ok() {
Some("cmd".to_string())
} else {
None
}
})
.unwrap_or_else(|| "unknown".to_string());
state.env.env_fingerprints.modify(|fps| {
@@ -102,10 +117,7 @@ impl McpTool for EnvironmentHandler {
}
EnvAction::GetDetails => {
let data = state.env.environments.read_with(|envs| {
let filtered: Vec<_> = envs
.iter()
.filter(|e| e.namespace == ns)
.collect();
let filtered: Vec<_> = envs.iter().filter(|e| e.namespace == ns).collect();
Ok::<String, crate::error::AppError>(serde_json::to_string(&filtered)?)
})?;
Ok(data)
@@ -114,7 +126,6 @@ impl McpTool for EnvironmentHandler {
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -143,7 +154,10 @@ mod tests {
assert_eq!(res, "Env fingerprint updated");
let res2 = handler
.execute(json!({"action": "read_fingerprint", "namespace": "global"}), state.clone())
.execute(
json!({"action": "read_fingerprint", "namespace": "global"}),
state.clone(),
)
.await
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
.unwrap();
@@ -171,7 +185,10 @@ mod tests {
let handler = EnvironmentHandler;
let res = handler
.execute(json!({"action": "get_details", "namespace": "global"}), state.clone())
.execute(
json!({"action": "get_details", "namespace": "global"}),
state.clone(),
)
.await
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
.unwrap();
@@ -215,7 +232,10 @@ mod tests {
assert_eq!(res2, "Environment registered");
let res3 = handler
.execute(json!({"action": "get_details", "namespace": "global"}), state.clone())
.execute(
json!({"action": "get_details", "namespace": "global"}),
state.clone(),
)
.await
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
.unwrap();
@@ -237,12 +257,8 @@ mod tests {
}
});
let res = handler
.execute(args, state.clone())
.await;
let res = handler.execute(args, state.clone()).await;
assert!(res.is_ok());
}
}
+2 -4
View File
@@ -139,8 +139,8 @@ impl McpTool for QueryGitDiffsHandler {
let git_matches = tokio::task::spawn_blocking(move || {
let mut results = Vec::new();
let cwd = env::current_dir().unwrap_or_default();
if let Ok(repo) = git2::Repository::discover(&cwd) {
if let Ok(mut revwalk) = repo.revwalk() {
if let Ok(repo) = git2::Repository::discover(&cwd)
&& let Ok(mut revwalk) = repo.revwalk() {
let _ = revwalk.push_head();
let mut count = 0;
for oid in revwalk.flatten() {
@@ -161,7 +161,6 @@ impl McpTool for QueryGitDiffsHandler {
}
}
}
}
results
})
.await
@@ -215,4 +214,3 @@ mod tests {
assert!(parsed.get("branch").is_some() || parsed.is_object());
}
}
+153 -69
View File
@@ -62,8 +62,8 @@ impl<'a> GraphQueryBuilder<'a> {
break;
}
nodes_at_current_depth -= 1;
if current_depth < self.max_depth {
if let Some(neighbors) = adj.get(current) {
if current_depth < self.max_depth
&& let Some(neighbors) = adj.get(current) {
for &(neighbor, rel_type, is_inverse) in neighbors {
if !visited.contains(neighbor) {
visited.insert(neighbor);
@@ -73,7 +73,6 @@ impl<'a> GraphQueryBuilder<'a> {
}
}
}
}
if nodes_at_current_depth == 0 {
current_depth += 1;
nodes_at_current_depth = nodes_at_next_depth;
@@ -138,7 +137,9 @@ impl McpTool for QueryGraphPathHandler {
})
})
.await
.map_err(|e| crate::error::AppError::Internal(format!("Graph traversal task failed: {}", e)))?
.map_err(|e| {
crate::error::AppError::Internal(format!("Graph traversal task failed: {}", e))
})?
}
}
@@ -169,12 +170,19 @@ impl McpTool for CreateEntitiesHandler {
}
}
});
let names: Vec<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() {
let idx = state.get_search_index().await;
let _ = idx.index_entities_batch(&inserted).await;
}
Ok(format!("Successfully created {} entity/entities: {}", names.len(), names.join(", ")))
Ok(format!(
"Successfully created {} entity/entities: {}",
names.len(),
names.join(", ")
))
}
}
@@ -236,7 +244,9 @@ impl McpTool for CreateRelationsHandler {
crate::models::Entity {
name: node_name.clone(),
entity_type: "Entity".to_string(),
observations: vec!["Auto-created stub entity for relation endpoint".to_string()],
observations: vec![
"Auto-created stub entity for relation endpoint".to_string(),
],
namespace: crate::models::default_namespace(),
git_branch: None,
..Default::default()
@@ -247,16 +257,28 @@ impl McpTool for CreateRelationsHandler {
}
for mut relation in req.relations {
if !relation.from.is_empty() && !relation.to.is_empty() {
relation.relation_type = crate::models::normalize_relation_type(&relation.relation_type);
added_relations.push(format!("{} -[{}]-> {}", relation.from, relation.relation_type, relation.to));
relation.relation_type =
crate::models::normalize_relation_type(&relation.relation_type);
added_relations.push(format!(
"{} -[{}]-> {}",
relation.from, relation.relation_type, relation.to
));
g.relations.push(relation);
}
}
});
let mut msg = format!("Successfully created {} relation(s):\n{}", added_relations.len(), added_relations.join("\n"));
let mut msg = format!(
"Successfully created {} relation(s):\n{}",
added_relations.len(),
added_relations.join("\n")
);
if !auto_created.is_empty() {
msg.push_str(&format!("\nNote: Auto-created {} missing stub entity/entities: {}", auto_created.len(), auto_created.join(", ")));
msg.push_str(&format!(
"\nNote: Auto-created {} missing stub entity/entities: {}",
auto_created.len(),
auto_created.join(", ")
));
}
Ok(msg)
}
@@ -462,7 +484,7 @@ impl McpTool for ReadGraphHandler {
let filtered_entities: Vec<(&String, &crate::models::Entity)> = full
.entities
.iter()
.filter(|(_, v)| req.namespace.as_ref().map_or(true, |ns| &v.namespace == ns))
.filter(|(_, v)| req.namespace.as_ref().is_none_or(|ns| &v.namespace == ns))
.collect();
let total_count = filtered_entities.len();
@@ -481,7 +503,7 @@ impl McpTool for ReadGraphHandler {
.relations
.iter()
.filter(|r| {
let ns_match = req.namespace.as_ref().map_or(true, |ns| &r.namespace == ns);
let ns_match = req.namespace.as_ref().is_none_or(|ns| &r.namespace == ns);
ns_match && (paged_entities.contains_key(&r.from) || paged_entities.contains_key(&r.to))
})
.collect();
@@ -499,7 +521,7 @@ impl McpTool for ReadGraphHandler {
let rel_vec: Vec<&crate::models::Relation> = full
.relations
.iter()
.filter(|r| req.namespace.as_ref().map_or(true, |ns| &r.namespace == ns))
.filter(|r| req.namespace.as_ref().is_none_or(|ns| &r.namespace == ns))
.collect();
(ent_map, rel_vec, None)
};
@@ -549,21 +571,19 @@ impl McpTool for ReadGraphHandler {
}
});
if let Some(meta) = &meta_json {
if let Some(obj) = truncated_val.as_object_mut() {
if let Some(meta) = &meta_json
&& let Some(obj) = truncated_val.as_object_mut() {
obj.insert("total_count".to_string(), meta["total_count"].clone());
obj.insert("offset".to_string(), meta["offset"].clone());
obj.insert("limit".to_string(), meta["limit"].clone());
obj.insert("has_more".to_string(), serde_json::Value::Bool(true));
}
}
if let Ok(trunc_str) = serde_json::to_string(&truncated_val) {
if trunc_str.len() <= max_chars || keep_count == 1 {
if let Ok(trunc_str) = serde_json::to_string(&truncated_val)
&& (trunc_str.len() <= max_chars || keep_count == 1) {
result_json = trunc_str;
break;
}
}
keep_count = keep_count.saturating_sub(keep_count / 3 + 1);
}
}
@@ -820,7 +840,10 @@ impl McpTool for CondenseEntityHandler {
e.observations = unique_obs;
}
});
return Ok(format!("Entity '{}' observations condensed with deduplication/AI summary.", req.entity_name));
return Ok(format!(
"Entity '{}' observations condensed with deduplication/AI summary.",
req.entity_name
));
}
state.modify_graph(|master| {
@@ -953,10 +976,9 @@ impl McpTool for GetSubgraphHandler {
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: GetSubgraphTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let root = req
.root_entity
.or(req.root_node)
.ok_or_else(|| crate::error::AppError::Internal("root_entity or root_node is required".to_string()))?;
let root = req.root_entity.or(req.root_node).ok_or_else(|| {
crate::error::AppError::Internal("root_entity or root_node is required".to_string())
})?;
let depth = req.depth.unwrap_or(2);
let format = req.format.unwrap_or(SubgraphFormat::Json);
@@ -974,15 +996,14 @@ impl McpTool for GetSubgraphHandler {
visited.insert(root.clone());
while let Some((curr, d)) = queue.pop_front() {
if d < depth {
if let Some(neighbors) = adj.get(curr) {
if d < depth
&& let Some(neighbors) = adj.get(curr) {
for &next in neighbors {
if visited.insert(next.to_string()) {
queue.push_back((next, d + 1));
}
}
}
}
}
let ent: std::collections::HashMap<String, crate::models::Entity> = visited
@@ -1011,7 +1032,10 @@ impl McpTool for GetSubgraphHandler {
markdown.push_str("\n## Relations\n");
for r in &matched_relations {
markdown.push_str(&format!("- `{}` --[{}]--> `{}`\n", r.from, r.relation_type, r.to));
markdown.push_str(&format!(
"- `{}` --[{}]--> `{}`\n",
r.from, r.relation_type, r.to
));
}
return Ok(markdown);
}
@@ -1038,7 +1062,10 @@ impl McpTool for SweepGraphHealthHandler {
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<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> {
@@ -1132,26 +1159,31 @@ impl McpTool for ResolveStaleSymbolsHandler {
}
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> {
let req: ResolveStaleSymbolsTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let req: ResolveStaleSymbolsTool =
serde_json::from_value(args).map_err(|e| e.to_string())?;
let auto_heal = req.auto_heal.unwrap_or(true);
let mut checked = 0;
let mut stale_entities = Vec::new();
state.modify_graph(|g| {
for (name, _entity) in g.entities.iter_mut() {
if let Some(target) = &req.target {
if name != target && !name.contains(target) {
for name in g.entities.keys() {
if let Some(target) = &req.target
&& name != target && !name.contains(target) {
continue;
}
}
checked += 1;
let path = std::path::Path::new(name);
if (name.contains('/') || name.contains('\\') || name.contains('.')) && !path.exists() {
if (name.contains('/') || name.contains('\\') || name.contains('.'))
&& !path.exists()
{
stale_entities.push(name.clone());
}
}
@@ -1184,7 +1216,10 @@ impl McpTool for SummarizeSubgraphHandler {
}
fn schema(&self) -> Value {
crate::mcp::tool_def::<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> {
@@ -1212,8 +1247,18 @@ impl McpTool for SummarizeSubgraphHandler {
}
}
let ent: Vec<_> = g.entities.values().filter(|e| visited.contains(&e.name)).cloned().collect();
let rel: Vec<_> = g.relations.iter().filter(|r| visited.contains(&r.from) && visited.contains(&r.to)).cloned().collect();
let ent: Vec<_> = g
.entities
.values()
.filter(|e| visited.contains(&e.name))
.cloned()
.collect();
let rel: Vec<_> = g
.relations
.iter()
.filter(|r| visited.contains(&r.from) && visited.contains(&r.to))
.cloned()
.collect();
(ent, rel)
});
@@ -1228,7 +1273,10 @@ impl McpTool for SummarizeSubgraphHandler {
markdown.push_str("\n## Relations\n");
for r in &sub_relations {
markdown.push_str(&format!("- `{}` --[{}]--> `{}`\n", r.from, r.relation_type, r.to));
markdown.push_str(&format!(
"- `{}` --[{}]--> `{}`\n",
r.from, r.relation_type, r.to
));
}
let max_tokens = req.max_tokens.unwrap_or(1000);
@@ -1249,7 +1297,6 @@ mod tests {
use crate::handlers::meta::AgentSignalsHandler;
use serde_json::json;
#[tokio::test]
async fn test_create_and_read_entities() {
let state = Arc::new(MemoryState::new_in_memory());
@@ -1324,10 +1371,7 @@ mod tests {
{"source": "A", "target": "B", "relationType": "knows"}
]
});
let alias_res = handler
.execute(alias_args, state.clone())
.await
.unwrap();
let alias_res = handler.execute(alias_args, state.clone()).await.unwrap();
assert!(alias_res.contains("Successfully created 1 relation(s)"));
// Test semantic LLM schema feedback on missing fields
@@ -1499,7 +1543,10 @@ mod tests {
{"name": "Beta", "entity_type": "Node", "observations": ["Obs3"], "namespace": "global"}
]
});
create_handler.execute(args_ent, state.clone()).await.unwrap();
create_handler
.execute(args_ent, state.clone())
.await
.unwrap();
let rel_handler = CreateRelationsHandler;
let args_rel = json!({
@@ -1510,48 +1557,86 @@ mod tests {
rel_handler.execute(args_rel, state.clone()).await.unwrap();
let path_handler = QueryGraphPathHandler;
let path_res = path_handler.execute(json!({"start_node": "Alpha", "end_node": "Beta"}), state.clone()).await.unwrap();
let path_res = path_handler
.execute(
json!({"start_node": "Alpha", "end_node": "Beta"}),
state.clone(),
)
.await
.unwrap();
assert!(path_res.contains("Alpha"));
let sub_handler = GetSubgraphHandler;
let sub_res = sub_handler.execute(json!({"root_entity": "Alpha"}), state.clone()).await.unwrap();
let sub_res = sub_handler
.execute(json!({"root_entity": "Alpha"}), state.clone())
.await
.unwrap();
assert!(sub_res.contains("Alpha"));
let sum_handler = SummarizeSubgraphHandler;
let sum_res = sum_handler.execute(json!({"root_entity": "Alpha"}), state.clone()).await.unwrap();
let sum_res = sum_handler
.execute(json!({"root_entity": "Alpha"}), state.clone())
.await
.unwrap();
assert!(!sum_res.is_empty());
let sweep_handler = SweepGraphHealthHandler;
let sweep_res = sweep_handler.execute(json!({}), state.clone()).await.unwrap();
let sweep_res = sweep_handler
.execute(json!({}), state.clone())
.await
.unwrap();
assert!(!sweep_res.is_empty());
let del_obs_handler = DeleteObservationsHandler;
let del_obs_res = del_obs_handler.execute(json!({
"deletions": [{"entity_name": "Alpha", "observations": ["Obs1"]}]
}), state.clone()).await.unwrap();
let del_obs_res = del_obs_handler
.execute(
json!({
"deletions": [{"entity_name": "Alpha", "observations": ["Obs1"]}]
}),
state.clone(),
)
.await
.unwrap();
assert_eq!(del_obs_res, "Observations deleted");
let del_rel_handler = DeleteRelationsHandler;
let del_rel_res = del_rel_handler.execute(json!({
"relations": [{"from": "Alpha", "to": "Beta", "relation_type": "links_to"}]
}), state.clone()).await.unwrap();
let del_rel_res = del_rel_handler
.execute(
json!({
"relations": [{"from": "Alpha", "to": "Beta", "relation_type": "links_to"}]
}),
state.clone(),
)
.await
.unwrap();
assert_eq!(del_rel_res, "Relations deleted");
let bcast_handler = AgentSignalsHandler;
let bcast_res = bcast_handler.execute(json!({
"action": "broadcast",
"sender": "agent1",
"signal_type": "task_completed",
"payload": "fix_bug"
}), state.clone()).await.unwrap();
let bcast_res = bcast_handler
.execute(
json!({
"action": "broadcast",
"sender": "agent1",
"signal_type": "task_completed",
"payload": "fix_bug"
}),
state.clone(),
)
.await
.unwrap();
assert!(bcast_res.contains("Broadcasted signal"));
let qsignal_handler = AgentSignalsHandler;
let qsignal_res = qsignal_handler.execute(json!({
"action": "query",
"sender": "agent1"
}), state.clone()).await.unwrap();
let qsignal_res = qsignal_handler
.execute(
json!({
"action": "query",
"sender": "agent1"
}),
state.clone(),
)
.await
.unwrap();
assert!(qsignal_res.contains("task_completed"));
let read_paged_handler = ReadGraphHandler;
let paged_res = read_paged_handler
@@ -1563,4 +1648,3 @@ mod tests {
assert_eq!(parsed_paged["has_more"], true);
}
}
+24 -22
View File
@@ -38,32 +38,34 @@ impl McpTool for ProcessLogsHandler {
}
ProcessLogAction::Get => {
let max_lines = tool_args.max_lines.unwrap_or(100);
let result = tokio::task::spawn_blocking(move || -> crate::error::Result<String> {
let mut file = File::open(&safe_path).map_err(|e| {
crate::error::AppError::Internal(format!("Failed to open file: {}", e))
})?;
let len = file.metadata().map_err(|e| e.to_string())?.len();
let result =
tokio::task::spawn_blocking(move || -> crate::error::Result<String> {
let mut file = File::open(&safe_path).map_err(|e| {
crate::error::AppError::Internal(format!("Failed to open file: {}", e))
})?;
let len = file.metadata().map_err(|e| e.to_string())?.len();
let read_size = std::cmp::min(16 * 1024, len);
file.seek(SeekFrom::End(-(read_size as i64)))
.map_err(|e| e.to_string())?;
let read_size = std::cmp::min(16 * 1024, len);
file.seek(SeekFrom::End(-(read_size as i64)))
.map_err(|e| e.to_string())?;
let mut vec_buf = Vec::new();
file.read_to_end(&mut vec_buf)
.map_err(|e| e.to_string())?;
let mut vec_buf = Vec::new();
file.read_to_end(&mut vec_buf).map_err(|e| e.to_string())?;
let buffer = String::from_utf8_lossy(&vec_buf).to_string();
let lines: Vec<&str> = buffer.lines().collect();
let recent_lines = if lines.len() > max_lines {
lines[lines.len() - max_lines..].join("\n")
} else {
buffer
};
let buffer = String::from_utf8_lossy(&vec_buf).to_string();
let lines: Vec<&str> = buffer.lines().collect();
let recent_lines = if lines.len() > max_lines {
lines[lines.len() - max_lines..].join("\n")
} else {
buffer
};
Ok(recent_lines)
})
.await
.map_err(|e| crate::error::AppError::Internal(format!("Task panic: {}", e)))??;
Ok(recent_lines)
})
.await
.map_err(|e| {
crate::error::AppError::Internal(format!("Task panic: {}", e))
})??;
Ok(result)
}
+134 -103
View File
@@ -110,11 +110,10 @@ impl McpTool for SearchErrorFixesHandler {
} else if st_lower.contains(&sol_lower) || sol_lower.contains(&st_lower) {
score += 0.5;
}
if let Some(emb) = &f.embedding {
if !query_emb.is_empty() {
if let Some(emb) = &f.embedding
&& !query_emb.is_empty() {
score += crate::embedding::cosine_similarity(&query_emb, emb);
}
}
(f, score)
})
.filter(|(_, score)| *score > 0.1)
@@ -198,11 +197,10 @@ impl McpTool for LogCodeChangeHandler {
if let Some(range) = &req.line_range {
description = format!("{} [Line Range: {}]", description, range);
}
if let Some(symbols) = &req.symbol_references {
if !symbols.is_empty() {
if let Some(symbols) = &req.symbol_references
&& !symbols.is_empty() {
description = format!("{} [Symbols: {}]", description, symbols.join(", "));
}
}
if state.ollama.is_available().await {
let prompt = format!(
@@ -323,13 +321,14 @@ impl McpTool for QueryRecentChangesHandler {
}
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: QueryRecentChangesTool = serde_json::from_value(args).unwrap_or(QueryRecentChangesTool {
namespace: None,
repo_name: None,
vcs_type: None,
limit: None,
offset: None,
});
let req: QueryRecentChangesTool =
serde_json::from_value(args).unwrap_or(QueryRecentChangesTool {
namespace: None,
repo_name: None,
vcs_type: None,
limit: None,
offset: None,
});
let limit = req.limit.unwrap_or(50);
let offset = req.offset.unwrap_or(0);
let data = state.code.ledger.read_with(|l| {
@@ -337,25 +336,22 @@ impl McpTool for QueryRecentChangesHandler {
.iter()
.rev()
.filter(|c| {
if let Some(ns) = &req.namespace {
if !c.namespace.eq_ignore_ascii_case(ns) {
if let Some(ns) = &req.namespace
&& !c.namespace.eq_ignore_ascii_case(ns) {
return false;
}
}
if let Some(repo) = &req.repo_name {
if c.repo_name
if let Some(repo) = &req.repo_name
&& c.repo_name
.as_ref()
.map(|rn| !rn.eq_ignore_ascii_case(repo))
.unwrap_or(true)
{
return false;
}
}
if let Some(vcs) = &req.vcs_type {
if !c.effective_vcs().eq_ignore_ascii_case(vcs) {
if let Some(vcs) = &req.vcs_type
&& !c.effective_vcs().eq_ignore_ascii_case(vcs) {
return false;
}
}
true
})
.skip(offset)
@@ -386,7 +382,7 @@ impl McpTool for DecisionsHandler {
let req: DecisionsTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let ns = req
.namespace
.unwrap_or_else(|| crate::models::default_namespace());
.unwrap_or_else(crate::models::default_namespace);
match req.action {
DecisionAction::Log => {
let title = req.title.ok_or_else(|| {
@@ -464,14 +460,14 @@ impl McpTool for DecisionsHandler {
state.code.adrs.modify(|adrs| {
let target_pos = adrs.iter().position(|a| a.id.eq_ignore_ascii_case(&id));
if let Some(pos) = target_pos {
if let Some(superseded_id) = &req.supersedes {
if let Some(s_pos) = adrs.iter().position(|a| a.id.eq_ignore_ascii_case(superseded_id)) {
if s_pos != pos {
if let Some(superseded_id) = &req.supersedes
&& let Some(s_pos) = adrs
.iter()
.position(|a| a.id.eq_ignore_ascii_case(superseded_id))
&& s_pos != pos {
adrs[s_pos].status = "superseded".to_string();
adrs_to_index.push(adrs[s_pos].clone());
}
}
}
let a = &mut adrs[pos];
if let Some(t) = req.title {
@@ -488,7 +484,9 @@ impl McpTool for DecisionsHandler {
}
if let Some(s) = req.status {
let s_lower = s.to_ascii_lowercase();
if (s_lower == "implemented" || s_lower == "resolved") && a.resolved_at.is_none() {
if (s_lower == "implemented" || s_lower == "resolved")
&& a.resolved_at.is_none()
{
a.resolved_at = Some(crate::handlers::utils::now_secs());
} else if s_lower != "implemented" && s_lower != "resolved" {
a.resolved_at = None;
@@ -535,11 +533,15 @@ impl McpTool for DecisionsHandler {
&format!("Updated {}: {}", adr.id, adr.title),
Some(&adr.status),
);
Ok(format!("Updated decision {}: {} (status: {})", adr.id, adr.title, adr.status))
} else {
Err(crate::error::AppError::Internal(
format!("Decision with id '{}' not found", id),
Ok(format!(
"Updated decision {}: {} (status: {})",
adr.id, adr.title, adr.status
))
} else {
Err(crate::error::AppError::Internal(format!(
"Decision with id '{}' not found",
id
)))
}
}
DecisionAction::Query => {
@@ -554,9 +556,17 @@ impl McpTool for DecisionsHandler {
}
if let Some(q) = &req.query {
crate::handlers::utils::contains_ignore_ascii_case(&a.title, q)
|| crate::handlers::utils::contains_ignore_ascii_case(&a.context, q)
|| crate::handlers::utils::contains_ignore_ascii_case(&a.decision, q)
|| crate::handlers::utils::contains_ignore_ascii_case(&a.consequence, q)
|| crate::handlers::utils::contains_ignore_ascii_case(
&a.context, q,
)
|| crate::handlers::utils::contains_ignore_ascii_case(
&a.decision,
q,
)
|| crate::handlers::utils::contains_ignore_ascii_case(
&a.consequence,
q,
)
} else {
true
}
@@ -632,17 +642,14 @@ impl McpTool for TechDebtHandler {
let req: TechDebtTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let ns = req
.namespace
.unwrap_or_else(|| crate::models::default_namespace());
.unwrap_or_else(crate::models::default_namespace);
match req.action {
TechDebtAction::Log => {
let desc = req.description.or(req.title).ok_or_else(|| {
crate::error::AppError::Internal("Missing required parameter 'description' for action 'log'. Next step: Provide tech debt 'description' in request and retry.".to_string())
})?;
let ideal = req.ideal_solution.unwrap_or_default();
let text_to_embed = format!(
"Description: {}\nIdeal Solution: {}",
desc, ideal
);
let text_to_embed = format!("Description: {}\nIdeal Solution: {}", desc, ideal);
let embedding = crate::embedding::generate_embedding_async(text_to_embed)
.await
.ok();
@@ -665,18 +672,21 @@ impl McpTool for TechDebtHandler {
effort_estimate: req.effort_estimate,
});
if debts.len() > 300 {
let severity_rank = |sev: Option<&str>| match sev.unwrap_or("").to_lowercase().as_str() {
"critical" => 4,
"high" => 3,
"medium" => 2,
"low" => 1,
_ => 1,
};
if let Some((idx_to_remove, _)) = debts.iter().enumerate().min_by_key(|(_, d)| {
let status_score = if d.is_resolved { 0 } else { 10 };
let sev_score = severity_rank(d.severity.as_deref());
(status_score + sev_score, d.created_at)
}) {
let severity_rank =
|sev: Option<&str>| match sev.unwrap_or("").to_lowercase().as_str() {
"critical" => 4,
"high" => 3,
"medium" => 2,
"low" => 1,
_ => 1,
};
if let Some((idx_to_remove, _)) =
debts.iter().enumerate().min_by_key(|(_, d)| {
let status_score = if d.is_resolved { 0 } else { 10 };
let sev_score = severity_rank(d.severity.as_deref());
(status_score + sev_score, d.created_at)
})
{
debts.remove(idx_to_remove);
}
}
@@ -712,9 +722,7 @@ impl McpTool for TechDebtHandler {
let data = state.code.tech_debts.read_with(|debts| {
let filtered: Vec<_> = debts
.iter()
.filter(|d| {
d.namespace == ns && (inc || !d.is_resolved)
})
.filter(|d| d.namespace == ns && (inc || !d.is_resolved))
.map(|d| match level {
"compact" => serde_json::json!({
"id": d.id,
@@ -811,7 +819,8 @@ impl McpTool for OmniSearchHandler {
let mut ranked_items: Vec<_> = rrf_scores.into_values().collect();
ranked_items.sort_by(|a, b| b.0.total_cmp(&a.0));
let matches: Vec<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 mut kg_results = serde_json::Map::new();
@@ -1263,25 +1272,21 @@ impl McpTool for ManageCheckpointHandler {
)));
}
if let Ok(graph_content) = std::fs::read_to_string(target_dir.join("graph.json")) {
if let Ok(graph) = serde_json::from_str(&graph_content) {
if let Ok(graph_content) = std::fs::read_to_string(target_dir.join("graph.json"))
&& let Ok(graph) = serde_json::from_str(&graph_content) {
state.graph.modify(|g| *g = graph);
}
}
if let Ok(tasks_content) = std::fs::read_to_string(target_dir.join("tasks.json")) {
if let Ok(tasks) = serde_json::from_str(&tasks_content) {
if let Ok(tasks_content) = std::fs::read_to_string(target_dir.join("tasks.json"))
&& let Ok(tasks) = serde_json::from_str(&tasks_content) {
state.project.tasks.modify(|t| *t = tasks);
}
}
if let Ok(debts_content) =
std::fs::read_to_string(target_dir.join("tech_debts.json"))
{
if let Ok(debts) = serde_json::from_str(&debts_content) {
&& let Ok(debts) = serde_json::from_str(&debts_content) {
state.code.tech_debts.modify(|d| *d = debts);
}
}
Ok(format!("Checkpoint '{}' restored successfully.", name))
}
@@ -1290,11 +1295,10 @@ impl McpTool for ManageCheckpointHandler {
let checkpoints_dir = state.base_dir.join("checkpoints");
if let Ok(entries) = std::fs::read_dir(&checkpoints_dir) {
for entry in entries.flatten() {
if entry.path().is_dir() {
if let Some(n) = entry.file_name().to_str() {
if entry.path().is_dir()
&& let Some(n) = entry.file_name().to_str() {
list.push(serde_json::json!({"type": "checkpoint", "name": n}));
}
}
}
}
let snaps = state.project.snapshots.read_with(|snaps| snaps.clone());
@@ -1461,13 +1465,11 @@ impl McpTool for GetNextActionableTasksHandler {
continue;
}
if let Some(branch) = &req.git_branch {
if let Some(tb) = &task.git_branch {
if tb != branch {
if let Some(branch) = &req.git_branch
&& let Some(tb) = &task.git_branch
&& tb != branch {
continue;
}
}
}
let unblocked = task.dependencies.is_empty()
|| task.dependencies.iter().all(|d| completed_ids.contains(d));
@@ -1509,7 +1511,9 @@ impl McpTool for HypothesesHandler {
match req.action {
HypothesisAction::Log => {
let hyp_text = req.hypothesis.ok_or_else(|| {
crate::error::AppError::Internal("Missing required 'hypothesis' for action 'log'".to_string())
crate::error::AppError::Internal(
"Missing required 'hypothesis' for action 'log'".to_string(),
)
})?;
let hyp_id = format!(
"HYP-{}",
@@ -1536,17 +1540,16 @@ impl McpTool for HypothesesHandler {
let filtered: Vec<_> = hypotheses
.into_iter()
.filter(|h| {
if let Some(tid) = &req.task_id {
if h.task_id.as_ref() != Some(tid) {
if let Some(tid) = &req.task_id
&& h.task_id.as_ref() != Some(tid) {
return false;
}
}
if let Some(q) = &req.query {
let lq = q.to_lowercase();
return h.hypothesis.to_lowercase().contains(&lq)
|| h.evidence
.as_ref()
.map_or(false, |e| e.to_lowercase().contains(&lq));
.is_some_and(|e| e.to_lowercase().contains(&lq));
}
true
})
@@ -1656,13 +1659,19 @@ impl McpTool for AgentSignalsHandler {
match req.action {
AgentSignalAction::Broadcast => {
let sender = req.sender.ok_or_else(|| {
crate::error::AppError::Internal("Missing required 'sender' for action 'broadcast'".to_string())
crate::error::AppError::Internal(
"Missing required 'sender' for action 'broadcast'".to_string(),
)
})?;
let signal_type = req.signal_type.ok_or_else(|| {
crate::error::AppError::Internal("Missing required 'signal_type' for action 'broadcast'".to_string())
crate::error::AppError::Internal(
"Missing required 'signal_type' for action 'broadcast'".to_string(),
)
})?;
let payload = req.payload.ok_or_else(|| {
crate::error::AppError::Internal("Missing required 'payload' for action 'broadcast'".to_string())
crate::error::AppError::Internal(
"Missing required 'payload' for action 'broadcast'".to_string(),
)
})?;
let timestamp = std::time::SystemTime::now()
@@ -1715,25 +1724,21 @@ impl McpTool for AgentSignalsHandler {
signals
.iter()
.filter(|s| {
if let Some(ttl) = s.ttl_seconds {
if now > s.timestamp + ttl {
if let Some(ttl) = s.ttl_seconds
&& now > s.timestamp + ttl {
return false;
}
}
if let Some(sender) = &req.sender {
if s.sender.to_lowercase() != sender.to_lowercase() {
if let Some(sender) = &req.sender
&& s.sender.to_lowercase() != sender.to_lowercase() {
return false;
}
}
if let Some(st) = &req.signal_type {
if s.signal_type.to_lowercase() != st.to_lowercase() {
if let Some(st) = &req.signal_type
&& s.signal_type.to_lowercase() != st.to_lowercase() {
return false;
}
}
true
})
.cloned()
.take(req.limit.unwrap_or(20))
.take(req.limit.unwrap_or(20)).cloned()
.collect::<Vec<_>>()
});
@@ -2110,7 +2115,10 @@ mod tests {
assert!(q_dec_res.contains("Fast async API routing"));
let q_by_consequence = handler_dec
.execute(serde_json::json!({"action": "query", "query": "Fast async"}), state.clone())
.execute(
serde_json::json!({"action": "query", "query": "Fast async"}),
state.clone(),
)
.await
.unwrap();
assert!(q_by_consequence.contains("Use Axum"));
@@ -2133,7 +2141,10 @@ mod tests {
assert!(update_res.contains("implemented"));
let q_after_update = handler_dec
.execute(serde_json::json!({"action": "query", "include_body": false}), state.clone())
.execute(
serde_json::json!({"action": "query", "include_body": false}),
state.clone(),
)
.await
.unwrap();
assert!(q_after_update.contains("implemented"));
@@ -2143,7 +2154,10 @@ mod tests {
assert!(q_after_update.contains("resolved_at"));
let del_dec_res = handler_dec
.execute(serde_json::json!({"action": "delete", "id": "ADR-0001"}), state.clone())
.execute(
serde_json::json!({"action": "delete", "id": "ADR-0001"}),
state.clone(),
)
.await;
assert!(del_dec_res.is_ok());
@@ -2163,14 +2177,20 @@ mod tests {
assert!(td_res.contains("Tech debt logged"));
let list_td_res = handler_td
.execute(serde_json::json!({"action": "list", "include_resolved": true}), state.clone())
.execute(
serde_json::json!({"action": "list", "include_resolved": true}),
state.clone(),
)
.await
.unwrap();
assert!(list_td_res.contains("Replace unwraps"));
let debt_id = state.code.tech_debts.read_with(|debts| debts[0].id.clone());
let res_td_res = handler_td
.execute(serde_json::json!({"action": "resolve", "id": debt_id}), state.clone())
.execute(
serde_json::json!({"action": "resolve", "id": debt_id}),
state.clone(),
)
.await;
assert!(res_td_res.is_ok());
@@ -2394,7 +2414,10 @@ mod tests {
)
.await
.unwrap();
assert!(rest_state_res.contains("restored successfully") || rest_state_res.contains("restored memory state from snapshot"));
assert!(
rest_state_res.contains("restored successfully")
|| rest_state_res.contains("restored memory state from snapshot")
);
let mg_list = mg_chk
.execute(serde_json::json!({"action": "list"}), state.clone())
@@ -2607,7 +2630,10 @@ mod tests {
// DeleteDecision non-existent
let del_dec_err = handler_dec
.execute(serde_json::json!({"action": "delete", "id": "ADR-9999"}), state.clone())
.execute(
serde_json::json!({"action": "delete", "id": "ADR-9999"}),
state.clone(),
)
.await;
assert!(del_dec_err.is_err());
@@ -2882,9 +2908,14 @@ mod tests {
state.code.tech_debts.read_with(|debts| {
assert_eq!(debts.len(), 300);
assert!(debts.iter().any(|d| d.id == "critical-debt"), "Critical unresolved debt must be retained");
assert!(!debts.iter().any(|d| d.id == "resolved-debt"), "Resolved debt should have been evicted first");
assert!(
debts.iter().any(|d| d.id == "critical-debt"),
"Critical unresolved debt must be retained"
);
assert!(
!debts.iter().any(|d| d.id == "resolved-debt"),
"Resolved debt should have been evicted first"
);
});
}
}
-1
View File
@@ -10,4 +10,3 @@ pub mod tasks;
pub mod utils;
pub mod vision;
pub mod workspaces;
+30 -14
View File
@@ -29,7 +29,9 @@ impl McpTool for HandoffMemosHandler {
let content = req.content.ok_or_else(|| {
crate::error::AppError::Internal("Missing required parameter 'content' for action 'leave'. Next step: Provide non-empty 'content' string in request and retry.".to_string())
})?;
let ns = req.namespace.unwrap_or_else(|| crate::models::default_namespace());
let ns = req
.namespace
.unwrap_or_else(crate::models::default_namespace);
state.telemetry.handoff_memos.modify(|memos| {
memos.push(crate::models::HandoffMemo {
id: uuid::Uuid::new_v4().to_string(),
@@ -109,11 +111,13 @@ impl McpTool for AddSessionSummaryHandler {
});
if summaries.len() > 200 {
let now = crate::handlers::utils::now_secs();
if let Some((idx_to_remove, _)) = summaries.iter().enumerate().min_by_key(|(_, s)| {
let is_expired = s.expires_at.map(|exp| exp < now).unwrap_or(false);
let status_score = if is_expired { 0 } else { 1 };
(status_score, s.timestamp)
}) {
if let Some((idx_to_remove, _)) =
summaries.iter().enumerate().min_by_key(|(_, s)| {
let is_expired = s.expires_at.map(|exp| exp < now).unwrap_or(false);
let status_score = if is_expired { 0 } else { 1 };
(status_score, s.timestamp)
})
{
summaries.remove(idx_to_remove);
}
}
@@ -143,13 +147,25 @@ impl McpTool for GenerateStandupReportHandler {
let cutoff = crate::handlers::utils::now_secs().saturating_sub(req.hours_lookback * 3600);
let filtered_tasks = state.project.tasks.read_with(|items| {
items.iter().filter(|t| t.updated_at >= cutoff).cloned().collect::<Vec<_>>()
items
.iter()
.filter(|t| t.updated_at >= cutoff)
.cloned()
.collect::<Vec<_>>()
});
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| {
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!({
@@ -178,8 +194,7 @@ impl McpTool for PromoteToEntityHandler {
}
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: PromoteToEntityTool =
serde_json::from_value(args).map_err(|e| e.to_string())?;
let req: PromoteToEntityTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let entity_name = req.entity_name.clone();
let obs = req.content.clone();
@@ -236,7 +251,10 @@ mod tests {
assert_eq!(res, "Handoff memo left.");
let res2 = handoff_handler
.execute(json!({"action": "read", "namespace": "global"}), state.clone())
.execute(
json!({"action": "read", "namespace": "global"}),
state.clone(),
)
.await
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
.unwrap();
@@ -266,5 +284,3 @@ mod tests {
assert!(!res4.is_empty());
}
}
+28 -16
View File
@@ -39,7 +39,9 @@ pub fn extract_adrs(text: &str) -> Vec<String> {
if i + 3 <= len && lower_chars[i..i + 3] == ['a', 'd', 'r'] {
let mut cursor = i + 3;
// Skip optional '-', ':', or whitespace
while cursor < len && (chars[cursor] == '-' || chars[cursor] == ':' || chars[cursor] == ' ') {
while cursor < len
&& (chars[cursor] == '-' || chars[cursor] == ':' || chars[cursor] == ' ')
{
cursor += 1;
}
// Collect consecutive digits
@@ -80,11 +82,17 @@ pub fn extract_tech_debts(text: &str) -> Vec<String> {
while i < len {
if i + 2 <= len && lower_chars[i..i + 2] == ['t', 'd'] {
let mut cursor = i + 2;
while cursor < len && (chars[cursor] == '-' || chars[cursor] == ':' || chars[cursor] == ' ') {
while cursor < len
&& (chars[cursor] == '-' || chars[cursor] == ':' || chars[cursor] == ' ')
{
cursor += 1;
}
let start = cursor;
while cursor < len && (chars[cursor].is_ascii_alphanumeric() || chars[cursor] == '_' || chars[cursor] == '-') {
while cursor < len
&& (chars[cursor].is_ascii_alphanumeric()
|| chars[cursor] == '_'
|| chars[cursor] == '-')
{
cursor += 1;
}
if cursor > start {
@@ -117,11 +125,17 @@ pub fn extract_tasks(text: &str) -> Vec<String> {
while i < len {
if i + 4 <= len && lower_chars[i..i + 4] == ['t', 'a', 's', 'k'] {
let mut cursor = i + 4;
while cursor < len && (chars[cursor] == '-' || chars[cursor] == ':' || chars[cursor] == ' ') {
while cursor < len
&& (chars[cursor] == '-' || chars[cursor] == ':' || chars[cursor] == ' ')
{
cursor += 1;
}
let start = cursor;
while cursor < len && (chars[cursor].is_ascii_alphanumeric() || chars[cursor] == '_' || chars[cursor] == '-') {
while cursor < len
&& (chars[cursor].is_ascii_alphanumeric()
|| chars[cursor] == '_'
|| chars[cursor] == '-')
{
cursor += 1;
}
if cursor > start {
@@ -162,8 +176,7 @@ pub fn cascade_unblock_tasks(state: &MemoryState) -> Vec<String> {
} else {
let all_deps_completed = t.dependencies.iter().all(|dep_id| {
tasks_snapshot.iter().any(|other| {
(other.id == *dep_id || other.title == *dep_id)
&& !other.is_active()
(other.id == *dep_id || other.title == *dep_id) && !other.is_active()
})
});
if all_deps_completed {
@@ -226,19 +239,17 @@ pub fn recalculate_milestones(state: &MemoryState) -> Vec<String> {
}
updated.push(m.id.clone());
}
} else if completed > 0
} else if (completed > 0
|| tasks_snapshot.iter().any(|t| {
m.task_ids.iter().any(|tid| tid == &t.id || tid == &t.title)
&& (t.status == "in_progress" || t.status == "active")
})
{
if !m.status.eq_ignore_ascii_case("in_progress")
}))
&& !m.status.eq_ignore_ascii_case("in_progress")
&& !m.status.eq_ignore_ascii_case("completed")
{
m.status = "in_progress".to_string();
updated.push(m.id.clone());
}
}
}
});
@@ -275,11 +286,10 @@ pub async fn reconcile_commit_or_code_change(
if adr.id.eq_ignore_ascii_case(cand) {
return true;
}
if let (Some(n1), Some(n2)) = (adr_numeric_id(&adr.id), adr_numeric_id(cand)) {
if n1 == n2 {
if let (Some(n1), Some(n2)) = (adr_numeric_id(&adr.id), adr_numeric_id(cand))
&& n1 == n2 {
return true;
}
}
false
});
@@ -324,7 +334,9 @@ pub async fn reconcile_commit_or_code_change(
if d.is_resolved {
continue;
}
let matches_id = td_candidates.iter().any(|cand| d.id.eq_ignore_ascii_case(cand));
let matches_id = td_candidates
.iter()
.any(|cand| d.id.eq_ignore_ascii_case(cand));
let matches_file = file_path
.map(|fp| d.file_path.as_deref() == Some(fp))
.unwrap_or(false)
+10 -10
View File
@@ -366,14 +366,14 @@ impl McpTool for TasksHandler {
let mut updated_task: Option<Task> = None;
state.project.tasks.modify(|tasks| {
if let Some(task) = tasks.iter_mut().find(|t| t.id == id || t.title == id) {
if let Some(ac) = task.acceptance_criteria.iter_mut().next() {
if let Some(ac) = task.acceptance_criteria.first_mut() {
if ac.is_met {
already_met = true;
} else {
ac.is_met = true;
success = true;
task.updated_at = crate::handlers::utils::now_secs();
updated_task = Some(task.clone());
task.updated_at = crate::handlers::utils::now_secs();
updated_task = Some(task.clone());
}
} else {
task.acceptance_criteria
@@ -444,7 +444,7 @@ impl McpTool for MilestonesHandler {
})?;
let ns = req
.namespace
.unwrap_or_else(|| crate::models::default_namespace());
.unwrap_or_else(crate::models::default_namespace);
state.project.milestones.modify(|ms| {
ms.push(crate::models::Milestone {
id: uuid::Uuid::new_v4().to_string(),
@@ -477,16 +477,16 @@ impl McpTool for MilestonesHandler {
for m in ms.iter_mut() {
if m.id == id {
m.status = status.clone();
if status.eq_ignore_ascii_case("completed") || status.eq_ignore_ascii_case("done") {
if m.completed_at.is_none() {
if (status.eq_ignore_ascii_case("completed")
|| status.eq_ignore_ascii_case("done"))
&& m.completed_at.is_none() {
m.completed_at = Some(crate::handlers::utils::now_secs());
}
}
if let Some(task_ids) = &req.task_ids {
m.task_ids = task_ids.clone();
if let Some(deliverables) = &req.deliverables {
m.deliverables = deliverables.clone();
}
if let Some(deliverables) = &req.deliverables {
m.deliverables = deliverables.clone();
}
}
found = true;
break;
+57 -41
View File
@@ -191,11 +191,10 @@ pub fn html_to_markdown(html: &str) -> String {
out.push_str("* ");
}
}
"/li" => {
if !in_cell {
"/li"
if !in_cell => {
out.push('\n');
}
}
_ => {}
}
} else if c == '&' {
@@ -298,38 +297,46 @@ pub async fn ensure_ocr_engine() -> Option<&'static OcrEngine> {
if let Some(engine) = OCR_ENGINE.get() {
return Some(engine);
}
let cache_dir = dirs::home_dir()?.join(".gemini/mcp_memory/models");
let _ = std::fs::create_dir_all(&cache_dir);
let det_path = cache_dir.join("text-detection.rten");
if !det_path.exists() || std::fs::metadata(&det_path).map(|m| m.len()).unwrap_or(0) < 100_000 {
match reqwest::get("https://ocrs-models.s3-accelerate.amazonaws.com/text-detection.rten").await {
match reqwest::get("https://ocrs-models.s3-accelerate.amazonaws.com/text-detection.rten")
.await
{
Ok(resp) => {
if let Ok(bytes) = resp.bytes().await {
if let Err(e) = std::fs::write(&det_path, &bytes) {
tracing::error!("Failed to write text-detection.rten to disk: {}", e);
}
} else {
tracing::error!("Failed to read bytes from text-detection.rten download response");
tracing::error!(
"Failed to read bytes from text-detection.rten download response"
);
}
},
}
Err(e) => tracing::error!("Failed to download text-detection.rten: {}", e),
}
}
let rec_path = cache_dir.join("text-recognition.rten");
if !rec_path.exists() || std::fs::metadata(&rec_path).map(|m| m.len()).unwrap_or(0) < 100_000 {
match reqwest::get("https://ocrs-models.s3-accelerate.amazonaws.com/text-recognition.rten").await {
match reqwest::get("https://ocrs-models.s3-accelerate.amazonaws.com/text-recognition.rten")
.await
{
Ok(resp) => {
if let Ok(bytes) = resp.bytes().await {
if let Err(e) = std::fs::write(&rec_path, &bytes) {
tracing::error!("Failed to write text-recognition.rten to disk: {}", e);
}
} else {
tracing::error!("Failed to read bytes from text-recognition.rten download response");
tracing::error!(
"Failed to read bytes from text-recognition.rten download response"
);
}
},
}
Err(e) => tracing::error!("Failed to download text-recognition.rten: {}", e),
}
}
@@ -350,10 +357,7 @@ pub async fn ensure_ocr_engine() -> Option<&'static OcrEngine> {
pub fn perform_ocrs_ocr(engine: &OcrEngine, img: &image::DynamicImage) -> Option<String> {
let rgb_img = img.to_rgb8();
let img_source = ocrs::ImageSource::from_bytes(
rgb_img.as_raw(),
rgb_img.dimensions(),
).ok()?;
let img_source = ocrs::ImageSource::from_bytes(rgb_img.as_raw(), rgb_img.dimensions()).ok()?;
let ocr_input = engine.prepare_input(img_source).ok()?;
let word_rects = engine.detect_words(&ocr_input).ok()?;
@@ -365,7 +369,7 @@ pub fn perform_ocrs_ocr(engine: &OcrEngine, img: &image::DynamicImage) -> Option
out.push_str(&text.to_string());
out.push('\n');
}
let trimmed = out.trim();
if trimmed.is_empty() {
None
@@ -482,23 +486,22 @@ pub async fn process_clipboard_snapshot(state: Arc<MemoryState>) {
if let Some(engine) = ensure_ocr_engine().await {
let img_clone = img.clone();
ocr_text_opt = tokio::task::spawn_blocking(move || {
perform_ocrs_ocr(engine, &img_clone)
})
.await
.ok()
.flatten();
ocr_text_opt =
tokio::task::spawn_blocking(move || perform_ocrs_ocr(engine, &img_clone))
.await
.ok()
.flatten();
}
let mut cache = state.clipboard_cache.write().await;
// Check if this is an identical image captured rapidly
let is_duplicate = if let Some(ref prev) = cache.last_image {
prev.width == w && prev.height == h && prev.size_bytes == size_bytes
} else {
false
};
if !is_duplicate {
// Index OCR text into Tantivy for omni_search
if let Some(ref text) = ocr_text_opt {
@@ -563,11 +566,10 @@ pub async fn process_clipboard_snapshot(state: Arc<MemoryState>) {
};
let mut cache = state.clipboard_cache.write().await;
if let Some(ref prev) = cache.last_text {
if prev.text == cached.text {
if let Some(ref prev) = cache.last_text
&& prev.text == cached.text {
return;
}
}
cache.last_text = Some(cached.clone());
cache
.history
@@ -669,7 +671,7 @@ pub fn spawn_clipboard_listener(state: Arc<MemoryState>) {
tokio::spawn(async move {
while let Some(()) = rx.recv().await {
tokio::time::sleep(tokio::time::Duration::from_millis(150)).await;
while let Ok(_) = rx.try_recv() {}
while rx.try_recv().is_ok() {}
process_clipboard_snapshot(Arc::clone(&state)).await;
}
@@ -843,8 +845,8 @@ impl McpTool for ClipboardHandler {
ClipboardAction::Read => {
let engine = ensure_ocr_engine().await;
let out =
tokio::task::spawn_blocking(move || -> crate::error::Result<serde_json::Map<String, Value>> {
let out = tokio::task::spawn_blocking(
move || -> crate::error::Result<serde_json::Map<String, Value>> {
let mut out = serde_json::Map::new();
if let Some(text) = get_native_clipboard_text() {
@@ -860,8 +862,19 @@ impl McpTool for ClipboardHandler {
let rgb_img = img.into_rgb8();
let mut jpeg_bytes = std::io::Cursor::new(Vec::new());
let mut encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut jpeg_bytes, 88);
if encoder.encode(&rgb_img, rgb_img.width(), rgb_img.height(), image::ExtendedColorType::Rgb8).is_ok() {
let mut encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(
&mut jpeg_bytes,
88,
);
if encoder
.encode(
&rgb_img,
rgb_img.width(),
rgb_img.height(),
image::ExtendedColorType::Rgb8,
)
.is_ok()
{
let bytes = jpeg_bytes.into_inner();
let cache_dir = dirs::home_dir()
@@ -876,17 +889,20 @@ impl McpTool for ClipboardHandler {
out.insert("image_path_wsl".into(), json!(wsl_path));
}
}
if let Some(eng) = engine {
if let Some(ocr_text) = perform_ocrs_ocr(eng, &dynamic_img) {
out.insert("image_analysis".to_string(), json!(ocr_text.trim()));
if let Some(eng) = engine
&& let Some(ocr_text) = perform_ocrs_ocr(eng, &dynamic_img) {
out.insert(
"image_analysis".to_string(),
json!(ocr_text.trim()),
);
}
}
}
Ok(out)
})
.await
.map_err(|e| crate::error::AppError::Internal(format!("Task panic: {}", e)))??;
},
)
.await
.map_err(|e| crate::error::AppError::Internal(format!("Task panic: {}", e)))??;
state.record_activity("clipboard", "Read contents from OS clipboard", None);
Ok::<String, crate::error::AppError>(serde_json::to_string_pretty(&Value::Object(
+23 -15
View File
@@ -6,14 +6,6 @@ use async_trait::async_trait;
use serde_json::Value;
use std::sync::Arc;
pub struct SnippetsHandler;
#[async_trait]
@@ -57,7 +49,7 @@ impl McpTool for SnippetsHandler {
embedding,
namespace: req
.namespace
.unwrap_or_else(|| crate::models::default_namespace()),
.unwrap_or_else(crate::models::default_namespace),
repo_name: req.repo_name,
origin_file: req.origin_file,
line_range: req.line_range,
@@ -89,8 +81,9 @@ impl McpTool for SnippetsHandler {
let mut scored: Vec<_> = snippets
.into_iter()
.filter_map(|s| {
let text = format!("{} {} {} {}", s.name, s.description, s.language, s.code)
.to_lowercase();
let text =
format!("{} {} {} {}", s.name, s.description, s.language, s.code)
.to_lowercase();
let mut score = 0.0;
for tok in &q_tokens {
if text.contains(tok) {
@@ -143,9 +136,9 @@ impl McpTool for SnippetsHandler {
|| s.tags.iter().any(|t| contains_ignore_ascii_case(t, &query));
let matches_tags = tags_filter.is_empty()
|| tags_filter
.iter()
.any(|tf| s.tags.iter().any(|st| contains_ignore_ascii_case(st, tf)));
|| tags_filter.iter().any(|tf| {
s.tags.iter().any(|st| contains_ignore_ascii_case(st, tf))
});
matches_text && matches_tags
})
@@ -352,13 +345,28 @@ impl McpTool for SemanticCodeSearchHandler {
}
let mut out = format!("Semantic Search Results for '{}':\n", tool_args.query);
for res in results {
for res in &results {
out.push_str(&format!(
"- [{:.2}] {}: {}\n",
res.score, res.title, res.body
));
}
if state.ollama.is_available().await {
let prompt = format!(
"You are an expert software architect. Synthesize a concise GraphRAG summary of the following graph nodes relevant to the query: '{}'.\n\nData:\n{}",
tool_args.query, out
);
if let Ok(summary) = state
.ollama
.generate(&prompt, None, Some("Respond clearly and concisely."))
.await
{
out.push_str("\n\n--- Local GraphRAG Summary ---\n");
out.push_str(&summary);
}
}
Ok(out)
}
}