lean_ctx/core/
claude_host.rs1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum GovernorArm {
14 NoMcp,
16 McpDeferred,
18}
19
20impl GovernorArm {
21 pub fn from_mode(raw: &str) -> Option<Self> {
23 match raw.trim().to_ascii_lowercase().as_str() {
24 "governor" | "governor-nomcp" => Some(Self::NoMcp),
25 "governor-mcp" => Some(Self::McpDeferred),
26 _ => None,
27 }
28 }
29
30 pub fn label(self) -> &'static str {
31 match self {
32 Self::NoMcp => "governor",
33 Self::McpDeferred => "governor-mcp",
34 }
35 }
36}
37
38const RULES_MCP: &str = "\
39<!-- lean-ctx -->
40<!-- lean-ctx-governor-v1 -->
41lean-ctx keeps native Read/Bash results lossless; an identical repeat may show `[unchanged …]` (the full result is earlier in this conversation).
42Optional `ctx_*` tools (e.g. ctx_compose for ranked files with source) load on demand via tool search.
43<!-- /lean-ctx -->";
44
45pub fn rules_block(arm: GovernorArm) -> Option<&'static str> {
48 match arm {
49 GovernorArm::NoMcp => None,
50 GovernorArm::McpDeferred => Some(RULES_MCP),
51 }
52}
53
54pub fn is_governor_installed(home: &std::path::Path) -> bool {
57 std::fs::read_to_string(
58 crate::core::editor_registry::claude_state_dir(home).join("settings.json"),
59 )
60 .is_ok_and(|c| settings_have_govern_hook(&c))
61}
62
63pub(crate) fn settings_have_govern_hook(content: &str) -> bool {
66 fn exec_form(value: &serde_json::Value) -> bool {
67 match value {
68 serde_json::Value::Object(object) => {
69 object
70 .get("args")
71 .and_then(serde_json::Value::as_array)
72 .is_some_and(|args| args.len() == 2 && args[0] == "hook" && args[1] == "govern")
73 || object.values().any(exec_form)
74 }
75 serde_json::Value::Array(array) => array.iter().any(exec_form),
76 _ => false,
77 }
78 }
79 content.contains(" hook govern\"")
80 || (content.contains("\"govern\"")
81 && crate::core::jsonc::parse_jsonc(content).is_ok_and(|v| exec_form(&v)))
82}
83
84pub fn has_lean_ctx_mcp_entry(home: &std::path::Path) -> bool {
89 lean_ctx_mcp_entry_state(home) == Some(true)
90}
91
92pub fn lean_ctx_mcp_entry_state(home: &std::path::Path) -> Option<bool> {
96 match std::fs::read_to_string(crate::core::editor_registry::claude_mcp_json_path(home)) {
97 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Some(false),
98 Err(_) => None,
99 Ok(c) if c.trim().is_empty() => Some(false),
100 Ok(c) => crate::core::jsonc::parse_jsonc(&c)
101 .ok()
102 .map(|v| v.pointer("/mcpServers/lean-ctx").is_some()),
103 }
104}
105
106pub fn is_lean_ctx_statusline(cmd: &str) -> bool {
108 statusline_args(cmd).is_some()
109}
110
111pub fn statusline_args(cmd: &str) -> Option<&str> {
114 let idx = cmd.find(" statusline")?;
115 let (bin, rest) = cmd.split_at(idx);
116 let rest = &rest[" statusline".len()..];
117 let ends_token = rest.is_empty() || rest.starts_with(' ');
118 (bin.contains("lean-ctx") && ends_token).then_some(rest)
119}
120
121pub const REGISTER_TS: &str = include_str!("../templates/claude_mod/register.ts");
122pub const HOOKS_JSON: &str = include_str!("../templates/claude_mod/hooks.json");
123pub const COCKPIT_TSX: &str = include_str!("../templates/claude_mod/cockpit.tsx");
125pub const RENDER_TSX: &str = include_str!("../templates/claude_mod/render.tsx");
128pub const TYPES_DTS: &str = include_str!("../templates/claude_mod/cockpit.d.ts");
130pub const PLUGIN_JSON: &str = include_str!("../templates/claude_mod/plugin.json");
132
133#[cfg(test)]
134mod tests {
135 use super::settings_have_govern_hook;
136
137 #[test]
138 fn govern_hook_detected_in_shell_and_exec_form() {
139 let shell = serde_json::json!({"hooks": {"PostToolUse": [{"hooks": [
140 {"type": "command", "command": "\"/a b/lean-ctx\" hook govern"}
141 ]}]}});
142 assert!(settings_have_govern_hook(
143 &serde_json::to_string_pretty(&shell).unwrap()
144 ));
145 let exec = serde_json::json!({"hooks": {"PostToolUse": [{"hooks": [
146 {"type": "command", "command": "C:/x/Author Software/lean-ctx.exe", "args": ["hook", "govern"]}
147 ]}]}});
148 assert!(settings_have_govern_hook(
149 &serde_json::to_string_pretty(&exec).unwrap()
150 ));
151 let standard = serde_json::json!({"hooks": {"PreToolUse": [{"hooks": [
152 {"type": "command", "command": "C:/x/Author Software/lean-ctx.exe", "args": ["hook", "rewrite"]}
153 ]}]}});
154 assert!(!settings_have_govern_hook(&standard.to_string()));
155 assert!(!settings_have_govern_hook("{}"));
156 }
157}