Skip to main content

mneme/cli/
commands.rs

1use std::path::PathBuf;
2use std::sync::Arc;
3
4use clap::{Parser, Subcommand};
5use uuid::Uuid;
6
7use crate::cli::output;
8use crate::cli::output::{BOLD, GREEN, RESET};
9use crate::config::settings::Settings;
10use crate::store::db::Database;
11use crate::store::memory::{CreateMemoryInput, Scope, SearchQuery};
12use std::str::FromStr;
13
14/// CLI principal de Mneme.
15#[derive(Parser)]
16#[command(
17    name = "mneme",
18    about = "mneme — la memoria persistente para tu agente IA",
19    version,
20    author
21)]
22pub struct Cli {
23    /// Comando a ejecutar.
24    #[command(subcommand)]
25    pub command: Commands,
26}
27
28/// Comandos disponibles.
29#[allow(clippy::large_enum_variant)]
30#[derive(Subcommand)]
31pub enum Commands {
32    /// Guarda una nueva memoria.
33    Save {
34        /// Título de la memoria.
35        title: String,
36        /// Contenido de la memoria.
37        content: String,
38        /// Proyecto asociado.
39        #[arg(long, short = 'p', env = "MNEME_PROJECT")]
40        project: Option<String>,
41        /// Tipo de memoria.
42        #[arg(long, short = 't', default_value = "note")]
43        r#type: String,
44        /// Nivel de importancia.
45        #[arg(long, short = 'i', default_value = "medium")]
46        importance: String,
47        /// Tags separados por coma.
48        #[arg(long, value_delimiter = ',')]
49        tags: Vec<String>,
50        /// Campo "what".
51        #[arg(long)]
52        what: Option<String>,
53        /// Campo "why".
54        #[arg(long)]
55        why: Option<String>,
56        /// Campo "context".
57        #[arg(long)]
58        context: Option<String>,
59        /// Campo "learned".
60        #[arg(long)]
61        learned: Option<String>,
62        /// Alcance de la memoria.
63        #[arg(long)]
64        scope: Option<String>,
65        /// Clave de tópico para upserts.
66        #[arg(long)]
67        topic_key: Option<String>,
68    },
69    /// Busca memorias.
70    Search {
71        /// Texto de búsqueda.
72        query: String,
73        /// Filtrar por proyecto.
74        #[arg(long, short = 'p', env = "MNEME_PROJECT")]
75        project: Option<String>,
76        /// Filtrar por tipo.
77        #[arg(long, short = 't')]
78        r#type: Option<String>,
79        /// Límite de resultados.
80        #[arg(long, short = 'l', default_value = "10")]
81        limit: u32,
82        /// Salida en JSON.
83        #[arg(long)]
84        json: bool,
85    },
86    /// Obtiene una memoria por ID.
87    Get {
88        /// UUID de la memoria.
89        id: String,
90        /// Salida en JSON.
91        #[arg(long)]
92        json: bool,
93    },
94    /// Lista memorias.
95    List {
96        /// Filtrar por proyecto.
97        #[arg(long, short = 'p', env = "MNEME_PROJECT")]
98        project: Option<String>,
99        /// Filtrar por tipo.
100        #[arg(long, short = 't')]
101        r#type: Option<String>,
102        /// Límite de resultados.
103        #[arg(long, short = 'l', default_value = "20")]
104        limit: u32,
105        /// Salida en JSON.
106        #[arg(long)]
107        json: bool,
108    },
109    /// Elimina una memoria.
110    Delete {
111        /// UUID de la memoria.
112        id: String,
113        /// Eliminación física (hard delete).
114        #[arg(long)]
115        hard: bool,
116    },
117    /// Restaura una memoria eliminada.
118    Restore {
119        /// UUID de la memoria.
120        id: String,
121    },
122    /// Crea una relación entre memorias.
123    Relate {
124        /// ID de origen.
125        from_id: String,
126        /// ID de destino.
127        to_id: String,
128        /// Tipo de relación.
129        relation_type: String,
130        /// Confianza de la relación.
131        #[arg(long)]
132        confidence: Option<f32>,
133    },
134    /// Obtiene contexto reciente.
135    Context {
136        /// Proyecto.
137        #[arg(long, short = 'p', env = "MNEME_PROJECT")]
138        project: Option<String>,
139        /// Límite de resultados.
140        #[arg(long, short = 'l', default_value = "10")]
141        limit: u32,
142    },
143    /// Muestra estadísticas.
144    Stats {
145        /// Proyecto.
146        #[arg(long, short = 'p', env = "MNEME_PROJECT")]
147        project: Option<String>,
148    },
149    /// Lista proyectos.
150    Projects,
151    /// Inicia servidor HTTP.
152    Serve {
153        /// Puerto.
154        #[arg(long, short = 'p', default_value = "7438")]
155        port: u16,
156        /// Host.
157        #[arg(long, default_value = "127.0.0.1")]
158        host: String,
159    },
160    /// Inicia servidor MCP.
161    Mcp,
162    /// Inicia TUI interactiva (lista, grafo, entidades, temporal, search).
163    Tui,
164    /// Configura integración con agentes.
165    Setup {
166        /// Agente a configurar.
167        #[command(subcommand)]
168        agent: AgentSetup,
169    },
170    /// Exporta memorias a JSON o Markdown.
171    Export {
172        /// Proyecto.
173        #[arg(long, short = 'p', env = "MNEME_PROJECT")]
174        project: Option<String>,
175        /// Archivo de salida.
176        #[arg(long, short = 'o')]
177        output: Option<PathBuf>,
178        /// Formato: json (default) o md.
179        #[arg(long, short = 'f', default_value = "json")]
180        format: String,
181    },
182    /// Importa memorias desde JSON.
183    Import {
184        /// Archivo a importar.
185        file: PathBuf,
186        /// Proyecto destino.
187        #[arg(long, short = 'p', env = "MNEME_PROJECT")]
188        project: Option<String>,
189    },
190    /// Ejecuta diagnósticos.
191    Doctor {
192        /// Proyecto.
193        #[arg(long, short = 'p', env = "MNEME_PROJECT")]
194        project: Option<String>,
195        /// Salida en JSON.
196        #[arg(long)]
197        json: bool,
198    },
199    /// Reindexa embeddings de un proyecto.
200    Reindex {
201        /// Proyecto.
202        #[arg(long, short = 'p', env = "MNEME_PROJECT")]
203        project: Option<String>,
204        /// Forzar reindexación de todos los embeddings.
205        #[arg(long)]
206        force: bool,
207        /// Salida en JSON.
208        #[arg(long)]
209        json: bool,
210    },
211    /// Ejecuta un benchmark de retrieval quality.
212    Bench {
213        /// Archivo de escenario (TOML/JSON). Si no se da, usa el ejemplo 'rust-decisions'.
214        #[arg(long, short = 's')]
215        scenario: Option<String>,
216        /// Salida en JSON.
217        #[arg(long)]
218        json: bool,
219    },
220    /// Minería de failures — analiza sesiones fallidas y genera memorias correctivas.
221    Learn {
222        /// Proyecto.
223        #[arg(long, short = 'p', env = "MNEME_PROJECT")]
224        project: Option<String>,
225        /// Salida en JSON.
226        #[arg(long)]
227        json: bool,
228    },
229    /// Consolidación: compacta memorias viejas/stale/deprecadas en resúmenes.
230    Consolidate {
231        /// Proyecto.
232        #[arg(long, short = 'p', env = "MNEME_PROJECT")]
233        project: Option<String>,
234        /// Estrategia: auto, age, deprecated, unused.
235        #[arg(long, default_value = "auto")]
236        strategy: String,
237        /// Días para stale (default 30).
238        #[arg(long, default_value_t = 30)]
239        days: u64,
240        /// Dry run (no borra).
241        #[arg(long)]
242        dry_run: bool,
243        /// JSON output.
244        #[arg(long)]
245        json: bool,
246    },
247    /// Memory blocks (Letta-inspired): human, persona, workflow.
248    Block {
249        /// Proyecto.
250        #[arg(long, short = 'p', env = "MNEME_PROJECT")]
251        project: Option<String>,
252        /// Slot: human, persona, workflow (omite para listar).
253        #[arg(long)]
254        slot: Option<String>,
255        /// Título para el slot.
256        #[arg(long)]
257        title: Option<String>,
258        /// Contenido para el slot. Si se omite, muestra slot.
259        #[arg(long)]
260        content: Option<String>,
261    },
262    /// Genera scripts de autocompletado para la shell.
263    Completion {
264        /// Shell objetivo: bash, zsh, fish, powershell.
265        shell: String,
266    },
267    /// Exporta la base de datos completa a un archivo.
268    DbExport {
269        /// Ruta de salida.
270        #[arg(long, short = 'o')]
271        output: Option<String>,
272    },
273    /// Importa una base de datos desde un archivo.
274    DbImport {
275        /// Archivo a importar.
276        file: String,
277    },
278    /// Guarda un lote de memorias desde un archivo JSON.
279    SaveBatch {
280        /// Archivo JSON con el lote de memorias.
281        file: PathBuf,
282        /// Proyecto.
283        #[arg(long, short = 'p', env = "MNEME_PROJECT")]
284        project: Option<String>,
285    },
286    /// Elimina una relación por ID.
287    DeleteRelation {
288        /// ID de la relación.
289        relation_id: String,
290    },
291    /// Audita calidad de memorias.
292    Audit {
293        /// Proyecto.
294        #[arg(long, short = 'p', env = "MNEME_PROJECT")]
295        project: Option<String>,
296        /// Días de umbral para stale.
297        #[arg(long, default_value = "30")]
298        days_threshold: u32,
299        /// Salida en JSON.
300        #[arg(long)]
301        json: bool,
302    },
303    /// Busca duplicados semánticos.
304    Deduplicate {
305        /// Proyecto.
306        #[arg(long, short = 'p', env = "MNEME_PROJECT")]
307        project: Option<String>,
308        /// Umbral de similitud.
309        #[arg(long, default_value = "0.85")]
310        threshold: f64,
311    },
312    /// Registra feedback sobre una memoria.
313    Feedback {
314        /// ID de la memoria.
315        memory_id: String,
316        /// Útil o no.
317        is_useful: bool,
318        /// Razón opcional.
319        #[arg(long)]
320        reason: Option<String>,
321    },
322    /// Depreca una memoria.
323    Deprecate {
324        /// ID de la memoria.
325        memory_id: String,
326        /// Razón de deprecación.
327        reason: String,
328        /// ID de la memoria que la reemplaza.
329        #[arg(long)]
330        supersedes_id: Option<String>,
331    },
332    /// Muestra el grafo de conocimiento.
333    Graph {
334        /// Proyecto.
335        #[arg(long, short = 'p', env = "MNEME_PROJECT")]
336        project: Option<String>,
337        /// Salida en JSON.
338        #[arg(long)]
339        json: bool,
340    },
341    /// Genera resumen ejecutivo.
342    Summarize {
343        /// Proyecto.
344        #[arg(long, short = 'p', env = "MNEME_PROJECT")]
345        project: Option<String>,
346        /// ID de sesión opcional.
347        #[arg(long)]
348        session_id: Option<String>,
349    },
350    /// Inyecta contexto formateado.
351    InjectContext {
352        /// Proyecto.
353        #[arg(long, short = 'p', env = "MNEME_PROJECT")]
354        project: Option<String>,
355        /// Archivo relacionado.
356        #[arg(long)]
357        file: Option<String>,
358        /// Límite de resultados.
359        #[arg(long, short = 'l', default_value = "10")]
360        limit: u32,
361    },
362    /// Elimina todas las memorias de un proyecto.
363    ForgetProject {
364        /// Proyecto.
365        #[arg(long, short = 'p', env = "MNEME_PROJECT")]
366        project: Option<String>,
367        /// Confirmación explícita.
368        #[arg(long)]
369        confirm: bool,
370    },
371    /// Muestra salud del sistema.
372    Health {
373        /// Proyecto.
374        #[arg(long, short = 'p', env = "MNEME_PROJECT")]
375        project: Option<String>,
376        /// Salida en JSON.
377        #[arg(long)]
378        json: bool,
379    },
380    /// Muestra recordatorios importantes.
381    Remind {
382        /// Proyecto.
383        #[arg(long, short = 'p', env = "MNEME_PROJECT")]
384        project: Option<String>,
385        /// Nivel de importancia.
386        #[arg(long, short = 'i', default_value = "high")]
387        importance: String,
388    },
389    /// Sugiere tags.
390    TagSuggest {
391        /// Proyecto.
392        #[arg(long, short = 'p', env = "MNEME_PROJECT")]
393        project: Option<String>,
394        /// Título de referencia.
395        title: String,
396        /// Contenido opcional.
397        #[arg(long)]
398        content: Option<String>,
399    },
400    /// Detecta brechas de conocimiento.
401    KnowledgeGaps {
402        /// Proyecto.
403        #[arg(long, short = 'p', env = "MNEME_PROJECT")]
404        project: Option<String>,
405        /// Salida en JSON.
406        #[arg(long)]
407        json: bool,
408    },
409    /// Comandos de sincronizacion.
410    Sync {
411        #[command(subcommand)]
412        command: SyncCommands,
413    },
414    /// Monitorear directorio y auto-guardar memorias.
415    Watch {
416        /// Proyecto asociado.
417        #[arg(long, short = 'p', env = "MNEME_PROJECT")]
418        project: Option<String>,
419        /// Directorio a monitorear (default: directorio actual).
420        #[arg(long, short = 'd')]
421        dir: Option<PathBuf>,
422        /// Intervalo de polling en segundos.
423        #[arg(long, default_value = "2")]
424        interval: u64,
425        /// Solo archivos con esta extension.
426        #[arg(long, default_value = ".mneme")]
427        ext: String,
428    },
429    /// Gestionar claves de encriptación.
430    Keys {
431        #[command(subcommand)]
432        cmd: KeysCommands,
433    },
434}
435
436/// Comandos de claves de encriptación.
437#[derive(Subcommand, Debug)]
438pub enum KeysCommands {
439    /// Listar claves registradas
440    List,
441    /// Agregar clave pública SSH o age
442    Add {
443        alias: String,
444        key: String,
445        #[arg(long)]
446        default: bool,
447    },
448    /// Eliminar clave registrada por alias o ID
449    Remove { alias_or_id: String },
450    /// Marcar clave como default
451    SetDefault { alias_or_id: String },
452    /// Detectar claves SSH disponibles en el sistema
453    Detect,
454    /// Verificar que la identidad de desencriptación funciona
455    Test,
456}
457
458/// Comandos de sincronizacion.
459#[derive(Subcommand)]
460pub enum SyncCommands {
461    /// Lista peers de sincronizacion.
462    Peers {
463        /// Proyecto.
464        #[arg(long, short = 'p', env = "MNEME_PROJECT")]
465        project: Option<String>,
466    },
467    /// Agrega un peer.
468    AddPeer {
469        /// Nombre del peer.
470        name: String,
471        /// Direccion del peer.
472        address: String,
473        /// Tipo de transporte.
474        #[arg(long, default_value = "http")]
475        transport: String,
476        /// Proyecto.
477        #[arg(long, short = 'p', env = "MNEME_PROJECT")]
478        project: Option<String>,
479        /// Auto-sync.
480        #[arg(long)]
481        auto_sync: bool,
482    },
483    /// Elimina un peer.
484    RemovePeer {
485        /// ID del peer.
486        id: String,
487    },
488    /// Sincroniza ahora con peers auto-sync.
489    Now {
490        /// Proyecto.
491        #[arg(long, short = 'p', env = "MNEME_PROJECT")]
492        project: Option<String>,
493    },
494    /// Exporta proyecto a archivo.
495    Export {
496        /// Proyecto.
497        #[arg(long, short = 'p', env = "MNEME_PROJECT")]
498        project: Option<String>,
499        /// Archivo de salida.
500        #[arg(long, short = 'o')]
501        output: Option<PathBuf>,
502    },
503    /// Importa desde archivo.
504    Import {
505        /// Archivo a importar.
506        file: PathBuf,
507    },
508    /// Muestra log de sincronizacion.
509    Log {
510        /// Proyecto.
511        #[arg(long, short = 'p', env = "MNEME_PROJECT")]
512        project: Option<String>,
513    },
514    /// Estado de sincronizacion.
515    Status {
516        /// Proyecto.
517        #[arg(long, short = 'p', env = "MNEME_PROJECT")]
518        project: Option<String>,
519    },
520}
521
522/// Agentes soportados para setup.
523#[derive(Subcommand)]
524pub enum AgentSetup {
525    /// Configura opencode.
526    Opencode,
527    /// Configura Claude Code.
528    ClaudeCode,
529    /// Configura Continue.
530    Continue,
531    /// Configura Cursor.
532    Cursor,
533    /// Configura Windsurf.
534    Windsurf,
535    /// Configura VS Code Copilot Chat.
536    VscodeCopilot,
537    /// Configura Gemini CLI (Google).
538    GeminiCli,
539    /// Configura Codex CLI (OpenAI).
540    Codex,
541    /// Configura Zed editor.
542    Zed,
543}
544
545/// Ejecuta un comando CLI contra la base de datos.
546pub fn run_command(
547    command: Commands,
548    db: &Database,
549    embeddings: Option<&std::sync::Arc<crate::embeddings::engine::EmbeddingEngine>>,
550) -> crate::error::Result<()> {
551    match command {
552        Commands::Save {
553            title,
554            content,
555            project,
556            r#type,
557            importance,
558            tags,
559            what,
560            why,
561            context,
562            learned,
563            scope,
564            topic_key,
565        } => {
566            let project = project.unwrap_or_else(Settings::infer_project);
567            let memory_type = r#type.parse()?;
568            let importance = importance.parse()?;
569            let scope = scope
570                .as_deref()
571                .map(Scope::from_str)
572                .transpose()?
573                .unwrap_or(Scope::Project);
574
575            let input = CreateMemoryInput {
576                project,
577                scope: Some(scope),
578                title,
579                content,
580                what,
581                why,
582                context,
583                learned,
584                memory_type,
585                importance,
586                tags,
587                topic_key,
588                capture_prompt: None,
589                encrypt: false,
590        valid_from: None,
591        valid_until: None,
592        provenance: None,
593            };
594
595            let engine = embeddings.map(std::sync::Arc::clone);
596            let embedding_store = db.embeddings();
597            let memory = db.memories().save(input, engine, Some(embedding_store))?;
598            output::print_success(&format!("Memory saved: {}", memory.id));
599            output::print_memory(&memory);
600        }
601        Commands::Search {
602            query,
603            project,
604            r#type,
605            limit,
606            json,
607        } => {
608            let project = project.unwrap_or_else(Settings::infer_project);
609            let memory_type = r#type.as_deref().map(str::parse).transpose()?;
610
611            let search_query = SearchQuery {
612                text: query,
613                project: Some(project),
614                scope: None,
615                memory_type,
616                importance: None,
617                tags: Vec::new(),
618                limit,
619                include_snippet: true,
620                all_projects: false,
621            };
622
623            let weights = crate::store::search::SearchWeights::default();
624            let results = db.memories().search(&search_query, &weights, None)?;
625
626            if json {
627                println!("{}", serde_json::to_string_pretty(&results)?);
628            } else {
629                output::print_search_results(&results);
630            }
631        }
632        Commands::Get { id, json } => {
633            let id = Uuid::parse_str(&id)
634                .map_err(|e| crate::error::MnemeError::Config(e.to_string()))?;
635            match db.memories().get(id)? {
636                Some(memory) => {
637                    if json {
638                        println!("{}", serde_json::to_string_pretty(&memory)?);
639                    } else {
640                        output::print_memory(&memory);
641                    }
642                }
643                None => {
644                    output::print_error(&format!("Memory not found: {}", id));
645                    std::process::exit(1);
646                }
647            }
648        }
649        Commands::List {
650            project,
651            r#type,
652            limit,
653            json,
654        } => {
655            let project = project.unwrap_or_else(Settings::infer_project);
656            let memory_type = r#type.as_deref().map(str::parse).transpose()?;
657
658            let memories =
659                db.memories()
660                    .list(&project, memory_type.as_ref(), None, None, limit, 0)?;
661
662            if json {
663                println!("{}", serde_json::to_string_pretty(&memories)?);
664            } else {
665                output::print_memory_list(&memories);
666            }
667        }
668        Commands::Delete { id, hard } => {
669            let id = Uuid::parse_str(&id)
670                .map_err(|e| crate::error::MnemeError::Config(e.to_string()))?;
671            db.memories().delete(id, hard)?;
672            if hard {
673                output::print_success(&format!("Memory hard-deleted: {}", id));
674            } else {
675                output::print_success(&format!("Memory soft-deleted: {}", id));
676            }
677        }
678        Commands::Restore { id } => {
679            let id = Uuid::parse_str(&id)
680                .map_err(|e| crate::error::MnemeError::Config(e.to_string()))?;
681            let memory = db.memories().restore(id)?;
682            output::print_success(&format!("Memory restored: {}", memory.id));
683            output::print_memory(&memory);
684        }
685        Commands::Relate {
686            from_id,
687            to_id,
688            relation_type,
689            confidence,
690        } => {
691            let from_id = Uuid::parse_str(&from_id)
692                .map_err(|e| crate::error::MnemeError::Config(e.to_string()))?;
693            let to_id = Uuid::parse_str(&to_id)
694                .map_err(|e| crate::error::MnemeError::Config(e.to_string()))?;
695            let relation_type = relation_type.parse()?;
696
697            let input = crate::store::memory::CreateRelationInput {
698                source_id: from_id,
699                target_id: to_id,
700                relation_type,
701                confidence: Some(confidence.unwrap_or(1.0)),
702                reason: None,
703            };
704
705            let store = db.memories();
706            store.create_relation(input)?;
707            output::print_success(&format!("Relation created: {} -> {}", from_id, to_id));
708        }
709        Commands::Context { project, limit } => {
710            let project = project.unwrap_or_else(Settings::infer_project);
711            let memories = db.memories().context(&project, None, limit)?;
712            output::print_memory_list(&memories);
713        }
714        Commands::Stats { project } => {
715            let project = project.unwrap_or_else(Settings::infer_project);
716            let stats = db.memories().stats(&project)?;
717            output::print_stats(&stats);
718        }
719        Commands::Projects => {
720            let projects = db.memories().list_projects()?;
721            output::print_projects(&projects);
722        }
723        Commands::Serve { port, host } => {
724            output::print_warning("Use 'mneme serve' via the async runtime, not sync.");
725            tracing::info!("Starting HTTP server on {}:{}", host, port);
726        }
727        Commands::Mcp => {
728            output::print_warning("Use 'mneme mcp' via the async runtime, not sync.");
729        }
730        Commands::Tui => {
731            println!("TUI coming in v0.3");
732        }
733        Commands::Setup { agent } => match agent {
734            AgentSetup::Opencode => setup_opencode()?,
735            AgentSetup::ClaudeCode => setup_claude_code()?,
736            AgentSetup::Continue => setup_continue()?,
737            AgentSetup::Cursor => setup_cursor()?,
738            AgentSetup::Windsurf => setup_windsurf()?,
739            AgentSetup::VscodeCopilot => setup_vscode_copilot()?,
740            AgentSetup::GeminiCli => setup_gemini_cli()?,
741            AgentSetup::Codex => setup_codex()?,
742            AgentSetup::Zed => setup_zed()?,
743        },
744        Commands::Export {
745            project,
746            output,
747            format,
748        } => {
749            let project = project.unwrap_or_else(Settings::infer_project);
750            let memories = db.memories().list(&project, None, None, None, 10000, 0)?;
751
752            let (content, default_ext) = match format.as_str() {
753                "md" | "markdown" => (crate::export::export_to_markdown(&memories, &project), "md"),
754                _ => (serde_json::to_string_pretty(&memories)?, "json"),
755            };
756
757            let output_path = output.unwrap_or_else(|| {
758                let mut path = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
759                let timestamp = chrono::Utc::now().format("%Y%m%d");
760                path.push(format!("{}_{}_export.{}", project, timestamp, default_ext));
761                path
762            });
763
764            std::fs::write(&output_path, &content)?;
765            output::print_success(&format!(
766                "Exported {} memories to {}",
767                memories.len(),
768                output_path.display()
769            ));
770        }
771        Commands::Import { file, project } => {
772            let project = project.unwrap_or_else(Settings::infer_project);
773            let content = std::fs::read_to_string(&file)?;
774            let store = db.memories();
775            let mut count = 0u32;
776            let engine = embeddings.map(std::sync::Arc::clone);
777            let embedding_store = db.embeddings();
778
779            let ext = file.extension().and_then(|e| e.to_str()).unwrap_or("json");
780            if ext == "md" {
781                let imported = crate::export::import_from_markdown(&content)?;
782                for mem in imported {
783                    let input = CreateMemoryInput {
784                        project: project.clone(),
785                        scope: Some(mem.scope),
786                        title: mem.title,
787                        content: mem.content,
788                        what: mem.what,
789                        why: mem.why,
790                        context: mem.context,
791                        learned: mem.learned,
792                        memory_type: mem.memory_type,
793                        importance: mem.importance,
794                        tags: mem.tags,
795                        topic_key: None,
796                        capture_prompt: None,
797                        encrypt: false,
798        valid_from: None,
799        valid_until: None,
800        provenance: None,
801                    };
802                    store.save(input, engine.clone(), Some(embedding_store.clone()))?;
803                    count += 1;
804                }
805            } else {
806                let memories: Vec<crate::store::memory::Memory> = serde_json::from_str(&content)?;
807                for mem in memories {
808                    let input = CreateMemoryInput {
809                        project: project.clone(),
810                        scope: Some(mem.scope),
811                        title: mem.title,
812                        content: mem.content,
813                        what: mem.what,
814                        why: mem.why,
815                        context: mem.context,
816                        learned: mem.learned,
817                        memory_type: mem.memory_type,
818                        importance: mem.importance,
819                        tags: mem.tags,
820                        topic_key: mem.topic_key,
821                        capture_prompt: None,
822                        encrypt: false,
823        valid_from: None,
824        valid_until: None,
825        provenance: None,
826                    };
827                    store.save(input, engine.clone(), Some(embedding_store.clone()))?;
828                    count += 1;
829                }
830            }
831            output::print_success(&format!(
832                "Imported {} memories from {}",
833                count,
834                file.display()
835            ));
836        }
837        Commands::Doctor { project, json } => {
838            let project = project.unwrap_or_else(Settings::infer_project);
839            let report = run_doctor(db, &project)?;
840            if json {
841                println!("{}", serde_json::to_string_pretty(&report)?);
842            } else {
843                print_doctor_report(&report);
844            }
845        }
846        Commands::Reindex {
847            project,
848            force: _force,
849            json,
850        } => {
851            let project = project.unwrap_or_else(Settings::infer_project);
852            match embeddings {
853                Some(engine) => {
854                    let embedding_store = db.embeddings();
855                    let rt = tokio::runtime::Runtime::new()?;
856                    let stats = rt.block_on(async {
857                        db.memories()
858                            .reindex_embeddings(&project, engine, &embedding_store)
859                            .await
860                    })?;
861                    if json {
862                        println!("{}", serde_json::to_string_pretty(&stats)?);
863                    } else {
864                        output::print_success(&format!(
865                            "Reindexed {}/{} embeddings in {}ms",
866                            stats.indexed, stats.total, stats.duration_ms
867                        ));
868                    }
869                }
870                None => {
871                    output::print_error("Embeddings engine not available");
872                    std::process::exit(1);
873                }
874            }
875        }
876        Commands::Bench { scenario, json } => {
877            let bench_db = std::sync::Arc::new(db.clone());
878            let runner = crate::bench::BenchmarkRunner::new(bench_db);
879            let scenario_obj = match scenario {
880                Some(path) => runner.load_scenario(std::path::Path::new(&path))?,
881                None => crate::bench::example_rust_scenario(),
882            };
883            let result = runner.run(&scenario_obj)?;
884            if json {
885                println!("{}", serde_json::to_string_pretty(&result)?);
886            } else {
887                println!("{}", crate::bench::format_report(&result));
888            }
889        }
890        Commands::Learn { project, json } => {
891            let learn_db = std::sync::Arc::new(db.clone());
892            let miner = crate::learn::FailureMiner::new(learn_db);
893            let project = project.unwrap_or_else(Settings::infer_project);
894            let report = miner.mine(&project)?;
895            if json {
896                println!("{}", serde_json::to_string_pretty(&report)?);
897            } else {
898                println!("{}", crate::learn::format_failure_report(&report));
899            }
900        }
901        Commands::Consolidate { project, strategy, days, dry_run, json } => {
902            let eng_db = std::sync::Arc::new(db.clone());
903            let eng = crate::consolidate::ConsolidationEngine::new(eng_db);
904            let project = project.unwrap_or_else(Settings::infer_project);
905            let strat = match strategy.as_str() {
906                "age" => crate::consolidate::ConsolidationStrategy::Age,
907                "deprecated" => crate::consolidate::ConsolidationStrategy::Deprecated,
908                "unused" => crate::consolidate::ConsolidationStrategy::Unused,
909                _ => crate::consolidate::ConsolidationStrategy::Auto,
910            };
911            let result = eng.consolidate(&project, strat, days, dry_run)?;
912            if json {
913                println!("{}", serde_json::to_string_pretty(&result)?);
914            } else {
915                println!("{}", crate::consolidate::format_consolidation_result(&result));
916            }
917        }
918        Commands::Block { project, slot, title, content } => {
919            let eng_db = std::sync::Arc::new(db.clone());
920            let eng = crate::consolidate::ConsolidationEngine::new(eng_db);
921            let project = project.unwrap_or_else(Settings::infer_project);
922            match (slot, content) {
923                (Some(s), Some(c)) => {
924                    let t = title.as_deref().unwrap_or(&s);
925                    let block = eng.set_block(&project, &s, t, &c)?;
926                    println!("✅ Block '{}' saved: {}", s, block.title);
927                }
928                (Some(s), None) => {
929                    match eng.get_block(&project, &s)? {
930                        Some(block) => {
931                            println!("# Block: {} ({})\n\n", block.slot, block.title);
932                            println!("{}", block.content);
933                        }
934                        None => println!("No block '{}' for project '{}'.", s, project),
935                    }
936                }
937                (None, _) => {
938                    let blocks = eng.list_blocks(&project)?;
939                    if blocks.is_empty() {
940                        println!("No blocks for project '{}'.", project);
941                    } else {
942                        println!("Blocks for '{}':", project);
943                        for b in &blocks {
944                            println!("  [{}] {} — {} chars", b.slot, b.title, b.content.len());
945                        }
946                    }
947                }
948            }
949        }
950        Commands::Completion { shell } => {
951            use clap::CommandFactory;
952            let cmd = Cli::command();
953            let shell_enum = match shell.as_str() {
954                "bash" => clap_complete::Shell::Bash,
955                "zsh" => clap_complete::Shell::Zsh,
956                "fish" => clap_complete::Shell::Fish,
957                "powershell" => clap_complete::Shell::PowerShell,
958                _ => {
959                    eprintln!("Unsupported shell: {}. Use: bash, zsh, fish, powershell", shell);
960                    std::process::exit(1);
961                }
962            };
963            clap_complete::generate(shell_enum, &mut cmd.clone(), "mneme", &mut std::io::stdout());
964        }
965        Commands::DbExport { output } => {
966            let output_path = output.unwrap_or_else(|| {
967                let date = chrono::Utc::now().format("%Y%m%d_%H%M%S");
968                format!("mneme-backup-{}.db", date)
969            });
970            let db_path = &crate::config::settings::Settings::load()?.database.path;
971            std::fs::copy(db_path, &output_path)
972                .map_err(|e| crate::error::MnemeError::Io(e))?;
973            println!("✅ Exported database to {}", output_path);
974        }
975        Commands::DbImport { file } => {
976            let db_path = &crate::config::settings::Settings::load()?.database.path;
977            let backup_path = format!("{}.pre-import", db_path.display());
978            std::fs::copy(db_path, &backup_path)
979                .map_err(|e| crate::error::MnemeError::Io(e))?;
980            std::fs::copy(&file, db_path)
981                .map_err(|e| crate::error::MnemeError::Io(e))?;
982            println!("✅ Imported database from {}", file);
983            println!("   Backup of old DB at {}", backup_path);
984            println!("   Restart mneme to use the new database.");
985        }
986        Commands::SaveBatch { file, project } => {
987            let project = project.unwrap_or_else(Settings::infer_project);
988            let content = std::fs::read_to_string(&file)?;
989            let memories: Vec<crate::store::memory::Memory> = serde_json::from_str(&content)?;
990            let store = db.memories();
991            let mut inputs = Vec::new();
992            for mem in memories {
993                inputs.push(CreateMemoryInput {
994                    project: project.clone(),
995                    scope: Some(mem.scope),
996                    title: mem.title,
997                    content: mem.content,
998                    what: mem.what,
999                    why: mem.why,
1000                    context: mem.context,
1001                    learned: mem.learned,
1002                    memory_type: mem.memory_type,
1003                    importance: mem.importance,
1004                    tags: mem.tags,
1005                    topic_key: mem.topic_key,
1006                    capture_prompt: None,
1007                    encrypt: false,
1008        valid_from: None,
1009        valid_until: None,
1010        provenance: None,
1011                });
1012            }
1013            let engine = embeddings.map(std::sync::Arc::clone);
1014            let embedding_store = db.embeddings();
1015            let (saved, duplicates) = store.save_batch(inputs, engine, Some(embedding_store))?;
1016            output::print_success(&format!(
1017                "Batch saved: {} new, {} duplicates",
1018                saved.len(),
1019                duplicates.len()
1020            ));
1021        }
1022        Commands::DeleteRelation { relation_id } => {
1023            let id = Uuid::parse_str(&relation_id)
1024                .map_err(|e| crate::error::MnemeError::Config(e.to_string()))?;
1025            db.memories().delete_relation(id)?;
1026            output::print_success(&format!("Relation deleted: {}", relation_id));
1027        }
1028        Commands::Audit {
1029            project,
1030            days_threshold,
1031            json,
1032        } => {
1033            let project = project.unwrap_or_else(Settings::infer_project);
1034            let report = db.memories().audit(&project, days_threshold)?;
1035            if json {
1036                println!("{}", serde_json::to_string_pretty(&report)?);
1037            } else {
1038                output::print_audit(&report);
1039            }
1040        }
1041        Commands::Deduplicate { project, threshold } => {
1042            let project = project.unwrap_or_else(Settings::infer_project);
1043            let embedding_store = db.embeddings();
1044            let groups =
1045                db.memories()
1046                    .find_duplicates_semantic(&project, threshold, &embedding_store)?;
1047            output::print_duplicate_groups(&groups);
1048        }
1049        Commands::Feedback {
1050            memory_id,
1051            is_useful,
1052            reason,
1053        } => {
1054            let id = Uuid::parse_str(&memory_id)
1055                .map_err(|e| crate::error::MnemeError::Config(e.to_string()))?;
1056            let feedback_id = db
1057                .memories()
1058                .add_feedback(id, is_useful, reason.as_deref())?;
1059            output::print_success(&format!("Feedback recorded: {}", feedback_id));
1060        }
1061        Commands::Deprecate {
1062            memory_id,
1063            reason,
1064            supersedes_id,
1065        } => {
1066            let id = Uuid::parse_str(&memory_id)
1067                .map_err(|e| crate::error::MnemeError::Config(e.to_string()))?;
1068            let supersedes = supersedes_id
1069                .as_deref()
1070                .map(Uuid::parse_str)
1071                .transpose()
1072                .map_err(|e| crate::error::MnemeError::Config(e.to_string()))?;
1073            let memory = db.memories().deprecate(id, &reason, supersedes)?;
1074            output::print_success(&format!("Memory deprecated: {}", memory.id));
1075        }
1076        Commands::Graph { project, json } => {
1077            let project = project.unwrap_or_else(Settings::infer_project);
1078            let graph = db.memories().get_graph(&project)?;
1079            if json {
1080                println!("{}", serde_json::to_string_pretty(&graph)?);
1081            } else {
1082                output::print_graph(&graph);
1083            }
1084        }
1085        Commands::Summarize {
1086            project,
1087            session_id,
1088        } => {
1089            let project = project.unwrap_or_else(Settings::infer_project);
1090            let sid = session_id
1091                .as_deref()
1092                .map(Uuid::parse_str)
1093                .transpose()
1094                .map_err(|e| crate::error::MnemeError::Config(e.to_string()))?;
1095            let summary = db.memories().summarize(&project, sid)?;
1096            println!("{}", summary.summary);
1097        }
1098        Commands::InjectContext {
1099            project,
1100            file,
1101            limit,
1102        } => {
1103            let project = project.unwrap_or_else(Settings::infer_project);
1104            let context = db
1105                .memories()
1106                .inject_context(&project, file.as_deref(), limit)?;
1107            println!("{}", context);
1108        }
1109        Commands::ForgetProject { project, confirm } => {
1110            if !confirm {
1111                output::print_error("Use --confirm to permanently delete all project memories");
1112                std::process::exit(1);
1113            }
1114            let project = project.unwrap_or_else(Settings::infer_project);
1115            let deleted = db.memories().forget_project(&project)?;
1116            output::print_success(&format!(
1117                "Deleted {} memories for project '{}'",
1118                deleted, project
1119            ));
1120        }
1121        Commands::Health { project, json } => {
1122            let report = db.memories().health(project.as_deref())?;
1123            if json {
1124                println!("{}", serde_json::to_string_pretty(&report)?);
1125            } else {
1126                output::print_health(&report);
1127            }
1128        }
1129        Commands::Remind {
1130            project,
1131            importance,
1132        } => {
1133            let project = project.unwrap_or_else(Settings::infer_project);
1134            let imp = importance.parse()?;
1135            let memories = db.memories().remind(&project, &imp)?;
1136            output::print_remind(&memories);
1137        }
1138        Commands::TagSuggest {
1139            project,
1140            title,
1141            content,
1142        } => {
1143            let project = project.unwrap_or_else(Settings::infer_project);
1144            let tags = db
1145                .memories()
1146                .suggest_tags(&project, &title, content.as_deref())?;
1147            println!("Suggested tags: {}", tags.join(", "));
1148        }
1149        Commands::KnowledgeGaps { project, json } => {
1150            let project = project.unwrap_or_else(Settings::infer_project);
1151            let report = db.memories().knowledge_gaps(&project)?;
1152            if json {
1153                println!("{}", serde_json::to_string_pretty(&report)?);
1154            } else {
1155                output::print_knowledge_gaps(&report);
1156            }
1157        }
1158        Commands::Sync { command } => {
1159            use crate::sync::peer::{Peer, TransportType};
1160            use std::str::FromStr;
1161
1162            match command {
1163                SyncCommands::Peers { project } => {
1164                    let project = project.unwrap_or_else(Settings::infer_project);
1165                    let peers = db.peers().list(&project)?;
1166                    output::print_peer_list(&peers);
1167                }
1168                SyncCommands::AddPeer {
1169                    name,
1170                    address,
1171                    transport,
1172                    project,
1173                    auto_sync,
1174                } => {
1175                    let project = project.unwrap_or_else(Settings::infer_project);
1176                    let transport = TransportType::from_str(&transport)?;
1177                    let peer = Peer {
1178                        id: Uuid::new_v4(),
1179                        name,
1180                        transport,
1181                        address,
1182                        project,
1183                        last_sync: None,
1184                        last_status: None,
1185                        auto_sync,
1186                        created_at: chrono::Utc::now(),
1187                    };
1188                    db.peers().add(&peer)?;
1189                    output::print_success(&format!("Peer added: {} ({})", peer.id, peer.name));
1190                }
1191                SyncCommands::RemovePeer { id } => {
1192                    let id = Uuid::parse_str(&id)
1193                        .map_err(|e| crate::error::MnemeError::Config(e.to_string()))?;
1194                    db.peers().remove(id)?;
1195                    output::print_success(&format!("Peer removed: {}", id));
1196                }
1197                SyncCommands::Now { project } => {
1198                    let project = project.unwrap_or_else(Settings::infer_project);
1199                    let settings = Settings::load()?;
1200                    let engine =
1201                        crate::sync::engine::SyncEngine::new(Arc::new(db.clone()), settings.sync)?;
1202                    let rt = tokio::runtime::Runtime::new()?;
1203                    let results = rt.block_on(async { engine.sync_auto(&project).await })?;
1204                    output::print_sync_result(&results);
1205                }
1206                SyncCommands::Export { project, output } => {
1207                    let project = project.unwrap_or_else(Settings::infer_project);
1208                    let settings = Settings::load()?;
1209                    let engine =
1210                        crate::sync::engine::SyncEngine::new(Arc::new(db.clone()), settings.sync)?;
1211                    let stats = engine.export_project(&project, output)?;
1212                    output::print_sync_status(&stats);
1213                }
1214                SyncCommands::Import { file: _file } => {
1215                    output::print_warning("Sync import not yet implemented in CLI");
1216                }
1217                SyncCommands::Log { project } => {
1218                    let _project = project.unwrap_or_else(Settings::infer_project);
1219                    output::print_warning("Sync log not yet implemented in CLI");
1220                }
1221                SyncCommands::Status { project } => {
1222                    let project = project.unwrap_or_else(Settings::infer_project);
1223                    let peers = db.peers().list(&project)?;
1224                    output::print_peer_list(&peers);
1225                }
1226            }
1227        }
1228        Commands::Keys { cmd } => {
1229            let conn = db.get_conn();
1230            let key_store = crate::crypto::KeyStore::new(conn);
1231            match cmd {
1232                KeysCommands::List => {
1233                    let keys = key_store.list()?;
1234                    if keys.is_empty() {
1235                        println!("No hay claves registradas.");
1236                    } else {
1237                        println!("{:<20} {:<15} {:<8} AGREGADA", "ALIAS", "TIPO", "DEFAULT");
1238                        println!("{}", "─".repeat(60));
1239                        for k in &keys {
1240                            println!(
1241                                "{:<20} {:<15} {:<8} {}",
1242                                k.alias,
1243                                k.key_type,
1244                                if k.is_default { "✓" } else { "✗" },
1245                                k.added_at.format("%Y-%m-%d")
1246                            );
1247                        }
1248                    }
1249                }
1250                KeysCommands::Add {
1251                    alias,
1252                    key,
1253                    default,
1254                } => {
1255                    let recipient = crate::crypto::RecipientKey::from_string(&key)?;
1256                    let registered = key_store.add(&alias, &recipient)?;
1257                    if default {
1258                        key_store.set_default(registered.id)?;
1259                    }
1260                    println!("✓ Clave '{}' registrada ({})", alias, registered.key_type);
1261                }
1262                KeysCommands::Remove { alias_or_id } => {
1263                    let keys = key_store.list()?;
1264                    let target = keys
1265                        .iter()
1266                        .find(|k| k.alias == *alias_or_id || k.id.to_string() == *alias_or_id)
1267                        .ok_or_else(|| crate::MnemeError::KeyNotFound(alias_or_id.clone()))?;
1268                    key_store.remove(target.id)?;
1269                    println!("✓ Clave '{}' eliminada", alias_or_id);
1270                }
1271                KeysCommands::SetDefault { alias_or_id } => {
1272                    let keys = key_store.list()?;
1273                    let target = keys
1274                        .iter()
1275                        .find(|k| k.alias == *alias_or_id || k.id.to_string() == *alias_or_id)
1276                        .ok_or_else(|| crate::MnemeError::KeyNotFound(alias_or_id.clone()))?;
1277                    key_store.set_default(target.id)?;
1278                    println!("✓ '{}' marcada como default", alias_or_id);
1279                }
1280                KeysCommands::Detect => {
1281                    println!("Claves SSH detectadas en el sistema:");
1282                    if let Some(mut home) = dirs::home_dir() {
1283                        home.push(".ssh");
1284                        for name in &["id_ed25519", "id_rsa", "id_ecdsa"] {
1285                            let pub_key = home.join(format!("{}.pub", name));
1286                            let priv_key = home.join(name);
1287                            if priv_key.exists() {
1288                                let available = if pub_key.exists() {
1289                                    "✓ disponible"
1290                                } else {
1291                                    "pub key no encontrada"
1292                                };
1293                                println!("  ~/.ssh/{}    {}", name, available);
1294                            }
1295                        }
1296                    }
1297                }
1298                KeysCommands::Test => match crate::crypto::IdentityKey::detect() {
1299                    Ok(id) => println!("✓ Identidad disponible: {}", id.path().display()),
1300                    Err(e) => println!("✗ Identidad no disponible: {}", e),
1301                },
1302            }
1303        }
1304        Commands::Watch { .. } => {
1305            output::print_warning("Use 'mneme watch' via the async runtime, not sync.");
1306        }
1307    }
1308
1309    Ok(())
1310}
1311
1312fn setup_opencode() -> crate::error::Result<()> {
1313    let config_dir = dirs::config_dir().unwrap_or_else(|| PathBuf::from("."));
1314    let opencode_dir = config_dir.join("opencode");
1315    std::fs::create_dir_all(&opencode_dir)?;
1316
1317    let config_path = opencode_dir.join("config.json");
1318    let mut config: serde_json::Value = if config_path.exists() {
1319        let content = std::fs::read_to_string(&config_path)?;
1320        serde_json::from_str(&content).unwrap_or_else(|_| serde_json::json!({}))
1321    } else {
1322        serde_json::json!({})
1323    };
1324
1325    let mcp_config = serde_json::json!({
1326        "mneme": {
1327            "command": "mneme",
1328            "args": ["mcp"],
1329            "env": {}
1330        }
1331    });
1332
1333    config["mcp"] = mcp_config;
1334
1335    let content = serde_json::to_string_pretty(&config)?;
1336    std::fs::write(&config_path, content)?;
1337
1338    output::print_success(&format!(
1339        "opencode config written to {}",
1340        config_path.display()
1341    ));
1342    Ok(())
1343}
1344
1345fn setup_claude_code() -> crate::error::Result<()> {
1346    let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
1347    let claude_dir = home.join(".claude");
1348    std::fs::create_dir_all(&claude_dir)?;
1349
1350    let config_path = claude_dir.join("settings.json");
1351    let mut config: serde_json::Value = if config_path.exists() {
1352        let content = std::fs::read_to_string(&config_path)?;
1353        serde_json::from_str(&content).unwrap_or_else(|_| serde_json::json!({}))
1354    } else {
1355        serde_json::json!({})
1356    };
1357
1358    config["mcpServers"] = serde_json::json!({
1359        "mneme": {
1360            "command": "mneme",
1361            "args": ["mcp"]
1362        }
1363    });
1364
1365    let content = serde_json::to_string_pretty(&config)?;
1366    std::fs::write(&config_path, content)?;
1367
1368    output::print_success(&format!(
1369        "Claude Code config written to {}",
1370        config_path.display()
1371    ));
1372    Ok(())
1373}
1374
1375fn setup_continue() -> crate::error::Result<()> {
1376    let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
1377    let continue_dir = home.join(".continue");
1378    std::fs::create_dir_all(&continue_dir)?;
1379
1380    let config_path = continue_dir.join("config.json");
1381    let mut config: serde_json::Value = if config_path.exists() {
1382        let content = std::fs::read_to_string(&config_path)?;
1383        serde_json::from_str(&content).unwrap_or_else(|_| serde_json::json!({}))
1384    } else {
1385        serde_json::json!({})
1386    };
1387
1388    let servers = config
1389        .get_mut("models")
1390        .and_then(|m| m.as_array_mut())
1391        .cloned()
1392        .unwrap_or_default();
1393
1394    let mut new_servers = servers;
1395    new_servers.push(serde_json::json!({
1396        "title": "Mneme MCP",
1397        "provider": "mcp",
1398        "model": "mneme",
1399        "apiBase": "",
1400        "server": {
1401            "command": "mneme",
1402            "args": ["mcp"]
1403        }
1404    }));
1405
1406    config["models"] = serde_json::json!(new_servers);
1407
1408    let content = serde_json::to_string_pretty(&config)?;
1409    std::fs::write(&config_path, content)?;
1410
1411    output::print_success(&format!(
1412        "Continue config written to {}",
1413        config_path.display()
1414    ));
1415    Ok(())
1416}
1417
1418// ─── Multi-agent setup helpers ──────────────────────────────────────────────
1419
1420fn home_dir() -> std::path::PathBuf {
1421    dirs::home_dir().unwrap_or_else(|| std::path::PathBuf::from("."))
1422}
1423
1424fn xdg_config_home() -> std::path::PathBuf {
1425    std::env::var("XDG_CONFIG_HOME")
1426        .map(std::path::PathBuf::from)
1427        .unwrap_or_else(|_| home_dir().join(".config"))
1428}
1429
1430/// Escribe un config MCP estándar con `mcpServers` → `mneme`.
1431/// Usado por: cursor, windsurf, gemini-cli, codex.
1432fn write_standard_mcp_config(config_path: std::path::PathBuf, label: &str) -> crate::error::Result<()> {
1433    if let Some(parent) = config_path.parent() {
1434        std::fs::create_dir_all(parent)?;
1435    }
1436
1437    let mut config: serde_json::Value = if config_path.exists() {
1438        let content = std::fs::read_to_string(&config_path)?;
1439        serde_json::from_str(&content).unwrap_or(serde_json::json!({}))
1440    } else {
1441        serde_json::json!({})
1442    };
1443
1444    if !config.is_object() {
1445        config = serde_json::json!({});
1446    }
1447    if config.get("mcpServers").map_or(true, |v| !v.is_object()) {
1448        config["mcpServers"] = serde_json::json!({});
1449    }
1450
1451    config["mcpServers"]["mneme"] = serde_json::json!({
1452        "command": "mneme",
1453        "args": ["mcp"]
1454    });
1455
1456    std::fs::write(&config_path, serde_json::to_string_pretty(&config)?)?;
1457    output::print_success(&format!("{label} config written to {}", config_path.display()));
1458    Ok(())
1459}
1460
1461/// Cursor — ~/.cursor/mcp.json
1462fn setup_cursor() -> crate::error::Result<()> {
1463    let config_dir = home_dir().join(".cursor");
1464    let config_path = config_dir.join("mcp.json");
1465    write_standard_mcp_config(config_path, "Cursor")
1466}
1467
1468/// Windsurf — ~/.codeium/windsurf/mcp_config.json
1469fn setup_windsurf() -> crate::error::Result<()> {
1470    let config_dir = home_dir().join(".codeium").join("windsurf");
1471    let config_path = config_dir.join("mcp_config.json");
1472    write_standard_mcp_config(config_path, "Windsurf")
1473}
1474
1475/// Gemini CLI — ~/.config/gemini-cli/mcp.json
1476fn setup_gemini_cli() -> crate::error::Result<()> {
1477    let config_dir = xdg_config_home().join("gemini-cli");
1478    let config_path = config_dir.join("mcp.json");
1479    write_standard_mcp_config(config_path, "Gemini CLI")
1480}
1481
1482/// Codex CLI (OpenAI) — ~/.codex/settings.json
1483fn setup_codex() -> crate::error::Result<()> {
1484    let config_dir = home_dir().join(".codex");
1485    let config_path = config_dir.join("settings.json");
1486    write_standard_mcp_config(config_path, "Codex CLI")
1487}
1488
1489/// VS Code Copilot Chat — varía por OS
1490fn setup_vscode_copilot() -> crate::error::Result<()> {
1491    // VS Code global storage path per platform
1492    #[cfg(target_os = "linux")]
1493    let base = xdg_config_home().join("Code").join("User");
1494    #[cfg(target_os = "macos")]
1495    let base = home_dir().join("Library").join("Application Support").join("Code").join("User");
1496    #[cfg(target_os = "windows")]
1497    let base = {
1498        let appdata = std::env::var("APPDATA").unwrap_or_else(|_| ".".to_string());
1499        std::path::PathBuf::from(appdata).join("Code").join("User")
1500    };
1501    #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
1502    let base = xdg_config_home().join("Code").join("User");
1503
1504    let config_dir = base.join("globalStorage").join("github.copilot-chat");
1505    std::fs::create_dir_all(&config_dir)?;
1506    let config_path = config_dir.join("mcp_config.json");
1507
1508    let mut config: serde_json::Value = if config_path.exists() {
1509        serde_json::from_str(&std::fs::read_to_string(&config_path)?)
1510            .unwrap_or(serde_json::json!({}))
1511    } else {
1512        serde_json::json!({})
1513    };
1514
1515    if !config.is_object() {
1516        config = serde_json::json!({});
1517    }
1518    if config.get("servers").map_or(true, |v| !v.is_object()) {
1519        config["servers"] = serde_json::json!({});
1520    }
1521
1522    config["servers"]["mneme"] = serde_json::json!({
1523        "type": "stdio",
1524        "command": "mneme",
1525        "args": ["mcp"]
1526    });
1527
1528    std::fs::write(&config_path, serde_json::to_string_pretty(&config)?)?;
1529    output::print_success(&format!(
1530        "VS Code Copilot Chat config written to {}",
1531        config_path.display()
1532    ));
1533    Ok(())
1534}
1535
1536/// Zed — ~/.config/zed/settings.json (mergea mcp_servers dentro del settings existente)
1537fn setup_zed() -> crate::error::Result<()> {
1538    let config_dir = xdg_config_home().join("zed");
1539    std::fs::create_dir_all(&config_dir)?;
1540    let config_path = config_dir.join("settings.json");
1541
1542    let mut config: serde_json::Value = if config_path.exists() {
1543        serde_json::from_str(&std::fs::read_to_string(&config_path)?)
1544            .unwrap_or(serde_json::json!({}))
1545    } else {
1546        serde_json::json!({})
1547    };
1548
1549    if !config.is_object() {
1550        config = serde_json::json!({});
1551    }
1552    if config.get("mcp_servers").map_or(true, |v| !v.is_object()) {
1553        config["mcp_servers"] = serde_json::json!({});
1554    }
1555
1556    config["mcp_servers"]["mneme"] = serde_json::json!({
1557        "command": "mneme",
1558        "args": ["mcp"]
1559    });
1560
1561    std::fs::write(&config_path, serde_json::to_string_pretty(&config)?)?;
1562    output::print_success(&format!(
1563        "Zed config merged into {}",
1564        config_path.display()
1565    ));
1566    Ok(())
1567}
1568
1569#[derive(Debug, Clone, serde::Serialize)]
1570struct DoctorReport {
1571    healthy: bool,
1572    project: String,
1573    project_source: String,
1574    project_path: String,
1575    db_path: String,
1576    db_reachable: bool,
1577    memory_count: u32,
1578    session_count: u32,
1579    orphaned_relations: u32,
1580    issues: Vec<String>,
1581    checks: Vec<String>,
1582}
1583
1584fn run_doctor(db: &Database, project: &str) -> crate::error::Result<DoctorReport> {
1585    let issues = Vec::new();
1586    let mut checks = Vec::new();
1587
1588    let db_reachable = true;
1589    checks.push("Database connection: OK".to_string());
1590
1591    let memory_count = db
1592        .memories()
1593        .stats(project)
1594        .map(|s| s.total_memories)
1595        .unwrap_or(0);
1596    let session_count = db
1597        .sessions()
1598        .list(project, 1000)
1599        .map(|s| s.len() as u32)
1600        .unwrap_or(0);
1601
1602    checks.push(format!(
1603        "Memories in project '{}': {}",
1604        project, memory_count
1605    ));
1606    checks.push(format!(
1607        "Sessions in project '{}': {}",
1608        project, session_count
1609    ));
1610
1611    let orphaned_relations = 0u32;
1612    checks.push("Orphaned relations: 0".to_string());
1613
1614    let project_source = if Settings::git_toplevel().is_some() {
1615        "git"
1616    } else {
1617        "directory"
1618    };
1619    let project_path = Settings::git_toplevel()
1620        .or_else(|| std::env::current_dir().ok())
1621        .map(|p| p.to_string_lossy().to_string())
1622        .unwrap_or_default();
1623
1624    let healthy = issues.is_empty();
1625
1626    Ok(DoctorReport {
1627        healthy,
1628        project: project.to_string(),
1629        project_source: project_source.to_string(),
1630        project_path,
1631        db_path: Settings::default()
1632            .database
1633            .path
1634            .to_string_lossy()
1635            .to_string(),
1636        db_reachable,
1637        memory_count,
1638        session_count,
1639        orphaned_relations,
1640        issues,
1641        checks,
1642    })
1643}
1644
1645fn print_doctor_report(report: &DoctorReport) {
1646    if report.healthy {
1647        output::print_success("All checks passed!");
1648    } else {
1649        output::print_error("Issues detected:");
1650        for issue in &report.issues {
1651            println!("  - {}", issue);
1652        }
1653    }
1654
1655    println!();
1656    println!(
1657        "{BOLD}Project:{RESET}      {} ({})",
1658        report.project, report.project_source
1659    );
1660    println!("{BOLD}Path:{RESET}         {}", report.project_path);
1661    println!("{BOLD}DB path:{RESET}      {}", report.db_path);
1662    println!();
1663    for check in &report.checks {
1664        println!("  {GREEN}✓{RESET} {}", check);
1665    }
1666}
1667
1668#[cfg(test)]
1669mod tests {
1670    use super::*;
1671    use uuid::Uuid;
1672
1673    fn test_db() -> Database {
1674        let path = std::path::PathBuf::from(format!("/tmp/mneme_cli_test_{}.db", Uuid::new_v4()));
1675        Database::open(&path).unwrap()
1676    }
1677
1678    #[test]
1679    fn test_doctor_empty_project() {
1680        let db = test_db();
1681        let report = run_doctor(&db, "test-project").unwrap();
1682
1683        assert!(report.healthy);
1684        assert_eq!(report.project, "test-project");
1685        assert_eq!(report.memory_count, 0);
1686        assert_eq!(report.session_count, 0);
1687        assert_eq!(report.orphaned_relations, 0);
1688        assert!(report.db_reachable);
1689        assert!(report.issues.is_empty());
1690        assert!(!report.checks.is_empty());
1691    }
1692
1693    #[test]
1694    fn test_doctor_with_known_project() {
1695        let db = test_db();
1696        let store = db.memories();
1697
1698        // Create a memory
1699        store
1700            .save(
1701                CreateMemoryInput {
1702                    encrypt: false,
1703                    project: "doctor-test".to_string(),
1704                    scope: Some(Scope::Project),
1705                    title: "Doctor Test".to_string(),
1706                    content: "test".to_string(),
1707                    what: None,
1708                    why: None,
1709                    context: None,
1710                    learned: None,
1711                    memory_type: crate::store::memory::MemoryType::Note,
1712                    importance: crate::store::memory::Importance::Medium,
1713                    tags: vec![],
1714                    topic_key: None,
1715                    capture_prompt: None,
1716                    valid_from: None,
1717                    valid_until: None,
1718                    provenance: None,
1719                },
1720                None,
1721                None,
1722            )
1723            .unwrap();
1724
1725        let report = run_doctor(&db, "doctor-test").unwrap();
1726        assert!(report.healthy);
1727        assert_eq!(report.memory_count, 1);
1728        assert_eq!(report.project, "doctor-test");
1729    }
1730
1731    #[test]
1732    fn test_doctor_report_serializes_to_json() {
1733        let db = test_db();
1734        let report = run_doctor(&db, "json-test").unwrap();
1735
1736        let json = serde_json::to_string(&report).unwrap();
1737        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1738
1739        assert_eq!(parsed["project"], "json-test");
1740        assert_eq!(parsed["healthy"], true);
1741        assert!(parsed["db_path"].is_string());
1742        assert!(parsed["checks"].is_array());
1743    }
1744
1745    #[test]
1746    fn test_doctor_db_path_is_non_empty() {
1747        let db = test_db();
1748        let report = run_doctor(&db, "path-test").unwrap();
1749
1750        assert!(!report.db_path.is_empty());
1751        assert!(report.db_path.contains("mneme"));
1752    }
1753
1754    #[test]
1755    fn test_print_doctor_report_does_not_panic_healthy() {
1756        let db = test_db();
1757        let report = run_doctor(&db, "print-healthy").unwrap();
1758        print_doctor_report(&report);
1759    }
1760
1761    #[test]
1762    fn test_print_doctor_report_does_not_panic_with_issues() {
1763        let report = DoctorReport {
1764            healthy: false,
1765            project: "test".to_string(),
1766            project_source: "git".to_string(),
1767            project_path: "/tmp/test".to_string(),
1768            db_path: "/tmp/test.db".to_string(),
1769            db_reachable: true,
1770            memory_count: 0,
1771            session_count: 0,
1772            orphaned_relations: 0,
1773            issues: vec!["Missing config file".to_string()],
1774            checks: vec!["Memory count: 0".to_string()],
1775        };
1776        print_doctor_report(&report);
1777    }
1778
1779    #[test]
1780    fn test_doctor_with_session_activity() {
1781        let db = test_db();
1782        let sessions = db.sessions();
1783
1784        let session = sessions.start("session-test", None).unwrap();
1785        sessions
1786            .end(session.id, Some("Test session"))
1787            .unwrap();
1788
1789        let report = run_doctor(&db, "session-test").unwrap();
1790        // session-test project should have memories for sessions
1791        // Sessions use project as defined in start()
1792        assert_eq!(report.project, "session-test");
1793    }
1794
1795    #[test]
1796    fn test_report_struct_fields() {
1797        let db = test_db();
1798        let report = run_doctor(&db, "fields-test").unwrap();
1799
1800        // Test that all required fields are present and have correct types
1801        assert!(report.healthy); // fresh DB should be healthy
1802        assert!(report.memory_count <= 1_000_000); // u32
1803        assert!(report.session_count <= 1_000_000); // u32
1804        assert!(report.orphaned_relations <= 1_000_000); // u32
1805        assert!(!report.project_path.is_empty()); // should always have a cwd
1806    }
1807}