1use anyhow::{Result, anyhow};
2use console::style;
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::fs;
6use std::path::Path;
7
8use crate::{Configurable, SnapshotScope, TemplateType};
9
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
12#[serde(rename_all = "camelCase")]
13pub struct ClaudeSettings {
14 pub provider: Option<ProviderConfig>,
16
17 pub model: Option<ModelConfig>,
19
20 pub endpoint: Option<EndpointConfig>,
22
23 pub http: Option<HTTPConfig>,
25
26 pub permissions: Option<Permissions>,
28
29 pub hooks: Option<Hooks>,
31
32 pub status_line: Option<StatusLine>,
34
35 pub environment: Option<HashMap<String, String>>,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
41pub struct ProviderConfig {
42 pub id: String,
43 pub metadata: Option<HashMap<String, String>>,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
48pub struct ModelConfig {
49 pub name: String,
50 pub metadata: Option<HashMap<String, String>>,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
55pub struct EndpointConfig {
56 pub id: String,
57 pub api_base: String,
58 pub api_key: Option<String>,
59 pub endpoint_id: Option<String>,
60 pub metadata: Option<HashMap<String, String>>,
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
65pub struct HTTPConfig {
66 pub timeout_ms: Option<u64>,
67 pub max_retries: Option<u32>,
68 pub retry_backoff_factor: Option<f64>,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
73pub struct Permissions {
74 pub allow_network_access: Option<bool>,
75 pub allow_filesystem_access: Option<bool>,
76 pub allow_command_execution: Option<bool>,
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
81pub struct Hooks {
82 pub on_start: Option<Vec<String>>,
83 pub on_save: Option<Vec<String>>,
84 pub on_send_message: Option<Vec<String>>,
85 pub on_receive_message: Option<Vec<String>>,
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
90pub struct StatusLine {
91 pub enabled: Option<bool>,
92 pub format: Option<String>,
93 pub style: Option<String>,
94}
95
96impl ClaudeSettings {
97 pub fn new() -> Self {
99 Self {
100 provider: None,
101 model: None,
102 endpoint: None,
103 http: None,
104 permissions: None,
105 hooks: None,
106 status_line: None,
107 environment: None,
108 }
109 }
110
111 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
113 let path = path.as_ref();
114 if !path.exists() {
115 return Ok(Self::new());
116 }
117
118 let content = fs::read_to_string(path)
119 .map_err(|e| anyhow!("Failed to read settings file {}: {}", path.display(), e))?;
120
121 if content.trim().is_empty() {
122 return Ok(Self::new());
123 }
124
125 serde_json::from_str(&content)
126 .map_err(|e| anyhow!("Failed to parse settings file {}: {}", path.display(), e))
127 }
128
129 pub fn to_file<P: AsRef<Path>>(&self, path: P) -> Result<()> {
131 let path = path.as_ref();
132 let parent = path.parent().ok_or_else(|| {
133 anyhow!(
134 "Settings file path {} has no parent directory",
135 path.display()
136 )
137 })?;
138
139 fs::create_dir_all(parent).map_err(|e| {
140 anyhow!(
141 "Failed to create settings directory {}: {}",
142 parent.display(),
143 e
144 )
145 })?;
146
147 let content = serde_json::to_string_pretty(self)
148 .map_err(|e| anyhow!("Failed to serialize settings: {}", e))?;
149
150 fs::write(path, content)
151 .map_err(|e| anyhow!("Failed to write settings file {}: {}", path.display(), e))
152 }
153
154 pub fn capture_environment() -> HashMap<String, String> {
156 let mut env = HashMap::new();
157
158 if let Ok(value) = std::env::var("CLAUDE_CODE_API_KEY") {
160 env.insert("CLAUDE_CODE_API_KEY".to_string(), value);
161 }
162 if let Ok(value) = std::env::var("ANTHROPIC_API_KEY") {
163 env.insert("ANTHROPIC_API_KEY".to_string(), value);
164 }
165
166 env
167 }
168
169 pub fn capture_template_environment(template_type: &TemplateType) -> HashMap<String, String> {
171 let mut env = HashMap::new();
172
173 match template_type {
174 TemplateType::DeepSeek => {
175 if let Ok(value) = std::env::var("DEEPSEEK_API_KEY") {
176 env.insert("DEEPSEEK_API_KEY".to_string(), value);
177 }
178 }
179 TemplateType::Zai => {
180 if let Ok(value) = std::env::var("Z_AI_API_KEY") {
181 env.insert("Z_AI_API_KEY".to_string(), value);
182 }
183 }
184 TemplateType::K2 | TemplateType::K2Thinking => {
185 if let Ok(value) = std::env::var("MOONSHOT_API_KEY") {
186 env.insert("MOONSHOT_API_KEY".to_string(), value);
187 }
188 }
189 TemplateType::KatCoder | TemplateType::KatCoderPro | TemplateType::KatCoderAir => {
190 if let Ok(value) = std::env::var("KAT_CODER_API_KEY") {
191 env.insert("KAT_CODER_API_KEY".to_string(), value);
192 }
193 }
194 TemplateType::Kimi => {
195 if let Ok(value) = std::env::var("KIMI_API_KEY") {
196 env.insert("KIMI_API_KEY".to_string(), value);
197 }
198 }
199 TemplateType::Longcat => {
200 if let Ok(value) = std::env::var("LONGCAT_API_KEY") {
201 env.insert("LONGCAT_API_KEY".to_string(), value);
202 }
203 }
204 TemplateType::MiniMax => {
205 if let Ok(value) = std::env::var("MINIMAX_API_KEY") {
206 env.insert("MINIMAX_API_KEY".to_string(), value);
207 }
208 }
209 }
210
211 env
212 }
213
214 pub fn mask_api_keys(&self) -> Self {
216 let mut masked = self.clone();
217
218 if let Some(ref mut endpoint) = masked.endpoint {
219 if endpoint.api_key.is_some() {
220 endpoint.api_key = Some("••••••••".to_string());
221 }
222 }
223
224 masked
225 }
226
227 pub fn get_api_key(&self) -> Option<String> {
229 if let Some(ref endpoint) = self.endpoint {
231 if let Some(ref api_key) = endpoint.api_key {
232 return Some(api_key.clone());
233 }
234 }
235
236 if let Ok(key) = std::env::var("CLAUDE_CODE_API_KEY") {
238 return Some(key);
239 }
240 if let Ok(key) = std::env::var("ANTHROPIC_API_KEY") {
241 return Some(key);
242 }
243
244 None
245 }
246}
247
248impl crate::Configurable for ClaudeSettings {
249 fn merge_with(mut self, other: Self) -> Self {
250 if other.provider.is_some() && self.provider.is_none() {
253 self.provider = other.provider;
254 }
255
256 if other.model.is_some() && self.model.is_none() {
257 self.model = other.model;
258 }
259
260 if other.endpoint.is_some() && self.endpoint.is_none() {
261 self.endpoint = other.endpoint;
262 }
263
264 if other.http.is_some() && self.http.is_none() {
265 self.http = other.http;
266 }
267
268 if other.permissions.is_some() && self.permissions.is_none() {
269 self.permissions = other.permissions;
270 }
271
272 if other.hooks.is_some() && self.hooks.is_none() {
273 self.hooks = other.hooks;
274 }
275
276 if other.status_line.is_some() && self.status_line.is_none() {
277 self.status_line = other.status_line;
278 }
279
280 if let Some(other_env) = other.environment {
282 let mut env = self.environment.unwrap_or_default();
283 env.extend(other_env);
284 self.environment = Some(env);
285 }
286
287 self
288 }
289
290 fn filter_by_scope(self, scope: &SnapshotScope) -> Self {
291 match scope {
292 SnapshotScope::Env => {
293 ClaudeSettings {
295 environment: self.environment,
296 ..Default::default()
297 }
298 }
299 SnapshotScope::Common => {
300 ClaudeSettings {
302 provider: self.provider,
303 model: self.model,
304 endpoint: self.endpoint,
305 http: self.http,
306 permissions: self.permissions,
307 hooks: self.hooks,
308 status_line: self.status_line,
309 environment: None,
310 }
311 }
312 SnapshotScope::All => self, }
314 }
315
316 fn mask_sensitive_data(self) -> Self {
317 self.mask_api_keys()
318 }
319}
320
321impl Default for ClaudeSettings {
322 fn default() -> Self {
323 Self::new()
324 }
325}
326
327pub fn merge_settings(settings: Vec<ClaudeSettings>) -> ClaudeSettings {
329 settings
330 .into_iter()
331 .fold(ClaudeSettings::new(), |acc, settings| {
332 settings.merge_with(acc)
333 })
334}
335
336pub fn format_settings_for_display(settings: &ClaudeSettings, verbose: bool) -> String {
338 let mut output = String::new();
339
340 if verbose {
341 output.push_str(&format!("{} Settings\n", style("Current").bold().cyan()));
342 output.push_str(&format!(
343 "{} {}\n",
344 style("Provider:").bold(),
345 settings
346 .provider
347 .as_ref()
348 .map(|p| &p.id)
349 .unwrap_or(&"None".to_string())
350 ));
351 output.push_str(&format!(
352 "{} {}\n",
353 style("Model:").bold(),
354 settings
355 .model
356 .as_ref()
357 .map(|m| &m.name)
358 .unwrap_or(&"None".to_string())
359 ));
360
361 if let Some(ref endpoint) = settings.endpoint {
362 output.push_str(&format!(
363 "{} {} ({})\n",
364 style("Endpoint:").bold(),
365 endpoint.id,
366 endpoint.api_base
367 ));
368 if let Some(ref api_key) = endpoint.api_key {
369 output.push_str(&format!(
370 "{} {}\n",
371 style("API Key:").bold(),
372 if api_key.len() > 8 {
373 format!("{}••••••••", &api_key[..8])
374 } else {
375 "••••••••".to_string()
376 }
377 ));
378 }
379 }
380
381 if let Some(ref http) = settings.http {
382 if let Some(timeout) = http.timeout_ms {
383 output.push_str(&format!("{} {}ms\n", style("Timeout:").bold(), timeout));
384 }
385 }
386 } else {
387 let provider = settings
388 .provider
389 .as_ref()
390 .map(|p| p.id.as_str())
391 .unwrap_or("None");
392 let model = settings
393 .model
394 .as_ref()
395 .map(|m| m.name.as_str())
396 .unwrap_or("None");
397
398 output.push_str(&format!(
399 "{}: {} | {}: {}\n",
400 style("Provider").bold(),
401 provider,
402 style("Model").bold(),
403 model
404 ));
405 }
406
407 output
408}