nexusforge-cli 1.1.1 → 1.2.1
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/dist/components/App.d.ts.map +1 -1
- package/dist/components/App.js +183 -17
- package/dist/components/App.js.map +1 -1
- package/dist/index.js +462 -10
- package/dist/index.js.map +1 -1
- package/dist/modules/commandEngine.d.ts +70 -0
- package/dist/modules/commandEngine.d.ts.map +1 -0
- package/dist/modules/commandEngine.js +672 -0
- package/dist/modules/commandEngine.js.map +1 -0
- package/dist/modules/contextBuilder.d.ts +51 -0
- package/dist/modules/contextBuilder.d.ts.map +1 -0
- package/dist/modules/contextBuilder.js +725 -0
- package/dist/modules/contextBuilder.js.map +1 -0
- package/dist/modules/domainDetector.d.ts +64 -0
- package/dist/modules/domainDetector.d.ts.map +1 -0
- package/dist/modules/domainDetector.js +722 -0
- package/dist/modules/domainDetector.js.map +1 -0
- package/dist/modules/fileOperations.d.ts +99 -0
- package/dist/modules/fileOperations.d.ts.map +1 -0
- package/dist/modules/fileOperations.js +543 -0
- package/dist/modules/fileOperations.js.map +1 -0
- package/dist/modules/forgeEngine.d.ts +153 -0
- package/dist/modules/forgeEngine.d.ts.map +1 -0
- package/dist/modules/forgeEngine.js +652 -0
- package/dist/modules/forgeEngine.js.map +1 -0
- package/dist/modules/gitManager.d.ts +151 -0
- package/dist/modules/gitManager.d.ts.map +1 -0
- package/dist/modules/gitManager.js +539 -0
- package/dist/modules/gitManager.js.map +1 -0
- package/dist/modules/index.d.ts +25 -0
- package/dist/modules/index.d.ts.map +1 -0
- package/dist/modules/index.js +25 -0
- package/dist/modules/index.js.map +1 -0
- package/dist/modules/prdProcessor.d.ts +125 -0
- package/dist/modules/prdProcessor.d.ts.map +1 -0
- package/dist/modules/prdProcessor.js +466 -0
- package/dist/modules/prdProcessor.js.map +1 -0
- package/dist/modules/projectScanner.d.ts +105 -0
- package/dist/modules/projectScanner.d.ts.map +1 -0
- package/dist/modules/projectScanner.js +859 -0
- package/dist/modules/projectScanner.js.map +1 -0
- package/dist/modules/safetyGuard.d.ts +83 -0
- package/dist/modules/safetyGuard.d.ts.map +1 -0
- package/dist/modules/safetyGuard.js +492 -0
- package/dist/modules/safetyGuard.js.map +1 -0
- package/dist/modules/templateManager.d.ts +78 -0
- package/dist/modules/templateManager.d.ts.map +1 -0
- package/dist/modules/templateManager.js +556 -0
- package/dist/modules/templateManager.js.map +1 -0
- package/dist/native/index.d.ts +125 -0
- package/dist/native/index.d.ts.map +1 -0
- package/dist/native/index.js +164 -0
- package/dist/native/index.js.map +1 -0
- package/dist/services/executor.d.ts +24 -0
- package/dist/services/executor.d.ts.map +1 -1
- package/dist/services/executor.js +149 -6
- package/dist/services/executor.js.map +1 -1
- package/dist/services/fileManager.d.ts +134 -0
- package/dist/services/fileManager.d.ts.map +1 -0
- package/dist/services/fileManager.js +489 -0
- package/dist/services/fileManager.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,725 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Builder for NexusForge CLI
|
|
3
|
+
* Manages context for LLM interactions
|
|
4
|
+
* Ported from Python nexusforge/modules/llm/context_builder.py
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// CONTEXT BUILDER CLASS
|
|
10
|
+
// ============================================================================
|
|
11
|
+
export class ContextBuilder {
|
|
12
|
+
projectPath;
|
|
13
|
+
maxContextTokens = 30000;
|
|
14
|
+
tokenEstimateRatio = 1.3;
|
|
15
|
+
projectPatterns;
|
|
16
|
+
// Directories to ignore
|
|
17
|
+
static IGNORE_PATTERNS = new Set([
|
|
18
|
+
'.git', '__pycache__', 'node_modules', '.nexusforge',
|
|
19
|
+
'venv', 'env', '.env', 'dist', 'build', 'target',
|
|
20
|
+
'.venv', 'coverage', '.pytest_cache', '.mypy_cache',
|
|
21
|
+
]);
|
|
22
|
+
// Extension to language mapping
|
|
23
|
+
static EXT_MAP = {
|
|
24
|
+
'.py': 'python',
|
|
25
|
+
'.js': 'javascript',
|
|
26
|
+
'.ts': 'typescript',
|
|
27
|
+
'.jsx': 'javascript',
|
|
28
|
+
'.tsx': 'typescript',
|
|
29
|
+
'.rs': 'rust',
|
|
30
|
+
'.go': 'go',
|
|
31
|
+
'.java': 'java',
|
|
32
|
+
'.cpp': 'cpp',
|
|
33
|
+
'.c': 'c',
|
|
34
|
+
'.cs': 'csharp',
|
|
35
|
+
'.rb': 'ruby',
|
|
36
|
+
'.php': 'php',
|
|
37
|
+
};
|
|
38
|
+
constructor(projectPath) {
|
|
39
|
+
this.projectPath = path.resolve(projectPath);
|
|
40
|
+
this.projectPatterns = this.detectProjectPatterns();
|
|
41
|
+
}
|
|
42
|
+
// ========================================================================
|
|
43
|
+
// MAIN CONTEXT BUILDING
|
|
44
|
+
// ========================================================================
|
|
45
|
+
buildContextForQuery(query) {
|
|
46
|
+
// Extract file references from query
|
|
47
|
+
const referencedFiles = this.extractFileReferences(query);
|
|
48
|
+
// Find relevant files based on query content
|
|
49
|
+
const relevantFiles = this.findRelevantFiles(query, referencedFiles);
|
|
50
|
+
// Add project structure info for multi-file operations
|
|
51
|
+
const multiFileKeywords = [
|
|
52
|
+
'create feature', 'add feature', 'new service', 'new component',
|
|
53
|
+
'implement', 'monitoring', 'control', 'dashboard',
|
|
54
|
+
];
|
|
55
|
+
if (multiFileKeywords.some(keyword => query.toLowerCase().includes(keyword))) {
|
|
56
|
+
const structureContext = this.getProjectStructureContext();
|
|
57
|
+
if (structureContext) {
|
|
58
|
+
return structureContext + '\n\n' + this.buildHierarchicalContext(relevantFiles);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// If user requested specific files that weren't found, provide feedback
|
|
62
|
+
if (referencedFiles.size > 0 && relevantFiles.length === 0) {
|
|
63
|
+
const missingFiles = Array.from(referencedFiles).slice(0, 5);
|
|
64
|
+
let context = 'Could not find the following files:\n';
|
|
65
|
+
for (const f of missingFiles) {
|
|
66
|
+
context += ` - ${f}\n`;
|
|
67
|
+
}
|
|
68
|
+
context += '\nSearching for similar files...\n';
|
|
69
|
+
// Try to find similar files
|
|
70
|
+
for (const ref of referencedFiles) {
|
|
71
|
+
const similar = this.findSimilarFiles(ref);
|
|
72
|
+
if (similar.length > 0) {
|
|
73
|
+
context += `\nFiles similar to '${ref}':\n`;
|
|
74
|
+
for (const s of similar.slice(0, 3)) {
|
|
75
|
+
context += ` - ${s}\n`;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return context;
|
|
80
|
+
}
|
|
81
|
+
// Build hierarchical context
|
|
82
|
+
return this.buildHierarchicalContext(relevantFiles);
|
|
83
|
+
}
|
|
84
|
+
// ========================================================================
|
|
85
|
+
// FILE REFERENCE EXTRACTION
|
|
86
|
+
// ========================================================================
|
|
87
|
+
extractFileReferences(query) {
|
|
88
|
+
const references = new Set();
|
|
89
|
+
// Pattern 1: @filename or @path/to/file
|
|
90
|
+
const atPattern = /@(\S+)/g;
|
|
91
|
+
let match;
|
|
92
|
+
while ((match = atPattern.exec(query)) !== null) {
|
|
93
|
+
references.add(match[1]);
|
|
94
|
+
}
|
|
95
|
+
// Pattern 2: "in file.py" or "from file.js"
|
|
96
|
+
const inPattern = /(?:in|from)\s+(\S+\.\w+)/gi;
|
|
97
|
+
while ((match = inPattern.exec(query)) !== null) {
|
|
98
|
+
references.add(match[1]);
|
|
99
|
+
}
|
|
100
|
+
// Pattern 3: Quoted filenames
|
|
101
|
+
const quotePattern = /["']([^"']+\.\w+)["']/g;
|
|
102
|
+
while ((match = quotePattern.exec(query)) !== null) {
|
|
103
|
+
references.add(match[1]);
|
|
104
|
+
}
|
|
105
|
+
// Pattern 4: Function/class references
|
|
106
|
+
const funcPattern = /(?:function|class|def)\s+(\w+)/gi;
|
|
107
|
+
const potentialFuncs = [];
|
|
108
|
+
while ((match = funcPattern.exec(query)) !== null) {
|
|
109
|
+
potentialFuncs.push(match[1]);
|
|
110
|
+
}
|
|
111
|
+
// Search for files containing these functions
|
|
112
|
+
for (const func of potentialFuncs) {
|
|
113
|
+
const matchingFiles = this.searchForSymbol(func);
|
|
114
|
+
for (const f of matchingFiles) {
|
|
115
|
+
references.add(f);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// Pattern 5: Common file patterns
|
|
119
|
+
const extensions = 'js|jsx|ts|tsx|py|rs|java|cpp|c|h|hpp|go|rb|php|swift|kt|scala|' +
|
|
120
|
+
'html|htm|css|scss|sass|less|vue|svelte|' +
|
|
121
|
+
'json|yaml|yml|toml|xml|env|ini|conf|cfg|' +
|
|
122
|
+
'md|mdx|rst|txt|' +
|
|
123
|
+
'sh|bash|ps1|bat|cmd';
|
|
124
|
+
const reviewPatterns = [
|
|
125
|
+
new RegExp(`(?:review|check|look at|examine|analyze|find|open)\\s+(?:the\\s+)?(\\S+?)(?:\\s+file)?`, 'gi'),
|
|
126
|
+
new RegExp(`(?:find|analyze|review)\\s+(?:the\\s+)?(\\S+\\.(?:${extensions}))\\b`, 'gi'),
|
|
127
|
+
new RegExp(`(\\S+\\.(?:${extensions}))\\s+file`, 'gi'),
|
|
128
|
+
];
|
|
129
|
+
for (const pattern of reviewPatterns) {
|
|
130
|
+
while ((match = pattern.exec(query)) !== null) {
|
|
131
|
+
const cleanMatch = match[1].replace(/[.,;:!?]$/, '');
|
|
132
|
+
if (!['the', 'a', 'an'].some(word => cleanMatch.startsWith(word)) && cleanMatch.length > 2) {
|
|
133
|
+
references.add(cleanMatch);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return references;
|
|
138
|
+
}
|
|
139
|
+
// ========================================================================
|
|
140
|
+
// FILE FINDING AND RELEVANCE
|
|
141
|
+
// ========================================================================
|
|
142
|
+
findRelevantFiles(query, explicitRefs) {
|
|
143
|
+
const relevantFiles = [];
|
|
144
|
+
// First, add explicitly referenced files
|
|
145
|
+
for (const ref of explicitRefs) {
|
|
146
|
+
const filePath = path.join(this.projectPath, ref);
|
|
147
|
+
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
|
|
148
|
+
const content = this.readFileSafely(filePath);
|
|
149
|
+
if (content) {
|
|
150
|
+
relevantFiles.push({
|
|
151
|
+
path: filePath,
|
|
152
|
+
relevanceScore: 1.0,
|
|
153
|
+
content,
|
|
154
|
+
language: this.detectLanguage(filePath),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
// Try to find file by name in subdirectories (LIMITED SEARCH)
|
|
160
|
+
const foundFiles = this.findFileByName(ref);
|
|
161
|
+
for (const foundPath of foundFiles.slice(0, 3)) {
|
|
162
|
+
const content = this.readFileSafely(foundPath);
|
|
163
|
+
if (content) {
|
|
164
|
+
relevantFiles.push({
|
|
165
|
+
path: foundPath,
|
|
166
|
+
relevanceScore: 0.9,
|
|
167
|
+
content,
|
|
168
|
+
language: this.detectLanguage(foundPath),
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// If no explicit files found, scan current directory
|
|
175
|
+
if (relevantFiles.length === 0) {
|
|
176
|
+
const keywords = this.extractKeywords(query);
|
|
177
|
+
const scannedFiles = this.scanCurrentDirectory(keywords);
|
|
178
|
+
relevantFiles.push(...scannedFiles);
|
|
179
|
+
}
|
|
180
|
+
// Extract keywords from query
|
|
181
|
+
const keywords = this.extractKeywords(query);
|
|
182
|
+
// Search for files containing keywords
|
|
183
|
+
for (const filePath of this.iterateProjectFiles()) {
|
|
184
|
+
if (Array.from(explicitRefs).some(ref => filePath.endsWith(ref))) {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
const content = this.readFileSafely(filePath);
|
|
188
|
+
if (!content)
|
|
189
|
+
continue;
|
|
190
|
+
// Calculate relevance score
|
|
191
|
+
const score = this.calculateRelevance(content, keywords, query);
|
|
192
|
+
if (score > 0.3) {
|
|
193
|
+
relevantFiles.push({
|
|
194
|
+
path: filePath,
|
|
195
|
+
relevanceScore: score,
|
|
196
|
+
content,
|
|
197
|
+
language: this.detectLanguage(filePath),
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// Sort by relevance
|
|
202
|
+
relevantFiles.sort((a, b) => b.relevanceScore - a.relevanceScore);
|
|
203
|
+
// Limit to top files that fit in context
|
|
204
|
+
return this.limitByContextSize(relevantFiles);
|
|
205
|
+
}
|
|
206
|
+
findFileByName(name) {
|
|
207
|
+
const results = [];
|
|
208
|
+
const scan = (dir, depth) => {
|
|
209
|
+
if (depth > 2)
|
|
210
|
+
return; // Only search 2 levels deep
|
|
211
|
+
try {
|
|
212
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
213
|
+
for (const entry of entries) {
|
|
214
|
+
const fullPath = path.join(dir, entry.name);
|
|
215
|
+
if (entry.isDirectory()) {
|
|
216
|
+
if (!this.shouldIgnore(fullPath)) {
|
|
217
|
+
scan(fullPath, depth + 1);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
else if (entry.name.includes(name) || name.includes(entry.name)) {
|
|
221
|
+
results.push(fullPath);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
// Ignore errors
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
scan(this.projectPath, 0);
|
|
230
|
+
return results;
|
|
231
|
+
}
|
|
232
|
+
scanCurrentDirectory(keywords) {
|
|
233
|
+
const relevantFiles = [];
|
|
234
|
+
// Scan project root and immediate subdirectories
|
|
235
|
+
const scanPaths = [this.projectPath];
|
|
236
|
+
try {
|
|
237
|
+
for (const item of fs.readdirSync(this.projectPath, { withFileTypes: true })) {
|
|
238
|
+
if (item.isDirectory() && !this.shouldIgnore(path.join(this.projectPath, item.name))) {
|
|
239
|
+
scanPaths.push(path.join(this.projectPath, item.name));
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
// Ignore errors
|
|
245
|
+
}
|
|
246
|
+
for (const scanPath of scanPaths) {
|
|
247
|
+
try {
|
|
248
|
+
for (const file of fs.readdirSync(scanPath, { withFileTypes: true })) {
|
|
249
|
+
if (file.isFile() && !this.shouldIgnore(path.join(scanPath, file.name))) {
|
|
250
|
+
const filePath = path.join(scanPath, file.name);
|
|
251
|
+
const content = this.readFileSafely(filePath);
|
|
252
|
+
if (content) {
|
|
253
|
+
const score = this.calculateRelevance(content, keywords, keywords.join(' '));
|
|
254
|
+
if (score > 0.1) {
|
|
255
|
+
relevantFiles.push({
|
|
256
|
+
path: filePath,
|
|
257
|
+
relevanceScore: score,
|
|
258
|
+
content,
|
|
259
|
+
language: this.detectLanguage(filePath),
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
// Ignore errors
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return relevantFiles;
|
|
271
|
+
}
|
|
272
|
+
shouldIgnore(filepath) {
|
|
273
|
+
const parts = filepath.split(path.sep);
|
|
274
|
+
return parts.some(part => ContextBuilder.IGNORE_PATTERNS.has(part));
|
|
275
|
+
}
|
|
276
|
+
searchForSymbol(symbol) {
|
|
277
|
+
const matches = new Set();
|
|
278
|
+
try {
|
|
279
|
+
for (const filePath of this.iterateProjectFiles()) {
|
|
280
|
+
const content = this.readFileSafely(filePath);
|
|
281
|
+
if (!content)
|
|
282
|
+
continue;
|
|
283
|
+
try {
|
|
284
|
+
const regex = new RegExp(`\\b${symbol}\\b`);
|
|
285
|
+
if (regex.test(content)) {
|
|
286
|
+
matches.add(path.relative(this.projectPath, filePath));
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
// Fallback to simple includes
|
|
291
|
+
if (content.includes(symbol)) {
|
|
292
|
+
matches.add(path.relative(this.projectPath, filePath));
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
catch {
|
|
298
|
+
// Ignore errors
|
|
299
|
+
}
|
|
300
|
+
return matches;
|
|
301
|
+
}
|
|
302
|
+
extractKeywords(query) {
|
|
303
|
+
const stopWords = new Set([
|
|
304
|
+
'the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for',
|
|
305
|
+
'of', 'with', 'by', 'from', 'up', 'about', 'into', 'through', 'during',
|
|
306
|
+
'how', 'when', 'where', 'what', 'which', 'who', 'why', 'can', 'could',
|
|
307
|
+
'should', 'would', 'will', 'add', 'create', 'make', 'build', 'implement',
|
|
308
|
+
'update', 'modify', 'change', 'fix', 'improve', 'refactor',
|
|
309
|
+
]);
|
|
310
|
+
const words = query.toLowerCase().match(/\b\w+\b/g) || [];
|
|
311
|
+
const keywords = [];
|
|
312
|
+
for (const word of words) {
|
|
313
|
+
if (word.length > 2 && !stopWords.has(word)) {
|
|
314
|
+
keywords.push(word);
|
|
315
|
+
if (word.includes('_')) {
|
|
316
|
+
keywords.push(word.replace(/_/g, ''));
|
|
317
|
+
keywords.push(word.split('_').map(p => p.charAt(0).toUpperCase() + p.slice(1)).join(''));
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return keywords;
|
|
322
|
+
}
|
|
323
|
+
calculateRelevance(content, keywords, query) {
|
|
324
|
+
let score = 0.0;
|
|
325
|
+
const contentLower = content.toLowerCase();
|
|
326
|
+
// Keyword matches
|
|
327
|
+
for (const keyword of keywords) {
|
|
328
|
+
const count = (contentLower.match(new RegExp(keyword.toLowerCase(), 'g')) || []).length;
|
|
329
|
+
if (count > 0) {
|
|
330
|
+
score += Math.min(count * 0.1, 0.3);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
// Special patterns
|
|
334
|
+
if (query.toLowerCase().includes('test') &&
|
|
335
|
+
(contentLower.includes('test') || contentLower.includes('spec'))) {
|
|
336
|
+
score += 0.2;
|
|
337
|
+
}
|
|
338
|
+
if (query.toLowerCase().includes('error') &&
|
|
339
|
+
(contentLower.includes('error') || contentLower.includes('exception'))) {
|
|
340
|
+
score += 0.2;
|
|
341
|
+
}
|
|
342
|
+
if (query.toLowerCase().includes('config') &&
|
|
343
|
+
['config', 'settings', 'env'].some(word => contentLower.includes(word))) {
|
|
344
|
+
score += 0.2;
|
|
345
|
+
}
|
|
346
|
+
if (['import', 'require', 'use'].some(word => contentLower.includes(word))) {
|
|
347
|
+
score += 0.1;
|
|
348
|
+
}
|
|
349
|
+
return Math.min(score, 1.0);
|
|
350
|
+
}
|
|
351
|
+
// ========================================================================
|
|
352
|
+
// CONTEXT SIZE MANAGEMENT
|
|
353
|
+
// ========================================================================
|
|
354
|
+
limitByContextSize(files) {
|
|
355
|
+
const selected = [];
|
|
356
|
+
let currentTokens = 0;
|
|
357
|
+
for (const fileCtx of files) {
|
|
358
|
+
const tokens = fileCtx.content.split(/\s+/).length * this.tokenEstimateRatio;
|
|
359
|
+
if (currentTokens + tokens > this.maxContextTokens) {
|
|
360
|
+
if (fileCtx.relevanceScore > 0.8 && currentTokens < this.maxContextTokens * 0.9) {
|
|
361
|
+
const lines = fileCtx.content.split('\n');
|
|
362
|
+
fileCtx.content = lines.slice(0, 50).join('\n') + '\n... (truncated)';
|
|
363
|
+
selected.push(fileCtx);
|
|
364
|
+
}
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
selected.push(fileCtx);
|
|
368
|
+
currentTokens += tokens;
|
|
369
|
+
}
|
|
370
|
+
return selected;
|
|
371
|
+
}
|
|
372
|
+
// ========================================================================
|
|
373
|
+
// CONTEXT BUILDING AND FORMATTING
|
|
374
|
+
// ========================================================================
|
|
375
|
+
buildHierarchicalContext(files) {
|
|
376
|
+
if (files.length === 0) {
|
|
377
|
+
return '';
|
|
378
|
+
}
|
|
379
|
+
const contextParts = [];
|
|
380
|
+
// High relevance files
|
|
381
|
+
const highRelevance = files.filter(f => f.relevanceScore >= 0.8);
|
|
382
|
+
if (highRelevance.length > 0) {
|
|
383
|
+
contextParts.push('=== Highly Relevant Files ===\n');
|
|
384
|
+
for (const fileCtx of highRelevance) {
|
|
385
|
+
const relPath = path.relative(this.projectPath, fileCtx.path);
|
|
386
|
+
contextParts.push(`\n--- ${relPath} ---\n`);
|
|
387
|
+
contextParts.push(fileCtx.content);
|
|
388
|
+
contextParts.push('\n');
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
// Medium relevance files
|
|
392
|
+
const mediumRelevance = files.filter(f => f.relevanceScore >= 0.5 && f.relevanceScore < 0.8);
|
|
393
|
+
if (mediumRelevance.length > 0) {
|
|
394
|
+
contextParts.push('\n=== Related Files ===\n');
|
|
395
|
+
for (const fileCtx of mediumRelevance) {
|
|
396
|
+
const relPath = path.relative(this.projectPath, fileCtx.path);
|
|
397
|
+
contextParts.push(`\n--- ${relPath} (key sections) ---\n`);
|
|
398
|
+
const keyContent = this.extractKeySections(fileCtx.content, fileCtx.language);
|
|
399
|
+
contextParts.push(keyContent);
|
|
400
|
+
contextParts.push('\n');
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
// Low relevance files
|
|
404
|
+
const lowRelevance = files.filter(f => f.relevanceScore < 0.5);
|
|
405
|
+
if (lowRelevance.length > 0) {
|
|
406
|
+
contextParts.push('\n=== Project Structure ===\n');
|
|
407
|
+
for (const fileCtx of lowRelevance) {
|
|
408
|
+
const relPath = path.relative(this.projectPath, fileCtx.path);
|
|
409
|
+
const structure = this.extractStructure(fileCtx.content, fileCtx.language);
|
|
410
|
+
if (structure) {
|
|
411
|
+
contextParts.push(`${relPath}: ${structure}\n`);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return contextParts.join('');
|
|
416
|
+
}
|
|
417
|
+
extractKeySections(content, language) {
|
|
418
|
+
const lines = content.split('\n');
|
|
419
|
+
const keyLines = [];
|
|
420
|
+
if (language === 'python') {
|
|
421
|
+
for (let i = 0; i < lines.length; i++) {
|
|
422
|
+
const line = lines[i];
|
|
423
|
+
if (line.startsWith('import ') || line.startsWith('from ') ||
|
|
424
|
+
line.startsWith('class ') || line.startsWith('def ') ||
|
|
425
|
+
line.trim().startsWith('@')) {
|
|
426
|
+
keyLines.push(line);
|
|
427
|
+
// Include docstring if present
|
|
428
|
+
if (i + 1 < lines.length && lines[i + 1].includes('"""')) {
|
|
429
|
+
let j = i + 1;
|
|
430
|
+
while (j < lines.length && !lines[j].slice(3).includes('"""')) {
|
|
431
|
+
keyLines.push(lines[j]);
|
|
432
|
+
j++;
|
|
433
|
+
if (j < lines.length) {
|
|
434
|
+
keyLines.push(lines[j]);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
else if (language === 'javascript' || language === 'typescript') {
|
|
442
|
+
for (const line of lines) {
|
|
443
|
+
if (line.startsWith('import ') || line.startsWith('export ') ||
|
|
444
|
+
line.includes('class ') || line.includes('function ') || line.includes('const ')) {
|
|
445
|
+
keyLines.push(line);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
else if (language === 'rust') {
|
|
450
|
+
for (const line of lines) {
|
|
451
|
+
if (line.startsWith('use ') || line.startsWith('pub ') ||
|
|
452
|
+
line.startsWith('struct ') || line.startsWith('trait ') ||
|
|
453
|
+
line.startsWith('fn ') || line.startsWith('impl ')) {
|
|
454
|
+
keyLines.push(line);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return keyLines.slice(0, 100).join('\n');
|
|
459
|
+
}
|
|
460
|
+
findSimilarFiles(target) {
|
|
461
|
+
const similarFiles = [];
|
|
462
|
+
const targetLower = target.toLowerCase();
|
|
463
|
+
const targetParts = targetLower.replace(/\./g, ' ').replace(/_/g, ' ').replace(/-/g, ' ').split(' ');
|
|
464
|
+
try {
|
|
465
|
+
for (const filePath of this.iterateProjectFiles()) {
|
|
466
|
+
const fileName = path.basename(filePath).toLowerCase();
|
|
467
|
+
if (targetParts.some(part => part.length > 2 && fileName.includes(part))) {
|
|
468
|
+
const relPath = path.relative(this.projectPath, filePath);
|
|
469
|
+
similarFiles.push(relPath);
|
|
470
|
+
}
|
|
471
|
+
if (targetLower.replace('.', '').includes(fileName.replace('.', ''))) {
|
|
472
|
+
const relPath = path.relative(this.projectPath, filePath);
|
|
473
|
+
if (!similarFiles.includes(relPath)) {
|
|
474
|
+
similarFiles.push(relPath);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
catch {
|
|
480
|
+
// Ignore errors
|
|
481
|
+
}
|
|
482
|
+
return similarFiles.slice(0, 10);
|
|
483
|
+
}
|
|
484
|
+
extractStructure(content, language) {
|
|
485
|
+
const structures = [];
|
|
486
|
+
if (language === 'python') {
|
|
487
|
+
const classes = content.match(/^class\s+(\w+)/gm);
|
|
488
|
+
const functions = content.match(/^def\s+(\w+)/gm);
|
|
489
|
+
if (classes) {
|
|
490
|
+
const classNames = classes.map(c => c.replace('class ', '')).slice(0, 5);
|
|
491
|
+
structures.push(`classes: ${classNames.join(', ')}`);
|
|
492
|
+
}
|
|
493
|
+
if (functions) {
|
|
494
|
+
const funcNames = functions.map(f => f.replace('def ', '')).slice(0, 5);
|
|
495
|
+
structures.push(`functions: ${funcNames.join(', ')}`);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
else if (language === 'javascript' || language === 'typescript') {
|
|
499
|
+
const exports = content.match(/export\s+(?:default\s+)?(?:class|function|const)\s+(\w+)/g);
|
|
500
|
+
if (exports) {
|
|
501
|
+
const exportNames = exports.map(e => e.replace(/export\s+(default\s+)?(class|function|const)\s+/, '')).slice(0, 5);
|
|
502
|
+
structures.push(`exports: ${exportNames.join(', ')}`);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
return structures.join('; ');
|
|
506
|
+
}
|
|
507
|
+
// ========================================================================
|
|
508
|
+
// FILE I/O UTILITIES
|
|
509
|
+
// ========================================================================
|
|
510
|
+
readFileSafely(filePath) {
|
|
511
|
+
try {
|
|
512
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
513
|
+
const lines = content.split('\n');
|
|
514
|
+
return lines.slice(0, 2000).join('\n');
|
|
515
|
+
}
|
|
516
|
+
catch {
|
|
517
|
+
try {
|
|
518
|
+
const content = fs.readFileSync(filePath, 'latin1');
|
|
519
|
+
const lines = content.split('\n');
|
|
520
|
+
return lines.slice(0, 2000).join('\n');
|
|
521
|
+
}
|
|
522
|
+
catch {
|
|
523
|
+
return null;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
detectLanguage(filePath) {
|
|
528
|
+
return ContextBuilder.EXT_MAP[path.extname(filePath).toLowerCase()] || 'text';
|
|
529
|
+
}
|
|
530
|
+
*iterateProjectFiles() {
|
|
531
|
+
const maxDepth = 2;
|
|
532
|
+
function* scan(dir, depth, self) {
|
|
533
|
+
if (depth > maxDepth)
|
|
534
|
+
return;
|
|
535
|
+
try {
|
|
536
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
537
|
+
for (const entry of entries) {
|
|
538
|
+
const fullPath = path.join(dir, entry.name);
|
|
539
|
+
if (ContextBuilder.IGNORE_PATTERNS.has(entry.name))
|
|
540
|
+
continue;
|
|
541
|
+
if (entry.isFile()) {
|
|
542
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
543
|
+
if (!['.pyc', '.exe', '.dll', '.so', '.dylib', '.jpg', '.png', '.gif', '.pdf'].includes(ext)) {
|
|
544
|
+
yield fullPath;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
else if (entry.isDirectory() && depth < maxDepth) {
|
|
548
|
+
yield* scan(fullPath, depth + 1, self);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
catch {
|
|
553
|
+
// Ignore errors
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
yield* scan(this.projectPath, 0, this);
|
|
557
|
+
}
|
|
558
|
+
// ========================================================================
|
|
559
|
+
// PROJECT PATTERN DETECTION
|
|
560
|
+
// ========================================================================
|
|
561
|
+
detectProjectPatterns() {
|
|
562
|
+
const patterns = {
|
|
563
|
+
serviceLocations: [],
|
|
564
|
+
uiLocations: [],
|
|
565
|
+
logicLocations: [],
|
|
566
|
+
configLocations: [],
|
|
567
|
+
styleLocations: [],
|
|
568
|
+
apiLocations: [],
|
|
569
|
+
};
|
|
570
|
+
try {
|
|
571
|
+
for (const filePath of this.iterateProjectFiles()) {
|
|
572
|
+
const relPath = path.relative(this.projectPath, filePath).replace(/\\/g, '/');
|
|
573
|
+
if (['service', 'monitor', 'reporter'].some(p => relPath.toLowerCase().includes(p))) {
|
|
574
|
+
patterns.serviceLocations.push(path.dirname(relPath));
|
|
575
|
+
}
|
|
576
|
+
if (['.tsx', '.jsx'].some(ext => relPath.includes(ext)) ||
|
|
577
|
+
['pages', 'components'].some(p => relPath.includes(p))) {
|
|
578
|
+
patterns.uiLocations.push(path.dirname(relPath));
|
|
579
|
+
}
|
|
580
|
+
if (['logic', 'controller'].some(p => relPath.toLowerCase().includes(p))) {
|
|
581
|
+
patterns.logicLocations.push(path.dirname(relPath));
|
|
582
|
+
}
|
|
583
|
+
if (relPath.toLowerCase().includes('config') ||
|
|
584
|
+
['.json', '.yaml', '.yml'].some(ext => relPath.endsWith(ext))) {
|
|
585
|
+
patterns.configLocations.push(path.dirname(relPath));
|
|
586
|
+
}
|
|
587
|
+
if (['.css', '.scss', '.less'].some(ext => relPath.endsWith(ext)) ||
|
|
588
|
+
relPath.includes('styles')) {
|
|
589
|
+
patterns.styleLocations.push(path.dirname(relPath));
|
|
590
|
+
}
|
|
591
|
+
if (['api', 'routes', 'server'].some(p => relPath.toLowerCase().includes(p))) {
|
|
592
|
+
patterns.apiLocations.push(path.dirname(relPath));
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
catch {
|
|
597
|
+
// Ignore errors
|
|
598
|
+
}
|
|
599
|
+
// Deduplicate and sort by frequency
|
|
600
|
+
for (const key of Object.keys(patterns)) {
|
|
601
|
+
const counts = new Map();
|
|
602
|
+
for (const loc of patterns[key]) {
|
|
603
|
+
counts.set(loc, (counts.get(loc) || 0) + 1);
|
|
604
|
+
}
|
|
605
|
+
patterns[key] = Array.from(counts.entries())
|
|
606
|
+
.sort((a, b) => b[1] - a[1])
|
|
607
|
+
.map(e => e[0])
|
|
608
|
+
.slice(0, 5);
|
|
609
|
+
}
|
|
610
|
+
return patterns;
|
|
611
|
+
}
|
|
612
|
+
getProjectStructureContext() {
|
|
613
|
+
let context = 'PROJECT STRUCTURE PATTERNS DETECTED:\n';
|
|
614
|
+
context += '='.repeat(50) + '\n\n';
|
|
615
|
+
if (this.projectPatterns.serviceLocations.length > 0) {
|
|
616
|
+
context += 'SERVICE FILES LOCATION:\n';
|
|
617
|
+
for (const loc of this.projectPatterns.serviceLocations.slice(0, 3)) {
|
|
618
|
+
context += ` - ${loc}/\n`;
|
|
619
|
+
}
|
|
620
|
+
context += '\n';
|
|
621
|
+
}
|
|
622
|
+
if (this.projectPatterns.uiLocations.length > 0) {
|
|
623
|
+
context += 'UI COMPONENTS LOCATION:\n';
|
|
624
|
+
for (const loc of this.projectPatterns.uiLocations.slice(0, 3)) {
|
|
625
|
+
context += ` - ${loc}/\n`;
|
|
626
|
+
}
|
|
627
|
+
context += '\n';
|
|
628
|
+
}
|
|
629
|
+
if (this.projectPatterns.logicLocations.length > 0) {
|
|
630
|
+
context += 'LOGIC/CONTROLLER FILES LOCATION:\n';
|
|
631
|
+
for (const loc of this.projectPatterns.logicLocations.slice(0, 3)) {
|
|
632
|
+
context += ` - ${loc}/\n`;
|
|
633
|
+
}
|
|
634
|
+
context += '\n';
|
|
635
|
+
}
|
|
636
|
+
if (this.projectPatterns.configLocations.length > 0) {
|
|
637
|
+
context += 'CONFIGURATION FILES LOCATION:\n';
|
|
638
|
+
for (const loc of this.projectPatterns.configLocations.slice(0, 3)) {
|
|
639
|
+
context += ` - ${loc}/\n`;
|
|
640
|
+
}
|
|
641
|
+
context += '\n';
|
|
642
|
+
}
|
|
643
|
+
if (this.projectPatterns.styleLocations.length > 0) {
|
|
644
|
+
context += 'STYLES/CSS FILES LOCATION:\n';
|
|
645
|
+
for (const loc of this.projectPatterns.styleLocations.slice(0, 3)) {
|
|
646
|
+
context += ` - ${loc}/\n`;
|
|
647
|
+
}
|
|
648
|
+
context += '\n';
|
|
649
|
+
}
|
|
650
|
+
if (this.projectPatterns.apiLocations.length > 0) {
|
|
651
|
+
context += 'API/SERVER FILES LOCATION:\n';
|
|
652
|
+
for (const loc of this.projectPatterns.apiLocations.slice(0, 3)) {
|
|
653
|
+
context += ` - ${loc}/\n`;
|
|
654
|
+
}
|
|
655
|
+
context += '\n';
|
|
656
|
+
}
|
|
657
|
+
context += 'EXAMPLE FILES TO FOLLOW PATTERNS FROM:\n';
|
|
658
|
+
try {
|
|
659
|
+
const examples = this.findExampleFiles();
|
|
660
|
+
for (const [category, files] of Object.entries(examples)) {
|
|
661
|
+
if (files.length > 0) {
|
|
662
|
+
context += `\n${category}:\n`;
|
|
663
|
+
for (const f of files.slice(0, 2)) {
|
|
664
|
+
context += ` - ${f}\n`;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
catch {
|
|
670
|
+
// Ignore errors
|
|
671
|
+
}
|
|
672
|
+
context += '\nREMEMBER: When creating new features, create ALL necessary files following these patterns!\n';
|
|
673
|
+
return context;
|
|
674
|
+
}
|
|
675
|
+
findExampleFiles() {
|
|
676
|
+
const examples = {
|
|
677
|
+
'Services': [],
|
|
678
|
+
'UI Components': [],
|
|
679
|
+
'Logic Files': [],
|
|
680
|
+
'API Routes': [],
|
|
681
|
+
};
|
|
682
|
+
try {
|
|
683
|
+
for (const filePath of this.iterateProjectFiles()) {
|
|
684
|
+
const relPath = path.relative(this.projectPath, filePath).replace(/\\/g, '/');
|
|
685
|
+
const ext = path.extname(filePath);
|
|
686
|
+
if (relPath.toLowerCase().includes('service') && ['.js', '.ts', '.py'].includes(ext)) {
|
|
687
|
+
examples['Services'].push(relPath);
|
|
688
|
+
}
|
|
689
|
+
if (['.tsx', '.jsx'].includes(ext) && (['pages', 'components'].some(p => relPath.includes(p)))) {
|
|
690
|
+
examples['UI Components'].push(relPath);
|
|
691
|
+
}
|
|
692
|
+
if (relPath.toLowerCase().includes('logic') && ['.js', '.ts', '.py'].includes(ext)) {
|
|
693
|
+
examples['Logic Files'].push(relPath);
|
|
694
|
+
}
|
|
695
|
+
if (['server', 'api'].some(p => relPath.toLowerCase().includes(p)) && ['.js', '.ts', '.py'].includes(ext)) {
|
|
696
|
+
examples['API Routes'].push(relPath);
|
|
697
|
+
}
|
|
698
|
+
// Limit to 2 per category
|
|
699
|
+
for (const key of Object.keys(examples)) {
|
|
700
|
+
if (examples[key].length >= 2) {
|
|
701
|
+
examples[key] = examples[key].slice(0, 2);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
catch {
|
|
707
|
+
// Ignore errors
|
|
708
|
+
}
|
|
709
|
+
return examples;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
// ============================================================================
|
|
713
|
+
// SINGLETON INSTANCE
|
|
714
|
+
// ============================================================================
|
|
715
|
+
let builderInstance = null;
|
|
716
|
+
export function getContextBuilder(projectPath) {
|
|
717
|
+
if (!builderInstance || (projectPath && builderInstance['projectPath'] !== path.resolve(projectPath))) {
|
|
718
|
+
builderInstance = new ContextBuilder(projectPath || process.cwd());
|
|
719
|
+
}
|
|
720
|
+
return builderInstance;
|
|
721
|
+
}
|
|
722
|
+
export function resetContextBuilder() {
|
|
723
|
+
builderInstance = null;
|
|
724
|
+
}
|
|
725
|
+
//# sourceMappingURL=contextBuilder.js.map
|