Skip to main content

totalreclaw_memory/
setup.rs

1//! First-use setup wizard for TotalReclaw.
2//!
3//! Handles:
4//! - Recovery phrase generation/import (saved to `~/.totalreclaw/credentials.json`)
5//! - Embedding mode selection (saved to `~/.totalreclaw/embedding-config.json`)
6
7use serde::{Deserialize, Serialize};
8use std::path::PathBuf;
9
10use crate::embedding::EmbeddingMode;
11use crate::Result;
12
13// ---------------------------------------------------------------------------
14// Config paths
15// ---------------------------------------------------------------------------
16
17/// Get the TotalReclaw config directory (~/.totalreclaw/).
18pub fn config_dir() -> PathBuf {
19    dirs::home_dir()
20        .unwrap_or_else(|| PathBuf::from("."))
21        .join(".totalreclaw")
22}
23
24/// Get the credentials file path.
25pub fn credentials_path() -> PathBuf {
26    config_dir().join("credentials.json")
27}
28
29/// Get the embedding config file path.
30pub fn embedding_config_path() -> PathBuf {
31    config_dir().join("embedding-config.json")
32}
33
34// ---------------------------------------------------------------------------
35// Credentials
36// ---------------------------------------------------------------------------
37
38/// Stored credentials.
39#[derive(Serialize, Deserialize)]
40pub struct Credentials {
41    pub recovery_phrase: String,
42}
43
44/// Load credentials from disk.
45pub fn load_credentials() -> Result<Option<Credentials>> {
46    let path = credentials_path();
47    if !path.exists() {
48        return Ok(None);
49    }
50    let data = std::fs::read_to_string(&path)?;
51    let creds: Credentials =
52        serde_json::from_str(&data).map_err(|e| crate::Error::Crypto(e.to_string()))?;
53    Ok(Some(creds))
54}
55
56/// Save credentials to disk.
57pub fn save_credentials(creds: &Credentials) -> Result<()> {
58    let dir = config_dir();
59    std::fs::create_dir_all(&dir)?;
60    let path = credentials_path();
61    let data = serde_json::to_string_pretty(creds)
62        .map_err(|e| crate::Error::Crypto(e.to_string()))?;
63    std::fs::write(&path, data)?;
64
65    // Restrict permissions on Unix
66    #[cfg(unix)]
67    {
68        use std::os::unix::fs::PermissionsExt;
69        std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600))?;
70    }
71
72    Ok(())
73}
74
75// ---------------------------------------------------------------------------
76// Embedding config
77// ---------------------------------------------------------------------------
78
79/// Persisted embedding configuration.
80#[derive(Serialize, Deserialize)]
81pub struct EmbeddingConfig {
82    pub mode: String,
83    pub model: Option<String>,
84    pub dimensions: usize,
85    pub ollama_url: Option<String>,
86    pub provider_url: Option<String>,
87    pub api_key_env: Option<String>,
88}
89
90/// Load embedding config from disk.
91pub fn load_embedding_config() -> Result<Option<EmbeddingConfig>> {
92    let path = embedding_config_path();
93    if !path.exists() {
94        return Ok(None);
95    }
96    let data = std::fs::read_to_string(&path)?;
97    let config: EmbeddingConfig =
98        serde_json::from_str(&data).map_err(|e| crate::Error::Embedding(e.to_string()))?;
99    Ok(Some(config))
100}
101
102/// Save embedding config to disk.
103pub fn save_embedding_config(config: &EmbeddingConfig) -> Result<()> {
104    let dir = config_dir();
105    std::fs::create_dir_all(&dir)?;
106    let path = embedding_config_path();
107    let data = serde_json::to_string_pretty(config)
108        .map_err(|e| crate::Error::Embedding(e.to_string()))?;
109    std::fs::write(&path, data)?;
110    Ok(())
111}
112
113/// Convert saved config to EmbeddingMode.
114pub fn config_to_mode(config: &EmbeddingConfig) -> EmbeddingMode {
115    match config.mode.as_str() {
116        "local" => EmbeddingMode::Local {
117            model_path: config
118                .model
119                .clone()
120                .unwrap_or_else(|| "onnx-community/harrier-oss-v1-270m-ONNX".into()),
121        },
122        "ollama" => EmbeddingMode::Ollama {
123            base_url: config
124                .ollama_url
125                .clone()
126                .unwrap_or_else(|| "http://localhost:11434".into()),
127            model: config
128                .model
129                .clone()
130                .unwrap_or_else(|| "nomic-embed-text".into()),
131        },
132        "zeroclaw" => EmbeddingMode::ZeroClaw {
133            base_url: config
134                .provider_url
135                .clone()
136                .unwrap_or_default(),
137            api_key: std::env::var(
138                config
139                    .api_key_env
140                    .as_deref()
141                    .unwrap_or("ZEROCLAW_EMBEDDING_API_KEY"),
142            )
143            .unwrap_or_default(),
144        },
145        "llm" | _ => EmbeddingMode::LlmProvider {
146            base_url: config
147                .provider_url
148                .clone()
149                .unwrap_or_else(|| "https://api.openai.com".into()),
150            api_key: std::env::var(
151                config
152                    .api_key_env
153                    .as_deref()
154                    .unwrap_or("OPENAI_API_KEY"),
155            )
156            .unwrap_or_default(),
157            model: config
158                .model
159                .clone()
160                .unwrap_or_else(|| "text-embedding-3-small".into()),
161        },
162    }
163}
164
165/// Generate a fresh BIP-39 mnemonic.
166pub fn generate_mnemonic() -> String {
167    // Generate 128 bits of entropy for a 12-word mnemonic
168    use rand::RngCore;
169    let mut entropy = [0u8; 16];
170    rand::thread_rng().fill_bytes(&mut entropy);
171    let mnemonic = bip39::Mnemonic::from_entropy(&entropy)
172        .expect("Failed to generate mnemonic from 16 bytes");
173    mnemonic.to_string()
174}
175
176// ---------------------------------------------------------------------------
177// Third-party crate for home directory
178// ---------------------------------------------------------------------------
179
180mod dirs {
181    use std::path::PathBuf;
182
183    pub fn home_dir() -> Option<PathBuf> {
184        std::env::var_os("HOME")
185            .or_else(|| std::env::var_os("USERPROFILE"))
186            .map(PathBuf::from)
187    }
188}