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.
- package/README.md +258 -44
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +2 -10
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/filter.d.ts +52 -0
- package/dist/cli/filter.d.ts.map +1 -0
- package/dist/cli/filter.js +200 -0
- package/dist/cli/filter.js.map +1 -0
- package/dist/cli/index.d.ts +8 -4
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +19 -6
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/lite.d.ts +15 -0
- package/dist/cli/lite.d.ts.map +1 -0
- package/dist/cli/lite.js +37 -0
- package/dist/cli/lite.js.map +1 -0
- package/dist/cli/setup.d.ts +23 -1
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +122 -21
- package/dist/cli/setup.js.map +1 -1
- package/dist/executor.d.ts +7 -1
- package/dist/executor.d.ts.map +1 -1
- package/dist/executor.js +51 -4
- package/dist/executor.js.map +1 -1
- package/dist/filters.d.ts +52 -0
- package/dist/filters.d.ts.map +1 -0
- package/dist/filters.js +719 -0
- package/dist/filters.js.map +1 -0
- package/dist/hooks/pretooluse.js +57 -0
- package/dist/hooks/pretooluse.js.map +1 -1
- package/dist/network.d.ts.map +1 -1
- package/dist/network.js +11 -0
- package/dist/network.js.map +1 -1
- package/dist/server.bundle.mjs +1333 -619
- package/dist/server.bundle.mjs.map +4 -4
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +44 -610
- package/dist/server.js.map +1 -1
- package/dist/stats.d.ts +7 -1
- package/dist/stats.d.ts.map +1 -1
- package/dist/stats.js +65 -0
- package/dist/stats.js.map +1 -1
- package/dist/store.d.ts +1 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +15 -2
- package/dist/store.js.map +1 -1
- package/dist/tools/batch-execute.d.ts +4 -0
- package/dist/tools/batch-execute.d.ts.map +1 -0
- package/dist/tools/batch-execute.js +75 -0
- package/dist/tools/batch-execute.js.map +1 -0
- package/dist/tools/context.d.ts +17 -0
- package/dist/tools/context.d.ts.map +1 -0
- package/dist/tools/context.js +2 -0
- package/dist/tools/context.js.map +1 -0
- package/dist/tools/discover.d.ts +4 -0
- package/dist/tools/discover.d.ts.map +1 -0
- package/dist/tools/discover.js +65 -0
- package/dist/tools/discover.js.map +1 -0
- package/dist/tools/execute-file.d.ts +4 -0
- package/dist/tools/execute-file.d.ts.map +1 -0
- package/dist/tools/execute-file.js +66 -0
- package/dist/tools/execute-file.js.map +1 -0
- package/dist/tools/execute.d.ts +4 -0
- package/dist/tools/execute.d.ts.map +1 -0
- package/dist/tools/execute.js +54 -0
- package/dist/tools/execute.js.map +1 -0
- package/dist/tools/fetch-and-index.d.ts +4 -0
- package/dist/tools/fetch-and-index.d.ts.map +1 -0
- package/dist/tools/fetch-and-index.js +91 -0
- package/dist/tools/fetch-and-index.js.map +1 -0
- package/dist/tools/index-content.d.ts +4 -0
- package/dist/tools/index-content.d.ts.map +1 -0
- package/dist/tools/index-content.js +85 -0
- package/dist/tools/index-content.js.map +1 -0
- package/dist/tools/search.d.ts +4 -0
- package/dist/tools/search.d.ts.map +1 -0
- package/dist/tools/search.js +57 -0
- package/dist/tools/search.js.map +1 -0
- package/dist/tools/stats.d.ts +4 -0
- package/dist/tools/stats.d.ts.map +1 -0
- package/dist/tools/stats.js +10 -0
- package/dist/tools/stats.js.map +1 -0
- package/dist/types.d.ts +11 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/util/auto-mode.d.ts +40 -0
- package/dist/util/auto-mode.d.ts.map +1 -0
- package/dist/util/auto-mode.js +181 -0
- package/dist/util/auto-mode.js.map +1 -0
- package/dist/util/fetch-code.d.ts +10 -0
- package/dist/util/fetch-code.d.ts.map +1 -0
- package/dist/util/fetch-code.js +87 -0
- package/dist/util/fetch-code.js.map +1 -0
- package/dist/util/intent-filter.d.ts +17 -0
- package/dist/util/intent-filter.d.ts.map +1 -0
- package/dist/util/intent-filter.js +28 -0
- package/dist/util/intent-filter.js.map +1 -0
- package/dist/util/label.d.ts +4 -0
- package/dist/util/label.d.ts.map +1 -0
- package/dist/util/label.js +14 -0
- package/dist/util/label.js.map +1 -0
- package/dist/util/path.d.ts +8 -0
- package/dist/util/path.d.ts.map +1 -0
- package/dist/util/path.js +21 -0
- package/dist/util/path.js.map +1 -0
- package/dist/util/stream-compress.d.ts +36 -0
- package/dist/util/stream-compress.d.ts.map +1 -0
- package/dist/util/stream-compress.js +104 -0
- package/dist/util/stream-compress.js.map +1 -0
- package/dist/util/version.d.ts +2 -0
- package/dist/util/version.d.ts.map +1 -0
- package/dist/util/version.js +15 -0
- package/dist/util/version.js.map +1 -0
- package/docs/token-reduction-report.md +164 -88
- package/hooks/pretooluse.mjs +38 -0
- package/package.json +5 -4
package/dist/filters.js
ADDED
|
@@ -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
|