Skip to main content

deepseek_rust_cli/
config.rs

1use std::{fs, path::PathBuf};
2
3use anyhow::Result;
4use dotenvy::dotenv;
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Serialize, Deserialize, Clone)]
8pub struct Config {
9    pub model: String,
10    pub base_url: String,
11    pub request_timeout: u64,
12    #[serde(default)]
13    pub proxy_url: Option<String>,
14    #[serde(default)]
15    pub proxy_username: Option<String>,
16    #[serde(default)]
17    pub proxy_password: Option<String>,
18    #[serde(default)]
19    pub danger_accept_invalid_certs: bool,
20    pub temperature: f32,
21    pub top_p: f32,
22    pub presence_penalty: f32,
23    pub frequency_penalty: f32,
24    pub max_tokens: u32,
25    pub max_iterations: usize,
26    pub show_token_usage: bool,
27    pub concise_reasoning: bool,
28    pub debug: bool,
29    pub system_prompt: String,
30    #[serde(default = "default_max_tool_output_chars")]
31    pub max_tool_output_chars: usize,
32    #[serde(default = "default_max_context_chars")]
33    pub max_context_chars: usize,
34    #[serde(default = "default_thinking_enabled")]
35    pub thinking_enabled: bool,
36    #[serde(default = "default_reasoning_effort")]
37    pub reasoning_effort: Option<String>,
38    #[serde(default)]
39    pub json_mode: bool,
40}
41
42impl Default for Config {
43    fn default() -> Self {
44        let os = std::env::consts::OS;
45        let shell = std::env::var("SHELL").unwrap_or_else(|_| {
46            if os == "windows" {
47                "cmd/powershell".to_string()
48            } else {
49                "sh".to_string()
50            }
51        });
52
53        let prompt = DEFAULT_SYSTEM_PROMPT
54            .replace("{os}", os)
55            .replace("{shell}", &shell);
56
57        Self {
58            model: "deepseek-v4-pro".to_string(), /* Reverting to deepseek-v4-pro as requested by
59                                                   * user */
60            base_url: "https://api.deepseek.com".to_string(),
61            request_timeout: 6000, // 100 minutes
62            proxy_url: None,
63            proxy_username: None,
64            proxy_password: None,
65            danger_accept_invalid_certs: false,
66            temperature: 0.0,
67            top_p: 1.0,
68            presence_penalty: 0.0,
69            frequency_penalty: 0.0,
70            max_tokens: 16_384, // 16K — sufficient for practical use; saves completion tokens
71            max_iterations: 500,
72            show_token_usage: true,
73            concise_reasoning: true,
74            debug: false,
75            system_prompt: prompt,
76            max_tool_output_chars: 15000,
77            max_context_chars: 100000,
78            thinking_enabled: true,
79            reasoning_effort: Some("high".to_string()),
80            json_mode: false,
81        }
82    }
83}
84
85fn default_max_tool_output_chars() -> usize {
86    15000
87}
88
89fn default_max_context_chars() -> usize {
90    100000
91}
92
93fn default_thinking_enabled() -> bool {
94    true
95}
96
97fn default_reasoning_effort() -> Option<String> {
98    Some("high".to_string())
99}
100const DEFAULT_SYSTEM_PROMPT: &str =
101    "You are a terminal-based AI coding assistant running on {os} via {shell}.
102Be concise and practical. You have full access to the workspace to read/write files and execute \
103     commands.
104Explain your actions briefly. To save tokens: do not print full file contents in your response \
105     (use diffs/summaries instead), do not repeat tool outputs verbatim, and keep reasoning and \
106     responses as short as possible.";
107
108/// Initialize the .deep directory structure in the current workspace.
109/// Creates .deep/ folder with config.json, memory.md, and history/ subdirectory
110/// if they don't already exist.
111pub fn init_workspace() {
112    let deep_dir = PathBuf::from(".deep");
113
114    // Create .deep directory if it doesn't exist
115    if !deep_dir.exists() {
116        let _ = fs::create_dir_all(&deep_dir);
117    }
118
119    // Create history subdirectory
120    let history_dir = deep_dir.join("history");
121    if !history_dir.exists() {
122        let _ = fs::create_dir_all(&history_dir);
123    }
124
125    // Create config.json if it doesn't exist
126    let config_path = deep_dir.join("config.json");
127    if !config_path.exists() {
128        let config = Config::default();
129        if let Ok(json) = serde_json::to_string_pretty(&config) {
130            let _ = fs::write(&config_path, json);
131        }
132    }
133
134    // Create memory.md if it doesn't exist
135    let memory_path = deep_dir.join("memory.md");
136    if !memory_path.exists() {
137        let default_memory = r#"# Local Memory
138
139This file serves as the agent's persistent memory for this project.
140You can update this file to store important context, decisions, and notes
141that the AI agent should remember across sessions.
142
143## Project Notes
144- 
145
146## Decisions
147- 
148
149## Important Context
150- 
151"#;
152        let _ = fs::write(&memory_path, default_memory);
153    }
154}
155
156pub fn load_config() -> Config {
157    // 1. Try workspace .deep/config.json (primary)
158    if let Ok(content) = fs::read_to_string(".deep/config.json") {
159        if let Ok(loaded) = serde_json::from_str::<Config>(&content) {
160            return loaded;
161        }
162    }
163
164    // 2. Fallback: Try global ~/.deep/config.json
165    if let Some(mut home) = dirs::home_dir() {
166        home.push(".deep/config.json");
167        if let Some(loaded) = fs::read_to_string(home)
168            .ok()
169            .and_then(|c| serde_json::from_str::<Config>(&c).ok())
170        {
171            return loaded;
172        }
173    }
174
175    Config::default()
176}
177
178impl Config {
179    pub fn save(&self) -> Result<()> {
180        let path = PathBuf::from(".deep/config.json");
181        let json = serde_json::to_string_pretty(self)?;
182        fs::write(path, json)?;
183        Ok(())
184    }
185}
186
187pub fn get_api_key() -> Result<String> {
188    dotenv().ok();
189
190    // 1. Check current environment (includes workspace .env if Step 1 succeeded)
191    if let Ok(key) = std::env::var("DEEPSEEK_API_KEY") {
192        return Ok(key);
193    }
194
195    // 2. Check user's home directory .deep/.env
196    if let Some(mut home) = dirs::home_dir() {
197        home.push(".deep/.env");
198        if home.exists() {
199            let _ = dotenvy::from_path(&home);
200            if let Ok(key) = std::env::var("DEEPSEEK_API_KEY") {
201                return Ok(key);
202            }
203        }
204    }
205
206    anyhow::bail!(
207        "DEEPSEEK_API_KEY not found.\nPlease create ~/.deep/.env or workspace .env \
208         with:\nDEEPSEEK_API_KEY=your_api_key_here"
209    )
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215
216    #[test]
217    fn test_default_config() {
218        let config = Config::default();
219        assert_eq!(config.model, "deepseek-v4-pro");
220        assert!(config.temperature >= 0.0);
221    }
222
223    #[test]
224    fn test_config_serialization() {
225        let config = Config::default();
226        let json = serde_json::to_string(&config).unwrap();
227        let decoded: Config = serde_json::from_str(&json).unwrap();
228        assert_eq!(config.model, decoded.model);
229        assert_eq!(config.max_tool_output_chars, decoded.max_tool_output_chars);
230        assert_eq!(config.max_context_chars, decoded.max_context_chars);
231    }
232
233    #[test]
234    fn test_config_backward_compatibility() {
235        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"}"#;
236        let decoded: Config = serde_json::from_str(json).unwrap();
237        assert_eq!(decoded.max_tool_output_chars, 15000);
238        assert_eq!(decoded.max_context_chars, 100000);
239    }
240}