use std::fs;
use std::path::{Path, PathBuf};
use serde::Deserialize;
use crate::models::{Provider, Session};
#[derive(Debug, Deserialize)]
struct RawSession {
pid: u32,
#[serde(rename = "sessionId")]
session_id: String,
cwd: String,
#[serde(default)]
name: Option<String>,
#[serde(rename = "nameSource", default)]
name_source: Option<String>,
#[serde(default)]
status: Option<String>,
#[serde(rename = "startedAt", default)]
started_at: u64,
#[serde(rename = "updatedAt", default)]
updated_at: u64,
#[serde(rename = "waitingFor", default)]
waiting_for: Option<String>,
#[serde(default)]
version: Option<String>,
#[serde(default)]
kind: Option<String>,
}
pub fn sessions_dir() -> PathBuf {
let home = std::env::var("HOME").unwrap_or_default();
PathBuf::from(home).join(".claude/sessions")
}
pub fn discover_live_sessions() -> Vec<Session> {
let dir = sessions_dir();
let Ok(entries) = fs::read_dir(&dir) else {
return Vec::new();
};
let mut out = Vec::new();
for entry in entries.flatten() {
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) != Some("json") {
continue;
}
let Ok(bytes) = fs::read(&path) else { continue };
let Ok(raw) = serde_json::from_slice::<RawSession>(&bytes) else {
continue;
};
if !pid_alive(raw.pid) {
continue;
}
if let Some(session) = raw_to_session(raw) {
out.push(session);
}
}
out
}
fn raw_to_session(raw: RawSession) -> Option<Session> {
if raw.kind.as_deref() == Some("bg") {
return None;
}
if raw.name.as_deref() == Some(crate::auditor::AUDITOR_NAME) {
return None;
}
let name_is_derived = raw.name_source.as_deref() == Some("derived");
let mut session = Session::new(
Provider::Claude,
raw.pid,
raw.session_id,
PathBuf::from(raw.cwd),
raw.name,
raw.status.unwrap_or_else(|| "unknown".to_string()),
raw.started_at,
raw.updated_at,
raw.waiting_for,
);
session.name_is_derived = name_is_derived;
session.cli_version = raw.version;
Some(session)
}
pub fn pid_alive(pid: u32) -> bool {
unsafe {
let r = libc::kill(pid as libc::pid_t, 0);
if r == 0 {
return true;
}
std::io::Error::last_os_error().raw_os_error() == Some(libc::EPERM)
}
}
#[allow(dead_code)]
pub fn projects_dir() -> PathBuf {
let home = std::env::var("HOME").unwrap_or_default();
PathBuf::from(home).join(".claude/projects")
}
pub fn encode_cwd(cwd: &Path) -> String {
cwd.to_string_lossy()
.chars()
.map(|c| if c == '/' || c == '.' { '-' } else { c })
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn raw(json: &str) -> RawSession {
serde_json::from_str(json).expect("valid session JSON")
}
#[test]
fn drops_bg_kind_sessions() {
let raw = raw(r#"{"pid":31903,"sessionId":"a12ff1bb","cwd":"/repo/ux",
"name":"Manage UAC work coordination","status":"idle",
"kind":"bg","version":"2.1.162"}"#);
assert!(raw_to_session(raw).is_none());
}
#[test]
fn keeps_interactive_and_kindless_sessions() {
let interactive = raw(r#"{"pid":12919,"sessionId":"02c00b0b","cwd":"/repo/ux",
"name":"agent-UAC-24","nameSource":"derived","status":"idle",
"kind":"interactive"}"#);
let session = raw_to_session(interactive).expect("interactive kept");
assert_eq!(session.pid, 12919);
assert!(session.name_is_derived);
let kindless = raw(r#"{"pid":777,"sessionId":"old","cwd":"/repo/ux","name":"legacy"}"#);
let session = raw_to_session(kindless).expect("kindless kept");
assert!(!session.name_is_derived);
}
#[test]
fn drops_auditor_sessions() {
let auditor = raw(&format!(
r#"{{"pid":42,"sessionId":"aud","cwd":"/repo/ux","name":"{}"}}"#,
crate::auditor::AUDITOR_NAME
));
assert!(raw_to_session(auditor).is_none());
}
}