1#[derive(Subcommand)]
2pub enum HookCommands {
3 Stop,
5}
6fn extract_content_text(content: Option<&serde_json::Value>) -> String {
7 match content {
8 None => String::new(),
9 Some(serde_json::Value::String(s)) => s.clone(),
10 Some(serde_json::Value::Array(arr)) => arr
11 .iter()
12 .filter_map(|b| b.get("text").and_then(|t| t.as_str()))
13 .collect::<Vec<_>>()
14 .join(" "),
15 _ => String::new(),
16 }
17}
18
19fn run_hook_stop() -> anyhow::Result<()> {
20 use std::io::{Read, Write};
21
22 let mut input = String::new();
23 std::io::stdin().read_to_string(&mut input)?;
24
25 let data: serde_json::Value = serde_json::from_str(&input).unwrap_or(serde_json::Value::Null);
26
27 let empty = vec![];
28 let transcript = data
29 .get("transcript")
30 .or_else(|| data.get("messages"))
31 .and_then(|v| v.as_array())
32 .unwrap_or(&empty);
33
34 let mut query = String::new();
35 let mut summary = String::new();
36
37 for m in transcript.iter().rev() {
38 let role = m.get("role").and_then(|r| r.as_str()).unwrap_or("");
39 if query.is_empty() && role == "user" {
40 query = extract_content_text(m.get("content"))
41 .chars()
42 .take(200)
43 .collect();
44 }
45 if summary.is_empty() && role == "assistant" {
46 summary = extract_content_text(m.get("content"))
47 .chars()
48 .take(400)
49 .collect();
50 }
51 if !query.is_empty() && !summary.is_empty() {
52 break;
53 }
54 }
55
56 let mut events: Vec<serde_json::Value> = Vec::new();
57 if !query.is_empty() {
58 events.push(json!({"event_type": "session_start", "query": query.trim()}));
59 }
60 if !summary.is_empty() {
61 events.push(json!({"event_type": "tool_success", "output_summary": summary.trim(), "outcome": "ok"}));
62 }
63 events.push(json!({"event_type": "session_end"}));
64
65 let log_path = crate::paths::session_log_path();
66
67 if let Some(parent) = log_path.parent() {
68 std::fs::create_dir_all(parent)?;
69 }
70
71 let mut file = std::fs::OpenOptions::new()
72 .create(true)
73 .append(true)
74 .open(&log_path)?;
75
76 for event in &events {
77 writeln!(file, "{}", serde_json::to_string(event)?)?;
78 }
79
80 Ok(())
81}
82
83pub(crate) fn run_command(action: &HookCommands) -> anyhow::Result<()> {
84 match action {
85 HookCommands::Stop => run_hook_stop(),
86 }
87}
88use clap::Subcommand;
89use serde_json::json;