1use std::collections::HashMap;
2use std::io::Read;
3use std::path::Path;
4
5use anyhow::{Context, Result};
6use mi6_core::{
7 Config, EventBuilder, EventType, FrameworkAdapter, ParsedHookInput, Storage, default_adapter,
8 detect_all_frameworks, detect_framework, get_adapter, get_branch_info,
9 is_branch_changing_command,
10};
11
12use crate::process::{ClaudeProcessInfo, find_claude_process, get_parent_pid};
13
14pub struct LogResult {
16 pub transcript_path: Option<String>,
18 pub machine_id: String,
20 pub session_id: String,
22}
23
24pub fn run_log<S: Storage>(
28 storage: &S,
29 event_type_arg: Option<String>,
30 json_payload: Option<String>,
31 framework_name: Option<String>,
32) -> Result<LogResult> {
33 let adapter: &dyn FrameworkAdapter = if let Some(ref name) = framework_name {
38 get_adapter(name).ok_or_else(|| anyhow::anyhow!("unknown framework: {}", name))?
39 } else {
40 let detected = detect_all_frameworks();
42 if detected.len() > 1 {
43 let names: Vec<_> = detected.iter().map(|a| a.name()).collect();
44 eprintln!(
45 "mi6: warning: multiple frameworks detected ({}), using {}",
46 names.join(", "),
47 detected[0].name()
48 );
49 }
50 detect_framework().unwrap_or_else(default_adapter)
51 };
52
53 let (actual_event_type, json_str) = if let Some(ref arg) = event_type_arg {
60 if arg.trim().starts_with('{') {
61 (None, arg.clone())
63 } else {
64 let json = if let Some(payload) = json_payload {
66 payload
67 } else {
68 let mut stdin_data = String::new();
69 std::io::stdin()
70 .read_to_string(&mut stdin_data)
71 .context("failed to read stdin")?;
72 stdin_data
73 };
74 (Some(arg.clone()), json)
75 }
76 } else {
77 let mut stdin_data = String::new();
79 std::io::stdin()
80 .read_to_string(&mut stdin_data)
81 .context("failed to read stdin")?;
82 (None, stdin_data)
83 };
84
85 let hook_data: serde_json::Value = if json_str.trim().is_empty() {
87 serde_json::json!({})
88 } else {
89 serde_json::from_str(&json_str).context("failed to parse hook JSON")?
90 };
91
92 let event_type_str = if let Some(ref et) = actual_event_type {
97 et.clone()
98 } else {
99 hook_data
101 .get("type")
102 .and_then(|v| v.as_str())
103 .map_or_else(|| "Unknown".to_string(), String::from)
104 };
105
106 let event_type: EventType = adapter.map_event_type(&event_type_str);
108
109 let parsed = adapter.parse_hook_input(&event_type_str, &hook_data);
111
112 let session_id = parsed
114 .session_id
115 .clone()
116 .unwrap_or_else(|| "unknown".to_string());
117
118 let process_info = if event_type == EventType::SessionStart {
120 find_claude_process()
121 } else {
122 None
123 };
124
125 let pid = process_info
127 .as_ref()
128 .map(|info| info.pid)
129 .or_else(get_parent_pid);
130
131 let payload = build_payload(hook_data, &event_type, process_info.as_ref())?;
133
134 let config = Config::load().unwrap_or_default();
136 let machine_id = config.machine_id();
137
138 let event = EventBuilder::new(&machine_id, event_type.clone(), session_id.clone())
140 .framework(adapter.name())
141 .tool_use_id_opt(parsed.tool_use_id.clone())
142 .spawned_agent_id_opt(parsed.spawned_agent_id.clone())
143 .tool_name_opt(parsed.tool_name.clone())
144 .subagent_type_opt(parsed.subagent_type.clone())
145 .permission_mode_opt(parsed.permission_mode.clone())
146 .transcript_path_opt(parsed.transcript_path.clone())
147 .pid_opt(pid)
148 .cwd_opt(parsed.cwd.clone())
149 .payload(payload)
150 .source("hook")
151 .build();
152
153 storage.insert(&event).context("failed to insert event")?;
154
155 capture_git_info_if_needed(storage, &event_type, &session_id, &parsed);
158
159 if rand::random::<u8>() < 3 {
161 let _ = storage.gc(config.retention_duration());
162 }
163
164 Ok(LogResult {
166 transcript_path: parsed.transcript_path.clone(),
167 machine_id,
168 session_id,
169 })
170}
171
172fn build_payload(
174 mut hook_data: serde_json::Value,
175 event_type: &EventType,
176 process_info: Option<&ClaudeProcessInfo>,
177) -> Result<String> {
178 let env_vars = [
180 ("CLAUDE_PROJECT_DIR", "project_dir"),
181 ("CLAUDE_FILE_PATHS", "file_paths"),
182 ("CLAUDE_TOOL_INPUT", "tool_input_env"),
183 ("CLAUDE_TOOL_OUTPUT", "tool_output_env"),
184 ("CLAUDE_NOTIFICATION", "notification_env"),
185 ("CLAUDE_CODE_REMOTE", "remote"),
186 ("CLAUDE_ENV_FILE", "env_file"),
187 ];
188
189 if !hook_data.is_object() {
191 hook_data = serde_json::json!({ "_raw": hook_data });
192 }
193
194 let Some(obj) = hook_data.as_object_mut() else {
195 return Ok(serde_json::to_string(&hook_data)?);
197 };
198
199 let mut env_data: HashMap<String, String> = HashMap::new();
201 for (env_var, key) in env_vars {
202 if let Ok(value) = std::env::var(env_var) {
203 env_data.insert(key.to_string(), value);
204 }
205 }
206
207 if !env_data.is_empty() {
208 obj.insert("_env".to_string(), serde_json::to_value(env_data)?);
209 }
210
211 if *event_type == EventType::SessionStart
213 && let Some(info) = process_info
214 {
215 obj.insert(
216 "_claude_process".to_string(),
217 serde_json::json!({
218 "pid": info.pid,
219 "comm": info.comm
220 }),
221 );
222 }
223
224 Ok(serde_json::to_string(&hook_data)?)
225}
226
227fn capture_git_info_if_needed<S: Storage>(
236 storage: &S,
237 event_type: &EventType,
238 session_id: &str,
239 parsed: &ParsedHookInput,
240) {
241 match event_type {
242 EventType::SessionStart => {
243 if let Some(ref cwd) = parsed.cwd
245 && let Some(git_info) = get_branch_info(Path::new(cwd))
246 {
247 let _ = storage.update_session_git_info(session_id, &git_info);
248 }
249 }
250 EventType::PostToolUse => {
251 if parsed.tool_name.as_deref() == Some("Bash") {
253 if let Ok(tool_input) = std::env::var("CLAUDE_TOOL_INPUT")
255 && let Some(cmd) = extract_bash_command(&tool_input)
256 && is_branch_changing_command(&cmd)
257 && let Some(ref cwd) = parsed.cwd
258 && let Some(git_info) = get_branch_info(Path::new(cwd))
259 {
260 let _ = storage.update_session_git_info(session_id, &git_info);
261 }
262 }
263 }
264 _ => {}
265 }
266}
267
268fn extract_bash_command(tool_input: &str) -> Option<String> {
274 if let Ok(json) = serde_json::from_str::<serde_json::Value>(tool_input)
276 && let Some(cmd) = json.get("command").and_then(|v| v.as_str())
277 {
278 return Some(cmd.to_string());
279 }
280 Some(tool_input.to_string())
282}