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",
23 "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 "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 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 "kubectl logs",
106 "kubectl exec -it",
107 "kubectl exec -ti",
108 "kubectl attach",
109 "kubectl port-forward",
110 "kubectl proxy",
111 "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 "less",
137 "more",
138 "vim",
139 "nvim",
140 "vi ",
141 "nano",
142 "micro ",
143 "helix ",
144 "hx ",
145 "emacs",
146 "tmux",
148 "screen",
149 "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 "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 "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 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 "./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 run ",
222 "air ",
223 "gin ",
224 "realize start",
225 "reflex ",
226 "gowatch ",
227 "dotnet run",
229 "dotnet watch",
230 "dotnet ef",
231 "mix phx.server",
233 "iex -s mix",
234 "swift run",
236 "swift package ",
237 "vapor serve",
238 "zig build run",
240 "cargo watch",
242 "cargo run",
243 "cargo leptos watch",
244 "bacon ",
245 "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 "act ",
263 "skaffold dev",
264 "tilt up",
265 "garden dev",
266 "telepresence ",
267 "ab ",
269 "wrk ",
270 "hey ",
271 "vegeta ",
272 "k6 run",
273 "artillery run",
274 "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
410pub 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
425pub 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
458fn is_cli_api_data_command(command: &str) -> bool {
461 let cl = command.trim().to_ascii_lowercase();
462
463 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 if cl.starts_with("glab ") && cl.starts_with("glab api ") {
481 return true;
482 }
483
484 if cl.starts_with("jira ") && (cl.contains(" view") || cl.contains(" list")) {
486 return true;
487 }
488
489 if cl.starts_with("linear ") {
491 return true;
492 }
493
494 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 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 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
533fn 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
984fn first_binary(command: &str) -> &str {
986 let first = command.split_whitespace().next().unwrap_or("");
987 first.rsplit('/').next().unwrap_or(first)
988}
989
990fn 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
1000fn 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
1035fn 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 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
1155fn 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
1220pub 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}