csusage-adapter-openclaw 0.0.2

Coding agent CLI usage reports (ccusage fork with Claude Science support)
use std::{
    collections::HashSet,
    env, fs,
    path::{Path, PathBuf},
};

use crate::Result;

const OPENCLAW_DIR_ENV: &str = "OPENCLAW_DIR";

pub(super) fn paths(custom_path: Option<&str>) -> Vec<PathBuf> {
    if let Some(custom_path) = custom_path.filter(|path| !path.trim().is_empty()) {
        return existing_path_list(custom_path);
    }
    if let Ok(env_paths) = env::var(OPENCLAW_DIR_ENV)
        && !env_paths.trim().is_empty()
    {
        return existing_path_list(&env_paths);
    }
    let Some(home) = crate::home::home_dir() else {
        return Vec::new();
    };
    [
        home.join(".openclaw"),
        home.join(".clawdbot"),
        home.join(".moltbot"),
        home.join(".moldbot"),
    ]
    .into_iter()
    .filter(|path| path.is_dir())
    .collect()
}

fn existing_path_list(raw: &str) -> Vec<PathBuf> {
    let mut seen = HashSet::new();
    raw.split(',')
        .map(str::trim)
        .filter(|path| !path.is_empty())
        .map(PathBuf::from)
        .filter(|path| path.is_dir() && seen.insert(path.clone()))
        .collect()
}

pub(super) fn collect_session_files(root: &Path) -> Result<Vec<PathBuf>> {
    let mut files = Vec::new();
    collect_session_files_inner(root, &mut files)?;
    files.sort();
    Ok(files)
}

fn collect_session_files_inner(path: &Path, files: &mut Vec<PathBuf>) -> Result<()> {
    for entry in fs::read_dir(path)? {
        let entry = entry?;
        let file_type = entry.file_type()?;
        let path = entry.path();
        if file_type.is_symlink() {
            continue;
        }
        if file_type.is_dir() {
            collect_session_files_inner(&path, files)?;
            continue;
        }
        if file_type.is_file()
            && path
                .file_name()
                .and_then(|name| name.to_str())
                .is_some_and(is_openclaw_session_file)
        {
            files.push(path);
        }
    }
    Ok(())
}

fn is_openclaw_session_file(name: &str) -> bool {
    let Some(index) = name.find(".jsonl") else {
        return false;
    };
    let suffix = &name[index..];
    suffix == ".jsonl"
        || suffix.starts_with(".jsonl.deleted.")
        || suffix.starts_with(".jsonl.reset.")
}

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

    #[test]
    fn detects_archived_session_files_as_openclaw_sessions() {
        assert!(is_openclaw_session_file("a.jsonl.deleted.1700000000000"));
        assert!(is_openclaw_session_file(
            "a.jsonl.reset.2026-03-20T06-34-44.520Z"
        ));
        assert!(!is_openclaw_session_file("a.json"));
    }
}