use std::{
env,
path::{Path, PathBuf},
};
use walkdir::WalkDir;
use crate::{
models::{UsageLoad, UsageWarning},
parser::parse_session_file,
};
pub trait UsageProvider: Send + Sync {
fn load_events(&self) -> UsageLoad;
}
#[derive(Debug, Clone)]
pub struct LocalCodexProvider {
codex_home: PathBuf,
}
impl LocalCodexProvider {
pub fn new(codex_home: Option<PathBuf>) -> Self {
Self {
codex_home: codex_home.unwrap_or_else(default_codex_home),
}
}
pub fn codex_home(&self) -> &Path {
&self.codex_home
}
}
impl UsageProvider for LocalCodexProvider {
fn load_events(&self) -> UsageLoad {
if !self.codex_home.exists() {
return UsageLoad {
warnings: vec![UsageWarning {
file: self.codex_home.display().to_string(),
line: None,
message: "Codex home not found".to_string(),
}],
..UsageLoad::default()
};
}
let mut load = UsageLoad::default();
for path in discover_session_files(&self.codex_home) {
load.session_count += 1;
let parsed = parse_session_file(&path);
load.events.extend(parsed.events);
load.warnings.extend(parsed.warnings);
}
load
}
}
fn discover_session_files(codex_home: &Path) -> Vec<PathBuf> {
let mut files = Vec::new();
collect_jsonl_files(&codex_home.join("sessions"), &mut files);
collect_jsonl_files(&codex_home.join("archived_sessions"), &mut files);
files.sort();
files
}
fn collect_jsonl_files(root: &Path, files: &mut Vec<PathBuf>) {
if !root.exists() {
return;
}
for entry in WalkDir::new(root).into_iter().filter_map(Result::ok) {
if entry.file_type().is_file()
&& entry
.path()
.extension()
.is_some_and(|extension| extension.eq_ignore_ascii_case("jsonl"))
{
files.push(entry.path().to_path_buf());
}
}
}
fn default_codex_home() -> PathBuf {
if let Some(home) = env::var_os("USERPROFILE").or_else(|| env::var_os("HOME")) {
return PathBuf::from(home).join(".codex");
}
PathBuf::from(".codex")
}
#[cfg(test)]
mod tests {
use std::{fs, path::Path};
use tempfile::tempdir;
use super::{LocalCodexProvider, UsageProvider};
fn write_session(path: &Path, session_id: &str, cwd: &str, tokens: u64) {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).expect("create fixture directory");
}
let session_meta = serde_json::json!({
"timestamp": "2026-07-06T08:00:00.000Z",
"type": "session_meta",
"payload": {
"id": session_id,
"cwd": cwd,
"thread_name": format!("Thread {session_id}"),
},
});
let token_count = serde_json::json!({
"timestamp": "2026-07-06T08:01:00.000Z",
"type": "event_msg",
"payload": {
"type": "token_count",
"info": {
"last_token_usage": {
"total_tokens": tokens,
},
},
},
});
fs::write(path, format!("{session_meta}\n{token_count}\n")).expect("write fixture session");
}
#[test]
fn discovers_active_and_archived_session_files() {
let root = tempdir().expect("create codex home");
write_session(
&root.path().join("sessions/2026/07/06/rollout-active.jsonl"),
"active",
"C:\\work\\active",
11,
);
write_session(
&root.path().join("archived_sessions/rollout-archived.jsonl"),
"archived",
"C:\\work\\archived",
17,
);
let provider = LocalCodexProvider::new(Some(root.path().to_path_buf()));
let loaded = provider.load_events();
assert_eq!(loaded.events.len(), 2);
assert_eq!(loaded.session_count, 2);
assert!(loaded.warnings.is_empty());
assert!(
loaded
.events
.iter()
.any(|event| event.project == "C:\\work\\active")
);
assert!(
loaded
.events
.iter()
.any(|event| event.project == "C:\\work\\archived")
);
assert_eq!(
loaded
.events
.iter()
.map(|event| event.usage.total_tokens)
.sum::<u64>(),
28
);
}
#[test]
fn missing_codex_home_returns_empty_load_with_warning() {
let root = tempdir().expect("create temp root");
let missing = root.path().join("missing-codex-home");
let provider = LocalCodexProvider::new(Some(missing));
let loaded = provider.load_events();
assert!(loaded.events.is_empty());
assert_eq!(loaded.session_count, 0);
assert_eq!(loaded.warnings.len(), 1);
assert!(loaded.warnings[0].message.contains("Codex home not found"));
}
}