#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GovernorArm {
NoMcp,
McpDeferred,
}
impl GovernorArm {
pub fn from_mode(raw: &str) -> Option<Self> {
match raw.trim().to_ascii_lowercase().as_str() {
"governor" | "governor-nomcp" => Some(Self::NoMcp),
"governor-mcp" => Some(Self::McpDeferred),
_ => None,
}
}
pub fn label(self) -> &'static str {
match self {
Self::NoMcp => "governor",
Self::McpDeferred => "governor-mcp",
}
}
}
const RULES_MCP: &str = "\
<!-- lean-ctx -->
<!-- lean-ctx-governor-v1 -->
lean-ctx keeps native Read/Bash results lossless; an identical repeat may show `[unchanged …]` (the full result is earlier in this conversation).
Optional `ctx_*` tools (e.g. ctx_compose for ranked files with source) load on demand via tool search.
<!-- /lean-ctx -->";
pub fn rules_block(arm: GovernorArm) -> Option<&'static str> {
match arm {
GovernorArm::NoMcp => None,
GovernorArm::McpDeferred => Some(RULES_MCP),
}
}
pub fn is_governor_installed(home: &std::path::Path) -> bool {
std::fs::read_to_string(
crate::core::editor_registry::claude_state_dir(home).join("settings.json"),
)
.is_ok_and(|c| settings_have_govern_hook(&c))
}
pub(crate) fn settings_have_govern_hook(content: &str) -> bool {
fn exec_form(value: &serde_json::Value) -> bool {
match value {
serde_json::Value::Object(object) => {
object
.get("args")
.and_then(serde_json::Value::as_array)
.is_some_and(|args| args.len() == 2 && args[0] == "hook" && args[1] == "govern")
|| object.values().any(exec_form)
}
serde_json::Value::Array(array) => array.iter().any(exec_form),
_ => false,
}
}
content.contains(" hook govern\"")
|| (content.contains("\"govern\"")
&& crate::core::jsonc::parse_jsonc(content).is_ok_and(|v| exec_form(&v)))
}
pub fn has_lean_ctx_mcp_entry(home: &std::path::Path) -> bool {
lean_ctx_mcp_entry_state(home) == Some(true)
}
pub fn lean_ctx_mcp_entry_state(home: &std::path::Path) -> Option<bool> {
match std::fs::read_to_string(crate::core::editor_registry::claude_mcp_json_path(home)) {
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Some(false),
Err(_) => None,
Ok(c) if c.trim().is_empty() => Some(false),
Ok(c) => crate::core::jsonc::parse_jsonc(&c)
.ok()
.map(|v| v.pointer("/mcpServers/lean-ctx").is_some()),
}
}
pub fn is_lean_ctx_statusline(cmd: &str) -> bool {
statusline_args(cmd).is_some()
}
pub fn statusline_args(cmd: &str) -> Option<&str> {
let idx = cmd.find(" statusline")?;
let (bin, rest) = cmd.split_at(idx);
let rest = &rest[" statusline".len()..];
let ends_token = rest.is_empty() || rest.starts_with(' ');
(bin.contains("lean-ctx") && ends_token).then_some(rest)
}
pub const REGISTER_TS: &str = include_str!("../templates/claude_mod/register.ts");
pub const HOOKS_JSON: &str = include_str!("../templates/claude_mod/hooks.json");
pub const COCKPIT_TSX: &str = include_str!("../templates/claude_mod/cockpit.tsx");
pub const RENDER_TSX: &str = include_str!("../templates/claude_mod/render.tsx");
pub const TYPES_DTS: &str = include_str!("../templates/claude_mod/cockpit.d.ts");
pub const PLUGIN_JSON: &str = include_str!("../templates/claude_mod/plugin.json");
#[cfg(test)]
mod tests {
use super::settings_have_govern_hook;
#[test]
fn govern_hook_detected_in_shell_and_exec_form() {
let shell = serde_json::json!({"hooks": {"PostToolUse": [{"hooks": [
{"type": "command", "command": "\"/a b/lean-ctx\" hook govern"}
]}]}});
assert!(settings_have_govern_hook(
&serde_json::to_string_pretty(&shell).unwrap()
));
let exec = serde_json::json!({"hooks": {"PostToolUse": [{"hooks": [
{"type": "command", "command": "C:/x/Author Software/lean-ctx.exe", "args": ["hook", "govern"]}
]}]}});
assert!(settings_have_govern_hook(
&serde_json::to_string_pretty(&exec).unwrap()
));
let standard = serde_json::json!({"hooks": {"PreToolUse": [{"hooks": [
{"type": "command", "command": "C:/x/Author Software/lean-ctx.exe", "args": ["hook", "rewrite"]}
]}]}});
assert!(!settings_have_govern_hook(&standard.to_string()));
assert!(!settings_have_govern_hook("{}"));
}
}