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