use std::{fs, path::PathBuf};
use anyhow::Result;
use dotenvy::dotenv;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Config {
pub model: String,
pub base_url: String,
pub request_timeout: u64,
#[serde(default)]
pub proxy_url: Option<String>,
#[serde(default)]
pub proxy_username: Option<String>,
#[serde(default)]
pub proxy_password: Option<String>,
#[serde(default)]
pub danger_accept_invalid_certs: bool,
pub temperature: f32,
pub top_p: f32,
pub presence_penalty: f32,
pub frequency_penalty: f32,
pub max_tokens: u32,
pub max_iterations: usize,
pub show_token_usage: bool,
pub concise_reasoning: bool,
pub debug: bool,
pub system_prompt: String,
#[serde(default = "default_max_tool_output_chars")]
pub max_tool_output_chars: usize,
#[serde(default = "default_max_context_chars")]
pub max_context_chars: usize,
#[serde(default = "default_thinking_enabled")]
pub thinking_enabled: bool,
#[serde(default = "default_reasoning_effort")]
pub reasoning_effort: Option<String>,
#[serde(default)]
pub json_mode: bool,
}
impl Default for Config {
fn default() -> Self {
let os = std::env::consts::OS;
let shell = std::env::var("SHELL").unwrap_or_else(|_| {
if os == "windows" {
"cmd/powershell".to_string()
} else {
"sh".to_string()
}
});
let prompt = DEFAULT_SYSTEM_PROMPT
.replace("{os}", os)
.replace("{shell}", &shell);
Self {
model: "deepseek-v4-pro".to_string(),
base_url: "https://api.deepseek.com".to_string(),
request_timeout: 6000, proxy_url: None,
proxy_username: None,
proxy_password: None,
danger_accept_invalid_certs: false,
temperature: 0.0,
top_p: 1.0,
presence_penalty: 0.0,
frequency_penalty: 0.0,
max_tokens: 16_384, max_iterations: 500,
show_token_usage: true,
concise_reasoning: true,
debug: false,
system_prompt: prompt,
max_tool_output_chars: 15000,
max_context_chars: 100000,
thinking_enabled: true,
reasoning_effort: Some("high".to_string()),
json_mode: false,
}
}
}
fn default_max_tool_output_chars() -> usize {
15000
}
fn default_max_context_chars() -> usize {
100000
}
fn default_thinking_enabled() -> bool {
true
}
fn default_reasoning_effort() -> Option<String> {
Some("high".to_string())
}
const DEFAULT_SYSTEM_PROMPT: &str =
"You are a terminal-based AI coding assistant running on {os} via {shell}.
Be concise and practical. You have full access to the workspace to read/write files and execute \
commands.
Explain your actions briefly. To save tokens: do not print full file contents in your response \
(use diffs/summaries instead), do not repeat tool outputs verbatim, and keep reasoning and \
responses as short as possible.";
pub fn init_workspace() {
let deep_dir = PathBuf::from(".deep");
if !deep_dir.exists() {
let _ = fs::create_dir_all(&deep_dir);
}
let history_dir = deep_dir.join("history");
if !history_dir.exists() {
let _ = fs::create_dir_all(&history_dir);
}
let config_path = deep_dir.join("config.json");
if !config_path.exists() {
let config = Config::default();
if let Ok(json) = serde_json::to_string_pretty(&config) {
let _ = fs::write(&config_path, json);
}
}
let memory_path = deep_dir.join("memory.md");
if !memory_path.exists() {
let default_memory = r#"# Local Memory
This file serves as the agent's persistent memory for this project.
You can update this file to store important context, decisions, and notes
that the AI agent should remember across sessions.
## Project Notes
-
## Decisions
-
## Important Context
-
"#;
let _ = fs::write(&memory_path, default_memory);
}
}
pub fn load_config() -> Config {
if let Ok(content) = fs::read_to_string(".deep/config.json") {
if let Ok(loaded) = serde_json::from_str::<Config>(&content) {
return loaded;
}
}
if let Some(mut home) = dirs::home_dir() {
home.push(".deep/config.json");
if let Some(loaded) = fs::read_to_string(home)
.ok()
.and_then(|c| serde_json::from_str::<Config>(&c).ok())
{
return loaded;
}
}
Config::default()
}
impl Config {
pub fn save(&self) -> Result<()> {
let path = PathBuf::from(".deep/config.json");
let json = serde_json::to_string_pretty(self)?;
fs::write(path, json)?;
Ok(())
}
}
pub fn get_api_key() -> Result<String> {
dotenv().ok();
if let Ok(key) = std::env::var("DEEPSEEK_API_KEY") {
return Ok(key);
}
if let Some(mut home) = dirs::home_dir() {
home.push(".deep/.env");
if home.exists() {
let _ = dotenvy::from_path(&home);
if let Ok(key) = std::env::var("DEEPSEEK_API_KEY") {
return Ok(key);
}
}
}
anyhow::bail!(
"DEEPSEEK_API_KEY not found.\nPlease create ~/.deep/.env or workspace .env \
with:\nDEEPSEEK_API_KEY=your_api_key_here"
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = Config::default();
assert_eq!(config.model, "deepseek-v4-pro");
assert!(config.temperature >= 0.0);
}
#[test]
fn test_config_serialization() {
let config = Config::default();
let json = serde_json::to_string(&config).unwrap();
let decoded: Config = serde_json::from_str(&json).unwrap();
assert_eq!(config.model, decoded.model);
assert_eq!(config.max_tool_output_chars, decoded.max_tool_output_chars);
assert_eq!(config.max_context_chars, decoded.max_context_chars);
}
#[test]
fn test_config_backward_compatibility() {
let json = r#"{"model":"test-model","base_url":"http://test","request_timeout":10,"temperature":0.5,"top_p":0.9,"presence_penalty":0.0,"frequency_penalty":0.0,"max_tokens":1000,"max_iterations":5,"show_token_usage":false,"concise_reasoning":false,"debug":false,"system_prompt":"sys"}"#;
let decoded: Config = serde_json::from_str(json).unwrap();
assert_eq!(decoded.max_tool_output_chars, 15000);
assert_eq!(decoded.max_context_chars, 100000);
}
}