claude-code-cli-acp 0.1.1

An ACP-compatible adapter for the real Claude Code CLI
Documentation
use std::{
    collections::BTreeMap,
    path::{Path, PathBuf},
};

use anyhow::Context;
use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ClaudeSettings {
    #[serde(default)]
    pub permissions: PermissionSettings,
    #[serde(default)]
    pub env: BTreeMap<String, String>,
    pub model: Option<String>,
    #[serde(rename = "effortLevel")]
    pub effort_level: Option<String>,
    pub available_models: Option<Vec<String>>,
}

#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PermissionSettings {
    pub default_mode: Option<String>,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SettingsPaths {
    pub user: PathBuf,
    pub project: PathBuf,
    pub local: PathBuf,
    pub managed: PathBuf,
}

impl SettingsPaths {
    pub fn for_cwd(cwd: impl AsRef<Path>) -> anyhow::Result<Self> {
        let home = dirs::home_dir().context("home directory unavailable")?;
        Ok(Self::for_cwd_and_home(cwd, home))
    }

    pub fn for_cwd_and_home(cwd: impl AsRef<Path>, home: impl AsRef<Path>) -> Self {
        let config_dir = std::env::var_os("CLAUDE_CONFIG_DIR")
            .map(PathBuf::from)
            .unwrap_or_else(|| home.as_ref().join(".claude"));
        Self {
            user: config_dir.join("settings.json"),
            project: cwd.as_ref().join(".claude/settings.json"),
            local: cwd.as_ref().join(".claude/settings.local.json"),
            managed: managed_settings_path(),
        }
    }

    pub fn with_managed(mut self, managed: impl Into<PathBuf>) -> Self {
        self.managed = managed.into();
        self
    }
}

#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct SettingsLoad {
    pub settings: ClaudeSettings,
    pub warnings: Vec<String>,
}

pub fn load_merged_settings(paths: &SettingsPaths) -> SettingsLoad {
    let mut load = SettingsLoad::default();
    for path in [&paths.user, &paths.project, &paths.local, &paths.managed] {
        match load_settings_file(path) {
            Ok(Some(settings)) => merge_settings(&mut load.settings, settings),
            Ok(None) => {}
            Err(err) => load.warnings.push(format!(
                "failed to load settings from {}: {err}",
                path.display()
            )),
        }
    }
    load
}

fn load_settings_file(path: &Path) -> anyhow::Result<Option<ClaudeSettings>> {
    match std::fs::read_to_string(path) {
        Ok(contents) => serde_json::from_str(&contents)
            .with_context(|| format!("parse settings {}", path.display()))
            .map(Some),
        Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
        Err(err) => Err(err).with_context(|| format!("read settings {}", path.display())),
    }
}

fn merge_settings(merged: &mut ClaudeSettings, settings: ClaudeSettings) {
    merged.env.extend(settings.env);
    if let Some(model) = settings.model {
        merged.model = Some(model);
    }
    if let Some(effort_level) = settings.effort_level {
        merged.effort_level = Some(effort_level);
    }
    if let Some(default_mode) = settings.permissions.default_mode {
        merged.permissions.default_mode = Some(default_mode);
    }
    if let Some(models) = settings.available_models {
        let existing = merged.available_models.get_or_insert_with(Vec::new);
        for model in models {
            if !existing.contains(&model) {
                existing.push(model);
            }
        }
    }
}

fn managed_settings_path() -> PathBuf {
    if let Some(path) = std::env::var_os("CLAUDE_CODE_ACP_MANAGED_SETTINGS") {
        return path.into();
    }

    #[cfg(target_os = "macos")]
    {
        PathBuf::from("/Library/Application Support/ClaudeCode/managed-settings.json")
    }
    #[cfg(target_os = "windows")]
    {
        PathBuf::from(r"C:\Program Files\ClaudeCode\managed-settings.json")
    }
    #[cfg(all(not(target_os = "macos"), not(target_os = "windows")))]
    {
        PathBuf::from("/etc/claude-code/managed-settings.json")
    }
}