Skip to main content

lean_ctx/cli/
profile_cmd.rs

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        // Tool profile subcommands
9        "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        // Existing compression profile subcommands
16        "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
236// ─── Tool Profile Commands ───────────────────────────────────────────────
237
238fn 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        6 essential tools
364  lean-ctx tools standard       21 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}