@aakrit512/gatekeep 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,1044 @@
1
+ import { execFileSync, execSync } from 'child_process';
2
+ import { createHash } from 'crypto';
3
+ import * as fs from 'fs';
4
+ import * as path from 'path';
5
+ const DEFAULT_IGNORES = [
6
+ '.git',
7
+ 'node_modules',
8
+ 'dist',
9
+ 'build',
10
+ 'coverage',
11
+ '.next',
12
+ '.turbo',
13
+ '.cache',
14
+ ];
15
+ const DEFAULT_RESULT_LIMIT = 100;
16
+ const MAX_RESULT_LIMIT = 1000;
17
+ const DEFAULT_CHUNK_LIMIT = 200;
18
+ const MAX_CHUNK_LIMIT = 1000;
19
+ const SOURCE_EXTENSIONS = new Set([
20
+ '.cjs',
21
+ '.css',
22
+ '.go',
23
+ '.html',
24
+ '.java',
25
+ '.js',
26
+ '.jsx',
27
+ '.mjs',
28
+ '.py',
29
+ '.rs',
30
+ '.sh',
31
+ '.ts',
32
+ '.tsx',
33
+ ]);
34
+ const DEPENDENCY_FILE_PATTERNS = [
35
+ 'package.json',
36
+ 'pnpm-lock.yaml',
37
+ 'package-lock.json',
38
+ 'yarn.lock',
39
+ 'bun.lockb',
40
+ 'Dockerfile',
41
+ 'docker-compose.yml',
42
+ 'docker-compose.yaml',
43
+ 'Cargo.toml',
44
+ 'go.mod',
45
+ 'requirements.txt',
46
+ 'pyproject.toml',
47
+ 'pom.xml',
48
+ 'build.gradle',
49
+ 'build.gradle.kts',
50
+ ];
51
+ const MANIFEST_FILE_PATTERNS = [
52
+ 'package.json',
53
+ 'pnpm-workspace.yaml',
54
+ 'pnpm-workspace.yml',
55
+ 'turbo.json',
56
+ 'tsconfig.json',
57
+ 'tsconfig.*.json',
58
+ 'docker-compose.yml',
59
+ 'docker-compose.yaml',
60
+ 'Dockerfile',
61
+ 'README*',
62
+ '.github/workflows/*',
63
+ 'vite.config.*',
64
+ 'next.config.*',
65
+ 'nuxt.config.*',
66
+ 'astro.config.*',
67
+ 'webpack.config.*',
68
+ 'rollup.config.*',
69
+ 'eslint.config.*',
70
+ '.eslintrc*',
71
+ '.prettierrc*',
72
+ ];
73
+ const ROUTE_METHODS = ['all', 'delete', 'get', 'head', 'options', 'patch', 'post', 'put'];
74
+ function readStringArg(args, key, fallback = '') {
75
+ const value = args[key];
76
+ return typeof value === 'string' ? value : fallback;
77
+ }
78
+ function readNumberArg(args, key, fallback) {
79
+ const value = args[key];
80
+ return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
81
+ }
82
+ function readBooleanArg(args, key, fallback) {
83
+ const value = args[key];
84
+ return typeof value === 'boolean' ? value : fallback;
85
+ }
86
+ function readStringArrayArg(args, key) {
87
+ const value = args[key];
88
+ return Array.isArray(value) ? value.filter((item) => typeof item === 'string') : [];
89
+ }
90
+ function clampInteger(value, min, max) {
91
+ return Math.max(min, Math.min(max, Math.trunc(value)));
92
+ }
93
+ function toPosixPath(filePath) {
94
+ return filePath.split(path.sep).join('/');
95
+ }
96
+ function workspacePath(inputPath) {
97
+ return path.resolve(process.cwd(), inputPath || '.');
98
+ }
99
+ function relativeFromWorkspace(filePath) {
100
+ return toPosixPath(path.relative(process.cwd(), filePath)) || '.';
101
+ }
102
+ function escapeRegExp(value) {
103
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
104
+ }
105
+ function globToRegExp(glob) {
106
+ const normalized = toPosixPath(glob);
107
+ let source = '';
108
+ for (let i = 0; i < normalized.length; i += 1) {
109
+ const char = normalized[i];
110
+ const next = normalized[i + 1];
111
+ if (char === '*' && next === '*') {
112
+ source += '.*';
113
+ i += 1;
114
+ }
115
+ else if (char === '*') {
116
+ source += '[^/]*';
117
+ }
118
+ else if (char === '?') {
119
+ source += '[^/]';
120
+ }
121
+ else {
122
+ source += escapeRegExp(char);
123
+ }
124
+ }
125
+ return new RegExp(`^${source}$`);
126
+ }
127
+ function looksLikeGlob(pattern) {
128
+ return /[*?[\]{}]/.test(pattern);
129
+ }
130
+ function matchesPattern(relativePath, pattern) {
131
+ const normalizedPattern = toPosixPath(pattern);
132
+ if (looksLikeGlob(normalizedPattern)) {
133
+ return globToRegExp(normalizedPattern).test(relativePath);
134
+ }
135
+ return relativePath.includes(normalizedPattern) || path.basename(relativePath).includes(normalizedPattern);
136
+ }
137
+ function matchesIgnorePattern(relativePath, pattern) {
138
+ const normalizedPattern = toPosixPath(pattern);
139
+ if (looksLikeGlob(normalizedPattern)) {
140
+ return globToRegExp(normalizedPattern).test(relativePath);
141
+ }
142
+ return relativePath === normalizedPattern
143
+ || relativePath.startsWith(`${normalizedPattern}/`)
144
+ || relativePath.split('/').includes(normalizedPattern);
145
+ }
146
+ function shouldSkip(relativePath, dirent, options) {
147
+ const baseName = dirent.name;
148
+ if (!options.includeHidden && baseName.startsWith('.')) {
149
+ return true;
150
+ }
151
+ return options.ignorePatterns.some((pattern) => matchesIgnorePattern(relativePath, pattern));
152
+ }
153
+ function walkFiles(startPath, options) {
154
+ const results = [];
155
+ const visit = (currentPath) => {
156
+ if (results.length >= options.maxEntries) {
157
+ return;
158
+ }
159
+ const entries = fs.readdirSync(currentPath, { withFileTypes: true }).sort((a, b) => {
160
+ if (a.isDirectory() !== b.isDirectory()) {
161
+ return a.isDirectory() ? -1 : 1;
162
+ }
163
+ return a.name.localeCompare(b.name);
164
+ });
165
+ for (const entry of entries) {
166
+ const absolutePath = path.join(currentPath, entry.name);
167
+ const relativePath = relativeFromWorkspace(absolutePath);
168
+ if (shouldSkip(relativePath, entry, options)) {
169
+ continue;
170
+ }
171
+ if (entry.isDirectory()) {
172
+ visit(absolutePath);
173
+ }
174
+ else if (entry.isFile()) {
175
+ results.push(absolutePath);
176
+ }
177
+ if (results.length >= options.maxEntries) {
178
+ break;
179
+ }
180
+ }
181
+ };
182
+ visit(startPath);
183
+ return results;
184
+ }
185
+ function inferLanguage(filePath) {
186
+ const extension = path.extname(filePath).toLowerCase();
187
+ const byExtension = {
188
+ '.cjs': 'JavaScript',
189
+ '.css': 'CSS',
190
+ '.go': 'Go',
191
+ '.html': 'HTML',
192
+ '.java': 'Java',
193
+ '.js': 'JavaScript',
194
+ '.json': 'JSON',
195
+ '.jsx': 'JavaScript React',
196
+ '.md': 'Markdown',
197
+ '.mjs': 'JavaScript',
198
+ '.py': 'Python',
199
+ '.rs': 'Rust',
200
+ '.sh': 'Shell',
201
+ '.ts': 'TypeScript',
202
+ '.tsx': 'TypeScript React',
203
+ '.yml': 'YAML',
204
+ '.yaml': 'YAML',
205
+ };
206
+ return byExtension[extension] ?? (extension ? extension.slice(1).toUpperCase() : 'Unknown');
207
+ }
208
+ function isSourceFile(filePath) {
209
+ return SOURCE_EXTENSIONS.has(path.extname(filePath).toLowerCase());
210
+ }
211
+ function isDependencyFile(relativePath) {
212
+ const baseName = path.basename(relativePath);
213
+ return DEPENDENCY_FILE_PATTERNS.some((pattern) => relativePath === pattern || baseName === pattern);
214
+ }
215
+ function isManifestFile(relativePath) {
216
+ return MANIFEST_FILE_PATTERNS.some((pattern) => (matchesPattern(relativePath, pattern) || matchesPattern(path.basename(relativePath), pattern)));
217
+ }
218
+ function readTextFile(filePath) {
219
+ return fs.readFileSync(filePath, 'utf-8');
220
+ }
221
+ function readLines(filePath) {
222
+ return readTextFile(filePath).split(/\r?\n/);
223
+ }
224
+ function extractDefinition(line, lineNumber, filePath) {
225
+ const trimmed = line.trim();
226
+ const exported = /\bexport\b/.test(trimmed);
227
+ const relativePath = relativeFromWorkspace(filePath);
228
+ const patterns = [
229
+ {
230
+ kind: 'class',
231
+ regex: /^(?:export\s+)?(?:default\s+)?(?:abstract\s+)?class\s+([A-Za-z_$][\w$]*)\b/,
232
+ },
233
+ {
234
+ kind: 'function',
235
+ regex: /^(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\b/,
236
+ },
237
+ {
238
+ kind: 'interface',
239
+ regex: /^(?:export\s+)?interface\s+([A-Za-z_$][\w$]*)\b/,
240
+ },
241
+ {
242
+ kind: 'type',
243
+ regex: /^(?:export\s+)?type\s+([A-Za-z_$][\w$]*)\b/,
244
+ },
245
+ {
246
+ kind: 'enum',
247
+ regex: /^(?:export\s+)?(?:const\s+)?enum\s+([A-Za-z_$][\w$]*)\b/,
248
+ },
249
+ {
250
+ kind: 'const',
251
+ regex: /^(?:export\s+)?const\s+([A-Za-z_$][\w$]*)\b/,
252
+ },
253
+ {
254
+ kind: 'let',
255
+ regex: /^(?:export\s+)?let\s+([A-Za-z_$][\w$]*)\b/,
256
+ },
257
+ {
258
+ kind: 'var',
259
+ regex: /^(?:export\s+)?var\s+([A-Za-z_$][\w$]*)\b/,
260
+ },
261
+ ];
262
+ for (const pattern of patterns) {
263
+ const match = trimmed.match(pattern.regex);
264
+ if (match) {
265
+ return {
266
+ kind: pattern.kind,
267
+ name: match[1],
268
+ file: relativePath,
269
+ line: lineNumber,
270
+ text: trimmed,
271
+ exported,
272
+ };
273
+ }
274
+ }
275
+ return null;
276
+ }
277
+ function collectSourceFiles(dirPath, maxEntries = MAX_RESULT_LIMIT * 20) {
278
+ return walkFiles(dirPath, {
279
+ includeHidden: false,
280
+ ignorePatterns: DEFAULT_IGNORES,
281
+ maxEntries,
282
+ }).filter(isSourceFile);
283
+ }
284
+ function collectDefinitions(filePath) {
285
+ return readLines(filePath)
286
+ .map((line, index) => extractDefinition(line, index + 1, filePath))
287
+ .filter((definition) => definition !== null);
288
+ }
289
+ function parseImportsExports(filePath) {
290
+ const lines = readLines(filePath);
291
+ const imports = [];
292
+ const exports = [];
293
+ lines.forEach((line, index) => {
294
+ const trimmed = line.trim();
295
+ const importMatch = trimmed.match(/^import\s+(?:.+?\s+from\s+)?['"]([^'"]+)['"];?$/);
296
+ const exportFromMatch = trimmed.match(/^export\s+.+?\s+from\s+['"]([^'"]+)['"];?$/);
297
+ if (importMatch) {
298
+ imports.push({
299
+ line: index + 1,
300
+ source: importMatch[1],
301
+ specifier: trimmed,
302
+ });
303
+ }
304
+ if (trimmed.startsWith('export ')) {
305
+ exports.push({
306
+ line: index + 1,
307
+ source: exportFromMatch?.[1] ?? null,
308
+ specifier: trimmed,
309
+ });
310
+ }
311
+ });
312
+ return { imports, exports };
313
+ }
314
+ function purposeHint(filePath, lines) {
315
+ const baseName = path.basename(filePath);
316
+ const firstComment = lines
317
+ .map((line) => line.trim())
318
+ .find((line) => line.startsWith('//') || line.startsWith('/*') || line.startsWith('*') || line.startsWith('#'));
319
+ if (firstComment) {
320
+ return firstComment.replace(/^\/\*+|^\*+\/?|^\/\/|^#+/g, '').trim();
321
+ }
322
+ if (/test|spec/.test(baseName)) {
323
+ return 'Test or specification file.';
324
+ }
325
+ if (/config/.test(baseName)) {
326
+ return 'Configuration file.';
327
+ }
328
+ if (/server|handler|route|controller/.test(baseName)) {
329
+ return 'Request or service boundary file.';
330
+ }
331
+ return 'No explicit purpose comment found.';
332
+ }
333
+ function detectSideEffects(lines) {
334
+ const text = lines.join('\n');
335
+ const sideEffects = [];
336
+ if (/\bprocess\.env\b/.test(text)) {
337
+ sideEffects.push('reads environment variables');
338
+ }
339
+ if (/\bexecSync\b|\bspawn\b|\bexec\b/.test(text)) {
340
+ sideEffects.push('runs child processes');
341
+ }
342
+ if (/\bfs\.(write|mkdir|rm|unlink|append|rename)/.test(text)) {
343
+ sideEffects.push('writes filesystem state');
344
+ }
345
+ if (/\bfs\.(read|readdir|stat)/.test(text)) {
346
+ sideEffects.push('reads filesystem state');
347
+ }
348
+ if (/\bfetch\s*\(|\baxios\b|\bhttp\.|\bhttps\./.test(text)) {
349
+ sideEffects.push('performs network I/O');
350
+ }
351
+ return sideEffects;
352
+ }
353
+ function readJsonFile(filePath) {
354
+ return JSON.parse(readTextFile(filePath));
355
+ }
356
+ function packageEntrypoints(packageJsonPath) {
357
+ const packageJson = readJsonFile(packageJsonPath);
358
+ const file = relativeFromWorkspace(packageJsonPath);
359
+ const entries = [];
360
+ for (const field of ['main', 'module', 'types', 'typings']) {
361
+ if (typeof packageJson[field] === 'string') {
362
+ entries.push({
363
+ kind: field,
364
+ file: toPosixPath(path.join(path.dirname(file), packageJson[field])),
365
+ source: file,
366
+ detail: packageJson[field],
367
+ });
368
+ }
369
+ }
370
+ if (typeof packageJson.bin === 'string') {
371
+ entries.push({
372
+ kind: 'bin',
373
+ file: toPosixPath(path.join(path.dirname(file), packageJson.bin)),
374
+ source: file,
375
+ detail: packageJson.bin,
376
+ });
377
+ }
378
+ else if (typeof packageJson.bin === 'object' && packageJson.bin) {
379
+ for (const [command, target] of Object.entries(packageJson.bin)) {
380
+ if (typeof target === 'string') {
381
+ entries.push({
382
+ kind: 'bin',
383
+ file: toPosixPath(path.join(path.dirname(file), target)),
384
+ source: file,
385
+ detail: { command, target },
386
+ });
387
+ }
388
+ }
389
+ }
390
+ const scripts = typeof packageJson.scripts === 'object' && packageJson.scripts
391
+ ? packageJson.scripts
392
+ : {};
393
+ for (const scriptName of ['start', 'dev', 'serve', 'worker', 'cli']) {
394
+ const script = scripts[scriptName];
395
+ if (typeof script === 'string') {
396
+ entries.push({
397
+ kind: `script:${scriptName}`,
398
+ file,
399
+ source: file,
400
+ detail: script,
401
+ });
402
+ }
403
+ }
404
+ return entries;
405
+ }
406
+ function entrypointKind(relativePath) {
407
+ const baseName = path.basename(relativePath).toLowerCase();
408
+ const pathParts = relativePath.toLowerCase().split('/');
409
+ if (/^(cli|command|bin)\.(c|m)?[jt]sx?$/.test(baseName) || pathParts.includes('bin')) {
410
+ return 'cli';
411
+ }
412
+ if (/^(worker|queue|job|processor)\.(c|m)?[jt]sx?$/.test(baseName) || pathParts.includes('workers')) {
413
+ return 'worker';
414
+ }
415
+ if (/^(server|app|main|index)\.(c|m)?[jt]sx?$/.test(baseName)) {
416
+ return 'bootstrap';
417
+ }
418
+ if (/^(page|route|layout)\.[jt]sx?$/.test(baseName) || pathParts.includes('routes') || pathParts.includes('pages')) {
419
+ return 'route_root';
420
+ }
421
+ return null;
422
+ }
423
+ function nextRouteFromFile(relativePath) {
424
+ const normalized = relativePath.replace(/\.(tsx|ts|jsx|js)$/, '');
425
+ const parts = normalized.split('/');
426
+ const appIndex = parts.findIndex((part) => part === 'app' || part === 'pages');
427
+ if (appIndex === -1) {
428
+ return null;
429
+ }
430
+ const routeParts = parts.slice(appIndex + 1);
431
+ const last = routeParts[routeParts.length - 1];
432
+ if (!last || !['page', 'route', 'index'].includes(last)) {
433
+ return null;
434
+ }
435
+ const visibleParts = routeParts
436
+ .slice(0, -1)
437
+ .filter((part) => !part.startsWith('(') && !part.startsWith('_'))
438
+ .map((part) => part.replace(/^\[\.{3}(.+)]$/, ':$1*').replace(/^\[(.+)]$/, ':$1'));
439
+ return `/${visibleParts.join('/')}`.replace(/\/+/g, '/') || '/';
440
+ }
441
+ function routeFilePathHint(relativePath) {
442
+ const route = nextRouteFromFile(relativePath);
443
+ if (route) {
444
+ return route;
445
+ }
446
+ const normalized = relativePath.replace(/\.(tsx|ts|jsx|js)$/, '');
447
+ const parts = normalized.split('/');
448
+ const routeIndex = parts.findIndex((part) => part === 'routes');
449
+ if (routeIndex === -1) {
450
+ return null;
451
+ }
452
+ const routeParts = parts
453
+ .slice(routeIndex + 1)
454
+ .filter((part) => part !== 'index')
455
+ .map((part) => part.replace(/^\[(.+)]$/, ':$1').replace(/^_/, ':'));
456
+ return `/${routeParts.join('/')}`.replace(/\/+/g, '/') || '/';
457
+ }
458
+ function normalizeRoutePath(routePath, controllerPrefix = '') {
459
+ const cleanPrefix = controllerPrefix.replace(/^\/|\/$/g, '');
460
+ const cleanPath = routePath.replace(/^\/|\/$/g, '');
461
+ const joined = [cleanPrefix, cleanPath].filter(Boolean).join('/');
462
+ return `/${joined}`.replace(/\/+/g, '/');
463
+ }
464
+ function parseRoutesInFile(filePath) {
465
+ const relativePath = relativeFromWorkspace(filePath);
466
+ const lines = readLines(filePath);
467
+ const routes = [];
468
+ const conventionRoute = routeFilePathHint(relativePath);
469
+ let controllerPrefix = '';
470
+ if (conventionRoute) {
471
+ routes.push({
472
+ file: relativePath,
473
+ line: null,
474
+ method: null,
475
+ path: conventionRoute,
476
+ source: 'file_convention',
477
+ });
478
+ }
479
+ lines.forEach((line, index) => {
480
+ const trimmed = line.trim();
481
+ const controllerMatch = trimmed.match(/@Controller\(\s*['"`]([^'"`]*)['"`]\s*\)/);
482
+ const decoratorMatch = trimmed.match(/@(Get|Post|Put|Patch|Delete|Options|Head|All)\(\s*(?:['"`]([^'"`]*)['"`])?\s*\)/);
483
+ const expressMatch = trimmed.match(/\b(?:app|router|server)\.(get|post|put|patch|delete|options|head|all)\(\s*['"`]([^'"`]+)['"`]/i);
484
+ const fastifyMatch = trimmed.match(/\b(?:fastify|server|app)\.route\(\s*\{\s*method:\s*['"`]([^'"`]+)['"`]\s*,\s*url:\s*['"`]([^'"`]+)['"`]/i);
485
+ if (controllerMatch) {
486
+ controllerPrefix = controllerMatch[1];
487
+ }
488
+ if (decoratorMatch) {
489
+ routes.push({
490
+ file: relativePath,
491
+ line: index + 1,
492
+ method: decoratorMatch[1].toUpperCase(),
493
+ path: normalizeRoutePath(decoratorMatch[2] ?? '', controllerPrefix),
494
+ source: 'nest_decorator',
495
+ });
496
+ }
497
+ if (expressMatch) {
498
+ routes.push({
499
+ file: relativePath,
500
+ line: index + 1,
501
+ method: expressMatch[1].toUpperCase(),
502
+ path: expressMatch[2],
503
+ source: 'router_call',
504
+ });
505
+ }
506
+ if (fastifyMatch) {
507
+ routes.push({
508
+ file: relativePath,
509
+ line: index + 1,
510
+ method: fastifyMatch[1].toUpperCase(),
511
+ path: fastifyMatch[2],
512
+ source: 'fastify_route',
513
+ });
514
+ }
515
+ });
516
+ return routes;
517
+ }
518
+ function parseGitStatus(rawStatus) {
519
+ const status = {
520
+ staged: [],
521
+ unstaged: [],
522
+ untracked: [],
523
+ renamed: [],
524
+ };
525
+ rawStatus.split(/\r?\n/).filter(Boolean).forEach((line) => {
526
+ const indexStatus = line[0];
527
+ const worktreeStatus = line[1];
528
+ const filePath = line.slice(3);
529
+ if (indexStatus === '?' && worktreeStatus === '?') {
530
+ status.untracked.push(filePath);
531
+ return;
532
+ }
533
+ if (indexStatus && indexStatus !== ' ') {
534
+ status.staged.push(filePath);
535
+ }
536
+ if (worktreeStatus && worktreeStatus !== ' ') {
537
+ status.unstaged.push(filePath);
538
+ }
539
+ if (indexStatus === 'R' || worktreeStatus === 'R') {
540
+ status.renamed.push(filePath);
541
+ }
542
+ });
543
+ return status;
544
+ }
545
+ export function executeTool(name, args) {
546
+ try {
547
+ if (name === 'list_directory') {
548
+ const dirPath = readStringArg(args, 'dir_path', '.');
549
+ const targetPath = workspacePath(dirPath);
550
+ const items = fs.readdirSync(targetPath, { withFileTypes: true });
551
+ return items
552
+ .map((item) => `${item.isDirectory() ? '[DIR]' : '[FILE]'} ${item.name}`)
553
+ .join('\n');
554
+ }
555
+ if (name === 'read_file') {
556
+ const filePath = readStringArg(args, 'file_path');
557
+ const targetPath = workspacePath(filePath);
558
+ return fs.readFileSync(targetPath, 'utf-8');
559
+ }
560
+ if (name === 'read_file_chunk') {
561
+ const filePath = readStringArg(args, 'file_path');
562
+ const targetPath = workspacePath(filePath);
563
+ const lines = fs.readFileSync(targetPath, 'utf-8').split(/\r?\n/);
564
+ const hasStartLine = typeof args.start_line === 'number' && Number.isFinite(args.start_line);
565
+ const limit = clampInteger(readNumberArg(args, 'limit', DEFAULT_CHUNK_LIMIT), 1, MAX_CHUNK_LIMIT);
566
+ const startLine = hasStartLine
567
+ ? clampInteger(readNumberArg(args, 'start_line', 1), 1, Math.max(lines.length, 1))
568
+ : clampInteger(readNumberArg(args, 'offset', 0), 0, Math.max(lines.length - 1, 0)) + 1;
569
+ const endLine = hasStartLine && typeof args.end_line === 'number' && Number.isFinite(args.end_line)
570
+ ? clampInteger(readNumberArg(args, 'end_line', startLine + limit - 1), startLine, lines.length)
571
+ : Math.min(lines.length, startLine + limit - 1);
572
+ const excerpt = lines
573
+ .slice(startLine - 1, endLine)
574
+ .map((line, index) => `${startLine + index}: ${line}`)
575
+ .join('\n');
576
+ return [
577
+ `File: ${relativeFromWorkspace(targetPath)}`,
578
+ `Lines: ${startLine}-${endLine} of ${lines.length}`,
579
+ excerpt,
580
+ ].join('\n');
581
+ }
582
+ if (name === 'search_files') {
583
+ const query = readStringArg(args, 'query');
584
+ const dirPath = readStringArg(args, 'dir_path', '.');
585
+ const includeHidden = readBooleanArg(args, 'include_hidden', false);
586
+ const maxResults = clampInteger(readNumberArg(args, 'max_results', DEFAULT_RESULT_LIMIT), 1, MAX_RESULT_LIMIT);
587
+ const targetPath = workspacePath(dirPath);
588
+ const files = walkFiles(targetPath, {
589
+ includeHidden,
590
+ ignorePatterns: DEFAULT_IGNORES,
591
+ maxEntries: MAX_RESULT_LIMIT * 20,
592
+ });
593
+ const matches = files
594
+ .map(relativeFromWorkspace)
595
+ .filter((relativePath) => {
596
+ if (query.startsWith('.') && !looksLikeGlob(query)) {
597
+ return path.extname(relativePath) === query;
598
+ }
599
+ return matchesPattern(relativePath, query);
600
+ })
601
+ .slice(0, maxResults);
602
+ return matches.length > 0 ? matches.join('\n') : 'No matching files found.';
603
+ }
604
+ if (name === 'grep_code') {
605
+ const pattern = readStringArg(args, 'pattern');
606
+ const dirPath = readStringArg(args, 'dir_path', '.');
607
+ const includeGlobs = readStringArrayArg(args, 'include_globs');
608
+ const excludeGlobs = readStringArrayArg(args, 'exclude_globs');
609
+ const caseSensitive = readBooleanArg(args, 'case_sensitive', true);
610
+ const useRegex = readBooleanArg(args, 'regex', false);
611
+ const maxResults = clampInteger(readNumberArg(args, 'max_results', DEFAULT_RESULT_LIMIT), 1, MAX_RESULT_LIMIT);
612
+ const flags = caseSensitive ? 'g' : 'gi';
613
+ const matcher = useRegex
614
+ ? new RegExp(pattern, flags)
615
+ : new RegExp(escapeRegExp(pattern), flags);
616
+ const targetPath = workspacePath(dirPath);
617
+ const files = walkFiles(targetPath, {
618
+ includeHidden: false,
619
+ ignorePatterns: [...DEFAULT_IGNORES, ...excludeGlobs],
620
+ maxEntries: MAX_RESULT_LIMIT * 20,
621
+ });
622
+ const results = [];
623
+ for (const file of files) {
624
+ const relativePath = relativeFromWorkspace(file);
625
+ if (includeGlobs.length > 0 && !includeGlobs.some((glob) => matchesPattern(relativePath, glob))) {
626
+ continue;
627
+ }
628
+ const stat = fs.statSync(file);
629
+ if (stat.size > 2_000_000) {
630
+ continue;
631
+ }
632
+ const lines = fs.readFileSync(file, 'utf-8').split(/\r?\n/);
633
+ for (let index = 0; index < lines.length; index += 1) {
634
+ matcher.lastIndex = 0;
635
+ if (matcher.test(lines[index])) {
636
+ results.push(`${relativePath}:${index + 1}: ${lines[index]}`);
637
+ }
638
+ if (results.length >= maxResults) {
639
+ break;
640
+ }
641
+ }
642
+ if (results.length >= maxResults) {
643
+ break;
644
+ }
645
+ }
646
+ return results.length > 0 ? results.join('\n') : 'No matches found.';
647
+ }
648
+ if (name === 'get_file_metadata') {
649
+ const filePath = readStringArg(args, 'file_path');
650
+ const targetPath = workspacePath(filePath);
651
+ const content = fs.readFileSync(targetPath);
652
+ const stat = fs.statSync(targetPath);
653
+ const text = content.toString('utf-8');
654
+ const lineCount = text.length === 0 ? 0 : text.split(/\r?\n/).length;
655
+ const checksum = createHash('sha256').update(content).digest('hex');
656
+ return [
657
+ `Path: ${relativeFromWorkspace(targetPath)}`,
658
+ `Size: ${stat.size} bytes`,
659
+ `Modified: ${stat.mtime.toISOString()}`,
660
+ `Lines: ${lineCount}`,
661
+ `Language: ${inferLanguage(targetPath)}`,
662
+ `SHA-256: ${checksum}`,
663
+ ].join('\n');
664
+ }
665
+ if (name === 'get_project_tree') {
666
+ const dirPath = readStringArg(args, 'dir_path', '.');
667
+ const depth = clampInteger(readNumberArg(args, 'depth', 3), 0, 20);
668
+ const includeHidden = readBooleanArg(args, 'include_hidden', false);
669
+ const maxEntries = clampInteger(readNumberArg(args, 'max_entries', 500), 1, 5000);
670
+ const ignorePatterns = [...DEFAULT_IGNORES, ...readStringArrayArg(args, 'ignore')];
671
+ const targetPath = workspacePath(dirPath);
672
+ const output = [relativeFromWorkspace(targetPath)];
673
+ let entryCount = 0;
674
+ let truncated = false;
675
+ const visit = (currentPath, currentDepth, prefix) => {
676
+ if (currentDepth >= depth || truncated) {
677
+ return;
678
+ }
679
+ const entries = fs.readdirSync(currentPath, { withFileTypes: true })
680
+ .filter((entry) => !shouldSkip(relativeFromWorkspace(path.join(currentPath, entry.name)), entry, {
681
+ includeHidden,
682
+ ignorePatterns,
683
+ maxEntries,
684
+ }))
685
+ .sort((a, b) => {
686
+ if (a.isDirectory() !== b.isDirectory()) {
687
+ return a.isDirectory() ? -1 : 1;
688
+ }
689
+ return a.name.localeCompare(b.name);
690
+ });
691
+ entries.forEach((entry, index) => {
692
+ if (entryCount >= maxEntries) {
693
+ truncated = true;
694
+ return;
695
+ }
696
+ const isLast = index === entries.length - 1;
697
+ const connector = isLast ? '`-- ' : '|-- ';
698
+ const childPrefix = `${prefix}${isLast ? ' ' : '| '}`;
699
+ const absolutePath = path.join(currentPath, entry.name);
700
+ const displayName = entry.isDirectory() ? `${entry.name}/` : entry.name;
701
+ output.push(`${prefix}${connector}${displayName}`);
702
+ entryCount += 1;
703
+ if (entry.isDirectory()) {
704
+ visit(absolutePath, currentDepth + 1, childPrefix);
705
+ }
706
+ });
707
+ };
708
+ visit(targetPath, 0, '');
709
+ if (truncated) {
710
+ output.push(`...truncated after ${maxEntries} entries`);
711
+ }
712
+ return output.join('\n');
713
+ }
714
+ if (name === 'find_symbol') {
715
+ const symbol = readStringArg(args, 'symbol');
716
+ const kind = readStringArg(args, 'kind');
717
+ const dirPath = readStringArg(args, 'dir_path', '.');
718
+ const maxResults = clampInteger(readNumberArg(args, 'max_results', 50), 1, MAX_RESULT_LIMIT);
719
+ const targetPath = workspacePath(dirPath);
720
+ const files = collectSourceFiles(targetPath);
721
+ const matches = [];
722
+ for (const file of files) {
723
+ const definitions = collectDefinitions(file).filter((definition) => {
724
+ const kindMatches = kind ? definition.kind === kind : true;
725
+ return definition.name === symbol && kindMatches;
726
+ });
727
+ matches.push(...definitions);
728
+ if (matches.length >= maxResults) {
729
+ break;
730
+ }
731
+ }
732
+ return matches.length > 0
733
+ ? JSON.stringify(matches.slice(0, maxResults), null, 2)
734
+ : `No definitions found for symbol "${symbol}".`;
735
+ }
736
+ if (name === 'find_references') {
737
+ const symbol = readStringArg(args, 'symbol');
738
+ const dirPath = readStringArg(args, 'dir_path', '.');
739
+ const includeDefinitions = readBooleanArg(args, 'include_definitions', false);
740
+ const maxResults = clampInteger(readNumberArg(args, 'max_results', DEFAULT_RESULT_LIMIT), 1, MAX_RESULT_LIMIT);
741
+ const targetPath = workspacePath(dirPath);
742
+ const files = collectSourceFiles(targetPath);
743
+ const symbolRegex = new RegExp(`\\b${escapeRegExp(symbol)}\\b`);
744
+ const references = [];
745
+ for (const file of files) {
746
+ const lines = readLines(file);
747
+ for (let index = 0; index < lines.length; index += 1) {
748
+ const line = lines[index];
749
+ if (!symbolRegex.test(line)) {
750
+ continue;
751
+ }
752
+ const definition = extractDefinition(line, index + 1, file);
753
+ if (!includeDefinitions && definition?.name === symbol) {
754
+ continue;
755
+ }
756
+ references.push({
757
+ file: relativeFromWorkspace(file),
758
+ line: index + 1,
759
+ kind: /\b[A-Za-z_$][\w$]*\s*\(/.test(line) && line.includes(symbol)
760
+ ? 'call_or_usage'
761
+ : 'usage',
762
+ text: line.trim(),
763
+ });
764
+ if (references.length >= maxResults) {
765
+ break;
766
+ }
767
+ }
768
+ if (references.length >= maxResults) {
769
+ break;
770
+ }
771
+ }
772
+ return references.length > 0
773
+ ? JSON.stringify(references, null, 2)
774
+ : `No references found for symbol "${symbol}".`;
775
+ }
776
+ if (name === 'get_imports_exports') {
777
+ const filePath = readStringArg(args, 'file_path');
778
+ const targetPath = workspacePath(filePath);
779
+ const moduleShape = parseImportsExports(targetPath);
780
+ return JSON.stringify({
781
+ file: relativeFromWorkspace(targetPath),
782
+ imports: moduleShape.imports,
783
+ exports: moduleShape.exports,
784
+ }, null, 2);
785
+ }
786
+ if (name === 'summarize_file') {
787
+ const filePath = readStringArg(args, 'file_path');
788
+ const maxSymbols = clampInteger(readNumberArg(args, 'max_symbols', 30), 1, MAX_RESULT_LIMIT);
789
+ const targetPath = workspacePath(filePath);
790
+ const lines = readLines(targetPath);
791
+ const moduleShape = parseImportsExports(targetPath);
792
+ const symbols = collectDefinitions(targetPath).slice(0, maxSymbols);
793
+ const dependencies = [...new Set(moduleShape.imports.map((item) => item.source).filter(Boolean))];
794
+ const exportedSymbols = symbols.filter((symbol) => symbol.exported).map((symbol) => symbol.name);
795
+ return JSON.stringify({
796
+ file: relativeFromWorkspace(targetPath),
797
+ language: inferLanguage(targetPath),
798
+ lines: lines.length,
799
+ purpose: purposeHint(targetPath, lines),
800
+ symbols,
801
+ dependencies,
802
+ exports: {
803
+ statements: moduleShape.exports,
804
+ symbols: exportedSymbols,
805
+ },
806
+ side_effects: detectSideEffects(lines),
807
+ }, null, 2);
808
+ }
809
+ if (name === 'list_dependencies') {
810
+ const dirPath = readStringArg(args, 'dir_path', '.');
811
+ const includeDev = readBooleanArg(args, 'include_dev', true);
812
+ const maxFiles = clampInteger(readNumberArg(args, 'max_files', 50), 1, MAX_RESULT_LIMIT);
813
+ const targetPath = workspacePath(dirPath);
814
+ const files = walkFiles(targetPath, {
815
+ includeHidden: false,
816
+ ignorePatterns: DEFAULT_IGNORES,
817
+ maxEntries: MAX_RESULT_LIMIT * 20,
818
+ })
819
+ .map(relativeFromWorkspace)
820
+ .filter(isDependencyFile)
821
+ .slice(0, maxFiles);
822
+ const packageFiles = files.filter((file) => path.basename(file) === 'package.json');
823
+ const packages = packageFiles.map((file) => {
824
+ const packageJson = JSON.parse(readTextFile(workspacePath(file)));
825
+ const dependencies = typeof packageJson.dependencies === 'object' && packageJson.dependencies
826
+ ? packageJson.dependencies
827
+ : {};
828
+ const devDependencies = includeDev
829
+ && typeof packageJson.devDependencies === 'object'
830
+ && packageJson.devDependencies
831
+ ? packageJson.devDependencies
832
+ : {};
833
+ const peerDependencies = typeof packageJson.peerDependencies === 'object' && packageJson.peerDependencies
834
+ ? packageJson.peerDependencies
835
+ : {};
836
+ const optionalDependencies = typeof packageJson.optionalDependencies === 'object' && packageJson.optionalDependencies
837
+ ? packageJson.optionalDependencies
838
+ : {};
839
+ return {
840
+ file,
841
+ name: packageJson.name ?? null,
842
+ runtime: dependencies,
843
+ development: devDependencies,
844
+ peer: peerDependencies,
845
+ optional: optionalDependencies,
846
+ scripts: packageJson.scripts ?? {},
847
+ packageManager: packageJson.packageManager ?? null,
848
+ engines: packageJson.engines ?? {},
849
+ devEngines: packageJson.devEngines ?? {},
850
+ };
851
+ });
852
+ const dockerFiles = files
853
+ .filter((file) => path.basename(file) === 'Dockerfile')
854
+ .map((file) => ({
855
+ file,
856
+ baseImages: readLines(workspacePath(file))
857
+ .map((line) => line.trim().match(/^FROM\s+(.+)$/i)?.[1])
858
+ .filter(Boolean),
859
+ }));
860
+ return JSON.stringify({
861
+ scanned_from: relativeFromWorkspace(targetPath),
862
+ dependency_files: files,
863
+ packages,
864
+ docker: dockerFiles,
865
+ lockfiles: files.filter((file) => /lock|pnpm-lock|yarn\.lock/.test(path.basename(file))),
866
+ build_files: files.filter((file) => /Cargo\.toml|go\.mod|requirements\.txt|pyproject\.toml|pom\.xml|build\.gradle/.test(path.basename(file))),
867
+ }, null, 2);
868
+ }
869
+ if (name === 'read_manifest_files') {
870
+ const dirPath = readStringArg(args, 'dir_path', '.');
871
+ const maxFiles = clampInteger(readNumberArg(args, 'max_files', 40), 1, MAX_RESULT_LIMIT);
872
+ const maxBytesPerFile = clampInteger(readNumberArg(args, 'max_bytes_per_file', 20_000), 1000, 100_000);
873
+ const targetPath = workspacePath(dirPath);
874
+ const files = walkFiles(targetPath, {
875
+ includeHidden: true,
876
+ ignorePatterns: DEFAULT_IGNORES,
877
+ maxEntries: MAX_RESULT_LIMIT * 20,
878
+ })
879
+ .map(relativeFromWorkspace)
880
+ .filter(isManifestFile)
881
+ .slice(0, maxFiles);
882
+ const manifests = files.map((file) => {
883
+ const absolutePath = workspacePath(file);
884
+ const content = readTextFile(absolutePath);
885
+ const truncated = Buffer.byteLength(content, 'utf-8') > maxBytesPerFile;
886
+ return {
887
+ file,
888
+ size_bytes: fs.statSync(absolutePath).size,
889
+ truncated,
890
+ content: truncated ? content.slice(0, maxBytesPerFile) : content,
891
+ };
892
+ });
893
+ return JSON.stringify({
894
+ scanned_from: relativeFromWorkspace(targetPath),
895
+ files,
896
+ manifests,
897
+ }, null, 2);
898
+ }
899
+ if (name === 'list_git_status') {
900
+ const rawStatus = execFileSync('git', ['status', '--porcelain=v1'], {
901
+ cwd: process.cwd(),
902
+ encoding: 'utf-8',
903
+ stdio: ['pipe', 'pipe', 'pipe'],
904
+ });
905
+ const status = parseGitStatus(rawStatus);
906
+ return JSON.stringify({
907
+ ...status,
908
+ clean: status.staged.length === 0
909
+ && status.unstaged.length === 0
910
+ && status.untracked.length === 0
911
+ && status.renamed.length === 0,
912
+ }, null, 2);
913
+ }
914
+ if (name === 'blame_file_range') {
915
+ const filePath = readStringArg(args, 'file_path');
916
+ const startLine = clampInteger(readNumberArg(args, 'start_line', 1), 1, Number.MAX_SAFE_INTEGER);
917
+ const endLine = clampInteger(readNumberArg(args, 'end_line', startLine), startLine, startLine + MAX_CHUNK_LIMIT - 1);
918
+ const targetPath = workspacePath(filePath);
919
+ const relativePath = relativeFromWorkspace(targetPath);
920
+ const blame = execFileSync('git', [
921
+ 'blame',
922
+ '--line-porcelain',
923
+ `-L${startLine},${endLine}`,
924
+ '--',
925
+ relativePath,
926
+ ], {
927
+ cwd: process.cwd(),
928
+ encoding: 'utf-8',
929
+ stdio: ['pipe', 'pipe', 'pipe'],
930
+ });
931
+ const entries = [];
932
+ let current = null;
933
+ blame.split(/\r?\n/).forEach((line) => {
934
+ const header = line.match(/^([0-9a-f]{40})\s+/);
935
+ if (header) {
936
+ current = {
937
+ commit: header[1],
938
+ author: null,
939
+ author_time: null,
940
+ summary: null,
941
+ };
942
+ }
943
+ else if (current && line.startsWith('author ')) {
944
+ current.author = line.slice('author '.length);
945
+ }
946
+ else if (current && line.startsWith('author-time ')) {
947
+ const timestamp = Number(line.slice('author-time '.length));
948
+ current.author_time = Number.isFinite(timestamp)
949
+ ? new Date(timestamp * 1000).toISOString()
950
+ : null;
951
+ }
952
+ else if (current && line.startsWith('summary ')) {
953
+ current.summary = line.slice('summary '.length);
954
+ }
955
+ else if (current && line.startsWith('\t')) {
956
+ entries.push({
957
+ ...current,
958
+ line: line.slice(1),
959
+ });
960
+ }
961
+ });
962
+ return JSON.stringify({
963
+ file: relativePath,
964
+ range: `${startLine}-${endLine}`,
965
+ entries,
966
+ }, null, 2);
967
+ }
968
+ if (name === 'find_entrypoints') {
969
+ const dirPath = readStringArg(args, 'dir_path', '.');
970
+ const maxResults = clampInteger(readNumberArg(args, 'max_results', DEFAULT_RESULT_LIMIT), 1, MAX_RESULT_LIMIT);
971
+ const targetPath = workspacePath(dirPath);
972
+ const files = walkFiles(targetPath, {
973
+ includeHidden: false,
974
+ ignorePatterns: DEFAULT_IGNORES,
975
+ maxEntries: MAX_RESULT_LIMIT * 20,
976
+ });
977
+ const packageEntries = files
978
+ .filter((file) => path.basename(file) === 'package.json')
979
+ .flatMap(packageEntrypoints);
980
+ const conventionEntries = files
981
+ .map(relativeFromWorkspace)
982
+ .map((file) => {
983
+ const kind = entrypointKind(file);
984
+ return kind
985
+ ? { kind, file, source: 'filename_or_directory_convention', detail: null }
986
+ : null;
987
+ })
988
+ .filter((entry) => entry !== null);
989
+ const entries = [...packageEntries, ...conventionEntries].slice(0, maxResults);
990
+ return JSON.stringify({
991
+ scanned_from: relativeFromWorkspace(targetPath),
992
+ entrypoints: entries,
993
+ }, null, 2);
994
+ }
995
+ if (name === 'list_routes') {
996
+ const dirPath = readStringArg(args, 'dir_path', '.');
997
+ const maxResults = clampInteger(readNumberArg(args, 'max_results', 200), 1, MAX_RESULT_LIMIT);
998
+ const targetPath = workspacePath(dirPath);
999
+ const files = collectSourceFiles(targetPath);
1000
+ const routes = files.flatMap(parseRoutesInFile).slice(0, maxResults);
1001
+ return JSON.stringify({
1002
+ scanned_from: relativeFromWorkspace(targetPath),
1003
+ routes,
1004
+ }, null, 2);
1005
+ }
1006
+ if (name === 'write_file') {
1007
+ const filePath = readStringArg(args, 'file_path');
1008
+ const content = readStringArg(args, 'content');
1009
+ const targetPath = workspacePath(filePath);
1010
+ const relativePath = path.relative(process.cwd(), targetPath).split(path.sep).join('/');
1011
+ if (relativePath !== 'docs/intro.md') {
1012
+ return `Error executing tool: write_file is restricted to ./docs/intro.md during initialization. Refused path: ${filePath}`;
1013
+ }
1014
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
1015
+ fs.writeFileSync(targetPath, content, 'utf-8');
1016
+ return `Success: Wrote ${content.length} characters to ${filePath}`;
1017
+ }
1018
+ if (name === 'get_git_diff') {
1019
+ // Helper: run a git command and return stdout, or '' on failure (e.g. no commits yet)
1020
+ const tryGit = (cmd) => {
1021
+ try {
1022
+ return execSync(cmd, { stdio: ['pipe', 'pipe', 'pipe'] }).toString().trim();
1023
+ }
1024
+ catch {
1025
+ return '';
1026
+ }
1027
+ };
1028
+ const working = tryGit('git diff');
1029
+ const staged = tryGit('git diff --cached');
1030
+ const log = tryGit('git log --oneline --stat -10');
1031
+ const sections = [
1032
+ `## Uncommitted changes (working tree)\n${working || 'None.'}`,
1033
+ `## Staged changes (index)\n${staged || 'None.'}`,
1034
+ `## Recent commits (last 10)\n${log || 'No commits found.'}`,
1035
+ ];
1036
+ return sections.join('\n\n');
1037
+ }
1038
+ return `Tool ${name} not found.`;
1039
+ }
1040
+ catch (err) {
1041
+ const message = err instanceof Error ? err.message : String(err);
1042
+ return `Error executing tool: ${message}`;
1043
+ }
1044
+ }