use super::*;
pub(super) fn detect_source(text: &str) -> Option<SessionSource> {
if let Ok(v) = serde_json::from_str::<Value>(text.trim()) {
if v.get("conversation").and_then(Value::as_array).is_some()
&& (v.get("working_dir").is_some() || v.get("workingDir").is_some())
{
return Some(SessionSource::Goose);
}
if v.get("info").is_some() && v.get("messages").and_then(Value::as_array).is_some() {
return Some(SessionSource::OpenCode);
}
}
for line in non_empty_lines(text) {
let Ok(v) = serde_json::from_str::<Value>(line) else {
continue;
};
if v.get("key").and_then(Value::as_array).is_some() && v.get("value").is_some() {
return Some(SessionSource::OpenCode);
}
if v.get("payload").is_some() {
return Some(SessionSource::Codex);
}
if v.get("sessionId").and_then(Value::as_str).is_some()
&& (v.get("projectHash").is_some()
|| v.get("startTime").is_some()
|| v.get("lastUpdated").is_some())
&& v.get("type").is_none()
{
return Some(SessionSource::Gemini);
}
let tag = v.get("type").and_then(Value::as_str);
if tag == Some("gemini") && v.get("content").is_some() {
return Some(SessionSource::Gemini);
}
if v.get("message").is_none()
&& v.get("uuid").is_none()
&& v.get("sessionId").is_none()
&& matches!(
tag,
Some(
"system"
| "user"
| "assistant"
| "tool_result"
| "reasoning"
| "backend_tool_call"
)
)
&& (v.get("content").is_some()
|| v.get("tool_calls").is_some()
|| v.get("tool_call_id").is_some()
|| v.get("encrypted_content").is_some()
|| v.get("kind").is_some())
{
return Some(SessionSource::Grok);
}
if v.get("type").and_then(Value::as_str) == Some("session")
&& v.get("id").and_then(Value::as_str).is_some()
&& v.get("message").is_none()
&& v.get("uuid").is_none()
{
for body in non_empty_lines(text) {
let Ok(entry) = serde_json::from_str::<Value>(body) else {
continue;
};
match entry.get("type").and_then(Value::as_str) {
Some("leaf") if entry.get("targetId").is_some() => {
return Some(SessionSource::OpenClaw);
}
Some("message")
if entry
.get("message")
.and_then(|message| message.get("__openclaw"))
.is_some() =>
{
return Some(SessionSource::OpenClaw);
}
_ => {}
}
}
return Some(SessionSource::Pi);
}
if v.get("type").is_some() || v.get("message").is_some() {
return Some(SessionSource::ClaudeCode);
}
}
None
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OpenCodeStorageSurface {
Sqlite,
JsonTreeB,
JsonTreeA,
}
pub fn detect_opencode_storage_surface(
data_root: &Path,
) -> Option<(OpenCodeStorageSurface, PathBuf)> {
if let Ok(p) = std::env::var("OPENCODE_DB") {
let pb = PathBuf::from(p);
if pb.is_file() {
return Some((OpenCodeStorageSurface::Sqlite, pb));
}
}
if let Ok(entries) = std::fs::read_dir(data_root) {
let mut candidates: Vec<PathBuf> = entries
.flatten()
.map(|entry| entry.path())
.filter(|p| {
p.file_name()
.and_then(|n| n.to_str())
.is_some_and(|name| name.starts_with("opencode") && name.ends_with(".db"))
})
.collect();
candidates.sort();
if let Some(exact) = candidates
.iter()
.find(|p| p.file_name().and_then(|n| n.to_str()) == Some("opencode.db"))
{
return Some((OpenCodeStorageSurface::Sqlite, exact.clone()));
}
if let Some(first) = candidates.into_iter().next() {
return Some((OpenCodeStorageSurface::Sqlite, first));
}
}
let storage = data_root.join("storage");
if storage.join("migration").is_file() {
return Some((OpenCodeStorageSurface::JsonTreeB, storage));
}
let project_dir = data_root.join("project");
if project_dir.is_dir() {
return Some((OpenCodeStorageSurface::JsonTreeA, project_dir));
}
None
}
const SQLITE_MAGIC: &[u8; 16] = b"SQLite format 3\0";
pub fn looks_like_sqlite(path: &Path) -> bool {
if !path.is_file() {
return false;
}
if path.extension().and_then(|e| e.to_str()) == Some("db") {
return true;
}
use std::io::Read;
let Ok(mut f) = std::fs::File::open(path) else {
return false;
};
let mut buf = [0u8; 16];
f.read_exact(&mut buf).is_ok() && &buf == SQLITE_MAGIC
}