use super::runtime::RuntimeConfig;
use super::schema_openclaw::OpenClawConfig;
use super::schema_rsclaw::RsclawConfig;
use anyhow::{Context, Result};
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::Arc;
pub struct ConfigLoader;
impl ConfigLoader {
pub fn load() -> Result<RuntimeConfig> {
let rsclaw_path = Self::get_rsclaw_path()?;
let openclaw_path = Self::get_openclaw_path()?;
if rsclaw_path.exists() {
tracing::info!("Loading config from {:?}", rsclaw_path);
return Self::load_toml(&rsclaw_path);
}
if openclaw_path.exists() {
tracing::info!("Loading config from {:?}", openclaw_path);
return Self::load_openclaw(&openclaw_path);
}
tracing::info!("No config file found, using defaults");
Ok(RuntimeConfig::default())
}
fn get_rsclaw_path() -> Result<PathBuf> {
if let Ok(custom) = env::var("RSCLAW_CONFIG_PATH") {
return Ok(PathBuf::from(custom));
}
let home = dirs::home_dir().context("Cannot determine home directory")?;
Ok(home.join(".rsclaw").join("rsclaw.toml"))
}
fn get_openclaw_path() -> Result<PathBuf> {
if let Ok(custom) = env::var("OPENCLAW_CONFIG_PATH") {
return Ok(PathBuf::from(custom));
}
let home = dirs::home_dir().context("Cannot determine home directory")?;
Ok(home.join(".openclaw").join("openclaw.json"))
}
pub fn load_toml(path: &Path) -> Result<RuntimeConfig> {
let content = fs::read_to_string(path)
.with_context(|| format!("Failed to read config file: {:?}", path))?;
let processed = Self::process_env_vars(&content);
let config: RsclawConfig = toml::from_str(&processed)
.with_context(|| format!("Failed to parse TOML config: {:?}", path))?;
Ok(Self::rsclaw_to_runtime(config))
}
pub fn load_openclaw(path: &Path) -> Result<RuntimeConfig> {
let content = fs::read_to_string(path)
.with_context(|| format!("Failed to read config file: {:?}", path))?;
let processed = Self::process_includes(&content, path.parent().unwrap_or(Path::new(".")))?;
let processed = Self::process_env_vars(&processed);
let config: OpenClawConfig = serde_json::from_str(&processed)
.with_context(|| format!("Failed to parse OpenClaw config: {:?}", path))?;
Ok(Self::openclaw_to_runtime(config))
}
pub fn load_json5(path: &Path) -> Result<RuntimeConfig> {
Self::load_openclaw(path)
}
fn process_env_vars(content: &str) -> String {
let re = regex::Regex::new(r"\$\{([^}]+)\}").unwrap();
let mut result = content.to_string();
for cap in re.captures_iter(content) {
let var_name = &cap[1];
let replacement = env::var(var_name).unwrap_or_else(|_| {
tracing::warn!("Environment variable {} not defined", var_name);
cap[0].to_string()
});
result = result.replace(&cap[0], &replacement);
}
result
}
fn process_includes(content: &str, base_dir: &Path) -> Result<String> {
let re = regex::Regex::new(r#""\$include"\s*:\s*"([^"]+)""#).unwrap();
let mut result = content.to_string();
for cap in re.captures_iter(content) {
let include_path = &cap[1];
let full_path = base_dir.join(include_path);
let include_content = fs::read_to_string(&full_path)
.with_context(|| format!("Failed to include config file: {:?}", full_path))?;
result = result.replace(&cap[0], &include_content);
}
Ok(result)
}
fn to_arc(s: Option<String>) -> Option<Arc<str>> {
s.map(|s| Arc::from(s.as_str()))
}
fn openclaw_to_runtime(config: OpenClawConfig) -> RuntimeConfig {
let providers = config.get_providers();
let agents = config.get_agents();
RuntimeConfig {
meta: RuntimeConfig::default().meta,
gateway: super::runtime::GatewayConfig {
host: Arc::from(config.gateway_host().as_str()),
port: config.gateway_port(),
},
agents: super::runtime::AgentsConfig {
list: agents
.into_iter()
.map(|a| super::runtime::AgentEntry {
name: Arc::from(a.name.as_str()),
model: Self::to_arc(a.model),
system_prompt: Self::to_arc(a.system_prompt),
tools: a
.tools
.map(|t| t.into_iter().map(|s| Arc::from(s.as_str())).collect()),
channels: a
.channels
.map(|c| c.into_iter().map(|s| Arc::from(s.as_str())).collect()),
default: a.default,
max_tokens: a.max_tokens,
memory_limit_mb: a.memory_limit_mb,
})
.collect(),
},
models: super::runtime::ModelsConfig {
providers: providers
.into_iter()
.map(|p| super::runtime::ProviderConfig {
name: Arc::from(p.name.as_str()),
provider_type: Arc::from(p.provider_type.as_str()),
api_key: Self::to_arc(p.api_key),
base_url: Self::to_arc(p.base_url),
models: p
.models
.into_iter()
.map(|m| super::runtime::ModelEntry {
name: Arc::from(m.name.as_str()),
max_tokens: m.max_tokens,
supports_functions: m.supports_functions,
supports_vision: m.supports_vision,
})
.collect(),
})
.collect(),
primary_model: None,
},
..RuntimeConfig::default()
}
}
fn rsclaw_to_runtime(config: RsclawConfig) -> RuntimeConfig {
RuntimeConfig {
meta: config
.meta
.map(|m| super::runtime::MetaConfig {
name: Self::to_arc(m.name).unwrap_or_else(|| Arc::from("rsclaw")),
version: Self::to_arc(m.version)
.unwrap_or_else(|| Arc::from(env!("CARGO_PKG_VERSION"))),
description: Self::to_arc(m.description).unwrap_or_else(|| Arc::from("")),
})
.unwrap_or_default(),
gateway: config
.gateway
.map(|g| super::runtime::GatewayConfig {
host: Self::to_arc(g.host).unwrap_or_else(|| Arc::from("127.0.0.1")),
port: g.port.unwrap_or(8080),
})
.unwrap_or_default(),
agents: config
.agents
.map(|a| super::runtime::AgentsConfig {
list: a
.list
.into_iter()
.map(|agent| super::runtime::AgentEntry {
name: Arc::from(agent.name.as_str()),
model: Self::to_arc(agent.model),
system_prompt: Self::to_arc(agent.system_prompt),
tools: agent
.tools
.map(|t| t.into_iter().map(|s| Arc::from(s.as_str())).collect()),
channels: agent
.channels
.map(|c| c.into_iter().map(|s| Arc::from(s.as_str())).collect()),
default: agent.default,
max_tokens: agent.max_tokens,
memory_limit_mb: agent.memory_limit_mb,
})
.collect(),
})
.unwrap_or_default(),
models: config
.models
.map(|m| super::runtime::ModelsConfig {
providers: m
.providers
.into_iter()
.map(|p| super::runtime::ProviderConfig {
name: Arc::from(p.name.as_str()),
provider_type: Arc::from(p.provider_type.as_str()),
api_key: Self::to_arc(p.api_key),
base_url: Self::to_arc(p.base_url),
models: p
.models
.into_iter()
.map(|model| super::runtime::ModelEntry {
name: Arc::from(model.name.as_str()),
max_tokens: model.max_tokens,
supports_functions: model.supports_functions,
supports_vision: model.supports_vision,
})
.collect(),
})
.collect(),
primary_model: None,
})
.unwrap_or_default(),
auth: config
.auth
.map(|a| super::runtime::AuthConfig {
api_keys: a
.api_keys
.unwrap_or_default()
.into_iter()
.map(|(k, v)| (Arc::from(k.as_str()), Arc::from(v.as_str())))
.collect(),
})
.unwrap_or_default(),
channels: config
.channels
.map(|c| super::runtime::ChannelsConfig {
telegram: c.telegram.map(|ch| super::runtime::ChannelConfig {
enabled: ch.enabled.unwrap_or(false),
token: Self::to_arc(ch.token),
webhook_url: Self::to_arc(ch.webhook_url),
}),
discord: c.discord.map(|ch| super::runtime::ChannelConfig {
enabled: ch.enabled.unwrap_or(false),
token: Self::to_arc(ch.token),
webhook_url: Self::to_arc(ch.webhook_url),
}),
slack: c.slack.map(|ch| super::runtime::ChannelConfig {
enabled: ch.enabled.unwrap_or(false),
token: Self::to_arc(ch.token),
webhook_url: Self::to_arc(ch.webhook_url),
}),
whatsapp: c.whatsapp.map(|ch| super::runtime::ChannelConfig {
enabled: ch.enabled.unwrap_or(false),
token: Self::to_arc(ch.token),
webhook_url: Self::to_arc(ch.webhook_url),
}),
})
.unwrap_or_default(),
session: config
.session
.map(|s| super::runtime::SessionConfig {
timeout_minutes: s.timeout_minutes.unwrap_or(30),
max_history: s.max_history.unwrap_or(100),
})
.unwrap_or_default(),
bindings: config
.bindings
.unwrap_or_default()
.into_iter()
.map(|b| super::runtime::BindingRule {
channel: Self::to_arc(b.channel),
agent: Arc::from(b.agent.as_str()),
peer_id: Self::to_arc(b.peer_id),
group_id: Self::to_arc(b.group_id),
path: Self::to_arc(b.path),
priority: b.priority.unwrap_or(0),
})
.collect(),
cron: config
.cron
.map(|c| super::runtime::CronConfig {
enabled: c.enabled.unwrap_or(false),
})
.unwrap_or_default(),
tools: config
.tools
.map(|t| super::runtime::ToolsConfig {
enabled: t
.enabled
.unwrap_or_default()
.into_iter()
.map(|s| Arc::from(s.as_str()))
.collect(),
web_search: t.web_search.map(|ws| super::runtime::WebSearchConfig {
engine: Self::to_arc(ws.engine).unwrap_or_else(|| Arc::from("google")),
api_key: Self::to_arc(ws.api_key),
}),
})
.unwrap_or_default(),
sandbox: config
.sandbox
.map(|s| super::runtime::SandboxConfig {
enabled: s.enabled.unwrap_or(false),
timeout_seconds: s.timeout_seconds.unwrap_or(30),
})
.unwrap_or_default(),
skills: config
.skills
.map(|s| super::runtime::SkillsConfig {
paths: s
.paths
.unwrap_or_default()
.into_iter()
.map(PathBuf::from)
.collect(),
})
.unwrap_or_default(),
plugins: config
.plugins
.map(|p| super::runtime::PluginsConfig {
paths: p
.paths
.unwrap_or_default()
.into_iter()
.map(PathBuf::from)
.collect(),
})
.unwrap_or_default(),
hooks: config
.hooks
.map(|h| super::runtime::HooksConfig {
enabled: h.enabled.unwrap_or(false),
})
.unwrap_or_default(),
memory: config
.memory
.map(|m| super::runtime::MemoryConfig {
max_agent_memory_mb: m.max_agent_memory_mb.unwrap_or(512),
conversation_cache_size: m.conversation_cache_size.unwrap_or(100),
token_threshold: m.token_threshold.unwrap_or(1500),
max_concurrent_agents: m.max_concurrent_agents.unwrap_or(3),
})
.unwrap_or_default(),
}
}
pub fn init_default_config(path: &Path) -> Result<()> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("Failed to create config directory: {:?}", parent))?;
}
let default_config = RuntimeConfig::default();
let content = toml::to_string_pretty(&default_config)
.context("Failed to serialize default config")?;
fs::write(path, content)
.with_context(|| format!("Failed to write config file: {:?}", path))?;
Ok(())
}
pub fn save(config: &RuntimeConfig, path: &Path) -> Result<()> {
let content = toml::to_string_pretty(config).context("Failed to serialize config")?;
fs::write(path, content)
.with_context(|| format!("Failed to write config file: {:?}", path))?;
Ok(())
}
}