use anyhow::Result;
use serde_json::{json, Value};
use std::env;
use std::fs;
use std::path::PathBuf;
use std::process::Command;
#[derive(Debug, Clone)]
pub struct HookConfig {
pub hook_type: String,
pub command: String,
pub enabled: bool,
pub description: String,
}
pub async fn list_hooks(_params: Value) -> Result<Value> {
let hooks = vec![
HookConfig {
hook_type: "UserPromptSubmit".to_string(),
command: format!("{} --agent-context", get_hook_path()),
enabled: check_hook_enabled("UserPromptSubmit"),
description: "Provides intelligent context based on user prompts".to_string(),
},
HookConfig {
hook_type: "PreToolUse".to_string(),
command: format!("{} --agent-pre-tool", get_hook_path()),
enabled: check_hook_enabled("PreToolUse"),
description: "Validates tool usage before execution".to_string(),
},
HookConfig {
hook_type: "PostToolUse".to_string(),
command: format!("{} --agent-post-tool", get_hook_path()),
enabled: check_hook_enabled("PostToolUse"),
description: "Processes tool results for better context".to_string(),
},
HookConfig {
hook_type: "SessionStart".to_string(),
command: format!("{} --agent-restore", get_hook_path()),
enabled: check_hook_enabled("SessionStart"),
description: "Restores consciousness from previous session".to_string(),
},
];
let mut result = Vec::new();
for hook in hooks {
result.push(json!({
"type": hook.hook_type,
"command": hook.command,
"enabled": hook.enabled,
"description": hook.description,
}));
}
Ok(json!({
"hooks": result,
"hook_path": get_hook_path(),
"hooks_file": get_hooks_file_path().display().to_string(),
}))
}
pub async fn set_hook(params: Value) -> Result<Value> {
let hook_type = params["hook_type"]
.as_str()
.ok_or_else(|| anyhow::anyhow!("Missing hook_type parameter"))?;
let flag = match hook_type {
"UserPromptSubmit" => "--agent-context",
"PreToolUse" => "--agent-pre-tool",
"PostToolUse" => "--agent-post-tool",
"SessionStart" => "--agent-restore",
_ => "--agent-context",
};
let default_command = format!("{} {}", get_hook_path(), flag);
let command = params["command"].as_str().unwrap_or(&default_command);
let enabled = params["enabled"].as_bool().unwrap_or(true);
let hooks_file = get_hooks_file_path();
let mut hooks_config = if hooks_file.exists() {
let content = fs::read_to_string(&hooks_file)?;
serde_json::from_str(&content).unwrap_or_else(|_| json!({}))
} else {
json!({})
};
hooks_config[hook_type] = json!({
"command": command,
"enabled": enabled,
"updated_at": chrono::Utc::now().to_rfc3339(),
});
fs::write(&hooks_file, serde_json::to_string_pretty(&hooks_config)?)?;
Ok(json!({
"success": true,
"hook_type": hook_type,
"command": command,
"enabled": enabled,
"message": if enabled {
format!("Hook '{}' has been configured. You may need to restart your AI Agent or use /hooks to apply.", hook_type)
} else {
format!("Hook '{}' has been disabled.", hook_type)
}
}))
}
pub async fn remove_hook(params: Value) -> Result<Value> {
let hook_type = params["hook_type"]
.as_str()
.ok_or_else(|| anyhow::anyhow!("Missing hook_type parameter"))?;
let hooks_file = get_hooks_file_path();
if !hooks_file.exists() {
return Ok(json!({
"success": false,
"message": "No hooks configuration found"
}));
}
let mut hooks_config: Value = {
let content = fs::read_to_string(&hooks_file)?;
serde_json::from_str(&content)?
};
if let Some(obj) = hooks_config.as_object_mut() {
if obj.remove(hook_type).is_some() {
fs::write(&hooks_file, serde_json::to_string_pretty(&hooks_config)?)?;
return Ok(json!({
"success": true,
"message": format!("Hook '{}' has been removed. Use /hooks in your AI Agent to update.", hook_type)
}));
}
}
Ok(json!({
"success": false,
"message": format!("Hook '{}' was not found", hook_type)
}))
}
pub async fn test_hook(params: Value) -> Result<Value> {
let hook_type = params["hook_type"]
.as_str()
.ok_or_else(|| anyhow::anyhow!("Missing hook_type parameter"))?;
let test_input = params["input"].as_str().unwrap_or("test input");
let command = match hook_type {
"UserPromptSubmit" => format!("{} --agent-context", get_hook_path()),
"PreToolUse" => format!("{} --agent-pre-tool", get_hook_path()),
"PostToolUse" => format!("{} --agent-post-tool", get_hook_path()),
"SessionStart" => format!("{} --agent-restore", get_hook_path()),
_ => {
return Ok(json!({
"success": false,
"message": format!("Unknown hook type: {}", hook_type)
}));
}
};
let input_json = match hook_type {
"UserPromptSubmit" => json!({
"prompt": test_input
}),
"PreToolUse" => json!({
"tool": "test_tool",
"args": {"test": true}
}),
"PostToolUse" => json!({
"tool": "test_tool",
"result": {"success": true}
}),
_ => json!({}),
};
let output = Command::new("sh")
.arg("-c")
.arg(&command)
.env("HOOK_TEST_MODE", "true")
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.and_then(|mut child| {
if let Some(mut stdin) = child.stdin.take() {
use std::io::Write;
let _ = stdin.write_all(input_json.to_string().as_bytes());
}
child.wait_with_output()
});
match output {
Ok(output) => {
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
Ok(json!({
"success": output.status.success(),
"exit_code": output.status.code(),
"stdout": stdout.to_string(),
"stderr": stderr.to_string(),
"command": command,
"input": input_json,
}))
}
Err(e) => Ok(json!({
"success": false,
"error": format!("Failed to execute hook: {}", e),
"command": command,
})),
}
}
pub async fn get_hook_commands(_params: Value) -> Result<Value> {
let hook_path = get_hook_path();
Ok(json!({
"commands": {
"UserPromptSubmit": format!("{} --agent-context", hook_path),
"PreToolUse": format!("{} --agent-pre-tool", hook_path),
"PostToolUse": format!("{} --agent-post-tool", hook_path),
"SessionStart": format!("{} --agent-restore", hook_path),
"SessionEnd": format!("{} --agent-save", hook_path),
},
"instructions": "Copy the command for the hook you want and paste it in your AI Agent's /hooks command",
"hook_path": hook_path,
}))
}
fn get_hook_path() -> String {
if let Ok(current_dir) = env::current_dir() {
let st_hook = current_dir.join("st-hook");
if st_hook.exists() {
return st_hook.display().to_string();
}
}
let smart_tree_hook = PathBuf::from("/aidata/ayeverse/smart-tree/st-hook");
if smart_tree_hook.exists() {
return smart_tree_hook.display().to_string();
}
if let Ok(output) = Command::new("which").arg("st-hook").output() {
if output.status.success() {
return String::from_utf8_lossy(&output.stdout).trim().to_string();
}
}
get_st_path()
}
fn get_st_path() -> String {
if let Ok(output) = Command::new("which").arg("st-hook").output() {
if output.status.success() {
return String::from_utf8_lossy(&output.stdout).trim().to_string();
}
}
if let Ok(output) = Command::new("which").arg("st").output() {
if output.status.success() {
return String::from_utf8_lossy(&output.stdout).trim().to_string();
}
}
if let Ok(current_dir) = env::current_dir() {
let st_release = current_dir.join("target/release/st");
if st_release.exists() {
return st_release.display().to_string();
}
}
"/aidata/ayeverse/smart-tree/target/release/st".to_string()
}
fn get_hooks_file_path() -> PathBuf {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".claude")
.join("hooks.json")
}
fn check_hook_enabled(hook_type: &str) -> bool {
let hooks_file = get_hooks_file_path();
if !hooks_file.exists() {
return false;
}
if let Ok(content) = fs::read_to_string(&hooks_file) {
if let Ok(config) = serde_json::from_str::<Value>(&content) {
return config[hook_type]["enabled"].as_bool().unwrap_or(false);
}
}
false
}