nils-codex-cli 0.7.3

CLI crate for nils-codex-cli in the nils-cli workspace.
Documentation
use std::path::{Path, PathBuf};
use std::process::Stdio;
use std::time::{SystemTime, UNIX_EPOCH};

use chrono::Utc;

use crate::rate_limits::cache;
use crate::rate_limits::client::{UsageRequest, fetch_usage};
use crate::rate_limits::render;

use super::lock;
use super::render as prompt_segment_render;

pub fn enqueue_background_refresh(target_file: &Path) {
    let cache_file = match cache::cache_file_for_target(target_file) {
        Ok(value) => value,
        Err(_) => return,
    };
    let lock_dir = match lock::lock_dir_for_cache_file(&cache_file) {
        Some(value) => value,
        None => return,
    };

    let refresh_min_seconds = env_u64("CODEX_PROMPT_SEGMENT_REFRESH_MIN_SECONDS", 30);
    if refresh_min_seconds > 0 && is_within_min_interval(&cache_file, refresh_min_seconds) {
        return;
    }

    let lock_stale_seconds = env_u64("CODEX_PROMPT_SEGMENT_LOCK_STALE_SECONDS", 90);
    if lock_dir.exists() && !lock::is_stale(&lock_dir, lock_stale_seconds) {
        return;
    }

    write_last_attempt(&cache_file);

    let exe = match std::env::current_exe() {
        Ok(value) => value,
        Err(_) => return,
    };

    let mut cmd = std::process::Command::new(exe);
    cmd.arg("prompt-segment").arg("--refresh");
    cmd.stdin(Stdio::null());
    cmd.stdout(Stdio::null());
    cmd.stderr(Stdio::null());

    let _ = cmd.spawn();
}

pub fn refresh_blocking(target_file: &Path) -> Option<prompt_segment_render::CacheEntry> {
    let cache_file = cache::cache_file_for_target(target_file).ok()?;
    let lock_dir = lock::lock_dir_for_cache_file(&cache_file)?;
    let lock_stale_seconds = env_u64("CODEX_PROMPT_SEGMENT_LOCK_STALE_SECONDS", 90);

    cleanup_usage_files(&cache_file);

    let _lock = lock::RefreshLock::acquire(&lock_dir, lock_stale_seconds)?;

    let entry = fetch_and_write_cache(target_file).ok()?;
    write_last_attempt(&cache_file);
    Some(entry)
}

fn fetch_and_write_cache(target_file: &Path) -> anyhow::Result<prompt_segment_render::CacheEntry> {
    let base_url = std::env::var("CODEX_CHATGPT_BASE_URL")
        .unwrap_or_else(|_| "https://chatgpt.com/backend-api/".to_string());
    let connect_timeout = env_u64("CODEX_PROMPT_SEGMENT_CURL_CONNECT_TIMEOUT_SECONDS", 2);
    let max_time = env_u64("CODEX_PROMPT_SEGMENT_CURL_MAX_TIME_SECONDS", 8);

    let usage_request = UsageRequest {
        target_file: target_file.to_path_buf(),
        refresh_on_401: false,
        base_url,
        connect_timeout_seconds: connect_timeout,
        max_time_seconds: max_time,
    };

    let usage = fetch_usage(&usage_request)?;
    let usage_data =
        render::parse_usage(&usage.json).ok_or_else(|| anyhow::anyhow!("invalid usage payload"))?;
    let values = render::render_values(&usage_data);
    let weekly = render::weekly_values(&values);

    let fetched_at_epoch = Utc::now().timestamp();
    if fetched_at_epoch > 0 {
        let _ = cache::write_prompt_segment_cache(
            target_file,
            fetched_at_epoch,
            &weekly.non_weekly_label,
            weekly.non_weekly_remaining,
            weekly.weekly_remaining,
            weekly.weekly_reset_epoch,
            weekly.non_weekly_reset_epoch,
        );
    }

    Ok(prompt_segment_render::CacheEntry {
        fetched_at_epoch,
        non_weekly_label: weekly.non_weekly_label,
        non_weekly_remaining: weekly.non_weekly_remaining,
        non_weekly_reset_epoch: weekly.non_weekly_reset_epoch,
        weekly_remaining: weekly.weekly_remaining,
        weekly_reset_epoch: weekly.weekly_reset_epoch,
    })
}

fn env_u64(key: &str, default: u64) -> u64 {
    std::env::var(key)
        .ok()
        .and_then(|raw| raw.trim().parse::<u64>().ok())
        .unwrap_or(default)
}

fn last_attempt_path(cache_file: &Path) -> Option<PathBuf> {
    let stem = cache_file.file_stem()?.to_string_lossy();
    Some(cache_file.with_file_name(format!("{stem}.refresh.at")))
}

fn write_last_attempt(cache_file: &Path) {
    let path = match last_attempt_path(cache_file) {
        Some(value) => value,
        None => return,
    };
    let now_epoch = now_epoch();
    if now_epoch <= 0 {
        return;
    }

    if let Some(parent) = path.parent() {
        let _ = std::fs::create_dir_all(parent);
    }
    let _ = std::fs::write(&path, now_epoch.to_string());
}

fn is_within_min_interval(cache_file: &Path, refresh_min_seconds: u64) -> bool {
    let path = match last_attempt_path(cache_file) {
        Some(value) => value,
        None => return false,
    };
    let content = match std::fs::read_to_string(path) {
        Ok(value) => value,
        Err(_) => return false,
    };
    let last = match content.trim().parse::<i64>() {
        Ok(value) => value,
        Err(_) => return false,
    };
    if last <= 0 {
        return false;
    }
    let now = now_epoch();
    if now <= 0 {
        return false;
    }
    (now - last) >= 0 && (now - last) < refresh_min_seconds as i64
}

fn now_epoch() -> i64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .ok()
        .and_then(|d| i64::try_from(d.as_secs()).ok())
        .unwrap_or(0)
}

fn cleanup_usage_files(cache_file: &Path) {
    let cache_dir = match cache_file.parent() {
        Some(value) => value,
        None => return,
    };
    let entries = match std::fs::read_dir(cache_dir) {
        Ok(value) => value,
        Err(_) => return,
    };

    for entry in entries.flatten() {
        let path = entry.path();
        let name = path.file_name().and_then(|s| s.to_str()).unwrap_or("");
        if name.starts_with("wham.usage.") {
            let _ = std::fs::remove_file(path);
        }
    }
}