@oh-my-pi/pi-coding-agent 3.30.0 → 3.32.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 +85 -0
- package/package.json +5 -5
- package/src/cli/args.ts +4 -0
- package/src/core/agent-session.ts +29 -2
- package/src/core/bash-executor.ts +2 -1
- package/src/core/custom-commands/bundled/review/index.ts +367 -14
- package/src/core/custom-commands/bundled/wt/index.ts +1 -1
- package/src/core/sdk.ts +10 -2
- package/src/core/session-manager.ts +158 -246
- package/src/core/session-storage.ts +379 -0
- package/src/core/settings-manager.ts +155 -4
- package/src/core/slash-commands.ts +39 -13
- package/src/core/system-prompt.ts +62 -64
- package/src/core/tools/ask.ts +5 -4
- package/src/core/tools/bash-interceptor.ts +26 -61
- package/src/core/tools/bash.ts +13 -8
- package/src/core/tools/edit-diff.ts +11 -4
- package/src/core/tools/edit.ts +7 -13
- package/src/core/tools/find.ts +111 -50
- package/src/core/tools/gemini-image.ts +128 -147
- package/src/core/tools/grep.ts +397 -415
- package/src/core/tools/index.test.ts +5 -1
- package/src/core/tools/index.ts +8 -4
- package/src/core/tools/ls.ts +12 -10
- package/src/core/tools/lsp/client.ts +84 -19
- package/src/core/tools/lsp/config.ts +205 -656
- package/src/core/tools/lsp/defaults.json +465 -0
- package/src/core/tools/lsp/index.ts +72 -35
- package/src/core/tools/lsp/rust-analyzer.ts +49 -10
- package/src/core/tools/lsp/types.ts +1 -0
- package/src/core/tools/lsp/utils.ts +1 -1
- package/src/core/tools/read.ts +150 -74
- package/src/core/tools/render-utils.ts +70 -10
- package/src/core/tools/review.ts +38 -126
- package/src/core/tools/task/artifacts.ts +5 -4
- package/src/core/tools/task/commands.ts +4 -0
- package/src/core/tools/task/executor.ts +94 -83
- package/src/core/tools/task/index.ts +130 -92
- package/src/core/tools/task/parallel.ts +30 -3
- package/src/core/tools/task/render.ts +85 -39
- package/src/core/tools/task/types.ts +15 -6
- package/src/core/tools/task/worker.ts +124 -89
- package/src/core/tools/web-fetch.ts +112 -377
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/artifacthub.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/arxiv.ts +8 -4
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/aur.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/biorxiv.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/bluesky.ts +10 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/brew.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/cheatsh.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/chocolatey.ts +6 -1
- package/src/core/tools/web-scrapers/choosealicense.ts +110 -0
- package/src/core/tools/web-scrapers/cisa-kev.ts +100 -0
- package/src/core/tools/web-scrapers/clojars.ts +180 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/coingecko.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/crates-io.ts +7 -2
- package/src/core/tools/web-scrapers/crossref.ts +149 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/devto.ts +8 -4
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/discogs.ts +6 -1
- package/src/core/tools/web-scrapers/discourse.ts +221 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/dockerhub.ts +7 -3
- package/src/core/tools/web-scrapers/fdroid.ts +158 -0
- package/src/core/tools/web-scrapers/firefox-addons.ts +214 -0
- package/src/core/tools/web-scrapers/flathub.ts +239 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/github-gist.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/github.ts +63 -32
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/gitlab.ts +31 -19
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/go-pkg.ts +8 -4
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/hackage.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/hackernews.ts +18 -18
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/hex.ts +3 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/huggingface.ts +10 -10
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/iacr.ts +8 -4
- package/src/core/tools/web-scrapers/index.ts +250 -0
- package/src/core/tools/web-scrapers/jetbrains-marketplace.ts +169 -0
- package/src/core/tools/web-scrapers/lemmy.ts +220 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/lobsters.ts +3 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/mastodon.ts +11 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/maven.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/mdn.ts +2 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/metacpan.ts +13 -7
- package/src/core/tools/web-scrapers/musicbrainz.ts +273 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/npm.ts +12 -5
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/nuget.ts +9 -5
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/nvd.ts +6 -1
- package/src/core/tools/web-scrapers/ollama.ts +267 -0
- package/src/core/tools/web-scrapers/open-vsx.ts +119 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/opencorporates.ts +2 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/openlibrary.ts +18 -12
- package/src/core/tools/web-scrapers/orcid.ts +299 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/osv.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/packagist.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/pub-dev.ts +3 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/pubmed.ts +8 -4
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/pypi.ts +7 -3
- package/src/core/tools/web-scrapers/rawg.ts +124 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/readthedocs.ts +7 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/reddit.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/repology.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/rfc.ts +7 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/rubygems.ts +6 -1
- package/src/core/tools/web-scrapers/searchcode.ts +217 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/sec-edgar.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/semantic-scholar.ts +2 -2
- package/src/core/tools/web-scrapers/snapcraft.ts +200 -0
- package/src/core/tools/web-scrapers/sourcegraph.ts +373 -0
- package/src/core/tools/web-scrapers/spdx.ts +121 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/spotify.ts +3 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/stackoverflow.ts +3 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/terraform.ts +11 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/tldr.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/twitter.ts +15 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/types.ts +98 -27
- package/src/core/tools/web-scrapers/utils.ts +162 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/vimeo.ts +3 -3
- package/src/core/tools/web-scrapers/vscode-marketplace.ts +195 -0
- package/src/core/tools/web-scrapers/w3c.ts +163 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/wikidata.ts +13 -5
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/wikipedia.ts +7 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/youtube.ts +72 -20
- package/src/core/tools/write.ts +21 -18
- package/src/core/voice.ts +3 -2
- package/src/lib/worktree/collapse.ts +2 -1
- package/src/lib/worktree/git.ts +2 -18
- package/src/main.ts +59 -3
- package/src/modes/interactive/components/extensions/extension-dashboard.ts +33 -19
- package/src/modes/interactive/components/extensions/extension-list.ts +15 -8
- package/src/modes/interactive/components/hook-editor.ts +2 -1
- package/src/modes/interactive/components/model-selector.ts +19 -4
- package/src/modes/interactive/interactive-mode.ts +41 -63
- package/src/modes/interactive/theme/theme.ts +58 -58
- package/src/modes/rpc/rpc-mode.ts +10 -9
- package/src/prompts/review-request.md +27 -0
- package/src/prompts/reviewer.md +64 -68
- package/src/prompts/tools/output.md +22 -3
- package/src/prompts/tools/task.md +32 -33
- package/src/utils/clipboard.ts +2 -1
- package/examples/extensions/subagent/agents/reviewer.md +0 -35
- package/src/core/tools/web-fetch-handlers/index.ts +0 -69
- package/src/core/tools/web-fetch-handlers/utils.ts +0 -91
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/academic.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/business.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/dev-platforms.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/documentation.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/finance-media.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/git-hosting.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/media.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/package-managers-2.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/package-managers.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/package-registries.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/research.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/security.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/social-extended.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/social.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/stackexchange.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/standards.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/wikipedia.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/youtube.test.ts +0 -0
package/src/core/tools/grep.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { readFileSync, type Stats, statSync } from "node:fs";
|
|
2
1
|
import nodePath from "node:path";
|
|
3
2
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
4
3
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
@@ -9,19 +8,10 @@ import { getLanguageFromPath, type Theme } from "../../modes/interactive/theme/t
|
|
|
9
8
|
import grepDescription from "../../prompts/tools/grep.md" with { type: "text" };
|
|
10
9
|
import { ensureTool } from "../../utils/tools-manager";
|
|
11
10
|
import type { RenderResultOptions } from "../custom-tools/types";
|
|
11
|
+
import { ScopeSignal, untilAborted } from "../utils";
|
|
12
12
|
import type { ToolSession } from "./index";
|
|
13
13
|
import { resolveToCwd } from "./path-utils";
|
|
14
|
-
import {
|
|
15
|
-
formatCount,
|
|
16
|
-
formatEmptyMessage,
|
|
17
|
-
formatErrorMessage,
|
|
18
|
-
formatExpandHint,
|
|
19
|
-
formatMeta,
|
|
20
|
-
formatMoreItems,
|
|
21
|
-
formatScope,
|
|
22
|
-
formatTruncationSuffix,
|
|
23
|
-
PREVIEW_LIMITS,
|
|
24
|
-
} from "./render-utils";
|
|
14
|
+
import { createToolUIKit, PREVIEW_LIMITS } from "./render-utils";
|
|
25
15
|
import {
|
|
26
16
|
DEFAULT_MAX_BYTES,
|
|
27
17
|
formatSize,
|
|
@@ -119,155 +109,377 @@ export function createGrepTool(session: ToolSession): AgentTool<typeof grepSchem
|
|
|
119
109
|
},
|
|
120
110
|
signal?: AbortSignal,
|
|
121
111
|
) => {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
112
|
+
return untilAborted(signal, async () => {
|
|
113
|
+
const rgPath = await ensureTool("rg", true);
|
|
114
|
+
if (!rgPath) {
|
|
115
|
+
throw new Error("ripgrep (rg) is not available and could not be downloaded");
|
|
116
|
+
}
|
|
125
117
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
118
|
+
const searchPath = resolveToCwd(searchDir || ".", session.cwd);
|
|
119
|
+
const scopePath = (() => {
|
|
120
|
+
const relative = nodePath.relative(session.cwd, searchPath).replace(/\\/g, "/");
|
|
121
|
+
return relative.length === 0 ? "." : relative;
|
|
122
|
+
})();
|
|
123
|
+
let searchStat: Awaited<ReturnType<Bun.BunFile["stat"]>>;
|
|
124
|
+
try {
|
|
125
|
+
searchStat = await Bun.file(searchPath).stat();
|
|
126
|
+
} catch {
|
|
127
|
+
throw new Error(`Path not found: ${searchPath}`);
|
|
128
|
+
}
|
|
130
129
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
130
|
+
const isDirectory = searchStat.isDirectory();
|
|
131
|
+
const contextValue = context && context > 0 ? context : 0;
|
|
132
|
+
const effectiveLimit = Math.max(1, limit ?? DEFAULT_LIMIT);
|
|
133
|
+
const effectiveOutputMode = outputMode ?? "content";
|
|
134
|
+
const effectiveOffset = offset && offset > 0 ? offset : 0;
|
|
135
|
+
const hasHeadLimit = headLimit !== undefined && headLimit > 0;
|
|
136
|
+
|
|
137
|
+
const formatPath = (filePath: string): string => {
|
|
138
|
+
if (isDirectory) {
|
|
139
|
+
const relative = nodePath.relative(searchPath, filePath);
|
|
140
|
+
if (relative && !relative.startsWith("..")) {
|
|
141
|
+
return relative.replace(/\\/g, "/");
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return nodePath.basename(filePath);
|
|
145
|
+
};
|
|
142
146
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
147
|
+
const fileCache = new Map<string, Promise<string[]>>();
|
|
148
|
+
const getFileLines = async (filePath: string): Promise<string[]> => {
|
|
149
|
+
let linesPromise = fileCache.get(filePath);
|
|
150
|
+
if (!linesPromise) {
|
|
151
|
+
linesPromise = (async () => {
|
|
152
|
+
try {
|
|
153
|
+
const content = await Bun.file(filePath).text();
|
|
154
|
+
return content.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
|
|
155
|
+
} catch {
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
})();
|
|
159
|
+
fileCache.set(filePath, linesPromise);
|
|
155
160
|
}
|
|
161
|
+
return linesPromise;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const args: string[] = [];
|
|
165
|
+
|
|
166
|
+
// Base arguments depend on output mode
|
|
167
|
+
if (effectiveOutputMode === "files_with_matches") {
|
|
168
|
+
args.push("--files-with-matches", "--color=never", "--hidden");
|
|
169
|
+
} else if (effectiveOutputMode === "count") {
|
|
170
|
+
args.push("--count", "--color=never", "--hidden");
|
|
171
|
+
} else {
|
|
172
|
+
args.push("--json", "--line-number", "--color=never", "--hidden");
|
|
156
173
|
}
|
|
157
|
-
return nodePath.basename(filePath);
|
|
158
|
-
};
|
|
159
174
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
lines = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
|
|
167
|
-
} catch {
|
|
168
|
-
lines = [];
|
|
169
|
-
}
|
|
170
|
-
fileCache.set(filePath, lines);
|
|
175
|
+
if (caseSensitive) {
|
|
176
|
+
args.push("--case-sensitive");
|
|
177
|
+
} else if (ignoreCase) {
|
|
178
|
+
args.push("--ignore-case");
|
|
179
|
+
} else {
|
|
180
|
+
args.push("--smart-case");
|
|
171
181
|
}
|
|
172
|
-
return lines;
|
|
173
|
-
};
|
|
174
|
-
|
|
175
|
-
const args: string[] = [];
|
|
176
|
-
|
|
177
|
-
// Base arguments depend on output mode
|
|
178
|
-
if (effectiveOutputMode === "files_with_matches") {
|
|
179
|
-
args.push("--files-with-matches", "--color=never", "--hidden");
|
|
180
|
-
} else if (effectiveOutputMode === "count") {
|
|
181
|
-
args.push("--count", "--color=never", "--hidden");
|
|
182
|
-
} else {
|
|
183
|
-
args.push("--json", "--line-number", "--color=never", "--hidden");
|
|
184
|
-
}
|
|
185
182
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
args.push("--ignore-case");
|
|
190
|
-
} else {
|
|
191
|
-
args.push("--smart-case");
|
|
192
|
-
}
|
|
183
|
+
if (multiline) {
|
|
184
|
+
args.push("--multiline");
|
|
185
|
+
}
|
|
193
186
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
187
|
+
if (literal) {
|
|
188
|
+
args.push("--fixed-strings");
|
|
189
|
+
}
|
|
197
190
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
191
|
+
if (glob) {
|
|
192
|
+
args.push("--glob", glob);
|
|
193
|
+
}
|
|
201
194
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
195
|
+
if (type) {
|
|
196
|
+
args.push("--type", type);
|
|
197
|
+
}
|
|
205
198
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
199
|
+
args.push(pattern, searchPath);
|
|
200
|
+
|
|
201
|
+
const child: Subprocess = Bun.spawn([rgPath, ...args], {
|
|
202
|
+
stdin: "ignore",
|
|
203
|
+
stdout: "pipe",
|
|
204
|
+
stderr: "pipe",
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
let stderr = "";
|
|
208
|
+
let matchCount = 0;
|
|
209
|
+
let matchLimitReached = false;
|
|
210
|
+
let linesTruncated = false;
|
|
211
|
+
let aborted = false;
|
|
212
|
+
let killedDueToLimit = false;
|
|
213
|
+
const outputLines: string[] = [];
|
|
214
|
+
const files = new Set<string>();
|
|
215
|
+
const fileList: string[] = [];
|
|
216
|
+
const fileMatchCounts = new Map<string, number>();
|
|
217
|
+
|
|
218
|
+
const recordFile = (filePath: string) => {
|
|
219
|
+
const relative = formatPath(filePath);
|
|
220
|
+
if (!files.has(relative)) {
|
|
221
|
+
files.add(relative);
|
|
222
|
+
fileList.push(relative);
|
|
223
|
+
}
|
|
224
|
+
};
|
|
209
225
|
|
|
210
|
-
|
|
226
|
+
const recordFileMatch = (filePath: string) => {
|
|
227
|
+
const relative = formatPath(filePath);
|
|
228
|
+
fileMatchCounts.set(relative, (fileMatchCounts.get(relative) ?? 0) + 1);
|
|
229
|
+
};
|
|
211
230
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
231
|
+
const stopChild = (dueToLimit: boolean = false) => {
|
|
232
|
+
killedDueToLimit = dueToLimit;
|
|
233
|
+
child.kill();
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
using signalScope = new ScopeSignal(signal ? { signal } : undefined);
|
|
237
|
+
signalScope.catch(() => {
|
|
238
|
+
aborted = true;
|
|
239
|
+
stopChild();
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// For simple output modes (files_with_matches, count), process text directly
|
|
243
|
+
if (effectiveOutputMode === "files_with_matches" || effectiveOutputMode === "count") {
|
|
244
|
+
const stdoutReader = (child.stdout as ReadableStream<Uint8Array>).getReader();
|
|
245
|
+
const stderrReader = (child.stderr as ReadableStream<Uint8Array>).getReader();
|
|
246
|
+
const decoder = new TextDecoder();
|
|
247
|
+
let stdout = "";
|
|
248
|
+
|
|
249
|
+
await Promise.all([
|
|
250
|
+
(async () => {
|
|
251
|
+
while (true) {
|
|
252
|
+
const { done, value } = await stdoutReader.read();
|
|
253
|
+
if (done) break;
|
|
254
|
+
stdout += decoder.decode(value, { stream: true });
|
|
255
|
+
}
|
|
256
|
+
})(),
|
|
257
|
+
(async () => {
|
|
258
|
+
while (true) {
|
|
259
|
+
const { done, value } = await stderrReader.read();
|
|
260
|
+
if (done) break;
|
|
261
|
+
stderr += decoder.decode(value, { stream: true });
|
|
262
|
+
}
|
|
263
|
+
})(),
|
|
264
|
+
]);
|
|
265
|
+
|
|
266
|
+
const exitCode = await child.exited;
|
|
267
|
+
|
|
268
|
+
if (aborted) {
|
|
269
|
+
throw new Error("Operation aborted");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (exitCode !== 0 && exitCode !== 1) {
|
|
273
|
+
const errorMsg = stderr.trim() || `ripgrep exited with code ${exitCode}`;
|
|
274
|
+
throw new Error(errorMsg);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const lines = stdout
|
|
278
|
+
.trim()
|
|
279
|
+
.split("\n")
|
|
280
|
+
.filter((line) => line.length > 0);
|
|
281
|
+
|
|
282
|
+
if (lines.length === 0) {
|
|
283
|
+
return {
|
|
284
|
+
content: [{ type: "text", text: "No matches found" }],
|
|
285
|
+
details: {
|
|
286
|
+
scopePath,
|
|
287
|
+
matchCount: 0,
|
|
288
|
+
fileCount: 0,
|
|
289
|
+
files: [],
|
|
290
|
+
mode: effectiveOutputMode,
|
|
291
|
+
truncated: false,
|
|
292
|
+
},
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Apply offset and headLimit
|
|
297
|
+
let processedLines = lines;
|
|
298
|
+
if (effectiveOffset > 0) {
|
|
299
|
+
processedLines = processedLines.slice(effectiveOffset);
|
|
300
|
+
}
|
|
301
|
+
if (hasHeadLimit) {
|
|
302
|
+
processedLines = processedLines.slice(0, headLimit);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
let simpleMatchCount = 0;
|
|
306
|
+
let fileCount = 0;
|
|
307
|
+
const simpleFiles = new Set<string>();
|
|
308
|
+
const simpleFileList: string[] = [];
|
|
309
|
+
const simpleFileMatchCounts = new Map<string, number>();
|
|
310
|
+
|
|
311
|
+
const recordSimpleFile = (filePath: string) => {
|
|
312
|
+
const relative = formatPath(filePath);
|
|
313
|
+
if (!simpleFiles.has(relative)) {
|
|
314
|
+
simpleFiles.add(relative);
|
|
315
|
+
simpleFileList.push(relative);
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
// Count mode: ripgrep provides total count per file, so we set directly (not increment)
|
|
320
|
+
const setFileMatchCount = (filePath: string, count: number) => {
|
|
321
|
+
const relative = formatPath(filePath);
|
|
322
|
+
simpleFileMatchCounts.set(relative, count);
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
if (effectiveOutputMode === "files_with_matches") {
|
|
326
|
+
for (const line of lines) {
|
|
327
|
+
recordSimpleFile(line);
|
|
328
|
+
}
|
|
329
|
+
fileCount = simpleFiles.size;
|
|
330
|
+
simpleMatchCount = fileCount;
|
|
331
|
+
} else {
|
|
332
|
+
for (const line of lines) {
|
|
333
|
+
const separatorIndex = line.lastIndexOf(":");
|
|
334
|
+
const filePart = separatorIndex === -1 ? line : line.slice(0, separatorIndex);
|
|
335
|
+
const countPart = separatorIndex === -1 ? "" : line.slice(separatorIndex + 1);
|
|
336
|
+
const count = Number.parseInt(countPart, 10);
|
|
337
|
+
recordSimpleFile(filePart);
|
|
338
|
+
if (!Number.isNaN(count)) {
|
|
339
|
+
simpleMatchCount += count;
|
|
340
|
+
setFileMatchCount(filePart, count);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
fileCount = simpleFiles.size;
|
|
344
|
+
}
|
|
217
345
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
346
|
+
const truncatedByHeadLimit = hasHeadLimit && processedLines.length < lines.length;
|
|
347
|
+
|
|
348
|
+
// For count mode, format as "path:count"
|
|
349
|
+
if (effectiveOutputMode === "count") {
|
|
350
|
+
const formatted = processedLines.map((line) => {
|
|
351
|
+
const separatorIndex = line.lastIndexOf(":");
|
|
352
|
+
const relative = formatPath(separatorIndex === -1 ? line : line.slice(0, separatorIndex));
|
|
353
|
+
const count = separatorIndex === -1 ? "0" : line.slice(separatorIndex + 1);
|
|
354
|
+
return `${relative}:${count}`;
|
|
355
|
+
});
|
|
356
|
+
const output = formatted.join("\n");
|
|
357
|
+
return {
|
|
358
|
+
content: [{ type: "text", text: output }],
|
|
359
|
+
details: {
|
|
360
|
+
scopePath,
|
|
361
|
+
matchCount: simpleMatchCount,
|
|
362
|
+
fileCount,
|
|
363
|
+
files: simpleFileList,
|
|
364
|
+
fileMatches: simpleFileList.map((path) => ({
|
|
365
|
+
path,
|
|
366
|
+
count: simpleFileMatchCounts.get(path) ?? 0,
|
|
367
|
+
})),
|
|
368
|
+
mode: effectiveOutputMode,
|
|
369
|
+
truncated: truncatedByHeadLimit,
|
|
370
|
+
headLimitReached: truncatedByHeadLimit ? headLimit : undefined,
|
|
371
|
+
},
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// For files_with_matches, format paths
|
|
376
|
+
const formatted = processedLines.map((line) => formatPath(line));
|
|
377
|
+
const output = formatted.join("\n");
|
|
378
|
+
return {
|
|
379
|
+
content: [{ type: "text", text: output }],
|
|
380
|
+
details: {
|
|
381
|
+
scopePath,
|
|
382
|
+
matchCount: simpleMatchCount,
|
|
383
|
+
fileCount,
|
|
384
|
+
files: simpleFileList,
|
|
385
|
+
mode: effectiveOutputMode,
|
|
386
|
+
truncated: truncatedByHeadLimit,
|
|
387
|
+
headLimitReached: truncatedByHeadLimit ? headLimit : undefined,
|
|
388
|
+
},
|
|
389
|
+
};
|
|
234
390
|
}
|
|
235
|
-
};
|
|
236
391
|
|
|
237
|
-
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
392
|
+
// Content mode - existing JSON processing
|
|
393
|
+
const formatBlock = async (filePath: string, lineNumber: number): Promise<string[]> => {
|
|
394
|
+
const relativePath = formatPath(filePath);
|
|
395
|
+
const lines = await getFileLines(filePath);
|
|
396
|
+
if (!lines.length) {
|
|
397
|
+
return [`${relativePath}:${lineNumber}: (unable to read file)`];
|
|
398
|
+
}
|
|
241
399
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
};
|
|
400
|
+
const block: string[] = [];
|
|
401
|
+
const start = contextValue > 0 ? Math.max(1, lineNumber - contextValue) : lineNumber;
|
|
402
|
+
const end = contextValue > 0 ? Math.min(lines.length, lineNumber + contextValue) : lineNumber;
|
|
246
403
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
404
|
+
for (let current = start; current <= end; current++) {
|
|
405
|
+
const lineText = lines[current - 1] ?? "";
|
|
406
|
+
const sanitized = lineText.replace(/\r/g, "");
|
|
407
|
+
const isMatchLine = current === lineNumber;
|
|
251
408
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
409
|
+
const { text: truncatedText, wasTruncated } = truncateLine(sanitized);
|
|
410
|
+
if (wasTruncated) {
|
|
411
|
+
linesTruncated = true;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (isMatchLine) {
|
|
415
|
+
block.push(`${relativePath}:${current}: ${truncatedText}`);
|
|
416
|
+
} else {
|
|
417
|
+
block.push(`${relativePath}-${current}- ${truncatedText}`);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return block;
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
const processLine = async (line: string): Promise<void> => {
|
|
425
|
+
if (!line.trim() || matchCount >= effectiveLimit) {
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
let event: { type: string; data?: { path?: { text?: string }; line_number?: number } };
|
|
430
|
+
try {
|
|
431
|
+
event = JSON.parse(line);
|
|
432
|
+
} catch {
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (event.type === "match") {
|
|
437
|
+
matchCount++;
|
|
438
|
+
const filePath = event.data?.path?.text;
|
|
439
|
+
const lineNumber = event.data?.line_number;
|
|
255
440
|
|
|
256
|
-
|
|
257
|
-
|
|
441
|
+
if (filePath && typeof lineNumber === "number") {
|
|
442
|
+
recordFile(filePath);
|
|
443
|
+
recordFileMatch(filePath);
|
|
444
|
+
const block = await formatBlock(filePath, lineNumber);
|
|
445
|
+
outputLines.push(...block);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (matchCount >= effectiveLimit) {
|
|
449
|
+
matchLimitReached = true;
|
|
450
|
+
stopChild(true);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
// Read streams using Bun's ReadableStream API
|
|
258
456
|
const stdoutReader = (child.stdout as ReadableStream<Uint8Array>).getReader();
|
|
259
457
|
const stderrReader = (child.stderr as ReadableStream<Uint8Array>).getReader();
|
|
260
458
|
const decoder = new TextDecoder();
|
|
261
|
-
let
|
|
459
|
+
let stdoutBuffer = "";
|
|
262
460
|
|
|
263
461
|
await Promise.all([
|
|
462
|
+
// Process stdout line by line
|
|
264
463
|
(async () => {
|
|
265
464
|
while (true) {
|
|
266
465
|
const { done, value } = await stdoutReader.read();
|
|
267
466
|
if (done) break;
|
|
268
|
-
|
|
467
|
+
|
|
468
|
+
stdoutBuffer += decoder.decode(value, { stream: true });
|
|
469
|
+
const lines = stdoutBuffer.split("\n");
|
|
470
|
+
// Keep the last incomplete line in the buffer
|
|
471
|
+
stdoutBuffer = lines.pop() ?? "";
|
|
472
|
+
|
|
473
|
+
for (const line of lines) {
|
|
474
|
+
await processLine(line);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
// Process any remaining content
|
|
478
|
+
if (stdoutBuffer.trim()) {
|
|
479
|
+
await processLine(stdoutBuffer);
|
|
269
480
|
}
|
|
270
481
|
})(),
|
|
482
|
+
// Collect stderr
|
|
271
483
|
(async () => {
|
|
272
484
|
while (true) {
|
|
273
485
|
const { done, value } = await stderrReader.read();
|
|
@@ -279,25 +491,16 @@ export function createGrepTool(session: ToolSession): AgentTool<typeof grepSchem
|
|
|
279
491
|
|
|
280
492
|
const exitCode = await child.exited;
|
|
281
493
|
|
|
282
|
-
if (signal) {
|
|
283
|
-
signal.removeEventListener("abort", onAbort);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
494
|
if (aborted) {
|
|
287
495
|
throw new Error("Operation aborted");
|
|
288
496
|
}
|
|
289
497
|
|
|
290
|
-
if (exitCode !== 0 && exitCode !== 1) {
|
|
498
|
+
if (!killedDueToLimit && exitCode !== 0 && exitCode !== 1) {
|
|
291
499
|
const errorMsg = stderr.trim() || `ripgrep exited with code ${exitCode}`;
|
|
292
500
|
throw new Error(errorMsg);
|
|
293
501
|
}
|
|
294
502
|
|
|
295
|
-
|
|
296
|
-
.trim()
|
|
297
|
-
.split("\n")
|
|
298
|
-
.filter((line) => line.length > 0);
|
|
299
|
-
|
|
300
|
-
if (lines.length === 0) {
|
|
503
|
+
if (matchCount === 0) {
|
|
301
504
|
return {
|
|
302
505
|
content: [{ type: "text", text: "No matches found" }],
|
|
303
506
|
details: {
|
|
@@ -311,8 +514,8 @@ export function createGrepTool(session: ToolSession): AgentTool<typeof grepSchem
|
|
|
311
514
|
};
|
|
312
515
|
}
|
|
313
516
|
|
|
314
|
-
// Apply offset and headLimit
|
|
315
|
-
let processedLines =
|
|
517
|
+
// Apply offset and headLimit to output lines
|
|
518
|
+
let processedLines = outputLines;
|
|
316
519
|
if (effectiveOffset > 0) {
|
|
317
520
|
processedLines = processedLines.slice(effectiveOffset);
|
|
318
521
|
}
|
|
@@ -320,278 +523,55 @@ export function createGrepTool(session: ToolSession): AgentTool<typeof grepSchem
|
|
|
320
523
|
processedLines = processedLines.slice(0, headLimit);
|
|
321
524
|
}
|
|
322
525
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
if (effectiveOutputMode === "files_with_matches") {
|
|
343
|
-
for (const line of lines) {
|
|
344
|
-
recordSimpleFile(line);
|
|
345
|
-
}
|
|
346
|
-
fileCount = simpleFiles.size;
|
|
347
|
-
simpleMatchCount = fileCount;
|
|
348
|
-
} else {
|
|
349
|
-
for (const line of lines) {
|
|
350
|
-
const separatorIndex = line.lastIndexOf(":");
|
|
351
|
-
const filePart = separatorIndex === -1 ? line : line.slice(0, separatorIndex);
|
|
352
|
-
const countPart = separatorIndex === -1 ? "" : line.slice(separatorIndex + 1);
|
|
353
|
-
const count = Number.parseInt(countPart, 10);
|
|
354
|
-
recordSimpleFile(filePart);
|
|
355
|
-
if (!Number.isNaN(count)) {
|
|
356
|
-
simpleMatchCount += count;
|
|
357
|
-
recordSimpleFileMatch(filePart, count);
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
fileCount = simpleFiles.size;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
const truncatedByHeadLimit = hasHeadLimit && processedLines.length < lines.length;
|
|
364
|
-
|
|
365
|
-
// For count mode, format as "path:count"
|
|
366
|
-
if (effectiveOutputMode === "count") {
|
|
367
|
-
const formatted = processedLines.map((line) => {
|
|
368
|
-
const separatorIndex = line.lastIndexOf(":");
|
|
369
|
-
const relative = formatPath(separatorIndex === -1 ? line : line.slice(0, separatorIndex));
|
|
370
|
-
const count = separatorIndex === -1 ? "0" : line.slice(separatorIndex + 1);
|
|
371
|
-
return `${relative}:${count}`;
|
|
372
|
-
});
|
|
373
|
-
const output = formatted.join("\n");
|
|
374
|
-
return {
|
|
375
|
-
content: [{ type: "text", text: output }],
|
|
376
|
-
details: {
|
|
377
|
-
scopePath,
|
|
378
|
-
matchCount: simpleMatchCount,
|
|
379
|
-
fileCount,
|
|
380
|
-
files: simpleFileList,
|
|
381
|
-
fileMatches: simpleFileList.map((path) => ({
|
|
382
|
-
path,
|
|
383
|
-
count: simpleFileMatchCounts.get(path) ?? 0,
|
|
384
|
-
})),
|
|
385
|
-
mode: effectiveOutputMode,
|
|
386
|
-
truncated: truncatedByHeadLimit,
|
|
387
|
-
headLimitReached: truncatedByHeadLimit ? headLimit : undefined,
|
|
388
|
-
},
|
|
389
|
-
};
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// For files_with_matches, format paths
|
|
393
|
-
const formatted = processedLines.map((line) => formatPath(line));
|
|
394
|
-
const output = formatted.join("\n");
|
|
395
|
-
return {
|
|
396
|
-
content: [{ type: "text", text: output }],
|
|
397
|
-
details: {
|
|
398
|
-
scopePath,
|
|
399
|
-
matchCount: simpleMatchCount,
|
|
400
|
-
fileCount,
|
|
401
|
-
files: simpleFileList,
|
|
402
|
-
mode: effectiveOutputMode,
|
|
403
|
-
truncated: truncatedByHeadLimit,
|
|
404
|
-
headLimitReached: truncatedByHeadLimit ? headLimit : undefined,
|
|
405
|
-
},
|
|
526
|
+
// Apply byte truncation (no line limit since we already have match limit)
|
|
527
|
+
const rawOutput = processedLines.join("\n");
|
|
528
|
+
const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
|
|
529
|
+
|
|
530
|
+
let output = truncation.content;
|
|
531
|
+
const truncatedByHeadLimit = hasHeadLimit && processedLines.length < outputLines.length;
|
|
532
|
+
const details: GrepToolDetails = {
|
|
533
|
+
scopePath,
|
|
534
|
+
matchCount,
|
|
535
|
+
fileCount: files.size,
|
|
536
|
+
files: fileList,
|
|
537
|
+
fileMatches: fileList.map((path) => ({
|
|
538
|
+
path,
|
|
539
|
+
count: fileMatchCounts.get(path) ?? 0,
|
|
540
|
+
})),
|
|
541
|
+
mode: effectiveOutputMode,
|
|
542
|
+
truncated: matchLimitReached || truncation.truncated || truncatedByHeadLimit,
|
|
543
|
+
headLimitReached: truncatedByHeadLimit ? headLimit : undefined,
|
|
406
544
|
};
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// Content mode - existing JSON processing
|
|
410
|
-
const formatBlock = (filePath: string, lineNumber: number): string[] => {
|
|
411
|
-
const relativePath = formatPath(filePath);
|
|
412
|
-
const lines = getFileLines(filePath);
|
|
413
|
-
if (!lines.length) {
|
|
414
|
-
return [`${relativePath}:${lineNumber}: (unable to read file)`];
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
const block: string[] = [];
|
|
418
|
-
const start = contextValue > 0 ? Math.max(1, lineNumber - contextValue) : lineNumber;
|
|
419
|
-
const end = contextValue > 0 ? Math.min(lines.length, lineNumber + contextValue) : lineNumber;
|
|
420
545
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
const sanitized = lineText.replace(/\r/g, "");
|
|
424
|
-
const isMatchLine = current === lineNumber;
|
|
546
|
+
// Build notices
|
|
547
|
+
const notices: string[] = [];
|
|
425
548
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
if (isMatchLine) {
|
|
432
|
-
block.push(`${relativePath}:${current}: ${truncatedText}`);
|
|
433
|
-
} else {
|
|
434
|
-
block.push(`${relativePath}-${current}- ${truncatedText}`);
|
|
435
|
-
}
|
|
549
|
+
if (matchLimitReached) {
|
|
550
|
+
notices.push(
|
|
551
|
+
`${effectiveLimit} matches limit reached. Use limit=${effectiveLimit * 2} for more, or refine pattern`,
|
|
552
|
+
);
|
|
553
|
+
details.matchLimitReached = effectiveLimit;
|
|
436
554
|
}
|
|
437
555
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
const processLine = (line: string) => {
|
|
442
|
-
if (!line.trim() || matchCount >= effectiveLimit) {
|
|
443
|
-
return;
|
|
556
|
+
if (truncation.truncated) {
|
|
557
|
+
notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);
|
|
558
|
+
details.truncation = truncation;
|
|
444
559
|
}
|
|
445
560
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
} catch {
|
|
450
|
-
return;
|
|
561
|
+
if (linesTruncated) {
|
|
562
|
+
notices.push(`Some lines truncated to ${GREP_MAX_LINE_LENGTH} chars. Use read tool to see full lines`);
|
|
563
|
+
details.linesTruncated = true;
|
|
451
564
|
}
|
|
452
565
|
|
|
453
|
-
if (
|
|
454
|
-
|
|
455
|
-
const filePath = event.data?.path?.text;
|
|
456
|
-
const lineNumber = event.data?.line_number;
|
|
457
|
-
|
|
458
|
-
if (filePath && typeof lineNumber === "number") {
|
|
459
|
-
recordFile(filePath);
|
|
460
|
-
recordFileMatch(filePath);
|
|
461
|
-
outputLines.push(...formatBlock(filePath, lineNumber));
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
if (matchCount >= effectiveLimit) {
|
|
465
|
-
matchLimitReached = true;
|
|
466
|
-
stopChild(true);
|
|
467
|
-
}
|
|
566
|
+
if (notices.length > 0) {
|
|
567
|
+
output += `\n\n[${notices.join(". ")}]`;
|
|
468
568
|
}
|
|
469
|
-
};
|
|
470
|
-
|
|
471
|
-
// Read streams using Bun's ReadableStream API
|
|
472
|
-
const stdoutReader = (child.stdout as ReadableStream<Uint8Array>).getReader();
|
|
473
|
-
const stderrReader = (child.stderr as ReadableStream<Uint8Array>).getReader();
|
|
474
|
-
const decoder = new TextDecoder();
|
|
475
|
-
let stdoutBuffer = "";
|
|
476
|
-
|
|
477
|
-
await Promise.all([
|
|
478
|
-
// Process stdout line by line
|
|
479
|
-
(async () => {
|
|
480
|
-
while (true) {
|
|
481
|
-
const { done, value } = await stdoutReader.read();
|
|
482
|
-
if (done) break;
|
|
483
|
-
|
|
484
|
-
stdoutBuffer += decoder.decode(value, { stream: true });
|
|
485
|
-
const lines = stdoutBuffer.split("\n");
|
|
486
|
-
// Keep the last incomplete line in the buffer
|
|
487
|
-
stdoutBuffer = lines.pop() ?? "";
|
|
488
|
-
|
|
489
|
-
for (const line of lines) {
|
|
490
|
-
processLine(line);
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
// Process any remaining content
|
|
494
|
-
if (stdoutBuffer.trim()) {
|
|
495
|
-
processLine(stdoutBuffer);
|
|
496
|
-
}
|
|
497
|
-
})(),
|
|
498
|
-
// Collect stderr
|
|
499
|
-
(async () => {
|
|
500
|
-
while (true) {
|
|
501
|
-
const { done, value } = await stderrReader.read();
|
|
502
|
-
if (done) break;
|
|
503
|
-
stderr += decoder.decode(value, { stream: true });
|
|
504
|
-
}
|
|
505
|
-
})(),
|
|
506
|
-
]);
|
|
507
569
|
|
|
508
|
-
const exitCode = await child.exited;
|
|
509
|
-
|
|
510
|
-
// Cleanup
|
|
511
|
-
if (signal) {
|
|
512
|
-
signal.removeEventListener("abort", onAbort);
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
if (aborted) {
|
|
516
|
-
throw new Error("Operation aborted");
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
if (!killedDueToLimit && exitCode !== 0 && exitCode !== 1) {
|
|
520
|
-
const errorMsg = stderr.trim() || `ripgrep exited with code ${exitCode}`;
|
|
521
|
-
throw new Error(errorMsg);
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
if (matchCount === 0) {
|
|
525
570
|
return {
|
|
526
|
-
content: [{ type: "text", text:
|
|
527
|
-
details:
|
|
528
|
-
scopePath,
|
|
529
|
-
matchCount: 0,
|
|
530
|
-
fileCount: 0,
|
|
531
|
-
files: [],
|
|
532
|
-
mode: effectiveOutputMode,
|
|
533
|
-
truncated: false,
|
|
534
|
-
},
|
|
571
|
+
content: [{ type: "text", text: output }],
|
|
572
|
+
details: Object.keys(details).length > 0 ? details : undefined,
|
|
535
573
|
};
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
// Apply offset and headLimit to output lines
|
|
539
|
-
let processedLines = outputLines;
|
|
540
|
-
if (effectiveOffset > 0) {
|
|
541
|
-
processedLines = processedLines.slice(effectiveOffset);
|
|
542
|
-
}
|
|
543
|
-
if (hasHeadLimit) {
|
|
544
|
-
processedLines = processedLines.slice(0, headLimit);
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
// Apply byte truncation (no line limit since we already have match limit)
|
|
548
|
-
const rawOutput = processedLines.join("\n");
|
|
549
|
-
const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
|
|
550
|
-
|
|
551
|
-
let output = truncation.content;
|
|
552
|
-
const truncatedByHeadLimit = hasHeadLimit && processedLines.length < outputLines.length;
|
|
553
|
-
const details: GrepToolDetails = {
|
|
554
|
-
scopePath,
|
|
555
|
-
matchCount,
|
|
556
|
-
fileCount: files.size,
|
|
557
|
-
files: fileList,
|
|
558
|
-
fileMatches: fileList.map((path) => ({
|
|
559
|
-
path,
|
|
560
|
-
count: fileMatchCounts.get(path) ?? 0,
|
|
561
|
-
})),
|
|
562
|
-
mode: effectiveOutputMode,
|
|
563
|
-
truncated: matchLimitReached || truncation.truncated || truncatedByHeadLimit,
|
|
564
|
-
headLimitReached: truncatedByHeadLimit ? headLimit : undefined,
|
|
565
|
-
};
|
|
566
|
-
|
|
567
|
-
// Build notices
|
|
568
|
-
const notices: string[] = [];
|
|
569
|
-
|
|
570
|
-
if (matchLimitReached) {
|
|
571
|
-
notices.push(
|
|
572
|
-
`${effectiveLimit} matches limit reached. Use limit=${effectiveLimit * 2} for more, or refine pattern`,
|
|
573
|
-
);
|
|
574
|
-
details.matchLimitReached = effectiveLimit;
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
if (truncation.truncated) {
|
|
578
|
-
notices.push(`${formatSize(DEFAULT_MAX_BYTES)} limit reached`);
|
|
579
|
-
details.truncation = truncation;
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
if (linesTruncated) {
|
|
583
|
-
notices.push(`Some lines truncated to ${GREP_MAX_LINE_LENGTH} chars. Use read tool to see full lines`);
|
|
584
|
-
details.linesTruncated = true;
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
if (notices.length > 0) {
|
|
588
|
-
output += `\n\n[${notices.join(". ")}]`;
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
return {
|
|
592
|
-
content: [{ type: "text", text: output }],
|
|
593
|
-
details: Object.keys(details).length > 0 ? details : undefined,
|
|
594
|
-
};
|
|
574
|
+
});
|
|
595
575
|
},
|
|
596
576
|
};
|
|
597
577
|
}
|
|
@@ -619,7 +599,8 @@ const COLLAPSED_TEXT_LIMIT = PREVIEW_LIMITS.COLLAPSED_LINES * 2;
|
|
|
619
599
|
|
|
620
600
|
export const grepToolRenderer = {
|
|
621
601
|
renderCall(args: GrepRenderArgs, uiTheme: Theme): Component {
|
|
622
|
-
const
|
|
602
|
+
const ui = createToolUIKit(uiTheme);
|
|
603
|
+
const label = ui.title("Grep");
|
|
623
604
|
let text = `${label} ${uiTheme.fg("accent", args.pattern || "?")}`;
|
|
624
605
|
|
|
625
606
|
const meta: string[] = [];
|
|
@@ -637,7 +618,7 @@ export const grepToolRenderer = {
|
|
|
637
618
|
if (args.context !== undefined) meta.push(`context:${args.context}`);
|
|
638
619
|
if (args.limit !== undefined) meta.push(`limit:${args.limit}`);
|
|
639
620
|
|
|
640
|
-
text +=
|
|
621
|
+
text += ui.meta(meta);
|
|
641
622
|
|
|
642
623
|
return new Text(text, 0, 0);
|
|
643
624
|
},
|
|
@@ -647,10 +628,11 @@ export const grepToolRenderer = {
|
|
|
647
628
|
{ expanded }: RenderResultOptions,
|
|
648
629
|
uiTheme: Theme,
|
|
649
630
|
): Component {
|
|
631
|
+
const ui = createToolUIKit(uiTheme);
|
|
650
632
|
const details = result.details;
|
|
651
633
|
|
|
652
634
|
if (details?.error) {
|
|
653
|
-
return new Text(
|
|
635
|
+
return new Text(ui.errorMessage(details.error), 0, 0);
|
|
654
636
|
}
|
|
655
637
|
|
|
656
638
|
const hasDetailedData = details?.matchCount !== undefined || details?.fileCount !== undefined;
|
|
@@ -658,7 +640,7 @@ export const grepToolRenderer = {
|
|
|
658
640
|
if (!hasDetailedData) {
|
|
659
641
|
const textContent = result.content?.find((c) => c.type === "text")?.text;
|
|
660
642
|
if (!textContent || textContent === "No matches found") {
|
|
661
|
-
return new Text(
|
|
643
|
+
return new Text(ui.emptyMessage("No matches found"), 0, 0);
|
|
662
644
|
}
|
|
663
645
|
|
|
664
646
|
const lines = textContent.split("\n").filter((line) => line.trim() !== "");
|
|
@@ -668,8 +650,8 @@ export const grepToolRenderer = {
|
|
|
668
650
|
const hasMore = remaining > 0;
|
|
669
651
|
|
|
670
652
|
const icon = uiTheme.styledSymbol("status.success", "success");
|
|
671
|
-
const summary =
|
|
672
|
-
const expandHint =
|
|
653
|
+
const summary = ui.count("item", lines.length);
|
|
654
|
+
const expandHint = ui.expandHint(expanded, hasMore);
|
|
673
655
|
let text = `${icon} ${uiTheme.fg("dim", summary)}${expandHint}`;
|
|
674
656
|
|
|
675
657
|
for (let i = 0; i < displayLines.length; i++) {
|
|
@@ -679,7 +661,7 @@ export const grepToolRenderer = {
|
|
|
679
661
|
}
|
|
680
662
|
|
|
681
663
|
if (remaining > 0) {
|
|
682
|
-
text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg("muted",
|
|
664
|
+
text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg("muted", ui.moreItems(remaining, "item"))}`;
|
|
683
665
|
}
|
|
684
666
|
|
|
685
667
|
return new Text(text, 0, 0);
|
|
@@ -692,25 +674,25 @@ export const grepToolRenderer = {
|
|
|
692
674
|
const files = details?.files ?? [];
|
|
693
675
|
|
|
694
676
|
if (matchCount === 0) {
|
|
695
|
-
return new Text(
|
|
677
|
+
return new Text(ui.emptyMessage("No matches found"), 0, 0);
|
|
696
678
|
}
|
|
697
679
|
|
|
698
680
|
const icon = uiTheme.styledSymbol("status.success", "success");
|
|
699
681
|
const summaryParts =
|
|
700
682
|
mode === "files_with_matches"
|
|
701
|
-
? [
|
|
702
|
-
: [
|
|
683
|
+
? [ui.count("file", fileCount)]
|
|
684
|
+
: [ui.count("match", matchCount), ui.count("file", fileCount)];
|
|
703
685
|
const summaryText = summaryParts.join(uiTheme.sep.dot);
|
|
704
|
-
const scopeLabel =
|
|
686
|
+
const scopeLabel = ui.scope(details?.scopePath);
|
|
705
687
|
|
|
706
688
|
const fileEntries: Array<{ path: string; count?: number }> = details?.fileMatches?.length
|
|
707
689
|
? details.fileMatches.map((entry) => ({ path: entry.path, count: entry.count }))
|
|
708
690
|
: files.map((path) => ({ path }));
|
|
709
691
|
const maxFiles = expanded ? fileEntries.length : Math.min(fileEntries.length, COLLAPSED_LIST_LIMIT);
|
|
710
692
|
const hasMoreFiles = fileEntries.length > maxFiles;
|
|
711
|
-
const expandHint =
|
|
693
|
+
const expandHint = ui.expandHint(expanded, hasMoreFiles);
|
|
712
694
|
|
|
713
|
-
let text = `${icon} ${uiTheme.fg("dim", summaryText)}${
|
|
695
|
+
let text = `${icon} ${uiTheme.fg("dim", summaryText)}${ui.truncationSuffix(truncated)}${scopeLabel}${expandHint}`;
|
|
714
696
|
|
|
715
697
|
const truncationReasons: string[] = [];
|
|
716
698
|
if (details?.matchLimitReached) {
|
|
@@ -750,7 +732,7 @@ export const grepToolRenderer = {
|
|
|
750
732
|
const moreFilesBranch = hasTruncation ? uiTheme.tree.branch : uiTheme.tree.last;
|
|
751
733
|
text += `\n ${uiTheme.fg("dim", moreFilesBranch)} ${uiTheme.fg(
|
|
752
734
|
"muted",
|
|
753
|
-
|
|
735
|
+
ui.moreItems(fileEntries.length - maxFiles, "file"),
|
|
754
736
|
)}`;
|
|
755
737
|
}
|
|
756
738
|
}
|