1use std::str::FromStr;
2use std::sync::Arc;
3
4use axum::extract::{Extension, Path, Query, State};
5use axum::http::StatusCode;
6use axum::Json;
7use serde::{Deserialize, Serialize};
8use serde_json::json;
9use uuid::Uuid;
10
11use crate::config::settings::Settings;
12use crate::store::db::Database;
13use crate::store::memory::{
14 CreateMemoryInput, CreateRelationInput, Memory, MemoryStats, ProjectSummary, Scope,
15 SearchQuery, SearchResult, Session, UpdateMemoryInput,
16};
17
18#[derive(Debug, Serialize)]
20pub struct ApiError {
21 error: String,
22 code: String,
23}
24
25impl ApiError {
26 fn new(code: &str, message: &str) -> Self {
27 Self {
28 error: message.to_string(),
29 code: code.to_string(),
30 }
31 }
32}
33
34type ApiResult<T> = Result<Json<T>, (StatusCode, Json<ApiError>)>;
35
36fn map_err(e: crate::error::MnemeError) -> (StatusCode, Json<ApiError>) {
37 let (status, code) = match &e {
38 crate::error::MnemeError::NotFound(_) => (StatusCode::NOT_FOUND, "NOT_FOUND"),
39 crate::error::MnemeError::ProjectRequired => (StatusCode::BAD_REQUEST, "PROJECT_REQUIRED"),
40 crate::error::MnemeError::EmptyQuery => (StatusCode::BAD_REQUEST, "EMPTY_QUERY"),
41 crate::error::MnemeError::InvalidMemoryType(_) => {
42 (StatusCode::BAD_REQUEST, "INVALID_MEMORY_TYPE")
43 }
44 crate::error::MnemeError::InvalidImportance(_) => {
45 (StatusCode::BAD_REQUEST, "INVALID_IMPORTANCE")
46 }
47 crate::error::MnemeError::InvalidScope(_) => (StatusCode::BAD_REQUEST, "INVALID_SCOPE"),
48 crate::error::MnemeError::InvalidRelationType(_) => {
49 (StatusCode::BAD_REQUEST, "INVALID_RELATION_TYPE")
50 }
51 crate::error::MnemeError::RelationAlreadyExists(_, _) => {
52 (StatusCode::CONFLICT, "RELATION_EXISTS")
53 }
54 crate::error::MnemeError::SelfRelation(_) => (StatusCode::BAD_REQUEST, "SELF_RELATION"),
55 crate::error::MnemeError::DuplicateDetected(_) => (StatusCode::CONFLICT, "DUPLICATE"),
56 crate::error::MnemeError::Database(_) => {
57 (StatusCode::INTERNAL_SERVER_ERROR, "DATABASE_ERROR")
58 }
59 crate::error::MnemeError::Migration(_) => {
60 (StatusCode::INTERNAL_SERVER_ERROR, "MIGRATION_ERROR")
61 }
62 crate::error::MnemeError::Io(_) => (StatusCode::INTERNAL_SERVER_ERROR, "IO_ERROR"),
63 crate::error::MnemeError::Serialization(_) => {
64 (StatusCode::INTERNAL_SERVER_ERROR, "SERIALIZATION_ERROR")
65 }
66 crate::error::MnemeError::Config(_) => (StatusCode::BAD_REQUEST, "CONFIG_ERROR"),
67 crate::error::MnemeError::Http(_) => (StatusCode::INTERNAL_SERVER_ERROR, "HTTP_ERROR"),
68 crate::error::MnemeError::Mcp(_) => (StatusCode::INTERNAL_SERVER_ERROR, "MCP_ERROR"),
69 crate::error::MnemeError::Embeddings(_) => {
70 (StatusCode::INTERNAL_SERVER_ERROR, "EMBEDDINGS_ERROR")
71 }
72 crate::error::MnemeError::EmbeddingsDisabled => {
73 (StatusCode::SERVICE_UNAVAILABLE, "EMBEDDINGS_DISABLED")
74 }
75 crate::error::MnemeError::PeerNotFound(_) => (StatusCode::NOT_FOUND, "PEER_NOT_FOUND"),
76 crate::error::MnemeError::SyncFailed { .. } => {
77 (StatusCode::INTERNAL_SERVER_ERROR, "SYNC_FAILED")
78 }
79 crate::error::MnemeError::SyncDisabled => {
80 (StatusCode::SERVICE_UNAVAILABLE, "SYNC_DISABLED")
81 }
82 crate::error::MnemeError::UnsupportedTransport(_) => {
83 (StatusCode::BAD_REQUEST, "UNSUPPORTED_TRANSPORT")
84 }
85 crate::error::MnemeError::InvalidSyncFile(_) => {
86 (StatusCode::BAD_REQUEST, "INVALID_SYNC_FILE")
87 }
88 crate::error::MnemeError::Compression(_) => {
89 (StatusCode::INTERNAL_SERVER_ERROR, "COMPRESSION_ERROR")
90 }
91 crate::error::MnemeError::NoRecipientsConfigured => {
92 (StatusCode::SERVICE_UNAVAILABLE, "NO_RECIPIENTS")
93 }
94 crate::error::MnemeError::DecryptionFailed => {
95 (StatusCode::UNAUTHORIZED, "DECRYPTION_FAILED")
96 }
97 crate::error::MnemeError::IdentityNotLoaded => {
98 (StatusCode::UNAUTHORIZED, "IDENTITY_NOT_LOADED")
99 }
100 crate::error::MnemeError::AlreadyEncrypted(_) => {
101 (StatusCode::CONFLICT, "ALREADY_ENCRYPTED")
102 }
103 crate::error::MnemeError::NotEncrypted(_) => (StatusCode::CONFLICT, "NOT_ENCRYPTED"),
104 crate::error::MnemeError::KeyNotFound(_) => (StatusCode::NOT_FOUND, "KEY_NOT_FOUND"),
105 crate::error::MnemeError::Plugin(_) => (StatusCode::INTERNAL_SERVER_ERROR, "PLUGIN_ERROR"),
106 };
107 (status, Json(ApiError::new(code, &e.to_string())))
108}
109
110pub async fn health() -> Json<serde_json::Value> {
114 Json(json!({"status": "ok", "version": env!("CARGO_PKG_VERSION")}))
115}
116
117pub async fn dashboard(
120 Extension(settings): Extension<Arc<Settings>>,
121) -> ([(axum::http::HeaderName, &'static str); 1], String) {
122 let html = crate::http::dashboard::render_dashboard(&settings.mcp.default_project);
123 (
124 [(axum::http::HeaderName::from_static("content-type"), "text/html; charset=utf-8")],
125 html,
126 )
127}
128
129pub async fn favicon() -> ([(axum::http::HeaderName, &'static str); 1], &'static str) {
132 const FAVICON_SVG: &str = r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><rect width="100" height="100" rx="20" fill="#0d1117"/><text x="50" y="68" font-size="60" text-anchor="middle">🧠</text></svg>"##;
133 (
134 [(axum::http::HeaderName::from_static("content-type"), "image/svg+xml; charset=utf-8")],
135 FAVICON_SVG,
136 )
137}
138
139#[derive(Debug, Deserialize)]
143pub struct ListMemoriesQuery {
144 #[serde(default)]
145 project: Option<String>,
146 #[serde(default)]
147 limit: Option<u32>,
148 #[serde(default)]
149 offset: Option<u32>,
150 #[serde(rename = "type", default)]
151 memory_type: Option<String>,
152 #[serde(default)]
153 importance: Option<String>,
154}
155
156pub async fn list_memories(
158 State(db): State<Arc<Database>>,
159 Query(query): Query<ListMemoriesQuery>,
160) -> ApiResult<Vec<Memory>> {
161 let project = query.project.unwrap_or_else(Settings::infer_project);
162 let memory_type = query
163 .memory_type
164 .as_deref()
165 .map(str::parse)
166 .transpose()
167 .map_err(map_err)?;
168 let importance = query
169 .importance
170 .as_deref()
171 .map(str::parse)
172 .transpose()
173 .map_err(map_err)?;
174 let limit = query.limit.unwrap_or(20);
175 let offset = query.offset.unwrap_or(0);
176
177 let memories = db
178 .memories()
179 .list(
180 &project,
181 memory_type.as_ref(),
182 importance.as_ref(),
183 None,
184 limit,
185 offset,
186 )
187 .map_err(map_err)?;
188
189 Ok(Json(memories))
190}
191
192#[derive(Debug, Deserialize)]
194pub struct CreateMemoryBody {
195 title: String,
196 content: String,
197 #[serde(default)]
198 project: Option<String>,
199 #[serde(rename = "type", default = "default_note")]
200 memory_type: String,
201 #[serde(default = "default_medium")]
202 importance: String,
203 #[serde(default)]
204 tags: Vec<String>,
205 #[serde(default)]
206 what: Option<String>,
207 #[serde(default)]
208 why: Option<String>,
209 #[serde(default)]
210 context: Option<String>,
211 #[serde(default)]
212 learned: Option<String>,
213 #[serde(default)]
214 scope: Option<String>,
215 #[serde(default)]
216 topic_key: Option<String>,
217}
218
219fn default_note() -> String {
220 "note".into()
221}
222fn default_medium() -> String {
223 "medium".into()
224}
225
226pub async fn create_memory(
228 State(db): State<Arc<Database>>,
229 Extension(embeddings): Extension<Option<Arc<crate::embeddings::engine::EmbeddingEngine>>>,
230 Json(body): Json<CreateMemoryBody>,
231) -> ApiResult<Memory> {
232 let project = body.project.unwrap_or_else(Settings::infer_project);
233 let memory_type = body.memory_type.parse().map_err(map_err)?;
234 let importance = body.importance.parse().map_err(map_err)?;
235 let scope = body
236 .scope
237 .as_deref()
238 .map(Scope::from_str)
239 .transpose()
240 .map_err(map_err)?
241 .unwrap_or(Scope::Project);
242
243 let input = CreateMemoryInput {
244 project,
245 scope: Some(scope),
246 title: body.title,
247 content: body.content,
248 what: body.what,
249 why: body.why,
250 context: body.context,
251 learned: body.learned,
252 memory_type,
253 importance,
254 tags: body.tags,
255 topic_key: body.topic_key,
256 capture_prompt: None,
257 encrypt: false,
258 valid_from: None,
259 valid_until: None,
260 provenance: None,
261 };
262
263 let engine = embeddings.clone();
264 let embedding_store = db.embeddings();
265 let memory = db
266 .memories()
267 .save(input, engine, Some(embedding_store))
268 .map_err(map_err)?;
269 Ok(Json(memory))
270}
271
272pub async fn get_memory(
274 State(db): State<Arc<Database>>,
275 Path(id): Path<String>,
276) -> ApiResult<Memory> {
277 let id = Uuid::parse_str(&id)
278 .map_err(|e| map_err(crate::error::MnemeError::Config(e.to_string())))?;
279
280 match db.memories().get(id).map_err(map_err)? {
281 Some(memory) => Ok(Json(memory)),
282 None => Err(map_err(crate::error::MnemeError::NotFound(id))),
283 }
284}
285
286#[derive(Debug, Deserialize, Default)]
288pub struct UpdateMemoryBody {
289 #[serde(default)]
290 title: Option<String>,
291 #[serde(default)]
292 content: Option<String>,
293 #[serde(default)]
294 what: Option<String>,
295 #[serde(default)]
296 why: Option<String>,
297 #[serde(default)]
298 context: Option<String>,
299 #[serde(default)]
300 learned: Option<String>,
301 #[serde(rename = "type", default)]
302 memory_type: Option<String>,
303 #[serde(default)]
304 importance: Option<String>,
305 #[serde(default)]
306 tags: Option<Vec<String>>,
307 #[serde(default)]
308 scope: Option<String>,
309 #[serde(default)]
310 topic_key: Option<String>,
311}
312
313pub async fn update_memory(
315 State(db): State<Arc<Database>>,
316 Path(id): Path<String>,
317 Json(body): Json<UpdateMemoryBody>,
318) -> ApiResult<Memory> {
319 let id = Uuid::parse_str(&id)
320 .map_err(|e| map_err(crate::error::MnemeError::Config(e.to_string())))?;
321
322 let update = UpdateMemoryInput {
323 title: body.title,
324 content: body.content,
325 what: body.what,
326 why: body.why,
327 context: body.context,
328 learned: body.learned,
329 memory_type: body
330 .memory_type
331 .as_deref()
332 .map(str::parse)
333 .transpose()
334 .map_err(map_err)?,
335 importance: body
336 .importance
337 .as_deref()
338 .map(str::parse)
339 .transpose()
340 .map_err(map_err)?,
341 tags: body.tags,
342 scope: body
343 .scope
344 .as_deref()
345 .map(Scope::from_str)
346 .transpose()
347 .map_err(map_err)?,
348 topic_key: body.topic_key,
349 };
350
351 let memory = db.memories().update(id, update).map_err(map_err)?;
352 Ok(Json(memory))
353}
354
355pub async fn delete_memory(
357 State(db): State<Arc<Database>>,
358 Path(id): Path<String>,
359 Query(query): Query<DeleteMemoryQuery>,
360) -> Result<StatusCode, (StatusCode, Json<ApiError>)> {
361 let id = Uuid::parse_str(&id)
362 .map_err(|e| map_err(crate::error::MnemeError::Config(e.to_string())))?;
363
364 db.memories().delete(id, query.hard).map_err(map_err)?;
365 Ok(StatusCode::NO_CONTENT)
366}
367
368#[derive(Debug, Deserialize)]
369pub struct DeleteMemoryQuery {
370 #[serde(default)]
371 hard: bool,
372}
373
374#[derive(Debug, Deserialize)]
376pub struct SearchBody {
377 text: String,
378 #[serde(default)]
379 project: Option<String>,
380 #[serde(default)]
381 limit: Option<u32>,
382}
383
384pub async fn search_memories(
385 State(db): State<Arc<Database>>,
386 Extension(embeddings): Extension<Option<Arc<crate::embeddings::engine::EmbeddingEngine>>>,
387 Json(body): Json<SearchBody>,
388) -> ApiResult<Vec<SearchResult>> {
389 let project = body.project.unwrap_or_else(Settings::infer_project);
390 let query = SearchQuery {
391 text: body.text.clone(),
392 project: Some(project.clone()),
393 scope: None,
394 memory_type: None,
395 importance: None,
396 tags: Vec::new(),
397 limit: body.limit.unwrap_or(10),
398 include_snippet: true,
399 all_projects: false,
400 };
401
402 let weights = crate::store::search::SearchWeights::default();
403
404 let semantic_scores = if let Some(engine) = embeddings {
405 let embedding_store = db.embeddings();
406 match engine.embed(&body.text).await {
407 Ok(query_embedding) => match embedding_store.load_all_for_project(&project) {
408 Ok(all_embeddings) => {
409 let mut scores = std::collections::HashMap::new();
410 for (id, embedding) in all_embeddings {
411 let score = crate::embeddings::similarity::cosine_similarity(
412 &query_embedding,
413 &embedding,
414 );
415 scores.insert(id, score);
416 }
417 Some(scores)
418 }
419 Err(e) => {
420 tracing::warn!(error = %e, "failed to load embeddings for search");
421 None
422 }
423 },
424 Err(e) => {
425 tracing::warn!(error = %e, "failed to embed query for search");
426 None
427 }
428 }
429 } else {
430 None
431 };
432
433 let results = db
434 .memories()
435 .search(&query, &weights, semantic_scores.as_ref())
436 .map_err(map_err)?;
437 Ok(Json(results))
438}
439
440pub async fn get_relations(
444 State(_db): State<Arc<Database>>,
445 Path(_id): Path<String>,
446) -> ApiResult<Vec<serde_json::Value>> {
447 Ok(Json(vec![]))
448}
449
450#[derive(Debug, Deserialize)]
452pub struct CreateRelationBody {
453 target_id: String,
454 relation_type: String,
455 #[serde(default)]
456 confidence: Option<f32>,
457 #[serde(default)]
458 reason: Option<String>,
459}
460
461pub async fn create_relation(
463 State(db): State<Arc<Database>>,
464 Path(id): Path<String>,
465 Json(body): Json<CreateRelationBody>,
466) -> ApiResult<serde_json::Value> {
467 let source_id = Uuid::parse_str(&id)
468 .map_err(|e| map_err(crate::error::MnemeError::Config(e.to_string())))?;
469 let target_id = Uuid::parse_str(&body.target_id)
470 .map_err(|e| map_err(crate::error::MnemeError::Config(e.to_string())))?;
471 let relation_type = body.relation_type.parse().map_err(map_err)?;
472
473 let input = CreateRelationInput {
474 source_id,
475 target_id,
476 relation_type,
477 confidence: Some(body.confidence.unwrap_or(1.0)),
478 reason: body.reason,
479 };
480
481 db.memories().create_relation(input).map_err(map_err)?;
482 Ok(Json(json!({"created": true})))
483}
484
485pub async fn delete_relation(
487 State(db): State<Arc<Database>>,
488 Path(relation_id): Path<String>,
489) -> Result<StatusCode, (StatusCode, Json<ApiError>)> {
490 let id = Uuid::parse_str(&relation_id)
491 .map_err(|e| map_err(crate::error::MnemeError::Config(e.to_string())))?;
492 db.memories().delete_relation(id).map_err(map_err)?;
493 Ok(StatusCode::NO_CONTENT)
494}
495
496#[derive(Debug, Deserialize)]
500pub struct StatsQuery {
501 #[serde(default)]
502 project: Option<String>,
503}
504
505pub async fn get_stats(
506 State(db): State<Arc<Database>>,
507 Query(query): Query<StatsQuery>,
508) -> ApiResult<MemoryStats> {
509 let project = query.project.unwrap_or_else(Settings::infer_project);
510 let stats = db.memories().stats(&project).map_err(map_err)?;
511 Ok(Json(stats))
512}
513
514pub async fn list_projects(State(db): State<Arc<Database>>) -> ApiResult<Vec<ProjectSummary>> {
518 let projects = db.memories().list_projects().map_err(map_err)?;
519 Ok(Json(projects))
520}
521
522#[derive(Debug, Deserialize)]
526pub struct StartSessionBody {
527 #[serde(default)]
528 project: Option<String>,
529 #[serde(default)]
530 directory: Option<String>,
531}
532
533pub async fn start_session(
534 State(db): State<Arc<Database>>,
535 Json(body): Json<StartSessionBody>,
536) -> ApiResult<Session> {
537 let project = body.project.unwrap_or_else(Settings::infer_project);
538 let current_dir = std::env::current_dir().ok();
539 let dir = body
540 .directory
541 .as_deref()
542 .or_else(|| current_dir.as_ref().and_then(|p| p.to_str()));
543 let session = db.sessions().start(&project, dir).map_err(map_err)?;
544 Ok(Json(session))
545}
546
547#[derive(Debug, Deserialize)]
549pub struct EndSessionBody {
550 #[serde(default)]
551 summary: Option<String>,
552}
553
554pub async fn end_session(
555 State(db): State<Arc<Database>>,
556 Path(id): Path<String>,
557 Json(body): Json<EndSessionBody>,
558) -> ApiResult<Session> {
559 let id = Uuid::parse_str(&id)
560 .map_err(|e| map_err(crate::error::MnemeError::Config(e.to_string())))?;
561 let session = db
562 .sessions()
563 .end(id, body.summary.as_deref())
564 .map_err(map_err)?;
565 Ok(Json(session))
566}
567
568#[derive(Debug, Deserialize)]
570pub struct ActiveSessionQuery {
571 #[serde(default)]
572 project: Option<String>,
573}
574
575pub async fn get_active_session(
576 State(db): State<Arc<Database>>,
577 Query(query): Query<ActiveSessionQuery>,
578) -> ApiResult<Option<Session>> {
579 let project = query.project.unwrap_or_else(Settings::infer_project);
580 let session = db.sessions().get_active(&project).map_err(map_err)?;
581 Ok(Json(session))
582}
583
584#[derive(Debug, Deserialize)]
588pub struct ContextQuery {
589 #[serde(default)]
590 project: Option<String>,
591 #[serde(default)]
592 limit: Option<u32>,
593}
594
595pub async fn get_context(
596 State(db): State<Arc<Database>>,
597 Query(query): Query<ContextQuery>,
598) -> ApiResult<Vec<Memory>> {
599 let project = query.project.unwrap_or_else(Settings::infer_project);
600 let limit = query.limit.unwrap_or(10);
601 let memories = db
602 .memories()
603 .context(&project, None, limit)
604 .map_err(map_err)?;
605 Ok(Json(memories))
606}
607
608#[derive(Debug, Deserialize)]
612pub struct ExportQuery {
613 #[serde(default)]
614 project: Option<String>,
615}
616
617pub async fn export_memories(
618 State(db): State<Arc<Database>>,
619 Query(query): Query<ExportQuery>,
620) -> ApiResult<Vec<Memory>> {
621 let project = query.project.unwrap_or_else(Settings::infer_project);
622 let memories = db
623 .memories()
624 .list(&project, None, None, None, 10000, 0)
625 .map_err(map_err)?;
626 Ok(Json(memories))
627}
628
629pub async fn import_memories(
633 State(db): State<Arc<Database>>,
634 Extension(embeddings): Extension<Option<Arc<crate::embeddings::engine::EmbeddingEngine>>>,
635 Json(body): Json<Vec<Memory>>,
636) -> ApiResult<serde_json::Value> {
637 let store = db.memories();
638 let mut count = 0;
639 let engine = embeddings.clone();
640 let embedding_store = db.embeddings();
641 for mem in body {
642 let input = CreateMemoryInput {
643 project: mem.project.clone(),
644 scope: Some(mem.scope),
645 title: mem.title,
646 content: mem.content,
647 what: mem.what,
648 why: mem.why,
649 context: mem.context,
650 learned: mem.learned,
651 memory_type: mem.memory_type,
652 importance: mem.importance,
653 tags: mem.tags,
654 topic_key: mem.topic_key,
655 capture_prompt: None,
656 encrypt: false,
657 valid_from: None,
658 valid_until: None,
659 provenance: None,
660 };
661 store
662 .save(input, engine.clone(), Some(embedding_store.clone()))
663 .map_err(map_err)?;
664 count += 1;
665 }
666 Ok(Json(json!({"imported": count})))
667}
668
669#[derive(Debug, Deserialize)]
673pub struct DoctorQuery {
674 #[serde(default)]
675 project: Option<String>,
676 #[serde(default)]
677 check: Option<String>,
678}
679
680#[derive(Debug, Serialize)]
681pub struct DoctorResponse {
682 healthy: bool,
683 project: String,
684 checks: Vec<String>,
685 issues: Vec<String>,
686}
687
688pub async fn run_doctor(
689 State(db): State<Arc<Database>>,
690 Query(query): Query<DoctorQuery>,
691) -> ApiResult<DoctorResponse> {
692 let project = query.project.unwrap_or_else(Settings::infer_project);
693 let mut checks = Vec::new();
694 let issues = Vec::new();
695
696 checks.push("Database connection: OK".to_string());
697
698 let memory_count = db
699 .memories()
700 .stats(&project)
701 .map(|s| s.total_memories)
702 .unwrap_or(0);
703 let session_count = db
704 .sessions()
705 .list(&project, 1000)
706 .map(|s| s.len() as u32)
707 .unwrap_or(0);
708
709 checks.push(format!("Memories: {}", memory_count));
710 checks.push(format!("Sessions: {}", session_count));
711 checks.push("Orphaned relations: 0".to_string());
712
713 if let Some(check) = query.check {
714 if check == "relations" {
715 checks.push("Relation check: OK".to_string());
716 }
717 }
718
719 let healthy = issues.is_empty();
720
721 Ok(Json(DoctorResponse {
722 healthy,
723 project,
724 checks,
725 issues,
726 }))
727}
728
729#[derive(Debug, Deserialize)]
733pub struct SimilarBody {
734 query: String,
736 #[serde(default)]
737 project: Option<String>,
738 #[serde(default)]
739 limit: Option<u32>,
740 #[serde(default)]
741 threshold: Option<f32>,
742}
743
744pub async fn similar_memories(
745 State(db): State<Arc<Database>>,
746 Extension(embeddings): Extension<Option<Arc<crate::embeddings::engine::EmbeddingEngine>>>,
747 Json(body): Json<SimilarBody>,
748) -> ApiResult<Vec<SearchResult>> {
749 let engine = match embeddings {
750 Some(e) => e,
751 None => return Err(map_err(crate::error::MnemeError::EmbeddingsDisabled)),
752 };
753
754 let project = body.project.unwrap_or_else(Settings::infer_project);
755 let limit = body.limit.unwrap_or(5);
756 let threshold = body.threshold.unwrap_or(0.75);
757 let embedding_store = db.embeddings();
758
759 let query_embedding = if let Ok(id) = Uuid::parse_str(&body.query) {
760 match embedding_store.load(id).map_err(map_err)? {
761 Some(embedding) => embedding,
762 None => {
763 let memory = db
764 .memories()
765 .get(id)
766 .map_err(map_err)?
767 .ok_or_else(|| map_err(crate::error::MnemeError::NotFound(id)))?;
768 let text = crate::embeddings::engine::EmbeddingEngine::memory_to_text(&memory);
769 engine.embed(&text).await.map_err(map_err)?
770 }
771 }
772 } else {
773 engine.embed(&body.query).await.map_err(map_err)?
774 };
775
776 let all_embeddings = embedding_store
777 .load_all_for_project(&project)
778 .map_err(map_err)?;
779 let mut matches = Vec::new();
780 for (id, embedding) in all_embeddings {
781 let score = crate::embeddings::similarity::cosine_similarity(&query_embedding, &embedding);
782 if score >= threshold {
783 matches.push(crate::embeddings::similarity::SemanticMatch {
784 memory_id: id,
785 cosine_score: score,
786 combined_score: f64::from(score),
787 });
788 }
789 }
790 crate::embeddings::similarity::rank_by_combined_score(&mut matches);
791 matches.truncate(limit as usize);
792
793 let mut results = Vec::new();
794 for m in matches {
795 if let Some(memory) = db.memories().get(m.memory_id).map_err(map_err)? {
796 results.push(SearchResult {
797 memory,
798 score: m.combined_score,
799 snippet: None,
800 match_type: crate::store::memory::MatchType::Semantic,
801 cosine_score: Some(m.cosine_score),
802 });
803 }
804 }
805
806 Ok(Json(results))
807}
808
809#[derive(Debug, Deserialize)]
813pub struct ReindexBody {
814 #[serde(default)]
815 project: Option<String>,
816 #[allow(dead_code)]
817 #[serde(default)]
818 force: Option<bool>,
819}
820
821#[derive(Debug, Serialize)]
822pub struct ReindexResponse {
823 total: u32,
824 indexed: u32,
825 skipped: u32,
826 failed: u32,
827 duration_ms: u64,
828}
829
830pub async fn reindex_embeddings(
831 State(db): State<Arc<Database>>,
832 Extension(embeddings): Extension<Option<Arc<crate::embeddings::engine::EmbeddingEngine>>>,
833 Json(body): Json<ReindexBody>,
834) -> ApiResult<ReindexResponse> {
835 let engine = match embeddings {
836 Some(e) => e,
837 None => return Err(map_err(crate::error::MnemeError::EmbeddingsDisabled)),
838 };
839
840 let project = body.project.unwrap_or_else(Settings::infer_project);
841 let embedding_store = db.embeddings();
842 let stats = db
843 .memories()
844 .reindex_embeddings(&project, &engine, &embedding_store)
845 .await
846 .map_err(map_err)?;
847
848 Ok(Json(ReindexResponse {
849 total: stats.total,
850 indexed: stats.indexed,
851 skipped: stats.skipped,
852 failed: stats.failed,
853 duration_ms: stats.duration_ms,
854 }))
855}
856
857#[derive(Debug, Deserialize)]
860pub struct BatchSaveBody {
861 #[serde(default)]
862 project: Option<String>,
863 memories: Vec<CreateMemoryBody>,
864}
865
866#[derive(Debug, Serialize)]
867pub struct BatchSaveResponse {
868 saved: Vec<Memory>,
869 duplicates: Vec<Memory>,
870 saved_count: usize,
871 duplicate_count: usize,
872}
873
874pub async fn batch_save(
875 State(db): State<Arc<Database>>,
876 Extension(embeddings): Extension<Option<Arc<crate::embeddings::engine::EmbeddingEngine>>>,
877 Json(body): Json<BatchSaveBody>,
878) -> ApiResult<BatchSaveResponse> {
879 let project = body.project.unwrap_or_else(Settings::infer_project);
880 let mut inputs = Vec::new();
881 for item in body.memories {
882 let memory_type = item.memory_type.parse().map_err(map_err)?;
883 let importance = item.importance.parse().map_err(map_err)?;
884 let scope = item
885 .scope
886 .as_deref()
887 .map(Scope::from_str)
888 .transpose()
889 .map_err(map_err)?
890 .unwrap_or(Scope::Project);
891 inputs.push(CreateMemoryInput {
892 project: project.clone(),
893 scope: Some(scope),
894 title: item.title,
895 content: item.content,
896 what: item.what,
897 why: item.why,
898 context: item.context,
899 learned: item.learned,
900 memory_type,
901 importance,
902 tags: item.tags,
903 topic_key: item.topic_key,
904 capture_prompt: None,
905 encrypt: false,
906 valid_from: None,
907 valid_until: None,
908 provenance: None,
909 });
910 }
911
912 let engine = embeddings.clone();
913 let embedding_store = db.embeddings();
914 let (saved, duplicates) = db
915 .memories()
916 .save_batch(inputs, engine, Some(embedding_store))
917 .map_err(map_err)?;
918
919 Ok(Json(BatchSaveResponse {
920 saved_count: saved.len(),
921 duplicate_count: duplicates.len(),
922 saved,
923 duplicates,
924 }))
925}
926
927#[derive(Debug, Deserialize)]
930pub struct AuditQuery {
931 #[serde(default)]
932 project: Option<String>,
933 #[serde(default = "default_days_30")]
934 days_threshold: u32,
935}
936
937fn default_days_30() -> u32 {
938 30
939}
940
941pub async fn get_audit(
942 State(db): State<Arc<Database>>,
943 Query(query): Query<AuditQuery>,
944) -> ApiResult<crate::store::memory::AuditReport> {
945 let project = query.project.unwrap_or_else(Settings::infer_project);
946 let report = db
947 .memories()
948 .audit(&project, query.days_threshold)
949 .map_err(map_err)?;
950 Ok(Json(report))
951}
952
953#[derive(Debug, Deserialize)]
956pub struct DeduplicateBody {
957 #[serde(default)]
958 project: Option<String>,
959 #[serde(default = "default_threshold_85")]
960 threshold: f64,
961}
962
963fn default_threshold_85() -> f64 {
964 0.85
965}
966
967pub async fn deduplicate(
968 State(db): State<Arc<Database>>,
969 Json(body): Json<DeduplicateBody>,
970) -> ApiResult<Vec<crate::store::memory::DuplicateGroup>> {
971 let project = body.project.unwrap_or_else(Settings::infer_project);
972 let embedding_store = db.embeddings();
973 let groups = db
974 .memories()
975 .find_duplicates_semantic(&project, body.threshold, &embedding_store)
976 .map_err(map_err)?;
977 Ok(Json(groups))
978}
979
980#[derive(Debug, Deserialize)]
983pub struct FeedbackBody {
984 memory_id: String,
985 is_useful: bool,
986 #[serde(default)]
987 reason: Option<String>,
988}
989
990#[derive(Debug, Serialize)]
991pub struct FeedbackResponse {
992 memory_id: String,
993 feedback_id: i64,
994}
995
996pub async fn add_feedback(
997 State(db): State<Arc<Database>>,
998 Json(body): Json<FeedbackBody>,
999) -> ApiResult<FeedbackResponse> {
1000 let memory_id = Uuid::parse_str(&body.memory_id)
1001 .map_err(|e| map_err(crate::error::MnemeError::Config(e.to_string())))?;
1002 let feedback_id = db
1003 .memories()
1004 .add_feedback(memory_id, body.is_useful, body.reason.as_deref())
1005 .map_err(map_err)?;
1006 Ok(Json(FeedbackResponse {
1007 memory_id: body.memory_id,
1008 feedback_id,
1009 }))
1010}
1011
1012#[derive(Debug, Deserialize)]
1015pub struct DeprecateBody {
1016 memory_id: String,
1017 reason: String,
1018 #[serde(default)]
1019 supersedes_id: Option<String>,
1020}
1021
1022pub async fn deprecate(
1023 State(db): State<Arc<Database>>,
1024 Json(body): Json<DeprecateBody>,
1025) -> ApiResult<Memory> {
1026 let memory_id = Uuid::parse_str(&body.memory_id)
1027 .map_err(|e| map_err(crate::error::MnemeError::Config(e.to_string())))?;
1028 let supersedes_id = body
1029 .supersedes_id
1030 .as_deref()
1031 .map(Uuid::parse_str)
1032 .transpose()
1033 .map_err(|e| map_err(crate::error::MnemeError::Config(e.to_string())))?;
1034 let memory = db
1035 .memories()
1036 .deprecate(memory_id, &body.reason, supersedes_id)
1037 .map_err(map_err)?;
1038 Ok(Json(memory))
1039}
1040
1041#[derive(Debug, Deserialize)]
1044pub struct GraphQuery {
1045 #[serde(default)]
1046 project: Option<String>,
1047}
1048
1049pub async fn get_graph(
1050 State(db): State<Arc<Database>>,
1051 Query(query): Query<GraphQuery>,
1052) -> ApiResult<crate::store::memory::GraphData> {
1053 let project = query.project.unwrap_or_else(Settings::infer_project);
1054 let graph = db.memories().get_graph(&project).map_err(map_err)?;
1055 Ok(Json(graph))
1056}
1057
1058#[derive(Debug, Deserialize)]
1061pub struct SummarizeQuery {
1062 #[serde(default)]
1063 project: Option<String>,
1064 #[serde(default)]
1065 session_id: Option<String>,
1066}
1067
1068pub async fn get_summarize(
1069 State(db): State<Arc<Database>>,
1070 Query(query): Query<SummarizeQuery>,
1071) -> ApiResult<crate::store::memory::SummaryResult> {
1072 let project = query.project.unwrap_or_else(Settings::infer_project);
1073 let session_id = query
1074 .session_id
1075 .as_deref()
1076 .map(Uuid::parse_str)
1077 .transpose()
1078 .map_err(|e| map_err(crate::error::MnemeError::Config(e.to_string())))?;
1079 let summary = db
1080 .memories()
1081 .summarize(&project, session_id)
1082 .map_err(map_err)?;
1083 Ok(Json(summary))
1084}
1085
1086#[derive(Debug, Deserialize)]
1089pub struct InjectContextQuery {
1090 #[serde(default)]
1091 project: Option<String>,
1092 #[serde(default)]
1093 file: Option<String>,
1094 #[serde(default = "default_limit_10")]
1095 limit: u32,
1096}
1097
1098fn default_limit_10() -> u32 {
1099 10
1100}
1101
1102#[derive(Debug, Serialize)]
1103pub struct InjectContextResponse {
1104 context: String,
1105 project: String,
1106}
1107
1108pub async fn inject_context(
1109 State(db): State<Arc<Database>>,
1110 Query(query): Query<InjectContextQuery>,
1111) -> ApiResult<InjectContextResponse> {
1112 let project = query.project.unwrap_or_else(Settings::infer_project);
1113 let context = db
1114 .memories()
1115 .inject_context(&project, query.file.as_deref(), query.limit)
1116 .map_err(map_err)?;
1117 Ok(Json(InjectContextResponse { context, project }))
1118}
1119
1120#[derive(Debug, Deserialize)]
1123pub struct ForgetProjectBody {
1124 #[serde(default)]
1125 project: Option<String>,
1126 confirm: bool,
1127}
1128
1129#[derive(Debug, Serialize)]
1130pub struct ForgetProjectResponse {
1131 deleted: u32,
1132 project: String,
1133}
1134
1135pub async fn forget_project(
1136 State(db): State<Arc<Database>>,
1137 Json(body): Json<ForgetProjectBody>,
1138) -> ApiResult<ForgetProjectResponse> {
1139 if !body.confirm {
1140 return Err(map_err(crate::error::MnemeError::Config(
1141 "confirm must be true to forget project".into(),
1142 )));
1143 }
1144 let project = body.project.unwrap_or_else(Settings::infer_project);
1145 let deleted = db.memories().forget_project(&project).map_err(map_err)?;
1146 Ok(Json(ForgetProjectResponse { deleted, project }))
1147}
1148
1149#[derive(Debug, Deserialize)]
1152pub struct HealthQuery {
1153 #[serde(default)]
1154 project: Option<String>,
1155}
1156
1157pub async fn get_health(
1158 State(db): State<Arc<Database>>,
1159 Query(query): Query<HealthQuery>,
1160) -> ApiResult<crate::store::memory::HealthReport> {
1161 let report = db
1162 .memories()
1163 .health(query.project.as_deref())
1164 .map_err(map_err)?;
1165 Ok(Json(report))
1166}
1167
1168#[derive(Debug, Deserialize)]
1171pub struct RemindQuery {
1172 #[serde(default)]
1173 project: Option<String>,
1174 #[serde(default = "default_high")]
1175 importance: String,
1176}
1177
1178fn default_high() -> String {
1179 "high".into()
1180}
1181
1182pub async fn get_remind(
1183 State(db): State<Arc<Database>>,
1184 Query(query): Query<RemindQuery>,
1185) -> ApiResult<Vec<Memory>> {
1186 let project = query.project.unwrap_or_else(Settings::infer_project);
1187 let importance = query.importance.parse().map_err(map_err)?;
1188 let memories = db
1189 .memories()
1190 .remind(&project, &importance)
1191 .map_err(map_err)?;
1192 Ok(Json(memories))
1193}
1194
1195#[derive(Debug, Deserialize)]
1198pub struct TagSuggestBody {
1199 #[serde(default)]
1200 project: Option<String>,
1201 title: String,
1202 #[serde(default)]
1203 content: Option<String>,
1204}
1205
1206pub async fn tag_suggest(
1207 State(db): State<Arc<Database>>,
1208 Json(body): Json<TagSuggestBody>,
1209) -> ApiResult<Vec<String>> {
1210 let project = body.project.unwrap_or_else(Settings::infer_project);
1211 let tags = db
1212 .memories()
1213 .suggest_tags(&project, &body.title, body.content.as_deref())
1214 .map_err(map_err)?;
1215 Ok(Json(tags))
1216}
1217
1218#[derive(Debug, Deserialize)]
1221pub struct KnowledgeGapsQuery {
1222 #[serde(default)]
1223 project: Option<String>,
1224}
1225
1226pub async fn get_knowledge_gaps(
1227 State(db): State<Arc<Database>>,
1228 Query(query): Query<KnowledgeGapsQuery>,
1229) -> ApiResult<crate::store::memory::KnowledgeGapsReport> {
1230 let project = query.project.unwrap_or_else(Settings::infer_project);
1231 let report = db.memories().knowledge_gaps(&project).map_err(map_err)?;
1232 Ok(Json(report))
1233}
1234
1235use crate::sync::protocol::{SyncHello, SyncRequest, SyncResponse};
1238
1239pub async fn sync_hello(
1240 State(db): State<Arc<Database>>,
1241 Json(body): Json<SyncHello>,
1242) -> ApiResult<SyncHello> {
1243 let settings = Settings::load().map_err(map_err)?;
1244 let engine =
1245 crate::sync::engine::SyncEngine::new(db, settings.sync.clone()).map_err(map_err)?;
1246 let hello = engine.build_hello(&body.project).map_err(map_err)?;
1247 Ok(Json(hello))
1248}
1249
1250pub async fn sync_pull(
1251 State(db): State<Arc<Database>>,
1252 Json(body): Json<SyncRequest>,
1253) -> ApiResult<SyncResponse> {
1254 let settings = Settings::load().map_err(map_err)?;
1255 let engine =
1256 crate::sync::engine::SyncEngine::new(db, settings.sync.clone()).map_err(map_err)?;
1257 let response = engine.build_response(&body).map_err(map_err)?;
1258 Ok(Json(response))
1259}
1260
1261pub async fn sync_push(
1262 State(db): State<Arc<Database>>,
1263 Json(body): Json<SyncResponse>,
1264) -> ApiResult<serde_json::Value> {
1265 let settings = Settings::load().map_err(map_err)?;
1266 let engine =
1267 crate::sync::engine::SyncEngine::new(db, settings.sync.clone()).map_err(map_err)?;
1268 let stats = engine.apply_response(&body).map_err(map_err)?;
1269 Ok(Json(json!({
1270 "applied": stats.memories_applied,
1271 "conflicts": stats.conflicts_resolved
1272 })))
1273}
1274
1275pub async fn encrypt_memory(
1279 State(db): State<Arc<Database>>,
1280 Path(id): Path<String>,
1281) -> ApiResult<Memory> {
1282 let id = Uuid::parse_str(&id)
1283 .map_err(|e| map_err(crate::error::MnemeError::Config(e.to_string())))?;
1284 let memory = db.memories().encrypt_existing(id).map_err(map_err)?;
1285 Ok(Json(memory))
1286}
1287
1288pub async fn decrypt_memory(
1290 State(db): State<Arc<Database>>,
1291 Path(id): Path<String>,
1292) -> ApiResult<Memory> {
1293 let id = Uuid::parse_str(&id)
1294 .map_err(|e| map_err(crate::error::MnemeError::Config(e.to_string())))?;
1295 let memory = db.memories().decrypt_existing(id).map_err(map_err)?;
1296 Ok(Json(memory))
1297}
1298
1299pub async fn list_keys(
1303 State(db): State<Arc<Database>>,
1304) -> ApiResult<Vec<crate::crypto::RegisteredKey>> {
1305 let key_store = crate::crypto::KeyStore::new(db.get_conn());
1306 let keys = key_store.list().map_err(map_err)?;
1307 Ok(Json(keys))
1308}
1309
1310#[derive(Debug, Deserialize)]
1312pub struct AddKeyBody {
1313 alias: String,
1314 key: String,
1315 #[serde(default)]
1316 default: bool,
1317}
1318
1319pub async fn add_key(
1321 State(db): State<Arc<Database>>,
1322 Json(body): Json<AddKeyBody>,
1323) -> ApiResult<crate::crypto::RegisteredKey> {
1324 let recipient = crate::crypto::RecipientKey::from_string(&body.key).map_err(map_err)?;
1325 let key_store = crate::crypto::KeyStore::new(db.get_conn());
1326 let registered = key_store.add(&body.alias, &recipient).map_err(map_err)?;
1327 if body.default {
1328 key_store.set_default(registered.id).map_err(map_err)?;
1329 }
1330 Ok(Json(registered))
1331}
1332
1333pub async fn remove_key(
1335 State(db): State<Arc<Database>>,
1336 Path(id): Path<String>,
1337) -> Result<StatusCode, (StatusCode, Json<ApiError>)> {
1338 let id = Uuid::parse_str(&id)
1339 .map_err(|e| map_err(crate::error::MnemeError::Config(e.to_string())))?;
1340 let key_store = crate::crypto::KeyStore::new(db.get_conn());
1341 key_store.remove(id).map_err(map_err)?;
1342 Ok(StatusCode::NO_CONTENT)
1343}
1344
1345pub async fn keys_status(State(db): State<Arc<Database>>) -> ApiResult<serde_json::Value> {
1347 let key_store = crate::crypto::KeyStore::new(db.get_conn());
1348 let keys = key_store.list().unwrap_or_default();
1349 let keys_count = keys.len() as u32;
1350 let default_key = keys.iter().find(|k| k.is_default).map(|k| k.alias.clone());
1351
1352 Ok(Json(json!({
1353 "keys_count": keys_count,
1354 "encrypted_memories": 0,
1355 "identity_loaded": false,
1356 "default_key": default_key,
1357 })))
1358}
1359
1360#[derive(Debug, Deserialize)]
1363pub struct CloudEnrollRequest {
1364 pub server: String,
1365 pub token: String,
1366 pub project: Option<String>,
1367}
1368
1369#[derive(Debug, Deserialize)]
1370pub struct CloudSyncRequest {
1371 pub project: Option<String>,
1372}
1373
1374#[derive(Debug, Deserialize)]
1375pub struct CloudStatusQuery {
1376 pub project: Option<String>,
1377}
1378
1379pub async fn cloud_enroll(
1380 State(db): State<Arc<Database>>,
1381 Extension(settings): Extension<Arc<Settings>>,
1382 Json(req): Json<CloudEnrollRequest>,
1383) -> ApiResult<serde_json::Value> {
1384 let project = req.project.unwrap_or_else(|| settings.mcp.default_project.clone());
1385 let orch = crate::cloud::CloudOrchestrator::new(db, settings.sync.clone());
1386 let result = orch.enroll(&req.server, &req.token, &project).await
1387 .map_err(map_err)?;
1388 Ok(Json(serde_json::to_value(result).map_err(|e| map_err(crate::error::MnemeError::Serialization(e)))?))
1389}
1390
1391pub async fn cloud_sync(
1392 State(db): State<Arc<Database>>,
1393 Extension(settings): Extension<Arc<Settings>>,
1394 Json(req): Json<CloudSyncRequest>,
1395) -> ApiResult<serde_json::Value> {
1396 let project = req.project.unwrap_or_else(|| settings.mcp.default_project.clone());
1397 let orch = crate::cloud::CloudOrchestrator::new(db, settings.sync.clone());
1398 let result = orch.sync_cloud(&project).await
1399 .map_err(map_err)?;
1400 Ok(Json(serde_json::to_value(result).map_err(|e| map_err(crate::error::MnemeError::Serialization(e)))?))
1401}
1402
1403pub async fn cloud_status(
1404 State(db): State<Arc<Database>>,
1405 Extension(settings): Extension<Arc<Settings>>,
1406 Query(query): Query<CloudStatusQuery>,
1407) -> ApiResult<serde_json::Value> {
1408 let project = query.project.unwrap_or_else(|| settings.mcp.default_project.clone());
1409 let orch = crate::cloud::CloudOrchestrator::new(db, settings.sync.clone());
1410 let status = orch.cloud_status(&project)
1411 .map_err(map_err)?;
1412 Ok(Json(status))
1413}