Skip to main content

mneme/http/
handlers.rs

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/// Respuesta de error estándar de la API.
19#[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
110// --- Health ---
111
112/// GET /health
113pub async fn health() -> Json<serde_json::Value> {
114    Json(json!({"status": "ok", "version": env!("CARGO_PKG_VERSION")}))
115}
116
117/// Sirve la UI web del dashboard en `/`.
118/// No abre nada en el navegador automáticamente — el usuario accede manualmente a localhost:8080.
119pub 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
129/// Sirve un favicon SVG inline (cerebro emoji + fondo oscuro) para `/favicon.ico`.
130/// Evita el 404 que muchos browsers piden automáticamente.
131pub 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// --- Memories ---
140
141/// Query params para listar memorias.
142#[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
156/// GET /api/v1/memories
157pub 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/// Body para crear memoria.
193#[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
226/// POST /api/v1/memories
227pub 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
272/// GET /api/v1/memories/:id
273pub 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/// Body para actualizar memoria.
287#[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
313/// PUT /api/v1/memories/:id
314pub 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
355/// DELETE /api/v1/memories/:id
356pub 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/// POST /api/v1/memories/search
375#[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
440// --- Relations ---
441
442/// GET /api/v1/memories/:id/relations
443pub 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/// Body para crear relación.
451#[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
461/// POST /api/v1/memories/:id/relations
462pub 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
485/// DELETE /api/v1/relations/:relation_id
486pub 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// --- Stats ---
497
498/// GET /api/v1/stats
499#[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
514// --- Projects ---
515
516/// GET /api/v1/projects
517pub 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// --- Sessions ---
523
524/// POST /api/v1/sessions/start
525#[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/// POST /api/v1/sessions/:id/end
548#[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/// GET /api/v1/sessions/active
569#[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// --- Context ---
585
586/// GET /api/v1/context
587#[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// --- Export ---
609
610/// GET /api/v1/export
611#[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
629// --- Import ---
630
631/// POST /api/v1/import
632pub 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// --- Doctor ---
670
671/// GET /api/v1/doctor
672#[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// --- Similar ---
730
731/// POST /api/v1/memories/similar
732#[derive(Debug, Deserialize)]
733pub struct SimilarBody {
734    /// Texto o UUID de la memoria de referencia.
735    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// --- Reindex ---
810
811/// POST /api/v1/embeddings/reindex
812#[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// --- Batch Save ---
858
859#[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// --- Audit ---
928
929#[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// --- Deduplicate ---
954
955#[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// --- Feedback ---
981
982#[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// --- Deprecate ---
1013
1014#[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// --- Graph ---
1042
1043#[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// --- Summarize ---
1059
1060#[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// --- Inject Context ---
1087
1088#[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// --- Forget Project ---
1121
1122#[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// --- Health ---
1150
1151#[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// --- Remind ---
1169
1170#[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// --- Tag Suggest ---
1196
1197#[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// --- Knowledge Gaps ---
1219
1220#[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
1235// --- Sync ---
1236
1237use 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
1275// --- Encryption ---
1276
1277/// POST /api/v1/memories/:id/encrypt
1278pub 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
1288/// POST /api/v1/memories/:id/decrypt
1289pub 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
1299// --- Keys ---
1300
1301/// GET /api/v1/keys
1302pub 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/// Body para agregar clave.
1311#[derive(Debug, Deserialize)]
1312pub struct AddKeyBody {
1313    alias: String,
1314    key: String,
1315    #[serde(default)]
1316    default: bool,
1317}
1318
1319/// POST /api/v1/keys
1320pub 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
1333/// DELETE /api/v1/keys/:id
1334pub 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
1345/// GET /api/v1/keys/status
1346pub 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// --- Cloud Sync Handlers ---
1361
1362#[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}