1use crate::core::profiles;
2use crate::core::tool_profiles::{self, ToolProfile};
3
4pub fn cmd_profile(args: &[String]) {
5 let action = args.first().map_or("list", String::as_str);
6
7 match action {
8 "tools" => cmd_tool_profile(&args[1..]),
9 "minimal" | "min" | "standard" | "std" | "power" | "full" | "all" | "lean" | "lazy"
10 | "reset" => {
11 cmd_tool_profile_switch(action);
12 println!(" \x1b[2mTip: the canonical command is `lean-ctx tools {action}`.\x1b[0m");
13 }
14
15 "list" | "ls" => cmd_profile_list(),
16 "show" => {
17 let name = args
18 .get(1)
19 .map_or_else(profiles::active_profile_name, Clone::clone);
20 cmd_profile_show(&name);
21 }
22 "active" | "current" => cmd_profile_active(),
23 "diff" => {
24 if args.len() < 3 {
25 eprintln!("Usage: lean-ctx profile diff <profile-a> <profile-b>");
26 std::process::exit(1);
27 }
28 cmd_profile_diff(&args[1], &args[2]);
29 }
30 "create" => {
31 if args.len() < 2 {
32 eprintln!("Usage: lean-ctx profile create <name> [--from <base>] [--global]");
33 std::process::exit(1);
34 }
35 let name = &args[1];
36 let base = args
37 .iter()
38 .position(|a| a == "--from")
39 .and_then(|i| args.get(i + 1))
40 .map(String::as_str);
41 let global = args.iter().any(|a| a == "--global");
42 cmd_profile_create(name, base, global);
43 }
44 "set" => {
45 if args.len() < 2 {
46 eprintln!("Usage: lean-ctx profile set <name>");
47 eprintln!(" Sets LEAN_CTX_PROFILE for the current shell.");
48 std::process::exit(1);
49 }
50 cmd_profile_set(&args[1]);
51 }
52 "suggest" => cmd_profile_suggest(&args[1..]),
53 _ => {
54 if profiles::load_profile(action).is_some() {
55 cmd_profile_show(action);
56 } else {
57 print_profile_help();
58 std::process::exit(1);
59 }
60 }
61 }
62}
63
64fn cmd_profile_suggest(args: &[String]) {
69 use crate::core::profile_suggest;
70
71 let root = super::common::detect_project_root(args);
72 let signals = profile_suggest::analyze(&root);
73 let suggestion = profile_suggest::suggest(&signals);
74
75 if args.iter().any(|a| a == "--json") {
76 let payload = serde_json::json!({ "signals": signals, "suggestion": suggestion });
77 println!(
78 "{}",
79 serde_json::to_string_pretty(&payload).unwrap_or_else(|_| "{}".to_string())
80 );
81 return;
82 }
83
84 render_profile_suggestion(&signals, &suggestion);
85}
86
87fn render_profile_suggestion(
88 signals: &crate::core::profile_suggest::RepoSignals,
89 suggestion: &crate::core::profile_suggest::Suggestion,
90) {
91 const BOLD: &str = "\x1b[1m";
92 const DIM: &str = "\x1b[2m";
93 const CYAN: &str = "\x1b[36m";
94 const RST: &str = "\x1b[0m";
95
96 println!(
97 "{BOLD}Profile suggestion{RST} {DIM}for {}{RST}",
98 signals.root
99 );
100 println!();
101
102 let langs = if signals.languages.is_empty() {
103 "(none detected)".to_string()
104 } else {
105 signals
106 .languages
107 .iter()
108 .take(6)
109 .map(|l| format!("{} {}", l.language, l.files))
110 .collect::<Vec<_>>()
111 .join(", ")
112 };
113 println!(" {BOLD}Detected{RST}");
114 println!(" languages {langs}");
115 println!(" files {} source files", signals.source_files);
116 println!(
117 " monorepo {}",
118 if signals.monorepo {
119 let m = if signals.workspace_markers.is_empty() {
120 "nested project roots".to_string()
121 } else {
122 signals.workspace_markers.join(", ")
123 };
124 format!("yes ({m})")
125 } else {
126 "no".to_string()
127 }
128 );
129 if !signals.build_markers.is_empty() {
130 println!(" build {}", signals.build_markers.join(", "));
131 }
132 println!(" CI {}", if signals.ci { "yes" } else { "no" });
133 let providers = if signals.providers.is_empty() {
134 "(none detected)".to_string()
135 } else {
136 signals.providers.join(", ")
137 };
138 println!(" providers {providers}");
139 println!();
140
141 println!(
142 " {BOLD}Suggested profile:{RST} {CYAN}{}{RST}",
143 suggestion.profile
144 );
145 for reason in &suggestion.rationale {
146 println!(" • {reason}");
147 }
148 println!();
149
150 println!(" {BOLD}Recommended settings{RST}");
151 println!(" profile {}", suggestion.profile);
152 if let Some(hm) = &suggestion.settings.history_mode {
153 println!(" proxy.history_mode {hm}");
154 }
155 println!(
156 " output_density {}",
157 suggestion.settings.output_density
158 );
159 match &suggestion.settings.effort {
160 Some(e) => println!(" proxy.effort {e}"),
161 None => println!(" proxy.effort {DIM}off (opt-in; raise per task){RST}"),
162 }
163 println!();
164
165 println!(" {BOLD}Apply{RST} {DIM}(you choose — nothing is changed automatically){RST}");
166 println!(
167 " {DIM}# session-only:{RST} export LEAN_CTX_PROFILE={}",
168 suggestion.profile
169 );
170 println!(
171 " {DIM}# persistent: {RST} lean-ctx config set profile {}",
172 suggestion.profile
173 );
174 if let Some(hm) = &suggestion.settings.history_mode {
175 println!(" lean-ctx config set proxy.history_mode {hm}");
176 }
177 println!(
178 " lean-ctx config set output_density {}",
179 suggestion.settings.output_density
180 );
181 println!();
182
183 if !suggestion.alternatives.is_empty() {
184 println!(" {BOLD}Task profiles you can switch to{RST}");
185 for alt in &suggestion.alternatives {
186 println!(" {:<9} {DIM}— {}{RST}", alt.profile, alt.when);
187 }
188 }
189}
190
191fn cmd_profile_list() {
192 let list = profiles::list_profiles();
193 let active = profiles::active_profile_name();
194
195 let header = format!(" {:<16} {:<10} {}", "Name", "Source", "Description");
196 let sep = format!(" {}", "\u{2500}".repeat(60));
197 println!("Available profiles:\n");
198 println!("{header}");
199 println!("{sep}");
200
201 for p in &list {
202 let marker = if p.name == active { " *" } else { " " };
203 println!("{marker}{:<16} {:<10} {}", p.name, p.source, p.description);
204 }
205
206 println!("\n Active: {active}");
207 println!(" Set via: LEAN_CTX_PROFILE=<name> or lean-ctx profile set <name>");
208}
209
210fn cmd_profile_show(name: &str) {
211 if let Some(profile) = profiles::load_profile(name) {
212 println!("Profile: {name}\n");
213 println!("{}", profiles::format_as_toml(&profile));
214 } else {
215 eprintln!("Profile '{name}' not found.");
216 eprintln!("Run 'lean-ctx profile list' to see available profiles.");
217 std::process::exit(1);
218 }
219}
220
221fn cmd_profile_active() {
222 let name = profiles::active_profile_name();
223 let profile = profiles::active_profile();
224 println!("Active profile: {name}\n");
225 println!("{}", profiles::format_as_toml(&profile));
226}
227
228fn cmd_profile_diff(name_a: &str, name_b: &str) {
229 let Some(a) = profiles::load_profile(name_a) else {
230 eprintln!("Profile '{name_a}' not found.");
231 std::process::exit(1);
232 };
233 let Some(b) = profiles::load_profile(name_b) else {
234 eprintln!("Profile '{name_b}' not found.");
235 std::process::exit(1);
236 };
237
238 println!("Profile diff: {name_a} vs {name_b}\n");
239
240 let diffs = collect_diffs(&a, &b);
241 if diffs.is_empty() {
242 println!(" No differences.");
243 } else {
244 println!(" {:<32} {:<20} {:<20}", "Field", name_a, name_b);
245 println!(" {}", "\u{2500}".repeat(72));
246 for (field, val_a, val_b) in &diffs {
247 println!(" {field:<32} {val_a:<20} {val_b:<20}");
248 }
249 }
250}
251
252fn collect_diffs(a: &profiles::Profile, b: &profiles::Profile) -> Vec<(String, String, String)> {
253 let mut diffs = Vec::new();
254
255 macro_rules! cmp {
256 ($section:ident . $field:ident) => {
257 let va = format!("{:?}", a.$section.$field);
258 let vb = format!("{:?}", b.$section.$field);
259 if va != vb {
260 diffs.push((
261 format!("{}.{}", stringify!($section), stringify!($field)),
262 va,
263 vb,
264 ));
265 }
266 };
267 }
268
269 cmp!(read.default_mode);
270 cmp!(read.max_tokens_per_file);
271 cmp!(read.prefer_cache);
272 cmp!(compression.crp_mode);
273 cmp!(compression.output_density);
274 cmp!(compression.entropy_threshold);
275 cmp!(translation.enabled);
276 cmp!(translation.ruleset);
277 cmp!(layout.enabled);
278 cmp!(layout.min_lines);
279 cmp!(budget.max_context_tokens);
280 cmp!(budget.max_shell_invocations);
281 cmp!(budget.max_cost_usd);
282 cmp!(pipeline.intent);
283 cmp!(pipeline.relevance);
284 cmp!(pipeline.compression);
285 cmp!(pipeline.translation);
286 cmp!(autonomy.enabled);
287 cmp!(autonomy.auto_preload);
288 cmp!(autonomy.auto_dedup);
289 cmp!(autonomy.auto_related);
290 cmp!(autonomy.silent_preload);
291 cmp!(autonomy.auto_prefetch);
292 cmp!(autonomy.auto_response);
293 cmp!(autonomy.dedup_threshold);
294 cmp!(autonomy.prefetch_max_files);
295 cmp!(autonomy.prefetch_budget_tokens);
296 cmp!(autonomy.response_min_tokens);
297 cmp!(autonomy.checkpoint_interval);
298
299 diffs
300}
301
302fn cmd_profile_create(name: &str, base: Option<&str>, global: bool) {
303 let base_profile = base
304 .and_then(profiles::load_profile)
305 .unwrap_or_else(profiles::active_profile);
306
307 let mut new_profile = base_profile;
308 new_profile.profile.name = name.to_string();
309 new_profile.profile.inherits = base.map(String::from);
310 new_profile.profile.description = String::new();
311
312 let dir = if global {
313 let Ok(data_dir) = crate::core::data_dir::lean_ctx_data_dir() else {
314 eprintln!("Cannot determine global data directory.");
315 std::process::exit(1);
316 };
317 data_dir.join("profiles")
318 } else {
319 std::env::current_dir()
320 .unwrap_or_default()
321 .join(".lean-ctx")
322 .join("profiles")
323 };
324
325 if let Err(e) = std::fs::create_dir_all(&dir) {
326 eprintln!("Cannot create directory {}: {e}", dir.display());
327 std::process::exit(1);
328 }
329
330 let path = dir.join(format!("{name}.toml"));
331 let toml_content = profiles::format_as_toml(&new_profile);
332
333 if let Err(e) = std::fs::write(&path, &toml_content) {
334 eprintln!("Error writing {}: {e}", path.display());
335 std::process::exit(1);
336 }
337
338 println!("Created profile '{name}' at {}", path.display());
339 if let Some(b) = base {
340 println!(" Based on: {b}");
341 }
342 println!("\nEdit the file to customize, then activate with:");
343 println!(" LEAN_CTX_PROFILE={name}");
344}
345
346fn cmd_profile_set(name: &str) {
347 if profiles::load_profile(name).is_none() {
348 eprintln!("Profile '{name}' not found. Available profiles:");
349 for p in profiles::list_profiles() {
350 eprintln!(" {}", p.name);
351 }
352 std::process::exit(1);
353 }
354
355 println!("To activate profile '{name}', run:\n");
356 println!(" export LEAN_CTX_PROFILE={name}\n");
357 println!(
358 "Or add it to your shell config ({}).",
359 crate::shell_hook::shell_rc_file()
360 );
361}
362
363fn cmd_tool_profile(args: &[String]) {
366 let action = args.first().map_or("show", String::as_str);
367
368 match action {
369 "list" | "ls" => cmd_tool_profile_list(),
370 "show" | "current" => cmd_tool_profile_show(),
371 "minimal" | "min" | "standard" | "std" | "power" | "full" | "all" | "lean" | "lazy"
372 | "reset" => {
373 cmd_tool_profile_switch(action);
374 }
375 _ => {
376 if ToolProfile::parse(action).is_some() {
377 cmd_tool_profile_switch(action);
378 } else {
379 eprintln!("Unknown tool profile '{action}'.");
380 eprintln!("Available: lean (default), minimal, standard, power");
381 std::process::exit(1);
382 }
383 }
384 }
385}
386
387fn cmd_tool_profile_show() {
388 let cfg = crate::core::config::Config::load();
389 let profile = cfg.tool_profile_effective();
390 let registry_count = crate::server::registry::tool_count();
391 let pinned = cfg.tool_profile.is_some()
392 || std::env::var("LEAN_CTX_TOOL_PROFILE").is_ok()
393 || !cfg.tools_enabled.is_empty();
394
395 if !pinned {
396 let lazy_count = crate::tool_defs::core_tool_names().len();
397 println!("Tool Profile: lean (default)");
398 println!(" Tools advertised: {lazy_count} (lazy core)");
399 println!(" All {registry_count} registered tools stay callable via ctx_call.");
400 println!("\n Advertised tools:");
401 for name in crate::tool_defs::core_tool_names() {
402 println!(" {name}");
403 }
404 println!("\n Switch with: lean-ctx tools <minimal|standard|power>");
405 return;
406 }
407
408 let count_str = match &profile {
409 ToolProfile::Power => format!("{registry_count}"),
410 ToolProfile::Custom(list) => format!("{}", list.len()),
411 other => format!("{}", other.tool_count()),
412 };
413
414 println!("Tool Profile: {}", profile.as_str());
415 println!(" Tools exposed: {count_str}");
416 println!(" Description: {}", profile.description());
417
418 if let Some(ref cfg_val) = cfg.tool_profile {
419 println!(" Source: config.toml (tool_profile = \"{cfg_val}\")");
420 }
421 if std::env::var("LEAN_CTX_TOOL_PROFILE").is_ok() {
422 println!(" Source: LEAN_CTX_TOOL_PROFILE env var (overrides config)");
423 }
424
425 if !matches!(profile, ToolProfile::Power) {
426 println!("\n Enabled tools:");
427 let names = profile.tool_names();
428 for name in &names {
429 println!(" {name}");
430 }
431 }
432
433 println!("\n Switch with: lean-ctx tools <lean|minimal|standard|power>");
434 if matches!(profile, ToolProfile::Power) {
435 println!(" Tip: `lean-ctx tools lean` advertises only the lazy core (lowest overhead).");
436 }
437}
438
439fn cmd_tool_profile_list() {
440 let cfg = crate::core::config::Config::load();
441 let active = cfg.tool_profile_effective();
442 let registry_count = crate::server::registry::tool_count();
443 let pinned = cfg
446 .tool_profile
447 .as_deref()
448 .is_some_and(|p| !tool_profiles::is_unpinned_alias(p))
449 || std::env::var("LEAN_CTX_TOOL_PROFILE")
450 .is_ok_and(|v| !v.trim().is_empty() && !tool_profiles::is_unpinned_alias(v.trim()))
451 || !cfg.tools_enabled.is_empty();
452 let active_name = if pinned { active.as_str() } else { "lean" };
453 let lazy_count = crate::tool_defs::core_tool_names().len();
454
455 println!("Tool Profiles:\n");
456 println!(" {:<12} {:<8} Description", "Name", "Tools");
457 println!(" {}", "\u{2500}".repeat(60));
458
459 let lean_marker = if active_name == "lean" { "* " } else { " " };
460 println!(
461 "{lean_marker}{:<12} {lazy_count:<8} Lazy core advertised, all tools via ctx_call (default)",
462 "lean"
463 );
464 for info in tool_profiles::list_profiles() {
465 let marker = if info.name == active_name { "* " } else { " " };
466 let count = if info.name == "power" {
467 format!("{registry_count}")
468 } else {
469 info.tool_count.to_string()
470 };
471 println!(
472 "{marker}{:<12} {:<8} {}",
473 info.name, count, info.description
474 );
475 }
476
477 println!("\n Active: {active_name}");
478 println!(" Switch: lean-ctx profile <name>");
479 println!(" Env: LEAN_CTX_TOOL_PROFILE=<name>");
480}
481
482fn cmd_tool_profile_switch(name: &str) {
483 if tool_profiles::is_unpinned_alias(name) {
487 if let Err(e) = tool_profiles::clear_profile_in_config() {
488 eprintln!("Error saving profile: {e}");
489 std::process::exit(1);
490 }
491 let lazy_count = crate::tool_defs::core_tool_names().len();
492 println!("Tool profile set to: lean (default)");
493 println!(" Tools advertised: {lazy_count} (lazy core)");
494 println!(" All other tools stay callable via ctx_call.");
495 println!("\n Restart your AI tool / IDE for changes to take effect.");
496 return;
497 }
498
499 let Some(profile) = ToolProfile::parse(name) else {
500 eprintln!("Unknown tool profile '{name}'.");
501 eprintln!("Available: lean (default), minimal, standard, power");
502 std::process::exit(1);
503 };
504
505 let canonical = profile.as_str();
506
507 if let Err(e) = tool_profiles::set_profile_in_config(canonical) {
508 eprintln!("Error saving profile: {e}");
509 std::process::exit(1);
510 }
511
512 let registry_count = crate::server::registry::tool_count();
513 let count_str = match &profile {
514 ToolProfile::Power => format!("{registry_count}"),
515 other => format!("{}", other.tool_count()),
516 };
517
518 println!("Tool profile set to: {canonical}");
519 println!(" Tools exposed: {count_str}");
520 println!(" Description: {}", profile.description());
521
522 if !matches!(profile, ToolProfile::Power) {
523 println!("\n Enabled tools:");
524 for name in profile.tool_names() {
525 println!(" {name}");
526 }
527 }
528
529 println!("\n Restart your AI tool / IDE for changes to take effect.");
530}
531
532fn print_profile_help() {
533 eprintln!(
534 "lean-ctx has two kinds of profiles — here is which command to use:
535
536TOOL PROFILES — how many MCP tools your agent sees:
537 lean-ctx tools Show current tool profile
538 lean-ctx tools lean Lazy core advertised, all via ctx_call (default)
539 lean-ctx tools minimal 5 essential tools
540 lean-ctx tools standard 16 balanced tools
541 lean-ctx tools power All tools (highest context overhead)
542 lean-ctx tools list List tool profiles with counts
543
544CONTEXT PROFILES — how lean-ctx compresses and reads (this command):
545 lean-ctx profile list List available context profiles
546 lean-ctx profile show [name] Show context profile details (default: active)
547 lean-ctx profile active Show the currently active context profile
548 lean-ctx profile diff <a> <b> Compare two context profiles side by side
549 lean-ctx profile create <name> [--from <base>] [--global]
550 lean-ctx profile set <name> Show how to activate a context profile
551 lean-ctx profile suggest Recommend a profile from repo signals (read-only) [--json]"
552 );
553}