1mod custom_provider_config;
2mod hooks;
3mod paths;
4pub(crate) mod release_notes;
5mod settings;
6mod settings_storage;
7
8use crate::thinking::ThinkingLevel;
9pub(crate) use settings::DEFAULT_TUI_SUBAGENT_CARD_ROWS;
10pub(crate) use settings::{HeaderPanel, PanelLayoutSettings, set_panel_layout};
11use std::{collections::BTreeMap, env, fmt, io::IsTerminal};
12
13use crate::credentials::ProviderCredential;
14pub(crate) use custom_provider_config::{
15 CustomProviderConfig, CustomProviderHeaderValue, CustomReasoningProtocol,
16};
17pub(crate) use custom_provider_config::{
18 derive_custom_provider_id, looks_like_secret_value, make_custom_provider_config,
19 normalize_custom_provider_base_url, normalized_extra_models, validate_custom_provider_id,
20 validate_optional_env_var_name,
21};
22pub(crate) use hooks::{
23 HookDefinition, HookFailurePolicy, HookPayloadMode, HookSettings, InjectedContentSettings,
24 InjectedContentStyle,
25};
26pub(crate) use paths::McPaths;
27#[cfg(test)]
28pub(crate) use settings::profiling_support;
29pub(crate) use settings::set_selected_model;
30pub(crate) use settings::{
31 AnthropicCacheTtl, AstGrepToolSettings, AutoCompactionSettings, BashProtectionFailurePolicy,
32 BashProtectionLevel, BashProtectionSettings, BashToolSettings, CodeModeToolSettings,
33 CompactionSettings, CompactionTimingSettings, CompletionVerificationFailurePolicy,
34 CompletionVerificationSettings, FastSettings, FindToolSettings, GrepToolSettings,
35 HashEditToolSettings, HerdrSettings, HumanizeProtectionSettings, ListFilesToolSettings,
36 McpHttpServerConfig, McpOAuthConfig, McpServerConfig, McpServersSettings, McpStdioServerConfig,
37 PromptInjectionFailurePolicy, PromptInjectionProtectionSettings, ReadToolSettings, Settings,
38 SideAgentSettings, SkillSuggestionSettings, TextVerbosity, ToolSettings, ViewImageToolSettings,
39 ViewImageVisionModelSettings, WriteToolSettings,
40};
41pub(crate) use settings::{AppearanceSettings, AutoCompactionLimit, set_appearance_theme};
42pub(crate) use settings::{
43 CompactionConfig, SessionTitleConfig, clamp_subagent_max_depth, validate_mcp_http_url_field,
44 validate_mcp_server_name, validate_view_image_identifier, validate_view_image_max_image_bytes,
45};
46pub(crate) use settings::{DEFAULT_MCP_TIMEOUT_SECONDS, SettingsListKind, SettingsScope};
47pub(crate) use settings::{DEFAULT_VIEW_IMAGE_MAX_IMAGE_BYTES, MAX_VIEW_IMAGE_MAX_IMAGE_BYTES};
48pub(crate) use settings::{McpApprovalIdentity, McpApprovalSnapshot};
49pub(crate) use settings::{
50 disabled_skill_names_from_settings, disabled_subagent_profile_names_from_settings,
51 disabled_tool_names_from_settings, enabled_model_ids_from_settings,
52 load_startup_config_with_settings,
53};
54pub(crate) use settings::{
55 fast_mode_enabled, list_names_for_modal_scope, read_settings, remove_custom_provider,
56 set_fast_mode, set_mcp_server_enabled, set_model_enabled_for_scope, set_selected_primary_agent,
57 set_skill_disabled_for_scope, set_subagent_profile_disabled, set_tool_disabled,
58 toggle_fast_mode, upsert_custom_provider,
59};
60pub(crate) use settings::{load_settings_editor, save_settings_editor, set_code_mode_tool_enabled};
61
62pub(crate) fn skills_disabled() -> bool {
64 env::var_os("MC_DISABLE_SKILLS").is_some()
65}
66
67pub(crate) fn mcp_disabled() -> bool {
68 env::var_os("MC_DISABLE_MCP").is_some()
69}
70
71pub(crate) fn subagents_disabled() -> bool {
72 env::var_os("MC_DISABLE_SUBAGENTS").is_some()
73}
74
75#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, clap::ValueEnum)]
76pub(crate) enum ColorChoice {
77 #[default]
78 Auto,
79 Always,
80 Never,
81}
82
83#[derive(Debug, Clone, Default, PartialEq, Eq)]
84pub(crate) struct CliConfigOverrides {
85 pub(crate) provider: Option<String>,
86 pub(crate) model: Option<String>,
87 pub(crate) api_key: Option<String>,
88 pub(crate) color: Option<ColorChoice>,
89}
90
91#[derive(Clone, PartialEq, Eq)]
92pub(crate) struct EffectiveConfig {
93 pub(crate) provider: Option<String>,
94 pub(crate) model: Option<String>,
95 pub(crate) no_color: bool,
96 pub(crate) file_autocomplete_respects_gitignore: bool,
97 pub(crate) custom_providers: BTreeMap<String, CustomProviderConfig>,
98 pub(crate) thinking_level: ThinkingLevel,
99 pub(crate) auth: Option<ProviderCredential>,
100 pub(crate) paths: McPaths,
101}
102
103impl fmt::Debug for EffectiveConfig {
104 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105 f.debug_struct("EffectiveConfig")
106 .field("provider", &self.provider)
107 .field("model", &self.model)
108 .field("no_color", &self.no_color)
109 .field(
110 "file_autocomplete_respects_gitignore",
111 &self.file_autocomplete_respects_gitignore,
112 )
113 .field("custom_providers", &self.custom_providers)
114 .field("auth", &self.auth)
115 .field("thinking_level", &self.thinking_level)
116 .field("paths", &self.paths)
117 .finish()
118 }
119}
120
121impl EffectiveConfig {
122 pub(crate) fn selected_provider_model(settings: &Settings) -> (Option<String>, Option<String>) {
123 (
124 env::var("MC_PROVIDER")
125 .ok()
126 .or_else(|| settings.selected_model.provider.clone()),
127 env::var("MC_MODEL")
128 .ok()
129 .or_else(|| settings.selected_model.model.clone()),
130 )
131 }
132
133 pub(crate) fn missing_auth_error(&self) -> ConfigError {
134 ConfigError::missing_auth_for_custom_providers(
135 self.provider_id(),
136 &self.custom_providers,
137 &self.paths.auth_file,
138 )
139 }
140
141 pub(crate) fn provider_id(&self) -> &str {
142 self.provider
143 .as_deref()
144 .unwrap_or(crate::provider_ids::OPENAI_CODEX_PROVIDER)
145 }
146}
147
148pub(crate) fn reject_retired_provider_selection(provider: &str, model: &str) -> anyhow::Result<()> {
149 if provider != "claude-code" {
150 return Ok(());
151 }
152 let model = if model.trim().is_empty() {
153 "<model>"
154 } else {
155 model
156 };
157 anyhow::bail!(
158 "stale provider selection 'claude-code/{model}': the Claude Code provider was removed; select 'anthropic/{model}' instead, then set ANTHROPIC_API_KEY or configure a provider-keyed 'anthropic' API key; Claude Code OAuth/subscription credentials are not reused"
159 );
160}
161
162impl ConfigError {
163 pub(crate) fn missing_auth_for_custom_providers(
164 provider: &str,
165 custom_providers: &BTreeMap<String, CustomProviderConfig>,
166 auth_file: &std::path::Path,
167 ) -> Self {
168 missing_auth_error(provider, custom_providers.get(provider), auth_file)
169 }
170}
171
172fn missing_auth_error(
173 provider: &str,
174 custom: Option<&CustomProviderConfig>,
175 auth_file: &std::path::Path,
176) -> ConfigError {
177 let auth_path = auth_file.display();
178 let message = if let Some(custom) = custom {
179 match &custom.api_key_env_var {
180 Some(env_var) => format!(
181 "missing auth: custom provider '{provider}' is configured but environment variable {env_var} is missing or empty"
182 ),
183 None => format!(
184 "missing auth: custom provider '{provider}' is configured for no-auth but could not be prepared"
185 ),
186 }
187 } else if provider == crate::provider_ids::CLAUDE_SUBSCRIPTION_PROVIDER {
188 "Claude subscription unavailable: on Unix, install Claude Code and run `claude auth login`; API keys are not used".to_string()
189 } else if provider == crate::provider_ids::OPENAI_CODEX_PROVIDER {
190 format!(
191 "missing auth: missing OAuth auth or expired OAuth credentials without refresh for provider 'openai-codex'; needs re-login with /login openai-codex; --api-key, MC_API_KEY, and OPENAI_API_KEY are unsupported for openai-codex; OAuth auth includes access token and accountId in {auth_path}"
192 )
193 } else if provider == crate::provider_ids::ANTHROPIC_PROVIDER {
194 format!(
195 "missing auth: provider 'anthropic' requires an Anthropic API key; set ANTHROPIC_API_KEY or configure provider-keyed API-key auth in {auth_path}; OPENAI_API_KEY, MC_API_KEY, and --api-key are not used for Anthropic"
196 )
197 } else {
198 format!(
199 "missing auth for provider '{provider}'; configure provider-keyed auth in {auth_path}"
200 )
201 };
202 ConfigError::MissingAuth {
203 provider: provider.to_string(),
204 message,
205 }
206}
207
208pub(crate) fn resolve_output_style(
209 settings: &Settings,
210 cli_color: Option<ColorChoice>,
211) -> (bool, bool) {
212 let stdout_is_tty = std::io::stdout().is_terminal();
213 resolve_output_style_for_stdout(settings, cli_color, stdout_is_tty)
214}
215
216pub(crate) fn resolve_output_style_for_stdout(
217 settings: &Settings,
218 cli_color: Option<ColorChoice>,
219 stdout_is_tty: bool,
220) -> (bool, bool) {
221 let policy = crate::appearance::resolve_color_policy_from_env(
222 settings.no_color,
223 cli_color,
224 stdout_is_tty,
225 env::var_os("NO_COLOR").is_some(),
226 env::var("COLORTERM").ok().as_deref(),
227 env::var("TERM").ok().as_deref(),
228 );
229 (policy.color_enabled, policy.unicode_enabled)
230}
231#[derive(Debug, thiserror::Error, PartialEq, Eq)]
232pub(crate) enum ConfigError {
233 #[error("{message}")]
234 MissingAuth { provider: String, message: String },
235}
236
237pub(crate) fn load_context_budget(
238 config: &EffectiveConfig,
239) -> anyhow::Result<crate::context::ContextBudget> {
240 let settings = read_settings(&config.paths)?;
241 let mut budget = settings.context.unwrap_or_default();
242 let provider = config.provider_id();
243 let model = config
244 .model
245 .as_deref()
246 .unwrap_or_else(|| crate::providers::default_model_for_provider(provider));
247 if let Some(context_window) =
248 crate::model_catalog::cached_model_context_window(&config.paths, provider, model)
249 {
250 budget.max_tokens = context_window;
251 }
252 budget.apply_model_limits(provider, model);
253 Ok(budget)
254}