lean_ctx/cli/
profile_cmd.rs1use 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..]),
10 "minimal" | "min" | "standard" | "std" | "power" | "full" | "all" | "lean" | "lazy"
11 | "reset" => {
12 cmd_tool_profile_switch(action);
13 println!(" \x1b[2mTip: the canonical command is `lean-ctx tools {action}`.\x1b[0m");
14 }
15
16 "list" | "ls" => cmd_profile_list(),
18 "show" => {
19 let name = args
20 .get(1)
21 .map_or_else(profiles::active_profile_name, Clone::clone);
22 cmd_profile_show(&name);
23 }
24 "active" | "current" => cmd_profile_active(),
25 "diff" => {
26 if args.len() < 3 {
27 eprintln!("Usage: lean-ctx profile diff <profile-a> <profile-b>");
28 std::process::exit(1);
29 }
30 cmd_profile_diff(&args[1], &args[2]);
31 }
32 "create" => {
33 if args.len() < 2 {
34 eprintln!("Usage: lean-ctx profile create <name> [--from <base>] [--global]");
35 std::process::exit(1);
36 }
37 let name = &args[1];
38 let base = args
39 .iter()
40 .position(|a| a == "--from")
41 .and_then(|i| args.get(i + 1))
42 .map(String::as_str);
43 let global = args.iter().any(|a| a == "--global");
44 cmd_profile_create(name, base, global);
45 }
46 "set" => {
47 if args.len() < 2 {
48 eprintln!("Usage: lean-ctx profile set <name>");
49 eprintln!(" Sets LEAN_CTX_PROFILE for the current shell.");
50 std::process::exit(1);
51 }
52 cmd_profile_set(&args[1]);
53 }
54 _ => {
55 if profiles::load_profile(action).is_some() {
56 cmd_profile_show(action);
57 } else {
58 print_profile_help();
59 std::process::exit(1);
60 }
61 }
62 }
63}
64
65fn cmd_profile_list() {
66 let list = profiles::list_profiles();
67 let active = profiles::active_profile_name();
68
69 let header = format!(" {:<16} {:<10} {}", "Name", "Source", "Description");
70 let sep = format!(" {}", "\u{2500}".repeat(60));
71 println!("Available profiles:\n");
72 println!("{header}");
73 println!("{sep}");
74
75 for p in &list {
76 let marker = if p.name == active { " *" } else { " " };
77 println!("{marker}{:<16} {:<10} {}", p.name, p.source, p.description);
78 }
79
80 println!("\n Active: {active}");
81 println!(" Set via: LEAN_CTX_PROFILE=<name> or lean-ctx profile set <name>");
82}
83
84fn cmd_profile_show(name: &str) {
85 if let Some(profile) = profiles::load_profile(name) {
86 println!("Profile: {name}\n");
87 println!("{}", profiles::format_as_toml(&profile));
88 } else {
89 eprintln!("Profile '{name}' not found.");
90 eprintln!("Run 'lean-ctx profile list' to see available profiles.");
91 std::process::exit(1);
92 }
93}
94
95fn cmd_profile_active() {
96 let name = profiles::active_profile_name();
97 let profile = profiles::active_profile();
98 println!("Active profile: {name}\n");
99 println!("{}", profiles::format_as_toml(&profile));
100}
101
102fn cmd_profile_diff(name_a: &str, name_b: &str) {
103 let Some(a) = profiles::load_profile(name_a) else {
104 eprintln!("Profile '{name_a}' not found.");
105 std::process::exit(1);
106 };
107 let Some(b) = profiles::load_profile(name_b) else {
108 eprintln!("Profile '{name_b}' not found.");
109 std::process::exit(1);
110 };
111
112 println!("Profile diff: {name_a} vs {name_b}\n");
113
114 let diffs = collect_diffs(&a, &b);
115 if diffs.is_empty() {
116 println!(" No differences.");
117 } else {
118 println!(" {:<32} {:<20} {:<20}", "Field", name_a, name_b);
119 println!(" {}", "\u{2500}".repeat(72));
120 for (field, val_a, val_b) in &diffs {
121 println!(" {field:<32} {val_a:<20} {val_b:<20}");
122 }
123 }
124}
125
126fn collect_diffs(a: &profiles::Profile, b: &profiles::Profile) -> Vec<(String, String, String)> {
127 let mut diffs = Vec::new();
128
129 macro_rules! cmp {
130 ($section:ident . $field:ident) => {
131 let va = format!("{:?}", a.$section.$field);
132 let vb = format!("{:?}", b.$section.$field);
133 if va != vb {
134 diffs.push((
135 format!("{}.{}", stringify!($section), stringify!($field)),
136 va,
137 vb,
138 ));
139 }
140 };
141 }
142
143 cmp!(read.default_mode);
144 cmp!(read.max_tokens_per_file);
145 cmp!(read.prefer_cache);
146 cmp!(compression.crp_mode);
147 cmp!(compression.output_density);
148 cmp!(compression.entropy_threshold);
149 cmp!(translation.enabled);
150 cmp!(translation.ruleset);
151 cmp!(layout.enabled);
152 cmp!(layout.min_lines);
153 cmp!(budget.max_context_tokens);
154 cmp!(budget.max_shell_invocations);
155 cmp!(budget.max_cost_usd);
156 cmp!(pipeline.intent);
157 cmp!(pipeline.relevance);
158 cmp!(pipeline.compression);
159 cmp!(pipeline.translation);
160 cmp!(autonomy.enabled);
161 cmp!(autonomy.auto_preload);
162 cmp!(autonomy.auto_dedup);
163 cmp!(autonomy.auto_related);
164 cmp!(autonomy.silent_preload);
165 cmp!(autonomy.auto_prefetch);
166 cmp!(autonomy.auto_response);
167 cmp!(autonomy.dedup_threshold);
168 cmp!(autonomy.prefetch_max_files);
169 cmp!(autonomy.prefetch_budget_tokens);
170 cmp!(autonomy.response_min_tokens);
171 cmp!(autonomy.checkpoint_interval);
172
173 diffs
174}
175
176fn cmd_profile_create(name: &str, base: Option<&str>, global: bool) {
177 let base_profile = base
178 .and_then(profiles::load_profile)
179 .unwrap_or_else(profiles::active_profile);
180
181 let mut new_profile = base_profile;
182 new_profile.profile.name = name.to_string();
183 new_profile.profile.inherits = base.map(String::from);
184 new_profile.profile.description = String::new();
185
186 let dir = if global {
187 let Ok(data_dir) = crate::core::data_dir::lean_ctx_data_dir() else {
188 eprintln!("Cannot determine global data directory.");
189 std::process::exit(1);
190 };
191 data_dir.join("profiles")
192 } else {
193 std::env::current_dir()
194 .unwrap_or_default()
195 .join(".lean-ctx")
196 .join("profiles")
197 };
198
199 if let Err(e) = std::fs::create_dir_all(&dir) {
200 eprintln!("Cannot create directory {}: {e}", dir.display());
201 std::process::exit(1);
202 }
203
204 let path = dir.join(format!("{name}.toml"));
205 let toml_content = profiles::format_as_toml(&new_profile);
206
207 if let Err(e) = std::fs::write(&path, &toml_content) {
208 eprintln!("Error writing {}: {e}", path.display());
209 std::process::exit(1);
210 }
211
212 println!("Created profile '{name}' at {}", path.display());
213 if let Some(b) = base {
214 println!(" Based on: {b}");
215 }
216 println!("\nEdit the file to customize, then activate with:");
217 println!(" LEAN_CTX_PROFILE={name}");
218}
219
220fn cmd_profile_set(name: &str) {
221 if profiles::load_profile(name).is_none() {
222 eprintln!("Profile '{name}' not found. Available profiles:");
223 for p in profiles::list_profiles() {
224 eprintln!(" {}", p.name);
225 }
226 std::process::exit(1);
227 }
228
229 println!("To activate profile '{name}', run:\n");
230 println!(" export LEAN_CTX_PROFILE={name}\n");
231 println!(
232 "Or add it to your shell config ({}).",
233 crate::shell_hook::shell_rc_file()
234 );
235}
236
237fn cmd_tool_profile(args: &[String]) {
240 let action = args.first().map_or("show", String::as_str);
241
242 match action {
243 "list" | "ls" => cmd_tool_profile_list(),
244 "show" | "current" => cmd_tool_profile_show(),
245 "minimal" | "min" | "standard" | "std" | "power" | "full" | "all" | "lean" | "lazy"
246 | "reset" => {
247 cmd_tool_profile_switch(action);
248 }
249 _ => {
250 if ToolProfile::parse(action).is_some() {
251 cmd_tool_profile_switch(action);
252 } else {
253 eprintln!("Unknown tool profile '{action}'.");
254 eprintln!("Available: lean (default), minimal, standard, power");
255 std::process::exit(1);
256 }
257 }
258 }
259}
260
261fn cmd_tool_profile_show() {
262 let cfg = crate::core::config::Config::load();
263 let profile = cfg.tool_profile_effective();
264 let registry_count = crate::server::registry::tool_count();
265 let pinned = cfg.tool_profile.is_some()
266 || std::env::var("LEAN_CTX_TOOL_PROFILE").is_ok()
267 || !cfg.tools_enabled.is_empty();
268
269 if !pinned {
270 let lazy_count = crate::tool_defs::core_tool_names().len();
271 println!("Tool Profile: lean (default)");
272 println!(" Tools advertised: {lazy_count} (lazy core)");
273 println!(" All {registry_count} registered tools stay callable via ctx_call.");
274 println!("\n Advertised tools:");
275 for name in crate::tool_defs::core_tool_names() {
276 println!(" {name}");
277 }
278 println!("\n Switch with: lean-ctx tools <minimal|standard|power>");
279 return;
280 }
281
282 let count_str = match &profile {
283 ToolProfile::Power => format!("{registry_count}"),
284 ToolProfile::Custom(list) => format!("{}", list.len()),
285 other => format!("{}", other.tool_count()),
286 };
287
288 println!("Tool Profile: {}", profile.as_str());
289 println!(" Tools exposed: {count_str}");
290 println!(" Description: {}", profile.description());
291
292 if let Some(ref cfg_val) = cfg.tool_profile {
293 println!(" Source: config.toml (tool_profile = \"{cfg_val}\")");
294 }
295 if std::env::var("LEAN_CTX_TOOL_PROFILE").is_ok() {
296 println!(" Source: LEAN_CTX_TOOL_PROFILE env var (overrides config)");
297 }
298
299 if !matches!(profile, ToolProfile::Power) {
300 println!("\n Enabled tools:");
301 let names = profile.tool_names();
302 for name in &names {
303 println!(" {name}");
304 }
305 }
306
307 println!("\n Switch with: lean-ctx tools <lean|minimal|standard|power>");
308 if matches!(profile, ToolProfile::Power) {
309 println!(" Tip: `lean-ctx tools lean` advertises only the lazy core (lowest overhead).");
310 }
311}
312
313fn cmd_tool_profile_list() {
314 let cfg = crate::core::config::Config::load();
315 let active = cfg.tool_profile_effective();
316 let registry_count = crate::server::registry::tool_count();
317 let pinned = cfg.tool_profile.is_some()
318 || std::env::var("LEAN_CTX_TOOL_PROFILE").is_ok()
319 || !cfg.tools_enabled.is_empty();
320 let active_name = if pinned { active.as_str() } else { "lean" };
321 let lazy_count = crate::tool_defs::core_tool_names().len();
322
323 println!("Tool Profiles:\n");
324 println!(" {:<12} {:<8} Description", "Name", "Tools");
325 println!(" {}", "\u{2500}".repeat(60));
326
327 let lean_marker = if active_name == "lean" { "* " } else { " " };
328 println!(
329 "{lean_marker}{:<12} {lazy_count:<8} Lazy core advertised, all tools via ctx_call (default)",
330 "lean"
331 );
332 for info in tool_profiles::list_profiles() {
333 let marker = if info.name == active_name { "* " } else { " " };
334 let count = if info.name == "power" {
335 format!("{registry_count}")
336 } else {
337 info.tool_count.to_string()
338 };
339 println!(
340 "{marker}{:<12} {:<8} {}",
341 info.name, count, info.description
342 );
343 }
344
345 println!("\n Active: {active_name}");
346 println!(" Switch: lean-ctx profile <name>");
347 println!(" Env: LEAN_CTX_TOOL_PROFILE=<name>");
348}
349
350fn cmd_tool_profile_switch(name: &str) {
351 if matches!(name, "lean" | "lazy" | "reset") {
355 if let Err(e) = tool_profiles::clear_profile_in_config() {
356 eprintln!("Error saving profile: {e}");
357 std::process::exit(1);
358 }
359 let lazy_count = crate::tool_defs::core_tool_names().len();
360 println!("Tool profile set to: lean (default)");
361 println!(" Tools advertised: {lazy_count} (lazy core)");
362 println!(" All other tools stay callable via ctx_call.");
363 println!("\n Restart your AI tool / IDE for changes to take effect.");
364 return;
365 }
366
367 let Some(profile) = ToolProfile::parse(name) else {
368 eprintln!("Unknown tool profile '{name}'.");
369 eprintln!("Available: lean (default), minimal, standard, power");
370 std::process::exit(1);
371 };
372
373 let canonical = profile.as_str();
374
375 if let Err(e) = tool_profiles::set_profile_in_config(canonical) {
376 eprintln!("Error saving profile: {e}");
377 std::process::exit(1);
378 }
379
380 let registry_count = crate::server::registry::tool_count();
381 let count_str = match &profile {
382 ToolProfile::Power => format!("{registry_count}"),
383 other => format!("{}", other.tool_count()),
384 };
385
386 println!("Tool profile set to: {canonical}");
387 println!(" Tools exposed: {count_str}");
388 println!(" Description: {}", profile.description());
389
390 if !matches!(profile, ToolProfile::Power) {
391 println!("\n Enabled tools:");
392 for name in profile.tool_names() {
393 println!(" {name}");
394 }
395 }
396
397 println!("\n Restart your AI tool / IDE for changes to take effect.");
398}
399
400fn print_profile_help() {
401 eprintln!(
402 "lean-ctx has two kinds of profiles — here is which command to use:
403
404TOOL PROFILES — how many MCP tools your agent sees:
405 lean-ctx tools Show current tool profile
406 lean-ctx tools lean Lazy core advertised, all via ctx_call (default)
407 lean-ctx tools minimal 6 essential tools
408 lean-ctx tools standard 22 balanced tools
409 lean-ctx tools power All tools (highest context overhead)
410 lean-ctx tools list List tool profiles with counts
411
412CONTEXT PROFILES — how lean-ctx compresses and reads (this command):
413 lean-ctx profile list List available context profiles
414 lean-ctx profile show [name] Show context profile details (default: active)
415 lean-ctx profile active Show the currently active context profile
416 lean-ctx profile diff <a> <b> Compare two context profiles side by side
417 lean-ctx profile create <name> [--from <base>] [--global]
418 lean-ctx profile set <name> Show how to activate a context profile"
419 );
420}