lcagent-cli 0.1.5 → 0.1.7

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.
@@ -1,48 +1,308 @@
1
1
  import { readFile, writeFile } from 'node:fs/promises';
2
2
  import { isAbsolute, resolve } from 'node:path';
3
+ import { applyPatch as applyUnifiedPatch, parsePatch } from 'diff';
3
4
  import { z } from 'zod';
5
+ import { applyMatches, findEditMatches, stripTrailingWhitespace } from './editUtils.js';
4
6
  const inputSchema = z.object({
5
- path: z.string().min(1),
6
- oldText: z.string(),
7
- newText: z.string(),
7
+ path: z.string().min(1).optional(),
8
+ filePath: z.string().min(1).optional(),
9
+ file_path: z.string().min(1).optional(),
10
+ patch: z.string().optional(),
11
+ diff: z.string().optional(),
12
+ unifiedDiff: z.string().optional(),
13
+ unified_diff: z.string().optional(),
14
+ oldText: z.string().optional(),
15
+ old_text: z.string().optional(),
16
+ newText: z.string().optional(),
17
+ new_text: z.string().optional(),
18
+ replaceAll: z.boolean().optional(),
19
+ replace_all: z.boolean().optional(),
20
+ occurrence: z.number().int().positive().optional(),
21
+ occurrence_index: z.number().int().positive().optional(),
8
22
  });
9
23
  function resolvePath(cwd, filePath) {
10
24
  return isAbsolute(filePath) ? filePath : resolve(cwd, filePath);
11
25
  }
26
+ function normalizePatchPath(filePath) {
27
+ if (!filePath) {
28
+ return null;
29
+ }
30
+ return filePath.replace(/^(a|b)\//, '');
31
+ }
32
+ function getPathBasename(filePath) {
33
+ const segments = filePath.replace(/\\/g, '/').split('/').filter(Boolean);
34
+ return segments[segments.length - 1] ?? filePath;
35
+ }
36
+ function extractSinglePatch(patchText) {
37
+ const parsed = parsePatch(patchText);
38
+ if (parsed.length === 0) {
39
+ return { error: 'Patch could not be parsed. Provide a valid unified diff.' };
40
+ }
41
+ if (parsed.length > 1) {
42
+ return {
43
+ error: 'Patch mode currently supports exactly one file diff. Split multi-file patches into separate edit_file calls.',
44
+ };
45
+ }
46
+ const patch = parsed[0];
47
+ const targetPath = normalizePatchPath(patch.newFileName) ?? normalizePatchPath(patch.oldFileName);
48
+ return { patch, targetPath };
49
+ }
50
+ function patchTouchedLines(patch) {
51
+ const lines = new Set();
52
+ for (const hunk of patch.hunks) {
53
+ const removedOnly = hunk.lines.every(line => line.startsWith('-'));
54
+ const start = removedOnly ? hunk.oldStart : hunk.newStart;
55
+ const length = removedOnly ? hunk.oldLines : hunk.newLines;
56
+ const safeLength = Math.max(length, 1);
57
+ for (let offset = 0; offset < safeLength; offset += 1) {
58
+ lines.add(start + offset);
59
+ }
60
+ }
61
+ return [...lines].sort((left, right) => left - right);
62
+ }
63
+ function buildPatchMismatchDiagnostic(inputPath, patchPath) {
64
+ return [
65
+ 'Patch target path does not match the requested file path.',
66
+ `Input path: ${inputPath}`,
67
+ `Patch path: ${patchPath}`,
68
+ 'Either omit path and let the patch header decide, or make them match exactly.',
69
+ ].join('\n');
70
+ }
71
+ function applySingleFilePatch(current, patchText) {
72
+ const parsed = extractSinglePatch(patchText);
73
+ if ('error' in parsed) {
74
+ return parsed;
75
+ }
76
+ const updated = applyUnifiedPatch(current, parsed.patch, {
77
+ fuzzFactor: 0,
78
+ });
79
+ if (updated === false) {
80
+ return {
81
+ error: [
82
+ 'Patch could not be applied cleanly.',
83
+ 'The file content may have changed, or the patch context does not match exactly.',
84
+ 'Re-read the file and generate a fresh unified diff.',
85
+ ].join('\n'),
86
+ };
87
+ }
88
+ return {
89
+ updated,
90
+ touchedLines: patchTouchedLines(parsed.patch),
91
+ patchPath: parsed.targetPath,
92
+ };
93
+ }
94
+ function getLineNumberAtIndex(source, index) {
95
+ return source.slice(0, index).split(/\r?\n/).length;
96
+ }
97
+ function buildLinePreview(source, lineNumber) {
98
+ const lines = source.split(/\r?\n/);
99
+ const start = Math.max(lineNumber - 2, 0);
100
+ const end = Math.min(lineNumber + 1, lines.length);
101
+ return lines
102
+ .slice(start, end)
103
+ .map((line, offset) => `${start + offset + 1}: ${line}`)
104
+ .join('\n');
105
+ }
106
+ function buildNotFoundDiagnostic(source, oldText) {
107
+ const firstAnchor = oldText
108
+ .split(/\r?\n/)
109
+ .map(line => line.trim())
110
+ .find(Boolean);
111
+ if (!firstAnchor) {
112
+ return 'oldText was not found in the target file.';
113
+ }
114
+ const lines = source.split(/\r?\n/);
115
+ const anchorLower = firstAnchor.toLowerCase();
116
+ const matchingLines = lines
117
+ .map((line, index) => ({ line, lineNumber: index + 1 }))
118
+ .filter(item => item.line.toLowerCase().includes(anchorLower))
119
+ .slice(0, 3);
120
+ if (matchingLines.length === 0) {
121
+ const whitespaceNormalized = stripTrailingWhitespace(oldText);
122
+ const sourceWithoutTrailingWhitespace = stripTrailingWhitespace(source);
123
+ if (whitespaceNormalized !== oldText &&
124
+ sourceWithoutTrailingWhitespace.includes(whitespaceNormalized)) {
125
+ return [
126
+ 'oldText was not found exactly in the target file.',
127
+ 'A trailing-whitespace-normalized version appears to exist.',
128
+ 'Try reading the file again and resend oldText without trailing spaces.',
129
+ ].join('\n');
130
+ }
131
+ return 'oldText was not found in the target file.';
132
+ }
133
+ return [
134
+ 'oldText was not found exactly in the target file.',
135
+ 'Closest anchor matches:',
136
+ ...matchingLines.map(item => `${item.lineNumber}: ${item.line}`),
137
+ ].join('\n');
138
+ }
12
139
  export const editFileTool = {
13
140
  name: 'edit_file',
14
- description: 'Replace the first occurrence of oldText with newText in a text file.',
141
+ description: 'Edit a file either by replacing text or by applying a single-file unified diff patch.',
15
142
  inputSchema,
16
143
  inputSchemaJson: {
17
144
  type: 'object',
18
145
  properties: {
19
146
  path: { type: 'string' },
147
+ patch: {
148
+ type: 'string',
149
+ description: 'Unified diff patch for a single file. When provided, oldText/newText are not required.',
150
+ },
20
151
  oldText: { type: 'string' },
21
152
  newText: { type: 'string' },
153
+ replaceAll: { type: 'boolean' },
154
+ occurrence: {
155
+ type: 'integer',
156
+ description: '1-based occurrence index to replace when multiple exact matches exist.',
157
+ },
22
158
  },
23
- required: ['path', 'oldText', 'newText'],
24
159
  additionalProperties: false,
25
160
  },
26
161
  isReadOnly: false,
27
162
  async execute(input, context) {
28
- if (context.approvalMode === 'manual') {
163
+ const normalizedPath = input.path ?? input.filePath ?? input.file_path;
164
+ const patchText = input.patch ?? input.diff ?? input.unifiedDiff ?? input.unified_diff;
165
+ const oldText = input.oldText ?? input.old_text;
166
+ const newText = input.newText ?? input.new_text;
167
+ const replaceAll = input.replaceAll ?? input.replace_all ?? false;
168
+ const occurrence = input.occurrence ?? input.occurrence_index;
169
+ if (patchText !== undefined && (oldText !== undefined || newText !== undefined)) {
170
+ return {
171
+ isError: true,
172
+ content: 'Use either patch mode or oldText/newText mode, not both in the same call.',
173
+ };
174
+ }
175
+ if (patchText === undefined && (!normalizedPath || oldText === undefined || newText === undefined)) {
176
+ return {
177
+ isError: true,
178
+ content: 'Missing required fields. Use path + oldText + newText, or provide patch (and optionally path).',
179
+ };
180
+ }
181
+ if (patchText === undefined && oldText !== undefined && oldText.length === 0) {
29
182
  return {
30
183
  isError: true,
31
- content: 'edit_file is blocked in manual approval mode. Switch config approvalMode to auto to enable file edits.',
184
+ content: 'oldText must not be empty.',
32
185
  };
33
186
  }
34
- const absolutePath = resolvePath(context.cwd, input.path);
35
- const current = await readFile(absolutePath, 'utf8');
36
- if (!current.includes(input.oldText)) {
187
+ if (patchText !== undefined && (replaceAll || occurrence !== undefined)) {
37
188
  return {
38
189
  isError: true,
39
- content: 'oldText was not found in the target file.',
190
+ content: 'replaceAll and occurrence are only supported in oldText/newText mode.',
40
191
  };
41
192
  }
42
- const next = current.replace(input.oldText, input.newText);
193
+ if (patchText === undefined && replaceAll && occurrence !== undefined) {
194
+ return {
195
+ isError: true,
196
+ content: 'Use either replaceAll or occurrence, not both.',
197
+ };
198
+ }
199
+ let resolvedPath = normalizedPath;
200
+ if (patchText !== undefined) {
201
+ const parsed = extractSinglePatch(patchText);
202
+ if ('error' in parsed) {
203
+ return {
204
+ isError: true,
205
+ content: parsed.error,
206
+ };
207
+ }
208
+ if (!resolvedPath && parsed.targetPath) {
209
+ resolvedPath = parsed.targetPath;
210
+ }
211
+ if (!resolvedPath) {
212
+ return {
213
+ isError: true,
214
+ content: 'Patch mode requires a path field or a patch header with a target file path.',
215
+ };
216
+ }
217
+ if (parsed.targetPath && normalizePatchPath(resolvedPath) !== normalizePatchPath(parsed.targetPath)) {
218
+ return {
219
+ isError: true,
220
+ content: buildPatchMismatchDiagnostic(resolvedPath, parsed.targetPath),
221
+ };
222
+ }
223
+ }
224
+ const absolutePath = resolvePath(context.cwd, resolvedPath);
225
+ let current;
226
+ try {
227
+ current = await readFile(absolutePath, 'utf8');
228
+ }
229
+ catch (error) {
230
+ return {
231
+ isError: true,
232
+ content: error instanceof Error ? error.message : String(error),
233
+ };
234
+ }
235
+ if (patchText !== undefined) {
236
+ const patchResult = applySingleFilePatch(current, patchText);
237
+ if ('error' in patchResult) {
238
+ return {
239
+ isError: true,
240
+ content: patchResult.error,
241
+ };
242
+ }
243
+ await writeFile(absolutePath, patchResult.updated, 'utf8');
244
+ return {
245
+ content: `Applied patch to ${resolvedPath} touching line${patchResult.touchedLines.length === 1 ? '' : 's'} ${patchResult.touchedLines.join(', ')}.`,
246
+ };
247
+ }
248
+ const requiredOldText = oldText;
249
+ const requiredNewText = newText;
250
+ const matches = findEditMatches(current, requiredOldText);
251
+ if (matches.length === 0) {
252
+ return {
253
+ isError: true,
254
+ content: buildNotFoundDiagnostic(current, requiredOldText),
255
+ };
256
+ }
257
+ let next;
258
+ let replacedCount;
259
+ let replacedLines;
260
+ let matchTypeLabel = matches[0]?.matchType === 'normalized_quotes'
261
+ ? 'normalized quote match'
262
+ : 'exact match';
263
+ if (replaceAll) {
264
+ next = applyMatches(current, requiredOldText, requiredNewText, matches);
265
+ replacedCount = matches.length;
266
+ replacedLines = matches.map(match => getLineNumberAtIndex(current, match.index));
267
+ }
268
+ else if (occurrence !== undefined) {
269
+ if (occurrence > matches.length) {
270
+ return {
271
+ isError: true,
272
+ content: `Requested occurrence ${occurrence}, but only found ${matches.length} exact match(es).`,
273
+ };
274
+ }
275
+ const selected = matches[occurrence - 1];
276
+ next = applyMatches(current, requiredOldText, requiredNewText, [selected]);
277
+ replacedCount = 1;
278
+ replacedLines = [getLineNumberAtIndex(current, selected.index)];
279
+ matchTypeLabel = selected.matchType === 'normalized_quotes'
280
+ ? 'normalized quote match'
281
+ : 'exact match';
282
+ }
283
+ else if (matches.length > 1) {
284
+ const previews = matches.slice(0, 3).map(match => {
285
+ const lineNumber = getLineNumberAtIndex(current, match.index);
286
+ return `${match.matchType === 'normalized_quotes' ? '[normalized-quotes]' : '[exact]'}\n${buildLinePreview(current, lineNumber)}`;
287
+ });
288
+ return {
289
+ isError: true,
290
+ content: [
291
+ `Found ${matches.length} ${matches[0]?.matchType === 'normalized_quotes' ? 'normalized quote' : 'exact'} matches for oldText in ${resolvedPath}.`,
292
+ 'Specify occurrence (1-based) or set replaceAll=true.',
293
+ 'Example matching locations:',
294
+ ...previews,
295
+ ].join('\n---\n'),
296
+ };
297
+ }
298
+ else {
299
+ next = applyMatches(current, requiredOldText, requiredNewText, [matches[0]]);
300
+ replacedCount = 1;
301
+ replacedLines = [getLineNumberAtIndex(current, matches[0].index)];
302
+ }
43
303
  await writeFile(absolutePath, next, 'utf8');
44
304
  return {
45
- content: `Updated ${input.path}`,
305
+ content: `Updated ${resolvedPath} (${replacedCount} replacement${replacedCount === 1 ? '' : 's'}) using ${matchTypeLabel} at line${replacedLines.length === 1 ? '' : 's'} ${replacedLines.join(', ')}.`,
46
306
  };
47
307
  },
48
308
  };
@@ -0,0 +1,15 @@
1
+ export declare const LEFT_SINGLE_CURLY_QUOTE = "\u2018";
2
+ export declare const RIGHT_SINGLE_CURLY_QUOTE = "\u2019";
3
+ export declare const LEFT_DOUBLE_CURLY_QUOTE = "\u201C";
4
+ export declare const RIGHT_DOUBLE_CURLY_QUOTE = "\u201D";
5
+ export type EditMatch = {
6
+ index: number;
7
+ actual: string;
8
+ matchType: 'exact' | 'normalized_quotes';
9
+ };
10
+ export declare function normalizeQuotes(value: string): string;
11
+ export declare function stripTrailingWhitespace(value: string): string;
12
+ export declare function findExactMatchIndices(source: string, target: string): number[];
13
+ export declare function preserveQuoteStyle(requestedOldText: string, actualOldText: string, requestedNewText: string): string;
14
+ export declare function findEditMatches(source: string, requestedOldText: string): EditMatch[];
15
+ export declare function applyMatches(source: string, requestedOldText: string, requestedNewText: string, matches: EditMatch[]): string;
@@ -0,0 +1,142 @@
1
+ export const LEFT_SINGLE_CURLY_QUOTE = '‘';
2
+ export const RIGHT_SINGLE_CURLY_QUOTE = '’';
3
+ export const LEFT_DOUBLE_CURLY_QUOTE = '“';
4
+ export const RIGHT_DOUBLE_CURLY_QUOTE = '”';
5
+ export function normalizeQuotes(value) {
6
+ return value
7
+ .replaceAll(LEFT_SINGLE_CURLY_QUOTE, "'")
8
+ .replaceAll(RIGHT_SINGLE_CURLY_QUOTE, "'")
9
+ .replaceAll(LEFT_DOUBLE_CURLY_QUOTE, '"')
10
+ .replaceAll(RIGHT_DOUBLE_CURLY_QUOTE, '"');
11
+ }
12
+ export function stripTrailingWhitespace(value) {
13
+ const parts = value.split(/(\r\n|\n|\r)/);
14
+ let result = '';
15
+ for (let index = 0; index < parts.length; index += 1) {
16
+ const part = parts[index];
17
+ if (part === undefined) {
18
+ continue;
19
+ }
20
+ result += index % 2 === 0 ? part.replace(/\s+$/, '') : part;
21
+ }
22
+ return result;
23
+ }
24
+ export function findExactMatchIndices(source, target) {
25
+ const indices = [];
26
+ if (!target) {
27
+ return indices;
28
+ }
29
+ let searchStart = 0;
30
+ while (searchStart <= source.length) {
31
+ const index = source.indexOf(target, searchStart);
32
+ if (index === -1) {
33
+ break;
34
+ }
35
+ indices.push(index);
36
+ searchStart = index + Math.max(target.length, 1);
37
+ }
38
+ return indices;
39
+ }
40
+ function isOpeningContext(characters, index) {
41
+ if (index === 0) {
42
+ return true;
43
+ }
44
+ const previous = characters[index - 1];
45
+ return (previous === ' ' ||
46
+ previous === '\t' ||
47
+ previous === '\n' ||
48
+ previous === '\r' ||
49
+ previous === '(' ||
50
+ previous === '[' ||
51
+ previous === '{' ||
52
+ previous === '\u2014' ||
53
+ previous === '\u2013');
54
+ }
55
+ function applyCurlyDoubleQuotes(value) {
56
+ const characters = [...value];
57
+ const result = [];
58
+ for (let index = 0; index < characters.length; index += 1) {
59
+ const character = characters[index];
60
+ if (character === '"') {
61
+ result.push(isOpeningContext(characters, index)
62
+ ? LEFT_DOUBLE_CURLY_QUOTE
63
+ : RIGHT_DOUBLE_CURLY_QUOTE);
64
+ continue;
65
+ }
66
+ result.push(character ?? '');
67
+ }
68
+ return result.join('');
69
+ }
70
+ function applyCurlySingleQuotes(value) {
71
+ const characters = [...value];
72
+ const result = [];
73
+ for (let index = 0; index < characters.length; index += 1) {
74
+ const character = characters[index];
75
+ if (character === "'") {
76
+ const previous = index > 0 ? characters[index - 1] : undefined;
77
+ const next = index < characters.length - 1 ? characters[index + 1] : undefined;
78
+ const previousIsLetter = previous !== undefined && /\p{L}/u.test(previous);
79
+ const nextIsLetter = next !== undefined && /\p{L}/u.test(next);
80
+ if (previousIsLetter && nextIsLetter) {
81
+ result.push(RIGHT_SINGLE_CURLY_QUOTE);
82
+ }
83
+ else {
84
+ result.push(isOpeningContext(characters, index)
85
+ ? LEFT_SINGLE_CURLY_QUOTE
86
+ : RIGHT_SINGLE_CURLY_QUOTE);
87
+ }
88
+ continue;
89
+ }
90
+ result.push(character ?? '');
91
+ }
92
+ return result.join('');
93
+ }
94
+ export function preserveQuoteStyle(requestedOldText, actualOldText, requestedNewText) {
95
+ if (requestedOldText === actualOldText) {
96
+ return requestedNewText;
97
+ }
98
+ const hasDoubleCurlyQuotes = actualOldText.includes(LEFT_DOUBLE_CURLY_QUOTE) ||
99
+ actualOldText.includes(RIGHT_DOUBLE_CURLY_QUOTE);
100
+ const hasSingleCurlyQuotes = actualOldText.includes(LEFT_SINGLE_CURLY_QUOTE) ||
101
+ actualOldText.includes(RIGHT_SINGLE_CURLY_QUOTE);
102
+ if (!hasDoubleCurlyQuotes && !hasSingleCurlyQuotes) {
103
+ return requestedNewText;
104
+ }
105
+ let result = requestedNewText;
106
+ if (hasDoubleCurlyQuotes) {
107
+ result = applyCurlyDoubleQuotes(result);
108
+ }
109
+ if (hasSingleCurlyQuotes) {
110
+ result = applyCurlySingleQuotes(result);
111
+ }
112
+ return result;
113
+ }
114
+ export function findEditMatches(source, requestedOldText) {
115
+ const exactMatches = findExactMatchIndices(source, requestedOldText);
116
+ if (exactMatches.length > 0) {
117
+ return exactMatches.map(index => ({
118
+ index,
119
+ actual: requestedOldText,
120
+ matchType: 'exact',
121
+ }));
122
+ }
123
+ const normalizedOldText = normalizeQuotes(requestedOldText);
124
+ const normalizedSource = normalizeQuotes(source);
125
+ const normalizedIndices = findExactMatchIndices(normalizedSource, normalizedOldText);
126
+ return normalizedIndices.map(index => ({
127
+ index,
128
+ actual: source.slice(index, index + requestedOldText.length),
129
+ matchType: 'normalized_quotes',
130
+ }));
131
+ }
132
+ export function applyMatches(source, requestedOldText, requestedNewText, matches) {
133
+ let next = source;
134
+ for (const match of [...matches].sort((left, right) => right.index - left.index)) {
135
+ const replacement = preserveQuoteStyle(requestedOldText, match.actual, requestedNewText);
136
+ next =
137
+ next.slice(0, match.index) +
138
+ replacement +
139
+ next.slice(match.index + match.actual.length);
140
+ }
141
+ return next;
142
+ }
@@ -1,17 +1,74 @@
1
+ import { buildApprovalRequest } from './permissions.js';
2
+ function finalizeResult(result, context, startedAt, approval, failureStage) {
3
+ return {
4
+ ...result,
5
+ meta: {
6
+ cwd: context.cwd,
7
+ durationMs: Date.now() - startedAt,
8
+ approval,
9
+ failureStage,
10
+ },
11
+ };
12
+ }
1
13
  export async function executeToolCall(toolUse, tools, context) {
14
+ const startedAt = Date.now();
2
15
  const tool = tools.find(item => item.name === toolUse.name);
3
16
  if (!tool) {
4
- return {
17
+ return finalizeResult({
5
18
  isError: true,
6
19
  content: `Unknown tool: ${toolUse.name}`,
7
- };
20
+ }, context, startedAt, {
21
+ mode: context.approvalMode,
22
+ required: false,
23
+ }, 'lookup');
8
24
  }
9
25
  const parsed = tool.inputSchema.safeParse(toolUse.input);
10
26
  if (!parsed.success) {
11
- return {
27
+ return finalizeResult({
12
28
  isError: true,
13
29
  content: parsed.error.message,
14
- };
30
+ }, context, startedAt, {
31
+ mode: context.approvalMode,
32
+ required: false,
33
+ }, 'validation');
34
+ }
35
+ const approvalRequest = buildApprovalRequest(tool, parsed.data, context);
36
+ const approvalMeta = {
37
+ mode: context.approvalMode,
38
+ required: Boolean(approvalRequest),
39
+ risk: approvalRequest?.risk,
40
+ targets: approvalRequest?.targets,
41
+ };
42
+ if (approvalRequest) {
43
+ if (!context.requestApproval) {
44
+ return finalizeResult({
45
+ isError: true,
46
+ content: 'This tool requires manual approval, but no approval handler is available in the current session.',
47
+ }, context, startedAt, {
48
+ ...approvalMeta,
49
+ approved: false,
50
+ }, 'approval');
51
+ }
52
+ const decision = await context.requestApproval(approvalRequest);
53
+ if (!decision.approved) {
54
+ return finalizeResult({
55
+ isError: true,
56
+ content: decision.reason ?? `User denied ${tool.name}.`,
57
+ }, context, startedAt, {
58
+ ...approvalMeta,
59
+ approved: false,
60
+ }, 'approval');
61
+ }
62
+ approvalMeta.approved = true;
63
+ }
64
+ try {
65
+ const result = await tool.execute(parsed.data, context);
66
+ return finalizeResult(result, context, startedAt, approvalMeta);
67
+ }
68
+ catch (error) {
69
+ return finalizeResult({
70
+ isError: true,
71
+ content: error instanceof Error ? error.message : String(error),
72
+ }, context, startedAt, approvalMeta, 'execution');
15
73
  }
16
- return tool.execute(parsed.data, context);
17
74
  }
@@ -1,10 +1,14 @@
1
1
  import { z } from 'zod';
2
2
  import type { ToolDefinition } from './types.js';
3
3
  declare const inputSchema: z.ZodObject<{
4
- pattern: z.ZodString;
4
+ pattern: z.ZodOptional<z.ZodString>;
5
+ query: z.ZodOptional<z.ZodString>;
5
6
  path: z.ZodOptional<z.ZodString>;
7
+ directory: z.ZodOptional<z.ZodString>;
6
8
  maxResults: z.ZodOptional<z.ZodNumber>;
9
+ max_results: z.ZodOptional<z.ZodNumber>;
7
10
  isRegex: z.ZodOptional<z.ZodBoolean>;
11
+ is_regex: z.ZodOptional<z.ZodBoolean>;
8
12
  }, z.core.$strip>;
9
13
  export declare const grepTool: ToolDefinition<z.infer<typeof inputSchema>>;
10
14
  export {};
@@ -2,10 +2,14 @@ import { readdir, readFile } from 'node:fs/promises';
2
2
  import { extname, isAbsolute, join, resolve } from 'node:path';
3
3
  import { z } from 'zod';
4
4
  const inputSchema = z.object({
5
- pattern: z.string().min(1),
5
+ pattern: z.string().min(1).optional(),
6
+ query: z.string().min(1).optional(),
6
7
  path: z.string().optional(),
8
+ directory: z.string().optional(),
7
9
  maxResults: z.number().int().positive().max(200).optional(),
10
+ max_results: z.number().int().positive().max(200).optional(),
8
11
  isRegex: z.boolean().optional(),
12
+ is_regex: z.boolean().optional(),
9
13
  });
10
14
  const textExtensions = new Set([
11
15
  '.ts', '.tsx', '.js', '.jsx', '.json', '.md', '.txt', '.yml', '.yaml', '.css', '.html', '.mjs', '.cjs', '.sh', '.py', '.java', '.go', '.rs'
@@ -44,17 +48,26 @@ export const grepTool = {
44
48
  },
45
49
  isReadOnly: true,
46
50
  async execute(input, context) {
47
- const root = input.path
48
- ? isAbsolute(input.path)
49
- ? input.path
50
- : resolve(context.cwd, input.path)
51
+ const pattern = input.pattern ?? input.query;
52
+ if (!pattern) {
53
+ return {
54
+ isError: true,
55
+ content: 'Missing required field: pattern',
56
+ };
57
+ }
58
+ const searchPath = input.path ?? input.directory;
59
+ const root = searchPath
60
+ ? isAbsolute(searchPath)
61
+ ? searchPath
62
+ : resolve(context.cwd, searchPath)
51
63
  : context.cwd;
52
- const matcher = input.isRegex
53
- ? new RegExp(input.pattern, 'i')
64
+ const isRegex = input.isRegex ?? input.is_regex;
65
+ const matcher = isRegex
66
+ ? new RegExp(pattern, 'i')
54
67
  : null;
55
68
  const files = await walk(root);
56
69
  const results = [];
57
- const maxResults = input.maxResults ?? 50;
70
+ const maxResults = input.maxResults ?? input.max_results ?? 50;
58
71
  for (const filePath of files) {
59
72
  if (!textExtensions.has(extname(filePath))) {
60
73
  continue;
@@ -68,7 +81,7 @@ export const grepTool = {
68
81
  const line = lines[index] ?? '';
69
82
  const matched = matcher
70
83
  ? matcher.test(line)
71
- : line.toLowerCase().includes(input.pattern.toLowerCase());
84
+ : line.toLowerCase().includes(pattern.toLowerCase());
72
85
  if (!matched) {
73
86
  continue;
74
87
  }
@@ -0,0 +1,12 @@
1
+ import type { ToolContext, ToolDefinition } from './types.js';
2
+ export declare const DANGEROUS_FILES: readonly [".gitconfig", ".gitmodules", ".bashrc", ".bash_profile", ".zshrc", ".zprofile", ".profile", ".mcp.json", ".claude.json"];
3
+ export declare const DANGEROUS_DIRECTORIES: readonly [".git", ".vscode", ".idea", ".claude"];
4
+ export type ToolApprovalRequest = {
5
+ toolName: string;
6
+ summary: string;
7
+ reason: string;
8
+ targets: string[];
9
+ risk: 'low' | 'medium' | 'high';
10
+ input: unknown;
11
+ };
12
+ export declare function buildApprovalRequest(tool: ToolDefinition, input: unknown, context: ToolContext): ToolApprovalRequest | null;