refactor: eradicate deprecated tools (sticky_notes, pinned_files, context_workspaces, pr_checklist, preferences) and dead code
This commit is contained in:
1 parent
d80915635f
commit
79209da711
31 files changed
+1230
-3920
No files matched your search
@@ -26,11 +26,15 @@ pub async fn wait_for_event_handler(
|
||||
return axum::Json(event);
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
Err(tokio::sync::broadcast::error::RecvError::Lagged(skipped)) => {
|
||||
tracing::warn!("Event bus receiver lagged by {} messages; continuing wait.", skipped);
|
||||
continue;
|
||||
}
|
||||
Err(tokio::sync::broadcast::error::RecvError::Closed) => {
|
||||
return axum::Json(GenericEvent {
|
||||
topic: "error".to_string(),
|
||||
session_id: None,
|
||||
payload: serde_json::json!({"error": "Event bus lagged or closed"}),
|
||||
payload: serde_json::json!({"error": "Event bus closed"}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,18 +237,14 @@ async fn run_server(state: Arc<MemoryState>) -> Result<(), Box<dyn std::error::E
|
||||
let adrs = state_clone.adrs.read_with(|items| items.len());
|
||||
|
||||
let ledger = state_clone.ledger.read_with(|items| items.len());
|
||||
let sticky = state_clone.sticky.read_with(|items| items.len());
|
||||
let error_fixes = state_clone.error_fixes.read_with(|items| items.len());
|
||||
let pinned_files = state_clone.pinned_files.read_with(|items| items.len());
|
||||
let session_summaries = state_clone.session_summaries.read_with(|items| items.len());
|
||||
let handoff_memos = state_clone.handoff_memos.read_with(|items| items.len());
|
||||
let env_fingerprints = state_clone.env_fingerprints.read_with(|items| items.len());
|
||||
let env_requirements = state_clone.env_requirements.read_with(|items| items.len());
|
||||
let milestones = state_clone.milestones.read_with(|items| items.len());
|
||||
let environments = state_clone.environments.read_with(|items| items.len());
|
||||
let pr_checklists = state_clone.pr_checklists.read_with(|items| items.len());
|
||||
let gates = state_clone.gates.read_with(|items| items.len());
|
||||
let context_workspaces = state_clone.context_workspaces.read_with(|items| items.len());
|
||||
|
||||
axum::Json(serde_json::json!({
|
||||
"entities": entities,
|
||||
@@ -258,18 +254,14 @@ async fn run_server(state: Arc<MemoryState>) -> Result<(), Box<dyn std::error::E
|
||||
"tech_debts": tech_debts,
|
||||
"adrs": adrs,
|
||||
"ledger": ledger,
|
||||
"sticky": sticky,
|
||||
"error_fixes": error_fixes,
|
||||
"pinned_files": pinned_files,
|
||||
"session_summaries": session_summaries,
|
||||
"handoff_memos": handoff_memos,
|
||||
"env_fingerprints": env_fingerprints,
|
||||
"env_requirements": env_requirements,
|
||||
"milestones": milestones,
|
||||
"environments": environments,
|
||||
"pr_checklists": pr_checklists,
|
||||
"gates": gates,
|
||||
"context_workspaces": context_workspaces
|
||||
"gates": gates
|
||||
}))
|
||||
}
|
||||
}),
|
||||
|
||||
+2
-42
@@ -205,16 +205,7 @@ pub fn create_router(app_state: Arc<AppState>) -> Router {
|
||||
}
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/api/sticky",
|
||||
get({
|
||||
let state_clone = app_state.handler.state.clone();
|
||||
move || async move {
|
||||
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)
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
.route(
|
||||
"/api/search",
|
||||
get({
|
||||
@@ -264,16 +255,6 @@ pub fn create_router(app_state: Arc<AppState>) -> Router {
|
||||
}
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/api/context_workspaces",
|
||||
get({
|
||||
let state_clone = app_state.handler.state.clone();
|
||||
move || async move {
|
||||
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)
|
||||
}
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/api/handoff_memos",
|
||||
get({
|
||||
@@ -304,16 +285,6 @@ pub fn create_router(app_state: Arc<AppState>) -> Router {
|
||||
}
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/api/pr_checklists",
|
||||
get({
|
||||
let state_clone = app_state.handler.state.clone();
|
||||
move || async move {
|
||||
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)
|
||||
}
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/api/error_fixes",
|
||||
get({
|
||||
@@ -336,18 +307,14 @@ pub fn create_router(app_state: Arc<AppState>) -> Router {
|
||||
let adrs = state_clone.code.adrs.read_with(|items| items.len());
|
||||
|
||||
let ledger = state_clone.code.ledger.read_with(|items| items.len());
|
||||
let sticky = state_clone.code.sticky.read_with(|items| items.len());
|
||||
let error_fixes = state_clone.code.error_fixes.read_with(|items| items.len());
|
||||
let pinned_files = state_clone.project.pinned_files.read_with(|items| items.len());
|
||||
let session_summaries = state_clone.telemetry.session_summaries.read_with(|items| items.len());
|
||||
let handoff_memos = state_clone.telemetry.handoff_memos.read_with(|items| items.len());
|
||||
let env_fingerprints = state_clone.env.env_fingerprints.read_with(|items| items.len());
|
||||
let env_requirements = state_clone.env.env_requirements.read_with(|items| items.len());
|
||||
let milestones = state_clone.project.milestones.read_with(|items| items.len());
|
||||
let environments = state_clone.env.environments.read_with(|items| items.len());
|
||||
let pr_checklists = state_clone.project.pr_checklists.read_with(|items| items.len());
|
||||
let gates = state_clone.env.gates.read_with(|items| items.len());
|
||||
let context_workspaces = state_clone.project.context_workspaces.read_with(|items| items.len());
|
||||
|
||||
axum::Json(serde_json::json!({
|
||||
"entities": entities,
|
||||
@@ -357,18 +324,14 @@ pub fn create_router(app_state: Arc<AppState>) -> Router {
|
||||
"tech_debts": tech_debts,
|
||||
"adrs": adrs,
|
||||
"ledger": ledger,
|
||||
"sticky": sticky,
|
||||
"error_fixes": error_fixes,
|
||||
"pinned_files": pinned_files,
|
||||
"session_summaries": session_summaries,
|
||||
"handoff_memos": handoff_memos,
|
||||
"env_fingerprints": env_fingerprints,
|
||||
"env_requirements": env_requirements,
|
||||
"milestones": milestones,
|
||||
"environments": environments,
|
||||
"pr_checklists": pr_checklists,
|
||||
"gates": gates,
|
||||
"context_workspaces": context_workspaces
|
||||
"gates": gates
|
||||
}))
|
||||
}
|
||||
}),
|
||||
@@ -640,15 +603,12 @@ mod tests {
|
||||
"/",
|
||||
"/api/graph",
|
||||
"/api/tasks",
|
||||
"/api/sticky",
|
||||
"/api/search?q=test",
|
||||
"/api/tech_debts",
|
||||
"/api/adrs",
|
||||
"/api/context_workspaces",
|
||||
"/api/handoff_memos",
|
||||
"/api/milestones",
|
||||
"/api/snippets",
|
||||
"/api/pr_checklists",
|
||||
"/api/error_fixes",
|
||||
"/api/stats",
|
||||
];
|
||||
|
||||
@@ -127,23 +127,18 @@ pub fn init_redb(base: &Path) -> Arc<Database> {
|
||||
let stores = vec![
|
||||
("knowledge_graph_master", "knowledge_graph_master.json"),
|
||||
("audit_ledger", "audit_ledger.json"),
|
||||
("sticky_notes", "sticky_notes.json"),
|
||||
("tasks", "tasks.json"),
|
||||
("snippets", "snippets.json"),
|
||||
("adrs", "adrs.json"),
|
||||
("preferences", "preferences.json"),
|
||||
("error_fixes", "error_fixes.json"),
|
||||
("pinned_files", "pinned_files.json"),
|
||||
("session_summaries", "session_summaries.json"),
|
||||
("handoff_memos", "handoff_memos.json"),
|
||||
("env_fingerprints", "env_fingerprints.json"),
|
||||
("env_requirements", "env_requirements.json"),
|
||||
("milestones", "milestones.json"),
|
||||
("environments", "environments.json"),
|
||||
("pr_checklists", "pr_checklists.json"),
|
||||
("tech_debts", "tech_debts.json"),
|
||||
("gates", "gates.json"),
|
||||
("context_workspaces", "context_workspaces.json"),
|
||||
("state_snapshots", "state_snapshots.json"),
|
||||
("hypotheses", "hypotheses.json"),
|
||||
("agent_signals", "agent_signals.json"),
|
||||
|
||||
@@ -280,12 +280,16 @@ pub fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
|
||||
norm_b_sq += y * y;
|
||||
}
|
||||
|
||||
let norm_a = norm_a_sq.sqrt();
|
||||
let norm_b = norm_b_sq.sqrt();
|
||||
if norm_a == 0.0 || norm_b == 0.0 {
|
||||
// Fast path: If vectors are already normalized (Candle & fallback embeddings), skip square roots
|
||||
if (norm_a_sq - 1.0).abs() < 1e-4 && (norm_b_sq - 1.0).abs() < 1e-4 {
|
||||
return dot_product.clamp(-1.0, 1.0);
|
||||
}
|
||||
|
||||
let norm_product = norm_a_sq * norm_b_sq;
|
||||
if norm_product <= 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
dot_product / (norm_a * norm_b)
|
||||
(dot_product / norm_product.sqrt()).clamp(-1.0, 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+67
-46
@@ -81,27 +81,64 @@ impl McpTool for ReadFileSkeletonHandler {
|
||||
return;
|
||||
}
|
||||
let kind = node.kind();
|
||||
|
||||
let is_structural = matches!(
|
||||
let is_container = 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"
|
||||
"impl_item"
|
||||
| "class_declaration"
|
||||
| "class_definition"
|
||||
| "trait_item"
|
||||
| "interface_declaration"
|
||||
);
|
||||
|
||||
if is_structural {
|
||||
let is_structural = is_container
|
||||
|| matches!(
|
||||
kind,
|
||||
"use_declaration"
|
||||
| "import_statement"
|
||||
| "import_from_statement"
|
||||
| "struct_item"
|
||||
| "enum_item"
|
||||
| "function_item"
|
||||
| "function_declaration"
|
||||
| "function_definition"
|
||||
| "method_definition"
|
||||
| "type_alias_declaration"
|
||||
);
|
||||
|
||||
if is_container {
|
||||
let indent = " ".repeat(depth);
|
||||
let node_text = node.utf8_text(code.as_bytes()).unwrap_or("");
|
||||
let mut header = String::new();
|
||||
for line in node_text.lines() {
|
||||
let trimmed = line.trim();
|
||||
if trimmed.ends_with('{') || trimmed.ends_with(':') {
|
||||
header.push_str(line);
|
||||
break;
|
||||
} else {
|
||||
header.push_str(line);
|
||||
header.push('\n');
|
||||
}
|
||||
}
|
||||
if header.is_empty() {
|
||||
if let Some(first_line) = node_text.lines().next() {
|
||||
header = first_line.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
out.push_str(&indent);
|
||||
out.push_str(header.trim());
|
||||
out.push('\n');
|
||||
|
||||
let mut cursor = node.walk();
|
||||
for child in node.named_children(&mut cursor) {
|
||||
extract_skeleton(child, code, out, depth + 1);
|
||||
}
|
||||
|
||||
if header.trim().ends_with('{') {
|
||||
out.push_str(&indent);
|
||||
out.push_str("}\n");
|
||||
}
|
||||
} else if is_structural {
|
||||
let indent = " ".repeat(depth);
|
||||
|
||||
let node_text = node.utf8_text(code.as_bytes()).unwrap_or("");
|
||||
@@ -127,7 +164,7 @@ impl McpTool for ReadFileSkeletonHandler {
|
||||
} else if node.is_named() {
|
||||
let mut cursor = node.walk();
|
||||
for child in node.named_children(&mut cursor) {
|
||||
extract_skeleton(child, code, out, depth + 1);
|
||||
extract_skeleton(child, code, out, depth);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -231,6 +268,7 @@ impl McpTool for ReplaceAstNodeHandler {
|
||||
actual_kind,
|
||||
"trait_item" | "interface_declaration" | "interface_item"
|
||||
),
|
||||
"impl" | "impl_item" => actual_kind == "impl_item",
|
||||
"type" | "type_alias" => matches!(
|
||||
actual_kind,
|
||||
"type_alias_declaration" | "type_item" | "type_definition"
|
||||
@@ -253,6 +291,7 @@ impl McpTool for ReplaceAstNodeHandler {
|
||||
let kind = child.kind();
|
||||
if kind == "identifier"
|
||||
|| kind == "name"
|
||||
|| kind == "type_identifier"
|
||||
|| kind == "property_identifier"
|
||||
|| kind == "field_identifier"
|
||||
{
|
||||
@@ -386,6 +425,11 @@ fn scan_workspace_for_symbol(
|
||||
if scanned_files > 500 {
|
||||
break;
|
||||
}
|
||||
if let Ok(meta) = std::fs::metadata(path) {
|
||||
if meta.len() > 1024 * 1024 {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if let Ok(content) = std::fs::read_to_string(path) {
|
||||
for (line_num, line) in content.lines().enumerate() {
|
||||
let is_match = if filter_fn_call {
|
||||
@@ -450,17 +494,7 @@ impl McpTool for FindSymbolReferencesHandler {
|
||||
Ok::<Vec<serde_json::Value>, crate::error::AppError>(refs)
|
||||
})?;
|
||||
|
||||
let custom_dir = if let Some(ref d) = req.workspace_dir {
|
||||
Some(std::path::PathBuf::from(d))
|
||||
} else {
|
||||
state.project.pinned_files.read_with(|pinned| {
|
||||
pinned.first().and_then(|pin| {
|
||||
std::path::Path::new(&pin.file_path)
|
||||
.parent()
|
||||
.map(|p| p.to_path_buf())
|
||||
})
|
||||
})
|
||||
};
|
||||
let custom_dir = req.workspace_dir.as_ref().map(std::path::PathBuf::from);
|
||||
|
||||
if matches.len() < limit {
|
||||
let remaining = limit - matches.len();
|
||||
@@ -516,17 +550,7 @@ impl McpTool for GetCallersHandler {
|
||||
Ok::<Vec<serde_json::Value>, crate::error::AppError>(matching)
|
||||
})?;
|
||||
|
||||
let custom_dir = if let Some(ref d) = req.workspace_dir {
|
||||
Some(std::path::PathBuf::from(d))
|
||||
} else {
|
||||
state.project.pinned_files.read_with(|pinned| {
|
||||
pinned.first().and_then(|pin| {
|
||||
std::path::Path::new(&pin.file_path)
|
||||
.parent()
|
||||
.map(|p| p.to_path_buf())
|
||||
})
|
||||
})
|
||||
};
|
||||
let custom_dir = req.workspace_dir.as_ref().map(std::path::PathBuf::from);
|
||||
|
||||
if callers.len() < limit {
|
||||
let remaining = limit - callers.len();
|
||||
@@ -571,13 +595,10 @@ impl McpTool for AnalyzeImpactHandler {
|
||||
}
|
||||
});
|
||||
|
||||
let custom_dir = state.project.pinned_files.read_with(|pinned| {
|
||||
pinned.first().and_then(|pin| {
|
||||
std::path::Path::new(&pin.file_path)
|
||||
.parent()
|
||||
.map(|p| p.to_path_buf())
|
||||
})
|
||||
});
|
||||
let custom_dir = req
|
||||
.file_path
|
||||
.as_ref()
|
||||
.and_then(|p| std::path::Path::new(p).parent().map(|p| p.to_path_buf()));
|
||||
|
||||
let sym_clone = sym.clone();
|
||||
let disk_refs = tokio::task::spawn_blocking(move || {
|
||||
|
||||
+74
-183
@@ -5,166 +5,6 @@ use async_trait::async_trait;
|
||||
use serde_json::Value;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct UpdateEnvFingerprintHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for UpdateEnvFingerprintHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"update_env_fingerprint"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<UpdateEnvFingerprintTool>(
|
||||
"update_env_fingerprint",
|
||||
"Update system OS, shell, and installed tool version fingerprints for a workspace namespace.",
|
||||
)
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||
let req: UpdateEnvFingerprintTool =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
state.env.env_fingerprints.modify(|fps| {
|
||||
fps.insert(
|
||||
req.namespace.clone(),
|
||||
crate::models::EnvFingerprint {
|
||||
namespace: req.namespace.clone(),
|
||||
os: std::env::consts::OS.to_string(),
|
||||
shell: std::env::var("SHELL").unwrap_or_else(|_| "unknown".to_string()),
|
||||
tool_versions: req.tool_versions,
|
||||
updated_at: crate::handlers::utils::now_secs(),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
});
|
||||
Ok("Env fingerprint updated".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ReadEnvFingerprintHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for ReadEnvFingerprintHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"read_env_fingerprint"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<ReadEnvFingerprintTool>(
|
||||
"read_env_fingerprint",
|
||||
"Read system OS, shell, and tool version fingerprints for a workspace namespace.",
|
||||
)
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||
let req: ReadEnvFingerprintTool =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let data = state
|
||||
.env
|
||||
.env_fingerprints
|
||||
.read_with(|fps| fps.get(&req.namespace).cloned());
|
||||
if let Some(fp) = data {
|
||||
let data = Ok::<String, crate::error::AppError>(serde_json::to_string(&fp)?)?;
|
||||
Ok(data)
|
||||
} else {
|
||||
Ok("{}".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LogEnvRequirementHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for LogEnvRequirementHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"log_env_requirement"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<LogEnvRequirementTool>(
|
||||
"log_env_requirement",
|
||||
"Log required environment variables, configuration keys, or secrets for a namespace.",
|
||||
)
|
||||
}
|
||||
|
||||
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())?;
|
||||
state.env.env_requirements.modify(|reqs| {
|
||||
reqs.retain(|r| !(r.namespace == req.namespace && r.key == req.key));
|
||||
reqs.push(crate::models::EnvRequirement {
|
||||
namespace: req.namespace,
|
||||
key: req.key,
|
||||
description: req.description,
|
||||
is_secret: req.is_secret,
|
||||
..Default::default()
|
||||
});
|
||||
});
|
||||
Ok("Env requirement logged".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RegisterEnvironmentHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for RegisterEnvironmentHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"register_environment"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<RegisterEnvironmentTool>(
|
||||
"register_environment",
|
||||
"Register infrastructure environment details (URL, VPN requirements, notes) for a namespace.",
|
||||
)
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||
let req: RegisterEnvironmentTool =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
state.env.environments.modify(|envs| {
|
||||
envs.retain(|e| !(e.namespace == req.namespace && e.name == req.name));
|
||||
envs.push(crate::models::EnvironmentDetail {
|
||||
namespace: req.namespace,
|
||||
name: req.name,
|
||||
url: req.url,
|
||||
description: req.description,
|
||||
requires_vpn: req.requires_vpn,
|
||||
updated_at: crate::handlers::utils::now_secs(),
|
||||
..Default::default()
|
||||
});
|
||||
});
|
||||
Ok("Environment registered".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct GetEnvironmentDetailsHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for GetEnvironmentDetailsHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"get_environment_details"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<GetEnvironmentDetailsTool>(
|
||||
"get_environment_details",
|
||||
"Retrieve registered infrastructure environment details for a namespace.",
|
||||
)
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||
let req: GetEnvironmentDetailsTool =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let data = state.env.environments.read_with(|envs| {
|
||||
let filtered: Vec<_> = envs
|
||||
.iter()
|
||||
.filter(|e| e.namespace == req.namespace)
|
||||
.collect();
|
||||
Ok::<String, crate::error::AppError>(serde_json::to_string(&filtered)?)
|
||||
})?;
|
||||
Ok(data)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EnvironmentHandler;
|
||||
|
||||
#[async_trait]
|
||||
@@ -186,10 +26,32 @@ impl McpTool for EnvironmentHandler {
|
||||
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
|
||||
state.env.env_fingerprints.modify(|fps| {
|
||||
fps.insert(
|
||||
ns.clone(),
|
||||
crate::models::EnvFingerprint {
|
||||
namespace: ns.clone(),
|
||||
os: std::env::consts::OS.to_string(),
|
||||
shell: std::env::var("SHELL").unwrap_or_else(|_| "unknown".to_string()),
|
||||
tool_versions,
|
||||
updated_at: crate::handlers::utils::now_secs(),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
});
|
||||
Ok("Env fingerprint updated".to_string())
|
||||
}
|
||||
EnvAction::ReadFingerprint => {
|
||||
ReadEnvFingerprintHandler.execute(serde_json::json!({"namespace": ns}), state).await
|
||||
let data = state
|
||||
.env
|
||||
.env_fingerprints
|
||||
.read_with(|fps| fps.get(&ns).cloned());
|
||||
if let Some(fp) = data {
|
||||
let data = Ok::<String, crate::error::AppError>(serde_json::to_string(&fp)?)?;
|
||||
Ok(data)
|
||||
} else {
|
||||
Ok("{}".to_string())
|
||||
}
|
||||
}
|
||||
EnvAction::LogRequirement => {
|
||||
let key = req.key.ok_or_else(|| {
|
||||
@@ -197,7 +59,17 @@ impl McpTool for EnvironmentHandler {
|
||||
})?;
|
||||
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
|
||||
state.env.env_requirements.modify(|reqs| {
|
||||
reqs.retain(|r| !(r.namespace == ns && r.key == key));
|
||||
reqs.push(crate::models::EnvRequirement {
|
||||
namespace: ns,
|
||||
key,
|
||||
description: desc,
|
||||
is_secret: secret,
|
||||
..Default::default()
|
||||
});
|
||||
});
|
||||
Ok("Env requirement logged".to_string())
|
||||
}
|
||||
EnvAction::Register => {
|
||||
let name = req.name.ok_or_else(|| {
|
||||
@@ -206,15 +78,35 @@ impl McpTool for EnvironmentHandler {
|
||||
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
|
||||
state.env.environments.modify(|envs| {
|
||||
envs.retain(|e| !(e.namespace == ns && e.name == name));
|
||||
envs.push(crate::models::EnvironmentDetail {
|
||||
namespace: ns,
|
||||
name,
|
||||
url,
|
||||
description: desc,
|
||||
requires_vpn: vpn,
|
||||
updated_at: crate::handlers::utils::now_secs(),
|
||||
..Default::default()
|
||||
});
|
||||
});
|
||||
Ok("Environment registered".to_string())
|
||||
}
|
||||
EnvAction::GetDetails => {
|
||||
GetEnvironmentDetailsHandler.execute(serde_json::json!({"namespace": ns}), state).await
|
||||
let data = state.env.environments.read_with(|envs| {
|
||||
let filtered: Vec<_> = envs
|
||||
.iter()
|
||||
.filter(|e| e.namespace == ns)
|
||||
.collect();
|
||||
Ok::<String, crate::error::AppError>(serde_json::to_string(&filtered)?)
|
||||
})?;
|
||||
Ok(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -225,25 +117,25 @@ mod tests {
|
||||
async fn test_env_fingerprint() {
|
||||
let dir = tempdir().unwrap();
|
||||
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
|
||||
let handler = EnvironmentHandler;
|
||||
|
||||
let update_handler = UpdateEnvFingerprintHandler;
|
||||
let args = json!({
|
||||
"action": "update_fingerprint",
|
||||
"namespace": "global",
|
||||
"tool_versions": {
|
||||
"rustc": "1.70.0"
|
||||
}
|
||||
});
|
||||
|
||||
let res = update_handler
|
||||
let res = handler
|
||||
.execute(args, state.clone())
|
||||
.await
|
||||
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
||||
.unwrap();
|
||||
assert_eq!(res, "Env fingerprint updated");
|
||||
|
||||
let read_handler = ReadEnvFingerprintHandler;
|
||||
let res2 = read_handler
|
||||
.execute(json!({"namespace": "global"}), state.clone())
|
||||
let res2 = handler
|
||||
.execute(json!({"action": "read_fingerprint", "namespace": "global"}), state.clone())
|
||||
.await
|
||||
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
||||
.unwrap();
|
||||
@@ -269,9 +161,9 @@ mod tests {
|
||||
});
|
||||
});
|
||||
|
||||
let handler = GetEnvironmentDetailsHandler;
|
||||
let handler = EnvironmentHandler;
|
||||
let res = handler
|
||||
.execute(json!({"namespace": "global"}), state.clone())
|
||||
.execute(json!({"action": "get_details", "namespace": "global"}), state.clone())
|
||||
.await
|
||||
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
||||
.unwrap();
|
||||
@@ -283,41 +175,39 @@ mod tests {
|
||||
async fn test_env_requirements_and_registration() {
|
||||
let dir = tempdir().unwrap();
|
||||
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
|
||||
let handler = EnvironmentHandler;
|
||||
|
||||
let req_handler = LogEnvRequirementHandler;
|
||||
let args_req = json!({
|
||||
"action": "log_requirement",
|
||||
"key": "vpn",
|
||||
"description": "Requires VPN",
|
||||
"is_secret": false,
|
||||
"requirement": "Needs specific VPN",
|
||||
"context": "For database access",
|
||||
"namespace": "global"
|
||||
});
|
||||
let res1 = req_handler
|
||||
let res1 = handler
|
||||
.execute(args_req, state.clone())
|
||||
.await
|
||||
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
||||
.unwrap();
|
||||
assert_eq!(res1, "Env requirement logged");
|
||||
|
||||
let reg_handler = RegisterEnvironmentHandler;
|
||||
let args_reg = json!({
|
||||
"action": "register",
|
||||
"name": "prod",
|
||||
"url": "https://prod.local",
|
||||
"description": "Production",
|
||||
"requires_vpn": true,
|
||||
"namespace": "global"
|
||||
});
|
||||
let res2 = reg_handler
|
||||
let res2 = handler
|
||||
.execute(args_reg, state.clone())
|
||||
.await
|
||||
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
||||
.unwrap();
|
||||
assert_eq!(res2, "Environment registered");
|
||||
|
||||
let get_handler = GetEnvironmentDetailsHandler;
|
||||
let res3 = get_handler
|
||||
.execute(json!({"namespace": "global"}), state.clone())
|
||||
let res3 = handler
|
||||
.execute(json!({"action": "get_details", "namespace": "global"}), state.clone())
|
||||
.await
|
||||
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
||||
.unwrap();
|
||||
@@ -329,16 +219,17 @@ mod tests {
|
||||
async fn test_empty_namespace_in_update_env_fingerprint() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
|
||||
let handler = EnvironmentHandler;
|
||||
|
||||
let update_handler = UpdateEnvFingerprintHandler;
|
||||
let args = serde_json::json!({
|
||||
"action": "update_fingerprint",
|
||||
"namespace": "",
|
||||
"tool_versions": {
|
||||
"rustc": "1.70.0"
|
||||
}
|
||||
});
|
||||
|
||||
let res = update_handler
|
||||
let res = handler
|
||||
.execute(args, state.clone())
|
||||
.await;
|
||||
|
||||
|
||||
+122
-95
@@ -459,14 +459,15 @@ impl McpTool for ReadGraphHandler {
|
||||
let offset = req.offset.unwrap_or(0);
|
||||
|
||||
let data = state.read_graph(|full| -> crate::error::Result<String> {
|
||||
let mut result_json = if limit.is_some() || req.offset.is_some() {
|
||||
let filtered_entities: Vec<(&String, &crate::models::Entity)> = full
|
||||
.entities
|
||||
.iter()
|
||||
.filter(|(_, v)| req.namespace.as_ref().map_or(true, |ns| &v.namespace == ns))
|
||||
.collect();
|
||||
let filtered_entities: Vec<(&String, &crate::models::Entity)> = full
|
||||
.entities
|
||||
.iter()
|
||||
.filter(|(_, v)| req.namespace.as_ref().map_or(true, |ns| &v.namespace == ns))
|
||||
.collect();
|
||||
|
||||
let total_count = filtered_entities.len();
|
||||
let total_count = filtered_entities.len();
|
||||
|
||||
let (entities_map, relations_vec, meta_json) = if limit.is_some() || req.offset.is_some() {
|
||||
let effective_limit = limit.unwrap_or(50);
|
||||
let paged_entities: std::collections::HashMap<&String, &crate::models::Entity> = filtered_entities
|
||||
.into_iter()
|
||||
@@ -485,45 +486,94 @@ impl McpTool for ReadGraphHandler {
|
||||
})
|
||||
.collect();
|
||||
|
||||
serde_json::to_string(&serde_json::json!({
|
||||
"entities": paged_entities,
|
||||
"relations": relevant_relations,
|
||||
let meta = serde_json::json!({
|
||||
"total_count": total_count,
|
||||
"offset": offset,
|
||||
"limit": effective_limit,
|
||||
"has_more": has_more,
|
||||
}))?
|
||||
} else if let Some(ns) = req.namespace {
|
||||
let mut filtered = BorrowedGraph::default();
|
||||
for (k, v) in &full.entities {
|
||||
if v.namespace == ns {
|
||||
filtered.entities.insert(k, v);
|
||||
}
|
||||
}
|
||||
for r in &full.relations {
|
||||
if r.namespace == ns {
|
||||
filtered.relations.push(r);
|
||||
}
|
||||
}
|
||||
serde_json::to_string(&filtered)?
|
||||
});
|
||||
(paged_entities, relevant_relations, Some(meta))
|
||||
} else {
|
||||
serde_json::to_string(full)?
|
||||
let ent_map: std::collections::HashMap<&String, &crate::models::Entity> =
|
||||
filtered_entities.into_iter().collect();
|
||||
let rel_vec: Vec<&crate::models::Relation> = full
|
||||
.relations
|
||||
.iter()
|
||||
.filter(|r| req.namespace.as_ref().map_or(true, |ns| &r.namespace == ns))
|
||||
.collect();
|
||||
(ent_map, rel_vec, None)
|
||||
};
|
||||
|
||||
let serialized = if let Some(meta) = &meta_json {
|
||||
serde_json::json!({
|
||||
"entities": entities_map,
|
||||
"relations": relations_vec,
|
||||
"total_count": meta["total_count"],
|
||||
"offset": meta["offset"],
|
||||
"limit": meta["limit"],
|
||||
"has_more": meta["has_more"],
|
||||
})
|
||||
} else {
|
||||
serde_json::json!({
|
||||
"entities": entities_map,
|
||||
"relations": relations_vec,
|
||||
})
|
||||
};
|
||||
|
||||
let mut result_json = serde_json::to_string(&serialized)?;
|
||||
|
||||
if let Some(max_tok) = max_tokens {
|
||||
let max_chars = max_tok * 4;
|
||||
if result_json.len() > max_chars {
|
||||
let valid_boundary = result_json.floor_char_boundary(max_chars);
|
||||
result_json.truncate(valid_boundary);
|
||||
result_json.push_str("\n... [TRUNCATED_TO_MAX_TOKENS. Use search_nodes, pagination (limit, offset), or get_subgraph for targeted discovery]");
|
||||
let total_ent = entities_map.len();
|
||||
let total_rel = relations_vec.len();
|
||||
let frac = (max_chars as f64) / (result_json.len() as f64);
|
||||
let mut keep_count = (total_ent as f64 * frac * 0.85).max(1.0) as usize;
|
||||
|
||||
let ent_vec: Vec<_> = entities_map.into_iter().collect();
|
||||
|
||||
while keep_count > 0 {
|
||||
let sliced_ent: std::collections::HashMap<_, _> = ent_vec.iter().take(keep_count).cloned().collect();
|
||||
let sliced_rel: Vec<_> = relations_vec.iter().filter(|r| sliced_ent.contains_key(&r.from) && sliced_ent.contains_key(&r.to)).copied().collect();
|
||||
|
||||
let mut truncated_val = serde_json::json!({
|
||||
"entities": sliced_ent,
|
||||
"relations": sliced_rel,
|
||||
"_meta": {
|
||||
"truncated": true,
|
||||
"total_entities": total_ent,
|
||||
"returned_entities": sliced_ent.len(),
|
||||
"total_relations": total_rel,
|
||||
"returned_relations": sliced_rel.len(),
|
||||
"note": "Output structurally truncated to fit within max_tokens. Use pagination (limit, offset), search_nodes, or get_subgraph for targeted discovery."
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(meta) = &meta_json {
|
||||
if let Some(obj) = truncated_val.as_object_mut() {
|
||||
obj.insert("total_count".to_string(), meta["total_count"].clone());
|
||||
obj.insert("offset".to_string(), meta["offset"].clone());
|
||||
obj.insert("limit".to_string(), meta["limit"].clone());
|
||||
obj.insert("has_more".to_string(), serde_json::Value::Bool(true));
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(trunc_str) = serde_json::to_string(&truncated_val) {
|
||||
if trunc_str.len() <= max_chars || keep_count == 1 {
|
||||
result_json = trunc_str;
|
||||
break;
|
||||
}
|
||||
}
|
||||
keep_count = keep_count.saturating_sub(keep_count / 3 + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result_json)
|
||||
})?;
|
||||
Ok(data)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SearchNodesHandler;
|
||||
|
||||
#[async_trait]
|
||||
@@ -910,36 +960,49 @@ impl McpTool for GetSubgraphHandler {
|
||||
let depth = req.depth.unwrap_or(2);
|
||||
let format = req.format.unwrap_or(SubgraphFormat::Json);
|
||||
|
||||
if format == SubgraphFormat::MarkdownTree {
|
||||
let (sub_entities, sub_relations) = state.read_graph(|g| {
|
||||
let mut visited = std::collections::HashSet::new();
|
||||
let mut queue = std::collections::VecDeque::new();
|
||||
queue.push_back((root.clone(), 0));
|
||||
visited.insert(root.clone());
|
||||
let (matched_entities, matched_relations) = state.read_graph(|full| {
|
||||
let mut adj: std::collections::HashMap<&str, Vec<&str>> =
|
||||
std::collections::HashMap::with_capacity(full.relations.len() * 2);
|
||||
for r in &full.relations {
|
||||
adj.entry(r.from.as_str()).or_default().push(r.to.as_str());
|
||||
adj.entry(r.to.as_str()).or_default().push(r.from.as_str());
|
||||
}
|
||||
|
||||
while let Some((curr, d)) = queue.pop_front() {
|
||||
if d >= depth {
|
||||
continue;
|
||||
}
|
||||
for r in &g.relations {
|
||||
if r.from == curr && !visited.contains(&r.to) {
|
||||
visited.insert(r.to.clone());
|
||||
queue.push_back((r.to.clone(), d + 1));
|
||||
} else if r.to == curr && !visited.contains(&r.from) {
|
||||
visited.insert(r.from.clone());
|
||||
queue.push_back((r.from.clone(), d + 1));
|
||||
let mut visited = std::collections::HashSet::new();
|
||||
let mut queue = std::collections::VecDeque::new();
|
||||
queue.push_back((root.as_str(), 0));
|
||||
visited.insert(root.clone());
|
||||
|
||||
while let Some((curr, d)) = queue.pop_front() {
|
||||
if d < depth {
|
||||
if let Some(neighbors) = adj.get(curr) {
|
||||
for &next in neighbors {
|
||||
if visited.insert(next.to_string()) {
|
||||
queue.push_back((next, d + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let ent: Vec<_> = g.entities.values().filter(|e| visited.contains(&e.name)).cloned().collect();
|
||||
let rel: Vec<_> = g.relations.iter().filter(|r| visited.contains(&r.from) && visited.contains(&r.to)).cloned().collect();
|
||||
(ent, rel)
|
||||
});
|
||||
let ent: std::collections::HashMap<String, crate::models::Entity> = visited
|
||||
.iter()
|
||||
.filter_map(|name| full.entities.get(name).map(|e| (name.clone(), e.clone())))
|
||||
.collect();
|
||||
let rel: Vec<crate::models::Relation> = full
|
||||
.relations
|
||||
.iter()
|
||||
.filter(|r| visited.contains(&r.from) && visited.contains(&r.to))
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
(ent, rel)
|
||||
});
|
||||
|
||||
if format == SubgraphFormat::MarkdownTree {
|
||||
let mut markdown = format!("# Subgraph Topology for `{}`\n\n", root);
|
||||
markdown.push_str("## Entities\n");
|
||||
for e in &sub_entities {
|
||||
for e in matched_entities.values() {
|
||||
markdown.push_str(&format!("- **{}** (`{}`)\n", e.name, e.entity_type));
|
||||
for obs in &e.observations {
|
||||
markdown.push_str(&format!(" - {}\n", obs));
|
||||
@@ -947,56 +1010,20 @@ impl McpTool for GetSubgraphHandler {
|
||||
}
|
||||
|
||||
markdown.push_str("\n## Relations\n");
|
||||
for r in &sub_relations {
|
||||
for r in &matched_relations {
|
||||
markdown.push_str(&format!("- `{}` --[{}]--> `{}`\n", r.from, r.relation_type, r.to));
|
||||
}
|
||||
return Ok(markdown);
|
||||
}
|
||||
|
||||
let data = state.read_graph(|full| {
|
||||
let mut visited_nodes: std::collections::HashSet<String> = std::collections::HashSet::new();
|
||||
let mut matched_entities = std::collections::HashMap::new();
|
||||
let mut matched_relations = Vec::new();
|
||||
let result = serde_json::json!({
|
||||
"root_node": root,
|
||||
"depth": depth,
|
||||
"entities": matched_entities,
|
||||
"relations": matched_relations,
|
||||
});
|
||||
|
||||
let mut queue = std::collections::VecDeque::new();
|
||||
queue.push_back((root.clone(), 0));
|
||||
visited_nodes.insert(root.clone());
|
||||
|
||||
while let Some((curr, current_depth)) = queue.pop_front() {
|
||||
if let Some(entity) = full.entities.get(&curr) {
|
||||
matched_entities.insert(curr.clone(), entity.clone());
|
||||
}
|
||||
|
||||
if current_depth < depth {
|
||||
for rel in &full.relations {
|
||||
if rel.from == curr {
|
||||
matched_relations.push(rel.clone());
|
||||
if !visited_nodes.contains(&rel.to) {
|
||||
visited_nodes.insert(rel.to.clone());
|
||||
queue.push_back((rel.to.clone(), current_depth + 1));
|
||||
}
|
||||
} else if rel.to == curr {
|
||||
matched_relations.push(rel.clone());
|
||||
if !visited_nodes.contains(&rel.from) {
|
||||
visited_nodes.insert(rel.from.clone());
|
||||
queue.push_back((rel.from.clone(), current_depth + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let result = serde_json::json!({
|
||||
"root_node": root,
|
||||
"depth": depth,
|
||||
"entities": matched_entities,
|
||||
"relations": matched_relations,
|
||||
});
|
||||
|
||||
Ok::<String, crate::error::AppError>(serde_json::to_string_pretty(&result)?)
|
||||
})?;
|
||||
|
||||
Ok(data)
|
||||
Ok(serde_json::to_string_pretty(&result)?)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+252
-794
File diff suppressed because it is too large.
Load diff
@@ -7,100 +7,6 @@ use serde_json::Value;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct StickyNotesHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for StickyNotesHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"sticky_notes"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<StickyNotesTool>(
|
||||
"sticky_notes",
|
||||
"Ephemeral sticky notes with TTL (add, read, delete, clear)",
|
||||
)
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<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(|| {
|
||||
crate::error::AppError::Internal("Missing required parameter 'content' for action 'add'. Next step: Provide non-empty 'content' string in request and retry.".to_string())
|
||||
})?;
|
||||
let now = crate::handlers::utils::now_secs();
|
||||
let expires_at = if let Some(ttl) = req.ttl_seconds {
|
||||
Some(now + ttl)
|
||||
} else if req.session_only.unwrap_or(false) {
|
||||
Some(now + 14400)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
state.code.sticky.modify(|notes| {
|
||||
notes.push(StickyNote {
|
||||
timestamp: now,
|
||||
content,
|
||||
expires_at,
|
||||
..Default::default()
|
||||
});
|
||||
});
|
||||
Ok("Sticky note added.".to_string())
|
||||
}
|
||||
StickyNoteAction::Read => {
|
||||
let now = crate::handlers::utils::now_secs();
|
||||
let has_expired = state.code.sticky.read_with(|notes| {
|
||||
notes.iter().any(|n| matches!(n.expires_at, Some(exp) if exp <= now))
|
||||
});
|
||||
if has_expired {
|
||||
state.code.sticky.modify(|notes| {
|
||||
notes.retain(|n| {
|
||||
if let Some(exp) = n.expires_at {
|
||||
exp > now
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
let active_notes = state.code.sticky.read_with(|notes| notes.clone());
|
||||
Ok(serde_json::to_string(&active_notes)?)
|
||||
}
|
||||
StickyNoteAction::Delete => {
|
||||
let idx = req.index.ok_or_else(|| {
|
||||
crate::error::AppError::Internal("Missing required parameter 'index' for action 'delete'. Next step: Provide 1-based sticky note 'index' integer in request and retry.".to_string())
|
||||
})?;
|
||||
let mut success = false;
|
||||
state.code.sticky.modify(|notes| {
|
||||
if idx > 0 && idx <= notes.len() {
|
||||
notes.remove(idx - 1);
|
||||
success = true;
|
||||
} else if idx == 0 && !notes.is_empty() {
|
||||
notes.remove(0);
|
||||
success = true;
|
||||
}
|
||||
});
|
||||
if success {
|
||||
Ok("Sticky note deleted.".to_string())
|
||||
} else {
|
||||
Err(crate::error::AppError::Internal(
|
||||
"Invalid sticky note index. Next step: Call sticky_notes with action='read' to view active notes and valid indices, then retry.".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
StickyNoteAction::Clear => {
|
||||
state.code.sticky.modify(|notes| {
|
||||
notes.clear();
|
||||
});
|
||||
Ok("All sticky notes cleared.".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
pub struct HandoffMemosHandler;
|
||||
|
||||
#[async_trait]
|
||||
@@ -255,7 +161,7 @@ impl McpTool for PromoteToEntityHandler {
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<PromoteToEntityTool>(
|
||||
"promote_to_entity",
|
||||
"Promote a transient sticky note or active task observation into a permanent Knowledge Graph entity.",
|
||||
"Promote a task observation or finding into a permanent Knowledge Graph entity.",
|
||||
)
|
||||
}
|
||||
|
||||
@@ -285,10 +191,6 @@ impl McpTool for PromoteToEntityHandler {
|
||||
}
|
||||
});
|
||||
|
||||
state.code.sticky.modify(|notes| {
|
||||
notes.retain(|n| !n.content.contains(&obs));
|
||||
});
|
||||
|
||||
Ok(format!(
|
||||
"Promoted transient note into permanent entity '{}'.",
|
||||
req.entity_name
|
||||
@@ -302,47 +204,6 @@ mod tests {
|
||||
use serde_json::json;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_notes_lifecycle() {
|
||||
let dir = tempdir().unwrap();
|
||||
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
|
||||
|
||||
let handler = StickyNotesHandler;
|
||||
let args = json!({
|
||||
"action": "add",
|
||||
"content": "Buy milk",
|
||||
});
|
||||
|
||||
let res = handler
|
||||
.execute(args, state.clone())
|
||||
.await
|
||||
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
||||
.unwrap();
|
||||
assert!(res.contains("Sticky note added"));
|
||||
|
||||
let res2 = handler
|
||||
.execute(json!({"action": "read"}), state.clone())
|
||||
.await
|
||||
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
||||
.unwrap();
|
||||
assert!(res2.contains("Buy milk"));
|
||||
|
||||
let args2 = json!({"action": "delete", "index": 1});
|
||||
let res3 = handler
|
||||
.execute(args2, state.clone())
|
||||
.await
|
||||
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
||||
.unwrap();
|
||||
assert_eq!(res3, "Sticky note deleted.");
|
||||
|
||||
let res4 = handler
|
||||
.execute(json!({"action": "read"}), state.clone())
|
||||
.await
|
||||
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
|
||||
.unwrap();
|
||||
assert!(!res4.contains("Buy milk"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_handoff_and_summaries() {
|
||||
let dir = tempdir().unwrap();
|
||||
@@ -392,22 +253,6 @@ mod tests {
|
||||
.unwrap();
|
||||
assert!(!res4.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_invalid_sticky_note_action() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
|
||||
|
||||
let handler = StickyNotesHandler;
|
||||
let invalid_args = serde_json::json!({
|
||||
"action": "invalid_action"
|
||||
});
|
||||
|
||||
let res = handler
|
||||
.execute(invalid_args, state.clone())
|
||||
.await;
|
||||
assert!(res.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ impl McpTool for TasksHandler {
|
||||
crate::error::AppError::Internal("Missing required parameter 'id' for action 'update'. Next step: Provide task 'id' string in request and retry.".to_string())
|
||||
})?;
|
||||
let status = req.status.ok_or_else(|| {
|
||||
crate::error::AppError::Internal("Missing required parameter 'status' for action 'update'. Next step: Provide valid 'status' ('pending', 'completed', or 'cancelled') in request and retry.".to_string())
|
||||
crate::error::AppError::Internal("Missing required parameter 'status' for action 'update'. Next step: Provide valid 'status' ('pending', 'active', 'in_progress', 'blocked', 'completed', or 'cancelled') in request and retry.".to_string())
|
||||
})?;
|
||||
let target_status = status.to_lowercase();
|
||||
let mut found = false;
|
||||
@@ -113,6 +113,7 @@ impl McpTool for TasksHandler {
|
||||
if let Some(dep_task) = tasks.iter().find(|dt| dt.id == *dep_id)
|
||||
&& dep_task.status != "completed"
|
||||
&& dep_task.status != "done"
|
||||
&& dep_task.status != "cancelled"
|
||||
{
|
||||
uncompleted_deps.push(dep_task.title.as_str());
|
||||
}
|
||||
@@ -132,7 +133,10 @@ impl McpTool for TasksHandler {
|
||||
.iter()
|
||||
.filter(|t| t.parent_id.as_deref() == Some(target_id_ref))
|
||||
{
|
||||
if child.status != "completed" && child.status != "done" {
|
||||
if child.status != "completed"
|
||||
&& child.status != "done"
|
||||
&& child.status != "cancelled"
|
||||
{
|
||||
uncompleted_children.push(child.title.as_str());
|
||||
}
|
||||
}
|
||||
@@ -303,6 +307,7 @@ impl McpTool for TasksHandler {
|
||||
crate::error::AppError::Internal("Missing required parameter 'criteria' for action 'set_criteria'. Next step: Provide array of acceptance criteria strings in request and retry.".to_string())
|
||||
})?;
|
||||
let mut success = false;
|
||||
let mut updated_task: Option<Task> = None;
|
||||
state.project.tasks.modify(|tasks| {
|
||||
if let Some(task) = tasks.iter_mut().find(|t| t.id == id || t.title == id) {
|
||||
task.acceptance_criteria = criteria_list
|
||||
@@ -315,10 +320,15 @@ impl McpTool for TasksHandler {
|
||||
})
|
||||
.collect();
|
||||
task.updated_at = crate::handlers::utils::now_secs();
|
||||
updated_task = Some(task.clone());
|
||||
success = true;
|
||||
}
|
||||
});
|
||||
if success {
|
||||
if let Some(ref ut) = updated_task {
|
||||
let idx = state.get_search_index().await;
|
||||
drop(idx.index_task(ut));
|
||||
}
|
||||
state.broadcast_task_event(TaskEvent {
|
||||
task_id: id.clone(),
|
||||
status: "criteria_set".to_string(),
|
||||
@@ -343,6 +353,7 @@ impl McpTool for TasksHandler {
|
||||
let proof_str = req.proof.unwrap_or_else(|| "Verified".to_string());
|
||||
let mut success = false;
|
||||
let mut already_met = false;
|
||||
let mut updated_task: Option<Task> = None;
|
||||
state.project.tasks.modify(|tasks| {
|
||||
if let Some(task) = tasks.iter_mut().find(|t| t.id == id || t.title == id) {
|
||||
if let Some(ac) = task.acceptance_criteria.iter_mut().next() {
|
||||
@@ -351,7 +362,8 @@ impl McpTool for TasksHandler {
|
||||
} else {
|
||||
ac.is_met = true;
|
||||
success = true;
|
||||
task.updated_at = crate::handlers::utils::now_secs();
|
||||
task.updated_at = crate::handlers::utils::now_secs();
|
||||
updated_task = Some(task.clone());
|
||||
}
|
||||
} else {
|
||||
task.acceptance_criteria
|
||||
@@ -362,11 +374,16 @@ impl McpTool for TasksHandler {
|
||||
..Default::default()
|
||||
});
|
||||
task.updated_at = crate::handlers::utils::now_secs();
|
||||
updated_task = Some(task.clone());
|
||||
success = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
if success {
|
||||
if let Some(ref ut) = updated_task {
|
||||
let idx = state.get_search_index().await;
|
||||
drop(idx.index_task(ut));
|
||||
}
|
||||
state.broadcast_task_event(TaskEvent {
|
||||
task_id: id.clone(),
|
||||
status: "verified".to_string(),
|
||||
|
||||
+201
-213
@@ -1,6 +1,6 @@
|
||||
use crate::router::McpTool;
|
||||
use crate::state::MemoryState;
|
||||
use crate::tools::{ReadClipboardTool, WriteClipboardTool};
|
||||
use crate::tools::{ClipboardAction, ClipboardTool};
|
||||
use arboard::{Clipboard, ImageData};
|
||||
use async_trait::async_trait;
|
||||
use image::{ImageBuffer, imageops::FilterType};
|
||||
@@ -8,104 +8,6 @@ use serde_json::{Value, json};
|
||||
use std::borrow::Cow;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct WriteClipboardHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for WriteClipboardHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"write_clipboard"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<WriteClipboardTool>(
|
||||
"write_clipboard",
|
||||
"Write text, HTML, files, and/or images directly to the OS clipboard.",
|
||||
)
|
||||
}
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||
let tool_args: WriteClipboardTool = serde_json::from_value(args)
|
||||
.map_err(|e| crate::error::AppError::Internal(format!("Invalid args: {}", e)))?;
|
||||
|
||||
let res = tokio::task::spawn_blocking(move || {
|
||||
let mut msgs = Vec::new();
|
||||
|
||||
if let Some(text) = &tool_args.text {
|
||||
let mut written = false;
|
||||
if let Ok(mut clipboard) = Clipboard::new() {
|
||||
for _ in 0..3 {
|
||||
if clipboard.set_text(text).is_ok() {
|
||||
written = true;
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(20));
|
||||
}
|
||||
}
|
||||
if written {
|
||||
msgs.push("Wrote text");
|
||||
}
|
||||
}
|
||||
|
||||
// Handle arboard for image
|
||||
if let Some(image_path) = &tool_args.image_path {
|
||||
match image::open(image_path) {
|
||||
Ok(img) => {
|
||||
let img = if img.width() > 2048 || img.height() > 2048 {
|
||||
img.resize(2048, 2048, FilterType::Triangle)
|
||||
} else {
|
||||
img
|
||||
};
|
||||
let rgba = img.into_rgba8();
|
||||
let (w, h) = rgba.dimensions();
|
||||
let raw_bytes = rgba.into_raw();
|
||||
let mut written = false;
|
||||
if let Ok(mut clipboard) = arboard::Clipboard::new() {
|
||||
for _ in 0..3 {
|
||||
let img_data = ImageData {
|
||||
width: w as usize,
|
||||
height: h as usize,
|
||||
bytes: Cow::Borrowed(&raw_bytes),
|
||||
};
|
||||
if clipboard.set_image(img_data).is_ok() {
|
||||
written = true;
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(20));
|
||||
}
|
||||
}
|
||||
if written {
|
||||
msgs.push("Wrote image");
|
||||
} else {
|
||||
return Err(crate::error::AppError::Internal(
|
||||
"Failed to write image to clipboard".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(crate::error::AppError::Internal(format!(
|
||||
"Failed to open image {}: {}",
|
||||
image_path, e
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if msgs.is_empty() {
|
||||
Ok("No valid clipboard data provided or failed to write.".to_string())
|
||||
} else {
|
||||
Ok(format!(
|
||||
"Successfully populated clipboard: {}",
|
||||
msgs.join(", ")
|
||||
))
|
||||
}
|
||||
})
|
||||
.await
|
||||
.map_err(|e| crate::error::AppError::Internal(format!("Task panic: {}", e)))??;
|
||||
|
||||
state.record_activity("clipboard", &res, None);
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_native_clipboard_text() -> Option<String> {
|
||||
for _ in 0..3 {
|
||||
@@ -121,7 +23,53 @@ pub fn get_native_clipboard_text() -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn get_windows_png_clipboard_image() -> Option<image::DynamicImage> {
|
||||
use windows_sys::Win32::System::DataExchange::{
|
||||
CloseClipboard, GetClipboardData, IsClipboardFormatAvailable, OpenClipboard,
|
||||
RegisterClipboardFormatW,
|
||||
};
|
||||
use windows_sys::Win32::System::Memory::{GlobalLock, GlobalSize, GlobalUnlock};
|
||||
let format_name: Vec<u16> = "PNG\0".encode_utf16().collect();
|
||||
unsafe {
|
||||
let format_id = RegisterClipboardFormatW(format_name.as_ptr());
|
||||
if format_id == 0 {
|
||||
return None;
|
||||
}
|
||||
for _ in 0..3 {
|
||||
if OpenClipboard(std::ptr::null_mut()) != 0 {
|
||||
let has_format = IsClipboardFormatAvailable(format_id) != 0;
|
||||
let mut img_opt = None;
|
||||
if has_format {
|
||||
let handle = GetClipboardData(format_id);
|
||||
if !handle.is_null() {
|
||||
let size = GlobalSize(handle as _);
|
||||
if size > 0 {
|
||||
let ptr = GlobalLock(handle as _);
|
||||
if !ptr.is_null() {
|
||||
let slice = std::slice::from_raw_parts(ptr as *const u8, size);
|
||||
img_opt = image::load_from_memory(slice).ok();
|
||||
GlobalUnlock(handle as _);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
CloseClipboard();
|
||||
if img_opt.is_some() {
|
||||
return img_opt;
|
||||
}
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(20));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
pub fn get_native_clipboard_image() -> Option<image::DynamicImage> {
|
||||
#[cfg(target_os = "windows")]
|
||||
if let Some(img) = get_windows_png_clipboard_image() {
|
||||
return Some(img);
|
||||
}
|
||||
for _ in 0..3 {
|
||||
if let Ok(mut clipboard) = arboard::Clipboard::new() {
|
||||
if let Ok(image_data) = clipboard.get_image() {
|
||||
@@ -139,105 +87,6 @@ pub fn get_native_clipboard_image() -> Option<image::DynamicImage> {
|
||||
None
|
||||
}
|
||||
|
||||
pub struct ReadClipboardHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl McpTool for ReadClipboardHandler {
|
||||
fn name(&self) -> &'static str {
|
||||
"read_clipboard"
|
||||
}
|
||||
|
||||
fn schema(&self) -> Value {
|
||||
crate::mcp::tool_def::<ReadClipboardTool>(
|
||||
"read_clipboard",
|
||||
"Read the current OS clipboard. Extracts text, HTML, file paths, and/or an image if available.",
|
||||
)
|
||||
}
|
||||
|
||||
async fn execute(&self, _args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||
let result =
|
||||
tokio::task::spawn_blocking(move || -> crate::error::Result<serde_json::Value> {
|
||||
let mut out = serde_json::Map::new();
|
||||
|
||||
if let Some(text) = get_native_clipboard_text() {
|
||||
out.insert("text".into(), json!(text));
|
||||
}
|
||||
|
||||
if let Some(dynamic_img) = get_native_clipboard_image() {
|
||||
let mut img = dynamic_img;
|
||||
let max_dim = 1024;
|
||||
if img.width() > max_dim || img.height() > max_dim {
|
||||
img = img.resize(max_dim, max_dim, FilterType::Lanczos3);
|
||||
}
|
||||
let rgb_img = img.into_rgb8();
|
||||
|
||||
let cache_dir = dirs::home_dir()
|
||||
.unwrap_or_default()
|
||||
.join(".gemini/mcp_memory/clipboard");
|
||||
let _ = std::fs::create_dir_all(&cache_dir);
|
||||
|
||||
let timestamp = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
let file_path = cache_dir.join(format!("clipboard_{}.jpg", timestamp));
|
||||
|
||||
if rgb_img
|
||||
.save_with_format(&file_path, image::ImageFormat::Jpeg)
|
||||
.is_ok()
|
||||
{
|
||||
let path_str = file_path.to_string_lossy().to_string();
|
||||
out.insert("image_path".into(), json!(path_str));
|
||||
#[cfg(target_os = "windows")]
|
||||
if let Some(rest) = path_str.strip_prefix("C:\\") {
|
||||
let wsl_path = format!("/mnt/c/{}", rest.replace('\\', "/"));
|
||||
out.insert("image_path_wsl".into(), json!(wsl_path));
|
||||
} else if let Some(rest) = path_str.strip_prefix("c:\\") {
|
||||
let wsl_path = format!("/mnt/c/{}", rest.replace('\\', "/"));
|
||||
out.insert("image_path_wsl".into(), json!(wsl_path));
|
||||
}
|
||||
|
||||
if let Ok(bytes) = std::fs::read(&file_path) {
|
||||
use base64::Engine;
|
||||
let b64 = base64::engine::general_purpose::STANDARD.encode(&bytes);
|
||||
out.insert("image_base64".into(), json!(b64));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Value::Object(out))
|
||||
})
|
||||
.await
|
||||
.map_err(|e| crate::error::AppError::Internal(format!("Task panic: {}", e)))??;
|
||||
|
||||
let mut final_obj = result;
|
||||
if let Some(b64) = final_obj.get("image_base64").and_then(|v| v.as_str()) {
|
||||
let b64_str = b64.to_string();
|
||||
if let Some(obj) = final_obj.as_object_mut() {
|
||||
obj.remove("image_base64");
|
||||
}
|
||||
if state.ollama.is_available().await
|
||||
&& let Ok(analysis) = state
|
||||
.ollama
|
||||
.generate_vision(
|
||||
"Describe the content, text, or UI layout of this image concisely:",
|
||||
&b64_str,
|
||||
)
|
||||
.await
|
||||
&& let Some(obj) = final_obj.as_object_mut()
|
||||
{
|
||||
obj.insert("image_analysis".to_string(), json!(analysis.trim()));
|
||||
}
|
||||
}
|
||||
|
||||
state.record_activity("clipboard", "Read contents from OS clipboard", None);
|
||||
Ok::<String, crate::error::AppError>(serde_json::to_string_pretty(&final_obj)?)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
use crate::tools::{ClipboardAction, ClipboardTool};
|
||||
|
||||
pub struct ClipboardHandler;
|
||||
|
||||
#[async_trait]
|
||||
@@ -257,23 +106,161 @@ impl McpTool for ClipboardHandler {
|
||||
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
|
||||
let result =
|
||||
tokio::task::spawn_blocking(move || -> crate::error::Result<serde_json::Value> {
|
||||
let mut out = serde_json::Map::new();
|
||||
|
||||
if let Some(text) = get_native_clipboard_text() {
|
||||
out.insert("text".into(), json!(text));
|
||||
}
|
||||
|
||||
if let Some(dynamic_img) = get_native_clipboard_image() {
|
||||
let mut img = dynamic_img;
|
||||
let max_dim = 1024;
|
||||
if img.width() > max_dim || img.height() > max_dim {
|
||||
img = img.resize(max_dim, max_dim, FilterType::Lanczos3);
|
||||
}
|
||||
let rgb_img = img.into_rgb8();
|
||||
|
||||
let mut jpeg_bytes = std::io::Cursor::new(Vec::new());
|
||||
if rgb_img.write_to(&mut jpeg_bytes, image::ImageFormat::Jpeg).is_ok() {
|
||||
let bytes = jpeg_bytes.into_inner();
|
||||
use base64::Engine;
|
||||
let b64 = base64::engine::general_purpose::STANDARD.encode(&bytes);
|
||||
out.insert("image_base64".into(), json!(b64));
|
||||
|
||||
let cache_dir = dirs::home_dir()
|
||||
.unwrap_or_default()
|
||||
.join(".gemini/mcp_memory/clipboard");
|
||||
let _ = std::fs::create_dir_all(&cache_dir);
|
||||
let file_path = cache_dir.join("clipboard_latest.jpg");
|
||||
if std::fs::write(&file_path, &bytes).is_ok() {
|
||||
let path_str = file_path.to_string_lossy().to_string();
|
||||
out.insert("image_path".into(), json!(path_str));
|
||||
#[cfg(target_os = "windows")]
|
||||
if let Some(rest) = path_str.strip_prefix("C:\\") {
|
||||
let wsl_path = format!("/mnt/c/{}", rest.replace('\\', "/"));
|
||||
out.insert("image_path_wsl".into(), json!(wsl_path));
|
||||
} else if let Some(rest) = path_str.strip_prefix("c:\\") {
|
||||
let wsl_path = format!("/mnt/c/{}", rest.replace('\\', "/"));
|
||||
out.insert("image_path_wsl".into(), json!(wsl_path));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Value::Object(out))
|
||||
})
|
||||
.await
|
||||
.map_err(|e| crate::error::AppError::Internal(format!("Task panic: {}", e)))??;
|
||||
|
||||
let mut final_obj = result;
|
||||
if let Some(b64) = final_obj.get("image_base64").and_then(|v| v.as_str()) {
|
||||
let b64_str = b64.to_string();
|
||||
if let Some(obj) = final_obj.as_object_mut() {
|
||||
obj.remove("image_base64");
|
||||
}
|
||||
if state.ollama.is_available().await
|
||||
&& let Ok(analysis) = state
|
||||
.ollama
|
||||
.generate_vision(
|
||||
"Describe the content, text, or UI layout of this image concisely:",
|
||||
&b64_str,
|
||||
)
|
||||
.await
|
||||
&& let Some(obj) = final_obj.as_object_mut()
|
||||
{
|
||||
obj.insert("image_analysis".to_string(), json!(analysis.trim()));
|
||||
}
|
||||
}
|
||||
|
||||
state.record_activity("clipboard", "Read contents from OS clipboard", None);
|
||||
Ok::<String, crate::error::AppError>(serde_json::to_string_pretty(&final_obj)?)
|
||||
}
|
||||
ClipboardAction::Write => {
|
||||
WriteClipboardHandler.execute(
|
||||
serde_json::json!({
|
||||
"text": req.text,
|
||||
"html": req.html,
|
||||
"files": req.files,
|
||||
"image_path": req.image_path
|
||||
}),
|
||||
state,
|
||||
).await
|
||||
let text_opt = req.text;
|
||||
let image_path_opt = req.image_path;
|
||||
let res = tokio::task::spawn_blocking(move || {
|
||||
let mut msgs = Vec::new();
|
||||
|
||||
if let Some(text) = &text_opt {
|
||||
let mut written = false;
|
||||
if let Ok(mut clipboard) = Clipboard::new() {
|
||||
for _ in 0..3 {
|
||||
if clipboard.set_text(text).is_ok() {
|
||||
written = true;
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(20));
|
||||
}
|
||||
}
|
||||
if written {
|
||||
msgs.push("Wrote text");
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(image_path) = &image_path_opt {
|
||||
match image::open(image_path) {
|
||||
Ok(img) => {
|
||||
let img = if img.width() > 2048 || img.height() > 2048 {
|
||||
img.resize(2048, 2048, FilterType::Triangle)
|
||||
} else {
|
||||
img
|
||||
};
|
||||
let rgba = img.into_rgba8();
|
||||
let (w, h) = rgba.dimensions();
|
||||
let raw_bytes = rgba.into_raw();
|
||||
let mut written = false;
|
||||
if let Ok(mut clipboard) = arboard::Clipboard::new() {
|
||||
for _ in 0..3 {
|
||||
let img_data = ImageData {
|
||||
width: w as usize,
|
||||
height: h as usize,
|
||||
bytes: Cow::Borrowed(&raw_bytes),
|
||||
};
|
||||
if clipboard.set_image(img_data).is_ok() {
|
||||
written = true;
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(20));
|
||||
}
|
||||
}
|
||||
if written {
|
||||
msgs.push("Wrote image");
|
||||
} else {
|
||||
return Err(crate::error::AppError::Internal(
|
||||
"Failed to write image to clipboard".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(crate::error::AppError::Internal(format!(
|
||||
"Failed to open image {}: {}",
|
||||
image_path, e
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if msgs.is_empty() {
|
||||
Ok("No valid clipboard data provided or failed to write.".to_string())
|
||||
} else {
|
||||
Ok(format!(
|
||||
"Successfully populated clipboard: {}",
|
||||
msgs.join(", ")
|
||||
))
|
||||
}
|
||||
})
|
||||
.await
|
||||
.map_err(|e| crate::error::AppError::Internal(format!("Task panic: {}", e)))??;
|
||||
|
||||
state.record_activity("clipboard", &res, None);
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -286,9 +273,10 @@ mod tests {
|
||||
async fn test_write_clipboard() {
|
||||
let dir = tempdir().unwrap();
|
||||
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
|
||||
let handler = WriteClipboardHandler;
|
||||
let handler = ClipboardHandler;
|
||||
|
||||
let args = json!({
|
||||
"action": "write",
|
||||
"text": "test_text"
|
||||
});
|
||||
|
||||
@@ -310,10 +298,10 @@ mod tests {
|
||||
async fn test_read_clipboard() {
|
||||
let dir = tempdir().unwrap();
|
||||
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
|
||||
let handler = ReadClipboardHandler;
|
||||
let handler = ClipboardHandler;
|
||||
|
||||
let result = handler
|
||||
.execute(json!({}), state)
|
||||
.execute(json!({"action": "read"}), state)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to read clipboard: {}", e))
|
||||
.unwrap();
|
||||
@@ -327,10 +315,10 @@ mod tests {
|
||||
async fn test_read_clipboard_empty() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let state = Arc::new(MemoryState::new(dir.path().to_str().unwrap()));
|
||||
let handler = ReadClipboardHandler;
|
||||
let handler = ClipboardHandler;
|
||||
|
||||
let result = handler
|
||||
.execute(serde_json::json!({}), state)
|
||||
.execute(serde_json::json!({"action": "read"}), state)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to read clipboard: {}", e))
|
||||
.unwrap();
|
||||
|
||||
+257
-1186
File diff suppressed because it is too large.
Load diff
@@ -36,34 +36,11 @@ The server consolidates granular single-purpose tools into domain-named smart to
|
||||
- `action: "update"`: Update milestone status (requires `id`, `status: "active" | "completed" | "cancelled"`).
|
||||
- `action: "list"`: List milestones (optional `namespace`).
|
||||
|
||||
* **`sticky_notes`**: Ephemeral scratchpad notes with TTL.
|
||||
- `action: "add"`: Add note (requires `content`, optional `ttl_seconds`, `session_only`).
|
||||
- `action: "read"`: Read all active notes.
|
||||
- `action: "delete"`: Delete note by index (requires 1-based `index`).
|
||||
- `action: "clear"`: Clear all sticky notes.
|
||||
|
||||
* **`handoff_memos`**: Session handoff notes for future agents.
|
||||
- `action: "leave"`: Leave a memo (requires `content`, optional `vcs_revision`, `repo_name`, `git_branch`, `blockers: Vec<String>`, `action_items: Vec<String>`, `expires_at`).
|
||||
- `action: "read"`: Read active handoff memos.
|
||||
- `action: "clear"`: Clear memos.
|
||||
|
||||
* **`pinned_files`**: Focus file working set.
|
||||
- `action: "pin"`: Pin file to focus set (requires `path`, optional `pin_reason`, `symbol_or_line`, `repo_name`, `git_branch`).
|
||||
- `action: "unpin"`: Unpin file from focus set (requires `path`).
|
||||
- `action: "list"`: List pinned files (optional `namespace`).
|
||||
|
||||
* **`context_workspaces`**: Workspace context state snapshots.
|
||||
- `action: "save"`: Save context workspace (requires `name`, optional `pinned_files`, `active_task_ids`, `description`, `git_branch`, `vcs_revision`, `repo_name`).
|
||||
- `action: "load"`: Restore saved context workspace (requires `name`).
|
||||
- `action: "list"`: List saved context workspaces.
|
||||
- `action: "delete"`: Delete saved context workspace (requires `name`).
|
||||
- `action: "diff"`: Compare two saved context workspaces (requires `name`, `other_name`).
|
||||
|
||||
* **`pr_checklist`**: Pre-commit and PR checklist.
|
||||
- `action: "add"`: Add checklist item (requires `description`, optional `category: "Security" | "Testing" | "Formatting" | ...`, `automated_check`, `is_checked`, `repo_name`).
|
||||
- `action: "get"`: Get PR checklist items.
|
||||
- `action: "clear"`: Clear PR checklist.
|
||||
|
||||
* **`snippets`**: Reusable code snippet vault.
|
||||
- `action: "store"`: Store snippet (requires `query` as name, optional `language`, `code`, `description`, `tags`, `origin_file`, `line_range`, `repo_name`).
|
||||
- `action: "search"`: Search snippet vault (optional `query`, `tags`, `hybrid: true`).
|
||||
@@ -90,7 +67,6 @@ The server consolidates granular single-purpose tools into domain-named smart to
|
||||
* **`clipboard`**: OS Clipboard management.
|
||||
- `action: "read"`: Read OS clipboard.
|
||||
- `action: "write"`: Write text/html/files/image to clipboard.
|
||||
- `action: "toggle_watch"`: Toggle auto-clipboard watcher.
|
||||
|
||||
---
|
||||
|
||||
@@ -189,9 +165,8 @@ To maintain maximum security, speed, and cross-platform reliability:
|
||||
* **Zero-Allocation Stream Formatting**: Graph condensation loops (`condense_graph_worker`) format node/relation subgraphs into stream buffers via `std::fmt::Write`, eliminating intermediate String allocation overhead.
|
||||
* **Single-Handle OS Clipboard Retries & Image Downsampling**: `WriteClipboardHandler` initializes OS clipboard handles once per operation and downsamples images exceeding $2048 \times 2048$ resolution before writing raw RGBA bytes.
|
||||
* **Zero Transaction Drop Persistence Guarantee**: `Store::modify` automatically spawns an async task to execute `push_async` with channel backpressure if `push` encounters queue saturation, ensuring zero data loss under spike write loads.
|
||||
* **Non-Blocking Read Lock Sticky Notes**: `StickyNotesHandler` (`action = "read"`) queries notes using shared read locks, executing write pruning only when expired items exist.
|
||||
* **Token-Budgeted Query Projections**: Decision queries (`query_decisions`) support `limit` caps and compact `include_body: false` projections for token budget optimization.
|
||||
* **Serde Parameter & Enum Ergonomics**: Action enums (`StickyNoteAction`, `SnippetSearchMode`, `Relation`) support case-insensitive variants and common synonyms (`create`/`add`, `remove`/`delete`, `list`/`read`, `source`/`from`, `target`/`to`, `relationType`/`relation_type`) ensuring seamless LLM tool execution.
|
||||
* **Serde Parameter & Enum Ergonomics**: Action enums (`SnippetSearchMode`, `Relation`) support case-insensitive variants and common synonyms (`create`/`add`, `remove`/`delete`, `list`/`read`, `source`/`from`, `target`/`to`, `relationType`/`relation_type`) ensuring seamless LLM tool execution.
|
||||
* **Atomic Search Index Swaps**: `MemoryState::rebuild_index` constructs and populates a new `MemoryIndex` instance in isolation before performing an atomic pointer swap (`*self.search_index.write().await = new_idx`), eliminating transient empty search result windows.
|
||||
* **Non-Blocking Tantivy Search Queries**: `MemoryIndex::search` queries current index searcher snapshots without executing synchronous disk commit locks, preventing query stalls during heavy background indexing.
|
||||
* **Zero-Allocation HashSet<&str> Snippet Deduplication**: `indexer.rs` utilizes borrowed `HashSet<&str>` name lookups during snippet batch modifications, eliminating heap string re-allocations inside the store write lock.
|
||||
@@ -214,7 +189,7 @@ To maintain maximum security, speed, and cross-platform reliability:
|
||||
* **Embedding Input Safeguard**: `generate_embedding_async` returns explicit errors for empty/0-length text inputs instead of returning empty vectors, preventing downstream vector dimension mismatches during cosine similarity calculations.
|
||||
* **Path Traversal Security Guards**: `validate_safe_path` enforces path canonicalization and rejects relative parent traversal components (`..`) across file and process log handlers (`GetRecentLogsTool`, `WatchProcessLogsTool`).
|
||||
* **Watcher Map Memory Eviction**: Proactive daemon file watcher in `watcher.rs` caps `last_processed` map size at 1,000 entries and purges entries older than 10 minutes to prevent monotonic memory leakage.
|
||||
* **Comprehensive Serde Casing Aliases**: All 11 consolidated tool action enums (`TaskAction`, `MilestoneAction`, `PinnedFileAction`, `ContextWorkspaceAction`, `PrChecklistAction`, `SnippetAction`, `DecisionAction`, `TechDebtAction`, `EnvAction`, `ClipboardAction`, `HandoffMemoAction`) include serde alias attributes supporting `snake_case`, `camelCase`, `PascalCase`, and uppercase variants for maximum LLM casing resilience.
|
||||
* **Comprehensive Serde Casing Aliases**: All 8 consolidated tool action enums (TaskAction, MilestoneAction, SnippetAction, DecisionAction, TechDebtAction, EnvAction, ClipboardAction, HandoffMemoAction) include serde alias attributes supporting `snake_case`, `camelCase`, `PascalCase`, and uppercase variants for maximum LLM casing resilience.
|
||||
* **Two-Phase Graph Condensation**: `condense_graph_worker` uses a 2-phase commit (non-destructive `read_with` -> graph insert -> prune by timestamp/content) to prevent data loss if summarization or graph insertion fails.
|
||||
* **Store Write Lock Minimization**: `Store::modify` and `Store::modify_async` unblock concurrent readers during JSON serialization by releasing the write lock immediately after mutating memory state.
|
||||
* **Redb Database Lock Retry Backoff**: `init_db` retries transient Redb lock contention with exponential backoff (3 attempts, 150ms delay) before falling back.
|
||||
|
||||
+1
-45
@@ -105,9 +105,7 @@ pub async fn ttl_sweeper_worker(state: Arc<MemoryState>) {
|
||||
state.project.tasks.modify(|tasks| {
|
||||
tasks.retain(|t| t.expires_at.is_none_or(|exp| exp > now));
|
||||
});
|
||||
state.code.sticky.modify(|notes| {
|
||||
notes.retain(|n| n.expires_at.is_none_or(|exp| exp > now));
|
||||
});
|
||||
|
||||
state.telemetry.handoff_memos.modify(|memos| {
|
||||
memos.retain(|m| m.expires_at.is_none_or(|exp| exp > now));
|
||||
});
|
||||
@@ -139,48 +137,6 @@ pub async fn condense_graph_worker(state: Arc<MemoryState>) {
|
||||
.unwrap_or_default()
|
||||
.as_secs();
|
||||
|
||||
let sticky_condensation = state.code.sticky.read_with(|notes| {
|
||||
if notes.len() > threshold {
|
||||
let mut sorted = notes.clone();
|
||||
sorted.sort_by_key(|n| n.timestamp);
|
||||
let to_remove = sorted.len() - (threshold / 2);
|
||||
let removed: Vec<_> = sorted.into_iter().take(to_remove).collect();
|
||||
let mut content = String::new();
|
||||
let mut ids = Vec::new();
|
||||
for r in &removed {
|
||||
content.push_str(&format!("{}\n", r.content));
|
||||
ids.push(r.id.clone());
|
||||
}
|
||||
Some((content, ids))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
if let Some((content, ids)) = sticky_condensation {
|
||||
if !content.is_empty() {
|
||||
let name = format!("StickyNote History {}", now);
|
||||
state.modify_graph(|graph| {
|
||||
graph.entities.insert(
|
||||
name.clone(),
|
||||
crate::models::Entity {
|
||||
name: name.clone(),
|
||||
entity_type: "Historical Summary".to_string(),
|
||||
observations: vec![content],
|
||||
namespace: crate::models::default_namespace(),
|
||||
git_branch: None,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
});
|
||||
let id_set: std::collections::HashSet<String> = ids.into_iter().collect();
|
||||
state.code.sticky.modify(|notes| {
|
||||
notes.retain(|n| !id_set.contains(&n.id));
|
||||
});
|
||||
tracing::info!("Condensed sticky notes into Historical Summary.");
|
||||
}
|
||||
}
|
||||
|
||||
let snippet_condensation = state.code.snippets.read_with(|snippets| {
|
||||
if snippets.len() > threshold {
|
||||
let mut sorted = snippets.clone();
|
||||
|
||||
+5
-71
@@ -81,21 +81,6 @@ impl CodeChange {
|
||||
self.branch.as_deref().or(self.git_branch.as_deref())
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct StickyNote {
|
||||
#[serde(default = "generate_uuid")]
|
||||
pub id: String,
|
||||
pub timestamp: u64,
|
||||
pub content: String,
|
||||
#[serde(default)]
|
||||
pub expires_at: Option<u64>,
|
||||
#[serde(default = "default_namespace")]
|
||||
pub namespace: String,
|
||||
#[serde(default)]
|
||||
pub category: Option<String>,
|
||||
#[serde(default)]
|
||||
pub author: Option<String>,
|
||||
}
|
||||
|
||||
pub fn generate_uuid() -> String {
|
||||
uuid::Uuid::new_v4().to_string()
|
||||
@@ -162,18 +147,20 @@ pub fn normalize_relation_type(input: &str) -> String {
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum TaskStatus {
|
||||
Pending,
|
||||
#[default]
|
||||
Active,
|
||||
InProgress,
|
||||
Blocked,
|
||||
Completed,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
impl TaskStatus {
|
||||
pub fn is_active(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
TaskStatus::Active | TaskStatus::InProgress | TaskStatus::Blocked
|
||||
TaskStatus::Pending | TaskStatus::Active | TaskStatus::InProgress | TaskStatus::Blocked
|
||||
)
|
||||
}
|
||||
|
||||
@@ -183,10 +170,12 @@ impl TaskStatus {
|
||||
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
TaskStatus::Pending => "pending",
|
||||
TaskStatus::Active => "active",
|
||||
TaskStatus::InProgress => "in_progress",
|
||||
TaskStatus::Blocked => "blocked",
|
||||
TaskStatus::Completed => "completed",
|
||||
TaskStatus::Cancelled => "cancelled",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -400,18 +389,6 @@ fn default_adr_status() -> String {
|
||||
"accepted".to_string()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct Preference {
|
||||
pub key: String,
|
||||
pub value: String,
|
||||
pub updated_at: u64,
|
||||
#[serde(default = "default_namespace")]
|
||||
pub namespace: String,
|
||||
#[serde(default)]
|
||||
pub category: Option<String>,
|
||||
#[serde(default)]
|
||||
pub rationale: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct ErrorFix {
|
||||
@@ -434,19 +411,6 @@ pub struct ErrorFix {
|
||||
pub toolchain: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct PinnedFile {
|
||||
pub namespace: String,
|
||||
pub file_path: String,
|
||||
pub timestamp: u64,
|
||||
pub git_branch: Option<String>,
|
||||
#[serde(default)]
|
||||
pub repo_name: Option<String>,
|
||||
#[serde(default)]
|
||||
pub symbol_or_line: Option<String>,
|
||||
#[serde(default)]
|
||||
pub pin_reason: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct SessionSummary {
|
||||
@@ -554,20 +518,6 @@ pub struct EnvironmentDetail {
|
||||
pub ssh_host: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct PrChecklistItem {
|
||||
pub namespace: String,
|
||||
pub id: String,
|
||||
pub description: String,
|
||||
#[serde(default)]
|
||||
pub is_checked: bool,
|
||||
#[serde(default)]
|
||||
pub category: Option<String>,
|
||||
#[serde(default)]
|
||||
pub automated_check: Option<String>,
|
||||
#[serde(default)]
|
||||
pub repo_name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct TechDebt {
|
||||
@@ -595,22 +545,6 @@ pub struct TechDebt {
|
||||
pub effort_estimate: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct ContextWorkspace {
|
||||
pub name: String,
|
||||
pub namespace: String,
|
||||
pub pinned_files: Vec<String>,
|
||||
pub active_task_ids: Vec<String>,
|
||||
pub saved_at: u64,
|
||||
#[serde(default)]
|
||||
pub repo_name: Option<String>,
|
||||
#[serde(default)]
|
||||
pub git_branch: Option<String>,
|
||||
#[serde(default)]
|
||||
pub vcs_revision: Option<String>,
|
||||
#[serde(default)]
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct GateRecord {
|
||||
|
||||
+26
-207
@@ -261,21 +261,9 @@ impl McpResource for SessionDeltaResource {
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
let active_tasks: Vec<_> = tasks.iter().filter(|t| t.is_active()).collect();
|
||||
|
||||
let notes = state
|
||||
.code
|
||||
.sticky
|
||||
.cache
|
||||
.read()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
let active_notes: Vec<_> = notes
|
||||
.iter()
|
||||
.filter(|n| n.expires_at.map_or(true, |exp| exp > now))
|
||||
.collect();
|
||||
|
||||
let delta = serde_json::json!({
|
||||
"recent_changes": recent_changes,
|
||||
"active_tasks": active_tasks,
|
||||
"active_sticky_notes": active_notes,
|
||||
"timestamp": now,
|
||||
});
|
||||
|
||||
@@ -291,7 +279,7 @@ impl McpPrompt for ContextWarmupPrompt {
|
||||
}
|
||||
fn description(&self) -> Option<&'static str> {
|
||||
Some(
|
||||
"Consolidated context warmup prompt providing active tasks, pinned files, open tech debt, and sticky notes",
|
||||
"Consolidated context warmup prompt providing active tasks, active decisions, and unresolved tech debt",
|
||||
)
|
||||
}
|
||||
async fn get(
|
||||
@@ -305,7 +293,7 @@ impl McpPrompt for ContextWarmupPrompt {
|
||||
"role": "user",
|
||||
"content": {
|
||||
"type": "text",
|
||||
"text": "Perform a complete context warmup. Please read memory://session/delta, memory://tasks/active, memory://tech_debt/active, and memory://pinned_files to synthesize current project focus and active workspace constraints."
|
||||
"text": "Perform a complete context warmup. Please read memory://session/delta, memory://tasks/active, memory://decisions/active, and memory://tech_debt/unresolved to synthesize current project focus and active workspace constraints."
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -397,28 +385,6 @@ impl MemoryHandler {
|
||||
Ok(serde_json::to_string_pretty(&*items)?)
|
||||
}
|
||||
}
|
||||
struct PinnedFilesResource;
|
||||
#[async_trait]
|
||||
impl McpResource for PinnedFilesResource {
|
||||
fn uri(&self) -> &'static str {
|
||||
"memory://pinned_files"
|
||||
}
|
||||
fn name(&self) -> &'static str {
|
||||
"Pinned Files"
|
||||
}
|
||||
fn description(&self) -> Option<&'static str> {
|
||||
Some("Currently pinned files in the workspace")
|
||||
}
|
||||
async fn read(&self, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||
let items = state
|
||||
.project
|
||||
.pinned_files
|
||||
.cache
|
||||
.read()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
Ok(serde_json::to_string_pretty(&*items)?)
|
||||
}
|
||||
}
|
||||
|
||||
struct MilestonesResource;
|
||||
#[async_trait]
|
||||
@@ -473,7 +439,6 @@ impl MemoryHandler {
|
||||
|
||||
register_resource!(TerminalHistoryResource);
|
||||
register_resource!(RecentActivitiesResource);
|
||||
register_resource!(PinnedFilesResource);
|
||||
register_resource!(MilestonesResource);
|
||||
struct ArchiveRoutinePrompt;
|
||||
#[async_trait]
|
||||
@@ -529,8 +494,6 @@ impl MemoryHandler {
|
||||
|
||||
register!(tasks::TasksHandler);
|
||||
register!(tasks::MilestonesHandler);
|
||||
|
||||
register!(notes::StickyNotesHandler);
|
||||
register!(notes::HandoffMemosHandler);
|
||||
register!(notes::AddSessionSummaryHandler);
|
||||
register!(notes::GenerateStandupReportHandler);
|
||||
@@ -542,16 +505,11 @@ impl MemoryHandler {
|
||||
register!(meta::SearchErrorFixesHandler);
|
||||
register!(meta::LogCodeChangeHandler);
|
||||
register!(meta::QueryRecentChangesHandler);
|
||||
register!(meta::LearnPreferenceHandler);
|
||||
register!(meta::ReadPreferencesHandler);
|
||||
register!(meta::OmniSearchHandler);
|
||||
register!(meta::GetProjectHealthHandler);
|
||||
|
||||
register!(env::EnvironmentHandler);
|
||||
|
||||
register!(workspaces::PinnedFilesHandler);
|
||||
register!(workspaces::ContextWorkspacesHandler);
|
||||
register!(workspaces::PrChecklistHandler);
|
||||
register!(workspaces::SnippetsHandler);
|
||||
register!(vision::ClipboardHandler);
|
||||
|
||||
@@ -574,7 +532,6 @@ impl MemoryHandler {
|
||||
register!(meta::GetNextActionableTasksHandler);
|
||||
register!(meta::LogHypothesisHandler);
|
||||
register!(meta::QueryHypothesesHandler);
|
||||
register!(workspaces::DiffContextWorkspacesHandler);
|
||||
register!(meta::GetPreflightContextHandler);
|
||||
register!(graph::ResolveStaleSymbolsHandler);
|
||||
register!(graph::SummarizeSubgraphHandler);
|
||||
@@ -774,14 +731,13 @@ impl MemoryHandler {
|
||||
"log_error_fix" => "ERROR_FIX",
|
||||
"tech_debt" => "TECH_DEBT",
|
||||
"tasks" | "milestones" => "TASK",
|
||||
"sticky_notes" | "handoff_memos" => "STICKY_NOTE",
|
||||
"handoff_memos" => "STICKY_NOTE",
|
||||
"manage_checkpoint" => "CHECKPOINT",
|
||||
"manage_subagent_namespace" => "SUBAGENT",
|
||||
"snippets" => "SNIPPET",
|
||||
"search_web" => "WEB_SEARCH",
|
||||
"omni_search" => "OMNI_SEARCH",
|
||||
"environment" => "ENVIRONMENT",
|
||||
"pinned_files" | "context_workspaces" | "pr_checklist" => "WORKSPACE",
|
||||
_ => "TOOL",
|
||||
};
|
||||
|
||||
@@ -880,157 +836,25 @@ pub fn format_tool_activity_description(name: &str, args: &serde_json::Value) ->
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
"sticky_notes" => {
|
||||
let act = args.get("action").and_then(|v| v.as_str()).unwrap_or("add");
|
||||
let preview = args
|
||||
.get("content")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|c| c.chars().take(40).collect::<String>())
|
||||
.unwrap_or_default();
|
||||
(
|
||||
"Sticky Notes",
|
||||
format!("{}: {}", act, preview)
|
||||
.trim_end_matches(": ")
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
"clipboard" => {
|
||||
let act = args
|
||||
.get("action")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("read");
|
||||
("Clipboard", act.to_string())
|
||||
}
|
||||
"snippets" => {
|
||||
let act = args
|
||||
.get("action")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("search");
|
||||
let q = args.get("query").and_then(|v| v.as_str()).unwrap_or("");
|
||||
(
|
||||
"Snippets",
|
||||
format!("{}: {}", act, q).trim_end_matches(": ").to_string(),
|
||||
)
|
||||
}
|
||||
"log_code_change" => {
|
||||
let file = args
|
||||
.get("file_path")
|
||||
.or_else(|| args.get("file"))
|
||||
.or_else(|| args.get("target_file"))
|
||||
.and_then(|v| v.as_str());
|
||||
let summary = args
|
||||
.get("summary")
|
||||
.or_else(|| args.get("description"))
|
||||
.and_then(|v| v.as_str());
|
||||
|
||||
let d = match (file, summary) {
|
||||
(Some(f), Some(s)) => format!("{}: {}", f, s),
|
||||
(Some(f), None) => f.to_string(),
|
||||
(None, Some(s)) => s.to_string(),
|
||||
(None, None) => String::new(),
|
||||
};
|
||||
("Log Code Change", d)
|
||||
}
|
||||
"log_error_fix" => {
|
||||
let d = args
|
||||
.get("error")
|
||||
.or_else(|| args.get("summary"))
|
||||
.or_else(|| args.get("solution"))
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
("Log Error Fix", d)
|
||||
}
|
||||
"create_entities" => {
|
||||
let d = if let Some(entities) = args.get("entities").and_then(|v| v.as_array()) {
|
||||
let names: Vec<&str> = entities
|
||||
.iter()
|
||||
.filter_map(|e| {
|
||||
e.get("name")
|
||||
.or_else(|| e.get("entity_name"))
|
||||
.and_then(|n| n.as_str())
|
||||
})
|
||||
.take(3)
|
||||
.collect();
|
||||
if !names.is_empty() {
|
||||
let mut s = names.join(", ");
|
||||
if entities.len() > 3 {
|
||||
s.push_str(&format!(" (+{} more)", entities.len() - 3));
|
||||
}
|
||||
s
|
||||
} else {
|
||||
format!("{} entity/entities", entities.len())
|
||||
}
|
||||
} else if let Some(name) = args
|
||||
.get("name")
|
||||
.or_else(|| args.get("entity_name"))
|
||||
.and_then(|v| v.as_str())
|
||||
{
|
||||
name.to_string()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
("Create Entities", d)
|
||||
}
|
||||
"create_relations" => {
|
||||
let d = if let Some(relations) = args.get("relations").and_then(|v| v.as_array()) {
|
||||
if let Some(first) = relations.first() {
|
||||
let from = first.get("from").and_then(|v| v.as_str()).unwrap_or("?");
|
||||
let to = first.get("to").and_then(|v| v.as_str()).unwrap_or("?");
|
||||
let pred = first
|
||||
.get("relation_type")
|
||||
.or_else(|| first.get("predicate"))
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("->");
|
||||
if relations.len() > 1 {
|
||||
format!("{} {} {} (+{} more)", from, pred, to, relations.len() - 1)
|
||||
} else {
|
||||
format!("{} {} {}", from, pred, to)
|
||||
}
|
||||
} else {
|
||||
format!("{} relation(s)", relations.len())
|
||||
}
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
("Create Relations", d)
|
||||
}
|
||||
"omni_search" | "search_graph" | "search_web" => {
|
||||
let d = args
|
||||
.get("query")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|q| format!("\"{}\"", q))
|
||||
let names = args
|
||||
.get("entities")
|
||||
.and_then(|v| v.as_array())
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.filter_map(|e| e.get("name").and_then(|n| n.as_str()))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
})
|
||||
.unwrap_or_default();
|
||||
("Search", d)
|
||||
}
|
||||
_ => {
|
||||
let d = args
|
||||
.get("title")
|
||||
.or_else(|| args.get("action"))
|
||||
.or_else(|| args.get("summary"))
|
||||
.or_else(|| args.get("description"))
|
||||
.or_else(|| args.get("name"))
|
||||
.or_else(|| args.get("query"))
|
||||
.or_else(|| args.get("file_path"))
|
||||
.or_else(|| args.get("file"))
|
||||
.or_else(|| args.get("path"))
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("");
|
||||
(name, d.to_string())
|
||||
("Create Entities", names)
|
||||
}
|
||||
_ => return name.to_string(),
|
||||
};
|
||||
|
||||
let mut truncated = detail.trim().to_string();
|
||||
if truncated.len() > 80 {
|
||||
let valid_len = truncated.floor_char_boundary(77);
|
||||
truncated.truncate(valid_len);
|
||||
truncated.push_str("...");
|
||||
}
|
||||
|
||||
if !truncated.is_empty() {
|
||||
format!("{}: {}", action, truncated)
|
||||
if detail.is_empty() {
|
||||
action.to_string()
|
||||
} else {
|
||||
format!("{}", action)
|
||||
format!("{}: {}", action, detail)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1047,20 +871,20 @@ async fn execute_tool_span(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
use tempfile::tempdir;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn test_format_tool_activity_description() {
|
||||
let msg1 = format_tool_activity_description(
|
||||
"log_code_change",
|
||||
&json!({ "file": "src/lib.rs", "summary": "Refactored library" }),
|
||||
let msg = format_tool_activity_description(
|
||||
"tasks",
|
||||
&serde_json::json!({ "action": "add", "title": "Refactor router" }),
|
||||
);
|
||||
assert_eq!(msg1, "Log Code Change: src/lib.rs: Refactored library");
|
||||
assert_eq!(msg, "Tasks: add: Refactor router");
|
||||
|
||||
let msg2 = format_tool_activity_description(
|
||||
"create_entities",
|
||||
&json!({ "entities": [{ "name": "EntityA" }, { "name": "EntityB" }] }),
|
||||
&serde_json::json!({ "entities": [{ "name": "EntityA" }, { "name": "EntityB" }] }),
|
||||
);
|
||||
assert_eq!(msg2, "Create Entities: EntityA, EntityB");
|
||||
|
||||
@@ -1115,7 +939,7 @@ mod tests {
|
||||
assert!(
|
||||
resources_arr
|
||||
.iter()
|
||||
.any(|r| r["uri"] == "memory://pinned_files")
|
||||
.any(|r| r["uri"] == "memory://decisions/active")
|
||||
);
|
||||
assert!(
|
||||
resources_arr
|
||||
@@ -1292,8 +1116,8 @@ mod tests {
|
||||
.as_array()
|
||||
.expect("tools array expected");
|
||||
assert!(
|
||||
tools.len() >= 40,
|
||||
"Expected at least 40 registered MCP tools, found {}",
|
||||
tools.len() >= 30,
|
||||
"Expected at least 30 registered MCP tools, found {}",
|
||||
tools.len()
|
||||
);
|
||||
|
||||
@@ -1353,16 +1177,11 @@ mod tests {
|
||||
("tasks", json!({"action": "list"})),
|
||||
("decisions", json!({"action": "query"})),
|
||||
("tech_debt", json!({"action": "list"})),
|
||||
("sticky_notes", json!({"action": "read"})),
|
||||
("pinned_files", json!({"action": "list"})),
|
||||
("context_workspaces", json!({"action": "list"})),
|
||||
("pr_checklist", json!({"action": "get"})),
|
||||
("snippets", json!({"action": "search", "query": "test"})),
|
||||
("clipboard", json!({"action": "read"})),
|
||||
("environment", json!({"action": "read_fingerprint"})),
|
||||
("omni_search", json!({"query": "test"})),
|
||||
("get_project_health", json!({})),
|
||||
("read_preferences", json!({})),
|
||||
("find_orphans", json!({})),
|
||||
("sweep_graph_health", json!({})),
|
||||
("get_next_actionable_tasks", json!({})),
|
||||
|
||||
+26
-19
@@ -253,15 +253,35 @@ impl MemoryIndex {
|
||||
})?;
|
||||
|
||||
let target_limit = if is_wildcard_fallback { 10 } else { 50 };
|
||||
let fetch_limit = if namespace.is_some() {
|
||||
// Fetch deeper candidate pool so other namespaces do not starve the target namespace
|
||||
1000
|
||||
let final_query: Box<dyn tantivy::query::Query> = if let Some(ns) = namespace {
|
||||
let ns_term_query: Box<dyn tantivy::query::Query> = Box::new(tantivy::query::TermQuery::new(
|
||||
tantivy::Term::from_field_text(self.namespace_field, ns),
|
||||
tantivy::schema::IndexRecordOption::Basic,
|
||||
));
|
||||
let ns_filter: Box<dyn tantivy::query::Query> = if ns != "global" {
|
||||
let global_term_query = Box::new(tantivy::query::TermQuery::new(
|
||||
tantivy::Term::from_field_text(self.namespace_field, "global"),
|
||||
tantivy::schema::IndexRecordOption::Basic,
|
||||
));
|
||||
Box::new(tantivy::query::BooleanQuery::new(vec![
|
||||
(tantivy::query::Occur::Should, ns_term_query),
|
||||
(tantivy::query::Occur::Should, global_term_query),
|
||||
]))
|
||||
} else {
|
||||
ns_term_query
|
||||
};
|
||||
|
||||
Box::new(tantivy::query::BooleanQuery::new(vec![
|
||||
(tantivy::query::Occur::Must, q),
|
||||
(tantivy::query::Occur::Must, ns_filter),
|
||||
]))
|
||||
} else {
|
||||
target_limit
|
||||
q
|
||||
};
|
||||
|
||||
let top_docs = searcher.search(
|
||||
&q,
|
||||
&tantivy::collector::TopDocs::with_limit(fetch_limit).order_by_score(),
|
||||
&final_query,
|
||||
&tantivy::collector::TopDocs::with_limit(target_limit).order_by_score(),
|
||||
)?;
|
||||
|
||||
let mut results = Vec::with_capacity(target_limit);
|
||||
@@ -287,20 +307,7 @@ impl MemoryIndex {
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
let doc_ns = retrieved_doc
|
||||
.get_first(self.namespace_field)
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("");
|
||||
if let Some(ns) = namespace
|
||||
&& doc_ns != ns
|
||||
&& doc_ns != "global"
|
||||
{
|
||||
continue;
|
||||
}
|
||||
results.push((id, doc_type, title, body, score));
|
||||
if results.len() >= target_limit {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
@@ -15,9 +15,6 @@ pub struct GenericEvent {
|
||||
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 snapshots: Store<Vec<StateSnapshot>>,
|
||||
}
|
||||
|
||||
@@ -27,7 +24,6 @@ pub struct CodeStores {
|
||||
pub adrs: Store<Vec<Adr>>,
|
||||
pub error_fixes: Store<Vec<ErrorFix>>,
|
||||
pub tech_debts: Store<Vec<TechDebt>>,
|
||||
pub sticky: Store<Vec<StickyNote>>,
|
||||
pub hypotheses: Store<Vec<Hypothesis>>,
|
||||
}
|
||||
|
||||
@@ -36,7 +32,6 @@ pub struct EnvironmentStores {
|
||||
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 {
|
||||
@@ -112,9 +107,6 @@ impl MemoryState {
|
||||
project: ProjectStores {
|
||||
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()),
|
||||
snapshots: Store::new("state_snapshots", db.clone()),
|
||||
},
|
||||
code: CodeStores {
|
||||
@@ -123,7 +115,6 @@ impl MemoryState {
|
||||
adrs: Store::new("adrs", db.clone()),
|
||||
error_fixes: Store::new("error_fixes", db.clone()),
|
||||
tech_debts: Store::new("tech_debts", db.clone()),
|
||||
sticky: Store::new("sticky_notes", db.clone()),
|
||||
hypotheses: Store::new("hypotheses", db.clone()),
|
||||
},
|
||||
env: EnvironmentStores {
|
||||
@@ -131,7 +122,6 @@ impl MemoryState {
|
||||
env_requirements: Store::new("env_requirements", db.clone()),
|
||||
environments: Store::new("environments", db.clone()),
|
||||
gates: Store::new("gates", db.clone()),
|
||||
prefs: Store::new("preferences", db.clone()),
|
||||
},
|
||||
telemetry: TelemetryStores {
|
||||
session_summaries: Store::new("session_summaries", db.clone()),
|
||||
@@ -527,21 +517,6 @@ impl SearchService {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
self.state.code.sticky.read_with(|sticky| {
|
||||
for note in sticky.iter() {
|
||||
if uncached_texts.len() >= 50 {
|
||||
break;
|
||||
}
|
||||
let content_preview = note.content.chars().take(200).collect::<String>();
|
||||
uncached_texts.push(note.content.clone());
|
||||
uncached_meta.push((
|
||||
"StickyNote".to_string(),
|
||||
"sticky".to_string(),
|
||||
content_preview,
|
||||
));
|
||||
}
|
||||
});
|
||||
self.state.read_graph(|graph| {
|
||||
for entity in graph.entities.values() {
|
||||
if let Some(ns) = filter_namespace {
|
||||
|
||||
+28
-6
@@ -446,7 +446,10 @@ impl<T: DeserializeOwned + Default + Serialize + Send + Sync + 'static> Store<T>
|
||||
|
||||
// Expensive serialization and granular extraction run completely unblocked outside the lock
|
||||
let full_bytes_res = serde_json::to_vec(&new_snapshot);
|
||||
let granular_entries = serde_json::to_value(&new_snapshot)
|
||||
let granular_entries = full_bytes_res
|
||||
.as_ref()
|
||||
.ok()
|
||||
.and_then(|bytes| serde_json::from_slice::<serde_json::Value>(bytes).ok())
|
||||
.map(|val| Self::extract_granular_entries(&self.key, &val))
|
||||
.unwrap_or_default();
|
||||
|
||||
@@ -454,7 +457,10 @@ impl<T: DeserializeOwned + Default + Serialize + Send + Sync + 'static> Store<T>
|
||||
granular_entries.iter().map(|(k, _)| k.clone()).collect();
|
||||
let mut removed_keys = Vec::new();
|
||||
{
|
||||
let mut known = self.known_granular_keys.write().unwrap_or_else(|e| e.into_inner());
|
||||
let mut known = self
|
||||
.known_granular_keys
|
||||
.write()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
for old_k in known.iter() {
|
||||
if !new_keys.contains(old_k) {
|
||||
removed_keys.push(old_k.clone());
|
||||
@@ -473,7 +479,12 @@ impl<T: DeserializeOwned + Default + Serialize + Send + Sync + 'static> Store<T>
|
||||
|
||||
if self
|
||||
.queue
|
||||
.push_batch(self.key.clone(), batch_inserts.clone(), removed_keys.clone(), self.flushed.clone())
|
||||
.push_batch(
|
||||
self.key.clone(),
|
||||
batch_inserts.clone(),
|
||||
removed_keys.clone(),
|
||||
self.flushed.clone(),
|
||||
)
|
||||
.is_none()
|
||||
{
|
||||
tracing::warn!(
|
||||
@@ -521,7 +532,10 @@ impl<T: DeserializeOwned + Default + Serialize + Send + Sync + 'static> Store<T>
|
||||
};
|
||||
|
||||
let full_bytes_res = serde_json::to_vec(&new_snapshot);
|
||||
let granular_entries = serde_json::to_value(&new_snapshot)
|
||||
let granular_entries = full_bytes_res
|
||||
.as_ref()
|
||||
.ok()
|
||||
.and_then(|bytes| serde_json::from_slice::<serde_json::Value>(bytes).ok())
|
||||
.map(|val| Self::extract_granular_entries(&self.key, &val))
|
||||
.unwrap_or_default();
|
||||
|
||||
@@ -529,7 +543,10 @@ impl<T: DeserializeOwned + Default + Serialize + Send + Sync + 'static> Store<T>
|
||||
granular_entries.iter().map(|(k, _)| k.clone()).collect();
|
||||
let mut removed_keys = Vec::new();
|
||||
{
|
||||
let mut known = self.known_granular_keys.write().unwrap_or_else(|e| e.into_inner());
|
||||
let mut known = self
|
||||
.known_granular_keys
|
||||
.write()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
for old_k in known.iter() {
|
||||
if !new_keys.contains(old_k) {
|
||||
removed_keys.push(old_k.clone());
|
||||
@@ -548,7 +565,12 @@ impl<T: DeserializeOwned + Default + Serialize + Send + Sync + 'static> Store<T>
|
||||
|
||||
if let Some(rx) = self
|
||||
.queue
|
||||
.push_batch_async(self.key.clone(), batch_inserts, removed_keys, self.flushed.clone())
|
||||
.push_batch_async(
|
||||
self.key.clone(),
|
||||
batch_inserts,
|
||||
removed_keys,
|
||||
self.flushed.clone(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
let _ = rx.await;
|
||||
|
||||
+15
-726
@@ -1,23 +1,6 @@
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Extracts the current contents of the OS clipboard (text, HTML, files, and image).
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct ReadClipboardTool {}
|
||||
|
||||
/// Writes contents to the OS clipboard. You can provide any combination of text, HTML, file paths, or an image path.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct WriteClipboardTool {
|
||||
/// Optional plain text to write to the clipboard.
|
||||
pub text: Option<String>,
|
||||
/// Optional HTML to write to the clipboard (useful for rich text pasting).
|
||||
pub html: Option<String>,
|
||||
/// Optional absolute file paths to write to the clipboard as a Windows File Drop.
|
||||
pub files: Option<Vec<String>>,
|
||||
/// Optional absolute path to a local image to write to the clipboard.
|
||||
pub image_path: Option<String>,
|
||||
}
|
||||
|
||||
/// Create new entities in the knowledge graph. Note: entity_type MUST ALWAYS be PascalCase (e.g. DatabaseTable, McpTool, File).
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct CreateEntitiesTool {
|
||||
@@ -166,39 +149,6 @@ pub struct VisualizeGraphTool {
|
||||
pub namespace: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum StickyNoteAction {
|
||||
#[serde(alias = "ADD", alias = "Add", alias = "create", alias = "insert")]
|
||||
Add,
|
||||
#[serde(alias = "READ", alias = "Read", alias = "list", alias = "get")]
|
||||
Read,
|
||||
#[serde(alias = "DELETE", alias = "Delete", alias = "remove", alias = "rm")]
|
||||
Delete,
|
||||
#[serde(alias = "CLEAR", alias = "Clear", alias = "purge")]
|
||||
Clear,
|
||||
}
|
||||
|
||||
/// Ephemeral sticky notes with TTL for scratchpad items, ports, and transient notes.
|
||||
/// Actions:
|
||||
/// - 'add': Add note. Required: content. Optional: ttl_seconds, session_only.
|
||||
/// - 'read': Read all active notes.
|
||||
/// - 'delete': Delete note. Required: index (1-based integer).
|
||||
/// - 'clear': Clear all active notes.
|
||||
/// Next steps on error: Ensure required fields for the specific action are supplied.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct StickyNotesTool {
|
||||
/// Action to perform: 'add', 'read', 'delete', or 'clear'.
|
||||
pub action: StickyNoteAction,
|
||||
/// Content of the sticky note (required for 'add').
|
||||
pub content: Option<String>,
|
||||
/// 1-indexed position of the sticky note to delete (required for 'delete').
|
||||
pub index: Option<usize>,
|
||||
/// Optional time-to-live in seconds (for 'add').
|
||||
pub ttl_seconds: Option<u64>,
|
||||
/// Optional flag to auto-expire the note at the end of the current session (for 'add').
|
||||
pub session_only: Option<bool>,
|
||||
}
|
||||
|
||||
/// Condense or summarize an entity's observations to reduce size.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
@@ -209,124 +159,6 @@ pub struct CondenseEntityTool {
|
||||
pub summarized_observations: Vec<String>,
|
||||
}
|
||||
|
||||
/// Add a new task to the task tracker.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct AddTaskTool {
|
||||
/// The title of the task.
|
||||
pub title: String,
|
||||
/// A detailed description of the task.
|
||||
pub description: String,
|
||||
/// The associated git branch, if any.
|
||||
pub git_branch: Option<String>,
|
||||
/// Optional parent task ID to create a nested sub-task.
|
||||
pub parent_id: Option<String>,
|
||||
/// Optional list of task IDs this task depends on.
|
||||
pub dependencies: Option<Vec<String>>,
|
||||
/// Optional namespace (defaults to 'default').
|
||||
pub namespace: Option<String>,
|
||||
/// Optional repository name.
|
||||
pub repo_name: Option<String>,
|
||||
/// Optional task priority: 'low', 'medium', 'high', 'urgent'.
|
||||
pub priority: Option<String>,
|
||||
/// Optional assigned subagent role or identifier.
|
||||
pub assigned_agent: Option<String>,
|
||||
/// Optional verification command to validate criteria completion.
|
||||
pub verification_command: Option<String>,
|
||||
}
|
||||
|
||||
/// Delete a task and all its children.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct DeleteTaskTool {
|
||||
/// The ID of the task to delete.
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
/// Update the status of an existing task.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct UpdateTaskStatusTool {
|
||||
/// The ID of the task to update.
|
||||
pub id: String,
|
||||
/// The new status of the task (e.g., 'pending', 'completed', 'cancelled').
|
||||
#[schemars(description = "Must be 'pending', 'completed', or 'cancelled'")]
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
/// List all currently active tasks.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct ListActiveTasksTool {
|
||||
/// Optional git branch to filter tasks by.
|
||||
pub git_branch: Option<String>,
|
||||
/// Optional summary level: 'compact' (IDs and titles), 'detailed', or 'full'. Defaults to 'detailed'.
|
||||
pub summary_level: Option<String>,
|
||||
/// Optional token budget cap. Truncates results to stay within max_tokens.
|
||||
pub max_tokens: Option<usize>,
|
||||
}
|
||||
|
||||
/// Store a reusable code snippet.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct StoreSnippetTool {
|
||||
/// The name of the snippet.
|
||||
pub name: String,
|
||||
/// The programming language of the snippet.
|
||||
pub language: String,
|
||||
/// The code snippet content.
|
||||
pub code: String,
|
||||
/// A description of what the snippet does.
|
||||
pub description: String,
|
||||
/// Optional classification tags (e.g. ['rust', 'async', 'mcp']).
|
||||
pub tags: Option<Vec<String>>,
|
||||
/// Optional project namespace.
|
||||
pub namespace: Option<String>,
|
||||
/// Optional repository name.
|
||||
pub repo_name: Option<String>,
|
||||
/// Optional origin file path where the snippet was extracted from.
|
||||
pub origin_file: Option<String>,
|
||||
/// Optional line range in origin file (e.g. 'L10-L45').
|
||||
pub line_range: Option<String>,
|
||||
}
|
||||
|
||||
/// Add classification tags to an existing code snippet.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct TagSnippetTool {
|
||||
/// The name of the snippet to tag.
|
||||
pub name: String,
|
||||
/// Array of tags (e.g. ['rust', 'async', 'mcp']).
|
||||
pub tags: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum SnippetSearchMode {
|
||||
#[serde(alias = "HYBRID", alias = "Hybrid")]
|
||||
Hybrid,
|
||||
#[serde(alias = "KEYWORD", alias = "Keyword")]
|
||||
Keyword,
|
||||
#[serde(alias = "SEMANTIC", alias = "Semantic")]
|
||||
Semantic,
|
||||
}
|
||||
|
||||
/// Search stored code snippets using Hybrid BM25 + Vector ranking or keyword search.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct SearchSnippetsTool {
|
||||
/// The search query.
|
||||
pub query: String,
|
||||
/// Search mode: 'hybrid' (BM25 + Semantic Tags), 'keyword', or 'semantic'. Defaults to 'hybrid'.
|
||||
pub mode: Option<SnippetSearchMode>,
|
||||
/// Optional classification tags to filter by.
|
||||
pub tags: Option<Vec<String>>,
|
||||
/// Limit the number of results to avoid context limit overflow. Defaults to 10.
|
||||
pub limit: Option<usize>,
|
||||
/// Include the full code content of the matched items. If false, returns only IDs and descriptions. Defaults to false.
|
||||
pub include_body: Option<bool>,
|
||||
}
|
||||
|
||||
/// Delete a stored code snippet.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct DeleteSnippetTool {
|
||||
/// The name of the snippet to delete.
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum SubgraphFormat {
|
||||
@@ -351,15 +183,6 @@ pub struct GetSubgraphTool {
|
||||
pub max_tokens: Option<usize>,
|
||||
}
|
||||
|
||||
/// Search historical error fixes using keyword search or semantic vector similarity and return matching solutions.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct SuggestErrorFixTool {
|
||||
/// The exact stack trace or error signature.
|
||||
pub stack_trace: String,
|
||||
/// Maximum number of suggested fixes to return. Defaults to 3.
|
||||
pub limit: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CheckpointAction {
|
||||
@@ -382,66 +205,6 @@ pub struct ManageCheckpointTool {
|
||||
pub namespace: Option<String>,
|
||||
}
|
||||
|
||||
/// Save a named memory snapshot checkpoint before risky operations or subagent invocations.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct CheckpointStateTool {
|
||||
/// The unique name for this state snapshot checkpoint.
|
||||
pub name: String,
|
||||
/// Optional description of why this checkpoint was created.
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
/// Restore a previously saved memory snapshot checkpoint.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct RestoreStateTool {
|
||||
/// The name of the checkpoint to restore.
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
/// Log an architectural decision record (ADR).
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct LogDecisionTool {
|
||||
/// The title of the decision.
|
||||
pub title: String,
|
||||
/// The context or problem requiring a decision.
|
||||
pub context: String,
|
||||
/// The decision made.
|
||||
pub decision: String,
|
||||
/// The consequence of the decision.
|
||||
#[serde(alias = "consequences")]
|
||||
pub consequence: String,
|
||||
/// The ID of an older ADR this decision supersedes, if any.
|
||||
pub supersedes: Option<String>,
|
||||
/// Optional project namespace.
|
||||
pub namespace: Option<String>,
|
||||
/// Optional repository name.
|
||||
pub repo_name: Option<String>,
|
||||
/// Optional author or architect behind the decision.
|
||||
pub author: Option<String>,
|
||||
/// Optional affected system components or crates.
|
||||
pub affected_components: Option<Vec<String>>,
|
||||
/// Optional alternative designs or libraries considered.
|
||||
pub alternatives_considered: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
/// Query architectural decision records.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct QueryDecisionsTool {
|
||||
/// Optional search query.
|
||||
pub query: Option<String>,
|
||||
/// Limit the number of results to avoid context limit overflow. Defaults to 5.
|
||||
pub limit: Option<usize>,
|
||||
/// Include the full body/content of the matched ADRs. If false, returns only titles and IDs (recommended for LLMs to prevent context bloat). Defaults to false.
|
||||
pub include_body: Option<bool>,
|
||||
}
|
||||
|
||||
/// Delete an architectural decision record (ADR).
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct DeleteDecisionTool {
|
||||
/// The ID of the decision to delete.
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
/// Merge two entities in the knowledge graph into one.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct MergeEntitiesTool {
|
||||
@@ -455,19 +218,6 @@ pub struct MergeEntitiesTool {
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct FindOrphansTool {}
|
||||
|
||||
/// Record a user preference or behavior to adapt future interactions.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct LearnPreferenceTool {
|
||||
/// The key for the preference.
|
||||
pub key: String,
|
||||
/// The value of the preference.
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
/// Read all learned user preferences.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct ReadPreferencesTool {}
|
||||
|
||||
/// Log a complex error and its fix for future reference.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct LogErrorFixTool {
|
||||
@@ -497,44 +247,6 @@ pub struct SearchErrorFixesTool {
|
||||
/// Include the full solution details. If false, returns only error signatures. Defaults to false.
|
||||
pub include_body: Option<bool>,
|
||||
}
|
||||
|
||||
/// Pin a file to keep it explicitly in the context workspace.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct PinFileTool {
|
||||
/// The namespace to pin the file in.
|
||||
#[serde(default = "crate::models::default_namespace")]
|
||||
pub namespace: String,
|
||||
/// The absolute path of the file to pin.
|
||||
pub file_path: String,
|
||||
/// The associated git branch, if any.
|
||||
pub git_branch: Option<String>,
|
||||
/// Optional reason for pinning this file.
|
||||
pub pin_reason: Option<String>,
|
||||
/// Optional symbol focus or line range.
|
||||
pub symbol_or_line: Option<String>,
|
||||
/// Optional repository name.
|
||||
pub repo_name: Option<String>,
|
||||
}
|
||||
|
||||
/// Unpin a file from the context workspace.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct UnpinFileTool {
|
||||
/// The namespace the file is pinned in.
|
||||
#[serde(default = "crate::models::default_namespace")]
|
||||
pub namespace: String,
|
||||
/// The absolute path of the file to unpin.
|
||||
pub file_path: String,
|
||||
}
|
||||
|
||||
/// List all currently pinned files.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct ListPinnedFilesTool {
|
||||
/// Optional namespace to filter by.
|
||||
pub namespace: Option<String>,
|
||||
/// Optional git branch to filter by.
|
||||
pub git_branch: Option<String>,
|
||||
}
|
||||
|
||||
/// Add a summary of the current session.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct AddSessionSummaryTool {
|
||||
@@ -544,105 +256,6 @@ pub struct AddSessionSummaryTool {
|
||||
#[serde(default = "crate::models::default_namespace")]
|
||||
pub namespace: String,
|
||||
}
|
||||
|
||||
/// Get a timeline of major project events.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
#[allow(dead_code)]
|
||||
pub struct GetProjectTimelineTool {
|
||||
/// Optional namespace to restrict the timeline to.
|
||||
pub namespace: Option<String>,
|
||||
}
|
||||
|
||||
/// Leave a memo for the next session or agent.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct LeaveHandoffMemoTool {
|
||||
/// The content of the memo.
|
||||
pub content: String,
|
||||
/// The namespace to leave the memo in.
|
||||
#[serde(default = "crate::models::default_namespace")]
|
||||
pub namespace: String,
|
||||
}
|
||||
|
||||
/// Read pending handoff memos.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct ReadHandoffMemosTool {
|
||||
/// Optional namespace to restrict the read to.
|
||||
pub namespace: Option<String>,
|
||||
}
|
||||
|
||||
/// Clear handoff memos after reading.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct ClearHandoffMemosTool {
|
||||
/// Array of memo IDs to clear.
|
||||
pub ids: Vec<String>,
|
||||
}
|
||||
|
||||
/// Update the environment fingerprint (e.g., OS, tool versions).
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct UpdateEnvFingerprintTool {
|
||||
/// The namespace to update the fingerprint for.
|
||||
#[serde(default = "crate::models::default_namespace")]
|
||||
pub namespace: String,
|
||||
/// A map of tool names to their versions.
|
||||
pub tool_versions: std::collections::HashMap<String, String>,
|
||||
}
|
||||
|
||||
/// Read the environment fingerprint.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct ReadEnvFingerprintTool {
|
||||
/// The namespace to read the fingerprint for.
|
||||
#[serde(default = "crate::models::default_namespace")]
|
||||
pub namespace: String,
|
||||
}
|
||||
|
||||
/// Log a required environment variable or configuration.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct LogEnvRequirementTool {
|
||||
/// The namespace.
|
||||
#[serde(default = "crate::models::default_namespace")]
|
||||
pub namespace: String,
|
||||
/// The environment variable key (e.g., DATABASE_URL).
|
||||
pub key: String,
|
||||
/// A description of what the variable is used for.
|
||||
pub description: String,
|
||||
/// Whether the variable contains a secret.
|
||||
pub is_secret: bool,
|
||||
}
|
||||
|
||||
/// Add a new milestone.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct AddMilestoneTool {
|
||||
/// The title of the milestone.
|
||||
pub title: String,
|
||||
/// The namespace for the milestone.
|
||||
#[serde(default = "crate::models::default_namespace")]
|
||||
pub namespace: String,
|
||||
/// Optional target completion date.
|
||||
pub target_date: Option<String>,
|
||||
/// Optional description of milestone scope.
|
||||
pub description: Option<String>,
|
||||
/// Optional deliverables or task checklist.
|
||||
pub deliverables: Option<Vec<String>>,
|
||||
/// Optional repository name.
|
||||
pub repo_name: Option<String>,
|
||||
}
|
||||
|
||||
/// Update the status of a milestone.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct UpdateMilestoneTool {
|
||||
/// The ID of the milestone to update.
|
||||
pub id: String,
|
||||
/// The new status (e.g., 'active', 'completed').
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
/// List all milestones.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct ListMilestonesTool {
|
||||
/// Optional namespace to restrict the list to.
|
||||
pub namespace: Option<String>,
|
||||
}
|
||||
|
||||
/// Generate a standup report for a specific time window.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct GenerateStandupReportTool {
|
||||
@@ -652,165 +265,6 @@ pub struct GenerateStandupReportTool {
|
||||
/// The number of hours to look back for activity.
|
||||
pub hours_lookback: u64,
|
||||
}
|
||||
|
||||
/// Register a new infrastructure environment.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct RegisterEnvironmentTool {
|
||||
/// The namespace.
|
||||
#[serde(default = "crate::models::default_namespace")]
|
||||
pub namespace: String,
|
||||
/// The name of the environment (e.g., 'staging', 'prod').
|
||||
pub name: String,
|
||||
/// The URL or connection string for the environment.
|
||||
pub url: String,
|
||||
/// A description of the environment.
|
||||
pub description: String,
|
||||
/// Whether a VPN is required to access the environment.
|
||||
pub requires_vpn: bool,
|
||||
}
|
||||
|
||||
/// Get details about a registered environment.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct GetEnvironmentDetailsTool {
|
||||
/// The namespace to retrieve details for.
|
||||
#[serde(default = "crate::models::default_namespace")]
|
||||
pub namespace: String,
|
||||
}
|
||||
|
||||
/// Add an item to the PR checklist.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct AddPrChecklistItemTool {
|
||||
/// The namespace.
|
||||
#[serde(default = "crate::models::default_namespace")]
|
||||
pub namespace: String,
|
||||
/// The description of the checklist item.
|
||||
pub description: String,
|
||||
/// Optional checklist category (e.g. 'Security', 'Testing', 'Formatting').
|
||||
pub category: Option<String>,
|
||||
/// Optional automated verification command or script.
|
||||
pub automated_check: Option<String>,
|
||||
/// Optional checked state.
|
||||
pub is_checked: Option<bool>,
|
||||
/// Optional repository name.
|
||||
pub repo_name: Option<String>,
|
||||
}
|
||||
|
||||
/// Get the PR checklist.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct GetPrChecklistTool {
|
||||
/// The namespace.
|
||||
#[serde(default = "crate::models::default_namespace")]
|
||||
pub namespace: String,
|
||||
}
|
||||
|
||||
/// Clear the PR checklist.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct ClearPrChecklistTool {
|
||||
/// The namespace.
|
||||
#[serde(default = "crate::models::default_namespace")]
|
||||
pub namespace: String,
|
||||
}
|
||||
|
||||
/// Log a technical debt record.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct LogTechDebtTool {
|
||||
/// The namespace.
|
||||
#[serde(default = "crate::models::default_namespace")]
|
||||
pub namespace: String,
|
||||
/// A description of the technical debt.
|
||||
pub description: String,
|
||||
/// The ideal solution to resolve the debt.
|
||||
pub ideal_solution: String,
|
||||
/// The associated git commit hash, if any.
|
||||
pub git_commit: Option<String>,
|
||||
/// The associated git branch, if any.
|
||||
pub git_branch: Option<String>,
|
||||
/// Optional symbol references (e.g. ['MemoryIndex', 'switchTab']) associated with the tech debt.
|
||||
pub symbol_references: Option<Vec<String>>,
|
||||
/// Optional line range (e.g. 'L123-L145') associated with the tech debt.
|
||||
pub line_range: Option<String>,
|
||||
/// Optional debt severity: 'low', 'medium', 'high', 'critical'.
|
||||
pub severity: Option<String>,
|
||||
/// Optional repository name.
|
||||
pub repo_name: Option<String>,
|
||||
/// Optional file path where debt exists.
|
||||
pub file_path: Option<String>,
|
||||
/// Optional workaround currently in place.
|
||||
pub workaround: Option<String>,
|
||||
/// Optional effort estimate to fix (e.g. '1 hour', '2 days').
|
||||
pub effort_estimate: Option<String>,
|
||||
}
|
||||
|
||||
/// Resolve a technical debt record.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct ResolveTechDebtTool {
|
||||
/// The ID of the technical debt record to resolve.
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
/// List technical debt records.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct ListTechDebtTool {
|
||||
/// The namespace.
|
||||
#[serde(default = "crate::models::default_namespace")]
|
||||
pub namespace: String,
|
||||
/// Whether to include resolved technical debt in the results.
|
||||
pub include_resolved: bool,
|
||||
/// Optional summary level: 'compact' (IDs and descriptions), 'detailed', or 'full'. Defaults to 'detailed'.
|
||||
pub summary_level: Option<String>,
|
||||
/// Optional token budget cap. Truncates results to stay within max_tokens.
|
||||
pub max_tokens: Option<usize>,
|
||||
}
|
||||
|
||||
/// Save the current context workspace.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct SaveContextWorkspaceTool {
|
||||
/// The namespace for the workspace.
|
||||
#[serde(default = "crate::models::default_namespace")]
|
||||
pub namespace: String,
|
||||
/// The name to save the workspace as.
|
||||
pub name: String,
|
||||
/// Array of pinned file paths.
|
||||
pub pinned_files: Vec<String>,
|
||||
/// Array of active task IDs.
|
||||
pub active_task_ids: Vec<String>,
|
||||
/// Optional description of the context workspace purpose.
|
||||
pub description: Option<String>,
|
||||
/// Optional git branch associated with this workspace.
|
||||
pub git_branch: Option<String>,
|
||||
/// Optional repository name.
|
||||
pub repo_name: Option<String>,
|
||||
/// Optional VCS revision or commit hash.
|
||||
pub vcs_revision: Option<String>,
|
||||
}
|
||||
|
||||
/// Load a saved context workspace.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct LoadContextWorkspaceTool {
|
||||
/// The namespace of the workspace.
|
||||
#[serde(default = "crate::models::default_namespace")]
|
||||
pub namespace: String,
|
||||
/// The name of the workspace to load.
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
/// List all saved context workspaces.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct ListContextWorkspacesTool {
|
||||
/// The namespace to list workspaces for.
|
||||
pub namespace: Option<String>,
|
||||
}
|
||||
|
||||
/// Delete a saved context workspace.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct DeleteContextWorkspaceTool {
|
||||
/// The namespace of the workspace.
|
||||
#[serde(default = "crate::models::default_namespace")]
|
||||
pub namespace: String,
|
||||
/// The name of the workspace to delete.
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
/// Search across all memory stores (Graph, Tasks, Snippets, ADRs, Error Fixes, Tech Debt) using Hybrid Reciprocal Rank Fusion (BM25 + Dense Vectors).
|
||||
/// Highly recommended for discovery. Supports Lucene query syntax.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
@@ -905,16 +359,6 @@ pub struct QueryHypothesesTool {
|
||||
/// Optional search query text.
|
||||
pub query: Option<String>,
|
||||
}
|
||||
|
||||
/// Compare two saved context workspaces and return a structured diff of pinned files, active tasks, and metadata.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct DiffContextWorkspacesTool {
|
||||
/// The name of the first context workspace.
|
||||
pub workspace_a: String,
|
||||
/// The name of the second context workspace.
|
||||
pub workspace_b: String,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -990,20 +434,6 @@ pub struct SemanticCodeSearchTool {
|
||||
pub struct ReadDirectoryArchitectureTool {
|
||||
pub directory: String,
|
||||
}
|
||||
|
||||
/// Create a point-in-time state snapshot of memory stores for safety.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct CreateSnapshotTool {
|
||||
pub description: String,
|
||||
pub namespace: Option<String>,
|
||||
}
|
||||
|
||||
/// Restore a point-in-time state snapshot of memory stores.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct RestoreSnapshotTool {
|
||||
pub snapshot_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum SubagentNamespaceAction {
|
||||
@@ -1022,19 +452,6 @@ pub struct ManageSubagentNamespaceTool {
|
||||
/// For 'condense': whether to auto-purge the subagent namespace after promotion. Defaults to true.
|
||||
pub purge_after_promotion: Option<bool>,
|
||||
}
|
||||
|
||||
/// Create an isolated memory namespace for a subagent session.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct CreateSubagentNamespaceTool {
|
||||
pub subagent_id: String,
|
||||
}
|
||||
|
||||
/// Purge an isolated subagent memory namespace after session completion.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct PurgeSubagentNamespaceTool {
|
||||
pub subagent_id: String,
|
||||
}
|
||||
|
||||
/// Find all source locations and AST chunks where a specific symbol (function, struct, method, type) is referenced or called.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct FindSymbolReferencesTool {
|
||||
@@ -1068,7 +485,7 @@ pub struct QueryGitDiffsTool {
|
||||
pub limit: Option<usize>,
|
||||
}
|
||||
|
||||
/// Promote a transient sticky note or active task observation into a permanent Knowledge Graph entity.
|
||||
/// Promote a task observation or finding into a permanent Knowledge Graph entity.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct PromoteToEntityTool {
|
||||
/// Note text or content to promote.
|
||||
@@ -1090,17 +507,7 @@ pub struct AnalyzeImpactTool {
|
||||
/// Optional file path.
|
||||
pub file_path: Option<String>,
|
||||
}
|
||||
|
||||
/// Auto-condense a subagent's memory namespace and promote its high-value entities/relations to the global Knowledge Graph.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct CondenseSubagentNamespaceTool {
|
||||
/// The subagent namespace ID to consolidate.
|
||||
pub subagent_id: String,
|
||||
/// Whether to auto-purge the subagent namespace after promotion. Defaults to true.
|
||||
pub purge_after_promotion: Option<bool>,
|
||||
}
|
||||
|
||||
/// Get a consolidated 1-page executive summary of current active branch, active task, pinned files, open tech debt, and diagnostic hypotheses in 1 turn.
|
||||
/// Get a consolidated 1-page executive summary of current active branch, active task, open tech debt, and diagnostic hypotheses in 1 turn.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct GetPreflightContextTool {
|
||||
/// Optional namespace. Defaults to 'default'.
|
||||
@@ -1163,18 +570,6 @@ pub struct AutoSessionCheckpointTool {
|
||||
#[serde(default = "crate::models::default_namespace")]
|
||||
pub namespace: String,
|
||||
}
|
||||
|
||||
/// Hybrid BM25 & semantic tag search across stored code snippets, returning ranked code patterns with relevance scores.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct SearchSnippetsHybridTool {
|
||||
/// Query string (e.g. 'rate limiter', 'websocket client', 'shutdown poll').
|
||||
pub query: String,
|
||||
/// Optional tags to filter by.
|
||||
pub tags: Option<Vec<String>>,
|
||||
/// Maximum number of snippets to return. Defaults to 10.
|
||||
pub limit: Option<usize>,
|
||||
}
|
||||
|
||||
// Consolidated Smart Management Tools
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
|
||||
@@ -1322,124 +717,6 @@ pub struct MilestonesTool {
|
||||
/// Optional repository name.
|
||||
pub repo_name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum PinnedFileAction {
|
||||
#[serde(alias = "pin", alias = "PIN", alias = "Pin")]
|
||||
Pin,
|
||||
#[serde(alias = "unpin", alias = "UNPIN", alias = "Unpin")]
|
||||
Unpin,
|
||||
#[serde(alias = "list", alias = "LIST", alias = "List")]
|
||||
List,
|
||||
}
|
||||
|
||||
/// Pinned working set files management (pin, unpin, list).
|
||||
/// Actions:
|
||||
/// - 'pin': Pin file to active focus set. Required: path. Optional: namespace.
|
||||
/// - 'unpin': Unpin file from focus set. Required: path. Optional: namespace.
|
||||
/// - 'list': List pinned files. Optional: namespace.
|
||||
/// Next steps on error: Provide exact file path for pin/unpin operations.
|
||||
#[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<String>,
|
||||
/// Optional namespace.
|
||||
pub namespace: Option<String>,
|
||||
/// Optional reason for pinning this file.
|
||||
pub pin_reason: Option<String>,
|
||||
/// Optional symbol focus or line range.
|
||||
pub symbol_or_line: Option<String>,
|
||||
/// Optional repository name.
|
||||
pub repo_name: Option<String>,
|
||||
/// Optional git branch.
|
||||
pub git_branch: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ContextWorkspaceAction {
|
||||
#[serde(alias = "save", alias = "SAVE", alias = "Save")]
|
||||
Save,
|
||||
#[serde(alias = "load", alias = "LOAD", alias = "Load")]
|
||||
Load,
|
||||
#[serde(alias = "list", alias = "LIST", alias = "List")]
|
||||
List,
|
||||
#[serde(alias = "delete", alias = "DELETE", alias = "Delete")]
|
||||
Delete,
|
||||
#[serde(alias = "diff", alias = "DIFF", alias = "Diff")]
|
||||
Diff,
|
||||
}
|
||||
|
||||
/// Context workspace snapshots management (save, load, list, delete, diff).
|
||||
/// Actions:
|
||||
/// - 'save': Save current pinned files & task IDs. Required: name. Optional: pinned_files, active_task_ids, namespace.
|
||||
/// - 'load': Restore saved workspace state. Required: name. Optional: namespace.
|
||||
/// - 'list': List saved context workspaces. Optional: namespace.
|
||||
/// - 'delete': Delete saved workspace. Required: name. Optional: namespace.
|
||||
/// - 'diff': Compare two context workspaces. Required: name, other_name. Optional: namespace.
|
||||
/// Next steps on error: Check workspace name or list existing workspaces first.
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema)]
|
||||
pub struct ContextWorkspacesTool {
|
||||
/// Action to perform: 'save', 'load', 'list', 'delete', or 'diff'.
|
||||
pub action: ContextWorkspaceAction,
|
||||
/// Workspace name (required for 'save', 'load', 'delete', 'diff').
|
||||
pub name: Option<String>,
|
||||
/// Second workspace name (required for 'diff').
|
||||
pub other_name: Option<String>,
|
||||
/// Pinned file paths (for 'save').
|
||||
pub pinned_files: Option<Vec<String>>,
|
||||
/// Active task IDs (for 'save').
|
||||
pub active_task_ids: Option<Vec<String>>,
|
||||
/// Optional namespace.
|
||||
pub namespace: Option<String>,
|
||||
/// Optional description of the context workspace purpose.
|
||||
pub description: Option<String>,
|
||||
/// Optional git branch associated with this workspace.
|
||||
pub git_branch: Option<String>,
|
||||
/// Optional repository name.
|
||||
pub repo_name: Option<String>,
|
||||
/// Optional VCS revision or commit hash.
|
||||
pub vcs_revision: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum PrChecklistAction {
|
||||
#[serde(alias = "add", alias = "ADD", alias = "Add")]
|
||||
Add,
|
||||
#[serde(alias = "get", alias = "GET", alias = "Get")]
|
||||
Get,
|
||||
#[serde(alias = "clear", alias = "CLEAR", alias = "Clear")]
|
||||
Clear,
|
||||
}
|
||||
|
||||
/// Pre-commit and Pull Request checklist management (add, get, clear).
|
||||
/// Actions:
|
||||
/// - 'add': Add PR checklist item. Required: description. Optional: namespace.
|
||||
/// - 'get': Get PR checklist items. Optional: namespace.
|
||||
/// - 'clear': Clear PR checklist. Optional: namespace.
|
||||
/// Next steps on error: Provide description for 'add' action.
|
||||
#[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<String>,
|
||||
/// Optional namespace.
|
||||
pub namespace: Option<String>,
|
||||
/// Optional checklist category (e.g. 'Security', 'Testing', 'Formatting').
|
||||
pub category: Option<String>,
|
||||
/// Optional automated verification command or script.
|
||||
pub automated_check: Option<String>,
|
||||
/// Optional checked state.
|
||||
pub is_checked: Option<bool>,
|
||||
/// Optional repository name.
|
||||
pub repo_name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum SnippetAction {
|
||||
@@ -1535,6 +812,12 @@ pub struct DecisionsTool {
|
||||
pub affected_components: Option<Vec<String>>,
|
||||
/// Optional alternative designs or libraries considered.
|
||||
pub alternatives_considered: Option<Vec<String>>,
|
||||
/// Optional superseded ADR ID.
|
||||
pub supersedes: Option<String>,
|
||||
/// Limit the number of query results. Defaults to 20.
|
||||
pub limit: Option<usize>,
|
||||
/// Include the full body of the matched decisions. Defaults to true.
|
||||
pub include_body: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
|
||||
@@ -1562,8 +845,10 @@ pub struct TechDebtTool {
|
||||
#[serde(alias = "tech_debt_id", alias = "debt_id")]
|
||||
pub id: Option<String>,
|
||||
/// Tech debt description (required for 'log').
|
||||
#[serde(alias = "desc", alias = "title")]
|
||||
#[serde(alias = "desc")]
|
||||
pub description: Option<String>,
|
||||
/// Optional title or summary of tech debt.
|
||||
pub title: Option<String>,
|
||||
/// Ideal solution (optional for 'log').
|
||||
#[serde(alias = "solution", alias = "fix")]
|
||||
pub ideal_solution: Option<String>,
|
||||
@@ -1591,6 +876,10 @@ pub struct TechDebtTool {
|
||||
pub workaround: Option<String>,
|
||||
/// Optional effort estimate to fix.
|
||||
pub effort_estimate: Option<String>,
|
||||
/// Summary level for list action: 'compact', 'detailed', or 'full'.
|
||||
pub summary_level: Option<String>,
|
||||
/// Optional token budget cap for list output.
|
||||
pub max_tokens: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
|
||||
|
||||
Reference in new issue
Block a user