1use 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#[derive(Subcommand)]
15pub enum ConfigCommand {
16 Set(ConfigSetArgs),
18 Get(ConfigGetArgs),
20 List(ConfigListArgs),
22 Unset(ConfigUnsetArgs),
24 Check(ConfigCheckArgs),
26}
27
28#[derive(Args)]
30pub struct ConfigArgs {
31 #[command(subcommand)]
32 pub command: ConfigCommand,
33}
34
35#[derive(Args)]
37pub struct ConfigSetArgs {
38 pub key: String,
40 pub value: String,
42 #[arg(long)]
44 pub json: bool,
45}
46
47#[derive(Args)]
49pub struct ConfigGetArgs {
50 pub key: String,
52 #[arg(long)]
54 pub json: bool,
55}
56
57#[derive(Args)]
59pub struct ConfigListArgs {
60 #[arg(long)]
62 pub json: bool,
63 #[arg(long)]
65 pub show_values: bool,
66}
67
68#[derive(Args)]
70pub struct ConfigUnsetArgs {
71 pub key: String,
73 #[arg(long)]
75 pub json: bool,
76}
77
78#[derive(Args)]
80pub struct ConfigCheckArgs {
81 #[arg(long)]
83 pub json: bool,
84}
85
86#[derive(Debug, Clone, Default, Serialize, Deserialize)]
88pub struct PersistentConfig {
89 pub api_key: Option<String>,
91 pub api_url: Option<String>,
93 pub dashboard_url: Option<String>,
95 pub memory_id: Option<String>,
97 #[serde(default)]
99 pub memory: HashMap<String, String>,
100 pub default_embedding_provider: Option<String>,
102 pub default_llm_provider: Option<String>,
104 #[serde(flatten)]
106 pub extra: HashMap<String, String>,
107}
108
109impl PersistentConfig {
110 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 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 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 pub fn get(&self, key: &str) -> Option<String> {
147 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 pub fn set(&mut self, key: &str, value: String) {
165 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 pub fn unset(&mut self, key: &str) -> bool {
186 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 pub fn default_memory_id(&self) -> Option<String> {
228 self.memory.get("default").cloned().or_else(|| self.memory_id.clone())
229 }
230
231 pub fn get_memory(&self, name: &str) -> Option<String> {
233 self.memory.get(name).cloned()
234 }
235
236 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 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 pub fn is_sensitive(key: &str) -> bool {
255 key.contains("key") || key.contains("secret") || key.contains("token") || key.contains("password")
256 }
257
258 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
268const 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 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 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 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 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 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 let api_key_valid = has_api_key; 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 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}