Skip to main content

lean_ctx/shell/
compress.rs

1use crate::core::patterns;
2use crate::core::tokens::count_tokens;
3
4const BUILTIN_PASSTHROUGH: &[&str] = &[
5    // JS/TS dev servers & watchers
6    "turbo",
7    "nx serve",
8    "nx dev",
9    "next dev",
10    "vite dev",
11    "vite preview",
12    "vitest",
13    "nuxt dev",
14    "astro dev",
15    "webpack serve",
16    "webpack-dev-server",
17    "nodemon",
18    "concurrently",
19    "pm2",
20    "pm2 logs",
21    "gatsby develop",
22    "expo start",
23    "react-scripts start",
24    "ng serve",
25    "remix dev",
26    "wrangler dev",
27    "hugo server",
28    "hugo serve",
29    "jekyll serve",
30    "bun dev",
31    "ember serve",
32    // Package manager script runners (wrap dev servers via package.json)
33    "npm run dev",
34    "npm run start",
35    "npm run serve",
36    "npm run watch",
37    "npm run preview",
38    "npm run storybook",
39    "npm run test:watch",
40    "npm start",
41    "npx ",
42    "pnpm run dev",
43    "pnpm run start",
44    "pnpm run serve",
45    "pnpm run watch",
46    "pnpm run preview",
47    "pnpm run storybook",
48    "pnpm dev",
49    "pnpm start",
50    "pnpm preview",
51    "yarn dev",
52    "yarn start",
53    "yarn serve",
54    "yarn watch",
55    "yarn preview",
56    "yarn storybook",
57    "bun run dev",
58    "bun run start",
59    "bun run serve",
60    "bun run watch",
61    "bun run preview",
62    "bun start",
63    "deno task dev",
64    "deno task start",
65    "deno task serve",
66    "deno run --watch",
67    // Docker
68    "docker compose up",
69    "docker-compose up",
70    "docker compose logs",
71    "docker-compose logs",
72    "docker compose exec",
73    "docker-compose exec",
74    "docker compose run",
75    "docker-compose run",
76    "docker compose watch",
77    "docker-compose watch",
78    "docker logs",
79    "docker attach",
80    "docker exec -it",
81    "docker exec -ti",
82    "docker run -it",
83    "docker run -ti",
84    "docker stats",
85    "docker events",
86    // Kubernetes
87    "kubectl logs",
88    "kubectl exec -it",
89    "kubectl exec -ti",
90    "kubectl attach",
91    "kubectl port-forward",
92    "kubectl proxy",
93    // System monitors & streaming
94    "top",
95    "htop",
96    "btop",
97    "watch ",
98    "tail -f",
99    "tail -f ",
100    "journalctl -f",
101    "journalctl --follow",
102    "dmesg -w",
103    "dmesg --follow",
104    "strace",
105    "tcpdump",
106    "ping ",
107    "ping6 ",
108    "traceroute",
109    "mtr ",
110    "nmap ",
111    "iperf ",
112    "iperf3 ",
113    "ss -l",
114    "netstat -l",
115    "lsof -i",
116    "socat ",
117    // Editors & pagers
118    "less",
119    "more",
120    "vim",
121    "nvim",
122    "vi ",
123    "nano",
124    "micro ",
125    "helix ",
126    "hx ",
127    "emacs",
128    // Terminal multiplexers
129    "tmux",
130    "screen",
131    // Interactive shells & REPLs
132    "ssh ",
133    "telnet ",
134    "nc ",
135    "ncat ",
136    "psql",
137    "mysql",
138    "sqlite3",
139    "redis-cli",
140    "mongosh",
141    "mongo ",
142    "python3 -i",
143    "python -i",
144    "irb",
145    "rails console",
146    "rails c ",
147    "iex",
148    // Python servers, workers, watchers
149    "flask run",
150    "uvicorn ",
151    "gunicorn ",
152    "hypercorn ",
153    "daphne ",
154    "django-admin runserver",
155    "manage.py runserver",
156    "python manage.py runserver",
157    "python -m http.server",
158    "python3 -m http.server",
159    "streamlit run",
160    "gradio ",
161    "celery worker",
162    "celery -a",
163    "celery -b",
164    "dramatiq ",
165    "rq worker",
166    "watchmedo ",
167    "ptw ",
168    "pytest-watch",
169    // Ruby / Rails
170    "rails server",
171    "rails s",
172    "puma ",
173    "unicorn ",
174    "thin start",
175    "foreman start",
176    "overmind start",
177    "guard ",
178    "sidekiq",
179    "resque ",
180    // PHP / Laravel
181    "php artisan serve",
182    "php -s ",
183    "php artisan queue:work",
184    "php artisan queue:listen",
185    "php artisan horizon",
186    "php artisan tinker",
187    "sail up",
188    // Java / JVM
189    "./gradlew bootrun",
190    "gradlew bootrun",
191    "gradle bootrun",
192    "./gradlew run",
193    "mvn spring-boot:run",
194    "./mvnw spring-boot:run",
195    "mvnw spring-boot:run",
196    "mvn quarkus:dev",
197    "./mvnw quarkus:dev",
198    "sbt run",
199    "sbt ~compile",
200    "lein run",
201    "lein repl",
202    // Go
203    "go run ",
204    "air ",
205    "gin ",
206    "realize start",
207    "reflex ",
208    "gowatch ",
209    // .NET / C#
210    "dotnet run",
211    "dotnet watch",
212    "dotnet ef",
213    // Elixir / Erlang
214    "mix phx.server",
215    "iex -s mix",
216    // Swift
217    "swift run",
218    "swift package ",
219    "vapor serve",
220    // Zig
221    "zig build run",
222    // Rust
223    "cargo watch",
224    "cargo run",
225    "cargo leptos watch",
226    "bacon ",
227    // General watchers & task runners
228    "make dev",
229    "make serve",
230    "make watch",
231    "make run",
232    "make start",
233    "just dev",
234    "just serve",
235    "just watch",
236    "just start",
237    "just run",
238    "task dev",
239    "task serve",
240    "task watch",
241    "nix develop",
242    "devenv up",
243    // CI/CD & infrastructure (long-running)
244    "act ",
245    "skaffold dev",
246    "tilt up",
247    "garden dev",
248    "telepresence ",
249    // Load testing & benchmarking
250    "ab ",
251    "wrk ",
252    "hey ",
253    "vegeta ",
254    "k6 run",
255    "artillery run",
256    // Authentication flows (device code, OAuth, SSO)
257    "az login",
258    "az account",
259    "gh",
260    "gcloud auth",
261    "gcloud init",
262    "aws sso",
263    "aws configure sso",
264    "firebase login",
265    "netlify login",
266    "vercel login",
267    "heroku login",
268    "flyctl auth",
269    "fly auth",
270    "railway login",
271    "supabase login",
272    "wrangler login",
273    "doppler login",
274    "vault login",
275    "oc login",
276    "kubelogin",
277    "--use-device-code",
278];
279
280const SCRIPT_RUNNER_PREFIXES: &[&str] = &[
281    "npm run ",
282    "npm start",
283    "npx ",
284    "pnpm run ",
285    "pnpm dev",
286    "pnpm start",
287    "pnpm preview",
288    "yarn ",
289    "bun run ",
290    "bun start",
291    "deno task ",
292];
293
294const DEV_SCRIPT_KEYWORDS: &[&str] = &[
295    "dev",
296    "start",
297    "serve",
298    "watch",
299    "preview",
300    "storybook",
301    "hot",
302    "live",
303    "hmr",
304];
305
306fn is_dev_script_runner(cmd: &str) -> bool {
307    for prefix in SCRIPT_RUNNER_PREFIXES {
308        if let Some(rest) = cmd.strip_prefix(prefix) {
309            let script_name = rest.split_whitespace().next().unwrap_or("");
310            for kw in DEV_SCRIPT_KEYWORDS {
311                if script_name.contains(kw) {
312                    return true;
313                }
314            }
315        }
316    }
317    false
318}
319
320pub(super) fn is_excluded_command(command: &str, excluded: &[String]) -> bool {
321    let cmd = command.trim().to_lowercase();
322    for pattern in BUILTIN_PASSTHROUGH {
323        if pattern.starts_with("--") {
324            if cmd.contains(pattern) {
325                return true;
326            }
327        } else if pattern.ends_with(' ') || pattern.ends_with('\t') {
328            if cmd == pattern.trim() || cmd.starts_with(pattern) {
329                return true;
330            }
331        } else if cmd == *pattern
332            || cmd.starts_with(&format!("{pattern} "))
333            || cmd.starts_with(&format!("{pattern}\t"))
334            || cmd.contains(&format!(" {pattern} "))
335            || cmd.contains(&format!(" {pattern}\t"))
336            || cmd.contains(&format!("|{pattern} "))
337            || cmd.contains(&format!("|{pattern}\t"))
338            || cmd.ends_with(&format!(" {pattern}"))
339            || cmd.ends_with(&format!("|{pattern}"))
340        {
341            return true;
342        }
343    }
344
345    if is_dev_script_runner(&cmd) {
346        return true;
347    }
348
349    if excluded.is_empty() {
350        return false;
351    }
352    excluded.iter().any(|excl| {
353        let excl_lower = excl.trim().to_lowercase();
354        cmd == excl_lower || cmd.starts_with(&format!("{excl_lower} "))
355    })
356}
357
358pub(super) fn compress_and_measure(command: &str, stdout: &str, stderr: &str) -> (String, usize) {
359    let compressed_stdout = compress_if_beneficial(command, stdout);
360    let compressed_stderr = compress_if_beneficial(command, stderr);
361
362    let mut result = String::new();
363    if !compressed_stdout.is_empty() {
364        result.push_str(&compressed_stdout);
365    }
366    if !compressed_stderr.is_empty() {
367        if !result.is_empty() {
368            result.push('\n');
369        }
370        result.push_str(&compressed_stderr);
371    }
372
373    let content_for_counting = if let Some(pos) = result.rfind("\n[lean-ctx: ") {
374        &result[..pos]
375    } else {
376        &result
377    };
378    let output_tokens = count_tokens(content_for_counting);
379    (result, output_tokens)
380}
381
382fn is_search_output(command: &str) -> bool {
383    let c = command.trim_start();
384    c.starts_with("grep ")
385        || c.starts_with("rg ")
386        || c.starts_with("find ")
387        || c.starts_with("fd ")
388        || c.starts_with("ag ")
389        || c.starts_with("ack ")
390}
391
392/// Returns true for commands whose output structure is critical for developer
393/// readability. Pattern compression (light cleanup like removing `index` lines
394/// or limiting context) still applies, but the terse pipeline and generic
395/// compressors are skipped so diff hunks, blame annotations, etc. remain
396/// fully readable.
397pub fn has_structural_output(command: &str) -> bool {
398    if is_verbatim_output(command) {
399        return true;
400    }
401    if is_standalone_diff_command(command) {
402        return true;
403    }
404    is_structural_git_command(command)
405}
406
407/// Returns true for commands where the output IS the purpose of the command.
408/// These must never have their content transformed — only size-limited if huge.
409/// Checks both the full command AND the last pipe segment for comprehensive coverage.
410pub fn is_verbatim_output(command: &str) -> bool {
411    is_verbatim_single(command) || is_verbatim_pipe_tail(command)
412}
413
414fn is_verbatim_single(command: &str) -> bool {
415    is_http_client(command)
416        || is_file_viewer(command)
417        || is_data_format_tool(command)
418        || is_binary_viewer(command)
419        || is_infra_inspection(command)
420        || is_crypto_command(command)
421        || is_database_query(command)
422        || is_dns_network_inspection(command)
423        || is_language_one_liner(command)
424        || is_container_listing(command)
425        || is_file_listing(command)
426        || is_system_query(command)
427        || is_cloud_cli_query(command)
428        || is_package_manager_info(command)
429        || is_version_or_help(command)
430        || is_config_viewer(command)
431        || is_log_viewer(command)
432        || is_archive_listing(command)
433        || is_clipboard_tool(command)
434        || is_git_data_command(command)
435        || is_task_dry_run(command)
436        || is_env_dump(command)
437}
438
439/// For piped commands like `kubectl get pods -o json | jq '.items[]'`,
440/// check if the LAST command in the pipe is a verbatim tool.
441fn is_verbatim_pipe_tail(command: &str) -> bool {
442    if !command.contains('|') {
443        return false;
444    }
445    let last_segment = command.rsplit('|').next().unwrap_or("").trim();
446    if last_segment.is_empty() {
447        return false;
448    }
449    is_verbatim_single(last_segment)
450}
451
452fn is_http_client(command: &str) -> bool {
453    let first = first_binary(command);
454    matches!(
455        first,
456        "curl" | "wget" | "http" | "https" | "xh" | "curlie" | "grpcurl" | "grpc_cli"
457    )
458}
459
460fn is_file_viewer(command: &str) -> bool {
461    let first = first_binary(command);
462    match first {
463        "cat" | "bat" | "batcat" | "pygmentize" | "highlight" => true,
464        "head" | "tail" => !command.contains("-f") && !command.contains("--follow"),
465        _ => false,
466    }
467}
468
469fn is_data_format_tool(command: &str) -> bool {
470    let first = first_binary(command);
471    matches!(
472        first,
473        "jq" | "yq"
474            | "xq"
475            | "fx"
476            | "gron"
477            | "mlr"
478            | "miller"
479            | "dasel"
480            | "csvlook"
481            | "csvcut"
482            | "csvgrep"
483            | "csvjson"
484            | "in2csv"
485            | "sql2csv"
486    )
487}
488
489fn is_binary_viewer(command: &str) -> bool {
490    let first = first_binary(command);
491    matches!(first, "xxd" | "hexdump" | "od" | "strings" | "file")
492}
493
494fn is_infra_inspection(command: &str) -> bool {
495    let cl = command.trim().to_ascii_lowercase();
496    if cl.starts_with("terraform output")
497        || cl.starts_with("terraform show")
498        || cl.starts_with("terraform state show")
499        || cl.starts_with("terraform state list")
500        || cl.starts_with("terraform state pull")
501        || cl.starts_with("tofu output")
502        || cl.starts_with("tofu show")
503        || cl.starts_with("tofu state show")
504        || cl.starts_with("tofu state list")
505        || cl.starts_with("tofu state pull")
506        || cl.starts_with("pulumi stack output")
507        || cl.starts_with("pulumi stack export")
508    {
509        return true;
510    }
511    if cl.starts_with("docker inspect") || cl.starts_with("podman inspect") {
512        return true;
513    }
514    if (cl.starts_with("kubectl get") || cl.starts_with("k get"))
515        && (cl.contains("-o yaml")
516            || cl.contains("-o json")
517            || cl.contains("-oyaml")
518            || cl.contains("-ojson")
519            || cl.contains("--output yaml")
520            || cl.contains("--output json")
521            || cl.contains("--output=yaml")
522            || cl.contains("--output=json"))
523    {
524        return true;
525    }
526    if cl.starts_with("kubectl describe") || cl.starts_with("k describe") {
527        return true;
528    }
529    if cl.starts_with("helm get") || cl.starts_with("helm template") {
530        return true;
531    }
532    false
533}
534
535fn is_crypto_command(command: &str) -> bool {
536    let first = first_binary(command);
537    if first == "openssl" {
538        return true;
539    }
540    matches!(first, "gpg" | "age" | "ssh-keygen" | "certutil")
541}
542
543fn is_database_query(command: &str) -> bool {
544    let cl = command.to_ascii_lowercase();
545    if cl.starts_with("psql ") && (cl.contains(" -c ") || cl.contains("--command")) {
546        return true;
547    }
548    if cl.starts_with("mysql ") && (cl.contains(" -e ") || cl.contains("--execute")) {
549        return true;
550    }
551    if cl.starts_with("mariadb ") && (cl.contains(" -e ") || cl.contains("--execute")) {
552        return true;
553    }
554    if cl.starts_with("sqlite3 ") && cl.contains('"') {
555        return true;
556    }
557    if cl.starts_with("mongosh ") && cl.contains("--eval") {
558        return true;
559    }
560    false
561}
562
563fn is_dns_network_inspection(command: &str) -> bool {
564    let first = first_binary(command);
565    matches!(
566        first,
567        "dig" | "nslookup" | "host" | "whois" | "drill" | "resolvectl"
568    )
569}
570
571fn is_language_one_liner(command: &str) -> bool {
572    let cl = command.to_ascii_lowercase();
573    (cl.starts_with("python ") || cl.starts_with("python3 "))
574        && (cl.contains(" -c ") || cl.contains(" -c\"") || cl.contains(" -c'"))
575        || (cl.starts_with("node ") && (cl.contains(" -e ") || cl.contains(" --eval")))
576        || (cl.starts_with("ruby ") && cl.contains(" -e "))
577        || (cl.starts_with("perl ") && cl.contains(" -e "))
578        || (cl.starts_with("php ") && cl.contains(" -r "))
579}
580
581fn is_container_listing(command: &str) -> bool {
582    let cl = command.trim().to_ascii_lowercase();
583    if cl.starts_with("docker ps") || cl.starts_with("docker images") {
584        return true;
585    }
586    if cl.starts_with("podman ps") || cl.starts_with("podman images") {
587        return true;
588    }
589    if cl.starts_with("kubectl get") || cl.starts_with("k get") {
590        return true;
591    }
592    if cl.starts_with("helm list") || cl.starts_with("helm ls") {
593        return true;
594    }
595    if cl.starts_with("docker compose ps") || cl.starts_with("docker-compose ps") {
596        return true;
597    }
598    false
599}
600
601fn is_file_listing(command: &str) -> bool {
602    let first = first_binary(command);
603    matches!(
604        first,
605        "find" | "fd" | "fdfind" | "ls" | "exa" | "eza" | "lsd"
606    )
607}
608
609fn is_system_query(command: &str) -> bool {
610    let first = first_binary(command);
611    matches!(
612        first,
613        "stat"
614            | "wc"
615            | "du"
616            | "df"
617            | "free"
618            | "uname"
619            | "id"
620            | "whoami"
621            | "hostname"
622            | "uptime"
623            | "lscpu"
624            | "lsblk"
625            | "ip"
626            | "ifconfig"
627            | "route"
628            | "ss"
629            | "netstat"
630            | "base64"
631            | "sha256sum"
632            | "sha1sum"
633            | "md5sum"
634            | "cksum"
635            | "readlink"
636            | "realpath"
637            | "which"
638            | "type"
639            | "command"
640    )
641}
642
643fn is_cloud_cli_query(command: &str) -> bool {
644    let cl = command.trim().to_ascii_lowercase();
645    let cloud_query_verbs = [
646        "describe",
647        "get",
648        "list",
649        "show",
650        "export",
651        "inspect",
652        "info",
653        "status",
654        "whoami",
655        "caller-identity",
656        "account",
657    ];
658
659    let is_aws = cl.starts_with("aws ") && !cl.starts_with("aws configure");
660    let is_gcloud =
661        cl.starts_with("gcloud ") && !cl.starts_with("gcloud auth") && !cl.contains(" deploy");
662    let is_az = cl.starts_with("az ") && !cl.starts_with("az login");
663
664    if !(is_aws || is_gcloud || is_az) {
665        return false;
666    }
667
668    cloud_query_verbs
669        .iter()
670        .any(|verb| cl.contains(&format!(" {verb}")))
671}
672
673fn is_package_manager_info(command: &str) -> bool {
674    let cl = command.trim().to_ascii_lowercase();
675
676    if cl.starts_with("npm ") {
677        return cl.starts_with("npm list")
678            || cl.starts_with("npm ls")
679            || cl.starts_with("npm info")
680            || cl.starts_with("npm view")
681            || cl.starts_with("npm show")
682            || cl.starts_with("npm outdated")
683            || cl.starts_with("npm audit");
684    }
685    if cl.starts_with("yarn ") {
686        return cl.starts_with("yarn list")
687            || cl.starts_with("yarn info")
688            || cl.starts_with("yarn why")
689            || cl.starts_with("yarn outdated")
690            || cl.starts_with("yarn audit");
691    }
692    if cl.starts_with("pnpm ") {
693        return cl.starts_with("pnpm list")
694            || cl.starts_with("pnpm ls")
695            || cl.starts_with("pnpm why")
696            || cl.starts_with("pnpm outdated")
697            || cl.starts_with("pnpm audit");
698    }
699    if cl.starts_with("pip ") || cl.starts_with("pip3 ") {
700        return cl.contains(" list") || cl.contains(" show") || cl.contains(" freeze");
701    }
702    if cl.starts_with("gem ") {
703        return cl.starts_with("gem list")
704            || cl.starts_with("gem info")
705            || cl.starts_with("gem specification");
706    }
707    if cl.starts_with("cargo ") {
708        return cl.starts_with("cargo metadata")
709            || cl.starts_with("cargo tree")
710            || cl.starts_with("cargo pkgid");
711    }
712    if cl.starts_with("go ") {
713        return cl.starts_with("go list") || cl.starts_with("go version");
714    }
715    if cl.starts_with("composer ") {
716        return cl.starts_with("composer show")
717            || cl.starts_with("composer info")
718            || cl.starts_with("composer outdated");
719    }
720    if cl.starts_with("brew ") {
721        return cl.starts_with("brew list")
722            || cl.starts_with("brew info")
723            || cl.starts_with("brew deps")
724            || cl.starts_with("brew outdated");
725    }
726    if cl.starts_with("apt ") || cl.starts_with("dpkg ") {
727        return cl.starts_with("apt list")
728            || cl.starts_with("apt show")
729            || cl.starts_with("dpkg -l")
730            || cl.starts_with("dpkg --list")
731            || cl.starts_with("dpkg -s");
732    }
733    false
734}
735
736fn is_version_or_help(command: &str) -> bool {
737    let parts: Vec<&str> = command.split_whitespace().collect();
738    if parts.len() < 2 || parts.len() > 3 {
739        return false;
740    }
741    parts.iter().any(|p| {
742        *p == "--version"
743            || *p == "-V"
744            || p.eq_ignore_ascii_case("version")
745            || *p == "--help"
746            || *p == "-h"
747            || p.eq_ignore_ascii_case("help")
748    })
749}
750
751fn is_config_viewer(command: &str) -> bool {
752    let cl = command.trim().to_ascii_lowercase();
753    if cl.starts_with("git config") && !cl.contains("--set") && !cl.contains("--unset") {
754        return true;
755    }
756    if cl.starts_with("npm config list") || cl.starts_with("npm config get") {
757        return true;
758    }
759    if cl.starts_with("yarn config") && !cl.contains(" set") {
760        return true;
761    }
762    if cl.starts_with("pip config list") || cl.starts_with("pip3 config list") {
763        return true;
764    }
765    if cl.starts_with("rustup show") || cl.starts_with("rustup target list") {
766        return true;
767    }
768    if cl.starts_with("docker context ls") || cl.starts_with("docker context list") {
769        return true;
770    }
771    if cl.starts_with("kubectl config")
772        && (cl.contains("view") || cl.contains("get-contexts") || cl.contains("current-context"))
773    {
774        return true;
775    }
776    false
777}
778
779fn is_log_viewer(command: &str) -> bool {
780    let cl = command.trim().to_ascii_lowercase();
781    if cl.starts_with("journalctl") && !cl.contains("-f") && !cl.contains("--follow") {
782        return true;
783    }
784    if cl.starts_with("dmesg") && !cl.contains("-w") && !cl.contains("--follow") {
785        return true;
786    }
787    if cl.starts_with("docker logs") && !cl.contains("-f") && !cl.contains("--follow") {
788        return true;
789    }
790    if cl.starts_with("kubectl logs") && !cl.contains("-f") && !cl.contains("--follow") {
791        return true;
792    }
793    if cl.starts_with("docker compose logs") && !cl.contains("-f") && !cl.contains("--follow") {
794        return true;
795    }
796    false
797}
798
799fn is_archive_listing(command: &str) -> bool {
800    let cl = command.trim().to_ascii_lowercase();
801    if cl.starts_with("tar ") && (cl.contains(" -tf") || cl.contains(" -t") || cl.contains(" tf")) {
802        return true;
803    }
804    if cl.starts_with("unzip -l") || cl.starts_with("unzip -Z") {
805        return true;
806    }
807    let first = first_binary(command);
808    matches!(first, "zipinfo" | "lsar" | "7z" if cl.contains(" l ") || cl.contains(" l\t"))
809        || first == "zipinfo"
810        || first == "lsar"
811}
812
813fn is_clipboard_tool(command: &str) -> bool {
814    let first = first_binary(command);
815    if matches!(first, "pbpaste" | "wl-paste") {
816        return true;
817    }
818    let cl = command.trim().to_ascii_lowercase();
819    if cl.starts_with("xclip") && cl.contains("-o") {
820        return true;
821    }
822    if cl.starts_with("xsel") && (cl.contains("-o") || cl.contains("--output")) {
823        return true;
824    }
825    false
826}
827
828fn is_git_data_command(command: &str) -> bool {
829    let cl = command.trim().to_ascii_lowercase();
830    if !cl.contains("git") {
831        return false;
832    }
833    let exact_data_subs = [
834        "remote",
835        "rev-parse",
836        "rev-list",
837        "ls-files",
838        "ls-tree",
839        "ls-remote",
840        "shortlog",
841        "for-each-ref",
842        "cat-file",
843        "name-rev",
844        "describe",
845        "merge-base",
846    ];
847
848    let mut tokens = cl.split_whitespace();
849    while let Some(tok) = tokens.next() {
850        let base = tok.rsplit('/').next().unwrap_or(tok);
851        if base != "git" {
852            continue;
853        }
854        let mut skip_next = false;
855        for arg in tokens.by_ref() {
856            if skip_next {
857                skip_next = false;
858                continue;
859            }
860            if arg == "-c" || arg == "-C" || arg == "--git-dir" || arg == "--work-tree" {
861                skip_next = true;
862                continue;
863            }
864            if arg.starts_with('-') {
865                continue;
866            }
867            return exact_data_subs.contains(&arg);
868        }
869        return false;
870    }
871    false
872}
873
874fn is_task_dry_run(command: &str) -> bool {
875    let cl = command.trim().to_ascii_lowercase();
876    if cl.starts_with("make ") && (cl.contains(" -n") || cl.contains(" --dry-run")) {
877        return true;
878    }
879    if cl.starts_with("ansible") && (cl.contains("--check") || cl.contains("--diff")) {
880        return true;
881    }
882    false
883}
884
885fn is_env_dump(command: &str) -> bool {
886    let first = first_binary(command);
887    matches!(first, "env" | "printenv" | "set" | "export" | "locale")
888}
889
890/// Extracts the binary name (basename, no path) from the first token of a command.
891fn first_binary(command: &str) -> &str {
892    let first = command.split_whitespace().next().unwrap_or("");
893    first.rsplit('/').next().unwrap_or(first)
894}
895
896/// Non-git diff tools: `diff`, `colordiff`, `icdiff`, `delta`.
897fn is_standalone_diff_command(command: &str) -> bool {
898    let first = command.split_whitespace().next().unwrap_or("");
899    let base = first.rsplit('/').next().unwrap_or(first);
900    base.eq_ignore_ascii_case("diff")
901        || base.eq_ignore_ascii_case("colordiff")
902        || base.eq_ignore_ascii_case("icdiff")
903        || base.eq_ignore_ascii_case("delta")
904}
905
906/// Git subcommands that produce structural output the developer must read verbatim.
907fn is_structural_git_command(command: &str) -> bool {
908    let mut tokens = command.split_whitespace();
909    while let Some(tok) = tokens.next() {
910        let base = tok.rsplit('/').next().unwrap_or(tok);
911        if !base.eq_ignore_ascii_case("git") {
912            continue;
913        }
914        let mut skip_next = false;
915        let remaining: Vec<&str> = tokens.collect();
916        for arg in &remaining {
917            if skip_next {
918                skip_next = false;
919                continue;
920            }
921            if *arg == "-C" || *arg == "-c" || *arg == "--git-dir" || *arg == "--work-tree" {
922                skip_next = true;
923                continue;
924            }
925            if arg.starts_with('-') {
926                continue;
927            }
928            let sub = arg.to_ascii_lowercase();
929            return match sub.as_str() {
930                "diff" | "show" | "blame" => true,
931                "log" => has_patch_flag(&remaining),
932                "stash" => remaining.iter().any(|a| a.eq_ignore_ascii_case("show")),
933                _ => false,
934            };
935        }
936        return false;
937    }
938    false
939}
940
941/// Returns true if the argument list contains `-p` or `--patch`.
942fn has_patch_flag(args: &[&str]) -> bool {
943    args.iter()
944        .any(|a| *a == "-p" || *a == "--patch" || a.starts_with("-p"))
945}
946
947fn compress_if_beneficial(command: &str, output: &str) -> String {
948    if output.trim().is_empty() {
949        return String::new();
950    }
951
952    if !is_search_output(command) && crate::tools::ctx_shell::contains_auth_flow(output) {
953        return output.to_string();
954    }
955
956    let original_tokens = count_tokens(output);
957
958    if original_tokens < 50 {
959        return output.to_string();
960    }
961
962    let min_output_tokens = 5;
963
964    if is_verbatim_output(command) {
965        return truncate_verbatim(output, original_tokens);
966    }
967
968    if has_structural_output(command) {
969        let cl = command.to_ascii_lowercase();
970        if let Some(compressed) = patterns::try_specific_pattern(&cl, output) {
971            if !compressed.trim().is_empty() {
972                let compressed_tokens = count_tokens(&compressed);
973                if compressed_tokens >= min_output_tokens && compressed_tokens < original_tokens {
974                    let saved = original_tokens - compressed_tokens;
975                    let pct = (saved as f64 / original_tokens as f64 * 100.0).round() as usize;
976                    if pct >= 5 {
977                        return format!(
978                            "{compressed}\n[lean-ctx: {original_tokens}→{compressed_tokens} tok, -{pct}%]"
979                        );
980                    }
981                    return compressed;
982                }
983            }
984        }
985        return output.to_string();
986    }
987
988    if let Some(mut compressed) = patterns::compress_output(command, output) {
989        if !compressed.trim().is_empty() {
990            let config = crate::core::config::Config::load();
991            let level = crate::core::config::CompressionLevel::effective(&config);
992            if level.is_active() {
993                let terse_result =
994                    crate::core::terse::pipeline::compress(output, &level, Some(&compressed));
995                if terse_result.quality_passed {
996                    compressed = terse_result.output;
997                }
998            }
999
1000            let compressed_tokens = count_tokens(&compressed);
1001            if compressed_tokens >= min_output_tokens && compressed_tokens < original_tokens {
1002                let ratio = compressed_tokens as f64 / original_tokens as f64;
1003                if ratio < 0.05 && original_tokens > 100 && original_tokens < 2000 {
1004                    tracing::warn!("compression removed >95% of small output, returning original");
1005                    return output.to_string();
1006                }
1007                let saved = original_tokens - compressed_tokens;
1008                let pct = (saved as f64 / original_tokens as f64 * 100.0).round() as usize;
1009                if pct >= 5 {
1010                    return format!(
1011                        "{compressed}\n[lean-ctx: {original_tokens}→{compressed_tokens} tok, -{pct}%]"
1012                    );
1013                }
1014                return compressed;
1015            }
1016            if compressed_tokens < min_output_tokens {
1017                return output.to_string();
1018            }
1019        }
1020    }
1021
1022    {
1023        let config = crate::core::config::Config::load();
1024        let level = crate::core::config::CompressionLevel::effective(&config);
1025        if level.is_active() {
1026            let terse_result = crate::core::terse::pipeline::compress(output, &level, None);
1027            if terse_result.quality_passed && terse_result.savings_pct >= 3.0 {
1028                let tok_before = terse_result.tokens_before;
1029                let tok_after = terse_result.tokens_after;
1030                let pct = terse_result.savings_pct.round() as usize;
1031                return format!(
1032                    "{}\n[lean-ctx: {tok_before}→{tok_after} tok, -{pct}%]",
1033                    terse_result.output
1034                );
1035            }
1036        }
1037    }
1038
1039    let cleaned = crate::core::compressor::lightweight_cleanup(output);
1040    let cleaned_tokens = count_tokens(&cleaned);
1041    if cleaned_tokens < original_tokens {
1042        let lines: Vec<&str> = cleaned.lines().collect();
1043        if lines.len() > 30 {
1044            let compressed = truncate_with_safety_scan(&lines, original_tokens);
1045            if let Some(c) = compressed {
1046                return c;
1047            }
1048        }
1049        if cleaned_tokens < original_tokens {
1050            let saved = original_tokens - cleaned_tokens;
1051            let pct = (saved as f64 / original_tokens as f64 * 100.0).round() as usize;
1052            if pct >= 5 {
1053                return format!(
1054                    "{cleaned}\n[lean-ctx: {original_tokens}→{cleaned_tokens} tok, -{pct}%]"
1055                );
1056            }
1057            return cleaned;
1058        }
1059    }
1060
1061    let lines: Vec<&str> = output.lines().collect();
1062    if lines.len() > 30 {
1063        if let Some(c) = truncate_with_safety_scan(&lines, original_tokens) {
1064            return c;
1065        }
1066    }
1067
1068    output.to_string()
1069}
1070
1071const MAX_VERBATIM_TOKENS: usize = 8000;
1072
1073/// For verbatim commands: never transform content, only head/tail truncate if huge.
1074fn truncate_verbatim(output: &str, original_tokens: usize) -> String {
1075    if original_tokens <= MAX_VERBATIM_TOKENS {
1076        return output.to_string();
1077    }
1078    let lines: Vec<&str> = output.lines().collect();
1079    let total = lines.len();
1080    if total <= 60 {
1081        return output.to_string();
1082    }
1083    let head = 30.min(total);
1084    let tail = 20.min(total.saturating_sub(head));
1085    let omitted = total - head - tail;
1086    let mut result = String::with_capacity(output.len() / 2);
1087    for line in &lines[..head] {
1088        result.push_str(line);
1089        result.push('\n');
1090    }
1091    result.push_str(&format!(
1092        "\n[{omitted} lines omitted — output too large for context window]\n\n"
1093    ));
1094    for line in lines.iter().skip(total - tail) {
1095        result.push_str(line);
1096        result.push('\n');
1097    }
1098    let truncated_tokens = count_tokens(&result);
1099    result.push_str(&format!(
1100        "[lean-ctx: {original_tokens}→{truncated_tokens} tok, verbatim truncated]"
1101    ));
1102    result
1103}
1104
1105fn truncate_with_safety_scan(lines: &[&str], original_tokens: usize) -> Option<String> {
1106    use crate::core::safety_needles;
1107
1108    let first = &lines[..5];
1109    let last = &lines[lines.len() - 5..];
1110    let middle = &lines[5..lines.len() - 5];
1111
1112    let safety_lines = safety_needles::extract_safety_lines(middle, 20);
1113    let safety_count = safety_lines.len();
1114    let omitted = middle.len() - safety_count;
1115
1116    let mut parts = Vec::new();
1117    parts.push(first.join("\n"));
1118    if safety_count > 0 {
1119        parts.push(format!(
1120            "[{omitted} lines omitted, {safety_count} safety-relevant lines preserved]"
1121        ));
1122        parts.push(safety_lines.join("\n"));
1123    } else {
1124        parts.push(format!("[{omitted} lines omitted]"));
1125    }
1126    parts.push(last.join("\n"));
1127
1128    let compressed = parts.join("\n");
1129    let ct = count_tokens(&compressed);
1130    if ct >= original_tokens {
1131        return None;
1132    }
1133    let saved = original_tokens - ct;
1134    let pct = (saved as f64 / original_tokens as f64 * 100.0).round() as usize;
1135    if pct >= 5 {
1136        Some(format!(
1137            "{compressed}\n[lean-ctx: {original_tokens}→{ct} tok, -{pct}%]"
1138        ))
1139    } else {
1140        Some(compressed)
1141    }
1142}
1143
1144/// Public wrapper for integration tests to exercise the compression pipeline.
1145pub fn compress_if_beneficial_pub(command: &str, output: &str) -> String {
1146    compress_if_beneficial(command, output)
1147}
1148
1149#[cfg(test)]
1150mod passthrough_tests {
1151    use super::is_excluded_command;
1152
1153    #[test]
1154    fn turbo_is_passthrough() {
1155        assert!(is_excluded_command("turbo run dev", &[]));
1156        assert!(is_excluded_command("turbo run build", &[]));
1157        assert!(is_excluded_command("pnpm turbo run dev", &[]));
1158        assert!(is_excluded_command("npx turbo run dev", &[]));
1159    }
1160
1161    #[test]
1162    fn dev_servers_are_passthrough() {
1163        assert!(is_excluded_command("next dev", &[]));
1164        assert!(is_excluded_command("vite dev", &[]));
1165        assert!(is_excluded_command("nuxt dev", &[]));
1166        assert!(is_excluded_command("astro dev", &[]));
1167        assert!(is_excluded_command("nodemon server.js", &[]));
1168    }
1169
1170    #[test]
1171    fn interactive_tools_are_passthrough() {
1172        assert!(is_excluded_command("vim file.rs", &[]));
1173        assert!(is_excluded_command("nvim", &[]));
1174        assert!(is_excluded_command("htop", &[]));
1175        assert!(is_excluded_command("ssh user@host", &[]));
1176        assert!(is_excluded_command("tail -f /var/log/syslog", &[]));
1177    }
1178
1179    #[test]
1180    fn docker_streaming_is_passthrough() {
1181        assert!(is_excluded_command("docker logs my-container", &[]));
1182        assert!(is_excluded_command("docker logs -f webapp", &[]));
1183        assert!(is_excluded_command("docker attach my-container", &[]));
1184        assert!(is_excluded_command("docker exec -it web bash", &[]));
1185        assert!(is_excluded_command("docker exec -ti web bash", &[]));
1186        assert!(is_excluded_command("docker run -it ubuntu bash", &[]));
1187        assert!(is_excluded_command("docker compose exec web bash", &[]));
1188        assert!(is_excluded_command("docker stats", &[]));
1189        assert!(is_excluded_command("docker events", &[]));
1190    }
1191
1192    #[test]
1193    fn kubectl_is_passthrough() {
1194        assert!(is_excluded_command("kubectl logs my-pod", &[]));
1195        assert!(is_excluded_command("kubectl logs -f deploy/web", &[]));
1196        assert!(is_excluded_command("kubectl exec -it pod -- bash", &[]));
1197        assert!(is_excluded_command(
1198            "kubectl port-forward svc/web 8080:80",
1199            &[]
1200        ));
1201        assert!(is_excluded_command("kubectl attach my-pod", &[]));
1202        assert!(is_excluded_command("kubectl proxy", &[]));
1203    }
1204
1205    #[test]
1206    fn database_repls_are_passthrough() {
1207        assert!(is_excluded_command("psql -U user mydb", &[]));
1208        assert!(is_excluded_command("mysql -u root -p", &[]));
1209        assert!(is_excluded_command("sqlite3 data.db", &[]));
1210        assert!(is_excluded_command("redis-cli", &[]));
1211        assert!(is_excluded_command("mongosh", &[]));
1212    }
1213
1214    #[test]
1215    fn streaming_tools_are_passthrough() {
1216        assert!(is_excluded_command("journalctl -f", &[]));
1217        assert!(is_excluded_command("ping 8.8.8.8", &[]));
1218        assert!(is_excluded_command("strace -p 1234", &[]));
1219        assert!(is_excluded_command("tcpdump -i eth0", &[]));
1220        assert!(is_excluded_command("tail -F /var/log/app.log", &[]));
1221        assert!(is_excluded_command("tmux new -s work", &[]));
1222        assert!(is_excluded_command("screen -S dev", &[]));
1223    }
1224
1225    #[test]
1226    fn additional_dev_servers_are_passthrough() {
1227        assert!(is_excluded_command("gatsby develop", &[]));
1228        assert!(is_excluded_command("ng serve --port 4200", &[]));
1229        assert!(is_excluded_command("remix dev", &[]));
1230        assert!(is_excluded_command("wrangler dev", &[]));
1231        assert!(is_excluded_command("hugo server", &[]));
1232        assert!(is_excluded_command("bun dev", &[]));
1233        assert!(is_excluded_command("cargo watch -x test", &[]));
1234    }
1235
1236    #[test]
1237    fn normal_commands_not_excluded() {
1238        assert!(!is_excluded_command("git status", &[]));
1239        assert!(!is_excluded_command("cargo test", &[]));
1240        assert!(!is_excluded_command("npm run build", &[]));
1241        assert!(!is_excluded_command("ls -la", &[]));
1242    }
1243
1244    #[test]
1245    fn user_exclusions_work() {
1246        let excl = vec!["myapp".to_string()];
1247        assert!(is_excluded_command("myapp serve", &excl));
1248        assert!(!is_excluded_command("git status", &excl));
1249    }
1250
1251    #[test]
1252    fn auth_commands_excluded() {
1253        assert!(is_excluded_command("az login --use-device-code", &[]));
1254        assert!(is_excluded_command("gh auth login", &[]));
1255        assert!(is_excluded_command("gh pr close --comment 'done'", &[]));
1256        assert!(is_excluded_command("gh issue list", &[]));
1257        assert!(is_excluded_command("gcloud auth login", &[]));
1258        assert!(is_excluded_command("aws sso login", &[]));
1259        assert!(is_excluded_command("firebase login", &[]));
1260        assert!(is_excluded_command("vercel login", &[]));
1261        assert!(is_excluded_command("heroku login", &[]));
1262        assert!(is_excluded_command("az login", &[]));
1263        assert!(is_excluded_command("kubelogin convert-kubeconfig", &[]));
1264        assert!(is_excluded_command("vault login -method=oidc", &[]));
1265        assert!(is_excluded_command("flyctl auth login", &[]));
1266    }
1267
1268    #[test]
1269    fn auth_exclusion_does_not_affect_normal_commands() {
1270        assert!(!is_excluded_command("git log", &[]));
1271        assert!(!is_excluded_command("npm run build", &[]));
1272        assert!(!is_excluded_command("cargo test", &[]));
1273        assert!(!is_excluded_command("aws s3 ls", &[]));
1274        assert!(!is_excluded_command("gcloud compute instances list", &[]));
1275        assert!(!is_excluded_command("az vm list", &[]));
1276    }
1277
1278    #[test]
1279    fn npm_script_runners_are_passthrough() {
1280        assert!(is_excluded_command("npm run dev", &[]));
1281        assert!(is_excluded_command("npm run start", &[]));
1282        assert!(is_excluded_command("npm run serve", &[]));
1283        assert!(is_excluded_command("npm run watch", &[]));
1284        assert!(is_excluded_command("npm run preview", &[]));
1285        assert!(is_excluded_command("npm run storybook", &[]));
1286        assert!(is_excluded_command("npm run test:watch", &[]));
1287        assert!(is_excluded_command("npm start", &[]));
1288        assert!(is_excluded_command("npx vite", &[]));
1289        assert!(is_excluded_command("npx next dev", &[]));
1290    }
1291
1292    #[test]
1293    fn pnpm_script_runners_are_passthrough() {
1294        assert!(is_excluded_command("pnpm run dev", &[]));
1295        assert!(is_excluded_command("pnpm run start", &[]));
1296        assert!(is_excluded_command("pnpm run serve", &[]));
1297        assert!(is_excluded_command("pnpm run watch", &[]));
1298        assert!(is_excluded_command("pnpm run preview", &[]));
1299        assert!(is_excluded_command("pnpm dev", &[]));
1300        assert!(is_excluded_command("pnpm start", &[]));
1301        assert!(is_excluded_command("pnpm preview", &[]));
1302    }
1303
1304    #[test]
1305    fn yarn_script_runners_are_passthrough() {
1306        assert!(is_excluded_command("yarn dev", &[]));
1307        assert!(is_excluded_command("yarn start", &[]));
1308        assert!(is_excluded_command("yarn serve", &[]));
1309        assert!(is_excluded_command("yarn watch", &[]));
1310        assert!(is_excluded_command("yarn preview", &[]));
1311        assert!(is_excluded_command("yarn storybook", &[]));
1312    }
1313
1314    #[test]
1315    fn bun_deno_script_runners_are_passthrough() {
1316        assert!(is_excluded_command("bun run dev", &[]));
1317        assert!(is_excluded_command("bun run start", &[]));
1318        assert!(is_excluded_command("bun run serve", &[]));
1319        assert!(is_excluded_command("bun run watch", &[]));
1320        assert!(is_excluded_command("bun run preview", &[]));
1321        assert!(is_excluded_command("bun start", &[]));
1322        assert!(is_excluded_command("deno task dev", &[]));
1323        assert!(is_excluded_command("deno task start", &[]));
1324        assert!(is_excluded_command("deno task serve", &[]));
1325        assert!(is_excluded_command("deno run --watch main.ts", &[]));
1326    }
1327
1328    #[test]
1329    fn python_servers_are_passthrough() {
1330        assert!(is_excluded_command("flask run --port 5000", &[]));
1331        assert!(is_excluded_command("uvicorn app:app --reload", &[]));
1332        assert!(is_excluded_command("gunicorn app:app -w 4", &[]));
1333        assert!(is_excluded_command("hypercorn app:app", &[]));
1334        assert!(is_excluded_command("daphne app.asgi:application", &[]));
1335        assert!(is_excluded_command(
1336            "django-admin runserver 0.0.0.0:8000",
1337            &[]
1338        ));
1339        assert!(is_excluded_command("python manage.py runserver", &[]));
1340        assert!(is_excluded_command("python -m http.server 8080", &[]));
1341        assert!(is_excluded_command("python3 -m http.server", &[]));
1342        assert!(is_excluded_command("streamlit run app.py", &[]));
1343        assert!(is_excluded_command("gradio app.py", &[]));
1344        assert!(is_excluded_command("celery worker -A app", &[]));
1345        assert!(is_excluded_command("celery -A app worker", &[]));
1346        assert!(is_excluded_command("celery -B", &[]));
1347        assert!(is_excluded_command("dramatiq tasks", &[]));
1348        assert!(is_excluded_command("rq worker", &[]));
1349        assert!(is_excluded_command("ptw tests/", &[]));
1350        assert!(is_excluded_command("pytest-watch", &[]));
1351    }
1352
1353    #[test]
1354    fn ruby_servers_are_passthrough() {
1355        assert!(is_excluded_command("rails server -p 3000", &[]));
1356        assert!(is_excluded_command("rails s", &[]));
1357        assert!(is_excluded_command("puma -C config.rb", &[]));
1358        assert!(is_excluded_command("unicorn -c config.rb", &[]));
1359        assert!(is_excluded_command("thin start", &[]));
1360        assert!(is_excluded_command("foreman start", &[]));
1361        assert!(is_excluded_command("overmind start", &[]));
1362        assert!(is_excluded_command("guard -G Guardfile", &[]));
1363        assert!(is_excluded_command("sidekiq", &[]));
1364        assert!(is_excluded_command("resque work", &[]));
1365    }
1366
1367    #[test]
1368    fn php_servers_are_passthrough() {
1369        assert!(is_excluded_command("php artisan serve", &[]));
1370        assert!(is_excluded_command("php -S localhost:8000", &[]));
1371        assert!(is_excluded_command("php artisan queue:work", &[]));
1372        assert!(is_excluded_command("php artisan queue:listen", &[]));
1373        assert!(is_excluded_command("php artisan horizon", &[]));
1374        assert!(is_excluded_command("php artisan tinker", &[]));
1375        assert!(is_excluded_command("sail up", &[]));
1376    }
1377
1378    #[test]
1379    fn java_servers_are_passthrough() {
1380        assert!(is_excluded_command("./gradlew bootRun", &[]));
1381        assert!(is_excluded_command("gradlew bootRun", &[]));
1382        assert!(is_excluded_command("gradle bootRun", &[]));
1383        assert!(is_excluded_command("mvn spring-boot:run", &[]));
1384        assert!(is_excluded_command("./mvnw spring-boot:run", &[]));
1385        assert!(is_excluded_command("mvn quarkus:dev", &[]));
1386        assert!(is_excluded_command("./mvnw quarkus:dev", &[]));
1387        assert!(is_excluded_command("sbt run", &[]));
1388        assert!(is_excluded_command("sbt ~compile", &[]));
1389        assert!(is_excluded_command("lein run", &[]));
1390        assert!(is_excluded_command("lein repl", &[]));
1391        assert!(is_excluded_command("./gradlew run", &[]));
1392    }
1393
1394    #[test]
1395    fn go_servers_are_passthrough() {
1396        assert!(is_excluded_command("go run main.go", &[]));
1397        assert!(is_excluded_command("go run ./cmd/server", &[]));
1398        assert!(is_excluded_command("air -c .air.toml", &[]));
1399        assert!(is_excluded_command("gin --port 3000", &[]));
1400        assert!(is_excluded_command("realize start", &[]));
1401        assert!(is_excluded_command("reflex -r '.go$' go run .", &[]));
1402        assert!(is_excluded_command("gowatch run", &[]));
1403    }
1404
1405    #[test]
1406    fn dotnet_servers_are_passthrough() {
1407        assert!(is_excluded_command("dotnet run", &[]));
1408        assert!(is_excluded_command("dotnet run --project src/Api", &[]));
1409        assert!(is_excluded_command("dotnet watch run", &[]));
1410        assert!(is_excluded_command("dotnet ef database update", &[]));
1411    }
1412
1413    #[test]
1414    fn elixir_servers_are_passthrough() {
1415        assert!(is_excluded_command("mix phx.server", &[]));
1416        assert!(is_excluded_command("iex -s mix phx.server", &[]));
1417        assert!(is_excluded_command("iex -S mix phx.server", &[]));
1418    }
1419
1420    #[test]
1421    fn swift_zig_servers_are_passthrough() {
1422        assert!(is_excluded_command("swift run MyApp", &[]));
1423        assert!(is_excluded_command("swift package resolve", &[]));
1424        assert!(is_excluded_command("vapor serve --port 8080", &[]));
1425        assert!(is_excluded_command("zig build run", &[]));
1426    }
1427
1428    #[test]
1429    fn rust_watchers_are_passthrough() {
1430        assert!(is_excluded_command("cargo watch -x test", &[]));
1431        assert!(is_excluded_command("cargo run --bin server", &[]));
1432        assert!(is_excluded_command("cargo leptos watch", &[]));
1433        assert!(is_excluded_command("bacon test", &[]));
1434    }
1435
1436    #[test]
1437    fn general_task_runners_are_passthrough() {
1438        assert!(is_excluded_command("make dev", &[]));
1439        assert!(is_excluded_command("make serve", &[]));
1440        assert!(is_excluded_command("make watch", &[]));
1441        assert!(is_excluded_command("make run", &[]));
1442        assert!(is_excluded_command("make start", &[]));
1443        assert!(is_excluded_command("just dev", &[]));
1444        assert!(is_excluded_command("just serve", &[]));
1445        assert!(is_excluded_command("just watch", &[]));
1446        assert!(is_excluded_command("just start", &[]));
1447        assert!(is_excluded_command("just run", &[]));
1448        assert!(is_excluded_command("task dev", &[]));
1449        assert!(is_excluded_command("task serve", &[]));
1450        assert!(is_excluded_command("task watch", &[]));
1451        assert!(is_excluded_command("nix develop", &[]));
1452        assert!(is_excluded_command("devenv up", &[]));
1453    }
1454
1455    #[test]
1456    fn cicd_infra_are_passthrough() {
1457        assert!(is_excluded_command("act push", &[]));
1458        assert!(is_excluded_command("docker compose watch", &[]));
1459        assert!(is_excluded_command("docker-compose watch", &[]));
1460        assert!(is_excluded_command("skaffold dev", &[]));
1461        assert!(is_excluded_command("tilt up", &[]));
1462        assert!(is_excluded_command("garden dev", &[]));
1463        assert!(is_excluded_command("telepresence connect", &[]));
1464    }
1465
1466    #[test]
1467    fn networking_monitoring_are_passthrough() {
1468        assert!(is_excluded_command("mtr 8.8.8.8", &[]));
1469        assert!(is_excluded_command("nmap -sV host", &[]));
1470        assert!(is_excluded_command("iperf -s", &[]));
1471        assert!(is_excluded_command("iperf3 -c host", &[]));
1472        assert!(is_excluded_command("socat TCP-LISTEN:8080,fork -", &[]));
1473    }
1474
1475    #[test]
1476    fn load_testing_is_passthrough() {
1477        assert!(is_excluded_command("ab -n 1000 http://localhost/", &[]));
1478        assert!(is_excluded_command("wrk -t12 -c400 http://localhost/", &[]));
1479        assert!(is_excluded_command("hey -n 10000 http://localhost/", &[]));
1480        assert!(is_excluded_command("vegeta attack", &[]));
1481        assert!(is_excluded_command("k6 run script.js", &[]));
1482        assert!(is_excluded_command("artillery run test.yml", &[]));
1483    }
1484
1485    #[test]
1486    fn smart_script_detection_works() {
1487        assert!(is_excluded_command("npm run dev:ssr", &[]));
1488        assert!(is_excluded_command("npm run dev:local", &[]));
1489        assert!(is_excluded_command("yarn start:production", &[]));
1490        assert!(is_excluded_command("pnpm run serve:local", &[]));
1491        assert!(is_excluded_command("bun run watch:css", &[]));
1492        assert!(is_excluded_command("deno task dev:api", &[]));
1493        assert!(is_excluded_command("npm run storybook:ci", &[]));
1494        assert!(is_excluded_command("yarn preview:staging", &[]));
1495        assert!(is_excluded_command("pnpm run hot-reload", &[]));
1496        assert!(is_excluded_command("npm run hmr-server", &[]));
1497        assert!(is_excluded_command("bun run live-server", &[]));
1498    }
1499
1500    #[test]
1501    fn smart_detection_does_not_false_positive() {
1502        assert!(!is_excluded_command("npm run build", &[]));
1503        assert!(!is_excluded_command("npm run lint", &[]));
1504        assert!(!is_excluded_command("npm run test", &[]));
1505        assert!(!is_excluded_command("npm run format", &[]));
1506        assert!(!is_excluded_command("yarn build", &[]));
1507        assert!(!is_excluded_command("yarn test", &[]));
1508        assert!(!is_excluded_command("pnpm run lint", &[]));
1509        assert!(!is_excluded_command("bun run build", &[]));
1510    }
1511
1512    #[test]
1513    fn gh_fully_excluded() {
1514        assert!(is_excluded_command("gh", &[]));
1515        assert!(is_excluded_command(
1516            "gh pr close --comment 'closing — see #407'",
1517            &[]
1518        ));
1519        assert!(is_excluded_command(
1520            "gh issue create --title \"bug\" --body \"desc\"",
1521            &[]
1522        ));
1523        assert!(is_excluded_command("gh api repos/owner/repo/pulls", &[]));
1524        assert!(is_excluded_command("gh run list --limit 5", &[]));
1525    }
1526}
1527
1528#[cfg(test)]
1529mod verbatim_output_tests {
1530    use super::{compress_if_beneficial, is_verbatim_output};
1531
1532    #[test]
1533    fn http_clients_are_verbatim() {
1534        assert!(is_verbatim_output("curl https://api.example.com"));
1535        assert!(is_verbatim_output(
1536            "curl -s -H 'Accept: application/json' https://api.example.com/data"
1537        ));
1538        assert!(is_verbatim_output(
1539            "curl -X POST -d '{\"key\":\"val\"}' https://api.example.com"
1540        ));
1541        assert!(is_verbatim_output("/usr/bin/curl https://example.com"));
1542        assert!(is_verbatim_output("wget -qO- https://example.com"));
1543        assert!(is_verbatim_output("wget https://example.com/file.json"));
1544        assert!(is_verbatim_output("http GET https://api.example.com"));
1545        assert!(is_verbatim_output("https PUT https://api.example.com/data"));
1546        assert!(is_verbatim_output("xh https://api.example.com"));
1547        assert!(is_verbatim_output("curlie https://api.example.com"));
1548        assert!(is_verbatim_output(
1549            "grpcurl -plaintext localhost:50051 list"
1550        ));
1551    }
1552
1553    #[test]
1554    fn file_viewers_are_verbatim() {
1555        assert!(is_verbatim_output("cat package.json"));
1556        assert!(is_verbatim_output("cat /etc/hosts"));
1557        assert!(is_verbatim_output("/bin/cat file.txt"));
1558        assert!(is_verbatim_output("bat src/main.rs"));
1559        assert!(is_verbatim_output("batcat README.md"));
1560        assert!(is_verbatim_output("head -20 log.txt"));
1561        assert!(is_verbatim_output("head -n 50 file.rs"));
1562        assert!(is_verbatim_output("tail -100 server.log"));
1563        assert!(is_verbatim_output("tail -n 20 file.txt"));
1564    }
1565
1566    #[test]
1567    fn tail_follow_not_verbatim() {
1568        assert!(!is_verbatim_output("tail -f /var/log/syslog"));
1569        assert!(!is_verbatim_output("tail --follow server.log"));
1570    }
1571
1572    #[test]
1573    fn data_format_tools_are_verbatim() {
1574        assert!(is_verbatim_output("jq '.items' data.json"));
1575        assert!(is_verbatim_output("jq -r '.name' package.json"));
1576        assert!(is_verbatim_output("yq '.spec' deployment.yaml"));
1577        assert!(is_verbatim_output("xq '.rss.channel.title' feed.xml"));
1578        assert!(is_verbatim_output("fx data.json"));
1579        assert!(is_verbatim_output("gron data.json"));
1580        assert!(is_verbatim_output("mlr --csv head -n 5 data.csv"));
1581        assert!(is_verbatim_output("miller --json head data.json"));
1582        assert!(is_verbatim_output("dasel -f config.toml '.database.host'"));
1583        assert!(is_verbatim_output("csvlook data.csv"));
1584        assert!(is_verbatim_output("csvcut -c 1,3 data.csv"));
1585        assert!(is_verbatim_output("csvjson data.csv"));
1586    }
1587
1588    #[test]
1589    fn binary_viewers_are_verbatim() {
1590        assert!(is_verbatim_output("xxd binary.dat"));
1591        assert!(is_verbatim_output("hexdump -C binary.dat"));
1592        assert!(is_verbatim_output("od -A x -t x1z binary.dat"));
1593        assert!(is_verbatim_output("strings /usr/bin/curl"));
1594        assert!(is_verbatim_output("file unknown.bin"));
1595    }
1596
1597    #[test]
1598    fn infra_inspection_is_verbatim() {
1599        assert!(is_verbatim_output("terraform output"));
1600        assert!(is_verbatim_output("terraform show"));
1601        assert!(is_verbatim_output("terraform state show aws_instance.web"));
1602        assert!(is_verbatim_output("terraform state list"));
1603        assert!(is_verbatim_output("terraform state pull"));
1604        assert!(is_verbatim_output("tofu output"));
1605        assert!(is_verbatim_output("tofu show"));
1606        assert!(is_verbatim_output("pulumi stack output"));
1607        assert!(is_verbatim_output("pulumi stack export"));
1608        assert!(is_verbatim_output("docker inspect my-container"));
1609        assert!(is_verbatim_output("podman inspect my-pod"));
1610        assert!(is_verbatim_output("kubectl get pods -o yaml"));
1611        assert!(is_verbatim_output("kubectl get deploy -ojson"));
1612        assert!(is_verbatim_output("kubectl get svc --output yaml"));
1613        assert!(is_verbatim_output("kubectl get pods --output=json"));
1614        assert!(is_verbatim_output("k get pods -o yaml"));
1615        assert!(is_verbatim_output("kubectl describe pod my-pod"));
1616        assert!(is_verbatim_output("k describe deployment web"));
1617        assert!(is_verbatim_output("helm get values my-release"));
1618        assert!(is_verbatim_output("helm template my-chart"));
1619    }
1620
1621    #[test]
1622    fn terraform_plan_not_verbatim() {
1623        assert!(!is_verbatim_output("terraform plan"));
1624        assert!(!is_verbatim_output("terraform apply"));
1625        assert!(!is_verbatim_output("terraform init"));
1626    }
1627
1628    #[test]
1629    fn kubectl_get_is_now_verbatim() {
1630        assert!(is_verbatim_output("kubectl get pods"));
1631        assert!(is_verbatim_output("kubectl get deployments"));
1632    }
1633
1634    #[test]
1635    fn crypto_commands_are_verbatim() {
1636        assert!(is_verbatim_output("openssl x509 -in cert.pem -text"));
1637        assert!(is_verbatim_output(
1638            "openssl s_client -connect example.com:443"
1639        ));
1640        assert!(is_verbatim_output("openssl req -new -x509 -key key.pem"));
1641        assert!(is_verbatim_output("gpg --list-keys"));
1642        assert!(is_verbatim_output("ssh-keygen -l -f key.pub"));
1643    }
1644
1645    #[test]
1646    fn database_queries_are_verbatim() {
1647        assert!(is_verbatim_output(r#"psql -c "SELECT * FROM users" mydb"#));
1648        assert!(is_verbatim_output("psql --command 'SELECT 1' mydb"));
1649        assert!(is_verbatim_output(r#"mysql -e "SELECT * FROM users" mydb"#));
1650        assert!(is_verbatim_output("mysql --execute 'SHOW TABLES' mydb"));
1651        assert!(is_verbatim_output(
1652            r#"mariadb -e "SELECT * FROM users" mydb"#
1653        ));
1654        assert!(is_verbatim_output(
1655            r#"sqlite3 data.db "SELECT * FROM users""#
1656        ));
1657        assert!(is_verbatim_output("mongosh --eval 'db.users.find()' mydb"));
1658    }
1659
1660    #[test]
1661    fn interactive_db_not_verbatim() {
1662        assert!(!is_verbatim_output("psql mydb"));
1663        assert!(!is_verbatim_output("mysql -u root mydb"));
1664    }
1665
1666    #[test]
1667    fn dns_network_inspection_is_verbatim() {
1668        assert!(is_verbatim_output("dig example.com"));
1669        assert!(is_verbatim_output("dig +short example.com A"));
1670        assert!(is_verbatim_output("nslookup example.com"));
1671        assert!(is_verbatim_output("host example.com"));
1672        assert!(is_verbatim_output("whois example.com"));
1673        assert!(is_verbatim_output("drill example.com"));
1674    }
1675
1676    #[test]
1677    fn language_one_liners_are_verbatim() {
1678        assert!(is_verbatim_output(
1679            "python -c 'import json; print(json.dumps({\"key\": \"value\"}))'"
1680        ));
1681        assert!(is_verbatim_output("python3 -c 'print(42)'"));
1682        assert!(is_verbatim_output(
1683            "node -e 'console.log(JSON.stringify({a:1}))'"
1684        ));
1685        assert!(is_verbatim_output("node --eval 'console.log(1)'"));
1686        assert!(is_verbatim_output("ruby -e 'puts 42'"));
1687        assert!(is_verbatim_output("perl -e 'print 42'"));
1688        assert!(is_verbatim_output("php -r 'echo json_encode([1,2,3]);'"));
1689    }
1690
1691    #[test]
1692    fn language_scripts_not_verbatim() {
1693        assert!(!is_verbatim_output("python script.py"));
1694        assert!(!is_verbatim_output("node server.js"));
1695        assert!(!is_verbatim_output("ruby app.rb"));
1696    }
1697
1698    #[test]
1699    fn container_listings_are_verbatim() {
1700        assert!(is_verbatim_output("docker ps"));
1701        assert!(is_verbatim_output("docker ps -a"));
1702        assert!(is_verbatim_output("docker images"));
1703        assert!(is_verbatim_output("docker images -a"));
1704        assert!(is_verbatim_output("podman ps"));
1705        assert!(is_verbatim_output("podman images"));
1706        assert!(is_verbatim_output("kubectl get pods"));
1707        assert!(is_verbatim_output("kubectl get deployments -A"));
1708        assert!(is_verbatim_output("kubectl get svc --all-namespaces"));
1709        assert!(is_verbatim_output("k get pods"));
1710        assert!(is_verbatim_output("helm list"));
1711        assert!(is_verbatim_output("helm ls --all-namespaces"));
1712        assert!(is_verbatim_output("docker compose ps"));
1713        assert!(is_verbatim_output("docker-compose ps"));
1714    }
1715
1716    #[test]
1717    fn file_listings_are_verbatim() {
1718        assert!(is_verbatim_output("find . -name '*.rs'"));
1719        assert!(is_verbatim_output("find /var/log -type f"));
1720        assert!(is_verbatim_output("fd --extension rs"));
1721        assert!(is_verbatim_output("fdfind .rs src/"));
1722        assert!(is_verbatim_output("ls -la"));
1723        assert!(is_verbatim_output("ls -lah /tmp"));
1724        assert!(is_verbatim_output("exa -la"));
1725        assert!(is_verbatim_output("eza --long"));
1726    }
1727
1728    #[test]
1729    fn system_queries_are_verbatim() {
1730        assert!(is_verbatim_output("stat file.txt"));
1731        assert!(is_verbatim_output("wc -l file.txt"));
1732        assert!(is_verbatim_output("du -sh /var"));
1733        assert!(is_verbatim_output("df -h"));
1734        assert!(is_verbatim_output("free -m"));
1735        assert!(is_verbatim_output("uname -a"));
1736        assert!(is_verbatim_output("id"));
1737        assert!(is_verbatim_output("whoami"));
1738        assert!(is_verbatim_output("hostname"));
1739        assert!(is_verbatim_output("which python3"));
1740        assert!(is_verbatim_output("readlink -f ./link"));
1741        assert!(is_verbatim_output("sha256sum file.tar.gz"));
1742        assert!(is_verbatim_output("base64 file.bin"));
1743        assert!(is_verbatim_output("ip addr show"));
1744        assert!(is_verbatim_output("ss -tlnp"));
1745    }
1746
1747    #[test]
1748    fn pipe_tail_detection() {
1749        assert!(
1750            is_verbatim_output("kubectl get pods -o json | jq '.items[].metadata.name'"),
1751            "piped to jq must be verbatim"
1752        );
1753        assert!(
1754            is_verbatim_output("aws s3api list-objects --bucket x | jq '.Contents'"),
1755            "piped to jq must be verbatim"
1756        );
1757        assert!(
1758            is_verbatim_output("docker inspect web | head -50"),
1759            "piped to head must be verbatim"
1760        );
1761        assert!(
1762            is_verbatim_output("terraform state pull | jq '.resources'"),
1763            "piped to jq must be verbatim"
1764        );
1765        assert!(
1766            is_verbatim_output("echo hello | wc -l"),
1767            "piped to wc (system query) should be verbatim"
1768        );
1769    }
1770
1771    #[test]
1772    fn build_commands_not_verbatim() {
1773        assert!(!is_verbatim_output("cargo build"));
1774        assert!(!is_verbatim_output("npm run build"));
1775        assert!(!is_verbatim_output("make"));
1776        assert!(!is_verbatim_output("docker build ."));
1777        assert!(!is_verbatim_output("go build ./..."));
1778        assert!(!is_verbatim_output("cargo test"));
1779        assert!(!is_verbatim_output("pytest"));
1780        assert!(!is_verbatim_output("npm install"));
1781        assert!(!is_verbatim_output("pip install requests"));
1782        assert!(!is_verbatim_output("terraform plan"));
1783        assert!(!is_verbatim_output("terraform apply"));
1784    }
1785
1786    #[test]
1787    fn cloud_cli_queries_are_verbatim() {
1788        assert!(is_verbatim_output("aws sts get-caller-identity"));
1789        assert!(is_verbatim_output("aws ec2 describe-instances"));
1790        assert!(is_verbatim_output(
1791            "aws s3api list-objects --bucket my-bucket"
1792        ));
1793        assert!(is_verbatim_output("aws iam list-users"));
1794        assert!(is_verbatim_output("aws ecs describe-tasks --cluster x"));
1795        assert!(is_verbatim_output("aws rds describe-db-instances"));
1796        assert!(is_verbatim_output("gcloud compute instances list"));
1797        assert!(is_verbatim_output("gcloud projects describe my-project"));
1798        assert!(is_verbatim_output("gcloud iam roles list"));
1799        assert!(is_verbatim_output("gcloud container clusters list"));
1800        assert!(is_verbatim_output("az vm list"));
1801        assert!(is_verbatim_output("az account show"));
1802        assert!(is_verbatim_output("az network nsg list"));
1803        assert!(is_verbatim_output("az aks show --name mycluster"));
1804    }
1805
1806    #[test]
1807    fn cloud_cli_mutations_not_verbatim() {
1808        assert!(!is_verbatim_output("aws configure"));
1809        assert!(!is_verbatim_output("gcloud auth login"));
1810        assert!(!is_verbatim_output("az login"));
1811        assert!(!is_verbatim_output("gcloud app deploy"));
1812    }
1813
1814    #[test]
1815    fn package_manager_info_is_verbatim() {
1816        assert!(is_verbatim_output("npm list"));
1817        assert!(is_verbatim_output("npm ls --all"));
1818        assert!(is_verbatim_output("npm info react"));
1819        assert!(is_verbatim_output("npm view react versions"));
1820        assert!(is_verbatim_output("npm outdated"));
1821        assert!(is_verbatim_output("npm audit"));
1822        assert!(is_verbatim_output("yarn list"));
1823        assert!(is_verbatim_output("yarn info react"));
1824        assert!(is_verbatim_output("yarn why react"));
1825        assert!(is_verbatim_output("yarn audit"));
1826        assert!(is_verbatim_output("pnpm list"));
1827        assert!(is_verbatim_output("pnpm why react"));
1828        assert!(is_verbatim_output("pnpm outdated"));
1829        assert!(is_verbatim_output("pip list"));
1830        assert!(is_verbatim_output("pip show requests"));
1831        assert!(is_verbatim_output("pip freeze"));
1832        assert!(is_verbatim_output("pip3 list"));
1833        assert!(is_verbatim_output("gem list"));
1834        assert!(is_verbatim_output("gem info rails"));
1835        assert!(is_verbatim_output("cargo metadata"));
1836        assert!(is_verbatim_output("cargo tree"));
1837        assert!(is_verbatim_output("go list ./..."));
1838        assert!(is_verbatim_output("go version"));
1839        assert!(is_verbatim_output("composer show"));
1840        assert!(is_verbatim_output("composer outdated"));
1841        assert!(is_verbatim_output("brew list"));
1842        assert!(is_verbatim_output("brew info node"));
1843        assert!(is_verbatim_output("brew deps node"));
1844        assert!(is_verbatim_output("apt list --installed"));
1845        assert!(is_verbatim_output("apt show nginx"));
1846        assert!(is_verbatim_output("dpkg -l"));
1847        assert!(is_verbatim_output("dpkg -s nginx"));
1848    }
1849
1850    #[test]
1851    fn package_manager_install_not_verbatim() {
1852        assert!(!is_verbatim_output("npm install"));
1853        assert!(!is_verbatim_output("yarn add react"));
1854        assert!(!is_verbatim_output("pip install requests"));
1855        assert!(!is_verbatim_output("cargo build"));
1856        assert!(!is_verbatim_output("go build"));
1857        assert!(!is_verbatim_output("brew install node"));
1858        assert!(!is_verbatim_output("apt install nginx"));
1859    }
1860
1861    #[test]
1862    fn version_and_help_are_verbatim() {
1863        assert!(is_verbatim_output("node --version"));
1864        assert!(is_verbatim_output("python3 --version"));
1865        assert!(is_verbatim_output("rustc -V"));
1866        assert!(is_verbatim_output("docker version"));
1867        assert!(is_verbatim_output("git --version"));
1868        assert!(is_verbatim_output("cargo --help"));
1869        assert!(is_verbatim_output("docker help"));
1870        assert!(is_verbatim_output("git -h"));
1871        assert!(is_verbatim_output("npm help install"));
1872    }
1873
1874    #[test]
1875    fn version_flag_needs_binary_context() {
1876        assert!(!is_verbatim_output("--version"));
1877        assert!(
1878            !is_verbatim_output("some command with --version and other args too"),
1879            "commands with 4+ tokens should not match version check"
1880        );
1881    }
1882
1883    #[test]
1884    fn config_viewers_are_verbatim() {
1885        assert!(is_verbatim_output("git config --list"));
1886        assert!(is_verbatim_output("git config --global --list"));
1887        assert!(is_verbatim_output("git config user.email"));
1888        assert!(is_verbatim_output("npm config list"));
1889        assert!(is_verbatim_output("npm config get registry"));
1890        assert!(is_verbatim_output("yarn config list"));
1891        assert!(is_verbatim_output("pip config list"));
1892        assert!(is_verbatim_output("rustup show"));
1893        assert!(is_verbatim_output("rustup target list"));
1894        assert!(is_verbatim_output("docker context ls"));
1895        assert!(is_verbatim_output("kubectl config view"));
1896        assert!(is_verbatim_output("kubectl config get-contexts"));
1897        assert!(is_verbatim_output("kubectl config current-context"));
1898    }
1899
1900    #[test]
1901    fn config_setters_not_verbatim() {
1902        assert!(!is_verbatim_output("git config --set user.name foo"));
1903        assert!(!is_verbatim_output("git config --unset user.name"));
1904    }
1905
1906    #[test]
1907    fn log_viewers_are_verbatim() {
1908        assert!(is_verbatim_output("journalctl -u nginx"));
1909        assert!(is_verbatim_output("journalctl --since '1 hour ago'"));
1910        assert!(is_verbatim_output("dmesg"));
1911        assert!(is_verbatim_output("dmesg --level=err"));
1912        assert!(is_verbatim_output("docker logs mycontainer"));
1913        assert!(is_verbatim_output("docker logs --tail 100 web"));
1914        assert!(is_verbatim_output("kubectl logs pod/web"));
1915        assert!(is_verbatim_output("docker compose logs web"));
1916    }
1917
1918    #[test]
1919    fn follow_logs_not_verbatim() {
1920        assert!(!is_verbatim_output("journalctl -f"));
1921        assert!(!is_verbatim_output("journalctl --follow -u nginx"));
1922        assert!(!is_verbatim_output("dmesg -w"));
1923        assert!(!is_verbatim_output("dmesg --follow"));
1924        assert!(!is_verbatim_output("docker logs -f web"));
1925        assert!(!is_verbatim_output("kubectl logs -f pod/web"));
1926        assert!(!is_verbatim_output("docker compose logs -f"));
1927    }
1928
1929    #[test]
1930    fn archive_listings_are_verbatim() {
1931        assert!(is_verbatim_output("tar -tf archive.tar.gz"));
1932        assert!(is_verbatim_output("tar tf archive.tar"));
1933        assert!(is_verbatim_output("unzip -l archive.zip"));
1934        assert!(is_verbatim_output("zipinfo archive.zip"));
1935        assert!(is_verbatim_output("lsar archive.7z"));
1936    }
1937
1938    #[test]
1939    fn clipboard_tools_are_verbatim() {
1940        assert!(is_verbatim_output("pbpaste"));
1941        assert!(is_verbatim_output("wl-paste"));
1942        assert!(is_verbatim_output("xclip -o"));
1943        assert!(is_verbatim_output("xclip -selection clipboard -o"));
1944        assert!(is_verbatim_output("xsel -o"));
1945        assert!(is_verbatim_output("xsel --output"));
1946    }
1947
1948    #[test]
1949    fn git_data_commands_are_verbatim() {
1950        assert!(is_verbatim_output("git remote -v"));
1951        assert!(is_verbatim_output("git remote show origin"));
1952        assert!(is_verbatim_output("git config --list"));
1953        assert!(is_verbatim_output("git rev-parse HEAD"));
1954        assert!(is_verbatim_output("git rev-parse --show-toplevel"));
1955        assert!(is_verbatim_output("git ls-files"));
1956        assert!(is_verbatim_output("git ls-tree HEAD"));
1957        assert!(is_verbatim_output("git ls-remote origin"));
1958        assert!(is_verbatim_output("git shortlog -sn"));
1959        assert!(is_verbatim_output("git for-each-ref --format='%(refname)'"));
1960        assert!(is_verbatim_output("git cat-file -p HEAD"));
1961        assert!(is_verbatim_output("git describe --tags"));
1962        assert!(is_verbatim_output("git merge-base main feature"));
1963    }
1964
1965    #[test]
1966    fn git_mutations_not_verbatim_via_git_data() {
1967        assert!(!super::is_git_data_command("git commit -m 'fix'"));
1968        assert!(!super::is_git_data_command("git push"));
1969        assert!(!super::is_git_data_command("git pull"));
1970        assert!(!super::is_git_data_command("git fetch"));
1971        assert!(!super::is_git_data_command("git add ."));
1972        assert!(!super::is_git_data_command("git rebase main"));
1973        assert!(!super::is_git_data_command("git cherry-pick abc123"));
1974    }
1975
1976    #[test]
1977    fn task_dry_run_is_verbatim() {
1978        assert!(is_verbatim_output("make -n build"));
1979        assert!(is_verbatim_output("make --dry-run"));
1980        assert!(is_verbatim_output("ansible-playbook --check site.yml"));
1981        assert!(is_verbatim_output(
1982            "ansible-playbook --diff --check site.yml"
1983        ));
1984    }
1985
1986    #[test]
1987    fn task_execution_not_verbatim() {
1988        assert!(!is_verbatim_output("make build"));
1989        assert!(!is_verbatim_output("make"));
1990        assert!(!is_verbatim_output("ansible-playbook site.yml"));
1991    }
1992
1993    #[test]
1994    fn env_dump_is_verbatim() {
1995        assert!(is_verbatim_output("env"));
1996        assert!(is_verbatim_output("printenv"));
1997        assert!(is_verbatim_output("printenv PATH"));
1998        assert!(is_verbatim_output("locale"));
1999    }
2000
2001    #[test]
2002    fn curl_json_output_preserved() {
2003        let json = r#"{"users":[{"id":1,"name":"Alice","email":"alice@example.com"},{"id":2,"name":"Bob","email":"bob@example.com"}],"total":2,"page":1}"#;
2004        let result = compress_if_beneficial("curl https://api.example.com/users", json);
2005        assert!(
2006            result.contains("alice@example.com"),
2007            "curl JSON data must be preserved verbatim, got: {result}"
2008        );
2009        assert!(
2010            result.contains(r#""name":"Bob""#),
2011            "curl JSON data must be preserved verbatim, got: {result}"
2012        );
2013    }
2014
2015    #[test]
2016    fn curl_html_output_preserved() {
2017        let html = "<!DOCTYPE html><html><head><title>Test Page</title></head><body><h1>Hello World</h1><p>Some important content here that should not be summarized.</p></body></html>";
2018        let result = compress_if_beneficial("curl https://example.com", html);
2019        assert!(
2020            result.contains("Hello World"),
2021            "curl HTML content must be preserved, got: {result}"
2022        );
2023        assert!(
2024            result.contains("important content"),
2025            "curl HTML content must be preserved, got: {result}"
2026        );
2027    }
2028
2029    #[test]
2030    fn curl_headers_preserved() {
2031        let headers = "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nX-Request-Id: abc-123\r\nX-RateLimit-Remaining: 59\r\nContent-Length: 1234\r\nServer: nginx\r\nDate: Mon, 01 Jan 2024 00:00:00 GMT\r\n\r\n";
2032        let result = compress_if_beneficial("curl -I https://api.example.com", headers);
2033        assert!(
2034            result.contains("X-Request-Id: abc-123"),
2035            "curl headers must be preserved, got: {result}"
2036        );
2037        assert!(
2038            result.contains("X-RateLimit-Remaining"),
2039            "curl headers must be preserved, got: {result}"
2040        );
2041    }
2042
2043    #[test]
2044    fn cat_output_preserved() {
2045        let content = r#"{
2046  "name": "lean-ctx",
2047  "version": "3.5.16",
2048  "description": "Context Runtime for AI Agents",
2049  "main": "index.js",
2050  "scripts": {
2051    "build": "cargo build --release",
2052    "test": "cargo test"
2053  }
2054}"#;
2055        let result = compress_if_beneficial("cat package.json", content);
2056        assert!(
2057            result.contains(r#""version": "3.5.16""#),
2058            "cat output must be preserved, got: {result}"
2059        );
2060    }
2061
2062    #[test]
2063    fn jq_output_preserved() {
2064        let json = r#"[
2065  {"id": 1, "status": "active", "name": "Alice"},
2066  {"id": 2, "status": "inactive", "name": "Bob"},
2067  {"id": 3, "status": "active", "name": "Charlie"}
2068]"#;
2069        let result =
2070            compress_if_beneficial("jq '.[] | select(.status==\"active\")' data.json", json);
2071        assert!(
2072            result.contains("Charlie"),
2073            "jq output must be preserved, got: {result}"
2074        );
2075    }
2076
2077    #[test]
2078    fn wget_output_preserved() {
2079        let content = r#"{"key": "value", "data": [1, 2, 3]}"#;
2080        let result = compress_if_beneficial("wget -qO- https://api.example.com/data", content);
2081        assert!(
2082            result.contains(r#""data": [1, 2, 3]"#),
2083            "wget data output must be preserved, got: {result}"
2084        );
2085    }
2086
2087    #[test]
2088    fn large_curl_output_gets_truncated_not_destroyed() {
2089        let mut json = String::from("[");
2090        for i in 0..500 {
2091            if i > 0 {
2092                json.push(',');
2093            }
2094            json.push_str(&format!(
2095                r#"{{"id":{i},"name":"user_{i}","email":"user{i}@example.com","role":"admin"}}"#
2096            ));
2097        }
2098        json.push(']');
2099        let result = compress_if_beneficial("curl https://api.example.com/all-users", &json);
2100        assert!(
2101            result.contains("user_0"),
2102            "first items must be preserved in truncated output, got len: {}",
2103            result.len()
2104        );
2105        if result.contains("lines omitted") {
2106            assert!(
2107                result.contains("verbatim truncated"),
2108                "must mark as verbatim truncated, got: {result}"
2109            );
2110        }
2111    }
2112}
2113
2114#[cfg(test)]
2115mod structural_output_tests {
2116    use super::has_structural_output;
2117
2118    #[test]
2119    fn git_diff_is_structural() {
2120        assert!(has_structural_output("git diff"));
2121        assert!(has_structural_output("git diff --cached"));
2122        assert!(has_structural_output("git diff --staged"));
2123        assert!(has_structural_output("git diff HEAD~1"));
2124        assert!(has_structural_output("git diff main..feature"));
2125        assert!(has_structural_output("git diff -- src/main.rs"));
2126    }
2127
2128    #[test]
2129    fn git_show_is_structural() {
2130        assert!(has_structural_output("git show"));
2131        assert!(has_structural_output("git show HEAD"));
2132        assert!(has_structural_output("git show abc1234"));
2133        assert!(has_structural_output("git show stash@{0}"));
2134    }
2135
2136    #[test]
2137    fn git_blame_is_structural() {
2138        assert!(has_structural_output("git blame src/main.rs"));
2139        assert!(has_structural_output("git blame -L 10,20 file.rs"));
2140    }
2141
2142    #[test]
2143    fn git_with_flags_is_structural() {
2144        assert!(has_structural_output("git -C /tmp diff"));
2145        assert!(has_structural_output("git --git-dir /path diff HEAD"));
2146        assert!(has_structural_output("git -c core.pager=cat show abc"));
2147    }
2148
2149    #[test]
2150    fn case_insensitive() {
2151        assert!(has_structural_output("Git Diff"));
2152        assert!(has_structural_output("GIT DIFF --cached"));
2153        assert!(has_structural_output("git SHOW HEAD"));
2154    }
2155
2156    #[test]
2157    fn full_path_git_binary() {
2158        assert!(has_structural_output("/usr/bin/git diff"));
2159        assert!(has_structural_output("/usr/local/bin/git show HEAD"));
2160    }
2161
2162    #[test]
2163    fn standalone_diff_is_structural() {
2164        assert!(has_structural_output("diff file1.txt file2.txt"));
2165        assert!(has_structural_output("diff -u old.py new.py"));
2166        assert!(has_structural_output("diff -r dir1 dir2"));
2167        assert!(has_structural_output("/usr/bin/diff a b"));
2168        assert!(has_structural_output("colordiff file1 file2"));
2169        assert!(has_structural_output("icdiff old.rs new.rs"));
2170        assert!(has_structural_output("delta"));
2171    }
2172
2173    #[test]
2174    fn git_log_with_patch_is_structural() {
2175        assert!(has_structural_output("git log -p"));
2176        assert!(has_structural_output("git log --patch"));
2177        assert!(has_structural_output("git log -p HEAD~5"));
2178        assert!(has_structural_output("git log -p --stat"));
2179        assert!(has_structural_output("git log --patch --follow file.rs"));
2180    }
2181
2182    #[test]
2183    fn git_log_without_patch_not_structural() {
2184        assert!(!has_structural_output("git log"));
2185        assert!(!has_structural_output("git log --oneline"));
2186        assert!(!has_structural_output("git log --stat"));
2187        assert!(!has_structural_output("git log -n 5"));
2188    }
2189
2190    #[test]
2191    fn git_stash_show_is_structural() {
2192        assert!(has_structural_output("git stash show"));
2193        assert!(has_structural_output("git stash show -p"));
2194        assert!(has_structural_output("git stash show --patch"));
2195        assert!(has_structural_output("git stash show stash@{0}"));
2196    }
2197
2198    #[test]
2199    fn git_stash_without_show_not_structural() {
2200        assert!(!has_structural_output("git stash"));
2201        assert!(!has_structural_output("git stash list"));
2202        assert!(!has_structural_output("git stash pop"));
2203        assert!(!has_structural_output("git stash drop"));
2204    }
2205
2206    #[test]
2207    fn non_structural_git_commands() {
2208        assert!(!has_structural_output("git status"));
2209        assert!(!has_structural_output("git commit -m 'fix'"));
2210        assert!(!has_structural_output("git push"));
2211        assert!(!has_structural_output("git pull"));
2212        assert!(!has_structural_output("git branch"));
2213        assert!(!has_structural_output("git fetch"));
2214        assert!(!has_structural_output("git add ."));
2215    }
2216
2217    #[test]
2218    fn non_git_commands() {
2219        assert!(!has_structural_output("cargo build"));
2220        assert!(!has_structural_output("npm run build"));
2221    }
2222
2223    #[test]
2224    fn verbatim_commands_are_also_structural() {
2225        assert!(has_structural_output("ls -la"));
2226        assert!(has_structural_output("docker ps"));
2227        assert!(has_structural_output("curl https://api.example.com"));
2228        assert!(has_structural_output("cat file.txt"));
2229        assert!(has_structural_output("aws ec2 describe-instances"));
2230        assert!(has_structural_output("npm list"));
2231        assert!(has_structural_output("node --version"));
2232        assert!(has_structural_output("journalctl -u nginx"));
2233        assert!(has_structural_output("git remote -v"));
2234        assert!(has_structural_output("pbpaste"));
2235        assert!(has_structural_output("env"));
2236    }
2237
2238    #[test]
2239    fn git_diff_output_preserves_hunks() {
2240        let diff = "diff --git a/src/main.rs b/src/main.rs\n\
2241            index abc1234..def5678 100644\n\
2242            --- a/src/main.rs\n\
2243            +++ b/src/main.rs\n\
2244            @@ -1,5 +1,6 @@\n\
2245             fn main() {\n\
2246            +    println!(\"hello\");\n\
2247                 let x = 1;\n\
2248                 let y = 2;\n\
2249            -    let z = 3;\n\
2250            +    let z = x + y;\n\
2251             }";
2252        let result = super::compress_if_beneficial("git diff", diff);
2253        assert!(
2254            result.contains("+    println!"),
2255            "must preserve added lines, got: {result}"
2256        );
2257        assert!(
2258            result.contains("-    let z = 3;"),
2259            "must preserve removed lines, got: {result}"
2260        );
2261        assert!(
2262            result.contains("@@ -1,5 +1,6 @@"),
2263            "must preserve hunk headers, got: {result}"
2264        );
2265    }
2266
2267    #[test]
2268    fn git_diff_large_preserves_content() {
2269        let mut diff = String::new();
2270        diff.push_str("diff --git a/file.rs b/file.rs\n");
2271        diff.push_str("--- a/file.rs\n+++ b/file.rs\n");
2272        diff.push_str("@@ -1,100 +1,100 @@\n");
2273        for i in 0..80 {
2274            diff.push_str(&format!("+added line {i}: some actual code content\n"));
2275            diff.push_str(&format!("-removed line {i}: old code content\n"));
2276        }
2277        let result = super::compress_if_beneficial("git diff", &diff);
2278        assert!(
2279            result.contains("+added line 0"),
2280            "must preserve first added line, got len: {}",
2281            result.len()
2282        );
2283        assert!(
2284            result.contains("-removed line 0"),
2285            "must preserve first removed line, got len: {}",
2286            result.len()
2287        );
2288    }
2289}