use std::io::Read;
use serde::Deserialize;
use tracing::{Level, instrument};
use crate::AgentKind;
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub enum HookInput {
ToolUse(ToolUseHookInput),
SessionStart(SessionStartHookInput),
}
#[derive(Debug, Clone, Deserialize, Default)]
pub struct ToolUseHookInput {
pub session_id: String,
pub transcript_path: String,
pub cwd: String,
pub permission_mode: String,
pub hook_event_name: String,
pub tool_name: String,
pub tool_input: serde_json::Value,
pub tool_use_id: Option<String>,
#[serde(default)]
pub tool_response: Option<serde_json::Value>,
#[serde(skip)]
pub agent: Option<AgentKind>,
#[serde(skip)]
pub original_tool_name: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Default)]
pub struct SessionStartHookInput {
#[serde(default)]
pub session_id: String,
#[serde(default)]
pub transcript_path: String,
#[serde(default)]
pub cwd: String,
#[serde(default)]
pub permission_mode: Option<String>,
#[serde(default)]
pub hook_event_name: String,
#[serde(default)]
pub source: Option<String>,
#[serde(default)]
pub model: Option<String>,
}
impl SessionStartHookInput {
#[instrument(level = Level::TRACE, skip(reader))]
pub fn from_reader(reader: impl Read) -> anyhow::Result<Self> {
Ok(serde_json::from_reader(reader)?)
}
}
#[derive(Debug, Clone, Deserialize, Default)]
pub struct StopHookInput {
#[serde(default)]
pub session_id: String,
#[serde(default)]
pub transcript_path: String,
#[serde(default)]
pub cwd: String,
#[serde(default)]
pub hook_event_name: String,
}
impl StopHookInput {
#[instrument(level = Level::TRACE, skip(reader))]
pub fn from_reader(reader: impl Read) -> anyhow::Result<Self> {
Ok(serde_json::from_reader(reader)?)
}
}
impl HookInput {
#[instrument(level = Level::TRACE, skip(reader))]
pub fn from_reader(reader: impl Read) -> anyhow::Result<Self> {
Ok(serde_json::from_reader(reader)?)
}
#[instrument(level = Level::TRACE)]
pub fn from_stdin() -> anyhow::Result<Self> {
Self::from_reader(std::io::stdin().lock())
}
pub fn hook_event_name(&self) -> &str {
match self {
HookInput::ToolUse(input) => &input.hook_event_name,
HookInput::SessionStart(input) => &input.hook_event_name,
}
}
pub fn session_id(&self) -> &str {
match self {
HookInput::ToolUse(input) => &input.session_id,
HookInput::SessionStart(input) => &input.session_id,
}
}
pub fn as_tool_use(&self) -> Option<&ToolUseHookInput> {
match self {
HookInput::ToolUse(input) => Some(input),
_ => None,
}
}
pub fn as_session_start(&self) -> Option<&SessionStartHookInput> {
match self {
HookInput::SessionStart(input) => Some(input),
_ => None,
}
}
}
impl ToolUseHookInput {
#[instrument(level = Level::TRACE, skip(reader))]
pub fn from_reader(reader: impl Read) -> anyhow::Result<Self> {
Ok(serde_json::from_reader(reader)?)
}
}
pub mod exit_code {
pub const SUCCESS: i32 = 0;
pub const BLOCKING_ERROR: i32 = 2;
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_tool_use_json() -> &'static str {
r#"{
"session_id": "test-session",
"transcript_path": "/tmp/transcript.jsonl",
"cwd": "/home/user/project",
"permission_mode": "default",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {"command": "git status", "timeout": 120000},
"tool_use_id": "toolu_01ABC"
}"#
}
#[test]
fn test_parse_tool_use_input() {
let input = HookInput::from_reader(sample_tool_use_json().as_bytes()).unwrap();
assert_eq!(input.session_id(), "test-session");
assert_eq!(input.hook_event_name(), "PreToolUse");
let tool_use = input.as_tool_use().expect("Should be ToolUse variant");
assert_eq!(tool_use.tool_name, "Bash");
}
}