1use std::path::Path;
2
3use crate::core::compressor;
4use crate::core::deps as dep_extract;
5use crate::core::entropy;
6use crate::core::io_boundary;
7use crate::core::patterns::deps_cmd;
8use crate::core::protocol;
9use crate::core::roles;
10use crate::core::signatures;
11
12fn resolve_cli_path(raw: &str) -> String {
13 if let Ok(abs) = std::path::Path::new(raw).canonicalize() {
14 return abs.to_string_lossy().to_string();
15 }
16 if Path::new(raw).is_relative() {
17 if let Ok(cwd) = std::env::current_dir() {
18 return cwd.join(raw).to_string_lossy().into_owned();
19 }
20 }
21 raw.to_string()
22}
23use crate::core::tokens::count_tokens;
24
25use super::common::print_savings;
26
27pub fn cmd_read(args: &[String]) {
28 if args.is_empty() {
29 eprintln!(
30 "Usage: lean-ctx read <file> [--mode auto|full|map|signatures|aggressive|entropy] [--fresh]"
31 );
32 std::process::exit(1);
33 }
34
35 let raw_path = &args[0];
36 let path = if Path::new(raw_path).is_relative() {
37 std::env::current_dir().ok().map_or_else(
38 || raw_path.clone(),
39 |cwd| cwd.join(raw_path).to_string_lossy().into_owned(),
40 )
41 } else {
42 raw_path.clone()
43 };
44 let path = path.as_str();
45 let mode = args
46 .iter()
47 .position(|a| a == "--mode" || a == "-m")
48 .and_then(|i| args.get(i + 1))
49 .map_or("auto", std::string::String::as_str);
50 let force_fresh = args.iter().any(|a| a == "--fresh" || a == "--no-cache");
51
52 let short = protocol::shorten_path(path);
53
54 if let Ok(abs) = std::fs::canonicalize(path) {
57 match io_boundary::check_secret_path_for_tool("cli_read", &abs) {
58 Ok(Some(w)) => eprintln!("{w}"),
59 Ok(None) => {}
60 Err(e) => {
61 eprintln!("{e}");
62 std::process::exit(1);
63 }
64 }
65 } else {
66 let raw = std::path::Path::new(path);
68 match io_boundary::check_secret_path_for_tool("cli_read", raw) {
69 Ok(Some(w)) => eprintln!("{w}"),
70 Ok(None) => {}
71 Err(e) => {
72 eprintln!("{e}");
73 std::process::exit(1);
74 }
75 }
76 }
77
78 #[cfg(unix)]
79 {
80 #[cfg(unix)]
81 if let Some(out) = crate::daemon_client::try_daemon_tool_call_blocking_text(
82 "ctx_read",
83 Some(serde_json::json!({
84 "path": path,
85 "mode": mode,
86 "fresh": force_fresh,
87 })),
88 ) {
89 println!("{out}");
90 return;
91 }
92 }
93 super::common::daemon_fallback_hint();
94
95 if !force_fresh && mode == "full" {
96 use crate::core::cli_cache::{self, CacheResult};
97 match cli_cache::check_and_read(path) {
98 CacheResult::Hit { entry, file_ref } => {
99 let msg = cli_cache::format_hit(&entry, &file_ref, &short);
100 println!("{msg}");
101 let sent = count_tokens(&msg);
102 super::common::cli_track_read_cached(path, "full", entry.original_tokens, sent);
103 return;
104 }
105 CacheResult::Miss { content } if content.is_empty() => {
106 eprintln!("Error: could not read {path}");
107 std::process::exit(1);
108 }
109 CacheResult::Miss { content } => {
110 let line_count = content.lines().count();
111 println!("{short} [{line_count}L]");
112 println!("{content}");
113 let tok = count_tokens(&content);
114 super::common::cli_track_read(path, "full", tok, tok);
115 return;
116 }
117 }
118 }
119
120 let content = match crate::tools::ctx_read::read_file_lossy(path) {
121 Ok(c) => c,
122 Err(e) => {
123 eprintln!("Error: {e}");
124 std::process::exit(1);
125 }
126 };
127
128 let ext = Path::new(path)
129 .extension()
130 .and_then(|e| e.to_str())
131 .unwrap_or("");
132 let line_count = content.lines().count();
133 let original_tokens = count_tokens(&content);
134
135 let mode = if mode == "auto" {
136 if crate::tools::ctx_read::is_instruction_file(path) {
137 "full".to_string()
138 } else {
139 let sig = crate::core::mode_predictor::FileSignature::from_path(path, original_tokens);
140 let predictor = crate::core::mode_predictor::ModePredictor::new();
141 predictor
142 .predict_best_mode(&sig)
143 .unwrap_or_else(|| "full".to_string())
144 }
145 } else if mode != "full" && crate::tools::ctx_read::is_instruction_file(path) {
146 "full".to_string()
147 } else {
148 mode.to_string()
149 };
150 let mode = mode.as_str();
151
152 match mode {
153 "map" => {
154 let sigs = signatures::extract_signatures(&content, ext);
155 let dep_info = dep_extract::extract_deps(&content, ext);
156
157 let mut output_buf = format!("{short} [{line_count}L]");
158 if !dep_info.imports.is_empty() {
159 output_buf.push_str(&format!("\n deps: {}", dep_info.imports.join(", ")));
160 }
161 if !dep_info.exports.is_empty() {
162 output_buf.push_str(&format!("\n exports: {}", dep_info.exports.join(", ")));
163 }
164 let key_sigs: Vec<_> = sigs
165 .iter()
166 .filter(|s| s.is_exported || s.indent == 0)
167 .collect();
168 if !key_sigs.is_empty() {
169 output_buf.push_str("\n API:");
170 for sig in &key_sigs {
171 output_buf.push_str(&format!("\n {}", sig.to_compact()));
172 }
173 }
174 println!("{output_buf}");
175 let sent = count_tokens(&output_buf);
176 print_savings(original_tokens, sent);
177 super::common::cli_track_read(path, "map", original_tokens, sent);
178 }
179 "signatures" => {
180 let sigs = signatures::extract_signatures(&content, ext);
181 let mut output_buf = format!("{short} [{line_count}L]");
182 for sig in &sigs {
183 output_buf.push_str(&format!("\n{}", sig.to_compact()));
184 }
185 println!("{output_buf}");
186 let sent = count_tokens(&output_buf);
187 print_savings(original_tokens, sent);
188 super::common::cli_track_read(path, "signatures", original_tokens, sent);
189 }
190 "aggressive" => {
191 let compressed = compressor::aggressive_compress(&content, Some(ext));
192 println!("{short} [{line_count}L]");
193 println!("{compressed}");
194 let sent = count_tokens(&compressed);
195 print_savings(original_tokens, sent);
196 super::common::cli_track_read(path, "aggressive", original_tokens, sent);
197 }
198 "entropy" => {
199 let result = entropy::entropy_compress(&content);
200 let avg_h = entropy::analyze_entropy(&content).avg_entropy;
201 println!("{short} [{line_count}L] (H̄={avg_h:.1})");
202 for tech in &result.techniques {
203 println!("{tech}");
204 }
205 println!("{}", result.output);
206 let sent = count_tokens(&result.output);
207 print_savings(original_tokens, sent);
208 super::common::cli_track_read(path, "entropy", original_tokens, sent);
209 }
210 _ => {
211 let mut output = format!("{short} [{line_count}L]\n{content}");
212 let config = crate::core::config::Config::load();
213 let level = crate::core::config::CompressionLevel::effective(&config);
214 if level.is_active() {
215 let terse_result = crate::core::terse::pipeline::compress(&output, &level, None);
216 if terse_result.quality_passed && terse_result.savings_pct >= 1.0 {
217 output = terse_result.output;
218 }
219 }
220 println!("{output}");
221 let sent = count_tokens(&output);
222 super::common::cli_track_read(path, "full", original_tokens, sent);
223 }
224 }
225}
226
227pub fn cmd_diff(args: &[String]) {
228 if args.len() < 2 {
229 eprintln!("Usage: lean-ctx diff <file1> <file2>");
230 std::process::exit(1);
231 }
232
233 let content1 = match crate::tools::ctx_read::read_file_lossy(&args[0]) {
234 Ok(c) => c,
235 Err(e) => {
236 eprintln!("Error reading {}: {e}", args[0]);
237 std::process::exit(1);
238 }
239 };
240
241 let content2 = match crate::tools::ctx_read::read_file_lossy(&args[1]) {
242 Ok(c) => c,
243 Err(e) => {
244 eprintln!("Error reading {}: {e}", args[1]);
245 std::process::exit(1);
246 }
247 };
248
249 let diff = compressor::diff_content(&content1, &content2);
250 let original = count_tokens(&content1) + count_tokens(&content2);
251 let sent = count_tokens(&diff);
252
253 println!(
254 "diff {} {}",
255 protocol::shorten_path(&args[0]),
256 protocol::shorten_path(&args[1])
257 );
258 println!("{diff}");
259 print_savings(original, sent);
260 crate::core::stats::record("cli_diff", original, sent);
261}
262
263pub fn cmd_grep(args: &[String]) {
264 if args.is_empty() {
265 eprintln!("Usage: lean-ctx grep <pattern> [path]");
266 std::process::exit(1);
267 }
268
269 let pattern = &args[0];
270 let raw_path = args.get(1).map_or(".", std::string::String::as_str);
271 let abs_path = resolve_cli_path(raw_path);
272 let path = abs_path.as_str();
273
274 #[cfg(unix)]
275 {
276 #[cfg(unix)]
277 if let Some(out) = crate::daemon_client::try_daemon_tool_call_blocking_text(
278 "ctx_search",
279 Some(serde_json::json!({
280 "pattern": pattern,
281 "path": path,
282 })),
283 ) {
284 println!("{out}");
285 if out.trim_start().starts_with("0 matches") {
286 std::process::exit(1);
287 }
288 return;
289 }
290 }
291 super::common::daemon_fallback_hint();
292
293 let (out, original) = crate::tools::ctx_search::handle(
294 pattern,
295 path,
296 None,
297 20,
298 crate::tools::CrpMode::effective(),
299 true,
300 roles::active_role().io.allow_secret_paths,
301 );
302 println!("{out}");
303 super::common::cli_track_search(original, count_tokens(&out));
304 if original == 0 && out.trim_start().starts_with("0 matches") {
305 std::process::exit(1);
306 }
307}
308
309pub fn cmd_find(args: &[String]) {
310 if args.is_empty() {
311 eprintln!("Usage: lean-ctx find <pattern> [path]");
312 std::process::exit(1);
313 }
314
315 let raw_pattern = &args[0];
316 let path = args.get(1).map_or(".", std::string::String::as_str);
317
318 let is_glob = raw_pattern.contains('*') || raw_pattern.contains('?');
319 let glob_matcher = if is_glob {
320 glob::Pattern::new(&raw_pattern.to_lowercase()).ok()
321 } else {
322 None
323 };
324 let substring = raw_pattern.to_lowercase();
325
326 let mut found = false;
327 for entry in ignore::WalkBuilder::new(path)
328 .hidden(true)
329 .git_ignore(true)
330 .git_global(true)
331 .git_exclude(true)
332 .max_depth(Some(10))
333 .build()
334 .flatten()
335 {
336 let name = entry.file_name().to_string_lossy().to_lowercase();
337 let matches = if let Some(ref g) = glob_matcher {
338 g.matches(&name)
339 } else {
340 name.contains(&substring)
341 };
342 if matches {
343 println!("{}", entry.path().display());
344 found = true;
345 }
346 }
347
348 crate::core::stats::record("cli_find", 0, 0);
349
350 if !found {
351 std::process::exit(1);
352 }
353}
354
355pub fn cmd_ls(args: &[String]) {
356 let raw_path = args.first().map_or(".", std::string::String::as_str);
357 let abs_path = resolve_cli_path(raw_path);
358 let path = abs_path.as_str();
359 let depth = 3usize;
360 let show_hidden = false;
361
362 #[cfg(unix)]
363 {
364 #[cfg(unix)]
365 if let Some(out) = crate::daemon_client::try_daemon_tool_call_blocking_text(
366 "ctx_tree",
367 Some(serde_json::json!({
368 "path": path,
369 "depth": depth,
370 "show_hidden": show_hidden,
371 })),
372 ) {
373 println!("{out}");
374 return;
375 }
376 }
377 super::common::daemon_fallback_hint();
378
379 let (out, _original) = crate::tools::ctx_tree::handle(path, depth, show_hidden);
380 println!("{out}");
381 super::common::cli_track_tree(0, count_tokens(&out));
382}
383
384pub fn cmd_deps(args: &[String]) {
385 let path = args.first().map_or(".", std::string::String::as_str);
386
387 if let Some(result) = deps_cmd::detect_and_compress(path) {
388 println!("{result}");
389 crate::core::stats::record("cli_deps", 0, 0);
390 } else {
391 eprintln!("No dependency file found in {path}");
392 std::process::exit(1);
393 }
394}