diff --git a/src/agent.rs b/src/agent.rs index aceed5f..69d7604 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -292,7 +292,7 @@ impl Drop for Agent { /// Load user/bot identity from shared config. fn load_user_context() -> String { - let path = crate::config::shared_config_path(); + let path = crate::config::config_path(); let config: serde_json::Value = std::fs::read_to_string(&path).ok() .and_then(|s| serde_json::from_str(&s).ok()) .unwrap_or_default(); diff --git a/src/config.rs b/src/config.rs index ee9d9c7..75118c5 100644 --- a/src/config.rs +++ b/src/config.rs @@ -11,21 +11,48 @@ pub fn config_dir() -> String { } } -/// Path to shared config: $cfg/ai.syui.log/config.json -pub fn shared_config_path() -> String { +/// Shared config: $cfg/ai.syui.log/config.json +pub fn config_path() -> String { format!("{}/ai.syui.log/config.json", config_dir()) } -/// Path to aigpt config (same content as shared, may be symlink) -pub fn gpt_config_path() -> String { - format!("{}/ai.syui.gpt/config.json", config_dir()) +/// Sessions dir: $cfg/ai.syui.log/sessions +pub fn sessions_dir() -> String { + format!("{}/ai.syui.log/sessions", config_dir()) } -/// Path to sessions dir: $cfg/ai.syui.gpt/sessions -pub fn sessions_dir() -> String { - format!("{}/ai.syui.gpt/sessions", config_dir()) +/// Load shared config as JSON Value. +pub fn load_config() -> serde_json::Value { + std::fs::read_to_string(config_path()).ok() + .and_then(|s| serde_json::from_str(&s).ok()) + .unwrap_or_default() } +/// Resolve bot data dir from config's bot.path field. +/// Falls back to $cfg/ai.syui.log/at +pub fn bot_data_dir() -> String { + let config = load_config(); + let path = config["bot"]["path"].as_str().unwrap_or(""); + let did = config["bot"]["did"].as_str().unwrap_or("did"); + let expanded = expand_tilde(path); + if expanded.is_empty() { + format!("{}/ai.syui.log/at/{did}", config_dir()) + } else { + format!("{expanded}/{did}") + } +} + +pub fn expand_tilde(path: &str) -> String { + if let Some(rest) = path.strip_prefix('~') { + if let Ok(home) = std::env::var("HOME") { + return format!("{home}{rest}"); + } + } + path.to_string() +} + +// ── Agent configs ────────────────────────────────────────── + #[derive(Clone, Deserialize)] pub struct AgentConfig { pub name: String, @@ -40,6 +67,11 @@ fn default_cwd() -> String { .unwrap_or_else(|_| ".".to_string()) } +pub struct LoadedConfig { + pub agents: Vec, + pub interval: Option, +} + /// Built-in presets. pub fn preset(name: &str) -> Option> { let cwd = default_cwd(); @@ -70,10 +102,18 @@ pub fn preset(name: &str) -> Option> { } } -/// Parsed config with optional loop interval. -pub struct LoadedConfig { - pub agents: Vec, - pub interval: Option, +// ── File loading ─────────────────────────────────────────── + +pub fn load(path: &str) -> Vec { + load_full(path).agents +} + +pub fn load_full(path: &str) -> LoadedConfig { + let path = Path::new(path); + if path.is_dir() { + return LoadedConfig { agents: load_dir(path), interval: None }; + } + load_file_full(path) } #[derive(Deserialize)] @@ -85,19 +125,6 @@ struct MultiConfig { cwd: Option, } -pub fn load(path: &str) -> Vec { - load_full(path).agents -} - -/// Load config with metadata (interval etc). -pub fn load_full(path: &str) -> LoadedConfig { - let path = Path::new(path); - if path.is_dir() { - return LoadedConfig { agents: load_dir(path), interval: None }; - } - load_file_full(path) -} - fn load_file_full(path: &Path) -> LoadedConfig { let content = match std::fs::read_to_string(path) { Ok(c) => c, @@ -118,35 +145,15 @@ fn load_file_full(path: &Path) -> LoadedConfig { LoadedConfig { agents, interval: None } } -/// Legacy: load agents only from file/directory. fn load_file(path: &Path) -> Vec { - let content = match std::fs::read_to_string(path) { - Ok(c) => c, - Err(_) => return Vec::new(), - }; - - // Try as { "agents": [...] } or single { "name", "task", "cwd" } - if let Ok(multi) = serde_json::from_str::(&content) { - if let Some(agents) = multi.agents { - return agents; - } - if let (Some(name), Some(task), Some(cwd)) = (multi.name, multi.task, multi.cwd) { - return vec![AgentConfig { name, task, cwd }]; - } - } - - // Try as bare array [{ ... }, { ... }] - serde_json::from_str::>(&content).unwrap_or_default() + load_file_full(path).agents } fn load_dir(path: &Path) -> Vec { let mut entries: Vec<_> = std::fs::read_dir(path) - .into_iter() - .flatten() - .flatten() + .into_iter().flatten().flatten() .filter(|e| e.path().extension().is_some_and(|ext| ext == "json")) .collect(); entries.sort_by_key(|e| e.file_name()); - entries.iter().flat_map(|e| load_file(&e.path())).collect() } diff --git a/src/headless.rs b/src/headless.rs index 6d6a99f..0e838ab 100644 --- a/src/headless.rs +++ b/src/headless.rs @@ -499,7 +499,7 @@ fn wait_for_loop_signal(running: &Arc) -> LoopAction { // ── Session persistence ──────────────────────────────────── fn save_session(cycle: usize, agents: &[Agent], decision: &str, indices: &[usize]) -> String { - let session_dir = session_base_dir(); + let session_dir = config::sessions_dir(); let _ = std::fs::create_dir_all(&session_dir); let timestamp = std::time::SystemTime::now() @@ -575,7 +575,7 @@ fn save_to_aigpt_memory(decision: &str, agents: &[serde_json::Value]) { } fn aigpt_memory_dir() -> Option { - let config_path = config::shared_config_path(); + let config_path = config::config_path(); let content = std::fs::read_to_string(&config_path).ok()?; let config: serde_json::Value = serde_json::from_str(&content).ok()?; @@ -621,9 +621,6 @@ fn days_to_ymd(mut days: u64) -> (u64, u64, u64) { (y, mo + 1, days + 1) } -fn session_base_dir() -> String { - config::sessions_dir() -} // ── AI integration ───────────────────────────────────────── @@ -881,7 +878,7 @@ pub fn signal_quit() { /// Update cmd_history.json: deduplicated command log across all agents. fn update_cmd_history(agents: &[serde_json::Value], session_ts: u64) { - let history_path = format!("{}/cmd_history.json", session_base_dir()); + let history_path = format!("{}/cmd_history.json", config::sessions_dir()); // Load existing let mut entries: Vec = std::fs::read_to_string(&history_path) @@ -953,7 +950,7 @@ fn update_cmd_history(agents: &[serde_json::Value], session_ts: u64) { } pub fn history(detail: Option<&str>) { - let dir = session_base_dir(); + let dir = config::sessions_dir(); let mut files: Vec<_> = std::fs::read_dir(&dir) .into_iter().flatten().flatten() .filter(|e| { @@ -1037,7 +1034,7 @@ pub fn context() { // Session info let dec_path = format!("{STATE_DIR}/decision.json"); - let session_dir = session_base_dir(); + let session_dir = config::sessions_dir(); let session_count = std::fs::read_dir(&session_dir) .into_iter().flatten().flatten() .filter(|e| e.path().extension().is_some_and(|x| x == "json") && e.file_name().to_string_lossy() != "cmd_history.json") @@ -1137,7 +1134,7 @@ pub fn context() { } // Command history (last 10) - let history_path = format!("{}/cmd_history.json", session_base_dir()); + let history_path = format!("{}/cmd_history.json", config::sessions_dir()); if let Ok(content) = std::fs::read_to_string(&history_path) { if let Ok(entries) = serde_json::from_str::>(&content) { if !entries.is_empty() { @@ -1155,7 +1152,7 @@ pub fn context() { /// Clean old sessions, keep last N. pub fn history_clean() { - let dir = session_base_dir(); + let dir = config::sessions_dir(); let mut files: Vec<_> = std::fs::read_dir(&dir) .into_iter().flatten().flatten() .filter(|e| { @@ -1182,7 +1179,7 @@ pub fn history_cmd(filter: Option<&str>) { } fn show_cmd_history(filter: Option<&str>) { - let path = format!("{}/cmd_history.json", session_base_dir()); + let path = format!("{}/cmd_history.json", config::sessions_dir()); match std::fs::read_to_string(&path) { Ok(content) => { if let Ok(entries) = serde_json::from_str::>(&content) { @@ -1279,13 +1276,6 @@ fn create_state_dir() { } } -pub fn expand_tilde_pub(path: &str) -> String { expand_tilde(path) } - fn expand_tilde(path: &str) -> String { - if let Some(rest) = path.strip_prefix('~') { - if let Ok(home) = std::env::var("HOME") { - return format!("{home}{rest}"); - } - } - path.to_string() + config::expand_tilde(path) } diff --git a/src/tui.rs b/src/tui.rs index 4874f45..799527b 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -664,7 +664,7 @@ fn parse_agent_commands(text: &str) -> Vec<(String, String, String)> { /// Load identity context from atproto config + recent chat. fn load_identity_context() -> String { - let config_path = crate::config::shared_config_path(); + let config_path = crate::config::config_path(); let config: serde_json::Value = match std::fs::read_to_string(&config_path) { Ok(s) => serde_json::from_str(&s).unwrap_or_default(), diff --git a/src/watch.rs b/src/watch.rs index 8b353fa..e7fcc2a 100644 --- a/src/watch.rs +++ b/src/watch.rs @@ -9,7 +9,7 @@ const DEBOUNCE_MS: u64 = 2000; /// Watch a directory for file changes. /// On change, write next=true to loop.json with changed file list. pub fn run(dir: &str, config: Option<&str>) -> Result<(), String> { - let dir = crate::headless::expand_tilde_pub(dir); + let dir = crate::config::expand_tilde(dir); let dir_path = Path::new(&dir); if !dir_path.is_dir() { return Err(format!("not a directory: {dir}"));