cntx-ui 3.0.7 → 3.0.9
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/bin/cntx-ui.js +70 -0
- package/dist/lib/agent-runtime.js +269 -0
- package/dist/lib/agent-tools.js +162 -0
- package/dist/lib/api-router.js +387 -0
- package/dist/lib/bundle-manager.js +236 -0
- package/dist/lib/configuration-manager.js +230 -0
- package/dist/lib/database-manager.js +277 -0
- package/dist/lib/file-system-manager.js +305 -0
- package/dist/lib/function-level-chunker.js +144 -0
- package/dist/lib/heuristics-manager.js +491 -0
- package/dist/lib/mcp-server.js +159 -0
- package/dist/lib/mcp-transport.js +10 -0
- package/dist/lib/semantic-splitter.js +335 -0
- package/dist/lib/simple-vector-store.js +98 -0
- package/dist/lib/treesitter-semantic-chunker.js +277 -0
- package/dist/lib/websocket-manager.js +268 -0
- package/dist/server.js +225 -0
- package/package.json +18 -8
- package/bin/cntx-ui-mcp.sh +0 -3
- package/bin/cntx-ui.js +0 -123
- package/lib/agent-runtime.js +0 -371
- package/lib/agent-tools.js +0 -370
- package/lib/api-router.js +0 -1026
- package/lib/bundle-manager.js +0 -326
- package/lib/configuration-manager.js +0 -760
- package/lib/database-manager.js +0 -397
- package/lib/file-system-manager.js +0 -489
- package/lib/function-level-chunker.js +0 -406
- package/lib/heuristics-manager.js +0 -529
- package/lib/mcp-server.js +0 -1380
- package/lib/mcp-transport.js +0 -97
- package/lib/semantic-splitter.js +0 -304
- package/lib/simple-vector-store.js +0 -108
- package/lib/treesitter-semantic-chunker.js +0 -1485
- package/lib/websocket-manager.js +0 -470
- package/server.js +0 -687
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Heuristics Manager - Centralized service for loading and applying heuristics
|
|
3
|
+
* Manages configuration-based code categorization logic
|
|
4
|
+
*/
|
|
5
|
+
import { readFileSync, writeFileSync, existsSync, watchFile } from 'fs';
|
|
6
|
+
export default class HeuristicsManager {
|
|
7
|
+
configPath;
|
|
8
|
+
config;
|
|
9
|
+
cache;
|
|
10
|
+
isWatching;
|
|
11
|
+
lastLoaded;
|
|
12
|
+
constructor(configPath = './heuristics-config.json') {
|
|
13
|
+
this.configPath = configPath;
|
|
14
|
+
this.config = null;
|
|
15
|
+
this.cache = new Map();
|
|
16
|
+
this.isWatching = false;
|
|
17
|
+
this.lastLoaded = null;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Load heuristics configuration from file
|
|
21
|
+
*/
|
|
22
|
+
loadConfig() {
|
|
23
|
+
try {
|
|
24
|
+
if (!existsSync(this.configPath)) {
|
|
25
|
+
console.warn(`Heuristics config not found at ${this.configPath}, using fallback`);
|
|
26
|
+
this.config = this.getFallbackConfig();
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const configContent = readFileSync(this.configPath, 'utf8');
|
|
30
|
+
const newConfig = JSON.parse(configContent);
|
|
31
|
+
// Validate config structure
|
|
32
|
+
this.validateConfig(newConfig);
|
|
33
|
+
this.config = newConfig;
|
|
34
|
+
this.lastLoaded = Date.now();
|
|
35
|
+
this.cache.clear(); // Clear cache when config changes
|
|
36
|
+
// Set up file watching if not already watching
|
|
37
|
+
if (!this.isWatching) {
|
|
38
|
+
this.setupFileWatcher();
|
|
39
|
+
}
|
|
40
|
+
console.log('✅ Heuristics configuration loaded successfully');
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
console.error('❌ Failed to load heuristics config:', error.message);
|
|
44
|
+
console.log('📦 Falling back to hardcoded heuristics');
|
|
45
|
+
this.config = this.getFallbackConfig();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Validate heuristics configuration structure
|
|
50
|
+
*/
|
|
51
|
+
validateConfig(config) {
|
|
52
|
+
const required = ['purposeHeuristics', 'bundleHeuristics', 'semanticTypeMapping'];
|
|
53
|
+
for (const field of required) {
|
|
54
|
+
if (!config[field]) {
|
|
55
|
+
throw new Error(`Missing required field: ${field}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Validate purpose heuristics structure
|
|
59
|
+
if (!config.purposeHeuristics.patterns || !config.purposeHeuristics.fallback) {
|
|
60
|
+
throw new Error('Invalid purposeHeuristics structure');
|
|
61
|
+
}
|
|
62
|
+
// Validate bundle heuristics structure
|
|
63
|
+
if (!config.bundleHeuristics.patterns || !config.bundleHeuristics.fallback) {
|
|
64
|
+
throw new Error('Invalid bundleHeuristics structure');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Set up file watcher for config changes
|
|
69
|
+
*/
|
|
70
|
+
setupFileWatcher() {
|
|
71
|
+
if (!existsSync(this.configPath))
|
|
72
|
+
return;
|
|
73
|
+
watchFile(this.configPath, (curr, prev) => {
|
|
74
|
+
if (curr.mtime !== prev.mtime) {
|
|
75
|
+
console.log('📝 Heuristics config file changed, reloading...');
|
|
76
|
+
this.loadConfig();
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
this.isWatching = true;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Get configuration, loading if necessary
|
|
83
|
+
*/
|
|
84
|
+
getConfig() {
|
|
85
|
+
if (!this.config) {
|
|
86
|
+
this.loadConfig();
|
|
87
|
+
}
|
|
88
|
+
return this.config;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Determine function purpose using configured heuristics
|
|
92
|
+
*/
|
|
93
|
+
determinePurpose(func) {
|
|
94
|
+
const config = this.getConfig();
|
|
95
|
+
const name = (func.name || '').toLowerCase();
|
|
96
|
+
const pathParts = func.pathParts || [];
|
|
97
|
+
// Check each purpose pattern
|
|
98
|
+
for (const [patternName, pattern] of Object.entries(config.purposeHeuristics.patterns)) {
|
|
99
|
+
if (this.evaluateConditions(pattern.conditions, { func, name, pathParts })) {
|
|
100
|
+
return pattern.purpose;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// Return fallback
|
|
104
|
+
return config.purposeHeuristics.fallback.purpose;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Infer business domains (e.g. auth, editing, file-mgmt)
|
|
108
|
+
*/
|
|
109
|
+
inferBusinessDomains(func) {
|
|
110
|
+
const domains = new Set();
|
|
111
|
+
const name = (func.name || '').toLowerCase();
|
|
112
|
+
const path = (func.pathParts || []).join('/').toLowerCase();
|
|
113
|
+
const imports = (func.includes?.imports || []).filter((i) => typeof i === 'string');
|
|
114
|
+
// Path-based domains
|
|
115
|
+
if (path.includes('auth'))
|
|
116
|
+
domains.add('authentication');
|
|
117
|
+
if (path.includes('component') || path.includes('ui'))
|
|
118
|
+
domains.add('ui-layer');
|
|
119
|
+
if (path.includes('service') || path.includes('api'))
|
|
120
|
+
domains.add('api-integration');
|
|
121
|
+
if (path.includes('test') || path.includes('spec'))
|
|
122
|
+
domains.add('testing');
|
|
123
|
+
// Import-based domains
|
|
124
|
+
if (imports.some(i => i.includes('tauri')))
|
|
125
|
+
domains.add('desktop-runtime');
|
|
126
|
+
if (imports.some(i => i.includes('tiptap') || i.includes('prosemirror')))
|
|
127
|
+
domains.add('text-editing');
|
|
128
|
+
if (imports.some(i => i.includes('react')))
|
|
129
|
+
domains.add('frontend-ui');
|
|
130
|
+
// Name-based domains
|
|
131
|
+
if (/file|save|export|read|write/i.test(name))
|
|
132
|
+
domains.add('file-management');
|
|
133
|
+
if (/login|user|session/i.test(name))
|
|
134
|
+
domains.add('authentication');
|
|
135
|
+
return Array.from(domains);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Infer technical patterns (e.g. hooks, async-io, event-handlers)
|
|
139
|
+
*/
|
|
140
|
+
inferTechnicalPatterns(func) {
|
|
141
|
+
const patterns = new Set();
|
|
142
|
+
const name = (func.name || '').toLowerCase();
|
|
143
|
+
const code = func.code || '';
|
|
144
|
+
if (name.startsWith('use'))
|
|
145
|
+
patterns.add('react-hooks');
|
|
146
|
+
if (code.includes('async') || code.includes('await'))
|
|
147
|
+
patterns.add('async-io');
|
|
148
|
+
if (code.includes('on(') || code.includes('addListener') || name.startsWith('handle'))
|
|
149
|
+
patterns.add('event-driven');
|
|
150
|
+
if (code.includes('new ') || code.includes('class '))
|
|
151
|
+
patterns.add('object-oriented');
|
|
152
|
+
if (func.isExported)
|
|
153
|
+
patterns.add('public-api');
|
|
154
|
+
return Array.from(patterns);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Suggest bundles for file using configured heuristics
|
|
158
|
+
*/
|
|
159
|
+
suggestBundlesForFile(filePath) {
|
|
160
|
+
const config = this.getConfig();
|
|
161
|
+
const fileName = filePath.toLowerCase();
|
|
162
|
+
const pathParts = fileName.split('/');
|
|
163
|
+
const suggestions = [];
|
|
164
|
+
// Check each bundle pattern
|
|
165
|
+
for (const [patternName, pattern] of Object.entries(config.bundleHeuristics.patterns)) {
|
|
166
|
+
if (this.evaluateConditions(pattern.conditions, { fileName, filePath, pathParts })) {
|
|
167
|
+
suggestions.push(pattern.bundle);
|
|
168
|
+
// Check sub-patterns
|
|
169
|
+
if (pattern.subPatterns) {
|
|
170
|
+
for (const [subName, subPattern] of Object.entries(pattern.subPatterns)) {
|
|
171
|
+
if (this.evaluateConditions(subPattern.conditions, { fileName, filePath, pathParts })) {
|
|
172
|
+
suggestions.push(subPattern.bundle);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// Apply fallback logic if no suggestions
|
|
179
|
+
if (suggestions.length === 0) {
|
|
180
|
+
const fallback = config.bundleHeuristics.fallback;
|
|
181
|
+
if (fallback.webFallback && this.evaluateConditions(fallback.webFallback.conditions, { fileName, filePath, pathParts })) {
|
|
182
|
+
suggestions.push(fallback.webFallback.bundle);
|
|
183
|
+
}
|
|
184
|
+
else if (fallback.defaultFallback) {
|
|
185
|
+
suggestions.push(...fallback.defaultFallback.bundles);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return [...new Set(suggestions)]; // Remove duplicates
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Get semantic type cluster mapping
|
|
192
|
+
*/
|
|
193
|
+
getSemanticTypeMapping() {
|
|
194
|
+
const config = this.getConfig();
|
|
195
|
+
const mapping = {};
|
|
196
|
+
for (const [clusterName, cluster] of Object.entries(config.semanticTypeMapping.clusters)) {
|
|
197
|
+
for (const type of cluster.types) {
|
|
198
|
+
mapping[type] = cluster.clusterId;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return mapping;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Evaluate condition strings against context
|
|
205
|
+
*/
|
|
206
|
+
evaluateConditions(conditions, context) {
|
|
207
|
+
const conds = Array.isArray(conditions) ? conditions : [conditions];
|
|
208
|
+
// For purpose heuristics, we generally want high precision, so use AND logic
|
|
209
|
+
// if there are multiple conditions
|
|
210
|
+
const needsAndLogic = conds.length > 1 || this.requiresAndLogic(conds);
|
|
211
|
+
if (needsAndLogic) {
|
|
212
|
+
return conds.every(condition => {
|
|
213
|
+
try {
|
|
214
|
+
return this.evaluateCondition(condition, context);
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
console.warn(`Failed to evaluate condition: ${condition}`, error);
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
return conds.some(condition => {
|
|
224
|
+
try {
|
|
225
|
+
return this.evaluateCondition(condition, context);
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
console.warn(`Failed to evaluate condition: ${condition}`, error);
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Determine if conditions require AND logic vs OR logic
|
|
236
|
+
*/
|
|
237
|
+
requiresAndLogic(conditions) {
|
|
238
|
+
// React hook pattern specifically needs AND logic
|
|
239
|
+
if (conditions.length === 2 &&
|
|
240
|
+
conditions.some(c => c.includes('name.startsWith')) &&
|
|
241
|
+
conditions.some(c => c.includes('func.type'))) {
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
// Frontend pattern needs AND logic for web + src
|
|
245
|
+
if (conditions.length === 2 &&
|
|
246
|
+
conditions.some(c => c.includes("pathParts.includes('web')")) &&
|
|
247
|
+
conditions.some(c => c.includes("pathParts.includes('src')"))) {
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
// Default to OR logic for other patterns
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Evaluate a single condition
|
|
255
|
+
*/
|
|
256
|
+
evaluateCondition(condition, context) {
|
|
257
|
+
const { func, name, fileName, filePath, pathParts } = context;
|
|
258
|
+
// Handle function type conditions
|
|
259
|
+
if (condition.includes('func.type ===')) {
|
|
260
|
+
const typeMatch = condition.match(/func\.type === ['"]([^'"]+)['"]/);
|
|
261
|
+
if (typeMatch && func) {
|
|
262
|
+
return func.type === typeMatch[1];
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
// Handle name-based conditions
|
|
266
|
+
if (condition.includes('name.startsWith(')) {
|
|
267
|
+
const prefixMatch = condition.match(/name\.startsWith\(['"]([^'"]+)['"]\)/);
|
|
268
|
+
if (prefixMatch && name) {
|
|
269
|
+
return name.startsWith(prefixMatch[1]);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
if (condition.includes('name.includes(')) {
|
|
273
|
+
const includesMatch = condition.match(/name\.includes\(['"]([^'"]+)['"]\)/);
|
|
274
|
+
if (includesMatch && name) {
|
|
275
|
+
return name.includes(includesMatch[1]);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// Handle fileName conditions
|
|
279
|
+
if (condition.includes('fileName.includes(')) {
|
|
280
|
+
const includesMatch = condition.match(/fileName\.includes\(['"]([^'"]+)['"]\)/);
|
|
281
|
+
if (includesMatch && fileName) {
|
|
282
|
+
return fileName.includes(includesMatch[1]);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (condition.includes('fileName.endsWith(')) {
|
|
286
|
+
const endsWithMatch = condition.match(/fileName\.endsWith\(['"]([^'"]+)['"]\)/);
|
|
287
|
+
if (endsWithMatch && fileName) {
|
|
288
|
+
return fileName.endsWith(endsWithMatch[1]);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
// Handle pathParts conditions
|
|
292
|
+
if (condition.includes('pathParts.includes(')) {
|
|
293
|
+
const includesMatch = condition.match(/pathParts\.includes\(['"]([^'"]+)['"]\)/);
|
|
294
|
+
if (includesMatch && pathParts) {
|
|
295
|
+
return pathParts.includes(includesMatch[1]);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
// New: Check imports in the chunk context
|
|
299
|
+
if (condition.includes('chunk.imports.includes(')) {
|
|
300
|
+
const importMatch = condition.match(/chunk\.imports\.includes\(['"]([^'"]+)['"]\)/);
|
|
301
|
+
if (importMatch && func?.includes?.imports) {
|
|
302
|
+
return func.includes.imports
|
|
303
|
+
.filter((i) => typeof i === 'string')
|
|
304
|
+
.some((imp) => imp.includes(importMatch[1]));
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
// New: Check for specific naming patterns (case-insensitive)
|
|
308
|
+
if (condition.includes('name.matches(')) {
|
|
309
|
+
const regexMatch = condition.match(/name\.matches\(['"]([^'"]+)['"]\)/);
|
|
310
|
+
if (regexMatch && name) {
|
|
311
|
+
return new RegExp(regexMatch[1], 'i').test(name);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Fallback configuration for when config file is unavailable
|
|
318
|
+
*/
|
|
319
|
+
getFallbackConfig() {
|
|
320
|
+
return {
|
|
321
|
+
version: "1.0.0",
|
|
322
|
+
purposeHeuristics: {
|
|
323
|
+
patterns: {
|
|
324
|
+
reactComponent: {
|
|
325
|
+
conditions: ["func.type === 'react_component'"],
|
|
326
|
+
purpose: "React component",
|
|
327
|
+
confidence: 0.95
|
|
328
|
+
},
|
|
329
|
+
reactHook: {
|
|
330
|
+
conditions: ["name.startsWith('use')", "func.type === 'function'"],
|
|
331
|
+
purpose: "React hook",
|
|
332
|
+
confidence: 0.9
|
|
333
|
+
},
|
|
334
|
+
serviceLayer: {
|
|
335
|
+
conditions: ["pathParts.includes('services')"],
|
|
336
|
+
purpose: "Service layer logic",
|
|
337
|
+
confidence: 0.7
|
|
338
|
+
},
|
|
339
|
+
componentLayer: {
|
|
340
|
+
conditions: ["pathParts.includes('components')"],
|
|
341
|
+
purpose: "UI component logic",
|
|
342
|
+
confidence: 0.7
|
|
343
|
+
},
|
|
344
|
+
tauriCommand: {
|
|
345
|
+
conditions: ["name.matches('command')", "chunk.imports.includes('tauri')"],
|
|
346
|
+
purpose: "Tauri backend command",
|
|
347
|
+
confidence: 0.9
|
|
348
|
+
},
|
|
349
|
+
textEditing: {
|
|
350
|
+
conditions: ["chunk.imports.includes('tiptap')", "chunk.imports.includes('prosemirror')"],
|
|
351
|
+
purpose: "Rich text editing logic",
|
|
352
|
+
confidence: 0.95
|
|
353
|
+
},
|
|
354
|
+
fileManagement: {
|
|
355
|
+
conditions: ["pathParts.includes('services')", "name.matches('file|export|save')"],
|
|
356
|
+
purpose: "File system service",
|
|
357
|
+
confidence: 0.85
|
|
358
|
+
},
|
|
359
|
+
uiComponent: {
|
|
360
|
+
conditions: ["pathParts.includes('components')", "name.matches('modal|button|menu|bar')"],
|
|
361
|
+
purpose: "UI component logic",
|
|
362
|
+
confidence: 0.85
|
|
363
|
+
},
|
|
364
|
+
apiHandler: {
|
|
365
|
+
conditions: ["name.includes('api')", "name.includes('endpoint')"],
|
|
366
|
+
purpose: "API handler",
|
|
367
|
+
confidence: 0.85
|
|
368
|
+
},
|
|
369
|
+
dataRetrieval: {
|
|
370
|
+
conditions: ["name.includes('get')", "name.includes('fetch')"],
|
|
371
|
+
purpose: "Data retrieval",
|
|
372
|
+
confidence: 0.8
|
|
373
|
+
},
|
|
374
|
+
dataCreation: {
|
|
375
|
+
conditions: ["name.includes('create')", "name.includes('add')"],
|
|
376
|
+
purpose: "Data creation",
|
|
377
|
+
confidence: 0.8
|
|
378
|
+
},
|
|
379
|
+
dataModification: {
|
|
380
|
+
conditions: ["name.includes('update')", "name.includes('edit')"],
|
|
381
|
+
purpose: "Data modification",
|
|
382
|
+
confidence: 0.8
|
|
383
|
+
},
|
|
384
|
+
dataDeletion: {
|
|
385
|
+
conditions: ["name.includes('delete')", "name.includes('remove')"],
|
|
386
|
+
purpose: "Data deletion",
|
|
387
|
+
confidence: 0.8
|
|
388
|
+
},
|
|
389
|
+
validation: {
|
|
390
|
+
conditions: ["name.includes('validate')", "name.includes('check')"],
|
|
391
|
+
purpose: "Validation",
|
|
392
|
+
confidence: 0.75
|
|
393
|
+
},
|
|
394
|
+
dataProcessing: {
|
|
395
|
+
conditions: ["name.includes('parse')", "name.includes('format')"],
|
|
396
|
+
purpose: "Data processing",
|
|
397
|
+
confidence: 0.75
|
|
398
|
+
}
|
|
399
|
+
},
|
|
400
|
+
fallback: {
|
|
401
|
+
purpose: "Utility function",
|
|
402
|
+
confidence: 0.5
|
|
403
|
+
}
|
|
404
|
+
},
|
|
405
|
+
bundleHeuristics: {
|
|
406
|
+
patterns: {
|
|
407
|
+
frontend: {
|
|
408
|
+
conditions: ["pathParts.includes('web')", "pathParts.includes('src')"],
|
|
409
|
+
bundle: "frontend",
|
|
410
|
+
confidence: 0.8,
|
|
411
|
+
subPatterns: {
|
|
412
|
+
uiComponents: {
|
|
413
|
+
conditions: ["pathParts.includes('components')"],
|
|
414
|
+
bundle: "ui-components",
|
|
415
|
+
confidence: 0.9
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
},
|
|
419
|
+
server: {
|
|
420
|
+
conditions: ["fileName.includes('server')", "fileName.includes('api')", "pathParts.includes('bin')"],
|
|
421
|
+
bundle: "server",
|
|
422
|
+
confidence: 0.85
|
|
423
|
+
},
|
|
424
|
+
configuration: {
|
|
425
|
+
conditions: ["fileName.includes('config')", "fileName.includes('setup')", "fileName.endsWith('.json')", "fileName.endsWith('.sh')", "fileName.includes('package')"],
|
|
426
|
+
bundle: "config",
|
|
427
|
+
confidence: 0.9
|
|
428
|
+
},
|
|
429
|
+
documentation: {
|
|
430
|
+
conditions: ["fileName.endsWith('.md')", "fileName.includes('doc')", "fileName.includes('readme')"],
|
|
431
|
+
bundle: "docs",
|
|
432
|
+
confidence: 0.95
|
|
433
|
+
}
|
|
434
|
+
},
|
|
435
|
+
fallback: {
|
|
436
|
+
webFallback: {
|
|
437
|
+
conditions: ["pathParts.includes('web')"],
|
|
438
|
+
bundle: "frontend",
|
|
439
|
+
confidence: 0.6
|
|
440
|
+
},
|
|
441
|
+
defaultFallback: {
|
|
442
|
+
bundles: ["server", "config"],
|
|
443
|
+
confidence: 0.4
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
},
|
|
447
|
+
semanticTypeMapping: {
|
|
448
|
+
clusters: {
|
|
449
|
+
businessLogic: { types: ["business_logic", "algorithm"], clusterId: 0 },
|
|
450
|
+
dataLayer: { types: ["data_processing", "database"], clusterId: 1 },
|
|
451
|
+
apiLayer: { types: ["api_integration", "middleware", "routing"], clusterId: 2 },
|
|
452
|
+
uiLayer: { types: ["ui_component", "page_component", "layout_component", "hook"], clusterId: 3 },
|
|
453
|
+
utilities: { types: ["utility", "configuration", "function", "type_definition"], clusterId: 4 },
|
|
454
|
+
testing: { types: ["testing", "documentation", "monitoring"], clusterId: 5 },
|
|
455
|
+
infrastructure: { types: ["error_handling", "performance", "security"], clusterId: 6 },
|
|
456
|
+
unknown: { types: ["unknown"], clusterId: 7 }
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Update configuration (for API endpoints) and persist to disk
|
|
463
|
+
*/
|
|
464
|
+
async updateConfig(newConfig) {
|
|
465
|
+
try {
|
|
466
|
+
this.validateConfig(newConfig);
|
|
467
|
+
// Write to disk
|
|
468
|
+
writeFileSync(this.configPath, JSON.stringify(newConfig, null, 2), 'utf8');
|
|
469
|
+
this.config = newConfig;
|
|
470
|
+
this.cache.clear();
|
|
471
|
+
this.lastLoaded = Date.now();
|
|
472
|
+
console.log('📝 Heuristics configuration updated and saved to disk');
|
|
473
|
+
return true;
|
|
474
|
+
}
|
|
475
|
+
catch (error) {
|
|
476
|
+
console.error('❌ Failed to update heuristics config:', error.message);
|
|
477
|
+
throw error;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Get performance metrics
|
|
482
|
+
*/
|
|
483
|
+
getPerformanceMetrics() {
|
|
484
|
+
// TODO: Implement performance tracking
|
|
485
|
+
return {
|
|
486
|
+
totalEvaluations: 0,
|
|
487
|
+
accuracyScore: 0.0,
|
|
488
|
+
lastUpdated: this.lastLoaded
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model Context Protocol (MCP) Server Implementation
|
|
3
|
+
* Exposes repository intelligence tools to AI agents
|
|
4
|
+
*/
|
|
5
|
+
import { readFileSync } from 'fs';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
export class MCPServer {
|
|
8
|
+
cntxServer;
|
|
9
|
+
constructor(cntxServer) {
|
|
10
|
+
this.cntxServer = cntxServer;
|
|
11
|
+
// Listen for MCP requests on stdin
|
|
12
|
+
process.stdin.on('data', (data) => {
|
|
13
|
+
this.handleInput(data.toString());
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
handleInput(input) {
|
|
17
|
+
try {
|
|
18
|
+
const lines = input.split('\n').filter(l => l.trim());
|
|
19
|
+
for (const line of lines) {
|
|
20
|
+
const request = JSON.parse(line);
|
|
21
|
+
this.routeRequest(request);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
catch (e) {
|
|
25
|
+
// Ignore invalid JSON
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async routeRequest(request) {
|
|
29
|
+
const { method, params, id } = request;
|
|
30
|
+
switch (method) {
|
|
31
|
+
case 'initialize':
|
|
32
|
+
return this.sendResponse(this.createSuccessResponse(id, {
|
|
33
|
+
protocolVersion: '2024-11-05',
|
|
34
|
+
capabilities: {
|
|
35
|
+
tools: {},
|
|
36
|
+
resources: {},
|
|
37
|
+
prompts: {}
|
|
38
|
+
},
|
|
39
|
+
serverInfo: { name: 'cntx-ui', version: '3.0.0' }
|
|
40
|
+
}));
|
|
41
|
+
case 'tools/list':
|
|
42
|
+
return this.sendResponse(this.handleListTools(id));
|
|
43
|
+
case 'tools/call':
|
|
44
|
+
return this.sendResponse(await this.handleCallTool(params, id));
|
|
45
|
+
case 'resources/list':
|
|
46
|
+
return this.sendResponse(this.handleListResources(id));
|
|
47
|
+
case 'prompts/list':
|
|
48
|
+
return this.sendResponse(this.handleListPrompts(id));
|
|
49
|
+
case 'prompts/get':
|
|
50
|
+
return this.sendResponse(this.handleGetPrompt(params, id));
|
|
51
|
+
default:
|
|
52
|
+
return this.sendResponse(this.createErrorResponse(id, -32601, `Method not found: ${method}`));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
sendResponse(response) {
|
|
56
|
+
process.stdout.write(JSON.stringify(response) + '\n');
|
|
57
|
+
}
|
|
58
|
+
createSuccessResponse(id, result) {
|
|
59
|
+
return { jsonrpc: '2.0', id, result };
|
|
60
|
+
}
|
|
61
|
+
createErrorResponse(id, code, message, data = null) {
|
|
62
|
+
const error = { code, message };
|
|
63
|
+
if (data)
|
|
64
|
+
error.data = data;
|
|
65
|
+
return { jsonrpc: '2.0', id, error };
|
|
66
|
+
}
|
|
67
|
+
// --- Handlers ---
|
|
68
|
+
handleListTools(id) {
|
|
69
|
+
const tools = this.getToolDefinitions();
|
|
70
|
+
return this.createSuccessResponse(id, { tools });
|
|
71
|
+
}
|
|
72
|
+
async handleCallTool(params, id) {
|
|
73
|
+
const { name, arguments: args } = params;
|
|
74
|
+
try {
|
|
75
|
+
if (name.startsWith('agent/')) {
|
|
76
|
+
const mode = name.split('/')[1];
|
|
77
|
+
let result;
|
|
78
|
+
switch (mode) {
|
|
79
|
+
case 'discover':
|
|
80
|
+
result = await this.cntxServer.agentRuntime.discoverCodebase(args);
|
|
81
|
+
break;
|
|
82
|
+
case 'query':
|
|
83
|
+
result = await this.cntxServer.agentRuntime.answerQuery(args.question, args);
|
|
84
|
+
break;
|
|
85
|
+
case 'investigate':
|
|
86
|
+
result = await this.cntxServer.agentRuntime.investigateFeature(args.feature, args);
|
|
87
|
+
break;
|
|
88
|
+
default:
|
|
89
|
+
return this.createErrorResponse(id, -32602, `Unknown agent tool: ${name}`);
|
|
90
|
+
}
|
|
91
|
+
return this.createSuccessResponse(id, { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] });
|
|
92
|
+
}
|
|
93
|
+
// Legacy tools mapping
|
|
94
|
+
switch (name) {
|
|
95
|
+
case 'list_bundles':
|
|
96
|
+
const bundles = this.cntxServer.bundleManager.getAllBundleInfo();
|
|
97
|
+
return this.createSuccessResponse(id, { content: [{ type: 'text', text: JSON.stringify(bundles, null, 2) }] });
|
|
98
|
+
case 'read_file':
|
|
99
|
+
return await this.toolReadFile(args, id);
|
|
100
|
+
default:
|
|
101
|
+
return this.createErrorResponse(id, -32602, `Tool not found: ${name}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
catch (e) {
|
|
105
|
+
return this.createErrorResponse(id, -32603, e.message);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async toolReadFile(args, id) {
|
|
109
|
+
const { path: filePath } = args;
|
|
110
|
+
try {
|
|
111
|
+
const fullPath = join(this.cntxServer.CWD, filePath);
|
|
112
|
+
const content = readFileSync(fullPath, 'utf8');
|
|
113
|
+
return this.createSuccessResponse(id, {
|
|
114
|
+
content: [{ type: 'text', text: content }]
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
catch (e) {
|
|
118
|
+
return this.createErrorResponse(id, -32603, e.message);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
handleListResources(id) {
|
|
122
|
+
return this.createSuccessResponse(id, { resources: [] });
|
|
123
|
+
}
|
|
124
|
+
handleListPrompts(id) {
|
|
125
|
+
return this.createSuccessResponse(id, { prompts: [] });
|
|
126
|
+
}
|
|
127
|
+
handleGetPrompt(params, id) {
|
|
128
|
+
return this.createErrorResponse(id, -32602, 'Prompt not found');
|
|
129
|
+
}
|
|
130
|
+
getToolDefinitions() {
|
|
131
|
+
return [
|
|
132
|
+
{
|
|
133
|
+
name: 'agent/discover',
|
|
134
|
+
description: 'Discovery Mode: Comprehensive architectural overview.',
|
|
135
|
+
inputSchema: { type: 'object', properties: { scope: { type: 'string' } } }
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: 'agent/query',
|
|
139
|
+
description: 'Query Mode: Answer technical questions.',
|
|
140
|
+
inputSchema: { type: 'object', properties: { question: { type: 'string' } }, required: ['question'] }
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
name: 'agent/investigate',
|
|
144
|
+
description: 'Investigation Mode: Suggest integration points.',
|
|
145
|
+
inputSchema: { type: 'object', properties: { feature: { type: 'string' } }, required: ['feature'] }
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
name: 'list_bundles',
|
|
149
|
+
description: 'List all project bundles.',
|
|
150
|
+
inputSchema: { type: 'object', properties: {} }
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: 'read_file',
|
|
154
|
+
description: 'Read a file.',
|
|
155
|
+
inputSchema: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] }
|
|
156
|
+
}
|
|
157
|
+
];
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Stdio Transport
|
|
3
|
+
* Handles the low-level communication for the Model Context Protocol
|
|
4
|
+
*/
|
|
5
|
+
import { MCPServer } from './mcp-server.js';
|
|
6
|
+
export function startMCPTransport(cntxServer) {
|
|
7
|
+
// The MCPServer constructor already sets up stdin listener
|
|
8
|
+
const mcpServer = new MCPServer(cntxServer);
|
|
9
|
+
return mcpServer;
|
|
10
|
+
}
|