345 lines
11 KiB
Rust
345 lines
11 KiB
Rust
use anyhow::{Result, anyhow};
|
|
use log::debug;
|
|
use serde_json::Value;
|
|
use std::collections::HashMap;
|
|
|
|
use crate::modes::server::common::AppState;
|
|
use crate::services::async_item_service::AsyncItemService;
|
|
use crate::services::error::CoreError;
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum ToolError {
|
|
#[error("Unknown tool: {0}")]
|
|
UnknownTool(String),
|
|
#[error("Invalid arguments: {0}")]
|
|
InvalidArguments(String),
|
|
#[error("Database error: {0}")]
|
|
Database(#[from] rusqlite::Error),
|
|
#[error("IO error: {0}")]
|
|
Io(#[from] std::io::Error),
|
|
#[error("JSON error: {0}")]
|
|
Json(#[from] serde_json::Error),
|
|
#[error("Parse error: {0}")]
|
|
Parse(#[from] strum::ParseError),
|
|
#[error("Other error: {0}")]
|
|
Other(#[from] anyhow::Error),
|
|
}
|
|
|
|
pub struct KeepTools {
|
|
state: AppState,
|
|
}
|
|
|
|
impl KeepTools {
|
|
pub fn new(state: AppState) -> Self {
|
|
Self { state }
|
|
}
|
|
|
|
pub async fn save_item(&self, args: Option<Value>) -> Result<String, ToolError> {
|
|
let args =
|
|
args.ok_or_else(|| ToolError::InvalidArguments("Missing arguments".to_string()))?;
|
|
|
|
let content = args
|
|
.get("content")
|
|
.and_then(|v| v.as_str())
|
|
.ok_or_else(|| ToolError::InvalidArguments("Missing 'content' field".to_string()))?;
|
|
|
|
let tags: Vec<String> = args
|
|
.get("tags")
|
|
.and_then(|v| v.as_array())
|
|
.map(|arr| {
|
|
arr.iter()
|
|
.filter_map(|v| v.as_str().map(|s| s.to_string()))
|
|
.collect()
|
|
})
|
|
.unwrap_or_default();
|
|
|
|
let metadata: HashMap<String, String> = args
|
|
.get("metadata")
|
|
.and_then(|v| v.as_object())
|
|
.map(|obj| {
|
|
obj.iter()
|
|
.filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
|
|
.collect()
|
|
})
|
|
.unwrap_or_default();
|
|
|
|
debug!(
|
|
"MCP: Saving item with {} bytes, {} tags, {} metadata entries",
|
|
content.len(),
|
|
tags.len(),
|
|
metadata.len()
|
|
);
|
|
|
|
let service = AsyncItemService::new(
|
|
self.state.data_dir.clone(),
|
|
self.state.db.clone(),
|
|
self.state.item_service.clone(),
|
|
self.state.cmd.clone(),
|
|
self.state.settings.clone(),
|
|
);
|
|
let item_with_meta = service
|
|
.save_item_from_mcp(content.as_bytes().to_vec(), tags, metadata)
|
|
.await
|
|
.map_err(|e| ToolError::Other(anyhow::Error::from(e)))?;
|
|
|
|
let item_id = item_with_meta
|
|
.item
|
|
.id
|
|
.ok_or_else(|| anyhow!("Failed to get item ID"))?;
|
|
|
|
Ok(format!("Successfully saved item with ID: {}", item_id))
|
|
}
|
|
|
|
pub async fn get_item(&self, args: Option<Value>) -> Result<String, ToolError> {
|
|
let args =
|
|
args.ok_or_else(|| ToolError::InvalidArguments("Missing arguments".to_string()))?;
|
|
|
|
let item_id = args.get("id").and_then(|v| v.as_i64()).ok_or_else(|| {
|
|
ToolError::InvalidArguments("Missing or invalid 'id' field".to_string())
|
|
})?;
|
|
|
|
let service = AsyncItemService::new(
|
|
self.state.data_dir.clone(),
|
|
self.state.db.clone(),
|
|
self.state.item_service.clone(),
|
|
self.state.cmd.clone(),
|
|
self.state.settings.clone(),
|
|
);
|
|
|
|
let item_with_content = match service.get_item_content(item_id).await {
|
|
Ok(iwc) => iwc,
|
|
Err(CoreError::ItemNotFound(_)) => {
|
|
return Err(ToolError::InvalidArguments(format!(
|
|
"Item {} not found",
|
|
item_id
|
|
)));
|
|
}
|
|
Err(e) => return Err(ToolError::Other(anyhow::Error::from(e))),
|
|
};
|
|
|
|
let content = String::from_utf8_lossy(&item_with_content.content).to_string();
|
|
let tags: Vec<String> = item_with_content
|
|
.item_with_meta
|
|
.tags
|
|
.iter()
|
|
.map(|t| t.name.clone())
|
|
.collect();
|
|
let metadata = item_with_content.item_with_meta.meta_as_map();
|
|
let item = item_with_content.item_with_meta.item;
|
|
|
|
let response = serde_json::json!({
|
|
"id": item_id,
|
|
"content": content,
|
|
"timestamp": item.ts.to_rfc3339(),
|
|
"size": item.size,
|
|
"compression": item.compression,
|
|
"tags": tags,
|
|
"metadata": metadata,
|
|
});
|
|
|
|
Ok(serde_json::to_string_pretty(&response)?)
|
|
}
|
|
|
|
pub async fn get_latest_item(&self, args: Option<Value>) -> Result<String, ToolError> {
|
|
let tags: Vec<String> = args
|
|
.as_ref()
|
|
.and_then(|v| v.get("tags"))
|
|
.and_then(|v| v.as_array())
|
|
.map(|arr| {
|
|
arr.iter()
|
|
.filter_map(|v| v.as_str().map(|s| s.to_string()))
|
|
.collect()
|
|
})
|
|
.unwrap_or_default();
|
|
|
|
let service = AsyncItemService::new(
|
|
self.state.data_dir.clone(),
|
|
self.state.db.clone(),
|
|
self.state.item_service.clone(),
|
|
self.state.cmd.clone(),
|
|
self.state.settings.clone(),
|
|
);
|
|
|
|
let item_with_meta = match service.find_item(vec![], tags, HashMap::new()).await {
|
|
Ok(iwm) => iwm,
|
|
Err(CoreError::ItemNotFoundGeneric) => {
|
|
return Err(ToolError::InvalidArguments("No items found".to_string()));
|
|
}
|
|
Err(e) => return Err(ToolError::Other(anyhow::Error::from(e))),
|
|
};
|
|
|
|
let item_id = item_with_meta
|
|
.item
|
|
.id
|
|
.ok_or_else(|| anyhow!("Item missing ID after find"))?;
|
|
let item_with_content = service
|
|
.get_item_content(item_id)
|
|
.await
|
|
.map_err(|e| ToolError::Other(anyhow::Error::from(e)))?;
|
|
|
|
let content = String::from_utf8_lossy(&item_with_content.content).to_string();
|
|
let tags: Vec<String> = item_with_content
|
|
.item_with_meta
|
|
.tags
|
|
.iter()
|
|
.map(|t| t.name.clone())
|
|
.collect();
|
|
let metadata = item_with_content.item_with_meta.meta_as_map();
|
|
let item = item_with_content.item_with_meta.item;
|
|
|
|
let response = serde_json::json!({
|
|
"id": item_id,
|
|
"content": content,
|
|
"timestamp": item.ts.to_rfc3339(),
|
|
"size": item.size,
|
|
"compression": item.compression,
|
|
"tags": tags,
|
|
"metadata": metadata,
|
|
});
|
|
|
|
Ok(serde_json::to_string_pretty(&response)?)
|
|
}
|
|
|
|
pub async fn list_items(&self, args: Option<Value>) -> Result<String, ToolError> {
|
|
let args_ref = args.as_ref();
|
|
let tags: Vec<String> = args_ref
|
|
.and_then(|v| v.get("tags"))
|
|
.and_then(|v| v.as_array())
|
|
.map(|arr| {
|
|
arr.iter()
|
|
.filter_map(|v| v.as_str().map(|s| s.to_string()))
|
|
.collect()
|
|
})
|
|
.unwrap_or_default();
|
|
|
|
let limit = args_ref
|
|
.and_then(|v| v.get("limit"))
|
|
.and_then(|v| v.as_u64())
|
|
.unwrap_or(10) as usize;
|
|
|
|
let offset = args_ref
|
|
.and_then(|v| v.get("offset"))
|
|
.and_then(|v| v.as_u64())
|
|
.unwrap_or(0) as usize;
|
|
|
|
let service = AsyncItemService::new(
|
|
self.state.data_dir.clone(),
|
|
self.state.db.clone(),
|
|
self.state.item_service.clone(),
|
|
self.state.cmd.clone(),
|
|
self.state.settings.clone(),
|
|
);
|
|
let mut items_with_meta = service
|
|
.list_items(tags, HashMap::new())
|
|
.await
|
|
.map_err(|e| ToolError::Other(anyhow::Error::from(e)))?;
|
|
|
|
// Sort by timestamp (newest first) and apply pagination
|
|
items_with_meta.sort_by(|a, b| b.item.ts.cmp(&a.item.ts));
|
|
let items_with_meta: Vec<_> = items_with_meta
|
|
.into_iter()
|
|
.skip(offset)
|
|
.take(limit)
|
|
.collect();
|
|
|
|
let items_info: Vec<_> = items_with_meta
|
|
.into_iter()
|
|
.map(|item_with_meta| {
|
|
let item_tags: Vec<String> =
|
|
item_with_meta.tags.iter().map(|t| t.name.clone()).collect();
|
|
let item_meta = item_with_meta.meta_as_map();
|
|
let item = item_with_meta.item;
|
|
let item_id = item.id.unwrap_or(0);
|
|
|
|
serde_json::json!({
|
|
"id": item_id,
|
|
"timestamp": item.ts.to_rfc3339(),
|
|
"size": item.size,
|
|
"compression": item.compression,
|
|
"tags": item_tags,
|
|
"metadata": item_meta
|
|
})
|
|
})
|
|
.collect();
|
|
|
|
let response = serde_json::json!({
|
|
"items": items_info,
|
|
"count": items_info.len(),
|
|
"offset": offset,
|
|
"limit": limit
|
|
});
|
|
|
|
Ok(serde_json::to_string_pretty(&response)?)
|
|
}
|
|
|
|
pub async fn search_items(&self, args: Option<Value>) -> Result<String, ToolError> {
|
|
let tags: Vec<String> = args
|
|
.as_ref()
|
|
.and_then(|v| v.get("tags"))
|
|
.and_then(|v| v.as_array())
|
|
.map(|arr| {
|
|
arr.iter()
|
|
.filter_map(|v| v.as_str().map(|s| s.to_string()))
|
|
.collect()
|
|
})
|
|
.unwrap_or_default();
|
|
|
|
let metadata: HashMap<String, String> = args
|
|
.as_ref()
|
|
.and_then(|v| v.get("metadata"))
|
|
.and_then(|v| v.as_object())
|
|
.map(|obj| {
|
|
obj.iter()
|
|
.filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
|
|
.collect()
|
|
})
|
|
.unwrap_or_default();
|
|
|
|
let service = AsyncItemService::new(
|
|
self.state.data_dir.clone(),
|
|
self.state.db.clone(),
|
|
self.state.item_service.clone(),
|
|
self.state.cmd.clone(),
|
|
self.state.settings.clone(),
|
|
);
|
|
let mut items_with_meta = service
|
|
.list_items(tags.clone(), metadata.clone())
|
|
.await
|
|
.map_err(|e| ToolError::Other(anyhow::Error::from(e)))?;
|
|
|
|
// Sort by timestamp (newest first)
|
|
items_with_meta.sort_by(|a, b| b.item.ts.cmp(&a.item.ts));
|
|
|
|
let items_info: Vec<_> = items_with_meta
|
|
.into_iter()
|
|
.map(|item_with_meta| {
|
|
let item_tags: Vec<String> =
|
|
item_with_meta.tags.iter().map(|t| t.name.clone()).collect();
|
|
let item_meta = item_with_meta.meta_as_map();
|
|
let item = item_with_meta.item;
|
|
let item_id = item.id.unwrap_or(0);
|
|
|
|
serde_json::json!({
|
|
"id": item_id,
|
|
"timestamp": item.ts.to_rfc3339(),
|
|
"size": item.size,
|
|
"compression": item.compression,
|
|
"tags": item_tags,
|
|
"metadata": item_meta
|
|
})
|
|
})
|
|
.collect();
|
|
|
|
let response = serde_json::json!({
|
|
"items": items_info,
|
|
"count": items_info.len(),
|
|
"search_criteria": {
|
|
"tags": tags,
|
|
"metadata": metadata
|
|
}
|
|
});
|
|
|
|
Ok(serde_json::to_string_pretty(&response)?)
|
|
}
|
|
}
|