use anyhow::{anyhow, Context, Result};
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
use crate::paths;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkerConfig {
pub monitor_dir: PathBuf,
#[serde(default = "default_db_path")]
pub db_path: PathBuf,
#[serde(default)]
pub supabase_url: Option<String>,
#[serde(default)]
pub supabase_key: Option<String>,
#[serde(default = "default_sync_interval")]
pub sync_interval_secs: u64,
#[serde(default = "default_debounce_ms")]
pub debounce_ms: u64,
#[serde(default)]
pub mem0_api_key: Option<String>,
#[serde(default)]
pub mem0_user_id: Option<String>,
#[serde(default = "default_true")]
pub advice_enabled: bool,
#[serde(default = "default_advice_idle")]
pub advice_idle_seconds: u64,
#[serde(default = "default_advice_min_batch")]
pub advice_min_batch: usize,
#[serde(default = "default_advice_max_per_hour")]
pub advice_max_per_hour: u32,
#[serde(default = "default_mem0_max_writes_per_hour")]
pub mem0_max_writes_per_hour: u32,
#[serde(default = "default_mem0_confidence_threshold")]
pub mem0_confidence_threshold: f32,
#[serde(default = "default_claude_bin")]
pub claude_bin: String,
#[serde(default)]
pub client_id: Option<String>,
#[serde(default = "default_advice_locale")]
pub advice_locale: String,
}
fn default_advice_locale() -> String {
"ko".to_string()
}
fn default_db_path() -> PathBuf {
paths::worker_db_file().unwrap_or_else(|_| PathBuf::from("worker.db"))
}
fn default_sync_interval() -> u64 {
30
}
fn default_debounce_ms() -> u64 {
500
}
fn default_true() -> bool {
true
}
fn default_advice_idle() -> u64 {
60
}
fn default_advice_min_batch() -> usize {
3
}
fn default_advice_max_per_hour() -> u32 {
6
}
fn default_mem0_max_writes_per_hour() -> u32 {
20
}
fn default_mem0_confidence_threshold() -> f32 {
0.7
}
fn default_claude_bin() -> String {
"claude".to_string()
}
impl WorkerConfig {
pub fn load() -> Result<Self> {
let path = paths::worker_config_file()?;
if !path.exists() {
return Err(anyhow!(
"Worker config not found at {}. Run `devist worker start` to set up.",
path.display()
));
}
let text = fs::read_to_string(&path).with_context(|| format!("read {}", path.display()))?;
let cfg: WorkerConfig =
toml::from_str(&text).with_context(|| format!("parse {}", path.display()))?;
Ok(cfg)
}
pub fn save(&self) -> Result<()> {
let dir = paths::worker_dir()?;
fs::create_dir_all(&dir)?;
let path = paths::worker_config_file()?;
let text = toml::to_string_pretty(self)?;
fs::write(&path, text).with_context(|| format!("write {}", path.display()))?;
Ok(())
}
pub fn exists() -> bool {
paths::worker_config_file()
.map(|p| p.exists())
.unwrap_or(false)
}
pub fn new_default(monitor_dir: PathBuf) -> Result<Self> {
Ok(WorkerConfig {
monitor_dir,
db_path: paths::worker_db_file()?,
supabase_url: None,
supabase_key: None,
sync_interval_secs: default_sync_interval(),
debounce_ms: default_debounce_ms(),
mem0_api_key: None,
mem0_user_id: hostname(),
advice_enabled: default_true(),
advice_idle_seconds: default_advice_idle(),
advice_min_batch: default_advice_min_batch(),
advice_max_per_hour: default_advice_max_per_hour(),
mem0_max_writes_per_hour: default_mem0_max_writes_per_hour(),
mem0_confidence_threshold: default_mem0_confidence_threshold(),
claude_bin: default_claude_bin(),
client_id: hostname(),
advice_locale: default_advice_locale(),
})
}
pub fn set_key(&mut self, key: &str, value: &str) -> Result<()> {
match key {
"monitor_dir" => self.monitor_dir = PathBuf::from(value),
"supabase_url" => self.supabase_url = Some(value.to_string()),
"supabase_key" => self.supabase_key = Some(value.to_string()),
"sync_interval_secs" => {
self.sync_interval_secs = value
.parse()
.context("sync_interval_secs must be a number")?
}
"debounce_ms" => {
self.debounce_ms = value.parse().context("debounce_ms must be a number")?
}
"mem0_api_key" => self.mem0_api_key = Some(value.to_string()),
"mem0_user_id" => self.mem0_user_id = Some(value.to_string()),
"advice_enabled" => {
self.advice_enabled = value.parse().context("advice_enabled must be true/false")?
}
"advice_idle_seconds" => {
self.advice_idle_seconds = value.parse().context("must be a number")?
}
"advice_min_batch" => {
self.advice_min_batch = value.parse().context("must be a number")?
}
"advice_max_per_hour" => {
self.advice_max_per_hour = value.parse().context("must be a number")?
}
"mem0_max_writes_per_hour" => {
self.mem0_max_writes_per_hour = value.parse().context("must be a number")?
}
"mem0_confidence_threshold" => {
self.mem0_confidence_threshold =
value.parse().context("must be a number 0.0..=1.0")?
}
"claude_bin" => self.claude_bin = value.to_string(),
"client_id" => self.client_id = Some(value.to_string()),
"advice_locale" => self.advice_locale = value.to_string(),
_ => return Err(anyhow!("Unknown config key: {}", key)),
}
Ok(())
}
}
fn hostname() -> Option<String> {
std::process::Command::new("hostname")
.output()
.ok()
.and_then(|o| String::from_utf8(o.stdout).ok())
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
}