use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorClass {
Transport,
Client,
Fatal,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum HookEvent {
PostToolUse {
tool_name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
file_path: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
diff: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
session_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
new_text: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
old_text: Option<String>,
},
PreToolUseRead {
file_path: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
session_id: Option<String>,
},
SessionStart {
cwd: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
session_id: Option<String>,
},
UserPromptSubmit {
prompt: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
session_id: Option<String>,
},
Stop {
#[serde(default, skip_serializing_if = "Option::is_none")]
session_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
transcript_path: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
cwd: Option<String>,
},
SessionEnd {
#[serde(default, skip_serializing_if = "Option::is_none")]
session_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
transcript_path: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
cwd: Option<String>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub struct HookResult {
pub continue_: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub system_message: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub additional_context: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub event_name: Option<String>,
#[serde(default, skip)]
pub rules_injected: Option<usize>,
}
impl HookResult {
pub(crate) const fn noop() -> Self {
Self {
continue_: true,
system_message: None,
additional_context: None,
event_name: None,
rules_injected: None,
}
}
pub(crate) fn with_context(ctx: impl Into<String>) -> Self {
Self {
continue_: true,
system_message: None,
additional_context: Some(ctx.into()),
event_name: None,
rules_injected: None,
}
}
}
impl HookEvent {
pub(crate) const fn wire_name(&self) -> &'static str {
match self {
Self::PreToolUseRead { .. } => "PreToolUse",
Self::PostToolUse { .. } => "PostToolUse",
Self::SessionStart { .. } => "SessionStart",
Self::UserPromptSubmit { .. } => "UserPromptSubmit",
Self::Stop { .. } => "Stop",
Self::SessionEnd { .. } => "SessionEnd",
}
}
pub(crate) fn target_file_path(&self) -> Option<String> {
match self {
Self::PreToolUseRead { file_path, .. } => Some(file_path.clone()),
Self::PostToolUse { file_path, .. } => file_path.clone(),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn post_tool_use_omits_absent_optional_fields() {
let event = HookEvent::PostToolUse {
tool_name: "Write".into(),
file_path: Some("README.md".into()),
diff: None,
session_id: None,
new_text: None,
old_text: None,
};
let s = serde_json::to_string(&event).unwrap();
assert!(!s.contains("diff"), "expected diff omitted, got: {s}");
}
#[test]
fn hook_result_noop_has_continue_true() {
let r = HookResult::noop();
assert!(r.continue_);
assert!(r.system_message.is_none());
assert!(r.additional_context.is_none());
}
}