use crate::models::*;
use anyhow::{Context, Result};
use std::fs;
use std::path::PathBuf;
use dirs::home_dir;
use serde_json;
pub struct ConfigManager {
config_dir: PathBuf,
config_file: PathBuf,
claude_settings_file: PathBuf,
}
impl ConfigManager {
pub fn new() -> Result<Self> {
let home_dir = home_dir().context("Could not find home directory")?;
let config_dir = home_dir.join(".cckit");
let config_file = config_dir.join("config.json");
let claude_settings_file = home_dir.join(".claude/settings.json");
Ok(Self {
config_dir,
config_file,
claude_settings_file,
})
}
pub fn initialize(&self) -> Result<()> {
if !self.config_dir.exists() {
fs::create_dir_all(&self.config_dir)
.context("Failed to create config directory")?;
}
if !self.config_file.exists() {
let default_config = CCKitConfig::new();
self.save_config(&default_config)?;
}
Ok(())
}
fn load_config(&self) -> Result<CCKitConfig> {
if !self.config_file.exists() {
return Ok(CCKitConfig::new());
}
let content = fs::read_to_string(&self.config_file)
.context("Failed to read config file")?;
serde_json::from_str(&content)
.context("Failed to parse config file")
}
fn save_config(&self, config: &CCKitConfig) -> Result<()> {
let content = serde_json::to_string_pretty(config)
.context("Failed to serialize config")?;
fs::write(&self.config_file, content)
.context("Failed to write config file")
}
pub fn list_providers(&self) -> Result<Vec<ModelProvider>> {
let config = self.load_config()?;
Ok(config.providers)
}
pub fn save_provider(&self, provider: &ModelProvider) -> Result<()> {
let mut config = self.load_config()?;
config.add_provider(provider.clone());
self.save_config(&config)
}
pub fn get_current_provider(&self) -> Result<Option<ModelProvider>> {
let config = self.load_config()?;
Ok(config.get_current_provider().cloned())
}
pub fn switch_provider(&mut self, provider_id: &str) -> Result<()> {
let mut config = self.load_config()?;
config.set_current_provider(provider_id)?;
self.save_config(&config)?;
self.apply_provider_to_claude_settings(provider_id)?;
Ok(())
}
fn apply_provider_to_claude_settings(&self, provider_id: &str) -> Result<()> {
let config = self.load_config()?;
let provider = config.get_provider_by_id(provider_id)
.context("Provider not found")?;
let mut claude_settings = if self.claude_settings_file.exists() {
let content = fs::read_to_string(&self.claude_settings_file)
.context("Failed to read Claude settings")?;
serde_json::from_str(&content)
.context("Failed to parse Claude settings")?
} else {
serde_json::json!({})
};
let env_section = match provider.provider_type.as_str() {
"claude" => {
serde_json::json!({
"ANTHROPIC_API_KEY": provider.api_key
})
}
"zhipu" | "minimax" => {
serde_json::json!({
"ANTHROPIC_AUTH_TOKEN": provider.api_key,
"ANTHROPIC_BASE_URL": provider.base_url.as_ref().unwrap_or(&"".to_string()),
"ANTHROPIC_MODEL": provider.model.as_ref().unwrap_or(&"claude-3-5-sonnet-20241022".to_string())
})
}
"kimi" => {
serde_json::json!({
"ANTHROPIC_API_KEY": provider.api_key,
"ANTHROPIC_BASE_URL": provider.base_url.as_ref().unwrap_or(&"".to_string()),
"ANTHROPIC_MODEL": provider.model.as_ref().unwrap_or(&"kimi-for-coding".to_string()),
"ANTHROPIC_DEFAULT_OPUS_MODEL": provider.model.as_ref().unwrap_or(&"kimi-for-coding".to_string()),
"ANTHROPIC_DEFAULT_SONNET_MODEL": provider.model.as_ref().unwrap_or(&"kimi-for-coding".to_string()),
"ANTHROPIC_DEFAULT_HAIKU_MODEL": provider.model.as_ref().unwrap_or(&"kimi-for-coding".to_string())
})
}
_ => {
serde_json::json!({
"ANTHROPIC_API_KEY": provider.api_key,
"ANTHROPIC_BASE_URL": provider.base_url.as_ref().unwrap_or(&"".to_string()),
"ANTHROPIC_MODEL": provider.model.as_ref().unwrap_or(&"claude-3-5-sonnet-20241022".to_string()),
"CC_PROVIDER": provider.provider_type
})
}
};
claude_settings["env"] = env_section;
if let Some(parent) = self.claude_settings_file.parent() {
fs::create_dir_all(parent)
.context("Failed to create .claude directory")?;
}
let content = serde_json::to_string_pretty(&claude_settings)
.context("Failed to serialize Claude settings")?;
fs::write(&self.claude_settings_file, content)
.context("Failed to write Claude settings")?;
Ok(())
}
pub fn reset_to_claude_default(&mut self) -> Result<()> {
let mut config = self.load_config()?;
config.current_provider_id = None;
self.save_config(&config)?;
let claude_settings = serde_json::json!({
"env": {
"ANTHROPIC_API_KEY": ""
}
});
if let Some(parent) = self.claude_settings_file.parent() {
fs::create_dir_all(parent)
.context("Failed to create .claude directory")?;
}
let content = serde_json::to_string_pretty(&claude_settings)
.context("Failed to serialize Claude settings")?;
fs::write(&self.claude_settings_file, content)
.context("Failed to write Claude settings")?;
Ok(())
}
#[allow(dead_code)]
pub fn export_config(&self) -> Result<CCKitConfig> {
self.load_config()
}
#[allow(dead_code)]
pub fn import_config(&mut self, imported_config: CCKitConfig) -> Result<()> {
self.save_config(&imported_config)?;
if let Some(current_id) = &imported_config.current_provider_id {
self.apply_provider_to_claude_settings(current_id)?;
}
Ok(())
}
}
pub fn get_default_base_url(provider_type: &str) -> Option<String> {
match provider_type {
"zhipu" => Some("https://open.bigmodel.cn/api/anthropic".to_string()),
"minimax" => Some("https://api.minimaxi.com/anthropic".to_string()),
"kimi" => Some("https://api.kimi.com/coding/".to_string()),
"claude" => None, _ => None,
}
}
pub fn get_default_model(provider_type: &str) -> Option<String> {
match provider_type {
"zhipu" => Some("GLM-4.6".to_string()),
"minimax" => Some("MiniMax-M2".to_string()),
"kimi" => Some("kimi-for-coding".to_string()),
"claude" => Some("claude-3-5-sonnet-20241022".to_string()),
_ => Some("claude-3-5-sonnet-20241022".to_string()),
}
}