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.
@@ -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
+ }