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 #[serde(default)]
48 pub messengers: Vec<MessengerConfig>,
49 pub use_secrets: bool,
51 #[serde(default)]
53 pub gateway_url: Option<String>,
54 #[serde(default)]
56 pub model: Option<ModelProvider>,
57 #[serde(default)]
60 pub secrets_password_protected: bool,
61 #[serde(default)]
63 pub totp_enabled: bool,
64 #[serde(default)]
66 pub agent_access: bool,
67 #[serde(default = "Config::default_agent_name")]
70 pub agent_name: String,
71 #[serde(default = "Config::default_message_spacing")]
74 pub message_spacing: u16,
75 #[serde(default = "Config::default_tab_width")]
78 pub tab_width: u16,
79 #[serde(default)]
81 pub sandbox: SandboxConfig,
82 #[serde(default)]
84 pub clawhub_url: Option<String>,
85 #[serde(default)]
87 pub clawhub_token: Option<String>,
88 #[serde(default)]
90 pub system_prompt: Option<String>,
91 #[serde(default)]
93 pub messenger_poll_interval_ms: Option<u32>,
94 #[serde(default)]
96 pub tool_permissions: HashMap<String, crate::tools::ToolPermission>,
97 #[serde(default)]
99 pub tls_cert: Option<PathBuf>,
100 #[serde(default)]
102 pub tls_key: Option<PathBuf>,
103 #[serde(default)]
105 pub memory_flush: MemoryFlushConfig,
106 #[serde(default)]
108 pub workspace_context: WorkspaceContextConfig,
109 #[serde(default)]
111 pub mnemo: Option<crate::mnemo::MnemoConfig>,
112}
113
114#[derive(Debug, Clone, Default, Serialize, Deserialize)]
116pub struct MessengerConfig {
117 #[serde(default)]
119 pub name: String,
120 #[serde(default)]
122 pub messenger_type: String,
123 #[serde(default = "default_true")]
125 pub enabled: bool,
126 #[serde(default)]
128 pub config_path: Option<PathBuf>,
129 #[serde(default)]
131 pub token: Option<String>,
132 #[serde(default)]
134 pub webhook_url: Option<String>,
135 #[serde(default)]
137 pub homeserver: Option<String>,
138 #[serde(default)]
140 pub user_id: Option<String>,
141 #[serde(default)]
143 pub password: Option<String>,
144 #[serde(default)]
146 pub access_token: Option<String>,
147 #[serde(default)]
149 pub phone: Option<String>,
150 #[serde(default)]
152 pub allowed_chats: Vec<String>,
153 #[serde(default)]
155 pub allowed_users: Vec<String>,
156
157 #[serde(default)]
160 pub app_token: Option<String>,
161 #[serde(default)]
163 pub default_channel: Option<String>,
164
165 #[serde(default)]
168 pub server: Option<String>,
169 #[serde(default)]
171 pub port: Option<u16>,
172 #[serde(default)]
174 pub nick: Option<String>,
175 #[serde(default)]
177 pub irc_channels: Vec<String>,
178 #[serde(default)]
180 pub use_tls: Option<bool>,
181
182 #[serde(default)]
185 pub credentials_path: Option<String>,
186 #[serde(default)]
188 pub spaces: Vec<String>,
189
190 #[serde(default)]
193 pub app_id: Option<String>,
194 #[serde(default)]
196 pub app_password: Option<String>,
197
198 #[serde(default)]
201 pub pairing_code: Option<String>,
202 #[serde(default)]
204 pub require_pairing: bool,
205 #[serde(default)]
207 pub paired_users: Vec<String>,
208
209 #[serde(default)]
212 pub dm: Option<DmConfig>,
213}
214
215#[derive(Debug, Clone, Default, Serialize, Deserialize)]
217pub struct DmConfig {
218 #[serde(default)]
220 pub enabled: bool,
221 #[serde(default)]
223 pub policy: Option<String>,
224 #[serde(default)]
226 pub allow_from: Vec<String>,
227}
228
229fn default_true() -> bool {
230 true
231}
232
233impl Default for Config {
234 fn default() -> Self {
235 let home_dir = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
236 Self {
237 settings_dir: home_dir.join(".rustyclaw"),
238 soul_path: None,
239 skills_dir: None,
240 workspace_dir: None,
241 credentials_dir: None,
242 messengers: Vec::new(),
243 use_secrets: true,
244 gateway_url: None,
245 model: None,
246 secrets_password_protected: false,
247 totp_enabled: false,
248 agent_access: false,
249 agent_name: Self::default_agent_name(),
250 message_spacing: Self::default_message_spacing(),
251 tab_width: Self::default_tab_width(),
252 sandbox: SandboxConfig::default(),
253 clawhub_url: None,
254 clawhub_token: None,
255 system_prompt: None,
256 messenger_poll_interval_ms: None,
257 tool_permissions: HashMap::new(),
258 tls_cert: None,
259 tls_key: None,
260 memory_flush: MemoryFlushConfig::default(),
261 workspace_context: WorkspaceContextConfig::default(),
262 mnemo: None,
263 }
264 }
265}
266
267impl Config {
268 fn default_agent_name() -> String {
269 "RustyClaw".to_string()
270 }
271
272 fn default_message_spacing() -> u16 {
273 1
274 }
275
276 fn default_tab_width() -> u16 {
277 5
278 }
279
280 pub fn workspace_dir(&self) -> PathBuf {
285 self.workspace_dir
286 .clone()
287 .unwrap_or_else(|| self.settings_dir.join("workspace"))
288 }
289
290 pub fn credentials_dir(&self) -> PathBuf {
293 self.credentials_dir
294 .clone()
295 .unwrap_or_else(|| self.settings_dir.join("credentials"))
296 }
297
298 pub fn agent_dir(&self) -> PathBuf {
301 self.settings_dir.join("agents").join("main")
302 }
303
304 pub fn sessions_dir(&self) -> PathBuf {
306 self.agent_dir().join("sessions")
307 }
308
309 pub fn soul_path(&self) -> PathBuf {
311 self.soul_path
312 .clone()
313 .unwrap_or_else(|| self.workspace_dir().join("SOUL.md"))
314 }
315
316 pub fn skills_dir(&self) -> PathBuf {
318 self.skills_dir
319 .clone()
320 .unwrap_or_else(|| self.workspace_dir().join("skills"))
321 }
322
323 pub fn skills_dirs(&self) -> Vec<PathBuf> {
326 let mut dirs = Vec::new();
327
328 let openclaw_bundled = PathBuf::from("/usr/lib/node_modules/openclaw/skills");
330 if openclaw_bundled.exists() {
331 dirs.push(openclaw_bundled);
332 }
333
334 if let Some(home) = dirs::home_dir() {
336 let openclaw_user = home.join(".openclaw/workspace/skills");
337 if openclaw_user.exists() {
338 dirs.push(openclaw_user);
339 }
340 }
341
342 dirs.push(self.skills_dir());
344
345 dirs
346 }
347
348 pub fn logs_dir(&self) -> PathBuf {
350 self.settings_dir.join("logs")
351 }
352
353 pub fn ensure_dirs(&self) -> Result<()> {
355 let dirs = [
356 self.settings_dir.clone(),
357 self.workspace_dir(),
358 self.credentials_dir(),
359 self.agent_dir(),
360 self.sessions_dir(),
361 self.skills_dir(),
362 self.logs_dir(),
363 ];
364 for d in &dirs {
365 std::fs::create_dir_all(d)?;
366 }
367 Ok(())
368 }
369
370 pub fn load(path: Option<PathBuf>) -> Result<Self> {
374 let config_path = if let Some(p) = path {
375 p
376 } else {
377 let home_dir = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
378 home_dir.join(".rustyclaw").join("config.toml")
379 };
380
381 if config_path.exists() {
382 let content = std::fs::read_to_string(&config_path)?;
383 let config: Config = match toml::from_str(&content) {
384 Ok(c) => c,
385 Err(e) => {
386 eprintln!("ERROR: Failed to parse config: {}", e);
387 return Err(e.into());
388 }
389 };
390 let mut config = config;
391 config.migrate_legacy_layout()?;
393 Ok(config)
394 } else {
395 Ok(Config::default())
396 }
397 }
398
399 pub fn save(&self, path: Option<PathBuf>) -> Result<()> {
401 let config_path = if let Some(p) = path {
402 p
403 } else {
404 self.settings_dir.join("config.toml")
405 };
406
407 if let Some(parent) = config_path.parent() {
408 std::fs::create_dir_all(parent)?;
409 }
410
411 let content = toml::to_string_pretty(self)?;
412 std::fs::write(&config_path, content)?;
413 Ok(())
414 }
415
416 fn migrate_legacy_layout(&mut self) -> Result<()> {
421 let old_secrets = self.settings_dir.join("secrets.json");
422 let old_key = self.settings_dir.join("secrets.key");
423 let old_soul = self.settings_dir.join("SOUL.md");
424 let old_skills = self.settings_dir.join("skills");
425
426 let new_creds = self.credentials_dir();
429 let new_workspace = self.workspace_dir();
430
431 let has_legacy = old_secrets.exists() || old_soul.exists();
432 let already_migrated =
433 new_creds.join("secrets.json").exists() || new_workspace.join("SOUL.md").exists();
434
435 if !has_legacy || already_migrated {
436 return Ok(());
437 }
438
439 eprintln!("Migrating ~/.rustyclaw to new directory layout…");
440
441 std::fs::create_dir_all(&new_creds)?;
443 std::fs::create_dir_all(&new_workspace)?;
444
445 if old_secrets.exists() {
447 let dest = new_creds.join("secrets.json");
448 std::fs::rename(&old_secrets, &dest)?;
449 eprintln!(" secrets.json → credentials/secrets.json");
450 }
451 if old_key.exists() {
452 let dest = new_creds.join("secrets.key");
453 std::fs::rename(&old_key, &dest)?;
454 eprintln!(" secrets.key → credentials/secrets.key");
455 }
456
457 if old_soul.exists() {
459 let dest = new_workspace.join("SOUL.md");
460 std::fs::rename(&old_soul, &dest)?;
461 eprintln!(" SOUL.md → workspace/SOUL.md");
462 }
463
464 if old_skills.exists() && old_skills.is_dir() {
466 let dest = new_workspace.join("skills");
467 if !dest.exists() {
468 std::fs::rename(&old_skills, &dest)?;
469 eprintln!(" skills/ → workspace/skills/");
470 }
471 }
472
473 if self.soul_path.as_ref() == Some(&self.settings_dir.join("SOUL.md")) {
475 self.soul_path = None; }
477 if self.skills_dir.as_ref() == Some(&self.settings_dir.join("skills")) {
478 self.skills_dir = None;
479 }
480
481 self.save(None)?;
483
484 eprintln!("Migration complete.");
485 Ok(())
486 }
487}