@skroyc/librarian 0.1.0 → 0.2.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.
Files changed (76) hide show
  1. package/README.md +4 -16
  2. package/dist/agents/context-schema.d.ts +1 -1
  3. package/dist/agents/context-schema.d.ts.map +1 -1
  4. package/dist/agents/context-schema.js +5 -2
  5. package/dist/agents/context-schema.js.map +1 -1
  6. package/dist/agents/react-agent.d.ts.map +1 -1
  7. package/dist/agents/react-agent.js +36 -27
  8. package/dist/agents/react-agent.js.map +1 -1
  9. package/dist/agents/tool-runtime.d.ts.map +1 -1
  10. package/dist/cli.d.ts +1 -1
  11. package/dist/cli.d.ts.map +1 -1
  12. package/dist/cli.js +53 -49
  13. package/dist/cli.js.map +1 -1
  14. package/dist/config.d.ts +1 -1
  15. package/dist/config.d.ts.map +1 -1
  16. package/dist/config.js +115 -69
  17. package/dist/config.js.map +1 -1
  18. package/dist/index.d.ts +1 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +246 -150
  21. package/dist/index.js.map +1 -1
  22. package/dist/tools/file-finding.tool.d.ts +1 -1
  23. package/dist/tools/file-finding.tool.d.ts.map +1 -1
  24. package/dist/tools/file-finding.tool.js +70 -130
  25. package/dist/tools/file-finding.tool.js.map +1 -1
  26. package/dist/tools/file-listing.tool.d.ts +7 -1
  27. package/dist/tools/file-listing.tool.d.ts.map +1 -1
  28. package/dist/tools/file-listing.tool.js +96 -80
  29. package/dist/tools/file-listing.tool.js.map +1 -1
  30. package/dist/tools/file-reading.tool.d.ts +4 -1
  31. package/dist/tools/file-reading.tool.d.ts.map +1 -1
  32. package/dist/tools/file-reading.tool.js +107 -45
  33. package/dist/tools/file-reading.tool.js.map +1 -1
  34. package/dist/tools/grep-content.tool.d.ts +13 -1
  35. package/dist/tools/grep-content.tool.d.ts.map +1 -1
  36. package/dist/tools/grep-content.tool.js +186 -144
  37. package/dist/tools/grep-content.tool.js.map +1 -1
  38. package/dist/utils/error-utils.d.ts +9 -0
  39. package/dist/utils/error-utils.d.ts.map +1 -0
  40. package/dist/utils/error-utils.js +61 -0
  41. package/dist/utils/error-utils.js.map +1 -0
  42. package/dist/utils/file-utils.d.ts +1 -0
  43. package/dist/utils/file-utils.d.ts.map +1 -1
  44. package/dist/utils/file-utils.js +81 -9
  45. package/dist/utils/file-utils.js.map +1 -1
  46. package/dist/utils/format-utils.d.ts +25 -0
  47. package/dist/utils/format-utils.d.ts.map +1 -0
  48. package/dist/utils/format-utils.js +111 -0
  49. package/dist/utils/format-utils.js.map +1 -0
  50. package/dist/utils/gitignore-service.d.ts +10 -0
  51. package/dist/utils/gitignore-service.d.ts.map +1 -0
  52. package/dist/utils/gitignore-service.js +91 -0
  53. package/dist/utils/gitignore-service.js.map +1 -0
  54. package/dist/utils/logger.d.ts +2 -2
  55. package/dist/utils/logger.d.ts.map +1 -1
  56. package/dist/utils/logger.js +35 -34
  57. package/dist/utils/logger.js.map +1 -1
  58. package/dist/utils/path-utils.js +3 -3
  59. package/dist/utils/path-utils.js.map +1 -1
  60. package/package.json +1 -1
  61. package/src/agents/context-schema.ts +5 -2
  62. package/src/agents/react-agent.ts +667 -641
  63. package/src/agents/tool-runtime.ts +4 -4
  64. package/src/cli.ts +95 -57
  65. package/src/config.ts +192 -90
  66. package/src/index.ts +402 -180
  67. package/src/tools/file-finding.tool.ts +198 -310
  68. package/src/tools/file-listing.tool.ts +245 -202
  69. package/src/tools/file-reading.tool.ts +225 -138
  70. package/src/tools/grep-content.tool.ts +387 -307
  71. package/src/utils/error-utils.ts +95 -0
  72. package/src/utils/file-utils.ts +104 -19
  73. package/src/utils/format-utils.ts +190 -0
  74. package/src/utils/gitignore-service.ts +123 -0
  75. package/src/utils/logger.ts +112 -77
  76. 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 { readdir, stat } from "node:fs/promises";
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
- // Safe file read with encoding detection
9
- async function readFileContent(filePath: string): Promise<string> {
10
- try {
11
- return await Bun.file(filePath).text();
12
- } catch (error: unknown) {
13
- const errorMessage =
14
- error instanceof Error ? error.message : "Unknown error";
15
- throw new Error(`Failed to read file: ${errorMessage}`);
16
- }
17
- }
18
-
19
- // Search for content in a single file
20
- function searchFileContent(content: string, regex: RegExp) {
21
- const lines = content.split("\n");
22
- const matches: Array<{ line: number; text: string; column: number; match: RegExpExecArray | null }> = [];
23
-
24
- for (let i = 0; i < lines.length; i++) {
25
- const line = lines[i];
26
- if (line === undefined) {
27
- continue;
28
- }
29
-
30
- const match = regex.exec(line);
31
-
32
- if (match) {
33
- matches.push({
34
- line: i + 1,
35
- column: match.index + 1,
36
- text: line,
37
- match,
38
- });
39
- regex.lastIndex = 0;
40
- }
41
- }
42
-
43
- return matches;
44
- }
45
-
46
- // Find files matching a pattern in a directory
47
- async function findFiles(
48
- dirPath: string,
49
- pattern: string,
50
- recursive = true,
51
- ): Promise<string[]> {
52
- const foundFiles: string[] = [];
53
-
54
- const entries = await readdir(dirPath, { withFileTypes: true });
55
-
56
- for (const entry of entries) {
57
- const fullPath = path.join(dirPath, entry.name);
58
-
59
- if (entry.isDirectory()) {
60
- if (recursive) {
61
- const subDirFiles = await findFiles(fullPath, pattern, recursive);
62
- foundFiles.push(...subDirFiles);
63
- }
64
- } else if (entry.isFile() && (pattern === "*" || entry.name.includes(pattern.replace(/\*/g, "")))) {
65
- foundFiles.push(fullPath);
66
- }
67
- }
68
-
69
- return foundFiles;
70
- }
71
-
72
- async function validateAndResolvePath(workingDir: string, searchPath: string): Promise<string> {
73
- const resolvedPath = path.resolve(workingDir, searchPath);
74
- const resolvedWorkingDir = path.resolve(workingDir);
75
- const relativePath = path.relative(resolvedWorkingDir, resolvedPath);
76
-
77
- logger.debug("TOOL", "Path validation", {
78
- resolvedPath: resolvedPath.replace(Bun.env.HOME || "", "~"),
79
- resolvedWorkingDir: resolvedWorkingDir.replace(Bun.env.HOME || "", "~"),
80
- relativePath,
81
- validated: !relativePath.startsWith(".."),
82
- });
83
-
84
- if (relativePath.startsWith("..")) {
85
- logger.error(
86
- "PATH",
87
- "Search path escapes working directory sandbox",
88
- undefined,
89
- { searchPath, relativePath },
90
- );
91
- throw new Error(
92
- `Search path "${searchPath}" attempts to escape the working directory sandbox`,
93
- );
94
- }
95
-
96
- const stats = await stat(resolvedPath);
97
- if (!stats.isDirectory()) {
98
- logger.error("TOOL", "Search path is not a directory", undefined, {
99
- searchPath,
100
- });
101
- throw new Error(`Search path "${searchPath}" is not a directory`);
102
- }
103
-
104
- return resolvedPath;
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
- resolvedPath: string,
109
- patterns: string[],
110
- recursive: boolean,
130
+ dirPath: string,
131
+ patterns: string[],
132
+ recursive: boolean,
133
+ includeHidden: boolean
111
134
  ): Promise<string[]> {
112
- let filesToSearch: string[] = [];
113
- for (const pattern of patterns) {
114
- const foundFiles = await findFiles(resolvedPath, pattern, recursive);
115
- filesToSearch = [...filesToSearch, ...foundFiles];
116
- }
117
-
118
- logger.debug("TOOL", "Files to search", {
119
- count: filesToSearch.length,
120
- patterns,
121
- });
122
-
123
- return filesToSearch;
124
- }
125
-
126
- function compileSearchRegex(query: string, regex: boolean, caseSensitive: boolean): RegExp {
127
- const flags = caseSensitive ? "gm" : "gim";
128
-
129
- if (regex) {
130
- try {
131
- const searchRegex = new RegExp(query, flags);
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 formatGrepResults(
189
- results: Array<{ path: string; matches: Array<{ line: number; text: string; column: number; match: RegExpExecArray | null }> }>,
190
- query: string,
191
- ): string {
192
- if (results.length === 0) {
193
- return `No matches found for query "${query}" in the searched files`;
194
- }
195
-
196
- const totalMatches = results.reduce((sum, r) => sum + r.matches.length, 0);
197
- let output = `Found ${totalMatches} matches for query "${query}" in ${results.length} files:\n\n`;
198
- for (const result of results) {
199
- output += `File: ${result.path}\n`;
200
- for (const match of result.matches) {
201
- output += ` Line ${match.line}, Col ${match.column}: ${match.text}\n`;
202
- }
203
- output += "\n";
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 grepContentTool = tool(
211
- async (
212
- {
213
- searchPath = ".",
214
- query,
215
- patterns = ["*"],
216
- caseSensitive = false,
217
- regex = false,
218
- recursive = true,
219
- maxResults = 100,
220
- },
221
- config,
222
- ) => {
223
- const timingId = logger.timingStart("grepContent");
224
-
225
- logger.info("TOOL", "grep_content called", {
226
- searchPath,
227
- queryLength: query.length,
228
- patterns,
229
- caseSensitive,
230
- regex,
231
- recursive,
232
- maxResults,
233
- });
234
-
235
- try {
236
- const workingDir = config?.context?.workingDir;
237
- if (!workingDir) {
238
- throw new Error(
239
- "Context with workingDir is required for file operations",
240
- );
241
- }
242
- logger.debug("TOOL", "Working directory", {
243
- workingDir: workingDir.replace(Bun.env.HOME || "", "~"),
244
- });
245
-
246
- if (!query) {
247
- logger.error("TOOL", "Query parameter missing", undefined, {});
248
- throw new Error('The "query" parameter is required');
249
- }
250
-
251
- const resolvedPath = await validateAndResolvePath(workingDir, searchPath);
252
- const filesToSearch = await findFilesToSearch(resolvedPath, patterns, recursive);
253
-
254
- const searchRegex = compileSearchRegex(query, regex, caseSensitive);
255
- const results = await performGrepSearch(filesToSearch, searchRegex, maxResults);
256
-
257
- logger.timingEnd(timingId, "TOOL", "grep_content completed");
258
- logger.debug("TOOL", "Search completed", {
259
- filesSearched: filesToSearch.length,
260
- filesWithMatches: results.length,
261
- totalMatches: results.reduce((sum, r) => sum + r.matches.length, 0),
262
- });
263
-
264
- return formatGrepResults(results, query);
265
- } catch (error) {
266
- logger.error(
267
- "TOOL",
268
- "grep_content failed",
269
- error instanceof Error ? error : new Error(String(error)),
270
- { searchPath, query },
271
- );
272
- return `Error searching content: ${(error as Error).message}`;
273
- }
274
- },
275
- {
276
- name: "grep_content",
277
- description: `A powerful search tool built on ripgrep
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 grep_content for search tasks.
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") or type parameter (e.g., "js", "py", "rust")
283
- - Pattern syntax: Uses ripgrep (not grep) - literal braces need escaping (use \`interface{}\` to find \`interface{}\` in Go code)
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
- schema: z.object({
286
- searchPath: z.string().describe("The directory path to search in"),
287
- query: z
288
- .string()
289
- .describe(
290
- "The search query - the text or pattern to look for in files",
291
- ),
292
- patterns: z
293
- .array(z.string())
294
- .optional()
295
- .default(["*"])
296
- .describe("File patterns to search in (e.g., ['*.js', '*.ts'])"),
297
- caseSensitive: z
298
- .boolean()
299
- .optional()
300
- .default(false)
301
- .describe(
302
- "Whether the search should be case-sensitive. Defaults to `false`",
303
- ),
304
- regex: z
305
- .boolean()
306
- .optional()
307
- .default(false)
308
- .describe(
309
- "Whether the query should be treated as a regular expression. Defaults to `false`",
310
- ),
311
- recursive: z
312
- .boolean()
313
- .optional()
314
- .default(true)
315
- .describe(
316
- "Whether to search recursively in subdirectories. Defaults to `true`",
317
- ),
318
- maxResults: z
319
- .number()
320
- .optional()
321
- .default(100)
322
- .describe("Maximum number of matches to return. Defaults to 100"),
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
  );