2
0
Files
shell/src/config.rs

153 lines
6.3 KiB
Rust

use serde::Deserialize;
use std::path::Path;
/// OS standard config directory.
pub fn config_dir() -> String {
let home = std::env::var("HOME").unwrap_or_default();
if cfg!(target_os = "macos") {
format!("{home}/Library/Application Support")
} else {
std::env::var("XDG_CONFIG_HOME").unwrap_or_else(|_| format!("{home}/.config"))
}
}
/// Path to shared config: $cfg/ai.syui.log/config.json
pub fn shared_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())
}
/// Path to sessions dir: $cfg/ai.syui.gpt/sessions
pub fn sessions_dir() -> String {
format!("{}/ai.syui.gpt/sessions", config_dir())
}
#[derive(Clone, Deserialize)]
pub struct AgentConfig {
pub name: String,
pub task: String,
#[serde(default = "default_cwd")]
pub cwd: String,
}
fn default_cwd() -> String {
std::env::current_dir()
.map(|p| p.display().to_string())
.unwrap_or_else(|_| ".".to_string())
}
/// Built-in presets.
pub fn preset(name: &str) -> Option<Vec<AgentConfig>> {
let cwd = default_cwd();
match name {
"daily" => Some(vec![
AgentConfig { name: "health".into(), task: "Run cargo test and cargo build --release. One line: pass or fail, warning count.".into(), cwd: cwd.clone() },
AgentConfig { name: "quality".into(), task: "Find one unwrap() that could panic and one function over 50 lines. Give file:line only.".into(), cwd: cwd.clone() },
AgentConfig { name: "idea".into(), task: "Read docs/architecture.md and git log -5. Suggest one practical improvement. 3 sentences max.".into(), cwd },
]),
"review" => Some(vec![
AgentConfig { name: "diff-review".into(), task: "Review the git diff. Report problems or say 'no issues'.".into(), cwd: cwd.clone() },
AgentConfig { name: "commit-msg".into(), task: "Suggest a commit message for the current changes. Conventional commits format.".into(), cwd },
]),
"improve" => Some(vec![
AgentConfig { name: "bug-hunt".into(), task: "Find one concrete bug in the codebase. Give file:line and a fix.".into(), cwd: cwd.clone() },
AgentConfig { name: "simplify".into(), task: "Find one function that can be removed or simplified. Be specific.".into(), cwd },
]),
"security" => Some(vec![
AgentConfig { name: "secrets".into(), task: "Scan src/ for hardcoded secrets, API keys, tokens, passwords, personal emails, private IPs. Also check for hardcoded absolute paths like /Users/ or /home/. Report file:line for each finding, or 'clean' if none.".into(), cwd: cwd.clone() },
AgentConfig { name: "safe-publish".into(), task: "Check if this repo is safe to push publicly. Look for: .env files, credentials in config/, personal info in docs/, sensitive data in git history (check git log --all --oneline for suspicious commit messages). Report issues or 'safe'.".into(), cwd },
]),
"report" => Some(vec![
AgentConfig { name: "agent-view".into(), task: "You are an agent inside aishell. Reflect: What context did you receive? What was missing? What would make your job easier? 3 concrete points from your perspective.".into(), cwd: cwd.clone() },
AgentConfig { name: "user-view".into(), task: "You are a developer using aishell daily. Run aishell help and aishell context. What are the 3 biggest friction points in the daily workflow?".into(), cwd: cwd.clone() },
AgentConfig { name: "system-view".into(), task: "Read src/headless.rs and src/tui.rs. From the system's perspective: what is the most fragile part? What would break first under heavy use? One concrete issue.".into(), cwd },
]),
_ => None,
}
}
/// Parsed config with optional loop interval.
pub struct LoadedConfig {
pub agents: Vec<AgentConfig>,
pub interval: Option<u64>,
}
#[derive(Deserialize)]
struct MultiConfig {
agents: Option<Vec<AgentConfig>>,
interval: Option<u64>,
name: Option<String>,
task: Option<String>,
cwd: Option<String>,
}
pub fn load(path: &str) -> Vec<AgentConfig> {
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,
Err(_) => return LoadedConfig { agents: Vec::new(), interval: None },
};
if let Ok(multi) = serde_json::from_str::<MultiConfig>(&content) {
if let Some(agents) = multi.agents {
return LoadedConfig { agents, interval: multi.interval };
}
if let (Some(name), Some(task), Some(cwd)) = (multi.name, multi.task, multi.cwd) {
return LoadedConfig {
agents: vec![AgentConfig { name, task, cwd }],
interval: multi.interval,
};
}
}
let agents = serde_json::from_str::<Vec<AgentConfig>>(&content).unwrap_or_default();
LoadedConfig { agents, interval: None }
}
/// Legacy: load agents only from file/directory.
fn load_file(path: &Path) -> Vec<AgentConfig> {
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::<MultiConfig>(&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::<Vec<AgentConfig>>(&content).unwrap_or_default()
}
fn load_dir(path: &Path) -> Vec<AgentConfig> {
let mut entries: Vec<_> = std::fs::read_dir(path)
.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()
}