1use super::typescript::{self, Harness};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub struct Event {
12 pub name: &'static str,
13 pub matcher: Option<&'static str>,
16 pub default_timeout_ms: u32,
20}
21
22const DEFAULT_TIMEOUT_MS: u32 = 600_000;
25
26impl Event {
27 const fn timeout_ms(self, default_timeout_ms: u32) -> Self {
32 Self {
33 default_timeout_ms,
34 ..self
35 }
36 }
37
38 pub fn is_tool_event(&self) -> bool {
40 self.matcher == Some("tool_name")
41 }
42}
43
44const fn event(name: &'static str) -> Event {
45 Event {
46 name,
47 matcher: None,
48 default_timeout_ms: DEFAULT_TIMEOUT_MS,
49 }
50}
51
52const fn matched(name: &'static str, field: &'static str) -> Event {
53 Event {
54 name,
55 matcher: Some(field),
56 default_timeout_ms: DEFAULT_TIMEOUT_MS,
57 }
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub struct MergeFile {
63 pub file: &'static str,
65 pub top_level: &'static [(&'static str, i64)],
67 pub trust_gated: bool,
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum Placement {
75 Merge(MergeFile),
77 File { dir: &'static str },
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub enum Rules {
84 Claude,
86 Codex,
88 TypeScript(&'static Harness),
90 Withheld { reason: &'static str },
92}
93
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98pub struct Profile {
99 pub events: &'static [Event],
101 pub verified_against: Option<&'static str>,
104 pub rules: Rules,
105}
106
107impl Profile {
108 pub fn event(&self, name: &str) -> Option<&'static Event> {
110 self.events.iter().find(|e| e.name == name)
111 }
112
113 pub fn renders(&self) -> bool {
115 !matches!(self.rules, Rules::Withheld { .. })
116 }
117}
118
119#[derive(Debug, Clone, Copy, PartialEq, Eq)]
121pub struct Target {
122 pub root: &'static str,
123 pub placement: Placement,
124 pub profile: Profile,
125 pub other_events: &'static [&'static str],
128}
129
130const GENERATED_SUFFIX: &str = super::RESERVED_NAME_SUFFIX;
133
134impl Target {
135 pub fn reads(&self, event: &str) -> bool {
137 self.profile.event(event).is_some() || self.other_events.contains(&event)
138 }
139
140 pub fn merge_file(&self) -> Option<&MergeFile> {
142 match &self.placement {
143 Placement::Merge(merge) => Some(merge),
144 Placement::File { .. } => None,
145 }
146 }
147
148 pub fn native_hook_problem(&self, content: &str) -> Option<&'static str> {
151 match self.profile.rules {
152 Rules::TypeScript(harness) => harness.native_hook_problem(content),
153 _ => None,
154 }
155 }
156
157 pub fn is_file(&self) -> bool {
159 matches!(self.placement, Placement::File { .. })
160 }
161
162 pub fn native_hook_path(&self, hook_name: &str) -> Option<String> {
165 let Placement::File { dir } = self.placement else {
166 return None;
167 };
168 Some(format!("{dir}/mars-{hook_name}.ts"))
169 }
170
171 pub fn generated_path(&self, hook_name: &str) -> Option<String> {
174 let Placement::File { dir } = self.placement else {
175 return None;
176 };
177 Some(format!("{dir}/mars-{hook_name}{GENERATED_SUFFIX}.ts"))
178 }
179}
180
181const CLAUDE_EVENTS: &[Event] = &[
182 matched("SessionStart", "source"),
183 event("Setup"),
184 event("UserPromptSubmit").timeout_ms(30_000),
185 event("UserPromptExpansion"),
186 matched("PreToolUse", "tool_name"),
187 matched("PermissionRequest", "tool_name"),
188 matched("PermissionDenied", "tool_name"),
189 matched("PostToolUse", "tool_name"),
190 matched("PostToolUseFailure", "tool_name"),
191 event("PostToolBatch"),
192 matched("SubagentStart", "agent_type"),
193 matched("SubagentStop", "agent_type"),
194 event("TaskCreated"),
195 event("TaskCompleted"),
196 event("Stop"),
197 event("StopFailure"),
198 event("TeammateIdle"),
199 matched("PreCompact", "trigger"),
200 matched("PostCompact", "trigger"),
201 event("Elicitation"),
202 event("ElicitationResult"),
203 matched("Notification", "notification_type"),
204 event("ConfigChange"),
205 event("InstructionsLoaded"),
206 event("CwdChanged"),
207 event("FileChanged"),
208 event("WorktreeCreate"),
209 event("WorktreeRemove"),
210 matched("SessionEnd", "reason").timeout_ms(1_500),
211];
212
213const CODEX_EVENTS: &[Event] = &[
215 matched("SessionStart", "source"),
216 event("UserPromptSubmit"),
217 matched("PreToolUse", "tool_name"),
218 matched("PermissionRequest", "tool_name"),
219 matched("PostToolUse", "tool_name"),
220 matched("PreCompact", "trigger"),
221 matched("PostCompact", "trigger"),
222 matched("SubagentStart", "agent_type"),
223 matched("SubagentStop", "agent_type"),
224 event("Stop"),
225 matched("SessionEnd", "reason"),
226];
227
228const CURSOR_EVENTS: &[Event] = &[
229 event("beforeShellExecution"),
230 event("beforeMCPExecution"),
231 event("afterShellExecution"),
232 event("afterMCPExecution"),
233 event("beforeReadFile"),
234 event("afterFileEdit"),
235 event("beforeTabFileRead"),
236 event("afterTabFileEdit"),
237 event("stop"),
238 event("beforeSubmitPrompt"),
239 event("afterAgentResponse"),
240 event("afterAgentThought"),
241 event("sessionStart"),
242 event("sessionEnd"),
243 event("preCompact"),
244 event("subagentStart"),
245 event("subagentStop"),
246 event("preToolUse"),
247 event("postToolUse"),
248 event("postToolUseFailure"),
249 event("workspaceOpen"),
250];
251
252const OPENCODE_EVENTS: &[Event] = &[
254 matched("PreToolUse", "tool_name"),
255 matched("PostToolUse", "tool_name"),
256 matched("PostToolUseFailure", "tool_name"),
257 event("UserPromptSubmit"),
258 event("PreCompact"),
259 matched("PermissionRequest", "tool_name"),
260];
261
262const PI_EVENTS: &[Event] = &[
263 matched("PreToolUse", "tool_name"),
264 matched("PostToolUse", "tool_name"),
265 matched("PostToolUseFailure", "tool_name"),
266 event("UserPromptSubmit"),
267 matched("SessionStart", "source"),
268 event("Stop"),
269 matched("PreCompact", "trigger"),
270 matched("PostCompact", "trigger"),
271 matched("SessionEnd", "reason"),
272];
273
274const CLAUDE_VERSION: &str = "Claude Code 2.1.312";
275
276pub const PLUGIN_CLAUDE_TREE: Profile = Profile {
279 events: CLAUDE_EVENTS,
280 verified_against: Some(CLAUDE_VERSION),
281 rules: Rules::Codex,
282};
283
284const TARGETS: &[Target] = &[
285 Target {
286 root: ".claude",
287 placement: Placement::Merge(MergeFile {
290 file: "settings.local.json",
291 top_level: &[],
292 trust_gated: false,
293 }),
294 profile: Profile {
295 events: CLAUDE_EVENTS,
296 verified_against: Some(CLAUDE_VERSION),
297 rules: Rules::Claude,
298 },
299 other_events: &[],
300 },
301 Target {
302 root: ".codex",
303 placement: Placement::Merge(MergeFile {
304 file: "hooks.json",
305 top_level: &[],
306 trust_gated: true,
307 }),
308 profile: Profile {
309 events: CODEX_EVENTS,
310 verified_against: Some("codex-cli 0.164.0"),
311 rules: Rules::Codex,
312 },
313 other_events: &["PreModelRequest", "PostModelRequest", "Interrupt"],
314 },
315 Target {
316 root: ".cursor",
317 placement: Placement::Merge(MergeFile {
319 file: "hooks.json",
320 top_level: &[("version", 1)],
321 trust_gated: false,
322 }),
323 profile: Profile {
324 events: CURSOR_EVENTS,
325 verified_against: Some("cursor-agent 2026.07.16"),
326 rules: Rules::Withheld {
327 reason: "mars renders no generated hooks for Cursor before 0.26",
328 },
329 },
330 other_events: &[],
331 },
332 Target {
333 root: ".opencode",
334 placement: Placement::File { dir: "plugins" },
335 profile: Profile {
336 events: OPENCODE_EVENTS,
337 verified_against: Some("OpenCode 2.2.0"),
338 rules: Rules::TypeScript(&typescript::OPENCODE),
339 },
340 other_events: &[],
341 },
342 Target {
343 root: ".pi",
344 placement: Placement::File { dir: "extensions" },
345 profile: Profile {
346 events: PI_EVENTS,
347 verified_against: Some("Pi 1.4.0"),
348 rules: Rules::TypeScript(&typescript::PI),
349 },
350 other_events: &[],
351 },
352];
353
354pub fn target(root: &str) -> Option<&'static Target> {
356 TARGETS.iter().find(|target| target.root == root)
357}
358
359pub fn all() -> &'static [Target] {
361 TARGETS
362}
363
364pub fn claude_event(name: &str) -> Option<&'static Event> {
366 CLAUDE_EVENTS.iter().find(|event| event.name == name)
367}
368
369pub fn claude_event_names() -> String {
371 CLAUDE_EVENTS
372 .iter()
373 .map(|event| event.name)
374 .collect::<Vec<_>>()
375 .join(", ")
376}