@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.
- package/README.md +91 -0
- package/dist/ai/chat.js +33 -0
- package/dist/ai/contextUtils.js +79 -0
- package/dist/ai/openAiClient.js +72 -0
- package/dist/ai/summarizer.js +65 -0
- package/dist/cli/configStore.js +68 -0
- package/dist/cli/validation.js +45 -0
- package/dist/cli.js +74 -0
- package/dist/config.js +36 -0
- package/dist/functions/toolCallHandler.js +25 -0
- package/dist/functions/toolDefinitions.js +422 -0
- package/dist/functions/toolExecutor.js +1044 -0
- package/dist/prompts/initializationPrompt.js +46 -0
- package/dist/prompts/systemPrompt.js +72 -0
- package/dist/ui/projectDb.js +220 -0
- package/dist/ui/server.js +523 -0
- package/dist/ui/webAgent.js +170 -0
- package/package.json +47 -0
- package/ui/app.html +952 -0
|
@@ -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
|
+
}
|