Skip to main content

ai_agent/
env.rs

1// Source: /data/home/swei/claudecode/openclaudecode/src/utils/env.ts
2//! ENV configuration reader
3//!
4//! Reads configuration from .env file and environment variables.
5//! Supports AI_* prefixed variables for SDK configuration.
6
7use crate::constants::env::ai;
8use std::collections::HashMap;
9use std::fs;
10use std::path::Path;
11
12/// Configuration values from environment
13#[derive(Debug, Clone, Default)]
14pub struct EnvConfig {
15    /// Base URL for the AI API
16    pub base_url: Option<String>,
17    /// Authentication token
18    pub auth_token: Option<String>,
19    /// Model to use
20    pub model: Option<String>,
21    /// Additional raw env values (AI_* prefixed)
22    pub extras: HashMap<String, String>,
23}
24
25impl EnvConfig {
26    /// Load env config from .env file and environment variables
27    /// Searches for .env in: current directory, then parent directories, then exe directory
28    /// Also loads from ~/.ai/settings.json
29    pub fn load() -> Self {
30        // First try current directory
31        let mut config = Self::load_from_dir(".");
32
33        // If no config found, try the executable's directory
34        if config.base_url.is_none() && config.auth_token.is_none() && config.model.is_none() {
35            if let Ok(exe_path) = std::env::current_exe() {
36                if let Some(exe_dir) = exe_path.parent() {
37                    let exe_config = Self::load_from_dir(exe_dir.to_str().unwrap_or("."));
38                    // Merge if we found something
39                    if exe_config.base_url.is_some()
40                        || exe_config.auth_token.is_some()
41                        || exe_config.model.is_some()
42                    {
43                        config = exe_config;
44                    }
45                }
46            }
47        }
48
49        // Also try loading from ~/.ai/settings.json
50        if let Some(home_dir) = dirs::home_dir() {
51            let settings_path = home_dir.join(".ai").join("settings.json");
52            if settings_path.exists() {
53                if let Ok(content) = fs::read_to_string(&settings_path) {
54                    if let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) {
55                        if let Some(env) = json.get("env").and_then(|v| v.as_object()) {
56                            for (key, value) in env {
57                                if let Some(v) = value.as_str() {
58                                    config.set_value(key, v);
59                                }
60                            }
61                        }
62                    }
63                }
64            }
65        }
66
67        // Override with system environment variables
68        config.load_from_env();
69
70        config
71    }
72
73    /// Load env config from a specific directory
74    pub fn load_from_dir(dir: &str) -> Self {
75        let mut config = Self::default();
76
77        // Try to load from .env file
78        let env_path = Path::new(dir).join(".env");
79        if env_path.exists() {
80            if let Ok(content) = fs::read_to_string(&env_path) {
81                config.parse_env_file(&content);
82            }
83        }
84
85        // Also check parent directories (up to 3 levels)
86        let mut current = Path::new(dir);
87        for _ in 0..3 {
88            if let Some(parent) = current.parent() {
89                let parent_env = parent.join(".env");
90                if parent_env.exists() && parent_env != env_path {
91                    if let Ok(content) = fs::read_to_string(&parent_env) {
92                        config.parse_env_file(&content);
93                    }
94                }
95                current = parent;
96            } else {
97                break;
98            }
99        }
100
101        // Override with system environment variables
102        config.load_from_env();
103
104        config
105    }
106
107    /// Parse .env file content
108    fn parse_env_file(&mut self, content: &str) {
109        for line in content.lines() {
110            let line = line.trim();
111            // Skip empty lines and comments
112            if line.is_empty() || line.starts_with('#') {
113                continue;
114            }
115
116            // Parse KEY=VALUE
117            if let Some((key, value)) = line.split_once('=') {
118                let key = key.trim();
119                let value = value.trim();
120
121                // Remove quotes if present
122                let value = value.trim_matches('"').trim_matches('\'');
123
124                self.set_value(key, value);
125            }
126        }
127    }
128
129    /// Load from system environment variables
130    fn load_from_env(&mut self) {
131        // AI_BASE_URL (SDK native)
132        if let Ok(val) = std::env::var(ai::BASE_URL) {
133            self.base_url = Some(val);
134        }
135
136        // AI_AUTH_TOKEN (SDK native)
137        if let Ok(val) = std::env::var(ai::AUTH_TOKEN) {
138            self.auth_token = Some(val);
139        }
140
141        // ANTHROPIC_API_KEY fallback - check if no AI_AUTH_TOKEN
142        if self.auth_token.is_none() {
143            if let Ok(val) = std::env::var(ai::API_KEY) {
144                self.auth_token = Some(val);
145            }
146        }
147
148        // ANTHROPIC_AUTH_TOKEN fallback - check if no auth_token yet
149        if self.auth_token.is_none() {
150            if let Ok(val) = std::env::var(ai::ANTHROPIC_AUTH_TOKEN) {
151                self.auth_token = Some(val);
152            }
153        }
154
155        // AI_MODEL (SDK native)
156        if let Ok(val) = std::env::var(ai::MODEL) {
157            self.model = Some(val);
158        }
159
160        // ANTHROPIC_MODEL fallback - check if no AI_MODEL
161        if self.model.is_none() {
162            if let Ok(val) = std::env::var(ai::ANTHROPIC_MODEL) {
163                self.model = Some(val);
164            }
165        }
166
167        // Any other AI_* variables
168        for (key, value) in std::env::vars() {
169            if key.starts_with("AI_") {
170                match key.as_str() {
171                    "AI_BASE_URL" | "AI_AUTH_TOKEN" | "AI_MODEL" => {} // Already handled
172                    _ => {
173                        self.extras.insert(key, value);
174                    }
175                }
176            }
177        }
178    }
179
180    /// Set a configuration value
181    fn set_value(&mut self, key: &str, value: &str) {
182        match key {
183            "AI_BASE_URL" => self.base_url = Some(value.to_string()),
184            ai::BASE_URL => {
185                if self.base_url.is_none() {
186                    self.base_url = Some(value.to_string());
187                }
188            }
189            "AI_AUTH_TOKEN" => self.auth_token = Some(value.to_string()),
190            ai::API_KEY | ai::AUTH_TOKEN => {
191                // Only set if no auth_token already (AI_AUTH_TOKEN takes priority)
192                if self.auth_token.is_none() {
193                    self.auth_token = Some(value.to_string());
194                }
195            }
196            "AI_MODEL" => self.model = Some(value.to_string()),
197            ai::MODEL => {
198                if self.model.is_none() {
199                    self.model = Some(value.to_string());
200                }
201            }
202            _ => {
203                if key.starts_with("AI_") {
204                    self.extras.insert(key.to_string(), value.to_string());
205                }
206            }
207        }
208    }
209
210    /// Get a value by key
211    pub fn get(&self, key: &str) -> Option<&str> {
212        match key {
213            "AI_BASE_URL" => self.base_url.as_deref(),
214            "AI_AUTH_TOKEN" => self.auth_token.as_deref(),
215            "AI_MODEL" => self.model.as_deref(),
216            _ => self.extras.get(key).map(|s| s.as_str()),
217        }
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224
225    #[test]
226    fn test_parse_env_file() {
227        let mut config = EnvConfig::default();
228        config.parse_env_file(
229            r#"
230# Comment
231AI_BASE_URL="http://localhost:8000"
232AI_AUTH_TOKEN='test-token'
233AI_MODEL=claude-sonnet-4-6
234"#,
235        );
236
237        assert_eq!(config.base_url, Some("http://localhost:8000".to_string()));
238        assert_eq!(config.auth_token, Some("test-token".to_string()));
239        assert_eq!(config.model, Some("claude-sonnet-4-6".to_string()));
240    }
241
242    #[test]
243    fn test_get_values() {
244        let config = EnvConfig {
245            base_url: Some("http://test".to_string()),
246            auth_token: Some("token".to_string()),
247            model: Some("model".to_string()),
248            extras: HashMap::new(),
249        };
250
251        assert_eq!(config.get("AI_BASE_URL"), Some("http://test"));
252        assert_eq!(config.get("AI_AUTH_TOKEN"), Some("token"));
253        assert_eq!(config.get("AI_MODEL"), Some("model"));
254        assert_eq!(config.get("UNKNOWN"), None);
255    }
256}
257
258// =============================================================================
259// ASSISTANT MODE
260// =============================================================================
261
262/// Read the assistant mode flag from environment variables.
263/// Checks AI_CODE_ASSISTANT_MODE.
264fn read_assistant_mode_flag() -> bool {
265    if let Ok(val) = std::env::var(ai::CODE_ASSISTANT_MODE) {
266        return val == "1" || val == "true";
267    }
268
269    false
270}
271
272/// Check if assistant mode is enabled.
273/// This function is used to determine if the CLI is running in assistant mode.
274pub fn is_assistant_mode() -> bool {
275    read_assistant_mode_flag()
276}
277
278/// Check if assistant mode is enabled (alias for is_assistant_mode).
279pub fn is_assistant_mode_enabled() -> bool {
280    read_assistant_mode_flag()
281}