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).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 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
229 .get("default")
230 .cloned()
231 .or_else(|| self.memory_id.clone())
232 }
233
234 pub fn get_memory(&self, name: &str) -> Option<String> {
236 self.memory.get(name).cloned()
237 }
238
239 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 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 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 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
280const 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 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 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 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 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 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 let api_key_valid = has_api_key; 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 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}