@oh-my-pi/pi-coding-agent 6.1.0 → 6.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +56 -0
- package/docs/sdk.md +1 -1
- package/package.json +5 -5
- package/scripts/generate-template.ts +6 -6
- package/src/cli/args.ts +3 -0
- package/src/core/agent-session.ts +39 -0
- package/src/core/bash-executor.ts +3 -3
- package/src/core/cursor/exec-bridge.ts +95 -88
- package/src/core/custom-commands/bundled/review/index.ts +142 -145
- package/src/core/custom-commands/bundled/wt/index.ts +68 -66
- package/src/core/custom-commands/loader.ts +4 -6
- package/src/core/custom-tools/index.ts +2 -2
- package/src/core/custom-tools/loader.ts +66 -61
- package/src/core/custom-tools/types.ts +4 -4
- package/src/core/custom-tools/wrapper.ts +61 -25
- package/src/core/event-bus.ts +19 -47
- package/src/core/extensions/index.ts +8 -4
- package/src/core/extensions/loader.ts +160 -120
- package/src/core/extensions/types.ts +4 -4
- package/src/core/extensions/wrapper.ts +149 -100
- package/src/core/hooks/index.ts +1 -1
- package/src/core/hooks/tool-wrapper.ts +96 -70
- package/src/core/hooks/types.ts +1 -2
- package/src/core/index.ts +1 -0
- package/src/core/mcp/index.ts +6 -2
- package/src/core/mcp/json-rpc.ts +88 -0
- package/src/core/mcp/loader.ts +22 -4
- package/src/core/mcp/manager.ts +202 -48
- package/src/core/mcp/tool-bridge.ts +143 -55
- package/src/core/mcp/tool-cache.ts +122 -0
- package/src/core/python-executor.ts +3 -9
- package/src/core/sdk.ts +33 -32
- package/src/core/session-manager.ts +30 -0
- package/src/core/settings-manager.ts +34 -1
- package/src/core/ssh/ssh-executor.ts +6 -84
- package/src/core/streaming-output.ts +107 -53
- package/src/core/tools/ask.ts +92 -93
- package/src/core/tools/bash.ts +103 -94
- package/src/core/tools/calculator.ts +41 -26
- package/src/core/tools/complete.ts +76 -66
- package/src/core/tools/context.ts +25 -25
- package/src/core/tools/exa/index.ts +1 -1
- package/src/core/tools/exa/mcp-client.ts +56 -101
- package/src/core/tools/find.ts +250 -253
- package/src/core/tools/git.ts +39 -33
- package/src/core/tools/grep.ts +440 -427
- package/src/core/tools/index.ts +62 -61
- package/src/core/tools/ls.ts +119 -114
- package/src/core/tools/lsp/clients/biome-client.ts +5 -7
- package/src/core/tools/lsp/clients/index.ts +4 -4
- package/src/core/tools/lsp/clients/lsp-linter-client.ts +5 -7
- package/src/core/tools/lsp/config.ts +2 -2
- package/src/core/tools/lsp/index.ts +824 -639
- package/src/core/tools/notebook.ts +121 -119
- package/src/core/tools/output.ts +163 -147
- package/src/core/tools/patch/applicator.ts +1100 -0
- package/src/core/tools/patch/diff.ts +362 -0
- package/src/core/tools/patch/fuzzy.ts +647 -0
- package/src/core/tools/patch/index.ts +430 -0
- package/src/core/tools/patch/normalize.ts +220 -0
- package/src/core/tools/patch/normative.ts +49 -0
- package/src/core/tools/patch/parser.ts +528 -0
- package/src/core/tools/patch/shared.ts +228 -0
- package/src/core/tools/patch/types.ts +244 -0
- package/src/core/tools/python.ts +139 -136
- package/src/core/tools/read.ts +237 -216
- package/src/core/tools/render-utils.ts +196 -77
- package/src/core/tools/renderers.ts +1 -1
- package/src/core/tools/ssh.ts +99 -80
- package/src/core/tools/task/executor.ts +11 -7
- package/src/core/tools/task/index.ts +352 -343
- package/src/core/tools/task/worker.ts +13 -23
- package/src/core/tools/todo-write.ts +74 -59
- package/src/core/tools/web-fetch.ts +54 -47
- package/src/core/tools/web-search/index.ts +27 -16
- package/src/core/tools/write.ts +89 -41
- package/src/core/ttsr.ts +106 -152
- package/src/core/voice.ts +49 -39
- package/src/index.ts +16 -12
- package/src/lib/worktree/index.ts +1 -9
- package/src/modes/interactive/components/diff.ts +15 -8
- package/src/modes/interactive/components/settings-defs.ts +24 -0
- package/src/modes/interactive/components/tool-execution.ts +34 -6
- package/src/modes/interactive/controllers/event-controller.ts +6 -19
- package/src/modes/interactive/controllers/input-controller.ts +1 -1
- package/src/modes/interactive/utils/ui-helpers.ts +5 -1
- package/src/modes/rpc/rpc-mode.ts +99 -81
- package/src/prompts/tools/patch.md +76 -0
- package/src/prompts/tools/read.md +1 -1
- package/src/prompts/tools/{edit.md → replace.md} +1 -0
- package/src/utils/shell.ts +0 -40
- package/src/core/tools/edit-diff.ts +0 -574
- package/src/core/tools/edit.ts +0 -326
package/src/core/tools/grep.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import nodePath from "node:path";
|
|
2
|
-
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
2
|
+
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
3
3
|
import { StringEnum } from "@oh-my-pi/pi-ai";
|
|
4
4
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
5
5
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
@@ -93,416 +93,204 @@ export interface GrepToolOptions {
|
|
|
93
93
|
operations?: GrepOperations;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
caseSensitive,
|
|
112
|
-
literal,
|
|
113
|
-
multiline,
|
|
114
|
-
context,
|
|
115
|
-
limit,
|
|
116
|
-
outputMode,
|
|
117
|
-
headLimit,
|
|
118
|
-
offset,
|
|
119
|
-
}: {
|
|
120
|
-
pattern: string;
|
|
121
|
-
path?: string;
|
|
122
|
-
glob?: string;
|
|
123
|
-
type?: string;
|
|
124
|
-
ignoreCase?: boolean;
|
|
125
|
-
caseSensitive?: boolean;
|
|
126
|
-
literal?: boolean;
|
|
127
|
-
multiline?: boolean;
|
|
128
|
-
context?: number;
|
|
129
|
-
limit?: number;
|
|
130
|
-
outputMode?: "content" | "files_with_matches" | "count";
|
|
131
|
-
headLimit?: number;
|
|
132
|
-
offset?: number;
|
|
133
|
-
},
|
|
134
|
-
signal?: AbortSignal,
|
|
135
|
-
) => {
|
|
136
|
-
return untilAborted(signal, async () => {
|
|
137
|
-
const rgPath = await ensureTool("rg", true);
|
|
138
|
-
if (!rgPath) {
|
|
139
|
-
throw new Error("ripgrep (rg) is not available and could not be downloaded");
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const searchPath = resolveToCwd(searchDir || ".", session.cwd);
|
|
143
|
-
const scopePath = (() => {
|
|
144
|
-
const relative = nodePath.relative(session.cwd, searchPath).replace(/\\/g, "/");
|
|
145
|
-
return relative.length === 0 ? "." : relative;
|
|
146
|
-
})();
|
|
96
|
+
interface GrepParams {
|
|
97
|
+
pattern: string;
|
|
98
|
+
path?: string;
|
|
99
|
+
glob?: string;
|
|
100
|
+
type?: string;
|
|
101
|
+
ignoreCase?: boolean;
|
|
102
|
+
caseSensitive?: boolean;
|
|
103
|
+
literal?: boolean;
|
|
104
|
+
multiline?: boolean;
|
|
105
|
+
context?: number;
|
|
106
|
+
limit?: number;
|
|
107
|
+
outputMode?: "content" | "files_with_matches" | "count";
|
|
108
|
+
headLimit?: number;
|
|
109
|
+
offset?: number;
|
|
110
|
+
}
|
|
147
111
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
112
|
+
export class GrepTool implements AgentTool<typeof grepSchema, GrepToolDetails> {
|
|
113
|
+
public readonly name = "grep";
|
|
114
|
+
public readonly label = "Grep";
|
|
115
|
+
public readonly description: string;
|
|
116
|
+
public readonly parameters = grepSchema;
|
|
117
|
+
|
|
118
|
+
private readonly session: ToolSession;
|
|
119
|
+
private readonly ops: GrepOperations;
|
|
120
|
+
|
|
121
|
+
constructor(session: ToolSession, options?: GrepToolOptions) {
|
|
122
|
+
this.session = session;
|
|
123
|
+
this.ops = options?.operations ?? defaultGrepOperations;
|
|
124
|
+
this.description = renderPromptTemplate(grepDescription);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
public async execute(
|
|
128
|
+
_toolCallId: string,
|
|
129
|
+
params: GrepParams,
|
|
130
|
+
signal?: AbortSignal,
|
|
131
|
+
_onUpdate?: AgentToolUpdateCallback<GrepToolDetails>,
|
|
132
|
+
_context?: AgentToolContext,
|
|
133
|
+
): Promise<AgentToolResult<GrepToolDetails>> {
|
|
134
|
+
const {
|
|
135
|
+
pattern,
|
|
136
|
+
path: searchDir,
|
|
137
|
+
glob,
|
|
138
|
+
type,
|
|
139
|
+
ignoreCase,
|
|
140
|
+
caseSensitive,
|
|
141
|
+
literal,
|
|
142
|
+
multiline,
|
|
143
|
+
context,
|
|
144
|
+
limit,
|
|
145
|
+
outputMode,
|
|
146
|
+
headLimit,
|
|
147
|
+
offset,
|
|
148
|
+
} = params;
|
|
149
|
+
|
|
150
|
+
return untilAborted(signal, async () => {
|
|
151
|
+
const rgPath = await ensureTool("rg", true);
|
|
152
|
+
if (!rgPath) {
|
|
153
|
+
throw new Error("ripgrep (rg) is not available and could not be downloaded");
|
|
154
|
+
}
|
|
169
155
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
156
|
+
const searchPath = resolveToCwd(searchDir || ".", this.session.cwd);
|
|
157
|
+
const scopePath = (() => {
|
|
158
|
+
const relative = nodePath.relative(this.session.cwd, searchPath).replace(/\\/g, "/");
|
|
159
|
+
return relative.length === 0 ? "." : relative;
|
|
160
|
+
})();
|
|
161
|
+
|
|
162
|
+
let isDirectory: boolean;
|
|
163
|
+
try {
|
|
164
|
+
isDirectory = await this.ops.isDirectory(searchPath);
|
|
165
|
+
} catch {
|
|
166
|
+
throw new Error(`Path not found: ${searchPath}`);
|
|
167
|
+
}
|
|
168
|
+
const contextValue = context && context > 0 ? context : 0;
|
|
169
|
+
const effectiveLimit = Math.max(1, limit ?? DEFAULT_LIMIT);
|
|
170
|
+
const effectiveOutputMode = outputMode ?? "content";
|
|
171
|
+
const effectiveOffset = offset && offset > 0 ? offset : 0;
|
|
172
|
+
const hasHeadLimit = headLimit !== undefined && headLimit > 0;
|
|
173
|
+
|
|
174
|
+
const formatPath = (filePath: string): string => {
|
|
175
|
+
if (isDirectory) {
|
|
176
|
+
const relative = nodePath.relative(searchPath, filePath);
|
|
177
|
+
if (relative && !relative.startsWith("..")) {
|
|
178
|
+
return relative.replace(/\\/g, "/");
|
|
183
179
|
}
|
|
184
|
-
return linesPromise;
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
const args: string[] = [];
|
|
188
|
-
|
|
189
|
-
// Base arguments depend on output mode
|
|
190
|
-
if (effectiveOutputMode === "files_with_matches") {
|
|
191
|
-
args.push("--files-with-matches", "--color=never", "--hidden");
|
|
192
|
-
} else if (effectiveOutputMode === "count") {
|
|
193
|
-
args.push("--count", "--color=never", "--hidden");
|
|
194
|
-
} else {
|
|
195
|
-
args.push("--json", "--line-number", "--color=never", "--hidden");
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
if (caseSensitive) {
|
|
199
|
-
args.push("--case-sensitive");
|
|
200
|
-
} else if (ignoreCase) {
|
|
201
|
-
args.push("--ignore-case");
|
|
202
|
-
} else {
|
|
203
|
-
args.push("--smart-case");
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
if (multiline) {
|
|
207
|
-
args.push("--multiline");
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
if (literal) {
|
|
211
|
-
args.push("--fixed-strings");
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
if (glob) {
|
|
215
|
-
args.push("--glob", glob);
|
|
216
180
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
let stderr = "";
|
|
231
|
-
let matchCount = 0;
|
|
232
|
-
let matchLimitReached = false;
|
|
233
|
-
let linesTruncated = false;
|
|
234
|
-
let aborted = false;
|
|
235
|
-
let killedDueToLimit = false;
|
|
236
|
-
const outputLines: string[] = [];
|
|
237
|
-
const files = new Set<string>();
|
|
238
|
-
const fileList: string[] = [];
|
|
239
|
-
const fileMatchCounts = new Map<string, number>();
|
|
240
|
-
|
|
241
|
-
const recordFile = (filePath: string) => {
|
|
242
|
-
const relative = formatPath(filePath);
|
|
243
|
-
if (!files.has(relative)) {
|
|
244
|
-
files.add(relative);
|
|
245
|
-
fileList.push(relative);
|
|
246
|
-
}
|
|
247
|
-
};
|
|
248
|
-
|
|
249
|
-
const recordFileMatch = (filePath: string) => {
|
|
250
|
-
const relative = formatPath(filePath);
|
|
251
|
-
fileMatchCounts.set(relative, (fileMatchCounts.get(relative) ?? 0) + 1);
|
|
252
|
-
};
|
|
253
|
-
|
|
254
|
-
const stopChild = (dueToLimit: boolean = false) => {
|
|
255
|
-
killedDueToLimit = dueToLimit;
|
|
256
|
-
child.kill();
|
|
257
|
-
};
|
|
258
|
-
|
|
259
|
-
using signalScope = new ScopeSignal(signal ? { signal } : undefined);
|
|
260
|
-
signalScope.catch(() => {
|
|
261
|
-
aborted = true;
|
|
262
|
-
stopChild();
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
// For simple output modes (files_with_matches, count), process text directly
|
|
266
|
-
if (effectiveOutputMode === "files_with_matches" || effectiveOutputMode === "count") {
|
|
267
|
-
const stdoutReader = (child.stdout as ReadableStream<Uint8Array>).getReader();
|
|
268
|
-
const stderrReader = (child.stderr as ReadableStream<Uint8Array>).getReader();
|
|
269
|
-
const decoder = new TextDecoder();
|
|
270
|
-
let stdout = "";
|
|
271
|
-
|
|
272
|
-
await Promise.all([
|
|
273
|
-
(async () => {
|
|
274
|
-
while (true) {
|
|
275
|
-
const { done, value } = await stdoutReader.read();
|
|
276
|
-
if (done) break;
|
|
277
|
-
stdout += decoder.decode(value, { stream: true });
|
|
278
|
-
}
|
|
279
|
-
})(),
|
|
280
|
-
(async () => {
|
|
281
|
-
while (true) {
|
|
282
|
-
const { done, value } = await stderrReader.read();
|
|
283
|
-
if (done) break;
|
|
284
|
-
stderr += decoder.decode(value, { stream: true });
|
|
285
|
-
}
|
|
286
|
-
})(),
|
|
287
|
-
]);
|
|
288
|
-
|
|
289
|
-
const exitCode = await child.exited;
|
|
290
|
-
|
|
291
|
-
if (aborted) {
|
|
292
|
-
throw new Error("Operation aborted");
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
if (exitCode !== 0 && exitCode !== 1) {
|
|
296
|
-
const errorMsg = stderr.trim() || `ripgrep exited with code ${exitCode}`;
|
|
297
|
-
throw new Error(errorMsg);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
const lines = stdout
|
|
301
|
-
.trim()
|
|
302
|
-
.split("\n")
|
|
303
|
-
.filter((line) => line.length > 0);
|
|
304
|
-
|
|
305
|
-
if (lines.length === 0) {
|
|
306
|
-
return {
|
|
307
|
-
content: [{ type: "text", text: "No matches found" }],
|
|
308
|
-
details: {
|
|
309
|
-
scopePath,
|
|
310
|
-
matchCount: 0,
|
|
311
|
-
fileCount: 0,
|
|
312
|
-
files: [],
|
|
313
|
-
mode: effectiveOutputMode,
|
|
314
|
-
truncated: false,
|
|
315
|
-
},
|
|
316
|
-
};
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
// Apply offset and headLimit
|
|
320
|
-
let processedLines = lines;
|
|
321
|
-
if (effectiveOffset > 0) {
|
|
322
|
-
processedLines = processedLines.slice(effectiveOffset);
|
|
323
|
-
}
|
|
324
|
-
if (hasHeadLimit) {
|
|
325
|
-
processedLines = processedLines.slice(0, headLimit);
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
let simpleMatchCount = 0;
|
|
329
|
-
let fileCount = 0;
|
|
330
|
-
const simpleFiles = new Set<string>();
|
|
331
|
-
const simpleFileList: string[] = [];
|
|
332
|
-
const simpleFileMatchCounts = new Map<string, number>();
|
|
333
|
-
|
|
334
|
-
const recordSimpleFile = (filePath: string) => {
|
|
335
|
-
const relative = formatPath(filePath);
|
|
336
|
-
if (!simpleFiles.has(relative)) {
|
|
337
|
-
simpleFiles.add(relative);
|
|
338
|
-
simpleFileList.push(relative);
|
|
181
|
+
return nodePath.basename(filePath);
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const fileCache = new Map<string, Promise<string[]>>();
|
|
185
|
+
const getFileLines = async (filePath: string): Promise<string[]> => {
|
|
186
|
+
let linesPromise = fileCache.get(filePath);
|
|
187
|
+
if (!linesPromise) {
|
|
188
|
+
linesPromise = (async () => {
|
|
189
|
+
try {
|
|
190
|
+
const content = await this.ops.readFile(filePath);
|
|
191
|
+
return content.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
|
|
192
|
+
} catch {
|
|
193
|
+
return [];
|
|
339
194
|
}
|
|
340
|
-
};
|
|
341
|
-
|
|
342
|
-
// Count mode: ripgrep provides total count per file, so we set directly (not increment)
|
|
343
|
-
const setFileMatchCount = (filePath: string, count: number) => {
|
|
344
|
-
const relative = formatPath(filePath);
|
|
345
|
-
simpleFileMatchCounts.set(relative, count);
|
|
346
|
-
};
|
|
347
|
-
|
|
348
|
-
if (effectiveOutputMode === "files_with_matches") {
|
|
349
|
-
for (const line of lines) {
|
|
350
|
-
recordSimpleFile(line);
|
|
351
|
-
}
|
|
352
|
-
fileCount = simpleFiles.size;
|
|
353
|
-
simpleMatchCount = fileCount;
|
|
354
|
-
} else {
|
|
355
|
-
for (const line of lines) {
|
|
356
|
-
const separatorIndex = line.lastIndexOf(":");
|
|
357
|
-
const filePart = separatorIndex === -1 ? line : line.slice(0, separatorIndex);
|
|
358
|
-
const countPart = separatorIndex === -1 ? "" : line.slice(separatorIndex + 1);
|
|
359
|
-
const count = Number.parseInt(countPart, 10);
|
|
360
|
-
recordSimpleFile(filePart);
|
|
361
|
-
if (!Number.isNaN(count)) {
|
|
362
|
-
simpleMatchCount += count;
|
|
363
|
-
setFileMatchCount(filePart, count);
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
fileCount = simpleFiles.size;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
const truncatedByHeadLimit = hasHeadLimit && processedLines.length < lines.length;
|
|
370
|
-
|
|
371
|
-
// For count mode, format as "path:count"
|
|
372
|
-
if (effectiveOutputMode === "count") {
|
|
373
|
-
const formatted = processedLines.map((line) => {
|
|
374
|
-
const separatorIndex = line.lastIndexOf(":");
|
|
375
|
-
const relative = formatPath(separatorIndex === -1 ? line : line.slice(0, separatorIndex));
|
|
376
|
-
const count = separatorIndex === -1 ? "0" : line.slice(separatorIndex + 1);
|
|
377
|
-
return `${relative}:${count}`;
|
|
378
|
-
});
|
|
379
|
-
const output = formatted.join("\n");
|
|
380
|
-
return {
|
|
381
|
-
content: [{ type: "text", text: output }],
|
|
382
|
-
details: {
|
|
383
|
-
scopePath,
|
|
384
|
-
matchCount: simpleMatchCount,
|
|
385
|
-
fileCount,
|
|
386
|
-
files: simpleFileList,
|
|
387
|
-
fileMatches: simpleFileList.map((path) => ({
|
|
388
|
-
path,
|
|
389
|
-
count: simpleFileMatchCounts.get(path) ?? 0,
|
|
390
|
-
})),
|
|
391
|
-
mode: effectiveOutputMode,
|
|
392
|
-
truncated: truncatedByHeadLimit,
|
|
393
|
-
headLimitReached: truncatedByHeadLimit ? headLimit : undefined,
|
|
394
|
-
},
|
|
395
|
-
};
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
// For files_with_matches, format paths
|
|
399
|
-
const formatted = processedLines.map((line) => formatPath(line));
|
|
400
|
-
const output = formatted.join("\n");
|
|
401
|
-
return {
|
|
402
|
-
content: [{ type: "text", text: output }],
|
|
403
|
-
details: {
|
|
404
|
-
scopePath,
|
|
405
|
-
matchCount: simpleMatchCount,
|
|
406
|
-
fileCount,
|
|
407
|
-
files: simpleFileList,
|
|
408
|
-
mode: effectiveOutputMode,
|
|
409
|
-
truncated: truncatedByHeadLimit,
|
|
410
|
-
headLimitReached: truncatedByHeadLimit ? headLimit : undefined,
|
|
411
|
-
},
|
|
412
|
-
};
|
|
195
|
+
})();
|
|
196
|
+
fileCache.set(filePath, linesPromise);
|
|
413
197
|
}
|
|
198
|
+
return linesPromise;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const args: string[] = [];
|
|
202
|
+
|
|
203
|
+
// Base arguments depend on output mode
|
|
204
|
+
if (effectiveOutputMode === "files_with_matches") {
|
|
205
|
+
args.push("--files-with-matches", "--color=never", "--hidden");
|
|
206
|
+
} else if (effectiveOutputMode === "count") {
|
|
207
|
+
args.push("--count", "--color=never", "--hidden");
|
|
208
|
+
} else {
|
|
209
|
+
args.push("--json", "--line-number", "--color=never", "--hidden");
|
|
210
|
+
}
|
|
414
211
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
const block: string[] = [];
|
|
424
|
-
const start = contextValue > 0 ? Math.max(1, lineNumber - contextValue) : lineNumber;
|
|
425
|
-
const end = contextValue > 0 ? Math.min(lines.length, lineNumber + contextValue) : lineNumber;
|
|
426
|
-
|
|
427
|
-
for (let current = start; current <= end; current++) {
|
|
428
|
-
const lineText = lines[current - 1] ?? "";
|
|
429
|
-
const sanitized = lineText.replace(/\r/g, "");
|
|
430
|
-
const isMatchLine = current === lineNumber;
|
|
431
|
-
|
|
432
|
-
const { text: truncatedText, wasTruncated } = truncateLine(sanitized);
|
|
433
|
-
if (wasTruncated) {
|
|
434
|
-
linesTruncated = true;
|
|
435
|
-
}
|
|
212
|
+
if (caseSensitive) {
|
|
213
|
+
args.push("--case-sensitive");
|
|
214
|
+
} else if (ignoreCase) {
|
|
215
|
+
args.push("--ignore-case");
|
|
216
|
+
} else {
|
|
217
|
+
args.push("--smart-case");
|
|
218
|
+
}
|
|
436
219
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
block.push(`${relativePath}-${current}- ${truncatedText}`);
|
|
441
|
-
}
|
|
442
|
-
}
|
|
220
|
+
if (multiline) {
|
|
221
|
+
args.push("--multiline");
|
|
222
|
+
}
|
|
443
223
|
|
|
444
|
-
|
|
445
|
-
|
|
224
|
+
if (literal) {
|
|
225
|
+
args.push("--fixed-strings");
|
|
226
|
+
}
|
|
446
227
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
}
|
|
228
|
+
if (glob) {
|
|
229
|
+
args.push("--glob", glob);
|
|
230
|
+
}
|
|
451
231
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
} catch {
|
|
456
|
-
return;
|
|
457
|
-
}
|
|
232
|
+
if (type) {
|
|
233
|
+
args.push("--type", type);
|
|
234
|
+
}
|
|
458
235
|
|
|
459
|
-
|
|
460
|
-
matchCount++;
|
|
461
|
-
const filePath = event.data?.path?.text;
|
|
462
|
-
const lineNumber = event.data?.line_number;
|
|
236
|
+
args.push("--", pattern, searchPath);
|
|
463
237
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
}
|
|
238
|
+
const child: Subprocess = Bun.spawn([rgPath, ...args], {
|
|
239
|
+
stdin: "ignore",
|
|
240
|
+
stdout: "pipe",
|
|
241
|
+
stderr: "pipe",
|
|
242
|
+
});
|
|
470
243
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
244
|
+
let stderr = "";
|
|
245
|
+
let matchCount = 0;
|
|
246
|
+
let matchLimitReached = false;
|
|
247
|
+
let linesTruncated = false;
|
|
248
|
+
let aborted = false;
|
|
249
|
+
let killedDueToLimit = false;
|
|
250
|
+
const outputLines: string[] = [];
|
|
251
|
+
const files = new Set<string>();
|
|
252
|
+
const fileList: string[] = [];
|
|
253
|
+
const fileMatchCounts = new Map<string, number>();
|
|
254
|
+
|
|
255
|
+
const recordFile = (filePath: string) => {
|
|
256
|
+
const relative = formatPath(filePath);
|
|
257
|
+
if (!files.has(relative)) {
|
|
258
|
+
files.add(relative);
|
|
259
|
+
fileList.push(relative);
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const recordFileMatch = (filePath: string) => {
|
|
264
|
+
const relative = formatPath(filePath);
|
|
265
|
+
fileMatchCounts.set(relative, (fileMatchCounts.get(relative) ?? 0) + 1);
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const stopChild = (dueToLimit: boolean = false) => {
|
|
269
|
+
killedDueToLimit = dueToLimit;
|
|
270
|
+
child.kill();
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
using signalScope = new ScopeSignal(signal ? { signal } : undefined);
|
|
274
|
+
signalScope.catch(() => {
|
|
275
|
+
aborted = true;
|
|
276
|
+
stopChild();
|
|
277
|
+
});
|
|
477
278
|
|
|
478
|
-
|
|
279
|
+
// For simple output modes (files_with_matches, count), process text directly
|
|
280
|
+
if (effectiveOutputMode === "files_with_matches" || effectiveOutputMode === "count") {
|
|
479
281
|
const stdoutReader = (child.stdout as ReadableStream<Uint8Array>).getReader();
|
|
480
282
|
const stderrReader = (child.stderr as ReadableStream<Uint8Array>).getReader();
|
|
481
283
|
const decoder = new TextDecoder();
|
|
482
|
-
let
|
|
284
|
+
let stdout = "";
|
|
483
285
|
|
|
484
286
|
await Promise.all([
|
|
485
|
-
// Process stdout line by line
|
|
486
287
|
(async () => {
|
|
487
288
|
while (true) {
|
|
488
289
|
const { done, value } = await stdoutReader.read();
|
|
489
290
|
if (done) break;
|
|
490
|
-
|
|
491
|
-
stdoutBuffer += decoder.decode(value, { stream: true });
|
|
492
|
-
const lines = stdoutBuffer.split("\n");
|
|
493
|
-
// Keep the last incomplete line in the buffer
|
|
494
|
-
stdoutBuffer = lines.pop() ?? "";
|
|
495
|
-
|
|
496
|
-
for (const line of lines) {
|
|
497
|
-
await processLine(line);
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
// Process any remaining content
|
|
501
|
-
if (stdoutBuffer.trim()) {
|
|
502
|
-
await processLine(stdoutBuffer);
|
|
291
|
+
stdout += decoder.decode(value, { stream: true });
|
|
503
292
|
}
|
|
504
293
|
})(),
|
|
505
|
-
// Collect stderr
|
|
506
294
|
(async () => {
|
|
507
295
|
while (true) {
|
|
508
296
|
const { done, value } = await stderrReader.read();
|
|
@@ -518,12 +306,17 @@ export function createGrepTool(session: ToolSession, options?: GrepToolOptions):
|
|
|
518
306
|
throw new Error("Operation aborted");
|
|
519
307
|
}
|
|
520
308
|
|
|
521
|
-
if (
|
|
309
|
+
if (exitCode !== 0 && exitCode !== 1) {
|
|
522
310
|
const errorMsg = stderr.trim() || `ripgrep exited with code ${exitCode}`;
|
|
523
311
|
throw new Error(errorMsg);
|
|
524
312
|
}
|
|
525
313
|
|
|
526
|
-
|
|
314
|
+
const lines = stdout
|
|
315
|
+
.trim()
|
|
316
|
+
.split("\n")
|
|
317
|
+
.filter((line) => line.length > 0);
|
|
318
|
+
|
|
319
|
+
if (lines.length === 0) {
|
|
527
320
|
return {
|
|
528
321
|
content: [{ type: "text", text: "No matches found" }],
|
|
529
322
|
details: {
|
|
@@ -537,8 +330,8 @@ export function createGrepTool(session: ToolSession, options?: GrepToolOptions):
|
|
|
537
330
|
};
|
|
538
331
|
}
|
|
539
332
|
|
|
540
|
-
// Apply offset and headLimit
|
|
541
|
-
let processedLines =
|
|
333
|
+
// Apply offset and headLimit
|
|
334
|
+
let processedLines = lines;
|
|
542
335
|
if (effectiveOffset > 0) {
|
|
543
336
|
processedLines = processedLines.slice(effectiveOffset);
|
|
544
337
|
}
|
|
@@ -546,57 +339,277 @@ export function createGrepTool(session: ToolSession, options?: GrepToolOptions):
|
|
|
546
339
|
processedLines = processedLines.slice(0, headLimit);
|
|
547
340
|
}
|
|
548
341
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
const
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
const
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
path,
|
|
562
|
-
count: fileMatchCounts.get(path) ?? 0,
|
|
563
|
-
})),
|
|
564
|
-
mode: effectiveOutputMode,
|
|
565
|
-
truncated: matchLimitReached || truncation.truncated || truncatedByHeadLimit,
|
|
566
|
-
headLimitReached: truncatedByHeadLimit ? headLimit : undefined,
|
|
342
|
+
let simpleMatchCount = 0;
|
|
343
|
+
let fileCount = 0;
|
|
344
|
+
const simpleFiles = new Set<string>();
|
|
345
|
+
const simpleFileList: string[] = [];
|
|
346
|
+
const simpleFileMatchCounts = new Map<string, number>();
|
|
347
|
+
|
|
348
|
+
const recordSimpleFile = (filePath: string) => {
|
|
349
|
+
const relative = formatPath(filePath);
|
|
350
|
+
if (!simpleFiles.has(relative)) {
|
|
351
|
+
simpleFiles.add(relative);
|
|
352
|
+
simpleFileList.push(relative);
|
|
353
|
+
}
|
|
567
354
|
};
|
|
568
355
|
|
|
569
|
-
//
|
|
570
|
-
const
|
|
356
|
+
// Count mode: ripgrep provides total count per file, so we set directly (not increment)
|
|
357
|
+
const setFileMatchCount = (filePath: string, count: number) => {
|
|
358
|
+
const relative = formatPath(filePath);
|
|
359
|
+
simpleFileMatchCounts.set(relative, count);
|
|
360
|
+
};
|
|
571
361
|
|
|
572
|
-
if (
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
362
|
+
if (effectiveOutputMode === "files_with_matches") {
|
|
363
|
+
for (const line of lines) {
|
|
364
|
+
recordSimpleFile(line);
|
|
365
|
+
}
|
|
366
|
+
fileCount = simpleFiles.size;
|
|
367
|
+
simpleMatchCount = fileCount;
|
|
368
|
+
} else {
|
|
369
|
+
for (const line of lines) {
|
|
370
|
+
const separatorIndex = line.lastIndexOf(":");
|
|
371
|
+
const filePart = separatorIndex === -1 ? line : line.slice(0, separatorIndex);
|
|
372
|
+
const countPart = separatorIndex === -1 ? "" : line.slice(separatorIndex + 1);
|
|
373
|
+
const count = Number.parseInt(countPart, 10);
|
|
374
|
+
recordSimpleFile(filePart);
|
|
375
|
+
if (!Number.isNaN(count)) {
|
|
376
|
+
simpleMatchCount += count;
|
|
377
|
+
setFileMatchCount(filePart, count);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
fileCount = simpleFiles.size;
|
|
577
381
|
}
|
|
578
382
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
383
|
+
const truncatedByHeadLimit = hasHeadLimit && processedLines.length < lines.length;
|
|
384
|
+
|
|
385
|
+
// For count mode, format as "path:count"
|
|
386
|
+
if (effectiveOutputMode === "count") {
|
|
387
|
+
const formatted = processedLines.map((line) => {
|
|
388
|
+
const separatorIndex = line.lastIndexOf(":");
|
|
389
|
+
const relative = formatPath(separatorIndex === -1 ? line : line.slice(0, separatorIndex));
|
|
390
|
+
const count = separatorIndex === -1 ? "0" : line.slice(separatorIndex + 1);
|
|
391
|
+
return `${relative}:${count}`;
|
|
392
|
+
});
|
|
393
|
+
const output = formatted.join("\n");
|
|
394
|
+
return {
|
|
395
|
+
content: [{ type: "text", text: output }],
|
|
396
|
+
details: {
|
|
397
|
+
scopePath,
|
|
398
|
+
matchCount: simpleMatchCount,
|
|
399
|
+
fileCount,
|
|
400
|
+
files: simpleFileList,
|
|
401
|
+
fileMatches: simpleFileList.map((path) => ({
|
|
402
|
+
path,
|
|
403
|
+
count: simpleFileMatchCounts.get(path) ?? 0,
|
|
404
|
+
})),
|
|
405
|
+
mode: effectiveOutputMode,
|
|
406
|
+
truncated: truncatedByHeadLimit,
|
|
407
|
+
headLimitReached: truncatedByHeadLimit ? headLimit : undefined,
|
|
408
|
+
},
|
|
409
|
+
};
|
|
582
410
|
}
|
|
583
411
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
412
|
+
// For files_with_matches, format paths
|
|
413
|
+
const formatted = processedLines.map((line) => formatPath(line));
|
|
414
|
+
const output = formatted.join("\n");
|
|
415
|
+
return {
|
|
416
|
+
content: [{ type: "text", text: output }],
|
|
417
|
+
details: {
|
|
418
|
+
scopePath,
|
|
419
|
+
matchCount: simpleMatchCount,
|
|
420
|
+
fileCount,
|
|
421
|
+
files: simpleFileList,
|
|
422
|
+
mode: effectiveOutputMode,
|
|
423
|
+
truncated: truncatedByHeadLimit,
|
|
424
|
+
headLimitReached: truncatedByHeadLimit ? headLimit : undefined,
|
|
425
|
+
},
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Content mode - existing JSON processing
|
|
430
|
+
const formatBlock = async (filePath: string, lineNumber: number): Promise<string[]> => {
|
|
431
|
+
const relativePath = formatPath(filePath);
|
|
432
|
+
const lines = await getFileLines(filePath);
|
|
433
|
+
if (!lines.length) {
|
|
434
|
+
return [`${relativePath}:${lineNumber}: (unable to read file)`];
|
|
587
435
|
}
|
|
588
436
|
|
|
589
|
-
|
|
590
|
-
|
|
437
|
+
const block: string[] = [];
|
|
438
|
+
const start = contextValue > 0 ? Math.max(1, lineNumber - contextValue) : lineNumber;
|
|
439
|
+
const end = contextValue > 0 ? Math.min(lines.length, lineNumber + contextValue) : lineNumber;
|
|
440
|
+
|
|
441
|
+
for (let current = start; current <= end; current++) {
|
|
442
|
+
const lineText = lines[current - 1] ?? "";
|
|
443
|
+
const sanitized = lineText.replace(/\r/g, "");
|
|
444
|
+
const isMatchLine = current === lineNumber;
|
|
445
|
+
|
|
446
|
+
const { text: truncatedText, wasTruncated } = truncateLine(sanitized);
|
|
447
|
+
if (wasTruncated) {
|
|
448
|
+
linesTruncated = true;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (isMatchLine) {
|
|
452
|
+
block.push(`${relativePath}:${current}: ${truncatedText}`);
|
|
453
|
+
} else {
|
|
454
|
+
block.push(`${relativePath}-${current}- ${truncatedText}`);
|
|
455
|
+
}
|
|
591
456
|
}
|
|
592
457
|
|
|
458
|
+
return block;
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
const processLine = async (line: string): Promise<void> => {
|
|
462
|
+
if (!line.trim() || matchCount >= effectiveLimit) {
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
let event: { type: string; data?: { path?: { text?: string }; line_number?: number } };
|
|
467
|
+
try {
|
|
468
|
+
event = JSON.parse(line);
|
|
469
|
+
} catch {
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (event.type === "match") {
|
|
474
|
+
matchCount++;
|
|
475
|
+
const filePath = event.data?.path?.text;
|
|
476
|
+
const lineNumber = event.data?.line_number;
|
|
477
|
+
|
|
478
|
+
if (filePath && typeof lineNumber === "number") {
|
|
479
|
+
recordFile(filePath);
|
|
480
|
+
recordFileMatch(filePath);
|
|
481
|
+
const block = await formatBlock(filePath, lineNumber);
|
|
482
|
+
outputLines.push(...block);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (matchCount >= effectiveLimit) {
|
|
486
|
+
matchLimitReached = true;
|
|
487
|
+
stopChild(true);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
// Read streams using Bun's ReadableStream API
|
|
493
|
+
const stdoutReader = (child.stdout as ReadableStream<Uint8Array>).getReader();
|
|
494
|
+
const stderrReader = (child.stderr as ReadableStream<Uint8Array>).getReader();
|
|
495
|
+
const decoder = new TextDecoder();
|
|
496
|
+
let stdoutBuffer = "";
|
|
497
|
+
|
|
498
|
+
await Promise.all([
|
|
499
|
+
// Process stdout line by line
|
|
500
|
+
(async () => {
|
|
501
|
+
while (true) {
|
|
502
|
+
const { done, value } = await stdoutReader.read();
|
|
503
|
+
if (done) break;
|
|
504
|
+
|
|
505
|
+
stdoutBuffer += decoder.decode(value, { stream: true });
|
|
506
|
+
const lines = stdoutBuffer.split("\n");
|
|
507
|
+
// Keep the last incomplete line in the buffer
|
|
508
|
+
stdoutBuffer = lines.pop() ?? "";
|
|
509
|
+
|
|
510
|
+
for (const line of lines) {
|
|
511
|
+
await processLine(line);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
// Process any remaining content
|
|
515
|
+
if (stdoutBuffer.trim()) {
|
|
516
|
+
await processLine(stdoutBuffer);
|
|
517
|
+
}
|
|
518
|
+
})(),
|
|
519
|
+
// Collect stderr
|
|
520
|
+
(async () => {
|
|
521
|
+
while (true) {
|
|
522
|
+
const { done, value } = await stderrReader.read();
|
|
523
|
+
if (done) break;
|
|
524
|
+
stderr += decoder.decode(value, { stream: true });
|
|
525
|
+
}
|
|
526
|
+
})(),
|
|
527
|
+
]);
|
|
528
|
+
|
|
529
|
+
const exitCode = await child.exited;
|
|
530
|
+
|
|
531
|
+
if (aborted) {
|
|
532
|
+
throw new Error("Operation aborted");
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (!killedDueToLimit && exitCode !== 0 && exitCode !== 1) {
|
|
536
|
+
const errorMsg = stderr.trim() || `ripgrep exited with code ${exitCode}`;
|
|
537
|
+
throw new Error(errorMsg);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if (matchCount === 0) {
|
|
593
541
|
return {
|
|
594
|
-
content: [{ type: "text", text:
|
|
595
|
-
details:
|
|
542
|
+
content: [{ type: "text", text: "No matches found" }],
|
|
543
|
+
details: {
|
|
544
|
+
scopePath,
|
|
545
|
+
matchCount: 0,
|
|
546
|
+
fileCount: 0,
|
|
547
|
+
files: [],
|
|
548
|
+
mode: effectiveOutputMode,
|
|
549
|
+
truncated: false,
|
|
550
|
+
},
|
|
596
551
|
};
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Apply offset and headLimit to output lines
|
|
555
|
+
let processedLines = outputLines;
|
|
556
|
+
if (effectiveOffset > 0) {
|
|
557
|
+
processedLines = processedLines.slice(effectiveOffset);
|
|
558
|
+
}
|
|
559
|
+
if (hasHeadLimit) {
|
|
560
|
+
processedLines = processedLines.slice(0, headLimit);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Apply byte truncation (no line limit since we already have match limit)
|
|
564
|
+
const rawOutput = processedLines.join("\n");
|
|
565
|
+
const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
|
|
566
|
+
|
|
567
|
+
let output = truncation.content;
|
|
568
|
+
const truncatedByHeadLimit = hasHeadLimit && processedLines.length < outputLines.length;
|
|
569
|
+
const details: GrepToolDetails = {
|
|
570
|
+
scopePath,
|
|
571
|
+
matchCount,
|
|
572
|
+
fileCount: files.size,
|
|
573
|
+
files: fileList,
|
|
574
|
+
fileMatches: fileList.map((path) => ({
|
|
575
|
+
path,
|
|
576
|
+
count: fileMatchCounts.get(path) ?? 0,
|
|
577
|
+
})),
|
|
578
|
+
mode: effectiveOutputMode,
|
|
579
|
+
truncated: matchLimitReached || truncation.truncated || truncatedByHeadLimit,
|
|
580
|
+
headLimitReached: truncatedByHeadLimit ? headLimit : undefined,
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
// Build notices
|
|
584
|
+
const notices: string[] = [];
|
|
585
|
+
|
|
586
|
+
if (matchLimitReached) {
|
|
587
|
+
notices.push(
|
|
588
|
+
`${effectiveLimit} matches limit reached. Use limit=${effectiveLimit * 2} for more, or refine pattern`,
|
|
589
|
+
);
|
|
590
|
+
details.matchLimitReached = effectiveLimit;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (truncation.truncated) {
|
|
594
|
+
notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);
|
|
595
|
+
details.truncation = truncation;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if (linesTruncated) {
|
|
599
|
+
notices.push(`Some lines truncated to ${GREP_MAX_LINE_LENGTH} chars. Use read tool to see full lines`);
|
|
600
|
+
details.linesTruncated = true;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (notices.length > 0) {
|
|
604
|
+
output += `\n\n[${notices.join(". ")}]`;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
return {
|
|
608
|
+
content: [{ type: "text", text: output }],
|
|
609
|
+
details,
|
|
610
|
+
};
|
|
611
|
+
});
|
|
612
|
+
}
|
|
600
613
|
}
|
|
601
614
|
|
|
602
615
|
// =============================================================================
|