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