kyawthiha-nextjs-agent-cli 1.0.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/README.md +188 -0
- package/dist/agent/agent.js +340 -0
- package/dist/agent/index.js +6 -0
- package/dist/agent/prompts/agent-prompt.js +527 -0
- package/dist/agent/summarizer.js +97 -0
- package/dist/agent/tools/ast-tools.js +601 -0
- package/dist/agent/tools/code-tools.js +1059 -0
- package/dist/agent/tools/file-tools.js +199 -0
- package/dist/agent/tools/index.js +25 -0
- package/dist/agent/tools/search-tools.js +404 -0
- package/dist/agent/tools/shell-tools.js +334 -0
- package/dist/agent/types.js +4 -0
- package/dist/cli/commands/config.js +61 -0
- package/dist/cli/commands/start.js +236 -0
- package/dist/cli/index.js +12 -0
- package/dist/utils/cred-store.js +70 -0
- package/dist/utils/logger.js +9 -0
- package/package.json +52 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File system tools for the AI Agent
|
|
3
|
+
*/
|
|
4
|
+
import fs from 'fs/promises';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
/**
|
|
7
|
+
* Tool: Read file contents
|
|
8
|
+
*/
|
|
9
|
+
export const readFileTool = {
|
|
10
|
+
name: 'read_file',
|
|
11
|
+
description: 'Read the contents of a file at the specified path. Use this to understand existing code before modifying.',
|
|
12
|
+
parameters: {
|
|
13
|
+
type: 'object',
|
|
14
|
+
properties: {
|
|
15
|
+
path: {
|
|
16
|
+
type: 'string',
|
|
17
|
+
description: 'The file path to read'
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
required: ['path']
|
|
21
|
+
},
|
|
22
|
+
execute: async (input) => {
|
|
23
|
+
try {
|
|
24
|
+
const content = await fs.readFile(input.path, 'utf-8');
|
|
25
|
+
return content;
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
if (error.code === 'ENOENT') {
|
|
29
|
+
return `Error: File not found: ${input.path}`;
|
|
30
|
+
}
|
|
31
|
+
return `Error reading file: ${error.message}`;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Tool: Write file contents
|
|
37
|
+
*/
|
|
38
|
+
export const writeFileTool = {
|
|
39
|
+
name: 'write_file',
|
|
40
|
+
description: 'Write content to a file. Creates the file if it does not exist, or overwrites it if it does. Also creates parent directories if needed. ALWAYS write complete file content.',
|
|
41
|
+
parameters: {
|
|
42
|
+
type: 'object',
|
|
43
|
+
properties: {
|
|
44
|
+
path: {
|
|
45
|
+
type: 'string',
|
|
46
|
+
description: 'The file path to write to'
|
|
47
|
+
},
|
|
48
|
+
content: {
|
|
49
|
+
type: 'string',
|
|
50
|
+
description: 'The COMPLETE content to write to the file'
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
required: ['path', 'content']
|
|
54
|
+
},
|
|
55
|
+
execute: async (input) => {
|
|
56
|
+
try {
|
|
57
|
+
// Ensure parent directory exists
|
|
58
|
+
const dir = path.dirname(input.path);
|
|
59
|
+
await fs.mkdir(dir, { recursive: true });
|
|
60
|
+
await fs.writeFile(input.path, input.content, 'utf-8');
|
|
61
|
+
return `Successfully wrote ${input.content.length} characters to ${input.path}`;
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
return `Error writing file: ${error.message}`;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Tool: List files in directory
|
|
70
|
+
*/
|
|
71
|
+
export const listFilesTool = {
|
|
72
|
+
name: 'list_files',
|
|
73
|
+
description: 'List all files and directories at the specified path. Returns a JSON array of file/directory names.',
|
|
74
|
+
parameters: {
|
|
75
|
+
type: 'object',
|
|
76
|
+
properties: {
|
|
77
|
+
path: {
|
|
78
|
+
type: 'string',
|
|
79
|
+
description: 'The directory path to list'
|
|
80
|
+
},
|
|
81
|
+
recursive: {
|
|
82
|
+
type: 'string',
|
|
83
|
+
description: 'Set to "true" to list files recursively (default: false)',
|
|
84
|
+
enum: ['true', 'false']
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
required: ['path']
|
|
88
|
+
},
|
|
89
|
+
execute: async (input) => {
|
|
90
|
+
try {
|
|
91
|
+
const targetPath = input.path;
|
|
92
|
+
const recursive = input.recursive === 'true';
|
|
93
|
+
const stat = await fs.stat(targetPath).catch(() => null);
|
|
94
|
+
if (!stat) {
|
|
95
|
+
return `Directory does not exist: ${targetPath}`;
|
|
96
|
+
}
|
|
97
|
+
if (!stat.isDirectory()) {
|
|
98
|
+
return `Not a directory: ${targetPath}`;
|
|
99
|
+
}
|
|
100
|
+
if (recursive) {
|
|
101
|
+
const files = [];
|
|
102
|
+
const walk = async (dir) => {
|
|
103
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
104
|
+
for (const entry of entries) {
|
|
105
|
+
// Skip node_modules and hidden directories
|
|
106
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules') {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const fullPath = path.join(dir, entry.name);
|
|
110
|
+
const relativePath = path.relative(targetPath, fullPath);
|
|
111
|
+
if (entry.isDirectory()) {
|
|
112
|
+
files.push(relativePath + '/');
|
|
113
|
+
await walk(fullPath);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
files.push(relativePath);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
await walk(targetPath);
|
|
121
|
+
return JSON.stringify(files, null, 2);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
const entries = await fs.readdir(targetPath, { withFileTypes: true });
|
|
125
|
+
const files = entries
|
|
126
|
+
.filter(e => !e.name.startsWith('.'))
|
|
127
|
+
.map(e => e.isDirectory() ? e.name + '/' : e.name);
|
|
128
|
+
return JSON.stringify(files, null, 2);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
return `Error listing files: ${error.message}`;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
/**
|
|
137
|
+
* Tool: Create directory
|
|
138
|
+
*/
|
|
139
|
+
export const createDirectoryTool = {
|
|
140
|
+
name: 'create_directory',
|
|
141
|
+
description: 'Create a directory at the specified path. Creates parent directories if needed.',
|
|
142
|
+
parameters: {
|
|
143
|
+
type: 'object',
|
|
144
|
+
properties: {
|
|
145
|
+
path: {
|
|
146
|
+
type: 'string',
|
|
147
|
+
description: 'The directory path to create'
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
required: ['path']
|
|
151
|
+
},
|
|
152
|
+
execute: async (input) => {
|
|
153
|
+
try {
|
|
154
|
+
await fs.mkdir(input.path, { recursive: true });
|
|
155
|
+
return `Successfully created directory: ${input.path}`;
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
return `Error creating directory: ${error.message}`;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
/**
|
|
163
|
+
* Tool: Check if file exists
|
|
164
|
+
*/
|
|
165
|
+
export const fileExistsTool = {
|
|
166
|
+
name: 'file_exists',
|
|
167
|
+
description: 'Check if a file or directory exists at the specified path.',
|
|
168
|
+
parameters: {
|
|
169
|
+
type: 'object',
|
|
170
|
+
properties: {
|
|
171
|
+
path: {
|
|
172
|
+
type: 'string',
|
|
173
|
+
description: 'The file or directory path to check'
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
required: ['path']
|
|
177
|
+
},
|
|
178
|
+
execute: async (input) => {
|
|
179
|
+
try {
|
|
180
|
+
const stat = await fs.stat(input.path);
|
|
181
|
+
return `Exists: ${stat.isDirectory() ? 'directory' : 'file'}`;
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
return 'Does not exist';
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
/**
|
|
189
|
+
* Get all file tools
|
|
190
|
+
*/
|
|
191
|
+
export function getFileTools() {
|
|
192
|
+
return [
|
|
193
|
+
readFileTool,
|
|
194
|
+
writeFileTool,
|
|
195
|
+
listFilesTool,
|
|
196
|
+
createDirectoryTool,
|
|
197
|
+
fileExistsTool,
|
|
198
|
+
];
|
|
199
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool registry - exports all tools for the agent
|
|
3
|
+
*/
|
|
4
|
+
export { getFileTools } from './file-tools.js';
|
|
5
|
+
export { getCodeTools } from './code-tools.js';
|
|
6
|
+
export { getSearchTools } from './search-tools.js';
|
|
7
|
+
export { getAstTools } from './ast-tools.js';
|
|
8
|
+
export { getShellTools } from './shell-tools.js';
|
|
9
|
+
import { getFileTools } from './file-tools.js';
|
|
10
|
+
import { getCodeTools } from './code-tools.js';
|
|
11
|
+
import { getSearchTools } from './search-tools.js';
|
|
12
|
+
import { getAstTools } from './ast-tools.js';
|
|
13
|
+
import { getShellTools } from './shell-tools.js';
|
|
14
|
+
/**
|
|
15
|
+
* Get all available tools
|
|
16
|
+
*/
|
|
17
|
+
export function getAllTools() {
|
|
18
|
+
return [
|
|
19
|
+
...getFileTools(),
|
|
20
|
+
...getCodeTools(),
|
|
21
|
+
...getSearchTools(),
|
|
22
|
+
...getAstTools(),
|
|
23
|
+
...getShellTools(),
|
|
24
|
+
];
|
|
25
|
+
}
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search tools for the AI Agent
|
|
3
|
+
* Uses ripgrep for fast pattern search across codebase
|
|
4
|
+
*/
|
|
5
|
+
import { exec } from 'child_process';
|
|
6
|
+
import { promisify } from 'util';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import fs from 'fs/promises';
|
|
9
|
+
const execAsync = promisify(exec);
|
|
10
|
+
/**
|
|
11
|
+
* Check if ripgrep is available
|
|
12
|
+
*/
|
|
13
|
+
async function hasRipgrep() {
|
|
14
|
+
try {
|
|
15
|
+
await execAsync('rg --version');
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Check if fd is available (faster file finder)
|
|
24
|
+
*/
|
|
25
|
+
async function hasFd() {
|
|
26
|
+
try {
|
|
27
|
+
await execAsync('fd --version');
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Tool: Ripgrep Search
|
|
36
|
+
* Fast pattern search using ripgrep with context lines
|
|
37
|
+
*/
|
|
38
|
+
export const ripgrepSearchTool = {
|
|
39
|
+
name: 'ripgrep_search',
|
|
40
|
+
description: `Fast pattern search across codebase using ripgrep.
|
|
41
|
+
Returns matches with file path, line number, and context.
|
|
42
|
+
|
|
43
|
+
Output format:
|
|
44
|
+
FILE:LINE: content
|
|
45
|
+
FILE:LINE: content
|
|
46
|
+
|
|
47
|
+
Best for:
|
|
48
|
+
- Finding function/class definitions
|
|
49
|
+
- Searching for specific text patterns
|
|
50
|
+
- Finding usages of variables/imports`,
|
|
51
|
+
parameters: {
|
|
52
|
+
type: 'object',
|
|
53
|
+
properties: {
|
|
54
|
+
pattern: {
|
|
55
|
+
type: 'string',
|
|
56
|
+
description: 'Search pattern (supports regex)'
|
|
57
|
+
},
|
|
58
|
+
path: {
|
|
59
|
+
type: 'string',
|
|
60
|
+
description: 'Directory or file to search in'
|
|
61
|
+
},
|
|
62
|
+
fileType: {
|
|
63
|
+
type: 'string',
|
|
64
|
+
description: 'File type filter (e.g., "ts", "tsx", "js", "py")'
|
|
65
|
+
},
|
|
66
|
+
contextLines: {
|
|
67
|
+
type: 'string',
|
|
68
|
+
description: 'Number of context lines before/after match (default: 2)'
|
|
69
|
+
},
|
|
70
|
+
caseSensitive: {
|
|
71
|
+
type: 'string',
|
|
72
|
+
description: 'Case sensitive search',
|
|
73
|
+
enum: ['true', 'false']
|
|
74
|
+
},
|
|
75
|
+
wholeWord: {
|
|
76
|
+
type: 'string',
|
|
77
|
+
description: 'Match whole words only',
|
|
78
|
+
enum: ['true', 'false']
|
|
79
|
+
},
|
|
80
|
+
maxResults: {
|
|
81
|
+
type: 'string',
|
|
82
|
+
description: 'Maximum number of results (default: 50)'
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
required: ['pattern', 'path']
|
|
86
|
+
},
|
|
87
|
+
execute: async (input) => {
|
|
88
|
+
try {
|
|
89
|
+
const searchPath = input.path;
|
|
90
|
+
const pattern = input.pattern;
|
|
91
|
+
const fileType = input.fileType;
|
|
92
|
+
const contextLines = parseInt(input.contextLines || '2', 10);
|
|
93
|
+
const caseSensitive = input.caseSensitive === 'true';
|
|
94
|
+
const wholeWord = input.wholeWord === 'true';
|
|
95
|
+
const maxResults = parseInt(input.maxResults || '50', 10);
|
|
96
|
+
// Check if path exists
|
|
97
|
+
try {
|
|
98
|
+
await fs.access(searchPath);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return `Error: Path does not exist: ${searchPath}`;
|
|
102
|
+
}
|
|
103
|
+
// Check if ripgrep is available
|
|
104
|
+
if (!(await hasRipgrep())) {
|
|
105
|
+
return `Error: ripgrep (rg) is not installed.
|
|
106
|
+
Install it:
|
|
107
|
+
- Windows: winget install BurntSushi.ripgrep.MSVC
|
|
108
|
+
- Mac: brew install ripgrep
|
|
109
|
+
- Linux: apt install ripgrep`;
|
|
110
|
+
}
|
|
111
|
+
// Build ripgrep command
|
|
112
|
+
const args = [
|
|
113
|
+
'--line-number', // Show line numbers
|
|
114
|
+
'--no-heading', // Don't group by file
|
|
115
|
+
'--color=never', // No color codes
|
|
116
|
+
`--context=${contextLines}`,
|
|
117
|
+
`--max-count=${maxResults}`,
|
|
118
|
+
];
|
|
119
|
+
// File type filter
|
|
120
|
+
if (fileType) {
|
|
121
|
+
args.push(`--type-add=custom:*.${fileType}`);
|
|
122
|
+
args.push('--type=custom');
|
|
123
|
+
}
|
|
124
|
+
// Case sensitivity
|
|
125
|
+
if (!caseSensitive) {
|
|
126
|
+
args.push('--ignore-case');
|
|
127
|
+
}
|
|
128
|
+
// Whole word
|
|
129
|
+
if (wholeWord) {
|
|
130
|
+
args.push('--word-regexp');
|
|
131
|
+
}
|
|
132
|
+
// Exclude common directories
|
|
133
|
+
args.push('--glob=!node_modules');
|
|
134
|
+
args.push('--glob=!.git');
|
|
135
|
+
args.push('--glob=!dist');
|
|
136
|
+
args.push('--glob=!.next');
|
|
137
|
+
args.push('--glob=!build');
|
|
138
|
+
args.push('--glob=!*.lock');
|
|
139
|
+
// Escape pattern for shell
|
|
140
|
+
const escapedPattern = pattern.replace(/"/g, '\\"');
|
|
141
|
+
const command = `rg ${args.join(' ')} "${escapedPattern}" "${searchPath}"`;
|
|
142
|
+
const shell = process.platform === 'win32' ? 'powershell.exe' : '/bin/bash';
|
|
143
|
+
const { stdout, stderr } = await execAsync(command, {
|
|
144
|
+
shell,
|
|
145
|
+
maxBuffer: 10 * 1024 * 1024, // 10MB
|
|
146
|
+
timeout: 30000
|
|
147
|
+
});
|
|
148
|
+
if (!stdout.trim()) {
|
|
149
|
+
return `No matches found for "${pattern}" in ${searchPath}`;
|
|
150
|
+
}
|
|
151
|
+
// Format output
|
|
152
|
+
const lines = stdout.trim().split('\n');
|
|
153
|
+
const output = [];
|
|
154
|
+
output.push(`Found ${lines.filter(l => !l.startsWith('--')).length} matches for "${pattern}"\n`);
|
|
155
|
+
// Group by file for cleaner output
|
|
156
|
+
let currentFile = '';
|
|
157
|
+
for (const line of lines.slice(0, maxResults * 3)) { // Account for context lines
|
|
158
|
+
if (line.startsWith('--')) {
|
|
159
|
+
output.push('---');
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
const match = line.match(/^(.+?):(\d+)[:-](.*)$/);
|
|
163
|
+
if (match) {
|
|
164
|
+
const [, file, lineNum, content] = match;
|
|
165
|
+
const relPath = path.relative(searchPath, file);
|
|
166
|
+
if (file !== currentFile) {
|
|
167
|
+
currentFile = file;
|
|
168
|
+
output.push(`\n=== ${relPath} ===`);
|
|
169
|
+
}
|
|
170
|
+
output.push(`L${lineNum}: ${content}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return output.join('\n');
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
// ripgrep returns exit code 1 when no matches found
|
|
177
|
+
if (error.code === 1 && !error.stderr) {
|
|
178
|
+
return `No matches found for "${input.pattern}"`;
|
|
179
|
+
}
|
|
180
|
+
return `Search error: ${error.message}`;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
/**
|
|
185
|
+
* Tool: Find Files
|
|
186
|
+
* Find files by name pattern using fd or fallback
|
|
187
|
+
*/
|
|
188
|
+
export const findFilesTool = {
|
|
189
|
+
name: 'find_files',
|
|
190
|
+
description: `Find files by name pattern.
|
|
191
|
+
Uses fd for speed if available, falls back to filesystem walk.
|
|
192
|
+
|
|
193
|
+
Output: List of matching file paths`,
|
|
194
|
+
parameters: {
|
|
195
|
+
type: 'object',
|
|
196
|
+
properties: {
|
|
197
|
+
pattern: {
|
|
198
|
+
type: 'string',
|
|
199
|
+
description: 'File name pattern (supports glob)'
|
|
200
|
+
},
|
|
201
|
+
path: {
|
|
202
|
+
type: 'string',
|
|
203
|
+
description: 'Directory to search in'
|
|
204
|
+
},
|
|
205
|
+
type: {
|
|
206
|
+
type: 'string',
|
|
207
|
+
description: 'Filter by type',
|
|
208
|
+
enum: ['file', 'directory', 'all']
|
|
209
|
+
},
|
|
210
|
+
maxDepth: {
|
|
211
|
+
type: 'string',
|
|
212
|
+
description: 'Maximum directory depth (default: 10)'
|
|
213
|
+
},
|
|
214
|
+
extension: {
|
|
215
|
+
type: 'string',
|
|
216
|
+
description: 'Filter by file extension (e.g., "ts")'
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
required: ['pattern', 'path']
|
|
220
|
+
},
|
|
221
|
+
execute: async (input) => {
|
|
222
|
+
try {
|
|
223
|
+
const searchPath = input.path;
|
|
224
|
+
const pattern = input.pattern;
|
|
225
|
+
const fileType = input.type || 'all';
|
|
226
|
+
const maxDepth = parseInt(input.maxDepth || '10', 10);
|
|
227
|
+
const extension = input.extension;
|
|
228
|
+
// Check if path exists
|
|
229
|
+
try {
|
|
230
|
+
await fs.access(searchPath);
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
return `Error: Path does not exist: ${searchPath}`;
|
|
234
|
+
}
|
|
235
|
+
const shell = process.platform === 'win32' ? 'powershell.exe' : '/bin/bash';
|
|
236
|
+
// Try fd first (faster)
|
|
237
|
+
if (await hasFd()) {
|
|
238
|
+
const args = [
|
|
239
|
+
'--max-depth', maxDepth.toString(),
|
|
240
|
+
'--hidden',
|
|
241
|
+
'--no-ignore-vcs',
|
|
242
|
+
'--exclude', 'node_modules',
|
|
243
|
+
'--exclude', '.git',
|
|
244
|
+
'--exclude', 'dist',
|
|
245
|
+
'--exclude', '.next',
|
|
246
|
+
];
|
|
247
|
+
if (fileType === 'file')
|
|
248
|
+
args.push('--type', 'f');
|
|
249
|
+
if (fileType === 'directory')
|
|
250
|
+
args.push('--type', 'd');
|
|
251
|
+
if (extension)
|
|
252
|
+
args.push('--extension', extension);
|
|
253
|
+
const command = `fd ${args.join(' ')} "${pattern}" "${searchPath}"`;
|
|
254
|
+
try {
|
|
255
|
+
const { stdout } = await execAsync(command, { shell, timeout: 30000 });
|
|
256
|
+
const files = stdout.trim().split('\n').filter(f => f);
|
|
257
|
+
if (files.length === 0) {
|
|
258
|
+
return `No files found matching "${pattern}"`;
|
|
259
|
+
}
|
|
260
|
+
return `Found ${files.length} files:\n${files.map(f => path.relative(searchPath, f)).join('\n')}`;
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
if (error.code === 1) {
|
|
264
|
+
return `No files found matching "${pattern}"`;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
// Fallback: manual filesystem walk
|
|
269
|
+
const results = [];
|
|
270
|
+
const patternLower = pattern.toLowerCase();
|
|
271
|
+
const walk = async (dir, depth) => {
|
|
272
|
+
if (depth > maxDepth)
|
|
273
|
+
return;
|
|
274
|
+
try {
|
|
275
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
276
|
+
for (const entry of entries) {
|
|
277
|
+
if (entry.name === 'node_modules' ||
|
|
278
|
+
entry.name === '.git' ||
|
|
279
|
+
entry.name === 'dist' ||
|
|
280
|
+
entry.name === '.next') {
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
const fullPath = path.join(dir, entry.name);
|
|
284
|
+
const matchesPattern = entry.name.toLowerCase().includes(patternLower);
|
|
285
|
+
const matchesExt = !extension || entry.name.endsWith(`.${extension}`);
|
|
286
|
+
if (entry.isDirectory()) {
|
|
287
|
+
if (fileType !== 'file' && matchesPattern) {
|
|
288
|
+
results.push(path.relative(searchPath, fullPath));
|
|
289
|
+
}
|
|
290
|
+
await walk(fullPath, depth + 1);
|
|
291
|
+
}
|
|
292
|
+
else if (fileType !== 'directory' && matchesPattern && matchesExt) {
|
|
293
|
+
results.push(path.relative(searchPath, fullPath));
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
catch {
|
|
298
|
+
// Skip unreadable directories
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
await walk(searchPath, 0);
|
|
302
|
+
if (results.length === 0) {
|
|
303
|
+
return `No files found matching "${pattern}"`;
|
|
304
|
+
}
|
|
305
|
+
return `Found ${results.length} files:\n${results.slice(0, 100).join('\n')}${results.length > 100 ? `\n... and ${results.length - 100} more` : ''}`;
|
|
306
|
+
}
|
|
307
|
+
catch (error) {
|
|
308
|
+
return `Error finding files: ${error.message}`;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
/**
|
|
313
|
+
* Tool: Grep in File
|
|
314
|
+
* Search within a specific file with line numbers
|
|
315
|
+
*/
|
|
316
|
+
export const grepInFileTool = {
|
|
317
|
+
name: 'grep_in_file',
|
|
318
|
+
description: `Search for a pattern within a specific file.
|
|
319
|
+
Returns matching lines with line numbers.
|
|
320
|
+
|
|
321
|
+
Use this when you know the file and want to find specific content.`,
|
|
322
|
+
parameters: {
|
|
323
|
+
type: 'object',
|
|
324
|
+
properties: {
|
|
325
|
+
pattern: {
|
|
326
|
+
type: 'string',
|
|
327
|
+
description: 'Pattern to search for'
|
|
328
|
+
},
|
|
329
|
+
filePath: {
|
|
330
|
+
type: 'string',
|
|
331
|
+
description: 'Path to the file to search'
|
|
332
|
+
},
|
|
333
|
+
contextLines: {
|
|
334
|
+
type: 'string',
|
|
335
|
+
description: 'Lines of context (default: 2)'
|
|
336
|
+
}
|
|
337
|
+
},
|
|
338
|
+
required: ['pattern', 'filePath']
|
|
339
|
+
},
|
|
340
|
+
execute: async (input) => {
|
|
341
|
+
try {
|
|
342
|
+
const filePath = input.filePath;
|
|
343
|
+
const pattern = input.pattern.toLowerCase();
|
|
344
|
+
const contextLines = parseInt(input.contextLines || '2', 10);
|
|
345
|
+
// Read file
|
|
346
|
+
let content;
|
|
347
|
+
try {
|
|
348
|
+
content = await fs.readFile(filePath, 'utf-8');
|
|
349
|
+
}
|
|
350
|
+
catch (error) {
|
|
351
|
+
if (error.code === 'ENOENT') {
|
|
352
|
+
return `Error: File not found: ${filePath}`;
|
|
353
|
+
}
|
|
354
|
+
return `Error reading file: ${error.message}`;
|
|
355
|
+
}
|
|
356
|
+
const lines = content.split('\n');
|
|
357
|
+
const matches = [];
|
|
358
|
+
const matchedLineNums = new Set();
|
|
359
|
+
// Find matching lines
|
|
360
|
+
lines.forEach((line, idx) => {
|
|
361
|
+
if (line.toLowerCase().includes(pattern)) {
|
|
362
|
+
matchedLineNums.add(idx);
|
|
363
|
+
// Add context lines
|
|
364
|
+
for (let i = Math.max(0, idx - contextLines); i <= Math.min(lines.length - 1, idx + contextLines); i++) {
|
|
365
|
+
if (!matchedLineNums.has(i) || i === idx) {
|
|
366
|
+
matchedLineNums.add(i);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
if (matchedLineNums.size === 0) {
|
|
372
|
+
return `No matches found for "${input.pattern}" in ${path.basename(filePath)}`;
|
|
373
|
+
}
|
|
374
|
+
// Build output
|
|
375
|
+
const output = [];
|
|
376
|
+
output.push(`Found matches in ${path.basename(filePath)}:\n`);
|
|
377
|
+
const sortedNums = Array.from(matchedLineNums).sort((a, b) => a - b);
|
|
378
|
+
let lastNum = -2;
|
|
379
|
+
for (const num of sortedNums) {
|
|
380
|
+
if (num > lastNum + 1) {
|
|
381
|
+
output.push('---');
|
|
382
|
+
}
|
|
383
|
+
const isMatch = lines[num].toLowerCase().includes(pattern);
|
|
384
|
+
const prefix = isMatch ? '>' : ' ';
|
|
385
|
+
output.push(`${prefix} L${num + 1}: ${lines[num]}`);
|
|
386
|
+
lastNum = num;
|
|
387
|
+
}
|
|
388
|
+
return output.join('\n');
|
|
389
|
+
}
|
|
390
|
+
catch (error) {
|
|
391
|
+
return `Error: ${error.message}`;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
/**
|
|
396
|
+
* Get all search tools
|
|
397
|
+
*/
|
|
398
|
+
export function getSearchTools() {
|
|
399
|
+
return [
|
|
400
|
+
ripgrepSearchTool,
|
|
401
|
+
findFilesTool,
|
|
402
|
+
grepInFileTool,
|
|
403
|
+
];
|
|
404
|
+
}
|