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