cntx-ui 2.0.12 → 2.0.15

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 (45) hide show
  1. package/bin/cntx-ui.js +137 -55
  2. package/lib/agent-runtime.js +1480 -0
  3. package/lib/agent-tools.js +368 -0
  4. package/lib/api-router.js +978 -0
  5. package/lib/bundle-manager.js +471 -0
  6. package/lib/configuration-manager.js +725 -0
  7. package/lib/file-system-manager.js +472 -0
  8. package/lib/function-level-chunker.js +406 -0
  9. package/lib/heuristics-manager.js +425 -0
  10. package/lib/mcp-server.js +1054 -1
  11. package/lib/semantic-splitter.js +588 -0
  12. package/lib/simple-vector-store.js +329 -0
  13. package/lib/treesitter-semantic-chunker.js +1485 -0
  14. package/lib/websocket-manager.js +470 -0
  15. package/package.json +14 -3
  16. package/server.js +673 -1704
  17. package/templates/activities/README.md +67 -0
  18. package/templates/activities/activities/create-project-bundles/README.md +83 -0
  19. package/templates/activities/activities/create-project-bundles/notes.md +102 -0
  20. package/templates/activities/activities/create-project-bundles/progress.md +63 -0
  21. package/templates/activities/activities/create-project-bundles/tasks.md +39 -0
  22. package/templates/activities/activities.json +219 -0
  23. package/templates/activities/lib/.markdownlint.jsonc +18 -0
  24. package/templates/activities/lib/create-activity.mdc +63 -0
  25. package/templates/activities/lib/generate-tasks.mdc +64 -0
  26. package/templates/activities/lib/process-task-list.mdc +52 -0
  27. package/templates/agent-config.yaml +78 -0
  28. package/templates/agent-instructions.md +218 -0
  29. package/templates/agent-rules/capabilities/activities-system.md +147 -0
  30. package/templates/agent-rules/capabilities/bundle-system.md +131 -0
  31. package/templates/agent-rules/capabilities/vector-search.md +135 -0
  32. package/templates/agent-rules/core/codebase-navigation.md +91 -0
  33. package/templates/agent-rules/core/performance-hierarchy.md +48 -0
  34. package/templates/agent-rules/core/response-formatting.md +120 -0
  35. package/templates/agent-rules/project-specific/architecture.md +145 -0
  36. package/templates/config.json +76 -0
  37. package/templates/hidden-files.json +14 -0
  38. package/web/dist/assets/heuristics-manager-browser-DfonOP5I.js +1 -0
  39. package/web/dist/assets/index-dF3qg-y_.js +2486 -0
  40. package/web/dist/assets/index-h5FGSg_P.css +1 -0
  41. package/web/dist/cntx-ui.svg +18 -0
  42. package/web/dist/index.html +25 -8
  43. package/web/dist/assets/index-8Kli5657.js +0 -541
  44. package/web/dist/assets/index-C-Ldi33E.css +0 -1
  45. package/web/dist/vite.svg +0 -1
@@ -0,0 +1,588 @@
1
+ /**
2
+ * True Semantic Splitting - Function-level code chunks with context
3
+ * Creates surgical, self-contained chunks for AI consumption
4
+ * Operates parallel to file-level bundle system
5
+ */
6
+
7
+ import { readFileSync, existsSync } from 'fs'
8
+ import { extname, basename, dirname, join } from 'path'
9
+ import glob from 'glob'
10
+ import HeuristicsManager from './heuristics-manager.js'
11
+
12
+ export default class SemanticSplitter {
13
+ constructor(options = {}) {
14
+ this.options = {
15
+ maxChunkSize: 2000, // Max chars per chunk
16
+ includeContext: true, // Include imports/types needed
17
+ groupRelated: true, // Group related functions
18
+ minFunctionSize: 50, // Skip tiny functions
19
+ ...options
20
+ }
21
+
22
+ // Initialize heuristics manager
23
+ this.heuristicsManager = new HeuristicsManager()
24
+ }
25
+
26
+ /**
27
+ * Extract semantic chunks from project
28
+ */
29
+ async extractSemanticChunks(projectPath, patterns = ['**/*.{js,jsx,ts,tsx,mjs}'], bundleConfig = null) {
30
+ console.log('🔪 Starting semantic splitting...')
31
+
32
+ const files = this.findFiles(projectPath, patterns)
33
+ console.log(`📁 Found ${files.length} files to split`)
34
+
35
+ // Load bundle configuration if provided
36
+ this.bundleConfig = bundleConfig
37
+
38
+ const allFunctions = []
39
+ const allTypes = []
40
+ const allImports = []
41
+
42
+ // Extract all code elements
43
+ for (const filePath of files) {
44
+ try {
45
+ const elements = this.extractCodeElements(filePath, projectPath)
46
+ allFunctions.push(...elements.functions)
47
+ allTypes.push(...elements.types)
48
+ allImports.push(...elements.imports)
49
+ } catch (error) {
50
+ console.warn(`Failed to extract from ${filePath}: ${error.message}`)
51
+ }
52
+ }
53
+
54
+ console.log(`⚡ Extracted ${allFunctions.length} functions, ${allTypes.length} types`)
55
+
56
+ // Create semantic chunks
57
+ const chunks = this.createSemanticChunks(allFunctions, allTypes, allImports)
58
+ console.log(`🧩 Created ${chunks.length} semantic chunks`)
59
+
60
+ return {
61
+ summary: {
62
+ totalFiles: files.length,
63
+ totalFunctions: allFunctions.length,
64
+ totalChunks: chunks.length,
65
+ averageChunkSize: chunks.reduce((sum, c) => sum + c.code.length, 0) / chunks.length
66
+ },
67
+ chunks: chunks
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Find files to analyze (same logic as bundles)
73
+ */
74
+ findFiles(projectPath, patterns) {
75
+ const files = []
76
+
77
+ for (const pattern of patterns) {
78
+ const matches = glob.sync(pattern, {
79
+ cwd: projectPath,
80
+ ignore: [
81
+ 'node_modules/**', 'dist/**', 'build/**', '.git/**',
82
+ '*.test.*', '*.spec.*', '**/test/**', '**/tests/**',
83
+ '**/*.min.js', '**/*.bundle.js'
84
+ ]
85
+ })
86
+
87
+ files.push(...matches.filter(file =>
88
+ !file.includes('node_modules') &&
89
+ !file.includes('dist/') &&
90
+ !file.includes('.min.')
91
+ ))
92
+ }
93
+
94
+ return [...new Set(files)]
95
+ }
96
+
97
+ /**
98
+ * Extract functions, types, and imports from a file
99
+ */
100
+ extractCodeElements(relativePath, projectPath) {
101
+ const fullPath = join(projectPath, relativePath)
102
+ if (!existsSync(fullPath)) return { functions: [], types: [], imports: [] }
103
+
104
+ const content = readFileSync(fullPath, 'utf8')
105
+ const lines = content.split('\n')
106
+
107
+ return {
108
+ functions: this.extractFunctions(content, lines, relativePath),
109
+ types: this.extractTypes(content, lines, relativePath),
110
+ imports: this.extractImports(content, relativePath)
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Extract functions with robust regex patterns
116
+ */
117
+ extractFunctions(content, lines, filePath) {
118
+ const functions = []
119
+
120
+ // Pattern 1: Regular function declarations
121
+ const functionRegex = /^(\s*)(?:export\s+)?(?:async\s+)?function\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\([^)]*\)\s*\{/gm
122
+
123
+ // Pattern 2: Arrow functions assigned to const/let
124
+ const arrowRegex = /^(\s*)(?:export\s+)?const\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=\s*(?:async\s+)?\([^)]*\)\s*=>\s*[\{]/gm
125
+
126
+ // Pattern 3: Class methods
127
+ const methodRegex = /^(\s+)(?:async\s+)?([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\([^)]*\)\s*\{/gm
128
+
129
+ // Pattern 4: React components (function components)
130
+ const componentRegex = /^(\s*)(?:export\s+(?:default\s+)?)?function\s+([A-Z][a-zA-Z0-9_$]*)\s*\([^)]*\)\s*\{/gm
131
+
132
+ const patterns = [
133
+ { regex: functionRegex, type: 'function' },
134
+ { regex: arrowRegex, type: 'arrow_function' },
135
+ { regex: methodRegex, type: 'method' },
136
+ { regex: componentRegex, type: 'react_component' }
137
+ ]
138
+
139
+ for (const { regex, type } of patterns) {
140
+ let match
141
+ while ((match = regex.exec(content)) !== null) {
142
+ const functionName = match[2]
143
+ const indentation = match[1]
144
+ const startIndex = match.index
145
+
146
+ // Skip if it's a keyword or common false positive
147
+ if (['if', 'for', 'while', 'switch', 'catch'].includes(functionName)) {
148
+ continue
149
+ }
150
+
151
+ const startLine = content.substring(0, startIndex).split('\n').length
152
+ const functionBody = this.extractFunctionBody(content, startIndex)
153
+
154
+ if (functionBody && functionBody.length > this.options.minFunctionSize) {
155
+ functions.push({
156
+ name: functionName,
157
+ type: type,
158
+ filePath: filePath,
159
+ startLine: startLine,
160
+ code: functionBody,
161
+ indentation: indentation.length,
162
+ isExported: match[0].includes('export'),
163
+ isAsync: match[0].includes('async'),
164
+ size: functionBody.length
165
+ })
166
+ }
167
+ }
168
+ }
169
+
170
+ return functions
171
+ }
172
+
173
+ /**
174
+ * Extract function body using brace matching
175
+ */
176
+ extractFunctionBody(content, startIndex) {
177
+ const openBraceIndex = content.indexOf('{', startIndex)
178
+ if (openBraceIndex === -1) return null
179
+
180
+ let braceCount = 0
181
+ let currentIndex = openBraceIndex
182
+ let inString = false
183
+ let stringChar = null
184
+
185
+ while (currentIndex < content.length) {
186
+ const char = content[currentIndex]
187
+ const prevChar = content[currentIndex - 1] || ''
188
+
189
+ // Handle string literals
190
+ if ((char === '"' || char === "'" || char === '`') && prevChar !== '\\') {
191
+ if (!inString) {
192
+ inString = true
193
+ stringChar = char
194
+ } else if (char === stringChar) {
195
+ inString = false
196
+ stringChar = null
197
+ }
198
+ }
199
+
200
+ // Count braces outside strings
201
+ if (!inString) {
202
+ if (char === '{') braceCount++
203
+ else if (char === '}') braceCount--
204
+
205
+ if (braceCount === 0) {
206
+ // Found the closing brace
207
+ return content.substring(startIndex, currentIndex + 1).trim()
208
+ }
209
+ }
210
+
211
+ currentIndex++
212
+ }
213
+
214
+ return null // Unmatched braces
215
+ }
216
+
217
+ /**
218
+ * Extract type definitions and interfaces
219
+ */
220
+ extractTypes(content, lines, filePath) {
221
+ const types = []
222
+
223
+ // TypeScript interfaces
224
+ const interfaceRegex = /^(\s*)(?:export\s+)?interface\s+([A-Z][a-zA-Z0-9_$]*)\s*\{/gm
225
+
226
+ // Type aliases
227
+ const typeRegex = /^(\s*)(?:export\s+)?type\s+([A-Z][a-zA-Z0-9_$]*)\s*=/gm
228
+
229
+ const patterns = [
230
+ { regex: interfaceRegex, type: 'interface' },
231
+ { regex: typeRegex, type: 'type_alias' }
232
+ ]
233
+
234
+ for (const { regex, type } of patterns) {
235
+ let match
236
+ while ((match = regex.exec(content)) !== null) {
237
+ const typeName = match[2]
238
+ const startIndex = match.index
239
+ const startLine = content.substring(0, startIndex).split('\n').length
240
+
241
+ let typeBody
242
+ if (type === 'interface') {
243
+ typeBody = this.extractTypeBody(content, startIndex)
244
+ } else {
245
+ // For type aliases, extract until semicolon or newline
246
+ const endIndex = content.indexOf(';', startIndex)
247
+ typeBody = content.substring(startIndex, endIndex + 1).trim()
248
+ }
249
+
250
+ if (typeBody) {
251
+ types.push({
252
+ name: typeName,
253
+ type: type,
254
+ filePath: filePath,
255
+ startLine: startLine,
256
+ code: typeBody,
257
+ isExported: match[0].includes('export')
258
+ })
259
+ }
260
+ }
261
+ }
262
+
263
+ return types
264
+ }
265
+
266
+ /**
267
+ * Extract type body (similar to function body)
268
+ */
269
+ extractTypeBody(content, startIndex) {
270
+ const openBraceIndex = content.indexOf('{', startIndex)
271
+ if (openBraceIndex === -1) return null
272
+
273
+ let braceCount = 0
274
+ let currentIndex = openBraceIndex
275
+
276
+ while (currentIndex < content.length) {
277
+ const char = content[currentIndex]
278
+
279
+ if (char === '{') braceCount++
280
+ else if (char === '}') braceCount--
281
+
282
+ if (braceCount === 0) {
283
+ return content.substring(startIndex, currentIndex + 1).trim()
284
+ }
285
+
286
+ currentIndex++
287
+ }
288
+
289
+ return null
290
+ }
291
+
292
+ /**
293
+ * Extract import statements
294
+ */
295
+ extractImports(content, filePath) {
296
+ const imports = []
297
+ const importRegex = /^(\s*)import\s+(.+?)\s+from\s+['"`]([^'"`]+)['"`]/gm
298
+
299
+ let match
300
+ while ((match = importRegex.exec(content)) !== null) {
301
+ const importStatement = match[0].trim()
302
+ const importPath = match[3]
303
+
304
+ imports.push({
305
+ statement: importStatement,
306
+ path: importPath,
307
+ filePath: filePath,
308
+ isRelative: importPath.startsWith('.'),
309
+ isExternal: !importPath.startsWith('.')
310
+ })
311
+ }
312
+
313
+ return imports
314
+ }
315
+
316
+ /**
317
+ * Create semantic chunks from extracted elements
318
+ */
319
+ createSemanticChunks(functions, types, imports) {
320
+ const chunks = []
321
+
322
+ // Create function-level chunks
323
+ for (const func of functions) {
324
+ const chunk = this.createFunctionChunk(func, types, imports)
325
+ if (chunk) {
326
+ chunks.push(chunk)
327
+ }
328
+ }
329
+
330
+ // Create type-only chunks for standalone types
331
+ for (const type of types) {
332
+ if (!this.isTypeUsedInFunctions(type, functions)) {
333
+ chunks.push(this.createTypeChunk(type, imports))
334
+ }
335
+ }
336
+
337
+ return chunks
338
+ }
339
+
340
+ /**
341
+ * Create a semantic chunk for a function with its context
342
+ */
343
+ createFunctionChunk(func, allTypes, allImports) {
344
+ let chunkCode = ''
345
+ const includedImports = new Set()
346
+ const includedTypes = new Set()
347
+
348
+ // Find relevant imports for this function
349
+ const fileImports = allImports.filter(imp => imp.filePath === func.filePath)
350
+
351
+ // Find types referenced in the function
352
+ const referencedTypes = this.findReferencedTypes(func.code, allTypes)
353
+
354
+ // Add necessary imports
355
+ for (const imp of fileImports) {
356
+ if (this.isImportRelevant(imp, func.code)) {
357
+ chunkCode += imp.statement + '\n'
358
+ includedImports.add(imp.path)
359
+ }
360
+ }
361
+
362
+ // Add referenced types
363
+ for (const type of referencedTypes) {
364
+ chunkCode += '\n' + type.code + '\n'
365
+ includedTypes.add(type.name)
366
+ }
367
+
368
+ // Add the function itself
369
+ chunkCode += '\n' + func.code
370
+
371
+ // Create chunk with adaptive sizing - never lose functions
372
+ let finalCode = chunkCode.trim()
373
+ let contextLevel = 'full'
374
+
375
+ // If too large, try with reduced context
376
+ if (chunkCode.length > this.options.maxChunkSize) {
377
+ // Fallback 1: Function + essential imports only (no types)
378
+ finalCode = ''
379
+ for (const imp of fileImports.slice(0, 3)) { // Limit to 3 imports
380
+ if (this.isImportRelevant(imp, func.code)) {
381
+ finalCode += imp.statement + '\n'
382
+ }
383
+ }
384
+ finalCode += '\n' + func.code
385
+ contextLevel = 'reduced'
386
+ }
387
+
388
+ // If still too large, function only
389
+ if (finalCode.length > this.options.maxChunkSize) {
390
+ finalCode = func.code
391
+ contextLevel = 'minimal'
392
+ }
393
+
394
+ // Always create a chunk - never lose functions
395
+ return {
396
+ name: func.name,
397
+ type: 'function_chunk',
398
+ subtype: func.type,
399
+ code: finalCode,
400
+ size: finalCode.length,
401
+ filePath: func.filePath,
402
+ startLine: func.startLine,
403
+ isExported: func.isExported,
404
+ isAsync: func.isAsync,
405
+ complexity: this.calculateComplexity(func.code),
406
+ includes: {
407
+ imports: contextLevel === 'minimal' ? [] : Array.from(includedImports),
408
+ types: contextLevel === 'full' ? Array.from(includedTypes) : []
409
+ },
410
+ purpose: this.determinePurpose(func),
411
+ tags: [...this.generateTags(func), contextLevel === 'full' ? 'full-context' : contextLevel === 'reduced' ? 'reduced-context' : 'minimal-context'],
412
+ bundles: this.getFileBundles(func.filePath)
413
+ }
414
+ }
415
+
416
+ /**
417
+ * Create a chunk for standalone types
418
+ */
419
+ createTypeChunk(type, allImports) {
420
+ let chunkCode = ''
421
+ const includedImports = new Set()
422
+
423
+ // Add relevant imports if any
424
+ const fileImports = allImports.filter(imp => imp.filePath === type.filePath)
425
+ for (const imp of fileImports.slice(0, 3)) { // Limit imports
426
+ chunkCode += imp.statement + '\n'
427
+ }
428
+
429
+ chunkCode += '\n' + type.code
430
+
431
+ return {
432
+ name: type.name,
433
+ type: 'type_chunk',
434
+ subtype: type.type,
435
+ code: chunkCode.trim(),
436
+ size: chunkCode.length,
437
+ filePath: type.filePath,
438
+ startLine: type.startLine,
439
+ isExported: type.isExported,
440
+ purpose: 'Type definition',
441
+ tags: ['type', type.type],
442
+ bundles: this.getFileBundles(type.filePath)
443
+ }
444
+ }
445
+
446
+ /**
447
+ * Find types referenced in function code
448
+ */
449
+ findReferencedTypes(functionCode, allTypes) {
450
+ const referenced = []
451
+
452
+ for (const type of allTypes) {
453
+ // Check if type name appears in function code
454
+ const typeRegex = new RegExp(`\\b${type.name}\\b`, 'g')
455
+ if (typeRegex.test(functionCode)) {
456
+ referenced.push(type)
457
+ }
458
+ }
459
+
460
+ return referenced
461
+ }
462
+
463
+ /**
464
+ * Check if import is relevant to function
465
+ */
466
+ isImportRelevant(importStatement, functionCode) {
467
+ // Simple heuristic: check if any imported identifiers appear in function
468
+ const importMatch = importStatement.statement.match(/import\s+(.+?)\s+from/)
469
+ if (!importMatch) return false
470
+
471
+ const imported = importMatch[1]
472
+
473
+ // Handle different import styles
474
+ if (imported.includes('{')) {
475
+ // Named imports: import { foo, bar } from 'module'
476
+ const namedImports = imported.match(/\{([^}]+)\}/)?.[1]
477
+ if (namedImports) {
478
+ const names = namedImports.split(',').map(name => name.trim())
479
+ return names.some(name => functionCode.includes(name))
480
+ }
481
+ } else {
482
+ // Default import: import foo from 'module'
483
+ const defaultImport = imported.trim()
484
+ return functionCode.includes(defaultImport)
485
+ }
486
+
487
+ return false
488
+ }
489
+
490
+ /**
491
+ * Check if type is used in any function
492
+ */
493
+ isTypeUsedInFunctions(type, functions) {
494
+ const typeRegex = new RegExp(`\\b${type.name}\\b`, 'g')
495
+ return functions.some(func => typeRegex.test(func.code))
496
+ }
497
+
498
+ /**
499
+ * Calculate function complexity (cyclomatic complexity)
500
+ */
501
+ calculateComplexity(code) {
502
+ let complexity = 1 // Base complexity
503
+
504
+ // Simple complexity indicators - just count control flow structures
505
+ const indicators = {
506
+ 'if': (code.match(/\bif\s*\(/g) || []).length,
507
+ 'else if': (code.match(/\belse\s+if\b/g) || []).length,
508
+ 'for': (code.match(/\bfor\s*\(/g) || []).length,
509
+ 'while': (code.match(/\bwhile\s*\(/g) || []).length,
510
+ 'switch': (code.match(/\bswitch\s*\(/g) || []).length,
511
+ 'case': (code.match(/\bcase\s+/g) || []).length,
512
+ 'catch': (code.match(/\bcatch\s*\(/g) || []).length,
513
+ 'ternary': (code.match(/\?\s*[^?\.\s]/g) || []).length,
514
+ 'logical_and': (code.match(/&&\s*[^&=]/g) || []).length,
515
+ 'logical_or': (code.match(/\|\|\s*[^|=]/g) || []).length
516
+ }
517
+
518
+ // Sum all complexity indicators
519
+ for (const count of Object.values(indicators)) {
520
+ complexity += count
521
+ }
522
+
523
+ // Return complexity with reasonable thresholds
524
+ return {
525
+ score: complexity,
526
+ level: complexity <= 3 ? 'low' : complexity <= 8 ? 'medium' : 'high'
527
+ }
528
+ }
529
+
530
+ /**
531
+ * Determine function purpose using heuristics configuration
532
+ */
533
+ determinePurpose(func) {
534
+ return this.heuristicsManager.determinePurpose(func)
535
+ }
536
+
537
+ /**
538
+ * Generate tags for function
539
+ */
540
+ generateTags(func) {
541
+ const tags = [func.type]
542
+
543
+ if (func.isExported) tags.push('exported')
544
+ if (func.isAsync) tags.push('async')
545
+ if (func.size > 1000) tags.push('large')
546
+ if (func.code.includes('console.log')) tags.push('has-logging')
547
+ if (func.code.includes('throw')) tags.push('can-throw')
548
+ if (func.code.includes('return')) tags.push('returns-value')
549
+
550
+ return tags
551
+ }
552
+
553
+ /**
554
+ * Determine which bundles a file belongs to
555
+ */
556
+ getFileBundles(filePath) {
557
+ if (!this.bundleConfig?.bundles) return []
558
+
559
+ const bundles = []
560
+ for (const [bundleName, patterns] of Object.entries(this.bundleConfig.bundles)) {
561
+ // Skip master bundle as requested
562
+ if (bundleName === 'master') continue
563
+
564
+ // Check if file matches any pattern in this bundle
565
+ for (const pattern of patterns) {
566
+ if (this.matchesPattern(filePath, pattern)) {
567
+ bundles.push(bundleName)
568
+ break // Don't add the same bundle multiple times
569
+ }
570
+ }
571
+ }
572
+
573
+ return bundles
574
+ }
575
+
576
+ /**
577
+ * Simple pattern matching (basic glob support)
578
+ */
579
+ matchesPattern(filePath, pattern) {
580
+ // Convert glob pattern to regex
581
+ const regex = pattern
582
+ .replace(/\*\*/g, '.*') // ** matches any directories
583
+ .replace(/\*/g, '[^/]*') // * matches any characters except /
584
+ .replace(/\./g, '\\.') // Escape dots
585
+
586
+ return new RegExp(`^${regex}$`).test(filePath)
587
+ }
588
+ }