@compilr-dev/sdk 0.8.0 → 0.9.1
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/dist/agent.js +8 -0
- package/dist/compressors/bash.d.ts +12 -0
- package/dist/compressors/bash.js +342 -0
- package/dist/compressors/index.d.ts +31 -0
- package/dist/compressors/index.js +169 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -0
- package/package.json +1 -1
package/dist/agent.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* CompilrAgent — high-level wrapper around @compilr-dev/agents Agent
|
|
3
3
|
*/
|
|
4
4
|
import { Agent, ContextManager, createSuggestTool, } from '@compilr-dev/agents';
|
|
5
|
+
import { createCompressorHook } from './compressors/index.js';
|
|
5
6
|
import { resolveProvider } from './provider.js';
|
|
6
7
|
import { resolvePreset } from './presets/index.js';
|
|
7
8
|
import { assembleTools, deduplicateTools } from './tools.js';
|
|
@@ -241,6 +242,13 @@ class CompilrAgentImpl {
|
|
|
241
242
|
: [];
|
|
242
243
|
mergedHooks.beforeLLM = [...existingHooks, ...capabilityHooks];
|
|
243
244
|
}
|
|
245
|
+
// Add output compressor as the first afterTool hook (runs before delegation)
|
|
246
|
+
const existingAfterTool = config?.hooks?.afterTool
|
|
247
|
+
? Array.isArray(config.hooks.afterTool)
|
|
248
|
+
? config.hooks.afterTool
|
|
249
|
+
: [config.hooks.afterTool]
|
|
250
|
+
: [];
|
|
251
|
+
mergedHooks.afterTool = [createCompressorHook(), ...existingAfterTool];
|
|
244
252
|
// Build observation mask config — SDK defaults include platform tools
|
|
245
253
|
const observationMask = config?.context?.observationMask !== false
|
|
246
254
|
? {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bash Output Compressors — command-specific output compression
|
|
3
|
+
*
|
|
4
|
+
* Each compressor understands the structure of a CLI tool's output
|
|
5
|
+
* and strips noise (progress bars, boilerplate, redundant info)
|
|
6
|
+
* while preserving signal (errors, changed files, test failures).
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Compress bash command output based on the command that was run.
|
|
10
|
+
* Returns null if no compressor matches (output passes through unchanged).
|
|
11
|
+
*/
|
|
12
|
+
export declare function compressBashOutput(command: string, stdout: string): string | null;
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bash Output Compressors — command-specific output compression
|
|
3
|
+
*
|
|
4
|
+
* Each compressor understands the structure of a CLI tool's output
|
|
5
|
+
* and strips noise (progress bars, boilerplate, redundant info)
|
|
6
|
+
* while preserving signal (errors, changed files, test failures).
|
|
7
|
+
*/
|
|
8
|
+
// ─── Router ─────────────────────────────────────────────────────────────────
|
|
9
|
+
/**
|
|
10
|
+
* Compress bash command output based on the command that was run.
|
|
11
|
+
* Returns null if no compressor matches (output passes through unchanged).
|
|
12
|
+
*/
|
|
13
|
+
export function compressBashOutput(command, stdout) {
|
|
14
|
+
const cmd = command.trim();
|
|
15
|
+
// git commands
|
|
16
|
+
if (cmd.match(/^git\s+status/))
|
|
17
|
+
return compressGitStatus(stdout);
|
|
18
|
+
if (cmd.match(/^git\s+log/))
|
|
19
|
+
return compressGitLog(stdout);
|
|
20
|
+
if (cmd.match(/^git\s+diff/))
|
|
21
|
+
return compressGitDiff(stdout);
|
|
22
|
+
if (cmd.match(/^git\s+(add|commit|push|pull|fetch|merge|rebase|checkout|switch|branch)/))
|
|
23
|
+
return compressGitAction(stdout);
|
|
24
|
+
// npm/yarn/pnpm
|
|
25
|
+
if (cmd.match(/^(npm|yarn|pnpm)\s+install/))
|
|
26
|
+
return compressNpmInstall(stdout);
|
|
27
|
+
if (cmd.match(/^(npm|yarn|pnpm)\s+(test|run\s+test)/))
|
|
28
|
+
return compressTestOutput(stdout);
|
|
29
|
+
if (cmd.match(/^(npm|yarn|pnpm)\s+run\s+(lint|eslint)/))
|
|
30
|
+
return compressLintOutput(stdout);
|
|
31
|
+
if (cmd.match(/^(npm|yarn|pnpm)\s+run\s+(build|tsc)/))
|
|
32
|
+
return compressBuildOutput(stdout);
|
|
33
|
+
// Direct test runners
|
|
34
|
+
if (cmd.match(/^(jest|vitest|mocha|pytest|cargo\s+test|go\s+test)/))
|
|
35
|
+
return compressTestOutput(stdout);
|
|
36
|
+
// Direct linters
|
|
37
|
+
if (cmd.match(/^(eslint|tsc|ruff|cargo\s+clippy|golangci-lint)/))
|
|
38
|
+
return compressLintOutput(stdout);
|
|
39
|
+
// ls / find / tree
|
|
40
|
+
if (cmd.match(/^(ls|find|tree|fd)\b/))
|
|
41
|
+
return compressFileList(stdout);
|
|
42
|
+
// curl / wget (HTTP responses)
|
|
43
|
+
if (cmd.match(/^(curl|wget)\b/))
|
|
44
|
+
return compressCurlOutput(stdout);
|
|
45
|
+
return null; // No compressor matched
|
|
46
|
+
}
|
|
47
|
+
// ─── Git Status ─────────────────────────────────────────────────────────────
|
|
48
|
+
function compressGitStatus(output) {
|
|
49
|
+
const lines = output.split('\n').filter((l) => l.trim());
|
|
50
|
+
if (lines.length <= 20)
|
|
51
|
+
return output;
|
|
52
|
+
const sections = [];
|
|
53
|
+
let current = null;
|
|
54
|
+
for (const line of lines) {
|
|
55
|
+
if (line.startsWith('On branch') ||
|
|
56
|
+
line.startsWith('Your branch') ||
|
|
57
|
+
line.startsWith('HEAD detached')) {
|
|
58
|
+
sections.push({ header: line, files: [] });
|
|
59
|
+
}
|
|
60
|
+
else if (line.match(/^(Changes|Untracked|Unmerged)/)) {
|
|
61
|
+
current = { header: line, files: [] };
|
|
62
|
+
sections.push(current);
|
|
63
|
+
}
|
|
64
|
+
else if (current &&
|
|
65
|
+
(line.startsWith('\t') || line.match(/^\s+(modified|new file|deleted|renamed)/))) {
|
|
66
|
+
current.files.push(line.trim());
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Rebuild with capped file lists
|
|
70
|
+
const maxFilesPerSection = 15;
|
|
71
|
+
const result = [];
|
|
72
|
+
for (const section of sections) {
|
|
73
|
+
result.push(section.header);
|
|
74
|
+
const shown = section.files.slice(0, maxFilesPerSection);
|
|
75
|
+
for (const f of shown)
|
|
76
|
+
result.push(` ${f}`);
|
|
77
|
+
if (section.files.length > maxFilesPerSection) {
|
|
78
|
+
result.push(` ... +${String(section.files.length - maxFilesPerSection)} more files`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return result.join('\n');
|
|
82
|
+
}
|
|
83
|
+
// ─── Git Log ────────────────────────────────────────────────────────────────
|
|
84
|
+
function compressGitLog(output) {
|
|
85
|
+
const lines = output.split('\n');
|
|
86
|
+
if (lines.length <= 30)
|
|
87
|
+
return output;
|
|
88
|
+
// Keep first 25 lines (recent commits), summarize the rest
|
|
89
|
+
const kept = lines.slice(0, 25);
|
|
90
|
+
const remaining = lines.slice(25).filter((l) => l.match(/^commit\s/));
|
|
91
|
+
if (remaining.length > 0) {
|
|
92
|
+
kept.push(`\n... +${String(remaining.length)} older commits omitted`);
|
|
93
|
+
}
|
|
94
|
+
return kept.join('\n');
|
|
95
|
+
}
|
|
96
|
+
// ─── Git Diff ───────────────────────────────────────────────────────────────
|
|
97
|
+
function compressGitDiff(output) {
|
|
98
|
+
const lines = output.split('\n');
|
|
99
|
+
if (lines.length <= 50)
|
|
100
|
+
return output;
|
|
101
|
+
// Keep diff headers and first N lines of each file, collapse large hunks
|
|
102
|
+
const result = [];
|
|
103
|
+
let currentFile = '';
|
|
104
|
+
let hunkLines = 0;
|
|
105
|
+
const maxHunkLines = 30;
|
|
106
|
+
for (const line of lines) {
|
|
107
|
+
if (line.startsWith('diff --git')) {
|
|
108
|
+
currentFile = line;
|
|
109
|
+
hunkLines = 0;
|
|
110
|
+
result.push(line);
|
|
111
|
+
}
|
|
112
|
+
else if (line.startsWith('---') || line.startsWith('+++') || line.startsWith('@@')) {
|
|
113
|
+
hunkLines = 0;
|
|
114
|
+
result.push(line);
|
|
115
|
+
}
|
|
116
|
+
else if (line.startsWith('+') || line.startsWith('-') || line.startsWith(' ')) {
|
|
117
|
+
hunkLines++;
|
|
118
|
+
if (hunkLines <= maxHunkLines) {
|
|
119
|
+
result.push(line);
|
|
120
|
+
}
|
|
121
|
+
else if (hunkLines === maxHunkLines + 1) {
|
|
122
|
+
result.push(`... (hunk truncated, ${currentFile.split(' b/')[1] ?? 'file'} continues)`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
result.push(line);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return result.join('\n');
|
|
130
|
+
}
|
|
131
|
+
// ─── Git Action (add, commit, push, pull, etc.) ─────────────────────────────
|
|
132
|
+
function compressGitAction(output) {
|
|
133
|
+
const lines = output.split('\n').filter((l) => l.trim());
|
|
134
|
+
if (lines.length <= 15)
|
|
135
|
+
return output;
|
|
136
|
+
// Keep summary lines, strip verbose file lists
|
|
137
|
+
const important = lines.filter((l) => l.match(/^(\[|To |From |Already|Everything|Branch|Updating|Fast-forward|CONFLICT|error:|fatal:|warning:|\s*\d+ file|create mode|delete mode)/) ||
|
|
138
|
+
l.includes('->') ||
|
|
139
|
+
l.includes('insertions') ||
|
|
140
|
+
l.includes('deletions'));
|
|
141
|
+
if (important.length > 0 && important.length < lines.length) {
|
|
142
|
+
const omitted = lines.length - important.length;
|
|
143
|
+
important.push(`(${String(omitted)} lines of detail omitted)`);
|
|
144
|
+
return important.join('\n');
|
|
145
|
+
}
|
|
146
|
+
return output;
|
|
147
|
+
}
|
|
148
|
+
// ─── npm install ────────────────────────────────────────────────────────────
|
|
149
|
+
function compressNpmInstall(output) {
|
|
150
|
+
const lines = output.split('\n');
|
|
151
|
+
if (lines.length <= 10)
|
|
152
|
+
return output;
|
|
153
|
+
// Keep: added/removed/changed summary, warnings, errors
|
|
154
|
+
// Strip: progress bars, individual package resolutions, timing
|
|
155
|
+
const kept = lines.filter((l) => {
|
|
156
|
+
const t = l.trim();
|
|
157
|
+
if (!t)
|
|
158
|
+
return false;
|
|
159
|
+
if (t.startsWith('npm warn') || t.startsWith('npm error') || t.startsWith('npm ERR'))
|
|
160
|
+
return true;
|
|
161
|
+
if (t.match(/^added \d|^removed \d|^changed \d|^up to date/))
|
|
162
|
+
return true;
|
|
163
|
+
if (t.includes('vulnerabilit'))
|
|
164
|
+
return true;
|
|
165
|
+
if (t.startsWith('found '))
|
|
166
|
+
return true;
|
|
167
|
+
return false;
|
|
168
|
+
});
|
|
169
|
+
if (kept.length === 0)
|
|
170
|
+
return output; // Couldn't parse — return original
|
|
171
|
+
return kept.join('\n');
|
|
172
|
+
}
|
|
173
|
+
// ─── Test Output ────────────────────────────────────────────────────────────
|
|
174
|
+
function compressTestOutput(output) {
|
|
175
|
+
const lines = output.split('\n');
|
|
176
|
+
if (lines.length <= 30)
|
|
177
|
+
return output;
|
|
178
|
+
const result = [];
|
|
179
|
+
let inFailure = false;
|
|
180
|
+
let failureLines = 0;
|
|
181
|
+
const maxFailureLines = 20;
|
|
182
|
+
for (const line of lines) {
|
|
183
|
+
const t = line.trim();
|
|
184
|
+
// Always keep: summary lines, failures, errors
|
|
185
|
+
if (t.match(/^(Tests?|Test Suites?|FAIL|PASS|ERROR|✓|✗|✘|×|●|Ran \d|passed|failed|\d+ passing|\d+ failing|Test Files|Duration)/i)) {
|
|
186
|
+
result.push(line);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
// Keep failure context (limited)
|
|
190
|
+
if (t.match(/^(FAIL|✗|✘|×|Error:|AssertionError|Expected|Received|at\s)/i) || inFailure) {
|
|
191
|
+
if (t.match(/^(FAIL|✗|✘|×)/i)) {
|
|
192
|
+
inFailure = true;
|
|
193
|
+
failureLines = 0;
|
|
194
|
+
}
|
|
195
|
+
failureLines++;
|
|
196
|
+
if (failureLines <= maxFailureLines) {
|
|
197
|
+
result.push(line);
|
|
198
|
+
}
|
|
199
|
+
if (t === '' || t.match(/^(PASS|✓)/)) {
|
|
200
|
+
inFailure = false;
|
|
201
|
+
}
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
// Strip: individual passing test names, progress, timing of each test
|
|
205
|
+
// (these are the bulk of test output — passing tests don't need names)
|
|
206
|
+
}
|
|
207
|
+
if (result.length < lines.length) {
|
|
208
|
+
const omitted = lines.length - result.length;
|
|
209
|
+
result.push(`(${String(omitted)} lines of passing test detail omitted)`);
|
|
210
|
+
}
|
|
211
|
+
return result.join('\n');
|
|
212
|
+
}
|
|
213
|
+
// ─── Lint Output ────────────────────────────────────────────────────────────
|
|
214
|
+
function compressLintOutput(output) {
|
|
215
|
+
const lines = output.split('\n');
|
|
216
|
+
if (lines.length <= 30)
|
|
217
|
+
return output;
|
|
218
|
+
// Group errors by rule, cap per rule
|
|
219
|
+
const errors = [];
|
|
220
|
+
const warnings = [];
|
|
221
|
+
const summaryLines = [];
|
|
222
|
+
const ruleCount = new Map();
|
|
223
|
+
const maxPerRule = 5;
|
|
224
|
+
for (const line of lines) {
|
|
225
|
+
const t = line.trim();
|
|
226
|
+
// Summary lines
|
|
227
|
+
if (t.match(/^✖|^\d+ problem|^\d+ error|^\d+ warning|^error:|^warning:/i)) {
|
|
228
|
+
summaryLines.push(line);
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
// ESLint-style: "file:line:col error message rule"
|
|
232
|
+
const ruleMatch = t.match(/\s+(error|warning)\s+.+\s+(\S+)$/);
|
|
233
|
+
if (ruleMatch) {
|
|
234
|
+
const rule = ruleMatch[2];
|
|
235
|
+
const count = (ruleCount.get(rule) ?? 0) + 1;
|
|
236
|
+
ruleCount.set(rule, count);
|
|
237
|
+
if (count <= maxPerRule) {
|
|
238
|
+
if (ruleMatch[1] === 'error')
|
|
239
|
+
errors.push(line);
|
|
240
|
+
else
|
|
241
|
+
warnings.push(line);
|
|
242
|
+
}
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
// TypeScript-style: "file(line,col): error TS..."
|
|
246
|
+
if (t.match(/error TS\d+/)) {
|
|
247
|
+
errors.push(line);
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const result = [...errors, ...warnings.slice(0, 20)];
|
|
252
|
+
// Show rules that were capped
|
|
253
|
+
for (const [rule, count] of ruleCount) {
|
|
254
|
+
if (count > maxPerRule) {
|
|
255
|
+
result.push(` ... +${String(count - maxPerRule)} more ${rule}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
result.push(...summaryLines);
|
|
259
|
+
if (result.length < lines.length) {
|
|
260
|
+
return result.join('\n');
|
|
261
|
+
}
|
|
262
|
+
return output;
|
|
263
|
+
}
|
|
264
|
+
// ─── Build Output ───────────────────────────────────────────────────────────
|
|
265
|
+
function compressBuildOutput(output) {
|
|
266
|
+
const lines = output.split('\n');
|
|
267
|
+
if (lines.length <= 20)
|
|
268
|
+
return output;
|
|
269
|
+
// Keep errors and summary, strip progress/compilation messages
|
|
270
|
+
const kept = lines.filter((l) => {
|
|
271
|
+
const t = l.trim();
|
|
272
|
+
if (!t)
|
|
273
|
+
return false;
|
|
274
|
+
if (t.match(/^(error|Error|ERROR|warning|Warning|WARN)/))
|
|
275
|
+
return true;
|
|
276
|
+
if (t.match(/^(✓|✗|✘|Done|Built|Compiled|Successfully|Failed|FAIL)/))
|
|
277
|
+
return true;
|
|
278
|
+
if (t.match(/error TS\d+/))
|
|
279
|
+
return true;
|
|
280
|
+
if (t.includes('bundle') && t.includes('kB'))
|
|
281
|
+
return true; // Bundle size info
|
|
282
|
+
return false;
|
|
283
|
+
});
|
|
284
|
+
if (kept.length > 0 && kept.length < lines.length * 0.7) {
|
|
285
|
+
const omitted = lines.length - kept.length;
|
|
286
|
+
kept.push(`(${String(omitted)} lines of build output omitted)`);
|
|
287
|
+
return kept.join('\n');
|
|
288
|
+
}
|
|
289
|
+
return output;
|
|
290
|
+
}
|
|
291
|
+
// ─── File List (ls, find, tree) ─────────────────────────────────────────────
|
|
292
|
+
function compressFileList(output) {
|
|
293
|
+
const lines = output.split('\n').filter((l) => l.trim());
|
|
294
|
+
if (lines.length <= 20)
|
|
295
|
+
return output;
|
|
296
|
+
// Group by top-level directory, cap entries
|
|
297
|
+
const groups = new Map();
|
|
298
|
+
for (const line of lines) {
|
|
299
|
+
const parts = line.replace(/^\.\//, '').split('/');
|
|
300
|
+
const dir = parts.length > 1 ? parts[0] : '.';
|
|
301
|
+
const arr = groups.get(dir) ?? [];
|
|
302
|
+
arr.push(line);
|
|
303
|
+
groups.set(dir, arr);
|
|
304
|
+
}
|
|
305
|
+
const maxPerDir = 10;
|
|
306
|
+
const result = [];
|
|
307
|
+
for (const [dir, files] of groups) {
|
|
308
|
+
if (dir !== '.')
|
|
309
|
+
result.push(`${dir}/`);
|
|
310
|
+
const shown = files.slice(0, maxPerDir);
|
|
311
|
+
for (const f of shown)
|
|
312
|
+
result.push(f);
|
|
313
|
+
if (files.length > maxPerDir) {
|
|
314
|
+
result.push(` ... +${String(files.length - maxPerDir)} more in ${dir}/`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (result.length < lines.length) {
|
|
318
|
+
result.push(`\n(${String(lines.length)} total entries, showing ${String(result.length)} lines)`);
|
|
319
|
+
}
|
|
320
|
+
return result.join('\n');
|
|
321
|
+
}
|
|
322
|
+
// ─── curl / wget ────────────────────────────────────────────────────────────
|
|
323
|
+
function compressCurlOutput(output) {
|
|
324
|
+
const lines = output.split('\n');
|
|
325
|
+
if (lines.length <= 30)
|
|
326
|
+
return output;
|
|
327
|
+
// Strip progress bars (curl -# output), keep headers and body
|
|
328
|
+
const kept = lines.filter((l) => {
|
|
329
|
+
const t = l.trim();
|
|
330
|
+
// Strip progress indicators
|
|
331
|
+
if (t.match(/^[#\s]*\d+(\.\d+)?%/) || t.match(/^\s*\d+\s+\d+\s+\d+\s+\d+/))
|
|
332
|
+
return false;
|
|
333
|
+
// Strip curl stats
|
|
334
|
+
if (t.match(/^\s*(Dload|Upload|Total|Spent|Left|Speed)/))
|
|
335
|
+
return false;
|
|
336
|
+
return true;
|
|
337
|
+
});
|
|
338
|
+
if (kept.length < lines.length) {
|
|
339
|
+
return kept.join('\n');
|
|
340
|
+
}
|
|
341
|
+
return output;
|
|
342
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool Output Compressors — Format-aware output compression
|
|
3
|
+
*
|
|
4
|
+
* Reduces token usage by 60-90% on common tool outputs.
|
|
5
|
+
* Understands output structure (git, npm, test runners, etc.)
|
|
6
|
+
* and strips noise while preserving signal.
|
|
7
|
+
*
|
|
8
|
+
* Runs as an AfterToolHook — transparent to agents.
|
|
9
|
+
* Inspired by rtk-ai/rtk.
|
|
10
|
+
*/
|
|
11
|
+
import type { AfterToolHook } from '@compilr-dev/agents';
|
|
12
|
+
export interface CompressorConfig {
|
|
13
|
+
/** Minimum output length to trigger compression (default: 500) */
|
|
14
|
+
minLength?: number;
|
|
15
|
+
/** Enable/disable specific compressors */
|
|
16
|
+
disabled?: string[];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Create an AfterToolHook that compresses tool outputs.
|
|
20
|
+
*
|
|
21
|
+
* Usage:
|
|
22
|
+
* ```typescript
|
|
23
|
+
* const agent = createCompilrAgent({
|
|
24
|
+
* hooks: {
|
|
25
|
+
* afterTool: [createCompressorHook()],
|
|
26
|
+
* },
|
|
27
|
+
* });
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export declare function createCompressorHook(config?: CompressorConfig): AfterToolHook;
|
|
31
|
+
export { compressBashOutput } from './bash.js';
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool Output Compressors — Format-aware output compression
|
|
3
|
+
*
|
|
4
|
+
* Reduces token usage by 60-90% on common tool outputs.
|
|
5
|
+
* Understands output structure (git, npm, test runners, etc.)
|
|
6
|
+
* and strips noise while preserving signal.
|
|
7
|
+
*
|
|
8
|
+
* Runs as an AfterToolHook — transparent to agents.
|
|
9
|
+
* Inspired by rtk-ai/rtk.
|
|
10
|
+
*/
|
|
11
|
+
import { compressBashOutput } from './bash.js';
|
|
12
|
+
// ─── Hook Factory ───────────────────────────────────────────────────────────
|
|
13
|
+
/**
|
|
14
|
+
* Create an AfterToolHook that compresses tool outputs.
|
|
15
|
+
*
|
|
16
|
+
* Usage:
|
|
17
|
+
* ```typescript
|
|
18
|
+
* const agent = createCompilrAgent({
|
|
19
|
+
* hooks: {
|
|
20
|
+
* afterTool: [createCompressorHook()],
|
|
21
|
+
* },
|
|
22
|
+
* });
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export function createCompressorHook(config) {
|
|
26
|
+
const minLength = config?.minLength ?? 500;
|
|
27
|
+
return (ctx) => {
|
|
28
|
+
if (!ctx.result.success)
|
|
29
|
+
return undefined;
|
|
30
|
+
const result = ctx.result.result;
|
|
31
|
+
if (!result || typeof result !== 'object')
|
|
32
|
+
return undefined;
|
|
33
|
+
const toolName = ctx.toolName;
|
|
34
|
+
// ── Bash tool ────────────────────────────────────────────────────
|
|
35
|
+
if (toolName === 'bash') {
|
|
36
|
+
const bashResult = result;
|
|
37
|
+
const stdout = bashResult.stdout;
|
|
38
|
+
if (!stdout || stdout.length < minLength)
|
|
39
|
+
return undefined;
|
|
40
|
+
// Get the command from tool input
|
|
41
|
+
const command = ctx.input.command ?? '';
|
|
42
|
+
const compressed = compressBashOutput(command, stdout);
|
|
43
|
+
if (compressed !== null && compressed.length < stdout.length) {
|
|
44
|
+
return {
|
|
45
|
+
result: {
|
|
46
|
+
...ctx.result,
|
|
47
|
+
result: { ...bashResult, stdout: compressed },
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// ── Grep tool ────────────────────────────────────────────────────
|
|
53
|
+
if (toolName === 'grep') {
|
|
54
|
+
const grepResult = result;
|
|
55
|
+
const content = grepResult.content;
|
|
56
|
+
if (content && typeof content === 'string' && content.length >= minLength) {
|
|
57
|
+
const compressed = compressGrepOutput(content);
|
|
58
|
+
if (compressed.length < content.length) {
|
|
59
|
+
return {
|
|
60
|
+
result: {
|
|
61
|
+
...ctx.result,
|
|
62
|
+
result: { ...grepResult, content: compressed },
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// ── Read file tool ───────────────────────────────────────────────
|
|
69
|
+
if (toolName === 'read_file') {
|
|
70
|
+
const fileResult = result;
|
|
71
|
+
const content = fileResult.content;
|
|
72
|
+
if (content && typeof content === 'string' && content.length >= minLength) {
|
|
73
|
+
const compressed = compressFileContent(content);
|
|
74
|
+
if (compressed.length < content.length) {
|
|
75
|
+
return {
|
|
76
|
+
result: {
|
|
77
|
+
...ctx.result,
|
|
78
|
+
result: { ...fileResult, content: compressed },
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return undefined;
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
// ─── Grep Compressor ────────────────────────────────────────────────────────
|
|
88
|
+
/**
|
|
89
|
+
* Compress grep output: deduplicate, cap matches per file.
|
|
90
|
+
*/
|
|
91
|
+
function compressGrepOutput(content) {
|
|
92
|
+
const lines = content.split('\n');
|
|
93
|
+
if (lines.length <= 50)
|
|
94
|
+
return content;
|
|
95
|
+
// Group by file
|
|
96
|
+
const groups = new Map();
|
|
97
|
+
for (const line of lines) {
|
|
98
|
+
const colonIdx = line.indexOf(':');
|
|
99
|
+
if (colonIdx > 0) {
|
|
100
|
+
const file = line.substring(0, colonIdx);
|
|
101
|
+
const rest = line.substring(colonIdx + 1);
|
|
102
|
+
const arr = groups.get(file) ?? [];
|
|
103
|
+
arr.push(rest);
|
|
104
|
+
groups.set(file, arr);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Cap at 10 matches per file
|
|
108
|
+
const maxPerFile = 10;
|
|
109
|
+
const compressed = [];
|
|
110
|
+
for (const [file, matches] of groups) {
|
|
111
|
+
const shown = matches.slice(0, maxPerFile);
|
|
112
|
+
for (const m of shown) {
|
|
113
|
+
compressed.push(`${file}:${m}`);
|
|
114
|
+
}
|
|
115
|
+
if (matches.length > maxPerFile) {
|
|
116
|
+
compressed.push(` ... +${String(matches.length - maxPerFile)} more matches in ${file}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return compressed.join('\n');
|
|
120
|
+
}
|
|
121
|
+
// ─── File Content Compressor ────────────────────────────────────────────────
|
|
122
|
+
/**
|
|
123
|
+
* Compress file content: collapse consecutive blank lines, large comment blocks.
|
|
124
|
+
*/
|
|
125
|
+
function compressFileContent(content) {
|
|
126
|
+
const lines = content.split('\n');
|
|
127
|
+
if (lines.length <= 100)
|
|
128
|
+
return content;
|
|
129
|
+
const result = [];
|
|
130
|
+
let consecutiveBlanks = 0;
|
|
131
|
+
let inBlockComment = false;
|
|
132
|
+
let blockCommentLines = 0;
|
|
133
|
+
for (const line of lines) {
|
|
134
|
+
const trimmed = line.trim();
|
|
135
|
+
// Collapse consecutive blank lines
|
|
136
|
+
if (trimmed === '') {
|
|
137
|
+
consecutiveBlanks++;
|
|
138
|
+
if (consecutiveBlanks <= 1)
|
|
139
|
+
result.push(line);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
consecutiveBlanks = 0;
|
|
143
|
+
// Collapse large block comments (keep first and last line)
|
|
144
|
+
if (trimmed.startsWith('/*') && !trimmed.endsWith('*/')) {
|
|
145
|
+
inBlockComment = true;
|
|
146
|
+
blockCommentLines = 0;
|
|
147
|
+
result.push(line);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (inBlockComment) {
|
|
151
|
+
blockCommentLines++;
|
|
152
|
+
if (trimmed.endsWith('*/') || trimmed === '*/') {
|
|
153
|
+
if (blockCommentLines > 5) {
|
|
154
|
+
result.push(` // ... ${String(blockCommentLines - 1)} comment lines omitted`);
|
|
155
|
+
}
|
|
156
|
+
result.push(line);
|
|
157
|
+
inBlockComment = false;
|
|
158
|
+
}
|
|
159
|
+
else if (blockCommentLines <= 3) {
|
|
160
|
+
result.push(line);
|
|
161
|
+
}
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
result.push(line);
|
|
165
|
+
}
|
|
166
|
+
return result.join('\n');
|
|
167
|
+
}
|
|
168
|
+
// ─── Re-exports ─────────────────────────────────────────────────────────────
|
|
169
|
+
export { compressBashOutput } from './bash.js';
|
package/dist/index.d.ts
CHANGED
|
@@ -79,3 +79,5 @@ export { readFileTool, writeFileTool, createBashTool, bashTool, bashOutputTool,
|
|
|
79
79
|
export { gitStatusTool, gitDiffTool, gitLogTool, gitCommitTool, gitBranchTool, gitStashTool, gitBlameTool, gitFileHistoryTool, detectProjectTool, findProjectRootTool, runTestsTool, runLintTool, runBuildTool, runFormatTool, findDefinitionTool, findReferencesTool, findTodosTool, checkOutdatedTool, findVulnerabilitiesTool, analyzeTestCoverageTool, getFileStructureTool, getComplexityTool, allCodingTools, unifiedTools, } from '@compilr-dev/agents-coding';
|
|
80
80
|
export { createLogger, createSilentLogger } from '@compilr-dev/logger';
|
|
81
81
|
export type { Logger, LoggerOptions, LogLevel } from '@compilr-dev/logger';
|
|
82
|
+
export { createCompressorHook, compressBashOutput } from './compressors/index.js';
|
|
83
|
+
export type { CompressorConfig } from './compressors/index.js';
|
package/dist/index.js
CHANGED
|
@@ -225,3 +225,7 @@ allCodingTools, unifiedTools, } from '@compilr-dev/agents-coding';
|
|
|
225
225
|
// Logger (re-export for consumers)
|
|
226
226
|
// =============================================================================
|
|
227
227
|
export { createLogger, createSilentLogger } from '@compilr-dev/logger';
|
|
228
|
+
// =============================================================================
|
|
229
|
+
// Tool Output Compressors
|
|
230
|
+
// =============================================================================
|
|
231
|
+
export { createCompressorHook, compressBashOutput } from './compressors/index.js';
|