@kolisachint/hoocode-agent 0.4.74 → 0.4.76
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/CHANGELOG.md +57 -0
- package/dist/cli/args.d.ts +0 -2
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +0 -11
- package/dist/cli/args.js.map +1 -1
- package/dist/core/agent-frontmatter.d.ts.map +1 -1
- package/dist/core/agent-frontmatter.js +2 -2
- package/dist/core/agent-frontmatter.js.map +1 -1
- package/dist/core/agent-session-services.d.ts +0 -1
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js +0 -1
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/agent-session.d.ts +1 -17
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +10 -199
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/bash-executor.d.ts.map +1 -1
- package/dist/core/bash-executor.js +7 -4
- package/dist/core/bash-executor.js.map +1 -1
- package/dist/core/compaction/utils.d.ts.map +1 -1
- package/dist/core/compaction/utils.js +8 -5
- package/dist/core/compaction/utils.js.map +1 -1
- package/dist/core/exec.d.ts.map +1 -1
- package/dist/core/exec.js +13 -4
- package/dist/core/exec.js.map +1 -1
- package/dist/core/model-categories.d.ts +28 -0
- package/dist/core/model-categories.d.ts.map +1 -0
- package/dist/core/model-categories.js +51 -0
- package/dist/core/model-categories.js.map +1 -0
- package/dist/core/model-registry.d.ts +0 -6
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +0 -32
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/sdk.d.ts +0 -6
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +0 -1
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +16 -3
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-manager.d.ts +14 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/subagent-pool-instance.d.ts.map +1 -1
- package/dist/core/subagent-pool-instance.js +9 -1
- package/dist/core/subagent-pool-instance.js.map +1 -1
- package/dist/core/subagent-pool.d.ts +5 -0
- package/dist/core/subagent-pool.d.ts.map +1 -1
- package/dist/core/subagent-pool.js +13 -2
- package/dist/core/subagent-pool.js.map +1 -1
- package/dist/core/tools/edit-diff.d.ts +3 -0
- package/dist/core/tools/edit-diff.d.ts.map +1 -1
- package/dist/core/tools/edit-diff.js +132 -40
- package/dist/core/tools/edit-diff.js.map +1 -1
- package/dist/core/tools/edit.d.ts.map +1 -1
- package/dist/core/tools/edit.js +7 -1
- package/dist/core/tools/edit.js.map +1 -1
- package/dist/core/tools/fd-utils.d.ts +23 -0
- package/dist/core/tools/fd-utils.d.ts.map +1 -0
- package/dist/core/tools/fd-utils.js +47 -0
- package/dist/core/tools/fd-utils.js.map +1 -0
- package/dist/core/tools/file-mutation-queue.d.ts.map +1 -1
- package/dist/core/tools/file-mutation-queue.js +15 -2
- package/dist/core/tools/file-mutation-queue.js.map +1 -1
- package/dist/core/tools/find.d.ts.map +1 -1
- package/dist/core/tools/find.js +3 -24
- package/dist/core/tools/find.js.map +1 -1
- package/dist/core/tools/glob.d.ts +42 -0
- package/dist/core/tools/glob.d.ts.map +1 -0
- package/dist/core/tools/glob.js +432 -0
- package/dist/core/tools/glob.js.map +1 -0
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js +34 -19
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/index.d.ts +4 -1
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +13 -1
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/ls.d.ts +12 -1
- package/dist/core/tools/ls.d.ts.map +1 -1
- package/dist/core/tools/ls.js +58 -33
- package/dist/core/tools/ls.js.map +1 -1
- package/dist/core/tools/output-accumulator.d.ts +4 -0
- package/dist/core/tools/output-accumulator.d.ts.map +1 -1
- package/dist/core/tools/output-accumulator.js +39 -20
- package/dist/core/tools/output-accumulator.js.map +1 -1
- package/dist/core/tools/output-compression.d.ts +53 -0
- package/dist/core/tools/output-compression.d.ts.map +1 -0
- package/dist/core/tools/output-compression.js +392 -0
- package/dist/core/tools/output-compression.js.map +1 -0
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +9 -1
- package/dist/core/tools/read.js.map +1 -1
- package/dist/core/tools/render-utils.d.ts.map +1 -1
- package/dist/core/tools/render-utils.js +9 -2
- package/dist/core/tools/render-utils.js.map +1 -1
- package/dist/core/tools/write.d.ts.map +1 -1
- package/dist/core/tools/write.js +4 -1
- package/dist/core/tools/write.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +0 -1
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/bash-execution.js +2 -2
- package/dist/modes/interactive/components/bash-execution.js.map +1 -1
- package/dist/modes/interactive/components/task-panel.d.ts.map +1 -1
- package/dist/modes/interactive/components/task-panel.js +10 -26
- package/dist/modes/interactive/components/task-panel.js.map +1 -1
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js +16 -0
- package/dist/utils/tools-manager.js.map +1 -1
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +4 -4
- package/templates/agents/explore.md +1 -1
- package/templates/agents/general-purpose.md +1 -1
- package/templates/agents/plan.md +1 -1
- package/dist/core/routing/local-inference.d.ts +0 -128
- package/dist/core/routing/local-inference.d.ts.map +0 -1
- package/dist/core/routing/local-inference.js +0 -145
- package/dist/core/routing/local-inference.js.map +0 -1
- package/dist/core/routing/metrics.d.ts +0 -27
- package/dist/core/routing/metrics.d.ts.map +0 -1
- package/dist/core/routing/metrics.js +0 -35
- package/dist/core/routing/metrics.js.map +0 -1
- package/dist/core/routing/mlx-server.d.ts +0 -43
- package/dist/core/routing/mlx-server.d.ts.map +0 -1
- package/dist/core/routing/mlx-server.js +0 -115
- package/dist/core/routing/mlx-server.js.map +0 -1
- package/dist/core/routing/tool-result-prompts.d.ts +0 -26
- package/dist/core/routing/tool-result-prompts.d.ts.map +0 -1
- package/dist/core/routing/tool-result-prompts.js +0 -45
- package/dist/core/routing/tool-result-prompts.js.map +0 -1
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lossless output compression utilities for tool outputs.
|
|
3
|
+
*
|
|
4
|
+
* All compression is strictly lossless - no useful information is removed.
|
|
5
|
+
* Compression is command-aware where possible, and general-purpose otherwise.
|
|
6
|
+
*/
|
|
7
|
+
/** Minimum output size (in bytes) to apply compression. Below this, overhead exceeds savings. */
|
|
8
|
+
const MIN_COMPRESSION_SIZE = 1024; // 1KB
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// General Compression (applied to all outputs)
|
|
11
|
+
// ============================================================================
|
|
12
|
+
/**
|
|
13
|
+
* Collapse 3+ consecutive blank lines to 2 (preserves paragraph separation).
|
|
14
|
+
*/
|
|
15
|
+
export function collapseBlankLines(text) {
|
|
16
|
+
return text.replace(/\n{3,}/g, "\n\n");
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Strip trailing whitespace from each line.
|
|
20
|
+
*/
|
|
21
|
+
export function stripTrailingWhitespace(text) {
|
|
22
|
+
return text
|
|
23
|
+
.split("\n")
|
|
24
|
+
.map((line) => line.trimEnd())
|
|
25
|
+
.join("\n");
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Remove duplicate consecutive lines (keeps first occurrence).
|
|
29
|
+
*/
|
|
30
|
+
export function removeDuplicateLines(text) {
|
|
31
|
+
const lines = text.split("\n");
|
|
32
|
+
const result = [];
|
|
33
|
+
let prevLine;
|
|
34
|
+
for (const line of lines) {
|
|
35
|
+
if (line !== prevLine) {
|
|
36
|
+
result.push(line);
|
|
37
|
+
prevLine = line;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return result.join("\n");
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Apply general lossless compression to any output.
|
|
44
|
+
* Single-pass implementation for efficiency.
|
|
45
|
+
*/
|
|
46
|
+
export function compressGeneral(text) {
|
|
47
|
+
const lines = text.split("\n");
|
|
48
|
+
const result = [];
|
|
49
|
+
let blankCount = 0;
|
|
50
|
+
for (const line of lines) {
|
|
51
|
+
const trimmed = line.trimEnd();
|
|
52
|
+
if (trimmed === "") {
|
|
53
|
+
blankCount++;
|
|
54
|
+
// Allow at most 2 consecutive blank lines
|
|
55
|
+
if (blankCount <= 2) {
|
|
56
|
+
result.push(trimmed);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
blankCount = 0;
|
|
61
|
+
result.push(trimmed);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return result.join("\n");
|
|
65
|
+
}
|
|
66
|
+
// ============================================================================
|
|
67
|
+
// Command-Specific Compression (bash only)
|
|
68
|
+
// ============================================================================
|
|
69
|
+
/**
|
|
70
|
+
* Detect the primary command from a bash command string.
|
|
71
|
+
*/
|
|
72
|
+
function detectCommand(command) {
|
|
73
|
+
const trimmed = command.trim();
|
|
74
|
+
// Strip leading env vars (FOO=bar command)
|
|
75
|
+
const withoutEnv = trimmed.replace(/^[A-Z_]+=\S+\s+/, "");
|
|
76
|
+
// Strip leading sudo
|
|
77
|
+
const withoutSudo = withoutEnv.replace(/^sudo\s+/, "");
|
|
78
|
+
// Get first word (the actual command)
|
|
79
|
+
const firstWord = withoutSudo.split(/\s+/)[0] ?? "";
|
|
80
|
+
// Strip path prefix if any
|
|
81
|
+
return firstWord.split("/").pop() ?? "";
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Compress npm/yarn/pnpm install output.
|
|
85
|
+
* - Remove download progress lines
|
|
86
|
+
* - Collapse "added N packages" summaries
|
|
87
|
+
*/
|
|
88
|
+
function compressNpmInstall(output) {
|
|
89
|
+
const lines = output.split("\n");
|
|
90
|
+
const result = [];
|
|
91
|
+
for (const line of lines) {
|
|
92
|
+
// Skip download progress (e.g., "fetchMetadata: ...", "reify: ...")
|
|
93
|
+
if (/^(fetchMetadata|reify|audit|idealTree|sill|warn)\b/.test(line)) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
// Skip verbose fetch/cache lines
|
|
97
|
+
if (/^\s*(http|https|fetch|cache|tarball|extract)\b/.test(line)) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
// Keep everything else (errors, warnings, summaries)
|
|
101
|
+
result.push(line);
|
|
102
|
+
}
|
|
103
|
+
return result.join("\n");
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Compress git diff output.
|
|
107
|
+
* - Strip file headers (diff --git, index, ---, +++)
|
|
108
|
+
* - Collapse unchanged context lines
|
|
109
|
+
*/
|
|
110
|
+
function compressGitDiff(output) {
|
|
111
|
+
const lines = output.split("\n");
|
|
112
|
+
const result = [];
|
|
113
|
+
let contextCount = 0;
|
|
114
|
+
for (const line of lines) {
|
|
115
|
+
// Skip git diff metadata
|
|
116
|
+
if (/^(diff --git|index [0-9a-f]+|--- a\/|\+\+\+ b\/)/.test(line)) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
// Count context lines (lines starting with space)
|
|
120
|
+
if (line.startsWith(" ") && !line.startsWith(" ")) {
|
|
121
|
+
contextCount++;
|
|
122
|
+
// Only show first 2 context lines per hunk, then skip
|
|
123
|
+
if (contextCount > 2) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
contextCount = 0;
|
|
129
|
+
}
|
|
130
|
+
result.push(line);
|
|
131
|
+
}
|
|
132
|
+
return result.join("\n");
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Compress cargo/rustc test output.
|
|
136
|
+
* - Collapse passing tests, keep failures
|
|
137
|
+
*/
|
|
138
|
+
function compressCargoTest(output) {
|
|
139
|
+
const lines = output.split("\n");
|
|
140
|
+
const result = [];
|
|
141
|
+
let passingCount = 0;
|
|
142
|
+
for (const line of lines) {
|
|
143
|
+
// Count passing test lines (e.g., "test foo ... ok")
|
|
144
|
+
if (/^test\s+.+\s+\.\.\.\s+ok\s*$/.test(line)) {
|
|
145
|
+
passingCount++;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
// Count ignored tests
|
|
149
|
+
if (/^test\s+.+\s+\.\.\.\s+ignored\s*$/.test(line)) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
// Show test summary line
|
|
153
|
+
if (/^test result:/.test(line)) {
|
|
154
|
+
result.push(line);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
// Keep failures, errors, and everything else
|
|
158
|
+
if (passingCount > 0 && line === "") {
|
|
159
|
+
// Add summary before blank line
|
|
160
|
+
result.push(` (${passingCount} passing tests omitted)`);
|
|
161
|
+
passingCount = 0;
|
|
162
|
+
}
|
|
163
|
+
result.push(line);
|
|
164
|
+
}
|
|
165
|
+
// Handle case where output ends with passing tests
|
|
166
|
+
if (passingCount > 0) {
|
|
167
|
+
result.push(` (${passingCount} passing tests omitted)`);
|
|
168
|
+
}
|
|
169
|
+
return result.join("\n");
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Compress docker build output.
|
|
173
|
+
* - Strip layer download progress
|
|
174
|
+
* - Keep build steps and errors
|
|
175
|
+
*/
|
|
176
|
+
function compressDockerBuild(output) {
|
|
177
|
+
const lines = output.split("\n");
|
|
178
|
+
const result = [];
|
|
179
|
+
for (const line of lines) {
|
|
180
|
+
// Skip download progress (e.g., "Downloading layer...")
|
|
181
|
+
if (/^(Downloading|Pulling|Extracting|Waiting|Verifying)/.test(line)) {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
// Skip progress bars
|
|
185
|
+
if (/[\u2588\u2591\u2592]{10,}/.test(line)) {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
// Keep build steps (FROM, RUN, COPY, etc.) and errors
|
|
189
|
+
result.push(line);
|
|
190
|
+
}
|
|
191
|
+
return result.join("\n");
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Compress jest/mocha test output.
|
|
195
|
+
* - Collapse passing tests
|
|
196
|
+
* - Keep failures
|
|
197
|
+
*/
|
|
198
|
+
function compressJsTest(output) {
|
|
199
|
+
const lines = output.split("\n");
|
|
200
|
+
const result = [];
|
|
201
|
+
let passingCount = 0;
|
|
202
|
+
for (const line of lines) {
|
|
203
|
+
// Skip passing test indicators (✓, ✔, ○)
|
|
204
|
+
if (/^\s*[✓✔○●]\s+/.test(line)) {
|
|
205
|
+
passingCount++;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
// Skip passing test lines (e.g., " PASS src/foo.test.ts")
|
|
209
|
+
if (/^\s*PASS\s+/.test(line)) {
|
|
210
|
+
passingCount++;
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
// Show test summary
|
|
214
|
+
if (/^(Tests|Test Suites):/.test(line)) {
|
|
215
|
+
result.push(line);
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
// Keep failures (×, ✗, FAIL) and everything else
|
|
219
|
+
if (passingCount > 0 && (line.includes("FAIL") || line.includes("×") || line.includes("✗"))) {
|
|
220
|
+
result.push(` (${passingCount} passing tests omitted)`);
|
|
221
|
+
passingCount = 0;
|
|
222
|
+
}
|
|
223
|
+
result.push(line);
|
|
224
|
+
}
|
|
225
|
+
if (passingCount > 0) {
|
|
226
|
+
result.push(` (${passingCount} passing tests omitted)`);
|
|
227
|
+
}
|
|
228
|
+
return result.join("\n");
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Compress go test output.
|
|
232
|
+
* - Collapse passing tests
|
|
233
|
+
* - Keep failures
|
|
234
|
+
*/
|
|
235
|
+
function compressGoTest(output) {
|
|
236
|
+
const lines = output.split("\n");
|
|
237
|
+
const result = [];
|
|
238
|
+
let passingCount = 0;
|
|
239
|
+
for (const line of lines) {
|
|
240
|
+
// Skip passing test lines (e.g., "--- PASS: TestFoo (0.00s)")
|
|
241
|
+
if (/^---\s+PASS:/.test(line)) {
|
|
242
|
+
passingCount++;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
// Skip PASS lines
|
|
246
|
+
if (/^PASS$/.test(line)) {
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
// Show test summary
|
|
250
|
+
if (/^(ok|FAIL)\s+/.test(line)) {
|
|
251
|
+
result.push(line);
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
// Keep failures and everything else
|
|
255
|
+
if (passingCount > 0 && line.includes("FAIL")) {
|
|
256
|
+
result.push(` (${passingCount} passing tests omitted)`);
|
|
257
|
+
passingCount = 0;
|
|
258
|
+
}
|
|
259
|
+
result.push(line);
|
|
260
|
+
}
|
|
261
|
+
if (passingCount > 0) {
|
|
262
|
+
result.push(` (${passingCount} passing tests omitted)`);
|
|
263
|
+
}
|
|
264
|
+
return result.join("\n");
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Apply command-specific compression based on the detected command.
|
|
268
|
+
*/
|
|
269
|
+
function compressCommandSpecific(command, output) {
|
|
270
|
+
const cmd = detectCommand(command);
|
|
271
|
+
switch (cmd) {
|
|
272
|
+
case "npm":
|
|
273
|
+
case "yarn":
|
|
274
|
+
case "pnpm":
|
|
275
|
+
// Check if it's an install/add command
|
|
276
|
+
if (/\b(install|add|i)\b/.test(command)) {
|
|
277
|
+
return compressNpmInstall(output);
|
|
278
|
+
}
|
|
279
|
+
break;
|
|
280
|
+
case "git":
|
|
281
|
+
// Check if it's a diff command
|
|
282
|
+
if (/\bdiff\b/.test(command)) {
|
|
283
|
+
return compressGitDiff(output);
|
|
284
|
+
}
|
|
285
|
+
break;
|
|
286
|
+
case "cargo":
|
|
287
|
+
// Check if it's a test command
|
|
288
|
+
if (/\btest\b/.test(command)) {
|
|
289
|
+
return compressCargoTest(output);
|
|
290
|
+
}
|
|
291
|
+
break;
|
|
292
|
+
case "docker":
|
|
293
|
+
// Check if it's a build command
|
|
294
|
+
if (/\bbuild\b/.test(command)) {
|
|
295
|
+
return compressDockerBuild(output);
|
|
296
|
+
}
|
|
297
|
+
break;
|
|
298
|
+
case "jest":
|
|
299
|
+
case "mocha":
|
|
300
|
+
case "vitest":
|
|
301
|
+
case "pytest":
|
|
302
|
+
return compressJsTest(output);
|
|
303
|
+
case "go":
|
|
304
|
+
// Check if it's a test command
|
|
305
|
+
if (/\btest\b/.test(command)) {
|
|
306
|
+
return compressGoTest(output);
|
|
307
|
+
}
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
return output;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Strip common noise patterns from any bash output.
|
|
314
|
+
* This is applied to all commands regardless of detection.
|
|
315
|
+
*/
|
|
316
|
+
function stripNoisePatterns(text) {
|
|
317
|
+
return (text
|
|
318
|
+
// Strip npm warnings (keep errors)
|
|
319
|
+
.replace(/^npm (warn|notice) .+$/gm, "")
|
|
320
|
+
// Strip yarn warnings
|
|
321
|
+
.replace(/^warning .+$/gm, "")
|
|
322
|
+
// Strip pnpm warnings
|
|
323
|
+
.replace(/^pnpm (warn|notice) .+$/gm, "")
|
|
324
|
+
// Strip common shell warnings
|
|
325
|
+
.replace(/^bash: .+ warning: .+$/gm, "")
|
|
326
|
+
// Strip "The command completed with non-zero exit status" noise
|
|
327
|
+
.replace(/^The command exited with exit code .+$/gm, ""));
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Apply all compression to a bash command output.
|
|
331
|
+
* This is the main entry point for bash output compression.
|
|
332
|
+
*
|
|
333
|
+
* @param command - The bash command that was executed
|
|
334
|
+
* @param output - The raw output from the command
|
|
335
|
+
* @returns Compressed output (lossless)
|
|
336
|
+
*/
|
|
337
|
+
export function compressBashOutput(command, output) {
|
|
338
|
+
// Skip compression for small outputs (overhead > savings)
|
|
339
|
+
if (Buffer.byteLength(output, "utf-8") < MIN_COMPRESSION_SIZE) {
|
|
340
|
+
return output;
|
|
341
|
+
}
|
|
342
|
+
let result = output;
|
|
343
|
+
// Phase 1: Strip noise patterns
|
|
344
|
+
result = stripNoisePatterns(result);
|
|
345
|
+
// Phase 2: Command-specific compression
|
|
346
|
+
result = compressCommandSpecific(command, result);
|
|
347
|
+
// Phase 3: General compression
|
|
348
|
+
result = compressGeneral(result);
|
|
349
|
+
return result;
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Apply compression to grep output.
|
|
353
|
+
* This strips redundant context and normalizes output.
|
|
354
|
+
*/
|
|
355
|
+
export function compressGrepOutput(output) {
|
|
356
|
+
if (Buffer.byteLength(output, "utf-8") < MIN_COMPRESSION_SIZE) {
|
|
357
|
+
return output;
|
|
358
|
+
}
|
|
359
|
+
return compressGeneral(output);
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Apply compression to read output.
|
|
363
|
+
* This normalizes whitespace but preserves content.
|
|
364
|
+
*/
|
|
365
|
+
export function compressReadOutput(output) {
|
|
366
|
+
// For read, we only do light compression - no line collapsing
|
|
367
|
+
if (Buffer.byteLength(output, "utf-8") < MIN_COMPRESSION_SIZE) {
|
|
368
|
+
return output;
|
|
369
|
+
}
|
|
370
|
+
return stripTrailingWhitespace(output);
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Apply compression to find output.
|
|
374
|
+
* This normalizes paths and removes duplicates.
|
|
375
|
+
*/
|
|
376
|
+
export function compressFindOutput(output) {
|
|
377
|
+
if (Buffer.byteLength(output, "utf-8") < MIN_COMPRESSION_SIZE) {
|
|
378
|
+
return output;
|
|
379
|
+
}
|
|
380
|
+
return compressGeneral(output);
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Apply compression to ls output.
|
|
384
|
+
* This normalizes formatting.
|
|
385
|
+
*/
|
|
386
|
+
export function compressLsOutput(output) {
|
|
387
|
+
if (Buffer.byteLength(output, "utf-8") < MIN_COMPRESSION_SIZE) {
|
|
388
|
+
return output;
|
|
389
|
+
}
|
|
390
|
+
return compressGeneral(output);
|
|
391
|
+
}
|
|
392
|
+
//# sourceMappingURL=output-compression.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"output-compression.js","sourceRoot":"","sources":["../../../src/core/tools/output-compression.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Lossless output compression utilities for tool outputs.\n *\n * All compression is strictly lossless - no useful information is removed.\n * Compression is command-aware where possible, and general-purpose otherwise.\n */\n\n/** Minimum output size (in bytes) to apply compression. Below this, overhead exceeds savings. */\nconst MIN_COMPRESSION_SIZE = 1024; // 1KB\n\n// ============================================================================\n// General Compression (applied to all outputs)\n// ============================================================================\n\n/**\n * Collapse 3+ consecutive blank lines to 2 (preserves paragraph separation).\n */\nexport function collapseBlankLines(text: string): string {\n\treturn text.replace(/\\n{3,}/g, \"\\n\\n\");\n}\n\n/**\n * Strip trailing whitespace from each line.\n */\nexport function stripTrailingWhitespace(text: string): string {\n\treturn text\n\t\t.split(\"\\n\")\n\t\t.map((line) => line.trimEnd())\n\t\t.join(\"\\n\");\n}\n\n/**\n * Remove duplicate consecutive lines (keeps first occurrence).\n */\nexport function removeDuplicateLines(text: string): string {\n\tconst lines = text.split(\"\\n\");\n\tconst result: string[] = [];\n\tlet prevLine: string | undefined;\n\n\tfor (const line of lines) {\n\t\tif (line !== prevLine) {\n\t\t\tresult.push(line);\n\t\t\tprevLine = line;\n\t\t}\n\t}\n\n\treturn result.join(\"\\n\");\n}\n\n/**\n * Apply general lossless compression to any output.\n * Single-pass implementation for efficiency.\n */\nexport function compressGeneral(text: string): string {\n\tconst lines = text.split(\"\\n\");\n\tconst result: string[] = [];\n\tlet blankCount = 0;\n\n\tfor (const line of lines) {\n\t\tconst trimmed = line.trimEnd();\n\n\t\tif (trimmed === \"\") {\n\t\t\tblankCount++;\n\t\t\t// Allow at most 2 consecutive blank lines\n\t\t\tif (blankCount <= 2) {\n\t\t\t\tresult.push(trimmed);\n\t\t\t}\n\t\t} else {\n\t\t\tblankCount = 0;\n\t\t\tresult.push(trimmed);\n\t\t}\n\t}\n\n\treturn result.join(\"\\n\");\n}\n\n// ============================================================================\n// Command-Specific Compression (bash only)\n// ============================================================================\n\n/**\n * Detect the primary command from a bash command string.\n */\nfunction detectCommand(command: string): string {\n\tconst trimmed = command.trim();\n\t// Strip leading env vars (FOO=bar command)\n\tconst withoutEnv = trimmed.replace(/^[A-Z_]+=\\S+\\s+/, \"\");\n\t// Strip leading sudo\n\tconst withoutSudo = withoutEnv.replace(/^sudo\\s+/, \"\");\n\t// Get first word (the actual command)\n\tconst firstWord = withoutSudo.split(/\\s+/)[0] ?? \"\";\n\t// Strip path prefix if any\n\treturn firstWord.split(\"/\").pop() ?? \"\";\n}\n\n/**\n * Compress npm/yarn/pnpm install output.\n * - Remove download progress lines\n * - Collapse \"added N packages\" summaries\n */\nfunction compressNpmInstall(output: string): string {\n\tconst lines = output.split(\"\\n\");\n\tconst result: string[] = [];\n\n\tfor (const line of lines) {\n\t\t// Skip download progress (e.g., \"fetchMetadata: ...\", \"reify: ...\")\n\t\tif (/^(fetchMetadata|reify|audit|idealTree|sill|warn)\\b/.test(line)) {\n\t\t\tcontinue;\n\t\t}\n\t\t// Skip verbose fetch/cache lines\n\t\tif (/^\\s*(http|https|fetch|cache|tarball|extract)\\b/.test(line)) {\n\t\t\tcontinue;\n\t\t}\n\t\t// Keep everything else (errors, warnings, summaries)\n\t\tresult.push(line);\n\t}\n\n\treturn result.join(\"\\n\");\n}\n\n/**\n * Compress git diff output.\n * - Strip file headers (diff --git, index, ---, +++)\n * - Collapse unchanged context lines\n */\nfunction compressGitDiff(output: string): string {\n\tconst lines = output.split(\"\\n\");\n\tconst result: string[] = [];\n\tlet contextCount = 0;\n\n\tfor (const line of lines) {\n\t\t// Skip git diff metadata\n\t\tif (/^(diff --git|index [0-9a-f]+|--- a\\/|\\+\\+\\+ b\\/)/.test(line)) {\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Count context lines (lines starting with space)\n\t\tif (line.startsWith(\" \") && !line.startsWith(\" \")) {\n\t\t\tcontextCount++;\n\t\t\t// Only show first 2 context lines per hunk, then skip\n\t\t\tif (contextCount > 2) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t} else {\n\t\t\tcontextCount = 0;\n\t\t}\n\n\t\tresult.push(line);\n\t}\n\n\treturn result.join(\"\\n\");\n}\n\n/**\n * Compress cargo/rustc test output.\n * - Collapse passing tests, keep failures\n */\nfunction compressCargoTest(output: string): string {\n\tconst lines = output.split(\"\\n\");\n\tconst result: string[] = [];\n\tlet passingCount = 0;\n\n\tfor (const line of lines) {\n\t\t// Count passing test lines (e.g., \"test foo ... ok\")\n\t\tif (/^test\\s+.+\\s+\\.\\.\\.\\s+ok\\s*$/.test(line)) {\n\t\t\tpassingCount++;\n\t\t\tcontinue;\n\t\t}\n\t\t// Count ignored tests\n\t\tif (/^test\\s+.+\\s+\\.\\.\\.\\s+ignored\\s*$/.test(line)) {\n\t\t\tcontinue;\n\t\t}\n\t\t// Show test summary line\n\t\tif (/^test result:/.test(line)) {\n\t\t\tresult.push(line);\n\t\t\tcontinue;\n\t\t}\n\t\t// Keep failures, errors, and everything else\n\t\tif (passingCount > 0 && line === \"\") {\n\t\t\t// Add summary before blank line\n\t\t\tresult.push(` (${passingCount} passing tests omitted)`);\n\t\t\tpassingCount = 0;\n\t\t}\n\t\tresult.push(line);\n\t}\n\n\t// Handle case where output ends with passing tests\n\tif (passingCount > 0) {\n\t\tresult.push(` (${passingCount} passing tests omitted)`);\n\t}\n\n\treturn result.join(\"\\n\");\n}\n\n/**\n * Compress docker build output.\n * - Strip layer download progress\n * - Keep build steps and errors\n */\nfunction compressDockerBuild(output: string): string {\n\tconst lines = output.split(\"\\n\");\n\tconst result: string[] = [];\n\n\tfor (const line of lines) {\n\t\t// Skip download progress (e.g., \"Downloading layer...\")\n\t\tif (/^(Downloading|Pulling|Extracting|Waiting|Verifying)/.test(line)) {\n\t\t\tcontinue;\n\t\t}\n\t\t// Skip progress bars\n\t\tif (/[\\u2588\\u2591\\u2592]{10,}/.test(line)) {\n\t\t\tcontinue;\n\t\t}\n\t\t// Keep build steps (FROM, RUN, COPY, etc.) and errors\n\t\tresult.push(line);\n\t}\n\n\treturn result.join(\"\\n\");\n}\n\n/**\n * Compress jest/mocha test output.\n * - Collapse passing tests\n * - Keep failures\n */\nfunction compressJsTest(output: string): string {\n\tconst lines = output.split(\"\\n\");\n\tconst result: string[] = [];\n\tlet passingCount = 0;\n\n\tfor (const line of lines) {\n\t\t// Skip passing test indicators (✓, ✔, ○)\n\t\tif (/^\\s*[✓✔○●]\\s+/.test(line)) {\n\t\t\tpassingCount++;\n\t\t\tcontinue;\n\t\t}\n\t\t// Skip passing test lines (e.g., \" PASS src/foo.test.ts\")\n\t\tif (/^\\s*PASS\\s+/.test(line)) {\n\t\t\tpassingCount++;\n\t\t\tcontinue;\n\t\t}\n\t\t// Show test summary\n\t\tif (/^(Tests|Test Suites):/.test(line)) {\n\t\t\tresult.push(line);\n\t\t\tcontinue;\n\t\t}\n\t\t// Keep failures (×, ✗, FAIL) and everything else\n\t\tif (passingCount > 0 && (line.includes(\"FAIL\") || line.includes(\"×\") || line.includes(\"✗\"))) {\n\t\t\tresult.push(` (${passingCount} passing tests omitted)`);\n\t\t\tpassingCount = 0;\n\t\t}\n\t\tresult.push(line);\n\t}\n\n\tif (passingCount > 0) {\n\t\tresult.push(` (${passingCount} passing tests omitted)`);\n\t}\n\n\treturn result.join(\"\\n\");\n}\n\n/**\n * Compress go test output.\n * - Collapse passing tests\n * - Keep failures\n */\nfunction compressGoTest(output: string): string {\n\tconst lines = output.split(\"\\n\");\n\tconst result: string[] = [];\n\tlet passingCount = 0;\n\n\tfor (const line of lines) {\n\t\t// Skip passing test lines (e.g., \"--- PASS: TestFoo (0.00s)\")\n\t\tif (/^---\\s+PASS:/.test(line)) {\n\t\t\tpassingCount++;\n\t\t\tcontinue;\n\t\t}\n\t\t// Skip PASS lines\n\t\tif (/^PASS$/.test(line)) {\n\t\t\tcontinue;\n\t\t}\n\t\t// Show test summary\n\t\tif (/^(ok|FAIL)\\s+/.test(line)) {\n\t\t\tresult.push(line);\n\t\t\tcontinue;\n\t\t}\n\t\t// Keep failures and everything else\n\t\tif (passingCount > 0 && line.includes(\"FAIL\")) {\n\t\t\tresult.push(` (${passingCount} passing tests omitted)`);\n\t\t\tpassingCount = 0;\n\t\t}\n\t\tresult.push(line);\n\t}\n\n\tif (passingCount > 0) {\n\t\tresult.push(` (${passingCount} passing tests omitted)`);\n\t}\n\n\treturn result.join(\"\\n\");\n}\n\n/**\n * Apply command-specific compression based on the detected command.\n */\nfunction compressCommandSpecific(command: string, output: string): string {\n\tconst cmd = detectCommand(command);\n\n\tswitch (cmd) {\n\t\tcase \"npm\":\n\t\tcase \"yarn\":\n\t\tcase \"pnpm\":\n\t\t\t// Check if it's an install/add command\n\t\t\tif (/\\b(install|add|i)\\b/.test(command)) {\n\t\t\t\treturn compressNpmInstall(output);\n\t\t\t}\n\t\t\tbreak;\n\t\tcase \"git\":\n\t\t\t// Check if it's a diff command\n\t\t\tif (/\\bdiff\\b/.test(command)) {\n\t\t\t\treturn compressGitDiff(output);\n\t\t\t}\n\t\t\tbreak;\n\t\tcase \"cargo\":\n\t\t\t// Check if it's a test command\n\t\t\tif (/\\btest\\b/.test(command)) {\n\t\t\t\treturn compressCargoTest(output);\n\t\t\t}\n\t\t\tbreak;\n\t\tcase \"docker\":\n\t\t\t// Check if it's a build command\n\t\t\tif (/\\bbuild\\b/.test(command)) {\n\t\t\t\treturn compressDockerBuild(output);\n\t\t\t}\n\t\t\tbreak;\n\t\tcase \"jest\":\n\t\tcase \"mocha\":\n\t\tcase \"vitest\":\n\t\tcase \"pytest\":\n\t\t\treturn compressJsTest(output);\n\t\tcase \"go\":\n\t\t\t// Check if it's a test command\n\t\t\tif (/\\btest\\b/.test(command)) {\n\t\t\t\treturn compressGoTest(output);\n\t\t\t}\n\t\t\tbreak;\n\t}\n\n\treturn output;\n}\n\n/**\n * Strip common noise patterns from any bash output.\n * This is applied to all commands regardless of detection.\n */\nfunction stripNoisePatterns(text: string): string {\n\treturn (\n\t\ttext\n\t\t\t// Strip npm warnings (keep errors)\n\t\t\t.replace(/^npm (warn|notice) .+$/gm, \"\")\n\t\t\t// Strip yarn warnings\n\t\t\t.replace(/^warning .+$/gm, \"\")\n\t\t\t// Strip pnpm warnings\n\t\t\t.replace(/^pnpm (warn|notice) .+$/gm, \"\")\n\t\t\t// Strip common shell warnings\n\t\t\t.replace(/^bash: .+ warning: .+$/gm, \"\")\n\t\t\t// Strip \"The command completed with non-zero exit status\" noise\n\t\t\t.replace(/^The command exited with exit code .+$/gm, \"\")\n\t);\n}\n\n/**\n * Apply all compression to a bash command output.\n * This is the main entry point for bash output compression.\n *\n * @param command - The bash command that was executed\n * @param output - The raw output from the command\n * @returns Compressed output (lossless)\n */\nexport function compressBashOutput(command: string, output: string): string {\n\t// Skip compression for small outputs (overhead > savings)\n\tif (Buffer.byteLength(output, \"utf-8\") < MIN_COMPRESSION_SIZE) {\n\t\treturn output;\n\t}\n\n\tlet result = output;\n\n\t// Phase 1: Strip noise patterns\n\tresult = stripNoisePatterns(result);\n\n\t// Phase 2: Command-specific compression\n\tresult = compressCommandSpecific(command, result);\n\n\t// Phase 3: General compression\n\tresult = compressGeneral(result);\n\n\treturn result;\n}\n\n/**\n * Apply compression to grep output.\n * This strips redundant context and normalizes output.\n */\nexport function compressGrepOutput(output: string): string {\n\tif (Buffer.byteLength(output, \"utf-8\") < MIN_COMPRESSION_SIZE) {\n\t\treturn output;\n\t}\n\treturn compressGeneral(output);\n}\n\n/**\n * Apply compression to read output.\n * This normalizes whitespace but preserves content.\n */\nexport function compressReadOutput(output: string): string {\n\t// For read, we only do light compression - no line collapsing\n\tif (Buffer.byteLength(output, \"utf-8\") < MIN_COMPRESSION_SIZE) {\n\t\treturn output;\n\t}\n\treturn stripTrailingWhitespace(output);\n}\n\n/**\n * Apply compression to find output.\n * This normalizes paths and removes duplicates.\n */\nexport function compressFindOutput(output: string): string {\n\tif (Buffer.byteLength(output, \"utf-8\") < MIN_COMPRESSION_SIZE) {\n\t\treturn output;\n\t}\n\treturn compressGeneral(output);\n}\n\n/**\n * Apply compression to ls output.\n * This normalizes formatting.\n */\nexport function compressLsOutput(output: string): string {\n\tif (Buffer.byteLength(output, \"utf-8\") < MIN_COMPRESSION_SIZE) {\n\t\treturn output;\n\t}\n\treturn compressGeneral(output);\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"read.d.ts","sourceRoot":"","sources":["../../../src/core/tools/read.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iCAAiC,CAAC;AAKjE,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAO5C,OAAO,KAAK,EAAE,cAAc,EAA2B,MAAM,wBAAwB,CAAC;AAItF,OAAO,EAAoD,KAAK,gBAAgB,EAAgB,MAAM,eAAe,CAAC;AAEtH,QAAA,MAAM,UAAU;;;;EAId,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,OAAO,UAAU,CAAC,CAAC;AAEtD,MAAM,WAAW,eAAe;IAC/B,UAAU,CAAC,EAAE,gBAAgB,CAAC;CAC9B;AASD;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC9B,qCAAqC;IACrC,QAAQ,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACpD,+CAA+C;IAC/C,MAAM,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAChD,sEAAsE;IACtE,mBAAmB,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;CACnF;AAQD,MAAM,WAAW,eAAe;IAC/B,oEAAoE;IACpE,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,oEAAoE;IACpE,UAAU,CAAC,EAAE,cAAc,CAAC;CAC5B;AA+ID,wBAAgB,wBAAwB,CACvC,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,eAAe,GACvB,cAAc,CAAC,OAAO,UAAU,EAAE,eAAe,GAAG,SAAS,CAAC,CAsJhE;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,SAAS,CAAC,OAAO,UAAU,CAAC,CAEnG","sourcesContent":["import { basename, dirname, isAbsolute, relative, resolve as resolvePath, sep } from \"node:path\";\nimport type { AgentTool } from \"@kolisachint/hoocode-agent-core\";\nimport type { Api, ImageContent, Model, TextContent } from \"@kolisachint/hoocode-ai\";\nimport { Text } from \"@kolisachint/hoocode-tui\";\nimport { constants } from \"fs\";\nimport { access as fsAccess, readFile as fsReadFile } from \"fs/promises\";\nimport { type Static, Type } from \"typebox\";\nimport { getReadmePath } from \"../../config.js\";\nimport { keyHint, keyText } from \"../../modes/interactive/components/keybinding-hints.js\";\nimport { getLanguageFromPath, highlightCode, type Theme } from \"../../modes/interactive/theme/theme.js\";\nimport { formatDimensionNote, resizeImage } from \"../../utils/image-resize.js\";\nimport { detectSupportedImageMimeTypeFromFile } from \"../../utils/mime.js\";\nimport { formatPathRelativeToCwdOrAbsolute } from \"../../utils/paths.js\";\nimport type { ToolDefinition, ToolRenderResultOptions } from \"../extensions/types.js\";\nimport { resolveReadPath } from \"./path-utils.js\";\nimport { getTextOutput, invalidArgText, replaceTabs, shortenPath, str } from \"./render-utils.js\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.js\";\nimport { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateHead } from \"./truncate.js\";\n\nconst readSchema = Type.Object({\n\tpath: Type.String({ description: \"Path to the file to read (relative or absolute)\" }),\n\toffset: Type.Optional(Type.Number({ description: \"Line number to start reading from (1-indexed)\" })),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of lines to read\" })),\n});\n\nexport type ReadToolInput = Static<typeof readSchema>;\n\nexport interface ReadToolDetails {\n\ttruncation?: TruncationResult;\n}\n\ninterface CompactReadClassification {\n\tkind: \"docs\" | \"resource\" | \"skill\";\n\tlabel: string;\n}\n\nconst COMPACT_RESOURCE_FILE_NAMES = new Set([\"AGENTS.md\", \"AGENTS.MD\", \"CLAUDE.md\", \"CLAUDE.MD\"]);\n\n/**\n * Pluggable operations for the read tool.\n * Override these to delegate file reading to remote systems (for example SSH).\n */\nexport interface ReadOperations {\n\t/** Read file contents as a Buffer */\n\treadFile: (absolutePath: string) => Promise<Buffer>;\n\t/** Check if file is readable (throw if not) */\n\taccess: (absolutePath: string) => Promise<void>;\n\t/** Detect image MIME type, return null or undefined for non-images */\n\tdetectImageMimeType?: (absolutePath: string) => Promise<string | null | undefined>;\n}\n\nconst defaultReadOperations: ReadOperations = {\n\treadFile: (path) => fsReadFile(path),\n\taccess: (path) => fsAccess(path, constants.R_OK),\n\tdetectImageMimeType: detectSupportedImageMimeTypeFromFile,\n};\n\nexport interface ReadToolOptions {\n\t/** Whether to auto-resize images to 2000x2000 max. Default: true */\n\tautoResizeImages?: boolean;\n\t/** Custom operations for file reading. Default: local filesystem */\n\toperations?: ReadOperations;\n}\n\ntype ReadRenderArgs = { path?: string; file_path?: string; offset?: number; limit?: number };\n\nfunction formatReadLineRange(args: ReadRenderArgs | undefined, theme: Theme): string {\n\tif (args?.offset === undefined && args?.limit === undefined) return \"\";\n\tconst startLine = args.offset ?? 1;\n\tconst endLine = args.limit !== undefined ? startLine + args.limit - 1 : \"\";\n\treturn theme.fg(\"warning\", `:${startLine}${endLine ? `-${endLine}` : \"\"}`);\n}\n\nfunction formatReadCall(args: ReadRenderArgs | undefined, theme: Theme): string {\n\tconst rawPath = str(args?.file_path ?? args?.path);\n\tconst path = rawPath !== null ? shortenPath(rawPath) : null;\n\tconst invalidArg = invalidArgText(theme);\n\tconst pathDisplay = path === null ? invalidArg : path ? theme.fg(\"accent\", path) : theme.fg(\"toolOutput\", \"...\");\n\treturn `${theme.fg(\"toolTitle\", theme.bold(\"read\"))} ${pathDisplay}${formatReadLineRange(args, theme)}`;\n}\n\nfunction trimTrailingEmptyLines(lines: string[]): string[] {\n\tlet end = lines.length;\n\twhile (end > 0 && lines[end - 1] === \"\") {\n\t\tend--;\n\t}\n\treturn lines.slice(0, end);\n}\n\nfunction getNonVisionImageNote(model: Model<Api> | undefined): string | undefined {\n\tif (!model || model.input.includes(\"image\")) {\n\t\treturn undefined;\n\t}\n\treturn \"[Current model does not support images. The image will be omitted from this request.]\";\n}\n\nfunction toPosixPath(filePath: string): string {\n\treturn filePath.split(sep).join(\"/\");\n}\n\nfunction getHooCodeDocsClassification(absolutePath: string): CompactReadClassification | undefined {\n\tconst packageRoot = dirname(getReadmePath());\n\tconst relativePath = relative(resolvePath(packageRoot), resolvePath(absolutePath));\n\tif (\n\t\trelativePath === \"\" ||\n\t\trelativePath === \"..\" ||\n\t\trelativePath.startsWith(`..${sep}`) ||\n\t\tisAbsolute(relativePath)\n\t) {\n\t\treturn undefined;\n\t}\n\n\tconst label = toPosixPath(relativePath);\n\tif (label === \"README.md\" || label.startsWith(\"docs/\") || label.startsWith(\"examples/\")) {\n\t\treturn { kind: \"docs\", label };\n\t}\n\treturn undefined;\n}\n\nfunction getCompactReadClassification(\n\targs: ReadRenderArgs | undefined,\n\tcwd: string,\n): CompactReadClassification | undefined {\n\tconst rawPath = str(args?.file_path ?? args?.path);\n\tif (!rawPath) return undefined;\n\n\tconst absolutePath = resolveReadPath(rawPath, cwd);\n\tconst fileName = basename(absolutePath);\n\tif (fileName === \"SKILL.md\") {\n\t\treturn { kind: \"skill\", label: basename(dirname(absolutePath)) || fileName };\n\t}\n\n\tconst docsClassification = getHooCodeDocsClassification(absolutePath);\n\tif (docsClassification) return docsClassification;\n\n\tif (COMPACT_RESOURCE_FILE_NAMES.has(fileName)) {\n\t\treturn { kind: \"resource\", label: formatPathRelativeToCwdOrAbsolute(absolutePath, cwd) };\n\t}\n\n\treturn undefined;\n}\n\nfunction formatCompactReadCall(\n\tclassification: CompactReadClassification,\n\targs: ReadRenderArgs | undefined,\n\ttheme: Theme,\n): string {\n\tconst expandHint = theme.fg(\"dim\", ` (${keyText(\"app.tools.expand\")} to expand)`);\n\tif (classification.kind === \"skill\") {\n\t\treturn (\n\t\t\ttheme.fg(\"customMessageLabel\", `\\x1b[1m[skill]\\x1b[22m `) +\n\t\t\ttheme.fg(\"customMessageText\", classification.label) +\n\t\t\tformatReadLineRange(args, theme) +\n\t\t\texpandHint\n\t\t);\n\t}\n\n\treturn (\n\t\ttheme.fg(\"toolTitle\", theme.bold(`read ${classification.kind}`)) +\n\t\t\" \" +\n\t\ttheme.fg(\"accent\", classification.label) +\n\t\tformatReadLineRange(args, theme) +\n\t\texpandHint\n\t);\n}\n\nfunction formatReadResult(\n\targs: ReadRenderArgs | undefined,\n\tresult: { content: (TextContent | ImageContent)[]; details?: ReadToolDetails },\n\toptions: ToolRenderResultOptions,\n\ttheme: Theme,\n\tshowImages: boolean,\n\tcwd: string,\n\tisError: boolean,\n): string {\n\tif (!options.expanded && !isError && getCompactReadClassification(args, cwd)) {\n\t\treturn \"\";\n\t}\n\n\tconst rawPath = str(args?.file_path ?? args?.path);\n\tconst output = getTextOutput(result, showImages);\n\tconst lang = rawPath ? getLanguageFromPath(rawPath) : undefined;\n\tconst renderedLines = lang ? highlightCode(replaceTabs(output), lang) : output.split(\"\\n\");\n\tconst lines = trimTrailingEmptyLines(renderedLines);\n\tconst maxLines = options.expanded ? lines.length : 10;\n\tconst displayLines = lines.slice(0, maxLines);\n\tconst remaining = lines.length - maxLines;\n\tlet text = `\\n${displayLines.map((line) => (lang ? replaceTabs(line) : theme.fg(\"toolOutput\", replaceTabs(line)))).join(\"\\n\")}`;\n\tif (remaining > 0) {\n\t\ttext += `${theme.fg(\"muted\", `\\n... (${remaining} more lines,`)} ${keyHint(\"app.tools.expand\", \"to expand\")})`;\n\t}\n\n\tconst truncation = result.details?.truncation;\n\tif (truncation?.truncated) {\n\t\tif (truncation.firstLineExceedsLimit) {\n\t\t\ttext += `\\n${theme.fg(\"warning\", `[First line exceeds ${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit]`)}`;\n\t\t} else if (truncation.truncatedBy === \"lines\") {\n\t\t\ttext += `\\n${theme.fg(\"warning\", `[Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines (${truncation.maxLines ?? DEFAULT_MAX_LINES} line limit)]`)}`;\n\t\t} else {\n\t\t\ttext += `\\n${theme.fg(\"warning\", `[Truncated: ${truncation.outputLines} lines shown (${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit)]`)}`;\n\t\t}\n\t}\n\treturn text;\n}\n\nexport function createReadToolDefinition(\n\tcwd: string,\n\toptions?: ReadToolOptions,\n): ToolDefinition<typeof readSchema, ReadToolDetails | undefined> {\n\tconst autoResizeImages = options?.autoResizeImages ?? true;\n\tconst ops = options?.operations ?? defaultReadOperations;\n\treturn {\n\t\tname: \"read\",\n\t\tlabel: \"read\",\n\t\tdescription: `Read the contents of a file. Supports text files and images (jpg, png, gif, webp). Images are sent as attachments. For text files, output is truncated to ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Use offset/limit for large files. When you need the full file, continue with offset until complete.`,\n\t\tpromptSnippet: \"Read file contents\",\n\t\tpromptGuidelines: [\"Use read to examine files instead of cat or sed.\"],\n\t\tparameters: readSchema,\n\t\tasync execute(\n\t\t\t_toolCallId,\n\t\t\t{ path, offset, limit }: { path: string; offset?: number; limit?: number },\n\t\t\tsignal?: AbortSignal,\n\t\t\t_onUpdate?,\n\t\t\tctx?,\n\t\t) {\n\t\t\tconst absolutePath = resolveReadPath(path, cwd);\n\t\t\treturn new Promise<{ content: (TextContent | ImageContent)[]; details: ReadToolDetails | undefined }>(\n\t\t\t\t(resolve, reject) => {\n\t\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tlet aborted = false;\n\t\t\t\t\tconst onAbort = () => {\n\t\t\t\t\t\taborted = true;\n\t\t\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\t\t};\n\t\t\t\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\n\t\t\t\t\t(async () => {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t// Check if file exists and is readable.\n\t\t\t\t\t\t\tawait ops.access(absolutePath);\n\t\t\t\t\t\t\tif (aborted) return;\n\t\t\t\t\t\t\tconst mimeType = ops.detectImageMimeType ? await ops.detectImageMimeType(absolutePath) : undefined;\n\t\t\t\t\t\t\tlet content: (TextContent | ImageContent)[];\n\t\t\t\t\t\t\tlet details: ReadToolDetails | undefined;\n\t\t\t\t\t\t\tconst nonVisionImageNote = getNonVisionImageNote(ctx?.model);\n\t\t\t\t\t\t\tif (mimeType) {\n\t\t\t\t\t\t\t\t// Read image as binary.\n\t\t\t\t\t\t\t\tconst buffer = await ops.readFile(absolutePath);\n\t\t\t\t\t\t\t\tconst base64 = buffer.toString(\"base64\");\n\t\t\t\t\t\t\t\tif (autoResizeImages) {\n\t\t\t\t\t\t\t\t\t// Resize image if needed before sending it back to the model.\n\t\t\t\t\t\t\t\t\tconst resized = await resizeImage({ type: \"image\", data: base64, mimeType });\n\t\t\t\t\t\t\t\t\tif (!resized) {\n\t\t\t\t\t\t\t\t\t\tlet textNote = `Read image file [${mimeType}]\\n[Image omitted: could not be resized below the inline image size limit.]`;\n\t\t\t\t\t\t\t\t\t\tif (nonVisionImageNote) textNote += `\\n${nonVisionImageNote}`;\n\t\t\t\t\t\t\t\t\t\tcontent = [{ type: \"text\", text: textNote }];\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\tconst dimensionNote = formatDimensionNote(resized);\n\t\t\t\t\t\t\t\t\t\tlet textNote = `Read image file [${resized.mimeType}]`;\n\t\t\t\t\t\t\t\t\t\tif (dimensionNote) textNote += `\\n${dimensionNote}`;\n\t\t\t\t\t\t\t\t\t\tif (nonVisionImageNote) textNote += `\\n${nonVisionImageNote}`;\n\t\t\t\t\t\t\t\t\t\tcontent = [\n\t\t\t\t\t\t\t\t\t\t\t{ type: \"text\", text: textNote },\n\t\t\t\t\t\t\t\t\t\t\t{ type: \"image\", data: resized.data, mimeType: resized.mimeType },\n\t\t\t\t\t\t\t\t\t\t];\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tlet textNote = `Read image file [${mimeType}]`;\n\t\t\t\t\t\t\t\t\tif (nonVisionImageNote) textNote += `\\n${nonVisionImageNote}`;\n\t\t\t\t\t\t\t\t\tcontent = [\n\t\t\t\t\t\t\t\t\t\t{ type: \"text\", text: textNote },\n\t\t\t\t\t\t\t\t\t\t{ type: \"image\", data: base64, mimeType },\n\t\t\t\t\t\t\t\t\t];\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t// Read text content.\n\t\t\t\t\t\t\t\tconst buffer = await ops.readFile(absolutePath);\n\t\t\t\t\t\t\t\tconst textContent = buffer.toString(\"utf-8\");\n\t\t\t\t\t\t\t\tconst allLines = textContent.split(\"\\n\");\n\t\t\t\t\t\t\t\tconst totalFileLines = allLines.length;\n\t\t\t\t\t\t\t\t// Apply offset if specified. Convert from 1-indexed input to 0-indexed array access.\n\t\t\t\t\t\t\t\tconst startLine = offset ? Math.max(0, offset - 1) : 0;\n\t\t\t\t\t\t\t\tconst startLineDisplay = startLine + 1;\n\t\t\t\t\t\t\t\t// Check if offset is out of bounds.\n\t\t\t\t\t\t\t\tif (startLine >= allLines.length) {\n\t\t\t\t\t\t\t\t\tthrow new Error(`Offset ${offset} is beyond end of file (${allLines.length} lines total)`);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tlet selectedContent: string;\n\t\t\t\t\t\t\t\tlet userLimitedLines: number | undefined;\n\t\t\t\t\t\t\t\t// If limit is specified by the user, honor it first. Otherwise truncateHead decides.\n\t\t\t\t\t\t\t\tif (limit !== undefined) {\n\t\t\t\t\t\t\t\t\tconst endLine = Math.min(startLine + limit, allLines.length);\n\t\t\t\t\t\t\t\t\tselectedContent = allLines.slice(startLine, endLine).join(\"\\n\");\n\t\t\t\t\t\t\t\t\tuserLimitedLines = endLine - startLine;\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tselectedContent = allLines.slice(startLine).join(\"\\n\");\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t// Apply truncation, respecting both line and byte limits.\n\t\t\t\t\t\t\t\tconst truncation = truncateHead(selectedContent);\n\t\t\t\t\t\t\t\tlet outputText: string;\n\t\t\t\t\t\t\t\tif (truncation.firstLineExceedsLimit) {\n\t\t\t\t\t\t\t\t\t// First line alone exceeds the byte limit. Point the model at a bash fallback.\n\t\t\t\t\t\t\t\t\tconst firstLineSize = formatSize(Buffer.byteLength(allLines[startLine], \"utf-8\"));\n\t\t\t\t\t\t\t\t\toutputText = `[Line ${startLineDisplay} is ${firstLineSize}, exceeds ${formatSize(DEFAULT_MAX_BYTES)} limit. Use bash: sed -n '${startLineDisplay}p' ${path} | head -c ${DEFAULT_MAX_BYTES}]`;\n\t\t\t\t\t\t\t\t\tdetails = { truncation };\n\t\t\t\t\t\t\t\t} else if (truncation.truncated) {\n\t\t\t\t\t\t\t\t\t// Truncation occurred. Build an actionable continuation notice.\n\t\t\t\t\t\t\t\t\tconst endLineDisplay = startLineDisplay + truncation.outputLines - 1;\n\t\t\t\t\t\t\t\t\tconst nextOffset = endLineDisplay + 1;\n\t\t\t\t\t\t\t\t\toutputText = truncation.content;\n\t\t\t\t\t\t\t\t\tif (truncation.truncatedBy === \"lines\") {\n\t\t\t\t\t\t\t\t\t\toutputText += `\\n\\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}. Use offset=${nextOffset} to continue.]`;\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\toutputText += `\\n\\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Use offset=${nextOffset} to continue.]`;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tdetails = { truncation };\n\t\t\t\t\t\t\t\t} else if (userLimitedLines !== undefined && startLine + userLimitedLines < allLines.length) {\n\t\t\t\t\t\t\t\t\t// User-specified limit stopped early, but the file still has more content.\n\t\t\t\t\t\t\t\t\tconst remaining = allLines.length - (startLine + userLimitedLines);\n\t\t\t\t\t\t\t\t\tconst nextOffset = startLine + userLimitedLines + 1;\n\t\t\t\t\t\t\t\t\toutputText = `${truncation.content}\\n\\n[${remaining} more lines in file. Use offset=${nextOffset} to continue.]`;\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t// No truncation and no remaining user-limited content.\n\t\t\t\t\t\t\t\t\toutputText = truncation.content;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tcontent = [{ type: \"text\", text: outputText }];\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (aborted) return;\n\t\t\t\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t\t\tresolve({ content, details });\n\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t\t\tif (!aborted) reject(error);\n\t\t\t\t\t\t}\n\t\t\t\t\t})();\n\t\t\t\t},\n\t\t\t);\n\t\t},\n\t\trenderCall(args, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\tconst classification = !context.expanded ? getCompactReadClassification(args, context.cwd) : undefined;\n\t\t\ttext.setText(\n\t\t\t\tclassification ? formatCompactReadCall(classification, args, theme) : formatReadCall(args, theme),\n\t\t\t);\n\t\t\treturn text;\n\t\t},\n\t\trenderResult(result, options, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(\n\t\t\t\tformatReadResult(context.args, result, options, theme, context.showImages, context.cwd, context.isError),\n\t\t\t);\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createReadTool(cwd: string, options?: ReadToolOptions): AgentTool<typeof readSchema> {\n\treturn wrapToolDefinition(createReadToolDefinition(cwd, options));\n}\n"]}
|
|
1
|
+
{"version":3,"file":"read.d.ts","sourceRoot":"","sources":["../../../src/core/tools/read.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iCAAiC,CAAC;AAKjE,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAO5C,OAAO,KAAK,EAAE,cAAc,EAA2B,MAAM,wBAAwB,CAAC;AAKtF,OAAO,EAAoD,KAAK,gBAAgB,EAAgB,MAAM,eAAe,CAAC;AAEtH,QAAA,MAAM,UAAU;;;;EAId,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,OAAO,UAAU,CAAC,CAAC;AAEtD,MAAM,WAAW,eAAe;IAC/B,UAAU,CAAC,EAAE,gBAAgB,CAAC;CAC9B;AASD;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC9B,qCAAqC;IACrC,QAAQ,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACpD,+CAA+C;IAC/C,MAAM,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAChD,sEAAsE;IACtE,mBAAmB,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;CACnF;AAQD,MAAM,WAAW,eAAe;IAC/B,oEAAoE;IACpE,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,oEAAoE;IACpE,UAAU,CAAC,EAAE,cAAc,CAAC;CAC5B;AA+ID,wBAAgB,wBAAwB,CACvC,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,eAAe,GACvB,cAAc,CAAC,OAAO,UAAU,EAAE,eAAe,GAAG,SAAS,CAAC,CA4JhE;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,SAAS,CAAC,OAAO,UAAU,CAAC,CAEnG","sourcesContent":["import { basename, dirname, isAbsolute, relative, resolve as resolvePath, sep } from \"node:path\";\nimport type { AgentTool } from \"@kolisachint/hoocode-agent-core\";\nimport type { Api, ImageContent, Model, TextContent } from \"@kolisachint/hoocode-ai\";\nimport { Text } from \"@kolisachint/hoocode-tui\";\nimport { constants } from \"fs\";\nimport { access as fsAccess, readFile as fsReadFile } from \"fs/promises\";\nimport { type Static, Type } from \"typebox\";\nimport { getReadmePath } from \"../../config.js\";\nimport { keyHint, keyText } from \"../../modes/interactive/components/keybinding-hints.js\";\nimport { getLanguageFromPath, highlightCode, type Theme } from \"../../modes/interactive/theme/theme.js\";\nimport { formatDimensionNote, resizeImage } from \"../../utils/image-resize.js\";\nimport { detectSupportedImageMimeTypeFromFile } from \"../../utils/mime.js\";\nimport { formatPathRelativeToCwdOrAbsolute } from \"../../utils/paths.js\";\nimport type { ToolDefinition, ToolRenderResultOptions } from \"../extensions/types.js\";\nimport { compressReadOutput } from \"./output-compression.js\";\nimport { resolveReadPath } from \"./path-utils.js\";\nimport { getTextOutput, invalidArgText, replaceTabs, shortenPath, str } from \"./render-utils.js\";\nimport { wrapToolDefinition } from \"./tool-definition-wrapper.js\";\nimport { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateHead } from \"./truncate.js\";\n\nconst readSchema = Type.Object({\n\tpath: Type.String({ description: \"Path to the file to read (relative or absolute)\" }),\n\toffset: Type.Optional(Type.Number({ description: \"Line number to start reading from (1-indexed)\" })),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of lines to read\" })),\n});\n\nexport type ReadToolInput = Static<typeof readSchema>;\n\nexport interface ReadToolDetails {\n\ttruncation?: TruncationResult;\n}\n\ninterface CompactReadClassification {\n\tkind: \"docs\" | \"resource\" | \"skill\";\n\tlabel: string;\n}\n\nconst COMPACT_RESOURCE_FILE_NAMES = new Set([\"AGENTS.md\", \"AGENTS.MD\", \"CLAUDE.md\", \"CLAUDE.MD\"]);\n\n/**\n * Pluggable operations for the read tool.\n * Override these to delegate file reading to remote systems (for example SSH).\n */\nexport interface ReadOperations {\n\t/** Read file contents as a Buffer */\n\treadFile: (absolutePath: string) => Promise<Buffer>;\n\t/** Check if file is readable (throw if not) */\n\taccess: (absolutePath: string) => Promise<void>;\n\t/** Detect image MIME type, return null or undefined for non-images */\n\tdetectImageMimeType?: (absolutePath: string) => Promise<string | null | undefined>;\n}\n\nconst defaultReadOperations: ReadOperations = {\n\treadFile: (path) => fsReadFile(path),\n\taccess: (path) => fsAccess(path, constants.R_OK),\n\tdetectImageMimeType: detectSupportedImageMimeTypeFromFile,\n};\n\nexport interface ReadToolOptions {\n\t/** Whether to auto-resize images to 2000x2000 max. Default: true */\n\tautoResizeImages?: boolean;\n\t/** Custom operations for file reading. Default: local filesystem */\n\toperations?: ReadOperations;\n}\n\ntype ReadRenderArgs = { path?: string; file_path?: string; offset?: number; limit?: number };\n\nfunction formatReadLineRange(args: ReadRenderArgs | undefined, theme: Theme): string {\n\tif (args?.offset === undefined && args?.limit === undefined) return \"\";\n\tconst startLine = args.offset ?? 1;\n\tconst endLine = args.limit !== undefined ? startLine + args.limit - 1 : \"\";\n\treturn theme.fg(\"warning\", `:${startLine}${endLine ? `-${endLine}` : \"\"}`);\n}\n\nfunction formatReadCall(args: ReadRenderArgs | undefined, theme: Theme): string {\n\tconst rawPath = str(args?.file_path ?? args?.path);\n\tconst path = rawPath !== null ? shortenPath(rawPath) : null;\n\tconst invalidArg = invalidArgText(theme);\n\tconst pathDisplay = path === null ? invalidArg : path ? theme.fg(\"accent\", path) : theme.fg(\"toolOutput\", \"...\");\n\treturn `${theme.fg(\"toolTitle\", theme.bold(\"read\"))} ${pathDisplay}${formatReadLineRange(args, theme)}`;\n}\n\nfunction trimTrailingEmptyLines(lines: string[]): string[] {\n\tlet end = lines.length;\n\twhile (end > 0 && lines[end - 1] === \"\") {\n\t\tend--;\n\t}\n\treturn lines.slice(0, end);\n}\n\nfunction getNonVisionImageNote(model: Model<Api> | undefined): string | undefined {\n\tif (!model || model.input.includes(\"image\")) {\n\t\treturn undefined;\n\t}\n\treturn \"[Current model does not support images. The image will be omitted from this request.]\";\n}\n\nfunction toPosixPath(filePath: string): string {\n\treturn filePath.split(sep).join(\"/\");\n}\n\nfunction getHooCodeDocsClassification(absolutePath: string): CompactReadClassification | undefined {\n\tconst packageRoot = dirname(getReadmePath());\n\tconst relativePath = relative(resolvePath(packageRoot), resolvePath(absolutePath));\n\tif (\n\t\trelativePath === \"\" ||\n\t\trelativePath === \"..\" ||\n\t\trelativePath.startsWith(`..${sep}`) ||\n\t\tisAbsolute(relativePath)\n\t) {\n\t\treturn undefined;\n\t}\n\n\tconst label = toPosixPath(relativePath);\n\tif (label === \"README.md\" || label.startsWith(\"docs/\") || label.startsWith(\"examples/\")) {\n\t\treturn { kind: \"docs\", label };\n\t}\n\treturn undefined;\n}\n\nfunction getCompactReadClassification(\n\targs: ReadRenderArgs | undefined,\n\tcwd: string,\n): CompactReadClassification | undefined {\n\tconst rawPath = str(args?.file_path ?? args?.path);\n\tif (!rawPath) return undefined;\n\n\tconst absolutePath = resolveReadPath(rawPath, cwd);\n\tconst fileName = basename(absolutePath);\n\tif (fileName === \"SKILL.md\") {\n\t\treturn { kind: \"skill\", label: basename(dirname(absolutePath)) || fileName };\n\t}\n\n\tconst docsClassification = getHooCodeDocsClassification(absolutePath);\n\tif (docsClassification) return docsClassification;\n\n\tif (COMPACT_RESOURCE_FILE_NAMES.has(fileName)) {\n\t\treturn { kind: \"resource\", label: formatPathRelativeToCwdOrAbsolute(absolutePath, cwd) };\n\t}\n\n\treturn undefined;\n}\n\nfunction formatCompactReadCall(\n\tclassification: CompactReadClassification,\n\targs: ReadRenderArgs | undefined,\n\ttheme: Theme,\n): string {\n\tconst expandHint = theme.fg(\"dim\", ` (${keyText(\"app.tools.expand\")} to expand)`);\n\tif (classification.kind === \"skill\") {\n\t\treturn (\n\t\t\ttheme.fg(\"customMessageLabel\", `\\x1b[1m[skill]\\x1b[22m `) +\n\t\t\ttheme.fg(\"customMessageText\", classification.label) +\n\t\t\tformatReadLineRange(args, theme) +\n\t\t\texpandHint\n\t\t);\n\t}\n\n\treturn (\n\t\ttheme.fg(\"toolTitle\", theme.bold(`read ${classification.kind}`)) +\n\t\t\" \" +\n\t\ttheme.fg(\"accent\", classification.label) +\n\t\tformatReadLineRange(args, theme) +\n\t\texpandHint\n\t);\n}\n\nfunction formatReadResult(\n\targs: ReadRenderArgs | undefined,\n\tresult: { content: (TextContent | ImageContent)[]; details?: ReadToolDetails },\n\toptions: ToolRenderResultOptions,\n\ttheme: Theme,\n\tshowImages: boolean,\n\tcwd: string,\n\tisError: boolean,\n): string {\n\tif (!options.expanded && !isError && getCompactReadClassification(args, cwd)) {\n\t\treturn \"\";\n\t}\n\n\tconst rawPath = str(args?.file_path ?? args?.path);\n\tconst output = getTextOutput(result, showImages);\n\tconst lang = rawPath ? getLanguageFromPath(rawPath) : undefined;\n\tconst renderedLines = lang ? highlightCode(replaceTabs(output), lang) : output.split(\"\\n\");\n\tconst lines = trimTrailingEmptyLines(renderedLines);\n\tconst maxLines = options.expanded ? lines.length : 10;\n\tconst displayLines = lines.slice(0, maxLines);\n\tconst remaining = lines.length - maxLines;\n\tlet text = `\\n${displayLines.map((line) => (lang ? replaceTabs(line) : theme.fg(\"toolOutput\", replaceTabs(line)))).join(\"\\n\")}`;\n\tif (remaining > 0) {\n\t\ttext += `${theme.fg(\"muted\", `\\n... (${remaining} more lines,`)} ${keyHint(\"app.tools.expand\", \"to expand\")})`;\n\t}\n\n\tconst truncation = result.details?.truncation;\n\tif (truncation?.truncated) {\n\t\tif (truncation.firstLineExceedsLimit) {\n\t\t\ttext += `\\n${theme.fg(\"warning\", `[First line exceeds ${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit]`)}`;\n\t\t} else if (truncation.truncatedBy === \"lines\") {\n\t\t\ttext += `\\n${theme.fg(\"warning\", `[Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines (${truncation.maxLines ?? DEFAULT_MAX_LINES} line limit)]`)}`;\n\t\t} else {\n\t\t\ttext += `\\n${theme.fg(\"warning\", `[Truncated: ${truncation.outputLines} lines shown (${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit)]`)}`;\n\t\t}\n\t}\n\treturn text;\n}\n\nexport function createReadToolDefinition(\n\tcwd: string,\n\toptions?: ReadToolOptions,\n): ToolDefinition<typeof readSchema, ReadToolDetails | undefined> {\n\tconst autoResizeImages = options?.autoResizeImages ?? true;\n\tconst ops = options?.operations ?? defaultReadOperations;\n\treturn {\n\t\tname: \"read\",\n\t\tlabel: \"read\",\n\t\tdescription: `Read the contents of a file. Supports text files and images (jpg, png, gif, webp). Images are sent as attachments. For text files, output is truncated to ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Use offset/limit for large files. When you need the full file, continue with offset until complete.`,\n\t\tpromptSnippet: \"Read file contents\",\n\t\tpromptGuidelines: [\"Use read to examine files instead of cat or sed.\"],\n\t\tparameters: readSchema,\n\t\tasync execute(\n\t\t\t_toolCallId,\n\t\t\t{ path, offset, limit }: { path: string; offset?: number; limit?: number },\n\t\t\tsignal?: AbortSignal,\n\t\t\t_onUpdate?,\n\t\t\tctx?,\n\t\t) {\n\t\t\tconst absolutePath = resolveReadPath(path, cwd);\n\t\t\treturn new Promise<{ content: (TextContent | ImageContent)[]; details: ReadToolDetails | undefined }>(\n\t\t\t\t(resolve, reject) => {\n\t\t\t\t\tif (signal?.aborted) {\n\t\t\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\t\t\t\t\tlet aborted = false;\n\t\t\t\t\tconst onAbort = () => {\n\t\t\t\t\t\taborted = true;\n\t\t\t\t\t\treject(new Error(\"Operation aborted\"));\n\t\t\t\t\t};\n\t\t\t\t\tsignal?.addEventListener(\"abort\", onAbort, { once: true });\n\n\t\t\t\t\t(async () => {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t// Check if file exists and is readable.\n\t\t\t\t\t\t\tawait ops.access(absolutePath);\n\t\t\t\t\t\t\tif (aborted) return;\n\t\t\t\t\t\t\tconst mimeType = ops.detectImageMimeType ? await ops.detectImageMimeType(absolutePath) : undefined;\n\t\t\t\t\t\t\tlet content: (TextContent | ImageContent)[];\n\t\t\t\t\t\t\tlet details: ReadToolDetails | undefined;\n\t\t\t\t\t\t\tconst nonVisionImageNote = getNonVisionImageNote(ctx?.model);\n\t\t\t\t\t\t\tif (mimeType) {\n\t\t\t\t\t\t\t\t// Read image as binary.\n\t\t\t\t\t\t\t\tconst buffer = await ops.readFile(absolutePath);\n\t\t\t\t\t\t\t\tconst base64 = buffer.toString(\"base64\");\n\t\t\t\t\t\t\t\tif (autoResizeImages) {\n\t\t\t\t\t\t\t\t\t// Resize image if needed before sending it back to the model.\n\t\t\t\t\t\t\t\t\tconst resized = await resizeImage({ type: \"image\", data: base64, mimeType });\n\t\t\t\t\t\t\t\t\tif (!resized) {\n\t\t\t\t\t\t\t\t\t\tlet textNote = `Read image file [${mimeType}]\\n[Image omitted: could not be resized below the inline image size limit.]`;\n\t\t\t\t\t\t\t\t\t\tif (nonVisionImageNote) textNote += `\\n${nonVisionImageNote}`;\n\t\t\t\t\t\t\t\t\t\tcontent = [{ type: \"text\", text: textNote }];\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\tconst dimensionNote = formatDimensionNote(resized);\n\t\t\t\t\t\t\t\t\t\tlet textNote = `Read image file [${resized.mimeType}]`;\n\t\t\t\t\t\t\t\t\t\tif (dimensionNote) textNote += `\\n${dimensionNote}`;\n\t\t\t\t\t\t\t\t\t\tif (nonVisionImageNote) textNote += `\\n${nonVisionImageNote}`;\n\t\t\t\t\t\t\t\t\t\tcontent = [\n\t\t\t\t\t\t\t\t\t\t\t{ type: \"text\", text: textNote },\n\t\t\t\t\t\t\t\t\t\t\t{ type: \"image\", data: resized.data, mimeType: resized.mimeType },\n\t\t\t\t\t\t\t\t\t\t];\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tlet textNote = `Read image file [${mimeType}]`;\n\t\t\t\t\t\t\t\t\tif (nonVisionImageNote) textNote += `\\n${nonVisionImageNote}`;\n\t\t\t\t\t\t\t\t\tcontent = [\n\t\t\t\t\t\t\t\t\t\t{ type: \"text\", text: textNote },\n\t\t\t\t\t\t\t\t\t\t{ type: \"image\", data: base64, mimeType },\n\t\t\t\t\t\t\t\t\t];\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t// Read text content.\n\t\t\t\t\t\t\t\tconst buffer = await ops.readFile(absolutePath);\n\t\t\t\t\t\t\t\tconst textContent = buffer.toString(\"utf-8\");\n\t\t\t\t\t\t\t\tconst allLines = textContent.split(\"\\n\");\n\t\t\t\t\t\t\t\tconst totalFileLines = allLines.length;\n\t\t\t\t\t\t\t\t// Apply offset if specified. Convert from 1-indexed input to 0-indexed array access.\n\t\t\t\t\t\t\t\tconst startLine = offset ? Math.max(0, offset - 1) : 0;\n\t\t\t\t\t\t\t\tconst startLineDisplay = startLine + 1;\n\t\t\t\t\t\t\t\t// Check if offset is out of bounds.\n\t\t\t\t\t\t\t\tif (startLine >= allLines.length) {\n\t\t\t\t\t\t\t\t\tthrow new Error(`Offset ${offset} is beyond end of file (${allLines.length} lines total)`);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tlet selectedContent: string;\n\t\t\t\t\t\t\t\tlet userLimitedLines: number | undefined;\n\t\t\t\t\t\t\t\t// If limit is specified by the user, honor it first. Otherwise truncateHead decides.\n\t\t\t\t\t\t\t\tif (limit !== undefined) {\n\t\t\t\t\t\t\t\t\tconst endLine = Math.min(startLine + limit, allLines.length);\n\t\t\t\t\t\t\t\t\tselectedContent = allLines.slice(startLine, endLine).join(\"\\n\");\n\t\t\t\t\t\t\t\t\tuserLimitedLines = endLine - startLine;\n\t\t\t\t\t\t\t\t} else if (startLine === 0) {\n\t\t\t\t\t\t\t\t\t// Whole-file read (common case): reuse the original string instead of\n\t\t\t\t\t\t\t\t\t// rebuilding it from the split lines.\n\t\t\t\t\t\t\t\t\tselectedContent = textContent;\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\tselectedContent = allLines.slice(startLine).join(\"\\n\");\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t// Apply truncation, respecting both line and byte limits.\n\t\t\t\t\t\t\t\t// Apply lossless compression then truncation.\n\t\t\t\t\t\t\t\tconst compressedContent = compressReadOutput(selectedContent);\n\t\t\t\t\t\t\t\tconst truncation = truncateHead(compressedContent);\n\t\t\t\t\t\t\t\tlet outputText: string;\n\t\t\t\t\t\t\t\tif (truncation.firstLineExceedsLimit) {\n\t\t\t\t\t\t\t\t\t// First line alone exceeds the byte limit. Point the model at a bash fallback.\n\t\t\t\t\t\t\t\t\tconst firstLineSize = formatSize(Buffer.byteLength(allLines[startLine], \"utf-8\"));\n\t\t\t\t\t\t\t\t\toutputText = `[Line ${startLineDisplay} is ${firstLineSize}, exceeds ${formatSize(DEFAULT_MAX_BYTES)} limit. Use bash: sed -n '${startLineDisplay}p' ${path} | head -c ${DEFAULT_MAX_BYTES}]`;\n\t\t\t\t\t\t\t\t\tdetails = { truncation };\n\t\t\t\t\t\t\t\t} else if (truncation.truncated) {\n\t\t\t\t\t\t\t\t\t// Truncation occurred. Build an actionable continuation notice.\n\t\t\t\t\t\t\t\t\tconst endLineDisplay = startLineDisplay + truncation.outputLines - 1;\n\t\t\t\t\t\t\t\t\tconst nextOffset = endLineDisplay + 1;\n\t\t\t\t\t\t\t\t\toutputText = truncation.content;\n\t\t\t\t\t\t\t\t\tif (truncation.truncatedBy === \"lines\") {\n\t\t\t\t\t\t\t\t\t\toutputText += `\\n\\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}. Use offset=${nextOffset} to continue.]`;\n\t\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t\toutputText += `\\n\\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Use offset=${nextOffset} to continue.]`;\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t\tdetails = { truncation };\n\t\t\t\t\t\t\t\t} else if (userLimitedLines !== undefined && startLine + userLimitedLines < allLines.length) {\n\t\t\t\t\t\t\t\t\t// User-specified limit stopped early, but the file still has more content.\n\t\t\t\t\t\t\t\t\tconst remaining = allLines.length - (startLine + userLimitedLines);\n\t\t\t\t\t\t\t\t\tconst nextOffset = startLine + userLimitedLines + 1;\n\t\t\t\t\t\t\t\t\toutputText = `${truncation.content}\\n\\n[${remaining} more lines in file. Use offset=${nextOffset} to continue.]`;\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t// No truncation and no remaining user-limited content.\n\t\t\t\t\t\t\t\t\toutputText = truncation.content;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tcontent = [{ type: \"text\", text: outputText }];\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (aborted) return;\n\t\t\t\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t\t\tresolve({ content, details });\n\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\tsignal?.removeEventListener(\"abort\", onAbort);\n\t\t\t\t\t\t\tif (!aborted) reject(error);\n\t\t\t\t\t\t}\n\t\t\t\t\t})();\n\t\t\t\t},\n\t\t\t);\n\t\t},\n\t\trenderCall(args, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\tconst classification = !context.expanded ? getCompactReadClassification(args, context.cwd) : undefined;\n\t\t\ttext.setText(\n\t\t\t\tclassification ? formatCompactReadCall(classification, args, theme) : formatReadCall(args, theme),\n\t\t\t);\n\t\t\treturn text;\n\t\t},\n\t\trenderResult(result, options, theme, context) {\n\t\t\tconst text = (context.lastComponent as Text | undefined) ?? new Text(\"\", 0, 0);\n\t\t\ttext.setText(\n\t\t\t\tformatReadResult(context.args, result, options, theme, context.showImages, context.cwd, context.isError),\n\t\t\t);\n\t\t\treturn text;\n\t\t},\n\t};\n}\n\nexport function createReadTool(cwd: string, options?: ReadToolOptions): AgentTool<typeof readSchema> {\n\treturn wrapToolDefinition(createReadToolDefinition(cwd, options));\n}\n"]}
|
package/dist/core/tools/read.js
CHANGED
|
@@ -9,6 +9,7 @@ import { getLanguageFromPath, highlightCode } from "../../modes/interactive/them
|
|
|
9
9
|
import { formatDimensionNote, resizeImage } from "../../utils/image-resize.js";
|
|
10
10
|
import { detectSupportedImageMimeTypeFromFile } from "../../utils/mime.js";
|
|
11
11
|
import { formatPathRelativeToCwdOrAbsolute } from "../../utils/paths.js";
|
|
12
|
+
import { compressReadOutput } from "./output-compression.js";
|
|
12
13
|
import { resolveReadPath } from "./path-utils.js";
|
|
13
14
|
import { getTextOutput, invalidArgText, replaceTabs, shortenPath, str } from "./render-utils.js";
|
|
14
15
|
import { wrapToolDefinition } from "./tool-definition-wrapper.js";
|
|
@@ -220,11 +221,18 @@ export function createReadToolDefinition(cwd, options) {
|
|
|
220
221
|
selectedContent = allLines.slice(startLine, endLine).join("\n");
|
|
221
222
|
userLimitedLines = endLine - startLine;
|
|
222
223
|
}
|
|
224
|
+
else if (startLine === 0) {
|
|
225
|
+
// Whole-file read (common case): reuse the original string instead of
|
|
226
|
+
// rebuilding it from the split lines.
|
|
227
|
+
selectedContent = textContent;
|
|
228
|
+
}
|
|
223
229
|
else {
|
|
224
230
|
selectedContent = allLines.slice(startLine).join("\n");
|
|
225
231
|
}
|
|
226
232
|
// Apply truncation, respecting both line and byte limits.
|
|
227
|
-
|
|
233
|
+
// Apply lossless compression then truncation.
|
|
234
|
+
const compressedContent = compressReadOutput(selectedContent);
|
|
235
|
+
const truncation = truncateHead(compressedContent);
|
|
228
236
|
let outputText;
|
|
229
237
|
if (truncation.firstLineExceedsLimit) {
|
|
230
238
|
// First line alone exceeds the byte limit. Point the model at a bash fallback.
|