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] = &[
184 matched("SessionStart", "source"),
185 event("Setup"),
186 event("UserPromptSubmit").timeout_ms(30_000),
187 event("UserPromptExpansion"),
188 matched("PreToolUse", "tool_name"),
189 matched("PermissionRequest", "tool_name"),
190 matched("PermissionDenied", "tool_name"),
191 matched("PostToolUse", "tool_name"),
192 matched("PostToolUseFailure", "tool_name"),
193 event("PostToolBatch"),
194 matched("SubagentStart", "agent_type"),
195 matched("SubagentStop", "agent_type"),
196 event("TaskCreated"),
197 event("TaskCompleted"),
198 event("Stop"),
199 event("StopFailure"),
200 event("TeammateIdle"),
201 matched("PreCompact", "trigger"),
202 matched("PostCompact", "trigger"),
203 event("Elicitation"),
204 event("ElicitationResult"),
205 matched("Notification", "notification_type"),
206 event("ConfigChange"),
207 event("InstructionsLoaded"),
208 event("CwdChanged"),
209 event("FileChanged"),
210 event("WorktreeCreate"),
211 event("WorktreeRemove"),
212 matched("SessionEnd", "reason").timeout_ms(1_500),
213];
214
215const CODEX_EVENTS: &[Event] = &[
220 matched("SessionStart", "source"),
221 event("UserPromptSubmit"),
222 matched("PreToolUse", "tool_name"),
223 matched("PermissionRequest", "tool_name"),
224 matched("PostToolUse", "tool_name"),
225 matched("PreCompact", "trigger"),
226 matched("PostCompact", "trigger"),
227 matched("SubagentStart", "agent_type"),
228 matched("SubagentStop", "agent_type"),
229 event("Stop"),
230 matched("SessionEnd", "reason"),
231];
232
233const CURSOR_EVENTS: &[Event] = &[
237 event("beforeShellExecution"),
238 event("beforeMCPExecution"),
239 event("afterShellExecution"),
240 event("afterMCPExecution"),
241 event("beforeReadFile"),
242 event("afterFileEdit"),
243 event("beforeTabFileRead"),
244 event("afterTabFileEdit"),
245 event("stop"),
246 event("beforeSubmitPrompt"),
247 event("afterAgentResponse"),
248 event("afterAgentThought"),
249 event("sessionStart"),
250 event("sessionEnd"),
251 event("preCompact"),
252 event("subagentStart"),
253 event("subagentStop"),
254 event("preToolUse"),
255 event("postToolUse"),
256 event("postToolUseFailure"),
257 event("workspaceOpen"),
258];
259
260const OPENCODE_EVENTS: &[Event] = &[
264 matched("PreToolUse", "tool_name"),
265 matched("PostToolUse", "tool_name"),
266 matched("PostToolUseFailure", "tool_name"),
267 event("UserPromptSubmit"),
268 event("PreCompact"),
269 matched("PermissionRequest", "tool_name"),
270];
271
272const PI_EVENTS: &[Event] = &[
274 matched("PreToolUse", "tool_name"),
275 matched("PostToolUse", "tool_name"),
276 matched("PostToolUseFailure", "tool_name"),
277 event("UserPromptSubmit"),
278 matched("SessionStart", "source"),
279 event("Stop"),
280 matched("PreCompact", "trigger"),
281 matched("PostCompact", "trigger"),
282 matched("SessionEnd", "reason"),
283];
284
285const CLAUDE_VERSION: &str = "Claude Code 2.1.312";
286
287pub const PLUGIN_CLAUDE_TREE: Profile = Profile {
290 events: CLAUDE_EVENTS,
291 verified_against: Some(CLAUDE_VERSION),
292 rules: Rules::Codex,
293};
294
295const TARGETS: &[Target] = &[
296 Target {
297 root: ".claude",
298 placement: Placement::Merge(MergeFile {
301 file: "settings.local.json",
302 top_level: &[],
303 trust_gated: false,
304 }),
305 profile: Profile {
306 events: CLAUDE_EVENTS,
307 verified_against: Some(CLAUDE_VERSION),
308 rules: Rules::Claude,
309 },
310 other_events: &[],
311 },
312 Target {
313 root: ".codex",
314 placement: Placement::Merge(MergeFile {
315 file: "hooks.json",
316 top_level: &[],
317 trust_gated: true,
318 }),
319 profile: Profile {
320 events: CODEX_EVENTS,
321 verified_against: Some("codex-cli 0.164.0"),
322 rules: Rules::Codex,
323 },
324 other_events: &["PreModelRequest", "PostModelRequest", "Interrupt"],
325 },
326 Target {
327 root: ".cursor",
328 placement: Placement::Merge(MergeFile {
330 file: "hooks.json",
331 top_level: &[("version", 1)],
332 trust_gated: false,
333 }),
334 profile: Profile {
335 events: CURSOR_EVENTS,
336 verified_against: Some("cursor-agent 2026.07.16"),
337 rules: Rules::Withheld {
338 reason: "mars renders no generated hooks for Cursor before 0.26",
339 },
340 },
341 other_events: &[],
342 },
343 Target {
344 root: ".opencode",
345 placement: Placement::File { dir: "plugins" },
346 profile: Profile {
347 events: OPENCODE_EVENTS,
348 verified_against: Some("OpenCode 2.2.0"),
349 rules: Rules::TypeScript(&typescript::OPENCODE),
350 },
351 other_events: &[],
352 },
353 Target {
354 root: ".pi",
355 placement: Placement::File { dir: "extensions" },
356 profile: Profile {
357 events: PI_EVENTS,
358 verified_against: Some("Pi 1.4.0"),
359 rules: Rules::TypeScript(&typescript::PI),
360 },
361 other_events: &[],
362 },
363];
364
365pub fn target(root: &str) -> Option<&'static Target> {
367 TARGETS.iter().find(|target| target.root == root)
368}
369
370pub fn all() -> &'static [Target] {
372 TARGETS
373}
374
375pub fn claude_event(name: &str) -> Option<&'static Event> {
377 CLAUDE_EVENTS.iter().find(|event| event.name == name)
378}
379
380pub fn claude_event_names() -> String {
382 CLAUDE_EVENTS
383 .iter()
384 .map(|event| event.name)
385 .collect::<Vec<_>>()
386 .join(", ")
387}