use serde_json::Value;
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub struct LifecycleProfile {
pub id: &'static str,
pub claude_command_prefix: &'static str,
pub claude_legacy_substrings: &'static [&'static str],
pub claude_managed_events: &'static [(&'static str, &'static str, &'static str)],
pub codex_command_prefix: &'static str,
pub codex_managed_events: &'static [(&'static str, &'static str, &'static str, &'static str)],
}
impl LifecycleProfile {
pub fn validate(&self) -> Result<(), &'static str> {
if self.id.is_empty() {
return Err("profile id must not be empty");
}
if self.claude_command_prefix.is_empty() {
return Err("claude command prefix must not be empty");
}
if self.codex_command_prefix.is_empty() {
return Err("codex command prefix must not be empty");
}
if self
.claude_legacy_substrings
.iter()
.any(|legacy| legacy.is_empty())
{
return Err("claude legacy substrings must not be empty");
}
Ok(())
}
pub fn claude_command(&self, hook_arg: &str) -> String {
format!("{}{}", self.claude_command_prefix, hook_arg)
}
pub fn codex_command(&self, hook_arg: &str) -> String {
format!("{}{}", self.codex_command_prefix, hook_arg)
}
pub(super) fn claude_entry_is_managed_or_legacy(&self, entry: &Value) -> bool {
let cmd = entry.get("command").and_then(Value::as_str).unwrap_or("");
(!self.claude_command_prefix.is_empty() && cmd.starts_with(self.claude_command_prefix))
|| self
.claude_legacy_substrings
.iter()
.any(|legacy| !legacy.is_empty() && cmd.contains(legacy))
}
pub(super) fn codex_entry_is_managed(&self, entry: &Value) -> bool {
entry
.get("command")
.and_then(Value::as_str)
.map(|cmd| {
!self.codex_command_prefix.is_empty() && cmd.starts_with(self.codex_command_prefix)
})
.unwrap_or(false)
}
}
const STANDARD_CLAUDE_MANAGED_EVENTS: &[(&str, &str, &str)] = &[
(
"SessionStart",
"on-session-start",
"startup|resume|clear|compact",
),
("UserPromptSubmit", "before-prompt-build", "*"),
("PreCompact", "on-compaction-notice", "*"),
("Stop", "on-agent-end", "*"),
("SessionEnd", "on-session-end", "*"),
];
const STANDARD_CODEX_MANAGED_EVENTS: &[(&str, &str, &str, &str)] = &[
(
"SessionStart",
"on-session-start",
"startup|resume|clear",
"Loading CCD session context",
),
(
"UserPromptSubmit",
"before-prompt-build",
"*",
"Refreshing CCD prompt context",
),
(
"PreCompact",
"on-compaction-notice",
"*",
"Recording CCD compaction boundary",
),
(
"PostCompact",
"on-compaction-notice",
"*",
"Recording CCD compacted context boundary",
),
(
"Stop",
"on-agent-end",
"*",
"Checking CCD continuation boundary",
),
];
const LIFELOOP_DIRECT_CODEX_MANAGED_EVENTS: &[(&str, &str, &str, &str)] = &[
(
"SessionStart",
"on-session-start",
"startup|resume|clear",
"Loading Lifeloop session context",
),
(
"UserPromptSubmit",
"before-prompt-build",
"*",
"Refreshing Lifeloop prompt context",
),
(
"Stop",
"on-agent-end",
"*",
"Checking Lifeloop continuation boundary",
),
];
pub const CCD_COMPAT_PROFILE: LifecycleProfile = LifecycleProfile {
id: "ccd-compat",
claude_command_prefix: "\"${CCD_BIN:-ccd}\" --output hook-protocol host-hook --path \"$CLAUDE_PROJECT_DIR\" --host claude --hook ",
claude_legacy_substrings: &["ccd-hook.py"],
claude_managed_events: STANDARD_CLAUDE_MANAGED_EVENTS,
codex_command_prefix: "\"${CCD_BIN:-ccd}\" --output hook-protocol host-hook --path \"$(git rev-parse --show-toplevel)\" --host codex --hook ",
codex_managed_events: STANDARD_CODEX_MANAGED_EVENTS,
};
pub const LIFELOOP_DIRECT_PROFILE: LifecycleProfile = LifecycleProfile {
id: "lifeloop-direct",
claude_command_prefix: "\"${LIFELOOP_BIN:-lifeloop}\" --output hook-protocol host-hook --path \"$CLAUDE_PROJECT_DIR\" --host claude --hook ",
claude_legacy_substrings: &[CCD_COMPAT_PROFILE.claude_command_prefix, "ccd-hook.py"],
claude_managed_events: STANDARD_CLAUDE_MANAGED_EVENTS,
codex_command_prefix: "\"${LIFELOOP_BIN:-lifeloop}\" --output hook-protocol host-hook --path \"$(git rev-parse --show-toplevel)\" --host codex --hook ",
codex_managed_events: LIFELOOP_DIRECT_CODEX_MANAGED_EVENTS,
};
pub const CCD_COMPAT_CLAUDE_COMMAND_PREFIX: &str = CCD_COMPAT_PROFILE.claude_command_prefix;
pub const CCD_COMPAT_CODEX_COMMAND_PREFIX: &str = CCD_COMPAT_PROFILE.codex_command_prefix;
pub const CCD_COMPAT_CLAUDE_LEGACY_PYTHON_HOOK: &str = "ccd-hook.py";
pub fn ccd_compat_claude_command(hook_arg: &str) -> String {
CCD_COMPAT_PROFILE.claude_command(hook_arg)
}
pub fn ccd_compat_codex_command(hook_arg: &str) -> String {
CCD_COMPAT_PROFILE.codex_command(hook_arg)
}