context-compress 2026.3.21 → 2026.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/README.md +258 -44
  2. package/dist/cli/doctor.d.ts.map +1 -1
  3. package/dist/cli/doctor.js +2 -10
  4. package/dist/cli/doctor.js.map +1 -1
  5. package/dist/cli/filter.d.ts +52 -0
  6. package/dist/cli/filter.d.ts.map +1 -0
  7. package/dist/cli/filter.js +200 -0
  8. package/dist/cli/filter.js.map +1 -0
  9. package/dist/cli/index.d.ts +8 -4
  10. package/dist/cli/index.d.ts.map +1 -1
  11. package/dist/cli/index.js +19 -6
  12. package/dist/cli/index.js.map +1 -1
  13. package/dist/cli/lite.d.ts +15 -0
  14. package/dist/cli/lite.d.ts.map +1 -0
  15. package/dist/cli/lite.js +37 -0
  16. package/dist/cli/lite.js.map +1 -0
  17. package/dist/cli/setup.d.ts +23 -1
  18. package/dist/cli/setup.d.ts.map +1 -1
  19. package/dist/cli/setup.js +122 -21
  20. package/dist/cli/setup.js.map +1 -1
  21. package/dist/executor.d.ts +7 -1
  22. package/dist/executor.d.ts.map +1 -1
  23. package/dist/executor.js +51 -4
  24. package/dist/executor.js.map +1 -1
  25. package/dist/filters.d.ts +52 -0
  26. package/dist/filters.d.ts.map +1 -0
  27. package/dist/filters.js +719 -0
  28. package/dist/filters.js.map +1 -0
  29. package/dist/hooks/pretooluse.js +57 -0
  30. package/dist/hooks/pretooluse.js.map +1 -1
  31. package/dist/network.d.ts.map +1 -1
  32. package/dist/network.js +11 -0
  33. package/dist/network.js.map +1 -1
  34. package/dist/server.bundle.mjs +1333 -619
  35. package/dist/server.bundle.mjs.map +4 -4
  36. package/dist/server.d.ts.map +1 -1
  37. package/dist/server.js +44 -610
  38. package/dist/server.js.map +1 -1
  39. package/dist/stats.d.ts +7 -1
  40. package/dist/stats.d.ts.map +1 -1
  41. package/dist/stats.js +65 -0
  42. package/dist/stats.js.map +1 -1
  43. package/dist/store.d.ts +1 -0
  44. package/dist/store.d.ts.map +1 -1
  45. package/dist/store.js +15 -2
  46. package/dist/store.js.map +1 -1
  47. package/dist/tools/batch-execute.d.ts +4 -0
  48. package/dist/tools/batch-execute.d.ts.map +1 -0
  49. package/dist/tools/batch-execute.js +75 -0
  50. package/dist/tools/batch-execute.js.map +1 -0
  51. package/dist/tools/context.d.ts +17 -0
  52. package/dist/tools/context.d.ts.map +1 -0
  53. package/dist/tools/context.js +2 -0
  54. package/dist/tools/context.js.map +1 -0
  55. package/dist/tools/discover.d.ts +4 -0
  56. package/dist/tools/discover.d.ts.map +1 -0
  57. package/dist/tools/discover.js +65 -0
  58. package/dist/tools/discover.js.map +1 -0
  59. package/dist/tools/execute-file.d.ts +4 -0
  60. package/dist/tools/execute-file.d.ts.map +1 -0
  61. package/dist/tools/execute-file.js +66 -0
  62. package/dist/tools/execute-file.js.map +1 -0
  63. package/dist/tools/execute.d.ts +4 -0
  64. package/dist/tools/execute.d.ts.map +1 -0
  65. package/dist/tools/execute.js +54 -0
  66. package/dist/tools/execute.js.map +1 -0
  67. package/dist/tools/fetch-and-index.d.ts +4 -0
  68. package/dist/tools/fetch-and-index.d.ts.map +1 -0
  69. package/dist/tools/fetch-and-index.js +91 -0
  70. package/dist/tools/fetch-and-index.js.map +1 -0
  71. package/dist/tools/index-content.d.ts +4 -0
  72. package/dist/tools/index-content.d.ts.map +1 -0
  73. package/dist/tools/index-content.js +85 -0
  74. package/dist/tools/index-content.js.map +1 -0
  75. package/dist/tools/search.d.ts +4 -0
  76. package/dist/tools/search.d.ts.map +1 -0
  77. package/dist/tools/search.js +57 -0
  78. package/dist/tools/search.js.map +1 -0
  79. package/dist/tools/stats.d.ts +4 -0
  80. package/dist/tools/stats.d.ts.map +1 -0
  81. package/dist/tools/stats.js +10 -0
  82. package/dist/tools/stats.js.map +1 -0
  83. package/dist/types.d.ts +11 -0
  84. package/dist/types.d.ts.map +1 -1
  85. package/dist/util/auto-mode.d.ts +40 -0
  86. package/dist/util/auto-mode.d.ts.map +1 -0
  87. package/dist/util/auto-mode.js +181 -0
  88. package/dist/util/auto-mode.js.map +1 -0
  89. package/dist/util/fetch-code.d.ts +10 -0
  90. package/dist/util/fetch-code.d.ts.map +1 -0
  91. package/dist/util/fetch-code.js +87 -0
  92. package/dist/util/fetch-code.js.map +1 -0
  93. package/dist/util/intent-filter.d.ts +17 -0
  94. package/dist/util/intent-filter.d.ts.map +1 -0
  95. package/dist/util/intent-filter.js +28 -0
  96. package/dist/util/intent-filter.js.map +1 -0
  97. package/dist/util/label.d.ts +4 -0
  98. package/dist/util/label.d.ts.map +1 -0
  99. package/dist/util/label.js +14 -0
  100. package/dist/util/label.js.map +1 -0
  101. package/dist/util/path.d.ts +8 -0
  102. package/dist/util/path.d.ts.map +1 -0
  103. package/dist/util/path.js +21 -0
  104. package/dist/util/path.js.map +1 -0
  105. package/dist/util/stream-compress.d.ts +36 -0
  106. package/dist/util/stream-compress.d.ts.map +1 -0
  107. package/dist/util/stream-compress.js +104 -0
  108. package/dist/util/stream-compress.js.map +1 -0
  109. package/dist/util/version.d.ts +2 -0
  110. package/dist/util/version.d.ts.map +1 -0
  111. package/dist/util/version.js +15 -0
  112. package/dist/util/version.js.map +1 -0
  113. package/docs/token-reduction-report.md +164 -88
  114. package/hooks/pretooluse.mjs +38 -0
  115. package/package.json +5 -4
@@ -0,0 +1,719 @@
1
+ /**
2
+ * Command-specific output filters.
3
+ *
4
+ * Three modes balance fidelity vs aggressiveness:
5
+ * - "conservative": no command-specific compression (callers strip ANSI only)
6
+ * - "balanced": remove obvious noise (progress, hints, deprecations);
7
+ * preserve metadata (commit bodies, file dates, etc.)
8
+ * - "aggressive": match RTK's tactic of dropping metadata too — git log
9
+ * becomes one-line per commit, ls -la drops perms/dates,
10
+ * find lower threshold, grep groups by file.
11
+ *
12
+ * The mode is plumbed through the pipeline via compressOutput in cli/filter.ts.
13
+ * Default is "balanced".
14
+ */
15
+ export const DEFAULT_MODE = "balanced";
16
+ export function parseMode(input) {
17
+ if (input === "aggressive" || input === "conservative")
18
+ return input;
19
+ return "balanced";
20
+ }
21
+ /** Parse mode allowing "auto" as a value. */
22
+ export function parseRequestedMode(input) {
23
+ if (input === "auto")
24
+ return "auto";
25
+ return parseMode(input);
26
+ }
27
+ /** Detect command type from code string and apply specialized filter */
28
+ export function applyCommandFilter(code, stdout, mode = DEFAULT_MODE) {
29
+ if (mode === "conservative")
30
+ return { output: stdout, filtered: false };
31
+ const cmd = code.trim().split(/\s+/)[0];
32
+ const fullCmd = code.trim();
33
+ // Git commands
34
+ if (cmd === "git")
35
+ return filterGit(fullCmd, stdout, mode);
36
+ // Package managers
37
+ if (cmd === "npm" || cmd === "yarn" || cmd === "pnpm" || cmd === "bun")
38
+ return filterPackageManager(fullCmd, stdout, mode);
39
+ // Test runners
40
+ if (fullCmd.includes("test") ||
41
+ fullCmd.includes("jest") ||
42
+ fullCmd.includes("vitest") ||
43
+ fullCmd.includes("pytest") ||
44
+ fullCmd.includes("cargo test")) {
45
+ return filterTestOutput(stdout);
46
+ }
47
+ // Build tools
48
+ if (cmd === "cargo" || cmd === "make" || cmd === "gradle")
49
+ return filterBuildOutput(fullCmd, stdout);
50
+ // Docker/container
51
+ if (cmd === "docker" || cmd === "kubectl")
52
+ return filterContainerOutput(fullCmd, stdout);
53
+ // ls/find/tree
54
+ if (cmd === "ls" || cmd === "find" || cmd === "tree")
55
+ return filterFileList(fullCmd, stdout, mode);
56
+ // grep — aggressive mode only (group by file, drop long lines)
57
+ if (cmd === "grep" || cmd === "rg" || cmd === "ripgrep") {
58
+ if (mode === "aggressive")
59
+ return filterGrep(stdout);
60
+ }
61
+ // System tabular commands: df, du, ps — aggressive mode only.
62
+ if (mode === "aggressive") {
63
+ if (cmd === "df")
64
+ return filterDf(stdout);
65
+ if (cmd === "du")
66
+ return filterDu(stdout);
67
+ if (cmd === "ps")
68
+ return filterPs(stdout);
69
+ }
70
+ return { output: stdout, filtered: false };
71
+ }
72
+ export function filterGit(cmd, stdout, mode = DEFAULT_MODE) {
73
+ // git push/pull/fetch/clone: strip progress lines
74
+ if (/git\s+(push|pull|fetch|clone)/.test(cmd)) {
75
+ const lines = stdout.split("\n");
76
+ const filtered = lines.filter((l) => !l.startsWith("remote: Counting") &&
77
+ !l.startsWith("remote: Compressing") &&
78
+ !l.startsWith("remote: Total") &&
79
+ !l.includes("Unpacking objects:") &&
80
+ !l.includes("Receiving objects:") &&
81
+ !l.includes("Resolving deltas:") &&
82
+ !/^\s*\d+%/.test(l));
83
+ return { output: filtered.join("\n"), filtered: true };
84
+ }
85
+ // git status: remove hint lines, keep branch and file status
86
+ if (/git\s+status/.test(cmd)) {
87
+ return { output: filterGitStatus(stdout, mode), filtered: true };
88
+ }
89
+ // git log — aggressive mode collapses each commit to one line.
90
+ // Balanced mode preserves the header + first 3 body lines and replaces
91
+ // the remainder with "[+N more lines]". Keeps the "why" intact while
92
+ // dropping verbose tail content.
93
+ if (/git\s+log/.test(cmd) && !cmd.includes("--oneline")) {
94
+ if (mode === "aggressive") {
95
+ return { output: aggressiveGitLog(stdout), filtered: true };
96
+ }
97
+ if (mode === "balanced") {
98
+ const truncated = balancedGitLog(stdout);
99
+ // Only mark filtered if we actually dropped something
100
+ if (truncated.length < stdout.length) {
101
+ return { output: truncated, filtered: true };
102
+ }
103
+ }
104
+ }
105
+ // git diff — aggressive mode drops context lines for unified diffs.
106
+ // Already-compact forms (--stat, --name-only, --name-status, --shortstat)
107
+ // pass through since they ARE the summary.
108
+ if (/git\s+diff/.test(cmd) && mode === "aggressive") {
109
+ if (/--(stat|name-only|name-status|shortstat|numstat)\b/.test(cmd)) {
110
+ return { output: stdout, filtered: false };
111
+ }
112
+ return { output: aggressiveGitDiff(stdout), filtered: true };
113
+ }
114
+ return { output: stdout, filtered: false };
115
+ }
116
+ function filterGitStatus(stdout, mode) {
117
+ const lines = stdout.split("\n");
118
+ const balanced = lines.filter((l) => !l.startsWith(" (use ") && l.trim() !== "");
119
+ if (mode !== "aggressive")
120
+ return balanced.join("\n");
121
+ // Aggressive: collapse "Changes not staged"/"Untracked" sections to terse counts.
122
+ // Keep: branch line, file paths with status prefix.
123
+ const out = [];
124
+ for (const l of balanced) {
125
+ if (/^On branch/.test(l)) {
126
+ out.push(l.replace(/^On branch /, "* "));
127
+ continue;
128
+ }
129
+ if (/^Your branch is/.test(l))
130
+ continue;
131
+ if (/^Changes (to be committed|not staged for commit):/.test(l))
132
+ continue;
133
+ if (/^Untracked files:/.test(l)) {
134
+ out.push("? Untracked:");
135
+ continue;
136
+ }
137
+ if (/^no changes added to commit/.test(l))
138
+ continue;
139
+ if (/^nothing to commit/.test(l)) {
140
+ out.push("(clean)");
141
+ continue;
142
+ }
143
+ // File status lines: "\tmodified: foo.ts" → "M foo.ts"
144
+ const m = l.match(/^\s*(modified|new file|deleted|renamed|typechange):\s*(.+)$/);
145
+ if (m) {
146
+ const code = { modified: "M", "new file": "A", deleted: "D", renamed: "R", typechange: "T" }[m[1]] ?? "?";
147
+ out.push(`${code} ${m[2]}`);
148
+ continue;
149
+ }
150
+ out.push(l);
151
+ }
152
+ return out.join("\n");
153
+ }
154
+ /**
155
+ * Convert verbose `git log` output to one line per commit:
156
+ * "<sha7> <subject> (<reltime>) <author>"
157
+ * Body and "Date:" lines are dropped. Merge commits keep their subject.
158
+ */
159
+ function aggressiveGitLog(stdout) {
160
+ const lines = stdout.split("\n");
161
+ const out = [];
162
+ let sha = "";
163
+ let author = "";
164
+ let date = "";
165
+ let subject = "";
166
+ let inCommit = false;
167
+ let blanksAfterDate = 0;
168
+ const flush = () => {
169
+ if (!sha)
170
+ return;
171
+ const reltime = date ? ` (${humanReltime(date)})` : "";
172
+ const auth = author ? ` <${author.replace(/\s*<.*?>/, "").trim()}>` : "";
173
+ out.push(`${sha.slice(0, 7)} ${subject}${reltime}${auth}`);
174
+ };
175
+ for (const line of lines) {
176
+ const m = line.match(/^commit\s+([0-9a-f]{7,40})/);
177
+ if (m) {
178
+ flush();
179
+ sha = m[1];
180
+ author = "";
181
+ date = "";
182
+ subject = "";
183
+ inCommit = true;
184
+ blanksAfterDate = 0;
185
+ continue;
186
+ }
187
+ if (!inCommit)
188
+ continue;
189
+ if (/^Author:\s/.test(line)) {
190
+ author = line.replace(/^Author:\s+/, "").trim();
191
+ continue;
192
+ }
193
+ if (/^Date:\s/.test(line)) {
194
+ date = line.replace(/^Date:\s+/, "").trim();
195
+ continue;
196
+ }
197
+ if (line.trim() === "") {
198
+ blanksAfterDate++;
199
+ continue;
200
+ }
201
+ // First non-blank line after Date: is the subject. Skip body afterward.
202
+ if (!subject && blanksAfterDate >= 1) {
203
+ subject = line.trim();
204
+ }
205
+ }
206
+ flush();
207
+ return out.join("\n");
208
+ }
209
+ /**
210
+ * Truncate `git log` commit bodies to the first 3 lines, replacing the
211
+ * tail with "[+N lines omitted]". Keeps full headers (sha, author, date,
212
+ * subject) and the first 3 body paragraphs verbatim — so the agent still
213
+ * gets the "why" of each commit but doesn't pay for verbose tails.
214
+ *
215
+ * Returns the original input unchanged if no truncation was needed.
216
+ */
217
+ const BALANCED_GIT_LOG_BODY_LINES = 3;
218
+ function balancedGitLog(stdout) {
219
+ const lines = stdout.split("\n");
220
+ const out = [];
221
+ let bodyKept = 0;
222
+ let bodyDropped = 0;
223
+ let subjectSeen = false;
224
+ let inCommit = false;
225
+ let inBody = false;
226
+ let blanksAfterDate = 0;
227
+ const flushOmitted = () => {
228
+ if (bodyDropped > 0) {
229
+ out.push(` [+${bodyDropped} lines omitted]`);
230
+ bodyDropped = 0;
231
+ }
232
+ };
233
+ for (const line of lines) {
234
+ // New commit boundary — flush any pending omission marker, reset state.
235
+ if (/^commit\s+[0-9a-f]{7,40}/.test(line)) {
236
+ flushOmitted();
237
+ out.push(line);
238
+ inCommit = true;
239
+ inBody = false;
240
+ subjectSeen = false;
241
+ bodyKept = 0;
242
+ blanksAfterDate = 0;
243
+ continue;
244
+ }
245
+ if (!inCommit) {
246
+ out.push(line);
247
+ continue;
248
+ }
249
+ // Headers (Author/Date/Merge) always kept.
250
+ if (/^(Author|Date|Merge):\s/.test(line)) {
251
+ out.push(line);
252
+ continue;
253
+ }
254
+ // Blank line — kept; counts as "body separator" transition.
255
+ if (line.trim() === "") {
256
+ out.push(line);
257
+ blanksAfterDate++;
258
+ continue;
259
+ }
260
+ // Once past the first blank after Date, we're in the body.
261
+ if (blanksAfterDate >= 1)
262
+ inBody = true;
263
+ if (inBody) {
264
+ // Always keep the subject (first non-blank line in body).
265
+ if (!subjectSeen) {
266
+ subjectSeen = true;
267
+ out.push(line);
268
+ continue;
269
+ }
270
+ // Keep up to N body lines past the subject; drop the rest with a marker.
271
+ if (bodyKept >= BALANCED_GIT_LOG_BODY_LINES) {
272
+ bodyDropped++;
273
+ continue;
274
+ }
275
+ bodyKept++;
276
+ }
277
+ out.push(line);
278
+ }
279
+ flushOmitted();
280
+ return out.join("\n");
281
+ }
282
+ /**
283
+ * Convert verbose unified diff to "+ added\n- removed" only — drop hunks/context.
284
+ */
285
+ function aggressiveGitDiff(stdout) {
286
+ const lines = stdout.split("\n");
287
+ const out = [];
288
+ let currentFile = "";
289
+ for (const line of lines) {
290
+ const fm = line.match(/^diff --git a\/(.+?) b\//);
291
+ if (fm) {
292
+ currentFile = fm[1];
293
+ out.push(`@@ ${currentFile}`);
294
+ continue;
295
+ }
296
+ if (/^---\s|^\+\+\+\s|^index\s|^@@\s/.test(line))
297
+ continue;
298
+ // Keep only +/- content lines (not "+++" / "---" headers, already filtered above)
299
+ if (line.startsWith("+") || line.startsWith("-"))
300
+ out.push(line);
301
+ }
302
+ return out.join("\n");
303
+ }
304
+ function humanReltime(dateStr) {
305
+ const d = new Date(dateStr);
306
+ if (Number.isNaN(d.getTime()))
307
+ return dateStr;
308
+ const ms = Date.now() - d.getTime();
309
+ const hours = Math.round(ms / 3600_000);
310
+ if (hours < 1)
311
+ return "just now";
312
+ if (hours < 24)
313
+ return `${hours}h ago`;
314
+ const days = Math.round(hours / 24);
315
+ if (days < 30)
316
+ return `${days}d ago`;
317
+ const months = Math.round(days / 30);
318
+ if (months < 12)
319
+ return `${months}mo ago`;
320
+ return `${Math.round(months / 12)}y ago`;
321
+ }
322
+ export function filterPackageManager(cmd, stdout, mode = DEFAULT_MODE) {
323
+ // npm/yarn install: strip noise, keep summary
324
+ if (/\b(install|add|i)\b/.test(cmd)) {
325
+ const lines = stdout.split("\n");
326
+ const filtered = lines.filter((l) => !l.startsWith("npm warn") &&
327
+ !l.includes("packages are looking for funding") &&
328
+ !l.includes("run `npm fund`") &&
329
+ !l.startsWith("npm notice") &&
330
+ !/^[\s│├└─]+$/.test(l) && // tree-drawing characters
331
+ !/^\s*$/.test(l));
332
+ // Aggressive: keep only the final "added N packages" / vulnerability summary lines
333
+ if (mode === "aggressive") {
334
+ const summaryOnly = filtered.filter((l) => /^(added|removed|changed|audited)\s+\d+/.test(l) ||
335
+ /vulnerabilit(y|ies)/i.test(l) ||
336
+ /^npm\s+ERR/.test(l));
337
+ return { output: summaryOnly.join("\n"), filtered: true };
338
+ }
339
+ return { output: filtered.join("\n"), filtered: true };
340
+ }
341
+ // npm test: delegate to test filter
342
+ if (/\btest\b/.test(cmd)) {
343
+ return filterTestOutput(stdout);
344
+ }
345
+ // npm ls / list / ll — aggressive mode strips tree-drawing chars and
346
+ // collapses identical version lines.
347
+ if (mode === "aggressive" && /\b(ls|list|ll)\b/.test(cmd)) {
348
+ return filterNpmLs(stdout);
349
+ }
350
+ return { output: stdout, filtered: false };
351
+ }
352
+ /** npm ls output — strip tree-drawing characters, drop "deduped" markers, dedupe identical lines. */
353
+ function filterNpmLs(stdout) {
354
+ const lines = stdout.split("\n");
355
+ const seen = new Set();
356
+ const out = [];
357
+ for (const l of lines) {
358
+ // Strip box-drawing prefix: ├── ┬ │ └── ─ etc.
359
+ const stripped = l.replace(/^[\s│├└─┬]+/u, "").trimEnd();
360
+ if (!stripped)
361
+ continue;
362
+ // Drop "deduped" markers — they're noise once you know there's deduplication.
363
+ if (/\bdeduped\b/.test(stripped))
364
+ continue;
365
+ // Drop "extraneous" labels (can appear leading or inline).
366
+ const cleaned = stripped.replace(/^extraneous\s+/, "").replace(/\s+\bextraneous\b/g, "");
367
+ if (seen.has(cleaned))
368
+ continue;
369
+ seen.add(cleaned);
370
+ out.push(cleaned);
371
+ }
372
+ return { output: out.join("\n"), filtered: true };
373
+ }
374
+ const FAIL_MARKER_RE = /^\s*[✗✘×]\s/;
375
+ const FAIL_WORD_RE = /\bFAIL\b/;
376
+ const FAILED_RE = /\bfailed?\b/i;
377
+ const ERROR_RE = /\bERROR\b/;
378
+ const SUMMARY_RE = /^\s*(Tests?|Suites?|Test Suites)\s*:|^\s*(pass|fail|skip|pending|todo)\s|\b\d+\s+(passing|failing|pending|skipped)\b|^(ok|not ok)\s|^ℹ\s|^(PASS|FAIL)\s/i;
379
+ function isFailMarker(line) {
380
+ return (FAIL_MARKER_RE.test(line) ||
381
+ FAIL_WORD_RE.test(line) ||
382
+ FAILED_RE.test(line) ||
383
+ ERROR_RE.test(line));
384
+ }
385
+ function isSummaryLine(line) {
386
+ return SUMMARY_RE.test(line);
387
+ }
388
+ export function filterTestOutput(stdout) {
389
+ const lines = stdout.split("\n");
390
+ const failures = [];
391
+ const summary = [];
392
+ let inFailure = false;
393
+ let failCount = 0;
394
+ for (const line of lines) {
395
+ if (isFailMarker(line)) {
396
+ inFailure = true;
397
+ failCount++;
398
+ }
399
+ if (inFailure) {
400
+ failures.push(line);
401
+ if (line.trim() === "" && failures.length > 3)
402
+ inFailure = false;
403
+ }
404
+ if (isSummaryLine(line)) {
405
+ summary.push(line);
406
+ }
407
+ }
408
+ // If all pass, return compact summary
409
+ if (failCount === 0 && summary.length > 0) {
410
+ return { output: summary.join("\n"), filtered: true };
411
+ }
412
+ // If failures exist, return failures + the rollup summary lines only.
413
+ // Drop per-file PASS lines from the summary (the FAIL lines + counts are
414
+ // what the agent needs; 200 PASS lines just inflate context).
415
+ if (failures.length > 0) {
416
+ const rollup = summary.filter((l) => !/^PASS\s/i.test(l));
417
+ const result = [...failures, "", ...rollup].join("\n");
418
+ return { output: result, filtered: true };
419
+ }
420
+ return { output: stdout, filtered: false };
421
+ }
422
+ export function filterBuildOutput(cmd, stdout) {
423
+ const lines = stdout.split("\n");
424
+ // Strip: download progress, "Compiling X/Y" or "Compiling crate v1.2.3" lines,
425
+ // blocking-on-lock messages, blank lines.
426
+ // Keep: "Finished" lines, errors, and other meaningful output.
427
+ const filtered = lines.filter((l) => !l.includes("Downloading") &&
428
+ !l.includes("Downloaded") &&
429
+ !/Compiling\s+\d+\s+of\s+\d+/.test(l) &&
430
+ !/^\s*Compiling\s+[\w-]+\s+v\d/.test(l) &&
431
+ !/^\s*Checking\s+[\w-]+\s+v\d/.test(l) &&
432
+ !l.includes("Blocking waiting for file lock") &&
433
+ !/^\s*$/.test(l));
434
+ return { output: filtered.join("\n"), filtered: filtered.length < lines.length };
435
+ }
436
+ export function filterContainerOutput(cmd, stdout) {
437
+ // docker build: strip layer progress, keep step lines and summary
438
+ if (/docker\s+build/.test(cmd)) {
439
+ const lines = stdout.split("\n");
440
+ const filtered = lines.filter((l) => !l.startsWith(" ---> ") && !l.startsWith("Sending build context") && !/^\s*$/.test(l));
441
+ return { output: filtered.join("\n"), filtered: true };
442
+ }
443
+ // kubectl get / describe / logs with many rows: summarize per namespace/status
444
+ if (/^kubectl\s+(get|describe)\b/.test(cmd)) {
445
+ const lines = stdout.split("\n").filter((l) => l.length > 0);
446
+ // Keep header and short outputs as-is.
447
+ if (lines.length <= 30)
448
+ return { output: stdout, filtered: false };
449
+ const header = lines[0];
450
+ const rows = lines.slice(1);
451
+ // `get` rows are columnar — first column is usually NAMESPACE or NAME.
452
+ // Group by first column + last interesting column (STATUS or AGE).
453
+ const headerCols = header.split(/\s{2,}/);
454
+ const hasNamespace = headerCols[0]?.toUpperCase() === "NAMESPACE";
455
+ const statusIdx = headerCols.findIndex((c) => /^STATUS$/i.test(c));
456
+ const counts = new Map();
457
+ for (const row of rows) {
458
+ const cols = row.split(/\s{2,}/);
459
+ const ns = hasNamespace ? cols[0] : "(default)";
460
+ const status = statusIdx >= 0 ? cols[statusIdx] : "—";
461
+ const key = `${ns}\t${status}`;
462
+ counts.set(key, (counts.get(key) ?? 0) + 1);
463
+ }
464
+ const summaryLines = [`${header}`, `(${rows.length} rows summarized by namespace/status)`];
465
+ for (const [key, n] of [...counts.entries()].sort((a, b) => b[1] - a[1])) {
466
+ const [ns, status] = key.split("\t");
467
+ summaryLines.push(` ${ns} — ${status}: ${n}`);
468
+ }
469
+ return { output: summaryLines.join("\n"), filtered: true };
470
+ }
471
+ // docker ps and other compact tabular outputs: pass through.
472
+ return { output: stdout, filtered: false };
473
+ }
474
+ export function filterFileList(cmd, stdout, mode = DEFAULT_MODE) {
475
+ const isLs = /^ls\b/.test(cmd);
476
+ const isLong = /-l/.test(cmd);
477
+ // Aggressive mode for `ls -l*` — strip permissions/owner/date, keep name + size
478
+ if (mode === "aggressive" && isLs && isLong) {
479
+ return { output: aggressiveLsLong(stdout), filtered: true };
480
+ }
481
+ // Balanced mode for `ls -l*` — drop universal noise (./.., total N, blank
482
+ // lines) but keep all metadata. Users who run `ls -l` want perms/dates;
483
+ // they don't want ./.. entries or the "total" summary.
484
+ if (mode === "balanced" && isLs && isLong) {
485
+ return { output: balancedLsLong(stdout), filtered: true };
486
+ }
487
+ // Threshold for find/ls -R summarization. Above this many lines, we
488
+ // summarize by directory if the entries span enough dirs. Below this,
489
+ // the output is short enough to keep verbatim.
490
+ const { summarizeAt, minDirs } = mode === "aggressive" ? { summarizeAt: 10, minDirs: 3 } : { summarizeAt: 20, minDirs: 4 };
491
+ const lines = stdout.split("\n").filter((l) => l.trim() !== "");
492
+ if (lines.length <= summarizeAt)
493
+ return { output: stdout, filtered: false };
494
+ // Group by directory for find/ls -R
495
+ if (cmd.includes("-R") || cmd.startsWith("find")) {
496
+ const dirs = new Map();
497
+ for (const line of lines) {
498
+ const parts = line.split("/");
499
+ const dir = parts.length > 1 ? parts.slice(0, -1).join("/") : ".";
500
+ dirs.set(dir, (dirs.get(dir) ?? 0) + 1);
501
+ }
502
+ if (dirs.size > minDirs) {
503
+ const summary = Array.from(dirs.entries())
504
+ .sort((a, b) => b[1] - a[1])
505
+ .map(([dir, count]) => ` ${dir}/ (${count} files)`)
506
+ .join("\n");
507
+ return {
508
+ output: `${lines.length} files found:\n${summary}`,
509
+ filtered: true,
510
+ };
511
+ }
512
+ }
513
+ return { output: stdout, filtered: false };
514
+ }
515
+ /**
516
+ * Balanced ls -l: keep full metadata (perms/owner/date/size) for every
517
+ * file but drop the universally-useless lines: ./.., "total N", and blank
518
+ * separators between recursive sections.
519
+ */
520
+ function balancedLsLong(stdout) {
521
+ const lines = stdout.split("\n");
522
+ const out = [];
523
+ for (const line of lines) {
524
+ if (line.trim() === "")
525
+ continue;
526
+ if (/^total\s+\d+/.test(line))
527
+ continue;
528
+ // Match the . and .. entries and skip them — they convey nothing.
529
+ const m = line.match(/^([dlcb-])[rwxst@+\-]{9,}\s+\d+\s+\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+(\.\.?)$/);
530
+ if (m)
531
+ continue;
532
+ out.push(line);
533
+ }
534
+ return out.join("\n");
535
+ }
536
+ /**
537
+ * Strip `ls -l` metadata; emit "name [size]" rows + directory headers.
538
+ *
539
+ * For `ls -laR` (recursive), each subdir gets its own header section. The
540
+ * subdir entries inside the parent's listing are redundant (they reappear
541
+ * as section headers below), so we drop them. We also drop "." and ".."
542
+ * entries, "total N" lines, and blank lines.
543
+ */
544
+ function aggressiveLsLong(stdout) {
545
+ const lines = stdout.split("\n");
546
+ const out = [];
547
+ let inSection = false;
548
+ for (const line of lines) {
549
+ // Directory header from `ls -laR`: "src/dir:" — emit as `dir/`.
550
+ if (/^[^\s]+:$/.test(line.trim())) {
551
+ out.push(line.trim());
552
+ inSection = true;
553
+ continue;
554
+ }
555
+ // `total N` lines from ls -l — drop
556
+ if (/^total\s+\d+/.test(line))
557
+ continue;
558
+ // Empty lines — drop
559
+ if (line.trim() === "")
560
+ continue;
561
+ // ls -l row: drwxr-xr-x 3 jiun staff 96 May 6 14:20 name
562
+ const m = line.match(/^([dlcb-])[rwxst@+\-]{9,}\s+\d+\s+\S+\s+\S+\s+(\S+)\s+\S+\s+\S+\s+\S+\s+(.+)$/);
563
+ if (m) {
564
+ const type = m[1];
565
+ const sizeStr = m[2];
566
+ const name = m[3];
567
+ // "." and ".." entries are noise in any listing
568
+ if (name === "." || name === "..")
569
+ continue;
570
+ // In recursive sections, directory entries get their own section
571
+ // header below — emitting them here is redundant.
572
+ if (type === "d" && inSection)
573
+ continue;
574
+ out.push(name + (type === "d" ? "/" : ` ${formatSize(sizeStr)}`));
575
+ continue;
576
+ }
577
+ // Fallback: keep line (unknown format)
578
+ out.push(line);
579
+ }
580
+ return out.join("\n");
581
+ }
582
+ function formatSize(s) {
583
+ const n = Number.parseInt(s, 10);
584
+ if (Number.isNaN(n))
585
+ return s;
586
+ if (n >= 1024 * 1024)
587
+ return `${(n / 1024 / 1024).toFixed(1)}M`;
588
+ if (n >= 1024)
589
+ return `${(n / 1024).toFixed(1)}K`;
590
+ return `${n}B`;
591
+ }
592
+ /**
593
+ * Group grep output by file, truncate long matched lines, drop redundant context.
594
+ * Aggressive only — balanced mode passes grep through.
595
+ */
596
+ export function filterGrep(stdout) {
597
+ const lines = stdout.split("\n").filter((l) => l.length > 0);
598
+ if (lines.length === 0)
599
+ return { output: stdout, filtered: false };
600
+ const byFile = new Map();
601
+ for (const line of lines) {
602
+ // grep -rn / rg: "path:lineNo:content"
603
+ const m = line.match(/^([^:]+):(\d+):(.*)$/);
604
+ if (!m) {
605
+ // Plain match — group under "(no path)"
606
+ const arr = byFile.get("(no path)") ?? [];
607
+ arr.push(line.length > 100 ? `${line.slice(0, 100)}…` : line);
608
+ byFile.set("(no path)", arr);
609
+ continue;
610
+ }
611
+ const [, file, lineNo, content] = m;
612
+ const truncated = content.length > 100 ? `${content.slice(0, 100)}…` : content;
613
+ const arr = byFile.get(file) ?? [];
614
+ arr.push(` L${lineNo}: ${truncated.trim()}`);
615
+ byFile.set(file, arr);
616
+ }
617
+ const out = [];
618
+ for (const [file, hits] of byFile) {
619
+ out.push(`${file} (${hits.length})`);
620
+ for (const h of hits.slice(0, 8))
621
+ out.push(h);
622
+ if (hits.length > 8)
623
+ out.push(` ... +${hits.length - 8} more matches`);
624
+ }
625
+ return { output: out.join("\n"), filtered: true };
626
+ }
627
+ /**
628
+ * df output — drop pseudo-filesystems (tmpfs, devfs, /dev/loop, etc.) that
629
+ * are usually noise, and shrink padding to single space.
630
+ */
631
+ export function filterDf(stdout) {
632
+ const lines = stdout.split("\n");
633
+ if (lines.length === 0)
634
+ return { output: stdout, filtered: false };
635
+ const header = lines[0];
636
+ const out = [header.replace(/\s{2,}/g, " ")];
637
+ for (const line of lines.slice(1)) {
638
+ if (!line.trim())
639
+ continue;
640
+ // Drop noisy pseudo-filesystems
641
+ if (/^(tmpfs|devfs|devtmpfs|udev|overlay|map\s|none\s|\/dev\/loop)/.test(line))
642
+ continue;
643
+ out.push(line.replace(/\s{2,}/g, " "));
644
+ }
645
+ return { output: out.join("\n"), filtered: true };
646
+ }
647
+ /** du -a / du -h with many entries: keep just the largest 20 + total. */
648
+ export function filterDu(stdout) {
649
+ const lines = stdout.split("\n").filter((l) => l.trim() !== "");
650
+ if (lines.length <= 25)
651
+ return { output: stdout, filtered: false };
652
+ // Each line is "<size>\t<path>" — sort by size descending.
653
+ const parsed = lines
654
+ .map((l) => {
655
+ const m = l.match(/^([\d.]+[KMGT]?B?)?\s*(.*)$/);
656
+ if (!m)
657
+ return null;
658
+ const sizeRaw = m[1] ?? "0";
659
+ const path = m[2];
660
+ return { sizeRaw, sizeBytes: parseDuSize(sizeRaw), path, line: l };
661
+ })
662
+ .filter((x) => x !== null);
663
+ parsed.sort((a, b) => b.sizeBytes - a.sizeBytes);
664
+ const top = parsed.slice(0, 20).map((p) => p.line);
665
+ return {
666
+ output: `(top 20 of ${parsed.length} entries by size)\n${top.join("\n")}`,
667
+ filtered: true,
668
+ };
669
+ }
670
+ function parseDuSize(s) {
671
+ const m = s.match(/^([\d.]+)([KMGT])?B?$/i);
672
+ if (!m)
673
+ return 0;
674
+ const n = Number.parseFloat(m[1]);
675
+ const unit = (m[2] ?? "").toUpperCase();
676
+ const factor = unit === "T"
677
+ ? 1024 ** 4
678
+ : unit === "G"
679
+ ? 1024 ** 3
680
+ : unit === "M"
681
+ ? 1024 ** 2
682
+ : unit === "K"
683
+ ? 1024
684
+ : 1;
685
+ return n * factor;
686
+ }
687
+ /**
688
+ * ps aux output — keep PID, %CPU, %MEM, COMMAND only. Strip USER, VSZ, RSS,
689
+ * STAT, START, TIME and the heavy padding. Drop kernel/system noise.
690
+ */
691
+ export function filterPs(stdout) {
692
+ const lines = stdout.split("\n");
693
+ if (lines.length <= 2)
694
+ return { output: stdout, filtered: false };
695
+ const header = lines[0];
696
+ // `\b%CPU\b` doesn't match because % is not a word char; use plain includes.
697
+ const isAux = header.includes("USER") && header.includes("%CPU") && header.includes("%MEM");
698
+ if (!isAux)
699
+ return { output: stdout, filtered: false };
700
+ const out = ["PID %CPU %MEM CMD"];
701
+ for (const line of lines.slice(1)) {
702
+ if (!line.trim())
703
+ continue;
704
+ // ps aux columns: USER PID %CPU %MEM VSZ RSS TT STAT STARTED TIME COMMAND
705
+ const parts = line.trim().split(/\s+/);
706
+ if (parts.length < 11)
707
+ continue;
708
+ const pid = parts[1];
709
+ const cpu = parts[2];
710
+ const mem = parts[3];
711
+ const cmd = parts.slice(10).join(" ");
712
+ // Drop kernel threads (PID < 100, COMMAND in brackets) — usually noise
713
+ if (/^\[.*\]$/.test(cmd))
714
+ continue;
715
+ out.push(`${pid.padEnd(5)} ${cpu.padStart(4)} ${mem.padStart(4)} ${cmd}`);
716
+ }
717
+ return { output: out.join("\n"), filtered: true };
718
+ }
719
+ //# sourceMappingURL=filters.js.map