cntx-ui 2.0.15 → 3.0.1

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 (32) hide show
  1. package/README.md +40 -344
  2. package/bin/cntx-ui-mcp.sh +3 -0
  3. package/bin/cntx-ui.js +2 -1
  4. package/lib/agent-runtime.js +161 -1340
  5. package/lib/agent-tools.js +9 -7
  6. package/lib/api-router.js +262 -79
  7. package/lib/bundle-manager.js +172 -407
  8. package/lib/configuration-manager.js +94 -59
  9. package/lib/database-manager.js +397 -0
  10. package/lib/file-system-manager.js +17 -0
  11. package/lib/heuristics-manager.js +119 -17
  12. package/lib/mcp-server.js +125 -55
  13. package/lib/semantic-splitter.js +222 -481
  14. package/lib/simple-vector-store.js +69 -300
  15. package/package.json +18 -31
  16. package/server.js +151 -73
  17. package/templates/TOOLS.md +41 -0
  18. package/templates/activities/activities/create-project-bundles/README.md +4 -3
  19. package/templates/activities/activities/create-project-bundles/notes.md +15 -19
  20. package/templates/activities/activities/create-project-bundles/tasks.md +4 -4
  21. package/templates/activities/activities.json +1 -1
  22. package/templates/agent-config.yaml +0 -13
  23. package/templates/agent-instructions.md +22 -6
  24. package/templates/agent-rules/capabilities/bundle-system.md +1 -1
  25. package/templates/agent-rules/project-specific/architecture.md +1 -1
  26. package/web/dist/assets/index-B2OdTzzI.css +1 -0
  27. package/web/dist/assets/index-D0tBsKiR.js +2016 -0
  28. package/web/dist/index.html +2 -2
  29. package/mcp-config-example.json +0 -9
  30. package/web/dist/assets/heuristics-manager-browser-DfonOP5I.js +0 -1
  31. package/web/dist/assets/index-dF3qg-y_.js +0 -2486
  32. package/web/dist/assets/index-h5FGSg_P.css +0 -1
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Bundle Manager for cntx-ui
3
- * Handles bundle generation, XML creation, and file organization
3
+ * Handles Smart Dynamic Bundles and traditional XML generation
4
4
  */
5
5
 
6
6
  import { readFileSync, statSync } from 'fs';
@@ -10,462 +10,227 @@ export default class BundleManager {
10
10
  constructor(configManager, fileSystemManager, verbose = false) {
11
11
  this.configManager = configManager;
12
12
  this.fileSystemManager = fileSystemManager;
13
+ this.db = configManager.dbManager;
13
14
  this.verbose = verbose;
14
15
  this._isScanning = false;
15
16
  }
16
17
 
17
- // === Bundle Generation ===
18
-
19
- async generateAllBundles() {
20
- this._isScanning = true;
21
-
22
- try {
23
- const bundles = this.configManager.getBundles();
24
-
25
- const totalBundles = bundles.size;
26
- let processedBundles = 0;
27
-
28
- for (const [name] of bundles) {
29
- if (this.verbose) {
30
- processedBundles++;
31
- const progress = `📊 Generating bundles: ${processedBundles}/${totalBundles} (${name})`;
32
- process.stdout.write(`\r${progress.padEnd(80)}`); // Pad to clear previous longer messages
33
- }
34
- await this.generateBundle(name);
35
- }
36
-
37
- if (this.verbose) {
38
- process.stdout.write('\r' + ''.padEnd(80) + '\r'); // Clear the line
39
- }
40
- this.configManager.saveBundleStates();
41
- } finally {
42
- this._isScanning = false;
43
- }
44
- }
45
-
46
- async generateBundle(name) {
47
- const bundles = this.configManager.getBundles();
48
- const bundle = bundles.get(name);
49
-
50
- if (!bundle) {
51
- throw new Error(`Bundle "${name}" not found`);
52
- }
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
+ }));
53
32
 
54
- // Only regenerate from patterns if no manual files exist
55
- // This preserves manual file management while allowing pattern-based initialization
56
- if (!bundle.files || bundle.files.length === 0) {
57
- // Get all files matching bundle patterns
58
- const allFiles = this.fileSystemManager.getAllFiles();
59
- const bundleFiles = allFiles.filter(file =>
60
- bundle.patterns.some(pattern =>
61
- this.fileSystemManager.matchesPattern(file, pattern)
62
- )
63
- );
33
+ if (this.verbose) console.log(`📦 Found ${manualBundles.length} manual bundles`);
64
34
 
65
- // Convert to relative paths for storage (portable across environments)
66
- bundle.files = bundleFiles.map(file => this.fileSystemManager.relativePath(file));
67
- }
68
- // If bundle.files already exists, preserve manual file management
35
+ const smartBundles = this.generateSmartBundleDefinitions();
36
+ if (this.verbose) console.log(`📦 Found ${smartBundles.length} smart bundle definitions`);
69
37
 
70
- // Ensure bundle.files always contains relative paths for storage consistency
71
- bundle.files = bundle.files.map(file =>
72
- file.startsWith('/') ? this.fileSystemManager.relativePath(file) : file
73
- );
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);
74
47
 
75
- // Convert relative paths to absolute for XML generation
76
- const absoluteFiles = bundle.files.map(file =>
77
- this.fileSystemManager.absolutePath(file)
78
- );
79
- bundle.content = this.generateBundleXML(name, absoluteFiles);
80
- bundle.size = Buffer.byteLength(bundle.content, 'utf8');
81
- bundle.generated = new Date().toISOString();
82
- bundle.changed = false;
48
+ if (this.verbose) console.log(`📦 Active smart bundles: ${activeSmartBundles.length}`);
83
49
 
84
- return bundle;
50
+ return [...manualBundles, ...activeSmartBundles];
85
51
  }
86
52
 
87
- generateBundleXML(bundleName, files) {
88
- const projectInfo = this.getProjectInfo();
89
- const categorizedFiles = this.categorizeFiles(files);
90
- const entryPoints = this.identifyEntryPoints(files);
91
- const bundlePurpose = this.getBundlePurpose(bundleName);
92
-
93
- let xml = `<?xml version="1.0" encoding="UTF-8"?>
94
- <codebase>
95
- <project_info>
96
- <name>${this.escapeXml(projectInfo.name)}</name>
97
- <bundle_name>${this.escapeXml(bundleName)}</bundle_name>
98
- <bundle_purpose>${this.escapeXml(bundlePurpose)}</bundle_purpose>
99
- <total_files>${files.length}</total_files>
100
- <generated_at>${new Date().toISOString()}</generated_at>
101
- </project_info>
102
-
103
- <overview>
104
- <description>
105
- This bundle contains ${files.length} files organized into different categories.
106
- ${bundlePurpose}
107
- </description>
108
- <entry_points>
109
- ${entryPoints.map(file => ` <file>${this.escapeXml(file)}</file>`).join('\n')}
110
- </entry_points>
111
- </overview>
112
-
113
- <file_structure>`;
114
-
115
- // Add categorized files
116
- Object.entries(categorizedFiles).forEach(([category, categoryFiles]) => {
117
- if (categoryFiles.length > 0) {
118
- xml += `
119
- <group name="${category}" description="${this.getTypeDescription(category)}">`;
120
-
121
- categoryFiles.forEach(file => {
122
- xml += `
123
- ${this.generateFileXML(file)}`;
53
+ /**
54
+ * Generate Smart Bundle definitions from indexed semantic data
55
+ */
56
+ generateSmartBundleDefinitions() {
57
+ const smartBundles = [];
58
+ try {
59
+ // 1. Group by Purpose (Heuristics)
60
+ const purposeRows = this.db.db.prepare('SELECT DISTINCT purpose, COUNT(*) as count FROM semantic_chunks GROUP BY purpose').all();
61
+ purposeRows.forEach(row => {
62
+ if (!row.purpose) return;
63
+ const name = `smart:${row.purpose.toLowerCase().replace(/\s+/g, '-')}`;
64
+ smartBundles.push({
65
+ name,
66
+ purpose: row.purpose,
67
+ fileCount: row.count,
68
+ type: 'smart',
69
+ description: `Automatically grouped by purpose: ${row.purpose}`
124
70
  });
71
+ });
125
72
 
126
- xml += `
127
- </group>`;
128
- }
129
- });
130
-
131
- xml += `
132
- </file_structure>
133
- </codebase>`;
134
-
135
- return xml;
136
- }
137
-
138
- // === File Organization ===
139
-
140
- categorizeFiles(files) {
141
- const categories = {
142
- 'entry_points': [],
143
- 'components': [],
144
- 'hooks': [],
145
- 'utilities': [],
146
- 'types': [],
147
- 'styles': [],
148
- 'tests': [],
149
- 'configuration': [],
150
- 'documentation': [],
151
- 'other': []
152
- };
153
-
154
- files.forEach(file => {
155
- const ext = extname(file).toLowerCase();
156
- const fileName = basename(file).toLowerCase();
157
- const filePath = file.toLowerCase();
158
-
159
- // Entry points
160
- if (fileName.match(/^(main|index|app)\.(js|jsx|ts|tsx)$/)) {
161
- categories.entry_points.push(file);
162
- }
163
- // Components
164
- else if (ext.match(/\.(jsx|tsx|vue)$/) || filePath.includes('/components/')) {
165
- categories.components.push(file);
166
- }
167
- // Hooks
168
- else if (filePath.includes('/hooks/') || fileName.startsWith('use') && ext.match(/\.(js|ts)$/)) {
169
- categories.hooks.push(file);
170
- }
171
- // Utilities
172
- else if (filePath.includes('/utils/') || filePath.includes('/helpers/') || filePath.includes('/lib/')) {
173
- categories.utilities.push(file);
174
- }
175
- // Types
176
- else if (fileName.includes('.d.ts') || filePath.includes('/types/') || fileName.includes('types')) {
177
- categories.types.push(file);
178
- }
179
- // Styles
180
- else if (ext.match(/\.(css|scss|sass|less|styl)$/)) {
181
- categories.styles.push(file);
182
- }
183
- // Tests
184
- else if (fileName.includes('.test.') || fileName.includes('.spec.') || filePath.includes('/test/') || filePath.includes('/__tests__/')) {
185
- categories.tests.push(file);
186
- }
187
- // Configuration
188
- else if (ext.match(/\.(json|yaml|yml|toml|ini)$/) || fileName.includes('config')) {
189
- categories.configuration.push(file);
190
- }
191
- // Documentation
192
- else if (ext.match(/\.(md|txt|rst)$/)) {
193
- categories.documentation.push(file);
194
- }
195
- // Other
196
- else {
197
- categories.other.push(file);
198
- }
199
- });
200
-
201
- return categories;
73
+ // 2. Group by Component Types (Subtypes)
74
+ const subtypeRows = this.db.db.prepare('SELECT DISTINCT subtype, COUNT(*) as count FROM semantic_chunks GROUP BY subtype').all();
75
+ subtypeRows.forEach(row => {
76
+ if (!row.subtype) return;
77
+ const name = `smart:type-${row.subtype.toLowerCase().replace(/_/g, '-')}`;
78
+ smartBundles.push({
79
+ name,
80
+ purpose: row.subtype,
81
+ fileCount: row.count,
82
+ type: 'smart',
83
+ description: `All ${row.subtype} elements across the codebase`
84
+ });
85
+ });
86
+ } catch (e) {
87
+ if (this.verbose) console.warn('Smart bundle discovery failed:', e.message);
88
+ }
89
+ return smartBundles;
202
90
  }
203
91
 
204
- identifyEntryPoints(files) {
205
- const entryPoints = [];
206
- const entryPatterns = [
207
- /^(main|index|app)\.(js|jsx|ts|tsx)$/i,
208
- /^server\.(js|ts)$/i,
209
- /^app\.(js|jsx|ts|tsx)$/i
210
- ];
92
+ /**
93
+ * Resolve files for a bundle (Manual or Smart)
94
+ */
95
+ async resolveBundleFiles(bundleName) {
96
+ if (bundleName.startsWith('smart:')) {
97
+ return this.resolveSmartBundle(bundleName);
98
+ }
211
99
 
212
- files.forEach(file => {
213
- const fileName = basename(file);
214
- if (entryPatterns.some(pattern => pattern.test(fileName))) {
215
- entryPoints.push(file);
100
+ const bundle = this.configManager.getBundles().get(bundleName);
101
+ if (!bundle) return [];
102
+
103
+ const allFiles = this.fileSystemManager.getAllFiles();
104
+ return allFiles.filter(file =>
105
+ bundle.patterns.some(pattern => this.fileSystemManager.matchesPattern(file, pattern))
106
+ ).map(f => this.fileSystemManager.relativePath(f));
107
+ }
108
+
109
+ /**
110
+ * Resolve a Smart Bundle query against SQLite
111
+ */
112
+ resolveSmartBundle(bundleName) {
113
+ const query = bundleName.replace('smart:', '');
114
+ let rows = [];
115
+
116
+ if (query.startsWith('type-')) {
117
+ const type = query.replace('type-', '').replace(/-/g, '_');
118
+ rows = this.db.db.prepare('SELECT DISTINCT file_path FROM semantic_chunks WHERE LOWER(subtype) = ?').all(type);
119
+ } else {
120
+ const purposeRows = this.db.db.prepare('SELECT DISTINCT purpose FROM semantic_chunks').all();
121
+ const matched = purposeRows.find(r => r.purpose?.toLowerCase().replace(/\s+/g, '-') === query);
122
+ if (matched) {
123
+ rows = this.db.db.prepare('SELECT DISTINCT file_path FROM semantic_chunks WHERE purpose = ?').all(matched.purpose);
216
124
  }
217
- });
218
-
219
- return entryPoints;
220
- }
221
-
222
- getBundlePurpose(bundleName) {
223
- const purposes = {
224
- 'master': 'Complete codebase overview with all project files',
225
- 'frontend': 'User interface components, styling, and client-side logic',
226
- 'backend': 'Server-side logic, API endpoints, and business logic',
227
- 'components': 'Reusable UI components and their associated styles',
228
- 'utilities': 'Shared utility functions and helper modules',
229
- 'configuration': 'Project configuration files and environment setup',
230
- 'tests': 'Test files and testing utilities',
231
- 'documentation': 'Project documentation and README files',
232
- 'types': 'TypeScript type definitions and interfaces',
233
- 'styles': 'CSS, SCSS, and other styling files'
234
- };
235
-
236
- return purposes[bundleName] || `Files matching the ${bundleName} bundle patterns`;
237
- }
238
-
239
- getTypeDescription(type) {
240
- const descriptions = {
241
- 'entry_points': 'Main application entry points and bootstrap files',
242
- 'components': 'Reusable UI components and their implementations',
243
- 'hooks': 'Custom React hooks and composable functions',
244
- 'utilities': 'Shared utility functions and helper modules',
245
- 'types': 'TypeScript type definitions and interfaces',
246
- 'styles': 'CSS, SCSS, and other styling files',
247
- 'tests': 'Test files and testing utilities',
248
- 'configuration': 'Configuration files and environment setup',
249
- 'documentation': 'Documentation, README files, and guides',
250
- 'other': 'Other project files not fitting specific categories'
251
- };
252
-
253
- return descriptions[type] || 'Project files';
125
+ }
126
+ return rows.map(r => r.file_path);
254
127
  }
255
128
 
256
- // === File Processing ===
129
+ // === Bundle Generation ===
257
130
 
258
- generateFileXML(file) {
131
+ async generateAllBundles() {
132
+ this._isScanning = true;
259
133
  try {
260
- const stats = this.getFileStats(file);
261
- const content = readFileSync(file, 'utf8');
262
- const role = this.getFileRole(file);
263
- const relativePath = relative(this.configManager.CWD, file);
264
-
265
- return `<file path="${this.escapeXml(relativePath)}" role="${this.escapeXml(role)}" size="${stats.size}" modified="${stats.mtime.toISOString()}">
266
- <![CDATA[${content}]]>
267
- </file>`;
268
- } catch (error) {
269
- const relativePath = relative(this.configManager.CWD, file);
270
- return `<file path="${this.escapeXml(relativePath)}" role="error" error="${this.escapeXml(error.message)}">
271
- <!-- File could not be read -->
272
- </file>`;
134
+ const bundles = this.configManager.getBundles();
135
+ for (const [name] of bundles) {
136
+ await this.regenerateBundle(name);
137
+ }
138
+ } finally {
139
+ this._isScanning = false;
273
140
  }
274
141
  }
275
142
 
276
- getFileRole(file) {
277
- const fileName = basename(file).toLowerCase();
278
- const filePath = file.toLowerCase();
279
- const ext = extname(file).toLowerCase();
143
+ async regenerateBundle(bundleName) {
144
+ if (this.verbose) console.log(`🔄 Regenerating bundle: ${bundleName}`);
145
+
146
+ const files = await this.resolveBundleFiles(bundleName);
147
+ const content = await this.generateBundleXML(bundleName, files);
148
+
149
+ const bundleData = {
150
+ files,
151
+ content,
152
+ size: Buffer.byteLength(content, 'utf8'),
153
+ generated: new Date().toISOString(),
154
+ changed: false
155
+ };
280
156
 
281
- if (fileName.match(/^(main|index|app)\.(js|jsx|ts|tsx)$/)) {
282
- return 'entry_point';
283
- }
284
- if (fileName.includes('config') || fileName.includes('setup')) {
285
- return 'configuration';
286
- }
287
- if (fileName.includes('readme') || ext === '.md') {
288
- return 'documentation';
157
+ if (!bundleName.startsWith('smart:')) {
158
+ const existing = this.configManager.getBundles().get(bundleName);
159
+ this.configManager.getBundles().set(bundleName, { ...existing, ...bundleData });
160
+ this.configManager.saveBundleStates();
289
161
  }
290
162
 
291
- return 'implementation';
292
- }
293
-
294
- getFileStats(filePath) {
295
- try {
296
- const stats = statSync(filePath);
297
- return {
298
- size: stats.size,
299
- mtime: stats.mtime,
300
- ctime: stats.ctime
301
- };
302
- } catch (error) {
303
- return {
304
- size: 0,
305
- mtime: new Date(0),
306
- ctime: new Date(0)
307
- };
308
- }
163
+ return bundleData;
309
164
  }
310
165
 
311
- escapeXml(text) {
312
- if (typeof text !== 'string') {
313
- text = String(text);
166
+ async generateBundleXML(bundleName, relativeFiles) {
167
+ const projectInfo = this.getProjectInfo();
168
+ let xml = `<?xml version="1.0" encoding="UTF-8"?>\n<codebase>\n`;
169
+ xml += ` <project_info>\n <name>${this.escapeXml(projectInfo.name)}</name>\n <bundle>${this.escapeXml(bundleName)}</bundle>\n </project_info>\n`;
170
+
171
+ for (const relPath of relativeFiles) {
172
+ xml += await this.generateFileXML(relPath);
314
173
  }
315
- return text
316
- .replace(/&/g, '&amp;')
317
- .replace(/</g, '&lt;')
318
- .replace(/>/g, '&gt;')
319
- .replace(/"/g, '&quot;')
320
- .replace(/'/g, '&apos;');
321
- }
322
-
323
- // === Bundle State Management ===
324
-
325
- markBundlesChanged(filename) {
326
- const bundles = this.configManager.getBundles();
327
-
328
- bundles.forEach((bundle, name) => {
329
- const matchesBundle = bundle.patterns.some(pattern =>
330
- this.fileSystemManager.matchesPattern(filename, pattern)
331
- );
332
-
333
- if (matchesBundle) {
334
- bundle.changed = true;
335
- }
336
- });
174
+
175
+ xml += `</codebase>`;
176
+ return xml;
337
177
  }
338
178
 
339
- getFileListWithVisibility(bundleName) {
179
+ async generateFileXML(relativePath) {
340
180
  try {
341
- const allFiles = this.fileSystemManager.getAllFiles();
342
- const bundle = this.configManager.getBundles().get(bundleName);
343
-
344
- if (!bundle) {
345
- return [];
181
+ const fullPath = this.fileSystemManager.fullPath(relativePath);
182
+ const content = readFileSync(fullPath, 'utf8');
183
+ const chunks = this.db.getChunksByFile(relativePath);
184
+
185
+ let xml = ` <file path="${this.escapeXml(relativePath)}">\n`;
186
+ if (chunks.length > 0) {
187
+ xml += ` <semantic_context>\n`;
188
+ chunks.forEach(c => {
189
+ xml += ` <chunk name="${this.escapeXml(c.name)}" purpose="${this.escapeXml(c.purpose)}" complexity="${c.complexity?.score || 0}" />\n`;
190
+ });
191
+ xml += ` </semantic_context>\n`;
346
192
  }
347
-
348
- return allFiles.map(file => {
349
- const matchesBundle = bundle.patterns.some(pattern =>
350
- this.fileSystemManager.matchesPattern(file, pattern)
351
- );
352
-
353
- const isHidden = this.configManager.isFileHidden(file, bundleName);
354
- const relativePath = relative(this.configManager.CWD, file);
355
-
356
- return {
357
- path: relativePath,
358
- fullPath: file,
359
- included: matchesBundle && !isHidden,
360
- hidden: isHidden,
361
- matchesPattern: matchesBundle
362
- };
363
- });
364
- } catch (error) {
365
- console.error('Failed to get file list with visibility:', error.message);
366
- return [];
193
+ xml += ` <content><![CDATA[${content}]]></content>\n </file>\n`;
194
+ return xml;
195
+ } catch (e) {
196
+ return ` <file path="${this.escapeXml(relativePath)}" error="${this.escapeXml(e.message)}" />\n`;
367
197
  }
368
198
  }
369
199
 
370
- // === Project Information ===
371
-
372
200
  getProjectInfo() {
373
201
  try {
374
- const packagePath = this.configManager.CWD + '/package.json';
375
- const pkg = JSON.parse(readFileSync(packagePath, 'utf8'));
376
- return {
377
- name: pkg.name || 'Unknown Project',
378
- version: pkg.version || '1.0.0',
379
- description: pkg.description || 'No description available'
380
- };
381
- } catch (error) {
382
- return {
383
- name: 'Unknown Project',
384
- version: '1.0.0',
385
- description: 'No description available'
386
- };
387
- }
388
- }
389
-
390
- // === Bundle Operations ===
391
-
392
- async regenerateBundle(bundleName) {
393
- // Notify WebSocket clients that sync has started
394
- if (this.webSocketManager) {
395
- this.webSocketManager.onBundleSyncStarted(bundleName);
396
- }
397
-
398
- try {
399
- const bundle = await this.generateBundle(bundleName);
400
- this.configManager.saveBundleStates();
401
-
402
- // Notify WebSocket clients that sync completed successfully
403
- if (this.webSocketManager) {
404
- this.webSocketManager.onBundleSyncCompleted(bundleName);
405
- }
406
-
407
- return bundle;
408
- } catch (error) {
409
- // Notify WebSocket clients that sync failed
410
- if (this.webSocketManager) {
411
- this.webSocketManager.onBundleSyncFailed(bundleName, error);
412
- }
413
- throw error;
202
+ const pkg = JSON.parse(readFileSync(this.fileSystemManager.fullPath('package.json'), 'utf8'));
203
+ return { name: pkg.name || 'Unknown', version: pkg.version || '1.0.0' };
204
+ } catch {
205
+ return { name: 'Unknown', version: '1.0.0' };
414
206
  }
415
207
  }
416
208
 
417
- async regenerateChangedBundles() {
418
- const bundles = this.configManager.getBundles();
419
- const changedBundles = [];
420
-
421
- for (const [name, bundle] of bundles) {
422
- if (bundle.changed) {
423
- await this.generateBundle(name);
424
- changedBundles.push(name);
425
- }
426
- }
427
-
428
- if (changedBundles.length > 0) {
429
- this.configManager.saveBundleStates();
430
- }
431
-
432
- return changedBundles;
209
+ escapeXml(text) {
210
+ return String(text).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&apos;');
433
211
  }
434
212
 
435
213
  getBundleContent(bundleName) {
214
+ if (bundleName.startsWith('smart:')) {
215
+ // For smart bundles, we generate on the fly if not cached
216
+ return this.regenerateBundle(bundleName).then(data => data.content);
217
+ }
436
218
  const bundle = this.configManager.getBundles().get(bundleName);
437
219
  return bundle ? bundle.content : null;
438
220
  }
439
221
 
440
- getBundleInfo(bundleName) {
441
- const bundle = this.configManager.getBundles().get(bundleName);
442
- if (!bundle) return null;
443
-
444
- return {
445
- name: bundleName,
446
- fileCount: bundle.files.length,
447
- size: bundle.size,
448
- generated: bundle.generated,
449
- changed: bundle.changed,
450
- patterns: bundle.patterns
451
- };
452
- }
453
-
454
- getAllBundleInfo() {
455
- const bundles = this.configManager.getBundles();
456
- return Array.from(bundles.entries()).map(([name, bundle]) => ({
457
- name,
458
- fileCount: bundle.files.length,
459
- size: bundle.size,
460
- generated: bundle.generated,
461
- changed: bundle.changed,
462
- patterns: bundle.patterns
463
- }));
222
+ markBundlesChanged(filename) {
223
+ this.configManager.getBundles().forEach((bundle, name) => {
224
+ if (bundle.patterns?.some(p => this.fileSystemManager.matchesPattern(filename, p))) {
225
+ bundle.changed = true;
226
+ }
227
+ });
464
228
  }
465
229
 
466
- // === Getters ===
467
-
468
- get isScanning() {
469
- return this._isScanning;
230
+ getBundleInfo(bundleName) {
231
+ if (bundleName.startsWith('smart:')) {
232
+ return this.generateSmartBundleDefinitions().find(b => b.name === bundleName);
233
+ }
234
+ return this.configManager.getBundles().get(bundleName);
470
235
  }
471
236
  }