mod engine;
mod events;
mod matcher;
pub use engine::{Hook, HookConfig, HookEngine, HookExecutor, HookHandler, HookResult};
pub use events::{
ErrorType, GenerateEndEvent, GenerateStartEvent, HookEvent, HookEventType, OnErrorEvent,
PostResponseEvent, PostToolUseEvent, PrePromptEvent, PreToolUseEvent, SessionEndEvent,
SessionStartEvent, SkillLoadEvent, SkillUnloadEvent, TokenUsageInfo, ToolCallInfo,
ToolResultData,
};
pub use matcher::HookMatcher;
#[derive(Debug, Clone, PartialEq)]
pub enum HookAction {
Continue,
Block,
Retry,
Skip,
}
#[derive(Debug, Clone)]
pub struct HookResponse {
pub hook_id: String,
pub action: HookAction,
pub reason: Option<String>,
pub modified: Option<serde_json::Value>,
pub retry_delay_ms: Option<u64>,
}
impl HookResponse {
pub fn continue_() -> Self {
Self {
hook_id: String::new(),
action: HookAction::Continue,
reason: None,
modified: None,
retry_delay_ms: None,
}
}
pub fn continue_with(modified: serde_json::Value) -> Self {
Self {
hook_id: String::new(),
action: HookAction::Continue,
reason: None,
modified: Some(modified),
retry_delay_ms: None,
}
}
pub fn block(reason: impl Into<String>) -> Self {
Self {
hook_id: String::new(),
action: HookAction::Block,
reason: Some(reason.into()),
modified: None,
retry_delay_ms: None,
}
}
pub fn retry(delay_ms: u64) -> Self {
Self {
hook_id: String::new(),
action: HookAction::Retry,
reason: None,
modified: None,
retry_delay_ms: Some(delay_ms),
}
}
pub fn skip() -> Self {
Self {
hook_id: String::new(),
action: HookAction::Skip,
reason: None,
modified: None,
retry_delay_ms: None,
}
}
pub fn with_hook_id(mut self, id: impl Into<String>) -> Self {
self.hook_id = id.into();
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hook_response_continue() {
let response = HookResponse::continue_();
assert_eq!(response.action, HookAction::Continue);
assert!(response.reason.is_none());
assert!(response.modified.is_none());
}
#[test]
fn test_hook_response_continue_with_modified() {
let modified = serde_json::json!({"timeout": 5000});
let response = HookResponse::continue_with(modified.clone());
assert_eq!(response.action, HookAction::Continue);
assert_eq!(response.modified, Some(modified));
}
#[test]
fn test_hook_response_block() {
let response = HookResponse::block("Dangerous command");
assert_eq!(response.action, HookAction::Block);
assert_eq!(response.reason, Some("Dangerous command".to_string()));
}
#[test]
fn test_hook_response_retry() {
let response = HookResponse::retry(1000);
assert_eq!(response.action, HookAction::Retry);
assert_eq!(response.retry_delay_ms, Some(1000));
}
#[test]
fn test_hook_response_skip() {
let response = HookResponse::skip();
assert_eq!(response.action, HookAction::Skip);
}
#[test]
fn test_hook_response_with_hook_id() {
let response = HookResponse::continue_().with_hook_id("hook-123");
assert_eq!(response.hook_id, "hook-123");
}
}