use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct AppConfig {
pub model: String,
pub provider: String,
pub max_turns: u32,
pub max_tokens: u32,
pub effort: String,
pub output_style: String,
pub theme: String,
pub auto_compact: bool,
pub graph_memory: bool,
pub permissions_mode: String,
pub working_dir: PathBuf,
#[serde(default)]
pub fallback_models: Vec<String>,
#[serde(default)]
pub mcp_servers: Vec<McpServerEntry>,
#[serde(default)]
pub hooks: Vec<HookEntry>,
#[serde(default)]
pub proxy: ProxyConfig,
#[serde(default)]
pub benchmark_mode: bool,
#[serde(default)]
pub embedding_api: bool,
#[serde(default = "default_output_format")]
pub output_format: String,
#[serde(default = "default_compression")]
pub compression_level: String,
}
fn default_output_format() -> String {
"text".into()
}
fn default_compression() -> String {
"off".into()
}
impl Default for AppConfig {
fn default() -> Self {
Self {
model: "auto".into(),
provider: "auto".into(),
max_turns: 50,
max_tokens: 16384,
effort: "medium".into(),
output_style: "default".into(),
theme: "dark".into(),
auto_compact: true,
graph_memory: true,
permissions_mode: "interactive".into(),
working_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
fallback_models: Vec::new(),
mcp_servers: Vec::new(),
hooks: Vec::new(),
proxy: ProxyConfig::default(),
benchmark_mode: false,
embedding_api: false,
output_format: "text".into(),
compression_level: "off".into(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProxyConfig {
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default)]
pub force: bool,
#[serde(default = "default_proxy_url")]
pub url: String,
}
impl Default for ProxyConfig {
fn default() -> Self {
Self {
enabled: true,
force: false,
url: default_proxy_url(),
}
}
}
fn default_true() -> bool {
true
}
fn default_proxy_url() -> String {
"http://localhost:8317/v1".into()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpServerEntry {
pub name: String,
pub command: String,
#[serde(default)]
pub args: Vec<String>,
#[serde(default)]
pub env: std::collections::HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HookEntry {
pub event: String,
pub command: String,
}
pub fn global_config_dir() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".abstract")
}
pub fn project_config_dir() -> PathBuf {
std::env::current_dir()
.unwrap_or_else(|_| PathBuf::from("."))
.join(".abstract")
}
pub fn global_config_path() -> PathBuf {
global_config_dir().join("config.toml")
}
pub fn project_config_path() -> PathBuf {
project_config_dir().join("config.toml")
}
pub fn history_path() -> PathBuf {
global_config_dir().join("history")
}
pub fn graph_db_path() -> PathBuf {
global_config_dir().join("graph.db")
}
pub fn load() -> AppConfig {
let mut config = AppConfig::default();
if let Some(loaded) = load_toml_file(&global_config_path()) {
merge(&mut config, loaded);
}
if let Some(loaded) = load_toml_file(&project_config_path()) {
merge(&mut config, loaded);
}
apply_env(&mut config);
config
}
fn load_toml_file(path: &Path) -> Option<AppConfig> {
let content = std::fs::read_to_string(path).ok()?;
toml::from_str(&content).ok()
}
fn merge(base: &mut AppConfig, overlay: AppConfig) {
if overlay.model != AppConfig::default().model {
base.model = overlay.model;
}
if overlay.provider != AppConfig::default().provider {
base.provider = overlay.provider;
}
if overlay.max_turns != AppConfig::default().max_turns {
base.max_turns = overlay.max_turns;
}
if overlay.max_tokens != AppConfig::default().max_tokens {
base.max_tokens = overlay.max_tokens;
}
if overlay.effort != AppConfig::default().effort {
base.effort = overlay.effort;
}
if overlay.output_style != AppConfig::default().output_style {
base.output_style = overlay.output_style;
}
if overlay.theme != AppConfig::default().theme {
base.theme = overlay.theme;
}
if !overlay.auto_compact && AppConfig::default().auto_compact {
base.auto_compact = false;
}
if !overlay.graph_memory && AppConfig::default().graph_memory {
base.graph_memory = false;
}
if overlay.permissions_mode != AppConfig::default().permissions_mode {
base.permissions_mode = overlay.permissions_mode;
}
if !overlay.fallback_models.is_empty() {
base.fallback_models = overlay.fallback_models;
}
if !overlay.mcp_servers.is_empty() {
base.mcp_servers = overlay.mcp_servers;
}
if !overlay.hooks.is_empty() {
base.hooks = overlay.hooks;
}
if overlay.compression_level != AppConfig::default().compression_level {
base.compression_level = overlay.compression_level;
}
}
fn apply_env(config: &mut AppConfig) {
if let Ok(v) = std::env::var("ABSTRACT_MODEL") {
config.model = v;
}
if let Ok(v) = std::env::var("ABSTRACT_PROVIDER") {
config.provider = v;
}
if let Ok(v) = std::env::var("ABSTRACT_EFFORT") {
config.effort = v;
}
if let Ok(v) = std::env::var("ABSTRACT_THEME") {
config.theme = v;
}
if let Ok(v) = std::env::var("ABSTRACT_FALLBACK_MODELS") {
config.fallback_models = v
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
}
if let Ok(v) = std::env::var("ABSTRACT_MAX_TURNS") {
if let Ok(n) = v.parse() {
config.max_turns = n;
}
}
if let Ok(v) = std::env::var("ABSTRACT_COMPRESSION") {
config.compression_level = v;
}
}
pub fn save_to(config: &AppConfig, path: &Path) -> anyhow::Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let content = toml::to_string_pretty(config)?;
std::fs::write(path, content)?;
Ok(())
}