Skip to main content

lean_ctx/shell/
compress.rs

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