use crate::traits::{LogDiscovery, ProbeResult, SessionIndex};
use crate::{Error, Result};
use agtrace_core::project_hash_from_root;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use walkdir::WalkDir;
use super::io::{extract_gemini_header, extract_project_hash_from_gemini_file};
pub struct GeminiDiscovery;
impl LogDiscovery for GeminiDiscovery {
fn id(&self) -> &'static str {
"gemini"
}
fn probe(&self, path: &Path) -> ProbeResult {
if !path.is_file() {
return ProbeResult::NoMatch;
}
if let Ok(metadata) = std::fs::metadata(path)
&& metadata.len() == 0
{
return ProbeResult::NoMatch;
}
let filename = path.file_name().and_then(|f| f.to_str()).unwrap_or("");
if filename.starts_with("session-") && filename.ends_with(".json") {
ProbeResult::match_high()
} else {
ProbeResult::NoMatch
}
}
fn resolve_log_root(&self, project_root: &Path) -> Option<PathBuf> {
let hash = project_hash_from_root(&project_root.to_string_lossy());
Some(PathBuf::from(hash.to_string()))
}
fn scan_sessions(&self, log_root: &Path) -> Result<Vec<SessionIndex>> {
let mut sessions: HashMap<String, SessionIndex> = HashMap::new();
if !log_root.exists() {
return Ok(Vec::new());
}
for entry in WalkDir::new(log_root)
.max_depth(3)
.into_iter()
.filter_map(|e| e.ok())
{
let path = entry.path();
if self.probe(path) == ProbeResult::NoMatch {
continue;
}
let header = match extract_gemini_header(path) {
Ok(h) => h,
Err(_) => continue,
};
let session_id = match header.session_id {
Some(id) => id,
None => continue,
};
sessions
.entry(session_id.clone())
.or_insert_with(|| SessionIndex {
session_id: session_id.clone(),
timestamp: header.timestamp.clone(),
latest_mod_time: None, main_file: path.to_path_buf(),
sidechain_files: Vec::new(),
project_root: None,
repository_hash: None, snippet: header.snippet.clone(),
parent_session_id: None, spawned_by: None,
});
}
for session in sessions.values_mut() {
let all_files = vec![session.main_file.as_path()];
session.latest_mod_time = crate::get_latest_mod_time_rfc3339(&all_files);
}
Ok(sessions.into_values().collect())
}
fn extract_session_id(&self, path: &Path) -> Result<String> {
let header = extract_gemini_header(path)?;
header
.session_id
.ok_or_else(|| Error::Parse(format!("No session_id in file: {}", path.display())))
}
fn extract_project_hash(&self, path: &Path) -> Result<Option<agtrace_types::ProjectHash>> {
Ok(extract_project_hash_from_gemini_file(path))
}
fn find_session_files(&self, log_root: &Path, session_id: &str) -> Result<Vec<PathBuf>> {
let mut matching_files = Vec::new();
for entry in WalkDir::new(log_root)
.max_depth(3)
.into_iter()
.filter_map(|e| e.ok())
{
let path = entry.path();
if self.probe(path) == ProbeResult::NoMatch {
continue;
}
if let Ok(header) = extract_gemini_header(path)
&& header.session_id.as_deref() == Some(session_id)
{
matching_files.push(path.to_path_buf());
}
}
Ok(matching_files)
}
fn is_sidechain_file(&self, _path: &Path) -> Result<bool> {
Ok(false)
}
}