cntx-ui 3.0.6 → 3.0.8

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.
@@ -9,6 +9,7 @@ import { join, extname } from 'path'
9
9
  import Parser from 'tree-sitter'
10
10
  import JavaScript from 'tree-sitter-javascript'
11
11
  import TypeScript from 'tree-sitter-typescript'
12
+ import Rust from 'tree-sitter-rust'
12
13
  import HeuristicsManager from './heuristics-manager.js'
13
14
 
14
15
  export default class SemanticSplitter {
@@ -24,11 +25,13 @@ export default class SemanticSplitter {
24
25
  this.parsers = {
25
26
  javascript: new Parser(),
26
27
  typescript: new Parser(),
27
- tsx: new Parser()
28
+ tsx: new Parser(),
29
+ rust: new Parser()
28
30
  }
29
31
  this.parsers.javascript.setLanguage(JavaScript)
30
32
  this.parsers.typescript.setLanguage(TypeScript.typescript)
31
33
  this.parsers.tsx.setLanguage(TypeScript.tsx)
34
+ this.parsers.rust.setLanguage(Rust)
32
35
 
33
36
  this.heuristicsManager = new HeuristicsManager()
34
37
  }
@@ -38,6 +41,7 @@ export default class SemanticSplitter {
38
41
  switch (ext) {
39
42
  case '.ts': return this.parsers.typescript
40
43
  case '.tsx': return this.parsers.tsx
44
+ case '.rs': return this.parsers.rust
41
45
  default: return this.parsers.javascript
42
46
  }
43
47
  }
@@ -98,7 +102,7 @@ export default class SemanticSplitter {
98
102
  }
99
103
 
100
104
  traverse(node, content, filePath, elements) {
101
- // Detect Function Declarations
105
+ // Detect Function Declarations (JS/TS)
102
106
  if (node.type === 'function_declaration' || node.type === 'method_definition' || node.type === 'arrow_function') {
103
107
  const func = this.mapFunctionNode(node, content, filePath)
104
108
  if (func && func.code.length > this.options.minFunctionSize) {
@@ -106,14 +110,39 @@ export default class SemanticSplitter {
106
110
  }
107
111
  }
108
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
+
109
121
  // Detect Type Definitions (TS)
110
122
  if (node.type === 'interface_declaration' || node.type === 'type_alias_declaration') {
111
123
  const typeDef = this.mapTypeNode(node, content, filePath)
112
124
  if (typeDef) elements.types.push(typeDef)
113
125
  }
114
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
+
115
144
  // Recurse unless we've already captured the block (like a function body)
116
- if (node.type !== 'function_declaration' && node.type !== 'method_definition') {
145
+ if (node.type !== 'function_declaration' && node.type !== 'method_definition' && node.type !== 'function_item') {
117
146
  for (let i = 0; i < node.namedChildCount; i++) {
118
147
  this.traverse(node.namedChild(i), content, filePath, elements)
119
148
  }
@@ -124,7 +153,7 @@ export default class SemanticSplitter {
124
153
  let name = 'anonymous';
125
154
 
126
155
  // Find name identifier based on node type
127
- if (node.type === 'function_declaration' || node.type === 'method_definition') {
156
+ if (node.type === 'function_declaration' || node.type === 'method_definition' || node.type === 'function_item') {
128
157
  const nameNode = node.childForFieldName('name');
129
158
  if (nameNode) name = content.slice(nameNode.startIndex, nameNode.endIndex);
130
159
  } else if (node.type === 'arrow_function') {
@@ -175,10 +204,10 @@ export default class SemanticSplitter {
175
204
 
176
205
  extractImports(root, content, filePath) {
177
206
  const imports = []
178
- // Simple traversal for import statements
207
+ // Simple traversal for import/use statements
179
208
  for (let i = 0; i < root.namedChildCount; i++) {
180
209
  const node = root.namedChild(i)
181
- if (node.type === 'import_statement') {
210
+ if (node.type === 'import_statement' || node.type === 'use_declaration') {
182
211
  imports.push({
183
212
  statement: content.slice(node.startIndex, node.endIndex),
184
213
  filePath
@@ -189,6 +218,11 @@ export default class SemanticSplitter {
189
218
  }
190
219
 
191
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
192
226
  let current = node
193
227
  while (current) {
194
228
  if (current.type === 'export_statement') return true
@@ -250,7 +284,16 @@ export default class SemanticSplitter {
250
284
  }
251
285
 
252
286
  isImportRelevant(importStatement, functionCode) {
253
- // Heuristic: does the function use any name from the import?
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
254
297
  const match = importStatement.match(/import\s+(?:\{([^}]+)\}|(\w+))/i)
255
298
  if (!match) return false
256
299
  const importedNames = match[1] ? match[1].split(',').map(n => n.trim()) : [match[2]]
@@ -258,7 +301,7 @@ export default class SemanticSplitter {
258
301
  }
259
302
 
260
303
  calculateComplexity(code) {
261
- const indicators = ['if', 'else', 'for', 'while', 'switch', 'case', 'catch', '?', '&&', '||'];
304
+ const indicators = ['if', 'else', 'for', 'while', 'switch', 'case', 'catch', '?', '&&', '||', 'match', 'loop', 'unsafe', 'unwrap', 'expect'];
262
305
  let score = 1;
263
306
  indicators.forEach(ind => {
264
307
  // Escape special regex characters
@@ -1,19 +1,18 @@
1
1
  /**
2
- * Treesitter-based Semantic Chunker for JavaScript/TypeScript Files
2
+ * Treesitter-based Semantic Chunker for JavaScript/TypeScript and Rust Files
3
3
  * Uses tree-sitter for true AST-based code analysis and semantic chunking
4
- * Supports JS/TS/JSX/TSX with equal treatment
4
+ * Supports JS/TS/JSX/TSX and Rust with equal treatment
5
5
  * Node ecosystem focus: React components, Express APIs, CLI tools, utilities
6
+ * Rust ecosystem focus: standalone functions, structs, enums, traits
6
7
  */
7
8
 
8
9
  import { readFileSync, existsSync } from 'fs'
9
10
  import { extname, basename, dirname, relative, join } from 'path'
10
- import glob from 'glob'
11
- import { promisify } from 'util'
11
+ import { glob } from 'glob'
12
12
  import Parser from 'tree-sitter'
13
13
  import JavaScript from 'tree-sitter-javascript'
14
14
  import TypeScript from 'tree-sitter-typescript'
15
-
16
- const globAsync = promisify(glob)
15
+ import Rust from 'tree-sitter-rust'
17
16
 
18
17
  class TreesitterSemanticChunker {
19
18
  constructor(options = {}) {
@@ -32,7 +31,7 @@ class TreesitterSemanticChunker {
32
31
  this.parsers = {}
33
32
  this.initializeParsers()
34
33
 
35
- // Semantic patterns for Node ecosystem
34
+ // Semantic patterns for Node/Rust ecosystem
36
35
  this.semanticPatterns = {
37
36
  reactComponent: this.isReactComponent.bind(this),
38
37
  reactHook: this.isReactHook.bind(this),
@@ -61,6 +60,10 @@ class TreesitterSemanticChunker {
61
60
  // TSX parser
62
61
  this.parsers.tsx = new Parser()
63
62
  this.parsers.tsx.setLanguage(TypeScript.tsx)
63
+
64
+ // Rust parser
65
+ this.parsers.rust = new Parser()
66
+ this.parsers.rust.setLanguage(Rust)
64
67
  }
65
68
 
66
69
  /**
@@ -71,6 +74,7 @@ class TreesitterSemanticChunker {
71
74
  switch (ext) {
72
75
  case '.ts': return this.parsers.typescript
73
76
  case '.tsx': return this.parsers.tsx
77
+ case '.rs': return this.parsers.rust
74
78
  case '.js':
75
79
  case '.jsx':
76
80
  default: return this.parsers.javascript
@@ -80,7 +84,7 @@ class TreesitterSemanticChunker {
80
84
  /**
81
85
  * Main entry point - analyze files and create semantic chunks
82
86
  */
83
- async analyzeProject(projectPath, patterns = ['**/*.{js,jsx,ts,tsx}']) {
87
+ async analyzeProject(projectPath, patterns = ['**/*.{js,jsx,ts,tsx,rs}']) {
84
88
  console.log('🔍 Starting treesitter-based semantic analysis...')
85
89
 
86
90
  const files = await this.findFiles(projectPath, patterns)
@@ -115,7 +119,7 @@ class TreesitterSemanticChunker {
115
119
  const files = []
116
120
 
117
121
  for (const pattern of patterns) {
118
- const matches = await globAsync(pattern, {
122
+ const matches = await glob(pattern, {
119
123
  cwd: projectPath,
120
124
  ignore: [
121
125
  'node_modules/**', 'dist/**', 'build/**', '.git/**',
@@ -186,11 +190,6 @@ class TreesitterSemanticChunker {
186
190
  // Use simple string parsing (confirmed working in tests)
187
191
  tree = parser.parse(content)
188
192
  rootNode = tree.rootNode
189
-
190
- // Check for parse errors
191
- if (rootNode.hasError()) {
192
- throw new Error('Parse error in file')
193
- }
194
193
  } catch (error) {
195
194
  throw new Error(`Tree-sitter parse failed: ${error.message}`)
196
195
  }
@@ -239,7 +238,7 @@ class TreesitterSemanticChunker {
239
238
  extractFunctions(rootNode, content) {
240
239
  const functions = []
241
240
 
242
- // Function declarations
241
+ // JS/TS Function declarations
243
242
  const functionDeclarations = this.queryNode(rootNode, '(function_declaration name: (identifier) @name)')
244
243
  functions.push(...functionDeclarations.map(capture => ({
245
244
  name: this.getNodeText(capture.node, content),
@@ -249,7 +248,7 @@ class TreesitterSemanticChunker {
249
248
  isExported: this.isNodeExported(capture.node)
250
249
  })))
251
250
 
252
- // Arrow functions
251
+ // JS/TS Arrow functions
253
252
  const arrowFunctions = this.queryNode(rootNode, '(variable_declarator name: (identifier) @name value: (arrow_function))')
254
253
  functions.push(...arrowFunctions.map(capture => ({
255
254
  name: this.getNodeText(capture.node, content),
@@ -259,7 +258,7 @@ class TreesitterSemanticChunker {
259
258
  isExported: this.isNodeExported(capture.node.parent.parent)
260
259
  })))
261
260
 
262
- // Method definitions
261
+ // JS/TS Method definitions
263
262
  const methods = this.queryNode(rootNode, '(method_definition name: (property_name) @name)')
264
263
  functions.push(...methods.map(capture => ({
265
264
  name: this.getNodeText(capture.node, content),
@@ -268,6 +267,16 @@ class TreesitterSemanticChunker {
268
267
  endPosition: capture.node.endPosition,
269
268
  isExported: false // methods are part of classes
270
269
  })))
270
+
271
+ // Rust function items
272
+ const rustFunctions = this.queryNode(rootNode, '(function_item name: (identifier) @name)')
273
+ functions.push(...rustFunctions.map(capture => ({
274
+ name: this.getNodeText(capture.node, content),
275
+ type: 'function_item',
276
+ startPosition: capture.node.startPosition,
277
+ endPosition: capture.node.endPosition,
278
+ isExported: this.isNodeExported(capture.node)
279
+ })))
271
280
 
272
281
  return functions
273
282
  }
@@ -318,6 +327,7 @@ class TreesitterSemanticChunker {
318
327
  extractImports(rootNode, content) {
319
328
  const imports = []
320
329
 
330
+ // JS/TS imports
321
331
  const importStatements = this.queryNode(rootNode, '(import_statement source: (string) @source)')
322
332
  imports.push(...importStatements.map(capture => {
323
333
  const source = this.getNodeText(capture.node, content).replace(/['"]/g, '')
@@ -329,6 +339,19 @@ class TreesitterSemanticChunker {
329
339
  importedNames: this.extractImportedNames(capture.node.parent, content)
330
340
  }
331
341
  }))
342
+
343
+ // Rust use declarations
344
+ const rustUseStatements = this.queryNode(rootNode, '(use_declaration)')
345
+ imports.push(...rustUseStatements.map(capture => {
346
+ const statement = this.getNodeText(capture.node, content)
347
+ return {
348
+ source: 'rust',
349
+ statement,
350
+ isRelative: statement.includes('self::') || statement.includes('super::'),
351
+ isExternal: !statement.includes('crate::') && !statement.includes('self::') && !statement.includes('super::'),
352
+ importedNames: [] // Complexity of parsing Rust use paths is high for this simple chunker
353
+ }
354
+ }))
332
355
 
333
356
  return imports
334
357
  }
@@ -339,7 +362,7 @@ class TreesitterSemanticChunker {
339
362
  extractExports(rootNode, content) {
340
363
  const exports = []
341
364
 
342
- // Export declarations
365
+ // Export declarations (JS/TS)
343
366
  const exportDeclarations = this.queryNode(rootNode, '(export_statement)')
344
367
  exports.push(...exportDeclarations.map(capture => {
345
368
  const exportNode = capture.node
@@ -405,7 +428,7 @@ class TreesitterSemanticChunker {
405
428
  const types = []
406
429
 
407
430
  try {
408
- // Interface declarations
431
+ // JS/TS Interface declarations
409
432
  const interfaces = this.queryNode(rootNode, '(interface_declaration name: (type_identifier) @name)')
410
433
  types.push(...interfaces.map(capture => ({
411
434
  name: this.getNodeText(capture.node, content),
@@ -415,7 +438,7 @@ class TreesitterSemanticChunker {
415
438
  isExported: this.isNodeExported(capture.node.parent)
416
439
  })))
417
440
 
418
- // Type alias declarations
441
+ // JS/TS Type alias declarations
419
442
  const typeAliases = this.queryNode(rootNode, '(type_alias_declaration name: (type_identifier) @name)')
420
443
  types.push(...typeAliases.map(capture => ({
421
444
  name: this.getNodeText(capture.node, content),
@@ -424,8 +447,38 @@ class TreesitterSemanticChunker {
424
447
  endPosition: capture.node.endPosition,
425
448
  isExported: this.isNodeExported(capture.node.parent)
426
449
  })))
450
+
451
+ // Rust struct definitions
452
+ const structs = this.queryNode(rootNode, '(struct_item name: (type_identifier) @name)')
453
+ types.push(...structs.map(capture => ({
454
+ name: this.getNodeText(capture.node, content),
455
+ type: 'struct',
456
+ startPosition: capture.node.startPosition,
457
+ endPosition: capture.node.endPosition,
458
+ isExported: this.isNodeExported(capture.node)
459
+ })))
460
+
461
+ // Rust enum definitions
462
+ const enums = this.queryNode(rootNode, '(enum_item name: (type_identifier) @name)')
463
+ types.push(...enums.map(capture => ({
464
+ name: this.getNodeText(capture.node, content),
465
+ type: 'enum',
466
+ startPosition: capture.node.startPosition,
467
+ endPosition: capture.node.endPosition,
468
+ isExported: this.isNodeExported(capture.node)
469
+ })))
470
+
471
+ // Rust trait definitions
472
+ const traits = this.queryNode(rootNode, '(trait_item name: (type_identifier) @name)')
473
+ types.push(...traits.map(capture => ({
474
+ name: this.getNodeText(capture.node, content),
475
+ type: 'trait',
476
+ startPosition: capture.node.startPosition,
477
+ endPosition: capture.node.endPosition,
478
+ isExported: this.isNodeExported(capture.node)
479
+ })))
427
480
  } catch (error) {
428
- // TypeScript types might not be available in JavaScript parser
481
+ // TypeScript/Rust types might not be available
429
482
  }
430
483
 
431
484
  return types
@@ -576,6 +629,18 @@ class TreesitterSemanticChunker {
576
629
  return hasTypeFileName || hasOnlyTypes
577
630
  }
578
631
 
632
+ /**
633
+ * Semantic pattern: UI Component (generic for any language)
634
+ */
635
+ isUiComponent(rootNode, content, filePath) {
636
+ // JS/TS logic
637
+ if (extname(filePath).match(/\.(jsx|tsx|js|ts)$/)) {
638
+ return this.isReactComponent(rootNode, content, filePath)
639
+ }
640
+ // TODO: Add generic patterns for other languages (Rust templates, etc.)
641
+ return false
642
+ }
643
+
579
644
  /**
580
645
  * Semantic pattern: Config Module
581
646
  */
@@ -583,7 +648,7 @@ class TreesitterSemanticChunker {
583
648
  const fileName = basename(filePath).toLowerCase()
584
649
  const hasConfigName = fileName.includes('config') || fileName.includes('setting')
585
650
 
586
- const hasConfigPatterns = content.includes('module.exports') || content.includes('export default')
651
+ const hasConfigPatterns = content.includes('module.exports') || content.includes('export default') || content.includes('Cargo.toml')
587
652
  const hasConfigObject = /\{[\s\S]*\}/.test(content) && !/function|class/.test(content)
588
653
 
589
654
  return hasConfigName && (hasConfigPatterns || hasConfigObject)
@@ -630,11 +695,13 @@ class TreesitterSemanticChunker {
630
695
  if (content.includes('react')) patterns.push('react')
631
696
  if (content.includes('express')) patterns.push('express')
632
697
  if (content.includes('typescript')) patterns.push('typescript')
698
+ if (content.includes('cargo') || extname(content) === '.rs') patterns.push('rust')
633
699
 
634
700
  // Architecture patterns
635
- if (content.includes('async') && content.includes('await')) patterns.push('async-await')
701
+ if (content.includes('async') && (content.includes('await') || content.includes('.await'))) patterns.push('async-await')
636
702
  if (content.includes('Promise')) patterns.push('promises')
637
703
  if (content.includes('class') && content.includes('extends')) patterns.push('inheritance')
704
+ if (content.includes('unsafe')) patterns.push('unsafe-code')
638
705
 
639
706
  // Design patterns
640
707
  const functions = this.extractFunctions(rootNode, content)
@@ -996,10 +1063,14 @@ class TreesitterSemanticChunker {
996
1063
  }
997
1064
 
998
1065
  isNodeExported(node) {
999
- // Check if node is part of an export statement
1066
+ // Rust: check for visibility_modifier (pub) as direct child
1067
+ for (let i = 0; i < node.namedChildCount; i++) {
1068
+ if (node.namedChild(i).type === 'visibility_modifier') return true
1069
+ }
1070
+ // JS/TS: check for export_statement ancestor or parent
1000
1071
  let parent = node.parent
1001
1072
  while (parent) {
1002
- if (parent.type === 'export_statement') {
1073
+ if (parent.type === 'export_statement' || parent.type === 'export_declaration') {
1003
1074
  return true
1004
1075
  }
1005
1076
  parent = parent.parent
@@ -1048,7 +1119,8 @@ class TreesitterSemanticChunker {
1048
1119
  let complexity = 1
1049
1120
 
1050
1121
  const complexityNodes = ['if_statement', 'while_statement', 'for_statement',
1051
- 'switch_statement', 'try_statement', 'catch_clause']
1122
+ 'switch_statement', 'try_statement', 'catch_clause',
1123
+ 'match_arm', 'loop_expression']
1052
1124
 
1053
1125
  const traverse = (node) => {
1054
1126
  if (complexityNodes.includes(node.type)) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cntx-ui",
3
3
  "type": "module",
4
- "version": "3.0.6",
4
+ "version": "3.0.8",
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",
@@ -49,6 +49,7 @@
49
49
  "glob": "^9.0.0",
50
50
  "tree-sitter": "^0.21.1",
51
51
  "tree-sitter-javascript": "^0.23.1",
52
+ "tree-sitter-rust": "^0.21.0",
52
53
  "tree-sitter-typescript": "^0.23.2",
53
54
  "ws": "^8.13.0"
54
55
  }
package/server.js CHANGED
@@ -340,16 +340,19 @@ export class CntxServer {
340
340
  // === Semantic Analysis (Legacy methods for compatibility) ===
341
341
 
342
342
  async getSemanticAnalysis() {
343
+ // Return cached result if available
344
+ if (this.semanticCache) {
345
+ return this.semanticCache;
346
+ }
347
+
343
348
  // 1. Try to load from SQLite first
344
349
  try {
345
350
  const dbChunks = this.databaseManager.db.prepare('SELECT * FROM semantic_chunks').all();
346
351
  if (dbChunks.length > 0) {
347
- if (!this.semanticCache) {
348
- this.semanticCache = {
349
- chunks: dbChunks.map(row => this.databaseManager.mapChunkRow(row)),
350
- summary: { totalChunks: dbChunks.length }
351
- };
352
- }
352
+ this.semanticCache = {
353
+ chunks: dbChunks.map(row => this.databaseManager.mapChunkRow(row)),
354
+ summary: { totalChunks: dbChunks.length }
355
+ };
353
356
  return this.semanticCache;
354
357
  }
355
358
  } catch (e) {
@@ -358,7 +361,7 @@ export class CntxServer {
358
361
 
359
362
  // 2. Perform fresh analysis if DB is empty
360
363
  try {
361
- const supportedExtensions = ['.js', '.jsx', '.ts', '.tsx', '.mjs'];
364
+ const supportedExtensions = ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.rs'];
362
365
  const files = this.fileSystemManager.getAllFiles()
363
366
  .filter(f => supportedExtensions.includes(extname(f).toLowerCase()))
364
367
  .map(f => relative(this.CWD, f));
@@ -649,8 +652,6 @@ export async function initConfig(cwd = process.cwd()) {
649
652
  }
650
653
 
651
654
  console.log('Configuration initialized');
652
- console.log('');
653
- console.log('Run cntx-ui to start the server, then open http://localhost:3333');
654
655
  }
655
656
 
656
657
  export async function getStatus() {