use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SupportTier {
NativeLifecycle,
WrapperLifecycle,
MonitorOnly,
}
impl std::fmt::Display for SupportTier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SupportTier::NativeLifecycle => write!(f, "native-lifecycle"),
SupportTier::WrapperLifecycle => write!(f, "wrapper-lifecycle"),
SupportTier::MonitorOnly => write!(f, "monitor-only"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum AgentType {
ClaudeCode,
PiMono,
OhMyPi,
PiSkills,
Gemini,
Qwen,
OpenCode,
Codex,
Amp,
Droid,
Hermes,
Generic,
}
impl std::fmt::Display for AgentType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AgentType::ClaudeCode => write!(f, "claude-code"),
AgentType::Gemini => write!(f, "gemini"),
AgentType::Qwen => write!(f, "qwen"),
AgentType::PiMono => write!(f, "pi-mono"),
AgentType::OhMyPi => write!(f, "oh-my-pi"),
AgentType::PiSkills => write!(f, "pi-skills"),
AgentType::OpenCode => write!(f, "opencode"),
AgentType::Codex => write!(f, "codex"),
AgentType::Amp => write!(f, "amp"),
AgentType::Droid => write!(f, "droid"),
AgentType::Hermes => write!(f, "hermes"),
AgentType::Generic => write!(f, "generic"),
}
}
}
impl AgentType {
pub fn parse(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"claude-code" | "claude" => Some(Self::ClaudeCode),
"gemini" => Some(Self::Gemini),
"qwen" => Some(Self::Qwen),
"pi-mono" | "pimono" | "pi" => Some(Self::PiMono),
"oh-my-pi" | "ohmypi" | "omp" => Some(Self::OhMyPi),
"pi-skills" => Some(Self::PiSkills),
"opencode" => Some(Self::OpenCode),
"codex" => Some(Self::Codex),
"amp" => Some(Self::Amp),
"droid" | "factory" | "factory-cli" => Some(Self::Droid),
"hermes" | "hermes-cli" => Some(Self::Hermes),
_ => None,
}
}
pub fn detection_layer(&self) -> DetectionLayer {
match self {
AgentType::ClaudeCode
| AgentType::PiMono
| AgentType::OhMyPi
| AgentType::PiSkills
| AgentType::Droid => DetectionLayer::Native,
AgentType::Gemini | AgentType::Qwen => DetectionLayer::Monitor,
AgentType::OpenCode | AgentType::Codex | AgentType::Amp | AgentType::Hermes => {
DetectionLayer::CLI
}
AgentType::Generic => DetectionLayer::CLI,
}
}
pub fn process_names(&self) -> &'static [&'static str] {
match self {
AgentType::ClaudeCode => &["claude", "claude-code"],
AgentType::Gemini => &["gemini", "gemini-cli"],
AgentType::Qwen => &["qwen", "qwen-agent"],
AgentType::PiMono => &["pi", "pi-coding-agent", "pi-mono"],
AgentType::OhMyPi => &["omp", "oh-my-pi", "ohmypi"],
AgentType::PiSkills => &["pi-skills"],
AgentType::OpenCode => &["opencode"],
AgentType::Codex => &["codex", "codex-cli"],
AgentType::Amp => &["amp"],
AgentType::Droid => &["droid", "factory-cli"],
AgentType::Hermes => &["hermes", "hermes-cli"],
AgentType::Generic => &[],
}
}
pub fn config_dir(&self) -> &'static str {
match self {
AgentType::ClaudeCode => ".claude",
AgentType::Gemini => ".gemini",
AgentType::Qwen => ".qwen",
AgentType::PiMono => ".pi",
AgentType::OhMyPi => ".omp",
AgentType::PiSkills => ".pi-skills",
AgentType::OpenCode => ".opencode",
AgentType::Codex => ".codex",
AgentType::Amp => ".amp",
AgentType::Droid => ".factory",
AgentType::Hermes => ".hermes",
AgentType::Generic => ".nexus",
}
}
pub fn skills_dir(&self) -> &'static str {
match self {
AgentType::ClaudeCode => "skills",
AgentType::PiMono => "agent/skills",
AgentType::OhMyPi => "agent/skills",
AgentType::PiSkills => "skills",
_ => "skills",
}
}
pub fn support_tier(&self) -> SupportTier {
match self {
AgentType::ClaudeCode => SupportTier::NativeLifecycle,
AgentType::PiMono => SupportTier::NativeLifecycle,
AgentType::OhMyPi => SupportTier::NativeLifecycle,
AgentType::PiSkills => SupportTier::NativeLifecycle,
AgentType::Gemini => SupportTier::MonitorOnly,
AgentType::Qwen => SupportTier::MonitorOnly,
AgentType::OpenCode
| AgentType::Codex
| AgentType::Amp
| AgentType::Hermes
| AgentType::Generic => SupportTier::WrapperLifecycle,
AgentType::Droid => SupportTier::NativeLifecycle,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DetectionLayer {
Native,
CLI,
Monitor,
Inactivity,
Buffer,
}
impl std::fmt::Display for DetectionLayer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DetectionLayer::Native => write!(f, "native"),
DetectionLayer::CLI => write!(f, "cli"),
DetectionLayer::Monitor => write!(f, "monitor"),
DetectionLayer::Inactivity => write!(f, "inactivity"),
DetectionLayer::Buffer => write!(f, "buffer"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ExtractionSource {
NativeHook(String),
ProcessMonitor,
InactivityTimeout,
SignalHandler(String),
BufferRecovery,
Manual,
}
impl std::fmt::Display for ExtractionSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ExtractionSource::NativeHook(name) => write!(f, "native:{}", name),
ExtractionSource::ProcessMonitor => write!(f, "process_monitor"),
ExtractionSource::InactivityTimeout => write!(f, "inactivity_timeout"),
ExtractionSource::SignalHandler(sig) => write!(f, "signal:{}", sig),
ExtractionSource::BufferRecovery => write!(f, "buffer_recovery"),
ExtractionSource::Manual => write!(f, "manual"),
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SkillMetadata {
pub name: String,
pub description: Option<String>,
pub version: Option<String>,
pub author: Option<String>,
#[serde(default)]
pub triggers: Vec<SkillTrigger>,
pub priority: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SkillTrigger {
OnSessionEnd,
OnCheckpoint,
OnCompletion,
OnError,
Manual,
}
impl std::fmt::Display for SkillTrigger {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SkillTrigger::OnSessionEnd => write!(f, "on_session_end"),
SkillTrigger::OnCheckpoint => write!(f, "on_checkpoint"),
SkillTrigger::OnCompletion => write!(f, "on_completion"),
SkillTrigger::OnError => write!(f, "on_error"),
SkillTrigger::Manual => write!(f, "manual"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProcessInfo {
pub pid: u32,
pub name: String,
pub status: String,
pub command: Option<String>,
pub working_dir: Option<String>,
pub create_time: Option<DateTime<Utc>>,
pub cpu_percent: Option<f32>,
pub memory_bytes: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionActivity {
pub agent_type: AgentType,
pub is_active: bool,
pub processes: Vec<ProcessInfo>,
pub session_id: Option<String>,
pub last_activity: Option<DateTime<Utc>>,
pub context: HashMap<String, serde_json::Value>,
}
impl SessionActivity {
pub fn new(agent_type: AgentType) -> Self {
Self {
agent_type,
is_active: false,
processes: Vec::new(),
session_id: None,
last_activity: None,
context: HashMap::new(),
}
}
pub fn with_active(mut self, active: bool) -> Self {
self.is_active = active;
self
}
pub fn with_session_id(mut self, id: impl Into<String>) -> Self {
self.session_id = Some(id.into());
self
}
pub fn with_process(mut self, process: ProcessInfo) -> Self {
self.processes.push(process);
self.is_active = true;
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_agent_type_display() {
assert_eq!(AgentType::ClaudeCode.to_string(), "claude-code");
assert_eq!(AgentType::PiMono.to_string(), "pi-mono");
assert_eq!(AgentType::OhMyPi.to_string(), "oh-my-pi");
}
#[test]
fn test_agent_type_parse() {
assert_eq!(AgentType::parse("claude-code"), Some(AgentType::ClaudeCode));
assert_eq!(AgentType::parse("claude"), Some(AgentType::ClaudeCode));
assert_eq!(AgentType::parse("pi"), Some(AgentType::PiMono));
assert_eq!(AgentType::parse("omp"), Some(AgentType::OhMyPi));
assert_eq!(AgentType::parse("unknown"), None);
}
#[test]
fn test_agent_type_detection_layer() {
assert_eq!(
AgentType::ClaudeCode.detection_layer(),
DetectionLayer::Native
);
assert_eq!(AgentType::PiMono.detection_layer(), DetectionLayer::Native);
assert_eq!(AgentType::Gemini.detection_layer(), DetectionLayer::Monitor);
assert_eq!(AgentType::Qwen.detection_layer(), DetectionLayer::Monitor);
assert_eq!(AgentType::Amp.detection_layer(), DetectionLayer::CLI);
}
#[test]
fn test_agent_type_process_names() {
let names = AgentType::PiMono.process_names();
assert!(names.contains(&"pi"));
assert!(names.contains(&"pi-mono"));
}
#[test]
fn test_agent_type_config_dir() {
assert_eq!(AgentType::PiMono.config_dir(), ".pi");
assert_eq!(AgentType::OhMyPi.config_dir(), ".omp");
assert_eq!(AgentType::ClaudeCode.config_dir(), ".claude");
}
#[test]
fn test_detection_layer_display() {
assert_eq!(DetectionLayer::Native.to_string(), "native");
assert_eq!(DetectionLayer::Buffer.to_string(), "buffer");
}
#[test]
fn test_session_activity() {
let activity = SessionActivity::new(AgentType::ClaudeCode)
.with_active(true)
.with_session_id("test-123");
assert!(activity.is_active);
assert_eq!(activity.session_id, Some("test-123".to_string()));
}
#[test]
fn test_support_tier_display() {
assert_eq!(SupportTier::NativeLifecycle.to_string(), "native-lifecycle");
assert_eq!(
SupportTier::WrapperLifecycle.to_string(),
"wrapper-lifecycle"
);
assert_eq!(SupportTier::MonitorOnly.to_string(), "monitor-only");
}
#[test]
fn test_agent_support_tier_honest_mapping() {
assert_eq!(
AgentType::ClaudeCode.support_tier(),
SupportTier::NativeLifecycle
);
assert_eq!(
AgentType::PiMono.support_tier(),
SupportTier::NativeLifecycle
);
assert_eq!(
AgentType::OhMyPi.support_tier(),
SupportTier::NativeLifecycle
);
assert_eq!(
AgentType::PiSkills.support_tier(),
SupportTier::NativeLifecycle
);
assert_eq!(
AgentType::Droid.support_tier(),
SupportTier::NativeLifecycle
);
assert_eq!(AgentType::Gemini.support_tier(), SupportTier::MonitorOnly);
assert_eq!(AgentType::Qwen.support_tier(), SupportTier::MonitorOnly);
assert_eq!(
AgentType::Codex.support_tier(),
SupportTier::WrapperLifecycle
);
assert_eq!(AgentType::Amp.support_tier(), SupportTier::WrapperLifecycle);
assert_eq!(
AgentType::Generic.support_tier(),
SupportTier::WrapperLifecycle
);
}
#[test]
fn test_all_agents_have_valid_detection_layer() {
for agent_type in &[
AgentType::ClaudeCode,
AgentType::Gemini,
AgentType::Qwen,
AgentType::PiMono,
AgentType::OhMyPi,
AgentType::PiSkills,
AgentType::OpenCode,
AgentType::Codex,
AgentType::Amp,
AgentType::Droid,
AgentType::Hermes,
] {
assert!(
!agent_type.config_dir().is_empty(),
"{} must have a config_dir",
agent_type
);
assert!(
!agent_type.to_string().is_empty(),
"{} must have a display name",
agent_type
);
}
}
#[test]
fn test_wrapper_agents_have_wrapper_lifecycle_tier() {
let wrapper_agents = [
AgentType::OpenCode,
AgentType::Codex,
AgentType::Amp,
AgentType::Hermes,
];
for agent in &wrapper_agents {
assert_eq!(
agent.support_tier(),
SupportTier::WrapperLifecycle,
"{} should be WrapperLifecycle",
agent
);
}
}
}