Skip to main content

walrus_daemon/
config.rs

1//! Daemon configuration loaded from TOML.
2
3use anyhow::{Context, Result};
4use compact_str::CompactString;
5pub use model::{ProviderConfig, ProviderManager};
6use serde::{Deserialize, Serialize};
7use std::path::{Path, PathBuf};
8
9/// Agents subdirectory (contains *.md files).
10pub const AGENTS_DIR: &str = "agents";
11/// Skills subdirectory.
12pub const SKILLS_DIR: &str = "skills";
13/// Cron subdirectory (contains *.md files).
14pub const CRON_DIR: &str = "cron";
15/// Data subdirectory.
16pub const DATA_DIR: &str = "data";
17/// SQLite memory database filename.
18pub const MEMORY_DB: &str = "memory.db";
19
20/// Resolve the global configuration directory (`~/.walrus/`).
21pub fn global_config_dir() -> PathBuf {
22    dirs::home_dir().expect("no home directory").join(".walrus")
23}
24
25/// Pinned socket path (`~/.walrus/walrus.sock`).
26pub fn socket_path() -> PathBuf {
27    global_config_dir().join("walrus.sock")
28}
29
30/// Top-level daemon configuration.
31#[derive(Debug, Serialize, Deserialize)]
32pub struct DaemonConfig {
33    /// LLM provider configurations (`[[models]]` array).
34    pub models: Vec<ProviderConfig>,
35    /// Channel configurations.
36    #[serde(default)]
37    pub channels: Vec<ChannelConfig>,
38    /// MCP server configurations.
39    #[serde(default)]
40    pub mcp_servers: Vec<McpServerConfig>,
41}
42
43impl Default for DaemonConfig {
44    fn default() -> Self {
45        Self {
46            models: vec![ProviderConfig {
47                model: "deepseek-chat".into(),
48                api_key: Some("${DEEPSEEK_API_KEY}".to_owned()),
49                base_url: None,
50                loader: None,
51                quantization: None,
52                chat_template: None,
53            }],
54            channels: Vec::new(),
55            mcp_servers: Vec::new(),
56        }
57    }
58}
59
60/// Channel configuration.
61#[derive(Debug, Serialize, Deserialize)]
62pub struct ChannelConfig {
63    /// Platform name.
64    pub platform: CompactString,
65    /// Bot token (supports `${ENV_VAR}` expansion).
66    pub bot_token: String,
67    /// Default agent for this channel.
68    pub agent: CompactString,
69    /// Optional specific channel ID for exact routing.
70    pub channel_id: Option<CompactString>,
71}
72
73/// MCP server configuration.
74#[derive(Debug, Serialize, Deserialize)]
75pub struct McpServerConfig {
76    /// Server name.
77    pub name: CompactString,
78    /// Command to spawn.
79    pub command: String,
80    /// Command arguments.
81    #[serde(default)]
82    pub args: Vec<String>,
83    /// Environment variables.
84    #[serde(default)]
85    pub env: std::collections::BTreeMap<String, String>,
86    /// Auto-restart on failure.
87    #[serde(default = "default_true")]
88    pub auto_restart: bool,
89}
90
91fn default_true() -> bool {
92    true
93}
94
95/// Default agent markdown content for first-run scaffold.
96pub const DEFAULT_AGENT_MD: &str = r#"---
97name: assistant
98description: A helpful assistant
99tools:
100  - remember
101---
102
103You are a helpful assistant. Be concise.
104"#;
105
106impl DaemonConfig {
107    /// Parse a TOML string into a `DaemonConfig`, expanding environment
108    /// variables in supported fields.
109    pub fn from_toml(toml_str: &str) -> anyhow::Result<Self> {
110        let expanded = crate::utils::expand_env_vars(toml_str);
111        let config: Self = toml::from_str(&expanded)?;
112        Ok(config)
113    }
114
115    /// Load configuration from a file path.
116    pub fn load(path: &std::path::Path) -> anyhow::Result<Self> {
117        let content = std::fs::read_to_string(path)?;
118        Self::from_toml(&content)
119    }
120}
121
122/// Scaffold the full config directory structure on first run.
123///
124/// Creates subdirectories (agents, skills, cron, data), writes a default
125/// walrus.toml and a default assistant agent markdown file.
126pub fn scaffold_config_dir(config_dir: &Path) -> Result<()> {
127    std::fs::create_dir_all(config_dir.join(AGENTS_DIR))
128        .context("failed to create agents directory")?;
129    std::fs::create_dir_all(config_dir.join(SKILLS_DIR))
130        .context("failed to create skills directory")?;
131    std::fs::create_dir_all(config_dir.join(CRON_DIR))
132        .context("failed to create cron directory")?;
133    std::fs::create_dir_all(config_dir.join(DATA_DIR))
134        .context("failed to create data directory")?;
135
136    let gateway_toml = config_dir.join("walrus.toml");
137    let contents = toml::to_string_pretty(&DaemonConfig::default())
138        .context("failed to serialize default config")?;
139    std::fs::write(&gateway_toml, contents)
140        .with_context(|| format!("failed to write {}", gateway_toml.display()))?;
141
142    let agent_path = config_dir.join(AGENTS_DIR).join("assistant.md");
143    std::fs::write(&agent_path, DEFAULT_AGENT_MD)
144        .with_context(|| format!("failed to write {}", agent_path.display()))?;
145
146    Ok(())
147}