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
@@ -7,6 +7,90 @@ use serde_json::Value;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct ManageStickyNotesHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for ManageStickyNotesHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"manage_sticky_notes"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<ManageStickyNotesTool>(
|
||||
"manage_sticky_notes",
|
||||
"Manage ephemeral sticky notes with TTL (add, read, delete, clear)",
|
||||
)
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||
let req: ManageStickyNotesTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
match req.action {
|
||||
StickyNoteAction::Add => {
|
||||
let content = req.content.ok_or_else(|| {
|
||||
crate::error::AppError::Internal("content is required for 'add' action".to_string())
|
||||
})?;
|
||||
let now = crate::handlers::utils::now_secs();
|
||||
let expires_at = if let Some(ttl) = req.ttl_seconds {
|
||||
Some(now + ttl)
|
||||
} else if req.session_only.unwrap_or(false) {
|
||||
Some(now + 14400)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
state.code.sticky.modify(|notes| {
|
||||
notes.push(StickyNote {
|
||||
timestamp: now,
|
||||
content,
|
||||
expires_at,
|
||||
});
|
||||
});
|
||||
Ok("Sticky note added.".to_string())
|
||||
}
|
||||
StickyNoteAction::Read => {
|
||||
let now = crate::handlers::utils::now_secs();
|
||||
let mut active_notes = Vec::new();
|
||||
state.code.sticky.modify(|notes| {
|
||||
notes.retain(|n| {
|
||||
if let Some(exp) = n.expires_at {
|
||||
exp > now
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
active_notes = notes.clone();
|
||||
});
|
||||
Ok(serde_json::to_string(&active_notes)?)
|
||||
}
|
||||
StickyNoteAction::Delete => {
|
||||
let idx = req.index.ok_or_else(|| {
|
||||
crate::error::AppError::Internal("index is required for 'delete' action".to_string())
|
||||
})?;
|
||||
let mut success = false;
|
||||
state.code.sticky.modify(|notes| {
|
||||
if idx > 0 && idx <= notes.len() {
|
||||
notes.remove(idx - 1);
|
||||
success = true;
|
||||
}
|
||||
});
|
||||
if success {
|
||||
Ok("Sticky note deleted.".to_string())
|
||||
} else {
|
||||
Err(crate::error::AppError::Internal(
|
||||
"Invalid sticky note index.".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
StickyNoteAction::Clear => {
|
||||
state.code.sticky.modify(|notes| {
|
||||
notes.clear();
|
||||
});
|
||||
Ok("All sticky notes cleared.".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AddStickyNoteHandler;
|
||||
|
||||
#[async_trait]
|
||||
@@ -21,11 +105,20 @@ impl McpTool for AddStickyNoteHandler {
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||
let req: AddStickyNoteTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let now = crate::handlers::utils::now_secs();
|
||||
let expires_at = if let Some(ttl) = req.ttl_seconds {
|
||||
Some(now + ttl)
|
||||
} else if req.session_only.unwrap_or(false) {
|
||||
Some(now + 14400) // Default 4-hour session TTL
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
state.code.sticky.modify(|notes| {
|
||||
notes.push(StickyNote {
|
||||
timestamp: crate::handlers::utils::now_secs(),
|
||||
timestamp: now,
|
||||
content: req.content,
|
||||
expires_at: None,
|
||||
expires_at,
|
||||
});
|
||||
});
|
||||
Ok("Sticky note added.".to_string())
|
||||
@@ -48,11 +141,21 @@ impl McpTool for ReadStickyNotesHandler {
|
||||
}
|
||||
|
||||
async fn execute(&self, _args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||
let data = state
|
||||
.code
|
||||
.sticky
|
||||
.read_with(|s| Ok::<String, crate::error::AppError>(serde_json::to_string(s)?))?;
|
||||
Ok(data)
|
||||
let now = crate::handlers::utils::now_secs();
|
||||
let mut active_notes = Vec::new();
|
||||
|
||||
state.code.sticky.modify(|notes| {
|
||||
notes.retain(|n| {
|
||||
if let Some(exp) = n.expires_at {
|
||||
exp > now
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
active_notes = notes.clone();
|
||||
});
|
||||
|
||||
Ok(serde_json::to_string(&active_notes)?)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,7 +241,10 @@ impl McpTool for LeaveHandoffMemoHandler {
|
||||
namespace: req.namespace,
|
||||
timestamp: crate::handlers::utils::now_secs(),
|
||||
expires_at: None,
|
||||
})
|
||||
});
|
||||
if memos.len() > 200 {
|
||||
memos.remove(0);
|
||||
}
|
||||
});
|
||||
Ok("Handoff memo left".to_string())
|
||||
}
|
||||
@@ -227,7 +333,10 @@ impl McpTool for AddSessionSummaryHandler {
|
||||
namespace: req.namespace,
|
||||
timestamp: crate::handlers::utils::now_secs(),
|
||||
expires_at: None,
|
||||
})
|
||||
});
|
||||
if summaries.len() > 200 {
|
||||
summaries.remove(0);
|
||||
}
|
||||
});
|
||||
Ok("Session summary added".to_string())
|
||||
}
|
||||
@@ -268,6 +377,57 @@ impl McpTool for GenerateStandupReportHandler {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PromoteToEntityHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for PromoteToEntityHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"promote_to_entity"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<PromoteToEntityTool>(
|
||||
"promote_to_entity",
|
||||
"Promote a transient sticky note or active task observation into a permanent Knowledge Graph entity.",
|
||||
)
|
||||
}
|
||||
|
||||
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 entity_name = req.entity_name.clone();
|
||||
let obs = req.content.clone();
|
||||
let entity_type = req.entity_type.clone();
|
||||
let namespace = req.namespace.clone();
|
||||
|
||||
state.modify_graph(|g| {
|
||||
let entity = g
|
||||
.entities
|
||||
.entry(entity_name.clone())
|
||||
.or_insert_with(|| Entity {
|
||||
name: entity_name.clone(),
|
||||
entity_type,
|
||||
observations: Vec::new(),
|
||||
namespace,
|
||||
git_branch: None,
|
||||
});
|
||||
if !entity.observations.contains(&obs) {
|
||||
entity.observations.push(obs.clone());
|
||||
}
|
||||
});
|
||||
|
||||
state.code.sticky.modify(|notes| {
|
||||
notes.retain(|n| !n.content.contains(&obs));
|
||||
});
|
||||
|
||||
Ok(format!(
|
||||
"Promoted transient note into permanent entity '{}'.",
|
||||
req.entity_name
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -366,4 +526,22 @@ mod tests {
|
||||
.unwrap();
|
||||
assert!(!res4.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_invalid_sticky_note_action() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
|
||||
|
||||
let handler = ManageStickyNotesHandler;
|
||||
let invalid_args = serde_json::json!({
|
||||
"action": "invalid_action"
|
||||
});
|
||||
|
||||
let res = handler
|
||||
.execute(invalid_args, state.clone())
|
||||
.await;
|
||||
assert!(res.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user