cntx-ui 3.0.8 → 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.
Files changed (36) hide show
  1. package/dist/bin/cntx-ui.js +70 -0
  2. package/dist/lib/agent-runtime.js +269 -0
  3. package/dist/lib/agent-tools.js +162 -0
  4. package/dist/lib/api-router.js +387 -0
  5. package/dist/lib/bundle-manager.js +236 -0
  6. package/dist/lib/configuration-manager.js +230 -0
  7. package/dist/lib/database-manager.js +277 -0
  8. package/dist/lib/file-system-manager.js +305 -0
  9. package/dist/lib/function-level-chunker.js +144 -0
  10. package/dist/lib/heuristics-manager.js +491 -0
  11. package/dist/lib/mcp-server.js +159 -0
  12. package/dist/lib/mcp-transport.js +10 -0
  13. package/dist/lib/semantic-splitter.js +335 -0
  14. package/dist/lib/simple-vector-store.js +98 -0
  15. package/dist/lib/treesitter-semantic-chunker.js +277 -0
  16. package/dist/lib/websocket-manager.js +268 -0
  17. package/dist/server.js +225 -0
  18. package/package.json +17 -8
  19. package/bin/cntx-ui-mcp.sh +0 -3
  20. package/bin/cntx-ui.js +0 -123
  21. package/lib/agent-runtime.js +0 -371
  22. package/lib/agent-tools.js +0 -370
  23. package/lib/api-router.js +0 -1026
  24. package/lib/bundle-manager.js +0 -326
  25. package/lib/configuration-manager.js +0 -760
  26. package/lib/database-manager.js +0 -397
  27. package/lib/file-system-manager.js +0 -489
  28. package/lib/function-level-chunker.js +0 -406
  29. package/lib/heuristics-manager.js +0 -529
  30. package/lib/mcp-server.js +0 -1380
  31. package/lib/mcp-transport.js +0 -97
  32. package/lib/semantic-splitter.js +0 -347
  33. package/lib/simple-vector-store.js +0 -108
  34. package/lib/treesitter-semantic-chunker.js +0 -1557
  35. package/lib/websocket-manager.js +0 -470
  36. package/server.js +0 -687
@@ -1,97 +0,0 @@
1
- import { MCPServer } from './mcp-server.js';
2
-
3
- export class MCPTransport {
4
- constructor(cntxServer) {
5
- this.mcpServer = new MCPServer(cntxServer);
6
- this.buffer = '';
7
- }
8
-
9
- // Start stdio transport
10
- start() {
11
- console.error('🚀 MCP server starting on stdio transport');
12
-
13
- // Handle incoming messages from stdin
14
- process.stdin.on('data', (data) => {
15
- this.handleIncomingData(data.toString());
16
- });
17
-
18
- // Handle process cleanup
19
- process.on('SIGINT', () => {
20
- console.error('📡 MCP server shutting down');
21
- process.exit(0);
22
- });
23
-
24
- process.on('SIGTERM', () => {
25
- console.error('📡 MCP server shutting down');
26
- process.exit(0);
27
- });
28
-
29
- // Set stdin to raw mode for proper JSON-RPC communication
30
- process.stdin.setEncoding('utf8');
31
-
32
- console.error('✅ MCP server ready for JSON-RPC messages');
33
- }
34
-
35
- // Handle incoming data and parse JSON-RPC messages
36
- async handleIncomingData(data) {
37
- this.buffer += data;
38
-
39
- // Split by newlines to handle multiple messages
40
- const lines = this.buffer.split('\n');
41
- this.buffer = lines.pop() || ''; // Keep incomplete line in buffer
42
-
43
- for (const line of lines) {
44
- if (line.trim()) {
45
- try {
46
- const message = JSON.parse(line.trim());
47
- await this.processMessage(message);
48
- } catch (error) {
49
- console.error('❌ Failed to parse JSON-RPC message:', error.message);
50
- this.sendError(null, -32700, 'Parse error');
51
- }
52
- }
53
- }
54
- }
55
-
56
- // Process a single JSON-RPC message
57
- async processMessage(message) {
58
- try {
59
- const response = await this.mcpServer.handleMessage(message);
60
-
61
- // Only send response if not null (notifications don't need responses)
62
- if (response !== null) {
63
- this.sendMessage(response);
64
- }
65
- } catch (error) {
66
- console.error('❌ Error processing message:', error.message);
67
- this.sendError(message.id || null, -32603, 'Internal error');
68
- }
69
- }
70
-
71
- // Send a message via stdout
72
- sendMessage(message) {
73
- const messageStr = JSON.stringify(message);
74
- process.stdout.write(messageStr + '\n');
75
- }
76
-
77
- // Send an error response
78
- sendError(id, code, message, data = null) {
79
- const error = { code, message };
80
- if (data) error.data = data;
81
-
82
- const response = {
83
- jsonrpc: '2.0',
84
- id,
85
- error
86
- };
87
-
88
- this.sendMessage(response);
89
- }
90
- }
91
-
92
- // Factory function to start MCP transport
93
- export function startMCPTransport(cntxServer) {
94
- const transport = new MCPTransport(cntxServer);
95
- transport.start();
96
- return transport;
97
- }
@@ -1,347 +0,0 @@
1
- /**
2
- * Semantic Splitter - High-Performance AST-based Chunker
3
- * Uses tree-sitter for surgical, function-level code extraction
4
- * Integrated with HeuristicsManager for intelligent categorization
5
- */
6
-
7
- import { readFileSync, existsSync } from 'fs'
8
- import { join, extname } from 'path'
9
- import Parser from 'tree-sitter'
10
- import JavaScript from 'tree-sitter-javascript'
11
- import TypeScript from 'tree-sitter-typescript'
12
- import Rust from 'tree-sitter-rust'
13
- import HeuristicsManager from './heuristics-manager.js'
14
-
15
- export default class SemanticSplitter {
16
- constructor(options = {}) {
17
- this.options = {
18
- maxChunkSize: 3000, // Max chars per chunk
19
- includeContext: true, // Include imports/types needed
20
- minFunctionSize: 40, // Skip tiny functions
21
- ...options
22
- }
23
-
24
- // Initialize tree-sitter parsers
25
- this.parsers = {
26
- javascript: new Parser(),
27
- typescript: new Parser(),
28
- tsx: new Parser(),
29
- rust: new Parser()
30
- }
31
- this.parsers.javascript.setLanguage(JavaScript)
32
- this.parsers.typescript.setLanguage(TypeScript.typescript)
33
- this.parsers.tsx.setLanguage(TypeScript.tsx)
34
- this.parsers.rust.setLanguage(Rust)
35
-
36
- this.heuristicsManager = new HeuristicsManager()
37
- }
38
-
39
- getParser(filePath) {
40
- const ext = extname(filePath)
41
- switch (ext) {
42
- case '.ts': return this.parsers.typescript
43
- case '.tsx': return this.parsers.tsx
44
- case '.rs': return this.parsers.rust
45
- default: return this.parsers.javascript
46
- }
47
- }
48
-
49
- /**
50
- * Main entry point - extract semantic chunks from project
51
- * Now accepts a pre-filtered list of files from FileSystemManager
52
- */
53
- async extractSemanticChunks(projectPath, files = [], bundleConfig = null) {
54
- console.log('🔪 Starting surgical semantic splitting via tree-sitter...')
55
- console.log(`📂 Project path: ${projectPath}`)
56
-
57
- this.bundleConfig = bundleConfig
58
- console.log(`📁 Processing ${files.length} filtered files`)
59
-
60
- const allChunks = []
61
-
62
- for (const filePath of files) {
63
- try {
64
- const fileChunks = this.processFile(filePath, projectPath)
65
- allChunks.push(...fileChunks)
66
- } catch (error) {
67
- console.warn(`Failed to process ${filePath}: ${error.message}`)
68
- }
69
- }
70
-
71
- console.log(`🧩 Created ${allChunks.length} semantic chunks across project`)
72
- return {
73
- summary: {
74
- totalFiles: files.length,
75
- totalChunks: allChunks.length,
76
- averageSize: allChunks.length > 0 ? allChunks.reduce((sum, c) => sum + c.code.length, 0) / allChunks.length : 0
77
- },
78
- chunks: allChunks
79
- }
80
- }
81
-
82
- processFile(relativePath, projectPath) {
83
- const fullPath = join(projectPath, relativePath)
84
- if (!existsSync(fullPath)) return []
85
-
86
- const content = readFileSync(fullPath, 'utf8')
87
- const parser = this.getParser(relativePath)
88
- const tree = parser.parse(content)
89
- const root = tree.rootNode
90
-
91
- const elements = {
92
- functions: [],
93
- types: [],
94
- imports: this.extractImports(root, content, relativePath)
95
- }
96
-
97
- // Traverse AST for functions and types
98
- this.traverse(root, content, relativePath, elements)
99
-
100
- // Create chunks from elements
101
- return this.createChunks(elements, content, relativePath)
102
- }
103
-
104
- traverse(node, content, filePath, elements) {
105
- // Detect Function Declarations (JS/TS)
106
- if (node.type === 'function_declaration' || node.type === 'method_definition' || node.type === 'arrow_function') {
107
- const func = this.mapFunctionNode(node, content, filePath)
108
- if (func && func.code.length > this.options.minFunctionSize) {
109
- elements.functions.push(func)
110
- }
111
- }
112
-
113
- // Detect Rust function items
114
- if (node.type === 'function_item') {
115
- const func = this.mapFunctionNode(node, content, filePath)
116
- if (func && func.code.length > this.options.minFunctionSize) {
117
- elements.functions.push(func)
118
- }
119
- }
120
-
121
- // Detect Type Definitions (TS)
122
- if (node.type === 'interface_declaration' || node.type === 'type_alias_declaration') {
123
- const typeDef = this.mapTypeNode(node, content, filePath)
124
- if (typeDef) elements.types.push(typeDef)
125
- }
126
-
127
- // Detect Rust type definitions
128
- if (node.type === 'struct_item' || node.type === 'enum_item' || node.type === 'trait_item') {
129
- const typeDef = this.mapTypeNode(node, content, filePath)
130
- if (typeDef) elements.types.push(typeDef)
131
- }
132
-
133
- // Detect Rust impl blocks — traverse into body for methods
134
- if (node.type === 'impl_item') {
135
- const body = node.childForFieldName('body')
136
- if (body) {
137
- for (let i = 0; i < body.namedChildCount; i++) {
138
- this.traverse(body.namedChild(i), content, filePath, elements)
139
- }
140
- }
141
- return // Don't recurse again below
142
- }
143
-
144
- // Recurse unless we've already captured the block (like a function body)
145
- if (node.type !== 'function_declaration' && node.type !== 'method_definition' && node.type !== 'function_item') {
146
- for (let i = 0; i < node.namedChildCount; i++) {
147
- this.traverse(node.namedChild(i), content, filePath, elements)
148
- }
149
- }
150
- }
151
-
152
- mapFunctionNode(node, content, filePath) {
153
- let name = 'anonymous';
154
-
155
- // Find name identifier based on node type
156
- if (node.type === 'function_declaration' || node.type === 'method_definition' || node.type === 'function_item') {
157
- const nameNode = node.childForFieldName('name');
158
- if (nameNode) name = content.slice(nameNode.startIndex, nameNode.endIndex);
159
- } else if (node.type === 'arrow_function') {
160
- // 1. Check if assigned to a variable: const foo = () => {}
161
- const parent = node.parent;
162
- if (parent && parent.type === 'variable_declarator') {
163
- const nameNode = parent.childForFieldName('name');
164
- if (nameNode) name = content.slice(nameNode.startIndex, nameNode.endIndex);
165
- }
166
- // 2. Check if part of an object property: { foo: () => {} }
167
- else if (parent && parent.type === 'pair') {
168
- const keyNode = parent.childForFieldName('key');
169
- if (keyNode) name = content.slice(keyNode.startIndex, keyNode.endIndex);
170
- }
171
- // 3. Check if part of an assignment: this.foo = () => {}
172
- else if (parent && parent.type === 'assignment_expression') {
173
- const leftNode = parent.childForFieldName('left');
174
- if (leftNode) name = content.slice(leftNode.startIndex, leftNode.endIndex);
175
- }
176
- }
177
-
178
- const code = content.slice(node.startIndex, node.endIndex)
179
-
180
- return {
181
- name,
182
- type: node.type,
183
- filePath,
184
- startLine: node.startPosition.row + 1,
185
- code,
186
- isExported: this.isExported(node),
187
- isAsync: code.includes('async')
188
- }
189
- }
190
-
191
- mapTypeNode(node, content, filePath) {
192
- const nameNode = node.childForFieldName('name')
193
- if (!nameNode) return null
194
-
195
- return {
196
- name: content.slice(nameNode.startIndex, nameNode.endIndex),
197
- type: node.type,
198
- filePath,
199
- startLine: node.startPosition.row + 1,
200
- code: content.slice(node.startIndex, node.endIndex),
201
- isExported: this.isExported(node)
202
- }
203
- }
204
-
205
- extractImports(root, content, filePath) {
206
- const imports = []
207
- // Simple traversal for import/use statements
208
- for (let i = 0; i < root.namedChildCount; i++) {
209
- const node = root.namedChild(i)
210
- if (node.type === 'import_statement' || node.type === 'use_declaration') {
211
- imports.push({
212
- statement: content.slice(node.startIndex, node.endIndex),
213
- filePath
214
- })
215
- }
216
- }
217
- return imports
218
- }
219
-
220
- isExported(node) {
221
- // Rust: check for visibility_modifier (pub) as direct child
222
- for (let i = 0; i < node.namedChildCount; i++) {
223
- if (node.namedChild(i).type === 'visibility_modifier') return true
224
- }
225
- // JS/TS: check for export_statement ancestor
226
- let current = node
227
- while (current) {
228
- if (current.type === 'export_statement') return true
229
- current = current.parent
230
- }
231
- return false
232
- }
233
-
234
- createChunks(elements, content, filePath) {
235
- const chunks = [];
236
- const pathParts = filePath.toLowerCase().split(/[\\\/]/);
237
-
238
- for (const func of elements.functions) {
239
- // Pass full context to heuristics
240
- const heuristicContext = {
241
- ...func,
242
- includes: elements,
243
- pathParts
244
- };
245
-
246
- const purpose = this.heuristicsManager.determinePurpose(heuristicContext);
247
- const businessDomain = this.heuristicsManager.inferBusinessDomains(heuristicContext);
248
- const technicalPatterns = this.heuristicsManager.inferTechnicalPatterns(heuristicContext);
249
- const tags = this.generateTags(func);
250
-
251
- let chunkCode = '';
252
- if (this.options.includeContext) {
253
- const relevantImports = elements.imports
254
- .filter(imp => this.isImportRelevant(imp.statement, func.code))
255
- .map(imp => imp.statement)
256
- .join('\n');
257
-
258
- if (relevantImports) chunkCode += relevantImports + '\n\n';
259
- }
260
- chunkCode += func.code;
261
-
262
- chunks.push({
263
- id: `${filePath}:${func.name}:${func.startLine}`,
264
- name: func.name,
265
- filePath,
266
- type: 'function',
267
- subtype: func.type,
268
- code: chunkCode,
269
- startLine: func.startLine,
270
- complexity: this.calculateComplexity(func.code),
271
- purpose,
272
- tags,
273
- businessDomain,
274
- technicalPatterns,
275
- includes: {
276
- imports: elements.imports.map(i => i.statement),
277
- types: elements.types.map(t => t.name)
278
- },
279
- bundles: this.getFileBundles(filePath)
280
- });
281
- }
282
-
283
- return chunks;
284
- }
285
-
286
- isImportRelevant(importStatement, functionCode) {
287
- // Rust use statements: use std::collections::HashMap;
288
- const useMatch = importStatement.match(/^use\s+(.+);?\s*$/)
289
- if (useMatch) {
290
- const path = useMatch[1]
291
- // Extract the last segment (the actual imported name)
292
- const segments = path.replace(/[{}]/g, '').split('::')
293
- const lastSegment = segments[segments.length - 1].trim()
294
- return functionCode.includes(lastSegment)
295
- }
296
- // JS/TS import statements
297
- const match = importStatement.match(/import\s+(?:\{([^}]+)\}|(\w+))/i)
298
- if (!match) return false
299
- const importedNames = match[1] ? match[1].split(',').map(n => n.trim()) : [match[2]]
300
- return importedNames.some(name => functionCode.includes(name))
301
- }
302
-
303
- calculateComplexity(code) {
304
- const indicators = ['if', 'else', 'for', 'while', 'switch', 'case', 'catch', '?', '&&', '||', 'match', 'loop', 'unsafe', 'unwrap', 'expect'];
305
- let score = 1;
306
- indicators.forEach(ind => {
307
- // Escape special regex characters
308
- const escaped = ind.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
309
- // Only use word boundaries for word-like indicators
310
- const pattern = /^[a-zA-Z]+$/.test(ind) ? `\\b${escaped}\\b` : escaped;
311
- const regex = new RegExp(pattern, 'g');
312
- score += (code.match(regex) || []).length;
313
- });
314
- return {
315
- score,
316
- level: score < 5 ? 'low' : score < 15 ? 'medium' : 'high'
317
- };
318
- }
319
-
320
- generateTags(func) {
321
- const tags = [func.type]
322
- if (func.isExported) tags.push('exported')
323
- if (func.isAsync) tags.push('async')
324
- if (func.code.length > 2000) tags.push('large')
325
- return tags
326
- }
327
-
328
- getFileBundles(filePath) {
329
- if (!this.bundleConfig?.bundles) return []
330
- const bundles = []
331
- for (const [name, patterns] of Object.entries(this.bundleConfig.bundles)) {
332
- if (name === 'master') continue
333
- for (const pattern of patterns) {
334
- if (this.matchesPattern(filePath, pattern)) {
335
- bundles.push(name)
336
- break
337
- }
338
- }
339
- }
340
- return bundles
341
- }
342
-
343
- matchesPattern(filePath, pattern) {
344
- const regex = pattern.replace(/\*\*/g, '.*').replace(/\*/g, '[^/]*').replace(/\./g, '\\.')
345
- return new RegExp(`^${regex}$`).test(filePath)
346
- }
347
- }
@@ -1,108 +0,0 @@
1
- /**
2
- * Simple Vector Store with SQLite Persistence
3
- * Powered by Transformers.js for local embeddings
4
- * Persists vectors to SQLite for instant startup
5
- */
6
-
7
- import { pipeline } from '@xenova/transformers';
8
-
9
- export default class SimpleVectorStore {
10
- constructor(databaseManager, options = {}) {
11
- this.db = databaseManager;
12
- this.modelName = options.modelName || 'Xenova/all-MiniLM-L6-v2';
13
- this.pipe = null;
14
- this.initialized = false;
15
- }
16
-
17
- async init() {
18
- if (this.initialized) return;
19
- console.log(`🤖 Initializing local RAG engine (${this.modelName})...`);
20
- this.pipe = await pipeline('feature-extraction', this.modelName);
21
- this.initialized = true;
22
- console.log('✅ Local RAG engine ready');
23
- }
24
-
25
- async generateEmbedding(text) {
26
- await this.init();
27
- const output = await this.pipe(text, { pooling: 'mean', normalize: true });
28
- return new Float32Array(output.data);
29
- }
30
-
31
- /**
32
- * Upsert a chunk's embedding to persistence
33
- */
34
- async upsertChunk(chunk) {
35
- const chunkId = chunk.id;
36
- // Check if we already have it in DB
37
- const existing = this.db.getEmbedding(chunkId);
38
- if (existing) return existing;
39
-
40
- // Generate new embedding
41
- const textToEmbed = `${chunk.name} ${chunk.purpose} ${chunk.code}`;
42
- const embedding = await this.generateEmbedding(textToEmbed);
43
-
44
- // Save to SQLite
45
- this.db.saveEmbedding(chunkId, embedding, this.modelName);
46
- return embedding;
47
- }
48
-
49
- /**
50
- * Semantic Search across persistent embeddings
51
- */
52
- async search(query, options = {}) {
53
- const { limit = 10, threshold = 0.5 } = options;
54
- const queryEmbedding = await this.generateEmbedding(query);
55
-
56
- // Load all embeddings from DB
57
- const rows = this.db.db.prepare('SELECT chunk_id, embedding FROM vector_embeddings WHERE model_name = ?').all(this.modelName);
58
-
59
- const results = [];
60
- const batchSize = 100;
61
-
62
- // Process in batches to prevent blocking the event loop
63
- for (let i = 0; i < rows.length; i += batchSize) {
64
- const batch = rows.slice(i, i + batchSize);
65
-
66
- for (const row of batch) {
67
- const embedding = new Float32Array(row.embedding.buffer, row.embedding.byteOffset, row.embedding.byteLength / 4);
68
- const similarity = this.cosineSimilarity(queryEmbedding, embedding);
69
-
70
- if (similarity >= threshold) {
71
- results.push({
72
- chunkId: row.chunk_id,
73
- similarity
74
- });
75
- }
76
- }
77
-
78
- // Give other tasks a chance to run
79
- if (i + batchSize < rows.length) {
80
- await new Promise(resolve => setImmediate(resolve));
81
- }
82
- }
83
-
84
- // Sort by similarity and get chunk details
85
- return results
86
- .sort((a, b) => b.similarity - a.similarity)
87
- .slice(0, limit)
88
- .map(res => {
89
- const chunk = this.db.db.prepare('SELECT * FROM semantic_chunks WHERE id = ?').get(res.chunkId);
90
- return {
91
- ...this.db.mapChunkRow(chunk),
92
- similarity: res.similarity
93
- };
94
- });
95
- }
96
-
97
- cosineSimilarity(vecA, vecB) {
98
- let dotProduct = 0;
99
- let normA = 0;
100
- let normB = 0;
101
- for (let i = 0; i < vecA.length; i++) {
102
- dotProduct += vecA[i] * vecB[i];
103
- normA += vecA[i] * vecA[i];
104
- normB += vecB[i] * vecB[i];
105
- }
106
- return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
107
- }
108
- }