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