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.
@@ -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', question);
142
+ await this.logInteraction('user', actualQuestion);
139
143
  // Perform semantic search via Vector Store
140
- const combinedResults = await this.cntxServer.vectorStore.search(question, { limit: maxResults });
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, { chunks: combinedResults, files: [] }, includeCode);
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(combinedResults.map(c => c.filePath))].slice(0, 5)
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: ${featureDescription}`);
204
+ await this.logInteraction('user', `Investigating feature: ${actualDescription}`);
164
205
  const investigation = {
165
- feature: featureDescription,
166
- existing: await this.findExistingImplementations(featureDescription),
167
- related: await this.findRelatedCode(featureDescription),
168
- integration: await this.findIntegrationPoints(featureDescription)
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 = bundles.reduce((sum, b) => sum + b.fileCount, 0);
184
- const totalSize = bundles.reduce((sum, b) => sum + b.size, 0);
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
- ...b,
198
- purpose: this.inferBundlePurpose(b.name, b.files || [])
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
- if (name.includes('component') || name.includes('ui'))
203
- return 'UI Components';
204
- if (name.includes('api') || name.includes('server'))
205
- return 'Backend API';
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
- return await this.cntxServer.vectorStore.search(featureDescription, { limit: 5 });
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
- return [];
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
- return [];
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
- return { strategy: 'TBD', description: 'Ready to plan' };
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
- if (results.chunks.length > 0) {
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: results.chunks.length > 0 ? 0.8 : 0.2
409
+ confidence: hasSemantic ? 0.8 : (hasFallbacks ? 0.4 : 0.2)
274
410
  };
275
411
  }
276
412
  }
@@ -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 results = await this.vectorStore.search(query, { limit });
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) {
@@ -6,8 +6,10 @@ import { readFileSync } from 'fs';
6
6
  import { join } from 'path';
7
7
  export class MCPServer {
8
8
  cntxServer;
9
- constructor(cntxServer) {
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: '3.0.0' }
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
- console.log('🔪 Starting surgical semantic splitting via tree-sitter...');
81
- console.log(`📂 Project path: ${projectPath}`);
90
+ this.log('🔪 Starting surgical semantic splitting via tree-sitter...');
91
+ this.log(`📂 Project path: ${projectPath}`);
82
92
  this.bundleConfig = bundleConfig;
83
- console.log(`📁 Processing ${files.length} filtered files`);
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(`Failed to process ${filePath}: ${error.message}`);
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
- console.log(`🧩 Created ${allChunks.length} semantic chunks across project`);
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
- console.warn(`Skipping ${relativePath}: file too large (${Math.round(content.length / 1024)}KB)`);
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
- const tree = parser.parse(content);
116
- const root = tree.rootNode;
117
- const ext = extname(relativePath).toLowerCase();
118
- const elements = {
119
- functions: [],
120
- types: [],
121
- imports: []
122
- };
123
- if (['.js', '.jsx', '.ts', '.tsx', '.rs'].includes(ext)) {
124
- elements.imports = this.extractImports(root, content, relativePath);
125
- // Traverse AST for functions and types
126
- this.traverse(root, content, relativePath, elements);
127
- }
128
- else if (ext === '.json') {
129
- this.extractJsonStructures(root, content, relativePath, elements);
130
- }
131
- else if (ext === '.css' || ext === '.scss') {
132
- this.extractCssStructures(root, content, relativePath, elements);
133
- }
134
- else if (ext === '.html') {
135
- this.extractHtmlStructures(root, content, relativePath, elements);
136
- }
137
- else if (ext === '.sql') {
138
- this.extractSqlStructures(root, content, relativePath, elements);
139
- }
140
- else if (ext === '.md') {
141
- this.extractMarkdownStructures(root, content, relativePath, elements);
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
- else if (ext === '.toml') {
144
- this.extractTomlStructures(root, content, relativePath, elements);
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.5 } = options;
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
- console.log('🔌 WebSocket server initialized');
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
- console.log(`📱 WebSocket client connected (${this.clients.size} total clients)`);
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
- console.log(`📱 WebSocket client disconnected (${this.clients.size} total clients)`);
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
- console.log('📩 Received client message:', data.type);
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
- console.log('📢 Broadcasting status update to all clients');
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
- console.log(`📢 Broadcasting update for bundle: ${bundleName}`);
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
- console.log('🔌 Closing WebSocket server');
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
- console.log('🔌 WebSocket server closed');
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, { verbose: this.verbose });
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
- this.webSocketManager.initialize(server);
219
- return new Promise((resolve) => {
220
- server.listen(port, host, () => {
221
- this.log(`🚀 cntx-ui server running at http://${host}:${port}`);
222
- resolve(server);
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",
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",