@leing2021/super-pi 0.14.2 → 0.15.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.
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import type { AssistantMessage } from "@mariozechner/pi-ai"
|
|
2
1
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"
|
|
3
2
|
import { Type } from "@sinclair/typebox"
|
|
4
|
-
import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui"
|
|
5
3
|
import { createArtifactHelperTool, type ArtifactType } from "./tools/artifact-helper"
|
|
6
4
|
import { createAskUserQuestionTool } from "./tools/ask-user-question"
|
|
7
5
|
import { createSubagentTool } from "./tools/subagent"
|
|
@@ -15,6 +13,7 @@ import { createBrainstormDialogTool } from "./tools/brainstorm-dialog"
|
|
|
15
13
|
import { createPlanDiffTool } from "./tools/plan-diff"
|
|
16
14
|
import { createSessionHistoryTool } from "./tools/session-history"
|
|
17
15
|
import { createPatternExtractorTool } from "./tools/pattern-extractor"
|
|
16
|
+
import { filterBashOutput } from "./tools/bash-output-filter"
|
|
18
17
|
|
|
19
18
|
const artifactHelperParams = Type.Object({
|
|
20
19
|
repoRoot: Type.String({ description: "Repository root where workflow artifacts should be created" }),
|
|
@@ -171,36 +170,6 @@ const patternExtractorParams = Type.Object({
|
|
|
171
170
|
categories: Type.Optional(Type.Record(Type.String(), Type.Array(Type.String()), { description: "Category name to keyword mapping" })),
|
|
172
171
|
})
|
|
173
172
|
|
|
174
|
-
// Session-scoped stats for custom footer
|
|
175
|
-
let statsEnabled = false
|
|
176
|
-
const toolCounts = new Map<string, number>()
|
|
177
|
-
let subagentCount = 0
|
|
178
|
-
let projectPath = ""
|
|
179
|
-
let footerTui: any = null
|
|
180
|
-
|
|
181
|
-
function renderStatsFooter(ctx: any, tui: any, theme: any, footerData: any, width: number): string[] {
|
|
182
|
-
const shortPath = projectPath
|
|
183
|
-
.replace(/^\/Users\/[^/]+/, "~")
|
|
184
|
-
.split("/")
|
|
185
|
-
.slice(-2)
|
|
186
|
-
.join("/")
|
|
187
|
-
|
|
188
|
-
const sorted = [...toolCounts.entries()].sort((a, b) => b[1] - a[1])
|
|
189
|
-
const total = sorted.reduce((sum, e) => sum + e[1], 0)
|
|
190
|
-
const top3 = sorted.slice(0, 3).map(([name, count]) => `${name}:${count}`).join(" ")
|
|
191
|
-
const toolsStr = total > 0 ? `tools(${total}) ${top3}` : ""
|
|
192
|
-
const subStr = subagentCount > 0 ? `sub:${subagentCount}` : ""
|
|
193
|
-
const branch = footerData.getGitBranch()
|
|
194
|
-
const branchStr = branch ? ` (${branch})` : ""
|
|
195
|
-
|
|
196
|
-
const left = theme.fg("dim", `${shortPath}${branchStr}`)
|
|
197
|
-
const rightParts = [subStr, toolsStr].filter(Boolean)
|
|
198
|
-
const right = rightParts.length > 0 ? theme.fg("dim", rightParts.join(" │ ")) : ""
|
|
199
|
-
|
|
200
|
-
const pad = " ".repeat(Math.max(1, width - visibleWidth(left) - visibleWidth(right)))
|
|
201
|
-
return [truncateToWidth(left + pad + right, width)]
|
|
202
|
-
}
|
|
203
|
-
|
|
204
173
|
export default function ceCoreExtension(pi: ExtensionAPI) {
|
|
205
174
|
const artifactHelper = createArtifactHelperTool()
|
|
206
175
|
const askUserQuestion = createAskUserQuestionTool()
|
|
@@ -537,68 +506,46 @@ export default function ceCoreExtension(pi: ExtensionAPI) {
|
|
|
537
506
|
},
|
|
538
507
|
})
|
|
539
508
|
|
|
540
|
-
//
|
|
541
|
-
pi.on("
|
|
542
|
-
if (
|
|
543
|
-
const name = event.toolName
|
|
544
|
-
toolCounts.set(name, (toolCounts.get(name) || 0) + 1)
|
|
545
|
-
if (name === "subagent" || name === "parallel_subagent") {
|
|
546
|
-
subagentCount++
|
|
547
|
-
}
|
|
548
|
-
if (footerTui) footerTui.requestRender()
|
|
549
|
-
})
|
|
509
|
+
// Bash output smart filter — reduces context waste from verbose command output
|
|
510
|
+
pi.on("tool_result", async (event, _ctx) => {
|
|
511
|
+
if (event.toolName !== "bash") return undefined
|
|
550
512
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
return renderStatsFooter(ctx, tui, theme, footerData, width)
|
|
568
|
-
},
|
|
569
|
-
}
|
|
513
|
+
// Extract command from input
|
|
514
|
+
const command = (event.input as any)?.command ?? ""
|
|
515
|
+
if (!command) return undefined
|
|
516
|
+
|
|
517
|
+
// Extract text content from tool result
|
|
518
|
+
const textBlocks = (event.content as Array<any>)?.filter((b: any) => b.type === "text") ?? []
|
|
519
|
+
if (textBlocks.length === 0) return undefined
|
|
520
|
+
|
|
521
|
+
const output = textBlocks.map((b: any) => b.text).join("")
|
|
522
|
+
const fullOutputPath = (event.details as any)?.fullOutputPath
|
|
523
|
+
|
|
524
|
+
const result = filterBashOutput({
|
|
525
|
+
command,
|
|
526
|
+
output,
|
|
527
|
+
isError: event.isError ?? false,
|
|
528
|
+
fullOutputPath,
|
|
570
529
|
})
|
|
571
|
-
})
|
|
572
530
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
invalidate() {},
|
|
588
|
-
render(width: number): string[] {
|
|
589
|
-
return renderStatsFooter(ctx, tui, theme, footerData, width)
|
|
590
|
-
},
|
|
591
|
-
}
|
|
592
|
-
})
|
|
593
|
-
ctx.ui.notify("Stats footer enabled", "info")
|
|
594
|
-
} else {
|
|
595
|
-
ctx.ui.setFooter(undefined)
|
|
596
|
-
ctx.ui.notify("Default footer restored", "info")
|
|
597
|
-
}
|
|
598
|
-
},
|
|
531
|
+
if (!result.filtered) return undefined
|
|
532
|
+
|
|
533
|
+
// Replace content with filtered version
|
|
534
|
+
return {
|
|
535
|
+
content: [{ type: "text", text: result.output }],
|
|
536
|
+
details: {
|
|
537
|
+
...event.details,
|
|
538
|
+
bashFilter: {
|
|
539
|
+
strategy: result.strategy,
|
|
540
|
+
originalBytes: result.originalBytes,
|
|
541
|
+
filteredBytes: result.filteredBytes,
|
|
542
|
+
},
|
|
543
|
+
},
|
|
544
|
+
}
|
|
599
545
|
})
|
|
600
546
|
}
|
|
601
547
|
|
|
548
|
+
|
|
602
549
|
export { createArtifactHelperTool } from "./tools/artifact-helper"
|
|
603
550
|
export { createAskUserQuestionTool } from "./tools/ask-user-question"
|
|
604
551
|
export { createSubagentTool } from "./tools/subagent"
|
|
@@ -619,3 +566,4 @@ export {
|
|
|
619
566
|
getRunArtifactPath,
|
|
620
567
|
} from "./utils/artifact-paths"
|
|
621
568
|
export { normalizeSlug } from "./utils/name-utils"
|
|
569
|
+
export { filterBashOutput } from "./tools/bash-output-filter"
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox"
|
|
2
|
+
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Types
|
|
5
|
+
// ============================================================================
|
|
6
|
+
|
|
7
|
+
export interface BashOutputFilterInput {
|
|
8
|
+
/** The bash command that was run */
|
|
9
|
+
command: string
|
|
10
|
+
/** The raw output text */
|
|
11
|
+
output: string
|
|
12
|
+
/** Whether the command exited with an error */
|
|
13
|
+
isError: boolean
|
|
14
|
+
/** Original full output path (if truncated by pi) */
|
|
15
|
+
fullOutputPath?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface BashOutputFilterResult {
|
|
19
|
+
/** Filtered output text */
|
|
20
|
+
output: string
|
|
21
|
+
/** Whether filtering was applied */
|
|
22
|
+
filtered: boolean
|
|
23
|
+
/** Original size in bytes */
|
|
24
|
+
originalBytes: number
|
|
25
|
+
/** Filtered size in bytes */
|
|
26
|
+
filteredBytes: number
|
|
27
|
+
/** The filter strategy that was applied */
|
|
28
|
+
strategy: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ============================================================================
|
|
32
|
+
// Command Classification
|
|
33
|
+
// ============================================================================
|
|
34
|
+
|
|
35
|
+
type CommandCategory = "install" | "test" | "build" | "search" | "git-diff" | "list" | "http" | "unknown"
|
|
36
|
+
|
|
37
|
+
interface CommandPattern {
|
|
38
|
+
pattern: RegExp
|
|
39
|
+
category: CommandCategory
|
|
40
|
+
/** Whether this is a "means" command (output is a side-effect) */
|
|
41
|
+
isMeans: boolean
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const COMMAND_PATTERNS: CommandPattern[] = [
|
|
45
|
+
// Install commands — output is a side-effect
|
|
46
|
+
{ pattern: /\b(npm|yarn|pnpm|bun)\s+(install|i|add|ci|update|upgrade)\b/, category: "install", isMeans: true },
|
|
47
|
+
{ pattern: /\b(pip|pip3|poetry|conda)\s+(install|add)\b/, category: "install", isMeans: true },
|
|
48
|
+
{ pattern: /\b(cargo)\s+(install|add|update)\b/, category: "install", isMeans: true },
|
|
49
|
+
{ pattern: /\b(bundle|gem)\s+(install)\b/, category: "install", isMeans: true },
|
|
50
|
+
{ pattern: /\b(go)\s+(get|install|mod\s+(tidy|download))\b/, category: "install", isMeans: true },
|
|
51
|
+
// Test commands — output is mostly noise (pass lines), failures are signal
|
|
52
|
+
{ pattern: /\b(npm|yarn|pnpm|bun)\s+(test|t|run\s+test)/, category: "test", isMeans: true },
|
|
53
|
+
{ pattern: /\b(vitest|jest|mocha|pytest|cargo\s+test|go\s+test|ruby|-Ilib)\b.*\b(test|spec)\b/, category: "test", isMeans: true },
|
|
54
|
+
{ pattern: /\b(jest|vitest|mocha|pytest|npx\s+(jest|vitest|mocha))\b/, category: "test", isMeans: true },
|
|
55
|
+
{ pattern: /\b(cargo)\s+test\b/, category: "test", isMeans: true },
|
|
56
|
+
{ pattern: /\b(go)\s+test\b/, category: "test", isMeans: true },
|
|
57
|
+
// Build/compile commands — warnings/errors are signal
|
|
58
|
+
{ pattern: /\b(tsc|typescript|tscl)\b/, category: "build", isMeans: true },
|
|
59
|
+
{ pattern: /\b(npm|yarn|pnpm|bun)\s+(run\s+)?(build|compile)\b/, category: "build", isMeans: true },
|
|
60
|
+
{ pattern: /\b(cargo)\s+(build|check|clippy)\b/, category: "build", isMeans: true },
|
|
61
|
+
{ pattern: /\b(make|cmake|gcc|g\+\+|clang)\b/, category: "build", isMeans: true },
|
|
62
|
+
{ pattern: /\b(dotnet)\s+(build|publish)\b/, category: "build", isMeans: true },
|
|
63
|
+
{ pattern: /\b(gradle|mvn|mvnw)\b/, category: "build", isMeans: true },
|
|
64
|
+
// Search commands — output IS the purpose, don't filter
|
|
65
|
+
{ pattern: /\b(grep|rg|ag|ack|git\s+grep)\b/, category: "search", isMeans: false },
|
|
66
|
+
{ pattern: /\b(rg|ripgrep)\b/, category: "search", isMeans: false },
|
|
67
|
+
// Git diff/log — output IS the purpose
|
|
68
|
+
{ pattern: /\bgit\s+(diff|log|show|range-diff)\b/, category: "git-diff", isMeans: false },
|
|
69
|
+
// File listing — moderate filtering ok
|
|
70
|
+
{ pattern: /\b(find|fd|ls\s+-R|tree|du|ncdu)\b/, category: "list", isMeans: false },
|
|
71
|
+
// HTTP requests — can be verbose
|
|
72
|
+
{ pattern: /\b(curl|wget|httpie|http\s+)\b/, category: "http", isMeans: false },
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
function classifyCommand(command: string): CommandPattern | null {
|
|
76
|
+
// Normalize command: remove leading whitespace, handle pipes
|
|
77
|
+
const cmd = command.trim()
|
|
78
|
+
|
|
79
|
+
// Check primary command (before any pipe)
|
|
80
|
+
const primaryCmd = cmd.split("|")[0].trim()
|
|
81
|
+
|
|
82
|
+
for (const pattern of COMMAND_PATTERNS) {
|
|
83
|
+
if (pattern.pattern.test(primaryCmd)) {
|
|
84
|
+
return pattern
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return null
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ============================================================================
|
|
92
|
+
// Output Filters
|
|
93
|
+
// ============================================================================
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Filter for install command output.
|
|
97
|
+
* Keep: error lines, warning lines, summary lines (added/removed/changed counts).
|
|
98
|
+
* Remove: progress bars, download lines, individual package install lines.
|
|
99
|
+
*/
|
|
100
|
+
function filterInstallOutput(output: string): string {
|
|
101
|
+
const lines = output.split("\n")
|
|
102
|
+
const kept: string[] = []
|
|
103
|
+
let removedCount = 0
|
|
104
|
+
|
|
105
|
+
for (const line of lines) {
|
|
106
|
+
// Always keep: empty lines (structure), errors, warnings, summary
|
|
107
|
+
if (
|
|
108
|
+
line.trim() === "" ||
|
|
109
|
+
/(error|ERR!|fatal|FAILED|failed|abort|timeout)/i.test(line) ||
|
|
110
|
+
/\b(warn|WARN|warning|deprecat)\b/i.test(line) ||
|
|
111
|
+
/\b(added|removed|changed|updated|audited)\s+\d+/i.test(line) ||
|
|
112
|
+
/\bpackages?\b.*\b(look|found|installed|removed)\b/i.test(line) ||
|
|
113
|
+
/\bvulnerabilities\b/i.test(line) ||
|
|
114
|
+
/^\s*$/.test(line) ||
|
|
115
|
+
line.startsWith("[") ||
|
|
116
|
+
/^\s*(\d+ packages?|up to date|already|nothing)/i.test(line)
|
|
117
|
+
) {
|
|
118
|
+
kept.push(line)
|
|
119
|
+
} else {
|
|
120
|
+
removedCount++
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Deduplicate consecutive empty lines
|
|
125
|
+
return deduplicateEmptyLines(kept.join("\n"))
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Filter for test command output.
|
|
130
|
+
* Keep: FAIL, ERROR, summary, test suite headers.
|
|
131
|
+
* Remove: PASS lines (✓, ✔, PASS, ✓), progress dots.
|
|
132
|
+
*/
|
|
133
|
+
function filterTestOutput(output: string): string {
|
|
134
|
+
const lines = output.split("\n")
|
|
135
|
+
const kept: string[] = []
|
|
136
|
+
let removedCount = 0
|
|
137
|
+
|
|
138
|
+
for (const line of lines) {
|
|
139
|
+
// Always keep: errors, failures, summary, suite names
|
|
140
|
+
if (
|
|
141
|
+
line.trim() === "" ||
|
|
142
|
+
/\b(FAIL|FAILING|FAILED|ERROR|✕|✗|×|✘|broken)\b/i.test(line) ||
|
|
143
|
+
/\b(fail|failure|error|timeout|crash)\b.*\d/i.test(line) ||
|
|
144
|
+
/\b(tests?|suites?|files?|passed|failed|skipped|pending)\s*[:=]?\s*\d/i.test(line) ||
|
|
145
|
+
/\b(RUN|RUNS)\s/i.test(line) || // vitest running indicator
|
|
146
|
+
/^\s*(FAIL|PASS|ERROR|SKIP|TODO)\s+(?:\s|\[)/i.test(line) ||
|
|
147
|
+
/^[\s│┌┐└┘├┤┬┴┼─]+/.test(line) || // Box drawing (test summaries)
|
|
148
|
+
/\b(synopsis|assert|expect|received|expected)\b/i.test(line) ||
|
|
149
|
+
/\s+(→|at)\s+.*\.\w+\s*$/i.test(line) || // Stack traces
|
|
150
|
+
/\d+\.\d+\s*(s|ms)\s*$/.test(line.trim()) || // Timing lines
|
|
151
|
+
line.includes("Test Suites:") ||
|
|
152
|
+
line.includes("Tests:") ||
|
|
153
|
+
line.includes("Snapshots:") ||
|
|
154
|
+
line.includes("Time:")
|
|
155
|
+
) {
|
|
156
|
+
kept.push(line)
|
|
157
|
+
} else if (
|
|
158
|
+
// Specifically skip PASS lines
|
|
159
|
+
/^\s*✓|✔|PASS|✅|·/.test(line) ||
|
|
160
|
+
/^\s*√/.test(line) ||
|
|
161
|
+
/^\s*ok\s+\d+/.test(line) ||
|
|
162
|
+
/^\s*\.\s*$/.test(line.trim()) // Progress dots
|
|
163
|
+
) {
|
|
164
|
+
removedCount++
|
|
165
|
+
} else {
|
|
166
|
+
// Keep lines that don't match pass patterns (context lines)
|
|
167
|
+
kept.push(line)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return deduplicateEmptyLines(kept.join("\n"))
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Filter for build/compile output.
|
|
176
|
+
* Keep: error lines, warning lines (deduplicated), summary.
|
|
177
|
+
* Remove: successful compilation lines, progress indicators.
|
|
178
|
+
*/
|
|
179
|
+
function filterBuildOutput(output: string): string {
|
|
180
|
+
const lines = output.split("\n")
|
|
181
|
+
const kept: string[] = []
|
|
182
|
+
const seenWarnings = new Set<string>()
|
|
183
|
+
let removedCount = 0
|
|
184
|
+
|
|
185
|
+
for (const line of lines) {
|
|
186
|
+
if (line.trim() === "") {
|
|
187
|
+
kept.push(line)
|
|
188
|
+
continue
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Always keep errors
|
|
192
|
+
if (/\b(error|Error:|fatal|FAILED|failed)\b/i.test(line)) {
|
|
193
|
+
kept.push(line)
|
|
194
|
+
continue
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Deduplicate warnings (keep first occurrence)
|
|
198
|
+
if (/\b(warning|warn)\b/i.test(line)) {
|
|
199
|
+
const normalized = line.replace(/:\d+:\d+/g, ":N:N").trim()
|
|
200
|
+
if (!seenWarnings.has(normalized)) {
|
|
201
|
+
seenWarnings.add(normalized)
|
|
202
|
+
kept.push(line)
|
|
203
|
+
} else {
|
|
204
|
+
kept.push(` ... (${countByPattern(lines, normalized)} similar warnings omitted)`)
|
|
205
|
+
}
|
|
206
|
+
continue
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Keep summary lines
|
|
210
|
+
if (
|
|
211
|
+
/\b(compiled|built|generated|error|warning)\s*\d/i.test(line) ||
|
|
212
|
+
/\b(Finished|Compiling|Building)\b/i.test(line) ||
|
|
213
|
+
/error\(s\)|warning\(s\)/i.test(line)
|
|
214
|
+
) {
|
|
215
|
+
kept.push(line)
|
|
216
|
+
continue
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Skip progress/success lines
|
|
220
|
+
if (
|
|
221
|
+
/^\s*(Compiling|Building|Generating)\s/i.test(line) ||
|
|
222
|
+
/^\s*\[.*\]\s/.test(line) // [1/10] style progress
|
|
223
|
+
) {
|
|
224
|
+
removedCount++
|
|
225
|
+
continue
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Keep everything else (might be important context)
|
|
229
|
+
kept.push(line)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return deduplicateEmptyLines(kept.join("\n"))
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* General filter for any large output.
|
|
237
|
+
* Applies safe, universal compression:
|
|
238
|
+
* - Collapse consecutive empty lines
|
|
239
|
+
* - Remove ANSI escape sequences
|
|
240
|
+
* - Trim trailing whitespace
|
|
241
|
+
*/
|
|
242
|
+
function filterGenericOutput(output: string): string {
|
|
243
|
+
// Remove ANSI escape sequences
|
|
244
|
+
const cleaned = output.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "")
|
|
245
|
+
// Deduplicate empty lines
|
|
246
|
+
return deduplicateEmptyLines(cleaned)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ============================================================================
|
|
250
|
+
// Helpers
|
|
251
|
+
// ============================================================================
|
|
252
|
+
|
|
253
|
+
function deduplicateEmptyLines(text: string): string {
|
|
254
|
+
return text.replace(/\n{3,}/g, "\n\n")
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function countByPattern(lines: string[], normalizedPattern: string): number {
|
|
258
|
+
let count = 0
|
|
259
|
+
for (const line of lines) {
|
|
260
|
+
if (line.replace(/:\d+:\d+/g, ":N:N").trim() === normalizedPattern) {
|
|
261
|
+
count++
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return count
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function formatBytes(bytes: number): string {
|
|
268
|
+
if (bytes < 1024) return `${bytes}B`
|
|
269
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`
|
|
270
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ============================================================================
|
|
274
|
+
// Main Filter Function
|
|
275
|
+
// ============================================================================
|
|
276
|
+
|
|
277
|
+
/** Minimum output size (bytes) to trigger any filtering */
|
|
278
|
+
const MIN_FILTER_THRESHOLD = 2048 // 2KB
|
|
279
|
+
|
|
280
|
+
/** Maximum filtered output size (bytes) — aggressive cutoff for very large outputs */
|
|
281
|
+
const MAX_FILTERED_SIZE = 30720 // 30KB
|
|
282
|
+
|
|
283
|
+
export function filterBashOutput(input: BashOutputFilterInput): BashOutputFilterResult {
|
|
284
|
+
const { command, output, isError, fullOutputPath } = input
|
|
285
|
+
const originalBytes = Buffer.byteLength(output, "utf-8")
|
|
286
|
+
|
|
287
|
+
// Don't filter small outputs
|
|
288
|
+
if (originalBytes < MIN_FILTER_THRESHOLD) {
|
|
289
|
+
return {
|
|
290
|
+
output,
|
|
291
|
+
filtered: false,
|
|
292
|
+
originalBytes,
|
|
293
|
+
filteredBytes: originalBytes,
|
|
294
|
+
strategy: "none (below threshold)",
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Don't filter error outputs — they need to be complete
|
|
299
|
+
if (isError) {
|
|
300
|
+
return {
|
|
301
|
+
output,
|
|
302
|
+
filtered: false,
|
|
303
|
+
originalBytes,
|
|
304
|
+
filteredBytes: originalBytes,
|
|
305
|
+
strategy: "none (error output)",
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const matched = classifyCommand(command)
|
|
310
|
+
|
|
311
|
+
// If no pattern matched, apply generic filtering for large outputs
|
|
312
|
+
if (!matched) {
|
|
313
|
+
if (originalBytes > MAX_FILTERED_SIZE) {
|
|
314
|
+
const filtered = filterGenericOutput(output)
|
|
315
|
+
const filteredBytes = Buffer.byteLength(filtered, "utf-8")
|
|
316
|
+
if (filteredBytes < originalBytes * 0.9) {
|
|
317
|
+
return {
|
|
318
|
+
output: appendFilterNotice(filtered, originalBytes, filteredBytes, "generic", fullOutputPath),
|
|
319
|
+
filtered: true,
|
|
320
|
+
originalBytes,
|
|
321
|
+
filteredBytes,
|
|
322
|
+
strategy: "generic (large unknown command)",
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return {
|
|
327
|
+
output,
|
|
328
|
+
filtered: false,
|
|
329
|
+
originalBytes,
|
|
330
|
+
filteredBytes: originalBytes,
|
|
331
|
+
strategy: "none (unknown command, below aggressive threshold)",
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Don't filter "purpose" commands (search, git diff) unless extremely large
|
|
336
|
+
if (!matched.isMeans && originalBytes < MAX_FILTERED_SIZE) {
|
|
337
|
+
return {
|
|
338
|
+
output,
|
|
339
|
+
filtered: false,
|
|
340
|
+
originalBytes,
|
|
341
|
+
filteredBytes: originalBytes,
|
|
342
|
+
strategy: `none (${matched.category}: purpose command, within limit)`,
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Apply category-specific filter
|
|
347
|
+
let filtered: string
|
|
348
|
+
let strategyName: string
|
|
349
|
+
|
|
350
|
+
switch (matched.category) {
|
|
351
|
+
case "install":
|
|
352
|
+
filtered = filterInstallOutput(output)
|
|
353
|
+
strategyName = "install (errors + summary only)"
|
|
354
|
+
break
|
|
355
|
+
case "test":
|
|
356
|
+
filtered = filterTestOutput(output)
|
|
357
|
+
strategyName = "test (failures + summary only)"
|
|
358
|
+
break
|
|
359
|
+
case "build":
|
|
360
|
+
filtered = filterBuildOutput(output)
|
|
361
|
+
strategyName = "build (errors + deduplicated warnings)"
|
|
362
|
+
break
|
|
363
|
+
default:
|
|
364
|
+
// Purpose commands that are very large get generic filtering
|
|
365
|
+
filtered = filterGenericOutput(output)
|
|
366
|
+
strategyName = `generic (${matched.category}: large output)`
|
|
367
|
+
break
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const filteredBytes = Buffer.byteLength(filtered, "utf-8")
|
|
371
|
+
|
|
372
|
+
// If filtering didn't help much (< 20% reduction), keep original
|
|
373
|
+
if (filteredBytes > originalBytes * 0.8) {
|
|
374
|
+
return {
|
|
375
|
+
output,
|
|
376
|
+
filtered: false,
|
|
377
|
+
originalBytes,
|
|
378
|
+
filteredBytes,
|
|
379
|
+
strategy: `none (${matched.category}: filtering insufficient, <20% reduction)`,
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return {
|
|
384
|
+
output: appendFilterNotice(filtered, originalBytes, filteredBytes, strategyName, fullOutputPath),
|
|
385
|
+
filtered: true,
|
|
386
|
+
originalBytes,
|
|
387
|
+
filteredBytes,
|
|
388
|
+
strategy: strategyName,
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function appendFilterNotice(
|
|
393
|
+
output: string,
|
|
394
|
+
originalBytes: number,
|
|
395
|
+
filteredBytes: number,
|
|
396
|
+
strategy: string,
|
|
397
|
+
fullOutputPath?: string,
|
|
398
|
+
): string {
|
|
399
|
+
const saved = formatBytes(originalBytes - filteredBytes)
|
|
400
|
+
const notice = `\n\n[Output filtered: ${formatBytes(originalBytes)} → ${formatBytes(filteredBytes)} (${saved} saved, strategy: ${strategy})${fullOutputPath ? `. Full output: ${fullOutputPath}` : ""}]`
|
|
401
|
+
return output + notice
|
|
402
|
+
}
|