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.
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 +18 -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 -304
  33. package/lib/simple-vector-store.js +0 -108
  34. package/lib/treesitter-semantic-chunker.js +0 -1485
  35. package/lib/websocket-manager.js +0 -470
  36. package/server.js +0 -687
@@ -0,0 +1,387 @@
1
+ /**
2
+ * API Router for cntx-ui
3
+ * Handles all HTTP API endpoints and request routing
4
+ */
5
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
6
+ import path, { join } from 'path';
7
+ export default class APIRouter {
8
+ cntxServer;
9
+ configManager;
10
+ bundleManager;
11
+ fileSystemManager;
12
+ semanticAnalysisManager;
13
+ vectorStore;
14
+ activityManager;
15
+ constructor(cntxServer, configManager, bundleManager, fileSystemManager, semanticAnalysisManager, vectorStore, activityManager) {
16
+ this.cntxServer = cntxServer;
17
+ this.configManager = configManager;
18
+ this.bundleManager = bundleManager;
19
+ this.fileSystemManager = fileSystemManager;
20
+ this.semanticAnalysisManager = semanticAnalysisManager;
21
+ this.vectorStore = vectorStore;
22
+ this.activityManager = activityManager;
23
+ }
24
+ async handleRequest(req, res, url) {
25
+ const { pathname } = url;
26
+ const method = req.method;
27
+ try {
28
+ // === Bundle Endpoints ===
29
+ if (pathname === '/api/bundles' && method === 'GET') {
30
+ return await this.handleGetBundles(req, res, url);
31
+ }
32
+ if (pathname === '/api/bundles' && method === 'POST') {
33
+ return await this.handlePostBundles(req, res);
34
+ }
35
+ if (pathname.startsWith('/api/bundles/') && method === 'GET') {
36
+ const bundleName = pathname.split('/')[3];
37
+ return await this.handleGetBundle(req, res, bundleName);
38
+ }
39
+ if (pathname.startsWith('/api/regenerate/') && (method === 'GET' || method === 'POST')) {
40
+ const bundleName = pathname.split('/')[3];
41
+ return await this.handleRegenerateBundle(req, res, bundleName);
42
+ }
43
+ // === File Endpoints ===
44
+ if (pathname === '/api/files' && method === 'GET') {
45
+ return await this.handleGetFiles(req, res);
46
+ }
47
+ if (pathname.startsWith('/api/files/') && method === 'GET') {
48
+ const filePath = pathname.substring(11); // Remove /api/files/
49
+ return await this.handleGetFile(req, res, filePath);
50
+ }
51
+ if (pathname === '/api/open-file' && method === 'POST') {
52
+ return await this.handlePostOpenFile(req, res);
53
+ }
54
+ // === Configuration Endpoints ===
55
+ if (pathname === '/api/config' && method === 'GET') {
56
+ return await this.handleGetConfig(req, res);
57
+ }
58
+ if (pathname === '/api/config' && method === 'POST') {
59
+ return await this.handlePostConfig(req, res);
60
+ }
61
+ if (pathname === '/api/cntxignore' && method === 'GET') {
62
+ return await this.handleGetCntxignore(req, res);
63
+ }
64
+ if (pathname === '/api/cntxignore' && method === 'POST') {
65
+ return await this.handlePostCntxignore(req, res);
66
+ }
67
+ // === Semantic Analysis Endpoints ===
68
+ if (pathname === '/api/semantic-chunks' && method === 'GET') {
69
+ return await this.handleGetSemanticChunks(req, res, url);
70
+ }
71
+ if (pathname === '/api/semantic-search' && method === 'POST') {
72
+ return await this.handlePostSemanticSearch(req, res);
73
+ }
74
+ // === Vector DB Endpoints ===
75
+ if (pathname === '/api/vector-db/status' && method === 'GET') {
76
+ return await this.handleGetVectorDbStatus(req, res);
77
+ }
78
+ if (pathname === '/api/vector-db/rebuild' && method === 'POST') {
79
+ return await this.handlePostVectorDbRebuild(req, res);
80
+ }
81
+ if (pathname === '/api/vector-db/search' && method === 'POST') {
82
+ return await this.handlePostVectorDbSearch(req, res);
83
+ }
84
+ if (pathname === '/api/vector-db/network' && method === 'GET') {
85
+ return await this.handleGetVectorDbNetwork(req, res);
86
+ }
87
+ // === Database Endpoints ===
88
+ if (pathname === '/api/database/info' && method === 'GET') {
89
+ return await this.handleGetDatabaseInfo(req, res);
90
+ }
91
+ if (pathname === '/api/database/query' && method === 'POST') {
92
+ return await this.handlePostDatabaseQuery(req, res);
93
+ }
94
+ // === Activity Endpoints ===
95
+ if (pathname === '/api/activities' && method === 'GET') {
96
+ return await this.handleGetActivities(req, res);
97
+ }
98
+ if (pathname.startsWith('/api/activities/') && pathname.endsWith('/reasoning') && method === 'GET') {
99
+ const activityId = pathname.split('/')[3];
100
+ return await this.handleGetActivityReasoning(req, res, activityId);
101
+ }
102
+ // === Status & MCP ===
103
+ if (pathname === '/api/status' && method === 'GET') {
104
+ return await this.handleGetStatus(req, res);
105
+ }
106
+ if (pathname === '/api/mcp-status' && method === 'GET') {
107
+ return await this.handleGetMcpStatus(req, res);
108
+ }
109
+ // === Rule Management ===
110
+ if (pathname === '/api/cursor-rules' && method === 'GET') {
111
+ return await this.handleGetCursorRules(req, res);
112
+ }
113
+ if (pathname === '/api/cursor-rules' && method === 'POST') {
114
+ return await this.handlePostCursorRules(req, res);
115
+ }
116
+ if (pathname === '/api/claude-md' && method === 'GET') {
117
+ return await this.handleGetClaudeMd(req, res);
118
+ }
119
+ if (pathname === '/api/claude-md' && method === 'POST') {
120
+ return await this.handlePostClaudeMd(req, res);
121
+ }
122
+ // 404 for unknown API routes
123
+ this.sendError(res, 404, `API endpoint not found: ${method} ${pathname}`);
124
+ }
125
+ catch (error) {
126
+ console.error(`API Error: ${error.message}`);
127
+ this.sendError(res, 500, error.message);
128
+ }
129
+ }
130
+ // === Helper Methods ===
131
+ sendResponse(res, status, data) {
132
+ res.writeHead(status, { 'Content-Type': 'application/json' });
133
+ res.end(JSON.stringify(data));
134
+ }
135
+ sendError(res, status, message) {
136
+ this.sendResponse(res, status, { error: message });
137
+ }
138
+ async getRequestBody(req) {
139
+ return new Promise((resolve, reject) => {
140
+ let body = '';
141
+ req.on('data', chunk => { body += chunk.toString(); });
142
+ req.on('end', () => { resolve(body); });
143
+ req.on('error', reject);
144
+ });
145
+ }
146
+ // === Handlers ===
147
+ async handleGetBundles(req, res, url) {
148
+ try {
149
+ const bundleInfo = this.bundleManager.getAllBundleInfo();
150
+ this.sendResponse(res, 200, bundleInfo);
151
+ }
152
+ catch (error) {
153
+ this.sendError(res, 500, error.message);
154
+ }
155
+ }
156
+ async handlePostBundles(req, res) {
157
+ const body = await this.getRequestBody(req);
158
+ const { action, bundleName, fileName, fileNames } = JSON.parse(body);
159
+ if (!action || !bundleName) {
160
+ return this.sendError(res, 400, 'Missing required fields: action and bundleName');
161
+ }
162
+ const bundles = this.configManager.getBundles();
163
+ const bundle = bundles.get(bundleName);
164
+ if (!bundle) {
165
+ return this.sendError(res, 404, `Bundle not found: ${bundleName}`);
166
+ }
167
+ switch (action) {
168
+ case 'add-file':
169
+ if (!fileName)
170
+ return this.sendError(res, 400, 'Missing fileName');
171
+ const relAdd = fileName.startsWith('/') ? path.relative(this.configManager.CWD, fileName) : fileName;
172
+ if (!bundle.files.includes(relAdd)) {
173
+ bundle.files.push(relAdd);
174
+ bundle.changed = true;
175
+ this.configManager.saveBundleStates();
176
+ }
177
+ break;
178
+ case 'remove-file':
179
+ if (!fileName)
180
+ return this.sendError(res, 400, 'Missing fileName');
181
+ const relRem = fileName.startsWith('/') ? path.relative(this.configManager.CWD, fileName) : fileName;
182
+ const idx = bundle.files.indexOf(relRem);
183
+ if (idx > -1) {
184
+ bundle.files.splice(idx, 1);
185
+ bundle.changed = true;
186
+ this.configManager.saveBundleStates();
187
+ }
188
+ break;
189
+ }
190
+ this.sendResponse(res, 200, { success: true });
191
+ }
192
+ async handleGetBundle(req, res, bundleName) {
193
+ const content = await this.bundleManager.getBundleContent(bundleName);
194
+ if (content === null)
195
+ return this.sendError(res, 404, 'Bundle not found');
196
+ res.writeHead(200, { 'Content-Type': 'application/xml' });
197
+ res.end(content);
198
+ }
199
+ async handleRegenerateBundle(req, res, bundleName) {
200
+ await this.bundleManager.regenerateBundle(bundleName);
201
+ this.sendResponse(res, 200, { success: true });
202
+ }
203
+ async handleGetFiles(req, res) {
204
+ const files = this.fileSystemManager.getFileTree();
205
+ this.sendResponse(res, 200, files);
206
+ }
207
+ async handleGetFile(req, res, filePath) {
208
+ const fullPath = this.fileSystemManager.fullPath(filePath);
209
+ if (!existsSync(fullPath))
210
+ return this.sendError(res, 404, 'File not found');
211
+ const content = readFileSync(fullPath, 'utf8');
212
+ res.writeHead(200, { 'Content-Type': 'text/plain' });
213
+ res.end(content);
214
+ }
215
+ async handlePostOpenFile(req, res) {
216
+ const body = await this.getRequestBody(req);
217
+ const { filePath, line } = JSON.parse(body);
218
+ // Simple mock for opening in editor
219
+ console.log(`Editor requested for ${filePath}:${line || 1}`);
220
+ this.sendResponse(res, 200, { success: true });
221
+ }
222
+ async handleGetConfig(req, res) {
223
+ const config = this.configManager.loadConfig();
224
+ this.sendResponse(res, 200, config);
225
+ }
226
+ async handlePostConfig(req, res) {
227
+ const body = await this.getRequestBody(req);
228
+ this.configManager.saveConfig(JSON.parse(body));
229
+ this.sendResponse(res, 200, { success: true });
230
+ }
231
+ async handleGetCntxignore(req, res) {
232
+ if (existsSync(this.configManager.IGNORE_FILE)) {
233
+ const content = readFileSync(this.configManager.IGNORE_FILE, 'utf8');
234
+ res.writeHead(200, { 'Content-Type': 'text/plain' });
235
+ res.end(content);
236
+ }
237
+ else {
238
+ res.end('');
239
+ }
240
+ }
241
+ async handlePostCntxignore(req, res) {
242
+ const body = await this.getRequestBody(req);
243
+ const { content } = JSON.parse(body);
244
+ const success = this.configManager.saveCntxignore(content);
245
+ if (!success)
246
+ return this.sendError(res, 500, 'Failed to save .cntxignore');
247
+ this.fileSystemManager.setIgnorePatterns(this.configManager.ignorePatterns);
248
+ this.sendResponse(res, 200, { success: true });
249
+ }
250
+ async handleGetSemanticChunks(req, res, url) {
251
+ const refresh = url.query?.refresh === 'true';
252
+ let analysis;
253
+ if (refresh) {
254
+ analysis = await this.cntxServer.refreshSemanticAnalysis();
255
+ }
256
+ else {
257
+ analysis = await this.cntxServer.getSemanticAnalysis();
258
+ }
259
+ const chunks = analysis.chunks.map((chunk) => ({
260
+ id: chunk.id || chunk.name,
261
+ name: chunk.name,
262
+ code: chunk.code,
263
+ semanticType: chunk.subtype || chunk.type,
264
+ businessDomain: chunk.businessDomain || [],
265
+ technicalPatterns: chunk.technicalPatterns || [],
266
+ purpose: chunk.purpose || '',
267
+ filePath: chunk.filePath,
268
+ complexity: chunk.complexity || { score: 0, level: 'low' },
269
+ tags: chunk.tags || [],
270
+ startLine: chunk.startLine
271
+ }));
272
+ this.sendResponse(res, 200, { summary: analysis.summary, chunks });
273
+ }
274
+ async handlePostSemanticSearch(req, res) {
275
+ const body = await this.getRequestBody(req);
276
+ const { query, limit = 20 } = JSON.parse(body);
277
+ const results = await this.vectorStore.search(query, { limit });
278
+ this.sendResponse(res, 200, { results });
279
+ }
280
+ async handleGetVectorDbStatus(req, res) {
281
+ const info = this.configManager.dbManager.getInfo();
282
+ this.sendResponse(res, 200, {
283
+ stats: {
284
+ totalChunks: info.chunkCount,
285
+ embeddingCount: info.embeddingCount,
286
+ modelName: this.vectorStore.modelName
287
+ }
288
+ });
289
+ }
290
+ async handlePostVectorDbRebuild(req, res) {
291
+ const analysis = await this.cntxServer.getSemanticAnalysis();
292
+ for (const chunk of analysis.chunks) {
293
+ await this.vectorStore.upsertChunk(chunk);
294
+ }
295
+ const info = this.configManager.dbManager.getInfo();
296
+ this.sendResponse(res, 200, { success: true, embeddingCount: info.embeddingCount });
297
+ }
298
+ async handlePostVectorDbSearch(req, res) {
299
+ const body = await this.getRequestBody(req);
300
+ const { query, limit = 10 } = JSON.parse(body);
301
+ const results = await this.vectorStore.search(query, { limit });
302
+ this.sendResponse(res, 200, results);
303
+ }
304
+ async handleGetVectorDbNetwork(req, res) {
305
+ const chunks = this.configManager.dbManager.db.prepare('SELECT * FROM semantic_chunks').all();
306
+ const embeddings = this.configManager.dbManager.db.prepare('SELECT * FROM vector_embeddings').all();
307
+ const nodes = chunks.map(c => this.configManager.dbManager.mapChunkRow(c)).slice(0, 100);
308
+ const edges = [];
309
+ // Simple pairwise similarity
310
+ this.sendResponse(res, 200, { nodes, edges });
311
+ }
312
+ async handleGetDatabaseInfo(req, res) {
313
+ const info = this.configManager.dbManager.getInfo();
314
+ this.sendResponse(res, 200, info);
315
+ }
316
+ async handlePostDatabaseQuery(req, res) {
317
+ const body = await this.getRequestBody(req);
318
+ const { query } = JSON.parse(body);
319
+ const results = this.configManager.dbManager.query(query);
320
+ this.sendResponse(res, 200, { results });
321
+ }
322
+ async handleGetActivities(req, res) {
323
+ const activities = await this.activityManager.loadActivities();
324
+ this.sendResponse(res, 200, activities);
325
+ }
326
+ async handleGetActivityReasoning(req, res, activityId) {
327
+ const history = this.configManager.dbManager.getSessionHistory(activityId);
328
+ this.sendResponse(res, 200, { history });
329
+ }
330
+ async handleGetStatus(req, res) {
331
+ const bundles = this.bundleManager.getAllBundleInfo();
332
+ this.sendResponse(res, 200, {
333
+ uptime: process.uptime(),
334
+ memory: process.memoryUsage(),
335
+ bundles,
336
+ scanning: this.bundleManager.isScanning,
337
+ totalFiles: this.fileSystemManager.getAllFiles().length,
338
+ mcp: {
339
+ enabled: this.cntxServer.mcpServerStarted,
340
+ available: true
341
+ }
342
+ });
343
+ }
344
+ async handleGetMcpStatus(req, res) {
345
+ const isRunning = this.cntxServer.mcpServerStarted;
346
+ this.sendResponse(res, 200, {
347
+ enabled: isRunning,
348
+ running: isRunning,
349
+ available: true,
350
+ message: isRunning ? 'MCP server is running' : 'MCP server integration available'
351
+ });
352
+ }
353
+ async handleGetCursorRules(req, res) {
354
+ const filePath = join(this.configManager.CWD, '.cursorrules');
355
+ if (existsSync(filePath)) {
356
+ const content = readFileSync(filePath, 'utf8');
357
+ res.writeHead(200, { 'Content-Type': 'text/plain' });
358
+ res.end(content);
359
+ }
360
+ else {
361
+ res.end('');
362
+ }
363
+ }
364
+ async handlePostCursorRules(req, res) {
365
+ const body = await this.getRequestBody(req);
366
+ const { content } = JSON.parse(body);
367
+ writeFileSync(join(this.configManager.CWD, '.cursorrules'), content, 'utf8');
368
+ this.sendResponse(res, 200, { success: true });
369
+ }
370
+ async handleGetClaudeMd(req, res) {
371
+ const filePath = join(this.configManager.CWD, 'CLAUDE.md');
372
+ if (existsSync(filePath)) {
373
+ const content = readFileSync(filePath, 'utf8');
374
+ res.writeHead(200, { 'Content-Type': 'text/plain' });
375
+ res.end(content);
376
+ }
377
+ else {
378
+ res.end('');
379
+ }
380
+ }
381
+ async handlePostClaudeMd(req, res) {
382
+ const body = await this.getRequestBody(req);
383
+ const { content } = JSON.parse(body);
384
+ writeFileSync(join(this.configManager.CWD, 'CLAUDE.md'), content, 'utf8');
385
+ this.sendResponse(res, 200, { success: true });
386
+ }
387
+ }
@@ -0,0 +1,236 @@
1
+ /**
2
+ * Bundle Manager for cntx-ui
3
+ * Handles Smart Dynamic Bundles and traditional XML generation
4
+ */
5
+ import { readFileSync } from 'fs';
6
+ export default class BundleManager {
7
+ configManager;
8
+ fileSystemManager;
9
+ webSocketManager;
10
+ db;
11
+ verbose;
12
+ _isScanning;
13
+ constructor(configManager, fileSystemManager, verbose = false) {
14
+ this.configManager = configManager;
15
+ this.fileSystemManager = fileSystemManager;
16
+ this.webSocketManager = null;
17
+ this.db = configManager.dbManager;
18
+ this.verbose = verbose;
19
+ this._isScanning = false;
20
+ }
21
+ /**
22
+ * Get all bundle information, including Smart Dynamic Bundles
23
+ */
24
+ getAllBundleInfo() {
25
+ const manualBundles = Array.from(this.configManager.getBundles().entries()).map(([name, bundle]) => ({
26
+ name,
27
+ fileCount: bundle.files?.length || 0,
28
+ size: bundle.size || 0,
29
+ generated: bundle.generated,
30
+ changed: !!bundle.changed,
31
+ patterns: bundle.patterns,
32
+ type: 'manual'
33
+ }));
34
+ const smartBundles = this.generateSmartBundleDefinitions();
35
+ // Filter out smart bundles that have no files
36
+ const activeSmartBundles = smartBundles.map(b => {
37
+ const files = this.resolveSmartBundle(b.name);
38
+ return {
39
+ ...b,
40
+ fileCount: files.length,
41
+ files
42
+ };
43
+ }).filter(b => b.fileCount > 0);
44
+ return [...manualBundles, ...activeSmartBundles];
45
+ }
46
+ /**
47
+ * Generate Smart Bundle definitions from indexed semantic data
48
+ */
49
+ generateSmartBundleDefinitions() {
50
+ const smartBundles = [];
51
+ try {
52
+ // 1. Group by Purpose (Heuristics)
53
+ const purposeRows = this.db.db.prepare('SELECT DISTINCT purpose, COUNT(*) as count FROM semantic_chunks GROUP BY purpose').all();
54
+ purposeRows.forEach(row => {
55
+ if (!row.purpose)
56
+ return;
57
+ const name = `smart:${row.purpose.toLowerCase().replace(/\s+/g, '-')}`;
58
+ smartBundles.push({
59
+ name,
60
+ purpose: row.purpose,
61
+ fileCount: row.count,
62
+ size: 0,
63
+ changed: false,
64
+ type: 'smart',
65
+ description: `Automatically grouped by purpose: ${row.purpose}`
66
+ });
67
+ });
68
+ // 2. Group by Component Types (Subtypes)
69
+ const subtypeRows = this.db.db.prepare('SELECT DISTINCT subtype, COUNT(*) as count FROM semantic_chunks GROUP BY subtype').all();
70
+ subtypeRows.forEach(row => {
71
+ if (!row.subtype)
72
+ return;
73
+ const name = `smart:type-${row.subtype.toLowerCase().replace(/_/g, '-')}`;
74
+ smartBundles.push({
75
+ name,
76
+ purpose: row.subtype,
77
+ fileCount: row.count,
78
+ size: 0,
79
+ changed: false,
80
+ type: 'smart',
81
+ description: `All ${row.subtype} elements across the codebase`
82
+ });
83
+ });
84
+ }
85
+ catch (e) {
86
+ if (this.verbose)
87
+ console.warn('Smart bundle discovery failed:', e.message);
88
+ }
89
+ return smartBundles;
90
+ }
91
+ /**
92
+ * Resolve files for a bundle (Manual or Smart)
93
+ */
94
+ async resolveBundleFiles(bundleName) {
95
+ if (bundleName.startsWith('smart:')) {
96
+ return this.resolveSmartBundle(bundleName);
97
+ }
98
+ const bundle = this.configManager.getBundles().get(bundleName);
99
+ if (!bundle)
100
+ return [];
101
+ const allFiles = this.fileSystemManager.getAllFiles();
102
+ return allFiles.filter(file => bundle.patterns.some(pattern => this.fileSystemManager.matchesPattern(file, pattern))).map(f => this.fileSystemManager.relativePath(f));
103
+ }
104
+ /**
105
+ * Resolve a Smart Bundle query against SQLite
106
+ */
107
+ resolveSmartBundle(bundleName) {
108
+ const query = bundleName.replace('smart:', '');
109
+ let rows = [];
110
+ if (query.startsWith('type-')) {
111
+ const type = query.replace('type-', '').replace(/-/g, '_');
112
+ rows = this.db.db.prepare('SELECT DISTINCT file_path FROM semantic_chunks WHERE LOWER(subtype) = ?').all(type);
113
+ }
114
+ else {
115
+ const purposeRows = this.db.db.prepare('SELECT DISTINCT purpose FROM semantic_chunks').all();
116
+ const matched = purposeRows.find(r => r.purpose?.toLowerCase().replace(/\s+/g, '-') === query);
117
+ if (matched) {
118
+ rows = this.db.db.prepare('SELECT DISTINCT file_path FROM semantic_chunks WHERE purpose = ?').all(matched.purpose);
119
+ }
120
+ }
121
+ return rows.map(r => r.file_path);
122
+ }
123
+ // === Bundle Generation ===
124
+ async generateAllBundles() {
125
+ this._isScanning = true;
126
+ try {
127
+ const bundles = this.configManager.getBundles();
128
+ for (const [name] of bundles) {
129
+ await this.regenerateBundle(name);
130
+ }
131
+ }
132
+ finally {
133
+ this._isScanning = false;
134
+ }
135
+ }
136
+ async regenerateBundle(bundleName) {
137
+ if (this.verbose)
138
+ console.log(`🔄 Regenerating bundle: ${bundleName}`);
139
+ const files = await this.resolveBundleFiles(bundleName);
140
+ const content = await this.generateBundleXML(bundleName, files);
141
+ const bundleData = {
142
+ files,
143
+ content,
144
+ size: Buffer.byteLength(content, 'utf8'),
145
+ generated: new Date().toISOString(),
146
+ changed: false,
147
+ patterns: [] // Default patterns
148
+ };
149
+ if (!bundleName.startsWith('smart:')) {
150
+ const existing = this.configManager.getBundles().get(bundleName);
151
+ if (existing) {
152
+ bundleData.patterns = existing.patterns;
153
+ }
154
+ this.configManager.getBundles().set(bundleName, bundleData);
155
+ this.configManager.saveBundleStates();
156
+ }
157
+ return bundleData;
158
+ }
159
+ async generateBundleXML(bundleName, relativeFiles) {
160
+ const projectInfo = this.getProjectInfo();
161
+ let xml = `<?xml version="1.0" encoding="UTF-8"?>\n<codebase>\n`;
162
+ xml += ` <project_info>\n <name>${this.escapeXml(projectInfo.name)}</name>\n <bundle>${this.escapeXml(bundleName)}</bundle>\n </project_info>\n`;
163
+ for (const relPath of relativeFiles) {
164
+ xml += await this.generateFileXML(relPath);
165
+ }
166
+ xml += `</codebase>`;
167
+ return xml;
168
+ }
169
+ async generateFileXML(relativePath) {
170
+ try {
171
+ const fullPath = this.fileSystemManager.fullPath(relativePath);
172
+ const content = readFileSync(fullPath, 'utf8');
173
+ const chunks = this.db.getChunksByFile(relativePath);
174
+ let xml = ` <file path="${this.escapeXml(relativePath)}">\n`;
175
+ if (chunks.length > 0) {
176
+ xml += ` <semantic_context>\n`;
177
+ chunks.forEach(c => {
178
+ xml += ` <chunk name="${this.escapeXml(c.name)}" purpose="${this.escapeXml(c.purpose)}" complexity="${c.complexity?.score || 0}" />\n`;
179
+ });
180
+ xml += ` </semantic_context>\n`;
181
+ }
182
+ xml += ` <content><![CDATA[${content}]]></content>\n </file>\n`;
183
+ return xml;
184
+ }
185
+ catch (e) {
186
+ return ` <file path="${this.escapeXml(relativePath)}" error="${this.escapeXml(e.message)}" />\n`;
187
+ }
188
+ }
189
+ getProjectInfo() {
190
+ try {
191
+ const pkg = JSON.parse(readFileSync(this.fileSystemManager.fullPath('package.json'), 'utf8'));
192
+ return { name: pkg.name || 'Unknown', version: pkg.version || '1.0.0' };
193
+ }
194
+ catch {
195
+ return { name: 'Unknown', version: '1.0.0' };
196
+ }
197
+ }
198
+ escapeXml(text) {
199
+ return String(text).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&apos;');
200
+ }
201
+ getBundleContent(bundleName) {
202
+ if (bundleName.startsWith('smart:')) {
203
+ // For smart bundles, we generate on the fly if not cached
204
+ return this.regenerateBundle(bundleName).then(data => data.content || '');
205
+ }
206
+ const bundle = this.configManager.getBundles().get(bundleName);
207
+ return bundle ? bundle.content || null : null;
208
+ }
209
+ markBundlesChanged(filename) {
210
+ this.configManager.getBundles().forEach((bundle, name) => {
211
+ if (bundle.patterns?.some(p => this.fileSystemManager.matchesPattern(filename, p))) {
212
+ bundle.changed = true;
213
+ }
214
+ });
215
+ }
216
+ getBundleInfo(bundleName) {
217
+ if (bundleName.startsWith('smart:')) {
218
+ return this.generateSmartBundleDefinitions().find(b => b.name === bundleName);
219
+ }
220
+ const bundle = this.configManager.getBundles().get(bundleName);
221
+ if (!bundle)
222
+ return undefined;
223
+ return {
224
+ name: bundleName,
225
+ fileCount: bundle.files?.length || 0,
226
+ size: bundle.size || 0,
227
+ generated: bundle.generated,
228
+ changed: !!bundle.changed,
229
+ patterns: bundle.patterns,
230
+ type: 'manual'
231
+ };
232
+ }
233
+ get isScanning() {
234
+ return this._isScanning;
235
+ }
236
+ }