1use anyhow::{Context, Result};
4use compact_str::CompactString;
5pub use model::{ProviderConfig, ProviderManager};
6use serde::{Deserialize, Serialize};
7use std::path::{Path, PathBuf};
8
9pub const AGENTS_DIR: &str = "agents";
11pub const SKILLS_DIR: &str = "skills";
13pub const CRON_DIR: &str = "cron";
15pub const DATA_DIR: &str = "data";
17pub const MEMORY_DB: &str = "memory.db";
19
20pub fn global_config_dir() -> PathBuf {
22 dirs::home_dir().expect("no home directory").join(".walrus")
23}
24
25pub fn socket_path() -> PathBuf {
27 global_config_dir().join("walrus.sock")
28}
29
30#[derive(Debug, Serialize, Deserialize)]
32pub struct DaemonConfig {
33 pub models: Vec<ProviderConfig>,
35 #[serde(default)]
37 pub channels: Vec<ChannelConfig>,
38 #[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#[derive(Debug, Serialize, Deserialize)]
62pub struct ChannelConfig {
63 pub platform: CompactString,
65 pub bot_token: String,
67 pub agent: CompactString,
69 pub channel_id: Option<CompactString>,
71}
72
73#[derive(Debug, Serialize, Deserialize)]
75pub struct McpServerConfig {
76 pub name: CompactString,
78 pub command: String,
80 #[serde(default)]
82 pub args: Vec<String>,
83 #[serde(default)]
85 pub env: std::collections::BTreeMap<String, String>,
86 #[serde(default = "default_true")]
88 pub auto_restart: bool,
89}
90
91fn default_true() -> bool {
92 true
93}
94
95pub 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 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 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
122pub 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}