lean_ctx/setup/
helpers.rs1#[allow(clippy::wildcard_imports)]
5use super::*;
6
7pub fn install_skill_files(home: &std::path::Path) -> Vec<(String, bool)> {
8 crate::rules_inject::install_all_skills(home)
9}
10
11pub(crate) fn install_kiro_steering(home: &std::path::Path) {
12 let cwd = std::env::current_dir().unwrap_or_else(|_| home.to_path_buf());
13 let steering_dir = cwd.join(".kiro").join("steering");
14 let steering_file = steering_dir.join("lean-ctx.md");
15
16 if steering_file.exists()
17 && std::fs::read_to_string(&steering_file)
18 .unwrap_or_default()
19 .contains("lean-ctx")
20 {
21 println!(" Kiro steering file already exists at .kiro/steering/lean-ctx.md");
22 return;
23 }
24
25 let _ = std::fs::create_dir_all(&steering_dir);
26 let _ = std::fs::write(&steering_file, crate::hooks::KIRO_STEERING_TEMPLATE);
27 println!(" \x1b[32m✓\x1b[0m Created .kiro/steering/lean-ctx.md (Kiro will now prefer lean-ctx tools)");
28}
29
30pub(crate) fn configure_plan_mode_settings(newly_configured: &[&str], already_configured: &[&str]) {
31 use crate::terminal_ui;
32
33 let all_configured: Vec<&str> = newly_configured
34 .iter()
35 .chain(already_configured.iter())
36 .copied()
37 .collect();
38
39 let has_vscode = all_configured.contains(&"VS Code");
40 let has_claude = all_configured.contains(&"Claude Code");
41 let has_codebuddy = all_configured.contains(&"CodeBuddy");
42
43 if !has_vscode && !has_claude && !has_codebuddy {
44 return;
45 }
46
47 if has_vscode {
48 match crate::core::editor_registry::plan_mode::write_vscode_plan_settings() {
49 Ok(r) if r.action == WriteAction::Already => {
50 terminal_ui::print_status_ok(
51 "VS Code \x1b[2mplan mode already configured\x1b[0m",
52 );
53 }
54 Ok(_) => {
55 terminal_ui::print_status_new(
56 "VS Code \x1b[2mplan mode tools configured\x1b[0m",
57 );
58 }
59 Err(e) => {
60 terminal_ui::print_status_warn(&format!("VS Code plan mode: {e}"));
61 }
62 }
63 }
64
65 if has_claude {
66 match crate::core::editor_registry::plan_mode::write_claude_code_plan_permissions() {
67 Ok(r) if r.action == WriteAction::Already => {
68 terminal_ui::print_status_ok(
69 "Claude Code \x1b[2mplan mode permissions present\x1b[0m",
70 );
71 }
72 Ok(_) => {
73 terminal_ui::print_status_new(
74 "Claude Code \x1b[2mplan mode permissions added\x1b[0m",
75 );
76 }
77 Err(e) => {
78 terminal_ui::print_status_warn(&format!("Claude Code plan mode: {e}"));
79 }
80 }
81 }
82
83 if has_codebuddy {
84 match crate::core::editor_registry::plan_mode::write_claude_code_plan_permissions() {
85 Ok(r) if r.action == WriteAction::Already => {
86 terminal_ui::print_status_ok(
87 "CodeBuddy \x1b[2mplan mode permissions present\x1b[0m",
88 );
89 }
90 Ok(_) => {
91 terminal_ui::print_status_new(
92 "CodeBuddy \x1b[2mplan mode permissions added\x1b[0m",
93 );
94 }
95 Err(e) => {
96 terminal_ui::print_status_warn(&format!("CodeBuddy plan mode: {e}"));
97 }
98 }
99 }
100}
101
102pub(crate) fn shorten_path(path: &str, home: &str) -> String {
103 if let Some(stripped) = path.strip_prefix(home) {
104 format!("~{stripped}")
105 } else {
106 path.to_string()
107 }
108}
109
110fn upsert_toml_key(content: &mut String, key: &str, value: &str) {
111 let pattern = format!("{key} = ");
112 if let Some(start) = content.find(&pattern) {
113 let line_end = content[start..]
114 .find('\n')
115 .map_or(content.len(), |p| start + p);
116 content.replace_range(start..line_end, &format!("{key} = \"{value}\""));
117 } else {
118 if !content.is_empty() && !content.ends_with('\n') {
119 content.push('\n');
120 }
121 content.push_str(&format!("{key} = \"{value}\"\n"));
122 }
123}
124
125fn remove_toml_key(content: &mut String, key: &str) {
126 let pattern = format!("{key} = ");
127 if let Some(start) = content.find(&pattern) {
128 let line_end = content[start..]
129 .find('\n')
130 .map_or(content.len(), |p| start + p + 1);
131 content.replace_range(start..line_end, "");
132 }
133}
134
135pub(crate) fn configure_tool_profile() {
136 use crate::terminal_ui;
137 use std::io::Write;
138
139 let cfg = crate::core::config::Config::load();
140 let current = cfg.tool_profile_effective();
141 let pinned = cfg.tool_profile.is_some() || std::env::var("LEAN_CTX_TOOL_PROFILE").is_ok();
142
143 if pinned && !matches!(current, crate::core::tool_profiles::ToolProfile::Power) {
147 terminal_ui::print_status_ok(&format!(
148 "Tool profile: {} ({} tools)",
149 current.as_str(),
150 current.tool_count()
151 ));
152 return;
153 }
154
155 let dim = "\x1b[2m";
156 let bold = "\x1b[1m";
157 let cyan = "\x1b[36m";
158 let rst = "\x1b[0m";
159
160 let registry_count = crate::server::registry::tool_count();
161 let lazy_count = crate::tool_defs::core_tool_names().len();
162
163 println!(" {dim}Control how many MCP tool schemas your AI agent sees.{rst}");
164 println!(" {dim}Fewer advertised tools = less context overhead. Every tool stays{rst}");
165 println!(" {dim}callable through ctx_call, even when its schema is not advertised.{rst}");
166 println!();
167 println!(
168 " {cyan}lean{rst} — {lazy_count} tools {dim}(lazy core, recommended — lowest token overhead){rst}"
169 );
170 println!(
171 " {cyan}minimal{rst} — 6 tools {dim}(ctx_read, ctx_shell, shell, ctx_search, ctx_tree, ctx_session){rst}"
172 );
173 println!(" {cyan}standard{rst} — 22 tools {dim}(balanced set for most workflows){rst}");
174 println!(
175 " {cyan}power{rst} — {registry_count} tools {dim}(everything advertised, costs the most context){rst}"
176 );
177 println!();
178 print!(" Tool profile? {bold}[lean/minimal/standard/power]{rst} {dim}(default: lean){rst} ");
179 std::io::stdout().flush().ok();
180
181 let mut profile_input = String::new();
182 let profile_name = if std::io::stdin().read_line(&mut profile_input).is_ok() {
183 let trimmed = profile_input.trim().to_lowercase();
184 match trimmed.as_str() {
185 "minimal" | "min" => "minimal",
186 "standard" | "std" => "standard",
187 "power" | "full" | "all" => "power",
188 _ => "lean",
189 }
190 } else {
191 "lean"
192 };
193
194 if profile_name == "lean" {
195 match crate::core::tool_profiles::clear_profile_in_config() {
196 Ok(()) => terminal_ui::print_status_ok(&format!(
197 "Tool profile: lean ({lazy_count} tools advertised, all reachable via ctx_call)"
198 )),
199 Err(e) => terminal_ui::print_status_warn(&format!("Could not save tool profile: {e}")),
200 }
201 return;
202 }
203
204 match crate::core::tool_profiles::set_profile_in_config(profile_name) {
205 Ok(()) => {
206 let profile = crate::core::tool_profiles::ToolProfile::parse(profile_name)
207 .unwrap_or(crate::core::tool_profiles::ToolProfile::Standard);
208 let count = match &profile {
209 crate::core::tool_profiles::ToolProfile::Power => registry_count,
210 other => other.tool_count(),
211 };
212 terminal_ui::print_status_ok(&format!("Tool profile: {profile_name} ({count} tools)"));
213 }
214 Err(e) => {
215 terminal_ui::print_status_warn(&format!("Could not save tool profile: {e}"));
216 }
217 }
218}
219
220pub(crate) fn configure_premium_features(home: &std::path::Path) {
221 use crate::terminal_ui;
222 use std::io::Write;
223
224 let config_path = crate::core::config::Config::path()
225 .unwrap_or_else(|| home.join(".config/lean-ctx").join("config.toml"));
226 if let Some(dir) = config_path.parent() {
227 let _ = std::fs::create_dir_all(dir);
228 }
229 let mut config_content = std::fs::read_to_string(&config_path).unwrap_or_default();
230
231 let dim = "\x1b[2m";
232 let bold = "\x1b[1m";
233 let cyan = "\x1b[36m";
234 let rst = "\x1b[0m";
235
236 println!("\n {bold}Compression Level{rst} {dim}(controls all token optimization layers){rst}");
238 println!(" {dim}Applies to tool output, agent prompts, and protocol mode.{rst}");
239 println!();
240 println!(" {cyan}off{rst} — No compression (full verbose output)");
241 println!(" {cyan}lite{rst} — Light: concise output, basic terse filtering {dim}(~25% savings){rst}");
242 println!(" {cyan}standard{rst} — Dense output + compact protocol + pattern-aware {dim}(~45% savings){rst}");
243 println!(" {cyan}max{rst} — Expert mode: TDD protocol, all layers active {dim}(~65% savings){rst}");
244 println!();
245 print!(" Compression level? {bold}[off/lite/standard/max]{rst} {dim}(default: off){rst} ");
246 std::io::stdout().flush().ok();
247
248 let mut level_input = String::new();
249 let level = if std::io::stdin().read_line(&mut level_input).is_ok() {
250 match level_input.trim().to_lowercase().as_str() {
251 "lite" => "lite",
252 "standard" | "std" => "standard",
253 "max" => "max",
254 _ => "off",
255 }
256 } else {
257 "off"
258 };
259
260 let (effective_level, compression_status) = if level != "off" {
263 upsert_toml_key(&mut config_content, "compression_level", level);
264 remove_toml_key(&mut config_content, "terse_agent");
265 remove_toml_key(&mut config_content, "output_density");
266 (
267 crate::core::config::CompressionLevel::from_str_label(level),
268 StatusLine::ok(format!("Compression: {level}")),
269 )
270 } else if config_content.contains("compression_level") {
271 upsert_toml_key(&mut config_content, "compression_level", "off");
272 (
273 Some(crate::core::config::CompressionLevel::Off),
274 StatusLine::ok("Compression: off".to_string()),
275 )
276 } else {
277 (
278 Some(crate::core::config::CompressionLevel::Off),
279 StatusLine::skip(
280 "Compression: off (change later with: lean-ctx compression <level>)".to_string(),
281 ),
282 )
283 };
284
285 println!(
287 "\n {bold}Tool Result Archive{rst} {dim}(zero-loss: large outputs archived, retrievable via ctx_expand){rst}"
288 );
289 print!(" Enable auto-archive? {bold}[Y/n]{rst} ");
290 std::io::stdout().flush().ok();
291
292 let mut archive_input = String::new();
293 let archive_on = if std::io::stdin().read_line(&mut archive_input).is_ok() {
294 let a = archive_input.trim().to_lowercase();
295 a.is_empty() || a == "y" || a == "yes"
296 } else {
297 true
298 };
299
300 let archive_status = if archive_on && !config_content.contains("[archive]") {
301 if !config_content.is_empty() && !config_content.ends_with('\n') {
302 config_content.push('\n');
303 }
304 config_content.push_str("\n[archive]\nenabled = true\n");
305 Some(StatusLine::ok("Tool Result Archive: enabled".to_string()))
306 } else if !archive_on {
307 Some(StatusLine::skip(
308 "Archive: off (enable later in config.toml)".to_string(),
309 ))
310 } else {
311 None
312 };
313
314 match crate::config_io::write_atomic_with_backup(&config_path, &config_content) {
318 Ok(()) => {
319 compression_status.emit();
320 if let Some(lvl) = effective_level {
321 let n = crate::core::terse::rules_inject::inject(&lvl);
322 if n > 0 {
323 terminal_ui::print_status_ok(&format!(
324 "Updated {n} rules file(s) with compression prompt"
325 ));
326 }
327 }
328 if let Some(status) = archive_status {
329 status.emit();
330 }
331 }
332 Err(e) => {
333 terminal_ui::print_status_warn(&format!(
334 "Could not save settings to {}: {e}",
335 config_path.display()
336 ));
337 terminal_ui::print_status_warn(
338 "Premium features were not applied — re-run `lean-ctx setup` or edit config.toml manually",
339 );
340 }
341 }
342}
343
344struct StatusLine {
347 skip: bool,
348 msg: String,
349}
350
351impl StatusLine {
352 fn ok(msg: String) -> Self {
353 Self { skip: false, msg }
354 }
355 fn skip(msg: String) -> Self {
356 Self { skip: true, msg }
357 }
358 fn emit(&self) {
359 if self.skip {
360 crate::terminal_ui::print_status_skip(&self.msg);
361 } else {
362 crate::terminal_ui::print_status_ok(&self.msg);
363 }
364 }
365}