memvid_cli/commands/
config.rs

1//! Config management commands (set, get, list, unset, check)
2//!
3//! Commands for managing persistent CLI configuration including API keys.
4
5use anyhow::{Context, Result};
6use clap::{Args, Subcommand};
7use serde::{Deserialize, Serialize};
8use serde_json::json;
9use std::collections::HashMap;
10use std::fs;
11use std::path::PathBuf;
12
13/// Config management commands
14#[derive(Subcommand)]
15pub enum ConfigCommand {
16    /// Set a configuration value
17    Set(ConfigSetArgs),
18    /// Get a configuration value
19    Get(ConfigGetArgs),
20    /// List all configuration values
21    List(ConfigListArgs),
22    /// Remove a configuration value
23    Unset(ConfigUnsetArgs),
24    /// Check configuration validity (verifies API key with server)
25    Check(ConfigCheckArgs),
26}
27
28/// Arguments for the `config` command
29#[derive(Args)]
30pub struct ConfigArgs {
31    #[command(subcommand)]
32    pub command: ConfigCommand,
33}
34
35/// Arguments for `config set`
36#[derive(Args)]
37pub struct ConfigSetArgs {
38    /// Configuration key (e.g., api_key, api_url)
39    pub key: String,
40    /// Value to set
41    pub value: String,
42    /// Output as JSON
43    #[arg(long)]
44    pub json: bool,
45}
46
47/// Arguments for `config get`
48#[derive(Args)]
49pub struct ConfigGetArgs {
50    /// Configuration key to retrieve
51    pub key: String,
52    /// Output as JSON
53    #[arg(long)]
54    pub json: bool,
55}
56
57/// Arguments for `config list`
58#[derive(Args)]
59pub struct ConfigListArgs {
60    /// Output as JSON
61    #[arg(long)]
62    pub json: bool,
63    /// Show values (by default, sensitive values are masked)
64    #[arg(long)]
65    pub show_values: bool,
66}
67
68/// Arguments for `config unset`
69#[derive(Args)]
70pub struct ConfigUnsetArgs {
71    /// Configuration key to remove
72    pub key: String,
73    /// Output as JSON
74    #[arg(long)]
75    pub json: bool,
76}
77
78/// Arguments for `config check`
79#[derive(Args)]
80pub struct ConfigCheckArgs {
81    /// Output as JSON
82    #[arg(long)]
83    pub json: bool,
84}
85
86/// Persistent configuration stored in config file
87#[derive(Debug, Clone, Default, Serialize, Deserialize)]
88pub struct PersistentConfig {
89    /// API key for authentication
90    pub api_key: Option<String>,
91    /// API URL (defaults to https://memvid.com) - legacy, use dashboard_url
92    pub api_url: Option<String>,
93    /// Dashboard URL (defaults to https://memvid.com)
94    pub dashboard_url: Option<String>,
95    /// Default memory ID for dashboard sync (legacy, use [memory] section)
96    pub memory_id: Option<String>,
97    /// Named memory IDs (e.g., memory.default, memory.work)
98    #[serde(default)]
99    pub memory: HashMap<String, String>,
100    /// Default embedding provider
101    pub default_embedding_provider: Option<String>,
102    /// Default LLM provider
103    pub default_llm_provider: Option<String>,
104    /// Additional custom settings
105    #[serde(flatten)]
106    pub extra: HashMap<String, String>,
107}
108
109impl PersistentConfig {
110    /// Get the config file path (~/.config/memvid/config.toml)
111    pub fn config_path() -> Result<PathBuf> {
112        let config_dir = dirs::config_dir()
113            .context("Could not determine config directory")?
114            .join("memvid");
115        Ok(config_dir.join("config.toml"))
116    }
117
118    /// Load config from file, or return default if file doesn't exist
119    pub fn load() -> Result<Self> {
120        let path = Self::config_path()?;
121        if !path.exists() {
122            return Ok(Self::default());
123        }
124        let content = fs::read_to_string(&path)
125            .with_context(|| format!("Failed to read config file: {}", path.display()))?;
126        let config: PersistentConfig = toml::from_str(&content)
127            .with_context(|| format!("Failed to parse config file: {}", path.display()))?;
128        Ok(config)
129    }
130
131    /// Save config to file
132    pub fn save(&self) -> Result<()> {
133        let path = Self::config_path()?;
134        if let Some(parent) = path.parent() {
135            fs::create_dir_all(parent)
136                .with_context(|| format!("Failed to create config directory: {}", parent.display()))?;
137        }
138        let content = toml::to_string_pretty(self)
139            .context("Failed to serialize config")?;
140        fs::write(&path, content)
141            .with_context(|| format!("Failed to write config file: {}", path.display()))?;
142        Ok(())
143    }
144
145    /// Get a value by key (supports memory.<name> syntax)
146    pub fn get(&self, key: &str) -> Option<String> {
147        // Handle memory.<name> syntax
148        if let Some(name) = key.strip_prefix("memory.") {
149            return self.memory.get(name).cloned();
150        }
151
152        match key {
153            "api_key" => self.api_key.clone(),
154            "api_url" => self.api_url.clone(),
155            "dashboard_url" => self.dashboard_url.clone(),
156            "memory_id" => self.memory_id.clone(),
157            "default_embedding_provider" => self.default_embedding_provider.clone(),
158            "default_llm_provider" => self.default_llm_provider.clone(),
159            _ => self.extra.get(key).cloned(),
160        }
161    }
162
163    /// Set a value by key (supports memory.<name> syntax)
164    pub fn set(&mut self, key: &str, value: String) {
165        // Handle memory.<name> syntax
166        if let Some(name) = key.strip_prefix("memory.") {
167            self.memory.insert(name.to_string(), value);
168            return;
169        }
170
171        match key {
172            "api_key" => self.api_key = Some(value),
173            "api_url" => self.api_url = Some(value),
174            "dashboard_url" => self.dashboard_url = Some(value),
175            "memory_id" => self.memory_id = Some(value),
176            "default_embedding_provider" => self.default_embedding_provider = Some(value),
177            "default_llm_provider" => self.default_llm_provider = Some(value),
178            _ => {
179                self.extra.insert(key.to_string(), value);
180            }
181        }
182    }
183
184    /// Unset a value by key (supports memory.<name> syntax)
185    pub fn unset(&mut self, key: &str) -> bool {
186        // Handle memory.<name> syntax
187        if let Some(name) = key.strip_prefix("memory.") {
188            return self.memory.remove(name).is_some();
189        }
190
191        match key {
192            "api_key" => {
193                let had_value = self.api_key.is_some();
194                self.api_key = None;
195                had_value
196            }
197            "api_url" => {
198                let had_value = self.api_url.is_some();
199                self.api_url = None;
200                had_value
201            }
202            "dashboard_url" => {
203                let had_value = self.dashboard_url.is_some();
204                self.dashboard_url = None;
205                had_value
206            }
207            "memory_id" => {
208                let had_value = self.memory_id.is_some();
209                self.memory_id = None;
210                had_value
211            }
212            "default_embedding_provider" => {
213                let had_value = self.default_embedding_provider.is_some();
214                self.default_embedding_provider = None;
215                had_value
216            }
217            "default_llm_provider" => {
218                let had_value = self.default_llm_provider.is_some();
219                self.default_llm_provider = None;
220                had_value
221            }
222            _ => self.extra.remove(key).is_some(),
223        }
224    }
225
226    /// Get the default memory ID (from memory.default or legacy memory_id)
227    pub fn default_memory_id(&self) -> Option<String> {
228        self.memory.get("default").cloned().or_else(|| self.memory_id.clone())
229    }
230
231    /// Get a named memory ID
232    pub fn get_memory(&self, name: &str) -> Option<String> {
233        self.memory.get(name).cloned()
234    }
235
236    /// Get all keys and values as a map
237    pub fn to_map(&self) -> HashMap<String, Option<String>> {
238        let mut map = HashMap::new();
239        map.insert("api_key".to_string(), self.api_key.clone());
240        map.insert("dashboard_url".to_string(), self.dashboard_url.clone());
241        // Include named memories
242        for (name, id) in &self.memory {
243            map.insert(format!("memory.{}", name), Some(id.clone()));
244        }
245        map.insert("default_embedding_provider".to_string(), self.default_embedding_provider.clone());
246        map.insert("default_llm_provider".to_string(), self.default_llm_provider.clone());
247        for (k, v) in &self.extra {
248            map.insert(k.clone(), Some(v.clone()));
249        }
250        map
251    }
252
253    /// Check if a key is sensitive (should be masked in output)
254    pub fn is_sensitive(key: &str) -> bool {
255        key.contains("key") || key.contains("secret") || key.contains("token") || key.contains("password")
256    }
257
258    /// Mask a sensitive value for display
259    pub fn mask_value(value: &str) -> String {
260        if value.len() <= 8 {
261            "*".repeat(value.len())
262        } else {
263            format!("{}...{}", &value[..4], &value[value.len()-4..])
264        }
265    }
266}
267
268/// Known configuration keys for help text
269const KNOWN_KEYS: &[(&str, &str)] = &[
270    ("api_key", "Memvid API key for authentication"),
271    ("dashboard_url", "Dashboard URL (default: https://memvid.com)"),
272    ("memory.<name>", "Named memory ID (e.g., memory.default, memory.work)"),
273    ("default_embedding_provider", "Default embedding provider (e.g., openai, local)"),
274    ("default_llm_provider", "Default LLM provider for ask commands"),
275];
276
277pub fn handle_config(args: ConfigArgs) -> Result<()> {
278    match args.command {
279        ConfigCommand::Set(set_args) => handle_config_set(set_args),
280        ConfigCommand::Get(get_args) => handle_config_get(get_args),
281        ConfigCommand::List(list_args) => handle_config_list(list_args),
282        ConfigCommand::Unset(unset_args) => handle_config_unset(unset_args),
283        ConfigCommand::Check(check_args) => handle_config_check(check_args),
284    }
285}
286
287fn handle_config_set(args: ConfigSetArgs) -> Result<()> {
288    let mut config = PersistentConfig::load()?;
289    config.set(&args.key, args.value.clone());
290    config.save()?;
291
292    let display_value = if PersistentConfig::is_sensitive(&args.key) {
293        PersistentConfig::mask_value(&args.value)
294    } else {
295        args.value.clone()
296    };
297
298    if args.json {
299        let output = json!({
300            "success": true,
301            "key": args.key,
302            "value": display_value,
303            "message": format!("Configuration '{}' has been set", args.key),
304        });
305        println!("{}", serde_json::to_string_pretty(&output)?);
306    } else {
307        println!("Set {} = {}", args.key, display_value);
308    }
309
310    Ok(())
311}
312
313fn handle_config_get(args: ConfigGetArgs) -> Result<()> {
314    let config = PersistentConfig::load()?;
315
316    match config.get(&args.key) {
317        Some(value) => {
318            let display_value = if PersistentConfig::is_sensitive(&args.key) {
319                PersistentConfig::mask_value(&value)
320            } else {
321                value.clone()
322            };
323
324            if args.json {
325                let output = json!({
326                    "key": args.key,
327                    "value": display_value,
328                    "found": true,
329                });
330                println!("{}", serde_json::to_string_pretty(&output)?);
331            } else {
332                println!("{}", display_value);
333            }
334        }
335        None => {
336            if args.json {
337                let output = json!({
338                    "key": args.key,
339                    "value": null,
340                    "found": false,
341                });
342                println!("{}", serde_json::to_string_pretty(&output)?);
343            } else {
344                // Check if it's a known key
345                if let Some((_, description)) = KNOWN_KEYS.iter().find(|(k, _)| *k == args.key) {
346                    println!("'{}' is not set", args.key);
347                    println!("Description: {}", description);
348                } else {
349                    println!("'{}' is not set", args.key);
350                }
351            }
352        }
353    }
354
355    Ok(())
356}
357
358fn handle_config_list(args: ConfigListArgs) -> Result<()> {
359    let config = PersistentConfig::load()?;
360    let map = config.to_map();
361    let config_path = PersistentConfig::config_path()?;
362
363    if args.json {
364        let mut values = serde_json::Map::new();
365        for (key, value) in &map {
366            if let Some(v) = value {
367                let display_value = if !args.show_values && PersistentConfig::is_sensitive(&key) {
368                    PersistentConfig::mask_value(v)
369                } else {
370                    v.clone()
371                };
372                values.insert(key.clone(), json!(display_value));
373            }
374        }
375        let output = json!({
376            "config_path": config_path.display().to_string(),
377            "values": values,
378        });
379        println!("{}", serde_json::to_string_pretty(&output)?);
380    } else {
381        println!("Config file: {}", config_path.display());
382        println!();
383
384        let mut has_values = false;
385        for (key, description) in KNOWN_KEYS {
386            if let Some(Some(value)) = map.get(*key) {
387                has_values = true;
388                let display_value = if !args.show_values && PersistentConfig::is_sensitive(key) {
389                    PersistentConfig::mask_value(value)
390                } else {
391                    value.clone()
392                };
393                println!("  {} = {}", key, display_value);
394                println!("    {}", description);
395            }
396        }
397
398        // Show named memories
399        if !config.memory.is_empty() {
400            has_values = true;
401            println!();
402            println!("  [memory]");
403            for (name, id) in &config.memory {
404                println!("    {} = {}", name, id);
405            }
406        }
407
408        // Show any extra custom keys
409        for (key, value) in &config.extra {
410            has_values = true;
411            let display_value = if !args.show_values && PersistentConfig::is_sensitive(&key) {
412                PersistentConfig::mask_value(value)
413            } else {
414                value.clone()
415            };
416            println!("  {} = {}", key, display_value);
417        }
418
419        if !has_values {
420            println!("  (no configuration set)");
421            println!();
422            println!("Available keys:");
423            for (key, description) in KNOWN_KEYS {
424                println!("  {} - {}", key, description);
425            }
426        }
427
428        if !args.show_values {
429            println!();
430            println!("Use --show-values to display full values");
431        }
432    }
433
434    Ok(())
435}
436
437fn handle_config_unset(args: ConfigUnsetArgs) -> Result<()> {
438    let mut config = PersistentConfig::load()?;
439    let was_set = config.unset(&args.key);
440    config.save()?;
441
442    if args.json {
443        let output = json!({
444            "success": true,
445            "key": args.key,
446            "was_set": was_set,
447            "message": if was_set {
448                format!("Configuration '{}' has been removed", args.key)
449            } else {
450                format!("Configuration '{}' was not set", args.key)
451            },
452        });
453        println!("{}", serde_json::to_string_pretty(&output)?);
454    } else {
455        if was_set {
456            println!("Removed '{}'", args.key);
457        } else {
458            println!("'{}' was not set", args.key);
459        }
460    }
461
462    Ok(())
463}
464
465fn handle_config_check(args: ConfigCheckArgs) -> Result<()> {
466    let config = PersistentConfig::load()?;
467    let config_path = PersistentConfig::config_path()?;
468
469    // Also check environment variables
470    let env_api_key = std::env::var("MEMVID_API_KEY").ok();
471    let env_api_url = std::env::var("MEMVID_API_URL").ok();
472
473    // Determine effective values (env takes precedence)
474    let effective_api_key = env_api_key.clone().or(config.api_key.clone());
475    let effective_api_url = env_api_url.clone()
476        .or(config.api_url.clone())
477        .unwrap_or_else(|| "https://memvid.com".to_string());
478
479    let has_api_key = effective_api_key.is_some();
480    let api_key_source = if env_api_key.is_some() {
481        "environment"
482    } else if config.api_key.is_some() {
483        "config file"
484    } else {
485        "not set"
486    };
487
488    let api_url_source = if env_api_url.is_some() {
489        "environment"
490    } else if config.api_url.is_some() {
491        "config file"
492    } else {
493        "default"
494    };
495
496    // TODO: Actually verify the API key with the server
497    // For now, just check if it's set
498    let api_key_valid = has_api_key; // Placeholder - should verify with server
499
500    if args.json {
501        let output = json!({
502            "config_path": config_path.display().to_string(),
503            "api_key": {
504                "set": has_api_key,
505                "source": api_key_source,
506                "valid": api_key_valid,
507            },
508            "api_url": {
509                "value": effective_api_url,
510                "source": api_url_source,
511            },
512            "ready": has_api_key && api_key_valid,
513        });
514        println!("{}", serde_json::to_string_pretty(&output)?);
515    } else {
516        println!("Configuration Check");
517        println!("===================");
518        println!();
519        println!("Config file: {}", config_path.display());
520        println!();
521
522        // API Key status
523        if has_api_key {
524            println!("API Key: {} (source: {})",
525                if api_key_valid { "valid" } else { "invalid" },
526                api_key_source
527            );
528            if let Some(key) = &effective_api_key {
529                println!("  Value: {}", PersistentConfig::mask_value(key));
530            }
531        } else {
532            println!("API Key: not configured");
533            println!();
534            println!("To set your API key:");
535            println!("  memvid config set api_key <your-key>");
536            println!("  OR");
537            println!("  export MEMVID_API_KEY=<your-key>");
538            println!();
539            println!("Get your API key at: https://memvid.com/dashboard/api-keys");
540        }
541
542        println!();
543        println!("API URL: {} (source: {})", effective_api_url, api_url_source);
544
545        println!();
546        if has_api_key && api_key_valid {
547            println!("Status: Ready to use");
548        } else {
549            println!("Status: API key required");
550        }
551    }
552
553    Ok(())
554}