1pub use lean_ctx_patterns::*;
9
10pub fn compress_output(command: &str, output: &str) -> Option<String> {
13 let policy = crate::shell::output_policy::classify(command, &[]);
18 if policy.is_protected() {
19 return None;
20 }
21 let command = strip_leading_cd(command);
22
23 let stripped;
24 let clean_output = {
25 let s = crate::core::compressor::strip_ansi(output);
26 if s.len() < output.len() {
27 stripped = s;
28 &stripped
29 } else {
30 output
31 }
32 };
33
34 if let Some(engine) = crate::core::filters::FilterEngine::load()
35 && let Some(filtered) = engine.apply(command, clean_output)
36 {
37 return shorter_only(filtered, output);
38 }
39
40 compress_clean(&EngineEnv, command, clean_output, output)
41}
42
43pub fn try_specific_pattern(cmd: &str, output: &str) -> Option<String> {
45 try_specific_pattern_with(&EngineEnv, cmd, output)
46}
47
48pub(crate) fn strip_leading_cd(command: &str) -> &str {
53 use crate::core::shell_allowlist::Separator;
54 let segments = crate::core::shell_allowlist::segments_with_separators(command);
55 let [(first, Some(Separator::And)), (rest, None)] = segments.as_slice() else {
56 return command;
57 };
58 let is_plain_cd = first
59 .strip_prefix("cd ")
60 .is_some_and(|dir| !dir.trim().is_empty() && !dir.contains(['$', '`', '(']));
61 if !is_plain_cd {
62 return command;
63 }
64 command
65 .rfind(rest.as_str())
66 .map_or(command, |at| &command[at..])
67}
68
69struct EngineEnv;
72
73impl PatternEnv for EngineEnv {
74 fn is_curl_passthrough(&self, command: &str) -> bool {
75 let cfg = crate::core::config::Config::load();
76 curl::is_passthrough(command, &cfg.passthrough_urls)
77 }
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83 use crate::core::tokens::count_tokens;
84
85 #[test]
86 fn routes_git_commands() {
87 let output = "On branch main\nnothing to commit";
88 assert!(compress_output("git status", output).is_some());
89 }
90
91 #[test]
92 fn vcs_path_never_inflates_tokens() {
93 let output = " Abu a\nAa aa_A00A\n\n-";
99 if let Some(compressed) = compress_output("git status", output) {
100 assert!(
101 count_tokens(&compressed) <= count_tokens(output),
102 "VCS compress inflated: {} > {}",
103 count_tokens(&compressed),
104 count_tokens(output),
105 );
106 }
107 }
108
109 #[test]
110 fn routes_cargo_commands() {
111 let output = " Compiling lean-ctx v2.1.1\n Finished `release` profile [optimized] target(s) in 30.5s";
112 assert!(compress_output("cargo build --release", output).is_some());
113 }
114
115 #[test]
116 fn routes_npm_commands() {
117 let output = "added 150 packages, and audited 151 packages in 5s\n\n25 packages are looking for funding\n run `npm fund` for details\n\nfound 0 vulnerabilities";
118 assert!(compress_output("npm install", output).is_some());
119 }
120
121 #[test]
122 fn routes_docker_commands() {
123 let output = "CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES";
124 assert!(compress_output("docker ps", output).is_none());
127 let build_output =
128 "Step 1/5 : FROM node:18\n ---> abc123\nStep 2/5 : COPY . .\nSuccessfully built def456";
129 assert!(compress_output("docker build .", build_output).is_some());
130 }
131
132 #[test]
133 fn routes_mypy_commands() {
134 let output = "src/main.py:10: error: Missing return [return]\nFound 1 error in 1 file (checked 3 source files)";
135 assert!(compress_output("mypy .", output).is_some());
136 assert!(compress_output("python -m mypy src/", output).is_some());
137 }
138
139 #[test]
140 fn routes_pytest_commands() {
141 let output = "===== test session starts =====\ncollected 5 items\ntest_main.py ..... [100%]\n===== 5 passed in 0.5s =====";
142 assert!(compress_output("pytest", output).is_some());
143 assert!(compress_output("python -m pytest tests/", output).is_some());
144 }
145
146 #[test]
147 fn routes_data_domain() {
148 let dbt =
149 "20:14:02 Found 12 models\n20:14:20 Done. PASS=11 WARN=0 ERROR=1 SKIP=0 TOTAL=12";
150 assert!(
151 compress_output("dbt run", dbt).is_some(),
152 "dbt routed+compressible"
153 );
154 let alembic = "INFO [alembic.runtime.migration] Context impl PostgresqlImpl.\nINFO [alembic.runtime.migration] Running upgrade -> a1b2c3, create users table\nINFO [alembic.runtime.migration] Running upgrade a1b2c3 -> d4e5f6, add email index";
155 assert!(compress_output("alembic upgrade head", alembic).is_some());
156 let flyway = "Flyway Community Edition 9.22.0 by Redgate\nDatabase: jdbc:postgresql://localhost/db\nMigrating schema \"public\" to version \"5 - add orders\"\nSuccessfully applied 1 migration to schema \"public\", now at version v5";
157 assert!(compress_output("flyway migrate", flyway).is_some());
158 let spark = "23/01/01 12:00:00 INFO SparkContext: Running Spark version 3.4.0\n23/01/01 12:00:01 INFO ResourceUtils: none configured\n23/01/01 12:00:10 INFO DAGScheduler: Job 0 finished: collect, took 5.1 s\n23/01/01 12:00:15 ERROR Executor: Exception in task 0.0";
159 assert!(compress_output("spark-submit app.py", spark).is_some());
160 }
161
162 #[test]
163 fn routes_ai_domain() {
164 let ollama = "NAME ID SIZE MODIFIED\nllama3.2:latest a80c4f17acd5 2.0 GB 3 days ago\nqwen2.5-coder:7b 2b0496514337 4.7 GB 2 weeks ago";
165 assert!(compress_output("ollama list", ollama).is_some());
166 let mlflow = "2024/01/01 12:00:01 INFO mlflow.projects.backend.local: === Running command 'python train.py' ===\nCollecting numpy==1.26.0\nDownloading numpy-1.26.0.whl (18.2 MB)\n2024/01/01 12:00:30 INFO mlflow.projects: === Run (ID 'abc123def456') succeeded ===";
167 assert!(compress_output("mlflow run .", mlflow).is_some());
168 }
169
170 #[test]
171 fn routes_security_domain() {
172 let trivy = "2024-01-01T12:00:00.000Z\tINFO\tscanning\nnginx:latest (debian 12.1)\n=====\nTotal: 45 (LOW: 20, HIGH: 8, CRITICAL: 2)";
173 assert!(compress_output("trivy image nginx", trivy).is_some());
174 let grype = "NAME INSTALLED FIXED-IN TYPE VULNERABILITY SEVERITY\nlibssl1.1 1.1.1n 1.1.1w deb CVE-2023-1234 Critical\nzlib1g 1.2.11 1.2.13 deb CVE-2022-5678 High";
175 assert!(compress_output("grype nginx", grype).is_some());
176 let syft = "NAME VERSION TYPE\nadduser 3.118 deb\napt 2.6.1 deb\nlodash 4.17.21 npm";
177 assert!(compress_output("syft nginx", syft).is_some());
178 let semgrep = "Scanning 120 files.\n src/app.py\n python.security.dangerous-subprocess-use\n Detected subprocess.\n 42┆ subprocess.call(x)\nRan 450 rules on 120 files: 1 findings.";
179 assert!(compress_output("semgrep scan", semgrep).is_some());
180 let swiftlint = "Linting Swift files in current working directory\nLinting 'A.swift' (1/3)\nLinting 'B.swift' (2/3)\nLinting 'C.swift' (3/3)\n/path/A.swift:10:5: warning: Line Length Violation: Line should be 120 chars or less (line_length)\n/path/A.swift:22:1: warning: Trailing Whitespace Violation: no trailing whitespace (trailing_whitespace)\n/path/B.swift:5:1: error: Force Cast Violation: avoid force casts (force_cast)\n/path/C.swift:8:3: warning: Todo Violation: resolve TODOs (todo)\nDone linting! Found 4 violations, 1 serious in 3 files.";
181 assert!(compress_output("swiftlint", swiftlint).is_some());
182 }
183
184 #[test]
185 fn routes_vcs_toolchain_domain() {
186 let jj = "@ qpvuntsm user@host.com 2024-01-01 12:00:00 1234abcd\n│ add feature x\n○ zzzzmmmm user@host.com 2024-01-01 11:00:00 main 5678efab\n│ initial commit\n~";
187 assert!(compress_output("jj log", jj).is_some());
188 let mise = "node 20.10.0 ~/.config/mise/config.toml\npython 3.12.0 ~/.tool-versions\nrust 1.75.0 ~/.config/mise/config.toml";
189 assert!(compress_output("mise ls", mise).is_some());
190 let buf_lines: Vec<String> = (0..30)
191 .map(|i| format!("proto/f{i}.proto:{i}:1:Field name should be lower_snake_case here."))
192 .collect();
193 let buf = buf_lines.join("\n");
194 assert!(compress_output("buf lint", &buf).is_some());
195 let gem = "Fetching rails-7.1.0.gem\nSuccessfully installed activesupport-7.1.0\nSuccessfully installed rails-7.1.0\nParsing documentation for rails-7.1.0\nInstalling ri documentation for rails-7.1.0\nDone installing documentation for rails after 3 seconds\n2 gems installed";
196 assert!(compress_output("gem install rails", gem).is_some());
197 let uv = "Resolved 42 packages in 120ms\nDownloading numpy (18.2MiB)\n 100%|████████| 18.2M/18.2M [00:01<00:00, 15.3MiB/s]\nDownloading pandas (12.1MiB)\n 100%|████████| 12.1M/12.1M [00:00<00:00, 14.1MiB/s]\nPrepared 5 packages in 1.2s\nInstalled 5 packages in 30ms\n + numpy==1.26.0\n + pandas==2.1.0";
198 assert!(compress_output("uv add pandas", uv).is_some());
199 }
200
201 #[test]
202 fn routes_edge_infra_domain() {
203 let pulumi = "Updating (dev):\n Type Name Status\n + pulumi:pulumi:Stack proj created\n + aws:s3:Bucket b1 created\n + aws:s3:Bucket b2 created\n + aws:s3:Bucket b3 created\n + aws:lambda:Function fn created\n\nOutputs:\n url: \"https://x.example.com\"\n\nResources:\n + 5 created\n 10 unchanged\n\nDuration: 35s";
204 assert!(compress_output("pulumi up", pulumi).is_some());
205 let linkerd = "kubernetes-api\n--------------\n√ can initialize the client\n√ can query the Kubernetes API\n√ is running the minimum kubectl version\n\nlinkerd-existence\n-----------------\n√ 'linkerd-config' config map exists\n× control plane pods are ready\n some pods are not ready\n\nStatus check results are ×";
206 assert!(compress_output("linkerd check", linkerd).is_some());
207 let argocd = "Name: argocd/myapp\nProject: default\nSync Status: Synced\nHealth Status: Healthy\n\nGROUP KIND NAMESPACE NAME STATUS HEALTH HOOK MESSAGE\n Service ns s1 Synced Healthy\n Service ns s2 Synced Healthy\n Service ns s3 Synced Healthy\napps Deployment ns d1 OutOfSync Progressing";
208 assert!(compress_output("argocd app get myapp", argocd).is_some());
209 let vercel = "Vercel CLI 33.0.0\nInstalling dependencies...\nadded 420 packages in 12s\nBuilding...\nCompiling pages\nCollecting page data\nGenerating static pages\nProduction: https://my-app.vercel.app [45s]";
210 assert!(compress_output("vercel deploy --prod", vercel).is_some());
211 let wrangler = "wrangler 3.0.0\n-------------------\nyour worker has access to the following bindings:\n- KV Namespaces:\n - CACHE: abc123\nTotal Upload: 1.2 MiB / gzip: 0.4 MiB\nUploaded my-worker (3.5 sec)\nPublished my-worker (1.2 sec)\n https://my-worker.example.workers.dev";
212 assert!(compress_output("wrangler deploy", wrangler).is_some());
213 }
214
215 #[test]
216 fn unknown_command_returns_none() {
217 assert!(compress_output("some-unknown-tool --version", "v1.0").is_none());
218 }
219
220 #[test]
221 fn case_insensitive_routing() {
222 let output = "On branch main\nnothing to commit";
223 assert!(compress_output("Git Status", output).is_some());
224 assert!(compress_output("GIT STATUS", output).is_some());
225 }
226
227 #[test]
228 fn routes_vp_and_vite_plus() {
229 let output = " VITE v5.0.0 ready in 200 ms\n\n -> Local: http://localhost:5173/\n -> Network: http://192.168.1.2:5173/";
230 assert!(compress_output("vp build", output).is_some());
231 assert!(compress_output("vite-plus build", output).is_some());
232 }
233
234 #[test]
235 fn routes_bunx_commands() {
236 let output = "1 pass tests\n0 fail tests\n3 skip tests\nDone 12ms\nsome extra line\nmore output here";
237 let result = compress_output("bunx test", output);
238 assert!(
239 result.is_some(),
240 "bunx should compress when output is large enough"
241 );
242 assert!(result.unwrap().contains("bun test: 1 passed"));
243 }
244
245 #[test]
246 fn routes_deno_task() {
247 let output = "Task dev deno run --allow-net server.ts\nListening on http://localhost:8000";
248 assert!(try_specific_pattern("deno task dev", output).is_some());
249 }
250
251 #[test]
252 fn routes_jest_commands() {
253 let output = "PASS tests/main.test.js\nTest Suites: 1 passed, 1 total\nTests: 5 passed, 5 total\nTime: 2.5 s";
254 assert!(try_specific_pattern("jest", output).is_some());
255 assert!(try_specific_pattern("npx jest --coverage", output).is_some());
256 }
257
258 #[test]
259 fn routes_mocha_commands() {
260 let output = " 3 passing (50ms)\n 1 failing\n\n 1) Array #indexOf():\n Error: expected -1 to equal 0";
261 assert!(try_specific_pattern("mocha", output).is_some());
262 assert!(try_specific_pattern("npx mocha tests/", output).is_some());
263 }
264
265 #[test]
266 fn routes_tofu_commands() {
267 let output = "Initializing the backend...\nInitializing provider plugins...\nTerraform has been successfully initialized!";
268 assert!(try_specific_pattern("tofu init", output).is_some());
269 }
270
271 #[test]
272 fn routes_ps_commands() {
273 let mut lines = vec!["USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND".to_string()];
274 for i in 0..20 {
275 lines.push(format!("user {i} 0.0 0.1 1234 123 ? S 10:00 0:00 proc_{i}"));
276 }
277 let output = lines.join("\n");
278 assert!(try_specific_pattern("ps aux", &output).is_some());
279 }
280
281 #[test]
282 fn routes_ping_commands() {
283 let output = "PING google.com (1.2.3.4): 56 data bytes\n64 bytes from 1.2.3.4: icmp_seq=0 ttl=116 time=12ms\n3 packets transmitted, 3 packets received, 0.0% packet loss\nrtt min/avg/max/stddev = 11/12/13/1 ms";
284 assert!(try_specific_pattern("ping -c 3 google.com", output).is_some());
285 }
286
287 #[test]
288 fn routes_jq_to_json_schema() {
289 let output = "{\"name\": \"test\", \"version\": \"1.0\", \"items\": [{\"id\": 1}, {\"id\": 2}, {\"id\": 3}, {\"id\": 4}, {\"id\": 5}, {\"id\": 6}, {\"id\": 7}, {\"id\": 8}, {\"id\": 9}, {\"id\": 10}]}";
290 assert!(try_specific_pattern("jq '.items' data.json", output).is_some());
291 }
292
293 #[test]
294 fn routes_linting_tools() {
295 let lint_output = "src/main.py:10: error: Missing return\nsrc/main.py:20: error: Unused var\nFound 2 errors";
296 assert!(try_specific_pattern("hadolint Dockerfile", lint_output).is_some());
297 assert!(try_specific_pattern("oxlint src/", lint_output).is_some());
298 assert!(try_specific_pattern("pyright src/", lint_output).is_some());
299 assert!(try_specific_pattern("basedpyright src/", lint_output).is_some());
300 }
301
302 #[test]
303 fn routes_fd_commands() {
304 let output = "src/main.rs\nsrc/lib.rs\nsrc/util/helpers.rs\nsrc/util/math.rs\ntests/integration.rs\n";
305 assert!(try_specific_pattern("fd --extension rs", output).is_some());
306 assert!(try_specific_pattern("fdfind .rs", output).is_some());
307 }
308
309 #[test]
310 fn routes_just_commands() {
311 let output = "Available recipes:\n build\n test\n lint\n";
312 assert!(try_specific_pattern("just --list", output).is_some());
313 assert!(try_specific_pattern("just build", output).is_some());
314 }
315
316 #[test]
317 fn routes_ninja_commands() {
318 let output = "[1/10] Compiling foo.c\n[10/10] Linking app\n";
319 assert!(try_specific_pattern("ninja", output).is_some());
320 assert!(try_specific_pattern("ninja -j4", output).is_some());
321 }
322
323 #[test]
324 fn routes_clang_commands() {
325 let output =
326 "src/main.c:10:5: error: use of undeclared identifier 'foo'\n1 error generated.\n";
327 assert!(try_specific_pattern("clang src/main.c", output).is_some());
328 assert!(try_specific_pattern("clang++ -std=c++17 main.cpp", output).is_some());
329 }
330
331 #[test]
332 fn routes_cargo_run() {
333 let output = " Compiling foo v0.1.0\n Finished `dev` profile\nHello, world!";
334 assert!(try_specific_pattern("cargo run", output).is_some());
335 }
336
337 #[test]
338 fn routes_cargo_bench() {
339 let output = " Compiling foo v0.1.0\ntest bench_parse ... bench: 1234 ns/iter";
340 assert!(try_specific_pattern("cargo bench", output).is_some());
341 }
342
343 #[test]
344 fn routes_build_tools() {
345 let build_output = " Compiling foo v0.1.0\n Finished release [optimized]";
346 assert!(try_specific_pattern("gcc -o main main.c", build_output).is_some());
347 assert!(try_specific_pattern("g++ -o main main.cpp", build_output).is_some());
348 }
349
350 #[test]
351 fn routes_monorepo_tools() {
352 let output = "npm warn deprecated inflight@1.0.6\nnpm warn deprecated rimraf@3.0.2\nadded 150 packages, and audited 151 packages in 5s\n\n25 packages are looking for funding\n run `npm fund` for details\n\nfound 0 vulnerabilities";
353 assert!(try_specific_pattern("turbo install", output).is_some());
354 assert!(try_specific_pattern("nx install", output).is_some());
355 }
356
357 #[test]
358 fn gh_api_passthrough_never_compresses() {
359 let huge = "line\n".repeat(5000);
360 assert!(
361 compress_output("gh api repos/owner/repo/actions/jobs/123/logs", &huge).is_none(),
362 "gh api must never be compressed, even for large output"
363 );
364 assert!(compress_output("gh api repos/owner/repo/actions/runs/123/logs", &huge).is_none());
365 }
366
367 #[test]
368 fn gh_log_flags_passthrough() {
369 let huge = "line\n".repeat(5000);
370 assert!(compress_output("gh run view 123 --log-failed", &huge).is_none());
371 assert!(compress_output("gh run view 123 --log", &huge).is_none());
372 }
373
374 #[test]
375 fn strip_leading_cd_unwraps_only_plain_two_segment_cd() {
376 assert_eq!(strip_leading_cd("cd /x && git log -5"), "git log -5");
377 assert_eq!(strip_leading_cd("cd 'a b' && git status"), "git status");
378 assert_eq!(strip_leading_cd("git log -5"), "git log -5");
379 for kept in [
380 "cd /x && git log && git status",
381 "cd /x; git log",
382 "cd /x || git log",
383 "cd $HOME && git log",
384 "cd $(pwd) && git log",
385 "pushd /x && git log",
386 ] {
387 assert_eq!(strip_leading_cd(kept), kept, "{kept}");
388 }
389 }
390
391 #[test]
392 fn cd_prefixed_git_log_uses_git_pattern() {
393 let mut out = String::new();
394 for i in 0..30 {
395 out.push_str(&format!(
396 "commit {i:040x}\nAuthor: A <a@b.c>\nDate: Mon Jan 1 00:00:00 2026 +0000\n\n subject {i}\n\n - bullet body line\n\n src/f{i}.rs | 4 ++--\n 1 file changed, 2 insertions(+), 2 deletions(-)\n\n"
397 ));
398 }
399 let plain = compress_output("git -C /x log --stat -30", &out);
400 let cd = compress_output("cd /x && git log --stat -30", &out);
401 assert!(plain.is_some());
402 assert_eq!(cd, plain);
403 assert!(cd.unwrap().contains("+60/-60 total"));
404 }
405
406 #[test]
407 fn gh_structured_commands_still_compress() {
408 let output = "On branch main\nnothing to commit";
409 assert!(try_specific_pattern("gh pr list", output).is_some());
410 assert!(try_specific_pattern("gh run list", output).is_some());
411 }
412}