zeroclaw_runtime/hooks/builtin/
command_logger.rs1use async_trait::async_trait;
2use std::sync::{Arc, Mutex};
3use std::time::Duration;
4
5use crate::hooks::traits::HookHandler;
6use zeroclaw_api::tool::ToolResult;
7
8pub struct CommandLoggerHook {
10 log: Arc<Mutex<Vec<String>>>,
11}
12
13impl Default for CommandLoggerHook {
14 fn default() -> Self {
15 Self::new()
16 }
17}
18
19impl CommandLoggerHook {
20 pub fn new() -> Self {
21 Self {
22 log: Arc::new(Mutex::new(Vec::new())),
23 }
24 }
25
26 #[cfg(test)]
27 pub fn entries(&self) -> Vec<String> {
28 self.log.lock().unwrap().clone()
29 }
30}
31
32#[async_trait]
33impl HookHandler for CommandLoggerHook {
34 fn name(&self) -> &str {
35 "command-logger"
36 }
37
38 fn priority(&self) -> i32 {
39 -50
40 }
41
42 async fn on_after_tool_call(&self, tool: &str, result: &ToolResult, duration: Duration) {
43 let entry = format!(
44 "[{}] {} ({}ms) success={}",
45 chrono::Utc::now().format("%H:%M:%S"),
46 tool,
47 duration.as_millis(),
48 result.success,
49 );
50 ::zeroclaw_log::record!(
51 INFO,
52 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
53 .with_attrs(::serde_json::json!({"hook": "command-logger"})),
54 &format!("{}", entry)
55 );
56 self.log.lock().unwrap().push(entry);
57 }
58}
59
60#[cfg(test)]
61mod tests {
62 use super::*;
63
64 #[tokio::test]
65 async fn logs_tool_calls() {
66 let hook = CommandLoggerHook::new();
67 let result = ToolResult {
68 success: true,
69 output: "ok".into(),
70 error: None,
71 };
72 hook.on_after_tool_call("shell", &result, Duration::from_millis(42))
73 .await;
74 let entries = hook.entries();
75 assert_eq!(entries.len(), 1);
76 assert!(entries[0].contains("shell"));
77 assert!(entries[0].contains("42ms"));
78 assert!(entries[0].contains("success=true"));
79 }
80}