use std::path::Path;
use crate::core::compressor;
use crate::core::deps as dep_extract;
use crate::core::entropy;
use crate::core::io_boundary;
use crate::core::patterns::deps_cmd;
use crate::core::protocol;
use crate::core::roles;
use crate::core::signatures;
use crate::core::stats;
use crate::core::tokens::count_tokens;
use super::common::print_savings;
pub fn cmd_read(args: &[String]) {
if args.is_empty() {
eprintln!(
"Usage: lean-ctx read <file> [--mode auto|full|map|signatures|aggressive|entropy] [--fresh]"
);
std::process::exit(1);
}
let raw_path = &args[0];
let path = if Path::new(raw_path).is_relative() {
std::env::current_dir().ok().map_or_else(
|| raw_path.clone(),
|cwd| cwd.join(raw_path).to_string_lossy().into_owned(),
)
} else {
raw_path.clone()
};
let path = path.as_str();
let mode = args
.iter()
.position(|a| a == "--mode" || a == "-m")
.and_then(|i| args.get(i + 1))
.map_or("auto", std::string::String::as_str);
let force_fresh = args.iter().any(|a| a == "--fresh" || a == "--no-cache");
let short = protocol::shorten_path(path);
if let Ok(abs) = std::fs::canonicalize(path) {
match io_boundary::check_secret_path_for_tool("cli_read", &abs) {
Ok(Some(w)) => eprintln!("{w}"),
Ok(None) => {}
Err(e) => {
eprintln!("{e}");
std::process::exit(1);
}
}
} else {
let raw = std::path::Path::new(path);
match io_boundary::check_secret_path_for_tool("cli_read", raw) {
Ok(Some(w)) => eprintln!("{w}"),
Ok(None) => {}
Err(e) => {
eprintln!("{e}");
std::process::exit(1);
}
}
}
#[cfg(unix)]
{
#[cfg(unix)]
if let Some(out) = crate::daemon_client::try_daemon_tool_call_blocking_text(
"ctx_read",
Some(serde_json::json!({
"path": path,
"mode": mode,
"fresh": force_fresh,
})),
) {
println!("{out}");
let sent = count_tokens(&out);
super::common::cli_track_read(path, mode, sent, sent);
return;
}
}
super::common::daemon_fallback_hint();
if !force_fresh && mode == "full" {
use crate::core::cli_cache::{self, CacheResult};
match cli_cache::check_and_read(path) {
CacheResult::Hit { entry, file_ref } => {
let msg = cli_cache::format_hit(&entry, &file_ref, &short);
println!("{msg}");
let sent = count_tokens(&msg);
stats::record("cli_read", entry.original_tokens, sent);
crate::core::heatmap::record_file_access(
path,
entry.original_tokens,
entry.original_tokens.saturating_sub(sent),
);
return;
}
CacheResult::Miss { content } if content.is_empty() => {
eprintln!("Error: could not read {path}");
std::process::exit(1);
}
CacheResult::Miss { content } => {
let line_count = content.lines().count();
println!("{short} [{line_count}L]");
println!("{content}");
let tok = count_tokens(&content);
stats::record("cli_read", tok, tok);
crate::core::heatmap::record_file_access(path, tok, 0);
return;
}
}
}
let content = match crate::tools::ctx_read::read_file_lossy(path) {
Ok(c) => c,
Err(e) => {
eprintln!("Error: {e}");
std::process::exit(1);
}
};
let ext = Path::new(path)
.extension()
.and_then(|e| e.to_str())
.unwrap_or("");
let line_count = content.lines().count();
let original_tokens = count_tokens(&content);
let mode = if mode == "auto" {
let sig = crate::core::mode_predictor::FileSignature::from_path(path, original_tokens);
let predictor = crate::core::mode_predictor::ModePredictor::new();
predictor
.predict_best_mode(&sig)
.unwrap_or_else(|| "full".to_string())
} else {
mode.to_string()
};
let mode = mode.as_str();
match mode {
"map" => {
let sigs = signatures::extract_signatures(&content, ext);
let dep_info = dep_extract::extract_deps(&content, ext);
let mut output_buf = format!("{short} [{line_count}L]");
if !dep_info.imports.is_empty() {
output_buf.push_str(&format!("\n deps: {}", dep_info.imports.join(", ")));
}
if !dep_info.exports.is_empty() {
output_buf.push_str(&format!("\n exports: {}", dep_info.exports.join(", ")));
}
let key_sigs: Vec<_> = sigs
.iter()
.filter(|s| s.is_exported || s.indent == 0)
.collect();
if !key_sigs.is_empty() {
output_buf.push_str("\n API:");
for sig in &key_sigs {
output_buf.push_str(&format!("\n {}", sig.to_compact()));
}
}
println!("{output_buf}");
let sent = count_tokens(&output_buf);
print_savings(original_tokens, sent);
super::common::cli_track_read(path, "map", original_tokens, sent);
}
"signatures" => {
let sigs = signatures::extract_signatures(&content, ext);
let mut output_buf = format!("{short} [{line_count}L]");
for sig in &sigs {
output_buf.push_str(&format!("\n{}", sig.to_compact()));
}
println!("{output_buf}");
let sent = count_tokens(&output_buf);
print_savings(original_tokens, sent);
super::common::cli_track_read(path, "signatures", original_tokens, sent);
}
"aggressive" => {
let compressed = compressor::aggressive_compress(&content, Some(ext));
println!("{short} [{line_count}L]");
println!("{compressed}");
let sent = count_tokens(&compressed);
print_savings(original_tokens, sent);
super::common::cli_track_read(path, "aggressive", original_tokens, sent);
}
"entropy" => {
let result = entropy::entropy_compress(&content);
let avg_h = entropy::analyze_entropy(&content).avg_entropy;
println!("{short} [{line_count}L] (HÌ„={avg_h:.1})");
for tech in &result.techniques {
println!("{tech}");
}
println!("{}", result.output);
let sent = count_tokens(&result.output);
print_savings(original_tokens, sent);
super::common::cli_track_read(path, "entropy", original_tokens, sent);
}
_ => {
let full_output = format!("{short} [{line_count}L]\n{content}");
println!("{full_output}");
let sent = count_tokens(&full_output);
super::common::cli_track_read(path, "full", original_tokens, sent);
}
}
}
pub fn cmd_diff(args: &[String]) {
if args.len() < 2 {
eprintln!("Usage: lean-ctx diff <file1> <file2>");
std::process::exit(1);
}
let content1 = match crate::tools::ctx_read::read_file_lossy(&args[0]) {
Ok(c) => c,
Err(e) => {
eprintln!("Error reading {}: {e}", args[0]);
std::process::exit(1);
}
};
let content2 = match crate::tools::ctx_read::read_file_lossy(&args[1]) {
Ok(c) => c,
Err(e) => {
eprintln!("Error reading {}: {e}", args[1]);
std::process::exit(1);
}
};
let diff = compressor::diff_content(&content1, &content2);
let original = count_tokens(&content1) + count_tokens(&content2);
let sent = count_tokens(&diff);
println!(
"diff {} {}",
protocol::shorten_path(&args[0]),
protocol::shorten_path(&args[1])
);
println!("{diff}");
print_savings(original, sent);
stats::record("cli_diff", original, sent);
}
pub fn cmd_grep(args: &[String]) {
if args.is_empty() {
eprintln!("Usage: lean-ctx grep <pattern> [path]");
std::process::exit(1);
}
let pattern = &args[0];
let path = args.get(1).map_or(".", std::string::String::as_str);
#[cfg(unix)]
{
#[cfg(unix)]
if let Some(out) = crate::daemon_client::try_daemon_tool_call_blocking_text(
"ctx_search",
Some(serde_json::json!({
"pattern": pattern,
"path": path,
})),
) {
println!("{out}");
if out.trim_start().starts_with("0 matches") {
std::process::exit(1);
}
return;
}
}
super::common::daemon_fallback_hint();
let (out, original) = crate::tools::ctx_search::handle(
pattern,
path,
None,
20,
crate::tools::CrpMode::effective(),
true,
roles::active_role().io.allow_secret_paths,
);
println!("{out}");
super::common::cli_track_search(original, count_tokens(&out));
if original == 0 && out.trim_start().starts_with("0 matches") {
std::process::exit(1);
}
}
pub fn cmd_find(args: &[String]) {
if args.is_empty() {
eprintln!("Usage: lean-ctx find <pattern> [path]");
std::process::exit(1);
}
let raw_pattern = &args[0];
let path = args.get(1).map_or(".", std::string::String::as_str);
let is_glob = raw_pattern.contains('*') || raw_pattern.contains('?');
let glob_matcher = if is_glob {
glob::Pattern::new(&raw_pattern.to_lowercase()).ok()
} else {
None
};
let substring = raw_pattern.to_lowercase();
let mut found = false;
for entry in ignore::WalkBuilder::new(path)
.hidden(true)
.git_ignore(true)
.git_global(true)
.git_exclude(true)
.max_depth(Some(10))
.build()
.flatten()
{
let name = entry.file_name().to_string_lossy().to_lowercase();
let matches = if let Some(ref g) = glob_matcher {
g.matches(&name)
} else {
name.contains(&substring)
};
if matches {
println!("{}", entry.path().display());
found = true;
}
}
stats::record("cli_find", 0, 0);
if !found {
std::process::exit(1);
}
}
pub fn cmd_ls(args: &[String]) {
let path = args.first().map_or(".", std::string::String::as_str);
let depth = 3usize;
let show_hidden = false;
#[cfg(unix)]
{
#[cfg(unix)]
if let Some(out) = crate::daemon_client::try_daemon_tool_call_blocking_text(
"ctx_tree",
Some(serde_json::json!({
"path": path,
"depth": depth,
"show_hidden": show_hidden,
})),
) {
println!("{out}");
return;
}
}
super::common::daemon_fallback_hint();
let (out, _original) = crate::tools::ctx_tree::handle(path, depth, show_hidden);
println!("{out}");
super::common::cli_track_tree(0, count_tokens(&out));
}
pub fn cmd_deps(args: &[String]) {
let path = args.first().map_or(".", std::string::String::as_str);
if let Some(result) = deps_cmd::detect_and_compress(path) {
println!("{result}");
stats::record("cli_deps", 0, 0);
} else {
eprintln!("No dependency file found in {path}");
std::process::exit(1);
}
}