tokr 0.1.0

Persistent token-usage ledger for AI coding agents. Captures on write, queries forever.
use anyhow::{Context, Result};
use directories::{BaseDirs, ProjectDirs};
use std::path::{MAIN_SEPARATOR, PathBuf};

pub fn home() -> Result<PathBuf> {
    BaseDirs::new().map(|b| b.home_dir().to_path_buf()).context(
        "could not determine the home directory; set it explicitly if your environment is unusual",
    )
}

pub fn claude_projects_dir() -> Result<PathBuf> {
    Ok(home()?.join(".claude").join("projects"))
}

fn project_dirs() -> Result<ProjectDirs> {
    ProjectDirs::from("", "", "tokr")
        .context("could not determine the local application data directory")
}

pub fn data_dir() -> Result<PathBuf> {
    Ok(project_dirs()?.data_local_dir().to_path_buf())
}

pub fn db_path() -> Result<PathBuf> {
    Ok(data_dir()?.join("tokr.db"))
}

pub fn archive_dir() -> Result<PathBuf> {
    Ok(data_dir()?.join("archive"))
}

pub fn ensure_data_dir() -> Result<()> {
    let dir = data_dir()?;
    std::fs::create_dir_all(&dir).with_context(|| format!("creating {}", dir.display()))
}

pub fn project_from_transcript(transcript_path: &str) -> String {
    let dir_name = parent_dir_name(transcript_path).unwrap_or("");
    decode_project_dir(dir_name)
}

fn parent_dir_name(path: &str) -> Option<&str> {
    let mut parts = path
        .trim_end_matches(['/', '\\'])
        .rsplit(['/', '\\'])
        .filter(|part| !part.is_empty());
    let _file_name = parts.next()?;
    parts.next()
}

pub fn decode_project_dir(name: &str) -> String {
    decode_project_dir_with_separator(name, MAIN_SEPARATOR)
}

// Claude Code flattens the project path into the transcript directory name
// using `-` as a separator. That encoding is lossy — a real `-` in the
// original path becomes indistinguishable from an encoded separator. We decode
// it into a display label using the host platform separator; it is not meant to
// be used as a canonical path to reopen on disk.
fn decode_project_dir_with_separator(name: &str, separator: char) -> String {
    if name.is_empty() {
        return String::new();
    }
    name.replace('-', &separator.to_string())
}

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

    #[test]
    fn decodes_project_dir_with_unix_separator() {
        assert_eq!(
            decode_project_dir_with_separator("-home-skynet-repo", '/'),
            "/home/skynet/repo"
        );
    }

    #[test]
    fn decodes_project_dir_with_windows_separator() {
        assert_eq!(
            decode_project_dir_with_separator(r"C:-Users-Alice-repo", '\\'),
            r"C:\Users\Alice\repo"
        );
    }

    #[test]
    fn extracts_project_dir_from_unix_transcript_path() {
        assert_eq!(
            project_from_transcript("/tmp/.claude/projects/-home-skynet-repo/session.jsonl"),
            format!("{MAIN_SEPARATOR}home{MAIN_SEPARATOR}skynet{MAIN_SEPARATOR}repo")
        );
    }

    #[test]
    fn extracts_project_dir_from_windows_transcript_path() {
        assert_eq!(
            project_from_transcript(
                r"C:\Users\Alice\.claude\projects\C:-Users-Alice-repo\session.jsonl"
            ),
            format!("C:{MAIN_SEPARATOR}Users{MAIN_SEPARATOR}Alice{MAIN_SEPARATOR}repo")
        );
    }
}