refactor: consolidate nvim crates, extract server library, and update workspace dependencies

This commit is contained in:
Riz Ashraf committed 2026-10-04 01:42:59 +01:00
1 parent a083719cf1
commit 533adfd41b
53 files changed
+5967 -1230

No files matched your search

+326 -3
View File
@@ -61,7 +61,16 @@ impl McpResource for GraphEntitiesResource {
tokio::task::spawn_blocking(move || -> crate::error::Result<String> {
let graph = state_clone.graph.cache.read().unwrap();
let data: Vec<_> = graph.entities.values().collect();
Ok(serde_json::to_string_pretty(&data)?)
if data.len() > 100 {
let truncated: Vec<_> = data.iter().take(100).cloned().collect();
let res = serde_json::json!({
"_meta": format!("Showing 100 of {} total entities. Use omni_search or get_subgraph for targeted entity discovery.", data.len()),
"entities": truncated
});
Ok(serde_json::to_string_pretty(&res)?)
} else {
Ok(serde_json::to_string_pretty(&data)?)
}
})
.await
.unwrap()
@@ -223,6 +232,76 @@ impl McpPrompt for SummarizeArchitecturePrompt {
}
}
struct SessionDeltaResource;
#[async_trait]
impl McpResource for SessionDeltaResource {
fn uri(&self) -> &'static str {
"memory://session/delta"
}
fn name(&self) -> &'static str {
"Session Delta"
}
fn description(&self) -> Option<&'static str> {
Some("Recent changes, active tasks, code edits, and notes created in the current session")
}
async fn read(&self, state: Arc<MemoryState>) -> crate::error::Result<String> {
let state_clone = Arc::clone(&state);
tokio::task::spawn_blocking(move || -> crate::error::Result<String> {
let now = crate::handlers::utils::now_secs();
let lookback = 7200; // last 2 hours
let cutoff = now.saturating_sub(lookback);
let changes = state_clone.code.ledger.cache.read().unwrap();
let recent_changes: Vec<_> = changes.iter().filter(|c| c.timestamp >= cutoff).collect();
let tasks = state_clone.project.tasks.cache.read().unwrap();
let active_tasks: Vec<_> = tasks.iter().filter(|t| t.status != "completed" && t.status != "done").collect();
let notes = state_clone.code.sticky.cache.read().unwrap();
let active_notes: Vec<_> = notes.iter().filter(|n| n.expires_at.map_or(true, |exp| exp > now)).collect();
let delta = serde_json::json!({
"recent_changes": recent_changes,
"active_tasks": active_tasks,
"active_sticky_notes": active_notes,
"timestamp": now,
});
Ok(serde_json::to_string_pretty(&delta)?)
})
.await
.unwrap()
}
}
pub struct ContextWarmupPrompt;
#[async_trait]
impl McpPrompt for ContextWarmupPrompt {
fn name(&self) -> &'static str {
"context_warmup"
}
fn description(&self) -> Option<&'static str> {
Some("Consolidated context warmup prompt providing active tasks, pinned files, open tech debt, and sticky notes")
}
async fn get(
&self,
_args: Value,
_state: Arc<MemoryState>,
) -> crate::error::Result<serde_json::Value> {
Ok(serde_json::json!({
"messages": [
{
"role": "user",
"content": {
"type": "text",
"text": "Perform a complete context warmup. Please read memory://session/delta, memory://tasks/active, memory://tech_debt/active, and memory://pinned_files to synthesize current project focus and active workspace constraints."
}
}
]
}))
}
}
pub struct MemoryHandler {
pub state: Arc<MemoryState>,
pub tools: std::collections::HashMap<String, Box<dyn McpTool>>,
@@ -258,9 +337,11 @@ impl MemoryHandler {
register_resource!(TasksActiveResource);
register_resource!(DecisionsActiveResource);
register_resource!(TechDebtActiveResource);
register_resource!(SessionDeltaResource);
register_prompt!(AnalyzeTechDebtPrompt);
register_prompt!(SummarizeArchitecturePrompt);
register_prompt!(ContextWarmupPrompt);
struct TerminalHistoryResource;
#[async_trait]
impl McpResource for TerminalHistoryResource {
@@ -422,6 +503,7 @@ impl MemoryHandler {
register!(tasks::UpdateMilestoneHandler);
register!(tasks::ListMilestonesHandler);
register!(notes::ManageStickyNotesHandler);
register!(notes::AddStickyNoteHandler);
register!(notes::ReadStickyNotesHandler);
register!(notes::DeleteStickyNoteHandler);
@@ -431,6 +513,7 @@ impl MemoryHandler {
register!(notes::ClearHandoffMemosHandler);
register!(notes::AddSessionSummaryHandler);
register!(notes::GenerateStandupReportHandler);
register!(notes::PromoteToEntityHandler);
register!(meta::LogDecisionHandler);
register!(meta::QueryDecisionsHandler);
@@ -469,17 +552,42 @@ impl MemoryHandler {
register!(vision::ReadClipboardHandler);
register!(vision::WriteClipboardHandler);
register!(git::GetActiveWorktreeContextHandler);
register!(git::QueryGitDiffsHandler);
register!(logs::WatchProcessLogsHandler);
register!(logs::GetRecentLogsHandler);
register!(ast::ReadFileSkeletonHandler);
register!(vision::ToggleClipboardWatchModeHandler);
register!(ast::ReplaceAstNodeHandler);
register!(ast::FindSymbolReferencesHandler);
register!(ast::GetCallersHandler);
register!(ast::AnalyzeImpactHandler);
register!(workspaces::ReadDirectoryArchitectureHandler);
register!(workspaces::SemanticCodeSearchHandler);
register!(workspaces::CreateSnapshotHandler);
register!(workspaces::RestoreSnapshotHandler);
register!(workspaces::CreateSubagentNamespaceHandler);
register!(workspaces::ManageSubagentNamespaceHandler);
register!(graph::GetSubgraphHandler);
register!(meta::SuggestErrorFixHandler);
register!(meta::CheckpointStateHandler);
register!(meta::ManageCheckpointHandler);
register!(meta::RestoreStateHandler);
register!(workspaces::TagSnippetHandler);
register!(workspaces::PurgeSubagentNamespaceHandler);
register!(workspaces::CondenseSubagentNamespaceHandler);
register!(graph::SweepGraphHealthHandler);
register!(meta::QueryLineageHandler);
register!(meta::GetNextActionableTasksHandler);
register!(meta::LogHypothesisHandler);
register!(meta::QueryHypothesesHandler);
register!(workspaces::DiffContextWorkspacesHandler);
register!(meta::GetPreflightContextHandler);
register!(graph::ResolveStaleSymbolsHandler);
register!(graph::SummarizeSubgraphHandler);
register!(meta::BroadcastAgentSignalHandler);
register!(meta::QueryAgentSignalsHandler);
register!(meta::AutoSessionCheckpointHandler);
register!(meta::SearchSnippetsHybridHandler);
Self {
state,
@@ -646,8 +754,25 @@ impl MemoryHandler {
.cloned()
.unwrap_or(serde_json::Value::Object(Default::default()));
self.state
.broadcast_activity(&format!("Agent executed tool: {}", name));
let category = match name {
"read_clipboard" | "write_clipboard" | "toggle_clipboard_watch_mode" => "CLIPBOARD",
"create_entities" | "create_relations" | "read_graph" | "get_subgraph" | "search_graph" | "get_schema" => "GRAPH",
"log_decision" => "DECISION",
"log_code_change" => "CODE",
"log_error_fix" => "ERROR_FIX",
"log_tech_debt" => "TECH_DEBT",
"add_task" | "update_task_status" | "delete_task" | "add_milestone" => "TASK",
"manage_sticky_notes" | "read_notes" => "STICKY_NOTE",
"manage_checkpoint" => "CHECKPOINT",
"manage_subagent_namespace" => "SUBAGENT",
"search_snippets" => "SNIPPET",
"search_web" => "WEB_SEARCH",
"omni_search" => "OMNI_SEARCH",
_ => "TOOL",
};
let activity_msg = format_tool_activity_description(name, &args);
self.state.broadcast_activity(category, &activity_msg);
let result: crate::error::Result<String> = if let Some(tool) = self.tools.get(name)
{
@@ -694,12 +819,174 @@ impl MemoryHandler {
}
}
pub fn format_tool_activity_description(name: &str, args: &serde_json::Value) -> String {
let detail = match name {
"log_code_change" => {
if let Some(file) = args.get("file").or_else(|| args.get("path")).and_then(|v| v.as_str()) {
if let Some(summary) = args.get("summary").and_then(|v| v.as_str()) {
format!("{}: {}", file, summary)
} else {
file.to_string()
}
} else {
args.get("summary").and_then(|v| v.as_str()).unwrap_or("").to_string()
}
}
"log_decision" => {
args.get("title")
.or_else(|| args.get("decision"))
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string()
}
"log_error_fix" => {
args.get("error")
.or_else(|| args.get("summary"))
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string()
}
"log_tech_debt" => {
if let Some(summary) = args.get("summary").and_then(|v| v.as_str()) {
summary.to_string()
} else if let Some(file) = args.get("file").and_then(|v| v.as_str()) {
file.to_string()
} else {
"".to_string()
}
}
"create_entities" => {
if let Some(entities) = args.get("entities").and_then(|v| v.as_array()) {
let names: Vec<&str> = entities
.iter()
.filter_map(|e| e.get("name").and_then(|n| n.as_str()))
.take(3)
.collect();
if !names.is_empty() {
let mut s = names.join(", ");
if entities.len() > 3 {
s.push_str(&format!(" (+{} more)", entities.len() - 3));
}
s
} else {
format!("{} entity/entities", entities.len())
}
} else if let Some(name) = args.get("name").and_then(|v| v.as_str()) {
name.to_string()
} else {
"".to_string()
}
}
"create_relations" => {
if let Some(relations) = args.get("relations").and_then(|v| v.as_array()) {
if let Some(first) = relations.first() {
let from = first.get("from").and_then(|v| v.as_str()).unwrap_or("?");
let to = first.get("to").and_then(|v| v.as_str()).unwrap_or("?");
let pred = first.get("predicate").and_then(|v| v.as_str()).unwrap_or("->");
if relations.len() > 1 {
format!("{} {} {} (+{} more)", from, pred, to, relations.len() - 1)
} else {
format!("{} {} {}", from, pred, to)
}
} else {
format!("{} relation(s)", relations.len())
}
} else {
"".to_string()
}
}
"add_task" => {
args.get("title")
.or_else(|| args.get("name"))
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string()
}
"update_task_status" => {
if let (Some(id), Some(status)) = (
args.get("task_id").or_else(|| args.get("id")).and_then(|v| v.as_str()),
args.get("status").and_then(|v| v.as_str()),
) {
format!("Task {} -> {}", id, status)
} else {
"".to_string()
}
}
"omni_search" | "search_graph" | "search_snippets" | "search_web" => {
args.get("query")
.and_then(|v| v.as_str())
.map(|q| format!("\"{}\"", q))
.unwrap_or_default()
}
"manage_sticky_notes" => {
let action = args.get("action").and_then(|v| v.as_str()).unwrap_or("");
if let Some(content) = args.get("content").and_then(|v| v.as_str()) {
let preview: String = content.chars().take(40).collect();
if !action.is_empty() {
format!("{} \"{}\"", action, preview)
} else {
format!("\"{}\"", preview)
}
} else {
action.to_string()
}
}
"write_clipboard" => {
if let Some(text) = args.get("text").or_else(|| args.get("content")).and_then(|v| v.as_str()) {
let preview: String = text.chars().take(40).collect();
format!("\"{}\"", preview)
} else {
"".to_string()
}
}
_ => {
if let Some(title) = args
.get("title")
.or_else(|| args.get("summary"))
.or_else(|| args.get("name"))
.or_else(|| args.get("query"))
.or_else(|| args.get("file"))
.or_else(|| args.get("path"))
.and_then(|v| v.as_str())
{
title.to_string()
} else {
"".to_string()
}
}
};
let mut truncated = detail.trim().to_string();
if truncated.len() > 80 {
truncated.truncate(77);
truncated.push_str("...");
}
if !truncated.is_empty() {
format!("Agent executed tool: {} ({})", name, truncated)
} else {
format!("Agent executed tool: {}", name)
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use tempfile::tempdir;
#[test]
fn test_format_tool_activity_description() {
let msg1 = format_tool_activity_description("log_code_change", &json!({ "file": "src/lib.rs", "summary": "Refactored library" }));
assert_eq!(msg1, "Agent executed tool: log_code_change (src/lib.rs: Refactored library)");
let msg2 = format_tool_activity_description("create_entities", &json!({ "entities": [{ "name": "EntityA" }, { "name": "EntityB" }] }));
assert_eq!(msg2, "Agent executed tool: create_entities (EntityA, EntityB)");
let msg3 = format_tool_activity_description("read_graph", &json!({}));
assert_eq!(msg3, "Agent executed tool: read_graph");
}
#[tokio::test]
async fn test_memory_handler_tools_registration() {
let dir = tempdir().unwrap();
@@ -861,4 +1148,40 @@ mod tests {
assert!(res_unknown.get("error").is_some());
assert_eq!(res_unknown["error"]["code"], -32601);
}
#[tokio::test]
async fn test_handle_request() {
let dir = tempfile::tempdir().unwrap();
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
let handler = MemoryHandler::new(state);
let req1 = serde_json::json!({
"jsonrpc": "2.0",
"id": 5,
"method": "tools/call",
"params": {
"name": "create_entities",
"arguments": {
"entities": [{ "name": "EntityA" }, { "name": "EntityB" }]
}
}
});
let res1 = handler.handle_request(req1).await.unwrap();
assert_eq!(res1["jsonrpc"], "2.0");
assert_eq!(res1["id"], 5);
let req2 = serde_json::json!({
"jsonrpc": "2.0",
"id": 6,
"method": "tools/call",
"params": {
"name": "read_graph",
"arguments": {}
}
});
let res2 = handler.handle_request(req2).await.unwrap();
assert_eq!(res2["jsonrpc"], "2.0");
assert_eq!(res2["id"], 6);
}
}