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