use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Deserialize, Serialize)]
pub struct Config {
pub commands: HashMap<String, String>,
#[serde(default)]
pub semantic_directories: HashMap<String, String>,
#[serde(default)]
pub command_history: Option<CommandHistoryConfig>,
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct CommandHistoryConfig {
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default = "default_history_path")]
pub log_file: String,
}
fn default_true() -> bool {
true
}
fn default_history_path() -> String {
"~/.claude-hook-advisor/bash-history.db".to_string()
}
#[derive(Debug, Deserialize, Serialize)]
pub struct HookInput {
#[allow(dead_code)]
pub session_id: String,
#[allow(dead_code)]
pub transcript_path: Option<String>,
#[allow(dead_code)]
pub cwd: Option<String>,
pub hook_event_name: String,
#[serde(default)]
pub tool_name: Option<String>,
#[serde(default)]
pub tool_input: Option<ToolInput>,
#[serde(default)]
pub prompt: Option<String>,
#[serde(default)]
pub tool_response: Option<ToolResponse>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct ToolResponse {
#[allow(dead_code)]
pub exit_code: Option<i32>,
#[allow(dead_code)]
pub stdout: Option<String>,
#[allow(dead_code)]
pub stderr: Option<String>,
}
#[derive(Debug, Deserialize, Serialize, Default, Clone)]
pub struct ToolInput {
#[serde(default)]
pub command: Option<String>,
#[allow(dead_code)]
pub description: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct HookOutput {
pub decision: String,
pub reason: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub replacement_command: Option<String>,
}
#[derive(Debug, Clone)]
pub struct DirectoryResolution {
pub canonical_path: String,
pub alias_used: String,
pub variables_substituted: Vec<(String, String)>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hook_input_deserialization() {
let json = r#"{
"session_id": "test-session",
"transcript_path": "/path/to/transcript",
"cwd": "/current/directory",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "npm install",
"description": "Install packages"
}
}"#;
let input: HookInput = serde_json::from_str(json).unwrap();
assert_eq!(input.session_id, "test-session");
assert_eq!(input.tool_name.unwrap(), "Bash");
assert_eq!(input.tool_input.unwrap().command.unwrap(), "npm install");
}
#[test]
fn test_hook_input_minimal() {
let json = r#"{
"session_id": "test",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "ls -la"
}
}"#;
let input: HookInput = serde_json::from_str(json).unwrap();
let tool_input = input.tool_input.unwrap();
assert_eq!(tool_input.command.unwrap(), "ls -la");
assert!(tool_input.description.is_none());
}
#[test]
fn test_user_prompt_submit_hook() {
let json = r#"{
"session_id": "test",
"hook_event_name": "UserPromptSubmit",
"prompt": "use bun instead of npm"
}"#;
let input: HookInput = serde_json::from_str(json).unwrap();
assert_eq!(input.hook_event_name, "UserPromptSubmit");
assert_eq!(input.prompt.unwrap(), "use bun instead of npm");
assert!(input.tool_name.is_none());
}
#[test]
fn test_post_tool_use_hook() {
let json = r#"{
"session_id": "test",
"hook_event_name": "PostToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "bun install"
},
"tool_response": {
"exit_code": 0,
"stdout": "Dependencies installed",
"stderr": ""
}
}"#;
let input: HookInput = serde_json::from_str(json).unwrap();
assert_eq!(input.hook_event_name, "PostToolUse");
assert_eq!(input.tool_name.unwrap(), "Bash");
assert_eq!(input.tool_response.unwrap().exit_code.unwrap(), 0);
}
}