Skip to main content

zeroclaw_runtime/hooks/builtin/
command_logger.rs

1use 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
8/// Logs tool calls for auditing.
9pub 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}