refactor: consolidate nvim crates, extract server library, and update workspace dependencies
This commit is contained in:
1 parent
a083719cf1
commit
533adfd41b
53 files changed
+5967
-1230
No files matched your search
+326
-3
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user