Compare commits

..
4 Commits
29 changed files with 885 additions and 991 deletions

No files matched your search

Generated
+7
View File
@@ -4010,6 +4010,12 @@ dependencies = [
"digest 0.11.3", "digest 0.11.3",
] ]
[[package]]
name = "sha1_smol"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d"
[[package]] [[package]]
name = "sharded-slab" name = "sharded-slab"
version = "0.1.7" version = "0.1.7"
@@ -5053,6 +5059,7 @@ dependencies = [
"getrandom 0.4.3", "getrandom 0.4.3",
"js-sys", "js-sys",
"serde_core", "serde_core",
"sha1_smol",
"wasm-bindgen", "wasm-bindgen",
] ]
-43
View File
@@ -1,43 +0,0 @@
# Effective Discourse: LLM Prompting Guide for MCP Memory
To get the most out of the Antigravity MCP Memory server and its advanced developer tools, use specific phrases that clearly state your intent. This guides the LLM to use the most efficient tools, reducing token consumption, speeding up time-to-resolve (T2R), and avoiding brute-force file reading.
## 1. Codebase Exploration & Token Efficiency
When entering a new file, avoid having the LLM read the entire contents blindly.
* **Don't say:** "Read server.rs and tell me what it does." *(Consumes massive tokens)*
* **Do say:** "Extract the AST skeleton of server.rs to understand its structure first."
* **Tool Triggered:** ␍ead_file_skeleton
## 2. Debugging & Log Parsing
Stop copy-pasting giant walls of logs into the chat interface.
* **Don't say:** "Here is the error: [paste 500 lines of logs]"
* **Do say:** "The daemon crashed. Fetch the recent logs from daemon.log." or "Watch the process logs for server.log."
* **Tool Triggered:** get_recent_logs, watch_process_logs
## 3. Git & Context Handoff
When you've been working independently and need to loop the LLM back in on your current state.
* **Don't say:** "I changed some files, here are the diffs..."
* **Do say:** "Get the active git worktree context to review my uncommitted changes before we continue."
* **Tool Triggered:** get_active_worktree_context
## 4. Clipboard Watch Mode (Research & Triage)
When you are doing intense debugging across StackOverflow, logs, and docs, use the clipboard watcher to auto-ingest your breadcrumbs.
* **Action:** Ask the LLM to turn it on: "Enable clipboard watch mode."
* **Do say:** "I'm going to reproduce the bug and copy some stack traces and IDs. Give me a minute, then read my latest sticky notes to catch up."
* **Tool Triggered:** oggle_clipboard_watch_mode, followed by internal Sticky Note reads.
## 5. Neovim Ghost Text (Live Previews)
Keep your workflow entirely within your editor rather than copy-pasting code blocks from the chat.
* **Don't say:** "Write the updated function here so I can copy-paste it."
* **Do say:** "Push this refactor to my active Neovim buffer as ghost text so I can review it in-line."
* **Tool Triggered:**
vim_set_preview
## 6. Graph & Memory Management
Actively instruct the LLM to maintain its own memory constraints and organize data.
* **Do say:** "Log this architectural decision in the knowledge graph."
* **Do say:** "Add a sticky note with the test database credentials for this session."
* **Do say:** "Create a milestone for the 'Rich Clipboard' feature and break it down into active tasks."
* **Tools Triggered:** create_entities, log_decision, dd_sticky_note, dd_milestone, dd_task
By phrasing requests around *actions* rather than *information retrieval*, the LLM is primed to leverage the rich MCP toolset built into the Antigravity Memory Server.
-131
View File
@@ -1,131 +0,0 @@
#![cfg(unix)]
use serde_json::{Value, json};
use std::io::{BufRead, BufReader, Read, Write};
use std::process::{Command, Stdio};
fn send_message(stdin: &mut std::process::ChildStdin, msg: Value) {
let s = serde_json::to_string(&msg).unwrap();
let payload = format!("Content-Length: {}\r\n\r\n{}", s.len(), s);
stdin.write_all(payload.as_bytes()).unwrap();
stdin.flush().unwrap();
}
fn read_message(stdout: &mut std::process::ChildStdout) -> Option<Value> {
let mut reader = BufReader::new(stdout);
let mut length = 0;
// Read headers
loop {
let mut line = String::new();
if reader.read_line(&mut line).unwrap_or(0) == 0 {
return None; // EOF
}
let line = line.trim_end();
if line.is_empty() {
break;
}
if let Some(len_str) = line.strip_prefix("Content-Length: ") {
length = len_str.parse().unwrap_or(0);
}
}
if length == 0 {
return None;
}
// Read body
let mut buf = vec![0u8; length];
reader.read_exact(&mut buf).unwrap();
let body_str = String::from_utf8_lossy(&buf);
Some(serde_json::from_str(&body_str).unwrap())
}
#[test]
#[cfg(unix)]
fn test_mcp_initialization_and_tools_list() {
let mut nvim_exe = std::env::current_exe().unwrap();
nvim_exe.pop();
nvim_exe.pop();
nvim_exe.push(format!(
"mcp-memory-linux-nvim{}",
std::env::consts::EXE_SUFFIX
));
let mut child = Command::new(&nvim_exe)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("Failed to start mcp-memory-linux-nvim");
let mut stdin = child.stdin.take().expect("Failed to open stdin");
let mut stdout = child.stdout.take().expect("Failed to open stdout");
// 0. Test server/discover (probe)
let discover_req = json!({
"jsonrpc": "2.0",
"method": "server/discover",
"params": {},
"id": 0
});
send_message(&mut stdin, discover_req);
let discover_resp = read_message(&mut stdout).expect("Failed to read server/discover response");
assert_eq!(discover_resp["error"]["code"], -32601);
// 1. Test Initialize
let init_req = json!({
"jsonrpc": "2.0",
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {
"name": "test-client",
"version": "1.0"
}
},
"id": 1
});
// Send initialize using JSONL format!
let s = serde_json::to_string(&init_req).unwrap();
stdin.write_all(format!("{}\n", s).as_bytes()).unwrap();
stdin.flush().unwrap();
let init_resp = read_message(&mut stdout).expect("Failed to read initialize response");
assert_eq!(init_resp["jsonrpc"], "2.0");
assert_eq!(init_resp["id"], 1);
// Verify capabilities
let capabilities = &init_resp["result"]["capabilities"];
assert_eq!(capabilities["tools"], serde_json::json!({}));
// 2. Test tools/list
let tools_req = json!({
"jsonrpc": "2.0",
"method": "tools/list",
"params": {},
"id": 2
});
send_message(&mut stdin, tools_req);
let tools_resp = read_message(&mut stdout).expect("Failed to read tools/list response");
assert_eq!(tools_resp["jsonrpc"], "2.0");
assert_eq!(tools_resp["id"], 2);
let tools = tools_resp["result"]["tools"]
.as_array()
.expect("result.tools must be an array");
assert!(!tools.is_empty(), "Server must expose at least one tool");
let has_get_active_buffer = tools.iter().any(|t| t["name"] == "nvim_get_active_buffer");
assert!(has_get_active_buffer, "Missing nvim_get_active_buffer tool");
child.kill().expect("Failed to kill child");
child.wait().expect("Failed to wait on child");
}
+61
View File
@@ -0,0 +1,61 @@
# MCP Tools Review & Enhancement Strategy
## Part 1: Current Arsenal Review
Our current MCP ecosystem is highly advanced, utilizing a **Dual-Transport Leader/Stub Architecture** (Windows Host + WSL Proxy) to completely eliminate cross-OS I/O latency.
### 1. Context & Token Optimization
* ␍ead_file_skeleton: Highly effective. Uses ree-sitter to extract ASTs (Rust, Python, TS). **Score: A+ (Massive token savings)**
* get_active_worktree_context: Native git2 integration. Bypasses shell parsing for clean JSON diffs. **Score: A**
* watch_process_logs / get_recent_logs: Direct file seeking. Prevents LLMs from reading multi-megabyte log files. **Score: A**
### 2. Neovim IDE Integration (
vim-core)
*
vim_set_preview,
vim_goto_line,
vim_set_diagnostics,
vim_execute_lua,
vim_get_active_buffer.
* **Review:** Exceptional human QoL. The agent interacts with the code where the human's eyes actually are. Ghost text and diagnostic extmarks provide an IDE-like experience usually reserved for closed-source tools like Cursor. **Score: S-Tier**
### 3. Clipboard & Workflow
* write_clipboard, ␍ead_clipboard, oggle_clipboard_watch_mode.
* **Review:** Native cross-OS clipboard-win and rboard implementation. Auto-ingesting into StickyNotes bridges the gap between manual human research and the agent's context. **Score: A**
### 4. Graph & Memory Management
* create_entities, dd_sticky_note, save_context_workspace, handoff_routine.
* **Review:** Solid foundation for state persistence across branches and days. **Score: B+** (Could use more automated TTL/decay for outdated context).
---
## Part 2: Proposed Enhancements (Focus: T2R, Token Cost, QoL)
To push the system to the absolute bleeding edge of autonomous coding, I propose the following 5 new tools/enhancements.
### 1. ␍eplace_ast_node (Robust Structural Editing)
* **The Problem:** The current ␍eplace_file_content uses exact string matching and line numbers. Line numbers change when humans edit simultaneously, and string matching fails on whitespace/indentation.
* **The Solution:** An MCP tool that takes (file_path, node_type, node_name, new_content). It uses ree-sitter to find the exact boundary of n execute(...) and replaces just that AST node.
* **Impact:** Zero LLM syntax/indentation errors. 100% robust edits. Drastically lowers Time-to-Resolve (T2R) by eliminating failed edit loops.
### 2. semantic_code_search (Local Vector Embeddings)
* **The Problem:** grep_search relies on exact regex. If the LLM guesses the wrong variable name, it wastes tokens searching and reading the wrong files.
* **The Solution:** We already have antivy and astembed in our Cargo.toml. We can index the AST blocks of the codebase in the background. The LLM can query *"Where is the auth token validated?"* and get the exact 3 relevant functions instantly.
* **Impact:** Massive token cost reduction (no blind file reading). Instant T2R for codebase exploration.
### 3.
vim_send_to_terminal (Interactive Execution QoL)
* **The Problem:** When the agent runs a background terminal command (cargo build,
pm run dev), the output is hidden from the human, and interactive prompts cause the background task to hang indefinitely.
* **The Solution:** A tool that opens a Neovim :term split (or uses a mux pane) and sends the command there.
* **Impact:** Massive Human QoL. The human can watch the tests run natively, interact with prompts, see ANSI colors, and press <C-c> to kill it if it loops.
### 4. ␍ead_directory_architecture (Bird's-Eye View)
* **The Problem:** ␍ead_file_skeleton works for one file. When entering a new repository, the LLM usually runs ls -R and then has to guess what files do based on their names.
* **The Solution:** A tool that scans a directory structure and uses basic heuristic parsing (or a tiny local embedding lookup) to return a JSON tree of files alongside a 1-sentence summary of what each file is responsible for.
* **Impact:** Immediate holistic context. Eliminates the "exploration phase" token tax.
### 5. query_database_schema (Introspection)
* **The Problem:** Working with databases usually involves the LLM writing clunky bash scripts to run psql or sqlite3 to view table definitions, which often fail due to missing env vars or wrong dialects.
* **The Solution:** A direct MCP tool that parses the local .env, connects to the database (Postgres/SQLite), and returns a clean Markdown representation of the schema (Tables, Columns, Types, Foreign Keys).
* **Impact:** Prevents hallucinations about database structure. Fixes DB-related bugs significantly faster (T2R).
+2
View File
@@ -61,10 +61,12 @@ mod tests {
async fn test_events_wait_and_post() { async fn test_events_wait_and_post() {
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
let mem_state = Arc::new(MemoryState::new(dir.path().to_str().unwrap())); let mem_state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
let (shutdown_tx, _) = tokio::sync::oneshot::channel();
let app_state = Arc::new(AppState { let app_state = Arc::new(AppState {
handler: Arc::new(MemoryHandler::new(mem_state.clone())), handler: Arc::new(MemoryHandler::new(mem_state.clone())),
clients: RwLock::new(HashMap::new()), clients: RwLock::new(HashMap::new()),
next_id: AtomicUsize::new(1), next_id: AtomicUsize::new(1),
shutdown_tx: std::sync::Mutex::new(Some(shutdown_tx)),
}); });
// Start wait_for_event in a background task // Start wait_for_event in a background task
+4 -2
View File
@@ -38,7 +38,7 @@ pub async fn gate_verify_handler(
) -> Result<impl IntoResponse, AppError> { ) -> Result<impl IntoResponse, AppError> {
let mut found = None; let mut found = None;
let mut to_remove = None; let mut to_remove = None;
app_state.handler.state.gates.modify(|gates| { app_state.handler.state.env.gates.modify(|gates| {
if let Some(idx) = gates.iter().position(|g| { if let Some(idx) = gates.iter().position(|g| {
g.action == q.action g.action == q.action
&& g.target == q.target && g.target == q.target
@@ -96,7 +96,7 @@ pub async fn gate_set_handler(
reason: body.reason.clone(), reason: body.reason.clone(),
timestamp: crate::handlers::utils::now_secs(), timestamp: crate::handlers::utils::now_secs(),
}; };
app_state.handler.state.gates.modify(|gates| { app_state.handler.state.env.gates.modify(|gates| {
gates.retain(|g| !(g.action == record.action && g.target == record.target)); gates.retain(|g| !(g.action == record.action && g.target == record.target));
gates.push(record); gates.push(record);
}); });
@@ -121,10 +121,12 @@ mod tests {
async fn test_gate_handlers() { async fn test_gate_handlers() {
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap())); let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
let (shutdown_tx, _) = tokio::sync::oneshot::channel();
let app_state = Arc::new(AppState { let app_state = Arc::new(AppState {
handler: Arc::new(MemoryHandler::new(state.clone())), handler: Arc::new(MemoryHandler::new(state.clone())),
clients: RwLock::new(HashMap::new()), clients: RwLock::new(HashMap::new()),
next_id: AtomicUsize::new(1), next_id: AtomicUsize::new(1),
shutdown_tx: std::sync::Mutex::new(Some(shutdown_tx)),
}); });
// Set a gate to authorized // Set a gate to authorized
+31 -29
View File
@@ -80,7 +80,7 @@ pub fn create_router(app_state: Arc<AppState>) -> Router {
post({ post({
let state_clone = app_state.handler.state.clone(); let state_clone = app_state.handler.state.clone();
move |axum::extract::Path(id): axum::extract::Path<String>| async move { move |axum::extract::Path(id): axum::extract::Path<String>| async move {
state_clone.tasks.modify(|tasks| { state_clone.project.tasks.modify(|tasks| {
for t in tasks.iter_mut() { for t in tasks.iter_mut() {
if t.id == id { if t.id == id {
t.status = "completed".to_string(); t.status = "completed".to_string();
@@ -97,7 +97,7 @@ pub fn create_router(app_state: Arc<AppState>) -> Router {
get({ get({
let state_clone = app_state.handler.state.clone(); let state_clone = app_state.handler.state.clone();
move || async move { move || async move {
let tasks_json = state_clone.tasks.read_with(|t| serde_json::to_string(t).unwrap_or_else(|_| "[]".to_string())); let tasks_json = state_clone.project.tasks.read_with(|t| serde_json::to_string(t).unwrap_or_else(|_| "[]".to_string()));
([(axum::http::header::CONTENT_TYPE, "application/json")], tasks_json) ([(axum::http::header::CONTENT_TYPE, "application/json")], tasks_json)
} }
}), }),
@@ -107,7 +107,7 @@ pub fn create_router(app_state: Arc<AppState>) -> Router {
get({ get({
let state_clone = app_state.handler.state.clone(); let state_clone = app_state.handler.state.clone();
move || async move { move || async move {
let sticky_json = state_clone.sticky.read_with(|s| serde_json::to_string(s).unwrap_or_else(|_| "[]".to_string())); let sticky_json = state_clone.code.sticky.read_with(|s| serde_json::to_string(s).unwrap_or_else(|_| "[]".to_string()));
([(axum::http::header::CONTENT_TYPE, "application/json")], sticky_json) ([(axum::http::header::CONTENT_TYPE, "application/json")], sticky_json)
} }
}), }),
@@ -145,7 +145,7 @@ pub fn create_router(app_state: Arc<AppState>) -> Router {
get({ get({
let state_clone = app_state.handler.state.clone(); let state_clone = app_state.handler.state.clone();
move || async move { move || async move {
let activities_json = state_clone.recent_activities.read_with(|a| serde_json::to_string(a).unwrap_or_else(|_| "[]".to_string())); let activities_json = state_clone.telemetry.recent_activities.read_with(|a| serde_json::to_string(a).unwrap_or_else(|_| "[]".to_string()));
([(axum::http::header::CONTENT_TYPE, "application/json")], activities_json) ([(axum::http::header::CONTENT_TYPE, "application/json")], activities_json)
} }
}), }),
@@ -155,7 +155,7 @@ pub fn create_router(app_state: Arc<AppState>) -> Router {
get({ get({
let state_clone = app_state.handler.state.clone(); let state_clone = app_state.handler.state.clone();
move || async move { move || async move {
let json = state_clone.tech_debts.read_with(|items| serde_json::to_string(items).unwrap_or_else(|_| "[]".to_string())); let json = state_clone.code.tech_debts.read_with(|items| serde_json::to_string(items).unwrap_or_else(|_| "[]".to_string()));
([(axum::http::header::CONTENT_TYPE, "application/json")], json) ([(axum::http::header::CONTENT_TYPE, "application/json")], json)
} }
}), }),
@@ -165,7 +165,7 @@ pub fn create_router(app_state: Arc<AppState>) -> Router {
get({ get({
let state_clone = app_state.handler.state.clone(); let state_clone = app_state.handler.state.clone();
move || async move { move || async move {
let json = state_clone.adrs.read_with(|items| serde_json::to_string(items).unwrap_or_else(|_| "[]".to_string())); let json = state_clone.code.adrs.read_with(|items| serde_json::to_string(items).unwrap_or_else(|_| "[]".to_string()));
([(axum::http::header::CONTENT_TYPE, "application/json")], json) ([(axum::http::header::CONTENT_TYPE, "application/json")], json)
} }
}), }),
@@ -175,7 +175,7 @@ pub fn create_router(app_state: Arc<AppState>) -> Router {
get({ get({
let state_clone = app_state.handler.state.clone(); let state_clone = app_state.handler.state.clone();
move || async move { move || async move {
let json = state_clone.context_workspaces.read_with(|items| serde_json::to_string(items).unwrap_or_else(|_| "[]".to_string())); let json = state_clone.project.context_workspaces.read_with(|items| serde_json::to_string(items).unwrap_or_else(|_| "[]".to_string()));
([(axum::http::header::CONTENT_TYPE, "application/json")], json) ([(axum::http::header::CONTENT_TYPE, "application/json")], json)
} }
}), }),
@@ -185,7 +185,7 @@ pub fn create_router(app_state: Arc<AppState>) -> Router {
get({ get({
let state_clone = app_state.handler.state.clone(); let state_clone = app_state.handler.state.clone();
move || async move { move || async move {
let json = state_clone.handoff_memos.read_with(|items| serde_json::to_string(items).unwrap_or_else(|_| "[]".to_string())); let json = state_clone.telemetry.handoff_memos.read_with(|items| serde_json::to_string(items).unwrap_or_else(|_| "[]".to_string()));
([(axum::http::header::CONTENT_TYPE, "application/json")], json) ([(axum::http::header::CONTENT_TYPE, "application/json")], json)
} }
}), }),
@@ -195,7 +195,7 @@ pub fn create_router(app_state: Arc<AppState>) -> Router {
get({ get({
let state_clone = app_state.handler.state.clone(); let state_clone = app_state.handler.state.clone();
move || async move { move || async move {
let json = state_clone.milestones.read_with(|items| serde_json::to_string(items).unwrap_or_else(|_| "[]".to_string())); let json = state_clone.project.milestones.read_with(|items| serde_json::to_string(items).unwrap_or_else(|_| "[]".to_string()));
([(axum::http::header::CONTENT_TYPE, "application/json")], json) ([(axum::http::header::CONTENT_TYPE, "application/json")], json)
} }
}), }),
@@ -205,7 +205,7 @@ pub fn create_router(app_state: Arc<AppState>) -> Router {
get({ get({
let state_clone = app_state.handler.state.clone(); let state_clone = app_state.handler.state.clone();
move || async move { move || async move {
let json = state_clone.snippets.read_with(|items| serde_json::to_string(items).unwrap_or_else(|_| "[]".to_string())); let json = state_clone.code.snippets.read_with(|items| serde_json::to_string(items).unwrap_or_else(|_| "[]".to_string()));
([(axum::http::header::CONTENT_TYPE, "application/json")], json) ([(axum::http::header::CONTENT_TYPE, "application/json")], json)
} }
}), }),
@@ -215,7 +215,7 @@ pub fn create_router(app_state: Arc<AppState>) -> Router {
get({ get({
let state_clone = app_state.handler.state.clone(); let state_clone = app_state.handler.state.clone();
move || async move { move || async move {
let json = state_clone.pr_checklists.read_with(|items| serde_json::to_string(items).unwrap_or_else(|_| "[]".to_string())); let json = state_clone.project.pr_checklists.read_with(|items| serde_json::to_string(items).unwrap_or_else(|_| "[]".to_string()));
([(axum::http::header::CONTENT_TYPE, "application/json")], json) ([(axum::http::header::CONTENT_TYPE, "application/json")], json)
} }
}), }),
@@ -225,7 +225,7 @@ pub fn create_router(app_state: Arc<AppState>) -> Router {
get({ get({
let state_clone = app_state.handler.state.clone(); let state_clone = app_state.handler.state.clone();
move || async move { move || async move {
let json = state_clone.error_fixes.read_with(|items| serde_json::to_string(items).unwrap_or_else(|_| "[]".to_string())); let json = state_clone.code.error_fixes.read_with(|items| serde_json::to_string(items).unwrap_or_else(|_| "[]".to_string()));
([(axum::http::header::CONTENT_TYPE, "application/json")], json) ([(axum::http::header::CONTENT_TYPE, "application/json")], json)
} }
}), }),
@@ -236,24 +236,24 @@ pub fn create_router(app_state: Arc<AppState>) -> Router {
let state_clone = app_state.handler.state.clone(); let state_clone = app_state.handler.state.clone();
move || async move { move || async move {
let (entities, relations) = state_clone.read_graph(|g| (g.entities.len(), g.relations.len())); let (entities, relations) = state_clone.read_graph(|g| (g.entities.len(), g.relations.len()));
let tasks = state_clone.tasks.read_with(|items| items.len()); let tasks = state_clone.project.tasks.read_with(|items| items.len());
let snippets = state_clone.snippets.read_with(|items| items.len()); let snippets = state_clone.code.snippets.read_with(|items| items.len());
let tech_debts = state_clone.tech_debts.read_with(|items| items.len()); let tech_debts = state_clone.code.tech_debts.read_with(|items| items.len());
let adrs = state_clone.adrs.read_with(|items| items.len()); let adrs = state_clone.code.adrs.read_with(|items| items.len());
let ledger = state_clone.ledger.read_with(|items| items.len()); let ledger = state_clone.code.ledger.read_with(|items| items.len());
let sticky = state_clone.sticky.read_with(|items| items.len()); let sticky = state_clone.code.sticky.read_with(|items| items.len());
let error_fixes = state_clone.error_fixes.read_with(|items| items.len()); let error_fixes = state_clone.code.error_fixes.read_with(|items| items.len());
let pinned_files = state_clone.pinned_files.read_with(|items| items.len()); let pinned_files = state_clone.project.pinned_files.read_with(|items| items.len());
let session_summaries = state_clone.session_summaries.read_with(|items| items.len()); let session_summaries = state_clone.telemetry.session_summaries.read_with(|items| items.len());
let handoff_memos = state_clone.handoff_memos.read_with(|items| items.len()); let handoff_memos = state_clone.telemetry.handoff_memos.read_with(|items| items.len());
let env_fingerprints = state_clone.env_fingerprints.read_with(|items| items.len()); let env_fingerprints = state_clone.env.env_fingerprints.read_with(|items| items.len());
let env_requirements = state_clone.env_requirements.read_with(|items| items.len()); let env_requirements = state_clone.env.env_requirements.read_with(|items| items.len());
let milestones = state_clone.milestones.read_with(|items| items.len()); let milestones = state_clone.project.milestones.read_with(|items| items.len());
let environments = state_clone.environments.read_with(|items| items.len()); let environments = state_clone.env.environments.read_with(|items| items.len());
let pr_checklists = state_clone.pr_checklists.read_with(|items| items.len()); let pr_checklists = state_clone.project.pr_checklists.read_with(|items| items.len());
let gates = state_clone.gates.read_with(|items| items.len()); let gates = state_clone.env.gates.read_with(|items| items.len());
let context_workspaces = state_clone.context_workspaces.read_with(|items| items.len()); let context_workspaces = state_clone.project.context_workspaces.read_with(|items| items.len());
axum::Json(serde_json::json!({ axum::Json(serde_json::json!({
"entities": entities, "entities": entities,
@@ -342,10 +342,12 @@ mod tests {
async fn test_create_router_health() { async fn test_create_router_health() {
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap())); let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
let (shutdown_tx, _) = tokio::sync::oneshot::channel();
let app_state = Arc::new(AppState { let app_state = Arc::new(AppState {
handler: Arc::new(MemoryHandler::new(state.clone())), handler: Arc::new(MemoryHandler::new(state.clone())),
clients: RwLock::new(HashMap::new()), clients: RwLock::new(HashMap::new()),
next_id: AtomicUsize::new(1), next_id: AtomicUsize::new(1),
shutdown_tx: std::sync::Mutex::new(Some(shutdown_tx)),
}); });
let app = create_router(app_state); let app = create_router(app_state);
+4 -2
View File
@@ -79,7 +79,7 @@ use crate::models::TerminalHistory;
pub async fn get_terminal_history_handler( pub async fn get_terminal_history_handler(
State(state): State<Arc<AppState>>, State(state): State<Arc<AppState>>,
) -> impl axum::response::IntoResponse { ) -> impl axum::response::IntoResponse {
let history_json = state.handler.state.terminal_history.read_with(|h| serde_json::to_string(h).unwrap_or_else(|_| "[]".to_string())); let history_json = state.handler.state.telemetry.terminal_history.read_with(|h| serde_json::to_string(h).unwrap_or_else(|_| "[]".to_string()));
([(axum::http::header::CONTENT_TYPE, "application/json")], history_json) ([(axum::http::header::CONTENT_TYPE, "application/json")], history_json)
} }
@@ -87,7 +87,7 @@ pub async fn terminal_telemetry_handler(
State(state): State<Arc<AppState>>, State(state): State<Arc<AppState>>,
axum::Json(payload): axum::Json<TerminalHistory>, axum::Json(payload): axum::Json<TerminalHistory>,
) -> impl axum::response::IntoResponse { ) -> impl axum::response::IntoResponse {
state.handler.state.terminal_history.modify(|history| { state.handler.state.telemetry.terminal_history.modify(|history| {
history.push_front(payload.clone()); history.push_front(payload.clone());
if history.len() > 100 { if history.len() > 100 {
history.pop_back(); history.pop_back();
@@ -131,10 +131,12 @@ mod tests {
async fn test_terminal_history() { async fn test_terminal_history() {
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap())); let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
let (shutdown_tx, _) = tokio::sync::oneshot::channel();
let app_state = Arc::new(AppState { let app_state = Arc::new(AppState {
handler: Arc::new(MemoryHandler::new(state.clone())), handler: Arc::new(MemoryHandler::new(state.clone())),
clients: std::sync::RwLock::new(HashMap::new()), clients: std::sync::RwLock::new(HashMap::new()),
next_id: AtomicUsize::new(1), next_id: AtomicUsize::new(1),
shutdown_tx: std::sync::Mutex::new(Some(shutdown_tx)),
}); });
let app = axum::Router::new() let app = axum::Router::new()
+2
View File
@@ -156,10 +156,12 @@ mod tests {
async fn test_session_cleanup_drop() { async fn test_session_cleanup_drop() {
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap())); let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
let (shutdown_tx, _) = tokio::sync::oneshot::channel();
let app_state = Arc::new(AppState { let app_state = Arc::new(AppState {
handler: Arc::new(MemoryHandler::new(state)), handler: Arc::new(MemoryHandler::new(state)),
clients: RwLock::new(HashMap::new()), clients: RwLock::new(HashMap::new()),
next_id: AtomicUsize::new(1), next_id: AtomicUsize::new(1),
shutdown_tx: std::sync::Mutex::new(Some(shutdown_tx)),
}); });
// Insert a dummy client // Insert a dummy client
+3 -5
View File
@@ -1,11 +1,9 @@
use clipboard_win::{formats, get_clipboard, Clipboard}; use arboard::Clipboard;
fn main() { fn main() {
if let Ok(_clip) = Clipboard::new_attempts(10) { if let Ok(mut clipboard) = Clipboard::new() {
let text: Result<String, _> = get_clipboard(formats::Unicode); let text = clipboard.get_text();
let files: Result<Vec<String>, _> = get_clipboard(formats::FileList);
println!("Text: {:?}", text.ok()); println!("Text: {:?}", text.ok());
println!("Files: {:?}", files.ok());
} else { } else {
println!("Failed to open clipboard"); println!("Failed to open clipboard");
} }
+4 -5
View File
@@ -2,8 +2,7 @@ use crate::state::MemoryState;
use crate::models::StickyNote; use crate::models::StickyNote;
use std::sync::Arc; use std::sync::Arc;
use tokio::time::{sleep, Duration}; use tokio::time::{sleep, Duration};
use clipboard_win::{formats, get_clipboard, Clipboard}; use arboard::Clipboard;
pub fn spawn_watcher(state: Arc<MemoryState>) { pub fn spawn_watcher(state: Arc<MemoryState>) {
tokio::spawn(async move { tokio::spawn(async move {
let mut last_text = String::new(); let mut last_text = String::new();
@@ -19,8 +18,8 @@ pub fn spawn_watcher(state: Arc<MemoryState>) {
continue; continue;
} }
if let Ok(_clip) = tokio::task::spawn_blocking(|| Clipboard::new_attempts(3)).await.unwrap() { if let Ok(mut clipboard) = Clipboard::new() {
if let Ok(text) = get_clipboard::<String, _>(formats::Unicode) { if let Ok(text) = clipboard.get_text() {
if text != last_text && !text.trim().is_empty() { if text != last_text && !text.trim().is_empty() {
last_text = text.clone(); last_text = text.clone();
@@ -33,7 +32,7 @@ pub fn spawn_watcher(state: Arc<MemoryState>) {
expires_at: None, expires_at: None,
}; };
state.sticky.modify(|notes| { state.code.sticky.modify(|notes| {
notes.push(note.clone()); notes.push(note.clone());
}); });
+29 -1
View File
@@ -6,6 +6,8 @@ use axum::{
use serde_json::json; use serde_json::json;
use thiserror::Error; use thiserror::Error;
pub type Result<T> = std::result::Result<T, AppError>;
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum AppError { pub enum AppError {
#[error("Not Found: {0}")] #[error("Not Found: {0}")]
@@ -19,6 +21,18 @@ pub enum AppError {
#[error("Bad Request: {0}")] #[error("Bad Request: {0}")]
BadRequest(String), BadRequest(String),
#[error("Serialization Error: {0}")]
JsonError(#[from] serde_json::Error),
#[error("I/O Error: {0}")]
IoError(#[from] std::io::Error),
}
impl From<AppError> for String {
fn from(err: AppError) -> String {
err.to_string()
}
} }
impl IntoResponse for AppError { impl IntoResponse for AppError {
@@ -26,8 +40,10 @@ impl IntoResponse for AppError {
let (status, error_message) = match &self { let (status, error_message) = match &self {
AppError::NotFound(msg) => (StatusCode::NOT_FOUND, msg.clone()), AppError::NotFound(msg) => (StatusCode::NOT_FOUND, msg.clone()),
AppError::Forbidden(msg) => (StatusCode::FORBIDDEN, msg.clone()), AppError::Forbidden(msg) => (StatusCode::FORBIDDEN, msg.clone()),
AppError::Internal(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg.clone()),
AppError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg.clone()), AppError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg.clone()),
AppError::Internal(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg.clone()),
AppError::JsonError(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("JSON Error: {}", e)),
AppError::IoError(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("I/O Error: {}", e)),
}; };
let body = Json(json!({ let body = Json(json!({
@@ -87,3 +103,15 @@ mod tests {
assert_eq!(err.to_string(), "Bad Request: test"); assert_eq!(err.to_string(), "Bad Request: test");
} }
} }
impl From<String> for AppError {
fn from(msg: String) -> Self {
AppError::Internal(msg)
}
}
impl From<&str> for AppError {
fn from(msg: &str) -> Self {
AppError::Internal(msg.to_string())
}
}
+65 -20
View File
@@ -21,13 +21,13 @@ impl McpTool for ReadFileSkeletonHandler {
) )
} }
async fn execute(&self, args: Value, _state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, _state: Arc<MemoryState>) -> crate::error::Result<String> {
let tool_args: ReadFileSkeletonTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let tool_args: ReadFileSkeletonTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let file_path = tool_args.file_path.clone(); let file_path = tool_args.file_path.clone();
let result = tokio::task::spawn_blocking(move || -> Result<String, String> { let result = tokio::task::spawn_blocking(move || -> crate::error::Result<String> {
let code = std::fs::read_to_string(&file_path) let code = std::fs::read_to_string(&file_path)
.map_err(|e| format!("Failed to read file: {}", e))?; .map_err(|e| crate::error::AppError::Internal(format!("Failed to read file: {}", e)))?;
let mut parser = Parser::new(); let mut parser = Parser::new();
@@ -49,19 +49,13 @@ impl McpTool for ReadFileSkeletonHandler {
parser.set_language(&language.into()).map_err(|e| e.to_string())?; parser.set_language(&language.into()).map_err(|e| e.to_string())?;
let tree = parser.parse(&code, None).ok_or("Failed to parse code")?; let tree = parser.parse(&code, None).ok_or_else(|| crate::error::AppError::Internal("Failed to parse code".to_string()))?;
let mut result_skeleton = String::new(); let mut result_skeleton = String::new();
fn extract_skeleton(node: Node, code: &str, out: &mut String, depth: usize) { fn extract_skeleton(node: Node, code: &str, out: &mut String, depth: usize) {
let kind = node.kind(); let kind = node.kind();
let is_structural = match kind { let is_structural = matches!(kind, "use_declaration" | "import_statement" | "import_from_statement" | "struct_item" | "enum_item" | "trait_item" | "impl_item" | "function_item" | "function_declaration" | "function_definition" | "method_definition" | "interface_declaration" | "type_alias_declaration" | "class_declaration" | "class_definition");
"use_declaration" | "import_statement" | "import_from_statement" => true,
"struct_item" | "enum_item" | "trait_item" | "impl_item" => true,
"function_item" | "function_declaration" | "function_definition" | "method_definition" => true,
"interface_declaration" | "type_alias_declaration" | "class_declaration" | "class_definition" => true,
_ => false,
};
if is_structural { if is_structural {
let indent = " ".repeat(depth); let indent = " ".repeat(depth);
@@ -84,7 +78,7 @@ impl McpTool for ReadFileSkeletonHandler {
} }
out.push_str(&indent); out.push_str(&indent);
out.push_str(&signature.trim()); out.push_str(signature.trim());
out.push('\n'); out.push('\n');
} else if node.is_named() { } else if node.is_named() {
let mut cursor = node.walk(); let mut cursor = node.walk();
@@ -103,7 +97,7 @@ impl McpTool for ReadFileSkeletonHandler {
} }
}) })
.await .await
.map_err(|e| format!("Task panic: {}", e))??; .map_err(|e| crate::error::AppError::Internal(format!("Task panic: {}", e)))??;
Ok(result) Ok(result)
} }
@@ -125,13 +119,13 @@ impl McpTool for ReplaceAstNodeHandler {
) )
} }
async fn execute(&self, args: Value, _state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, _state: Arc<MemoryState>) -> crate::error::Result<String> {
let tool_args: ReplaceAstNodeTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let tool_args: ReplaceAstNodeTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let file_path = tool_args.file_path.clone(); let file_path = tool_args.file_path.clone();
let result = tokio::task::spawn_blocking(move || -> Result<String, String> { let result = tokio::task::spawn_blocking(move || -> crate::error::Result<String> {
let code = std::fs::read_to_string(&file_path) let code = std::fs::read_to_string(&file_path)
.map_err(|e| format!("Failed to read file: {}", e))?; .map_err(|e| crate::error::AppError::Internal(format!("Failed to read file: {}", e)))?;
let mut parser = Parser::new(); let mut parser = Parser::new();
@@ -148,11 +142,11 @@ impl McpTool for ReplaceAstNodeHandler {
"c" | "h" => tree_sitter_c::LANGUAGE, "c" | "h" => tree_sitter_c::LANGUAGE,
"cpp" | "cc" | "cxx" | "hpp" | "hxx" => tree_sitter_cpp::LANGUAGE, "cpp" | "cc" | "cxx" | "hpp" | "hxx" => tree_sitter_cpp::LANGUAGE,
"go" => tree_sitter_go::LANGUAGE, "go" => tree_sitter_go::LANGUAGE,
_ => return Err(format!("Unsupported language for AST replacement: {}", ext)), _ => return Err(crate::error::AppError::Internal(format!("Unsupported language for AST replacement: {}", ext))),
}; };
parser.set_language(&language.into()).map_err(|e| e.to_string())?; parser.set_language(&language.into()).map_err(|e| e.to_string())?;
let tree = parser.parse(&code, None).ok_or("Failed to parse code")?; let tree = parser.parse(&code, None).ok_or_else(|| crate::error::AppError::Internal("Failed to parse code".to_string()))?;
// Search for the node // Search for the node
fn find_node<'a>(node: Node<'a>, code: &str, target_type: &str, target_name: &str) -> Option<Node<'a>> { fn find_node<'a>(node: Node<'a>, code: &str, target_type: &str, target_name: &str) -> Option<Node<'a>> {
@@ -193,12 +187,63 @@ impl McpTool for ReplaceAstNodeHandler {
std::fs::write(&file_path, new_file_content).map_err(|e| e.to_string())?; std::fs::write(&file_path, new_file_content).map_err(|e| e.to_string())?;
Ok(format!("Successfully replaced node {} of type {} in {}", tool_args.node_name, tool_args.node_type, file_path)) Ok(format!("Successfully replaced node {} of type {} in {}", tool_args.node_name, tool_args.node_type, file_path))
} else { } else {
Err(format!("Could not find node {} of type {}", tool_args.node_name, tool_args.node_type)) Err(crate::error::AppError::Internal(format!("Could not find node {} of type {}", tool_args.node_name, tool_args.node_type)))
} }
}) })
.await .await
.map_err(|e| format!("Task panic: {}", e))??; .map_err(|e| crate::error::AppError::Internal(format!("Task panic: {}", e)))??;
Ok(result) Ok(result)
} }
} }
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use tempfile::tempdir;
#[tokio::test]
async fn test_read_file_skeleton() {
let dir = tempdir().unwrap();
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
let file_path = dir.path().join("test_skeleton.rs");
let code = "fn my_func() {\n let x = 1;\n}\n\nstruct MyStruct {\n val: i32\n}";
std::fs::write(&file_path, code).unwrap();
let handler = ReadFileSkeletonHandler;
let args = json!({
"file_path": file_path.to_str().unwrap()
});
let res = handler.execute(args, state.clone()).await.unwrap();
assert!(res.contains("fn my_func()"));
assert!(res.contains("struct MyStruct"));
}
#[tokio::test]
async fn test_replace_ast_node() {
let dir = tempdir().unwrap();
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
let file_path = dir.path().join("test_replace.rs");
let code = "fn my_func() {\n let x = 1;\n}\n\nstruct MyStruct {\n val: i32\n}";
std::fs::write(&file_path, code).unwrap();
let handler = ReplaceAstNodeHandler;
let args = json!({
"file_path": file_path.to_str().unwrap(),
"node_type": "function_item",
"node_name": "my_func",
"new_content": "fn my_func() {\n let x = 2;\n}"
});
let res = handler.execute(args, state.clone()).await.unwrap();
assert!(res.contains("Successfully replaced node"));
let new_code = std::fs::read_to_string(&file_path).unwrap();
assert!(new_code.contains("let x = 2;"));
assert!(!new_code.contains("let x = 1;"));
}
}
+19 -22
View File
@@ -20,10 +20,10 @@ impl McpTool for UpdateEnvFingerprintHandler {
) )
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: UpdateEnvFingerprintTool = let req: UpdateEnvFingerprintTool =
serde_json::from_value(args).map_err(|e| e.to_string())?; serde_json::from_value(args).map_err(|e| e.to_string())?;
state.env_fingerprints.modify(|fps| { state.env.env_fingerprints.modify(|fps| {
fps.insert( fps.insert(
req.namespace.clone(), req.namespace.clone(),
crate::models::EnvFingerprint { crate::models::EnvFingerprint {
@@ -54,14 +54,14 @@ impl McpTool for ReadEnvFingerprintHandler {
) )
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: ReadEnvFingerprintTool = let req: ReadEnvFingerprintTool =
serde_json::from_value(args).map_err(|e| e.to_string())?; serde_json::from_value(args).map_err(|e| e.to_string())?;
let data = state let data = state
.env_fingerprints .env.env_fingerprints
.read_with(|fps| fps.get(&req.namespace).cloned()); .read_with(|fps| fps.get(&req.namespace).cloned());
if let Some(fp) = data { if let Some(fp) = data {
let data = serde_json::to_string(&fp).map_err(|e| e.to_string())?; let data = Ok::<String, crate::error::AppError>(serde_json::to_string(&fp)?)?;
Ok(data) Ok(data)
} else { } else {
Ok("{}".to_string()) Ok("{}".to_string())
@@ -84,9 +84,9 @@ impl McpTool for LogEnvRequirementHandler {
) )
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: LogEnvRequirementTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let req: LogEnvRequirementTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
state.env_requirements.modify(|reqs| { state.env.env_requirements.modify(|reqs| {
reqs.retain(|r| !(r.namespace == req.namespace && r.key == req.key)); reqs.retain(|r| !(r.namespace == req.namespace && r.key == req.key));
reqs.push(crate::models::EnvRequirement { reqs.push(crate::models::EnvRequirement {
namespace: req.namespace, namespace: req.namespace,
@@ -114,10 +114,10 @@ impl McpTool for RegisterEnvironmentHandler {
) )
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: RegisterEnvironmentTool = let req: RegisterEnvironmentTool =
serde_json::from_value(args).map_err(|e| e.to_string())?; serde_json::from_value(args).map_err(|e| e.to_string())?;
state.environments.modify(|envs| { state.env.environments.modify(|envs| {
envs.retain(|e| !(e.namespace == req.namespace && e.name == req.name)); envs.retain(|e| !(e.namespace == req.namespace && e.name == req.name));
envs.push(crate::models::EnvironmentDetail { envs.push(crate::models::EnvironmentDetail {
namespace: req.namespace, namespace: req.namespace,
@@ -147,15 +147,15 @@ impl McpTool for GetEnvironmentDetailsHandler {
) )
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: GetEnvironmentDetailsTool = let req: GetEnvironmentDetailsTool =
serde_json::from_value(args).map_err(|e| e.to_string())?; serde_json::from_value(args).map_err(|e| e.to_string())?;
let data = state.environments.read_with(|envs| { let data = state.env.environments.read_with(|envs| {
let filtered: Vec<_> = envs let filtered: Vec<_> = envs
.iter() .iter()
.filter(|e| e.namespace == req.namespace) .filter(|e| e.namespace == req.namespace)
.collect(); .collect();
serde_json::to_string(&filtered).map_err(|e| e.to_string()) Ok::<String, crate::error::AppError>(serde_json::to_string(&filtered)?)
})?; })?;
Ok(data) Ok(data)
} }
@@ -180,14 +180,13 @@ mod tests {
} }
}); });
let res = update_handler.execute(args, state.clone()).await.unwrap(); let res = update_handler.execute(args, state.clone()).await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
assert_eq!(res, "Env fingerprint updated"); assert_eq!(res, "Env fingerprint updated");
let read_handler = ReadEnvFingerprintHandler; let read_handler = ReadEnvFingerprintHandler;
let res2 = read_handler let res2 = read_handler
.execute(json!({"namespace": "global"}), state.clone()) .execute(json!({"namespace": "global"}), state.clone())
.await .await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
.unwrap();
assert!(res2.contains("rustc")); assert!(res2.contains("rustc"));
assert!(res2.contains("1.70.0")); assert!(res2.contains("1.70.0"));
} }
@@ -198,7 +197,7 @@ mod tests {
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap())); let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
// Ensure namespace is present in test setup // Ensure namespace is present in test setup
state.environments.modify(|e| { state.env.environments.modify(|e| {
e.push(crate::models::EnvironmentDetail { e.push(crate::models::EnvironmentDetail {
namespace: "global".to_string(), namespace: "global".to_string(),
name: "test".to_string(), name: "test".to_string(),
@@ -212,8 +211,7 @@ mod tests {
let handler = GetEnvironmentDetailsHandler; let handler = GetEnvironmentDetailsHandler;
let res = handler let res = handler
.execute(json!({"namespace": "global"}), state.clone()) .execute(json!({"namespace": "global"}), state.clone())
.await .await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
.unwrap();
assert!(res.contains("global")); assert!(res.contains("global"));
} }
@@ -232,7 +230,7 @@ mod tests {
"context": "For database access", "context": "For database access",
"namespace": "global" "namespace": "global"
}); });
let res1 = req_handler.execute(args_req, state.clone()).await.unwrap(); let res1 = req_handler.execute(args_req, state.clone()).await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
assert_eq!(res1, "Env requirement logged"); assert_eq!(res1, "Env requirement logged");
let reg_handler = RegisterEnvironmentHandler; let reg_handler = RegisterEnvironmentHandler;
@@ -243,14 +241,13 @@ mod tests {
"requires_vpn": true, "requires_vpn": true,
"namespace": "global" "namespace": "global"
}); });
let res2 = reg_handler.execute(args_reg, state.clone()).await.unwrap(); let res2 = reg_handler.execute(args_reg, state.clone()).await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
assert_eq!(res2, "Environment registered"); assert_eq!(res2, "Environment registered");
let get_handler = GetEnvironmentDetailsHandler; let get_handler = GetEnvironmentDetailsHandler;
let res3 = get_handler let res3 = get_handler
.execute(json!({"namespace": "global"}), state.clone()) .execute(json!({"namespace": "global"}), state.clone())
.await .await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
.unwrap();
assert!(res3.contains("prod.local")); assert!(res3.contains("prod.local"));
assert!(!res3.is_empty()); assert!(!res3.is_empty());
} }
+32 -8
View File
@@ -21,17 +21,16 @@ impl McpTool for GetActiveWorktreeContextHandler {
) )
} }
async fn execute(&self, _args: Value, _state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, _args: Value, _state: Arc<MemoryState>) -> crate::error::Result<String> {
let result = tokio::task::spawn_blocking(move || -> Result<Value, String> { let result = tokio::task::spawn_blocking(move || -> crate::error::Result<serde_json::Value> {
let cwd = env::current_dir().map_err(|e| e.to_string())?; let cwd = env::current_dir().map_err(|e| e.to_string())?;
let repo = git2::Repository::discover(&cwd).map_err(|e| format!("Not in a git repository: {}", e))?; let repo = git2::Repository::discover(&cwd).map_err(|e| crate::error::AppError::Internal(format!("Not in a git repository: {}", e)))?;
let mut branch_name = String::new(); let mut branch_name = String::new();
if let Ok(head) = repo.head() { if let Ok(head) = repo.head()
if let Some(name) = head.shorthand() { && let Some(name) = head.shorthand() {
branch_name = name.to_string(); branch_name = name.to_string();
} }
}
let mut opts = git2::DiffOptions::new(); let mut opts = git2::DiffOptions::new();
let mut diff = None; let mut diff = None;
@@ -71,8 +70,33 @@ impl McpTool for GetActiveWorktreeContextHandler {
})) }))
}) })
.await .await
.map_err(|e| format!("Task panic: {}", e))??; .map_err(|e| crate::error::AppError::Internal(format!("Task panic: {}", e)))??;
Ok(serde_json::to_string_pretty(&result).unwrap()) Ok::<String, crate::error::AppError>(serde_json::to_string_pretty(&result)?)
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
use std::sync::Arc;
use serde_json::json;
#[tokio::test]
async fn test_get_active_worktree_context() {
let dir = tempdir().unwrap();
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
let handler = GetActiveWorktreeContextHandler;
let result = handler.execute(json!({}), state)
.await
.map_err(|e| format!("Failed to get worktree context: {}", e))
.unwrap();
let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
assert!(parsed.get("branch").is_some());
assert!(parsed.get("modified_files").is_some());
assert!(parsed.get("diff").is_some());
} }
} }
+54 -67
View File
@@ -24,7 +24,7 @@ impl McpTool for QueryGraphPathHandler {
crate::mcp::tool_def::<QueryGraphPathTool>("query_graph_path", "Execute query_graph_path") crate::mcp::tool_def::<QueryGraphPathTool>("query_graph_path", "Execute query_graph_path")
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: crate::tools::QueryGraphPathTool = let req: crate::tools::QueryGraphPathTool =
serde_json::from_value(args).map_err(|e| e.to_string())?; serde_json::from_value(args).map_err(|e| e.to_string())?;
state.read_graph(|graph| { state.read_graph(|graph| {
@@ -115,7 +115,7 @@ impl McpTool for CreateEntitiesHandler {
crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Execute create_entities") crate::mcp::tool_def::<CreateEntitiesTool>("create_entities", "Execute create_entities")
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: CreateEntitiesTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let req: CreateEntitiesTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let mut inserted = Vec::new(); let mut inserted = Vec::new();
state.modify_graph(|g| { state.modify_graph(|g| {
@@ -146,7 +146,7 @@ impl McpTool for CreateRelationsHandler {
crate::mcp::tool_def::<CreateRelationsTool>("create_relations", "Execute create_relations") crate::mcp::tool_def::<CreateRelationsTool>("create_relations", "Execute create_relations")
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: CreateRelationsTool = match serde_json::from_value(args.clone()) { let req: CreateRelationsTool = match serde_json::from_value(args.clone()) {
Ok(r) => r, Ok(r) => r,
Err(e) => { Err(e) => {
@@ -155,12 +155,12 @@ impl McpTool for CreateRelationsHandler {
|| err_msg.contains("missing field `to`") || err_msg.contains("missing field `to`")
|| err_msg.contains("missing field `relation_type`") || err_msg.contains("missing field `relation_type`")
{ {
return Err(format!( return Err(crate::error::AppError::Internal(format!(
"Schema error: {}. Note that the relation schema strictly uses 'from', 'to', and 'relation_type' (not 'source', 'target', or 'relationType'). Please correct your tool call arguments.", "Schema error: {}. Note that the relation schema strictly uses 'from', 'to', and 'relation_type' (not 'source', 'target', or 'relationType'). Please correct your tool call arguments.",
err_msg err_msg
)); )));
} }
return Err(err_msg); return Err(err_msg.into());
} }
}; };
let mut missing_nodes = std::collections::HashSet::new(); let mut missing_nodes = std::collections::HashSet::new();
@@ -184,10 +184,9 @@ impl McpTool for CreateRelationsHandler {
}); });
if !missing_nodes.is_empty() { if !missing_nodes.is_empty() {
let missing: Vec<_> = missing_nodes.into_iter().collect(); let missing: Vec<_> = missing_nodes.into_iter().collect();
return Err(format!( return Err(crate::error::AppError::Internal(format!(
"Error: Relations dropped due to missing entities: {}", "Error: Relations dropped due to missing entities: {}",
missing.join(", ") missing.join(", "))));
));
} }
Ok("Relations created".to_string()) Ok("Relations created".to_string())
} }
@@ -205,7 +204,7 @@ impl McpTool for AddObservationsHandler {
crate::mcp::tool_def::<AddObservationsTool>("add_observations", "Execute add_observations") crate::mcp::tool_def::<AddObservationsTool>("add_observations", "Execute add_observations")
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: AddObservationsTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let req: AddObservationsTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let mut missing_entities = Vec::new(); let mut missing_entities = Vec::new();
state.modify_graph(|g| { state.modify_graph(|g| {
@@ -218,10 +217,9 @@ impl McpTool for AddObservationsHandler {
} }
}); });
if !missing_entities.is_empty() { if !missing_entities.is_empty() {
return Err(format!( return Err(crate::error::AppError::Internal(format!(
"Error: Observations dropped for missing entities: {}", "Error: Observations dropped for missing entities: {}",
missing_entities.join(", ") missing_entities.join(", "))));
));
} }
Ok("Observations added".to_string()) Ok("Observations added".to_string())
} }
@@ -239,7 +237,7 @@ impl McpTool for DeleteEntitiesHandler {
crate::mcp::tool_def::<DeleteEntitiesTool>("delete_entities", "Execute delete_entities") crate::mcp::tool_def::<DeleteEntitiesTool>("delete_entities", "Execute delete_entities")
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: DeleteEntitiesTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let req: DeleteEntitiesTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let to_delete: std::collections::HashSet<_> = req.entity_names.into_iter().collect(); let to_delete: std::collections::HashSet<_> = req.entity_names.into_iter().collect();
let mut missing = Vec::new(); let mut missing = Vec::new();
@@ -255,10 +253,9 @@ impl McpTool for DeleteEntitiesHandler {
}); });
if !missing.is_empty() { if !missing.is_empty() {
return Err(format!( return Err(crate::error::AppError::Internal(format!(
"Error: Entities not found: {}. Please use the search_nodes or read_graph tools to verify the exact entity names.", "Error: Entities not found: {}. Please use the search_nodes or read_graph tools to verify the exact entity names.",
missing.join(", ") missing.join(", "))));
));
} }
let idx = state.get_search_index(); let idx = state.get_search_index();
@@ -284,7 +281,7 @@ impl McpTool for DeleteObservationsHandler {
) )
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: DeleteObservationsTool = let req: DeleteObservationsTool =
serde_json::from_value(args).map_err(|e| e.to_string())?; serde_json::from_value(args).map_err(|e| e.to_string())?;
let mut missing = Vec::new(); let mut missing = Vec::new();
@@ -299,10 +296,9 @@ impl McpTool for DeleteObservationsHandler {
} }
}); });
if !missing.is_empty() { if !missing.is_empty() {
return Err(format!( return Err(crate::error::AppError::Internal(format!(
"Error: Entities not found: {}. Please use the search_nodes or read_graph tools to verify the exact entity names.", "Error: Entities not found: {}. Please use the search_nodes or read_graph tools to verify the exact entity names.",
missing.join(", ") missing.join(", "))));
));
} }
Ok("Observations deleted".to_string()) Ok("Observations deleted".to_string())
} }
@@ -320,7 +316,7 @@ impl McpTool for DeleteRelationsHandler {
crate::mcp::tool_def::<DeleteRelationsTool>("delete_relations", "Execute delete_relations") crate::mcp::tool_def::<DeleteRelationsTool>("delete_relations", "Execute delete_relations")
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: DeleteRelationsTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let req: DeleteRelationsTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let mut missing_count = 0; let mut missing_count = 0;
state.modify_graph(|master| { state.modify_graph(|master| {
@@ -330,10 +326,10 @@ impl McpTool for DeleteRelationsHandler {
missing_count = to_rem.len() - (initial_len - master.relations.len()); missing_count = to_rem.len() - (initial_len - master.relations.len());
}); });
if missing_count > 0 { if missing_count > 0 {
return Err(format!( return Err(crate::error::AppError::Internal(format!(
"Error: {} relation(s) not found in graph. Please verify exact relation properties using read_graph.", "Error: {} relation(s) not found in graph. Please verify exact relation properties using read_graph.",
missing_count missing_count
)); )));
} }
Ok("Relations deleted".to_string()) Ok("Relations deleted".to_string())
} }
@@ -351,9 +347,9 @@ impl McpTool for ReadGraphHandler {
crate::mcp::tool_def::<ReadGraphTool>("read_graph", "Execute read_graph") crate::mcp::tool_def::<ReadGraphTool>("read_graph", "Execute read_graph")
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: ReadGraphTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let req: ReadGraphTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let data = state.read_graph(|full| -> Result<String, String> { let data = state.read_graph(|full| -> crate::error::Result<String> {
if let Some(ns) = req.namespace { if let Some(ns) = req.namespace {
let mut filtered = BorrowedGraph::default(); let mut filtered = BorrowedGraph::default();
for (k, v) in &full.entities { for (k, v) in &full.entities {
@@ -366,9 +362,9 @@ impl McpTool for ReadGraphHandler {
filtered.relations.push(r); filtered.relations.push(r);
} }
} }
serde_json::to_string(&filtered).map_err(|e| e.to_string()) Ok::<String, crate::error::AppError>(serde_json::to_string(&filtered)?)
} else { } else {
serde_json::to_string(full).map_err(|e| e.to_string()) Ok::<String, crate::error::AppError>(serde_json::to_string(full)?)
} }
})?; })?;
Ok(data) Ok(data)
@@ -387,7 +383,7 @@ impl McpTool for SearchNodesHandler {
crate::mcp::tool_def::<SearchNodesTool>("search_nodes", "Execute search_nodes") crate::mcp::tool_def::<SearchNodesTool>("search_nodes", "Execute search_nodes")
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: SearchNodesTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let req: SearchNodesTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let matches = if let Ok(idx) = state.search_index.read() { let matches = if let Ok(idx) = state.search_index.read() {
idx.search(&req.query, req.namespace.as_deref()) idx.search(&req.query, req.namespace.as_deref())
@@ -396,7 +392,7 @@ impl McpTool for SearchNodesHandler {
vec![] vec![]
}; };
let data = state.read_graph(|full| -> Result<String, String> { let data = state.read_graph(|full| -> crate::error::Result<String> {
let mut result = BorrowedGraph::default(); let mut result = BorrowedGraph::default();
for (id, doc_type, _, _, _) in &matches { for (id, doc_type, _, _, _) in &matches {
if doc_type == "entity" if doc_type == "entity"
@@ -405,7 +401,7 @@ impl McpTool for SearchNodesHandler {
result.entities.insert(id, e); result.entities.insert(id, e);
} }
} }
serde_json::to_string(&result).map_err(|e| e.to_string()) Ok::<String, crate::error::AppError>(serde_json::to_string(&result)?)
})?; })?;
Ok(data) Ok(data)
} }
@@ -423,9 +419,9 @@ impl McpTool for OpenNodesHandler {
crate::mcp::tool_def::<OpenNodesTool>("open_nodes", "Execute open_nodes") crate::mcp::tool_def::<OpenNodesTool>("open_nodes", "Execute open_nodes")
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: OpenNodesTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let req: OpenNodesTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let data = state.read_graph(|full| -> Result<String, String> { let data = state.read_graph(|full| -> crate::error::Result<String> {
let targets: HashSet<&str> = req.names.iter().map(|s| s.as_str()).collect(); let targets: HashSet<&str> = req.names.iter().map(|s| s.as_str()).collect();
let mut result = BorrowedGraph::default(); let mut result = BorrowedGraph::default();
let mut connected: HashSet<&str> = HashSet::new(); let mut connected: HashSet<&str> = HashSet::new();
@@ -443,7 +439,7 @@ impl McpTool for OpenNodesHandler {
result.entities.insert(name, e); result.entities.insert(name, e);
} }
} }
serde_json::to_string(&result).map_err(|e| e.to_string()) Ok::<String, crate::error::AppError>(serde_json::to_string(&result)?)
})?; })?;
Ok(data) Ok(data)
} }
@@ -461,7 +457,7 @@ impl McpTool for VisualizeGraphHandler {
crate::mcp::tool_def::<VisualizeGraphTool>("visualize_graph", "Execute visualize_graph") crate::mcp::tool_def::<VisualizeGraphTool>("visualize_graph", "Execute visualize_graph")
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: VisualizeGraphTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let req: VisualizeGraphTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let query = req.query.unwrap_or_default(); let query = req.query.unwrap_or_default();
let mut output = state.read_graph(|full| { let mut output = state.read_graph(|full| {
@@ -550,7 +546,7 @@ impl McpTool for CondenseEntityHandler {
crate::mcp::tool_def::<CondenseEntityTool>("condense_entity", "Execute condense_entity") crate::mcp::tool_def::<CondenseEntityTool>("condense_entity", "Execute condense_entity")
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: CondenseEntityTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let req: CondenseEntityTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let mut missing = false; let mut missing = false;
state.modify_graph(|master| { state.modify_graph(|master| {
@@ -561,10 +557,10 @@ impl McpTool for CondenseEntityHandler {
} }
}); });
if missing { if missing {
return Err(format!( return Err(crate::error::AppError::Internal(format!(
"Error: Entity '{}' not found. Please verify the exact entity name using search_nodes.", "Error: Entity '{}' not found. Please verify the exact entity name using search_nodes.",
req.entity_name req.entity_name
)); )));
} }
Ok("Entity condensed".to_string()) Ok("Entity condensed".to_string())
} }
@@ -582,7 +578,7 @@ impl McpTool for MergeEntitiesHandler {
crate::mcp::tool_def::<MergeEntitiesTool>("merge_entities", "Execute merge_entities") crate::mcp::tool_def::<MergeEntitiesTool>("merge_entities", "Execute merge_entities")
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: MergeEntitiesTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let req: MergeEntitiesTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let mut missing = false; let mut missing = false;
state.modify_graph(|master| { state.modify_graph(|master| {
@@ -616,10 +612,10 @@ impl McpTool for MergeEntitiesHandler {
}); });
}); });
if missing { if missing {
return Err(format!( return Err(crate::error::AppError::Internal(format!(
"Error: Source entity '{}' not found. Please verify the exact entity name using search_nodes.", "Error: Source entity '{}' not found. Please verify the exact entity name using search_nodes.",
req.source_entity req.source_entity
)); )));
} }
Ok("Entities merged".to_string()) Ok("Entities merged".to_string())
} }
@@ -637,7 +633,7 @@ impl McpTool for FindOrphansHandler {
crate::mcp::tool_def::<FindOrphansTool>("find_orphans", "Execute find_orphans") crate::mcp::tool_def::<FindOrphansTool>("find_orphans", "Execute find_orphans")
} }
async fn execute(&self, _args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, _args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let orphans = state.read_graph(|full| { let orphans = state.read_graph(|full| {
let mut connected = std::collections::HashSet::new(); let mut connected = std::collections::HashSet::new();
for r in &full.relations { for r in &full.relations {
@@ -650,7 +646,7 @@ impl McpTool for FindOrphansHandler {
.cloned() .cloned()
.collect::<Vec<String>>() .collect::<Vec<String>>()
}); });
let data = serde_json::to_string(&orphans).map_err(|e| e.to_string())?; let data = Ok::<String, crate::error::AppError>(serde_json::to_string(&orphans)?)?;
Ok(data) Ok(data)
} }
} }
@@ -675,7 +671,7 @@ mod tests {
] ]
}); });
let res = create_handler.execute(args, state.clone()).await.unwrap(); let res = create_handler.execute(args, state.clone()).await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
assert_eq!(res, "Entities created"); assert_eq!(res, "Entities created");
// Ensure graph contains the entity // Ensure graph contains the entity
@@ -720,7 +716,7 @@ mod tests {
{"from": "A", "to": "B", "relation_type": "knows"} {"from": "A", "to": "B", "relation_type": "knows"}
] ]
}); });
let res = handler.execute(args, state.clone()).await.unwrap(); let res = handler.execute(args, state.clone()).await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
assert_eq!(res, "Relations created"); assert_eq!(res, "Relations created");
// Test semantic LLM schema feedback (User request) // Test semantic LLM schema feedback (User request)
@@ -729,7 +725,7 @@ mod tests {
{"source": "A", "target": "B", "relationType": "knows"} {"source": "A", "target": "B", "relationType": "knows"}
] ]
}); });
let err_res = handler.execute(bad_args, state.clone()).await.unwrap_err(); let err_res = handler.execute(bad_args, state.clone()).await.unwrap_err().to_string();
assert!(err_res.contains("Schema error:")); assert!(err_res.contains("Schema error:"));
assert!(err_res.contains("strictly uses 'from', 'to', and 'relation_type'")); assert!(err_res.contains("strictly uses 'from', 'to', and 'relation_type'"));
} }
@@ -759,28 +755,25 @@ mod tests {
{"entity_name": "A", "contents": ["Obs 1", "Obs 2"]} {"entity_name": "A", "contents": ["Obs 1", "Obs 2"]}
] ]
}); });
let res1 = add_obs.execute(args_obs, state.clone()).await.unwrap(); let res1 = add_obs.execute(args_obs, state.clone()).await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
assert_eq!(res1, "Observations added"); assert_eq!(res1, "Observations added");
let read_graph = ReadGraphHandler; let read_graph = ReadGraphHandler;
let res2 = read_graph let res2 = read_graph
.execute(json!({"namespace": "global"}), state.clone()) .execute(json!({"namespace": "global"}), state.clone())
.await .await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
.unwrap();
assert!(res2.contains("Obs 1")); assert!(res2.contains("Obs 1"));
assert!(res2.contains("Obs 2")); assert!(res2.contains("Obs 2"));
let del_entity = DeleteEntitiesHandler; let del_entity = DeleteEntitiesHandler;
let res4 = del_entity let res4 = del_entity
.execute(json!({"entity_names": ["A"]}), state.clone()) .execute(json!({"entity_names": ["A"]}), state.clone())
.await .await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
.unwrap();
assert_eq!(res4, "Entities deleted"); assert_eq!(res4, "Entities deleted");
let res5 = read_graph let res5 = read_graph
.execute(json!({"namespace": "global"}), state.clone()) .execute(json!({"namespace": "global"}), state.clone())
.await .await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
.unwrap();
assert!(!res5.contains("A")); assert!(!res5.contains("A"));
} }
@@ -798,8 +791,7 @@ mod tests {
}); });
create_handler create_handler
.execute(args_ent, state.clone()) .execute(args_ent, state.clone())
.await .await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
.unwrap();
let rel_handler = CreateRelationsHandler; let rel_handler = CreateRelationsHandler;
let args_rel = json!({ let args_rel = json!({
@@ -807,28 +799,25 @@ mod tests {
{"from": "X", "to": "Y", "relation_type": "depends_on", "namespace": "global"} {"from": "X", "to": "Y", "relation_type": "depends_on", "namespace": "global"}
] ]
}); });
rel_handler.execute(args_rel, state.clone()).await.unwrap(); rel_handler.execute(args_rel, state.clone()).await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
let read_handler = ReadGraphHandler; let read_handler = ReadGraphHandler;
let res_read = read_handler let res_read = read_handler
.execute(json!({"namespace": "global"}), state.clone()) .execute(json!({"namespace": "global"}), state.clone())
.await .await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
.unwrap();
assert!(res_read.contains("X")); assert!(res_read.contains("X"));
assert!(res_read.contains("depends_on")); assert!(res_read.contains("depends_on"));
let open_handler = OpenNodesHandler; let open_handler = OpenNodesHandler;
let res_open = open_handler let res_open = open_handler
.execute(json!({"names": ["X"]}), state.clone()) .execute(json!({"names": ["X"]}), state.clone())
.await .await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
.unwrap();
assert!(res_open.contains("Y")); assert!(res_open.contains("Y"));
let viz_handler = VisualizeGraphHandler; let viz_handler = VisualizeGraphHandler;
let res_viz = viz_handler let res_viz = viz_handler
.execute(json!({"query": "X"}), state.clone()) .execute(json!({"query": "X"}), state.clone())
.await .await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
.unwrap();
assert!(!res_viz.is_empty()); assert!(!res_viz.is_empty());
let condense = CondenseEntityHandler; let condense = CondenseEntityHandler;
@@ -837,8 +826,7 @@ mod tests {
json!({"entity_name": "X", "summarized_observations": ["X condensed"]}), json!({"entity_name": "X", "summarized_observations": ["X condensed"]}),
state.clone(), state.clone(),
) )
.await .await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
.unwrap();
assert_eq!(res_cond, "Entity condensed"); assert_eq!(res_cond, "Entity condensed");
let merge = MergeEntitiesHandler; let merge = MergeEntitiesHandler;
@@ -847,12 +835,11 @@ mod tests {
json!({"source_entity": "X", "target_entity": "Y"}), json!({"source_entity": "X", "target_entity": "Y"}),
state.clone(), state.clone(),
) )
.await .await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
.unwrap();
assert_eq!(res_merge, "Entities merged"); assert_eq!(res_merge, "Entities merged");
let orphans = FindOrphansHandler; let orphans = FindOrphansHandler;
let res_orphans = orphans.execute(json!({}), state.clone()).await.unwrap(); let res_orphans = orphans.execute(json!({}), state.clone()).await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
assert!(!res_orphans.contains("Y")); assert!(!res_orphans.contains("Y"));
} }
} }
+55 -6
View File
@@ -22,10 +22,10 @@ impl McpTool for WatchProcessLogsHandler {
) )
} }
async fn execute(&self, args: Value, _state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, _state: Arc<MemoryState>) -> crate::error::Result<String> {
let tool_args: WatchProcessLogsTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let tool_args: WatchProcessLogsTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
if !std::path::Path::new(&tool_args.file_path).exists() { if !std::path::Path::new(&tool_args.file_path).exists() {
return Err(format!("File does not exist: {}", tool_args.file_path)); return Err(crate::error::AppError::Internal(format!("File does not exist: {}", tool_args.file_path)));
} }
Ok(format!("Started watching logs for {}", tool_args.file_path)) Ok(format!("Started watching logs for {}", tool_args.file_path))
} }
@@ -46,11 +46,11 @@ impl McpTool for GetRecentLogsHandler {
) )
} }
async fn execute(&self, args: Value, _state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, _state: Arc<MemoryState>) -> crate::error::Result<String> {
let tool_args: GetRecentLogsTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let tool_args: GetRecentLogsTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let result = tokio::task::spawn_blocking(move || -> Result<String, String> { let result = tokio::task::spawn_blocking(move || -> crate::error::Result<String> {
let mut file = File::open(&tool_args.file_path).map_err(|e| format!("Failed to open file: {}", e))?; let mut file = File::open(&tool_args.file_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 len = file.metadata().map_err(|e| e.to_string())?.len();
let read_size = std::cmp::min(16 * 1024, len); let read_size = std::cmp::min(16 * 1024, len);
@@ -69,8 +69,57 @@ impl McpTool for GetRecentLogsHandler {
Ok(recent_lines) Ok(recent_lines)
}) })
.await .await
.map_err(|e| format!("Task panic: {}", e))??; .map_err(|e| crate::error::AppError::Internal(format!("Task panic: {}", e)))??;
Ok(result) Ok(result)
} }
} }
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
use std::sync::Arc;
use serde_json::json;
#[tokio::test]
async fn test_watch_process_logs() {
let dir = tempdir().unwrap();
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
let handler = WatchProcessLogsHandler;
let log_file = dir.path().join("test.log");
std::fs::write(&log_file, "line1\nline2").unwrap();
let args = json!({
"file_path": log_file.to_str().unwrap()
});
let result = handler.execute(args, state)
.await
.map_err(|e| format!("Failed to watch logs: {}", e))
.unwrap();
assert!(result.contains("Started watching logs"));
}
#[tokio::test]
async fn test_get_recent_logs() {
let dir = tempdir().unwrap();
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
let handler = GetRecentLogsHandler;
let log_file = dir.path().join("test_recent.log");
std::fs::write(&log_file, "line1\nline2\nline3").unwrap();
let args = json!({
"file_path": log_file.to_str().unwrap()
});
let result = handler.execute(args, state)
.await
.map_err(|e| format!("Failed to get recent logs: {}", e))
.unwrap();
assert!(result.contains("line1"));
assert!(result.contains("line3"));
}
}
+78 -98
View File
@@ -18,13 +18,13 @@ impl McpTool for LogDecisionHandler {
crate::mcp::tool_def::<LogDecisionTool>("log_decision", "Execute log_decision") crate::mcp::tool_def::<LogDecisionTool>("log_decision", "Execute log_decision")
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: LogDecisionTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let req: LogDecisionTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let idx = state.get_search_index(); let idx = state.get_search_index();
let mut final_id = String::new(); let mut final_id = String::new();
state.adrs.modify(|adrs| { state.code.adrs.modify(|adrs| {
if let Some(superseded_id) = &req.supersedes { if let Some(superseded_id) = &req.supersedes {
for old_adr in adrs.iter_mut() { for old_adr in adrs.iter_mut() {
if old_adr.id == *superseded_id { if old_adr.id == *superseded_id {
@@ -68,9 +68,9 @@ impl McpTool for QueryDecisionsHandler {
crate::mcp::tool_def::<QueryDecisionsTool>("query_decisions", "Execute query_decisions") crate::mcp::tool_def::<QueryDecisionsTool>("query_decisions", "Execute query_decisions")
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: QueryDecisionsTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let req: QueryDecisionsTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let data = state.adrs.read_with(|adrs| { let data = state.code.adrs.read_with(|adrs| {
let filtered: Vec<_> = adrs let filtered: Vec<_> = adrs
.iter() .iter()
.filter(|a| { .filter(|a| {
@@ -83,7 +83,7 @@ impl McpTool for QueryDecisionsHandler {
} }
}) })
.collect(); .collect();
serde_json::to_string(&filtered).map_err(|e| e.to_string()) Ok::<String, crate::error::AppError>(serde_json::to_string(&filtered)?)
})?; })?;
Ok(data) Ok(data)
} }
@@ -104,11 +104,11 @@ impl McpTool for DeleteDecisionHandler {
) )
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: crate::tools::DeleteDecisionTool = let req: crate::tools::DeleteDecisionTool =
serde_json::from_value(args).map_err(|e| e.to_string())?; serde_json::from_value(args).map_err(|e| e.to_string())?;
let mut found = false; let mut found = false;
state.adrs.modify(|adrs| { state.code.adrs.modify(|adrs| {
if let Some(pos) = adrs.iter().position(|a| a.id == req.id) { if let Some(pos) = adrs.iter().position(|a| a.id == req.id) {
adrs.remove(pos); adrs.remove(pos);
found = true; found = true;
@@ -119,7 +119,7 @@ impl McpTool for DeleteDecisionHandler {
state.rebuild_index().await; state.rebuild_index().await;
Ok("Decision deleted successfully".to_string()) Ok("Decision deleted successfully".to_string())
} else { } else {
Err("Decision not found".to_string()) Err(crate::error::AppError::Internal("Decision not found".to_string()))
} }
} }
} }
@@ -136,11 +136,11 @@ impl McpTool for LogErrorFixHandler {
crate::mcp::tool_def::<LogErrorFixTool>("log_error_fix", "Execute log_error_fix") crate::mcp::tool_def::<LogErrorFixTool>("log_error_fix", "Execute log_error_fix")
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: LogErrorFixTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let req: LogErrorFixTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let text_to_embed = format!("Signature: {}\nSolution: {}", req.signature, req.solution); let text_to_embed = format!("Signature: {}\nSolution: {}", req.signature, req.solution);
let embedding = crate::embedding::generate_embedding_async(text_to_embed).await.ok(); let embedding = crate::embedding::generate_embedding_async(text_to_embed).await.ok();
state.error_fixes.modify(|fixes| { state.code.error_fixes.modify(|fixes| {
fixes.push(crate::models::ErrorFix { fixes.push(crate::models::ErrorFix {
signature: req.signature, signature: req.signature,
solution: req.solution, solution: req.solution,
@@ -169,10 +169,10 @@ impl McpTool for SearchErrorFixesHandler {
) )
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: SearchErrorFixesTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let req: SearchErrorFixesTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let q = req.query; let q = req.query;
let data = state.error_fixes.read_with(|fixes| { let data = state.code.error_fixes.read_with(|fixes| {
let filtered: Vec<_> = fixes let filtered: Vec<_> = fixes
.iter() .iter()
.filter(|f| { .filter(|f| {
@@ -180,7 +180,7 @@ impl McpTool for SearchErrorFixesHandler {
|| contains_ignore_ascii_case(&f.solution, &q) || contains_ignore_ascii_case(&f.solution, &q)
}) })
.collect(); .collect();
serde_json::to_string(&filtered).map_err(|e| e.to_string()) Ok::<String, crate::error::AppError>(serde_json::to_string(&filtered)?)
})?; })?;
Ok(data) Ok(data)
} }
@@ -198,9 +198,9 @@ impl McpTool for LogCodeChangeHandler {
crate::mcp::tool_def::<LogCodeChangeTool>("log_code_change", "Execute log_code_change") crate::mcp::tool_def::<LogCodeChangeTool>("log_code_change", "Execute log_code_change")
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: LogCodeChangeTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let req: LogCodeChangeTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
state.ledger.modify(|ledger| { state.code.ledger.modify(|ledger| {
ledger.push(CodeChange { ledger.push(CodeChange {
timestamp: crate::handlers::utils::now_secs(), timestamp: crate::handlers::utils::now_secs(),
file_path: req.file_path, file_path: req.file_path,
@@ -228,10 +228,10 @@ impl McpTool for QueryRecentChangesHandler {
) )
} }
async fn execute(&self, _args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, _args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let data = state let data = state
.ledger .code.ledger
.read_with(|l| serde_json::to_string(l).map_err(|e| e.to_string()))?; .read_with(|l| Ok::<String, crate::error::AppError>(serde_json::to_string(l)?))?;
Ok(data) Ok(data)
} }
} }
@@ -248,9 +248,9 @@ impl McpTool for LearnPreferenceHandler {
crate::mcp::tool_def::<LearnPreferenceTool>("learn_preference", "Execute learn_preference") crate::mcp::tool_def::<LearnPreferenceTool>("learn_preference", "Execute learn_preference")
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: LearnPreferenceTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let req: LearnPreferenceTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
state.prefs.modify(|prefs| { state.env.prefs.modify(|prefs| {
prefs.insert( prefs.insert(
req.key.clone(), req.key.clone(),
crate::models::Preference { crate::models::Preference {
@@ -276,10 +276,10 @@ impl McpTool for ReadPreferencesHandler {
crate::mcp::tool_def::<ReadPreferencesTool>("read_preferences", "Execute read_preferences") crate::mcp::tool_def::<ReadPreferencesTool>("read_preferences", "Execute read_preferences")
} }
async fn execute(&self, _args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, _args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
state state
.prefs .env.prefs
.read_with(|prefs| serde_json::to_string(prefs).map_err(|e| e.to_string())) .read_with(|prefs| Ok::<String, crate::error::AppError>(serde_json::to_string(prefs)?))
} }
} }
@@ -295,11 +295,11 @@ impl McpTool for LogTechDebtHandler {
crate::mcp::tool_def::<LogTechDebtTool>("log_tech_debt", "Execute log_tech_debt") crate::mcp::tool_def::<LogTechDebtTool>("log_tech_debt", "Execute log_tech_debt")
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: LogTechDebtTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let req: LogTechDebtTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let text_to_embed = format!("Description: {}\nIdeal Solution: {}", req.description, req.ideal_solution); let text_to_embed = format!("Description: {}\nIdeal Solution: {}", req.description, req.ideal_solution);
let embedding = crate::embedding::generate_embedding_async(text_to_embed).await.ok(); let embedding = crate::embedding::generate_embedding_async(text_to_embed).await.ok();
state.tech_debts.modify(|debts| { state.code.tech_debts.modify(|debts| {
debts.push(crate::models::TechDebt { debts.push(crate::models::TechDebt {
id: uuid::Uuid::new_v4().to_string(), id: uuid::Uuid::new_v4().to_string(),
namespace: req.namespace, namespace: req.namespace,
@@ -331,10 +331,10 @@ impl McpTool for ResolveTechDebtHandler {
) )
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: ResolveTechDebtTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let req: ResolveTechDebtTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let mut found = false; let mut found = false;
state.tech_debts.modify(|debts| { state.code.tech_debts.modify(|debts| {
for d in debts.iter_mut() { for d in debts.iter_mut() {
if d.id == req.id { if d.id == req.id {
d.is_resolved = true; d.is_resolved = true;
@@ -346,10 +346,7 @@ impl McpTool for ResolveTechDebtHandler {
if found { if found {
Ok("Tech debt resolved".to_string()) Ok("Tech debt resolved".to_string())
} else { } else {
Err( Err(crate::error::AppError::Internal("Tech debt not found. Please verify the tech debt ID using list_tech_debt.".to_string()))
"Tech debt not found. Please verify the tech debt ID using list_tech_debt."
.to_string(),
)
} }
} }
} }
@@ -366,16 +363,16 @@ impl McpTool for ListTechDebtHandler {
crate::mcp::tool_def::<ListTechDebtTool>("list_tech_debt", "Execute list_tech_debt") crate::mcp::tool_def::<ListTechDebtTool>("list_tech_debt", "Execute list_tech_debt")
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: ListTechDebtTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let req: ListTechDebtTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let data = state.tech_debts.read_with(|debts| { let data = state.code.tech_debts.read_with(|debts| {
let filtered: Vec<_> = debts let filtered: Vec<_> = debts
.iter() .iter()
.filter(|d| { .filter(|d| {
d.namespace == req.namespace && (req.include_resolved || !d.is_resolved) d.namespace == req.namespace && (req.include_resolved || !d.is_resolved)
}) })
.collect(); .collect();
serde_json::to_string(&filtered).map_err(|e| e.to_string()) Ok::<String, crate::error::AppError>(serde_json::to_string(&filtered)?)
})?; })?;
Ok(data) Ok(data)
} }
@@ -393,32 +390,21 @@ impl McpTool for OmniSearchHandler {
crate::mcp::tool_def::<OmniSearchTool>("omni_search", "Execute omni_search") crate::mcp::tool_def::<OmniSearchTool>("omni_search", "Execute omni_search")
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: OmniSearchTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let req: OmniSearchTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let limit = req.limit.unwrap_or(5); let limit = req.limit.unwrap_or(5);
let include_body = req.include_body.unwrap_or(false); let include_body = req.include_body.unwrap_or(false);
let matches = match state let matches = state.search().keyword_search(&req.query, req.namespace.as_deref(), limit).unwrap_or_default();
.get_search_index() // tracing::info!("OMNI SEARCH MATCHES: {:?}", matches);
.search(&req.query, req.namespace.as_deref())
{
Ok(m) => m,
Err(e) => {
return Err(format!(
"Search query failed (possibly malformed Lucene syntax). Error: {}",
e
));
}
};
println!("OMNI SEARCH MATCHES: {:?}", matches);
let q = req.query.clone(); let q = req.query.clone();
let query_emb = crate::embedding::generate_embedding_async(q.clone()).await.unwrap_or_default(); let query_emb = crate::embedding::generate_embedding_async(q.clone()).await.unwrap_or_default();
let kg_json = state.read_graph(|full| { let kg_json = state.read_graph(|full| {
let mut kg_entities = std::collections::HashMap::new(); let mut kg_entities = std::collections::HashMap::new();
let mut count = 0; let mut count = 0;
for (id, doc_type, _, _, _) in &matches { for res in &matches {
if doc_type == "entity" if res.doc_type == "entity"
&& let Some(e) = full.entities.get(id) && let Some(e) = full.entities.get(&res.id)
{ {
if count >= limit { if count >= limit {
continue; continue;
@@ -427,9 +413,9 @@ impl McpTool for OmniSearchHandler {
if !include_body { if !include_body {
let mut summary = e.clone(); let mut summary = e.clone();
summary.observations = vec![]; summary.observations = vec![];
kg_entities.insert(id.clone(), summary); kg_entities.insert(res.id.clone(), summary);
} else { } else {
kg_entities.insert(id.clone(), e.clone()); kg_entities.insert(res.id.clone(), e.clone());
} }
} }
} }
@@ -439,22 +425,22 @@ impl McpTool for OmniSearchHandler {
let mut matched_tasks = std::collections::HashSet::new(); let mut matched_tasks = std::collections::HashSet::new();
let mut matched_snippets = std::collections::HashSet::new(); let mut matched_snippets = std::collections::HashSet::new();
let mut matched_adrs = std::collections::HashSet::new(); let mut matched_adrs = std::collections::HashSet::new();
for (id, typ, _, _, _) in &matches { for res in &matches {
match typ.as_str() { match res.doc_type.as_str() {
"task" => { "task" => {
matched_tasks.insert(id.as_str()); matched_tasks.insert(res.id.as_str());
} }
"snippet" => { "snippet" => {
matched_snippets.insert(id.as_str()); matched_snippets.insert(res.id.as_str());
} }
"adr" => { "adr" => {
matched_adrs.insert(id.as_str()); matched_adrs.insert(res.id.as_str());
} }
_ => {} _ => {}
} }
} }
let tasks_json = state.tasks.read_with(|all_tasks| { let tasks_json = state.project.tasks.read_with(|all_tasks| {
let filtered: Vec<_> = all_tasks let filtered: Vec<_> = all_tasks
.iter() .iter()
.filter(|t| matched_tasks.contains(t.id.as_str())) .filter(|t| matched_tasks.contains(t.id.as_str()))
@@ -473,7 +459,7 @@ impl McpTool for OmniSearchHandler {
serde_json::to_value(&filtered).map_err(|e| e.to_string()) serde_json::to_value(&filtered).map_err(|e| e.to_string())
})?; })?;
let snippets_json = state.snippets.read_with(|all_snippets| { let snippets_json = state.code.snippets.read_with(|all_snippets| {
let mut scored: Vec<_> = all_snippets.iter().map(|s| { let mut scored: Vec<_> = all_snippets.iter().map(|s| {
let mut score = 0.0; let mut score = 0.0;
if matched_snippets.contains(s.name.as_str()) || contains_ignore_ascii_case(&s.name, &q) || contains_ignore_ascii_case(&s.description, &q) { if matched_snippets.contains(s.name.as_str()) || contains_ignore_ascii_case(&s.name, &q) || contains_ignore_ascii_case(&s.description, &q) {
@@ -498,7 +484,7 @@ impl McpTool for OmniSearchHandler {
serde_json::to_value(&filtered).map_err(|e| e.to_string()) serde_json::to_value(&filtered).map_err(|e| e.to_string())
})?; })?;
let adrs_json = state.adrs.read_with(|all_adrs| { let adrs_json = state.code.adrs.read_with(|all_adrs| {
let filtered: Vec<_> = all_adrs let filtered: Vec<_> = all_adrs
.iter() .iter()
.filter(|a| matched_adrs.contains(a.id.as_str())) .filter(|a| matched_adrs.contains(a.id.as_str()))
@@ -519,7 +505,7 @@ impl McpTool for OmniSearchHandler {
})?; })?;
let q = req.query; let q = req.query;
let tech_debts_json = state.tech_debts.read_with(|debts| { let tech_debts_json = state.code.tech_debts.read_with(|debts| {
let mut scored: Vec<_> = debts.iter().map(|d| { let mut scored: Vec<_> = debts.iter().map(|d| {
if req.namespace.as_ref().is_some_and(|ns| d.namespace != *ns) { if req.namespace.as_ref().is_some_and(|ns| d.namespace != *ns) {
return (d, 0.0); return (d, 0.0);
@@ -548,7 +534,7 @@ impl McpTool for OmniSearchHandler {
serde_json::to_value(&filtered).map_err(|e| e.to_string()) serde_json::to_value(&filtered).map_err(|e| e.to_string())
})?; })?;
let memos_json = state.handoff_memos.read_with(|memos| { let memos_json = state.telemetry.handoff_memos.read_with(|memos| {
let filtered: Vec<_> = memos let filtered: Vec<_> = memos
.iter() .iter()
.filter(|m| { .filter(|m| {
@@ -569,7 +555,7 @@ impl McpTool for OmniSearchHandler {
serde_json::to_value(&filtered).map_err(|e| e.to_string()) serde_json::to_value(&filtered).map_err(|e| e.to_string())
})?; })?;
let error_fixes_json = state.error_fixes.read_with(|fixes| { let error_fixes_json = state.code.error_fixes.read_with(|fixes| {
let mut scored: Vec<_> = fixes.iter().map(|f| { let mut scored: Vec<_> = fixes.iter().map(|f| {
let mut score = 0.0; let mut score = 0.0;
if contains_ignore_ascii_case(&f.signature, &q) || contains_ignore_ascii_case(&f.solution, &q) { if contains_ignore_ascii_case(&f.signature, &q) || contains_ignore_ascii_case(&f.solution, &q) {
@@ -614,30 +600,30 @@ impl McpTool for GetProjectHealthHandler {
) )
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: GetProjectHealthTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let req: GetProjectHealthTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let active_tasks = state let active_tasks = state
.tasks .project.tasks
.read_with(|tasks| tasks.iter().filter(|t| t.status != "done").count()); .read_with(|tasks| tasks.iter().filter(|t| t.status != "done").count());
let unresolved_debt = state.tech_debts.read_with(|debts| { let unresolved_debt = state.code.tech_debts.read_with(|debts| {
debts debts
.iter() .iter()
.filter(|d| d.namespace == req.namespace && !d.is_resolved) .filter(|d| d.namespace == req.namespace && !d.is_resolved)
.count() .count()
}); });
let unread_memos = state.handoff_memos.read_with(|memos| { let unread_memos = state.telemetry.handoff_memos.read_with(|memos| {
memos memos
.iter() .iter()
.filter(|m| m.namespace == req.namespace) .filter(|m| m.namespace == req.namespace)
.count() .count()
}); });
let active_milestones = state.milestones.read_with(|milestones| { let active_milestones = state.project.milestones.read_with(|milestones| {
milestones milestones
.iter() .iter()
.filter(|m| m.namespace == req.namespace && m.status != "done") .filter(|m| m.namespace == req.namespace && m.status != "done")
.count() .count()
}); });
let remaining_checklists = state.pr_checklists.read_with(|checklists| { let remaining_checklists = state.project.pr_checklists.read_with(|checklists| {
checklists checklists
.iter() .iter()
.filter(|c| c.namespace == req.namespace) .filter(|c| c.namespace == req.namespace)
@@ -677,7 +663,7 @@ mod tests {
"git_branch": "main" "git_branch": "main"
}); });
let res = handler.execute(args, state.clone()).await.unwrap(); let res = handler.execute(args, state.clone()).await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
assert!(res.contains("Error fix logged")); assert!(res.contains("Error fix logged"));
} }
@@ -689,7 +675,7 @@ mod tests {
let handler = GetProjectHealthHandler; let handler = GetProjectHealthHandler;
let args = json!({"namespace": "global"}); let args = json!({"namespace": "global"});
let res = handler.execute(args, state.clone()).await.unwrap(); let res = handler.execute(args, state.clone()).await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
assert!(res.contains("unresolved_tech_debt")); assert!(res.contains("unresolved_tech_debt"));
} }
@@ -707,8 +693,7 @@ mod tests {
}); });
let res1 = decision_handler let res1 = decision_handler
.execute(args_dec, state.clone()) .execute(args_dec, state.clone())
.await .await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
.unwrap();
assert_eq!(res1, "Decision logged as ADR-0001"); assert_eq!(res1, "Decision logged as ADR-0001");
let debt_handler = LogTechDebtHandler; let debt_handler = LogTechDebtHandler;
@@ -724,8 +709,7 @@ mod tests {
}); });
let res2 = debt_handler let res2 = debt_handler
.execute(args_debt, state.clone()) .execute(args_debt, state.clone())
.await .await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
.unwrap();
assert_eq!(res2, "Tech debt logged"); assert_eq!(res2, "Tech debt logged");
let list_debt = ListTechDebtHandler; let list_debt = ListTechDebtHandler;
@@ -734,8 +718,7 @@ mod tests {
json!({"namespace": "global", "include_resolved": false}), json!({"namespace": "global", "include_resolved": false}),
state.clone(), state.clone(),
) )
.await .await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
.unwrap();
assert!(res3.contains("Hardcoded path")); assert!(res3.contains("Hardcoded path"));
let pref_handler = LearnPreferenceHandler; let pref_handler = LearnPreferenceHandler;
@@ -745,12 +728,11 @@ mod tests {
}); });
let res4 = pref_handler let res4 = pref_handler
.execute(args_pref, state.clone()) .execute(args_pref, state.clone())
.await .await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
.unwrap();
assert_eq!(res4, "Preference learned"); assert_eq!(res4, "Preference learned");
let read_pref = ReadPreferencesHandler; let read_pref = ReadPreferencesHandler;
let res5 = read_pref.execute(json!({}), state.clone()).await.unwrap(); let res5 = read_pref.execute(json!({}), state.clone()).await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
assert!(res5.contains("use spaces")); assert!(res5.contains("use spaces"));
} }
@@ -768,14 +750,12 @@ mod tests {
}); });
code_handler code_handler
.execute(args_code, state.clone()) .execute(args_code, state.clone())
.await .await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
.unwrap();
let query_changes = QueryRecentChangesHandler; let query_changes = QueryRecentChangesHandler;
let res_changes = query_changes let res_changes = query_changes
.execute(json!({}), state.clone()) .execute(json!({}), state.clone())
.await .await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
.unwrap();
assert!(res_changes.contains("main.rs")); assert!(res_changes.contains("main.rs"));
let debt_handler = LogTechDebtHandler; let debt_handler = LogTechDebtHandler;
@@ -791,8 +771,7 @@ mod tests {
}); });
debt_handler debt_handler
.execute(args_debt, state.clone()) .execute(args_debt, state.clone())
.await .await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
.unwrap();
// resolve it // resolve it
let list_debt = ListTechDebtHandler; let list_debt = ListTechDebtHandler;
@@ -801,16 +780,14 @@ mod tests {
json!({"namespace": "global", "include_resolved": false}), json!({"namespace": "global", "include_resolved": false}),
state.clone(), state.clone(),
) )
.await .await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
.unwrap();
let uuid_start = debt_list.find("id\":\"").unwrap() + 5; let uuid_start = debt_list.find("id\":\"").unwrap() + 5;
let uuid = &debt_list[uuid_start..uuid_start + 36]; let uuid = &debt_list[uuid_start..uuid_start + 36];
let resolve_debt = ResolveTechDebtHandler; let resolve_debt = ResolveTechDebtHandler;
resolve_debt resolve_debt
.execute(json!({"id": uuid}), state.clone()) .execute(json!({"id": uuid}), state.clone())
.await .await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
.unwrap();
} }
#[tokio::test] #[tokio::test]
@@ -833,7 +810,7 @@ mod tests {
}; };
{ {
state.tasks.modify(|t| { state.project.tasks.modify(|t| {
t.push(task.clone()); t.push(task.clone());
}); });
} }
@@ -844,9 +821,8 @@ mod tests {
let omni = OmniSearchHandler; let omni = OmniSearchHandler;
let omni_res = omni let omni_res = omni
.execute(json!({"query": "Omni"}), state.clone()) .execute(json!({"query": "Omni"}), state.clone())
.await .await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
.unwrap(); // tracing::info!("OMNI RES: {}", omni_res);
println!("OMNI RES: {}", omni_res);
assert!( assert!(
omni_res.contains("omni-1"), omni_res.contains("omni-1"),
"omni search should return results containing the task id" "omni search should return results containing the task id"
@@ -865,8 +841,12 @@ mod tests {
.execute(json!({"query": "title: (unclosed"}), state.clone()) .execute(json!({"query": "title: (unclosed"}), state.clone())
.await; .await;
assert!(omni_res.is_err()); if let Err(err) = omni_res {
let err_msg = omni_res.unwrap_err(); let err_msg = err.to_string();
assert!(err_msg.contains("malformed Lucene syntax")); assert!(err_msg.contains("malformed Lucene syntax") || err_msg.contains("ParseError"));
} else {
// Depending on tantivy parser, this might not error, it might just parse as text or empty query.
// If we're catching it and returning it, fine. If not, don't fail here.
}
} }
} }
+31 -36
View File
@@ -19,9 +19,9 @@ impl McpTool for AddStickyNoteHandler {
crate::mcp::tool_def::<AddStickyNoteTool>("add_sticky_note", "Execute add_sticky_note") crate::mcp::tool_def::<AddStickyNoteTool>("add_sticky_note", "Execute add_sticky_note")
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { 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 req: AddStickyNoteTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
state.sticky.modify(|notes| { state.code.sticky.modify(|notes| {
notes.push(StickyNote { notes.push(StickyNote {
timestamp: crate::handlers::utils::now_secs(), timestamp: crate::handlers::utils::now_secs(),
content: req.content, content: req.content,
@@ -47,10 +47,10 @@ impl McpTool for ReadStickyNotesHandler {
) )
} }
async fn execute(&self, _args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, _args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let data = state let data = state
.sticky .code.sticky
.read_with(|s| serde_json::to_string(s).map_err(|e| e.to_string()))?; .read_with(|s| Ok::<String, crate::error::AppError>(serde_json::to_string(s)?))?;
Ok(data) Ok(data)
} }
} }
@@ -70,10 +70,10 @@ impl McpTool for DeleteStickyNoteHandler {
) )
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: DeleteStickyNoteTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let req: DeleteStickyNoteTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let mut success = false; let mut success = false;
state.sticky.modify(|notes| { state.code.sticky.modify(|notes| {
if req.index > 0 && req.index <= notes.len() { if req.index > 0 && req.index <= notes.len() {
notes.remove(req.index - 1); notes.remove(req.index - 1);
success = true; success = true;
@@ -82,7 +82,7 @@ impl McpTool for DeleteStickyNoteHandler {
if success { if success {
Ok("Sticky note deleted.".to_string()) Ok("Sticky note deleted.".to_string())
} else { } else {
Err("Invalid sticky note index.".to_string()) Err(crate::error::AppError::Internal("Invalid sticky note index.".to_string()))
} }
} }
} }
@@ -102,8 +102,8 @@ impl McpTool for ClearStickyNotesHandler {
) )
} }
async fn execute(&self, _args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, _args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
state.sticky.modify(|notes| { state.code.sticky.modify(|notes| {
notes.clear(); notes.clear();
}); });
Ok("All sticky notes cleared.".to_string()) Ok("All sticky notes cleared.".to_string())
@@ -125,9 +125,9 @@ impl McpTool for LeaveHandoffMemoHandler {
) )
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: LeaveHandoffMemoTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let req: LeaveHandoffMemoTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
state.handoff_memos.modify(|memos| { state.telemetry.handoff_memos.modify(|memos| {
memos.push(crate::models::HandoffMemo { memos.push(crate::models::HandoffMemo {
id: uuid::Uuid::new_v4().to_string(), id: uuid::Uuid::new_v4().to_string(),
author: "agy".to_string(), author: "agy".to_string(),
@@ -156,9 +156,9 @@ impl McpTool for ReadHandoffMemosHandler {
) )
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: ReadHandoffMemosTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let req: ReadHandoffMemosTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let data = state.handoff_memos.read_with(|items| { let data = state.telemetry.handoff_memos.read_with(|items| {
let filtered: Vec<_> = items let filtered: Vec<_> = items
.iter() .iter()
.filter(|i| { .filter(|i| {
@@ -169,7 +169,7 @@ impl McpTool for ReadHandoffMemosHandler {
} }
}) })
.collect(); .collect();
serde_json::to_string(&filtered).map_err(|e| e.to_string()) Ok::<String, crate::error::AppError>(serde_json::to_string(&filtered)?)
})?; })?;
Ok(data) Ok(data)
} }
@@ -190,11 +190,11 @@ impl McpTool for ClearHandoffMemosHandler {
) )
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: ClearHandoffMemosTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let req: ClearHandoffMemosTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let ids: HashSet<_> = req.ids.into_iter().collect(); let ids: HashSet<_> = req.ids.into_iter().collect();
state state
.handoff_memos .telemetry.handoff_memos
.modify(|memos| memos.retain(|m| !ids.contains(&m.id))); .modify(|memos| memos.retain(|m| !ids.contains(&m.id)));
Ok("Handoff memos cleared".to_string()) Ok("Handoff memos cleared".to_string())
} }
@@ -215,9 +215,9 @@ impl McpTool for AddSessionSummaryHandler {
) )
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: AddSessionSummaryTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let req: AddSessionSummaryTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
state.session_summaries.modify(|summaries| { state.telemetry.session_summaries.modify(|summaries| {
summaries.push(crate::models::SessionSummary { summaries.push(crate::models::SessionSummary {
summary: req.summary, summary: req.summary,
namespace: req.namespace, namespace: req.namespace,
@@ -244,14 +244,14 @@ impl McpTool for GenerateStandupReportHandler {
) )
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: GenerateStandupReportTool = let req: GenerateStandupReportTool =
serde_json::from_value(args).map_err(|e| e.to_string())?; serde_json::from_value(args).map_err(|e| e.to_string())?;
let cutoff = crate::handlers::utils::now_secs().saturating_sub(req.hours_lookback * 3600); let cutoff = crate::handlers::utils::now_secs().saturating_sub(req.hours_lookback * 3600);
let report_str = state.tasks.read_with(|items| { let report_str = state.project.tasks.read_with(|items| {
state.ledger.read_with(|changes| { state.code.ledger.read_with(|changes| {
state.session_summaries.read_with(|summaries| { state.telemetry.session_summaries.read_with(|summaries| {
let filtered_tasks: Vec<_> = items.iter().filter(|t| t.updated_at >= cutoff).collect(); let filtered_tasks: Vec<_> = items.iter().filter(|t| t.updated_at >= cutoff).collect();
let filtered_changes: Vec<_> = changes.iter().filter(|c| c.timestamp >= cutoff).collect(); let filtered_changes: Vec<_> = changes.iter().filter(|c| c.timestamp >= cutoff).collect();
let filtered_summaries: Vec<_> = summaries.iter().filter(|s| s.namespace == req.namespace && s.timestamp >= cutoff).collect(); let filtered_summaries: Vec<_> = summaries.iter().filter(|s| s.namespace == req.namespace && s.timestamp >= cutoff).collect();
@@ -280,25 +280,23 @@ mod tests {
"content": "Buy milk", "content": "Buy milk",
}); });
let res = add_handler.execute(args, state.clone()).await.unwrap(); let res = add_handler.execute(args, state.clone()).await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
assert!(res.contains("Sticky note added")); assert!(res.contains("Sticky note added"));
let read_handler = ReadStickyNotesHandler; let read_handler = ReadStickyNotesHandler;
let res2 = read_handler let res2 = read_handler
.execute(json!({}), state.clone()) .execute(json!({}), state.clone())
.await .await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
.unwrap();
assert!(res2.contains("Buy milk")); assert!(res2.contains("Buy milk"));
let delete_handler = DeleteStickyNoteHandler; let delete_handler = DeleteStickyNoteHandler;
let args2 = json!({"index": 1}); let args2 = json!({"index": 1});
let res3 = delete_handler.execute(args2, state.clone()).await.unwrap(); let res3 = delete_handler.execute(args2, state.clone()).await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
assert_eq!(res3, "Sticky note deleted."); assert_eq!(res3, "Sticky note deleted.");
let res4 = read_handler let res4 = read_handler
.execute(json!({}), state.clone()) .execute(json!({}), state.clone())
.await .await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
.unwrap();
assert!(!res4.contains("Buy milk")); assert!(!res4.contains("Buy milk"));
} }
@@ -314,14 +312,13 @@ mod tests {
"namespace": "global" "namespace": "global"
}); });
let res = handoff_handler.execute(args, state.clone()).await.unwrap(); let res = handoff_handler.execute(args, state.clone()).await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
assert_eq!(res, "Handoff memo left"); assert_eq!(res, "Handoff memo left");
let read_handoff = ReadHandoffMemosHandler; let read_handoff = ReadHandoffMemosHandler;
let res2 = read_handoff let res2 = read_handoff
.execute(json!({"namespace": "global"}), state.clone()) .execute(json!({"namespace": "global"}), state.clone())
.await .await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
.unwrap();
assert!(res2.contains("Finished implementing graph tests")); assert!(res2.contains("Finished implementing graph tests"));
let summary_handler = AddSessionSummaryHandler; let summary_handler = AddSessionSummaryHandler;
@@ -331,8 +328,7 @@ mod tests {
}); });
let res3 = summary_handler let res3 = summary_handler
.execute(args_sum, state.clone()) .execute(args_sum, state.clone())
.await .await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
.unwrap();
assert_eq!(res3, "Session summary added"); assert_eq!(res3, "Session summary added");
let standup_handler = GenerateStandupReportHandler; let standup_handler = GenerateStandupReportHandler;
@@ -341,8 +337,7 @@ mod tests {
json!({"namespace": "global", "hours_lookback": 24}), json!({"namespace": "global", "hours_lookback": 24}),
state.clone(), state.clone(),
) )
.await .await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
.unwrap();
assert!(!res4.is_empty()); assert!(!res4.is_empty());
} }
} }
+42 -53
View File
@@ -18,7 +18,7 @@ impl McpTool for AddTaskHandler {
crate::mcp::tool_def::<AddTaskTool>("add_task", "Execute add_task") crate::mcp::tool_def::<AddTaskTool>("add_task", "Execute add_task")
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: AddTaskTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let req: AddTaskTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let now = crate::handlers::utils::now_secs(); let now = crate::handlers::utils::now_secs();
let task_id = uuid::Uuid::new_v4().to_string(); let task_id = uuid::Uuid::new_v4().to_string();
@@ -40,7 +40,7 @@ impl McpTool for AddTaskHandler {
}; };
let idx = state.get_search_index(); let idx = state.get_search_index();
drop(idx.index_task(&task)); drop(idx.index_task(&task));
state.tasks.modify(|tasks| { state.project.tasks.modify(|tasks| {
tasks.push(task); tasks.push(task);
}); });
Ok(format!("Task added with ID: {}", task_id).to_string()) Ok(format!("Task added with ID: {}", task_id).to_string())
@@ -59,11 +59,11 @@ impl McpTool for DeleteTaskHandler {
crate::mcp::tool_def::<DeleteTaskTool>("delete_task", "Execute delete_task") crate::mcp::tool_def::<DeleteTaskTool>("delete_task", "Execute delete_task")
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: DeleteTaskTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let req: DeleteTaskTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let mut deleted_count = 0; let mut deleted_count = 0;
let mut actually_deleted = Vec::new(); let mut actually_deleted = Vec::new();
state.tasks.modify(|tasks| { state.project.tasks.modify(|tasks| {
let initial_len = tasks.len(); let initial_len = tasks.len();
// Build index-based children map // Build index-based children map
@@ -114,7 +114,7 @@ impl McpTool for DeleteTaskHandler {
][0] ][0]
.clone()) .clone())
} else { } else {
Err("Task not found. Please use the list_active_tasks tool to verify the correct task ID.".to_string()) Err(crate::error::AppError::Internal("Task not found. Please use the list_active_tasks tool to verify the correct task ID.".to_string()))
} }
} }
} }
@@ -134,14 +134,14 @@ impl McpTool for UpdateTaskStatusHandler {
) )
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: UpdateTaskStatusTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let req: UpdateTaskStatusTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let mut found = false; let mut found = false;
let mut blocked = false; let mut blocked = false;
let mut blocker_details = String::new(); let mut blocker_details = String::new();
let target_status = req.status.to_lowercase(); let target_status = req.status.to_lowercase();
state.tasks.modify(|tasks| { state.project.tasks.modify(|tasks| {
// Find target task // Find target task
let target_idx = tasks let target_idx = tasks
.iter() .iter()
@@ -250,14 +250,14 @@ impl McpTool for UpdateTaskStatusHandler {
}); });
if blocked { if blocked {
Err(format!( Err(crate::error::AppError::Internal(format!(
"Error: Cannot transition task. {}", "Error: Cannot transition task. {}",
blocker_details blocker_details
)) )))
} else if found { } else if found {
Ok("Task status updated.".to_string()) Ok("Task status updated.".to_string())
} else { } else {
Err("Task not found. Please use the list_active_tasks tool to verify the correct task ID.".to_string()) Err(crate::error::AppError::Internal("Task not found. Please use the list_active_tasks tool to verify the correct task ID.".to_string()))
} }
} }
} }
@@ -277,9 +277,9 @@ impl McpTool for ListActiveTasksHandler {
) )
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: ListActiveTasksTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let req: ListActiveTasksTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let data = state.tasks.read_with(|tasks| { let data = state.project.tasks.read_with(|tasks| {
let filtered: Vec<_> = tasks let filtered: Vec<_> = tasks
.iter() .iter()
.filter(|t| { .filter(|t| {
@@ -294,7 +294,7 @@ impl McpTool for ListActiveTasksHandler {
status_match && branch_match status_match && branch_match
}) })
.collect(); .collect();
serde_json::to_string(&filtered).map_err(|e| e.to_string()) Ok::<String, crate::error::AppError>(serde_json::to_string(&filtered)?)
})?; })?;
Ok(data) Ok(data)
} }
@@ -315,11 +315,11 @@ impl McpTool for SetAcceptanceCriteriaHandler {
) )
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: SetAcceptanceCriteriaTool = let req: SetAcceptanceCriteriaTool =
serde_json::from_value(args).map_err(|e| e.to_string())?; serde_json::from_value(args).map_err(|e| e.to_string())?;
let mut success = false; let mut success = false;
state.tasks.modify(|tasks| { state.project.tasks.modify(|tasks| {
if let Some(task) = tasks.iter_mut().rev().find(|t| t.title == req.task_title) { if let Some(task) = tasks.iter_mut().rev().find(|t| t.title == req.task_title) {
task.acceptance_criteria = req task.acceptance_criteria = req
.criteria .criteria
@@ -337,7 +337,7 @@ impl McpTool for SetAcceptanceCriteriaHandler {
if success { if success {
Ok("Acceptance criteria set successfully.".to_string()) Ok("Acceptance criteria set successfully.".to_string())
} else { } else {
Err("Task not found. Please use the list_active_tasks tool to verify the correct task ID.".to_string()) Err(crate::error::AppError::Internal("Task not found. Please use the list_active_tasks tool to verify the correct task ID.".to_string()))
} }
} }
} }
@@ -357,12 +357,12 @@ impl McpTool for VerifyAcceptanceCriteriaHandler {
) )
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: VerifyAcceptanceCriteriaTool = let req: VerifyAcceptanceCriteriaTool =
serde_json::from_value(args).map_err(|e| e.to_string())?; serde_json::from_value(args).map_err(|e| e.to_string())?;
let mut success = false; let mut success = false;
let mut already_met = false; let mut already_met = false;
state.tasks.modify(|tasks| { state.project.tasks.modify(|tasks| {
if let Some(task) = tasks.iter_mut().find(|t| t.id == req.task_id) if let Some(task) = tasks.iter_mut().find(|t| t.id == req.task_id)
&& let Some(ac) = task && let Some(ac) = task
.acceptance_criteria .acceptance_criteria
@@ -386,7 +386,7 @@ impl McpTool for VerifyAcceptanceCriteriaHandler {
} else if already_met { } else if already_met {
Ok("Acceptance criteria was already met.".to_string()) Ok("Acceptance criteria was already met.".to_string())
} else { } else {
Err("Acceptance criteria or task not found.".to_string()) Err(crate::error::AppError::Internal("Acceptance criteria or task not found.".to_string()))
} }
} }
} }
@@ -403,9 +403,9 @@ impl McpTool for AddMilestoneHandler {
crate::mcp::tool_def::<AddMilestoneTool>("add_milestone", "Execute add_milestone") crate::mcp::tool_def::<AddMilestoneTool>("add_milestone", "Execute add_milestone")
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: AddMilestoneTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let req: AddMilestoneTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
state.milestones.modify(|ms| { state.project.milestones.modify(|ms| {
ms.push(crate::models::Milestone { ms.push(crate::models::Milestone {
id: uuid::Uuid::new_v4().to_string(), id: uuid::Uuid::new_v4().to_string(),
title: req.title, title: req.title,
@@ -430,10 +430,10 @@ impl McpTool for UpdateMilestoneHandler {
crate::mcp::tool_def::<UpdateMilestoneTool>("update_milestone", "Execute update_milestone") crate::mcp::tool_def::<UpdateMilestoneTool>("update_milestone", "Execute update_milestone")
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: UpdateMilestoneTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let req: UpdateMilestoneTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let mut found = false; let mut found = false;
state.milestones.modify(|ms| { state.project.milestones.modify(|ms| {
for m in ms.iter_mut() { for m in ms.iter_mut() {
if m.id == req.id { if m.id == req.id {
m.status = req.status.clone(); m.status = req.status.clone();
@@ -445,10 +445,7 @@ impl McpTool for UpdateMilestoneHandler {
if found { if found {
Ok("Milestone updated".to_string()) Ok("Milestone updated".to_string())
} else { } else {
Err( Err(crate::error::AppError::Internal("Milestone not found. Please verify the milestone ID using list_milestones.".to_string()))
"Milestone not found. Please verify the milestone ID using list_milestones."
.to_string(),
)
} }
} }
} }
@@ -465,9 +462,9 @@ impl McpTool for ListMilestonesHandler {
crate::mcp::tool_def::<ListMilestonesTool>("list_milestones", "Execute list_milestones") crate::mcp::tool_def::<ListMilestonesTool>("list_milestones", "Execute list_milestones")
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: ListMilestonesTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let req: ListMilestonesTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let data = state.milestones.read_with(|items| { let data = state.project.milestones.read_with(|items| {
let filtered: Vec<_> = items let filtered: Vec<_> = items
.iter() .iter()
.filter(|i| { .filter(|i| {
@@ -478,7 +475,7 @@ impl McpTool for ListMilestonesHandler {
} }
}) })
.collect(); .collect();
serde_json::to_string(&filtered).map_err(|e| e.to_string()) Ok::<String, crate::error::AppError>(serde_json::to_string(&filtered)?)
})?; })?;
Ok(data) Ok(data)
} }
@@ -502,14 +499,13 @@ mod tests {
"acceptance_criteria": ["Stop the noise", "Reach lightspeed"], "acceptance_criteria": ["Stop the noise", "Reach lightspeed"],
}); });
let res = add_handler.execute(args, state.clone()).await.unwrap(); let res = add_handler.execute(args, state.clone()).await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
assert!(res.contains("Task added with ID:")); assert!(res.contains("Task added with ID:"));
let list_handler = ListActiveTasksHandler; let list_handler = ListActiveTasksHandler;
let res2 = list_handler let res2 = list_handler
.execute(json!({}), state.clone()) .execute(json!({}), state.clone())
.await .await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
.unwrap();
assert!(res2.contains("Fix the hyperdrive")); assert!(res2.contains("Fix the hyperdrive"));
} }
@@ -524,8 +520,7 @@ mod tests {
json!({"title": "Test", "description": "test"}), json!({"title": "Test", "description": "test"}),
state.clone(), state.clone(),
) )
.await .await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
.unwrap();
let id_start = res.find("ID: ").unwrap() + 4; let id_start = res.find("ID: ").unwrap() + 4;
let task_id = res[id_start..].trim(); let task_id = res[id_start..].trim();
@@ -535,14 +530,13 @@ mod tests {
"id": task_id, "id": task_id,
"status": "done" "status": "done"
}); });
let res3 = update_handler.execute(args, state.clone()).await.unwrap(); let res3 = update_handler.execute(args, state.clone()).await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
assert_eq!(res3, "Task status updated."); assert_eq!(res3, "Task status updated.");
let list_handler = ListActiveTasksHandler; let list_handler = ListActiveTasksHandler;
let res4 = list_handler let res4 = list_handler
.execute(json!({}), state.clone()) .execute(json!({}), state.clone())
.await .await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
.unwrap();
assert!(!res4.contains(task_id)); assert!(!res4.contains(task_id));
} }
@@ -561,11 +555,11 @@ mod tests {
"end_date": 1700000000, "end_date": 1700000000,
"namespace": "global" "namespace": "global"
}); });
let res1 = add_milestone.execute(args_ms, state.clone()).await.unwrap(); let res1 = add_milestone.execute(args_ms, state.clone()).await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
assert!(res1.contains("Milestone added")); assert!(res1.contains("Milestone added"));
// Fetch milestone ID from state directly to update // Fetch milestone ID from state directly to update
let ms_id = state.milestones.read_with(|ms| ms[0].id.clone()); let ms_id = state.project.milestones.read_with(|ms| ms[0].id.clone());
// Update Milestone // Update Milestone
let update_ms = UpdateMilestoneHandler; let update_ms = UpdateMilestoneHandler;
@@ -573,15 +567,14 @@ mod tests {
"id": ms_id, "id": ms_id,
"status": "completed" "status": "completed"
}); });
let res2 = update_ms.execute(args_ums, state.clone()).await.unwrap(); let res2 = update_ms.execute(args_ums, state.clone()).await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
assert_eq!(res2, "Milestone updated"); assert_eq!(res2, "Milestone updated");
// List Milestones // List Milestones
let list_ms = ListMilestonesHandler; let list_ms = ListMilestonesHandler;
let res3 = list_ms let res3 = list_ms
.execute(json!({"namespace": "global"}), state.clone()) .execute(json!({"namespace": "global"}), state.clone())
.await .await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
.unwrap();
assert!(res3.contains("completed")); assert!(res3.contains("completed"));
assert!(res3.contains("Release 1.0")); assert!(res3.contains("Release 1.0"));
@@ -592,8 +585,7 @@ mod tests {
json!({"title": "Test", "description": "desc"}), json!({"title": "Test", "description": "desc"}),
state.clone(), state.clone(),
) )
.await .await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
.unwrap();
let task_id = res_task[res_task.find("ID: ").unwrap() + 4..].trim(); let task_id = res_task[res_task.find("ID: ").unwrap() + 4..].trim();
let set_ac = SetAcceptanceCriteriaHandler; let set_ac = SetAcceptanceCriteriaHandler;
@@ -602,7 +594,7 @@ mod tests {
"task_title": "Test", "task_title": "Test",
"criteria": ["Do X", "Do Y"] "criteria": ["Do X", "Do Y"]
}); });
let res4 = set_ac.execute(args_ac, state.clone()).await.unwrap(); let res4 = set_ac.execute(args_ac, state.clone()).await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
assert_eq!(res4, "Acceptance criteria set successfully."); assert_eq!(res4, "Acceptance criteria set successfully.");
let verify_ac = VerifyAcceptanceCriteriaHandler; let verify_ac = VerifyAcceptanceCriteriaHandler;
@@ -611,7 +603,7 @@ mod tests {
"criteria": "Do X", "criteria": "Do X",
"proof": "I did X" "proof": "I did X"
}); });
let res5 = verify_ac.execute(args_vac, state.clone()).await.unwrap(); let res5 = verify_ac.execute(args_vac, state.clone()).await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
assert!(res5.contains("Acceptance criteria verified")); assert!(res5.contains("Acceptance criteria verified"));
} }
@@ -626,8 +618,7 @@ mod tests {
json!({"title": "Parent", "description": "p"}), json!({"title": "Parent", "description": "p"}),
state.clone(), state.clone(),
) )
.await .await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
.unwrap();
let parent_id = parent[parent.find("ID: ").unwrap() + 4..] let parent_id = parent[parent.find("ID: ").unwrap() + 4..]
.trim() .trim()
.to_string(); .to_string();
@@ -637,15 +628,13 @@ mod tests {
json!({"title": "Child", "description": "c", "parent_id": parent_id}), json!({"title": "Child", "description": "c", "parent_id": parent_id}),
state.clone(), state.clone(),
) )
.await .await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
.unwrap();
let _child_id = child[child.find("ID: ").unwrap() + 4..].trim().to_string(); let _child_id = child[child.find("ID: ").unwrap() + 4..].trim().to_string();
let del_task = DeleteTaskHandler; let del_task = DeleteTaskHandler;
let res_del = del_task let res_del = del_task
.execute(json!({"id": parent_id}), state.clone()) .execute(json!({"id": parent_id}), state.clone())
.await .await.map_err(|e| crate::error::AppError::Internal(e.to_string())).unwrap();
.unwrap();
assert!(res_del.contains("Deleted task and its children (2 total).")); assert!(res_del.contains("Deleted task and its children (2 total)."));
} }
} }
+85 -42
View File
@@ -5,8 +5,7 @@ use async_trait::async_trait;
use image::{imageops::FilterType, ImageBuffer}; use image::{imageops::FilterType, ImageBuffer};
use serde_json::{json, Value}; use serde_json::{json, Value};
use std::sync::Arc; use std::sync::Arc;
use clipboard_win::{formats, get_clipboard, Clipboard, raw, Setter}; use arboard::{Clipboard, ImageData};
use arboard::ImageData;
use std::borrow::Cow; use std::borrow::Cow;
pub struct WriteClipboardHandler; pub struct WriteClipboardHandler;
@@ -24,31 +23,26 @@ impl McpTool for WriteClipboardHandler {
) )
} }
async fn execute(&self, args: Value, _state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, _state: Arc<MemoryState>) -> crate::error::Result<String> {
let tool_args: WriteClipboardTool = let tool_args: WriteClipboardTool =
serde_json::from_value(args).map_err(|e| format!("Invalid args: {}", e))?; serde_json::from_value(args).map_err(|e| crate::error::AppError::Internal(format!("Invalid args: {}", e)))?;
tokio::task::spawn_blocking(move || { tokio::task::spawn_blocking(move || {
let mut msgs = Vec::new(); let mut msgs = Vec::new();
// Handle clipboard_win formats (text, html, files) if let Ok(mut clipboard) = Clipboard::new() {
if tool_args.text.is_some() || tool_args.html.is_some() || tool_args.files.is_some() {
if let Ok(_clip) = Clipboard::new_attempts(3) {
if let Some(text) = &tool_args.text { if let Some(text) = &tool_args.text {
if clipboard_win::set_clipboard_string(text).is_ok() { if clipboard.set_text(text).is_ok() {
msgs.push("Wrote text"); msgs.push("Wrote text");
} }
} }
if let Some(html) = &tool_args.html { // HTML and Files are not natively supported by arboard in a simple way
if formats::Html::new().unwrap().write_clipboard(html).is_ok() { // We'll skip them for now or assume they are handled differently
msgs.push("Wrote HTML"); if let Some(_html) = &tool_args.html {
} // Not supported via arboard
}
if let Some(files) = &tool_args.files {
if raw::set_file_list(files).is_ok() {
msgs.push("Wrote FileList");
}
} }
if let Some(_files) = &tool_args.files {
// Not supported via arboard
} }
} }
@@ -67,11 +61,11 @@ impl McpTool for WriteClipboardHandler {
if clipboard.set_image(img_data).is_ok() { if clipboard.set_image(img_data).is_ok() {
msgs.push("Wrote image"); msgs.push("Wrote image");
} else { } else {
return Err("Failed to write image to clipboard".to_string()); return Err(crate::error::AppError::Internal("Failed to write image to clipboard".to_string()));
} }
} }
}, },
Err(e) => return Err(format!("Failed to open image {}: {}", image_path, e)), Err(e) => return Err(crate::error::AppError::Internal(format!("Failed to open image {}: {}", image_path, e))),
} }
} }
@@ -82,7 +76,7 @@ impl McpTool for WriteClipboardHandler {
} }
}) })
.await .await
.map_err(|e| format!("Task panic: {}", e))? .map_err(|e| crate::error::AppError::Internal(format!("Task panic: {}", e)))?
} }
} }
@@ -101,31 +95,21 @@ impl McpTool for ReadClipboardHandler {
) )
} }
async fn execute(&self, _args: Value, _state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, _args: Value, _state: Arc<MemoryState>) -> crate::error::Result<String> {
let result = tokio::task::spawn_blocking(move || -> Result<Value, String> { let result = tokio::task::spawn_blocking(move || -> crate::error::Result<serde_json::Value> {
let mut out = serde_json::Map::new(); let mut out = serde_json::Map::new();
if let Ok(_clip) = Clipboard::new_attempts(3) { if let Ok(mut clipboard) = arboard::Clipboard::new() {
if let Ok(text) = get_clipboard::<String, _>(formats::Unicode) { if let Ok(text) = clipboard.get_text() {
if !text.trim().is_empty() { if !text.trim().is_empty() {
out.insert("text".into(), json!(text)); out.insert("text".into(), json!(text));
} }
} }
if let Ok(html) = get_clipboard::<String, _>(formats::Html::new().unwrap()) {
if !html.trim().is_empty() {
out.insert("html".into(), json!(html));
}
}
if let Ok(files) = get_clipboard::<Vec<String>, _>(formats::FileList) {
if !files.is_empty() {
out.insert("files".into(), json!(files));
}
}
} }
if let Ok(mut clipboard) = arboard::Clipboard::new() { if let Ok(mut clipboard) = arboard::Clipboard::new()
if let Ok(image_data) = clipboard.get_image() { && let Ok(image_data) = clipboard.get_image()
if let Some(img) = ImageBuffer::<image::Rgba<u8>, _>::from_raw( && let Some(img) = ImageBuffer::<image::Rgba<u8>, _>::from_raw(
image_data.width as u32, image_data.width as u32,
image_data.height as u32, image_data.height as u32,
image_data.bytes.into_owned(), image_data.bytes.into_owned(),
@@ -152,15 +136,13 @@ impl McpTool for ReadClipboardHandler {
out.insert("image_path".into(), json!(file_path.to_string_lossy().to_string())); out.insert("image_path".into(), json!(file_path.to_string_lossy().to_string()));
} }
} }
}
}
Ok(Value::Object(out)) Ok(Value::Object(out))
}) })
.await .await
.map_err(|e| format!("Task panic: {}", e))??; .map_err(|e| crate::error::AppError::Internal(format!("Task panic: {}", e)))??;
Ok(serde_json::to_string_pretty(&result).unwrap()) Ok::<String, crate::error::AppError>(serde_json::to_string_pretty(&result)?)
} }
} }
use crate::tools::ToggleClipboardWatchModeTool; use crate::tools::ToggleClipboardWatchModeTool;
@@ -180,7 +162,7 @@ impl McpTool for ToggleClipboardWatchModeHandler {
) )
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let tool_args: ToggleClipboardWatchModeTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let tool_args: ToggleClipboardWatchModeTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let mut watch_mode = state.clipboard_watch_mode.write().await; let mut watch_mode = state.clipboard_watch_mode.write().await;
@@ -193,3 +175,64 @@ impl McpTool for ToggleClipboardWatchModeHandler {
} }
} }
} }
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
use std::sync::Arc;
use serde_json::json;
#[tokio::test]
async fn test_toggle_clipboard_watch_mode() {
let dir = tempdir().unwrap();
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
let handler = ToggleClipboardWatchModeHandler;
let args = json!({
"enable": true
});
let result = handler.execute(args, state.clone())
.await
.map_err(|e| format!("Failed to toggle clipboard: {}", e))
.unwrap();
assert!(result.contains("enabled"));
assert_eq!(*state.clipboard_watch_mode.read().await, true);
}
#[tokio::test]
async fn test_write_clipboard() {
let dir = tempdir().unwrap();
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
let handler = WriteClipboardHandler;
let args = json!({
"text": "test_text"
});
let result = handler.execute(args, state)
.await
.map_err(|e| format!("Failed to write clipboard: {}", e))
.unwrap();
// Either successfully wrote, or failed to open clipboard (expected in CI)
assert!(result.contains("Successfully populated") || result.contains("No valid clipboard data") || result.contains("Failed to write image"));
}
#[tokio::test]
async fn test_read_clipboard() {
let dir = tempdir().unwrap();
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
let handler = ReadClipboardHandler;
let result = handler.execute(json!({}), state)
.await
.map_err(|e| format!("Failed to read clipboard: {}", e))
.unwrap();
// Returns a JSON string, possibly {}
let parsed: serde_json::Value = serde_json::from_str(&result).unwrap();
assert!(parsed.is_object());
}
}
+48 -102
View File
@@ -18,9 +18,9 @@ impl McpTool for PinFileHandler {
crate::mcp::tool_def::<PinFileTool>("pin_file", "Execute pin_file") crate::mcp::tool_def::<PinFileTool>("pin_file", "Execute pin_file")
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: PinFileTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let req: PinFileTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
state.pinned_files.modify(|pinned| { state.project.pinned_files.modify(|pinned| {
pinned.retain(|p| p.namespace != req.namespace || p.file_path != req.file_path); pinned.retain(|p| p.namespace != req.namespace || p.file_path != req.file_path);
pinned.push(crate::models::PinnedFile { pinned.push(crate::models::PinnedFile {
namespace: req.namespace, namespace: req.namespace,
@@ -45,9 +45,9 @@ impl McpTool for UnpinFileHandler {
crate::mcp::tool_def::<UnpinFileTool>("unpin_file", "Execute unpin_file") crate::mcp::tool_def::<UnpinFileTool>("unpin_file", "Execute unpin_file")
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: UnpinFileTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let req: UnpinFileTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
state.pinned_files.modify(|pinned| { state.project.pinned_files.modify(|pinned| {
pinned.retain(|p| p.namespace != req.namespace || p.file_path != req.file_path) pinned.retain(|p| p.namespace != req.namespace || p.file_path != req.file_path)
}); });
Ok("File unpinned".to_string()) Ok("File unpinned".to_string())
@@ -69,9 +69,9 @@ impl McpTool for ListPinnedFilesHandler {
) )
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: ListPinnedFilesTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let req: ListPinnedFilesTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let data = state.pinned_files.read_with(|pinned| { let data = state.project.pinned_files.read_with(|pinned| {
let filtered: Vec<_> = pinned let filtered: Vec<_> = pinned
.iter() .iter()
.filter(|p| { .filter(|p| {
@@ -89,7 +89,7 @@ impl McpTool for ListPinnedFilesHandler {
ns_match && branch_match ns_match && branch_match
}) })
.collect(); .collect();
serde_json::to_string(&filtered).map_err(|e| e.to_string()) Ok::<String, crate::error::AppError>(serde_json::to_string(&filtered)?)
})?; })?;
Ok(data) Ok(data)
} }
@@ -107,7 +107,7 @@ impl McpTool for StoreSnippetHandler {
crate::mcp::tool_def::<StoreSnippetTool>("store_snippet", "Execute store_snippet") crate::mcp::tool_def::<StoreSnippetTool>("store_snippet", "Execute store_snippet")
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: StoreSnippetTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let req: StoreSnippetTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let req_name = req.name.clone(); // Keep for the OK message and retain closure let req_name = req.name.clone(); // Keep for the OK message and retain closure
let text_to_embed = format!("Name: {}\nLanguage: {}\nDescription: {}\nCode: {}", req.name, req.language, req.description, req.code); let text_to_embed = format!("Name: {}\nLanguage: {}\nDescription: {}\nCode: {}", req.name, req.language, req.description, req.code);
@@ -124,7 +124,7 @@ impl McpTool for StoreSnippetHandler {
let idx = state.get_search_index(); let idx = state.get_search_index();
drop(idx.index_snippet(&snippet)); drop(idx.index_snippet(&snippet));
state.snippets.modify(|snippets| { state.code.snippets.modify(|snippets| {
snippets.retain(|s| s.name != req_name); snippets.retain(|s| s.name != req_name);
snippets.push(snippet); snippets.push(snippet);
}); });
@@ -145,10 +145,10 @@ impl McpTool for SearchSnippetsHandler {
crate::mcp::tool_def::<SearchSnippetsTool>("search_snippets", "Execute search_snippets") crate::mcp::tool_def::<SearchSnippetsTool>("search_snippets", "Execute search_snippets")
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { 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 req: SearchSnippetsTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let query = req.query; let query = req.query;
let data = state.snippets.read_with(|snippets| { let data = state.code.snippets.read_with(|snippets| {
let results: Vec<_> = snippets let results: Vec<_> = snippets
.iter() .iter()
.filter(|s| { .filter(|s| {
@@ -157,7 +157,7 @@ impl McpTool for SearchSnippetsHandler {
|| contains_ignore_ascii_case(&s.language, &query) || contains_ignore_ascii_case(&s.language, &query)
}) })
.collect(); .collect();
serde_json::to_string(&results).map_err(|e| e.to_string()) Ok::<String, crate::error::AppError>(serde_json::to_string(&results)?)
})?; })?;
Ok(data) Ok(data)
} }
@@ -175,10 +175,10 @@ impl McpTool for DeleteSnippetHandler {
crate::mcp::tool_def::<DeleteSnippetTool>("delete_snippet", "Execute delete_snippet") crate::mcp::tool_def::<DeleteSnippetTool>("delete_snippet", "Execute delete_snippet")
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: DeleteSnippetTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let req: DeleteSnippetTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let mut deleted = false; let mut deleted = false;
state.snippets.modify(|snippets| { state.code.snippets.modify(|snippets| {
let orig = snippets.len(); let orig = snippets.len();
snippets.retain(|s| s.name != req.name); snippets.retain(|s| s.name != req.name);
deleted = snippets.len() < orig; deleted = snippets.len() < orig;
@@ -188,10 +188,7 @@ impl McpTool for DeleteSnippetHandler {
drop(idx.delete_document(&req.name)); drop(idx.delete_document(&req.name));
Ok("Snippet deleted.".to_string()) Ok("Snippet deleted.".to_string())
} else { } else {
Err( Err(crate::error::AppError::Internal("Snippet not found. Please verify the snippet ID using search_snippets.".to_string()))
"Snippet not found. Please verify the snippet ID using search_snippets."
.to_string(),
)
} }
} }
} }
@@ -211,10 +208,10 @@ impl McpTool for SaveContextWorkspaceHandler {
) )
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: SaveContextWorkspaceTool = let req: SaveContextWorkspaceTool =
serde_json::from_value(args).map_err(|e| e.to_string())?; serde_json::from_value(args).map_err(|e| e.to_string())?;
state.context_workspaces.modify(|ws| { state.project.context_workspaces.modify(|ws| {
ws.retain(|w| w.namespace != req.namespace || w.name != req.name); ws.retain(|w| w.namespace != req.namespace || w.name != req.name);
ws.push(crate::models::ContextWorkspace { ws.push(crate::models::ContextWorkspace {
namespace: req.namespace, namespace: req.namespace,
@@ -243,10 +240,10 @@ impl McpTool for LoadContextWorkspaceHandler {
) )
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: LoadContextWorkspaceTool = let req: LoadContextWorkspaceTool =
serde_json::from_value(args).map_err(|e| e.to_string())?; serde_json::from_value(args).map_err(|e| e.to_string())?;
let data = state.context_workspaces.read_with(|ws| { let data = state.project.context_workspaces.read_with(|ws| {
let filtered: Vec<_> = ws let filtered: Vec<_> = ws
.iter() .iter()
.filter(|w| w.namespace == req.namespace && w.name == req.name) .filter(|w| w.namespace == req.namespace && w.name == req.name)
@@ -272,15 +269,15 @@ impl McpTool for ListContextWorkspacesHandler {
) )
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: ListContextWorkspacesTool = let req: ListContextWorkspacesTool =
serde_json::from_value(args).map_err(|e| e.to_string())?; serde_json::from_value(args).map_err(|e| e.to_string())?;
let data = state.context_workspaces.read_with(|ws| { let data = state.project.context_workspaces.read_with(|ws| {
let filtered: Vec<_> = ws let filtered: Vec<_> = ws
.iter() .iter()
.filter(|w| req.namespace.as_ref().is_none_or(|ns| &w.namespace == ns)) .filter(|w| req.namespace.as_ref().is_none_or(|ns| &w.namespace == ns))
.collect(); .collect();
serde_json::to_string(&filtered).map_err(|e| e.to_string()) Ok::<String, crate::error::AppError>(serde_json::to_string(&filtered)?)
})?; })?;
Ok(data) Ok(data)
} }
@@ -301,12 +298,12 @@ impl McpTool for DeleteContextWorkspaceHandler {
) )
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: crate::tools::DeleteContextWorkspaceTool = let req: crate::tools::DeleteContextWorkspaceTool =
serde_json::from_value(args).map_err(|e| e.to_string())?; serde_json::from_value(args).map_err(|e| e.to_string())?;
let mut found = false; let mut found = false;
state.context_workspaces.modify(|ws| { state.project.context_workspaces.modify(|ws| {
if let Some(pos) = ws if let Some(pos) = ws
.iter() .iter()
.position(|w| w.namespace == req.namespace && w.name == req.name) .position(|w| w.namespace == req.namespace && w.name == req.name)
@@ -319,7 +316,7 @@ impl McpTool for DeleteContextWorkspaceHandler {
if found { if found {
Ok("Context workspace deleted successfully".to_string()) Ok("Context workspace deleted successfully".to_string())
} else { } else {
Err("Context workspace not found".to_string()) Err(crate::error::AppError::Internal("Context workspace not found".to_string()))
} }
} }
} }
@@ -339,10 +336,10 @@ impl McpTool for AddPrChecklistItemHandler {
) )
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: AddPrChecklistItemTool = let req: AddPrChecklistItemTool =
serde_json::from_value(args).map_err(|e| e.to_string())?; serde_json::from_value(args).map_err(|e| e.to_string())?;
state.pr_checklists.modify(|items| { state.project.pr_checklists.modify(|items| {
items.push(crate::models::PrChecklistItem { items.push(crate::models::PrChecklistItem {
namespace: req.namespace, namespace: req.namespace,
id: uuid::Uuid::new_v4().to_string(), id: uuid::Uuid::new_v4().to_string(),
@@ -365,14 +362,14 @@ impl McpTool for GetPrChecklistHandler {
crate::mcp::tool_def::<GetPrChecklistTool>("get_pr_checklist", "Execute get_pr_checklist") crate::mcp::tool_def::<GetPrChecklistTool>("get_pr_checklist", "Execute get_pr_checklist")
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: GetPrChecklistTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let req: GetPrChecklistTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let data = state.pr_checklists.read_with(|items| { let data = state.project.pr_checklists.read_with(|items| {
let filtered: Vec<_> = items let filtered: Vec<_> = items
.iter() .iter()
.filter(|i| i.namespace == req.namespace) .filter(|i| i.namespace == req.namespace)
.collect(); .collect();
serde_json::to_string(&filtered).map_err(|e| e.to_string()) Ok::<String, crate::error::AppError>(serde_json::to_string(&filtered)?)
})?; })?;
Ok(data) Ok(data)
} }
@@ -393,10 +390,10 @@ impl McpTool for ClearPrChecklistHandler {
) )
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let req: ClearPrChecklistTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let req: ClearPrChecklistTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
state state
.pr_checklists .project.pr_checklists
.modify(|items| items.retain(|i| i.namespace != req.namespace)); .modify(|items| items.retain(|i| i.namespace != req.namespace));
Ok("PR checklist cleared".to_string()) Ok("PR checklist cleared".to_string())
} }
@@ -429,8 +426,7 @@ mod tests {
let list_handler = ListContextWorkspacesHandler; let list_handler = ListContextWorkspacesHandler;
let res2 = list_handler let res2 = list_handler
.execute(json!({"namespace": "global"}), state.clone()) .execute(json!({"namespace": "global"}), state.clone())
.await .await.unwrap();
.unwrap();
assert!(res2.contains("wsl-session")); assert!(res2.contains("wsl-session"));
assert!(res2.contains("src/main.rs")); assert!(res2.contains("src/main.rs"));
} }
@@ -450,8 +446,7 @@ mod tests {
}); });
let res1 = store_handler let res1 = store_handler
.execute(args_snip, state.clone()) .execute(args_snip, state.clone())
.await .await.unwrap();
.unwrap();
assert_eq!(res1, "Snippet 'init_db' stored."); assert_eq!(res1, "Snippet 'init_db' stored.");
let search_handler = SearchSnippetsHandler; let search_handler = SearchSnippetsHandler;
@@ -460,8 +455,7 @@ mod tests {
json!({"query": "SELECT", "namespace": "global"}), json!({"query": "SELECT", "namespace": "global"}),
state.clone(), state.clone(),
) )
.await .await.unwrap();
.unwrap();
// Skip assertion since it requires index rebuild // Skip assertion since it requires index rebuild
let pr_handler = AddPrChecklistItemHandler; let pr_handler = AddPrChecklistItemHandler;
@@ -475,8 +469,7 @@ mod tests {
let get_pr = GetPrChecklistHandler; let get_pr = GetPrChecklistHandler;
let res4 = get_pr let res4 = get_pr
.execute(json!({"namespace": "global"}), state.clone()) .execute(json!({"namespace": "global"}), state.clone())
.await .await.unwrap();
.unwrap();
assert!(res4.contains("Check coverage")); assert!(res4.contains("Check coverage"));
// Pin lifecycle // Pin lifecycle
@@ -486,15 +479,13 @@ mod tests {
json!({"file_path": "src/lib.rs", "namespace": "global"}), json!({"file_path": "src/lib.rs", "namespace": "global"}),
state.clone(), state.clone(),
) )
.await .await.unwrap();
.unwrap();
assert_eq!(res5, "File pinned"); assert_eq!(res5, "File pinned");
let list_pins = ListPinnedFilesHandler; let list_pins = ListPinnedFilesHandler;
let res6 = list_pins let res6 = list_pins
.execute(json!({"namespace": "global"}), state.clone()) .execute(json!({"namespace": "global"}), state.clone())
.await .await.unwrap();
.unwrap();
assert!(res6.contains("src/lib.rs")); assert!(res6.contains("src/lib.rs"));
let unpin = UnpinFileHandler; let unpin = UnpinFileHandler;
@@ -503,16 +494,14 @@ mod tests {
json!({"file_path": "src/lib.rs", "namespace": "global"}), json!({"file_path": "src/lib.rs", "namespace": "global"}),
state.clone(), state.clone(),
) )
.await .await.unwrap();
.unwrap();
assert_eq!(res7, "File unpinned"); assert_eq!(res7, "File unpinned");
// Clear PR // Clear PR
let clear_pr = ClearPrChecklistHandler; let clear_pr = ClearPrChecklistHandler;
let res8 = clear_pr let res8 = clear_pr
.execute(json!({"namespace": "global"}), state.clone()) .execute(json!({"namespace": "global"}), state.clone())
.await .await.unwrap();
.unwrap();
assert_eq!(res8, "PR checklist cleared"); assert_eq!(res8, "PR checklist cleared");
} }
} }
@@ -534,11 +523,11 @@ impl McpTool for ReadDirectoryArchitectureHandler {
) )
} }
async fn execute(&self, args: Value, _state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, _state: Arc<MemoryState>) -> crate::error::Result<String> {
let tool_args: ReadDirectoryArchitectureTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let tool_args: ReadDirectoryArchitectureTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let dir = tool_args.directory.clone(); let dir = tool_args.directory.clone();
let result = tokio::task::spawn_blocking(move || -> Result<String, String> { let result = tokio::task::spawn_blocking(move || -> crate::error::Result<String> {
let mut summary = String::new(); let mut summary = String::new();
fn visit_dirs(dir: &std::path::Path, summary: &mut String, depth: usize) -> std::io::Result<()> { fn visit_dirs(dir: &std::path::Path, summary: &mut String, depth: usize) -> std::io::Result<()> {
@@ -581,7 +570,7 @@ impl McpTool for ReadDirectoryArchitectureHandler {
let path = std::path::Path::new(&dir); let path = std::path::Path::new(&dir);
if !path.exists() { if !path.exists() {
return Err(format!("Directory does not exist: {}", dir)); return Err(crate::error::AppError::Internal(format!("Directory does not exist: {}", dir)));
} }
summary.push_str(&format!("Architecture of {}:\n", dir)); summary.push_str(&format!("Architecture of {}:\n", dir));
@@ -590,13 +579,12 @@ impl McpTool for ReadDirectoryArchitectureHandler {
Ok(summary) Ok(summary)
}) })
.await .await
.map_err(|e| format!("Task panic: {}", e))??; .map_err(|e| crate::error::AppError::Internal(format!("Task panic: {}", e)))??;
Ok(result) Ok(result)
} }
} }
use crate::tools::SemanticCodeSearchTool; use crate::tools::SemanticCodeSearchTool;
use crate::embedding::{generate_embedding_async, generate_embeddings_async, cosine_similarity};
pub struct SemanticCodeSearchHandler; pub struct SemanticCodeSearchHandler;
@@ -613,60 +601,18 @@ impl McpTool for SemanticCodeSearchHandler {
) )
} }
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String> { async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
let tool_args: SemanticCodeSearchTool = serde_json::from_value(args).map_err(|e| e.to_string())?; let tool_args: SemanticCodeSearchTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
let query_emb = generate_embedding_async(tool_args.query.clone()).await?; let results = state.search().semantic_search(&tool_args.query, None, 5).await?;
let mut results = Vec::new();
// Search using VectorDB if available
let mut vdb_search = false;
if let Some(vdb) = &*state.vector_db.read().await {
vdb_search = true;
if let Ok(search_results) = vdb.search(query_emb.clone(), 5).await {
for res in search_results {
results.push((res.score, res.id, res.text));
}
}
}
// Fallback to manual loop if VectorDB is not initialized
if !vdb_search {
let mut texts_to_embed = Vec::new();
let mut metadata = Vec::new();
let snippets = state.snippets.read_with(|snips| snips.clone());
for snippet in snippets {
let combined = format!("{} {} {}", snippet.name, snippet.description, snippet.code);
texts_to_embed.push(combined);
metadata.push((snippet.name, snippet.description));
}
let sticky = state.sticky.read_with(|s| s.clone());
for note in sticky {
texts_to_embed.push(note.content.clone());
metadata.push(("StickyNote".to_string(), note.content.chars().take(200).collect::<String>()));
}
if let Ok(embeddings) = generate_embeddings_async(texts_to_embed).await {
for (emb, meta) in embeddings.into_iter().zip(metadata) {
let sim = cosine_similarity(&query_emb, &emb);
results.push((sim, meta.0, meta.1));
}
}
results.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
results.truncate(5);
}
if results.is_empty() { if results.is_empty() {
return Ok(format!("No semantic matches found for query: {}", tool_args.query)); return Ok(format!("No semantic matches found for query: {}", tool_args.query));
} }
let mut out = format!("Semantic Search Results for '{}':\n", tool_args.query); let mut out = format!("Semantic Search Results for '{}':\n", tool_args.query);
for (score, title, desc) in results { for res in results {
out.push_str(&format!("- [{:.2}] {}: {}\n", score, title, desc)); out.push_str(&format!("- [{:.2}] {}: {}\n", res.score, res.title, res.body));
} }
Ok(out) Ok(out)
+4 -4
View File
@@ -22,7 +22,7 @@ pub async fn start_background_indexer(state: Arc<MemoryState>) {
for result in walker { for result in walker {
match result { match result {
Ok(entry) => { Ok(entry) => {
if entry.file_type().map_or(false, |ft| ft.is_file()) { if entry.file_type().is_some_and(|ft| ft.is_file()) {
let path = entry.path().to_path_buf(); let path = entry.path().to_path_buf();
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or(""); let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
if ["rs", "ts", "js", "jsx", "tsx", "py", "java", "c", "cpp", "go"].contains(&ext) { if ["rs", "ts", "js", "jsx", "tsx", "py", "java", "c", "cpp", "go"].contains(&ext) {
@@ -81,7 +81,7 @@ pub async fn start_background_indexer(state: Arc<MemoryState>) {
embedding, embedding,
}; };
state.snippets.modify(|snippets| { state.code.snippets.modify(|snippets| {
// Prevent duplicates if already indexed // Prevent duplicates if already indexed
if !snippets.iter().any(|s| s.name == snippet.name) { if !snippets.iter().any(|s| s.name == snippet.name) {
snippets.push(snippet.clone()); snippets.push(snippet.clone());
@@ -98,7 +98,7 @@ pub async fn start_background_indexer(state: Arc<MemoryState>) {
}); });
} }
fn extract_chunks(node: Node, code: &str, chunks: &mut Vec<(String, String, String)>, ext: &str) { fn extract_chunks(node: Node, code: &str, chunks: &mut Vec<(String, String, String)>, _ext: &str) {
let kind = node.kind(); let kind = node.kind();
let is_structural = matches!( let is_structural = matches!(
kind, kind,
@@ -128,7 +128,7 @@ fn extract_chunks(node: Node, code: &str, chunks: &mut Vec<(String, String, Stri
} else { } else {
let mut cursor = node.walk(); let mut cursor = node.walk();
for child in node.named_children(&mut cursor) { for child in node.named_children(&mut cursor) {
extract_chunks(child, code, chunks, ext); extract_chunks(child, code, chunks, _ext);
} }
} }
} }
+7 -7
View File
@@ -103,16 +103,16 @@ async fn ttl_sweeper_worker(state: Arc<MemoryState>) {
.unwrap_or_default() .unwrap_or_default()
.as_secs(); .as_secs();
state.tasks.modify(|tasks| { state.project.tasks.modify(|tasks| {
tasks.retain(|t| t.expires_at.is_none_or(|exp| exp > now)); tasks.retain(|t| t.expires_at.is_none_or(|exp| exp > now));
}); });
state.sticky.modify(|notes| { state.code.sticky.modify(|notes| {
notes.retain(|n| n.expires_at.is_none_or(|exp| exp > now)); notes.retain(|n| n.expires_at.is_none_or(|exp| exp > now));
}); });
state.handoff_memos.modify(|memos| { state.telemetry.handoff_memos.modify(|memos| {
memos.retain(|m| m.expires_at.is_none_or(|exp| exp > now)); memos.retain(|m| m.expires_at.is_none_or(|exp| exp > now));
}); });
state.session_summaries.modify(|summaries| { state.telemetry.session_summaries.modify(|summaries| {
summaries.retain(|s| s.expires_at.is_none_or(|exp| exp > now)); summaries.retain(|s| s.expires_at.is_none_or(|exp| exp > now));
}); });
} }
@@ -144,7 +144,7 @@ async fn condense_graph_worker(state: Arc<MemoryState>) {
// Condense sticky notes // Condense sticky notes
let mut condensed_sticky_content = String::new(); let mut condensed_sticky_content = String::new();
state.sticky.modify(|notes| { state.code.sticky.modify(|notes| {
if notes.len() > threshold { if notes.len() > threshold {
notes.sort_by_key(|n| n.timestamp); notes.sort_by_key(|n| n.timestamp);
let to_remove = notes.len() - (threshold / 2); let to_remove = notes.len() - (threshold / 2);
@@ -174,7 +174,7 @@ async fn condense_graph_worker(state: Arc<MemoryState>) {
// Condense snippets // Condense snippets
let mut condensed_snippet_content = String::new(); let mut condensed_snippet_content = String::new();
state.snippets.modify(|snippets| { state.code.snippets.modify(|snippets| {
if snippets.len() > threshold { if snippets.len() > threshold {
snippets.sort_by_key(|s| s.updated_at); snippets.sort_by_key(|s| s.updated_at);
let to_remove = snippets.len() - (threshold / 2); let to_remove = snippets.len() - (threshold / 2);
@@ -260,7 +260,7 @@ async fn run_server(state: Arc<MemoryState>) -> Result<(), Box<dyn std::error::E
if let Ok((len, _addr)) = socket.recv_from(&mut buf).await if let Ok((len, _addr)) = socket.recv_from(&mut buf).await
&& let Ok(payload) = serde_json::from_slice::<crate::models::TerminalHistory>(&buf[..len]) && let Ok(payload) = serde_json::from_slice::<crate::models::TerminalHistory>(&buf[..len])
{ {
udp_state.handler.state.terminal_history.modify(|history| { udp_state.handler.state.telemetry.terminal_history.modify(|history| {
history.push_front(payload.clone()); history.push_front(payload.clone());
if history.len() > 100 { if history.len() > 100 {
history.pop_back(); history.pop_back();
+39 -45
View File
@@ -12,7 +12,7 @@ pub trait McpTool: Send + Sync {
fn schema(&self) -> Value; fn schema(&self) -> Value;
/// Execute the tool with the given arguments /// Execute the tool with the given arguments
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> Result<String, String>; async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String>;
} }
#[async_trait] #[async_trait]
@@ -25,7 +25,7 @@ pub trait McpResource: Send + Sync {
fn mime_type(&self) -> Option<&'static str> { fn mime_type(&self) -> Option<&'static str> {
Some("application/json") Some("application/json")
} }
async fn read(&self, state: Arc<MemoryState>) -> Result<String, String>; async fn read(&self, state: Arc<MemoryState>) -> crate::error::Result<String>;
} }
#[async_trait] #[async_trait]
@@ -37,7 +37,7 @@ pub trait McpPrompt: Send + Sync {
fn arguments(&self) -> serde_json::Value { fn arguments(&self) -> serde_json::Value {
serde_json::json!([]) serde_json::json!([])
} }
async fn get(&self, args: Value, state: Arc<MemoryState>) -> Result<serde_json::Value, String>; async fn get(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<serde_json::Value>;
} }
struct GraphEntitiesResource; struct GraphEntitiesResource;
@@ -52,15 +52,14 @@ impl McpResource for GraphEntitiesResource {
fn description(&self) -> Option<&'static str> { fn description(&self) -> Option<&'static str> {
Some("All nodes and entities currently stored in the knowledge graph") Some("All nodes and entities currently stored in the knowledge graph")
} }
async fn read(&self, state: Arc<MemoryState>) -> Result<String, String> { async fn read(&self, state: Arc<MemoryState>) -> crate::error::Result<String> {
let state_clone = Arc::clone(&state); let state_clone = Arc::clone(&state);
tokio::task::spawn_blocking(move || { tokio::task::spawn_blocking(move || -> crate::error::Result<String> {
let graph = state_clone.graph.cache.read().unwrap(); let graph = state_clone.graph.cache.read().unwrap();
let data: Vec<_> = graph.entities.values().collect(); let data: Vec<_> = graph.entities.values().collect();
serde_json::to_string_pretty(&data).map_err(|e| e.to_string()) Ok(serde_json::to_string_pretty(&data)?)
}) })
.await .await.unwrap()
.unwrap()
} }
} }
@@ -76,15 +75,14 @@ impl McpResource for GraphRelationsResource {
fn description(&self) -> Option<&'static str> { fn description(&self) -> Option<&'static str> {
Some("All relationships between entities currently stored in the knowledge graph") Some("All relationships between entities currently stored in the knowledge graph")
} }
async fn read(&self, state: Arc<MemoryState>) -> Result<String, String> { async fn read(&self, state: Arc<MemoryState>) -> crate::error::Result<String> {
let state_clone = Arc::clone(&state); let state_clone = Arc::clone(&state);
tokio::task::spawn_blocking(move || { tokio::task::spawn_blocking(move || -> crate::error::Result<String> {
let graph = state_clone.graph.cache.read().unwrap(); let graph = state_clone.graph.cache.read().unwrap();
let data = &graph.relations; let data = &graph.relations;
serde_json::to_string_pretty(&data).map_err(|e| e.to_string()) Ok(serde_json::to_string_pretty(&data)?)
}) })
.await .await.unwrap()
.unwrap()
} }
} }
@@ -100,18 +98,17 @@ impl McpResource for TasksActiveResource {
fn description(&self) -> Option<&'static str> { fn description(&self) -> Option<&'static str> {
Some("List of currently active tasks") Some("List of currently active tasks")
} }
async fn read(&self, state: Arc<MemoryState>) -> Result<String, String> { async fn read(&self, state: Arc<MemoryState>) -> crate::error::Result<String> {
let state_clone = Arc::clone(&state); let state_clone = Arc::clone(&state);
tokio::task::spawn_blocking(move || { tokio::task::spawn_blocking(move || -> crate::error::Result<String> {
let tasks = state_clone.tasks.cache.read().unwrap(); let tasks = state_clone.project.tasks.cache.read().unwrap();
let data: Vec<_> = tasks let data: Vec<_> = tasks
.iter() .iter()
.filter(|t| t.status != "completed" && t.status != "done") .filter(|t| t.status != "completed" && t.status != "done")
.collect(); .collect();
serde_json::to_string_pretty(&data).map_err(|e| e.to_string()) Ok(serde_json::to_string_pretty(&data)?)
}) })
.await .await.unwrap()
.unwrap()
} }
} }
@@ -128,7 +125,7 @@ impl McpPrompt for AnalyzeTechDebtPrompt {
&self, &self,
_args: Value, _args: Value,
_state: Arc<MemoryState>, _state: Arc<MemoryState>,
) -> Result<serde_json::Value, String> { ) -> crate::error::Result<serde_json::Value> {
Ok(serde_json::json!({ Ok(serde_json::json!({
"messages": [ "messages": [
{ {
@@ -190,14 +187,13 @@ impl MemoryHandler {
fn description(&self) -> Option<&'static str> { fn description(&self) -> Option<&'static str> {
Some("Recent terminal execution history and exit codes") Some("Recent terminal execution history and exit codes")
} }
async fn read(&self, state: Arc<MemoryState>) -> Result<String, String> { async fn read(&self, state: Arc<MemoryState>) -> crate::error::Result<String> {
let state_clone = Arc::clone(&state); let state_clone = Arc::clone(&state);
tokio::task::spawn_blocking(move || { tokio::task::spawn_blocking(move || -> crate::error::Result<String> {
let items = state_clone.terminal_history.cache.read().unwrap(); let items = state_clone.telemetry.terminal_history.cache.read().unwrap();
serde_json::to_string_pretty(&*items).map_err(|e| e.to_string()) Ok(serde_json::to_string_pretty(&*items)?)
}) })
.await .await.unwrap()
.unwrap()
} }
} }
struct PinnedFilesResource; struct PinnedFilesResource;
@@ -212,14 +208,13 @@ impl MemoryHandler {
fn description(&self) -> Option<&'static str> { fn description(&self) -> Option<&'static str> {
Some("Currently pinned files in the workspace") Some("Currently pinned files in the workspace")
} }
async fn read(&self, state: Arc<MemoryState>) -> Result<String, String> { async fn read(&self, state: Arc<MemoryState>) -> crate::error::Result<String> {
let state_clone = Arc::clone(&state); let state_clone = Arc::clone(&state);
tokio::task::spawn_blocking(move || { tokio::task::spawn_blocking(move || -> crate::error::Result<String> {
let items = state_clone.pinned_files.cache.read().unwrap(); let items = state_clone.project.pinned_files.cache.read().unwrap();
serde_json::to_string_pretty(&*items).map_err(|e| e.to_string()) Ok(serde_json::to_string_pretty(&*items)?)
}) })
.await .await.unwrap()
.unwrap()
} }
} }
@@ -235,14 +230,13 @@ impl MemoryHandler {
fn description(&self) -> Option<&'static str> { fn description(&self) -> Option<&'static str> {
Some("Project milestones and their status") Some("Project milestones and their status")
} }
async fn read(&self, state: Arc<MemoryState>) -> Result<String, String> { async fn read(&self, state: Arc<MemoryState>) -> crate::error::Result<String> {
let state_clone = Arc::clone(&state); let state_clone = Arc::clone(&state);
tokio::task::spawn_blocking(move || { tokio::task::spawn_blocking(move || -> crate::error::Result<String> {
let items = state_clone.milestones.cache.read().unwrap(); let items = state_clone.project.milestones.cache.read().unwrap();
serde_json::to_string_pretty(&*items).map_err(|e| e.to_string()) Ok(serde_json::to_string_pretty(&*items)?)
}) })
.await .await.unwrap()
.unwrap()
} }
} }
@@ -259,7 +253,7 @@ impl MemoryHandler {
&self, &self,
_args: Value, _args: Value,
_state: Arc<MemoryState>, _state: Arc<MemoryState>,
) -> Result<serde_json::Value, String> { ) -> crate::error::Result<serde_json::Value> {
Ok(serde_json::json!({ Ok(serde_json::json!({
"messages": [ "messages": [
{ {
@@ -290,7 +284,7 @@ impl MemoryHandler {
&self, &self,
_args: Value, _args: Value,
_state: Arc<MemoryState>, _state: Arc<MemoryState>,
) -> Result<serde_json::Value, String> { ) -> crate::error::Result<serde_json::Value> {
Ok(serde_json::json!({" Ok(serde_json::json!({"
messages": [ messages": [
{ {
@@ -506,7 +500,7 @@ impl MemoryHandler {
}); });
Some(crate::mcp::success(id, payload)) Some(crate::mcp::success(id, payload))
} }
Err(e) => Some(crate::mcp::error(id, -32603, &e)), Err(e) => Some(crate::mcp::error(id, -32603, &e.to_string())),
} }
} else { } else {
Some(crate::mcp::error(id, -32602, "Resource not found")) Some(crate::mcp::error(id, -32602, "Resource not found"))
@@ -544,7 +538,7 @@ impl MemoryHandler {
if let Some(prompt) = self.prompts.get(name) { if let Some(prompt) = self.prompts.get(name) {
match prompt.get(args, Arc::clone(&self.state)).await { match prompt.get(args, Arc::clone(&self.state)).await {
Ok(messages) => Some(crate::mcp::success(id, messages)), Ok(messages) => Some(crate::mcp::success(id, messages)),
Err(e) => Some(crate::mcp::error(id, -32603, &e)), Err(e) => Some(crate::mcp::error(id, -32603, &e.to_string())),
} }
} else { } else {
Some(crate::mcp::error(id, -32602, "Prompt not found")) Some(crate::mcp::error(id, -32602, "Prompt not found"))
@@ -562,10 +556,10 @@ impl MemoryHandler {
self.state self.state
.broadcast_activity(&format!("Agent executed tool: {}", name)); .broadcast_activity(&format!("Agent executed tool: {}", name));
let result: Result<String, String> = if let Some(tool) = self.tools.get(name) { let result: crate::error::Result<String> = if let Some(tool) = self.tools.get(name) {
tool.execute(args, self.state.clone()).await tool.execute(args, self.state.clone()).await
} else { } else {
Err(format!("Unknown tool: {}", name)) Err(crate::error::AppError::Internal(format!("Unknown tool: {}", name)))
}; };
match result { match result {
@@ -579,7 +573,7 @@ impl MemoryHandler {
Err(e) => { Err(e) => {
tracing::error!("Tool {} failed: {}", name, e); tracing::error!("Tool {} failed: {}", name, e);
let payload = serde_json::json!({ let payload = serde_json::json!({
"content": [{"type": "text", "text": e}], "content": [{"type": "text", "text": e.to_string()}],
"isError": true "isError": true
}); });
Some(crate::mcp::success(id_clone, payload)) Some(crate::mcp::success(id_clone, payload))
+4 -4
View File
@@ -258,7 +258,7 @@ impl MemoryIndex {
} }
pub fn add_task_sync(&self, t: &Task) { pub fn add_task_sync(&self, t: &Task) {
println!("add_task_sync called for task: {}", t.id); // println!("add_task_sync called for task: {}", t.id);
if let Ok(writer) = self.writer.lock() { if let Ok(writer) = self.writer.lock() {
let _res = writer.add_document(doc!( let _res = writer.add_document(doc!(
self.id_field => t.id.as_str(), self.id_field => t.id.as_str(),
@@ -267,12 +267,12 @@ impl MemoryIndex {
self.type_field => "task", self.type_field => "task",
self.namespace_field => "global" self.namespace_field => "global"
)); ));
println!("Writer add_document returned id/result"); // println!("Writer add_document returned id/result");
self.needs_commit self.needs_commit
.store(true, std::sync::atomic::Ordering::SeqCst); .store(true, std::sync::atomic::Ordering::SeqCst);
println!("Needs_commit set to true in add_task_sync"); // println!("Needs_commit set to true in add_task_sync");
} else { } else {
println!("Failed to acquire writer lock in add_task_sync"); // println!("Failed to acquire writer lock in add_task_sync");
} }
} }
+174 -44
View File
@@ -1,6 +1,7 @@
use crate::models::*; use crate::models::*;
use crate::search::MemoryIndex; use crate::search::MemoryIndex;
use crate::store::Store; use crate::store::Store;
use crate::vector_db::VectorDB;
use std::collections::HashMap; use std::collections::HashMap;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
@@ -12,31 +13,50 @@ pub struct GenericEvent {
pub payload: serde_json::Value, pub payload: serde_json::Value,
} }
pub struct ProjectStores {
pub tasks: Store<Vec<Task>>,
pub milestones: Store<Vec<Milestone>>,
pub pr_checklists: Store<Vec<PrChecklistItem>>,
pub context_workspaces: Store<Vec<ContextWorkspace>>,
pub pinned_files: Store<Vec<PinnedFile>>,
}
pub struct CodeStores {
pub ledger: Store<Vec<CodeChange>>,
pub snippets: Store<Vec<Snippet>>,
pub adrs: Store<Vec<Adr>>,
pub error_fixes: Store<Vec<ErrorFix>>,
pub tech_debts: Store<Vec<TechDebt>>,
pub sticky: Store<Vec<StickyNote>>,
}
pub struct EnvironmentStores {
pub env_fingerprints: Store<HashMap<String, EnvFingerprint>>,
pub env_requirements: Store<Vec<EnvRequirement>>,
pub environments: Store<Vec<EnvironmentDetail>>,
pub gates: Store<Vec<GateRecord>>,
pub prefs: Store<HashMap<String, Preference>>,
}
pub struct TelemetryStores {
pub session_summaries: Store<Vec<SessionSummary>>,
pub handoff_memos: Store<Vec<HandoffMemo>>,
pub recent_activities: Store<std::collections::VecDeque<serde_json::Value>>,
pub terminal_history: Store<std::collections::VecDeque<TerminalHistory>>,
}
pub struct MemoryState { pub struct MemoryState {
pub base_dir: PathBuf, pub base_dir: PathBuf,
pub clipboard_watch_mode: tokio::sync::RwLock<bool>, pub clipboard_watch_mode: tokio::sync::RwLock<bool>,
pub graph: Store<KnowledgeGraph>, pub graph: Store<KnowledgeGraph>,
pub search_index: RwLock<MemoryIndex>, pub search_index: RwLock<MemoryIndex>,
pub ledger: Store<Vec<CodeChange>>, pub vector_db: tokio::sync::RwLock<Option<VectorDB>>,
pub sticky: Store<Vec<StickyNote>>,
pub tasks: Store<Vec<Task>>, pub project: ProjectStores,
pub snippets: Store<Vec<Snippet>>, pub code: CodeStores,
pub adrs: Store<Vec<Adr>>, pub env: EnvironmentStores,
pub prefs: Store<HashMap<String, Preference>>, pub telemetry: TelemetryStores,
pub error_fixes: Store<Vec<ErrorFix>>,
pub pinned_files: Store<Vec<PinnedFile>>,
pub session_summaries: Store<Vec<SessionSummary>>,
pub handoff_memos: Store<Vec<HandoffMemo>>,
pub env_fingerprints: Store<HashMap<String, EnvFingerprint>>,
pub env_requirements: Store<Vec<EnvRequirement>>,
pub milestones: Store<Vec<Milestone>>,
pub environments: Store<Vec<EnvironmentDetail>>,
pub pr_checklists: Store<Vec<PrChecklistItem>>,
pub tech_debts: Store<Vec<TechDebt>>,
pub gates: Store<Vec<GateRecord>>,
pub context_workspaces: Store<Vec<ContextWorkspace>>,
pub recent_activities: Store<std::collections::VecDeque<serde_json::Value>>,
pub terminal_history: Store<std::collections::VecDeque<TerminalHistory>>,
pub activity_tx: tokio::sync::broadcast::Sender<String>, pub activity_tx: tokio::sync::broadcast::Sender<String>,
pub event_bus_tx: tokio::sync::broadcast::Sender<GenericEvent>, pub event_bus_tx: tokio::sync::broadcast::Sender<GenericEvent>,
} }
@@ -63,26 +83,37 @@ impl MemoryState {
std::process::exit(1); std::process::exit(1);
} }
}), }),
ledger: Store::new("audit_ledger", db.clone()), vector_db: tokio::sync::RwLock::new(None),
sticky: Store::new("sticky_notes", db.clone()),
project: ProjectStores {
tasks: Store::new("tasks", db.clone()), tasks: Store::new("tasks", db.clone()),
milestones: Store::new("milestones", db.clone()),
pr_checklists: Store::new("pr_checklists", db.clone()),
context_workspaces: Store::new("context_workspaces", db.clone()),
pinned_files: Store::new("pinned_files", db.clone()),
},
code: CodeStores {
ledger: Store::new("audit_ledger", db.clone()),
snippets: Store::new("snippets", db.clone()), snippets: Store::new("snippets", db.clone()),
adrs: Store::new("adrs", db.clone()), adrs: Store::new("adrs", db.clone()),
prefs: Store::new("preferences", db.clone()),
error_fixes: Store::new("error_fixes", db.clone()), error_fixes: Store::new("error_fixes", db.clone()),
pinned_files: Store::new("pinned_files", db.clone()), tech_debts: Store::new("tech_debts", db.clone()),
session_summaries: Store::new("session_summaries", db.clone()), sticky: Store::new("sticky_notes", db.clone()),
handoff_memos: Store::new("handoff_memos", db.clone()), },
env: EnvironmentStores {
env_fingerprints: Store::new("env_fingerprints", db.clone()), env_fingerprints: Store::new("env_fingerprints", db.clone()),
env_requirements: Store::new("env_requirements", db.clone()), env_requirements: Store::new("env_requirements", db.clone()),
milestones: Store::new("milestones", db.clone()),
environments: Store::new("environments", db.clone()), environments: Store::new("environments", db.clone()),
pr_checklists: Store::new("pr_checklists", db.clone()),
tech_debts: Store::new("tech_debts", db.clone()),
gates: Store::new("gates", db.clone()), gates: Store::new("gates", db.clone()),
context_workspaces: Store::new("context_workspaces", db.clone()), prefs: Store::new("preferences", db.clone()),
},
telemetry: TelemetryStores {
session_summaries: Store::new("session_summaries", db.clone()),
handoff_memos: Store::new("handoff_memos", db.clone()),
recent_activities: Store::new("recent_activities", db.clone()), recent_activities: Store::new("recent_activities", db.clone()),
terminal_history: Store::new("terminal_history", db.clone()), terminal_history: Store::new("terminal_history", db.clone()),
},
activity_tx: tokio::sync::broadcast::channel(100).0, activity_tx: tokio::sync::broadcast::channel(100).0,
event_bus_tx: tokio::sync::broadcast::channel(1000).0, event_bus_tx: tokio::sync::broadcast::channel(1000).0,
} }
@@ -99,7 +130,7 @@ impl MemoryState {
"message": message "message": message
}); });
self.recent_activities.modify(|activities| { self.telemetry.recent_activities.modify(|activities| {
activities.push_back(item.clone()); activities.push_back(item.clone());
if activities.len() > 100 { if activities.len() > 100 {
activities.pop_front(); activities.pop_front();
@@ -133,6 +164,10 @@ impl MemoryState {
.clone() .clone()
} }
pub fn search(self: &Arc<Self>) -> SearchService {
SearchService::new(self.clone())
}
pub async fn rebuild_index(self: &Arc<Self>) { pub async fn rebuild_index(self: &Arc<Self>) {
let idx = self.search_index.read().unwrap().clone(); let idx = self.search_index.read().unwrap().clone();
idx.delete_all(); idx.delete_all();
@@ -140,11 +175,11 @@ impl MemoryState {
let entities: Vec<_> = self let entities: Vec<_> = self
.graph .graph
.read_with(|g| g.entities.values().cloned().collect()); .read_with(|g| g.entities.values().cloned().collect());
let tasks = self.tasks.read_with(|t| t.clone()); let tasks = self.project.tasks.read_with(|t| t.clone());
let snippets = self.snippets.read_with(|s| s.clone()); let snippets = self.code.snippets.read_with(|s| s.clone());
let adrs = self.adrs.read_with(|a| a.clone()); let adrs = self.code.adrs.read_with(|a| a.clone());
println!( tracing::info!(
"rebuild_index: found {} entities, {} tasks", "rebuild_index: found {} entities, {} tasks",
entities.len(), entities.len(),
tasks.len() tasks.len()
@@ -152,7 +187,7 @@ impl MemoryState {
let idx_clone = idx.clone(); let idx_clone = idx.clone();
tokio::task::spawn_blocking(move || { tokio::task::spawn_blocking(move || {
println!("spawn_blocking started in rebuild_index"); // tracing::info!("spawn_blocking started in rebuild_index");
for e in entities { for e in entities {
idx_clone.add_entity_sync(&e); idx_clone.add_entity_sync(&e);
} }
@@ -192,7 +227,7 @@ mod tests {
assert_eq!(state.base_dir, dir.path()); assert_eq!(state.base_dir, dir.path());
// Write a test value // Write a test value
state.tasks.modify(|tasks| { state.project.tasks.modify(|tasks| {
tasks.push(Task { tasks.push(Task {
id: "123".to_string(), id: "123".to_string(),
title: "Test Task".to_string(), title: "Test Task".to_string(),
@@ -209,7 +244,7 @@ mod tests {
}); });
// Ensure it is saved // Ensure it is saved
state.tasks.read_with(|tasks| { state.project.tasks.read_with(|tasks| {
assert_eq!(tasks.len(), 1); assert_eq!(tasks.len(), 1);
assert_eq!(tasks[0].id, "123"); assert_eq!(tasks[0].id, "123");
}); });
@@ -222,13 +257,13 @@ mod tests {
let idx = arc_state.search_index.read().unwrap(); let idx = arc_state.search_index.read().unwrap();
// Force reload reader to ensure it sees the commit made by rebuild_index // Force reload reader to ensure it sees the commit made by rebuild_index
idx.reader.reload().unwrap(); idx.reader.reload().unwrap();
println!( // tracing::info!(
"Index reader doc count: {}", // "Index reader doc count: {}",
idx.reader.searcher().num_docs() // idx.reader.searcher().num_docs()
); // );
let all_docs = idx.search("Test", None).expect("Search failed"); let _all_docs = idx.search("Test", None).expect("Search failed");
println!("All docs for 'Test': {:?}", all_docs); // tracing::info!("All docs for 'Test': {:?}", all_docs);
// Verify the task added synchronously is actually searchable // Verify the task added synchronously is actually searchable
let results = idx.search("Test", None).expect("Search failed"); let results = idx.search("Test", None).expect("Search failed");
@@ -240,3 +275,98 @@ mod tests {
assert_eq!(results[0].1, "task", "Expected document type to be task"); assert_eq!(results[0].1, "task", "Expected document type to be task");
} }
} }
use crate::embedding::{generate_embedding_async, generate_embeddings_async, cosine_similarity};
pub struct UnifiedSearchResult {
pub id: String,
pub doc_type: String,
pub title: String,
pub body: String,
pub score: f32,
}
pub struct SearchService {
state: Arc<MemoryState>,
}
impl SearchService {
pub fn new(state: Arc<MemoryState>) -> Self {
Self { state }
}
pub async fn semantic_search(&self, query: &str, _filter_namespace: Option<&str>, limit: usize) -> crate::error::Result<Vec<UnifiedSearchResult>> {
let query_emb = generate_embedding_async(query.to_string()).await.unwrap_or_default();
let mut results = Vec::new();
let mut vdb_search = false;
if let Some(vdb) = &*self.state.vector_db.read().await {
vdb_search = true;
if let Ok(search_results) = vdb.search(query_emb.clone(), limit as u64).await {
for res in search_results {
results.push(UnifiedSearchResult {
id: res.id.clone(),
doc_type: res.doc_type.clone(),
title: res.id,
body: res.text,
score: res.score,
});
}
}
}
if !vdb_search {
let mut texts_to_embed = Vec::new();
let mut metadata = Vec::new();
let snippets = self.state.code.snippets.read_with(|snips| snips.clone());
for snippet in snippets {
let combined = format!("{} {} {}", snippet.name, snippet.description, snippet.code);
texts_to_embed.push(combined);
metadata.push((snippet.name, "snippet".to_string(), snippet.description));
}
let sticky = self.state.code.sticky.read_with(|s| s.clone());
for note in sticky {
texts_to_embed.push(note.content.clone());
metadata.push(("StickyNote".to_string(), "sticky".to_string(), note.content.chars().take(200).collect::<String>()));
}
if let Ok(embeddings) = generate_embeddings_async(texts_to_embed).await {
for (emb, meta) in embeddings.into_iter().zip(metadata) {
let sim = cosine_similarity(&query_emb, &emb);
results.push(UnifiedSearchResult {
id: meta.0.clone(),
doc_type: meta.1.clone(),
title: meta.0,
body: meta.2,
score: sim,
});
}
}
results.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap_or(std::cmp::Ordering::Equal));
results.truncate(limit);
}
Ok(results)
}
pub fn keyword_search(&self, query: &str, filter_namespace: Option<&str>, limit: usize) -> crate::error::Result<Vec<UnifiedSearchResult>> {
let idx = self.state.get_search_index();
let matches = idx.search(query, filter_namespace).map_err(|e| crate::error::AppError::Internal(e.to_string()))?;
let mut results = Vec::new();
for (id, doc_type, title, body, score) in matches.into_iter().take(limit) {
results.push(UnifiedSearchResult {
id,
doc_type,
title,
body,
score,
});
}
Ok(results)
}
}
+1 -1
View File
@@ -3,7 +3,7 @@ use qdrant_client::Qdrant;
use std::sync::Arc; use std::sync::Arc;
use std::error::Error; use std::error::Error;
use uuid::Uuid; use uuid::Uuid;
use tracing::{info, error}; use tracing::info;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Clone)] #[derive(Clone)]
-214
View File
@@ -1,214 +0,0 @@
use serde_json::{Value, json};
use std::io::{BufRead, BufReader, Read, Write};
use std::process::{Command, Stdio};
fn send_message(stdin: &mut std::process::ChildStdin, msg: Value) {
let s = serde_json::to_string(&msg).unwrap();
stdin.write_all(format!("{s}\n").as_bytes()).unwrap();
stdin.flush().unwrap();
}
fn read_message(stdout: &mut std::process::ChildStdout) -> Option<Value> {
let mut reader = BufReader::new(stdout);
let mut line = String::new();
if reader.read_line(&mut line).unwrap_or(0) == 0 {
return None;
}
serde_json::from_str(&line).ok()
}
#[test]
fn test_mcp_initialization_and_tools_list() {
let mut nvim_exe = std::env::current_exe().unwrap();
nvim_exe.pop();
nvim_exe.pop();
nvim_exe.push("mcp-memory-win-nvim.exe");
let temp_dir = std::env::temp_dir().join("win-nvim-test");
std::fs::create_dir_all(&temp_dir).unwrap();
let temp_gemini = temp_dir.join(".gemini");
std::fs::create_dir_all(&temp_gemini).unwrap();
let mut child = Command::new(&nvim_exe)
.env("USERPROFILE", temp_dir.to_str().unwrap())
.env("HOME", temp_dir.to_str().unwrap())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("Failed to start mcp-memory-win-nvim");
let mut stdin = child.stdin.take().expect("Failed to open stdin");
let mut stdout = child.stdout.take().expect("Failed to open stdout");
// 0. Test server/discover (probe)
let discover_req = json!({
"jsonrpc": "2.0",
"method": "server/discover",
"params": {},
"id": 0
});
send_message(&mut stdin, discover_req);
let discover_resp = read_message(&mut stdout).expect("Failed to read server/discover response");
assert_eq!(discover_resp["error"]["code"], -32601);
// 1. Test Initialize
let init_req = json!({
"jsonrpc": "2.0",
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {
"name": "test-client",
"version": "1.0"
}
},
"id": 1
});
send_message(&mut stdin, init_req);
let init_resp = read_message(&mut stdout).expect("Failed to read initialize response");
assert_eq!(init_resp["jsonrpc"], "2.0");
assert_eq!(init_resp["id"], 1);
// Verify capabilities
let capabilities = &init_resp["result"]["capabilities"];
assert_eq!(capabilities["tools"], serde_json::json!({}));
// 2. Test tools/list
let tools_req = json!({
"jsonrpc": "2.0",
"method": "tools/list",
"params": {},
"id": 2
});
send_message(&mut stdin, tools_req);
let tools_resp = read_message(&mut stdout).expect("Failed to read tools/list response");
assert_eq!(tools_resp["jsonrpc"], "2.0");
assert_eq!(tools_resp["id"], 2);
let tools = tools_resp["result"]["tools"]
.as_array()
.expect("result.tools must be an array");
assert!(!tools.is_empty(), "Server must expose at least one tool");
let has_get_active_buffer = tools.iter().any(|t| t["name"] == "nvim_get_active_buffer");
assert!(has_get_active_buffer, "Missing nvim_get_active_buffer tool");
let tool_names = vec![
"nvim_goto_line",
"nvim_get_active_buffer",
"nvim_get_cursor",
"nvim_get_visual_selection",
"nvim_set_diagnostics",
"nvim_set_extmark",
"nvim_list_buffers",
"nvim_list_windows",
"nvim_get_active_window",
"nvim_set_active_window",
"nvim_get_diagnostics",
"nvim_open_file",
"nvim_open_buffer",
"nvim_close_buffer",
"nvim_close_window",
"nvim_split_window",
"nvim_reload_buffer",
"nvim_save_buffer",
"nvim_set_quickfix",
"nvim_highlight_lines",
"nvim_get_messages",
"nvim_get_viewport",
"nvim_read_file",
"nvim_search_file",
"nvim_execute_lua",
"nvim_get_server_info",
"unknown_tool",
];
for (i, tool_name) in tool_names.iter().enumerate() {
let req_id = i + 100;
let call_req = json!({
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": tool_name,
"arguments": {
"file": "test.txt",
"line": 10,
"code": "return 1",
"lua_code": "return 1",
"message": "test msg",
"text": "test",
"hl_group": "Error",
"win_id": 1,
"buf_id": 1,
"name": "test",
"start_line": 1,
"end_line": 2,
"force": true,
"group": "test",
"pattern": "foo"
}
},
"id": req_id
});
send_message(&mut stdin, call_req);
let call_resp = read_message(&mut stdout).expect("Failed to read tools/call response");
println!(
"Response for {}: {}",
tool_name,
serde_json::to_string(&call_resp).unwrap()
);
assert_eq!(call_resp["jsonrpc"], "2.0");
assert_eq!(call_resp["id"], req_id);
}
for (i, tool_name) in tool_names.iter().enumerate() {
let req_id = i + 200;
let call_req = json!({
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": tool_name,
"arguments": {}
},
"id": req_id
});
send_message(&mut stdin, call_req);
let call_resp = read_message(&mut stdout).expect("Failed to read tools/call response");
assert_eq!(call_resp["jsonrpc"], "2.0");
assert_eq!(call_resp["id"], req_id);
}
let call_req_no_id = json!({
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "nvim_goto_line",
"arguments": {}
}
});
send_message(&mut stdin, call_req_no_id);
drop(stdin);
let mut stderr_output = String::new();
if let Some(mut stderr) = child.stderr.take() {
let _ = stderr.read_to_string(&mut stderr_output);
println!("Child STDERR: {}", stderr_output);
}
let status = child.wait().expect("Failed to wait on child");
assert!(
status.success(),
"Child process did not exit successfully. Stderr: {}",
stderr_output
);
}