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
@@ -123,6 +123,7 @@ impl McpTool for StoreSnippetHandler {
|
||||
code: req.code,
|
||||
description: req.description,
|
||||
updated_at: crate::handlers::utils::now_secs(),
|
||||
tags: req.tags.unwrap_or_default(),
|
||||
embedding,
|
||||
};
|
||||
|
||||
@@ -138,6 +139,43 @@ impl McpTool for StoreSnippetHandler {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TagSnippetHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for TagSnippetHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"tag_snippet"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<TagSnippetTool>("tag_snippet", "Execute tag_snippet")
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||
let req: TagSnippetTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let mut found = false;
|
||||
state.code.snippets.modify(|snippets| {
|
||||
for s in snippets.iter_mut() {
|
||||
if s.name == req.name {
|
||||
for tag in &req.tags {
|
||||
if !s.tags.contains(tag) {
|
||||
s.tags.push(tag.clone());
|
||||
}
|
||||
}
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if found {
|
||||
Ok(format!("Snippet '{}' tagged with: {:?}", req.name, req.tags))
|
||||
} else {
|
||||
Err(crate::error::AppError::Internal(format!("Snippet '{}' not found.", req.name)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SearchSnippetsHandler;
|
||||
|
||||
#[async_trait]
|
||||
@@ -147,22 +185,49 @@ impl McpTool for SearchSnippetsHandler {
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<SearchSnippetsTool>("search_snippets", "Execute search_snippets")
|
||||
crate::mcp::tool_def::<SearchSnippetsTool>(
|
||||
"search_snippets",
|
||||
"Search stored code snippets using Hybrid BM25 + Vector ranking or text search",
|
||||
)
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||
let req: SearchSnippetsTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let query = req.query;
|
||||
let limit = req.limit.unwrap_or(10);
|
||||
let include_body = req.include_body.unwrap_or(false);
|
||||
let tags_filter = req.tags.unwrap_or_default();
|
||||
|
||||
let data = state.code.snippets.read_with(|snippets| {
|
||||
let results: Vec<_> = snippets
|
||||
.iter()
|
||||
.filter(|s| {
|
||||
contains_ignore_ascii_case(&s.name, &query)
|
||||
let matches_text = query.is_empty()
|
||||
|| contains_ignore_ascii_case(&s.name, &query)
|
||||
|| contains_ignore_ascii_case(&s.description, &query)
|
||||
|| contains_ignore_ascii_case(&s.language, &query)
|
||||
|| 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)));
|
||||
|
||||
matches_text && matches_tags
|
||||
})
|
||||
.take(limit)
|
||||
.map(|s| {
|
||||
if include_body {
|
||||
serde_json::json!(s)
|
||||
} else {
|
||||
serde_json::json!({
|
||||
"name": s.name,
|
||||
"description": s.description,
|
||||
"language": s.language,
|
||||
"tags": s.tags
|
||||
})
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
Ok::<String, crate::error::AppError>(serde_json::to_string(&results)?)
|
||||
Ok::<String, crate::error::AppError>(serde_json::to_string_pretty(&results)?)
|
||||
})?;
|
||||
Ok(data)
|
||||
}
|
||||
@@ -523,7 +588,60 @@ mod tests {
|
||||
.unwrap();
|
||||
assert_eq!(res8, "PR checklist cleared");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_workspace_and_subagent_handlers() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let state = Arc::new(MemoryState::new(temp_dir.path().to_str().unwrap()));
|
||||
|
||||
// Snapshots
|
||||
let create_snap = CreateSnapshotHandler;
|
||||
let snap_res = create_snap.execute(json!({"description": "Test snap", "namespace": "global"}), state.clone()).await.unwrap();
|
||||
assert!(snap_res.contains("SNAP-"));
|
||||
let snap_id = snap_res.split_whitespace().nth(1).unwrap();
|
||||
|
||||
let restore_snap = RestoreSnapshotHandler;
|
||||
let restore_res = restore_snap.execute(json!({"snapshot_id": snap_id}), state.clone()).await.unwrap();
|
||||
assert!(restore_res.contains("Successfully restored"));
|
||||
|
||||
// Subagent Namespace
|
||||
let create_sub_ns = CreateSubagentNamespaceHandler;
|
||||
let sub_ns_res = create_sub_ns.execute(json!({"subagent_id": "test_agent"}), state.clone()).await.unwrap();
|
||||
assert!(sub_ns_res.contains("subagent-test_agent"));
|
||||
|
||||
let manage_sub_ns = ManageSubagentNamespaceHandler;
|
||||
let manage_res = manage_sub_ns.execute(json!({
|
||||
"subagent_id": "test_agent",
|
||||
"action": "create"
|
||||
}), state.clone()).await.unwrap();
|
||||
assert!(manage_res.contains("initialized"));
|
||||
|
||||
// Context Workspace Diff
|
||||
let save_ws = SaveContextWorkspaceHandler;
|
||||
save_ws.execute(json!({"name": "ws_a", "description": "Workspace A", "pinned_files": ["src/lib.rs"], "active_task_ids": []}), state.clone()).await.unwrap();
|
||||
save_ws.execute(json!({"name": "ws_b", "description": "Workspace B", "pinned_files": ["src/main.rs"], "active_task_ids": []}), state.clone()).await.unwrap();
|
||||
|
||||
|
||||
let diff_ws = DiffContextWorkspacesHandler;
|
||||
let diff_res = diff_ws.execute(json!({"workspace_a": "ws_a", "workspace_b": "ws_b"}), state.clone()).await.unwrap();
|
||||
assert!(diff_res.contains("workspace_a"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_diff_context_workspaces_handler() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
|
||||
|
||||
let save_ws = SaveContextWorkspaceHandler;
|
||||
save_ws.execute(json!({"name": "ws_1", "description": "Workspace 1", "pinned_files": ["src/lib.rs"], "active_task_ids": []}), state.clone()).await.unwrap();
|
||||
save_ws.execute(json!({"name": "ws_2", "description": "Workspace 2", "pinned_files": ["src/main.rs"], "active_task_ids": []}), state.clone()).await.unwrap();
|
||||
|
||||
let diff_ws = DiffContextWorkspacesHandler;
|
||||
let diff_res = diff_ws.execute(json!({"workspace_a": "ws_1", "workspace_b": "ws_2"}), state.clone()).await.unwrap();
|
||||
assert!(diff_res.contains("ws_1"));
|
||||
}
|
||||
}
|
||||
|
||||
use crate::tools::ReadDirectoryArchitectureTool;
|
||||
use std::fs;
|
||||
|
||||
@@ -726,6 +844,95 @@ impl McpTool for RestoreSnapshotHandler {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ManageSubagentNamespaceHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for ManageSubagentNamespaceHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"manage_subagent_namespace"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<ManageSubagentNamespaceTool>(
|
||||
"manage_subagent_namespace",
|
||||
"Manage isolated memory namespaces for subagent sessions (create, condense/promote, or purge)",
|
||||
)
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||
let req: ManageSubagentNamespaceTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let subagent_ns = format!("subagent-{}", req.subagent_id);
|
||||
|
||||
match req.action {
|
||||
SubagentNamespaceAction::Create => {
|
||||
state.record_activity("namespace", &format!("Created subagent namespace {}", subagent_ns), None);
|
||||
Ok(format!("Subagent memory namespace '{}' initialized.", subagent_ns))
|
||||
}
|
||||
SubagentNamespaceAction::Purge => {
|
||||
state.project.pinned_files.modify(|files| {
|
||||
files.retain(|f| f.namespace != subagent_ns);
|
||||
});
|
||||
state.record_activity("namespace", &format!("Purged subagent namespace {}", subagent_ns), None);
|
||||
Ok(format!("Subagent memory namespace '{}' purged.", subagent_ns))
|
||||
}
|
||||
SubagentNamespaceAction::Condense => {
|
||||
let purge = req.purge_after_promotion.unwrap_or(true);
|
||||
let mut promoted_entities = 0;
|
||||
let mut promoted_relations = 0;
|
||||
|
||||
state.modify_graph(|g| {
|
||||
let sub_entities: Vec<Entity> = g
|
||||
.entities
|
||||
.values()
|
||||
.filter(|e| e.namespace == subagent_ns)
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
for mut e in sub_entities {
|
||||
promoted_entities += 1;
|
||||
e.namespace = "global".to_string();
|
||||
let global_entry = g.entities.entry(e.name.clone()).or_insert_with(|| Entity {
|
||||
name: e.name.clone(),
|
||||
entity_type: e.entity_type.clone(),
|
||||
observations: Vec::new(),
|
||||
namespace: "global".to_string(),
|
||||
git_branch: e.git_branch.clone(),
|
||||
});
|
||||
for obs in e.observations {
|
||||
if !global_entry.observations.contains(&obs) {
|
||||
global_entry.observations.push(obs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for r in &mut g.relations {
|
||||
if r.namespace == subagent_ns {
|
||||
promoted_relations += 1;
|
||||
r.namespace = "global".to_string();
|
||||
}
|
||||
}
|
||||
|
||||
if purge {
|
||||
g.entities.retain(|_, e| e.namespace != subagent_ns);
|
||||
g.relations.retain(|r| r.namespace != subagent_ns);
|
||||
}
|
||||
});
|
||||
|
||||
if purge {
|
||||
state.project.pinned_files.modify(|files| {
|
||||
files.retain(|f| f.namespace != subagent_ns);
|
||||
});
|
||||
}
|
||||
|
||||
Ok(format!(
|
||||
"Subagent namespace '{}' condensed: promoted {} entities and {} relations to global graph.",
|
||||
subagent_ns, promoted_entities, promoted_relations
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CreateSubagentNamespaceHandler;
|
||||
|
||||
#[async_trait]
|
||||
@@ -770,3 +977,141 @@ impl McpTool for PurgeSubagentNamespaceHandler {
|
||||
Ok(format!("Subagent memory namespace '{}' purged.", ns))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CondenseSubagentNamespaceHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for CondenseSubagentNamespaceHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"condense_subagent_namespace"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<CondenseSubagentNamespaceTool>(
|
||||
"condense_subagent_namespace",
|
||||
"Auto-condense a subagent's memory namespace and promote its entities/relations to the global Knowledge Graph.",
|
||||
)
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||
let req: CondenseSubagentNamespaceTool =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let subagent_ns = format!("subagent-{}", req.subagent_id);
|
||||
let purge = req.purge_after_promotion.unwrap_or(true);
|
||||
|
||||
let mut promoted_entities = 0;
|
||||
let mut promoted_relations = 0;
|
||||
|
||||
state.modify_graph(|g| {
|
||||
let sub_entities: Vec<Entity> = g
|
||||
.entities
|
||||
.values()
|
||||
.filter(|e| e.namespace == subagent_ns)
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
for mut e in sub_entities {
|
||||
promoted_entities += 1;
|
||||
e.namespace = "global".to_string();
|
||||
let global_entry = g.entities.entry(e.name.clone()).or_insert_with(|| Entity {
|
||||
name: e.name.clone(),
|
||||
entity_type: e.entity_type.clone(),
|
||||
observations: Vec::new(),
|
||||
namespace: "global".to_string(),
|
||||
git_branch: e.git_branch.clone(),
|
||||
});
|
||||
for obs in e.observations {
|
||||
if !global_entry.observations.contains(&obs) {
|
||||
global_entry.observations.push(obs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for r in &mut g.relations {
|
||||
if r.namespace == subagent_ns {
|
||||
r.namespace = "global".to_string();
|
||||
promoted_relations += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if purge {
|
||||
g.entities.retain(|_, e| e.namespace != subagent_ns);
|
||||
g.relations.retain(|r| r.namespace != subagent_ns);
|
||||
}
|
||||
});
|
||||
|
||||
if purge {
|
||||
state.project.pinned_files.modify(|files| {
|
||||
files.retain(|f| f.namespace != subagent_ns);
|
||||
});
|
||||
}
|
||||
|
||||
Ok(format!(
|
||||
"Consolidated subagent namespace '{}': Promoted {} entities, {} relations to global. Purged: {}",
|
||||
subagent_ns, promoted_entities, promoted_relations, purge
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DiffContextWorkspacesHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for DiffContextWorkspacesHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"diff_context_workspaces"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<DiffContextWorkspacesTool>("diff_context_workspaces", "Execute diff_context_workspaces")
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||
let req: DiffContextWorkspacesTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
|
||||
let workspaces = state.project.context_workspaces.read_with(|ws| ws.clone());
|
||||
let ws_a = workspaces.iter().find(|w| w.name == req.workspace_a);
|
||||
let ws_b = workspaces.iter().find(|w| w.name == req.workspace_b);
|
||||
|
||||
if ws_a.is_none() || ws_b.is_none() {
|
||||
return Err(crate::error::AppError::Internal(format!(
|
||||
"One or both context workspaces not found: '{}' found={}, '{}' found={}",
|
||||
req.workspace_a, ws_a.is_some(), req.workspace_b, ws_b.is_some()
|
||||
)));
|
||||
}
|
||||
|
||||
let a = ws_a.unwrap();
|
||||
let b = ws_b.unwrap();
|
||||
|
||||
let set_files_a: std::collections::HashSet<_> = a.pinned_files.iter().cloned().collect();
|
||||
let set_files_b: std::collections::HashSet<_> = b.pinned_files.iter().cloned().collect();
|
||||
|
||||
let added_files: Vec<_> = set_files_b.difference(&set_files_a).cloned().collect();
|
||||
let removed_files: Vec<_> = set_files_a.difference(&set_files_b).cloned().collect();
|
||||
let shared_files: Vec<_> = set_files_a.intersection(&set_files_b).cloned().collect();
|
||||
|
||||
let set_tasks_a: std::collections::HashSet<_> = a.active_task_ids.iter().cloned().collect();
|
||||
let set_tasks_b: std::collections::HashSet<_> = b.active_task_ids.iter().cloned().collect();
|
||||
|
||||
let added_tasks: Vec<_> = set_tasks_b.difference(&set_tasks_a).cloned().collect();
|
||||
let removed_tasks: Vec<_> = set_tasks_a.difference(&set_tasks_b).cloned().collect();
|
||||
let shared_tasks: Vec<_> = set_tasks_a.intersection(&set_tasks_b).cloned().collect();
|
||||
|
||||
let diff = serde_json::json!({
|
||||
"workspace_a": a.name,
|
||||
"workspace_b": b.name,
|
||||
"pinned_files": {
|
||||
"added_in_b": added_files,
|
||||
"removed_in_b": removed_files,
|
||||
"shared": shared_files
|
||||
},
|
||||
"active_tasks": {
|
||||
"added_in_b": added_tasks,
|
||||
"removed_in_b": removed_tasks,
|
||||
"shared": shared_tasks
|
||||
}
|
||||
});
|
||||
|
||||
Ok(serde_json::to_string_pretty(&diff)?)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user