1use anyhow::Result;
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::path::PathBuf;
5
6use crate::memory_flush::MemoryFlushConfig;
7use crate::workspace_context::WorkspaceContextConfig;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct ModelProvider {
11 pub provider: String,
13 pub model: Option<String>,
15 pub base_url: Option<String>,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize, Default)]
21pub struct SandboxConfig {
22 #[serde(default)]
24 pub mode: String,
25 #[serde(default)]
27 pub deny_paths: Vec<PathBuf>,
28 #[serde(default)]
30 pub allow_paths: Vec<PathBuf>,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct Config {
35 pub settings_dir: PathBuf,
38 pub soul_path: Option<PathBuf>,
40 pub skills_dir: Option<PathBuf>,
42 pub workspace_dir: Option<PathBuf>,
44 pub credentials_dir: Option<PathBuf>,
46 pub messengers: Vec<MessengerConfig>,
48 pub use_secrets: bool,
50 #[serde(default)]
52 pub gateway_url: Option<String>,
53 #[serde(default)]
55 pub model: Option<ModelProvider>,
56 #[serde(default)]
59 pub secrets_password_protected: bool,
60 #[serde(default)]
62 pub totp_enabled: bool,
63 #[serde(default)]
65 pub agent_access: bool,
66 #[serde(default = "Config::default_agent_name")]
69 pub agent_name: String,
70 #[serde(default = "Config::default_message_spacing")]
73 pub message_spacing: u16,
74 #[serde(default = "Config::default_tab_width")]
77 pub tab_width: u16,
78 #[serde(default)]
80 pub sandbox: SandboxConfig,
81 #[serde(default)]
83 pub clawhub_url: Option<String>,
84 #[serde(default)]
86 pub clawhub_token: Option<String>,
87 #[serde(default)]
89 pub system_prompt: Option<String>,
90 #[serde(default)]
92 pub messenger_poll_interval_ms: Option<u32>,
93 #[serde(default)]
95 pub tool_permissions: HashMap<String, crate::tools::ToolPermission>,
96 #[serde(default)]
98 pub tls_cert: Option<PathBuf>,
99 #[serde(default)]
101 pub tls_key: Option<PathBuf>,
102 #[serde(default)]
104 pub memory_flush: MemoryFlushConfig,
105 #[serde(default)]
107 pub workspace_context: WorkspaceContextConfig,
108}
109
110#[derive(Debug, Clone, Default, Serialize, Deserialize)]
112pub struct MessengerConfig {
113 #[serde(default)]
115 pub name: String,
116 #[serde(default)]
118 pub messenger_type: String,
119 #[serde(default = "default_true")]
121 pub enabled: bool,
122 #[serde(default)]
124 pub config_path: Option<PathBuf>,
125 #[serde(default)]
127 pub token: Option<String>,
128 #[serde(default)]
130 pub webhook_url: Option<String>,
131 #[serde(default)]
133 pub homeserver: Option<String>,
134 #[serde(default)]
136 pub user_id: Option<String>,
137 #[serde(default)]
139 pub password: Option<String>,
140 #[serde(default)]
142 pub access_token: Option<String>,
143 #[serde(default)]
145 pub phone: Option<String>,
146 #[serde(default)]
148 pub allowed_chats: Vec<String>,
149 #[serde(default)]
151 pub allowed_users: Vec<String>,
152}
153
154fn default_true() -> bool {
155 true
156}
157
158impl Default for Config {
159 fn default() -> Self {
160 let home_dir = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
161 Self {
162 settings_dir: home_dir.join(".rustyclaw"),
163 soul_path: None,
164 skills_dir: None,
165 workspace_dir: None,
166 credentials_dir: None,
167 messengers: Vec::new(),
168 use_secrets: true,
169 gateway_url: None,
170 model: None,
171 secrets_password_protected: false,
172 totp_enabled: false,
173 agent_access: false,
174 agent_name: Self::default_agent_name(),
175 message_spacing: Self::default_message_spacing(),
176 tab_width: Self::default_tab_width(),
177 sandbox: SandboxConfig::default(),
178 clawhub_url: None,
179 clawhub_token: None,
180 system_prompt: None,
181 messenger_poll_interval_ms: None,
182 tool_permissions: HashMap::new(),
183 tls_cert: None,
184 tls_key: None,
185 memory_flush: MemoryFlushConfig::default(),
186 workspace_context: WorkspaceContextConfig::default(),
187 }
188 }
189}
190
191impl Config {
192 fn default_agent_name() -> String {
193 "RustyClaw".to_string()
194 }
195
196 fn default_message_spacing() -> u16 {
197 1
198 }
199
200 fn default_tab_width() -> u16 {
201 5
202 }
203
204 pub fn workspace_dir(&self) -> PathBuf {
209 self.workspace_dir
210 .clone()
211 .unwrap_or_else(|| self.settings_dir.join("workspace"))
212 }
213
214 pub fn credentials_dir(&self) -> PathBuf {
217 self.credentials_dir
218 .clone()
219 .unwrap_or_else(|| self.settings_dir.join("credentials"))
220 }
221
222 pub fn agent_dir(&self) -> PathBuf {
225 self.settings_dir.join("agents").join("main")
226 }
227
228 pub fn sessions_dir(&self) -> PathBuf {
230 self.agent_dir().join("sessions")
231 }
232
233 pub fn soul_path(&self) -> PathBuf {
235 self.soul_path
236 .clone()
237 .unwrap_or_else(|| self.workspace_dir().join("SOUL.md"))
238 }
239
240 pub fn skills_dir(&self) -> PathBuf {
242 self.skills_dir
243 .clone()
244 .unwrap_or_else(|| self.workspace_dir().join("skills"))
245 }
246
247 pub fn skills_dirs(&self) -> Vec<PathBuf> {
250 let mut dirs = Vec::new();
251
252 let openclaw_bundled = PathBuf::from("/usr/lib/node_modules/openclaw/skills");
254 if openclaw_bundled.exists() {
255 dirs.push(openclaw_bundled);
256 }
257
258 if let Some(home) = dirs::home_dir() {
260 let openclaw_user = home.join(".openclaw/workspace/skills");
261 if openclaw_user.exists() {
262 dirs.push(openclaw_user);
263 }
264 }
265
266 dirs.push(self.skills_dir());
268
269 dirs
270 }
271
272 pub fn logs_dir(&self) -> PathBuf {
274 self.settings_dir.join("logs")
275 }
276
277 pub fn ensure_dirs(&self) -> Result<()> {
279 let dirs = [
280 self.settings_dir.clone(),
281 self.workspace_dir(),
282 self.credentials_dir(),
283 self.agent_dir(),
284 self.sessions_dir(),
285 self.skills_dir(),
286 self.logs_dir(),
287 ];
288 for d in &dirs {
289 std::fs::create_dir_all(d)?;
290 }
291 Ok(())
292 }
293
294 pub fn load(path: Option<PathBuf>) -> Result<Self> {
298 let config_path = if let Some(p) = path {
299 p
300 } else {
301 let home_dir = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
302 home_dir.join(".rustyclaw").join("config.toml")
303 };
304
305 if config_path.exists() {
306 let content = std::fs::read_to_string(&config_path)?;
307 let mut config: Config = toml::from_str(&content)?;
308 config.migrate_legacy_layout()?;
310 Ok(config)
311 } else {
312 Ok(Config::default())
313 }
314 }
315
316 pub fn save(&self, path: Option<PathBuf>) -> Result<()> {
318 let config_path = if let Some(p) = path {
319 p
320 } else {
321 self.settings_dir.join("config.toml")
322 };
323
324 if let Some(parent) = config_path.parent() {
325 std::fs::create_dir_all(parent)?;
326 }
327
328 let content = toml::to_string_pretty(self)?;
329 std::fs::write(&config_path, content)?;
330 Ok(())
331 }
332
333 fn migrate_legacy_layout(&mut self) -> Result<()> {
338 let old_secrets = self.settings_dir.join("secrets.json");
339 let old_key = self.settings_dir.join("secrets.key");
340 let old_soul = self.settings_dir.join("SOUL.md");
341 let old_skills = self.settings_dir.join("skills");
342
343 let new_creds = self.credentials_dir();
346 let new_workspace = self.workspace_dir();
347
348 let has_legacy = old_secrets.exists() || old_soul.exists();
349 let already_migrated =
350 new_creds.join("secrets.json").exists() || new_workspace.join("SOUL.md").exists();
351
352 if !has_legacy || already_migrated {
353 return Ok(());
354 }
355
356 eprintln!("Migrating ~/.rustyclaw to new directory layout…");
357
358 std::fs::create_dir_all(&new_creds)?;
360 std::fs::create_dir_all(&new_workspace)?;
361
362 if old_secrets.exists() {
364 let dest = new_creds.join("secrets.json");
365 std::fs::rename(&old_secrets, &dest)?;
366 eprintln!(" secrets.json → credentials/secrets.json");
367 }
368 if old_key.exists() {
369 let dest = new_creds.join("secrets.key");
370 std::fs::rename(&old_key, &dest)?;
371 eprintln!(" secrets.key → credentials/secrets.key");
372 }
373
374 if old_soul.exists() {
376 let dest = new_workspace.join("SOUL.md");
377 std::fs::rename(&old_soul, &dest)?;
378 eprintln!(" SOUL.md → workspace/SOUL.md");
379 }
380
381 if old_skills.exists() && old_skills.is_dir() {
383 let dest = new_workspace.join("skills");
384 if !dest.exists() {
385 std::fs::rename(&old_skills, &dest)?;
386 eprintln!(" skills/ → workspace/skills/");
387 }
388 }
389
390 if self.soul_path.as_ref() == Some(&self.settings_dir.join("SOUL.md")) {
392 self.soul_path = None; }
394 if self.skills_dir.as_ref() == Some(&self.settings_dir.join("skills")) {
395 self.skills_dir = None;
396 }
397
398 self.save(None)?;
400
401 eprintln!("Migration complete.");
402 Ok(())
403 }
404}