claude-code-sdk-rust 0.1.0

Async Rust SDK for the Claude Code CLI: streaming agent turns, tool use, and sessions.
Documentation
use std::path::{Path, PathBuf};

use crate::error::{CLINotFoundError, ClaudeSDKError, Result};

const DEFAULT_CLI_NAME: &str = "claude";
const MINIMUM_CLAUDE_CODE_VERSION: (u64, u64, u64) = (2, 0, 0);

pub(crate) fn find_cli_path(explicit: Option<&str>) -> Result<String> {
    if let Some(path) = explicit {
        return Ok(path.to_string());
    }

    if let Some(path) = bundled_cli_path().filter(|path| path.is_file()) {
        return Ok(path.to_string_lossy().to_string());
    }

    if let Ok(path) = which::which(DEFAULT_CLI_NAME) {
        return Ok(path.to_string_lossy().to_string());
    }

    if let Some(home) = dirs::home_dir() {
        for path in fallback_cli_locations(&home) {
            if path.is_file() {
                return Ok(path.to_string_lossy().to_string());
            }
        }
    }

    Err(ClaudeSDKError::CLINotFound(CLINotFoundError::new(
        "Claude Code not found. Install with npm install -g @anthropic-ai/claude-code, ensure it is on PATH, or set cli_path.",
        DEFAULT_CLI_NAME,
    )))
}

pub(crate) fn fallback_cli_locations(home: &Path) -> Vec<PathBuf> {
    vec![
        home.join(".npm-global/bin/claude"),
        PathBuf::from("/usr/local/bin/claude"),
        home.join(".local/bin/claude"),
        home.join("node_modules/.bin/claude"),
        home.join(".yarn/bin/claude"),
        home.join(".claude/local/claude"),
    ]
}

pub(crate) fn parse_cli_version(output: &str) -> Option<(u64, u64, u64)> {
    let version = output.split_whitespace().next()?;
    let mut parts = version.split('.');
    let major = parts.next()?.parse().ok()?;
    let minor = parts.next()?.parse().ok()?;
    let patch = parts.next()?.parse().ok()?;
    Some((major, minor, patch))
}

pub(crate) fn is_supported_cli_version(version: (u64, u64, u64)) -> bool {
    version >= MINIMUM_CLAUDE_CODE_VERSION
}

pub(crate) fn unsupported_cli_version_warning(cli_path: &str, version: (u64, u64, u64)) -> String {
    format!(
        "Claude Code version {}.{}.{} at {} is unsupported in the Agent SDK. Minimum required version is {}.{}.{}.",
        version.0,
        version.1,
        version.2,
        cli_path,
        MINIMUM_CLAUDE_CODE_VERSION.0,
        MINIMUM_CLAUDE_CODE_VERSION.1,
        MINIMUM_CLAUDE_CODE_VERSION.2,
    )
}

pub(crate) async fn check_cli_version(cli_path: &str) -> Option<bool> {
    let output = tokio::time::timeout(
        std::time::Duration::from_secs(2),
        tokio::process::Command::new(cli_path).arg("-v").output(),
    )
    .await
    .ok()?
    .ok()?;
    let stdout = String::from_utf8_lossy(&output.stdout);
    let version = parse_cli_version(stdout.trim())?;
    let supported = is_supported_cli_version(version);
    if !supported {
        tracing::warn!("{}", unsupported_cli_version_warning(cli_path, version));
    }
    Some(supported)
}

fn bundled_cli_path() -> Option<PathBuf> {
    let cli_name = if cfg!(windows) {
        "claude.exe"
    } else {
        DEFAULT_CLI_NAME
    };
    let exe = std::env::current_exe().ok()?;
    let dir = exe.parent()?;
    Some(dir.join("_bundled").join(cli_name))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn fallback_locations_match_python_sdk_order() {
        let home = PathBuf::from("/home/alice");
        let locations = fallback_cli_locations(&home);

        assert_eq!(
            locations[0],
            PathBuf::from("/home/alice/.npm-global/bin/claude")
        );
        assert_eq!(locations[1], PathBuf::from("/usr/local/bin/claude"));
        assert_eq!(locations[2], PathBuf::from("/home/alice/.local/bin/claude"));
        assert_eq!(
            locations[3],
            PathBuf::from("/home/alice/node_modules/.bin/claude")
        );
        assert_eq!(locations[4], PathBuf::from("/home/alice/.yarn/bin/claude"));
        assert_eq!(
            locations[5],
            PathBuf::from("/home/alice/.claude/local/claude")
        );
    }

    #[test]
    fn parses_semver_prefix_from_cli_version_output() {
        assert_eq!(parse_cli_version("2.1.110"), Some((2, 1, 110)));
        assert_eq!(parse_cli_version("2.0.0 (Claude Code)"), Some((2, 0, 0)));
        assert_eq!(parse_cli_version("not-a-version"), None);
    }

    #[test]
    fn checks_minimum_supported_version() {
        assert!(!is_supported_cli_version((1, 9, 99)));
        assert!(is_supported_cli_version((2, 0, 0)));
        assert!(is_supported_cli_version((2, 1, 0)));
    }

    #[test]
    fn unsupported_version_warning_includes_version_path_and_minimum() {
        let warning = unsupported_cli_version_warning("/usr/bin/claude", (1, 9, 99));

        assert!(warning.contains("1.9.99"));
        assert!(warning.contains("/usr/bin/claude"));
        assert!(warning.contains("2.0.0"));
    }
}