skiagram_core/adapters/mod.rs
1//! The `Adapter` trait — skiagram's extension point (CLAUDE.md §5).
2//!
3//! Adding a new agent = implement [`Adapter`] + register it in [`all`] + commit
4//! redacted fixtures and snapshot tests. Keep the trait minimal and stable.
5
6pub mod claude_code;
7pub mod codex;
8pub mod copilot;
9pub mod cursor;
10pub mod gemini;
11
12use crate::error::CoreError;
13use crate::model::{Session, SessionRef};
14
15/// One supported agent's on-disk format.
16pub trait Adapter {
17 /// Stable identifier used by `--agent`, e.g. `"claude-code"`.
18 fn id(&self) -> &'static str;
19 /// Cheap check: are this agent's data files present on this machine?
20 fn detect(&self) -> bool;
21 /// Find session files (read-only; never modifies anything).
22 fn discover(&self) -> anyhow::Result<Vec<SessionRef>>;
23 /// Parse one session file into the normalized model. Must be lenient:
24 /// unknown/corrupt lines are skipped with a `tracing::debug`, never a panic,
25 /// and never abort the whole file (CLAUDE.md §9).
26 fn parse(&self, r: &SessionRef) -> anyhow::Result<Session>;
27}
28
29/// Every known adapter, in priority order (fully implemented ones first; the
30/// deferred Cursor stub is last so a real coding-agent wins auto-detect).
31pub fn all() -> Vec<Box<dyn Adapter>> {
32 vec![
33 Box::new(claude_code::ClaudeCode),
34 Box::new(codex::Codex),
35 Box::new(gemini::Gemini),
36 Box::new(copilot::Copilot),
37 Box::new(cursor::Cursor),
38 ]
39}
40
41/// Look an adapter up by id (the `--agent` flag).
42pub fn by_id(id: &str) -> Result<Box<dyn Adapter>, CoreError> {
43 all()
44 .into_iter()
45 .find(|a| a.id() == id)
46 .ok_or_else(|| CoreError::UnknownAgent {
47 requested: id.to_string(),
48 known: all().iter().map(|a| a.id()).collect::<Vec<_>>().join(", "),
49 })
50}
51
52/// First adapter whose data files exist on this machine (used when `--agent` is
53/// not given).
54pub fn auto_detect() -> Option<Box<dyn Adapter>> {
55 all().into_iter().find(|a| a.detect())
56}