Skip to main content

cli_denoiser/hooks/
codex.rs

1use std::path::PathBuf;
2
3use super::{InstallResult, InstallStatus};
4
5const HOOK_MARKER: &str = "cli-denoiser";
6
7/// Codex CLI uses ~/.codex/config.json or ~/.config/codex/config.json
8fn config_path() -> PathBuf {
9    // Try XDG first, then fallback
10    if let Some(config_dir) = dirs::config_dir() {
11        let xdg_path = config_dir.join("codex").join("config.json");
12        if xdg_path.exists() {
13            return xdg_path;
14        }
15    }
16
17    dirs::home_dir()
18        .unwrap_or_default()
19        .join(".codex")
20        .join("config.json")
21}
22
23#[must_use]
24pub fn install() -> InstallResult {
25    let path = config_path();
26    let agent = "Codex CLI".to_string();
27
28    if !path.exists() {
29        return InstallResult {
30            agent,
31            config_path: path,
32            status: InstallStatus::ConfigNotFound,
33        };
34    }
35
36    let content = match std::fs::read_to_string(&path) {
37        Ok(c) => c,
38        Err(e) => {
39            return InstallResult {
40                agent,
41                config_path: path,
42                status: InstallStatus::Failed(e.to_string()),
43            };
44        }
45    };
46
47    if content.contains(HOOK_MARKER) {
48        return InstallResult {
49            agent,
50            config_path: path,
51            status: InstallStatus::AlreadyInstalled,
52        };
53    }
54
55    // Codex uses a similar hook system to Claude Code
56    let mut config: serde_json::Value = match serde_json::from_str(&content) {
57        Ok(v) => v,
58        Err(e) => {
59            return InstallResult {
60                agent,
61                config_path: path,
62                status: InstallStatus::Failed(format!("invalid JSON: {e}")),
63            };
64        }
65    };
66
67    let hooks = config.as_object_mut().and_then(|obj| {
68        obj.entry("hooks")
69            .or_insert_with(|| serde_json::json!({}))
70            .as_object_mut()
71    });
72
73    let Some(hooks) = hooks else {
74        return InstallResult {
75            agent,
76            config_path: path,
77            status: InstallStatus::Failed("could not access hooks object".to_string()),
78        };
79    };
80
81    let post_exec = hooks
82        .entry("post_exec")
83        .or_insert_with(|| serde_json::json!([]));
84
85    let hook_entry = serde_json::json!({
86        "name": "cli-denoiser",
87        "command": "cli-denoiser --hook-mode",
88        "on": ["shell"]
89    });
90
91    if let Some(arr) = post_exec.as_array_mut() {
92        arr.push(hook_entry);
93    }
94
95    match write_json(&path, &config) {
96        Ok(()) => InstallResult {
97            agent,
98            config_path: path,
99            status: InstallStatus::Installed,
100        },
101        Err(e) => InstallResult {
102            agent,
103            config_path: path,
104            status: InstallStatus::Failed(e),
105        },
106    }
107}
108
109#[must_use]
110pub fn uninstall() -> InstallResult {
111    let path = config_path();
112    let agent = "Codex CLI".to_string();
113
114    if !path.exists() {
115        return InstallResult {
116            agent,
117            config_path: path,
118            status: InstallStatus::ConfigNotFound,
119        };
120    }
121
122    let content = match std::fs::read_to_string(&path) {
123        Ok(c) => c,
124        Err(e) => {
125            return InstallResult {
126                agent,
127                config_path: path,
128                status: InstallStatus::Failed(e.to_string()),
129            };
130        }
131    };
132
133    if !content.contains(HOOK_MARKER) {
134        return InstallResult {
135            agent,
136            config_path: path,
137            status: InstallStatus::ConfigNotFound,
138        };
139    }
140
141    let mut config: serde_json::Value = match serde_json::from_str(&content) {
142        Ok(v) => v,
143        Err(e) => {
144            return InstallResult {
145                agent,
146                config_path: path,
147                status: InstallStatus::Failed(format!("invalid JSON: {e}")),
148            };
149        }
150    };
151
152    if let Some(hooks) = config.pointer_mut("/hooks/post_exec")
153        && let Some(arr) = hooks.as_array_mut()
154    {
155        arr.retain(|entry| {
156            let s = entry.to_string();
157            !s.contains(HOOK_MARKER)
158        });
159    }
160
161    match write_json(&path, &config) {
162        Ok(()) => InstallResult {
163            agent,
164            config_path: path,
165            status: InstallStatus::Installed,
166        },
167        Err(e) => InstallResult {
168            agent,
169            config_path: path,
170            status: InstallStatus::Failed(e),
171        },
172    }
173}
174
175fn write_json(path: &std::path::Path, value: &serde_json::Value) -> Result<(), String> {
176    let json = serde_json::to_string_pretty(value).map_err(|e| e.to_string())?;
177    std::fs::write(path, json).map_err(|e| e.to_string())
178}