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    // OpenCode reads opencode.json (not config.json)
1318    let config_path = opencode_dir.join("opencode.json");
1319    let mut config: serde_json::Value = if config_path.exists() {
1320        let content = std::fs::read_to_string(&config_path)?;
1321        serde_json::from_str(&content).unwrap_or(serde_json::json!({}))
1322    } else {
1323        // Legacy: check if config.json exists (old format) and migrate
1324        let legacy_path = opencode_dir.join("config.json");
1325        if legacy_path.exists() {
1326            let content = std::fs::read_to_string(&legacy_path)?;
1327            let cfg = serde_json::from_str(&content).unwrap_or(serde_json::json!({}));
1328            std::fs::remove_file(&legacy_path)?;
1329            cfg
1330        } else {
1331            serde_json::json!({})
1332        }
1333    };
1334
1335    if !config.is_object() {
1336        config = serde_json::json!({});
1337    }
1338    if config.get("mcp").map_or(true, |v| !v.is_object()) {
1339        config["mcp"] = serde_json::json!({});
1340    }
1341
1342    config["mcp"]["mneme"] = serde_json::json!({
1343        "command": ["mneme", "mcp"],
1344        "type": "local",
1345        "enabled": true
1346    });
1347
1348    let content = serde_json::to_string_pretty(&config)?;
1349    std::fs::write(&config_path, content)?;
1350
1351    output::print_success(&format!(
1352        "opencode config written to {}",
1353        config_path.display()
1354    ));
1355    Ok(())
1356}
1357
1358fn setup_claude_code() -> crate::error::Result<()> {
1359    let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
1360    let claude_dir = home.join(".claude");
1361    std::fs::create_dir_all(&claude_dir)?;
1362
1363    let config_path = claude_dir.join("settings.json");
1364    let mut config: serde_json::Value = if config_path.exists() {
1365        let content = std::fs::read_to_string(&config_path)?;
1366        serde_json::from_str(&content).unwrap_or_else(|_| serde_json::json!({}))
1367    } else {
1368        serde_json::json!({})
1369    };
1370
1371    config["mcpServers"] = serde_json::json!({
1372        "mneme": {
1373            "command": "mneme",
1374            "args": ["mcp"]
1375        }
1376    });
1377
1378    let content = serde_json::to_string_pretty(&config)?;
1379    std::fs::write(&config_path, content)?;
1380
1381    output::print_success(&format!(
1382        "Claude Code config written to {}",
1383        config_path.display()
1384    ));
1385    Ok(())
1386}
1387
1388fn setup_continue() -> crate::error::Result<()> {
1389    let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
1390    let continue_dir = home.join(".continue");
1391    std::fs::create_dir_all(&continue_dir)?;
1392
1393    let config_path = continue_dir.join("config.json");
1394    let mut config: serde_json::Value = if config_path.exists() {
1395        let content = std::fs::read_to_string(&config_path)?;
1396        serde_json::from_str(&content).unwrap_or_else(|_| serde_json::json!({}))
1397    } else {
1398        serde_json::json!({})
1399    };
1400
1401    let servers = config
1402        .get_mut("models")
1403        .and_then(|m| m.as_array_mut())
1404        .cloned()
1405        .unwrap_or_default();
1406
1407    let mut new_servers = servers;
1408    new_servers.push(serde_json::json!({
1409        "title": "Mneme MCP",
1410        "provider": "mcp",
1411        "model": "mneme",
1412        "apiBase": "",
1413        "server": {
1414            "command": "mneme",
1415            "args": ["mcp"]
1416        }
1417    }));
1418
1419    config["models"] = serde_json::json!(new_servers);
1420
1421    let content = serde_json::to_string_pretty(&config)?;
1422    std::fs::write(&config_path, content)?;
1423
1424    output::print_success(&format!(
1425        "Continue config written to {}",
1426        config_path.display()
1427    ));
1428    Ok(())
1429}
1430
1431// ─── Multi-agent setup helpers ──────────────────────────────────────────────
1432
1433fn home_dir() -> std::path::PathBuf {
1434    dirs::home_dir().unwrap_or_else(|| std::path::PathBuf::from("."))
1435}
1436
1437fn xdg_config_home() -> std::path::PathBuf {
1438    std::env::var("XDG_CONFIG_HOME")
1439        .map(std::path::PathBuf::from)
1440        .unwrap_or_else(|_| home_dir().join(".config"))
1441}
1442
1443/// Escribe un config MCP estándar con `mcpServers` → `mneme`.
1444/// Usado por: cursor, windsurf, gemini-cli, codex.
1445fn write_standard_mcp_config(config_path: std::path::PathBuf, label: &str) -> crate::error::Result<()> {
1446    if let Some(parent) = config_path.parent() {
1447        std::fs::create_dir_all(parent)?;
1448    }
1449
1450    let mut config: serde_json::Value = if config_path.exists() {
1451        let content = std::fs::read_to_string(&config_path)?;
1452        serde_json::from_str(&content).unwrap_or(serde_json::json!({}))
1453    } else {
1454        serde_json::json!({})
1455    };
1456
1457    if !config.is_object() {
1458        config = serde_json::json!({});
1459    }
1460    if config.get("mcpServers").map_or(true, |v| !v.is_object()) {
1461        config["mcpServers"] = serde_json::json!({});
1462    }
1463
1464    config["mcpServers"]["mneme"] = serde_json::json!({
1465        "command": "mneme",
1466        "args": ["mcp"]
1467    });
1468
1469    std::fs::write(&config_path, serde_json::to_string_pretty(&config)?)?;
1470    output::print_success(&format!("{label} config written to {}", config_path.display()));
1471    Ok(())
1472}
1473
1474/// Cursor — ~/.cursor/mcp.json
1475fn setup_cursor() -> crate::error::Result<()> {
1476    let config_dir = home_dir().join(".cursor");
1477    let config_path = config_dir.join("mcp.json");
1478    write_standard_mcp_config(config_path, "Cursor")
1479}
1480
1481/// Windsurf — ~/.codeium/windsurf/mcp_config.json
1482fn setup_windsurf() -> crate::error::Result<()> {
1483    let config_dir = home_dir().join(".codeium").join("windsurf");
1484    let config_path = config_dir.join("mcp_config.json");
1485    write_standard_mcp_config(config_path, "Windsurf")
1486}
1487
1488/// Gemini CLI — ~/.config/gemini-cli/mcp.json
1489fn setup_gemini_cli() -> crate::error::Result<()> {
1490    let config_dir = xdg_config_home().join("gemini-cli");
1491    let config_path = config_dir.join("mcp.json");
1492    write_standard_mcp_config(config_path, "Gemini CLI")
1493}
1494
1495/// Codex CLI (OpenAI) — ~/.codex/settings.json
1496fn setup_codex() -> crate::error::Result<()> {
1497    let config_dir = home_dir().join(".codex");
1498    let config_path = config_dir.join("settings.json");
1499    write_standard_mcp_config(config_path, "Codex CLI")
1500}
1501
1502/// VS Code Copilot Chat — varía por OS
1503fn setup_vscode_copilot() -> crate::error::Result<()> {
1504    // VS Code global storage path per platform
1505    #[cfg(target_os = "linux")]
1506    let base = xdg_config_home().join("Code").join("User");
1507    #[cfg(target_os = "macos")]
1508    let base = home_dir().join("Library").join("Application Support").join("Code").join("User");
1509    #[cfg(target_os = "windows")]
1510    let base = {
1511        let appdata = std::env::var("APPDATA").unwrap_or_else(|_| ".".to_string());
1512        std::path::PathBuf::from(appdata).join("Code").join("User")
1513    };
1514    #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
1515    let base = xdg_config_home().join("Code").join("User");
1516
1517    let config_dir = base.join("globalStorage").join("github.copilot-chat");
1518    std::fs::create_dir_all(&config_dir)?;
1519    let config_path = config_dir.join("mcp_config.json");
1520
1521    let mut config: serde_json::Value = if config_path.exists() {
1522        serde_json::from_str(&std::fs::read_to_string(&config_path)?)
1523            .unwrap_or(serde_json::json!({}))
1524    } else {
1525        serde_json::json!({})
1526    };
1527
1528    if !config.is_object() {
1529        config = serde_json::json!({});
1530    }
1531    if config.get("servers").map_or(true, |v| !v.is_object()) {
1532        config["servers"] = serde_json::json!({});
1533    }
1534
1535    config["servers"]["mneme"] = serde_json::json!({
1536        "type": "stdio",
1537        "command": "mneme",
1538        "args": ["mcp"]
1539    });
1540
1541    std::fs::write(&config_path, serde_json::to_string_pretty(&config)?)?;
1542    output::print_success(&format!(
1543        "VS Code Copilot Chat config written to {}",
1544        config_path.display()
1545    ));
1546    Ok(())
1547}
1548
1549/// Zed — ~/.config/zed/settings.json (mergea mcp_servers dentro del settings existente)
1550fn setup_zed() -> crate::error::Result<()> {
1551    let config_dir = xdg_config_home().join("zed");
1552    std::fs::create_dir_all(&config_dir)?;
1553    let config_path = config_dir.join("settings.json");
1554
1555    let mut config: serde_json::Value = if config_path.exists() {
1556        serde_json::from_str(&std::fs::read_to_string(&config_path)?)
1557            .unwrap_or(serde_json::json!({}))
1558    } else {
1559        serde_json::json!({})
1560    };
1561
1562    if !config.is_object() {
1563        config = serde_json::json!({});
1564    }
1565    if config.get("mcp_servers").map_or(true, |v| !v.is_object()) {
1566        config["mcp_servers"] = serde_json::json!({});
1567    }
1568
1569    config["mcp_servers"]["mneme"] = serde_json::json!({
1570        "command": "mneme",
1571        "args": ["mcp"]
1572    });
1573
1574    std::fs::write(&config_path, serde_json::to_string_pretty(&config)?)?;
1575    output::print_success(&format!(
1576        "Zed config merged into {}",
1577        config_path.display()
1578    ));
1579    Ok(())
1580}
1581
1582#[derive(Debug, Clone, serde::Serialize)]
1583struct DoctorReport {
1584    healthy: bool,
1585    project: String,
1586    project_source: String,
1587    project_path: String,
1588    db_path: String,
1589    db_reachable: bool,
1590    memory_count: u32,
1591    session_count: u32,
1592    orphaned_relations: u32,
1593    issues: Vec<String>,
1594    checks: Vec<String>,
1595}
1596
1597fn run_doctor(db: &Database, project: &str) -> crate::error::Result<DoctorReport> {
1598    let issues = Vec::new();
1599    let mut checks = Vec::new();
1600
1601    let db_reachable = true;
1602    checks.push("Database connection: OK".to_string());
1603
1604    let memory_count = db
1605        .memories()
1606        .stats(project)
1607        .map(|s| s.total_memories)
1608        .unwrap_or(0);
1609    let session_count = db
1610        .sessions()
1611        .list(project, 1000)
1612        .map(|s| s.len() as u32)
1613        .unwrap_or(0);
1614
1615    checks.push(format!(
1616        "Memories in project '{}': {}",
1617        project, memory_count
1618    ));
1619    checks.push(format!(
1620        "Sessions in project '{}': {}",
1621        project, session_count
1622    ));
1623
1624    let orphaned_relations = 0u32;
1625    checks.push("Orphaned relations: 0".to_string());
1626
1627    let project_source = if Settings::git_toplevel().is_some() {
1628        "git"
1629    } else {
1630        "directory"
1631    };
1632    let project_path = Settings::git_toplevel()
1633        .or_else(|| std::env::current_dir().ok())
1634        .map(|p| p.to_string_lossy().to_string())
1635        .unwrap_or_default();
1636
1637    let healthy = issues.is_empty();
1638
1639    Ok(DoctorReport {
1640        healthy,
1641        project: project.to_string(),
1642        project_source: project_source.to_string(),
1643        project_path,
1644        db_path: Settings::default()
1645            .database
1646            .path
1647            .to_string_lossy()
1648            .to_string(),
1649        db_reachable,
1650        memory_count,
1651        session_count,
1652        orphaned_relations,
1653        issues,
1654        checks,
1655    })
1656}
1657
1658fn print_doctor_report(report: &DoctorReport) {
1659    if report.healthy {
1660        output::print_success("All checks passed!");
1661    } else {
1662        output::print_error("Issues detected:");
1663        for issue in &report.issues {
1664            println!("  - {}", issue);
1665        }
1666    }
1667
1668    println!();
1669    println!(
1670        "{BOLD}Project:{RESET}      {} ({})",
1671        report.project, report.project_source
1672    );
1673    println!("{BOLD}Path:{RESET}         {}", report.project_path);
1674    println!("{BOLD}DB path:{RESET}      {}", report.db_path);
1675    println!();
1676    for check in &report.checks {
1677        println!("  {GREEN}✓{RESET} {}", check);
1678    }
1679}
1680
1681#[cfg(test)]
1682mod tests {
1683    use super::*;
1684    use uuid::Uuid;
1685
1686    fn test_db() -> Database {
1687        let path = std::path::PathBuf::from(format!("/tmp/mneme_cli_test_{}.db", Uuid::new_v4()));
1688        Database::open(&path).unwrap()
1689    }
1690
1691    #[test]
1692    fn test_doctor_empty_project() {
1693        let db = test_db();
1694        let report = run_doctor(&db, "test-project").unwrap();
1695
1696        assert!(report.healthy);
1697        assert_eq!(report.project, "test-project");
1698        assert_eq!(report.memory_count, 0);
1699        assert_eq!(report.session_count, 0);
1700        assert_eq!(report.orphaned_relations, 0);
1701        assert!(report.db_reachable);
1702        assert!(report.issues.is_empty());
1703        assert!(!report.checks.is_empty());
1704    }
1705
1706    #[test]
1707    fn test_doctor_with_known_project() {
1708        let db = test_db();
1709        let store = db.memories();
1710
1711        // Create a memory
1712        store
1713            .save(
1714                CreateMemoryInput {
1715                    encrypt: false,
1716                    project: "doctor-test".to_string(),
1717                    scope: Some(Scope::Project),
1718                    title: "Doctor Test".to_string(),
1719                    content: "test".to_string(),
1720                    what: None,
1721                    why: None,
1722                    context: None,
1723                    learned: None,
1724                    memory_type: crate::store::memory::MemoryType::Note,
1725                    importance: crate::store::memory::Importance::Medium,
1726                    tags: vec![],
1727                    topic_key: None,
1728                    capture_prompt: None,
1729                    valid_from: None,
1730                    valid_until: None,
1731                    provenance: None,
1732                },
1733                None,
1734                None,
1735            )
1736            .unwrap();
1737
1738        let report = run_doctor(&db, "doctor-test").unwrap();
1739        assert!(report.healthy);
1740        assert_eq!(report.memory_count, 1);
1741        assert_eq!(report.project, "doctor-test");
1742    }
1743
1744    #[test]
1745    fn test_doctor_report_serializes_to_json() {
1746        let db = test_db();
1747        let report = run_doctor(&db, "json-test").unwrap();
1748
1749        let json = serde_json::to_string(&report).unwrap();
1750        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
1751
1752        assert_eq!(parsed["project"], "json-test");
1753        assert_eq!(parsed["healthy"], true);
1754        assert!(parsed["db_path"].is_string());
1755        assert!(parsed["checks"].is_array());
1756    }
1757
1758    #[test]
1759    fn test_doctor_db_path_is_non_empty() {
1760        let db = test_db();
1761        let report = run_doctor(&db, "path-test").unwrap();
1762
1763        assert!(!report.db_path.is_empty());
1764        assert!(report.db_path.contains("mneme"));
1765    }
1766
1767    #[test]
1768    fn test_print_doctor_report_does_not_panic_healthy() {
1769        let db = test_db();
1770        let report = run_doctor(&db, "print-healthy").unwrap();
1771        print_doctor_report(&report);
1772    }
1773
1774    #[test]
1775    fn test_print_doctor_report_does_not_panic_with_issues() {
1776        let report = DoctorReport {
1777            healthy: false,
1778            project: "test".to_string(),
1779            project_source: "git".to_string(),
1780            project_path: "/tmp/test".to_string(),
1781            db_path: "/tmp/test.db".to_string(),
1782            db_reachable: true,
1783            memory_count: 0,
1784            session_count: 0,
1785            orphaned_relations: 0,
1786            issues: vec!["Missing config file".to_string()],
1787            checks: vec!["Memory count: 0".to_string()],
1788        };
1789        print_doctor_report(&report);
1790    }
1791
1792    #[test]
1793    fn test_doctor_with_session_activity() {
1794        let db = test_db();
1795        let sessions = db.sessions();
1796
1797        let session = sessions.start("session-test", None).unwrap();
1798        sessions
1799            .end(session.id, Some("Test session"))
1800            .unwrap();
1801
1802        let report = run_doctor(&db, "session-test").unwrap();
1803        // session-test project should have memories for sessions
1804        // Sessions use project as defined in start()
1805        assert_eq!(report.project, "session-test");
1806    }
1807
1808    #[test]
1809    fn test_report_struct_fields() {
1810        let db = test_db();
1811        let report = run_doctor(&db, "fields-test").unwrap();
1812
1813        // Test that all required fields are present and have correct types
1814        assert!(report.healthy); // fresh DB should be healthy
1815        assert!(report.memory_count <= 1_000_000); // u32
1816        assert!(report.session_count <= 1_000_000); // u32
1817        assert!(report.orphaned_relations <= 1_000_000); // u32
1818        assert!(!report.project_path.is_empty()); // should always have a cwd
1819    }
1820}