1use crate::{DisplayEntry, DisplayEntryUpdate, HookContext, HookEvent};
2use serde_json::Value;
3
4#[derive(Debug)]
5pub enum RewriteDecision {
6 Passthrough,
7 Rewrite(Value),
10}
11
12pub trait Capturer: Send + Sync + 'static {
14 fn name(&self) -> &'static str;
16
17 fn tool_name(&self) -> &'static str;
19
20 fn subscribes_to(&self) -> &'static [HookEvent] {
22 &[HookEvent::Pre, HookEvent::Post]
23 }
24
25 fn pre_rewrite(&self, _ctx: &HookContext, _input: &Value) -> RewriteDecision {
27 RewriteDecision::Passthrough
28 }
29
30 fn render_pre(&self, ctx: &HookContext, input: &Value) -> Option<DisplayEntry>;
32
33 fn render_post(
36 &self,
37 _ctx: &HookContext,
38 _input: &Value,
39 _response: &Value,
40 ) -> Option<DisplayEntryUpdate> {
41 None
42 }
43}
44
45#[cfg(test)]
46mod tests {
47 use super::*;
48 use crate::{EntryBody, EntryStatus};
49 use std::time::SystemTime;
50
51 struct NoOpCapturer;
52 impl Capturer for NoOpCapturer {
53 fn name(&self) -> &'static str {
54 "noop"
55 }
56 fn tool_name(&self) -> &'static str {
57 "NoSuchTool"
58 }
59 fn render_pre(&self, ctx: &HookContext, _: &Value) -> Option<DisplayEntry> {
60 Some(DisplayEntry {
61 agent_key: ctx.agent_key().to_string(),
62 tool_use_id: ctx.tool_use_id.clone(),
63 tool: "noop".to_string(),
64 timestamp: SystemTime::now(),
65 headline: "noop".into(),
66 body: EntryBody::None,
67 status: EntryStatus::Pending,
68 })
69 }
70 }
71
72 #[test]
73 fn trait_is_object_safe() {
74 let _: Box<dyn Capturer> = Box::new(NoOpCapturer);
75 }
76
77 #[test]
78 fn default_pre_rewrite_is_passthrough() {
79 let c = NoOpCapturer;
80 let ctx: HookContext = serde_json::from_str(
81 r#"{"session_id":"s","transcript_path":"/t","cwd":"/c","hook_event_name":"PreToolUse","tool_name":"Bash","tool_use_id":"t1"}"#,
82 ).unwrap();
83 match c.pre_rewrite(&ctx, &Value::Null) {
84 RewriteDecision::Passthrough => {}
85 _ => panic!("expected Passthrough"),
86 }
87 }
88}