use crate::{DisplayEntry, DisplayEntryUpdate, HookContext, HookEvent};
use serde_json::Value;
#[derive(Debug)]
pub enum RewriteDecision {
Passthrough,
Rewrite(Value),
}
pub trait Capturer: Send + Sync + 'static {
fn name(&self) -> &'static str;
fn tool_name(&self) -> &'static str;
fn subscribes_to(&self) -> &'static [HookEvent] {
&[HookEvent::Pre, HookEvent::Post]
}
fn pre_rewrite(&self, _ctx: &HookContext, _input: &Value) -> RewriteDecision {
RewriteDecision::Passthrough
}
fn render_pre(&self, ctx: &HookContext, input: &Value) -> Option<DisplayEntry>;
fn render_post(
&self,
_ctx: &HookContext,
_input: &Value,
_response: &Value,
) -> Option<DisplayEntryUpdate> {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{EntryBody, EntryStatus};
use std::time::SystemTime;
struct NoOpCapturer;
impl Capturer for NoOpCapturer {
fn name(&self) -> &'static str {
"noop"
}
fn tool_name(&self) -> &'static str {
"NoSuchTool"
}
fn render_pre(&self, ctx: &HookContext, _: &Value) -> Option<DisplayEntry> {
Some(DisplayEntry {
agent_key: ctx.agent_key().to_string(),
tool_use_id: ctx.tool_use_id.clone(),
tool: "noop".to_string(),
timestamp: SystemTime::now(),
headline: "noop".into(),
body: EntryBody::None,
status: EntryStatus::Pending,
})
}
}
#[test]
fn trait_is_object_safe() {
let _: Box<dyn Capturer> = Box::new(NoOpCapturer);
}
#[test]
fn default_pre_rewrite_is_passthrough() {
let c = NoOpCapturer;
let ctx: HookContext = serde_json::from_str(
r#"{"session_id":"s","transcript_path":"/t","cwd":"/c","hook_event_name":"PreToolUse","tool_name":"Bash","tool_use_id":"t1"}"#,
).unwrap();
match c.pre_rewrite(&ctx, &Value::Null) {
RewriteDecision::Passthrough => {}
_ => panic!("expected Passthrough"),
}
}
}