use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum AgentType {
ClaudeCode,
Gemini,
Qwen,
PiMono,
OhMyPi,
PiSkills,
OpenCode,
Codex,
Amp,
Droid,
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::Generic => write!(f, "generic"),
}
}
}
impl AgentType {
pub fn from_str(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" => Some(Self::Droid),
_ => None,
}
}
pub fn detection_layer(&self) -> DetectionLayer {
match self {
AgentType::ClaudeCode
| AgentType::Gemini
| AgentType::Qwen
| AgentType::PiMono
| AgentType::OhMyPi
| AgentType::PiSkills => DetectionLayer::Native,
AgentType::OpenCode | AgentType::Codex | AgentType::Amp | AgentType::Droid => {
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"],
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 => ".droid",
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",
}
}
}
#[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_from_str() {
assert_eq!(
AgentType::from_str("claude-code"),
Some(AgentType::ClaudeCode)
);
assert_eq!(AgentType::from_str("claude"), Some(AgentType::ClaudeCode));
assert_eq!(AgentType::from_str("pi"), Some(AgentType::PiMono));
assert_eq!(AgentType::from_str("omp"), Some(AgentType::OhMyPi));
assert_eq!(AgentType::from_str("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::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()));
}
}