Skip to main content

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).with_context(|| {
136                format!("Failed to create config directory: {}", parent.display())
137            })?;
138        }
139        let content = toml::to_string_pretty(self).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
229            .get("default")
230            .cloned()
231            .or_else(|| self.memory_id.clone())
232    }
233
234    /// Get a named memory ID
235    pub fn get_memory(&self, name: &str) -> Option<String> {
236        self.memory.get(name).cloned()
237    }
238
239    /// Get all keys and values as a map
240    pub fn to_map(&self) -> HashMap<String, Option<String>> {
241        let mut map = HashMap::new();
242        map.insert("api_key".to_string(), self.api_key.clone());
243        map.insert("dashboard_url".to_string(), self.dashboard_url.clone());
244        // Include named memories
245        for (name, id) in &self.memory {
246            map.insert(format!("memory.{}", name), Some(id.clone()));
247        }
248        map.insert(
249            "default_embedding_provider".to_string(),
250            self.default_embedding_provider.clone(),
251        );
252        map.insert(
253            "default_llm_provider".to_string(),
254            self.default_llm_provider.clone(),
255        );
256        for (k, v) in &self.extra {
257            map.insert(k.clone(), Some(v.clone()));
258        }
259        map
260    }
261
262    /// Check if a key is sensitive (should be masked in output)
263    pub fn is_sensitive(key: &str) -> bool {
264        key.contains("key")
265            || key.contains("secret")
266            || key.contains("token")
267            || key.contains("password")
268    }
269
270    /// Mask a sensitive value for display
271    pub fn mask_value(value: &str) -> String {
272        if value.len() <= 8 {
273            "*".repeat(value.len())
274        } else {
275            format!("{}...{}", &value[..4], &value[value.len() - 4..])
276        }
277    }
278}
279
280/// Known configuration keys for help text
281const KNOWN_KEYS: &[(&str, &str)] = &[
282    ("api_key", "Memvid API key for authentication"),
283    (
284        "dashboard_url",
285        "Dashboard URL (default: https://memvid.com)",
286    ),
287    (
288        "memory.<name>",
289        "Named memory ID (e.g., memory.default, memory.work)",
290    ),
291    (
292        "default_embedding_provider",
293        "Default embedding provider (e.g., openai, local)",
294    ),
295    (
296        "default_llm_provider",
297        "Default LLM provider for ask commands",
298    ),
299];
300
301pub fn handle_config(args: ConfigArgs) -> Result<()> {
302    match args.command {
303        ConfigCommand::Set(set_args) => handle_config_set(set_args),
304        ConfigCommand::Get(get_args) => handle_config_get(get_args),
305        ConfigCommand::List(list_args) => handle_config_list(list_args),
306        ConfigCommand::Unset(unset_args) => handle_config_unset(unset_args),
307        ConfigCommand::Check(check_args) => handle_config_check(check_args),
308    }
309}
310
311fn handle_config_set(args: ConfigSetArgs) -> Result<()> {
312    let mut config = PersistentConfig::load()?;
313    config.set(&args.key, args.value.clone());
314    config.save()?;
315
316    let display_value = if PersistentConfig::is_sensitive(&args.key) {
317        PersistentConfig::mask_value(&args.value)
318    } else {
319        args.value.clone()
320    };
321
322    if args.json {
323        let output = json!({
324            "success": true,
325            "key": args.key,
326            "value": display_value,
327            "message": format!("Configuration '{}' has been set", args.key),
328        });
329        println!("{}", serde_json::to_string_pretty(&output)?);
330    } else {
331        println!("Set {} = {}", args.key, display_value);
332    }
333
334    Ok(())
335}
336
337fn handle_config_get(args: ConfigGetArgs) -> Result<()> {
338    let config = PersistentConfig::load()?;
339
340    match config.get(&args.key) {
341        Some(value) => {
342            let display_value = if PersistentConfig::is_sensitive(&args.key) {
343                PersistentConfig::mask_value(&value)
344            } else {
345                value.clone()
346            };
347
348            if args.json {
349                let output = json!({
350                    "key": args.key,
351                    "value": display_value,
352                    "found": true,
353                });
354                println!("{}", serde_json::to_string_pretty(&output)?);
355            } else {
356                println!("{}", display_value);
357            }
358        }
359        None => {
360            if args.json {
361                let output = json!({
362                    "key": args.key,
363                    "value": null,
364                    "found": false,
365                });
366                println!("{}", serde_json::to_string_pretty(&output)?);
367            } else {
368                // Check if it's a known key
369                if let Some((_, description)) = KNOWN_KEYS.iter().find(|(k, _)| *k == args.key) {
370                    println!("'{}' is not set", args.key);
371                    println!("Description: {}", description);
372                } else {
373                    println!("'{}' is not set", args.key);
374                }
375            }
376        }
377    }
378
379    Ok(())
380}
381
382fn handle_config_list(args: ConfigListArgs) -> Result<()> {
383    let config = PersistentConfig::load()?;
384    let map = config.to_map();
385    let config_path = PersistentConfig::config_path()?;
386
387    if args.json {
388        let mut values = serde_json::Map::new();
389        for (key, value) in &map {
390            if let Some(v) = value {
391                let display_value = if !args.show_values && PersistentConfig::is_sensitive(&key) {
392                    PersistentConfig::mask_value(v)
393                } else {
394                    v.clone()
395                };
396                values.insert(key.clone(), json!(display_value));
397            }
398        }
399        let output = json!({
400            "config_path": config_path.display().to_string(),
401            "values": values,
402        });
403        println!("{}", serde_json::to_string_pretty(&output)?);
404    } else {
405        println!("Config file: {}", config_path.display());
406        println!();
407
408        let mut has_values = false;
409        for (key, description) in KNOWN_KEYS {
410            if let Some(Some(value)) = map.get(*key) {
411                has_values = true;
412                let display_value = if !args.show_values && PersistentConfig::is_sensitive(key) {
413                    PersistentConfig::mask_value(value)
414                } else {
415                    value.clone()
416                };
417                println!("  {} = {}", key, display_value);
418                println!("    {}", description);
419            }
420        }
421
422        // Show named memories
423        if !config.memory.is_empty() {
424            has_values = true;
425            println!();
426            println!("  [memory]");
427            for (name, id) in &config.memory {
428                println!("    {} = {}", name, id);
429            }
430        }
431
432        // Show any extra custom keys
433        for (key, value) in &config.extra {
434            has_values = true;
435            let display_value = if !args.show_values && PersistentConfig::is_sensitive(&key) {
436                PersistentConfig::mask_value(value)
437            } else {
438                value.clone()
439            };
440            println!("  {} = {}", key, display_value);
441        }
442
443        if !has_values {
444            println!("  (no configuration set)");
445            println!();
446            println!("Available keys:");
447            for (key, description) in KNOWN_KEYS {
448                println!("  {} - {}", key, description);
449            }
450        }
451
452        if !args.show_values {
453            println!();
454            println!("Use --show-values to display full values");
455        }
456    }
457
458    Ok(())
459}
460
461fn handle_config_unset(args: ConfigUnsetArgs) -> Result<()> {
462    let mut config = PersistentConfig::load()?;
463    let was_set = config.unset(&args.key);
464    config.save()?;
465
466    if args.json {
467        let output = json!({
468            "success": true,
469            "key": args.key,
470            "was_set": was_set,
471            "message": if was_set {
472                format!("Configuration '{}' has been removed", args.key)
473            } else {
474                format!("Configuration '{}' was not set", args.key)
475            },
476        });
477        println!("{}", serde_json::to_string_pretty(&output)?);
478    } else {
479        if was_set {
480            println!("Removed '{}'", args.key);
481        } else {
482            println!("'{}' was not set", args.key);
483        }
484    }
485
486    Ok(())
487}
488
489fn handle_config_check(args: ConfigCheckArgs) -> Result<()> {
490    let config = PersistentConfig::load()?;
491    let config_path = PersistentConfig::config_path()?;
492
493    // Also check environment variables
494    let env_api_key = std::env::var("MEMVID_API_KEY").ok();
495    let env_api_url = std::env::var("MEMVID_API_URL").ok();
496
497    // Determine effective values (env takes precedence)
498    let effective_api_key = env_api_key.clone().or(config.api_key.clone());
499    let effective_api_url = env_api_url
500        .clone()
501        .or(config.api_url.clone())
502        .unwrap_or_else(|| "https://memvid.com".to_string());
503
504    let has_api_key = effective_api_key.is_some();
505    let api_key_source = if env_api_key.is_some() {
506        "environment"
507    } else if config.api_key.is_some() {
508        "config file"
509    } else {
510        "not set"
511    };
512
513    let api_url_source = if env_api_url.is_some() {
514        "environment"
515    } else if config.api_url.is_some() {
516        "config file"
517    } else {
518        "default"
519    };
520
521    // TODO: Actually verify the API key with the server
522    // For now, just check if it's set
523    let api_key_valid = has_api_key; // Placeholder - should verify with server
524
525    if args.json {
526        let output = json!({
527            "config_path": config_path.display().to_string(),
528            "api_key": {
529                "set": has_api_key,
530                "source": api_key_source,
531                "valid": api_key_valid,
532            },
533            "api_url": {
534                "value": effective_api_url,
535                "source": api_url_source,
536            },
537            "ready": has_api_key && api_key_valid,
538        });
539        println!("{}", serde_json::to_string_pretty(&output)?);
540    } else {
541        println!("Configuration Check");
542        println!("===================");
543        println!();
544        println!("Config file: {}", config_path.display());
545        println!();
546
547        // API Key status
548        if has_api_key {
549            println!(
550                "API Key: {} (source: {})",
551                if api_key_valid { "valid" } else { "invalid" },
552                api_key_source
553            );
554            if let Some(key) = &effective_api_key {
555                println!("  Value: {}", PersistentConfig::mask_value(key));
556            }
557        } else {
558            println!("API Key: not configured");
559            println!();
560            println!("To set your API key:");
561            println!("  memvid config set api_key <your-key>");
562            println!("  OR");
563            println!("  export MEMVID_API_KEY=<your-key>");
564            println!();
565            println!("Get your API key at: https://memvid.com/dashboard/api-keys");
566        }
567
568        println!();
569        println!(
570            "API URL: {} (source: {})",
571            effective_api_url, api_url_source
572        );
573
574        println!();
575        if has_api_key && api_key_valid {
576            println!("Status: Ready to use");
577        } else {
578            println!("Status: API key required");
579        }
580    }
581
582    Ok(())
583}