diff --git a/server/src/handlers/env.rs b/server/src/handlers/env.rs index 2ba7c51..1235cdd 100644 --- a/server/src/handlers/env.rs +++ b/server/src/handlers/env.rs @@ -162,6 +162,56 @@ impl McpTool for GetEnvironmentDetailsHandler { } } +pub struct EnvironmentHandler; + +#[async_trait] +impl McpTool for EnvironmentHandler { + fn name(&self) -> &'static str { + "environment" + } + + fn schema(&self) -> Value { + crate::mcp::tool_def::( + "environment", + "Consolidated environment & infrastructure management", + ) + } + + async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { + let req: EnvironmentTool = serde_json::from_value(args).map_err(|e| e.to_string())?; + let ns = req.namespace.unwrap_or_else(|| crate::models::default_namespace()); + match req.action { + EnvAction::UpdateFingerprint => { + let tool_versions = req.tool_versions.unwrap_or_default(); + UpdateEnvFingerprintHandler.execute(serde_json::json!({"namespace": ns, "tool_versions": tool_versions}), state).await + } + EnvAction::ReadFingerprint => { + ReadEnvFingerprintHandler.execute(serde_json::json!({"namespace": ns}), state).await + } + EnvAction::LogRequirement => { + let key = req.key.ok_or_else(|| { + crate::error::AppError::Internal("key is required for 'log_requirement' action".to_string()) + })?; + let desc = req.description.unwrap_or_default(); + let secret = req.is_secret.unwrap_or(false); + LogEnvRequirementHandler.execute(serde_json::json!({"key": key, "description": desc, "is_secret": secret, "namespace": ns}), state).await + } + EnvAction::Register => { + let name = req.name.ok_or_else(|| { + crate::error::AppError::Internal("name is required for 'register' action".to_string()) + })?; + let url = req.url.unwrap_or_default(); + let desc = req.description.unwrap_or_default(); + let vpn = req.requires_vpn.unwrap_or(false); + RegisterEnvironmentHandler.execute(serde_json::json!({"name": name, "url": url, "description": desc, "requires_vpn": vpn, "namespace": ns}), state).await + } + EnvAction::GetDetails => { + GetEnvironmentDetailsHandler.execute(serde_json::json!({"namespace": ns}), state).await + } + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/server/src/handlers/meta.rs b/server/src/handlers/meta.rs index a38aec8..36b574b 100644 --- a/server/src/handlers/meta.rs +++ b/server/src/handlers/meta.rs @@ -524,6 +524,109 @@ impl McpTool for ListTechDebtHandler { } } +pub struct DecisionsHandler; + +#[async_trait] +impl McpTool for DecisionsHandler { + fn name(&self) -> &'static str { + "decisions" + } + + fn schema(&self) -> Value { + crate::mcp::tool_def::( + "decisions", + "Consolidated Architectural Decision Records (ADRs) management (log, query, delete)", + ) + } + + async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { + let req: DecisionsTool = serde_json::from_value(args).map_err(|e| e.to_string())?; + let ns = req.namespace.unwrap_or_else(|| crate::models::default_namespace()); + match req.action { + DecisionAction::Log => { + let title = req.title.ok_or_else(|| { + crate::error::AppError::Internal("title is required for 'log' action".to_string()) + })?; + let status = req.status.unwrap_or_else(|| "accepted".to_string()); + let context = req.context.unwrap_or_default(); + let decision = req.decision.unwrap_or_default(); + let consequences = req.consequences.unwrap_or_default(); + LogDecisionHandler.execute( + serde_json::json!({ + "title": title, + "status": status, + "context": context, + "decision": decision, + "consequences": consequences, + "namespace": ns + }), + state, + ).await + } + DecisionAction::Query => { + QueryDecisionsHandler.execute(serde_json::json!({"query": req.query, "namespace": ns}), state).await + } + DecisionAction::Delete => { + let id = req.id.ok_or_else(|| { + crate::error::AppError::Internal("id is required for 'delete' action".to_string()) + })?; + DeleteDecisionHandler.execute(serde_json::json!({"id": id}), state).await + } + } + } +} + +pub struct TechDebtHandler; + +#[async_trait] +impl McpTool for TechDebtHandler { + fn name(&self) -> &'static str { + "tech_debt" + } + + fn schema(&self) -> Value { + crate::mcp::tool_def::( + "tech_debt", + "Consolidated technical debt management (log, resolve, list)", + ) + } + + async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { + let req: TechDebtTool = serde_json::from_value(args).map_err(|e| e.to_string())?; + let ns = req.namespace.unwrap_or_else(|| crate::models::default_namespace()); + match req.action { + TechDebtAction::Log => { + let desc = req.description.ok_or_else(|| { + crate::error::AppError::Internal("description is required for 'log' action".to_string()) + })?; + let ideal = req.ideal_solution.unwrap_or_default(); + LogTechDebtHandler.execute( + serde_json::json!({ + "description": desc, + "ideal_solution": ideal, + "git_commit": req.git_commit, + "git_branch": req.git_branch, + "symbol_references": req.symbol_references, + "line_range": req.line_range, + "namespace": ns + }), + state, + ).await + } + TechDebtAction::Resolve => { + let id = req.id.ok_or_else(|| { + crate::error::AppError::Internal("id is required for 'resolve' action".to_string()) + })?; + ResolveTechDebtHandler.execute(serde_json::json!({"id": id}), state).await + } + TechDebtAction::List => { + let inc = req.include_resolved.unwrap_or(false); + ListTechDebtHandler.execute(serde_json::json!({"include_resolved": inc, "namespace": ns}), state).await + } + } + } +} + pub struct OmniSearchHandler; #[async_trait] diff --git a/server/src/handlers/notes.rs b/server/src/handlers/notes.rs index b384398..3c0c0a1 100644 --- a/server/src/handlers/notes.rs +++ b/server/src/handlers/notes.rs @@ -7,23 +7,23 @@ use serde_json::Value; use std::collections::HashSet; use std::sync::Arc; -pub struct ManageStickyNotesHandler; +pub struct StickyNotesHandler; #[async_trait] -impl McpTool for ManageStickyNotesHandler { +impl McpTool for StickyNotesHandler { fn name(&self) -> &'static str { - "manage_sticky_notes" + "sticky_notes" } fn schema(&self) -> Value { - crate::mcp::tool_def::( - "manage_sticky_notes", - "Manage ephemeral sticky notes with TTL (add, read, delete, clear)", + crate::mcp::tool_def::( + "sticky_notes", + "Ephemeral sticky notes with TTL (add, read, delete, clear)", ) } async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { - let req: ManageStickyNotesTool = serde_json::from_value(args).map_err(|e| e.to_string())?; + let req: StickyNotesTool = serde_json::from_value(args).map_err(|e| e.to_string())?; match req.action { StickyNoteAction::Add => { let content = req.content.ok_or_else(|| { @@ -216,97 +216,72 @@ impl McpTool for ClearStickyNotesHandler { } } -pub struct LeaveHandoffMemoHandler; +pub struct HandoffMemosHandler; #[async_trait] -impl McpTool for LeaveHandoffMemoHandler { +impl McpTool for HandoffMemosHandler { fn name(&self) -> &'static str { - "leave_handoff_memo" + "handoff_memos" } fn schema(&self) -> Value { - crate::mcp::tool_def::( - "leave_handoff_memo", - "Execute leave_handoff_memo", + crate::mcp::tool_def::( + "handoff_memos", + "Handoff memos across sessions (leave, read, clear)", ) } async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { - let req: LeaveHandoffMemoTool = serde_json::from_value(args).map_err(|e| e.to_string())?; - state.telemetry.handoff_memos.modify(|memos| { - memos.push(crate::models::HandoffMemo { - id: uuid::Uuid::new_v4().to_string(), - author: "agy".to_string(), - content: req.content, - namespace: req.namespace, - timestamp: crate::handlers::utils::now_secs(), - expires_at: None, - }); - if memos.len() > 200 { - memos.remove(0); - } - }); - Ok("Handoff memo left".to_string()) - } -} - -pub struct ReadHandoffMemosHandler; - -#[async_trait] -impl McpTool for ReadHandoffMemosHandler { - fn name(&self) -> &'static str { - "read_handoff_memos" - } - - fn schema(&self) -> Value { - crate::mcp::tool_def::( - "read_handoff_memos", - "Execute read_handoff_memos", - ) - } - - async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { - let req: ReadHandoffMemosTool = serde_json::from_value(args).map_err(|e| e.to_string())?; - let data = state.telemetry.handoff_memos.read_with(|items| { - let filtered: Vec<_> = items - .iter() - .filter(|i| { - if let Some(ns) = &req.namespace { - &i.namespace == ns - } else { - true + let req: HandoffMemosTool = serde_json::from_value(args).map_err(|e| e.to_string())?; + match req.action { + HandoffMemoAction::Leave => { + let content = req.content.ok_or_else(|| { + crate::error::AppError::Internal("content is required for 'leave' action".to_string()) + })?; + let ns = req.namespace.unwrap_or_else(|| crate::models::default_namespace()); + state.telemetry.handoff_memos.modify(|memos| { + memos.push(crate::models::HandoffMemo { + id: uuid::Uuid::new_v4().to_string(), + author: "agy".to_string(), + content, + namespace: ns, + timestamp: crate::handlers::utils::now_secs(), + expires_at: None, + }); + if memos.len() > 200 { + memos.remove(0); } - }) - .collect(); - Ok::(serde_json::to_string(&filtered)?) - })?; - Ok(data) - } -} - -pub struct ClearHandoffMemosHandler; - -#[async_trait] -impl McpTool for ClearHandoffMemosHandler { - fn name(&self) -> &'static str { - "clear_handoff_memos" - } - - fn schema(&self) -> Value { - crate::mcp::tool_def::( - "clear_handoff_memos", - "Execute clear_handoff_memos", - ) - } - - async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { - let req: ClearHandoffMemosTool = serde_json::from_value(args).map_err(|e| e.to_string())?; - let ids: HashSet<_> = req.ids.into_iter().collect(); - state - .telemetry - .handoff_memos - .modify(|memos| memos.retain(|m| !ids.contains(&m.id))); - Ok("Handoff memos cleared".to_string()) + }); + Ok("Handoff memo left.".to_string()) + } + HandoffMemoAction::Read => { + let data = state.telemetry.handoff_memos.read_with(|items| { + let filtered: Vec<_> = items + .iter() + .filter(|i| { + if let Some(ns) = &req.namespace { + &i.namespace == ns + } else { + true + } + }) + .collect(); + Ok::(serde_json::to_string(&filtered)?) + })?; + Ok(data) + } + HandoffMemoAction::Clear => { + let ids: HashSet<_> = req.ids.unwrap_or_default().into_iter().collect(); + state.telemetry.handoff_memos.modify(|memos| { + if ids.is_empty() { + memos.clear(); + } else { + memos.retain(|m| !ids.contains(&m.id)); + } + }); + Ok("Handoff memos cleared.".to_string()) + } + } } } @@ -481,10 +456,10 @@ mod tests { let dir = tempdir().unwrap(); let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap())); - let handoff_handler = LeaveHandoffMemoHandler; + let handoff_handler = HandoffMemosHandler; let args = json!({ + "action": "leave", "content": "Finished implementing graph tests", - "author": "Antigravity", "namespace": "global" }); @@ -493,11 +468,10 @@ mod tests { .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 res2 = read_handoff - .execute(json!({"namespace": "global"}), state.clone()) + let res2 = handoff_handler + .execute(json!({"action": "read", "namespace": "global"}), state.clone()) .await .map_err(|e| crate::error::AppError::Internal(e.to_string())) .unwrap(); @@ -532,7 +506,7 @@ mod tests { let dir = tempfile::tempdir().unwrap(); let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap())); - let handler = ManageStickyNotesHandler; + let handler = StickyNotesHandler; let invalid_args = serde_json::json!({ "action": "invalid_action" }); diff --git a/server/src/handlers/tasks.rs b/server/src/handlers/tasks.rs index 051ccdd..ceb9962 100644 --- a/server/src/handlers/tasks.rs +++ b/server/src/handlers/tasks.rs @@ -512,6 +512,145 @@ impl McpTool for ListMilestonesHandler { } } +pub struct TasksHandler; + +#[async_trait] +impl McpTool for TasksHandler { + fn name(&self) -> &'static str { + "tasks" + } + + fn schema(&self) -> Value { + crate::mcp::tool_def::( + "tasks", + "Consolidated task management (add, update, delete, list, set_criteria, verify)", + ) + } + + async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { + let req: TasksTool = serde_json::from_value(args).map_err(|e| e.to_string())?; + match req.action { + TaskAction::Add => { + let title = req.title.ok_or_else(|| { + crate::error::AppError::Internal("title is required for 'add' action".to_string()) + })?; + let description = req.description.unwrap_or_default(); + let add_args = serde_json::json!({ + "title": title, + "description": description, + "git_branch": req.git_branch, + "parent_id": req.parent_id, + "dependencies": req.dependencies, + }); + AddTaskHandler.execute(add_args, state).await + } + TaskAction::Update => { + let id = req.id.ok_or_else(|| { + crate::error::AppError::Internal("id is required for 'update' action".to_string()) + })?; + let status = req.status.ok_or_else(|| { + crate::error::AppError::Internal("status is required for 'update' action".to_string()) + })?; + let update_args = serde_json::json!({ + "id": id, + "status": status, + }); + UpdateTaskStatusHandler.execute(update_args, state).await + } + TaskAction::Delete => { + let id = req.id.ok_or_else(|| { + crate::error::AppError::Internal("id is required for 'delete' action".to_string()) + })?; + let del_args = serde_json::json!({ + "id": id, + }); + DeleteTaskHandler.execute(del_args, state).await + } + TaskAction::List => { + let list_args = serde_json::json!({ + "git_branch": req.git_branch, + "summary_level": req.summary_level, + "max_tokens": req.max_tokens, + }); + ListActiveTasksHandler.execute(list_args, state).await + } + TaskAction::SetCriteria => { + let id = req.id.ok_or_else(|| { + crate::error::AppError::Internal("id is required for 'set_criteria' action".to_string()) + })?; + let criteria = req.criteria.ok_or_else(|| { + crate::error::AppError::Internal("criteria array is required for 'set_criteria' action".to_string()) + })?; + let set_args = serde_json::json!({ + "id": id, + "acceptance_criteria": criteria, + }); + SetAcceptanceCriteriaHandler.execute(set_args, state).await + } + TaskAction::Verify => { + let id = req.id.ok_or_else(|| { + crate::error::AppError::Internal("id is required for 'verify' action".to_string()) + })?; + let verify_args = serde_json::json!({ + "id": id, + }); + VerifyAcceptanceCriteriaHandler.execute(verify_args, state).await + } + } + } +} + +pub struct MilestonesHandler; + +#[async_trait] +impl McpTool for MilestonesHandler { + fn name(&self) -> &'static str { + "milestones" + } + + fn schema(&self) -> Value { + crate::mcp::tool_def::( + "milestones", + "Consolidated milestone management (add, update, list)", + ) + } + + async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { + let req: MilestonesTool = serde_json::from_value(args).map_err(|e| e.to_string())?; + match req.action { + MilestoneAction::Add => { + let title = req.title.ok_or_else(|| { + crate::error::AppError::Internal("title is required for 'add' action".to_string()) + })?; + let add_args = serde_json::json!({ + "title": title, + "namespace": req.namespace.unwrap_or_else(|| crate::models::default_namespace()), + }); + AddMilestoneHandler.execute(add_args, state).await + } + MilestoneAction::Update => { + let id = req.id.ok_or_else(|| { + crate::error::AppError::Internal("id is required for 'update' action".to_string()) + })?; + let status = req.status.ok_or_else(|| { + crate::error::AppError::Internal("status is required for 'update' action".to_string()) + })?; + let update_args = serde_json::json!({ + "id": id, + "status": status, + }); + UpdateMilestoneHandler.execute(update_args, state).await + } + MilestoneAction::List => { + let list_args = serde_json::json!({ + "namespace": req.namespace, + }); + ListMilestonesHandler.execute(list_args, state).await + } + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/server/src/handlers/vision.rs b/server/src/handlers/vision.rs index eaa451b..5242f7f 100644 --- a/server/src/handlers/vision.rs +++ b/server/src/handlers/vision.rs @@ -218,6 +218,48 @@ impl McpTool for ToggleClipboardWatchModeHandler { } } +use crate::tools::{ClipboardAction, ClipboardTool}; + +pub struct ClipboardHandler; + +#[async_trait] +impl McpTool for ClipboardHandler { + fn name(&self) -> &'static str { + "clipboard" + } + + fn schema(&self) -> Value { + crate::mcp::tool_def::( + "clipboard", + "Consolidated OS clipboard management (read, write, toggle_watch)", + ) + } + + async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { + let req: ClipboardTool = serde_json::from_value(args).map_err(|e| e.to_string())?; + match req.action { + ClipboardAction::Read => { + ReadClipboardHandler.execute(serde_json::json!({}), state).await + } + ClipboardAction::Write => { + WriteClipboardHandler.execute( + serde_json::json!({ + "text": req.text, + "html": req.html, + "files": req.files, + "image_path": req.image_path + }), + state, + ).await + } + ClipboardAction::ToggleWatch => { + let enable = req.enabled.unwrap_or(true); + ToggleClipboardWatchModeHandler.execute(serde_json::json!({"enable": enable}), state).await + } + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/server/src/handlers/workspaces.rs b/server/src/handlers/workspaces.rs index 6cb2a0e..5c08aee 100644 --- a/server/src/handlers/workspaces.rs +++ b/server/src/handlers/workspaces.rs @@ -475,6 +475,203 @@ impl McpTool for ClearPrChecklistHandler { } } +pub struct PinnedFilesHandler; + +#[async_trait] +impl McpTool for PinnedFilesHandler { + fn name(&self) -> &'static str { + "pinned_files" + } + + fn schema(&self) -> Value { + crate::mcp::tool_def::( + "pinned_files", + "Consolidated pinned files focus management (pin, unpin, list)", + ) + } + + async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { + let req: PinnedFilesTool = serde_json::from_value(args).map_err(|e| e.to_string())?; + let ns = req.namespace.unwrap_or_else(|| crate::models::default_namespace()); + match req.action { + PinnedFileAction::Pin => { + let path = req.path.ok_or_else(|| { + crate::error::AppError::Internal("path is required for 'pin' action".to_string()) + })?; + PinFileHandler.execute(serde_json::json!({"file_path": path, "namespace": ns}), state).await + } + PinnedFileAction::Unpin => { + let path = req.path.ok_or_else(|| { + crate::error::AppError::Internal("path is required for 'unpin' action".to_string()) + })?; + UnpinFileHandler.execute(serde_json::json!({"file_path": path, "namespace": ns}), state).await + } + PinnedFileAction::List => { + ListPinnedFilesHandler.execute(serde_json::json!({"namespace": ns}), state).await + } + } + } +} + +pub struct ContextWorkspacesHandler; + +#[async_trait] +impl McpTool for ContextWorkspacesHandler { + fn name(&self) -> &'static str { + "context_workspaces" + } + + fn schema(&self) -> Value { + crate::mcp::tool_def::( + "context_workspaces", + "Consolidated context workspace state management (save, load, list, delete, diff)", + ) + } + + async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { + let req: ContextWorkspacesTool = serde_json::from_value(args).map_err(|e| e.to_string())?; + let ns = req.namespace.unwrap_or_else(|| crate::models::default_namespace()); + match req.action { + ContextWorkspaceAction::Save => { + let name = req.name.ok_or_else(|| { + crate::error::AppError::Internal("name is required for 'save' action".to_string()) + })?; + let pinned_files = req.pinned_files.unwrap_or_default(); + let active_task_ids = req.active_task_ids.unwrap_or_default(); + SaveContextWorkspaceHandler.execute( + serde_json::json!({ + "name": name, + "namespace": ns, + "pinned_files": pinned_files, + "active_task_ids": active_task_ids + }), + state, + ).await + } + ContextWorkspaceAction::Load => { + let name = req.name.ok_or_else(|| { + crate::error::AppError::Internal("name is required for 'load' action".to_string()) + })?; + LoadContextWorkspaceHandler.execute(serde_json::json!({"name": name, "namespace": ns}), state).await + } + ContextWorkspaceAction::List => { + ListContextWorkspacesHandler.execute(serde_json::json!({"namespace": ns}), state).await + } + ContextWorkspaceAction::Delete => { + let name = req.name.ok_or_else(|| { + crate::error::AppError::Internal("name is required for 'delete' action".to_string()) + })?; + DeleteContextWorkspaceHandler.execute(serde_json::json!({"name": name, "namespace": ns}), state).await + } + ContextWorkspaceAction::Diff => { + let name = req.name.ok_or_else(|| { + crate::error::AppError::Internal("name is required for 'diff' action".to_string()) + })?; + let other = req.other_name.ok_or_else(|| { + crate::error::AppError::Internal("other_name is required for 'diff' action".to_string()) + })?; + DiffContextWorkspacesHandler.execute(serde_json::json!({"workspace_a": name, "workspace_b": other, "namespace": ns}), state).await + } + } + } +} + +pub struct PrChecklistHandler; + +#[async_trait] +impl McpTool for PrChecklistHandler { + fn name(&self) -> &'static str { + "pr_checklist" + } + + fn schema(&self) -> Value { + crate::mcp::tool_def::( + "pr_checklist", + "Consolidated PR checklist management (add, get, clear)", + ) + } + + async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { + let req: PrChecklistTool = serde_json::from_value(args).map_err(|e| e.to_string())?; + let ns = req.namespace.unwrap_or_else(|| crate::models::default_namespace()); + match req.action { + PrChecklistAction::Add => { + let desc = req.description.ok_or_else(|| { + crate::error::AppError::Internal("description is required for 'add' action".to_string()) + })?; + AddPrChecklistItemHandler.execute(serde_json::json!({"description": desc, "namespace": ns}), state).await + } + PrChecklistAction::Get => { + GetPrChecklistHandler.execute(serde_json::json!({"namespace": ns}), state).await + } + PrChecklistAction::Clear => { + ClearPrChecklistHandler.execute(serde_json::json!({"namespace": ns}), state).await + } + } + } +} + +pub struct SnippetsHandler; + +#[async_trait] +impl McpTool for SnippetsHandler { + fn name(&self) -> &'static str { + "snippets" + } + + fn schema(&self) -> Value { + crate::mcp::tool_def::( + "snippets", + "Consolidated reusable code snippets management (store, search, delete, tag)", + ) + } + + async fn execute(&self, args: Value, state: Arc) -> crate::error::Result { + let req: SnippetsTool = serde_json::from_value(args).map_err(|e| e.to_string())?; + match req.action { + SnippetAction::Store => { + let name = req.query.or(req.id).ok_or_else(|| { + crate::error::AppError::Internal("name is required for 'store' action".to_string()) + })?; + let lang = req.language.unwrap_or_else(|| "text".to_string()); + let code = req.code.unwrap_or_default(); + let desc = req.description.unwrap_or_default(); + StoreSnippetHandler.execute( + serde_json::json!({ + "name": name, + "language": lang, + "code": code, + "description": desc, + "tags": req.tags + }), + state, + ).await + } + SnippetAction::Search => { + let q = req.query.unwrap_or_default(); + if req.hybrid.unwrap_or(false) { + crate::handlers::meta::SearchSnippetsHybridHandler.execute(serde_json::json!({"query": q, "tags": req.tags}), state).await + } else { + SearchSnippetsHandler.execute(serde_json::json!({"query": q, "tags": req.tags}), state).await + } + } + SnippetAction::Delete => { + let id = req.id.or(req.query).ok_or_else(|| { + crate::error::AppError::Internal("id/name is required for 'delete' action".to_string()) + })?; + DeleteSnippetHandler.execute(serde_json::json!({"name": id}), state).await + } + SnippetAction::Tag => { + let id = req.id.or(req.query).ok_or_else(|| { + crate::error::AppError::Internal("id/name is required for 'tag' action".to_string()) + })?; + let tags = req.tags.unwrap_or_default(); + TagSnippetHandler.execute(serde_json::json!({"name": id, "tags": tags}), state).await + } + } + } +} + use crate::handlers::utils::*; #[cfg(test)] diff --git a/server/src/router.rs b/server/src/router.rs index d966544..4380323 100644 --- a/server/src/router.rs +++ b/server/src/router.rs @@ -493,6 +493,8 @@ impl MemoryHandler { register!(graph::MergeEntitiesHandler); register!(graph::FindOrphansHandler); + register!(tasks::TasksHandler); + register!(tasks::MilestonesHandler); register!(tasks::AddTaskHandler); register!(tasks::DeleteTaskHandler); register!(tasks::UpdateTaskStatusHandler); @@ -503,18 +505,18 @@ impl MemoryHandler { register!(tasks::UpdateMilestoneHandler); register!(tasks::ListMilestonesHandler); - register!(notes::ManageStickyNotesHandler); + register!(notes::StickyNotesHandler); + register!(notes::HandoffMemosHandler); register!(notes::AddStickyNoteHandler); register!(notes::ReadStickyNotesHandler); register!(notes::DeleteStickyNoteHandler); register!(notes::ClearStickyNotesHandler); - register!(notes::LeaveHandoffMemoHandler); - register!(notes::ReadHandoffMemosHandler); - register!(notes::ClearHandoffMemosHandler); register!(notes::AddSessionSummaryHandler); register!(notes::GenerateStandupReportHandler); register!(notes::PromoteToEntityHandler); + register!(meta::DecisionsHandler); + register!(meta::TechDebtHandler); register!(meta::LogDecisionHandler); register!(meta::QueryDecisionsHandler); register!(meta::DeleteDecisionHandler); @@ -530,12 +532,17 @@ impl MemoryHandler { register!(meta::OmniSearchHandler); register!(meta::GetProjectHealthHandler); + register!(env::EnvironmentHandler); register!(env::UpdateEnvFingerprintHandler); register!(env::ReadEnvFingerprintHandler); register!(env::LogEnvRequirementHandler); register!(env::RegisterEnvironmentHandler); register!(env::GetEnvironmentDetailsHandler); + register!(workspaces::PinnedFilesHandler); + register!(workspaces::ContextWorkspacesHandler); + register!(workspaces::PrChecklistHandler); + register!(workspaces::SnippetsHandler); register!(workspaces::PinFileHandler); register!(workspaces::UnpinFileHandler); register!(workspaces::ListPinnedFilesHandler); @@ -549,6 +556,7 @@ impl MemoryHandler { register!(workspaces::AddPrChecklistItemHandler); register!(workspaces::GetPrChecklistHandler); register!(workspaces::ClearPrChecklistHandler); + register!(vision::ClipboardHandler); register!(vision::ReadClipboardHandler); register!(vision::WriteClipboardHandler); register!(git::GetActiveWorktreeContextHandler); diff --git a/server/src/tools.rs b/server/src/tools.rs index d80291d..c0874ef 100644 --- a/server/src/tools.rs +++ b/server/src/tools.rs @@ -140,9 +140,9 @@ pub enum StickyNoteAction { Clear, } -/// Manage ephemeral sticky notes with TTL (add, read, delete, clear). +/// Ephemeral sticky notes with TTL (add, read, delete, clear). #[derive(Debug, Deserialize, Serialize, JsonSchema)] -pub struct ManageStickyNotesTool { +pub struct StickyNotesTool { /// Action to perform: 'add', 'read', 'delete', or 'clear'. pub action: StickyNoteAction, /// Content of the sticky note (required for 'add'). @@ -1083,3 +1083,304 @@ pub struct SearchSnippetsHybridTool { pub limit: Option, } +// Consolidated Smart Management Tools + +#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum HandoffMemoAction { + Leave, + Read, + Clear, +} + +/// Manage handoff memos across sessions (leave, read, clear). +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +pub struct HandoffMemosTool { + /// Action to perform: 'leave', 'read', or 'clear'. + pub action: HandoffMemoAction, + /// Content of the handoff memo (required for 'leave'). + pub content: Option, + /// Optional namespace filter or assignment. + pub namespace: Option, + /// Array of memo IDs to clear (required for 'clear'). + pub ids: Option>, +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum TaskAction { + Add, + Update, + Delete, + List, + SetCriteria, + Verify, +} + +/// Consolidated task management (add, update, delete, list, set_criteria, verify). +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +pub struct TasksTool { + /// Action to perform: 'add', 'update', 'delete', 'list', 'set_criteria', or 'verify'. + pub action: TaskAction, + /// Task ID (required for 'update', 'delete', 'set_criteria', 'verify'). + pub id: Option, + /// Task title (required for 'add'). + pub title: Option, + /// Task description (required for 'add'). + pub description: Option, + /// New status: 'pending', 'completed', or 'cancelled' (for 'update'). + pub status: Option, + /// Parent task ID (optional for 'add'). + pub parent_id: Option, + /// List of dependency task IDs (optional for 'add'). + pub dependencies: Option>, + /// Git branch filter or assignment. + pub git_branch: Option, + /// Acceptance criteria (required for 'set_criteria'). + pub criteria: Option>, + /// Summary level: 'compact', 'detailed', or 'full' (for 'list'). + pub summary_level: Option, + /// Maximum tokens budget cap (for 'list'). + pub max_tokens: Option, +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum MilestoneAction { + Add, + Update, + List, +} + +/// Consolidated milestone management (add, update, list). +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +pub struct MilestonesTool { + /// Action to perform: 'add', 'update', or 'list'. + pub action: MilestoneAction, + /// Milestone ID (required for 'update'). + pub id: Option, + /// Milestone title (required for 'add'). + pub title: Option, + /// New status (for 'update'). + pub status: Option, + /// Optional namespace filter or assignment. + pub namespace: Option, +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum PinnedFileAction { + Pin, + Unpin, + List, +} + +/// Consolidated pinned files focus management (pin, unpin, list). +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +pub struct PinnedFilesTool { + /// Action to perform: 'pin', 'unpin', or 'list'. + pub action: PinnedFileAction, + /// Absolute or workspace relative file path (required for 'pin' and 'unpin'). + pub path: Option, + /// Optional namespace. + pub namespace: Option, +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum ContextWorkspaceAction { + Save, + Load, + List, + Delete, + Diff, +} + +/// Consolidated context workspace state management (save, load, list, delete, diff). +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +pub struct ContextWorkspacesTool { + /// Action to perform: 'save', 'load', 'list', 'delete', or 'diff'. + pub action: ContextWorkspaceAction, + /// Workspace name. + pub name: Option, + /// Second workspace name (for 'diff'). + pub other_name: Option, + /// Pinned file paths (for 'save'). + pub pinned_files: Option>, + /// Active task IDs (for 'save'). + pub active_task_ids: Option>, + /// Optional namespace. + pub namespace: Option, +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum PrChecklistAction { + Add, + Get, + Clear, +} + +/// Consolidated PR checklist management (add, get, clear). +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +pub struct PrChecklistTool { + /// Action to perform: 'add', 'get', or 'clear'. + pub action: PrChecklistAction, + /// Checklist item description (required for 'add'). + pub description: Option, + /// Optional namespace. + pub namespace: Option, +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum SnippetAction { + Store, + Search, + Delete, + Tag, +} + +/// Consolidated reusable code snippets management (store, search, delete, tag). +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +pub struct SnippetsTool { + /// Action to perform: 'store', 'search', 'delete', or 'tag'. + pub action: SnippetAction, + /// Snippet ID (required for 'delete', 'tag'). + pub id: Option, + /// Search query or snippet name. + pub query: Option, + /// Snippet language (for 'store'). + pub language: Option, + /// Code content (for 'store'). + pub code: Option, + /// Snippet description (for 'store'). + pub description: Option, + /// Classification tags (for 'store', 'search', 'tag'). + pub tags: Option>, + /// Enable hybrid lexical + semantic vector ranking (for 'search'). + pub hybrid: Option, +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum DecisionAction { + Log, + Query, + Delete, +} + +/// Consolidated Architectural Decision Records (ADRs) management (log, query, delete). +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +pub struct DecisionsTool { + /// Action to perform: 'log', 'query', or 'delete'. + pub action: DecisionAction, + /// ADR ID (required for 'delete'). + pub id: Option, + /// ADR title (required for 'log'). + pub title: Option, + /// ADR status (required for 'log'). + pub status: Option, + /// Context & problem statement (required for 'log'). + pub context: Option, + /// Decision made (required for 'log'). + pub decision: Option, + /// Consequences & tradeoffs (required for 'log'). + pub consequences: Option, + /// Search query string (for 'query'). + pub query: Option, + /// Optional namespace. + pub namespace: Option, +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum TechDebtAction { + Log, + Resolve, + List, +} + +/// Consolidated technical debt management (log, resolve, list). +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +pub struct TechDebtTool { + /// Action to perform: 'log', 'resolve', or 'list'. + pub action: TechDebtAction, + /// Tech debt ID (required for 'resolve'). + pub id: Option, + /// Tech debt description (required for 'log'). + pub description: Option, + /// Ideal solution (required for 'log'). + pub ideal_solution: Option, + /// Include resolved tech debt items (for 'list'). + pub include_resolved: Option, + /// Associated git commit hash. + pub git_commit: Option, + /// Associated git branch. + pub git_branch: Option, + /// Symbol references associated with the tech debt. + pub symbol_references: Option>, + /// Line range associated with the tech debt. + pub line_range: Option, + /// Optional namespace. + pub namespace: Option, +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum EnvAction { + UpdateFingerprint, + ReadFingerprint, + LogRequirement, + Register, + GetDetails, +} + +/// Consolidated environment & infrastructure management (update_fingerprint, read_fingerprint, log_requirement, register, get_details). +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +pub struct EnvironmentTool { + /// Action to perform. + pub action: EnvAction, + /// Map of tool names to versions (for 'update_fingerprint'). + pub tool_versions: Option>, + /// Env variable key (for 'log_requirement'). + pub key: Option, + /// Description (for 'log_requirement', 'register'). + pub description: Option, + /// Secret flag (for 'log_requirement'). + pub is_secret: Option, + /// Environment name (for 'register'). + pub name: Option, + /// Environment URL (for 'register'). + pub url: Option, + /// VPN required flag (for 'register'). + pub requires_vpn: Option, + /// Optional namespace. + pub namespace: Option, +} + +#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum ClipboardAction { + Read, + Write, + ToggleWatch, +} + +/// Consolidated OS clipboard management (read, write, toggle_watch). +#[derive(Debug, Deserialize, Serialize, JsonSchema)] +pub struct ClipboardTool { + /// Action to perform: 'read', 'write', or 'toggle_watch'. + pub action: ClipboardAction, + /// Plain text content (for 'write'). + pub text: Option, + /// HTML content (for 'write'). + pub html: Option, + /// File paths for Windows File Drop (for 'write'). + pub files: Option>, + /// Image path (for 'write'). + pub image_path: Option, + /// Enable watch mode flag (for 'toggle_watch'). + pub enabled: Option, +} +