cntx-ui 3.0.7 → 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.
- package/dist/bin/cntx-ui.js +70 -0
- package/dist/lib/agent-runtime.js +269 -0
- package/dist/lib/agent-tools.js +162 -0
- package/dist/lib/api-router.js +387 -0
- package/dist/lib/bundle-manager.js +236 -0
- package/dist/lib/configuration-manager.js +230 -0
- package/dist/lib/database-manager.js +277 -0
- package/dist/lib/file-system-manager.js +305 -0
- package/dist/lib/function-level-chunker.js +144 -0
- package/dist/lib/heuristics-manager.js +491 -0
- package/dist/lib/mcp-server.js +159 -0
- package/dist/lib/mcp-transport.js +10 -0
- package/dist/lib/semantic-splitter.js +335 -0
- package/dist/lib/simple-vector-store.js +98 -0
- package/dist/lib/treesitter-semantic-chunker.js +277 -0
- package/dist/lib/websocket-manager.js +268 -0
- package/dist/server.js +225 -0
- package/package.json +18 -8
- package/bin/cntx-ui-mcp.sh +0 -3
- package/bin/cntx-ui.js +0 -123
- package/lib/agent-runtime.js +0 -371
- package/lib/agent-tools.js +0 -370
- package/lib/api-router.js +0 -1026
- package/lib/bundle-manager.js +0 -326
- package/lib/configuration-manager.js +0 -760
- package/lib/database-manager.js +0 -397
- package/lib/file-system-manager.js +0 -489
- package/lib/function-level-chunker.js +0 -406
- package/lib/heuristics-manager.js +0 -529
- package/lib/mcp-server.js +0 -1380
- package/lib/mcp-transport.js +0 -97
- package/lib/semantic-splitter.js +0 -304
- package/lib/simple-vector-store.js +0 -108
- package/lib/treesitter-semantic-chunker.js +0 -1485
- package/lib/websocket-manager.js +0 -470
- package/server.js +0 -687
package/lib/bundle-manager.js
DELETED
|
@@ -1,326 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Bundle Manager for cntx-ui
|
|
3
|
-
* Handles Smart Dynamic Bundles and traditional XML generation
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { readFileSync, statSync } from 'fs';
|
|
7
|
-
import { relative, extname, basename, dirname } from 'path';
|
|
8
|
-
|
|
9
|
-
export default class BundleManager {
|
|
10
|
-
constructor(configManager, fileSystemManager, verbose = false) {
|
|
11
|
-
this.configManager = configManager;
|
|
12
|
-
this.fileSystemManager = fileSystemManager;
|
|
13
|
-
this.db = configManager.dbManager;
|
|
14
|
-
this.verbose = verbose;
|
|
15
|
-
this._isScanning = false;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Get all bundle information, including Smart Dynamic Bundles
|
|
20
|
-
*/
|
|
21
|
-
getAllBundleInfo() {
|
|
22
|
-
if (this.verbose) console.log('📦 Getting all bundle info...');
|
|
23
|
-
const manualBundles = Array.from(this.configManager.getBundles().entries()).map(([name, bundle]) => ({
|
|
24
|
-
name,
|
|
25
|
-
fileCount: bundle.files?.length || 0,
|
|
26
|
-
size: bundle.size || 0,
|
|
27
|
-
generated: bundle.generated,
|
|
28
|
-
changed: bundle.changed,
|
|
29
|
-
patterns: bundle.patterns,
|
|
30
|
-
type: 'manual'
|
|
31
|
-
}));
|
|
32
|
-
|
|
33
|
-
if (this.verbose) console.log(`📦 Found ${manualBundles.length} manual bundles`);
|
|
34
|
-
|
|
35
|
-
const smartBundles = this.generateSmartBundleDefinitions();
|
|
36
|
-
if (this.verbose) console.log(`📦 Found ${smartBundles.length} smart bundle definitions`);
|
|
37
|
-
|
|
38
|
-
// Filter out smart bundles that have no files
|
|
39
|
-
const activeSmartBundles = smartBundles.map(b => {
|
|
40
|
-
const files = this.resolveSmartBundle(b.name);
|
|
41
|
-
return {
|
|
42
|
-
...b,
|
|
43
|
-
fileCount: files.length,
|
|
44
|
-
files
|
|
45
|
-
};
|
|
46
|
-
}).filter(b => b.fileCount > 0);
|
|
47
|
-
|
|
48
|
-
if (this.verbose) console.log(`📦 Active smart bundles: ${activeSmartBundles.length}`);
|
|
49
|
-
|
|
50
|
-
return [...manualBundles, ...activeSmartBundles];
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Generate Smart Bundle definitions from indexed semantic data.
|
|
55
|
-
* Uses business domain, directory structure, and technical patterns
|
|
56
|
-
* instead of raw AST node types.
|
|
57
|
-
*/
|
|
58
|
-
generateSmartBundleDefinitions() {
|
|
59
|
-
const smartBundles = [];
|
|
60
|
-
const MIN_CHUNKS = 3; // Skip bundles with fewer than this many chunks
|
|
61
|
-
|
|
62
|
-
try {
|
|
63
|
-
// 1. Group by business domain (from metadata JSON)
|
|
64
|
-
const allRows = this.db.db.prepare('SELECT metadata FROM semantic_chunks WHERE metadata IS NOT NULL').all();
|
|
65
|
-
const domainCounts = new Map();
|
|
66
|
-
for (const row of allRows) {
|
|
67
|
-
try {
|
|
68
|
-
const meta = JSON.parse(row.metadata);
|
|
69
|
-
for (const domain of (meta.businessDomain || [])) {
|
|
70
|
-
domainCounts.set(domain, (domainCounts.get(domain) || 0) + 1);
|
|
71
|
-
}
|
|
72
|
-
} catch { /* skip malformed */ }
|
|
73
|
-
}
|
|
74
|
-
for (const [domain, count] of domainCounts) {
|
|
75
|
-
if (count < MIN_CHUNKS) continue;
|
|
76
|
-
smartBundles.push({
|
|
77
|
-
name: `smart:${domain}`,
|
|
78
|
-
purpose: domain,
|
|
79
|
-
fileCount: count,
|
|
80
|
-
type: 'smart',
|
|
81
|
-
description: `${count} chunks in the ${domain} domain`
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// 2. Group by directory structure (components, hooks, pages, api, etc.)
|
|
86
|
-
const dirRows = this.db.db.prepare(`
|
|
87
|
-
SELECT
|
|
88
|
-
CASE
|
|
89
|
-
WHEN file_path LIKE '%/hooks/%' THEN 'hooks'
|
|
90
|
-
WHEN file_path LIKE '%/components/%' THEN 'components'
|
|
91
|
-
WHEN file_path LIKE '%/api/%' OR file_path LIKE '%/services/%' THEN 'api-services'
|
|
92
|
-
WHEN file_path LIKE '%/pages/%' OR file_path LIKE '%/routes/%' THEN 'pages'
|
|
93
|
-
WHEN file_path LIKE '%/stores/%' OR file_path LIKE '%/store/%' THEN 'state'
|
|
94
|
-
WHEN file_path LIKE '%/lib/%' OR file_path LIKE '%/utils/%' THEN 'lib-utils'
|
|
95
|
-
WHEN file_path LIKE '%/types/%' THEN 'types'
|
|
96
|
-
ELSE NULL
|
|
97
|
-
END as dir_group,
|
|
98
|
-
COUNT(DISTINCT file_path) as file_cnt
|
|
99
|
-
FROM semantic_chunks
|
|
100
|
-
GROUP BY dir_group
|
|
101
|
-
HAVING dir_group IS NOT NULL
|
|
102
|
-
`).all();
|
|
103
|
-
for (const row of dirRows) {
|
|
104
|
-
if (row.file_cnt < 2) continue;
|
|
105
|
-
smartBundles.push({
|
|
106
|
-
name: `smart:dir-${row.dir_group}`,
|
|
107
|
-
purpose: row.dir_group,
|
|
108
|
-
fileCount: row.file_cnt,
|
|
109
|
-
type: 'smart',
|
|
110
|
-
description: `${row.file_cnt} files in ${row.dir_group} directories`
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// 3. Group by technical pattern (react-hooks, async-io, event-driven)
|
|
115
|
-
const patternCounts = new Map();
|
|
116
|
-
for (const row of allRows) {
|
|
117
|
-
try {
|
|
118
|
-
const meta = JSON.parse(row.metadata);
|
|
119
|
-
for (const pattern of (meta.technicalPatterns || [])) {
|
|
120
|
-
if (pattern === 'public-api') continue; // Too generic
|
|
121
|
-
patternCounts.set(pattern, (patternCounts.get(pattern) || 0) + 1);
|
|
122
|
-
}
|
|
123
|
-
} catch { /* skip */ }
|
|
124
|
-
}
|
|
125
|
-
for (const [pattern, count] of patternCounts) {
|
|
126
|
-
if (count < MIN_CHUNKS) continue;
|
|
127
|
-
smartBundles.push({
|
|
128
|
-
name: `smart:pattern-${pattern}`,
|
|
129
|
-
purpose: pattern,
|
|
130
|
-
fileCount: count,
|
|
131
|
-
type: 'smart',
|
|
132
|
-
description: `${count} chunks using ${pattern} patterns`
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
} catch (e) {
|
|
136
|
-
if (this.verbose) console.warn('Smart bundle discovery failed:', e.message);
|
|
137
|
-
}
|
|
138
|
-
return smartBundles;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Resolve files for a bundle (Manual or Smart)
|
|
143
|
-
*/
|
|
144
|
-
async resolveBundleFiles(bundleName) {
|
|
145
|
-
if (bundleName.startsWith('smart:')) {
|
|
146
|
-
return this.resolveSmartBundle(bundleName);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const bundle = this.configManager.getBundles().get(bundleName);
|
|
150
|
-
if (!bundle) return [];
|
|
151
|
-
|
|
152
|
-
const allFiles = this.fileSystemManager.getAllFiles();
|
|
153
|
-
return allFiles.filter(file =>
|
|
154
|
-
bundle.patterns.some(pattern => this.fileSystemManager.matchesPattern(file, pattern))
|
|
155
|
-
).map(f => this.fileSystemManager.relativePath(f));
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Resolve a Smart Bundle query against SQLite
|
|
160
|
-
*/
|
|
161
|
-
resolveSmartBundle(bundleName) {
|
|
162
|
-
const query = bundleName.replace('smart:', '');
|
|
163
|
-
let rows = [];
|
|
164
|
-
|
|
165
|
-
if (query.startsWith('dir-')) {
|
|
166
|
-
// Directory-based bundle
|
|
167
|
-
const dirGroup = query.replace('dir-', '');
|
|
168
|
-
const dirPatterns = {
|
|
169
|
-
'hooks': '%/hooks/%',
|
|
170
|
-
'components': '%/components/%',
|
|
171
|
-
'api-services': null, // handled below
|
|
172
|
-
'pages': null,
|
|
173
|
-
'state': null,
|
|
174
|
-
'lib-utils': null,
|
|
175
|
-
'types': '%/types/%'
|
|
176
|
-
};
|
|
177
|
-
if (dirGroup === 'api-services') {
|
|
178
|
-
rows = this.db.db.prepare("SELECT DISTINCT file_path FROM semantic_chunks WHERE file_path LIKE '%/api/%' OR file_path LIKE '%/services/%'").all();
|
|
179
|
-
} else if (dirGroup === 'pages') {
|
|
180
|
-
rows = this.db.db.prepare("SELECT DISTINCT file_path FROM semantic_chunks WHERE file_path LIKE '%/pages/%' OR file_path LIKE '%/routes/%'").all();
|
|
181
|
-
} else if (dirGroup === 'state') {
|
|
182
|
-
rows = this.db.db.prepare("SELECT DISTINCT file_path FROM semantic_chunks WHERE file_path LIKE '%/stores/%' OR file_path LIKE '%/store/%'").all();
|
|
183
|
-
} else if (dirGroup === 'lib-utils') {
|
|
184
|
-
rows = this.db.db.prepare("SELECT DISTINCT file_path FROM semantic_chunks WHERE file_path LIKE '%/lib/%' OR file_path LIKE '%/utils/%'").all();
|
|
185
|
-
} else if (dirPatterns[dirGroup]) {
|
|
186
|
-
rows = this.db.db.prepare('SELECT DISTINCT file_path FROM semantic_chunks WHERE file_path LIKE ?').all(dirPatterns[dirGroup]);
|
|
187
|
-
}
|
|
188
|
-
} else if (query.startsWith('pattern-')) {
|
|
189
|
-
// Technical pattern bundle — search metadata JSON
|
|
190
|
-
const pattern = query.replace('pattern-', '');
|
|
191
|
-
const allRows = this.db.db.prepare('SELECT DISTINCT file_path, metadata FROM semantic_chunks WHERE metadata IS NOT NULL').all();
|
|
192
|
-
const files = new Set();
|
|
193
|
-
for (const row of allRows) {
|
|
194
|
-
try {
|
|
195
|
-
const meta = JSON.parse(row.metadata);
|
|
196
|
-
if ((meta.technicalPatterns || []).includes(pattern)) {
|
|
197
|
-
files.add(row.file_path);
|
|
198
|
-
}
|
|
199
|
-
} catch { /* skip */ }
|
|
200
|
-
}
|
|
201
|
-
return Array.from(files);
|
|
202
|
-
} else {
|
|
203
|
-
// Business domain bundle — search metadata JSON
|
|
204
|
-
const allRows = this.db.db.prepare('SELECT DISTINCT file_path, metadata FROM semantic_chunks WHERE metadata IS NOT NULL').all();
|
|
205
|
-
const files = new Set();
|
|
206
|
-
for (const row of allRows) {
|
|
207
|
-
try {
|
|
208
|
-
const meta = JSON.parse(row.metadata);
|
|
209
|
-
if ((meta.businessDomain || []).includes(query)) {
|
|
210
|
-
files.add(row.file_path);
|
|
211
|
-
}
|
|
212
|
-
} catch { /* skip */ }
|
|
213
|
-
}
|
|
214
|
-
return Array.from(files);
|
|
215
|
-
}
|
|
216
|
-
return rows.map(r => r.file_path);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// === Bundle Generation ===
|
|
220
|
-
|
|
221
|
-
async generateAllBundles() {
|
|
222
|
-
this._isScanning = true;
|
|
223
|
-
try {
|
|
224
|
-
const bundles = this.configManager.getBundles();
|
|
225
|
-
for (const [name] of bundles) {
|
|
226
|
-
await this.regenerateBundle(name);
|
|
227
|
-
}
|
|
228
|
-
} finally {
|
|
229
|
-
this._isScanning = false;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
async regenerateBundle(bundleName) {
|
|
234
|
-
if (this.verbose) console.log(`🔄 Regenerating bundle: ${bundleName}`);
|
|
235
|
-
|
|
236
|
-
const files = await this.resolveBundleFiles(bundleName);
|
|
237
|
-
const content = await this.generateBundleXML(bundleName, files);
|
|
238
|
-
|
|
239
|
-
const bundleData = {
|
|
240
|
-
files,
|
|
241
|
-
content,
|
|
242
|
-
size: Buffer.byteLength(content, 'utf8'),
|
|
243
|
-
generated: new Date().toISOString(),
|
|
244
|
-
changed: false
|
|
245
|
-
};
|
|
246
|
-
|
|
247
|
-
if (!bundleName.startsWith('smart:')) {
|
|
248
|
-
const existing = this.configManager.getBundles().get(bundleName);
|
|
249
|
-
this.configManager.getBundles().set(bundleName, { ...existing, ...bundleData });
|
|
250
|
-
this.configManager.saveBundleStates();
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
return bundleData;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
async generateBundleXML(bundleName, relativeFiles) {
|
|
257
|
-
const projectInfo = this.getProjectInfo();
|
|
258
|
-
let xml = `<?xml version="1.0" encoding="UTF-8"?>\n<codebase>\n`;
|
|
259
|
-
xml += ` <project_info>\n <name>${this.escapeXml(projectInfo.name)}</name>\n <bundle>${this.escapeXml(bundleName)}</bundle>\n </project_info>\n`;
|
|
260
|
-
|
|
261
|
-
for (const relPath of relativeFiles) {
|
|
262
|
-
xml += await this.generateFileXML(relPath);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
xml += `</codebase>`;
|
|
266
|
-
return xml;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
async generateFileXML(relativePath) {
|
|
270
|
-
try {
|
|
271
|
-
const fullPath = this.fileSystemManager.fullPath(relativePath);
|
|
272
|
-
const content = readFileSync(fullPath, 'utf8');
|
|
273
|
-
const chunks = this.db.getChunksByFile(relativePath);
|
|
274
|
-
|
|
275
|
-
let xml = ` <file path="${this.escapeXml(relativePath)}">\n`;
|
|
276
|
-
if (chunks.length > 0) {
|
|
277
|
-
xml += ` <semantic_context>\n`;
|
|
278
|
-
chunks.forEach(c => {
|
|
279
|
-
xml += ` <chunk name="${this.escapeXml(c.name)}" purpose="${this.escapeXml(c.purpose)}" complexity="${c.complexity?.score || 0}" />\n`;
|
|
280
|
-
});
|
|
281
|
-
xml += ` </semantic_context>\n`;
|
|
282
|
-
}
|
|
283
|
-
xml += ` <content><![CDATA[${content}]]></content>\n </file>\n`;
|
|
284
|
-
return xml;
|
|
285
|
-
} catch (e) {
|
|
286
|
-
return ` <file path="${this.escapeXml(relativePath)}" error="${this.escapeXml(e.message)}" />\n`;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
getProjectInfo() {
|
|
291
|
-
try {
|
|
292
|
-
const pkg = JSON.parse(readFileSync(this.fileSystemManager.fullPath('package.json'), 'utf8'));
|
|
293
|
-
return { name: pkg.name || 'Unknown', version: pkg.version || '1.0.0' };
|
|
294
|
-
} catch {
|
|
295
|
-
return { name: 'Unknown', version: '1.0.0' };
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
escapeXml(text) {
|
|
300
|
-
return String(text).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
getBundleContent(bundleName) {
|
|
304
|
-
if (bundleName.startsWith('smart:')) {
|
|
305
|
-
// For smart bundles, we generate on the fly if not cached
|
|
306
|
-
return this.regenerateBundle(bundleName).then(data => data.content);
|
|
307
|
-
}
|
|
308
|
-
const bundle = this.configManager.getBundles().get(bundleName);
|
|
309
|
-
return bundle ? bundle.content : null;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
markBundlesChanged(filename) {
|
|
313
|
-
this.configManager.getBundles().forEach((bundle, name) => {
|
|
314
|
-
if (bundle.patterns?.some(p => this.fileSystemManager.matchesPattern(filename, p))) {
|
|
315
|
-
bundle.changed = true;
|
|
316
|
-
}
|
|
317
|
-
});
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
getBundleInfo(bundleName) {
|
|
321
|
-
if (bundleName.startsWith('smart:')) {
|
|
322
|
-
return this.generateSmartBundleDefinitions().find(b => b.name === bundleName);
|
|
323
|
-
}
|
|
324
|
-
return this.configManager.getBundles().get(bundleName);
|
|
325
|
-
}
|
|
326
|
-
}
|