pub(crate) mod claude;
mod codex;
pub(crate) mod common;
mod copilot;
mod cursor;
mod gemini;
use std::path::{Path, PathBuf};
use std::time::SystemTime;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MessageRole {
User,
Assistant,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Message {
pub role: MessageRole,
pub text: String,
}
impl Message {
pub fn user(text: String) -> Self {
Self {
role: MessageRole::User,
text,
}
}
pub fn assistant(text: String) -> Self {
Self {
role: MessageRole::Assistant,
text,
}
}
}
pub trait AgentPlugin: Sync {
fn id(&self) -> &'static str;
fn description(&self) -> &'static str;
fn glob_patterns(&self) -> &'static [&'static str];
fn path_markers(&self) -> &'static [&'static str];
fn can_search(&self) -> bool {
true
}
fn can_show(&self) -> bool {
true
}
fn can_resume(&self) -> bool {
false
}
fn can_detect_running(&self) -> bool {
false
}
fn can_memory(&self) -> bool {
false
}
fn can_follow(&self) -> bool {
false
}
fn project_desc(&self) -> &'static str {
"parent directory name of session file"
}
fn iter_messages(&self, path: &Path, visit: &mut dyn FnMut(Message) -> bool);
fn iter_messages_from_bytes(
&self,
path: &Path,
_data: &[u8],
visit: &mut dyn FnMut(Message) -> bool,
) {
self.iter_messages(path, visit);
}
fn messages_from_value(&self, _val: &serde_json::Value) -> Vec<Message> {
Vec::new()
}
fn search_path(&self, path: &Path) -> PathBuf {
path.to_path_buf()
}
fn resolve_project(&self, _path: &Path, _home: &Path) -> Option<String> {
None
}
fn resolve_date(&self, _path: &Path, _mtime: SystemTime) -> Option<String> {
None
}
fn resolve_cwd(&self, _path: &Path, _home: &Path) -> Option<String> {
None
}
fn resolve_cwd_from_mmap(&self, path: &Path, home: &Path, _mmap: &[u8]) -> Option<String> {
self.resolve_cwd(path, home)
}
fn resolve_title(&self, _path: &Path, _home: &Path) -> Option<String> {
None
}
fn resolve_title_from_mmap(&self, path: &Path, home: &Path, _mmap: &[u8]) -> Option<String> {
self.resolve_title(path, home)
}
fn resolve_resume_id(&self, _path: &Path, _home: &Path) -> Option<String> {
None
}
fn resume_args(&self, _path: &Path, _home: &Path) -> Option<Vec<String>> {
None
}
}
struct UnknownPlugin;
impl AgentPlugin for UnknownPlugin {
fn id(&self) -> &'static str {
"unknown"
}
fn description(&self) -> &'static str {
"Unknown agent"
}
fn glob_patterns(&self) -> &'static [&'static str] {
&[]
}
fn path_markers(&self) -> &'static [&'static str] {
&[]
}
fn can_search(&self) -> bool {
false
}
fn can_show(&self) -> bool {
false
}
fn iter_messages(&self, _path: &Path, _visit: &mut dyn FnMut(Message) -> bool) {}
}
static UNKNOWN_PLUGIN: UnknownPlugin = UnknownPlugin;
static PLUGINS: [&'static dyn AgentPlugin; 5] = [
&claude::PLUGIN,
&codex::PLUGIN,
&gemini::PLUGIN,
&copilot::PLUGIN,
&cursor::PLUGIN,
];
pub fn all_plugins() -> &'static [&'static dyn AgentPlugin] {
&PLUGINS
}
pub fn find_builtin_plugin(id: &str) -> Option<&'static dyn AgentPlugin> {
all_plugins()
.iter()
.copied()
.find(|plugin| plugin.id() == id)
}
#[cfg(test)]
pub fn find_plugin(id: &str) -> Option<&'static dyn AgentPlugin> {
find_builtin_plugin(id)
}
pub fn unknown_plugin() -> &'static dyn AgentPlugin {
&UNKNOWN_PLUGIN
}
pub fn find_plugin_for_path(path: &Path) -> &'static dyn AgentPlugin {
crate::config::find_plugin_for_path(path)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_find_plugin() {
assert_eq!(find_plugin("claude").unwrap().id(), "claude");
assert_eq!(find_plugin("codex").unwrap().id(), "codex");
assert_eq!(find_plugin("gemini").unwrap().id(), "gemini");
assert_eq!(find_plugin("copilot").unwrap().id(), "copilot");
assert_eq!(find_plugin("cursor").unwrap().id(), "cursor");
assert!(find_plugin("foobar").is_none());
}
#[test]
fn test_find_plugin_for_path() {
crate::config::init(Path::new("/nonexistent/home"));
assert_eq!(
find_plugin_for_path(Path::new("/home/user/.claude/projects/foo/bar.jsonl")).id(),
"claude"
);
assert_eq!(
find_plugin_for_path(Path::new(
"/home/user/.codex/sessions/2026/01/01/rollout.jsonl"
))
.id(),
"codex"
);
assert_eq!(
find_plugin_for_path(Path::new("/home/user/.gemini/tmp/proj/chats/session.json")).id(),
"gemini"
);
assert_eq!(
find_plugin_for_path(Path::new(
"/home/user/.copilot/session-state/uuid/workspace.yaml"
))
.id(),
"copilot"
);
assert_eq!(
find_plugin_for_path(Path::new(
"/home/user/.cursor/projects/foo/agent-transcripts/s.jsonl"
))
.id(),
"cursor"
);
assert_eq!(
find_plugin_for_path(Path::new("/tmp/random.txt")).id(),
"unknown"
);
}
}