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; }
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";