context-compress 2026.3.22 → 2026.6.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 (120) hide show
  1. package/.claude-plugin/marketplace.json +17 -0
  2. package/.claude-plugin/plugin.json +12 -0
  3. package/.codex-plugin/plugin.json +40 -0
  4. package/.mcp.json +11 -0
  5. package/README.md +275 -44
  6. package/dist/cli/doctor.d.ts.map +1 -1
  7. package/dist/cli/doctor.js +2 -10
  8. package/dist/cli/doctor.js.map +1 -1
  9. package/dist/cli/filter.d.ts +52 -0
  10. package/dist/cli/filter.d.ts.map +1 -0
  11. package/dist/cli/filter.js +200 -0
  12. package/dist/cli/filter.js.map +1 -0
  13. package/dist/cli/index.d.ts +8 -4
  14. package/dist/cli/index.d.ts.map +1 -1
  15. package/dist/cli/index.js +19 -6
  16. package/dist/cli/index.js.map +1 -1
  17. package/dist/cli/lite.d.ts +15 -0
  18. package/dist/cli/lite.d.ts.map +1 -0
  19. package/dist/cli/lite.js +37 -0
  20. package/dist/cli/lite.js.map +1 -0
  21. package/dist/cli/setup.d.ts +23 -1
  22. package/dist/cli/setup.d.ts.map +1 -1
  23. package/dist/cli/setup.js +122 -21
  24. package/dist/cli/setup.js.map +1 -1
  25. package/dist/executor.d.ts.map +1 -1
  26. package/dist/executor.js +7 -4
  27. package/dist/executor.js.map +1 -1
  28. package/dist/filters.d.ts +39 -5
  29. package/dist/filters.d.ts.map +1 -1
  30. package/dist/filters.js +577 -25
  31. package/dist/filters.js.map +1 -1
  32. package/dist/hooks/pretooluse.js +57 -0
  33. package/dist/hooks/pretooluse.js.map +1 -1
  34. package/dist/network.d.ts.map +1 -1
  35. package/dist/network.js +11 -0
  36. package/dist/network.js.map +1 -1
  37. package/dist/server.bundle.mjs +1140 -641
  38. package/dist/server.bundle.mjs.map +4 -4
  39. package/dist/server.d.ts.map +1 -1
  40. package/dist/server.js +36 -612
  41. package/dist/server.js.map +1 -1
  42. package/dist/stats.js +1 -1
  43. package/dist/stats.js.map +1 -1
  44. package/dist/store.d.ts +1 -0
  45. package/dist/store.d.ts.map +1 -1
  46. package/dist/store.js +15 -2
  47. package/dist/store.js.map +1 -1
  48. package/dist/tools/batch-execute.d.ts +4 -0
  49. package/dist/tools/batch-execute.d.ts.map +1 -0
  50. package/dist/tools/batch-execute.js +75 -0
  51. package/dist/tools/batch-execute.js.map +1 -0
  52. package/dist/tools/context.d.ts +17 -0
  53. package/dist/tools/context.d.ts.map +1 -0
  54. package/dist/tools/context.js +2 -0
  55. package/dist/tools/context.js.map +1 -0
  56. package/dist/tools/discover.d.ts +4 -0
  57. package/dist/tools/discover.d.ts.map +1 -0
  58. package/dist/tools/discover.js +65 -0
  59. package/dist/tools/discover.js.map +1 -0
  60. package/dist/tools/execute-file.d.ts +4 -0
  61. package/dist/tools/execute-file.d.ts.map +1 -0
  62. package/dist/tools/execute-file.js +66 -0
  63. package/dist/tools/execute-file.js.map +1 -0
  64. package/dist/tools/execute.d.ts +4 -0
  65. package/dist/tools/execute.d.ts.map +1 -0
  66. package/dist/tools/execute.js +54 -0
  67. package/dist/tools/execute.js.map +1 -0
  68. package/dist/tools/fetch-and-index.d.ts +4 -0
  69. package/dist/tools/fetch-and-index.d.ts.map +1 -0
  70. package/dist/tools/fetch-and-index.js +91 -0
  71. package/dist/tools/fetch-and-index.js.map +1 -0
  72. package/dist/tools/index-content.d.ts +4 -0
  73. package/dist/tools/index-content.d.ts.map +1 -0
  74. package/dist/tools/index-content.js +85 -0
  75. package/dist/tools/index-content.js.map +1 -0
  76. package/dist/tools/search.d.ts +4 -0
  77. package/dist/tools/search.d.ts.map +1 -0
  78. package/dist/tools/search.js +57 -0
  79. package/dist/tools/search.js.map +1 -0
  80. package/dist/tools/stats.d.ts +4 -0
  81. package/dist/tools/stats.d.ts.map +1 -0
  82. package/dist/tools/stats.js +10 -0
  83. package/dist/tools/stats.js.map +1 -0
  84. package/dist/types.d.ts +0 -1
  85. package/dist/types.d.ts.map +1 -1
  86. package/dist/util/auto-mode.d.ts +40 -0
  87. package/dist/util/auto-mode.d.ts.map +1 -0
  88. package/dist/util/auto-mode.js +181 -0
  89. package/dist/util/auto-mode.js.map +1 -0
  90. package/dist/util/fetch-code.d.ts +10 -0
  91. package/dist/util/fetch-code.d.ts.map +1 -0
  92. package/dist/util/fetch-code.js +87 -0
  93. package/dist/util/fetch-code.js.map +1 -0
  94. package/dist/util/intent-filter.d.ts +17 -0
  95. package/dist/util/intent-filter.d.ts.map +1 -0
  96. package/dist/util/intent-filter.js +28 -0
  97. package/dist/util/intent-filter.js.map +1 -0
  98. package/dist/util/label.d.ts +4 -0
  99. package/dist/util/label.d.ts.map +1 -0
  100. package/dist/util/label.js +14 -0
  101. package/dist/util/label.js.map +1 -0
  102. package/dist/util/path.d.ts +8 -0
  103. package/dist/util/path.d.ts.map +1 -0
  104. package/dist/util/path.js +21 -0
  105. package/dist/util/path.js.map +1 -0
  106. package/dist/util/stream-compress.d.ts +36 -0
  107. package/dist/util/stream-compress.d.ts.map +1 -0
  108. package/dist/util/stream-compress.js +104 -0
  109. package/dist/util/stream-compress.js.map +1 -0
  110. package/dist/util/version.d.ts +2 -0
  111. package/dist/util/version.d.ts.map +1 -0
  112. package/dist/util/version.js +15 -0
  113. package/dist/util/version.js.map +1 -0
  114. package/docs/agentic-benchmark.md +110 -0
  115. package/docs/token-reduction-report.md +47 -18
  116. package/hooks/claude-codex-hooks.json +19 -0
  117. package/hooks/pretooluse.mjs +38 -0
  118. package/package.json +12 -8
  119. package/skills/context-compress-audit/SKILL.md +49 -0
  120. package/skills/context-compress-audit/agents/openai.yaml +13 -0
package/dist/filters.js CHANGED
@@ -1,17 +1,41 @@
1
1
  /**
2
2
  * Command-specific output filters.
3
- * Applied before generic dedup/truncation for better token reduction.
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".
4
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
+ }
5
27
  /** Detect command type from code string and apply specialized filter */
6
- export function applyCommandFilter(code, stdout) {
28
+ export function applyCommandFilter(code, stdout, mode = DEFAULT_MODE) {
29
+ if (mode === "conservative")
30
+ return { output: stdout, filtered: false };
7
31
  const cmd = code.trim().split(/\s+/)[0];
8
32
  const fullCmd = code.trim();
9
33
  // Git commands
10
34
  if (cmd === "git")
11
- return filterGit(fullCmd, stdout);
35
+ return filterGit(fullCmd, stdout, mode);
12
36
  // Package managers
13
37
  if (cmd === "npm" || cmd === "yarn" || cmd === "pnpm" || cmd === "bun")
14
- return filterPackageManager(fullCmd, stdout);
38
+ return filterPackageManager(fullCmd, stdout, mode);
15
39
  // Test runners
16
40
  if (fullCmd.includes("test") ||
17
41
  fullCmd.includes("jest") ||
@@ -28,10 +52,24 @@ export function applyCommandFilter(code, stdout) {
28
52
  return filterContainerOutput(fullCmd, stdout);
29
53
  // ls/find/tree
30
54
  if (cmd === "ls" || cmd === "find" || cmd === "tree")
31
- return filterFileList(fullCmd, stdout);
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
+ }
32
70
  return { output: stdout, filtered: false };
33
71
  }
34
- export function filterGit(cmd, stdout) {
72
+ export function filterGit(cmd, stdout, mode = DEFAULT_MODE) {
35
73
  // git push/pull/fetch/clone: strip progress lines
36
74
  if (/git\s+(push|pull|fetch|clone)/.test(cmd)) {
37
75
  const lines = stdout.split("\n");
@@ -46,14 +84,242 @@ export function filterGit(cmd, stdout) {
46
84
  }
47
85
  // git status: remove hint lines, keep branch and file status
48
86
  if (/git\s+status/.test(cmd)) {
49
- const lines = stdout.split("\n");
50
- const filtered = lines.filter((l) => !l.startsWith(" (use ") && l.trim() !== "");
51
- return { output: filtered.join("\n"), filtered: true };
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 };
52
113
  }
53
- // git log and other commands: keep as-is
54
114
  return { output: stdout, filtered: false };
55
115
  }
56
- export function filterPackageManager(cmd, stdout) {
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) {
57
323
  // npm/yarn install: strip noise, keep summary
58
324
  if (/\b(install|add|i)\b/.test(cmd)) {
59
325
  const lines = stdout.split("\n");
@@ -61,21 +327,55 @@ export function filterPackageManager(cmd, stdout) {
61
327
  !l.includes("packages are looking for funding") &&
62
328
  !l.includes("run `npm fund`") &&
63
329
  !l.startsWith("npm notice") &&
64
- !/^[\s\u2502\u251C\u2514\u2500]+$/.test(l) && // tree-drawing characters
330
+ !/^[\s│├└─]+$/.test(l) && // tree-drawing characters
65
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
+ }
66
339
  return { output: filtered.join("\n"), filtered: true };
67
340
  }
68
341
  // npm test: delegate to test filter
69
342
  if (/\btest\b/.test(cmd)) {
70
343
  return filterTestOutput(stdout);
71
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
+ }
72
350
  return { output: stdout, filtered: false };
73
351
  }
74
- const FAIL_MARKER_RE = /^\s*[\u2717\u2718\u00D7]\s/;
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/;
75
375
  const FAIL_WORD_RE = /\bFAIL\b/;
76
376
  const FAILED_RE = /\bfailed?\b/i;
77
377
  const ERROR_RE = /\bERROR\b/;
78
- 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|^\u2139\s|^(PASS|FAIL)\s/i;
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;
79
379
  function isFailMarker(line) {
80
380
  return (FAIL_MARKER_RE.test(line) ||
81
381
  FAIL_WORD_RE.test(line) ||
@@ -109,20 +409,26 @@ export function filterTestOutput(stdout) {
109
409
  if (failCount === 0 && summary.length > 0) {
110
410
  return { output: summary.join("\n"), filtered: true };
111
411
  }
112
- // If failures exist, return failures + summary
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).
113
415
  if (failures.length > 0) {
114
- const result = [...failures, "", ...summary].join("\n");
416
+ const rollup = summary.filter((l) => !/^PASS\s/i.test(l));
417
+ const result = [...failures, "", ...rollup].join("\n");
115
418
  return { output: result, filtered: true };
116
419
  }
117
420
  return { output: stdout, filtered: false };
118
421
  }
119
422
  export function filterBuildOutput(cmd, stdout) {
120
423
  const lines = stdout.split("\n");
121
- // Strip: download progress, "Compiling X/Y", blocking messages, blank lines
122
- // Keep: "Finished" lines and other meaningful output
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.
123
427
  const filtered = lines.filter((l) => !l.includes("Downloading") &&
124
428
  !l.includes("Downloaded") &&
125
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) &&
126
432
  !l.includes("Blocking waiting for file lock") &&
127
433
  !/^\s*$/.test(l));
128
434
  return { output: filtered.join("\n"), filtered: filtered.length < lines.length };
@@ -131,16 +437,59 @@ export function filterContainerOutput(cmd, stdout) {
131
437
  // docker build: strip layer progress, keep step lines and summary
132
438
  if (/docker\s+build/.test(cmd)) {
133
439
  const lines = stdout.split("\n");
134
- const filtered = lines.filter((l) => !l.startsWith(" ---> ") && !l.startsWith("Sending build context"));
440
+ const filtered = lines.filter((l) => !l.startsWith(" ---> ") && !l.startsWith("Sending build context") && !/^\s*$/.test(l));
135
441
  return { output: filtered.join("\n"), filtered: true };
136
442
  }
137
- // docker ps, kubectl: keep as-is (already compact)
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.
138
472
  return { output: stdout, filtered: false };
139
473
  }
140
- export function filterFileList(cmd, stdout) {
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 };
141
491
  const lines = stdout.split("\n").filter((l) => l.trim() !== "");
142
- // If output is short, keep as-is
143
- if (lines.length <= 30)
492
+ if (lines.length <= summarizeAt)
144
493
  return { output: stdout, filtered: false };
145
494
  // Group by directory for find/ls -R
146
495
  if (cmd.includes("-R") || cmd.startsWith("find")) {
@@ -150,8 +499,7 @@ export function filterFileList(cmd, stdout) {
150
499
  const dir = parts.length > 1 ? parts.slice(0, -1).join("/") : ".";
151
500
  dirs.set(dir, (dirs.get(dir) ?? 0) + 1);
152
501
  }
153
- // If many files, summarize by directory
154
- if (dirs.size > 5 && lines.length > 50) {
502
+ if (dirs.size > minDirs) {
155
503
  const summary = Array.from(dirs.entries())
156
504
  .sort((a, b) => b[1] - a[1])
157
505
  .map(([dir, count]) => ` ${dir}/ (${count} files)`)
@@ -164,4 +512,208 @@ export function filterFileList(cmd, stdout) {
164
512
  }
165
513
  return { output: stdout, filtered: false };
166
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
+ }
167
719
  //# sourceMappingURL=filters.js.map