@skroyc/librarian 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -16
- package/dist/agents/context-schema.d.ts +1 -1
- package/dist/agents/context-schema.d.ts.map +1 -1
- package/dist/agents/context-schema.js +5 -2
- package/dist/agents/context-schema.js.map +1 -1
- package/dist/agents/react-agent.d.ts.map +1 -1
- package/dist/agents/react-agent.js +63 -170
- package/dist/agents/react-agent.js.map +1 -1
- package/dist/agents/tool-runtime.d.ts.map +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +53 -49
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +115 -69
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +246 -150
- package/dist/index.js.map +1 -1
- package/dist/tools/file-finding.tool.d.ts +1 -1
- package/dist/tools/file-finding.tool.d.ts.map +1 -1
- package/dist/tools/file-finding.tool.js +70 -130
- package/dist/tools/file-finding.tool.js.map +1 -1
- package/dist/tools/file-listing.tool.d.ts +7 -1
- package/dist/tools/file-listing.tool.d.ts.map +1 -1
- package/dist/tools/file-listing.tool.js +96 -80
- package/dist/tools/file-listing.tool.js.map +1 -1
- package/dist/tools/file-reading.tool.d.ts +4 -1
- package/dist/tools/file-reading.tool.d.ts.map +1 -1
- package/dist/tools/file-reading.tool.js +107 -45
- package/dist/tools/file-reading.tool.js.map +1 -1
- package/dist/tools/grep-content.tool.d.ts +13 -1
- package/dist/tools/grep-content.tool.d.ts.map +1 -1
- package/dist/tools/grep-content.tool.js +186 -144
- package/dist/tools/grep-content.tool.js.map +1 -1
- package/dist/utils/error-utils.d.ts +9 -0
- package/dist/utils/error-utils.d.ts.map +1 -0
- package/dist/utils/error-utils.js +61 -0
- package/dist/utils/error-utils.js.map +1 -0
- package/dist/utils/file-utils.d.ts +1 -0
- package/dist/utils/file-utils.d.ts.map +1 -1
- package/dist/utils/file-utils.js +81 -9
- package/dist/utils/file-utils.js.map +1 -1
- package/dist/utils/format-utils.d.ts +25 -0
- package/dist/utils/format-utils.d.ts.map +1 -0
- package/dist/utils/format-utils.js +111 -0
- package/dist/utils/format-utils.js.map +1 -0
- package/dist/utils/gitignore-service.d.ts +10 -0
- package/dist/utils/gitignore-service.d.ts.map +1 -0
- package/dist/utils/gitignore-service.js +91 -0
- package/dist/utils/gitignore-service.js.map +1 -0
- package/dist/utils/logger.d.ts +2 -2
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +35 -34
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/path-utils.js +3 -3
- package/dist/utils/path-utils.js.map +1 -1
- package/package.json +1 -1
- package/src/agents/context-schema.ts +5 -2
- package/src/agents/react-agent.ts +694 -784
- package/src/agents/tool-runtime.ts +4 -4
- package/src/cli.ts +95 -57
- package/src/config.ts +192 -90
- package/src/index.ts +402 -180
- package/src/tools/file-finding.tool.ts +198 -310
- package/src/tools/file-listing.tool.ts +245 -202
- package/src/tools/file-reading.tool.ts +225 -138
- package/src/tools/grep-content.tool.ts +387 -307
- package/src/utils/error-utils.ts +95 -0
- package/src/utils/file-utils.ts +104 -19
- package/src/utils/format-utils.ts +190 -0
- package/src/utils/gitignore-service.ts +123 -0
- package/src/utils/logger.ts +112 -77
- package/src/utils/path-utils.ts +3 -3
|
@@ -1,325 +1,405 @@
|
|
|
1
|
+
import { stat } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { Glob } from "bun";
|
|
1
4
|
import { tool } from "langchain";
|
|
2
5
|
import { z } from "zod";
|
|
3
|
-
import {
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import { logger } from "../utils/logger.js";
|
|
6
|
+
import { formatToolError, getToolSuggestion } from "../utils/error-utils.js";
|
|
6
7
|
import { isTextFile } from "../utils/file-utils.js";
|
|
8
|
+
import {
|
|
9
|
+
formatSearchResults,
|
|
10
|
+
type SearchMatch,
|
|
11
|
+
} from "../utils/format-utils.js";
|
|
12
|
+
import { GitIgnoreService } from "../utils/gitignore-service.js";
|
|
13
|
+
import { logger } from "../utils/logger.js";
|
|
7
14
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Robust search for context-aware grep.
|
|
17
|
+
* While streaming is good for huge files, contextAfter requires looking ahead or buffering.
|
|
18
|
+
* Since Librarian typically works on source code files (KB to low MBs), loading the file into memory
|
|
19
|
+
* but processing it efficiently is a balanced trade-off for accurate context.
|
|
20
|
+
*/
|
|
21
|
+
async function searchFileWithContext(
|
|
22
|
+
filePath: string,
|
|
23
|
+
regex: RegExp,
|
|
24
|
+
contextBefore = 0,
|
|
25
|
+
contextAfter = 0
|
|
26
|
+
): Promise<SearchMatch[]> {
|
|
27
|
+
const file = Bun.file(filePath);
|
|
28
|
+
const stream = file.stream();
|
|
29
|
+
const reader = stream.getReader();
|
|
30
|
+
const decoder = new TextDecoder();
|
|
31
|
+
|
|
32
|
+
const matches: SearchMatch[] = [];
|
|
33
|
+
const beforeBuffer: string[] = [];
|
|
34
|
+
let currentLineNum = 1;
|
|
35
|
+
let partialLine = "";
|
|
36
|
+
|
|
37
|
+
// Track matches that are still waiting for their contextAfter lines
|
|
38
|
+
const pendingMatches: Array<{
|
|
39
|
+
match: SearchMatch;
|
|
40
|
+
linesRemaining: number;
|
|
41
|
+
}> = [];
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
while (true) {
|
|
45
|
+
const { done, value } = await reader.read();
|
|
46
|
+
if (done) {
|
|
47
|
+
if (partialLine) {
|
|
48
|
+
processLine(partialLine);
|
|
49
|
+
}
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
54
|
+
const lines = (partialLine + chunk).split("\n");
|
|
55
|
+
partialLine = lines.pop() || "";
|
|
56
|
+
|
|
57
|
+
for (const line of lines) {
|
|
58
|
+
processLine(line);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
} finally {
|
|
62
|
+
reader.releaseLock();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function processLine(line: string) {
|
|
66
|
+
// Update pending matches
|
|
67
|
+
for (const pending of pendingMatches) {
|
|
68
|
+
if (pending.linesRemaining > 0) {
|
|
69
|
+
pending.match.context?.after.push(line);
|
|
70
|
+
pending.linesRemaining--;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Remove completed matches from pendingMatches to prevent memory leak
|
|
75
|
+
// This keeps the array small for files with many matches
|
|
76
|
+
for (let i = pendingMatches.length - 1; i >= 0; i--) {
|
|
77
|
+
const pending = pendingMatches[i];
|
|
78
|
+
if (pending && pending.linesRemaining === 0) {
|
|
79
|
+
pendingMatches.splice(i, 1);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Check for new match
|
|
84
|
+
regex.lastIndex = 0;
|
|
85
|
+
const matchExec = regex.exec(line);
|
|
86
|
+
|
|
87
|
+
// We only take the first match on a line for context-aware grep to avoid duplication
|
|
88
|
+
// or complex context merging if there are multiple matches on the same line.
|
|
89
|
+
if (matchExec !== null) {
|
|
90
|
+
const searchMatch: SearchMatch = {
|
|
91
|
+
line: currentLineNum,
|
|
92
|
+
column: matchExec.index + 1,
|
|
93
|
+
text: line,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
if (contextBefore > 0 || contextAfter > 0) {
|
|
97
|
+
const context: { before: string[]; after: string[] } = {
|
|
98
|
+
before: contextBefore > 0 ? [...beforeBuffer] : [],
|
|
99
|
+
after: [],
|
|
100
|
+
};
|
|
101
|
+
searchMatch.context = context;
|
|
102
|
+
|
|
103
|
+
if (contextAfter > 0) {
|
|
104
|
+
pendingMatches.push({
|
|
105
|
+
match: searchMatch,
|
|
106
|
+
linesRemaining: contextAfter,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
matches.push(searchMatch);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Update before buffer
|
|
115
|
+
if (contextBefore > 0) {
|
|
116
|
+
beforeBuffer.push(line);
|
|
117
|
+
if (beforeBuffer.length > contextBefore) {
|
|
118
|
+
beforeBuffer.shift();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
currentLineNum++;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return matches;
|
|
105
126
|
}
|
|
106
127
|
|
|
128
|
+
// Find files matching a pattern in a directory using Bun.Glob
|
|
107
129
|
async function findFilesToSearch(
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
130
|
+
dirPath: string,
|
|
131
|
+
patterns: string[],
|
|
132
|
+
recursive: boolean,
|
|
133
|
+
includeHidden: boolean
|
|
111
134
|
): Promise<string[]> {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
logger.debug("TOOL", "Regex pattern compiled", { query, flags });
|
|
133
|
-
return searchRegex;
|
|
134
|
-
} catch (e) {
|
|
135
|
-
logger.error(
|
|
136
|
-
"TOOL",
|
|
137
|
-
"Invalid regex pattern",
|
|
138
|
-
e instanceof Error ? e : new Error(String(e)),
|
|
139
|
-
{ query },
|
|
140
|
-
);
|
|
141
|
-
throw new Error(`Invalid regex pattern: ${(e as Error).message}`);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
146
|
-
const searchRegex = new RegExp(escapedQuery, flags);
|
|
147
|
-
logger.debug("TOOL", "Escaped query compiled to regex", {
|
|
148
|
-
originalQuery: query,
|
|
149
|
-
flags,
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
return searchRegex;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
async function performGrepSearch(
|
|
156
|
-
filesToSearch: string[],
|
|
157
|
-
searchRegex: RegExp,
|
|
158
|
-
maxResults: number,
|
|
159
|
-
): Promise<Array<{ path: string; matches: Array<{ line: number; text: string; column: number; match: RegExpExecArray | null }> }>> {
|
|
160
|
-
const results: Array<{ path: string; matches: Array<{ line: number; text: string; column: number; match: RegExpExecArray | null }> }> = [];
|
|
161
|
-
let totalMatches = 0;
|
|
162
|
-
|
|
163
|
-
for (const file of filesToSearch) {
|
|
164
|
-
if (totalMatches >= maxResults) {
|
|
165
|
-
break;
|
|
166
|
-
}
|
|
167
|
-
if (await isTextFile(file)) {
|
|
168
|
-
try {
|
|
169
|
-
const content = await readFileContent(file);
|
|
170
|
-
const fileMatches = searchFileContent(content, searchRegex);
|
|
171
|
-
if (fileMatches.length > 0) {
|
|
172
|
-
const limitedMatches = fileMatches.slice(
|
|
173
|
-
0,
|
|
174
|
-
maxResults - totalMatches,
|
|
175
|
-
);
|
|
176
|
-
results.push({ path: file, matches: limitedMatches });
|
|
177
|
-
totalMatches += limitedMatches.length;
|
|
178
|
-
}
|
|
179
|
-
} catch {
|
|
180
|
-
// Silent fail for unreadable files
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
return results;
|
|
135
|
+
const foundFiles: string[] = [];
|
|
136
|
+
for (const pattern of patterns) {
|
|
137
|
+
const effectivePattern =
|
|
138
|
+
recursive && !pattern.includes("/") && !pattern.includes("**")
|
|
139
|
+
? `**/${pattern}`
|
|
140
|
+
: pattern;
|
|
141
|
+
|
|
142
|
+
const glob = new Glob(effectivePattern);
|
|
143
|
+
for await (const file of glob.scan({
|
|
144
|
+
cwd: dirPath,
|
|
145
|
+
onlyFiles: true,
|
|
146
|
+
dot: includeHidden,
|
|
147
|
+
})) {
|
|
148
|
+
const fullPath = path.resolve(dirPath, file);
|
|
149
|
+
if (!foundFiles.includes(fullPath)) {
|
|
150
|
+
foundFiles.push(fullPath);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return foundFiles;
|
|
186
155
|
}
|
|
187
156
|
|
|
188
|
-
function
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
return output;
|
|
157
|
+
function compileSearchRegex(
|
|
158
|
+
query: string,
|
|
159
|
+
regex: boolean,
|
|
160
|
+
caseSensitive: boolean
|
|
161
|
+
): RegExp {
|
|
162
|
+
const flags = caseSensitive ? "gm" : "gim";
|
|
163
|
+
|
|
164
|
+
if (regex) {
|
|
165
|
+
try {
|
|
166
|
+
return new RegExp(query, flags);
|
|
167
|
+
} catch (e) {
|
|
168
|
+
throw new Error(`Invalid regex pattern: ${(e as Error).message}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
173
|
+
return new RegExp(escapedQuery, flags);
|
|
207
174
|
}
|
|
208
175
|
|
|
209
176
|
// Create the modernized tool using the tool() function
|
|
210
|
-
export const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
177
|
+
export const grepTool = tool(
|
|
178
|
+
async (
|
|
179
|
+
{
|
|
180
|
+
searchPath = ".",
|
|
181
|
+
query,
|
|
182
|
+
patterns = ["*"],
|
|
183
|
+
caseSensitive = false,
|
|
184
|
+
regex = false,
|
|
185
|
+
recursive = true,
|
|
186
|
+
maxResults = 100,
|
|
187
|
+
contextBefore = 0,
|
|
188
|
+
contextAfter = 0,
|
|
189
|
+
exclude = [],
|
|
190
|
+
includeHidden = false,
|
|
191
|
+
},
|
|
192
|
+
config
|
|
193
|
+
) => {
|
|
194
|
+
const timingId = logger.timingStart("grep");
|
|
195
|
+
|
|
196
|
+
logger.info("TOOL", "grep called", {
|
|
197
|
+
searchPath,
|
|
198
|
+
query,
|
|
199
|
+
patterns,
|
|
200
|
+
caseSensitive,
|
|
201
|
+
regex,
|
|
202
|
+
recursive,
|
|
203
|
+
maxResults,
|
|
204
|
+
contextBefore,
|
|
205
|
+
contextAfter,
|
|
206
|
+
exclude,
|
|
207
|
+
includeHidden,
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
const workingDir = config?.context?.workingDir;
|
|
212
|
+
if (!workingDir) {
|
|
213
|
+
throw new Error(
|
|
214
|
+
"Context with workingDir is required for file operations"
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (!query) {
|
|
219
|
+
throw new Error('The "query" parameter is required');
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Validate path
|
|
223
|
+
const resolvedPath = path.resolve(workingDir, searchPath);
|
|
224
|
+
const resolvedWorkingDir = path.resolve(workingDir);
|
|
225
|
+
const relativePath = path.relative(resolvedWorkingDir, resolvedPath);
|
|
226
|
+
|
|
227
|
+
if (relativePath.startsWith("..")) {
|
|
228
|
+
throw new Error(
|
|
229
|
+
`Search path "${searchPath}" attempts to escape the working directory sandbox`
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
let stats: import("node:fs").Stats;
|
|
234
|
+
try {
|
|
235
|
+
stats = await stat(resolvedPath);
|
|
236
|
+
} catch (error) {
|
|
237
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
238
|
+
throw new Error(`Search path "${searchPath}" does not exist`);
|
|
239
|
+
}
|
|
240
|
+
throw error;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (!stats.isDirectory()) {
|
|
244
|
+
throw new Error(`Search path "${searchPath}" is not a directory`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Initialize GitIgnoreService
|
|
248
|
+
const gitignore = new GitIgnoreService(workingDir);
|
|
249
|
+
await gitignore.initialize();
|
|
250
|
+
|
|
251
|
+
const filesToSearch = await findFilesToSearch(
|
|
252
|
+
resolvedPath,
|
|
253
|
+
patterns,
|
|
254
|
+
recursive,
|
|
255
|
+
includeHidden
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
const searchRegex = compileSearchRegex(query, regex, caseSensitive);
|
|
259
|
+
const excludeGlobs = exclude.map((pattern) => new Glob(pattern));
|
|
260
|
+
const results: Array<{ path: string; matches: SearchMatch[] }> = [];
|
|
261
|
+
let totalMatches = 0;
|
|
262
|
+
|
|
263
|
+
for (const file of filesToSearch) {
|
|
264
|
+
if (totalMatches >= maxResults) {
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const relativeFileToWorkingDir = path.relative(workingDir, file);
|
|
269
|
+
|
|
270
|
+
// 1. Check if file should be ignored by .gitignore
|
|
271
|
+
if (gitignore.shouldIgnore(file, false)) {
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// 2. Check if file should be excluded by manual exclude patterns
|
|
276
|
+
const isExcluded = excludeGlobs.some((eg) =>
|
|
277
|
+
eg.match(relativeFileToWorkingDir)
|
|
278
|
+
);
|
|
279
|
+
if (isExcluded) {
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (await isTextFile(file)) {
|
|
284
|
+
const fileMatches = await searchFileWithContext(
|
|
285
|
+
file,
|
|
286
|
+
searchRegex,
|
|
287
|
+
contextBefore,
|
|
288
|
+
contextAfter
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
if (fileMatches.length > 0) {
|
|
292
|
+
const remainingSlot = maxResults - totalMatches;
|
|
293
|
+
const limitedMatches = fileMatches.slice(0, remainingSlot);
|
|
294
|
+
|
|
295
|
+
results.push({
|
|
296
|
+
path: path.relative(workingDir, file),
|
|
297
|
+
matches: limitedMatches,
|
|
298
|
+
});
|
|
299
|
+
totalMatches += limitedMatches.length;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
logger.timingEnd(timingId, "TOOL", "grep completed");
|
|
305
|
+
|
|
306
|
+
if (results.length === 0) {
|
|
307
|
+
return `No matches found for query "${query}" in the searched files`;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return formatSearchResults(results);
|
|
311
|
+
} catch (error) {
|
|
312
|
+
logger.error(
|
|
313
|
+
"TOOL",
|
|
314
|
+
"grep failed",
|
|
315
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
316
|
+
{ searchPath, query }
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
return formatToolError({
|
|
320
|
+
operation: "grep",
|
|
321
|
+
path: searchPath,
|
|
322
|
+
cause: error,
|
|
323
|
+
suggestion: getToolSuggestion("grep", searchPath),
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
name: "grep",
|
|
329
|
+
description: `A powerful search tool for finding text patterns in files.
|
|
278
330
|
|
|
279
331
|
Usage:
|
|
280
|
-
- ALWAYS use
|
|
332
|
+
- ALWAYS use grep for search tasks.
|
|
281
333
|
- Supports full regex syntax (e.g., "log.*Error", "function\\s+\\w+")
|
|
282
|
-
- Filter files with glob parameter (e.g., "*.js", "**/*.tsx")
|
|
283
|
-
- Pattern syntax: Uses
|
|
334
|
+
- Filter files with glob parameter (e.g., "*.js", "**/*.tsx")
|
|
335
|
+
- Pattern syntax: Uses JavaScript regex - literal braces need escaping (use \`interface{}\` to find \`interface{}\` in Go code)
|
|
284
336
|
`,
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
337
|
+
schema: z.object({
|
|
338
|
+
searchPath: z.string().describe("The directory path to search in"),
|
|
339
|
+
query: z
|
|
340
|
+
.string()
|
|
341
|
+
.describe(
|
|
342
|
+
"The search query - the text or pattern to look for in files"
|
|
343
|
+
),
|
|
344
|
+
patterns: z
|
|
345
|
+
.array(z.string())
|
|
346
|
+
.optional()
|
|
347
|
+
.default(["*"])
|
|
348
|
+
.describe("File patterns to search in (e.g., ['*.js', '*.ts'])"),
|
|
349
|
+
caseSensitive: z
|
|
350
|
+
.boolean()
|
|
351
|
+
.optional()
|
|
352
|
+
.default(false)
|
|
353
|
+
.describe(
|
|
354
|
+
"Whether the search should be case-sensitive. Defaults to `false`"
|
|
355
|
+
),
|
|
356
|
+
regex: z
|
|
357
|
+
.boolean()
|
|
358
|
+
.optional()
|
|
359
|
+
.default(false)
|
|
360
|
+
.describe(
|
|
361
|
+
"Whether the query should be treated as a regular expression. Defaults to `false`"
|
|
362
|
+
),
|
|
363
|
+
recursive: z
|
|
364
|
+
.boolean()
|
|
365
|
+
.optional()
|
|
366
|
+
.default(true)
|
|
367
|
+
.describe(
|
|
368
|
+
"Whether to search recursively in subdirectories. Defaults to `true`"
|
|
369
|
+
),
|
|
370
|
+
maxResults: z
|
|
371
|
+
.number()
|
|
372
|
+
.optional()
|
|
373
|
+
.default(100)
|
|
374
|
+
.describe("Maximum number of matches to return. Defaults to 100"),
|
|
375
|
+
contextBefore: z
|
|
376
|
+
.number()
|
|
377
|
+
.optional()
|
|
378
|
+
.default(0)
|
|
379
|
+
.describe(
|
|
380
|
+
"Number of lines of context to show before each match. Defaults to 0"
|
|
381
|
+
),
|
|
382
|
+
contextAfter: z
|
|
383
|
+
.number()
|
|
384
|
+
.optional()
|
|
385
|
+
.default(0)
|
|
386
|
+
.describe(
|
|
387
|
+
"Number of lines of context to show after each match. Defaults to 0"
|
|
388
|
+
),
|
|
389
|
+
exclude: z
|
|
390
|
+
.array(z.string())
|
|
391
|
+
.optional()
|
|
392
|
+
.default([])
|
|
393
|
+
.describe(
|
|
394
|
+
"Array of glob patterns to exclude from results (e.g., ['dist/**', 'node_modules/**'])"
|
|
395
|
+
),
|
|
396
|
+
includeHidden: z
|
|
397
|
+
.boolean()
|
|
398
|
+
.optional()
|
|
399
|
+
.default(false)
|
|
400
|
+
.describe(
|
|
401
|
+
"Whether to include hidden files and directories in the search. Defaults to `false`"
|
|
402
|
+
),
|
|
403
|
+
}),
|
|
404
|
+
}
|
|
325
405
|
);
|