153 lines
6.3 KiB
Rust
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()
|
|
}
|