Skip to main content

ai_agent/utils/
config.rs

1// Source: /data/home/swei/claudecode/openclaudecode/src/services/autoDream/config.ts
2//! Configuration management utilities
3//! Translated from /data/home/swei/claudecode/openclaudecode/src/utils/config.ts
4
5use crate::constants::env::{ai, system};
6use std::collections::HashMap;
7use std::fs;
8use std::path::PathBuf;
9use std::sync::Mutex;
10
11use once_cell::sync::Lazy;
12
13// Re-entrancy guard: prevents get_config -> log_event -> get_global_config -> get_config
14// infinite recursion when the config file is corrupted.
15static INSIDE_GET_CONFIG: Lazy<Mutex<bool>> = Lazy::new(|| Mutex::new(false));
16
17// Cache for global config
18static GLOBAL_CONFIG_CACHE: Lazy<Mutex<GlobalConfigCache>> =
19    Lazy::new(|| Mutex::new(GlobalConfigCache::default()));
20
21#[derive(Default)]
22struct GlobalConfigCache {
23    config: Option<GlobalConfig>,
24    mtime: u64,
25}
26
27/// Install method for the application
28#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
29#[serde(rename_all = "lowercase")]
30pub enum InstallMethod {
31    #[default]
32    Unknown,
33    Local,
34    Native,
35    Global,
36}
37
38/// Release channel
39#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
40#[serde(rename_all = "lowercase")]
41pub enum ReleaseChannel {
42    #[default]
43    Stable,
44    Latest,
45}
46
47/// Diff tool configuration
48#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
49#[serde(rename_all = "lowercase")]
50pub enum DiffTool {
51    #[default]
52    Auto,
53    Terminal,
54}
55
56/// Editor mode
57#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
58#[serde(rename_all = "lowercase")]
59pub enum EditorMode {
60    #[default]
61    Normal,
62    Emacs,
63    Vim,
64}
65
66/// Notification channel
67#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
68#[serde(rename_all = "kebab-case")]
69pub enum NotificationChannel {
70    #[default]
71    Auto,
72    Terminal,
73    Native,
74}
75
76/// Theme setting
77#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
78#[serde(rename_all = "lowercase")]
79pub enum ThemeSetting {
80    #[default]
81    Dark,
82    Light,
83    System,
84}
85
86/// Account info from OAuth
87#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
88pub struct AccountInfo {
89    pub account_uuid: String,
90    pub email_address: String,
91    pub organization_uuid: Option<String>,
92    pub organization_name: Option<String>,
93    pub organization_role: Option<String>,
94    pub workspace_role: Option<String>,
95    pub display_name: Option<String>,
96    pub has_extra_usage_enabled: Option<bool>,
97    pub billing_type: Option<String>,
98    pub account_created_at: Option<String>,
99    pub subscription_created_at: Option<String>,
100}
101
102/// MCP server configuration
103#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
104pub struct McpServerConfig {
105    pub command: Option<String>,
106    pub args: Option<Vec<String>>,
107    pub env: Option<HashMap<String, String>>,
108}
109
110/// Project-specific configuration
111#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
112pub struct ProjectConfig {
113    pub allowed_tools: Vec<String>,
114    pub mcp_context_uris: Vec<String>,
115    pub mcp_servers: Option<HashMap<String, McpServerConfig>>,
116    pub last_api_duration: Option<u64>,
117    pub last_api_duration_without_retries: Option<u64>,
118    pub last_tool_duration: Option<u64>,
119    pub last_cost: Option<f64>,
120    pub last_duration: Option<u64>,
121    pub last_lines_added: Option<u32>,
122    pub last_lines_removed: Option<u32>,
123    pub last_total_input_tokens: Option<u32>,
124    pub last_total_output_tokens: Option<u32>,
125    pub last_total_cache_creation_input_tokens: Option<u32>,
126    pub last_total_cache_read_input_tokens: Option<u32>,
127    pub last_total_web_search_requests: Option<u32>,
128    pub last_fps_average: Option<f64>,
129    pub last_fps_low_1_pct: Option<f64>,
130    pub last_session_id: Option<String>,
131    pub last_model_usage: Option<HashMap<String, ModelUsage>>,
132    pub last_session_metrics: Option<HashMap<String, f64>>,
133    pub example_files: Option<Vec<String>>,
134    pub example_files_generated_at: Option<u64>,
135    pub has_trust_dialog_accepted: bool,
136    pub has_completed_project_onboarding: bool,
137    pub project_onboarding_seen_count: u32,
138    pub has_claude_md_external_includes_approved: bool,
139    pub has_claude_md_external_includes_warning_shown: bool,
140    pub enabled_mcpjson_servers: Option<Vec<String>>,
141    pub disabled_mcpjson_servers: Option<Vec<String>>,
142    pub enable_all_project_mcp_servers: Option<bool>,
143    pub disabled_mcp_servers: Option<Vec<String>>,
144    pub enabled_mcp_servers: Option<Vec<String>>,
145    pub active_worktree_session: Option<WorktreeSession>,
146    pub remote_control_spawn_mode: Option<String>,
147}
148
149/// Model usage statistics
150#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
151pub struct ModelUsage {
152    pub input_tokens: u32,
153    pub output_tokens: u32,
154    pub cache_read_input_tokens: u32,
155    pub cache_creation_input_tokens: u32,
156    pub web_search_requests: u32,
157    pub cost_usd: f64,
158}
159
160/// Worktree session information
161#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
162pub struct WorktreeSession {
163    pub original_cwd: String,
164    pub worktree_path: String,
165    pub worktree_name: String,
166    pub original_branch: Option<String>,
167    pub session_id: String,
168    pub hook_based: Option<bool>,
169}
170
171/// Global application configuration
172#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
173pub struct GlobalConfig {
174    pub api_key_helper: Option<String>,
175    pub projects: Option<HashMap<String, ProjectConfig>>,
176    pub num_startups: u32,
177    pub install_method: Option<InstallMethod>,
178    pub auto_updates: Option<bool>,
179    pub auto_updates_protected_for_native: Option<bool>,
180    pub doctor_shown_at_session: Option<u32>,
181    pub user_id: Option<String>,
182    pub theme: ThemeSetting,
183    pub has_completed_onboarding: Option<bool>,
184    pub last_onboarding_version: Option<String>,
185    pub last_release_notes_seen: Option<String>,
186    pub changelog_last_fetched: Option<u64>,
187    pub cached_changelog: Option<String>,
188    pub mcp_servers: Option<HashMap<String, McpServerConfig>>,
189    pub claude_ai_mcp_ever_connected: Option<Vec<String>>,
190    pub preferred_notif_channel: NotificationChannel,
191    pub custom_notify_command: Option<String>,
192    pub verbose: bool,
193    pub custom_api_key_responses: Option<CustomApiKeyResponses>,
194    pub primary_api_key: Option<String>,
195    pub has_acknowledged_cost_threshold: Option<bool>,
196    pub has_seen_undercover_auto_notice: Option<bool>,
197    pub has_seen_ultraplan_terms: Option<bool>,
198    pub has_reset_auto_mode_opt_in_for_default_offer: Option<bool>,
199    pub oauth_account: Option<AccountInfo>,
200    pub iterm2_key_binding_installed: Option<bool>,
201    pub editor_mode: Option<EditorMode>,
202    pub bypass_permissions_mode_accepted: Option<bool>,
203    pub has_used_backslash_return: Option<bool>,
204    pub auto_compact_enabled: bool,
205    pub show_turn_duration: bool,
206    pub env: HashMap<String, String>,
207    pub has_seen_tasks_hint: Option<bool>,
208    pub has_used_stash: Option<bool>,
209    pub has_used_background_task: Option<bool>,
210    pub queued_command_up_hint_count: Option<u32>,
211    pub diff_tool: Option<DiffTool>,
212    pub iterm2_setup_in_progress: Option<bool>,
213    pub iterm2_backup_path: Option<String>,
214    pub apple_terminal_backup_path: Option<String>,
215    pub apple_terminal_setup_in_progress: Option<bool>,
216    pub shift_enter_key_binding_installed: Option<bool>,
217    pub option_as_meta_key_installed: Option<bool>,
218    pub auto_connect_ide: Option<bool>,
219    pub auto_install_ide_extension: Option<bool>,
220    pub has_ide_onboarding_been_shown: Option<HashMap<String, bool>>,
221    pub ide_hint_shown_count: Option<u32>,
222    pub has_ide_auto_connect_dialog_been_shown: Option<bool>,
223    pub tips_history: HashMap<String, u32>,
224    pub companion: Option<serde_json::Value>,
225    pub companion_muted: Option<bool>,
226    pub feedback_survey_state: Option<FeedbackSurveyState>,
227    pub transcript_share_dismissed: Option<bool>,
228    pub memory_usage_count: u32,
229    pub has_shown_s1m_welcome_v2: Option<HashMap<String, bool>>,
230    pub s1m_access_cache: Option<HashMap<String, S1mAccessCacheEntry>>,
231    pub s1m_non_subscriber_access_cache: Option<HashMap<String, S1mAccessCacheEntry>>,
232    pub passes_eligibility_cache: Option<HashMap<String, serde_json::Value>>,
233    pub grove_config_cache: Option<HashMap<String, GroveConfigCacheEntry>>,
234    pub passes_upsell_seen_count: Option<u32>,
235    pub has_visited_passes: Option<bool>,
236    pub passes_last_seen_remaining: Option<u32>,
237    pub overage_credit_grant_cache: Option<HashMap<String, OverageCreditCacheEntry>>,
238    pub overage_credit_upsell_seen_count: Option<u32>,
239    pub has_visited_extra_usage: Option<bool>,
240    pub voice_notice_seen_count: Option<u32>,
241    pub voice_lang_hint_shown_count: Option<u32>,
242    pub voice_lang_hint_last_language: Option<String>,
243    pub voice_footer_hint_seen_count: Option<u32>,
244    pub opus_1m_merge_notice_seen_count: Option<u32>,
245    pub experiment_notices_seen_count: Option<HashMap<String, u32>>,
246    pub has_shown_opus_plan_welcome: Option<HashMap<String, bool>>,
247    pub prompt_queue_use_count: u32,
248    pub btw_use_count: u32,
249    pub last_plan_mode_use: Option<u64>,
250    pub subscription_notice_count: Option<u32>,
251    pub has_available_subscription: Option<bool>,
252    pub subscription_upsell_seen_count: Option<u32>,
253    pub recommended_subscription: Option<String>,
254    pub todo_feature_enabled: bool,
255    pub show_expanded_todos: Option<bool>,
256    pub show_spinner_tree: Option<bool>,
257    pub first_start_time: Option<String>,
258    pub message_idle_notif_threshold_ms: u64,
259    pub github_action_setup_count: Option<u32>,
260    pub slack_app_install_count: Option<u32>,
261    pub file_checkpointing_enabled: bool,
262    pub terminal_progress_bar_enabled: bool,
263    pub show_status_in_terminal_tab: Option<bool>,
264    pub task_complete_notif_enabled: Option<bool>,
265    pub input_needed_notif_enabled: Option<bool>,
266    pub agent_push_notif_enabled: Option<bool>,
267    pub claude_code_first_token_date: Option<String>,
268    pub model_switch_callout_dismissed: Option<bool>,
269    pub model_switch_callout_last_shown: Option<u64>,
270    pub model_switch_callout_version: Option<String>,
271    pub effort_callout_dismissed: Option<bool>,
272    pub effort_callout_v2_dismissed: Option<bool>,
273    pub remote_dialog_seen: Option<bool>,
274    pub bridge_oauth_dead_expires_at: Option<u64>,
275    pub bridge_oauth_dead_fail_count: Option<u32>,
276    pub desktop_upsell_seen_count: Option<u32>,
277    pub desktop_upsell_dismissed: Option<bool>,
278    pub idle_return_dismissed: Option<bool>,
279    pub opus_pro_migration_complete: Option<bool>,
280    pub opus_pro_migration_timestamp: Option<u64>,
281    pub sonnet_1m_45_migration_complete: Option<bool>,
282    pub legacy_opus_migration_timestamp: Option<u64>,
283    pub sonnet_45_to_46_migration_timestamp: Option<u64>,
284    pub cached_statsig_gates: HashMap<String, bool>,
285    pub cached_dynamic_configs: Option<HashMap<String, serde_json::Value>>,
286    pub cached_growth_book_features: Option<HashMap<String, serde_json::Value>>,
287    pub growth_book_overrides: Option<HashMap<String, serde_json::Value>>,
288    pub last_shown_emergency_tip: Option<String>,
289    pub respect_gitignore: bool,
290    pub copy_full_response: bool,
291    pub copy_on_select: Option<bool>,
292    pub github_repo_paths: Option<HashMap<String, Vec<String>>>,
293    pub deep_link_terminal: Option<String>,
294    pub iterm2_it2_setup_complete: Option<bool>,
295    pub prefer_tmux_over_iterm2: Option<bool>,
296    pub skill_usage: Option<HashMap<String, SkillUsageEntry>>,
297    pub official_marketplace_auto_install_attempted: Option<bool>,
298    pub official_marketplace_auto_installed: Option<bool>,
299    pub official_marketplace_auto_install_fail_reason: Option<String>,
300    pub official_marketplace_auto_install_retry_count: Option<u32>,
301    pub official_marketplace_auto_install_last_attempt_time: Option<u64>,
302    pub official_marketplace_auto_install_next_retry_time: Option<u64>,
303    pub has_completed_claude_in_chrome_onboarding: Option<bool>,
304    pub claude_in_chrome_default_enabled: Option<bool>,
305    pub cached_chrome_extension_installed: Option<bool>,
306    pub chrome_extension: Option<ChromeExtensionState>,
307    pub lsp_recommendation_disabled: Option<bool>,
308    pub lsp_recommendation_never_plugins: Option<Vec<String>>,
309    pub lsp_recommendation_ignored_count: Option<u32>,
310    pub claude_code_hints: Option<ClaudeCodeHints>,
311    pub permission_explainer_enabled: Option<bool>,
312    pub teammate_mode: Option<String>,
313    pub teammate_default_model: Option<String>,
314    pub pr_status_footer_enabled: Option<bool>,
315    pub tungsten_panel_visible: Option<bool>,
316    pub penguin_mode_org_enabled: Option<bool>,
317    pub startup_prefetched_at: Option<u64>,
318    pub remote_control_at_startup: Option<bool>,
319    pub cached_extra_usage_disabled_reason: Option<String>,
320    pub auto_permissions_notification_count: Option<u32>,
321    pub speculation_enabled: Option<bool>,
322    pub client_data_cache: Option<serde_json::Value>,
323    pub additional_model_options_cache: Option<Vec<serde_json::Value>>,
324    pub metrics_status_cache: Option<MetricsStatusCache>,
325    pub migration_version: Option<u32>,
326}
327
328/// Feedback survey state
329#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
330pub struct FeedbackSurveyState {
331    pub last_shown_time: Option<u64>,
332}
333
334/// S1M access cache entry
335#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
336pub struct S1mAccessCacheEntry {
337    pub has_access: bool,
338    pub has_access_not_as_default: Option<bool>,
339    pub timestamp: u64,
340}
341
342/// Grove config cache entry
343#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
344pub struct GroveConfigCacheEntry {
345    pub grove_enabled: bool,
346    pub timestamp: u64,
347}
348
349/// Overage credit cache entry
350#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
351pub struct OverageCreditCacheEntry {
352    pub info: OverageCreditInfo,
353    pub timestamp: u64,
354}
355
356/// Overage credit info
357#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
358pub struct OverageCreditInfo {
359    pub available: bool,
360    pub eligible: bool,
361    pub granted: bool,
362    pub amount_minor_units: Option<i64>,
363    pub currency: Option<String>,
364}
365
366/// Skill usage entry
367#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
368pub struct SkillUsageEntry {
369    pub usage_count: u32,
370    pub last_used_at: u64,
371}
372
373/// Chrome extension state
374#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
375pub struct ChromeExtensionState {
376    pub paired_device_id: Option<String>,
377    pub paired_device_name: Option<String>,
378}
379
380/// Claude code hints
381#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
382pub struct ClaudeCodeHints {
383    pub plugin: Option<Vec<String>>,
384    pub disabled: Option<bool>,
385}
386
387/// Metrics status cache
388#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
389pub struct MetricsStatusCache {
390    pub enabled: bool,
391    pub timestamp: u64,
392}
393
394/// Custom API key responses
395#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
396pub struct CustomApiKeyResponses {
397    pub approved: Option<Vec<String>>,
398    pub rejected: Option<Vec<String>>,
399}
400
401impl Default for GlobalConfig {
402    fn default() -> Self {
403        GlobalConfig {
404            num_startups: 0,
405            install_method: None,
406            auto_updates: None,
407            theme: ThemeSetting::Dark,
408            preferred_notif_channel: NotificationChannel::Auto,
409            verbose: true,
410            editor_mode: Some(EditorMode::Normal),
411            auto_compact_enabled: true,
412            show_turn_duration: true,
413            queued_command_up_hint_count: Some(0),
414            diff_tool: Some(DiffTool::Auto),
415            custom_api_key_responses: Some(CustomApiKeyResponses {
416                approved: Some(vec![]),
417                rejected: Some(vec![]),
418            }),
419            env: HashMap::new(),
420            tips_history: HashMap::new(),
421            memory_usage_count: 0,
422            prompt_queue_use_count: 0,
423            btw_use_count: 0,
424            todo_feature_enabled: true,
425            show_expanded_todos: Some(false),
426            message_idle_notif_threshold_ms: 60000,
427            auto_connect_ide: Some(false),
428            auto_install_ide_extension: Some(true),
429            file_checkpointing_enabled: true,
430            terminal_progress_bar_enabled: true,
431            cached_statsig_gates: HashMap::new(),
432            cached_dynamic_configs: Some(HashMap::new()),
433            cached_growth_book_features: Some(HashMap::new()),
434            respect_gitignore: true,
435            copy_full_response: false,
436            // All other fields are None/false/empty by default
437            api_key_helper: None,
438            projects: None,
439            auto_updates_protected_for_native: None,
440            doctor_shown_at_session: None,
441            user_id: None,
442            has_completed_onboarding: None,
443            last_onboarding_version: None,
444            last_release_notes_seen: None,
445            changelog_last_fetched: None,
446            cached_changelog: None,
447            mcp_servers: None,
448            claude_ai_mcp_ever_connected: None,
449            custom_notify_command: None,
450            primary_api_key: None,
451            has_acknowledged_cost_threshold: None,
452            has_seen_undercover_auto_notice: None,
453            has_seen_ultraplan_terms: None,
454            has_reset_auto_mode_opt_in_for_default_offer: None,
455            oauth_account: None,
456            iterm2_key_binding_installed: None,
457            bypass_permissions_mode_accepted: None,
458            has_used_backslash_return: None,
459            has_seen_tasks_hint: None,
460            has_used_stash: None,
461            has_used_background_task: None,
462            iterm2_setup_in_progress: None,
463            iterm2_backup_path: None,
464            apple_terminal_backup_path: None,
465            apple_terminal_setup_in_progress: None,
466            shift_enter_key_binding_installed: None,
467            option_as_meta_key_installed: None,
468            has_ide_onboarding_been_shown: None,
469            ide_hint_shown_count: None,
470            has_ide_auto_connect_dialog_been_shown: None,
471            companion: None,
472            companion_muted: None,
473            feedback_survey_state: None,
474            transcript_share_dismissed: None,
475            has_shown_s1m_welcome_v2: None,
476            s1m_access_cache: None,
477            s1m_non_subscriber_access_cache: None,
478            passes_eligibility_cache: None,
479            grove_config_cache: None,
480            passes_upsell_seen_count: None,
481            has_visited_passes: None,
482            passes_last_seen_remaining: None,
483            overage_credit_grant_cache: None,
484            overage_credit_upsell_seen_count: None,
485            has_visited_extra_usage: None,
486            voice_notice_seen_count: None,
487            voice_lang_hint_shown_count: None,
488            voice_lang_hint_last_language: None,
489            voice_footer_hint_seen_count: None,
490            opus_1m_merge_notice_seen_count: None,
491            experiment_notices_seen_count: None,
492            has_shown_opus_plan_welcome: None,
493            last_plan_mode_use: None,
494            subscription_notice_count: None,
495            has_available_subscription: None,
496            subscription_upsell_seen_count: None,
497            recommended_subscription: None,
498            show_spinner_tree: None,
499            first_start_time: None,
500            github_action_setup_count: None,
501            slack_app_install_count: None,
502            show_status_in_terminal_tab: None,
503            task_complete_notif_enabled: None,
504            input_needed_notif_enabled: None,
505            agent_push_notif_enabled: None,
506            claude_code_first_token_date: None,
507            model_switch_callout_dismissed: None,
508            model_switch_callout_last_shown: None,
509            model_switch_callout_version: None,
510            effort_callout_dismissed: None,
511            effort_callout_v2_dismissed: None,
512            remote_dialog_seen: None,
513            bridge_oauth_dead_expires_at: None,
514            bridge_oauth_dead_fail_count: None,
515            desktop_upsell_seen_count: None,
516            desktop_upsell_dismissed: None,
517            idle_return_dismissed: None,
518            opus_pro_migration_complete: None,
519            opus_pro_migration_timestamp: None,
520            sonnet_1m_45_migration_complete: None,
521            legacy_opus_migration_timestamp: None,
522            sonnet_45_to_46_migration_timestamp: None,
523            growth_book_overrides: None,
524            last_shown_emergency_tip: None,
525            copy_on_select: None,
526            github_repo_paths: None,
527            deep_link_terminal: None,
528            iterm2_it2_setup_complete: None,
529            prefer_tmux_over_iterm2: None,
530            skill_usage: None,
531            official_marketplace_auto_install_attempted: None,
532            official_marketplace_auto_installed: None,
533            official_marketplace_auto_install_fail_reason: None,
534            official_marketplace_auto_install_retry_count: None,
535            official_marketplace_auto_install_last_attempt_time: None,
536            official_marketplace_auto_install_next_retry_time: None,
537            has_completed_claude_in_chrome_onboarding: None,
538            claude_in_chrome_default_enabled: None,
539            cached_chrome_extension_installed: None,
540            chrome_extension: None,
541            lsp_recommendation_disabled: None,
542            lsp_recommendation_never_plugins: None,
543            lsp_recommendation_ignored_count: None,
544            claude_code_hints: None,
545            permission_explainer_enabled: None,
546            teammate_mode: None,
547            teammate_default_model: None,
548            pr_status_footer_enabled: None,
549            tungsten_panel_visible: None,
550            penguin_mode_org_enabled: None,
551            startup_prefetched_at: None,
552            remote_control_at_startup: None,
553            cached_extra_usage_disabled_reason: None,
554            auto_permissions_notification_count: None,
555            speculation_enabled: None,
556            client_data_cache: None,
557            additional_model_options_cache: None,
558            metrics_status_cache: None,
559            migration_version: None,
560        }
561    }
562}
563
564/// Get the global config file path
565pub fn get_global_config_path() -> PathBuf {
566    // Use AI_ prefix for localization (AI_CONFIG_DIR or CLAUDE_CONFIG_DIR)
567    let config_dir = std::env::var(ai::CONFIG_DIR)
568        .or_else(|_| std::env::var(ai::CLAUDE_CONFIG_DIR))
569        .unwrap_or_else(|_| {
570            dirs::home_dir()
571                .map(|h| h.join(".claude").to_string_lossy().to_string())
572                .unwrap_or_else(|| "~/.claude".to_string())
573        });
574
575    PathBuf::from(config_dir).join("settings.json")
576}
577
578/// Load global config from file
579pub fn get_global_config() -> GlobalConfig {
580    let path = get_global_config_path();
581
582    if !path.exists() {
583        return GlobalConfig::default();
584    }
585
586    match fs::read_to_string(&path) {
587        Ok(content) => match serde_json::from_str::<GlobalConfig>(&content) {
588            Ok(config) => {
589                // Merge with defaults to ensure all fields are present
590                let mut default_config = GlobalConfig::default();
591                merge_config(&mut default_config, config);
592                default_config
593            }
594            Err(e) => {
595                eprintln!("Failed to parse config: {}", e);
596                GlobalConfig::default()
597            }
598        },
599        Err(e) => {
600            eprintln!("Failed to read config file: {}", e);
601            GlobalConfig::default()
602        }
603    }
604}
605
606/// Merge loaded config with defaults
607fn merge_config(default: &mut GlobalConfig, loaded: GlobalConfig) {
608    // This manually merges fields, preferring loaded values over defaults
609    // for fields that are Some in loaded
610
611    macro_rules! merge_option {
612        ($field:ident) => {
613            if let Some(v) = loaded.$field {
614                default.$field = Some(v);
615            }
616        };
617    }
618
619    macro_rules! merge_hashmap {
620        ($field:ident) => {
621            if let Some(v) = loaded.$field {
622                default.$field = v;
623            }
624        };
625    }
626
627    merge_option!(api_key_helper);
628    merge_option!(projects);
629    default.num_startups = loaded.num_startups;
630    merge_option!(install_method);
631    merge_option!(auto_updates);
632    merge_option!(auto_updates_protected_for_native);
633    merge_option!(doctor_shown_at_session);
634    merge_option!(user_id);
635    default.theme = loaded.theme;
636    merge_option!(has_completed_onboarding);
637    merge_option!(last_onboarding_version);
638    merge_option!(last_release_notes_seen);
639    merge_option!(changelog_last_fetched);
640    merge_option!(cached_changelog);
641    merge_option!(mcp_servers);
642    merge_option!(claude_ai_mcp_ever_connected);
643    default.preferred_notif_channel = loaded.preferred_notif_channel;
644    merge_option!(custom_notify_command);
645    default.verbose = loaded.verbose;
646    merge_option!(custom_api_key_responses);
647    merge_option!(primary_api_key);
648    merge_option!(has_acknowledged_cost_threshold);
649    merge_option!(has_seen_undercover_auto_notice);
650    merge_option!(has_seen_ultraplan_terms);
651    merge_option!(has_reset_auto_mode_opt_in_for_default_offer);
652    merge_option!(oauth_account);
653    merge_option!(iterm2_key_binding_installed);
654    merge_option!(editor_mode);
655    merge_option!(bypass_permissions_mode_accepted);
656    merge_option!(has_used_backslash_return);
657    default.auto_compact_enabled = loaded.auto_compact_enabled;
658    default.show_turn_duration = loaded.show_turn_duration;
659    default.env = loaded.env;
660    merge_option!(has_seen_tasks_hint);
661    merge_option!(has_used_stash);
662    merge_option!(has_used_background_task);
663    merge_option!(queued_command_up_hint_count);
664    merge_option!(diff_tool);
665    merge_option!(iterm2_setup_in_progress);
666    merge_option!(iterm2_backup_path);
667    merge_option!(apple_terminal_backup_path);
668    merge_option!(apple_terminal_setup_in_progress);
669    merge_option!(shift_enter_key_binding_installed);
670    merge_option!(option_as_meta_key_installed);
671    merge_option!(auto_connect_ide);
672    merge_option!(auto_install_ide_extension);
673    merge_option!(has_ide_onboarding_been_shown);
674    merge_option!(ide_hint_shown_count);
675    merge_option!(has_ide_auto_connect_dialog_been_shown);
676    default.tips_history = loaded.tips_history;
677    merge_option!(companion);
678    merge_option!(companion_muted);
679    merge_option!(feedback_survey_state);
680    merge_option!(transcript_share_dismissed);
681    default.memory_usage_count = loaded.memory_usage_count;
682    merge_option!(has_shown_s1m_welcome_v2);
683    merge_option!(s1m_access_cache);
684    merge_option!(s1m_non_subscriber_access_cache);
685    merge_option!(passes_eligibility_cache);
686    merge_option!(grove_config_cache);
687    merge_option!(passes_upsell_seen_count);
688    merge_option!(has_visited_passes);
689    merge_option!(passes_last_seen_remaining);
690    merge_option!(overage_credit_grant_cache);
691    merge_option!(overage_credit_upsell_seen_count);
692    merge_option!(has_visited_extra_usage);
693    merge_option!(voice_notice_seen_count);
694    merge_option!(voice_lang_hint_shown_count);
695    merge_option!(voice_lang_hint_last_language);
696    merge_option!(voice_footer_hint_seen_count);
697    merge_option!(opus_1m_merge_notice_seen_count);
698    merge_option!(experiment_notices_seen_count);
699    merge_option!(has_shown_opus_plan_welcome);
700    default.prompt_queue_use_count = loaded.prompt_queue_use_count;
701    default.btw_use_count = loaded.btw_use_count;
702    merge_option!(last_plan_mode_use);
703    merge_option!(subscription_notice_count);
704    merge_option!(has_available_subscription);
705    merge_option!(subscription_upsell_seen_count);
706    merge_option!(recommended_subscription);
707    default.todo_feature_enabled = loaded.todo_feature_enabled;
708    merge_option!(show_expanded_todos);
709    merge_option!(show_spinner_tree);
710    merge_option!(first_start_time);
711    default.message_idle_notif_threshold_ms = loaded.message_idle_notif_threshold_ms;
712    merge_option!(github_action_setup_count);
713    merge_option!(slack_app_install_count);
714    default.file_checkpointing_enabled = loaded.file_checkpointing_enabled;
715    default.terminal_progress_bar_enabled = loaded.terminal_progress_bar_enabled;
716    merge_option!(show_status_in_terminal_tab);
717    merge_option!(task_complete_notif_enabled);
718    merge_option!(input_needed_notif_enabled);
719    merge_option!(agent_push_notif_enabled);
720    merge_option!(claude_code_first_token_date);
721    merge_option!(model_switch_callout_dismissed);
722    merge_option!(model_switch_callout_last_shown);
723    merge_option!(model_switch_callout_version);
724    merge_option!(effort_callout_dismissed);
725    merge_option!(effort_callout_v2_dismissed);
726    merge_option!(remote_dialog_seen);
727    merge_option!(bridge_oauth_dead_expires_at);
728    merge_option!(bridge_oauth_dead_fail_count);
729    merge_option!(desktop_upsell_seen_count);
730    merge_option!(desktop_upsell_dismissed);
731    merge_option!(idle_return_dismissed);
732    merge_option!(opus_pro_migration_complete);
733    merge_option!(opus_pro_migration_timestamp);
734    merge_option!(sonnet_1m_45_migration_complete);
735    merge_option!(legacy_opus_migration_timestamp);
736    merge_option!(sonnet_45_to_46_migration_timestamp);
737    default.cached_statsig_gates = loaded.cached_statsig_gates;
738    merge_option!(cached_dynamic_configs);
739    merge_option!(cached_growth_book_features);
740    merge_option!(growth_book_overrides);
741    merge_option!(last_shown_emergency_tip);
742    default.respect_gitignore = loaded.respect_gitignore;
743    default.copy_full_response = loaded.copy_full_response;
744    merge_option!(copy_on_select);
745    merge_option!(github_repo_paths);
746    merge_option!(deep_link_terminal);
747    merge_option!(iterm2_it2_setup_complete);
748    merge_option!(prefer_tmux_over_iterm2);
749    merge_option!(skill_usage);
750    merge_option!(official_marketplace_auto_install_attempted);
751    merge_option!(official_marketplace_auto_installed);
752    merge_option!(official_marketplace_auto_install_fail_reason);
753    merge_option!(official_marketplace_auto_install_retry_count);
754    merge_option!(official_marketplace_auto_install_last_attempt_time);
755    merge_option!(official_marketplace_auto_install_next_retry_time);
756    merge_option!(has_completed_claude_in_chrome_onboarding);
757    merge_option!(claude_in_chrome_default_enabled);
758    merge_option!(cached_chrome_extension_installed);
759    merge_option!(chrome_extension);
760    merge_option!(lsp_recommendation_disabled);
761    merge_option!(lsp_recommendation_never_plugins);
762    merge_option!(lsp_recommendation_ignored_count);
763    merge_option!(claude_code_hints);
764    merge_option!(permission_explainer_enabled);
765    merge_option!(teammate_mode);
766    merge_option!(teammate_default_model);
767    merge_option!(pr_status_footer_enabled);
768    merge_option!(tungsten_panel_visible);
769    merge_option!(penguin_mode_org_enabled);
770    merge_option!(startup_prefetched_at);
771    merge_option!(remote_control_at_startup);
772    merge_option!(cached_extra_usage_disabled_reason);
773    merge_option!(auto_permissions_notification_count);
774    merge_option!(speculation_enabled);
775    merge_option!(client_data_cache);
776    merge_option!(additional_model_options_cache);
777    merge_option!(metrics_status_cache);
778    merge_option!(migration_version);
779}
780
781/// Save global config to file
782pub fn save_global_config(config: &GlobalConfig) -> Result<(), String> {
783    let path = get_global_config_path();
784
785    // Ensure the directory exists
786    if let Some(parent) = path.parent() {
787        fs::create_dir_all(parent)
788            .map_err(|e| format!("Failed to create config directory: {}", e))?;
789    }
790
791    let json = serde_json::to_string_pretty(config)
792        .map_err(|e| format!("Failed to serialize config: {}", e))?;
793
794    fs::write(&path, json).map_err(|e| format!("Failed to write config file: {}", e))?;
795
796    // Update cache
797    if let Ok(mut cache) = GLOBAL_CONFIG_CACHE.lock() {
798        cache.config = Some(config.clone());
799        cache.mtime = std::time::SystemTime::now()
800            .duration_since(std::time::UNIX_EPOCH)
801            .map(|d| d.as_millis() as u64)
802            .unwrap_or(0);
803    }
804
805    Ok(())
806}
807
808/// Get project config for current directory
809pub fn get_current_project_config() -> ProjectConfig {
810    let global_config = get_global_config();
811
812    // Try to get project path from environment or use current directory
813    let project_path = std::env::var(ai::PROJECT_PATH)
814        .or_else(|_| std::env::var(ai::CLAUDE_PROJECT_PATH))
815        .unwrap_or_else(|_err| {
816            std::env::current_dir()
817                .map(|p| p.to_string_lossy().to_string())
818                .unwrap_or_default()
819        });
820
821    global_config
822        .projects
823        .and_then(|p| p.get(&project_path).cloned())
824        .unwrap_or_default()
825}
826
827/// Save project config for current directory
828pub fn save_current_project_config(config: ProjectConfig) -> Result<(), String> {
829    let project_path = std::env::var(ai::PROJECT_PATH)
830        .or_else(|_| std::env::var(ai::CLAUDE_PROJECT_PATH))
831        .unwrap_or_else(|_| {
832            std::env::current_dir()
833                .map(|p| p.to_string_lossy().to_string())
834                .unwrap_or_default()
835        });
836
837    let mut global_config = get_global_config();
838
839    let projects = global_config.projects.get_or_insert_with(HashMap::new);
840    projects.insert(project_path, config);
841
842    save_global_config(&global_config)
843}
844
845/// Get or create user ID
846pub fn get_or_create_user_id() -> String {
847    let mut config = get_global_config();
848
849    if let Some(user_id) = &config.user_id {
850        return user_id.clone();
851    }
852
853    // Generate new user ID
854    let user_id = uuid::Uuid::new_v4().to_string();
855    config.user_id = Some(user_id.clone());
856
857    let _ = save_global_config(&config);
858
859    user_id
860}
861
862/// Auto-updater disabled reason
863#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
864#[serde(tag = "type", content = "envVar")]
865pub enum AutoUpdaterDisabledReason {
866    Development,
867    Env { env_var: String },
868    Config,
869}
870
871/// Get auto-updater disabled reason
872pub fn get_auto_updater_disabled_reason() -> Option<AutoUpdaterDisabledReason> {
873    // Check for development mode
874    if std::env::var(system::NODE_ENV)
875        .map(|v| v == "development")
876        .unwrap_or(false)
877    {
878        return Some(AutoUpdaterDisabledReason::Development);
879    }
880
881    // Check for DISABLE_AUTOUPDATER env var (with AI_ prefix support)
882    if std::env::var(ai::DISABLE_AUTOUPDATER)
883        .or_else(|_| std::env::var(system::DISABLE_AUTOUPDATER))
884        .map(|v| !v.is_empty() && v != "false")
885        .unwrap_or(false)
886    {
887        return Some(AutoUpdaterDisabledReason::Env {
888            env_var: "DISABLE_AUTOUPDATER".to_string(),
889        });
890    }
891
892    // Check config
893    let config = get_global_config();
894    if config.auto_updates == Some(false)
895        && (config.install_method != Some(InstallMethod::Native)
896            || config.auto_updates_protected_for_native != Some(true))
897    {
898        return Some(AutoUpdaterDisabledReason::Config);
899    }
900
901    None
902}
903
904/// Check if auto-updater is disabled
905pub fn is_auto_updater_disabled() -> bool {
906    get_auto_updater_disabled_reason().is_some()
907}
908
909/// Get custom API key status
910pub fn get_custom_api_key_status(truncated_api_key: &str) -> &'static str {
911    let config = get_global_config();
912
913    if let Some(responses) = &config.custom_api_key_responses {
914        if let Some(approved) = &responses.approved {
915            if approved.contains(&truncated_api_key.to_string()) {
916                return "approved";
917            }
918        }
919        if let Some(rejected) = &responses.rejected {
920            if rejected.contains(&truncated_api_key.to_string()) {
921                return "rejected";
922            }
923        }
924    }
925
926    "new"
927}
928
929/// Record first start time
930pub fn record_first_start_time() {
931    let mut config = get_global_config();
932
933    if config.first_start_time.is_none() {
934        config.first_start_time = Some(chrono::Utc::now().to_rfc3339());
935        let _ = save_global_config(&config);
936    }
937}
938
939#[cfg(test)]
940mod tests {
941    use super::*;
942
943    #[test]
944    fn test_default_config() {
945        let config = GlobalConfig::default();
946        assert_eq!(config.num_startups, 0);
947        assert_eq!(config.theme, ThemeSetting::Dark);
948        assert!(config.verbose);
949        assert!(config.auto_compact_enabled);
950    }
951
952    #[test]
953    fn test_get_global_config_path() {
954        let path = get_global_config_path();
955        assert!(path.to_string_lossy().contains(".claude"));
956    }
957
958    #[test]
959    fn test_is_auto_updater_disabled() {
960        // Without env vars set, should not be disabled
961        let _ = is_auto_updater_disabled();
962    }
963
964    #[test]
965    fn test_get_custom_api_key_status() {
966        assert_eq!(get_custom_api_key_status("test-key"), "new");
967    }
968}