refactor: address 5-pass audit findings for antipatterns, bottlenecks, memory efficiency, and LLM handlers
This commit is contained in:
1 parent
626403900f
commit
924b6d09fa
30 files changed
+1120
-503
No files matched your search
@@ -59,6 +59,9 @@ impl McpTool for ReadFileSkeletonHandler {
|
||||
let mut result_skeleton = String::new();
|
||||
|
||||
fn extract_skeleton(node: Node, code: &str, out: &mut String, depth: usize) {
|
||||
if depth > 128 {
|
||||
return;
|
||||
}
|
||||
let kind = node.kind();
|
||||
|
||||
let is_structural = matches!(
|
||||
@@ -106,7 +109,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);
|
||||
extract_skeleton(child, code, out, depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,18 @@ impl McpTool for QueryGraphPathHandler {
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
state.read_graph(|graph| {
|
||||
let max_depth = req.max_depth.unwrap_or(5);
|
||||
|
||||
// Pre-index relations into an adjacency map for O(1) neighbor lookups
|
||||
let mut adj: std::collections::HashMap<&str, Vec<(&str, &str, bool)>> = std::collections::HashMap::new();
|
||||
for rel in &graph.relations {
|
||||
adj.entry(rel.from.as_str())
|
||||
.or_default()
|
||||
.push((rel.to.as_str(), rel.relation_type.as_str(), false));
|
||||
adj.entry(rel.to.as_str())
|
||||
.or_default()
|
||||
.push((rel.from.as_str(), rel.relation_type.as_str(), true));
|
||||
}
|
||||
|
||||
let mut queue: std::collections::VecDeque<&str> = std::collections::VecDeque::new();
|
||||
let mut visited: std::collections::HashSet<&str> = std::collections::HashSet::new();
|
||||
let mut parents: std::collections::HashMap<&str, (&str, &str, bool)> =
|
||||
@@ -49,23 +61,14 @@ impl McpTool for QueryGraphPathHandler {
|
||||
}
|
||||
nodes_at_current_depth -= 1;
|
||||
if current_depth < max_depth {
|
||||
for rel in &graph.relations {
|
||||
if rel.from == current && !visited.contains(rel.to.as_str()) {
|
||||
visited.insert(rel.to.as_str());
|
||||
parents.insert(
|
||||
rel.to.as_str(),
|
||||
(current, rel.relation_type.as_str(), false),
|
||||
);
|
||||
queue.push_back(rel.to.as_str());
|
||||
nodes_at_next_depth += 1;
|
||||
} else if rel.to == current && !visited.contains(rel.from.as_str()) {
|
||||
visited.insert(rel.from.as_str());
|
||||
parents.insert(
|
||||
rel.from.as_str(),
|
||||
(current, rel.relation_type.as_str(), true),
|
||||
);
|
||||
queue.push_back(rel.from.as_str());
|
||||
nodes_at_next_depth += 1;
|
||||
if let Some(neighbors) = adj.get(current) {
|
||||
for &(neighbor, rel_type, is_inverse) in neighbors {
|
||||
if !visited.contains(neighbor) {
|
||||
visited.insert(neighbor);
|
||||
parents.insert(neighbor, (current, rel_type, is_inverse));
|
||||
queue.push_back(neighbor);
|
||||
nodes_at_next_depth += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -165,25 +168,21 @@ impl McpTool for CreateRelationsHandler {
|
||||
}
|
||||
};
|
||||
let mut missing_nodes = std::collections::HashSet::new();
|
||||
state.modify_graph(|g| {
|
||||
for mut relation in req.relations {
|
||||
state.read_graph(|g| {
|
||||
for relation in &req.relations {
|
||||
if !relation.from.is_empty() && !relation.to.is_empty() {
|
||||
relation.relation_type = crate::models::normalize_relation_type(&relation.relation_type);
|
||||
let from_exists = g.entities.contains_key(&relation.from);
|
||||
let to_exists = g.entities.contains_key(&relation.to);
|
||||
if from_exists && to_exists {
|
||||
g.relations.push(relation);
|
||||
} else {
|
||||
if !from_exists {
|
||||
missing_nodes.insert(relation.from);
|
||||
}
|
||||
if !to_exists {
|
||||
missing_nodes.insert(relation.to);
|
||||
}
|
||||
if !from_exists {
|
||||
missing_nodes.insert(relation.from.clone());
|
||||
}
|
||||
if !to_exists {
|
||||
missing_nodes.insert(relation.to.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if !missing_nodes.is_empty() {
|
||||
let missing: Vec<_> = missing_nodes.into_iter().collect();
|
||||
return Err(crate::error::AppError::Internal(format!(
|
||||
@@ -191,6 +190,15 @@ impl McpTool for CreateRelationsHandler {
|
||||
missing.join(", ")
|
||||
)));
|
||||
}
|
||||
|
||||
state.modify_graph(|g| {
|
||||
for mut relation in req.relations {
|
||||
if !relation.from.is_empty() && !relation.to.is_empty() {
|
||||
relation.relation_type = crate::models::normalize_relation_type(&relation.relation_type);
|
||||
g.relations.push(relation);
|
||||
}
|
||||
}
|
||||
});
|
||||
Ok("Relations created".to_string())
|
||||
}
|
||||
}
|
||||
@@ -210,21 +218,28 @@ impl McpTool for AddObservationsHandler {
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||
let req: AddObservationsTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let mut missing_entities = Vec::new();
|
||||
state.modify_graph(|g| {
|
||||
for o in req.observations {
|
||||
if let Some(e) = g.entities.get_mut(&o.entity_name) {
|
||||
e.observations.extend(o.contents);
|
||||
} else {
|
||||
missing_entities.push(o.entity_name);
|
||||
state.read_graph(|g| {
|
||||
for o in &req.observations {
|
||||
if !g.entities.contains_key(&o.entity_name) {
|
||||
missing_entities.push(o.entity_name.clone());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if !missing_entities.is_empty() {
|
||||
return Err(crate::error::AppError::Internal(format!(
|
||||
"Error: Observations dropped for missing entities: {}",
|
||||
missing_entities.join(", ")
|
||||
)));
|
||||
}
|
||||
|
||||
state.modify_graph(|g| {
|
||||
for o in req.observations {
|
||||
if let Some(e) = g.entities.get_mut(&o.entity_name) {
|
||||
e.observations.extend(o.contents);
|
||||
}
|
||||
}
|
||||
});
|
||||
Ok("Observations added".to_string())
|
||||
}
|
||||
}
|
||||
@@ -245,15 +260,12 @@ impl McpTool for DeleteEntitiesHandler {
|
||||
let req: DeleteEntitiesTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let to_delete: std::collections::HashSet<_> = req.entity_names.into_iter().collect();
|
||||
let mut missing = Vec::new();
|
||||
state.modify_graph(|master| {
|
||||
state.read_graph(|g| {
|
||||
for name in &to_delete {
|
||||
if master.entities.remove(name).is_none() {
|
||||
if !g.entities.contains_key(name) {
|
||||
missing.push(name.clone());
|
||||
}
|
||||
}
|
||||
master
|
||||
.relations
|
||||
.retain(|r| !to_delete.contains(&r.from) && !to_delete.contains(&r.to));
|
||||
});
|
||||
|
||||
if !missing.is_empty() {
|
||||
@@ -263,6 +275,15 @@ impl McpTool for DeleteEntitiesHandler {
|
||||
)));
|
||||
}
|
||||
|
||||
state.modify_graph(|master| {
|
||||
for name in &to_delete {
|
||||
master.entities.remove(name);
|
||||
}
|
||||
master
|
||||
.relations
|
||||
.retain(|r| !to_delete.contains(&r.from) && !to_delete.contains(&r.to));
|
||||
});
|
||||
|
||||
let idx = state.get_search_index();
|
||||
for name in to_delete {
|
||||
drop(idx.delete_document(&name));
|
||||
@@ -290,22 +311,29 @@ impl McpTool for DeleteObservationsHandler {
|
||||
let req: DeleteObservationsTool =
|
||||
serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let mut missing = Vec::new();
|
||||
state.modify_graph(|master| {
|
||||
for d in req.deletions {
|
||||
if let Some(e) = master.entities.get_mut(&d.entity_name) {
|
||||
let to_rem: HashSet<_> = d.observations.into_iter().collect();
|
||||
e.observations.retain(|o| !to_rem.contains(o));
|
||||
} else {
|
||||
missing.push(d.entity_name);
|
||||
state.read_graph(|g| {
|
||||
for d in &req.deletions {
|
||||
if !g.entities.contains_key(&d.entity_name) {
|
||||
missing.push(d.entity_name.clone());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if !missing.is_empty() {
|
||||
return Err(crate::error::AppError::Internal(format!(
|
||||
"Error: Entities not found: {}. Please use the search_nodes or read_graph tools to verify the exact entity names.",
|
||||
missing.join(", ")
|
||||
)));
|
||||
}
|
||||
|
||||
state.modify_graph(|master| {
|
||||
for d in req.deletions {
|
||||
if let Some(e) = master.entities.get_mut(&d.entity_name) {
|
||||
let to_rem: HashSet<_> = d.observations.into_iter().collect();
|
||||
e.observations.retain(|o| !to_rem.contains(o));
|
||||
}
|
||||
}
|
||||
});
|
||||
Ok("Observations deleted".to_string())
|
||||
}
|
||||
}
|
||||
@@ -401,6 +429,9 @@ impl McpTool for SearchNodesHandler {
|
||||
|
||||
async fn execute(&self, args: Value, state: Arc<MemoryState>) -> crate::error::Result<String> {
|
||||
let req: SearchNodesTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
let limit = req.limit.unwrap_or(10);
|
||||
let include_body = req.include_body.unwrap_or(false);
|
||||
|
||||
let matches = if let Ok(idx) = state.search_index.read() {
|
||||
idx.search(&req.query, req.namespace.as_deref())
|
||||
.unwrap_or_default()
|
||||
@@ -409,15 +440,25 @@ impl McpTool for SearchNodesHandler {
|
||||
};
|
||||
|
||||
let data = state.read_graph(|full| -> crate::error::Result<String> {
|
||||
let mut result = BorrowedGraph::default();
|
||||
for (id, doc_type, _, _, _) in &matches {
|
||||
let mut matched_entities = Vec::new();
|
||||
for (id, doc_type, _, _, _) in matches.iter().take(limit) {
|
||||
if doc_type == "entity"
|
||||
&& let Some(e) = full.entities.get(id)
|
||||
{
|
||||
result.entities.insert(id, e);
|
||||
if include_body {
|
||||
matched_entities.push(serde_json::to_value(e)?);
|
||||
} else {
|
||||
matched_entities.push(serde_json::json!({
|
||||
"name": e.name,
|
||||
"entity_type": e.entity_type,
|
||||
"namespace": e.namespace,
|
||||
"git_branch": e.git_branch,
|
||||
"observations_count": e.observations.len()
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok::<String, crate::error::AppError>(serde_json::to_string(&result)?)
|
||||
Ok::<String, crate::error::AppError>(serde_json::to_string(&matched_entities)?)
|
||||
})?;
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
@@ -23,14 +23,14 @@ impl McpTool for LogDecisionHandler {
|
||||
|
||||
let idx = state.get_search_index();
|
||||
let mut final_id = String::new();
|
||||
let mut adrs_to_index = Vec::new();
|
||||
|
||||
state.code.adrs.modify(|adrs| {
|
||||
if let Some(superseded_id) = &req.supersedes {
|
||||
for old_adr in adrs.iter_mut() {
|
||||
if old_adr.id == *superseded_id {
|
||||
old_adr.status = "superseded".to_string();
|
||||
// Re-index the modified old ADR
|
||||
drop(idx.index_adr(old_adr));
|
||||
adrs_to_index.push(old_adr.clone());
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -48,10 +48,15 @@ impl McpTool for LogDecisionHandler {
|
||||
timestamp: crate::handlers::utils::now_secs(),
|
||||
};
|
||||
|
||||
drop(idx.index_adr(&a));
|
||||
adrs_to_index.push(a.clone());
|
||||
adrs.push(a);
|
||||
});
|
||||
|
||||
// Index in Tantivy outside the store write lock
|
||||
for adr in &adrs_to_index {
|
||||
drop(idx.index_adr(adr));
|
||||
}
|
||||
|
||||
state.record_activity("decision", &format!("Logged {}: {}", final_id, req.title), Some(&req.decision));
|
||||
Ok(format!("Logged decision {}: {}", final_id, req.title))
|
||||
}
|
||||
|
||||
+119
-41
@@ -84,6 +84,96 @@ impl McpTool for WriteClipboardHandler {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_native_clipboard_text() -> Option<String> {
|
||||
if let Ok(mut clipboard) = arboard::Clipboard::new() {
|
||||
if let Ok(text) = clipboard.get_text() {
|
||||
if !text.trim().is_empty() {
|
||||
return Some(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut cmd_wl = std::process::Command::new("wl-paste");
|
||||
cmd_wl.arg("--no-newline");
|
||||
if std::env::var("WAYLAND_DISPLAY").is_err() && std::path::Path::new("/mnt/wslg/runtime-dir").exists() {
|
||||
cmd_wl.env("WAYLAND_DISPLAY", "wayland-0");
|
||||
cmd_wl.env("XDG_RUNTIME_DIR", "/mnt/wslg/runtime-dir");
|
||||
}
|
||||
if let Ok(output) = cmd_wl.output() {
|
||||
if output.status.success() && !output.stdout.is_empty() {
|
||||
if let Ok(text) = String::from_utf8(output.stdout) {
|
||||
if !text.trim().is_empty() {
|
||||
return Some(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut cmd_xc = std::process::Command::new("xclip");
|
||||
cmd_xc.args(["-selection", "clipboard", "-o"]);
|
||||
if std::env::var("DISPLAY").is_err() {
|
||||
cmd_xc.env("DISPLAY", ":0");
|
||||
}
|
||||
if let Ok(output) = cmd_xc.output() {
|
||||
if output.status.success() && !output.stdout.is_empty() {
|
||||
if let Ok(text) = String::from_utf8(output.stdout) {
|
||||
if !text.trim().is_empty() {
|
||||
return Some(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn get_native_clipboard_image() -> Option<image::DynamicImage> {
|
||||
if let Ok(mut clipboard) = arboard::Clipboard::new() {
|
||||
if let Ok(image_data) = clipboard.get_image() {
|
||||
if let Some(img) = ImageBuffer::<image::Rgba<u8>, _>::from_raw(
|
||||
image_data.width as u32,
|
||||
image_data.height as u32,
|
||||
image_data.bytes.into_owned(),
|
||||
) {
|
||||
return Some(image::DynamicImage::ImageRgba8(img));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for mime in &["image/png", "image/jpeg", "image/bmp", "image/tiff"] {
|
||||
let mut cmd_wl = std::process::Command::new("wl-paste");
|
||||
cmd_wl.args(["--type", mime]);
|
||||
if std::env::var("WAYLAND_DISPLAY").is_err() && std::path::Path::new("/mnt/wslg/runtime-dir").exists() {
|
||||
cmd_wl.env("WAYLAND_DISPLAY", "wayland-0");
|
||||
cmd_wl.env("XDG_RUNTIME_DIR", "/mnt/wslg/runtime-dir");
|
||||
}
|
||||
if let Ok(output) = cmd_wl.output() {
|
||||
if output.status.success() && !output.stdout.is_empty() {
|
||||
if let Ok(img) = image::load_from_memory(&output.stdout) {
|
||||
return Some(img);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for mime in &["image/png", "image/jpeg", "image/bmp"] {
|
||||
let mut cmd_xc = std::process::Command::new("xclip");
|
||||
cmd_xc.args(["-selection", "clipboard", "-t", mime, "-o"]);
|
||||
if std::env::var("DISPLAY").is_err() {
|
||||
cmd_xc.env("DISPLAY", ":0");
|
||||
}
|
||||
if let Ok(output) = cmd_xc.output() {
|
||||
if output.status.success() && !output.stdout.is_empty() {
|
||||
if let Ok(img) = image::load_from_memory(&output.stdout) {
|
||||
return Some(img);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub struct ReadClipboardHandler;
|
||||
|
||||
#[async_trait]
|
||||
@@ -104,51 +194,40 @@ impl McpTool for ReadClipboardHandler {
|
||||
tokio::task::spawn_blocking(move || -> crate::error::Result<serde_json::Value> {
|
||||
let mut out = serde_json::Map::new();
|
||||
|
||||
if let Ok(mut clipboard) = arboard::Clipboard::new() {
|
||||
if let Ok(text) = clipboard.get_text() {
|
||||
if !text.trim().is_empty() {
|
||||
out.insert("text".into(), json!(text));
|
||||
}
|
||||
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();
|
||||
|
||||
if let Ok(image_data) = clipboard.get_image() {
|
||||
if let Some(img) = ImageBuffer::<image::Rgba<u8>, _>::from_raw(
|
||||
image_data.width as u32,
|
||||
image_data.height as u32,
|
||||
image_data.bytes.into_owned(),
|
||||
) {
|
||||
let mut dynamic_img = image::DynamicImage::ImageRgba8(img);
|
||||
let max_dim = 1024;
|
||||
if dynamic_img.width() > max_dim || dynamic_img.height() > max_dim {
|
||||
dynamic_img = dynamic_img.resize(max_dim, max_dim, FilterType::Lanczos3);
|
||||
}
|
||||
let rgb_img = dynamic_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 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));
|
||||
|
||||
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));
|
||||
|
||||
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));
|
||||
|
||||
// Read image bytes for base64 encoding if needed by vision
|
||||
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));
|
||||
}
|
||||
}
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -161,7 +240,6 @@ impl McpTool for ReadClipboardHandler {
|
||||
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();
|
||||
// Remove huge base64 string from final user output
|
||||
if let Some(obj) = final_obj.as_object_mut() {
|
||||
obj.remove("image_base64");
|
||||
}
|
||||
|
||||
@@ -630,8 +630,8 @@ impl McpTool for SnippetsHandler {
|
||||
let req: SnippetsTool = serde_json::from_value(args).map_err(|e| e.to_string())?;
|
||||
match req.action {
|
||||
SnippetAction::Store => {
|
||||
let name = req.query.or(req.id).ok_or_else(|| {
|
||||
crate::error::AppError::Internal("Missing required parameter 'query' or 'id' as snippet name for action 'store'. Next step: Provide snippet name in 'query' field and retry.".to_string())
|
||||
let name = req.name.or(req.query).or(req.id).ok_or_else(|| {
|
||||
crate::error::AppError::Internal("Missing required parameter 'name', 'query', or 'id' as snippet name for action 'store'. Next step: Provide snippet name in 'name' or 'query' field and retry.".to_string())
|
||||
})?;
|
||||
let lang = req.language.unwrap_or_else(|| "text".to_string());
|
||||
let code = req.code.unwrap_or_default();
|
||||
@@ -648,7 +648,7 @@ impl McpTool for SnippetsHandler {
|
||||
).await
|
||||
}
|
||||
SnippetAction::Search => {
|
||||
let q = req.query.unwrap_or_default();
|
||||
let q = req.query.or(req.name).unwrap_or_default();
|
||||
if req.hybrid.unwrap_or(false) {
|
||||
crate::handlers::meta::SearchSnippetsHybridHandler.execute(serde_json::json!({"query": q, "tags": req.tags}), state).await
|
||||
} else {
|
||||
@@ -656,14 +656,14 @@ impl McpTool for SnippetsHandler {
|
||||
}
|
||||
}
|
||||
SnippetAction::Delete => {
|
||||
let id = req.id.or(req.query).ok_or_else(|| {
|
||||
crate::error::AppError::Internal("Missing required parameter 'id' or 'query' for action 'delete'. Next step: Provide snippet ID/name in request and retry.".to_string())
|
||||
let id = req.name.or(req.id).or(req.query).ok_or_else(|| {
|
||||
crate::error::AppError::Internal("Missing required parameter 'name', 'id', or 'query' for action 'delete'. Next step: Provide snippet ID/name in request and retry.".to_string())
|
||||
})?;
|
||||
DeleteSnippetHandler.execute(serde_json::json!({"name": id}), state).await
|
||||
}
|
||||
SnippetAction::Tag => {
|
||||
let id = req.id.or(req.query).ok_or_else(|| {
|
||||
crate::error::AppError::Internal("Missing required parameter 'id' or 'query' for action 'tag'. Next step: Provide snippet ID/name and 'tags' array in request and retry.".to_string())
|
||||
let id = req.name.or(req.id).or(req.query).ok_or_else(|| {
|
||||
crate::error::AppError::Internal("Missing required parameter 'name', 'id', or 'query' for action 'tag'. Next step: Provide snippet ID/name and 'tags' array in request and retry.".to_string())
|
||||
})?;
|
||||
let tags = req.tags.unwrap_or_default();
|
||||
TagSnippetHandler.execute(serde_json::json!({"name": id, "tags": tags}), state).await
|
||||
|
||||
Reference in new issue
Block a user