pub mod claude;
pub mod codex;
pub mod crush;
pub mod gemini;
pub mod goose;
pub mod ir;
pub mod jsonl;
pub mod opencode;
use crate::message::{Context, ContextListing, ConversationMessage, Entry, Part};
use anyhow::Context as _;
use serde_json::{Value, json};
use std::collections::{HashMap, HashSet};
use std::fs;
use std::io::{BufRead, BufReader};
use std::path::Path;
pub trait ContextReader {
fn list_contexts(&self) -> anyhow::Result<Vec<ContextListing>>;
fn read_context(&self, context_id: &str) -> anyhow::Result<Context>;
fn delete_context(&self, context_id: &str) -> anyhow::Result<()>;
}
pub fn for_each_jsonl_record(
file_path: &Path,
mut f: impl FnMut(usize, &Value),
) -> anyhow::Result<()> {
let file =
fs::File::open(file_path).with_context(|| format!("open {}", file_path.display()))?;
let mut reader = BufReader::new(file);
let mut buf = Vec::new();
let mut line_num = 0;
loop {
buf.clear();
let read = reader
.read_until(b'\n', &mut buf)
.with_context(|| format!("{}: read error", file_path.display()))?;
if read == 0 {
break;
}
let line = String::from_utf8_lossy(&buf);
let trimmed = line.trim();
if !trimmed.is_empty()
&& let Ok(value) = serde_json::from_str::<Value>(trimmed)
{
f(line_num, &value);
}
line_num += 1;
}
Ok(())
}
pub fn open_sqlite(path: &Path) -> anyhow::Result<rusqlite::Connection> {
let conn =
rusqlite::Connection::open(path).with_context(|| format!("open {}", path.display()))?;
let _ = conn.busy_timeout(std::time::Duration::from_secs(5));
Ok(conn)
}
#[must_use]
pub fn content_fingerprint(ctx: &Context) -> String {
let rows: Vec<Value> = ctx
.messages
.iter()
.map(|m| {
let parts: Vec<Value> = m.parts.iter().map(part_fingerprint).collect();
json!([m.role, parts])
})
.collect();
serde_json::to_string(&rows).unwrap_or_default()
}
fn part_fingerprint(part: &Part) -> Value {
match part {
Part::Text(text) => json!(["text", text]),
Part::Reasoning(reasoning) => json!(["reasoning", reasoning.text]),
Part::ToolCall(call) => json!(["tool_call", call.name, call.arguments]),
Part::ToolResult(result) => {
json!([
"tool_result",
result.tool_name,
result.content,
result.is_error
])
}
Part::Image(image) => json!(["image", image.mime_type, image.data]),
Part::Bash(bash) => json!(["bash", bash.command, bash.output]),
Part::Passthrough(passthrough) => json!(["passthrough", passthrough.kind]),
}
}
pub fn active_lineage_ids(entries: &[Entry]) -> Vec<String> {
let parent_map: HashMap<&str, &str> = entries
.iter()
.filter(|e| !e.parent_id.is_empty())
.map(|e| (e.id.as_str(), e.parent_id.as_str()))
.collect();
let child_ids: HashSet<&str> = parent_map.values().copied().collect();
let mut leaves: Vec<&str> = entries
.iter()
.map(|e| e.id.as_str())
.filter(|id| !child_ids.contains(id))
.collect();
leaves.sort_by_key(|&id| entries.iter().position(|e| e.id == id));
let mut active_ids = Vec::new();
for leaf in leaves {
let mut current = leaf;
let mut visited = HashSet::new();
active_ids.push(current.to_string());
while let Some(&parent) = parent_map.get(current) {
if parent.is_empty() || !visited.insert(parent) {
break;
}
active_ids.push(parent.to_string());
current = parent;
}
}
active_ids
}
pub fn filter_messages(
messages: Vec<ConversationMessage>,
ids: &[String],
) -> Vec<ConversationMessage> {
if ids.is_empty() {
return messages;
}
let id_set: HashSet<&str> = ids.iter().map(std::string::String::as_str).collect();
messages
.into_iter()
.filter(|m| id_set.contains(m.entry_id.as_str()))
.collect()
}
pub fn filter_messages_range(
messages: Vec<ConversationMessage>,
from: Option<&str>,
until: Option<&str>,
) -> Result<Vec<ConversationMessage>, &'static str> {
let start = match from {
Some(entry_id) => messages
.iter()
.position(|message| message.entry_id == entry_id)
.ok_or("goosedump: --from <from> not found")?,
None => 0,
};
let end = match until {
Some(entry_id) => messages
.iter()
.position(|message| message.entry_id == entry_id)
.ok_or("goosedump: --until <until> not found")?,
None => messages.len(),
};
if start > end {
return Err("goosedump: --from precedes --until");
}
Ok(messages.into_iter().skip(start).take(end - start).collect())
}