Skip to main content

lean_ctx/hooks/
agents.rs

1use std::path::PathBuf;
2
3use super::{
4    full_server_entry, generate_compact_rewrite_script, generate_rewrite_script,
5    install_mcp_json_agent, install_project_rules, is_inside_git_repo, make_executable,
6    mcp_server_quiet_mode, resolve_binary_path, resolve_binary_path_for_bash, write_file,
7    KIRO_STEERING_TEMPLATE, REDIRECT_SCRIPT_CLAUDE, REDIRECT_SCRIPT_GENERIC,
8};
9
10pub(super) fn install_claude_hook(global: bool) {
11    let home = match dirs::home_dir() {
12        Some(h) => h,
13        None => {
14            eprintln!("Cannot resolve home directory");
15            return;
16        }
17    };
18
19    install_claude_hook_scripts(&home);
20    install_claude_hook_config(&home);
21
22    let scope = crate::core::config::Config::load().rules_scope_effective();
23    if scope != crate::core::config::RulesScope::Project {
24        install_claude_rules_file(&home);
25        install_claude_global_claude_md(&home);
26        install_claude_skill(&home);
27    }
28
29    let _ = global;
30}
31
32const CLAUDE_MD_BLOCK_START: &str = "<!-- lean-ctx -->";
33const CLAUDE_MD_BLOCK_END: &str = "<!-- /lean-ctx -->";
34const CLAUDE_MD_BLOCK_VERSION: &str = "lean-ctx-claude-v2";
35
36const CLAUDE_MD_BLOCK_CONTENT: &str = "\
37<!-- lean-ctx -->
38<!-- lean-ctx-claude-v2 -->
39## lean-ctx — Context Runtime
40
41Always prefer lean-ctx MCP tools over native equivalents:
42- `ctx_read` instead of `Read` / `cat` (cached, 10 modes, re-reads ~13 tokens)
43- `ctx_shell` instead of `bash` / `Shell` (90+ compression patterns)
44- `ctx_search` instead of `Grep` / `rg` (compact results)
45- `ctx_tree` instead of `ls` / `find` (compact directory maps)
46- Native Edit/StrReplace stay unchanged. If Edit requires Read and Read is unavailable, use `ctx_edit(path, old_string, new_string)` instead.
47- Write, Delete, Glob — use normally.
48
49Full rules: @rules/lean-ctx.md
50
51Verify setup: run `/mcp` to check lean-ctx is connected, `/memory` to confirm this file loaded.
52<!-- /lean-ctx -->";
53
54fn install_claude_global_claude_md(home: &std::path::Path) {
55    let claude_dir = crate::core::editor_registry::claude_state_dir(home);
56    let _ = std::fs::create_dir_all(&claude_dir);
57    let claude_md_path = claude_dir.join("CLAUDE.md");
58
59    let existing = std::fs::read_to_string(&claude_md_path).unwrap_or_default();
60
61    if existing.contains(CLAUDE_MD_BLOCK_START) {
62        if existing.contains(CLAUDE_MD_BLOCK_VERSION) {
63            return;
64        }
65        let cleaned = remove_block(&existing, CLAUDE_MD_BLOCK_START, CLAUDE_MD_BLOCK_END);
66        let updated = format!("{}\n\n{}\n", cleaned.trim(), CLAUDE_MD_BLOCK_CONTENT);
67        write_file(&claude_md_path, &updated);
68        return;
69    }
70
71    if existing.trim().is_empty() {
72        write_file(&claude_md_path, CLAUDE_MD_BLOCK_CONTENT);
73    } else {
74        let updated = format!("{}\n\n{}\n", existing.trim(), CLAUDE_MD_BLOCK_CONTENT);
75        write_file(&claude_md_path, &updated);
76    }
77}
78
79fn remove_block(content: &str, start: &str, end: &str) -> String {
80    let s = content.find(start);
81    let e = content.find(end);
82    match (s, e) {
83        (Some(si), Some(ei)) if ei >= si => {
84            let after_end = ei + end.len();
85            let before = content[..si].trim_end_matches('\n');
86            let after = &content[after_end..];
87            let mut out = before.to_string();
88            out.push('\n');
89            if !after.trim().is_empty() {
90                out.push('\n');
91                out.push_str(after.trim_start_matches('\n'));
92            }
93            out
94        }
95        _ => content.to_string(),
96    }
97}
98
99fn install_claude_skill(home: &std::path::Path) {
100    let skill_dir = home.join(".claude/skills/lean-ctx");
101    let _ = std::fs::create_dir_all(skill_dir.join("scripts"));
102
103    let skill_md = include_str!("../../skills/lean-ctx/SKILL.md");
104    let install_sh = include_str!("../../skills/lean-ctx/scripts/install.sh");
105
106    let skill_path = skill_dir.join("SKILL.md");
107    let script_path = skill_dir.join("scripts/install.sh");
108
109    write_file(&skill_path, skill_md);
110    write_file(&script_path, install_sh);
111
112    #[cfg(unix)]
113    {
114        use std::os::unix::fs::PermissionsExt;
115        if let Ok(mut perms) = std::fs::metadata(&script_path).map(|m| m.permissions()) {
116            perms.set_mode(0o755);
117            let _ = std::fs::set_permissions(&script_path, perms);
118        }
119    }
120}
121
122fn install_claude_rules_file(home: &std::path::Path) {
123    let rules_dir = crate::core::editor_registry::claude_rules_dir(home);
124    let _ = std::fs::create_dir_all(&rules_dir);
125    let rules_path = rules_dir.join("lean-ctx.md");
126
127    let desired = crate::rules_inject::rules_dedicated_markdown();
128    let existing = std::fs::read_to_string(&rules_path).unwrap_or_default();
129
130    if existing.is_empty() {
131        write_file(&rules_path, desired);
132        return;
133    }
134    if existing.contains(crate::rules_inject::RULES_VERSION_STR) {
135        return;
136    }
137    if existing.contains("<!-- lean-ctx-rules-") {
138        write_file(&rules_path, desired);
139    }
140}
141
142pub(super) fn install_claude_hook_scripts(home: &std::path::Path) {
143    let hooks_dir = crate::core::editor_registry::claude_state_dir(home).join("hooks");
144    let _ = std::fs::create_dir_all(&hooks_dir);
145
146    let binary = resolve_binary_path();
147
148    let rewrite_path = hooks_dir.join("lean-ctx-rewrite.sh");
149    let rewrite_script = generate_rewrite_script(&resolve_binary_path_for_bash());
150    write_file(&rewrite_path, &rewrite_script);
151    make_executable(&rewrite_path);
152
153    let redirect_path = hooks_dir.join("lean-ctx-redirect.sh");
154    write_file(&redirect_path, REDIRECT_SCRIPT_CLAUDE);
155    make_executable(&redirect_path);
156
157    let wrapper = |subcommand: &str| -> String {
158        if cfg!(windows) {
159            format!("{binary} hook {subcommand}")
160        } else {
161            format!("{} hook {subcommand}", resolve_binary_path_for_bash())
162        }
163    };
164
165    let rewrite_native = hooks_dir.join("lean-ctx-rewrite-native");
166    write_file(
167        &rewrite_native,
168        &format!(
169            "#!/bin/sh\nexec {} hook rewrite\n",
170            resolve_binary_path_for_bash()
171        ),
172    );
173    make_executable(&rewrite_native);
174
175    let redirect_native = hooks_dir.join("lean-ctx-redirect-native");
176    write_file(
177        &redirect_native,
178        &format!(
179            "#!/bin/sh\nexec {} hook redirect\n",
180            resolve_binary_path_for_bash()
181        ),
182    );
183    make_executable(&redirect_native);
184
185    let _ = wrapper; // suppress unused warning on unix
186}
187
188pub(super) fn install_claude_hook_config(home: &std::path::Path) {
189    let hooks_dir = crate::core::editor_registry::claude_state_dir(home).join("hooks");
190    let binary = resolve_binary_path();
191
192    let rewrite_cmd = format!("{binary} hook rewrite");
193    let redirect_cmd = format!("{binary} hook redirect");
194
195    let settings_path = crate::core::editor_registry::claude_state_dir(home).join("settings.json");
196    let settings_content = if settings_path.exists() {
197        std::fs::read_to_string(&settings_path).unwrap_or_default()
198    } else {
199        String::new()
200    };
201
202    let needs_update =
203        !settings_content.contains("hook rewrite") || !settings_content.contains("hook redirect");
204    let has_old_hooks = settings_content.contains("lean-ctx-rewrite.sh")
205        || settings_content.contains("lean-ctx-redirect.sh");
206
207    if !needs_update && !has_old_hooks {
208        return;
209    }
210
211    let hook_entry = serde_json::json!({
212        "hooks": {
213            "PreToolUse": [
214                {
215                    "matcher": "Bash|bash",
216                    "hooks": [{
217                        "type": "command",
218                        "command": rewrite_cmd
219                    }]
220                },
221                {
222                    "matcher": "Read|read|ReadFile|read_file|View|view|Grep|grep|Search|search|ListFiles|list_files|ListDirectory|list_directory",
223                    "hooks": [{
224                        "type": "command",
225                        "command": redirect_cmd
226                    }]
227                }
228            ]
229        }
230    });
231
232    if settings_content.is_empty() {
233        write_file(
234            &settings_path,
235            &serde_json::to_string_pretty(&hook_entry).unwrap(),
236        );
237    } else if let Ok(mut existing) = serde_json::from_str::<serde_json::Value>(&settings_content) {
238        if let Some(obj) = existing.as_object_mut() {
239            obj.insert("hooks".to_string(), hook_entry["hooks"].clone());
240            write_file(
241                &settings_path,
242                &serde_json::to_string_pretty(&existing).unwrap(),
243            );
244        }
245    }
246    if !mcp_server_quiet_mode() {
247        println!("Installed Claude Code hooks at {}", hooks_dir.display());
248    }
249}
250
251pub(super) fn install_claude_project_hooks(cwd: &std::path::Path) {
252    let binary = resolve_binary_path();
253    let rewrite_cmd = format!("{binary} hook rewrite");
254    let redirect_cmd = format!("{binary} hook redirect");
255
256    let settings_path = cwd.join(".claude").join("settings.local.json");
257    let _ = std::fs::create_dir_all(cwd.join(".claude"));
258
259    let existing = std::fs::read_to_string(&settings_path).unwrap_or_default();
260    if existing.contains("hook rewrite") && existing.contains("hook redirect") {
261        return;
262    }
263
264    let hook_entry = serde_json::json!({
265        "hooks": {
266            "PreToolUse": [
267                {
268                    "matcher": "Bash|bash",
269                    "hooks": [{
270                        "type": "command",
271                        "command": rewrite_cmd
272                    }]
273                },
274                {
275                    "matcher": "Read|read|ReadFile|read_file|View|view|Grep|grep|Search|search|ListFiles|list_files|ListDirectory|list_directory",
276                    "hooks": [{
277                        "type": "command",
278                        "command": redirect_cmd
279                    }]
280                }
281            ]
282        }
283    });
284
285    if existing.is_empty() {
286        write_file(
287            &settings_path,
288            &serde_json::to_string_pretty(&hook_entry).unwrap(),
289        );
290    } else if let Ok(mut json) = serde_json::from_str::<serde_json::Value>(&existing) {
291        if let Some(obj) = json.as_object_mut() {
292            obj.insert("hooks".to_string(), hook_entry["hooks"].clone());
293            write_file(
294                &settings_path,
295                &serde_json::to_string_pretty(&json).unwrap(),
296            );
297        }
298    }
299    println!("Created .claude/settings.local.json (project-local PreToolUse hooks).");
300}
301
302pub fn install_cursor_hook(global: bool) {
303    let home = match dirs::home_dir() {
304        Some(h) => h,
305        None => {
306            eprintln!("Cannot resolve home directory");
307            return;
308        }
309    };
310
311    install_cursor_hook_scripts(&home);
312    install_cursor_hook_config(&home);
313
314    let scope = crate::core::config::Config::load().rules_scope_effective();
315    let skip_project = global || scope == crate::core::config::RulesScope::Global;
316
317    if !skip_project {
318        let rules_dir = PathBuf::from(".cursor").join("rules");
319        let _ = std::fs::create_dir_all(&rules_dir);
320        let rule_path = rules_dir.join("lean-ctx.mdc");
321        if !rule_path.exists() {
322            let rule_content = include_str!("../templates/lean-ctx.mdc");
323            write_file(&rule_path, rule_content);
324            println!("Created .cursor/rules/lean-ctx.mdc in current project.");
325        } else {
326            println!("Cursor rule already exists.");
327        }
328    } else {
329        println!("Global mode: skipping project-local .cursor/rules/ (use without --global in a project).");
330    }
331
332    println!("Restart Cursor to activate.");
333}
334
335pub(super) fn install_cursor_hook_scripts(home: &std::path::Path) {
336    let hooks_dir = home.join(".cursor").join("hooks");
337    let _ = std::fs::create_dir_all(&hooks_dir);
338
339    let binary = resolve_binary_path_for_bash();
340
341    let rewrite_path = hooks_dir.join("lean-ctx-rewrite.sh");
342    let rewrite_script = generate_compact_rewrite_script(&binary);
343    write_file(&rewrite_path, &rewrite_script);
344    make_executable(&rewrite_path);
345
346    let redirect_path = hooks_dir.join("lean-ctx-redirect.sh");
347    write_file(&redirect_path, REDIRECT_SCRIPT_GENERIC);
348    make_executable(&redirect_path);
349
350    let native_binary = resolve_binary_path();
351    let rewrite_native = hooks_dir.join("lean-ctx-rewrite-native");
352    write_file(
353        &rewrite_native,
354        &format!("#!/bin/sh\nexec {} hook rewrite\n", native_binary),
355    );
356    make_executable(&rewrite_native);
357
358    let redirect_native = hooks_dir.join("lean-ctx-redirect-native");
359    write_file(
360        &redirect_native,
361        &format!("#!/bin/sh\nexec {} hook redirect\n", native_binary),
362    );
363    make_executable(&redirect_native);
364}
365
366pub(super) fn install_cursor_hook_config(home: &std::path::Path) {
367    let binary = resolve_binary_path();
368    let rewrite_cmd = format!("{binary} hook rewrite");
369    let redirect_cmd = format!("{binary} hook redirect");
370
371    let hooks_json = home.join(".cursor").join("hooks.json");
372
373    let hook_config = serde_json::json!({
374        "version": 1,
375        "hooks": {
376            "preToolUse": [
377                {
378                    "matcher": "Shell",
379                    "command": rewrite_cmd
380                },
381                {
382                    "matcher": "Read|Grep",
383                    "command": redirect_cmd
384                }
385            ]
386        }
387    });
388
389    let content = if hooks_json.exists() {
390        std::fs::read_to_string(&hooks_json).unwrap_or_default()
391    } else {
392        String::new()
393    };
394
395    let has_correct_matchers = content.contains("\"Shell\"")
396        && (content.contains("\"Read|Grep\"") || content.contains("\"Read\""));
397    let has_correct_format = content.contains("\"version\"") && content.contains("\"preToolUse\"");
398    if has_correct_format
399        && has_correct_matchers
400        && content.contains("hook rewrite")
401        && content.contains("hook redirect")
402    {
403        return;
404    }
405
406    if content.is_empty() || !content.contains("\"version\"") {
407        write_file(
408            &hooks_json,
409            &serde_json::to_string_pretty(&hook_config).unwrap(),
410        );
411    } else if let Ok(mut existing) = serde_json::from_str::<serde_json::Value>(&content) {
412        if let Some(obj) = existing.as_object_mut() {
413            obj.insert("version".to_string(), serde_json::json!(1));
414            obj.insert("hooks".to_string(), hook_config["hooks"].clone());
415            write_file(
416                &hooks_json,
417                &serde_json::to_string_pretty(&existing).unwrap(),
418            );
419        }
420    } else {
421        write_file(
422            &hooks_json,
423            &serde_json::to_string_pretty(&hook_config).unwrap(),
424        );
425    }
426
427    if !mcp_server_quiet_mode() {
428        println!("Installed Cursor hooks at {}", hooks_json.display());
429    }
430}
431
432pub(super) fn install_gemini_hook() {
433    let home = match dirs::home_dir() {
434        Some(h) => h,
435        None => {
436            eprintln!("Cannot resolve home directory");
437            return;
438        }
439    };
440
441    install_gemini_hook_scripts(&home);
442    install_gemini_hook_config(&home);
443}
444
445pub(super) fn install_gemini_hook_scripts(home: &std::path::Path) {
446    let hooks_dir = home.join(".gemini").join("hooks");
447    let _ = std::fs::create_dir_all(&hooks_dir);
448
449    let binary = resolve_binary_path_for_bash();
450
451    let rewrite_path = hooks_dir.join("lean-ctx-rewrite-gemini.sh");
452    let rewrite_script = generate_compact_rewrite_script(&binary);
453    write_file(&rewrite_path, &rewrite_script);
454    make_executable(&rewrite_path);
455
456    let redirect_path = hooks_dir.join("lean-ctx-redirect-gemini.sh");
457    write_file(&redirect_path, REDIRECT_SCRIPT_GENERIC);
458    make_executable(&redirect_path);
459}
460
461pub(super) fn install_gemini_hook_config(home: &std::path::Path) {
462    let binary = resolve_binary_path();
463    let rewrite_cmd = format!("{binary} hook rewrite");
464    let redirect_cmd = format!("{binary} hook redirect");
465
466    let settings_path = home.join(".gemini").join("settings.json");
467    let settings_content = if settings_path.exists() {
468        std::fs::read_to_string(&settings_path).unwrap_or_default()
469    } else {
470        String::new()
471    };
472
473    let has_new_format = settings_content.contains("hook rewrite")
474        && settings_content.contains("hook redirect")
475        && settings_content.contains("\"type\"")
476        && settings_content.contains("\"matcher\"");
477    let has_old_hooks = settings_content.contains("lean-ctx-rewrite")
478        || settings_content.contains("lean-ctx-redirect")
479        || (settings_content.contains("hook rewrite") && !settings_content.contains("\"matcher\""));
480
481    if has_new_format && !has_old_hooks {
482        return;
483    }
484
485    let hook_config = serde_json::json!({
486        "hooks": {
487            "BeforeTool": [
488                {
489                    "matcher": "shell|execute_command|run_shell_command",
490                    "hooks": [{
491                        "type": "command",
492                        "command": rewrite_cmd
493                    }]
494                },
495                {
496                    "matcher": "read_file|read_many_files|grep|search|list_dir",
497                    "hooks": [{
498                        "type": "command",
499                        "command": redirect_cmd
500                    }]
501                }
502            ]
503        }
504    });
505
506    if settings_content.is_empty() {
507        write_file(
508            &settings_path,
509            &serde_json::to_string_pretty(&hook_config).unwrap(),
510        );
511    } else if let Ok(mut existing) = serde_json::from_str::<serde_json::Value>(&settings_content) {
512        if let Some(obj) = existing.as_object_mut() {
513            obj.insert("hooks".to_string(), hook_config["hooks"].clone());
514            write_file(
515                &settings_path,
516                &serde_json::to_string_pretty(&existing).unwrap(),
517            );
518        }
519    }
520    if !mcp_server_quiet_mode() {
521        println!(
522            "Installed Gemini CLI hooks at {}",
523            settings_path.parent().unwrap_or(&settings_path).display()
524        );
525    }
526}
527
528pub fn install_codex_hook() {
529    let home = match dirs::home_dir() {
530        Some(h) => h,
531        None => {
532            eprintln!("Cannot resolve home directory");
533            return;
534        }
535    };
536
537    let codex_dir = home.join(".codex");
538    let _ = std::fs::create_dir_all(&codex_dir);
539
540    install_codex_hook_scripts(&home);
541    install_codex_hook_config(&home);
542
543    let agents_path = codex_dir.join("AGENTS.md");
544    let agents_content = "# Global Agent Instructions\n\n@LEAN-CTX.md\n";
545
546    let lean_ctx_md = codex_dir.join("LEAN-CTX.md");
547    let binary = resolve_binary_path();
548    let lean_ctx_content = format!(
549        r#"# lean-ctx — Token Optimization
550
551Prefix all shell commands with `{binary} -c` for compressed output:
552
553```bash
554{binary} -c git status    # instead of: git status
555{binary} -c cargo test    # instead of: cargo test
556{binary} -c ls src/       # instead of: ls src/
557```
558
559This saves 60-90% tokens per command. Works with: git, cargo, npm, pnpm, docker, kubectl, pip, ruff, go, curl, grep, find, ls, aws, helm, and 90+ more commands.
560Use `{binary} -c --raw <cmd>` to skip compression and get full output.
561"#
562    );
563
564    if agents_path.exists() {
565        let content = std::fs::read_to_string(&agents_path).unwrap_or_default();
566        if content.contains("lean-ctx") || content.contains("LEAN-CTX") {
567            println!("Codex AGENTS.md already configured.");
568            return;
569        }
570    }
571
572    write_file(&agents_path, agents_content);
573    write_file(&lean_ctx_md, &lean_ctx_content);
574    println!("Installed Codex instructions at {}", codex_dir.display());
575}
576
577fn install_codex_hook_config(home: &std::path::Path) {
578    let binary = resolve_binary_path();
579    let rewrite_cmd = format!("{binary} hook rewrite");
580
581    let codex_dir = home.join(".codex");
582
583    let hooks_json_path = codex_dir.join("hooks.json");
584    let hook_config = serde_json::json!({
585        "hooks": {
586            "PreToolUse": [
587                {
588                    "matcher": "Bash",
589                    "hooks": [{
590                        "type": "command",
591                        "command": rewrite_cmd,
592                        "timeout": 15
593                    }]
594                }
595            ]
596        }
597    });
598
599    let needs_write = if hooks_json_path.exists() {
600        let content = std::fs::read_to_string(&hooks_json_path).unwrap_or_default();
601        !content.contains("hook rewrite")
602    } else {
603        true
604    };
605
606    if needs_write {
607        if hooks_json_path.exists() {
608            if let Ok(mut existing) = serde_json::from_str::<serde_json::Value>(
609                &std::fs::read_to_string(&hooks_json_path).unwrap_or_default(),
610            ) {
611                if let Some(obj) = existing.as_object_mut() {
612                    obj.insert("hooks".to_string(), hook_config["hooks"].clone());
613                    write_file(
614                        &hooks_json_path,
615                        &serde_json::to_string_pretty(&existing).unwrap(),
616                    );
617                    if !mcp_server_quiet_mode() {
618                        println!("Updated Codex hooks.json at {}", hooks_json_path.display());
619                    }
620                    return;
621                }
622            }
623        }
624        write_file(
625            &hooks_json_path,
626            &serde_json::to_string_pretty(&hook_config).unwrap(),
627        );
628        if !mcp_server_quiet_mode() {
629            println!(
630                "Installed Codex hooks.json at {}",
631                hooks_json_path.display()
632            );
633        }
634    }
635
636    let config_toml_path = codex_dir.join("config.toml");
637    let config_content = std::fs::read_to_string(&config_toml_path).unwrap_or_default();
638    if !config_content.contains("codex_hooks") {
639        let mut out = config_content;
640        if !out.is_empty() && !out.ends_with('\n') {
641            out.push('\n');
642        }
643        if !out.contains("[features]") {
644            out.push_str("\n[features]\ncodex_hooks = true\n");
645        } else {
646            out.push_str("codex_hooks = true\n");
647        }
648        write_file(&config_toml_path, &out);
649        if !mcp_server_quiet_mode() {
650            println!(
651                "Enabled codex_hooks feature in {}",
652                config_toml_path.display()
653            );
654        }
655    }
656}
657
658pub(super) fn install_codex_hook_scripts(home: &std::path::Path) {
659    let hooks_dir = home.join(".codex").join("hooks");
660    let _ = std::fs::create_dir_all(&hooks_dir);
661
662    let binary = resolve_binary_path_for_bash();
663    let rewrite_path = hooks_dir.join("lean-ctx-rewrite-codex.sh");
664    let rewrite_script = generate_compact_rewrite_script(&binary);
665    write_file(&rewrite_path, &rewrite_script);
666    make_executable(&rewrite_path);
667    if !mcp_server_quiet_mode() {
668        println!(
669            "  \x1b[32m✓\x1b[0m Installed Codex hook scripts at {}",
670            hooks_dir.display()
671        );
672    }
673}
674
675pub(super) fn install_windsurf_rules(global: bool) {
676    let scope = crate::core::config::Config::load().rules_scope_effective();
677    if global || scope == crate::core::config::RulesScope::Global {
678        println!("Global mode: skipping project-local .windsurfrules (use without --global in a project).");
679        return;
680    }
681
682    let cwd = std::env::current_dir().unwrap_or_default();
683    if !is_inside_git_repo(&cwd) || cwd == dirs::home_dir().unwrap_or_default() {
684        eprintln!("  Skipping .windsurfrules: not inside a git repository or in home directory.");
685        return;
686    }
687
688    let rules_path = PathBuf::from(".windsurfrules");
689    if rules_path.exists() {
690        let content = std::fs::read_to_string(&rules_path).unwrap_or_default();
691        if content.contains("lean-ctx") {
692            println!(".windsurfrules already configured.");
693            return;
694        }
695    }
696
697    let rules = include_str!("../templates/windsurfrules.txt");
698    write_file(&rules_path, rules);
699    println!("Installed .windsurfrules in current project.");
700}
701
702pub(super) fn install_cline_rules(global: bool) {
703    let scope = crate::core::config::Config::load().rules_scope_effective();
704    if global || scope == crate::core::config::RulesScope::Global {
705        println!(
706            "Global mode: skipping project-local .clinerules (use without --global in a project)."
707        );
708        return;
709    }
710
711    let cwd = std::env::current_dir().unwrap_or_default();
712    if !is_inside_git_repo(&cwd) || cwd == dirs::home_dir().unwrap_or_default() {
713        eprintln!("  Skipping .clinerules: not inside a git repository or in home directory.");
714        return;
715    }
716
717    let rules_path = PathBuf::from(".clinerules");
718    if rules_path.exists() {
719        let content = std::fs::read_to_string(&rules_path).unwrap_or_default();
720        if content.contains("lean-ctx") {
721            println!(".clinerules already configured.");
722            return;
723        }
724    }
725
726    let binary = resolve_binary_path();
727    let rules = format!(
728        r#"# lean-ctx Shell Optimization
729# Prefix all shell commands with `{binary} -c` for 60-90% token savings.
730
731When running shell commands, always prefix them with `{binary} -c` to compress output. For example:
732- `{binary} -c git status` instead of `git status`
733- `{binary} -c cargo test` instead of `cargo test`
734- `{binary} -c ls src/` instead of `ls src/`
735
736Supported commands: git, cargo, npm, pnpm, docker, kubectl, pip, ruff, go, curl, grep, find, ls, aws, helm, and 90+ more.
737"#
738    );
739
740    write_file(&rules_path, &rules);
741    println!("Installed .clinerules in current project.");
742}
743
744pub(super) fn install_pi_hook(global: bool) {
745    let has_pi = std::process::Command::new("pi")
746        .arg("--version")
747        .output()
748        .is_ok();
749
750    if !has_pi {
751        println!("Pi Coding Agent not found in PATH.");
752        println!("Install Pi first: npm install -g @mariozechner/pi-coding-agent");
753        println!();
754    }
755
756    println!("Installing pi-lean-ctx Pi Package...");
757    println!();
758
759    let install_result = std::process::Command::new("pi")
760        .args(["install", "npm:pi-lean-ctx"])
761        .status();
762
763    match install_result {
764        Ok(status) if status.success() => {
765            println!("Installed pi-lean-ctx Pi Package.");
766        }
767        _ => {
768            println!("Could not auto-install pi-lean-ctx. Install manually:");
769            println!("  pi install npm:pi-lean-ctx");
770            println!();
771        }
772    }
773
774    write_pi_mcp_config();
775
776    let scope = crate::core::config::Config::load().rules_scope_effective();
777    let skip_project = global || scope == crate::core::config::RulesScope::Global;
778
779    if !skip_project {
780        let agents_md = PathBuf::from("AGENTS.md");
781        if !agents_md.exists()
782            || !std::fs::read_to_string(&agents_md)
783                .unwrap_or_default()
784                .contains("lean-ctx")
785        {
786            let content = include_str!("../templates/PI_AGENTS.md");
787            write_file(&agents_md, content);
788            println!("Created AGENTS.md in current project directory.");
789        } else {
790            println!("AGENTS.md already contains lean-ctx configuration.");
791        }
792    } else {
793        println!(
794            "Global mode: skipping project-local AGENTS.md (use without --global in a project)."
795        );
796    }
797
798    println!();
799    println!("Setup complete. All Pi tools (bash, read, grep, find, ls) route through lean-ctx.");
800    println!("MCP tools (ctx_session, ctx_knowledge, ctx_semantic_search, ...) also available.");
801    println!("Use /lean-ctx in Pi to verify the binary path and MCP status.");
802}
803
804fn write_pi_mcp_config() {
805    let home = match dirs::home_dir() {
806        Some(h) => h,
807        None => return,
808    };
809
810    let mcp_config_path = home.join(".pi/agent/mcp.json");
811
812    if !home.join(".pi/agent").exists() {
813        println!("  \x1b[2m○ ~/.pi/agent/ not found — skipping MCP config\x1b[0m");
814        return;
815    }
816
817    if mcp_config_path.exists() {
818        let content = match std::fs::read_to_string(&mcp_config_path) {
819            Ok(c) => c,
820            Err(_) => return,
821        };
822        if content.contains("lean-ctx") {
823            println!("  \x1b[32m✓\x1b[0m Pi MCP config already contains lean-ctx");
824            return;
825        }
826
827        if let Ok(mut json) = serde_json::from_str::<serde_json::Value>(&content) {
828            if let Some(obj) = json.as_object_mut() {
829                let servers = obj
830                    .entry("mcpServers")
831                    .or_insert_with(|| serde_json::json!({}));
832                if let Some(servers_obj) = servers.as_object_mut() {
833                    servers_obj.insert("lean-ctx".to_string(), pi_mcp_server_entry());
834                }
835                if let Ok(formatted) = serde_json::to_string_pretty(&json) {
836                    let _ = std::fs::write(&mcp_config_path, formatted);
837                    println!(
838                        "  \x1b[32m✓\x1b[0m Added lean-ctx to Pi MCP config (~/.pi/agent/mcp.json)"
839                    );
840                }
841            }
842        }
843        return;
844    }
845
846    let content = serde_json::json!({
847        "mcpServers": {
848            "lean-ctx": pi_mcp_server_entry()
849        }
850    });
851    if let Ok(formatted) = serde_json::to_string_pretty(&content) {
852        let _ = std::fs::write(&mcp_config_path, formatted);
853        println!("  \x1b[32m✓\x1b[0m Created Pi MCP config (~/.pi/agent/mcp.json)");
854    }
855}
856
857fn pi_mcp_server_entry() -> serde_json::Value {
858    let binary = resolve_binary_path();
859    let mut entry = full_server_entry(&binary);
860    if let Some(obj) = entry.as_object_mut() {
861        obj.insert("lifecycle".to_string(), serde_json::json!("lazy"));
862        obj.insert("directTools".to_string(), serde_json::json!(true));
863    }
864    entry
865}
866
867pub(super) fn install_copilot_hook(global: bool) {
868    let binary = resolve_binary_path();
869
870    if global {
871        let mcp_path = copilot_global_mcp_path();
872        if mcp_path.as_os_str() == "/nonexistent" {
873            println!("  \x1b[2mVS Code not found — skipping global Copilot config\x1b[0m");
874            return;
875        }
876        write_vscode_mcp_file(&mcp_path, &binary, "global VS Code User MCP");
877        install_copilot_pretooluse_hook(true);
878    } else {
879        let vscode_dir = PathBuf::from(".vscode");
880        let _ = std::fs::create_dir_all(&vscode_dir);
881        let mcp_path = vscode_dir.join("mcp.json");
882        write_vscode_mcp_file(&mcp_path, &binary, ".vscode/mcp.json");
883        install_copilot_pretooluse_hook(false);
884    }
885}
886
887fn install_copilot_pretooluse_hook(global: bool) {
888    let binary = resolve_binary_path();
889    let rewrite_cmd = format!("{binary} hook rewrite");
890    let redirect_cmd = format!("{binary} hook redirect");
891
892    let hook_config = serde_json::json!({
893        "version": 1,
894        "hooks": {
895            "preToolUse": [
896                {
897                    "type": "command",
898                    "bash": rewrite_cmd,
899                    "timeoutSec": 15
900                },
901                {
902                    "type": "command",
903                    "bash": redirect_cmd,
904                    "timeoutSec": 5
905                }
906            ]
907        }
908    });
909
910    let hook_path = if global {
911        let Some(home) = dirs::home_dir() else { return };
912        let dir = home.join(".github").join("hooks");
913        let _ = std::fs::create_dir_all(&dir);
914        dir.join("hooks.json")
915    } else {
916        let dir = PathBuf::from(".github").join("hooks");
917        let _ = std::fs::create_dir_all(&dir);
918        dir.join("hooks.json")
919    };
920
921    let needs_write = if hook_path.exists() {
922        let content = std::fs::read_to_string(&hook_path).unwrap_or_default();
923        !content.contains("hook rewrite") || content.contains("\"PreToolUse\"")
924    } else {
925        true
926    };
927
928    if !needs_write {
929        return;
930    }
931
932    if hook_path.exists() {
933        if let Ok(mut existing) = serde_json::from_str::<serde_json::Value>(
934            &std::fs::read_to_string(&hook_path).unwrap_or_default(),
935        ) {
936            if let Some(obj) = existing.as_object_mut() {
937                obj.insert("version".to_string(), serde_json::json!(1));
938                obj.insert("hooks".to_string(), hook_config["hooks"].clone());
939                write_file(
940                    &hook_path,
941                    &serde_json::to_string_pretty(&existing).unwrap(),
942                );
943                if !mcp_server_quiet_mode() {
944                    println!("Updated Copilot hooks at {}", hook_path.display());
945                }
946                return;
947            }
948        }
949    }
950
951    write_file(
952        &hook_path,
953        &serde_json::to_string_pretty(&hook_config).unwrap(),
954    );
955    if !mcp_server_quiet_mode() {
956        println!("Installed Copilot hooks at {}", hook_path.display());
957    }
958}
959
960fn copilot_global_mcp_path() -> PathBuf {
961    if let Some(home) = dirs::home_dir() {
962        #[cfg(target_os = "macos")]
963        {
964            return home.join("Library/Application Support/Code/User/mcp.json");
965        }
966        #[cfg(target_os = "linux")]
967        {
968            return home.join(".config/Code/User/mcp.json");
969        }
970        #[cfg(target_os = "windows")]
971        {
972            if let Ok(appdata) = std::env::var("APPDATA") {
973                return PathBuf::from(appdata).join("Code/User/mcp.json");
974            }
975        }
976        #[allow(unreachable_code)]
977        home.join(".config/Code/User/mcp.json")
978    } else {
979        PathBuf::from("/nonexistent")
980    }
981}
982
983fn write_vscode_mcp_file(mcp_path: &PathBuf, binary: &str, label: &str) {
984    let data_dir = crate::core::data_dir::lean_ctx_data_dir()
985        .map(|d| d.to_string_lossy().to_string())
986        .unwrap_or_default();
987    let desired = serde_json::json!({ "type": "stdio", "command": binary, "args": [], "env": { "LEAN_CTX_DATA_DIR": data_dir } });
988    if mcp_path.exists() {
989        let content = std::fs::read_to_string(mcp_path).unwrap_or_default();
990        match serde_json::from_str::<serde_json::Value>(&content) {
991            Ok(mut json) => {
992                if let Some(obj) = json.as_object_mut() {
993                    let servers = obj
994                        .entry("servers")
995                        .or_insert_with(|| serde_json::json!({}));
996                    if let Some(servers_obj) = servers.as_object_mut() {
997                        if servers_obj.get("lean-ctx") == Some(&desired) {
998                            println!("  \x1b[32m✓\x1b[0m Copilot already configured in {label}");
999                            return;
1000                        }
1001                        servers_obj.insert("lean-ctx".to_string(), desired);
1002                    }
1003                    write_file(
1004                        mcp_path,
1005                        &serde_json::to_string_pretty(&json).unwrap_or_default(),
1006                    );
1007                    println!("  \x1b[32m✓\x1b[0m Added lean-ctx to {label}");
1008                    return;
1009                }
1010            }
1011            Err(e) => {
1012                eprintln!(
1013                    "Could not parse VS Code MCP config at {}: {e}\nAdd to \"servers\": \"lean-ctx\": {{ \"command\": \"{}\", \"args\": [] }}",
1014                    mcp_path.display(),
1015                    binary
1016                );
1017                return;
1018            }
1019        };
1020    }
1021
1022    if let Some(parent) = mcp_path.parent() {
1023        let _ = std::fs::create_dir_all(parent);
1024    }
1025
1026    let data_dir = crate::core::data_dir::lean_ctx_data_dir()
1027        .map(|d| d.to_string_lossy().to_string())
1028        .unwrap_or_default();
1029    let config = serde_json::json!({
1030        "servers": {
1031            "lean-ctx": {
1032                "type": "stdio",
1033                "command": binary,
1034                "args": [],
1035                "env": { "LEAN_CTX_DATA_DIR": data_dir }
1036            }
1037        }
1038    });
1039
1040    write_file(
1041        mcp_path,
1042        &serde_json::to_string_pretty(&config).unwrap_or_default(),
1043    );
1044    println!("  \x1b[32m✓\x1b[0m Created {label} with lean-ctx MCP server");
1045}
1046
1047pub(super) fn install_amp_hook() {
1048    let binary = resolve_binary_path();
1049    let home = dirs::home_dir().unwrap_or_default();
1050    let config_path = home.join(".config/amp/settings.json");
1051    let display_path = "~/.config/amp/settings.json";
1052
1053    if let Some(parent) = config_path.parent() {
1054        let _ = std::fs::create_dir_all(parent);
1055    }
1056
1057    let data_dir = crate::core::data_dir::lean_ctx_data_dir()
1058        .map(|d| d.to_string_lossy().to_string())
1059        .unwrap_or_default();
1060    let entry = serde_json::json!({
1061        "command": binary,
1062        "env": { "LEAN_CTX_DATA_DIR": data_dir }
1063    });
1064
1065    if config_path.exists() {
1066        let content = std::fs::read_to_string(&config_path).unwrap_or_default();
1067        if content.contains("lean-ctx") {
1068            println!("Amp MCP already configured at {display_path}");
1069            return;
1070        }
1071
1072        if let Ok(mut json) = serde_json::from_str::<serde_json::Value>(&content) {
1073            if let Some(obj) = json.as_object_mut() {
1074                let servers = obj
1075                    .entry("amp.mcpServers")
1076                    .or_insert_with(|| serde_json::json!({}));
1077                if let Some(servers_obj) = servers.as_object_mut() {
1078                    servers_obj.insert("lean-ctx".to_string(), entry.clone());
1079                }
1080                if let Ok(formatted) = serde_json::to_string_pretty(&json) {
1081                    let _ = std::fs::write(&config_path, formatted);
1082                    println!("  \x1b[32m✓\x1b[0m Amp MCP configured at {display_path}");
1083                    return;
1084                }
1085            }
1086        }
1087    }
1088
1089    let config = serde_json::json!({ "amp.mcpServers": { "lean-ctx": entry } });
1090    if let Ok(json_str) = serde_json::to_string_pretty(&config) {
1091        let _ = std::fs::write(&config_path, json_str);
1092        println!("  \x1b[32m✓\x1b[0m Amp MCP configured at {display_path}");
1093    } else {
1094        eprintln!("  \x1b[31m✗\x1b[0m Failed to configure Amp");
1095    }
1096}
1097
1098pub(super) fn install_jetbrains_hook() {
1099    let binary = resolve_binary_path();
1100    let home = dirs::home_dir().unwrap_or_default();
1101    let config_path = home.join(".jb-mcp.json");
1102    let display_path = "~/.jb-mcp.json";
1103
1104    let entry = serde_json::json!({
1105        "name": "lean-ctx",
1106        "command": binary,
1107        "args": [],
1108        "env": {
1109            "LEAN_CTX_DATA_DIR": crate::core::data_dir::lean_ctx_data_dir()
1110                .map(|d| d.to_string_lossy().to_string())
1111                .unwrap_or_default()
1112        }
1113    });
1114
1115    if config_path.exists() {
1116        let content = std::fs::read_to_string(&config_path).unwrap_or_default();
1117        if content.contains("lean-ctx") {
1118            println!("JetBrains MCP already configured at {display_path}");
1119            return;
1120        }
1121
1122        if let Ok(mut json) = serde_json::from_str::<serde_json::Value>(&content) {
1123            if let Some(obj) = json.as_object_mut() {
1124                let servers = obj
1125                    .entry("servers")
1126                    .or_insert_with(|| serde_json::json!([]));
1127                if let Some(arr) = servers.as_array_mut() {
1128                    arr.push(entry.clone());
1129                }
1130                if let Ok(formatted) = serde_json::to_string_pretty(&json) {
1131                    let _ = std::fs::write(&config_path, formatted);
1132                    println!("  \x1b[32m✓\x1b[0m JetBrains MCP configured at {display_path}");
1133                    return;
1134                }
1135            }
1136        }
1137    }
1138
1139    let config = serde_json::json!({ "servers": [entry] });
1140    if let Ok(json_str) = serde_json::to_string_pretty(&config) {
1141        let _ = std::fs::write(&config_path, json_str);
1142        println!("  \x1b[32m✓\x1b[0m JetBrains MCP configured at {display_path}");
1143    } else {
1144        eprintln!("  \x1b[31m✗\x1b[0m Failed to configure JetBrains");
1145    }
1146}
1147
1148pub(super) fn install_opencode_hook() {
1149    let binary = resolve_binary_path();
1150    let home = dirs::home_dir().unwrap_or_default();
1151    let config_path = home.join(".config/opencode/opencode.json");
1152    let display_path = "~/.config/opencode/opencode.json";
1153
1154    if let Some(parent) = config_path.parent() {
1155        let _ = std::fs::create_dir_all(parent);
1156    }
1157
1158    let data_dir = crate::core::data_dir::lean_ctx_data_dir()
1159        .map(|d| d.to_string_lossy().to_string())
1160        .unwrap_or_default();
1161    let desired = serde_json::json!({
1162        "type": "local",
1163        "command": [&binary],
1164        "enabled": true,
1165        "environment": { "LEAN_CTX_DATA_DIR": data_dir }
1166    });
1167
1168    if config_path.exists() {
1169        let content = std::fs::read_to_string(&config_path).unwrap_or_default();
1170        if content.contains("lean-ctx") {
1171            println!("OpenCode MCP already configured at {display_path}");
1172        } else if let Ok(mut json) = serde_json::from_str::<serde_json::Value>(&content) {
1173            if let Some(obj) = json.as_object_mut() {
1174                let mcp = obj.entry("mcp").or_insert_with(|| serde_json::json!({}));
1175                if let Some(mcp_obj) = mcp.as_object_mut() {
1176                    mcp_obj.insert("lean-ctx".to_string(), desired.clone());
1177                }
1178                if let Ok(formatted) = serde_json::to_string_pretty(&json) {
1179                    let _ = std::fs::write(&config_path, formatted);
1180                    println!("  \x1b[32m✓\x1b[0m OpenCode MCP configured at {display_path}");
1181                }
1182            }
1183        }
1184    } else {
1185        let content = serde_json::to_string_pretty(&serde_json::json!({
1186            "$schema": "https://opencode.ai/config.json",
1187            "mcp": {
1188                "lean-ctx": desired
1189            }
1190        }));
1191
1192        if let Ok(json_str) = content {
1193            let _ = std::fs::write(&config_path, json_str);
1194            println!("  \x1b[32m✓\x1b[0m OpenCode MCP configured at {display_path}");
1195        } else {
1196            eprintln!("  \x1b[31m✗\x1b[0m Failed to configure OpenCode");
1197        }
1198    }
1199
1200    install_opencode_plugin(&home);
1201}
1202
1203fn install_opencode_plugin(home: &std::path::Path) {
1204    let plugin_dir = home.join(".config/opencode/plugins");
1205    let _ = std::fs::create_dir_all(&plugin_dir);
1206    let plugin_path = plugin_dir.join("lean-ctx.ts");
1207
1208    let plugin_content = include_str!("../templates/opencode-plugin.ts");
1209    let _ = std::fs::write(&plugin_path, plugin_content);
1210
1211    if !mcp_server_quiet_mode() {
1212        println!(
1213            "  \x1b[32m✓\x1b[0m OpenCode plugin installed at {}",
1214            plugin_path.display()
1215        );
1216    }
1217}
1218
1219pub(super) fn install_crush_hook() {
1220    let binary = resolve_binary_path();
1221    let home = dirs::home_dir().unwrap_or_default();
1222    let config_path = home.join(".config/crush/crush.json");
1223    let display_path = "~/.config/crush/crush.json";
1224
1225    if let Some(parent) = config_path.parent() {
1226        let _ = std::fs::create_dir_all(parent);
1227    }
1228
1229    if config_path.exists() {
1230        let content = std::fs::read_to_string(&config_path).unwrap_or_default();
1231        if content.contains("lean-ctx") {
1232            println!("Crush MCP already configured at {display_path}");
1233            return;
1234        }
1235
1236        if let Ok(mut json) = serde_json::from_str::<serde_json::Value>(&content) {
1237            if let Some(obj) = json.as_object_mut() {
1238                let servers = obj.entry("mcp").or_insert_with(|| serde_json::json!({}));
1239                if let Some(servers_obj) = servers.as_object_mut() {
1240                    servers_obj.insert(
1241                        "lean-ctx".to_string(),
1242                        serde_json::json!({ "type": "stdio", "command": binary }),
1243                    );
1244                }
1245                if let Ok(formatted) = serde_json::to_string_pretty(&json) {
1246                    let _ = std::fs::write(&config_path, formatted);
1247                    println!("  \x1b[32m✓\x1b[0m Crush MCP configured at {display_path}");
1248                    return;
1249                }
1250            }
1251        }
1252    }
1253
1254    let content = serde_json::to_string_pretty(&serde_json::json!({
1255        "mcp": {
1256            "lean-ctx": {
1257                "type": "stdio",
1258                "command": binary
1259            }
1260        }
1261    }));
1262
1263    if let Ok(json_str) = content {
1264        let _ = std::fs::write(&config_path, json_str);
1265        println!("  \x1b[32m✓\x1b[0m Crush MCP configured at {display_path}");
1266    } else {
1267        eprintln!("  \x1b[31m✗\x1b[0m Failed to configure Crush");
1268    }
1269}
1270
1271pub(super) fn install_kiro_hook() {
1272    let home = dirs::home_dir().unwrap_or_default();
1273
1274    install_mcp_json_agent(
1275        "AWS Kiro",
1276        "~/.kiro/settings/mcp.json",
1277        &home.join(".kiro/settings/mcp.json"),
1278    );
1279
1280    let cwd = std::env::current_dir().unwrap_or_default();
1281    let steering_dir = cwd.join(".kiro").join("steering");
1282    let steering_file = steering_dir.join("lean-ctx.md");
1283
1284    if steering_file.exists()
1285        && std::fs::read_to_string(&steering_file)
1286            .unwrap_or_default()
1287            .contains("lean-ctx")
1288    {
1289        println!("  Kiro steering file already exists at .kiro/steering/lean-ctx.md");
1290    } else {
1291        let _ = std::fs::create_dir_all(&steering_dir);
1292        write_file(&steering_file, KIRO_STEERING_TEMPLATE);
1293        println!("  \x1b[32m✓\x1b[0m Created .kiro/steering/lean-ctx.md (Kiro will now prefer lean-ctx tools)");
1294    }
1295}
1296
1297pub(super) fn install_hermes_hook(global: bool) {
1298    let home = match dirs::home_dir() {
1299        Some(h) => h,
1300        None => {
1301            eprintln!("Cannot resolve home directory");
1302            return;
1303        }
1304    };
1305
1306    let binary = resolve_binary_path();
1307    let config_path = home.join(".hermes/config.yaml");
1308    let target = crate::core::editor_registry::EditorTarget {
1309        name: "Hermes Agent",
1310        agent_key: "hermes".to_string(),
1311        config_path: config_path.clone(),
1312        detect_path: home.join(".hermes"),
1313        config_type: crate::core::editor_registry::ConfigType::HermesYaml,
1314    };
1315
1316    match crate::core::editor_registry::write_config_with_options(
1317        &target,
1318        &binary,
1319        crate::core::editor_registry::WriteOptions {
1320            overwrite_invalid: true,
1321        },
1322    ) {
1323        Ok(res) => match res.action {
1324            crate::core::editor_registry::WriteAction::Created => {
1325                println!("  \x1b[32m✓\x1b[0m Hermes Agent MCP configured at ~/.hermes/config.yaml");
1326            }
1327            crate::core::editor_registry::WriteAction::Updated => {
1328                println!("  \x1b[32m✓\x1b[0m Hermes Agent MCP updated at ~/.hermes/config.yaml");
1329            }
1330            crate::core::editor_registry::WriteAction::Already => {
1331                println!("  Hermes Agent MCP already configured at ~/.hermes/config.yaml");
1332            }
1333        },
1334        Err(e) => {
1335            eprintln!("  \x1b[31m✗\x1b[0m Failed to configure Hermes Agent MCP: {e}");
1336        }
1337    }
1338
1339    let scope = crate::core::config::Config::load().rules_scope_effective();
1340
1341    match scope {
1342        crate::core::config::RulesScope::Global => {
1343            install_hermes_rules(&home);
1344        }
1345        crate::core::config::RulesScope::Project => {
1346            if !global {
1347                install_project_hermes_rules();
1348                install_project_rules();
1349            }
1350        }
1351        crate::core::config::RulesScope::Both => {
1352            if global {
1353                install_hermes_rules(&home);
1354            } else {
1355                install_hermes_rules(&home);
1356                install_project_hermes_rules();
1357                install_project_rules();
1358            }
1359        }
1360    }
1361}
1362
1363fn install_hermes_rules(home: &std::path::Path) {
1364    let rules_path = home.join(".hermes/HERMES.md");
1365    let content = HERMES_RULES_TEMPLATE;
1366
1367    if rules_path.exists() {
1368        let existing = std::fs::read_to_string(&rules_path).unwrap_or_default();
1369        if existing.contains("lean-ctx") {
1370            println!("  Hermes rules already present in ~/.hermes/HERMES.md");
1371            return;
1372        }
1373        let mut updated = existing;
1374        if !updated.ends_with('\n') {
1375            updated.push('\n');
1376        }
1377        updated.push('\n');
1378        updated.push_str(content);
1379        let _ = std::fs::write(&rules_path, updated);
1380        println!("  \x1b[32m✓\x1b[0m Appended lean-ctx rules to ~/.hermes/HERMES.md");
1381    } else {
1382        let _ = std::fs::create_dir_all(rules_path.parent().unwrap());
1383        let _ = std::fs::write(&rules_path, content);
1384        println!("  \x1b[32m✓\x1b[0m Created ~/.hermes/HERMES.md with lean-ctx rules");
1385    }
1386}
1387
1388fn install_project_hermes_rules() {
1389    let cwd = match std::env::current_dir() {
1390        Ok(d) => d,
1391        Err(_) => return,
1392    };
1393    let rules_path = cwd.join(".hermes.md");
1394    if rules_path.exists() {
1395        let existing = std::fs::read_to_string(&rules_path).unwrap_or_default();
1396        if existing.contains("lean-ctx") {
1397            println!("  .hermes.md already contains lean-ctx rules");
1398            return;
1399        }
1400        let mut updated = existing;
1401        if !updated.ends_with('\n') {
1402            updated.push('\n');
1403        }
1404        updated.push('\n');
1405        updated.push_str(HERMES_RULES_TEMPLATE);
1406        let _ = std::fs::write(&rules_path, updated);
1407        println!("  \x1b[32m✓\x1b[0m Appended lean-ctx rules to .hermes.md");
1408    } else {
1409        let _ = std::fs::write(&rules_path, HERMES_RULES_TEMPLATE);
1410        println!("  \x1b[32m✓\x1b[0m Created .hermes.md with lean-ctx rules");
1411    }
1412}
1413
1414const HERMES_RULES_TEMPLATE: &str = "\
1415# lean-ctx — Context Engineering Layer
1416
1417PREFER lean-ctx MCP tools over native equivalents for token savings:
1418
1419| PREFER | OVER | Why |
1420|--------|------|-----|
1421| `ctx_read(path, mode)` | `Read` / `cat` | Cached, 10 read modes, re-reads ~13 tokens |
1422| `ctx_shell(command)` | `Shell` / `bash` | Pattern compression for git/npm/cargo output |
1423| `ctx_search(pattern, path)` | `Grep` / `rg` | Compact search results |
1424| `ctx_tree(path, depth)` | `ls` / `find` | Compact directory maps |
1425
1426- Native Edit/StrReplace stay unchanged. If Edit requires Read and Read is unavailable, use `ctx_edit(path, old_string, new_string)`.
1427- Write, Delete, Glob — use normally.
1428
1429ctx_read modes: full|map|signatures|diff|task|reference|aggressive|entropy|lines:N-M. Auto-selects optimal mode.
1430Re-reads cost ~13 tokens (cached).
1431
1432Available tools: ctx_overview, ctx_preload, ctx_dedup, ctx_compress, ctx_session, ctx_knowledge, ctx_semantic_search.
1433Multi-agent: ctx_agent(action=handoff|sync). Diary: ctx_agent(action=diary, category=discovery|decision|blocker|progress|insight).
1434";