use anyhow::{Context, Result};
use serde_json::{json, Value};
use std::path::Path;
use crate::install::json_io::{read_json_file, write_json_file};
#[derive(Clone, Copy)]
pub(in crate::install) enum HookStrategy {
ClaudeCode,
Codex,
}
impl HookStrategy {
fn include_session_init(self) -> bool {
matches!(self, Self::ClaudeCode)
}
fn include_observe(self) -> bool {
matches!(self, Self::ClaudeCode)
}
fn observe_matcher(self) -> &'static str {
match self {
Self::ClaudeCode => "Write|Edit|NotebookEdit|Bash|Grep|Glob|Task",
Self::Codex => "Bash",
}
}
fn observe_timeout_ms(self) -> i64 {
match self {
Self::ClaudeCode => 120000,
Self::Codex => 3000,
}
}
fn runtime_host(self) -> &'static str {
match self {
Self::ClaudeCode => "claude-code",
Self::Codex => "codex-cli",
}
}
}
fn hook_command(bin: &str, strategy: HookStrategy, subcommand: &str) -> String {
format!("{} {} --host {}", bin, subcommand, strategy.runtime_host())
}
pub(in crate::install) fn build_hooks(bin: &str, strategy: HookStrategy) -> Value {
let mut hooks = serde_json::Map::new();
hooks.insert(
"SessionStart".to_string(),
json!([{
"hooks": [{ "type": "command", "command": hook_command(bin, strategy, "context"), "timeout": 15000 }]
}]),
);
if strategy.include_session_init() {
hooks.insert(
"UserPromptSubmit".to_string(),
json!([{
"hooks": [{ "type": "command", "command": hook_command(bin, strategy, "session-init"), "timeout": 15000 }]
}]),
);
}
if strategy.include_observe() {
hooks.insert(
"PostToolUse".to_string(),
json!([{
"matcher": strategy.observe_matcher(),
"hooks": [{ "type": "command", "command": hook_command(bin, strategy, "observe"), "timeout": strategy.observe_timeout_ms() }]
}]),
);
}
hooks.insert(
"Stop".to_string(),
json!([{
"hooks": [{ "type": "command", "command": hook_command(bin, strategy, "summarize"), "timeout": 120000 }]
}]),
);
Value::Object(hooks)
}
pub(in crate::install) fn build_mcp_server(bin: &str) -> Value {
json!({
"type": "stdio",
"command": bin,
"args": ["mcp"]
})
}
fn is_remem_hook(hook_entry: &Value, bin: &str) -> bool {
if let Some(hooks) = hook_entry.get("hooks").and_then(|hooks| hooks.as_array()) {
for hook in hooks {
if let Some(cmd) = hook.get("command").and_then(|command| command.as_str()) {
if cmd.contains(bin) || cmd.contains("remem") {
return true;
}
}
}
}
false
}
pub(in crate::install) fn remove_remem_hooks(settings: &mut Value, bin: &str) {
if let Some(hooks) = settings
.get_mut("hooks")
.and_then(|hooks| hooks.as_object_mut())
{
let event_types: Vec<String> = hooks.keys().cloned().collect();
for event_type in event_types {
if let Some(entries) = hooks
.get_mut(&event_type)
.and_then(|entries| entries.as_array_mut())
{
entries.retain(|entry| !is_remem_hook(entry, bin));
if entries.is_empty() {
hooks.remove(&event_type);
}
}
}
if hooks.is_empty() {
if let Some(obj) = settings.as_object_mut() {
obj.remove("hooks");
}
}
}
}
pub(in crate::install) fn apply_hooks_json(
path: &Path,
bin: &str,
strategy: HookStrategy,
) -> Result<()> {
let mut doc = read_json_file(&path.to_path_buf())?;
remove_remem_hooks(&mut doc, bin);
let new_hooks = build_hooks(bin, strategy);
let obj = doc
.as_object_mut()
.with_context(|| format!("{} 根节点不是 Object", path.display()))?;
let hooks = obj.entry("hooks").or_insert_with(|| json!({}));
if let (Some(existing), Some(new)) = (hooks.as_object_mut(), new_hooks.as_object()) {
for (event_type, entries) in new {
let arr = existing.entry(event_type).or_insert_with(|| json!([]));
if let (Some(arr), Some(new_entries)) = (arr.as_array_mut(), entries.as_array()) {
for entry in new_entries {
arr.push(entry.clone());
}
}
}
}
write_json_file(&path.to_path_buf(), &doc)
}
pub(in crate::install) fn strip_hooks_json(path: &Path, bin: &str) -> Result<()> {
if !path.exists() {
return Ok(());
}
let mut doc = read_json_file(&path.to_path_buf())?;
remove_remem_hooks(&mut doc, bin);
write_json_file(&path.to_path_buf(), &doc)
}
pub(in crate::install) fn remove_remem_mcp(settings: &mut Value, bin: &str) {
if let Some(servers) = settings
.get_mut("mcpServers")
.and_then(|servers| servers.as_object_mut())
{
let keys: Vec<String> = servers.keys().cloned().collect();
for key in keys {
if key == "remem" {
servers.remove(&key);
continue;
}
if let Some(cmd) = servers
.get(&key)
.and_then(|server| server.get("command"))
.and_then(|command| command.as_str())
{
if cmd.contains(bin) || cmd.contains("remem") {
servers.remove(&key);
}
}
}
if servers.is_empty() {
if let Some(obj) = settings.as_object_mut() {
obj.remove("mcpServers");
}
}
}
}