magi-code 0.100.0

Repository-aware CLI coding agent for terminal work
Documentation
mod custom_provider_config;
mod hooks;
mod paths;
pub(crate) mod release_notes;
mod settings;
mod settings_storage;

use crate::thinking::ThinkingLevel;
pub(crate) use settings::DEFAULT_TUI_SUBAGENT_CARD_ROWS;
pub(crate) use settings::{HeaderPanel, PanelLayoutSettings, set_panel_layout};
use std::{collections::BTreeMap, env, fmt, io::IsTerminal};

use crate::credentials::ProviderCredential;
pub(crate) use custom_provider_config::{
    CustomProviderConfig, CustomProviderHeaderValue, CustomReasoningProtocol,
};
pub(crate) use custom_provider_config::{
    derive_custom_provider_id, looks_like_secret_value, make_custom_provider_config,
    normalize_custom_provider_base_url, normalized_extra_models, validate_custom_provider_id,
    validate_optional_env_var_name,
};
pub(crate) use hooks::{
    HookDefinition, HookFailurePolicy, HookPayloadMode, HookSettings, InjectedContentSettings,
    InjectedContentStyle,
};
pub(crate) use paths::McPaths;
#[cfg(test)]
pub(crate) use settings::profiling_support;
pub(crate) use settings::set_selected_model;
pub(crate) use settings::{
    AnthropicCacheTtl, AstGrepToolSettings, AutoCompactionSettings, BashProtectionFailurePolicy,
    BashProtectionLevel, BashProtectionSettings, BashToolSettings, CodeModeToolSettings,
    CompactionSettings, CompactionTimingSettings, CompletionVerificationFailurePolicy,
    CompletionVerificationSettings, FastSettings, FindToolSettings, GrepToolSettings,
    HashEditToolSettings, HerdrSettings, HumanizeProtectionSettings, ListFilesToolSettings,
    LspServerConfig, LspSettings, McpHttpServerConfig, McpOAuthConfig, McpServerConfig,
    McpServersSettings, McpStdioServerConfig, PromptInjectionFailurePolicy,
    PromptInjectionProtectionSettings, ReadToolSettings, Settings, SideAgentSettings,
    SkillSuggestionSettings, TextVerbosity, ToolSettings, ViewImageToolSettings,
    ViewImageVisionModelSettings, WriteToolSettings,
};
pub(crate) use settings::{AppearanceSettings, AutoCompactionLimit, set_appearance_theme};
pub(crate) use settings::{
    CompactionConfig, SessionTitleConfig, clamp_subagent_max_depth, validate_mcp_http_url_field,
    validate_mcp_server_name, validate_view_image_identifier, validate_view_image_max_image_bytes,
};
pub(crate) use settings::{
    DEFAULT_MCP_TIMEOUT_SECONDS, DEFAULT_SESSION_RETENTION_DAYS, SettingsListKind, SettingsScope,
};
pub(crate) use settings::{DEFAULT_VIEW_IMAGE_MAX_IMAGE_BYTES, MAX_VIEW_IMAGE_MAX_IMAGE_BYTES};
pub(crate) use settings::{McpApprovalIdentity, McpApprovalSnapshot};
pub(crate) use settings::{
    disabled_skill_names_from_settings, disabled_subagent_profile_names_from_settings,
    disabled_tool_names_from_settings, enabled_model_ids_from_settings,
    load_startup_config_with_settings,
};
pub(crate) use settings::{
    fast_mode_enabled, list_names_for_modal_scope, read_settings, remove_custom_provider,
    set_fast_mode, set_mcp_server_enabled, set_model_enabled_for_scope, set_selected_primary_agent,
    set_skill_disabled_for_scope, set_subagent_profile_disabled, set_tool_disabled,
    toggle_fast_mode, upsert_custom_provider,
};
pub(crate) use settings::{load_settings_editor, save_settings_editor, set_code_mode_tool_enabled};

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, clap::ValueEnum)]
pub(crate) enum ColorChoice {
    #[default]
    Auto,
    Always,
    Never,
}

#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(crate) struct CliConfigOverrides {
    pub(crate) provider: Option<String>,
    pub(crate) model: Option<String>,
    pub(crate) api_key: Option<String>,
    pub(crate) color: Option<ColorChoice>,
}

#[derive(Clone, PartialEq, Eq)]
pub(crate) struct EffectiveConfig {
    pub(crate) provider: Option<String>,
    pub(crate) model: Option<String>,
    pub(crate) no_color: bool,
    pub(crate) file_autocomplete_respects_gitignore: bool,
    pub(crate) custom_providers: BTreeMap<String, CustomProviderConfig>,
    pub(crate) thinking_level: ThinkingLevel,
    pub(crate) auth: Option<ProviderCredential>,
    pub(crate) paths: McPaths,
}

impl fmt::Debug for EffectiveConfig {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("EffectiveConfig")
            .field("provider", &self.provider)
            .field("model", &self.model)
            .field("no_color", &self.no_color)
            .field(
                "file_autocomplete_respects_gitignore",
                &self.file_autocomplete_respects_gitignore,
            )
            .field("custom_providers", &self.custom_providers)
            .field("auth", &self.auth)
            .field("thinking_level", &self.thinking_level)
            .field("paths", &self.paths)
            .finish()
    }
}

impl EffectiveConfig {
    pub(crate) fn selected_provider_model(settings: &Settings) -> (Option<String>, Option<String>) {
        (
            env::var("MC_PROVIDER")
                .ok()
                .or_else(|| settings.selected_model.provider.clone()),
            env::var("MC_MODEL")
                .ok()
                .or_else(|| settings.selected_model.model.clone()),
        )
    }

    pub(crate) fn missing_auth_error(&self) -> ConfigError {
        ConfigError::missing_auth_for_custom_providers(
            self.provider_id(),
            &self.custom_providers,
            &self.paths.auth_file,
        )
    }

    pub(crate) fn provider_id(&self) -> &str {
        self.provider
            .as_deref()
            .unwrap_or(crate::provider_ids::OPENAI_CODEX_PROVIDER)
    }
}

pub(crate) fn reject_retired_provider_selection(provider: &str, model: &str) -> anyhow::Result<()> {
    if provider != "claude-code" {
        return Ok(());
    }
    let model = if model.trim().is_empty() {
        "<model>"
    } else {
        model
    };
    anyhow::bail!(
        "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"
    );
}

impl ConfigError {
    pub(crate) fn missing_auth_for_custom_providers(
        provider: &str,
        custom_providers: &BTreeMap<String, CustomProviderConfig>,
        auth_file: &std::path::Path,
    ) -> Self {
        missing_auth_error(provider, custom_providers.get(provider), auth_file)
    }
}

fn missing_auth_error(
    provider: &str,
    custom: Option<&CustomProviderConfig>,
    auth_file: &std::path::Path,
) -> ConfigError {
    let auth_path = auth_file.display();
    let message = if let Some(custom) = custom {
        match &custom.api_key_env_var {
            Some(env_var) => format!(
                "missing auth: custom provider '{provider}' is configured but environment variable {env_var} is missing or empty"
            ),
            None => format!(
                "missing auth: custom provider '{provider}' is configured for no-auth but could not be prepared"
            ),
        }
    } else if provider == crate::provider_ids::CLAUDE_SUBSCRIPTION_PROVIDER {
        "Claude subscription unavailable: on Unix, install Claude Code and run `claude auth login`; API keys are not used".to_string()
    } else if provider == crate::provider_ids::OPENAI_CODEX_PROVIDER {
        format!(
            "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}"
        )
    } else if provider == crate::provider_ids::ANTHROPIC_PROVIDER {
        format!(
            "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"
        )
    } else {
        format!(
            "missing auth for provider '{provider}'; configure provider-keyed auth in {auth_path}"
        )
    };
    ConfigError::MissingAuth {
        provider: provider.to_string(),
        message,
    }
}

pub(crate) fn resolve_output_style(
    settings: &Settings,
    cli_color: Option<ColorChoice>,
) -> (bool, bool) {
    let stdout_is_tty = std::io::stdout().is_terminal();
    resolve_output_style_for_stdout(settings, cli_color, stdout_is_tty)
}

pub(crate) fn resolve_output_style_for_stdout(
    settings: &Settings,
    cli_color: Option<ColorChoice>,
    stdout_is_tty: bool,
) -> (bool, bool) {
    let policy = crate::appearance::resolve_color_policy_from_env(
        settings.no_color,
        cli_color,
        stdout_is_tty,
        env::var_os("NO_COLOR").is_some(),
        env::var("COLORTERM").ok().as_deref(),
        env::var("TERM").ok().as_deref(),
    );
    (policy.color_enabled, policy.unicode_enabled)
}
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub(crate) enum ConfigError {
    #[error("{message}")]
    MissingAuth { provider: String, message: String },
}

pub(crate) fn load_context_budget(
    config: &EffectiveConfig,
) -> anyhow::Result<crate::context::ContextBudget> {
    let settings = read_settings(&config.paths)?;
    let mut budget = settings.context.unwrap_or_default();
    let provider = config.provider_id();
    let model = config
        .model
        .as_deref()
        .unwrap_or_else(|| crate::providers::default_model_for_provider(provider));
    if let Some(context_window) =
        crate::model_catalog::cached_model_context_window(&config.paths, provider, model)
    {
        budget.max_tokens = context_window;
    }
    budget.apply_model_limits(provider, model);
    Ok(budget)
}