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    #[serde(default)]
90    pub account_uuid: String,
91    #[serde(default)]
92    pub email_address: String,
93    pub organization_uuid: Option<String>,
94    pub organization_name: Option<String>,
95    pub organization_role: Option<String>,
96    pub workspace_role: Option<String>,
97    pub display_name: Option<String>,
98    pub has_extra_usage_enabled: Option<bool>,
99    pub billing_type: Option<String>,
100    pub account_created_at: Option<String>,
101    pub subscription_created_at: Option<String>,
102}
103
104/// MCP server configuration
105#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
106pub struct McpServerConfig {
107    pub command: Option<String>,
108    pub args: Option<Vec<String>>,
109    pub env: Option<HashMap<String, String>>,
110}
111
112/// Project-specific configuration (all fields use camelCase in settings.json)
113#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
114#[serde(rename_all = "camelCase")]
115pub struct ProjectConfig {
116    #[serde(default)]
117    pub allowed_tools: Vec<String>,
118    #[serde(default)]
119    pub mcp_context_uris: Vec<String>,
120    pub mcp_servers: Option<HashMap<String, McpServerConfig>>,
121    pub last_api_duration: Option<u64>,
122    pub last_api_duration_without_retries: Option<u64>,
123    pub last_tool_duration: Option<u64>,
124    pub last_cost: Option<f64>,
125    pub last_duration: Option<u64>,
126    pub last_lines_added: Option<u32>,
127    pub last_lines_removed: Option<u32>,
128    pub last_total_input_tokens: Option<u32>,
129    pub last_total_output_tokens: Option<u32>,
130    pub last_total_cache_creation_input_tokens: Option<u32>,
131    pub last_total_cache_read_input_tokens: Option<u32>,
132    pub last_total_web_search_requests: Option<u32>,
133    pub last_fps_average: Option<f64>,
134    pub last_fps_low_1_pct: Option<f64>,
135    pub last_session_id: Option<String>,
136    pub last_model_usage: Option<HashMap<String, ModelUsage>>,
137    pub last_session_metrics: Option<HashMap<String, f64>>,
138    pub example_files: Option<Vec<String>>,
139    pub example_files_generated_at: Option<u64>,
140    #[serde(default)]
141    pub has_trust_dialog_accepted: bool,
142    #[serde(default)]
143    pub has_completed_project_onboarding: bool,
144    #[serde(default)]
145    pub project_onboarding_seen_count: u32,
146    #[serde(default)]
147    pub has_claude_md_external_includes_approved: bool,
148    #[serde(default)]
149    pub has_claude_md_external_includes_warning_shown: bool,
150    pub enabled_mcpjson_servers: Option<Vec<String>>,
151    pub disabled_mcpjson_servers: Option<Vec<String>>,
152    pub enable_all_project_mcp_servers: Option<bool>,
153    pub disabled_mcp_servers: Option<Vec<String>>,
154    pub enabled_mcp_servers: Option<Vec<String>>,
155    pub active_worktree_session: Option<WorktreeSession>,
156    pub remote_control_spawn_mode: Option<String>,
157}
158
159/// Model usage statistics
160#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
161pub struct ModelUsage {
162    #[serde(default)]
163    pub input_tokens: u32,
164    #[serde(default)]
165    pub output_tokens: u32,
166    #[serde(default)]
167    pub cache_read_input_tokens: u32,
168    #[serde(default)]
169    pub cache_creation_input_tokens: u32,
170    #[serde(default)]
171    pub web_search_requests: u32,
172    #[serde(default)]
173    pub cost_usd: f64,
174}
175
176/// Worktree session information
177#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
178pub struct WorktreeSession {
179    #[serde(default)]
180    pub original_cwd: String,
181    #[serde(default)]
182    pub worktree_path: String,
183    #[serde(default)]
184    pub worktree_name: String,
185    pub original_branch: Option<String>,
186    pub session_id: String,
187    pub hook_based: Option<bool>,
188}
189
190/// Global application configuration
191/// Note: All fields are serialized to camelCase in settings.json
192#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
193#[serde(rename_all = "camelCase")]
194pub struct GlobalConfig {
195    pub api_key_helper: Option<String>,
196    pub projects: Option<HashMap<String, ProjectConfig>>,
197    #[serde(default)]
198    pub num_startups: u32,
199    pub install_method: Option<InstallMethod>,
200    pub auto_updates: Option<bool>,
201    pub auto_updates_protected_for_native: Option<bool>,
202    pub doctor_shown_at_session: Option<u32>,
203    pub user_id: Option<String>,
204    #[serde(default)]
205    pub theme: Option<ThemeSetting>,
206    pub has_completed_onboarding: Option<bool>,
207    pub last_onboarding_version: Option<String>,
208    pub last_release_notes_seen: Option<String>,
209    pub changelog_last_fetched: Option<u64>,
210    pub cached_changelog: Option<String>,
211    pub mcp_servers: Option<HashMap<String, McpServerConfig>>,
212    pub claude_ai_mcp_ever_connected: Option<Vec<String>>,
213    #[serde(default)]
214    pub preferred_notif_channel: NotificationChannel,
215    pub custom_notify_command: Option<String>,
216    #[serde(default)]
217    pub verbose: Option<bool>,
218    pub custom_api_key_responses: Option<CustomApiKeyResponses>,
219    pub primary_api_key: Option<String>,
220    pub has_acknowledged_cost_threshold: Option<bool>,
221    pub has_seen_undercover_auto_notice: Option<bool>,
222    pub has_seen_ultraplan_terms: Option<bool>,
223    pub has_reset_auto_mode_opt_in_for_default_offer: Option<bool>,
224    pub oauth_account: Option<AccountInfo>,
225    pub iterm2_key_binding_installed: Option<bool>,
226    pub editor_mode: Option<EditorMode>,
227    pub bypass_permissions_mode_accepted: Option<bool>,
228    pub has_used_backslash_return: Option<bool>,
229    #[serde(default)]
230    pub auto_compact_enabled: bool,
231    #[serde(default)]
232    pub show_turn_duration: bool,
233    #[serde(default)]
234    pub env: HashMap<String, String>,
235    pub has_seen_tasks_hint: Option<bool>,
236    pub has_used_stash: Option<bool>,
237    pub has_used_background_task: Option<bool>,
238    pub queued_command_up_hint_count: Option<u32>,
239    pub diff_tool: Option<DiffTool>,
240    pub iterm2_setup_in_progress: Option<bool>,
241    pub iterm2_backup_path: Option<String>,
242    pub apple_terminal_backup_path: Option<String>,
243    pub apple_terminal_setup_in_progress: Option<bool>,
244    pub shift_enter_key_binding_installed: Option<bool>,
245    pub option_as_meta_key_installed: Option<bool>,
246    pub auto_connect_ide: Option<bool>,
247    pub auto_install_ide_extension: Option<bool>,
248    pub has_ide_onboarding_been_shown: Option<HashMap<String, bool>>,
249    pub ide_hint_shown_count: Option<u32>,
250    pub has_ide_auto_connect_dialog_been_shown: Option<bool>,
251    #[serde(default)]
252    pub tips_history: HashMap<String, u32>,
253    pub companion: Option<serde_json::Value>,
254    pub companion_muted: Option<bool>,
255    pub feedback_survey_state: Option<FeedbackSurveyState>,
256    pub transcript_share_dismissed: Option<bool>,
257    #[serde(default)]
258    pub memory_usage_count: u32,
259    pub has_shown_s1m_welcome_v2: Option<HashMap<String, bool>>,
260    pub s1m_access_cache: Option<HashMap<String, S1mAccessCacheEntry>>,
261    pub s1m_non_subscriber_access_cache: Option<HashMap<String, S1mAccessCacheEntry>>,
262    pub passes_eligibility_cache: Option<HashMap<String, serde_json::Value>>,
263    pub grove_config_cache: Option<HashMap<String, GroveConfigCacheEntry>>,
264    pub passes_upsell_seen_count: Option<u32>,
265    pub has_visited_passes: Option<bool>,
266    pub passes_last_seen_remaining: Option<u32>,
267    pub overage_credit_grant_cache: Option<HashMap<String, OverageCreditCacheEntry>>,
268    pub overage_credit_upsell_seen_count: Option<u32>,
269    pub has_visited_extra_usage: Option<bool>,
270    pub voice_notice_seen_count: Option<u32>,
271    pub voice_lang_hint_shown_count: Option<u32>,
272    pub voice_lang_hint_last_language: Option<String>,
273    pub voice_footer_hint_seen_count: Option<u32>,
274    pub opus_1m_merge_notice_seen_count: Option<u32>,
275    pub experiment_notices_seen_count: Option<HashMap<String, u32>>,
276    pub has_shown_opus_plan_welcome: Option<HashMap<String, bool>>,
277    #[serde(default)]
278    pub prompt_queue_use_count: u32,
279    #[serde(default)]
280    pub btw_use_count: u32,
281    pub last_plan_mode_use: Option<u64>,
282    pub subscription_notice_count: Option<u32>,
283    pub has_available_subscription: Option<bool>,
284    pub subscription_upsell_seen_count: Option<u32>,
285    pub recommended_subscription: Option<String>,
286    #[serde(default)]
287    pub todo_feature_enabled: bool,
288    pub show_expanded_todos: Option<bool>,
289    pub show_spinner_tree: Option<bool>,
290    pub first_start_time: Option<String>,
291    #[serde(default)]
292    pub message_idle_notif_threshold_ms: u64,
293    pub github_action_setup_count: Option<u32>,
294    pub slack_app_install_count: Option<u32>,
295    #[serde(default)]
296    pub file_checkpointing_enabled: bool,
297    #[serde(default)]
298    pub terminal_progress_bar_enabled: bool,
299    pub show_status_in_terminal_tab: Option<bool>,
300    pub task_complete_notif_enabled: Option<bool>,
301    pub input_needed_notif_enabled: Option<bool>,
302    pub agent_push_notif_enabled: Option<bool>,
303    pub claude_code_first_token_date: Option<String>,
304    pub model_switch_callout_dismissed: Option<bool>,
305    pub model_switch_callout_last_shown: Option<u64>,
306    pub model_switch_callout_version: Option<String>,
307    pub effort_callout_dismissed: Option<bool>,
308    pub effort_callout_v2_dismissed: Option<bool>,
309    pub remote_dialog_seen: Option<bool>,
310    pub bridge_oauth_dead_expires_at: Option<u64>,
311    pub bridge_oauth_dead_fail_count: Option<u32>,
312    pub desktop_upsell_seen_count: Option<u32>,
313    pub desktop_upsell_dismissed: Option<bool>,
314    pub idle_return_dismissed: Option<bool>,
315    pub opus_pro_migration_complete: Option<bool>,
316    pub opus_pro_migration_timestamp: Option<u64>,
317    pub sonnet_1m_45_migration_complete: Option<bool>,
318    pub legacy_opus_migration_timestamp: Option<u64>,
319    pub sonnet_45_to_46_migration_timestamp: Option<u64>,
320    #[serde(default)]
321    pub cached_statsig_gates: HashMap<String, bool>,
322    pub cached_dynamic_configs: Option<HashMap<String, serde_json::Value>>,
323    pub cached_growth_book_features: Option<HashMap<String, serde_json::Value>>,
324    pub growth_book_overrides: Option<HashMap<String, serde_json::Value>>,
325    pub last_shown_emergency_tip: Option<String>,
326    #[serde(default)]
327    pub respect_gitignore: bool,
328    #[serde(default)]
329    pub copy_full_response: bool,
330    pub copy_on_select: Option<bool>,
331    pub github_repo_paths: Option<HashMap<String, Vec<String>>>,
332    pub deep_link_terminal: Option<String>,
333    pub iterm2_it2_setup_complete: Option<bool>,
334    pub prefer_tmux_over_iterm2: Option<bool>,
335    pub skill_usage: Option<HashMap<String, SkillUsageEntry>>,
336    pub official_marketplace_auto_install_attempted: Option<bool>,
337    pub official_marketplace_auto_installed: Option<bool>,
338    pub official_marketplace_auto_install_fail_reason: Option<String>,
339    pub official_marketplace_auto_install_retry_count: Option<u32>,
340    pub official_marketplace_auto_install_last_attempt_time: Option<u64>,
341    pub official_marketplace_auto_install_next_retry_time: Option<u64>,
342    pub has_completed_claude_in_chrome_onboarding: Option<bool>,
343    pub claude_in_chrome_default_enabled: Option<bool>,
344    pub cached_chrome_extension_installed: Option<bool>,
345    pub chrome_extension: Option<ChromeExtensionState>,
346    pub lsp_recommendation_disabled: Option<bool>,
347    pub lsp_recommendation_never_plugins: Option<Vec<String>>,
348    pub lsp_recommendation_ignored_count: Option<u32>,
349    pub claude_code_hints: Option<ClaudeCodeHints>,
350    pub permission_explainer_enabled: Option<bool>,
351    pub teammate_mode: Option<String>,
352    pub teammate_default_model: Option<String>,
353    pub pr_status_footer_enabled: Option<bool>,
354    pub tungsten_panel_visible: Option<bool>,
355    pub penguin_mode_org_enabled: Option<bool>,
356    pub startup_prefetched_at: Option<u64>,
357    pub remote_control_at_startup: Option<bool>,
358    pub cached_extra_usage_disabled_reason: Option<String>,
359    pub auto_permissions_notification_count: Option<u32>,
360    pub speculation_enabled: Option<bool>,
361    pub client_data_cache: Option<serde_json::Value>,
362    pub additional_model_options_cache: Option<Vec<serde_json::Value>>,
363    pub metrics_status_cache: Option<MetricsStatusCache>,
364    pub migration_version: Option<u32>,
365}
366
367/// Feedback survey state
368#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
369pub struct FeedbackSurveyState {
370    pub last_shown_time: Option<u64>,
371}
372
373/// S1M access cache entry
374#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
375pub struct S1mAccessCacheEntry {
376    pub has_access: bool,
377    pub has_access_not_as_default: Option<bool>,
378    pub timestamp: u64,
379}
380
381/// Grove config cache entry
382#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
383pub struct GroveConfigCacheEntry {
384    pub grove_enabled: bool,
385    pub timestamp: u64,
386}
387
388/// Overage credit cache entry
389#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
390pub struct OverageCreditCacheEntry {
391    pub info: OverageCreditInfo,
392    pub timestamp: u64,
393}
394
395/// Overage credit info
396#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
397pub struct OverageCreditInfo {
398    pub available: bool,
399    pub eligible: bool,
400    pub granted: bool,
401    pub amount_minor_units: Option<i64>,
402    pub currency: Option<String>,
403}
404
405/// Skill usage entry
406#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
407pub struct SkillUsageEntry {
408    pub usage_count: u32,
409    pub last_used_at: u64,
410}
411
412/// Chrome extension state
413#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
414pub struct ChromeExtensionState {
415    pub paired_device_id: Option<String>,
416    pub paired_device_name: Option<String>,
417}
418
419/// Claude code hints
420#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
421pub struct ClaudeCodeHints {
422    pub plugin: Option<Vec<String>>,
423    pub disabled: Option<bool>,
424}
425
426/// Metrics status cache
427#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
428pub struct MetricsStatusCache {
429    pub enabled: bool,
430    pub timestamp: u64,
431}
432
433/// Custom API key responses
434#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
435pub struct CustomApiKeyResponses {
436    pub approved: Option<Vec<String>>,
437    pub rejected: Option<Vec<String>>,
438}
439
440impl Default for GlobalConfig {
441    fn default() -> Self {
442        GlobalConfig {
443            num_startups: 0,
444            install_method: None,
445            auto_updates: None,
446            theme: Some(ThemeSetting::Dark),
447            preferred_notif_channel: NotificationChannel::Auto,
448            verbose: Some(false),
449            editor_mode: Some(EditorMode::Normal),
450            auto_compact_enabled: true,
451            show_turn_duration: true,
452            queued_command_up_hint_count: Some(0),
453            diff_tool: Some(DiffTool::Auto),
454            custom_api_key_responses: Some(CustomApiKeyResponses {
455                approved: Some(vec![]),
456                rejected: Some(vec![]),
457            }),
458            env: HashMap::new(),
459            tips_history: HashMap::new(),
460            memory_usage_count: 0,
461            prompt_queue_use_count: 0,
462            btw_use_count: 0,
463            todo_feature_enabled: true,
464            show_expanded_todos: Some(false),
465            message_idle_notif_threshold_ms: 60000,
466            auto_connect_ide: Some(false),
467            auto_install_ide_extension: Some(true),
468            file_checkpointing_enabled: true,
469            terminal_progress_bar_enabled: true,
470            cached_statsig_gates: HashMap::new(),
471            cached_dynamic_configs: Some(HashMap::new()),
472            cached_growth_book_features: Some(HashMap::new()),
473            respect_gitignore: true,
474            copy_full_response: false,
475            // All other fields are None/false/empty by default
476            api_key_helper: None,
477            projects: None,
478            auto_updates_protected_for_native: None,
479            doctor_shown_at_session: None,
480            user_id: None,
481            has_completed_onboarding: None,
482            last_onboarding_version: None,
483            last_release_notes_seen: None,
484            changelog_last_fetched: None,
485            cached_changelog: None,
486            mcp_servers: None,
487            claude_ai_mcp_ever_connected: None,
488            custom_notify_command: None,
489            primary_api_key: None,
490            has_acknowledged_cost_threshold: None,
491            has_seen_undercover_auto_notice: None,
492            has_seen_ultraplan_terms: None,
493            has_reset_auto_mode_opt_in_for_default_offer: None,
494            oauth_account: None,
495            iterm2_key_binding_installed: None,
496            bypass_permissions_mode_accepted: None,
497            has_used_backslash_return: None,
498            has_seen_tasks_hint: None,
499            has_used_stash: None,
500            has_used_background_task: None,
501            iterm2_setup_in_progress: None,
502            iterm2_backup_path: None,
503            apple_terminal_backup_path: None,
504            apple_terminal_setup_in_progress: None,
505            shift_enter_key_binding_installed: None,
506            option_as_meta_key_installed: None,
507            has_ide_onboarding_been_shown: None,
508            ide_hint_shown_count: None,
509            has_ide_auto_connect_dialog_been_shown: None,
510            companion: None,
511            companion_muted: None,
512            feedback_survey_state: None,
513            transcript_share_dismissed: None,
514            has_shown_s1m_welcome_v2: None,
515            s1m_access_cache: None,
516            s1m_non_subscriber_access_cache: None,
517            passes_eligibility_cache: None,
518            grove_config_cache: None,
519            passes_upsell_seen_count: None,
520            has_visited_passes: None,
521            passes_last_seen_remaining: None,
522            overage_credit_grant_cache: None,
523            overage_credit_upsell_seen_count: None,
524            has_visited_extra_usage: None,
525            voice_notice_seen_count: None,
526            voice_lang_hint_shown_count: None,
527            voice_lang_hint_last_language: None,
528            voice_footer_hint_seen_count: None,
529            opus_1m_merge_notice_seen_count: None,
530            experiment_notices_seen_count: None,
531            has_shown_opus_plan_welcome: None,
532            last_plan_mode_use: None,
533            subscription_notice_count: None,
534            has_available_subscription: None,
535            subscription_upsell_seen_count: None,
536            recommended_subscription: None,
537            show_spinner_tree: None,
538            first_start_time: None,
539            github_action_setup_count: None,
540            slack_app_install_count: None,
541            show_status_in_terminal_tab: None,
542            task_complete_notif_enabled: None,
543            input_needed_notif_enabled: None,
544            agent_push_notif_enabled: None,
545            claude_code_first_token_date: None,
546            model_switch_callout_dismissed: None,
547            model_switch_callout_last_shown: None,
548            model_switch_callout_version: None,
549            effort_callout_dismissed: None,
550            effort_callout_v2_dismissed: None,
551            remote_dialog_seen: None,
552            bridge_oauth_dead_expires_at: None,
553            bridge_oauth_dead_fail_count: None,
554            desktop_upsell_seen_count: None,
555            desktop_upsell_dismissed: None,
556            idle_return_dismissed: None,
557            opus_pro_migration_complete: None,
558            opus_pro_migration_timestamp: None,
559            sonnet_1m_45_migration_complete: None,
560            legacy_opus_migration_timestamp: None,
561            sonnet_45_to_46_migration_timestamp: None,
562            growth_book_overrides: None,
563            last_shown_emergency_tip: None,
564            copy_on_select: None,
565            github_repo_paths: None,
566            deep_link_terminal: None,
567            iterm2_it2_setup_complete: None,
568            prefer_tmux_over_iterm2: None,
569            skill_usage: None,
570            official_marketplace_auto_install_attempted: None,
571            official_marketplace_auto_installed: None,
572            official_marketplace_auto_install_fail_reason: None,
573            official_marketplace_auto_install_retry_count: None,
574            official_marketplace_auto_install_last_attempt_time: None,
575            official_marketplace_auto_install_next_retry_time: None,
576            has_completed_claude_in_chrome_onboarding: None,
577            claude_in_chrome_default_enabled: None,
578            cached_chrome_extension_installed: None,
579            chrome_extension: None,
580            lsp_recommendation_disabled: None,
581            lsp_recommendation_never_plugins: None,
582            lsp_recommendation_ignored_count: None,
583            claude_code_hints: None,
584            permission_explainer_enabled: None,
585            teammate_mode: None,
586            teammate_default_model: None,
587            pr_status_footer_enabled: None,
588            tungsten_panel_visible: None,
589            penguin_mode_org_enabled: None,
590            startup_prefetched_at: None,
591            remote_control_at_startup: None,
592            cached_extra_usage_disabled_reason: None,
593            auto_permissions_notification_count: None,
594            speculation_enabled: None,
595            client_data_cache: None,
596            additional_model_options_cache: None,
597            metrics_status_cache: None,
598            migration_version: None,
599        }
600    }
601}
602
603/// Get the global config file path
604pub fn get_global_config_path() -> PathBuf {
605    // Use AI_ prefix for localization (AI_CONFIG_DIR or CLAUDE_CONFIG_DIR)
606    let config_dir = std::env::var(ai::CONFIG_DIR)
607        .or_else(|_| std::env::var(ai::CLAUDE_CONFIG_DIR))
608        .unwrap_or_else(|_| {
609            dirs::home_dir()
610                .map(|h| h.join(".ai").to_string_lossy().to_string())
611                .unwrap_or_else(|| "~/.ai".to_string())
612        });
613
614    dirs::home_dir()
615        .map(|h| h.join(".ai.json"))
616        .unwrap_or_else(|| PathBuf::from(".ai.json"))
617}
618
619/// Load global config from file
620pub fn get_global_config() -> GlobalConfig {
621    let path = get_global_config_path();
622
623    if !path.exists() {
624        return GlobalConfig::default();
625    }
626
627    match fs::read_to_string(&path) {
628        Ok(content) => match serde_json::from_str::<GlobalConfig>(&content) {
629            Ok(config) => {
630                // Merge with defaults to ensure all fields are present
631                let mut default_config = GlobalConfig::default();
632                merge_config(&mut default_config, config);
633                default_config
634            }
635            Err(e) => {
636                eprintln!("Failed to parse config: {}", e);
637                GlobalConfig::default()
638            }
639        },
640        Err(e) => {
641            eprintln!("Failed to read config file: {}", e);
642            GlobalConfig::default()
643        }
644    }
645}
646
647/// Normalize a path for use as a JSON config key.
648/// Converts backslashes to forward slashes for consistent JSON serialization
649/// (Windows paths can be C:\path or C:/path depending on source).
650fn normalize_path_for_config_key(path: &str) -> String {
651    path.replace('\\', "/")
652}
653
654/// Find the git root for a given path (searches parents for .git directory)
655fn find_git_root(path: &str) -> Option<String> {
656    let mut current = std::path::Path::new(path);
657    loop {
658        if current.join(".git").exists() {
659            return Some(current.to_string_lossy().to_string());
660        }
661        match current.parent() {
662            Some(p) => current = p,
663            None => return None,
664        }
665    }
666}
667
668/// Resolve a worktree .git file to the main repo root (follows gitdir: pointing to common dir)
669fn resolve_canonical_git_root(git_root: &str) -> Option<String> {
670    let git_dir = std::path::Path::new(git_root).join(".git");
671    let git_dir_contents = std::fs::read_to_string(&git_dir).ok()?;
672    for line in git_dir_contents.lines() {
673        if line.starts_with("gitdir: ") {
674            let common_dir = &line[8..].trim_end_matches('/');
675            // The common dir is <main-repo>/.git/worktrees/<worktree-name>
676            // We want <main-repo>
677            if let Some(worktrees_idx) = common_dir.find("/worktrees/") {
678                return Some(common_dir[..worktrees_idx].to_string());
679            }
680            // Fallback: strip trailing /.git or /git-dir
681            let normalized = std::path::Path::new(common_dir)
682                .parent()?
683                .to_string_lossy()
684                .to_string();
685            return Some(normalized);
686        }
687    }
688    Some(git_root.to_string())
689}
690
691/// Find the canonical git repository root, resolving through worktrees.
692/// Unlike find_git_root (which returns the worktree directory), this returns
693/// the main repository root so all worktrees of the same repo map to the same project.
694fn find_canonical_git_root(start_path: &str) -> Option<String> {
695    let root = find_git_root(start_path)?;
696    Some(resolve_canonical_git_root(&root).unwrap_or(root))
697}
698
699/// Get the project path for config lookup (git root or cwd)
700fn get_project_path_for_config() -> String {
701    use crate::utils::cwd::get_original_cwd;
702
703    let original_cwd = get_original_cwd();
704    let original_cwd_str = original_cwd.to_string_lossy();
705
706    // Try git root first
707    if let Some(canonical) = find_canonical_git_root(&original_cwd_str) {
708        return normalize_path_for_config_key(&canonical);
709    }
710
711    // Fall back to original cwd
712    normalize_path_for_config_key(&original_cwd_str)
713}
714
715// Session-level trust cache: trust only transitions false->true during a session
716static SESSION_TRUST_ACCEPTED: std::sync::atomic::AtomicBool =
717    std::sync::atomic::AtomicBool::new(false);
718
719/// Check if trust dialog has been accepted for this session.
720/// Uses session-level cache (latched true) and checks global config paths.
721pub fn check_has_trust_dialog_accepted() -> bool {
722    // If session trust was already accepted, latch to true
723    if SESSION_TRUST_ACCEPTED.load(std::sync::atomic::Ordering::SeqCst) {
724        return true;
725    }
726
727    // Check session-level trust (for home directory case where trust is not persisted)
728    // Note: get_session_trust_accepted() requires bootstrap module which isn't available in SDK.
729    // SDK users set AI_CODE_SESSION_TRUST_ACCEPTED=1 to indicate trust was accepted.
730    if std::env::var("AI_CODE_SESSION_TRUST_ACCEPTED").as_deref() == Ok("1") {
731        SESSION_TRUST_ACCEPTED.store(true, std::sync::atomic::Ordering::SeqCst);
732        return true;
733    }
734
735    let config = get_global_config();
736
737    // Always check where trust would be saved (git root or original cwd)
738    // This is the primary location where trust is persisted by save_current_project_config
739    let project_path = get_project_path_for_config();
740    if let Some(projects) = &config.projects {
741        if let Some(project_config) = projects.get(&project_path) {
742            if project_config.has_trust_dialog_accepted {
743                SESSION_TRUST_ACCEPTED.store(true, std::sync::atomic::Ordering::SeqCst);
744                return true;
745            }
746        }
747    }
748
749    // Now check from current working directory and its parents
750    let cwd = crate::utils::cwd::get_cwd();
751    let cwd_str = cwd.to_string_lossy();
752    let mut current_path = normalize_path_for_config_key(&cwd_str);
753
754    loop {
755        if let Some(projects) = &config.projects {
756            if let Some(project_config) = projects.get(&current_path) {
757                if project_config.has_trust_dialog_accepted {
758                    SESSION_TRUST_ACCEPTED.store(true, std::sync::atomic::Ordering::SeqCst);
759                    return true;
760                }
761            }
762        }
763
764        // Move to parent directory
765        let parent_path = std::path::Path::new(&current_path)
766            .parent()
767            .map(|p| normalize_path_for_config_key(&p.to_string_lossy()));
768
769        match parent_path {
770            Some(parent) if parent != current_path => current_path = parent,
771            _ => break,
772        }
773    }
774
775    false
776}
777
778/// Merge loaded config with defaults
779fn merge_config(default: &mut GlobalConfig, loaded: GlobalConfig) {
780    // This manually merges fields, preferring loaded values over defaults
781    // for fields that are Some in loaded
782
783    macro_rules! merge_option {
784        ($field:ident) => {
785            if let Some(v) = loaded.$field {
786                default.$field = Some(v);
787            }
788        };
789    }
790
791    macro_rules! merge_hashmap {
792        ($field:ident) => {
793            if let Some(v) = loaded.$field {
794                default.$field = v;
795            }
796        };
797    }
798
799    merge_option!(api_key_helper);
800    merge_option!(projects);
801    default.num_startups = loaded.num_startups;
802    merge_option!(install_method);
803    merge_option!(auto_updates);
804    merge_option!(auto_updates_protected_for_native);
805    merge_option!(doctor_shown_at_session);
806    merge_option!(user_id);
807    default.theme = loaded.theme;
808    merge_option!(has_completed_onboarding);
809    merge_option!(last_onboarding_version);
810    merge_option!(last_release_notes_seen);
811    merge_option!(changelog_last_fetched);
812    merge_option!(cached_changelog);
813    merge_option!(mcp_servers);
814    merge_option!(claude_ai_mcp_ever_connected);
815    default.preferred_notif_channel = loaded.preferred_notif_channel;
816    merge_option!(custom_notify_command);
817    merge_option!(verbose);
818    merge_option!(custom_api_key_responses);
819    merge_option!(primary_api_key);
820    merge_option!(has_acknowledged_cost_threshold);
821    merge_option!(has_seen_undercover_auto_notice);
822    merge_option!(has_seen_ultraplan_terms);
823    merge_option!(has_reset_auto_mode_opt_in_for_default_offer);
824    merge_option!(oauth_account);
825    merge_option!(iterm2_key_binding_installed);
826    merge_option!(editor_mode);
827    merge_option!(bypass_permissions_mode_accepted);
828    merge_option!(has_used_backslash_return);
829    default.auto_compact_enabled = loaded.auto_compact_enabled;
830    default.show_turn_duration = loaded.show_turn_duration;
831    default.env = loaded.env;
832    merge_option!(has_seen_tasks_hint);
833    merge_option!(has_used_stash);
834    merge_option!(has_used_background_task);
835    merge_option!(queued_command_up_hint_count);
836    merge_option!(diff_tool);
837    merge_option!(iterm2_setup_in_progress);
838    merge_option!(iterm2_backup_path);
839    merge_option!(apple_terminal_backup_path);
840    merge_option!(apple_terminal_setup_in_progress);
841    merge_option!(shift_enter_key_binding_installed);
842    merge_option!(option_as_meta_key_installed);
843    merge_option!(auto_connect_ide);
844    merge_option!(auto_install_ide_extension);
845    merge_option!(has_ide_onboarding_been_shown);
846    merge_option!(ide_hint_shown_count);
847    merge_option!(has_ide_auto_connect_dialog_been_shown);
848    default.tips_history = loaded.tips_history;
849    merge_option!(companion);
850    merge_option!(companion_muted);
851    merge_option!(feedback_survey_state);
852    merge_option!(transcript_share_dismissed);
853    default.memory_usage_count = loaded.memory_usage_count;
854    merge_option!(has_shown_s1m_welcome_v2);
855    merge_option!(s1m_access_cache);
856    merge_option!(s1m_non_subscriber_access_cache);
857    merge_option!(passes_eligibility_cache);
858    merge_option!(grove_config_cache);
859    merge_option!(passes_upsell_seen_count);
860    merge_option!(has_visited_passes);
861    merge_option!(passes_last_seen_remaining);
862    merge_option!(overage_credit_grant_cache);
863    merge_option!(overage_credit_upsell_seen_count);
864    merge_option!(has_visited_extra_usage);
865    merge_option!(voice_notice_seen_count);
866    merge_option!(voice_lang_hint_shown_count);
867    merge_option!(voice_lang_hint_last_language);
868    merge_option!(voice_footer_hint_seen_count);
869    merge_option!(opus_1m_merge_notice_seen_count);
870    merge_option!(experiment_notices_seen_count);
871    merge_option!(has_shown_opus_plan_welcome);
872    default.prompt_queue_use_count = loaded.prompt_queue_use_count;
873    default.btw_use_count = loaded.btw_use_count;
874    merge_option!(last_plan_mode_use);
875    merge_option!(subscription_notice_count);
876    merge_option!(has_available_subscription);
877    merge_option!(subscription_upsell_seen_count);
878    merge_option!(recommended_subscription);
879    default.todo_feature_enabled = loaded.todo_feature_enabled;
880    merge_option!(show_expanded_todos);
881    merge_option!(show_spinner_tree);
882    merge_option!(first_start_time);
883    default.message_idle_notif_threshold_ms = loaded.message_idle_notif_threshold_ms;
884    merge_option!(github_action_setup_count);
885    merge_option!(slack_app_install_count);
886    default.file_checkpointing_enabled = loaded.file_checkpointing_enabled;
887    default.terminal_progress_bar_enabled = loaded.terminal_progress_bar_enabled;
888    merge_option!(show_status_in_terminal_tab);
889    merge_option!(task_complete_notif_enabled);
890    merge_option!(input_needed_notif_enabled);
891    merge_option!(agent_push_notif_enabled);
892    merge_option!(claude_code_first_token_date);
893    merge_option!(model_switch_callout_dismissed);
894    merge_option!(model_switch_callout_last_shown);
895    merge_option!(model_switch_callout_version);
896    merge_option!(effort_callout_dismissed);
897    merge_option!(effort_callout_v2_dismissed);
898    merge_option!(remote_dialog_seen);
899    merge_option!(bridge_oauth_dead_expires_at);
900    merge_option!(bridge_oauth_dead_fail_count);
901    merge_option!(desktop_upsell_seen_count);
902    merge_option!(desktop_upsell_dismissed);
903    merge_option!(idle_return_dismissed);
904    merge_option!(opus_pro_migration_complete);
905    merge_option!(opus_pro_migration_timestamp);
906    merge_option!(sonnet_1m_45_migration_complete);
907    merge_option!(legacy_opus_migration_timestamp);
908    merge_option!(sonnet_45_to_46_migration_timestamp);
909    default.cached_statsig_gates = loaded.cached_statsig_gates;
910    merge_option!(cached_dynamic_configs);
911    merge_option!(cached_growth_book_features);
912    merge_option!(growth_book_overrides);
913    merge_option!(last_shown_emergency_tip);
914    default.respect_gitignore = loaded.respect_gitignore;
915    default.copy_full_response = loaded.copy_full_response;
916    merge_option!(copy_on_select);
917    merge_option!(github_repo_paths);
918    merge_option!(deep_link_terminal);
919    merge_option!(iterm2_it2_setup_complete);
920    merge_option!(prefer_tmux_over_iterm2);
921    merge_option!(skill_usage);
922    merge_option!(official_marketplace_auto_install_attempted);
923    merge_option!(official_marketplace_auto_installed);
924    merge_option!(official_marketplace_auto_install_fail_reason);
925    merge_option!(official_marketplace_auto_install_retry_count);
926    merge_option!(official_marketplace_auto_install_last_attempt_time);
927    merge_option!(official_marketplace_auto_install_next_retry_time);
928    merge_option!(has_completed_claude_in_chrome_onboarding);
929    merge_option!(claude_in_chrome_default_enabled);
930    merge_option!(cached_chrome_extension_installed);
931    merge_option!(chrome_extension);
932    merge_option!(lsp_recommendation_disabled);
933    merge_option!(lsp_recommendation_never_plugins);
934    merge_option!(lsp_recommendation_ignored_count);
935    merge_option!(claude_code_hints);
936    merge_option!(permission_explainer_enabled);
937    merge_option!(teammate_mode);
938    merge_option!(teammate_default_model);
939    merge_option!(pr_status_footer_enabled);
940    merge_option!(tungsten_panel_visible);
941    merge_option!(penguin_mode_org_enabled);
942    merge_option!(startup_prefetched_at);
943    merge_option!(remote_control_at_startup);
944    merge_option!(cached_extra_usage_disabled_reason);
945    merge_option!(auto_permissions_notification_count);
946    merge_option!(speculation_enabled);
947    merge_option!(client_data_cache);
948    merge_option!(additional_model_options_cache);
949    merge_option!(metrics_status_cache);
950    merge_option!(migration_version);
951}
952
953/// Remove null and default values from JSON string (for .ai.json cleanliness)
954fn remove_nulls(json: &str) -> String {
955    let value: serde_json::Value = match serde_json::from_str(json) {
956        Ok(v) => v,
957        Err(_) => return json.to_string(),
958    };
959    let cleaned = remove_defaults_impl(&value);
960    serde_json::to_string_pretty(&cleaned)
961        .map(|s| s.to_string())
962        .unwrap_or_else(|_| json.to_string())
963}
964
965fn remove_defaults_impl(value: &serde_json::Value) -> serde_json::Value {
966    match value {
967        serde_json::Value::Object(map) => {
968            let filtered: serde_json::Map<String, serde_json::Value> = map
969                .iter()
970                .filter(|(_, v)| !is_default_value(v))
971                .map(|(k, v)| (k.clone(), remove_defaults_impl(v)))
972                .collect();
973            serde_json::Value::Object(filtered)
974        }
975        serde_json::Value::Array(arr) => {
976            serde_json::Value::Array(arr.iter().map(remove_defaults_impl).collect())
977        }
978        _ => value.clone(),
979    }
980}
981
982/// Check if a value is a default that should be stripped from settings.json
983fn is_default_value(value: &serde_json::Value) -> bool {
984    match value {
985        // Null values
986        serde_json::Value::Null => true,
987        // Boolean false
988        serde_json::Value::Bool(b) if !b => true,
989        // Number zero
990        serde_json::Value::Number(n) if n.as_i64() == Some(0) => true,
991        // Empty strings
992        serde_json::Value::String(s) if s.is_empty() => true,
993        // Empty arrays
994        serde_json::Value::Array(arr) if arr.is_empty() => true,
995        // Empty objects
996        serde_json::Value::Object(obj) if obj.is_empty() => true,
997        _ => false,
998    }
999}
1000
1001/// Save global config to file
1002pub fn save_global_config(config: &GlobalConfig) -> Result<(), String> {
1003    let path = get_global_config_path();
1004
1005    // Ensure the directory exists
1006    if let Some(parent) = path.parent() {
1007        fs::create_dir_all(parent)
1008            .map_err(|e| format!("Failed to create config directory: {}", e))?;
1009    }
1010
1011    let json = serde_json::to_string_pretty(config)
1012        .map_err(|e| format!("Failed to serialize config: {}", e))?;
1013
1014    // Remove null values from JSON
1015    let json = remove_nulls(&json);
1016
1017    fs::write(&path, json).map_err(|e| format!("Failed to write config file: {}", e))?;
1018
1019    // Update cache
1020    if let Ok(mut cache) = GLOBAL_CONFIG_CACHE.lock() {
1021        cache.config = Some(config.clone());
1022        cache.mtime = std::time::SystemTime::now()
1023            .duration_since(std::time::UNIX_EPOCH)
1024            .map(|d| d.as_millis() as u64)
1025            .unwrap_or(0);
1026    }
1027
1028    Ok(())
1029}
1030
1031/// Get project config for current directory
1032pub fn get_current_project_config() -> ProjectConfig {
1033    let global_config = get_global_config();
1034
1035    // Try to get project path from environment or use current directory
1036    let project_path = std::env::var(ai::PROJECT_PATH)
1037        .or_else(|_| std::env::var(ai::CLAUDE_PROJECT_PATH))
1038        .unwrap_or_else(|_err| {
1039            std::env::current_dir()
1040                .map(|p| p.to_string_lossy().to_string())
1041                .unwrap_or_default()
1042        });
1043
1044    global_config
1045        .projects
1046        .and_then(|p| p.get(&project_path).cloned())
1047        .unwrap_or_default()
1048}
1049
1050/// Save project config for current directory
1051pub fn save_current_project_config(config: ProjectConfig) -> Result<(), String> {
1052    let project_path = std::env::var(ai::PROJECT_PATH)
1053        .or_else(|_| std::env::var(ai::CLAUDE_PROJECT_PATH))
1054        .unwrap_or_else(|_| {
1055            std::env::current_dir()
1056                .map(|p| p.to_string_lossy().to_string())
1057                .unwrap_or_default()
1058        });
1059
1060    let mut global_config = get_global_config();
1061
1062    let projects = global_config.projects.get_or_insert_with(HashMap::new);
1063    projects.insert(project_path, config);
1064
1065    save_global_config(&global_config)
1066}
1067
1068/// Get or create user ID
1069pub fn get_or_create_user_id() -> String {
1070    let mut config = get_global_config();
1071
1072    if let Some(user_id) = &config.user_id {
1073        return user_id.clone();
1074    }
1075
1076    // Generate new user ID
1077    let user_id = uuid::Uuid::new_v4().to_string();
1078    config.user_id = Some(user_id.clone());
1079
1080    let _ = save_global_config(&config);
1081
1082    user_id
1083}
1084
1085/// Auto-updater disabled reason
1086#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1087#[serde(tag = "type", content = "envVar")]
1088pub enum AutoUpdaterDisabledReason {
1089    Development,
1090    Env { env_var: String },
1091    Config,
1092}
1093
1094/// Get auto-updater disabled reason
1095pub fn get_auto_updater_disabled_reason() -> Option<AutoUpdaterDisabledReason> {
1096    // Check for development mode
1097    if std::env::var(system::NODE_ENV)
1098        .map(|v| v == "development")
1099        .unwrap_or(false)
1100    {
1101        return Some(AutoUpdaterDisabledReason::Development);
1102    }
1103
1104    // Check for DISABLE_AUTOUPDATER env var (with AI_ prefix support)
1105    if std::env::var(ai::DISABLE_AUTOUPDATER)
1106        .or_else(|_| std::env::var(system::DISABLE_AUTOUPDATER))
1107        .map(|v| !v.is_empty() && v != "false")
1108        .unwrap_or(false)
1109    {
1110        return Some(AutoUpdaterDisabledReason::Env {
1111            env_var: "DISABLE_AUTOUPDATER".to_string(),
1112        });
1113    }
1114
1115    // Check config
1116    let config = get_global_config();
1117    if config.auto_updates == Some(false)
1118        && (config.install_method != Some(InstallMethod::Native)
1119            || config.auto_updates_protected_for_native != Some(true))
1120    {
1121        return Some(AutoUpdaterDisabledReason::Config);
1122    }
1123
1124    None
1125}
1126
1127/// Check if auto-updater is disabled
1128pub fn is_auto_updater_disabled() -> bool {
1129    get_auto_updater_disabled_reason().is_some()
1130}
1131
1132/// Get custom API key status
1133pub fn get_custom_api_key_status(truncated_api_key: &str) -> &'static str {
1134    let config = get_global_config();
1135
1136    if let Some(responses) = &config.custom_api_key_responses {
1137        if let Some(approved) = &responses.approved {
1138            if approved.contains(&truncated_api_key.to_string()) {
1139                return "approved";
1140            }
1141        }
1142        if let Some(rejected) = &responses.rejected {
1143            if rejected.contains(&truncated_api_key.to_string()) {
1144                return "rejected";
1145            }
1146        }
1147    }
1148
1149    "new"
1150}
1151
1152/// Record first start time
1153pub fn record_first_start_time() {
1154    let mut config = get_global_config();
1155
1156    if config.first_start_time.is_none() {
1157        config.first_start_time = Some(chrono::Utc::now().to_rfc3339());
1158        let _ = save_global_config(&config);
1159    }
1160}
1161
1162/// Complete the onboarding flow - marks onboarding as done
1163pub fn complete_onboarding(version: &str) {
1164    let mut config = get_global_config();
1165    config.has_completed_onboarding = Some(true);
1166    config.last_onboarding_version = Some(version.to_string());
1167    let _ = save_global_config(&config);
1168}
1169
1170#[cfg(test)]
1171mod tests {
1172    use super::*;
1173
1174    #[test]
1175    fn test_default_config() {
1176        let config = GlobalConfig::default();
1177        assert_eq!(config.num_startups, 0);
1178        assert_eq!(config.theme, Some(ThemeSetting::Dark));
1179        assert_eq!(config.verbose, Some(false));
1180        assert!(config.auto_compact_enabled);
1181    }
1182
1183    #[test]
1184    fn test_get_global_config_path() {
1185        let path = get_global_config_path();
1186        assert!(path.to_string_lossy().contains(".ai"));
1187    }
1188
1189    #[test]
1190    fn test_is_auto_updater_disabled() {
1191        // Without env vars set, should not be disabled
1192        let _ = is_auto_updater_disabled();
1193    }
1194
1195    #[test]
1196    fn test_get_custom_api_key_status() {
1197        assert_eq!(get_custom_api_key_status("test-key"), "new");
1198    }
1199}