use crate::error::Result;
use crate::storage::SessionIndex;
use std::io::{self, BufRead};
#[derive(serde::Deserialize, Default)]
#[serde(default)]
pub struct FullHookPayload {
pub session_id: Option<String>,
pub transcript_path: Option<String>,
pub cwd: Option<String>,
pub hook_event_name: Option<String>,
pub tool_name: Option<String>,
pub tool_input: Option<serde_json::Value>,
pub tool_response: Option<serde_json::Value>,
pub error: Option<String>,
pub is_interrupt: Option<bool>,
pub tool_use_id: Option<String>,
pub agent_type: Option<String>,
pub agent_name: Option<String>,
pub action: Option<String>,
pub result: Option<String>,
pub worktree_path: Option<String>,
pub branch: Option<String>,
pub prompt: Option<String>,
pub key: Option<String>,
pub old_value: Option<serde_json::Value>,
pub new_value: Option<serde_json::Value>,
}
pub fn read_payload() -> FullHookPayload {
let stdin = io::stdin();
let mut raw = String::new();
for line in stdin.lock().lines() {
match line {
Ok(l) => {
raw.push_str(&l);
raw.push('\n');
}
Err(_) => break,
}
}
serde_json::from_str(raw.trim()).unwrap_or_default()
}
fn with_session(f: impl FnOnce(&str, &SessionIndex, &FullHookPayload)) -> Result<()> {
let p = read_payload();
let sid = match p.session_id.as_deref() {
Some(s) if !s.is_empty() => s,
_ => return Ok(()),
};
if let Ok(idx) = SessionIndex::new() {
f(sid, &idx, &p);
}
Ok(())
}
fn ensure_otlp_daemon() {
use std::net::TcpStream;
use std::time::Duration;
let port: u16 = crate::config::Config::load()
.map(|c| c.telemetry.otel_port)
.unwrap_or(7228);
if TcpStream::connect_timeout(
&std::net::SocketAddr::from(([127, 0, 0, 1], port)),
Duration::from_millis(200),
)
.is_ok()
{
return;
}
let bin = match std::env::current_exe() {
Ok(p) => p,
Err(_) => return,
};
let _ = std::process::Command::new(bin)
.arg("daemon")
.arg("--port")
.arg(port.to_string())
.arg("--idle-timeout")
.arg("600")
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn(); }
pub fn run_session_start() -> Result<()> {
ensure_otlp_daemon();
crate::commands::hook_index::run()
}
pub fn run_stop() -> Result<()> {
crate::commands::hook_index::run()
}
pub fn run_session_end() -> Result<()> {
crate::commands::hook_index::run()
}
pub fn run_user_prompt_submit() -> Result<()> {
ensure_otlp_daemon();
with_session(|sid, idx, p| {
let attrs = serde_json::json!({
"prompt": p.prompt,
"cwd": p.cwd,
});
let attrs_str = attrs.to_string();
let _ = idx.insert_hook_lifecycle_event(sid, "UserPromptSubmit", Some(&attrs_str));
})
}
pub fn run_pre_tool_use() -> Result<()> {
ensure_otlp_daemon();
with_session(|sid, idx, p| {
let input_str = p.tool_input.as_ref().map(|v| v.to_string());
let _ = idx.insert_hook_tool_event(
sid,
"PreToolUse",
p.tool_name.as_deref(),
input_str.as_deref(),
None,
None,
None,
p.tool_use_id.as_deref(),
p.cwd.as_deref(),
);
})
}
pub fn run_post_tool_use() -> Result<()> {
with_session(|sid, idx, p| {
let input_str = p.tool_input.as_ref().map(|v| v.to_string());
let result_str = p.tool_response.as_ref().map(|v| v.to_string());
let _ = idx.insert_hook_tool_event(
sid,
"PostToolUse",
p.tool_name.as_deref(),
input_str.as_deref(),
result_str.as_deref(),
None,
p.is_interrupt,
p.tool_use_id.as_deref(),
p.cwd.as_deref(),
);
})
}
pub fn run_post_tool_use_failure() -> Result<()> {
with_session(|sid, idx, p| {
let input_str = p.tool_input.as_ref().map(|v| v.to_string());
let _ = idx.insert_hook_tool_event(
sid,
"PostToolUseFailure",
p.tool_name.as_deref(),
input_str.as_deref(),
None,
p.error.as_deref(),
p.is_interrupt,
p.tool_use_id.as_deref(),
p.cwd.as_deref(),
);
})
}
pub fn run_subagent_start() -> Result<()> {
with_session(|sid, idx, p| {
let _ = idx.insert_hook_subagent_event(
sid,
"SubagentStart",
p.agent_type.as_deref(),
p.agent_name.as_deref(),
p.cwd.as_deref(),
);
})
}
pub fn run_subagent_stop() -> Result<()> {
with_session(|sid, idx, p| {
let _ = idx.insert_hook_subagent_event(
sid,
"SubagentStop",
p.agent_type.as_deref(),
p.agent_name.as_deref(),
p.cwd.as_deref(),
);
})
}
pub fn run_pre_compact() -> Result<()> {
with_session(|sid, idx, p| {
let _ = idx.insert_hook_compaction_event(sid, p.hook_event_name.as_deref());
})
}
pub fn run_permission_request() -> Result<()> {
with_session(|sid, idx, p| {
let input_str = p.tool_input.as_ref().map(|v| v.to_string());
let _ = idx.insert_hook_permission_event(
sid,
p.tool_name.as_deref(),
input_str.as_deref(),
p.cwd.as_deref(),
);
})
}
fn run_lifecycle(event_name: &str, payload: FullHookPayload) -> Result<()> {
let sid = match payload.session_id.as_deref() {
Some(s) if !s.is_empty() => s,
_ => return Ok(()),
};
if let Ok(idx) = SessionIndex::new() {
let attrs = serde_json::json!({
"result": payload.result,
"worktree_path": payload.worktree_path,
"branch": payload.branch,
"key": payload.key,
"old_value": payload.old_value,
"new_value": payload.new_value,
"cwd": payload.cwd,
});
let attrs_str = attrs.to_string();
let _ = idx.insert_hook_lifecycle_event(sid, event_name, Some(&attrs_str));
}
Ok(())
}
pub fn run_task_completed() -> Result<()> {
run_lifecycle("TaskCompleted", read_payload())
}
pub fn run_worktree_create() -> Result<()> {
run_lifecycle("WorktreeCreate", read_payload())
}
pub fn run_worktree_remove() -> Result<()> {
run_lifecycle("WorktreeRemove", read_payload())
}
pub fn run_config_change() -> Result<()> {
run_lifecycle("ConfigChange", read_payload())
}