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