magi-code 0.96.2

Repository-aware CLI coding agent for terminal work
Documentation
//! Claude subscription quota: CLI-owned credentials, never persisted or logged here.
#[cfg(target_os = "macos")]
use std::process::{Command, Stdio};
use std::time::Duration;

use reqwest::{blocking::Client, redirect::Policy};
use serde::Deserialize;

#[cfg(target_os = "macos")]
use crate::{
    cancellation::AgentCancellation,
    tools::process::{BoundedChildProcessLimits, run_bounded_child_process},
};
use crate::{
    http_body::read_bounded_response_text, providers::claude_subscription::cli_subscription_status,
};

const USAGE_URL: &str = "https://api.anthropic.com/api/oauth/usage";
const AUTH_ERROR: &str = "Claude usage authentication failed; run `claude auth login`";

#[derive(Debug, Deserialize)]
struct CredentialFile {
    #[serde(rename = "claudeAiOauth")]
    oauth: OAuthToken,
}

#[derive(Debug, Deserialize)]
struct OAuthToken {
    #[serde(rename = "accessToken")]
    access: String,
}

#[derive(Clone, Debug, Deserialize)]
pub(crate) struct ClaudeUsage {
    pub(crate) five_hour: Option<ClaudeWindow>,
    pub(crate) seven_day: Option<ClaudeWindow>,
}

#[derive(Clone, Debug, Deserialize)]
pub(crate) struct ClaudeWindow {
    pub(crate) utilization: f64,
    pub(crate) resets_at: Option<String>,
}

/// Hash the CLI-reported account, not its rotating OAuth token.
pub(crate) fn connected_account() -> Option<[u8; 32]> {
    use sha2::{Digest, Sha256};
    let status = cli_subscription_status()?;
    let email = status.get("email")?.as_str()?.trim();
    let org = status
        .get("orgId")
        .and_then(serde_json::Value::as_str)
        .unwrap_or("");
    if email.is_empty() {
        return None;
    }
    Some(Sha256::digest(format!("{email}\0{org}").as_bytes()).into())
}

/// Blocking: call only on a worker. Read credentials from CLI-owned storage, never magi-code auth.
pub(crate) fn load_claude_usage() -> Result<ClaudeUsage, String> {
    let status = cli_subscription_status().ok_or(AUTH_ERROR)?;
    let token = read_cli_token(&status)?;
    let client = Client::builder()
        .timeout(Duration::from_secs(15))
        .connect_timeout(Duration::from_secs(15))
        .redirect(Policy::none())
        .build()
        .map_err(|_| "Could not create Claude usage HTTP client".to_owned())?;
    let response = client
        .get(USAGE_URL)
        .bearer_auth(&token)
        .header("anthropic-beta", "oauth-2025-04-20")
        .header("Accept", "application/json")
        .header("Cache-Control", "no-cache")
        .send()
        .map_err(|error| {
            if error.is_timeout() {
                "Claude usage request timed out"
            } else {
                "Claude usage request failed"
            }
            .to_owned()
        })?;
    if response.status() == reqwest::StatusCode::UNAUTHORIZED
        || response.status() == reqwest::StatusCode::FORBIDDEN
    {
        return Err(AUTH_ERROR.into());
    }
    if !response.status().is_success() {
        return Err(format!(
            "Claude usage request failed (HTTP {})",
            response.status().as_u16()
        ));
    }
    let body = read_bounded_response_text(response, 256 * 1024).map_err(|_| {
        "Could not read Claude usage response within size and time limits".to_owned()
    })?;
    parse_usage(&body)
}

fn parse_usage(body: &str) -> Result<ClaudeUsage, String> {
    let usage: ClaudeUsage =
        serde_json::from_str(body).map_err(|_| "Invalid Claude usage response".to_owned())?;
    if usage.five_hour.is_none() && usage.seven_day.is_none() {
        return Err("Claude usage response has no quota windows".into());
    }
    if [usage.five_hour.as_ref(), usage.seven_day.as_ref()]
        .into_iter()
        .flatten()
        .any(|window| !window.utilization.is_finite() || window.utilization < 0.0)
    {
        return Err("Invalid Claude usage percentage".into());
    }
    Ok(usage)
}

fn read_cli_token(status: &serde_json::Value) -> Result<String, String> {
    #[cfg(target_os = "macos")]
    {
        let mut command = Command::new("security");
        command
            .args([
                "find-generic-password",
                "-s",
                "Claude Code-credentials",
                "-w",
            ])
            .stdin(Stdio::null())
            .stdout(Stdio::piped())
            .stderr(Stdio::piped());
        use std::os::unix::process::CommandExt;
        command.process_group(0);
        let child = command.spawn().map_err(|_| AUTH_ERROR.to_owned())?;
        let output = run_bounded_child_process(
            child,
            BoundedChildProcessLimits {
                stdout_max_bytes: 64 * 1024,
                stderr_max_bytes: 1024,
                timeout: Duration::from_secs(3),
                poll_interval: Duration::from_millis(20),
            },
            &AgentCancellation::default(),
        )
        .map_err(|_| AUTH_ERROR.to_owned())?;
        if output.timed_out || output.stdout_truncated || output.cleanup_warning.is_some() {
            return Err(AUTH_ERROR.into());
        }
        if output.status.is_some_and(|status| status.success()) {
            return parse_token(&output.stdout);
        }
        // Keychain may be empty: the CLI then uses its credentials file.
        if output.status.and_then(|status| status.code()) != Some(44) {
            return Err(AUTH_ERROR.into());
        }
    }
    let directory = status
        .get("configDirectory")
        .and_then(serde_json::Value::as_str)
        .filter(|path| !path.is_empty())
        .ok_or(AUTH_ERROR)?;
    let path = std::path::Path::new(directory).join(".credentials.json");
    let metadata = std::fs::symlink_metadata(&path).map_err(|_| AUTH_ERROR.to_owned())?;
    if !metadata.is_file() || metadata.len() > 64 * 1024 {
        return Err(AUTH_ERROR.into());
    }
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        if metadata.permissions().mode() & 0o077 != 0 {
            return Err(AUTH_ERROR.into());
        }
    }
    parse_token(&std::fs::read_to_string(path).map_err(|_| AUTH_ERROR.to_owned())?)
}

fn parse_token(text: &str) -> Result<String, String> {
    let credentials: CredentialFile =
        serde_json::from_str(text).map_err(|_| AUTH_ERROR.to_owned())?;
    let access = credentials.oauth.access;
    if access.trim().is_empty() {
        return Err(AUTH_ERROR.into());
    }
    Ok(access)
}

pub(crate) fn quota_label(
    usage: &ClaudeUsage,
    now: chrono::DateTime<chrono::Utc>,
) -> Option<String> {
    let (window, suffix) = usage
        .five_hour
        .as_ref()
        .map(|window| (window, "5h"))
        .or_else(|| usage.seven_day.as_ref().map(|window| (window, "w")))?;
    let reset = if suffix == "5h" {
        window.resets_at.as_deref()
    } else {
        None
    }
    .and_then(|timestamp| chrono::DateTime::parse_from_rfc3339(timestamp).ok())
    .map(|reset| reset.signed_duration_since(now).num_seconds())
    .filter(|&seconds| seconds > 0)
    .map(|seconds| {
        let minutes = (seconds + 59) / 60;
        if minutes < 60 {
            format!("{minutes}m")
        } else {
            format!("{}h{}m", minutes / 60, minutes % 60)
        }
    });
    Some(format!(
        "Claude {:.0}/100%({})",
        window.utilization,
        reset.as_deref().unwrap_or(suffix)
    ))
}