cntx-ui 3.1.4 → 3.1.13
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/lib/agent-runtime.js +166 -30
- package/dist/lib/api-router.js +6 -2
- package/dist/lib/mcp-server.js +4 -2
- package/dist/lib/semantic-splitter.js +59 -37
- package/dist/lib/simple-vector-store.js +1 -1
- package/dist/lib/websocket-manager.js +23 -8
- package/dist/server.js +55 -8
- package/package.json +1 -1
|
@@ -104,12 +104,12 @@ This agent is **stateful**. All interactions in this directory are logged to a p
|
|
|
104
104
|
* Now logs the discovery process to memory
|
|
105
105
|
*/
|
|
106
106
|
async discoverCodebase(options = {}) {
|
|
107
|
-
const { scope = 'all', includeDetails = true } = options;
|
|
107
|
+
const { scope = 'all', includeDetails = true, verbose = false } = options;
|
|
108
108
|
try {
|
|
109
109
|
await this.logInteraction('agent', `Starting codebase discovery for scope: ${scope}`);
|
|
110
110
|
const discovery = {
|
|
111
111
|
overview: await this.getCodebaseOverview(),
|
|
112
|
-
bundles: await this.analyzeBundles(scope),
|
|
112
|
+
bundles: await this.analyzeBundles(scope, verbose),
|
|
113
113
|
architecture: await this.analyzeArchitecture(),
|
|
114
114
|
patterns: await this.identifyPatterns(),
|
|
115
115
|
recommendations: []
|
|
@@ -133,19 +133,56 @@ This agent is **stateful**. All interactions in this directory are logged to a p
|
|
|
133
133
|
* Now recalls previous context from SQLite
|
|
134
134
|
*/
|
|
135
135
|
async answerQuery(question, options = {}) {
|
|
136
|
-
const { maxResults = 10, includeCode = false } = options;
|
|
136
|
+
const { maxResults = 10, includeCode = false, query } = options;
|
|
137
|
+
const actualQuestion = question || query;
|
|
138
|
+
if (!actualQuestion) {
|
|
139
|
+
throw new Error('Missing question or query for search.');
|
|
140
|
+
}
|
|
137
141
|
try {
|
|
138
|
-
await this.logInteraction('user',
|
|
142
|
+
await this.logInteraction('user', actualQuestion);
|
|
139
143
|
// Perform semantic search via Vector Store
|
|
140
|
-
|
|
144
|
+
let combinedResults = await this.cntxServer.vectorStore.search(actualQuestion, { limit: maxResults });
|
|
145
|
+
// Heuristic fallback for common onboarding questions if results are poor
|
|
146
|
+
const lowConfidence = combinedResults.length === 0 || combinedResults[0].similarity < 0.6;
|
|
147
|
+
const isEntryQuery = /entry|start|main|index|run/i.test(actualQuestion);
|
|
148
|
+
const isModelQuery = /model|schema|data|db|database/i.test(actualQuestion);
|
|
149
|
+
let fallbackFiles = [];
|
|
150
|
+
if (lowConfidence && (isEntryQuery || isModelQuery)) {
|
|
151
|
+
const allFiles = this.cntxServer.fileSystemManager.getAllFiles();
|
|
152
|
+
if (isEntryQuery) {
|
|
153
|
+
// Look for common entry points like main.tsx, main.rs, App.tsx, etc.
|
|
154
|
+
const entryPatterns = [/main\./i, /index\./i, /app\./i, /router\./i, /server\./i];
|
|
155
|
+
entryPatterns.forEach(pattern => {
|
|
156
|
+
fallbackFiles.push(...allFiles.filter(f => pattern.test(f)).slice(0, 3));
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
if (isModelQuery) {
|
|
160
|
+
const modelPatterns = [/model/i, /schema/i, /db/i, /database/i, /entity/i];
|
|
161
|
+
modelPatterns.forEach(pattern => {
|
|
162
|
+
fallbackFiles.push(...allFiles.filter(f => pattern.test(f)).slice(0, 3));
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
fallbackFiles = [...new Set(fallbackFiles)].slice(0, 8);
|
|
166
|
+
}
|
|
141
167
|
// Generate contextual answer
|
|
142
|
-
const answer = await this.generateContextualAnswer(question, {
|
|
168
|
+
const answer = await this.generateContextualAnswer(question, {
|
|
169
|
+
chunks: combinedResults,
|
|
170
|
+
files: fallbackFiles
|
|
171
|
+
}, includeCode);
|
|
172
|
+
// If no semantic results but we found fallbacks, improve the answer
|
|
173
|
+
if (combinedResults.length === 0 && fallbackFiles.length > 0) {
|
|
174
|
+
answer.response = `I couldn't find exact semantic matches, but based on common project structures, these files look relevant: ${fallbackFiles.join(', ')}`;
|
|
175
|
+
answer.confidence = 0.4;
|
|
176
|
+
}
|
|
143
177
|
const response = {
|
|
144
178
|
question,
|
|
145
179
|
answer: answer.response,
|
|
146
180
|
evidence: answer.evidence,
|
|
147
181
|
confidence: answer.confidence,
|
|
148
|
-
relatedFiles: [...new Set(
|
|
182
|
+
relatedFiles: [...new Set([
|
|
183
|
+
...combinedResults.map(c => c.filePath),
|
|
184
|
+
...fallbackFiles
|
|
185
|
+
])].slice(0, 8)
|
|
149
186
|
};
|
|
150
187
|
await this.logInteraction('agent', response.answer, { response });
|
|
151
188
|
return response;
|
|
@@ -158,14 +195,18 @@ This agent is **stateful**. All interactions in this directory are logged to a p
|
|
|
158
195
|
* Feature Investigation Mode: Now persists the investigation approach
|
|
159
196
|
*/
|
|
160
197
|
async investigateFeature(featureDescription, options = {}) {
|
|
161
|
-
const { includeRecommendations = true } = options;
|
|
198
|
+
const { includeRecommendations = true, feature, description, area } = options;
|
|
199
|
+
const actualDescription = featureDescription || feature || description || area;
|
|
200
|
+
if (!actualDescription) {
|
|
201
|
+
throw new Error('Missing feature description for investigation.');
|
|
202
|
+
}
|
|
162
203
|
try {
|
|
163
|
-
await this.logInteraction('user', `Investigating feature: ${
|
|
204
|
+
await this.logInteraction('user', `Investigating feature: ${actualDescription}`);
|
|
164
205
|
const investigation = {
|
|
165
|
-
feature:
|
|
166
|
-
existing: await this.findExistingImplementations(
|
|
167
|
-
related: await this.findRelatedCode(
|
|
168
|
-
integration: await this.findIntegrationPoints(
|
|
206
|
+
feature: actualDescription,
|
|
207
|
+
existing: await this.findExistingImplementations(actualDescription),
|
|
208
|
+
related: await this.findRelatedCode(actualDescription),
|
|
209
|
+
integration: await this.findIntegrationPoints(actualDescription)
|
|
169
210
|
};
|
|
170
211
|
if (includeRecommendations) {
|
|
171
212
|
investigation.approach = await this.suggestImplementationApproach(investigation);
|
|
@@ -180,8 +221,9 @@ This agent is **stateful**. All interactions in this directory are logged to a p
|
|
|
180
221
|
// --- Helper Methods ---
|
|
181
222
|
async getCodebaseOverview() {
|
|
182
223
|
const bundles = Array.from(this.cntxServer.bundleManager.getAllBundleInfo());
|
|
183
|
-
const totalFiles =
|
|
184
|
-
const
|
|
224
|
+
const totalFiles = this.cntxServer.fileSystemManager.getAllFiles().length;
|
|
225
|
+
const masterBundle = bundles.find(b => b.name === 'master');
|
|
226
|
+
const totalSize = masterBundle ? masterBundle.size : bundles.reduce((sum, b) => sum + b.size, 0);
|
|
185
227
|
return {
|
|
186
228
|
projectPath: this.cntxServer.CWD,
|
|
187
229
|
totalBundles: bundles.length,
|
|
@@ -190,19 +232,57 @@ This agent is **stateful**. All interactions in this directory are logged to a p
|
|
|
190
232
|
bundleNames: bundles.map(b => b.name)
|
|
191
233
|
};
|
|
192
234
|
}
|
|
193
|
-
async analyzeBundles(scope) {
|
|
235
|
+
async analyzeBundles(scope, verbose = false) {
|
|
194
236
|
const bundles = this.cntxServer.bundleManager.getAllBundleInfo();
|
|
195
237
|
const filtered = scope === 'all' ? bundles : bundles.filter(b => b.name === scope);
|
|
196
|
-
return filtered.map(b =>
|
|
197
|
-
|
|
198
|
-
purpose
|
|
199
|
-
|
|
238
|
+
return filtered.map(b => {
|
|
239
|
+
const files = b.files || [];
|
|
240
|
+
const purpose = this.inferBundlePurpose(b.name, files);
|
|
241
|
+
// Implement compact mode: only show top 5 files if not verbose
|
|
242
|
+
let displayFiles = files;
|
|
243
|
+
if (!verbose && files.length > 5) {
|
|
244
|
+
// Pick high-signal files: main, index, App, or just the first few
|
|
245
|
+
const keyFiles = files.filter(f => /main|index|app|router|api|models/i.test(f));
|
|
246
|
+
displayFiles = [...new Set([...keyFiles, ...files])].slice(0, 5);
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
...b,
|
|
250
|
+
purpose,
|
|
251
|
+
files: displayFiles,
|
|
252
|
+
totalFiles: files.length,
|
|
253
|
+
isTruncated: !verbose && files.length > 5
|
|
254
|
+
};
|
|
255
|
+
});
|
|
200
256
|
}
|
|
201
257
|
inferBundlePurpose(name, files) {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
258
|
+
const n = name.toLowerCase();
|
|
259
|
+
if (n.includes('component') || n.includes('ui') || n.includes('view') || n.includes('screen'))
|
|
260
|
+
return 'UI Components & Views';
|
|
261
|
+
if (n.includes('api') || n.includes('server') || n.includes('backend') || n.includes('netlify'))
|
|
262
|
+
return 'Backend API & Functions';
|
|
263
|
+
if (n.includes('hook'))
|
|
264
|
+
return 'React Hooks';
|
|
265
|
+
if (n.includes('util') || n.includes('helper'))
|
|
266
|
+
return 'Utility functions';
|
|
267
|
+
if (n.includes('lib') || n.includes('service') || n.includes('store'))
|
|
268
|
+
return 'Business logic & services';
|
|
269
|
+
if (n.includes('database') || n.includes('db') || n.includes('model') || n.includes('schema'))
|
|
270
|
+
return 'Data models & DB';
|
|
271
|
+
if (n.includes('test') || n.includes('spec'))
|
|
272
|
+
return 'Test suite';
|
|
273
|
+
if (n.includes('doc') || n.includes('readme'))
|
|
274
|
+
return 'Documentation';
|
|
275
|
+
if (n.includes('script') || n.includes('bin'))
|
|
276
|
+
return 'Scripts & CLI';
|
|
277
|
+
if (n.includes('asset') || n.includes('public'))
|
|
278
|
+
return 'Assets & static files';
|
|
279
|
+
if (n.includes('style') || n.includes('css'))
|
|
280
|
+
return 'Styles';
|
|
281
|
+
// Fallback to file extension analysis if name is generic
|
|
282
|
+
if (files.some(f => f.endsWith('.rs')))
|
|
283
|
+
return 'Rust Source';
|
|
284
|
+
if (files.some(f => f.endsWith('.ts') || f.endsWith('.tsx')))
|
|
285
|
+
return 'TypeScript Source';
|
|
206
286
|
return 'General Module';
|
|
207
287
|
}
|
|
208
288
|
async analyzeArchitecture() {
|
|
@@ -247,30 +327,86 @@ This agent is **stateful**. All interactions in this directory are logged to a p
|
|
|
247
327
|
return [{ type: 'info', message: 'Continue organizing by semantic purpose.' }];
|
|
248
328
|
}
|
|
249
329
|
async findExistingImplementations(featureDescription) {
|
|
250
|
-
|
|
330
|
+
const results = await this.cntxServer.vectorStore.search(featureDescription, { limit: 5 });
|
|
331
|
+
return results.map(r => ({
|
|
332
|
+
file: r.filePath,
|
|
333
|
+
name: r.name,
|
|
334
|
+
purpose: r.purpose,
|
|
335
|
+
relevance: r.similarity
|
|
336
|
+
}));
|
|
251
337
|
}
|
|
252
338
|
async findRelatedCode(featureDescription) {
|
|
253
|
-
|
|
339
|
+
// Search for keywords in the description
|
|
340
|
+
const keywords = featureDescription.split(' ').filter(w => w.length > 4);
|
|
341
|
+
const allFiles = this.cntxServer.fileSystemManager.getAllFiles();
|
|
342
|
+
const matches = allFiles.filter(f => keywords.some(k => f.toLowerCase().includes(k.toLowerCase()))).slice(0, 5);
|
|
343
|
+
return matches.map(f => ({
|
|
344
|
+
file: f,
|
|
345
|
+
reason: 'Filename contains relevant keywords'
|
|
346
|
+
}));
|
|
254
347
|
}
|
|
255
348
|
async findIntegrationPoints(featureDescription) {
|
|
256
|
-
|
|
349
|
+
const existing = await this.findExistingImplementations(featureDescription);
|
|
350
|
+
const related = await this.findRelatedCode(featureDescription);
|
|
351
|
+
const candidates = [...new Set([
|
|
352
|
+
...existing.map(e => e.file),
|
|
353
|
+
...related.map(r => r.file)
|
|
354
|
+
])];
|
|
355
|
+
return candidates.map(f => {
|
|
356
|
+
const ext = path.extname(f);
|
|
357
|
+
let role = 'Likely touch point';
|
|
358
|
+
if (ext === '.rs')
|
|
359
|
+
role = 'Backend logic (Rust)';
|
|
360
|
+
if (ext === '.tsx')
|
|
361
|
+
role = 'UI/Frontend component';
|
|
362
|
+
if (f.includes('router') || f.includes('api'))
|
|
363
|
+
role = 'API/Routing';
|
|
364
|
+
if (f.includes('store') || f.includes('hook'))
|
|
365
|
+
role = 'State/Data management';
|
|
366
|
+
return { file: f, role };
|
|
367
|
+
});
|
|
257
368
|
}
|
|
258
369
|
async suggestImplementationApproach(investigation) {
|
|
259
|
-
|
|
370
|
+
const points = investigation.integration || [];
|
|
371
|
+
if (points.length === 0) {
|
|
372
|
+
return {
|
|
373
|
+
strategy: 'Exploratory Search',
|
|
374
|
+
description: 'No clear integration points found. Recommendation: Perform a broader semantic search for core business entities.'
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
const primaryFile = points[0].file;
|
|
378
|
+
return {
|
|
379
|
+
strategy: `Extend ${primaryFile}`,
|
|
380
|
+
description: `Based on the feature description, the primary integration point seems to be ${primaryFile}. You should examine this file and its dependencies to determine the exact insertion point.`,
|
|
381
|
+
steps: [
|
|
382
|
+
`1. Analyze ${primaryFile} for existing patterns.`,
|
|
383
|
+
`2. Check related files: ${points.slice(1, 3).map((p) => p.file).join(', ')}`,
|
|
384
|
+
`3. Implement the feature following the established coding style.`
|
|
385
|
+
]
|
|
386
|
+
};
|
|
260
387
|
}
|
|
261
388
|
async generateContextualAnswer(question, results, includeCode) {
|
|
262
389
|
let response = `Based on the codebase analysis:\n\n`;
|
|
263
|
-
|
|
390
|
+
const hasSemantic = results.chunks.length > 0;
|
|
391
|
+
const hasFallbacks = results.files && results.files.length > 0;
|
|
392
|
+
if (hasSemantic) {
|
|
264
393
|
const top = results.chunks[0];
|
|
265
394
|
response += `The most relevant implementation found is \`${top.name}\` in \`${top.filePath}\` (Purpose: ${top.purpose}).\n\n`;
|
|
266
395
|
}
|
|
396
|
+
else if (hasFallbacks) {
|
|
397
|
+
response += `I couldn't find an exact semantic match for your query, but these files look like strong candidates for the entry point or data model:\n\n`;
|
|
398
|
+
results.files.forEach((f) => {
|
|
399
|
+
response += `- \`${f}\`\n`;
|
|
400
|
+
});
|
|
401
|
+
response += `\nYou should start by examining these files.`;
|
|
402
|
+
}
|
|
267
403
|
else {
|
|
268
404
|
response += `No direct semantic matches found. Try refining your query.`;
|
|
269
405
|
}
|
|
270
406
|
return {
|
|
271
407
|
response,
|
|
272
408
|
evidence: results.chunks.slice(0, 3),
|
|
273
|
-
confidence:
|
|
409
|
+
confidence: hasSemantic ? 0.8 : (hasFallbacks ? 0.4 : 0.2)
|
|
274
410
|
};
|
|
275
411
|
}
|
|
276
412
|
}
|
package/dist/lib/api-router.js
CHANGED
|
@@ -286,8 +286,12 @@ export default class APIRouter {
|
|
|
286
286
|
}
|
|
287
287
|
async handlePostSemanticSearch(req, res) {
|
|
288
288
|
const body = await this.getRequestBody(req);
|
|
289
|
-
const { query, limit = 20 } = JSON.parse(body);
|
|
290
|
-
const
|
|
289
|
+
const { query, question, limit = 20 } = JSON.parse(body);
|
|
290
|
+
const searchTerm = query || question;
|
|
291
|
+
if (!searchTerm) {
|
|
292
|
+
return this.sendError(res, 400, 'Missing search term (query or question)');
|
|
293
|
+
}
|
|
294
|
+
const results = await this.vectorStore.search(searchTerm, { limit });
|
|
291
295
|
this.sendResponse(res, 200, { results });
|
|
292
296
|
}
|
|
293
297
|
async handleGetVectorDbStatus(req, res) {
|
package/dist/lib/mcp-server.js
CHANGED
|
@@ -6,8 +6,10 @@ import { readFileSync } from 'fs';
|
|
|
6
6
|
import { join } from 'path';
|
|
7
7
|
export class MCPServer {
|
|
8
8
|
cntxServer;
|
|
9
|
-
|
|
9
|
+
version;
|
|
10
|
+
constructor(cntxServer, version = '3.0.0') {
|
|
10
11
|
this.cntxServer = cntxServer;
|
|
12
|
+
this.version = version;
|
|
11
13
|
// Listen for MCP requests on stdin
|
|
12
14
|
process.stdin.on('data', (data) => {
|
|
13
15
|
this.handleInput(data.toString());
|
|
@@ -36,7 +38,7 @@ export class MCPServer {
|
|
|
36
38
|
resources: {},
|
|
37
39
|
prompts: {}
|
|
38
40
|
},
|
|
39
|
-
serverInfo: { name: 'cntx-ui', version:
|
|
41
|
+
serverInfo: { name: 'cntx-ui', version: this.version }
|
|
40
42
|
}));
|
|
41
43
|
case 'tools/list':
|
|
42
44
|
return this.sendResponse(this.handleListTools(id));
|
|
@@ -28,6 +28,8 @@ export default class SemanticSplitter {
|
|
|
28
28
|
includeContext: true, // Include imports/types needed
|
|
29
29
|
minFunctionSize: 40, // Skip tiny functions
|
|
30
30
|
minStructureSize: 20, // Skip tiny structures
|
|
31
|
+
verbose: options.verbose || false,
|
|
32
|
+
isMcp: options.isMcp || false,
|
|
31
33
|
...options
|
|
32
34
|
};
|
|
33
35
|
// Initialize tree-sitter parsers
|
|
@@ -55,6 +57,14 @@ export default class SemanticSplitter {
|
|
|
55
57
|
this.parsers.toml.setLanguage(Toml);
|
|
56
58
|
this.heuristicsManager = new HeuristicsManager();
|
|
57
59
|
}
|
|
60
|
+
log(message) {
|
|
61
|
+
if (this.options.isMcp) {
|
|
62
|
+
process.stderr.write(message + '\n');
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
console.log(message);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
58
68
|
getParser(filePath) {
|
|
59
69
|
const ext = extname(filePath);
|
|
60
70
|
switch (ext) {
|
|
@@ -77,10 +87,10 @@ export default class SemanticSplitter {
|
|
|
77
87
|
* Now accepts a pre-filtered list of files from FileSystemManager
|
|
78
88
|
*/
|
|
79
89
|
async extractSemanticChunks(projectPath, files = [], bundleConfig = null) {
|
|
80
|
-
|
|
81
|
-
|
|
90
|
+
this.log('🔪 Starting surgical semantic splitting via tree-sitter...');
|
|
91
|
+
this.log(`📂 Project path: ${projectPath}`);
|
|
82
92
|
this.bundleConfig = bundleConfig;
|
|
83
|
-
|
|
93
|
+
this.log(`📁 Processing ${files.length} filtered files`);
|
|
84
94
|
const allChunks = [];
|
|
85
95
|
for (const filePath of files) {
|
|
86
96
|
try {
|
|
@@ -88,10 +98,13 @@ export default class SemanticSplitter {
|
|
|
88
98
|
allChunks.push(...fileChunks);
|
|
89
99
|
}
|
|
90
100
|
catch (error) {
|
|
91
|
-
console.warn(
|
|
101
|
+
console.warn(`⚠️ Failed to process ${filePath}: ${error.message}`);
|
|
102
|
+
if (this.options.verbose) {
|
|
103
|
+
console.error(error.stack);
|
|
104
|
+
}
|
|
92
105
|
}
|
|
93
106
|
}
|
|
94
|
-
|
|
107
|
+
this.log(`🧩 Created ${allChunks.length} semantic chunks across project`);
|
|
95
108
|
return {
|
|
96
109
|
summary: {
|
|
97
110
|
totalFiles: files.length,
|
|
@@ -108,43 +121,52 @@ export default class SemanticSplitter {
|
|
|
108
121
|
const content = readFileSync(fullPath, 'utf8');
|
|
109
122
|
// Skip files larger than 200KB — tree-sitter and embeddings can't handle them well
|
|
110
123
|
if (content.length > 200_000) {
|
|
111
|
-
|
|
124
|
+
this.log(`⚠️ Skipping ${relativePath}: file too large (${Math.round(content.length / 1024)}KB)`);
|
|
112
125
|
return [];
|
|
113
126
|
}
|
|
114
127
|
const parser = this.getParser(relativePath);
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
128
|
+
try {
|
|
129
|
+
const tree = parser.parse(content);
|
|
130
|
+
const root = tree.rootNode;
|
|
131
|
+
const ext = extname(relativePath).toLowerCase();
|
|
132
|
+
const elements = {
|
|
133
|
+
functions: [],
|
|
134
|
+
types: [],
|
|
135
|
+
imports: []
|
|
136
|
+
};
|
|
137
|
+
if (['.js', '.jsx', '.ts', '.tsx', '.rs'].includes(ext)) {
|
|
138
|
+
elements.imports = this.extractImports(root, content, relativePath);
|
|
139
|
+
// Traverse AST for functions and types
|
|
140
|
+
this.traverse(root, content, relativePath, elements);
|
|
141
|
+
}
|
|
142
|
+
else if (ext === '.json') {
|
|
143
|
+
this.extractJsonStructures(root, content, relativePath, elements);
|
|
144
|
+
}
|
|
145
|
+
else if (ext === '.css' || ext === '.scss') {
|
|
146
|
+
this.extractCssStructures(root, content, relativePath, elements);
|
|
147
|
+
}
|
|
148
|
+
else if (ext === '.html') {
|
|
149
|
+
this.extractHtmlStructures(root, content, relativePath, elements);
|
|
150
|
+
}
|
|
151
|
+
else if (ext === '.sql') {
|
|
152
|
+
this.extractSqlStructures(root, content, relativePath, elements);
|
|
153
|
+
}
|
|
154
|
+
else if (ext === '.md') {
|
|
155
|
+
this.extractMarkdownStructures(root, content, relativePath, elements);
|
|
156
|
+
}
|
|
157
|
+
else if (ext === '.toml') {
|
|
158
|
+
this.extractTomlStructures(root, content, relativePath, elements);
|
|
159
|
+
}
|
|
160
|
+
// Create chunks from elements
|
|
161
|
+
return this.createChunks(elements, content, relativePath);
|
|
142
162
|
}
|
|
143
|
-
|
|
144
|
-
this.
|
|
163
|
+
catch (error) {
|
|
164
|
+
this.log(`⚠️ Parser failed for ${relativePath}: ${error.message}`);
|
|
165
|
+
if (this.options.verbose) {
|
|
166
|
+
console.error(error.stack);
|
|
167
|
+
}
|
|
168
|
+
return [];
|
|
145
169
|
}
|
|
146
|
-
// Create chunks from elements
|
|
147
|
-
return this.createChunks(elements, content, relativePath);
|
|
148
170
|
}
|
|
149
171
|
traverse(node, content, filePath, elements) {
|
|
150
172
|
// Detect Function Declarations (JS/TS)
|
|
@@ -59,7 +59,7 @@ export default class SimpleVectorStore {
|
|
|
59
59
|
* Semantic Search across persistent embeddings
|
|
60
60
|
*/
|
|
61
61
|
async search(query, options = {}) {
|
|
62
|
-
const { limit = 10, threshold = 0.
|
|
62
|
+
const { limit = 10, threshold = 0.2 } = options;
|
|
63
63
|
const queryEmbedding = await this.generateEmbedding(query);
|
|
64
64
|
// Load all embeddings from DB
|
|
65
65
|
const rows = this.db.db.prepare('SELECT chunk_id, embedding FROM vector_embeddings WHERE model_name = ?').all(this.modelName);
|
|
@@ -7,35 +7,50 @@ export default class WebSocketManager {
|
|
|
7
7
|
bundleManager;
|
|
8
8
|
configManager;
|
|
9
9
|
verbose;
|
|
10
|
+
isMcp;
|
|
10
11
|
clients;
|
|
11
12
|
wss;
|
|
12
13
|
constructor(bundleManager, configManager, options = {}) {
|
|
13
14
|
this.bundleManager = bundleManager;
|
|
14
15
|
this.configManager = configManager;
|
|
15
16
|
this.verbose = options.verbose || false;
|
|
17
|
+
this.isMcp = options.isMcp || false;
|
|
16
18
|
this.clients = new Set();
|
|
17
19
|
this.wss = null;
|
|
18
20
|
}
|
|
21
|
+
log(message) {
|
|
22
|
+
if (this.isMcp) {
|
|
23
|
+
process.stderr.write(message + '\n');
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
console.log(message);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
19
29
|
initialize(httpServer) {
|
|
20
30
|
this.wss = new WebSocketServer({ server: httpServer });
|
|
21
31
|
this.wss.on('connection', (ws) => {
|
|
22
32
|
this.handleConnection(ws);
|
|
23
33
|
});
|
|
34
|
+
this.wss.on('error', (error) => {
|
|
35
|
+
if (this.verbose) {
|
|
36
|
+
console.error('🔌 WebSocket server error:', error.message);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
24
39
|
if (this.verbose) {
|
|
25
|
-
|
|
40
|
+
this.log('🔌 WebSocket server initialized');
|
|
26
41
|
}
|
|
27
42
|
}
|
|
28
43
|
handleConnection(ws) {
|
|
29
44
|
this.clients.add(ws);
|
|
30
45
|
if (this.verbose) {
|
|
31
|
-
|
|
46
|
+
this.log(`📱 WebSocket client connected (${this.clients.size} total clients)`);
|
|
32
47
|
}
|
|
33
48
|
// Send initial status
|
|
34
49
|
this.sendUpdate(ws);
|
|
35
50
|
ws.on('close', () => {
|
|
36
51
|
this.clients.delete(ws);
|
|
37
52
|
if (this.verbose) {
|
|
38
|
-
|
|
53
|
+
this.log(`📱 WebSocket client disconnected (${this.clients.size} total clients)`);
|
|
39
54
|
}
|
|
40
55
|
});
|
|
41
56
|
ws.on('error', (error) => {
|
|
@@ -58,7 +73,7 @@ export default class WebSocketManager {
|
|
|
58
73
|
}
|
|
59
74
|
handleClientMessage(ws, data) {
|
|
60
75
|
if (this.verbose) {
|
|
61
|
-
|
|
76
|
+
this.log('📩 Received client message: ' + data.type);
|
|
62
77
|
}
|
|
63
78
|
switch (data.type) {
|
|
64
79
|
case 'ping':
|
|
@@ -94,7 +109,7 @@ export default class WebSocketManager {
|
|
|
94
109
|
*/
|
|
95
110
|
broadcastUpdate() {
|
|
96
111
|
if (this.verbose) {
|
|
97
|
-
|
|
112
|
+
this.log('📢 Broadcasting status update to all clients');
|
|
98
113
|
}
|
|
99
114
|
this.clients.forEach(client => {
|
|
100
115
|
this.sendUpdate(client);
|
|
@@ -135,7 +150,7 @@ export default class WebSocketManager {
|
|
|
135
150
|
}
|
|
136
151
|
broadcastBundleUpdate(bundleName) {
|
|
137
152
|
if (this.verbose) {
|
|
138
|
-
|
|
153
|
+
this.log(`📢 Broadcasting update for bundle: ${bundleName}`);
|
|
139
154
|
}
|
|
140
155
|
const bundle = this.configManager.getBundles().get(bundleName);
|
|
141
156
|
if (!bundle)
|
|
@@ -186,7 +201,7 @@ export default class WebSocketManager {
|
|
|
186
201
|
close() {
|
|
187
202
|
if (this.wss) {
|
|
188
203
|
if (this.verbose) {
|
|
189
|
-
|
|
204
|
+
this.log('🔌 Closing WebSocket server');
|
|
190
205
|
}
|
|
191
206
|
this.clients.forEach(client => {
|
|
192
207
|
try {
|
|
@@ -200,7 +215,7 @@ export default class WebSocketManager {
|
|
|
200
215
|
});
|
|
201
216
|
this.wss.close(() => {
|
|
202
217
|
if (this.verbose) {
|
|
203
|
-
|
|
218
|
+
this.log('🔌 WebSocket server closed');
|
|
204
219
|
}
|
|
205
220
|
});
|
|
206
221
|
this.clients.clear();
|
package/dist/server.js
CHANGED
|
@@ -38,6 +38,7 @@ function getProjectName(cwd) {
|
|
|
38
38
|
export class CntxServer {
|
|
39
39
|
CWD;
|
|
40
40
|
CNTX_DIR;
|
|
41
|
+
version;
|
|
41
42
|
verbose;
|
|
42
43
|
isMcp;
|
|
43
44
|
mcpServerStarted;
|
|
@@ -66,6 +67,20 @@ export class CntxServer {
|
|
|
66
67
|
this.mcpServerStarted = false;
|
|
67
68
|
this.mcpServer = null;
|
|
68
69
|
this.initMessages = [];
|
|
70
|
+
// Read package version
|
|
71
|
+
try {
|
|
72
|
+
let pkgDir = __dirname;
|
|
73
|
+
let pkgPath = join(pkgDir, 'package.json');
|
|
74
|
+
if (!existsSync(pkgPath)) {
|
|
75
|
+
pkgDir = join(__dirname, '..');
|
|
76
|
+
pkgPath = join(pkgDir, 'package.json');
|
|
77
|
+
}
|
|
78
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
79
|
+
this.version = pkg.version;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
this.version = '3.1.5';
|
|
83
|
+
}
|
|
69
84
|
// Ensure directory exists
|
|
70
85
|
if (!existsSync(this.CNTX_DIR))
|
|
71
86
|
mkdirSync(this.CNTX_DIR, { recursive: true });
|
|
@@ -74,13 +89,18 @@ export class CntxServer {
|
|
|
74
89
|
this.databaseManager = this.configManager.dbManager;
|
|
75
90
|
this.fileSystemManager = new FileSystemManager(cwd, { verbose: this.verbose });
|
|
76
91
|
this.bundleManager = new BundleManager(this.configManager, this.fileSystemManager, this.verbose);
|
|
77
|
-
this.webSocketManager = new WebSocketManager(this.bundleManager, this.configManager, {
|
|
92
|
+
this.webSocketManager = new WebSocketManager(this.bundleManager, this.configManager, {
|
|
93
|
+
verbose: this.verbose,
|
|
94
|
+
isMcp: this.isMcp
|
|
95
|
+
});
|
|
78
96
|
this.artifactManager = new ArtifactManager(cwd);
|
|
79
97
|
// AI Components
|
|
80
98
|
this.semanticSplitter = new SemanticSplitter({
|
|
81
99
|
maxChunkSize: 2000,
|
|
82
100
|
includeContext: true,
|
|
83
|
-
minFunctionSize: 50
|
|
101
|
+
minFunctionSize: 50,
|
|
102
|
+
verbose: this.verbose,
|
|
103
|
+
isMcp: this.isMcp
|
|
84
104
|
});
|
|
85
105
|
this.vectorStore = new SimpleVectorStore(this.databaseManager, {
|
|
86
106
|
modelName: 'Xenova/all-MiniLM-L6-v2',
|
|
@@ -201,11 +221,15 @@ export class CntxServer {
|
|
|
201
221
|
}
|
|
202
222
|
startMCPServer() {
|
|
203
223
|
if (!this.mcpServer) {
|
|
204
|
-
this.mcpServer = new MCPServer(this);
|
|
224
|
+
this.mcpServer = new MCPServer(this, this.version);
|
|
205
225
|
this.mcpServerStarted = true;
|
|
206
226
|
}
|
|
207
227
|
}
|
|
208
228
|
async listen(port = 3333, host = 'localhost') {
|
|
229
|
+
if (this.isMcp) {
|
|
230
|
+
this.log('Mode: MCP (stdio) - Skipping HTTP server start');
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
209
233
|
const server = createServer((req, res) => {
|
|
210
234
|
const url = parse(req.url || '/', true);
|
|
211
235
|
// Serve static files from web/dist
|
|
@@ -215,12 +239,30 @@ export class CntxServer {
|
|
|
215
239
|
// Route API requests
|
|
216
240
|
this.apiRouter.handleRequest(req, res, url);
|
|
217
241
|
});
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
242
|
+
return new Promise((resolve, reject) => {
|
|
243
|
+
let currentPort = port;
|
|
244
|
+
const maxRetries = 10;
|
|
245
|
+
let retries = 0;
|
|
246
|
+
const tryListen = (p) => {
|
|
247
|
+
server.listen(p, host, () => {
|
|
248
|
+
this.webSocketManager.initialize(server);
|
|
249
|
+
this.log(`🚀 cntx-ui server running at http://${host}:${p}`);
|
|
250
|
+
resolve(server);
|
|
251
|
+
});
|
|
252
|
+
};
|
|
253
|
+
server.on('error', (e) => {
|
|
254
|
+
if (e.code === 'EADDRINUSE' && retries < maxRetries) {
|
|
255
|
+
retries++;
|
|
256
|
+
const failedPort = currentPort;
|
|
257
|
+
currentPort++;
|
|
258
|
+
this.log(`⚠️ Port ${failedPort} busy, trying ${currentPort}...`);
|
|
259
|
+
tryListen(currentPort);
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
reject(e);
|
|
263
|
+
}
|
|
223
264
|
});
|
|
265
|
+
tryListen(currentPort);
|
|
224
266
|
});
|
|
225
267
|
}
|
|
226
268
|
handleStaticFile(req, res, url) {
|
|
@@ -377,6 +419,11 @@ export async function initConfig(cwd = process.cwd()) {
|
|
|
377
419
|
cpSync(agentRulesSource, agentRulesDest, { recursive: true });
|
|
378
420
|
server.log('📁 Created agent-rules directory with templates');
|
|
379
421
|
}
|
|
422
|
+
// 5. Trigger initial semantic scan (master bundle)
|
|
423
|
+
server.log('🔪 Performing initial semantic scan...');
|
|
424
|
+
await server.init({ skipFileWatcher: true, skipBundleGeneration: true });
|
|
425
|
+
await server.bundleManager.regenerateBundle('master');
|
|
426
|
+
server.log('✅ Project is ready for AI agents');
|
|
380
427
|
return server.initMessages;
|
|
381
428
|
}
|
|
382
429
|
export async function generateBundle(name) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cntx-ui",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "3.1.
|
|
4
|
+
"version": "3.1.13",
|
|
5
5
|
"description": "Autonomous Repository Intelligence engine with web UI and MCP server. Unified semantic code understanding, local RAG, and agent working memory.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"repository-intelligence",
|